@topolo/mcp 0.14.2 → 0.14.4
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 +351 -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.4 resolves nonempty string credentials 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.4/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,132 @@ 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) throw new TopoloSdkError("action_secret_unsupported_type", "Protected action handoff requires a nonempty string credential.");
|
|
376
|
+
secrets.set(resolved, value);
|
|
377
|
+
return resolved;
|
|
378
|
+
}
|
|
379
|
+
if (Array.isArray(value)) return Promise.all(value.map((item) => resolveActionSecrets(item, store, secrets)));
|
|
380
|
+
if (!isRecord(value)) return value;
|
|
381
|
+
return Object.fromEntries(await Promise.all(Object.entries(value).map(async ([key, item]) => [key, await resolveActionSecrets(item, store, secrets)])));
|
|
382
|
+
}
|
|
383
|
+
async function protectActionResult(value, store) {
|
|
384
|
+
const secrets = /* @__PURE__ */ new Map();
|
|
385
|
+
async function protect(item, key = "") {
|
|
386
|
+
if (isActionSecretReference(item)) return item;
|
|
387
|
+
if (sensitive(key, item)) {
|
|
388
|
+
const reference = store ? await store.save(item) : "[REDACTED]";
|
|
389
|
+
if (typeof item === "string" && item) secrets.set(item, reference);
|
|
390
|
+
return reference;
|
|
391
|
+
}
|
|
392
|
+
if (Array.isArray(item)) return Promise.all(item.map((child) => protect(child)));
|
|
393
|
+
if (!isRecord(item)) return item;
|
|
394
|
+
return Object.fromEntries(await Promise.all(Object.entries(item).map(async ([name, child]) => [name, await protect(child, name)])));
|
|
395
|
+
}
|
|
396
|
+
const protectedValue = await protect(value);
|
|
397
|
+
async function protectEmbedded(item) {
|
|
398
|
+
if (typeof item === "string" && !isActionSecretReference(item) && maskActionSecrets(item, secrets) !== item) {
|
|
399
|
+
return store ? store.save(item) : maskActionSecrets(item, secrets);
|
|
400
|
+
}
|
|
401
|
+
if (Array.isArray(item)) return Promise.all(item.map(protectEmbedded));
|
|
402
|
+
if (!isRecord(item)) return item;
|
|
403
|
+
return Object.fromEntries(await Promise.all(Object.entries(item).map(async ([key, child]) => [key, await protectEmbedded(child)])));
|
|
404
|
+
}
|
|
405
|
+
return protectEmbedded(protectedValue);
|
|
406
|
+
}
|
|
407
|
+
function maskActionSecretValue(value, secrets) {
|
|
408
|
+
if (value instanceof Error) return maskActionSecretError(value, secrets);
|
|
409
|
+
if (typeof value === "string") return maskActionSecrets(value, secrets);
|
|
410
|
+
if (Array.isArray(value)) return value.map((item) => maskActionSecretValue(item, secrets));
|
|
411
|
+
if (!isRecord(value)) return value;
|
|
412
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, maskActionSecretValue(item, secrets)]));
|
|
413
|
+
}
|
|
414
|
+
function maskActionSecretError(error, secrets) {
|
|
415
|
+
if (!(error instanceof Error)) return maskActionSecretValue(error, secrets);
|
|
416
|
+
const masked = Object.create(Object.getPrototypeOf(error));
|
|
417
|
+
for (const key of Object.getOwnPropertyNames(error)) {
|
|
418
|
+
const descriptor = Object.getOwnPropertyDescriptor(error, key);
|
|
419
|
+
if ("value" in descriptor) descriptor.value = maskActionSecretValue(descriptor.value, secrets);
|
|
420
|
+
Object.defineProperty(masked, key, descriptor);
|
|
421
|
+
}
|
|
422
|
+
return masked;
|
|
423
|
+
}
|
|
424
|
+
var encoder = new TextEncoder();
|
|
425
|
+
function encodeBase64Url(value) {
|
|
426
|
+
const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
427
|
+
let binary = "";
|
|
428
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
429
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
430
|
+
}
|
|
431
|
+
function encodeJson(value) {
|
|
432
|
+
return encodeBase64Url(encoder.encode(JSON.stringify(value)));
|
|
433
|
+
}
|
|
434
|
+
async function sha256Base64Url(value) {
|
|
435
|
+
return encodeBase64Url(await crypto.subtle.digest("SHA-256", encoder.encode(value)));
|
|
436
|
+
}
|
|
437
|
+
function publicJwk(privateJwk) {
|
|
438
|
+
const { crv, kty, x, y } = privateJwk;
|
|
439
|
+
if (kty !== "EC" || crv !== "P-256" || !x || !y || !privateJwk.d) {
|
|
440
|
+
throw new TypeError("A private P-256 JSON Web Key is required for DPoP signing.");
|
|
441
|
+
}
|
|
442
|
+
return { crv, kty, x, y };
|
|
443
|
+
}
|
|
444
|
+
function canonicalTargetUrl(value) {
|
|
445
|
+
const url = new URL(value);
|
|
446
|
+
return `${url.origin}${url.pathname}`;
|
|
447
|
+
}
|
|
448
|
+
async function createDpopProof(input) {
|
|
449
|
+
const jwk = publicJwk(input.privateJwk);
|
|
450
|
+
const key = await crypto.subtle.importKey(
|
|
451
|
+
"jwk",
|
|
452
|
+
input.privateJwk,
|
|
453
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
454
|
+
false,
|
|
455
|
+
["sign"]
|
|
456
|
+
);
|
|
457
|
+
const header = encodeJson({ alg: "ES256", typ: "dpop+jwt", jwk });
|
|
458
|
+
const claims = encodeJson({
|
|
459
|
+
jti: crypto.randomUUID(),
|
|
460
|
+
iat: Math.floor(Date.now() / 1e3),
|
|
461
|
+
htm: input.method.toUpperCase(),
|
|
462
|
+
htu: canonicalTargetUrl(input.targetUrl),
|
|
463
|
+
ath: await sha256Base64Url(input.accessToken)
|
|
464
|
+
});
|
|
465
|
+
const signature = await crypto.subtle.sign(
|
|
466
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
467
|
+
key,
|
|
468
|
+
encoder.encode(`${header}.${claims}`)
|
|
469
|
+
);
|
|
470
|
+
return `${header}.${claims}.${encodeBase64Url(signature)}`;
|
|
471
|
+
}
|
|
394
472
|
function hostnameHasSuffix(hostname, suffix) {
|
|
395
473
|
return hostname === suffix || hostname.endsWith(`.${suffix}`);
|
|
396
474
|
}
|
|
@@ -564,9 +642,9 @@ function inspectSchema(schema, path, root, depth) {
|
|
|
564
642
|
return { path, code: "unsupported_schema_ref", message: `Cannot resolve schema reference ${schema.$ref}.` };
|
|
565
643
|
}
|
|
566
644
|
const children = [];
|
|
567
|
-
if (
|
|
568
|
-
if (
|
|
569
|
-
if (
|
|
645
|
+
if (isRecord2(schema.properties)) children.push(...Object.entries(schema.properties));
|
|
646
|
+
if (isRecord2(schema.$defs)) children.push(...Object.entries(schema.$defs));
|
|
647
|
+
if (isRecord2(schema.definitions)) children.push(...Object.entries(schema.definitions));
|
|
570
648
|
children.push(
|
|
571
649
|
["additionalProperties", schema.additionalProperties],
|
|
572
650
|
["propertyNames", schema.propertyNames],
|
|
@@ -576,7 +654,7 @@ function inspectSchema(schema, path, root, depth) {
|
|
|
576
654
|
...arraySchemaEntries("oneOf", schema.oneOf)
|
|
577
655
|
);
|
|
578
656
|
for (const [key, child] of children) {
|
|
579
|
-
if (!
|
|
657
|
+
if (!isRecord2(child)) continue;
|
|
580
658
|
const issue = inspectSchema(child, `${path}.${key}`, root, depth + 1);
|
|
581
659
|
if (issue) return issue;
|
|
582
660
|
}
|
|
@@ -600,7 +678,7 @@ function validateNode(schema, value, path, root, errors, depth) {
|
|
|
600
678
|
}
|
|
601
679
|
if (Array.isArray(schema.allOf)) {
|
|
602
680
|
for (const candidate of schema.allOf) {
|
|
603
|
-
if (
|
|
681
|
+
if (isRecord2(candidate)) validateNode(candidate, value, path, root, errors, depth + 1);
|
|
604
682
|
}
|
|
605
683
|
}
|
|
606
684
|
if (Array.isArray(schema.anyOf) && !schema.anyOf.some((candidate) => candidateMatches(candidate, value, root, depth))) {
|
|
@@ -630,7 +708,7 @@ function validateNode(schema, value, path, root, errors, depth) {
|
|
|
630
708
|
if (typeof value === "string") validateString(schema, value, path, errors);
|
|
631
709
|
if (typeof value === "number") validateNumber(schema, value, path, errors);
|
|
632
710
|
if (Array.isArray(value)) validateArray(schema, value, path, root, errors, depth);
|
|
633
|
-
if (
|
|
711
|
+
if (isRecord2(value)) validateObject(schema, value, path, root, errors, depth);
|
|
634
712
|
}
|
|
635
713
|
function validateObject(schema, value, path, root, errors, depth) {
|
|
636
714
|
if (typeof schema.minProperties === "number" && Object.keys(value).length < schema.minProperties) {
|
|
@@ -639,25 +717,25 @@ function validateObject(schema, value, path, root, errors, depth) {
|
|
|
639
717
|
if (typeof schema.maxProperties === "number" && Object.keys(value).length > schema.maxProperties) {
|
|
640
718
|
errors.push({ path, code: "max_properties", message: `Expected at most ${schema.maxProperties} properties.` });
|
|
641
719
|
}
|
|
642
|
-
const properties =
|
|
720
|
+
const properties = isRecord2(schema.properties) ? schema.properties : {};
|
|
643
721
|
const required = Array.isArray(schema.required) ? schema.required.map(String) : [];
|
|
644
722
|
for (const key of required) {
|
|
645
723
|
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
646
724
|
errors.push({ path: `${path}.${key}`, code: "required", message: "Required property is missing." });
|
|
647
725
|
}
|
|
648
726
|
}
|
|
649
|
-
if (
|
|
727
|
+
if (isRecord2(schema.propertyNames)) {
|
|
650
728
|
for (const key of Object.keys(value)) {
|
|
651
729
|
validateNode(schema.propertyNames, key, `${path}.${key}`, root, errors, depth + 1);
|
|
652
730
|
}
|
|
653
731
|
}
|
|
654
732
|
for (const [key, child] of Object.entries(value)) {
|
|
655
733
|
const childSchema = properties[key];
|
|
656
|
-
if (
|
|
734
|
+
if (isRecord2(childSchema)) {
|
|
657
735
|
validateNode(childSchema, child, `${path}.${key}`, root, errors, depth + 1);
|
|
658
736
|
} else if (schema.additionalProperties === false) {
|
|
659
737
|
errors.push({ path: `${path}.${key}`, code: "additional_property", message: "Property is not declared by the action schema." });
|
|
660
|
-
} else if (
|
|
738
|
+
} else if (isRecord2(schema.additionalProperties)) {
|
|
661
739
|
validateNode(schema.additionalProperties, child, `${path}.${key}`, root, errors, depth + 1);
|
|
662
740
|
}
|
|
663
741
|
}
|
|
@@ -673,7 +751,7 @@ function validateArray(schema, value, path, root, errors, depth) {
|
|
|
673
751
|
const unique = new Set(value.map((item) => JSON.stringify(item)));
|
|
674
752
|
if (unique.size !== value.length) errors.push({ path, code: "unique_items", message: "Expected every item to be unique." });
|
|
675
753
|
}
|
|
676
|
-
if (
|
|
754
|
+
if (isRecord2(schema.items)) {
|
|
677
755
|
value.forEach((item, index) => validateNode(schema.items, item, `${path}[${index}]`, root, errors, depth + 1));
|
|
678
756
|
}
|
|
679
757
|
}
|
|
@@ -711,7 +789,7 @@ function validateNumber(schema, value, path, errors) {
|
|
|
711
789
|
}
|
|
712
790
|
}
|
|
713
791
|
function candidateMatches(candidate, value, root, depth) {
|
|
714
|
-
if (!
|
|
792
|
+
if (!isRecord2(candidate)) return false;
|
|
715
793
|
const issues = [];
|
|
716
794
|
validateNode(candidate, value, "$", root, issues, depth + 1);
|
|
717
795
|
return issues.length === 0;
|
|
@@ -725,24 +803,24 @@ function resolveLocalReference(root, reference) {
|
|
|
725
803
|
current = current[Number(token)];
|
|
726
804
|
continue;
|
|
727
805
|
}
|
|
728
|
-
if (!
|
|
806
|
+
if (!isRecord2(current) || !Object.prototype.hasOwnProperty.call(current, token)) return null;
|
|
729
807
|
current = current[token];
|
|
730
808
|
}
|
|
731
|
-
return
|
|
809
|
+
return isRecord2(current) ? current : null;
|
|
732
810
|
}
|
|
733
811
|
function matchesType(type, value) {
|
|
734
812
|
if (type === void 0) return true;
|
|
735
813
|
if (Array.isArray(type)) return type.some((candidate) => matchesType(candidate, value));
|
|
736
814
|
if (type === "null") return value === null;
|
|
737
815
|
if (type === "array") return Array.isArray(value);
|
|
738
|
-
if (type === "object") return
|
|
816
|
+
if (type === "object") return isRecord2(value);
|
|
739
817
|
if (type === "integer") return typeof value === "number" && Number.isInteger(value);
|
|
740
818
|
return typeof value === type;
|
|
741
819
|
}
|
|
742
820
|
function sameJsonValue(left, right) {
|
|
743
821
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
744
822
|
}
|
|
745
|
-
function
|
|
823
|
+
function isRecord2(value) {
|
|
746
824
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
747
825
|
}
|
|
748
826
|
function isUrl(value) {
|
|
@@ -977,7 +1055,7 @@ function fail(path, message) {
|
|
|
977
1055
|
throw new ActorEnvelopeValidationError(path, message);
|
|
978
1056
|
}
|
|
979
1057
|
function parseTopoloActionUploadContract(value) {
|
|
980
|
-
if (!
|
|
1058
|
+
if (!isRecord3(value) || value.profile !== "inline-base64" && value.profile !== "url-session") {
|
|
981
1059
|
throw new TopoloSdkError("upload_contract_unavailable", "This action does not publish a supported upload contract.");
|
|
982
1060
|
}
|
|
983
1061
|
const contentTypeField = requiredField(value.contentTypeField, "contentTypeField");
|
|
@@ -1056,17 +1134,17 @@ function prepareTopoloUrlSessionUploadInput(contractValue, byteLength, input, op
|
|
|
1056
1134
|
};
|
|
1057
1135
|
}
|
|
1058
1136
|
function topoloUploadResponseValue(response, path) {
|
|
1059
|
-
return path.split(".").reduce((value, segment) =>
|
|
1137
|
+
return path.split(".").reduce((value, segment) => isRecord3(value) ? value[segment] : void 0, response);
|
|
1060
1138
|
}
|
|
1061
1139
|
function redactTopoloUrlSessionResult(response, uploadUrlPath) {
|
|
1062
1140
|
const redacted = cloneUploadValue(response);
|
|
1063
1141
|
let target = redacted;
|
|
1064
1142
|
const segments = uploadUrlPath.split(".");
|
|
1065
1143
|
for (const segment of segments.slice(0, -1)) {
|
|
1066
|
-
target =
|
|
1144
|
+
target = isRecord3(target) ? target[segment] : void 0;
|
|
1067
1145
|
}
|
|
1068
1146
|
const finalSegment = segments.at(-1);
|
|
1069
|
-
if (finalSegment &&
|
|
1147
|
+
if (finalSegment && isRecord3(target) && finalSegment in target) target[finalSegment] = "[REDACTED]";
|
|
1070
1148
|
redactUploadHeaders(redacted);
|
|
1071
1149
|
return redacted;
|
|
1072
1150
|
}
|
|
@@ -1090,7 +1168,7 @@ function mapUploadInput(contract, contentBase64, input, options) {
|
|
|
1090
1168
|
}
|
|
1091
1169
|
function cloneUploadValue(value) {
|
|
1092
1170
|
if (Array.isArray(value)) return value.map(cloneUploadValue);
|
|
1093
|
-
if (!
|
|
1171
|
+
if (!isRecord3(value)) return value;
|
|
1094
1172
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneUploadValue(item)]));
|
|
1095
1173
|
}
|
|
1096
1174
|
function redactUploadHeaders(value) {
|
|
@@ -1098,9 +1176,9 @@ function redactUploadHeaders(value) {
|
|
|
1098
1176
|
value.forEach(redactUploadHeaders);
|
|
1099
1177
|
return;
|
|
1100
1178
|
}
|
|
1101
|
-
if (!
|
|
1179
|
+
if (!isRecord3(value)) return;
|
|
1102
1180
|
for (const [key, item] of Object.entries(value)) {
|
|
1103
|
-
if (key.toLowerCase() === "headers" &&
|
|
1181
|
+
if (key.toLowerCase() === "headers" && isRecord3(item)) {
|
|
1104
1182
|
for (const header of Object.keys(item)) {
|
|
1105
1183
|
if (["authorization", "cookie", "set-cookie", "proxy-authorization", "x-api-key"].includes(header.toLowerCase())) {
|
|
1106
1184
|
item[header] = "[REDACTED]";
|
|
@@ -1129,7 +1207,7 @@ function decodedBase64Size(contentBase64) {
|
|
|
1129
1207
|
return contentBase64.length / 4 * 3 - padding;
|
|
1130
1208
|
}
|
|
1131
1209
|
function parseCompleteContract(value) {
|
|
1132
|
-
if (!
|
|
1210
|
+
if (!isRecord3(value)) throw new TopoloSdkError("invalid_upload_contract", "The action upload completion contract is invalid.");
|
|
1133
1211
|
return {
|
|
1134
1212
|
actionId: requiredPath(value.actionId, "complete.actionId"),
|
|
1135
1213
|
inputField: requiredField(value.inputField, "complete.inputField"),
|
|
@@ -1167,7 +1245,7 @@ function encodeTopoloActionBase64(bytes) {
|
|
|
1167
1245
|
}
|
|
1168
1246
|
return btoa(binary);
|
|
1169
1247
|
}
|
|
1170
|
-
function
|
|
1248
|
+
function isRecord3(value) {
|
|
1171
1249
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1172
1250
|
}
|
|
1173
1251
|
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
@@ -1178,6 +1256,8 @@ var TOPOLO_RESOURCE_ID_HEADER = "X-Topolo-Resource-ID";
|
|
|
1178
1256
|
var POSITIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
1179
1257
|
var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
|
|
1180
1258
|
var TopoloClient = class _TopoloClient {
|
|
1259
|
+
actionSecretStore;
|
|
1260
|
+
actionSecrets = /* @__PURE__ */ new Map();
|
|
1181
1261
|
credential;
|
|
1182
1262
|
agent;
|
|
1183
1263
|
actorContext;
|
|
@@ -1207,6 +1287,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1207
1287
|
_TopoloClient.appIdCache.clear();
|
|
1208
1288
|
}
|
|
1209
1289
|
constructor(options) {
|
|
1290
|
+
this.actionSecretStore = options.actionSecretStore;
|
|
1210
1291
|
if (!options.credential) throw new TopoloAuthError("credential is required");
|
|
1211
1292
|
if (!options.agent?.clientName) throw new TopoloAuthError("agent.clientName is required");
|
|
1212
1293
|
this.credential = options.credential;
|
|
@@ -1228,6 +1309,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1228
1309
|
}
|
|
1229
1310
|
emit(event) {
|
|
1230
1311
|
if (!this.debug) return;
|
|
1312
|
+
event = maskActionSecretValue(event, this.actionSecrets);
|
|
1231
1313
|
try {
|
|
1232
1314
|
this.debug(event);
|
|
1233
1315
|
} catch {
|
|
@@ -1613,7 +1695,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1613
1695
|
}
|
|
1614
1696
|
const entries = [];
|
|
1615
1697
|
for (const item of raw) {
|
|
1616
|
-
if (!
|
|
1698
|
+
if (!isRecord4(item)) continue;
|
|
1617
1699
|
const s = item;
|
|
1618
1700
|
const appId = typeof s.app_id === "string" ? s.app_id : typeof s.id === "string" ? s.id : null;
|
|
1619
1701
|
if (!appId) continue;
|
|
@@ -1688,8 +1770,8 @@ var TopoloClient = class _TopoloClient {
|
|
|
1688
1770
|
"app_topolo_auth.service_api_key_resources.list",
|
|
1689
1771
|
{ appId: service.appId }
|
|
1690
1772
|
);
|
|
1691
|
-
const payload =
|
|
1692
|
-
if (!
|
|
1773
|
+
const payload = isRecord4(response) && isRecord4(response.data) ? response.data : response;
|
|
1774
|
+
if (!isRecord4(payload) || !Array.isArray(payload.resources)) {
|
|
1693
1775
|
throw new TopoloSdkError("resource_catalog_unavailable", "TopoloAuth did not return bindable resources.");
|
|
1694
1776
|
}
|
|
1695
1777
|
return {
|
|
@@ -1870,17 +1952,20 @@ var TopoloClient = class _TopoloClient {
|
|
|
1870
1952
|
}
|
|
1871
1953
|
async validateActionInput(action, input = {}) {
|
|
1872
1954
|
const entry = await this.getAction(action);
|
|
1873
|
-
|
|
1955
|
+
const resolved = await resolveActionSecrets(input, this.actionSecretStore, this.actionSecrets);
|
|
1956
|
+
return { action: entry, validation: validateTopoloActionInput(entry.inputSchema, resolved) };
|
|
1874
1957
|
}
|
|
1875
1958
|
async planAction(action, input = {}, options = {}) {
|
|
1876
1959
|
const entry = await this.getAction(action);
|
|
1877
|
-
|
|
1960
|
+
const resolved = await resolveActionSecrets(input, this.actionSecretStore, this.actionSecrets);
|
|
1961
|
+
return this.planResolvedAction(entry, resolved, options);
|
|
1878
1962
|
}
|
|
1879
1963
|
async prepareAction(action, input, options = {}) {
|
|
1880
1964
|
const entry = await this.getAction(action);
|
|
1881
1965
|
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
1966
|
const selectedInput = input ?? example?.input ?? {};
|
|
1883
|
-
const
|
|
1967
|
+
const resolved = await resolveActionSecrets(selectedInput, this.actionSecretStore, this.actionSecrets);
|
|
1968
|
+
const plan = this.planResolvedAction(entry, resolved, options);
|
|
1884
1969
|
return {
|
|
1885
1970
|
action: entry,
|
|
1886
1971
|
example,
|
|
@@ -1917,7 +2002,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1917
2002
|
confirmationRequired,
|
|
1918
2003
|
confirmationProvided: options.confirm === true,
|
|
1919
2004
|
executable: validation.valid && agentAccess !== "off" && (!confirmationRequired || options.confirm === true),
|
|
1920
|
-
request,
|
|
2005
|
+
request: maskActionSecretValue(request, this.actionSecrets),
|
|
1921
2006
|
verification: stringArray(entry.agentContract?.verify),
|
|
1922
2007
|
rollbackActionId: pickActionString(entry.agentContract?.rollbackActionId),
|
|
1923
2008
|
nextActionIds: stringArray(entry.agentContract?.nextActionIds)
|
|
@@ -1947,6 +2032,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
1947
2032
|
`Topolo action "${entry.actionId}" requires { confirm: true } before invocation.`
|
|
1948
2033
|
);
|
|
1949
2034
|
}
|
|
2035
|
+
input = await resolveActionSecrets(input, this.actionSecretStore, this.actionSecrets);
|
|
1950
2036
|
const validation = validateTopoloActionInput(entry.inputSchema, input);
|
|
1951
2037
|
if (!validation.valid) {
|
|
1952
2038
|
throw new TopoloSdkError(
|
|
@@ -1977,22 +2063,31 @@ var TopoloClient = class _TopoloClient {
|
|
|
1977
2063
|
}
|
|
1978
2064
|
}
|
|
1979
2065
|
const service = entry.appSlug || entry.appId;
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
2066
|
+
let result;
|
|
2067
|
+
try {
|
|
2068
|
+
result = await this.request({
|
|
2069
|
+
service,
|
|
2070
|
+
...entry.apiBaseUrl ? { baseUrl: entry.apiBaseUrl } : {},
|
|
2071
|
+
method: entry.method,
|
|
2072
|
+
path: mapped.path,
|
|
2073
|
+
...mapped.query ? { query: mapped.query } : {},
|
|
2074
|
+
...mapped.body !== void 0 ? { body: mapped.body } : {},
|
|
2075
|
+
...options.resource ? { resource: options.resource } : {},
|
|
2076
|
+
...resolvedActorEnvelope ? { actorEnvelope: resolvedActorEnvelope } : {},
|
|
2077
|
+
headers: {
|
|
2078
|
+
[TOPOLO_ACTION_ID_HEADER]: entry.actionId,
|
|
2079
|
+
[TOPOLO_ACTION_NAME_HEADER]: entry.name
|
|
2080
|
+
},
|
|
2081
|
+
confirm: options.confirm === true || entry.readOnly && !entry.destructive && !entry.requiresConfirmation
|
|
2082
|
+
});
|
|
2083
|
+
} catch (error) {
|
|
2084
|
+
throw maskActionSecretError(error, this.actionSecrets);
|
|
2085
|
+
}
|
|
2086
|
+
return preserveSensitiveResult ? result : await protectActionResult(result, this.actionSecretStore);
|
|
2087
|
+
}
|
|
2088
|
+
async releaseActionSecret(reference) {
|
|
2089
|
+
if (!this.actionSecretStore) throw new TopoloSdkError("action_secret_store_required", "Protected action references require a secret store.");
|
|
2090
|
+
await this.actionSecretStore.delete(reference);
|
|
1996
2091
|
}
|
|
1997
2092
|
async buildVerifiedAgentActorEnvelope(actionId, input, resource) {
|
|
1998
2093
|
if (!this.actorContext) return void 0;
|
|
@@ -2139,7 +2234,7 @@ function resolveConfiguredAppId(slug, appIds) {
|
|
|
2139
2234
|
return null;
|
|
2140
2235
|
}
|
|
2141
2236
|
function normalizeApiKeyResourceMetadata(input) {
|
|
2142
|
-
if (!
|
|
2237
|
+
if (!isRecord4(input)) return null;
|
|
2143
2238
|
const resourceType = readString(input.resourceType ?? input.resource_type);
|
|
2144
2239
|
const label = readString(input.label);
|
|
2145
2240
|
const emptyLabel = readString(input.emptyLabel ?? input.empty_label);
|
|
@@ -2173,7 +2268,7 @@ function normalizeApiKeyResourceMetadataList(input) {
|
|
|
2173
2268
|
return resources;
|
|
2174
2269
|
}
|
|
2175
2270
|
function normalizeBindableResource(input) {
|
|
2176
|
-
if (!
|
|
2271
|
+
if (!isRecord4(input)) return null;
|
|
2177
2272
|
const resourceType = readString(input.resourceType ?? input.resource_type);
|
|
2178
2273
|
const resourceId = readString(input.resourceId ?? input.resource_id);
|
|
2179
2274
|
if (!resourceType || !resourceId) return null;
|
|
@@ -2184,13 +2279,13 @@ function normalizeBindableResource(input) {
|
|
|
2184
2279
|
slug: readOptionalString(input.slug),
|
|
2185
2280
|
status: readOptionalString(input.status),
|
|
2186
2281
|
organizationId: readOptionalString(input.organizationId ?? input.organization_id),
|
|
2187
|
-
metadata:
|
|
2282
|
+
metadata: isRecord4(input.metadata) ? input.metadata : {}
|
|
2188
2283
|
};
|
|
2189
2284
|
}
|
|
2190
2285
|
function normalizeResourceBindings(input) {
|
|
2191
2286
|
if (!Array.isArray(input)) return [];
|
|
2192
2287
|
return input.flatMap((binding) => {
|
|
2193
|
-
if (!
|
|
2288
|
+
if (!isRecord4(binding)) return [];
|
|
2194
2289
|
const resourceType = readString(binding.resourceType ?? binding.resource_type);
|
|
2195
2290
|
const resourceId = readString(binding.resourceId ?? binding.resource_id);
|
|
2196
2291
|
return resourceType && resourceId ? [{ resourceType, resourceId }] : [];
|
|
@@ -2203,30 +2298,9 @@ function readOptionalString(value) {
|
|
|
2203
2298
|
const normalized = readString(value);
|
|
2204
2299
|
return normalized || null;
|
|
2205
2300
|
}
|
|
2206
|
-
function
|
|
2301
|
+
function isRecord4(value) {
|
|
2207
2302
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2208
2303
|
}
|
|
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
2304
|
function resolveConfiguredServiceUrl(slug, serviceUrls) {
|
|
2231
2305
|
for (const serviceKey of serviceKeyCandidates2(slug)) {
|
|
2232
2306
|
const configured = serviceUrls?.[serviceKey];
|
|
@@ -2296,14 +2370,14 @@ function catalogSearchQuery(options) {
|
|
|
2296
2370
|
return query;
|
|
2297
2371
|
}
|
|
2298
2372
|
function normalizeCatalogPage(value) {
|
|
2299
|
-
if (!
|
|
2373
|
+
if (!isRecord4(value)) return { nextCursor: null, hasMore: false };
|
|
2300
2374
|
return {
|
|
2301
2375
|
nextCursor: typeof value.nextCursor === "string" && value.nextCursor ? value.nextCursor : null,
|
|
2302
2376
|
hasMore: value.hasMore === true
|
|
2303
2377
|
};
|
|
2304
2378
|
}
|
|
2305
2379
|
function normalizeServiceCatalogEntry(item) {
|
|
2306
|
-
if (!
|
|
2380
|
+
if (!isRecord4(item)) return null;
|
|
2307
2381
|
const appId = typeof item.app_id === "string" ? item.app_id : typeof item.id === "string" ? item.id : null;
|
|
2308
2382
|
if (!appId) return null;
|
|
2309
2383
|
const permissions = Array.isArray(item.granted_permissions) ? item.granted_permissions.filter(
|
|
@@ -3254,11 +3328,13 @@ function createTopolo(options) {
|
|
|
3254
3328
|
};
|
|
3255
3329
|
}
|
|
3256
3330
|
|
|
3257
|
-
// node_modules/.pnpm/@topolo+sdk@0.21.
|
|
3331
|
+
// node_modules/.pnpm/@topolo+sdk@0.21.4/node_modules/@topolo/sdk/dist/node/credential-store.js
|
|
3258
3332
|
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2, chmod as chmod2, rm as rm2, stat } from "fs/promises";
|
|
3259
3333
|
import { existsSync as existsSync2 } from "fs";
|
|
3260
3334
|
import { homedir as homedir2, platform } from "os";
|
|
3261
|
-
import { dirname as dirname2, join as
|
|
3335
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
3336
|
+
import { createHash, randomUUID } from "crypto";
|
|
3337
|
+
import { join as join2 } from "path";
|
|
3262
3338
|
import { spawnSync } from "child_process";
|
|
3263
3339
|
import { mkdir, readFile, readdir, rename, rm, writeFile, chmod } from "fs/promises";
|
|
3264
3340
|
import { existsSync } from "fs";
|
|
@@ -3610,66 +3686,7 @@ var TopoloOAuth = class {
|
|
|
3610
3686
|
return parsed;
|
|
3611
3687
|
}
|
|
3612
3688
|
};
|
|
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
|
-
}
|
|
3689
|
+
var ACTION_SECRET_PREFIX2 = "topolo-secret:";
|
|
3673
3690
|
var KEYCHAIN_SERVICE = "topolo-external-agent-installation";
|
|
3674
3691
|
var MACOS_KEYCHAIN_CHUNK_SIZE = 96;
|
|
3675
3692
|
var MACOS_KEYCHAIN_CHUNK_PREFIX = "topolo-keychain-chunks:v1:";
|
|
@@ -3712,6 +3729,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
|
|
|
3712
3729
|
const platform2 = options.platform ?? hostPlatform();
|
|
3713
3730
|
const spawn = options.spawn ?? ((command, args, spawnOptions) => spawnSync(command, args, spawnOptions));
|
|
3714
3731
|
const root = options.root ?? installationStorePath();
|
|
3732
|
+
const service = options.service ?? KEYCHAIN_SERVICE;
|
|
3715
3733
|
if (platform2 === "darwin") {
|
|
3716
3734
|
const write = (account, value) => runOrThrow(spawn("/usr/bin/expect", ["-c", MACOS_KEYCHAIN_WRITE_SCRIPT], {
|
|
3717
3735
|
input: `${value}
|
|
@@ -3720,7 +3738,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
|
|
|
3720
3738
|
env: {
|
|
3721
3739
|
...process.env,
|
|
3722
3740
|
TOPOLO_KEYCHAIN_ACCOUNT: account,
|
|
3723
|
-
TOPOLO_KEYCHAIN_SERVICE:
|
|
3741
|
+
TOPOLO_KEYCHAIN_SERVICE: service
|
|
3724
3742
|
}
|
|
3725
3743
|
}), "macOS Keychain write");
|
|
3726
3744
|
const read = (account) => {
|
|
@@ -3729,7 +3747,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
|
|
|
3729
3747
|
"-a",
|
|
3730
3748
|
account,
|
|
3731
3749
|
"-s",
|
|
3732
|
-
|
|
3750
|
+
service,
|
|
3733
3751
|
"-w"
|
|
3734
3752
|
], { encoding: "utf8" });
|
|
3735
3753
|
if (result.status === 44) return null;
|
|
@@ -3741,7 +3759,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
|
|
|
3741
3759
|
"-a",
|
|
3742
3760
|
account,
|
|
3743
3761
|
"-s",
|
|
3744
|
-
|
|
3762
|
+
service
|
|
3745
3763
|
], { encoding: "utf8" });
|
|
3746
3764
|
if (result.status !== 44) runOrThrow(result, "macOS Keychain delete");
|
|
3747
3765
|
};
|
|
@@ -3788,9 +3806,9 @@ function createPlatformInstallationSecretBackend(options = {}) {
|
|
|
3788
3806
|
async set(account, value) {
|
|
3789
3807
|
runOrThrow(spawn("secret-tool", [
|
|
3790
3808
|
"store",
|
|
3791
|
-
"--label=Topolo
|
|
3809
|
+
"--label=Topolo protected credential",
|
|
3792
3810
|
"service",
|
|
3793
|
-
|
|
3811
|
+
service,
|
|
3794
3812
|
"account",
|
|
3795
3813
|
account
|
|
3796
3814
|
], { input: value, encoding: "utf8" }), "Linux Secret Service write");
|
|
@@ -3799,7 +3817,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
|
|
|
3799
3817
|
const result = spawn("secret-tool", [
|
|
3800
3818
|
"lookup",
|
|
3801
3819
|
"service",
|
|
3802
|
-
|
|
3820
|
+
service,
|
|
3803
3821
|
"account",
|
|
3804
3822
|
account
|
|
3805
3823
|
], { encoding: "utf8" });
|
|
@@ -3810,7 +3828,7 @@ function createPlatformInstallationSecretBackend(options = {}) {
|
|
|
3810
3828
|
const result = spawn("secret-tool", [
|
|
3811
3829
|
"clear",
|
|
3812
3830
|
"service",
|
|
3813
|
-
|
|
3831
|
+
service,
|
|
3814
3832
|
"account",
|
|
3815
3833
|
account
|
|
3816
3834
|
], { encoding: "utf8" });
|
|
@@ -3853,12 +3871,119 @@ async function readExternalAgentInstallation(environment, id, backend = createPl
|
|
|
3853
3871
|
if (!encodedSecret) throw new Error(`Protected secret is missing for installation ${id}.`);
|
|
3854
3872
|
return { installation, secret: JSON.parse(encodedSecret) };
|
|
3855
3873
|
}
|
|
3874
|
+
function createProtectedActionSecretStore(options) {
|
|
3875
|
+
const backend = options.backend ?? createPlatformInstallationSecretBackend({
|
|
3876
|
+
service: "topolo-action-handoff",
|
|
3877
|
+
root: join2(installationStorePath(), "action-handoff")
|
|
3878
|
+
});
|
|
3879
|
+
const now = options.now ?? Date.now;
|
|
3880
|
+
async function account(reference) {
|
|
3881
|
+
if (!/^topolo-secret:[0-9a-f-]{36}$/.test(reference)) throw new TopoloSdkError2("invalid_action_secret_reference", "Invalid protected action reference.");
|
|
3882
|
+
const identity = await options.identity();
|
|
3883
|
+
if (!identity.user.id) throw new TopoloSdkError2("action_secret_identity_required", "A verified identity is required for protected action handoff.");
|
|
3884
|
+
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.");
|
|
3885
|
+
const scope = createHash("sha256").update(JSON.stringify([
|
|
3886
|
+
options.environment,
|
|
3887
|
+
identity.user.principalId ?? identity.user.id,
|
|
3888
|
+
identity.organization?.id ?? null,
|
|
3889
|
+
identity.apiKey?.id ?? null
|
|
3890
|
+
])).digest("hex");
|
|
3891
|
+
return `${scope}-${reference.slice(ACTION_SECRET_PREFIX2.length)}`;
|
|
3892
|
+
}
|
|
3893
|
+
return {
|
|
3894
|
+
async save(value) {
|
|
3895
|
+
const reference = `${ACTION_SECRET_PREFIX2}${randomUUID()}`;
|
|
3896
|
+
await backend.set(await account(reference), JSON.stringify({ value, expiresAt: now() + 30 * 60 * 1e3 }));
|
|
3897
|
+
return reference;
|
|
3898
|
+
},
|
|
3899
|
+
async load(reference) {
|
|
3900
|
+
const key = await account(reference);
|
|
3901
|
+
const encoded = await backend.get(key);
|
|
3902
|
+
if (!encoded) throw new TopoloSdkError2("action_secret_unavailable", "Protected action reference is missing or belongs to another identity or environment.");
|
|
3903
|
+
let record2;
|
|
3904
|
+
try {
|
|
3905
|
+
record2 = JSON.parse(encoded);
|
|
3906
|
+
} catch {
|
|
3907
|
+
throw new TopoloSdkError2("action_secret_invalid", "Protected action reference is corrupt.");
|
|
3908
|
+
}
|
|
3909
|
+
if (!Number.isFinite(record2?.expiresAt) || !Object.hasOwn(record2, "value")) throw new TopoloSdkError2("action_secret_invalid", "Protected action reference is corrupt.");
|
|
3910
|
+
if (record2.expiresAt <= now()) {
|
|
3911
|
+
await backend.delete(key);
|
|
3912
|
+
throw new TopoloSdkError2("action_secret_expired", "Protected action reference expired. Repeat the originating action if its contract permits.");
|
|
3913
|
+
}
|
|
3914
|
+
return record2.value;
|
|
3915
|
+
},
|
|
3916
|
+
async delete(reference) {
|
|
3917
|
+
await backend.delete(await account(reference));
|
|
3918
|
+
}
|
|
3919
|
+
};
|
|
3920
|
+
}
|
|
3921
|
+
var TOPOLO_ENVIRONMENT_SERVICE_URLS = {
|
|
3922
|
+
production: {
|
|
3923
|
+
auth: "https://auth.topolo.app",
|
|
3924
|
+
developers: "https://developers.topolo.app"
|
|
3925
|
+
},
|
|
3926
|
+
staging: {
|
|
3927
|
+
auth: "https://auth.stg.topolo.us",
|
|
3928
|
+
developers: "https://developers.stg.topolo.us"
|
|
3929
|
+
},
|
|
3930
|
+
development: {
|
|
3931
|
+
auth: "https://auth.topolo.dev",
|
|
3932
|
+
developers: "https://developers.topolo.dev"
|
|
3933
|
+
},
|
|
3934
|
+
local: {}
|
|
3935
|
+
};
|
|
3936
|
+
function hostnameHasSuffix2(hostname, suffix) {
|
|
3937
|
+
return hostname === suffix || hostname.endsWith(`.${suffix}`);
|
|
3938
|
+
}
|
|
3939
|
+
function workersDevAccountMatches2(hostname, account) {
|
|
3940
|
+
if (!hostname.endsWith(".workers.dev")) return false;
|
|
3941
|
+
const labels = hostname.slice(0, -".workers.dev".length).split(".");
|
|
3942
|
+
return labels.length >= 2 && labels.at(-1) === account;
|
|
3943
|
+
}
|
|
3944
|
+
function originMatchesEnvironment2(url, environment) {
|
|
3945
|
+
const hostname = url.hostname.toLowerCase();
|
|
3946
|
+
if (environment === "local") {
|
|
3947
|
+
return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
3948
|
+
}
|
|
3949
|
+
if (url.protocol !== "https:") return false;
|
|
3950
|
+
if (environment === "production") {
|
|
3951
|
+
return hostnameHasSuffix2(hostname, "topolo.app") || hostnameHasSuffix2(hostname, "topolo.io") || hostname === "api.ol0.me" || workersDevAccountMatches2(hostname, "topolo");
|
|
3952
|
+
}
|
|
3953
|
+
if (environment === "staging") {
|
|
3954
|
+
return hostnameHasSuffix2(hostname, "stg.topolo.us") || workersDevAccountMatches2(hostname, "topolo-staging");
|
|
3955
|
+
}
|
|
3956
|
+
return hostnameHasSuffix2(hostname, "topolo.dev") || workersDevAccountMatches2(hostname, "topolo-development");
|
|
3957
|
+
}
|
|
3958
|
+
function assertTopoloServiceUrlEnvironment2(service, value, environment) {
|
|
3959
|
+
let url;
|
|
3960
|
+
try {
|
|
3961
|
+
url = new URL(value);
|
|
3962
|
+
} catch {
|
|
3963
|
+
throw new TopoloSdkError2("invalid_service_url", `Invalid URL configured for Topolo service "${service}".`);
|
|
3964
|
+
}
|
|
3965
|
+
if (url.protocol !== "https:" && url.protocol !== "http:" || url.username || url.password || !originMatchesEnvironment2(url, environment)) {
|
|
3966
|
+
throw new TopoloSdkError2(
|
|
3967
|
+
"environment_mismatch",
|
|
3968
|
+
`Refusing Topolo service "${service}": ${url.origin} does not belong to ${environment}.`
|
|
3969
|
+
);
|
|
3970
|
+
}
|
|
3971
|
+
return url.origin;
|
|
3972
|
+
}
|
|
3973
|
+
function topoloServiceOrigins2(serviceUrls, environment) {
|
|
3974
|
+
return Object.fromEntries(
|
|
3975
|
+
Object.entries(serviceUrls ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([service, value]) => [
|
|
3976
|
+
service,
|
|
3977
|
+
assertTopoloServiceUrlEnvironment2(service, value, environment)
|
|
3978
|
+
])
|
|
3979
|
+
);
|
|
3980
|
+
}
|
|
3856
3981
|
var ENVIRONMENT_DEFAULT_SERVICE_URLS = TOPOLO_ENVIRONMENT_SERVICE_URLS;
|
|
3857
3982
|
function configPath() {
|
|
3858
3983
|
const override = process.env.TOPOLO_CONFIG_PATH;
|
|
3859
3984
|
if (override) return override;
|
|
3860
|
-
const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ?
|
|
3861
|
-
return
|
|
3985
|
+
const base = process.env.XDG_CONFIG_HOME || (platform() === "win32" ? join3(homedir2(), "AppData", "Roaming") : join3(homedir2(), ".config"));
|
|
3986
|
+
return join3(base, "topolo", "config.json");
|
|
3862
3987
|
}
|
|
3863
3988
|
async function readConfig() {
|
|
3864
3989
|
const path = configPath();
|
|
@@ -4152,6 +4277,28 @@ function publicMcpErrorMessage(error, fallback) {
|
|
|
4152
4277
|
return error instanceof TopoloMcpPublicError ? error.message : fallback;
|
|
4153
4278
|
}
|
|
4154
4279
|
|
|
4280
|
+
// src/action-secret-tools.ts
|
|
4281
|
+
var actionSecretTools = [{
|
|
4282
|
+
name: "topolo_release_action_secret",
|
|
4283
|
+
title: "Release a protected action reference",
|
|
4284
|
+
description: "Deletes only a local protected handoff reference for the verified identity and environment. Does not revoke the upstream credential. Requires confirm:true.",
|
|
4285
|
+
requiredScopes: [],
|
|
4286
|
+
destructive: true,
|
|
4287
|
+
inputSchema: {
|
|
4288
|
+
type: "object",
|
|
4289
|
+
properties: { reference: { type: "string", minLength: 1 }, confirm: { type: "boolean" } },
|
|
4290
|
+
required: ["reference", "confirm"],
|
|
4291
|
+
additionalProperties: false
|
|
4292
|
+
},
|
|
4293
|
+
handler: async (topolo, args) => {
|
|
4294
|
+
const reference = typeof args.reference === "string" ? args.reference.trim() : "";
|
|
4295
|
+
if (!reference) throw new TopoloMcpPublicError("reference is required.");
|
|
4296
|
+
if (args.confirm !== true) throw new TopoloMcpPublicError("Releasing a protected action reference requires confirm:true.");
|
|
4297
|
+
await topolo.client.releaseActionSecret(reference);
|
|
4298
|
+
return { released: true };
|
|
4299
|
+
}
|
|
4300
|
+
}];
|
|
4301
|
+
|
|
4155
4302
|
// src/tools.ts
|
|
4156
4303
|
var HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
|
|
4157
4304
|
var AGENT_INVOCATIONS_CREATE = "app_topolo_agent.agent_invocations.create";
|
|
@@ -4230,6 +4377,7 @@ function agentRunResult(invocation) {
|
|
|
4230
4377
|
};
|
|
4231
4378
|
}
|
|
4232
4379
|
var TOOLS = [
|
|
4380
|
+
...actionSecretTools,
|
|
4233
4381
|
{
|
|
4234
4382
|
name: "topolo_whoami",
|
|
4235
4383
|
title: "Describe the current Topolo credential",
|
|
@@ -4912,14 +5060,14 @@ function displayAppId(entry) {
|
|
|
4912
5060
|
|
|
4913
5061
|
// src/version.ts
|
|
4914
5062
|
import { readFileSync } from "fs";
|
|
4915
|
-
import { dirname as dirname3, join as
|
|
5063
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
4916
5064
|
import { fileURLToPath } from "url";
|
|
4917
5065
|
function readMcpVersion() {
|
|
4918
5066
|
try {
|
|
4919
5067
|
const here = dirname3(fileURLToPath(import.meta.url));
|
|
4920
5068
|
const candidates = [
|
|
4921
|
-
|
|
4922
|
-
|
|
5069
|
+
join4(here, "package.json"),
|
|
5070
|
+
join4(here, "..", "package.json")
|
|
4923
5071
|
];
|
|
4924
5072
|
for (const path of candidates) {
|
|
4925
5073
|
try {
|
|
@@ -5042,7 +5190,7 @@ function toolError(context, error) {
|
|
|
5042
5190
|
}
|
|
5043
5191
|
|
|
5044
5192
|
// src/context-credential.ts
|
|
5045
|
-
import { randomUUID } from "crypto";
|
|
5193
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
5046
5194
|
|
|
5047
5195
|
// src/auth.ts
|
|
5048
5196
|
function resolveCredentialFromEnv(environment = "production") {
|
|
@@ -5186,7 +5334,7 @@ async function resolveMcpCredential(environment, selector = process.env.TOPOLO_C
|
|
|
5186
5334
|
"TOPOLO_AGENT_INSTALLATION_ID cannot be combined with TOPOLO_CONTEXT or an environment credential"
|
|
5187
5335
|
);
|
|
5188
5336
|
}
|
|
5189
|
-
const taskId = taskSelector?.trim() || `task_${
|
|
5337
|
+
const taskId = taskSelector?.trim() || `task_${randomUUID2()}`;
|
|
5190
5338
|
const config2 = await dependencies.readConfig();
|
|
5191
5339
|
const serviceUrls = resolveServiceUrls(
|
|
5192
5340
|
profileForEnvironment(config2, environment).serviceUrls,
|
|
@@ -5285,6 +5433,7 @@ var DEFAULT_DEPENDENCIES2 = {
|
|
|
5285
5433
|
resolveAgentName,
|
|
5286
5434
|
resolveServiceUrls,
|
|
5287
5435
|
createTopolo,
|
|
5436
|
+
createProtectedActionSecretStore,
|
|
5288
5437
|
buildServer,
|
|
5289
5438
|
createTransport: createStdioTransport,
|
|
5290
5439
|
advertisedToolNames,
|
|
@@ -5307,6 +5456,7 @@ async function runStdioMcp(dependencies = DEFAULT_DEPENDENCIES2) {
|
|
|
5307
5456
|
const topolo = dependencies.createTopolo({
|
|
5308
5457
|
credential,
|
|
5309
5458
|
environment,
|
|
5459
|
+
actionSecretStore: dependencies.createProtectedActionSecretStore({ environment, identity: () => topolo.client.introspect() }),
|
|
5310
5460
|
actorContext: {
|
|
5311
5461
|
channel: "mcp",
|
|
5312
5462
|
...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.4",
|
|
5
|
+
"topoloPublishedVersion": "0.14.4",
|
|
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.4"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "^20.12.0",
|