open-managed-agents 0.1.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.
- package/LICENSE +93 -0
- package/README.md +248 -0
- package/bin/oma.mjs +499 -0
- package/dist/scripts/alpha-openai-compatible-fixture.mjs +134 -0
- package/dist/scripts/alpha-smoke-models.mjs +92 -0
- package/dist/scripts/alpha-smoke.mjs +581 -0
- package/dist/scripts/oma-doctor.js +230 -0
- package/dist/scripts/oma-models.js +299 -0
- package/dist/scripts/oma-workspaces.js +133 -0
- package/dist/src/control-plane/admin/auth.js +43 -0
- package/dist/src/control-plane/admin/routes.js +76 -0
- package/dist/src/control-plane/admin/service.js +102 -0
- package/dist/src/control-plane/admission.js +90 -0
- package/dist/src/control-plane/agents/routes.js +63 -0
- package/dist/src/control-plane/agents/service.js +687 -0
- package/dist/src/control-plane/agents/store.js +391 -0
- package/dist/src/control-plane/agents/types.js +1 -0
- package/dist/src/control-plane/api-constants.js +3 -0
- package/dist/src/control-plane/app.js +907 -0
- package/dist/src/control-plane/console/auth.js +68 -0
- package/dist/src/control-plane/console/static.js +112 -0
- package/dist/src/control-plane/deployment-runtime-config.js +328 -0
- package/dist/src/control-plane/deployment-runtime-event-coordinator.js +25 -0
- package/dist/src/control-plane/deployment-runtime-turn-guard.js +14 -0
- package/dist/src/control-plane/deployment-session-coordinator.js +25 -0
- package/dist/src/control-plane/deployment-session-output-coordinator.js +27 -0
- package/dist/src/control-plane/deployment-storage.js +354 -0
- package/dist/src/control-plane/egress/image.js +48 -0
- package/dist/src/control-plane/egress/policy.js +571 -0
- package/dist/src/control-plane/egress/presets.js +77 -0
- package/dist/src/control-plane/egress/proxy.js +72 -0
- package/dist/src/control-plane/egress/ssrf.js +106 -0
- package/dist/src/control-plane/egress/utils/debug.js +14 -0
- package/dist/src/control-plane/egress/vendor/http-proxy.js +335 -0
- package/dist/src/control-plane/egress/vendor/mitm-ca.js +226 -0
- package/dist/src/control-plane/egress/vendor/mitm-leaf.js +124 -0
- package/dist/src/control-plane/egress/vendor/parent-proxy.js +438 -0
- package/dist/src/control-plane/egress/vendor/request-filter.js +105 -0
- package/dist/src/control-plane/egress/vendor/sandbox-config.js +1 -0
- package/dist/src/control-plane/egress/vendor/tls-terminate-proxy.js +269 -0
- package/dist/src/control-plane/environments/routes.js +54 -0
- package/dist/src/control-plane/environments/service.js +112 -0
- package/dist/src/control-plane/environments/store.js +132 -0
- package/dist/src/control-plane/environments/types.js +1 -0
- package/dist/src/control-plane/errors.js +81 -0
- package/dist/src/control-plane/events/broadcaster.js +217 -0
- package/dist/src/control-plane/events/constants.js +2 -0
- package/dist/src/control-plane/events/persist.js +39 -0
- package/dist/src/control-plane/events/request.js +190 -0
- package/dist/src/control-plane/events/routes.js +159 -0
- package/dist/src/control-plane/events/runtime-helpers.js +134 -0
- package/dist/src/control-plane/events/service.js +1985 -0
- package/dist/src/control-plane/events/session-guards.js +29 -0
- package/dist/src/control-plane/events/sse.js +6 -0
- package/dist/src/control-plane/events/store.js +772 -0
- package/dist/src/control-plane/events/tool-persistence.js +102 -0
- package/dist/src/control-plane/events/types.js +21 -0
- package/dist/src/control-plane/files/routes.js +121 -0
- package/dist/src/control-plane/files/service.js +65 -0
- package/dist/src/control-plane/files/store-common.js +87 -0
- package/dist/src/control-plane/files/store-local.js +488 -0
- package/dist/src/control-plane/files/store-memory.js +270 -0
- package/dist/src/control-plane/files/store.js +2 -0
- package/dist/src/control-plane/files/types.js +7 -0
- package/dist/src/control-plane/http.js +25 -0
- package/dist/src/control-plane/ids.js +43 -0
- package/dist/src/control-plane/logging.js +263 -0
- package/dist/src/control-plane/models/auth-storage-backend.js +238 -0
- package/dist/src/control-plane/models/catalog.js +78 -0
- package/dist/src/control-plane/models/config-security.js +376 -0
- package/dist/src/control-plane/models/deployment-config.js +93 -0
- package/dist/src/control-plane/models/routes.js +32 -0
- package/dist/src/control-plane/models/service.js +138 -0
- package/dist/src/control-plane/observability/instruments.js +65 -0
- package/dist/src/control-plane/observability/metrics.js +125 -0
- package/dist/src/control-plane/observability/routes.js +95 -0
- package/dist/src/control-plane/openapi/document.js +282 -0
- package/dist/src/control-plane/openapi/routes.js +78 -0
- package/dist/src/control-plane/request-idempotency.js +85 -0
- package/dist/src/control-plane/secrets/envelope.js +132 -0
- package/dist/src/control-plane/secrets/master-key.js +44 -0
- package/dist/src/control-plane/secrets/routes.js +20 -0
- package/dist/src/control-plane/secrets/service.js +71 -0
- package/dist/src/control-plane/secrets/store.js +212 -0
- package/dist/src/control-plane/secrets/types.js +3 -0
- package/dist/src/control-plane/sessions/pi/custom-tools.js +146 -0
- package/dist/src/control-plane/sessions/pi/mcp/bridge.js +527 -0
- package/dist/src/control-plane/sessions/pi/mcp/client.js +157 -0
- package/dist/src/control-plane/sessions/pi/mcp/credential.js +28 -0
- package/dist/src/control-plane/sessions/pi/mcp/fetch.js +69 -0
- package/dist/src/control-plane/sessions/pi/mcp/probe.js +202 -0
- package/dist/src/control-plane/sessions/pi/mcp/runtime.js +13 -0
- package/dist/src/control-plane/sessions/pi/runner.js +1104 -0
- package/dist/src/control-plane/sessions/pi/sandbox/cma-glob.js +388 -0
- package/dist/src/control-plane/sessions/pi/sandbox/cma-grep.js +124 -0
- package/dist/src/control-plane/sessions/pi/sandbox/docker-egress.js +347 -0
- package/dist/src/control-plane/sessions/pi/sandbox/docker.js +1589 -0
- package/dist/src/control-plane/sessions/pi/sandbox/glob.js +48 -0
- package/dist/src/control-plane/sessions/pi/sandbox/image.js +7 -0
- package/dist/src/control-plane/sessions/pi/sandbox/microsandbox.js +1677 -0
- package/dist/src/control-plane/sessions/pi/sandbox/provider.js +561 -0
- package/dist/src/control-plane/sessions/pi/sandbox/selection.js +168 -0
- package/dist/src/control-plane/sessions/pi/span-normalizer.js +82 -0
- package/dist/src/control-plane/sessions/pi/tool-permissions.js +392 -0
- package/dist/src/control-plane/sessions/pi/translator.js +100 -0
- package/dist/src/control-plane/sessions/request.js +164 -0
- package/dist/src/control-plane/sessions/resources.js +63 -0
- package/dist/src/control-plane/sessions/routes.js +88 -0
- package/dist/src/control-plane/sessions/serialize.js +24 -0
- package/dist/src/control-plane/sessions/service.js +733 -0
- package/dist/src/control-plane/sessions/store.js +538 -0
- package/dist/src/control-plane/sessions/types.js +1 -0
- package/dist/src/control-plane/skills/archive.js +169 -0
- package/dist/src/control-plane/skills/routes.js +41 -0
- package/dist/src/control-plane/skills/service.js +29 -0
- package/dist/src/control-plane/skills/store.js +233 -0
- package/dist/src/control-plane/skills/types.js +5 -0
- package/dist/src/control-plane/sqlite-transaction.js +15 -0
- package/dist/src/control-plane/vaults/mcp-oauth-validate.js +165 -0
- package/dist/src/control-plane/vaults/oauth-refresh-ticker.js +43 -0
- package/dist/src/control-plane/vaults/oauth-refresh.js +448 -0
- package/dist/src/control-plane/vaults/routes.js +66 -0
- package/dist/src/control-plane/vaults/service.js +553 -0
- package/dist/src/control-plane/vaults/store.js +691 -0
- package/dist/src/control-plane/vaults/types.js +1 -0
- package/dist/src/control-plane/wake-loop.js +65 -0
- package/dist/src/control-plane/wiring.js +92 -0
- package/dist/src/control-plane/workspace.js +4 -0
- package/dist/src/control-plane/workspaces/store.js +158 -0
- package/dist/src/egress-proxy-main.js +103 -0
- package/dist/src/main.js +124 -0
- package/dist/src/types/agents.js +1 -0
- package/dist/src/types/common.js +1 -0
- package/dist/src/types/environments.js +1 -0
- package/dist/src/types/events.js +121 -0
- package/dist/src/types/files.js +1 -0
- package/dist/src/types/json.js +19 -0
- package/dist/src/types/sessions.js +1 -0
- package/dist/ui/managed-agents-console/README.md +52 -0
- package/dist/ui/managed-agents-console/docs/agents.md +23 -0
- package/dist/ui/managed-agents-console/docs/console.md +23 -0
- package/dist/ui/managed-agents-console/docs/dreams.md +5 -0
- package/dist/ui/managed-agents-console/docs/environments.md +27 -0
- package/dist/ui/managed-agents-console/docs/events.md +19 -0
- package/dist/ui/managed-agents-console/docs/files.md +17 -0
- package/dist/ui/managed-agents-console/docs/github.md +11 -0
- package/dist/ui/managed-agents-console/docs/integrations.md +19 -0
- package/dist/ui/managed-agents-console/docs/memory.md +11 -0
- package/dist/ui/managed-agents-console/docs/migration.md +17 -0
- package/dist/ui/managed-agents-console/docs/multiagent.md +11 -0
- package/dist/ui/managed-agents-console/docs/outcomes.md +11 -0
- package/dist/ui/managed-agents-console/docs/overview.md +41 -0
- package/dist/ui/managed-agents-console/docs/permissions.md +24 -0
- package/dist/ui/managed-agents-console/docs/quickstart.md +51 -0
- package/dist/ui/managed-agents-console/docs/reference.md +27 -0
- package/dist/ui/managed-agents-console/docs/sandbox-reference.md +19 -0
- package/dist/ui/managed-agents-console/docs/sandbox-security.md +15 -0
- package/dist/ui/managed-agents-console/docs/scheduled-deployments.md +5 -0
- package/dist/ui/managed-agents-console/docs/self-hosted-sandboxes.md +15 -0
- package/dist/ui/managed-agents-console/docs/session-operations.md +21 -0
- package/dist/ui/managed-agents-console/docs/sessions.md +21 -0
- package/dist/ui/managed-agents-console/docs/skills.md +15 -0
- package/dist/ui/managed-agents-console/docs/tools.md +23 -0
- package/dist/ui/managed-agents-console/docs/vaults.md +19 -0
- package/dist/ui/managed-agents-console/docs/webhooks.md +11 -0
- package/dist/ui/managed-agents-console/index.html +47 -0
- package/dist/ui/managed-agents-console/serve.mjs +116 -0
- package/dist/ui/managed-agents-console/src/agents-files.jsx +273 -0
- package/dist/ui/managed-agents-console/src/api.js +1043 -0
- package/dist/ui/managed-agents-console/src/app.jsx +633 -0
- package/dist/ui/managed-agents-console/src/auth.jsx +215 -0
- package/dist/ui/managed-agents-console/src/console.css +733 -0
- package/dist/ui/managed-agents-console/src/data.js +188 -0
- package/dist/ui/managed-agents-console/src/detail.jsx +719 -0
- package/dist/ui/managed-agents-console/src/docs.jsx +200 -0
- package/dist/ui/managed-agents-console/src/environments.jsx +365 -0
- package/dist/ui/managed-agents-console/src/forms.jsx +494 -0
- package/dist/ui/managed-agents-console/src/icons.jsx +48 -0
- package/dist/ui/managed-agents-console/src/skills.jsx +21 -0
- package/dist/ui/managed-agents-console/src/sse.js +185 -0
- package/dist/ui/managed-agents-console/src/states.jsx +90 -0
- package/dist/ui/managed-agents-console/src/tweaks-panel.jsx +541 -0
- package/dist/ui/managed-agents-console/src/ui.jsx +190 -0
- package/dist/ui/managed-agents-console/src/vaults-data.js +89 -0
- package/dist/ui/managed-agents-console/src/vaults.jsx +171 -0
- package/dist/ui/managed-agents-console/vendor/babel.min.js +4 -0
- package/dist/ui/managed-agents-console/vendor/react-dom.production.min.js +267 -0
- package/dist/ui/managed-agents-console/vendor/react.production.min.js +31 -0
- package/dist/ui/openapi-docs/VENDOR.md +27 -0
- package/dist/ui/openapi-docs/index.html +16 -0
- package/dist/ui/openapi-docs/swagger-initializer.js +14 -0
- package/dist/ui/openapi-docs/vendor/LICENSE +202 -0
- package/dist/ui/openapi-docs/vendor/NOTICE +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui-bundle.js +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui-standalone-preset.js +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui.css +3 -0
- package/package.json +53 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process TLS termination for HTTPS traffic through the forward proxy.
|
|
3
|
+
*
|
|
4
|
+
* When a MitmCA is configured, the forward proxy hands CONNECT requests here
|
|
5
|
+
* instead of opening an opaque byte tunnel. We terminate the client's TLS
|
|
6
|
+
* with a per-host leaf cert (see mitm-leaf.ts), parse the decrypted stream
|
|
7
|
+
* as HTTP/1.1, and re-issue each request upstream over a real TLS
|
|
8
|
+
* connection. The optional `filterRequest` callback runs on each parsed
|
|
9
|
+
* request before it is forwarded.
|
|
10
|
+
*/
|
|
11
|
+
import { createServer as createHttpsServer, request as httpsRequest, } from 'node:https';
|
|
12
|
+
import { connect, isIP } from 'node:net';
|
|
13
|
+
import { unlink } from 'node:fs';
|
|
14
|
+
import { tmpdir } from 'node:os';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { logForDebugging } from "../utils/debug.js";
|
|
17
|
+
import { decideAndRespond, } from "./request-filter.js";
|
|
18
|
+
import { mintLeafCert, secureContextFor } from "./mitm-leaf.js";
|
|
19
|
+
import { stripHopByHop } from "./parent-proxy.js";
|
|
20
|
+
/**
|
|
21
|
+
* True if `buf` starts with a TLS Handshake record header.
|
|
22
|
+
*
|
|
23
|
+
* Three bytes: content type 0x16 (Handshake) + legacy_record_version
|
|
24
|
+
* 0x03,0x00–0x03. RFC 8446 §5.1 froze the record-layer version (TLS 1.3+
|
|
25
|
+
* negotiate via the supported_versions extension, the wire header stays
|
|
26
|
+
* ≤0x0303), so this holds for current and future TLS. Same predicate as
|
|
27
|
+
* mitmproxy `starts_like_tls_record`; nginx `ssl_preread` routes on byte 0
|
|
28
|
+
* alone and HAProxy `req.ssl_hello_type` reads 9 bytes to also extract the
|
|
29
|
+
* handshake type — 3 is the established middle ground for "is this TLS".
|
|
30
|
+
*
|
|
31
|
+
* Routing heuristic, not a security check: a non-TLS stream that happens to
|
|
32
|
+
* start 16 03 0x is handed to the TLS server, which then rejects it properly.
|
|
33
|
+
*/
|
|
34
|
+
export function looksLikeClientHello(buf) {
|
|
35
|
+
return (buf.length >= 3 && buf[0] === 0x16 && buf[1] === 0x03 && buf[2] <= 0x03);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Wait for the client's first post-CONNECT bytes and report whether they look
|
|
39
|
+
* like a TLS ClientHello. The caller must already have written the
|
|
40
|
+
* `200 Connection Established` line — clients don't send until they see it.
|
|
41
|
+
*
|
|
42
|
+
* Any bytes consumed here are returned in `.head` so the caller can forward
|
|
43
|
+
* them to whichever downstream (terminate or opaque tunnel) it picks. The
|
|
44
|
+
* socket is left paused so further bytes buffer until the downstream
|
|
45
|
+
* `pipe()` resumes it.
|
|
46
|
+
*/
|
|
47
|
+
export function peekForClientHello(socket, head) {
|
|
48
|
+
if (head.length >= 3) {
|
|
49
|
+
return Promise.resolve({ isTLS: looksLikeClientHello(head), head });
|
|
50
|
+
}
|
|
51
|
+
return new Promise(resolve => {
|
|
52
|
+
let buf = head;
|
|
53
|
+
const done = () => {
|
|
54
|
+
socket.removeListener('data', onData);
|
|
55
|
+
socket.removeListener('close', done);
|
|
56
|
+
resolve({ isTLS: looksLikeClientHello(buf), head: buf });
|
|
57
|
+
};
|
|
58
|
+
const onData = (chunk) => {
|
|
59
|
+
// Pause synchronously so anything after this chunk buffers for the
|
|
60
|
+
// downstream pipe() rather than being dropped in flowing mode.
|
|
61
|
+
socket.pause();
|
|
62
|
+
buf = buf.length ? Buffer.concat([buf, chunk]) : chunk;
|
|
63
|
+
if (buf.length >= 3)
|
|
64
|
+
return done();
|
|
65
|
+
socket.resume();
|
|
66
|
+
};
|
|
67
|
+
socket.on('data', onData);
|
|
68
|
+
socket.once('close', done);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Terminate the client's TLS on `socket`, parse the decrypted HTTP/1.1
|
|
73
|
+
* stream, and forward each request to `target` over a fresh upstream TLS
|
|
74
|
+
* connection.
|
|
75
|
+
*
|
|
76
|
+
* Preconditions: the caller has already validated `target` against the
|
|
77
|
+
* domain allowlist; this function does not re-check it.
|
|
78
|
+
*
|
|
79
|
+
* Implementation: we stand up a short-lived https.Server on a unix socket
|
|
80
|
+
* and pipe the client socket through it. The Node-idiomatic alternative —
|
|
81
|
+
* feeding the raw socket to a non-listening server via
|
|
82
|
+
* `emit('connection', socket)` — is not implemented by Bun's https.Server,
|
|
83
|
+
* and SRT runs under both runtimes. A per-connection server lets the
|
|
84
|
+
* request handler close over `target` (which carries the originally-
|
|
85
|
+
* requested host:port) without socket-keyed lookups.
|
|
86
|
+
*/
|
|
87
|
+
export function terminateAndForward(ca, filterRequest, mutateHeaders, socket, head, target) {
|
|
88
|
+
// ALPN advertises HTTP/1.1 only — terminating HTTP/2 would require a frame
|
|
89
|
+
// parser; clients negotiate down. The base secureContext covers clients
|
|
90
|
+
// that don't send SNI; SNICallback covers everyone else.
|
|
91
|
+
const baseLeaf = mintLeafCert(ca, target.hostname);
|
|
92
|
+
const inner = createHttpsServer({
|
|
93
|
+
ALPNProtocols: ['http/1.1'],
|
|
94
|
+
cert: baseLeaf.certPem,
|
|
95
|
+
key: baseLeaf.keyPem,
|
|
96
|
+
SNICallback: (servername, cb) => {
|
|
97
|
+
try {
|
|
98
|
+
cb(null, secureContextFor(ca, servername || target.hostname));
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
cb(err);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
inner.on('request', (req, res) => {
|
|
106
|
+
// OMA: fail closed — an unexpected throw inside the forward path (e.g. a
|
|
107
|
+
// hook) must kill this exchange, not become an unhandled rejection.
|
|
108
|
+
forwardUpstream(filterRequest, mutateHeaders, req, res, target).catch((err) => {
|
|
109
|
+
logForDebugging(`[tls-terminate] forward failed for ${target.hostname}: ${err.message}`, { level: 'error' });
|
|
110
|
+
res.destroy();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
inner.on('tlsClientError', (err, sock) => {
|
|
114
|
+
logForDebugging(`[tls-terminate] client TLS error for ${target.hostname}: ${err.message}`, { level: 'error' });
|
|
115
|
+
sock.destroy();
|
|
116
|
+
});
|
|
117
|
+
inner.on('upgrade', (_req, sock) => {
|
|
118
|
+
// WebSocket / non-HTTP over TLS — out of scope for now.
|
|
119
|
+
logForDebugging('[tls-terminate] upgrade request refused', {
|
|
120
|
+
level: 'warn',
|
|
121
|
+
});
|
|
122
|
+
sock.destroy();
|
|
123
|
+
});
|
|
124
|
+
const sockPath = innerSocketPath();
|
|
125
|
+
const cleanup = () => {
|
|
126
|
+
inner.close();
|
|
127
|
+
unlink(sockPath, () => { });
|
|
128
|
+
};
|
|
129
|
+
inner.on('error', err => {
|
|
130
|
+
logForDebugging(`[tls-terminate] inner server listen failed: ${err.message}`, { level: 'error' });
|
|
131
|
+
socket.destroy();
|
|
132
|
+
cleanup();
|
|
133
|
+
});
|
|
134
|
+
inner.listen(sockPath, () => {
|
|
135
|
+
const loop = connect({ path: sockPath });
|
|
136
|
+
loop.on('error', err => {
|
|
137
|
+
logForDebugging(`[tls-terminate] inner loopback failed: ${err.message}`, {
|
|
138
|
+
level: 'error',
|
|
139
|
+
});
|
|
140
|
+
socket.destroy();
|
|
141
|
+
cleanup();
|
|
142
|
+
});
|
|
143
|
+
loop.once('connect', () => {
|
|
144
|
+
// The caller wrote `200 Connection Established` before sniffing for the
|
|
145
|
+
// ClientHello; `head` holds whatever the sniff consumed.
|
|
146
|
+
if (head.length)
|
|
147
|
+
loop.write(head);
|
|
148
|
+
socket.pipe(loop);
|
|
149
|
+
loop.pipe(socket);
|
|
150
|
+
});
|
|
151
|
+
socket.on('error', () => loop.destroy());
|
|
152
|
+
socket.once('close', () => {
|
|
153
|
+
loop.destroy();
|
|
154
|
+
cleanup();
|
|
155
|
+
});
|
|
156
|
+
loop.once('close', () => socket.destroy());
|
|
157
|
+
});
|
|
158
|
+
inner.unref();
|
|
159
|
+
}
|
|
160
|
+
async function forwardUpstream(filterRequest, mutateHeaders, req, res, target) {
|
|
161
|
+
// req.url is the request-target verbatim. Inside a CONNECT tunnel almost
|
|
162
|
+
// every client sends origin-form (`/path?q`), but RFC 7230 §5.3.2 also
|
|
163
|
+
// permits absolute-form (`https://host/path`) and servers MUST accept it.
|
|
164
|
+
// Normalize to origin-form so concatenating onto `https://${host}` below
|
|
165
|
+
// yields a well-formed URL, and discard any client-supplied authority so
|
|
166
|
+
// the CONNECT-verified target stays authoritative (same rationale as the
|
|
167
|
+
// Host-header note below).
|
|
168
|
+
const path = originFormPath(req.url);
|
|
169
|
+
let body = req;
|
|
170
|
+
if (filterRequest) {
|
|
171
|
+
const ac = new AbortController();
|
|
172
|
+
res.once('close', () => ac.abort());
|
|
173
|
+
// Build the URL passed to filterRequest from the CONNECT target,
|
|
174
|
+
// NOT from `req.headers.host`. The Host header is supplied by the
|
|
175
|
+
// sandboxed client and can be spoofed: a sandboxed process can
|
|
176
|
+
// CONNECT to allowlisted host A and then send a decrypted request
|
|
177
|
+
// with `Host: B` (where B is some other allowlisted host). If we
|
|
178
|
+
// built the filterRequest URL from req.headers.host the callback
|
|
179
|
+
// would see "host=B" while the request is actually delivered to A.
|
|
180
|
+
// A consumer using filterRequest for per-host method gating (e.g.
|
|
181
|
+
// "POST allowed only to inference endpoints") would be bypassed —
|
|
182
|
+
// the agent could spoof Host: api.example.com on a CONNECT to a
|
|
183
|
+
// different allowlisted host, get the POST allowed, and have it
|
|
184
|
+
// delivered to the CONNECT target instead.
|
|
185
|
+
//
|
|
186
|
+
// Always derive the URL from the verified CONNECT target so
|
|
187
|
+
// filterRequest sees the actual upstream destination.
|
|
188
|
+
const host = target.port === 443
|
|
189
|
+
? target.hostname
|
|
190
|
+
: `${target.hostname}:${target.port}`;
|
|
191
|
+
const out = await decideAndRespond(filterRequest, req, res, `https://${host}${path}`, ac.signal, { leg: 'terminated' });
|
|
192
|
+
if (out === null)
|
|
193
|
+
return;
|
|
194
|
+
body = out;
|
|
195
|
+
}
|
|
196
|
+
// Bun's https.request verifies the upstream cert against headers.host
|
|
197
|
+
// verbatim (including ":port"), which never matches a SAN. Drop the host
|
|
198
|
+
// header and let the runtime derive it from {host, port} — same wire value,
|
|
199
|
+
// correct verification under both Node and Bun.
|
|
200
|
+
const fwdHeaders = stripHopByHop(req.headers);
|
|
201
|
+
delete fwdHeaders.host;
|
|
202
|
+
// Header mutation runs after the allow decision and before httpsRequest.
|
|
203
|
+
// The upstream TLS handshake (rejectUnauthorized defaults to true)
|
|
204
|
+
// completes before any HTTP bytes are written, so mutated headers never
|
|
205
|
+
// reach an unverified server.
|
|
206
|
+
// OMA: pass request context so injection can be path/method-scoped
|
|
207
|
+
// (ADR 0016 §6). method/path come from the decrypted request; port from the
|
|
208
|
+
// CONNECT-verified target (never the spoofable Host header).
|
|
209
|
+
mutateHeaders?.(fwdHeaders, target.hostname, {
|
|
210
|
+
method: req.method ?? 'GET',
|
|
211
|
+
path,
|
|
212
|
+
port: target.port,
|
|
213
|
+
});
|
|
214
|
+
// TODO(terminating-tls): honour parentProxy for the upstream leg.
|
|
215
|
+
const upstream = httpsRequest({
|
|
216
|
+
host: target.hostname,
|
|
217
|
+
port: target.port,
|
|
218
|
+
path,
|
|
219
|
+
method: req.method,
|
|
220
|
+
headers: fwdHeaders,
|
|
221
|
+
// We're a TLS-terminating proxy, not a trust boundary for the upstream
|
|
222
|
+
// server's identity — let the runtime do normal verification against
|
|
223
|
+
// system roots (and NODE_EXTRA_CA_CERTS). servername must match the
|
|
224
|
+
// host the client intended; SNI cannot carry an IP literal, and Bun's
|
|
225
|
+
// https.request treats `servername: undefined` differently from
|
|
226
|
+
// omitting the key, so spread conditionally.
|
|
227
|
+
...(isIP(target.hostname) ? {} : { servername: target.hostname }),
|
|
228
|
+
...(target.upstreamCA ? { ca: target.upstreamCA } : {}),
|
|
229
|
+
...(target.lookup ? { lookup: target.lookup } : {}), // OMA: connect-to-pinned-IP
|
|
230
|
+
// No global agent: a proxy's outbound leg shouldn't share a connection
|
|
231
|
+
// pool keyed on the proxy process. Also works around a Bun quirk where
|
|
232
|
+
// the first request's `ca:` value is cached on the global agent and
|
|
233
|
+
// subsequent calls with a different `ca:` are silently ignored.
|
|
234
|
+
agent: false,
|
|
235
|
+
}, upRes => {
|
|
236
|
+
res.writeHead(upRes.statusCode ?? 502, stripHopByHop(upRes.headers));
|
|
237
|
+
upRes.pipe(res);
|
|
238
|
+
});
|
|
239
|
+
upstream.on('error', err => {
|
|
240
|
+
logForDebugging(`[tls-terminate] upstream ${target.hostname}:${target.port} failed: ${err.message}`, { level: 'error' });
|
|
241
|
+
if (!res.headersSent) {
|
|
242
|
+
res.writeHead(502, { 'Content-Type': 'text/plain' });
|
|
243
|
+
res.end('Bad Gateway');
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
res.destroy();
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
res.on('close', () => upstream.destroy());
|
|
250
|
+
body.pipe(upstream);
|
|
251
|
+
}
|
|
252
|
+
function originFormPath(reqUrl) {
|
|
253
|
+
const raw = reqUrl ?? '/';
|
|
254
|
+
if (raw.startsWith('/'))
|
|
255
|
+
return raw;
|
|
256
|
+
try {
|
|
257
|
+
const u = new URL(raw);
|
|
258
|
+
return `${u.pathname}${u.search}` || '/';
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
// asterisk-form (`OPTIONS *`) or anything else non-absolute — pass through.
|
|
262
|
+
return raw;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
let sockSeq = 0;
|
|
266
|
+
function innerSocketPath() {
|
|
267
|
+
// Keep it short — macOS sun_path is 104 bytes.
|
|
268
|
+
return join(tmpdir(), `srt-tt-${process.pid}-${(sockSeq++).toString(36)}.sock`);
|
|
269
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { parseJsonBody, parseLimit } from "../http.js";
|
|
3
|
+
import { workspaceIdFrom } from "../workspace.js";
|
|
4
|
+
import { environmentNetworkingPresetCatalog, validateEnvironmentNetworkingHosts, } from "../egress/presets.js";
|
|
5
|
+
import { invalidRequest } from "../errors.js";
|
|
6
|
+
const NO_EGRESS_CAPABILITY = {
|
|
7
|
+
provider: null,
|
|
8
|
+
egress_supported: false,
|
|
9
|
+
reason: "This deployment has no Docker-local egress sidecar configured.",
|
|
10
|
+
};
|
|
11
|
+
export function environmentsRoutes(service, networking = NO_EGRESS_CAPABILITY) {
|
|
12
|
+
const app = new Hono();
|
|
13
|
+
app.post("/", async (c) => {
|
|
14
|
+
const body = await parseJsonBody(c.req);
|
|
15
|
+
const environment = service.create(workspaceIdFrom(c), body);
|
|
16
|
+
return c.json(environment, 200);
|
|
17
|
+
});
|
|
18
|
+
app.get("/", (c) => {
|
|
19
|
+
const limit = parseLimit(c.req.query("limit"));
|
|
20
|
+
const page = c.req.query("page") || undefined;
|
|
21
|
+
const includeArchived = parseBoolean(c.req.query("include_archived"));
|
|
22
|
+
return c.json(service.list(workspaceIdFrom(c), {
|
|
23
|
+
...(limit === undefined ? {} : { limit }),
|
|
24
|
+
...(page === undefined ? {} : { page }),
|
|
25
|
+
...(includeArchived === undefined ? {} : { includeArchived }),
|
|
26
|
+
}), 200);
|
|
27
|
+
});
|
|
28
|
+
app.get("/networking-presets", (c) => {
|
|
29
|
+
return c.json(environmentNetworkingPresetCatalog(networking), 200);
|
|
30
|
+
});
|
|
31
|
+
app.post("/networking-presets/validate", async (c) => {
|
|
32
|
+
const body = await parseJsonBody(c.req);
|
|
33
|
+
return c.json(validateEnvironmentNetworkingHosts(body), 200);
|
|
34
|
+
});
|
|
35
|
+
app.post("/:id/archive", (c) => {
|
|
36
|
+
return c.json(service.archive(workspaceIdFrom(c), c.req.param("id")), 200);
|
|
37
|
+
});
|
|
38
|
+
app.delete("/:id", (c) => {
|
|
39
|
+
return c.json(service.delete(workspaceIdFrom(c), c.req.param("id")), 200);
|
|
40
|
+
});
|
|
41
|
+
app.get("/:id", (c) => {
|
|
42
|
+
return c.json(service.retrieve(workspaceIdFrom(c), c.req.param("id")), 200);
|
|
43
|
+
});
|
|
44
|
+
return app;
|
|
45
|
+
}
|
|
46
|
+
function parseBoolean(value) {
|
|
47
|
+
if (value === undefined)
|
|
48
|
+
return undefined;
|
|
49
|
+
if (value === "true")
|
|
50
|
+
return true;
|
|
51
|
+
if (value === "false")
|
|
52
|
+
return false;
|
|
53
|
+
throw invalidRequest("`include_archived` must be `true` or `false`");
|
|
54
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { newEnvironmentId } from "../ids.js";
|
|
2
|
+
import { conflict, invalidRequest, notFound } from "../errors.js";
|
|
3
|
+
import { canonicalizeNetworkingConfig, EgressPolicyError, } from "../egress/policy.js";
|
|
4
|
+
import { isJsonObject, isJsonValue } from "../../types/json.js";
|
|
5
|
+
export class DefaultEnvironmentService {
|
|
6
|
+
store;
|
|
7
|
+
sessions;
|
|
8
|
+
constructor(store, sessions) {
|
|
9
|
+
this.store = store;
|
|
10
|
+
this.sessions = sessions;
|
|
11
|
+
}
|
|
12
|
+
create(workspaceId, input) {
|
|
13
|
+
const req = parseCreateEnvironment(input);
|
|
14
|
+
const now = new Date().toISOString();
|
|
15
|
+
const row = {
|
|
16
|
+
id: newEnvironmentId(),
|
|
17
|
+
workspace_id: workspaceId,
|
|
18
|
+
type: "environment",
|
|
19
|
+
name: req.name,
|
|
20
|
+
config: req.config,
|
|
21
|
+
created_at: now,
|
|
22
|
+
updated_at: now,
|
|
23
|
+
archived_at: null,
|
|
24
|
+
};
|
|
25
|
+
return toManagedEnvironment(this.store.create({ row }));
|
|
26
|
+
}
|
|
27
|
+
retrieve(workspaceId, environmentId) {
|
|
28
|
+
const row = this.store.retrieveAny(workspaceId, environmentId);
|
|
29
|
+
if (!row) {
|
|
30
|
+
throw notFound(`Environment ${environmentId} not found`);
|
|
31
|
+
}
|
|
32
|
+
return toManagedEnvironment(row);
|
|
33
|
+
}
|
|
34
|
+
list(workspaceId, opts = {}) {
|
|
35
|
+
const page = this.store.list(workspaceId, opts);
|
|
36
|
+
return {
|
|
37
|
+
data: page.data.map(toManagedEnvironment),
|
|
38
|
+
has_more: page.has_more,
|
|
39
|
+
next_page: page.next_page,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
archive(workspaceId, environmentId) {
|
|
43
|
+
const row = this.store.archive(workspaceId, environmentId, new Date().toISOString());
|
|
44
|
+
if (!row) {
|
|
45
|
+
throw notFound(`Environment ${environmentId} not found`);
|
|
46
|
+
}
|
|
47
|
+
return toManagedEnvironment(row);
|
|
48
|
+
}
|
|
49
|
+
delete(workspaceId, environmentId) {
|
|
50
|
+
const existing = this.store.retrieveAny(workspaceId, environmentId);
|
|
51
|
+
if (!existing) {
|
|
52
|
+
throw notFound(`Environment ${environmentId} not found`);
|
|
53
|
+
}
|
|
54
|
+
if (!this.sessions) {
|
|
55
|
+
throw conflict("Environment deletion is unavailable without session reference checks");
|
|
56
|
+
}
|
|
57
|
+
if (this.sessions.hasEnvironmentReference(workspaceId, environmentId)) {
|
|
58
|
+
throw conflict("Environment cannot be deleted while sessions reference it");
|
|
59
|
+
}
|
|
60
|
+
this.store.delete(workspaceId, environmentId);
|
|
61
|
+
return { type: "environment_deleted", id: environmentId };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function parseCreateEnvironment(input) {
|
|
65
|
+
const obj = objectInput(input);
|
|
66
|
+
const name = stringField(obj, "name", { required: true });
|
|
67
|
+
const config = obj.config;
|
|
68
|
+
if (!isJsonObject(config)) {
|
|
69
|
+
throw invalidRequest("`config` must be a JSON object");
|
|
70
|
+
}
|
|
71
|
+
if (!isJsonValue(config)) {
|
|
72
|
+
throw invalidRequest("`config` must be JSON-compatible");
|
|
73
|
+
}
|
|
74
|
+
let canonicalConfig;
|
|
75
|
+
try {
|
|
76
|
+
// Validate before the row is built or handed to the store. This keeps
|
|
77
|
+
// unsupported hosted shapes from becoming durable, silently inert config.
|
|
78
|
+
canonicalConfig = canonicalizeNetworkingConfig(config);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error instanceof EgressPolicyError) {
|
|
82
|
+
throw invalidRequest(`Invalid environment networking config: ${error.message}`);
|
|
83
|
+
}
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
return { name, config: canonicalConfig };
|
|
87
|
+
}
|
|
88
|
+
function toManagedEnvironment(row) {
|
|
89
|
+
return {
|
|
90
|
+
id: row.id,
|
|
91
|
+
type: row.type,
|
|
92
|
+
name: row.name,
|
|
93
|
+
config: row.config,
|
|
94
|
+
created_at: row.created_at,
|
|
95
|
+
updated_at: row.updated_at,
|
|
96
|
+
archived_at: row.archived_at,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function objectInput(input) {
|
|
100
|
+
if (!isJsonObject(input)) {
|
|
101
|
+
throw invalidRequest("Request body must be a JSON object");
|
|
102
|
+
}
|
|
103
|
+
return input;
|
|
104
|
+
}
|
|
105
|
+
function stringField(obj, field, opts = {}) {
|
|
106
|
+
const value = obj[field];
|
|
107
|
+
if (typeof value === "string" && value.length > 0)
|
|
108
|
+
return value;
|
|
109
|
+
if (value === undefined && opts.required !== true)
|
|
110
|
+
return "";
|
|
111
|
+
throw invalidRequest(`\`${field}\` must be a non-empty string`);
|
|
112
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
const SCHEMA = `
|
|
3
|
+
CREATE TABLE IF NOT EXISTS environments (
|
|
4
|
+
id TEXT PRIMARY KEY,
|
|
5
|
+
workspace_id TEXT NOT NULL,
|
|
6
|
+
type TEXT NOT NULL,
|
|
7
|
+
name TEXT NOT NULL,
|
|
8
|
+
config TEXT NOT NULL,
|
|
9
|
+
created_at TEXT NOT NULL,
|
|
10
|
+
updated_at TEXT NOT NULL,
|
|
11
|
+
archived_at TEXT
|
|
12
|
+
);
|
|
13
|
+
CREATE INDEX IF NOT EXISTS environments_by_workspace ON environments (workspace_id, id);
|
|
14
|
+
`;
|
|
15
|
+
export class SqliteEnvironmentStore {
|
|
16
|
+
db;
|
|
17
|
+
insertStmt;
|
|
18
|
+
retrieveActiveStmt;
|
|
19
|
+
retrieveAnyStmt;
|
|
20
|
+
archiveStmt;
|
|
21
|
+
deleteStmt;
|
|
22
|
+
listActiveStmt;
|
|
23
|
+
listAllStmt;
|
|
24
|
+
listActiveSinceStmt;
|
|
25
|
+
listAllSinceStmt;
|
|
26
|
+
constructor(db) {
|
|
27
|
+
this.db = db;
|
|
28
|
+
this.db.exec(SCHEMA);
|
|
29
|
+
this.insertStmt = this.db.prepare(`INSERT INTO environments (
|
|
30
|
+
id, workspace_id, type, name, config, created_at, updated_at, archived_at
|
|
31
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
32
|
+
this.retrieveActiveStmt = this.db.prepare(`SELECT * FROM environments
|
|
33
|
+
WHERE workspace_id = ? AND id = ? AND archived_at IS NULL`);
|
|
34
|
+
this.retrieveAnyStmt = this.db.prepare(`SELECT * FROM environments WHERE workspace_id = ? AND id = ?`);
|
|
35
|
+
this.archiveStmt = this.db.prepare(`UPDATE environments
|
|
36
|
+
SET archived_at = COALESCE(archived_at, ?),
|
|
37
|
+
updated_at = CASE WHEN archived_at IS NULL THEN ? ELSE updated_at END
|
|
38
|
+
WHERE workspace_id = ? AND id = ?`);
|
|
39
|
+
this.deleteStmt = this.db.prepare(`DELETE FROM environments WHERE workspace_id = ? AND id = ?`);
|
|
40
|
+
this.listActiveStmt = this.db.prepare(`SELECT * FROM environments
|
|
41
|
+
WHERE workspace_id = ? AND archived_at IS NULL
|
|
42
|
+
ORDER BY id ASC
|
|
43
|
+
LIMIT ?`);
|
|
44
|
+
this.listAllStmt = this.db.prepare(`SELECT * FROM environments
|
|
45
|
+
WHERE workspace_id = ?
|
|
46
|
+
ORDER BY id ASC
|
|
47
|
+
LIMIT ?`);
|
|
48
|
+
this.listActiveSinceStmt = this.db.prepare(`SELECT * FROM environments
|
|
49
|
+
WHERE workspace_id = ? AND id > ? AND archived_at IS NULL
|
|
50
|
+
ORDER BY id ASC
|
|
51
|
+
LIMIT ?`);
|
|
52
|
+
this.listAllSinceStmt = this.db.prepare(`SELECT * FROM environments
|
|
53
|
+
WHERE workspace_id = ? AND id > ?
|
|
54
|
+
ORDER BY id ASC
|
|
55
|
+
LIMIT ?`);
|
|
56
|
+
}
|
|
57
|
+
static open(path = ":memory:") {
|
|
58
|
+
return new SqliteEnvironmentStore(new DatabaseSync(path));
|
|
59
|
+
}
|
|
60
|
+
create(record) {
|
|
61
|
+
const e = record.row;
|
|
62
|
+
this.insertStmt.run(e.id, e.workspace_id, e.type, e.name, JSON.stringify(e.config), e.created_at, e.updated_at, e.archived_at);
|
|
63
|
+
return e;
|
|
64
|
+
}
|
|
65
|
+
retrieve(workspaceId, environmentId) {
|
|
66
|
+
const row = this.retrieveActiveStmt.get(workspaceId, environmentId);
|
|
67
|
+
return row ? deserialize(row) : undefined;
|
|
68
|
+
}
|
|
69
|
+
retrieveAny(workspaceId, environmentId) {
|
|
70
|
+
const row = this.retrieveAnyStmt.get(workspaceId, environmentId);
|
|
71
|
+
return row ? deserialize(row) : undefined;
|
|
72
|
+
}
|
|
73
|
+
archive(workspaceId, environmentId, archivedAt) {
|
|
74
|
+
this.archiveStmt.run(archivedAt, archivedAt, workspaceId, environmentId);
|
|
75
|
+
return this.retrieveAny(workspaceId, environmentId);
|
|
76
|
+
}
|
|
77
|
+
delete(workspaceId, environmentId) {
|
|
78
|
+
const existing = this.retrieveAny(workspaceId, environmentId);
|
|
79
|
+
if (!existing)
|
|
80
|
+
return undefined;
|
|
81
|
+
this.deleteStmt.run(workspaceId, environmentId);
|
|
82
|
+
return existing;
|
|
83
|
+
}
|
|
84
|
+
list(workspaceId, opts = {}) {
|
|
85
|
+
if (opts.page === "") {
|
|
86
|
+
return { data: [], has_more: false, next_page: null };
|
|
87
|
+
}
|
|
88
|
+
const limit = normalizeLimit(opts.limit);
|
|
89
|
+
const queryLimit = limit + 1;
|
|
90
|
+
const includeArchived = opts.includeArchived ?? false;
|
|
91
|
+
// Environments list is ASC-only in B.1; sessions carry order because SDK examples use it.
|
|
92
|
+
const rows = this.listStmt(includeArchived, opts.page !== undefined).all(...selectListArgs(workspaceId, queryLimit, opts.page));
|
|
93
|
+
const pageRows = rows.slice(0, limit);
|
|
94
|
+
const data = pageRows.map(deserialize);
|
|
95
|
+
return {
|
|
96
|
+
data,
|
|
97
|
+
has_more: rows.length > limit,
|
|
98
|
+
next_page: rows.length > limit ? data[data.length - 1]?.id ?? null : null,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
close() {
|
|
102
|
+
this.db.close();
|
|
103
|
+
}
|
|
104
|
+
listStmt(includeArchived, hasPage) {
|
|
105
|
+
if (includeArchived) {
|
|
106
|
+
return hasPage ? this.listAllSinceStmt : this.listAllStmt;
|
|
107
|
+
}
|
|
108
|
+
return hasPage ? this.listActiveSinceStmt : this.listActiveStmt;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function selectListArgs(workspaceId, limit, page) {
|
|
112
|
+
return page === undefined ? [workspaceId, limit] : [workspaceId, page, limit];
|
|
113
|
+
}
|
|
114
|
+
function normalizeLimit(limit) {
|
|
115
|
+
if (limit === undefined)
|
|
116
|
+
return 20;
|
|
117
|
+
if (!Number.isSafeInteger(limit) || limit <= 0)
|
|
118
|
+
return 20;
|
|
119
|
+
return Math.min(limit, 100);
|
|
120
|
+
}
|
|
121
|
+
function deserialize(row) {
|
|
122
|
+
return {
|
|
123
|
+
id: row.id,
|
|
124
|
+
workspace_id: row.workspace_id,
|
|
125
|
+
type: "environment",
|
|
126
|
+
name: row.name,
|
|
127
|
+
config: JSON.parse(row.config),
|
|
128
|
+
created_at: row.created_at,
|
|
129
|
+
updated_at: row.updated_at,
|
|
130
|
+
archived_at: row.archived_at,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { newRequestId } from "./ids.js";
|
|
2
|
+
export const API_ERROR_TYPES = [
|
|
3
|
+
"invalid_request_error",
|
|
4
|
+
"authentication_error",
|
|
5
|
+
"billing_error",
|
|
6
|
+
"permission_error",
|
|
7
|
+
"not_found_error",
|
|
8
|
+
"request_too_large",
|
|
9
|
+
"rate_limit_error",
|
|
10
|
+
"api_error",
|
|
11
|
+
"timeout_error",
|
|
12
|
+
"overloaded_error",
|
|
13
|
+
];
|
|
14
|
+
export class ApiError extends Error {
|
|
15
|
+
status;
|
|
16
|
+
type;
|
|
17
|
+
developerMessage;
|
|
18
|
+
retryAfterSeconds;
|
|
19
|
+
constructor(status, type, message, opts = {}) {
|
|
20
|
+
super(message, opts.cause === undefined ? undefined : { cause: opts.cause });
|
|
21
|
+
this.name = "ApiError";
|
|
22
|
+
this.status = status;
|
|
23
|
+
this.type = type;
|
|
24
|
+
this.developerMessage = opts.developerMessage;
|
|
25
|
+
if (opts.retryAfterSeconds !== undefined) {
|
|
26
|
+
this.retryAfterSeconds = opts.retryAfterSeconds;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// 0113 D9 admission rejections. Hosted Managed Agents documents 429 with a
|
|
31
|
+
// retry-after header for rate limits; 529 uses the hosted "Overloaded"
|
|
32
|
+
// message verbatim.
|
|
33
|
+
export function rateLimited(message) {
|
|
34
|
+
return new ApiError(429, "rate_limit_error", message, {
|
|
35
|
+
retryAfterSeconds: 1,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export function overloaded() {
|
|
39
|
+
return new ApiError(529, "overloaded_error", "Overloaded");
|
|
40
|
+
}
|
|
41
|
+
export function invalidRequest(message, developerMessage) {
|
|
42
|
+
return new ApiError(400, "invalid_request_error", message, {
|
|
43
|
+
developerMessage,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
// Hosted-identical envelope (0113 probe rows 1-5): one generic message for
|
|
47
|
+
// missing and invalid keys, no key-existence leak.
|
|
48
|
+
export function authenticationFailed() {
|
|
49
|
+
return new ApiError(401, "authentication_error", "Authentication failed");
|
|
50
|
+
}
|
|
51
|
+
export function notFound(message) {
|
|
52
|
+
return new ApiError(404, "not_found_error", message);
|
|
53
|
+
}
|
|
54
|
+
export function conflict(message) {
|
|
55
|
+
return new ApiError(409, "invalid_request_error", message);
|
|
56
|
+
}
|
|
57
|
+
export function requestTooLarge(message = "Request body is too large") {
|
|
58
|
+
return new ApiError(413, "request_too_large", message);
|
|
59
|
+
}
|
|
60
|
+
export function internalError(cause) {
|
|
61
|
+
return new ApiError(500, "api_error", "Internal server error", {
|
|
62
|
+
cause,
|
|
63
|
+
developerMessage: cause instanceof Error ? cause.message : String(cause),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
export function requestId() {
|
|
67
|
+
return newRequestId();
|
|
68
|
+
}
|
|
69
|
+
export function toApiErrorBody(error, id = requestId()) {
|
|
70
|
+
return {
|
|
71
|
+
type: "error",
|
|
72
|
+
error: {
|
|
73
|
+
type: error.type,
|
|
74
|
+
message: error.message,
|
|
75
|
+
},
|
|
76
|
+
request_id: id,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function ensureApiError(error) {
|
|
80
|
+
return error instanceof ApiError ? error : internalError(error);
|
|
81
|
+
}
|