@spfn/core 0.2.0-beta.8 → 0.3.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +444 -305
  3. package/dist/authz/index.d.ts +34 -0
  4. package/dist/authz/index.js +810 -0
  5. package/dist/authz/index.js.map +1 -0
  6. package/dist/{boss-DI1r4kTS.d.ts → boss-D16fO2oG.d.ts} +41 -1
  7. package/dist/cache/index.js +42 -30
  8. package/dist/cache/index.js.map +1 -1
  9. package/dist/codegen/index.d.ts +121 -13
  10. package/dist/codegen/index.js +212 -15
  11. package/dist/codegen/index.js.map +1 -1
  12. package/dist/config/index.d.ts +615 -6
  13. package/dist/config/index.js +124 -5
  14. package/dist/config/index.js.map +1 -1
  15. package/dist/contract/index.d.ts +220 -0
  16. package/dist/contract/index.js +558 -0
  17. package/dist/contract/index.js.map +1 -0
  18. package/dist/db/index.d.ts +528 -85
  19. package/dist/db/index.js +831 -122
  20. package/dist/db/index.js.map +1 -1
  21. package/dist/define-middleware-DfDP39Nq.d.ts +167 -0
  22. package/dist/env/index.d.ts +26 -2
  23. package/dist/env/index.js +15 -5
  24. package/dist/env/index.js.map +1 -1
  25. package/dist/env/loader.d.ts +26 -19
  26. package/dist/env/loader.js +32 -25
  27. package/dist/env/loader.js.map +1 -1
  28. package/dist/errors/index.d.ts +10 -0
  29. package/dist/errors/index.js +418 -5
  30. package/dist/errors/index.js.map +1 -1
  31. package/dist/event/index.d.ts +33 -3
  32. package/dist/event/index.js +24 -3
  33. package/dist/event/index.js.map +1 -1
  34. package/dist/event/sse/client.d.ts +42 -3
  35. package/dist/event/sse/client.js +128 -45
  36. package/dist/event/sse/client.js.map +1 -1
  37. package/dist/event/sse/index.d.ts +12 -5
  38. package/dist/event/sse/index.js +280 -32
  39. package/dist/event/sse/index.js.map +1 -1
  40. package/dist/event/ws/client.d.ts +59 -0
  41. package/dist/event/ws/client.js +273 -0
  42. package/dist/event/ws/client.js.map +1 -0
  43. package/dist/event/ws/index.d.ts +94 -0
  44. package/dist/event/ws/index.js +272 -0
  45. package/dist/event/ws/index.js.map +1 -0
  46. package/dist/job/index.d.ts +2 -2
  47. package/dist/job/index.js +155 -42
  48. package/dist/job/index.js.map +1 -1
  49. package/dist/logger/index.d.ts +5 -0
  50. package/dist/logger/index.js +14 -0
  51. package/dist/logger/index.js.map +1 -1
  52. package/dist/middleware/index.d.ts +347 -9
  53. package/dist/middleware/index.js +1462 -15
  54. package/dist/middleware/index.js.map +1 -1
  55. package/dist/nextjs/index.d.ts +2 -2
  56. package/dist/nextjs/index.js +42 -28
  57. package/dist/nextjs/index.js.map +1 -1
  58. package/dist/nextjs/server.d.ts +35 -51
  59. package/dist/nextjs/server.js +126 -60
  60. package/dist/nextjs/server.js.map +1 -1
  61. package/dist/ops/index.d.ts +107 -0
  62. package/dist/ops/index.js +476 -0
  63. package/dist/ops/index.js.map +1 -0
  64. package/dist/route/index.d.ts +8 -694
  65. package/dist/route/index.js +111 -22
  66. package/dist/route/index.js.map +1 -1
  67. package/dist/router-ukNdAZcN.d.ts +676 -0
  68. package/dist/security/index.d.ts +83 -0
  69. package/dist/security/index.js +173 -0
  70. package/dist/security/index.js.map +1 -0
  71. package/dist/server/index.d.ts +491 -22
  72. package/dist/server/index.js +1887 -308
  73. package/dist/server/index.js.map +1 -1
  74. package/dist/token-manager-BT5EnUAR.d.ts +278 -0
  75. package/dist/types-2AbaW4Ie.d.ts +205 -0
  76. package/dist/{types-BOPTApC2.d.ts → types-9oszaJqp.d.ts} +7 -2
  77. package/dist/types-Bvvig_tT.d.ts +115 -0
  78. package/dist/types-ZQODsBft.d.ts +282 -0
  79. package/package.json +244 -208
  80. package/dist/router-Di7ENoah.d.ts +0 -151
  81. package/dist/types-B-e_f2dQ.d.ts +0 -121
  82. package/docs/cache.md +0 -133
  83. package/docs/codegen.md +0 -74
  84. package/docs/database.md +0 -346
  85. package/docs/entity.md +0 -539
  86. package/docs/env.md +0 -477
  87. package/docs/errors.md +0 -319
  88. package/docs/event.md +0 -116
  89. package/docs/job.md +0 -131
  90. package/docs/logger.md +0 -108
  91. package/docs/middleware.md +0 -337
  92. package/docs/nextjs.md +0 -241
  93. package/docs/repository.md +0 -496
  94. package/docs/route.md +0 -497
  95. package/docs/server.md +0 -307
@@ -0,0 +1,558 @@
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", "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 assertNoFormData(where, routeDef) {
55
+ const input = routeDef.input;
56
+ const interceptor = routeDef.interceptor;
57
+ const section = input?.formData ? "input" : interceptor?.formData ? "interceptor" : void 0;
58
+ if (!section) {
59
+ return;
60
+ }
61
+ throw new ContractCollectionError(
62
+ `Contracted route "${where}" declares ${section}.formData, which a contract cannot describe. The contract grammar covers JSON values, and multipart carries file parts that have no spelling in it. Drop .contract() from this route, or move the operation to a JSON body.`
63
+ );
64
+ }
65
+ function addOperation(name, routeDef, trail, found) {
66
+ const where = trail.join(".");
67
+ const contract = routeDef.contract;
68
+ assertNoFormData(where, routeDef);
69
+ if (!routeDef.method || !routeDef.path) {
70
+ throw new ContractCollectionError(
71
+ `Contracted route "${where}" has no method or path. A contract describes an operation on the wire, so both are required.`
72
+ );
73
+ }
74
+ if (!contract.since) {
75
+ throw new ContractCollectionError(
76
+ `Contracted route "${where}" has no "since" version. The version an operation first appeared in is part of the promise.`
77
+ );
78
+ }
79
+ if (!contract.response) {
80
+ throw new ContractCollectionError(
81
+ `Contracted route "${where}" declares no response schema. An operation with no body declares Type.Null().`
82
+ );
83
+ }
84
+ const existing = found.get(name);
85
+ if (existing) {
86
+ throw new ContractCollectionError(
87
+ `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.`
88
+ );
89
+ }
90
+ found.set(name, {
91
+ trail: where,
92
+ operation: {
93
+ name,
94
+ method: routeDef.method,
95
+ path: routeDef.path,
96
+ since: contract.since,
97
+ auth: contract.auth ?? "none",
98
+ requiresSession: contract.requiresSession ?? false,
99
+ ...contract.deprecatedIn ? { deprecatedIn: contract.deprecatedIn } : {},
100
+ ...contract.removedIn ? { removedIn: contract.removedIn } : {},
101
+ request: toContractRequest(routeDef.input),
102
+ interceptor: toContractRequest(routeDef.interceptor),
103
+ response: toJsonSchema(contract.response)
104
+ }
105
+ });
106
+ }
107
+ function collectContractDocument(router) {
108
+ const found = /* @__PURE__ */ new Map();
109
+ visitRouter(router, ["router"], found);
110
+ const operations = [...found.values()].map((entry) => entry.operation).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
111
+ return {
112
+ documentVersion: 1,
113
+ ...router._contractVersion ? { contractVersion: router._contractVersion } : {},
114
+ compatibilityPolicy: "perOperation",
115
+ operations
116
+ };
117
+ }
118
+ function canonicalize(value) {
119
+ if (Array.isArray(value)) {
120
+ return value.map(canonicalize);
121
+ }
122
+ if (value === null || typeof value !== "object") {
123
+ return value;
124
+ }
125
+ const source = value;
126
+ const sorted = {};
127
+ for (const key of Object.keys(source).sort()) {
128
+ if (source[key] === void 0) {
129
+ continue;
130
+ }
131
+ sorted[key] = canonicalize(source[key]);
132
+ }
133
+ return sorted;
134
+ }
135
+ function stableStringify(value) {
136
+ return JSON.stringify(canonicalize(value));
137
+ }
138
+ function stableStringifyPretty(value) {
139
+ return `${JSON.stringify(canonicalize(value), null, 4)}
140
+ `;
141
+ }
142
+ function stableDigest(value) {
143
+ return createHash("sha256").update(stableStringify(value), "utf8").digest("hex");
144
+ }
145
+
146
+ // src/contract/compare.ts
147
+ var REQUEST_SECTIONS2 = ["params", "query", "body", "formData", "headers", "cookies"];
148
+ function leafSignature(schema) {
149
+ const { properties, required, items, ...rest } = schema;
150
+ return stableStringify(rest);
151
+ }
152
+ function propertiesOf(schema) {
153
+ const properties = schema.properties;
154
+ if (!properties || typeof properties !== "object") {
155
+ return {};
156
+ }
157
+ return properties;
158
+ }
159
+ function requiredOf(schema) {
160
+ const required = schema.required;
161
+ if (!Array.isArray(required)) {
162
+ return /* @__PURE__ */ new Set();
163
+ }
164
+ return new Set(required.filter((name) => typeof name === "string"));
165
+ }
166
+ function requiresClientToSend(schema) {
167
+ if (schema.type !== "object") {
168
+ return true;
169
+ }
170
+ return requiredOf(schema).size > 0;
171
+ }
172
+ function report(ctx, kind, location, detail) {
173
+ ctx.violations.push({ kind, operation: ctx.operation, location, detail });
174
+ }
175
+ function compareSchema(before, after, location, ctx) {
176
+ if (!before && !after) {
177
+ return;
178
+ }
179
+ if (before && !after) {
180
+ if (ctx.side === "response") {
181
+ report(ctx, "response.field-removed", location, "the response no longer carries this, and old clients read it");
182
+ }
183
+ return;
184
+ }
185
+ if (!before && after) {
186
+ if (ctx.side === "request" && requiresClientToSend(after)) {
187
+ report(
188
+ ctx,
189
+ "request.required-field-added",
190
+ location,
191
+ "this is new and mandatory, so every already-released client is refused"
192
+ );
193
+ }
194
+ return;
195
+ }
196
+ const from = before;
197
+ const to = after;
198
+ if (leafSignature(from) !== leafSignature(to)) {
199
+ report(
200
+ ctx,
201
+ ctx.side === "request" ? "request.type-changed" : "response.type-changed",
202
+ location,
203
+ `the declared type changed: ${leafSignature(from)} \u2192 ${leafSignature(to)}`
204
+ );
205
+ return;
206
+ }
207
+ compareProperties(from, to, location, ctx);
208
+ compareSchema(
209
+ from.items,
210
+ to.items,
211
+ `${location}[]`,
212
+ ctx
213
+ );
214
+ }
215
+ function compareProperties(from, to, location, ctx) {
216
+ const beforeProperties = propertiesOf(from);
217
+ const afterProperties = propertiesOf(to);
218
+ const beforeRequired = requiredOf(from);
219
+ const afterRequired = requiredOf(to);
220
+ for (const [name, beforeSchema] of Object.entries(beforeProperties)) {
221
+ const where = `${location}.${name}`;
222
+ const afterSchema = afterProperties[name];
223
+ compareSchema(beforeSchema, afterSchema, where, ctx);
224
+ if (!afterSchema) {
225
+ continue;
226
+ }
227
+ const wasRequired = beforeRequired.has(name);
228
+ const isRequired = afterRequired.has(name);
229
+ if (ctx.side === "request" && !wasRequired && isRequired) {
230
+ report(ctx, "request.field-became-required", where, "clients that never sent this are now refused");
231
+ }
232
+ if (ctx.side === "response" && wasRequired && !isRequired) {
233
+ report(ctx, "response.field-became-optional", where, "clients that counted on this always arriving now break");
234
+ }
235
+ }
236
+ for (const name of Object.keys(afterProperties)) {
237
+ if (beforeProperties[name]) {
238
+ continue;
239
+ }
240
+ if (ctx.side === "request" && afterRequired.has(name)) {
241
+ report(
242
+ ctx,
243
+ "request.required-field-added",
244
+ `${location}.${name}`,
245
+ "this is new and mandatory, so every already-released client is refused"
246
+ );
247
+ }
248
+ }
249
+ }
250
+ function compareRequestSections(before, after, prefix, ctx) {
251
+ for (const section of REQUEST_SECTIONS2) {
252
+ compareSchema(before[section], after[section], `${prefix}.${section}`, ctx);
253
+ }
254
+ }
255
+ function compareOperation(before, after) {
256
+ const violations = [];
257
+ if (before.path !== after.path) {
258
+ violations.push({
259
+ kind: "operation.path-changed",
260
+ operation: before.name,
261
+ detail: `path moved ${before.path} \u2192 ${after.path}; released clients call the old one`
262
+ });
263
+ }
264
+ if (before.method !== after.method) {
265
+ violations.push({
266
+ kind: "operation.method-changed",
267
+ operation: before.name,
268
+ detail: `method changed ${before.method} \u2192 ${after.method}; released clients send the old one`
269
+ });
270
+ }
271
+ const requestCtx = { operation: before.name, side: "request", violations };
272
+ compareRequestSections(before.request, after.request, "request", requestCtx);
273
+ compareRequestSections(before.interceptor, after.interceptor, "interceptor", requestCtx);
274
+ compareSchema(
275
+ before.response,
276
+ after.response,
277
+ "response",
278
+ { operation: before.name, side: "response", violations }
279
+ );
280
+ return violations;
281
+ }
282
+ function compareDocuments(before, after) {
283
+ const current = new Map(after.operations.map((operation) => [operation.name, operation]));
284
+ const violations = [];
285
+ const removedOperations = [];
286
+ for (const operation of before.operations) {
287
+ const now = current.get(operation.name);
288
+ if (!now) {
289
+ removedOperations.push(operation.name);
290
+ continue;
291
+ }
292
+ violations.push(...compareOperation(operation, now));
293
+ }
294
+ return { violations, removedOperations };
295
+ }
296
+ var ContractSnapshotError = class extends Error {
297
+ constructor(message) {
298
+ super(message);
299
+ this.name = "ContractSnapshotError";
300
+ }
301
+ };
302
+ var CURRENT_FILENAME = "current.json";
303
+ var RELEASED_DIRNAME = "released";
304
+ var USAGE_DIRNAME = "usage";
305
+ function currentPath(contractsDir) {
306
+ return join(contractsDir, CURRENT_FILENAME);
307
+ }
308
+ function releasedDir(contractsDir) {
309
+ return join(contractsDir, RELEASED_DIRNAME);
310
+ }
311
+ function usageDir(contractsDir) {
312
+ return join(contractsDir, USAGE_DIRNAME);
313
+ }
314
+ function compareVersions(a, b) {
315
+ const [aCore, aPre] = splitVersion(a);
316
+ const [bCore, bPre] = splitVersion(b);
317
+ for (let i = 0; i < 3; i++) {
318
+ if (aCore[i] !== bCore[i]) {
319
+ return aCore[i] - bCore[i];
320
+ }
321
+ }
322
+ if (aPre === bPre) {
323
+ return 0;
324
+ }
325
+ if (aPre === void 0) {
326
+ return 1;
327
+ }
328
+ if (bPre === void 0) {
329
+ return -1;
330
+ }
331
+ return comparePreRelease(aPre, bPre);
332
+ }
333
+ function splitVersion(version) {
334
+ const [core, ...rest] = version.split("-");
335
+ const parts = core.split(".").map((part) => Number.parseInt(part, 10));
336
+ if (parts.length !== 3 || parts.some((part) => !Number.isInteger(part) || part < 0)) {
337
+ throw new ContractSnapshotError(`"${version}" is not a version of the form major.minor.patch`);
338
+ }
339
+ return [parts, rest.length > 0 ? rest.join("-") : void 0];
340
+ }
341
+ function comparePreRelease(a, b) {
342
+ const aParts = a.split(".");
343
+ const bParts = b.split(".");
344
+ for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
345
+ const left = aParts[i];
346
+ const right = bParts[i];
347
+ if (left === void 0) {
348
+ return -1;
349
+ }
350
+ if (right === void 0) {
351
+ return 1;
352
+ }
353
+ if (left === right) {
354
+ continue;
355
+ }
356
+ const leftNumeric = /^\d+$/.test(left);
357
+ const rightNumeric = /^\d+$/.test(right);
358
+ if (leftNumeric && rightNumeric) {
359
+ return Number(left) - Number(right);
360
+ }
361
+ if (leftNumeric !== rightNumeric) {
362
+ return leftNumeric ? -1 : 1;
363
+ }
364
+ return left < right ? -1 : 1;
365
+ }
366
+ return 0;
367
+ }
368
+ function listSnapshots(contractsDir) {
369
+ const dir = releasedDir(contractsDir);
370
+ if (!existsSync(dir)) {
371
+ return [];
372
+ }
373
+ 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));
374
+ }
375
+ function newestSnapshot(contractsDir) {
376
+ const snapshots = listSnapshots(contractsDir);
377
+ return snapshots[snapshots.length - 1];
378
+ }
379
+ function readSnapshot(file) {
380
+ let parsed;
381
+ try {
382
+ parsed = JSON.parse(readFileSync(file, "utf-8"));
383
+ } catch (error) {
384
+ const message = error instanceof Error ? error.message : String(error);
385
+ throw new ContractSnapshotError(`${file} could not be read: ${message}`);
386
+ }
387
+ if (typeof parsed.version !== "string" || typeof parsed.sha256 !== "string" || !parsed.document) {
388
+ throw new ContractSnapshotError(`${file} is not a contract snapshot: expected version, sha256 and document`);
389
+ }
390
+ const digest = stableDigest(parsed.document);
391
+ if (digest !== parsed.sha256) {
392
+ throw new ContractSnapshotError(
393
+ `${file} was edited after release: it records sha256 ${parsed.sha256} but its document hashes to ${digest}`
394
+ );
395
+ }
396
+ return { version: parsed.version, sha256: parsed.sha256, document: parsed.document };
397
+ }
398
+ function writeSnapshot(contractsDir, document) {
399
+ const version = document.contractVersion;
400
+ if (!version) {
401
+ throw new ContractSnapshotError(
402
+ 'The contract declares no version, so a release cannot be named. Add .contractVersion("x.y.z") to the router before cutting one.'
403
+ );
404
+ }
405
+ compareVersions(version, version);
406
+ const dir = releasedDir(contractsDir);
407
+ const file = join(dir, `${version}.json`);
408
+ if (existsSync(file)) {
409
+ throw new ContractSnapshotError(
410
+ `${file} already exists. A released version's contract is never rewritten \u2014 cut a new version instead.`
411
+ );
412
+ }
413
+ const newest = newestSnapshot(contractsDir);
414
+ if (newest && compareVersions(version, newest.version) <= 0) {
415
+ throw new ContractSnapshotError(
416
+ `${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.`
417
+ );
418
+ }
419
+ mkdirSync(dir, { recursive: true });
420
+ const snapshot = { version, sha256: stableDigest(document), document };
421
+ writeFileSync(file, stableStringifyPretty(snapshot), "utf-8");
422
+ return file;
423
+ }
424
+ function readCurrentDocument(contractsDir) {
425
+ const file = currentPath(contractsDir);
426
+ try {
427
+ return JSON.parse(readFileSync(file, "utf-8"));
428
+ } catch (error) {
429
+ const message = error instanceof Error ? error.message : String(error);
430
+ throw new ContractSnapshotError(
431
+ `${file} could not be read: ${message}. Run the @spfn/core:contract generator first.`
432
+ );
433
+ }
434
+ }
435
+ function writeCurrentDocument(contractsDir, document) {
436
+ const file = currentPath(contractsDir);
437
+ const content = stableStringifyPretty(document);
438
+ const existing = existsSync(file) ? readFileSync(file, "utf-8") : void 0;
439
+ if (existing === content) {
440
+ return false;
441
+ }
442
+ mkdirSync(contractsDir, { recursive: true });
443
+ writeFileSync(file, content, "utf-8");
444
+ return true;
445
+ }
446
+ function parseRecord(file, raw) {
447
+ const parsed = JSON.parse(raw);
448
+ if (typeof parsed.platform !== "string" || parsed.platform.length === 0) {
449
+ throw new Error('"platform" must be a non-empty string');
450
+ }
451
+ if (typeof parsed.appVersion !== "string" || parsed.appVersion.length === 0) {
452
+ throw new Error('"appVersion" must be a non-empty string');
453
+ }
454
+ if (!Array.isArray(parsed.operations) || parsed.operations.some((name) => typeof name !== "string")) {
455
+ throw new Error('"operations" must be an array of operation names');
456
+ }
457
+ return {
458
+ platform: parsed.platform,
459
+ appVersion: parsed.appVersion,
460
+ operations: parsed.operations,
461
+ file
462
+ };
463
+ }
464
+ function readUsageRecords(usageDir2) {
465
+ let entries;
466
+ try {
467
+ if (!statSync(usageDir2).isDirectory()) {
468
+ return { decidable: false, reason: `${usageDir2} is not a directory` };
469
+ }
470
+ entries = readdirSync(usageDir2).filter((name) => name.endsWith(".json")).sort();
471
+ } catch {
472
+ return { decidable: false, reason: `${usageDir2} does not exist` };
473
+ }
474
+ if (entries.length === 0) {
475
+ return { decidable: false, reason: `${usageDir2} holds no usage file` };
476
+ }
477
+ const records = [];
478
+ for (const entry of entries) {
479
+ const file = join(usageDir2, entry);
480
+ try {
481
+ records.push(parseRecord(file, readFileSync(file, "utf-8")));
482
+ } catch (error) {
483
+ const message = error instanceof Error ? error.message : String(error);
484
+ return { decidable: false, reason: `${file} could not be read: ${message}` };
485
+ }
486
+ }
487
+ return { decidable: true, records };
488
+ }
489
+ function callersOf(operation, records) {
490
+ return records.filter((record) => record.operations.includes(operation));
491
+ }
492
+
493
+ // src/contract/check.ts
494
+ function checkContract(contractsDir, current) {
495
+ const baseline = newestSnapshot(contractsDir);
496
+ if (!baseline) {
497
+ return {
498
+ violations: [],
499
+ warnings: [
500
+ "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."
501
+ ]
502
+ };
503
+ }
504
+ let previous;
505
+ try {
506
+ previous = readSnapshot(baseline.file).document;
507
+ } catch (error) {
508
+ return {
509
+ baselineVersion: baseline.version,
510
+ warnings: [],
511
+ violations: [{
512
+ kind: "snapshot.digest-mismatch",
513
+ detail: error instanceof Error ? error.message : String(error)
514
+ }]
515
+ };
516
+ }
517
+ const { violations, removedOperations } = compareDocuments(previous, current);
518
+ return {
519
+ baselineVersion: baseline.version,
520
+ warnings: [],
521
+ violations: [...violations, ...judgeRemovals(contractsDir, removedOperations)]
522
+ };
523
+ }
524
+ function judgeRemovals(contractsDir, removedOperations) {
525
+ if (removedOperations.length === 0) {
526
+ return [];
527
+ }
528
+ const usage = readUsageRecords(usageDir(contractsDir));
529
+ if (!usage.decidable) {
530
+ return [{
531
+ kind: "usage.undecidable",
532
+ 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.`
533
+ }];
534
+ }
535
+ const violations = [];
536
+ for (const operation of removedOperations) {
537
+ const callers = callersOf(operation, usage.records);
538
+ if (callers.length === 0) {
539
+ continue;
540
+ }
541
+ violations.push({
542
+ kind: "usage.still-called",
543
+ operation,
544
+ detail: "still called by " + callers.map((caller) => `${caller.platform} ${caller.appVersion}`).join(", ")
545
+ });
546
+ }
547
+ return violations;
548
+ }
549
+ function formatViolations(violations) {
550
+ return violations.map((violation) => {
551
+ const where = [violation.operation, violation.location].filter(Boolean).join(" ");
552
+ return ` - [${violation.kind}]${where ? ` ${where}` : ""}: ${violation.detail}`;
553
+ }).join("\n");
554
+ }
555
+
556
+ 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 };
557
+ //# sourceMappingURL=index.js.map
558
+ //# sourceMappingURL=index.js.map