@frontmcp/adapters 1.10.8 → 1.10.9-rc.2
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/esm/index.mjs +7 -2284
- package/esm/openapi/index.mjs +7 -2284
- package/esm/package.json +6 -6
- package/esm/skills/index.mjs +1 -3174
- package/index.js +7 -2314
- package/openapi/index.js +7 -2313
- package/package.json +6 -6
- package/skills/index.js +1 -3255
package/skills/index.js
CHANGED
|
@@ -1,3255 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __export = (target, all) => {
|
|
9
|
-
for (var name in all)
|
|
10
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
-
};
|
|
12
|
-
var __copyProps = (to, from, except, desc) => {
|
|
13
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
-
for (let key of __getOwnPropNames(from))
|
|
15
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
-
}
|
|
18
|
-
return to;
|
|
19
|
-
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
-
mod
|
|
27
|
-
));
|
|
28
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
-
|
|
30
|
-
// libs/adapters/src/skills/index.ts
|
|
31
|
-
var skills_exports = {};
|
|
32
|
-
__export(skills_exports, {
|
|
33
|
-
BundlePinnedError: () => BundlePinnedError,
|
|
34
|
-
BundlePushJwtVerifier: () => BundlePushJwtVerifier,
|
|
35
|
-
BundleStore: () => BundleStore,
|
|
36
|
-
ClassificationRegistry: () => ClassificationRegistry,
|
|
37
|
-
FilesystemSkillsSource: () => FilesystemSkillsSource,
|
|
38
|
-
Hs256AuditSigner: () => Hs256AuditSigner,
|
|
39
|
-
InlineSource: () => InlineSource,
|
|
40
|
-
MemoryAuditStore: () => MemoryAuditStore,
|
|
41
|
-
NpmSource: () => NpmSource,
|
|
42
|
-
OverlayParseError: () => OverlayParseError,
|
|
43
|
-
Rs256AuditSigner: () => Rs256AuditSigner,
|
|
44
|
-
SKILL_AUDIT_ERROR_MESSAGE_MAX: () => SKILL_AUDIT_ERROR_MESSAGE_MAX,
|
|
45
|
-
SKILL_AUDIT_GENESIS_PREV_HASH: () => SKILL_AUDIT_GENESIS_PREV_HASH,
|
|
46
|
-
SKILL_AUDIT_KEYS: () => SKILL_AUDIT_KEYS,
|
|
47
|
-
SKILL_AUDIT_QUEUE_MAX: () => SKILL_AUDIT_QUEUE_MAX,
|
|
48
|
-
SaasPullSource: () => SaasPullSource,
|
|
49
|
-
SkillAuditWriter: () => SkillAuditWriter,
|
|
50
|
-
SkillAuditWriterToken: () => SkillAuditWriterToken,
|
|
51
|
-
SkillDependencyCycleError: () => SkillDependencyCycleError,
|
|
52
|
-
SkillDependencyInvariantError: () => SkillDependencyInvariantError,
|
|
53
|
-
SkillDependencyMissingError: () => SkillDependencyMissingError,
|
|
54
|
-
StaticSource: () => StaticSource,
|
|
55
|
-
StorageAdapterAuditStore: () => StorageAdapterAuditStore,
|
|
56
|
-
WebhookReplayGuard: () => WebhookReplayGuard,
|
|
57
|
-
applyClassificationOverrides: () => applyClassificationOverrides,
|
|
58
|
-
applyEnvironmentOverlay: () => applyEnvironmentOverlay,
|
|
59
|
-
buildFrontMcpConfigFromManifest: () => buildFrontMcpConfigFromManifest,
|
|
60
|
-
buildKnownOps: () => buildKnownOps,
|
|
61
|
-
buildResourceChangeNotification: () => buildResourceChangeNotification,
|
|
62
|
-
bundleDigest: () => bundleDigest,
|
|
63
|
-
bundleSkillToActions: () => bundleSkillToActions,
|
|
64
|
-
bundleSourceSchema: () => bundleSourceSchema,
|
|
65
|
-
canonicalize: () => canonicalize,
|
|
66
|
-
canonicalizeRecordForSigning: () => canonicalizeRecordForSigning,
|
|
67
|
-
classifyOne: () => classifyOne,
|
|
68
|
-
classifyOperations: () => classifyOperations,
|
|
69
|
-
compileSkilledBundleFromOpenApi: () => compileSkilledBundleFromOpenApi,
|
|
70
|
-
createBundleSource: () => createBundleSource,
|
|
71
|
-
createSkillAuditMetrics: () => createSkillAuditMetrics,
|
|
72
|
-
crossValidate: () => crossValidate,
|
|
73
|
-
crossValidateManifest: () => crossValidateManifest,
|
|
74
|
-
dedupeOpReferences: () => dedupeOpReferences,
|
|
75
|
-
defaultAuditSignatureVerifier: () => defaultAuditSignatureVerifier,
|
|
76
|
-
deployManifestSchema: () => deployManifestSchema,
|
|
77
|
-
diffBundles: () => diffBundles,
|
|
78
|
-
extractOpReferences: () => extractOpReferences,
|
|
79
|
-
formatDiffSummary: () => formatDiffSummary,
|
|
80
|
-
hasSkillAuditFactory: () => import_sdk.hasSkillAuditFactory,
|
|
81
|
-
linkRecord: () => linkRecord,
|
|
82
|
-
loadFilesystemSkill: () => loadFilesystemSkill,
|
|
83
|
-
nextPrevHash: () => nextPrevHash,
|
|
84
|
-
npmSourceSchema: () => npmSourceSchema,
|
|
85
|
-
parseOverlay: () => parseOverlay,
|
|
86
|
-
parseSkillFrontmatter: () => parseSkillFrontmatter,
|
|
87
|
-
renderResourceUri: () => renderResourceUri,
|
|
88
|
-
resolveSkillLoadOrder: () => resolveSkillLoadOrder,
|
|
89
|
-
resolvedBundleSchema: () => resolvedBundleSchema,
|
|
90
|
-
saasSourceSchema: () => saasSourceSchema,
|
|
91
|
-
setSkillAuditFactory: () => import_sdk.setSkillAuditFactory,
|
|
92
|
-
signatureKeySchema: () => signatureKeySchema,
|
|
93
|
-
staticSourceSchema: () => staticSourceSchema,
|
|
94
|
-
validateOpReferences: () => validateOpReferences,
|
|
95
|
-
verifyBundleSignature: () => verifyBundleSignature,
|
|
96
|
-
verifyChain: () => verifyChain
|
|
97
|
-
});
|
|
98
|
-
module.exports = __toCommonJS(skills_exports);
|
|
99
|
-
|
|
100
|
-
// libs/adapters/src/skills/bundle/bundle.types.ts
|
|
101
|
-
function bundleSkillToActions(skill, ops) {
|
|
102
|
-
return skill.operationIds.map((opId) => {
|
|
103
|
-
const op = ops[opId];
|
|
104
|
-
if (!op) {
|
|
105
|
-
throw new Error(`bundleSkillToActions: skill "${skill.id}" references unknown operationId "${opId}"`);
|
|
106
|
-
}
|
|
107
|
-
const action = {
|
|
108
|
-
actionId: op.operationId,
|
|
109
|
-
summary: op.summary ?? `${op.httpMethod} ${op.pathTemplate}`,
|
|
110
|
-
...op.description !== void 0 && { description: op.description },
|
|
111
|
-
inputJsonSchema: op.inputSchema,
|
|
112
|
-
outputJsonSchema: op.outputSchema,
|
|
113
|
-
...op.requiredAuthorities !== void 0 && { requiredAuthorities: op.requiredAuthorities }
|
|
114
|
-
};
|
|
115
|
-
return action;
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// libs/adapters/src/skills/bundle/bundle.schema.ts
|
|
120
|
-
var import_lazy_zod = require("@frontmcp/lazy-zod");
|
|
121
|
-
var httpMethodSchema = import_lazy_zod.z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]);
|
|
122
|
-
var RFC7230_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
123
|
-
var pathTemplateRegex = /^\/[^\s?#]*$/;
|
|
124
|
-
var FORBIDDEN_PATH_FRAGMENTS = ["..", "\\", "`", "$(", "${"];
|
|
125
|
-
var pathTemplateSchema = import_lazy_zod.z.string().min(1).max(2048).refine((v) => pathTemplateRegex.test(v), {
|
|
126
|
-
message: "pathTemplate must start with `/` and contain no whitespace, `?`, or `#`"
|
|
127
|
-
}).refine((v) => !FORBIDDEN_PATH_FRAGMENTS.some((bad) => v.includes(bad)), {
|
|
128
|
-
message: "pathTemplate contains a forbidden character or fragment (`..`, backtick, `$(`, etc.)"
|
|
129
|
-
});
|
|
130
|
-
var jsonSchemaShape = import_lazy_zod.z.record(import_lazy_zod.z.string(), import_lazy_zod.z.unknown());
|
|
131
|
-
var authoritiesPolicySchema = import_lazy_zod.z.record(import_lazy_zod.z.string(), import_lazy_zod.z.unknown());
|
|
132
|
-
var authBindingSchema = import_lazy_zod.z.discriminatedUnion("kind", [
|
|
133
|
-
import_lazy_zod.z.object({ kind: import_lazy_zod.z.literal("none") }).strict(),
|
|
134
|
-
import_lazy_zod.z.object({
|
|
135
|
-
kind: import_lazy_zod.z.literal("bearer"),
|
|
136
|
-
vaultRef: import_lazy_zod.z.string().min(1).max(256),
|
|
137
|
-
passthroughCallerToken: import_lazy_zod.z.boolean().optional()
|
|
138
|
-
}).strict(),
|
|
139
|
-
import_lazy_zod.z.object({
|
|
140
|
-
kind: import_lazy_zod.z.literal("apiKey"),
|
|
141
|
-
in: import_lazy_zod.z.enum(["header", "query"]),
|
|
142
|
-
name: import_lazy_zod.z.string().min(1).max(128).regex(RFC7230_TOKEN, "apiKey name must match RFC 7230 token grammar"),
|
|
143
|
-
vaultRef: import_lazy_zod.z.string().min(1).max(256)
|
|
144
|
-
}).strict(),
|
|
145
|
-
import_lazy_zod.z.object({
|
|
146
|
-
kind: import_lazy_zod.z.literal("oauth2"),
|
|
147
|
-
flow: import_lazy_zod.z.literal("client_credentials"),
|
|
148
|
-
vaultRef: import_lazy_zod.z.string().min(1).max(256)
|
|
149
|
-
}).strict()
|
|
150
|
-
]);
|
|
151
|
-
var serviceDescriptorSchema = import_lazy_zod.z.object({
|
|
152
|
-
id: import_lazy_zod.z.string().min(1).max(128).regex(/^[a-zA-Z0-9_-]+$/, "service id must be alphanumeric / `-` / `_`"),
|
|
153
|
-
baseUrl: import_lazy_zod.z.string().url().max(2048),
|
|
154
|
-
description: import_lazy_zod.z.string().max(4096).optional()
|
|
155
|
-
}).strict();
|
|
156
|
-
var parameterMapperSchema = import_lazy_zod.z.object({
|
|
157
|
-
inputKey: import_lazy_zod.z.string().min(1).max(256),
|
|
158
|
-
type: import_lazy_zod.z.enum(["path", "query", "header", "cookie", "body"]),
|
|
159
|
-
key: import_lazy_zod.z.string().min(1).max(256),
|
|
160
|
-
required: import_lazy_zod.z.boolean().optional(),
|
|
161
|
-
style: import_lazy_zod.z.string().max(64).optional(),
|
|
162
|
-
explode: import_lazy_zod.z.boolean().optional(),
|
|
163
|
-
serialization: import_lazy_zod.z.record(import_lazy_zod.z.string(), import_lazy_zod.z.unknown()).optional(),
|
|
164
|
-
security: import_lazy_zod.z.record(import_lazy_zod.z.string(), import_lazy_zod.z.unknown()).optional()
|
|
165
|
-
}).passthrough().refine((mapper) => mapper.type !== "header" && mapper.type !== "cookie" || RFC7230_TOKEN.test(mapper.key), {
|
|
166
|
-
message: "a header or cookie mapper key must match RFC 7230 token grammar",
|
|
167
|
-
path: ["key"]
|
|
168
|
-
});
|
|
169
|
-
var operationDescriptorSchema = import_lazy_zod.z.object({
|
|
170
|
-
operationId: import_lazy_zod.z.string().min(1).max(256).regex(/^[a-zA-Z0-9_.\-:]+$/, "operationId must be alphanumeric / `-` / `_` / `.` / `:`"),
|
|
171
|
-
serviceId: import_lazy_zod.z.string().min(1).max(128),
|
|
172
|
-
httpMethod: httpMethodSchema,
|
|
173
|
-
pathTemplate: pathTemplateSchema,
|
|
174
|
-
inputSchema: jsonSchemaShape,
|
|
175
|
-
outputSchema: jsonSchemaShape,
|
|
176
|
-
mapper: import_lazy_zod.z.array(parameterMapperSchema).max(256),
|
|
177
|
-
authBindingRef: import_lazy_zod.z.string().min(1).max(128),
|
|
178
|
-
requiredAuthorities: authoritiesPolicySchema.optional(),
|
|
179
|
-
// Explicit opt-in that a policy-less op is intentionally public — consulted
|
|
180
|
-
// only when the deployment runs the executor with `unprotectedOps: 'deny'`.
|
|
181
|
-
public: import_lazy_zod.z.boolean().optional(),
|
|
182
|
-
maxResponseBytes: import_lazy_zod.z.number().int().positive().max(64 * 1024 * 1024).optional(),
|
|
183
|
-
timeoutMs: import_lazy_zod.z.number().int().positive().max(6e5).optional(),
|
|
184
|
-
summary: import_lazy_zod.z.string().max(512).optional(),
|
|
185
|
-
description: import_lazy_zod.z.string().max(8192).optional()
|
|
186
|
-
}).strict();
|
|
187
|
-
var bundledSkillSchema = import_lazy_zod.z.object({
|
|
188
|
-
id: import_lazy_zod.z.string().min(1).max(128).regex(/^[a-zA-Z0-9_.-]+$/),
|
|
189
|
-
name: import_lazy_zod.z.string().min(1).max(256),
|
|
190
|
-
description: import_lazy_zod.z.string().max(2048),
|
|
191
|
-
instructions: import_lazy_zod.z.string().max(256 * 1024),
|
|
192
|
-
tags: import_lazy_zod.z.array(import_lazy_zod.z.string().max(64)).max(64).optional(),
|
|
193
|
-
operationIds: import_lazy_zod.z.array(import_lazy_zod.z.string().min(1).max(256)).min(0).max(2e3),
|
|
194
|
-
requiredAuthorities: authoritiesPolicySchema.optional(),
|
|
195
|
-
/**
|
|
196
|
-
* Other skill ids this skill depends on. Hosts that consume the bundle
|
|
197
|
-
* register skills in dependency order; cycles and missing deps are caught
|
|
198
|
-
* at apply time. Field name aligned with the agentskills Skill Package
|
|
199
|
-
* Manifest proposal (https://github.com/agentskills/agentskills/discussions/210)
|
|
200
|
-
* so bundles round-trip across runtimes without a schema bump.
|
|
201
|
-
*/
|
|
202
|
-
requires: import_lazy_zod.z.array(import_lazy_zod.z.string().min(1).max(128)).max(64).optional()
|
|
203
|
-
}).strict();
|
|
204
|
-
var integritySchema = import_lazy_zod.z.object({
|
|
205
|
-
alg: import_lazy_zod.z.enum(["RS256", "EdDSA"]),
|
|
206
|
-
keyId: import_lazy_zod.z.string().min(1).max(256),
|
|
207
|
-
signature: import_lazy_zod.z.string().min(1).max(8192),
|
|
208
|
-
digest: import_lazy_zod.z.string().min(1).max(256)
|
|
209
|
-
}).strict();
|
|
210
|
-
var resolvedBundleSchema = import_lazy_zod.z.object({
|
|
211
|
-
schemaVersion: import_lazy_zod.z.literal(1),
|
|
212
|
-
bundleId: import_lazy_zod.z.string().min(1).max(256).regex(/^[a-zA-Z0-9_.\-:]+$/),
|
|
213
|
-
version: import_lazy_zod.z.string().min(1).max(256),
|
|
214
|
-
generatedAt: import_lazy_zod.z.string().min(1).refine((v) => !Number.isNaN(Date.parse(v)), { message: "generatedAt must be ISO 8601 parsable" }),
|
|
215
|
-
sourceDigest: import_lazy_zod.z.string().min(1).max(256).regex(/^[a-fA-F0-9]+$/, "sourceDigest must be a hex sha256"),
|
|
216
|
-
services: import_lazy_zod.z.array(serviceDescriptorSchema).min(1).max(64),
|
|
217
|
-
authBindings: import_lazy_zod.z.record(import_lazy_zod.z.string().min(1).max(128), authBindingSchema),
|
|
218
|
-
skills: import_lazy_zod.z.array(bundledSkillSchema).max(1e3),
|
|
219
|
-
operations: import_lazy_zod.z.record(import_lazy_zod.z.string().min(1).max(256), operationDescriptorSchema),
|
|
220
|
-
integrity: integritySchema.optional()
|
|
221
|
-
}).strict();
|
|
222
|
-
function crossValidate(bundle) {
|
|
223
|
-
const errors = [];
|
|
224
|
-
const serviceIds = new Set(bundle.services.map((s) => s.id));
|
|
225
|
-
const authRefs = new Set(Object.keys(bundle.authBindings));
|
|
226
|
-
const opIds = new Set(Object.keys(bundle.operations));
|
|
227
|
-
for (const [opId, op] of Object.entries(bundle.operations)) {
|
|
228
|
-
if (op.operationId !== opId) {
|
|
229
|
-
errors.push(`operation key "${opId}" does not match operationId "${op.operationId}"`);
|
|
230
|
-
}
|
|
231
|
-
if (!serviceIds.has(op.serviceId)) {
|
|
232
|
-
errors.push(`operation "${opId}" references unknown service "${op.serviceId}"`);
|
|
233
|
-
}
|
|
234
|
-
if (!authRefs.has(op.authBindingRef)) {
|
|
235
|
-
errors.push(`operation "${opId}" references unknown authBinding "${op.authBindingRef}"`);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
const seenSkillIds = /* @__PURE__ */ new Set();
|
|
239
|
-
for (const skill of bundle.skills) {
|
|
240
|
-
if (seenSkillIds.has(skill.id)) {
|
|
241
|
-
errors.push(`duplicate skill id "${skill.id}"`);
|
|
242
|
-
}
|
|
243
|
-
seenSkillIds.add(skill.id);
|
|
244
|
-
for (const opId of skill.operationIds) {
|
|
245
|
-
if (!opIds.has(opId)) {
|
|
246
|
-
errors.push(`skill "${skill.id}" references unknown operationId "${opId}"`);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return errors.length === 0 ? { ok: true } : { ok: false, errors };
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// libs/adapters/src/skills/bundle/overlay-parser.ts
|
|
254
|
-
var yaml = __toESM(require("js-yaml"));
|
|
255
|
-
var OVERLAY_BUNDLE_KEY = "x-frontmcp-bundle";
|
|
256
|
-
var OverlayParseError = class extends Error {
|
|
257
|
-
constructor(message, errors) {
|
|
258
|
-
super(message);
|
|
259
|
-
this.errors = errors;
|
|
260
|
-
this.name = "OverlayParseError";
|
|
261
|
-
}
|
|
262
|
-
errors;
|
|
263
|
-
};
|
|
264
|
-
function parseRaw(input) {
|
|
265
|
-
switch (input.kind) {
|
|
266
|
-
case "yaml":
|
|
267
|
-
try {
|
|
268
|
-
return yaml.load(input.content);
|
|
269
|
-
} catch (e) {
|
|
270
|
-
throw new OverlayParseError(`Overlay YAML parse failed: ${e.message}`);
|
|
271
|
-
}
|
|
272
|
-
case "json":
|
|
273
|
-
try {
|
|
274
|
-
return JSON.parse(input.content);
|
|
275
|
-
} catch (e) {
|
|
276
|
-
throw new OverlayParseError(`Overlay JSON parse failed: ${e.message}`);
|
|
277
|
-
}
|
|
278
|
-
case "object":
|
|
279
|
-
return input.content;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
function extractBundle(overlay) {
|
|
283
|
-
if (overlay === null || typeof overlay !== "object") {
|
|
284
|
-
throw new OverlayParseError("Overlay root must be an object");
|
|
285
|
-
}
|
|
286
|
-
const root = overlay;
|
|
287
|
-
if (OVERLAY_BUNDLE_KEY in root) {
|
|
288
|
-
return root[OVERLAY_BUNDLE_KEY];
|
|
289
|
-
}
|
|
290
|
-
const info = root["info"];
|
|
291
|
-
if (info && typeof info === "object" && OVERLAY_BUNDLE_KEY in info) {
|
|
292
|
-
return info[OVERLAY_BUNDLE_KEY];
|
|
293
|
-
}
|
|
294
|
-
if ("schemaVersion" in root && "bundleId" in root) {
|
|
295
|
-
return root;
|
|
296
|
-
}
|
|
297
|
-
throw new OverlayParseError(
|
|
298
|
-
"Overlay does not contain `info.x-frontmcp-bundle` or a bare bundle object with schemaVersion + bundleId"
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
function parseOverlay(input) {
|
|
302
|
-
const raw = parseRaw(input);
|
|
303
|
-
const bundleCandidate = extractBundle(raw);
|
|
304
|
-
const result = resolvedBundleSchema.safeParse(bundleCandidate);
|
|
305
|
-
if (!result.success) {
|
|
306
|
-
const errors = result.error.issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`);
|
|
307
|
-
throw new OverlayParseError(`Bundle schema validation failed (${errors.length} issue(s))`, errors);
|
|
308
|
-
}
|
|
309
|
-
const cross = crossValidate(result.data);
|
|
310
|
-
if (!cross.ok) {
|
|
311
|
-
throw new OverlayParseError(
|
|
312
|
-
`Bundle cross-reference validation failed (${cross.errors.length} issue(s))`,
|
|
313
|
-
cross.errors
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
return result.data;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// libs/adapters/src/skills/bundle/bundle-diff.ts
|
|
320
|
-
var opEqual = (a, b) => a.operationId === b.operationId && a.serviceId === b.serviceId && a.httpMethod === b.httpMethod && a.pathTemplate === b.pathTemplate && a.authBindingRef === b.authBindingRef && JSON.stringify(a.mapper ?? null) === JSON.stringify(b.mapper ?? null) && JSON.stringify(a.inputSchema) === JSON.stringify(b.inputSchema) && JSON.stringify(a.outputSchema) === JSON.stringify(b.outputSchema) && JSON.stringify(a.requiredAuthorities ?? null) === JSON.stringify(b.requiredAuthorities ?? null) && (a.public ?? false) === (b.public ?? false) && a.maxResponseBytes === b.maxResponseBytes && a.timeoutMs === b.timeoutMs;
|
|
321
|
-
var skillEqual = (a, b) => a.id === b.id && a.name === b.name && a.description === b.description && a.instructions === b.instructions && JSON.stringify(a.tags ?? []) === JSON.stringify(b.tags ?? []) && JSON.stringify(a.operationIds) === JSON.stringify(b.operationIds) && JSON.stringify(a.requiredAuthorities ?? null) === JSON.stringify(b.requiredAuthorities ?? null);
|
|
322
|
-
function diffBundles(oldBundle, newBundle) {
|
|
323
|
-
const oldSkills = new Map((oldBundle?.skills ?? []).map((s) => [s.id, s]));
|
|
324
|
-
const newSkills = new Map(newBundle.skills.map((s) => [s.id, s]));
|
|
325
|
-
const addedSkillIds = [];
|
|
326
|
-
const removedSkillIds = [];
|
|
327
|
-
const changedSkillIds = [];
|
|
328
|
-
for (const [id, skill] of newSkills) {
|
|
329
|
-
const prev = oldSkills.get(id);
|
|
330
|
-
if (!prev) addedSkillIds.push(id);
|
|
331
|
-
else if (!skillEqual(prev, skill)) changedSkillIds.push(id);
|
|
332
|
-
}
|
|
333
|
-
for (const id of oldSkills.keys()) {
|
|
334
|
-
if (!newSkills.has(id)) removedSkillIds.push(id);
|
|
335
|
-
}
|
|
336
|
-
const oldOps = oldBundle?.operations ?? {};
|
|
337
|
-
const newOps = newBundle.operations;
|
|
338
|
-
const addedOperationIds = [];
|
|
339
|
-
const removedOperationIds = [];
|
|
340
|
-
const changedOperationIds = [];
|
|
341
|
-
for (const [id, op] of Object.entries(newOps)) {
|
|
342
|
-
const prev = oldOps[id];
|
|
343
|
-
if (!prev) addedOperationIds.push(id);
|
|
344
|
-
else if (!opEqual(prev, op)) changedOperationIds.push(id);
|
|
345
|
-
}
|
|
346
|
-
for (const id of Object.keys(oldOps)) {
|
|
347
|
-
if (!(id in newOps)) removedOperationIds.push(id);
|
|
348
|
-
}
|
|
349
|
-
const isNoOp = !!oldBundle && oldBundle.version === newBundle.version && addedSkillIds.length === 0 && removedSkillIds.length === 0 && changedSkillIds.length === 0 && addedOperationIds.length === 0 && removedOperationIds.length === 0 && changedOperationIds.length === 0;
|
|
350
|
-
return {
|
|
351
|
-
addedSkillIds,
|
|
352
|
-
removedSkillIds,
|
|
353
|
-
changedSkillIds,
|
|
354
|
-
addedOperationIds,
|
|
355
|
-
removedOperationIds,
|
|
356
|
-
changedOperationIds,
|
|
357
|
-
isNoOp
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
function formatDiffSummary(diff) {
|
|
361
|
-
if (diff.isNoOp) return "no-op";
|
|
362
|
-
return [
|
|
363
|
-
`+${diff.addedSkillIds.length}/-${diff.removedSkillIds.length}/~${diff.changedSkillIds.length} skills`,
|
|
364
|
-
`+${diff.addedOperationIds.length}/-${diff.removedOperationIds.length}/~${diff.changedOperationIds.length} ops`
|
|
365
|
-
].join(", ");
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// libs/adapters/src/skills/bundle/bundle.store.ts
|
|
369
|
-
var KNOWN_BUNDLE_SOURCES = ["static", "npm", "saas-pull", "webhook", "filesystem", "unknown"];
|
|
370
|
-
function normalizeBundleSource(value) {
|
|
371
|
-
if (typeof value !== "string") return "unknown";
|
|
372
|
-
return KNOWN_BUNDLE_SOURCES.includes(value) ? value : "unknown";
|
|
373
|
-
}
|
|
374
|
-
function classifySwapError(err) {
|
|
375
|
-
if (err instanceof BundlePinnedError) return "pinned";
|
|
376
|
-
if (err instanceof Error) {
|
|
377
|
-
const name = err.name;
|
|
378
|
-
const msg = err.message.toLowerCase();
|
|
379
|
-
if (name === "BundlePinnedError") return "pinned";
|
|
380
|
-
if (msg.includes("bundle is required") || msg.includes("malformed")) return "invalid_bundle";
|
|
381
|
-
if (msg.includes("listener")) return "listener_error";
|
|
382
|
-
}
|
|
383
|
-
return "unknown";
|
|
384
|
-
}
|
|
385
|
-
var BundleStore = class {
|
|
386
|
-
active;
|
|
387
|
-
listeners = /* @__PURE__ */ new Set();
|
|
388
|
-
history = [];
|
|
389
|
-
pinnedVersion;
|
|
390
|
-
historySize;
|
|
391
|
-
telemetry;
|
|
392
|
-
pullsCounter;
|
|
393
|
-
constructor(options = {}) {
|
|
394
|
-
const requested = options.historySize ?? 3;
|
|
395
|
-
if (!Number.isInteger(requested) || requested < 1) {
|
|
396
|
-
throw new Error("BundleStore: historySize must be a positive integer");
|
|
397
|
-
}
|
|
398
|
-
this.historySize = requested;
|
|
399
|
-
this.telemetry = options.telemetry;
|
|
400
|
-
this.pullsCounter = this.telemetry?.createCounter(
|
|
401
|
-
"frontmcp_skills_bundle_pulls_total",
|
|
402
|
-
"Number of bundle pulls (swaps) attempted, partitioned by status / source."
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
current() {
|
|
406
|
-
return this.active;
|
|
407
|
-
}
|
|
408
|
-
/**
|
|
409
|
-
* Swap to a new bundle. Returns the structural diff (no-op = same version
|
|
410
|
-
* with no field changes; the swap itself still fires listeners with isNoOp=true
|
|
411
|
-
* so observers can record the heartbeat).
|
|
412
|
-
*
|
|
413
|
-
* Throws when the store is pinned. The caller should check `isPinned()` first
|
|
414
|
-
* if it wants to skip silently rather than throw.
|
|
415
|
-
*
|
|
416
|
-
* @param next - new bundle to activate
|
|
417
|
-
* @param source - optional source label (e.g. `npm`, `saas-pull`, `webhook`)
|
|
418
|
-
* used as a counter / span attribute. When omitted, `unknown`
|
|
419
|
-
* is reported.
|
|
420
|
-
*/
|
|
421
|
-
swap(next, source) {
|
|
422
|
-
if (!next) {
|
|
423
|
-
throw new Error("BundleStore.swap: bundle is required");
|
|
424
|
-
}
|
|
425
|
-
const sourceLabel = normalizeBundleSource(source);
|
|
426
|
-
if (this.telemetry) {
|
|
427
|
-
const span = this.telemetry.startSpan("skill.bundle.swap", {
|
|
428
|
-
source: sourceLabel,
|
|
429
|
-
bundle_id: next.bundleId,
|
|
430
|
-
version: next.version,
|
|
431
|
-
skill_count: next.skills.length,
|
|
432
|
-
...this.active?.version ? { from_version: this.active.version } : {}
|
|
433
|
-
});
|
|
434
|
-
try {
|
|
435
|
-
if (this.pinnedVersion !== void 0) {
|
|
436
|
-
throw new BundlePinnedError(this.pinnedVersion);
|
|
437
|
-
}
|
|
438
|
-
const diff = this.commit(next, "normal");
|
|
439
|
-
span.setAttributes({ status: "ok", is_no_op: diff.isNoOp });
|
|
440
|
-
span.end();
|
|
441
|
-
this.pullsCounter?.inc(1, { status: "ok", source: sourceLabel });
|
|
442
|
-
return diff;
|
|
443
|
-
} catch (err) {
|
|
444
|
-
const reason = classifySwapError(err);
|
|
445
|
-
if (span.endWithError) {
|
|
446
|
-
span.endWithError(err instanceof Error ? err : String(err));
|
|
447
|
-
} else {
|
|
448
|
-
span.recordError?.(err instanceof Error ? err : new Error(String(err)));
|
|
449
|
-
span.end();
|
|
450
|
-
}
|
|
451
|
-
this.pullsCounter?.inc(1, { status: "error", source: sourceLabel, reason });
|
|
452
|
-
throw err;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
if (this.pinnedVersion !== void 0) {
|
|
456
|
-
throw new BundlePinnedError(this.pinnedVersion);
|
|
457
|
-
}
|
|
458
|
-
return this.commit(next, "normal");
|
|
459
|
-
}
|
|
460
|
-
/**
|
|
461
|
-
* Pin the active bundle to a specific historical version. Pinned stores
|
|
462
|
-
* reject `swap()` so source-driven updates accumulate elsewhere without
|
|
463
|
-
* activating. Returns the diff of the swap that activated the pinned
|
|
464
|
-
* version, or undefined if the pinned version was already active.
|
|
465
|
-
*
|
|
466
|
-
* Throws when `version` is unknown to the store's history.
|
|
467
|
-
*/
|
|
468
|
-
pin(version) {
|
|
469
|
-
if (this.active && this.active.version === version) {
|
|
470
|
-
this.pinnedVersion = version;
|
|
471
|
-
return void 0;
|
|
472
|
-
}
|
|
473
|
-
const entry = this.history.find((h) => h.bundle.version === version);
|
|
474
|
-
if (!entry) {
|
|
475
|
-
throw new Error(`BundleStore.pin: version "${version}" not in history`);
|
|
476
|
-
}
|
|
477
|
-
const diff = this.commit(entry.bundle, "pin");
|
|
478
|
-
this.pinnedVersion = version;
|
|
479
|
-
return diff;
|
|
480
|
-
}
|
|
481
|
-
/**
|
|
482
|
-
* Re-activate the most recent bundle in history (the bundle that was
|
|
483
|
-
* active before the current one). Useful after a bad swap is detected.
|
|
484
|
-
* Returns the diff. Throws when no prior bundle is available.
|
|
485
|
-
*
|
|
486
|
-
* Rollback does NOT pin — sources can resume auto-swapping after rollback.
|
|
487
|
-
*/
|
|
488
|
-
rollback() {
|
|
489
|
-
const prior = this.history[this.history.length - 1];
|
|
490
|
-
if (!prior) {
|
|
491
|
-
throw new Error("BundleStore.rollback: no prior bundle in history");
|
|
492
|
-
}
|
|
493
|
-
this.pinnedVersion = void 0;
|
|
494
|
-
return this.commit(prior.bundle, "rollback");
|
|
495
|
-
}
|
|
496
|
-
/** Lift the pin so `swap()` accepts new bundles again. */
|
|
497
|
-
unpin() {
|
|
498
|
-
this.pinnedVersion = void 0;
|
|
499
|
-
}
|
|
500
|
-
/** True when the store is currently pinned. */
|
|
501
|
-
isPinned() {
|
|
502
|
-
return this.pinnedVersion !== void 0;
|
|
503
|
-
}
|
|
504
|
-
/** Pinned version string, or undefined when unpinned. */
|
|
505
|
-
pinned() {
|
|
506
|
-
return this.pinnedVersion;
|
|
507
|
-
}
|
|
508
|
-
/**
|
|
509
|
-
* Snapshot of the history ring (oldest first; does not include the active
|
|
510
|
-
* bundle). Returned array is a shallow copy — safe to inspect.
|
|
511
|
-
*/
|
|
512
|
-
historySnapshot() {
|
|
513
|
-
return this.history.map((h) => ({
|
|
514
|
-
version: h.bundle.version,
|
|
515
|
-
bundleId: h.bundle.bundleId,
|
|
516
|
-
validatedAt: h.validatedAt
|
|
517
|
-
}));
|
|
518
|
-
}
|
|
519
|
-
/** Subscribe to swap events. Returns an unsubscribe function. */
|
|
520
|
-
subscribe(fn) {
|
|
521
|
-
this.listeners.add(fn);
|
|
522
|
-
return () => {
|
|
523
|
-
this.listeners.delete(fn);
|
|
524
|
-
};
|
|
525
|
-
}
|
|
526
|
-
/** Reset to empty state. Used by tests. Does NOT fire listeners. */
|
|
527
|
-
reset() {
|
|
528
|
-
this.active = void 0;
|
|
529
|
-
this.history = [];
|
|
530
|
-
this.pinnedVersion = void 0;
|
|
531
|
-
}
|
|
532
|
-
/** Commit a new active bundle (shared by swap / pin / rollback). */
|
|
533
|
-
commit(next, reason) {
|
|
534
|
-
const previous = this.active;
|
|
535
|
-
const diff = diffBundles(previous, next);
|
|
536
|
-
if (previous) {
|
|
537
|
-
const lastInHistory = this.history[this.history.length - 1];
|
|
538
|
-
if (!lastInHistory || lastInHistory.bundle.version !== previous.version) {
|
|
539
|
-
this.history.push({ bundle: previous, validatedAt: Date.now() });
|
|
540
|
-
if (this.history.length > this.historySize) {
|
|
541
|
-
this.history.splice(0, this.history.length - this.historySize);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
this.active = next;
|
|
546
|
-
for (const fn of this.listeners) {
|
|
547
|
-
try {
|
|
548
|
-
fn({ previous, current: next, diff, reason });
|
|
549
|
-
} catch {
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
return diff;
|
|
553
|
-
}
|
|
554
|
-
};
|
|
555
|
-
var BundlePinnedError = class extends Error {
|
|
556
|
-
constructor(pinnedVersion) {
|
|
557
|
-
super(`BundleStore is pinned to version "${pinnedVersion}"; new swaps are rejected until unpin().`);
|
|
558
|
-
this.pinnedVersion = pinnedVersion;
|
|
559
|
-
this.name = "BundlePinnedError";
|
|
560
|
-
}
|
|
561
|
-
pinnedVersion;
|
|
562
|
-
};
|
|
563
|
-
|
|
564
|
-
// libs/adapters/src/skills/dependency/skill-dag.ts
|
|
565
|
-
var SkillDependencyCycleError = class extends Error {
|
|
566
|
-
constructor(cycle) {
|
|
567
|
-
super(`skill dependency cycle: ${cycle.join(" -> ")}`);
|
|
568
|
-
this.cycle = cycle;
|
|
569
|
-
this.name = "SkillDependencyCycleError";
|
|
570
|
-
}
|
|
571
|
-
cycle;
|
|
572
|
-
};
|
|
573
|
-
var SkillDependencyMissingError = class extends Error {
|
|
574
|
-
constructor(skillId, missingId) {
|
|
575
|
-
super(`skill "${skillId}" requires unknown skill "${missingId}"`);
|
|
576
|
-
this.skillId = skillId;
|
|
577
|
-
this.missingId = missingId;
|
|
578
|
-
this.name = "SkillDependencyMissingError";
|
|
579
|
-
}
|
|
580
|
-
skillId;
|
|
581
|
-
missingId;
|
|
582
|
-
};
|
|
583
|
-
var SkillDependencyInvariantError = class extends Error {
|
|
584
|
-
constructor(message) {
|
|
585
|
-
super(message);
|
|
586
|
-
this.name = "SkillDependencyInvariantError";
|
|
587
|
-
}
|
|
588
|
-
};
|
|
589
|
-
function resolveSkillLoadOrder(skills) {
|
|
590
|
-
const byId = /* @__PURE__ */ new Map();
|
|
591
|
-
for (const skill of skills) {
|
|
592
|
-
if (byId.has(skill.id)) {
|
|
593
|
-
byId.set(skill.id, skill);
|
|
594
|
-
continue;
|
|
595
|
-
}
|
|
596
|
-
byId.set(skill.id, skill);
|
|
597
|
-
}
|
|
598
|
-
const inDegree = /* @__PURE__ */ new Map();
|
|
599
|
-
const requiredBy = /* @__PURE__ */ new Map();
|
|
600
|
-
for (const id of byId.keys()) {
|
|
601
|
-
inDegree.set(id, 0);
|
|
602
|
-
requiredBy.set(id, []);
|
|
603
|
-
}
|
|
604
|
-
for (const skill of byId.values()) {
|
|
605
|
-
const deps = skill.requires ?? [];
|
|
606
|
-
for (const depId of deps) {
|
|
607
|
-
if (!byId.has(depId)) {
|
|
608
|
-
throw new SkillDependencyMissingError(skill.id, depId);
|
|
609
|
-
}
|
|
610
|
-
inDegree.set(skill.id, (inDegree.get(skill.id) ?? 0) + 1);
|
|
611
|
-
const reverse = requiredBy.get(depId) ?? [];
|
|
612
|
-
reverse.push(skill.id);
|
|
613
|
-
requiredBy.set(depId, reverse);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
const ready = [...byId.keys()].filter((id) => inDegree.get(id) === 0).sort();
|
|
617
|
-
const order = [];
|
|
618
|
-
while (ready.length > 0) {
|
|
619
|
-
const next = ready.shift();
|
|
620
|
-
if (next === void 0) {
|
|
621
|
-
throw new SkillDependencyInvariantError(
|
|
622
|
-
"resolveSkillLoadOrder: invariant broken \u2014 ready queue drained mid-iteration"
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
|
-
const nextSkill = byId.get(next);
|
|
626
|
-
if (!nextSkill) {
|
|
627
|
-
throw new SkillDependencyInvariantError(
|
|
628
|
-
`resolveSkillLoadOrder: invariant broken \u2014 id "${next}" missing from byId map`
|
|
629
|
-
);
|
|
630
|
-
}
|
|
631
|
-
order.push(nextSkill);
|
|
632
|
-
for (const dependent of requiredBy.get(next) ?? []) {
|
|
633
|
-
const remaining = (inDegree.get(dependent) ?? 0) - 1;
|
|
634
|
-
inDegree.set(dependent, remaining);
|
|
635
|
-
if (remaining === 0) {
|
|
636
|
-
let i = 0;
|
|
637
|
-
while (i < ready.length && ready[i] < dependent) i++;
|
|
638
|
-
ready.splice(i, 0, dependent);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
if (order.length === byId.size) {
|
|
643
|
-
return order;
|
|
644
|
-
}
|
|
645
|
-
const remainingIds = [...byId.keys()].filter((id) => (inDegree.get(id) ?? 0) > 0);
|
|
646
|
-
const cycle = findCycle(remainingIds, byId);
|
|
647
|
-
throw new SkillDependencyCycleError(cycle);
|
|
648
|
-
}
|
|
649
|
-
function findCycle(remaining, byId) {
|
|
650
|
-
const adj = /* @__PURE__ */ new Map();
|
|
651
|
-
for (const id of remaining) {
|
|
652
|
-
adj.set(
|
|
653
|
-
id,
|
|
654
|
-
(byId.get(id)?.requires ?? []).filter((d) => remaining.includes(d))
|
|
655
|
-
);
|
|
656
|
-
}
|
|
657
|
-
const WHITE = 0;
|
|
658
|
-
const GRAY = 1;
|
|
659
|
-
const BLACK = 2;
|
|
660
|
-
const color = /* @__PURE__ */ new Map();
|
|
661
|
-
for (const id of remaining) color.set(id, WHITE);
|
|
662
|
-
const stack = [];
|
|
663
|
-
function dfs(node) {
|
|
664
|
-
color.set(node, GRAY);
|
|
665
|
-
stack.push(node);
|
|
666
|
-
for (const next of adj.get(node) ?? []) {
|
|
667
|
-
const c = color.get(next);
|
|
668
|
-
if (c === GRAY) {
|
|
669
|
-
const startIdx = stack.indexOf(next);
|
|
670
|
-
return [...stack.slice(startIdx), next];
|
|
671
|
-
}
|
|
672
|
-
if (c === WHITE) {
|
|
673
|
-
const found = dfs(next);
|
|
674
|
-
if (found) return found;
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
color.set(node, BLACK);
|
|
678
|
-
stack.pop();
|
|
679
|
-
return void 0;
|
|
680
|
-
}
|
|
681
|
-
for (const id of remaining) {
|
|
682
|
-
if (color.get(id) === WHITE) {
|
|
683
|
-
const cycle = dfs(id);
|
|
684
|
-
if (cycle) return cycle;
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
return remaining;
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
// libs/adapters/src/skills/source-options.ts
|
|
691
|
-
var import_lazy_zod2 = require("@frontmcp/lazy-zod");
|
|
692
|
-
var httpsUrl = import_lazy_zod2.z.string().url().refine(
|
|
693
|
-
(v) => {
|
|
694
|
-
try {
|
|
695
|
-
return new URL(v).protocol === "https:";
|
|
696
|
-
} catch {
|
|
697
|
-
return false;
|
|
698
|
-
}
|
|
699
|
-
},
|
|
700
|
-
{ message: "must use https://" }
|
|
701
|
-
);
|
|
702
|
-
var staticSourceSchema = import_lazy_zod2.z.object({
|
|
703
|
-
type: import_lazy_zod2.z.literal("static"),
|
|
704
|
-
/** Path to the bundle directory (containing spec.yaml + overlay.yaml) or a single overlay file. */
|
|
705
|
-
path: import_lazy_zod2.z.string().min(1),
|
|
706
|
-
/** Watch the path for changes (fs.watch). Default false. */
|
|
707
|
-
watch: import_lazy_zod2.z.boolean().default(false)
|
|
708
|
-
});
|
|
709
|
-
var npmSourceSchema = import_lazy_zod2.z.object({
|
|
710
|
-
type: import_lazy_zod2.z.literal("npm"),
|
|
711
|
-
/** npm package specifier, e.g. `@acme/frontmcp-billing-bundle`. */
|
|
712
|
-
packageName: import_lazy_zod2.z.string().min(1),
|
|
713
|
-
/** Optional named export to read; default is the package's default export. */
|
|
714
|
-
exportName: import_lazy_zod2.z.string().optional(),
|
|
715
|
-
/**
|
|
716
|
-
* Verify GitHub artifact attestation / npm provenance for the package before loading.
|
|
717
|
-
* Defaults to true. Provenance verification is not implemented yet, so `true` refuses
|
|
718
|
-
* to load the package; set `false` to load it without a provenance check.
|
|
719
|
-
*
|
|
720
|
-
* With `false`, the package is imported (its code runs) before the bundle is parsed, so
|
|
721
|
-
* bundle signing (`requireSignature` + `trustedKeys`) cannot protect the import itself.
|
|
722
|
-
* Only use it for a package you trust: pin its exact version in `package.json` and
|
|
723
|
-
* install from a lockfile with integrity hashes.
|
|
724
|
-
*/
|
|
725
|
-
verifyProvenance: import_lazy_zod2.z.boolean().default(true)
|
|
726
|
-
});
|
|
727
|
-
var saasSourceSchema = import_lazy_zod2.z.object({
|
|
728
|
-
type: import_lazy_zod2.z.literal("saas"),
|
|
729
|
-
/** SaaS pull endpoint, e.g. `https://cloud.frontmcp.dev/v1/bundles/<bundleId>`. */
|
|
730
|
-
endpoint: httpsUrl,
|
|
731
|
-
/**
|
|
732
|
-
* Pinned JWT issued by the SaaS for the customer's FrontMCP server. Verified
|
|
733
|
-
* before every pull: signed by a key in `jwksUrl`, `iss` = `expectedIssuer`,
|
|
734
|
-
* `aud` includes `expectedAudience`, not expired. A rejected token stops the pull.
|
|
735
|
-
*/
|
|
736
|
-
authToken: import_lazy_zod2.z.string().min(1),
|
|
737
|
-
/**
|
|
738
|
-
* RFC 8707 resource indicator the JWT must encode: its `aud` claim must include
|
|
739
|
-
* this value (and its `resource` claim too, when it has one).
|
|
740
|
-
*/
|
|
741
|
-
expectedAudience: import_lazy_zod2.z.string().min(1),
|
|
742
|
-
/** Polling interval in ms (boot pull is always immediate). Default 300000 (5 min). */
|
|
743
|
-
pollIntervalMs: import_lazy_zod2.z.number().int().positive().default(3e5),
|
|
744
|
-
/**
|
|
745
|
-
* Mount a webhook handler at `POST /__skilled_openapi/push` so the SaaS can push
|
|
746
|
-
* updates synchronously. Default false in v1.2 (interval polling only).
|
|
747
|
-
*/
|
|
748
|
-
enableWebhook: import_lazy_zod2.z.boolean().default(false),
|
|
749
|
-
/** JWKS URL for verifying SaaS-issued tokens (fetched without credentials, redirects refused). */
|
|
750
|
-
jwksUrl: httpsUrl,
|
|
751
|
-
/** Expected issuer (`iss`) claim of `authToken`. */
|
|
752
|
-
expectedIssuer: import_lazy_zod2.z.string().min(1)
|
|
753
|
-
});
|
|
754
|
-
var inlineSourceSchema = import_lazy_zod2.z.object({
|
|
755
|
-
type: import_lazy_zod2.z.literal("inline"),
|
|
756
|
-
/**
|
|
757
|
-
* The skilled-OpenAPI bundle object, embedded directly (no filesystem, no
|
|
758
|
-
* network). The right source for V8-isolate runtimes (Cloudflare Workers),
|
|
759
|
-
* where `static` (fs) and `npm` can't run and `saas` needs an endpoint. The
|
|
760
|
-
* object is validated by the overlay parser, same as file/remote bundles.
|
|
761
|
-
*/
|
|
762
|
-
content: import_lazy_zod2.z.unknown()
|
|
763
|
-
});
|
|
764
|
-
var bundleSourceSchema = import_lazy_zod2.z.discriminatedUnion("type", [
|
|
765
|
-
staticSourceSchema,
|
|
766
|
-
npmSourceSchema,
|
|
767
|
-
saasSourceSchema,
|
|
768
|
-
inlineSourceSchema
|
|
769
|
-
]);
|
|
770
|
-
var signatureKeySchema = import_lazy_zod2.z.object({
|
|
771
|
-
/** Stable key id (matches `kid` claim in the bundle JWT). */
|
|
772
|
-
keyId: import_lazy_zod2.z.string().min(1),
|
|
773
|
-
/** Algorithm for the signing key. */
|
|
774
|
-
alg: import_lazy_zod2.z.enum(["RS256", "EdDSA"]),
|
|
775
|
-
/** PEM-encoded public key (RSA SPKI or Ed25519). */
|
|
776
|
-
publicKeyPem: import_lazy_zod2.z.string().min(1)
|
|
777
|
-
});
|
|
778
|
-
|
|
779
|
-
// libs/adapters/src/skills/sources/inline.source.ts
|
|
780
|
-
var InlineSource = class {
|
|
781
|
-
constructor(options, logger) {
|
|
782
|
-
this.options = options;
|
|
783
|
-
this.logger = logger;
|
|
784
|
-
}
|
|
785
|
-
options;
|
|
786
|
-
logger;
|
|
787
|
-
id = "inline:bundle";
|
|
788
|
-
listeners = /* @__PURE__ */ new Set();
|
|
789
|
-
async start() {
|
|
790
|
-
const bundle = this.parse();
|
|
791
|
-
for (const fn of this.listeners) {
|
|
792
|
-
try {
|
|
793
|
-
fn(bundle);
|
|
794
|
-
} catch (e) {
|
|
795
|
-
this.logger.warn(`[inline-source] listener threw: ${e.message}`);
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
onChange(listener) {
|
|
800
|
-
this.listeners.add(listener);
|
|
801
|
-
return () => {
|
|
802
|
-
this.listeners.delete(listener);
|
|
803
|
-
};
|
|
804
|
-
}
|
|
805
|
-
async stop() {
|
|
806
|
-
this.listeners.clear();
|
|
807
|
-
}
|
|
808
|
-
parse() {
|
|
809
|
-
return parseOverlay({ kind: "object", content: this.options.content });
|
|
810
|
-
}
|
|
811
|
-
};
|
|
812
|
-
|
|
813
|
-
// libs/adapters/src/skills/sources/npm.source.ts
|
|
814
|
-
var npmDynamicImport;
|
|
815
|
-
var NpmSource = class {
|
|
816
|
-
constructor(options, logger) {
|
|
817
|
-
this.options = options;
|
|
818
|
-
this.logger = logger;
|
|
819
|
-
this.id = `npm:${this.options.packageName}${this.options.exportName ? `#${this.options.exportName}` : ""}`;
|
|
820
|
-
}
|
|
821
|
-
options;
|
|
822
|
-
logger;
|
|
823
|
-
id;
|
|
824
|
-
listeners = /* @__PURE__ */ new Set();
|
|
825
|
-
cached;
|
|
826
|
-
async start() {
|
|
827
|
-
const bundle = await this.loadBundleFromPackage();
|
|
828
|
-
this.cached = bundle;
|
|
829
|
-
this.notify(bundle);
|
|
830
|
-
}
|
|
831
|
-
onChange(listener) {
|
|
832
|
-
this.listeners.add(listener);
|
|
833
|
-
if (this.cached) {
|
|
834
|
-
try {
|
|
835
|
-
listener(this.cached);
|
|
836
|
-
} catch (e) {
|
|
837
|
-
this.logger.warn(`[npm-source] listener threw on replay: ${e.message}`);
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
return () => {
|
|
841
|
-
this.listeners.delete(listener);
|
|
842
|
-
};
|
|
843
|
-
}
|
|
844
|
-
async stop() {
|
|
845
|
-
this.listeners.clear();
|
|
846
|
-
}
|
|
847
|
-
async loadBundleFromPackage() {
|
|
848
|
-
if (this.options.verifyProvenance) {
|
|
849
|
-
throw new Error(
|
|
850
|
-
`npm source "${this.options.packageName}": verifyProvenance is true, but npm provenance verification is not implemented, so the package was not loaded. Set verifyProvenance: false to load it without a provenance check, only for a package you trust: importing it runs its code before any bundle signature is checked, so pin its exact version and install it from a lockfile with integrity hashes.`
|
|
851
|
-
);
|
|
852
|
-
}
|
|
853
|
-
const mod = await this.dynamicImport(this.options.packageName);
|
|
854
|
-
const exportName = this.options.exportName ?? "default";
|
|
855
|
-
const modRecord = mod;
|
|
856
|
-
const exported = modRecord[exportName] ?? (exportName === "default" ? mod : void 0);
|
|
857
|
-
if (exported === void 0 || exported === null) {
|
|
858
|
-
throw new Error(`npm source "${this.options.packageName}": export "${exportName}" not found on the package`);
|
|
859
|
-
}
|
|
860
|
-
return parseOverlay({ kind: "object", content: exported });
|
|
861
|
-
}
|
|
862
|
-
// Indirected so tests can stub. The dynamic `import()` is wrapped in a
|
|
863
|
-
// lazily-built `new Function` so static bundlers (esbuild / `wrangler dev` /
|
|
864
|
-
// miniflare) don't try to resolve the runtime-computed package specifier at
|
|
865
|
-
// build time — installing an npm bundle is a Node-only path that never runs
|
|
866
|
-
// on a V8 isolate, but `createBundleSource` keeps NpmSource statically
|
|
867
|
-
// reachable in the worker bundle. The Function is only built when this runs
|
|
868
|
-
// (Node), so a Worker that merely bundles it never evaluates it.
|
|
869
|
-
dynamicImport(specifier) {
|
|
870
|
-
npmDynamicImport ??= new Function("s", "return import(s)");
|
|
871
|
-
return npmDynamicImport(specifier);
|
|
872
|
-
}
|
|
873
|
-
notify(bundle) {
|
|
874
|
-
for (const fn of this.listeners) {
|
|
875
|
-
try {
|
|
876
|
-
fn(bundle);
|
|
877
|
-
} catch (e) {
|
|
878
|
-
this.logger.warn(`[npm-source] listener threw: ${e.message}`);
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
};
|
|
883
|
-
|
|
884
|
-
// libs/adapters/src/skills/sources/saas-pull.source.ts
|
|
885
|
-
var import_auth = require("@frontmcp/auth");
|
|
886
|
-
var import_utils = require("@frontmcp/utils");
|
|
887
|
-
var DEFAULT_CACHE_DIR = ".frontmcp/skilled-openapi";
|
|
888
|
-
var DEFAULT_PULL_TIMEOUT_MS = 3e4;
|
|
889
|
-
var USER_AGENT = "frontmcp-skilled-openapi";
|
|
890
|
-
var SaasPullTokenRejectedError = class extends Error {
|
|
891
|
-
constructor(reason) {
|
|
892
|
-
super(`[saas-source] pull token rejected: ${reason}`);
|
|
893
|
-
this.name = "SaasPullTokenRejectedError";
|
|
894
|
-
}
|
|
895
|
-
};
|
|
896
|
-
function claimValues(claim) {
|
|
897
|
-
if (Array.isArray(claim)) return claim;
|
|
898
|
-
return claim === void 0 ? [] : [claim];
|
|
899
|
-
}
|
|
900
|
-
var PUBLIC_KEY_MEMBERS = /* @__PURE__ */ new Map([
|
|
901
|
-
["RSA", ["n", "e"]],
|
|
902
|
-
["EC", ["crv", "x", "y"]],
|
|
903
|
-
["OKP", ["crv", "x"]]
|
|
904
|
-
]);
|
|
905
|
-
function isUsableSigningKey(key) {
|
|
906
|
-
if (typeof key !== "object" || key === null) return false;
|
|
907
|
-
const jwk = key;
|
|
908
|
-
const members = typeof jwk["kty"] === "string" ? PUBLIC_KEY_MEMBERS.get(jwk["kty"]) : void 0;
|
|
909
|
-
if (!members || jwk["use"] !== void 0 && jwk["use"] !== "sig") return false;
|
|
910
|
-
return members.every((m) => typeof jwk[m] === "string" && jwk[m] !== "");
|
|
911
|
-
}
|
|
912
|
-
var SaasPullSource = class {
|
|
913
|
-
constructor(options, cacheDir, logger, deps = {}) {
|
|
914
|
-
this.options = options;
|
|
915
|
-
this.cacheDir = cacheDir;
|
|
916
|
-
this.logger = logger;
|
|
917
|
-
this.deps = deps;
|
|
918
|
-
this.id = `saas:${this.options.endpoint}`;
|
|
919
|
-
}
|
|
920
|
-
options;
|
|
921
|
-
cacheDir;
|
|
922
|
-
logger;
|
|
923
|
-
deps;
|
|
924
|
-
id;
|
|
925
|
-
listeners = /* @__PURE__ */ new Set();
|
|
926
|
-
pollHandle;
|
|
927
|
-
inFlight = false;
|
|
928
|
-
stopped = false;
|
|
929
|
-
jwksService = new import_auth.JwksService({});
|
|
930
|
-
async start() {
|
|
931
|
-
if (this.stopped || this.inFlight) {
|
|
932
|
-
this.schedulePoll();
|
|
933
|
-
return;
|
|
934
|
-
}
|
|
935
|
-
this.inFlight = true;
|
|
936
|
-
let bundle;
|
|
937
|
-
try {
|
|
938
|
-
try {
|
|
939
|
-
bundle = await this.fetchOnce();
|
|
940
|
-
await this.persistCache(bundle);
|
|
941
|
-
} catch (e) {
|
|
942
|
-
if (e instanceof SaasPullTokenRejectedError) throw e;
|
|
943
|
-
this.logger.warn(
|
|
944
|
-
`[saas-source] initial pull failed (${e.message}); attempting cached bundle fallback`
|
|
945
|
-
);
|
|
946
|
-
bundle = await this.loadCache();
|
|
947
|
-
if (!bundle) {
|
|
948
|
-
throw new Error(
|
|
949
|
-
`[saas-source] initial pull failed and no cached bundle is available ${this.cacheLocation()}: ${e.message}`,
|
|
950
|
-
{ cause: e }
|
|
951
|
-
);
|
|
952
|
-
}
|
|
953
|
-
this.logger.warn(`[saas-source] using cached bundle "${bundle.bundleId}@${bundle.version}"`);
|
|
954
|
-
}
|
|
955
|
-
this.notify(bundle);
|
|
956
|
-
} finally {
|
|
957
|
-
this.inFlight = false;
|
|
958
|
-
}
|
|
959
|
-
this.schedulePoll();
|
|
960
|
-
}
|
|
961
|
-
onChange(listener) {
|
|
962
|
-
this.listeners.add(listener);
|
|
963
|
-
return () => {
|
|
964
|
-
this.listeners.delete(listener);
|
|
965
|
-
};
|
|
966
|
-
}
|
|
967
|
-
async stop() {
|
|
968
|
-
this.stopped = true;
|
|
969
|
-
if (this.pollHandle) {
|
|
970
|
-
clearTimeout(this.pollHandle);
|
|
971
|
-
this.pollHandle = void 0;
|
|
972
|
-
}
|
|
973
|
-
this.listeners.clear();
|
|
974
|
-
}
|
|
975
|
-
schedulePoll() {
|
|
976
|
-
if (this.stopped || this.deps.disablePolling) return;
|
|
977
|
-
if (this.pollHandle) clearTimeout(this.pollHandle);
|
|
978
|
-
this.pollHandle = setTimeout(() => {
|
|
979
|
-
void this.pollOnce();
|
|
980
|
-
}, this.options.pollIntervalMs);
|
|
981
|
-
this.pollHandle.unref?.();
|
|
982
|
-
}
|
|
983
|
-
/**
|
|
984
|
-
* Manually pull a fresh bundle, persist it, and notify listeners. Drive this
|
|
985
|
-
* from a Cron Trigger / Durable Object alarm on runtimes without background
|
|
986
|
-
* timers (where `disablePolling` is set). Single-flight: returns `undefined`
|
|
987
|
-
* if a pull is already in flight; throws if the pull itself fails (the caller
|
|
988
|
-
* — e.g. a scheduled handler — decides how to surface it).
|
|
989
|
-
*/
|
|
990
|
-
async refresh() {
|
|
991
|
-
if (this.stopped || this.inFlight) return void 0;
|
|
992
|
-
this.inFlight = true;
|
|
993
|
-
try {
|
|
994
|
-
const bundle = await this.fetchOnce();
|
|
995
|
-
await this.persistCache(bundle);
|
|
996
|
-
this.notify(bundle);
|
|
997
|
-
return bundle;
|
|
998
|
-
} finally {
|
|
999
|
-
this.inFlight = false;
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
async pollOnce() {
|
|
1003
|
-
if (this.stopped) return;
|
|
1004
|
-
if (this.inFlight) {
|
|
1005
|
-
this.schedulePoll();
|
|
1006
|
-
return;
|
|
1007
|
-
}
|
|
1008
|
-
this.inFlight = true;
|
|
1009
|
-
try {
|
|
1010
|
-
const bundle = await this.fetchOnce();
|
|
1011
|
-
await this.persistCache(bundle);
|
|
1012
|
-
this.notify(bundle);
|
|
1013
|
-
} catch (e) {
|
|
1014
|
-
if (e instanceof SaasPullTokenRejectedError) {
|
|
1015
|
-
this.logger.error(`[saas-source] poll refused: ${e.message}; keeping the current bundle`);
|
|
1016
|
-
} else {
|
|
1017
|
-
this.logger.warn(`[saas-source] poll failed: ${e.message}`);
|
|
1018
|
-
}
|
|
1019
|
-
} finally {
|
|
1020
|
-
this.inFlight = false;
|
|
1021
|
-
this.schedulePoll();
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
// Indirected so tests can stub the network call.
|
|
1025
|
-
async httpGet(url, headers) {
|
|
1026
|
-
const controller = new AbortController();
|
|
1027
|
-
const timer = setTimeout(() => controller.abort(), DEFAULT_PULL_TIMEOUT_MS);
|
|
1028
|
-
try {
|
|
1029
|
-
const res = await fetch(url, { method: "GET", headers, signal: controller.signal, redirect: "manual" });
|
|
1030
|
-
if ((0, import_utils.isRedirectResponse)(res)) {
|
|
1031
|
-
throw new Error(`pull endpoint returned a redirect (${res.status}); not followed to protect the bearer token`);
|
|
1032
|
-
}
|
|
1033
|
-
const body = await res.text();
|
|
1034
|
-
return { status: res.status, body };
|
|
1035
|
-
} finally {
|
|
1036
|
-
clearTimeout(timer);
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
/**
|
|
1040
|
-
* Verify the pinned pull token against the issuer's JWKS, issuer and audience.
|
|
1041
|
-
* Throws {@link SaasPullTokenRejectedError} when the token is refused, and a
|
|
1042
|
-
* plain Error when the JWKS can't be fetched (an outage).
|
|
1043
|
-
*/
|
|
1044
|
-
async verifyPullToken() {
|
|
1045
|
-
const { jwksUrl, expectedIssuer, expectedAudience, authToken } = this.options;
|
|
1046
|
-
const { status, body } = await this.httpGet(jwksUrl, { Accept: "application/json", "User-Agent": USER_AGENT });
|
|
1047
|
-
if (status < 200 || status >= 300) {
|
|
1048
|
-
throw new Error(`JWKS ${jwksUrl} responded ${status}`);
|
|
1049
|
-
}
|
|
1050
|
-
let jwks;
|
|
1051
|
-
try {
|
|
1052
|
-
jwks = JSON.parse(body);
|
|
1053
|
-
} catch {
|
|
1054
|
-
throw new Error(`JWKS ${jwksUrl} is not valid JSON`);
|
|
1055
|
-
}
|
|
1056
|
-
if (!jwks || !Array.isArray(jwks.keys) || jwks.keys.length === 0) {
|
|
1057
|
-
throw new Error(`JWKS ${jwksUrl} has no keys`);
|
|
1058
|
-
}
|
|
1059
|
-
const signingKeys = jwks.keys.filter(isUsableSigningKey);
|
|
1060
|
-
if (signingKeys.length === 0) {
|
|
1061
|
-
throw new Error(`JWKS ${jwksUrl} has no usable signing keys`);
|
|
1062
|
-
}
|
|
1063
|
-
const verified = await this.jwksService.verifyTransparentToken(authToken, [
|
|
1064
|
-
{ id: "skilled-openapi-saas", issuerUrl: expectedIssuer, jwks: { keys: signingKeys } }
|
|
1065
|
-
]);
|
|
1066
|
-
if (!verified.ok || !verified.payload) {
|
|
1067
|
-
throw new SaasPullTokenRejectedError(
|
|
1068
|
-
`not a valid token from issuer "${expectedIssuer}" signed by a key in ${jwksUrl} (${verified.error ?? "verification failed"})`
|
|
1069
|
-
);
|
|
1070
|
-
}
|
|
1071
|
-
const payload = verified.payload;
|
|
1072
|
-
if (!claimValues(payload["aud"]).includes(expectedAudience)) {
|
|
1073
|
-
throw new SaasPullTokenRejectedError(`its "aud" claim does not include "${expectedAudience}"`);
|
|
1074
|
-
}
|
|
1075
|
-
const resource = claimValues(payload["resource"]);
|
|
1076
|
-
if (resource.length > 0 && !resource.includes(expectedAudience)) {
|
|
1077
|
-
throw new SaasPullTokenRejectedError(`its "resource" claim does not include "${expectedAudience}"`);
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
async fetchOnce() {
|
|
1081
|
-
await this.verifyPullToken();
|
|
1082
|
-
const { status, body } = await this.httpGet(this.options.endpoint, {
|
|
1083
|
-
Authorization: `Bearer ${this.options.authToken}`,
|
|
1084
|
-
Accept: "application/json, application/yaml",
|
|
1085
|
-
"User-Agent": USER_AGENT
|
|
1086
|
-
});
|
|
1087
|
-
if (status < 200 || status >= 300) {
|
|
1088
|
-
throw new Error(`pull responded ${status}`);
|
|
1089
|
-
}
|
|
1090
|
-
const trimmed = body.trimStart();
|
|
1091
|
-
return parseOverlay(
|
|
1092
|
-
trimmed.startsWith("{") || trimmed.startsWith("[") ? { kind: "json", content: body } : { kind: "yaml", content: body }
|
|
1093
|
-
);
|
|
1094
|
-
}
|
|
1095
|
-
/** Where the last-good bundle is kept, for messages: an injected store (a Worker's KV) has no file path. */
|
|
1096
|
-
cacheLocation() {
|
|
1097
|
-
return this.deps.cache ? "in the injected bundle cache" : `at ${this.cachePath()}`;
|
|
1098
|
-
}
|
|
1099
|
-
cachePath() {
|
|
1100
|
-
const dir = this.cacheDir ?? DEFAULT_CACHE_DIR;
|
|
1101
|
-
return (0, import_utils.pathResolve)(dir, `${this.options.expectedAudience.replace(/[^a-zA-Z0-9_.-]/g, "_")}.json`);
|
|
1102
|
-
}
|
|
1103
|
-
async persistCache(bundle) {
|
|
1104
|
-
try {
|
|
1105
|
-
if (this.deps.cache) {
|
|
1106
|
-
await this.deps.cache.write(bundle);
|
|
1107
|
-
return;
|
|
1108
|
-
}
|
|
1109
|
-
const filePath = this.cachePath();
|
|
1110
|
-
await (0, import_utils.ensureDir)((0, import_utils.dirname)(filePath));
|
|
1111
|
-
await (0, import_utils.writeFile)(filePath, JSON.stringify(bundle));
|
|
1112
|
-
} catch (e) {
|
|
1113
|
-
this.logger.warn(`[saas-source] cache persist failed: ${e.message}`);
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
async loadCache() {
|
|
1117
|
-
try {
|
|
1118
|
-
if (this.deps.cache) {
|
|
1119
|
-
const cached = await this.deps.cache.read();
|
|
1120
|
-
if (cached === void 0 || cached === null) return void 0;
|
|
1121
|
-
return parseOverlay({ kind: "object", content: cached });
|
|
1122
|
-
}
|
|
1123
|
-
const filePath = this.cachePath();
|
|
1124
|
-
const raw = await (0, import_utils.readFile)(filePath, "utf8");
|
|
1125
|
-
return parseOverlay({ kind: "json", content: raw });
|
|
1126
|
-
} catch {
|
|
1127
|
-
return void 0;
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
notify(bundle) {
|
|
1131
|
-
for (const fn of this.listeners) {
|
|
1132
|
-
try {
|
|
1133
|
-
fn(bundle);
|
|
1134
|
-
} catch (e) {
|
|
1135
|
-
this.logger.warn(`[saas-source] listener threw: ${e.message}`);
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
};
|
|
1140
|
-
|
|
1141
|
-
// libs/adapters/src/skills/sources/static.source.ts
|
|
1142
|
-
var import_utils2 = require("@frontmcp/utils");
|
|
1143
|
-
var StaticSource = class {
|
|
1144
|
-
constructor(options, logger) {
|
|
1145
|
-
this.options = options;
|
|
1146
|
-
this.logger = logger;
|
|
1147
|
-
this.id = `static:${this.options.path}`;
|
|
1148
|
-
}
|
|
1149
|
-
options;
|
|
1150
|
-
logger;
|
|
1151
|
-
id;
|
|
1152
|
-
listeners = /* @__PURE__ */ new Set();
|
|
1153
|
-
watcher;
|
|
1154
|
-
debounceTimer;
|
|
1155
|
-
stopped = false;
|
|
1156
|
-
async start() {
|
|
1157
|
-
const bundle = await this.fetchOnce();
|
|
1158
|
-
this.notify(bundle);
|
|
1159
|
-
if (this.options.watch) {
|
|
1160
|
-
this.beginWatch();
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
onChange(listener) {
|
|
1164
|
-
this.listeners.add(listener);
|
|
1165
|
-
return () => {
|
|
1166
|
-
this.listeners.delete(listener);
|
|
1167
|
-
};
|
|
1168
|
-
}
|
|
1169
|
-
async stop() {
|
|
1170
|
-
this.stopped = true;
|
|
1171
|
-
if (this.watcher) {
|
|
1172
|
-
this.watcher.close();
|
|
1173
|
-
this.watcher = void 0;
|
|
1174
|
-
}
|
|
1175
|
-
if (this.debounceTimer) {
|
|
1176
|
-
clearTimeout(this.debounceTimer);
|
|
1177
|
-
this.debounceTimer = void 0;
|
|
1178
|
-
}
|
|
1179
|
-
this.listeners.clear();
|
|
1180
|
-
}
|
|
1181
|
-
async fetchOnce() {
|
|
1182
|
-
const absPath = (0, import_utils2.pathResolve)(this.options.path);
|
|
1183
|
-
const raw = await (0, import_utils2.readFile)(absPath, "utf8");
|
|
1184
|
-
const ext = (0, import_utils2.extname)(absPath).toLowerCase();
|
|
1185
|
-
let input;
|
|
1186
|
-
if (ext === ".json") {
|
|
1187
|
-
input = { kind: "json", content: raw };
|
|
1188
|
-
} else if (ext === ".yaml" || ext === ".yml") {
|
|
1189
|
-
input = { kind: "yaml", content: raw };
|
|
1190
|
-
} else {
|
|
1191
|
-
const trimmed = raw.trimStart();
|
|
1192
|
-
input = trimmed.startsWith("{") || trimmed.startsWith("[") ? { kind: "json", content: raw } : { kind: "yaml", content: raw };
|
|
1193
|
-
}
|
|
1194
|
-
return parseOverlay(input);
|
|
1195
|
-
}
|
|
1196
|
-
notify(bundle) {
|
|
1197
|
-
for (const fn of this.listeners) {
|
|
1198
|
-
try {
|
|
1199
|
-
fn(bundle);
|
|
1200
|
-
} catch (e) {
|
|
1201
|
-
this.logger.warn(`[static-source] listener threw: ${e.message}`);
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
beginWatch() {
|
|
1206
|
-
const absPath = (0, import_utils2.pathResolve)(this.options.path);
|
|
1207
|
-
try {
|
|
1208
|
-
this.watcher = (0, import_utils2.watchFile)(absPath, () => this.scheduleRefresh());
|
|
1209
|
-
} catch (e) {
|
|
1210
|
-
this.logger.warn(`[static-source] fs.watch failed (${e.message}); polling at 2s instead`);
|
|
1211
|
-
const tick = () => {
|
|
1212
|
-
if (this.stopped) return;
|
|
1213
|
-
this.scheduleRefresh();
|
|
1214
|
-
setTimeout(tick, 2e3).unref?.();
|
|
1215
|
-
};
|
|
1216
|
-
setTimeout(tick, 2e3).unref?.();
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
scheduleRefresh() {
|
|
1220
|
-
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
1221
|
-
this.debounceTimer = setTimeout(async () => {
|
|
1222
|
-
if (this.stopped) return;
|
|
1223
|
-
try {
|
|
1224
|
-
const bundle = await this.fetchOnce();
|
|
1225
|
-
this.notify(bundle);
|
|
1226
|
-
} catch (e) {
|
|
1227
|
-
this.logger.warn(`[static-source] refresh failed: ${e.message}`);
|
|
1228
|
-
}
|
|
1229
|
-
}, 250);
|
|
1230
|
-
this.debounceTimer.unref?.();
|
|
1231
|
-
}
|
|
1232
|
-
};
|
|
1233
|
-
|
|
1234
|
-
// libs/adapters/src/skills/sources/index.ts
|
|
1235
|
-
function createBundleSource(source, cacheDir, logger, deps) {
|
|
1236
|
-
switch (source.type) {
|
|
1237
|
-
case "static":
|
|
1238
|
-
return new StaticSource(source, logger);
|
|
1239
|
-
case "npm":
|
|
1240
|
-
return new NpmSource(source, logger);
|
|
1241
|
-
case "saas":
|
|
1242
|
-
return new SaasPullSource(source, cacheDir, logger, deps);
|
|
1243
|
-
case "inline":
|
|
1244
|
-
return new InlineSource(source, logger);
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
// libs/adapters/src/skills/openapi/compile-skilled-bundle.ts
|
|
1249
|
-
var import_mcp_from_openapi = require("mcp-from-openapi");
|
|
1250
|
-
async function compileSkilledBundleFromOpenApi(openapi, skills, options) {
|
|
1251
|
-
const serviceId = options.serviceId ?? "api";
|
|
1252
|
-
const baseUrl = options.baseUrl ?? openapi.servers?.[0]?.url;
|
|
1253
|
-
if (!baseUrl) {
|
|
1254
|
-
throw new Error("compileSkilledBundleFromOpenApi: no baseUrl \u2014 pass options.baseUrl or declare servers[] in the OpenAPI doc.");
|
|
1255
|
-
}
|
|
1256
|
-
const generator = await import_mcp_from_openapi.OpenAPIToolGenerator.fromJSON(openapi);
|
|
1257
|
-
const tools = await generator.generateTools();
|
|
1258
|
-
const byOperationId = /* @__PURE__ */ new Map();
|
|
1259
|
-
for (const tool of tools) {
|
|
1260
|
-
const opId = tool.metadata.operationId;
|
|
1261
|
-
if (opId) byOperationId.set(opId, tool);
|
|
1262
|
-
}
|
|
1263
|
-
const referenced = /* @__PURE__ */ new Set();
|
|
1264
|
-
for (const skill of skills) for (const opId of skill.operationIds) referenced.add(opId);
|
|
1265
|
-
const operations = {};
|
|
1266
|
-
for (const opId of referenced) {
|
|
1267
|
-
const tool = byOperationId.get(opId);
|
|
1268
|
-
if (!tool) {
|
|
1269
|
-
throw new Error(`compileSkilledBundleFromOpenApi: operationId "${opId}" is referenced by a skill but not defined in the OpenAPI document.`);
|
|
1270
|
-
}
|
|
1271
|
-
operations[opId] = {
|
|
1272
|
-
operationId: opId,
|
|
1273
|
-
serviceId,
|
|
1274
|
-
httpMethod: tool.metadata.method.toUpperCase(),
|
|
1275
|
-
pathTemplate: tool.metadata.path,
|
|
1276
|
-
summary: tool.metadata.operationSummary ?? tool.description,
|
|
1277
|
-
inputSchema: tool.inputSchema,
|
|
1278
|
-
outputSchema: tool.outputSchema ?? { type: "object" },
|
|
1279
|
-
mapper: tool.mapper,
|
|
1280
|
-
authBindingRef: "default"
|
|
1281
|
-
};
|
|
1282
|
-
}
|
|
1283
|
-
return {
|
|
1284
|
-
schemaVersion: 1,
|
|
1285
|
-
bundleId: options.bundleId,
|
|
1286
|
-
version: options.version,
|
|
1287
|
-
generatedAt: options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1288
|
-
sourceDigest: options.sourceDigest ?? "0".repeat(64),
|
|
1289
|
-
services: [{ id: serviceId, baseUrl }],
|
|
1290
|
-
authBindings: { default: options.authBinding ?? { kind: "none" } },
|
|
1291
|
-
skills: skills.map((s) => ({
|
|
1292
|
-
id: s.id,
|
|
1293
|
-
name: s.name,
|
|
1294
|
-
description: s.description,
|
|
1295
|
-
instructions: s.instructions,
|
|
1296
|
-
tags: s.tags ?? [],
|
|
1297
|
-
operationIds: s.operationIds
|
|
1298
|
-
})),
|
|
1299
|
-
operations
|
|
1300
|
-
};
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
// libs/adapters/src/skills/sources/filesystem-skills.source.ts
|
|
1304
|
-
var import_utils3 = require("@frontmcp/utils");
|
|
1305
|
-
var DEFAULT_OPTIONS = {
|
|
1306
|
-
skillsDir: "./skills",
|
|
1307
|
-
watch: true,
|
|
1308
|
-
debounceMs: 200,
|
|
1309
|
-
pollIntervalMs: 2e3
|
|
1310
|
-
};
|
|
1311
|
-
var FilesystemSkillsSource = class {
|
|
1312
|
-
constructor(options, logger) {
|
|
1313
|
-
this.logger = logger;
|
|
1314
|
-
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
1315
|
-
this.id = `filesystem-skills:${this.options.skillsDir}`;
|
|
1316
|
-
}
|
|
1317
|
-
logger;
|
|
1318
|
-
id;
|
|
1319
|
-
options;
|
|
1320
|
-
listeners = /* @__PURE__ */ new Set();
|
|
1321
|
-
known = /* @__PURE__ */ new Map();
|
|
1322
|
-
// absPath → last emitted content
|
|
1323
|
-
watcher;
|
|
1324
|
-
pollTimer;
|
|
1325
|
-
debounceTimers = /* @__PURE__ */ new Map();
|
|
1326
|
-
stopped = false;
|
|
1327
|
-
rootResolved = "";
|
|
1328
|
-
onChange(listener) {
|
|
1329
|
-
this.listeners.add(listener);
|
|
1330
|
-
return () => {
|
|
1331
|
-
this.listeners.delete(listener);
|
|
1332
|
-
};
|
|
1333
|
-
}
|
|
1334
|
-
async start() {
|
|
1335
|
-
const absRoot = (0, import_utils3.pathResolve)(this.options.skillsDir);
|
|
1336
|
-
try {
|
|
1337
|
-
this.rootResolved = await (0, import_utils3.realpath)(absRoot);
|
|
1338
|
-
} catch {
|
|
1339
|
-
this.rootResolved = absRoot;
|
|
1340
|
-
}
|
|
1341
|
-
await this.initialScan();
|
|
1342
|
-
if (this.options.watch) {
|
|
1343
|
-
this.beginWatch();
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
async stop() {
|
|
1347
|
-
this.stopped = true;
|
|
1348
|
-
if (this.watcher) {
|
|
1349
|
-
try {
|
|
1350
|
-
this.watcher.close();
|
|
1351
|
-
} catch {
|
|
1352
|
-
}
|
|
1353
|
-
this.watcher = void 0;
|
|
1354
|
-
}
|
|
1355
|
-
if (this.pollTimer) {
|
|
1356
|
-
clearTimeout(this.pollTimer);
|
|
1357
|
-
this.pollTimer = void 0;
|
|
1358
|
-
}
|
|
1359
|
-
for (const t of this.debounceTimers.values()) clearTimeout(t);
|
|
1360
|
-
this.debounceTimers.clear();
|
|
1361
|
-
this.listeners.clear();
|
|
1362
|
-
this.known.clear();
|
|
1363
|
-
}
|
|
1364
|
-
async initialScan() {
|
|
1365
|
-
const entries = await this.listSkillSubdirs();
|
|
1366
|
-
for (const dir of entries) {
|
|
1367
|
-
await this.refreshSkill(dir);
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
async listSkillSubdirs() {
|
|
1371
|
-
let entries;
|
|
1372
|
-
try {
|
|
1373
|
-
entries = await (0, import_utils3.readdir)(this.rootResolved);
|
|
1374
|
-
} catch (e) {
|
|
1375
|
-
this.logger.warn(`[fs-skills] cannot read ${this.rootResolved}: ${e.message}`);
|
|
1376
|
-
return [];
|
|
1377
|
-
}
|
|
1378
|
-
const out = [];
|
|
1379
|
-
for (const name of entries) {
|
|
1380
|
-
if (name.startsWith(".")) continue;
|
|
1381
|
-
const abs = (0, import_utils3.joinPath)(this.rootResolved, name);
|
|
1382
|
-
let resolved;
|
|
1383
|
-
try {
|
|
1384
|
-
resolved = await (0, import_utils3.realpath)(abs);
|
|
1385
|
-
} catch {
|
|
1386
|
-
continue;
|
|
1387
|
-
}
|
|
1388
|
-
if (!resolved.startsWith(this.rootResolved + import_utils3.sep) && resolved !== this.rootResolved) {
|
|
1389
|
-
this.logger.warn(`[fs-skills] skipping ${name}: realpath escapes skillsDir`);
|
|
1390
|
-
continue;
|
|
1391
|
-
}
|
|
1392
|
-
try {
|
|
1393
|
-
const st = await (0, import_utils3.stat)(resolved);
|
|
1394
|
-
if (st.isDirectory()) out.push(resolved);
|
|
1395
|
-
} catch {
|
|
1396
|
-
}
|
|
1397
|
-
}
|
|
1398
|
-
return out.sort();
|
|
1399
|
-
}
|
|
1400
|
-
/** Reload a single skill subdir and emit upsert (or delete if vanished). */
|
|
1401
|
-
async refreshSkill(absDir) {
|
|
1402
|
-
if (this.stopped) return;
|
|
1403
|
-
let exists = false;
|
|
1404
|
-
try {
|
|
1405
|
-
const st = await (0, import_utils3.stat)(absDir);
|
|
1406
|
-
exists = st.isDirectory();
|
|
1407
|
-
} catch {
|
|
1408
|
-
exists = false;
|
|
1409
|
-
}
|
|
1410
|
-
if (!exists) {
|
|
1411
|
-
const prev2 = this.known.get(absDir);
|
|
1412
|
-
if (prev2) {
|
|
1413
|
-
this.known.delete(absDir);
|
|
1414
|
-
this.emit({ op: "delete", skillId: prev2.id, absPath: absDir });
|
|
1415
|
-
}
|
|
1416
|
-
return;
|
|
1417
|
-
}
|
|
1418
|
-
let content;
|
|
1419
|
-
try {
|
|
1420
|
-
content = await loadFilesystemSkill(absDir);
|
|
1421
|
-
} catch (e) {
|
|
1422
|
-
this.logger.warn(`[fs-skills] failed to load ${absDir}: ${e.message}`);
|
|
1423
|
-
return;
|
|
1424
|
-
}
|
|
1425
|
-
const prev = this.known.get(absDir);
|
|
1426
|
-
if (prev && skillContentEqual(prev, content)) {
|
|
1427
|
-
return;
|
|
1428
|
-
}
|
|
1429
|
-
this.known.set(absDir, content);
|
|
1430
|
-
this.emit({ op: "upsert", skill: content, absPath: absDir });
|
|
1431
|
-
}
|
|
1432
|
-
emit(event) {
|
|
1433
|
-
for (const fn of this.listeners) {
|
|
1434
|
-
try {
|
|
1435
|
-
fn(event);
|
|
1436
|
-
} catch (e) {
|
|
1437
|
-
this.logger.warn(`[fs-skills] listener threw: ${e.message}`);
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
beginWatch() {
|
|
1442
|
-
try {
|
|
1443
|
-
this.watcher = (0, import_utils3.watchFile)(
|
|
1444
|
-
this.rootResolved,
|
|
1445
|
-
(_eventType, filename) => {
|
|
1446
|
-
if (this.stopped) return;
|
|
1447
|
-
if (!filename) {
|
|
1448
|
-
this.scheduleRescan();
|
|
1449
|
-
return;
|
|
1450
|
-
}
|
|
1451
|
-
const first = filename.split(import_utils3.sep)[0];
|
|
1452
|
-
if (!first || first.startsWith(".")) return;
|
|
1453
|
-
const absDir = (0, import_utils3.joinPath)(this.rootResolved, first);
|
|
1454
|
-
this.scheduleRefresh(absDir);
|
|
1455
|
-
},
|
|
1456
|
-
{ recursive: true }
|
|
1457
|
-
);
|
|
1458
|
-
this.watcher.onError((e) => {
|
|
1459
|
-
this.logger.warn(`[fs-skills] fs.watch error: ${e.message}; falling back to polling`);
|
|
1460
|
-
this.watcher?.close();
|
|
1461
|
-
this.watcher = void 0;
|
|
1462
|
-
this.beginPolling();
|
|
1463
|
-
});
|
|
1464
|
-
} catch (e) {
|
|
1465
|
-
this.logger.warn(
|
|
1466
|
-
`[fs-skills] fs.watch failed (${e.message}); polling at ${this.options.pollIntervalMs}ms instead`
|
|
1467
|
-
);
|
|
1468
|
-
this.beginPolling();
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
beginPolling() {
|
|
1472
|
-
const tick = async () => {
|
|
1473
|
-
if (this.stopped) return;
|
|
1474
|
-
try {
|
|
1475
|
-
await this.pollOnce();
|
|
1476
|
-
} catch (e) {
|
|
1477
|
-
this.logger.warn(`[fs-skills] poll tick error: ${e.message}`);
|
|
1478
|
-
}
|
|
1479
|
-
if (this.stopped) return;
|
|
1480
|
-
this.pollTimer = setTimeout(tick, this.options.pollIntervalMs);
|
|
1481
|
-
this.pollTimer.unref?.();
|
|
1482
|
-
};
|
|
1483
|
-
this.pollTimer = setTimeout(tick, this.options.pollIntervalMs);
|
|
1484
|
-
this.pollTimer.unref?.();
|
|
1485
|
-
}
|
|
1486
|
-
/** Compare current dir listing to known set; emit upserts/deletes. */
|
|
1487
|
-
async pollOnce() {
|
|
1488
|
-
const dirs = new Set(await this.listSkillSubdirs());
|
|
1489
|
-
for (const known of [...this.known.keys()]) {
|
|
1490
|
-
if (!dirs.has(known)) {
|
|
1491
|
-
await this.refreshSkill(known);
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
for (const abs of dirs) {
|
|
1495
|
-
await this.refreshSkill(abs);
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
/** Coarse rescan when filename is unknown (some Linux/macOS fs.watch events). */
|
|
1499
|
-
scheduleRescan() {
|
|
1500
|
-
const key = "<rescan>";
|
|
1501
|
-
const existing = this.debounceTimers.get(key);
|
|
1502
|
-
if (existing) clearTimeout(existing);
|
|
1503
|
-
const t = setTimeout(() => {
|
|
1504
|
-
this.debounceTimers.delete(key);
|
|
1505
|
-
void this.pollOnce().catch((e) => {
|
|
1506
|
-
this.logger.warn(`[fs-skills] rescan failed: ${e.message}`);
|
|
1507
|
-
});
|
|
1508
|
-
}, this.options.debounceMs);
|
|
1509
|
-
t.unref?.();
|
|
1510
|
-
this.debounceTimers.set(key, t);
|
|
1511
|
-
}
|
|
1512
|
-
scheduleRefresh(absDir) {
|
|
1513
|
-
const existing = this.debounceTimers.get(absDir);
|
|
1514
|
-
if (existing) clearTimeout(existing);
|
|
1515
|
-
const t = setTimeout(() => {
|
|
1516
|
-
this.debounceTimers.delete(absDir);
|
|
1517
|
-
void this.refreshSkill(absDir).catch((e) => {
|
|
1518
|
-
this.logger.warn(`[fs-skills] refresh failed for ${absDir}: ${e.message}`);
|
|
1519
|
-
});
|
|
1520
|
-
}, this.options.debounceMs);
|
|
1521
|
-
t.unref?.();
|
|
1522
|
-
this.debounceTimers.set(absDir, t);
|
|
1523
|
-
}
|
|
1524
|
-
};
|
|
1525
|
-
async function loadFilesystemSkill(absDir) {
|
|
1526
|
-
const skillMdPath = (0, import_utils3.joinPath)(absDir, "SKILL.md");
|
|
1527
|
-
const raw = await (0, import_utils3.readFile)(skillMdPath, "utf8");
|
|
1528
|
-
const { frontmatter, body } = parseSkillFrontmatter(raw);
|
|
1529
|
-
const dirName = (0, import_utils3.basename)(absDir);
|
|
1530
|
-
const name = frontmatter["name"] ?? dirName;
|
|
1531
|
-
const description = frontmatter["description"] ?? "";
|
|
1532
|
-
if (!description) {
|
|
1533
|
-
throw new Error(`SKILL.md missing 'description' frontmatter (in ${absDir})`);
|
|
1534
|
-
}
|
|
1535
|
-
const instructions = body.trim();
|
|
1536
|
-
if (!instructions) {
|
|
1537
|
-
throw new Error(`SKILL.md missing body content (in ${absDir})`);
|
|
1538
|
-
}
|
|
1539
|
-
const tools = parseToolList(frontmatter["tools"]);
|
|
1540
|
-
const resources = {};
|
|
1541
|
-
for (const sub of ["scripts", "references", "examples", "assets"]) {
|
|
1542
|
-
const subAbs = (0, import_utils3.joinPath)(absDir, sub);
|
|
1543
|
-
try {
|
|
1544
|
-
const st = await (0, import_utils3.stat)(subAbs);
|
|
1545
|
-
if (st.isDirectory()) resources[sub] = subAbs;
|
|
1546
|
-
} catch {
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
const out = {
|
|
1550
|
-
id: name,
|
|
1551
|
-
name,
|
|
1552
|
-
description,
|
|
1553
|
-
instructions,
|
|
1554
|
-
tools,
|
|
1555
|
-
...Object.keys(resources).length > 0 && { resources }
|
|
1556
|
-
};
|
|
1557
|
-
if (typeof frontmatter["license"] === "string") out.license = frontmatter["license"];
|
|
1558
|
-
if (typeof frontmatter["compatibility"] === "string") out.compatibility = frontmatter["compatibility"];
|
|
1559
|
-
return out;
|
|
1560
|
-
}
|
|
1561
|
-
function parseSkillFrontmatter(raw) {
|
|
1562
|
-
if (!raw.startsWith("---")) {
|
|
1563
|
-
return { frontmatter: {}, body: raw };
|
|
1564
|
-
}
|
|
1565
|
-
const closeIdx = raw.indexOf("\n---", 3);
|
|
1566
|
-
if (closeIdx === -1) {
|
|
1567
|
-
return { frontmatter: {}, body: raw };
|
|
1568
|
-
}
|
|
1569
|
-
const yamlBlock = raw.slice(3, closeIdx).trim();
|
|
1570
|
-
let bodyStart = closeIdx + 4;
|
|
1571
|
-
if (raw[bodyStart] === "\n") bodyStart += 1;
|
|
1572
|
-
const body = raw.slice(bodyStart);
|
|
1573
|
-
const fm = {};
|
|
1574
|
-
let currentListKey;
|
|
1575
|
-
for (const line of yamlBlock.split("\n")) {
|
|
1576
|
-
if (!line.trim()) continue;
|
|
1577
|
-
if (line.startsWith("- ") && currentListKey) {
|
|
1578
|
-
const arr = fm[currentListKey] ?? [];
|
|
1579
|
-
arr.push(stripQuotes(line.slice(2).trim()));
|
|
1580
|
-
fm[currentListKey] = arr;
|
|
1581
|
-
continue;
|
|
1582
|
-
}
|
|
1583
|
-
const colon = line.indexOf(":");
|
|
1584
|
-
if (colon === -1) continue;
|
|
1585
|
-
const key = line.slice(0, colon).trim();
|
|
1586
|
-
const rest = line.slice(colon + 1).trim();
|
|
1587
|
-
if (!rest) {
|
|
1588
|
-
currentListKey = key;
|
|
1589
|
-
fm[key] = [];
|
|
1590
|
-
continue;
|
|
1591
|
-
}
|
|
1592
|
-
currentListKey = void 0;
|
|
1593
|
-
fm[key] = stripQuotes(rest);
|
|
1594
|
-
}
|
|
1595
|
-
return { frontmatter: fm, body };
|
|
1596
|
-
}
|
|
1597
|
-
function stripQuotes(s) {
|
|
1598
|
-
if (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'")) {
|
|
1599
|
-
return s.slice(1, -1);
|
|
1600
|
-
}
|
|
1601
|
-
return s;
|
|
1602
|
-
}
|
|
1603
|
-
function parseToolList(raw) {
|
|
1604
|
-
if (Array.isArray(raw)) {
|
|
1605
|
-
return raw.map((t) => typeof t === "string" ? { name: t } : null).filter((t) => t !== null);
|
|
1606
|
-
}
|
|
1607
|
-
if (typeof raw === "string") {
|
|
1608
|
-
return raw.split(",").map((t) => t.trim()).filter(Boolean).map((name) => ({ name }));
|
|
1609
|
-
}
|
|
1610
|
-
return [];
|
|
1611
|
-
}
|
|
1612
|
-
function skillContentEqual(a, b) {
|
|
1613
|
-
return a.id === b.id && a.name === b.name && a.description === b.description && a.instructions === b.instructions && a.license === b.license && a.compatibility === b.compatibility && JSON.stringify(a.tools) === JSON.stringify(b.tools) && JSON.stringify(a.resources ?? {}) === JSON.stringify(b.resources ?? {});
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
// libs/adapters/src/skills/security/bundle-signature.ts
|
|
1617
|
-
var import_utils4 = require("@frontmcp/utils");
|
|
1618
|
-
function classifyReason(reason) {
|
|
1619
|
-
if (reason.includes("missing integrity")) return "missing_integrity";
|
|
1620
|
-
if (reason.includes("digest mismatch")) return "digest_mismatch";
|
|
1621
|
-
if (reason.includes("unknown signing keyId")) return "unknown_key_id";
|
|
1622
|
-
if (reason.includes("alg=")) return "alg_mismatch";
|
|
1623
|
-
if (reason.includes("parse public key")) return "malformed_public_key";
|
|
1624
|
-
if (reason.includes("not valid base64url")) return "malformed_signature";
|
|
1625
|
-
if (reason.includes("signature verify threw")) return "verify_threw";
|
|
1626
|
-
if (reason.includes("signature verification failed")) return "verify_failed";
|
|
1627
|
-
return "other";
|
|
1628
|
-
}
|
|
1629
|
-
function canonicalize(value) {
|
|
1630
|
-
if (value === null || typeof value !== "object") {
|
|
1631
|
-
return JSON.stringify(value);
|
|
1632
|
-
}
|
|
1633
|
-
if (Array.isArray(value)) {
|
|
1634
|
-
return `[${value.map((v) => canonicalize(v)).join(",")}]`;
|
|
1635
|
-
}
|
|
1636
|
-
const obj = value;
|
|
1637
|
-
const keys = Object.keys(obj).sort();
|
|
1638
|
-
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(obj[k])}`).join(",")}}`;
|
|
1639
|
-
}
|
|
1640
|
-
function bundleDigest(bundle) {
|
|
1641
|
-
const { integrity: _drop, ...rest } = bundle;
|
|
1642
|
-
void _drop;
|
|
1643
|
-
return (0, import_utils4.sha256Hex)(canonicalize(rest));
|
|
1644
|
-
}
|
|
1645
|
-
function decodeSignatureBytes(signatureBase64Url) {
|
|
1646
|
-
try {
|
|
1647
|
-
return (0, import_utils4.base64urlDecode)(signatureBase64Url);
|
|
1648
|
-
} catch {
|
|
1649
|
-
return void 0;
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
function verifyBundleSignature(bundle, trustedKeys, telemetry) {
|
|
1653
|
-
const failuresCounter = telemetry?.createCounter(
|
|
1654
|
-
"frontmcp_skills_signature_failures_total",
|
|
1655
|
-
"Number of bundle signature verification failures, partitioned by reason."
|
|
1656
|
-
);
|
|
1657
|
-
const verificationsCounter = telemetry?.createCounter(
|
|
1658
|
-
"frontmcp_skills_signature_verifications_total",
|
|
1659
|
-
"Number of bundle signature verification attempts, partitioned by status (ok|error)."
|
|
1660
|
-
);
|
|
1661
|
-
const fail = (reason) => {
|
|
1662
|
-
failuresCounter?.inc(1, { reason: classifyReason(reason) });
|
|
1663
|
-
verificationsCounter?.inc(1, { status: "error" });
|
|
1664
|
-
return { ok: false, reason };
|
|
1665
|
-
};
|
|
1666
|
-
if (!bundle.integrity) {
|
|
1667
|
-
return fail("bundle missing integrity envelope");
|
|
1668
|
-
}
|
|
1669
|
-
const { alg, keyId, signature, digest } = bundle.integrity;
|
|
1670
|
-
const expectedDigest = bundleDigest(bundle);
|
|
1671
|
-
if (digest.toLowerCase() !== expectedDigest.toLowerCase()) {
|
|
1672
|
-
return fail(`digest mismatch (envelope=${digest.slice(0, 12)}.. computed=${expectedDigest.slice(0, 12)}..)`);
|
|
1673
|
-
}
|
|
1674
|
-
const trusted = trustedKeys.find((k) => k.keyId === keyId);
|
|
1675
|
-
if (!trusted) {
|
|
1676
|
-
return fail(`unknown signing keyId "${keyId}" \u2014 not in trustedKeys allowlist`);
|
|
1677
|
-
}
|
|
1678
|
-
if (trusted.alg !== alg) {
|
|
1679
|
-
return fail(`key "${keyId}" alg=${trusted.alg} but envelope alg=${alg}`);
|
|
1680
|
-
}
|
|
1681
|
-
let publicJwk;
|
|
1682
|
-
try {
|
|
1683
|
-
publicJwk = (0, import_utils4.pemToPublicJwk)(trusted.publicKeyPem);
|
|
1684
|
-
} catch (e) {
|
|
1685
|
-
return fail(`failed to parse public key for "${keyId}": ${e.message}`);
|
|
1686
|
-
}
|
|
1687
|
-
const sigBytes = decodeSignatureBytes(signature);
|
|
1688
|
-
if (!sigBytes) {
|
|
1689
|
-
return fail("signature is not valid base64url");
|
|
1690
|
-
}
|
|
1691
|
-
const { integrity: _drop, ...rest } = bundle;
|
|
1692
|
-
void _drop;
|
|
1693
|
-
const canonicalBytes = new TextEncoder().encode(canonicalize(rest));
|
|
1694
|
-
const verified = (0, import_utils4.rsaVerifySync)(alg, canonicalBytes, publicJwk, sigBytes);
|
|
1695
|
-
if (!verified) {
|
|
1696
|
-
return fail("signature verification failed");
|
|
1697
|
-
}
|
|
1698
|
-
verificationsCounter?.inc(1, { status: "ok" });
|
|
1699
|
-
return { ok: true, keyId, alg };
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
// libs/adapters/src/skills/security/jwt-verifier.ts
|
|
1703
|
-
var import_auth2 = require("@frontmcp/auth");
|
|
1704
|
-
var DEFAULT_REQUIRED_ROLES = ["frontmcp:cloud:push"];
|
|
1705
|
-
var BundlePushJwtVerifier = class {
|
|
1706
|
-
constructor(options, logger, jwks) {
|
|
1707
|
-
this.options = options;
|
|
1708
|
-
this.logger = logger;
|
|
1709
|
-
this.jwks = jwks ?? new import_auth2.JwksService({});
|
|
1710
|
-
this.providerRef = {
|
|
1711
|
-
id: "skilled-openapi-cloud-push",
|
|
1712
|
-
issuerUrl: options.expectedIssuer,
|
|
1713
|
-
jwksUri: options.jwksUri
|
|
1714
|
-
};
|
|
1715
|
-
}
|
|
1716
|
-
options;
|
|
1717
|
-
logger;
|
|
1718
|
-
providerRef;
|
|
1719
|
-
jwks;
|
|
1720
|
-
/**
|
|
1721
|
-
* Verify a SaaS-issued bearer token. Returns ok=true only if signature,
|
|
1722
|
-
* issuer, audience, RFC 8707 resource indicator, and role claims all check.
|
|
1723
|
-
*/
|
|
1724
|
-
async verify(token) {
|
|
1725
|
-
if (!token || typeof token !== "string") {
|
|
1726
|
-
return { ok: false, reason: "token missing" };
|
|
1727
|
-
}
|
|
1728
|
-
let res;
|
|
1729
|
-
try {
|
|
1730
|
-
res = await this.jwks.verifyTransparentToken(token, [this.providerRef]);
|
|
1731
|
-
} catch (e) {
|
|
1732
|
-
this.logger.debug(`[skilled-openapi] jwt verify threw: ${e.message}`);
|
|
1733
|
-
return { ok: false, reason: `verify failed: ${e.message}` };
|
|
1734
|
-
}
|
|
1735
|
-
if (!res.ok || !res.payload) {
|
|
1736
|
-
return { ok: false, reason: res.error ?? "signature verify failed" };
|
|
1737
|
-
}
|
|
1738
|
-
const payload = res.payload;
|
|
1739
|
-
const aud = payload["aud"];
|
|
1740
|
-
const audValues = Array.isArray(aud) ? aud : aud !== void 0 ? [aud] : [];
|
|
1741
|
-
if (!audValues.includes(this.options.expectedAudience)) {
|
|
1742
|
-
return { ok: false, reason: `aud mismatch (expected "${this.options.expectedAudience}")` };
|
|
1743
|
-
}
|
|
1744
|
-
const resource = payload["resource"];
|
|
1745
|
-
const resValues = Array.isArray(resource) ? resource : resource !== void 0 ? [resource] : [];
|
|
1746
|
-
if (!resValues.includes(this.options.expectedResource)) {
|
|
1747
|
-
return {
|
|
1748
|
-
ok: false,
|
|
1749
|
-
reason: `RFC 8707 resource indicator mismatch (expected "${this.options.expectedResource}")`
|
|
1750
|
-
};
|
|
1751
|
-
}
|
|
1752
|
-
const requiredRoles = this.options.requiredRoles ?? DEFAULT_REQUIRED_ROLES;
|
|
1753
|
-
if (requiredRoles.length > 0 && !this.matchesAny(payload["roles"], requiredRoles)) {
|
|
1754
|
-
return { ok: false, reason: `missing required role (need one of ${requiredRoles.join(", ")})` };
|
|
1755
|
-
}
|
|
1756
|
-
if (this.options.requiredScopes && this.options.requiredScopes.length > 0) {
|
|
1757
|
-
if (!this.matchesAny(payload["scope"] ?? payload["scp"], this.options.requiredScopes)) {
|
|
1758
|
-
return {
|
|
1759
|
-
ok: false,
|
|
1760
|
-
reason: `missing required scope (need one of ${this.options.requiredScopes.join(", ")})`
|
|
1761
|
-
};
|
|
1762
|
-
}
|
|
1763
|
-
}
|
|
1764
|
-
return { ok: true, payload };
|
|
1765
|
-
}
|
|
1766
|
-
matchesAny(claim, required) {
|
|
1767
|
-
const have = Array.isArray(claim) ? claim.filter((v) => typeof v === "string") : typeof claim === "string" ? claim.split(/\s+/).filter(Boolean) : [];
|
|
1768
|
-
return required.some((r) => have.includes(r));
|
|
1769
|
-
}
|
|
1770
|
-
};
|
|
1771
|
-
|
|
1772
|
-
// libs/adapters/src/skills/security/webhook-replay-guard.ts
|
|
1773
|
-
var import_utils5 = require("@frontmcp/utils");
|
|
1774
|
-
function classifyReplayReason(reason) {
|
|
1775
|
-
if (reason.includes("not finite")) return "invalid_timestamp";
|
|
1776
|
-
if (reason.includes("too short")) return "invalid_nonce";
|
|
1777
|
-
if (reason.includes("outside")) return "outside_window";
|
|
1778
|
-
if (reason.includes("replay")) return "nonce_replay";
|
|
1779
|
-
return "other";
|
|
1780
|
-
}
|
|
1781
|
-
var DEFAULT_WINDOW_MS = 5 * 60 * 1e3;
|
|
1782
|
-
var DEFAULT_CAPACITY = 5e3;
|
|
1783
|
-
var WebhookReplayGuard = class {
|
|
1784
|
-
windowMs;
|
|
1785
|
-
capacity;
|
|
1786
|
-
seen = /* @__PURE__ */ new Map();
|
|
1787
|
-
now = Date.now;
|
|
1788
|
-
rejectsCounter;
|
|
1789
|
-
checksCounter;
|
|
1790
|
-
constructor(options = {}) {
|
|
1791
|
-
const windowMs = options.windowMs ?? DEFAULT_WINDOW_MS;
|
|
1792
|
-
const capacity = options.capacity ?? DEFAULT_CAPACITY;
|
|
1793
|
-
if (!Number.isFinite(windowMs) || windowMs <= 0) {
|
|
1794
|
-
throw new RangeError("WebhookReplayGuard: windowMs must be a finite number > 0");
|
|
1795
|
-
}
|
|
1796
|
-
if (!Number.isInteger(capacity) || capacity <= 0) {
|
|
1797
|
-
throw new RangeError("WebhookReplayGuard: capacity must be a positive integer");
|
|
1798
|
-
}
|
|
1799
|
-
this.windowMs = windowMs;
|
|
1800
|
-
this.capacity = capacity;
|
|
1801
|
-
this.rejectsCounter = options.telemetry?.createCounter(
|
|
1802
|
-
"frontmcp_skills_replay_rejects_total",
|
|
1803
|
-
"Number of webhook replay-guard rejections, partitioned by reason."
|
|
1804
|
-
);
|
|
1805
|
-
this.checksCounter = options.telemetry?.createCounter(
|
|
1806
|
-
"frontmcp_skills_replay_checks_total",
|
|
1807
|
-
"Number of webhook replay-guard checks, partitioned by status (ok|error)."
|
|
1808
|
-
);
|
|
1809
|
-
}
|
|
1810
|
-
/** For tests: override the time source. */
|
|
1811
|
-
setNowProvider(fn) {
|
|
1812
|
-
this.now = fn;
|
|
1813
|
-
}
|
|
1814
|
-
/**
|
|
1815
|
-
* Check that a (timestamp, nonce) pair is fresh and unseen. Returns ok=false
|
|
1816
|
-
* if the timestamp is outside the freshness window or the nonce has already
|
|
1817
|
-
* been observed within that window.
|
|
1818
|
-
*/
|
|
1819
|
-
check(args) {
|
|
1820
|
-
const { timestampMs, nonce } = args;
|
|
1821
|
-
const reject = (reason) => {
|
|
1822
|
-
this.rejectsCounter?.inc(1, { reason: classifyReplayReason(reason) });
|
|
1823
|
-
this.checksCounter?.inc(1, { status: "error" });
|
|
1824
|
-
return { ok: false, reason };
|
|
1825
|
-
};
|
|
1826
|
-
if (!Number.isFinite(timestampMs)) {
|
|
1827
|
-
return reject("timestamp not finite");
|
|
1828
|
-
}
|
|
1829
|
-
if (typeof nonce !== "string" || nonce.length < 8) {
|
|
1830
|
-
return reject("nonce too short");
|
|
1831
|
-
}
|
|
1832
|
-
const now = this.now();
|
|
1833
|
-
if (Math.abs(now - timestampMs) > this.windowMs) {
|
|
1834
|
-
return reject(`timestamp outside ${this.windowMs}ms window`);
|
|
1835
|
-
}
|
|
1836
|
-
const key = (0, import_utils5.sha256Hex)(nonce);
|
|
1837
|
-
const expiresAt = this.seen.get(key);
|
|
1838
|
-
if (expiresAt !== void 0 && expiresAt >= now) {
|
|
1839
|
-
return reject("nonce replay detected within freshness window");
|
|
1840
|
-
}
|
|
1841
|
-
if (this.seen.size >= this.capacity) {
|
|
1842
|
-
this.prune();
|
|
1843
|
-
}
|
|
1844
|
-
if (this.seen.size >= this.capacity) {
|
|
1845
|
-
let victimKey;
|
|
1846
|
-
let victimExpiry = Number.POSITIVE_INFINITY;
|
|
1847
|
-
for (const [candidateKey, candidateExpiry] of this.seen) {
|
|
1848
|
-
if (candidateExpiry < victimExpiry) {
|
|
1849
|
-
victimKey = candidateKey;
|
|
1850
|
-
victimExpiry = candidateExpiry;
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
if (victimKey !== void 0) this.seen.delete(victimKey);
|
|
1854
|
-
}
|
|
1855
|
-
this.seen.set(key, timestampMs + this.windowMs);
|
|
1856
|
-
this.checksCounter?.inc(1, { status: "ok" });
|
|
1857
|
-
return { ok: true };
|
|
1858
|
-
}
|
|
1859
|
-
/** Test/maintenance helper: drop expired entries proactively. */
|
|
1860
|
-
prune() {
|
|
1861
|
-
const now = this.now();
|
|
1862
|
-
let removed = 0;
|
|
1863
|
-
for (const [k, expiresAt] of this.seen) {
|
|
1864
|
-
if (expiresAt < now) {
|
|
1865
|
-
this.seen.delete(k);
|
|
1866
|
-
removed++;
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
return removed;
|
|
1870
|
-
}
|
|
1871
|
-
/** Inspect: number of nonces currently tracked. */
|
|
1872
|
-
get size() {
|
|
1873
|
-
return this.seen.size;
|
|
1874
|
-
}
|
|
1875
|
-
};
|
|
1876
|
-
|
|
1877
|
-
// libs/adapters/src/skills/audit/audit-record.types.ts
|
|
1878
|
-
var SKILL_AUDIT_GENESIS_PREV_HASH = "0".repeat(64);
|
|
1879
|
-
var SKILL_AUDIT_KEYS = {
|
|
1880
|
-
/** Atomic chain head counter — incremented per append. */
|
|
1881
|
-
sequence: "audit:skills:sequence",
|
|
1882
|
-
/** Per-record key. Use `SKILL_AUDIT_KEYS.record(seq)` to build. */
|
|
1883
|
-
record: (sequence) => `audit:skills:records:${sequence}`
|
|
1884
|
-
};
|
|
1885
|
-
|
|
1886
|
-
// libs/adapters/src/skills/audit/audit-chain.ts
|
|
1887
|
-
var import_utils6 = require("@frontmcp/utils");
|
|
1888
|
-
function canonicalizeRecordForSigning(record) {
|
|
1889
|
-
const { signature: _drop, ...rest } = record;
|
|
1890
|
-
void _drop;
|
|
1891
|
-
return canonicalize(rest);
|
|
1892
|
-
}
|
|
1893
|
-
function nextPrevHash(prev) {
|
|
1894
|
-
if (!prev) return SKILL_AUDIT_GENESIS_PREV_HASH;
|
|
1895
|
-
return (0, import_utils6.sha256Hex)(canonicalizeRecordForSigning(prev));
|
|
1896
|
-
}
|
|
1897
|
-
function linkRecord(prev, partial) {
|
|
1898
|
-
return {
|
|
1899
|
-
...partial,
|
|
1900
|
-
prevHash: nextPrevHash(prev)
|
|
1901
|
-
};
|
|
1902
|
-
}
|
|
1903
|
-
function verifyChain(records, trustedKeys, verifier, options = {}) {
|
|
1904
|
-
if (records.length === 0) {
|
|
1905
|
-
return { ok: true, verified: 0 };
|
|
1906
|
-
}
|
|
1907
|
-
let prev = options.previous;
|
|
1908
|
-
for (let i = 0; i < records.length; i++) {
|
|
1909
|
-
const rec = records[i];
|
|
1910
|
-
if (!rec) {
|
|
1911
|
-
return { ok: false, breakAt: i, reason: "record at index is null/undefined" };
|
|
1912
|
-
}
|
|
1913
|
-
if (prev && rec.sequence !== prev.sequence + 1) {
|
|
1914
|
-
return {
|
|
1915
|
-
ok: false,
|
|
1916
|
-
breakAt: rec.sequence,
|
|
1917
|
-
reason: `sequence gap: expected ${prev.sequence + 1}, got ${rec.sequence}`
|
|
1918
|
-
};
|
|
1919
|
-
}
|
|
1920
|
-
const expectedPrev = nextPrevHash(prev);
|
|
1921
|
-
if (rec.prevHash !== expectedPrev) {
|
|
1922
|
-
return {
|
|
1923
|
-
ok: false,
|
|
1924
|
-
breakAt: rec.sequence,
|
|
1925
|
-
reason: `prevHash mismatch at sequence ${rec.sequence}: expected ${expectedPrev.slice(0, 12)}.. got ${rec.prevHash.slice(0, 12)}..`
|
|
1926
|
-
};
|
|
1927
|
-
}
|
|
1928
|
-
const canonicalBytes = new TextEncoder().encode(canonicalizeRecordForSigning(rec));
|
|
1929
|
-
const ok = verifier({
|
|
1930
|
-
alg: rec.signatureAlg,
|
|
1931
|
-
keyId: rec.signatureKeyId,
|
|
1932
|
-
data: canonicalBytes,
|
|
1933
|
-
signatureBase64Url: rec.signature,
|
|
1934
|
-
trustedKeys: [...trustedKeys]
|
|
1935
|
-
});
|
|
1936
|
-
if (!ok) {
|
|
1937
|
-
return {
|
|
1938
|
-
ok: false,
|
|
1939
|
-
breakAt: rec.sequence,
|
|
1940
|
-
reason: `signature verification failed at sequence ${rec.sequence} (keyId="${rec.signatureKeyId}", alg=${rec.signatureAlg})`
|
|
1941
|
-
};
|
|
1942
|
-
}
|
|
1943
|
-
prev = rec;
|
|
1944
|
-
}
|
|
1945
|
-
return { ok: true, verified: records.length };
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
// libs/adapters/src/skills/audit/audit-signer.ts
|
|
1949
|
-
var import_utils7 = require("@frontmcp/utils");
|
|
1950
|
-
function deriveFromKeyMaterial(ikm, info, length) {
|
|
1951
|
-
return (0, import_utils7.hkdfSha256)(ikm, new Uint8Array(0), new TextEncoder().encode(info), length);
|
|
1952
|
-
}
|
|
1953
|
-
var Hs256AuditSigner = class {
|
|
1954
|
-
key;
|
|
1955
|
-
keyId;
|
|
1956
|
-
/**
|
|
1957
|
-
* @param secret - HMAC secret. Strings are UTF-8 encoded; pass a
|
|
1958
|
-
* Uint8Array directly when you've derived the key (e.g.
|
|
1959
|
-
* from HKDF) and want to avoid the extra encode step.
|
|
1960
|
-
* @param keyId - Stable identifier (e.g. `'audit-dev-key'`) — surfaces in
|
|
1961
|
-
* the signed record's `signatureKeyId` field.
|
|
1962
|
-
*/
|
|
1963
|
-
constructor(secret, keyId) {
|
|
1964
|
-
if (!keyId) {
|
|
1965
|
-
throw new Error("Hs256AuditSigner: keyId must be a non-empty string");
|
|
1966
|
-
}
|
|
1967
|
-
if (typeof secret === "string") {
|
|
1968
|
-
if (secret.length === 0) throw new Error("Hs256AuditSigner: secret must be non-empty");
|
|
1969
|
-
this.key = new TextEncoder().encode(secret);
|
|
1970
|
-
} else {
|
|
1971
|
-
if (secret.length === 0) throw new Error("Hs256AuditSigner: secret must be non-empty");
|
|
1972
|
-
this.key = new Uint8Array(secret);
|
|
1973
|
-
}
|
|
1974
|
-
this.keyId = keyId;
|
|
1975
|
-
}
|
|
1976
|
-
sign(record) {
|
|
1977
|
-
const canonicalBytes = new TextEncoder().encode(
|
|
1978
|
-
canonicalizeRecordForSigning({
|
|
1979
|
-
...record,
|
|
1980
|
-
// Stub the signature fields to a deterministic shape so canonicalize
|
|
1981
|
-
// sees the exact same byte layout the verifier will reconstruct.
|
|
1982
|
-
signature: "",
|
|
1983
|
-
signatureKeyId: this.keyId,
|
|
1984
|
-
signatureAlg: "HS256"
|
|
1985
|
-
})
|
|
1986
|
-
);
|
|
1987
|
-
const mac = (0, import_utils7.hmacSha256)(this.key, canonicalBytes);
|
|
1988
|
-
return {
|
|
1989
|
-
signature: (0, import_utils7.base64urlEncode)(mac),
|
|
1990
|
-
keyId: this.keyId,
|
|
1991
|
-
alg: "HS256"
|
|
1992
|
-
};
|
|
1993
|
-
}
|
|
1994
|
-
getKeyId() {
|
|
1995
|
-
return this.keyId;
|
|
1996
|
-
}
|
|
1997
|
-
getAlg() {
|
|
1998
|
-
return "HS256";
|
|
1999
|
-
}
|
|
2000
|
-
deriveKey(info, length) {
|
|
2001
|
-
return deriveFromKeyMaterial(this.key, info, length);
|
|
2002
|
-
}
|
|
2003
|
-
};
|
|
2004
|
-
var Rs256AuditSigner = class {
|
|
2005
|
-
privateJwk;
|
|
2006
|
-
privateExponent;
|
|
2007
|
-
keyId;
|
|
2008
|
-
constructor(privateJwk, keyId) {
|
|
2009
|
-
if (!keyId) {
|
|
2010
|
-
throw new Error("Rs256AuditSigner: keyId must be a non-empty string");
|
|
2011
|
-
}
|
|
2012
|
-
if (!privateJwk || typeof privateJwk !== "object") {
|
|
2013
|
-
throw new Error("Rs256AuditSigner: privateJwk must be a JWK object");
|
|
2014
|
-
}
|
|
2015
|
-
if (privateJwk.kty !== "RSA") {
|
|
2016
|
-
throw new Error(`Rs256AuditSigner: privateJwk.kty must be "RSA", got "${String(privateJwk.kty)}"`);
|
|
2017
|
-
}
|
|
2018
|
-
if (!privateJwk.n || !privateJwk.e || !privateJwk.d) {
|
|
2019
|
-
throw new Error("Rs256AuditSigner: privateJwk must include `n`, `e`, and `d` (RSA private key)");
|
|
2020
|
-
}
|
|
2021
|
-
this.privateJwk = { ...privateJwk };
|
|
2022
|
-
this.privateExponent = privateJwk.d;
|
|
2023
|
-
this.keyId = keyId;
|
|
2024
|
-
}
|
|
2025
|
-
sign(record) {
|
|
2026
|
-
const canonicalBytes = new TextEncoder().encode(
|
|
2027
|
-
canonicalizeRecordForSigning({
|
|
2028
|
-
...record,
|
|
2029
|
-
signature: "",
|
|
2030
|
-
signatureKeyId: this.keyId,
|
|
2031
|
-
signatureAlg: "RS256"
|
|
2032
|
-
})
|
|
2033
|
-
);
|
|
2034
|
-
const signature = (0, import_utils7.rsaSignBase64Url)("RS256", canonicalBytes, this.privateJwk);
|
|
2035
|
-
return {
|
|
2036
|
-
signature,
|
|
2037
|
-
keyId: this.keyId,
|
|
2038
|
-
alg: "RS256"
|
|
2039
|
-
};
|
|
2040
|
-
}
|
|
2041
|
-
getKeyId() {
|
|
2042
|
-
return this.keyId;
|
|
2043
|
-
}
|
|
2044
|
-
getAlg() {
|
|
2045
|
-
return "RS256";
|
|
2046
|
-
}
|
|
2047
|
-
/** Derived from the private exponent `d`, which only the holder of the private key has. */
|
|
2048
|
-
deriveKey(info, length) {
|
|
2049
|
-
return deriveFromKeyMaterial((0, import_utils7.base64urlDecode)(this.privateExponent), info, length);
|
|
2050
|
-
}
|
|
2051
|
-
};
|
|
2052
|
-
var defaultAuditSignatureVerifier = ({
|
|
2053
|
-
alg,
|
|
2054
|
-
keyId,
|
|
2055
|
-
data,
|
|
2056
|
-
signatureBase64Url,
|
|
2057
|
-
trustedKeys
|
|
2058
|
-
}) => {
|
|
2059
|
-
const trusted = trustedKeys.find((k) => k.keyId === keyId && k.alg === alg);
|
|
2060
|
-
if (!trusted) return false;
|
|
2061
|
-
if (alg === "HS256") {
|
|
2062
|
-
if (!trusted.secret) return false;
|
|
2063
|
-
const expected = (0, import_utils7.hmacSha256)(trusted.secret, data);
|
|
2064
|
-
let actual;
|
|
2065
|
-
try {
|
|
2066
|
-
actual = (0, import_utils7.base64urlDecode)(signatureBase64Url);
|
|
2067
|
-
} catch {
|
|
2068
|
-
return false;
|
|
2069
|
-
}
|
|
2070
|
-
if (actual.length !== expected.length) return false;
|
|
2071
|
-
return (0, import_utils7.timingSafeEqual)(actual, expected);
|
|
2072
|
-
}
|
|
2073
|
-
if (alg === "RS256") {
|
|
2074
|
-
if (!trusted.publicKeyPem && !trusted.publicJwk) return false;
|
|
2075
|
-
let publicJwk;
|
|
2076
|
-
try {
|
|
2077
|
-
publicJwk = trusted.publicJwk ?? (0, import_utils7.pemToPublicJwk)(trusted.publicKeyPem);
|
|
2078
|
-
} catch {
|
|
2079
|
-
return false;
|
|
2080
|
-
}
|
|
2081
|
-
let sigBytes;
|
|
2082
|
-
try {
|
|
2083
|
-
sigBytes = (0, import_utils7.base64urlDecode)(signatureBase64Url);
|
|
2084
|
-
} catch {
|
|
2085
|
-
return false;
|
|
2086
|
-
}
|
|
2087
|
-
return (0, import_utils7.rsaVerifySync)("RS256", data, publicJwk, sigBytes);
|
|
2088
|
-
}
|
|
2089
|
-
const _exhaustive = alg;
|
|
2090
|
-
void _exhaustive;
|
|
2091
|
-
return false;
|
|
2092
|
-
};
|
|
2093
|
-
|
|
2094
|
-
// libs/adapters/src/skills/audit/audit-store.ts
|
|
2095
|
-
var MemoryAuditStore = class {
|
|
2096
|
-
records = [];
|
|
2097
|
-
seq = 0;
|
|
2098
|
-
async nextSequence() {
|
|
2099
|
-
this.seq += 1;
|
|
2100
|
-
return this.seq;
|
|
2101
|
-
}
|
|
2102
|
-
async appendAtSequence(record) {
|
|
2103
|
-
if (this.records.some((r) => r.sequence === record.sequence)) {
|
|
2104
|
-
throw new Error(`MemoryAuditStore: record at sequence ${record.sequence} already exists`);
|
|
2105
|
-
}
|
|
2106
|
-
this.records.push(record);
|
|
2107
|
-
}
|
|
2108
|
-
async tail() {
|
|
2109
|
-
if (this.records.length === 0) return void 0;
|
|
2110
|
-
return this.records[this.records.length - 1];
|
|
2111
|
-
}
|
|
2112
|
-
async releaseSequence(sequence) {
|
|
2113
|
-
if (sequence === this.seq && !this.records.some((record) => record.sequence === sequence)) this.seq -= 1;
|
|
2114
|
-
}
|
|
2115
|
-
async read(opts) {
|
|
2116
|
-
const from = opts?.from ?? 1;
|
|
2117
|
-
const limit = opts?.limit;
|
|
2118
|
-
const filtered = this.records.filter((r) => r.sequence >= from).sort((a, b) => a.sequence - b.sequence);
|
|
2119
|
-
return limit !== void 0 ? filtered.slice(0, limit) : filtered;
|
|
2120
|
-
}
|
|
2121
|
-
};
|
|
2122
|
-
var StorageAdapterAuditStore = class {
|
|
2123
|
-
constructor(adapter, options = {}) {
|
|
2124
|
-
this.adapter = adapter;
|
|
2125
|
-
this.options = options;
|
|
2126
|
-
}
|
|
2127
|
-
adapter;
|
|
2128
|
-
options;
|
|
2129
|
-
get sequenceKey() {
|
|
2130
|
-
return this.options.sequenceKey ?? SKILL_AUDIT_KEYS.sequence;
|
|
2131
|
-
}
|
|
2132
|
-
recordKey(sequence) {
|
|
2133
|
-
const prefix = this.options.recordKeyPrefix;
|
|
2134
|
-
return prefix ? `${prefix}${sequence}` : SKILL_AUDIT_KEYS.record(sequence);
|
|
2135
|
-
}
|
|
2136
|
-
async nextSequence() {
|
|
2137
|
-
return this.adapter.incr(this.sequenceKey);
|
|
2138
|
-
}
|
|
2139
|
-
/** DECR's result tells atomically whether the counter was still `sequence`; when it was not, INCR restores it. */
|
|
2140
|
-
async releaseSequence(sequence) {
|
|
2141
|
-
if (await this.adapter.get(this.recordKey(sequence)) !== null) return;
|
|
2142
|
-
if (await this.adapter.decr(this.sequenceKey) !== sequence - 1) await this.adapter.incr(this.sequenceKey);
|
|
2143
|
-
}
|
|
2144
|
-
async appendAtSequence(record) {
|
|
2145
|
-
const key = this.recordKey(record.sequence);
|
|
2146
|
-
const existing = await this.adapter.get(key);
|
|
2147
|
-
if (existing !== null) {
|
|
2148
|
-
throw new Error(`StorageAdapterAuditStore: record at sequence ${record.sequence} already exists`);
|
|
2149
|
-
}
|
|
2150
|
-
const payload = JSON.stringify(record);
|
|
2151
|
-
await this.adapter.set(key, payload, { ifNotExists: true });
|
|
2152
|
-
const stored = await this.adapter.get(key);
|
|
2153
|
-
if (stored !== payload) {
|
|
2154
|
-
throw new Error(`StorageAdapterAuditStore: record at sequence ${record.sequence} was clobbered or not stored`);
|
|
2155
|
-
}
|
|
2156
|
-
}
|
|
2157
|
-
async tail() {
|
|
2158
|
-
const seqRaw = await this.adapter.get(this.sequenceKey);
|
|
2159
|
-
if (!seqRaw) return void 0;
|
|
2160
|
-
const seq = Number.parseInt(seqRaw, 10);
|
|
2161
|
-
if (!Number.isFinite(seq) || seq <= 0) return void 0;
|
|
2162
|
-
const maxLookback = this.options.tailLookback ?? 1024;
|
|
2163
|
-
for (let s = seq; s > 0 && s > seq - maxLookback; s--) {
|
|
2164
|
-
const raw = await this.adapter.get(this.recordKey(s));
|
|
2165
|
-
if (!raw) continue;
|
|
2166
|
-
try {
|
|
2167
|
-
return JSON.parse(raw);
|
|
2168
|
-
} catch {
|
|
2169
|
-
throw new Error(`StorageAdapterAuditStore.tail: corrupt record at sequence ${s} cannot be parsed as JSON`);
|
|
2170
|
-
}
|
|
2171
|
-
}
|
|
2172
|
-
return void 0;
|
|
2173
|
-
}
|
|
2174
|
-
async read(opts) {
|
|
2175
|
-
const seqRaw = await this.adapter.get(this.sequenceKey);
|
|
2176
|
-
if (!seqRaw) return [];
|
|
2177
|
-
const head = Number.parseInt(seqRaw, 10);
|
|
2178
|
-
if (!Number.isFinite(head) || head <= 0) return [];
|
|
2179
|
-
const from = Math.max(1, opts?.from ?? 1);
|
|
2180
|
-
const limit = opts?.limit;
|
|
2181
|
-
const out = [];
|
|
2182
|
-
for (let s = from; s <= head; s++) {
|
|
2183
|
-
const raw = await this.adapter.get(this.recordKey(s));
|
|
2184
|
-
if (!raw) continue;
|
|
2185
|
-
try {
|
|
2186
|
-
const parsed = JSON.parse(raw);
|
|
2187
|
-
out.push(parsed);
|
|
2188
|
-
if (limit !== void 0 && out.length >= limit) break;
|
|
2189
|
-
} catch {
|
|
2190
|
-
}
|
|
2191
|
-
}
|
|
2192
|
-
return out;
|
|
2193
|
-
}
|
|
2194
|
-
};
|
|
2195
|
-
|
|
2196
|
-
// libs/adapters/src/skills/audit/audit-writer.ts
|
|
2197
|
-
var import_utils8 = require("@frontmcp/utils");
|
|
2198
|
-
function createSkillAuditMetrics(telemetry) {
|
|
2199
|
-
const writeFailures = telemetry.createCounter(
|
|
2200
|
-
"frontmcp_skills_audit_write_failures_total",
|
|
2201
|
-
"Skill audit records that failed to sign or append, partitioned by reason."
|
|
2202
|
-
);
|
|
2203
|
-
const dropped = telemetry.createCounter(
|
|
2204
|
-
"frontmcp_skills_audit_dropped_total",
|
|
2205
|
-
"Skill audit records dropped before they were written, partitioned by reason."
|
|
2206
|
-
);
|
|
2207
|
-
return {
|
|
2208
|
-
incrementWriteFailure: (reason) => writeFailures.inc(1, { reason }),
|
|
2209
|
-
incrementDropped: (reason) => dropped.inc(1, { reason })
|
|
2210
|
-
};
|
|
2211
|
-
}
|
|
2212
|
-
var SKILL_AUDIT_ERROR_MESSAGE_MAX = 500;
|
|
2213
|
-
var SKILL_AUDIT_QUEUE_MAX = 1e3;
|
|
2214
|
-
var SUBJECT_HASH_KEY_INFO = "frontmcp:audit:subject";
|
|
2215
|
-
var SUBJECT_HASH_KEY_LENGTH = 32;
|
|
2216
|
-
var SkillAuditWriter = class {
|
|
2217
|
-
constructor(store, signer, logger, metrics, options = {}) {
|
|
2218
|
-
this.store = store;
|
|
2219
|
-
this.signer = signer;
|
|
2220
|
-
this.logger = logger;
|
|
2221
|
-
this.metrics = metrics;
|
|
2222
|
-
this.subjectMode = options.subjectMode ?? "hash";
|
|
2223
|
-
if (this.subjectMode === "hash") {
|
|
2224
|
-
this.subjectHashKey = options.subjectHashSecret ?? signer.deriveKey?.(SUBJECT_HASH_KEY_INFO, SUBJECT_HASH_KEY_LENGTH);
|
|
2225
|
-
if (!this.subjectHashKey) {
|
|
2226
|
-
logger.warn(
|
|
2227
|
-
`[skill-audit] subjectMode 'hash' needs a key, and the signer (keyId "${signer.getKeyId()}") has no key material to derive one from, so subjects are recorded as 'redacted'. Set skillsConfig.audit.subjectHashSecret to record subject hashes.`
|
|
2228
|
-
);
|
|
2229
|
-
}
|
|
2230
|
-
}
|
|
2231
|
-
this.maxQueueDepth = options.maxQueueDepth ?? SKILL_AUDIT_QUEUE_MAX;
|
|
2232
|
-
}
|
|
2233
|
-
store;
|
|
2234
|
-
signer;
|
|
2235
|
-
logger;
|
|
2236
|
-
metrics;
|
|
2237
|
-
/**
|
|
2238
|
-
* Single-flight queue: every append() chains onto this promise so the
|
|
2239
|
-
* read-tail-then-append-with-prevHash sequence stays atomic per process.
|
|
2240
|
-
* This is the mate to the storage adapter's `incr` for cross-pod safety.
|
|
2241
|
-
*/
|
|
2242
|
-
chainHeadLock = Promise.resolve();
|
|
2243
|
-
/** Current in-flight queue depth (records awaiting the chain head lock). */
|
|
2244
|
-
queueDepth = 0;
|
|
2245
|
-
subjectMode;
|
|
2246
|
-
subjectHashKey;
|
|
2247
|
-
maxQueueDepth;
|
|
2248
|
-
/**
|
|
2249
|
-
* Phase 2 in the tool: authority check just passed. We log the inputHash
|
|
2250
|
-
* here so even invocations that fail at the network layer still leave a
|
|
2251
|
-
* forensic breadcrumb tying the input to the policy decision.
|
|
2252
|
-
*/
|
|
2253
|
-
writeAuthorityPass(ctx) {
|
|
2254
|
-
return this.write({
|
|
2255
|
-
phase: "authority-check-pass",
|
|
2256
|
-
ctx
|
|
2257
|
-
});
|
|
2258
|
-
}
|
|
2259
|
-
/**
|
|
2260
|
-
* Phase 2 in the tool: authority check denied. Writing this record is
|
|
2261
|
-
* security-relevant — without it, denied invocations would not appear in
|
|
2262
|
-
* the audit log at all, and the most security-relevant events (someone
|
|
2263
|
-
* trying to invoke an action they're not authorized for) would silently
|
|
2264
|
-
* disappear. The tool MUST call this before returning the denial envelope.
|
|
2265
|
-
*/
|
|
2266
|
-
writeAuthorityFail(ctx, extras) {
|
|
2267
|
-
return this.write({
|
|
2268
|
-
phase: "authority-check-fail",
|
|
2269
|
-
ctx,
|
|
2270
|
-
// Re-use the errorMessage field for the denial reason so the on-disk
|
|
2271
|
-
// shape stays identical to other failure phases.
|
|
2272
|
-
error: extras.reason
|
|
2273
|
-
});
|
|
2274
|
-
}
|
|
2275
|
-
/** Phase 4 in the tool, success path. */
|
|
2276
|
-
writeHttpCallSuccess(ctx, extras) {
|
|
2277
|
-
return this.write({
|
|
2278
|
-
phase: "http-call-success",
|
|
2279
|
-
ctx,
|
|
2280
|
-
status: extras.status,
|
|
2281
|
-
output: extras.output
|
|
2282
|
-
});
|
|
2283
|
-
}
|
|
2284
|
-
/** Phase 4 in the tool, failure path. */
|
|
2285
|
-
writeHttpCallFailure(ctx, extras) {
|
|
2286
|
-
return this.write({
|
|
2287
|
-
phase: "http-call-failure",
|
|
2288
|
-
ctx,
|
|
2289
|
-
status: extras.status,
|
|
2290
|
-
error: extras.error
|
|
2291
|
-
});
|
|
2292
|
-
}
|
|
2293
|
-
/**
|
|
2294
|
-
* Wait for all pending writes to complete. Useful for tests and graceful
|
|
2295
|
-
* shutdown — the chain head lock resolves once every queued write has
|
|
2296
|
-
* finished (success or swallowed-failure). Never throws.
|
|
2297
|
-
*/
|
|
2298
|
-
async flush() {
|
|
2299
|
-
while (this.queueDepth > 0) {
|
|
2300
|
-
await this.chainHeadLock;
|
|
2301
|
-
}
|
|
2302
|
-
}
|
|
2303
|
-
// ─── Internal ──────────────────────────────────────────────────────────
|
|
2304
|
-
write(args) {
|
|
2305
|
-
if (this.queueDepth >= this.maxQueueDepth) {
|
|
2306
|
-
this.logger.warn(
|
|
2307
|
-
`[skill-audit] queue overflow (${this.queueDepth}/${this.maxQueueDepth}); dropping ${args.phase} record. Audit backend likely unhealthy.`
|
|
2308
|
-
);
|
|
2309
|
-
this.metrics?.incrementDropped?.("queue-overflow");
|
|
2310
|
-
return Promise.resolve();
|
|
2311
|
-
}
|
|
2312
|
-
this.queueDepth += 1;
|
|
2313
|
-
const next = this.chainHeadLock.then(() => this.doWrite(args));
|
|
2314
|
-
this.chainHeadLock = next.catch(() => void 0).finally(() => {
|
|
2315
|
-
this.queueDepth -= 1;
|
|
2316
|
-
});
|
|
2317
|
-
return next.catch(() => void 0);
|
|
2318
|
-
}
|
|
2319
|
-
async doWrite(args) {
|
|
2320
|
-
const { phase, ctx, status, output, error } = args;
|
|
2321
|
-
let prev;
|
|
2322
|
-
try {
|
|
2323
|
-
prev = await this.store.tail();
|
|
2324
|
-
} catch (e) {
|
|
2325
|
-
this.logger.warn(`[skill-audit] failed to load chain tail: ${e.message}`);
|
|
2326
|
-
this.metrics?.incrementWriteFailure("append");
|
|
2327
|
-
return;
|
|
2328
|
-
}
|
|
2329
|
-
let inputHash;
|
|
2330
|
-
try {
|
|
2331
|
-
inputHash = (0, import_utils8.sha256Hex)(canonicalize(ctx.input ?? null));
|
|
2332
|
-
} catch (e) {
|
|
2333
|
-
this.logger.warn(`[skill-audit] failed to canonicalize input: ${e.message}`);
|
|
2334
|
-
this.metrics?.incrementWriteFailure("unexpected");
|
|
2335
|
-
return;
|
|
2336
|
-
}
|
|
2337
|
-
let sequence;
|
|
2338
|
-
try {
|
|
2339
|
-
sequence = await this.store.nextSequence();
|
|
2340
|
-
} catch (e) {
|
|
2341
|
-
this.logger.warn(`[skill-audit] failed to allocate sequence: ${e.message}`);
|
|
2342
|
-
this.metrics?.incrementWriteFailure("append");
|
|
2343
|
-
return;
|
|
2344
|
-
}
|
|
2345
|
-
const partial = {
|
|
2346
|
-
id: (0, import_utils8.randomUUID)(),
|
|
2347
|
-
sequence,
|
|
2348
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2349
|
-
subject: this.applySubjectMode(ctx.subject || "anonymous"),
|
|
2350
|
-
skillId: ctx.skillId,
|
|
2351
|
-
actionId: ctx.actionId,
|
|
2352
|
-
bundleId: ctx.bundleId,
|
|
2353
|
-
bundleVersion: ctx.bundleVersion,
|
|
2354
|
-
phase,
|
|
2355
|
-
inputHash,
|
|
2356
|
-
...status !== void 0 ? { status } : {},
|
|
2357
|
-
...phase === "http-call-success" ? { outputHash: this.safeHash(output) } : {},
|
|
2358
|
-
...phase === "http-call-failure" || phase === "authority-check-fail" ? { errorMessage: this.formatError(error) } : {}
|
|
2359
|
-
};
|
|
2360
|
-
const linked = linkRecord(prev, partial);
|
|
2361
|
-
let signed;
|
|
2362
|
-
try {
|
|
2363
|
-
const sig = this.signer.sign(linked);
|
|
2364
|
-
signed = {
|
|
2365
|
-
...linked,
|
|
2366
|
-
signature: sig.signature,
|
|
2367
|
-
signatureKeyId: sig.keyId,
|
|
2368
|
-
signatureAlg: sig.alg
|
|
2369
|
-
};
|
|
2370
|
-
} catch (e) {
|
|
2371
|
-
this.logger.warn(`[skill-audit] failed to sign record at seq=${sequence}: ${e.message}`);
|
|
2372
|
-
this.metrics?.incrementWriteFailure("sign");
|
|
2373
|
-
await this.releaseUnusedSequence(sequence);
|
|
2374
|
-
return;
|
|
2375
|
-
}
|
|
2376
|
-
try {
|
|
2377
|
-
await this.store.appendAtSequence(signed);
|
|
2378
|
-
this.logger.debug?.(`[skill-audit] appended seq=${sequence} phase=${phase}`);
|
|
2379
|
-
} catch (e) {
|
|
2380
|
-
this.logger.warn(`[skill-audit] failed to append record at seq=${sequence}: ${e.message}`);
|
|
2381
|
-
this.metrics?.incrementWriteFailure("append");
|
|
2382
|
-
await this.releaseUnusedSequence(sequence);
|
|
2383
|
-
}
|
|
2384
|
-
}
|
|
2385
|
-
/** Best-effort {@link SkillAuditStore.releaseSequence}; never throws, so a failed rollback leaves the gap. */
|
|
2386
|
-
async releaseUnusedSequence(allocated) {
|
|
2387
|
-
if (!this.store.releaseSequence) return;
|
|
2388
|
-
try {
|
|
2389
|
-
await this.store.releaseSequence(allocated);
|
|
2390
|
-
this.logger.debug?.(`[skill-audit] rolled back unused sequence ${allocated}`);
|
|
2391
|
-
} catch {
|
|
2392
|
-
}
|
|
2393
|
-
}
|
|
2394
|
-
/** Apply the configured subject-mode redaction. */
|
|
2395
|
-
applySubjectMode(subject) {
|
|
2396
|
-
if (this.subjectMode === "plain") return subject;
|
|
2397
|
-
if (this.subjectMode === "omit" || !this.subjectHashKey) return "redacted";
|
|
2398
|
-
const mac = (0, import_utils8.hmacSha256)(this.subjectHashKey, new TextEncoder().encode(subject));
|
|
2399
|
-
const hex = (0, import_utils8.bytesToHex)(mac);
|
|
2400
|
-
return `hashed:${hex.slice(0, 32)}`;
|
|
2401
|
-
}
|
|
2402
|
-
/**
|
|
2403
|
-
* Hash arbitrary output. Falls back to a sentinel if canonicalization
|
|
2404
|
-
* fails (circular references, non-serializable values) — the verifier
|
|
2405
|
-
* will treat it as a hash like any other.
|
|
2406
|
-
*/
|
|
2407
|
-
safeHash(value) {
|
|
2408
|
-
try {
|
|
2409
|
-
return (0, import_utils8.sha256Hex)(canonicalize(value ?? null));
|
|
2410
|
-
} catch {
|
|
2411
|
-
return (0, import_utils8.sha256Hex)("audit:non-canonicalizable-output");
|
|
2412
|
-
}
|
|
2413
|
-
}
|
|
2414
|
-
/** Truncate + sanitize an error to fit in the record. */
|
|
2415
|
-
formatError(error) {
|
|
2416
|
-
let raw;
|
|
2417
|
-
if (error instanceof Error) {
|
|
2418
|
-
raw = error.message;
|
|
2419
|
-
} else if (typeof error === "string") {
|
|
2420
|
-
raw = error;
|
|
2421
|
-
} else if (error === void 0 || error === null) {
|
|
2422
|
-
raw = "";
|
|
2423
|
-
} else {
|
|
2424
|
-
try {
|
|
2425
|
-
raw = JSON.stringify(error);
|
|
2426
|
-
} catch {
|
|
2427
|
-
raw = String(error);
|
|
2428
|
-
}
|
|
2429
|
-
}
|
|
2430
|
-
if (raw.length > SKILL_AUDIT_ERROR_MESSAGE_MAX) {
|
|
2431
|
-
return raw.slice(0, SKILL_AUDIT_ERROR_MESSAGE_MAX) + "...";
|
|
2432
|
-
}
|
|
2433
|
-
return raw;
|
|
2434
|
-
}
|
|
2435
|
-
};
|
|
2436
|
-
|
|
2437
|
-
// libs/adapters/src/skills/audit/audit-config.ts
|
|
2438
|
-
var SkillAuditWriterToken = /* @__PURE__ */ Symbol.for(
|
|
2439
|
-
"frontmcp:SKILL_AUDIT_WRITER"
|
|
2440
|
-
);
|
|
2441
|
-
|
|
2442
|
-
// libs/adapters/src/skills/index.ts
|
|
2443
|
-
var import_sdk = require("@frontmcp/sdk");
|
|
2444
|
-
|
|
2445
|
-
// libs/adapters/src/skills/harvester/op-reference.ts
|
|
2446
|
-
var SPEC_ID_RE = "[A-Za-z0-9][A-Za-z0-9_.-]*";
|
|
2447
|
-
var OPERATION_ID_RE = "[A-Za-z_][A-Za-z0-9_]*";
|
|
2448
|
-
var TRAILING_BOUNDARY = String.raw`(?![A-Za-z0-9_\-])(?!\.[A-Za-z0-9_])`;
|
|
2449
|
-
var REFERENCE_RE = new RegExp(
|
|
2450
|
-
String.raw`(?<![A-Za-z0-9])op:\/\/(${SPEC_ID_RE})\/(${OPERATION_ID_RE})${TRAILING_BOUNDARY}` + `|` + String.raw`\[\[op:\s?(${SPEC_ID_RE})\/(${OPERATION_ID_RE})\]\]`,
|
|
2451
|
-
"g"
|
|
2452
|
-
);
|
|
2453
|
-
function extractOpReferences(markdown) {
|
|
2454
|
-
if (typeof markdown !== "string" || markdown.length === 0) return [];
|
|
2455
|
-
const refs = [];
|
|
2456
|
-
const lineStarts = computeLineStarts(markdown);
|
|
2457
|
-
REFERENCE_RE.lastIndex = 0;
|
|
2458
|
-
let match;
|
|
2459
|
-
while ((match = REFERENCE_RE.exec(markdown)) !== null) {
|
|
2460
|
-
const offset = match.index;
|
|
2461
|
-
const [raw, uriSpec, uriOp, wikiSpec, wikiOp] = match;
|
|
2462
|
-
const isWikilink = wikiSpec !== void 0 && wikiOp !== void 0;
|
|
2463
|
-
const spec = isWikilink ? wikiSpec : uriSpec;
|
|
2464
|
-
const operationId = isWikilink ? wikiOp : uriOp;
|
|
2465
|
-
refs.push({
|
|
2466
|
-
spec,
|
|
2467
|
-
operationId,
|
|
2468
|
-
syntax: isWikilink ? "wikilink" : "uri",
|
|
2469
|
-
raw,
|
|
2470
|
-
location: offsetToLocation(offset, lineStarts)
|
|
2471
|
-
});
|
|
2472
|
-
}
|
|
2473
|
-
return refs;
|
|
2474
|
-
}
|
|
2475
|
-
function buildKnownOps(entries) {
|
|
2476
|
-
const map = /* @__PURE__ */ new Map();
|
|
2477
|
-
for (const entry of entries) {
|
|
2478
|
-
const slash = entry.indexOf("/");
|
|
2479
|
-
if (slash <= 0 || slash === entry.length - 1) continue;
|
|
2480
|
-
const spec = entry.slice(0, slash);
|
|
2481
|
-
const op = entry.slice(slash + 1);
|
|
2482
|
-
let set = map.get(spec);
|
|
2483
|
-
if (!set) {
|
|
2484
|
-
set = /* @__PURE__ */ new Set();
|
|
2485
|
-
map.set(spec, set);
|
|
2486
|
-
}
|
|
2487
|
-
set.add(op);
|
|
2488
|
-
}
|
|
2489
|
-
return map;
|
|
2490
|
-
}
|
|
2491
|
-
function validateOpReferences(refs, knownOps) {
|
|
2492
|
-
const diagnostics = [];
|
|
2493
|
-
for (const ref of refs) {
|
|
2494
|
-
const specOps = knownOps.get(ref.spec);
|
|
2495
|
-
if (!specOps) {
|
|
2496
|
-
diagnostics.push({
|
|
2497
|
-
ref,
|
|
2498
|
-
kind: "unknown-spec",
|
|
2499
|
-
message: `Unknown OpenAPI spec "${ref.spec}" referenced as ${ref.raw}`,
|
|
2500
|
-
suggestions: Array.from(knownOps.keys()).sort()
|
|
2501
|
-
});
|
|
2502
|
-
continue;
|
|
2503
|
-
}
|
|
2504
|
-
if (!specOps.has(ref.operationId)) {
|
|
2505
|
-
diagnostics.push({
|
|
2506
|
-
ref,
|
|
2507
|
-
kind: "unknown-operation",
|
|
2508
|
-
message: `Unknown operationId "${ref.operationId}" in spec "${ref.spec}" (${ref.raw})`,
|
|
2509
|
-
suggestions: suggestSimilarOps(ref.operationId, specOps)
|
|
2510
|
-
});
|
|
2511
|
-
}
|
|
2512
|
-
}
|
|
2513
|
-
return diagnostics;
|
|
2514
|
-
}
|
|
2515
|
-
function dedupeOpReferences(refs) {
|
|
2516
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2517
|
-
const result = [];
|
|
2518
|
-
for (const ref of refs) {
|
|
2519
|
-
const key = `${ref.spec}/${ref.operationId}`;
|
|
2520
|
-
if (seen.has(key)) continue;
|
|
2521
|
-
seen.add(key);
|
|
2522
|
-
result.push({ spec: ref.spec, operationId: ref.operationId });
|
|
2523
|
-
}
|
|
2524
|
-
return result;
|
|
2525
|
-
}
|
|
2526
|
-
function computeLineStarts(source) {
|
|
2527
|
-
const starts = [0];
|
|
2528
|
-
for (let i = 0; i < source.length; i++) {
|
|
2529
|
-
if (source.charCodeAt(i) === 10) starts.push(i + 1);
|
|
2530
|
-
}
|
|
2531
|
-
return starts;
|
|
2532
|
-
}
|
|
2533
|
-
function offsetToLocation(offset, lineStarts) {
|
|
2534
|
-
let lo = 0;
|
|
2535
|
-
let hi = lineStarts.length - 1;
|
|
2536
|
-
while (lo < hi) {
|
|
2537
|
-
const mid = lo + hi + 1 >>> 1;
|
|
2538
|
-
if (lineStarts[mid] <= offset) lo = mid;
|
|
2539
|
-
else hi = mid - 1;
|
|
2540
|
-
}
|
|
2541
|
-
const line = lo + 1;
|
|
2542
|
-
const column = offset - lineStarts[lo] + 1;
|
|
2543
|
-
return { line, column, offset };
|
|
2544
|
-
}
|
|
2545
|
-
function suggestSimilarOps(query, known) {
|
|
2546
|
-
if (known.size === 0) return [];
|
|
2547
|
-
const q = query.toLowerCase();
|
|
2548
|
-
const candidates = Array.from(known);
|
|
2549
|
-
const prefix = candidates.filter((c) => c.toLowerCase().startsWith(q));
|
|
2550
|
-
const substring = candidates.filter((c) => !prefix.includes(c) && c.toLowerCase().includes(q));
|
|
2551
|
-
const close = candidates.filter((c) => !prefix.includes(c) && !substring.includes(c)).filter((c) => levenshtein(q, c.toLowerCase()) <= Math.max(2, Math.floor(q.length / 4)));
|
|
2552
|
-
return [...prefix, ...substring, ...close].slice(0, 3);
|
|
2553
|
-
}
|
|
2554
|
-
function levenshtein(a, b) {
|
|
2555
|
-
if (a === b) return 0;
|
|
2556
|
-
if (a.length === 0) return b.length;
|
|
2557
|
-
if (b.length === 0) return a.length;
|
|
2558
|
-
let prev = new Array(b.length + 1);
|
|
2559
|
-
let curr = new Array(b.length + 1);
|
|
2560
|
-
for (let j = 0; j <= b.length; j++) prev[j] = j;
|
|
2561
|
-
for (let i = 1; i <= a.length; i++) {
|
|
2562
|
-
curr[0] = i;
|
|
2563
|
-
for (let j = 1; j <= b.length; j++) {
|
|
2564
|
-
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
|
|
2565
|
-
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
2566
|
-
}
|
|
2567
|
-
[prev, curr] = [curr, prev];
|
|
2568
|
-
}
|
|
2569
|
-
return prev[b.length];
|
|
2570
|
-
}
|
|
2571
|
-
|
|
2572
|
-
// libs/adapters/src/skills/deploy/deploy-manifest.schema.ts
|
|
2573
|
-
var import_lazy_zod3 = require("@frontmcp/lazy-zod");
|
|
2574
|
-
var IDENT_RE = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
2575
|
-
var SPEC_ID_RE2 = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
2576
|
-
var SECRET_NAME_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
2577
|
-
var BINDING_NAME_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
2578
|
-
var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
2579
|
-
var serverInfoSchema = import_lazy_zod3.z.object({
|
|
2580
|
-
name: import_lazy_zod3.z.string().min(1).max(128),
|
|
2581
|
-
version: import_lazy_zod3.z.string().min(1).max(64),
|
|
2582
|
-
title: import_lazy_zod3.z.string().min(1).max(256).optional()
|
|
2583
|
-
}).strict();
|
|
2584
|
-
var serverSchema = import_lazy_zod3.z.object({
|
|
2585
|
-
info: serverInfoSchema,
|
|
2586
|
-
/**
|
|
2587
|
-
* Top-level server instructions injected at `initialize` time. Picked up
|
|
2588
|
-
* by the existing `skillsConfig.injectInstructions` machinery (defaults
|
|
2589
|
-
* to `append` so the skill catalog summary follows).
|
|
2590
|
-
*/
|
|
2591
|
-
instructions: import_lazy_zod3.z.string().max(16 * 1024).optional()
|
|
2592
|
-
}).strict();
|
|
2593
|
-
var runtimeSchema = import_lazy_zod3.z.object({
|
|
2594
|
-
/** Only `cloudflare-worker` in v1. Future: `vercel-edge`, `deno-deploy`. */
|
|
2595
|
-
target: import_lazy_zod3.z.literal("cloudflare-worker"),
|
|
2596
|
-
compatibilityDate: import_lazy_zod3.z.string().regex(ISO_DATE_RE, "compatibilityDate must be ISO-8601 (YYYY-MM-DD)"),
|
|
2597
|
-
/** Worker compatibility flags (e.g. `nodejs_compat`). */
|
|
2598
|
-
compatibilityFlags: import_lazy_zod3.z.array(import_lazy_zod3.z.string().min(1).max(128)).max(32).optional()
|
|
2599
|
-
}).strict();
|
|
2600
|
-
var specSourceSchema = import_lazy_zod3.z.union([
|
|
2601
|
-
/** Local file path or HTTPS URL to a single spec. Spec id = filename stem. */
|
|
2602
|
-
import_lazy_zod3.z.string().min(1).max(2048),
|
|
2603
|
-
import_lazy_zod3.z.object({
|
|
2604
|
-
id: import_lazy_zod3.z.string().min(1).max(128).regex(SPEC_ID_RE2, "spec id must be a URI-safe identifier"),
|
|
2605
|
-
spec: import_lazy_zod3.z.string().min(1).max(2048),
|
|
2606
|
-
baseUrl: import_lazy_zod3.z.string().url("baseUrl must be a valid URL").optional(),
|
|
2607
|
-
/** Optional namespace override for AgentScript bindings (default = `id`). */
|
|
2608
|
-
bindingName: import_lazy_zod3.z.string().min(1).max(64).regex(IDENT_RE).optional()
|
|
2609
|
-
}).strict()
|
|
2610
|
-
]);
|
|
2611
|
-
var specsSchema = import_lazy_zod3.z.union([
|
|
2612
|
-
/** Single directory path; the loader auto-discovers `*.yaml` / `*.json`. */
|
|
2613
|
-
import_lazy_zod3.z.string().min(1).max(2048),
|
|
2614
|
-
/** Explicit list (mixed strings and detail objects allowed). */
|
|
2615
|
-
import_lazy_zod3.z.array(specSourceSchema).max(64)
|
|
2616
|
-
]);
|
|
2617
|
-
var skillsSchema = import_lazy_zod3.z.object({
|
|
2618
|
-
/** Directory under the manifest. Defaults to `./skills/`. */
|
|
2619
|
-
source: import_lazy_zod3.z.string().min(1).max(2048).default("./skills/"),
|
|
2620
|
-
/**
|
|
2621
|
-
* IDs of skills the worker pre-loads into every `execute()` regardless
|
|
2622
|
-
* of which skills the agent passed. Mirrors the per-skill
|
|
2623
|
-
* `alwaysLoad: true` frontmatter, but lets the server force it.
|
|
2624
|
-
*/
|
|
2625
|
-
alwaysLoad: import_lazy_zod3.z.array(import_lazy_zod3.z.string().min(1).max(128)).max(32).optional(),
|
|
2626
|
-
/** Optional tag filter applied at deploy time. */
|
|
2627
|
-
tags: import_lazy_zod3.z.object({
|
|
2628
|
-
include: import_lazy_zod3.z.array(import_lazy_zod3.z.string().min(1).max(64)).max(64).optional(),
|
|
2629
|
-
exclude: import_lazy_zod3.z.array(import_lazy_zod3.z.string().min(1).max(64)).max(64).optional()
|
|
2630
|
-
}).strict().optional()
|
|
2631
|
-
}).strict();
|
|
2632
|
-
var tagSchema = import_lazy_zod3.z.object({
|
|
2633
|
-
name: import_lazy_zod3.z.string().min(1).max(64),
|
|
2634
|
-
description: import_lazy_zod3.z.string().min(1).max(512).optional()
|
|
2635
|
-
}).strict();
|
|
2636
|
-
var classificationRuleSchema = import_lazy_zod3.z.object({
|
|
2637
|
-
/**
|
|
2638
|
-
* Match pattern: METHOD followed by a path-glob, e.g.
|
|
2639
|
-
* `POST <star>/reset-<star>` (where <star> is the literal `*` glob).
|
|
2640
|
-
* Written this way to avoid prematurely closing the JSDoc with star-slash.
|
|
2641
|
-
*/
|
|
2642
|
-
match: import_lazy_zod3.z.string().min(1).max(256),
|
|
2643
|
-
/**
|
|
2644
|
-
* Override the MCP surface for matching operations: tool (default for
|
|
2645
|
-
* mutations), resource (default for GET list / GET path-param), or both
|
|
2646
|
-
* (default for GET path-param). Omit to leave the classifier's default.
|
|
2647
|
-
*/
|
|
2648
|
-
expose: import_lazy_zod3.z.enum(["tool", "resource", "both"]).optional(),
|
|
2649
|
-
/**
|
|
2650
|
-
* Resource-update emit target. `self` (mutation on its own resource),
|
|
2651
|
-
* `parent` (mutation that affects the parent collection / parent path),
|
|
2652
|
-
* or `none` (suppress). Defaults from the HTTP-semantics classifier.
|
|
2653
|
-
*/
|
|
2654
|
-
emits: import_lazy_zod3.z.enum(["self", "parent", "none"]).optional()
|
|
2655
|
-
}).strict();
|
|
2656
|
-
var classificationSchema = import_lazy_zod3.z.object({
|
|
2657
|
-
rules: import_lazy_zod3.z.array(classificationRuleSchema).max(64).optional()
|
|
2658
|
-
}).strict();
|
|
2659
|
-
var durableObjectBindingSchema = import_lazy_zod3.z.object({
|
|
2660
|
-
binding: import_lazy_zod3.z.string().min(1).max(128).regex(BINDING_NAME_RE),
|
|
2661
|
-
className: import_lazy_zod3.z.string().min(1).max(128).regex(IDENT_RE),
|
|
2662
|
-
scriptName: import_lazy_zod3.z.string().min(1).max(128).optional()
|
|
2663
|
-
}).strict();
|
|
2664
|
-
var d1BindingSchema = import_lazy_zod3.z.object({
|
|
2665
|
-
binding: import_lazy_zod3.z.string().min(1).max(128).regex(BINDING_NAME_RE),
|
|
2666
|
-
databaseName: import_lazy_zod3.z.string().min(1).max(256),
|
|
2667
|
-
databaseId: import_lazy_zod3.z.string().min(1).max(128)
|
|
2668
|
-
}).strict();
|
|
2669
|
-
var kvBindingSchema = import_lazy_zod3.z.object({
|
|
2670
|
-
binding: import_lazy_zod3.z.string().min(1).max(128).regex(BINDING_NAME_RE),
|
|
2671
|
-
id: import_lazy_zod3.z.string().min(1).max(128)
|
|
2672
|
-
}).strict();
|
|
2673
|
-
var r2BindingSchema = import_lazy_zod3.z.object({
|
|
2674
|
-
binding: import_lazy_zod3.z.string().min(1).max(128).regex(BINDING_NAME_RE),
|
|
2675
|
-
bucketName: import_lazy_zod3.z.string().min(1).max(256)
|
|
2676
|
-
}).strict();
|
|
2677
|
-
var bindingsSchema = import_lazy_zod3.z.object({
|
|
2678
|
-
durableObjects: import_lazy_zod3.z.array(durableObjectBindingSchema).max(32).optional(),
|
|
2679
|
-
d1Databases: import_lazy_zod3.z.array(d1BindingSchema).max(16).optional(),
|
|
2680
|
-
kvNamespaces: import_lazy_zod3.z.array(kvBindingSchema).max(32).optional(),
|
|
2681
|
-
r2Buckets: import_lazy_zod3.z.array(r2BindingSchema).max(16).optional(),
|
|
2682
|
-
/** Plaintext vars surfaced as `env.<name>` on the Worker. */
|
|
2683
|
-
vars: import_lazy_zod3.z.record(import_lazy_zod3.z.string().min(1).max(128), import_lazy_zod3.z.string().max(4096)).optional()
|
|
2684
|
-
}).strict();
|
|
2685
|
-
var trustRootSchema = import_lazy_zod3.z.object({
|
|
2686
|
-
/** Key id — matches the JWS `kid` header on the signed bundle. */
|
|
2687
|
-
kid: import_lazy_zod3.z.string().min(1).max(128),
|
|
2688
|
-
/** Name of the secret binding holding the PEM-encoded public key. */
|
|
2689
|
-
publicKeySecret: import_lazy_zod3.z.string().min(1).max(128).regex(SECRET_NAME_RE)
|
|
2690
|
-
}).strict();
|
|
2691
|
-
var replayGuardSchema = import_lazy_zod3.z.object({
|
|
2692
|
-
/** Acceptable clock skew; rejects bundles older than this. */
|
|
2693
|
-
windowSeconds: import_lazy_zod3.z.number().int().min(10).max(3600).default(300),
|
|
2694
|
-
/** KV binding name that stores nonces for replay protection. */
|
|
2695
|
-
nonceKv: import_lazy_zod3.z.string().min(1).max(128).regex(BINDING_NAME_RE)
|
|
2696
|
-
}).strict();
|
|
2697
|
-
var signingSchema = import_lazy_zod3.z.object({
|
|
2698
|
-
algorithm: import_lazy_zod3.z.enum(["ed25519", "rs256"]),
|
|
2699
|
-
trustRoots: import_lazy_zod3.z.array(trustRootSchema).min(1).max(8),
|
|
2700
|
-
replay: replayGuardSchema
|
|
2701
|
-
}).strict();
|
|
2702
|
-
var fronteggAuthSchema = import_lazy_zod3.z.object({
|
|
2703
|
-
tenantResolver: import_lazy_zod3.z.enum(["subdomain", "header", "jwt-claim"]).default("subdomain"),
|
|
2704
|
-
audience: import_lazy_zod3.z.string().min(1).max(256),
|
|
2705
|
-
/** Secret name holding the issuer URL. */
|
|
2706
|
-
issuerSecret: import_lazy_zod3.z.string().min(1).max(128).regex(SECRET_NAME_RE)
|
|
2707
|
-
}).strict();
|
|
2708
|
-
var oauthAuthSchema = import_lazy_zod3.z.object({
|
|
2709
|
-
issuer: import_lazy_zod3.z.string().url(),
|
|
2710
|
-
audience: import_lazy_zod3.z.string().min(1).max(256),
|
|
2711
|
-
/** Optional secret name for client credentials (machine-to-machine). */
|
|
2712
|
-
credentialsSecret: import_lazy_zod3.z.string().min(1).max(128).regex(SECRET_NAME_RE).optional()
|
|
2713
|
-
}).strict();
|
|
2714
|
-
var apiKeyAuthSchema = import_lazy_zod3.z.object({
|
|
2715
|
-
/** Header name carrying the key (e.g. `X-API-Key`). */
|
|
2716
|
-
header: import_lazy_zod3.z.string().min(1).max(64).default("X-API-Key"),
|
|
2717
|
-
/** Secret name holding the allowlist (newline-separated keys). */
|
|
2718
|
-
allowlistSecret: import_lazy_zod3.z.string().min(1).max(128).regex(SECRET_NAME_RE)
|
|
2719
|
-
}).strict();
|
|
2720
|
-
var authSchema = import_lazy_zod3.z.discriminatedUnion("provider", [
|
|
2721
|
-
import_lazy_zod3.z.object({ provider: import_lazy_zod3.z.literal("none") }).strict(),
|
|
2722
|
-
import_lazy_zod3.z.object({ provider: import_lazy_zod3.z.literal("frontegg"), frontegg: fronteggAuthSchema }).strict(),
|
|
2723
|
-
import_lazy_zod3.z.object({ provider: import_lazy_zod3.z.literal("oauth"), oauth: oauthAuthSchema }).strict(),
|
|
2724
|
-
import_lazy_zod3.z.object({ provider: import_lazy_zod3.z.literal("apiKey"), apiKey: apiKeyAuthSchema }).strict()
|
|
2725
|
-
]);
|
|
2726
|
-
var secretSchema = import_lazy_zod3.z.object({
|
|
2727
|
-
name: import_lazy_zod3.z.string().min(1).max(128).regex(SECRET_NAME_RE),
|
|
2728
|
-
required: import_lazy_zod3.z.boolean().default(true),
|
|
2729
|
-
description: import_lazy_zod3.z.string().min(1).max(512).optional()
|
|
2730
|
-
}).strict();
|
|
2731
|
-
var environmentOverlaySchema = import_lazy_zod3.z.object({
|
|
2732
|
-
server: serverSchema.partial().optional(),
|
|
2733
|
-
specs: specsSchema.optional(),
|
|
2734
|
-
skills: skillsSchema.partial().optional(),
|
|
2735
|
-
classification: classificationSchema.optional(),
|
|
2736
|
-
bindings: bindingsSchema.optional(),
|
|
2737
|
-
auth: authSchema.optional(),
|
|
2738
|
-
vars: import_lazy_zod3.z.record(import_lazy_zod3.z.string().min(1).max(128), import_lazy_zod3.z.string().max(4096)).optional()
|
|
2739
|
-
}).strict();
|
|
2740
|
-
var deployManifestSchema = import_lazy_zod3.z.object({
|
|
2741
|
-
/** Optional JSON Schema URL — accepted for editor autocompletion. */
|
|
2742
|
-
$schema: import_lazy_zod3.z.string().url().optional(),
|
|
2743
|
-
/** Schema version. Only `1` accepted in v1.3. */
|
|
2744
|
-
version: import_lazy_zod3.z.literal(1),
|
|
2745
|
-
/** Project name (for logs + the Worker's `worker.name`). */
|
|
2746
|
-
name: import_lazy_zod3.z.string().min(1).max(128).regex(IDENT_RE),
|
|
2747
|
-
runtime: runtimeSchema,
|
|
2748
|
-
server: serverSchema,
|
|
2749
|
-
specs: specsSchema,
|
|
2750
|
-
skills: skillsSchema.default({ source: "./skills/" }),
|
|
2751
|
-
tags: import_lazy_zod3.z.array(tagSchema).max(64).optional(),
|
|
2752
|
-
classification: classificationSchema.optional(),
|
|
2753
|
-
bindings: bindingsSchema,
|
|
2754
|
-
signing: signingSchema,
|
|
2755
|
-
auth: authSchema,
|
|
2756
|
-
secrets: import_lazy_zod3.z.array(secretSchema).max(64).optional(),
|
|
2757
|
-
environments: import_lazy_zod3.z.record(import_lazy_zod3.z.string().min(1).max(64).regex(IDENT_RE), environmentOverlaySchema).optional()
|
|
2758
|
-
}).strict();
|
|
2759
|
-
function applyEnvironmentOverlay(base, overlay) {
|
|
2760
|
-
const effectiveBindings = overlay.bindings ?? base.bindings;
|
|
2761
|
-
const mergedBindings = overlay.vars !== void 0 ? { ...effectiveBindings, vars: { ...effectiveBindings.vars ?? {}, ...overlay.vars } } : effectiveBindings;
|
|
2762
|
-
return {
|
|
2763
|
-
...base,
|
|
2764
|
-
server: overlay.server ? { ...base.server, ...overlay.server } : base.server,
|
|
2765
|
-
specs: overlay.specs ?? base.specs,
|
|
2766
|
-
skills: overlay.skills ? { ...base.skills, ...overlay.skills } : base.skills,
|
|
2767
|
-
classification: overlay.classification ?? base.classification,
|
|
2768
|
-
bindings: mergedBindings,
|
|
2769
|
-
auth: overlay.auth ?? base.auth
|
|
2770
|
-
};
|
|
2771
|
-
}
|
|
2772
|
-
function validateEffectiveManifest(manifest, label, errors) {
|
|
2773
|
-
const declaredSecrets = new Set((manifest.secrets ?? []).map((s) => s.name));
|
|
2774
|
-
const refs = [];
|
|
2775
|
-
if (manifest.auth.provider === "frontegg") {
|
|
2776
|
-
refs.push({ where: "auth.frontegg.issuerSecret", name: manifest.auth.frontegg.issuerSecret });
|
|
2777
|
-
} else if (manifest.auth.provider === "oauth" && manifest.auth.oauth.credentialsSecret) {
|
|
2778
|
-
refs.push({ where: "auth.oauth.credentialsSecret", name: manifest.auth.oauth.credentialsSecret });
|
|
2779
|
-
} else if (manifest.auth.provider === "apiKey") {
|
|
2780
|
-
refs.push({ where: "auth.apiKey.allowlistSecret", name: manifest.auth.apiKey.allowlistSecret });
|
|
2781
|
-
}
|
|
2782
|
-
for (const root of manifest.signing.trustRoots) {
|
|
2783
|
-
refs.push({ where: `signing.trustRoots[kid=${root.kid}].publicKeySecret`, name: root.publicKeySecret });
|
|
2784
|
-
}
|
|
2785
|
-
for (const ref of refs) {
|
|
2786
|
-
if (!declaredSecrets.has(ref.name)) {
|
|
2787
|
-
errors.push(`${label}Secret "${ref.name}" referenced at ${ref.where} is not declared in secrets[]`);
|
|
2788
|
-
}
|
|
2789
|
-
}
|
|
2790
|
-
const declaredKv = new Set((manifest.bindings.kvNamespaces ?? []).map((kv) => kv.binding));
|
|
2791
|
-
if (!declaredKv.has(manifest.signing.replay.nonceKv)) {
|
|
2792
|
-
errors.push(
|
|
2793
|
-
`${label}signing.replay.nonceKv "${manifest.signing.replay.nonceKv}" does not match any bindings.kvNamespaces[].binding`
|
|
2794
|
-
);
|
|
2795
|
-
}
|
|
2796
|
-
for (const id of manifest.skills.alwaysLoad ?? []) {
|
|
2797
|
-
if (!/^[a-z0-9][a-z0-9-]*$/.test(id)) {
|
|
2798
|
-
errors.push(`${label}skills.alwaysLoad entry "${id}" is not a valid kebab-case skill id`);
|
|
2799
|
-
}
|
|
2800
|
-
}
|
|
2801
|
-
if (manifest.tags) {
|
|
2802
|
-
const declaredTags = new Set(manifest.tags.map((t) => t.name));
|
|
2803
|
-
const tagFilter = manifest.skills.tags;
|
|
2804
|
-
for (const t of [...tagFilter?.include ?? [], ...tagFilter?.exclude ?? []]) {
|
|
2805
|
-
if (!declaredTags.has(t)) {
|
|
2806
|
-
errors.push(`${label}skills.tags references unknown tag "${t}" (not in tags[])`);
|
|
2807
|
-
}
|
|
2808
|
-
}
|
|
2809
|
-
}
|
|
2810
|
-
}
|
|
2811
|
-
function crossValidateManifest(manifest) {
|
|
2812
|
-
const errors = [];
|
|
2813
|
-
validateEffectiveManifest(manifest, "", errors);
|
|
2814
|
-
for (const [envName, overlay] of Object.entries(manifest.environments ?? {})) {
|
|
2815
|
-
const effective = applyEnvironmentOverlay(manifest, overlay);
|
|
2816
|
-
validateEffectiveManifest(effective, `environments.${envName}: `, errors);
|
|
2817
|
-
}
|
|
2818
|
-
return errors.length === 0 ? { ok: true } : { ok: false, errors };
|
|
2819
|
-
}
|
|
2820
|
-
|
|
2821
|
-
// libs/adapters/src/skills/deploy/manifest-to-config.ts
|
|
2822
|
-
function specStem(ref) {
|
|
2823
|
-
const noQuery = ref.split("?")[0].split("#")[0];
|
|
2824
|
-
const base = noQuery.split("/").pop() ?? noQuery;
|
|
2825
|
-
const stem = base.replace(/\.(ya?ml|json)$/i, "");
|
|
2826
|
-
const safe = (stem || base).replace(/[^a-zA-Z0-9_.-]/g, "-");
|
|
2827
|
-
return safe || "spec";
|
|
2828
|
-
}
|
|
2829
|
-
function normalizeSpec(source) {
|
|
2830
|
-
if (typeof source === "string") {
|
|
2831
|
-
const id = specStem(source);
|
|
2832
|
-
return { id, spec: source, bindingName: id };
|
|
2833
|
-
}
|
|
2834
|
-
return {
|
|
2835
|
-
id: source.id,
|
|
2836
|
-
spec: source.spec,
|
|
2837
|
-
baseUrl: source.baseUrl,
|
|
2838
|
-
bindingName: source.bindingName ?? source.id
|
|
2839
|
-
};
|
|
2840
|
-
}
|
|
2841
|
-
function buildFrontMcpConfigFromManifest(manifest) {
|
|
2842
|
-
const info = {
|
|
2843
|
-
name: manifest.server.info.name,
|
|
2844
|
-
version: manifest.server.info.version,
|
|
2845
|
-
...manifest.server.info.title !== void 0 ? { title: manifest.server.info.title } : {}
|
|
2846
|
-
};
|
|
2847
|
-
const skills = {
|
|
2848
|
-
source: manifest.skills.source,
|
|
2849
|
-
...manifest.skills.alwaysLoad ? { alwaysLoad: manifest.skills.alwaysLoad } : {},
|
|
2850
|
-
...manifest.skills.tags ? { tags: manifest.skills.tags } : {}
|
|
2851
|
-
};
|
|
2852
|
-
let specs = [];
|
|
2853
|
-
let specsDir;
|
|
2854
|
-
if (typeof manifest.specs === "string") {
|
|
2855
|
-
specsDir = manifest.specs;
|
|
2856
|
-
} else {
|
|
2857
|
-
specs = manifest.specs.map(normalizeSpec);
|
|
2858
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2859
|
-
for (const s of specs) {
|
|
2860
|
-
if (seen.has(s.id)) {
|
|
2861
|
-
throw new Error(
|
|
2862
|
-
`manifest specs: duplicate spec id "${s.id}" (derived from the filename stem). Give the colliding spec an explicit { id } in frontmcp.deploy.yaml.`
|
|
2863
|
-
);
|
|
2864
|
-
}
|
|
2865
|
-
seen.add(s.id);
|
|
2866
|
-
}
|
|
2867
|
-
}
|
|
2868
|
-
return {
|
|
2869
|
-
info,
|
|
2870
|
-
...manifest.server.instructions !== void 0 ? { instructions: manifest.server.instructions } : {},
|
|
2871
|
-
skillsConfig: { enabled: true, injectInstructions: "append" },
|
|
2872
|
-
skills,
|
|
2873
|
-
specs,
|
|
2874
|
-
...specsDir !== void 0 ? { specsDir } : {}
|
|
2875
|
-
};
|
|
2876
|
-
}
|
|
2877
|
-
|
|
2878
|
-
// libs/adapters/src/skills/classifier/openapi-classify.ts
|
|
2879
|
-
function classifyOperations(specId, ops) {
|
|
2880
|
-
if (!specId || specId.length === 0) {
|
|
2881
|
-
throw new Error("classifyOperations: specId is required");
|
|
2882
|
-
}
|
|
2883
|
-
if (!Array.isArray(ops)) return [];
|
|
2884
|
-
const pathsWithGet = /* @__PURE__ */ new Set();
|
|
2885
|
-
for (const op of ops) {
|
|
2886
|
-
if (op && typeof op.path === "string" && typeof op.method === "string" && op.method.toUpperCase() === "GET") {
|
|
2887
|
-
pathsWithGet.add(op.path);
|
|
2888
|
-
}
|
|
2889
|
-
}
|
|
2890
|
-
return ops.map((op) => classifyOne(specId, op, pathsWithGet));
|
|
2891
|
-
}
|
|
2892
|
-
function classifyOne(specId, op, pathsWithGet) {
|
|
2893
|
-
const method = normaliseMethod(op.method);
|
|
2894
|
-
const path = op.path;
|
|
2895
|
-
const hasTerminalParam = pathHasTerminalParam(path);
|
|
2896
|
-
const parent = parentPath(path);
|
|
2897
|
-
const thisIsResource = pathsWithGet.has(path);
|
|
2898
|
-
const parentIsResource = parent !== null && pathsWithGet.has(parent);
|
|
2899
|
-
const selfUri = pathToResourceUri(specId, path);
|
|
2900
|
-
const parentUri = parent !== null ? pathToResourceUri(specId, parent) : null;
|
|
2901
|
-
if (method === "GET") {
|
|
2902
|
-
if (hasTerminalParam) {
|
|
2903
|
-
return {
|
|
2904
|
-
operationId: op.operationId,
|
|
2905
|
-
method,
|
|
2906
|
-
path,
|
|
2907
|
-
specId,
|
|
2908
|
-
expose: "both",
|
|
2909
|
-
resourceUriTemplate: selfUri
|
|
2910
|
-
};
|
|
2911
|
-
}
|
|
2912
|
-
return {
|
|
2913
|
-
operationId: op.operationId,
|
|
2914
|
-
method,
|
|
2915
|
-
path,
|
|
2916
|
-
specId,
|
|
2917
|
-
expose: "resource",
|
|
2918
|
-
resourceUriTemplate: selfUri
|
|
2919
|
-
};
|
|
2920
|
-
}
|
|
2921
|
-
const base = {
|
|
2922
|
-
operationId: op.operationId,
|
|
2923
|
-
method,
|
|
2924
|
-
path,
|
|
2925
|
-
specId,
|
|
2926
|
-
expose: "tool"
|
|
2927
|
-
// `resourceUriTemplate` left undefined for mutations — the emit target
|
|
2928
|
-
// is the authoritative thing to look at.
|
|
2929
|
-
};
|
|
2930
|
-
if (method === "POST") {
|
|
2931
|
-
if (thisIsResource && !hasTerminalParam) {
|
|
2932
|
-
return { ...base, emit: { kind: "listChanged", pathTemplate: path, resourceUriTemplate: selfUri } };
|
|
2933
|
-
}
|
|
2934
|
-
if (thisIsResource && hasTerminalParam) {
|
|
2935
|
-
return { ...base, emit: { kind: "updated", pathTemplate: path, resourceUriTemplate: selfUri } };
|
|
2936
|
-
}
|
|
2937
|
-
if (parentIsResource && parentUri !== null && parent !== null) {
|
|
2938
|
-
return { ...base, emit: { kind: "updated", pathTemplate: parent, resourceUriTemplate: parentUri } };
|
|
2939
|
-
}
|
|
2940
|
-
return base;
|
|
2941
|
-
}
|
|
2942
|
-
if (method === "PUT" || method === "PATCH") {
|
|
2943
|
-
if (thisIsResource) {
|
|
2944
|
-
return { ...base, emit: { kind: "updated", pathTemplate: path, resourceUriTemplate: selfUri } };
|
|
2945
|
-
}
|
|
2946
|
-
if (parentIsResource && parentUri !== null && parent !== null) {
|
|
2947
|
-
return { ...base, emit: { kind: "updated", pathTemplate: parent, resourceUriTemplate: parentUri } };
|
|
2948
|
-
}
|
|
2949
|
-
return base;
|
|
2950
|
-
}
|
|
2951
|
-
if (method === "DELETE") {
|
|
2952
|
-
if (hasTerminalParam && parentIsResource && parentUri !== null && parent !== null) {
|
|
2953
|
-
return { ...base, emit: { kind: "listChanged", pathTemplate: parent, resourceUriTemplate: parentUri } };
|
|
2954
|
-
}
|
|
2955
|
-
if (!hasTerminalParam && thisIsResource) {
|
|
2956
|
-
return { ...base, emit: { kind: "listChanged", pathTemplate: path, resourceUriTemplate: selfUri } };
|
|
2957
|
-
}
|
|
2958
|
-
return base;
|
|
2959
|
-
}
|
|
2960
|
-
return base;
|
|
2961
|
-
}
|
|
2962
|
-
function applyClassificationOverrides(classified, rules) {
|
|
2963
|
-
if (!Array.isArray(rules) || rules.length === 0) return classified.slice();
|
|
2964
|
-
const compiled = rules.map((rule) => ({ rule, matcher: compileMatcher(rule.match) }));
|
|
2965
|
-
return classified.map((op) => {
|
|
2966
|
-
for (const { rule, matcher } of compiled) {
|
|
2967
|
-
if (!matcher(op)) continue;
|
|
2968
|
-
return applyOne(op, rule);
|
|
2969
|
-
}
|
|
2970
|
-
return op;
|
|
2971
|
-
});
|
|
2972
|
-
}
|
|
2973
|
-
var KNOWN_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "TRACE"]);
|
|
2974
|
-
function normaliseMethod(m) {
|
|
2975
|
-
const up = (m ?? "").toUpperCase();
|
|
2976
|
-
return KNOWN_METHODS.has(up) ? up : up;
|
|
2977
|
-
}
|
|
2978
|
-
function pathHasTerminalParam(path) {
|
|
2979
|
-
if (typeof path !== "string" || path.length === 0) return false;
|
|
2980
|
-
const segments = path.split("/").filter((s) => s.length > 0);
|
|
2981
|
-
if (segments.length === 0) return false;
|
|
2982
|
-
const last = segments[segments.length - 1];
|
|
2983
|
-
return last.startsWith("{") && last.endsWith("}") && last.length >= 3;
|
|
2984
|
-
}
|
|
2985
|
-
function parentPath(path) {
|
|
2986
|
-
if (typeof path !== "string" || path.length === 0) return null;
|
|
2987
|
-
const segments = path.split("/").filter((s) => s.length > 0);
|
|
2988
|
-
if (segments.length <= 1) return null;
|
|
2989
|
-
return "/" + segments.slice(0, -1).join("/");
|
|
2990
|
-
}
|
|
2991
|
-
function pathToResourceUri(specId, path) {
|
|
2992
|
-
const cleaned = path.startsWith("/") ? path.slice(1) : path;
|
|
2993
|
-
return `mcp+op://${specId}/${cleaned}`;
|
|
2994
|
-
}
|
|
2995
|
-
function compileMatcher(match) {
|
|
2996
|
-
const trimmed = (match ?? "").trim();
|
|
2997
|
-
const sep2 = trimmed.indexOf(" ");
|
|
2998
|
-
if (sep2 <= 0 || sep2 === trimmed.length - 1) {
|
|
2999
|
-
return () => false;
|
|
3000
|
-
}
|
|
3001
|
-
const methodPattern = trimmed.slice(0, sep2).toUpperCase();
|
|
3002
|
-
const pathPattern = trimmed.slice(sep2 + 1);
|
|
3003
|
-
const pathRe = globToRegExp(pathPattern);
|
|
3004
|
-
return (op) => {
|
|
3005
|
-
if (methodPattern !== "*" && methodPattern !== op.method) return false;
|
|
3006
|
-
return pathRe.test(op.path);
|
|
3007
|
-
};
|
|
3008
|
-
}
|
|
3009
|
-
function globToRegExp(glob) {
|
|
3010
|
-
const STAR_STAR = "\0";
|
|
3011
|
-
const STAR = "";
|
|
3012
|
-
const replaced = glob.replace(/\*\*/g, STAR_STAR).replace(/\*/g, STAR);
|
|
3013
|
-
const escaped = replaced.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
3014
|
-
const expanded = escaped.split(STAR_STAR).join(".*").split(STAR).join("[^/]*");
|
|
3015
|
-
return new RegExp(`^${expanded}$`);
|
|
3016
|
-
}
|
|
3017
|
-
function defaultEmitKindForMethod(method) {
|
|
3018
|
-
return method === "DELETE" ? "listChanged" : "updated";
|
|
3019
|
-
}
|
|
3020
|
-
function applyOne(op, rule) {
|
|
3021
|
-
let next = { ...op };
|
|
3022
|
-
if (rule.expose !== void 0) {
|
|
3023
|
-
next = { ...next, expose: rule.expose };
|
|
3024
|
-
}
|
|
3025
|
-
if (rule.emits !== void 0) {
|
|
3026
|
-
if (rule.emits === "none") {
|
|
3027
|
-
const { emit: _drop, ...rest } = next;
|
|
3028
|
-
next = rest;
|
|
3029
|
-
} else if (rule.emits === "self") {
|
|
3030
|
-
const target = pathToResourceUri(op.specId, op.path);
|
|
3031
|
-
next = {
|
|
3032
|
-
...next,
|
|
3033
|
-
emit: {
|
|
3034
|
-
// Preserve the classifier's original kind if it set one; otherwise
|
|
3035
|
-
// derive from the HTTP method so a DELETE override doesn't end up
|
|
3036
|
-
// emitting `updated` on its own (now-gone) URI.
|
|
3037
|
-
kind: next.emit?.kind ?? defaultEmitKindForMethod(op.method),
|
|
3038
|
-
pathTemplate: op.path,
|
|
3039
|
-
resourceUriTemplate: target
|
|
3040
|
-
}
|
|
3041
|
-
};
|
|
3042
|
-
} else if (rule.emits === "parent") {
|
|
3043
|
-
const parent = parentPath(op.path);
|
|
3044
|
-
if (parent !== null) {
|
|
3045
|
-
next = {
|
|
3046
|
-
...next,
|
|
3047
|
-
emit: {
|
|
3048
|
-
kind: next.emit?.kind ?? defaultEmitKindForMethod(op.method),
|
|
3049
|
-
pathTemplate: parent,
|
|
3050
|
-
resourceUriTemplate: pathToResourceUri(op.specId, parent)
|
|
3051
|
-
}
|
|
3052
|
-
};
|
|
3053
|
-
} else {
|
|
3054
|
-
const { emit: _drop, ...rest } = next;
|
|
3055
|
-
next = rest;
|
|
3056
|
-
}
|
|
3057
|
-
}
|
|
3058
|
-
}
|
|
3059
|
-
return next;
|
|
3060
|
-
}
|
|
3061
|
-
|
|
3062
|
-
// libs/adapters/src/skills/classifier/render-resource-uri.ts
|
|
3063
|
-
var TEMPLATE_PLACEHOLDER_RE = /\{([^{}]+)\}/g;
|
|
3064
|
-
function renderResourceUri(template, args) {
|
|
3065
|
-
if (typeof template !== "string" || template.length === 0) {
|
|
3066
|
-
return { ok: false, missing: [] };
|
|
3067
|
-
}
|
|
3068
|
-
const argObj = args !== null && typeof args === "object" ? args : {};
|
|
3069
|
-
const missing = [];
|
|
3070
|
-
const uri = template.replace(TEMPLATE_PLACEHOLDER_RE, (_match, name) => {
|
|
3071
|
-
const value = argObj[name];
|
|
3072
|
-
if (value === void 0 || value === null) {
|
|
3073
|
-
missing.push(name);
|
|
3074
|
-
return "";
|
|
3075
|
-
}
|
|
3076
|
-
if (typeof value === "object") {
|
|
3077
|
-
missing.push(name);
|
|
3078
|
-
return "";
|
|
3079
|
-
}
|
|
3080
|
-
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
3081
|
-
missing.push(name);
|
|
3082
|
-
return "";
|
|
3083
|
-
}
|
|
3084
|
-
return encodeURIComponent(String(value));
|
|
3085
|
-
});
|
|
3086
|
-
if (missing.length > 0) return { ok: false, missing };
|
|
3087
|
-
return { ok: true, uri };
|
|
3088
|
-
}
|
|
3089
|
-
|
|
3090
|
-
// libs/adapters/src/skills/classifier/resource-change-notification.ts
|
|
3091
|
-
function buildResourceChangeNotification(classification, args) {
|
|
3092
|
-
const emit = classification.emit;
|
|
3093
|
-
if (!emit) return { notification: null, reason: "no-emit" };
|
|
3094
|
-
if (emit.kind === "listChanged") {
|
|
3095
|
-
const rendered2 = renderResourceUri(emit.resourceUriTemplate, args);
|
|
3096
|
-
if (!rendered2.ok) {
|
|
3097
|
-
return { notification: null, reason: "unresolved-template", missing: rendered2.missing };
|
|
3098
|
-
}
|
|
3099
|
-
return { notification: { method: "notifications/resources/list_changed" } };
|
|
3100
|
-
}
|
|
3101
|
-
const rendered = renderResourceUri(emit.resourceUriTemplate, args);
|
|
3102
|
-
if (!rendered.ok) {
|
|
3103
|
-
return { notification: null, reason: "unresolved-template", missing: rendered.missing };
|
|
3104
|
-
}
|
|
3105
|
-
return {
|
|
3106
|
-
notification: {
|
|
3107
|
-
method: "notifications/resources/updated",
|
|
3108
|
-
params: { uri: rendered.uri }
|
|
3109
|
-
}
|
|
3110
|
-
};
|
|
3111
|
-
}
|
|
3112
|
-
|
|
3113
|
-
// libs/adapters/src/skills/classifier/classification-registry.ts
|
|
3114
|
-
var ClassificationRegistry = class {
|
|
3115
|
-
byToolName = /* @__PURE__ */ new Map();
|
|
3116
|
-
/**
|
|
3117
|
-
* Register (or replace) a classification for a tool name. Returns the
|
|
3118
|
-
* previous classification if one existed.
|
|
3119
|
-
*
|
|
3120
|
-
* Useful for hot-reload: the deploy pipeline re-builds classifications
|
|
3121
|
-
* from the new bundle and overwrites in place.
|
|
3122
|
-
*/
|
|
3123
|
-
register(toolName, classification) {
|
|
3124
|
-
if (typeof toolName !== "string" || toolName.length === 0) {
|
|
3125
|
-
throw new Error("ClassificationRegistry.register: toolName must be a non-empty string");
|
|
3126
|
-
}
|
|
3127
|
-
const prev = this.byToolName.get(toolName);
|
|
3128
|
-
this.byToolName.set(toolName, classification);
|
|
3129
|
-
return prev;
|
|
3130
|
-
}
|
|
3131
|
-
/**
|
|
3132
|
-
* Bulk-register an array of classifications keyed by `${specId}.${operationId}`.
|
|
3133
|
-
* Returns the count of newly-added entries (existing entries are overwritten
|
|
3134
|
-
* but not counted toward `added`).
|
|
3135
|
-
*/
|
|
3136
|
-
registerAll(classifications) {
|
|
3137
|
-
let added = 0;
|
|
3138
|
-
let replaced = 0;
|
|
3139
|
-
for (const c of classifications) {
|
|
3140
|
-
const toolName = `${c.specId}.${c.operationId}`;
|
|
3141
|
-
const prev = this.byToolName.get(toolName);
|
|
3142
|
-
this.byToolName.set(toolName, c);
|
|
3143
|
-
if (prev) replaced++;
|
|
3144
|
-
else added++;
|
|
3145
|
-
}
|
|
3146
|
-
return { added, replaced };
|
|
3147
|
-
}
|
|
3148
|
-
/** Look up the classification for a tool. */
|
|
3149
|
-
lookup(toolName) {
|
|
3150
|
-
return this.byToolName.get(toolName);
|
|
3151
|
-
}
|
|
3152
|
-
/** Remove a single classification. Returns true if it was present. */
|
|
3153
|
-
unregister(toolName) {
|
|
3154
|
-
return this.byToolName.delete(toolName);
|
|
3155
|
-
}
|
|
3156
|
-
/** Drop every registration; used by the loader on full re-deploy. */
|
|
3157
|
-
clear() {
|
|
3158
|
-
this.byToolName.clear();
|
|
3159
|
-
}
|
|
3160
|
-
/** Current size — handy for tests and metrics. */
|
|
3161
|
-
size() {
|
|
3162
|
-
return this.byToolName.size;
|
|
3163
|
-
}
|
|
3164
|
-
/** Snapshot of every (toolName, classification) pair in insertion order. */
|
|
3165
|
-
getAll() {
|
|
3166
|
-
const out = [];
|
|
3167
|
-
for (const [toolName, classification] of this.byToolName.entries()) {
|
|
3168
|
-
out.push({ toolName, classification });
|
|
3169
|
-
}
|
|
3170
|
-
return out;
|
|
3171
|
-
}
|
|
3172
|
-
/**
|
|
3173
|
-
* Convenience: in one step, look up the classification for `toolName`
|
|
3174
|
-
* and build the resource-change notification it should produce after a
|
|
3175
|
-
* successful call with `args`. Returns `{ notification: null }` when the
|
|
3176
|
-
* tool has no classification (e.g. it's not an openapi-derived tool) or
|
|
3177
|
-
* when its classification has no `emit`.
|
|
3178
|
-
*
|
|
3179
|
-
* The dispatcher is expected to call this from a successful-call hook
|
|
3180
|
-
* and forward the resulting notification through whichever notification
|
|
3181
|
-
* channel the host provides.
|
|
3182
|
-
*/
|
|
3183
|
-
buildNotificationForCall(toolName, args) {
|
|
3184
|
-
const classification = this.byToolName.get(toolName);
|
|
3185
|
-
if (!classification) return { notification: null, reason: "no-emit" };
|
|
3186
|
-
return buildResourceChangeNotification(classification, args);
|
|
3187
|
-
}
|
|
3188
|
-
};
|
|
3189
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
3190
|
-
0 && (module.exports = {
|
|
3191
|
-
BundlePinnedError,
|
|
3192
|
-
BundlePushJwtVerifier,
|
|
3193
|
-
BundleStore,
|
|
3194
|
-
ClassificationRegistry,
|
|
3195
|
-
FilesystemSkillsSource,
|
|
3196
|
-
Hs256AuditSigner,
|
|
3197
|
-
InlineSource,
|
|
3198
|
-
MemoryAuditStore,
|
|
3199
|
-
NpmSource,
|
|
3200
|
-
OverlayParseError,
|
|
3201
|
-
Rs256AuditSigner,
|
|
3202
|
-
SKILL_AUDIT_ERROR_MESSAGE_MAX,
|
|
3203
|
-
SKILL_AUDIT_GENESIS_PREV_HASH,
|
|
3204
|
-
SKILL_AUDIT_KEYS,
|
|
3205
|
-
SKILL_AUDIT_QUEUE_MAX,
|
|
3206
|
-
SaasPullSource,
|
|
3207
|
-
SkillAuditWriter,
|
|
3208
|
-
SkillAuditWriterToken,
|
|
3209
|
-
SkillDependencyCycleError,
|
|
3210
|
-
SkillDependencyInvariantError,
|
|
3211
|
-
SkillDependencyMissingError,
|
|
3212
|
-
StaticSource,
|
|
3213
|
-
StorageAdapterAuditStore,
|
|
3214
|
-
WebhookReplayGuard,
|
|
3215
|
-
applyClassificationOverrides,
|
|
3216
|
-
applyEnvironmentOverlay,
|
|
3217
|
-
buildFrontMcpConfigFromManifest,
|
|
3218
|
-
buildKnownOps,
|
|
3219
|
-
buildResourceChangeNotification,
|
|
3220
|
-
bundleDigest,
|
|
3221
|
-
bundleSkillToActions,
|
|
3222
|
-
bundleSourceSchema,
|
|
3223
|
-
canonicalize,
|
|
3224
|
-
canonicalizeRecordForSigning,
|
|
3225
|
-
classifyOne,
|
|
3226
|
-
classifyOperations,
|
|
3227
|
-
compileSkilledBundleFromOpenApi,
|
|
3228
|
-
createBundleSource,
|
|
3229
|
-
createSkillAuditMetrics,
|
|
3230
|
-
crossValidate,
|
|
3231
|
-
crossValidateManifest,
|
|
3232
|
-
dedupeOpReferences,
|
|
3233
|
-
defaultAuditSignatureVerifier,
|
|
3234
|
-
deployManifestSchema,
|
|
3235
|
-
diffBundles,
|
|
3236
|
-
extractOpReferences,
|
|
3237
|
-
formatDiffSummary,
|
|
3238
|
-
hasSkillAuditFactory,
|
|
3239
|
-
linkRecord,
|
|
3240
|
-
loadFilesystemSkill,
|
|
3241
|
-
nextPrevHash,
|
|
3242
|
-
npmSourceSchema,
|
|
3243
|
-
parseOverlay,
|
|
3244
|
-
parseSkillFrontmatter,
|
|
3245
|
-
renderResourceUri,
|
|
3246
|
-
resolveSkillLoadOrder,
|
|
3247
|
-
resolvedBundleSchema,
|
|
3248
|
-
saasSourceSchema,
|
|
3249
|
-
setSkillAuditFactory,
|
|
3250
|
-
signatureKeySchema,
|
|
3251
|
-
staticSourceSchema,
|
|
3252
|
-
validateOpReferences,
|
|
3253
|
-
verifyBundleSignature,
|
|
3254
|
-
verifyChain
|
|
3255
|
-
});
|
|
1
|
+
"use strict";var __create=Object.create;var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __getProtoOf=Object.getPrototypeOf;var __hasOwnProp=Object.prototype.hasOwnProperty;var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:true})};var __copyProps=(to,from,except,desc)=>{if(from&&typeof from==="object"||typeof from==="function"){for(let key of __getOwnPropNames(from))if(!__hasOwnProp.call(to,key)&&key!==except)__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable})}return to};var __toESM=(mod,isNodeMode,target)=>(target=mod!=null?__create(__getProtoOf(mod)):{},__copyProps(isNodeMode||!mod||!mod.__esModule?__defProp(target,"default",{value:mod,enumerable:true}):target,mod));var __toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:true}),mod);var skills_exports={};__export(skills_exports,{BundlePinnedError:()=>BundlePinnedError,BundlePushJwtVerifier:()=>BundlePushJwtVerifier,BundleStore:()=>BundleStore,ClassificationRegistry:()=>ClassificationRegistry,FilesystemSkillsSource:()=>FilesystemSkillsSource,Hs256AuditSigner:()=>Hs256AuditSigner,InlineSource:()=>InlineSource,MemoryAuditStore:()=>MemoryAuditStore,NpmSource:()=>NpmSource,OverlayParseError:()=>OverlayParseError,Rs256AuditSigner:()=>Rs256AuditSigner,SKILL_AUDIT_ERROR_MESSAGE_MAX:()=>SKILL_AUDIT_ERROR_MESSAGE_MAX,SKILL_AUDIT_GENESIS_PREV_HASH:()=>SKILL_AUDIT_GENESIS_PREV_HASH,SKILL_AUDIT_KEYS:()=>SKILL_AUDIT_KEYS,SKILL_AUDIT_QUEUE_MAX:()=>SKILL_AUDIT_QUEUE_MAX,SaasPullSource:()=>SaasPullSource,SkillAuditWriter:()=>SkillAuditWriter,SkillAuditWriterToken:()=>SkillAuditWriterToken,SkillDependencyCycleError:()=>SkillDependencyCycleError,SkillDependencyInvariantError:()=>SkillDependencyInvariantError,SkillDependencyMissingError:()=>SkillDependencyMissingError,StaticSource:()=>StaticSource,StorageAdapterAuditStore:()=>StorageAdapterAuditStore,WebhookReplayGuard:()=>WebhookReplayGuard,applyClassificationOverrides:()=>applyClassificationOverrides,applyEnvironmentOverlay:()=>applyEnvironmentOverlay,buildFrontMcpConfigFromManifest:()=>buildFrontMcpConfigFromManifest,buildKnownOps:()=>buildKnownOps,buildResourceChangeNotification:()=>buildResourceChangeNotification,bundleDigest:()=>bundleDigest,bundleSkillToActions:()=>bundleSkillToActions,bundleSourceSchema:()=>bundleSourceSchema,canonicalize:()=>canonicalize,canonicalizeRecordForSigning:()=>canonicalizeRecordForSigning,classifyOne:()=>classifyOne,classifyOperations:()=>classifyOperations,compileSkilledBundleFromOpenApi:()=>compileSkilledBundleFromOpenApi,createBundleSource:()=>createBundleSource,createSkillAuditMetrics:()=>createSkillAuditMetrics,crossValidate:()=>crossValidate,crossValidateManifest:()=>crossValidateManifest,dedupeOpReferences:()=>dedupeOpReferences,defaultAuditSignatureVerifier:()=>defaultAuditSignatureVerifier,deployManifestSchema:()=>deployManifestSchema,diffBundles:()=>diffBundles,extractOpReferences:()=>extractOpReferences,formatDiffSummary:()=>formatDiffSummary,hasSkillAuditFactory:()=>import_sdk.hasSkillAuditFactory,linkRecord:()=>linkRecord,loadFilesystemSkill:()=>loadFilesystemSkill,nextPrevHash:()=>nextPrevHash,npmSourceSchema:()=>npmSourceSchema,parseOverlay:()=>parseOverlay,parseSkillFrontmatter:()=>parseSkillFrontmatter,renderResourceUri:()=>renderResourceUri,resolveSkillLoadOrder:()=>resolveSkillLoadOrder,resolvedBundleSchema:()=>resolvedBundleSchema,saasSourceSchema:()=>saasSourceSchema,setSkillAuditFactory:()=>import_sdk.setSkillAuditFactory,signatureKeySchema:()=>signatureKeySchema,staticSourceSchema:()=>staticSourceSchema,validateOpReferences:()=>validateOpReferences,verifyBundleSignature:()=>verifyBundleSignature,verifyChain:()=>verifyChain});module.exports=__toCommonJS(skills_exports);function bundleSkillToActions(skill,ops){return skill.operationIds.map(opId=>{const op=ops[opId];if(!op){throw new Error(`bundleSkillToActions: skill "${skill.id}" references unknown operationId "${opId}"`)}const action={actionId:op.operationId,summary:op.summary??`${op.httpMethod} ${op.pathTemplate}`,...op.description!==void 0&&{description:op.description},inputJsonSchema:op.inputSchema,outputJsonSchema:op.outputSchema,...op.requiredAuthorities!==void 0&&{requiredAuthorities:op.requiredAuthorities}};return action})}var import_lazy_zod=require("@frontmcp/lazy-zod");var httpMethodSchema=import_lazy_zod.z.enum(["GET","POST","PUT","PATCH","DELETE","HEAD"]);var RFC7230_TOKEN=/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;var pathTemplateRegex=/^\/[^\s?#]*$/;var FORBIDDEN_PATH_FRAGMENTS=["..","\\","`","$(","${"];var pathTemplateSchema=import_lazy_zod.z.string().min(1).max(2048).refine(v=>pathTemplateRegex.test(v),{message:"pathTemplate must start with `/` and contain no whitespace, `?`, or `#`"}).refine(v=>!FORBIDDEN_PATH_FRAGMENTS.some(bad=>v.includes(bad)),{message:"pathTemplate contains a forbidden character or fragment (`..`, backtick, `$(`, etc.)"});var jsonSchemaShape=import_lazy_zod.z.record(import_lazy_zod.z.string(),import_lazy_zod.z.unknown());var authoritiesPolicySchema=import_lazy_zod.z.record(import_lazy_zod.z.string(),import_lazy_zod.z.unknown());var authBindingSchema=import_lazy_zod.z.discriminatedUnion("kind",[import_lazy_zod.z.object({kind:import_lazy_zod.z.literal("none")}).strict(),import_lazy_zod.z.object({kind:import_lazy_zod.z.literal("bearer"),vaultRef:import_lazy_zod.z.string().min(1).max(256),passthroughCallerToken:import_lazy_zod.z.boolean().optional()}).strict(),import_lazy_zod.z.object({kind:import_lazy_zod.z.literal("apiKey"),in:import_lazy_zod.z.enum(["header","query"]),name:import_lazy_zod.z.string().min(1).max(128).regex(RFC7230_TOKEN,"apiKey name must match RFC 7230 token grammar"),vaultRef:import_lazy_zod.z.string().min(1).max(256)}).strict(),import_lazy_zod.z.object({kind:import_lazy_zod.z.literal("oauth2"),flow:import_lazy_zod.z.literal("client_credentials"),vaultRef:import_lazy_zod.z.string().min(1).max(256)}).strict()]);var serviceDescriptorSchema=import_lazy_zod.z.object({id:import_lazy_zod.z.string().min(1).max(128).regex(/^[a-zA-Z0-9_-]+$/,"service id must be alphanumeric / `-` / `_`"),baseUrl:import_lazy_zod.z.string().url().max(2048),description:import_lazy_zod.z.string().max(4096).optional()}).strict();var parameterMapperSchema=import_lazy_zod.z.object({inputKey:import_lazy_zod.z.string().min(1).max(256),type:import_lazy_zod.z.enum(["path","query","header","cookie","body"]),key:import_lazy_zod.z.string().min(1).max(256),required:import_lazy_zod.z.boolean().optional(),style:import_lazy_zod.z.string().max(64).optional(),explode:import_lazy_zod.z.boolean().optional(),serialization:import_lazy_zod.z.record(import_lazy_zod.z.string(),import_lazy_zod.z.unknown()).optional(),security:import_lazy_zod.z.record(import_lazy_zod.z.string(),import_lazy_zod.z.unknown()).optional()}).passthrough().refine(mapper=>mapper.type!=="header"&&mapper.type!=="cookie"||RFC7230_TOKEN.test(mapper.key),{message:"a header or cookie mapper key must match RFC 7230 token grammar",path:["key"]});var operationDescriptorSchema=import_lazy_zod.z.object({operationId:import_lazy_zod.z.string().min(1).max(256).regex(/^[a-zA-Z0-9_.\-:]+$/,"operationId must be alphanumeric / `-` / `_` / `.` / `:`"),serviceId:import_lazy_zod.z.string().min(1).max(128),httpMethod:httpMethodSchema,pathTemplate:pathTemplateSchema,inputSchema:jsonSchemaShape,outputSchema:jsonSchemaShape,mapper:import_lazy_zod.z.array(parameterMapperSchema).max(256),authBindingRef:import_lazy_zod.z.string().min(1).max(128),requiredAuthorities:authoritiesPolicySchema.optional(),public:import_lazy_zod.z.boolean().optional(),maxResponseBytes:import_lazy_zod.z.number().int().positive().max(64*1024*1024).optional(),timeoutMs:import_lazy_zod.z.number().int().positive().max(6e5).optional(),summary:import_lazy_zod.z.string().max(512).optional(),description:import_lazy_zod.z.string().max(8192).optional()}).strict();var bundledSkillSchema=import_lazy_zod.z.object({id:import_lazy_zod.z.string().min(1).max(128).regex(/^[a-zA-Z0-9_.-]+$/),name:import_lazy_zod.z.string().min(1).max(256),description:import_lazy_zod.z.string().max(2048),instructions:import_lazy_zod.z.string().max(256*1024),tags:import_lazy_zod.z.array(import_lazy_zod.z.string().max(64)).max(64).optional(),operationIds:import_lazy_zod.z.array(import_lazy_zod.z.string().min(1).max(256)).min(0).max(2e3),requiredAuthorities:authoritiesPolicySchema.optional(),requires:import_lazy_zod.z.array(import_lazy_zod.z.string().min(1).max(128)).max(64).optional()}).strict();var integritySchema=import_lazy_zod.z.object({alg:import_lazy_zod.z.enum(["RS256","EdDSA"]),keyId:import_lazy_zod.z.string().min(1).max(256),signature:import_lazy_zod.z.string().min(1).max(8192),digest:import_lazy_zod.z.string().min(1).max(256)}).strict();var resolvedBundleSchema=import_lazy_zod.z.object({schemaVersion:import_lazy_zod.z.literal(1),bundleId:import_lazy_zod.z.string().min(1).max(256).regex(/^[a-zA-Z0-9_.\-:]+$/),version:import_lazy_zod.z.string().min(1).max(256),generatedAt:import_lazy_zod.z.string().min(1).refine(v=>!Number.isNaN(Date.parse(v)),{message:"generatedAt must be ISO 8601 parsable"}),sourceDigest:import_lazy_zod.z.string().min(1).max(256).regex(/^[a-fA-F0-9]+$/,"sourceDigest must be a hex sha256"),services:import_lazy_zod.z.array(serviceDescriptorSchema).min(1).max(64),authBindings:import_lazy_zod.z.record(import_lazy_zod.z.string().min(1).max(128),authBindingSchema),skills:import_lazy_zod.z.array(bundledSkillSchema).max(1e3),operations:import_lazy_zod.z.record(import_lazy_zod.z.string().min(1).max(256),operationDescriptorSchema),integrity:integritySchema.optional()}).strict();function crossValidate(bundle){const errors=[];const serviceIds=new Set(bundle.services.map(s=>s.id));const authRefs=new Set(Object.keys(bundle.authBindings));const opIds=new Set(Object.keys(bundle.operations));for(const[opId,op]of Object.entries(bundle.operations)){if(op.operationId!==opId){errors.push(`operation key "${opId}" does not match operationId "${op.operationId}"`)}if(!serviceIds.has(op.serviceId)){errors.push(`operation "${opId}" references unknown service "${op.serviceId}"`)}if(!authRefs.has(op.authBindingRef)){errors.push(`operation "${opId}" references unknown authBinding "${op.authBindingRef}"`)}}const seenSkillIds=new Set;for(const skill of bundle.skills){if(seenSkillIds.has(skill.id)){errors.push(`duplicate skill id "${skill.id}"`)}seenSkillIds.add(skill.id);for(const opId of skill.operationIds){if(!opIds.has(opId)){errors.push(`skill "${skill.id}" references unknown operationId "${opId}"`)}}}return errors.length===0?{ok:true}:{ok:false,errors}}var yaml=__toESM(require("js-yaml"));var OVERLAY_BUNDLE_KEY="x-frontmcp-bundle";var OverlayParseError=class extends Error{constructor(message,errors){super(message);this.errors=errors;this.name="OverlayParseError"}errors};function parseRaw(input){switch(input.kind){case"yaml":try{return yaml.load(input.content)}catch(e){throw new OverlayParseError(`Overlay YAML parse failed: ${e.message}`)}case"json":try{return JSON.parse(input.content)}catch(e){throw new OverlayParseError(`Overlay JSON parse failed: ${e.message}`)}case"object":return input.content}}function extractBundle(overlay){if(overlay===null||typeof overlay!=="object"){throw new OverlayParseError("Overlay root must be an object")}const root=overlay;if(OVERLAY_BUNDLE_KEY in root){return root[OVERLAY_BUNDLE_KEY]}const info=root["info"];if(info&&typeof info==="object"&&OVERLAY_BUNDLE_KEY in info){return info[OVERLAY_BUNDLE_KEY]}if("schemaVersion"in root&&"bundleId"in root){return root}throw new OverlayParseError("Overlay does not contain `info.x-frontmcp-bundle` or a bare bundle object with schemaVersion + bundleId")}function parseOverlay(input){const raw=parseRaw(input);const bundleCandidate=extractBundle(raw);const result=resolvedBundleSchema.safeParse(bundleCandidate);if(!result.success){const errors=result.error.issues.map(i=>`${i.path.join(".")||"<root>"}: ${i.message}`);throw new OverlayParseError(`Bundle schema validation failed (${errors.length} issue(s))`,errors)}const cross=crossValidate(result.data);if(!cross.ok){throw new OverlayParseError(`Bundle cross-reference validation failed (${cross.errors.length} issue(s))`,cross.errors)}return result.data}var opEqual=(a,b)=>a.operationId===b.operationId&&a.serviceId===b.serviceId&&a.httpMethod===b.httpMethod&&a.pathTemplate===b.pathTemplate&&a.authBindingRef===b.authBindingRef&&JSON.stringify(a.mapper??null)===JSON.stringify(b.mapper??null)&&JSON.stringify(a.inputSchema)===JSON.stringify(b.inputSchema)&&JSON.stringify(a.outputSchema)===JSON.stringify(b.outputSchema)&&JSON.stringify(a.requiredAuthorities??null)===JSON.stringify(b.requiredAuthorities??null)&&(a.public??false)===(b.public??false)&&a.maxResponseBytes===b.maxResponseBytes&&a.timeoutMs===b.timeoutMs;var skillEqual=(a,b)=>a.id===b.id&&a.name===b.name&&a.description===b.description&&a.instructions===b.instructions&&JSON.stringify(a.tags??[])===JSON.stringify(b.tags??[])&&JSON.stringify(a.operationIds)===JSON.stringify(b.operationIds)&&JSON.stringify(a.requiredAuthorities??null)===JSON.stringify(b.requiredAuthorities??null);function diffBundles(oldBundle,newBundle){const oldSkills=new Map((oldBundle?.skills??[]).map(s=>[s.id,s]));const newSkills=new Map(newBundle.skills.map(s=>[s.id,s]));const addedSkillIds=[];const removedSkillIds=[];const changedSkillIds=[];for(const[id,skill]of newSkills){const prev=oldSkills.get(id);if(!prev)addedSkillIds.push(id);else if(!skillEqual(prev,skill))changedSkillIds.push(id)}for(const id of oldSkills.keys()){if(!newSkills.has(id))removedSkillIds.push(id)}const oldOps=oldBundle?.operations??{};const newOps=newBundle.operations;const addedOperationIds=[];const removedOperationIds=[];const changedOperationIds=[];for(const[id,op]of Object.entries(newOps)){const prev=oldOps[id];if(!prev)addedOperationIds.push(id);else if(!opEqual(prev,op))changedOperationIds.push(id)}for(const id of Object.keys(oldOps)){if(!(id in newOps))removedOperationIds.push(id)}const isNoOp=!!oldBundle&&oldBundle.version===newBundle.version&&addedSkillIds.length===0&&removedSkillIds.length===0&&changedSkillIds.length===0&&addedOperationIds.length===0&&removedOperationIds.length===0&&changedOperationIds.length===0;return{addedSkillIds,removedSkillIds,changedSkillIds,addedOperationIds,removedOperationIds,changedOperationIds,isNoOp}}function formatDiffSummary(diff){if(diff.isNoOp)return"no-op";return[`+${diff.addedSkillIds.length}/-${diff.removedSkillIds.length}/~${diff.changedSkillIds.length} skills`,`+${diff.addedOperationIds.length}/-${diff.removedOperationIds.length}/~${diff.changedOperationIds.length} ops`].join(", ")}var KNOWN_BUNDLE_SOURCES=["static","npm","saas-pull","webhook","filesystem","unknown"];function normalizeBundleSource(value){if(typeof value!=="string")return"unknown";return KNOWN_BUNDLE_SOURCES.includes(value)?value:"unknown"}function classifySwapError(err){if(err instanceof BundlePinnedError)return"pinned";if(err instanceof Error){const name=err.name;const msg=err.message.toLowerCase();if(name==="BundlePinnedError")return"pinned";if(msg.includes("bundle is required")||msg.includes("malformed"))return"invalid_bundle";if(msg.includes("listener"))return"listener_error"}return"unknown"}var BundleStore=class{active;listeners=new Set;history=[];pinnedVersion;historySize;telemetry;pullsCounter;constructor(options={}){const requested=options.historySize??3;if(!Number.isInteger(requested)||requested<1){throw new Error("BundleStore: historySize must be a positive integer")}this.historySize=requested;this.telemetry=options.telemetry;this.pullsCounter=this.telemetry?.createCounter("frontmcp_skills_bundle_pulls_total","Number of bundle pulls (swaps) attempted, partitioned by status / source.")}current(){return this.active}swap(next,source){if(!next){throw new Error("BundleStore.swap: bundle is required")}const sourceLabel=normalizeBundleSource(source);if(this.telemetry){const span=this.telemetry.startSpan("skill.bundle.swap",{source:sourceLabel,bundle_id:next.bundleId,version:next.version,skill_count:next.skills.length,...this.active?.version?{from_version:this.active.version}:{}});try{if(this.pinnedVersion!==void 0){throw new BundlePinnedError(this.pinnedVersion)}const diff=this.commit(next,"normal");span.setAttributes({status:"ok",is_no_op:diff.isNoOp});span.end();this.pullsCounter?.inc(1,{status:"ok",source:sourceLabel});return diff}catch(err){const reason=classifySwapError(err);if(span.endWithError){span.endWithError(err instanceof Error?err:String(err))}else{span.recordError?.(err instanceof Error?err:new Error(String(err)));span.end()}this.pullsCounter?.inc(1,{status:"error",source:sourceLabel,reason});throw err}}if(this.pinnedVersion!==void 0){throw new BundlePinnedError(this.pinnedVersion)}return this.commit(next,"normal")}pin(version){if(this.active&&this.active.version===version){this.pinnedVersion=version;return void 0}const entry=this.history.find(h=>h.bundle.version===version);if(!entry){throw new Error(`BundleStore.pin: version "${version}" not in history`)}const diff=this.commit(entry.bundle,"pin");this.pinnedVersion=version;return diff}rollback(){const prior=this.history[this.history.length-1];if(!prior){throw new Error("BundleStore.rollback: no prior bundle in history")}this.pinnedVersion=void 0;return this.commit(prior.bundle,"rollback")}unpin(){this.pinnedVersion=void 0}isPinned(){return this.pinnedVersion!==void 0}pinned(){return this.pinnedVersion}historySnapshot(){return this.history.map(h=>({version:h.bundle.version,bundleId:h.bundle.bundleId,validatedAt:h.validatedAt}))}subscribe(fn){this.listeners.add(fn);return()=>{this.listeners.delete(fn)}}reset(){this.active=void 0;this.history=[];this.pinnedVersion=void 0}commit(next,reason){const previous=this.active;const diff=diffBundles(previous,next);if(previous){const lastInHistory=this.history[this.history.length-1];if(!lastInHistory||lastInHistory.bundle.version!==previous.version){this.history.push({bundle:previous,validatedAt:Date.now()});if(this.history.length>this.historySize){this.history.splice(0,this.history.length-this.historySize)}}}this.active=next;for(const fn of this.listeners){try{fn({previous,current:next,diff,reason})}catch{}}return diff}};var BundlePinnedError=class extends Error{constructor(pinnedVersion){super(`BundleStore is pinned to version "${pinnedVersion}"; new swaps are rejected until unpin().`);this.pinnedVersion=pinnedVersion;this.name="BundlePinnedError"}pinnedVersion};var SkillDependencyCycleError=class extends Error{constructor(cycle){super(`skill dependency cycle: ${cycle.join(" -> ")}`);this.cycle=cycle;this.name="SkillDependencyCycleError"}cycle};var SkillDependencyMissingError=class extends Error{constructor(skillId,missingId){super(`skill "${skillId}" requires unknown skill "${missingId}"`);this.skillId=skillId;this.missingId=missingId;this.name="SkillDependencyMissingError"}skillId;missingId};var SkillDependencyInvariantError=class extends Error{constructor(message){super(message);this.name="SkillDependencyInvariantError"}};function resolveSkillLoadOrder(skills){const byId=new Map;for(const skill of skills){if(byId.has(skill.id)){byId.set(skill.id,skill);continue}byId.set(skill.id,skill)}const inDegree=new Map;const requiredBy=new Map;for(const id of byId.keys()){inDegree.set(id,0);requiredBy.set(id,[])}for(const skill of byId.values()){const deps=skill.requires??[];for(const depId of deps){if(!byId.has(depId)){throw new SkillDependencyMissingError(skill.id,depId)}inDegree.set(skill.id,(inDegree.get(skill.id)??0)+1);const reverse=requiredBy.get(depId)??[];reverse.push(skill.id);requiredBy.set(depId,reverse)}}const ready=[...byId.keys()].filter(id=>inDegree.get(id)===0).sort();const order=[];while(ready.length>0){const next=ready.shift();if(next===void 0){throw new SkillDependencyInvariantError("resolveSkillLoadOrder: invariant broken \u2014 ready queue drained mid-iteration")}const nextSkill=byId.get(next);if(!nextSkill){throw new SkillDependencyInvariantError(`resolveSkillLoadOrder: invariant broken \u2014 id "${next}" missing from byId map`)}order.push(nextSkill);for(const dependent of requiredBy.get(next)??[]){const remaining=(inDegree.get(dependent)??0)-1;inDegree.set(dependent,remaining);if(remaining===0){let i=0;while(i<ready.length&&ready[i]<dependent)i++;ready.splice(i,0,dependent)}}}if(order.length===byId.size){return order}const remainingIds=[...byId.keys()].filter(id=>(inDegree.get(id)??0)>0);const cycle=findCycle(remainingIds,byId);throw new SkillDependencyCycleError(cycle)}function findCycle(remaining,byId){const adj=new Map;for(const id of remaining){adj.set(id,(byId.get(id)?.requires??[]).filter(d=>remaining.includes(d)))}const WHITE=0;const GRAY=1;const BLACK=2;const color=new Map;for(const id of remaining)color.set(id,WHITE);const stack=[];function dfs(node){color.set(node,GRAY);stack.push(node);for(const next of adj.get(node)??[]){const c=color.get(next);if(c===GRAY){const startIdx=stack.indexOf(next);return[...stack.slice(startIdx),next]}if(c===WHITE){const found=dfs(next);if(found)return found}}color.set(node,BLACK);stack.pop();return void 0}for(const id of remaining){if(color.get(id)===WHITE){const cycle=dfs(id);if(cycle)return cycle}}return remaining}var import_lazy_zod2=require("@frontmcp/lazy-zod");var httpsUrl=import_lazy_zod2.z.string().url().refine(v=>{try{return new URL(v).protocol==="https:"}catch{return false}},{message:"must use https://"});var staticSourceSchema=import_lazy_zod2.z.object({type:import_lazy_zod2.z.literal("static"),path:import_lazy_zod2.z.string().min(1),watch:import_lazy_zod2.z.boolean().default(false)});var npmSourceSchema=import_lazy_zod2.z.object({type:import_lazy_zod2.z.literal("npm"),packageName:import_lazy_zod2.z.string().min(1),exportName:import_lazy_zod2.z.string().optional(),verifyProvenance:import_lazy_zod2.z.boolean().default(true)});var saasSourceSchema=import_lazy_zod2.z.object({type:import_lazy_zod2.z.literal("saas"),endpoint:httpsUrl,authToken:import_lazy_zod2.z.string().min(1),expectedAudience:import_lazy_zod2.z.string().min(1),pollIntervalMs:import_lazy_zod2.z.number().int().positive().default(3e5),enableWebhook:import_lazy_zod2.z.boolean().default(false),jwksUrl:httpsUrl,expectedIssuer:import_lazy_zod2.z.string().min(1)});var inlineSourceSchema=import_lazy_zod2.z.object({type:import_lazy_zod2.z.literal("inline"),content:import_lazy_zod2.z.unknown()});var bundleSourceSchema=import_lazy_zod2.z.discriminatedUnion("type",[staticSourceSchema,npmSourceSchema,saasSourceSchema,inlineSourceSchema]);var signatureKeySchema=import_lazy_zod2.z.object({keyId:import_lazy_zod2.z.string().min(1),alg:import_lazy_zod2.z.enum(["RS256","EdDSA"]),publicKeyPem:import_lazy_zod2.z.string().min(1)});var InlineSource=class{constructor(options,logger){this.options=options;this.logger=logger}options;logger;id="inline:bundle";listeners=new Set;async start(){const bundle=this.parse();for(const fn of this.listeners){try{fn(bundle)}catch(e){this.logger.warn(`[inline-source] listener threw: ${e.message}`)}}}onChange(listener){this.listeners.add(listener);return()=>{this.listeners.delete(listener)}}async stop(){this.listeners.clear()}parse(){return parseOverlay({kind:"object",content:this.options.content})}};var npmDynamicImport;var NpmSource=class{constructor(options,logger){this.options=options;this.logger=logger;this.id=`npm:${this.options.packageName}${this.options.exportName?`#${this.options.exportName}`:""}`}options;logger;id;listeners=new Set;cached;async start(){const bundle=await this.loadBundleFromPackage();this.cached=bundle;this.notify(bundle)}onChange(listener){this.listeners.add(listener);if(this.cached){try{listener(this.cached)}catch(e){this.logger.warn(`[npm-source] listener threw on replay: ${e.message}`)}}return()=>{this.listeners.delete(listener)}}async stop(){this.listeners.clear()}async loadBundleFromPackage(){if(this.options.verifyProvenance){throw new Error(`npm source "${this.options.packageName}": verifyProvenance is true, but npm provenance verification is not implemented, so the package was not loaded. Set verifyProvenance: false to load it without a provenance check, only for a package you trust: importing it runs its code before any bundle signature is checked, so pin its exact version and install it from a lockfile with integrity hashes.`)}const mod=await this.dynamicImport(this.options.packageName);const exportName=this.options.exportName??"default";const modRecord=mod;const exported=modRecord[exportName]??(exportName==="default"?mod:void 0);if(exported===void 0||exported===null){throw new Error(`npm source "${this.options.packageName}": export "${exportName}" not found on the package`)}return parseOverlay({kind:"object",content:exported})}dynamicImport(specifier){npmDynamicImport??=new Function("s","return import(s)");return npmDynamicImport(specifier)}notify(bundle){for(const fn of this.listeners){try{fn(bundle)}catch(e){this.logger.warn(`[npm-source] listener threw: ${e.message}`)}}}};var import_auth=require("@frontmcp/auth");var import_utils=require("@frontmcp/utils");var DEFAULT_CACHE_DIR=".frontmcp/skilled-openapi";var DEFAULT_PULL_TIMEOUT_MS=3e4;var USER_AGENT="frontmcp-skilled-openapi";var SaasPullTokenRejectedError=class extends Error{constructor(reason){super(`[saas-source] pull token rejected: ${reason}`);this.name="SaasPullTokenRejectedError"}};function claimValues(claim){if(Array.isArray(claim))return claim;return claim===void 0?[]:[claim]}var PUBLIC_KEY_MEMBERS=new Map([["RSA",["n","e"]],["EC",["crv","x","y"]],["OKP",["crv","x"]]]);function isUsableSigningKey(key){if(typeof key!=="object"||key===null)return false;const jwk=key;const members=typeof jwk["kty"]==="string"?PUBLIC_KEY_MEMBERS.get(jwk["kty"]):void 0;if(!members||jwk["use"]!==void 0&&jwk["use"]!=="sig")return false;return members.every(m=>typeof jwk[m]==="string"&&jwk[m]!=="")}var SaasPullSource=class{constructor(options,cacheDir,logger,deps={}){this.options=options;this.cacheDir=cacheDir;this.logger=logger;this.deps=deps;this.id=`saas:${this.options.endpoint}`}options;cacheDir;logger;deps;id;listeners=new Set;pollHandle;inFlight=false;stopped=false;jwksService=new import_auth.JwksService({});async start(){if(this.stopped||this.inFlight){this.schedulePoll();return}this.inFlight=true;let bundle;try{try{bundle=await this.fetchOnce();await this.persistCache(bundle)}catch(e){if(e instanceof SaasPullTokenRejectedError)throw e;this.logger.warn(`[saas-source] initial pull failed (${e.message}); attempting cached bundle fallback`);bundle=await this.loadCache();if(!bundle){throw new Error(`[saas-source] initial pull failed and no cached bundle is available ${this.cacheLocation()}: ${e.message}`,{cause:e})}this.logger.warn(`[saas-source] using cached bundle "${bundle.bundleId}@${bundle.version}"`)}this.notify(bundle)}finally{this.inFlight=false}this.schedulePoll()}onChange(listener){this.listeners.add(listener);return()=>{this.listeners.delete(listener)}}async stop(){this.stopped=true;if(this.pollHandle){clearTimeout(this.pollHandle);this.pollHandle=void 0}this.listeners.clear()}schedulePoll(){if(this.stopped||this.deps.disablePolling)return;if(this.pollHandle)clearTimeout(this.pollHandle);this.pollHandle=setTimeout(()=>{void this.pollOnce()},this.options.pollIntervalMs);this.pollHandle.unref?.()}async refresh(){if(this.stopped||this.inFlight)return void 0;this.inFlight=true;try{const bundle=await this.fetchOnce();await this.persistCache(bundle);this.notify(bundle);return bundle}finally{this.inFlight=false}}async pollOnce(){if(this.stopped)return;if(this.inFlight){this.schedulePoll();return}this.inFlight=true;try{const bundle=await this.fetchOnce();await this.persistCache(bundle);this.notify(bundle)}catch(e){if(e instanceof SaasPullTokenRejectedError){this.logger.error(`[saas-source] poll refused: ${e.message}; keeping the current bundle`)}else{this.logger.warn(`[saas-source] poll failed: ${e.message}`)}}finally{this.inFlight=false;this.schedulePoll()}}async httpGet(url,headers){const controller=new AbortController;const timer=setTimeout(()=>controller.abort(),DEFAULT_PULL_TIMEOUT_MS);try{const res=await fetch(url,{method:"GET",headers,signal:controller.signal,redirect:"manual"});if((0,import_utils.isRedirectResponse)(res)){throw new Error(`pull endpoint returned a redirect (${res.status}); not followed to protect the bearer token`)}const body=await res.text();return{status:res.status,body}}finally{clearTimeout(timer)}}async verifyPullToken(){const{jwksUrl,expectedIssuer,expectedAudience,authToken}=this.options;const{status,body}=await this.httpGet(jwksUrl,{Accept:"application/json","User-Agent":USER_AGENT});if(status<200||status>=300){throw new Error(`JWKS ${jwksUrl} responded ${status}`)}let jwks;try{jwks=JSON.parse(body)}catch{throw new Error(`JWKS ${jwksUrl} is not valid JSON`)}if(!jwks||!Array.isArray(jwks.keys)||jwks.keys.length===0){throw new Error(`JWKS ${jwksUrl} has no keys`)}const signingKeys=jwks.keys.filter(isUsableSigningKey);if(signingKeys.length===0){throw new Error(`JWKS ${jwksUrl} has no usable signing keys`)}const verified=await this.jwksService.verifyTransparentToken(authToken,[{id:"skilled-openapi-saas",issuerUrl:expectedIssuer,jwks:{keys:signingKeys}}]);if(!verified.ok||!verified.payload){throw new SaasPullTokenRejectedError(`not a valid token from issuer "${expectedIssuer}" signed by a key in ${jwksUrl} (${verified.error??"verification failed"})`)}const payload=verified.payload;if(!claimValues(payload["aud"]).includes(expectedAudience)){throw new SaasPullTokenRejectedError(`its "aud" claim does not include "${expectedAudience}"`)}const resource=claimValues(payload["resource"]);if(resource.length>0&&!resource.includes(expectedAudience)){throw new SaasPullTokenRejectedError(`its "resource" claim does not include "${expectedAudience}"`)}}async fetchOnce(){await this.verifyPullToken();const{status,body}=await this.httpGet(this.options.endpoint,{Authorization:`Bearer ${this.options.authToken}`,Accept:"application/json, application/yaml","User-Agent":USER_AGENT});if(status<200||status>=300){throw new Error(`pull responded ${status}`)}const trimmed=body.trimStart();return parseOverlay(trimmed.startsWith("{")||trimmed.startsWith("[")?{kind:"json",content:body}:{kind:"yaml",content:body})}cacheLocation(){return this.deps.cache?"in the injected bundle cache":`at ${this.cachePath()}`}cachePath(){const dir=this.cacheDir??DEFAULT_CACHE_DIR;return(0,import_utils.pathResolve)(dir,`${this.options.expectedAudience.replace(/[^a-zA-Z0-9_.-]/g,"_")}.json`)}async persistCache(bundle){try{if(this.deps.cache){await this.deps.cache.write(bundle);return}const filePath=this.cachePath();await(0,import_utils.ensureDir)((0,import_utils.dirname)(filePath));await(0,import_utils.writeFile)(filePath,JSON.stringify(bundle))}catch(e){this.logger.warn(`[saas-source] cache persist failed: ${e.message}`)}}async loadCache(){try{if(this.deps.cache){const cached=await this.deps.cache.read();if(cached===void 0||cached===null)return void 0;return parseOverlay({kind:"object",content:cached})}const filePath=this.cachePath();const raw=await(0,import_utils.readFile)(filePath,"utf8");return parseOverlay({kind:"json",content:raw})}catch{return void 0}}notify(bundle){for(const fn of this.listeners){try{fn(bundle)}catch(e){this.logger.warn(`[saas-source] listener threw: ${e.message}`)}}}};var import_utils2=require("@frontmcp/utils");var StaticSource=class{constructor(options,logger){this.options=options;this.logger=logger;this.id=`static:${this.options.path}`}options;logger;id;listeners=new Set;watcher;debounceTimer;stopped=false;async start(){const bundle=await this.fetchOnce();this.notify(bundle);if(this.options.watch){this.beginWatch()}}onChange(listener){this.listeners.add(listener);return()=>{this.listeners.delete(listener)}}async stop(){this.stopped=true;if(this.watcher){this.watcher.close();this.watcher=void 0}if(this.debounceTimer){clearTimeout(this.debounceTimer);this.debounceTimer=void 0}this.listeners.clear()}async fetchOnce(){const absPath=(0,import_utils2.pathResolve)(this.options.path);const raw=await(0,import_utils2.readFile)(absPath,"utf8");const ext=(0,import_utils2.extname)(absPath).toLowerCase();let input;if(ext===".json"){input={kind:"json",content:raw}}else if(ext===".yaml"||ext===".yml"){input={kind:"yaml",content:raw}}else{const trimmed=raw.trimStart();input=trimmed.startsWith("{")||trimmed.startsWith("[")?{kind:"json",content:raw}:{kind:"yaml",content:raw}}return parseOverlay(input)}notify(bundle){for(const fn of this.listeners){try{fn(bundle)}catch(e){this.logger.warn(`[static-source] listener threw: ${e.message}`)}}}beginWatch(){const absPath=(0,import_utils2.pathResolve)(this.options.path);try{this.watcher=(0,import_utils2.watchFile)(absPath,()=>this.scheduleRefresh())}catch(e){this.logger.warn(`[static-source] fs.watch failed (${e.message}); polling at 2s instead`);const tick=()=>{if(this.stopped)return;this.scheduleRefresh();setTimeout(tick,2e3).unref?.()};setTimeout(tick,2e3).unref?.()}}scheduleRefresh(){if(this.debounceTimer)clearTimeout(this.debounceTimer);this.debounceTimer=setTimeout(async()=>{if(this.stopped)return;try{const bundle=await this.fetchOnce();this.notify(bundle)}catch(e){this.logger.warn(`[static-source] refresh failed: ${e.message}`)}},250);this.debounceTimer.unref?.()}};function createBundleSource(source,cacheDir,logger,deps){switch(source.type){case"static":return new StaticSource(source,logger);case"npm":return new NpmSource(source,logger);case"saas":return new SaasPullSource(source,cacheDir,logger,deps);case"inline":return new InlineSource(source,logger)}}var import_mcp_from_openapi=require("mcp-from-openapi");async function compileSkilledBundleFromOpenApi(openapi,skills,options){const serviceId=options.serviceId??"api";const baseUrl=options.baseUrl??openapi.servers?.[0]?.url;if(!baseUrl){throw new Error("compileSkilledBundleFromOpenApi: no baseUrl \u2014 pass options.baseUrl or declare servers[] in the OpenAPI doc.")}const generator=await import_mcp_from_openapi.OpenAPIToolGenerator.fromJSON(openapi);const tools=await generator.generateTools();const byOperationId=new Map;for(const tool of tools){const opId=tool.metadata.operationId;if(opId)byOperationId.set(opId,tool)}const referenced=new Set;for(const skill of skills)for(const opId of skill.operationIds)referenced.add(opId);const operations={};for(const opId of referenced){const tool=byOperationId.get(opId);if(!tool){throw new Error(`compileSkilledBundleFromOpenApi: operationId "${opId}" is referenced by a skill but not defined in the OpenAPI document.`)}operations[opId]={operationId:opId,serviceId,httpMethod:tool.metadata.method.toUpperCase(),pathTemplate:tool.metadata.path,summary:tool.metadata.operationSummary??tool.description,inputSchema:tool.inputSchema,outputSchema:tool.outputSchema??{type:"object"},mapper:tool.mapper,authBindingRef:"default"}}return{schemaVersion:1,bundleId:options.bundleId,version:options.version,generatedAt:options.generatedAt??new Date().toISOString(),sourceDigest:options.sourceDigest??"0".repeat(64),services:[{id:serviceId,baseUrl}],authBindings:{default:options.authBinding??{kind:"none"}},skills:skills.map(s=>({id:s.id,name:s.name,description:s.description,instructions:s.instructions,tags:s.tags??[],operationIds:s.operationIds})),operations}}var import_utils3=require("@frontmcp/utils");var DEFAULT_OPTIONS={skillsDir:"./skills",watch:true,debounceMs:200,pollIntervalMs:2e3};var FilesystemSkillsSource=class{constructor(options,logger){this.logger=logger;this.options={...DEFAULT_OPTIONS,...options};this.id=`filesystem-skills:${this.options.skillsDir}`}logger;id;options;listeners=new Set;known=new Map;watcher;pollTimer;debounceTimers=new Map;stopped=false;rootResolved="";onChange(listener){this.listeners.add(listener);return()=>{this.listeners.delete(listener)}}async start(){const absRoot=(0,import_utils3.pathResolve)(this.options.skillsDir);try{this.rootResolved=await(0,import_utils3.realpath)(absRoot)}catch{this.rootResolved=absRoot}await this.initialScan();if(this.options.watch){this.beginWatch()}}async stop(){this.stopped=true;if(this.watcher){try{this.watcher.close()}catch{}this.watcher=void 0}if(this.pollTimer){clearTimeout(this.pollTimer);this.pollTimer=void 0}for(const t of this.debounceTimers.values())clearTimeout(t);this.debounceTimers.clear();this.listeners.clear();this.known.clear()}async initialScan(){const entries=await this.listSkillSubdirs();for(const dir of entries){await this.refreshSkill(dir)}}async listSkillSubdirs(){let entries;try{entries=await(0,import_utils3.readdir)(this.rootResolved)}catch(e){this.logger.warn(`[fs-skills] cannot read ${this.rootResolved}: ${e.message}`);return[]}const out=[];for(const name of entries){if(name.startsWith("."))continue;const abs=(0,import_utils3.joinPath)(this.rootResolved,name);let resolved;try{resolved=await(0,import_utils3.realpath)(abs)}catch{continue}if(!resolved.startsWith(this.rootResolved+import_utils3.sep)&&resolved!==this.rootResolved){this.logger.warn(`[fs-skills] skipping ${name}: realpath escapes skillsDir`);continue}try{const st=await(0,import_utils3.stat)(resolved);if(st.isDirectory())out.push(resolved)}catch{}}return out.sort()}async refreshSkill(absDir){if(this.stopped)return;let exists=false;try{const st=await(0,import_utils3.stat)(absDir);exists=st.isDirectory()}catch{exists=false}if(!exists){const prev2=this.known.get(absDir);if(prev2){this.known.delete(absDir);this.emit({op:"delete",skillId:prev2.id,absPath:absDir})}return}let content;try{content=await loadFilesystemSkill(absDir)}catch(e){this.logger.warn(`[fs-skills] failed to load ${absDir}: ${e.message}`);return}const prev=this.known.get(absDir);if(prev&&skillContentEqual(prev,content)){return}this.known.set(absDir,content);this.emit({op:"upsert",skill:content,absPath:absDir})}emit(event){for(const fn of this.listeners){try{fn(event)}catch(e){this.logger.warn(`[fs-skills] listener threw: ${e.message}`)}}}beginWatch(){try{this.watcher=(0,import_utils3.watchFile)(this.rootResolved,(_eventType,filename)=>{if(this.stopped)return;if(!filename){this.scheduleRescan();return}const first=filename.split(import_utils3.sep)[0];if(!first||first.startsWith("."))return;const absDir=(0,import_utils3.joinPath)(this.rootResolved,first);this.scheduleRefresh(absDir)},{recursive:true});this.watcher.onError(e=>{this.logger.warn(`[fs-skills] fs.watch error: ${e.message}; falling back to polling`);this.watcher?.close();this.watcher=void 0;this.beginPolling()})}catch(e){this.logger.warn(`[fs-skills] fs.watch failed (${e.message}); polling at ${this.options.pollIntervalMs}ms instead`);this.beginPolling()}}beginPolling(){const tick=async()=>{if(this.stopped)return;try{await this.pollOnce()}catch(e){this.logger.warn(`[fs-skills] poll tick error: ${e.message}`)}if(this.stopped)return;this.pollTimer=setTimeout(tick,this.options.pollIntervalMs);this.pollTimer.unref?.()};this.pollTimer=setTimeout(tick,this.options.pollIntervalMs);this.pollTimer.unref?.()}async pollOnce(){const dirs=new Set(await this.listSkillSubdirs());for(const known of[...this.known.keys()]){if(!dirs.has(known)){await this.refreshSkill(known)}}for(const abs of dirs){await this.refreshSkill(abs)}}scheduleRescan(){const key="<rescan>";const existing=this.debounceTimers.get(key);if(existing)clearTimeout(existing);const t=setTimeout(()=>{this.debounceTimers.delete(key);void this.pollOnce().catch(e=>{this.logger.warn(`[fs-skills] rescan failed: ${e.message}`)})},this.options.debounceMs);t.unref?.();this.debounceTimers.set(key,t)}scheduleRefresh(absDir){const existing=this.debounceTimers.get(absDir);if(existing)clearTimeout(existing);const t=setTimeout(()=>{this.debounceTimers.delete(absDir);void this.refreshSkill(absDir).catch(e=>{this.logger.warn(`[fs-skills] refresh failed for ${absDir}: ${e.message}`)})},this.options.debounceMs);t.unref?.();this.debounceTimers.set(absDir,t)}};async function loadFilesystemSkill(absDir){const skillMdPath=(0,import_utils3.joinPath)(absDir,"SKILL.md");const raw=await(0,import_utils3.readFile)(skillMdPath,"utf8");const{frontmatter,body}=parseSkillFrontmatter(raw);const dirName=(0,import_utils3.basename)(absDir);const name=frontmatter["name"]??dirName;const description=frontmatter["description"]??"";if(!description){throw new Error(`SKILL.md missing 'description' frontmatter (in ${absDir})`)}const instructions=body.trim();if(!instructions){throw new Error(`SKILL.md missing body content (in ${absDir})`)}const tools=parseToolList(frontmatter["tools"]);const resources={};for(const sub of["scripts","references","examples","assets"]){const subAbs=(0,import_utils3.joinPath)(absDir,sub);try{const st=await(0,import_utils3.stat)(subAbs);if(st.isDirectory())resources[sub]=subAbs}catch{}}const out={id:name,name,description,instructions,tools,...Object.keys(resources).length>0&&{resources}};if(typeof frontmatter["license"]==="string")out.license=frontmatter["license"];if(typeof frontmatter["compatibility"]==="string")out.compatibility=frontmatter["compatibility"];return out}function parseSkillFrontmatter(raw){if(!raw.startsWith("---")){return{frontmatter:{},body:raw}}const closeIdx=raw.indexOf("\n---",3);if(closeIdx===-1){return{frontmatter:{},body:raw}}const yamlBlock=raw.slice(3,closeIdx).trim();let bodyStart=closeIdx+4;if(raw[bodyStart]==="\n")bodyStart+=1;const body=raw.slice(bodyStart);const fm={};let currentListKey;for(const line of yamlBlock.split("\n")){if(!line.trim())continue;if(line.startsWith("- ")&¤tListKey){const arr=fm[currentListKey]??[];arr.push(stripQuotes(line.slice(2).trim()));fm[currentListKey]=arr;continue}const colon=line.indexOf(":");if(colon===-1)continue;const key=line.slice(0,colon).trim();const rest=line.slice(colon+1).trim();if(!rest){currentListKey=key;fm[key]=[];continue}currentListKey=void 0;fm[key]=stripQuotes(rest)}return{frontmatter:fm,body}}function stripQuotes(s){if(s.startsWith('"')&&s.endsWith('"')||s.startsWith("'")&&s.endsWith("'")){return s.slice(1,-1)}return s}function parseToolList(raw){if(Array.isArray(raw)){return raw.map(t=>typeof t==="string"?{name:t}:null).filter(t=>t!==null)}if(typeof raw==="string"){return raw.split(",").map(t=>t.trim()).filter(Boolean).map(name=>({name}))}return[]}function skillContentEqual(a,b){return a.id===b.id&&a.name===b.name&&a.description===b.description&&a.instructions===b.instructions&&a.license===b.license&&a.compatibility===b.compatibility&&JSON.stringify(a.tools)===JSON.stringify(b.tools)&&JSON.stringify(a.resources??{})===JSON.stringify(b.resources??{})}var import_utils4=require("@frontmcp/utils");function classifyReason(reason){if(reason.includes("missing integrity"))return"missing_integrity";if(reason.includes("digest mismatch"))return"digest_mismatch";if(reason.includes("unknown signing keyId"))return"unknown_key_id";if(reason.includes("alg="))return"alg_mismatch";if(reason.includes("parse public key"))return"malformed_public_key";if(reason.includes("not valid base64url"))return"malformed_signature";if(reason.includes("signature verify threw"))return"verify_threw";if(reason.includes("signature verification failed"))return"verify_failed";return"other"}function canonicalize(value){if(value===null||typeof value!=="object"){return JSON.stringify(value)}if(Array.isArray(value)){return`[${value.map(v=>canonicalize(v)).join(",")}]`}const obj=value;const keys=Object.keys(obj).sort();return`{${keys.map(k=>`${JSON.stringify(k)}:${canonicalize(obj[k])}`).join(",")}}`}function bundleDigest(bundle){const{integrity:_drop,...rest}=bundle;void _drop;return(0,import_utils4.sha256Hex)(canonicalize(rest))}function decodeSignatureBytes(signatureBase64Url){try{return(0,import_utils4.base64urlDecode)(signatureBase64Url)}catch{return void 0}}function verifyBundleSignature(bundle,trustedKeys,telemetry){const failuresCounter=telemetry?.createCounter("frontmcp_skills_signature_failures_total","Number of bundle signature verification failures, partitioned by reason.");const verificationsCounter=telemetry?.createCounter("frontmcp_skills_signature_verifications_total","Number of bundle signature verification attempts, partitioned by status (ok|error).");const fail=reason=>{failuresCounter?.inc(1,{reason:classifyReason(reason)});verificationsCounter?.inc(1,{status:"error"});return{ok:false,reason}};if(!bundle.integrity){return fail("bundle missing integrity envelope")}const{alg,keyId,signature,digest}=bundle.integrity;const expectedDigest=bundleDigest(bundle);if(digest.toLowerCase()!==expectedDigest.toLowerCase()){return fail(`digest mismatch (envelope=${digest.slice(0,12)}.. computed=${expectedDigest.slice(0,12)}..)`)}const trusted=trustedKeys.find(k=>k.keyId===keyId);if(!trusted){return fail(`unknown signing keyId "${keyId}" \u2014 not in trustedKeys allowlist`)}if(trusted.alg!==alg){return fail(`key "${keyId}" alg=${trusted.alg} but envelope alg=${alg}`)}let publicJwk;try{publicJwk=(0,import_utils4.pemToPublicJwk)(trusted.publicKeyPem)}catch(e){return fail(`failed to parse public key for "${keyId}": ${e.message}`)}const sigBytes=decodeSignatureBytes(signature);if(!sigBytes){return fail("signature is not valid base64url")}const{integrity:_drop,...rest}=bundle;void _drop;const canonicalBytes=new TextEncoder().encode(canonicalize(rest));const verified=(0,import_utils4.rsaVerifySync)(alg,canonicalBytes,publicJwk,sigBytes);if(!verified){return fail("signature verification failed")}verificationsCounter?.inc(1,{status:"ok"});return{ok:true,keyId,alg}}var import_auth2=require("@frontmcp/auth");var DEFAULT_REQUIRED_ROLES=["frontmcp:cloud:push"];var BundlePushJwtVerifier=class{constructor(options,logger,jwks){this.options=options;this.logger=logger;this.jwks=jwks??new import_auth2.JwksService({});this.providerRef={id:"skilled-openapi-cloud-push",issuerUrl:options.expectedIssuer,jwksUri:options.jwksUri}}options;logger;providerRef;jwks;async verify(token){if(!token||typeof token!=="string"){return{ok:false,reason:"token missing"}}let res;try{res=await this.jwks.verifyTransparentToken(token,[this.providerRef])}catch(e){this.logger.debug(`[skilled-openapi] jwt verify threw: ${e.message}`);return{ok:false,reason:`verify failed: ${e.message}`}}if(!res.ok||!res.payload){return{ok:false,reason:res.error??"signature verify failed"}}const payload=res.payload;const aud=payload["aud"];const audValues=Array.isArray(aud)?aud:aud!==void 0?[aud]:[];if(!audValues.includes(this.options.expectedAudience)){return{ok:false,reason:`aud mismatch (expected "${this.options.expectedAudience}")`}}const resource=payload["resource"];const resValues=Array.isArray(resource)?resource:resource!==void 0?[resource]:[];if(!resValues.includes(this.options.expectedResource)){return{ok:false,reason:`RFC 8707 resource indicator mismatch (expected "${this.options.expectedResource}")`}}const requiredRoles=this.options.requiredRoles??DEFAULT_REQUIRED_ROLES;if(requiredRoles.length>0&&!this.matchesAny(payload["roles"],requiredRoles)){return{ok:false,reason:`missing required role (need one of ${requiredRoles.join(", ")})`}}if(this.options.requiredScopes&&this.options.requiredScopes.length>0){if(!this.matchesAny(payload["scope"]??payload["scp"],this.options.requiredScopes)){return{ok:false,reason:`missing required scope (need one of ${this.options.requiredScopes.join(", ")})`}}}return{ok:true,payload}}matchesAny(claim,required){const have=Array.isArray(claim)?claim.filter(v=>typeof v==="string"):typeof claim==="string"?claim.split(/\s+/).filter(Boolean):[];return required.some(r=>have.includes(r))}};var import_utils5=require("@frontmcp/utils");function classifyReplayReason(reason){if(reason.includes("not finite"))return"invalid_timestamp";if(reason.includes("too short"))return"invalid_nonce";if(reason.includes("outside"))return"outside_window";if(reason.includes("replay"))return"nonce_replay";return"other"}var DEFAULT_WINDOW_MS=5*60*1e3;var DEFAULT_CAPACITY=5e3;var WebhookReplayGuard=class{windowMs;capacity;seen=new Map;now=Date.now;rejectsCounter;checksCounter;constructor(options={}){const windowMs=options.windowMs??DEFAULT_WINDOW_MS;const capacity=options.capacity??DEFAULT_CAPACITY;if(!Number.isFinite(windowMs)||windowMs<=0){throw new RangeError("WebhookReplayGuard: windowMs must be a finite number > 0")}if(!Number.isInteger(capacity)||capacity<=0){throw new RangeError("WebhookReplayGuard: capacity must be a positive integer")}this.windowMs=windowMs;this.capacity=capacity;this.rejectsCounter=options.telemetry?.createCounter("frontmcp_skills_replay_rejects_total","Number of webhook replay-guard rejections, partitioned by reason.");this.checksCounter=options.telemetry?.createCounter("frontmcp_skills_replay_checks_total","Number of webhook replay-guard checks, partitioned by status (ok|error).")}setNowProvider(fn){this.now=fn}check(args){const{timestampMs,nonce}=args;const reject=reason=>{this.rejectsCounter?.inc(1,{reason:classifyReplayReason(reason)});this.checksCounter?.inc(1,{status:"error"});return{ok:false,reason}};if(!Number.isFinite(timestampMs)){return reject("timestamp not finite")}if(typeof nonce!=="string"||nonce.length<8){return reject("nonce too short")}const now=this.now();if(Math.abs(now-timestampMs)>this.windowMs){return reject(`timestamp outside ${this.windowMs}ms window`)}const key=(0,import_utils5.sha256Hex)(nonce);const expiresAt=this.seen.get(key);if(expiresAt!==void 0&&expiresAt>=now){return reject("nonce replay detected within freshness window")}if(this.seen.size>=this.capacity){this.prune()}if(this.seen.size>=this.capacity){let victimKey;let victimExpiry=Number.POSITIVE_INFINITY;for(const[candidateKey,candidateExpiry]of this.seen){if(candidateExpiry<victimExpiry){victimKey=candidateKey;victimExpiry=candidateExpiry}}if(victimKey!==void 0)this.seen.delete(victimKey)}this.seen.set(key,timestampMs+this.windowMs);this.checksCounter?.inc(1,{status:"ok"});return{ok:true}}prune(){const now=this.now();let removed=0;for(const[k,expiresAt]of this.seen){if(expiresAt<now){this.seen.delete(k);removed++}}return removed}get size(){return this.seen.size}};var SKILL_AUDIT_GENESIS_PREV_HASH="0".repeat(64);var SKILL_AUDIT_KEYS={sequence:"audit:skills:sequence",record:sequence=>`audit:skills:records:${sequence}`};var import_utils6=require("@frontmcp/utils");function canonicalizeRecordForSigning(record){const{signature:_drop,...rest}=record;void _drop;return canonicalize(rest)}function nextPrevHash(prev){if(!prev)return SKILL_AUDIT_GENESIS_PREV_HASH;return(0,import_utils6.sha256Hex)(canonicalizeRecordForSigning(prev))}function linkRecord(prev,partial){return{...partial,prevHash:nextPrevHash(prev)}}function verifyChain(records,trustedKeys,verifier,options={}){if(records.length===0){return{ok:true,verified:0}}let prev=options.previous;for(let i=0;i<records.length;i++){const rec=records[i];if(!rec){return{ok:false,breakAt:i,reason:"record at index is null/undefined"}}if(prev&&rec.sequence!==prev.sequence+1){return{ok:false,breakAt:rec.sequence,reason:`sequence gap: expected ${prev.sequence+1}, got ${rec.sequence}`}}const expectedPrev=nextPrevHash(prev);if(rec.prevHash!==expectedPrev){return{ok:false,breakAt:rec.sequence,reason:`prevHash mismatch at sequence ${rec.sequence}: expected ${expectedPrev.slice(0,12)}.. got ${rec.prevHash.slice(0,12)}..`}}const canonicalBytes=new TextEncoder().encode(canonicalizeRecordForSigning(rec));const ok=verifier({alg:rec.signatureAlg,keyId:rec.signatureKeyId,data:canonicalBytes,signatureBase64Url:rec.signature,trustedKeys:[...trustedKeys]});if(!ok){return{ok:false,breakAt:rec.sequence,reason:`signature verification failed at sequence ${rec.sequence} (keyId="${rec.signatureKeyId}", alg=${rec.signatureAlg})`}}prev=rec}return{ok:true,verified:records.length}}var import_utils7=require("@frontmcp/utils");function deriveFromKeyMaterial(ikm,info,length){return(0,import_utils7.hkdfSha256)(ikm,new Uint8Array(0),new TextEncoder().encode(info),length)}var Hs256AuditSigner=class{key;keyId;constructor(secret,keyId){if(!keyId){throw new Error("Hs256AuditSigner: keyId must be a non-empty string")}if(typeof secret==="string"){if(secret.length===0)throw new Error("Hs256AuditSigner: secret must be non-empty");this.key=new TextEncoder().encode(secret)}else{if(secret.length===0)throw new Error("Hs256AuditSigner: secret must be non-empty");this.key=new Uint8Array(secret)}this.keyId=keyId}sign(record){const canonicalBytes=new TextEncoder().encode(canonicalizeRecordForSigning({...record,signature:"",signatureKeyId:this.keyId,signatureAlg:"HS256"}));const mac=(0,import_utils7.hmacSha256)(this.key,canonicalBytes);return{signature:(0,import_utils7.base64urlEncode)(mac),keyId:this.keyId,alg:"HS256"}}getKeyId(){return this.keyId}getAlg(){return"HS256"}deriveKey(info,length){return deriveFromKeyMaterial(this.key,info,length)}};var Rs256AuditSigner=class{privateJwk;privateExponent;keyId;constructor(privateJwk,keyId){if(!keyId){throw new Error("Rs256AuditSigner: keyId must be a non-empty string")}if(!privateJwk||typeof privateJwk!=="object"){throw new Error("Rs256AuditSigner: privateJwk must be a JWK object")}if(privateJwk.kty!=="RSA"){throw new Error(`Rs256AuditSigner: privateJwk.kty must be "RSA", got "${String(privateJwk.kty)}"`)}if(!privateJwk.n||!privateJwk.e||!privateJwk.d){throw new Error("Rs256AuditSigner: privateJwk must include `n`, `e`, and `d` (RSA private key)")}this.privateJwk={...privateJwk};this.privateExponent=privateJwk.d;this.keyId=keyId}sign(record){const canonicalBytes=new TextEncoder().encode(canonicalizeRecordForSigning({...record,signature:"",signatureKeyId:this.keyId,signatureAlg:"RS256"}));const signature=(0,import_utils7.rsaSignBase64Url)("RS256",canonicalBytes,this.privateJwk);return{signature,keyId:this.keyId,alg:"RS256"}}getKeyId(){return this.keyId}getAlg(){return"RS256"}deriveKey(info,length){return deriveFromKeyMaterial((0,import_utils7.base64urlDecode)(this.privateExponent),info,length)}};var defaultAuditSignatureVerifier=({alg,keyId,data,signatureBase64Url,trustedKeys})=>{const trusted=trustedKeys.find(k=>k.keyId===keyId&&k.alg===alg);if(!trusted)return false;if(alg==="HS256"){if(!trusted.secret)return false;const expected=(0,import_utils7.hmacSha256)(trusted.secret,data);let actual;try{actual=(0,import_utils7.base64urlDecode)(signatureBase64Url)}catch{return false}if(actual.length!==expected.length)return false;return(0,import_utils7.timingSafeEqual)(actual,expected)}if(alg==="RS256"){if(!trusted.publicKeyPem&&!trusted.publicJwk)return false;let publicJwk;try{publicJwk=trusted.publicJwk??(0,import_utils7.pemToPublicJwk)(trusted.publicKeyPem)}catch{return false}let sigBytes;try{sigBytes=(0,import_utils7.base64urlDecode)(signatureBase64Url)}catch{return false}return(0,import_utils7.rsaVerifySync)("RS256",data,publicJwk,sigBytes)}const _exhaustive=alg;void _exhaustive;return false};var MemoryAuditStore=class{records=[];seq=0;async nextSequence(){this.seq+=1;return this.seq}async appendAtSequence(record){if(this.records.some(r=>r.sequence===record.sequence)){throw new Error(`MemoryAuditStore: record at sequence ${record.sequence} already exists`)}this.records.push(record)}async tail(){if(this.records.length===0)return void 0;return this.records[this.records.length-1]}async releaseSequence(sequence){if(sequence===this.seq&&!this.records.some(record=>record.sequence===sequence))this.seq-=1}async read(opts){const from=opts?.from??1;const limit=opts?.limit;const filtered=this.records.filter(r=>r.sequence>=from).sort((a,b)=>a.sequence-b.sequence);return limit!==void 0?filtered.slice(0,limit):filtered}};var StorageAdapterAuditStore=class{constructor(adapter,options={}){this.adapter=adapter;this.options=options}adapter;options;get sequenceKey(){return this.options.sequenceKey??SKILL_AUDIT_KEYS.sequence}recordKey(sequence){const prefix=this.options.recordKeyPrefix;return prefix?`${prefix}${sequence}`:SKILL_AUDIT_KEYS.record(sequence)}async nextSequence(){return this.adapter.incr(this.sequenceKey)}async releaseSequence(sequence){if(await this.adapter.get(this.recordKey(sequence))!==null)return;if(await this.adapter.decr(this.sequenceKey)!==sequence-1)await this.adapter.incr(this.sequenceKey)}async appendAtSequence(record){const key=this.recordKey(record.sequence);const existing=await this.adapter.get(key);if(existing!==null){throw new Error(`StorageAdapterAuditStore: record at sequence ${record.sequence} already exists`)}const payload=JSON.stringify(record);await this.adapter.set(key,payload,{ifNotExists:true});const stored=await this.adapter.get(key);if(stored!==payload){throw new Error(`StorageAdapterAuditStore: record at sequence ${record.sequence} was clobbered or not stored`)}}async tail(){const seqRaw=await this.adapter.get(this.sequenceKey);if(!seqRaw)return void 0;const seq=Number.parseInt(seqRaw,10);if(!Number.isFinite(seq)||seq<=0)return void 0;const maxLookback=this.options.tailLookback??1024;for(let s=seq;s>0&&s>seq-maxLookback;s--){const raw=await this.adapter.get(this.recordKey(s));if(!raw)continue;try{return JSON.parse(raw)}catch{throw new Error(`StorageAdapterAuditStore.tail: corrupt record at sequence ${s} cannot be parsed as JSON`)}}return void 0}async read(opts){const seqRaw=await this.adapter.get(this.sequenceKey);if(!seqRaw)return[];const head=Number.parseInt(seqRaw,10);if(!Number.isFinite(head)||head<=0)return[];const from=Math.max(1,opts?.from??1);const limit=opts?.limit;const out=[];for(let s=from;s<=head;s++){const raw=await this.adapter.get(this.recordKey(s));if(!raw)continue;try{const parsed=JSON.parse(raw);out.push(parsed);if(limit!==void 0&&out.length>=limit)break}catch{}}return out}};var import_utils8=require("@frontmcp/utils");function createSkillAuditMetrics(telemetry){const writeFailures=telemetry.createCounter("frontmcp_skills_audit_write_failures_total","Skill audit records that failed to sign or append, partitioned by reason.");const dropped=telemetry.createCounter("frontmcp_skills_audit_dropped_total","Skill audit records dropped before they were written, partitioned by reason.");return{incrementWriteFailure:reason=>writeFailures.inc(1,{reason}),incrementDropped:reason=>dropped.inc(1,{reason})}}var SKILL_AUDIT_ERROR_MESSAGE_MAX=500;var SKILL_AUDIT_QUEUE_MAX=1e3;var SUBJECT_HASH_KEY_INFO="frontmcp:audit:subject";var SUBJECT_HASH_KEY_LENGTH=32;var SkillAuditWriter=class{constructor(store,signer,logger,metrics,options={}){this.store=store;this.signer=signer;this.logger=logger;this.metrics=metrics;this.subjectMode=options.subjectMode??"hash";if(this.subjectMode==="hash"){this.subjectHashKey=options.subjectHashSecret??signer.deriveKey?.(SUBJECT_HASH_KEY_INFO,SUBJECT_HASH_KEY_LENGTH);if(!this.subjectHashKey){logger.warn(`[skill-audit] subjectMode 'hash' needs a key, and the signer (keyId "${signer.getKeyId()}") has no key material to derive one from, so subjects are recorded as 'redacted'. Set skillsConfig.audit.subjectHashSecret to record subject hashes.`)}}this.maxQueueDepth=options.maxQueueDepth??SKILL_AUDIT_QUEUE_MAX}store;signer;logger;metrics;chainHeadLock=Promise.resolve();queueDepth=0;subjectMode;subjectHashKey;maxQueueDepth;writeAuthorityPass(ctx){return this.write({phase:"authority-check-pass",ctx})}writeAuthorityFail(ctx,extras){return this.write({phase:"authority-check-fail",ctx,error:extras.reason})}writeHttpCallSuccess(ctx,extras){return this.write({phase:"http-call-success",ctx,status:extras.status,output:extras.output})}writeHttpCallFailure(ctx,extras){return this.write({phase:"http-call-failure",ctx,status:extras.status,error:extras.error})}async flush(){while(this.queueDepth>0){await this.chainHeadLock}}write(args){if(this.queueDepth>=this.maxQueueDepth){this.logger.warn(`[skill-audit] queue overflow (${this.queueDepth}/${this.maxQueueDepth}); dropping ${args.phase} record. Audit backend likely unhealthy.`);this.metrics?.incrementDropped?.("queue-overflow");return Promise.resolve()}this.queueDepth+=1;const next=this.chainHeadLock.then(()=>this.doWrite(args));this.chainHeadLock=next.catch(()=>void 0).finally(()=>{this.queueDepth-=1});return next.catch(()=>void 0)}async doWrite(args){const{phase,ctx,status,output,error}=args;let prev;try{prev=await this.store.tail()}catch(e){this.logger.warn(`[skill-audit] failed to load chain tail: ${e.message}`);this.metrics?.incrementWriteFailure("append");return}let inputHash;try{inputHash=(0,import_utils8.sha256Hex)(canonicalize(ctx.input??null))}catch(e){this.logger.warn(`[skill-audit] failed to canonicalize input: ${e.message}`);this.metrics?.incrementWriteFailure("unexpected");return}let sequence;try{sequence=await this.store.nextSequence()}catch(e){this.logger.warn(`[skill-audit] failed to allocate sequence: ${e.message}`);this.metrics?.incrementWriteFailure("append");return}const partial={id:(0,import_utils8.randomUUID)(),sequence,timestamp:new Date().toISOString(),subject:this.applySubjectMode(ctx.subject||"anonymous"),skillId:ctx.skillId,actionId:ctx.actionId,bundleId:ctx.bundleId,bundleVersion:ctx.bundleVersion,phase,inputHash,...status!==void 0?{status}:{},...phase==="http-call-success"?{outputHash:this.safeHash(output)}:{},...phase==="http-call-failure"||phase==="authority-check-fail"?{errorMessage:this.formatError(error)}:{}};const linked=linkRecord(prev,partial);let signed;try{const sig=this.signer.sign(linked);signed={...linked,signature:sig.signature,signatureKeyId:sig.keyId,signatureAlg:sig.alg}}catch(e){this.logger.warn(`[skill-audit] failed to sign record at seq=${sequence}: ${e.message}`);this.metrics?.incrementWriteFailure("sign");await this.releaseUnusedSequence(sequence);return}try{await this.store.appendAtSequence(signed);this.logger.debug?.(`[skill-audit] appended seq=${sequence} phase=${phase}`)}catch(e){this.logger.warn(`[skill-audit] failed to append record at seq=${sequence}: ${e.message}`);this.metrics?.incrementWriteFailure("append");await this.releaseUnusedSequence(sequence)}}async releaseUnusedSequence(allocated){if(!this.store.releaseSequence)return;try{await this.store.releaseSequence(allocated);this.logger.debug?.(`[skill-audit] rolled back unused sequence ${allocated}`)}catch{}}applySubjectMode(subject){if(this.subjectMode==="plain")return subject;if(this.subjectMode==="omit"||!this.subjectHashKey)return"redacted";const mac=(0,import_utils8.hmacSha256)(this.subjectHashKey,new TextEncoder().encode(subject));const hex=(0,import_utils8.bytesToHex)(mac);return`hashed:${hex.slice(0,32)}`}safeHash(value){try{return(0,import_utils8.sha256Hex)(canonicalize(value??null))}catch{return(0,import_utils8.sha256Hex)("audit:non-canonicalizable-output")}}formatError(error){let raw;if(error instanceof Error){raw=error.message}else if(typeof error==="string"){raw=error}else if(error===void 0||error===null){raw=""}else{try{raw=JSON.stringify(error)}catch{raw=String(error)}}if(raw.length>SKILL_AUDIT_ERROR_MESSAGE_MAX){return raw.slice(0,SKILL_AUDIT_ERROR_MESSAGE_MAX)+"..."}return raw}};var SkillAuditWriterToken=Symbol.for("frontmcp:SKILL_AUDIT_WRITER");var import_sdk=require("@frontmcp/sdk");var SPEC_ID_RE="[A-Za-z0-9][A-Za-z0-9_.-]*";var OPERATION_ID_RE="[A-Za-z_][A-Za-z0-9_]*";var TRAILING_BOUNDARY=String.raw`(?![A-Za-z0-9_\-])(?!\.[A-Za-z0-9_])`;var REFERENCE_RE=new RegExp(String.raw`(?<![A-Za-z0-9])op:\/\/(${SPEC_ID_RE})\/(${OPERATION_ID_RE})${TRAILING_BOUNDARY}`+`|`+String.raw`\[\[op:\s?(${SPEC_ID_RE})\/(${OPERATION_ID_RE})\]\]`,"g");function extractOpReferences(markdown){if(typeof markdown!=="string"||markdown.length===0)return[];const refs=[];const lineStarts=computeLineStarts(markdown);REFERENCE_RE.lastIndex=0;let match;while((match=REFERENCE_RE.exec(markdown))!==null){const offset=match.index;const[raw,uriSpec,uriOp,wikiSpec,wikiOp]=match;const isWikilink=wikiSpec!==void 0&&wikiOp!==void 0;const spec=isWikilink?wikiSpec:uriSpec;const operationId=isWikilink?wikiOp:uriOp;refs.push({spec,operationId,syntax:isWikilink?"wikilink":"uri",raw,location:offsetToLocation(offset,lineStarts)})}return refs}function buildKnownOps(entries){const map=new Map;for(const entry of entries){const slash=entry.indexOf("/");if(slash<=0||slash===entry.length-1)continue;const spec=entry.slice(0,slash);const op=entry.slice(slash+1);let set=map.get(spec);if(!set){set=new Set;map.set(spec,set)}set.add(op)}return map}function validateOpReferences(refs,knownOps){const diagnostics=[];for(const ref of refs){const specOps=knownOps.get(ref.spec);if(!specOps){diagnostics.push({ref,kind:"unknown-spec",message:`Unknown OpenAPI spec "${ref.spec}" referenced as ${ref.raw}`,suggestions:Array.from(knownOps.keys()).sort()});continue}if(!specOps.has(ref.operationId)){diagnostics.push({ref,kind:"unknown-operation",message:`Unknown operationId "${ref.operationId}" in spec "${ref.spec}" (${ref.raw})`,suggestions:suggestSimilarOps(ref.operationId,specOps)})}}return diagnostics}function dedupeOpReferences(refs){const seen=new Set;const result=[];for(const ref of refs){const key=`${ref.spec}/${ref.operationId}`;if(seen.has(key))continue;seen.add(key);result.push({spec:ref.spec,operationId:ref.operationId})}return result}function computeLineStarts(source){const starts=[0];for(let i=0;i<source.length;i++){if(source.charCodeAt(i)===10)starts.push(i+1)}return starts}function offsetToLocation(offset,lineStarts){let lo=0;let hi=lineStarts.length-1;while(lo<hi){const mid=lo+hi+1>>>1;if(lineStarts[mid]<=offset)lo=mid;else hi=mid-1}const line=lo+1;const column=offset-lineStarts[lo]+1;return{line,column,offset}}function suggestSimilarOps(query,known){if(known.size===0)return[];const q=query.toLowerCase();const candidates=Array.from(known);const prefix=candidates.filter(c=>c.toLowerCase().startsWith(q));const substring=candidates.filter(c=>!prefix.includes(c)&&c.toLowerCase().includes(q));const close=candidates.filter(c=>!prefix.includes(c)&&!substring.includes(c)).filter(c=>levenshtein(q,c.toLowerCase())<=Math.max(2,Math.floor(q.length/4)));return[...prefix,...substring,...close].slice(0,3)}function levenshtein(a,b){if(a===b)return 0;if(a.length===0)return b.length;if(b.length===0)return a.length;let prev=new Array(b.length+1);let curr=new Array(b.length+1);for(let j=0;j<=b.length;j++)prev[j]=j;for(let i=1;i<=a.length;i++){curr[0]=i;for(let j=1;j<=b.length;j++){const cost=a.charCodeAt(i-1)===b.charCodeAt(j-1)?0:1;curr[j]=Math.min(curr[j-1]+1,prev[j]+1,prev[j-1]+cost)}[prev,curr]=[curr,prev]}return prev[b.length]}var import_lazy_zod3=require("@frontmcp/lazy-zod");var IDENT_RE=/^[A-Za-z][A-Za-z0-9_-]*$/;var SPEC_ID_RE2=/^[A-Za-z0-9][A-Za-z0-9_.-]*$/;var SECRET_NAME_RE=/^[A-Z][A-Z0-9_]*$/;var BINDING_NAME_RE=/^[A-Z][A-Z0-9_]*$/;var ISO_DATE_RE=/^\d{4}-\d{2}-\d{2}$/;var serverInfoSchema=import_lazy_zod3.z.object({name:import_lazy_zod3.z.string().min(1).max(128),version:import_lazy_zod3.z.string().min(1).max(64),title:import_lazy_zod3.z.string().min(1).max(256).optional()}).strict();var serverSchema=import_lazy_zod3.z.object({info:serverInfoSchema,instructions:import_lazy_zod3.z.string().max(16*1024).optional()}).strict();var runtimeSchema=import_lazy_zod3.z.object({target:import_lazy_zod3.z.literal("cloudflare-worker"),compatibilityDate:import_lazy_zod3.z.string().regex(ISO_DATE_RE,"compatibilityDate must be ISO-8601 (YYYY-MM-DD)"),compatibilityFlags:import_lazy_zod3.z.array(import_lazy_zod3.z.string().min(1).max(128)).max(32).optional()}).strict();var specSourceSchema=import_lazy_zod3.z.union([import_lazy_zod3.z.string().min(1).max(2048),import_lazy_zod3.z.object({id:import_lazy_zod3.z.string().min(1).max(128).regex(SPEC_ID_RE2,"spec id must be a URI-safe identifier"),spec:import_lazy_zod3.z.string().min(1).max(2048),baseUrl:import_lazy_zod3.z.string().url("baseUrl must be a valid URL").optional(),bindingName:import_lazy_zod3.z.string().min(1).max(64).regex(IDENT_RE).optional()}).strict()]);var specsSchema=import_lazy_zod3.z.union([import_lazy_zod3.z.string().min(1).max(2048),import_lazy_zod3.z.array(specSourceSchema).max(64)]);var skillsSchema=import_lazy_zod3.z.object({source:import_lazy_zod3.z.string().min(1).max(2048).default("./skills/"),alwaysLoad:import_lazy_zod3.z.array(import_lazy_zod3.z.string().min(1).max(128)).max(32).optional(),tags:import_lazy_zod3.z.object({include:import_lazy_zod3.z.array(import_lazy_zod3.z.string().min(1).max(64)).max(64).optional(),exclude:import_lazy_zod3.z.array(import_lazy_zod3.z.string().min(1).max(64)).max(64).optional()}).strict().optional()}).strict();var tagSchema=import_lazy_zod3.z.object({name:import_lazy_zod3.z.string().min(1).max(64),description:import_lazy_zod3.z.string().min(1).max(512).optional()}).strict();var classificationRuleSchema=import_lazy_zod3.z.object({match:import_lazy_zod3.z.string().min(1).max(256),expose:import_lazy_zod3.z.enum(["tool","resource","both"]).optional(),emits:import_lazy_zod3.z.enum(["self","parent","none"]).optional()}).strict();var classificationSchema=import_lazy_zod3.z.object({rules:import_lazy_zod3.z.array(classificationRuleSchema).max(64).optional()}).strict();var durableObjectBindingSchema=import_lazy_zod3.z.object({binding:import_lazy_zod3.z.string().min(1).max(128).regex(BINDING_NAME_RE),className:import_lazy_zod3.z.string().min(1).max(128).regex(IDENT_RE),scriptName:import_lazy_zod3.z.string().min(1).max(128).optional()}).strict();var d1BindingSchema=import_lazy_zod3.z.object({binding:import_lazy_zod3.z.string().min(1).max(128).regex(BINDING_NAME_RE),databaseName:import_lazy_zod3.z.string().min(1).max(256),databaseId:import_lazy_zod3.z.string().min(1).max(128)}).strict();var kvBindingSchema=import_lazy_zod3.z.object({binding:import_lazy_zod3.z.string().min(1).max(128).regex(BINDING_NAME_RE),id:import_lazy_zod3.z.string().min(1).max(128)}).strict();var r2BindingSchema=import_lazy_zod3.z.object({binding:import_lazy_zod3.z.string().min(1).max(128).regex(BINDING_NAME_RE),bucketName:import_lazy_zod3.z.string().min(1).max(256)}).strict();var bindingsSchema=import_lazy_zod3.z.object({durableObjects:import_lazy_zod3.z.array(durableObjectBindingSchema).max(32).optional(),d1Databases:import_lazy_zod3.z.array(d1BindingSchema).max(16).optional(),kvNamespaces:import_lazy_zod3.z.array(kvBindingSchema).max(32).optional(),r2Buckets:import_lazy_zod3.z.array(r2BindingSchema).max(16).optional(),vars:import_lazy_zod3.z.record(import_lazy_zod3.z.string().min(1).max(128),import_lazy_zod3.z.string().max(4096)).optional()}).strict();var trustRootSchema=import_lazy_zod3.z.object({kid:import_lazy_zod3.z.string().min(1).max(128),publicKeySecret:import_lazy_zod3.z.string().min(1).max(128).regex(SECRET_NAME_RE)}).strict();var replayGuardSchema=import_lazy_zod3.z.object({windowSeconds:import_lazy_zod3.z.number().int().min(10).max(3600).default(300),nonceKv:import_lazy_zod3.z.string().min(1).max(128).regex(BINDING_NAME_RE)}).strict();var signingSchema=import_lazy_zod3.z.object({algorithm:import_lazy_zod3.z.enum(["ed25519","rs256"]),trustRoots:import_lazy_zod3.z.array(trustRootSchema).min(1).max(8),replay:replayGuardSchema}).strict();var fronteggAuthSchema=import_lazy_zod3.z.object({tenantResolver:import_lazy_zod3.z.enum(["subdomain","header","jwt-claim"]).default("subdomain"),audience:import_lazy_zod3.z.string().min(1).max(256),issuerSecret:import_lazy_zod3.z.string().min(1).max(128).regex(SECRET_NAME_RE)}).strict();var oauthAuthSchema=import_lazy_zod3.z.object({issuer:import_lazy_zod3.z.string().url(),audience:import_lazy_zod3.z.string().min(1).max(256),credentialsSecret:import_lazy_zod3.z.string().min(1).max(128).regex(SECRET_NAME_RE).optional()}).strict();var apiKeyAuthSchema=import_lazy_zod3.z.object({header:import_lazy_zod3.z.string().min(1).max(64).default("X-API-Key"),allowlistSecret:import_lazy_zod3.z.string().min(1).max(128).regex(SECRET_NAME_RE)}).strict();var authSchema=import_lazy_zod3.z.discriminatedUnion("provider",[import_lazy_zod3.z.object({provider:import_lazy_zod3.z.literal("none")}).strict(),import_lazy_zod3.z.object({provider:import_lazy_zod3.z.literal("frontegg"),frontegg:fronteggAuthSchema}).strict(),import_lazy_zod3.z.object({provider:import_lazy_zod3.z.literal("oauth"),oauth:oauthAuthSchema}).strict(),import_lazy_zod3.z.object({provider:import_lazy_zod3.z.literal("apiKey"),apiKey:apiKeyAuthSchema}).strict()]);var secretSchema=import_lazy_zod3.z.object({name:import_lazy_zod3.z.string().min(1).max(128).regex(SECRET_NAME_RE),required:import_lazy_zod3.z.boolean().default(true),description:import_lazy_zod3.z.string().min(1).max(512).optional()}).strict();var environmentOverlaySchema=import_lazy_zod3.z.object({server:serverSchema.partial().optional(),specs:specsSchema.optional(),skills:skillsSchema.partial().optional(),classification:classificationSchema.optional(),bindings:bindingsSchema.optional(),auth:authSchema.optional(),vars:import_lazy_zod3.z.record(import_lazy_zod3.z.string().min(1).max(128),import_lazy_zod3.z.string().max(4096)).optional()}).strict();var deployManifestSchema=import_lazy_zod3.z.object({$schema:import_lazy_zod3.z.string().url().optional(),version:import_lazy_zod3.z.literal(1),name:import_lazy_zod3.z.string().min(1).max(128).regex(IDENT_RE),runtime:runtimeSchema,server:serverSchema,specs:specsSchema,skills:skillsSchema.default({source:"./skills/"}),tags:import_lazy_zod3.z.array(tagSchema).max(64).optional(),classification:classificationSchema.optional(),bindings:bindingsSchema,signing:signingSchema,auth:authSchema,secrets:import_lazy_zod3.z.array(secretSchema).max(64).optional(),environments:import_lazy_zod3.z.record(import_lazy_zod3.z.string().min(1).max(64).regex(IDENT_RE),environmentOverlaySchema).optional()}).strict();function applyEnvironmentOverlay(base,overlay){const effectiveBindings=overlay.bindings??base.bindings;const mergedBindings=overlay.vars!==void 0?{...effectiveBindings,vars:{...effectiveBindings.vars??{},...overlay.vars}}:effectiveBindings;return{...base,server:overlay.server?{...base.server,...overlay.server}:base.server,specs:overlay.specs??base.specs,skills:overlay.skills?{...base.skills,...overlay.skills}:base.skills,classification:overlay.classification??base.classification,bindings:mergedBindings,auth:overlay.auth??base.auth}}function validateEffectiveManifest(manifest,label,errors){const declaredSecrets=new Set((manifest.secrets??[]).map(s=>s.name));const refs=[];if(manifest.auth.provider==="frontegg"){refs.push({where:"auth.frontegg.issuerSecret",name:manifest.auth.frontegg.issuerSecret})}else if(manifest.auth.provider==="oauth"&&manifest.auth.oauth.credentialsSecret){refs.push({where:"auth.oauth.credentialsSecret",name:manifest.auth.oauth.credentialsSecret})}else if(manifest.auth.provider==="apiKey"){refs.push({where:"auth.apiKey.allowlistSecret",name:manifest.auth.apiKey.allowlistSecret})}for(const root of manifest.signing.trustRoots){refs.push({where:`signing.trustRoots[kid=${root.kid}].publicKeySecret`,name:root.publicKeySecret})}for(const ref of refs){if(!declaredSecrets.has(ref.name)){errors.push(`${label}Secret "${ref.name}" referenced at ${ref.where} is not declared in secrets[]`)}}const declaredKv=new Set((manifest.bindings.kvNamespaces??[]).map(kv=>kv.binding));if(!declaredKv.has(manifest.signing.replay.nonceKv)){errors.push(`${label}signing.replay.nonceKv "${manifest.signing.replay.nonceKv}" does not match any bindings.kvNamespaces[].binding`)}for(const id of manifest.skills.alwaysLoad??[]){if(!/^[a-z0-9][a-z0-9-]*$/.test(id)){errors.push(`${label}skills.alwaysLoad entry "${id}" is not a valid kebab-case skill id`)}}if(manifest.tags){const declaredTags=new Set(manifest.tags.map(t=>t.name));const tagFilter=manifest.skills.tags;for(const t of[...tagFilter?.include??[],...tagFilter?.exclude??[]]){if(!declaredTags.has(t)){errors.push(`${label}skills.tags references unknown tag "${t}" (not in tags[])`)}}}}function crossValidateManifest(manifest){const errors=[];validateEffectiveManifest(manifest,"",errors);for(const[envName,overlay]of Object.entries(manifest.environments??{})){const effective=applyEnvironmentOverlay(manifest,overlay);validateEffectiveManifest(effective,`environments.${envName}: `,errors)}return errors.length===0?{ok:true}:{ok:false,errors}}function specStem(ref){const noQuery=ref.split("?")[0].split("#")[0];const base=noQuery.split("/").pop()??noQuery;const stem=base.replace(/\.(ya?ml|json)$/i,"");const safe=(stem||base).replace(/[^a-zA-Z0-9_.-]/g,"-");return safe||"spec"}function normalizeSpec(source){if(typeof source==="string"){const id=specStem(source);return{id,spec:source,bindingName:id}}return{id:source.id,spec:source.spec,baseUrl:source.baseUrl,bindingName:source.bindingName??source.id}}function buildFrontMcpConfigFromManifest(manifest){const info={name:manifest.server.info.name,version:manifest.server.info.version,...manifest.server.info.title!==void 0?{title:manifest.server.info.title}:{}};const skills={source:manifest.skills.source,...manifest.skills.alwaysLoad?{alwaysLoad:manifest.skills.alwaysLoad}:{},...manifest.skills.tags?{tags:manifest.skills.tags}:{}};let specs=[];let specsDir;if(typeof manifest.specs==="string"){specsDir=manifest.specs}else{specs=manifest.specs.map(normalizeSpec);const seen=new Set;for(const s of specs){if(seen.has(s.id)){throw new Error(`manifest specs: duplicate spec id "${s.id}" (derived from the filename stem). Give the colliding spec an explicit { id } in frontmcp.deploy.yaml.`)}seen.add(s.id)}}return{info,...manifest.server.instructions!==void 0?{instructions:manifest.server.instructions}:{},skillsConfig:{enabled:true,injectInstructions:"append"},skills,specs,...specsDir!==void 0?{specsDir}:{}}}function classifyOperations(specId,ops){if(!specId||specId.length===0){throw new Error("classifyOperations: specId is required")}if(!Array.isArray(ops))return[];const pathsWithGet=new Set;for(const op of ops){if(op&&typeof op.path==="string"&&typeof op.method==="string"&&op.method.toUpperCase()==="GET"){pathsWithGet.add(op.path)}}return ops.map(op=>classifyOne(specId,op,pathsWithGet))}function classifyOne(specId,op,pathsWithGet){const method=normaliseMethod(op.method);const path=op.path;const hasTerminalParam=pathHasTerminalParam(path);const parent=parentPath(path);const thisIsResource=pathsWithGet.has(path);const parentIsResource=parent!==null&&pathsWithGet.has(parent);const selfUri=pathToResourceUri(specId,path);const parentUri=parent!==null?pathToResourceUri(specId,parent):null;if(method==="GET"){if(hasTerminalParam){return{operationId:op.operationId,method,path,specId,expose:"both",resourceUriTemplate:selfUri}}return{operationId:op.operationId,method,path,specId,expose:"resource",resourceUriTemplate:selfUri}}const base={operationId:op.operationId,method,path,specId,expose:"tool"};if(method==="POST"){if(thisIsResource&&!hasTerminalParam){return{...base,emit:{kind:"listChanged",pathTemplate:path,resourceUriTemplate:selfUri}}}if(thisIsResource&&hasTerminalParam){return{...base,emit:{kind:"updated",pathTemplate:path,resourceUriTemplate:selfUri}}}if(parentIsResource&&parentUri!==null&&parent!==null){return{...base,emit:{kind:"updated",pathTemplate:parent,resourceUriTemplate:parentUri}}}return base}if(method==="PUT"||method==="PATCH"){if(thisIsResource){return{...base,emit:{kind:"updated",pathTemplate:path,resourceUriTemplate:selfUri}}}if(parentIsResource&&parentUri!==null&&parent!==null){return{...base,emit:{kind:"updated",pathTemplate:parent,resourceUriTemplate:parentUri}}}return base}if(method==="DELETE"){if(hasTerminalParam&&parentIsResource&&parentUri!==null&&parent!==null){return{...base,emit:{kind:"listChanged",pathTemplate:parent,resourceUriTemplate:parentUri}}}if(!hasTerminalParam&&thisIsResource){return{...base,emit:{kind:"listChanged",pathTemplate:path,resourceUriTemplate:selfUri}}}return base}return base}function applyClassificationOverrides(classified,rules){if(!Array.isArray(rules)||rules.length===0)return classified.slice();const compiled=rules.map(rule=>({rule,matcher:compileMatcher(rule.match)}));return classified.map(op=>{for(const{rule,matcher}of compiled){if(!matcher(op))continue;return applyOne(op,rule)}return op})}var KNOWN_METHODS=new Set(["GET","POST","PUT","PATCH","DELETE","HEAD","OPTIONS","TRACE"]);function normaliseMethod(m){const up=(m??"").toUpperCase();return KNOWN_METHODS.has(up)?up:up}function pathHasTerminalParam(path){if(typeof path!=="string"||path.length===0)return false;const segments=path.split("/").filter(s=>s.length>0);if(segments.length===0)return false;const last=segments[segments.length-1];return last.startsWith("{")&&last.endsWith("}")&&last.length>=3}function parentPath(path){if(typeof path!=="string"||path.length===0)return null;const segments=path.split("/").filter(s=>s.length>0);if(segments.length<=1)return null;return"/"+segments.slice(0,-1).join("/")}function pathToResourceUri(specId,path){const cleaned=path.startsWith("/")?path.slice(1):path;return`mcp+op://${specId}/${cleaned}`}function compileMatcher(match){const trimmed=(match??"").trim();const sep2=trimmed.indexOf(" ");if(sep2<=0||sep2===trimmed.length-1){return()=>false}const methodPattern=trimmed.slice(0,sep2).toUpperCase();const pathPattern=trimmed.slice(sep2+1);const pathRe=globToRegExp(pathPattern);return op=>{if(methodPattern!=="*"&&methodPattern!==op.method)return false;return pathRe.test(op.path)}}function globToRegExp(glob){const STAR_STAR="\0";const STAR="";const replaced=glob.replace(/\*\*/g,STAR_STAR).replace(/\*/g,STAR);const escaped=replaced.replace(/[.+^${}()|[\]\\]/g,"\\$&");const expanded=escaped.split(STAR_STAR).join(".*").split(STAR).join("[^/]*");return new RegExp(`^${expanded}$`)}function defaultEmitKindForMethod(method){return method==="DELETE"?"listChanged":"updated"}function applyOne(op,rule){let next={...op};if(rule.expose!==void 0){next={...next,expose:rule.expose}}if(rule.emits!==void 0){if(rule.emits==="none"){const{emit:_drop,...rest}=next;next=rest}else if(rule.emits==="self"){const target=pathToResourceUri(op.specId,op.path);next={...next,emit:{kind:next.emit?.kind??defaultEmitKindForMethod(op.method),pathTemplate:op.path,resourceUriTemplate:target}}}else if(rule.emits==="parent"){const parent=parentPath(op.path);if(parent!==null){next={...next,emit:{kind:next.emit?.kind??defaultEmitKindForMethod(op.method),pathTemplate:parent,resourceUriTemplate:pathToResourceUri(op.specId,parent)}}}else{const{emit:_drop,...rest}=next;next=rest}}}return next}var TEMPLATE_PLACEHOLDER_RE=/\{([^{}]+)\}/g;function renderResourceUri(template,args){if(typeof template!=="string"||template.length===0){return{ok:false,missing:[]}}const argObj=args!==null&&typeof args==="object"?args:{};const missing=[];const uri=template.replace(TEMPLATE_PLACEHOLDER_RE,(_match,name)=>{const value=argObj[name];if(value===void 0||value===null){missing.push(name);return""}if(typeof value==="object"){missing.push(name);return""}if(typeof value==="number"&&!Number.isFinite(value)){missing.push(name);return""}return encodeURIComponent(String(value))});if(missing.length>0)return{ok:false,missing};return{ok:true,uri}}function buildResourceChangeNotification(classification,args){const emit=classification.emit;if(!emit)return{notification:null,reason:"no-emit"};if(emit.kind==="listChanged"){const rendered2=renderResourceUri(emit.resourceUriTemplate,args);if(!rendered2.ok){return{notification:null,reason:"unresolved-template",missing:rendered2.missing}}return{notification:{method:"notifications/resources/list_changed"}}}const rendered=renderResourceUri(emit.resourceUriTemplate,args);if(!rendered.ok){return{notification:null,reason:"unresolved-template",missing:rendered.missing}}return{notification:{method:"notifications/resources/updated",params:{uri:rendered.uri}}}}var ClassificationRegistry=class{byToolName=new Map;register(toolName,classification){if(typeof toolName!=="string"||toolName.length===0){throw new Error("ClassificationRegistry.register: toolName must be a non-empty string")}const prev=this.byToolName.get(toolName);this.byToolName.set(toolName,classification);return prev}registerAll(classifications){let added=0;let replaced=0;for(const c of classifications){const toolName=`${c.specId}.${c.operationId}`;const prev=this.byToolName.get(toolName);this.byToolName.set(toolName,c);if(prev)replaced++;else added++}return{added,replaced}}lookup(toolName){return this.byToolName.get(toolName)}unregister(toolName){return this.byToolName.delete(toolName)}clear(){this.byToolName.clear()}size(){return this.byToolName.size}getAll(){const out=[];for(const[toolName,classification]of this.byToolName.entries()){out.push({toolName,classification})}return out}buildNotificationForCall(toolName,args){const classification=this.byToolName.get(toolName);if(!classification)return{notification:null,reason:"no-emit"};return buildResourceChangeNotification(classification,args)}};0&&(module.exports={BundlePinnedError,BundlePushJwtVerifier,BundleStore,ClassificationRegistry,FilesystemSkillsSource,Hs256AuditSigner,InlineSource,MemoryAuditStore,NpmSource,OverlayParseError,Rs256AuditSigner,SKILL_AUDIT_ERROR_MESSAGE_MAX,SKILL_AUDIT_GENESIS_PREV_HASH,SKILL_AUDIT_KEYS,SKILL_AUDIT_QUEUE_MAX,SaasPullSource,SkillAuditWriter,SkillAuditWriterToken,SkillDependencyCycleError,SkillDependencyInvariantError,SkillDependencyMissingError,StaticSource,StorageAdapterAuditStore,WebhookReplayGuard,applyClassificationOverrides,applyEnvironmentOverlay,buildFrontMcpConfigFromManifest,buildKnownOps,buildResourceChangeNotification,bundleDigest,bundleSkillToActions,bundleSourceSchema,canonicalize,canonicalizeRecordForSigning,classifyOne,classifyOperations,compileSkilledBundleFromOpenApi,createBundleSource,createSkillAuditMetrics,crossValidate,crossValidateManifest,dedupeOpReferences,defaultAuditSignatureVerifier,deployManifestSchema,diffBundles,extractOpReferences,formatDiffSummary,hasSkillAuditFactory,linkRecord,loadFilesystemSkill,nextPrevHash,npmSourceSchema,parseOverlay,parseSkillFrontmatter,renderResourceUri,resolveSkillLoadOrder,resolvedBundleSchema,saasSourceSchema,setSkillAuditFactory,signatureKeySchema,staticSourceSchema,validateOpReferences,verifyBundleSignature,verifyChain});
|