@aexhq/sdk 0.39.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +22 -16
  2. package/dist/_contracts/api-key.d.ts +49 -0
  3. package/dist/_contracts/api-key.js +87 -0
  4. package/dist/_contracts/bundle-manifest.d.ts +86 -0
  5. package/dist/_contracts/bundle-manifest.js +157 -0
  6. package/dist/_contracts/error-codes.d.ts +26 -0
  7. package/dist/_contracts/error-codes.js +79 -0
  8. package/dist/_contracts/error-factory.d.ts +32 -0
  9. package/dist/_contracts/error-factory.js +142 -0
  10. package/dist/_contracts/event-envelope.d.ts +33 -10
  11. package/dist/_contracts/event-envelope.js +46 -10
  12. package/dist/_contracts/event-view.d.ts +123 -0
  13. package/dist/_contracts/event-view.js +120 -0
  14. package/dist/_contracts/http.js +12 -3
  15. package/dist/_contracts/index.d.ts +8 -4
  16. package/dist/_contracts/index.js +11 -7
  17. package/dist/_contracts/models.d.ts +15 -0
  18. package/dist/_contracts/models.js +33 -0
  19. package/dist/_contracts/operations.d.ts +50 -2
  20. package/dist/_contracts/operations.js +100 -7
  21. package/dist/_contracts/run-record.d.ts +3 -2
  22. package/dist/_contracts/runtime-types.d.ts +126 -36
  23. package/dist/_contracts/runtime-types.js +33 -1
  24. package/dist/_contracts/sdk-errors.d.ts +61 -2
  25. package/dist/_contracts/sdk-errors.js +83 -3
  26. package/dist/_contracts/sdk-secrets.js +31 -6
  27. package/dist/_contracts/stable.d.ts +14 -0
  28. package/dist/_contracts/stable.js +14 -0
  29. package/dist/_contracts/status.d.ts +28 -1
  30. package/dist/_contracts/status.js +48 -3
  31. package/dist/_contracts/submission.d.ts +79 -2
  32. package/dist/_contracts/submission.js +148 -5
  33. package/dist/_contracts/suggest.d.ts +15 -0
  34. package/dist/_contracts/suggest.js +54 -0
  35. package/dist/asset-upload.d.ts +26 -0
  36. package/dist/asset-upload.js +218 -3
  37. package/dist/asset-upload.js.map +1 -1
  38. package/dist/bundle.d.ts +14 -3
  39. package/dist/bundle.js +34 -14
  40. package/dist/bundle.js.map +1 -1
  41. package/dist/canonical-zip.d.ts +68 -0
  42. package/dist/canonical-zip.js +307 -0
  43. package/dist/canonical-zip.js.map +1 -0
  44. package/dist/cli.mjs +1900 -340
  45. package/dist/cli.mjs.sha256 +1 -1
  46. package/dist/client.d.ts +217 -58
  47. package/dist/client.js +746 -261
  48. package/dist/client.js.map +1 -1
  49. package/dist/file.d.ts +33 -6
  50. package/dist/file.js +120 -54
  51. package/dist/file.js.map +1 -1
  52. package/dist/index.d.ts +17 -8
  53. package/dist/index.js +26 -9
  54. package/dist/index.js.map +1 -1
  55. package/dist/node-fs.d.ts +26 -9
  56. package/dist/node-fs.js +13 -38
  57. package/dist/node-fs.js.map +1 -1
  58. package/dist/node-walk.d.ts +69 -0
  59. package/dist/node-walk.js +146 -0
  60. package/dist/node-walk.js.map +1 -0
  61. package/dist/retry.d.ts +9 -13
  62. package/dist/retry.js +18 -17
  63. package/dist/retry.js.map +1 -1
  64. package/dist/skill.d.ts +16 -4
  65. package/dist/skill.js +18 -9
  66. package/dist/skill.js.map +1 -1
  67. package/dist/tool.d.ts +14 -2
  68. package/dist/tool.js +33 -7
  69. package/dist/tool.js.map +1 -1
  70. package/dist/version.d.ts +1 -1
  71. package/dist/version.js +1 -1
  72. package/docs/authentication.md +29 -5
  73. package/docs/billing.md +6 -0
  74. package/docs/concepts/agent-tools.md +11 -0
  75. package/docs/defaults.md +1 -0
  76. package/docs/errors.md +64 -4
  77. package/docs/events.md +84 -49
  78. package/docs/limits-and-quotas.md +24 -0
  79. package/docs/networking.md +7 -1
  80. package/docs/outputs.md +36 -7
  81. package/docs/quickstart.md +17 -7
  82. package/docs/run-config.md +3 -3
  83. package/docs/secrets.md +14 -0
  84. package/examples/feature-tour.ts +4 -6
  85. package/examples/spike-settle-latency.ts +125 -0
  86. package/package.json +4 -3
  87. package/dist/_contracts/event-guards.d.ts +0 -67
  88. package/dist/_contracts/event-guards.js +0 -36
package/README.md CHANGED
@@ -19,9 +19,11 @@ The package ships:
19
19
  npm i @aexhq/sdk
20
20
  ```
21
21
 
22
- This installs the TypeScript SDK exports and the bundled `aex` CLI. Set both
23
- credentials before running the examples: `AEX_API_KEY` authenticates to aex,
24
- and `ANTHROPIC_API_KEY` is your BYOK provider key for Claude.
22
+ This installs the TypeScript SDK exports and the bundled `aex` CLI. The CLI
23
+ ships inside the package — invoke it with `npx aex …` after a local install, or
24
+ `npm i -g @aexhq/sdk` to put a bare `aex` on your PATH. Set both credentials
25
+ before running the examples: `AEX_API_KEY` authenticates to aex, and
26
+ `ANTHROPIC_API_KEY` is your BYOK provider key for Claude.
25
27
 
26
28
  ```bash
27
29
  export AEX_API_KEY="<your-aex-api-key>"
@@ -33,7 +35,7 @@ export ANTHROPIC_API_KEY="<your-anthropic-api-key>"
33
35
  ```ts
34
36
  import { Aex, Models, Sizes } from "@aexhq/sdk";
35
37
 
36
- const aex = new Aex({ apiKey: process.env.AEX_API_KEY! });
38
+ const aex = new Aex(process.env.AEX_API_KEY!);
37
39
 
38
40
  const session = await aex.openSession({
39
41
  model: Models.CLAUDE_HAIKU_4_5,
@@ -44,8 +46,10 @@ const session = await aex.openSession({
44
46
  apiKeys: { anthropic: process.env.ANTHROPIC_API_KEY! }
45
47
  });
46
48
 
49
+ // done() awaits settle by default, so status is the terminal outcome
50
+ // ("succeeded"), and costUsd/usage are populated.
47
51
  const first = await session.send("Summarize this repo.").done();
48
- console.log(first.status, first.text);
52
+ console.log(first.status, first.costUsd, first.text);
49
53
 
50
54
  const resumed = await aex.openSession(session.id);
51
55
  await resumed.send("Continue with the follow-up validation.").done();
@@ -78,10 +82,10 @@ await aex.run({
78
82
  });
79
83
  ```
80
84
 
81
- The same request can run from the bundled CLI:
85
+ The same request can run from the bundled CLI (`npx aex` on a local install):
82
86
 
83
87
  ```bash
84
- aex run \
88
+ npx aex run \
85
89
  --api-key "$AEX_API_KEY" \
86
90
  --anthropic-api-key "$ANTHROPIC_API_KEY" \
87
91
  --model claude-haiku-4-5 \
@@ -97,20 +101,22 @@ your default `--aex-url`) is persisted to a `0600` config file
97
101
  on Windows). An explicit `--api-key` flag always overrides the stored one.
98
102
 
99
103
  ```bash
100
- aex login --api-key "$AEX_API_KEY" [--aex-url https://api.aex.dev]
101
- aex whoami # no --api-key needed after login
102
- aex auth status # show the resolved config (the token value is never printed)
103
- aex logout # clear the stored token
104
+ npx aex login --api-key "$AEX_API_KEY" [--aex-url https://api.aex.dev]
105
+ npx aex whoami # no --api-key needed after login
106
+ npx aex whoami --json # machine-readable workspace + scopes + limits
107
+ npx aex auth status # show the resolved config (the token value is never printed)
108
+ npx aex logout # clear the stored token
104
109
  ```
105
110
 
106
111
  Discover the closed sets the platform accepts — no token, no network (human table
107
- by default, machine JSON under `--json`):
112
+ by default, machine JSON under `--json`). Per-verb `--help` is also key-free:
108
113
 
109
114
  ```bash
110
- aex models list # canonical models + their default provider
111
- aex providers list # providers + the models each serves
112
- aex tools list # complete builtin tool set
113
- aex runtime-sizes list # managed runtime presets (cpus / memory / default)
115
+ npx aex models list # canonical models + their default provider
116
+ npx aex providers list # providers + the models each serves
117
+ npx aex tools list # complete builtin tool set
118
+ npx aex runtime-sizes list # managed runtime presets (cpus / memory / default)
119
+ npx aex run --help # flags for one verb (no API key required)
114
120
  ```
115
121
 
116
122
  Errors are typed and actionable. Every `openSession()` / `run()` config-validation
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Self-describing API-key codec (SSoT).
3
+ *
4
+ * An aex API key is `aex_<plane>_<regionCode>_<workspaceId>_<secret>_<crc>` —
5
+ * it embeds its own plane and region, CRC-protected. This is the canonical
6
+ * client-side codec: the SDK constructor parses the key to DERIVE the target
7
+ * plane (zero-network) and to fail fast on a plane/baseUrl mismatch instead of
8
+ * surfacing a bare `token_invalid` after a full round-trip.
9
+ *
10
+ * Ported byte-for-byte from the platform codec
11
+ * (`aex-platform apps/dashboard/src/server/auth.ts`), which is pinned to this
12
+ * module by a cross-repo parity test. Parse-only: it makes NO trust decision
13
+ * (the server still validates the secret).
14
+ */
15
+ export declare const API_KEY_PLANES: readonly ["dev", "prd"];
16
+ export type ApiKeyPlane = (typeof API_KEY_PLANES)[number];
17
+ /** Supported region → embedded region code. */
18
+ export declare const API_KEY_REGION_TO_CODE: Readonly<Record<string, string>>;
19
+ export interface ParsedApiKey {
20
+ readonly plane: ApiKeyPlane;
21
+ readonly regionCode: string;
22
+ readonly region: string;
23
+ /** The dash-free workspace id embedded in the key. */
24
+ readonly workspaceId: string;
25
+ }
26
+ /**
27
+ * Canonical form of a workspaceId for EMBEDDING in a key: dashes stripped so the
28
+ * whole key is a single double-click-selectable word (`_` is a word char, `-`
29
+ * is not). Kept byte-for-byte in sync with the platform codec.
30
+ */
31
+ export declare function normalizeWorkspaceId(workspaceId: string): string;
32
+ /**
33
+ * Parse a self-describing API key, or `null` for any legacy/opaque/tampered
34
+ * value. Validates the `aex_` prefix, the 6-part shape, a known plane and
35
+ * region code, and the CRC over the first 5 parts.
36
+ */
37
+ export declare function parseApiKey(token: string): ParsedApiKey | null;
38
+ /**
39
+ * Assemble a valid API key from its parts (the inverse of {@link parseApiKey}).
40
+ * Unlike the server's `mintApiKeyValue` this takes an EXPLICIT `secret` so it is
41
+ * deterministic — used by codec round-trip / cross-repo parity tests. The
42
+ * embedded workspace id is dash-normalized; `workspaceId` must not contain `_`.
43
+ */
44
+ export declare function formatApiKey(input: {
45
+ readonly plane: ApiKeyPlane;
46
+ readonly region: string;
47
+ readonly workspaceId: string;
48
+ readonly secret: string;
49
+ }): string;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Self-describing API-key codec (SSoT).
3
+ *
4
+ * An aex API key is `aex_<plane>_<regionCode>_<workspaceId>_<secret>_<crc>` —
5
+ * it embeds its own plane and region, CRC-protected. This is the canonical
6
+ * client-side codec: the SDK constructor parses the key to DERIVE the target
7
+ * plane (zero-network) and to fail fast on a plane/baseUrl mismatch instead of
8
+ * surfacing a bare `token_invalid` after a full round-trip.
9
+ *
10
+ * Ported byte-for-byte from the platform codec
11
+ * (`aex-platform apps/dashboard/src/server/auth.ts`), which is pinned to this
12
+ * module by a cross-repo parity test. Parse-only: it makes NO trust decision
13
+ * (the server still validates the secret).
14
+ */
15
+ export const API_KEY_PLANES = ["dev", "prd"];
16
+ /** Supported region → embedded region code. */
17
+ export const API_KEY_REGION_TO_CODE = {
18
+ "eu-west-2": "euw2",
19
+ "us-west-2": "usw2",
20
+ "ap-northeast-1": "apn1"
21
+ };
22
+ const CODE_TO_REGION = Object.fromEntries(Object.entries(API_KEY_REGION_TO_CODE).map(([region, code]) => [code, region]));
23
+ const API_KEY_PLANE_SET = new Set(API_KEY_PLANES);
24
+ /**
25
+ * Canonical form of a workspaceId for EMBEDDING in a key: dashes stripped so the
26
+ * whole key is a single double-click-selectable word (`_` is a word char, `-`
27
+ * is not). Kept byte-for-byte in sync with the platform codec.
28
+ */
29
+ export function normalizeWorkspaceId(workspaceId) {
30
+ return workspaceId.replace(/-/g, "");
31
+ }
32
+ /**
33
+ * Parse a self-describing API key, or `null` for any legacy/opaque/tampered
34
+ * value. Validates the `aex_` prefix, the 6-part shape, a known plane and
35
+ * region code, and the CRC over the first 5 parts.
36
+ */
37
+ export function parseApiKey(token) {
38
+ if (typeof token !== "string" || !token.startsWith("aex_"))
39
+ return null;
40
+ const parts = token.split("_");
41
+ if (parts.length !== 6)
42
+ return null;
43
+ const [prefix, plane, regionCode, workspaceId, secret, crc] = parts;
44
+ if (prefix !== "aex" || !API_KEY_PLANE_SET.has(plane) || !regionCode || !workspaceId || !secret || !crc) {
45
+ return null;
46
+ }
47
+ if (crc32Base36(parts.slice(0, 5).join("_")) !== crc)
48
+ return null;
49
+ const region = CODE_TO_REGION[regionCode];
50
+ if (region === undefined)
51
+ return null;
52
+ return { plane: plane, regionCode, region, workspaceId };
53
+ }
54
+ /**
55
+ * Assemble a valid API key from its parts (the inverse of {@link parseApiKey}).
56
+ * Unlike the server's `mintApiKeyValue` this takes an EXPLICIT `secret` so it is
57
+ * deterministic — used by codec round-trip / cross-repo parity tests. The
58
+ * embedded workspace id is dash-normalized; `workspaceId` must not contain `_`.
59
+ */
60
+ export function formatApiKey(input) {
61
+ const code = API_KEY_REGION_TO_CODE[input.region];
62
+ if (code === undefined) {
63
+ throw new Error(`API key region is not supported: ${input.region}`);
64
+ }
65
+ if (!input.workspaceId || input.workspaceId.includes("_")) {
66
+ throw new Error("workspaceId must be non-empty and contain no '_'");
67
+ }
68
+ if (!input.secret || input.secret.includes("_")) {
69
+ throw new Error("secret must be non-empty and contain no '_'");
70
+ }
71
+ const body = ["aex", input.plane, code, normalizeWorkspaceId(input.workspaceId), input.secret].join("_");
72
+ return `${body}_${crc32Base36(body)}`;
73
+ }
74
+ function crc32Base36(input) {
75
+ return crc32(input).toString(36);
76
+ }
77
+ function crc32(input) {
78
+ let crc = 0xffffffff;
79
+ for (const byte of Buffer.from(input, "utf8")) {
80
+ crc ^= byte;
81
+ for (let i = 0; i < 8; i += 1) {
82
+ crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
83
+ }
84
+ }
85
+ return (crc ^ 0xffffffff) >>> 0;
86
+ }
87
+ //# sourceMappingURL=api-key.js.map
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Bundle fidelity sidecar — the shared source of truth for the `.aexmeta.json`
3
+ * metadata entry that rides inside a canonical bundle zip.
4
+ *
5
+ * A canonical bundle (skill / tool / file / agentsMd) is a deterministic zip of
6
+ * regular files. fflate's `unzipSync` on the restore side is asymmetric: it
7
+ * surfaces only entry bytes, never external attributes / os field / symlink
8
+ * flags. So exec bits and symlinks — the metadata a local dir → zip →
9
+ * container-restore round-trip must preserve — cannot travel in the zip's
10
+ * external attributes and be read back. Instead they travel in ONE reserved
11
+ * entry, `.aexmeta.json`, which both the SDK (encode) and the container
12
+ * materializer (restore) agree on. This module is that agreement.
13
+ *
14
+ * SECURITY: {@link symlinkTargetEscapes} is a restore-time boundary — it decides
15
+ * whether a captured symlink target may be recreated on the container FS. It is
16
+ * PURELY LEXICAL (no realpath, no FS access → no TOCTOU) and is fuzzed. Both
17
+ * repos import it; do not fork it.
18
+ *
19
+ * Determinism: the sidecar is emitted ONLY when there is metadata to carry (exec
20
+ * or symlinks non-empty) and is serialized canonically (fixed field order, sorted
21
+ * arrays, no insignificant whitespace, no trailing newline). A pure-content
22
+ * bundle emits NO sidecar → its bytes stay identical to the pre-fidelity output,
23
+ * so existing content-addressed dedup is preserved.
24
+ */
25
+ /** Reserved zip entry carrying the fidelity sidecar. Rejected as a user file path. */
26
+ export declare const RESERVED_META_ENTRY = ".aexmeta.json";
27
+ /** Normalised mode for an executable regular file (has any exec bit). */
28
+ export declare const EXEC_MODE = 493;
29
+ /** Normalised mode for an ordinary regular file. */
30
+ export declare const DEFAULT_FILE_MODE = 420;
31
+ /** Bundle-time cap on a captured symlink target string. */
32
+ export declare const MAX_SYMLINK_TARGET_LENGTH = 4096;
33
+ /** A captured symlink: `path` is the bundle-relative link name, `target` the raw `readlink()` string. */
34
+ export interface BundleSymlink {
35
+ readonly path: string;
36
+ readonly target: string;
37
+ }
38
+ /**
39
+ * The fidelity sidecar. `exec` lists the bundle-relative paths that restore to
40
+ * {@link EXEC_MODE} (all other files stay {@link DEFAULT_FILE_MODE}); `symlinks`
41
+ * lists links to recreate (subject to {@link symlinkTargetEscapes}).
42
+ */
43
+ export interface BundleManifest {
44
+ readonly v: 1;
45
+ readonly exec: readonly string[];
46
+ readonly symlinks: readonly BundleSymlink[];
47
+ }
48
+ /** True when a manifest carries no metadata worth emitting (so no sidecar is written). */
49
+ export declare function bundleManifestIsEmpty(manifest: {
50
+ readonly exec?: readonly string[];
51
+ readonly symlinks?: readonly {
52
+ readonly path: string;
53
+ }[];
54
+ }): boolean;
55
+ /**
56
+ * Serialize a manifest to canonical, byte-stable UTF-8 bytes: fixed field order
57
+ * (`v`, `exec`, `symlinks`), `exec` sorted ascending, `symlinks` sorted by
58
+ * `path`, no insignificant whitespace (compact JSON), no trailing newline. Two
59
+ * equal manifests always serialize to identical bytes → the sidecar never
60
+ * perturbs dedup determinism.
61
+ */
62
+ export declare function serializeBundleManifest(manifest: BundleManifest): Uint8Array;
63
+ /**
64
+ * Parse the sidecar bytes into a validated {@link BundleManifest}, or `null` when
65
+ * the bytes are absent, not valid canonical JSON, or carry an unknown version.
66
+ * Forward-compatible: an unknown `v` yields `null` so a restore falls back to
67
+ * content-only (metadata is dropped, never a hard failure). Structurally-invalid
68
+ * entries are dropped individually; the parse never throws.
69
+ */
70
+ export declare function parseBundleManifest(bytes: Uint8Array | null | undefined): BundleManifest | null;
71
+ /**
72
+ * SECURITY boundary — decide whether recreating a symlink `linkRelPath` → `target`
73
+ * would let the link escape the bundle root. PURELY LEXICAL (no realpath / no FS
74
+ * access → no TOCTOU). Returns `true` (REJECT / do not create) when ANY of:
75
+ *
76
+ * - `target` is empty or contains a NUL;
77
+ * - `target` is absolute — starts with `/`, matches a Windows drive (`C:`), is a
78
+ * UNC path (`\\host`), or contains any backslash;
79
+ * - the lexically-resolved target (join the link's dirname with the target, then
80
+ * normalise `.`/`..`) climbs at or above the bundle root (leading `..`).
81
+ *
82
+ * Otherwise the link is relative and resolves to a path inside (or at) the bundle
83
+ * root — safe to recreate with the ORIGINAL relative `target`. A dangling in-root
84
+ * target (pointing at a not-yet-existing sibling) is SAFE and allowed.
85
+ */
86
+ export declare function symlinkTargetEscapes(linkRelPath: string, target: string): boolean;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Bundle fidelity sidecar — the shared source of truth for the `.aexmeta.json`
3
+ * metadata entry that rides inside a canonical bundle zip.
4
+ *
5
+ * A canonical bundle (skill / tool / file / agentsMd) is a deterministic zip of
6
+ * regular files. fflate's `unzipSync` on the restore side is asymmetric: it
7
+ * surfaces only entry bytes, never external attributes / os field / symlink
8
+ * flags. So exec bits and symlinks — the metadata a local dir → zip →
9
+ * container-restore round-trip must preserve — cannot travel in the zip's
10
+ * external attributes and be read back. Instead they travel in ONE reserved
11
+ * entry, `.aexmeta.json`, which both the SDK (encode) and the container
12
+ * materializer (restore) agree on. This module is that agreement.
13
+ *
14
+ * SECURITY: {@link symlinkTargetEscapes} is a restore-time boundary — it decides
15
+ * whether a captured symlink target may be recreated on the container FS. It is
16
+ * PURELY LEXICAL (no realpath, no FS access → no TOCTOU) and is fuzzed. Both
17
+ * repos import it; do not fork it.
18
+ *
19
+ * Determinism: the sidecar is emitted ONLY when there is metadata to carry (exec
20
+ * or symlinks non-empty) and is serialized canonically (fixed field order, sorted
21
+ * arrays, no insignificant whitespace, no trailing newline). A pure-content
22
+ * bundle emits NO sidecar → its bytes stay identical to the pre-fidelity output,
23
+ * so existing content-addressed dedup is preserved.
24
+ */
25
+ /** Reserved zip entry carrying the fidelity sidecar. Rejected as a user file path. */
26
+ export const RESERVED_META_ENTRY = ".aexmeta.json";
27
+ /** Normalised mode for an executable regular file (has any exec bit). */
28
+ export const EXEC_MODE = 0o755;
29
+ /** Normalised mode for an ordinary regular file. */
30
+ export const DEFAULT_FILE_MODE = 0o644;
31
+ /** Bundle-time cap on a captured symlink target string. */
32
+ export const MAX_SYMLINK_TARGET_LENGTH = 4096;
33
+ /** True when a manifest carries no metadata worth emitting (so no sidecar is written). */
34
+ export function bundleManifestIsEmpty(manifest) {
35
+ return (manifest.exec?.length ?? 0) === 0 && (manifest.symlinks?.length ?? 0) === 0;
36
+ }
37
+ const META_TEXT_ENCODER = new TextEncoder();
38
+ const META_TEXT_DECODER = new TextDecoder("utf-8", { fatal: false });
39
+ /**
40
+ * Serialize a manifest to canonical, byte-stable UTF-8 bytes: fixed field order
41
+ * (`v`, `exec`, `symlinks`), `exec` sorted ascending, `symlinks` sorted by
42
+ * `path`, no insignificant whitespace (compact JSON), no trailing newline. Two
43
+ * equal manifests always serialize to identical bytes → the sidecar never
44
+ * perturbs dedup determinism.
45
+ */
46
+ export function serializeBundleManifest(manifest) {
47
+ const exec = [...manifest.exec].sort(byString);
48
+ const symlinks = [...manifest.symlinks]
49
+ .map((s) => ({ path: s.path, target: s.target }))
50
+ .sort((a, b) => byString(a.path, b.path));
51
+ // Object literal key order is the wire order under JSON.stringify (fixed).
52
+ const json = JSON.stringify({ v: 1, exec, symlinks });
53
+ return META_TEXT_ENCODER.encode(json);
54
+ }
55
+ /**
56
+ * Parse the sidecar bytes into a validated {@link BundleManifest}, or `null` when
57
+ * the bytes are absent, not valid canonical JSON, or carry an unknown version.
58
+ * Forward-compatible: an unknown `v` yields `null` so a restore falls back to
59
+ * content-only (metadata is dropped, never a hard failure). Structurally-invalid
60
+ * entries are dropped individually; the parse never throws.
61
+ */
62
+ export function parseBundleManifest(bytes) {
63
+ if (!bytes || bytes.byteLength === 0)
64
+ return null;
65
+ let parsed;
66
+ try {
67
+ parsed = JSON.parse(META_TEXT_DECODER.decode(bytes));
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
73
+ return null;
74
+ const record = parsed;
75
+ if (record.v !== 1)
76
+ return null;
77
+ const exec = [];
78
+ if (Array.isArray(record.exec)) {
79
+ for (const item of record.exec) {
80
+ if (typeof item === "string" && item.length > 0)
81
+ exec.push(item);
82
+ }
83
+ }
84
+ const symlinks = [];
85
+ if (Array.isArray(record.symlinks)) {
86
+ for (const item of record.symlinks) {
87
+ if (!item || typeof item !== "object" || Array.isArray(item))
88
+ continue;
89
+ const rec = item;
90
+ const path = rec.path;
91
+ const target = rec.target;
92
+ if (typeof path === "string" && path.length > 0 && typeof target === "string") {
93
+ symlinks.push({ path, target });
94
+ }
95
+ }
96
+ }
97
+ return { v: 1, exec, symlinks };
98
+ }
99
+ /**
100
+ * SECURITY boundary — decide whether recreating a symlink `linkRelPath` → `target`
101
+ * would let the link escape the bundle root. PURELY LEXICAL (no realpath / no FS
102
+ * access → no TOCTOU). Returns `true` (REJECT / do not create) when ANY of:
103
+ *
104
+ * - `target` is empty or contains a NUL;
105
+ * - `target` is absolute — starts with `/`, matches a Windows drive (`C:`), is a
106
+ * UNC path (`\\host`), or contains any backslash;
107
+ * - the lexically-resolved target (join the link's dirname with the target, then
108
+ * normalise `.`/`..`) climbs at or above the bundle root (leading `..`).
109
+ *
110
+ * Otherwise the link is relative and resolves to a path inside (or at) the bundle
111
+ * root — safe to recreate with the ORIGINAL relative `target`. A dangling in-root
112
+ * target (pointing at a not-yet-existing sibling) is SAFE and allowed.
113
+ */
114
+ export function symlinkTargetEscapes(linkRelPath, target) {
115
+ if (typeof target !== "string" || target.length === 0)
116
+ return true;
117
+ if (target.includes("\0"))
118
+ return true;
119
+ // Absolute / platform-specific targets are always rejected.
120
+ if (target.startsWith("/"))
121
+ return true;
122
+ if (/^[A-Za-z]:/.test(target))
123
+ return true;
124
+ if (target.includes("\\"))
125
+ return true;
126
+ // Lexically resolve relative to the link's own directory.
127
+ const linkDir = posixDirname(linkRelPath);
128
+ const joined = linkDir.length > 0 ? `${linkDir}/${target}` : target;
129
+ const stack = [];
130
+ for (const segment of joined.split("/")) {
131
+ if (segment === "" || segment === ".")
132
+ continue;
133
+ if (segment === "..") {
134
+ // Pop an in-root component, or record an escaping climb.
135
+ if (stack.length > 0 && stack[stack.length - 1] !== "..") {
136
+ stack.pop();
137
+ }
138
+ else {
139
+ stack.push("..");
140
+ }
141
+ }
142
+ else {
143
+ stack.push(segment);
144
+ }
145
+ }
146
+ // A leading `..` means the resolved path is at or above the bundle root.
147
+ return stack.length > 0 && stack[0] === "..";
148
+ }
149
+ /** POSIX dirname of a forward-slash relative path ("" when there is no parent segment). */
150
+ function posixDirname(path) {
151
+ const idx = path.lastIndexOf("/");
152
+ return idx <= 0 ? "" : path.slice(0, idx);
153
+ }
154
+ function byString(a, b) {
155
+ return a < b ? -1 : a > b ? 1 : 0;
156
+ }
157
+ //# sourceMappingURL=bundle-manifest.js.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * SSoT for the platform's STABLE API error codes.
3
+ *
4
+ * The server (aex-platform `api.ts`) imports this table instead of keeping its
5
+ * own private copy, so a route emitting a code absent from the union fails to
6
+ * compile and adding a code without a message is caught. The SDK error factory
7
+ * ({@link import("./error-factory.js").apiErrorFromResponse}) dispatches on the
8
+ * code to a typed subclass; the CLI maps it to a human remedy.
9
+ *
10
+ * The code is the stable, machine-branchable identity of a failure — distinct
11
+ * from the human `message`. `idempotency_conflict` and `insufficient_scope`
12
+ * (previously message-less bare 409/403 bodies) are first-class here.
13
+ */
14
+ export declare const AEX_API_ERROR_CODES: readonly ["unauthorized", "forbidden", "insufficient_scope", "token_invalid", "token_revoked", "token_expired", "malformed_token", "not_found", "idempotency_conflict", "session_busy", "run_not_terminal", "unknown_workspace", "workspace_concurrency_exceeded", "workspace_submit_rate_exceeded", "workspace_spend_cap_exceeded", "insufficient_balance", "rate_limited", "upstream_error", "internal_error"];
15
+ export type AexApiErrorCode = (typeof AEX_API_ERROR_CODES)[number];
16
+ /**
17
+ * Human message per stable code. A `Record` (not `Partial`) so a code added to
18
+ * {@link AEX_API_ERROR_CODES} without a message is a COMPILE error. These are
19
+ * the fallback messages the factory uses when the server sends a BARE code with
20
+ * no `message` detail.
21
+ */
22
+ export declare const AEX_API_ERROR_MESSAGES: Record<AexApiErrorCode, string>;
23
+ /** Optional per-code fix hint the CLI surfaces alongside the error message. */
24
+ export declare const AEX_API_ERROR_REMEDIES: Partial<Record<AexApiErrorCode, string>>;
25
+ /** Narrow an arbitrary value to a known {@link AexApiErrorCode}. */
26
+ export declare function isAexApiErrorCode(value: unknown): value is AexApiErrorCode;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * SSoT for the platform's STABLE API error codes.
3
+ *
4
+ * The server (aex-platform `api.ts`) imports this table instead of keeping its
5
+ * own private copy, so a route emitting a code absent from the union fails to
6
+ * compile and adding a code without a message is caught. The SDK error factory
7
+ * ({@link import("./error-factory.js").apiErrorFromResponse}) dispatches on the
8
+ * code to a typed subclass; the CLI maps it to a human remedy.
9
+ *
10
+ * The code is the stable, machine-branchable identity of a failure — distinct
11
+ * from the human `message`. `idempotency_conflict` and `insufficient_scope`
12
+ * (previously message-less bare 409/403 bodies) are first-class here.
13
+ */
14
+ export const AEX_API_ERROR_CODES = [
15
+ "unauthorized",
16
+ "forbidden",
17
+ "insufficient_scope",
18
+ "token_invalid",
19
+ "token_revoked",
20
+ "token_expired",
21
+ "malformed_token",
22
+ "not_found",
23
+ "idempotency_conflict",
24
+ "session_busy",
25
+ "run_not_terminal",
26
+ "unknown_workspace",
27
+ "workspace_concurrency_exceeded",
28
+ "workspace_submit_rate_exceeded",
29
+ "workspace_spend_cap_exceeded",
30
+ "insufficient_balance",
31
+ "rate_limited",
32
+ "upstream_error",
33
+ "internal_error"
34
+ ];
35
+ /**
36
+ * Human message per stable code. A `Record` (not `Partial`) so a code added to
37
+ * {@link AEX_API_ERROR_CODES} without a message is a COMPILE error. These are
38
+ * the fallback messages the factory uses when the server sends a BARE code with
39
+ * no `message` detail.
40
+ */
41
+ export const AEX_API_ERROR_MESSAGES = {
42
+ unauthorized: "The request was not authenticated.",
43
+ forbidden: "The API key is not permitted to perform this action.",
44
+ insufficient_scope: "The API key is missing a scope this endpoint requires.",
45
+ token_invalid: "The API key is not valid.",
46
+ token_revoked: "The API key has been revoked.",
47
+ token_expired: "The API key has expired.",
48
+ malformed_token: "The API key is malformed.",
49
+ not_found: "The requested resource was not found.",
50
+ idempotency_conflict: "This idempotency key was already used with a different request body.",
51
+ session_busy: "The session is busy handling another turn.",
52
+ run_not_terminal: "The run has not reached a terminal state yet.",
53
+ unknown_workspace: "The workspace could not be resolved from the API key.",
54
+ workspace_concurrency_exceeded: "The workspace has reached its concurrent-run limit.",
55
+ workspace_submit_rate_exceeded: "The workspace submit-rate limit was exceeded.",
56
+ workspace_spend_cap_exceeded: "The workspace monthly spend cap was reached.",
57
+ insufficient_balance: "The workspace prepaid balance is insufficient to submit this run.",
58
+ rate_limited: "Too many requests — retry after a short backoff.",
59
+ upstream_error: "An upstream provider returned an error.",
60
+ internal_error: "The aex API encountered an internal error."
61
+ };
62
+ /** Optional per-code fix hint the CLI surfaces alongside the error message. */
63
+ export const AEX_API_ERROR_REMEDIES = {
64
+ insufficient_scope: "Mint an API key that includes the scope this endpoint requires.",
65
+ idempotency_conflict: "Use a fresh idempotency key, or resubmit the byte-identical request body to replay the original result.",
66
+ token_invalid: "Check the API key value and that its plane matches your baseUrl.",
67
+ token_expired: "Mint a new API key.",
68
+ token_revoked: "Mint a new API key.",
69
+ workspace_spend_cap_exceeded: "Raise the workspace spend cap or wait for the next billing cycle.",
70
+ insufficient_balance: "Top up the workspace balance or add a payment method.",
71
+ workspace_concurrency_exceeded: "Wait for in-flight runs to finish or raise the concurrency limit.",
72
+ workspace_submit_rate_exceeded: "Slow the submit rate or raise the workspace submit-rate limit."
73
+ };
74
+ const API_ERROR_CODE_SET = new Set(AEX_API_ERROR_CODES);
75
+ /** Narrow an arbitrary value to a known {@link AexApiErrorCode}. */
76
+ export function isAexApiErrorCode(value) {
77
+ return typeof value === "string" && API_ERROR_CODE_SET.has(value);
78
+ }
79
+ //# sourceMappingURL=error-codes.js.map
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The SINGLE wire→exception mapping. `apiErrorFromResponse` reads a non-2xx
3
+ * body's stable `error` code, attaches it (typed) as `apiCode` plus the
4
+ * `requestId`, and dispatches to the right {@link AexApiError} subclass. Both
5
+ * `HttpClient.request` and `HttpClient.download` construct errors ONLY through
6
+ * here, so the wire→exception mapping lives in one place — no inline
7
+ * `new AexApiError(...)` with a hardcoded coarse code.
8
+ */
9
+ import { AexApiError } from "./sdk-errors.js";
10
+ import { type AexApiErrorCode } from "./error-codes.js";
11
+ export interface ApiErrorFromResponseInput {
12
+ readonly status: number;
13
+ readonly body: unknown;
14
+ /**
15
+ * Precomputed human message (e.g. `HttpClient`'s `extractErrorMessage`). When
16
+ * absent the factory derives one from the stable code table.
17
+ */
18
+ readonly message?: string;
19
+ readonly requestId?: string;
20
+ readonly cause?: unknown;
21
+ }
22
+ type ApiErrorKind = "auth" | "idempotency" | "not_found" | "rate_limit" | "generic";
23
+ /**
24
+ * EXHAUSTIVE stable-code → subclass-kind map. There is NO `default`: adding a
25
+ * code to {@link AEX_API_ERROR_CODES} without a case here fails to compile
26
+ * (the function would fall through and return `undefined`, which the
27
+ * `ApiErrorKind` return type forbids). This is the whole-class guard that a new
28
+ * server code can never silently collapse to the base error.
29
+ */
30
+ export declare function apiErrorKindForCode(code: AexApiErrorCode): ApiErrorKind;
31
+ export declare function apiErrorFromResponse(input: ApiErrorFromResponseInput): AexApiError;
32
+ export {};