@aexhq/sdk 0.35.0 → 0.36.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 +16 -15
- package/dist/_contracts/index.d.ts +3 -4
- package/dist/_contracts/index.js +1 -4
- package/dist/_contracts/operations.d.ts +2 -1
- package/dist/_contracts/operations.js +10 -0
- package/dist/_contracts/run-config.d.ts +1 -3
- package/dist/_contracts/run-config.js +2 -7
- package/dist/_contracts/run-trace.d.ts +0 -86
- package/dist/_contracts/run-trace.js +1 -184
- package/dist/_contracts/run-unit.d.ts +2 -25
- package/dist/_contracts/run-unit.js +1 -2
- package/dist/_contracts/runtime-manifest.d.ts +1 -1
- package/dist/_contracts/runtime-security-profile.d.ts +0 -2
- package/dist/_contracts/runtime-security-profile.js +0 -9
- package/dist/_contracts/runtime-types.d.ts +25 -4
- package/dist/_contracts/stable.d.ts +1 -1
- package/dist/_contracts/stable.js +1 -1
- package/dist/_contracts/submission.d.ts +4 -72
- package/dist/_contracts/submission.js +5 -472
- package/dist/cli.mjs +20 -442
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +30 -25
- package/dist/client.js +251 -66
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +7 -15
- package/dist/index.js +5 -17
- package/dist/index.js.map +1 -1
- package/dist/secret.d.ts +2 -2
- package/dist/secret.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/concepts/composition.md +8 -14
- package/docs/credentials.md +59 -101
- package/docs/defaults.md +0 -8
- package/docs/events.md +8 -9
- package/docs/limits-and-quotas.md +1 -4
- package/docs/limits.md +2 -6
- package/docs/mcp.md +4 -5
- package/docs/networking.md +6 -16
- package/docs/outputs.md +0 -4
- package/docs/public-surface.json +3 -3
- package/docs/quickstart.md +3 -7
- package/docs/run-config.md +6 -3
- package/docs/secrets.md +1 -1
- package/docs/skills.md +3 -3
- package/docs/vision-skills.md +52 -101
- package/examples/feature-tour.ts +4 -21
- package/package.json +1 -1
- package/dist/_contracts/proxy-protocol.d.ts +0 -305
- package/dist/_contracts/proxy-protocol.js +0 -297
- package/dist/_contracts/proxy-validation.d.ts +0 -19
- package/dist/_contracts/proxy-validation.js +0 -51
- package/dist/data-tools.d.ts +0 -82
- package/dist/data-tools.js +0 -251
- package/dist/data-tools.js.map +0 -1
- package/dist/proxy-endpoint.d.ts +0 -131
- package/dist/proxy-endpoint.js +0 -144
- package/dist/proxy-endpoint.js.map +0 -1
- package/examples/chat-corpus.ts +0 -84
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
import { authShapeHeaderName, PROXY_ALLOWED_METHODS, PROXY_ENDPOINT_DEFAULTS, PROXY_RETRY_JITTERS, PROXY_RETRY_POLICY_DEFAULTS, PROXY_RESPONSE_MODES } from "./proxy-protocol.js";
|
|
2
|
-
// Re-exported from the protocol module (its canonical home, alongside the
|
|
3
|
-
// index-file shape the builder fills). Kept on the submission surface so
|
|
4
|
-
// existing `@aexhq/contracts` consumers of `PROXY_ENDPOINT_DEFAULTS` are
|
|
5
|
-
// unaffected by the move.
|
|
6
|
-
export { PROXY_ENDPOINT_DEFAULTS };
|
|
7
1
|
import { TOOL_NAME_PATTERN, normaliseSkillBundlePath, parseAssetRefFields, parseMcpServerRef } from "./run-config.js";
|
|
8
2
|
import { parseRunTimeout, parseRuntimeSize } from "./runtime-sizes.js";
|
|
9
3
|
import { assertRunModelMatchesProvider, parseRunModel } from "./models.js";
|
|
@@ -118,29 +112,6 @@ export const SECRETS_KEY = "secrets";
|
|
|
118
112
|
export const SECRET_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
119
113
|
/** Workspace secret handle a `secretEnv` ref points at (and the name `secret.upload` persists to). */
|
|
120
114
|
export const SECRET_HANDLE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
121
|
-
export const PROXY_ENDPOINT_NAME_PATTERN = /^[a-z][a-z0-9_-]{0,62}$/;
|
|
122
|
-
export const RESERVED_PROXY_ENDPOINT_NAMES = new Set(["proxy", "aex", "internal", "admin"]);
|
|
123
|
-
/**
|
|
124
|
-
* Headers the proxy never lets through, regardless of policy. Lowercase.
|
|
125
|
-
* Anything that could re-introduce credentials, cookies, or routing
|
|
126
|
-
* primitives. Kept in lockstep with the proxy route's reject list.
|
|
127
|
-
*/
|
|
128
|
-
const PROXY_DENY_HEADER_LIST = new Set([
|
|
129
|
-
"authorization",
|
|
130
|
-
"cookie",
|
|
131
|
-
"set-cookie",
|
|
132
|
-
"proxy-authorization",
|
|
133
|
-
"host",
|
|
134
|
-
"content-length",
|
|
135
|
-
"transfer-encoding",
|
|
136
|
-
"connection",
|
|
137
|
-
"upgrade",
|
|
138
|
-
"expect",
|
|
139
|
-
"x-forwarded-for",
|
|
140
|
-
"x-forwarded-host",
|
|
141
|
-
"x-forwarded-proto",
|
|
142
|
-
"x-real-ip"
|
|
143
|
-
]);
|
|
144
115
|
export const deniedSecretFields = new Set([
|
|
145
116
|
"providerApiKey",
|
|
146
117
|
"anthropicApiKey",
|
|
@@ -313,322 +284,9 @@ function parsePackages(input) {
|
|
|
313
284
|
return version ? { name, version, ecosystem } : { name, ecosystem };
|
|
314
285
|
});
|
|
315
286
|
}
|
|
316
|
-
function parseProxyEndpoints(input) {
|
|
317
|
-
if (input === undefined) {
|
|
318
|
-
return undefined;
|
|
319
|
-
}
|
|
320
|
-
if (!Array.isArray(input)) {
|
|
321
|
-
throw new Error("proxyEndpoints must be an array");
|
|
322
|
-
}
|
|
323
|
-
if (input.length === 0) {
|
|
324
|
-
return undefined;
|
|
325
|
-
}
|
|
326
|
-
const seen = new Set();
|
|
327
|
-
return input.map((entry, index) => {
|
|
328
|
-
const endpoint = parseProxyEndpoint(entry, `proxyEndpoints[${index}]`);
|
|
329
|
-
if (seen.has(endpoint.name)) {
|
|
330
|
-
throw new Error(`proxyEndpoints duplicate name: ${endpoint.name}`);
|
|
331
|
-
}
|
|
332
|
-
seen.add(endpoint.name);
|
|
333
|
-
return endpoint;
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
function parseProxyEndpoint(input, path) {
|
|
337
|
-
const value = requireRecord(input, path);
|
|
338
|
-
const allowed = new Set([
|
|
339
|
-
"name",
|
|
340
|
-
"baseUrl",
|
|
341
|
-
"authShape",
|
|
342
|
-
"allowMethods",
|
|
343
|
-
"allowPathPrefixes",
|
|
344
|
-
"allowHeaders",
|
|
345
|
-
"responseMode",
|
|
346
|
-
"maxRequestBytes",
|
|
347
|
-
"maxResponseBytes",
|
|
348
|
-
"timeoutMs",
|
|
349
|
-
"retry"
|
|
350
|
-
]);
|
|
351
|
-
for (const key of Object.keys(value)) {
|
|
352
|
-
if (!allowed.has(key)) {
|
|
353
|
-
throw new Error(`${path}.${key} is not an allowed field`);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
const name = requireString(value.name, `${path}.name`);
|
|
357
|
-
if (!PROXY_ENDPOINT_NAME_PATTERN.test(name)) {
|
|
358
|
-
throw new Error(`${path}.name must match ${PROXY_ENDPOINT_NAME_PATTERN} (lowercase letters, digits, '_' and '-'; <=63 chars)`);
|
|
359
|
-
}
|
|
360
|
-
if (RESERVED_PROXY_ENDPOINT_NAMES.has(name)) {
|
|
361
|
-
throw new Error(`${path}.name is reserved: ${name}`);
|
|
362
|
-
}
|
|
363
|
-
const baseUrl = parseProxyBaseUrl(value.baseUrl, `${path}.baseUrl`);
|
|
364
|
-
const authShape = parseProxyAuthShape(value.authShape, `${path}.authShape`);
|
|
365
|
-
const allowMethods = parseProxyMethods(value.allowMethods, `${path}.allowMethods`);
|
|
366
|
-
const allowPathPrefixes = parseProxyPathPrefixes(value.allowPathPrefixes, `${path}.allowPathPrefixes`);
|
|
367
|
-
const allowHeaders = parseProxyAllowedHeaders(value.allowHeaders, `${path}.allowHeaders`, authShape);
|
|
368
|
-
const responseMode = optionalEnum(value.responseMode, `${path}.responseMode`, PROXY_RESPONSE_MODES);
|
|
369
|
-
const maxRequestBytes = optionalPositiveInt(value.maxRequestBytes, `${path}.maxRequestBytes`);
|
|
370
|
-
const maxResponseBytes = optionalPositiveInt(value.maxResponseBytes, `${path}.maxResponseBytes`);
|
|
371
|
-
const timeoutMs = optionalPositiveInt(value.timeoutMs, `${path}.timeoutMs`);
|
|
372
|
-
const retry = parseProxyRetryPolicy(value.retry, `${path}.retry`);
|
|
373
|
-
return {
|
|
374
|
-
name,
|
|
375
|
-
baseUrl,
|
|
376
|
-
authShape,
|
|
377
|
-
allowMethods,
|
|
378
|
-
allowPathPrefixes,
|
|
379
|
-
...(allowHeaders ? { allowHeaders } : {}),
|
|
380
|
-
...(responseMode ? { responseMode } : {}),
|
|
381
|
-
...(maxRequestBytes !== undefined ? { maxRequestBytes } : {}),
|
|
382
|
-
...(maxResponseBytes !== undefined ? { maxResponseBytes } : {}),
|
|
383
|
-
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
384
|
-
...(retry !== undefined ? { retry } : {})
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
export function parseProxyRetryPolicy(input, field) {
|
|
388
|
-
if (input === undefined) {
|
|
389
|
-
return undefined;
|
|
390
|
-
}
|
|
391
|
-
const value = requireRecord(input, field);
|
|
392
|
-
const allowed = new Set([
|
|
393
|
-
"maxAttempts",
|
|
394
|
-
"initialDelayMs",
|
|
395
|
-
"maxDelayMs",
|
|
396
|
-
"jitter",
|
|
397
|
-
"retryOnStatuses",
|
|
398
|
-
"retryOnMethods",
|
|
399
|
-
"respectRetryAfter"
|
|
400
|
-
]);
|
|
401
|
-
for (const key of Object.keys(value)) {
|
|
402
|
-
if (!allowed.has(key)) {
|
|
403
|
-
throw new Error(`${field}.${key} is not an allowed field`);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
const maxAttempts = parseOptionalBoundedInt(value.maxAttempts, `${field}.maxAttempts`, 1, 5);
|
|
407
|
-
const initialDelayMs = optionalPositiveInt(value.initialDelayMs, `${field}.initialDelayMs`);
|
|
408
|
-
const maxDelayMs = optionalPositiveInt(value.maxDelayMs, `${field}.maxDelayMs`);
|
|
409
|
-
const effectiveInitialDelayMs = initialDelayMs ?? PROXY_RETRY_POLICY_DEFAULTS.initialDelayMs;
|
|
410
|
-
const effectiveMaxDelayMs = maxDelayMs ?? PROXY_RETRY_POLICY_DEFAULTS.maxDelayMs;
|
|
411
|
-
if (effectiveMaxDelayMs < effectiveInitialDelayMs) {
|
|
412
|
-
throw new Error(`${field}.maxDelayMs must be greater than or equal to ${field}.initialDelayMs`);
|
|
413
|
-
}
|
|
414
|
-
const jitter = optionalEnum(value.jitter, `${field}.jitter`, PROXY_RETRY_JITTERS);
|
|
415
|
-
const retryOnStatuses = parseProxyRetryStatuses(value.retryOnStatuses, `${field}.retryOnStatuses`);
|
|
416
|
-
const retryOnMethods = parseProxyRetryMethods(value.retryOnMethods, `${field}.retryOnMethods`);
|
|
417
|
-
const respectRetryAfter = parseOptionalBoolean(value.respectRetryAfter, `${field}.respectRetryAfter`);
|
|
418
|
-
return {
|
|
419
|
-
...(maxAttempts !== undefined ? { maxAttempts } : {}),
|
|
420
|
-
...(initialDelayMs !== undefined ? { initialDelayMs } : {}),
|
|
421
|
-
...(maxDelayMs !== undefined ? { maxDelayMs } : {}),
|
|
422
|
-
...(jitter !== undefined ? { jitter } : {}),
|
|
423
|
-
...(retryOnStatuses !== undefined ? { retryOnStatuses } : {}),
|
|
424
|
-
...(retryOnMethods !== undefined ? { retryOnMethods } : {}),
|
|
425
|
-
...(respectRetryAfter !== undefined ? { respectRetryAfter } : {})
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
function parseProxyRetryStatuses(input, field) {
|
|
429
|
-
if (input === undefined) {
|
|
430
|
-
return undefined;
|
|
431
|
-
}
|
|
432
|
-
if (!Array.isArray(input)) {
|
|
433
|
-
throw new Error(`${field} must be an array of HTTP status codes`);
|
|
434
|
-
}
|
|
435
|
-
const seen = new Set();
|
|
436
|
-
for (const entry of input) {
|
|
437
|
-
if (typeof entry !== "number" ||
|
|
438
|
-
!Number.isSafeInteger(entry) ||
|
|
439
|
-
entry < 100 ||
|
|
440
|
-
entry > 599) {
|
|
441
|
-
throw new Error(`${field} entries must be HTTP status codes between 100 and 599`);
|
|
442
|
-
}
|
|
443
|
-
seen.add(entry);
|
|
444
|
-
}
|
|
445
|
-
return Array.from(seen);
|
|
446
|
-
}
|
|
447
|
-
function parseProxyRetryMethods(input, field) {
|
|
448
|
-
if (input === undefined) {
|
|
449
|
-
return undefined;
|
|
450
|
-
}
|
|
451
|
-
if (!Array.isArray(input)) {
|
|
452
|
-
throw new Error(`${field} must be an array of HTTP methods`);
|
|
453
|
-
}
|
|
454
|
-
const seen = new Set();
|
|
455
|
-
for (const entry of input) {
|
|
456
|
-
if (typeof entry !== "string") {
|
|
457
|
-
throw new Error(`${field} entries must be strings`);
|
|
458
|
-
}
|
|
459
|
-
const upper = entry.toUpperCase();
|
|
460
|
-
if (!PROXY_ALLOWED_METHODS.includes(upper)) {
|
|
461
|
-
throw new Error(`${field} contains unsupported method: ${entry}`);
|
|
462
|
-
}
|
|
463
|
-
seen.add(upper);
|
|
464
|
-
}
|
|
465
|
-
return Array.from(seen);
|
|
466
|
-
}
|
|
467
|
-
function parseProxyBaseUrl(input, field) {
|
|
468
|
-
const raw = requireString(input, field);
|
|
469
|
-
let parsed;
|
|
470
|
-
try {
|
|
471
|
-
parsed = new URL(raw);
|
|
472
|
-
}
|
|
473
|
-
catch {
|
|
474
|
-
throw new Error(`${field} must be a valid absolute URL`);
|
|
475
|
-
}
|
|
476
|
-
if (parsed.protocol !== "https:") {
|
|
477
|
-
throw new Error(`${field} must use https://`);
|
|
478
|
-
}
|
|
479
|
-
if (parsed.username || parsed.password) {
|
|
480
|
-
throw new Error(`${field} must not embed credentials`);
|
|
481
|
-
}
|
|
482
|
-
if (parsed.search || parsed.hash) {
|
|
483
|
-
throw new Error(`${field} must not include a query string or fragment`);
|
|
484
|
-
}
|
|
485
|
-
// Normalize: strip trailing slash so prefix matching is predictable.
|
|
486
|
-
const normalized = `${parsed.origin}${parsed.pathname.replace(/\/+$/, "")}`;
|
|
487
|
-
return normalized;
|
|
488
|
-
}
|
|
489
|
-
export function parseProxyAuthShape(input, field) {
|
|
490
|
-
const value = requireRecord(input, field);
|
|
491
|
-
const type = requireString(value.type, `${field}.type`);
|
|
492
|
-
switch (type) {
|
|
493
|
-
case "none":
|
|
494
|
-
assertOnlyKeys(value, field, ["type"]);
|
|
495
|
-
return { type: "none" };
|
|
496
|
-
case "bearer":
|
|
497
|
-
assertOnlyKeys(value, field, ["type"]);
|
|
498
|
-
return { type: "bearer" };
|
|
499
|
-
case "basic":
|
|
500
|
-
assertOnlyKeys(value, field, ["type"]);
|
|
501
|
-
return { type: "basic" };
|
|
502
|
-
case "header": {
|
|
503
|
-
assertOnlyKeys(value, field, ["type", "name"]);
|
|
504
|
-
const name = requireString(value.name, `${field}.name`);
|
|
505
|
-
assertHeaderName(name, `${field}.name`);
|
|
506
|
-
return { type: "header", name };
|
|
507
|
-
}
|
|
508
|
-
case "query": {
|
|
509
|
-
assertOnlyKeys(value, field, ["type", "name"]);
|
|
510
|
-
const name = requireString(value.name, `${field}.name`);
|
|
511
|
-
if (!/^[a-zA-Z0-9_\-.]{1,64}$/.test(name)) {
|
|
512
|
-
throw new Error(`${field}.name must be a URL-safe identifier (<=64 chars)`);
|
|
513
|
-
}
|
|
514
|
-
return { type: "query", name };
|
|
515
|
-
}
|
|
516
|
-
default:
|
|
517
|
-
throw new Error(`${field}.type must be one of: none, bearer, basic, header, query`);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
export function parseProxyMethods(input, field) {
|
|
521
|
-
if (!Array.isArray(input) || input.length === 0) {
|
|
522
|
-
throw new Error(`${field} must be a non-empty array of HTTP methods`);
|
|
523
|
-
}
|
|
524
|
-
const seen = new Set();
|
|
525
|
-
for (const entry of input) {
|
|
526
|
-
if (typeof entry !== "string") {
|
|
527
|
-
throw new Error(`${field} entries must be strings`);
|
|
528
|
-
}
|
|
529
|
-
const upper = entry.toUpperCase();
|
|
530
|
-
if (!PROXY_ALLOWED_METHODS.includes(upper)) {
|
|
531
|
-
throw new Error(`${field} contains unsupported method: ${entry}`);
|
|
532
|
-
}
|
|
533
|
-
seen.add(upper);
|
|
534
|
-
}
|
|
535
|
-
return Array.from(seen);
|
|
536
|
-
}
|
|
537
|
-
export function parseProxyPathPrefixes(input, field) {
|
|
538
|
-
if (!Array.isArray(input) || input.length === 0) {
|
|
539
|
-
throw new Error(`${field} must be a non-empty array of path prefixes`);
|
|
540
|
-
}
|
|
541
|
-
const seen = new Set();
|
|
542
|
-
for (const entry of input) {
|
|
543
|
-
if (typeof entry !== "string" || !entry.startsWith("/")) {
|
|
544
|
-
throw new Error(`${field} entries must be non-empty strings starting with '/'`);
|
|
545
|
-
}
|
|
546
|
-
// Reject traversal / encoded traversal at config time so we never
|
|
547
|
-
// need to second-guess at request time.
|
|
548
|
-
if (entry.includes("..") || entry.toLowerCase().includes("%2e%2e")) {
|
|
549
|
-
throw new Error(`${field} entry must not contain path traversal: ${entry}`);
|
|
550
|
-
}
|
|
551
|
-
seen.add(entry);
|
|
552
|
-
}
|
|
553
|
-
return Array.from(seen);
|
|
554
|
-
}
|
|
555
|
-
export function parseProxyAllowedHeaders(input, field, authShape) {
|
|
556
|
-
if (input === undefined) {
|
|
557
|
-
return undefined;
|
|
558
|
-
}
|
|
559
|
-
if (!Array.isArray(input)) {
|
|
560
|
-
throw new Error(`${field} must be an array of header names`);
|
|
561
|
-
}
|
|
562
|
-
const seen = new Set();
|
|
563
|
-
const result = [];
|
|
564
|
-
for (const entry of input) {
|
|
565
|
-
if (typeof entry !== "string" || entry.length === 0) {
|
|
566
|
-
throw new Error(`${field} entries must be non-empty strings`);
|
|
567
|
-
}
|
|
568
|
-
const lower = entry.toLowerCase();
|
|
569
|
-
assertHeaderName(entry, field);
|
|
570
|
-
if (PROXY_DENY_HEADER_LIST.has(lower)) {
|
|
571
|
-
throw new Error(`${field} contains a forbidden header: ${entry}`);
|
|
572
|
-
}
|
|
573
|
-
const authHeader = authShapeHeaderName(authShape);
|
|
574
|
-
if (authHeader && lower === authHeader) {
|
|
575
|
-
throw new Error(`${field} must not contain the auth header for this endpoint (${authHeader}); the proxy injects it from secrets.proxyEndpointAuth`);
|
|
576
|
-
}
|
|
577
|
-
if (seen.has(lower)) {
|
|
578
|
-
continue;
|
|
579
|
-
}
|
|
580
|
-
seen.add(lower);
|
|
581
|
-
result.push(lower);
|
|
582
|
-
}
|
|
583
|
-
return result;
|
|
584
|
-
}
|
|
585
|
-
function assertHeaderName(value, field) {
|
|
586
|
-
// RFC 7230 token chars, conservative.
|
|
587
|
-
if (!/^[A-Za-z0-9!#$%&'*+\-.^_`|~]{1,64}$/.test(value)) {
|
|
588
|
-
throw new Error(`${field} must be a valid header token (<=64 chars): ${value}`);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
function assertOnlyKeys(value, field, allowed) {
|
|
592
|
-
const permitted = new Set(allowed);
|
|
593
|
-
for (const key of Object.keys(value)) {
|
|
594
|
-
if (!permitted.has(key)) {
|
|
595
|
-
throw new Error(`${field}.${key} is not an allowed field; permitted: ${allowed.join(", ")}`);
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
export function crossValidateProxyEndpointsAndAuth(endpoints, auth) {
|
|
600
|
-
const endpointsList = endpoints ?? [];
|
|
601
|
-
const authList = auth ?? [];
|
|
602
|
-
const endpointsByName = new Map(endpointsList.map((e) => [e.name, e]));
|
|
603
|
-
const authByName = new Map(authList.map((a) => [a.name, a]));
|
|
604
|
-
for (const endpoint of endpointsList) {
|
|
605
|
-
const authEntry = authByName.get(endpoint.name);
|
|
606
|
-
if (endpoint.authShape.type === "none") {
|
|
607
|
-
// Keyless endpoints carry no auth value. Reject any matching
|
|
608
|
-
// auth entry so callers don't accidentally ship a secret bound
|
|
609
|
-
// to a "none" endpoint (which would be silently ignored at
|
|
610
|
-
// request time — confusing and a leak risk).
|
|
611
|
-
if (authEntry) {
|
|
612
|
-
throw new Error(`proxyEndpoints[${endpoint.name}] has authShape "none" but a matching secrets.proxyEndpointAuth entry was supplied; remove the auth entry`);
|
|
613
|
-
}
|
|
614
|
-
continue;
|
|
615
|
-
}
|
|
616
|
-
if (!authEntry) {
|
|
617
|
-
throw new Error(`proxyEndpoints[${endpoint.name}] has no matching secrets.proxyEndpointAuth entry`);
|
|
618
|
-
}
|
|
619
|
-
if (authEntry.value.type !== endpoint.authShape.type) {
|
|
620
|
-
throw new Error(`secrets.proxyEndpointAuth[${endpoint.name}].value.type must equal proxyEndpoints[${endpoint.name}].authShape.type (expected ${endpoint.authShape.type}, got ${authEntry.value.type})`);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
for (const authEntry of authList) {
|
|
624
|
-
if (!endpointsByName.has(authEntry.name)) {
|
|
625
|
-
throw new Error(`secrets.proxyEndpointAuth[${authEntry.name}] has no matching proxyEndpoints entry`);
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
287
|
/**
|
|
630
288
|
* Cross-check `submission.secretEnv` declarations against `secrets.envSecrets`
|
|
631
|
-
* values
|
|
289
|
+
* values:
|
|
632
290
|
*
|
|
633
291
|
* - `{ ephemeral: true }` MUST have a matching `secrets.envSecrets` value.
|
|
634
292
|
* - `{ ref }` MUST NOT supply a value (the value lives in the workspace store).
|
|
@@ -664,13 +322,12 @@ export function parseInlineSecrets(input) {
|
|
|
664
322
|
if (input === undefined || input === null)
|
|
665
323
|
return {};
|
|
666
324
|
const value = requireRecord(input, "secrets");
|
|
667
|
-
const allowedTopLevel = new Set(["apiKeys", "mcpServers", "
|
|
325
|
+
const allowedTopLevel = new Set(["apiKeys", "mcpServers", "envSecrets"]);
|
|
668
326
|
for (const key of Object.keys(value)) {
|
|
669
327
|
if (key.startsWith("__aex_")) {
|
|
670
|
-
// Platform-internal namespace
|
|
671
|
-
//
|
|
672
|
-
//
|
|
673
|
-
// from forging the bearer.
|
|
328
|
+
// Platform-internal namespace. The BFF may mutate the vaulted bundle
|
|
329
|
+
// to inject reserved values; inbound submissions are never allowed to
|
|
330
|
+
// set them.
|
|
674
331
|
throw new Error(`secrets.${key} uses the platform-internal __aex_ namespace and may not be set by callers`);
|
|
675
332
|
}
|
|
676
333
|
if (!allowedTopLevel.has(key)) {
|
|
@@ -679,12 +336,10 @@ export function parseInlineSecrets(input) {
|
|
|
679
336
|
}
|
|
680
337
|
const apiKeys = parseApiKeys(value.apiKeys);
|
|
681
338
|
const mcpServers = parseMcpServerSecrets(value.mcpServers);
|
|
682
|
-
const proxyEndpointAuth = parseProxyEndpointAuth(value.proxyEndpointAuth);
|
|
683
339
|
const envSecrets = parseEnvSecrets(value.envSecrets);
|
|
684
340
|
return {
|
|
685
341
|
...(apiKeys ? { apiKeys } : {}),
|
|
686
342
|
...(mcpServers ? { mcpServers } : {}),
|
|
687
|
-
...(proxyEndpointAuth ? { proxyEndpointAuth } : {}),
|
|
688
343
|
...(envSecrets ? { envSecrets } : {})
|
|
689
344
|
};
|
|
690
345
|
}
|
|
@@ -755,103 +410,6 @@ function parseMcpServerSecret(input, path) {
|
|
|
755
410
|
const headers = optionalStringRecord(value.headers, `${path}.headers`);
|
|
756
411
|
return headers ? { name, url, headers } : { name, url };
|
|
757
412
|
}
|
|
758
|
-
function parseProxyEndpointAuth(input) {
|
|
759
|
-
if (input === undefined) {
|
|
760
|
-
return undefined;
|
|
761
|
-
}
|
|
762
|
-
if (!Array.isArray(input)) {
|
|
763
|
-
throw new Error("secrets.proxyEndpointAuth must be an array");
|
|
764
|
-
}
|
|
765
|
-
if (input.length === 0) {
|
|
766
|
-
return undefined;
|
|
767
|
-
}
|
|
768
|
-
const seen = new Set();
|
|
769
|
-
return input.map((entry, index) => {
|
|
770
|
-
const auth = parseProxyEndpointAuthEntry(entry, `secrets.proxyEndpointAuth[${index}]`);
|
|
771
|
-
if (seen.has(auth.name)) {
|
|
772
|
-
throw new Error(`secrets.proxyEndpointAuth duplicate name: ${auth.name}`);
|
|
773
|
-
}
|
|
774
|
-
seen.add(auth.name);
|
|
775
|
-
return auth;
|
|
776
|
-
});
|
|
777
|
-
}
|
|
778
|
-
function parseProxyEndpointAuthEntry(input, path) {
|
|
779
|
-
const value = requireRecord(input, path);
|
|
780
|
-
const allowed = new Set(["name", "value"]);
|
|
781
|
-
for (const key of Object.keys(value)) {
|
|
782
|
-
if (!allowed.has(key)) {
|
|
783
|
-
throw new Error(`${path}.${key} is not an allowed field; permitted: name, value`);
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
const name = requireString(value.name, `${path}.name`);
|
|
787
|
-
if (!PROXY_ENDPOINT_NAME_PATTERN.test(name)) {
|
|
788
|
-
throw new Error(`${path}.name must match the same pattern as proxyEndpoints[].name (lowercase letters, digits, '_' and '-'; <=63 chars)`);
|
|
789
|
-
}
|
|
790
|
-
const valueField = parseProxyAuthValue(value.value, `${path}.value`);
|
|
791
|
-
return { name, value: valueField };
|
|
792
|
-
}
|
|
793
|
-
function parseProxyAuthValue(input, path) {
|
|
794
|
-
const value = requireRecord(input, path);
|
|
795
|
-
const type = requireString(value.type, `${path}.type`);
|
|
796
|
-
switch (type) {
|
|
797
|
-
case "bearer": {
|
|
798
|
-
assertOnlyKeys(value, path, ["type", "token"]);
|
|
799
|
-
const token = requireSecretValue(value.token, `${path}.token`);
|
|
800
|
-
return { type: "bearer", token };
|
|
801
|
-
}
|
|
802
|
-
case "basic": {
|
|
803
|
-
assertOnlyKeys(value, path, ["type", "username", "password"]);
|
|
804
|
-
// Usernames are not redactable in the strict sense (often public
|
|
805
|
-
// identifiers like an email), so we only enforce non-emptiness.
|
|
806
|
-
// The password is the secret-bearing half.
|
|
807
|
-
const username = requireString(value.username, `${path}.username`);
|
|
808
|
-
const password = requireSecretValue(value.password, `${path}.password`);
|
|
809
|
-
return { type: "basic", username, password };
|
|
810
|
-
}
|
|
811
|
-
case "header": {
|
|
812
|
-
assertOnlyKeys(value, path, ["type", "value"]);
|
|
813
|
-
const headerValue = requireSecretValue(value.value, `${path}.value`);
|
|
814
|
-
return { type: "header", value: headerValue };
|
|
815
|
-
}
|
|
816
|
-
case "query": {
|
|
817
|
-
assertOnlyKeys(value, path, ["type", "value"]);
|
|
818
|
-
const queryValue = requireSecretValue(value.value, `${path}.value`);
|
|
819
|
-
return { type: "query", value: queryValue };
|
|
820
|
-
}
|
|
821
|
-
default:
|
|
822
|
-
throw new Error(`${path}.type must be one of: bearer, basic, header, query`);
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
/**
|
|
826
|
-
* The proxy body-redactor refuses to mask any derived target string shorter
|
|
827
|
-
* than this many bytes — masking a 1-byte literal would corrupt the response
|
|
828
|
-
* body. This is the floor for the *derived* redaction targets (e.g.
|
|
829
|
-
* `Bearer <token>`, base64 fragments), used by
|
|
830
|
-
* the hosted proxy redactor, which imports this constant so the two sides can
|
|
831
|
-
* never silently diverge.
|
|
832
|
-
*/
|
|
833
|
-
export const MIN_REDACTION_TARGET_BYTES = 4;
|
|
834
|
-
/**
|
|
835
|
-
* Minimum byte length for an accepted proxy secret *value*. Strictly greater
|
|
836
|
-
* than {@link MIN_REDACTION_TARGET_BYTES}: a secret short enough to fall under
|
|
837
|
-
* the redactor's floor could slip through unmasked, so the submission parser
|
|
838
|
-
* rejects it up front. The `satisfies` check below pins that invariant at
|
|
839
|
-
* compile time.
|
|
840
|
-
*/
|
|
841
|
-
const MIN_PROXY_SECRET_BYTES = 8;
|
|
842
|
-
// Invariant: an accepted secret must always be long enough for the redactor to
|
|
843
|
-
// mask it. If someone lowers MIN_PROXY_SECRET_BYTES below the redaction floor,
|
|
844
|
-
// this errors at compile time.
|
|
845
|
-
const _MIN_PROXY_SECRET_BYTES_OK = (MIN_PROXY_SECRET_BYTES >= MIN_REDACTION_TARGET_BYTES);
|
|
846
|
-
void _MIN_PROXY_SECRET_BYTES_OK;
|
|
847
|
-
function requireSecretValue(input, field) {
|
|
848
|
-
const value = requireString(input, field);
|
|
849
|
-
const byteLen = Buffer.byteLength(value, "utf8");
|
|
850
|
-
if (byteLen < MIN_PROXY_SECRET_BYTES) {
|
|
851
|
-
throw new Error(`${field} must be at least ${MIN_PROXY_SECRET_BYTES} bytes; shorter values cannot be reliably redacted from upstream responses`);
|
|
852
|
-
}
|
|
853
|
-
return value;
|
|
854
|
-
}
|
|
855
413
|
export function assertNoSecretBearingFields(input, path) {
|
|
856
414
|
if (Array.isArray(input)) {
|
|
857
415
|
input.forEach((item, index) => assertNoSecretBearingFields(item, [...path, String(index)]));
|
|
@@ -949,27 +507,6 @@ export function optionalPositiveNumber(input, field) {
|
|
|
949
507
|
}
|
|
950
508
|
return input;
|
|
951
509
|
}
|
|
952
|
-
function parseOptionalBoundedInt(input, field, min, max) {
|
|
953
|
-
if (input === undefined) {
|
|
954
|
-
return undefined;
|
|
955
|
-
}
|
|
956
|
-
if (typeof input !== "number" ||
|
|
957
|
-
!Number.isSafeInteger(input) ||
|
|
958
|
-
input < min ||
|
|
959
|
-
input > max) {
|
|
960
|
-
throw new Error(`${field} must be a safe integer between ${min} and ${max}`);
|
|
961
|
-
}
|
|
962
|
-
return input;
|
|
963
|
-
}
|
|
964
|
-
function parseOptionalBoolean(input, field) {
|
|
965
|
-
if (input === undefined) {
|
|
966
|
-
return undefined;
|
|
967
|
-
}
|
|
968
|
-
if (typeof input !== "boolean") {
|
|
969
|
-
throw new Error(`${field} must be a boolean`);
|
|
970
|
-
}
|
|
971
|
-
return input;
|
|
972
|
-
}
|
|
973
510
|
function isJsonValue(input) {
|
|
974
511
|
if (typeof input === "number") {
|
|
975
512
|
return Number.isFinite(input);
|
|
@@ -994,7 +531,6 @@ export function parseRunSubmissionRequest(input, options = {}) {
|
|
|
994
531
|
"submission",
|
|
995
532
|
"runtimeSize",
|
|
996
533
|
"timeout",
|
|
997
|
-
"proxyEndpoints",
|
|
998
534
|
"webhook",
|
|
999
535
|
"limits",
|
|
1000
536
|
"machine",
|
|
@@ -1024,10 +560,8 @@ export function parseRunSubmissionRequest(input, options = {}) {
|
|
|
1024
560
|
const webhook = parseRunWebhook(value.webhook);
|
|
1025
561
|
const limits = parseRunLimits(value.limits);
|
|
1026
562
|
const machine = parseRunMachine(value.machine);
|
|
1027
|
-
const proxyEndpoints = parseProxyEndpoints(value.proxyEndpoints);
|
|
1028
563
|
const secrets = parseInlineSecrets(value.secrets);
|
|
1029
564
|
enforceCredentialSecretPolicy(secrets, provider);
|
|
1030
|
-
crossValidateProxyEndpointsAndAuth(proxyEndpoints, secrets.proxyEndpointAuth);
|
|
1031
565
|
const submission = parseSubmission(value.submission);
|
|
1032
566
|
assertRunModelMatchesProvider(provider, submission.model);
|
|
1033
567
|
crossValidateSecretEnvAndValues(submission.secretEnv, secrets.envSecrets);
|
|
@@ -1056,7 +590,6 @@ export function parseRunSubmissionRequest(input, options = {}) {
|
|
|
1056
590
|
submission,
|
|
1057
591
|
...(runtimeSize ? { runtimeSize } : {}),
|
|
1058
592
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
1059
|
-
...(proxyEndpoints ? { proxyEndpoints } : {}),
|
|
1060
593
|
...(webhook !== undefined ? { webhook } : {}),
|
|
1061
594
|
...(limits !== undefined ? { limits } : {}),
|
|
1062
595
|
...(machine !== undefined ? { machine } : {}),
|