@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.
Files changed (52) hide show
  1. package/README.md +22 -0
  2. package/package.json +1 -1
  3. package/dist/agent-guide.d.ts +0 -18
  4. package/dist/agent-guide.js +0 -60
  5. package/dist/args.d.ts +0 -15
  6. package/dist/args.js +0 -47
  7. package/dist/auth-store.d.ts +0 -23
  8. package/dist/auth-store.js +0 -113
  9. package/dist/bin.d.ts +0 -2
  10. package/dist/bin.js +0 -8
  11. package/dist/browser.d.ts +0 -1
  12. package/dist/browser.js +0 -18
  13. package/dist/commands/apps.d.ts +0 -15
  14. package/dist/commands/apps.js +0 -53
  15. package/dist/commands/doctor.d.ts +0 -17
  16. package/dist/commands/doctor.js +0 -153
  17. package/dist/commands/init.d.ts +0 -18
  18. package/dist/commands/init.js +0 -252
  19. package/dist/commands/login.d.ts +0 -25
  20. package/dist/commands/login.js +0 -149
  21. package/dist/commands/session.d.ts +0 -6
  22. package/dist/commands/session.js +0 -63
  23. package/dist/commands/status.d.ts +0 -3
  24. package/dist/commands/status.js +0 -24
  25. package/dist/commands/test.d.ts +0 -12
  26. package/dist/commands/test.js +0 -159
  27. package/dist/commands/unlink.d.ts +0 -5
  28. package/dist/commands/unlink.js +0 -70
  29. package/dist/detect.d.ts +0 -27
  30. package/dist/detect.js +0 -79
  31. package/dist/developer-api.d.ts +0 -14
  32. package/dist/developer-api.js +0 -74
  33. package/dist/gate.d.ts +0 -13
  34. package/dist/gate.js +0 -29
  35. package/dist/index.d.ts +0 -22
  36. package/dist/index.js +0 -22
  37. package/dist/loopback.d.ts +0 -37
  38. package/dist/loopback.js +0 -95
  39. package/dist/main.d.ts +0 -5
  40. package/dist/main.js +0 -90
  41. package/dist/output.d.ts +0 -36
  42. package/dist/output.js +0 -52
  43. package/dist/project.d.ts +0 -50
  44. package/dist/project.js +0 -107
  45. package/dist/remote.d.ts +0 -21
  46. package/dist/remote.js +0 -41
  47. package/dist/setup-events.d.ts +0 -3
  48. package/dist/setup-events.js +0 -28
  49. package/dist/templates.d.ts +0 -24
  50. package/dist/templates.js +0 -226
  51. package/dist/version.d.ts +0 -1
  52. package/dist/version.js +0 -10
@@ -1,159 +0,0 @@
1
- /**
2
- * `aipay test [--model <id>] [--no-open] [--timeout <s>] [--json]`
3
- *
4
- * The one check only a CLI can do end to end: a real sign-in through the AI-Pay
5
- * authorization server on the registered loopback callback, then one chat completion
6
- * on the user's behalf. The receipt in the response is the proof (and the Console's
7
- * verify step turns green from the server-side facts). Tokens live in memory only.
8
- */
9
- import { AIPayClient, AuthorizationError, ConnectError, createConnectUrl, getUserInfo, handleCallback, InsufficientBalanceError, } from '@coswic/aipay-sdk';
10
- import { flagBool, flagString } from '../args.js';
11
- import { openBrowser } from '../browser.js';
12
- import { GATE_ENV, gatedFetch, looksGated } from '../gate.js';
13
- import { parseCallback, portIsVariable, redirectUriFor, startLoopback } from '../loopback.js';
14
- import { CliError, EXIT } from '../output.js';
15
- import { readProject } from '../project.js';
16
- import { GATE_HINT } from '../remote.js';
17
- import { reportSetupEvent } from '../setup-events.js';
18
- export async function runTest(args, out, deps = {}) {
19
- const env = deps.env ?? process.env;
20
- const dir = flagString(args.flags, 'dir') ?? '.';
21
- const project = readProject(dir);
22
- if (!project)
23
- throw new CliError('not_linked', 'this directory is not linked to an app — run `aipay init --app <client_id> ...` first', EXIT.problem);
24
- const timeoutS = Number(flagString(args.flags, 'timeout') ?? '300');
25
- const fetchImpl = gatedFetch(env);
26
- const target = parseCallback(project.callback);
27
- // 127.0.0.1: the authorization server ignores the port for loopback redirect URIs
28
- // (RFC 8252 §7.3), so the CLI takes any free port and your dev server can keep running.
29
- // localhost / explicit --port: bind exactly that port.
30
- const portFlag = flagString(args.flags, 'port');
31
- const bindPort = portFlag ? Number(portFlag) : portIsVariable(target) ? 0 : target.port;
32
- const loop = await startLoopback({ ...target, port: bindPort }, { timeoutMs: timeoutS * 1000 }).catch((e) => {
33
- if (e.code === 'EADDRINUSE')
34
- throw new CliError('port_busy', `port ${bindPort} is in use — stop what is listening there, or pass --port <free port>`, EXIT.problem);
35
- throw e;
36
- });
37
- const redirectUri = redirectUriFor(target, loop.port);
38
- const cfg = { hydraPublicUrl: project.auth_origin, clientId: project.app.client_id, redirectUri };
39
- const { url, state, codeVerifier } = await createConnectUrl(cfg);
40
- const note = redirectUri === project.callback
41
- ? ''
42
- : ` (registered: ${project.callback}; the port is not compared for 127.0.0.1)`;
43
- out.line('info', `listening on ${redirectUri}${note}`);
44
- out.line('info', `sign in here (opens in your browser):\n\n ${url}\n`);
45
- deps.onAuthorizeUrl?.(url);
46
- if (out.json)
47
- out.debug(`authorize_url ${url}`); // --json keeps stdout for the result; drivers read this
48
- if (flagBool(args.flags, 'open', true))
49
- (deps.open ?? openBrowser)(url);
50
- const result = {
51
- ok: false,
52
- app: project.app,
53
- signin: null,
54
- completion: null,
55
- };
56
- try {
57
- const cb = await loop.waitForCallback;
58
- let tokens;
59
- try {
60
- tokens = await handleCallback(cfg, cb.url, { state, codeVerifier }, { fetchImpl });
61
- }
62
- catch (e) {
63
- if (e instanceof ConnectError)
64
- throw new CliError('signin_failed', `sign-in failed: ${e.message}${e.body ? ` — ${e.body}` : ''}`, EXIT.remote);
65
- throw e;
66
- }
67
- const user = await getUserInfo(cfg, tokens.accessToken, { fetchImpl });
68
- result.signin = {
69
- sub: user.sub,
70
- email: user.email ?? null,
71
- scope: tokens.scope,
72
- expires_in: tokens.expiresInSeconds,
73
- };
74
- out.line('ok', `signed in: ${user.email ?? user.sub} (scope: ${tokens.scope || '—'})`);
75
- const client = new AIPayClient({
76
- baseUrl: project.api_base,
77
- accessToken: tokens.accessToken,
78
- fetchImpl,
79
- });
80
- let model = flagString(args.flags, 'model');
81
- if (!model) {
82
- const models = await client.models.list().catch((e) => {
83
- throw new CliError('models', `cannot list models: ${e.message}`, EXIT.remote);
84
- });
85
- model = pickModel(models.map((m) => m.id));
86
- if (!model)
87
- throw new CliError('models', 'no sellable model right now — pass --model <id>', EXIT.remote);
88
- }
89
- out.line('info', `calling ${model} …`);
90
- try {
91
- const res = await client.chat.completions.create({
92
- model,
93
- messages: [{ role: 'user', content: 'Reply with exactly one word: pong' }],
94
- max_tokens: 8,
95
- });
96
- const r = res.aipay;
97
- const charged = r.total_charged_uusd ?? r.cost_uusd;
98
- result.completion = {
99
- model: res.model,
100
- text: res.choices[0]?.message.content ?? '',
101
- request_id: r.request_id,
102
- usage_event_id: r.usage_event_id,
103
- charged_uusd: charged,
104
- currency: r.currency,
105
- usage: res.usage,
106
- };
107
- result.ok = true;
108
- out.line('ok', `receipt: request_id ${r.request_id}, charged ${formatUusd(charged)} ${r.currency}`);
109
- // C2 setup session:登入狀態下回報 test 通過(伺服器本來就看得到回執;這條是給 Console 的時間戳)
110
- await reportSetupEvent(project.app.client_id, 'cli.test', { request_id: r.request_id, model: res.model, ok: true }, out, env);
111
- out.line('ok', "done — the Console's verify step reads this from the server side");
112
- }
113
- catch (e) {
114
- if (e instanceof InsufficientBalanceError) {
115
- result.completion = { error: 'insufficient_balance' };
116
- out.line('warn', 'signed in fine, but this account has no balance for a completion — top up in the Console and rerun');
117
- out.result(result);
118
- return EXIT.problem;
119
- }
120
- if (e instanceof AuthorizationError)
121
- throw new CliError('unauthorized', `the API rejected the token: ${e.message}`, EXIT.remote);
122
- throw e;
123
- }
124
- out.result(result);
125
- return EXIT.ok;
126
- }
127
- catch (e) {
128
- if (e instanceof CliError) {
129
- result.error = { code: e.code, message: e.message };
130
- out.line('bad', e.message);
131
- out.result(result);
132
- return e.exitCode;
133
- }
134
- const msg = e.message ?? String(e);
135
- const gated = /404/.test(msg) && looksGated(404, '');
136
- result.error = { code: 'failed', message: msg };
137
- out.line('bad', gated ? `${msg}\n${GATE_HINT}` : msg);
138
- if (!env[GATE_ENV])
139
- out.line('info', `pilot: if this was a 404 with an empty body, export ${GATE_ENV} and retry`);
140
- out.result(result);
141
- return EXIT.remote;
142
- }
143
- finally {
144
- await loop.close();
145
- }
146
- }
147
- /** Cheapest sensible default: prefer a small/cheap model id if present, else the first. */
148
- export function pickModel(ids) {
149
- if (!ids.length)
150
- return null;
151
- const cheap = ids.find((id) => /mini|flash|haiku|nano|small|8b|7b/i.test(id));
152
- return cheap ?? ids[0];
153
- }
154
- export function formatUusd(uusd) {
155
- const n = Number(uusd);
156
- if (!Number.isFinite(n))
157
- return `${uusd} µUSD`;
158
- return `$${(n / 1_000_000).toFixed(6)}`;
159
- }
@@ -1,5 +0,0 @@
1
- import { type ParsedArgs } from '../args.js';
2
- import { type Reporter } from '../output.js';
3
- export declare function runUnlink(args: ParsedArgs, out: Reporter): number;
4
- /** Drop our keys and the marker comment; keep everything else byte-for-byte. */
5
- export declare function stripEnvKeys(content: string, keys: string[]): string;
@@ -1,70 +0,0 @@
1
- /**
2
- * `aipay unlink [--dir] [--dry-run] [--json]` — forgets the link: removes
3
- * .aipay/project.json and our four keys from .env.local. Scaffolded code and the agent
4
- * guide are the developer's files now; they are listed, not deleted.
5
- */
6
- import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
7
- import { join, resolve } from 'node:path';
8
- import { flagBool, flagString } from '../args.js';
9
- import { EXIT } from '../output.js';
10
- import { ENV_FILE, ENV_KEYS, PROJECT_DIR, projectPath, readProject } from '../project.js';
11
- export function runUnlink(args, out) {
12
- const dir = resolve(flagString(args.flags, 'dir') ?? '.');
13
- const dryRun = flagBool(args.flags, 'dry-run');
14
- const project = readProject(dir);
15
- if (!project) {
16
- out.line('info', 'nothing to unlink here');
17
- out.result({ ok: true, unlinked: false, dir });
18
- return EXIT.ok;
19
- }
20
- const removed = [];
21
- const envPath = join(dir, ENV_FILE);
22
- let envChanged = false;
23
- if (existsSync(envPath)) {
24
- const before = readFileSync(envPath, 'utf8');
25
- const after = stripEnvKeys(before, Object.values(ENV_KEYS));
26
- envChanged = after !== before;
27
- if (envChanged && !dryRun)
28
- writeFileSync(envPath, after);
29
- }
30
- if (!dryRun) {
31
- rmSync(projectPath(dir));
32
- try {
33
- rmSync(join(dir, PROJECT_DIR), { recursive: false });
34
- }
35
- catch {
36
- /* not empty (e.g. .aipay/local/) — leave it */
37
- }
38
- }
39
- removed.push('.aipay/project.json');
40
- out.line('ok', `${dryRun ? 'would remove' : 'removed'} .aipay/project.json`);
41
- if (envChanged)
42
- out.line('ok', `${dryRun ? 'would strip' : 'stripped'} AIPAY_* from ${ENV_FILE}`);
43
- if (project.scaffold.files.length)
44
- out.line('info', `kept your files: ${project.scaffold.files.join(', ')}, aipay-agent.md`);
45
- out.result({
46
- ok: true,
47
- unlinked: !dryRun,
48
- dry_run: dryRun,
49
- dir,
50
- removed,
51
- env_stripped: envChanged,
52
- kept: project.scaffold.files,
53
- });
54
- return EXIT.ok;
55
- }
56
- /** Drop our keys and the marker comment; keep everything else byte-for-byte. */
57
- export function stripEnvKeys(content, keys) {
58
- const set = new Set(keys);
59
- const lines = content.split(/\r?\n/);
60
- const kept = lines.filter((line) => {
61
- if (line.startsWith('# AI-Pay (written by `aipay init`'))
62
- return false;
63
- const m = /^\s*(?:export\s+)?([A-Z0-9_]+)\s*=/.exec(line);
64
- return !(m && set.has(m[1]));
65
- });
66
- // collapse a trailing blank run left behind by the removed block
67
- while (kept.length > 1 && kept[kept.length - 1] === '' && kept[kept.length - 2] === '')
68
- kept.pop();
69
- return kept.join('\n');
70
- }
package/dist/detect.d.ts DELETED
@@ -1,27 +0,0 @@
1
- import type { Framework } from './project.js';
2
- export type PackageManager = 'pnpm' | 'npm' | 'yarn' | 'bun';
3
- export interface Detection {
4
- packageManager: PackageManager | null;
5
- framework: Framework | null;
6
- hasPackageJson: boolean;
7
- typescript: boolean;
8
- /** Whether @coswic/aipay-sdk is already a dependency. */
9
- sdkInstalled: boolean;
10
- /** `src/` exists → scaffold under it, else project root. */
11
- srcDir: string;
12
- }
13
- export interface PackageJsonLike {
14
- packageManager?: string;
15
- dependencies?: Record<string, string>;
16
- devDependencies?: Record<string, string>;
17
- }
18
- export declare function detectPackageManager(pkg: PackageJsonLike | null, lockfiles: {
19
- pnpm: boolean;
20
- yarn: boolean;
21
- bun: boolean;
22
- npm: boolean;
23
- }): PackageManager | null;
24
- export declare function detectFramework(pkg: PackageJsonLike | null): Framework | null;
25
- export declare function detectProject(dir: string): Detection;
26
- /** The install command for the detected manager (printed in --dry-run, run otherwise). */
27
- export declare function installCommand(pm: PackageManager, pkgName: string): string[];
package/dist/detect.js DELETED
@@ -1,79 +0,0 @@
1
- /**
2
- * Project detection: package manager (from lockfile / packageManager field) and web
3
- * framework (from dependencies). Pure functions over the file contents so they are
4
- * unit-testable without a filesystem.
5
- */
6
- import { existsSync, readFileSync } from 'node:fs';
7
- import { join } from 'node:path';
8
- export function detectPackageManager(pkg, lockfiles) {
9
- const pm = pkg?.packageManager;
10
- if (typeof pm === 'string') {
11
- const name = pm.split('@')[0];
12
- if (name === 'pnpm' || name === 'npm' || name === 'yarn' || name === 'bun')
13
- return name;
14
- }
15
- if (lockfiles.pnpm)
16
- return 'pnpm';
17
- if (lockfiles.bun)
18
- return 'bun';
19
- if (lockfiles.yarn)
20
- return 'yarn';
21
- if (lockfiles.npm)
22
- return 'npm';
23
- return pkg ? 'npm' : null;
24
- }
25
- export function detectFramework(pkg) {
26
- if (!pkg)
27
- return null;
28
- const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
29
- if ('next' in deps)
30
- return 'next';
31
- if ('hono' in deps)
32
- return 'hono';
33
- if ('fastify' in deps)
34
- return 'fastify';
35
- if ('express' in deps)
36
- return 'express';
37
- return 'node';
38
- }
39
- export function detectProject(dir) {
40
- const pkgPath = join(dir, 'package.json');
41
- const hasPackageJson = existsSync(pkgPath);
42
- let pkg = null;
43
- if (hasPackageJson) {
44
- try {
45
- pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
46
- }
47
- catch {
48
- pkg = null;
49
- }
50
- }
51
- const lockfiles = {
52
- pnpm: existsSync(join(dir, 'pnpm-lock.yaml')),
53
- yarn: existsSync(join(dir, 'yarn.lock')),
54
- bun: existsSync(join(dir, 'bun.lockb')) || existsSync(join(dir, 'bun.lock')),
55
- npm: existsSync(join(dir, 'package-lock.json')),
56
- };
57
- const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
58
- return {
59
- packageManager: detectPackageManager(pkg, lockfiles),
60
- framework: detectFramework(pkg),
61
- hasPackageJson,
62
- typescript: 'typescript' in deps || existsSync(join(dir, 'tsconfig.json')),
63
- sdkInstalled: '@coswic/aipay-sdk' in deps,
64
- srcDir: existsSync(join(dir, 'src')) ? 'src' : '.',
65
- };
66
- }
67
- /** The install command for the detected manager (printed in --dry-run, run otherwise). */
68
- export function installCommand(pm, pkgName) {
69
- switch (pm) {
70
- case 'pnpm':
71
- return ['pnpm', 'add', pkgName];
72
- case 'yarn':
73
- return ['yarn', 'add', pkgName];
74
- case 'bun':
75
- return ['bun', 'add', pkgName];
76
- default:
77
- return ['npm', 'install', pkgName];
78
- }
79
- }
@@ -1,14 +0,0 @@
1
- import type { AuthStore, StoredAuth } from './auth-store.js';
2
- export interface ApiResult<T> {
3
- status: number;
4
- body: T;
5
- }
6
- export interface DeveloperApi {
7
- auth: StoredAuth;
8
- request<T = Record<string, unknown>>(method: string, path: string, opts?: {
9
- body?: unknown;
10
- headers?: Record<string, string>;
11
- }): Promise<ApiResult<T>>;
12
- }
13
- export declare function openDeveloperApi(store: AuthStore, env?: NodeJS.ProcessEnv): Promise<DeveloperApi>;
14
- export declare function errorMessage(body: unknown, status: number): string;
@@ -1,74 +0,0 @@
1
- /**
2
- * Calls to the developer management API with the developer's own token (audience
3
- * aipay-developer-api). Refreshes once on 401 when a refresh token exists; the gate header
4
- * comes from the environment like everywhere else.
5
- */
6
- import { refreshAccessToken } from '@coswic/aipay-sdk';
7
- import { gatedFetch, looksGated } from './gate.js';
8
- import { CliError, EXIT } from './output.js';
9
- import { GATE_HINT } from './remote.js';
10
- export async function openDeveloperApi(store, env = process.env) {
11
- let auth = store.load();
12
- if (!auth)
13
- throw new CliError('not_logged_in', 'not signed in — run `aipay login` first', EXIT.problem);
14
- const f = gatedFetch(env);
15
- if (auth.expires_at - 30_000 < Date.now())
16
- auth = await refresh(store, auth, f);
17
- const api = {
18
- auth,
19
- async request(method, path, opts = {}) {
20
- const call = async (a) => f(new URL(path, a.api_base), {
21
- method,
22
- headers: {
23
- authorization: `Bearer ${a.access_token}`,
24
- ...(opts.body !== undefined ? { 'content-type': 'application/json' } : {}),
25
- ...(opts.headers ?? {}),
26
- },
27
- ...(opts.body !== undefined ? { body: JSON.stringify(opts.body) } : {}),
28
- });
29
- let res = await call(api.auth);
30
- if (res.status === 401 && api.auth.refresh_token) {
31
- api.auth = await refresh(store, api.auth, f);
32
- res = await call(api.auth);
33
- }
34
- const text = await res.text();
35
- if (looksGated(res.status, text))
36
- throw new CliError('gated', GATE_HINT, EXIT.remote);
37
- if (res.status === 401)
38
- throw new CliError('unauthorized', 'the API rejected the developer token — run `aipay login` again', EXIT.remote);
39
- let body = null;
40
- try {
41
- body = text ? JSON.parse(text) : null;
42
- }
43
- catch {
44
- body = { raw: text.slice(0, 500) };
45
- }
46
- return { status: res.status, body: body };
47
- },
48
- };
49
- return api;
50
- }
51
- async function refresh(store, auth, fetchImpl) {
52
- if (!auth.refresh_token)
53
- throw new CliError('expired', 'the developer token expired — run `aipay login` again', EXIT.problem);
54
- const t = await refreshAccessToken({
55
- hydraPublicUrl: auth.auth_origin,
56
- clientId: auth.client_id,
57
- }, auth.refresh_token, { fetchImpl }).catch(() => {
58
- throw new CliError('expired', 'could not renew the developer token — run `aipay login` again', EXIT.problem);
59
- });
60
- const next = {
61
- ...auth,
62
- access_token: t.accessToken,
63
- refresh_token: t.refreshToken ?? auth.refresh_token,
64
- expires_at: Date.now() + t.expiresInSeconds * 1000,
65
- scope: t.scope || auth.scope,
66
- };
67
- store.save(next);
68
- return next;
69
- }
70
- export function errorMessage(body, status) {
71
- const b = (body ?? {});
72
- const msg = typeof b.message === 'string' ? b.message : typeof b.error === 'string' ? b.error : '';
73
- return msg ? `${msg} (HTTP ${status})` : `HTTP ${status}`;
74
- }
package/dist/gate.d.ts DELETED
@@ -1,13 +0,0 @@
1
- /**
2
- * Pilot gate: during the closed pilot, non-public paths answer 404 with an empty body
3
- * unless the request carries `X-AIPay-Gate: <token>` (or the browser cookie set by
4
- * `/__gate/<token>`). The CLI only ever reads the token from the environment — it is
5
- * never written to any file and never printed.
6
- */
7
- export declare const GATE_ENV = "AIPAY_GATE_TOKEN";
8
- export declare function gateHeaders(env?: NodeJS.ProcessEnv): Record<string, string>;
9
- /** fetch wrapper that adds the gate header; passed to the SDK as `fetchImpl`. */
10
- export declare function gatedFetch(env?: NodeJS.ProcessEnv): typeof fetch;
11
- /** Explain a 404-with-empty-body in gate terms (the SDK/API otherwise say "not found"). */
12
- export declare function looksGated(status: number, bodyText: string): boolean;
13
- export declare const GATE_HINT = "AI-Pay answered 404 with an empty body \u2014 that is the pilot gate, not a missing route. Set AIPAY_GATE_TOKEN in your environment (the value is in the Console next to the app) and retry. Do not put it in code or version control.";
package/dist/gate.js DELETED
@@ -1,29 +0,0 @@
1
- /**
2
- * Pilot gate: during the closed pilot, non-public paths answer 404 with an empty body
3
- * unless the request carries `X-AIPay-Gate: <token>` (or the browser cookie set by
4
- * `/__gate/<token>`). The CLI only ever reads the token from the environment — it is
5
- * never written to any file and never printed.
6
- */
7
- export const GATE_ENV = 'AIPAY_GATE_TOKEN';
8
- export function gateHeaders(env = process.env) {
9
- const token = env[GATE_ENV];
10
- return token ? { 'X-AIPay-Gate': token } : {};
11
- }
12
- /** fetch wrapper that adds the gate header; passed to the SDK as `fetchImpl`. */
13
- export function gatedFetch(env = process.env) {
14
- const extra = gateHeaders(env);
15
- if (!Object.keys(extra).length)
16
- return fetch;
17
- return (input, init) => {
18
- const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
19
- for (const [k, v] of Object.entries(extra))
20
- if (!headers.has(k))
21
- headers.set(k, v);
22
- return fetch(input, { ...init, headers });
23
- };
24
- }
25
- /** Explain a 404-with-empty-body in gate terms (the SDK/API otherwise say "not found"). */
26
- export function looksGated(status, bodyText) {
27
- return status === 404 && bodyText.trim() === '';
28
- }
29
- export const GATE_HINT = `AI-Pay answered 404 with an empty body — that is the pilot gate, not a missing route. Set ${GATE_ENV} in your environment (the value is in the Console next to the app) and retry. Do not put it in code or version control.`;
package/dist/index.d.ts DELETED
@@ -1,22 +0,0 @@
1
- /** Programmatic surface (used by tests and by anyone embedding the commands). */
2
- export { AGENT_GUIDE_FILE, agentGuide } from './agent-guide.js';
3
- export { type ParsedArgs, parseArgs } from './args.js';
4
- export { configDir, createAuthStore, fileStore, type StoredAuth } from './auth-store.js';
5
- export { createApp, runApps } from './commands/apps.js';
6
- export { collectChecks, runDoctor } from './commands/doctor.js';
7
- export { DEFAULT_CALLBACK, runInit, SDK_PACKAGE } from './commands/init.js';
8
- export { fetchCliConfig, managementAuthorizeUrl, resolveApiBase, runLogin, } from './commands/login.js';
9
- export { runLogout, runWhoami } from './commands/session.js';
10
- export { runStatus } from './commands/status.js';
11
- export { formatUusd, pickModel, runTest } from './commands/test.js';
12
- export { runUnlink, stripEnvKeys } from './commands/unlink.js';
13
- export { detectFramework, detectPackageManager, detectProject, installCommand } from './detect.js';
14
- export { openDeveloperApi } from './developer-api.js';
15
- export { GATE_ENV, gatedFetch, gateHeaders, looksGated } from './gate.js';
16
- export { parseCallback, startLoopback } from './loopback.js';
17
- export { main } from './main.js';
18
- export { CliError, createReporter, EXIT, type Reporter } from './output.js';
19
- export { ENV_KEYS, isProjectConfig, mergeEnvFile, mergeGitignore, type ProjectConfig, readProject, writeProject, } from './project.js';
20
- export { createRemote, docsUrl, type Remote } from './remote.js';
21
- export { scaffoldFiles, startPathFor } from './templates.js';
22
- export { cliVersion } from './version.js';
package/dist/index.js DELETED
@@ -1,22 +0,0 @@
1
- /** Programmatic surface (used by tests and by anyone embedding the commands). */
2
- export { AGENT_GUIDE_FILE, agentGuide } from './agent-guide.js';
3
- export { parseArgs } from './args.js';
4
- export { configDir, createAuthStore, fileStore } from './auth-store.js';
5
- export { createApp, runApps } from './commands/apps.js';
6
- export { collectChecks, runDoctor } from './commands/doctor.js';
7
- export { DEFAULT_CALLBACK, runInit, SDK_PACKAGE } from './commands/init.js';
8
- export { fetchCliConfig, managementAuthorizeUrl, resolveApiBase, runLogin, } from './commands/login.js';
9
- export { runLogout, runWhoami } from './commands/session.js';
10
- export { runStatus } from './commands/status.js';
11
- export { formatUusd, pickModel, runTest } from './commands/test.js';
12
- export { runUnlink, stripEnvKeys } from './commands/unlink.js';
13
- export { detectFramework, detectPackageManager, detectProject, installCommand } from './detect.js';
14
- export { openDeveloperApi } from './developer-api.js';
15
- export { GATE_ENV, gatedFetch, gateHeaders, looksGated } from './gate.js';
16
- export { parseCallback, startLoopback } from './loopback.js';
17
- export { main } from './main.js';
18
- export { CliError, createReporter, EXIT } from './output.js';
19
- export { ENV_KEYS, isProjectConfig, mergeEnvFile, mergeGitignore, readProject, writeProject, } from './project.js';
20
- export { createRemote, docsUrl } from './remote.js';
21
- export { scaffoldFiles, startPathFor } from './templates.js';
22
- export { cliVersion } from './version.js';
@@ -1,37 +0,0 @@
1
- /**
2
- * Loopback redirect receiver (RFC 8252 §7.3) for `aipay test`: listens on the exact
3
- * registered callback URI (host + port + path), captures ONE redirect, answers the
4
- * browser with a small HTML page, then closes. The authorization code never touches
5
- * disk; the token exchange happens in the caller's memory.
6
- */
7
- import { type Server } from 'node:http';
8
- export interface LoopbackTarget {
9
- host: string;
10
- port: number;
11
- path: string;
12
- }
13
- /**
14
- * Hydra compares loopback redirect URIs per RFC 8252 §7.3: for 127.0.0.1 / [::1] the port
15
- * is ignored, so the CLI may receive the callback on any free port while the app keeps
16
- * its registered URI. `localhost` is NOT treated as loopback by the server — exact port.
17
- */
18
- export declare function portIsVariable(target: LoopbackTarget): boolean;
19
- /** The redirect_uri to send for a receiver bound to `port` (same host + path). */
20
- export declare function redirectUriFor(target: LoopbackTarget, port: number): string;
21
- export declare function parseCallback(callback: string): LoopbackTarget;
22
- export interface LoopbackResult {
23
- /** Full callback URL as received (query included) — hand it to the SDK's handleCallback. */
24
- url: string;
25
- }
26
- export interface LoopbackHandle {
27
- /** Resolves with the first request on the callback path; rejects on timeout. */
28
- waitForCallback: Promise<LoopbackResult>;
29
- close(): Promise<void>;
30
- server: Server;
31
- /** Port actually bound (differs from target.port when target.port is 0 = ephemeral). */
32
- port: number;
33
- }
34
- export declare function startLoopback(target: LoopbackTarget, opts?: {
35
- timeoutMs?: number;
36
- pageHtml?: (ok: boolean) => string;
37
- }): Promise<LoopbackHandle>;
package/dist/loopback.js DELETED
@@ -1,95 +0,0 @@
1
- /**
2
- * Loopback redirect receiver (RFC 8252 §7.3) for `aipay test`: listens on the exact
3
- * registered callback URI (host + port + path), captures ONE redirect, answers the
4
- * browser with a small HTML page, then closes. The authorization code never touches
5
- * disk; the token exchange happens in the caller's memory.
6
- */
7
- import { createServer } from 'node:http';
8
- /**
9
- * Hydra compares loopback redirect URIs per RFC 8252 §7.3: for 127.0.0.1 / [::1] the port
10
- * is ignored, so the CLI may receive the callback on any free port while the app keeps
11
- * its registered URI. `localhost` is NOT treated as loopback by the server — exact port.
12
- */
13
- export function portIsVariable(target) {
14
- return target.host === '127.0.0.1' || target.host === '::1';
15
- }
16
- /** The redirect_uri to send for a receiver bound to `port` (same host + path). */
17
- export function redirectUriFor(target, port) {
18
- const host = target.host === '::1' ? '[::1]' : target.host;
19
- return `http://${host}:${port}${target.path}`;
20
- }
21
- export function parseCallback(callback) {
22
- const u = new URL(callback);
23
- if (u.protocol !== 'http:')
24
- throw new Error(`callback must be http:// on a loopback host (got ${callback})`);
25
- const host = u.hostname;
26
- if (host !== '127.0.0.1' && host !== 'localhost' && host !== '[::1]' && host !== '::1')
27
- throw new Error(`callback host must be a loopback address (got ${host})`);
28
- const port = u.port ? Number(u.port) : 80;
29
- return { host: host === '[::1]' ? '::1' : host, port, path: u.pathname || '/' };
30
- }
31
- export function startLoopback(target, opts = {}) {
32
- const timeoutMs = opts.timeoutMs ?? 5 * 60_000;
33
- const page = opts.pageHtml ?? defaultPage;
34
- return new Promise((resolve, reject) => {
35
- let settle = null;
36
- let fail = null;
37
- const waitForCallback = new Promise((res, rej) => {
38
- settle = res;
39
- fail = rej;
40
- });
41
- const server = createServer((req, res) => {
42
- const addr = server.address();
43
- const boundPort = typeof addr === 'object' && addr ? addr.port : target.port;
44
- const url = new URL(req.url ?? '/', redirectUriFor(target, boundPort));
45
- if (url.pathname !== target.path) {
46
- res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
47
- res.end('not the callback path');
48
- return;
49
- }
50
- const ok = !url.searchParams.get('error');
51
- res.writeHead(200, {
52
- 'content-type': 'text/html; charset=utf-8',
53
- 'cache-control': 'no-store',
54
- 'referrer-policy': 'no-referrer',
55
- });
56
- res.end(page(ok));
57
- settle?.({ url: url.toString() });
58
- settle = null;
59
- });
60
- const timer = setTimeout(() => {
61
- fail?.(new Error(`no callback received within ${Math.round(timeoutMs / 1000)}s`));
62
- fail = null;
63
- }, timeoutMs);
64
- server.on('error', (err) => {
65
- clearTimeout(timer);
66
- reject(err);
67
- });
68
- server.listen(target.port, target.host, () => {
69
- const addr = server.address();
70
- const port = typeof addr === 'object' && addr ? addr.port : target.port;
71
- resolve({
72
- waitForCallback,
73
- server,
74
- port,
75
- close: () => new Promise((done) => {
76
- clearTimeout(timer);
77
- server.close(() => done());
78
- // Keep-alive connections would otherwise hold the process open.
79
- server.closeAllConnections?.();
80
- }),
81
- });
82
- });
83
- });
84
- }
85
- function defaultPage(ok) {
86
- const title = ok ? 'AI-Pay: signed in' : 'AI-Pay: authorization failed';
87
- const body = ok
88
- ? 'You can close this tab and return to the terminal.'
89
- : 'The terminal shows the error. You can close this tab.';
90
- return `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${title}</title>
91
- <style>body{font:16px/1.5 system-ui,sans-serif;margin:0;display:grid;place-items:center;min-height:100vh;background:#f6f7f9;color:#1b1f24}
92
- main{padding:32px 40px;border-radius:12px;background:#fff;box-shadow:0 2px 12px rgba(0,0,0,.08);text-align:center}
93
- h1{font-size:20px;margin:0 0 8px}</style></head>
94
- <body><main><h1>${title}</h1><p>${body}</p></main></body></html>`;
95
- }