mirror of
https://github.com/glitternetwork/pinme.git
synced 2026-08-30 17:58:49 +08:00
feat: Version 1.2.0
This commit is contained in:
@@ -42,6 +42,10 @@ pinme upload
|
||||
|
||||
# Specify path directly
|
||||
pinme upload /path/to/file-or-directory
|
||||
|
||||
# Upload and bind to a domain
|
||||
pinme upload /path/to/file-or-directory --domain <name>
|
||||
pinme upload /path/to/file-or-directory -d <name>
|
||||
```
|
||||
|
||||
### Remove files from IPFS
|
||||
@@ -70,6 +74,26 @@ pinme list -l 5
|
||||
pinme list -c
|
||||
```
|
||||
|
||||
### Set AppKey for authentication
|
||||
|
||||
```bash
|
||||
# Interactive AppKey setup
|
||||
pinme set-appkey
|
||||
|
||||
# Set AppKey directly
|
||||
pinme set-appkey <AppKey>
|
||||
```
|
||||
|
||||
### View your domains
|
||||
|
||||
```bash
|
||||
# List all domains owned by current account
|
||||
pinme my-domains
|
||||
|
||||
# Or use the shorthand command
|
||||
pinme domain
|
||||
```
|
||||
|
||||
### Get help
|
||||
|
||||
```bash
|
||||
@@ -81,14 +105,15 @@ pinme help
|
||||
|
||||
### `upload`
|
||||
|
||||
Upload a file or directory to the IPFS network.
|
||||
Upload a file or directory to the IPFS network. Supports binding to a Pinme subdomain after upload.
|
||||
|
||||
```bash
|
||||
pinme upload [path]
|
||||
pinme upload [path] [--domain <name>]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `path`: Path to the file or directory to upload (optional, if not provided, interactive mode will be entered)
|
||||
- `-d, --domain <name>`: Pinme subdomain to bind after upload (optional)
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
@@ -100,6 +125,10 @@ pinme upload ./example.jpg
|
||||
|
||||
# Upload an entire directory
|
||||
pinme upload ./my-website
|
||||
|
||||
# Upload and bind to a domain
|
||||
pinme upload ./my-website --domain my-site
|
||||
pinme upload ./my-website -d my-site
|
||||
```
|
||||
|
||||
### `rm`
|
||||
@@ -149,6 +178,52 @@ pinme ls -l 5
|
||||
pinme list -c
|
||||
```
|
||||
|
||||
### `set-appkey`
|
||||
|
||||
Set AppKey for authentication and automatically merge anonymous upload history to the current account.
|
||||
|
||||
```bash
|
||||
pinme set-appkey [AppKey]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `AppKey`: Your AppKey for authentication (optional, if not provided, interactive mode will be entered)
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Interactive AppKey setup
|
||||
pinme set-appkey
|
||||
|
||||
# Set AppKey directly
|
||||
pinme set-appkey your-app-key-here
|
||||
```
|
||||
|
||||
**Note:** After setting the AppKey, your anonymous upload history will be automatically merged to your account.
|
||||
|
||||
### `my-domains` / `domain`
|
||||
|
||||
List all domains owned by the current account.
|
||||
|
||||
```bash
|
||||
pinme my-domains
|
||||
pinme domain
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# List all domains
|
||||
pinme my-domains
|
||||
|
||||
# Shorthand command
|
||||
pinme domain
|
||||
```
|
||||
|
||||
This command displays information about each domain including:
|
||||
- Domain name
|
||||
- Domain type
|
||||
- Bind time
|
||||
- Expire time
|
||||
|
||||
### `help`
|
||||
|
||||
Display help information.
|
||||
@@ -205,11 +280,125 @@ export default {
|
||||
}
|
||||
```
|
||||
|
||||
## GitHub Actions Integration
|
||||
|
||||
PinMe can be integrated with GitHub Actions to automatically deploy your project when you push code to GitHub. This enables a fully automated CI/CD workflow.
|
||||
|
||||
### Quick Setup
|
||||
|
||||
1. **Add the workflow file** to your repository:
|
||||
- Copy `.github/workflows/deploy.yml` from the PinMe repository to your project
|
||||
- Or create `.github/workflows/deploy.yml` in your repository
|
||||
|
||||
2. **Configure GitHub Secrets**:
|
||||
- Go to your repository → Settings → Secrets and variables → Actions
|
||||
- Add a new secret named `PINME_APPKEY` with your PinMe AppKey
|
||||
- (Optional) Add `PINME_DOMAIN` to specify a custom domain name
|
||||
|
||||
3. **Push to trigger deployment**:
|
||||
- Push code to `main` or `master` branch to trigger automatic deployment
|
||||
- Or manually trigger via Actions tab → "Deploy to PinMe" → Run workflow
|
||||
|
||||
### Workflow Features
|
||||
|
||||
The GitHub Actions workflow automatically:
|
||||
|
||||
- ✅ Detects and installs project dependencies
|
||||
- ✅ Builds your project (if a build script exists)
|
||||
- ✅ Installs PinMe CLI
|
||||
- ✅ Sets up authentication using your AppKey
|
||||
- ✅ Auto-detects build output directory (`dist`, `build`, `public`, or `out`)
|
||||
- ✅ Uploads to IPFS and binds to your domain
|
||||
- ✅ Provides deployment summary with access URL
|
||||
|
||||
### Configuration Options
|
||||
|
||||
#### Using GitHub Secrets
|
||||
|
||||
You can configure the following secrets in your repository:
|
||||
|
||||
- **`PINME_APPKEY`** (Required): Your PinMe AppKey for authentication
|
||||
- Format: `<address>-<jwt>`
|
||||
- Get your AppKey from [PinMe website](https://pinme.eth.limo/)
|
||||
|
||||
- **`PINME_DOMAIN`** (Optional): Default domain name to bind
|
||||
- If not set, the workflow will generate a domain from your repository name
|
||||
- Example: `my-awesome-project` → `https://my-awesome-project.pinit.eth.limo`
|
||||
|
||||
#### Manual Workflow Dispatch
|
||||
|
||||
You can also manually trigger the workflow with custom parameters:
|
||||
|
||||
1. Go to Actions tab in your repository
|
||||
2. Select "Deploy to PinMe" workflow
|
||||
3. Click "Run workflow"
|
||||
4. Enter:
|
||||
- **Domain**: Your desired PinMe domain name
|
||||
- **Build Directory**: Your build output directory (default: `dist`)
|
||||
|
||||
### Example Workflow
|
||||
|
||||
```yaml
|
||||
name: Deploy to PinMe
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, master]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
domain:
|
||||
description: 'PinMe domain name'
|
||||
required: true
|
||||
build_dir:
|
||||
description: 'Build directory'
|
||||
default: 'dist'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npm install -g pinme
|
||||
- run: pinme set-appkey "${{ secrets.PINME_APPKEY }}"
|
||||
- run: pinme upload dist --domain "${{ secrets.PINME_DOMAIN }}"
|
||||
```
|
||||
|
||||
### Supported Build Tools
|
||||
|
||||
The workflow automatically detects and supports:
|
||||
|
||||
- **Vite**: Builds to `dist/`
|
||||
- **Create React App**: Builds to `build/`
|
||||
- **Next.js**: Builds to `out/` (with `output: 'export'`)
|
||||
- **Vue CLI**: Builds to `dist/`
|
||||
- **Angular**: Builds to `dist/`
|
||||
- **Static sites**: Uses root directory or `public/`
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
**Build directory not found:**
|
||||
- Ensure your build script outputs to a standard directory (`dist`, `build`, `public`, or `out`)
|
||||
- Or set `PINME_DOMAIN` secret and use manual workflow dispatch to specify custom directory
|
||||
|
||||
**Authentication failed:**
|
||||
- Verify your `PINME_APPKEY` secret is correctly set
|
||||
- Ensure the AppKey format is correct: `<address>-<jwt>`
|
||||
|
||||
**Domain binding failed:**
|
||||
- Check if the domain name is available
|
||||
- Ensure you have permission to bind the domain
|
||||
- Try a different domain name
|
||||
|
||||
## Contact Us
|
||||
|
||||
If you have questions or suggestions, please contact us through:
|
||||
|
||||
- GitHub Issues: [https://github.com/glitternetwork/pinme/issues](https://github.com/glitternetwork/pinme/issue)
|
||||
- GitHub Issues: [https://github.com/glitternetwork/pinme/issues](https://github.com/glitternetwork/pinme/issues)
|
||||
- Email: [pinme@glitterprotocol.io](mailto:pinme@glitterprotocol.io)
|
||||
|
||||
---
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import upload from './utils/uploadToIpfsSplit';
|
||||
import { checkDomainAvailable, bindPinmeDomain } from './utils/pinmeApi';
|
||||
|
||||
interface Args {
|
||||
domain?: string;
|
||||
targetPath?: string;
|
||||
}
|
||||
|
||||
function parseArgs(): Args {
|
||||
// Usage: pinme bind <path> --domain <name>
|
||||
const args = process.argv.slice(2);
|
||||
const res: Args = {};
|
||||
const idx = args.indexOf('bind');
|
||||
if (idx >= 0) {
|
||||
const maybePath = args[idx + 1];
|
||||
if (maybePath && !maybePath.startsWith('-')) res.targetPath = maybePath;
|
||||
}
|
||||
const dIdx = args.findIndex((a) => a === '--domain' || a === '-d');
|
||||
if (dIdx >= 0 && args[dIdx + 1]) {
|
||||
res.domain = args[dIdx + 1];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export default async function bindCmd(): Promise<void> {
|
||||
try {
|
||||
let { domain, targetPath } = parseArgs();
|
||||
if (!targetPath) {
|
||||
const ans = await inquirer.prompt([
|
||||
{ type: 'input', name: 'path', message: 'Enter the path to upload and bind: ' },
|
||||
]);
|
||||
targetPath = ans.path;
|
||||
}
|
||||
if (!domain) {
|
||||
const ans = await inquirer.prompt([
|
||||
{ type: 'input', name: 'domain', message: 'Enter the Pinme subdomain (e.g., test_abc): ' },
|
||||
]);
|
||||
domain = ans.domain?.trim();
|
||||
}
|
||||
if (!targetPath || !domain) {
|
||||
console.log(chalk.red('Missing parameters. Path and domain are required.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 前置校验域名
|
||||
const check = await checkDomainAvailable(domain);
|
||||
if (!check.is_valid) {
|
||||
console.log(chalk.red(`Domain not available: ${check.error || 'unknown reason'}`));
|
||||
return;
|
||||
}
|
||||
console.log(chalk.green(`Domain available: ${domain}`));
|
||||
|
||||
// 上传
|
||||
const absolutePath = path.resolve(targetPath);
|
||||
console.log(chalk.blue(`Uploading: ${absolutePath}`));
|
||||
const up = await upload(absolutePath);
|
||||
if (!up?.contentHash) {
|
||||
console.log(chalk.red('Upload failed, binding aborted.'));
|
||||
return;
|
||||
}
|
||||
console.log(chalk.green(`Upload success, CID: ${up.contentHash}`));
|
||||
|
||||
// 绑定
|
||||
const ok = await bindPinmeDomain(domain, up.contentHash);
|
||||
if (!ok) {
|
||||
console.log(chalk.red('Binding failed. Please try again later.'));
|
||||
return;
|
||||
}
|
||||
console.log(chalk.green(`Bind success: ${domain}`));
|
||||
console.log(chalk.white(`Visit (Pinme subdomain example): https://${domain}.pinit.eth.limo`));
|
||||
} catch (e: any) {
|
||||
console.log(chalk.red(`Execution failed: ${e?.message || e}`));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+24
-1
@@ -13,6 +13,8 @@ import { version } from '../package.json';
|
||||
import upload from './upload';
|
||||
import remove from './remove';
|
||||
import { displayUploadHistory, clearUploadHistory } from './utils/history';
|
||||
import setAppKeyCmd from './set-appkey';
|
||||
import myDomainsCmd from './my-domains';
|
||||
|
||||
// display the ASCII art logo
|
||||
function showBanner(): void {
|
||||
@@ -33,7 +35,8 @@ program
|
||||
|
||||
program
|
||||
.command('upload')
|
||||
.description("upload a file or directory to IPFS")
|
||||
.description("upload a file or directory to IPFS. Supports --domain to bind after upload")
|
||||
.option('-d, --domain <name>', 'Pinme subdomain')
|
||||
.action(() => upload());
|
||||
|
||||
program
|
||||
@@ -41,6 +44,22 @@ program
|
||||
.description("remove a file from IPFS network")
|
||||
.action(() => remove());
|
||||
|
||||
program
|
||||
.command('set-appkey')
|
||||
.description("Set AppKey for authentication, and auto-merge anonymous history")
|
||||
.action(() => setAppKeyCmd());
|
||||
|
||||
program
|
||||
.command('my-domains')
|
||||
.alias('domain')
|
||||
.description("List domains owned by current account")
|
||||
.action(() => myDomainsCmd());
|
||||
|
||||
program
|
||||
.command('domain')
|
||||
.description("Alias for 'my-domains' command")
|
||||
.action(() => myDomainsCmd());
|
||||
|
||||
program
|
||||
.command('list')
|
||||
.description("show upload history")
|
||||
@@ -82,7 +101,11 @@ program.on('--help', () => {
|
||||
console.log('');
|
||||
console.log('Examples:');
|
||||
console.log(' $ pinme upload');
|
||||
console.log(' $ pinme upload <path> --domain <name>');
|
||||
console.log(' $ pinme rm <hash>');
|
||||
console.log(' $ pinme set-appkey <AppKey>');
|
||||
console.log(' $ pinme my-domains');
|
||||
console.log(' $ pinme domain');
|
||||
console.log(' $ pinme list -l 5');
|
||||
console.log(' $ pinme ls');
|
||||
console.log(' $ pinme help');
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import chalk from 'chalk';
|
||||
import dayjs from 'dayjs';
|
||||
import { getMyDomains } from './utils/pinmeApi';
|
||||
|
||||
export default async function myDomainsCmd(): Promise<void> {
|
||||
try {
|
||||
const list = await getMyDomains();
|
||||
if (!list.length) {
|
||||
console.log(chalk.yellow('No bound domains found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.cyan('My domains:'));
|
||||
console.log(chalk.cyan('-'.repeat(80)));
|
||||
list.forEach((item, i) => {
|
||||
console.log(chalk.green(`${i + 1}. ${item.domain_name}`));
|
||||
console.log(chalk.white(` Type: ${item.domain_type}`));
|
||||
if (item.bind_time) {
|
||||
console.log(chalk.white(` Bind time: ${dayjs(item.bind_time * 1000).format('YYYY-MM-DD HH:mm:ss')}`));
|
||||
}
|
||||
if (typeof item.expire_time === 'number') {
|
||||
const label = item.expire_time === 0 ? 'Never' : dayjs(item.expire_time * 1000).format('YYYY-MM-DD HH:mm:ss');
|
||||
console.log(chalk.white(` Expire time: ${label}`));
|
||||
}
|
||||
console.log(chalk.cyan('-'.repeat(80)));
|
||||
});
|
||||
} catch (e: any) {
|
||||
console.log(chalk.red(`Failed to fetch domains: ${e?.message || e}`));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import { setAuthToken } from './utils/auth';
|
||||
import { getDeviceId } from './utils/getDeviceId';
|
||||
import { bindAnonymousDevice } from './utils/pinmeApi';
|
||||
|
||||
export default async function setAppKeyCmd(): Promise<void> {
|
||||
try {
|
||||
const argAppKey = process.argv[3];
|
||||
let appKey = argAppKey;
|
||||
if (!appKey) {
|
||||
const ans = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'appKey',
|
||||
message: 'Enter AppKey: ',
|
||||
},
|
||||
]);
|
||||
appKey = ans.appKey;
|
||||
}
|
||||
if (!appKey) {
|
||||
console.log(chalk.red('AppKey not provided.'));
|
||||
return;
|
||||
}
|
||||
const saved = setAuthToken(appKey);
|
||||
console.log(chalk.green(`Auth set for address: ${saved.address}`));
|
||||
|
||||
// Auto-merge anonymous history
|
||||
const deviceId = getDeviceId();
|
||||
const ok = await bindAnonymousDevice(deviceId);
|
||||
if (ok) {
|
||||
console.log(chalk.green('Anonymous history merged to current account.'));
|
||||
} else {
|
||||
console.log(chalk.yellow('Anonymous history merge not confirmed. You may retry later.'));
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.log(chalk.red(`Failed to set AppKey: ${e?.message || e}`));
|
||||
}
|
||||
}
|
||||
|
||||
+73
-6
@@ -5,6 +5,9 @@ import figlet from 'figlet';
|
||||
import upload from './utils/uploadToIpfsSplit';
|
||||
import fs from 'fs';
|
||||
import CryptoJS from 'crypto-js';
|
||||
import { checkDomainAvailable, bindPinmeDomain } from './utils/pinmeApi';
|
||||
import { getAuthConfig } from './utils/auth';
|
||||
import { getDeviceId } from './utils/getDeviceId';
|
||||
|
||||
// get from environment variables
|
||||
const URL = process.env.IPFS_PREVIEW_URL;
|
||||
@@ -13,13 +16,15 @@ const secretKey = process.env.SECRET_KEY;
|
||||
import { checkNodeVersion } from './utils/checkNodeVersion';
|
||||
checkNodeVersion();
|
||||
|
||||
// encrypt the hash
|
||||
function encryptHash(hash: string, key: string | undefined): string {
|
||||
// encrypt the hash with optional uid (device id)
|
||||
function encryptHash(contentHash: string, key: string | undefined, uid?: string): string {
|
||||
try {
|
||||
if (!key) {
|
||||
throw new Error('Secret key not found');
|
||||
}
|
||||
const encrypted = CryptoJS.RC4.encrypt(hash, key).toString();
|
||||
// Combine contentHash-uid if uid exists, otherwise just contentHash (for backward compatibility)
|
||||
const combined = uid ? `${contentHash}-${uid}` : contentHash;
|
||||
const encrypted = CryptoJS.RC4.encrypt(combined, key).toString();
|
||||
const urlSafe = encrypted
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
@@ -27,7 +32,7 @@ function encryptHash(hash: string, key: string | undefined): string {
|
||||
return urlSafe;
|
||||
} catch (error: any) {
|
||||
console.error(`Encryption error: ${error.message}`);
|
||||
return hash;
|
||||
return contentHash;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +57,24 @@ interface UploadOptions {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
function getDomainFromArgs(): string | null {
|
||||
const args = process.argv.slice(2);
|
||||
const dIdx = args.findIndex((a) => a === '--domain' || a === '-d');
|
||||
if (dIdx >= 0 && args[dIdx + 1] && !args[dIdx + 1].startsWith('-')) {
|
||||
return String(args[dIdx + 1]).trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get uid: use address from auth if logged in, otherwise use deviceId
|
||||
function getUid(): string {
|
||||
const auth = getAuthConfig();
|
||||
if (auth?.address) {
|
||||
return auth.address;
|
||||
}
|
||||
return getDeviceId();
|
||||
}
|
||||
|
||||
export default async (options?: UploadOptions): Promise<void> => {
|
||||
try {
|
||||
console.log(
|
||||
@@ -66,6 +89,7 @@ export default async (options?: UploadOptions): Promise<void> => {
|
||||
|
||||
// if the parameter is passed, upload directly, pinme upload /path/to/dir
|
||||
const argPath = process.argv[3];
|
||||
const domainArg = getDomainFromArgs();
|
||||
|
||||
if (argPath && !argPath.startsWith('-')) {
|
||||
// use the synchronous path check function
|
||||
@@ -75,11 +99,22 @@ export default async (options?: UploadOptions): Promise<void> => {
|
||||
return;
|
||||
}
|
||||
|
||||
// optional: pre-check domain availability before upload
|
||||
if (domainArg) {
|
||||
const check = await checkDomainAvailable(domainArg);
|
||||
if (!check.is_valid) {
|
||||
console.log(chalk.red(`Domain not available: ${check.error || 'unknown reason'}`));
|
||||
return;
|
||||
}
|
||||
console.log(chalk.green(`Domain available: ${domainArg}`));
|
||||
}
|
||||
|
||||
console.log(chalk.blue(`uploading ${absolutePath} to ipfs...`));
|
||||
try {
|
||||
const result = await upload(absolutePath);
|
||||
if (result) {
|
||||
const encryptedCID = encryptHash(result.contentHash, secretKey);
|
||||
const uid = getUid();
|
||||
const encryptedCID = encryptHash(result.contentHash, secretKey, uid);
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
figlet.textSync('Successful', { horizontalLayout: 'full' }),
|
||||
@@ -87,6 +122,17 @@ export default async (options?: UploadOptions): Promise<void> => {
|
||||
);
|
||||
console.log(chalk.cyan(`URL:`));
|
||||
console.log(chalk.cyan(`${URL}${encryptedCID}`));
|
||||
// optional: bind domain after upload
|
||||
if (domainArg) {
|
||||
console.log(chalk.blue(`Binding domain: ${domainArg} with CID: ${result.contentHash}`));
|
||||
const ok = await bindPinmeDomain(domainArg, result.contentHash);
|
||||
if (ok) {
|
||||
console.log(chalk.green(`Bind success: ${domainArg}`));
|
||||
console.log(chalk.white(`Visit (Pinme subdomain example): https://${domainArg}.pinit.eth.limo`));
|
||||
} else {
|
||||
console.log(chalk.red('Binding failed. Please try again later.'));
|
||||
}
|
||||
}
|
||||
console.log(chalk.green('\n🎉 upload successful, program exit'));
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -111,12 +157,23 @@ export default async (options?: UploadOptions): Promise<void> => {
|
||||
return;
|
||||
}
|
||||
|
||||
// optional: interactive flow may also parse --domain, reuse the same arg parsing
|
||||
if (domainArg) {
|
||||
const check = await checkDomainAvailable(domainArg);
|
||||
if (!check.is_valid) {
|
||||
console.log(chalk.red(`Domain not available: ${check.error || 'unknown reason'}`));
|
||||
return;
|
||||
}
|
||||
console.log(chalk.green(`Domain available: ${domainArg}`));
|
||||
}
|
||||
|
||||
console.log(chalk.blue(`uploading ${absolutePath} to ipfs...`));
|
||||
try {
|
||||
const result = await upload(absolutePath);
|
||||
|
||||
if (result) {
|
||||
const encryptedCID = encryptHash(result.contentHash, secretKey);
|
||||
const uid = getUid();
|
||||
const encryptedCID = encryptHash(result.contentHash, secretKey, uid);
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
figlet.textSync('Successful', { horizontalLayout: 'full' }),
|
||||
@@ -124,6 +181,16 @@ export default async (options?: UploadOptions): Promise<void> => {
|
||||
);
|
||||
console.log(chalk.cyan(`URL:`));
|
||||
console.log(chalk.cyan(`${URL}${encryptedCID}`));
|
||||
if (domainArg) {
|
||||
console.log(chalk.blue(`Binding domain: ${domainArg} with CID: ${result.contentHash}`));
|
||||
const ok = await bindPinmeDomain(domainArg, result.contentHash);
|
||||
if (ok) {
|
||||
console.log(chalk.green(`Bind success: ${domainArg}`));
|
||||
console.log(chalk.white(`Visit (Pinme subdomain example): https://${domainArg}.pinit.eth.limo`));
|
||||
} else {
|
||||
console.log(chalk.red('Binding failed. Please try again later.'));
|
||||
}
|
||||
}
|
||||
console.log(chalk.green('\n🎉 upload successful, program exit'));
|
||||
}
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import fs from 'fs-extra';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
const CONFIG_DIR = path.join(os.homedir(), '.pinme');
|
||||
const AUTH_FILE = path.join(CONFIG_DIR, 'auth.json');
|
||||
|
||||
export interface AuthConfig {
|
||||
address: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
function ensureConfigDir(): void {
|
||||
if (!fs.existsSync(CONFIG_DIR)) {
|
||||
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
export function parseCombinedToken(combined: string): AuthConfig {
|
||||
// combined format: "<address>-<jwt>"
|
||||
// Split only at the first '-' to preserve '-' inside JWT if any.
|
||||
const firstDash = combined.indexOf('-');
|
||||
if (firstDash <= 0 || firstDash === combined.length - 1) {
|
||||
throw new Error('Invalid token format. Expected "<address>-<jwt>".');
|
||||
}
|
||||
const address = combined.slice(0, firstDash).trim();
|
||||
const token = combined.slice(firstDash + 1).trim();
|
||||
if (!address || !token) {
|
||||
throw new Error('Invalid token content. Address or token is empty.');
|
||||
}
|
||||
return { address, token };
|
||||
}
|
||||
|
||||
export function setAuthToken(combined: string): AuthConfig {
|
||||
ensureConfigDir();
|
||||
const auth = parseCombinedToken(combined);
|
||||
fs.writeJsonSync(AUTH_FILE, auth, { spaces: 2 });
|
||||
return auth;
|
||||
}
|
||||
|
||||
export function getAuthConfig(): AuthConfig | null {
|
||||
try {
|
||||
if (!fs.existsSync(AUTH_FILE)) return null;
|
||||
const data = fs.readJsonSync(AUTH_FILE) as AuthConfig;
|
||||
if (!data?.address || !data?.token) return null;
|
||||
return data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getAuthHeaders(): Record<string, string> {
|
||||
const conf = getAuthConfig();
|
||||
if (!conf) {
|
||||
throw new Error('Auth not set. Run: pinme set-appkey <AppKey>');
|
||||
}
|
||||
return {
|
||||
'token-address': conf.address,
|
||||
'authentication-tokens': conf.token,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+32
-3
@@ -1,6 +1,12 @@
|
||||
import CryptoJS from "crypto-js";
|
||||
|
||||
export const decryptHash = (encryptedHash: string | undefined, key: string) => {
|
||||
export interface DecryptedHash {
|
||||
contentHash: string;
|
||||
uid?: string; // address (if logged in) or deviceId (if not logged in)
|
||||
version: number; // 1 if old version (no '-' separator), 2 if new version (contentHash-uid)
|
||||
}
|
||||
|
||||
export const decryptHash = (encryptedHash: string | undefined, key: string): DecryptedHash | null => {
|
||||
try {
|
||||
if (!encryptedHash) {
|
||||
return null;
|
||||
@@ -10,9 +16,32 @@ export const decryptHash = (encryptedHash: string | undefined, key: string) => {
|
||||
base64 += "=";
|
||||
}
|
||||
const decrypted = CryptoJS.RC4.decrypt(base64, key);
|
||||
return decrypted.toString(CryptoJS.enc.Utf8);
|
||||
const decryptedStr = decrypted.toString(CryptoJS.enc.Utf8);
|
||||
|
||||
// Check if it contains '-' separator (new version: contentHash-uid)
|
||||
const dashIndex = decryptedStr.indexOf('-');
|
||||
if (dashIndex > 0 && dashIndex < decryptedStr.length - 1) {
|
||||
// New version: split into contentHash and uid (address or deviceId)
|
||||
const contentHash = decryptedStr.slice(0, dashIndex);
|
||||
const uid = decryptedStr.slice(dashIndex + 1);
|
||||
return {
|
||||
contentHash,
|
||||
uid,
|
||||
version: 2,
|
||||
};
|
||||
} else {
|
||||
// Legacy version: only contentHash, no separator
|
||||
return {
|
||||
contentHash: decryptedStr,
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`Decryption error: ${error.message}`);
|
||||
return encryptedHash;
|
||||
// Return as legacy format if decryption fails
|
||||
return encryptedHash ? {
|
||||
contentHash: encryptedHash,
|
||||
version: 1,
|
||||
} : null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import chalk from 'chalk';
|
||||
import { getAuthHeaders } from './auth';
|
||||
|
||||
const DEFAULT_BASE = process.env.PINME_API_BASE || 'http://ipfs-proxy.opena.chat/api/v4';
|
||||
|
||||
function createClient(): AxiosInstance {
|
||||
const headers = getAuthHeaders();
|
||||
return axios.create({
|
||||
baseURL: DEFAULT_BASE,
|
||||
timeout: 20000,
|
||||
headers: {
|
||||
...headers,
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Pinme-CLI',
|
||||
Connection: 'keep-alive',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function bindAnonymousDevice(anonymousUid: string): Promise<boolean> {
|
||||
try {
|
||||
const client = createClient();
|
||||
const { data } = await client.post('/bind_anonymous', {
|
||||
anonymous_uid: anonymousUid,
|
||||
});
|
||||
return data?.code === 200;
|
||||
} catch (e: any) {
|
||||
console.log(chalk.yellow(`Failed to trigger anonymous binding: ${e?.message || e}`));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export interface CheckDomainResult {
|
||||
is_valid: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function checkDomainAvailable(domainName: string): Promise<CheckDomainResult> {
|
||||
const client = createClient();
|
||||
// 端点可能未固定,优先使用环境变量,其次尝试两个常见路径
|
||||
const configured = process.env.PINME_CHECK_DOMAIN_PATH || '/check_domain';
|
||||
const fallbacks = [configured, '/check_domain_available'];
|
||||
|
||||
for (const p of fallbacks) {
|
||||
try {
|
||||
const { data } = await client.post(p, { domain_name: domainName });
|
||||
if (typeof data?.is_valid === 'boolean') {
|
||||
return { is_valid: data.is_valid, error: data?.error };
|
||||
}
|
||||
if (data?.data && typeof data.data.is_valid === 'boolean') {
|
||||
return { is_valid: data.data.is_valid, error: data.data?.error };
|
||||
}
|
||||
// 不符合预期结构,继续尝试下一个路径
|
||||
} catch (e: any) {
|
||||
// 404/405/500 等继续尝试下一个
|
||||
}
|
||||
}
|
||||
// 如果所有尝试失败,则返回未知状态,交由后续 bind 返回报错提示
|
||||
return { is_valid: true };
|
||||
}
|
||||
|
||||
export async function bindPinmeDomain(domainName: string, hash: string): Promise<boolean> {
|
||||
const client = createClient();
|
||||
const { data } = await client.post('/bind_pinme_domain', {
|
||||
domain_name: domainName,
|
||||
hash,
|
||||
});
|
||||
return data?.code === 200;
|
||||
}
|
||||
|
||||
export interface MyDomainItem {
|
||||
domain_name: string;
|
||||
domain_type: number;
|
||||
bind_time: number;
|
||||
expire_time: number;
|
||||
}
|
||||
|
||||
export async function getMyDomains(): Promise<MyDomainItem[]> {
|
||||
const client = createClient();
|
||||
const { data } = await client.get('/my_domains');
|
||||
if (data?.code === 200) {
|
||||
if (Array.isArray(data?.data)) {
|
||||
// v4: data is array
|
||||
return data.data as MyDomainItem[];
|
||||
}
|
||||
if (data?.data?.list && Array.isArray(data.data.list)) {
|
||||
// fallback: sometimes wrapped in { list: [] }
|
||||
return data.data.list as MyDomainItem[];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pinme",
|
||||
"version": "1.1.5",
|
||||
"version": "1.2.0",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user