@coswic/aipay 0.7.2 → 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 +16 -1
- package/README.md +7 -7
- package/dist/agent-guide.d.ts +1 -1
- package/dist/agent-guide.js +5 -5
- package/dist/args.d.ts +1 -1
- package/dist/commands/app-callbacks.js +6 -6
- package/dist/commands/doctor.js +24 -14
- package/dist/commands/init.js +17 -7
- package/dist/commands/login.d.ts +2 -2
- package/dist/commands/login.js +10 -10
- package/dist/commands/region.d.ts +3 -3
- package/dist/commands/region.js +4 -4
- package/dist/commands/test.d.ts +13 -2
- package/dist/commands/test.js +21 -8
- package/dist/commands/unlink.js +1 -1
- package/dist/detect.d.ts +14 -0
- package/dist/detect.js +52 -0
- package/dist/developer-api.js +4 -4
- package/dist/device.js +2 -2
- package/dist/loopback.d.ts +4 -3
- package/dist/loopback.js +5 -4
- package/dist/main.d.ts +1 -1
- package/dist/main.js +2 -2
- package/dist/network.d.ts +1 -1
- package/dist/network.js +1 -1
- package/dist/output.d.ts +5 -4
- package/dist/output.js +2 -2
- package/dist/project.d.ts +1 -1
- package/dist/project.js +4 -4
- package/dist/remote.d.ts +1 -1
- package/dist/remote.js +1 -1
- package/dist/setup-events.js +1 -1
- package/dist/templates.d.ts +0 -10
- package/dist/templates.js +19 -8
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
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
|
+
|
|
10
|
+
## 0.8.0 — 2026-10-09
|
|
11
|
+
|
|
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.
|
|
13
|
+
- Developer commands: the 401 hint mentions that the developer account itself may not be invited yet, instead of only "run `aipay login` again".
|
|
14
|
+
- `aipay test` picks its default model among the ones the catalog marks available (`aipay.availability`, SDK 0.5.0 type); older servers without the field behave as before.
|
|
15
|
+
- Brand spelling follows the product: "AI Pay" (no hyphen) in the README, help text, messages, scaffolded templates and the generated `aipay-agent.md`; package.json names the provider, Coswic Labs (https://coswic.ai), as author. Command names, env keys and identifiers are unchanged.
|
|
16
|
+
- Requires `@coswic/aipay-sdk` ^0.5.0.
|
|
17
|
+
|
|
3
18
|
## 0.7.2 — 2026-10-09
|
|
4
19
|
|
|
5
20
|
- README: describe AI-Pay's invitation-only beta instead of self sign-up seats (only invited users can create an account; public registration is not open; your users must be invited before their first sign-in through your app; production does not accept top-ups at the moment), and explain what `aipay test` and the developer commands do when the user or the developer account is not invited. No behavior change.
|
|
@@ -23,7 +38,7 @@
|
|
|
23
38
|
- Add `apps callbacks list/add/remove` with the developer’s own management authorization and linked-project defaults.
|
|
24
39
|
- Preserve other registered callbacks, make no-op changes idempotent, and refuse to remove the final callback.
|
|
25
40
|
- Require server ETag support for mutations; concurrent edits and uncertain outcomes require readback instead of blind retries.
|
|
26
|
-
- Guide
|
|
41
|
+
- Guide the coding agent through developer CLI login and callback management; local `init` remains distinct from server registration.
|
|
27
42
|
- Allow HTTPS application callbacks in `init` and `doctor`; `test --callback` can use a separately registered loopback callback without changing project settings.
|
|
28
43
|
- Bound developer API requests to 30 seconds and allow conditional mutations to disable automatic retries after a 401.
|
|
29
44
|
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# @coswic/aipay — AI
|
|
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
|
|
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>
|
|
@@ -16,9 +16,9 @@ npx @coswic/aipay test
|
|
|
16
16
|
- **Models and prices**: https://ai-pay.coswic.ai/models?lang=en · **API contract**: https://ai-pay.coswic.ai/openapi.yaml
|
|
17
17
|
- **Product overview and what is open today**: https://ai-pay.coswic.ai/about?lang=en
|
|
18
18
|
|
|
19
|
-
AI
|
|
19
|
+
AI Pay's production environment is in a free beta limited to invited users: only invited users can create an account and use the service, public registration is not open, and your users must also be invited before their first sign-in through your app's "Sign in with AI Pay". Production does not accept top-ups at the moment. `<api base>` and `<authorization server>` are the endpoints of the environment you integrate with — the developer guide fetched from that environment has both filled in.
|
|
20
20
|
|
|
21
|
-
`aipay test` signs in as a real user: during the invitation-only beta that user must already hold an invitation (and have accepted the latest Terms), otherwise the sign-in never reaches the callback and `test` ends with the loopback timeout. The developer account you use with `aipay login` must be invited too; otherwise the developer commands
|
|
21
|
+
`aipay test` signs in as a real user: during the invitation-only beta that user must already hold an invitation (and have accepted the latest Terms), otherwise the sign-in never reaches the callback and `test` ends with the loopback timeout. The developer account you use with `aipay login` must be invited too; otherwise the developer commands answer 401 and say so (since 0.8.0 the message mentions the invitation requirement instead of only "run `aipay login` again").
|
|
22
22
|
|
|
23
23
|
## Commands
|
|
24
24
|
|
|
@@ -30,8 +30,8 @@ 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. |
|
|
34
|
-
| `aipay test` | The proof: a real sign-in through the AI
|
|
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
|
+
| `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. |
|
|
37
37
|
| `aipay unlink` | Forgets the link (removes `.aipay/project.json`, strips `AIPAY_*` from `.env.local`); keeps your code. |
|
|
@@ -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
|
|
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
|
```
|
package/dist/agent-guide.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `aipay-agent.md`: the file a coding agent reads first. Header = the facts of THIS
|
|
3
|
-
* project (app, endpoints, what was scaffolded, what "done" means); body = the AI
|
|
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
5
|
* network access to read them later.
|
|
6
6
|
*
|
package/dist/agent-guide.js
CHANGED
|
@@ -4,11 +4,11 @@ export function agentGuide(input) {
|
|
|
4
4
|
const callback = new URL(p.callback);
|
|
5
5
|
const verification = callback.protocol === 'https:' || callback.search
|
|
6
6
|
? 'Sign in through the running app and make one chat completion whose response carries a receipt (`aipay.request_id`). Alternatively, use `aipay test --callback <registered-http-loopback-url>` with an already registered HTTP loopback URL without a query; leave the project callback unchanged.'
|
|
7
|
-
: '`aipay test` completes: a real sign-in through the AI
|
|
7
|
+
: '`aipay test` completes: a real sign-in through the AI Pay authorization server and one chat completion whose response carries a receipt (`aipay.request_id`).';
|
|
8
8
|
const files = input.scaffolded.length
|
|
9
9
|
? input.scaffolded.map((f) => `- \`${f}\``).join('\n')
|
|
10
10
|
: '- (none — the project had them already)';
|
|
11
|
-
const head = `# AI
|
|
11
|
+
const head = `# AI Pay integration guide for this project
|
|
12
12
|
|
|
13
13
|
> Written by \`aipay init\` on ${p.linked_at}. Regenerate with \`aipay init --app ${p.app.client_id}\`. Safe to commit: it contains no secrets.
|
|
14
14
|
|
|
@@ -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
|
|
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.
|
|
@@ -47,7 +47,7 @@ ${files}
|
|
|
47
47
|
const body = input.docsMarkdown
|
|
48
48
|
? `---
|
|
49
49
|
|
|
50
|
-
# AI
|
|
50
|
+
# AI Pay developer documentation
|
|
51
51
|
|
|
52
52
|
_Fetched from ${input.docsUrl} by \`aipay init\`._
|
|
53
53
|
|
|
@@ -55,7 +55,7 @@ ${input.docsMarkdown.trim()}
|
|
|
55
55
|
`
|
|
56
56
|
: `---
|
|
57
57
|
|
|
58
|
-
# AI
|
|
58
|
+
# AI Pay developer documentation
|
|
59
59
|
|
|
60
60
|
The docs could not be fetched when this file was written (see the \`aipay init\` output).
|
|
61
61
|
Check the endpoint URL and network connectivity, then read
|
package/dist/args.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tiny argv parser — zero deps on purpose (the CLI
|
|
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.
|
|
@@ -53,7 +53,7 @@ function thrownError(error) {
|
|
|
53
53
|
? error.publicError
|
|
54
54
|
: error instanceof CliError
|
|
55
55
|
? { code: error.code, message: error.message }
|
|
56
|
-
: { code: 'network_error', message: 'Could not confirm the callback request with AI
|
|
56
|
+
: { code: 'network_error', message: 'Could not confirm the callback request with AI Pay.' };
|
|
57
57
|
}
|
|
58
58
|
/** Preserve exact URI strings: OAuth callback matching is not URL normalization. */
|
|
59
59
|
function validateCallback(value) {
|
|
@@ -134,7 +134,7 @@ function checkEnvironment(project, auth) {
|
|
|
134
134
|
if (!expected || expected !== normalized(auth[key]))
|
|
135
135
|
throw new CallbackError({
|
|
136
136
|
code: 'environment_mismatch',
|
|
137
|
-
message: 'The linked project belongs to a different AI
|
|
137
|
+
message: 'The linked project belongs to a different AI Pay environment. Sign in to the correct environment first; use --app explicitly only to manage an app in the current login environment.',
|
|
138
138
|
outcome: 'not_attempted',
|
|
139
139
|
}, EXIT.problem);
|
|
140
140
|
}
|
|
@@ -150,7 +150,7 @@ async function readCallbacks(api, path, appId) {
|
|
|
150
150
|
(body.client_id !== undefined && body.client_id !== appId))
|
|
151
151
|
throw new CallbackError({
|
|
152
152
|
code: 'invalid_response',
|
|
153
|
-
message: 'AI
|
|
153
|
+
message: 'AI Pay returned an invalid callback list.',
|
|
154
154
|
});
|
|
155
155
|
const system = typeof body.test_callback_uri === 'string' ? body.test_callback_uri : undefined;
|
|
156
156
|
return {
|
|
@@ -176,13 +176,13 @@ async function execute(api, input) {
|
|
|
176
176
|
!/^"redirect-uris-[0-9a-f]{64}"$/.test(before.redirect_uris_etag))
|
|
177
177
|
throw new CallbackError({
|
|
178
178
|
code: 'conditional_update_unavailable',
|
|
179
|
-
message: 'This AI
|
|
179
|
+
message: 'This AI Pay service does not provide a supported callback version. Update the service before changing callbacks; callbacks list remains available.',
|
|
180
180
|
outcome: 'not_attempted',
|
|
181
181
|
}, EXIT.problem);
|
|
182
182
|
if (input.callback === before.test_callback_uri)
|
|
183
183
|
throw new CallbackError({
|
|
184
184
|
code: 'system_callback',
|
|
185
|
-
message: 'This callback is managed by AI
|
|
185
|
+
message: 'This callback is managed by AI Pay and cannot be added or removed.',
|
|
186
186
|
outcome: 'not_attempted',
|
|
187
187
|
}, EXIT.problem);
|
|
188
188
|
const present = before.redirect_uris.includes(input.callback);
|
|
@@ -257,7 +257,7 @@ async function execute(api, input) {
|
|
|
257
257
|
};
|
|
258
258
|
}
|
|
259
259
|
export async function runCallbacks(args, out, env) {
|
|
260
|
-
// Reject malformed invocations before loading credentials or contacting AI
|
|
260
|
+
// Reject malformed invocations before loading credentials or contacting AI Pay.
|
|
261
261
|
const input = inputOf(args);
|
|
262
262
|
let environment;
|
|
263
263
|
try {
|
package/dist/commands/doctor.js
CHANGED
|
@@ -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
|
-
|
|
99
|
-
|
|
100
|
-
: det.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
: {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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}` });
|
package/dist/commands/init.js
CHANGED
|
@@ -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 &&
|
|
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',
|
|
211
|
+
out.line('ok', `${outdated ? 'upgraded' : 'installed'} ${SDK_PACKAGE}`);
|
|
205
212
|
}
|
|
206
213
|
}
|
|
207
|
-
//
|
|
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
|
});
|
package/dist/commands/login.d.ts
CHANGED
|
@@ -6,14 +6,14 @@ export interface CliConfig {
|
|
|
6
6
|
scopes: string[];
|
|
7
7
|
redirect_path: string;
|
|
8
8
|
audience: string;
|
|
9
|
-
/**
|
|
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
|
-
/**
|
|
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;
|
package/dist/commands/login.js
CHANGED
|
@@ -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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
*/
|
|
@@ -54,7 +54,7 @@ export async function fetchCliConfig(apiBase) {
|
|
|
54
54
|
export function resolveApiBase(args, env, dir) {
|
|
55
55
|
const v = flagString(args.flags, 'api') ?? env.AIPAY_API_BASE ?? readProjectSafe(dir)?.api_base ?? null;
|
|
56
56
|
if (!v)
|
|
57
|
-
throw new CliError('usage', 'which AI
|
|
57
|
+
throw new CliError('usage', 'which AI Pay? pass --api <api base> (the Console prompt lists it) or set AIPAY_API_BASE', EXIT.usage);
|
|
58
58
|
try {
|
|
59
59
|
return new URL(v).origin;
|
|
60
60
|
}
|
|
@@ -99,7 +99,7 @@ export async function runLogin(args, out, deps = {}) {
|
|
|
99
99
|
const loop = await startLoopback({ host: '127.0.0.1', port: 0, path: cfg.redirect_path }, { timeoutMs: timeoutS * 1000 });
|
|
100
100
|
const redirectUri = `http://127.0.0.1:${loop.port}${cfg.redirect_path}`;
|
|
101
101
|
const { url, state, codeVerifier } = managementAuthorizeUrl(cfg, redirectUri);
|
|
102
|
-
out.line('info', `signing in to ${apiBase} as a developer — approve "AI
|
|
102
|
+
out.line('info', `signing in to ${apiBase} as a developer — approve "AI Pay CLI" in your browser:\n\n ${url}\n`);
|
|
103
103
|
if (cfg.device_authorization)
|
|
104
104
|
out.line('info', 'no browser on this machine? run `aipay login --device` instead');
|
|
105
105
|
if (out.json)
|
|
@@ -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({
|
|
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
|
-
/**
|
|
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({
|
|
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
|
|
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 (
|
|
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
|
|
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;
|
package/dist/commands/region.js
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
-
//
|
|
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') ||
|
|
@@ -134,7 +134,7 @@ export async function runRegion(args, out, env = process.env) {
|
|
|
134
134
|
return EXIT.ok;
|
|
135
135
|
}
|
|
136
136
|
if (!current.region.terms_current && !acceptTerms)
|
|
137
|
-
throw new CliError('terms_required', `the AI
|
|
137
|
+
throw new CliError('terms_required', `the AI Pay Terms of Service (version ${current.terms_version_required}, ${termsUrl(api.auth.api_base, current)}) need your explicit acceptance — read them, then rerun with --accept-terms`, EXIT.problem);
|
|
138
138
|
const body = {
|
|
139
139
|
country,
|
|
140
140
|
...(state ? { subdivision: state } : {}),
|
package/dist/commands/test.d.ts
CHANGED
|
@@ -7,6 +7,17 @@ export interface TestDeps {
|
|
|
7
7
|
onAuthorizeUrl?: (url: string) => void;
|
|
8
8
|
}
|
|
9
9
|
export declare function runTest(args: ParsedArgs, out: Reporter, deps?: TestDeps): Promise<number>;
|
|
10
|
-
/**
|
|
11
|
-
export
|
|
10
|
+
/** What pickModel needs from a `GET /v1/models` entry (plain ids still accepted). */
|
|
11
|
+
export type PickableModel = string | {
|
|
12
|
+
id: string;
|
|
13
|
+
aipay?: {
|
|
14
|
+
availability?: {
|
|
15
|
+
status: string;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
/** Cheapest sensible default: skip models the catalog marks unavailable right now (v0.8.0;
|
|
20
|
+
* if every model is unavailable, fall back to all of them so the server can say why), then
|
|
21
|
+
* prefer a small/cheap model id if present, else the first. */
|
|
22
|
+
export declare function pickModel(models: PickableModel[]): string | null;
|
|
12
23
|
export declare function formatUusd(uusd: string): string;
|
package/dist/commands/test.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `aipay test [--model <id>] [--no-open] [--timeout <s>] [--json]`
|
|
3
3
|
*
|
|
4
|
-
* The one check only a CLI can do end to end: a real sign-in through the AI
|
|
4
|
+
* The one check only a CLI can do end to end: a real sign-in through the AI Pay
|
|
5
5
|
* authorization server on the registered loopback callback, then one chat completion
|
|
6
6
|
* on the user's behalf. The receipt in the response is the proof (and the Console's
|
|
7
7
|
* verify step turns green from the server-side facts). Tokens live in memory only.
|
|
8
8
|
*/
|
|
9
|
-
import { AIPayClient, AuthorizationError, ConnectError, createConnectUrl, getUserInfo, handleCallback, InsufficientBalanceError, } from '@coswic/aipay-sdk';
|
|
9
|
+
import { AIPayApiError, 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
12
|
import { parseCallback, portIsVariable, redirectUriFor, startLoopback } from '../loopback.js';
|
|
@@ -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 = {
|
|
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
|
? ''
|
|
@@ -89,7 +89,7 @@ export async function runTest(args, out, deps = {}) {
|
|
|
89
89
|
const models = await client.models.list().catch((e) => {
|
|
90
90
|
throw new CliError('models', `cannot list models: ${e.message}`, EXIT.remote);
|
|
91
91
|
});
|
|
92
|
-
model = pickModel(models
|
|
92
|
+
model = pickModel(models);
|
|
93
93
|
if (!model)
|
|
94
94
|
throw new CliError('models', 'no sellable model right now — pass --model <id>', EXIT.remote);
|
|
95
95
|
}
|
|
@@ -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
|
-
//
|
|
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
|
}
|
|
@@ -127,6 +127,10 @@ export async function runTest(args, out, deps = {}) {
|
|
|
127
127
|
}
|
|
128
128
|
if (e instanceof AuthorizationError)
|
|
129
129
|
throw new CliError('unauthorized', `the API rejected the token: ${e.message}`, EXIT.remote);
|
|
130
|
+
// v0.8.0: during the invitation-only beta a 401 is also what an uninvited (or
|
|
131
|
+
// invitation-revoked / terms-not-accepted) user gets — refreshing does not help.
|
|
132
|
+
if (e instanceof AIPayApiError && e.status === 401)
|
|
133
|
+
throw new CliError('unauthorized', `the API rejected the user's token (401 ${e.code}). During the invitation-only beta this also happens when the signed-in user holds no current invitation or has not accepted the latest Terms — refreshing does not help; see ${project.api_base}/about`, EXIT.remote);
|
|
130
134
|
throw e;
|
|
131
135
|
}
|
|
132
136
|
out.result(result);
|
|
@@ -136,7 +140,7 @@ export async function runTest(args, out, deps = {}) {
|
|
|
136
140
|
if (e instanceof CliError) {
|
|
137
141
|
result.error = { code: e.code, message: e.message };
|
|
138
142
|
out.line('bad', e.message);
|
|
139
|
-
//
|
|
143
|
+
// 失敗也回報(error_code 進 Console 的驗證步與 setup 事件;best effort、無 token)
|
|
140
144
|
await reportSetupEvent(project.app.client_id, 'cli.test', { ok: false, error_code: e.code }, out, env);
|
|
141
145
|
out.result(result);
|
|
142
146
|
return e.exitCode;
|
|
@@ -146,6 +150,8 @@ export async function runTest(args, out, deps = {}) {
|
|
|
146
150
|
out.line('bad', msg);
|
|
147
151
|
if (/\b404\b/.test(msg))
|
|
148
152
|
out.line('info', 'Check the endpoint URL and network connectivity before retrying.');
|
|
153
|
+
if (/no callback received/.test(msg))
|
|
154
|
+
out.line('info', `if the AI Pay sign-in page never completed, the user may not be invited: during the invitation-only beta only invited users can sign in (see ${project.api_base}/about)`);
|
|
149
155
|
out.result(result);
|
|
150
156
|
return EXIT.remote;
|
|
151
157
|
}
|
|
@@ -153,8 +159,15 @@ export async function runTest(args, out, deps = {}) {
|
|
|
153
159
|
await loop.close();
|
|
154
160
|
}
|
|
155
161
|
}
|
|
156
|
-
/** Cheapest sensible default:
|
|
157
|
-
|
|
162
|
+
/** Cheapest sensible default: skip models the catalog marks unavailable right now (v0.8.0;
|
|
163
|
+
* if every model is unavailable, fall back to all of them so the server can say why), then
|
|
164
|
+
* prefer a small/cheap model id if present, else the first. */
|
|
165
|
+
export function pickModel(models) {
|
|
166
|
+
const entries = models.map((m) => typeof m === 'string'
|
|
167
|
+
? { id: m, available: true }
|
|
168
|
+
: { id: m.id, available: m.aipay?.availability?.status !== 'unavailable' });
|
|
169
|
+
const pool = entries.filter((e) => e.available);
|
|
170
|
+
const ids = (pool.length ? pool : entries).map((e) => e.id);
|
|
158
171
|
if (!ids.length)
|
|
159
172
|
return null;
|
|
160
173
|
const cheap = ids.find((id) => /mini|flash|haiku|nano|small|8b|7b/i.test(id));
|
package/dist/commands/unlink.js
CHANGED
|
@@ -61,7 +61,7 @@ export function stripEnvKeys(content, keys) {
|
|
|
61
61
|
const set = new Set(keys);
|
|
62
62
|
const lines = content.split(/\r?\n/);
|
|
63
63
|
const kept = lines.filter((line) => {
|
|
64
|
-
if (line.startsWith('# AI
|
|
64
|
+
if (line.startsWith('# AI Pay (written by `aipay init`'))
|
|
65
65
|
return false;
|
|
66
66
|
const m = /^\s*(?:export\s+)?([A-Z0-9_]+)\s*=/.exec(line);
|
|
67
67
|
return !(m && set.has(m[1]));
|
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) {
|
package/dist/developer-api.js
CHANGED
|
@@ -35,7 +35,7 @@ export async function openDeveloperApi(store) {
|
|
|
35
35
|
if (isEmpty404(res.status, text))
|
|
36
36
|
throw new CliError('empty_404', EMPTY_404_HINT, EXIT.remote);
|
|
37
37
|
if (res.status === 401 && opts.retryUnauthorized !== false)
|
|
38
|
-
throw new CliError('unauthorized', 'the API rejected the developer token — run `aipay login` again', EXIT.remote);
|
|
38
|
+
throw new CliError('unauthorized', 'the API rejected the developer token — run `aipay login` again (if you just did, the developer account may not be invited yet: during the invitation-only beta only invited users can use the developer API — see the product overview at /about)', EXIT.remote);
|
|
39
39
|
let body = null;
|
|
40
40
|
try {
|
|
41
41
|
body = text ? JSON.parse(text) : null;
|
|
@@ -43,10 +43,10 @@ export async function openDeveloperApi(store) {
|
|
|
43
43
|
catch {
|
|
44
44
|
body = { raw: text.slice(0, 500) };
|
|
45
45
|
}
|
|
46
|
-
//
|
|
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
|
-
throw new CliError('region_required', 'AI
|
|
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);
|
|
50
50
|
return { status: res.status, body: body };
|
|
51
51
|
},
|
|
52
52
|
};
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
@@ -29,7 +29,7 @@ export async function requestDeviceCode(cfg, fetchImpl = fetch) {
|
|
|
29
29
|
if (!res.ok) {
|
|
30
30
|
const err = parseOAuthError(text);
|
|
31
31
|
if (err?.error === 'invalid_grant' || err?.error === 'unauthorized_client')
|
|
32
|
-
throw new CliError('device_unsupported', 'this AI
|
|
32
|
+
throw new CliError('device_unsupported', 'this AI Pay does not allow device sign-in for the CLI yet — use `aipay login` on a machine with a browser', EXIT.remote);
|
|
33
33
|
throw new CliError('remote', `device authorization failed: HTTP ${res.status} ${err?.error ?? ''} ${err?.error_description ?? ''}`.trim(), EXIT.remote);
|
|
34
34
|
}
|
|
35
35
|
const j = JSON.parse(text);
|
package/dist/loopback.d.ts
CHANGED
|
@@ -11,9 +11,10 @@ export interface LoopbackTarget {
|
|
|
11
11
|
path: string;
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
* is ignored, so the CLI may receive the callback on any free
|
|
16
|
-
* its registered URI. `localhost` is NOT treated as loopback by
|
|
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
|
-
*
|
|
11
|
-
* is ignored, so the CLI may receive the callback on any free
|
|
12
|
-
* its registered URI. `localhost` is NOT treated as loopback by
|
|
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';
|
|
@@ -86,7 +87,7 @@ export function startLoopback(target, opts = {}) {
|
|
|
86
87
|
});
|
|
87
88
|
}
|
|
88
89
|
function defaultPage(ok) {
|
|
89
|
-
const title = ok ? 'AI
|
|
90
|
+
const title = ok ? 'AI Pay: signed in' : 'AI Pay: authorization failed';
|
|
90
91
|
const body = ok
|
|
91
92
|
? 'You can close this tab and return to the terminal.'
|
|
92
93
|
: 'The terminal shows the error. You can close this tab.';
|
package/dist/main.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const HELP = "aipay \u2014 AI
|
|
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 region [--json] your country/region + Terms of Service status\n aipay region set --country <CC> [--state US-XX --line1 <street> [--line2 <apt>] --city <city> --zip <ZIP>]\n [--accept-terms] [--json] US accounts need the full address (sales-tax jurisdiction)\n aipay apps list [--json]\n aipay apps create --name <name> [--callback <url>] [--json]\n aipay apps callbacks list [--app <client_id>] [--dir <path>] [--json]\n aipay apps callbacks add|remove --callback <url> [--app <client_id>] [--dir <path>] [--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>] [--callback <registered-loopback-url>] [--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\nregion the developer API stays closed (403 region_required) until your account has a country/region\n (US: a state) and has accepted the current Terms of Service \u2014 \"region set\" does both; the\n version to accept and the terms URL are printed by \"aipay region\"\napps list/create your apps; callbacks list/add/remove manages registered redirects\n callback changes preserve other URLs and reject concurrent edits\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
|
@@ -10,7 +10,7 @@ import { runTest } from './commands/test.js';
|
|
|
10
10
|
import { runUnlink } from './commands/unlink.js';
|
|
11
11
|
import { CliError, createReporter, EXIT } from './output.js';
|
|
12
12
|
import { cliVersion } from './version.js';
|
|
13
|
-
export const HELP = `aipay — AI
|
|
13
|
+
export const HELP = `aipay — AI Pay CLI
|
|
14
14
|
|
|
15
15
|
Usage
|
|
16
16
|
aipay login [--api <base>] [--device] [--json] developer sign-in for the CLI itself
|
|
@@ -42,7 +42,7 @@ init link this project to an app: .aipay/project.json, .env.local, .gitignore
|
|
|
42
42
|
a callback route for your framework, aipay-agent.md, and the SDK install
|
|
43
43
|
(--auth/--api are optional once you are signed in; --create makes the app first)
|
|
44
44
|
test real sign-in via a loopback callback (any free port on 127.0.0.1) + one chat completion → receipt
|
|
45
|
-
doctor check the link, files, SDK, and that AI
|
|
45
|
+
doctor check the link, files, SDK, and that AI Pay is reachable
|
|
46
46
|
status what this directory is linked to
|
|
47
47
|
unlink forget the link (keeps your code)
|
|
48
48
|
|
package/dist/network.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
/** Describe the observed response without inferring its network or server-side cause. */
|
|
2
2
|
export declare function isEmpty404(status: number, bodyText: string): boolean;
|
|
3
|
-
export declare const EMPTY_404_HINT = "AI
|
|
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
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
export function isEmpty404(status, bodyText) {
|
|
3
3
|
return status === 404 && bodyText.trim() === '';
|
|
4
4
|
}
|
|
5
|
-
export const EMPTY_404_HINT = 'AI
|
|
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/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 (
|
|
4
|
-
* `aipay status --json | jq` works; diagnostics that must never pollute JSON go
|
|
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,14 +20,14 @@ 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 (
|
|
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. */
|
|
26
27
|
readonly problem: 1;
|
|
27
28
|
/** Bad invocation (unknown command/flag, missing required flag). */
|
|
28
29
|
readonly usage: 2;
|
|
29
|
-
/** Network/auth failure talking to AI
|
|
30
|
+
/** Network/auth failure talking to AI Pay. */
|
|
30
31
|
readonly remote: 3;
|
|
31
32
|
};
|
|
32
33
|
export declare class CliError extends Error {
|
package/dist/output.js
CHANGED
|
@@ -30,14 +30,14 @@ export function createReporter(opts) {
|
|
|
30
30
|
},
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
-
/** Exit codes are part of the contract (
|
|
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. */
|
|
37
37
|
problem: 1,
|
|
38
38
|
/** Bad invocation (unknown command/flag, missing required flag). */
|
|
39
39
|
usage: 2,
|
|
40
|
-
/** Network/auth failure talking to AI
|
|
40
|
+
/** Network/auth failure talking to AI Pay. */
|
|
41
41
|
remote: 3,
|
|
42
42
|
};
|
|
43
43
|
export class CliError extends Error {
|
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
|
|
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
|
|
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
|
|
@@ -33,7 +33,7 @@ export function readProject(dir) {
|
|
|
33
33
|
const raw = readFileSync(p, 'utf8');
|
|
34
34
|
const parsed = JSON.parse(raw);
|
|
35
35
|
if (!isProjectConfig(parsed))
|
|
36
|
-
throw new Error(`${p} is not a valid AI
|
|
36
|
+
throw new Error(`${p} is not a valid AI Pay project file`);
|
|
37
37
|
return parsed;
|
|
38
38
|
}
|
|
39
39
|
export function writeProject(dir, cfg) {
|
|
@@ -98,7 +98,7 @@ export function mergeEnvFile(existing, values) {
|
|
|
98
98
|
if (missing.length) {
|
|
99
99
|
if (out.length)
|
|
100
100
|
out.push('');
|
|
101
|
-
out.push('# AI
|
|
101
|
+
out.push('# AI Pay (written by `aipay init`; public values, safe to keep local)');
|
|
102
102
|
for (const k of missing) {
|
|
103
103
|
out.push(`${k}=${values[k]}`);
|
|
104
104
|
changed.push(k);
|
|
@@ -121,6 +121,6 @@ export function mergeGitignore(existing, entries) {
|
|
|
121
121
|
if (!added.length)
|
|
122
122
|
return { content: existing ?? '', added };
|
|
123
123
|
const base = existing ? (existing.endsWith('\n') ? existing : `${existing}\n`) : '';
|
|
124
|
-
const block = `${base}${base ? '\n' : ''}# AI
|
|
124
|
+
const block = `${base}${base ? '\n' : ''}# AI Pay local files (never commit)\n${added.join('\n')}\n`;
|
|
125
125
|
return { content: block, added };
|
|
126
126
|
}
|
package/dist/remote.d.ts
CHANGED
package/dist/remote.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Everything that talks to AI
|
|
2
|
+
* Everything that talks to AI Pay over the network, in one place so the commands stay
|
|
3
3
|
* readable and tests can stub it.
|
|
4
4
|
*/
|
|
5
5
|
import { EMPTY_404_HINT, isEmpty404 } from './network.js';
|
package/dist/setup-events.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Setup-session events
|
|
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.
|
package/dist/templates.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -82,7 +93,7 @@ const apiBase = process.env.AIPAY_API_BASE${t(' as string', '')}
|
|
|
82
93
|
const pendingFlows = new Map${t('<string, { codeVerifier: string; expires: number }>', '')}()
|
|
83
94
|
const sessions = new Map${t('<string, TokenSet>', '')}()
|
|
84
95
|
|
|
85
|
-
/** Step 1 — build the AI
|
|
96
|
+
/** Step 1 — build the AI Pay sign-in URL; remember the PKCE verifier under the state. */
|
|
86
97
|
export async function beginSignIn()${t(': Promise<string>', '')} {
|
|
87
98
|
const { url, state, codeVerifier } = await createConnectUrl(cfg)
|
|
88
99
|
pendingFlows.set(state, { codeVerifier, expires: Date.now() + 10 * 60_000 })
|
|
@@ -239,7 +250,7 @@ export async function handleAIPay(req${ts ? ': IncomingMessage' : ''}, res${ts ?
|
|
|
239
250
|
function browserModule(input) {
|
|
240
251
|
const ts = input.typescript;
|
|
241
252
|
const t = (typed, plain) => (ts ? typed : plain);
|
|
242
|
-
return `${ts ? '/// <reference types="vite/client" />\n' : ''}// Browser (Vite) build of the AI
|
|
253
|
+
return `${ts ? '/// <reference types="vite/client" />\n' : ''}// Browser (Vite) build of the AI Pay helper. Reads the VITE_AIPAY_* values \`aipay init\`
|
|
243
254
|
// wrote to .env.local (Vite exposes only VITE_-prefixed values to the page). No secret
|
|
244
255
|
// exists: the app is a public PKCE client and the token endpoint accepts browser calls.
|
|
245
256
|
import {
|
|
@@ -250,7 +261,7 @@ import {
|
|
|
250
261
|
} from '@coswic/aipay-sdk'
|
|
251
262
|
|
|
252
263
|
const cfg = {
|
|
253
|
-
|
|
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
|
}
|
|
@@ -258,10 +269,10 @@ const apiBase = import.meta.env.VITE_AIPAY_API_BASE${t(' as string', '')}
|
|
|
258
269
|
/** Path part of the registered callback — the page that must call completeSignInIfCallback(). */
|
|
259
270
|
export const CALLBACK_PATH = ${JSON.stringify(input.callbackPath)}
|
|
260
271
|
|
|
261
|
-
const PENDING_KEY = 'aipay:pending' // { state, codeVerifier } while the user is at AI
|
|
272
|
+
const PENDING_KEY = 'aipay:pending' // { state, codeVerifier } while the user is at AI Pay
|
|
262
273
|
const TOKENS_KEY = 'aipay:tokens' // TokenSet for this tab (sessionStorage: cleared when the tab closes)
|
|
263
274
|
|
|
264
|
-
/** Step 1 — send the user to AI
|
|
275
|
+
/** Step 1 — send the user to AI Pay. The PKCE verifier is kept under the state for the callback. */
|
|
265
276
|
export async function beginSignIn()${t(': Promise<void>', '')} {
|
|
266
277
|
const { url, state, codeVerifier } = await createConnectUrl(cfg)
|
|
267
278
|
sessionStorage.setItem(PENDING_KEY, JSON.stringify({ state, codeVerifier }))
|
|
@@ -313,7 +324,7 @@ function viteBoot(input) {
|
|
|
313
324
|
const ts = input.typescript;
|
|
314
325
|
const from = ts ? './aipay' : './aipay.js';
|
|
315
326
|
return `// Call bootAIPay() once when your app starts (top of main.${ts ? 'ts' : 'js'} / before rendering).
|
|
316
|
-
// It completes the sign-in when the page is the AI
|
|
327
|
+
// It completes the sign-in when the page is the AI Pay callback (${input.callbackPath}) and
|
|
317
328
|
// otherwise does nothing. Vite's dev server serves index.html for that path (SPA fallback),
|
|
318
329
|
// so no route configuration is needed; with a client-side router, keep the path unrouted.
|
|
319
330
|
import { beginSignIn, completeSignInIfCallback, currentTokens, firstCompletion } from '${from}'
|
|
@@ -323,7 +334,7 @@ export async function bootAIPay() {
|
|
|
323
334
|
const signedIn = justSignedIn !== null || currentTokens() !== null
|
|
324
335
|
return {
|
|
325
336
|
signedIn,
|
|
326
|
-
/** Wire this to your "Sign in with AI
|
|
337
|
+
/** Wire this to your "Sign in with AI Pay" button. */
|
|
327
338
|
beginSignIn,
|
|
328
339
|
/** After sign-in: one model call; the receipt (cost, request id) is on the result. */
|
|
329
340
|
firstCompletion,
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coswic/aipay",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "AI
|
|
3
|
+
"version": "0.8.1",
|
|
4
|
+
"description": "AI Pay CLI — developer sign-in, App and callback management, project integration, diagnostics, and real sign-in + AI receipt verification.",
|
|
5
|
+
"author": "Coswic Labs (https://coswic.ai)",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"homepage": "https://ai-pay.coswic.ai/docs?lang=en",
|
|
7
8
|
"type": "module",
|
|
@@ -26,7 +27,7 @@
|
|
|
26
27
|
"node": ">=20"
|
|
27
28
|
},
|
|
28
29
|
"dependencies": {
|
|
29
|
-
"@coswic/aipay-sdk": "^0.
|
|
30
|
+
"@coswic/aipay-sdk": "^0.5.1"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@types/node": "^26.1.1",
|