@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/dist/commands/init.js
DELETED
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `aipay init --app <client_id> --auth <origin> --api <base> [--dir] [--callback]
|
|
3
|
-
* [--name] [--json] [--dry-run] [--no-install]`
|
|
4
|
-
*
|
|
5
|
-
* Links the project to an app and scaffolds the integration. Idempotent: rerunning
|
|
6
|
-
* updates .aipay/project.json + .env.local and never overwrites files it did not write
|
|
7
|
-
* this run. Nothing here needs an AI-Pay login (public client info only) — the gate
|
|
8
|
-
* token, if any, is read from the environment for the docs fetch.
|
|
9
|
-
*/
|
|
10
|
-
import { spawnSync } from 'node:child_process';
|
|
11
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
12
|
-
import { dirname, join, resolve } from 'node:path';
|
|
13
|
-
import { AGENT_GUIDE_FILE, agentGuide } from '../agent-guide.js';
|
|
14
|
-
import { flagBool, flagString } from '../args.js';
|
|
15
|
-
import { createAuthStore } from '../auth-store.js';
|
|
16
|
-
import { detectProject, installCommand } from '../detect.js';
|
|
17
|
-
import { openDeveloperApi } from '../developer-api.js';
|
|
18
|
-
import { GATE_ENV } from '../gate.js';
|
|
19
|
-
import { parseCallback } from '../loopback.js';
|
|
20
|
-
import { CliError, EXIT } from '../output.js';
|
|
21
|
-
import { ENV_FILE, ENV_KEYS, mergeEnvFile, mergeGitignore, readProject, writeProject, } from '../project.js';
|
|
22
|
-
import { createRemote, docsUrl, GATE_HINT } from '../remote.js';
|
|
23
|
-
import { reportSetupEvent } from '../setup-events.js';
|
|
24
|
-
import { scaffoldFiles } from '../templates.js';
|
|
25
|
-
import { cliVersion } from '../version.js';
|
|
26
|
-
import { createApp } from './apps.js';
|
|
27
|
-
export const DEFAULT_CALLBACK = 'http://127.0.0.1:3000/auth/aipay/callback';
|
|
28
|
-
export const SDK_PACKAGE = '@coswic/aipay-sdk';
|
|
29
|
-
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
30
|
-
export async function runInit(args, out, deps = {}) {
|
|
31
|
-
const env = deps.env ?? process.env;
|
|
32
|
-
const dir = resolve(flagString(args.flags, 'dir') ?? '.');
|
|
33
|
-
const existing = safeReadProject(dir);
|
|
34
|
-
const callback = flagString(args.flags, 'callback') ?? existing?.callback ?? DEFAULT_CALLBACK;
|
|
35
|
-
// Signed in as a developer? Then --auth/--api can come from the login, and --create can
|
|
36
|
-
// make the app first (Idempotency-Key: rerunning never makes a second app).
|
|
37
|
-
const login = deps.login === undefined ? createAuthStore(env).load() : deps.login;
|
|
38
|
-
let clientId = flagString(args.flags, 'app');
|
|
39
|
-
let createdName = null;
|
|
40
|
-
const createName = flagString(args.flags, 'create');
|
|
41
|
-
if (createName) {
|
|
42
|
-
if (clientId)
|
|
43
|
-
throw new CliError('usage', 'pass either --app <client_id> or --create <name>, not both', EXIT.usage);
|
|
44
|
-
if (!login)
|
|
45
|
-
throw new CliError('not_logged_in', '--create needs a developer sign-in first: `aipay login --api <base>`', EXIT.problem);
|
|
46
|
-
const api = deps.developerApi ?? (await openDeveloperApi(createAuthStore(env), env));
|
|
47
|
-
const created = await createApp(api, {
|
|
48
|
-
name: createName,
|
|
49
|
-
callback,
|
|
50
|
-
key: flagString(args.flags, 'key'),
|
|
51
|
-
});
|
|
52
|
-
clientId = created.client_id;
|
|
53
|
-
createdName = createName;
|
|
54
|
-
out.line('ok', `${created.replayed ? 'app already existed' : 'created app'} "${createName}" — client_id ${clientId}`);
|
|
55
|
-
}
|
|
56
|
-
if (!clientId)
|
|
57
|
-
throw new CliError('usage', 'missing --app <client_id> (from the Console, after creating the app) or --create <name>', EXIT.usage);
|
|
58
|
-
if (!UUID_RE.test(clientId))
|
|
59
|
-
throw new CliError('usage', `--app must be the app's client_id (a UUID), got "${clientId}"`, EXIT.usage);
|
|
60
|
-
const authOrigin = originOf(flagString(args.flags, 'auth') ?? existing?.auth_origin ?? login?.auth_origin ?? null, '--auth');
|
|
61
|
-
const apiBase = originOf(flagString(args.flags, 'api') ?? existing?.api_base ?? login?.api_base ?? null, '--api');
|
|
62
|
-
const target = parseCallback(callback); // throws on non-loopback
|
|
63
|
-
const dryRun = flagBool(args.flags, 'dry-run');
|
|
64
|
-
const install = flagBool(args.flags, 'install', true);
|
|
65
|
-
const name = flagString(args.flags, 'name') ?? createdName ?? existing?.app.name ?? null;
|
|
66
|
-
const remote = deps.remote ?? createRemote(env);
|
|
67
|
-
const now = (deps.now ?? (() => new Date()))();
|
|
68
|
-
const det = detectProject(dir);
|
|
69
|
-
out.line('info', `project: ${dir}`);
|
|
70
|
-
out.kv('package.json', det.hasPackageJson ? 'found' : 'none (plain directory)');
|
|
71
|
-
out.kv('manager', det.packageManager ?? '—');
|
|
72
|
-
out.kv('framework', det.framework ?? '—');
|
|
73
|
-
out.kv('language', det.typescript ? 'TypeScript' : 'JavaScript');
|
|
74
|
-
out.kv('callback', callback);
|
|
75
|
-
const plan = [];
|
|
76
|
-
const writes = [];
|
|
77
|
-
// 1. project file
|
|
78
|
-
const project = {
|
|
79
|
-
version: 1,
|
|
80
|
-
app: { client_id: clientId, name },
|
|
81
|
-
auth_origin: authOrigin,
|
|
82
|
-
api_base: apiBase,
|
|
83
|
-
callback,
|
|
84
|
-
scaffold: {
|
|
85
|
-
package_manager: det.packageManager,
|
|
86
|
-
framework: det.framework,
|
|
87
|
-
files: existing?.scaffold.files ?? [],
|
|
88
|
-
},
|
|
89
|
-
linked_at: now.toISOString(),
|
|
90
|
-
};
|
|
91
|
-
// 2. env file (merge)
|
|
92
|
-
const envPath = join(dir, ENV_FILE);
|
|
93
|
-
const envBefore = existsSync(envPath) ? readFileSync(envPath, 'utf8') : null;
|
|
94
|
-
const envMerged = mergeEnvFile(envBefore, {
|
|
95
|
-
[ENV_KEYS.clientId]: clientId,
|
|
96
|
-
[ENV_KEYS.authOrigin]: authOrigin,
|
|
97
|
-
[ENV_KEYS.apiBase]: apiBase,
|
|
98
|
-
[ENV_KEYS.callback]: callback,
|
|
99
|
-
});
|
|
100
|
-
if (envMerged.changed.length) {
|
|
101
|
-
plan.push({
|
|
102
|
-
path: ENV_FILE,
|
|
103
|
-
action: envBefore === null ? 'write' : 'update',
|
|
104
|
-
why: envMerged.changed.join(', '),
|
|
105
|
-
});
|
|
106
|
-
writes.push({ path: envPath, content: envMerged.content });
|
|
107
|
-
}
|
|
108
|
-
else
|
|
109
|
-
plan.push({ path: ENV_FILE, action: 'skip', why: 'up to date' });
|
|
110
|
-
// 3. gitignore (append)
|
|
111
|
-
const giPath = join(dir, '.gitignore');
|
|
112
|
-
const giBefore = existsSync(giPath) ? readFileSync(giPath, 'utf8') : null;
|
|
113
|
-
const gi = mergeGitignore(giBefore, [ENV_FILE, '.aipay/local/']);
|
|
114
|
-
if (gi.added.length) {
|
|
115
|
-
plan.push({
|
|
116
|
-
path: '.gitignore',
|
|
117
|
-
action: giBefore === null ? 'write' : 'update',
|
|
118
|
-
why: gi.added.join(', '),
|
|
119
|
-
});
|
|
120
|
-
writes.push({ path: giPath, content: gi.content });
|
|
121
|
-
}
|
|
122
|
-
else
|
|
123
|
-
plan.push({ path: '.gitignore', action: 'skip', why: 'already ignores them' });
|
|
124
|
-
// 4. scaffold (never overwrite)
|
|
125
|
-
const scaffolded = [];
|
|
126
|
-
if (det.hasPackageJson && det.framework) {
|
|
127
|
-
for (const f of scaffoldFiles({
|
|
128
|
-
framework: det.framework,
|
|
129
|
-
typescript: det.typescript,
|
|
130
|
-
srcDir: det.srcDir,
|
|
131
|
-
callbackPath: target.path,
|
|
132
|
-
})) {
|
|
133
|
-
const abs = join(dir, f.path);
|
|
134
|
-
if (existsSync(abs))
|
|
135
|
-
plan.push({ path: f.path, action: 'skip', why: 'exists' });
|
|
136
|
-
else {
|
|
137
|
-
plan.push({ path: f.path, action: 'write' });
|
|
138
|
-
writes.push({ path: abs, content: f.content });
|
|
139
|
-
scaffolded.push(f.path);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
else
|
|
144
|
-
plan.push({ path: '(scaffold)', action: 'skip', why: 'no package.json — link only' });
|
|
145
|
-
project.scaffold.files = Array.from(new Set([...project.scaffold.files, ...scaffolded]));
|
|
146
|
-
// 5. docs → agent guide
|
|
147
|
-
const docs = await remote.fetchDocs(apiBase, 'en');
|
|
148
|
-
if (docs.gated)
|
|
149
|
-
out.line('warn', GATE_HINT);
|
|
150
|
-
else if (!docs.markdown)
|
|
151
|
-
out.line('warn', `docs fetch failed (${docs.status}); guide written without the docs body`);
|
|
152
|
-
const guidePath = join(dir, AGENT_GUIDE_FILE);
|
|
153
|
-
writes.push({
|
|
154
|
-
path: guidePath,
|
|
155
|
-
content: agentGuide({
|
|
156
|
-
project,
|
|
157
|
-
scaffolded: project.scaffold.files,
|
|
158
|
-
docsMarkdown: docs.markdown,
|
|
159
|
-
docsUrl: docsUrl(apiBase, 'en'),
|
|
160
|
-
}),
|
|
161
|
-
});
|
|
162
|
-
plan.push({ path: AGENT_GUIDE_FILE, action: existsSync(guidePath) ? 'update' : 'write' });
|
|
163
|
-
plan.push({ path: '.aipay/project.json', action: existing ? 'update' : 'write' });
|
|
164
|
-
// 6. install
|
|
165
|
-
let installCmd = null;
|
|
166
|
-
if (det.hasPackageJson && !det.sdkInstalled && install && det.packageManager)
|
|
167
|
-
installCmd = installCommand(det.packageManager, SDK_PACKAGE);
|
|
168
|
-
out.blank();
|
|
169
|
-
for (const p of plan)
|
|
170
|
-
out.line(p.action === 'skip' ? 'info' : 'ok', `${p.action.padEnd(6)} ${p.path}${p.why ? ` (${p.why})` : ''}`);
|
|
171
|
-
if (installCmd)
|
|
172
|
-
out.line('info', `${dryRun ? 'would run' : 'run '} ${installCmd.join(' ')}`);
|
|
173
|
-
else if (det.hasPackageJson && det.sdkInstalled)
|
|
174
|
-
out.line('info', `skip ${SDK_PACKAGE} (already a dependency)`);
|
|
175
|
-
let installExit = null;
|
|
176
|
-
if (!dryRun) {
|
|
177
|
-
for (const w of writes) {
|
|
178
|
-
mkdirSync(dirname(w.path), { recursive: true });
|
|
179
|
-
writeFileSync(w.path, w.content);
|
|
180
|
-
}
|
|
181
|
-
writeProject(dir, project);
|
|
182
|
-
if (installCmd) {
|
|
183
|
-
const run = deps.run ?? defaultRun;
|
|
184
|
-
// --json: the install's own output must not land in stdout (it would corrupt the document)
|
|
185
|
-
installExit = run(installCmd, dir, out.json);
|
|
186
|
-
if (installExit !== 0)
|
|
187
|
-
out.line('bad', `install failed (exit ${installExit}); run it yourself: ${installCmd.join(' ')}`);
|
|
188
|
-
else
|
|
189
|
-
out.line('ok', `installed ${SDK_PACKAGE}`);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
// C2 setup session:登入狀態下回報 init(Console 的「等待工具連線」變綠);失敗不影響結果
|
|
193
|
-
let reported = false;
|
|
194
|
-
if (!dryRun)
|
|
195
|
-
reported = await reportSetupEvent(clientId, 'cli.init', {
|
|
196
|
-
framework: det.framework ?? 'none',
|
|
197
|
-
package_manager: det.packageManager ?? 'none',
|
|
198
|
-
typescript: det.typescript,
|
|
199
|
-
files: project.scaffold.files,
|
|
200
|
-
cli_version: cliVersion(),
|
|
201
|
-
}, out, env);
|
|
202
|
-
out.blank();
|
|
203
|
-
out.line('ok', dryRun ? 'dry run — nothing written' : `linked to app ${name ?? clientId}`);
|
|
204
|
-
out.line('info', 'next: run `aipay test` — it receives the callback on a free loopback port, so your dev server can keep running');
|
|
205
|
-
if (!env[GATE_ENV])
|
|
206
|
-
out.line('info', `pilot: if AI-Pay answers 404 with an empty body, export ${GATE_ENV} (Console → your app)`);
|
|
207
|
-
out.result({
|
|
208
|
-
ok: installExit === null || installExit === 0,
|
|
209
|
-
dry_run: dryRun,
|
|
210
|
-
dir,
|
|
211
|
-
project,
|
|
212
|
-
detection: det,
|
|
213
|
-
plan,
|
|
214
|
-
install: installCmd ? { command: installCmd, exit: installExit } : null,
|
|
215
|
-
docs: { fetched: !!docs.markdown, gated: docs.gated, status: docs.status },
|
|
216
|
-
reported,
|
|
217
|
-
});
|
|
218
|
-
return installExit === null || installExit === 0 ? EXIT.ok : EXIT.problem;
|
|
219
|
-
}
|
|
220
|
-
function safeReadProject(dir) {
|
|
221
|
-
try {
|
|
222
|
-
return readProject(dir);
|
|
223
|
-
}
|
|
224
|
-
catch {
|
|
225
|
-
return null;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
function originOf(v, flag) {
|
|
229
|
-
if (!v)
|
|
230
|
-
throw new CliError('usage', `missing ${flag} <url> (the Console prompt lists both the authorization server and the API base)`, EXIT.usage);
|
|
231
|
-
let u;
|
|
232
|
-
try {
|
|
233
|
-
u = new URL(v);
|
|
234
|
-
}
|
|
235
|
-
catch {
|
|
236
|
-
throw new CliError('usage', `${flag} must be a URL, got "${v}"`, EXIT.usage);
|
|
237
|
-
}
|
|
238
|
-
if (u.protocol !== 'https:' && u.protocol !== 'http:')
|
|
239
|
-
throw new CliError('usage', `${flag} must be http(s)`, EXIT.usage);
|
|
240
|
-
return u.origin;
|
|
241
|
-
}
|
|
242
|
-
function defaultRun(cmd, cwd, quiet) {
|
|
243
|
-
const [bin, ...rest] = cmd;
|
|
244
|
-
const r = spawnSync(bin, rest, {
|
|
245
|
-
cwd,
|
|
246
|
-
stdio: quiet ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
247
|
-
encoding: 'utf8',
|
|
248
|
-
});
|
|
249
|
-
if (quiet && r.status !== 0)
|
|
250
|
-
process.stderr.write(`${r.stdout ?? ''}${r.stderr ?? ''}`);
|
|
251
|
-
return r.status ?? 1;
|
|
252
|
-
}
|
package/dist/commands/login.d.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { type ParsedArgs } from '../args.js';
|
|
2
|
-
import { type Reporter } from '../output.js';
|
|
3
|
-
export interface CliConfig {
|
|
4
|
-
client_id: string;
|
|
5
|
-
auth_origin: string;
|
|
6
|
-
scopes: string[];
|
|
7
|
-
redirect_path: string;
|
|
8
|
-
audience: string;
|
|
9
|
-
}
|
|
10
|
-
export interface LoginDeps {
|
|
11
|
-
env?: NodeJS.ProcessEnv;
|
|
12
|
-
open?: (url: string) => boolean;
|
|
13
|
-
onAuthorizeUrl?: (url: string) => void;
|
|
14
|
-
}
|
|
15
|
-
export declare function fetchCliConfig(apiBase: string, env: NodeJS.ProcessEnv): Promise<CliConfig>;
|
|
16
|
-
/** Resolve the API base: flag → env → linked project → error. */
|
|
17
|
-
export declare function resolveApiBase(args: ParsedArgs, env: NodeJS.ProcessEnv, dir: string): string;
|
|
18
|
-
/** Authorization URL for the management login. The SDK's createConnectUrl fixes the
|
|
19
|
-
* audience to aipay-api (apps), so the CLI builds its own with the developer audience. */
|
|
20
|
-
export declare function managementAuthorizeUrl(cfg: CliConfig, redirectUri: string): {
|
|
21
|
-
url: string;
|
|
22
|
-
state: string;
|
|
23
|
-
codeVerifier: string;
|
|
24
|
-
};
|
|
25
|
-
export declare function runLogin(args: ParsedArgs, out: Reporter, deps?: LoginDeps): Promise<number>;
|
package/dist/commands/login.js
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `aipay login [--api <base>] [--no-open] [--timeout <s>] [--json]`
|
|
3
|
-
*
|
|
4
|
-
* Developer sign-in for the CLI itself (consultant §10.2, auth model A / A1): the CLI is
|
|
5
|
-
* a first-party public client. It asks the API for its client config, runs loopback PKCE
|
|
6
|
-
* against the authorization server with audience aipay-developer-api and the management
|
|
7
|
-
* scope, then keeps the tokens in the OS keychain (or a 0600 file). No app secrets exist.
|
|
8
|
-
*/
|
|
9
|
-
import { createHash, randomBytes } from 'node:crypto';
|
|
10
|
-
import { exchangeCode, getUserInfo } from '@coswic/aipay-sdk';
|
|
11
|
-
import { flagBool, flagString } from '../args.js';
|
|
12
|
-
import { createAuthStore } from '../auth-store.js';
|
|
13
|
-
import { openBrowser } from '../browser.js';
|
|
14
|
-
import { GATE_ENV, gatedFetch, looksGated } from '../gate.js';
|
|
15
|
-
import { startLoopback } from '../loopback.js';
|
|
16
|
-
import { CliError, EXIT } from '../output.js';
|
|
17
|
-
import { readProject } from '../project.js';
|
|
18
|
-
import { GATE_HINT } from '../remote.js';
|
|
19
|
-
export async function fetchCliConfig(apiBase, env) {
|
|
20
|
-
const f = gatedFetch(env);
|
|
21
|
-
let res;
|
|
22
|
-
try {
|
|
23
|
-
res = await f(new URL('/v1/cli/config', apiBase));
|
|
24
|
-
}
|
|
25
|
-
catch (e) {
|
|
26
|
-
throw new CliError('network', `cannot reach ${apiBase}: ${e.message}`, EXIT.remote);
|
|
27
|
-
}
|
|
28
|
-
const text = await res.text();
|
|
29
|
-
if (looksGated(res.status, text))
|
|
30
|
-
throw new CliError('gated', GATE_HINT, EXIT.remote);
|
|
31
|
-
if (res.status === 404)
|
|
32
|
-
throw new CliError('cli_disabled', `${apiBase} does not offer CLI sign-in (developer management is not enabled there)`, EXIT.remote);
|
|
33
|
-
if (!res.ok)
|
|
34
|
-
throw new CliError('remote', `GET /v1/cli/config → HTTP ${res.status}`, EXIT.remote);
|
|
35
|
-
const cfg = JSON.parse(text);
|
|
36
|
-
if (typeof cfg.client_id !== 'string' ||
|
|
37
|
-
typeof cfg.auth_origin !== 'string' ||
|
|
38
|
-
!Array.isArray(cfg.scopes))
|
|
39
|
-
throw new CliError('remote', 'unexpected /v1/cli/config shape', EXIT.remote);
|
|
40
|
-
return {
|
|
41
|
-
client_id: cfg.client_id,
|
|
42
|
-
auth_origin: cfg.auth_origin,
|
|
43
|
-
scopes: cfg.scopes.filter((s) => typeof s === 'string'),
|
|
44
|
-
redirect_path: typeof cfg.redirect_path === 'string' ? cfg.redirect_path : '/cli/callback',
|
|
45
|
-
audience: typeof cfg.audience === 'string' ? cfg.audience : 'aipay-developer-api',
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
/** Resolve the API base: flag → env → linked project → error. */
|
|
49
|
-
export function resolveApiBase(args, env, dir) {
|
|
50
|
-
const v = flagString(args.flags, 'api') ?? env.AIPAY_API_BASE ?? readProjectSafe(dir)?.api_base ?? null;
|
|
51
|
-
if (!v)
|
|
52
|
-
throw new CliError('usage', 'which AI-Pay? pass --api <api base> (the Console prompt lists it) or set AIPAY_API_BASE', EXIT.usage);
|
|
53
|
-
try {
|
|
54
|
-
return new URL(v).origin;
|
|
55
|
-
}
|
|
56
|
-
catch {
|
|
57
|
-
throw new CliError('usage', `--api must be a URL, got "${v}"`, EXIT.usage);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
function readProjectSafe(dir) {
|
|
61
|
-
try {
|
|
62
|
-
return readProject(dir);
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
/** Authorization URL for the management login. The SDK's createConnectUrl fixes the
|
|
69
|
-
* audience to aipay-api (apps), so the CLI builds its own with the developer audience. */
|
|
70
|
-
export function managementAuthorizeUrl(cfg, redirectUri) {
|
|
71
|
-
const codeVerifier = randomBytes(32).toString('base64url');
|
|
72
|
-
const state = randomBytes(16).toString('base64url');
|
|
73
|
-
const challenge = createHash('sha256').update(codeVerifier).digest('base64url');
|
|
74
|
-
const u = new URL('/oauth2/auth', cfg.auth_origin);
|
|
75
|
-
u.searchParams.set('client_id', cfg.client_id);
|
|
76
|
-
u.searchParams.set('response_type', 'code');
|
|
77
|
-
u.searchParams.set('scope', cfg.scopes.join(' '));
|
|
78
|
-
u.searchParams.set('state', state);
|
|
79
|
-
u.searchParams.set('redirect_uri', redirectUri);
|
|
80
|
-
u.searchParams.set('code_challenge', challenge);
|
|
81
|
-
u.searchParams.set('code_challenge_method', 'S256');
|
|
82
|
-
u.searchParams.set('audience', cfg.audience);
|
|
83
|
-
return { url: u.toString(), state, codeVerifier };
|
|
84
|
-
}
|
|
85
|
-
export async function runLogin(args, out, deps = {}) {
|
|
86
|
-
const env = deps.env ?? process.env;
|
|
87
|
-
const dir = flagString(args.flags, 'dir') ?? '.';
|
|
88
|
-
const apiBase = resolveApiBase(args, env, dir);
|
|
89
|
-
const cfg = await fetchCliConfig(apiBase, env);
|
|
90
|
-
const timeoutS = Number(flagString(args.flags, 'timeout') ?? '300');
|
|
91
|
-
const loop = await startLoopback({ host: '127.0.0.1', port: 0, path: cfg.redirect_path }, { timeoutMs: timeoutS * 1000 });
|
|
92
|
-
const redirectUri = `http://127.0.0.1:${loop.port}${cfg.redirect_path}`;
|
|
93
|
-
const { url, state, codeVerifier } = managementAuthorizeUrl(cfg, redirectUri);
|
|
94
|
-
out.line('info', `signing in to ${apiBase} as a developer — approve "AI-Pay CLI" in your browser:\n\n ${url}\n`);
|
|
95
|
-
if (out.json)
|
|
96
|
-
out.debug(`authorize_url ${url}`);
|
|
97
|
-
deps.onAuthorizeUrl?.(url);
|
|
98
|
-
if (flagBool(args.flags, 'open', true))
|
|
99
|
-
(deps.open ?? openBrowser)(url);
|
|
100
|
-
const fetchImpl = gatedFetch(env);
|
|
101
|
-
try {
|
|
102
|
-
const cb = await loop.waitForCallback;
|
|
103
|
-
const u = new URL(cb.url);
|
|
104
|
-
const err = u.searchParams.get('error');
|
|
105
|
-
if (err)
|
|
106
|
-
throw new CliError('denied', `authorization failed: ${err} ${u.searchParams.get('error_description') ?? ''}`.trim(), EXIT.remote);
|
|
107
|
-
if (u.searchParams.get('state') !== state)
|
|
108
|
-
throw new CliError('state', 'state mismatch — restart `aipay login`', EXIT.remote);
|
|
109
|
-
const code = u.searchParams.get('code');
|
|
110
|
-
if (!code)
|
|
111
|
-
throw new CliError('remote', 'callback without a code', EXIT.remote);
|
|
112
|
-
const tokens = await exchangeCode({ hydraPublicUrl: cfg.auth_origin, clientId: cfg.client_id, redirectUri }, { code, codeVerifier, fetchImpl });
|
|
113
|
-
const user = await getUserInfo({ hydraPublicUrl: cfg.auth_origin }, tokens.accessToken, {
|
|
114
|
-
fetchImpl,
|
|
115
|
-
});
|
|
116
|
-
const store = createAuthStore(env);
|
|
117
|
-
const auth = {
|
|
118
|
-
api_base: apiBase,
|
|
119
|
-
auth_origin: cfg.auth_origin,
|
|
120
|
-
client_id: cfg.client_id,
|
|
121
|
-
access_token: tokens.accessToken,
|
|
122
|
-
refresh_token: tokens.refreshToken,
|
|
123
|
-
expires_at: Date.now() + tokens.expiresInSeconds * 1000,
|
|
124
|
-
scope: tokens.scope,
|
|
125
|
-
email: user.email ?? null,
|
|
126
|
-
sub: user.sub,
|
|
127
|
-
};
|
|
128
|
-
store.save(auth);
|
|
129
|
-
out.line('ok', `signed in as ${auth.email ?? auth.sub}`);
|
|
130
|
-
out.line('info', `tokens kept in ${store.where}${store.kind === 'file' ? ' — no keychain tool found; the file is readable only by you' : ''}`);
|
|
131
|
-
if (!tokens.refreshToken)
|
|
132
|
-
out.line('warn', 'no refresh token was granted; you will need to sign in again when this one expires');
|
|
133
|
-
if (!env[GATE_ENV])
|
|
134
|
-
out.line('info', `pilot: if the API answers 404 with an empty body later, export ${GATE_ENV}`);
|
|
135
|
-
out.result({
|
|
136
|
-
ok: true,
|
|
137
|
-
api_base: apiBase,
|
|
138
|
-
email: auth.email,
|
|
139
|
-
sub: auth.sub,
|
|
140
|
-
scope: auth.scope,
|
|
141
|
-
store: store.kind,
|
|
142
|
-
expires_at: auth.expires_at,
|
|
143
|
-
});
|
|
144
|
-
return EXIT.ok;
|
|
145
|
-
}
|
|
146
|
-
finally {
|
|
147
|
-
await loop.close();
|
|
148
|
-
}
|
|
149
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
/** `aipay whoami` and `aipay logout`. */
|
|
2
|
-
import type { ParsedArgs } from '../args.js';
|
|
3
|
-
import { type Reporter } from '../output.js';
|
|
4
|
-
export declare function runWhoami(_args: ParsedArgs, out: Reporter, env?: NodeJS.ProcessEnv): Promise<number>;
|
|
5
|
-
export declare function runLogout(_args: ParsedArgs, out: Reporter, env?: NodeJS.ProcessEnv): Promise<number>;
|
|
6
|
-
export declare function requireLogin(env: NodeJS.ProcessEnv): void;
|
package/dist/commands/session.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { createAuthStore } from '../auth-store.js';
|
|
2
|
-
import { openDeveloperApi } from '../developer-api.js';
|
|
3
|
-
import { gatedFetch } from '../gate.js';
|
|
4
|
-
import { CliError, EXIT } from '../output.js';
|
|
5
|
-
export async function runWhoami(_args, out, env = process.env) {
|
|
6
|
-
const store = createAuthStore(env);
|
|
7
|
-
const auth = store.load();
|
|
8
|
-
if (!auth) {
|
|
9
|
-
out.line('info', 'not signed in (run `aipay login --api <api base>`)');
|
|
10
|
-
out.result({ signed_in: false });
|
|
11
|
-
return EXIT.ok;
|
|
12
|
-
}
|
|
13
|
-
const api = await openDeveloperApi(store, env); // refreshes if needed
|
|
14
|
-
out.line('ok', `${api.auth.email ?? api.auth.sub} @ ${api.auth.api_base}`);
|
|
15
|
-
out.kv('scope', api.auth.scope || '—');
|
|
16
|
-
out.kv('expires', new Date(api.auth.expires_at).toISOString());
|
|
17
|
-
out.kv('store', store.where);
|
|
18
|
-
out.result({
|
|
19
|
-
signed_in: true,
|
|
20
|
-
api_base: api.auth.api_base,
|
|
21
|
-
email: api.auth.email,
|
|
22
|
-
sub: api.auth.sub,
|
|
23
|
-
scope: api.auth.scope,
|
|
24
|
-
expires_at: api.auth.expires_at,
|
|
25
|
-
store: store.kind,
|
|
26
|
-
});
|
|
27
|
-
return EXIT.ok;
|
|
28
|
-
}
|
|
29
|
-
export async function runLogout(_args, out, env = process.env) {
|
|
30
|
-
const store = createAuthStore(env);
|
|
31
|
-
const auth = store.load();
|
|
32
|
-
if (!auth) {
|
|
33
|
-
out.line('info', 'already signed out');
|
|
34
|
-
out.result({ ok: true, revoked: false });
|
|
35
|
-
return EXIT.ok;
|
|
36
|
-
}
|
|
37
|
-
// Best effort: revoke the refresh token (and with it the access token) at the authorization server.
|
|
38
|
-
let revoked = false;
|
|
39
|
-
const f = gatedFetch(env);
|
|
40
|
-
for (const token of [auth.refresh_token, auth.access_token]) {
|
|
41
|
-
if (!token)
|
|
42
|
-
continue;
|
|
43
|
-
try {
|
|
44
|
-
const res = await f(new URL('/oauth2/revoke', auth.auth_origin), {
|
|
45
|
-
method: 'POST',
|
|
46
|
-
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
47
|
-
body: new URLSearchParams({ token, client_id: auth.client_id }).toString(),
|
|
48
|
-
});
|
|
49
|
-
revoked = revoked || res.ok;
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
/* offline: local forget still happens */
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
store.clear();
|
|
56
|
-
out.line('ok', `signed out${revoked ? ' (token revoked)' : ' (could not reach the authorization server; token forgotten locally)'}`);
|
|
57
|
-
out.result({ ok: true, revoked });
|
|
58
|
-
return EXIT.ok;
|
|
59
|
-
}
|
|
60
|
-
export function requireLogin(env) {
|
|
61
|
-
if (!createAuthStore(env).load())
|
|
62
|
-
throw new CliError('not_logged_in', 'not signed in — run `aipay login` first', EXIT.problem);
|
|
63
|
-
}
|
package/dist/commands/status.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/** `aipay status [--dir] [--json]` — what this directory is linked to. No network. */
|
|
2
|
-
import { resolve } from 'node:path';
|
|
3
|
-
import { flagString } from '../args.js';
|
|
4
|
-
import { EXIT } from '../output.js';
|
|
5
|
-
import { readProject } from '../project.js';
|
|
6
|
-
export function runStatus(args, out) {
|
|
7
|
-
const dir = resolve(flagString(args.flags, 'dir') ?? '.');
|
|
8
|
-
const project = readProject(dir);
|
|
9
|
-
if (!project) {
|
|
10
|
-
out.line('info', 'not linked (no .aipay/project.json here)');
|
|
11
|
-
out.result({ linked: false, dir });
|
|
12
|
-
return EXIT.ok;
|
|
13
|
-
}
|
|
14
|
-
out.line('ok', `linked: ${project.app.name ?? '(unnamed app)'}`);
|
|
15
|
-
out.kv('client_id', project.app.client_id);
|
|
16
|
-
out.kv('auth', project.auth_origin);
|
|
17
|
-
out.kv('api', project.api_base);
|
|
18
|
-
out.kv('callback', project.callback);
|
|
19
|
-
out.kv('framework', `${project.scaffold.framework ?? '—'} / ${project.scaffold.package_manager ?? '—'}`);
|
|
20
|
-
out.kv('scaffolded', project.scaffold.files.length ? project.scaffold.files.join(', ') : '—');
|
|
21
|
-
out.kv('linked at', project.linked_at);
|
|
22
|
-
out.result({ linked: true, dir, project });
|
|
23
|
-
return EXIT.ok;
|
|
24
|
-
}
|
package/dist/commands/test.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { type ParsedArgs } from '../args.js';
|
|
2
|
-
import { type Reporter } from '../output.js';
|
|
3
|
-
export interface TestDeps {
|
|
4
|
-
env?: NodeJS.ProcessEnv;
|
|
5
|
-
open?: (url: string) => boolean;
|
|
6
|
-
/** For e2e drivers: receives the authorize URL (in addition to the printed line). */
|
|
7
|
-
onAuthorizeUrl?: (url: string) => void;
|
|
8
|
-
}
|
|
9
|
-
export declare function runTest(args: ParsedArgs, out: Reporter, deps?: TestDeps): Promise<number>;
|
|
10
|
-
/** Cheapest sensible default: prefer a small/cheap model id if present, else the first. */
|
|
11
|
-
export declare function pickModel(ids: string[]): string | null;
|
|
12
|
-
export declare function formatUusd(uusd: string): string;
|