antpath 0.2.1 → 0.4.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/README.md +54 -40
- package/dist/_shared/cleanup-policy.d.ts +8 -0
- package/dist/_shared/cleanup-policy.js +24 -0
- package/dist/_shared/config.d.ts +47 -0
- package/dist/_shared/config.js +150 -0
- package/dist/_shared/dev-stack.d.ts +19 -0
- package/dist/_shared/dev-stack.js +105 -0
- package/dist/_shared/errors.d.ts +8 -0
- package/dist/_shared/errors.js +18 -0
- package/dist/_shared/http.d.ts +25 -0
- package/dist/_shared/http.js +93 -0
- package/dist/_shared/index.d.ts +17 -0
- package/dist/_shared/index.js +20 -0
- package/dist/{providers → _shared}/known-events.d.ts +10 -10
- package/dist/{providers → _shared}/known-events.js +9 -9
- package/dist/_shared/operations.d.ts +24 -0
- package/dist/_shared/operations.js +47 -0
- package/dist/_shared/proxy-protocol.d.ts +148 -0
- package/dist/_shared/proxy-protocol.js +113 -0
- package/dist/_shared/proxy-validation.d.ts +19 -0
- package/dist/_shared/proxy-validation.js +51 -0
- package/dist/_shared/runtime-types.d.ts +90 -0
- package/dist/_shared/runtime-types.js +2 -0
- package/dist/{errors.d.ts → _shared/sdk-errors.d.ts} +10 -1
- package/dist/{errors.js → _shared/sdk-errors.js} +15 -2
- package/dist/{utils/secrets.js → _shared/sdk-secrets.js} +1 -1
- package/dist/_shared/secrets.d.ts +7 -0
- package/dist/_shared/secrets.js +20 -0
- package/dist/_shared/stable.d.ts +16 -0
- package/dist/{utils → _shared}/stable.js +14 -0
- package/dist/_shared/status.d.ts +8 -0
- package/dist/_shared/status.js +46 -0
- package/dist/_shared/submission.d.ts +157 -0
- package/dist/_shared/submission.js +681 -0
- package/dist/{template → _shared/template}/compiler.js +3 -3
- package/dist/{template/index.d.ts → _shared/template/helpers.d.ts} +0 -2
- package/dist/{template/index.js → _shared/template/helpers.js} +1 -2
- package/dist/_shared/template/index.d.ts +4 -0
- package/dist/_shared/template/index.js +4 -0
- package/dist/_shared/template/mapper.d.ts +11 -0
- package/dist/_shared/template/mapper.js +70 -0
- package/dist/cli.mjs +1223 -64
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +100 -8
- package/dist/client.js +193 -30
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +16 -10
- package/dist/index.js +16 -7
- package/dist/index.js.map +1 -1
- package/docs/cleanup.md +7 -4
- package/docs/credentials.md +10 -12
- package/docs/events.md +19 -82
- package/docs/outputs.md +15 -4
- package/docs/quickstart.md +40 -6
- package/docs/release.md +57 -12
- package/docs/skills.md +1 -1
- package/docs/templates.md +1 -1
- package/docs/testing.md +11 -8
- package/examples/mcp-static-bearer.ts +12 -9
- package/examples/quickstart.ts +6 -6
- package/package.json +5 -7
- package/dist/credentials.d.ts +0 -3
- package/dist/credentials.js +0 -56
- package/dist/credentials.js.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/files/downloader.d.ts +0 -3
- package/dist/files/downloader.js +0 -43
- package/dist/files/downloader.js.map +0 -1
- package/dist/platform/client.d.ts +0 -204
- package/dist/platform/client.js +0 -203
- package/dist/platform/client.js.map +0 -1
- package/dist/platform/index.d.ts +0 -1
- package/dist/platform/index.js +0 -2
- package/dist/platform/index.js.map +0 -1
- package/dist/providers/anthropic/provider.d.ts +0 -36
- package/dist/providers/anthropic/provider.js +0 -380
- package/dist/providers/anthropic/provider.js.map +0 -1
- package/dist/providers/known-events.js.map +0 -1
- package/dist/providers/types.d.ts +0 -42
- package/dist/providers/types.js.map +0 -1
- package/dist/run/controller.d.ts +0 -30
- package/dist/run/controller.js +0 -314
- package/dist/run/controller.js.map +0 -1
- package/dist/skills/packager.d.ts +0 -11
- package/dist/skills/packager.js +0 -76
- package/dist/skills/packager.js.map +0 -1
- package/dist/template/compiler.js.map +0 -1
- package/dist/template/index.js.map +0 -1
- package/dist/template/types.js +0 -2
- package/dist/template/types.js.map +0 -1
- package/dist/types.d.ts +0 -149
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/utils/events.d.ts +0 -27
- package/dist/utils/events.js +0 -120
- package/dist/utils/events.js.map +0 -1
- package/dist/utils/paths.d.ts +0 -3
- package/dist/utils/paths.js +0 -27
- package/dist/utils/paths.js.map +0 -1
- package/dist/utils/secrets.js.map +0 -1
- package/dist/utils/stable.d.ts +0 -2
- package/dist/utils/stable.js.map +0 -1
- package/references/architecture-decisions.md +0 -473
- package/references/implementation-plan.md +0 -452
- package/references/research-sources.md +0 -41
- package/references/testing-strategy.md +0 -29
- /package/dist/{utils/secrets.d.ts → _shared/sdk-secrets.d.ts} +0 -0
- /package/dist/{template → _shared/template}/compiler.d.ts +0 -0
- /package/dist/{template → _shared/template}/types.d.ts +0 -0
- /package/dist/{providers → _shared/template}/types.js +0 -0
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
import { authShapeHeaderName, PROXY_ALLOWED_METHODS, PROXY_RESPONSE_MODES } from "./proxy-protocol.js";
|
|
2
|
+
const SECRETS_KEY = "secrets";
|
|
3
|
+
/**
|
|
4
|
+
* Default caps for a proxy endpoint when the submission doesn't specify
|
|
5
|
+
* one. Conservative on purpose. Operators can override the platform-
|
|
6
|
+
* wide ceiling through env vars (see `references/environment-variables.md`).
|
|
7
|
+
*/
|
|
8
|
+
export const PROXY_ENDPOINT_DEFAULTS = {
|
|
9
|
+
allowHeaders: [],
|
|
10
|
+
responseMode: "headers_only",
|
|
11
|
+
maxRequestBytes: 64 * 1024,
|
|
12
|
+
maxResponseBytes: 1024 * 1024,
|
|
13
|
+
timeoutMs: 10_000,
|
|
14
|
+
perCallBudget: 60,
|
|
15
|
+
responseByteBudget: 1024 * 1024
|
|
16
|
+
};
|
|
17
|
+
const PROXY_ENDPOINT_NAME_PATTERN = /^[a-z][a-z0-9_-]{0,62}$/;
|
|
18
|
+
const RESERVED_PROXY_ENDPOINT_NAMES = new Set(["proxy", "antpath", "internal", "admin"]);
|
|
19
|
+
/**
|
|
20
|
+
* Headers the proxy never lets through, regardless of policy. Lowercase.
|
|
21
|
+
* Anything that could re-introduce credentials, cookies, or routing
|
|
22
|
+
* primitives. Kept in lockstep with the proxy route's reject list.
|
|
23
|
+
*/
|
|
24
|
+
const PROXY_DENY_HEADER_LIST = new Set([
|
|
25
|
+
"authorization",
|
|
26
|
+
"cookie",
|
|
27
|
+
"set-cookie",
|
|
28
|
+
"proxy-authorization",
|
|
29
|
+
"host",
|
|
30
|
+
"content-length",
|
|
31
|
+
"transfer-encoding",
|
|
32
|
+
"connection",
|
|
33
|
+
"upgrade",
|
|
34
|
+
"expect",
|
|
35
|
+
"x-forwarded-for",
|
|
36
|
+
"x-forwarded-host",
|
|
37
|
+
"x-forwarded-proto",
|
|
38
|
+
"x-real-ip"
|
|
39
|
+
]);
|
|
40
|
+
const deniedSecretFields = new Set([
|
|
41
|
+
"providerApiKey",
|
|
42
|
+
"anthropicApiKey",
|
|
43
|
+
"apiKey",
|
|
44
|
+
"accessToken",
|
|
45
|
+
"refreshToken",
|
|
46
|
+
"password",
|
|
47
|
+
"mcpCredentials",
|
|
48
|
+
"credentials"
|
|
49
|
+
]);
|
|
50
|
+
export function parseRunSubmissionRequest(input) {
|
|
51
|
+
const value = requireRecord(input, "submission");
|
|
52
|
+
// The `secrets` block at the top level is the single allowlisted carrier
|
|
53
|
+
// for credential material. Every other top-level field is recursively
|
|
54
|
+
// scanned for secret-bearing keys; the `secrets` block has its own strict
|
|
55
|
+
// schema (no unknown keys, no nested credential leakage).
|
|
56
|
+
for (const [key, fieldValue] of Object.entries(value)) {
|
|
57
|
+
if (key === SECRETS_KEY) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (deniedSecretFields.has(key)) {
|
|
61
|
+
throw new Error(`Secret-bearing field is not allowed in platform submission: ${key}`);
|
|
62
|
+
}
|
|
63
|
+
assertNoSecretBearingFields(fieldValue, [key]);
|
|
64
|
+
}
|
|
65
|
+
const variables = optionalJsonRecord(value.variables, "variables");
|
|
66
|
+
const cleanup = parseCleanupPolicy(value.cleanup);
|
|
67
|
+
const proxyEndpoints = parseProxyEndpoints(value.proxyEndpoints);
|
|
68
|
+
const secrets = parseInlineSecrets(value.secrets);
|
|
69
|
+
crossValidateProxyEndpointsAndAuth(proxyEndpoints, secrets.proxyEndpointAuth);
|
|
70
|
+
return {
|
|
71
|
+
workspaceId: requireString(value.workspaceId, "workspaceId"),
|
|
72
|
+
idempotencyKey: requireString(value.idempotencyKey, "idempotencyKey"),
|
|
73
|
+
template: parseTemplate(value.template),
|
|
74
|
+
...(variables ? { variables } : {}),
|
|
75
|
+
...(cleanup ? { cleanup } : {}),
|
|
76
|
+
...(proxyEndpoints ? { proxyEndpoints } : {}),
|
|
77
|
+
secrets
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function parseTemplate(input) {
|
|
81
|
+
const value = requireRecord(input, "template");
|
|
82
|
+
const system = optionalString(value.system, "template.system");
|
|
83
|
+
const metadata = optionalJsonRecord(value.metadata, "template.metadata");
|
|
84
|
+
const environment = parseTemplateEnvironment(value.environment);
|
|
85
|
+
return {
|
|
86
|
+
name: requireString(value.name, "template.name"),
|
|
87
|
+
model: requireString(value.model, "template.model"),
|
|
88
|
+
templateHash: requireString(value.templateHash, "template.templateHash"),
|
|
89
|
+
messages: requireStringArray(value.messages, "template.messages"),
|
|
90
|
+
...(system ? { system } : {}),
|
|
91
|
+
...(metadata ? { metadata } : {}),
|
|
92
|
+
...(environment ? { environment } : {})
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function parseTemplateEnvironment(input) {
|
|
96
|
+
if (input === undefined) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
const value = requireRecord(input, "template.environment");
|
|
100
|
+
const allowed = new Set(["networking", "packages"]);
|
|
101
|
+
for (const key of Object.keys(value)) {
|
|
102
|
+
if (!allowed.has(key)) {
|
|
103
|
+
throw new Error(`template.environment.${key} is not an allowed field; permitted: networking, packages`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const networking = parseTemplateNetworking(value.networking);
|
|
107
|
+
const packages = parseTemplatePackages(value.packages);
|
|
108
|
+
if (!networking && !packages) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
...(networking ? { networking } : {}),
|
|
113
|
+
...(packages ? { packages } : {})
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function parseTemplateNetworking(input) {
|
|
117
|
+
if (input === undefined) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
const value = requireRecord(input, "template.environment.networking");
|
|
121
|
+
const allowed = new Set(["mode", "allowedHosts"]);
|
|
122
|
+
for (const key of Object.keys(value)) {
|
|
123
|
+
if (!allowed.has(key)) {
|
|
124
|
+
throw new Error(`template.environment.networking.${key} is not an allowed field; permitted: mode, allowedHosts`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const mode = optionalEnum(value.mode, "template.environment.networking.mode", ["limited", "open"]);
|
|
128
|
+
if (!mode) {
|
|
129
|
+
throw new Error("template.environment.networking.mode is required when networking is provided");
|
|
130
|
+
}
|
|
131
|
+
const allowedHosts = parseAllowedHosts(value.allowedHosts);
|
|
132
|
+
return allowedHosts ? { mode, allowedHosts } : { mode };
|
|
133
|
+
}
|
|
134
|
+
function parseAllowedHosts(input) {
|
|
135
|
+
if (input === undefined) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
if (!Array.isArray(input)) {
|
|
139
|
+
throw new Error("template.environment.networking.allowedHosts must be an array of strings");
|
|
140
|
+
}
|
|
141
|
+
const seen = new Set();
|
|
142
|
+
return input.map((entry, index) => {
|
|
143
|
+
if (typeof entry !== "string" || entry.length === 0) {
|
|
144
|
+
throw new Error(`template.environment.networking.allowedHosts[${index}] must be a non-empty string`);
|
|
145
|
+
}
|
|
146
|
+
const lower = entry.toLowerCase();
|
|
147
|
+
if (seen.has(lower)) {
|
|
148
|
+
throw new Error(`template.environment.networking.allowedHosts duplicate entry: ${entry}`);
|
|
149
|
+
}
|
|
150
|
+
seen.add(lower);
|
|
151
|
+
return lower;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
function parseTemplatePackages(input) {
|
|
155
|
+
if (input === undefined) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
if (!Array.isArray(input)) {
|
|
159
|
+
throw new Error("template.environment.packages must be an array");
|
|
160
|
+
}
|
|
161
|
+
return input.map((entry, index) => {
|
|
162
|
+
const value = requireRecord(entry, `template.environment.packages[${index}]`);
|
|
163
|
+
const allowed = new Set(["name", "version"]);
|
|
164
|
+
for (const key of Object.keys(value)) {
|
|
165
|
+
if (!allowed.has(key)) {
|
|
166
|
+
throw new Error(`template.environment.packages[${index}].${key} is not an allowed field; permitted: name, version`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const name = requireString(value.name, `template.environment.packages[${index}].name`);
|
|
170
|
+
const version = optionalString(value.version, `template.environment.packages[${index}].version`);
|
|
171
|
+
return version ? { name, version } : { name };
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function parseProxyEndpoints(input) {
|
|
175
|
+
if (input === undefined) {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
if (!Array.isArray(input)) {
|
|
179
|
+
throw new Error("proxyEndpoints must be an array");
|
|
180
|
+
}
|
|
181
|
+
if (input.length === 0) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
const seen = new Set();
|
|
185
|
+
return input.map((entry, index) => {
|
|
186
|
+
const endpoint = parseProxyEndpoint(entry, `proxyEndpoints[${index}]`);
|
|
187
|
+
if (seen.has(endpoint.name)) {
|
|
188
|
+
throw new Error(`proxyEndpoints duplicate name: ${endpoint.name}`);
|
|
189
|
+
}
|
|
190
|
+
seen.add(endpoint.name);
|
|
191
|
+
return endpoint;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
function parseProxyEndpoint(input, path) {
|
|
195
|
+
const value = requireRecord(input, path);
|
|
196
|
+
const allowed = new Set([
|
|
197
|
+
"name",
|
|
198
|
+
"baseUrl",
|
|
199
|
+
"authShape",
|
|
200
|
+
"allowMethods",
|
|
201
|
+
"allowPathPrefixes",
|
|
202
|
+
"allowHeaders",
|
|
203
|
+
"responseMode",
|
|
204
|
+
"maxRequestBytes",
|
|
205
|
+
"maxResponseBytes",
|
|
206
|
+
"timeoutMs",
|
|
207
|
+
"perCallBudget",
|
|
208
|
+
"responseByteBudget"
|
|
209
|
+
]);
|
|
210
|
+
for (const key of Object.keys(value)) {
|
|
211
|
+
if (!allowed.has(key)) {
|
|
212
|
+
throw new Error(`${path}.${key} is not an allowed field`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const name = requireString(value.name, `${path}.name`);
|
|
216
|
+
if (!PROXY_ENDPOINT_NAME_PATTERN.test(name)) {
|
|
217
|
+
throw new Error(`${path}.name must match ${PROXY_ENDPOINT_NAME_PATTERN} (lowercase letters, digits, '_' and '-'; <=63 chars)`);
|
|
218
|
+
}
|
|
219
|
+
if (RESERVED_PROXY_ENDPOINT_NAMES.has(name)) {
|
|
220
|
+
throw new Error(`${path}.name is reserved: ${name}`);
|
|
221
|
+
}
|
|
222
|
+
const baseUrl = parseProxyBaseUrl(value.baseUrl, `${path}.baseUrl`);
|
|
223
|
+
const authShape = parseProxyAuthShape(value.authShape, `${path}.authShape`);
|
|
224
|
+
const allowMethods = parseProxyMethods(value.allowMethods, `${path}.allowMethods`);
|
|
225
|
+
const allowPathPrefixes = parseProxyPathPrefixes(value.allowPathPrefixes, `${path}.allowPathPrefixes`);
|
|
226
|
+
const allowHeaders = parseProxyAllowedHeaders(value.allowHeaders, `${path}.allowHeaders`, authShape);
|
|
227
|
+
const responseMode = optionalEnum(value.responseMode, `${path}.responseMode`, PROXY_RESPONSE_MODES);
|
|
228
|
+
const maxRequestBytes = optionalPositiveInt(value.maxRequestBytes, `${path}.maxRequestBytes`);
|
|
229
|
+
const maxResponseBytes = optionalPositiveInt(value.maxResponseBytes, `${path}.maxResponseBytes`);
|
|
230
|
+
const timeoutMs = optionalPositiveInt(value.timeoutMs, `${path}.timeoutMs`);
|
|
231
|
+
const perCallBudget = optionalPositiveInt(value.perCallBudget, `${path}.perCallBudget`);
|
|
232
|
+
const responseByteBudget = optionalPositiveInt(value.responseByteBudget, `${path}.responseByteBudget`);
|
|
233
|
+
return {
|
|
234
|
+
name,
|
|
235
|
+
baseUrl,
|
|
236
|
+
authShape,
|
|
237
|
+
allowMethods,
|
|
238
|
+
allowPathPrefixes,
|
|
239
|
+
...(allowHeaders ? { allowHeaders } : {}),
|
|
240
|
+
...(responseMode ? { responseMode } : {}),
|
|
241
|
+
...(maxRequestBytes !== undefined ? { maxRequestBytes } : {}),
|
|
242
|
+
...(maxResponseBytes !== undefined ? { maxResponseBytes } : {}),
|
|
243
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
244
|
+
...(perCallBudget !== undefined ? { perCallBudget } : {}),
|
|
245
|
+
...(responseByteBudget !== undefined ? { responseByteBudget } : {})
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function parseProxyBaseUrl(input, field) {
|
|
249
|
+
const raw = requireString(input, field);
|
|
250
|
+
let parsed;
|
|
251
|
+
try {
|
|
252
|
+
parsed = new URL(raw);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
throw new Error(`${field} must be a valid absolute URL`);
|
|
256
|
+
}
|
|
257
|
+
if (parsed.protocol !== "https:") {
|
|
258
|
+
throw new Error(`${field} must use https://`);
|
|
259
|
+
}
|
|
260
|
+
if (parsed.username || parsed.password) {
|
|
261
|
+
throw new Error(`${field} must not embed credentials`);
|
|
262
|
+
}
|
|
263
|
+
if (parsed.search || parsed.hash) {
|
|
264
|
+
throw new Error(`${field} must not include a query string or fragment`);
|
|
265
|
+
}
|
|
266
|
+
// Normalize: strip trailing slash so prefix matching is predictable.
|
|
267
|
+
const normalized = `${parsed.origin}${parsed.pathname.replace(/\/+$/, "")}`;
|
|
268
|
+
return normalized;
|
|
269
|
+
}
|
|
270
|
+
function parseProxyAuthShape(input, field) {
|
|
271
|
+
const value = requireRecord(input, field);
|
|
272
|
+
const type = requireString(value.type, `${field}.type`);
|
|
273
|
+
switch (type) {
|
|
274
|
+
case "bearer":
|
|
275
|
+
assertOnlyKeys(value, field, ["type"]);
|
|
276
|
+
return { type: "bearer" };
|
|
277
|
+
case "basic":
|
|
278
|
+
assertOnlyKeys(value, field, ["type"]);
|
|
279
|
+
return { type: "basic" };
|
|
280
|
+
case "header": {
|
|
281
|
+
assertOnlyKeys(value, field, ["type", "name"]);
|
|
282
|
+
const name = requireString(value.name, `${field}.name`);
|
|
283
|
+
assertHeaderName(name, `${field}.name`);
|
|
284
|
+
return { type: "header", name };
|
|
285
|
+
}
|
|
286
|
+
case "query": {
|
|
287
|
+
assertOnlyKeys(value, field, ["type", "name"]);
|
|
288
|
+
const name = requireString(value.name, `${field}.name`);
|
|
289
|
+
if (!/^[a-zA-Z0-9_\-.]{1,64}$/.test(name)) {
|
|
290
|
+
throw new Error(`${field}.name must be a URL-safe identifier (<=64 chars)`);
|
|
291
|
+
}
|
|
292
|
+
return { type: "query", name };
|
|
293
|
+
}
|
|
294
|
+
default:
|
|
295
|
+
throw new Error(`${field}.type must be one of: bearer, basic, header, query`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function parseProxyMethods(input, field) {
|
|
299
|
+
if (!Array.isArray(input) || input.length === 0) {
|
|
300
|
+
throw new Error(`${field} must be a non-empty array of HTTP methods`);
|
|
301
|
+
}
|
|
302
|
+
const seen = new Set();
|
|
303
|
+
for (const entry of input) {
|
|
304
|
+
if (typeof entry !== "string") {
|
|
305
|
+
throw new Error(`${field} entries must be strings`);
|
|
306
|
+
}
|
|
307
|
+
const upper = entry.toUpperCase();
|
|
308
|
+
if (!PROXY_ALLOWED_METHODS.includes(upper)) {
|
|
309
|
+
throw new Error(`${field} contains unsupported method: ${entry}`);
|
|
310
|
+
}
|
|
311
|
+
seen.add(upper);
|
|
312
|
+
}
|
|
313
|
+
return Array.from(seen);
|
|
314
|
+
}
|
|
315
|
+
function parseProxyPathPrefixes(input, field) {
|
|
316
|
+
if (!Array.isArray(input) || input.length === 0) {
|
|
317
|
+
throw new Error(`${field} must be a non-empty array of path prefixes`);
|
|
318
|
+
}
|
|
319
|
+
const seen = new Set();
|
|
320
|
+
for (const entry of input) {
|
|
321
|
+
if (typeof entry !== "string" || !entry.startsWith("/")) {
|
|
322
|
+
throw new Error(`${field} entries must be non-empty strings starting with '/'`);
|
|
323
|
+
}
|
|
324
|
+
// Reject traversal / encoded traversal at config time so we never
|
|
325
|
+
// need to second-guess at request time.
|
|
326
|
+
if (entry.includes("..") || entry.toLowerCase().includes("%2e%2e")) {
|
|
327
|
+
throw new Error(`${field} entry must not contain path traversal: ${entry}`);
|
|
328
|
+
}
|
|
329
|
+
seen.add(entry);
|
|
330
|
+
}
|
|
331
|
+
return Array.from(seen);
|
|
332
|
+
}
|
|
333
|
+
function parseProxyAllowedHeaders(input, field, authShape) {
|
|
334
|
+
if (input === undefined) {
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
if (!Array.isArray(input)) {
|
|
338
|
+
throw new Error(`${field} must be an array of header names`);
|
|
339
|
+
}
|
|
340
|
+
const seen = new Set();
|
|
341
|
+
const result = [];
|
|
342
|
+
for (const entry of input) {
|
|
343
|
+
if (typeof entry !== "string" || entry.length === 0) {
|
|
344
|
+
throw new Error(`${field} entries must be non-empty strings`);
|
|
345
|
+
}
|
|
346
|
+
const lower = entry.toLowerCase();
|
|
347
|
+
assertHeaderName(entry, field);
|
|
348
|
+
if (PROXY_DENY_HEADER_LIST.has(lower)) {
|
|
349
|
+
throw new Error(`${field} contains a forbidden header: ${entry}`);
|
|
350
|
+
}
|
|
351
|
+
const authHeader = authShapeHeaderName(authShape);
|
|
352
|
+
if (authHeader && lower === authHeader) {
|
|
353
|
+
throw new Error(`${field} must not contain the auth header for this endpoint (${authHeader}); the proxy injects it from secrets.proxyEndpointAuth`);
|
|
354
|
+
}
|
|
355
|
+
if (seen.has(lower)) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
seen.add(lower);
|
|
359
|
+
result.push(lower);
|
|
360
|
+
}
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
363
|
+
function assertHeaderName(value, field) {
|
|
364
|
+
// RFC 7230 token chars, conservative.
|
|
365
|
+
if (!/^[A-Za-z0-9!#$%&'*+\-.^_`|~]{1,64}$/.test(value)) {
|
|
366
|
+
throw new Error(`${field} must be a valid header token (<=64 chars): ${value}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function assertOnlyKeys(value, field, allowed) {
|
|
370
|
+
const permitted = new Set(allowed);
|
|
371
|
+
for (const key of Object.keys(value)) {
|
|
372
|
+
if (!permitted.has(key)) {
|
|
373
|
+
throw new Error(`${field}.${key} is not an allowed field; permitted: ${allowed.join(", ")}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function crossValidateProxyEndpointsAndAuth(endpoints, auth) {
|
|
378
|
+
const endpointsList = endpoints ?? [];
|
|
379
|
+
const authList = auth ?? [];
|
|
380
|
+
const endpointsByName = new Map(endpointsList.map((e) => [e.name, e]));
|
|
381
|
+
const authByName = new Map(authList.map((a) => [a.name, a]));
|
|
382
|
+
for (const endpoint of endpointsList) {
|
|
383
|
+
const authEntry = authByName.get(endpoint.name);
|
|
384
|
+
if (!authEntry) {
|
|
385
|
+
throw new Error(`proxyEndpoints[${endpoint.name}] has no matching secrets.proxyEndpointAuth entry`);
|
|
386
|
+
}
|
|
387
|
+
if (authEntry.value.type !== endpoint.authShape.type) {
|
|
388
|
+
throw new Error(`secrets.proxyEndpointAuth[${endpoint.name}].value.type must equal proxyEndpoints[${endpoint.name}].authShape.type (expected ${endpoint.authShape.type}, got ${authEntry.value.type})`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
for (const authEntry of authList) {
|
|
392
|
+
if (!endpointsByName.has(authEntry.name)) {
|
|
393
|
+
throw new Error(`secrets.proxyEndpointAuth[${authEntry.name}] has no matching proxyEndpoints entry`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function parseCleanupPolicy(input) {
|
|
398
|
+
if (input === undefined) {
|
|
399
|
+
return undefined;
|
|
400
|
+
}
|
|
401
|
+
const value = requireRecord(input, "cleanup");
|
|
402
|
+
const session = optionalEnum(value.session, "cleanup.session", ["retain", "delete"]);
|
|
403
|
+
const claudeSession = optionalEnum(value.claudeSession, "cleanup.claudeSession", ["retain", "delete"]);
|
|
404
|
+
if (session !== undefined && claudeSession !== undefined && session !== claudeSession) {
|
|
405
|
+
throw new Error("cleanup.session and cleanup.claudeSession must agree; cleanup.claudeSession is deprecated");
|
|
406
|
+
}
|
|
407
|
+
const resolved = session ?? claudeSession;
|
|
408
|
+
if (resolved === undefined) {
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
const policy = { session: resolved };
|
|
412
|
+
if (claudeSession !== undefined) {
|
|
413
|
+
return { ...policy, claudeSession: resolved };
|
|
414
|
+
}
|
|
415
|
+
return policy;
|
|
416
|
+
}
|
|
417
|
+
function parseInlineSecrets(input) {
|
|
418
|
+
const value = requireRecord(input, "secrets");
|
|
419
|
+
const allowedTopLevel = new Set(["anthropic", "mcpServers", "skills", "proxyEndpointAuth"]);
|
|
420
|
+
for (const key of Object.keys(value)) {
|
|
421
|
+
if (key.startsWith("__antpath_")) {
|
|
422
|
+
// Platform-internal namespace (e.g. __antpath_proxy_token). The BFF
|
|
423
|
+
// mutates the vaulted bundle to inject these; inbound submissions
|
|
424
|
+
// are never allowed to set them, to prevent a malicious caller
|
|
425
|
+
// from forging the bearer.
|
|
426
|
+
throw new Error(`secrets.${key} uses the platform-internal __antpath_ namespace and may not be set by callers`);
|
|
427
|
+
}
|
|
428
|
+
if (!allowedTopLevel.has(key)) {
|
|
429
|
+
throw new Error(`secrets.${key} is not an allowed field; permitted: anthropic, mcpServers, skills, proxyEndpointAuth`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const anthropic = parseAnthropicSecret(value.anthropic);
|
|
433
|
+
const mcpServers = parseMcpServers(value.mcpServers);
|
|
434
|
+
const skills = parseSkills(value.skills);
|
|
435
|
+
const proxyEndpointAuth = parseProxyEndpointAuth(value.proxyEndpointAuth);
|
|
436
|
+
return {
|
|
437
|
+
anthropic,
|
|
438
|
+
...(mcpServers ? { mcpServers } : {}),
|
|
439
|
+
...(skills ? { skills } : {}),
|
|
440
|
+
...(proxyEndpointAuth ? { proxyEndpointAuth } : {})
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
function parseAnthropicSecret(input) {
|
|
444
|
+
const value = requireRecord(input, "secrets.anthropic");
|
|
445
|
+
const allowed = new Set(["apiKey", "baseUrl"]);
|
|
446
|
+
for (const key of Object.keys(value)) {
|
|
447
|
+
if (!allowed.has(key)) {
|
|
448
|
+
throw new Error(`secrets.anthropic.${key} is not an allowed field; permitted: apiKey, baseUrl`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const apiKey = requireString(value.apiKey, "secrets.anthropic.apiKey");
|
|
452
|
+
const baseUrl = optionalString(value.baseUrl, "secrets.anthropic.baseUrl");
|
|
453
|
+
return baseUrl ? { apiKey, baseUrl } : { apiKey };
|
|
454
|
+
}
|
|
455
|
+
function parseMcpServers(input) {
|
|
456
|
+
if (input === undefined) {
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
if (!Array.isArray(input)) {
|
|
460
|
+
throw new Error("secrets.mcpServers must be an array");
|
|
461
|
+
}
|
|
462
|
+
return input.map((entry, index) => parseMcpServer(entry, `secrets.mcpServers[${index}]`));
|
|
463
|
+
}
|
|
464
|
+
function parseMcpServer(input, path) {
|
|
465
|
+
const value = requireRecord(input, path);
|
|
466
|
+
const allowed = new Set(["name", "url", "headers"]);
|
|
467
|
+
for (const key of Object.keys(value)) {
|
|
468
|
+
if (!allowed.has(key)) {
|
|
469
|
+
throw new Error(`${path}.${key} is not an allowed field; permitted: name, url, headers`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const name = requireString(value.name, `${path}.name`);
|
|
473
|
+
const url = requireString(value.url, `${path}.url`);
|
|
474
|
+
const headers = optionalStringRecord(value.headers, `${path}.headers`);
|
|
475
|
+
return headers ? { name, url, headers } : { name, url };
|
|
476
|
+
}
|
|
477
|
+
function parseSkills(input) {
|
|
478
|
+
if (input === undefined) {
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|
|
481
|
+
if (!Array.isArray(input)) {
|
|
482
|
+
throw new Error("secrets.skills must be an array");
|
|
483
|
+
}
|
|
484
|
+
return input.map((entry, index) => parseSkill(entry, `secrets.skills[${index}]`));
|
|
485
|
+
}
|
|
486
|
+
function parseSkill(input, path) {
|
|
487
|
+
const value = requireRecord(input, path);
|
|
488
|
+
const allowed = new Set(["skillId", "version"]);
|
|
489
|
+
for (const key of Object.keys(value)) {
|
|
490
|
+
if (!allowed.has(key)) {
|
|
491
|
+
throw new Error(`${path}.${key} is not an allowed field; permitted: skillId, version`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const skillId = requireString(value.skillId, `${path}.skillId`);
|
|
495
|
+
const version = optionalString(value.version, `${path}.version`);
|
|
496
|
+
return version ? { skillId, version } : { skillId };
|
|
497
|
+
}
|
|
498
|
+
function parseProxyEndpointAuth(input) {
|
|
499
|
+
if (input === undefined) {
|
|
500
|
+
return undefined;
|
|
501
|
+
}
|
|
502
|
+
if (!Array.isArray(input)) {
|
|
503
|
+
throw new Error("secrets.proxyEndpointAuth must be an array");
|
|
504
|
+
}
|
|
505
|
+
if (input.length === 0) {
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
const seen = new Set();
|
|
509
|
+
return input.map((entry, index) => {
|
|
510
|
+
const auth = parseProxyEndpointAuthEntry(entry, `secrets.proxyEndpointAuth[${index}]`);
|
|
511
|
+
if (seen.has(auth.name)) {
|
|
512
|
+
throw new Error(`secrets.proxyEndpointAuth duplicate name: ${auth.name}`);
|
|
513
|
+
}
|
|
514
|
+
seen.add(auth.name);
|
|
515
|
+
return auth;
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
function parseProxyEndpointAuthEntry(input, path) {
|
|
519
|
+
const value = requireRecord(input, path);
|
|
520
|
+
const allowed = new Set(["name", "value"]);
|
|
521
|
+
for (const key of Object.keys(value)) {
|
|
522
|
+
if (!allowed.has(key)) {
|
|
523
|
+
throw new Error(`${path}.${key} is not an allowed field; permitted: name, value`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const name = requireString(value.name, `${path}.name`);
|
|
527
|
+
if (!PROXY_ENDPOINT_NAME_PATTERN.test(name)) {
|
|
528
|
+
throw new Error(`${path}.name must match the same pattern as proxyEndpoints[].name (lowercase letters, digits, '_' and '-'; <=63 chars)`);
|
|
529
|
+
}
|
|
530
|
+
const valueField = parseProxyAuthValue(value.value, `${path}.value`);
|
|
531
|
+
return { name, value: valueField };
|
|
532
|
+
}
|
|
533
|
+
function parseProxyAuthValue(input, path) {
|
|
534
|
+
const value = requireRecord(input, path);
|
|
535
|
+
const type = requireString(value.type, `${path}.type`);
|
|
536
|
+
switch (type) {
|
|
537
|
+
case "bearer": {
|
|
538
|
+
assertOnlyKeys(value, path, ["type", "token"]);
|
|
539
|
+
const token = requireSecretValue(value.token, `${path}.token`);
|
|
540
|
+
return { type: "bearer", token };
|
|
541
|
+
}
|
|
542
|
+
case "basic": {
|
|
543
|
+
assertOnlyKeys(value, path, ["type", "username", "password"]);
|
|
544
|
+
// Usernames are not redactable in the strict sense (often public
|
|
545
|
+
// identifiers like an email), so we only enforce non-emptiness.
|
|
546
|
+
// The password is the secret-bearing half.
|
|
547
|
+
const username = requireString(value.username, `${path}.username`);
|
|
548
|
+
const password = requireSecretValue(value.password, `${path}.password`);
|
|
549
|
+
return { type: "basic", username, password };
|
|
550
|
+
}
|
|
551
|
+
case "header": {
|
|
552
|
+
assertOnlyKeys(value, path, ["type", "value"]);
|
|
553
|
+
const headerValue = requireSecretValue(value.value, `${path}.value`);
|
|
554
|
+
return { type: "header", value: headerValue };
|
|
555
|
+
}
|
|
556
|
+
case "query": {
|
|
557
|
+
assertOnlyKeys(value, path, ["type", "value"]);
|
|
558
|
+
const queryValue = requireSecretValue(value.value, `${path}.value`);
|
|
559
|
+
return { type: "query", value: queryValue };
|
|
560
|
+
}
|
|
561
|
+
default:
|
|
562
|
+
throw new Error(`${path}.type must be one of: bearer, basic, header, query`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Minimum byte length for a value that MUST be redacted from upstream
|
|
567
|
+
* response bodies. The proxy's body redactor refuses to mask anything
|
|
568
|
+
* shorter than this (masking a 1-byte literal would corrupt the body),
|
|
569
|
+
* so accepting a 4-byte secret here would silently let it through the
|
|
570
|
+
* redactor. The threshold is mirrored exactly in
|
|
571
|
+
* `apps/dashboard/src/server/proxy/redact.ts` — keep them in sync.
|
|
572
|
+
*/
|
|
573
|
+
const MIN_PROXY_SECRET_BYTES = 8;
|
|
574
|
+
function requireSecretValue(input, field) {
|
|
575
|
+
const value = requireString(input, field);
|
|
576
|
+
const byteLen = Buffer.byteLength(value, "utf8");
|
|
577
|
+
if (byteLen < MIN_PROXY_SECRET_BYTES) {
|
|
578
|
+
throw new Error(`${field} must be at least ${MIN_PROXY_SECRET_BYTES} bytes; shorter values cannot be reliably redacted from upstream responses`);
|
|
579
|
+
}
|
|
580
|
+
return value;
|
|
581
|
+
}
|
|
582
|
+
function assertNoSecretBearingFields(input, path) {
|
|
583
|
+
if (Array.isArray(input)) {
|
|
584
|
+
input.forEach((item, index) => assertNoSecretBearingFields(item, [...path, String(index)]));
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (!isRecord(input)) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
for (const [key, value] of Object.entries(input)) {
|
|
591
|
+
if (deniedSecretFields.has(key)) {
|
|
592
|
+
throw new Error(`Secret-bearing field is not allowed in platform submission: ${[...path, key].join(".")}`);
|
|
593
|
+
}
|
|
594
|
+
assertNoSecretBearingFields(value, [...path, key]);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
function requireRecord(input, field) {
|
|
598
|
+
if (!isRecord(input)) {
|
|
599
|
+
throw new Error(`${field} must be an object`);
|
|
600
|
+
}
|
|
601
|
+
return input;
|
|
602
|
+
}
|
|
603
|
+
function isRecord(input) {
|
|
604
|
+
return typeof input === "object" && input !== null && !Array.isArray(input);
|
|
605
|
+
}
|
|
606
|
+
function requireString(input, field) {
|
|
607
|
+
if (typeof input !== "string" || input.length === 0) {
|
|
608
|
+
throw new Error(`${field} must be a non-empty string`);
|
|
609
|
+
}
|
|
610
|
+
return input;
|
|
611
|
+
}
|
|
612
|
+
function optionalString(input, field) {
|
|
613
|
+
if (input === undefined) {
|
|
614
|
+
return undefined;
|
|
615
|
+
}
|
|
616
|
+
return requireString(input, field);
|
|
617
|
+
}
|
|
618
|
+
function optionalEnum(input, field, allowed) {
|
|
619
|
+
if (input === undefined) {
|
|
620
|
+
return undefined;
|
|
621
|
+
}
|
|
622
|
+
if (typeof input !== "string" || !allowed.includes(input)) {
|
|
623
|
+
throw new Error(`${field} must be one of: ${allowed.join(", ")}`);
|
|
624
|
+
}
|
|
625
|
+
return input;
|
|
626
|
+
}
|
|
627
|
+
function requireStringArray(input, field) {
|
|
628
|
+
if (!Array.isArray(input) || input.length === 0 || input.some((item) => typeof item !== "string" || item.length === 0)) {
|
|
629
|
+
throw new Error(`${field} must be a non-empty string array`);
|
|
630
|
+
}
|
|
631
|
+
return input;
|
|
632
|
+
}
|
|
633
|
+
function optionalStringRecord(input, field) {
|
|
634
|
+
if (input === undefined) {
|
|
635
|
+
return undefined;
|
|
636
|
+
}
|
|
637
|
+
const value = requireRecord(input, field);
|
|
638
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
639
|
+
if (typeof entry !== "string" || entry.length === 0) {
|
|
640
|
+
throw new Error(`${field}.${key} must be a non-empty string`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return value;
|
|
644
|
+
}
|
|
645
|
+
function optionalJsonRecord(input, field) {
|
|
646
|
+
if (input === undefined) {
|
|
647
|
+
return undefined;
|
|
648
|
+
}
|
|
649
|
+
const value = requireRecord(input, field);
|
|
650
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
651
|
+
if (!isJsonValue(entry)) {
|
|
652
|
+
throw new Error(`${field}.${key} must be JSON-serializable`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return value;
|
|
656
|
+
}
|
|
657
|
+
function optionalPositiveInt(input, field) {
|
|
658
|
+
if (input === undefined) {
|
|
659
|
+
return undefined;
|
|
660
|
+
}
|
|
661
|
+
if (typeof input !== "number" || !Number.isSafeInteger(input) || input <= 0) {
|
|
662
|
+
throw new Error(`${field} must be a positive safe integer`);
|
|
663
|
+
}
|
|
664
|
+
return input;
|
|
665
|
+
}
|
|
666
|
+
function isJsonValue(input) {
|
|
667
|
+
if (typeof input === "number") {
|
|
668
|
+
return Number.isFinite(input);
|
|
669
|
+
}
|
|
670
|
+
if (input === null || typeof input === "string" || typeof input === "boolean") {
|
|
671
|
+
return true;
|
|
672
|
+
}
|
|
673
|
+
if (Array.isArray(input)) {
|
|
674
|
+
return input.every(isJsonValue);
|
|
675
|
+
}
|
|
676
|
+
if (isRecord(input)) {
|
|
677
|
+
return Object.values(input).every(isJsonValue);
|
|
678
|
+
}
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
//# sourceMappingURL=submission.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { TemplateValidationError } from "../errors.js";
|
|
2
|
-
import { containsSecretLikeValue } from "../
|
|
3
|
-
import { sha256, stableStringify } from "../
|
|
1
|
+
import { TemplateValidationError } from "../sdk-errors.js";
|
|
2
|
+
import { containsSecretLikeValue } from "../sdk-secrets.js";
|
|
3
|
+
import { sha256, stableStringify } from "../stable.js";
|
|
4
4
|
const ESCAPED_OPEN = "\u0000ANTPATH_ESCAPED_OPEN\u0000";
|
|
5
5
|
const VARIABLE_PATTERN = /{{\s*([A-Za-z_][A-Za-z0-9_]*)\s*}}/g;
|
|
6
6
|
export function compileTemplate(template, variables = {}) {
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import type { CredentialRequirement, TemplateDefinition, TemplateVariableDefinition } from "./types.js";
|
|
2
|
-
export type { TemplateDefinition, TemplateVariableDefinition, EnvironmentDefinition } from "./types.js";
|
|
3
|
-
export { compileTemplate, type ResolvedTemplate } from "./compiler.js";
|
|
4
2
|
export declare function defineTemplate<TVariables extends Record<string, TemplateVariableDefinition> = Record<string, never>>(definition: TemplateDefinition<TVariables>): TemplateDefinition<TVariables>;
|
|
5
3
|
export declare function string(options?: {
|
|
6
4
|
default?: string;
|