@topolo/cli 0.12.9 → 0.12.11

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/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { platform as platform2 } from "os";
9
9
  import { createInterface } from "readline/promises";
10
10
  import { stdin as input } from "process";
11
11
 
12
- // node_modules/@topolo/sdk/dist/index.js
12
+ // node_modules/.pnpm/@topolo+sdk@0.21.4/node_modules/@topolo/sdk/dist/index.js
13
13
  function applyAuthHeaders(headers, credential) {
14
14
  if (credential.kind === "api_key") {
15
15
  headers.set("X-Api-Key", credential.apiKey);
@@ -28,58 +28,6 @@ function generateRequestId() {
28
28
  }
29
29
  return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
30
30
  }
31
- var encoder = new TextEncoder();
32
- function encodeBase64Url(value) {
33
- const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
34
- let binary = "";
35
- for (const byte of bytes) binary += String.fromCharCode(byte);
36
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
37
- }
38
- function encodeJson(value) {
39
- return encodeBase64Url(encoder.encode(JSON.stringify(value)));
40
- }
41
- async function sha256Base64Url(value) {
42
- return encodeBase64Url(await crypto.subtle.digest("SHA-256", encoder.encode(value)));
43
- }
44
- function publicJwk(privateJwk) {
45
- const { crv, kty, x, y } = privateJwk;
46
- if (kty !== "EC" || crv !== "P-256" || !x || !y || !privateJwk.d) {
47
- throw new TypeError("A private P-256 JSON Web Key is required for DPoP signing.");
48
- }
49
- return { crv, kty, x, y };
50
- }
51
- function canonicalTargetUrl(value) {
52
- const url = new URL(value);
53
- return `${url.origin}${url.pathname}`;
54
- }
55
- async function calculateDpopJkt(privateJwk) {
56
- const jwk = publicJwk(privateJwk);
57
- return sha256Base64Url(JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y }));
58
- }
59
- async function createDpopProof(input2) {
60
- const jwk = publicJwk(input2.privateJwk);
61
- const key = await crypto.subtle.importKey(
62
- "jwk",
63
- input2.privateJwk,
64
- { name: "ECDSA", namedCurve: "P-256" },
65
- false,
66
- ["sign"]
67
- );
68
- const header = encodeJson({ alg: "ES256", typ: "dpop+jwt", jwk });
69
- const claims = encodeJson({
70
- jti: crypto.randomUUID(),
71
- iat: Math.floor(Date.now() / 1e3),
72
- htm: input2.method.toUpperCase(),
73
- htu: canonicalTargetUrl(input2.targetUrl),
74
- ath: await sha256Base64Url(input2.accessToken)
75
- });
76
- const signature = await crypto.subtle.sign(
77
- { name: "ECDSA", hash: "SHA-256" },
78
- key,
79
- encoder.encode(`${header}.${claims}`)
80
- );
81
- return `${header}.${claims}.${encodeBase64Url(signature)}`;
82
- }
83
31
  var TopoloSdkError = class extends Error {
84
32
  code;
85
33
  constructor(code, message2) {
@@ -401,6 +349,136 @@ var TopoloHttpError = class extends TopoloSdkError {
401
349
  );
402
350
  }
403
351
  };
352
+ var ACTION_SECRET_PREFIX = "topolo-secret:";
353
+ function isActionSecretReference(value) {
354
+ return typeof value === "string" && value.startsWith(ACTION_SECRET_PREFIX);
355
+ }
356
+ function isRecord(value) {
357
+ return typeof value === "object" && value !== null && !Array.isArray(value);
358
+ }
359
+ function sensitive(key, value) {
360
+ const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
361
+ if (normalized === "capability") {
362
+ if (typeof value !== "string") return value !== null && value !== void 0 && typeof value !== "boolean" && typeof value !== "number";
363
+ const text2 = value.trim();
364
+ return Boolean(text2) && !/^[a-z][a-z0-9_-]{0,31}$/.test(text2);
365
+ }
366
+ return ["authorization", "cookie", "setcookie", "proxyauthorization"].includes(normalized) || /(token|secret|password|privatekey|apikey)$/.test(normalized);
367
+ }
368
+ function maskActionSecrets(value, secrets) {
369
+ let masked = value;
370
+ for (const [secret, reference] of secrets) {
371
+ const encoded = encodeURIComponent(secret);
372
+ const variants = [.../* @__PURE__ */ new Set([secret, encoded])].map((item) => item.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
373
+ masked = masked.replace(new RegExp(variants.join("|"), "g"), (match) => match === secret ? reference : encodeURIComponent(reference));
374
+ }
375
+ return masked;
376
+ }
377
+ async function resolveActionSecrets(value, store, secrets) {
378
+ if (isActionSecretReference(value)) {
379
+ if (!store) throw new TopoloSdkError("action_secret_store_required", "Protected action references require a secret store.");
380
+ const resolved = await store.load(value);
381
+ if (typeof resolved !== "string" || !resolved) throw new TopoloSdkError("action_secret_unsupported_type", "Protected action handoff requires a nonempty string credential.");
382
+ secrets.set(resolved, value);
383
+ return resolved;
384
+ }
385
+ if (Array.isArray(value)) return Promise.all(value.map((item) => resolveActionSecrets(item, store, secrets)));
386
+ if (!isRecord(value)) return value;
387
+ return Object.fromEntries(await Promise.all(Object.entries(value).map(async ([key, item]) => [key, await resolveActionSecrets(item, store, secrets)])));
388
+ }
389
+ async function protectActionResult(value, store) {
390
+ const secrets = /* @__PURE__ */ new Map();
391
+ async function protect(item, key = "") {
392
+ if (isActionSecretReference(item)) return item;
393
+ if (sensitive(key, item)) {
394
+ const reference = store ? await store.save(item) : "[REDACTED]";
395
+ if (typeof item === "string" && item) secrets.set(item, reference);
396
+ return reference;
397
+ }
398
+ if (Array.isArray(item)) return Promise.all(item.map((child) => protect(child)));
399
+ if (!isRecord(item)) return item;
400
+ return Object.fromEntries(await Promise.all(Object.entries(item).map(async ([name, child]) => [name, await protect(child, name)])));
401
+ }
402
+ const protectedValue = await protect(value);
403
+ async function protectEmbedded(item) {
404
+ if (typeof item === "string" && !isActionSecretReference(item) && maskActionSecrets(item, secrets) !== item) {
405
+ return store ? store.save(item) : maskActionSecrets(item, secrets);
406
+ }
407
+ if (Array.isArray(item)) return Promise.all(item.map(protectEmbedded));
408
+ if (!isRecord(item)) return item;
409
+ return Object.fromEntries(await Promise.all(Object.entries(item).map(async ([key, child]) => [key, await protectEmbedded(child)])));
410
+ }
411
+ return protectEmbedded(protectedValue);
412
+ }
413
+ function maskActionSecretValue(value, secrets) {
414
+ if (value instanceof Error) return maskActionSecretError(value, secrets);
415
+ if (typeof value === "string") return maskActionSecrets(value, secrets);
416
+ if (Array.isArray(value)) return value.map((item) => maskActionSecretValue(item, secrets));
417
+ if (!isRecord(value)) return value;
418
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, maskActionSecretValue(item, secrets)]));
419
+ }
420
+ function maskActionSecretError(error, secrets) {
421
+ if (!(error instanceof Error)) return maskActionSecretValue(error, secrets);
422
+ const masked = Object.create(Object.getPrototypeOf(error));
423
+ for (const key of Object.getOwnPropertyNames(error)) {
424
+ const descriptor = Object.getOwnPropertyDescriptor(error, key);
425
+ if ("value" in descriptor) descriptor.value = maskActionSecretValue(descriptor.value, secrets);
426
+ Object.defineProperty(masked, key, descriptor);
427
+ }
428
+ return masked;
429
+ }
430
+ var encoder = new TextEncoder();
431
+ function encodeBase64Url(value) {
432
+ const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
433
+ let binary = "";
434
+ for (const byte of bytes) binary += String.fromCharCode(byte);
435
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
436
+ }
437
+ function encodeJson(value) {
438
+ return encodeBase64Url(encoder.encode(JSON.stringify(value)));
439
+ }
440
+ async function sha256Base64Url(value) {
441
+ return encodeBase64Url(await crypto.subtle.digest("SHA-256", encoder.encode(value)));
442
+ }
443
+ function publicJwk(privateJwk) {
444
+ const { crv, kty, x, y } = privateJwk;
445
+ if (kty !== "EC" || crv !== "P-256" || !x || !y || !privateJwk.d) {
446
+ throw new TypeError("A private P-256 JSON Web Key is required for DPoP signing.");
447
+ }
448
+ return { crv, kty, x, y };
449
+ }
450
+ function canonicalTargetUrl(value) {
451
+ const url = new URL(value);
452
+ return `${url.origin}${url.pathname}`;
453
+ }
454
+ async function calculateDpopJkt(privateJwk) {
455
+ const jwk = publicJwk(privateJwk);
456
+ return sha256Base64Url(JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y }));
457
+ }
458
+ async function createDpopProof(input2) {
459
+ const jwk = publicJwk(input2.privateJwk);
460
+ const key = await crypto.subtle.importKey(
461
+ "jwk",
462
+ input2.privateJwk,
463
+ { name: "ECDSA", namedCurve: "P-256" },
464
+ false,
465
+ ["sign"]
466
+ );
467
+ const header = encodeJson({ alg: "ES256", typ: "dpop+jwt", jwk });
468
+ const claims = encodeJson({
469
+ jti: crypto.randomUUID(),
470
+ iat: Math.floor(Date.now() / 1e3),
471
+ htm: input2.method.toUpperCase(),
472
+ htu: canonicalTargetUrl(input2.targetUrl),
473
+ ath: await sha256Base64Url(input2.accessToken)
474
+ });
475
+ const signature = await crypto.subtle.sign(
476
+ { name: "ECDSA", hash: "SHA-256" },
477
+ key,
478
+ encoder.encode(`${header}.${claims}`)
479
+ );
480
+ return `${header}.${claims}.${encodeBase64Url(signature)}`;
481
+ }
404
482
  function hostnameHasSuffix(hostname, suffix) {
405
483
  return hostname === suffix || hostname.endsWith(`.${suffix}`);
406
484
  }
@@ -574,9 +652,9 @@ function inspectSchema(schema, path, root, depth) {
574
652
  return { path, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` };
575
653
  }
576
654
  const children = [];
577
- if (isRecord(schema.properties)) children.push(...Object.entries(schema.properties));
578
- if (isRecord(schema.$defs)) children.push(...Object.entries(schema.$defs));
579
- if (isRecord(schema.definitions)) children.push(...Object.entries(schema.definitions));
655
+ if (isRecord2(schema.properties)) children.push(...Object.entries(schema.properties));
656
+ if (isRecord2(schema.$defs)) children.push(...Object.entries(schema.$defs));
657
+ if (isRecord2(schema.definitions)) children.push(...Object.entries(schema.definitions));
580
658
  children.push(
581
659
  ["additionalProperties", schema.additionalProperties],
582
660
  ["propertyNames", schema.propertyNames],
@@ -586,7 +664,7 @@ function inspectSchema(schema, path, root, depth) {
586
664
  ...arraySchemaEntries("oneOf", schema.oneOf)
587
665
  );
588
666
  for (const [key, child] of children) {
589
- if (!isRecord(child)) continue;
667
+ if (!isRecord2(child)) continue;
590
668
  const issue = inspectSchema(child, `${path}.${key}`, root, depth + 1);
591
669
  if (issue) return issue;
592
670
  }
@@ -610,7 +688,7 @@ function validateNode(schema, value, path, root, errors, depth) {
610
688
  }
611
689
  if (Array.isArray(schema.allOf)) {
612
690
  for (const candidate of schema.allOf) {
613
- if (isRecord(candidate)) validateNode(candidate, value, path, root, errors, depth + 1);
691
+ if (isRecord2(candidate)) validateNode(candidate, value, path, root, errors, depth + 1);
614
692
  }
615
693
  }
616
694
  if (Array.isArray(schema.anyOf) && !schema.anyOf.some((candidate) => candidateMatches(candidate, value, root, depth))) {
@@ -640,7 +718,7 @@ function validateNode(schema, value, path, root, errors, depth) {
640
718
  if (typeof value === "string") validateString(schema, value, path, errors);
641
719
  if (typeof value === "number") validateNumber(schema, value, path, errors);
642
720
  if (Array.isArray(value)) validateArray(schema, value, path, root, errors, depth);
643
- if (isRecord(value)) validateObject(schema, value, path, root, errors, depth);
721
+ if (isRecord2(value)) validateObject(schema, value, path, root, errors, depth);
644
722
  }
645
723
  function validateObject(schema, value, path, root, errors, depth) {
646
724
  if (typeof schema.minProperties === "number" && Object.keys(value).length < schema.minProperties) {
@@ -649,25 +727,25 @@ function validateObject(schema, value, path, root, errors, depth) {
649
727
  if (typeof schema.maxProperties === "number" && Object.keys(value).length > schema.maxProperties) {
650
728
  errors.push({ path, code: "max_properties", message: `Expected at most ${schema.maxProperties} properties.` });
651
729
  }
652
- const properties = isRecord(schema.properties) ? schema.properties : {};
730
+ const properties = isRecord2(schema.properties) ? schema.properties : {};
653
731
  const required = Array.isArray(schema.required) ? schema.required.map(String) : [];
654
732
  for (const key of required) {
655
733
  if (!Object.prototype.hasOwnProperty.call(value, key)) {
656
734
  errors.push({ path: `${path}.${key}`, code: "required", message: "Required property is missing." });
657
735
  }
658
736
  }
659
- if (isRecord(schema.propertyNames)) {
737
+ if (isRecord2(schema.propertyNames)) {
660
738
  for (const key of Object.keys(value)) {
661
739
  validateNode(schema.propertyNames, key, `${path}.${key}`, root, errors, depth + 1);
662
740
  }
663
741
  }
664
742
  for (const [key, child] of Object.entries(value)) {
665
743
  const childSchema = properties[key];
666
- if (isRecord(childSchema)) {
744
+ if (isRecord2(childSchema)) {
667
745
  validateNode(childSchema, child, `${path}.${key}`, root, errors, depth + 1);
668
746
  } else if (schema.additionalProperties === false) {
669
747
  errors.push({ path: `${path}.${key}`, code: "additional_property", message: "Property is not declared by the action schema." });
670
- } else if (isRecord(schema.additionalProperties)) {
748
+ } else if (isRecord2(schema.additionalProperties)) {
671
749
  validateNode(schema.additionalProperties, child, `${path}.${key}`, root, errors, depth + 1);
672
750
  }
673
751
  }
@@ -683,7 +761,7 @@ function validateArray(schema, value, path, root, errors, depth) {
683
761
  const unique = new Set(value.map((item) => JSON.stringify(item)));
684
762
  if (unique.size !== value.length) errors.push({ path, code: "unique_items", message: "Expected every item to be unique." });
685
763
  }
686
- if (isRecord(schema.items)) {
764
+ if (isRecord2(schema.items)) {
687
765
  value.forEach((item, index) => validateNode(schema.items, item, `${path}[${index}]`, root, errors, depth + 1));
688
766
  }
689
767
  }
@@ -721,7 +799,7 @@ function validateNumber(schema, value, path, errors) {
721
799
  }
722
800
  }
723
801
  function candidateMatches(candidate, value, root, depth) {
724
- if (!isRecord(candidate)) return false;
802
+ if (!isRecord2(candidate)) return false;
725
803
  const issues = [];
726
804
  validateNode(candidate, value, "$", root, issues, depth + 1);
727
805
  return issues.length === 0;
@@ -735,24 +813,24 @@ function resolveLocalReference(root, reference) {
735
813
  current = current[Number(token)];
736
814
  continue;
737
815
  }
738
- if (!isRecord(current) || !Object.prototype.hasOwnProperty.call(current, token)) return null;
816
+ if (!isRecord2(current) || !Object.prototype.hasOwnProperty.call(current, token)) return null;
739
817
  current = current[token];
740
818
  }
741
- return isRecord(current) ? current : null;
819
+ return isRecord2(current) ? current : null;
742
820
  }
743
821
  function matchesType(type, value) {
744
822
  if (type === void 0) return true;
745
823
  if (Array.isArray(type)) return type.some((candidate) => matchesType(candidate, value));
746
824
  if (type === "null") return value === null;
747
825
  if (type === "array") return Array.isArray(value);
748
- if (type === "object") return isRecord(value);
826
+ if (type === "object") return isRecord2(value);
749
827
  if (type === "integer") return typeof value === "number" && Number.isInteger(value);
750
828
  return typeof value === type;
751
829
  }
752
830
  function sameJsonValue(left, right) {
753
831
  return JSON.stringify(left) === JSON.stringify(right);
754
832
  }
755
- function isRecord(value) {
833
+ function isRecord2(value) {
756
834
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
757
835
  }
758
836
  function isUrl(value) {
@@ -987,7 +1065,7 @@ function fail(path, message2) {
987
1065
  throw new ActorEnvelopeValidationError(path, message2);
988
1066
  }
989
1067
  function parseTopoloActionUploadContract(value) {
990
- if (!isRecord2(value) || value.profile !== "inline-base64" && value.profile !== "url-session") {
1068
+ if (!isRecord3(value) || value.profile !== "inline-base64" && value.profile !== "url-session") {
991
1069
  throw new TopoloSdkError("upload_contract_unavailable", "This action does not publish a supported upload contract.");
992
1070
  }
993
1071
  const contentTypeField = requiredField(value.contentTypeField, "contentTypeField");
@@ -1066,17 +1144,17 @@ function prepareTopoloUrlSessionUploadInput(contractValue, byteLength, input2, o
1066
1144
  };
1067
1145
  }
1068
1146
  function topoloUploadResponseValue(response, path) {
1069
- return path.split(".").reduce((value, segment) => isRecord2(value) ? value[segment] : void 0, response);
1147
+ return path.split(".").reduce((value, segment) => isRecord3(value) ? value[segment] : void 0, response);
1070
1148
  }
1071
1149
  function redactTopoloUrlSessionResult(response, uploadUrlPath) {
1072
1150
  const redacted = cloneUploadValue(response);
1073
1151
  let target = redacted;
1074
1152
  const segments = uploadUrlPath.split(".");
1075
1153
  for (const segment of segments.slice(0, -1)) {
1076
- target = isRecord2(target) ? target[segment] : void 0;
1154
+ target = isRecord3(target) ? target[segment] : void 0;
1077
1155
  }
1078
1156
  const finalSegment = segments.at(-1);
1079
- if (finalSegment && isRecord2(target) && finalSegment in target) target[finalSegment] = "[REDACTED]";
1157
+ if (finalSegment && isRecord3(target) && finalSegment in target) target[finalSegment] = "[REDACTED]";
1080
1158
  redactUploadHeaders(redacted);
1081
1159
  return redacted;
1082
1160
  }
@@ -1100,7 +1178,7 @@ function mapUploadInput(contract, contentBase64, input2, options) {
1100
1178
  }
1101
1179
  function cloneUploadValue(value) {
1102
1180
  if (Array.isArray(value)) return value.map(cloneUploadValue);
1103
- if (!isRecord2(value)) return value;
1181
+ if (!isRecord3(value)) return value;
1104
1182
  return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneUploadValue(item)]));
1105
1183
  }
1106
1184
  function redactUploadHeaders(value) {
@@ -1108,9 +1186,9 @@ function redactUploadHeaders(value) {
1108
1186
  value.forEach(redactUploadHeaders);
1109
1187
  return;
1110
1188
  }
1111
- if (!isRecord2(value)) return;
1189
+ if (!isRecord3(value)) return;
1112
1190
  for (const [key, item] of Object.entries(value)) {
1113
- if (key.toLowerCase() === "headers" && isRecord2(item)) {
1191
+ if (key.toLowerCase() === "headers" && isRecord3(item)) {
1114
1192
  for (const header of Object.keys(item)) {
1115
1193
  if (["authorization", "cookie", "set-cookie", "proxy-authorization", "x-api-key"].includes(header.toLowerCase())) {
1116
1194
  item[header] = "[REDACTED]";
@@ -1139,7 +1217,7 @@ function decodedBase64Size(contentBase64) {
1139
1217
  return contentBase64.length / 4 * 3 - padding;
1140
1218
  }
1141
1219
  function parseCompleteContract(value) {
1142
- if (!isRecord2(value)) throw new TopoloSdkError("invalid_upload_contract", "The action upload completion contract is invalid.");
1220
+ if (!isRecord3(value)) throw new TopoloSdkError("invalid_upload_contract", "The action upload completion contract is invalid.");
1143
1221
  return {
1144
1222
  actionId: requiredPath(value.actionId, "complete.actionId"),
1145
1223
  inputField: requiredField(value.inputField, "complete.inputField"),
@@ -1177,7 +1255,7 @@ function encodeTopoloActionBase64(bytes) {
1177
1255
  }
1178
1256
  return btoa(binary);
1179
1257
  }
1180
- function isRecord2(value) {
1258
+ function isRecord3(value) {
1181
1259
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1182
1260
  }
1183
1261
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
@@ -1188,6 +1266,8 @@ var TOPOLO_RESOURCE_ID_HEADER = "X-Topolo-Resource-ID";
1188
1266
  var POSITIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
1189
1267
  var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
1190
1268
  var TopoloClient = class _TopoloClient {
1269
+ actionSecretStore;
1270
+ actionSecrets = /* @__PURE__ */ new Map();
1191
1271
  credential;
1192
1272
  agent;
1193
1273
  actorContext;
@@ -1217,6 +1297,7 @@ var TopoloClient = class _TopoloClient {
1217
1297
  _TopoloClient.appIdCache.clear();
1218
1298
  }
1219
1299
  constructor(options) {
1300
+ this.actionSecretStore = options.actionSecretStore;
1220
1301
  if (!options.credential) throw new TopoloAuthError("credential is required");
1221
1302
  if (!options.agent?.clientName) throw new TopoloAuthError("agent.clientName is required");
1222
1303
  this.credential = options.credential;
@@ -1238,6 +1319,7 @@ var TopoloClient = class _TopoloClient {
1238
1319
  }
1239
1320
  emit(event) {
1240
1321
  if (!this.debug) return;
1322
+ event = maskActionSecretValue(event, this.actionSecrets);
1241
1323
  try {
1242
1324
  this.debug(event);
1243
1325
  } catch {
@@ -1623,7 +1705,7 @@ var TopoloClient = class _TopoloClient {
1623
1705
  }
1624
1706
  const entries = [];
1625
1707
  for (const item of raw) {
1626
- if (!isRecord3(item)) continue;
1708
+ if (!isRecord4(item)) continue;
1627
1709
  const s = item;
1628
1710
  const appId = typeof s.app_id === "string" ? s.app_id : typeof s.id === "string" ? s.id : null;
1629
1711
  if (!appId) continue;
@@ -1698,8 +1780,8 @@ var TopoloClient = class _TopoloClient {
1698
1780
  "app_topolo_auth.service_api_key_resources.list",
1699
1781
  { appId: service.appId }
1700
1782
  );
1701
- const payload = isRecord3(response) && isRecord3(response.data) ? response.data : response;
1702
- if (!isRecord3(payload) || !Array.isArray(payload.resources)) {
1783
+ const payload = isRecord4(response) && isRecord4(response.data) ? response.data : response;
1784
+ if (!isRecord4(payload) || !Array.isArray(payload.resources)) {
1703
1785
  throw new TopoloSdkError("resource_catalog_unavailable", "TopoloAuth did not return bindable resources.");
1704
1786
  }
1705
1787
  return {
@@ -1880,17 +1962,20 @@ var TopoloClient = class _TopoloClient {
1880
1962
  }
1881
1963
  async validateActionInput(action, input2 = {}) {
1882
1964
  const entry = await this.getAction(action);
1883
- return { action: entry, validation: validateTopoloActionInput(entry.inputSchema, input2) };
1965
+ const resolved = await resolveActionSecrets(input2, this.actionSecretStore, this.actionSecrets);
1966
+ return { action: entry, validation: validateTopoloActionInput(entry.inputSchema, resolved) };
1884
1967
  }
1885
1968
  async planAction(action, input2 = {}, options = {}) {
1886
1969
  const entry = await this.getAction(action);
1887
- return this.planResolvedAction(entry, input2, options);
1970
+ const resolved = await resolveActionSecrets(input2, this.actionSecretStore, this.actionSecrets);
1971
+ return this.planResolvedAction(entry, resolved, options);
1888
1972
  }
1889
1973
  async prepareAction(action, input2, options = {}) {
1890
1974
  const entry = await this.getAction(action);
1891
1975
  const example = (entry.agentContract?.examples ?? []).find((candidate) => Boolean(candidate?.input) && typeof candidate.input === "object" && !Array.isArray(candidate.input) && validateTopoloActionInput(entry.inputSchema, candidate.input).valid) ?? null;
1892
1976
  const selectedInput = input2 ?? example?.input ?? {};
1893
- const plan = this.planResolvedAction(entry, selectedInput, options);
1977
+ const resolved = await resolveActionSecrets(selectedInput, this.actionSecretStore, this.actionSecrets);
1978
+ const plan = this.planResolvedAction(entry, resolved, options);
1894
1979
  return {
1895
1980
  action: entry,
1896
1981
  example,
@@ -1927,7 +2012,7 @@ var TopoloClient = class _TopoloClient {
1927
2012
  confirmationRequired,
1928
2013
  confirmationProvided: options.confirm === true,
1929
2014
  executable: validation.valid && agentAccess !== "off" && (!confirmationRequired || options.confirm === true),
1930
- request,
2015
+ request: maskActionSecretValue(request, this.actionSecrets),
1931
2016
  verification: stringArray(entry.agentContract?.verify),
1932
2017
  rollbackActionId: pickActionString(entry.agentContract?.rollbackActionId),
1933
2018
  nextActionIds: stringArray(entry.agentContract?.nextActionIds)
@@ -1957,6 +2042,7 @@ var TopoloClient = class _TopoloClient {
1957
2042
  `Topolo action "${entry.actionId}" requires { confirm: true } before invocation.`
1958
2043
  );
1959
2044
  }
2045
+ input2 = await resolveActionSecrets(input2, this.actionSecretStore, this.actionSecrets);
1960
2046
  const validation = validateTopoloActionInput(entry.inputSchema, input2);
1961
2047
  if (!validation.valid) {
1962
2048
  throw new TopoloSdkError(
@@ -1987,22 +2073,31 @@ var TopoloClient = class _TopoloClient {
1987
2073
  }
1988
2074
  }
1989
2075
  const service = entry.appSlug || entry.appId;
1990
- const result = await this.request({
1991
- service,
1992
- ...entry.apiBaseUrl ? { baseUrl: entry.apiBaseUrl } : {},
1993
- method: entry.method,
1994
- path: mapped.path,
1995
- ...mapped.query ? { query: mapped.query } : {},
1996
- ...mapped.body !== void 0 ? { body: mapped.body } : {},
1997
- ...options.resource ? { resource: options.resource } : {},
1998
- ...resolvedActorEnvelope ? { actorEnvelope: resolvedActorEnvelope } : {},
1999
- headers: {
2000
- [TOPOLO_ACTION_ID_HEADER]: entry.actionId,
2001
- [TOPOLO_ACTION_NAME_HEADER]: entry.name
2002
- },
2003
- confirm: options.confirm === true || entry.readOnly && !entry.destructive && !entry.requiresConfirmation
2004
- });
2005
- return preserveSensitiveResult ? result : redactActionResult(result);
2076
+ let result;
2077
+ try {
2078
+ result = await this.request({
2079
+ service,
2080
+ ...entry.apiBaseUrl ? { baseUrl: entry.apiBaseUrl } : {},
2081
+ method: entry.method,
2082
+ path: mapped.path,
2083
+ ...mapped.query ? { query: mapped.query } : {},
2084
+ ...mapped.body !== void 0 ? { body: mapped.body } : {},
2085
+ ...options.resource ? { resource: options.resource } : {},
2086
+ ...resolvedActorEnvelope ? { actorEnvelope: resolvedActorEnvelope } : {},
2087
+ headers: {
2088
+ [TOPOLO_ACTION_ID_HEADER]: entry.actionId,
2089
+ [TOPOLO_ACTION_NAME_HEADER]: entry.name
2090
+ },
2091
+ confirm: options.confirm === true || entry.readOnly && !entry.destructive && !entry.requiresConfirmation
2092
+ });
2093
+ } catch (error) {
2094
+ throw maskActionSecretError(error, this.actionSecrets);
2095
+ }
2096
+ return preserveSensitiveResult ? result : await protectActionResult(result, this.actionSecretStore);
2097
+ }
2098
+ async releaseActionSecret(reference) {
2099
+ if (!this.actionSecretStore) throw new TopoloSdkError("action_secret_store_required", "Protected action references require a secret store.");
2100
+ await this.actionSecretStore.delete(reference);
2006
2101
  }
2007
2102
  async buildVerifiedAgentActorEnvelope(actionId, input2, resource) {
2008
2103
  if (!this.actorContext) return void 0;
@@ -2149,7 +2244,7 @@ function resolveConfiguredAppId(slug, appIds) {
2149
2244
  return null;
2150
2245
  }
2151
2246
  function normalizeApiKeyResourceMetadata(input2) {
2152
- if (!isRecord3(input2)) return null;
2247
+ if (!isRecord4(input2)) return null;
2153
2248
  const resourceType = readString(input2.resourceType ?? input2.resource_type);
2154
2249
  const label = readString(input2.label);
2155
2250
  const emptyLabel = readString(input2.emptyLabel ?? input2.empty_label);
@@ -2183,7 +2278,7 @@ function normalizeApiKeyResourceMetadataList(input2) {
2183
2278
  return resources;
2184
2279
  }
2185
2280
  function normalizeBindableResource(input2) {
2186
- if (!isRecord3(input2)) return null;
2281
+ if (!isRecord4(input2)) return null;
2187
2282
  const resourceType = readString(input2.resourceType ?? input2.resource_type);
2188
2283
  const resourceId = readString(input2.resourceId ?? input2.resource_id);
2189
2284
  if (!resourceType || !resourceId) return null;
@@ -2194,13 +2289,13 @@ function normalizeBindableResource(input2) {
2194
2289
  slug: readOptionalString(input2.slug),
2195
2290
  status: readOptionalString(input2.status),
2196
2291
  organizationId: readOptionalString(input2.organizationId ?? input2.organization_id),
2197
- metadata: isRecord3(input2.metadata) ? input2.metadata : {}
2292
+ metadata: isRecord4(input2.metadata) ? input2.metadata : {}
2198
2293
  };
2199
2294
  }
2200
2295
  function normalizeResourceBindings(input2) {
2201
2296
  if (!Array.isArray(input2)) return [];
2202
2297
  return input2.flatMap((binding) => {
2203
- if (!isRecord3(binding)) return [];
2298
+ if (!isRecord4(binding)) return [];
2204
2299
  const resourceType = readString(binding.resourceType ?? binding.resource_type);
2205
2300
  const resourceId = readString(binding.resourceId ?? binding.resource_id);
2206
2301
  return resourceType && resourceId ? [{ resourceType, resourceId }] : [];
@@ -2213,30 +2308,9 @@ function readOptionalString(value) {
2213
2308
  const normalized = readString(value);
2214
2309
  return normalized || null;
2215
2310
  }
2216
- function isRecord3(value) {
2311
+ function isRecord4(value) {
2217
2312
  return typeof value === "object" && value !== null && !Array.isArray(value);
2218
2313
  }
2219
- function redactActionResult(value) {
2220
- if (Array.isArray(value)) return value.map(redactActionResult);
2221
- if (!isRecord3(value)) return value;
2222
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [
2223
- key,
2224
- isSensitiveActionResultKey(key, item) ? "[REDACTED]" : redactActionResult(item)
2225
- ]));
2226
- }
2227
- function isSensitiveActionResultKey(key, value) {
2228
- const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
2229
- if (normalized === "capability") return isCredentialShaped(value);
2230
- return ["authorization", "cookie", "setcookie", "proxyauthorization"].includes(normalized) || /(token|secret|password|privatekey|apikey)$/.test(normalized);
2231
- }
2232
- var PLAIN_ENUM_VALUE = /^[a-z][a-z0-9_-]{0,31}$/;
2233
- function isCredentialShaped(value) {
2234
- if (typeof value !== "string") return value !== null && value !== void 0 && typeof value !== "boolean" && typeof value !== "number";
2235
- const trimmed = value.trim();
2236
- if (!trimmed) return false;
2237
- if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) return true;
2238
- return !PLAIN_ENUM_VALUE.test(trimmed);
2239
- }
2240
2314
  function resolveConfiguredServiceUrl(slug, serviceUrls) {
2241
2315
  for (const serviceKey of serviceKeyCandidates2(slug)) {
2242
2316
  const configured = serviceUrls?.[serviceKey];
@@ -2306,14 +2380,14 @@ function catalogSearchQuery(options) {
2306
2380
  return query;
2307
2381
  }
2308
2382
  function normalizeCatalogPage(value) {
2309
- if (!isRecord3(value)) return { nextCursor: null, hasMore: false };
2383
+ if (!isRecord4(value)) return { nextCursor: null, hasMore: false };
2310
2384
  return {
2311
2385
  nextCursor: typeof value.nextCursor === "string" && value.nextCursor ? value.nextCursor : null,
2312
2386
  hasMore: value.hasMore === true
2313
2387
  };
2314
2388
  }
2315
2389
  function normalizeServiceCatalogEntry(item) {
2316
- if (!isRecord3(item)) return null;
2390
+ if (!isRecord4(item)) return null;
2317
2391
  const appId = typeof item.app_id === "string" ? item.app_id : typeof item.id === "string" ? item.id : null;
2318
2392
  if (!appId) return null;
2319
2393
  const permissions = Array.isArray(item.granted_permissions) ? item.granted_permissions.filter(
@@ -3342,11 +3416,13 @@ function createTopolo(options) {
3342
3416
  };
3343
3417
  }
3344
3418
 
3345
- // node_modules/@topolo/sdk/dist/node/credential-store.js
3419
+ // node_modules/.pnpm/@topolo+sdk@0.21.4/node_modules/@topolo/sdk/dist/node/credential-store.js
3346
3420
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2, rm as rm2, stat } from "fs/promises";
3347
3421
  import { existsSync as existsSync2 } from "fs";
3348
3422
  import { homedir as homedir2, platform } from "os";
3349
- import { dirname as dirname2, join as join2 } from "path";
3423
+ import { dirname as dirname2, join as join3 } from "path";
3424
+ import { createHash, randomUUID } from "crypto";
3425
+ import { join as join2 } from "path";
3350
3426
  import { spawnSync } from "child_process";
3351
3427
  import { mkdir, readFile, readdir, rename, rm, writeFile, chmod } from "fs/promises";
3352
3428
  import { existsSync } from "fs";
@@ -3698,67 +3774,7 @@ var TopoloOAuth2 = class {
3698
3774
  return parsed;
3699
3775
  }
3700
3776
  };
3701
- var TOPOLO_RUNTIME_ENVIRONMENTS = ["production", "staging", "development", "local"];
3702
- var TOPOLO_ENVIRONMENT_SERVICE_URLS = {
3703
- production: {
3704
- auth: "https://auth.topolo.app",
3705
- developers: "https://developers.topolo.app"
3706
- },
3707
- staging: {
3708
- auth: "https://auth.stg.topolo.us",
3709
- developers: "https://developers.stg.topolo.us"
3710
- },
3711
- development: {
3712
- auth: "https://auth.topolo.dev",
3713
- developers: "https://developers.topolo.dev"
3714
- },
3715
- local: {}
3716
- };
3717
- function hostnameHasSuffix2(hostname, suffix) {
3718
- return hostname === suffix || hostname.endsWith(`.${suffix}`);
3719
- }
3720
- function workersDevAccountMatches2(hostname, account) {
3721
- if (!hostname.endsWith(".workers.dev")) return false;
3722
- const labels = hostname.slice(0, -".workers.dev".length).split(".");
3723
- return labels.length >= 2 && labels.at(-1) === account;
3724
- }
3725
- function originMatchesEnvironment2(url, environment) {
3726
- const hostname = url.hostname.toLowerCase();
3727
- if (environment === "local") {
3728
- return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "[::1]";
3729
- }
3730
- if (url.protocol !== "https:") return false;
3731
- if (environment === "production") {
3732
- return hostnameHasSuffix2(hostname, "topolo.app") || hostnameHasSuffix2(hostname, "topolo.io") || hostname === "api.ol0.me" || workersDevAccountMatches2(hostname, "topolo");
3733
- }
3734
- if (environment === "staging") {
3735
- return hostnameHasSuffix2(hostname, "stg.topolo.us") || workersDevAccountMatches2(hostname, "topolo-staging");
3736
- }
3737
- return hostnameHasSuffix2(hostname, "topolo.dev") || workersDevAccountMatches2(hostname, "topolo-development");
3738
- }
3739
- function assertTopoloServiceUrlEnvironment2(service, value, environment) {
3740
- let url;
3741
- try {
3742
- url = new URL(value);
3743
- } catch {
3744
- throw new TopoloSdkError2("invalid_service_url", `Invalid URL configured for Topolo service "${service}".`);
3745
- }
3746
- if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || !originMatchesEnvironment2(url, environment)) {
3747
- throw new TopoloSdkError2(
3748
- "environment_mismatch",
3749
- `Refusing Topolo service "${service}": ${url.origin} does not belong to ${environment}.`
3750
- );
3751
- }
3752
- return url.origin;
3753
- }
3754
- function topoloServiceOrigins2(serviceUrls, environment) {
3755
- return Object.fromEntries(
3756
- Object.entries(serviceUrls ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([service, value]) => [
3757
- service,
3758
- assertTopoloServiceUrlEnvironment2(service, value, environment)
3759
- ])
3760
- );
3761
- }
3777
+ var ACTION_SECRET_PREFIX2 = "topolo-secret:";
3762
3778
  var KEYCHAIN_SERVICE = "topolo-external-agent-installation";
3763
3779
  var MACOS_KEYCHAIN_CHUNK_SIZE = 96;
3764
3780
  var MACOS_KEYCHAIN_CHUNK_PREFIX = "topolo-keychain-chunks:v1:";
@@ -3801,6 +3817,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
3801
3817
  const platform22 = options.platform ?? hostPlatform();
3802
3818
  const spawn3 = options.spawn ?? ((command, args, spawnOptions) => spawnSync(command, args, spawnOptions));
3803
3819
  const root = options.root ?? installationStorePath();
3820
+ const service = options.service ?? KEYCHAIN_SERVICE;
3804
3821
  if (platform22 === "darwin") {
3805
3822
  const write = (account, value) => runOrThrow(spawn3("/usr/bin/expect", ["-c", MACOS_KEYCHAIN_WRITE_SCRIPT], {
3806
3823
  input: `${value}
@@ -3809,7 +3826,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
3809
3826
  env: {
3810
3827
  ...process.env,
3811
3828
  TOPOLO_KEYCHAIN_ACCOUNT: account,
3812
- TOPOLO_KEYCHAIN_SERVICE: KEYCHAIN_SERVICE
3829
+ TOPOLO_KEYCHAIN_SERVICE: service
3813
3830
  }
3814
3831
  }), "macOS Keychain write");
3815
3832
  const read = (account) => {
@@ -3818,7 +3835,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
3818
3835
  "-a",
3819
3836
  account,
3820
3837
  "-s",
3821
- KEYCHAIN_SERVICE,
3838
+ service,
3822
3839
  "-w"
3823
3840
  ], { encoding: "utf8" });
3824
3841
  if (result.status === 44) return null;
@@ -3830,7 +3847,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
3830
3847
  "-a",
3831
3848
  account,
3832
3849
  "-s",
3833
- KEYCHAIN_SERVICE
3850
+ service
3834
3851
  ], { encoding: "utf8" });
3835
3852
  if (result.status !== 44) runOrThrow(result, "macOS Keychain delete");
3836
3853
  };
@@ -3877,9 +3894,9 @@ function createPlatformInstallationSecretBackend(options = {}) {
3877
3894
  async set(account, value) {
3878
3895
  runOrThrow(spawn3("secret-tool", [
3879
3896
  "store",
3880
- "--label=Topolo external agent installation",
3897
+ "--label=Topolo protected credential",
3881
3898
  "service",
3882
- KEYCHAIN_SERVICE,
3899
+ service,
3883
3900
  "account",
3884
3901
  account
3885
3902
  ], { input: value, encoding: "utf8" }), "Linux Secret Service write");
@@ -3888,7 +3905,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
3888
3905
  const result = spawn3("secret-tool", [
3889
3906
  "lookup",
3890
3907
  "service",
3891
- KEYCHAIN_SERVICE,
3908
+ service,
3892
3909
  "account",
3893
3910
  account
3894
3911
  ], { encoding: "utf8" });
@@ -3899,7 +3916,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
3899
3916
  const result = spawn3("secret-tool", [
3900
3917
  "clear",
3901
3918
  "service",
3902
- KEYCHAIN_SERVICE,
3919
+ service,
3903
3920
  "account",
3904
3921
  account
3905
3922
  ], { encoding: "utf8" });
@@ -3961,6 +3978,114 @@ async function deleteExternalAgentInstallation(environment, id, backend = create
3961
3978
  await backend.delete(accountName(environment, id));
3962
3979
  await rm(installationPath(environment, id), { force: true });
3963
3980
  }
3981
+ function createProtectedActionSecretStore(options) {
3982
+ const backend = options.backend ?? createPlatformInstallationSecretBackend({
3983
+ service: "topolo-action-handoff",
3984
+ root: join2(installationStorePath(), "action-handoff")
3985
+ });
3986
+ const now = options.now ?? Date.now;
3987
+ async function account(reference) {
3988
+ if (!/^topolo-secret:[0-9a-f-]{36}$/.test(reference)) throw new TopoloSdkError2("invalid_action_secret_reference", "Invalid protected action reference.");
3989
+ const identity = await options.identity();
3990
+ if (!identity.user.id) throw new TopoloSdkError2("action_secret_identity_required", "A verified identity is required for protected action handoff.");
3991
+ if (identity.kind === "api_key" && !identity.apiKey?.id) throw new TopoloSdkError2("action_secret_identity_required", "A verified API key identity is required for protected action handoff.");
3992
+ const scope = createHash("sha256").update(JSON.stringify([
3993
+ options.environment,
3994
+ identity.user.principalId ?? identity.user.id,
3995
+ identity.organization?.id ?? null,
3996
+ identity.apiKey?.id ?? null
3997
+ ])).digest("hex");
3998
+ return `${scope}-${reference.slice(ACTION_SECRET_PREFIX2.length)}`;
3999
+ }
4000
+ return {
4001
+ async save(value) {
4002
+ const reference = `${ACTION_SECRET_PREFIX2}${randomUUID()}`;
4003
+ await backend.set(await account(reference), JSON.stringify({ value, expiresAt: now() + 30 * 60 * 1e3 }));
4004
+ return reference;
4005
+ },
4006
+ async load(reference) {
4007
+ const key = await account(reference);
4008
+ const encoded = await backend.get(key);
4009
+ if (!encoded) throw new TopoloSdkError2("action_secret_unavailable", "Protected action reference is missing or belongs to another identity or environment.");
4010
+ let record2;
4011
+ try {
4012
+ record2 = JSON.parse(encoded);
4013
+ } catch {
4014
+ throw new TopoloSdkError2("action_secret_invalid", "Protected action reference is corrupt.");
4015
+ }
4016
+ if (!Number.isFinite(record2?.expiresAt) || !Object.hasOwn(record2, "value")) throw new TopoloSdkError2("action_secret_invalid", "Protected action reference is corrupt.");
4017
+ if (record2.expiresAt <= now()) {
4018
+ await backend.delete(key);
4019
+ throw new TopoloSdkError2("action_secret_expired", "Protected action reference expired. Repeat the originating action if its contract permits.");
4020
+ }
4021
+ return record2.value;
4022
+ },
4023
+ async delete(reference) {
4024
+ await backend.delete(await account(reference));
4025
+ }
4026
+ };
4027
+ }
4028
+ var TOPOLO_RUNTIME_ENVIRONMENTS = ["production", "staging", "development", "local"];
4029
+ var TOPOLO_ENVIRONMENT_SERVICE_URLS = {
4030
+ production: {
4031
+ auth: "https://auth.topolo.app",
4032
+ developers: "https://developers.topolo.app"
4033
+ },
4034
+ staging: {
4035
+ auth: "https://auth.stg.topolo.us",
4036
+ developers: "https://developers.stg.topolo.us"
4037
+ },
4038
+ development: {
4039
+ auth: "https://auth.topolo.dev",
4040
+ developers: "https://developers.topolo.dev"
4041
+ },
4042
+ local: {}
4043
+ };
4044
+ function hostnameHasSuffix2(hostname, suffix) {
4045
+ return hostname === suffix || hostname.endsWith(`.${suffix}`);
4046
+ }
4047
+ function workersDevAccountMatches2(hostname, account) {
4048
+ if (!hostname.endsWith(".workers.dev")) return false;
4049
+ const labels = hostname.slice(0, -".workers.dev".length).split(".");
4050
+ return labels.length >= 2 && labels.at(-1) === account;
4051
+ }
4052
+ function originMatchesEnvironment2(url, environment) {
4053
+ const hostname = url.hostname.toLowerCase();
4054
+ if (environment === "local") {
4055
+ return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "[::1]";
4056
+ }
4057
+ if (url.protocol !== "https:") return false;
4058
+ if (environment === "production") {
4059
+ return hostnameHasSuffix2(hostname, "topolo.app") || hostnameHasSuffix2(hostname, "topolo.io") || hostname === "api.ol0.me" || workersDevAccountMatches2(hostname, "topolo");
4060
+ }
4061
+ if (environment === "staging") {
4062
+ return hostnameHasSuffix2(hostname, "stg.topolo.us") || workersDevAccountMatches2(hostname, "topolo-staging");
4063
+ }
4064
+ return hostnameHasSuffix2(hostname, "topolo.dev") || workersDevAccountMatches2(hostname, "topolo-development");
4065
+ }
4066
+ function assertTopoloServiceUrlEnvironment2(service, value, environment) {
4067
+ let url;
4068
+ try {
4069
+ url = new URL(value);
4070
+ } catch {
4071
+ throw new TopoloSdkError2("invalid_service_url", `Invalid URL configured for Topolo service "${service}".`);
4072
+ }
4073
+ if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || !originMatchesEnvironment2(url, environment)) {
4074
+ throw new TopoloSdkError2(
4075
+ "environment_mismatch",
4076
+ `Refusing Topolo service "${service}": ${url.origin} does not belong to ${environment}.`
4077
+ );
4078
+ }
4079
+ return url.origin;
4080
+ }
4081
+ function topoloServiceOrigins2(serviceUrls, environment) {
4082
+ return Object.fromEntries(
4083
+ Object.entries(serviceUrls ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([service, value]) => [
4084
+ service,
4085
+ assertTopoloServiceUrlEnvironment2(service, value, environment)
4086
+ ])
4087
+ );
4088
+ }
3964
4089
  var TOPOLO_ENVIRONMENTS = TOPOLO_RUNTIME_ENVIRONMENTS;
3965
4090
  function contextKey(contextType, organizationId) {
3966
4091
  return contextType === "organization" && organizationId ? `org:${organizationId}` : "personal";
@@ -3969,8 +4094,8 @@ var ENVIRONMENT_DEFAULT_SERVICE_URLS = TOPOLO_ENVIRONMENT_SERVICE_URLS;
3969
4094
  function configPath() {
3970
4095
  const override = process.env.TOPOLO_CONFIG_PATH;
3971
4096
  if (override) return override;
3972
- const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ? join2(homedir2(), "AppData", "Roaming") : join2(homedir2(), ".config"));
3973
- return join2(base, "topolo", "config.json");
4097
+ const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ? join3(homedir2(), "AppData", "Roaming") : join3(homedir2(), ".config"));
4098
+ return join3(base, "topolo", "config.json");
3974
4099
  }
3975
4100
  async function readConfig() {
3976
4101
  const path = configPath();
@@ -4373,14 +4498,14 @@ function normalizeError(err) {
4373
4498
 
4374
4499
  // src/version.ts
4375
4500
  import { readFileSync } from "fs";
4376
- import { dirname as dirname3, join as join3 } from "path";
4501
+ import { dirname as dirname3, join as join4 } from "path";
4377
4502
  import { fileURLToPath } from "url";
4378
4503
  function readCliVersion() {
4379
4504
  try {
4380
4505
  const here = dirname3(fileURLToPath(import.meta.url));
4381
4506
  const candidates = [
4382
- join3(here, "package.json"),
4383
- join3(here, "..", "package.json")
4507
+ join4(here, "package.json"),
4508
+ join4(here, "..", "package.json")
4384
4509
  ];
4385
4510
  for (const path of candidates) {
4386
4511
  try {
@@ -4662,13 +4787,15 @@ function sleep(ms) {
4662
4787
  // src/client.ts
4663
4788
  async function buildClient(options) {
4664
4789
  const environment = options?.environment ?? selectedEnvironment();
4665
- const credential = options?.context ? await refreshContextCredentialIfNeeded(environment, options.context) : await refreshCredentialIfNeeded(environment);
4790
+ const context = options?.context ?? process.env.TOPOLO_CONTEXT?.trim();
4791
+ const credential = context ? await refreshContextCredentialIfNeeded(environment, context) : await refreshCredentialIfNeeded(environment);
4666
4792
  if (!credential) return null;
4667
4793
  const config = await readConfig();
4668
4794
  const profile = resolvedProfileConfig(config, environment);
4669
- return createTopolo({
4795
+ const topolo = createTopolo({
4670
4796
  credential,
4671
4797
  environment,
4798
+ actionSecretStore: createProtectedActionSecretStore({ environment, identity: () => topolo.client.introspect() }),
4672
4799
  actorContext: { channel: "cli" },
4673
4800
  agent: {
4674
4801
  clientName: "topolo-cli",
@@ -4678,10 +4805,11 @@ async function buildClient(options) {
4678
4805
  serviceUrls: profile.serviceUrls,
4679
4806
  requireConfirmForWrites: options?.requireConfirmForWrites,
4680
4807
  ...credential.kind === "access_token" && credential.refreshToken ? {
4681
- refreshCredential: () => options?.context ? refreshContextCredentialIfNeeded(environment, options.context, { force: true }) : refreshCredentialIfNeeded(environment, { force: true })
4808
+ refreshCredential: () => context ? refreshContextCredentialIfNeeded(environment, context, { force: true }) : refreshCredentialIfNeeded(environment, { force: true })
4682
4809
  } : {},
4683
4810
  ...isDebugEnabled() ? { debug: stderrDebugLogger } : {}
4684
4811
  });
4812
+ return topolo;
4685
4813
  }
4686
4814
  function isDebugEnabled() {
4687
4815
  const v = process.env.TOPOLO_DEBUG;
@@ -4877,6 +5005,18 @@ function registerActionsCommand(program2) {
4877
5005
  fail2(err, mode);
4878
5006
  }
4879
5007
  });
5008
+ actions.command("release-secret <reference>").description("Delete a local protected action reference for the current verified identity and environment").option("--confirm", "Acknowledge local reference deletion; does not revoke the upstream credential").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (reference, opts, command) => {
5009
+ const mode = resolveOutputMode(opts.json ?? command.parent?.opts().json);
5010
+ try {
5011
+ if (opts.confirm !== true) throw new Error("Releasing a protected action reference requires --confirm.");
5012
+ const topolo = await buildClient();
5013
+ if (!topolo) throw new Error("No credential configured. Run `topolo auth login`.");
5014
+ await topolo.client.releaseActionSecret(reference);
5015
+ emit({ released: true }, mode, () => "Protected action reference released.");
5016
+ } catch (error) {
5017
+ fail2(error, mode);
5018
+ }
5019
+ });
4880
5020
  actions.command("get <action>").description("Describe one Topolo action by exact action id").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (action, opts, command) => {
4881
5021
  const mode = resolveOutputMode(opts.json ?? command.parent?.opts().json);
4882
5022
  try {
@@ -5078,7 +5218,7 @@ async function actionExecutionContext(topolo) {
5078
5218
  const source = credentialSourceFromConfig(config, environment);
5079
5219
  return createTopoloAgentExecutionContext({
5080
5220
  environment,
5081
- contextKey: source.startsWith("env:") ? null : profile.activeContext ?? null,
5221
+ contextKey: source.startsWith("env:") ? null : process.env.TOPOLO_CONTEXT?.trim() || profile.activeContext || null,
5082
5222
  identity: await topolo.identity.whoami(),
5083
5223
  serviceUrls: resolvedProfileConfig(config, environment).serviceUrls
5084
5224
  });
@@ -5522,7 +5662,7 @@ function normalizeList(values, fallback = []) {
5522
5662
 
5523
5663
  // src/commands/integrations.ts
5524
5664
  import { readdir as readdir2, readFile as readFile3, stat as stat2 } from "fs/promises";
5525
- import { join as join4, relative } from "path";
5665
+ import { join as join5, relative } from "path";
5526
5666
  function registerIntegrationsCommand(program2) {
5527
5667
  const integrations = program2.command("integrations").description("Generate and validate Topolo integration handoffs.");
5528
5668
  integrations.command("prompt").description("Print an agent prompt for adding a Topolo integration.").argument("<integration>", "Integration prompt id, currently login-with-topolo").option("--framework <framework>", "auto, authjs, express, or generic", "auto").option("--json", "Emit JSON output").action((integration, options) => {
@@ -5665,7 +5805,7 @@ function detectFramework(packageJson, haystack) {
5665
5805
  }
5666
5806
  async function readPackageJson(repoRoot) {
5667
5807
  try {
5668
- return JSON.parse(await readFile3(join4(repoRoot, "package.json"), "utf8"));
5808
+ return JSON.parse(await readFile3(join5(repoRoot, "package.json"), "utf8"));
5669
5809
  } catch {
5670
5810
  return null;
5671
5811
  }
@@ -5677,7 +5817,7 @@ async function collectFiles(repoRoot) {
5677
5817
  if (result.length > 2e3) return;
5678
5818
  const entries = await readdir2(dir, { withFileTypes: true });
5679
5819
  for (const entry of entries) {
5680
- const fullPath = join4(dir, entry.name);
5820
+ const fullPath = join5(dir, entry.name);
5681
5821
  if (entry.isDirectory()) {
5682
5822
  if ([".git", "node_modules", "dist", "build", ".next", "coverage"].includes(entry.name)) continue;
5683
5823
  await walk(fullPath);
@@ -5708,7 +5848,7 @@ import { spawnSync as spawnSync3 } from "child_process";
5708
5848
  import { existsSync as existsSync5 } from "fs";
5709
5849
  import { readFile as readFile6, rm as rm3 } from "fs/promises";
5710
5850
  import { homedir as homedir4 } from "os";
5711
- import { dirname as dirname5, join as join6, resolve } from "path";
5851
+ import { dirname as dirname5, join as join7, resolve } from "path";
5712
5852
  import { fileURLToPath as fileURLToPath2 } from "url";
5713
5853
  import { createRequire } from "module";
5714
5854
 
@@ -5717,7 +5857,7 @@ import { spawnSync as spawnSync2 } from "child_process";
5717
5857
  import { existsSync as existsSync3 } from "fs";
5718
5858
  import { readFile as readFile4 } from "fs/promises";
5719
5859
  import { homedir as homedir3 } from "os";
5720
- import { isAbsolute, join as join5 } from "path";
5860
+ import { isAbsolute, join as join6 } from "path";
5721
5861
  var SUPPORTED_AGENT_IDS = [
5722
5862
  "claude",
5723
5863
  "claude-desktop",
@@ -5775,23 +5915,23 @@ function stableNodeCommand() {
5775
5915
  function agentSkillPath(agent, homeDir = homedir3(), projectDir) {
5776
5916
  switch (agent) {
5777
5917
  case "claude":
5778
- return projectDir ? join5(projectDir, ".claude", "skills", "topolo", "SKILL.md") : join5(homeDir, ".claude", "skills", "topolo", "SKILL.md");
5918
+ return projectDir ? join6(projectDir, ".claude", "skills", "topolo", "SKILL.md") : join6(homeDir, ".claude", "skills", "topolo", "SKILL.md");
5779
5919
  case "claude-desktop":
5780
- return join5(homeDir, ".claude", "skills", "topolo", "SKILL.md");
5920
+ return join6(homeDir, ".claude", "skills", "topolo", "SKILL.md");
5781
5921
  case "codex":
5782
- return join5(homeDir, ".codex", "skills", "topolo", "SKILL.md");
5922
+ return join6(homeDir, ".codex", "skills", "topolo", "SKILL.md");
5783
5923
  case "cursor":
5784
- return join5(projectDir ?? homeDir, ".cursor", "rules", "topolo.mdc");
5924
+ return join6(projectDir ?? homeDir, ".cursor", "rules", "topolo.mdc");
5785
5925
  case "grok":
5786
- return join5(projectDir ?? homeDir, ".grok", "skills", "topolo", "SKILL.md");
5926
+ return join6(projectDir ?? homeDir, ".grok", "skills", "topolo", "SKILL.md");
5787
5927
  case "gemini":
5788
- return join5(projectDir ?? homeDir, ".gemini", "skills", "topolo", "SKILL.md");
5928
+ return join6(projectDir ?? homeDir, ".gemini", "skills", "topolo", "SKILL.md");
5789
5929
  case "copilot":
5790
- return projectDir ? join5(projectDir, ".github", "skills", "topolo", "SKILL.md") : join5(homeDir, ".copilot", "skills", "topolo", "SKILL.md");
5930
+ return projectDir ? join6(projectDir, ".github", "skills", "topolo", "SKILL.md") : join6(homeDir, ".copilot", "skills", "topolo", "SKILL.md");
5791
5931
  case "windsurf":
5792
- return projectDir ? join5(projectDir, ".windsurf", "rules", "topolo.md") : void 0;
5932
+ return projectDir ? join6(projectDir, ".windsurf", "rules", "topolo.md") : void 0;
5793
5933
  case "cline":
5794
- return join5(projectDir ?? homeDir, ".cline", "skills", "topolo", "SKILL.md");
5934
+ return join6(projectDir ?? homeDir, ".cline", "skills", "topolo", "SKILL.md");
5795
5935
  case "goose":
5796
5936
  case "amazon-q":
5797
5937
  return void 0;
@@ -5800,7 +5940,7 @@ function agentSkillPath(agent, homeDir = homedir3(), projectDir) {
5800
5940
  function agentConfigPaths(agent, homeDir = homedir3(), projectDir) {
5801
5941
  switch (agent) {
5802
5942
  case "claude":
5803
- return [{ path: join5(homeDir, ".claude.json"), format: "json", rootKey: "mcpServers" }];
5943
+ return [{ path: join6(homeDir, ".claude.json"), format: "json", rootKey: "mcpServers" }];
5804
5944
  case "claude-desktop":
5805
5945
  return [{
5806
5946
  path: claudeDesktopConfigPath(homeDir),
@@ -5808,31 +5948,31 @@ function agentConfigPaths(agent, homeDir = homedir3(), projectDir) {
5808
5948
  rootKey: "mcpServers"
5809
5949
  }];
5810
5950
  case "codex":
5811
- return [{ path: join5(homeDir, ".codex", "config.toml"), format: "toml" }];
5951
+ return [{ path: join6(homeDir, ".codex", "config.toml"), format: "toml" }];
5812
5952
  case "cursor":
5813
5953
  return [{
5814
- path: projectDir ? join5(projectDir, ".cursor", "mcp.json") : join5(homeDir, ".cursor", "mcp.json"),
5954
+ path: projectDir ? join6(projectDir, ".cursor", "mcp.json") : join6(homeDir, ".cursor", "mcp.json"),
5815
5955
  format: "json",
5816
5956
  rootKey: "mcpServers"
5817
5957
  }];
5818
5958
  case "grok":
5819
5959
  return [{
5820
- path: projectDir ? join5(projectDir, ".grok", "config.toml") : join5(homeDir, ".grok", "config.toml"),
5960
+ path: projectDir ? join6(projectDir, ".grok", "config.toml") : join6(homeDir, ".grok", "config.toml"),
5821
5961
  format: "toml"
5822
5962
  }];
5823
5963
  case "gemini":
5824
5964
  return [{
5825
- path: projectDir ? join5(projectDir, ".gemini", "settings.json") : join5(homeDir, ".gemini", "settings.json"),
5965
+ path: projectDir ? join6(projectDir, ".gemini", "settings.json") : join6(homeDir, ".gemini", "settings.json"),
5826
5966
  format: "json",
5827
5967
  rootKey: "mcpServers"
5828
5968
  }];
5829
5969
  case "copilot":
5830
5970
  return projectDir ? [
5831
- { path: join5(projectDir, ".mcp.json"), format: "json", rootKey: "mcpServers" },
5832
- { path: join5(projectDir, ".vscode", "mcp.json"), format: "json", rootKey: "servers" }
5971
+ { path: join6(projectDir, ".mcp.json"), format: "json", rootKey: "mcpServers" },
5972
+ { path: join6(projectDir, ".vscode", "mcp.json"), format: "json", rootKey: "servers" }
5833
5973
  ] : [
5834
5974
  {
5835
- path: join5(homeDir, ".copilot", "mcp-config.json"),
5975
+ path: join6(homeDir, ".copilot", "mcp-config.json"),
5836
5976
  format: "json",
5837
5977
  rootKey: "mcpServers"
5838
5978
  },
@@ -5840,24 +5980,24 @@ function agentConfigPaths(agent, homeDir = homedir3(), projectDir) {
5840
5980
  ];
5841
5981
  case "windsurf":
5842
5982
  return [{
5843
- path: join5(homeDir, ".codeium", "windsurf", "mcp_config.json"),
5983
+ path: join6(homeDir, ".codeium", "windsurf", "mcp_config.json"),
5844
5984
  format: "json",
5845
5985
  rootKey: "mcpServers"
5846
5986
  }];
5847
5987
  case "cline":
5848
5988
  return [{
5849
- path: projectDir ? join5(projectDir, ".cline", "mcp.json") : join5(homeDir, ".cline", "mcp.json"),
5989
+ path: projectDir ? join6(projectDir, ".cline", "mcp.json") : join6(homeDir, ".cline", "mcp.json"),
5850
5990
  format: "json",
5851
5991
  rootKey: "mcpServers"
5852
5992
  }];
5853
5993
  case "goose":
5854
- return [{ path: join5(homeDir, ".config", "goose", "config.yaml"), format: "yaml" }];
5994
+ return [{ path: join6(homeDir, ".config", "goose", "config.yaml"), format: "yaml" }];
5855
5995
  case "amazon-q":
5856
5996
  if (projectDir) {
5857
- return [{ path: join5(projectDir, ".amazonq", "mcp.json"), format: "json", rootKey: "mcpServers" }];
5997
+ return [{ path: join6(projectDir, ".amazonq", "mcp.json"), format: "json", rootKey: "mcpServers" }];
5858
5998
  }
5859
5999
  return [{
5860
- path: existsSync3(join5(homeDir, ".aws", "amazonq", "mcp.json")) ? join5(homeDir, ".aws", "amazonq", "mcp.json") : join5(homeDir, ".aws", "amazonq", "default.json"),
6000
+ path: existsSync3(join6(homeDir, ".aws", "amazonq", "mcp.json")) ? join6(homeDir, ".aws", "amazonq", "mcp.json") : join6(homeDir, ".aws", "amazonq", "default.json"),
5861
6001
  format: "json",
5862
6002
  rootKey: "mcpServers"
5863
6003
  }];
@@ -5893,14 +6033,14 @@ function agentConfigExists(agent, homeDir = homedir3(), projectDir) {
5893
6033
  return agentConfigPaths(agent, homeDir, projectDir).some((config) => existsSync3(config.path));
5894
6034
  }
5895
6035
  function claudeDesktopConfigPath(homeDir) {
5896
- if (process.platform === "darwin") return join5(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json");
5897
- if (process.platform === "win32") return join5(process.env.APPDATA ?? join5(homeDir, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
5898
- return join5(process.env.XDG_CONFIG_HOME ?? join5(homeDir, ".config"), "Claude", "claude_desktop_config.json");
6036
+ if (process.platform === "darwin") return join6(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json");
6037
+ if (process.platform === "win32") return join6(process.env.APPDATA ?? join6(homeDir, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
6038
+ return join6(process.env.XDG_CONFIG_HOME ?? join6(homeDir, ".config"), "Claude", "claude_desktop_config.json");
5899
6039
  }
5900
6040
  function vscodeUserConfigPath(homeDir) {
5901
- if (process.platform === "darwin") return join5(homeDir, "Library", "Application Support", "Code", "User", "mcp.json");
5902
- if (process.platform === "win32") return join5(process.env.APPDATA ?? join5(homeDir, "AppData", "Roaming"), "Code", "User", "mcp.json");
5903
- return join5(process.env.XDG_CONFIG_HOME ?? join5(homeDir, ".config"), "Code", "User", "mcp.json");
6041
+ if (process.platform === "darwin") return join6(homeDir, "Library", "Application Support", "Code", "User", "mcp.json");
6042
+ if (process.platform === "win32") return join6(process.env.APPDATA ?? join6(homeDir, "AppData", "Roaming"), "Code", "User", "mcp.json");
6043
+ return join6(process.env.XDG_CONFIG_HOME ?? join6(homeDir, ".config"), "Code", "User", "mcp.json");
5904
6044
  }
5905
6045
  async function readAgentRegistrations(agent, homeDir = homedir3(), projectDir) {
5906
6046
  const registrations = [];
@@ -6358,7 +6498,7 @@ function skillsSourceDir() {
6358
6498
  const distCandidate = resolve(here, "../skills");
6359
6499
  const srcCandidate = resolve(here, "../../skills");
6360
6500
  for (const candidate of [distCandidate, srcCandidate]) {
6361
- if (existsSync5(join6(candidate, "topolo", "SKILL.md"))) return candidate;
6501
+ if (existsSync5(join7(candidate, "topolo", "SKILL.md"))) return candidate;
6362
6502
  }
6363
6503
  return distCandidate;
6364
6504
  }
@@ -6401,7 +6541,7 @@ function resolveMcpRegistration(options) {
6401
6541
  return { name, ...context ? { context } : {}, environment, ...installationId ? { installationId } : {} };
6402
6542
  }
6403
6543
  async function installForAgent(agent, sourceDir, mcp, opts) {
6404
- const source = await readFile6(join6(sourceDir, "topolo", "SKILL.md"), "utf8");
6544
+ const source = await readFile6(join7(sourceDir, "topolo", "SKILL.md"), "utf8");
6405
6545
  const skill = await installAgentSkill(agent, source, opts.homeDir, opts.projectDir);
6406
6546
  if (!opts.withMcp) {
6407
6547
  return { agent, skillPath: skill.path, skillInstalled: skill.installed, mcp: "skipped" };
@@ -6497,7 +6637,7 @@ function registerSkillsCommand(program2) {
6497
6637
  }
6498
6638
  async function installAgentSurfaces(opts) {
6499
6639
  const sourceDir = skillsSourceDir();
6500
- if (!existsSync5(join6(sourceDir, "topolo", "SKILL.md"))) {
6640
+ if (!existsSync5(join7(sourceDir, "topolo", "SKILL.md"))) {
6501
6641
  throw new Error("Bundled skill file not found. Reinstall the CLI: npm install -g @topolo/cli");
6502
6642
  }
6503
6643
  const mcp = resolveMcpCommand();
@@ -6538,7 +6678,7 @@ function assertAgentSetupSucceeded(results) {
6538
6678
  }
6539
6679
  async function agentSurfaceStatus(options = {}) {
6540
6680
  const homeDir = options.homeDir ?? homedir4();
6541
- const sourceSkill = await readFile6(join6(skillsSourceDir(), "topolo", "SKILL.md"), "utf8");
6681
+ const sourceSkill = await readFile6(join7(skillsSourceDir(), "topolo", "SKILL.md"), "utf8");
6542
6682
  const agents = resolveTargets({
6543
6683
  agent: options.agent,
6544
6684
  all: options.all,
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@topolo/cli",
3
3
  "packageManager": "pnpm@10.17.1",
4
- "version": "0.12.9",
5
- "topoloPublishedVersion": "0.12.9",
4
+ "version": "0.12.11",
5
+ "topoloPublishedVersion": "0.12.11",
6
6
  "description": "Public command-line interface for Topolo customer organizations, developers, and agents.",
7
7
  "license": "UNLICENSED",
8
8
  "author": "Topolo",
@@ -28,8 +28,8 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@modelcontextprotocol/sdk": "^1.0.0",
31
- "@topolo/mcp": "0.14.2",
32
- "@topolo/sdk": "0.21.2",
31
+ "@topolo/mcp": "0.14.4",
32
+ "@topolo/sdk": "0.21.4",
33
33
  "commander": "^12.1.0"
34
34
  },
35
35
  "devDependencies": {
@@ -60,6 +60,16 @@ topolo context use <organization-name|personal> --json
60
60
  topolo whoami --json
61
61
  ```
62
62
 
63
+ For CLI automation, pin each process to an exact key returned by context discovery:
64
+
65
+ ```bash
66
+ TOPOLO_CONTEXT=org:org_acme topolo --env production whoami --json
67
+ TOPOLO_CONTEXT=org:org_acme topolo --env production actions --service brand --json
68
+ ```
69
+
70
+ The CLI keeps this pin through credential refresh and never changes the shared
71
+ active context. A missing pinned credential does not select another context.
72
+
63
73
  For concurrent agents or multiple MCP connections, never coordinate by changing
64
74
  one shared active context. Register each pinned context through the CLI so it
65
75
  uses the exact bundled MCP instead of a global binary:
@@ -242,6 +252,24 @@ If verification fails:
242
252
 
243
253
  Never improvise a rollback.
244
254
 
255
+ ## Protected action credential handoff
256
+
257
+ Local CLI and stdio MCP replace returned action credentials with opaque
258
+ `topolo-secret:<id>` references backed by the OS protected credential store.
259
+ Pass the reference unchanged into the next published action input; the SDK
260
+ resolves nonempty string credentials internally before validation and execution;
261
+ non-string or empty credentials fail closed. Never extract the
262
+ underlying token or put it in a file, terminal command, message, or log.
263
+
264
+ References are bound to the verified principal, organization, API key identity
265
+ when present, and environment. They expire after 30 minutes; access to an
266
+ expired reference deletes its local entry. Delete references once the workflow
267
+ finishes using `topolo actions release-secret <reference> --confirm --json`,
268
+ or `topolo_release_action_secret` with `confirm: true` in stdio MCP.
269
+ Local deletion does not revoke the upstream credential; use its published
270
+ revocation action separately when required. Other SDK/remote hosts keep
271
+ redaction unless they explicitly provide a protected store.
272
+
245
273
  ## Generic public API escape hatch
246
274
 
247
275
  Only when public docs provide a complete contract: