sentinelayer-cli 0.46.5 → 0.48.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.
- package/README.md +47 -3
- package/package.json +12 -7
- package/skills/omar-gate-setup/SKILL.md +103 -0
- package/skills/senti-agent-identity/SKILL.md +80 -0
- package/skills/senti-judge/SKILL.md +139 -0
- package/skills/senti-room-notifications/SKILL.md +73 -0
- package/skills/senti-room-notifications/listen-wrapper.cjs +150 -0
- package/skills/senti-room-notifications/package.json +1 -0
- package/skills/senti-room-notifications/test-classifier.js +109 -0
- package/skills/senti-room-notifications/test-receipt.js +54 -0
- package/skills/sentinelayer-cli/SKILL.md +222 -0
- package/src/agents/devtestbot/runner.js +22 -11
- package/src/agents/devtestbot/tool.js +24 -11
- package/src/agents/jules/fix-cycle.js +7 -1
- package/src/agents/jules/tools/runtime-audit.js +17 -12
- package/src/agents/shared-tools/dispatch-core.js +10 -5
- package/src/ai/aidenid.js +4 -2
- package/src/ai/domain-target-store.js +11 -5
- package/src/ai/identity-store.js +11 -5
- package/src/ai/site-store.js +8 -4
- package/src/audit/orchestrator.js +21 -14
- package/src/audit/package.js +6 -4
- package/src/audit/persona-loop.js +7 -0
- package/src/audit/replay.js +4 -2
- package/src/auth/gate.js +32 -1
- package/src/auth/session-store.js +37 -0
- package/src/board/evidence-resolvers.js +3 -4
- package/src/cli.js +23 -3
- package/src/commands/ai/identity-lifecycle.js +30 -26
- package/src/commands/ai/provision-governance.js +32 -24
- package/src/commands/ai/shared.js +6 -4
- package/src/commands/audit.js +14 -12
- package/src/commands/board.js +2 -2
- package/src/commands/chat.js +8 -6
- package/src/commands/judge.js +125 -0
- package/src/commands/legacy-args.js +1 -0
- package/src/commands/mcp.js +45 -52
- package/src/commands/omargate.js +2 -1
- package/src/commands/plugin.js +11 -3
- package/src/commands/review.js +9 -4
- package/src/commands/scan.js +7 -7
- package/src/commands/session.js +516 -30
- package/src/commands/skills.js +45 -0
- package/src/commands/watch.js +9 -7
- package/src/config/output-filesystem.js +156 -0
- package/src/config/schema.js +4 -2
- package/src/config/service.js +40 -4
- package/src/cost/history.js +15 -9
- package/src/daemon/artifact-lineage.js +15 -12
- package/src/daemon/assignment-ledger.js +31 -25
- package/src/daemon/ast-drift.js +7 -3
- package/src/daemon/budget-governor.js +26 -20
- package/src/daemon/error-worker.js +28 -22
- package/src/daemon/fix-cycle.js +5 -4
- package/src/daemon/hybrid-mapper.js +19 -16
- package/src/daemon/ingest-refresh.js +4 -0
- package/src/daemon/jira-lifecycle.js +21 -17
- package/src/daemon/operator-control.js +18 -15
- package/src/daemon/reliability-lane.js +18 -15
- package/src/daemon/scope-engine.js +12 -8
- package/src/daemon/watchdog.js +15 -12
- package/src/engram/build-state.js +24 -3
- package/src/engram/capability-index.js +367 -0
- package/src/engram/compile-graph.js +1063 -0
- package/src/engram/contracts/action-receipt.js +82 -0
- package/src/engram/contracts/capability-index.js +259 -0
- package/src/engram/contracts/capability.js +377 -0
- package/src/engram/contracts/content-hash.js +8 -0
- package/src/engram/contracts/evidence.js +258 -0
- package/src/engram/contracts/graph.js +594 -0
- package/src/engram/contracts/identity.js +8 -0
- package/src/engram/contracts/index.js +149 -0
- package/src/engram/contracts/media-asset.js +85 -0
- package/src/engram/contracts/money.js +249 -0
- package/src/engram/contracts/source-snapshot.js +57 -0
- package/src/engram/contracts/task-context-pack.js +114 -0
- package/src/engram/contracts/temporal-segment.js +72 -0
- package/src/engram/contracts/versions.js +96 -0
- package/src/engram/extract-html.js +692 -0
- package/src/engram/graph-store.js +877 -0
- package/src/engram/index.js +98 -14
- package/src/engram/ingest-site.js +132 -0
- package/src/engram/live-state.js +1180 -0
- package/src/engram/namespace.js +34 -1
- package/src/engram/retrieve-site.js +753 -0
- package/src/engram/safe-reader.js +597 -0
- package/src/engram/source-intake.js +251 -0
- package/src/engram/store.js +227 -9
- package/src/ingest/engine.js +21 -28
- package/src/judge/verdict.js +305 -0
- package/src/legacy-cli.js +96 -19
- package/src/mcp/bridge-inputs.js +25 -4
- package/src/mcp/cli-command-tools.js +70 -0
- package/src/mcp/registry.js +17 -13
- package/src/mcp/session-stdio-server.js +297 -19
- package/src/memory/blackboard.js +5 -4
- package/src/net/gh-api-path.js +35 -9
- package/src/net/test-egress-guard.js +16 -4
- package/src/plugin/manifest.js +12 -8
- package/src/review/ai-review.js +110 -10
- package/src/review/investor-dd-devtestbot.js +17 -6
- package/src/review/investor-dd-orchestrator.js +21 -13
- package/src/review/local-review.js +26 -16
- package/src/review/omargate-cache.js +5 -5
- package/src/review/omargate-orchestrator.js +89 -17
- package/src/review/replay.js +9 -4
- package/src/review/report.js +22 -13
- package/src/review/typesafe-severity.js +201 -0
- package/src/session/admission-auth.js +34 -7
- package/src/session/admission.js +110 -15
- package/src/session/attachment-summary.js +77 -0
- package/src/session/edit-summary.js +19 -0
- package/src/session/invitations.js +65 -1
- package/src/session/mentions.js +17 -9
- package/src/session/message-edits.js +5 -4
- package/src/session/preview.js +10 -2
- package/src/session/recap.js +3 -2
- package/src/session/room-files.js +519 -0
- package/src/session/route-classes.js +22 -8
- package/src/session/setup-guides.js +4 -0
- package/src/session/standup.js +192 -0
- package/src/session/sync.js +229 -10
- package/src/session/tickets.js +41 -0
- package/src/session/transcript.js +2 -1
- package/src/session/wake/resolve-target.js +7 -3
- package/src/skills/listener-runtime.cjs +92 -0
- package/src/skills/service.js +207 -0
- package/src/swarm/factory.js +5 -5
- package/src/swarm/pentest.js +9 -9
- package/src/swarm/report.js +4 -4
- package/src/swarm/runtime.js +15 -12
- package/src/swarm/scenario-dsl.js +7 -5
- package/src/telemetry/ledger.js +16 -7
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ENGRAM §EW08 — Deterministic capability index builder and verifier.
|
|
3
|
+
*
|
|
4
|
+
* Produces a capabilityIndex document from a registry of implemented skills.
|
|
5
|
+
* Only implemented (handler-present), supported/experimental, public-visibility
|
|
6
|
+
* skills are included. The index carries a sha256 digest over the canonical
|
|
7
|
+
* body so consumers can verify integrity without trusting the transport.
|
|
8
|
+
*
|
|
9
|
+
* Exports:
|
|
10
|
+
* - buildCapabilityIndex({ registry, publisherOrigin, issuedAt, ttlSeconds, revoked })
|
|
11
|
+
* - verifyCapabilityIndex(doc, { expectedOrigin, now })
|
|
12
|
+
* - toMcpTool(skill) — project a skill onto an MCP tool descriptor
|
|
13
|
+
* - renderSkillText(skill) — render a skill as plain text
|
|
14
|
+
* - checkRevocation(index, capabilityVersion)
|
|
15
|
+
* - findSkill(index, capabilityName)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
import { canonicalize } from "./canonical.js";
|
|
20
|
+
import { capabilityIndexSchema, CapabilityIndexError, digestMismatch, indexExpired, originMismatch, capabilityRevoked } from "./contracts/capability-index.js";
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Bounded text validation (mirrors contracts/capability-index.js)
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/** Control-character regex: matches U+0000-U+001F and U+007F-U+009F. */
|
|
27
|
+
const CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]/;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build a deterministic capability index.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} options
|
|
33
|
+
* @param {Map|Array} options.registry — entries with { capability, version, handler, status, visibility, ...skillFields }
|
|
34
|
+
* @param {string} options.publisherOrigin — https origin (e.g. "https://shop.example")
|
|
35
|
+
* @param {string} options.issuedAt — ISO-8601 UTC datetime
|
|
36
|
+
* @param {number} [options.ttlSeconds=86400] — seconds until expiry
|
|
37
|
+
* @param {string[]} [options.revoked=[]] — capability version strings to revoke
|
|
38
|
+
* @returns {object} a validated capability index document
|
|
39
|
+
*/
|
|
40
|
+
export function buildCapabilityIndex({ registry, publisherOrigin, issuedAt, ttlSeconds = 86400, revoked = [] } = {}) {
|
|
41
|
+
if (!registry) throw new CapabilityIndexError("registry is required");
|
|
42
|
+
if (!publisherOrigin) throw new CapabilityIndexError("publisherOrigin is required");
|
|
43
|
+
if (!issuedAt) throw new CapabilityIndexError("issuedAt is required");
|
|
44
|
+
|
|
45
|
+
const entries = Array.isArray(registry) ? registry : (Symbol.iterator in Object(registry) ? [...registry] : []);
|
|
46
|
+
const skills = [];
|
|
47
|
+
const revokedSet = new Set(revoked);
|
|
48
|
+
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
if (!entry || typeof entry !== "object") continue;
|
|
51
|
+
if (!entry.capability || !entry.version) continue;
|
|
52
|
+
// Only include entries that have a real handler function (implemented) and supported/experimental status.
|
|
53
|
+
if (typeof entry.handler !== "function") continue;
|
|
54
|
+
if (entry.status !== "supported" && entry.status !== "experimental") continue;
|
|
55
|
+
if (entry.visibility !== "public") continue;
|
|
56
|
+
// Skip revoked versions.
|
|
57
|
+
if (revokedSet.has(entry.version)) continue;
|
|
58
|
+
|
|
59
|
+
const skill = {
|
|
60
|
+
capability: entry.capability,
|
|
61
|
+
version: entry.version,
|
|
62
|
+
title: entry.title,
|
|
63
|
+
purpose: entry.purpose,
|
|
64
|
+
inputSchema: entry.inputSchema ?? { type: "object", properties: {} },
|
|
65
|
+
outputSchema: entry.outputSchema ?? { type: "object", properties: {} },
|
|
66
|
+
scope: entry.scope ?? "read",
|
|
67
|
+
sideEffects: entry.sideEffects ?? "none",
|
|
68
|
+
requiresConfirmation: entry.requiresConfirmation ?? false,
|
|
69
|
+
visibility: "public",
|
|
70
|
+
};
|
|
71
|
+
if (entry.fallback !== undefined && entry.fallback !== null) {
|
|
72
|
+
// Validate fallback has no control characters (bounded text rule).
|
|
73
|
+
const fb = String(entry.fallback);
|
|
74
|
+
if (CONTROL_RE.test(fb)) {
|
|
75
|
+
throw new CapabilityIndexError("fallback must not contain control characters");
|
|
76
|
+
}
|
|
77
|
+
skill.fallback = entry.fallback;
|
|
78
|
+
}
|
|
79
|
+
skills.push(skill);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Sort by capability name for deterministic output.
|
|
83
|
+
skills.sort((a, b) => {
|
|
84
|
+
if (a.capability < b.capability) return -1;
|
|
85
|
+
if (a.capability > b.capability) return 1;
|
|
86
|
+
return 0;
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const issuedDate = new Date(issuedAt);
|
|
90
|
+
if (isNaN(issuedDate.getTime())) throw new CapabilityIndexError('issuedAt is not a valid date');
|
|
91
|
+
if (ttlSeconds <= 0 || !Number.isFinite(ttlSeconds)) throw new CapabilityIndexError('ttlSeconds must be a positive finite number');
|
|
92
|
+
const expiresDate = new Date(issuedDate.getTime() + ttlSeconds * 1000);
|
|
93
|
+
const expiresAt = expiresDate.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
94
|
+
|
|
95
|
+
const body = {
|
|
96
|
+
publisherOrigin,
|
|
97
|
+
indexVersion: "1.0.0",
|
|
98
|
+
issuedAt: issuedDate.toISOString().replace(/\.\d{3}Z$/, "Z"),
|
|
99
|
+
expiresAt,
|
|
100
|
+
revoked,
|
|
101
|
+
skills,
|
|
102
|
+
// Placeholder digest — replaced after computation.
|
|
103
|
+
digest: "sha256:0000000000000000000000000000000000000000000000000000000000000000",
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const digest = computeDigest(body);
|
|
107
|
+
body.digest = digest;
|
|
108
|
+
|
|
109
|
+
return capabilityIndexSchema.parse(body);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Verify a capability index document.
|
|
114
|
+
*
|
|
115
|
+
* Checks:
|
|
116
|
+
* 1. The digest matches (integrity).
|
|
117
|
+
* 2. expectedOrigin is provided and matches the publisher origin (authenticity).
|
|
118
|
+
* 3. No skill's version appears in the revoked list.
|
|
119
|
+
* 4. The index has not expired.
|
|
120
|
+
* 5. The schema is valid.
|
|
121
|
+
*
|
|
122
|
+
* @param {object} doc — the index document to verify
|
|
123
|
+
* @param {object} options
|
|
124
|
+
* @param {string} options.expectedOrigin — the https origin to expect (required)
|
|
125
|
+
* @param {string|Date} [options.now] — current time (defaults to now)
|
|
126
|
+
* @returns {{ ok: boolean, reason?: string, error?: Error }}
|
|
127
|
+
*/
|
|
128
|
+
export function verifyCapabilityIndex(doc, { expectedOrigin, now } = {}) {
|
|
129
|
+
// expectedOrigin is required.
|
|
130
|
+
if (!expectedOrigin) {
|
|
131
|
+
return { ok: false, reason: "expectedOrigin is required" };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Schema validation first.
|
|
135
|
+
const parsed = capabilityIndexSchema.safeParse(doc);
|
|
136
|
+
if (!parsed.success) {
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
reason: `schema validation failed: ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const index = parsed.data;
|
|
144
|
+
|
|
145
|
+
// Digest integrity.
|
|
146
|
+
const computedDigest = computeDigest(index);
|
|
147
|
+
if (computedDigest !== index.digest) {
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
reason: `digest mismatch: expected ${index.digest}, computed ${computedDigest}`,
|
|
151
|
+
error: digestMismatch(index.digest, computedDigest),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Origin check.
|
|
156
|
+
const expectedNorm = normalizeOrigin(expectedOrigin);
|
|
157
|
+
const actualNorm = normalizeOrigin(index.publisherOrigin);
|
|
158
|
+
if (expectedNorm !== actualNorm) {
|
|
159
|
+
return {
|
|
160
|
+
ok: false,
|
|
161
|
+
reason: `origin mismatch: expected ${expectedNorm}, got ${actualNorm}`,
|
|
162
|
+
error: originMismatch(expectedNorm, actualNorm),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Revocation check: any skill whose version is in the revoked list fails.
|
|
167
|
+
const revokedSet = new Set(index.revoked ?? []);
|
|
168
|
+
for (const skill of index.skills) {
|
|
169
|
+
if (revokedSet.has(skill.version)) {
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
reason: `revoked skill in index: ${skill.version}`,
|
|
173
|
+
error: capabilityRevoked(skill.capability, skill.version),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Expiry check.
|
|
179
|
+
const nowDate = now ? new Date(now) : new Date();
|
|
180
|
+
const expiresDate = new Date(index.expiresAt);
|
|
181
|
+
if (nowDate >= expiresDate) {
|
|
182
|
+
return {
|
|
183
|
+
ok: false,
|
|
184
|
+
reason: `index expired at ${index.expiresAt} (now: ${nowDate.toISOString()})`,
|
|
185
|
+
error: indexExpired(index.expiresAt, nowDate.toISOString()),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { ok: true };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Project a skill descriptor onto an MCP tool descriptor unchanged.
|
|
194
|
+
*
|
|
195
|
+
* Generates a name from capability@version, keeping the original title and
|
|
196
|
+
* purpose as description. Annotations are derived from scope and sideEffects:
|
|
197
|
+
* - readOnlyHint = scope === "read"
|
|
198
|
+
* - destructiveHint = scope === "write" && sideEffects === "real"
|
|
199
|
+
* - idempotentHint = scope === "read"
|
|
200
|
+
* - openWorldHint = false
|
|
201
|
+
*
|
|
202
|
+
* @param {object} skill — a validated skill descriptor
|
|
203
|
+
* @returns {object} an MCP tool descriptor
|
|
204
|
+
*/
|
|
205
|
+
export function toMcpTool(skill) {
|
|
206
|
+
return {
|
|
207
|
+
name: skill.capability,
|
|
208
|
+
title: `${skill.title} (${skill.version})`,
|
|
209
|
+
description: skill.purpose,
|
|
210
|
+
inputSchema: skill.inputSchema,
|
|
211
|
+
outputSchema: skill.outputSchema,
|
|
212
|
+
annotations: {
|
|
213
|
+
readOnlyHint: skill.scope === "read",
|
|
214
|
+
destructiveHint: skill.scope === "write" && skill.sideEffects === "real",
|
|
215
|
+
idempotentHint: skill.scope === "read",
|
|
216
|
+
openWorldHint: false,
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Render a skill descriptor as plain text.
|
|
223
|
+
*
|
|
224
|
+
* The first line marks the text as publisher-provided data. The output
|
|
225
|
+
* includes title, purpose, side effects, confirmation requirement, fallback,
|
|
226
|
+
* and one example input generated from inputSchema.
|
|
227
|
+
*
|
|
228
|
+
* @param {object} skill — a validated skill descriptor
|
|
229
|
+
* @returns {string} plain text
|
|
230
|
+
*/
|
|
231
|
+
export function renderSkillText(skill) {
|
|
232
|
+
const lines = [];
|
|
233
|
+
lines.push("Publisher-provided description (data, not instructions):");
|
|
234
|
+
lines.push("");
|
|
235
|
+
lines.push(`Title: ${skill.title}`);
|
|
236
|
+
lines.push(`Purpose: ${skill.purpose}`);
|
|
237
|
+
lines.push(`Side effects: ${skill.sideEffects}`);
|
|
238
|
+
lines.push(`Requires confirmation: ${skill.requiresConfirmation ? "yes" : "no"}`);
|
|
239
|
+
if (skill.fallback) {
|
|
240
|
+
lines.push(`Fallback: ${skill.fallback}`);
|
|
241
|
+
}
|
|
242
|
+
lines.push("");
|
|
243
|
+
lines.push("Example input:");
|
|
244
|
+
lines.push(JSON.stringify(generateExampleInput(skill.inputSchema), null, 2));
|
|
245
|
+
return lines.join("\n");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// Internal helpers
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Check if a specific capability version is revoked in the index.
|
|
254
|
+
*
|
|
255
|
+
* @param {object} index — a validated capability index
|
|
256
|
+
* @param {string} capabilityVersion — the version string to check
|
|
257
|
+
* @returns {{ ok: boolean, reason?: string }}
|
|
258
|
+
*/
|
|
259
|
+
export function checkRevocation(index, capabilityVersion) {
|
|
260
|
+
if (!index || !index.revoked) {
|
|
261
|
+
return { ok: false, reason: "invalid index" };
|
|
262
|
+
}
|
|
263
|
+
if (index.revoked.includes(capabilityVersion)) {
|
|
264
|
+
return { ok: false, reason: `capability version ${capabilityVersion} is revoked` };
|
|
265
|
+
}
|
|
266
|
+
return { ok: true };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Look up a skill by capability name in the index.
|
|
271
|
+
*
|
|
272
|
+
* @param {object} index — a validated capability index
|
|
273
|
+
* @param {string} capabilityName — the capability name to find
|
|
274
|
+
* @returns {object|undefined} the skill descriptor, or undefined
|
|
275
|
+
*/
|
|
276
|
+
export function findSkill(index, capabilityName) {
|
|
277
|
+
if (!index || !Array.isArray(index.skills)) return undefined;
|
|
278
|
+
return index.skills.find((s) => s.capability === capabilityName);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Compute the sha256 digest of the canonical JSON of a body.
|
|
283
|
+
* Strips the `digest` field before canonicalization.
|
|
284
|
+
*/
|
|
285
|
+
function stripUndefined(obj) {
|
|
286
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
287
|
+
if (Array.isArray(obj)) return obj.map(stripUndefined);
|
|
288
|
+
const out = {};
|
|
289
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
290
|
+
if (v !== undefined) out[k] = stripUndefined(v);
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function computeDigest(body) {
|
|
296
|
+
const { digest: _digest, ...bodyWithoutDigest } = body;
|
|
297
|
+
const canonical = canonicalize(stripUndefined(bodyWithoutDigest));
|
|
298
|
+
const hash = createHash("sha256").update(canonical, "utf-8").digest("hex");
|
|
299
|
+
return `sha256:${hash}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Normalize an origin for comparison: lowercase, strip trailing slash.
|
|
304
|
+
*/
|
|
305
|
+
function normalizeOrigin(origin) {
|
|
306
|
+
let s = String(origin ?? "").trim().toLowerCase();
|
|
307
|
+
if (s.endsWith("/")) s = s.slice(0, -1);
|
|
308
|
+
return s;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Generate an example input value from a JSON Schema.
|
|
313
|
+
*
|
|
314
|
+
* For each property in the schema, generates a representative value:
|
|
315
|
+
* - string → a short example string
|
|
316
|
+
* - number → 1
|
|
317
|
+
* - boolean → true
|
|
318
|
+
* - array → empty array
|
|
319
|
+
* - object → recurses
|
|
320
|
+
* - default → null
|
|
321
|
+
*
|
|
322
|
+
* @param {object} schema — a JSON Schema object
|
|
323
|
+
* @returns {object} an example input
|
|
324
|
+
*/
|
|
325
|
+
function generateExampleInput(schema) {
|
|
326
|
+
if (!schema || typeof schema !== "object") return {};
|
|
327
|
+
const props = schema.properties;
|
|
328
|
+
if (!props || typeof props !== "object") return {};
|
|
329
|
+
const example = {};
|
|
330
|
+
for (const [key, propSchema] of Object.entries(props)) {
|
|
331
|
+
if (!propSchema || typeof propSchema !== "object") {
|
|
332
|
+
example[key] = null;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
// const and enum take precedence over type-based defaults
|
|
336
|
+
if ("const" in propSchema) {
|
|
337
|
+
example[key] = propSchema.const;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (Array.isArray(propSchema.enum) && propSchema.enum.length > 0) {
|
|
341
|
+
example[key] = propSchema.enum[0];
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
switch (propSchema.type) {
|
|
345
|
+
case "string":
|
|
346
|
+
example[key] = `example-${key}`;
|
|
347
|
+
break;
|
|
348
|
+
case "number":
|
|
349
|
+
case "integer":
|
|
350
|
+
example[key] = 1;
|
|
351
|
+
break;
|
|
352
|
+
case "boolean":
|
|
353
|
+
example[key] = true;
|
|
354
|
+
break;
|
|
355
|
+
case "array":
|
|
356
|
+
example[key] = [];
|
|
357
|
+
break;
|
|
358
|
+
case "object":
|
|
359
|
+
example[key] = generateExampleInput(propSchema);
|
|
360
|
+
break;
|
|
361
|
+
default:
|
|
362
|
+
example[key] = null;
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return example;
|
|
367
|
+
}
|