mirror of
https://github.com/glitternetwork/pinme.git
synced 2026-09-01 15:33:43 +08:00
feat: custom domain[version 1.2.5 ]
This commit is contained in:
@@ -33,6 +33,9 @@ Website: [https://pinme.eth.limo/](https://pinme.eth.limo/)
|
||||
- [For AI](#for-ai)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Bind Domain](#bind-domain-requires-vip)
|
||||
- [Command Details](#command-details)
|
||||
- [VIP Membership](#vip-membership)
|
||||
- [Common Static File Directories](#common-static-file-directories)
|
||||
- [Error Handling](#error-handling)
|
||||
- [Upload Limits](#upload-limits)
|
||||
@@ -216,10 +219,30 @@ 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>
|
||||
### Bind Domain (requires VIP)
|
||||
|
||||
```bash
|
||||
# Upload and bind to a domain (auto-detected: Pinme subdomain or DNS domain)
|
||||
pinme bind <path> --domain <name>
|
||||
pinme bind <path> -d <name>
|
||||
```
|
||||
|
||||
**Smart Auto-Detection:**
|
||||
- Domains with a dot (e.g., `example.com`) → **DNS domain**
|
||||
- Domains without a dot (e.g., `my-site`) → **Pinme subdomain**
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Bind to a Pinme subdomain (auto-detected)
|
||||
pinme bind ./dist --domain my-site
|
||||
|
||||
# Bind to a DNS domain (auto-detected by the dot)
|
||||
pinme bind ./dist --domain example.com
|
||||
|
||||
# Force DNS mode if needed
|
||||
pinme bind ./dist --domain my-site --dns
|
||||
```
|
||||
|
||||
### Import CAR files
|
||||
@@ -337,6 +360,51 @@ The selected directory must meet:
|
||||
|
||||
## Command Details
|
||||
|
||||
### `bind`
|
||||
|
||||
Upload files and bind them to a custom domain. **Domain binding requires VIP membership.**
|
||||
|
||||
```bash
|
||||
pinme bind <path> [options]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `path`: Path to the file or directory to upload (optional, interactive if not provided)
|
||||
- `-d, --domain <name>`: Domain name to bind (required)
|
||||
- `--dns`: Force DNS domain mode (optional, auto-detected from domain format)
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Interactive mode (will prompt for path and domain)
|
||||
pinme bind
|
||||
|
||||
# Bind to a Pinme subdomain (auto-detected: no dot in domain)
|
||||
pinme bind ./dist --domain my-site
|
||||
|
||||
# Bind to a DNS domain (auto-detected: contains dot)
|
||||
pinme bind ./dist --domain example.com
|
||||
|
||||
# Force DNS mode with --dns flag
|
||||
pinme bind ./dist --domain my-site --dns
|
||||
```
|
||||
|
||||
**Auto-Detection:**
|
||||
- Domains with a dot (e.g., `example.com`) are automatically treated as **DNS domains**
|
||||
- Domains without a dot (e.g., `my-site`) are automatically treated as **Pinme subdomains**
|
||||
- Use `--dns` or `-D` flag to force DNS domain mode when needed
|
||||
|
||||
**Requirements:**
|
||||
- VIP membership required for domain binding
|
||||
- Valid AppKey must be set (run: `pinme set-appkey <AppKey>`)
|
||||
- For DNS domains, you must own the domain
|
||||
|
||||
**URL Formats:**
|
||||
- Pinme subdomain: `https://<name>.pinit.eth.limo`
|
||||
- DNS domain: `https://<your-domain>`
|
||||
|
||||
**DNS Setup:**
|
||||
After successful DNS domain binding, visit the [DNS Configuration Guide](https://pinme.eth.limo/#/docs?id=custom-domain) to complete DNS setup.
|
||||
|
||||
### `upload`
|
||||
|
||||
Upload a file or directory to the IPFS network.
|
||||
@@ -347,20 +415,22 @@ pinme upload [path] [--domain <name>]
|
||||
|
||||
**Options:**
|
||||
- `path`: Path to the file or directory to upload (optional, interactive if not provided)
|
||||
- `-d, --domain <name>`: Pinme subdomain to bind after upload (optional)
|
||||
- `-d, --domain <name>`: Pinme subdomain to bind after upload (optional, requires VIP)
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
# Upload dist directory
|
||||
pinme upload dist
|
||||
|
||||
# Upload and bind to a domain (requires Plus membership)
|
||||
pinme upload dist --domain my-site
|
||||
# Upload only (no domain binding)
|
||||
pinme upload dist
|
||||
|
||||
# Upload a specific file
|
||||
pinme upload ./example.jpg
|
||||
```
|
||||
|
||||
**Note:** Domain binding during upload requires VIP. Use the `bind` command for domain binding.
|
||||
|
||||
### `import`
|
||||
|
||||
Import CAR (Content Addressable aRchive) files to the IPFS network. This command is specifically designed for importing CAR files while maintaining their original structure. Supports binding to a Pinme subdomain after import.
|
||||
@@ -475,6 +545,51 @@ List all domains owned by the current account.
|
||||
|
||||
---
|
||||
|
||||
## VIP Membership
|
||||
|
||||
### Overview
|
||||
|
||||
VIP membership provides access to premium features including domain binding and custom DNS support.
|
||||
|
||||
### VIP Features
|
||||
|
||||
| Feature | Free | VIP |
|
||||
|---------|------|-----|
|
||||
| Upload files to IPFS | ✅ | ✅ |
|
||||
| Preview URL | ✅ | ✅ |
|
||||
| Pinme subdomain binding | ❌ | ✅ |
|
||||
| Custom DNS domain binding | ❌ | ✅ |
|
||||
| Priority support | ❌ | ✅ |
|
||||
|
||||
### Domain Binding Requirements
|
||||
|
||||
Domain binding (both Pinme subdomains and custom DNS domains) requires VIP membership.
|
||||
|
||||
**Before using domain binding:**
|
||||
|
||||
1. **Upgrade to VIP**
|
||||
- Visit [PinMe website](https://pinme.eth.limo/) to upgrade
|
||||
|
||||
2. **Set AppKey**
|
||||
```bash
|
||||
pinme set-appkey <AppKey>
|
||||
```
|
||||
|
||||
3. **Bind your domain**
|
||||
```bash
|
||||
# Bind to a Pinme subdomain
|
||||
pinme bind ./dist --domain my-site
|
||||
|
||||
# Bind to a custom DNS domain
|
||||
pinme bind ./dist --domain example.com --dns
|
||||
```
|
||||
|
||||
### Checking VIP Status
|
||||
|
||||
If you attempt to bind a domain without VIP, you'll see an error message. You can check your VIP status on the [PinMe website](https://pinme.eth.limo/).
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Errors and Solutions
|
||||
|
||||
+147
-18
@@ -2,15 +2,59 @@ import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import upload from './utils/uploadToIpfsSplit';
|
||||
import { checkDomainAvailable, bindPinmeDomain } from './utils/pinmeApi';
|
||||
import { checkDomainAvailable, bindPinmeDomain, bindDnsDomainV4, isVip } from './utils/pinmeApi';
|
||||
import { getAuthConfig } from './utils/auth';
|
||||
|
||||
interface Args {
|
||||
domain?: string;
|
||||
targetPath?: string;
|
||||
dns?: boolean;
|
||||
}
|
||||
|
||||
// Check if a domain is a DNS domain (contains a dot)
|
||||
function isDnsDomain(domain: string): boolean {
|
||||
return domain.includes('.');
|
||||
}
|
||||
|
||||
// Validate DNS domain format
|
||||
function validateDnsDomain(domain: string): { valid: boolean; message?: string } {
|
||||
// Remove protocol if present
|
||||
const cleanDomain = domain.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
||||
|
||||
// Check for valid domain format
|
||||
const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]*(\.[a-zA-Z0-9][a-zA-Z0-9-]*)*\.[a-zA-Z]{2,}$/;
|
||||
const parts = cleanDomain.split('.');
|
||||
|
||||
// Check each part
|
||||
if (parts.length < 2) {
|
||||
return { valid: false, message: 'Invalid domain format. Please enter a complete domain (e.g., example.com)' };
|
||||
}
|
||||
|
||||
// Check each label (part between dots)
|
||||
for (const part of parts) {
|
||||
if (part.length === 0) {
|
||||
return { valid: false, message: 'Invalid domain format. Consecutive dots are not allowed' };
|
||||
}
|
||||
if (part.length > 63) {
|
||||
return { valid: false, message: 'Invalid domain format. Each label must be 63 characters or less' };
|
||||
}
|
||||
if (!/^[a-zA-Z0-9-]+$/.test(part)) {
|
||||
return { valid: false, message: 'Invalid domain format. Domains can only contain letters, numbers, and hyphens' };
|
||||
}
|
||||
if (/^-|-$/.test(part)) {
|
||||
return { valid: false, message: 'Invalid domain format. Labels cannot start or end with hyphens' };
|
||||
}
|
||||
}
|
||||
|
||||
if (!domainRegex.test(cleanDomain)) {
|
||||
return { valid: false, message: 'Invalid domain format' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
function parseArgs(): Args {
|
||||
// Usage: pinme bind <path> --domain <name>
|
||||
// Usage: pinme bind <path> --domain <name> [--dns]
|
||||
const args = process.argv.slice(2);
|
||||
const res: Args = {};
|
||||
const idx = args.indexOf('bind');
|
||||
@@ -22,12 +66,42 @@ function parseArgs(): Args {
|
||||
if (dIdx >= 0 && args[dIdx + 1]) {
|
||||
res.domain = args[dIdx + 1];
|
||||
}
|
||||
const dnsIdx = args.findIndex((a) => a === '--dns' || a === '-D');
|
||||
if (dnsIdx >= 0) {
|
||||
res.dns = true;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async function checkVipStatus(authConfig: { address: string; token: string }): Promise<boolean> {
|
||||
console.log(chalk.blue('Checking VIP status...'));
|
||||
try {
|
||||
const vipResult = await isVip(authConfig.address, authConfig.token);
|
||||
if (!vipResult.data?.is_vip) {
|
||||
return false;
|
||||
}
|
||||
console.log(chalk.green('VIP verified.'));
|
||||
return true;
|
||||
} catch (e: any) {
|
||||
if (e.message === 'Token expired') {
|
||||
throw e;
|
||||
}
|
||||
console.log(chalk.yellow('Failed to check VIP status, continuing...'));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function bindCmd(): Promise<void> {
|
||||
try {
|
||||
let { domain, targetPath } = parseArgs();
|
||||
let { domain, targetPath, dns } = parseArgs();
|
||||
|
||||
// Check auth
|
||||
const authConfig = getAuthConfig();
|
||||
if (!authConfig) {
|
||||
console.log(chalk.red('Please login first. Run: pinme set-appkey <AppKey>'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!targetPath) {
|
||||
const ans = await inquirer.prompt([
|
||||
{ type: 'input', name: 'path', message: 'Enter the path to upload and bind: ' },
|
||||
@@ -36,7 +110,7 @@ export default async function bindCmd(): Promise<void> {
|
||||
}
|
||||
if (!domain) {
|
||||
const ans = await inquirer.prompt([
|
||||
{ type: 'input', name: 'domain', message: 'Enter the Pinme subdomain (e.g., test_abc): ' },
|
||||
{ type: 'input', name: 'domain', message: 'Enter the domain to bind (e.g., my-site or example.com): ' },
|
||||
]);
|
||||
domain = ans.domain?.trim();
|
||||
}
|
||||
@@ -45,13 +119,48 @@ export default async function bindCmd(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pre-check domain availability
|
||||
const check = await checkDomainAvailable(domain);
|
||||
if (!check.is_valid) {
|
||||
console.log(chalk.red(`Domain not available: ${check.error || 'unknown reason'}`));
|
||||
return;
|
||||
// Auto-detect domain type if not explicitly specified
|
||||
const isDns = dns || isDnsDomain(domain);
|
||||
console.log(isDns,'isDns')
|
||||
const displayDomain = domain.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
||||
|
||||
// Validate DNS domain format
|
||||
if (isDns) {
|
||||
const validation = validateDnsDomain(domain);
|
||||
if (!validation.valid) {
|
||||
console.log(chalk.red(validation.message!));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// All domain binding requires VIP
|
||||
try {
|
||||
const isVipUser = await checkVipStatus(authConfig);
|
||||
if (!isVipUser) {
|
||||
console.log(chalk.red('Domain binding requires VIP. Please upgrade to VIP first.'));
|
||||
return;
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e.message === 'Token expired') {
|
||||
return; // Token expired hint already shown in API
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Pre-check domain availability
|
||||
try {
|
||||
const check = await checkDomainAvailable(displayDomain);
|
||||
if (!check.is_valid) {
|
||||
console.log(chalk.red(`Domain not available: ${check.error || 'unknown reason'}`));
|
||||
return;
|
||||
}
|
||||
console.log(chalk.green(`Domain available: ${displayDomain}`));
|
||||
} catch (e: any) {
|
||||
if (e.message === 'Token expired') {
|
||||
return; // Token expired hint already shown in API
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
console.log(chalk.green(`Domain available: ${domain}`));
|
||||
|
||||
// Upload
|
||||
const absolutePath = path.resolve(targetPath);
|
||||
@@ -64,16 +173,36 @@ export default async function bindCmd(): Promise<void> {
|
||||
console.log(chalk.green(`Upload success, CID: ${up.contentHash}`));
|
||||
|
||||
// Bind domain
|
||||
const ok = await bindPinmeDomain(domain, up.contentHash);
|
||||
if (!ok) {
|
||||
console.log(chalk.red('Binding failed. Please try again later.'));
|
||||
return;
|
||||
try {
|
||||
if (isDns) {
|
||||
// DNS domain binding
|
||||
console.log(chalk.blue('Binding DNS domain...'));
|
||||
const dnsResult = await bindDnsDomainV4(displayDomain, up.contentHash, authConfig.address, authConfig.token);
|
||||
if (dnsResult.code !== 200) {
|
||||
console.log(chalk.red(`DNS binding failed: ${dnsResult.msg}`));
|
||||
return;
|
||||
}
|
||||
console.log(chalk.green(`DNS bind success: ${displayDomain}`));
|
||||
console.log(chalk.white(`Visit: https://${displayDomain}`));
|
||||
console.log(chalk.cyan('\n📚 DNS Setup Guide: https://pinme.eth.limo/#/docs?id=custom-domain'));
|
||||
} else {
|
||||
// Pinme subdomain binding
|
||||
console.log(chalk.blue('Binding Pinme subdomain...'));
|
||||
const ok = await bindPinmeDomain(displayDomain, up.contentHash);
|
||||
if (!ok) {
|
||||
console.log(chalk.red('Binding failed. Please try again later.'));
|
||||
return;
|
||||
}
|
||||
console.log(chalk.green(`Bind success: ${displayDomain}`));
|
||||
console.log(chalk.white(`Visit: https://${displayDomain}.pinit.eth.limo`));
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e.message === 'Token expired') {
|
||||
return; // Token expired hint already shown in API
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
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}`));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+10
-1
@@ -19,6 +19,7 @@ import setAppKeyCmd from './set-appkey';
|
||||
import logoutCmd from './logout';
|
||||
import showAppKeyCmd from './show-appkey';
|
||||
import myDomainsCmd from './my-domains';
|
||||
import bindCmd from './bind';
|
||||
|
||||
// display the ASCII art logo
|
||||
function showBanner(): void {
|
||||
@@ -40,7 +41,8 @@ program
|
||||
.description(
|
||||
'upload a file or directory to IPFS. Supports --domain to bind after upload',
|
||||
)
|
||||
.option('-d, --domain <name>', 'Pinme subdomain')
|
||||
.option('-d, --domain <name>', 'Domain name to bind')
|
||||
.option('--dns', 'Force DNS domain mode')
|
||||
.action(() => upload());
|
||||
|
||||
program
|
||||
@@ -84,6 +86,13 @@ program
|
||||
.description('List domains owned by current account')
|
||||
.action(() => myDomainsCmd());
|
||||
|
||||
program
|
||||
.command('bind')
|
||||
.description('Upload and bind to a domain (requires VIP)')
|
||||
.option('-d, --domain <name>', 'Domain name to bind')
|
||||
.option('--dns', 'Force DNS domain mode')
|
||||
.action(() => bindCmd());
|
||||
|
||||
program
|
||||
.command('domain')
|
||||
.description("Alias for 'my-domains' command")
|
||||
|
||||
+217
-43
@@ -5,8 +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 { checkDomainAvailable, bindPinmeDomain, bindDnsDomainV4, isVip } from './utils/pinmeApi';
|
||||
import { getUid } from './utils/getDeviceId';
|
||||
import { getAuthConfig } from './utils/auth';
|
||||
|
||||
// get from environment variables
|
||||
const URL = process.env.IPFS_PREVIEW_URL;
|
||||
@@ -15,6 +16,48 @@ const secretKey = process.env.SECRET_KEY;
|
||||
import { checkNodeVersion } from './utils/checkNodeVersion';
|
||||
checkNodeVersion();
|
||||
|
||||
// Check if a domain is a DNS domain (contains a dot)
|
||||
function isDnsDomain(domain: string): boolean {
|
||||
return domain.includes('.');
|
||||
}
|
||||
|
||||
// Validate DNS domain format
|
||||
function validateDnsDomain(domain: string): { valid: boolean; message?: string } {
|
||||
// Remove protocol if present
|
||||
const cleanDomain = domain.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
||||
|
||||
// Check for valid domain format
|
||||
const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-]*(\.[a-zA-Z0-9][a-zA-Z0-9-]*)*\.[a-zA-Z]{2,}$/;
|
||||
const parts = cleanDomain.split('.');
|
||||
|
||||
// Check each part
|
||||
if (parts.length < 2) {
|
||||
return { valid: false, message: 'Invalid domain format. Please enter a complete domain (e.g., example.com)' };
|
||||
}
|
||||
|
||||
// Check each label (part between dots)
|
||||
for (const part of parts) {
|
||||
if (part.length === 0) {
|
||||
return { valid: false, message: 'Invalid domain format. Consecutive dots are not allowed' };
|
||||
}
|
||||
if (part.length > 63) {
|
||||
return { valid: false, message: 'Invalid domain format. Each label must be 63 characters or less' };
|
||||
}
|
||||
if (!/^[a-zA-Z0-9-]+$/.test(part)) {
|
||||
return { valid: false, message: 'Invalid domain format. Domains can only contain letters, numbers, and hyphens' };
|
||||
}
|
||||
if (/^-|-$/.test(part)) {
|
||||
return { valid: false, message: 'Invalid domain format. Labels cannot start or end with hyphens' };
|
||||
}
|
||||
}
|
||||
|
||||
if (!domainRegex.test(cleanDomain)) {
|
||||
return { valid: false, message: 'Invalid domain format' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
// encrypt the hash with optional uid (device id)
|
||||
function encryptHash(
|
||||
contentHash: string,
|
||||
@@ -69,6 +112,62 @@ function getDomainFromArgs(): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDnsFromArgs(): boolean {
|
||||
const args = process.argv.slice(2);
|
||||
return args.includes('--dns') || args.includes('-D');
|
||||
}
|
||||
|
||||
async function checkVipStatus(authConfig: { address: string; token: string }): Promise<boolean> {
|
||||
console.log(chalk.blue('Checking VIP status...'));
|
||||
try {
|
||||
const vipResult = await isVip(authConfig.address, authConfig.token);
|
||||
if (!vipResult.data?.is_vip) {
|
||||
return false;
|
||||
}
|
||||
console.log(chalk.green('VIP verified.'));
|
||||
return true;
|
||||
} catch (e: any) {
|
||||
if (e.message === 'Token expired') {
|
||||
throw e;
|
||||
}
|
||||
console.log(chalk.yellow('Failed to check VIP status, continuing...'));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async function bindDomain(
|
||||
domain: string,
|
||||
contentHash: string,
|
||||
isDns: boolean,
|
||||
authConfig: { address: string; token: string },
|
||||
): Promise<boolean> {
|
||||
const displayDomain = domain.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
||||
|
||||
if (isDns) {
|
||||
// DNS domain binding
|
||||
console.log(chalk.blue('Binding DNS domain...'));
|
||||
const dnsResult = await bindDnsDomainV4(displayDomain, contentHash, authConfig.address, authConfig.token);
|
||||
if (dnsResult.code !== 200) {
|
||||
console.log(chalk.red(`DNS binding failed: ${dnsResult.msg}`));
|
||||
return false;
|
||||
}
|
||||
console.log(chalk.green(`DNS bind success: ${displayDomain}`));
|
||||
console.log(chalk.white(`Visit: https://${displayDomain}`));
|
||||
console.log(chalk.cyan('\n📚 DNS Setup Guide: https://pinme.eth.limo/#/docs?id=custom-domain'));
|
||||
} else {
|
||||
// Pinme subdomain binding
|
||||
console.log(chalk.blue('Binding Pinme subdomain...'));
|
||||
const ok = await bindPinmeDomain(displayDomain, contentHash);
|
||||
if (!ok) {
|
||||
console.log(chalk.red('Binding failed. Please try again later.'));
|
||||
return false;
|
||||
}
|
||||
console.log(chalk.green(`Bind success: ${displayDomain}`));
|
||||
console.log(chalk.white(`Visit: https://${displayDomain}.pinit.eth.limo`));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export default async (options?: UploadOptions): Promise<void> => {
|
||||
try {
|
||||
console.log(
|
||||
@@ -81,9 +180,17 @@ export default async (options?: UploadOptions): Promise<void> => {
|
||||
}),
|
||||
);
|
||||
|
||||
// Check auth
|
||||
const authConfig = getAuthConfig();
|
||||
if (!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];
|
||||
const domainArg = getDomainFromArgs();
|
||||
const dnsArg = getDnsFromArgs();
|
||||
|
||||
if (argPath && !argPath.startsWith('-')) {
|
||||
// use the synchronous path check function
|
||||
@@ -93,18 +200,54 @@ 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'}`,
|
||||
),
|
||||
);
|
||||
// Auto-detect domain type
|
||||
const isDns = dnsArg || (domainArg ? isDnsDomain(domainArg) : false);
|
||||
const displayDomain = domainArg?.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
||||
|
||||
// Validate DNS domain format
|
||||
if (isDns && domainArg) {
|
||||
const validation = validateDnsDomain(domainArg);
|
||||
if (!validation.valid) {
|
||||
console.log(chalk.red(validation.message!));
|
||||
return;
|
||||
}
|
||||
console.log(chalk.green(`Domain available: ${domainArg}`));
|
||||
}
|
||||
|
||||
// Domain binding requires VIP
|
||||
if (domainArg) {
|
||||
try {
|
||||
const isVipUser = await checkVipStatus(authConfig);
|
||||
if (!isVipUser) {
|
||||
console.log(chalk.red('Domain binding requires VIP. Please upgrade to VIP first.'));
|
||||
return;
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e.message === 'Token expired') {
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// optional: pre-check domain availability before upload
|
||||
if (domainArg) {
|
||||
try {
|
||||
const check = await checkDomainAvailable(displayDomain!);
|
||||
if (!check.is_valid) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
`Domain not available: ${check.error || 'unknown reason'}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
console.log(chalk.green(`Domain available: ${displayDomain}`));
|
||||
} catch (e: any) {
|
||||
if (e.message === 'Token expired') {
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.blue(`uploading ${absolutePath} to ipfs...`));
|
||||
@@ -120,23 +263,21 @@ 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}`,
|
||||
`Binding domain: ${displayDomain} 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.'));
|
||||
try {
|
||||
await bindDomain(domainArg, result.contentHash, isDns, authConfig);
|
||||
} catch (e: any) {
|
||||
if (e.message === 'Token expired') {
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
console.log(chalk.green('\n🎉 upload successful, program exit'));
|
||||
@@ -163,18 +304,54 @@ 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'}`,
|
||||
),
|
||||
);
|
||||
// Auto-detect domain type
|
||||
const isDns = dnsArg || (domainArg ? isDnsDomain(domainArg) : false);
|
||||
const displayDomain = domainArg?.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
||||
|
||||
// Validate DNS domain format
|
||||
if (isDns && domainArg) {
|
||||
const validation = validateDnsDomain(domainArg);
|
||||
if (!validation.valid) {
|
||||
console.log(chalk.red(validation.message!));
|
||||
return;
|
||||
}
|
||||
console.log(chalk.green(`Domain available: ${domainArg}`));
|
||||
}
|
||||
|
||||
// Domain binding requires VIP
|
||||
if (domainArg) {
|
||||
try {
|
||||
const isVipUser = await checkVipStatus(authConfig);
|
||||
if (!isVipUser) {
|
||||
console.log(chalk.red('Domain binding requires VIP. Please upgrade to VIP first.'));
|
||||
return;
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e.message === 'Token expired') {
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// optional: interactive flow may also parse --domain, reuse the same arg parsing
|
||||
if (domainArg) {
|
||||
try {
|
||||
const check = await checkDomainAvailable(displayDomain!);
|
||||
if (!check.is_valid) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
`Domain not available: ${check.error || 'unknown reason'}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
console.log(chalk.green(`Domain available: ${displayDomain}`));
|
||||
} catch (e: any) {
|
||||
if (e.message === 'Token expired') {
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.blue(`uploading ${absolutePath} to ipfs...`));
|
||||
@@ -194,19 +371,16 @@ export default async (options?: UploadOptions): Promise<void> => {
|
||||
if (domainArg) {
|
||||
console.log(
|
||||
chalk.blue(
|
||||
`Binding domain: ${domainArg} with CID: ${result.contentHash}`,
|
||||
`Binding domain: ${displayDomain} 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.'));
|
||||
try {
|
||||
await bindDomain(domainArg, result.contentHash, isDns, authConfig);
|
||||
} catch (e: any) {
|
||||
if (e.message === 'Token expired') {
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
console.log(chalk.green('\n🎉 upload successful, program exit'));
|
||||
|
||||
+96
-24
@@ -14,12 +14,13 @@ function showBanner(): void {
|
||||
// general help
|
||||
function showGeneralHelp(): void {
|
||||
showBanner();
|
||||
|
||||
|
||||
console.log("USAGE:");
|
||||
console.log(" pinme [command] [options]\n");
|
||||
|
||||
|
||||
console.log("COMMANDS:");
|
||||
console.log(" upload Upload a file or directory to IPFS");
|
||||
console.log(" bind Upload and bind to a domain (requires VIP)");
|
||||
console.log(" list Show upload history");
|
||||
console.log(" ls Alias for 'list' command");
|
||||
console.log(" set-appkey Set AppKey for authentication");
|
||||
@@ -27,31 +28,99 @@ function showGeneralHelp(): void {
|
||||
console.log(" appkey Alias for 'show-appkey' command");
|
||||
console.log(" logout Log out and clear authentication");
|
||||
console.log(" help [command] Show help for a specific command\n");
|
||||
|
||||
|
||||
console.log("OPTIONS:");
|
||||
console.log(" -v, --version Output the current version");
|
||||
console.log(" -h, --help Display help for command\n");
|
||||
|
||||
|
||||
console.log("For more information on a specific command, try:");
|
||||
console.log(" pinme help [command]");
|
||||
}
|
||||
|
||||
// bind command help
|
||||
function showBindHelp(): void {
|
||||
console.log("COMMAND:");
|
||||
console.log(" bind - Upload and bind to a domain (requires VIP)\n");
|
||||
|
||||
console.log("USAGE:");
|
||||
console.log(" pinme bind <path> [options]\n");
|
||||
|
||||
console.log("DESCRIPTION:");
|
||||
console.log(" This command uploads files and binds them to a custom domain.");
|
||||
console.log(" Domain binding requires VIP membership.\n");
|
||||
|
||||
console.log("OPTIONS:");
|
||||
console.log(" -d, --domain <name> Domain name to bind (required)");
|
||||
console.log(" --dns Force DNS domain mode (optional, auto-detected)\n");
|
||||
|
||||
console.log("EXAMPLES:");
|
||||
console.log(" # Interactive mode (will prompt for path and domain)");
|
||||
console.log(" pinme bind\n");
|
||||
console.log(" # Bind to a Pinme subdomain (auto-detected: no dot in domain)");
|
||||
console.log(" pinme bind ./dist --domain my-site\n");
|
||||
console.log(" # Bind to a DNS domain (auto-detected: contains dot)");
|
||||
console.log(" pinme bind ./dist --domain example.com\n");
|
||||
console.log(" # Force DNS mode with --dns flag");
|
||||
console.log(" pinme bind ./dist --domain my-site --dns\n");
|
||||
|
||||
console.log("AUTO-DETECTION:");
|
||||
console.log(" - Domains with a dot (e.g., example.com) are treated as DNS domains");
|
||||
console.log(" - Domains without a dot (e.g., my-site) are treated as Pinme subdomains");
|
||||
console.log(" - Use --dns flag to force DNS domain mode\n");
|
||||
|
||||
console.log("REQUIREMENTS:");
|
||||
console.log(" - VIP membership required for all domain binding");
|
||||
console.log(" - Valid AppKey must be set (run: pinme set-appkey <AppKey>)");
|
||||
console.log(" - For DNS domains, you must own the domain\n");
|
||||
|
||||
console.log("URL FORMATS:");
|
||||
console.log(" - Pinme subdomain: https://<name>.pinit.eth.limo");
|
||||
console.log(" - DNS domain: https://<your-domain>.com\n");
|
||||
|
||||
console.log("DNS SETUP:");
|
||||
console.log(" After successful DNS binding, visit:");
|
||||
console.log(" https://pinme.eth.limo/#/docs?id=custom-domain");
|
||||
console.log(" for DNS configuration guide.\n");
|
||||
}
|
||||
|
||||
// upload command help
|
||||
function showUploadHelp(): void {
|
||||
console.log("COMMAND:");
|
||||
console.log(" upload - Upload a file or directory to IPFS\n");
|
||||
|
||||
|
||||
console.log("USAGE:");
|
||||
console.log(" pinme upload [path]\n");
|
||||
|
||||
console.log(" pinme upload [path] [options]\n");
|
||||
|
||||
console.log("DESCRIPTION:");
|
||||
console.log(" This command uploads files or directories to IPFS.");
|
||||
console.log(" If no path is provided, it will start in interactive mode.\n");
|
||||
|
||||
console.log(" If no path is provided, it will start in interactive mode.");
|
||||
console.log(" Supports --domain option to bind a custom domain after upload (requires VIP).\n");
|
||||
|
||||
console.log("OPTIONS:");
|
||||
console.log(" -d, --domain <name> Domain name to bind (optional, requires VIP)");
|
||||
console.log(" --dns Force DNS domain mode (optional, auto-detected)\n");
|
||||
|
||||
console.log("EXAMPLES:");
|
||||
console.log(" # Upload without binding");
|
||||
console.log(" pinme upload");
|
||||
console.log(" pinme upload ./my-website\n");
|
||||
|
||||
console.log(" # Upload and bind to a Pinme subdomain (auto-detected: no dot)");
|
||||
console.log(" pinme upload ./dist --domain my-site\n");
|
||||
console.log(" # Upload and bind to a DNS domain (auto-detected: contains dot)");
|
||||
console.log(" pinme upload ./dist --domain example.com\n");
|
||||
console.log(" # Force DNS mode with --dns flag");
|
||||
console.log(" pinme upload ./dist --domain my-site --dns\n");
|
||||
|
||||
console.log("AUTO-DETECTION:");
|
||||
console.log(" - Domains with a dot (e.g., example.com) are treated as DNS domains");
|
||||
console.log(" - Domains without a dot (e.g., my-site) are treated as Pinme subdomains");
|
||||
console.log(" - Use --dns flag to force DNS domain mode\n");
|
||||
|
||||
console.log("REQUIREMENTS:");
|
||||
console.log(" - VIP membership required for domain binding");
|
||||
console.log(" - Valid AppKey must be set (run: pinme set-appkey <AppKey>)");
|
||||
console.log(" - For DNS domains, you must own the domain\n");
|
||||
|
||||
console.log("LIMITATIONS:");
|
||||
console.log(" - Maximum file size: 10MB");
|
||||
console.log(" - Maximum directory size: 500MB");
|
||||
@@ -61,14 +130,14 @@ function showUploadHelp(): void {
|
||||
function showListHelp(): void {
|
||||
console.log("COMMAND:");
|
||||
console.log(" list - Show upload history\n");
|
||||
|
||||
|
||||
console.log("USAGE:");
|
||||
console.log(" pinme list [options]\n");
|
||||
|
||||
|
||||
console.log("OPTIONS:");
|
||||
console.log(" -l, --limit <number> Limit the number of records to show");
|
||||
console.log(" -c, --clear Clear all upload history\n");
|
||||
|
||||
|
||||
console.log("EXAMPLES:");
|
||||
console.log(" pinme list");
|
||||
console.log(" pinme list -l 5");
|
||||
@@ -79,14 +148,14 @@ function showListHelp(): void {
|
||||
function showLsHelp(): void {
|
||||
console.log("COMMAND:");
|
||||
console.log(" ls - Alias for 'list' command\n");
|
||||
|
||||
|
||||
console.log("USAGE:");
|
||||
console.log(" pinme ls [options]\n");
|
||||
|
||||
|
||||
console.log("OPTIONS:");
|
||||
console.log(" -l, --limit <number> Limit the number of records to show");
|
||||
console.log(" -c, --clear Clear all upload history\n");
|
||||
|
||||
|
||||
console.log("EXAMPLES:");
|
||||
console.log(" pinme ls");
|
||||
console.log(" pinme ls -l 5");
|
||||
@@ -97,15 +166,15 @@ function showLsHelp(): void {
|
||||
function showShowAppKeyHelp(): void {
|
||||
console.log("COMMAND:");
|
||||
console.log(" show-appkey - Show current AppKey information (masked)\n");
|
||||
|
||||
|
||||
console.log("USAGE:");
|
||||
console.log(" pinme show-appkey");
|
||||
console.log(" pinme appkey\n");
|
||||
|
||||
|
||||
console.log("DESCRIPTION:");
|
||||
console.log(" This command displays the current AppKey information.");
|
||||
console.log(" Sensitive information (token and AppKey) will be masked for security.\n");
|
||||
|
||||
|
||||
console.log("EXAMPLES:");
|
||||
console.log(" pinme show-appkey");
|
||||
console.log(" pinme appkey");
|
||||
@@ -115,15 +184,15 @@ function showShowAppKeyHelp(): void {
|
||||
function showLogoutHelp(): void {
|
||||
console.log("COMMAND:");
|
||||
console.log(" logout - Log out and clear authentication\n");
|
||||
|
||||
|
||||
console.log("USAGE:");
|
||||
console.log(" pinme logout\n");
|
||||
|
||||
|
||||
console.log("DESCRIPTION:");
|
||||
console.log(" This command logs out the current user and clears the authentication");
|
||||
console.log(" information from local storage. You will need to set AppKey again");
|
||||
console.log(" to use authenticated features.\n");
|
||||
|
||||
|
||||
console.log("EXAMPLES:");
|
||||
console.log(" pinme logout");
|
||||
}
|
||||
@@ -134,8 +203,11 @@ function showHelp(command?: string): void {
|
||||
showGeneralHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
switch (command) {
|
||||
case 'bind':
|
||||
showBindHelp();
|
||||
break;
|
||||
case 'upload':
|
||||
showUploadHelp();
|
||||
break;
|
||||
@@ -161,4 +233,4 @@ function showHelp(command?: string): void {
|
||||
export {
|
||||
showHelp,
|
||||
showBanner
|
||||
};
|
||||
};
|
||||
|
||||
+214
-30
@@ -2,7 +2,56 @@ 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';
|
||||
const DEFAULT_BASE =
|
||||
process.env.PINME_API_BASE || 'http://ipfs-proxy.opena.chat/api/v4';
|
||||
|
||||
// Token expired error codes
|
||||
const TOKEN_EXPIRED_CODES = [
|
||||
401,
|
||||
403,
|
||||
10001,
|
||||
10002,
|
||||
20001,
|
||||
'TOKEN_EXPIRED',
|
||||
'AUTH_FAILED',
|
||||
];
|
||||
const TOKEN_EXPIRED_MESSAGES = [
|
||||
'token expired',
|
||||
'invalid token',
|
||||
'authentication failed',
|
||||
'auth failed',
|
||||
'unauthorized',
|
||||
'登录',
|
||||
'过期',
|
||||
'token',
|
||||
'鉴权',
|
||||
];
|
||||
|
||||
function isTokenExpired(error: any): boolean {
|
||||
const status = error.response?.status;
|
||||
const message =
|
||||
error.response?.data?.msg ||
|
||||
error.response?.data?.message ||
|
||||
error.message ||
|
||||
'';
|
||||
const code = error.response?.data?.code;
|
||||
|
||||
if (status && TOKEN_EXPIRED_CODES.includes(status)) {
|
||||
return true;
|
||||
}
|
||||
if (code && TOKEN_EXPIRED_CODES.includes(code)) {
|
||||
return true;
|
||||
}
|
||||
const lowerMessage = message.toLowerCase();
|
||||
return TOKEN_EXPIRED_MESSAGES.some((m) =>
|
||||
lowerMessage.includes(m.toLowerCase()),
|
||||
);
|
||||
}
|
||||
|
||||
function showTokenExpiredHint(): void {
|
||||
console.log(chalk.red('\n⚠️ Token has expired or is invalid.'));
|
||||
console.log(chalk.yellow('Please re-run: pinme set-appkey <AppKey>\n'));
|
||||
}
|
||||
|
||||
function createClient(): AxiosInstance {
|
||||
const headers = getAuthHeaders();
|
||||
@@ -19,7 +68,9 @@ function createClient(): AxiosInstance {
|
||||
});
|
||||
}
|
||||
|
||||
export async function bindAnonymousDevice(anonymousUid: string): Promise<boolean> {
|
||||
export async function bindAnonymousDevice(
|
||||
anonymousUid: string,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const client = createClient();
|
||||
const { data } = await client.post('/bind_anonymous', {
|
||||
@@ -27,7 +78,13 @@ export async function bindAnonymousDevice(anonymousUid: string): Promise<boolean
|
||||
});
|
||||
return data?.code === 200;
|
||||
} catch (e: any) {
|
||||
console.log(chalk.yellow(`Failed to trigger anonymous binding: ${e?.message || e}`));
|
||||
if (isTokenExpired(e)) {
|
||||
showTokenExpiredHint();
|
||||
return false;
|
||||
}
|
||||
console.log(
|
||||
chalk.yellow(`Failed to trigger anonymous binding: ${e?.message || e}`),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -37,7 +94,9 @@ export interface CheckDomainResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function checkDomainAvailable(domainName: string): Promise<CheckDomainResult> {
|
||||
export async function checkDomainAvailable(
|
||||
domainName: string,
|
||||
): Promise<CheckDomainResult> {
|
||||
const client = createClient();
|
||||
// Endpoint may not be fixed, prioritize environment variable, then try two common paths
|
||||
const configured = process.env.PINME_CHECK_DOMAIN_PATH || '/check_domain';
|
||||
@@ -54,6 +113,10 @@ export async function checkDomainAvailable(domainName: string): Promise<CheckDom
|
||||
}
|
||||
// Unexpected structure, continue trying next path
|
||||
} catch (e: any) {
|
||||
if (isTokenExpired(e)) {
|
||||
showTokenExpiredHint();
|
||||
throw new Error('Token expired');
|
||||
}
|
||||
// 404/405/500 etc., continue trying next path
|
||||
}
|
||||
}
|
||||
@@ -61,13 +124,24 @@ export async function checkDomainAvailable(domainName: string): Promise<CheckDom
|
||||
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 async function bindPinmeDomain(
|
||||
domainName: string,
|
||||
hash: string,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const client = createClient();
|
||||
const { data } = await client.post('/bind_pinme_domain', {
|
||||
domain_name: domainName,
|
||||
hash,
|
||||
});
|
||||
return data?.code === 200;
|
||||
} catch (e: any) {
|
||||
if (isTokenExpired(e)) {
|
||||
showTokenExpiredHint();
|
||||
throw new Error('Token expired');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export interface MyDomainItem {
|
||||
@@ -78,23 +152,110 @@ export interface MyDomainItem {
|
||||
}
|
||||
|
||||
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[];
|
||||
try {
|
||||
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[];
|
||||
}
|
||||
}
|
||||
if (data?.data?.list && Array.isArray(data.data.list)) {
|
||||
// fallback: sometimes wrapped in { list: [] }
|
||||
return data.data.list as MyDomainItem[];
|
||||
if (data?.code === 401 || data?.code === 403) {
|
||||
showTokenExpiredHint();
|
||||
throw new Error('Token expired');
|
||||
}
|
||||
return [];
|
||||
} catch (e: any) {
|
||||
if (isTokenExpired(e)) {
|
||||
showTokenExpiredHint();
|
||||
throw new Error('Token expired');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export interface BindDnsDomainV4Response {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
domain_name: string;
|
||||
hash: string;
|
||||
bind_time?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export async function bindDnsDomainV4(
|
||||
domainName: string,
|
||||
hash: string,
|
||||
tokenAddress: string,
|
||||
authToken: string,
|
||||
): Promise<BindDnsDomainV4Response> {
|
||||
try {
|
||||
const client = createClient();
|
||||
const { data } = await client.post<BindDnsDomainV4Response>(
|
||||
'/bind_dns',
|
||||
{
|
||||
domain_name: domainName,
|
||||
hash: hash,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'x-auth-token': authToken,
|
||||
'x-token-address': tokenAddress,
|
||||
},
|
||||
},
|
||||
);
|
||||
return data as BindDnsDomainV4Response;
|
||||
} catch (e: any) {
|
||||
if (isTokenExpired(e)) {
|
||||
showTokenExpiredHint();
|
||||
throw new Error('Token expired');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IsVipResponse {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
is_vip: boolean;
|
||||
vip_expire_time?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export async function isVip(
|
||||
tokenAddress: string,
|
||||
authToken: string,
|
||||
): Promise<IsVipResponse> {
|
||||
try {
|
||||
const client = createClient();
|
||||
const { data } = await client.get<IsVipResponse>('/is_vip', {
|
||||
headers: {
|
||||
'x-auth-token': authToken,
|
||||
'x-token-address': tokenAddress,
|
||||
},
|
||||
});
|
||||
return data as IsVipResponse;
|
||||
} catch (e: any) {
|
||||
if (isTokenExpired(e)) {
|
||||
showTokenExpiredHint();
|
||||
throw new Error('Token expired');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// CAR Export API
|
||||
const CAR_API_BASE = process.env.CAR_API_BASE || process.env.PINME_API_BASE || 'http://ipfs-proxy.opena.chat/api/v3';
|
||||
const CAR_API_BASE =
|
||||
process.env.CAR_API_BASE ||
|
||||
process.env.PINME_API_BASE ||
|
||||
'http://ipfs-proxy.opena.chat/api/v3';
|
||||
|
||||
function createCarClient(): AxiosInstance {
|
||||
let headers = {};
|
||||
@@ -137,7 +298,10 @@ export interface CarExportStatusResponse {
|
||||
};
|
||||
}
|
||||
|
||||
export async function requestCarExport(cid: string, uid: string): Promise<CarExportResponse['data']> {
|
||||
export async function requestCarExport(
|
||||
cid: string,
|
||||
uid: string,
|
||||
): Promise<CarExportResponse['data']> {
|
||||
try {
|
||||
const client = createCarClient();
|
||||
// Use POST method as shown in the example
|
||||
@@ -152,6 +316,10 @@ export async function requestCarExport(cid: string, uid: string): Promise<CarExp
|
||||
}
|
||||
throw new Error(data?.msg || 'Failed to request CAR export');
|
||||
} catch (e: any) {
|
||||
if (isTokenExpired(e)) {
|
||||
showTokenExpiredHint();
|
||||
throw new Error('Token expired');
|
||||
}
|
||||
if (e.response?.data?.msg) {
|
||||
throw new Error(e.response.data.msg);
|
||||
}
|
||||
@@ -159,24 +327,40 @@ export async function requestCarExport(cid: string, uid: string): Promise<CarExp
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkCarExportStatus(taskId: string): Promise<CarExportStatusResponse['data']> {
|
||||
export interface BindDnsDomainV4Response {
|
||||
code: number;
|
||||
msg: string;
|
||||
data?: {
|
||||
domain_name: string;
|
||||
hash: string;
|
||||
bind_time?: number;
|
||||
};
|
||||
}
|
||||
export async function checkCarExportStatus(
|
||||
taskId: string,
|
||||
): Promise<CarExportStatusResponse['data']> {
|
||||
try {
|
||||
const client = createCarClient();
|
||||
const { data } = await client.get<CarExportStatusResponse>('/car/export/status', {
|
||||
params: {
|
||||
task_id: taskId,
|
||||
const { data } = await client.get<CarExportStatusResponse>(
|
||||
'/car/export/status',
|
||||
{
|
||||
params: {
|
||||
task_id: taskId,
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
if (data?.code === 200 && data?.data) {
|
||||
return data.data;
|
||||
}
|
||||
throw new Error(data?.msg || 'Failed to check export status');
|
||||
} catch (e: any) {
|
||||
if (isTokenExpired(e)) {
|
||||
showTokenExpiredHint();
|
||||
throw new Error('Token expired');
|
||||
}
|
||||
if (e.response?.data?.msg) {
|
||||
throw new Error(e.response.data.msg);
|
||||
}
|
||||
throw new Error(`Failed to check export status: ${e?.message || e}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pinme",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.5",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user