@aexhq/sdk 0.40.5 → 0.40.6
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/README.md +159 -159
- package/dist/_contracts/api-key.js +0 -1
- package/dist/_contracts/bundle-manifest.js +0 -1
- package/dist/_contracts/connection-ticket.js +0 -1
- package/dist/_contracts/error-codes.js +0 -1
- package/dist/_contracts/error-factory.js +0 -1
- package/dist/_contracts/event-envelope.js +0 -1
- package/dist/_contracts/event-stream-client.js +0 -1
- package/dist/_contracts/event-view.js +0 -1
- package/dist/_contracts/http.js +0 -1
- package/dist/_contracts/index.js +0 -1
- package/dist/_contracts/internal.d.ts +36 -0
- package/dist/_contracts/internal.js +155 -6
- package/dist/_contracts/models.js +0 -1
- package/dist/_contracts/operations.js +1 -2
- package/dist/_contracts/post-hook.js +0 -1
- package/dist/_contracts/provider-support.js +0 -1
- package/dist/_contracts/run-artifacts.js +0 -1
- package/dist/_contracts/run-config.js +0 -1
- package/dist/_contracts/run-cost.js +0 -1
- package/dist/_contracts/run-custody.js +0 -1
- package/dist/_contracts/run-record.js +0 -1
- package/dist/_contracts/run-retention.js +0 -1
- package/dist/_contracts/run-trace.js +0 -1
- package/dist/_contracts/run-unit.js +0 -1
- package/dist/_contracts/runner-event.js +0 -1
- package/dist/_contracts/runtime-manifest.js +0 -1
- package/dist/_contracts/runtime-security-profile.js +0 -1
- package/dist/_contracts/runtime-sizes.js +0 -1
- package/dist/_contracts/runtime-types.js +0 -1
- package/dist/_contracts/sdk-errors.js +0 -1
- package/dist/_contracts/sdk-secrets.js +0 -1
- package/dist/_contracts/side-effect-audit.js +0 -1
- package/dist/_contracts/sse.js +0 -0
- package/dist/_contracts/stable.js +0 -1
- package/dist/_contracts/status.js +0 -1
- package/dist/_contracts/submission.js +0 -1
- package/dist/_contracts/suggest.js +0 -1
- package/dist/_contracts/webhook-verify.js +0 -1
- package/dist/asset-upload.d.ts +3 -43
- package/dist/asset-upload.js +1 -109
- package/dist/asset-upload.js.map +1 -1
- package/dist/cli.mjs +354 -781
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/fetch-archive.js +76 -16
- package/dist/fetch-archive.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/authentication.md +125 -125
- package/docs/billing.md +118 -118
- package/docs/concepts/agent-tools.md +66 -66
- package/docs/concepts/composition.md +37 -37
- package/docs/concepts/providers-and-runtimes.md +54 -54
- package/docs/concepts/runs.md +49 -49
- package/docs/concepts/subagents.md +88 -88
- package/docs/credentials.md +125 -125
- package/docs/defaults.md +64 -64
- package/docs/errors.md +196 -196
- package/docs/events.md +243 -243
- package/docs/limits-and-quotas.md +144 -144
- package/docs/limits.md +50 -50
- package/docs/mcp.md +44 -44
- package/docs/networking.md +163 -163
- package/docs/outputs.md +267 -267
- package/docs/public-surface.json +77 -77
- package/docs/quickstart.md +119 -119
- package/docs/release.md +38 -38
- package/docs/retries.md +129 -129
- package/docs/run-config.md +58 -58
- package/docs/run-record.md +55 -55
- package/docs/secrets.md +125 -130
- package/docs/testing.md +35 -35
- package/docs/vision-skills.md +91 -91
- package/docs/webhooks.md +127 -131
- package/examples/feature-tour.ts +282 -282
- package/examples/spike-settle-latency.ts +125 -125
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,159 +1,159 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: aex
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# @aexhq/sdk
|
|
6
|
-
|
|
7
|
-
aex is an agent execution platform for launching autonomous agents from a simple TypeScript SDK and CLI.
|
|
8
|
-
|
|
9
|
-
The package ships:
|
|
10
|
-
|
|
11
|
-
- `Aex` for sessions, one-shot runs, inspect, download, cancel, and delete.
|
|
12
|
-
- `sessions` / `openSession()` for durable, resumable agent sessions.
|
|
13
|
-
- Typed run primitives: `Models`, `Providers`, `Sizes`, `Skill`, `Tool` / `Tools`, `AgentsMd`, `File`, `McpServer`, and `Secret`.
|
|
14
|
-
- A bundled `aex` CLI with the same run, status, events, outputs, download, cancel, delete, and whoami operations.
|
|
15
|
-
|
|
16
|
-
## Install
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
npm i @aexhq/sdk
|
|
20
|
-
```
|
|
21
|
-
|
|
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.
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
export AEX_API_KEY="<your-aex-api-key>"
|
|
30
|
-
export ANTHROPIC_API_KEY="<your-anthropic-api-key>"
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## First Session
|
|
34
|
-
|
|
35
|
-
```ts
|
|
36
|
-
import { Aex, Models, Sizes } from "@aexhq/sdk";
|
|
37
|
-
|
|
38
|
-
const aex = new Aex(process.env.AEX_API_KEY!);
|
|
39
|
-
|
|
40
|
-
const session = await aex.openSession({
|
|
41
|
-
model: Models.CLAUDE_HAIKU_4_5,
|
|
42
|
-
system: "You are a concise engineering assistant.",
|
|
43
|
-
runtime: Sizes.SHARED_0_25X_1GB,
|
|
44
|
-
// Default is "3m"; set it explicitly when you want a different idle window.
|
|
45
|
-
overrides: { idleTtl: "3m" },
|
|
46
|
-
apiKeys: { anthropic: process.env.ANTHROPIC_API_KEY! }
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
// done() awaits settle by default, so status is the terminal outcome
|
|
50
|
-
// ("succeeded"), and costUsd/usage are populated.
|
|
51
|
-
const first = await session.send("Summarize this repo.").done();
|
|
52
|
-
console.log(first.status, first.costUsd, first.text);
|
|
53
|
-
|
|
54
|
-
const resumed = await aex.openSession(session.id);
|
|
55
|
-
await resumed.send("Continue with the follow-up validation.").done();
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
Need a one-shot convenience? `run()` opens a session, sends `message` as one
|
|
59
|
-
turn, and returns the collected result. The returned `runId` is the session id,
|
|
60
|
-
so it can be resumed later with `openSession(runId)`.
|
|
61
|
-
|
|
62
|
-
```ts
|
|
63
|
-
const result = await aex.run({
|
|
64
|
-
model: Models.CLAUDE_HAIKU_4_5,
|
|
65
|
-
apiKeys: { anthropic: process.env.ANTHROPIC_API_KEY! },
|
|
66
|
-
message: "Write the report and save outputs."
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
console.log(result.runId, result.status, result.text);
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
For multiple providers, include each BYOK key in `apiKeys`:
|
|
73
|
-
|
|
74
|
-
```ts
|
|
75
|
-
await aex.run({
|
|
76
|
-
model: Models.CLAUDE_HAIKU_4_5,
|
|
77
|
-
apiKeys: {
|
|
78
|
-
anthropic: process.env.ANTHROPIC_API_KEY!,
|
|
79
|
-
openai: process.env.OPENAI_API_KEY!
|
|
80
|
-
},
|
|
81
|
-
message: "Delegate research to a subagent."
|
|
82
|
-
});
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
The same request can run from the bundled CLI (`npx aex` on a local install):
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
npx aex run \
|
|
89
|
-
--api-key "$AEX_API_KEY" \
|
|
90
|
-
--anthropic-api-key "$ANTHROPIC_API_KEY" \
|
|
91
|
-
--model claude-haiku-4-5 \
|
|
92
|
-
--prompt "Write the report and save outputs." \
|
|
93
|
-
--follow
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
## CLI: login, discovery, typed errors
|
|
97
|
-
|
|
98
|
-
Stop re-passing `--api-key` on every command — log in once and the token (plus
|
|
99
|
-
your default `--aex-url`) is persisted to a `0600` config file
|
|
100
|
-
(`$XDG_CONFIG_HOME/aex/config.json` or `~/.config/aex/config.json`; `%APPDATA%\aex\config.json`
|
|
101
|
-
on Windows). An explicit `--api-key` flag always overrides the stored one.
|
|
102
|
-
|
|
103
|
-
```bash
|
|
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
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
Discover the closed sets the platform accepts — no token, no network (human table
|
|
112
|
-
by default, machine JSON under `--json`). Per-verb `--help` is also key-free:
|
|
113
|
-
|
|
114
|
-
```bash
|
|
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)
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
Errors are typed and actionable. Every `openSession()` / `run()` config-validation
|
|
123
|
-
failure throws a `RunConfigValidationError` (`err.code === "RUN_CONFIG_INVALID"`)
|
|
124
|
-
you can `catch` by code; CLI failures print a JSON envelope carrying the HTTP
|
|
125
|
-
`status`, a one-line `remedy`, and the `runId` where known, and a wrong `--model`,
|
|
126
|
-
`--provider`, or `--runtime-size` gets a "did you mean?" suggestion.
|
|
127
|
-
|
|
128
|
-
## Continue with sessions
|
|
129
|
-
|
|
130
|
-
Use sessions for conversational flows. `run()` is the one-shot convenience; a
|
|
131
|
-
`SessionHandle` is the lower-level surface when you want multiple turns,
|
|
132
|
-
streaming, messages, events, outputs, or downloads.
|
|
133
|
-
|
|
134
|
-
```ts
|
|
135
|
-
const session = await aex.openSession(result.runId);
|
|
136
|
-
const next = await session.send("Turn this into a checklist.").done();
|
|
137
|
-
console.log(next.text);
|
|
138
|
-
|
|
139
|
-
const messages = await session.messages().list();
|
|
140
|
-
const outputs = await aex.sessions.outputs(session.id).list();
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
## Feature Areas
|
|
144
|
-
|
|
145
|
-
- **Agent runtime:** managed autonomous runs with filesystem read/edit, grep/glob/head/tail, open web fetch/search, background commands, code execution, git, and subagents.
|
|
146
|
-
- **Durable infrastructure:** run records, status, wait/cancel/delete, idempotency, typed events, output capture, downloads, timeouts, and runtime sizes.
|
|
147
|
-
- **Agent composition:** skills, files, AGENTS.md, remote MCP servers, environment variables, packages, and networking controls.
|
|
148
|
-
- **Subagents:** typed parent/child lineage for async child runs, output handoff, and bounded agent delegation.
|
|
149
|
-
- **Models and providers:** Anthropic, DeepSeek, OpenAI, Gemini, Mistral, OpenRouter, Doubao, and Doubao China behind one submission shape.
|
|
150
|
-
- **Typed control surface:** strongly typed SDK inputs, CLI parity, BYOK provider keys, workspace secrets, redaction, and output modes.
|
|
151
|
-
|
|
152
|
-
## Docs
|
|
153
|
-
|
|
154
|
-
- [Quickstart](docs/quickstart.md)
|
|
155
|
-
- [Run configuration](docs/run-config.md)
|
|
156
|
-
- [Composition](docs/concepts/composition.md)
|
|
157
|
-
- [Secrets](docs/secrets.md)
|
|
158
|
-
- [Limits](docs/limits.md)
|
|
159
|
-
- [Provider/runtime capabilities](docs/provider-runtime-capabilities.md)
|
|
1
|
+
---
|
|
2
|
+
title: aex
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# @aexhq/sdk
|
|
6
|
+
|
|
7
|
+
aex is an agent execution platform for launching autonomous agents from a simple TypeScript SDK and CLI.
|
|
8
|
+
|
|
9
|
+
The package ships:
|
|
10
|
+
|
|
11
|
+
- `Aex` for sessions, one-shot runs, inspect, download, cancel, and delete.
|
|
12
|
+
- `sessions` / `openSession()` for durable, resumable agent sessions.
|
|
13
|
+
- Typed run primitives: `Models`, `Providers`, `Sizes`, `Skill`, `Tool` / `Tools`, `AgentsMd`, `File`, `McpServer`, and `Secret`.
|
|
14
|
+
- A bundled `aex` CLI with the same run, status, events, outputs, download, cancel, delete, and whoami operations.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm i @aexhq/sdk
|
|
20
|
+
```
|
|
21
|
+
|
|
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.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
export AEX_API_KEY="<your-aex-api-key>"
|
|
30
|
+
export ANTHROPIC_API_KEY="<your-anthropic-api-key>"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## First Session
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { Aex, Models, Sizes } from "@aexhq/sdk";
|
|
37
|
+
|
|
38
|
+
const aex = new Aex(process.env.AEX_API_KEY!);
|
|
39
|
+
|
|
40
|
+
const session = await aex.openSession({
|
|
41
|
+
model: Models.CLAUDE_HAIKU_4_5,
|
|
42
|
+
system: "You are a concise engineering assistant.",
|
|
43
|
+
runtime: Sizes.SHARED_0_25X_1GB,
|
|
44
|
+
// Default is "3m"; set it explicitly when you want a different idle window.
|
|
45
|
+
overrides: { idleTtl: "3m" },
|
|
46
|
+
apiKeys: { anthropic: process.env.ANTHROPIC_API_KEY! }
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// done() awaits settle by default, so status is the terminal outcome
|
|
50
|
+
// ("succeeded"), and costUsd/usage are populated.
|
|
51
|
+
const first = await session.send("Summarize this repo.").done();
|
|
52
|
+
console.log(first.status, first.costUsd, first.text);
|
|
53
|
+
|
|
54
|
+
const resumed = await aex.openSession(session.id);
|
|
55
|
+
await resumed.send("Continue with the follow-up validation.").done();
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Need a one-shot convenience? `run()` opens a session, sends `message` as one
|
|
59
|
+
turn, and returns the collected result. The returned `runId` is the session id,
|
|
60
|
+
so it can be resumed later with `openSession(runId)`.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
const result = await aex.run({
|
|
64
|
+
model: Models.CLAUDE_HAIKU_4_5,
|
|
65
|
+
apiKeys: { anthropic: process.env.ANTHROPIC_API_KEY! },
|
|
66
|
+
message: "Write the report and save outputs."
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
console.log(result.runId, result.status, result.text);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
For multiple providers, include each BYOK key in `apiKeys`:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
await aex.run({
|
|
76
|
+
model: Models.CLAUDE_HAIKU_4_5,
|
|
77
|
+
apiKeys: {
|
|
78
|
+
anthropic: process.env.ANTHROPIC_API_KEY!,
|
|
79
|
+
openai: process.env.OPENAI_API_KEY!
|
|
80
|
+
},
|
|
81
|
+
message: "Delegate research to a subagent."
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The same request can run from the bundled CLI (`npx aex` on a local install):
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npx aex run \
|
|
89
|
+
--api-key "$AEX_API_KEY" \
|
|
90
|
+
--anthropic-api-key "$ANTHROPIC_API_KEY" \
|
|
91
|
+
--model claude-haiku-4-5 \
|
|
92
|
+
--prompt "Write the report and save outputs." \
|
|
93
|
+
--follow
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## CLI: login, discovery, typed errors
|
|
97
|
+
|
|
98
|
+
Stop re-passing `--api-key` on every command — log in once and the token (plus
|
|
99
|
+
your default `--aex-url`) is persisted to a `0600` config file
|
|
100
|
+
(`$XDG_CONFIG_HOME/aex/config.json` or `~/.config/aex/config.json`; `%APPDATA%\aex\config.json`
|
|
101
|
+
on Windows). An explicit `--api-key` flag always overrides the stored one.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
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
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Discover the closed sets the platform accepts — no token, no network (human table
|
|
112
|
+
by default, machine JSON under `--json`). Per-verb `--help` is also key-free:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
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)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Errors are typed and actionable. Every `openSession()` / `run()` config-validation
|
|
123
|
+
failure throws a `RunConfigValidationError` (`err.code === "RUN_CONFIG_INVALID"`)
|
|
124
|
+
you can `catch` by code; CLI failures print a JSON envelope carrying the HTTP
|
|
125
|
+
`status`, a one-line `remedy`, and the `runId` where known, and a wrong `--model`,
|
|
126
|
+
`--provider`, or `--runtime-size` gets a "did you mean?" suggestion.
|
|
127
|
+
|
|
128
|
+
## Continue with sessions
|
|
129
|
+
|
|
130
|
+
Use sessions for conversational flows. `run()` is the one-shot convenience; a
|
|
131
|
+
`SessionHandle` is the lower-level surface when you want multiple turns,
|
|
132
|
+
streaming, messages, events, outputs, or downloads.
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
const session = await aex.openSession(result.runId);
|
|
136
|
+
const next = await session.send("Turn this into a checklist.").done();
|
|
137
|
+
console.log(next.text);
|
|
138
|
+
|
|
139
|
+
const messages = await session.messages().list();
|
|
140
|
+
const outputs = await aex.sessions.outputs(session.id).list();
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Feature Areas
|
|
144
|
+
|
|
145
|
+
- **Agent runtime:** managed autonomous runs with filesystem read/edit, grep/glob/head/tail, open web fetch/search, background commands, code execution, git, and subagents.
|
|
146
|
+
- **Durable infrastructure:** run records, status, wait/cancel/delete, idempotency, typed events, output capture, downloads, timeouts, and runtime sizes.
|
|
147
|
+
- **Agent composition:** skills, files, AGENTS.md, remote MCP servers, environment variables, packages, and networking controls.
|
|
148
|
+
- **Subagents:** typed parent/child lineage for async child runs, output handoff, and bounded agent delegation.
|
|
149
|
+
- **Models and providers:** Anthropic, DeepSeek, OpenAI, Gemini, Mistral, OpenRouter, Doubao, and Doubao China behind one submission shape.
|
|
150
|
+
- **Typed control surface:** strongly typed SDK inputs, CLI parity, BYOK provider keys, workspace secrets, redaction, and output modes.
|
|
151
|
+
|
|
152
|
+
## Docs
|
|
153
|
+
|
|
154
|
+
- [Quickstart](docs/quickstart.md)
|
|
155
|
+
- [Run configuration](docs/run-config.md)
|
|
156
|
+
- [Composition](docs/concepts/composition.md)
|
|
157
|
+
- [Secrets](docs/secrets.md)
|
|
158
|
+
- [Limits](docs/limits.md)
|
|
159
|
+
- [Provider/runtime capabilities](docs/provider-runtime-capabilities.md)
|
package/dist/_contracts/http.js
CHANGED
package/dist/_contracts/index.js
CHANGED
|
@@ -1,3 +1,39 @@
|
|
|
1
1
|
export * from "./models.js";
|
|
2
2
|
export * from "./post-hook.js";
|
|
3
3
|
export * from "./submission.js";
|
|
4
|
+
/**
|
|
5
|
+
* Subset of `HttpClient` needed by the asset uploader. Defined structurally so
|
|
6
|
+
* SDK and CLI tests can supply thin stubs without dragging in the full client.
|
|
7
|
+
*/
|
|
8
|
+
export interface AssetsHttpClient {
|
|
9
|
+
request<T>(path: string, init?: RequestInit, query?: Record<string, string>): Promise<T>;
|
|
10
|
+
}
|
|
11
|
+
/** Minimal fetch shape for the direct-to-storage PUT. */
|
|
12
|
+
export type AssetFetch = (input: string, init?: RequestInit) => Promise<{
|
|
13
|
+
readonly ok: boolean;
|
|
14
|
+
readonly status: number;
|
|
15
|
+
text(): Promise<string>;
|
|
16
|
+
readonly headers?: {
|
|
17
|
+
get(name: string): string | null;
|
|
18
|
+
};
|
|
19
|
+
}>;
|
|
20
|
+
export interface UploadAssetArgs {
|
|
21
|
+
readonly http: AssetsHttpClient;
|
|
22
|
+
readonly bytes: Uint8Array;
|
|
23
|
+
/** `sha256:<hex>` - the canonical content hash declared on the wire. */
|
|
24
|
+
readonly hash: string;
|
|
25
|
+
readonly contentType?: string;
|
|
26
|
+
readonly fetch?: AssetFetch;
|
|
27
|
+
}
|
|
28
|
+
export interface UploadedAsset {
|
|
29
|
+
readonly assetId: string;
|
|
30
|
+
readonly contentHash: string;
|
|
31
|
+
readonly sizeBytes: number;
|
|
32
|
+
/** true if identical bytes were already present (dedup hit). */
|
|
33
|
+
readonly exists: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Upload `bytes` to the hosted API's content-addressable asset store via the
|
|
37
|
+
* direct-to-storage presign flow shared by SDK and CLI callers.
|
|
38
|
+
*/
|
|
39
|
+
export declare function uploadAsset(args: UploadAssetArgs): Promise<UploadedAsset>;
|
|
@@ -1,10 +1,159 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
import { extractErrorCode, redactUrl } from "./sdk-errors.js";
|
|
2
|
+
// Workspace-internal entry point. Re-exports submission building blocks and
|
|
3
|
+
// hosts small shared helpers so public packages can avoid hand-mirroring
|
|
4
|
+
// behavior. NOT part of the public `@aexhq/contracts` surface; do NOT add this
|
|
5
|
+
// to the package `index`. Consumers reach it via the explicit
|
|
6
6
|
// `@aexhq/contracts/internal` subpath.
|
|
7
7
|
export * from "./models.js";
|
|
8
8
|
export * from "./post-hook.js";
|
|
9
9
|
export * from "./submission.js";
|
|
10
|
-
|
|
10
|
+
const DIRECT_UPLOAD_MAX_ATTEMPTS = 3;
|
|
11
|
+
/**
|
|
12
|
+
* Upload `bytes` to the hosted API's content-addressable asset store via the
|
|
13
|
+
* direct-to-storage presign flow shared by SDK and CLI callers.
|
|
14
|
+
*/
|
|
15
|
+
export async function uploadAsset(args) {
|
|
16
|
+
const expected = args.hash.startsWith("sha256:") ? args.hash.slice("sha256:".length) : args.hash;
|
|
17
|
+
const actual = await computeSha256Hex(args.bytes);
|
|
18
|
+
if (actual !== expected) {
|
|
19
|
+
throw new Error(`uploadAsset: client-side hash mismatch: computed sha256:${actual} ` +
|
|
20
|
+
`but caller declared ${args.hash}. Aborting to avoid uploading corrupted data.`);
|
|
21
|
+
}
|
|
22
|
+
const contentHashHeader = `sha256:${actual}`;
|
|
23
|
+
const presign = await args.http.request("/assets/presign", {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: { "content-type": "application/json" },
|
|
26
|
+
body: JSON.stringify({ hash: contentHashHeader, sizeBytes: args.bytes.byteLength })
|
|
27
|
+
});
|
|
28
|
+
if (presign.exists) {
|
|
29
|
+
const contentHash = presign.contentHash ?? contentHashHeader;
|
|
30
|
+
return {
|
|
31
|
+
assetId: presign.assetId ?? assetIdFromContentHash(contentHash),
|
|
32
|
+
contentHash,
|
|
33
|
+
sizeBytes: presign.sizeBytes ?? args.bytes.byteLength,
|
|
34
|
+
exists: true
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (!presign.uploadUrl) {
|
|
38
|
+
throw new Error("uploadAsset: presign returned no uploadUrl and exists:false");
|
|
39
|
+
}
|
|
40
|
+
const doFetch = args.fetch ?? globalThis.fetch;
|
|
41
|
+
const putHeaders = {
|
|
42
|
+
"content-type": args.contentType ?? "application/zip",
|
|
43
|
+
...(presign.requiredHeaders ?? {})
|
|
44
|
+
};
|
|
45
|
+
await putWithRetry(doFetch, presign.uploadUrl, {
|
|
46
|
+
method: "PUT",
|
|
47
|
+
headers: putHeaders,
|
|
48
|
+
body: args.bytes
|
|
49
|
+
});
|
|
50
|
+
const fin = await args.http.request("/assets/finalize", {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: { "content-type": "application/json" },
|
|
53
|
+
body: JSON.stringify({ hash: contentHashHeader, sizeBytes: args.bytes.byteLength })
|
|
54
|
+
});
|
|
55
|
+
const contentHash = fin.contentHash ?? presign.contentHash ?? contentHashHeader;
|
|
56
|
+
return {
|
|
57
|
+
assetId: fin.assetId ?? presign.assetId ?? assetIdFromContentHash(contentHash),
|
|
58
|
+
contentHash,
|
|
59
|
+
sizeBytes: fin.sizeBytes ?? args.bytes.byteLength,
|
|
60
|
+
exists: false
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async function computeSha256Hex(bytes) {
|
|
64
|
+
const subtle = globalThis.crypto?.subtle;
|
|
65
|
+
if (!subtle) {
|
|
66
|
+
throw new Error("uploadAsset: globalThis.crypto.subtle is not available; " +
|
|
67
|
+
"Bun, Node 18+, or a Web-Crypto-capable runtime is required");
|
|
68
|
+
}
|
|
69
|
+
const digest = await subtle.digest("SHA-256", bytes);
|
|
70
|
+
return bufferToHex(digest);
|
|
71
|
+
}
|
|
72
|
+
function assetIdFromContentHash(contentHash) {
|
|
73
|
+
const hex = contentHash.startsWith("sha256:") ? contentHash.slice("sha256:".length) : contentHash;
|
|
74
|
+
return `asset_${hex}`;
|
|
75
|
+
}
|
|
76
|
+
async function putWithRetry(fetchImpl, uploadUrl, init) {
|
|
77
|
+
for (let attempt = 1; attempt <= DIRECT_UPLOAD_MAX_ATTEMPTS; attempt++) {
|
|
78
|
+
let response;
|
|
79
|
+
try {
|
|
80
|
+
response = await fetchImpl(uploadUrl, init);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
if (attempt < DIRECT_UPLOAD_MAX_ATTEMPTS && isRetryableUploadError(err)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
throw directUploadNetworkError(uploadUrl, err, attempt);
|
|
87
|
+
}
|
|
88
|
+
if (response.ok)
|
|
89
|
+
return;
|
|
90
|
+
if (attempt < DIRECT_UPLOAD_MAX_ATTEMPTS && isRetryableUploadStatus(response.status)) {
|
|
91
|
+
await response.text().catch(() => "");
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const detail = await response.text().catch(() => "");
|
|
95
|
+
throw directUploadResponseError(uploadUrl, response.status, detail, attempt);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function isRetryableUploadStatus(status) {
|
|
99
|
+
return status === 408 || status === 425 || status === 429 || (status >= 500 && status <= 599);
|
|
100
|
+
}
|
|
101
|
+
function isRetryableUploadError(err) {
|
|
102
|
+
if (isNamedError(err, "AbortError"))
|
|
103
|
+
return false;
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
function directUploadNetworkError(uploadUrl, err, attempts) {
|
|
107
|
+
const safeUrl = redactUrl(uploadUrl);
|
|
108
|
+
const code = extractErrorCode(err);
|
|
109
|
+
const detail = sanitizeUploadText(errorMessage(err)).slice(0, 500);
|
|
110
|
+
return new Error(`uploadAsset: direct upload PUT failed for ${safeUrl} after ${attemptsLabel(attempts)}` +
|
|
111
|
+
(code ? ` (${code})` : "") +
|
|
112
|
+
(detail ? `: ${detail}` : ""));
|
|
113
|
+
}
|
|
114
|
+
function directUploadResponseError(uploadUrl, status, detail, attempts) {
|
|
115
|
+
const safeUrl = redactUrl(uploadUrl);
|
|
116
|
+
const safeDetail = sanitizeUploadText(detail).slice(0, 500);
|
|
117
|
+
return new Error(`uploadAsset: direct upload PUT failed for ${safeUrl} with status ${status}` +
|
|
118
|
+
(attempts > 1 ? ` after ${attemptsLabel(attempts)}` : "") +
|
|
119
|
+
(safeDetail ? `: ${safeDetail}` : ""));
|
|
120
|
+
}
|
|
121
|
+
function attemptsLabel(attempts) {
|
|
122
|
+
return attempts === 1 ? "1 attempt" : `${attempts} attempts`;
|
|
123
|
+
}
|
|
124
|
+
function errorMessage(err) {
|
|
125
|
+
if (err instanceof Error)
|
|
126
|
+
return err.message || err.name;
|
|
127
|
+
if (typeof err === "string")
|
|
128
|
+
return err;
|
|
129
|
+
return String(err);
|
|
130
|
+
}
|
|
131
|
+
function isNamedError(err, name) {
|
|
132
|
+
return stringProperty(err, "name") === name;
|
|
133
|
+
}
|
|
134
|
+
function stringProperty(value, key) {
|
|
135
|
+
if (!value || typeof value !== "object")
|
|
136
|
+
return undefined;
|
|
137
|
+
const prop = value[key];
|
|
138
|
+
return typeof prop === "string" && prop.length > 0 ? prop : undefined;
|
|
139
|
+
}
|
|
140
|
+
function sanitizeUploadText(text) {
|
|
141
|
+
return text
|
|
142
|
+
.replace(/https?:\/\/[^\s<>"'`]+/g, (raw) => redactUrlPreservingTrailingPunctuation(raw))
|
|
143
|
+
.replace(/\b(?:X-Amz-(?:Algorithm|Credential|Date|Expires|Security-Token|Signature|SignedHeaders)|AWSAccessKeyId|Signature|Credential|Security-Token|AccessKeyId|SecretAccessKey|SessionToken)=([^&\s<>"'`]+)/gi, "[redacted]")
|
|
144
|
+
.replace(/\bAKIA[0-9A-Z]{8,}\b/g, "[redacted]");
|
|
145
|
+
}
|
|
146
|
+
function redactUrlPreservingTrailingPunctuation(raw) {
|
|
147
|
+
const trailing = raw.match(/[),.;:!?]+$/)?.[0] ?? "";
|
|
148
|
+
const candidate = trailing ? raw.slice(0, -trailing.length) : raw;
|
|
149
|
+
return `${redactUrl(candidate)}${trailing}`;
|
|
150
|
+
}
|
|
151
|
+
function bufferToHex(buffer) {
|
|
152
|
+
const view = new Uint8Array(buffer);
|
|
153
|
+
let out = "";
|
|
154
|
+
for (let i = 0; i < view.length; i++) {
|
|
155
|
+
const byte = view[i];
|
|
156
|
+
out += byte.toString(16).padStart(2, "0");
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
@@ -445,7 +445,7 @@ async function readCappedText(response, maxBytes) {
|
|
|
445
445
|
await reader.cancel().catch(() => { });
|
|
446
446
|
}
|
|
447
447
|
const merged = concatBytes(chunks).subarray(0, maxBytes);
|
|
448
|
-
const truncated = declared !== undefined ? declared > maxBytes : sawMore;
|
|
448
|
+
const truncated = declared !== undefined ? declared > maxBytes : read > maxBytes || sawMore;
|
|
449
449
|
const totalBytes = declared ?? read;
|
|
450
450
|
return { text: decoder.decode(merged), truncated, totalBytes };
|
|
451
451
|
}
|
|
@@ -1082,4 +1082,3 @@ export async function uploadWorkspaceAsset(http, input) {
|
|
|
1082
1082
|
body: input.bytes
|
|
1083
1083
|
});
|
|
1084
1084
|
}
|
|
1085
|
-
//# sourceMappingURL=operations.js.map
|