@coswic/aipay 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/package.json +1 -1
- package/dist/agent-guide.d.ts +0 -18
- package/dist/agent-guide.js +0 -60
- package/dist/args.d.ts +0 -15
- package/dist/args.js +0 -47
- package/dist/auth-store.d.ts +0 -23
- package/dist/auth-store.js +0 -113
- package/dist/bin.d.ts +0 -2
- package/dist/bin.js +0 -8
- package/dist/browser.d.ts +0 -1
- package/dist/browser.js +0 -18
- package/dist/commands/apps.d.ts +0 -15
- package/dist/commands/apps.js +0 -53
- package/dist/commands/doctor.d.ts +0 -17
- package/dist/commands/doctor.js +0 -153
- package/dist/commands/init.d.ts +0 -18
- package/dist/commands/init.js +0 -252
- package/dist/commands/login.d.ts +0 -25
- package/dist/commands/login.js +0 -149
- package/dist/commands/session.d.ts +0 -6
- package/dist/commands/session.js +0 -63
- package/dist/commands/status.d.ts +0 -3
- package/dist/commands/status.js +0 -24
- package/dist/commands/test.d.ts +0 -12
- package/dist/commands/test.js +0 -159
- package/dist/commands/unlink.d.ts +0 -5
- package/dist/commands/unlink.js +0 -70
- package/dist/detect.d.ts +0 -27
- package/dist/detect.js +0 -79
- package/dist/developer-api.d.ts +0 -14
- package/dist/developer-api.js +0 -74
- package/dist/gate.d.ts +0 -13
- package/dist/gate.js +0 -29
- package/dist/index.d.ts +0 -22
- package/dist/index.js +0 -22
- package/dist/loopback.d.ts +0 -37
- package/dist/loopback.js +0 -95
- package/dist/main.d.ts +0 -5
- package/dist/main.js +0 -90
- package/dist/output.d.ts +0 -36
- package/dist/output.js +0 -52
- package/dist/project.d.ts +0 -50
- package/dist/project.js +0 -107
- package/dist/remote.d.ts +0 -21
- package/dist/remote.js +0 -41
- package/dist/setup-events.d.ts +0 -3
- package/dist/setup-events.js +0 -28
- package/dist/templates.d.ts +0 -24
- package/dist/templates.js +0 -226
- package/dist/version.d.ts +0 -1
- package/dist/version.js +0 -10
package/README.md
CHANGED
|
@@ -22,6 +22,21 @@ Every command accepts `--json` (one JSON document on stdout) and `--dir <path>`.
|
|
|
22
22
|
|
|
23
23
|
Exit codes: `0` ok · `1` a problem you can fix locally · `2` usage · `3` network/auth.
|
|
24
24
|
|
|
25
|
+
## Signing in from a remote machine
|
|
26
|
+
|
|
27
|
+
`aipay login` opens a browser on this machine (loopback PKCE). On an SSH box, a container or
|
|
28
|
+
CI there is no browser, so use the device flow (RFC 8628):
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
aipay login --device --api https://api.example
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The CLI prints a URL and a short code. Open the URL in a browser on any device (your laptop,
|
|
35
|
+
your phone), enter the code, sign in and confirm — the CLI is polling and finishes on its
|
|
36
|
+
own. Nothing is written to the project; tokens go to the keychain or the 0600 file exactly
|
|
37
|
+
like the browser flow. With `--json`, the URL and code are echoed on stderr as
|
|
38
|
+
`device_url …` / `user_code …` so an agent can show them to you.
|
|
39
|
+
|
|
25
40
|
## Pilot gate
|
|
26
41
|
|
|
27
42
|
During the closed pilot, non-public AI-Pay paths answer `404` with an empty body unless the
|
|
@@ -45,6 +60,13 @@ src/aipay-routes.ts # framework wiring (or app/auth/aipay/*/route.ts for Next.
|
|
|
45
60
|
|
|
46
61
|
Access and refresh tokens obtained by `aipay test` live in memory only.
|
|
47
62
|
|
|
63
|
+
Your own developer tokens (`aipay login`) never touch the project either. They go to the
|
|
64
|
+
macOS Keychain (`security`), the Linux keyring (`secret-tool`), or on Windows a file
|
|
65
|
+
encrypted with DPAPI for your user account (`%APPDATA%\aipay\credentials.dpapi`, via
|
|
66
|
+
Windows PowerShell) — and only when none of those exist, a `credentials.json` readable by
|
|
67
|
+
you alone (mode 0600), with a warning at login. `AIPAY_TOKEN_STORE=file` forces the plain
|
|
68
|
+
file; `AIPAY_CONFIG_DIR` moves the directory.
|
|
69
|
+
|
|
48
70
|
## Development
|
|
49
71
|
|
|
50
72
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coswic/aipay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "AI-Pay CLI — init (link an app + scaffold the integration), doctor, test (real sign-in + first receipt), status, unlink. For developers and their coding agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/dist/agent-guide.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `aipay-agent.md`: the file a coding agent reads first. Header = the facts of THIS
|
|
3
|
-
* project (app, endpoints, what was scaffolded, what "done" means); body = the AI-Pay
|
|
4
|
-
* developer docs fetched from the API (`/docs.md?lang=en`), so the agent does not need
|
|
5
|
-
* network access — or the gate token — to read them later.
|
|
6
|
-
*
|
|
7
|
-
* Never contains tokens. If the docs are gated (404 with empty body) the file still gets
|
|
8
|
-
* written with the header and a pointer, and the CLI warns.
|
|
9
|
-
*/
|
|
10
|
-
import type { ProjectConfig } from './project.js';
|
|
11
|
-
export declare const AGENT_GUIDE_FILE = "aipay-agent.md";
|
|
12
|
-
export interface GuideInput {
|
|
13
|
-
project: ProjectConfig;
|
|
14
|
-
scaffolded: string[];
|
|
15
|
-
docsMarkdown: string | null;
|
|
16
|
-
docsUrl: string;
|
|
17
|
-
}
|
|
18
|
-
export declare function agentGuide(input: GuideInput): string;
|
package/dist/agent-guide.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
export const AGENT_GUIDE_FILE = 'aipay-agent.md';
|
|
2
|
-
export function agentGuide(input) {
|
|
3
|
-
const p = input.project;
|
|
4
|
-
const files = input.scaffolded.length
|
|
5
|
-
? input.scaffolded.map((f) => `- \`${f}\``).join('\n')
|
|
6
|
-
: '- (none — the project had them already)';
|
|
7
|
-
const head = `# AI-Pay integration guide for this project
|
|
8
|
-
|
|
9
|
-
> Written by \`aipay init\` on ${p.linked_at}. Regenerate with \`aipay init --app ${p.app.client_id}\`. Safe to commit: it contains no secrets.
|
|
10
|
-
|
|
11
|
-
## This project
|
|
12
|
-
|
|
13
|
-
| | |
|
|
14
|
-
|---|---|
|
|
15
|
-
| App | ${p.app.name ?? '(unnamed)'} |
|
|
16
|
-
| client_id | \`${p.app.client_id}\` (public client, PKCE S256, no secret) |
|
|
17
|
-
| Authorization server | ${p.auth_origin} |
|
|
18
|
-
| API base | ${p.api_base} |
|
|
19
|
-
| Registered callback | \`${p.callback}\` (for 127.0.0.1 the port is not compared, so any local port works) |
|
|
20
|
-
| Local config | \`.aipay/project.json\` (commit it), \`.env.local\` (do not commit) |
|
|
21
|
-
|
|
22
|
-
Files scaffolded by the CLI (edit freely; the CLI never overwrites existing files):
|
|
23
|
-
${files}
|
|
24
|
-
|
|
25
|
-
## Definition of done
|
|
26
|
-
|
|
27
|
-
1. \`aipay doctor\` reports no problems.
|
|
28
|
-
2. The app's dev server serves the start route and the callback route above.
|
|
29
|
-
3. \`aipay test\` completes: a real sign-in through the AI-Pay authorization server and
|
|
30
|
-
one chat completion whose response carries a receipt (\`aipay.request_id\`).
|
|
31
|
-
4. Typecheck/build pass. Do not commit automatically and do not deploy.
|
|
32
|
-
|
|
33
|
-
## Rules for agents
|
|
34
|
-
|
|
35
|
-
- Read the docs below before changing the integration; they are the source of truth.
|
|
36
|
-
- The pilot gate token comes only from the \`AIPAY_GATE_TOKEN\` environment variable.
|
|
37
|
-
Never write it into code, config files, or version control.
|
|
38
|
-
- Never request \`offline_access\` unless the product needs background renewal.
|
|
39
|
-
- Key user accounts on \`sub\` from the userinfo endpoint, never on email.
|
|
40
|
-
|
|
41
|
-
`;
|
|
42
|
-
const body = input.docsMarkdown
|
|
43
|
-
? `---
|
|
44
|
-
|
|
45
|
-
# AI-Pay developer documentation
|
|
46
|
-
|
|
47
|
-
_Fetched from ${input.docsUrl} by \`aipay init\`._
|
|
48
|
-
|
|
49
|
-
${input.docsMarkdown.trim()}
|
|
50
|
-
`
|
|
51
|
-
: `---
|
|
52
|
-
|
|
53
|
-
# AI-Pay developer documentation
|
|
54
|
-
|
|
55
|
-
The docs could not be fetched when this file was written (see the \`aipay init\` output).
|
|
56
|
-
Read them at ${input.docsUrl} — during the pilot send the header \`X-AIPay-Gate\` with the
|
|
57
|
-
value of \`AIPAY_GATE_TOKEN\`, or run \`aipay init\` again with the variable set.
|
|
58
|
-
`;
|
|
59
|
-
return head + body;
|
|
60
|
-
}
|
package/dist/args.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tiny argv parser — zero deps on purpose (the CLI is installed by coding agents in
|
|
3
|
-
* arbitrary projects; every dependency is attack surface and install time).
|
|
4
|
-
*
|
|
5
|
-
* Supports `--flag`, `--flag value`, `--flag=value`, `--no-flag`, and positionals.
|
|
6
|
-
* Every flag is a string or boolean; commands coerce what they need.
|
|
7
|
-
*/
|
|
8
|
-
export interface ParsedArgs {
|
|
9
|
-
command: string | null;
|
|
10
|
-
positionals: string[];
|
|
11
|
-
flags: Record<string, string | boolean>;
|
|
12
|
-
}
|
|
13
|
-
export declare function parseArgs(argv: string[]): ParsedArgs;
|
|
14
|
-
export declare function flagString(flags: ParsedArgs['flags'], name: string): string | null;
|
|
15
|
-
export declare function flagBool(flags: ParsedArgs['flags'], name: string, dflt?: boolean): boolean;
|
package/dist/args.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
export function parseArgs(argv) {
|
|
2
|
-
const out = { command: null, positionals: [], flags: {} };
|
|
3
|
-
for (let i = 0; i < argv.length; i++) {
|
|
4
|
-
const a = argv[i] ?? '';
|
|
5
|
-
if (a === '--') {
|
|
6
|
-
out.positionals.push(...argv.slice(i + 1));
|
|
7
|
-
break;
|
|
8
|
-
}
|
|
9
|
-
if (a.startsWith('--')) {
|
|
10
|
-
const body = a.slice(2);
|
|
11
|
-
const eq = body.indexOf('=');
|
|
12
|
-
if (eq >= 0) {
|
|
13
|
-
out.flags[body.slice(0, eq)] = body.slice(eq + 1);
|
|
14
|
-
continue;
|
|
15
|
-
}
|
|
16
|
-
if (body.startsWith('no-')) {
|
|
17
|
-
out.flags[body.slice(3)] = false;
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
const next = argv[i + 1];
|
|
21
|
-
if (next !== undefined && !next.startsWith('--')) {
|
|
22
|
-
out.flags[body] = next;
|
|
23
|
-
i++;
|
|
24
|
-
}
|
|
25
|
-
else
|
|
26
|
-
out.flags[body] = true;
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
if (out.command === null)
|
|
30
|
-
out.command = a;
|
|
31
|
-
else
|
|
32
|
-
out.positionals.push(a);
|
|
33
|
-
}
|
|
34
|
-
return out;
|
|
35
|
-
}
|
|
36
|
-
export function flagString(flags, name) {
|
|
37
|
-
const v = flags[name];
|
|
38
|
-
return typeof v === 'string' && v !== '' ? v : null;
|
|
39
|
-
}
|
|
40
|
-
export function flagBool(flags, name, dflt = false) {
|
|
41
|
-
const v = flags[name];
|
|
42
|
-
if (typeof v === 'boolean')
|
|
43
|
-
return v;
|
|
44
|
-
if (typeof v === 'string')
|
|
45
|
-
return v !== 'false' && v !== '0';
|
|
46
|
-
return dflt;
|
|
47
|
-
}
|
package/dist/auth-store.d.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
export interface StoredAuth {
|
|
2
|
-
api_base: string;
|
|
3
|
-
auth_origin: string;
|
|
4
|
-
client_id: string;
|
|
5
|
-
access_token: string;
|
|
6
|
-
refresh_token: string | null;
|
|
7
|
-
/** epoch ms */
|
|
8
|
-
expires_at: number;
|
|
9
|
-
scope: string;
|
|
10
|
-
email: string | null;
|
|
11
|
-
sub: string;
|
|
12
|
-
}
|
|
13
|
-
export interface AuthStore {
|
|
14
|
-
kind: 'keychain' | 'file';
|
|
15
|
-
load(): StoredAuth | null;
|
|
16
|
-
save(auth: StoredAuth): void;
|
|
17
|
-
clear(): void;
|
|
18
|
-
/** Human description of where tokens are kept (for `whoami`/`login` output). */
|
|
19
|
-
where: string;
|
|
20
|
-
}
|
|
21
|
-
export declare function configDir(env?: NodeJS.ProcessEnv): string;
|
|
22
|
-
export declare function createAuthStore(env?: NodeJS.ProcessEnv): AuthStore;
|
|
23
|
-
export declare function fileStore(dir: string): AuthStore;
|
package/dist/auth-store.js
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Where the developer's management tokens live between commands.
|
|
3
|
-
*
|
|
4
|
-
* Order of preference (decision D5, 2026-09-04): the OS keychain when a CLI for it exists
|
|
5
|
-
* (macOS `security`, Linux `secret-tool`), else a 0600 file under the user's config dir
|
|
6
|
-
* with a one-line warning at login. Tokens are never written into the project directory.
|
|
7
|
-
*/
|
|
8
|
-
import { spawnSync } from 'node:child_process';
|
|
9
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
10
|
-
import { homedir } from 'node:os';
|
|
11
|
-
import { join } from 'node:path';
|
|
12
|
-
const SERVICE = 'aipay-cli';
|
|
13
|
-
export function configDir(env = process.env) {
|
|
14
|
-
if (env.AIPAY_CONFIG_DIR)
|
|
15
|
-
return env.AIPAY_CONFIG_DIR;
|
|
16
|
-
const base = process.platform === 'win32'
|
|
17
|
-
? (env.APPDATA ?? join(homedir(), 'AppData', 'Roaming'))
|
|
18
|
-
: (env.XDG_CONFIG_HOME ?? join(homedir(), '.config'));
|
|
19
|
-
return join(base, 'aipay');
|
|
20
|
-
}
|
|
21
|
-
function has(bin) {
|
|
22
|
-
const r = spawnSync(process.platform === 'win32' ? 'where' : 'which', [bin], { stdio: 'ignore' });
|
|
23
|
-
return r.status === 0;
|
|
24
|
-
}
|
|
25
|
-
export function createAuthStore(env = process.env) {
|
|
26
|
-
if (env.AIPAY_TOKEN_STORE !== 'file') {
|
|
27
|
-
if (process.platform === 'darwin' && has('security'))
|
|
28
|
-
return macKeychain();
|
|
29
|
-
if (process.platform === 'linux' && has('secret-tool'))
|
|
30
|
-
return secretTool();
|
|
31
|
-
}
|
|
32
|
-
return fileStore(configDir(env));
|
|
33
|
-
}
|
|
34
|
-
function macKeychain() {
|
|
35
|
-
const account = 'developer';
|
|
36
|
-
return {
|
|
37
|
-
kind: 'keychain',
|
|
38
|
-
where: 'macOS Keychain (service aipay-cli)',
|
|
39
|
-
load() {
|
|
40
|
-
const r = spawnSync('security', ['find-generic-password', '-a', account, '-s', SERVICE, '-w'], { encoding: 'utf8' });
|
|
41
|
-
if (r.status !== 0)
|
|
42
|
-
return null;
|
|
43
|
-
return parse(r.stdout.trim());
|
|
44
|
-
},
|
|
45
|
-
save(auth) {
|
|
46
|
-
const r = spawnSync('security', ['add-generic-password', '-U', '-a', account, '-s', SERVICE, '-w', JSON.stringify(auth)], { stdio: 'ignore' });
|
|
47
|
-
if (r.status !== 0)
|
|
48
|
-
throw new Error('could not write to the macOS Keychain');
|
|
49
|
-
},
|
|
50
|
-
clear() {
|
|
51
|
-
spawnSync('security', ['delete-generic-password', '-a', account, '-s', SERVICE], {
|
|
52
|
-
stdio: 'ignore',
|
|
53
|
-
});
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
function secretTool() {
|
|
58
|
-
const attrs = ['service', SERVICE, 'account', 'developer'];
|
|
59
|
-
return {
|
|
60
|
-
kind: 'keychain',
|
|
61
|
-
where: 'the system keyring (secret-tool, service aipay-cli)',
|
|
62
|
-
load() {
|
|
63
|
-
const r = spawnSync('secret-tool', ['lookup', ...attrs], { encoding: 'utf8' });
|
|
64
|
-
if (r.status !== 0 || !r.stdout)
|
|
65
|
-
return null;
|
|
66
|
-
return parse(r.stdout.trim());
|
|
67
|
-
},
|
|
68
|
-
save(auth) {
|
|
69
|
-
const r = spawnSync('secret-tool', ['store', '--label', 'AI-Pay CLI', ...attrs], {
|
|
70
|
-
input: JSON.stringify(auth),
|
|
71
|
-
stdio: ['pipe', 'ignore', 'ignore'],
|
|
72
|
-
});
|
|
73
|
-
if (r.status !== 0)
|
|
74
|
-
throw new Error('could not write to the system keyring');
|
|
75
|
-
},
|
|
76
|
-
clear() {
|
|
77
|
-
spawnSync('secret-tool', ['clear', ...attrs], { stdio: 'ignore' });
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
export function fileStore(dir) {
|
|
82
|
-
const file = join(dir, 'credentials.json');
|
|
83
|
-
return {
|
|
84
|
-
kind: 'file',
|
|
85
|
-
where: `${file} (mode 0600)`,
|
|
86
|
-
load() {
|
|
87
|
-
if (!existsSync(file))
|
|
88
|
-
return null;
|
|
89
|
-
return parse(readFileSync(file, 'utf8'));
|
|
90
|
-
},
|
|
91
|
-
save(auth) {
|
|
92
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
93
|
-
writeFileSync(file, `${JSON.stringify(auth, null, 2)}\n`, { mode: 0o600 });
|
|
94
|
-
chmodSync(file, 0o600);
|
|
95
|
-
},
|
|
96
|
-
clear() {
|
|
97
|
-
rmSync(file, { force: true });
|
|
98
|
-
},
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
function parse(raw) {
|
|
102
|
-
try {
|
|
103
|
-
const v = JSON.parse(raw);
|
|
104
|
-
if (typeof v.access_token !== 'string' ||
|
|
105
|
-
typeof v.api_base !== 'string' ||
|
|
106
|
-
typeof v.client_id !== 'string')
|
|
107
|
-
return null;
|
|
108
|
-
return v;
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
}
|
package/dist/bin.d.ts
DELETED
package/dist/bin.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { main } from './main.js';
|
|
3
|
-
main(process.argv.slice(2)).then((code) => {
|
|
4
|
-
process.exitCode = code;
|
|
5
|
-
}, (err) => {
|
|
6
|
-
process.stderr.write(`${err instanceof Error ? (err.stack ?? err.message) : String(err)}\n`);
|
|
7
|
-
process.exitCode = 1;
|
|
8
|
-
});
|
package/dist/browser.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function openBrowser(url: string, platform?: NodeJS.Platform): boolean;
|
package/dist/browser.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/** Open a URL in the user's default browser without depending on the `open` package. */
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
3
|
-
export function openBrowser(url, platform = process.platform) {
|
|
4
|
-
const [cmd, args] = platform === 'darwin'
|
|
5
|
-
? ['open', [url]]
|
|
6
|
-
: platform === 'win32'
|
|
7
|
-
? ['cmd', ['/c', 'start', '', url.replace(/&/g, '^&')]]
|
|
8
|
-
: ['xdg-open', [url]];
|
|
9
|
-
try {
|
|
10
|
-
const child = spawn(cmd, args, { stdio: 'ignore', detached: true });
|
|
11
|
-
child.on('error', () => { });
|
|
12
|
-
child.unref();
|
|
13
|
-
return true;
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
}
|
package/dist/commands/apps.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { type ParsedArgs } from '../args.js';
|
|
2
|
-
import { type DeveloperApi } from '../developer-api.js';
|
|
3
|
-
import { type Reporter } from '../output.js';
|
|
4
|
-
export interface CreatedApp {
|
|
5
|
-
app_id: string;
|
|
6
|
-
client_id: string;
|
|
7
|
-
redirect_uris: string[];
|
|
8
|
-
replayed?: boolean;
|
|
9
|
-
}
|
|
10
|
-
export declare function createApp(api: DeveloperApi, input: {
|
|
11
|
-
name: string;
|
|
12
|
-
callback: string;
|
|
13
|
-
key?: string | null;
|
|
14
|
-
}): Promise<CreatedApp>;
|
|
15
|
-
export declare function runApps(args: ParsedArgs, out: Reporter, env?: NodeJS.ProcessEnv): Promise<number>;
|
package/dist/commands/apps.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `aipay apps list [--json]`
|
|
3
|
-
* `aipay apps create --name <name> [--callback <uri>] [--key <idempotency key>] [--json]`
|
|
4
|
-
*
|
|
5
|
-
* Uses the developer's management token. Creation sends an Idempotency-Key so a retried
|
|
6
|
-
* command never makes a second app.
|
|
7
|
-
*/
|
|
8
|
-
import { randomUUID } from 'node:crypto';
|
|
9
|
-
import { flagString } from '../args.js';
|
|
10
|
-
import { createAuthStore } from '../auth-store.js';
|
|
11
|
-
import { errorMessage, openDeveloperApi } from '../developer-api.js';
|
|
12
|
-
import { CliError, EXIT } from '../output.js';
|
|
13
|
-
import { DEFAULT_CALLBACK } from './init.js';
|
|
14
|
-
export async function createApp(api, input) {
|
|
15
|
-
const key = input.key ?? `aipay-cli-${randomUUID()}`;
|
|
16
|
-
const res = await api.request('POST', '/v1/developer/apps', {
|
|
17
|
-
body: { name: input.name, redirect_uris: [input.callback] },
|
|
18
|
-
headers: { 'idempotency-key': key },
|
|
19
|
-
});
|
|
20
|
-
if (res.status !== 201 && res.status !== 200)
|
|
21
|
-
throw new CliError('create_failed', `could not create the app: ${errorMessage(res.body, res.status)}`, EXIT.remote);
|
|
22
|
-
return res.body;
|
|
23
|
-
}
|
|
24
|
-
export async function runApps(args, out, env = process.env) {
|
|
25
|
-
const sub = args.positionals[0] ?? 'list';
|
|
26
|
-
const store = createAuthStore(env);
|
|
27
|
-
const api = await openDeveloperApi(store, env);
|
|
28
|
-
if (sub === 'list') {
|
|
29
|
-
const res = await api.request('GET', '/v1/developer/apps');
|
|
30
|
-
if (res.status !== 200)
|
|
31
|
-
throw new CliError('remote', `GET /v1/developer/apps → ${errorMessage(res.body, res.status)}`, EXIT.remote);
|
|
32
|
-
const apps = Array.isArray(res.body) ? res.body : (res.body.data ?? res.body.apps ?? []);
|
|
33
|
-
if (!apps.length)
|
|
34
|
-
out.line('info', 'no apps yet — `aipay apps create --name "My app"`');
|
|
35
|
-
for (const a of apps)
|
|
36
|
-
out.line('info', `${String(a.name ?? '?')} ${String(a.id ?? a.app_id ?? a.client_id ?? '')} ${String(a.status ?? '')}`);
|
|
37
|
-
out.result({ ok: true, apps });
|
|
38
|
-
return EXIT.ok;
|
|
39
|
-
}
|
|
40
|
-
if (sub === 'create') {
|
|
41
|
-
const name = flagString(args.flags, 'name');
|
|
42
|
-
if (!name)
|
|
43
|
-
throw new CliError('usage', 'missing --name <app name>', EXIT.usage);
|
|
44
|
-
const callback = flagString(args.flags, 'callback') ?? DEFAULT_CALLBACK;
|
|
45
|
-
const created = await createApp(api, { name, callback, key: flagString(args.flags, 'key') });
|
|
46
|
-
out.line('ok', `${created.replayed ? 'already created' : 'created'} "${name}" — client_id ${created.client_id}`);
|
|
47
|
-
out.kv('callback', created.redirect_uris.join(', '));
|
|
48
|
-
out.line('info', `next: aipay init --app ${created.client_id}`);
|
|
49
|
-
out.result({ ok: true, ...created, name });
|
|
50
|
-
return EXIT.ok;
|
|
51
|
-
}
|
|
52
|
-
throw new CliError('usage', `unknown apps subcommand "${sub}" (list | create)`, EXIT.usage);
|
|
53
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { type ParsedArgs } from '../args.js';
|
|
2
|
-
import { type Level, type Reporter } from '../output.js';
|
|
3
|
-
import { type Remote } from '../remote.js';
|
|
4
|
-
export interface Check {
|
|
5
|
-
id: string;
|
|
6
|
-
level: Level;
|
|
7
|
-
text: string;
|
|
8
|
-
}
|
|
9
|
-
export interface DoctorDeps {
|
|
10
|
-
remote?: Remote;
|
|
11
|
-
env?: NodeJS.ProcessEnv;
|
|
12
|
-
nodeVersion?: string;
|
|
13
|
-
}
|
|
14
|
-
export declare function runDoctor(args: ParsedArgs, out: Reporter, deps?: DoctorDeps): Promise<number>;
|
|
15
|
-
export declare function collectChecks(dir: string, opts: {
|
|
16
|
-
offline: boolean;
|
|
17
|
-
}, deps: DoctorDeps, env: NodeJS.ProcessEnv): Promise<Check[]>;
|
package/dist/commands/doctor.js
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `aipay doctor [--dir] [--json] [--offline]` — checks the link, the local files, the
|
|
3
|
-
* SDK install, and (unless --offline) reachability of the authorization server and the
|
|
4
|
-
* API. Exit 0 = nothing to fix, 1 = at least one problem.
|
|
5
|
-
*/
|
|
6
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
-
import { join, resolve } from 'node:path';
|
|
8
|
-
import { flagBool, flagString } from '../args.js';
|
|
9
|
-
import { detectProject } from '../detect.js';
|
|
10
|
-
import { GATE_ENV } from '../gate.js';
|
|
11
|
-
import { parseCallback } from '../loopback.js';
|
|
12
|
-
import { EXIT } from '../output.js';
|
|
13
|
-
import { ENV_FILE, ENV_KEYS, readProject } from '../project.js';
|
|
14
|
-
import { createRemote, docsUrl, GATE_HINT } from '../remote.js';
|
|
15
|
-
export async function runDoctor(args, out, deps = {}) {
|
|
16
|
-
const dir = resolve(flagString(args.flags, 'dir') ?? '.');
|
|
17
|
-
const env = deps.env ?? process.env;
|
|
18
|
-
const checks = await collectChecks(dir, { offline: flagBool(args.flags, 'offline') }, deps, env);
|
|
19
|
-
for (const c of checks)
|
|
20
|
-
out.line(c.level, c.text);
|
|
21
|
-
const problems = checks.filter((c) => c.level === 'bad');
|
|
22
|
-
out.blank();
|
|
23
|
-
out.line(problems.length ? 'bad' : 'ok', problems.length ? `${problems.length} problem(s)` : 'all good');
|
|
24
|
-
out.result({ ok: problems.length === 0, dir, checks });
|
|
25
|
-
return problems.length ? EXIT.problem : EXIT.ok;
|
|
26
|
-
}
|
|
27
|
-
export async function collectChecks(dir, opts, deps, env) {
|
|
28
|
-
const checks = [];
|
|
29
|
-
const major = Number((deps.nodeVersion ?? process.versions.node).split('.')[0]);
|
|
30
|
-
checks.push(major >= 20
|
|
31
|
-
? { id: 'node', level: 'ok', text: `Node ${deps.nodeVersion ?? process.versions.node}` }
|
|
32
|
-
: {
|
|
33
|
-
id: 'node',
|
|
34
|
-
level: 'bad',
|
|
35
|
-
text: `Node ${deps.nodeVersion ?? process.versions.node} — the SDK needs 20+`,
|
|
36
|
-
});
|
|
37
|
-
let project = null;
|
|
38
|
-
try {
|
|
39
|
-
project = readProject(dir);
|
|
40
|
-
}
|
|
41
|
-
catch (e) {
|
|
42
|
-
checks.push({ id: 'project', level: 'bad', text: e.message });
|
|
43
|
-
return checks;
|
|
44
|
-
}
|
|
45
|
-
if (!project) {
|
|
46
|
-
checks.push({
|
|
47
|
-
id: 'project',
|
|
48
|
-
level: 'bad',
|
|
49
|
-
text: 'not linked — run `aipay init --app <client_id> --auth <origin> --api <base>`',
|
|
50
|
-
});
|
|
51
|
-
return checks;
|
|
52
|
-
}
|
|
53
|
-
checks.push({
|
|
54
|
-
id: 'project',
|
|
55
|
-
level: 'ok',
|
|
56
|
-
text: `linked to ${project.app.name ?? project.app.client_id} (.aipay/project.json)`,
|
|
57
|
-
});
|
|
58
|
-
try {
|
|
59
|
-
const t = parseCallback(project.callback);
|
|
60
|
-
checks.push({
|
|
61
|
-
id: 'callback',
|
|
62
|
-
level: 'ok',
|
|
63
|
-
text: `callback ${project.callback} (loopback, port ${t.port})`,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
catch (e) {
|
|
67
|
-
checks.push({ id: 'callback', level: 'bad', text: `callback: ${e.message}` });
|
|
68
|
-
}
|
|
69
|
-
const envPath = join(dir, ENV_FILE);
|
|
70
|
-
if (!existsSync(envPath))
|
|
71
|
-
checks.push({ id: 'env', level: 'bad', text: `${ENV_FILE} missing — rerun \`aipay init\`` });
|
|
72
|
-
else {
|
|
73
|
-
const content = readFileSync(envPath, 'utf8');
|
|
74
|
-
const want = {
|
|
75
|
-
[ENV_KEYS.clientId]: project.app.client_id,
|
|
76
|
-
[ENV_KEYS.authOrigin]: project.auth_origin,
|
|
77
|
-
[ENV_KEYS.apiBase]: project.api_base,
|
|
78
|
-
[ENV_KEYS.callback]: project.callback,
|
|
79
|
-
};
|
|
80
|
-
const wrong = Object.entries(want)
|
|
81
|
-
.filter(([k, v]) => !new RegExp(`^\\s*(?:export\\s+)?${k}\\s*=\\s*["']?${escapeRe(v)}["']?\\s*$`, 'm').test(content))
|
|
82
|
-
.map(([k]) => k);
|
|
83
|
-
checks.push(wrong.length
|
|
84
|
-
? {
|
|
85
|
-
id: 'env',
|
|
86
|
-
level: 'bad',
|
|
87
|
-
text: `${ENV_FILE} out of sync: ${wrong.join(', ')} — rerun \`aipay init\``,
|
|
88
|
-
}
|
|
89
|
-
: { id: 'env', level: 'ok', text: `${ENV_FILE} has the four AIPAY_* values` });
|
|
90
|
-
}
|
|
91
|
-
const gi = join(dir, '.gitignore');
|
|
92
|
-
if (existsSync(gi) &&
|
|
93
|
-
!readFileSync(gi, 'utf8')
|
|
94
|
-
.split(/\r?\n/)
|
|
95
|
-
.some((l) => l.trim() === ENV_FILE))
|
|
96
|
-
checks.push({ id: 'gitignore', level: 'warn', text: `.gitignore does not list ${ENV_FILE}` });
|
|
97
|
-
const det = detectProject(dir);
|
|
98
|
-
if (det.hasPackageJson) {
|
|
99
|
-
const installed = det.sdkInstalled && existsSync(join(dir, 'node_modules', '@coswic', 'aipay-sdk'));
|
|
100
|
-
checks.push(installed
|
|
101
|
-
? { id: 'sdk', level: 'ok', text: '@coswic/aipay-sdk installed' }
|
|
102
|
-
: det.sdkInstalled
|
|
103
|
-
? {
|
|
104
|
-
id: 'sdk',
|
|
105
|
-
level: 'bad',
|
|
106
|
-
text: '@coswic/aipay-sdk is in package.json but not installed — run your package manager install',
|
|
107
|
-
}
|
|
108
|
-
: {
|
|
109
|
-
id: 'sdk',
|
|
110
|
-
level: 'bad',
|
|
111
|
-
text: '@coswic/aipay-sdk not a dependency — rerun `aipay init` (or add it yourself)',
|
|
112
|
-
});
|
|
113
|
-
for (const f of project.scaffold.files)
|
|
114
|
-
if (!existsSync(join(dir, f)))
|
|
115
|
-
checks.push({ id: 'scaffold', level: 'warn', text: `scaffolded file removed: ${f}` });
|
|
116
|
-
}
|
|
117
|
-
if (!env[GATE_ENV])
|
|
118
|
-
checks.push({
|
|
119
|
-
id: 'gate',
|
|
120
|
-
level: 'info',
|
|
121
|
-
text: `${GATE_ENV} not set (fine after the pilot; required during it)`,
|
|
122
|
-
});
|
|
123
|
-
if (!opts.offline) {
|
|
124
|
-
const remote = deps.remote ?? createRemote(env);
|
|
125
|
-
const oidc = await remote.probe(new URL('/.well-known/openid-configuration', project.auth_origin).toString());
|
|
126
|
-
checks.push(oidc.ok
|
|
127
|
-
? {
|
|
128
|
-
id: 'auth',
|
|
129
|
-
level: 'ok',
|
|
130
|
-
text: `authorization server reachable (${project.auth_origin})`,
|
|
131
|
-
}
|
|
132
|
-
: {
|
|
133
|
-
id: 'auth',
|
|
134
|
-
level: 'bad',
|
|
135
|
-
text: `authorization server ${project.auth_origin}: ${oidc.error ?? `HTTP ${oidc.status}`}`,
|
|
136
|
-
});
|
|
137
|
-
const api = await remote.probe(docsUrl(project.api_base, 'en'));
|
|
138
|
-
if (api.ok)
|
|
139
|
-
checks.push({ id: 'api', level: 'ok', text: `API reachable (${project.api_base})` });
|
|
140
|
-
else if (api.gated)
|
|
141
|
-
checks.push({ id: 'api', level: 'bad', text: `API ${project.api_base}: ${GATE_HINT}` });
|
|
142
|
-
else
|
|
143
|
-
checks.push({
|
|
144
|
-
id: 'api',
|
|
145
|
-
level: 'bad',
|
|
146
|
-
text: `API ${project.api_base}: ${api.error ?? `HTTP ${api.status}`}`,
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
return checks;
|
|
150
|
-
}
|
|
151
|
-
function escapeRe(s) {
|
|
152
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
153
|
-
}
|
package/dist/commands/init.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { type ParsedArgs } from '../args.js';
|
|
2
|
-
import { type StoredAuth } from '../auth-store.js';
|
|
3
|
-
import { type DeveloperApi } from '../developer-api.js';
|
|
4
|
-
import { type Reporter } from '../output.js';
|
|
5
|
-
import { type Remote } from '../remote.js';
|
|
6
|
-
export declare const DEFAULT_CALLBACK = "http://127.0.0.1:3000/auth/aipay/callback";
|
|
7
|
-
export declare const SDK_PACKAGE = "@coswic/aipay-sdk";
|
|
8
|
-
export interface InitDeps {
|
|
9
|
-
remote?: Remote;
|
|
10
|
-
/** Injected for tests: the developer login (null = signed out). */
|
|
11
|
-
login?: StoredAuth | null;
|
|
12
|
-
developerApi?: DeveloperApi;
|
|
13
|
-
now?: () => Date;
|
|
14
|
-
/** Runs the package-manager install; returns exit code. Injected for tests. */
|
|
15
|
-
run?: (cmd: string[], cwd: string, quiet: boolean) => number;
|
|
16
|
-
env?: NodeJS.ProcessEnv;
|
|
17
|
-
}
|
|
18
|
-
export declare function runInit(args: ParsedArgs, out: Reporter, deps?: InitDeps): Promise<number>;
|