@coswic/aipay 0.3.0 → 0.4.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/CHANGELOG.md +40 -0
- package/README.md +5 -9
- package/dist/agent-guide.d.ts +2 -2
- package/dist/agent-guide.js +4 -4
- package/dist/commands/apps.js +1 -1
- package/dist/commands/doctor.d.ts +1 -2
- package/dist/commands/doctor.js +6 -14
- package/dist/commands/init.js +7 -11
- package/dist/commands/login.d.ts +1 -1
- package/dist/commands/login.js +9 -11
- package/dist/commands/session.js +2 -3
- package/dist/commands/test.js +4 -7
- package/dist/developer-api.d.ts +1 -1
- package/dist/developer-api.js +7 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/main.d.ts +1 -1
- package/dist/main.js +0 -1
- package/dist/network.d.ts +3 -0
- package/dist/network.js +5 -0
- package/dist/project.js +1 -2
- package/dist/remote.d.ts +6 -6
- package/dist/remote.js +10 -10
- package/dist/setup-events.js +1 -1
- package/package.json +4 -3
- package/dist/gate.d.ts +0 -13
- package/dist/gate.js +0 -29
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# @coswic/aipay
|
|
2
|
+
|
|
3
|
+
## 0.4.0 — 2026-09-08
|
|
4
|
+
|
|
5
|
+
- Simplify setup instructions around standard OAuth and HTTPS connectivity.
|
|
6
|
+
- Report empty 404 responses without inferring their cause.
|
|
7
|
+
- Remove the retired `AIPAY_GATE_TOKEN` setting and automatic `X-AIPay-Gate` header injection. OAuth tokens, PKCE, device authorization, refresh and revocation retain their existing behavior.
|
|
8
|
+
- **Programmatic compatibility:** remove the `GATE_ENV`, `gatedFetch`, `gateHeaders` and `looksGated` exports; use `isEmpty404` to identify an empty 404 response. Remove the gate-only environment arguments from `createRemote`, `fetchCliConfig`, `openDeveloperApi` and the doctor dependency interface. `Remote.fetchDocs`/`probe` results and init JSON use `empty404` instead of `gated`; the corresponding error code is `empty_404`.
|
|
9
|
+
|
|
10
|
+
## 0.3.0
|
|
11
|
+
|
|
12
|
+
- `aipay init --framework next|express|fastify|hono|vite|node` overrides framework detection.
|
|
13
|
+
- Vite (browser SPA) projects: `init` writes a browser helper (`aipay.ts`, PKCE in the page,
|
|
14
|
+
tokens in sessionStorage) plus `aipay-boot.ts`, and `VITE_AIPAY_*` aliases in `.env.local`
|
|
15
|
+
(Vite exposes only VITE_-prefixed values). Server frameworks still win when both are present.
|
|
16
|
+
- `aipay test` reports failures to the Console too (`cli.test` with `ok:false` and an error
|
|
17
|
+
code), so the verify step can show what went wrong.
|
|
18
|
+
|
|
19
|
+
## 0.2.1
|
|
20
|
+
|
|
21
|
+
Same content as 0.2.0. Version 0.2.0 on npm was published by mistake without `dist` (the
|
|
22
|
+
tarball only had README and package.json); it is deprecated and `latest` points past it.
|
|
23
|
+
Install 0.2.1 or later.
|
|
24
|
+
|
|
25
|
+
## 0.2.0
|
|
26
|
+
|
|
27
|
+
- `aipay login --device`: RFC 8628 device authorization for machines without a browser (SSH
|
|
28
|
+
boxes, containers, CI). Prints a URL and a short code; approve on any device; the CLI polls.
|
|
29
|
+
Requires an AI-Pay whose `/v1/cli/config` reports `device_authorization: true`.
|
|
30
|
+
- Windows: developer tokens are now protected with DPAPI (Windows PowerShell
|
|
31
|
+
`ConvertFrom-SecureString`) in `%APPDATA%\aipay\credentials.dpapi` instead of a plaintext
|
|
32
|
+
`credentials.json`. An existing plaintext file is read once and replaced on the next login.
|
|
33
|
+
`AIPAY_TOKEN_STORE=file` keeps the old behaviour.
|
|
34
|
+
- `aipay login` (browser flow) mentions `--device` when the server supports it.
|
|
35
|
+
|
|
36
|
+
## 0.1.0
|
|
37
|
+
|
|
38
|
+
- First release: `init`, `test`, `doctor`, `status`, `unlink`, `login`/`logout`/`whoami`,
|
|
39
|
+
`apps list`/`apps create`; loopback PKCE on any free port; tokens in the OS keychain or a
|
|
40
|
+
0600 file; setup progress reported to the Console.
|
package/README.md
CHANGED
|
@@ -37,16 +37,12 @@ own. Nothing is written to the project; tokens go to the keychain or the 0600 fi
|
|
|
37
37
|
like the browser flow. With `--json`, the URL and code are echoed on stderr as
|
|
38
38
|
`device_url …` / `user_code …` so an agent can show them to you.
|
|
39
39
|
|
|
40
|
-
##
|
|
40
|
+
## Connection troubleshooting
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
request
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
export AIPAY_GATE_TOKEN=<token from the Console>
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
The CLI never writes the token to any file and never prints it.
|
|
42
|
+
Check the configured API and authorization server URLs and network connectivity if a
|
|
43
|
+
request fails. An empty `404` alone does not identify the cause; it does not establish
|
|
44
|
+
whether a path is missing or why the server returned that response. Run `aipay doctor`
|
|
45
|
+
to check the configured endpoints before retrying.
|
|
50
46
|
|
|
51
47
|
## What gets written
|
|
52
48
|
|
package/dist/agent-guide.d.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* `aipay-agent.md`: the file a coding agent reads first. Header = the facts of THIS
|
|
3
3
|
* project (app, endpoints, what was scaffolded, what "done" means); body = the AI-Pay
|
|
4
4
|
* developer docs fetched from the API (`/docs.md?lang=en`), so the agent does not need
|
|
5
|
-
* network access
|
|
5
|
+
* network access to read them later.
|
|
6
6
|
*
|
|
7
|
-
* Never contains tokens. If the docs
|
|
7
|
+
* Never contains tokens. If the docs cannot be fetched, the file still gets
|
|
8
8
|
* written with the header and a pointer, and the CLI warns.
|
|
9
9
|
*/
|
|
10
10
|
import type { ProjectConfig } from './project.js';
|
package/dist/agent-guide.js
CHANGED
|
@@ -33,8 +33,8 @@ ${files}
|
|
|
33
33
|
## Rules for agents
|
|
34
34
|
|
|
35
35
|
- Read the docs below before changing the integration; they are the source of truth.
|
|
36
|
-
-
|
|
37
|
-
|
|
36
|
+
- Use standard OAuth sign-in and app authorization.
|
|
37
|
+
- Keep OAuth tokens out of code, config files, and version control.
|
|
38
38
|
- Never request \`offline_access\` unless the product needs background renewal.
|
|
39
39
|
- Key user accounts on \`sub\` from the userinfo endpoint, never on email.
|
|
40
40
|
|
|
@@ -53,8 +53,8 @@ ${input.docsMarkdown.trim()}
|
|
|
53
53
|
# AI-Pay developer documentation
|
|
54
54
|
|
|
55
55
|
The docs could not be fetched when this file was written (see the \`aipay init\` output).
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
Check the endpoint URL and network connectivity, then read
|
|
57
|
+
${input.docsUrl} or run \`aipay init\` again. An empty 404 alone does not identify the cause.
|
|
58
58
|
`;
|
|
59
59
|
return head + body;
|
|
60
60
|
}
|
package/dist/commands/apps.js
CHANGED
|
@@ -24,7 +24,7 @@ export async function createApp(api, input) {
|
|
|
24
24
|
export async function runApps(args, out, env = process.env) {
|
|
25
25
|
const sub = args.positionals[0] ?? 'list';
|
|
26
26
|
const store = createAuthStore(env);
|
|
27
|
-
const api = await openDeveloperApi(store
|
|
27
|
+
const api = await openDeveloperApi(store);
|
|
28
28
|
if (sub === 'list') {
|
|
29
29
|
const res = await api.request('GET', '/v1/developer/apps');
|
|
30
30
|
if (res.status !== 200)
|
|
@@ -8,10 +8,9 @@ export interface Check {
|
|
|
8
8
|
}
|
|
9
9
|
export interface DoctorDeps {
|
|
10
10
|
remote?: Remote;
|
|
11
|
-
env?: NodeJS.ProcessEnv;
|
|
12
11
|
nodeVersion?: string;
|
|
13
12
|
}
|
|
14
13
|
export declare function runDoctor(args: ParsedArgs, out: Reporter, deps?: DoctorDeps): Promise<number>;
|
|
15
14
|
export declare function collectChecks(dir: string, opts: {
|
|
16
15
|
offline: boolean;
|
|
17
|
-
}, deps: DoctorDeps
|
|
16
|
+
}, deps: DoctorDeps): Promise<Check[]>;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -7,15 +7,13 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
7
7
|
import { join, resolve } from 'node:path';
|
|
8
8
|
import { flagBool, flagString } from '../args.js';
|
|
9
9
|
import { detectProject } from '../detect.js';
|
|
10
|
-
import { GATE_ENV } from '../gate.js';
|
|
11
10
|
import { parseCallback } from '../loopback.js';
|
|
12
11
|
import { EXIT } from '../output.js';
|
|
13
12
|
import { ENV_FILE, ENV_KEYS, readProject } from '../project.js';
|
|
14
|
-
import { createRemote, docsUrl,
|
|
13
|
+
import { createRemote, docsUrl, EMPTY_404_HINT } from '../remote.js';
|
|
15
14
|
export async function runDoctor(args, out, deps = {}) {
|
|
16
15
|
const dir = resolve(flagString(args.flags, 'dir') ?? '.');
|
|
17
|
-
const
|
|
18
|
-
const checks = await collectChecks(dir, { offline: flagBool(args.flags, 'offline') }, deps, env);
|
|
16
|
+
const checks = await collectChecks(dir, { offline: flagBool(args.flags, 'offline') }, deps);
|
|
19
17
|
for (const c of checks)
|
|
20
18
|
out.line(c.level, c.text);
|
|
21
19
|
const problems = checks.filter((c) => c.level === 'bad');
|
|
@@ -24,7 +22,7 @@ export async function runDoctor(args, out, deps = {}) {
|
|
|
24
22
|
out.result({ ok: problems.length === 0, dir, checks });
|
|
25
23
|
return problems.length ? EXIT.problem : EXIT.ok;
|
|
26
24
|
}
|
|
27
|
-
export async function collectChecks(dir, opts, deps
|
|
25
|
+
export async function collectChecks(dir, opts, deps) {
|
|
28
26
|
const checks = [];
|
|
29
27
|
const major = Number((deps.nodeVersion ?? process.versions.node).split('.')[0]);
|
|
30
28
|
checks.push(major >= 20
|
|
@@ -114,14 +112,8 @@ export async function collectChecks(dir, opts, deps, env) {
|
|
|
114
112
|
if (!existsSync(join(dir, f)))
|
|
115
113
|
checks.push({ id: 'scaffold', level: 'warn', text: `scaffolded file removed: ${f}` });
|
|
116
114
|
}
|
|
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
115
|
if (!opts.offline) {
|
|
124
|
-
const remote = deps.remote ?? createRemote(
|
|
116
|
+
const remote = deps.remote ?? createRemote();
|
|
125
117
|
const oidc = await remote.probe(new URL('/.well-known/openid-configuration', project.auth_origin).toString());
|
|
126
118
|
checks.push(oidc.ok
|
|
127
119
|
? {
|
|
@@ -137,8 +129,8 @@ export async function collectChecks(dir, opts, deps, env) {
|
|
|
137
129
|
const api = await remote.probe(docsUrl(project.api_base, 'en'));
|
|
138
130
|
if (api.ok)
|
|
139
131
|
checks.push({ id: 'api', level: 'ok', text: `API reachable (${project.api_base})` });
|
|
140
|
-
else if (api.
|
|
141
|
-
checks.push({ id: 'api', level: 'bad', text: `API ${project.api_base}: ${
|
|
132
|
+
else if (api.empty404)
|
|
133
|
+
checks.push({ id: 'api', level: 'bad', text: `API ${project.api_base}: ${EMPTY_404_HINT}` });
|
|
142
134
|
else
|
|
143
135
|
checks.push({
|
|
144
136
|
id: 'api',
|
package/dist/commands/init.js
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Links the project to an app and scaffolds the integration. Idempotent: rerunning
|
|
6
6
|
* updates .aipay/project.json + .env.local and never overwrites files it did not write
|
|
7
|
-
* this run.
|
|
8
|
-
* token, if any, is read from the environment for the docs fetch.
|
|
7
|
+
* this run. Linking an existing app uses public client info.
|
|
9
8
|
*/
|
|
10
9
|
import { spawnSync } from 'node:child_process';
|
|
11
10
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
@@ -15,11 +14,10 @@ import { flagBool, flagString } from '../args.js';
|
|
|
15
14
|
import { createAuthStore } from '../auth-store.js';
|
|
16
15
|
import { detectProject, installCommand } from '../detect.js';
|
|
17
16
|
import { openDeveloperApi } from '../developer-api.js';
|
|
18
|
-
import { GATE_ENV } from '../gate.js';
|
|
19
17
|
import { parseCallback } from '../loopback.js';
|
|
20
18
|
import { CliError, EXIT } from '../output.js';
|
|
21
19
|
import { ENV_FILE, ENV_KEYS, FRAMEWORKS, isFramework, mergeEnvFile, mergeGitignore, readProject, VITE_ENV_KEYS, writeProject, } from '../project.js';
|
|
22
|
-
import { createRemote, docsUrl,
|
|
20
|
+
import { createRemote, docsUrl, EMPTY_404_HINT } from '../remote.js';
|
|
23
21
|
import { reportSetupEvent } from '../setup-events.js';
|
|
24
22
|
import { scaffoldFiles } from '../templates.js';
|
|
25
23
|
import { cliVersion } from '../version.js';
|
|
@@ -43,7 +41,7 @@ export async function runInit(args, out, deps = {}) {
|
|
|
43
41
|
throw new CliError('usage', 'pass either --app <client_id> or --create <name>, not both', EXIT.usage);
|
|
44
42
|
if (!login)
|
|
45
43
|
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)
|
|
44
|
+
const api = deps.developerApi ?? (await openDeveloperApi(createAuthStore(env)));
|
|
47
45
|
const created = await createApp(api, {
|
|
48
46
|
name: createName,
|
|
49
47
|
callback,
|
|
@@ -63,7 +61,7 @@ export async function runInit(args, out, deps = {}) {
|
|
|
63
61
|
const dryRun = flagBool(args.flags, 'dry-run');
|
|
64
62
|
const install = flagBool(args.flags, 'install', true);
|
|
65
63
|
const name = flagString(args.flags, 'name') ?? createdName ?? existing?.app.name ?? null;
|
|
66
|
-
const remote = deps.remote ?? createRemote(
|
|
64
|
+
const remote = deps.remote ?? createRemote();
|
|
67
65
|
const now = (deps.now ?? (() => new Date()))();
|
|
68
66
|
const det = detectProject(dir);
|
|
69
67
|
// --framework: detection can be wrong (monorepos, unusual layouts) — the developer/agent may
|
|
@@ -162,8 +160,8 @@ export async function runInit(args, out, deps = {}) {
|
|
|
162
160
|
project.scaffold.files = Array.from(new Set([...project.scaffold.files, ...scaffolded]));
|
|
163
161
|
// 5. docs → agent guide
|
|
164
162
|
const docs = await remote.fetchDocs(apiBase, 'en');
|
|
165
|
-
if (docs.
|
|
166
|
-
out.line('warn',
|
|
163
|
+
if (docs.empty404)
|
|
164
|
+
out.line('warn', EMPTY_404_HINT);
|
|
167
165
|
else if (!docs.markdown)
|
|
168
166
|
out.line('warn', `docs fetch failed (${docs.status}); guide written without the docs body`);
|
|
169
167
|
const guidePath = join(dir, AGENT_GUIDE_FILE);
|
|
@@ -219,8 +217,6 @@ export async function runInit(args, out, deps = {}) {
|
|
|
219
217
|
out.blank();
|
|
220
218
|
out.line('ok', dryRun ? 'dry run — nothing written' : `linked to app ${name ?? clientId}`);
|
|
221
219
|
out.line('info', 'next: run `aipay test` — it receives the callback on a free loopback port, so your dev server can keep running');
|
|
222
|
-
if (!env[GATE_ENV])
|
|
223
|
-
out.line('info', `pilot: if AI-Pay answers 404 with an empty body, export ${GATE_ENV} (Console → your app)`);
|
|
224
220
|
out.result({
|
|
225
221
|
ok: installExit === null || installExit === 0,
|
|
226
222
|
dry_run: dryRun,
|
|
@@ -229,7 +225,7 @@ export async function runInit(args, out, deps = {}) {
|
|
|
229
225
|
detection: det,
|
|
230
226
|
plan,
|
|
231
227
|
install: installCmd ? { command: installCmd, exit: installExit } : null,
|
|
232
|
-
docs: { fetched: !!docs.markdown,
|
|
228
|
+
docs: { fetched: !!docs.markdown, empty404: docs.empty404, status: docs.status },
|
|
233
229
|
reported,
|
|
234
230
|
});
|
|
235
231
|
return installExit === null || installExit === 0 ? EXIT.ok : EXIT.problem;
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface LoginDeps {
|
|
|
21
21
|
fetchImpl?: typeof fetch;
|
|
22
22
|
sleep?: (ms: number) => Promise<void>;
|
|
23
23
|
}
|
|
24
|
-
export declare function fetchCliConfig(apiBase: string
|
|
24
|
+
export declare function fetchCliConfig(apiBase: string): Promise<CliConfig>;
|
|
25
25
|
/** Resolve the API base: flag → env → linked project → error. */
|
|
26
26
|
export declare function resolveApiBase(args: ParsedArgs, env: NodeJS.ProcessEnv, dir: string): string;
|
|
27
27
|
/** Authorization URL for the management login. The SDK's createConnectUrl fixes the
|
package/dist/commands/login.js
CHANGED
|
@@ -15,23 +15,23 @@ import { flagBool, flagString } from '../args.js';
|
|
|
15
15
|
import { createAuthStore } from '../auth-store.js';
|
|
16
16
|
import { openBrowser } from '../browser.js';
|
|
17
17
|
import { pollDeviceToken, requestDeviceCode } from '../device.js';
|
|
18
|
-
import { GATE_ENV, gatedFetch, looksGated } from '../gate.js';
|
|
19
18
|
import { startLoopback } from '../loopback.js';
|
|
19
|
+
import { isEmpty404 } from '../network.js';
|
|
20
20
|
import { CliError, EXIT } from '../output.js';
|
|
21
21
|
import { readProject } from '../project.js';
|
|
22
|
-
import {
|
|
23
|
-
export async function fetchCliConfig(apiBase
|
|
24
|
-
const f =
|
|
22
|
+
import { EMPTY_404_HINT } from '../remote.js';
|
|
23
|
+
export async function fetchCliConfig(apiBase) {
|
|
24
|
+
const f = fetch;
|
|
25
25
|
let res;
|
|
26
26
|
try {
|
|
27
27
|
res = await f(new URL('/v1/cli/config', apiBase));
|
|
28
28
|
}
|
|
29
29
|
catch (e) {
|
|
30
|
-
throw new CliError('network', `cannot reach ${apiBase}: ${e.message}
|
|
30
|
+
throw new CliError('network', `cannot reach ${apiBase}: ${e.message}. Check the endpoint URL and network connectivity, then retry.`, EXIT.remote);
|
|
31
31
|
}
|
|
32
32
|
const text = await res.text();
|
|
33
|
-
if (
|
|
34
|
-
throw new CliError('
|
|
33
|
+
if (isEmpty404(res.status, text))
|
|
34
|
+
throw new CliError('empty_404', EMPTY_404_HINT, EXIT.remote);
|
|
35
35
|
if (res.status === 404)
|
|
36
36
|
throw new CliError('cli_disabled', `${apiBase} does not offer CLI sign-in (developer management is not enabled there)`, EXIT.remote);
|
|
37
37
|
if (!res.ok)
|
|
@@ -91,9 +91,9 @@ export async function runLogin(args, out, deps = {}) {
|
|
|
91
91
|
const env = deps.env ?? process.env;
|
|
92
92
|
const dir = flagString(args.flags, 'dir') ?? '.';
|
|
93
93
|
const apiBase = resolveApiBase(args, env, dir);
|
|
94
|
-
const cfg = await fetchCliConfig(apiBase
|
|
94
|
+
const cfg = await fetchCliConfig(apiBase);
|
|
95
95
|
const timeoutS = Number(flagString(args.flags, 'timeout') ?? '300');
|
|
96
|
-
const fetchImpl = deps.fetchImpl ??
|
|
96
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
97
97
|
if (flagBool(args.flags, 'device', false))
|
|
98
98
|
return deviceLogin(cfg, apiBase, timeoutS, fetchImpl, env, out, deps);
|
|
99
99
|
const loop = await startLoopback({ host: '127.0.0.1', port: 0, path: cfg.redirect_path }, { timeoutMs: timeoutS * 1000 });
|
|
@@ -174,8 +174,6 @@ async function finishLogin(tokens, cfg, apiBase, fetchImpl, env, out) {
|
|
|
174
174
|
out.line('info', `tokens kept in ${store.where}${store.kind === 'file' ? ' — no keychain tool found; the file is readable only by you' : ''}`);
|
|
175
175
|
if (!tokens.refreshToken)
|
|
176
176
|
out.line('warn', 'no refresh token was granted; you will need to sign in again when this one expires');
|
|
177
|
-
if (!env[GATE_ENV])
|
|
178
|
-
out.line('info', `pilot: if the API answers 404 with an empty body later, export ${GATE_ENV}`);
|
|
179
177
|
out.result({
|
|
180
178
|
ok: true,
|
|
181
179
|
api_base: apiBase,
|
package/dist/commands/session.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createAuthStore } from '../auth-store.js';
|
|
2
2
|
import { openDeveloperApi } from '../developer-api.js';
|
|
3
|
-
import { gatedFetch } from '../gate.js';
|
|
4
3
|
import { CliError, EXIT } from '../output.js';
|
|
5
4
|
export async function runWhoami(_args, out, env = process.env) {
|
|
6
5
|
const store = createAuthStore(env);
|
|
@@ -10,7 +9,7 @@ export async function runWhoami(_args, out, env = process.env) {
|
|
|
10
9
|
out.result({ signed_in: false });
|
|
11
10
|
return EXIT.ok;
|
|
12
11
|
}
|
|
13
|
-
const api = await openDeveloperApi(store
|
|
12
|
+
const api = await openDeveloperApi(store); // refreshes if needed
|
|
14
13
|
out.line('ok', `${api.auth.email ?? api.auth.sub} @ ${api.auth.api_base}`);
|
|
15
14
|
out.kv('scope', api.auth.scope || '—');
|
|
16
15
|
out.kv('expires', new Date(api.auth.expires_at).toISOString());
|
|
@@ -36,7 +35,7 @@ export async function runLogout(_args, out, env = process.env) {
|
|
|
36
35
|
}
|
|
37
36
|
// Best effort: revoke the refresh token (and with it the access token) at the authorization server.
|
|
38
37
|
let revoked = false;
|
|
39
|
-
const f =
|
|
38
|
+
const f = fetch;
|
|
40
39
|
for (const token of [auth.refresh_token, auth.access_token]) {
|
|
41
40
|
if (!token)
|
|
42
41
|
continue;
|
package/dist/commands/test.js
CHANGED
|
@@ -9,11 +9,9 @@
|
|
|
9
9
|
import { AIPayClient, AuthorizationError, ConnectError, createConnectUrl, getUserInfo, handleCallback, InsufficientBalanceError, } from '@coswic/aipay-sdk';
|
|
10
10
|
import { flagBool, flagString } from '../args.js';
|
|
11
11
|
import { openBrowser } from '../browser.js';
|
|
12
|
-
import { GATE_ENV, gatedFetch, looksGated } from '../gate.js';
|
|
13
12
|
import { parseCallback, portIsVariable, redirectUriFor, startLoopback } from '../loopback.js';
|
|
14
13
|
import { CliError, EXIT } from '../output.js';
|
|
15
14
|
import { readProject } from '../project.js';
|
|
16
|
-
import { GATE_HINT } from '../remote.js';
|
|
17
15
|
import { reportSetupEvent } from '../setup-events.js';
|
|
18
16
|
export async function runTest(args, out, deps = {}) {
|
|
19
17
|
const env = deps.env ?? process.env;
|
|
@@ -22,7 +20,7 @@ export async function runTest(args, out, deps = {}) {
|
|
|
22
20
|
if (!project)
|
|
23
21
|
throw new CliError('not_linked', 'this directory is not linked to an app — run `aipay init --app <client_id> ...` first', EXIT.problem);
|
|
24
22
|
const timeoutS = Number(flagString(args.flags, 'timeout') ?? '300');
|
|
25
|
-
const fetchImpl =
|
|
23
|
+
const fetchImpl = fetch;
|
|
26
24
|
const target = parseCallback(project.callback);
|
|
27
25
|
// 127.0.0.1: the authorization server ignores the port for loopback redirect URIs
|
|
28
26
|
// (RFC 8252 §7.3), so the CLI takes any free port and your dev server can keep running.
|
|
@@ -135,11 +133,10 @@ export async function runTest(args, out, deps = {}) {
|
|
|
135
133
|
return e.exitCode;
|
|
136
134
|
}
|
|
137
135
|
const msg = e.message ?? String(e);
|
|
138
|
-
const gated = /404/.test(msg) && looksGated(404, '');
|
|
139
136
|
result.error = { code: 'failed', message: msg };
|
|
140
|
-
out.line('bad',
|
|
141
|
-
if (
|
|
142
|
-
out.line('info',
|
|
137
|
+
out.line('bad', msg);
|
|
138
|
+
if (/\b404\b/.test(msg))
|
|
139
|
+
out.line('info', 'Check the endpoint URL and network connectivity before retrying.');
|
|
143
140
|
out.result(result);
|
|
144
141
|
return EXIT.remote;
|
|
145
142
|
}
|
package/dist/developer-api.d.ts
CHANGED
|
@@ -10,5 +10,5 @@ export interface DeveloperApi {
|
|
|
10
10
|
headers?: Record<string, string>;
|
|
11
11
|
}): Promise<ApiResult<T>>;
|
|
12
12
|
}
|
|
13
|
-
export declare function openDeveloperApi(store: AuthStore
|
|
13
|
+
export declare function openDeveloperApi(store: AuthStore): Promise<DeveloperApi>;
|
|
14
14
|
export declare function errorMessage(body: unknown, status: number): string;
|
package/dist/developer-api.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
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
|
|
4
|
-
* comes from the environment like everywhere else.
|
|
3
|
+
* aipay-developer-api). Refreshes once on 401 when a refresh token exists.
|
|
5
4
|
*/
|
|
6
5
|
import { refreshAccessToken } from '@coswic/aipay-sdk';
|
|
7
|
-
import {
|
|
6
|
+
import { isEmpty404 } from './network.js';
|
|
8
7
|
import { CliError, EXIT } from './output.js';
|
|
9
|
-
import {
|
|
10
|
-
export async function openDeveloperApi(store
|
|
8
|
+
import { EMPTY_404_HINT } from './remote.js';
|
|
9
|
+
export async function openDeveloperApi(store) {
|
|
11
10
|
let auth = store.load();
|
|
12
11
|
if (!auth)
|
|
13
12
|
throw new CliError('not_logged_in', 'not signed in — run `aipay login` first', EXIT.problem);
|
|
14
|
-
const f =
|
|
13
|
+
const f = fetch;
|
|
15
14
|
if (auth.expires_at - 30_000 < Date.now())
|
|
16
15
|
auth = await refresh(store, auth, f);
|
|
17
16
|
const api = {
|
|
@@ -32,8 +31,8 @@ export async function openDeveloperApi(store, env = process.env) {
|
|
|
32
31
|
res = await call(api.auth);
|
|
33
32
|
}
|
|
34
33
|
const text = await res.text();
|
|
35
|
-
if (
|
|
36
|
-
throw new CliError('
|
|
34
|
+
if (isEmpty404(res.status, text))
|
|
35
|
+
throw new CliError('empty_404', EMPTY_404_HINT, EXIT.remote);
|
|
37
36
|
if (res.status === 401)
|
|
38
37
|
throw new CliError('unauthorized', 'the API rejected the developer token — run `aipay login` again', EXIT.remote);
|
|
39
38
|
let body = null;
|
package/dist/index.d.ts
CHANGED
|
@@ -12,9 +12,9 @@ export { formatUusd, pickModel, runTest } from './commands/test.js';
|
|
|
12
12
|
export { runUnlink, stripEnvKeys } from './commands/unlink.js';
|
|
13
13
|
export { detectFramework, detectPackageManager, detectProject, installCommand } from './detect.js';
|
|
14
14
|
export { openDeveloperApi } from './developer-api.js';
|
|
15
|
-
export { GATE_ENV, gatedFetch, gateHeaders, looksGated } from './gate.js';
|
|
16
15
|
export { parseCallback, startLoopback } from './loopback.js';
|
|
17
16
|
export { main } from './main.js';
|
|
17
|
+
export { isEmpty404 } from './network.js';
|
|
18
18
|
export { CliError, createReporter, EXIT, type Reporter } from './output.js';
|
|
19
19
|
export { ENV_KEYS, isProjectConfig, mergeEnvFile, mergeGitignore, type ProjectConfig, readProject, writeProject, } from './project.js';
|
|
20
20
|
export { createRemote, docsUrl, type Remote } from './remote.js';
|
package/dist/index.js
CHANGED
|
@@ -12,9 +12,9 @@ export { formatUusd, pickModel, runTest } from './commands/test.js';
|
|
|
12
12
|
export { runUnlink, stripEnvKeys } from './commands/unlink.js';
|
|
13
13
|
export { detectFramework, detectPackageManager, detectProject, installCommand } from './detect.js';
|
|
14
14
|
export { openDeveloperApi } from './developer-api.js';
|
|
15
|
-
export { GATE_ENV, gatedFetch, gateHeaders, looksGated } from './gate.js';
|
|
16
15
|
export { parseCallback, startLoopback } from './loopback.js';
|
|
17
16
|
export { main } from './main.js';
|
|
17
|
+
export { isEmpty404 } from './network.js';
|
|
18
18
|
export { CliError, createReporter, EXIT } from './output.js';
|
|
19
19
|
export { ENV_KEYS, isProjectConfig, mergeEnvFile, mergeGitignore, readProject, writeProject, } from './project.js';
|
|
20
20
|
export { createRemote, docsUrl } from './remote.js';
|
package/dist/main.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const HELP = "aipay \u2014 AI-Pay CLI\n\nUsage\n aipay login [--api <base>] [--device] [--json] developer sign-in for the CLI itself\n aipay whoami [--json] \u00B7 aipay logout [--json]\n aipay apps list [--json]\n aipay apps create --name <name> [--callback <url>] [--json]\n aipay init --app <client_id> [--auth <origin>] [--api <base>] [--callback <url>] [--name <app name>]\n [--framework next|express|fastify|hono|vite|node] override detection\n [--dir <path>] [--dry-run] [--no-install] [--json]\n aipay init --create <app name> [--api <base>] ... create the app (needs login) and link it\n aipay test [--model <id>] [--port <n>] [--no-open] [--timeout <seconds>] [--dir <path>] [--json]\n aipay doctor [--offline] [--dir <path>] [--json]\n aipay status [--dir <path>] [--json]\n aipay unlink [--dry-run] [--dir <path>] [--json]\n\nlogin sign in as a developer (loopback PKCE, audience aipay-developer-api); tokens go to the\n OS keychain or a 0600 file \u2014 never into the project\napps list your apps / create one (Idempotency-Key: a retried create never makes a second app)\ninit link this project to an app: .aipay/project.json, .env.local, .gitignore,\n a callback route for your framework, aipay-agent.md, and the SDK install\n (--auth/--api are optional once you are signed in; --create makes the app first)\ntest real sign-in via a loopback callback (any free port on 127.0.0.1) + one chat completion \u2192 receipt\ndoctor check the link, files, SDK, and that AI-Pay is reachable\nstatus what this directory is linked to\nunlink forget the link (keeps your code)\n\nExit codes: 0 ok \u00B7 1 problem you can fix \u00B7 2 usage \u00B7 3 network/auth\
|
|
1
|
+
export declare const HELP = "aipay \u2014 AI-Pay CLI\n\nUsage\n aipay login [--api <base>] [--device] [--json] developer sign-in for the CLI itself\n aipay whoami [--json] \u00B7 aipay logout [--json]\n aipay apps list [--json]\n aipay apps create --name <name> [--callback <url>] [--json]\n aipay init --app <client_id> [--auth <origin>] [--api <base>] [--callback <url>] [--name <app name>]\n [--framework next|express|fastify|hono|vite|node] override detection\n [--dir <path>] [--dry-run] [--no-install] [--json]\n aipay init --create <app name> [--api <base>] ... create the app (needs login) and link it\n aipay test [--model <id>] [--port <n>] [--no-open] [--timeout <seconds>] [--dir <path>] [--json]\n aipay doctor [--offline] [--dir <path>] [--json]\n aipay status [--dir <path>] [--json]\n aipay unlink [--dry-run] [--dir <path>] [--json]\n\nlogin sign in as a developer (loopback PKCE, audience aipay-developer-api); tokens go to the\n OS keychain or a 0600 file \u2014 never into the project\napps list your apps / create one (Idempotency-Key: a retried create never makes a second app)\ninit link this project to an app: .aipay/project.json, .env.local, .gitignore,\n a callback route for your framework, aipay-agent.md, and the SDK install\n (--auth/--api are optional once you are signed in; --create makes the app first)\ntest real sign-in via a loopback callback (any free port on 127.0.0.1) + one chat completion \u2192 receipt\ndoctor check the link, files, SDK, and that AI-Pay is reachable\nstatus what this directory is linked to\nunlink forget the link (keeps your code)\n\nExit codes: 0 ok \u00B7 1 problem you can fix \u00B7 2 usage \u00B7 3 network/auth\n";
|
|
2
2
|
export declare function main(argv: string[], io?: {
|
|
3
3
|
stdout?: (s: string) => void;
|
|
4
4
|
stderr?: (s: string) => void;
|
package/dist/main.js
CHANGED
|
@@ -37,7 +37,6 @@ status what this directory is linked to
|
|
|
37
37
|
unlink forget the link (keeps your code)
|
|
38
38
|
|
|
39
39
|
Exit codes: 0 ok · 1 problem you can fix · 2 usage · 3 network/auth
|
|
40
|
-
Pilot gate: export AIPAY_GATE_TOKEN=<token from the Console>; never write it to files.
|
|
41
40
|
`;
|
|
42
41
|
export async function main(argv, io) {
|
|
43
42
|
const args = parseArgs(argv);
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Describe the observed response without inferring its network or server-side cause. */
|
|
2
|
+
export declare function isEmpty404(status: number, bodyText: string): boolean;
|
|
3
|
+
export declare const EMPTY_404_HINT = "AI-Pay answered 404 with an empty body. Check the endpoint URL and network connectivity, then retry. An empty 404 alone does not identify the cause.";
|
package/dist/network.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Describe the observed response without inferring its network or server-side cause. */
|
|
2
|
+
export function isEmpty404(status, bodyText) {
|
|
3
|
+
return status === 404 && bodyText.trim() === '';
|
|
4
|
+
}
|
|
5
|
+
export const EMPTY_404_HINT = 'AI-Pay answered 404 with an empty body. Check the endpoint URL and network connectivity, then retry. An empty 404 alone does not identify the cause.';
|
package/dist/project.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Local artifacts (consultant report §6.5):
|
|
3
3
|
* .aipay/project.json — committable: which app this project is linked to (no secrets)
|
|
4
|
-
* .env.local — NOT committed: only public values
|
|
5
|
-
* gate token is read from the environment and never written
|
|
4
|
+
* .env.local — NOT committed: only public values (client_id, URLs)
|
|
6
5
|
* .gitignore — gets `.env.local` and `.aipay/local/` appended once
|
|
7
6
|
*
|
|
8
7
|
* Tokens live only in memory for the duration of `aipay test`. Nothing here writes them.
|
package/dist/remote.d.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Everything that talks to AI-Pay over the network, in one place so the commands stay
|
|
3
|
-
* readable and tests can stub it.
|
|
3
|
+
* readable and tests can stub it.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { EMPTY_404_HINT } from './network.js';
|
|
6
6
|
export interface Remote {
|
|
7
7
|
fetchDocs(apiBase: string, locale?: 'en' | 'zh-Hant'): Promise<{
|
|
8
8
|
markdown: string | null;
|
|
9
|
-
|
|
9
|
+
empty404: boolean;
|
|
10
10
|
status: number;
|
|
11
11
|
}>;
|
|
12
12
|
probe(url: string): Promise<{
|
|
13
13
|
ok: boolean;
|
|
14
14
|
status: number;
|
|
15
|
-
|
|
15
|
+
empty404: boolean;
|
|
16
16
|
error?: string;
|
|
17
17
|
}>;
|
|
18
18
|
}
|
|
19
|
-
export declare function createRemote(
|
|
19
|
+
export declare function createRemote(): Remote;
|
|
20
20
|
export declare function docsUrl(apiBase: string, locale?: 'en' | 'zh-Hant'): string;
|
|
21
|
-
export {
|
|
21
|
+
export { EMPTY_404_HINT };
|
package/dist/remote.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Everything that talks to AI-Pay over the network, in one place so the commands stay
|
|
3
|
-
* readable and tests can stub it.
|
|
3
|
+
* readable and tests can stub it.
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { EMPTY_404_HINT, isEmpty404 } from './network.js';
|
|
6
6
|
import { CliError, EXIT } from './output.js';
|
|
7
|
-
export function createRemote(
|
|
8
|
-
const f =
|
|
7
|
+
export function createRemote() {
|
|
8
|
+
const f = fetch;
|
|
9
9
|
return {
|
|
10
10
|
async fetchDocs(apiBase, locale = 'en') {
|
|
11
11
|
const url = docsUrl(apiBase, locale);
|
|
@@ -14,21 +14,21 @@ export function createRemote(env = process.env) {
|
|
|
14
14
|
res = await f(url, { headers: { accept: 'text/markdown, text/plain' } });
|
|
15
15
|
}
|
|
16
16
|
catch (e) {
|
|
17
|
-
throw new CliError('network', `cannot reach ${url}: ${e.message}
|
|
17
|
+
throw new CliError('network', `cannot reach ${url}: ${e.message}. Check the endpoint URL and network connectivity, then retry.`, EXIT.remote);
|
|
18
18
|
}
|
|
19
19
|
const text = await res.text();
|
|
20
20
|
if (res.ok)
|
|
21
|
-
return { markdown: text,
|
|
22
|
-
return { markdown: null,
|
|
21
|
+
return { markdown: text, empty404: false, status: res.status };
|
|
22
|
+
return { markdown: null, empty404: isEmpty404(res.status, text), status: res.status };
|
|
23
23
|
},
|
|
24
24
|
async probe(url) {
|
|
25
25
|
try {
|
|
26
26
|
const res = await f(url, { method: 'GET', redirect: 'manual' });
|
|
27
27
|
const text = await res.text();
|
|
28
|
-
return { ok: res.ok, status: res.status,
|
|
28
|
+
return { ok: res.ok, status: res.status, empty404: isEmpty404(res.status, text) };
|
|
29
29
|
}
|
|
30
30
|
catch (e) {
|
|
31
|
-
return { ok: false, status: 0,
|
|
31
|
+
return { ok: false, status: 0, empty404: false, error: e.message };
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
};
|
|
@@ -38,4 +38,4 @@ export function docsUrl(apiBase, locale = 'en') {
|
|
|
38
38
|
u.searchParams.set('lang', locale);
|
|
39
39
|
return u.toString();
|
|
40
40
|
}
|
|
41
|
-
export {
|
|
41
|
+
export { EMPTY_404_HINT };
|
package/dist/setup-events.js
CHANGED
|
@@ -11,7 +11,7 @@ export async function reportSetupEvent(appId, kind, detail, out, env = process.e
|
|
|
11
11
|
if (!store.load())
|
|
12
12
|
return false; // not signed in with the CLI → the Console still sees server facts
|
|
13
13
|
try {
|
|
14
|
-
const api = await openDeveloperApi(store
|
|
14
|
+
const api = await openDeveloperApi(store);
|
|
15
15
|
const res = await api.request('POST', `/v1/developer/apps/${encodeURIComponent(appId)}/setup-events`, {
|
|
16
16
|
body: { kind, detail },
|
|
17
17
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coswic/aipay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"dist",
|
|
18
|
-
"README.md"
|
|
18
|
+
"README.md",
|
|
19
|
+
"CHANGELOG.md"
|
|
19
20
|
],
|
|
20
21
|
"publishConfig": {
|
|
21
22
|
"access": "public"
|
|
@@ -24,7 +25,7 @@
|
|
|
24
25
|
"node": ">=20"
|
|
25
26
|
},
|
|
26
27
|
"dependencies": {
|
|
27
|
-
"@coswic/aipay-sdk": "^0.3.
|
|
28
|
+
"@coswic/aipay-sdk": "^0.3.2"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
31
|
"@types/node": "^26.1.1",
|
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.`;
|