@topolo/mcp 0.14.2 → 0.14.3
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 +13 -0
- package/dist/dispatch.js +23 -0
- package/dist/http.js +23 -0
- package/dist/index.js +350 -201
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -55,6 +55,19 @@ entry point resolved from the installed CLI dependency graph. Advanced hosts
|
|
|
55
55
|
may execute the exported `@topolo/mcp/stdio` entry directly, but customer setup
|
|
56
56
|
must not use an `npx` launcher because that makes startup network-dependent.
|
|
57
57
|
|
|
58
|
+
## Protected action handoff
|
|
59
|
+
|
|
60
|
+
Local stdio action results return `topolo-secret:<id>` references instead of
|
|
61
|
+
bearer credentials. Pass these references unchanged to subsequent published
|
|
62
|
+
actions; SDK 0.21.3 resolves them internally using the OS protected store and
|
|
63
|
+
masks them in plans and diagnostics. Entries are bound to the verified
|
|
64
|
+
principal, organization, API key identity when present, and environment.
|
|
65
|
+
References expire after 30 minutes; accessing an expired entry deletes it.
|
|
66
|
+
Release finished workflow references with `topolo_release_action_secret`
|
|
67
|
+
and `confirm: true`. This deletes local storage only, not the upstream grant.
|
|
68
|
+
HTTP/Worker hosts remain redacted unless they explicitly configure a protected
|
|
69
|
+
SDK store; local references cannot be transferred to a different host.
|
|
70
|
+
|
|
58
71
|
## Supported env vars
|
|
59
72
|
|
|
60
73
|
| Var | Purpose |
|
package/dist/dispatch.js
CHANGED
|
@@ -63,6 +63,28 @@ function publicMcpErrorMessage(error, fallback) {
|
|
|
63
63
|
return error instanceof TopoloMcpPublicError ? error.message : fallback;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// src/action-secret-tools.ts
|
|
67
|
+
var actionSecretTools = [{
|
|
68
|
+
name: "topolo_release_action_secret",
|
|
69
|
+
title: "Release a protected action reference",
|
|
70
|
+
description: "Deletes only a local protected handoff reference for the verified identity and environment. Does not revoke the upstream credential. Requires confirm:true.",
|
|
71
|
+
requiredScopes: [],
|
|
72
|
+
destructive: true,
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: "object",
|
|
75
|
+
properties: { reference: { type: "string", minLength: 1 }, confirm: { type: "boolean" } },
|
|
76
|
+
required: ["reference", "confirm"],
|
|
77
|
+
additionalProperties: false
|
|
78
|
+
},
|
|
79
|
+
handler: async (topolo, args) => {
|
|
80
|
+
const reference = typeof args.reference === "string" ? args.reference.trim() : "";
|
|
81
|
+
if (!reference) throw new TopoloMcpPublicError("reference is required.");
|
|
82
|
+
if (args.confirm !== true) throw new TopoloMcpPublicError("Releasing a protected action reference requires confirm:true.");
|
|
83
|
+
await topolo.client.releaseActionSecret(reference);
|
|
84
|
+
return { released: true };
|
|
85
|
+
}
|
|
86
|
+
}];
|
|
87
|
+
|
|
66
88
|
// src/tools.ts
|
|
67
89
|
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
68
90
|
var AGENT_INVOCATIONS_CREATE = "app_topolo_agent.agent_invocations.create";
|
|
@@ -141,6 +163,7 @@ function agentRunResult(invocation) {
|
|
|
141
163
|
};
|
|
142
164
|
}
|
|
143
165
|
var TOOLS = [
|
|
166
|
+
...actionSecretTools,
|
|
144
167
|
{
|
|
145
168
|
name: "topolo_whoami",
|
|
146
169
|
title: "Describe the current Topolo credential",
|
package/dist/http.js
CHANGED
|
@@ -17790,6 +17790,28 @@ function splitService(servicePart) {
|
|
|
17790
17790
|
return [servicePart.slice(0, idx), servicePart.slice(idx + 1)];
|
|
17791
17791
|
}
|
|
17792
17792
|
|
|
17793
|
+
// src/action-secret-tools.ts
|
|
17794
|
+
var actionSecretTools = [{
|
|
17795
|
+
name: "topolo_release_action_secret",
|
|
17796
|
+
title: "Release a protected action reference",
|
|
17797
|
+
description: "Deletes only a local protected handoff reference for the verified identity and environment. Does not revoke the upstream credential. Requires confirm:true.",
|
|
17798
|
+
requiredScopes: [],
|
|
17799
|
+
destructive: true,
|
|
17800
|
+
inputSchema: {
|
|
17801
|
+
type: "object",
|
|
17802
|
+
properties: { reference: { type: "string", minLength: 1 }, confirm: { type: "boolean" } },
|
|
17803
|
+
required: ["reference", "confirm"],
|
|
17804
|
+
additionalProperties: false
|
|
17805
|
+
},
|
|
17806
|
+
handler: async (topolo, args) => {
|
|
17807
|
+
const reference = typeof args.reference === "string" ? args.reference.trim() : "";
|
|
17808
|
+
if (!reference) throw new TopoloMcpPublicError("reference is required.");
|
|
17809
|
+
if (args.confirm !== true) throw new TopoloMcpPublicError("Releasing a protected action reference requires confirm:true.");
|
|
17810
|
+
await topolo.client.releaseActionSecret(reference);
|
|
17811
|
+
return { released: true };
|
|
17812
|
+
}
|
|
17813
|
+
}];
|
|
17814
|
+
|
|
17793
17815
|
// src/tools.ts
|
|
17794
17816
|
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
17795
17817
|
var AGENT_INVOCATIONS_CREATE = "app_topolo_agent.agent_invocations.create";
|
|
@@ -17868,6 +17890,7 @@ function agentRunResult(invocation) {
|
|
|
17868
17890
|
};
|
|
17869
17891
|
}
|
|
17870
17892
|
var TOOLS = [
|
|
17893
|
+
...actionSecretTools,
|
|
17871
17894
|
{
|
|
17872
17895
|
name: "topolo_whoami",
|
|
17873
17896
|
title: "Describe the current Topolo credential",
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
|
|
6
|
-
// node_modules/.pnpm/@topolo+sdk@0.21.
|
|
6
|
+
// node_modules/.pnpm/@topolo+sdk@0.21.3/node_modules/@topolo/sdk/dist/index.js
|
|
7
7
|
function applyAuthHeaders(headers, credential) {
|
|
8
8
|
if (credential.kind === "api_key") {
|
|
9
9
|
headers.set("X-Api-Key", credential.apiKey);
|
|
@@ -22,54 +22,6 @@ function generateRequestId() {
|
|
|
22
22
|
}
|
|
23
23
|
return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
24
24
|
}
|
|
25
|
-
var encoder = new TextEncoder();
|
|
26
|
-
function encodeBase64Url(value) {
|
|
27
|
-
const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
28
|
-
let binary = "";
|
|
29
|
-
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
30
|
-
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
31
|
-
}
|
|
32
|
-
function encodeJson(value) {
|
|
33
|
-
return encodeBase64Url(encoder.encode(JSON.stringify(value)));
|
|
34
|
-
}
|
|
35
|
-
async function sha256Base64Url(value) {
|
|
36
|
-
return encodeBase64Url(await crypto.subtle.digest("SHA-256", encoder.encode(value)));
|
|
37
|
-
}
|
|
38
|
-
function publicJwk(privateJwk) {
|
|
39
|
-
const { crv, kty, x, y } = privateJwk;
|
|
40
|
-
if (kty !== "EC" || crv !== "P-256" || !x || !y || !privateJwk.d) {
|
|
41
|
-
throw new TypeError("A private P-256 JSON Web Key is required for DPoP signing.");
|
|
42
|
-
}
|
|
43
|
-
return { crv, kty, x, y };
|
|
44
|
-
}
|
|
45
|
-
function canonicalTargetUrl(value) {
|
|
46
|
-
const url = new URL(value);
|
|
47
|
-
return `${url.origin}${url.pathname}`;
|
|
48
|
-
}
|
|
49
|
-
async function createDpopProof(input) {
|
|
50
|
-
const jwk = publicJwk(input.privateJwk);
|
|
51
|
-
const key = await crypto.subtle.importKey(
|
|
52
|
-
"jwk",
|
|
53
|
-
input.privateJwk,
|
|
54
|
-
{ name: "ECDSA", namedCurve: "P-256" },
|
|
55
|
-
false,
|
|
56
|
-
["sign"]
|
|
57
|
-
);
|
|
58
|
-
const header = encodeJson({ alg: "ES256", typ: "dpop+jwt", jwk });
|
|
59
|
-
const claims = encodeJson({
|
|
60
|
-
jti: crypto.randomUUID(),
|
|
61
|
-
iat: Math.floor(Date.now() / 1e3),
|
|
62
|
-
htm: input.method.toUpperCase(),
|
|
63
|
-
htu: canonicalTargetUrl(input.targetUrl),
|
|
64
|
-
ath: await sha256Base64Url(input.accessToken)
|
|
65
|
-
});
|
|
66
|
-
const signature = await crypto.subtle.sign(
|
|
67
|
-
{ name: "ECDSA", hash: "SHA-256" },
|
|
68
|
-
key,
|
|
69
|
-
encoder.encode(`${header}.${claims}`)
|
|
70
|
-
);
|
|
71
|
-
return `${header}.${claims}.${encodeBase64Url(signature)}`;
|
|
72
|
-
}
|
|
73
25
|
var TopoloSdkError = class extends Error {
|
|
74
26
|
code;
|
|
75
27
|
constructor(code, message) {
|
|
@@ -391,6 +343,131 @@ var TopoloHttpError = class extends TopoloSdkError {
|
|
|
391
343
|
);
|
|
392
344
|
}
|
|
393
345
|
};
|
|
346
|
+
var ACTION_SECRET_PREFIX = "topolo-secret:";
|
|
347
|
+
function isActionSecretReference(value) {
|
|
348
|
+
return typeof value === "string" && value.startsWith(ACTION_SECRET_PREFIX);
|
|
349
|
+
}
|
|
350
|
+
function isRecord(value) {
|
|
351
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
352
|
+
}
|
|
353
|
+
function sensitive(key, value) {
|
|
354
|
+
const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
355
|
+
if (normalized === "capability") {
|
|
356
|
+
if (typeof value !== "string") return value !== null && value !== void 0 && typeof value !== "boolean" && typeof value !== "number";
|
|
357
|
+
const text2 = value.trim();
|
|
358
|
+
return Boolean(text2) && !/^[a-z][a-z0-9_-]{0,31}$/.test(text2);
|
|
359
|
+
}
|
|
360
|
+
return ["authorization", "cookie", "setcookie", "proxyauthorization"].includes(normalized) || /(token|secret|password|privatekey|apikey)$/.test(normalized);
|
|
361
|
+
}
|
|
362
|
+
function maskActionSecrets(value, secrets) {
|
|
363
|
+
let masked = value;
|
|
364
|
+
for (const [secret, reference] of secrets) {
|
|
365
|
+
const encoded = encodeURIComponent(secret);
|
|
366
|
+
const variants = [.../* @__PURE__ */ new Set([secret, encoded])].map((item) => item.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
367
|
+
masked = masked.replace(new RegExp(variants.join("|"), "g"), (match) => match === secret ? reference : encodeURIComponent(reference));
|
|
368
|
+
}
|
|
369
|
+
return masked;
|
|
370
|
+
}
|
|
371
|
+
async function resolveActionSecrets(value, store, secrets) {
|
|
372
|
+
if (isActionSecretReference(value)) {
|
|
373
|
+
if (!store) throw new TopoloSdkError("action_secret_store_required", "Protected action references require a secret store.");
|
|
374
|
+
const resolved = await store.load(value);
|
|
375
|
+
if (typeof resolved === "string" && resolved) secrets.set(resolved, value);
|
|
376
|
+
return resolved;
|
|
377
|
+
}
|
|
378
|
+
if (Array.isArray(value)) return Promise.all(value.map((item) => resolveActionSecrets(item, store, secrets)));
|
|
379
|
+
if (!isRecord(value)) return value;
|
|
380
|
+
return Object.fromEntries(await Promise.all(Object.entries(value).map(async ([key, item]) => [key, await resolveActionSecrets(item, store, secrets)])));
|
|
381
|
+
}
|
|
382
|
+
async function protectActionResult(value, store) {
|
|
383
|
+
const secrets = /* @__PURE__ */ new Map();
|
|
384
|
+
async function protect(item, key = "") {
|
|
385
|
+
if (isActionSecretReference(item)) return item;
|
|
386
|
+
if (sensitive(key, item)) {
|
|
387
|
+
const reference = store ? await store.save(item) : "[REDACTED]";
|
|
388
|
+
if (typeof item === "string" && item) secrets.set(item, reference);
|
|
389
|
+
return reference;
|
|
390
|
+
}
|
|
391
|
+
if (Array.isArray(item)) return Promise.all(item.map((child) => protect(child)));
|
|
392
|
+
if (!isRecord(item)) return item;
|
|
393
|
+
return Object.fromEntries(await Promise.all(Object.entries(item).map(async ([name, child]) => [name, await protect(child, name)])));
|
|
394
|
+
}
|
|
395
|
+
const protectedValue = await protect(value);
|
|
396
|
+
async function protectEmbedded(item) {
|
|
397
|
+
if (typeof item === "string" && !isActionSecretReference(item) && maskActionSecrets(item, secrets) !== item) {
|
|
398
|
+
return store ? store.save(item) : maskActionSecrets(item, secrets);
|
|
399
|
+
}
|
|
400
|
+
if (Array.isArray(item)) return Promise.all(item.map(protectEmbedded));
|
|
401
|
+
if (!isRecord(item)) return item;
|
|
402
|
+
return Object.fromEntries(await Promise.all(Object.entries(item).map(async ([key, child]) => [key, await protectEmbedded(child)])));
|
|
403
|
+
}
|
|
404
|
+
return protectEmbedded(protectedValue);
|
|
405
|
+
}
|
|
406
|
+
function maskActionSecretValue(value, secrets) {
|
|
407
|
+
if (value instanceof Error) return maskActionSecretError(value, secrets);
|
|
408
|
+
if (typeof value === "string") return maskActionSecrets(value, secrets);
|
|
409
|
+
if (Array.isArray(value)) return value.map((item) => maskActionSecretValue(item, secrets));
|
|
410
|
+
if (!isRecord(value)) return value;
|
|
411
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, maskActionSecretValue(item, secrets)]));
|
|
412
|
+
}
|
|
413
|
+
function maskActionSecretError(error, secrets) {
|
|
414
|
+
if (!(error instanceof Error)) return maskActionSecretValue(error, secrets);
|
|
415
|
+
const masked = Object.create(Object.getPrototypeOf(error));
|
|
416
|
+
for (const key of Object.getOwnPropertyNames(error)) {
|
|
417
|
+
const descriptor = Object.getOwnPropertyDescriptor(error, key);
|
|
418
|
+
if ("value" in descriptor) descriptor.value = maskActionSecretValue(descriptor.value, secrets);
|
|
419
|
+
Object.defineProperty(masked, key, descriptor);
|
|
420
|
+
}
|
|
421
|
+
return masked;
|
|
422
|
+
}
|
|
423
|
+
var encoder = new TextEncoder();
|
|
424
|
+
function encodeBase64Url(value) {
|
|
425
|
+
const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
426
|
+
let binary = "";
|
|
427
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
428
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
429
|
+
}
|
|
430
|
+
function encodeJson(value) {
|
|
431
|
+
return encodeBase64Url(encoder.encode(JSON.stringify(value)));
|
|
432
|
+
}
|
|
433
|
+
async function sha256Base64Url(value) {
|
|
434
|
+
return encodeBase64Url(await crypto.subtle.digest("SHA-256", encoder.encode(value)));
|
|
435
|
+
}
|
|
436
|
+
function publicJwk(privateJwk) {
|
|
437
|
+
const { crv, kty, x, y } = privateJwk;
|
|
438
|
+
if (kty !== "EC" || crv !== "P-256" || !x || !y || !privateJwk.d) {
|
|
439
|
+
throw new TypeError("A private P-256 JSON Web Key is required for DPoP signing.");
|
|
440
|
+
}
|
|
441
|
+
return { crv, kty, x, y };
|
|
442
|
+
}
|
|
443
|
+
function canonicalTargetUrl(value) {
|
|
444
|
+
const url = new URL(value);
|
|
445
|
+
return `${url.origin}${url.pathname}`;
|
|
446
|
+
}
|
|
447
|
+
async function createDpopProof(input) {
|
|
448
|
+
const jwk = publicJwk(input.privateJwk);
|
|
449
|
+
const key = await crypto.subtle.importKey(
|
|
450
|
+
"jwk",
|
|
451
|
+
input.privateJwk,
|
|
452
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
453
|
+
false,
|
|
454
|
+
["sign"]
|
|
455
|
+
);
|
|
456
|
+
const header = encodeJson({ alg: "ES256", typ: "dpop+jwt", jwk });
|
|
457
|
+
const claims = encodeJson({
|
|
458
|
+
jti: crypto.randomUUID(),
|
|
459
|
+
iat: Math.floor(Date.now() / 1e3),
|
|
460
|
+
htm: input.method.toUpperCase(),
|
|
461
|
+
htu: canonicalTargetUrl(input.targetUrl),
|
|
462
|
+
ath: await sha256Base64Url(input.accessToken)
|
|
463
|
+
});
|
|
464
|
+
const signature = await crypto.subtle.sign(
|
|
465
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
466
|
+
key,
|
|
467
|
+
encoder.encode(`${header}.${claims}`)
|
|
468
|
+
);
|
|
469
|
+
return `${header}.${claims}.${encodeBase64Url(signature)}`;
|
|
470
|
+
}
|
|
394
471
|
function hostnameHasSuffix(hostname, suffix) {
|
|
395
472
|
return hostname === suffix || hostname.endsWith(`.${suffix}`);
|
|
396
473
|
}
|
|
@@ -564,9 +641,9 @@ function inspectSchema(schema, path, root, depth) {
|
|
|
564
641
|
return { path, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` };
|
|
565
642
|
}
|
|
566
643
|
const children = [];
|
|
567
|
-
if (
|
|
568
|
-
if (
|
|
569
|
-
if (
|
|
644
|
+
if (isRecord2(schema.properties)) children.push(...Object.entries(schema.properties));
|
|
645
|
+
if (isRecord2(schema.$defs)) children.push(...Object.entries(schema.$defs));
|
|
646
|
+
if (isRecord2(schema.definitions)) children.push(...Object.entries(schema.definitions));
|
|
570
647
|
children.push(
|
|
571
648
|
["additionalProperties", schema.additionalProperties],
|
|
572
649
|
["propertyNames", schema.propertyNames],
|
|
@@ -576,7 +653,7 @@ function inspectSchema(schema, path, root, depth) {
|
|
|
576
653
|
...arraySchemaEntries("oneOf", schema.oneOf)
|
|
577
654
|
);
|
|
578
655
|
for (const [key, child] of children) {
|
|
579
|
-
if (!
|
|
656
|
+
if (!isRecord2(child)) continue;
|
|
580
657
|
const issue = inspectSchema(child, `${path}.${key}`, root, depth + 1);
|
|
581
658
|
if (issue) return issue;
|
|
582
659
|
}
|
|
@@ -600,7 +677,7 @@ function validateNode(schema, value, path, root, errors, depth) {
|
|
|
600
677
|
}
|
|
601
678
|
if (Array.isArray(schema.allOf)) {
|
|
602
679
|
for (const candidate of schema.allOf) {
|
|
603
|
-
if (
|
|
680
|
+
if (isRecord2(candidate)) validateNode(candidate, value, path, root, errors, depth + 1);
|
|
604
681
|
}
|
|
605
682
|
}
|
|
606
683
|
if (Array.isArray(schema.anyOf) && !schema.anyOf.some((candidate) => candidateMatches(candidate, value, root, depth))) {
|
|
@@ -630,7 +707,7 @@ function validateNode(schema, value, path, root, errors, depth) {
|
|
|
630
707
|
if (typeof value === "string") validateString(schema, value, path, errors);
|
|
631
708
|
if (typeof value === "number") validateNumber(schema, value, path, errors);
|
|
632
709
|
if (Array.isArray(value)) validateArray(schema, value, path, root, errors, depth);
|
|
633
|
-
if (
|
|
710
|
+
if (isRecord2(value)) validateObject(schema, value, path, root, errors, depth);
|
|
634
711
|
}
|
|
635
712
|
function validateObject(schema, value, path, root, errors, depth) {
|
|
636
713
|
if (typeof schema.minProperties === "number" && Object.keys(value).length < schema.minProperties) {
|
|
@@ -639,25 +716,25 @@ function validateObject(schema, value, path, root, errors, depth) {
|
|
|
639
716
|
if (typeof schema.maxProperties === "number" && Object.keys(value).length > schema.maxProperties) {
|
|
640
717
|
errors.push({ path, code: "max_properties", message: `Expected at most ${schema.maxProperties} properties.` });
|
|
641
718
|
}
|
|
642
|
-
const properties =
|
|
719
|
+
const properties = isRecord2(schema.properties) ? schema.properties : {};
|
|
643
720
|
const required = Array.isArray(schema.required) ? schema.required.map(String) : [];
|
|
644
721
|
for (const key of required) {
|
|
645
722
|
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
646
723
|
errors.push({ path: `${path}.${key}`, code: "required", message: "Required property is missing." });
|
|
647
724
|
}
|
|
648
725
|
}
|
|
649
|
-
if (
|
|
726
|
+
if (isRecord2(schema.propertyNames)) {
|
|
650
727
|
for (const key of Object.keys(value)) {
|
|
651
728
|
validateNode(schema.propertyNames, key, `${path}.${key}`, root, errors, depth + 1);
|
|
652
729
|
}
|
|
653
730
|
}
|
|
654
731
|
for (const [key, child] of Object.entries(value)) {
|
|
655
732
|
const childSchema = properties[key];
|
|
656
|
-
if (
|
|
733
|
+
if (isRecord2(childSchema)) {
|
|
657
734
|
validateNode(childSchema, child, `${path}.${key}`, root, errors, depth + 1);
|
|
658
735
|
} else if (schema.additionalProperties === false) {
|
|
659
736
|
errors.push({ path: `${path}.${key}`, code: "additional_property", message: "Property is not declared by the action schema." });
|
|
660
|
-
} else if (
|
|
737
|
+
} else if (isRecord2(schema.additionalProperties)) {
|
|
661
738
|
validateNode(schema.additionalProperties, child, `${path}.${key}`, root, errors, depth + 1);
|
|
662
739
|
}
|
|
663
740
|
}
|
|
@@ -673,7 +750,7 @@ function validateArray(schema, value, path, root, errors, depth) {
|
|
|
673
750
|
const unique = new Set(value.map((item) => JSON.stringify(item)));
|
|
674
751
|
if (unique.size !== value.length) errors.push({ path, code: "unique_items", message: "Expected every item to be unique." });
|
|
675
752
|
}
|
|
676
|
-
if (
|
|
753
|
+
if (isRecord2(schema.items)) {
|
|
677
754
|
value.forEach((item, index) => validateNode(schema.items, item, `${path}[${index}]`, root, errors, depth + 1));
|
|
678
755
|
}
|
|
679
756
|
}
|
|
@@ -711,7 +788,7 @@ function validateNumber(schema, value, path, errors) {
|
|
|
711
788
|
}
|
|
712
789
|
}
|
|
713
790
|
function candidateMatches(candidate, value, root, depth) {
|
|
714
|
-
if (!
|
|
791
|
+
if (!isRecord2(candidate)) return false;
|
|
715
792
|
const issues = [];
|
|
716
793
|
validateNode(candidate, value, "$", root, issues, depth + 1);
|
|
717
794
|
return issues.length === 0;
|
|
@@ -725,24 +802,24 @@ function resolveLocalReference(root, reference) {
|
|
|
725
802
|
current = current[Number(token)];
|
|
726
803
|
continue;
|
|
727
804
|
}
|
|
728
|
-
if (!
|
|
805
|
+
if (!isRecord2(current) || !Object.prototype.hasOwnProperty.call(current, token)) return null;
|
|
729
806
|
current = current[token];
|
|
730
807
|
}
|
|
731
|
-
return
|
|
808
|
+
return isRecord2(current) ? current : null;
|
|
732
809
|
}
|
|
733
810
|
function matchesType(type, value) {
|
|
734
811
|
if (type === void 0) return true;
|
|
735
812
|
if (Array.isArray(type)) return type.some((candidate) => matchesType(candidate, value));
|
|
736
813
|
if (type === "null") return value === null;
|
|
737
814
|
if (type === "array") return Array.isArray(value);
|
|
738
|
-
if (type === "object") return
|
|
815
|
+
if (type === "object") return isRecord2(value);
|
|
739
816
|
if (type === "integer") return typeof value === "number" && Number.isInteger(value);
|
|
740
817
|
return typeof value === type;
|
|
741
818
|
}
|
|
742
819
|
function sameJsonValue(left, right) {
|
|
743
820
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
744
821
|
}
|
|
745
|
-
function
|
|
822
|
+
function isRecord2(value) {
|
|
746
823
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
747
824
|
}
|
|
748
825
|
function isUrl(value) {
|
|
@@ -977,7 +1054,7 @@ function fail(path, message) {
|
|
|
977
1054
|
throw new ActorEnvelopeValidationError(path, message);
|
|
978
1055
|
}
|
|
979
1056
|
function parseTopoloActionUploadContract(value) {
|
|
980
|
-
if (!
|
|
1057
|
+
if (!isRecord3(value) || value.profile !== "inline-base64" && value.profile !== "url-session") {
|
|
981
1058
|
throw new TopoloSdkError("upload_contract_unavailable", "This action does not publish a supported upload contract.");
|
|
982
1059
|
}
|
|
983
1060
|
const contentTypeField = requiredField(value.contentTypeField, "contentTypeField");
|
|
@@ -1056,17 +1133,17 @@ function prepareTopoloUrlSessionUploadInput(contractValue, byteLength, input, op
|
|
|
1056
1133
|
};
|
|
1057
1134
|
}
|
|
1058
1135
|
function topoloUploadResponseValue(response, path) {
|
|
1059
|
-
return path.split(".").reduce((value, segment) =>
|
|
1136
|
+
return path.split(".").reduce((value, segment) => isRecord3(value) ? value[segment] : void 0, response);
|
|
1060
1137
|
}
|
|
1061
1138
|
function redactTopoloUrlSessionResult(response, uploadUrlPath) {
|
|
1062
1139
|
const redacted = cloneUploadValue(response);
|
|
1063
1140
|
let target = redacted;
|
|
1064
1141
|
const segments = uploadUrlPath.split(".");
|
|
1065
1142
|
for (const segment of segments.slice(0, -1)) {
|
|
1066
|
-
target =
|
|
1143
|
+
target = isRecord3(target) ? target[segment] : void 0;
|
|
1067
1144
|
}
|
|
1068
1145
|
const finalSegment = segments.at(-1);
|
|
1069
|
-
if (finalSegment &&
|
|
1146
|
+
if (finalSegment && isRecord3(target) && finalSegment in target) target[finalSegment] = "[REDACTED]";
|
|
1070
1147
|
redactUploadHeaders(redacted);
|
|
1071
1148
|
return redacted;
|
|
1072
1149
|
}
|
|
@@ -1090,7 +1167,7 @@ function mapUploadInput(contract, contentBase64, input, options) {
|
|
|
1090
1167
|
}
|
|
1091
1168
|
function cloneUploadValue(value) {
|
|
1092
1169
|
if (Array.isArray(value)) return value.map(cloneUploadValue);
|
|
1093
|
-
if (!
|
|
1170
|
+
if (!isRecord3(value)) return value;
|
|
1094
1171
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneUploadValue(item)]));
|
|
1095
1172
|
}
|
|
1096
1173
|
function redactUploadHeaders(value) {
|
|
@@ -1098,9 +1175,9 @@ function redactUploadHeaders(value) {
|
|
|
1098
1175
|
value.forEach(redactUploadHeaders);
|
|
1099
1176
|
return;
|
|
1100
1177
|
}
|
|
1101
|
-
if (!
|
|
1178
|
+
if (!isRecord3(value)) return;
|
|
1102
1179
|
for (const [key, item] of Object.entries(value)) {
|
|
1103
|
-
if (key.toLowerCase() === "headers" &&
|
|
1180
|
+
if (key.toLowerCase() === "headers" && isRecord3(item)) {
|
|
1104
1181
|
for (const header of Object.keys(item)) {
|
|
1105
1182
|
if (["authorization", "cookie", "set-cookie", "proxy-authorization", "x-api-key"].includes(header.toLowerCase())) {
|
|
1106
1183
|
item[header] = "[REDACTED]";
|
|
@@ -1129,7 +1206,7 @@ function decodedBase64Size(contentBase64) {
|
|
|
1129
1206
|
return contentBase64.length / 4 * 3 - padding;
|
|
1130
1207
|
}
|
|
1131
1208
|
function parseCompleteContract(value) {
|
|
1132
|
-
if (!
|
|
1209
|
+
if (!isRecord3(value)) throw new TopoloSdkError("invalid_upload_contract", "The action upload completion contract is invalid.");
|
|
1133
1210
|
return {
|
|
1134
1211
|
actionId: requiredPath(value.actionId, "complete.actionId"),
|
|
1135
1212
|
inputField: requiredField(value.inputField, "complete.inputField"),
|
|
@@ -1167,7 +1244,7 @@ function encodeTopoloActionBase64(bytes) {
|
|
|
1167
1244
|
}
|
|
1168
1245
|
return btoa(binary);
|
|
1169
1246
|
}
|
|
1170
|
-
function
|
|
1247
|
+
function isRecord3(value) {
|
|
1171
1248
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1172
1249
|
}
|
|
1173
1250
|
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
@@ -1178,6 +1255,8 @@ var TOPOLO_RESOURCE_ID_HEADER = "X-Topolo-Resource-ID";
|
|
|
1178
1255
|
var POSITIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
1179
1256
|
var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
|
|
1180
1257
|
var TopoloClient = class _TopoloClient {
|
|
1258
|
+
actionSecretStore;
|
|
1259
|
+
actionSecrets = /* @__PURE__ */ new Map();
|
|
1181
1260
|
credential;
|
|
1182
1261
|
agent;
|
|
1183
1262
|
actorContext;
|
|
@@ -1207,6 +1286,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1207
1286
|
_TopoloClient.appIdCache.clear();
|
|
1208
1287
|
}
|
|
1209
1288
|
constructor(options) {
|
|
1289
|
+
this.actionSecretStore = options.actionSecretStore;
|
|
1210
1290
|
if (!options.credential) throw new TopoloAuthError("credential is required");
|
|
1211
1291
|
if (!options.agent?.clientName) throw new TopoloAuthError("agent.clientName is required");
|
|
1212
1292
|
this.credential = options.credential;
|
|
@@ -1228,6 +1308,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1228
1308
|
}
|
|
1229
1309
|
emit(event) {
|
|
1230
1310
|
if (!this.debug) return;
|
|
1311
|
+
event = maskActionSecretValue(event, this.actionSecrets);
|
|
1231
1312
|
try {
|
|
1232
1313
|
this.debug(event);
|
|
1233
1314
|
} catch {
|
|
@@ -1613,7 +1694,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1613
1694
|
}
|
|
1614
1695
|
const entries = [];
|
|
1615
1696
|
for (const item of raw) {
|
|
1616
|
-
if (!
|
|
1697
|
+
if (!isRecord4(item)) continue;
|
|
1617
1698
|
const s = item;
|
|
1618
1699
|
const appId = typeof s.app_id === "string" ? s.app_id : typeof s.id === "string" ? s.id : null;
|
|
1619
1700
|
if (!appId) continue;
|
|
@@ -1688,8 +1769,8 @@ var TopoloClient = class _TopoloClient {
|
|
|
1688
1769
|
"app_topolo_auth.service_api_key_resources.list",
|
|
1689
1770
|
{ appId: service.appId }
|
|
1690
1771
|
);
|
|
1691
|
-
const payload =
|
|
1692
|
-
if (!
|
|
1772
|
+
const payload = isRecord4(response) && isRecord4(response.data) ? response.data : response;
|
|
1773
|
+
if (!isRecord4(payload) || !Array.isArray(payload.resources)) {
|
|
1693
1774
|
throw new TopoloSdkError("resource_catalog_unavailable", "TopoloAuth did not return bindable resources.");
|
|
1694
1775
|
}
|
|
1695
1776
|
return {
|
|
@@ -1870,17 +1951,20 @@ var TopoloClient = class _TopoloClient {
|
|
|
1870
1951
|
}
|
|
1871
1952
|
async validateActionInput(action, input = {}) {
|
|
1872
1953
|
const entry = await this.getAction(action);
|
|
1873
|
-
|
|
1954
|
+
const resolved = await resolveActionSecrets(input, this.actionSecretStore, this.actionSecrets);
|
|
1955
|
+
return { action: entry, validation: validateTopoloActionInput(entry.inputSchema, resolved) };
|
|
1874
1956
|
}
|
|
1875
1957
|
async planAction(action, input = {}, options = {}) {
|
|
1876
1958
|
const entry = await this.getAction(action);
|
|
1877
|
-
|
|
1959
|
+
const resolved = await resolveActionSecrets(input, this.actionSecretStore, this.actionSecrets);
|
|
1960
|
+
return this.planResolvedAction(entry, resolved, options);
|
|
1878
1961
|
}
|
|
1879
1962
|
async prepareAction(action, input, options = {}) {
|
|
1880
1963
|
const entry = await this.getAction(action);
|
|
1881
1964
|
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;
|
|
1882
1965
|
const selectedInput = input ?? example?.input ?? {};
|
|
1883
|
-
const
|
|
1966
|
+
const resolved = await resolveActionSecrets(selectedInput, this.actionSecretStore, this.actionSecrets);
|
|
1967
|
+
const plan = this.planResolvedAction(entry, resolved, options);
|
|
1884
1968
|
return {
|
|
1885
1969
|
action: entry,
|
|
1886
1970
|
example,
|
|
@@ -1917,7 +2001,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1917
2001
|
confirmationRequired,
|
|
1918
2002
|
confirmationProvided: options.confirm === true,
|
|
1919
2003
|
executable: validation.valid && agentAccess !== "off" && (!confirmationRequired || options.confirm === true),
|
|
1920
|
-
request,
|
|
2004
|
+
request: maskActionSecretValue(request, this.actionSecrets),
|
|
1921
2005
|
verification: stringArray(entry.agentContract?.verify),
|
|
1922
2006
|
rollbackActionId: pickActionString(entry.agentContract?.rollbackActionId),
|
|
1923
2007
|
nextActionIds: stringArray(entry.agentContract?.nextActionIds)
|
|
@@ -1947,6 +2031,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1947
2031
|
`Topolo action "${entry.actionId}" requires { confirm: true } before invocation.`
|
|
1948
2032
|
);
|
|
1949
2033
|
}
|
|
2034
|
+
input = await resolveActionSecrets(input, this.actionSecretStore, this.actionSecrets);
|
|
1950
2035
|
const validation = validateTopoloActionInput(entry.inputSchema, input);
|
|
1951
2036
|
if (!validation.valid) {
|
|
1952
2037
|
throw new TopoloSdkError(
|
|
@@ -1977,22 +2062,31 @@ var TopoloClient = class _TopoloClient {
|
|
|
1977
2062
|
}
|
|
1978
2063
|
}
|
|
1979
2064
|
const service = entry.appSlug || entry.appId;
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
2065
|
+
let result;
|
|
2066
|
+
try {
|
|
2067
|
+
result = await this.request({
|
|
2068
|
+
service,
|
|
2069
|
+
...entry.apiBaseUrl ? { baseUrl: entry.apiBaseUrl } : {},
|
|
2070
|
+
method: entry.method,
|
|
2071
|
+
path: mapped.path,
|
|
2072
|
+
...mapped.query ? { query: mapped.query } : {},
|
|
2073
|
+
...mapped.body !== void 0 ? { body: mapped.body } : {},
|
|
2074
|
+
...options.resource ? { resource: options.resource } : {},
|
|
2075
|
+
...resolvedActorEnvelope ? { actorEnvelope: resolvedActorEnvelope } : {},
|
|
2076
|
+
headers: {
|
|
2077
|
+
[TOPOLO_ACTION_ID_HEADER]: entry.actionId,
|
|
2078
|
+
[TOPOLO_ACTION_NAME_HEADER]: entry.name
|
|
2079
|
+
},
|
|
2080
|
+
confirm: options.confirm === true || entry.readOnly && !entry.destructive && !entry.requiresConfirmation
|
|
2081
|
+
});
|
|
2082
|
+
} catch (error) {
|
|
2083
|
+
throw maskActionSecretError(error, this.actionSecrets);
|
|
2084
|
+
}
|
|
2085
|
+
return preserveSensitiveResult ? result : await protectActionResult(result, this.actionSecretStore);
|
|
2086
|
+
}
|
|
2087
|
+
async releaseActionSecret(reference) {
|
|
2088
|
+
if (!this.actionSecretStore) throw new TopoloSdkError("action_secret_store_required", "Protected action references require a secret store.");
|
|
2089
|
+
await this.actionSecretStore.delete(reference);
|
|
1996
2090
|
}
|
|
1997
2091
|
async buildVerifiedAgentActorEnvelope(actionId, input, resource) {
|
|
1998
2092
|
if (!this.actorContext) return void 0;
|
|
@@ -2139,7 +2233,7 @@ function resolveConfiguredAppId(slug, appIds) {
|
|
|
2139
2233
|
return null;
|
|
2140
2234
|
}
|
|
2141
2235
|
function normalizeApiKeyResourceMetadata(input) {
|
|
2142
|
-
if (!
|
|
2236
|
+
if (!isRecord4(input)) return null;
|
|
2143
2237
|
const resourceType = readString(input.resourceType ?? input.resource_type);
|
|
2144
2238
|
const label = readString(input.label);
|
|
2145
2239
|
const emptyLabel = readString(input.emptyLabel ?? input.empty_label);
|
|
@@ -2173,7 +2267,7 @@ function normalizeApiKeyResourceMetadataList(input) {
|
|
|
2173
2267
|
return resources;
|
|
2174
2268
|
}
|
|
2175
2269
|
function normalizeBindableResource(input) {
|
|
2176
|
-
if (!
|
|
2270
|
+
if (!isRecord4(input)) return null;
|
|
2177
2271
|
const resourceType = readString(input.resourceType ?? input.resource_type);
|
|
2178
2272
|
const resourceId = readString(input.resourceId ?? input.resource_id);
|
|
2179
2273
|
if (!resourceType || !resourceId) return null;
|
|
@@ -2184,13 +2278,13 @@ function normalizeBindableResource(input) {
|
|
|
2184
2278
|
slug: readOptionalString(input.slug),
|
|
2185
2279
|
status: readOptionalString(input.status),
|
|
2186
2280
|
organizationId: readOptionalString(input.organizationId ?? input.organization_id),
|
|
2187
|
-
metadata:
|
|
2281
|
+
metadata: isRecord4(input.metadata) ? input.metadata : {}
|
|
2188
2282
|
};
|
|
2189
2283
|
}
|
|
2190
2284
|
function normalizeResourceBindings(input) {
|
|
2191
2285
|
if (!Array.isArray(input)) return [];
|
|
2192
2286
|
return input.flatMap((binding) => {
|
|
2193
|
-
if (!
|
|
2287
|
+
if (!isRecord4(binding)) return [];
|
|
2194
2288
|
const resourceType = readString(binding.resourceType ?? binding.resource_type);
|
|
2195
2289
|
const resourceId = readString(binding.resourceId ?? binding.resource_id);
|
|
2196
2290
|
return resourceType && resourceId ? [{ resourceType, resourceId }] : [];
|
|
@@ -2203,30 +2297,9 @@ function readOptionalString(value) {
|
|
|
2203
2297
|
const normalized = readString(value);
|
|
2204
2298
|
return normalized || null;
|
|
2205
2299
|
}
|
|
2206
|
-
function
|
|
2300
|
+
function isRecord4(value) {
|
|
2207
2301
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2208
2302
|
}
|
|
2209
|
-
function redactActionResult(value) {
|
|
2210
|
-
if (Array.isArray(value)) return value.map(redactActionResult);
|
|
2211
|
-
if (!isRecord3(value)) return value;
|
|
2212
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
2213
|
-
key,
|
|
2214
|
-
isSensitiveActionResultKey(key, item) ? "[REDACTED]" : redactActionResult(item)
|
|
2215
|
-
]));
|
|
2216
|
-
}
|
|
2217
|
-
function isSensitiveActionResultKey(key, value) {
|
|
2218
|
-
const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
2219
|
-
if (normalized === "capability") return isCredentialShaped(value);
|
|
2220
|
-
return ["authorization", "cookie", "setcookie", "proxyauthorization"].includes(normalized) || /(token|secret|password|privatekey|apikey)$/.test(normalized);
|
|
2221
|
-
}
|
|
2222
|
-
var PLAIN_ENUM_VALUE = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
2223
|
-
function isCredentialShaped(value) {
|
|
2224
|
-
if (typeof value !== "string") return value !== null && value !== void 0 && typeof value !== "boolean" && typeof value !== "number";
|
|
2225
|
-
const trimmed = value.trim();
|
|
2226
|
-
if (!trimmed) return false;
|
|
2227
|
-
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) return true;
|
|
2228
|
-
return !PLAIN_ENUM_VALUE.test(trimmed);
|
|
2229
|
-
}
|
|
2230
2303
|
function resolveConfiguredServiceUrl(slug, serviceUrls) {
|
|
2231
2304
|
for (const serviceKey of serviceKeyCandidates2(slug)) {
|
|
2232
2305
|
const configured = serviceUrls?.[serviceKey];
|
|
@@ -2296,14 +2369,14 @@ function catalogSearchQuery(options) {
|
|
|
2296
2369
|
return query;
|
|
2297
2370
|
}
|
|
2298
2371
|
function normalizeCatalogPage(value) {
|
|
2299
|
-
if (!
|
|
2372
|
+
if (!isRecord4(value)) return { nextCursor: null, hasMore: false };
|
|
2300
2373
|
return {
|
|
2301
2374
|
nextCursor: typeof value.nextCursor === "string" && value.nextCursor ? value.nextCursor : null,
|
|
2302
2375
|
hasMore: value.hasMore === true
|
|
2303
2376
|
};
|
|
2304
2377
|
}
|
|
2305
2378
|
function normalizeServiceCatalogEntry(item) {
|
|
2306
|
-
if (!
|
|
2379
|
+
if (!isRecord4(item)) return null;
|
|
2307
2380
|
const appId = typeof item.app_id === "string" ? item.app_id : typeof item.id === "string" ? item.id : null;
|
|
2308
2381
|
if (!appId) return null;
|
|
2309
2382
|
const permissions = Array.isArray(item.granted_permissions) ? item.granted_permissions.filter(
|
|
@@ -3254,11 +3327,13 @@ function createTopolo(options) {
|
|
|
3254
3327
|
};
|
|
3255
3328
|
}
|
|
3256
3329
|
|
|
3257
|
-
// node_modules/.pnpm/@topolo+sdk@0.21.
|
|
3330
|
+
// node_modules/.pnpm/@topolo+sdk@0.21.3/node_modules/@topolo/sdk/dist/node/credential-store.js
|
|
3258
3331
|
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2, rm as rm2, stat } from "fs/promises";
|
|
3259
3332
|
import { existsSync as existsSync2 } from "fs";
|
|
3260
3333
|
import { homedir as homedir2, platform } from "os";
|
|
3261
|
-
import { dirname as dirname2, join as
|
|
3334
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
3335
|
+
import { createHash, randomUUID } from "crypto";
|
|
3336
|
+
import { join as join2 } from "path";
|
|
3262
3337
|
import { spawnSync } from "child_process";
|
|
3263
3338
|
import { mkdir, readFile, readdir, rename, rm, writeFile, chmod } from "fs/promises";
|
|
3264
3339
|
import { existsSync } from "fs";
|
|
@@ -3610,66 +3685,7 @@ var TopoloOAuth = class {
|
|
|
3610
3685
|
return parsed;
|
|
3611
3686
|
}
|
|
3612
3687
|
};
|
|
3613
|
-
var
|
|
3614
|
-
production: {
|
|
3615
|
-
auth: "https://auth.topolo.app",
|
|
3616
|
-
developers: "https://developers.topolo.app"
|
|
3617
|
-
},
|
|
3618
|
-
staging: {
|
|
3619
|
-
auth: "https://auth.stg.topolo.us",
|
|
3620
|
-
developers: "https://developers.stg.topolo.us"
|
|
3621
|
-
},
|
|
3622
|
-
development: {
|
|
3623
|
-
auth: "https://auth.topolo.dev",
|
|
3624
|
-
developers: "https://developers.topolo.dev"
|
|
3625
|
-
},
|
|
3626
|
-
local: {}
|
|
3627
|
-
};
|
|
3628
|
-
function hostnameHasSuffix2(hostname, suffix) {
|
|
3629
|
-
return hostname === suffix || hostname.endsWith(`.${suffix}`);
|
|
3630
|
-
}
|
|
3631
|
-
function workersDevAccountMatches2(hostname, account) {
|
|
3632
|
-
if (!hostname.endsWith(".workers.dev")) return false;
|
|
3633
|
-
const labels = hostname.slice(0, -".workers.dev".length).split(".");
|
|
3634
|
-
return labels.length >= 2 && labels.at(-1) === account;
|
|
3635
|
-
}
|
|
3636
|
-
function originMatchesEnvironment2(url, environment) {
|
|
3637
|
-
const hostname = url.hostname.toLowerCase();
|
|
3638
|
-
if (environment === "local") {
|
|
3639
|
-
return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
3640
|
-
}
|
|
3641
|
-
if (url.protocol !== "https:") return false;
|
|
3642
|
-
if (environment === "production") {
|
|
3643
|
-
return hostnameHasSuffix2(hostname, "topolo.app") || hostnameHasSuffix2(hostname, "topolo.io") || hostname === "api.ol0.me" || workersDevAccountMatches2(hostname, "topolo");
|
|
3644
|
-
}
|
|
3645
|
-
if (environment === "staging") {
|
|
3646
|
-
return hostnameHasSuffix2(hostname, "stg.topolo.us") || workersDevAccountMatches2(hostname, "topolo-staging");
|
|
3647
|
-
}
|
|
3648
|
-
return hostnameHasSuffix2(hostname, "topolo.dev") || workersDevAccountMatches2(hostname, "topolo-development");
|
|
3649
|
-
}
|
|
3650
|
-
function assertTopoloServiceUrlEnvironment2(service, value, environment) {
|
|
3651
|
-
let url;
|
|
3652
|
-
try {
|
|
3653
|
-
url = new URL(value);
|
|
3654
|
-
} catch {
|
|
3655
|
-
throw new TopoloSdkError2("invalid_service_url", `Invalid URL configured for Topolo service "${service}".`);
|
|
3656
|
-
}
|
|
3657
|
-
if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || !originMatchesEnvironment2(url, environment)) {
|
|
3658
|
-
throw new TopoloSdkError2(
|
|
3659
|
-
"environment_mismatch",
|
|
3660
|
-
`Refusing Topolo service "${service}": ${url.origin} does not belong to ${environment}.`
|
|
3661
|
-
);
|
|
3662
|
-
}
|
|
3663
|
-
return url.origin;
|
|
3664
|
-
}
|
|
3665
|
-
function topoloServiceOrigins2(serviceUrls, environment) {
|
|
3666
|
-
return Object.fromEntries(
|
|
3667
|
-
Object.entries(serviceUrls ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([service, value]) => [
|
|
3668
|
-
service,
|
|
3669
|
-
assertTopoloServiceUrlEnvironment2(service, value, environment)
|
|
3670
|
-
])
|
|
3671
|
-
);
|
|
3672
|
-
}
|
|
3688
|
+
var ACTION_SECRET_PREFIX2 = "topolo-secret:";
|
|
3673
3689
|
var KEYCHAIN_SERVICE = "topolo-external-agent-installation";
|
|
3674
3690
|
var MACOS_KEYCHAIN_CHUNK_SIZE = 96;
|
|
3675
3691
|
var MACOS_KEYCHAIN_CHUNK_PREFIX = "topolo-keychain-chunks:v1:";
|
|
@@ -3712,6 +3728,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
|
|
|
3712
3728
|
const platform2 = options.platform ?? hostPlatform();
|
|
3713
3729
|
const spawn = options.spawn ?? ((command, args, spawnOptions) => spawnSync(command, args, spawnOptions));
|
|
3714
3730
|
const root = options.root ?? installationStorePath();
|
|
3731
|
+
const service = options.service ?? KEYCHAIN_SERVICE;
|
|
3715
3732
|
if (platform2 === "darwin") {
|
|
3716
3733
|
const write = (account, value) => runOrThrow(spawn("/usr/bin/expect", ["-c", MACOS_KEYCHAIN_WRITE_SCRIPT], {
|
|
3717
3734
|
input: `${value}
|
|
@@ -3720,7 +3737,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
|
|
|
3720
3737
|
env: {
|
|
3721
3738
|
...process.env,
|
|
3722
3739
|
TOPOLO_KEYCHAIN_ACCOUNT: account,
|
|
3723
|
-
TOPOLO_KEYCHAIN_SERVICE:
|
|
3740
|
+
TOPOLO_KEYCHAIN_SERVICE: service
|
|
3724
3741
|
}
|
|
3725
3742
|
}), "macOS Keychain write");
|
|
3726
3743
|
const read = (account) => {
|
|
@@ -3729,7 +3746,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
|
|
|
3729
3746
|
"-a",
|
|
3730
3747
|
account,
|
|
3731
3748
|
"-s",
|
|
3732
|
-
|
|
3749
|
+
service,
|
|
3733
3750
|
"-w"
|
|
3734
3751
|
], { encoding: "utf8" });
|
|
3735
3752
|
if (result.status === 44) return null;
|
|
@@ -3741,7 +3758,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
|
|
|
3741
3758
|
"-a",
|
|
3742
3759
|
account,
|
|
3743
3760
|
"-s",
|
|
3744
|
-
|
|
3761
|
+
service
|
|
3745
3762
|
], { encoding: "utf8" });
|
|
3746
3763
|
if (result.status !== 44) runOrThrow(result, "macOS Keychain delete");
|
|
3747
3764
|
};
|
|
@@ -3788,9 +3805,9 @@ function createPlatformInstallationSecretBackend(options = {}) {
|
|
|
3788
3805
|
async set(account, value) {
|
|
3789
3806
|
runOrThrow(spawn("secret-tool", [
|
|
3790
3807
|
"store",
|
|
3791
|
-
"--label=Topolo
|
|
3808
|
+
"--label=Topolo protected credential",
|
|
3792
3809
|
"service",
|
|
3793
|
-
|
|
3810
|
+
service,
|
|
3794
3811
|
"account",
|
|
3795
3812
|
account
|
|
3796
3813
|
], { input: value, encoding: "utf8" }), "Linux Secret Service write");
|
|
@@ -3799,7 +3816,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
|
|
|
3799
3816
|
const result = spawn("secret-tool", [
|
|
3800
3817
|
"lookup",
|
|
3801
3818
|
"service",
|
|
3802
|
-
|
|
3819
|
+
service,
|
|
3803
3820
|
"account",
|
|
3804
3821
|
account
|
|
3805
3822
|
], { encoding: "utf8" });
|
|
@@ -3810,7 +3827,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
|
|
|
3810
3827
|
const result = spawn("secret-tool", [
|
|
3811
3828
|
"clear",
|
|
3812
3829
|
"service",
|
|
3813
|
-
|
|
3830
|
+
service,
|
|
3814
3831
|
"account",
|
|
3815
3832
|
account
|
|
3816
3833
|
], { encoding: "utf8" });
|
|
@@ -3853,12 +3870,119 @@ async function readExternalAgentInstallation(environment, id, backend = createPl
|
|
|
3853
3870
|
if (!encodedSecret) throw new Error(`Protected secret is missing for installation ${id}.`);
|
|
3854
3871
|
return { installation, secret: JSON.parse(encodedSecret) };
|
|
3855
3872
|
}
|
|
3873
|
+
function createProtectedActionSecretStore(options) {
|
|
3874
|
+
const backend = options.backend ?? createPlatformInstallationSecretBackend({
|
|
3875
|
+
service: "topolo-action-handoff",
|
|
3876
|
+
root: join2(installationStorePath(), "action-handoff")
|
|
3877
|
+
});
|
|
3878
|
+
const now = options.now ?? Date.now;
|
|
3879
|
+
async function account(reference) {
|
|
3880
|
+
if (!/^topolo-secret:[0-9a-f-]{36}$/.test(reference)) throw new TopoloSdkError2("invalid_action_secret_reference", "Invalid protected action reference.");
|
|
3881
|
+
const identity = await options.identity();
|
|
3882
|
+
if (!identity.user.id) throw new TopoloSdkError2("action_secret_identity_required", "A verified identity is required for protected action handoff.");
|
|
3883
|
+
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.");
|
|
3884
|
+
const scope = createHash("sha256").update(JSON.stringify([
|
|
3885
|
+
options.environment,
|
|
3886
|
+
identity.user.principalId ?? identity.user.id,
|
|
3887
|
+
identity.organization?.id ?? null,
|
|
3888
|
+
identity.apiKey?.id ?? null
|
|
3889
|
+
])).digest("hex");
|
|
3890
|
+
return `${scope}-${reference.slice(ACTION_SECRET_PREFIX2.length)}`;
|
|
3891
|
+
}
|
|
3892
|
+
return {
|
|
3893
|
+
async save(value) {
|
|
3894
|
+
const reference = `${ACTION_SECRET_PREFIX2}${randomUUID()}`;
|
|
3895
|
+
await backend.set(await account(reference), JSON.stringify({ value, expiresAt: now() + 30 * 60 * 1e3 }));
|
|
3896
|
+
return reference;
|
|
3897
|
+
},
|
|
3898
|
+
async load(reference) {
|
|
3899
|
+
const key = await account(reference);
|
|
3900
|
+
const encoded = await backend.get(key);
|
|
3901
|
+
if (!encoded) throw new TopoloSdkError2("action_secret_unavailable", "Protected action reference is missing or belongs to another identity or environment.");
|
|
3902
|
+
let record2;
|
|
3903
|
+
try {
|
|
3904
|
+
record2 = JSON.parse(encoded);
|
|
3905
|
+
} catch {
|
|
3906
|
+
throw new TopoloSdkError2("action_secret_invalid", "Protected action reference is corrupt.");
|
|
3907
|
+
}
|
|
3908
|
+
if (!Number.isFinite(record2?.expiresAt) || !Object.hasOwn(record2, "value")) throw new TopoloSdkError2("action_secret_invalid", "Protected action reference is corrupt.");
|
|
3909
|
+
if (record2.expiresAt <= now()) {
|
|
3910
|
+
await backend.delete(key);
|
|
3911
|
+
throw new TopoloSdkError2("action_secret_expired", "Protected action reference expired. Repeat the originating action if its contract permits.");
|
|
3912
|
+
}
|
|
3913
|
+
return record2.value;
|
|
3914
|
+
},
|
|
3915
|
+
async delete(reference) {
|
|
3916
|
+
await backend.delete(await account(reference));
|
|
3917
|
+
}
|
|
3918
|
+
};
|
|
3919
|
+
}
|
|
3920
|
+
var TOPOLO_ENVIRONMENT_SERVICE_URLS = {
|
|
3921
|
+
production: {
|
|
3922
|
+
auth: "https://auth.topolo.app",
|
|
3923
|
+
developers: "https://developers.topolo.app"
|
|
3924
|
+
},
|
|
3925
|
+
staging: {
|
|
3926
|
+
auth: "https://auth.stg.topolo.us",
|
|
3927
|
+
developers: "https://developers.stg.topolo.us"
|
|
3928
|
+
},
|
|
3929
|
+
development: {
|
|
3930
|
+
auth: "https://auth.topolo.dev",
|
|
3931
|
+
developers: "https://developers.topolo.dev"
|
|
3932
|
+
},
|
|
3933
|
+
local: {}
|
|
3934
|
+
};
|
|
3935
|
+
function hostnameHasSuffix2(hostname, suffix) {
|
|
3936
|
+
return hostname === suffix || hostname.endsWith(`.${suffix}`);
|
|
3937
|
+
}
|
|
3938
|
+
function workersDevAccountMatches2(hostname, account) {
|
|
3939
|
+
if (!hostname.endsWith(".workers.dev")) return false;
|
|
3940
|
+
const labels = hostname.slice(0, -".workers.dev".length).split(".");
|
|
3941
|
+
return labels.length >= 2 && labels.at(-1) === account;
|
|
3942
|
+
}
|
|
3943
|
+
function originMatchesEnvironment2(url, environment) {
|
|
3944
|
+
const hostname = url.hostname.toLowerCase();
|
|
3945
|
+
if (environment === "local") {
|
|
3946
|
+
return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
3947
|
+
}
|
|
3948
|
+
if (url.protocol !== "https:") return false;
|
|
3949
|
+
if (environment === "production") {
|
|
3950
|
+
return hostnameHasSuffix2(hostname, "topolo.app") || hostnameHasSuffix2(hostname, "topolo.io") || hostname === "api.ol0.me" || workersDevAccountMatches2(hostname, "topolo");
|
|
3951
|
+
}
|
|
3952
|
+
if (environment === "staging") {
|
|
3953
|
+
return hostnameHasSuffix2(hostname, "stg.topolo.us") || workersDevAccountMatches2(hostname, "topolo-staging");
|
|
3954
|
+
}
|
|
3955
|
+
return hostnameHasSuffix2(hostname, "topolo.dev") || workersDevAccountMatches2(hostname, "topolo-development");
|
|
3956
|
+
}
|
|
3957
|
+
function assertTopoloServiceUrlEnvironment2(service, value, environment) {
|
|
3958
|
+
let url;
|
|
3959
|
+
try {
|
|
3960
|
+
url = new URL(value);
|
|
3961
|
+
} catch {
|
|
3962
|
+
throw new TopoloSdkError2("invalid_service_url", `Invalid URL configured for Topolo service "${service}".`);
|
|
3963
|
+
}
|
|
3964
|
+
if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || !originMatchesEnvironment2(url, environment)) {
|
|
3965
|
+
throw new TopoloSdkError2(
|
|
3966
|
+
"environment_mismatch",
|
|
3967
|
+
`Refusing Topolo service "${service}": ${url.origin} does not belong to ${environment}.`
|
|
3968
|
+
);
|
|
3969
|
+
}
|
|
3970
|
+
return url.origin;
|
|
3971
|
+
}
|
|
3972
|
+
function topoloServiceOrigins2(serviceUrls, environment) {
|
|
3973
|
+
return Object.fromEntries(
|
|
3974
|
+
Object.entries(serviceUrls ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([service, value]) => [
|
|
3975
|
+
service,
|
|
3976
|
+
assertTopoloServiceUrlEnvironment2(service, value, environment)
|
|
3977
|
+
])
|
|
3978
|
+
);
|
|
3979
|
+
}
|
|
3856
3980
|
var ENVIRONMENT_DEFAULT_SERVICE_URLS = TOPOLO_ENVIRONMENT_SERVICE_URLS;
|
|
3857
3981
|
function configPath() {
|
|
3858
3982
|
const override = process.env.TOPOLO_CONFIG_PATH;
|
|
3859
3983
|
if (override) return override;
|
|
3860
|
-
const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ?
|
|
3861
|
-
return
|
|
3984
|
+
const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ? join3(homedir2(), "AppData", "Roaming") : join3(homedir2(), ".config"));
|
|
3985
|
+
return join3(base, "topolo", "config.json");
|
|
3862
3986
|
}
|
|
3863
3987
|
async function readConfig() {
|
|
3864
3988
|
const path = configPath();
|
|
@@ -4152,6 +4276,28 @@ function publicMcpErrorMessage(error, fallback) {
|
|
|
4152
4276
|
return error instanceof TopoloMcpPublicError ? error.message : fallback;
|
|
4153
4277
|
}
|
|
4154
4278
|
|
|
4279
|
+
// src/action-secret-tools.ts
|
|
4280
|
+
var actionSecretTools = [{
|
|
4281
|
+
name: "topolo_release_action_secret",
|
|
4282
|
+
title: "Release a protected action reference",
|
|
4283
|
+
description: "Deletes only a local protected handoff reference for the verified identity and environment. Does not revoke the upstream credential. Requires confirm:true.",
|
|
4284
|
+
requiredScopes: [],
|
|
4285
|
+
destructive: true,
|
|
4286
|
+
inputSchema: {
|
|
4287
|
+
type: "object",
|
|
4288
|
+
properties: { reference: { type: "string", minLength: 1 }, confirm: { type: "boolean" } },
|
|
4289
|
+
required: ["reference", "confirm"],
|
|
4290
|
+
additionalProperties: false
|
|
4291
|
+
},
|
|
4292
|
+
handler: async (topolo, args) => {
|
|
4293
|
+
const reference = typeof args.reference === "string" ? args.reference.trim() : "";
|
|
4294
|
+
if (!reference) throw new TopoloMcpPublicError("reference is required.");
|
|
4295
|
+
if (args.confirm !== true) throw new TopoloMcpPublicError("Releasing a protected action reference requires confirm:true.");
|
|
4296
|
+
await topolo.client.releaseActionSecret(reference);
|
|
4297
|
+
return { released: true };
|
|
4298
|
+
}
|
|
4299
|
+
}];
|
|
4300
|
+
|
|
4155
4301
|
// src/tools.ts
|
|
4156
4302
|
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
4157
4303
|
var AGENT_INVOCATIONS_CREATE = "app_topolo_agent.agent_invocations.create";
|
|
@@ -4230,6 +4376,7 @@ function agentRunResult(invocation) {
|
|
|
4230
4376
|
};
|
|
4231
4377
|
}
|
|
4232
4378
|
var TOOLS = [
|
|
4379
|
+
...actionSecretTools,
|
|
4233
4380
|
{
|
|
4234
4381
|
name: "topolo_whoami",
|
|
4235
4382
|
title: "Describe the current Topolo credential",
|
|
@@ -4912,14 +5059,14 @@ function displayAppId(entry) {
|
|
|
4912
5059
|
|
|
4913
5060
|
// src/version.ts
|
|
4914
5061
|
import { readFileSync } from "fs";
|
|
4915
|
-
import { dirname as dirname3, join as
|
|
5062
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
4916
5063
|
import { fileURLToPath } from "url";
|
|
4917
5064
|
function readMcpVersion() {
|
|
4918
5065
|
try {
|
|
4919
5066
|
const here = dirname3(fileURLToPath(import.meta.url));
|
|
4920
5067
|
const candidates = [
|
|
4921
|
-
|
|
4922
|
-
|
|
5068
|
+
join4(here, "package.json"),
|
|
5069
|
+
join4(here, "..", "package.json")
|
|
4923
5070
|
];
|
|
4924
5071
|
for (const path of candidates) {
|
|
4925
5072
|
try {
|
|
@@ -5042,7 +5189,7 @@ function toolError(context, error) {
|
|
|
5042
5189
|
}
|
|
5043
5190
|
|
|
5044
5191
|
// src/context-credential.ts
|
|
5045
|
-
import { randomUUID } from "crypto";
|
|
5192
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
5046
5193
|
|
|
5047
5194
|
// src/auth.ts
|
|
5048
5195
|
function resolveCredentialFromEnv(environment = "production") {
|
|
@@ -5186,7 +5333,7 @@ async function resolveMcpCredential(environment, selector = process.env.TOPOLO_C
|
|
|
5186
5333
|
"TOPOLO_AGENT_INSTALLATION_ID cannot be combined with TOPOLO_CONTEXT or an environment credential"
|
|
5187
5334
|
);
|
|
5188
5335
|
}
|
|
5189
|
-
const taskId = taskSelector?.trim() || `task_${
|
|
5336
|
+
const taskId = taskSelector?.trim() || `task_${randomUUID2()}`;
|
|
5190
5337
|
const config2 = await dependencies.readConfig();
|
|
5191
5338
|
const serviceUrls = resolveServiceUrls(
|
|
5192
5339
|
profileForEnvironment(config2, environment).serviceUrls,
|
|
@@ -5285,6 +5432,7 @@ var DEFAULT_DEPENDENCIES2 = {
|
|
|
5285
5432
|
resolveAgentName,
|
|
5286
5433
|
resolveServiceUrls,
|
|
5287
5434
|
createTopolo,
|
|
5435
|
+
createProtectedActionSecretStore,
|
|
5288
5436
|
buildServer,
|
|
5289
5437
|
createTransport: createStdioTransport,
|
|
5290
5438
|
advertisedToolNames,
|
|
@@ -5307,6 +5455,7 @@ async function runStdioMcp(dependencies = DEFAULT_DEPENDENCIES2) {
|
|
|
5307
5455
|
const topolo = dependencies.createTopolo({
|
|
5308
5456
|
credential,
|
|
5309
5457
|
environment,
|
|
5458
|
+
actionSecretStore: dependencies.createProtectedActionSecretStore({ environment, identity: () => topolo.client.introspect() }),
|
|
5310
5459
|
actorContext: {
|
|
5311
5460
|
channel: "mcp",
|
|
5312
5461
|
...resolvedCredential.taskId ? { taskId: resolvedCredential.taskId } : {}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topolo/mcp",
|
|
3
3
|
"packageManager": "pnpm@10.17.1",
|
|
4
|
-
"version": "0.14.
|
|
5
|
-
"topoloPublishedVersion": "0.14.
|
|
4
|
+
"version": "0.14.3",
|
|
5
|
+
"topoloPublishedVersion": "0.14.3",
|
|
6
6
|
"description": "Model Context Protocol server for the Topolo platform. Exposes scope-gated tools that third-party agents (Claude, Codex, etc.) can call natively.",
|
|
7
7
|
"license": "UNLICENSED",
|
|
8
8
|
"author": "Topolo",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
39
|
-
"@topolo/sdk": "0.21.
|
|
39
|
+
"@topolo/sdk": "0.21.3"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "^20.12.0",
|