From a948860e15848bae4fd35d8d2675d270b74340f3 Mon Sep 17 00:00:00 2001 From: "junchi.zhang" <435900020@qq.com> Date: Sun, 26 Apr 2026 01:45:12 +0800 Subject: [PATCH] feat: upload require auth and send auth headers on upload APIs --- README.md | 60 +++++++++++++++++++++++++++------- bin/importCar.ts | 11 +++++-- bin/login.ts | 10 ++++-- bin/services/uploadService.ts | 8 ++++- bin/upload.ts | 14 ++++---- bin/utils/uploadToIpfsSplit.ts | 21 ++++++++---- llms.txt.md | 43 +++++++++++++++--------- package.json | 2 +- skills/pinme/SKILL.md | 22 +++++++++---- 9 files changed, 135 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index f1aaf3e..22343a3 100644 --- a/README.md +++ b/README.md @@ -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 +``` + +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 + ``` + +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 ``` -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 ", + "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://", "other_commands": { "version": "pinme --version", - "login": "pinme login", "list": "pinme list", "import": "pinme import ", "export": "pinme export ", @@ -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 ` +> 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 ` 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 `) - For DNS domains, you must own the domain @@ -495,6 +525,8 @@ pinme upload [path] [--domain ] - `-d, --domain `: 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 `. + **Examples:** ```bash # Upload dist directory @@ -529,6 +561,8 @@ pinme import [path] [--domain ] - `path`: Path to the CAR file to import (optional, if not provided, interactive mode will be entered) - `-d, --domain `: Pinme subdomain to bind after import (optional) +**Authentication:** This command requires login. Run `pinme login` first, or configure `pinme set-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://` ### 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/` diff --git a/bin/importCar.ts b/bin/importCar.ts index 3e69349..4dda8c1 100644 --- a/bin/importCar.ts +++ b/bin/importCar.ts @@ -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 => { @@ -85,6 +84,12 @@ export default async (options?: ImportOptions): Promise => { }), ); + 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(); diff --git a/bin/login.ts b/bin/login.ts index fa8b700..b6858f4 100644 --- a/bin/login.ts +++ b/bin/login.ts @@ -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 = { 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 { 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}`)); } diff --git a/bin/services/uploadService.ts b/bin/services/uploadService.ts index d854245..812a3e7 100644 --- a/bin/services/uploadService.ts +++ b/bin/services/uploadService.ts @@ -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 { + 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) { diff --git a/bin/upload.ts b/bin/upload.ts index cfea7a6..593065d 100644 --- a/bin/upload.ts +++ b/bin/upload.ts @@ -140,17 +140,15 @@ export default async (options?: UploadOptions): Promise => { }), ); + 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 ')); - return; - } // if the parameter is passed, upload directly, pinme upload /path/to/dir const argPath = process.argv[3]; diff --git a/bin/utils/uploadToIpfsSplit.ts b/bin/utils/uploadToIpfsSplit.ts index ce611b6..5823552 100644 --- a/bin/utils/uploadToIpfsSplit.ts +++ b/bin/utils/uploadToIpfsSplit.ts @@ -84,6 +84,10 @@ interface UploadExecutionOptions { uid?: string; } +function getUploadAuthHeaders(): Record { + 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 = {}; + 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( @@ -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 = {}; + const authHeaders = getUploadAuthHeaders(); if (options.importAsCar) { requestBody.import_as_car = true; } if (projectName) { requestBody.project_name = projectName; - authHeaders = getAuthHeaders(); } const response = await axios.post( `${IPFS_API_URL}/chunk/complete`, @@ -595,6 +600,7 @@ async function getChunkStatus( ): Promise { 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, + }, }, ); diff --git a/llms.txt.md b/llms.txt.md index 7c6fbf3..84e6137 100644 --- a/llms.txt.md +++ b/llms.txt.md @@ -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 + +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 + +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 -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 ", + "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://", "other_commands": { "version": "pinme --version", - "login": "pinme login", "list": "pinme list", "import": "pinme import ", "export": "pinme export ", @@ -146,7 +158,7 @@ If no suitable folder is found: ## CLI reference (high-level) - pinme upload [--domain ] - 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 --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 ] - 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 [--output ] 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 -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 ` +4) npm install && npm run build +5) Identify output folder (dist/build/out/public) containing index.html +6) pinme upload +7) Return ONLY the final URL printed by PinMe. If no custom URL is available, return the preview URL: https://pinme.eth.limo/#/preview/* diff --git a/package.json b/package.json index 5a53d86..e9b1163 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pinme", - "version": "2.0.2-beta.10", + "version": "2.0.2-beta.11", "publishConfig": { "access": "public" }, diff --git a/skills/pinme/SKILL.md b/skills/pinme/SKILL.md index a7d50ce..d86fc0d 100644 --- a/skills/pinme/SKILL.md +++ b/skills/pinme/SKILL.md @@ -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 ` 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 " [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 "; "pinme upload " -> "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 +``` + +**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 -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 ```