@frontmcp/adapters 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/esm/index.mjs +396 -385
  2. package/esm/openapi/index.mjs +396 -385
  3. package/esm/package.json +18 -6
  4. package/esm/skills/index.mjs +2117 -0
  5. package/index.js +407 -389
  6. package/openapi/index.d.ts +11 -1
  7. package/openapi/index.d.ts.map +1 -1
  8. package/openapi/index.js +407 -389
  9. package/openapi/openapi.adapter.d.ts +2 -2
  10. package/openapi/openapi.adapter.d.ts.map +1 -1
  11. package/openapi/openapi.tool.d.ts.map +1 -1
  12. package/openapi/openapi.types.d.ts +3 -3
  13. package/openapi/openapi.types.d.ts.map +1 -1
  14. package/openapi/openapi.utils.d.ts.map +1 -1
  15. package/package.json +18 -6
  16. package/skills/audit/audit-chain.d.ts +81 -0
  17. package/skills/audit/audit-chain.d.ts.map +1 -0
  18. package/skills/audit/audit-config.d.ts +87 -0
  19. package/skills/audit/audit-config.d.ts.map +1 -0
  20. package/skills/audit/audit-record.types.d.ts +114 -0
  21. package/skills/audit/audit-record.types.d.ts.map +1 -0
  22. package/skills/audit/audit-signer.d.ts +72 -0
  23. package/skills/audit/audit-signer.d.ts.map +1 -0
  24. package/skills/audit/audit-store.d.ts +84 -0
  25. package/skills/audit/audit-store.d.ts.map +1 -0
  26. package/skills/audit/audit-writer.d.ts +175 -0
  27. package/skills/audit/audit-writer.d.ts.map +1 -0
  28. package/skills/audit/index.d.ts +7 -0
  29. package/skills/audit/index.d.ts.map +1 -0
  30. package/skills/bundle/bundle-diff.d.ts +30 -0
  31. package/skills/bundle/bundle-diff.d.ts.map +1 -0
  32. package/skills/bundle/bundle.schema.d.ts +101 -0
  33. package/skills/bundle/bundle.schema.d.ts.map +1 -0
  34. package/skills/bundle/bundle.store.d.ts +143 -0
  35. package/skills/bundle/bundle.store.d.ts.map +1 -0
  36. package/skills/bundle/bundle.types.d.ts +143 -0
  37. package/skills/bundle/bundle.types.d.ts.map +1 -0
  38. package/skills/bundle/overlay-parser.d.ts +42 -0
  39. package/skills/bundle/overlay-parser.d.ts.map +1 -0
  40. package/skills/dependency/skill-dag.d.ts +39 -0
  41. package/skills/dependency/skill-dag.d.ts.map +1 -0
  42. package/skills/index.d.ts +17 -0
  43. package/skills/index.d.ts.map +1 -0
  44. package/skills/index.js +2192 -0
  45. package/skills/security/bundle-signature.d.ts +44 -0
  46. package/skills/security/bundle-signature.d.ts.map +1 -0
  47. package/skills/security/jwt-verifier.d.ts +38 -0
  48. package/skills/security/jwt-verifier.d.ts.map +1 -0
  49. package/skills/security/webhook-replay-guard.d.ts +52 -0
  50. package/skills/security/webhook-replay-guard.d.ts.map +1 -0
  51. package/skills/source-options.d.ts +55 -0
  52. package/skills/source-options.d.ts.map +1 -0
  53. package/skills/sources/filesystem-skills.source.d.ts +127 -0
  54. package/skills/sources/filesystem-skills.source.d.ts.map +1 -0
  55. package/skills/sources/index.d.ts +10 -0
  56. package/skills/sources/index.d.ts.map +1 -0
  57. package/skills/sources/npm.source.d.ts +30 -0
  58. package/skills/sources/npm.source.d.ts.map +1 -0
  59. package/skills/sources/saas-pull.source.d.ts +47 -0
  60. package/skills/sources/saas-pull.source.d.ts.map +1 -0
  61. package/skills/sources/skill-bundle-source.interface.d.ts +36 -0
  62. package/skills/sources/skill-bundle-source.interface.d.ts.map +1 -0
  63. package/skills/sources/static.source.d.ts +30 -0
  64. package/skills/sources/static.source.d.ts.map +1 -0
@@ -0,0 +1,2192 @@
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
+ FilesystemSkillsSource: () => FilesystemSkillsSource,
37
+ Hs256AuditSigner: () => Hs256AuditSigner,
38
+ MemoryAuditStore: () => MemoryAuditStore,
39
+ NpmSource: () => NpmSource,
40
+ OverlayParseError: () => OverlayParseError,
41
+ Rs256AuditSigner: () => Rs256AuditSigner,
42
+ SKILL_AUDIT_ERROR_MESSAGE_MAX: () => SKILL_AUDIT_ERROR_MESSAGE_MAX,
43
+ SKILL_AUDIT_GENESIS_PREV_HASH: () => SKILL_AUDIT_GENESIS_PREV_HASH,
44
+ SKILL_AUDIT_KEYS: () => SKILL_AUDIT_KEYS,
45
+ SKILL_AUDIT_QUEUE_MAX: () => SKILL_AUDIT_QUEUE_MAX,
46
+ SaasPullSource: () => SaasPullSource,
47
+ SkillAuditWriter: () => SkillAuditWriter,
48
+ SkillAuditWriterToken: () => SkillAuditWriterToken,
49
+ SkillDependencyCycleError: () => SkillDependencyCycleError,
50
+ SkillDependencyInvariantError: () => SkillDependencyInvariantError,
51
+ SkillDependencyMissingError: () => SkillDependencyMissingError,
52
+ StaticSource: () => StaticSource,
53
+ StorageAdapterAuditStore: () => StorageAdapterAuditStore,
54
+ WebhookReplayGuard: () => WebhookReplayGuard,
55
+ bundleDigest: () => bundleDigest,
56
+ bundleSkillToActions: () => bundleSkillToActions,
57
+ bundleSourceSchema: () => bundleSourceSchema,
58
+ canonicalize: () => canonicalize,
59
+ canonicalizeRecordForSigning: () => canonicalizeRecordForSigning,
60
+ createBundleSource: () => createBundleSource,
61
+ crossValidate: () => crossValidate,
62
+ defaultAuditSignatureVerifier: () => defaultAuditSignatureVerifier,
63
+ diffBundles: () => diffBundles,
64
+ formatDiffSummary: () => formatDiffSummary,
65
+ hasSkillAuditFactory: () => import_sdk.hasSkillAuditFactory,
66
+ linkRecord: () => linkRecord,
67
+ loadFilesystemSkill: () => loadFilesystemSkill,
68
+ nextPrevHash: () => nextPrevHash,
69
+ npmSourceSchema: () => npmSourceSchema,
70
+ parseOverlay: () => parseOverlay,
71
+ parseSkillFrontmatter: () => parseSkillFrontmatter,
72
+ resolveSkillLoadOrder: () => resolveSkillLoadOrder,
73
+ resolvedBundleSchema: () => resolvedBundleSchema,
74
+ saasSourceSchema: () => saasSourceSchema,
75
+ setSkillAuditFactory: () => import_sdk.setSkillAuditFactory,
76
+ signatureKeySchema: () => signatureKeySchema,
77
+ staticSourceSchema: () => staticSourceSchema,
78
+ verifyBundleSignature: () => verifyBundleSignature,
79
+ verifyChain: () => verifyChain
80
+ });
81
+ module.exports = __toCommonJS(skills_exports);
82
+
83
+ // libs/adapters/src/skills/bundle/bundle.types.ts
84
+ function bundleSkillToActions(skill, ops) {
85
+ return skill.operationIds.map((opId) => {
86
+ const op = ops[opId];
87
+ if (!op) {
88
+ throw new Error(`bundleSkillToActions: skill "${skill.id}" references unknown operationId "${opId}"`);
89
+ }
90
+ const action = {
91
+ actionId: op.operationId,
92
+ summary: op.summary ?? `${op.httpMethod} ${op.pathTemplate}`,
93
+ ...op.description !== void 0 && { description: op.description },
94
+ inputJsonSchema: op.inputSchema,
95
+ outputJsonSchema: op.outputSchema,
96
+ ...op.requiredAuthorities !== void 0 && { requiredAuthorities: op.requiredAuthorities }
97
+ };
98
+ return action;
99
+ });
100
+ }
101
+
102
+ // libs/adapters/src/skills/bundle/bundle.schema.ts
103
+ var import_lazy_zod = require("@frontmcp/lazy-zod");
104
+ var httpMethodSchema = import_lazy_zod.z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]);
105
+ var pathTemplateRegex = /^\/[^\s?#]*$/;
106
+ var FORBIDDEN_PATH_FRAGMENTS = ["..", "\\", "`", "$(", "${"];
107
+ var pathTemplateSchema = import_lazy_zod.z.string().min(1).max(2048).refine((v) => pathTemplateRegex.test(v), {
108
+ message: "pathTemplate must start with `/` and contain no whitespace, `?`, or `#`"
109
+ }).refine((v) => !FORBIDDEN_PATH_FRAGMENTS.some((bad) => v.includes(bad)), {
110
+ message: "pathTemplate contains a forbidden character or fragment (`..`, backtick, `$(`, etc.)"
111
+ });
112
+ var jsonSchemaShape = import_lazy_zod.z.record(import_lazy_zod.z.string(), import_lazy_zod.z.unknown());
113
+ var authoritiesPolicySchema = import_lazy_zod.z.record(import_lazy_zod.z.string(), import_lazy_zod.z.unknown());
114
+ var authBindingSchema = import_lazy_zod.z.discriminatedUnion("kind", [
115
+ import_lazy_zod.z.object({ kind: import_lazy_zod.z.literal("none") }).strict(),
116
+ import_lazy_zod.z.object({
117
+ kind: import_lazy_zod.z.literal("bearer"),
118
+ vaultRef: import_lazy_zod.z.string().min(1).max(256),
119
+ passthroughCallerToken: import_lazy_zod.z.boolean().optional()
120
+ }).strict(),
121
+ import_lazy_zod.z.object({
122
+ kind: import_lazy_zod.z.literal("apiKey"),
123
+ in: import_lazy_zod.z.enum(["header", "query"]),
124
+ name: import_lazy_zod.z.string().min(1).max(128).regex(/^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/, "apiKey name must match RFC 7230 token grammar"),
125
+ vaultRef: import_lazy_zod.z.string().min(1).max(256)
126
+ }).strict(),
127
+ import_lazy_zod.z.object({
128
+ kind: import_lazy_zod.z.literal("oauth2"),
129
+ flow: import_lazy_zod.z.literal("client_credentials"),
130
+ vaultRef: import_lazy_zod.z.string().min(1).max(256)
131
+ }).strict()
132
+ ]);
133
+ var serviceDescriptorSchema = import_lazy_zod.z.object({
134
+ id: import_lazy_zod.z.string().min(1).max(128).regex(/^[a-zA-Z0-9_-]+$/, "service id must be alphanumeric / `-` / `_`"),
135
+ baseUrl: import_lazy_zod.z.string().url().max(2048),
136
+ description: import_lazy_zod.z.string().max(4096).optional()
137
+ }).strict();
138
+ var parameterMapperSchema = import_lazy_zod.z.object({
139
+ inputKey: import_lazy_zod.z.string().min(1).max(256),
140
+ type: import_lazy_zod.z.enum(["path", "query", "header", "cookie", "body"]),
141
+ key: import_lazy_zod.z.string().min(1).max(256),
142
+ required: import_lazy_zod.z.boolean().optional(),
143
+ style: import_lazy_zod.z.string().max(64).optional(),
144
+ explode: import_lazy_zod.z.boolean().optional(),
145
+ serialization: import_lazy_zod.z.record(import_lazy_zod.z.string(), import_lazy_zod.z.unknown()).optional(),
146
+ security: import_lazy_zod.z.record(import_lazy_zod.z.string(), import_lazy_zod.z.unknown()).optional()
147
+ }).passthrough();
148
+ var operationDescriptorSchema = import_lazy_zod.z.object({
149
+ operationId: import_lazy_zod.z.string().min(1).max(256).regex(/^[a-zA-Z0-9_.\-:]+$/, "operationId must be alphanumeric / `-` / `_` / `.` / `:`"),
150
+ serviceId: import_lazy_zod.z.string().min(1).max(128),
151
+ httpMethod: httpMethodSchema,
152
+ pathTemplate: pathTemplateSchema,
153
+ inputSchema: jsonSchemaShape,
154
+ outputSchema: jsonSchemaShape,
155
+ mapper: import_lazy_zod.z.array(parameterMapperSchema).max(256),
156
+ authBindingRef: import_lazy_zod.z.string().min(1).max(128),
157
+ requiredAuthorities: authoritiesPolicySchema.optional(),
158
+ maxResponseBytes: import_lazy_zod.z.number().int().positive().max(64 * 1024 * 1024).optional(),
159
+ timeoutMs: import_lazy_zod.z.number().int().positive().max(6e5).optional(),
160
+ summary: import_lazy_zod.z.string().max(512).optional(),
161
+ description: import_lazy_zod.z.string().max(8192).optional()
162
+ }).strict();
163
+ var bundledSkillSchema = import_lazy_zod.z.object({
164
+ id: import_lazy_zod.z.string().min(1).max(128).regex(/^[a-zA-Z0-9_.-]+$/),
165
+ name: import_lazy_zod.z.string().min(1).max(256),
166
+ description: import_lazy_zod.z.string().max(2048),
167
+ instructions: import_lazy_zod.z.string().max(256 * 1024),
168
+ tags: import_lazy_zod.z.array(import_lazy_zod.z.string().max(64)).max(64).optional(),
169
+ operationIds: import_lazy_zod.z.array(import_lazy_zod.z.string().min(1).max(256)).min(0).max(2e3),
170
+ requiredAuthorities: authoritiesPolicySchema.optional(),
171
+ /**
172
+ * Other skill ids this skill depends on. Hosts that consume the bundle
173
+ * register skills in dependency order; cycles and missing deps are caught
174
+ * at apply time. Field name aligned with the agentskills Skill Package
175
+ * Manifest proposal (https://github.com/agentskills/agentskills/discussions/210)
176
+ * so bundles round-trip across runtimes without a schema bump.
177
+ */
178
+ requires: import_lazy_zod.z.array(import_lazy_zod.z.string().min(1).max(128)).max(64).optional()
179
+ }).strict();
180
+ var integritySchema = import_lazy_zod.z.object({
181
+ alg: import_lazy_zod.z.enum(["RS256", "EdDSA"]),
182
+ keyId: import_lazy_zod.z.string().min(1).max(256),
183
+ signature: import_lazy_zod.z.string().min(1).max(8192),
184
+ digest: import_lazy_zod.z.string().min(1).max(256)
185
+ }).strict();
186
+ var resolvedBundleSchema = import_lazy_zod.z.object({
187
+ schemaVersion: import_lazy_zod.z.literal(1),
188
+ bundleId: import_lazy_zod.z.string().min(1).max(256).regex(/^[a-zA-Z0-9_.\-:]+$/),
189
+ version: import_lazy_zod.z.string().min(1).max(256),
190
+ generatedAt: import_lazy_zod.z.string().min(1).refine((v) => !Number.isNaN(Date.parse(v)), { message: "generatedAt must be ISO 8601 parsable" }),
191
+ sourceDigest: import_lazy_zod.z.string().min(1).max(256).regex(/^[a-fA-F0-9]+$/, "sourceDigest must be a hex sha256"),
192
+ services: import_lazy_zod.z.array(serviceDescriptorSchema).min(1).max(64),
193
+ authBindings: import_lazy_zod.z.record(import_lazy_zod.z.string().min(1).max(128), authBindingSchema),
194
+ skills: import_lazy_zod.z.array(bundledSkillSchema).max(1e3),
195
+ operations: import_lazy_zod.z.record(import_lazy_zod.z.string().min(1).max(256), operationDescriptorSchema),
196
+ integrity: integritySchema.optional()
197
+ }).strict();
198
+ function crossValidate(bundle) {
199
+ const errors = [];
200
+ const serviceIds = new Set(bundle.services.map((s) => s.id));
201
+ const authRefs = new Set(Object.keys(bundle.authBindings));
202
+ const opIds = new Set(Object.keys(bundle.operations));
203
+ for (const [opId, op] of Object.entries(bundle.operations)) {
204
+ if (op.operationId !== opId) {
205
+ errors.push(`operation key "${opId}" does not match operationId "${op.operationId}"`);
206
+ }
207
+ if (!serviceIds.has(op.serviceId)) {
208
+ errors.push(`operation "${opId}" references unknown service "${op.serviceId}"`);
209
+ }
210
+ if (!authRefs.has(op.authBindingRef)) {
211
+ errors.push(`operation "${opId}" references unknown authBinding "${op.authBindingRef}"`);
212
+ }
213
+ }
214
+ const seenSkillIds = /* @__PURE__ */ new Set();
215
+ for (const skill of bundle.skills) {
216
+ if (seenSkillIds.has(skill.id)) {
217
+ errors.push(`duplicate skill id "${skill.id}"`);
218
+ }
219
+ seenSkillIds.add(skill.id);
220
+ for (const opId of skill.operationIds) {
221
+ if (!opIds.has(opId)) {
222
+ errors.push(`skill "${skill.id}" references unknown operationId "${opId}"`);
223
+ }
224
+ }
225
+ }
226
+ return errors.length === 0 ? { ok: true } : { ok: false, errors };
227
+ }
228
+
229
+ // libs/adapters/src/skills/bundle/overlay-parser.ts
230
+ var yaml = __toESM(require("js-yaml"));
231
+ var OVERLAY_BUNDLE_KEY = "x-frontmcp-bundle";
232
+ var OverlayParseError = class extends Error {
233
+ constructor(message, errors) {
234
+ super(message);
235
+ this.errors = errors;
236
+ this.name = "OverlayParseError";
237
+ }
238
+ };
239
+ function parseRaw(input) {
240
+ switch (input.kind) {
241
+ case "yaml":
242
+ try {
243
+ return yaml.load(input.content);
244
+ } catch (e) {
245
+ throw new OverlayParseError(`Overlay YAML parse failed: ${e.message}`);
246
+ }
247
+ case "json":
248
+ try {
249
+ return JSON.parse(input.content);
250
+ } catch (e) {
251
+ throw new OverlayParseError(`Overlay JSON parse failed: ${e.message}`);
252
+ }
253
+ case "object":
254
+ return input.content;
255
+ }
256
+ }
257
+ function extractBundle(overlay) {
258
+ if (overlay === null || typeof overlay !== "object") {
259
+ throw new OverlayParseError("Overlay root must be an object");
260
+ }
261
+ const root = overlay;
262
+ if (OVERLAY_BUNDLE_KEY in root) {
263
+ return root[OVERLAY_BUNDLE_KEY];
264
+ }
265
+ const info = root["info"];
266
+ if (info && typeof info === "object" && OVERLAY_BUNDLE_KEY in info) {
267
+ return info[OVERLAY_BUNDLE_KEY];
268
+ }
269
+ if ("schemaVersion" in root && "bundleId" in root) {
270
+ return root;
271
+ }
272
+ throw new OverlayParseError(
273
+ "Overlay does not contain `info.x-frontmcp-bundle` or a bare bundle object with schemaVersion + bundleId"
274
+ );
275
+ }
276
+ function parseOverlay(input) {
277
+ const raw = parseRaw(input);
278
+ const bundleCandidate = extractBundle(raw);
279
+ const result = resolvedBundleSchema.safeParse(bundleCandidate);
280
+ if (!result.success) {
281
+ const errors = result.error.issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`);
282
+ throw new OverlayParseError(`Bundle schema validation failed (${errors.length} issue(s))`, errors);
283
+ }
284
+ const cross = crossValidate(result.data);
285
+ if (!cross.ok) {
286
+ throw new OverlayParseError(
287
+ `Bundle cross-reference validation failed (${cross.errors.length} issue(s))`,
288
+ cross.errors
289
+ );
290
+ }
291
+ return result.data;
292
+ }
293
+
294
+ // libs/adapters/src/skills/bundle/bundle-diff.ts
295
+ 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.maxResponseBytes === b.maxResponseBytes && a.timeoutMs === b.timeoutMs;
296
+ 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);
297
+ function diffBundles(oldBundle, newBundle) {
298
+ const oldSkills = new Map((oldBundle?.skills ?? []).map((s) => [s.id, s]));
299
+ const newSkills = new Map(newBundle.skills.map((s) => [s.id, s]));
300
+ const addedSkillIds = [];
301
+ const removedSkillIds = [];
302
+ const changedSkillIds = [];
303
+ for (const [id, skill] of newSkills) {
304
+ const prev = oldSkills.get(id);
305
+ if (!prev) addedSkillIds.push(id);
306
+ else if (!skillEqual(prev, skill)) changedSkillIds.push(id);
307
+ }
308
+ for (const id of oldSkills.keys()) {
309
+ if (!newSkills.has(id)) removedSkillIds.push(id);
310
+ }
311
+ const oldOps = oldBundle?.operations ?? {};
312
+ const newOps = newBundle.operations;
313
+ const addedOperationIds = [];
314
+ const removedOperationIds = [];
315
+ const changedOperationIds = [];
316
+ for (const [id, op] of Object.entries(newOps)) {
317
+ const prev = oldOps[id];
318
+ if (!prev) addedOperationIds.push(id);
319
+ else if (!opEqual(prev, op)) changedOperationIds.push(id);
320
+ }
321
+ for (const id of Object.keys(oldOps)) {
322
+ if (!(id in newOps)) removedOperationIds.push(id);
323
+ }
324
+ 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;
325
+ return {
326
+ addedSkillIds,
327
+ removedSkillIds,
328
+ changedSkillIds,
329
+ addedOperationIds,
330
+ removedOperationIds,
331
+ changedOperationIds,
332
+ isNoOp
333
+ };
334
+ }
335
+ function formatDiffSummary(diff) {
336
+ if (diff.isNoOp) return "no-op";
337
+ return [
338
+ `+${diff.addedSkillIds.length}/-${diff.removedSkillIds.length}/~${diff.changedSkillIds.length} skills`,
339
+ `+${diff.addedOperationIds.length}/-${diff.removedOperationIds.length}/~${diff.changedOperationIds.length} ops`
340
+ ].join(", ");
341
+ }
342
+
343
+ // libs/adapters/src/skills/bundle/bundle.store.ts
344
+ var KNOWN_BUNDLE_SOURCES = ["static", "npm", "saas-pull", "webhook", "filesystem", "unknown"];
345
+ function normalizeBundleSource(value) {
346
+ if (typeof value !== "string") return "unknown";
347
+ return KNOWN_BUNDLE_SOURCES.includes(value) ? value : "unknown";
348
+ }
349
+ function classifySwapError(err) {
350
+ if (err instanceof BundlePinnedError) return "pinned";
351
+ if (err instanceof Error) {
352
+ const name = err.name;
353
+ const msg = err.message.toLowerCase();
354
+ if (name === "BundlePinnedError") return "pinned";
355
+ if (msg.includes("bundle is required") || msg.includes("malformed")) return "invalid_bundle";
356
+ if (msg.includes("listener")) return "listener_error";
357
+ }
358
+ return "unknown";
359
+ }
360
+ var BundleStore = class {
361
+ active;
362
+ listeners = /* @__PURE__ */ new Set();
363
+ history = [];
364
+ pinnedVersion;
365
+ historySize;
366
+ telemetry;
367
+ pullsCounter;
368
+ constructor(options = {}) {
369
+ const requested = options.historySize ?? 3;
370
+ if (!Number.isInteger(requested) || requested < 1) {
371
+ throw new Error("BundleStore: historySize must be a positive integer");
372
+ }
373
+ this.historySize = requested;
374
+ this.telemetry = options.telemetry;
375
+ this.pullsCounter = this.telemetry?.createCounter(
376
+ "frontmcp_skills_bundle_pulls_total",
377
+ "Number of bundle pulls (swaps) attempted, partitioned by status / source."
378
+ );
379
+ }
380
+ current() {
381
+ return this.active;
382
+ }
383
+ /**
384
+ * Swap to a new bundle. Returns the structural diff (no-op = same version
385
+ * with no field changes; the swap itself still fires listeners with isNoOp=true
386
+ * so observers can record the heartbeat).
387
+ *
388
+ * Throws when the store is pinned. The caller should check `isPinned()` first
389
+ * if it wants to skip silently rather than throw.
390
+ *
391
+ * @param next - new bundle to activate
392
+ * @param source - optional source label (e.g. `npm`, `saas-pull`, `webhook`)
393
+ * used as a counter / span attribute. When omitted, `unknown`
394
+ * is reported.
395
+ */
396
+ swap(next, source) {
397
+ if (!next) {
398
+ throw new Error("BundleStore.swap: bundle is required");
399
+ }
400
+ const sourceLabel = normalizeBundleSource(source);
401
+ if (this.telemetry) {
402
+ const span = this.telemetry.startSpan("skill.bundle.swap", {
403
+ source: sourceLabel,
404
+ bundle_id: next.bundleId,
405
+ version: next.version,
406
+ skill_count: next.skills.length,
407
+ ...this.active?.version ? { from_version: this.active.version } : {}
408
+ });
409
+ try {
410
+ if (this.pinnedVersion !== void 0) {
411
+ throw new BundlePinnedError(this.pinnedVersion);
412
+ }
413
+ const diff = this.commit(next, "normal");
414
+ span.setAttributes({ status: "ok", is_no_op: diff.isNoOp });
415
+ span.end();
416
+ this.pullsCounter?.inc(1, { status: "ok", source: sourceLabel });
417
+ return diff;
418
+ } catch (err) {
419
+ const reason = classifySwapError(err);
420
+ if (span.endWithError) {
421
+ span.endWithError(err instanceof Error ? err : String(err));
422
+ } else {
423
+ span.recordError?.(err instanceof Error ? err : new Error(String(err)));
424
+ span.end();
425
+ }
426
+ this.pullsCounter?.inc(1, { status: "error", source: sourceLabel, reason });
427
+ throw err;
428
+ }
429
+ }
430
+ if (this.pinnedVersion !== void 0) {
431
+ throw new BundlePinnedError(this.pinnedVersion);
432
+ }
433
+ return this.commit(next, "normal");
434
+ }
435
+ /**
436
+ * Pin the active bundle to a specific historical version. Pinned stores
437
+ * reject `swap()` so source-driven updates accumulate elsewhere without
438
+ * activating. Returns the diff of the swap that activated the pinned
439
+ * version, or undefined if the pinned version was already active.
440
+ *
441
+ * Throws when `version` is unknown to the store's history.
442
+ */
443
+ pin(version) {
444
+ if (this.active && this.active.version === version) {
445
+ this.pinnedVersion = version;
446
+ return void 0;
447
+ }
448
+ const entry = this.history.find((h) => h.bundle.version === version);
449
+ if (!entry) {
450
+ throw new Error(`BundleStore.pin: version "${version}" not in history`);
451
+ }
452
+ const diff = this.commit(entry.bundle, "pin");
453
+ this.pinnedVersion = version;
454
+ return diff;
455
+ }
456
+ /**
457
+ * Re-activate the most recent bundle in history (the bundle that was
458
+ * active before the current one). Useful after a bad swap is detected.
459
+ * Returns the diff. Throws when no prior bundle is available.
460
+ *
461
+ * Rollback does NOT pin — sources can resume auto-swapping after rollback.
462
+ */
463
+ rollback() {
464
+ const prior = this.history[this.history.length - 1];
465
+ if (!prior) {
466
+ throw new Error("BundleStore.rollback: no prior bundle in history");
467
+ }
468
+ this.pinnedVersion = void 0;
469
+ return this.commit(prior.bundle, "rollback");
470
+ }
471
+ /** Lift the pin so `swap()` accepts new bundles again. */
472
+ unpin() {
473
+ this.pinnedVersion = void 0;
474
+ }
475
+ /** True when the store is currently pinned. */
476
+ isPinned() {
477
+ return this.pinnedVersion !== void 0;
478
+ }
479
+ /** Pinned version string, or undefined when unpinned. */
480
+ pinned() {
481
+ return this.pinnedVersion;
482
+ }
483
+ /**
484
+ * Snapshot of the history ring (oldest first; does not include the active
485
+ * bundle). Returned array is a shallow copy — safe to inspect.
486
+ */
487
+ historySnapshot() {
488
+ return this.history.map((h) => ({
489
+ version: h.bundle.version,
490
+ bundleId: h.bundle.bundleId,
491
+ validatedAt: h.validatedAt
492
+ }));
493
+ }
494
+ /** Subscribe to swap events. Returns an unsubscribe function. */
495
+ subscribe(fn) {
496
+ this.listeners.add(fn);
497
+ return () => {
498
+ this.listeners.delete(fn);
499
+ };
500
+ }
501
+ /** Reset to empty state. Used by tests. Does NOT fire listeners. */
502
+ reset() {
503
+ this.active = void 0;
504
+ this.history = [];
505
+ this.pinnedVersion = void 0;
506
+ }
507
+ /** Commit a new active bundle (shared by swap / pin / rollback). */
508
+ commit(next, reason) {
509
+ const previous = this.active;
510
+ const diff = diffBundles(previous, next);
511
+ if (previous) {
512
+ const lastInHistory = this.history[this.history.length - 1];
513
+ if (!lastInHistory || lastInHistory.bundle.version !== previous.version) {
514
+ this.history.push({ bundle: previous, validatedAt: Date.now() });
515
+ if (this.history.length > this.historySize) {
516
+ this.history.splice(0, this.history.length - this.historySize);
517
+ }
518
+ }
519
+ }
520
+ this.active = next;
521
+ for (const fn of this.listeners) {
522
+ try {
523
+ fn({ previous, current: next, diff, reason });
524
+ } catch {
525
+ }
526
+ }
527
+ return diff;
528
+ }
529
+ };
530
+ var BundlePinnedError = class extends Error {
531
+ constructor(pinnedVersion) {
532
+ super(`BundleStore is pinned to version "${pinnedVersion}"; new swaps are rejected until unpin().`);
533
+ this.pinnedVersion = pinnedVersion;
534
+ this.name = "BundlePinnedError";
535
+ }
536
+ };
537
+
538
+ // libs/adapters/src/skills/dependency/skill-dag.ts
539
+ var SkillDependencyCycleError = class extends Error {
540
+ constructor(cycle) {
541
+ super(`skill dependency cycle: ${cycle.join(" -> ")}`);
542
+ this.cycle = cycle;
543
+ this.name = "SkillDependencyCycleError";
544
+ }
545
+ };
546
+ var SkillDependencyMissingError = class extends Error {
547
+ constructor(skillId, missingId) {
548
+ super(`skill "${skillId}" requires unknown skill "${missingId}"`);
549
+ this.skillId = skillId;
550
+ this.missingId = missingId;
551
+ this.name = "SkillDependencyMissingError";
552
+ }
553
+ };
554
+ var SkillDependencyInvariantError = class extends Error {
555
+ constructor(message) {
556
+ super(message);
557
+ this.name = "SkillDependencyInvariantError";
558
+ }
559
+ };
560
+ function resolveSkillLoadOrder(skills) {
561
+ const byId = /* @__PURE__ */ new Map();
562
+ for (const skill of skills) {
563
+ if (byId.has(skill.id)) {
564
+ byId.set(skill.id, skill);
565
+ continue;
566
+ }
567
+ byId.set(skill.id, skill);
568
+ }
569
+ const inDegree = /* @__PURE__ */ new Map();
570
+ const requiredBy = /* @__PURE__ */ new Map();
571
+ for (const id of byId.keys()) {
572
+ inDegree.set(id, 0);
573
+ requiredBy.set(id, []);
574
+ }
575
+ for (const skill of byId.values()) {
576
+ const deps = skill.requires ?? [];
577
+ for (const depId of deps) {
578
+ if (!byId.has(depId)) {
579
+ throw new SkillDependencyMissingError(skill.id, depId);
580
+ }
581
+ inDegree.set(skill.id, (inDegree.get(skill.id) ?? 0) + 1);
582
+ const reverse = requiredBy.get(depId) ?? [];
583
+ reverse.push(skill.id);
584
+ requiredBy.set(depId, reverse);
585
+ }
586
+ }
587
+ const ready = [...byId.keys()].filter((id) => inDegree.get(id) === 0).sort();
588
+ const order = [];
589
+ while (ready.length > 0) {
590
+ const next = ready.shift();
591
+ if (next === void 0) {
592
+ throw new SkillDependencyInvariantError(
593
+ "resolveSkillLoadOrder: invariant broken \u2014 ready queue drained mid-iteration"
594
+ );
595
+ }
596
+ const nextSkill = byId.get(next);
597
+ if (!nextSkill) {
598
+ throw new SkillDependencyInvariantError(
599
+ `resolveSkillLoadOrder: invariant broken \u2014 id "${next}" missing from byId map`
600
+ );
601
+ }
602
+ order.push(nextSkill);
603
+ for (const dependent of requiredBy.get(next) ?? []) {
604
+ const remaining = (inDegree.get(dependent) ?? 0) - 1;
605
+ inDegree.set(dependent, remaining);
606
+ if (remaining === 0) {
607
+ let i = 0;
608
+ while (i < ready.length && ready[i] < dependent) i++;
609
+ ready.splice(i, 0, dependent);
610
+ }
611
+ }
612
+ }
613
+ if (order.length === byId.size) {
614
+ return order;
615
+ }
616
+ const remainingIds = [...byId.keys()].filter((id) => (inDegree.get(id) ?? 0) > 0);
617
+ const cycle = findCycle(remainingIds, byId);
618
+ throw new SkillDependencyCycleError(cycle);
619
+ }
620
+ function findCycle(remaining, byId) {
621
+ const adj = /* @__PURE__ */ new Map();
622
+ for (const id of remaining) {
623
+ adj.set(
624
+ id,
625
+ (byId.get(id)?.requires ?? []).filter((d) => remaining.includes(d))
626
+ );
627
+ }
628
+ const WHITE = 0;
629
+ const GRAY = 1;
630
+ const BLACK = 2;
631
+ const color = /* @__PURE__ */ new Map();
632
+ for (const id of remaining) color.set(id, WHITE);
633
+ const stack = [];
634
+ function dfs(node) {
635
+ color.set(node, GRAY);
636
+ stack.push(node);
637
+ for (const next of adj.get(node) ?? []) {
638
+ const c = color.get(next);
639
+ if (c === GRAY) {
640
+ const startIdx = stack.indexOf(next);
641
+ return [...stack.slice(startIdx), next];
642
+ }
643
+ if (c === WHITE) {
644
+ const found = dfs(next);
645
+ if (found) return found;
646
+ }
647
+ }
648
+ color.set(node, BLACK);
649
+ stack.pop();
650
+ return void 0;
651
+ }
652
+ for (const id of remaining) {
653
+ if (color.get(id) === WHITE) {
654
+ const cycle = dfs(id);
655
+ if (cycle) return cycle;
656
+ }
657
+ }
658
+ return remaining;
659
+ }
660
+
661
+ // libs/adapters/src/skills/source-options.ts
662
+ var import_lazy_zod2 = require("@frontmcp/lazy-zod");
663
+ var httpsUrl = import_lazy_zod2.z.string().url().refine(
664
+ (v) => {
665
+ try {
666
+ return new URL(v).protocol === "https:";
667
+ } catch {
668
+ return false;
669
+ }
670
+ },
671
+ { message: "must use https://" }
672
+ );
673
+ var staticSourceSchema = import_lazy_zod2.z.object({
674
+ type: import_lazy_zod2.z.literal("static"),
675
+ /** Path to the bundle directory (containing spec.yaml + overlay.yaml) or a single overlay file. */
676
+ path: import_lazy_zod2.z.string().min(1),
677
+ /** Watch the path for changes (fs.watch). Default false. */
678
+ watch: import_lazy_zod2.z.boolean().default(false)
679
+ });
680
+ var npmSourceSchema = import_lazy_zod2.z.object({
681
+ type: import_lazy_zod2.z.literal("npm"),
682
+ /** npm package specifier, e.g. `@acme/frontmcp-billing-bundle`. */
683
+ packageName: import_lazy_zod2.z.string().min(1),
684
+ /** Optional named export to read; default is the package's default export. */
685
+ exportName: import_lazy_zod2.z.string().optional(),
686
+ /**
687
+ * Verify GitHub artifact attestation / npm provenance for the package before loading.
688
+ * Defaults to true. Set false (with warning) only for local development packages.
689
+ */
690
+ verifyProvenance: import_lazy_zod2.z.boolean().default(true)
691
+ });
692
+ var saasSourceSchema = import_lazy_zod2.z.object({
693
+ type: import_lazy_zod2.z.literal("saas"),
694
+ /** SaaS pull endpoint, e.g. `https://cloud.frontmcp.dev/v1/bundles/<bundleId>`. */
695
+ endpoint: httpsUrl,
696
+ /** Pinned JWT issued by the SaaS for the customer's FrontMCP server. */
697
+ authToken: import_lazy_zod2.z.string().min(1),
698
+ /**
699
+ * RFC 8707 resource indicator the JWT must encode. The plugin verifies
700
+ * `resource` AND `aud` claims match this on every push/pull.
701
+ */
702
+ expectedAudience: import_lazy_zod2.z.string().min(1),
703
+ /** Polling interval in ms (boot pull is always immediate). Default 300000 (5 min). */
704
+ pollIntervalMs: import_lazy_zod2.z.number().int().positive().default(3e5),
705
+ /**
706
+ * Mount a webhook handler at `POST /__skilled_openapi/push` so the SaaS can push
707
+ * updates synchronously. Default false in v1.2 (interval polling only).
708
+ */
709
+ enableWebhook: import_lazy_zod2.z.boolean().default(false),
710
+ /** JWKS URL for verifying SaaS-issued tokens. */
711
+ jwksUrl: httpsUrl,
712
+ /** Expected issuer (`iss`) claim. */
713
+ expectedIssuer: import_lazy_zod2.z.string().min(1)
714
+ });
715
+ var bundleSourceSchema = import_lazy_zod2.z.discriminatedUnion("type", [staticSourceSchema, npmSourceSchema, saasSourceSchema]);
716
+ var signatureKeySchema = import_lazy_zod2.z.object({
717
+ /** Stable key id (matches `kid` claim in the bundle JWT). */
718
+ keyId: import_lazy_zod2.z.string().min(1),
719
+ /** Algorithm for the signing key. */
720
+ alg: import_lazy_zod2.z.enum(["RS256", "EdDSA"]),
721
+ /** PEM-encoded public key (RSA SPKI or Ed25519). */
722
+ publicKeyPem: import_lazy_zod2.z.string().min(1)
723
+ });
724
+
725
+ // libs/adapters/src/skills/sources/npm.source.ts
726
+ var NpmSource = class {
727
+ constructor(options, logger) {
728
+ this.options = options;
729
+ this.logger = logger;
730
+ this.id = `npm:${this.options.packageName}${this.options.exportName ? `#${this.options.exportName}` : ""}`;
731
+ }
732
+ id;
733
+ listeners = /* @__PURE__ */ new Set();
734
+ cached;
735
+ async start() {
736
+ const bundle = await this.loadBundleFromPackage();
737
+ this.cached = bundle;
738
+ this.notify(bundle);
739
+ }
740
+ onChange(listener) {
741
+ this.listeners.add(listener);
742
+ if (this.cached) {
743
+ try {
744
+ listener(this.cached);
745
+ } catch (e) {
746
+ this.logger.warn(`[npm-source] listener threw on replay: ${e.message}`);
747
+ }
748
+ }
749
+ return () => {
750
+ this.listeners.delete(listener);
751
+ };
752
+ }
753
+ async stop() {
754
+ this.listeners.clear();
755
+ }
756
+ async loadBundleFromPackage() {
757
+ if (this.options.verifyProvenance) {
758
+ this.logger.warn(
759
+ `[npm-source] verifyProvenance=true requested but provenance verification is not yet implemented in OSS (planned v1.2.x). Loading "${this.options.packageName}" without provenance check.`
760
+ );
761
+ }
762
+ const mod = await this.dynamicImport(this.options.packageName);
763
+ const exportName = this.options.exportName ?? "default";
764
+ const modRecord = mod;
765
+ const exported = modRecord[exportName] ?? (exportName === "default" ? mod : void 0);
766
+ if (exported === void 0 || exported === null) {
767
+ throw new Error(`npm source "${this.options.packageName}": export "${exportName}" not found on the package`);
768
+ }
769
+ return parseOverlay({ kind: "object", content: exported });
770
+ }
771
+ // Indirected so tests can stub.
772
+ dynamicImport(specifier) {
773
+ return import(specifier);
774
+ }
775
+ notify(bundle) {
776
+ for (const fn of this.listeners) {
777
+ try {
778
+ fn(bundle);
779
+ } catch (e) {
780
+ this.logger.warn(`[npm-source] listener threw: ${e.message}`);
781
+ }
782
+ }
783
+ }
784
+ };
785
+
786
+ // libs/adapters/src/skills/sources/saas-pull.source.ts
787
+ var import_node_fs = require("node:fs");
788
+ var path = __toESM(require("node:path"));
789
+ var DEFAULT_CACHE_DIR = ".frontmcp/skilled-openapi";
790
+ var DEFAULT_PULL_TIMEOUT_MS = 3e4;
791
+ var SaasPullSource = class {
792
+ constructor(options, cacheDir, logger) {
793
+ this.options = options;
794
+ this.cacheDir = cacheDir;
795
+ this.logger = logger;
796
+ this.id = `saas:${this.options.endpoint}`;
797
+ }
798
+ id;
799
+ listeners = /* @__PURE__ */ new Set();
800
+ pollHandle;
801
+ inFlight = false;
802
+ stopped = false;
803
+ async start() {
804
+ let bundle;
805
+ try {
806
+ bundle = await this.fetchOnce();
807
+ await this.persistCache(bundle);
808
+ } catch (e) {
809
+ this.logger.warn(
810
+ `[saas-source] initial pull failed (${e.message}); attempting cached bundle fallback`
811
+ );
812
+ bundle = await this.loadCache();
813
+ if (!bundle) {
814
+ throw new Error(
815
+ `[saas-source] initial pull failed and no cached bundle is available at ${this.cachePath()}: ${e.message}`
816
+ );
817
+ }
818
+ this.logger.warn(`[saas-source] using cached bundle "${bundle.bundleId}@${bundle.version}"`);
819
+ }
820
+ this.notify(bundle);
821
+ this.schedulePoll();
822
+ }
823
+ onChange(listener) {
824
+ this.listeners.add(listener);
825
+ return () => {
826
+ this.listeners.delete(listener);
827
+ };
828
+ }
829
+ async stop() {
830
+ this.stopped = true;
831
+ if (this.pollHandle) {
832
+ clearTimeout(this.pollHandle);
833
+ this.pollHandle = void 0;
834
+ }
835
+ this.listeners.clear();
836
+ }
837
+ schedulePoll() {
838
+ if (this.stopped) return;
839
+ this.pollHandle = setTimeout(() => {
840
+ void this.pollOnce();
841
+ }, this.options.pollIntervalMs);
842
+ this.pollHandle.unref?.();
843
+ }
844
+ async pollOnce() {
845
+ if (this.stopped) return;
846
+ if (this.inFlight) {
847
+ this.schedulePoll();
848
+ return;
849
+ }
850
+ this.inFlight = true;
851
+ try {
852
+ const bundle = await this.fetchOnce();
853
+ await this.persistCache(bundle);
854
+ this.notify(bundle);
855
+ } catch (e) {
856
+ this.logger.warn(`[saas-source] poll failed: ${e.message}`);
857
+ } finally {
858
+ this.inFlight = false;
859
+ this.schedulePoll();
860
+ }
861
+ }
862
+ // Indirected so tests can stub the network call.
863
+ async httpGet(url, headers) {
864
+ const controller = new AbortController();
865
+ const timer = setTimeout(() => controller.abort(), DEFAULT_PULL_TIMEOUT_MS);
866
+ try {
867
+ const res = await fetch(url, { method: "GET", headers, signal: controller.signal });
868
+ const body = await res.text();
869
+ return { status: res.status, body };
870
+ } finally {
871
+ clearTimeout(timer);
872
+ }
873
+ }
874
+ async fetchOnce() {
875
+ const { status, body } = await this.httpGet(this.options.endpoint, {
876
+ Authorization: `Bearer ${this.options.authToken}`,
877
+ Accept: "application/json, application/yaml",
878
+ "User-Agent": "frontmcp-skilled-openapi"
879
+ });
880
+ if (status < 200 || status >= 300) {
881
+ throw new Error(`pull responded ${status}`);
882
+ }
883
+ const trimmed = body.trimStart();
884
+ return parseOverlay(
885
+ trimmed.startsWith("{") || trimmed.startsWith("[") ? { kind: "json", content: body } : { kind: "yaml", content: body }
886
+ );
887
+ }
888
+ cachePath() {
889
+ const dir = this.cacheDir ?? DEFAULT_CACHE_DIR;
890
+ return path.resolve(dir, `${this.options.expectedAudience.replace(/[^a-zA-Z0-9_.-]/g, "_")}.json`);
891
+ }
892
+ async persistCache(bundle) {
893
+ try {
894
+ const filePath = this.cachePath();
895
+ await import_node_fs.promises.mkdir(path.dirname(filePath), { recursive: true });
896
+ await import_node_fs.promises.writeFile(filePath, JSON.stringify(bundle), "utf8");
897
+ } catch (e) {
898
+ this.logger.warn(`[saas-source] cache persist failed: ${e.message}`);
899
+ }
900
+ }
901
+ async loadCache() {
902
+ try {
903
+ const filePath = this.cachePath();
904
+ const raw = await import_node_fs.promises.readFile(filePath, "utf8");
905
+ return parseOverlay({ kind: "json", content: raw });
906
+ } catch {
907
+ return void 0;
908
+ }
909
+ }
910
+ notify(bundle) {
911
+ for (const fn of this.listeners) {
912
+ try {
913
+ fn(bundle);
914
+ } catch (e) {
915
+ this.logger.warn(`[saas-source] listener threw: ${e.message}`);
916
+ }
917
+ }
918
+ }
919
+ };
920
+
921
+ // libs/adapters/src/skills/sources/static.source.ts
922
+ var import_node_fs2 = require("node:fs");
923
+ var path2 = __toESM(require("node:path"));
924
+ var import_utils = require("@frontmcp/utils");
925
+ var StaticSource = class {
926
+ constructor(options, logger) {
927
+ this.options = options;
928
+ this.logger = logger;
929
+ this.id = `static:${this.options.path}`;
930
+ }
931
+ id;
932
+ listeners = /* @__PURE__ */ new Set();
933
+ watcher;
934
+ debounceTimer;
935
+ stopped = false;
936
+ async start() {
937
+ const bundle = await this.fetchOnce();
938
+ this.notify(bundle);
939
+ if (this.options.watch) {
940
+ this.beginWatch();
941
+ }
942
+ }
943
+ onChange(listener) {
944
+ this.listeners.add(listener);
945
+ return () => {
946
+ this.listeners.delete(listener);
947
+ };
948
+ }
949
+ async stop() {
950
+ this.stopped = true;
951
+ if (this.watcher) {
952
+ this.watcher.close();
953
+ this.watcher = void 0;
954
+ }
955
+ if (this.debounceTimer) {
956
+ clearTimeout(this.debounceTimer);
957
+ this.debounceTimer = void 0;
958
+ }
959
+ this.listeners.clear();
960
+ }
961
+ async fetchOnce() {
962
+ const absPath = path2.resolve(this.options.path);
963
+ const raw = await (0, import_utils.readFile)(absPath, "utf8");
964
+ const ext = path2.extname(absPath).toLowerCase();
965
+ let input;
966
+ if (ext === ".json") {
967
+ input = { kind: "json", content: raw };
968
+ } else if (ext === ".yaml" || ext === ".yml") {
969
+ input = { kind: "yaml", content: raw };
970
+ } else {
971
+ const trimmed = raw.trimStart();
972
+ input = trimmed.startsWith("{") || trimmed.startsWith("[") ? { kind: "json", content: raw } : { kind: "yaml", content: raw };
973
+ }
974
+ return parseOverlay(input);
975
+ }
976
+ notify(bundle) {
977
+ for (const fn of this.listeners) {
978
+ try {
979
+ fn(bundle);
980
+ } catch (e) {
981
+ this.logger.warn(`[static-source] listener threw: ${e.message}`);
982
+ }
983
+ }
984
+ }
985
+ beginWatch() {
986
+ const absPath = path2.resolve(this.options.path);
987
+ try {
988
+ this.watcher = (0, import_node_fs2.watch)(absPath, { persistent: false }, () => this.scheduleRefresh());
989
+ } catch (e) {
990
+ this.logger.warn(`[static-source] fs.watch failed (${e.message}); polling at 2s instead`);
991
+ const tick = () => {
992
+ if (this.stopped) return;
993
+ this.scheduleRefresh();
994
+ setTimeout(tick, 2e3).unref?.();
995
+ };
996
+ setTimeout(tick, 2e3).unref?.();
997
+ }
998
+ }
999
+ scheduleRefresh() {
1000
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
1001
+ this.debounceTimer = setTimeout(async () => {
1002
+ if (this.stopped) return;
1003
+ try {
1004
+ const bundle = await this.fetchOnce();
1005
+ this.notify(bundle);
1006
+ } catch (e) {
1007
+ this.logger.warn(`[static-source] refresh failed: ${e.message}`);
1008
+ }
1009
+ }, 250);
1010
+ this.debounceTimer.unref?.();
1011
+ }
1012
+ };
1013
+
1014
+ // libs/adapters/src/skills/sources/index.ts
1015
+ function createBundleSource(source, cacheDir, logger) {
1016
+ switch (source.type) {
1017
+ case "static":
1018
+ return new StaticSource(source, logger);
1019
+ case "npm":
1020
+ return new NpmSource(source, logger);
1021
+ case "saas":
1022
+ return new SaasPullSource(source, cacheDir, logger);
1023
+ }
1024
+ }
1025
+
1026
+ // libs/adapters/src/skills/sources/filesystem-skills.source.ts
1027
+ var import_node_fs3 = require("node:fs");
1028
+ var import_node_path = require("node:path");
1029
+ var import_utils2 = require("@frontmcp/utils");
1030
+ var DEFAULT_OPTIONS = {
1031
+ skillsDir: "./skills",
1032
+ watch: true,
1033
+ debounceMs: 200,
1034
+ pollIntervalMs: 2e3
1035
+ };
1036
+ var FilesystemSkillsSource = class {
1037
+ constructor(options, logger) {
1038
+ this.logger = logger;
1039
+ this.options = { ...DEFAULT_OPTIONS, ...options };
1040
+ this.id = `filesystem-skills:${this.options.skillsDir}`;
1041
+ }
1042
+ id;
1043
+ options;
1044
+ listeners = /* @__PURE__ */ new Set();
1045
+ known = /* @__PURE__ */ new Map();
1046
+ // absPath → last emitted content
1047
+ watcher;
1048
+ pollTimer;
1049
+ debounceTimers = /* @__PURE__ */ new Map();
1050
+ stopped = false;
1051
+ rootResolved = "";
1052
+ onChange(listener) {
1053
+ this.listeners.add(listener);
1054
+ return () => {
1055
+ this.listeners.delete(listener);
1056
+ };
1057
+ }
1058
+ async start() {
1059
+ const absRoot = (0, import_utils2.pathResolve)(this.options.skillsDir);
1060
+ try {
1061
+ this.rootResolved = await (0, import_utils2.realpath)(absRoot);
1062
+ } catch {
1063
+ this.rootResolved = absRoot;
1064
+ }
1065
+ await this.initialScan();
1066
+ if (this.options.watch) {
1067
+ this.beginWatch();
1068
+ }
1069
+ }
1070
+ async stop() {
1071
+ this.stopped = true;
1072
+ if (this.watcher) {
1073
+ try {
1074
+ this.watcher.close();
1075
+ } catch {
1076
+ }
1077
+ this.watcher = void 0;
1078
+ }
1079
+ if (this.pollTimer) {
1080
+ clearTimeout(this.pollTimer);
1081
+ this.pollTimer = void 0;
1082
+ }
1083
+ for (const t of this.debounceTimers.values()) clearTimeout(t);
1084
+ this.debounceTimers.clear();
1085
+ this.listeners.clear();
1086
+ this.known.clear();
1087
+ }
1088
+ async initialScan() {
1089
+ const entries = await this.listSkillSubdirs();
1090
+ for (const dir of entries) {
1091
+ await this.refreshSkill(dir);
1092
+ }
1093
+ }
1094
+ async listSkillSubdirs() {
1095
+ let entries;
1096
+ try {
1097
+ entries = await (0, import_utils2.readdir)(this.rootResolved);
1098
+ } catch (e) {
1099
+ this.logger.warn(`[fs-skills] cannot read ${this.rootResolved}: ${e.message}`);
1100
+ return [];
1101
+ }
1102
+ const out = [];
1103
+ for (const name of entries) {
1104
+ if (name.startsWith(".")) continue;
1105
+ const abs = (0, import_utils2.joinPath)(this.rootResolved, name);
1106
+ let resolved;
1107
+ try {
1108
+ resolved = await (0, import_utils2.realpath)(abs);
1109
+ } catch {
1110
+ continue;
1111
+ }
1112
+ if (!resolved.startsWith(this.rootResolved + import_node_path.sep) && resolved !== this.rootResolved) {
1113
+ this.logger.warn(`[fs-skills] skipping ${name}: realpath escapes skillsDir`);
1114
+ continue;
1115
+ }
1116
+ try {
1117
+ const st = await (0, import_utils2.stat)(resolved);
1118
+ if (st.isDirectory()) out.push(resolved);
1119
+ } catch {
1120
+ }
1121
+ }
1122
+ return out.sort();
1123
+ }
1124
+ /** Reload a single skill subdir and emit upsert (or delete if vanished). */
1125
+ async refreshSkill(absDir) {
1126
+ if (this.stopped) return;
1127
+ let exists = false;
1128
+ try {
1129
+ const st = await (0, import_utils2.stat)(absDir);
1130
+ exists = st.isDirectory();
1131
+ } catch {
1132
+ exists = false;
1133
+ }
1134
+ if (!exists) {
1135
+ const prev2 = this.known.get(absDir);
1136
+ if (prev2) {
1137
+ this.known.delete(absDir);
1138
+ this.emit({ op: "delete", skillId: prev2.id, absPath: absDir });
1139
+ }
1140
+ return;
1141
+ }
1142
+ let content;
1143
+ try {
1144
+ content = await loadFilesystemSkill(absDir);
1145
+ } catch (e) {
1146
+ this.logger.warn(`[fs-skills] failed to load ${absDir}: ${e.message}`);
1147
+ return;
1148
+ }
1149
+ const prev = this.known.get(absDir);
1150
+ if (prev && skillContentEqual(prev, content)) {
1151
+ return;
1152
+ }
1153
+ this.known.set(absDir, content);
1154
+ this.emit({ op: "upsert", skill: content, absPath: absDir });
1155
+ }
1156
+ emit(event) {
1157
+ for (const fn of this.listeners) {
1158
+ try {
1159
+ fn(event);
1160
+ } catch (e) {
1161
+ this.logger.warn(`[fs-skills] listener threw: ${e.message}`);
1162
+ }
1163
+ }
1164
+ }
1165
+ beginWatch() {
1166
+ try {
1167
+ this.watcher = (0, import_node_fs3.watch)(this.rootResolved, { recursive: true, persistent: false }, (_eventType, filename) => {
1168
+ if (this.stopped) return;
1169
+ if (!filename) {
1170
+ this.scheduleRescan();
1171
+ return;
1172
+ }
1173
+ const first = filename.split(import_node_path.sep)[0];
1174
+ if (!first || first.startsWith(".")) return;
1175
+ const absDir = (0, import_utils2.joinPath)(this.rootResolved, first);
1176
+ this.scheduleRefresh(absDir);
1177
+ });
1178
+ this.watcher.on("error", (e) => {
1179
+ this.logger.warn(`[fs-skills] fs.watch error: ${e.message}; falling back to polling`);
1180
+ this.watcher?.close();
1181
+ this.watcher = void 0;
1182
+ this.beginPolling();
1183
+ });
1184
+ } catch (e) {
1185
+ this.logger.warn(
1186
+ `[fs-skills] fs.watch failed (${e.message}); polling at ${this.options.pollIntervalMs}ms instead`
1187
+ );
1188
+ this.beginPolling();
1189
+ }
1190
+ }
1191
+ beginPolling() {
1192
+ const tick = async () => {
1193
+ if (this.stopped) return;
1194
+ try {
1195
+ await this.pollOnce();
1196
+ } catch (e) {
1197
+ this.logger.warn(`[fs-skills] poll tick error: ${e.message}`);
1198
+ }
1199
+ if (this.stopped) return;
1200
+ this.pollTimer = setTimeout(tick, this.options.pollIntervalMs);
1201
+ this.pollTimer.unref?.();
1202
+ };
1203
+ this.pollTimer = setTimeout(tick, this.options.pollIntervalMs);
1204
+ this.pollTimer.unref?.();
1205
+ }
1206
+ /** Compare current dir listing to known set; emit upserts/deletes. */
1207
+ async pollOnce() {
1208
+ const dirs = new Set(await this.listSkillSubdirs());
1209
+ for (const known of [...this.known.keys()]) {
1210
+ if (!dirs.has(known)) {
1211
+ await this.refreshSkill(known);
1212
+ }
1213
+ }
1214
+ for (const abs of dirs) {
1215
+ await this.refreshSkill(abs);
1216
+ }
1217
+ }
1218
+ /** Coarse rescan when filename is unknown (some Linux/macOS fs.watch events). */
1219
+ scheduleRescan() {
1220
+ const key = "<rescan>";
1221
+ const existing = this.debounceTimers.get(key);
1222
+ if (existing) clearTimeout(existing);
1223
+ const t = setTimeout(() => {
1224
+ this.debounceTimers.delete(key);
1225
+ void this.pollOnce().catch((e) => {
1226
+ this.logger.warn(`[fs-skills] rescan failed: ${e.message}`);
1227
+ });
1228
+ }, this.options.debounceMs);
1229
+ t.unref?.();
1230
+ this.debounceTimers.set(key, t);
1231
+ }
1232
+ scheduleRefresh(absDir) {
1233
+ const existing = this.debounceTimers.get(absDir);
1234
+ if (existing) clearTimeout(existing);
1235
+ const t = setTimeout(() => {
1236
+ this.debounceTimers.delete(absDir);
1237
+ void this.refreshSkill(absDir).catch((e) => {
1238
+ this.logger.warn(`[fs-skills] refresh failed for ${absDir}: ${e.message}`);
1239
+ });
1240
+ }, this.options.debounceMs);
1241
+ t.unref?.();
1242
+ this.debounceTimers.set(absDir, t);
1243
+ }
1244
+ };
1245
+ async function loadFilesystemSkill(absDir) {
1246
+ const skillMdPath = (0, import_utils2.joinPath)(absDir, "SKILL.md");
1247
+ const raw = await (0, import_utils2.readFile)(skillMdPath, "utf8");
1248
+ const { frontmatter, body } = parseSkillFrontmatter(raw);
1249
+ const dirName = (0, import_utils2.basename)(absDir);
1250
+ const name = frontmatter["name"] ?? dirName;
1251
+ const description = frontmatter["description"] ?? "";
1252
+ if (!description) {
1253
+ throw new Error(`SKILL.md missing 'description' frontmatter (in ${absDir})`);
1254
+ }
1255
+ const instructions = body.trim();
1256
+ if (!instructions) {
1257
+ throw new Error(`SKILL.md missing body content (in ${absDir})`);
1258
+ }
1259
+ const tools = parseToolList(frontmatter["tools"]);
1260
+ const resources = {};
1261
+ for (const sub of ["scripts", "references", "examples", "assets"]) {
1262
+ const subAbs = (0, import_utils2.joinPath)(absDir, sub);
1263
+ try {
1264
+ const st = await (0, import_utils2.stat)(subAbs);
1265
+ if (st.isDirectory()) resources[sub] = subAbs;
1266
+ } catch {
1267
+ }
1268
+ }
1269
+ const out = {
1270
+ id: name,
1271
+ name,
1272
+ description,
1273
+ instructions,
1274
+ tools,
1275
+ ...Object.keys(resources).length > 0 && { resources }
1276
+ };
1277
+ if (typeof frontmatter["license"] === "string") out.license = frontmatter["license"];
1278
+ if (typeof frontmatter["compatibility"] === "string") out.compatibility = frontmatter["compatibility"];
1279
+ return out;
1280
+ }
1281
+ function parseSkillFrontmatter(raw) {
1282
+ if (!raw.startsWith("---")) {
1283
+ return { frontmatter: {}, body: raw };
1284
+ }
1285
+ const closeIdx = raw.indexOf("\n---", 3);
1286
+ if (closeIdx === -1) {
1287
+ return { frontmatter: {}, body: raw };
1288
+ }
1289
+ const yamlBlock = raw.slice(3, closeIdx).trim();
1290
+ let bodyStart = closeIdx + 4;
1291
+ if (raw[bodyStart] === "\n") bodyStart += 1;
1292
+ const body = raw.slice(bodyStart);
1293
+ const fm = {};
1294
+ let currentListKey;
1295
+ for (const line of yamlBlock.split("\n")) {
1296
+ if (!line.trim()) continue;
1297
+ if (line.startsWith("- ") && currentListKey) {
1298
+ const arr = fm[currentListKey] ?? [];
1299
+ arr.push(stripQuotes(line.slice(2).trim()));
1300
+ fm[currentListKey] = arr;
1301
+ continue;
1302
+ }
1303
+ const colon = line.indexOf(":");
1304
+ if (colon === -1) continue;
1305
+ const key = line.slice(0, colon).trim();
1306
+ const rest = line.slice(colon + 1).trim();
1307
+ if (!rest) {
1308
+ currentListKey = key;
1309
+ fm[key] = [];
1310
+ continue;
1311
+ }
1312
+ currentListKey = void 0;
1313
+ fm[key] = stripQuotes(rest);
1314
+ }
1315
+ return { frontmatter: fm, body };
1316
+ }
1317
+ function stripQuotes(s) {
1318
+ if (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'")) {
1319
+ return s.slice(1, -1);
1320
+ }
1321
+ return s;
1322
+ }
1323
+ function parseToolList(raw) {
1324
+ if (Array.isArray(raw)) {
1325
+ return raw.map((t) => typeof t === "string" ? { name: t } : null).filter((t) => t !== null);
1326
+ }
1327
+ if (typeof raw === "string") {
1328
+ return raw.split(",").map((t) => t.trim()).filter(Boolean).map((name) => ({ name }));
1329
+ }
1330
+ return [];
1331
+ }
1332
+ function skillContentEqual(a, b) {
1333
+ 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 ?? {});
1334
+ }
1335
+
1336
+ // libs/adapters/src/skills/security/bundle-signature.ts
1337
+ var import_utils3 = require("@frontmcp/utils");
1338
+ function classifyReason(reason) {
1339
+ if (reason.includes("missing integrity")) return "missing_integrity";
1340
+ if (reason.includes("digest mismatch")) return "digest_mismatch";
1341
+ if (reason.includes("unknown signing keyId")) return "unknown_key_id";
1342
+ if (reason.includes("alg=")) return "alg_mismatch";
1343
+ if (reason.includes("parse public key")) return "malformed_public_key";
1344
+ if (reason.includes("not valid base64url")) return "malformed_signature";
1345
+ if (reason.includes("signature verify threw")) return "verify_threw";
1346
+ if (reason.includes("signature verification failed")) return "verify_failed";
1347
+ return "other";
1348
+ }
1349
+ function canonicalize(value) {
1350
+ if (value === null || typeof value !== "object") {
1351
+ return JSON.stringify(value);
1352
+ }
1353
+ if (Array.isArray(value)) {
1354
+ return `[${value.map((v) => canonicalize(v)).join(",")}]`;
1355
+ }
1356
+ const obj = value;
1357
+ const keys = Object.keys(obj).sort();
1358
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(obj[k])}`).join(",")}}`;
1359
+ }
1360
+ function bundleDigest(bundle) {
1361
+ const { integrity: _drop, ...rest } = bundle;
1362
+ void _drop;
1363
+ return (0, import_utils3.sha256Hex)(canonicalize(rest));
1364
+ }
1365
+ function decodeSignatureBytes(signatureBase64Url) {
1366
+ try {
1367
+ return (0, import_utils3.base64urlDecode)(signatureBase64Url);
1368
+ } catch {
1369
+ return void 0;
1370
+ }
1371
+ }
1372
+ function verifyBundleSignature(bundle, trustedKeys, telemetry) {
1373
+ const failuresCounter = telemetry?.createCounter(
1374
+ "frontmcp_skills_signature_failures_total",
1375
+ "Number of bundle signature verification failures, partitioned by reason."
1376
+ );
1377
+ const verificationsCounter = telemetry?.createCounter(
1378
+ "frontmcp_skills_signature_verifications_total",
1379
+ "Number of bundle signature verification attempts, partitioned by status (ok|error)."
1380
+ );
1381
+ const fail = (reason) => {
1382
+ failuresCounter?.inc(1, { reason: classifyReason(reason) });
1383
+ verificationsCounter?.inc(1, { status: "error" });
1384
+ return { ok: false, reason };
1385
+ };
1386
+ if (!bundle.integrity) {
1387
+ return fail("bundle missing integrity envelope");
1388
+ }
1389
+ const { alg, keyId, signature, digest } = bundle.integrity;
1390
+ const expectedDigest = bundleDigest(bundle);
1391
+ if (digest.toLowerCase() !== expectedDigest.toLowerCase()) {
1392
+ return fail(`digest mismatch (envelope=${digest.slice(0, 12)}.. computed=${expectedDigest.slice(0, 12)}..)`);
1393
+ }
1394
+ const trusted = trustedKeys.find((k) => k.keyId === keyId);
1395
+ if (!trusted) {
1396
+ return fail(`unknown signing keyId "${keyId}" \u2014 not in trustedKeys allowlist`);
1397
+ }
1398
+ if (trusted.alg !== alg) {
1399
+ return fail(`key "${keyId}" alg=${trusted.alg} but envelope alg=${alg}`);
1400
+ }
1401
+ let publicJwk;
1402
+ try {
1403
+ publicJwk = (0, import_utils3.pemToPublicJwk)(trusted.publicKeyPem);
1404
+ } catch (e) {
1405
+ return fail(`failed to parse public key for "${keyId}": ${e.message}`);
1406
+ }
1407
+ const sigBytes = decodeSignatureBytes(signature);
1408
+ if (!sigBytes) {
1409
+ return fail("signature is not valid base64url");
1410
+ }
1411
+ const { integrity: _drop, ...rest } = bundle;
1412
+ void _drop;
1413
+ const canonicalBytes = new TextEncoder().encode(canonicalize(rest));
1414
+ const verified = (0, import_utils3.rsaVerifySync)(alg, canonicalBytes, publicJwk, sigBytes);
1415
+ if (!verified) {
1416
+ return fail("signature verification failed");
1417
+ }
1418
+ verificationsCounter?.inc(1, { status: "ok" });
1419
+ return { ok: true, keyId, alg };
1420
+ }
1421
+
1422
+ // libs/adapters/src/skills/security/jwt-verifier.ts
1423
+ var import_auth = require("@frontmcp/auth");
1424
+ var DEFAULT_REQUIRED_ROLES = ["frontmcp:cloud:push"];
1425
+ var BundlePushJwtVerifier = class {
1426
+ constructor(options, logger, jwks) {
1427
+ this.options = options;
1428
+ this.logger = logger;
1429
+ this.jwks = jwks ?? new import_auth.JwksService({});
1430
+ this.providerRef = {
1431
+ id: "skilled-openapi-cloud-push",
1432
+ issuerUrl: options.expectedIssuer,
1433
+ jwksUri: options.jwksUri
1434
+ };
1435
+ }
1436
+ providerRef;
1437
+ jwks;
1438
+ /**
1439
+ * Verify a SaaS-issued bearer token. Returns ok=true only if signature,
1440
+ * issuer, audience, RFC 8707 resource indicator, and role claims all check.
1441
+ */
1442
+ async verify(token) {
1443
+ if (!token || typeof token !== "string") {
1444
+ return { ok: false, reason: "token missing" };
1445
+ }
1446
+ let res;
1447
+ try {
1448
+ res = await this.jwks.verifyTransparentToken(token, [this.providerRef]);
1449
+ } catch (e) {
1450
+ this.logger.debug(`[skilled-openapi] jwt verify threw: ${e.message}`);
1451
+ return { ok: false, reason: `verify failed: ${e.message}` };
1452
+ }
1453
+ if (!res.ok || !res.payload) {
1454
+ return { ok: false, reason: res.error ?? "signature verify failed" };
1455
+ }
1456
+ const payload = res.payload;
1457
+ const aud = payload["aud"];
1458
+ const audValues = Array.isArray(aud) ? aud : aud !== void 0 ? [aud] : [];
1459
+ if (!audValues.includes(this.options.expectedAudience)) {
1460
+ return { ok: false, reason: `aud mismatch (expected "${this.options.expectedAudience}")` };
1461
+ }
1462
+ const resource = payload["resource"];
1463
+ const resValues = Array.isArray(resource) ? resource : resource !== void 0 ? [resource] : [];
1464
+ if (!resValues.includes(this.options.expectedResource)) {
1465
+ return {
1466
+ ok: false,
1467
+ reason: `RFC 8707 resource indicator mismatch (expected "${this.options.expectedResource}")`
1468
+ };
1469
+ }
1470
+ const requiredRoles = this.options.requiredRoles ?? DEFAULT_REQUIRED_ROLES;
1471
+ if (requiredRoles.length > 0 && !this.matchesAny(payload["roles"], requiredRoles)) {
1472
+ return { ok: false, reason: `missing required role (need one of ${requiredRoles.join(", ")})` };
1473
+ }
1474
+ if (this.options.requiredScopes && this.options.requiredScopes.length > 0) {
1475
+ if (!this.matchesAny(payload["scope"] ?? payload["scp"], this.options.requiredScopes)) {
1476
+ return {
1477
+ ok: false,
1478
+ reason: `missing required scope (need one of ${this.options.requiredScopes.join(", ")})`
1479
+ };
1480
+ }
1481
+ }
1482
+ return { ok: true, payload };
1483
+ }
1484
+ matchesAny(claim, required) {
1485
+ const have = Array.isArray(claim) ? claim.filter((v) => typeof v === "string") : typeof claim === "string" ? claim.split(/\s+/).filter(Boolean) : [];
1486
+ return required.some((r) => have.includes(r));
1487
+ }
1488
+ };
1489
+
1490
+ // libs/adapters/src/skills/security/webhook-replay-guard.ts
1491
+ var import_utils4 = require("@frontmcp/utils");
1492
+ function classifyReplayReason(reason) {
1493
+ if (reason.includes("not finite")) return "invalid_timestamp";
1494
+ if (reason.includes("too short")) return "invalid_nonce";
1495
+ if (reason.includes("outside")) return "outside_window";
1496
+ if (reason.includes("replay")) return "nonce_replay";
1497
+ return "other";
1498
+ }
1499
+ var DEFAULT_WINDOW_MS = 5 * 60 * 1e3;
1500
+ var DEFAULT_CAPACITY = 5e3;
1501
+ var WebhookReplayGuard = class {
1502
+ windowMs;
1503
+ capacity;
1504
+ seen = /* @__PURE__ */ new Map();
1505
+ now = Date.now;
1506
+ rejectsCounter;
1507
+ checksCounter;
1508
+ constructor(options = {}) {
1509
+ const windowMs = options.windowMs ?? DEFAULT_WINDOW_MS;
1510
+ const capacity = options.capacity ?? DEFAULT_CAPACITY;
1511
+ if (!Number.isFinite(windowMs) || windowMs <= 0) {
1512
+ throw new RangeError("WebhookReplayGuard: windowMs must be a finite number > 0");
1513
+ }
1514
+ if (!Number.isInteger(capacity) || capacity <= 0) {
1515
+ throw new RangeError("WebhookReplayGuard: capacity must be a positive integer");
1516
+ }
1517
+ this.windowMs = windowMs;
1518
+ this.capacity = capacity;
1519
+ this.rejectsCounter = options.telemetry?.createCounter(
1520
+ "frontmcp_skills_replay_rejects_total",
1521
+ "Number of webhook replay-guard rejections, partitioned by reason."
1522
+ );
1523
+ this.checksCounter = options.telemetry?.createCounter(
1524
+ "frontmcp_skills_replay_checks_total",
1525
+ "Number of webhook replay-guard checks, partitioned by status (ok|error)."
1526
+ );
1527
+ }
1528
+ /** For tests: override the time source. */
1529
+ setNowProvider(fn) {
1530
+ this.now = fn;
1531
+ }
1532
+ /**
1533
+ * Check that a (timestamp, nonce) pair is fresh and unseen. Returns ok=false
1534
+ * if the timestamp is outside the freshness window or the nonce has already
1535
+ * been observed within that window.
1536
+ */
1537
+ check(args) {
1538
+ const { timestampMs, nonce } = args;
1539
+ const reject = (reason) => {
1540
+ this.rejectsCounter?.inc(1, { reason: classifyReplayReason(reason) });
1541
+ this.checksCounter?.inc(1, { status: "error" });
1542
+ return { ok: false, reason };
1543
+ };
1544
+ if (!Number.isFinite(timestampMs)) {
1545
+ return reject("timestamp not finite");
1546
+ }
1547
+ if (typeof nonce !== "string" || nonce.length < 8) {
1548
+ return reject("nonce too short");
1549
+ }
1550
+ const now = this.now();
1551
+ if (Math.abs(now - timestampMs) > this.windowMs) {
1552
+ return reject(`timestamp outside ${this.windowMs}ms window`);
1553
+ }
1554
+ const key = (0, import_utils4.sha256Hex)(nonce);
1555
+ const expiresAt = this.seen.get(key);
1556
+ if (expiresAt !== void 0 && expiresAt >= now) {
1557
+ return reject("nonce replay detected within freshness window");
1558
+ }
1559
+ if (this.seen.size >= this.capacity) {
1560
+ this.prune();
1561
+ }
1562
+ if (this.seen.size >= this.capacity) {
1563
+ let victimKey;
1564
+ let victimExpiry = Number.POSITIVE_INFINITY;
1565
+ for (const [candidateKey, candidateExpiry] of this.seen) {
1566
+ if (candidateExpiry < victimExpiry) {
1567
+ victimKey = candidateKey;
1568
+ victimExpiry = candidateExpiry;
1569
+ }
1570
+ }
1571
+ if (victimKey !== void 0) this.seen.delete(victimKey);
1572
+ }
1573
+ this.seen.set(key, timestampMs + this.windowMs);
1574
+ this.checksCounter?.inc(1, { status: "ok" });
1575
+ return { ok: true };
1576
+ }
1577
+ /** Test/maintenance helper: drop expired entries proactively. */
1578
+ prune() {
1579
+ const now = this.now();
1580
+ let removed = 0;
1581
+ for (const [k, expiresAt] of this.seen) {
1582
+ if (expiresAt < now) {
1583
+ this.seen.delete(k);
1584
+ removed++;
1585
+ }
1586
+ }
1587
+ return removed;
1588
+ }
1589
+ /** Inspect: number of nonces currently tracked. */
1590
+ get size() {
1591
+ return this.seen.size;
1592
+ }
1593
+ };
1594
+
1595
+ // libs/adapters/src/skills/audit/audit-record.types.ts
1596
+ var SKILL_AUDIT_GENESIS_PREV_HASH = "0".repeat(64);
1597
+ var SKILL_AUDIT_KEYS = {
1598
+ /** Atomic chain head counter — incremented per append. */
1599
+ sequence: "audit:skills:sequence",
1600
+ /** Per-record key. Use `SKILL_AUDIT_KEYS.record(seq)` to build. */
1601
+ record: (sequence) => `audit:skills:records:${sequence}`
1602
+ };
1603
+
1604
+ // libs/adapters/src/skills/audit/audit-chain.ts
1605
+ var import_utils5 = require("@frontmcp/utils");
1606
+ function canonicalizeRecordForSigning(record) {
1607
+ const { signature: _drop, ...rest } = record;
1608
+ void _drop;
1609
+ return canonicalize(rest);
1610
+ }
1611
+ function nextPrevHash(prev) {
1612
+ if (!prev) return SKILL_AUDIT_GENESIS_PREV_HASH;
1613
+ return (0, import_utils5.sha256Hex)(canonicalizeRecordForSigning(prev));
1614
+ }
1615
+ function linkRecord(prev, partial) {
1616
+ return {
1617
+ ...partial,
1618
+ prevHash: nextPrevHash(prev)
1619
+ };
1620
+ }
1621
+ function verifyChain(records, trustedKeys, verifier) {
1622
+ if (records.length === 0) {
1623
+ return { ok: true, verified: 0 };
1624
+ }
1625
+ let prev;
1626
+ for (let i = 0; i < records.length; i++) {
1627
+ const rec = records[i];
1628
+ if (!rec) {
1629
+ return { ok: false, breakAt: i, reason: "record at index is null/undefined" };
1630
+ }
1631
+ if (prev && rec.sequence !== prev.sequence + 1) {
1632
+ return {
1633
+ ok: false,
1634
+ breakAt: rec.sequence,
1635
+ reason: `sequence gap: expected ${prev.sequence + 1}, got ${rec.sequence}`
1636
+ };
1637
+ }
1638
+ const expectedPrev = nextPrevHash(prev);
1639
+ if (rec.prevHash !== expectedPrev) {
1640
+ return {
1641
+ ok: false,
1642
+ breakAt: rec.sequence,
1643
+ reason: `prevHash mismatch at sequence ${rec.sequence}: expected ${expectedPrev.slice(0, 12)}.. got ${rec.prevHash.slice(0, 12)}..`
1644
+ };
1645
+ }
1646
+ const canonicalBytes = new TextEncoder().encode(canonicalizeRecordForSigning(rec));
1647
+ const ok = verifier({
1648
+ alg: rec.signatureAlg,
1649
+ keyId: rec.signatureKeyId,
1650
+ data: canonicalBytes,
1651
+ signatureBase64Url: rec.signature,
1652
+ trustedKeys: [...trustedKeys]
1653
+ });
1654
+ if (!ok) {
1655
+ return {
1656
+ ok: false,
1657
+ breakAt: rec.sequence,
1658
+ reason: `signature verification failed at sequence ${rec.sequence} (keyId="${rec.signatureKeyId}", alg=${rec.signatureAlg})`
1659
+ };
1660
+ }
1661
+ prev = rec;
1662
+ }
1663
+ return { ok: true, verified: records.length };
1664
+ }
1665
+
1666
+ // libs/adapters/src/skills/audit/audit-signer.ts
1667
+ var import_utils6 = require("@frontmcp/utils");
1668
+ var Hs256AuditSigner = class {
1669
+ key;
1670
+ keyId;
1671
+ /**
1672
+ * @param secret - HMAC secret. Strings are UTF-8 encoded; pass a
1673
+ * Uint8Array directly when you've derived the key (e.g.
1674
+ * from HKDF) and want to avoid the extra encode step.
1675
+ * @param keyId - Stable identifier (e.g. `'audit-dev-key'`) — surfaces in
1676
+ * the signed record's `signatureKeyId` field.
1677
+ */
1678
+ constructor(secret, keyId) {
1679
+ if (!keyId) {
1680
+ throw new Error("Hs256AuditSigner: keyId must be a non-empty string");
1681
+ }
1682
+ if (typeof secret === "string") {
1683
+ if (secret.length === 0) throw new Error("Hs256AuditSigner: secret must be non-empty");
1684
+ this.key = new TextEncoder().encode(secret);
1685
+ } else {
1686
+ if (secret.length === 0) throw new Error("Hs256AuditSigner: secret must be non-empty");
1687
+ this.key = new Uint8Array(secret);
1688
+ }
1689
+ this.keyId = keyId;
1690
+ }
1691
+ sign(record) {
1692
+ const canonicalBytes = new TextEncoder().encode(
1693
+ canonicalizeRecordForSigning({
1694
+ ...record,
1695
+ // Stub the signature fields to a deterministic shape so canonicalize
1696
+ // sees the exact same byte layout the verifier will reconstruct.
1697
+ signature: "",
1698
+ signatureKeyId: this.keyId,
1699
+ signatureAlg: "HS256"
1700
+ })
1701
+ );
1702
+ const mac = (0, import_utils6.hmacSha256)(this.key, canonicalBytes);
1703
+ return {
1704
+ signature: (0, import_utils6.base64urlEncode)(mac),
1705
+ keyId: this.keyId,
1706
+ alg: "HS256"
1707
+ };
1708
+ }
1709
+ getKeyId() {
1710
+ return this.keyId;
1711
+ }
1712
+ getAlg() {
1713
+ return "HS256";
1714
+ }
1715
+ };
1716
+ var Rs256AuditSigner = class {
1717
+ privateJwk;
1718
+ keyId;
1719
+ constructor(privateJwk, keyId) {
1720
+ if (!keyId) {
1721
+ throw new Error("Rs256AuditSigner: keyId must be a non-empty string");
1722
+ }
1723
+ if (!privateJwk || typeof privateJwk !== "object") {
1724
+ throw new Error("Rs256AuditSigner: privateJwk must be a JWK object");
1725
+ }
1726
+ if (privateJwk.kty !== "RSA") {
1727
+ throw new Error(`Rs256AuditSigner: privateJwk.kty must be "RSA", got "${String(privateJwk.kty)}"`);
1728
+ }
1729
+ if (!privateJwk.n || !privateJwk.e || !privateJwk.d) {
1730
+ throw new Error("Rs256AuditSigner: privateJwk must include `n`, `e`, and `d` (RSA private key)");
1731
+ }
1732
+ this.privateJwk = { ...privateJwk };
1733
+ this.keyId = keyId;
1734
+ }
1735
+ sign(record) {
1736
+ const canonicalBytes = new TextEncoder().encode(
1737
+ canonicalizeRecordForSigning({
1738
+ ...record,
1739
+ signature: "",
1740
+ signatureKeyId: this.keyId,
1741
+ signatureAlg: "RS256"
1742
+ })
1743
+ );
1744
+ const signature = (0, import_utils6.rsaSignBase64Url)("RS256", canonicalBytes, this.privateJwk);
1745
+ return {
1746
+ signature,
1747
+ keyId: this.keyId,
1748
+ alg: "RS256"
1749
+ };
1750
+ }
1751
+ getKeyId() {
1752
+ return this.keyId;
1753
+ }
1754
+ getAlg() {
1755
+ return "RS256";
1756
+ }
1757
+ };
1758
+ var defaultAuditSignatureVerifier = ({
1759
+ alg,
1760
+ keyId,
1761
+ data,
1762
+ signatureBase64Url,
1763
+ trustedKeys
1764
+ }) => {
1765
+ const trusted = trustedKeys.find((k) => k.keyId === keyId && k.alg === alg);
1766
+ if (!trusted) return false;
1767
+ if (alg === "HS256") {
1768
+ if (!trusted.secret) return false;
1769
+ const expected = (0, import_utils6.hmacSha256)(trusted.secret, data);
1770
+ let actual;
1771
+ try {
1772
+ actual = (0, import_utils6.base64urlDecode)(signatureBase64Url);
1773
+ } catch {
1774
+ return false;
1775
+ }
1776
+ if (actual.length !== expected.length) return false;
1777
+ return (0, import_utils6.timingSafeEqual)(actual, expected);
1778
+ }
1779
+ if (alg === "RS256") {
1780
+ if (!trusted.publicKeyPem && !trusted.publicJwk) return false;
1781
+ let publicJwk;
1782
+ try {
1783
+ publicJwk = trusted.publicJwk ?? (0, import_utils6.pemToPublicJwk)(trusted.publicKeyPem);
1784
+ } catch {
1785
+ return false;
1786
+ }
1787
+ let sigBytes;
1788
+ try {
1789
+ sigBytes = (0, import_utils6.base64urlDecode)(signatureBase64Url);
1790
+ } catch {
1791
+ return false;
1792
+ }
1793
+ return (0, import_utils6.rsaVerifySync)("RS256", data, publicJwk, sigBytes);
1794
+ }
1795
+ const _exhaustive = alg;
1796
+ void _exhaustive;
1797
+ return false;
1798
+ };
1799
+
1800
+ // libs/adapters/src/skills/audit/audit-store.ts
1801
+ var MemoryAuditStore = class {
1802
+ records = [];
1803
+ seq = 0;
1804
+ async nextSequence() {
1805
+ this.seq += 1;
1806
+ return this.seq;
1807
+ }
1808
+ async appendAtSequence(record) {
1809
+ if (this.records.some((r) => r.sequence === record.sequence)) {
1810
+ throw new Error(`MemoryAuditStore: record at sequence ${record.sequence} already exists`);
1811
+ }
1812
+ this.records.push(record);
1813
+ }
1814
+ async tail() {
1815
+ if (this.records.length === 0) return void 0;
1816
+ return this.records[this.records.length - 1];
1817
+ }
1818
+ async read(opts) {
1819
+ const from = opts?.from ?? 1;
1820
+ const limit = opts?.limit;
1821
+ const filtered = this.records.filter((r) => r.sequence >= from).sort((a, b) => a.sequence - b.sequence);
1822
+ return limit !== void 0 ? filtered.slice(0, limit) : filtered;
1823
+ }
1824
+ };
1825
+ var StorageAdapterAuditStore = class {
1826
+ constructor(adapter, options = {}) {
1827
+ this.adapter = adapter;
1828
+ this.options = options;
1829
+ }
1830
+ get sequenceKey() {
1831
+ return this.options.sequenceKey ?? SKILL_AUDIT_KEYS.sequence;
1832
+ }
1833
+ recordKey(sequence) {
1834
+ const prefix = this.options.recordKeyPrefix;
1835
+ return prefix ? `${prefix}${sequence}` : SKILL_AUDIT_KEYS.record(sequence);
1836
+ }
1837
+ async nextSequence() {
1838
+ return this.adapter.incr(this.sequenceKey);
1839
+ }
1840
+ async appendAtSequence(record) {
1841
+ const key = this.recordKey(record.sequence);
1842
+ const existing = await this.adapter.get(key);
1843
+ if (existing !== null) {
1844
+ throw new Error(`StorageAdapterAuditStore: record at sequence ${record.sequence} already exists`);
1845
+ }
1846
+ const payload = JSON.stringify(record);
1847
+ await this.adapter.set(key, payload, { ifNotExists: true });
1848
+ const stored = await this.adapter.get(key);
1849
+ if (stored !== payload) {
1850
+ throw new Error(`StorageAdapterAuditStore: record at sequence ${record.sequence} was clobbered or not stored`);
1851
+ }
1852
+ }
1853
+ async tail() {
1854
+ const seqRaw = await this.adapter.get(this.sequenceKey);
1855
+ if (!seqRaw) return void 0;
1856
+ const seq = Number.parseInt(seqRaw, 10);
1857
+ if (!Number.isFinite(seq) || seq <= 0) return void 0;
1858
+ const maxLookback = this.options.tailLookback ?? 1024;
1859
+ for (let s = seq; s > 0 && s > seq - maxLookback; s--) {
1860
+ const raw = await this.adapter.get(this.recordKey(s));
1861
+ if (!raw) continue;
1862
+ try {
1863
+ return JSON.parse(raw);
1864
+ } catch {
1865
+ throw new Error(`StorageAdapterAuditStore.tail: corrupt record at sequence ${s} cannot be parsed as JSON`);
1866
+ }
1867
+ }
1868
+ return void 0;
1869
+ }
1870
+ async read(opts) {
1871
+ const seqRaw = await this.adapter.get(this.sequenceKey);
1872
+ if (!seqRaw) return [];
1873
+ const head = Number.parseInt(seqRaw, 10);
1874
+ if (!Number.isFinite(head) || head <= 0) return [];
1875
+ const from = Math.max(1, opts?.from ?? 1);
1876
+ const limit = opts?.limit;
1877
+ const out = [];
1878
+ for (let s = from; s <= head; s++) {
1879
+ const raw = await this.adapter.get(this.recordKey(s));
1880
+ if (!raw) continue;
1881
+ try {
1882
+ const parsed = JSON.parse(raw);
1883
+ out.push(parsed);
1884
+ if (limit !== void 0 && out.length >= limit) break;
1885
+ } catch {
1886
+ }
1887
+ }
1888
+ return out;
1889
+ }
1890
+ };
1891
+
1892
+ // libs/adapters/src/skills/audit/audit-writer.ts
1893
+ var import_utils7 = require("@frontmcp/utils");
1894
+ var SKILL_AUDIT_ERROR_MESSAGE_MAX = 500;
1895
+ var SKILL_AUDIT_QUEUE_MAX = 1e3;
1896
+ var SkillAuditWriter = class {
1897
+ constructor(store, signer, logger, metrics, options = {}) {
1898
+ this.store = store;
1899
+ this.signer = signer;
1900
+ this.logger = logger;
1901
+ this.metrics = metrics;
1902
+ this.subjectMode = options.subjectMode ?? "hash";
1903
+ this.subjectHashSecret = options.subjectHashSecret ?? this.deriveDefaultSubjectSecret();
1904
+ this.maxQueueDepth = options.maxQueueDepth ?? SKILL_AUDIT_QUEUE_MAX;
1905
+ }
1906
+ /**
1907
+ * Single-flight queue: every append() chains onto this promise so the
1908
+ * read-tail-then-append-with-prevHash sequence stays atomic per process.
1909
+ * This is the mate to the storage adapter's `incr` for cross-pod safety.
1910
+ */
1911
+ chainHeadLock = Promise.resolve();
1912
+ /** Current in-flight queue depth (records awaiting the chain head lock). */
1913
+ queueDepth = 0;
1914
+ subjectMode;
1915
+ subjectHashSecret;
1916
+ maxQueueDepth;
1917
+ /**
1918
+ * Phase 2 in the tool: authority check just passed. We log the inputHash
1919
+ * here so even invocations that fail at the network layer still leave a
1920
+ * forensic breadcrumb tying the input to the policy decision.
1921
+ */
1922
+ writeAuthorityPass(ctx) {
1923
+ return this.write({
1924
+ phase: "authority-check-pass",
1925
+ ctx
1926
+ });
1927
+ }
1928
+ /**
1929
+ * Phase 2 in the tool: authority check denied. Writing this record is
1930
+ * security-relevant — without it, denied invocations would not appear in
1931
+ * the audit log at all, and the most security-relevant events (someone
1932
+ * trying to invoke an action they're not authorized for) would silently
1933
+ * disappear. The tool MUST call this before returning the denial envelope.
1934
+ */
1935
+ writeAuthorityFail(ctx, extras) {
1936
+ return this.write({
1937
+ phase: "authority-check-fail",
1938
+ ctx,
1939
+ // Re-use the errorMessage field for the denial reason so the on-disk
1940
+ // shape stays identical to other failure phases.
1941
+ error: extras.reason
1942
+ });
1943
+ }
1944
+ /** Phase 4 in the tool, success path. */
1945
+ writeHttpCallSuccess(ctx, extras) {
1946
+ return this.write({
1947
+ phase: "http-call-success",
1948
+ ctx,
1949
+ status: extras.status,
1950
+ output: extras.output
1951
+ });
1952
+ }
1953
+ /** Phase 4 in the tool, failure path. */
1954
+ writeHttpCallFailure(ctx, extras) {
1955
+ return this.write({
1956
+ phase: "http-call-failure",
1957
+ ctx,
1958
+ status: extras.status,
1959
+ error: extras.error
1960
+ });
1961
+ }
1962
+ /**
1963
+ * Wait for all pending writes to complete. Useful for tests and graceful
1964
+ * shutdown — the chain head lock resolves once every queued write has
1965
+ * finished (success or swallowed-failure). Never throws.
1966
+ */
1967
+ async flush() {
1968
+ while (this.queueDepth > 0) {
1969
+ await this.chainHeadLock;
1970
+ }
1971
+ }
1972
+ // ─── Internal ──────────────────────────────────────────────────────────
1973
+ write(args) {
1974
+ if (this.queueDepth >= this.maxQueueDepth) {
1975
+ this.logger.warn(
1976
+ `[skill-audit] queue overflow (${this.queueDepth}/${this.maxQueueDepth}); dropping ${args.phase} record. Audit backend likely unhealthy.`
1977
+ );
1978
+ this.metrics?.incrementDropped?.("queue-overflow");
1979
+ return Promise.resolve();
1980
+ }
1981
+ this.queueDepth += 1;
1982
+ const next = this.chainHeadLock.then(() => this.doWrite(args));
1983
+ this.chainHeadLock = next.catch(() => void 0).finally(() => {
1984
+ this.queueDepth -= 1;
1985
+ });
1986
+ return next.catch(() => void 0);
1987
+ }
1988
+ async doWrite(args) {
1989
+ const { phase, ctx, status, output, error } = args;
1990
+ let prev;
1991
+ try {
1992
+ prev = await this.store.tail();
1993
+ } catch (e) {
1994
+ this.logger.warn(`[skill-audit] failed to load chain tail: ${e.message}`);
1995
+ this.metrics?.incrementWriteFailure("append");
1996
+ return;
1997
+ }
1998
+ let inputHash;
1999
+ try {
2000
+ inputHash = (0, import_utils7.sha256Hex)(canonicalize(ctx.input ?? null));
2001
+ } catch (e) {
2002
+ this.logger.warn(`[skill-audit] failed to canonicalize input: ${e.message}`);
2003
+ this.metrics?.incrementWriteFailure("unexpected");
2004
+ return;
2005
+ }
2006
+ let sequence;
2007
+ try {
2008
+ sequence = await this.store.nextSequence();
2009
+ } catch (e) {
2010
+ this.logger.warn(`[skill-audit] failed to allocate sequence: ${e.message}`);
2011
+ this.metrics?.incrementWriteFailure("append");
2012
+ return;
2013
+ }
2014
+ const partial = {
2015
+ id: (0, import_utils7.randomUUID)(),
2016
+ sequence,
2017
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2018
+ subject: this.applySubjectMode(ctx.subject || "anonymous"),
2019
+ skillId: ctx.skillId,
2020
+ actionId: ctx.actionId,
2021
+ bundleId: ctx.bundleId,
2022
+ bundleVersion: ctx.bundleVersion,
2023
+ phase,
2024
+ inputHash,
2025
+ ...status !== void 0 ? { status } : {},
2026
+ ...phase === "http-call-success" ? { outputHash: this.safeHash(output) } : {},
2027
+ ...phase === "http-call-failure" || phase === "authority-check-fail" ? { errorMessage: this.formatError(error) } : {}
2028
+ };
2029
+ const linked = linkRecord(prev, partial);
2030
+ let signed;
2031
+ try {
2032
+ const sig = this.signer.sign(linked);
2033
+ signed = {
2034
+ ...linked,
2035
+ signature: sig.signature,
2036
+ signatureKeyId: sig.keyId,
2037
+ signatureAlg: sig.alg
2038
+ };
2039
+ } catch (e) {
2040
+ this.logger.warn(`[skill-audit] failed to sign record at seq=${sequence}: ${e.message}`);
2041
+ this.metrics?.incrementWriteFailure("sign");
2042
+ await this.tryDecrementSequence(sequence);
2043
+ return;
2044
+ }
2045
+ try {
2046
+ await this.store.appendAtSequence(signed);
2047
+ this.logger.debug?.(`[skill-audit] appended seq=${sequence} phase=${phase}`);
2048
+ } catch (e) {
2049
+ this.logger.warn(`[skill-audit] failed to append record at seq=${sequence}: ${e.message}`);
2050
+ this.metrics?.incrementWriteFailure("append");
2051
+ await this.tryDecrementSequence(sequence);
2052
+ }
2053
+ }
2054
+ /**
2055
+ * Best-effort sequence rollback. Storage adapters that support `decr` will
2056
+ * close the gap; ones that don't are no-ops. We never throw out of this
2057
+ * helper — a failed rollback is strictly worse than a permanent gap.
2058
+ *
2059
+ * Detection is duck-typed (we look at `.adapter.decr` and `.options.sequenceKey`)
2060
+ * because the SkillAuditStore interface intentionally doesn't expose
2061
+ * sequence-mutation methods — those are storage-specific implementation
2062
+ * detail, and forcing every implementation to expose them would push the
2063
+ * complexity onto every adapter even when the underlying store can't
2064
+ * support transactional rollback.
2065
+ */
2066
+ async tryDecrementSequence(allocated) {
2067
+ const storeAny = this.store;
2068
+ if (!storeAny.adapter || typeof storeAny.adapter.decr !== "function") return;
2069
+ const sequenceKey = storeAny.options?.sequenceKey ?? "audit:skills:sequence";
2070
+ try {
2071
+ await storeAny.adapter.decr(sequenceKey);
2072
+ this.logger.debug?.(`[skill-audit] rolled back unused sequence ${allocated}`);
2073
+ } catch {
2074
+ }
2075
+ }
2076
+ /** Apply the configured subject-mode redaction. */
2077
+ applySubjectMode(subject) {
2078
+ if (this.subjectMode === "plain") return subject;
2079
+ if (this.subjectMode === "omit") return "redacted";
2080
+ const mac = (0, import_utils7.hmacSha256)(this.subjectHashSecret, new TextEncoder().encode(subject));
2081
+ const hex = (0, import_utils7.bytesToHex)(mac);
2082
+ return `hashed:${hex.slice(0, 32)}`;
2083
+ }
2084
+ /**
2085
+ * Default subject-hash secret derivation when the host doesn't supply one.
2086
+ * Returns a deterministic 32-byte key seeded from the signer's keyId so
2087
+ * two SkillAuditWriter instances configured against the same signer
2088
+ * produce stable subject hashes — useful for joining records across
2089
+ * writer reconfigurations within the same logical deployment. NOT a
2090
+ * cryptographic key in its own right — the host SHOULD override via
2091
+ * constructor options for production deployments.
2092
+ */
2093
+ deriveDefaultSubjectSecret() {
2094
+ const key = new Uint8Array(32);
2095
+ const seed = new TextEncoder().encode(`frontmcp:audit:subject:${this.signer.getKeyId()}`);
2096
+ for (let i = 0; i < key.length; i++) {
2097
+ key[i] = seed[i % seed.length] ?? 0;
2098
+ }
2099
+ return key;
2100
+ }
2101
+ /**
2102
+ * Hash arbitrary output. Falls back to a sentinel if canonicalization
2103
+ * fails (circular references, non-serializable values) — the verifier
2104
+ * will treat it as a hash like any other.
2105
+ */
2106
+ safeHash(value) {
2107
+ try {
2108
+ return (0, import_utils7.sha256Hex)(canonicalize(value ?? null));
2109
+ } catch {
2110
+ return (0, import_utils7.sha256Hex)("audit:non-canonicalizable-output");
2111
+ }
2112
+ }
2113
+ /** Truncate + sanitize an error to fit in the record. */
2114
+ formatError(error) {
2115
+ let raw;
2116
+ if (error instanceof Error) {
2117
+ raw = error.message;
2118
+ } else if (typeof error === "string") {
2119
+ raw = error;
2120
+ } else if (error === void 0 || error === null) {
2121
+ raw = "";
2122
+ } else {
2123
+ try {
2124
+ raw = JSON.stringify(error);
2125
+ } catch {
2126
+ raw = String(error);
2127
+ }
2128
+ }
2129
+ if (raw.length > SKILL_AUDIT_ERROR_MESSAGE_MAX) {
2130
+ return raw.slice(0, SKILL_AUDIT_ERROR_MESSAGE_MAX) + "...";
2131
+ }
2132
+ return raw;
2133
+ }
2134
+ };
2135
+
2136
+ // libs/adapters/src/skills/audit/audit-config.ts
2137
+ var SkillAuditWriterToken = /* @__PURE__ */ Symbol.for(
2138
+ "frontmcp:SKILL_AUDIT_WRITER"
2139
+ );
2140
+
2141
+ // libs/adapters/src/skills/index.ts
2142
+ var import_sdk = require("@frontmcp/sdk");
2143
+ // Annotate the CommonJS export names for ESM import in node:
2144
+ 0 && (module.exports = {
2145
+ BundlePinnedError,
2146
+ BundlePushJwtVerifier,
2147
+ BundleStore,
2148
+ FilesystemSkillsSource,
2149
+ Hs256AuditSigner,
2150
+ MemoryAuditStore,
2151
+ NpmSource,
2152
+ OverlayParseError,
2153
+ Rs256AuditSigner,
2154
+ SKILL_AUDIT_ERROR_MESSAGE_MAX,
2155
+ SKILL_AUDIT_GENESIS_PREV_HASH,
2156
+ SKILL_AUDIT_KEYS,
2157
+ SKILL_AUDIT_QUEUE_MAX,
2158
+ SaasPullSource,
2159
+ SkillAuditWriter,
2160
+ SkillAuditWriterToken,
2161
+ SkillDependencyCycleError,
2162
+ SkillDependencyInvariantError,
2163
+ SkillDependencyMissingError,
2164
+ StaticSource,
2165
+ StorageAdapterAuditStore,
2166
+ WebhookReplayGuard,
2167
+ bundleDigest,
2168
+ bundleSkillToActions,
2169
+ bundleSourceSchema,
2170
+ canonicalize,
2171
+ canonicalizeRecordForSigning,
2172
+ createBundleSource,
2173
+ crossValidate,
2174
+ defaultAuditSignatureVerifier,
2175
+ diffBundles,
2176
+ formatDiffSummary,
2177
+ hasSkillAuditFactory,
2178
+ linkRecord,
2179
+ loadFilesystemSkill,
2180
+ nextPrevHash,
2181
+ npmSourceSchema,
2182
+ parseOverlay,
2183
+ parseSkillFrontmatter,
2184
+ resolveSkillLoadOrder,
2185
+ resolvedBundleSchema,
2186
+ saasSourceSchema,
2187
+ setSkillAuditFactory,
2188
+ signatureKeySchema,
2189
+ staticSourceSchema,
2190
+ verifyBundleSignature,
2191
+ verifyChain
2192
+ });