backend-skeleton 1.1.0 → 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.
- package/README.md +27 -0
- package/bin/bskel.mjs +147 -14
- package/handles/providers/java-spring/observe.mjs +15 -1
- package/handles/providers/java-spring/templates/ContractObservationAspect.java.tmpl +38 -0
- package/handles/providers/java-spring/templates/ObserveSchemaLoader.java.tmpl +13 -7
- package/handles/providers/java-spring/templates/ReceiptSigner.java.tmpl +174 -0
- package/handles/providers/python-fastapi/observe.mjs +5 -0
- package/handles/providers/python-fastapi/templates/observe_contract.py.tmpl +19 -0
- package/handles/providers/python-fastapi/templates/receipt_sign.py.tmpl +68 -0
- package/handles/providers/typescript-express/observe.mjs +5 -0
- package/handles/providers/typescript-express/plan.mjs +8 -1
- package/handles/providers/typescript-express/templates/observeContract.ts.tmpl +19 -0
- package/handles/providers/typescript-express/templates/receiptSign.ts.tmpl +65 -0
- package/lib/cli.mjs +16 -1
- package/lib/field-dependencies.mjs +2 -1
- package/lib/gate-definitions.mjs +8 -4
- package/lib/scan-report-paths.mjs +47 -0
- package/package.json +1 -1
- package/scanners/adapters/_java-spring-analyzer.mjs +16 -1
- package/scanners/adapters/python-fastapi.mjs +124 -20
- package/scanners/adapters/typescript-express.mjs +20 -5
- package/scanners/index.mjs +1 -1
- package/schemas/conformance-report.schema.json +12 -1
- package/schemas/observe-receipt.schema.json +10 -1
- package/schemas/scan-report.schema.json +1 -1
|
@@ -23,7 +23,14 @@ const EXCLUDE_GLOBS = ['!**/.venv/**', '!**/site-packages/**', '!**/node_modules
|
|
|
23
23
|
// `fastapi==0.100.0`.
|
|
24
24
|
const FASTAPI_DEP_RE = /(?:^|[\s"'[])fastapi(?:\[[^\]]*\])?(?:[\s"',\]=<>~!;]|$)/mi;
|
|
25
25
|
|
|
26
|
-
const VERB_DECORATOR_RE =
|
|
26
|
+
const VERB_DECORATOR_RE = /@(\w+)\.(get|post|put|patch|delete)\s*\(/gi;
|
|
27
|
+
const ROUTER_DECL_RE = /(\w+)\s*=\s*APIRouter\s*\(/g;
|
|
28
|
+
|
|
29
|
+
// D-fastapi-generic-module-name: real evidence only (polarsource/polar), not a speculative list --
|
|
30
|
+
// same "validated against a real oracle" precedent as KNOWN_DTO_SUFFIXES below. 57 real files
|
|
31
|
+
// literally named `endpoints.py`, 1 named `router.py`; no other generic stem (`routes`, `views`,
|
|
32
|
+
// `api`) has ever been observed.
|
|
33
|
+
const GENERIC_ROUTER_STEMS = new Set(['endpoints', 'router']);
|
|
27
34
|
const CLASS_RE = /^class\s+(\w+)\s*\(([^)]*)\)\s*:/gm;
|
|
28
35
|
const INCLUDE_ROUTER_RE = /include_router\s*\(/g;
|
|
29
36
|
|
|
@@ -104,24 +111,42 @@ function capitalize(s) {
|
|
|
104
111
|
// resolved against a global prefix applied elsewhere (e.g. `include_router(prefix=...)`) -- that
|
|
105
112
|
// asymmetry is exactly what `pathPrefixSignals`/`unknowns` exists to flag, same role java-spring's
|
|
106
113
|
// detectGlobalPathPrefixSignals plays for `configurePathMatch`/`context-path`.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
//
|
|
115
|
+
// D-fastapi-multi-router-per-file: a real dogfooding find against `polarsource/polar` (a 400+
|
|
116
|
+
// route production FastAPI monorepo) -- 3 real files (`checkout/endpoints.py`,
|
|
117
|
+
// `member/endpoints.py`, `auth/oauth2/router.py`) declare MORE THAN ONE `<var> = APIRouter(...)`
|
|
118
|
+
// in the same file (e.g. member.py's `router = APIRouter(prefix="/members")` plus a second,
|
|
119
|
+
// distinct `customer_members_router = APIRouter(prefix="/customers")` for a nested customer-scoped
|
|
120
|
+
// resource). The single-basePath-per-file design used to always read only the FIRST `APIRouter(`
|
|
121
|
+
// occurrence and apply it to every decorator in the file regardless of which router variable
|
|
122
|
+
// actually decorates it -- silently discarding the second router's own real prefix. Returns
|
|
123
|
+
// Map<varName, prefix> for every declared router in the file so each decorator can look up its
|
|
124
|
+
// OWN router's prefix instead.
|
|
125
|
+
function extractRouterPrefixes(text) {
|
|
126
|
+
const prefixes = new Map();
|
|
127
|
+
for (const m of text.matchAll(ROUTER_DECL_RE)) {
|
|
128
|
+
const openIdx = m.index + m[0].length - 1;
|
|
129
|
+
const closeIdx = matchBalancedParens(text, openIdx);
|
|
130
|
+
if (closeIdx === -1) continue;
|
|
131
|
+
const prefixMatch = text.slice(openIdx + 1, closeIdx).match(/prefix\s*=\s*["']([^"']*)["']/);
|
|
132
|
+
prefixes.set(m[1], prefixMatch ? prefixMatch[1] : '');
|
|
133
|
+
}
|
|
134
|
+
return prefixes;
|
|
115
135
|
}
|
|
116
136
|
|
|
117
137
|
// `operationId` is always null -- see the adapter's own `api.operations: false` and
|
|
118
138
|
// D-fastapi-adapter in DECISIONS.md: FastAPI generates operation ids at request-handling time
|
|
119
139
|
// (per-project, sometimes via a custom `generate_unique_id_function`), never pinned in source the
|
|
120
140
|
// way `@Operation(operationId=...)` is for Java, so there is nothing honest to statically correlate.
|
|
141
|
+
//
|
|
142
|
+
// D-fastapi-multi-router-per-file: `routerVar` (the exact identifier before `.get`/`.post`/...) is
|
|
143
|
+
// now captured per endpoint so the caller can resolve basePath per-router-variable rather than
|
|
144
|
+
// once for the whole file -- see extractRouterPrefixes() above.
|
|
121
145
|
function extractEndpoints(text) {
|
|
122
146
|
const endpoints = [];
|
|
123
147
|
for (const m of text.matchAll(VERB_DECORATOR_RE)) {
|
|
124
|
-
const
|
|
148
|
+
const routerVar = m[1];
|
|
149
|
+
const verb = m[2].toUpperCase();
|
|
125
150
|
const openIdx = m.index + m[0].length - 1;
|
|
126
151
|
const closeIdx = matchBalancedParens(text, openIdx);
|
|
127
152
|
if (closeIdx === -1) continue;
|
|
@@ -134,11 +159,19 @@ function extractEndpoints(text) {
|
|
|
134
159
|
const funcMatch = afterDecoratorRe.exec(text);
|
|
135
160
|
if (!funcMatch) continue;
|
|
136
161
|
|
|
137
|
-
endpoints.push({ verb, path: pathMatch[1], operationId: null, method: funcMatch[1], line: lineNumberAt(text, m.index) });
|
|
162
|
+
endpoints.push({ verb, path: pathMatch[1], operationId: null, method: funcMatch[1], routerVar, line: lineNumberAt(text, m.index) });
|
|
138
163
|
}
|
|
139
164
|
return endpoints;
|
|
140
165
|
}
|
|
141
166
|
|
|
167
|
+
// snake_case/mixed identifier -> PascalCase, e.g. "customer_members_router" -> "CustomerMembersRouter",
|
|
168
|
+
// "inner_router" -> "InnerRouter". Only used for a NON-default router variable name (see
|
|
169
|
+
// scanPythonFastApi below) -- the common single-router-per-file case keeps its existing
|
|
170
|
+
// `${capitalize(moduleName)}Router` className exactly as before, byte-for-byte.
|
|
171
|
+
function pascalCase(identifier) {
|
|
172
|
+
return identifier.split('_').filter(Boolean).map(capitalize).join('');
|
|
173
|
+
}
|
|
174
|
+
|
|
142
175
|
// SQLModel `class X(<bases>, table=True):` -- table name is the lowercased class name (SQLModel's
|
|
143
176
|
// own default when no explicit `__tablename__` is declared; cross-checked against the real
|
|
144
177
|
// oracle's own Alembic migration, `op.create_table("user", ...)`/`op.create_table("item", ...)`,
|
|
@@ -249,9 +282,10 @@ function extractIncludeRouterPrefixSignals(repoRoot, files) {
|
|
|
249
282
|
return signals;
|
|
250
283
|
}
|
|
251
284
|
|
|
252
|
-
// D-fastapi-adapter: paths are router-local (see
|
|
253
|
-
// at runtime, never pinned in source -- see extractEndpoints. --openapi-file +
|
|
254
|
-
// trustworthy path (contracts/openapi.mjs's existing, adapter-agnostic
|
|
285
|
+
// D-fastapi-adapter: paths are router-local (see extractRouterPrefixes); FastAPI generates
|
|
286
|
+
// operation ids at runtime, never pinned in source -- see extractEndpoints. --openapi-file +
|
|
287
|
+
// --path-prefix is the trustworthy path (contracts/openapi.mjs's existing, adapter-agnostic
|
|
288
|
+
// reconciliation).
|
|
255
289
|
const API_SURFACE_SOURCE = 'router-local paths only (this scan does not resolve a global prefix applied via ' +
|
|
256
290
|
'include_router(prefix=...) beyond a simple literal/single-variable lookup -- see unknowns below if one ' +
|
|
257
291
|
'was found) -- FastAPI generates operation ids at request-handling time (per-project, sometimes via a ' +
|
|
@@ -259,8 +293,59 @@ const API_SURFACE_SOURCE = 'router-local paths only (this scan does not resolve
|
|
|
259
293
|
'document via `bskel contract emit --openapi-file <path> --path-prefix <prefix>` for trustworthy ' +
|
|
260
294
|
'operation identity and schemas.';
|
|
261
295
|
|
|
296
|
+
// D-fastapi-generic-module-name: real polarsource/polar dogfooding found `path.basename(file,
|
|
297
|
+
// '.py')` (the module-name rule below) collapses to the literal string "endpoints"/"router" for
|
|
298
|
+
// every file using that repo's own real, common convention -- name every router file generically,
|
|
299
|
+
// carry the real domain identity in the PARENT DIRECTORY instead (`organization/endpoints.py`,
|
|
300
|
+
// `member/endpoints.py`). Two CLOSED passes, not interleaved -- a generic file's directory-derived
|
|
301
|
+
// candidate must never be adopted before every non-generic file's own literal name is known, or a
|
|
302
|
+
// later-discovered collision could arrive too late (the wrong merge would already be written).
|
|
303
|
+
// Pass 1: every non-generic-stem file's own literal name is `reserved`, unchanged from today.
|
|
304
|
+
// Pass 2: a generic-stem file's candidate (its own parent directory's basename) is adopted only if
|
|
305
|
+
// it collides with neither `reserved` nor another generic file's already-`claimed` candidate --
|
|
306
|
+
// real evidence found 9 such real collisions in polar alone (e.g. `subscription/endpoints.py`'s
|
|
307
|
+
// candidate "subscription" collides with the real, unrelated, non-generic
|
|
308
|
+
// `customer_portal/endpoints/subscription.py`). On any collision, the file keeps its old literal
|
|
309
|
+
// generic name -- never guess into a silent wrong merge. `files` is already sorted by full path
|
|
310
|
+
// (`listRgFiles`), so processing order (and therefore first-claim-wins) is deterministic.
|
|
311
|
+
function resolveGenericModuleNames(files, readFile, projectRoot) {
|
|
312
|
+
const routerFiles = [];
|
|
313
|
+
const reserved = new Set();
|
|
314
|
+
for (const file of files) {
|
|
315
|
+
if (!/APIRouter\s*\(/.test(readFile(file))) continue;
|
|
316
|
+
const stem = path.basename(file, '.py');
|
|
317
|
+
routerFiles.push(file);
|
|
318
|
+
if (!GENERIC_ROUTER_STEMS.has(stem)) reserved.add(stem);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const moduleNameByFile = new Map();
|
|
322
|
+
const claimed = new Set();
|
|
323
|
+
for (const file of routerFiles) {
|
|
324
|
+
const stem = path.basename(file, '.py');
|
|
325
|
+
if (!GENERIC_ROUTER_STEMS.has(stem)) {
|
|
326
|
+
moduleNameByFile.set(file, stem);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const dir = path.dirname(file);
|
|
330
|
+
const candidate = dir !== projectRoot ? path.basename(dir) : null;
|
|
331
|
+
if (candidate && !reserved.has(candidate) && !claimed.has(candidate)) {
|
|
332
|
+
claimed.add(candidate);
|
|
333
|
+
moduleNameByFile.set(file, candidate);
|
|
334
|
+
} else {
|
|
335
|
+
moduleNameByFile.set(file, stem); // collision or no meaningful parent -- keep the old literal name
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return moduleNameByFile;
|
|
339
|
+
}
|
|
340
|
+
|
|
262
341
|
export function scanPythonFastApi(repoRoot, projectRoot) {
|
|
263
342
|
const files = listPythonFiles(projectRoot);
|
|
343
|
+
const fileTextCache = new Map();
|
|
344
|
+
const readFile = (f) => {
|
|
345
|
+
if (!fileTextCache.has(f)) fileTextCache.set(f, fs.readFileSync(f, 'utf8'));
|
|
346
|
+
return fileTextCache.get(f);
|
|
347
|
+
};
|
|
348
|
+
const moduleNameByFile = resolveGenericModuleNames(files, readFile, projectRoot);
|
|
264
349
|
const modules = new Map();
|
|
265
350
|
const moduleEntry = (name) => {
|
|
266
351
|
if (!modules.has(name)) modules.set(name, { module: name, controllers: [], entities: [], enums: [], dtos: [] });
|
|
@@ -270,17 +355,36 @@ export function scanPythonFastApi(repoRoot, projectRoot) {
|
|
|
270
355
|
const allEntities = [];
|
|
271
356
|
const allDtos = [];
|
|
272
357
|
for (const file of files) {
|
|
273
|
-
const text =
|
|
358
|
+
const text = readFile(file);
|
|
274
359
|
|
|
275
360
|
if (/APIRouter\s*\(/.test(text)) {
|
|
276
361
|
// module = filename stem, NOT the router's own prefix -- verified against the real oracle's
|
|
277
362
|
// login.py, which declares `APIRouter(tags=["login"])` with no prefix at all, so a
|
|
278
363
|
// prefix-derived name fails on a real file while the filename stem works for every one.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
364
|
+
// D-fastapi-generic-module-name: for a GENERIC stem (`endpoints`/`router`), this is the
|
|
365
|
+
// collision-safe, directory-derived name instead -- see resolveGenericModuleNames() above.
|
|
366
|
+
const moduleName = moduleNameByFile.get(file);
|
|
367
|
+
const routerPrefixes = extractRouterPrefixes(text);
|
|
368
|
+
const rawEndpoints = extractEndpoints(text);
|
|
369
|
+
|
|
370
|
+
// D-fastapi-multi-router-per-file: group by the router variable each decorator actually
|
|
371
|
+
// belongs to (not the file as a whole) -- a decorator on a router variable this file never
|
|
372
|
+
// itself declares (e.g. imported from elsewhere, or a same-file `include_router()` alias)
|
|
373
|
+
// falls back to '' rather than guessing, same as the pre-fix single-router behavior.
|
|
374
|
+
const byRouterVar = new Map();
|
|
375
|
+
for (const ep of rawEndpoints) {
|
|
376
|
+
if (!byRouterVar.has(ep.routerVar)) byRouterVar.set(ep.routerVar, []);
|
|
377
|
+
byRouterVar.get(ep.routerVar).push(ep);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
for (const [routerVar, eps] of byRouterVar) {
|
|
381
|
+
const basePath = routerPrefixes.get(routerVar) ?? '';
|
|
382
|
+
const endpoints = eps.map((ep) => ({ verb: ep.verb, path: joinPath(basePath, ep.path), operationId: ep.operationId, method: ep.method, line: ep.line }));
|
|
383
|
+
// the common case (single router per file, conventionally named "router") keeps the
|
|
384
|
+
// existing className exactly as before -- only a second/other-named router variable
|
|
385
|
+
// gets a distinguishing className derived from its own identifier.
|
|
386
|
+
const className = routerVar === 'router' ? `${capitalize(moduleName)}Router` : `${pascalCase(routerVar)}`;
|
|
387
|
+
moduleEntry(moduleName).controllers.push({ className, basePath, operationIds: [], endpoints, file });
|
|
284
388
|
}
|
|
285
389
|
}
|
|
286
390
|
|
|
@@ -89,6 +89,20 @@ function listTypeScriptFiles(projectRoot) {
|
|
|
89
89
|
// of `router.use('/literal', subRouter)` mount edges from a graph root down to the leaf file. This
|
|
90
90
|
// extracts just the LOCAL endpoints (verb/path/handler/line) with an EMPTY prefix -- the mount-tree
|
|
91
91
|
// walk in scanTypeScriptExpress() below joins the real prefix chain afterward.
|
|
92
|
+
//
|
|
93
|
+
// D-typescript-express-inline-handlers: an inline function expression (`router.get('/x', async
|
|
94
|
+
// (req, res) => {...})`) is a DIFFERENT shape from a bare-identifier handler reference
|
|
95
|
+
// (`router.get('/x', show)`) -- confirmed live, dogfooding against a real, popular repo
|
|
96
|
+
// (gothinkster/node-express-realworld-example-app, 3,796 real GitHub stars): ALL 19 of its real
|
|
97
|
+
// route registrations use this inline form, and the bare-identifier-only gate this comment used to
|
|
98
|
+
// describe found ZERO of them (verdict: greenfield on a repo with a real, complete REST API).
|
|
99
|
+
// `method: null` (not a synthesized/guessed name) marks this case -- there is no export to
|
|
100
|
+
// correlate an inline handler to, and `handles/providers/typescript-express/plan.mjs`'s own
|
|
101
|
+
// `resolveHandlerFile()` already has a real, tested `null`-propagates-to-"resolver not generated"
|
|
102
|
+
// path for exactly this "handler correlates to nothing" case (see its own Update note in
|
|
103
|
+
// DECISIONS.md) -- this is NOT a new failure mode, just a new, real way to reach the existing one.
|
|
104
|
+
const INLINE_HANDLER_RE = /^(?:async\s+)?(?:\([^)]*\)|[$\w]+)\s*(?::[^=]*)?=>|^(?:async\s+)?function\b/;
|
|
105
|
+
|
|
92
106
|
function extractEndpoints(text) {
|
|
93
107
|
const endpoints = [];
|
|
94
108
|
for (const m of text.matchAll(VERB_CALL_RE)) {
|
|
@@ -102,13 +116,14 @@ function extractEndpoints(text) {
|
|
|
102
116
|
|
|
103
117
|
const args = splitTopLevelArgs(argsText);
|
|
104
118
|
const lastArg = args[args.length - 1]?.trim();
|
|
105
|
-
// A bare identifier only -- an inline arrow-function handler has no name to correlate to a
|
|
106
|
-
// controller file, so it's skipped rather than guessed at (same discipline as FastAPI's own
|
|
107
|
-
// "no path literal -> skip").
|
|
108
119
|
const handlerMatch = lastArg?.match(/^(\w+)$/);
|
|
109
|
-
|
|
120
|
+
const isInlineHandler = !handlerMatch && lastArg && INLINE_HANDLER_RE.test(lastArg);
|
|
121
|
+
// Neither a bare identifier nor a recognizable inline function expression (e.g. a member
|
|
122
|
+
// expression like `controller.show`, or something built dynamically) -- skip rather than
|
|
123
|
+
// guess, same discipline as the missing-path-literal case just above.
|
|
124
|
+
if (!handlerMatch && !isInlineHandler) continue;
|
|
110
125
|
|
|
111
|
-
endpoints.push({ verb, path: pathMatch[1], operationId: null, method: handlerMatch[1], line: lineNumberAt(text, m.index) });
|
|
126
|
+
endpoints.push({ verb, path: pathMatch[1], operationId: null, method: handlerMatch ? handlerMatch[1] : null, line: lineNumberAt(text, m.index) });
|
|
112
127
|
}
|
|
113
128
|
return endpoints;
|
|
114
129
|
}
|
package/scanners/index.mjs
CHANGED
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
"matched": { "type": "integer", "minimum": 0, "description": "receipts whose contract_ref equals the CURRENT contract hash -- these count as evidence for the current contract." },
|
|
31
31
|
"stale_contract_ref": { "type": "integer", "minimum": 0, "description": "receipts recorded against a contract_ref that no longer matches -- kept on record for audit/trend purposes, not counted as current evidence." },
|
|
32
32
|
"violations": { "type": "integer", "minimum": 0 },
|
|
33
|
-
"unsupported": { "type": "integer", "minimum": 0 }
|
|
33
|
+
"unsupported": { "type": "integer", "minimum": 0 },
|
|
34
|
+
"unsigned": { "type": "integer", "minimum": 0, "description": "receipts with no signature field -- computed regardless of --pubkey, for forward visibility." },
|
|
35
|
+
"signature_invalid": { "type": "integer", "minimum": 0, "description": "receipts whose signature field failed verification -- only ever non-zero when --pubkey was given; meaningless (always 0) otherwise. Excluded from matched/violations, same as stale_contract_ref." }
|
|
34
36
|
}
|
|
35
37
|
},
|
|
36
38
|
"by_operation": {
|
|
@@ -44,6 +46,15 @@
|
|
|
44
46
|
"violations": { "type": "integer", "minimum": 0 }
|
|
45
47
|
}
|
|
46
48
|
}
|
|
49
|
+
},
|
|
50
|
+
"verification": {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"additionalProperties": false,
|
|
53
|
+
"description": "Whether signature verification was even attempted at generation time -- without this, signature_invalid: 0 is ambiguous between 'verified, zero invalid' and 'never checked'. See D-runtime-conformance-receipts.",
|
|
54
|
+
"properties": {
|
|
55
|
+
"pubkey_given": { "type": "boolean" },
|
|
56
|
+
"require_signature": { "type": "boolean" }
|
|
57
|
+
}
|
|
47
58
|
}
|
|
48
59
|
}
|
|
49
60
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "urn:sbf:observe-receipt:1",
|
|
4
4
|
"title": "backend-skeleton runtime contract-conformance receipt",
|
|
5
|
-
"description": "One JSONL line ContractObservationAspect logs per @ObserveContract-annotated call. Verdict-only, by design -- never carries an observed request/response VALUE, only pointers + constraint kinds. See DECISIONS.md D-runtime-conformance-receipts. `bskel observe import --receipts <path>` reads a stream of these.",
|
|
5
|
+
"description": "One JSONL line ContractObservationAspect logs per @ObserveContract-annotated call. Verdict-only, by design -- never carries an observed request/response VALUE, only pointers + constraint kinds. See DECISIONS.md D-runtime-conformance-receipts. `bskel observe import --receipts <path>` reads a stream of these. `signature` is optional -- unsigned receipts stay valid; `bskel observe import --pubkey <path>` opts into verifying it where present. See the D-runtime-conformance-receipts 'cryptographic receipt attestation' Update note.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"required": ["feature_id", "feature_uid", "operation_id", "contract_ref", "verb", "recorded_at", "violations"],
|
|
@@ -27,6 +27,15 @@
|
|
|
27
27
|
"message": { "type": "string", "maxLength": 300 }
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
},
|
|
31
|
+
"signature": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"additionalProperties": false,
|
|
34
|
+
"required": ["algorithm", "value"],
|
|
35
|
+
"properties": {
|
|
36
|
+
"algorithm": { "const": "ed25519" },
|
|
37
|
+
"value": { "type": "string", "description": "base64-encoded raw Ed25519 signature bytes over the canonicalized receipt (this field excluded)." }
|
|
38
|
+
}
|
|
30
39
|
}
|
|
31
40
|
}
|
|
32
41
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"additionalProperties": false,
|
|
7
7
|
"required": ["schema", "terms", "adapter", "verdict", "related_modules", "collisions", "unknowns", "files_read"],
|
|
8
8
|
"properties": {
|
|
9
|
-
"schema": { "const": "sbf.scan-report/
|
|
9
|
+
"schema": { "const": "sbf.scan-report/2" },
|
|
10
10
|
"feature_id": { "type": "string" },
|
|
11
11
|
"terms": { "type": "array", "items": { "type": "string" } },
|
|
12
12
|
"adapter": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
|