feat: upload require auth and send auth headers on upload APIs

This commit is contained in:
junchi.zhang
2026-04-26 01:45:12 +08:00
parent 4cdcc288d5
commit a948860e15
9 changed files with 135 additions and 56 deletions
+48 -12
View File
@@ -14,7 +14,7 @@
# PinMe
[PinMe](https://pinme.eth.limo/) is a zero-config frontend deployment tool.
No servers. No accounts. No setup.
No servers to manage. Minimal setup.
Build a static site, generate a page with AI, export your frontend, or import CAR files — then deploy instantly with a single command.
@@ -55,14 +55,23 @@ Website: [https://pinme.eth.limo/](https://pinme.eth.limo/)
node --version
```
### Three Steps to Deploy
### Four Steps to Deploy
1. **Install PinMe**
```bash
npm install -g pinme
```
2. **Build your project** (if not already built)
2. **Login or set AppKey**
```bash
# Recommended: browser login
pinme login
# Or use an AppKey in CLI/CI environments
pinme set-appkey <AppKey>
```
3. **Build your project** (if not already built)
```bash
# For Vite/React/Vue projects
npm run build
@@ -71,7 +80,7 @@ npm run build
npm run build
```
3. **Upload to PinMe**
4. **Upload to PinMe**
```bash
# Upload the dist directory (most common)
pinme upload dist
@@ -105,7 +114,16 @@ This section provides AI-specific instructions for deploying websites using PinM
npm install -g pinme
```
3. **Identify the Static File Directory**
3. **Authenticate before upload**
```bash
# Recommended
pinme login
# Or for automation / non-browser environments
pinme set-appkey <AppKey>
```
4. **Identify the Static File Directory**
Priority order to check:
- `dist/` - Default output directory for Vue/React/Vite projects
- `build/` - Output directory for Create React App
@@ -125,12 +143,12 @@ This section provides AI-specific instructions for deploying websites using PinM
- For React: use `HashRouter` instead of `BrowserRouter`
- For Vue: use `createHashHistory()` instead of `createWebHistory()`
4. **Execute Deployment Command**
5. **Execute Deployment Command**
```bash
pinme upload <folder_path>
```
5. **Return Result**
6. **Return Result**
- Only return the final URL printed by PinMe
- URL priority is: DNS domain > Pinme subdomain > short URL > hash preview URL
- Hash fallback format: `https://pinme.eth.limo/#/preview/*`
@@ -154,6 +172,9 @@ For easy AI tool parsing:
"node_version": ">=16.13.0"
},
"install": "npm install -g pinme",
"login": "pinme login",
"set_appkey": "pinme set-appkey <AppKey>",
"auth_required_for_upload": true,
"upload": "pinme upload {{directory}}",
"upload_with_domain": "pinme upload {{directory}} --domain {{domain}}",
"upload_with_dns": "pinme upload {{directory}} --domain {{domain}} --dns",
@@ -171,7 +192,6 @@ For easy AI tool parsing:
"dns_domain_format": "https://<your-domain>",
"other_commands": {
"version": "pinme --version",
"login": "pinme login",
"list": "pinme list",
"import": "pinme import <car-file>",
"export": "pinme export <cid>",
@@ -201,9 +221,10 @@ For easy AI tool parsing:
> **Operation Steps:**
> 1. Check Node.js version (requires 16.13.0+)
> 2. Check if pinme is installed, install if not
> 3. Identify the static file directory for the website to deploy
> 4. Execute deployment command
> 5. Return the final public URL printed by PinMe. If no custom URL is available, return the preview page link: `https://pinme.eth.limo/#/preview/*`
> 3. Authenticate with `pinme login` or `pinme set-appkey <AppKey>`
> 4. Identify the static file directory for the website to deploy
> 5. Execute deployment command
> 6. Return the final public URL printed by PinMe. If no custom URL is available, return the preview page link: `https://pinme.eth.limo/#/preview/*`
---
@@ -235,6 +256,9 @@ pinme --version
### Upload Files or Directories
```bash
# Login is required before upload
pinme login
# Interactive upload
pinme upload
@@ -242,6 +266,8 @@ pinme upload
pinme upload /path/to/file-or-directory
```
**Authentication requirement:** `pinme upload` and `pinme import` require a valid login session or AppKey. Use `pinme login` for browser login, or `pinme set-appkey <AppKey>` for CLI/CI environments.
### Bind Domain (requires wallet balance)
```bash
@@ -311,6 +337,9 @@ pinme delete my-app --force
### Import CAR files
```bash
# Login is required before import
pinme login
# Interactive CAR import
pinme import
@@ -471,6 +500,7 @@ pinme upload ./dist --domain my-site --dns
- Use `--dns` or `-D` flag to force DNS domain mode when needed
**Requirements:**
- Login or AppKey authentication is required before upload/bind
- Sufficient wallet balance is required for domain binding
- Valid AppKey must be set (run: `pinme set-appkey <AppKey>`)
- For DNS domains, you must own the domain
@@ -495,6 +525,8 @@ pinme upload [path] [--domain <name>]
- `-d, --domain <name>`: Pinme subdomain or DNS domain to bind after upload (optional, requires wallet balance)
- `--dns`, `-D`: Force DNS domain mode
**Authentication:** This command requires login. Run `pinme login` first, or configure `pinme set-appkey <AppKey>`.
**Examples:**
```bash
# Upload dist directory
@@ -529,6 +561,8 @@ pinme import [path] [--domain <name>]
- `path`: Path to the CAR file to import (optional, if not provided, interactive mode will be entered)
- `-d, --domain <name>`: Pinme subdomain to bind after import (optional)
**Authentication:** This command requires login. Run `pinme login` first, or configure `pinme set-appkey <AppKey>`.
**Examples:**
```bash
# Interactive CAR import
@@ -816,7 +850,7 @@ Uploaded files are stored on the IPFS network and accessible through the Glitter
- Use a custom DNS domain: `https://<your-domain>`
### Login and Management
- Support user login via AppKey
- Support browser login and AppKey-based authentication
- View historical upload records
- Manage uploaded files
@@ -945,6 +979,8 @@ jobs:
- run: pinme upload dist --domain "${{ secrets.PINME_DOMAIN }}"
```
`pinme set-appkey` satisfies the authentication requirement for `pinme upload` in CI.
### Supported Build Tools
- **Vite**: Builds to `dist/`
+8 -3
View File
@@ -6,7 +6,6 @@ import fs from 'fs';
import CryptoJS from 'crypto-js';
import { checkDomainAvailable, bindPinmeDomain, getRootDomain } from './utils/pinmeApi';
import { getAuthConfig } from './utils/webLogin';
import { getDeviceId } from './utils/getDeviceId';
import { APP_CONFIG } from './utils/config';
import { uploadPath } from './services/uploadService';
// get from environment variables
@@ -64,13 +63,13 @@ function getDomainFromArgs(): string | null {
return null;
}
// Get uid: use address from auth if logged in, otherwise use deviceId
// Upload/import now requires login. Use the authenticated address as uid.
function getUid(): string {
const auth = getAuthConfig();
if (auth?.address) {
return auth.address;
}
return getDeviceId();
throw new Error('Please login first. Run: pinme login');
}
export default async (options?: ImportOptions): Promise<void> => {
@@ -85,6 +84,12 @@ export default async (options?: ImportOptions): Promise<void> => {
}),
);
const auth = getAuthConfig();
if (!auth) {
console.log(chalk.red('Please login first. Run: pinme login'));
return;
}
// if the parameter is passed, import directly, pinme import /path/to/dir
const argPath = process.argv[3];
const domainArg = getDomainFromArgs();
+7 -3
View File
@@ -1,5 +1,5 @@
import chalk from 'chalk';
import { getAuthConfig, WebLoginManager } from './utils/webLogin';
import { WebLoginManager } from './utils/webLogin';
import { getDeviceId } from './utils/getDeviceId';
import { bindAnonymousDevice } from './utils/pinmeApi';
@@ -9,7 +9,7 @@ export interface EnvOption {
const ENV_URLS: Record<string, string> = {
dev: 'http://localhost:5173',
test: "http://test-pinme.pinit.eth.limo",
test: 'http://test-pinme.pinit.eth.limo',
prod: 'https://pinme.eth.limo',
};
@@ -23,7 +23,11 @@ export default async function loginCmd(options: EnvOption = {}): Promise<void> {
webBaseUrl = ENV_URLS[env];
console.log(chalk.blue(`Using ${env} environment: ${webBaseUrl}`));
} else {
console.log(chalk.yellow(`Unknown environment: ${options.env}. Using default prod.`));
console.log(
chalk.yellow(
`Unknown environment: ${options.env}. Using default prod.`,
),
);
webBaseUrl = ENV_URLS.prod;
console.log(chalk.blue(`Using prod environment: ${webBaseUrl}`));
}
+7 -1
View File
@@ -3,6 +3,7 @@ import { getUid } from '../utils/getDeviceId';
import uploadToIpfsSplit from '../utils/uploadToIpfsSplit';
import { APP_CONFIG } from '../utils/config';
import { getRootDomain } from '../utils/pinmeApi';
import { getAuthConfig } from '../utils/webLogin';
export interface UploadServiceOptions {
importAsCar?: boolean;
@@ -111,10 +112,15 @@ export async function uploadPath(
targetPath: string,
options: UploadServiceOptions = {},
): Promise<UploadServiceResult> {
const authConfig = getAuthConfig();
if (!authConfig) {
throw new Error('Please login first. Run: pinme login');
}
const result = await uploadToIpfsSplit(targetPath, {
importAsCar: options.importAsCar,
projectName: options.projectName,
uid: options.uid,
uid: options.uid || authConfig.address,
});
if (!result?.contentHash) {
+6 -8
View File
@@ -140,17 +140,15 @@ export default async (options?: UploadOptions): Promise<void> => {
}),
);
const authConfig = getAuthConfig();
if (!authConfig) {
console.log(chalk.red('Please login first. Run: pinme login'));
return;
}
// Check if domain/dns options are provided
const domainArg = getDomainFromArgs();
const dnsArg = getDnsFromArgs();
const needsAuth = !!domainArg || dnsArg;
// Check auth only when domain/dns options are provided
const authConfig = getAuthConfig();
if (needsAuth && !authConfig) {
console.log(chalk.red('Please login first. Run: pinme set-appkey <AppKey>'));
return;
}
// if the parameter is passed, upload directly, pinme upload /path/to/dir
const argPath = process.argv[3];
+15 -6
View File
@@ -84,6 +84,10 @@ interface UploadExecutionOptions {
uid?: string;
}
function getUploadAuthHeaders(): Record<string, string> {
return getAuthHeaders();
}
function extractAxiosErrorMessage(error: any): string {
const responseData = error?.response?.data;
@@ -334,7 +338,7 @@ async function initChunkSession(
try {
const projectName = options.projectName?.trim();
let authHeaders: Record<string, string> = {};
const authHeaders = getUploadAuthHeaders();
const requestBody: any = {
file_name: fileName,
file_size: fileSize,
@@ -345,7 +349,6 @@ async function initChunkSession(
if (projectName) {
requestBody.project_name = projectName;
authHeaders = getAuthHeaders();
}
const response = await axios.post<ChunkSessionResponse>(
@@ -400,7 +403,10 @@ async function uploadChunkWithAbort(
`${IPFS_API_URL}/chunk/upload`,
form,
{
headers: { ...form.getHeaders() },
headers: {
...form.getHeaders(),
...getUploadAuthHeaders(),
},
timeout: TIMEOUT,
signal,
},
@@ -554,13 +560,12 @@ async function completeChunkUpload(
try {
const requestBody: any = { session_id: sessionId, uid: deviceId };
const projectName = options.projectName?.trim();
let authHeaders: Record<string, string> = {};
const authHeaders = getUploadAuthHeaders();
if (options.importAsCar) {
requestBody.import_as_car = true;
}
if (projectName) {
requestBody.project_name = projectName;
authHeaders = getAuthHeaders();
}
const response = await axios.post<ChunkCompleteResponse>(
`${IPFS_API_URL}/chunk/complete`,
@@ -595,6 +600,7 @@ async function getChunkStatus(
): Promise<ChunkStatusResponse['data']> {
try {
const projectName = options.projectName?.trim();
const authHeaders = getUploadAuthHeaders();
const queryParams = new URLSearchParams({
trace_id: sessionId,
uid: deviceId,
@@ -607,7 +613,10 @@ async function getChunkStatus(
`${IPFS_API_URL}/up_status?${queryParams.toString()}`,
{
timeout: TIMEOUT,
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
...authHeaders,
},
},
);
+28 -15
View File
@@ -1,10 +1,10 @@
# PinMe — llms.txt
Last updated: 2026-04-23
Last updated: 2026-04-26
Canonical repo: https://github.com/glitternetwork/pinme
Website: https://pinme.eth.limo/
## Purpose
PinMe is a zero-config frontend deployment tool: no servers, no accounts, no setup.
PinMe is a zero-config frontend deployment tool: no servers to manage, minimal setup.
It publishes static sites as verifiable content (content-addressed), making silent tampering and accidental breakage harder than traditional hosting.
This file is written for LLMs/AI agents to:
@@ -38,11 +38,15 @@ Prerequisite: Node.js >= 16.13.0
1) Install:
npm install -g pinme
2) Build the project (if not already built):
2) Authenticate:
pinme login
# or: pinme set-appkey <AppKey>
3) Build the project (if not already built):
npm install
npm run build
3) Upload the build output:
4) Upload the build output:
pinme upload dist
# or: pinme upload build
# or: pinme upload out
@@ -63,12 +67,18 @@ Goal: Upload static files to PinMe and return ONLY the final public URL printed
- If missing:
npm install -g pinme
2) Always ensure a static build exists:
2) Authenticate before upload:
- Preferred:
pinme login
- For automation/CI:
pinme set-appkey <AppKey>
3) Always ensure a static build exists:
- If the repo looks like a Node frontend project:
npm install
npm run build
3) Identify the static output directory (priority order):
4) Identify the static output directory (priority order):
1) dist/ (Vite/React/Vue/Astro common)
2) build/ (Create React App common)
3) out/ (Next.js static export common)
@@ -82,10 +92,10 @@ Validation rules:
If no suitable folder is found:
- Ask the user to specify the correct build output path.
4) Deploy:
5) Deploy:
pinme upload <folder_path>
5) Return result:
6) Return result:
- Return ONLY the final URL printed by PinMe.
- URL priority is:
1) DNS domain
@@ -107,6 +117,9 @@ If no suitable folder is found:
"node_version": ">=16.13.0"
},
"install": "npm install -g pinme",
"login": "pinme login",
"set_appkey": "pinme set-appkey <AppKey>",
"auth_required_for_upload": true,
"upload": "pinme upload {{directory}}",
"upload_with_domain": "pinme upload {{directory}} --domain {{domain}}",
"upload_with_dns": "pinme upload {{directory}} --domain {{domain}} --dns",
@@ -124,7 +137,6 @@ If no suitable folder is found:
"dns_domain_format": "https://<your-domain>",
"other_commands": {
"version": "pinme --version",
"login": "pinme login",
"list": "pinme list",
"import": "pinme import <car-file>",
"export": "pinme export <cid>",
@@ -146,7 +158,7 @@ If no suitable folder is found:
## CLI reference (high-level)
- pinme upload <dir> [--domain <name>]
Upload a static directory. Optionally bind to a Pinme subdomain or DNS domain.
Upload a static directory. Login or AppKey authentication is required. Optionally bind to a Pinme subdomain or DNS domain.
- pinme upload <dir> --domain <domain> --dns
Force DNS domain mode. Otherwise domains containing a dot are auto-detected as DNS domains.
@@ -158,7 +170,7 @@ If no suitable folder is found:
Show upload history (supports limiting and clearing per README).
- pinme import [path] [--domain <name>]
Import a CAR file (interactive if no path). Optionally bind to a domain.
Import a CAR file (interactive if no path). Login or AppKey authentication is required. Optionally bind to a domain.
- pinme export <CID> [--output <path>]
Export IPFS content as a CAR file (defaults to Downloads if no output path).
@@ -225,7 +237,8 @@ Deployment Request:
Steps:
1) node --version (must be >=16.13.0)
2) npm install -g pinme (if needed)
3) npm install && npm run build
4) Identify output folder (dist/build/out/public) containing index.html
5) pinme upload <folder>
6) Return ONLY the final URL printed by PinMe. If no custom URL is available, return the preview URL: https://pinme.eth.limo/#/preview/*
3) Authenticate with `pinme login` or `pinme set-appkey <AppKey>`
4) npm install && npm run build
5) Identify output folder (dist/build/out/public) containing index.html
6) pinme upload <folder>
7) Return ONLY the final URL printed by PinMe. If no custom URL is available, return the preview URL: https://pinme.eth.limo/#/preview/*
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "pinme",
"version": "2.0.2-beta.10",
"version": "2.0.2-beta.11",
"publishConfig": {
"access": "public"
},
+15 -7
View File
@@ -24,16 +24,18 @@ digraph pinme_decision {
## Path 1: Upload Files / Static Sites
> No login required.
> Login required. Use `pinme login` or `pinme set-appkey <AppKey>` before `pinme upload` or `pinme import`.
```dot
digraph upload_flow {
"Install/update pinme to latest" [shape=box];
"Authenticate" [shape=box];
"Determine build artifacts" [shape=box];
"pinme upload <path>" [shape=box];
"Return preview URL" [shape=doublecircle];
"Install/update pinme to latest" -> "Determine build artifacts";
"Install/update pinme to latest" -> "Authenticate";
"Authenticate" -> "Determine build artifacts";
"Determine build artifacts" -> "pinme upload <path>";
"pinme upload <path>" -> "Return preview URL";
}
@@ -46,19 +48,25 @@ LATEST=$(npm view pinme version)
[ "$LOCAL" != "$LATEST" ] && npm install -g pinme@latest || echo "pinme is up to date ($LOCAL)"
```
**2. Determine upload target** (priority order):
**2. Authenticate:**
```bash
pinme login
# or: pinme set-appkey <AppKey>
```
**3. Determine upload target** (priority order):
1. `dist/` — Vite / Vue / React
2. `build/` — Create React App
3. `out/` — Next.js static export
4. `public/` — Plain static files
**3. Upload:**
**4. Upload:**
```bash
pinme upload <path>
pinme upload ./dist --domain my-site # Optional: bind subdomain (VIP required)
pinme upload ./dist --domain my-site # Optional: bind subdomain (wallet balance required)
```
**4. Return** the preview URL (`https://pinme.eth.limo/#/preview/*`) to the user. Note: return the **full URL** including all hash characters — do not truncate.
**5. Return** the final URL printed by PinMe to the user. URL priority is: DNS domain > PinMe subdomain > short URL > preview URL. If it falls back to preview, return the **full URL** including all hash characters — do not truncate.
### Common Examples
@@ -68,7 +76,7 @@ pinme upload ./my-folder # Folder
pinme upload dist # Vite/Vue build artifacts
pinme upload build # CRA build artifacts
pinme upload out # Next.js static export
pinme upload ./dist --domain my-site # Bind PinMe subdomain (VIP required)
pinme upload ./dist --domain my-site # Bind PinMe subdomain (wallet balance required)
pinme import ./my-archive.car # Import CAR file
```