@spfn/core 0.2.0-beta.66 → 0.2.0-beta.68
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 +309 -116
- package/dist/authz/index.js +398 -3
- package/dist/authz/index.js.map +1 -1
- package/dist/codegen/index.d.ts +114 -8
- package/dist/codegen/index.js +162 -3
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.js +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/contract/index.d.ts +288 -0
- package/dist/contract/index.js +534 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/db/index.js +13 -10
- package/dist/db/index.js.map +1 -1
- package/dist/{define-middleware-DuXD8Hvu.d.ts → define-middleware-B9bFuXVU.d.ts} +1 -1
- package/dist/errors/index.js +398 -3
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +3 -3
- package/dist/event/sse/client.d.ts +2 -2
- package/dist/event/sse/index.d.ts +4 -4
- package/dist/event/sse/index.js +9 -0
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.d.ts +2 -2
- package/dist/event/ws/index.d.ts +3 -3
- package/dist/middleware/index.d.ts +108 -11
- package/dist/middleware/index.js +769 -632
- package/dist/middleware/index.js.map +1 -1
- package/dist/route/index.d.ts +8 -552
- package/dist/route/index.js +36 -0
- package/dist/route/index.js.map +1 -1
- package/dist/router-DhvbMhef.d.ts +641 -0
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.js +9 -0
- package/dist/server/index.js.map +1 -1
- package/dist/{token-manager-jKD_EsSE.d.ts → token-manager-vZeqBbtA.d.ts} +7 -0
- package/dist/{types-DVjf37yO.d.ts → types-CF-37KAG.d.ts} +1 -1
- package/dist/{types-BFB72jbM.d.ts → types-D9uMxeQS.d.ts} +1 -1
- package/package.json +11 -9
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, statSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
// src/contract/collect.ts
|
|
6
|
+
var ContractCollectionError = class extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "ContractCollectionError";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
function isRouter(value) {
|
|
13
|
+
return value !== null && typeof value === "object" && "routes" in value && "_routes" in value;
|
|
14
|
+
}
|
|
15
|
+
function isRouteDef(value) {
|
|
16
|
+
return value !== null && typeof value === "object" && "handler" in value;
|
|
17
|
+
}
|
|
18
|
+
function toJsonSchema(schema) {
|
|
19
|
+
return JSON.parse(JSON.stringify(schema));
|
|
20
|
+
}
|
|
21
|
+
var REQUEST_SECTIONS = ["params", "query", "body", "formData", "headers", "cookies"];
|
|
22
|
+
function toContractRequest(input) {
|
|
23
|
+
const request = {};
|
|
24
|
+
if (!input) {
|
|
25
|
+
return request;
|
|
26
|
+
}
|
|
27
|
+
for (const section of REQUEST_SECTIONS) {
|
|
28
|
+
const schema = input[section];
|
|
29
|
+
if (schema) {
|
|
30
|
+
request[section] = toJsonSchema(schema);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return request;
|
|
34
|
+
}
|
|
35
|
+
function visitRouter(router, trail, found) {
|
|
36
|
+
for (const [name, entry] of Object.entries(router.routes)) {
|
|
37
|
+
if (isRouter(entry)) {
|
|
38
|
+
visitRouter(entry, [...trail, name], found);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (!isRouteDef(entry)) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const routeDef = entry;
|
|
45
|
+
if (!routeDef.contract) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
addOperation(name, routeDef, [...trail, name], found);
|
|
49
|
+
}
|
|
50
|
+
for (const packageRouter of router._packageRouters ?? []) {
|
|
51
|
+
visitRouter(packageRouter, [...trail, "(package)"], found);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function addOperation(name, routeDef, trail, found) {
|
|
55
|
+
const where = trail.join(".");
|
|
56
|
+
const contract = routeDef.contract;
|
|
57
|
+
if (!routeDef.method || !routeDef.path) {
|
|
58
|
+
throw new ContractCollectionError(
|
|
59
|
+
`Contracted route "${where}" has no method or path. A contract describes an operation on the wire, so both are required.`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (!contract.since) {
|
|
63
|
+
throw new ContractCollectionError(
|
|
64
|
+
`Contracted route "${where}" has no "since" version. The version an operation first appeared in is part of the promise.`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (!contract.response) {
|
|
68
|
+
throw new ContractCollectionError(
|
|
69
|
+
`Contracted route "${where}" declares no response schema. An operation with no body declares Type.Null().`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const existing = found.get(name);
|
|
73
|
+
if (existing) {
|
|
74
|
+
throw new ContractCollectionError(
|
|
75
|
+
`Two contracted routes are both named "${name}" (${existing.trail} and ${where}). An operation is identified by its name across versions, so names must be unique.`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
found.set(name, {
|
|
79
|
+
trail: where,
|
|
80
|
+
operation: {
|
|
81
|
+
name,
|
|
82
|
+
method: routeDef.method,
|
|
83
|
+
path: routeDef.path,
|
|
84
|
+
since: contract.since,
|
|
85
|
+
auth: contract.auth ?? "none",
|
|
86
|
+
requiresSession: contract.requiresSession ?? false,
|
|
87
|
+
...contract.deprecatedIn ? { deprecatedIn: contract.deprecatedIn } : {},
|
|
88
|
+
request: toContractRequest(routeDef.input),
|
|
89
|
+
interceptor: toContractRequest(routeDef.interceptor),
|
|
90
|
+
response: toJsonSchema(contract.response)
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function collectContractDocument(router) {
|
|
95
|
+
const found = /* @__PURE__ */ new Map();
|
|
96
|
+
visitRouter(router, ["router"], found);
|
|
97
|
+
const operations = [...found.values()].map((entry) => entry.operation).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
98
|
+
return { documentVersion: 1, operations };
|
|
99
|
+
}
|
|
100
|
+
function canonicalize(value) {
|
|
101
|
+
if (Array.isArray(value)) {
|
|
102
|
+
return value.map(canonicalize);
|
|
103
|
+
}
|
|
104
|
+
if (value === null || typeof value !== "object") {
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
const source = value;
|
|
108
|
+
const sorted = {};
|
|
109
|
+
for (const key of Object.keys(source).sort()) {
|
|
110
|
+
if (source[key] === void 0) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
sorted[key] = canonicalize(source[key]);
|
|
114
|
+
}
|
|
115
|
+
return sorted;
|
|
116
|
+
}
|
|
117
|
+
function stableStringify(value) {
|
|
118
|
+
return JSON.stringify(canonicalize(value));
|
|
119
|
+
}
|
|
120
|
+
function stableStringifyPretty(value) {
|
|
121
|
+
return `${JSON.stringify(canonicalize(value), null, 4)}
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
124
|
+
function stableDigest(value) {
|
|
125
|
+
return createHash("sha256").update(stableStringify(value), "utf8").digest("hex");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/contract/compare.ts
|
|
129
|
+
var REQUEST_SECTIONS2 = ["params", "query", "body", "formData", "headers", "cookies"];
|
|
130
|
+
function leafSignature(schema) {
|
|
131
|
+
const { properties, required, items, ...rest } = schema;
|
|
132
|
+
return stableStringify(rest);
|
|
133
|
+
}
|
|
134
|
+
function propertiesOf(schema) {
|
|
135
|
+
const properties = schema.properties;
|
|
136
|
+
if (!properties || typeof properties !== "object") {
|
|
137
|
+
return {};
|
|
138
|
+
}
|
|
139
|
+
return properties;
|
|
140
|
+
}
|
|
141
|
+
function requiredOf(schema) {
|
|
142
|
+
const required = schema.required;
|
|
143
|
+
if (!Array.isArray(required)) {
|
|
144
|
+
return /* @__PURE__ */ new Set();
|
|
145
|
+
}
|
|
146
|
+
return new Set(required.filter((name) => typeof name === "string"));
|
|
147
|
+
}
|
|
148
|
+
function requiresClientToSend(schema) {
|
|
149
|
+
if (schema.type !== "object") {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
return requiredOf(schema).size > 0;
|
|
153
|
+
}
|
|
154
|
+
function report(ctx, kind, location, detail) {
|
|
155
|
+
ctx.violations.push({ kind, operation: ctx.operation, location, detail });
|
|
156
|
+
}
|
|
157
|
+
function compareSchema(before, after, location, ctx) {
|
|
158
|
+
if (!before && !after) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (before && !after) {
|
|
162
|
+
if (ctx.side === "response") {
|
|
163
|
+
report(ctx, "response.field-removed", location, "the response no longer carries this, and old clients read it");
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (!before && after) {
|
|
168
|
+
if (ctx.side === "request" && requiresClientToSend(after)) {
|
|
169
|
+
report(
|
|
170
|
+
ctx,
|
|
171
|
+
"request.required-field-added",
|
|
172
|
+
location,
|
|
173
|
+
"this is new and mandatory, so every already-released client is refused"
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const from = before;
|
|
179
|
+
const to = after;
|
|
180
|
+
if (leafSignature(from) !== leafSignature(to)) {
|
|
181
|
+
report(
|
|
182
|
+
ctx,
|
|
183
|
+
ctx.side === "request" ? "request.type-changed" : "response.type-changed",
|
|
184
|
+
location,
|
|
185
|
+
`the declared type changed: ${leafSignature(from)} \u2192 ${leafSignature(to)}`
|
|
186
|
+
);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
compareProperties(from, to, location, ctx);
|
|
190
|
+
compareSchema(
|
|
191
|
+
from.items,
|
|
192
|
+
to.items,
|
|
193
|
+
`${location}[]`,
|
|
194
|
+
ctx
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
function compareProperties(from, to, location, ctx) {
|
|
198
|
+
const beforeProperties = propertiesOf(from);
|
|
199
|
+
const afterProperties = propertiesOf(to);
|
|
200
|
+
const beforeRequired = requiredOf(from);
|
|
201
|
+
const afterRequired = requiredOf(to);
|
|
202
|
+
for (const [name, beforeSchema] of Object.entries(beforeProperties)) {
|
|
203
|
+
const where = `${location}.${name}`;
|
|
204
|
+
const afterSchema = afterProperties[name];
|
|
205
|
+
compareSchema(beforeSchema, afterSchema, where, ctx);
|
|
206
|
+
if (!afterSchema) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const wasRequired = beforeRequired.has(name);
|
|
210
|
+
const isRequired = afterRequired.has(name);
|
|
211
|
+
if (ctx.side === "request" && !wasRequired && isRequired) {
|
|
212
|
+
report(ctx, "request.field-became-required", where, "clients that never sent this are now refused");
|
|
213
|
+
}
|
|
214
|
+
if (ctx.side === "response" && wasRequired && !isRequired) {
|
|
215
|
+
report(ctx, "response.field-became-optional", where, "clients that counted on this always arriving now break");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
for (const name of Object.keys(afterProperties)) {
|
|
219
|
+
if (beforeProperties[name]) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (ctx.side === "request" && afterRequired.has(name)) {
|
|
223
|
+
report(
|
|
224
|
+
ctx,
|
|
225
|
+
"request.required-field-added",
|
|
226
|
+
`${location}.${name}`,
|
|
227
|
+
"this is new and mandatory, so every already-released client is refused"
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function compareRequestSections(before, after, prefix, ctx) {
|
|
233
|
+
for (const section of REQUEST_SECTIONS2) {
|
|
234
|
+
compareSchema(before[section], after[section], `${prefix}.${section}`, ctx);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function compareOperation(before, after) {
|
|
238
|
+
const violations = [];
|
|
239
|
+
if (before.path !== after.path) {
|
|
240
|
+
violations.push({
|
|
241
|
+
kind: "operation.path-changed",
|
|
242
|
+
operation: before.name,
|
|
243
|
+
detail: `path moved ${before.path} \u2192 ${after.path}; released clients call the old one`
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
if (before.method !== after.method) {
|
|
247
|
+
violations.push({
|
|
248
|
+
kind: "operation.method-changed",
|
|
249
|
+
operation: before.name,
|
|
250
|
+
detail: `method changed ${before.method} \u2192 ${after.method}; released clients send the old one`
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
const requestCtx = { operation: before.name, side: "request", violations };
|
|
254
|
+
compareRequestSections(before.request, after.request, "request", requestCtx);
|
|
255
|
+
compareRequestSections(before.interceptor, after.interceptor, "interceptor", requestCtx);
|
|
256
|
+
compareSchema(
|
|
257
|
+
before.response,
|
|
258
|
+
after.response,
|
|
259
|
+
"response",
|
|
260
|
+
{ operation: before.name, side: "response", violations }
|
|
261
|
+
);
|
|
262
|
+
return violations;
|
|
263
|
+
}
|
|
264
|
+
function compareDocuments(before, after) {
|
|
265
|
+
const current = new Map(after.operations.map((operation) => [operation.name, operation]));
|
|
266
|
+
const violations = [];
|
|
267
|
+
const removedOperations = [];
|
|
268
|
+
for (const operation of before.operations) {
|
|
269
|
+
const now = current.get(operation.name);
|
|
270
|
+
if (!now) {
|
|
271
|
+
removedOperations.push(operation.name);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
violations.push(...compareOperation(operation, now));
|
|
275
|
+
}
|
|
276
|
+
return { violations, removedOperations };
|
|
277
|
+
}
|
|
278
|
+
var ContractSnapshotError = class extends Error {
|
|
279
|
+
constructor(message) {
|
|
280
|
+
super(message);
|
|
281
|
+
this.name = "ContractSnapshotError";
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
var CURRENT_FILENAME = "current.json";
|
|
285
|
+
var RELEASED_DIRNAME = "released";
|
|
286
|
+
var USAGE_DIRNAME = "usage";
|
|
287
|
+
function currentPath(contractsDir) {
|
|
288
|
+
return join(contractsDir, CURRENT_FILENAME);
|
|
289
|
+
}
|
|
290
|
+
function releasedDir(contractsDir) {
|
|
291
|
+
return join(contractsDir, RELEASED_DIRNAME);
|
|
292
|
+
}
|
|
293
|
+
function usageDir(contractsDir) {
|
|
294
|
+
return join(contractsDir, USAGE_DIRNAME);
|
|
295
|
+
}
|
|
296
|
+
function compareVersions(a, b) {
|
|
297
|
+
const [aCore, aPre] = splitVersion(a);
|
|
298
|
+
const [bCore, bPre] = splitVersion(b);
|
|
299
|
+
for (let i = 0; i < 3; i++) {
|
|
300
|
+
if (aCore[i] !== bCore[i]) {
|
|
301
|
+
return aCore[i] - bCore[i];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (aPre === bPre) {
|
|
305
|
+
return 0;
|
|
306
|
+
}
|
|
307
|
+
if (aPre === void 0) {
|
|
308
|
+
return 1;
|
|
309
|
+
}
|
|
310
|
+
if (bPre === void 0) {
|
|
311
|
+
return -1;
|
|
312
|
+
}
|
|
313
|
+
return comparePreRelease(aPre, bPre);
|
|
314
|
+
}
|
|
315
|
+
function splitVersion(version) {
|
|
316
|
+
const [core, ...rest] = version.split("-");
|
|
317
|
+
const parts = core.split(".").map((part) => Number.parseInt(part, 10));
|
|
318
|
+
if (parts.length !== 3 || parts.some((part) => !Number.isInteger(part) || part < 0)) {
|
|
319
|
+
throw new ContractSnapshotError(`"${version}" is not a version of the form major.minor.patch`);
|
|
320
|
+
}
|
|
321
|
+
return [parts, rest.length > 0 ? rest.join("-") : void 0];
|
|
322
|
+
}
|
|
323
|
+
function comparePreRelease(a, b) {
|
|
324
|
+
const aParts = a.split(".");
|
|
325
|
+
const bParts = b.split(".");
|
|
326
|
+
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
|
327
|
+
const left = aParts[i];
|
|
328
|
+
const right = bParts[i];
|
|
329
|
+
if (left === void 0) {
|
|
330
|
+
return -1;
|
|
331
|
+
}
|
|
332
|
+
if (right === void 0) {
|
|
333
|
+
return 1;
|
|
334
|
+
}
|
|
335
|
+
if (left === right) {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const leftNumeric = /^\d+$/.test(left);
|
|
339
|
+
const rightNumeric = /^\d+$/.test(right);
|
|
340
|
+
if (leftNumeric && rightNumeric) {
|
|
341
|
+
return Number(left) - Number(right);
|
|
342
|
+
}
|
|
343
|
+
if (leftNumeric !== rightNumeric) {
|
|
344
|
+
return leftNumeric ? -1 : 1;
|
|
345
|
+
}
|
|
346
|
+
return left < right ? -1 : 1;
|
|
347
|
+
}
|
|
348
|
+
return 0;
|
|
349
|
+
}
|
|
350
|
+
function listSnapshots(contractsDir) {
|
|
351
|
+
const dir = releasedDir(contractsDir);
|
|
352
|
+
if (!existsSync(dir)) {
|
|
353
|
+
return [];
|
|
354
|
+
}
|
|
355
|
+
return readdirSync(dir).filter((name) => name.endsWith(".json")).map((name) => ({ version: name.slice(0, -".json".length), file: join(dir, name) })).sort((a, b) => compareVersions(a.version, b.version));
|
|
356
|
+
}
|
|
357
|
+
function newestSnapshot(contractsDir) {
|
|
358
|
+
const snapshots = listSnapshots(contractsDir);
|
|
359
|
+
return snapshots[snapshots.length - 1];
|
|
360
|
+
}
|
|
361
|
+
function readSnapshot(file) {
|
|
362
|
+
let parsed;
|
|
363
|
+
try {
|
|
364
|
+
parsed = JSON.parse(readFileSync(file, "utf-8"));
|
|
365
|
+
} catch (error) {
|
|
366
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
367
|
+
throw new ContractSnapshotError(`${file} could not be read: ${message}`);
|
|
368
|
+
}
|
|
369
|
+
if (typeof parsed.version !== "string" || typeof parsed.sha256 !== "string" || !parsed.document) {
|
|
370
|
+
throw new ContractSnapshotError(`${file} is not a contract snapshot: expected version, sha256 and document`);
|
|
371
|
+
}
|
|
372
|
+
const digest = stableDigest(parsed.document);
|
|
373
|
+
if (digest !== parsed.sha256) {
|
|
374
|
+
throw new ContractSnapshotError(
|
|
375
|
+
`${file} was edited after release: it records sha256 ${parsed.sha256} but its document hashes to ${digest}`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
return { version: parsed.version, sha256: parsed.sha256, document: parsed.document };
|
|
379
|
+
}
|
|
380
|
+
function writeSnapshot(contractsDir, version, document) {
|
|
381
|
+
compareVersions(version, version);
|
|
382
|
+
const dir = releasedDir(contractsDir);
|
|
383
|
+
const file = join(dir, `${version}.json`);
|
|
384
|
+
if (existsSync(file)) {
|
|
385
|
+
throw new ContractSnapshotError(
|
|
386
|
+
`${file} already exists. A released version's contract is never rewritten \u2014 cut a new version instead.`
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
const newest = newestSnapshot(contractsDir);
|
|
390
|
+
if (newest && compareVersions(version, newest.version) <= 0) {
|
|
391
|
+
throw new ContractSnapshotError(
|
|
392
|
+
`${version} is not newer than the released ${newest.version}. The gate compares against the newest snapshot, so filling one in behind it would never be checked.`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
mkdirSync(dir, { recursive: true });
|
|
396
|
+
const snapshot = { version, sha256: stableDigest(document), document };
|
|
397
|
+
writeFileSync(file, stableStringifyPretty(snapshot), "utf-8");
|
|
398
|
+
return file;
|
|
399
|
+
}
|
|
400
|
+
function readCurrentDocument(contractsDir) {
|
|
401
|
+
const file = currentPath(contractsDir);
|
|
402
|
+
try {
|
|
403
|
+
return JSON.parse(readFileSync(file, "utf-8"));
|
|
404
|
+
} catch (error) {
|
|
405
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
406
|
+
throw new ContractSnapshotError(
|
|
407
|
+
`${file} could not be read: ${message}. Run the @spfn/core:contract generator first.`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function writeCurrentDocument(contractsDir, document) {
|
|
412
|
+
const file = currentPath(contractsDir);
|
|
413
|
+
const content = stableStringifyPretty(document);
|
|
414
|
+
const existing = existsSync(file) ? readFileSync(file, "utf-8") : void 0;
|
|
415
|
+
if (existing === content) {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
mkdirSync(contractsDir, { recursive: true });
|
|
419
|
+
writeFileSync(file, content, "utf-8");
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
function parseRecord(file, raw) {
|
|
423
|
+
const parsed = JSON.parse(raw);
|
|
424
|
+
if (typeof parsed.platform !== "string" || parsed.platform.length === 0) {
|
|
425
|
+
throw new Error('"platform" must be a non-empty string');
|
|
426
|
+
}
|
|
427
|
+
if (typeof parsed.appVersion !== "string" || parsed.appVersion.length === 0) {
|
|
428
|
+
throw new Error('"appVersion" must be a non-empty string');
|
|
429
|
+
}
|
|
430
|
+
if (!Array.isArray(parsed.operations) || parsed.operations.some((name) => typeof name !== "string")) {
|
|
431
|
+
throw new Error('"operations" must be an array of operation names');
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
platform: parsed.platform,
|
|
435
|
+
appVersion: parsed.appVersion,
|
|
436
|
+
operations: parsed.operations,
|
|
437
|
+
file
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function readUsageRecords(usageDir2) {
|
|
441
|
+
let entries;
|
|
442
|
+
try {
|
|
443
|
+
if (!statSync(usageDir2).isDirectory()) {
|
|
444
|
+
return { decidable: false, reason: `${usageDir2} is not a directory` };
|
|
445
|
+
}
|
|
446
|
+
entries = readdirSync(usageDir2).filter((name) => name.endsWith(".json")).sort();
|
|
447
|
+
} catch {
|
|
448
|
+
return { decidable: false, reason: `${usageDir2} does not exist` };
|
|
449
|
+
}
|
|
450
|
+
if (entries.length === 0) {
|
|
451
|
+
return { decidable: false, reason: `${usageDir2} holds no usage file` };
|
|
452
|
+
}
|
|
453
|
+
const records = [];
|
|
454
|
+
for (const entry of entries) {
|
|
455
|
+
const file = join(usageDir2, entry);
|
|
456
|
+
try {
|
|
457
|
+
records.push(parseRecord(file, readFileSync(file, "utf-8")));
|
|
458
|
+
} catch (error) {
|
|
459
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
460
|
+
return { decidable: false, reason: `${file} could not be read: ${message}` };
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return { decidable: true, records };
|
|
464
|
+
}
|
|
465
|
+
function callersOf(operation, records) {
|
|
466
|
+
return records.filter((record) => record.operations.includes(operation));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// src/contract/check.ts
|
|
470
|
+
function checkContract(contractsDir, current) {
|
|
471
|
+
const baseline = newestSnapshot(contractsDir);
|
|
472
|
+
if (!baseline) {
|
|
473
|
+
return {
|
|
474
|
+
violations: [],
|
|
475
|
+
warnings: [
|
|
476
|
+
"No released contract snapshot found, so nothing was compared. This is expected for a first contract, and a mistake if a release forgot to write one."
|
|
477
|
+
]
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
let previous;
|
|
481
|
+
try {
|
|
482
|
+
previous = readSnapshot(baseline.file).document;
|
|
483
|
+
} catch (error) {
|
|
484
|
+
return {
|
|
485
|
+
baselineVersion: baseline.version,
|
|
486
|
+
warnings: [],
|
|
487
|
+
violations: [{
|
|
488
|
+
kind: "snapshot.digest-mismatch",
|
|
489
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
490
|
+
}]
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
const { violations, removedOperations } = compareDocuments(previous, current);
|
|
494
|
+
return {
|
|
495
|
+
baselineVersion: baseline.version,
|
|
496
|
+
warnings: [],
|
|
497
|
+
violations: [...violations, ...judgeRemovals(contractsDir, removedOperations)]
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function judgeRemovals(contractsDir, removedOperations) {
|
|
501
|
+
if (removedOperations.length === 0) {
|
|
502
|
+
return [];
|
|
503
|
+
}
|
|
504
|
+
const usage = readUsageRecords(usageDir(contractsDir));
|
|
505
|
+
if (!usage.decidable) {
|
|
506
|
+
return [{
|
|
507
|
+
kind: "usage.undecidable",
|
|
508
|
+
detail: `${removedOperations.join(", ")} would be removed, but no released client's call list could be read (${usage.reason}). Not knowing who calls an operation is not the same as knowing nobody does.`
|
|
509
|
+
}];
|
|
510
|
+
}
|
|
511
|
+
const violations = [];
|
|
512
|
+
for (const operation of removedOperations) {
|
|
513
|
+
const callers = callersOf(operation, usage.records);
|
|
514
|
+
if (callers.length === 0) {
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
violations.push({
|
|
518
|
+
kind: "usage.still-called",
|
|
519
|
+
operation,
|
|
520
|
+
detail: "still called by " + callers.map((caller) => `${caller.platform} ${caller.appVersion}`).join(", ")
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
return violations;
|
|
524
|
+
}
|
|
525
|
+
function formatViolations(violations) {
|
|
526
|
+
return violations.map((violation) => {
|
|
527
|
+
const where = [violation.operation, violation.location].filter(Boolean).join(" ");
|
|
528
|
+
return ` - [${violation.kind}]${where ? ` ${where}` : ""}: ${violation.detail}`;
|
|
529
|
+
}).join("\n");
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export { CURRENT_FILENAME, ContractCollectionError, ContractSnapshotError, RELEASED_DIRNAME, USAGE_DIRNAME, callersOf, canonicalize, checkContract, collectContractDocument, compareDocuments, compareOperation, compareVersions, currentPath, formatViolations, listSnapshots, newestSnapshot, readCurrentDocument, readSnapshot, readUsageRecords, releasedDir, stableDigest, stableStringify, stableStringifyPretty, usageDir, writeCurrentDocument, writeSnapshot };
|
|
533
|
+
//# sourceMappingURL=index.js.map
|
|
534
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/contract/collect.ts","../../src/contract/stable-json.ts","../../src/contract/compare.ts","../../src/contract/snapshot.ts","../../src/contract/usage.ts","../../src/contract/check.ts"],"names":["REQUEST_SECTIONS","usageDir","readdirSync","join","readFileSync"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Contract Collection\n *\n * Walks a loaded router and turns every route carrying `.contract()` into a\n * contract operation.\n *\n * The router is loaded and walked rather than parsed from source. Real routes\n * build their schemas from imported values — `EmailSchema`, `FileSchema()`,\n * `KEY_DEVICE_NAME_MAX_LENGTH` — which a source parser cannot resolve. Loading\n * costs a module import and no infrastructure: route modules have no\n * import-time side effects, so nothing here opens a database connection.\n */\n\nimport type { RouteDef } from '../route/route-builder';\nimport type { RouteInput } from '../route/route-input';\nimport type { Router } from '../route/router';\nimport type { ContractDocument, ContractOperation, ContractRequest, JsonSchema } from './types';\n\n/** Thrown when the router cannot produce a contract at all. */\nexport class ContractCollectionError extends Error\n{\n constructor(message: string)\n {\n super(message);\n this.name = 'ContractCollectionError';\n }\n}\n\nfunction isRouter(value: unknown): value is Router<any>\n{\n return value !== null\n && typeof value === 'object'\n && 'routes' in value\n && '_routes' in value;\n}\n\nfunction isRouteDef(value: unknown): value is RouteDef<any>\n{\n return value !== null\n && typeof value === 'object'\n && 'handler' in value;\n}\n\n/**\n * Strip TypeBox's symbol-keyed metadata and hand back plain JSON.\n *\n * The symbols carry no information the wire shape depends on, and they cannot\n * survive the round trip to a committed file.\n */\nfunction toJsonSchema(schema: unknown): JsonSchema\n{\n return JSON.parse(JSON.stringify(schema)) as JsonSchema;\n}\n\nconst REQUEST_SECTIONS = ['params', 'query', 'body', 'formData', 'headers', 'cookies'] as const;\n\nfunction toContractRequest(input: RouteInput | undefined): ContractRequest\n{\n const request: ContractRequest = {};\n\n if (!input)\n {\n return request;\n }\n\n for (const section of REQUEST_SECTIONS)\n {\n const schema = input[section];\n if (schema)\n {\n request[section] = toJsonSchema(schema);\n }\n }\n\n return request;\n}\n\ninterface Found\n{\n operation: ContractOperation;\n\n /** Where the route was found, for a collision message. */\n trail: string;\n}\n\nfunction visitRouter(router: Router<any>, trail: string[], found: Map<string, Found>): void\n{\n for (const [name, entry] of Object.entries(router.routes))\n {\n if (isRouter(entry))\n {\n visitRouter(entry, [...trail, name], found);\n continue;\n }\n\n if (!isRouteDef(entry))\n {\n continue;\n }\n\n const routeDef = entry as RouteDef<any>;\n if (!routeDef.contract)\n {\n continue;\n }\n\n addOperation(name, routeDef, [...trail, name], found);\n }\n\n for (const packageRouter of router._packageRouters ?? [])\n {\n visitRouter(packageRouter, [...trail, '(package)'], found);\n }\n}\n\nfunction addOperation(name: string, routeDef: RouteDef<any>, trail: string[], found: Map<string, Found>): void\n{\n const where = trail.join('.');\n const contract = routeDef.contract!;\n\n if (!routeDef.method || !routeDef.path)\n {\n throw new ContractCollectionError(\n `Contracted route \"${where}\" has no method or path. `\n + 'A contract describes an operation on the wire, so both are required.',\n );\n }\n\n if (!contract.since)\n {\n throw new ContractCollectionError(\n `Contracted route \"${where}\" has no \"since\" version. `\n + 'The version an operation first appeared in is part of the promise.',\n );\n }\n\n if (!contract.response)\n {\n throw new ContractCollectionError(\n `Contracted route \"${where}\" declares no response schema. `\n + 'An operation with no body declares Type.Null().',\n );\n }\n\n const existing = found.get(name);\n if (existing)\n {\n throw new ContractCollectionError(\n `Two contracted routes are both named \"${name}\" (${existing.trail} and ${where}). `\n + 'An operation is identified by its name across versions, so names must be unique.',\n );\n }\n\n found.set(name, {\n trail: where,\n operation: {\n name,\n method: routeDef.method,\n path: routeDef.path,\n since: contract.since,\n auth: contract.auth ?? 'none',\n requiresSession: contract.requiresSession ?? false,\n ...(contract.deprecatedIn ? { deprecatedIn: contract.deprecatedIn } : {}),\n request: toContractRequest(routeDef.input),\n interceptor: toContractRequest(routeDef.interceptor),\n response: toJsonSchema(contract.response),\n },\n });\n}\n\n/**\n * Build the contract document from a loaded router.\n *\n * Routes without `.contract()` are skipped — they are not part of the promise.\n */\nexport function collectContractDocument(router: Router<any>): ContractDocument\n{\n const found = new Map<string, Found>();\n visitRouter(router, ['router'], found);\n\n const operations = [...found.values()]\n .map(entry => entry.operation)\n .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));\n\n return { documentVersion: 1, operations };\n}\n","/**\n * Deterministic JSON\n *\n * The generated contract is committed and compared against what the generator\n * produces. If key order moved between runs, \"the committed file differs from\n * the generated one\" would flicker and the check would train people to ignore\n * it. So every emitted document is written with its object keys sorted, and the\n * digest that pins a released snapshot is taken over the same ordering with no\n * whitespace — formatting choices never move a digest.\n */\n\nimport { createHash } from 'node:crypto';\n\n/** Recursively sort object keys and drop `undefined` members. */\nexport function canonicalize(value: unknown): unknown\n{\n if (Array.isArray(value))\n {\n return value.map(canonicalize);\n }\n\n if (value === null || typeof value !== 'object')\n {\n return value;\n }\n\n const source = value as Record<string, unknown>;\n const sorted: Record<string, unknown> = {};\n\n for (const key of Object.keys(source).sort())\n {\n if (source[key] === undefined)\n {\n continue;\n }\n\n sorted[key] = canonicalize(source[key]);\n }\n\n return sorted;\n}\n\n/** Compact canonical encoding — what a digest is taken over. */\nexport function stableStringify(value: unknown): string\n{\n return JSON.stringify(canonicalize(value));\n}\n\n/** Indented canonical encoding — what gets written to disk and reviewed. */\nexport function stableStringifyPretty(value: unknown): string\n{\n return `${JSON.stringify(canonicalize(value), null, 4)}\\n`;\n}\n\n/** SHA-256 of the compact canonical encoding, lowercase hex. */\nexport function stableDigest(value: unknown): string\n{\n return createHash('sha256').update(stableStringify(value), 'utf8').digest('hex');\n}\n","/**\n * Backward-compatibility comparison\n *\n * Compares the contract a build produces against the newest released snapshot.\n *\n * The rule that shapes everything here: **optional runs in opposite directions\n * on the two sides.** A request is safe when the server grows more tolerant —\n * a new optional field, a dropped field, a requirement relaxed. A response is\n * safe when the server grows more certain — a new field, a value that used to\n * be optional now always present. Collapsing both into one rule would\n * necessarily get one of them backwards.\n *\n * | change | request | response |\n * |-------------------------|---------|----------|\n * | field added (optional) | pass | pass |\n * | field added (required) | refuse | pass |\n * | field removed | pass | refuse |\n * | required → optional | pass | refuse |\n * | optional → required | refuse | pass |\n * | type changed | refuse | refuse |\n */\n\nimport { stableStringify } from './stable-json';\nimport type {\n ContractDocument,\n ContractOperation,\n ContractRequest,\n ContractViolation,\n JsonSchema,\n} from './types';\n\n/** Which side of the wire a schema sits on. The direction optional runs. */\ntype Side = 'request' | 'response';\n\nexport interface DocumentComparison\n{\n violations: ContractViolation[];\n\n /**\n * Operations present in the baseline and gone from the current contract.\n *\n * Removal is not decided here: whether an operation may go depends on\n * whether any released app still calls it, which is a separate check.\n */\n removedOperations: string[];\n}\n\nconst REQUEST_SECTIONS = ['params', 'query', 'body', 'formData', 'headers', 'cookies'] as const;\n\n/**\n * The schema's own keywords, with the structural ones removed.\n *\n * Everything left — `type`, `format`, `enum`, `anyOf`, `minLength` — is\n * compared verbatim, so a narrowed constraint counts as a type change. That\n * refuses more than the table's \"type changed\" row strictly requires, and it\n * refuses in the recoverable direction: a build that stops is fixed by cutting\n * a new contract version, while a break that passes reaches a shipped app.\n */\nfunction leafSignature(schema: JsonSchema): string\n{\n const { properties, required, items, ...rest } = schema;\n\n return stableStringify(rest);\n}\n\nfunction propertiesOf(schema: JsonSchema): Record<string, JsonSchema>\n{\n const properties = schema.properties;\n\n if (!properties || typeof properties !== 'object')\n {\n return {};\n }\n\n return properties as Record<string, JsonSchema>;\n}\n\nfunction requiredOf(schema: JsonSchema): Set<string>\n{\n const required = schema.required;\n\n if (!Array.isArray(required))\n {\n return new Set();\n }\n\n return new Set(required.filter((name): name is string => typeof name === 'string'));\n}\n\n/**\n * Would a client that sends nothing here now be refused?\n *\n * Only this level counts. A required field nested inside an optional object is\n * required *if* that object is sent, and a client that never sends it is fine.\n */\nfunction requiresClientToSend(schema: JsonSchema): boolean\n{\n if (schema.type !== 'object')\n {\n return true;\n }\n\n return requiredOf(schema).size > 0;\n}\n\ninterface CompareContext\n{\n operation: string;\n side: Side;\n violations: ContractViolation[];\n}\n\nfunction report(\n ctx: CompareContext,\n kind: ContractViolation['kind'],\n location: string,\n detail: string,\n): void\n{\n ctx.violations.push({ kind, operation: ctx.operation, location, detail });\n}\n\n/**\n * Compare one schema position across versions.\n *\n * `before`/`after` may be absent: a whole request section can appear or vanish,\n * and so can a nested field.\n */\nfunction compareSchema(\n before: JsonSchema | undefined,\n after: JsonSchema | undefined,\n location: string,\n ctx: CompareContext,\n): void\n{\n if (!before && !after)\n {\n return;\n }\n\n if (before && !after)\n {\n if (ctx.side === 'response')\n {\n report(ctx, 'response.field-removed', location, 'the response no longer carries this, and old clients read it');\n }\n\n // A request the server stopped reading is a request old clients may still send.\n return;\n }\n\n if (!before && after)\n {\n if (ctx.side === 'request' && requiresClientToSend(after))\n {\n report(\n ctx,\n 'request.required-field-added',\n location,\n 'this is new and mandatory, so every already-released client is refused',\n );\n }\n\n // A response that carries more is a response old clients ignore the rest of.\n return;\n }\n\n const from = before!;\n const to = after!;\n\n if (leafSignature(from) !== leafSignature(to))\n {\n report(\n ctx,\n ctx.side === 'request' ? 'request.type-changed' : 'response.type-changed',\n location,\n `the declared type changed: ${leafSignature(from)} → ${leafSignature(to)}`,\n );\n\n return;\n }\n\n compareProperties(from, to, location, ctx);\n compareSchema(\n from.items as JsonSchema | undefined,\n to.items as JsonSchema | undefined,\n `${location}[]`,\n ctx,\n );\n}\n\nfunction compareProperties(from: JsonSchema, to: JsonSchema, location: string, ctx: CompareContext): void\n{\n const beforeProperties = propertiesOf(from);\n const afterProperties = propertiesOf(to);\n const beforeRequired = requiredOf(from);\n const afterRequired = requiredOf(to);\n\n for (const [name, beforeSchema] of Object.entries(beforeProperties))\n {\n const where = `${location}.${name}`;\n const afterSchema = afterProperties[name];\n\n compareSchema(beforeSchema, afterSchema, where, ctx);\n\n if (!afterSchema)\n {\n continue;\n }\n\n const wasRequired = beforeRequired.has(name);\n const isRequired = afterRequired.has(name);\n\n if (ctx.side === 'request' && !wasRequired && isRequired)\n {\n report(ctx, 'request.field-became-required', where, 'clients that never sent this are now refused');\n }\n\n if (ctx.side === 'response' && wasRequired && !isRequired)\n {\n report(ctx, 'response.field-became-optional', where, 'clients that counted on this always arriving now break');\n }\n }\n\n // An added field is judged at its own level only. Optional means an old\n // client that omits it still passes, whatever the field contains.\n for (const name of Object.keys(afterProperties))\n {\n if (beforeProperties[name])\n {\n continue;\n }\n\n if (ctx.side === 'request' && afterRequired.has(name))\n {\n report(\n ctx,\n 'request.required-field-added',\n `${location}.${name}`,\n 'this is new and mandatory, so every already-released client is refused',\n );\n }\n }\n}\n\nfunction compareRequestSections(\n before: ContractRequest,\n after: ContractRequest,\n prefix: string,\n ctx: CompareContext,\n): void\n{\n for (const section of REQUEST_SECTIONS)\n {\n compareSchema(before[section], after[section], `${prefix}.${section}`, ctx);\n }\n}\n\n/** Compare one operation that exists on both sides. */\nexport function compareOperation(before: ContractOperation, after: ContractOperation): ContractViolation[]\n{\n const violations: ContractViolation[] = [];\n\n if (before.path !== after.path)\n {\n violations.push({\n kind: 'operation.path-changed',\n operation: before.name,\n detail: `path moved ${before.path} → ${after.path}; released clients call the old one`,\n });\n }\n\n if (before.method !== after.method)\n {\n violations.push({\n kind: 'operation.method-changed',\n operation: before.name,\n detail: `method changed ${before.method} → ${after.method}; released clients send the old one`,\n });\n }\n\n const requestCtx: CompareContext = { operation: before.name, side: 'request', violations };\n compareRequestSections(before.request, after.request, 'request', requestCtx);\n compareRequestSections(before.interceptor, after.interceptor, 'interceptor', requestCtx);\n\n compareSchema(\n before.response,\n after.response,\n 'response',\n { operation: before.name, side: 'response', violations },\n );\n\n return violations;\n}\n\n/**\n * Compare a released contract against the one this build produced.\n *\n * Operations that are new in `after` need no check — nothing has been promised\n * about them yet.\n */\nexport function compareDocuments(before: ContractDocument, after: ContractDocument): DocumentComparison\n{\n const current = new Map(after.operations.map(operation => [operation.name, operation]));\n const violations: ContractViolation[] = [];\n const removedOperations: string[] = [];\n\n for (const operation of before.operations)\n {\n const now = current.get(operation.name);\n\n if (!now)\n {\n removedOperations.push(operation.name);\n continue;\n }\n\n violations.push(...compareOperation(operation, now));\n }\n\n return { violations, removedOperations };\n}\n","/**\n * Released snapshots\n *\n * `contracts/released/<version>.json` is what a version actually promised. It is\n * written once, at release, and never touched again — the gate compares against\n * the newest one.\n *\n * Comparing against the newest one alone is only sound if no release is missing\n * a snapshot: compatibility is transitive through the chain, and a gap in the\n * chain silently widens what passes. That is why cutting a release writes a\n * snapshot rather than offering to.\n *\n * Each snapshot carries the SHA-256 of its own document, so a hand-edited\n * snapshot fails the gate instead of quietly moving the baseline.\n */\n\nimport { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { stableDigest, stableStringifyPretty } from './stable-json';\nimport type { ContractDocument, ContractSnapshot } from './types';\n\nexport class ContractSnapshotError extends Error\n{\n constructor(message: string)\n {\n super(message);\n this.name = 'ContractSnapshotError';\n }\n}\n\nexport const CURRENT_FILENAME = 'current.json';\nexport const RELEASED_DIRNAME = 'released';\nexport const USAGE_DIRNAME = 'usage';\n\nexport function currentPath(contractsDir: string): string\n{\n return join(contractsDir, CURRENT_FILENAME);\n}\n\nexport function releasedDir(contractsDir: string): string\n{\n return join(contractsDir, RELEASED_DIRNAME);\n}\n\nexport function usageDir(contractsDir: string): string\n{\n return join(contractsDir, USAGE_DIRNAME);\n}\n\n/**\n * Order two versions the way a release line runs.\n *\n * Numeric identifiers compare numerically (so 1.10.0 follows 1.9.0), and a\n * pre-release sorts before the release it leads to.\n */\nexport function compareVersions(a: string, b: string): number\n{\n const [aCore, aPre] = splitVersion(a);\n const [bCore, bPre] = splitVersion(b);\n\n for (let i = 0; i < 3; i++)\n {\n if (aCore[i] !== bCore[i])\n {\n return aCore[i] - bCore[i];\n }\n }\n\n if (aPre === bPre)\n {\n return 0;\n }\n\n if (aPre === undefined)\n {\n return 1;\n }\n\n if (bPre === undefined)\n {\n return -1;\n }\n\n return comparePreRelease(aPre, bPre);\n}\n\nfunction splitVersion(version: string): [number[], string | undefined]\n{\n const [core, ...rest] = version.split('-');\n const parts = core.split('.').map(part => Number.parseInt(part, 10));\n\n if (parts.length !== 3 || parts.some(part => !Number.isInteger(part) || part < 0))\n {\n throw new ContractSnapshotError(`\"${version}\" is not a version of the form major.minor.patch`);\n }\n\n return [parts, rest.length > 0 ? rest.join('-') : undefined];\n}\n\nfunction comparePreRelease(a: string, b: string): number\n{\n const aParts = a.split('.');\n const bParts = b.split('.');\n\n for (let i = 0; i < Math.max(aParts.length, bParts.length); i++)\n {\n const left = aParts[i];\n const right = bParts[i];\n\n if (left === undefined)\n {\n return -1;\n }\n\n if (right === undefined)\n {\n return 1;\n }\n\n if (left === right)\n {\n continue;\n }\n\n const leftNumeric = /^\\d+$/.test(left);\n const rightNumeric = /^\\d+$/.test(right);\n\n if (leftNumeric && rightNumeric)\n {\n return Number(left) - Number(right);\n }\n\n if (leftNumeric !== rightNumeric)\n {\n return leftNumeric ? -1 : 1;\n }\n\n return left < right ? -1 : 1;\n }\n\n return 0;\n}\n\nexport interface SnapshotFile\n{\n version: string;\n file: string;\n}\n\n/** Every released snapshot, oldest first. */\nexport function listSnapshots(contractsDir: string): SnapshotFile[]\n{\n const dir = releasedDir(contractsDir);\n\n if (!existsSync(dir))\n {\n return [];\n }\n\n return readdirSync(dir)\n .filter(name => name.endsWith('.json'))\n .map(name => ({ version: name.slice(0, -'.json'.length), file: join(dir, name) }))\n .sort((a, b) => compareVersions(a.version, b.version));\n}\n\n/** The snapshot the gate compares against, or undefined when nothing is released yet. */\nexport function newestSnapshot(contractsDir: string): SnapshotFile | undefined\n{\n const snapshots = listSnapshots(contractsDir);\n\n return snapshots[snapshots.length - 1];\n}\n\n/**\n * Read a snapshot and check its digest.\n *\n * A mismatch means the file was edited after release. The baseline is the whole\n * point of the gate, so an edited baseline is refused rather than trusted.\n */\nexport function readSnapshot(file: string): ContractSnapshot\n{\n let parsed: Partial<ContractSnapshot>;\n\n try\n {\n parsed = JSON.parse(readFileSync(file, 'utf-8')) as Partial<ContractSnapshot>;\n }\n catch (error)\n {\n const message = error instanceof Error ? error.message : String(error);\n\n throw new ContractSnapshotError(`${file} could not be read: ${message}`);\n }\n\n if (typeof parsed.version !== 'string' || typeof parsed.sha256 !== 'string' || !parsed.document)\n {\n throw new ContractSnapshotError(`${file} is not a contract snapshot: expected version, sha256 and document`);\n }\n\n const digest = stableDigest(parsed.document);\n\n if (digest !== parsed.sha256)\n {\n throw new ContractSnapshotError(\n `${file} was edited after release: it records sha256 ${parsed.sha256} but its document hashes to ${digest}`,\n );\n }\n\n return { version: parsed.version, sha256: parsed.sha256, document: parsed.document as ContractDocument };\n}\n\n/**\n * Write the snapshot for a release.\n *\n * Refuses to overwrite: a published version's promise does not change, a\n * mistake becomes a new version.\n */\nexport function writeSnapshot(contractsDir: string, version: string, document: ContractDocument): string\n{\n compareVersions(version, version);\n\n const dir = releasedDir(contractsDir);\n const file = join(dir, `${version}.json`);\n\n if (existsSync(file))\n {\n throw new ContractSnapshotError(\n `${file} already exists. A released version's contract is never rewritten — cut a new version instead.`,\n );\n }\n\n // Releases have to reach the newest snapshot in order. The gate compares\n // against the newest one alone, which is only sound while the chain has no\n // gaps — a snapshot filled in behind the newest one is never compared to\n // anything, and quietly widens what passes.\n const newest = newestSnapshot(contractsDir);\n\n if (newest && compareVersions(version, newest.version) <= 0)\n {\n throw new ContractSnapshotError(\n `${version} is not newer than the released ${newest.version}. `\n + 'The gate compares against the newest snapshot, so filling one in behind it would never be checked.',\n );\n }\n\n mkdirSync(dir, { recursive: true });\n\n const snapshot: ContractSnapshot = { version, sha256: stableDigest(document), document };\n writeFileSync(file, stableStringifyPretty(snapshot), 'utf-8');\n\n return file;\n}\n\n/** Read `contracts/current.json`. */\nexport function readCurrentDocument(contractsDir: string): ContractDocument\n{\n const file = currentPath(contractsDir);\n\n try\n {\n return JSON.parse(readFileSync(file, 'utf-8')) as ContractDocument;\n }\n catch (error)\n {\n const message = error instanceof Error ? error.message : String(error);\n\n throw new ContractSnapshotError(\n `${file} could not be read: ${message}. Run the @spfn/core:contract generator first.`,\n );\n }\n}\n\n/** Write `contracts/current.json`. Returns true when the file changed. */\nexport function writeCurrentDocument(contractsDir: string, document: ContractDocument): boolean\n{\n const file = currentPath(contractsDir);\n const content = stableStringifyPretty(document);\n const existing = existsSync(file) ? readFileSync(file, 'utf-8') : undefined;\n\n if (existing === content)\n {\n return false;\n }\n\n mkdirSync(contractsDir, { recursive: true });\n writeFileSync(file, content, 'utf-8');\n\n return true;\n}\n","/**\n * Usage files — who still calls an operation\n *\n * A released app is compiled and shipped; the server cannot ask it what it\n * calls. So each released client writes down the operations it uses, and those\n * files are what a removal is judged against:\n * `contracts/usage/<platform>-<appVersion>.json`.\n *\n * The one rule this file exists to hold: **an unreadable file and \"nobody calls\n * it\" are not the same answer.** An empty scan result reading as a pass is how\n * a removal check quietly stops checking anything. Every failure to read is a\n * refusal that names the file.\n */\n\nimport { readdirSync, readFileSync, statSync } from 'node:fs';\nimport { join } from 'node:path';\n\n/** One released client's declared call list. */\nexport interface UsageRecord\n{\n platform: string;\n appVersion: string;\n operations: string[];\n\n /** File this came from, for messages. */\n file: string;\n}\n\nexport type UsageReadResult =\n | { decidable: true; records: UsageRecord[] }\n | { decidable: false; reason: string };\n\nfunction parseRecord(file: string, raw: string): UsageRecord\n{\n const parsed = JSON.parse(raw) as Partial<UsageRecord>;\n\n if (typeof parsed.platform !== 'string' || parsed.platform.length === 0)\n {\n throw new Error('\"platform\" must be a non-empty string');\n }\n\n if (typeof parsed.appVersion !== 'string' || parsed.appVersion.length === 0)\n {\n throw new Error('\"appVersion\" must be a non-empty string');\n }\n\n if (!Array.isArray(parsed.operations) || parsed.operations.some(name => typeof name !== 'string'))\n {\n throw new Error('\"operations\" must be an array of operation names');\n }\n\n return {\n platform: parsed.platform,\n appVersion: parsed.appVersion,\n operations: parsed.operations,\n file,\n };\n}\n\n/**\n * Read every usage file under `usageDir`.\n *\n * Returns undecidable — never an empty pass — when the directory is missing,\n * holds no usage file, or holds one that cannot be read.\n */\nexport function readUsageRecords(usageDir: string): UsageReadResult\n{\n let entries: string[];\n\n try\n {\n if (!statSync(usageDir).isDirectory())\n {\n return { decidable: false, reason: `${usageDir} is not a directory` };\n }\n\n entries = readdirSync(usageDir).filter(name => name.endsWith('.json')).sort();\n }\n catch\n {\n return { decidable: false, reason: `${usageDir} does not exist` };\n }\n\n if (entries.length === 0)\n {\n return { decidable: false, reason: `${usageDir} holds no usage file` };\n }\n\n const records: UsageRecord[] = [];\n\n for (const entry of entries)\n {\n const file = join(usageDir, entry);\n\n try\n {\n records.push(parseRecord(file, readFileSync(file, 'utf-8')));\n }\n catch (error)\n {\n const message = error instanceof Error ? error.message : String(error);\n\n return { decidable: false, reason: `${file} could not be read: ${message}` };\n }\n }\n\n return { decidable: true, records };\n}\n\n/** Which released clients still call `operation`. */\nexport function callersOf(operation: string, records: UsageRecord[]): UsageRecord[]\n{\n return records.filter(record => record.operations.includes(operation));\n}\n","/**\n * The gate\n *\n * Puts the pieces together: read the newest released snapshot, compare this\n * build's contract against it, and decide removals against what released\n * clients still call.\n *\n * Nothing released yet is a pass — with a warning. \"This is the first contract\"\n * and \"the release that should have written a snapshot didn't\" produce the same\n * empty directory, and only a person can tell them apart.\n */\n\nimport { compareDocuments } from './compare';\nimport { newestSnapshot, readSnapshot, usageDir } from './snapshot';\nimport { callersOf, readUsageRecords } from './usage';\nimport type { ContractDocument, ContractViolation } from './types';\n\nexport interface ContractCheckResult\n{\n /** Version compared against, absent when nothing is released yet. */\n baselineVersion?: string;\n\n violations: ContractViolation[];\n\n /** Things a person should look at that do not stop the build. */\n warnings: string[];\n}\n\nexport function checkContract(contractsDir: string, current: ContractDocument): ContractCheckResult\n{\n const baseline = newestSnapshot(contractsDir);\n\n if (!baseline)\n {\n return {\n violations: [],\n warnings: [\n 'No released contract snapshot found, so nothing was compared. '\n + 'This is expected for a first contract, and a mistake if a release forgot to write one.',\n ],\n };\n }\n\n let previous: ContractDocument;\n\n try\n {\n previous = readSnapshot(baseline.file).document;\n }\n catch (error)\n {\n return {\n baselineVersion: baseline.version,\n warnings: [],\n violations: [{\n kind: 'snapshot.digest-mismatch',\n detail: error instanceof Error ? error.message : String(error),\n }],\n };\n }\n\n const { violations, removedOperations } = compareDocuments(previous, current);\n\n return {\n baselineVersion: baseline.version,\n warnings: [],\n violations: [...violations, ...judgeRemovals(contractsDir, removedOperations)],\n };\n}\n\n/**\n * Decide whether removed operations may go.\n *\n * Only reached when something was actually removed — an app that removes\n * nothing never needs a usage file to exist.\n */\nfunction judgeRemovals(contractsDir: string, removedOperations: string[]): ContractViolation[]\n{\n if (removedOperations.length === 0)\n {\n return [];\n }\n\n const usage = readUsageRecords(usageDir(contractsDir));\n\n if (!usage.decidable)\n {\n return [{\n kind: 'usage.undecidable',\n detail:\n `${removedOperations.join(', ')} would be removed, but no released client's call list could be read `\n + `(${usage.reason}). Not knowing who calls an operation is not the same as knowing nobody does.`,\n }];\n }\n\n const violations: ContractViolation[] = [];\n\n for (const operation of removedOperations)\n {\n const callers = callersOf(operation, usage.records);\n\n if (callers.length === 0)\n {\n continue;\n }\n\n violations.push({\n kind: 'usage.still-called',\n operation,\n detail:\n 'still called by '\n + callers.map(caller => `${caller.platform} ${caller.appVersion}`).join(', '),\n });\n }\n\n return violations;\n}\n\n/** Render violations as the message a failing build prints. */\nexport function formatViolations(violations: ContractViolation[]): string\n{\n return violations\n .map((violation) =>\n {\n const where = [violation.operation, violation.location].filter(Boolean).join(' ');\n\n return ` - [${violation.kind}]${where ? ` ${where}` : ''}: ${violation.detail}`;\n })\n .join('\\n');\n}\n"]}
|