@coswic/aipay 0.8.0 → 0.8.1

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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # @coswic/aipay
2
2
 
3
+ ## 0.8.1 — 2026-10-11
4
+
5
+ - The sign-in helpers that `aipay init` generates, and the CLI's own sign-in, pass the SDK's new `authOrigin` option (the authorization server origin) instead of the option name the SDK now deprecates.
6
+ - `aipay init` upgrades an `@coswic/aipay-sdk` older than 0.5.1 that the project already has — the installed version, or the package.json range when nothing is installed yet — by running `<package manager> add @coswic/aipay-sdk@latest`; with `--no-install` it prints that command instead, and `--dry-run` shows it. The JSON result reports this as `sdk_upgrade`. `aipay doctor` warns when the installed SDK is older than 0.5.1.
7
+ - The rules section of the generated `aipay-agent.md` is now headed "Rules for the coding agent". README and code comments reworded; no other behavior change.
8
+ - Requires `@coswic/aipay-sdk` ^0.5.1.
9
+
3
10
  ## 0.8.0 — 2026-10-09
4
11
 
5
12
  - `aipay test`: a 401 from the API now says that, during the invitation-only beta, the signed-in user may hold no current invitation (or not have accepted the latest Terms) and that refreshing does not help; a sign-in that never reaches the loopback callback prints the same hint next to the timeout.
@@ -31,7 +38,7 @@
31
38
  - Add `apps callbacks list/add/remove` with the developer’s own management authorization and linked-project defaults.
32
39
  - Preserve other registered callbacks, make no-op changes idempotent, and refuse to remove the final callback.
33
40
  - Require server ETag support for mutations; concurrent edits and uncertain outcomes require readback instead of blind retries.
34
- - Guide agents through developer CLI login and callback management; local `init` remains distinct from server registration.
41
+ - Guide the coding agent through developer CLI login and callback management; local `init` remains distinct from server registration.
35
42
  - Allow HTTPS application callbacks in `init` and `doctor`; `test --callback` can use a separately registered loopback callback without changing project settings.
36
43
  - Bound developer API requests to 30 seconds and allow conditional mutations to disable automatic retries after a 401.
37
44
 
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @coswic/aipay — AI Pay CLI
2
2
 
3
3
  The command-line companion to [`@coswic/aipay-sdk`](https://www.npmjs.com/package/@coswic/aipay-sdk).
4
- Made for developers and for the coding agents they hand the Console prompt to.
4
+ Made for developers and for the coding agent they hand the Console prompt to.
5
5
 
6
6
  ```bash
7
7
  npx @coswic/aipay@latest login --api <api base>
@@ -30,7 +30,7 @@ AI Pay's production environment is in a free beta limited to invited users: only
30
30
  | `aipay apps callbacks list --app <client_id>` | Read the registered callback URLs. In a linked project, `--app` can be omitted; `--dir` selects the project. |
31
31
  | `aipay apps callbacks add --app <client_id> --callback <url>` | Add a registered callback while preserving the others. Adding an existing URL makes no write. |
32
32
  | `aipay apps callbacks remove --app <client_id> --callback <url>` | Remove one callback; removing an absent URL makes no write. The final callback cannot be removed. |
33
- | `aipay init` | Links the project to your app: writes `.aipay/project.json` (commit it), `.env.local` (public values only, git-ignored), a start/callback route for your framework (Next.js, Express, Fastify, Hono, plain Node) or a browser helper for a Vite SPA (`--framework` overrides detection), `aipay-agent.md` (the developer docs, fetched once, for your agent to read offline) and installs the SDK. Never overwrites a file it did not write this run. `--dry-run` shows the plan. |
33
+ | `aipay init` | Links the project to your app: writes `.aipay/project.json` (commit it), `.env.local` (public values only, git-ignored), a start/callback route for your framework (Next.js, Express, Fastify, Hono, plain Node) or a browser helper for a Vite SPA (`--framework` overrides detection), `aipay-agent.md` (the developer docs, fetched once, for your agent to read offline) and installs the SDK — or upgrades it when the project already has one older than 0.5.1, which the generated code needs (`--no-install` prints the command instead). Never overwrites a file it did not write this run. `--dry-run` shows the plan. |
34
34
  | `aipay test` | The proof: a real sign-in through the AI Pay authorization server, received by the CLI on a loopback callback, then one chat completion on that user's behalf. Prints the receipt (`request_id`, amount charged). The Console's verify step turns green from the server side. The authorization server ignores the port for `127.0.0.1` redirect URIs (RFC 8252), so the CLI binds any free port and your dev server can keep running; `--port <n>` forces one. `--no-open` prints the URL instead of opening a browser; `--model <id>` picks the model. |
35
35
  | `aipay doctor` | Checks Node, the link, `.env.local`, the SDK install, and that the authorization server and API are reachable. `--offline` skips the network. |
36
36
  | `aipay status` | What this directory is linked to. |
@@ -102,7 +102,7 @@ to check the configured endpoints before retrying.
102
102
  ```
103
103
  .aipay/project.json # app client_id, endpoints, callback, what was scaffolded — no secrets
104
104
  .env.local # AIPAY_CLIENT_ID / AIPAY_AUTH_ORIGIN / AIPAY_API_BASE / AIPAY_CALLBACK_URL
105
- aipay-agent.md # project facts + the developer docs, for coding agents
105
+ aipay-agent.md # project facts + the developer docs, for your coding agent
106
106
  src/aipay.ts # sign-in helpers (start → callback → first completion)
107
107
  src/aipay-routes.ts # framework wiring (or app/auth/aipay/*/route.ts for Next.js)
108
108
  ```
@@ -33,7 +33,7 @@ ${files}
33
33
  3. ${verification}
34
34
  4. Typecheck/build pass. Do not commit automatically and do not deploy.
35
35
 
36
- ## Rules for agents
36
+ ## Rules for the coding agent
37
37
 
38
38
  - Read the docs below before changing the integration; they are the source of truth.
39
39
  - Use standard OAuth sign-in and app authorization.
package/dist/args.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Tiny argv parser — zero deps on purpose (the CLI is installed by coding agents in
2
+ * Tiny argv parser — zero deps on purpose (the CLI gets installed by a coding agent into
3
3
  * arbitrary projects; every dependency is attack surface and install time).
4
4
  *
5
5
  * Supports `--flag`, `--flag value`, `--flag=value`, `--no-flag`, and positionals.
@@ -7,7 +7,7 @@ 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 { parseAppCallback } from '../callback-url.js';
10
- import { detectProject } from '../detect.js';
10
+ import { detectProject, outdatedSdk, SDK_MIN_VERSION } from '../detect.js';
11
11
  import { EXIT } from '../output.js';
12
12
  import { ENV_FILE, ENV_KEYS, readProject } from '../project.js';
13
13
  import { createRemote, docsUrl, EMPTY_404_HINT } from '../remote.js';
@@ -95,19 +95,29 @@ export async function collectChecks(dir, opts, deps) {
95
95
  const det = detectProject(dir);
96
96
  if (det.hasPackageJson) {
97
97
  const installed = det.sdkInstalled && existsSync(join(dir, 'node_modules', '@coswic', 'aipay-sdk'));
98
- checks.push(installed
99
- ? { id: 'sdk', level: 'ok', text: '@coswic/aipay-sdk installed' }
100
- : det.sdkInstalled
101
- ? {
102
- id: 'sdk',
103
- level: 'bad',
104
- text: '@coswic/aipay-sdk is in package.json but not installed — run your package manager install',
105
- }
106
- : {
107
- id: 'sdk',
108
- level: 'bad',
109
- text: '@coswic/aipay-sdk not a dependency — rerun `aipay init` (or add it yourself)',
110
- });
98
+ // Only the installed version counts here (the spec alone says nothing about what runs).
99
+ const old = installed && det.sdkVersion
100
+ ? outdatedSdk({ sdkVersion: det.sdkVersion, sdkSpec: null })
101
+ : null;
102
+ checks.push(old
103
+ ? {
104
+ id: 'sdk',
105
+ level: 'warn',
106
+ text: `@coswic/aipay-sdk ${old} installed — code generated by \`aipay init\` needs ${SDK_MIN_VERSION} or later; rerun \`aipay init\` or upgrade it`,
107
+ }
108
+ : installed
109
+ ? { id: 'sdk', level: 'ok', text: '@coswic/aipay-sdk installed' }
110
+ : det.sdkInstalled
111
+ ? {
112
+ id: 'sdk',
113
+ level: 'bad',
114
+ text: '@coswic/aipay-sdk is in package.json but not installed — run your package manager install',
115
+ }
116
+ : {
117
+ id: 'sdk',
118
+ level: 'bad',
119
+ text: '@coswic/aipay-sdk not a dependency — rerun `aipay init` (or add it yourself)',
120
+ });
111
121
  for (const f of project.scaffold.files)
112
122
  if (!existsSync(join(dir, f)))
113
123
  checks.push({ id: 'scaffold', level: 'warn', text: `scaffolded file removed: ${f}` });
@@ -13,7 +13,7 @@ import { AGENT_GUIDE_FILE, agentGuide } from '../agent-guide.js';
13
13
  import { flagBool, flagString } from '../args.js';
14
14
  import { createAuthStore } from '../auth-store.js';
15
15
  import { parseAppCallback } from '../callback-url.js';
16
- import { detectProject, installCommand } from '../detect.js';
16
+ import { detectProject, installCommand, outdatedSdk, SDK_MIN_VERSION } from '../detect.js';
17
17
  import { openDeveloperApi } from '../developer-api.js';
18
18
  import { CliError, EXIT } from '../output.js';
19
19
  import { ENV_FILE, ENV_KEYS, FRAMEWORKS, isFramework, mergeEnvFile, mergeGitignore, readProject, VITE_ENV_KEYS, writeProject, } from '../project.js';
@@ -176,16 +176,23 @@ export async function runInit(args, out, deps = {}) {
176
176
  });
177
177
  plan.push({ path: AGENT_GUIDE_FILE, action: existsSync(guidePath) ? 'update' : 'write' });
178
178
  plan.push({ path: '.aipay/project.json', action: existing ? 'update' : 'write' });
179
- // 6. install
179
+ // 6. install — or upgrade: the generated code passes `authOrigin`, which the SDK accepts
180
+ // from SDK_MIN_VERSION on, so an older SDK already in the project would break it.
181
+ const outdated = det.hasPackageJson && det.sdkInstalled ? outdatedSdk(det) : null;
182
+ const upgradeCmd = outdated
183
+ ? installCommand(det.packageManager ?? 'npm', `${SDK_PACKAGE}@latest`)
184
+ : null;
180
185
  let installCmd = null;
181
- if (det.hasPackageJson && !det.sdkInstalled && install && det.packageManager)
182
- installCmd = installCommand(det.packageManager, SDK_PACKAGE);
186
+ if (det.hasPackageJson && install && det.packageManager)
187
+ installCmd = det.sdkInstalled ? upgradeCmd : installCommand(det.packageManager, SDK_PACKAGE);
183
188
  out.blank();
184
189
  for (const p of plan)
185
190
  out.line(p.action === 'skip' ? 'info' : 'ok', `${p.action.padEnd(6)} ${p.path}${p.why ? ` (${p.why})` : ''}`);
191
+ if (outdated)
192
+ out.line(installCmd ? 'info' : 'warn', `${SDK_PACKAGE} ${outdated} is too old for the generated code (needs ${SDK_MIN_VERSION} or later)${installCmd ? '' : ` — upgrade it: ${upgradeCmd?.join(' ')}`}`);
186
193
  if (installCmd)
187
194
  out.line('info', `${dryRun ? 'would run' : 'run '} ${installCmd.join(' ')}`);
188
- else if (det.hasPackageJson && det.sdkInstalled)
195
+ else if (det.hasPackageJson && det.sdkInstalled && !outdated)
189
196
  out.line('info', `skip ${SDK_PACKAGE} (already a dependency)`);
190
197
  let installExit = null;
191
198
  if (!dryRun) {
@@ -201,10 +208,10 @@ export async function runInit(args, out, deps = {}) {
201
208
  if (installExit !== 0)
202
209
  out.line('bad', `install failed (exit ${installExit}); run it yourself: ${installCmd.join(' ')}`);
203
210
  else
204
- out.line('ok', `installed ${SDK_PACKAGE}`);
211
+ out.line('ok', `${outdated ? 'upgraded' : 'installed'} ${SDK_PACKAGE}`);
205
212
  }
206
213
  }
207
- // C2 setup session:登入狀態下回報 init(Console 的「等待工具連線」變綠);失敗不影響結果
214
+ // setup session:登入狀態下回報 init(Console 的「等待工具連線」變綠);失敗不影響結果
208
215
  let reported = false;
209
216
  if (!dryRun)
210
217
  reported = await reportSetupEvent(clientId, 'cli.init', {
@@ -227,6 +234,9 @@ export async function runInit(args, out, deps = {}) {
227
234
  detection: det,
228
235
  plan,
229
236
  install: installCmd ? { command: installCmd, exit: installExit } : null,
237
+ sdk_upgrade: outdated
238
+ ? { from: outdated, minimum: SDK_MIN_VERSION, command: upgradeCmd }
239
+ : null,
230
240
  docs: { fetched: !!docs.markdown, empty404: docs.empty404, status: docs.status },
231
241
  reported,
232
242
  });
@@ -6,14 +6,14 @@ export interface CliConfig {
6
6
  scopes: string[];
7
7
  redirect_path: string;
8
8
  audience: string;
9
- /** A2: the server allows the device grant for the CLI (older APIs omit it = false). */
9
+ /** The server allows the device grant for the CLI (older APIs omit it = false). */
10
10
  device_authorization: boolean;
11
11
  }
12
12
  export interface LoginDeps {
13
13
  env?: NodeJS.ProcessEnv;
14
14
  open?: (url: string) => boolean;
15
15
  onAuthorizeUrl?: (url: string) => void;
16
- /** A2: called with the verification URL and the user code (tests / drivers). */
16
+ /** `--device`: called with the verification URL and the user code (tests / drivers). */
17
17
  onDeviceCode?: (info: {
18
18
  url: string;
19
19
  userCode: string;
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * `aipay login [--api <base>] [--device] [--no-open] [--timeout <s>] [--json]`
3
3
  *
4
- * Developer sign-in for the CLI itself (consultant §10.2, auth model A): the CLI is a
5
- * first-party public client. It asks the API for its client config, then either
6
- * A1 — runs loopback PKCE against the authorization server (a browser on this machine), or
7
- * A2 — `--device`: RFC 8628 device authorization (no browser here: SSH boxes, containers,
8
- * CI) — prints a URL + short code, the developer approves on any device, the CLI polls.
4
+ * Developer sign-in for the CLI itself: the CLI is a first-party public client. It asks
5
+ * the API for its client config, then either
6
+ * - runs loopback PKCE against the authorization server (a browser on this machine), or
7
+ * - `--device`: RFC 8628 device authorization (no browser here: SSH boxes, containers,
8
+ * CI) — prints a URL + short code, the developer approves on any device, the CLI polls.
9
9
  * Both use audience aipay-developer-api and the management scope, and keep the tokens in
10
10
  * the OS keychain (or a 0600 file). No app secrets exist.
11
11
  */
@@ -118,7 +118,7 @@ export async function runLogin(args, out, deps = {}) {
118
118
  const code = u.searchParams.get('code');
119
119
  if (!code)
120
120
  throw new CliError('remote', 'callback without a code', EXIT.remote);
121
- const tokens = await exchangeCode({ hydraPublicUrl: cfg.auth_origin, clientId: cfg.client_id, redirectUri }, { code, codeVerifier, fetchImpl });
121
+ const tokens = await exchangeCode({ authOrigin: cfg.auth_origin, clientId: cfg.client_id, redirectUri }, { code, codeVerifier, fetchImpl });
122
122
  return finishLogin({
123
123
  accessToken: tokens.accessToken,
124
124
  refreshToken: tokens.refreshToken,
@@ -130,7 +130,7 @@ export async function runLogin(args, out, deps = {}) {
130
130
  await loop.close();
131
131
  }
132
132
  }
133
- /** A2 (RFC 8628): print URL + code, poll until approved on another device. */
133
+ /** `--device` (RFC 8628): print URL + code, poll until approved on another device. */
134
134
  async function deviceLogin(cfg, apiBase, timeoutS, fetchImpl, env, out, deps) {
135
135
  if (!cfg.device_authorization)
136
136
  throw new CliError('device_unsupported', `${apiBase} does not offer device sign-in yet — run \`aipay login\` on a machine with a browser`, EXIT.remote);
@@ -154,7 +154,7 @@ async function deviceLogin(cfg, apiBase, timeoutS, fetchImpl, env, out, deps) {
154
154
  }
155
155
  /** Shared tail of both flows: identity → keychain/file → summary. */
156
156
  async function finishLogin(tokens, cfg, apiBase, fetchImpl, env, out) {
157
- const user = await getUserInfo({ hydraPublicUrl: cfg.auth_origin }, tokens.accessToken, {
157
+ const user = await getUserInfo({ authOrigin: cfg.auth_origin }, tokens.accessToken, {
158
158
  fetchImpl,
159
159
  });
160
160
  const store = createAuthStore(env);
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * `aipay region` (show) and
3
3
  * `aipay region set --country <CC> [--state US-XX --line1 <street> [--line2 <apt>] --city <city> --zip <ZIP>] [--accept-terms]`.
4
- * US accounts (0121) must carry a full postal address: the backend locates the sales-tax jurisdiction
4
+ * US accounts must carry a full postal address: the backend locates the sales-tax jurisdiction
5
5
  * (state / county / city / special district) from it; `aipay region` shows the lookup result.
6
6
  *
7
7
  * The developer API is closed (403 region_required) until the account has a declared region and an
8
8
  * accepted current Terms of Service version. Both endpoints used here are exempt from that gate:
9
9
  * GET /v1/developer/me/region → current state + the version to accept
10
10
  * POST /v1/developer/me/region → declare (same body as the dashboard)
11
- * Non-interactive on purpose (agents drive this): everything is a flag; missing input is a usage error.
11
+ * Non-interactive on purpose (a coding agent may drive this): everything is a flag; missing input is a usage error.
12
12
  */
13
13
  import { type ParsedArgs } from '../args.js';
14
14
  import { type Reporter } from '../output.js';
@@ -38,7 +38,7 @@ export interface RegionState {
38
38
  declared: boolean;
39
39
  terms_current: boolean;
40
40
  complete: boolean;
41
- /** US only (0121): the full postal address on file */
41
+ /** US only: the full postal address on file */
42
42
  address?: RegionAddress | null;
43
43
  /** US only: background jurisdiction lookup (state / county / city / special district) */
44
44
  tax_location?: TaxLocationState | null;
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * `aipay region` (show) and
3
3
  * `aipay region set --country <CC> [--state US-XX --line1 <street> [--line2 <apt>] --city <city> --zip <ZIP>] [--accept-terms]`.
4
- * US accounts (0121) must carry a full postal address: the backend locates the sales-tax jurisdiction
4
+ * US accounts must carry a full postal address: the backend locates the sales-tax jurisdiction
5
5
  * (state / county / city / special district) from it; `aipay region` shows the lookup result.
6
6
  *
7
7
  * The developer API is closed (403 region_required) until the account has a declared region and an
8
8
  * accepted current Terms of Service version. Both endpoints used here are exempt from that gate:
9
9
  * GET /v1/developer/me/region → current state + the version to accept
10
10
  * POST /v1/developer/me/region → declare (same body as the dashboard)
11
- * Non-interactive on purpose (agents drive this): everything is a flag; missing input is a usage error.
11
+ * Non-interactive on purpose (a coding agent may drive this): everything is a flag; missing input is a usage error.
12
12
  */
13
13
  import { flagBool, flagString } from '../args.js';
14
14
  import { createAuthStore } from '../auth-store.js';
@@ -116,7 +116,7 @@ export async function runRegion(args, out, env = process.env) {
116
116
  }
117
117
  if (country === 'US' && !state)
118
118
  throw new CliError('usage', 'the United States needs --state (e.g. --state US-CA)', EXIT.usage);
119
- // 0121: the US needs a full address (street, city, ZIP) — the backend locates the sales-tax jurisdiction from it
119
+ // The US needs a full address (street, city, ZIP) — the backend locates the sales-tax jurisdiction from it
120
120
  if (country === 'US')
121
121
  address = parseAddressFlags(args.flags);
122
122
  else if (flagString(args.flags, 'line1') ||
@@ -42,7 +42,7 @@ export async function runTest(args, out, deps = {}) {
42
42
  throw e;
43
43
  });
44
44
  const redirectUri = redirectUriFor(target, loop.port);
45
- const cfg = { hydraPublicUrl: project.auth_origin, clientId: project.app.client_id, redirectUri };
45
+ const cfg = { authOrigin: project.auth_origin, clientId: project.app.client_id, redirectUri };
46
46
  const { url, state, codeVerifier } = await createConnectUrl(cfg);
47
47
  const note = redirectUri === callback
48
48
  ? ''
@@ -113,7 +113,7 @@ export async function runTest(args, out, deps = {}) {
113
113
  };
114
114
  result.ok = true;
115
115
  out.line('ok', `receipt: request_id ${r.request_id}, charged ${formatUusd(charged)} ${r.currency}`);
116
- // C2 setup session:登入狀態下回報 test 通過(伺服器本來就看得到回執;這條是給 Console 的時間戳)
116
+ // setup session:登入狀態下回報 test 通過(伺服器本來就看得到回執;這條是給 Console 的時間戳)
117
117
  await reportSetupEvent(project.app.client_id, 'cli.test', { request_id: r.request_id, model: res.model, ok: true }, out, env);
118
118
  out.line('ok', "done — the Console's verify step reads this from the server side");
119
119
  }
@@ -140,7 +140,7 @@ export async function runTest(args, out, deps = {}) {
140
140
  if (e instanceof CliError) {
141
141
  result.error = { code: e.code, message: e.message };
142
142
  out.line('bad', e.message);
143
- // 顧問 §13.2:失敗也回報(error_code 進 Console 的驗證步與 setup 事件;best effort、無 token)
143
+ // 失敗也回報(error_code 進 Console 的驗證步與 setup 事件;best effort、無 token)
144
144
  await reportSetupEvent(project.app.client_id, 'cli.test', { ok: false, error_code: e.code }, out, env);
145
145
  out.result(result);
146
146
  return e.exitCode;
package/dist/detect.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  import type { Framework } from './project.js';
2
2
  export type PackageManager = 'pnpm' | 'npm' | 'yarn' | 'bun';
3
+ /** Oldest @coswic/aipay-sdk the scaffolded code works with: it passes `authOrigin`, which
4
+ * the SDK accepts from 0.5.1 on. */
5
+ export declare const SDK_MIN_VERSION = "0.5.1";
3
6
  export interface Detection {
4
7
  packageManager: PackageManager | null;
5
8
  framework: Framework | null;
@@ -7,6 +10,10 @@ export interface Detection {
7
10
  typescript: boolean;
8
11
  /** Whether @coswic/aipay-sdk is already a dependency. */
9
12
  sdkInstalled: boolean;
13
+ /** Its version spec in package.json (null when it is not a dependency). */
14
+ sdkSpec: string | null;
15
+ /** Version in node_modules/@coswic/aipay-sdk/package.json (null when absent or unreadable). */
16
+ sdkVersion: string | null;
10
17
  /** `src/` exists → scaffold under it, else project root. */
11
18
  srcDir: string;
12
19
  }
@@ -23,5 +30,12 @@ export declare function detectPackageManager(pkg: PackageJsonLike | null, lockfi
23
30
  }): PackageManager | null;
24
31
  export declare function detectFramework(pkg: PackageJsonLike | null): Framework | null;
25
32
  export declare function detectProject(dir: string): Detection;
33
+ /**
34
+ * The SDK version this project would run when it is older than SDK_MIN_VERSION: the
35
+ * installed one when known, else the lowest version the package.json spec allows
36
+ * (`^0.5.0` → 0.5.0). Null when it is new enough or cannot be told (not a dependency, or a
37
+ * spec such as `latest`, `*`, a range or a `file:` / `workspace:` link).
38
+ */
39
+ export declare function outdatedSdk(det: Pick<Detection, 'sdkSpec' | 'sdkVersion'>): string | null;
26
40
  /** The install command for the detected manager (printed in --dry-run, run otherwise). */
27
41
  export declare function installCommand(pm: PackageManager, pkgName: string): string[];
package/dist/detect.js CHANGED
@@ -5,6 +5,9 @@
5
5
  */
6
6
  import { existsSync, readFileSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
+ /** Oldest @coswic/aipay-sdk the scaffolded code works with: it passes `authOrigin`, which
9
+ * the SDK accepts from 0.5.1 on. */
10
+ export const SDK_MIN_VERSION = '0.5.1';
8
11
  export function detectPackageManager(pkg, lockfiles) {
9
12
  const pm = pkg?.packageManager;
10
13
  if (typeof pm === 'string') {
@@ -59,15 +62,64 @@ export function detectProject(dir) {
59
62
  npm: existsSync(join(dir, 'package-lock.json')),
60
63
  };
61
64
  const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
65
+ const sdkSpec = deps['@coswic/aipay-sdk'];
62
66
  return {
63
67
  packageManager: detectPackageManager(pkg, lockfiles),
64
68
  framework: detectFramework(pkg),
65
69
  hasPackageJson,
66
70
  typescript: 'typescript' in deps || existsSync(join(dir, 'tsconfig.json')),
67
71
  sdkInstalled: '@coswic/aipay-sdk' in deps,
72
+ sdkSpec: typeof sdkSpec === 'string' ? sdkSpec : null,
73
+ sdkVersion: installedSdkVersion(dir),
68
74
  srcDir: existsSync(join(dir, 'src')) ? 'src' : '.',
69
75
  };
70
76
  }
77
+ function installedSdkVersion(dir) {
78
+ try {
79
+ const p = join(dir, 'node_modules', '@coswic', 'aipay-sdk', 'package.json');
80
+ const v = JSON.parse(readFileSync(p, 'utf8')).version;
81
+ return typeof v === 'string' ? v : null;
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ /**
88
+ * The SDK version this project would run when it is older than SDK_MIN_VERSION: the
89
+ * installed one when known, else the lowest version the package.json spec allows
90
+ * (`^0.5.0` → 0.5.0). Null when it is new enough or cannot be told (not a dependency, or a
91
+ * spec such as `latest`, `*`, a range or a `file:` / `workspace:` link).
92
+ */
93
+ export function outdatedSdk(det) {
94
+ const version = det.sdkVersion ?? lowestAllowed(det.sdkSpec);
95
+ return version !== null && isOlder(version, SDK_MIN_VERSION) ? version : null;
96
+ }
97
+ /** Exact, `^`, `~`, `=` or `>=` spec → the version it starts at; anything else → null. */
98
+ function lowestAllowed(spec) {
99
+ const m = /^\s*(?:[\^~=]|>=)?\s*v?(\d+)\.(\d+)(?:\.(\d+|x|\*))?(-[0-9A-Za-z.-]+)?\s*$/.exec(spec ?? '');
100
+ if (!m)
101
+ return null;
102
+ const patch = m[3] === undefined || m[3] === 'x' || m[3] === '*' ? '0' : m[3];
103
+ return `${m[1]}.${m[2]}.${patch}${m[4] ?? ''}`;
104
+ }
105
+ /** Semver precedence for `major.minor.patch[-prerelease]` (a prerelease sorts before its
106
+ * release); false when either side cannot be parsed. */
107
+ function isOlder(version, than) {
108
+ const parse = (v) => {
109
+ const m = /^v?(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.exec(v.trim());
110
+ return m ? { core: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] !== undefined } : null;
111
+ };
112
+ const a = parse(version);
113
+ const b = parse(than);
114
+ if (!a || !b)
115
+ return false;
116
+ for (let i = 0; i < 3; i++) {
117
+ const d = a.core[i] - b.core[i];
118
+ if (d !== 0)
119
+ return d < 0;
120
+ }
121
+ return a.pre && !b.pre;
122
+ }
71
123
  /** The install command for the detected manager (printed in --dry-run, run otherwise). */
72
124
  export function installCommand(pm, pkgName) {
73
125
  switch (pm) {
@@ -43,7 +43,7 @@ export async function openDeveloperApi(store) {
43
43
  catch {
44
44
  body = { raw: text.slice(0, 500) };
45
45
  }
46
- // 0074 hard gate: the developer API is closed until region + current Terms are on file.
46
+ // Hard gate: the developer API is closed until region + current Terms are on file.
47
47
  // `aipay region …` talks to the exempt endpoints, so this never traps the fix itself.
48
48
  if (res.status === 403 && errorCodeOf(body) === 'region_required')
49
49
  throw new CliError('region_required', 'AI Pay needs your country/region and your acceptance of the current Terms of Service before the developer API can be used — run `aipay region set --country <ISO 3166-1> [--state US-XX] --accept-terms` (`aipay region` shows the version and terms URL)', EXIT.problem);
@@ -56,7 +56,7 @@ async function refresh(store, auth, fetchImpl) {
56
56
  if (!auth.refresh_token)
57
57
  throw new CliError('expired', 'the developer token expired — run `aipay login` again', EXIT.problem);
58
58
  const t = await refreshAccessToken({
59
- hydraPublicUrl: auth.auth_origin,
59
+ authOrigin: auth.auth_origin,
60
60
  clientId: auth.client_id,
61
61
  }, auth.refresh_token, { fetchImpl }).catch(() => {
62
62
  throw new CliError('expired', 'could not renew the developer token — run `aipay login` again', EXIT.problem);
package/dist/device.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * RFC 8628 device authorization for `aipay login --device` (consultant §10.2, auth model A2):
2
+ * RFC 8628 device authorization for `aipay login --device`:
3
3
  * for machines without a browser (SSH boxes, containers, CI). The CLI asks the
4
4
  * authorization server for a device code + a short user code, prints the verification
5
5
  * URL and the code, then polls the token endpoint until the developer approves it in
@@ -11,9 +11,10 @@ export interface LoopbackTarget {
11
11
  path: string;
12
12
  }
13
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.
14
+ * The authorization server compares loopback redirect URIs per RFC 8252 §7.3: for
15
+ * 127.0.0.1 / [::1] the port is ignored, so the CLI may receive the callback on any free
16
+ * port while the app keeps its registered URI. `localhost` is NOT treated as loopback by
17
+ * the server — exact port.
17
18
  */
18
19
  export declare function portIsVariable(target: LoopbackTarget): boolean;
19
20
  /** The redirect_uri to send for a receiver bound to `port` (same host + path). */
package/dist/loopback.js CHANGED
@@ -7,9 +7,10 @@
7
7
  import { createServer } from 'node:http';
8
8
  import { hasUnsafeCallbackText } from './callback-url.js';
9
9
  /**
10
- * Hydra compares loopback redirect URIs per RFC 8252 §7.3: for 127.0.0.1 / [::1] the port
11
- * is ignored, so the CLI may receive the callback on any free port while the app keeps
12
- * its registered URI. `localhost` is NOT treated as loopback by the server — exact port.
10
+ * The authorization server compares loopback redirect URIs per RFC 8252 §7.3: for
11
+ * 127.0.0.1 / [::1] the port is ignored, so the CLI may receive the callback on any free
12
+ * port while the app keeps its registered URI. `localhost` is NOT treated as loopback by
13
+ * the server — exact port.
13
14
  */
14
15
  export function portIsVariable(target) {
15
16
  return target.host === '127.0.0.1' || target.host === '::1';
package/dist/output.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Output channel: human-readable lines on stderr-ish stdout, or one JSON document when
3
- * `--json` is set (agents parse it; humans read the other one). Both go to stdout so
4
- * `aipay status --json | jq` works; diagnostics that must never pollute JSON go to stderr.
3
+ * `--json` is set (for scripts and coding tools; humans read the other one). Both go to
4
+ * stdout so `aipay status --json | jq` works; diagnostics that must never pollute JSON go
5
+ * to stderr.
5
6
  */
6
7
  export type Level = 'ok' | 'warn' | 'bad' | 'info';
7
8
  export interface Reporter {
@@ -19,7 +20,7 @@ export declare function createReporter(opts: {
19
20
  stdout?: (s: string) => void;
20
21
  stderr?: (s: string) => void;
21
22
  }): Reporter;
22
- /** Exit codes are part of the contract (agents branch on them). */
23
+ /** Exit codes are part of the contract (scripts and coding tools branch on them). */
23
24
  export declare const EXIT: {
24
25
  readonly ok: 0;
25
26
  /** Something in the project/config is wrong and the user can fix it. */
package/dist/output.js CHANGED
@@ -30,7 +30,7 @@ export function createReporter(opts) {
30
30
  },
31
31
  };
32
32
  }
33
- /** Exit codes are part of the contract (agents branch on them). */
33
+ /** Exit codes are part of the contract (scripts and coding tools branch on them). */
34
34
  export const EXIT = {
35
35
  ok: 0,
36
36
  /** Something in the project/config is wrong and the user can fix it. */
package/dist/project.d.ts CHANGED
@@ -5,7 +5,7 @@ export interface ProjectConfig {
5
5
  client_id: string;
6
6
  name: string | null;
7
7
  };
8
- /** Authorization server origin (Hydra public), e.g. https://auth.ai-pay.example */
8
+ /** Authorization server origin, e.g. https://auth.ai-pay.example */
9
9
  auth_origin: string;
10
10
  /** API base for the SDK client, e.g. https://api.ai-pay.example */
11
11
  api_base: string;
package/dist/project.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Local artifacts (consultant report §6.5):
2
+ * Local artifacts:
3
3
  * .aipay/project.json — committable: which app this project is linked to (no secrets)
4
4
  * .env.local — NOT committed: only public values (client_id, URLs)
5
5
  * .gitignore — gets `.env.local` and `.aipay/local/` appended once
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Setup-session events (consultant §6.3): after `init` / `test` the CLI tells the Console
2
+ * Setup-session events: after `init` / `test` the CLI tells the Console
3
3
  * what happened so the "waiting for the tool" line turns into "the CLI finished init".
4
4
  * Best effort and strictly opt-in by state: only when the developer is signed in with the
5
5
  * CLI (`aipay login`); never blocks the command, never sends tokens or file contents.
@@ -1,13 +1,3 @@
1
- /**
2
- * Scaffold templates: one `aipay` helper module (sign-in flow + first completion) and a
3
- * thin wiring file per framework. Authored twice (TypeScript / JavaScript) so nothing is
4
- * generated by stripping types. The callback path is the one registered in the Console
5
- * for the app (default /auth/aipay/callback); the start path sits next to it.
6
- *
7
- * Sessions are the developer's business — the template keeps pending flows and signed-in
8
- * users in memory with a loud comment, which is correct for a dev server and obviously
9
- * wrong for production.
10
- */
11
1
  import type { Framework } from './project.js';
12
2
  export interface TemplateFile {
13
3
  /** Path relative to the project root. */
package/dist/templates.js CHANGED
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Scaffold templates: one `aipay` helper module (sign-in flow + first completion) and a
3
+ * thin wiring file per framework. Authored twice (TypeScript / JavaScript) so nothing is
4
+ * generated by stripping types. The callback path is the one registered in the Console
5
+ * for the app (default /auth/aipay/callback); the start path sits next to it.
6
+ *
7
+ * Sessions are the developer's business — the template keeps pending flows and signed-in
8
+ * users in memory with a loud comment, which is correct for a dev server and obviously
9
+ * wrong for production.
10
+ */
11
+ import { SDK_MIN_VERSION } from './detect.js';
1
12
  export function startPathFor(callbackPath) {
2
13
  return callbackPath.replace(/\/[^/]*$/, '/start');
3
14
  }
@@ -72,7 +83,7 @@ import {
72
83
  } from '@coswic/aipay-sdk'
73
84
 
74
85
  const cfg = {
75
- hydraPublicUrl: process.env.AIPAY_AUTH_ORIGIN${t(' as string', '')},
86
+ authOrigin: process.env.AIPAY_AUTH_ORIGIN${t(' as string', '')}, // needs @coswic/aipay-sdk ${SDK_MIN_VERSION}+
76
87
  clientId: process.env.AIPAY_CLIENT_ID${t(' as string', '')},
77
88
  redirectUri: process.env.AIPAY_CALLBACK_URL${t(' as string', '')},
78
89
  }
@@ -250,7 +261,7 @@ import {
250
261
  } from '@coswic/aipay-sdk'
251
262
 
252
263
  const cfg = {
253
- hydraPublicUrl: import.meta.env.VITE_AIPAY_AUTH_ORIGIN${t(' as string', '')},
264
+ authOrigin: import.meta.env.VITE_AIPAY_AUTH_ORIGIN${t(' as string', '')}, // needs @coswic/aipay-sdk ${SDK_MIN_VERSION}+
254
265
  clientId: import.meta.env.VITE_AIPAY_CLIENT_ID${t(' as string', '')},
255
266
  redirectUri: import.meta.env.VITE_AIPAY_CALLBACK_URL${t(' as string', '')},
256
267
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coswic/aipay",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "AI Pay CLI — developer sign-in, App and callback management, project integration, diagnostics, and real sign-in + AI receipt verification.",
5
5
  "author": "Coswic Labs (https://coswic.ai)",
6
6
  "license": "MIT",
@@ -27,7 +27,7 @@
27
27
  "node": ">=20"
28
28
  },
29
29
  "dependencies": {
30
- "@coswic/aipay-sdk": "^0.5.0"
30
+ "@coswic/aipay-sdk": "^0.5.1"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^26.1.1",