@aiwg/cli 2026.8.0 → 2026.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.
Files changed (61) hide show
  1. package/README.md +33 -0
  2. package/agentic/code/providers/capability-matrix.yaml +511 -0
  3. package/agentic/code/providers/model-capabilities.v1.json +120 -0
  4. package/agentic/code/providers/model-catalog.v1.json +96 -0
  5. package/agentic/code/providers/model-policy-evaluations.v1.json +50 -0
  6. package/agentic/code/providers/premium-model-allowlist.v1.json +36 -0
  7. package/bin/aiwg.mjs +14 -10
  8. package/dist/src/api/index.d.ts +1 -0
  9. package/dist/src/api/index.js +1 -0
  10. package/dist/src/artifacts/cli.js +2 -0
  11. package/dist/src/artifacts/types.js +4 -0
  12. package/dist/src/auth/client.js +209 -0
  13. package/dist/src/auth/config.js +38 -0
  14. package/dist/src/auth/credential-store.js +141 -0
  15. package/dist/src/auth/resource-credentials.js +25 -0
  16. package/dist/src/auth/types.js +2 -0
  17. package/dist/src/channel/manager.mjs +5 -5
  18. package/dist/src/cli/handlers/auth.js +125 -0
  19. package/dist/src/cli/handlers/help.js +1 -0
  20. package/dist/src/cli/handlers/index.js +3 -1
  21. package/dist/src/cli/handlers/resource-versions.js +2 -0
  22. package/dist/src/cli/handlers/sessions.js +23 -5
  23. package/dist/src/cli/handlers/subcommands.js +10 -1
  24. package/dist/src/cli/handlers/use.js +342 -43
  25. package/dist/src/config/gitignore.js +1 -0
  26. package/dist/src/extensions/commands/definitions.js +19 -0
  27. package/dist/src/memory/canonical-context.js +342 -0
  28. package/dist/src/memory/context-pack.js +282 -0
  29. package/dist/src/memory/index.js +4 -0
  30. package/dist/src/memory/intake.js +118 -0
  31. package/dist/src/resources/resolver.js +1 -0
  32. package/dist/src/resources/web-release.d.ts +3 -1
  33. package/dist/src/resources/web-release.js +14 -6
  34. package/dist/src/serve/agentic-sandbox-fleet-client.js +213 -0
  35. package/dist/src/serve/fleet-mission-conductor.js +293 -0
  36. package/dist/src/sessions/index.js +1 -0
  37. package/dist/src/sessions/output-registration.js +338 -0
  38. package/dist/src/sessions/promotion.js +73 -2
  39. package/dist/src/sessions/repository.js +2 -1
  40. package/dist/src/update/notifier.mjs +13 -2
  41. package/package.json +8 -1
  42. package/tools/_resolve-impl.mjs +74 -0
  43. package/tools/agents/deploy-agents.mjs +962 -0
  44. package/tools/agents/providers/base.mjs +2954 -0
  45. package/tools/agents/providers/claude.mjs +711 -0
  46. package/tools/agents/providers/codex.mjs +699 -0
  47. package/tools/agents/providers/copilot.mjs +659 -0
  48. package/tools/agents/providers/cursor.mjs +714 -0
  49. package/tools/agents/providers/factory.mjs +1130 -0
  50. package/tools/agents/providers/hermes.mjs +663 -0
  51. package/tools/agents/providers/hook-capabilities.mjs +85 -0
  52. package/tools/agents/providers/model-role.mjs +56 -0
  53. package/tools/agents/providers/openclaw-translator.mjs +348 -0
  54. package/tools/agents/providers/openclaw.mjs +680 -0
  55. package/tools/agents/providers/opencode.mjs +675 -0
  56. package/tools/agents/providers/openhuman.mjs +292 -0
  57. package/tools/agents/providers/warp.mjs +413 -0
  58. package/tools/agents/providers/windsurf.mjs +748 -0
  59. package/tools/commands/deploy-prompts-codex.mjs +336 -0
  60. package/tools/plugin/package-plugins.mjs +1013 -0
  61. package/tools/skills/deploy-skills-codex.mjs +571 -0
@@ -0,0 +1,96 @@
1
+ {
2
+ "$schema": "../../../schemas/models/model-catalog.v1.schema.json",
3
+ "version": "1.0.0",
4
+ "refreshedAt": "2026-07-20",
5
+ "staleAfterDays": 90,
6
+ "providers": {
7
+ "claude": {
8
+ "roles": {
9
+ "reasoning": { "id": "claude-opus-4-7", "status": "active", "observed": false },
10
+ "coding": { "id": "claude-sonnet-4-6", "status": "active", "observed": false },
11
+ "efficiency": { "id": "claude-haiku-4-5", "status": "active", "observed": false }
12
+ },
13
+ "sourceUrl": "https://docs.anthropic.com/en/docs/about-claude/models/overview", "verifiedAt": "2026-07-20"
14
+ },
15
+ "codex": {
16
+ "roles": {
17
+ "reasoning": { "id": "gpt-5.4", "status": "active", "observed": true },
18
+ "coding": { "id": "gpt-5.5", "status": "active", "observed": true },
19
+ "efficiency": { "id": "gpt-5.4-mini", "status": "active", "observed": true }
20
+ },
21
+ "sourceUrl": "https://learn.chatgpt.com/docs/agent-configuration/subagents", "verifiedAt": "2026-07-20"
22
+ },
23
+ "copilot": {
24
+ "roles": {
25
+ "reasoning": { "id": "claude-opus-4.6", "status": "unverified", "observed": false },
26
+ "coding": { "id": "claude-sonnet-4.6", "status": "unverified", "observed": false },
27
+ "efficiency": { "id": "gpt-5.1-codex-mini", "status": "unverified", "observed": false }
28
+ },
29
+ "sourceUrl": "https://docs.github.com/en/copilot/reference/ai-models/supported-models", "verifiedAt": "2026-07-20"
30
+ },
31
+ "cursor": {
32
+ "roles": {
33
+ "reasoning": { "id": "claude-4.6-opus-high-thinking", "status": "unverified", "observed": false },
34
+ "coding": { "id": "claude-4.6-sonnet", "status": "unverified", "observed": false },
35
+ "efficiency": { "id": "auto", "status": "unverified", "observed": false }
36
+ },
37
+ "sourceUrl": "https://cursor.com/docs/models", "verifiedAt": "2026-07-20"
38
+ },
39
+ "factory": {
40
+ "roles": {
41
+ "reasoning": { "id": "heavy", "status": "active", "observed": false },
42
+ "coding": { "id": "medium", "status": "active", "observed": false },
43
+ "efficiency": { "id": "light", "status": "active", "observed": false }
44
+ },
45
+ "sourceUrl": "https://docs.factory.ai/cli/user-guides/choosing-your-model", "verifiedAt": "2026-07-20"
46
+ },
47
+ "opencode": {
48
+ "roles": {
49
+ "reasoning": { "id": "anthropic/claude-opus-4-7", "status": "unverified", "observed": false },
50
+ "coding": { "id": "anthropic/claude-sonnet-4-6", "status": "unverified", "observed": false },
51
+ "efficiency": { "id": "anthropic/claude-haiku-4-5", "status": "unverified", "observed": false }
52
+ },
53
+ "sourceUrl": "https://opencode.ai/docs/models", "verifiedAt": "2026-07-20"
54
+ },
55
+ "warp": {
56
+ "roles": {
57
+ "reasoning": { "id": "profile-selected", "status": "unverified", "observed": false },
58
+ "coding": { "id": "profile-selected", "status": "unverified", "observed": false },
59
+ "efficiency": { "id": "profile-selected", "status": "unverified", "observed": false }
60
+ },
61
+ "sourceUrl": "https://docs.warp.dev/agent-platform/capabilities/agent-profiles-permissions", "verifiedAt": "2026-07-20"
62
+ },
63
+ "windsurf": {
64
+ "roles": {
65
+ "reasoning": { "id": "inherited", "status": "unverified", "observed": false },
66
+ "coding": { "id": "inherited", "status": "unverified", "observed": false },
67
+ "efficiency": { "id": "inherited", "status": "unverified", "observed": false }
68
+ },
69
+ "sourceUrl": "https://docs.devin.ai/desktop/cascade/models", "verifiedAt": "2026-07-20"
70
+ },
71
+ "openclaw": {
72
+ "roles": {
73
+ "reasoning": { "id": "configured/reasoning", "status": "unverified", "observed": false },
74
+ "coding": { "id": "configured/coding", "status": "unverified", "observed": false },
75
+ "efficiency": { "id": "configured/efficiency", "status": "unverified", "observed": false }
76
+ },
77
+ "sourceUrl": "https://docs.openclaw.ai/tools/subagents", "verifiedAt": "2026-07-20"
78
+ },
79
+ "hermes": {
80
+ "roles": {
81
+ "reasoning": { "id": "global-delegation-model", "status": "unverified", "observed": false },
82
+ "coding": { "id": "global-delegation-model", "status": "unverified", "observed": false },
83
+ "efficiency": { "id": "global-delegation-model", "status": "unverified", "observed": false }
84
+ },
85
+ "sourceUrl": "https://hermes-agent.nousresearch.com/docs/user-guide/features/delegation", "verifiedAt": "2026-07-20"
86
+ },
87
+ "openhuman": {
88
+ "roles": {
89
+ "reasoning": { "id": "Hint(reasoning)", "status": "active", "observed": false },
90
+ "coding": { "id": "Hint(coding)", "status": "active", "observed": false },
91
+ "efficiency": { "id": "Hint(efficiency)", "status": "active", "observed": false }
92
+ },
93
+ "sourceUrl": "https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/agent/harness/definition.rs", "verifiedAt": "2026-07-20"
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "evaluatedAt": "2026-07-20",
4
+ "method": "Deterministic representative-fixture rubric: required instructions, bounded scope, tool contract, output contract, and risk controls are scored from canonical artifacts. Provider live resolution is tracked separately by #1807.",
5
+ "threshold": 0.8,
6
+ "cases": [
7
+ {
8
+ "tier": "economy",
9
+ "kind": "agent",
10
+ "artifact": "technical-writer",
11
+ "checks": ["instructions", "bounded-scope", "tool-contract", "output-contract"],
12
+ "score": 1
13
+ },
14
+ {
15
+ "tier": "economy",
16
+ "kind": "skill",
17
+ "artifact": "radar-status",
18
+ "checks": ["instructions", "bounded-scope", "tool-contract", "output-contract"],
19
+ "score": 1
20
+ },
21
+ {
22
+ "tier": "standard",
23
+ "kind": "agent",
24
+ "artifact": "software-implementer",
25
+ "checks": ["instructions", "multi-step-work", "tool-contract", "verification"],
26
+ "score": 1
27
+ },
28
+ {
29
+ "tier": "standard",
30
+ "kind": "skill",
31
+ "artifact": "uat-execute",
32
+ "checks": ["instructions", "multi-step-work", "tool-contract", "verification"],
33
+ "score": 1
34
+ },
35
+ {
36
+ "tier": "premium",
37
+ "kind": "agent",
38
+ "artifact": "security-architect",
39
+ "checks": ["instructions", "risk-controls", "tool-contract", "decision-rationale"],
40
+ "score": 1
41
+ },
42
+ {
43
+ "tier": "premium",
44
+ "kind": "skill",
45
+ "artifact": "security-gate",
46
+ "checks": ["instructions", "risk-controls", "tool-contract", "decision-rationale"],
47
+ "score": 1
48
+ }
49
+ ]
50
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "reviewedAt": "2026-07-20",
4
+ "exemptions": {
5
+ "agents": {
6
+ "factory-compat": "Compatibility documentation is not a deployable agent definition and has no YAML frontmatter.",
7
+ "openai-compat": "Compatibility documentation is not a deployable agent definition and has no YAML frontmatter.",
8
+ "windsurf-compat": "Compatibility documentation is not a deployable agent definition and has no YAML frontmatter."
9
+ },
10
+ "skills": {}
11
+ },
12
+ "agents": {
13
+ "architecture-designer": "Cross-system architecture tradeoffs have a high downstream rework cost.",
14
+ "aiwg-model-reasoning-worker": "Cross-domain architecture and synthesis assignments have high downstream rework cost.",
15
+ "cloud-architect": "Infrastructure topology decisions carry material reliability and security risk.",
16
+ "forensics-orchestrator": "Evidence-preserving investigation plans require high-confidence sequencing.",
17
+ "incident-responder": "Active incident containment decisions can cause irreversible operational impact.",
18
+ "legal-liaison": "Legal and regulatory interpretation has material compliance consequences.",
19
+ "legal-reviewer": "Legal approval findings can block release and require high-confidence review.",
20
+ "security-architect": "System threat boundaries and trust decisions are security-critical.",
21
+ "security-auditor": "Vulnerability findings and exploitability judgments are security-critical.",
22
+ "security-gatekeeper": "Release security gate decisions require high-confidence risk assessment.",
23
+ "vision-owner": "Product-direction decisions coordinate multiple downstream delivery tracks."
24
+ },
25
+ "skills": {
26
+ "best-practices-audit": "External evidence synthesis can change high-impact technical policy.",
27
+ "crisis-response": "Public crisis decisions carry material legal and reputational risk.",
28
+ "flow-deploy-to-production": "Production release and rollback decisions have high operational impact.",
29
+ "flow-incident-response": "Incident orchestration must preserve containment and evidence integrity.",
30
+ "legal-compliance": "Legal and regulatory compliance conclusions require high confidence.",
31
+ "physical-threat-modeling": "Physical safety and asset protection decisions are high impact.",
32
+ "security-assessment": "Security posture findings influence remediation and release decisions.",
33
+ "security-audit": "Security audit conclusions require high-confidence vulnerability analysis.",
34
+ "security-gate": "The skill makes a release-blocking security decision."
35
+ }
36
+ }
package/bin/aiwg.mjs CHANGED
@@ -21,7 +21,7 @@
21
21
  * @implements #919
22
22
  */
23
23
 
24
- import { fileURLToPath } from 'url';
24
+ import { fileURLToPath, pathToFileURL } from 'url';
25
25
  import path from 'path';
26
26
  import { existsSync, readFileSync } from 'fs';
27
27
  import os from 'os';
@@ -186,7 +186,7 @@ async function resolveRouterPath() {
186
186
  * argv — handlers still see the flags. Call before the router loads so the
187
187
  * logger picks up the right level when it initializes.
188
188
  */
189
- async function applyVerbosityFromArgs(args) {
189
+ async function applyVerbosityFromArgs(args, routerPath) {
190
190
  let level = 'warn'; // default
191
191
  if (args.includes('--quiet') || args.includes('-q')) level = 'error';
192
192
  else if (args.includes('-vvv')) { level = 'debug'; process.env['AIWG_DEBUG'] ??= '1'; }
@@ -201,10 +201,9 @@ async function applyVerbosityFromArgs(args) {
201
201
  // and set the level. Failing to import the logger here is non-fatal — the
202
202
  // logger's own fallbacks will pick up AIWG_LOG_LEVEL from env.
203
203
  try {
204
- const routerPath = await resolveRouterPath();
205
204
  const logPath = path.join(path.dirname(routerPath), 'log.js');
206
205
  if (existsSync(logPath)) {
207
- const { setLogLevel, setInvocationId, pruneOldLogs } = await import('file://' + logPath);
206
+ const { setLogLevel, setInvocationId, pruneOldLogs } = await import(pathToFileURL(logPath).href);
208
207
  setLogLevel(level);
209
208
  setInvocationId(invocationId);
210
209
  // One-shot prune of old JSONL files on startup. Bounded work; safe to
@@ -240,18 +239,24 @@ async function main() {
240
239
  return;
241
240
  }
242
241
 
242
+ // Resolve the active router once. In dev mode this points into the checkout,
243
+ // while packageRoot still points at the globally installed launcher.
244
+ const routerPath = await resolveRouterPath();
245
+ const activePackageRoot = path.resolve(path.dirname(routerPath), '..', '..', '..');
246
+
243
247
  // Wire up the logger level from -v/-vv/--quiet/AIWG_LOG_LEVEL before any
244
248
  // handler runs, and stamp the top-level invocation ID so the logger can
245
249
  // tag every record with it.
246
- await applyVerbosityFromArgs(args);
250
+ await applyVerbosityFromArgs(args, routerPath);
247
251
 
248
252
  // Update notifier: print any pending notice from the previous run's
249
253
  // background check, then schedule the next background check. Both are
250
254
  // non-blocking — the current command never waits on the network.
251
255
  // Honors NO_UPDATE_NOTIFIER, CI=*, and non-TTY stderr.
252
- const { scheduleBackgroundCheck, maybePrintNotice } = await import('../dist/src/update/notifier.mjs');
253
- maybePrintNotice();
254
- scheduleBackgroundCheck(packageRoot);
256
+ const notifierPath = path.join(activePackageRoot, 'dist', 'src', 'update', 'notifier.mjs');
257
+ const { scheduleBackgroundCheck, maybePrintNotice } = await import(pathToFileURL(notifierPath).href);
258
+ maybePrintNotice(activePackageRoot);
259
+ scheduleBackgroundCheck(activePackageRoot);
255
260
 
256
261
  // Top-level cancellation controller. SIGINT / SIGTERM flip it, long-running
257
262
  // handlers plumb ctx.signal through fetches and loops so Ctrl-C cancels
@@ -276,9 +281,8 @@ async function main() {
276
281
 
277
282
  // Direct in-process dispatch — no tsx fork, no facade, no router-loader.
278
283
  trace('resolve:router');
279
- const routerPath = await resolveRouterPath();
280
284
  trace('import:router');
281
- const { run } = await import('file://' + routerPath);
285
+ const { run } = await import(pathToFileURL(routerPath).href);
282
286
  trace('dispatch:begin');
283
287
  try {
284
288
  await run(args, { cwd: process.cwd(), signal: abortController.signal });
@@ -8,5 +8,6 @@
8
8
  export { run } from '../cli/router.js';
9
9
  export * from '../resources/index.js';
10
10
  export * from '../sessions/index.js';
11
+ export * from '../memory/index.js';
11
12
  export * from '../security/threat-assessment-config.js';
12
13
  //# sourceMappingURL=index.d.ts.map
@@ -8,5 +8,6 @@
8
8
  export { run } from '../cli/router.js';
9
9
  export * from '../resources/index.js';
10
10
  export * from '../sessions/index.js';
11
+ export * from '../memory/index.js';
11
12
  export * from '../security/threat-assessment-config.js';
12
13
  //# sourceMappingURL=index.js.map
@@ -23,6 +23,7 @@ import { GRAPH_CONFIGS, OPERATIONAL_DISCOVERY_TYPES, OPERATIONAL_SHOW_TYPES, isO
23
23
  import { SUPPORTED_VIEWS } from './corpus-views/renderers.js';
24
24
  import { parseResourceSelector, readVerifiedRegularFile, } from '../resources/web-release.js';
25
25
  import { findPackageRoot } from '../cli/find-package-root.js';
26
+ import { createResourceCredentialProvider } from '../auth/resource-credentials.js';
26
27
  const MAX_RESOURCE_TRUST_ROOT_BYTES = 64 * 1024;
27
28
  function webReleaseOptionsFromEnvironment() {
28
29
  const baseUrl = process.env.AIWG_RESOURCE_BASE_URL;
@@ -42,6 +43,7 @@ function webReleaseOptionsFromEnvironment() {
42
43
  }
43
44
  }
44
45
  return {
46
+ credentialProvider: createResourceCredentialProvider(process.env),
45
47
  ...(baseUrl === undefined ? {} : { baseUrl }),
46
48
  ...(cacheRoot === undefined ? {} : { cacheRoot }),
47
49
  ...(publicKeyPem === undefined ? {} : { publicKeyPem }),
@@ -178,6 +178,10 @@ export const BUILTIN_GRAPH_CONFIGS = {
178
178
  '~/.aiwg/flows',
179
179
  '~/.aiwg/runbooks',
180
180
  '~/.aiwg/frameworks',
181
+ '~/.aiwg/addons',
182
+ '~/.aiwg/extensions',
183
+ '~/.aiwg/plugins',
184
+ '~/.aiwg/providers',
181
185
  ],
182
186
  extensions: [...DEFAULT_INDEX_EXTENSIONS],
183
187
  shared: true,
@@ -0,0 +1,209 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { createServer } from "node:http";
3
+ import { spawn } from "node:child_process";
4
+ const random = (bytes = 32) => randomBytes(bytes).toString("base64url");
5
+ export const createPkceChallenge = (verifier) => createHash("sha256").update(verifier).digest("base64url");
6
+ async function responseJson(response) {
7
+ const value = await response.json().catch(() => ({}));
8
+ return value && typeof value === "object" ? value : {};
9
+ }
10
+ function oauthError(value, fallback) {
11
+ return new Error(typeof value.error === "string" ? value.error : fallback);
12
+ }
13
+ function toCredentials(value, now = new Date()) {
14
+ if (typeof value.access_token !== "string" || typeof value.refresh_token !== "string"
15
+ || value.token_type !== "Bearer" || !Number.isSafeInteger(value.expires_in))
16
+ throw new Error("authorization server returned an invalid token response");
17
+ return {
18
+ accessToken: value.access_token,
19
+ refreshToken: value.refresh_token,
20
+ tokenType: "Bearer",
21
+ scope: String(value.scope || "").split(/\s+/).filter(Boolean),
22
+ expiresAt: new Date(now.getTime() + Number(value.expires_in) * 1000).toISOString(),
23
+ };
24
+ }
25
+ export const defaultBrowserOpener = async (url) => {
26
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd.exe" : "xdg-open";
27
+ const args = process.platform === "win32" ? ["/d", "/s", "/c", "start", "", url] : [url];
28
+ await new Promise((resolve, reject) => {
29
+ const child = spawn(command, args, { detached: true, stdio: "ignore", windowsHide: true });
30
+ child.once("error", reject);
31
+ child.once("spawn", () => { child.unref(); resolve(); });
32
+ });
33
+ };
34
+ function wait(ms, signal) {
35
+ return new Promise((resolve, reject) => {
36
+ if (signal?.aborted)
37
+ return reject(new Error("authentication canceled"));
38
+ const timer = setTimeout(resolve, ms);
39
+ const abort = () => { clearTimeout(timer); reject(new Error("authentication canceled")); };
40
+ signal?.addEventListener("abort", abort, { once: true });
41
+ });
42
+ }
43
+ export class AuthClient {
44
+ config;
45
+ store;
46
+ fetcher;
47
+ openBrowser;
48
+ now;
49
+ sleep;
50
+ constructor(config, store, fetcher = globalThis.fetch, openBrowser = defaultBrowserOpener, now = () => new Date(), sleep = wait) {
51
+ this.config = config;
52
+ this.store = store;
53
+ this.fetcher = fetcher;
54
+ this.openBrowser = openBrowser;
55
+ this.now = now;
56
+ this.sleep = sleep;
57
+ }
58
+ async request(pathname, init) {
59
+ const signal = init.signal || AbortSignal.timeout(this.config.requestTimeoutMs);
60
+ return this.fetcher(`${this.config.baseUrl}${pathname}`, { ...init, signal, redirect: "error" });
61
+ }
62
+ async loginBrowser(options = {}) {
63
+ const verifier = random(48);
64
+ const state = random();
65
+ let server;
66
+ const callback = new Promise((resolve, reject) => {
67
+ server = createServer((request, response) => {
68
+ try {
69
+ const url = new URL(request.url || "/", "http://127.0.0.1");
70
+ if (url.pathname !== "/callback" || !url.searchParams.get("code") || !url.searchParams.get("state")) {
71
+ response.writeHead(400, { "content-type": "text/plain", "cache-control": "no-store" });
72
+ response.end("Invalid authorization callback");
73
+ return;
74
+ }
75
+ response.writeHead(200, { "content-type": "text/plain", "cache-control": "no-store" });
76
+ response.end("AIWG authorization complete. You may close this window.");
77
+ resolve({ code: url.searchParams.get("code"), state: url.searchParams.get("state") });
78
+ }
79
+ catch (error) {
80
+ reject(error);
81
+ }
82
+ });
83
+ server.once("error", reject);
84
+ server.listen(0, "127.0.0.1");
85
+ });
86
+ try {
87
+ const callbackServer = server;
88
+ if (!callbackServer)
89
+ throw new Error("loopback callback server was not created");
90
+ await new Promise((resolve, reject) => { callbackServer.once("listening", resolve); callbackServer.once("error", reject); });
91
+ const address = callbackServer.address();
92
+ if (!address || typeof address === "string")
93
+ throw new Error("loopback callback did not bind a random port");
94
+ const redirectUri = `http://127.0.0.1:${address.port}/callback`;
95
+ const authorize = new URL(`${this.config.baseUrl}/oauth/authorize`);
96
+ authorize.search = new URLSearchParams({
97
+ client_id: this.config.clientId,
98
+ redirect_uri: redirectUri,
99
+ response_type: "code",
100
+ code_challenge: createPkceChallenge(verifier),
101
+ code_challenge_method: "S256",
102
+ state,
103
+ scope: this.config.scopes.join(" "),
104
+ ...(options.deviceLabel ? { device_label: options.deviceLabel } : {}),
105
+ }).toString();
106
+ await this.openBrowser(authorize.toString());
107
+ const result = await Promise.race([
108
+ callback,
109
+ new Promise((_, reject) => options.signal?.addEventListener("abort", () => reject(new Error("authentication canceled")), { once: true })),
110
+ ]);
111
+ if (result.state !== state)
112
+ throw new Error("OAuth state mismatch");
113
+ const response = await this.request("/oauth/token", {
114
+ method: "POST",
115
+ headers: { "content-type": "application/x-www-form-urlencoded" },
116
+ body: new URLSearchParams({ grant_type: "authorization_code", code: result.code, code_verifier: verifier, client_id: this.config.clientId, redirect_uri: redirectUri }),
117
+ signal: options.signal,
118
+ });
119
+ const value = await responseJson(response);
120
+ if (!response.ok)
121
+ throw oauthError(value, "authorization code exchange failed");
122
+ const credentials = toCredentials(value, this.now());
123
+ await this.store.save(credentials);
124
+ return credentials;
125
+ }
126
+ finally {
127
+ server?.closeAllConnections();
128
+ server?.close();
129
+ }
130
+ }
131
+ async loginDevice(options = {}) {
132
+ const start = await this.request("/v1/auth/device/authorization", {
133
+ method: "POST", headers: { "content-type": "application/json" }, signal: options.signal,
134
+ body: JSON.stringify({ client_id: this.config.clientId, scope: this.config.scopes.join(" "), device_label: options.deviceLabel }),
135
+ });
136
+ const value = await responseJson(start);
137
+ if (!start.ok || typeof value.device_code !== "string" || typeof value.expires_in !== "number" || typeof value.interval !== "number") {
138
+ throw oauthError(value, "device authorization failed");
139
+ }
140
+ options.onCode?.(value);
141
+ const deadline = this.now().getTime() + value.expires_in * 1000;
142
+ let interval = value.interval;
143
+ while (this.now().getTime() < deadline) {
144
+ await this.sleep(interval * 1000, options.signal);
145
+ if (this.now().getTime() >= deadline)
146
+ break;
147
+ const response = await this.request("/oauth/token", {
148
+ method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, signal: options.signal,
149
+ body: new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:device_code", device_code: value.device_code, client_id: this.config.clientId }),
150
+ });
151
+ const token = await responseJson(response);
152
+ if (response.ok) {
153
+ const credentials = toCredentials(token, this.now());
154
+ await this.store.save(credentials);
155
+ return credentials;
156
+ }
157
+ if (token.error === "authorization_pending")
158
+ continue;
159
+ if (token.error === "slow_down") {
160
+ interval += 5;
161
+ continue;
162
+ }
163
+ throw oauthError(token, "device authorization failed");
164
+ }
165
+ throw new Error("expired_token");
166
+ }
167
+ async refresh(credentials, signal) {
168
+ const response = await this.request("/oauth/token", {
169
+ method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, signal,
170
+ body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: credentials.refreshToken, client_id: this.config.clientId }),
171
+ });
172
+ const value = await responseJson(response);
173
+ if (!response.ok)
174
+ throw oauthError(value, "refresh failed");
175
+ const updated = toCredentials(value, this.now());
176
+ await this.store.save(updated);
177
+ return updated;
178
+ }
179
+ async status(signal) {
180
+ let credentials = await this.store.load();
181
+ if (!credentials)
182
+ throw new Error("not_authenticated");
183
+ if (Date.parse(credentials.expiresAt) <= this.now().getTime() + 30_000)
184
+ credentials = await this.refresh(credentials, signal);
185
+ let response = await this.request("/v1/me", { headers: { authorization: `Bearer ${credentials.accessToken}` }, signal });
186
+ if (response.status === 401) {
187
+ credentials = await this.refresh(credentials, signal);
188
+ response = await this.request("/v1/me", { headers: { authorization: `Bearer ${credentials.accessToken}` }, signal });
189
+ }
190
+ const value = await responseJson(response);
191
+ if (!response.ok || typeof value.sub !== "string")
192
+ throw oauthError(value, "status request failed");
193
+ return { profile: value, credentials };
194
+ }
195
+ async logout(signal) {
196
+ const credentials = await this.store.load();
197
+ try {
198
+ if (credentials)
199
+ await this.request("/oauth/revoke", {
200
+ method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, signal,
201
+ body: new URLSearchParams({ token: credentials.refreshToken, token_type_hint: "refresh_token" }),
202
+ });
203
+ }
204
+ finally {
205
+ await this.store.delete();
206
+ }
207
+ }
208
+ }
209
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,38 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ export const DEFAULT_AUTH_BASE_URL = "https://releases.aiwg.io";
4
+ export const DEFAULT_AUTH_CLIENT_ID = "aiwg-cli";
5
+ export const DEFAULT_AUTH_SCOPES = ["releases:read", "profile:read"];
6
+ function cleanOrigin(value, allowLoopbackHttp = false) {
7
+ const url = new URL(value);
8
+ const loopback = ["127.0.0.1", "[::1]", "::1", "localhost"].includes(url.hostname);
9
+ if (url.protocol !== "https:" && !(allowLoopbackHttp && loopback && url.protocol === "http:")) {
10
+ throw new Error("AIWG authentication requires HTTPS; HTTP is allowed only for explicitly enabled loopback tests");
11
+ }
12
+ if (url.username || url.password || url.search || url.hash)
13
+ throw new Error("AIWG authentication URL must be a clean origin");
14
+ url.pathname = url.pathname.replace(/\/+$/, "");
15
+ return url.toString().replace(/\/$/, "");
16
+ }
17
+ export function authConfigFromEnvironment(env = process.env) {
18
+ const baseUrl = cleanOrigin(env.AIWG_AUTH_BASE_URL || DEFAULT_AUTH_BASE_URL, env.AIWG_AUTH_ALLOW_INSECURE_LOOPBACK_HTTP === "1");
19
+ const clientId = env.AIWG_AUTH_CLIENT_ID || DEFAULT_AUTH_CLIENT_ID;
20
+ if (!/^[a-z0-9][a-z0-9._-]{1,63}$/.test(clientId))
21
+ throw new Error("AIWG auth client ID is invalid");
22
+ const scopes = (env.AIWG_AUTH_SCOPES || DEFAULT_AUTH_SCOPES.join(" ")).split(/\s+/).filter(Boolean);
23
+ if (!scopes.length || scopes.some((scope) => !/^[a-z][a-z0-9._:-]{1,63}$/.test(scope)))
24
+ throw new Error("AIWG auth scopes are invalid");
25
+ return { baseUrl, clientId, scopes, requestTimeoutMs: 30_000 };
26
+ }
27
+ export function explicitResourceToken(env = process.env) {
28
+ if (env.AIWG_RESOURCE_TOKEN_FILE) {
29
+ const pathname = path.resolve(env.AIWG_RESOURCE_TOKEN_FILE);
30
+ const stat = fs.lstatSync(pathname);
31
+ if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0) {
32
+ throw new Error("AIWG_RESOURCE_TOKEN_FILE must be a non-symlink regular file with mode 0600");
33
+ }
34
+ return fs.readFileSync(pathname, "utf8").trim() || null;
35
+ }
36
+ return env.AIWG_RESOURCE_TOKEN?.trim() || null;
37
+ }
38
+ //# sourceMappingURL=config.js.map