@noctcore/eslint-plugin-contracts 0.1.0 → 0.3.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/dist/index.js CHANGED
@@ -4,18 +4,587 @@ var recommended = {
4
4
  "noctcore-contracts/wire-message-naming": "error",
5
5
  "noctcore-contracts/no-error-stringify": "error",
6
6
  "noctcore-contracts/no-direct-process-env": "error",
7
- "noctcore-contracts/money-must-be-decimal": "error"
7
+ "noctcore-contracts/money-must-be-decimal": "error",
8
+ "noctcore-contracts/require-error-cause": "error",
9
+ "noctcore-contracts/restrict-throw-to-taxonomy": "error",
10
+ "noctcore-contracts/schema-enum-field-consistency": "error",
11
+ "noctcore-contracts/fetch-must-check-ok": "error",
12
+ // Config-required / heuristic rules ship inert. `require-registered-keys` and
13
+ // `env-var-schema-parity` do nothing until their `sinks` / `schema` options are
14
+ // set; `require-schema-parse-at-boundary` is a conservative syntactic slice of a
15
+ // type-aware concern. Enable them explicitly once configured for your project.
16
+ "noctcore-contracts/require-registered-keys": "off",
17
+ "noctcore-contracts/env-var-schema-parity": "off",
18
+ "noctcore-contracts/require-schema-parse-at-boundary": "off"
8
19
  };
9
20
 
10
- // src/rules/money-must-be-decimal.ts
21
+ // src/rules/env-var-schema-parity.ts
22
+ import { readFileSync } from "fs";
23
+ import path from "path";
11
24
  import { AST_NODE_TYPES } from "@typescript-eslint/utils";
12
25
 
13
26
  // src/createRule.ts
14
27
  import { makeCreateRule } from "@noctcore/eslint-utils";
15
28
  var createRule = makeCreateRule("contracts");
16
29
 
30
+ // src/rules/env-var-schema-parity.ts
31
+ var RULE_NAME = "env-var-schema-parity";
32
+ var optionSchema = {
33
+ type: "object",
34
+ additionalProperties: false,
35
+ properties: {
36
+ schema: { type: "string", minLength: 1 }
37
+ }
38
+ };
39
+ var schemaCache = /* @__PURE__ */ new Map();
40
+ var DOTENV_KEY = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/gm;
41
+ var OBJECT_KEY = /["'`]?([A-Za-z_][A-Za-z0-9_]*)["'`]?\s*:/g;
42
+ function parseDeclaredKeys(source) {
43
+ const keys = /* @__PURE__ */ new Set();
44
+ for (const match of source.matchAll(DOTENV_KEY)) {
45
+ if (match[1]) keys.add(match[1]);
46
+ }
47
+ for (const match of source.matchAll(OBJECT_KEY)) {
48
+ if (match[1]) keys.add(match[1]);
49
+ }
50
+ return keys;
51
+ }
52
+ function loadSchema(cwd, schema) {
53
+ const resolved = path.isAbsolute(schema) ? schema : path.resolve(cwd, schema);
54
+ const cached = schemaCache.get(resolved);
55
+ if (cached !== void 0) {
56
+ return cached;
57
+ }
58
+ let keys;
59
+ try {
60
+ keys = parseDeclaredKeys(readFileSync(resolved, "utf8"));
61
+ } catch {
62
+ keys = null;
63
+ }
64
+ schemaCache.set(resolved, keys);
65
+ return keys;
66
+ }
67
+ function isProcessEnv(node) {
68
+ return node.type === AST_NODE_TYPES.MemberExpression && !node.computed && node.object.type === AST_NODE_TYPES.Identifier && node.object.name === "process" && node.property.type === AST_NODE_TYPES.Identifier && node.property.name === "env";
69
+ }
70
+ function isImportMetaEnv(node) {
71
+ return node.type === AST_NODE_TYPES.MemberExpression && !node.computed && node.property.type === AST_NODE_TYPES.Identifier && node.property.name === "env" && node.object.type === AST_NODE_TYPES.MetaProperty && node.object.meta.name === "import" && node.object.property.name === "meta";
72
+ }
73
+ var envVarSchemaParityRule = createRule({
74
+ name: RULE_NAME,
75
+ meta: {
76
+ type: "suggestion",
77
+ docs: {
78
+ description: "Require every `process.env.FOO` / `import.meta.env.FOO` key to be declared in a schema file (`.env.example` or a zod-env module), so config access and config declaration cannot drift apart."
79
+ },
80
+ schema: [optionSchema],
81
+ messages: {
82
+ undeclaredEnvVar: "Env var `{{name}}` is read here but not declared in `{{schema}}`. Add it to the schema (or fix the typo) so config stays a validated contract."
83
+ }
84
+ },
85
+ defaultOptions: [{}],
86
+ create(context, [options]) {
87
+ const schema = options.schema;
88
+ if (!schema) {
89
+ return {};
90
+ }
91
+ const declared = loadSchema(context.cwd, schema);
92
+ if (declared === null) {
93
+ return {};
94
+ }
95
+ function check(node) {
96
+ if (node.computed || node.property.type !== AST_NODE_TYPES.Identifier) {
97
+ return;
98
+ }
99
+ const name = node.property.name;
100
+ if (!declared.has(name)) {
101
+ context.report({
102
+ node: node.property,
103
+ messageId: "undeclaredEnvVar",
104
+ data: { name, schema }
105
+ });
106
+ }
107
+ }
108
+ return {
109
+ MemberExpression(node) {
110
+ if (isProcessEnv(node.object) || isImportMetaEnv(node.object)) {
111
+ check(node);
112
+ }
113
+ }
114
+ };
115
+ }
116
+ });
117
+
118
+ // src/rules/fetch-must-check-ok.ts
119
+ import { AST_NODE_TYPES as AST_NODE_TYPES2 } from "@typescript-eslint/utils";
120
+ var RULE_NAME2 = "fetch-must-check-ok";
121
+ var optionSchema2 = {
122
+ type: "object",
123
+ additionalProperties: false,
124
+ properties: {
125
+ fetchFunctions: {
126
+ type: "array",
127
+ items: { type: "string", minLength: 1 },
128
+ uniqueItems: true
129
+ }
130
+ }
131
+ };
132
+ function isNode(value) {
133
+ return typeof value === "object" && value !== null && "type" in value;
134
+ }
135
+ function walkSome(root, keys, predicate) {
136
+ const stack = [root];
137
+ for (let node = stack.pop(); node !== void 0; node = stack.pop()) {
138
+ if (predicate(node)) {
139
+ return true;
140
+ }
141
+ for (const key of keys[node.type] ?? []) {
142
+ const value = Reflect.get(node, key);
143
+ if (Array.isArray(value)) {
144
+ for (const child of value) {
145
+ if (isNode(child)) {
146
+ stack.push(child);
147
+ }
148
+ }
149
+ } else if (isNode(value)) {
150
+ stack.push(value);
151
+ }
152
+ }
153
+ }
154
+ return false;
155
+ }
156
+ function calleePath(node) {
157
+ if (node.type === AST_NODE_TYPES2.Identifier) {
158
+ return node.name;
159
+ }
160
+ if (node.type === AST_NODE_TYPES2.MemberExpression && !node.computed && node.property.type === AST_NODE_TYPES2.Identifier) {
161
+ const object = calleePath(node.object);
162
+ return object === null ? null : `${object}.${node.property.name}`;
163
+ }
164
+ return null;
165
+ }
166
+ var OK_PROP = "ok";
167
+ var OK_PROPS = /* @__PURE__ */ new Set([OK_PROP, "status"]);
168
+ var ASSERTION_NAMES = /^(?:[Aa]ssert|[Ii]nvariant|[Ee]nsure|[Ee]xpect)(?:[A-Z_]\w*)?$/u;
169
+ var COMPARISONS = /* @__PURE__ */ new Set(["===", "!==", "==", "!=", "<", "<=", ">", ">="]);
170
+ var FIRST_ERROR_STATUS = 400;
171
+ function literalValue(node) {
172
+ if (node.type !== AST_NODE_TYPES2.Literal) {
173
+ return void 0;
174
+ }
175
+ return typeof node.value === "number" || typeof node.value === "boolean" ? node.value : void 0;
176
+ }
177
+ function mirror(operator) {
178
+ switch (operator) {
179
+ case "<":
180
+ return ">";
181
+ case "<=":
182
+ return ">=";
183
+ case ">":
184
+ return "<";
185
+ case ">=":
186
+ return "<=";
187
+ default:
188
+ return operator;
189
+ }
190
+ }
191
+ function booleanPolarity(operator, value) {
192
+ if (operator === "===" || operator === "==") {
193
+ return value ? "positive" : "negative";
194
+ }
195
+ if (operator === "!==" || operator === "!=") {
196
+ return value ? "negative" : "positive";
197
+ }
198
+ return "opaque";
199
+ }
200
+ function statusPolarity(operator, value) {
201
+ const isSuccessCode = value >= 200 && value < 300;
202
+ switch (operator) {
203
+ case "===":
204
+ case "==":
205
+ return isSuccessCode ? "positive" : "opaque";
206
+ case "!==":
207
+ case "!=":
208
+ return isSuccessCode ? "negative" : "opaque";
209
+ case "<":
210
+ return value <= FIRST_ERROR_STATUS ? "positive" : "opaque";
211
+ case "<=":
212
+ return value < FIRST_ERROR_STATUS ? "positive" : "opaque";
213
+ // A failure test is only useful for what it says about the OTHER side, so
214
+ // what matters is that everything below the threshold is a success:
215
+ // `>= 300` and `>= 400` both leave only good responses behind, while
216
+ // `>= 500` leaves every 4xx there.
217
+ case ">=":
218
+ return value <= FIRST_ERROR_STATUS ? "negative" : "opaque";
219
+ case ">":
220
+ return value < FIRST_ERROR_STATUS ? "negative" : "opaque";
221
+ default:
222
+ return "opaque";
223
+ }
224
+ }
225
+ function comparisonPolarity(node, readIsLeft) {
226
+ const value = literalValue(readIsLeft ? node.right : node.left);
227
+ const operator = readIsLeft ? node.operator : mirror(node.operator);
228
+ if (typeof value === "boolean") {
229
+ return booleanPolarity(operator, value);
230
+ }
231
+ return typeof value === "number" ? statusPolarity(operator, value) : "opaque";
232
+ }
233
+ function propReadOn(node, objectName, props) {
234
+ if (node.type !== AST_NODE_TYPES2.MemberExpression || node.computed) {
235
+ return false;
236
+ }
237
+ if (node.object.type !== AST_NODE_TYPES2.Identifier || node.object.name !== objectName || node.property.type !== AST_NODE_TYPES2.Identifier) {
238
+ return false;
239
+ }
240
+ const name = node.property.name;
241
+ return typeof props === "string" ? name === props : props.has(name);
242
+ }
243
+ function findJsonReads(root, keys, name) {
244
+ const reads = [];
245
+ walkSome(root, keys, (node) => {
246
+ if (propReadOn(node, name, "json")) {
247
+ reads.push(node);
248
+ }
249
+ return false;
250
+ });
251
+ return reads;
252
+ }
253
+ function isAssertionName(node) {
254
+ return node.type === AST_NODE_TYPES2.Identifier && ASSERTION_NAMES.test(node.name);
255
+ }
256
+ function isAssertionCallee(callee) {
257
+ if (callee.type === AST_NODE_TYPES2.Identifier) {
258
+ return isAssertionName(callee);
259
+ }
260
+ return callee.type === AST_NODE_TYPES2.MemberExpression && !callee.computed && (isAssertionName(callee.object) || isAssertionName(callee.property));
261
+ }
262
+ var TERMINAL_TYPES = /* @__PURE__ */ new Set([
263
+ AST_NODE_TYPES2.IfStatement,
264
+ AST_NODE_TYPES2.WhileStatement,
265
+ AST_NODE_TYPES2.DoWhileStatement,
266
+ AST_NODE_TYPES2.ConditionalExpression,
267
+ AST_NODE_TYPES2.SwitchStatement,
268
+ AST_NODE_TYPES2.CallExpression
269
+ ]);
270
+ var ASSERTION_OPERATORS = /* @__PURE__ */ new Map([
271
+ ["equal", "==="],
272
+ ["equals", "==="],
273
+ ["strictEqual", "==="],
274
+ ["deepEqual", "==="],
275
+ ["deepStrictEqual", "==="],
276
+ ["toBe", "==="],
277
+ ["toEqual", "==="],
278
+ ["notEqual", "!=="],
279
+ ["notStrictEqual", "!=="],
280
+ ["notDeepEqual", "!=="]
281
+ ]);
282
+ function assertionOperator(callee) {
283
+ return callee.type === AST_NODE_TYPES2.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES2.Identifier ? ASSERTION_OPERATORS.get(callee.property.name) : void 0;
284
+ }
285
+ function assertionCheck(parent, child, check) {
286
+ if (check.compared) {
287
+ return check;
288
+ }
289
+ const operator = assertionOperator(parent.callee);
290
+ if (operator === void 0) {
291
+ return check;
292
+ }
293
+ const other = parent.arguments.find((arg) => arg !== child);
294
+ const value = other === void 0 ? void 0 : literalValue(other);
295
+ if (typeof value === "boolean") {
296
+ return {
297
+ ...check,
298
+ compared: true,
299
+ polarity: booleanPolarity(operator, value)
300
+ };
301
+ }
302
+ return typeof value === "number" ? { ...check, compared: true, polarity: statusPolarity(operator, value) } : check;
303
+ }
304
+ function terminalCheck(parent, child, state) {
305
+ const check = { owner: parent, ...state };
306
+ switch (parent.type) {
307
+ case AST_NODE_TYPES2.IfStatement:
308
+ case AST_NODE_TYPES2.WhileStatement:
309
+ case AST_NODE_TYPES2.DoWhileStatement:
310
+ case AST_NODE_TYPES2.ConditionalExpression:
311
+ return parent.test === child ? check : null;
312
+ case AST_NODE_TYPES2.SwitchStatement:
313
+ return parent.discriminant === child ? { ...check, compared: true, polarity: "opaque" } : null;
314
+ // An assertion settles a status only when it COMPARES it.
315
+ // `assert.equal(res.status, 200)` does; `assert.ok(res.status)` asserts a
316
+ // number is truthy, which every response that arrived satisfies. Arity
317
+ // cannot tell them apart (`assert.ok(res.status, 'message')` also has two
318
+ // arguments), so the comparison is read from the assertion's own name.
319
+ case AST_NODE_TYPES2.CallExpression:
320
+ return isAssertionCallee(parent.callee) && parent.callee !== child ? assertionCheck(parent, child, check) : null;
321
+ default:
322
+ return null;
323
+ }
324
+ }
325
+ function combinatorStep(parent, child, state, parse) {
326
+ switch (parent.type) {
327
+ case AST_NODE_TYPES2.UnaryExpression:
328
+ if (parent.operator === "typeof") {
329
+ return "stop";
330
+ }
331
+ if (parent.operator === "!") {
332
+ state.polarity = state.polarity === "positive" ? "negative" : "positive";
333
+ }
334
+ return "continue";
335
+ case AST_NODE_TYPES2.BinaryExpression: {
336
+ if (!COMPARISONS.has(parent.operator)) {
337
+ return "stop";
338
+ }
339
+ const polarity = comparisonPolarity(parent, parent.left === child);
340
+ state.compared = true;
341
+ state.polarity = polarity;
342
+ return polarity === "opaque" ? "stop" : "continue";
343
+ }
344
+ case AST_NODE_TYPES2.LogicalExpression:
345
+ if (parent.operator === "&&") {
346
+ state.underAnd = true;
347
+ }
348
+ if (parent.operator === "||") {
349
+ state.underOr = true;
350
+ }
351
+ return parent.left === child && contains(parent.right, parse) ? { owner: parent, ...state } : "continue";
352
+ case AST_NODE_TYPES2.ChainExpression:
353
+ case AST_NODE_TYPES2.TSNonNullExpression:
354
+ return "continue";
355
+ default:
356
+ return "stop";
357
+ }
358
+ }
359
+ function climb(read, parse) {
360
+ const state = {
361
+ polarity: "positive",
362
+ compared: false,
363
+ underAnd: false,
364
+ underOr: false
365
+ };
366
+ let child = read;
367
+ let parent = read.parent;
368
+ while (parent !== void 0) {
369
+ if (TERMINAL_TYPES.has(parent.type)) {
370
+ return terminalCheck(parent, child, state);
371
+ }
372
+ const step = combinatorStep(parent, child, state, parse);
373
+ if (step === "stop") {
374
+ return null;
375
+ }
376
+ if (step !== "continue") {
377
+ return step;
378
+ }
379
+ child = parent;
380
+ parent = parent.parent;
381
+ }
382
+ return null;
383
+ }
384
+ function contains(outer, inner) {
385
+ return outer.range[0] <= inner.range[0] && outer.range[1] >= inner.range[1];
386
+ }
387
+ function alwaysExits(node) {
388
+ if (node.type === AST_NODE_TYPES2.ReturnStatement || node.type === AST_NODE_TYPES2.ThrowStatement) {
389
+ return true;
390
+ }
391
+ return node.type === AST_NODE_TYPES2.BlockStatement && node.body.some((stmt) => alwaysExits(stmt));
392
+ }
393
+ function scopeOfCheck(node) {
394
+ let current = node;
395
+ while (current.parent !== void 0 && !current.type.endsWith("Statement")) {
396
+ current = current.parent;
397
+ }
398
+ return current.parent ?? current;
399
+ }
400
+ function isSuccessCase(arm) {
401
+ if (arm.test === null) {
402
+ return false;
403
+ }
404
+ const value = literalValue(arm.test);
405
+ return typeof value === "number" && value >= 200 && value < 300;
406
+ }
407
+ function switchArmProtects(owner, parse) {
408
+ const index = owner.cases.findIndex((arm) => contains(arm, parse));
409
+ const own = owner.cases[index];
410
+ if (own === void 0 || !isSuccessCase(own)) {
411
+ return false;
412
+ }
413
+ for (let i = index - 1; i >= 0; i -= 1) {
414
+ const arm = owner.cases[i];
415
+ if (arm === void 0 || arm.consequent.length > 0) {
416
+ break;
417
+ }
418
+ if (!isSuccessCase(arm)) {
419
+ return false;
420
+ }
421
+ }
422
+ return true;
423
+ }
424
+ function branchProtects(check, parse) {
425
+ const { owner } = check;
426
+ if (owner.type === AST_NODE_TYPES2.SwitchStatement) {
427
+ return switchArmProtects(owner, parse);
428
+ }
429
+ if (owner.type === AST_NODE_TYPES2.LogicalExpression) {
430
+ if (!contains(owner.right, parse)) {
431
+ return false;
432
+ }
433
+ if (owner.operator === "&&") {
434
+ return check.polarity === "positive";
435
+ }
436
+ return owner.operator === "||" && check.polarity === "negative";
437
+ }
438
+ if (owner.type === AST_NODE_TYPES2.WhileStatement || owner.type === AST_NODE_TYPES2.DoWhileStatement) {
439
+ return contains(owner.body, parse) && entersOnSuccess(check);
440
+ }
441
+ if (owner.type !== AST_NODE_TYPES2.ConditionalExpression && owner.type !== AST_NODE_TYPES2.IfStatement) {
442
+ return false;
443
+ }
444
+ if (contains(owner.consequent, parse)) {
445
+ return entersOnSuccess(check);
446
+ }
447
+ return owner.alternate !== null && contains(owner.alternate, parse) && skipsOnSuccess(check);
448
+ }
449
+ function entersOnSuccess(check) {
450
+ return check.polarity === "positive" && !check.underOr;
451
+ }
452
+ function skipsOnSuccess(check) {
453
+ return check.polarity === "negative" && !check.underAnd;
454
+ }
455
+ function guardProtects(check, parse) {
456
+ const { owner } = check;
457
+ if (owner.type === AST_NODE_TYPES2.CallExpression) {
458
+ return entersOnSuccess(check) && owner.range[1] <= parse.range[0] && contains(scopeOfCheck(owner), parse);
459
+ }
460
+ if (owner.type !== AST_NODE_TYPES2.IfStatement) {
461
+ return false;
462
+ }
463
+ if (owner.range[1] > parse.range[0] || !contains(scopeOfCheck(owner), parse)) {
464
+ return false;
465
+ }
466
+ if (alwaysExits(owner.consequent)) {
467
+ return skipsOnSuccess(check);
468
+ }
469
+ return owner.alternate !== null && alwaysExits(owner.alternate) && entersOnSuccess(check);
470
+ }
471
+ function protects(check, parse) {
472
+ if (!check.compared) {
473
+ return false;
474
+ }
475
+ return branchProtects(check, parse) || guardProtects(check, parse);
476
+ }
477
+ function statusAliases(root, keys, name) {
478
+ const aliases = /* @__PURE__ */ new Map();
479
+ walkSome(root, keys, (node) => {
480
+ if (node.type === AST_NODE_TYPES2.VariableDeclarator && node.id.type === AST_NODE_TYPES2.Identifier && node.init !== null && propReadOn(node.init, name, OK_PROPS) && node.init.property.type === AST_NODE_TYPES2.Identifier) {
481
+ aliases.set(node.id.name, node.init.property.name);
482
+ }
483
+ return false;
484
+ });
485
+ return aliases;
486
+ }
487
+ function checkedProp(node, name, aliases) {
488
+ if (propReadOn(node, name, OK_PROPS)) {
489
+ return node.property.type === AST_NODE_TYPES2.Identifier ? node.property.name : void 0;
490
+ }
491
+ return node.type === AST_NODE_TYPES2.Identifier ? aliases.get(node.name) : void 0;
492
+ }
493
+ function isProtected(root, keys, name, aliases, parse) {
494
+ return walkSome(root, keys, (node) => {
495
+ const prop = checkedProp(node, name, aliases);
496
+ if (prop === void 0) {
497
+ return false;
498
+ }
499
+ const check = climb(node, parse);
500
+ if (check === null) {
501
+ return false;
502
+ }
503
+ return protects(
504
+ prop === OK_PROP ? { ...check, compared: true } : check,
505
+ parse
506
+ );
507
+ });
508
+ }
509
+ function scopeOf(node) {
510
+ let current = node.parent;
511
+ while (current !== void 0) {
512
+ if (current.type === AST_NODE_TYPES2.BlockStatement || current.type === AST_NODE_TYPES2.Program) {
513
+ return current;
514
+ }
515
+ current = current.parent;
516
+ }
517
+ return node;
518
+ }
519
+ function skipAwait(node) {
520
+ return node?.type === AST_NODE_TYPES2.AwaitExpression ? node.parent : node;
521
+ }
522
+ function thenCallbackParam(node) {
523
+ if (node.type !== AST_NODE_TYPES2.MemberExpression || node.computed || node.property.type !== AST_NODE_TYPES2.Identifier || node.property.name !== "then" || node.parent.type !== AST_NODE_TYPES2.CallExpression) {
524
+ return null;
525
+ }
526
+ const callback = node.parent.arguments[0];
527
+ if (callback === void 0 || callback.type !== AST_NODE_TYPES2.ArrowFunctionExpression && callback.type !== AST_NODE_TYPES2.FunctionExpression) {
528
+ return null;
529
+ }
530
+ const param = callback.params[0];
531
+ return param?.type === AST_NODE_TYPES2.Identifier ? { name: param.name, body: callback.body } : null;
532
+ }
533
+ var fetchMustCheckOkRule = createRule({
534
+ name: RULE_NAME2,
535
+ meta: {
536
+ type: "problem",
537
+ docs: {
538
+ description: "Require a fetch response to be checked with `.ok` or a status comparison before `.json()` parses its body."
539
+ },
540
+ schema: [optionSchema2],
541
+ messages: {
542
+ missingOkCheck: "`fetch` resolves on 4xx/5xx too, so `.json()` here can parse an error body as data. Check `response.ok` (or compare the status) and leave early before reading the body."
543
+ }
544
+ },
545
+ defaultOptions: [{ fetchFunctions: ["fetch"] }],
546
+ create(context, [options]) {
547
+ const fetchFunctions = new Set(options.fetchFunctions ?? ["fetch"]);
548
+ const keys = context.sourceCode.visitorKeys;
549
+ function reportUnprotected(root, name) {
550
+ const aliases = statusAliases(root, keys, name);
551
+ for (const read of findJsonReads(root, keys, name)) {
552
+ if (!isProtected(root, keys, name, aliases, read)) {
553
+ context.report({ node: read, messageId: "missingOkCheck" });
554
+ }
555
+ }
556
+ }
557
+ return {
558
+ CallExpression(node) {
559
+ const path2 = calleePath(node.callee);
560
+ if (path2 === null || !fetchFunctions.has(path2)) {
561
+ return;
562
+ }
563
+ const parent = skipAwait(node.parent);
564
+ if (parent === void 0) {
565
+ return;
566
+ }
567
+ if (parent.type === AST_NODE_TYPES2.MemberExpression && !parent.computed && parent.property.type === AST_NODE_TYPES2.Identifier && parent.property.name === "json") {
568
+ context.report({ node: parent, messageId: "missingOkCheck" });
569
+ return;
570
+ }
571
+ const callback = thenCallbackParam(parent);
572
+ if (callback !== null) {
573
+ reportUnprotected(callback.body, callback.name);
574
+ return;
575
+ }
576
+ if (parent.type !== AST_NODE_TYPES2.VariableDeclarator || parent.id.type !== AST_NODE_TYPES2.Identifier) {
577
+ return;
578
+ }
579
+ reportUnprotected(scopeOf(parent), parent.id.name);
580
+ }
581
+ };
582
+ }
583
+ });
584
+
17
585
  // src/rules/money-must-be-decimal.ts
18
- var RULE_NAME = "money-must-be-decimal";
586
+ import { AST_NODE_TYPES as AST_NODE_TYPES3 } from "@typescript-eslint/utils";
587
+ var RULE_NAME3 = "money-must-be-decimal";
19
588
  var DEFAULT_DECIMAL_TYPE = "Decimal";
20
589
  var DEFAULT_FIELD_PATTERNS = [
21
590
  "amount",
@@ -25,7 +594,7 @@ var DEFAULT_FIELD_PATTERNS = [
25
594
  "balance"
26
595
  ];
27
596
  var DEFAULT_ALLOWED_FILES = [];
28
- var optionSchema = {
597
+ var optionSchema3 = {
29
598
  type: "object",
30
599
  additionalProperties: false,
31
600
  properties: {
@@ -54,22 +623,22 @@ function isAllowedFile(filename, patterns) {
54
623
  return patterns.some((pattern) => normalized.endsWith(toForwardSlash(pattern)));
55
624
  }
56
625
  function staticName(node) {
57
- if (node.type === AST_NODE_TYPES.Identifier) {
626
+ if (node.type === AST_NODE_TYPES3.Identifier) {
58
627
  return node.name;
59
628
  }
60
629
  return void 0;
61
630
  }
62
631
  function isNumberAnnotation(annotation) {
63
- return annotation?.typeAnnotation.type === AST_NODE_TYPES.TSNumberKeyword;
632
+ return annotation?.typeAnnotation.type === AST_NODE_TYPES3.TSNumberKeyword;
64
633
  }
65
634
  var moneyMustBeDecimalRule = createRule({
66
- name: RULE_NAME,
635
+ name: RULE_NAME3,
67
636
  meta: {
68
637
  type: "problem",
69
638
  docs: {
70
639
  description: "Disallow monetary values typed as the JS primitive `number`. Money-named fields explicitly typed `: number` lose precision to float rounding; use a Decimal money type instead."
71
640
  },
72
- schema: [optionSchema],
641
+ schema: [optionSchema3],
73
642
  messages: {
74
643
  moneyMustBeDecimal: "Monetary values must use {{decimalType}}, never the JS `number` primitive, to avoid float rounding errors. Rename or retype this field to a {{decimalType}} money type."
75
644
  }
@@ -105,7 +674,7 @@ var moneyMustBeDecimalRule = createRule({
105
674
  },
106
675
  // `const total: number = ...`: annotated variable declarator.
107
676
  VariableDeclarator(node) {
108
- if (node.id.type !== AST_NODE_TYPES.Identifier) {
677
+ if (node.id.type !== AST_NODE_TYPES3.Identifier) {
109
678
  return;
110
679
  }
111
680
  const name = node.id.name;
@@ -118,15 +687,15 @@ var moneyMustBeDecimalRule = createRule({
118
687
  });
119
688
 
120
689
  // src/rules/no-direct-process-env.ts
121
- import { AST_NODE_TYPES as AST_NODE_TYPES2 } from "@typescript-eslint/utils";
122
- var RULE_NAME2 = "no-direct-process-env";
690
+ import { AST_NODE_TYPES as AST_NODE_TYPES4 } from "@typescript-eslint/utils";
691
+ var RULE_NAME4 = "no-direct-process-env";
123
692
  var DEFAULT_CONFIG_MODULE = "@/config";
124
693
  var DEFAULT_ALLOWED_FILES2 = [
125
694
  "**/*.config.{ts,js,mjs,cjs}",
126
695
  "**/*.{spec,test}.{ts,tsx}",
127
696
  "**/scripts/**"
128
697
  ];
129
- var optionSchema2 = {
698
+ var optionSchema4 = {
130
699
  type: "object",
131
700
  additionalProperties: false,
132
701
  properties: {
@@ -180,23 +749,23 @@ function isAllowedFile2(filename, patterns) {
180
749
  const normalized = filename.split("\\").join("/");
181
750
  return patterns.some((pattern) => globToRegExp(pattern).test(normalized));
182
751
  }
183
- function isProcessEnv(node) {
184
- if (node.type !== AST_NODE_TYPES2.MemberExpression || node.object.type !== AST_NODE_TYPES2.Identifier || node.object.name !== "process") {
752
+ function isProcessEnv2(node) {
753
+ if (node.type !== AST_NODE_TYPES4.MemberExpression || node.object.type !== AST_NODE_TYPES4.Identifier || node.object.name !== "process") {
185
754
  return false;
186
755
  }
187
756
  if (node.computed) {
188
- return node.property.type === AST_NODE_TYPES2.Literal && node.property.value === "env";
757
+ return node.property.type === AST_NODE_TYPES4.Literal && node.property.value === "env";
189
758
  }
190
- return node.property.type === AST_NODE_TYPES2.Identifier && node.property.name === "env";
759
+ return node.property.type === AST_NODE_TYPES4.Identifier && node.property.name === "env";
191
760
  }
192
761
  var noDirectProcessEnvRule = createRule({
193
- name: RULE_NAME2,
762
+ name: RULE_NAME4,
194
763
  meta: {
195
764
  type: "problem",
196
765
  docs: {
197
766
  description: "Disallow direct `process.env` access. Force every consumer through a typed, validated config accessor so a missing variable fails at boot, not at use."
198
767
  },
199
- schema: [optionSchema2],
768
+ schema: [optionSchema4],
200
769
  messages: {
201
770
  directProcessEnv: "Read environment variables through your typed config accessor (import from `{{configModule}}`). Direct `process.env` access bypasses boot-time validation."
202
771
  }
@@ -222,7 +791,7 @@ var noDirectProcessEnvRule = createRule({
222
791
  * returned, or assigned (`log(process.env)`, `return process.env`).
223
792
  */
224
793
  MemberExpression(node) {
225
- if (isProcessEnv(node)) {
794
+ if (isProcessEnv2(node)) {
226
795
  context.report({ node, messageId: "directProcessEnv", data: { configModule } });
227
796
  }
228
797
  }
@@ -231,10 +800,10 @@ var noDirectProcessEnvRule = createRule({
231
800
  });
232
801
 
233
802
  // src/rules/no-error-stringify.ts
234
- import { AST_NODE_TYPES as AST_NODE_TYPES3 } from "@typescript-eslint/utils";
235
- var RULE_NAME3 = "no-error-stringify";
803
+ import { AST_NODE_TYPES as AST_NODE_TYPES5 } from "@typescript-eslint/utils";
804
+ var RULE_NAME5 = "no-error-stringify";
236
805
  var DEFAULT_ERROR_NAMES = ["error", "err", "e", "cause"];
237
- var optionSchema3 = {
806
+ var optionSchema5 = {
238
807
  type: "object",
239
808
  additionalProperties: false,
240
809
  properties: {
@@ -247,19 +816,19 @@ var optionSchema3 = {
247
816
  }
248
817
  };
249
818
  function isEmptyStringLiteral(node) {
250
- return node.type === AST_NODE_TYPES3.Literal && node.value === "";
819
+ return node.type === AST_NODE_TYPES5.Literal && node.value === "";
251
820
  }
252
821
  function isErrorIdentifier(node, names) {
253
- return node.type === AST_NODE_TYPES3.Identifier && names.has(node.name);
822
+ return node.type === AST_NODE_TYPES5.Identifier && names.has(node.name);
254
823
  }
255
824
  var noErrorStringifyRule = createRule({
256
- name: RULE_NAME3,
825
+ name: RULE_NAME5,
257
826
  meta: {
258
827
  type: "problem",
259
828
  docs: {
260
829
  description: 'Disallow stringifying an error with bare `${error}` interpolation, `error.toString()`, or `error + ""`. These drop the cause chain. Use `error instanceof Error ? error.message : String(error)` instead.'
261
830
  },
262
- schema: [optionSchema3],
831
+ schema: [optionSchema5],
263
832
  messages: {
264
833
  noErrorStringify: "Stringifying an error this way drops its cause chain. Use `{{name}} instanceof Error ? {{name}}.message : String({{name}})` (or pass the Error object straight to the logger)."
265
834
  }
@@ -274,7 +843,7 @@ var noErrorStringifyRule = createRule({
274
843
  // `error.toString()`
275
844
  'CallExpression[callee.type="MemberExpression"]'(node) {
276
845
  const callee = node.callee;
277
- if (!callee.computed && callee.property.type === AST_NODE_TYPES3.Identifier && callee.property.name === "toString" && node.arguments.length === 0 && isErrorIdentifier(callee.object, errorNames)) {
846
+ if (!callee.computed && callee.property.type === AST_NODE_TYPES5.Identifier && callee.property.name === "toString" && node.arguments.length === 0 && isErrorIdentifier(callee.object, errorNames)) {
278
847
  report(node, callee.object.name);
279
848
  }
280
849
  },
@@ -306,10 +875,506 @@ var noErrorStringifyRule = createRule({
306
875
  }
307
876
  });
308
877
 
878
+ // src/rules/require-error-cause.ts
879
+ import { AST_NODE_TYPES as AST_NODE_TYPES6 } from "@typescript-eslint/utils";
880
+ var RULE_NAME6 = "require-error-cause";
881
+ function constructorSimpleName(node) {
882
+ const callee = node.callee;
883
+ if (callee.type === AST_NODE_TYPES6.Identifier) {
884
+ return callee.name;
885
+ }
886
+ if (callee.type === AST_NODE_TYPES6.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES6.Identifier) {
887
+ return callee.property.name;
888
+ }
889
+ return null;
890
+ }
891
+ function isErrorLikeName(name) {
892
+ return /(?:Error|Exception)$/.test(name);
893
+ }
894
+ function alreadyHasCause(node) {
895
+ for (const arg of node.arguments) {
896
+ if (arg.type === AST_NODE_TYPES6.SpreadElement) {
897
+ return true;
898
+ }
899
+ if (arg.type === AST_NODE_TYPES6.ObjectExpression) {
900
+ for (const prop of arg.properties) {
901
+ if (prop.type === AST_NODE_TYPES6.SpreadElement) {
902
+ return true;
903
+ }
904
+ const key = prop.key;
905
+ const isCause = key.type === AST_NODE_TYPES6.Identifier && key.name === "cause" || key.type === AST_NODE_TYPES6.Literal && key.value === "cause";
906
+ if (isCause) {
907
+ return true;
908
+ }
909
+ }
910
+ }
911
+ }
912
+ return false;
913
+ }
914
+ function buildFix(node, binding) {
915
+ const args = node.arguments;
916
+ if (args.length === 0) {
917
+ return null;
918
+ }
919
+ const last = args[args.length - 1];
920
+ if (last === void 0) {
921
+ return null;
922
+ }
923
+ if (last.type === AST_NODE_TYPES6.ObjectExpression) {
924
+ const props = last.properties;
925
+ if (props.length === 0) {
926
+ return (fixer) => fixer.replaceText(last, `{ cause: ${binding} }`);
927
+ }
928
+ const lastProp = props[props.length - 1];
929
+ if (lastProp === void 0) {
930
+ return null;
931
+ }
932
+ return (fixer) => fixer.insertTextAfter(lastProp, `, cause: ${binding}`);
933
+ }
934
+ return (fixer) => fixer.insertTextAfter(last, `, { cause: ${binding} }`);
935
+ }
936
+ var requireErrorCauseRule = createRule({
937
+ name: RULE_NAME6,
938
+ meta: {
939
+ type: "problem",
940
+ docs: {
941
+ description: "Require re-thrown errors inside a `catch` to forward the caught error as `{ cause }`. A `throw new SomeError(...)` that omits the cause severs the chain to the original failure."
942
+ },
943
+ fixable: "code",
944
+ schema: [],
945
+ messages: {
946
+ missingCause: "Re-throwing inside `catch` without `{ cause: {{binding}} }` drops the original error. Pass `{ cause: {{binding}} }` to `new {{ctor}}(...)` so the chain is preserved."
947
+ }
948
+ },
949
+ defaultOptions: [],
950
+ create(context) {
951
+ const catchBindings = [];
952
+ return {
953
+ CatchClause(node) {
954
+ const param = node.param;
955
+ catchBindings.push(
956
+ param && param.type === AST_NODE_TYPES6.Identifier ? param.name : null
957
+ );
958
+ },
959
+ "CatchClause:exit"() {
960
+ catchBindings.pop();
961
+ },
962
+ ThrowStatement(node) {
963
+ const binding = catchBindings[catchBindings.length - 1];
964
+ if (binding == null) {
965
+ return;
966
+ }
967
+ const arg = node.argument;
968
+ if (arg.type !== AST_NODE_TYPES6.NewExpression) {
969
+ return;
970
+ }
971
+ const ctor = constructorSimpleName(arg);
972
+ if (ctor === null || !isErrorLikeName(ctor)) {
973
+ return;
974
+ }
975
+ if (alreadyHasCause(arg)) {
976
+ return;
977
+ }
978
+ const fix = buildFix(arg, binding);
979
+ context.report({
980
+ node: arg,
981
+ messageId: "missingCause",
982
+ data: { binding, ctor },
983
+ ...fix ? { fix } : {}
984
+ });
985
+ }
986
+ };
987
+ }
988
+ });
989
+
990
+ // src/rules/require-registered-keys.ts
991
+ import { AST_NODE_TYPES as AST_NODE_TYPES7 } from "@typescript-eslint/utils";
992
+ var RULE_NAME7 = "require-registered-keys";
993
+ var optionSchema6 = {
994
+ type: "object",
995
+ additionalProperties: false,
996
+ properties: {
997
+ sinks: {
998
+ type: "array",
999
+ items: {
1000
+ type: "object",
1001
+ additionalProperties: false,
1002
+ required: ["callee", "argIndex"],
1003
+ properties: {
1004
+ callee: { type: "string", minLength: 1 },
1005
+ argIndex: { type: "integer", minimum: 0 }
1006
+ }
1007
+ }
1008
+ },
1009
+ registry: { type: "string", minLength: 1 }
1010
+ }
1011
+ };
1012
+ function calleePath2(callee) {
1013
+ if (callee.type === AST_NODE_TYPES7.Identifier) {
1014
+ return callee.name;
1015
+ }
1016
+ if (callee.type === AST_NODE_TYPES7.MemberExpression && !callee.computed) {
1017
+ if (callee.property.type !== AST_NODE_TYPES7.Identifier) {
1018
+ return null;
1019
+ }
1020
+ const objectPath = calleePath2(callee.object);
1021
+ return objectPath === null ? null : `${objectPath}.${callee.property.name}`;
1022
+ }
1023
+ return null;
1024
+ }
1025
+ function isStringLiteral(node) {
1026
+ return node.type === AST_NODE_TYPES7.Literal && typeof node.value === "string";
1027
+ }
1028
+ var requireRegisteredKeysRule = createRule({
1029
+ name: RULE_NAME7,
1030
+ meta: {
1031
+ type: "suggestion",
1032
+ docs: {
1033
+ description: "Require the key/name argument of configured sink APIs (storage, event channels, cache keys) to be an imported constant from a registry module, not a raw string literal."
1034
+ },
1035
+ schema: [optionSchema6],
1036
+ messages: {
1037
+ unregisteredKey: "Pass an imported key constant to `{{callee}}`, not the raw string {{value}}{{registryHint}}. Raw string keys drift out of sync across call sites."
1038
+ }
1039
+ },
1040
+ defaultOptions: [{ sinks: [] }],
1041
+ create(context, [options]) {
1042
+ const sinks = options.sinks ?? [];
1043
+ if (sinks.length === 0) {
1044
+ return {};
1045
+ }
1046
+ const sinkMap = /* @__PURE__ */ new Map();
1047
+ for (const sink of sinks) {
1048
+ const existing = sinkMap.get(sink.callee);
1049
+ if (existing) {
1050
+ existing.add(sink.argIndex);
1051
+ } else {
1052
+ sinkMap.set(sink.callee, /* @__PURE__ */ new Set([sink.argIndex]));
1053
+ }
1054
+ }
1055
+ const registry = options.registry;
1056
+ const registryHint = registry ? ` (import it from '${registry}')` : "";
1057
+ return {
1058
+ CallExpression(node) {
1059
+ const path2 = calleePath2(node.callee);
1060
+ if (path2 === null) {
1061
+ return;
1062
+ }
1063
+ const indexes = sinkMap.get(path2);
1064
+ if (indexes === void 0) {
1065
+ return;
1066
+ }
1067
+ for (const index of indexes) {
1068
+ const arg = node.arguments[index];
1069
+ if (arg !== void 0 && isStringLiteral(arg)) {
1070
+ context.report({
1071
+ node: arg,
1072
+ messageId: "unregisteredKey",
1073
+ data: { callee: path2, value: `'${arg.value}'`, registryHint }
1074
+ });
1075
+ }
1076
+ }
1077
+ }
1078
+ };
1079
+ }
1080
+ });
1081
+
1082
+ // src/rules/require-schema-parse-at-boundary.ts
1083
+ import { AST_NODE_TYPES as AST_NODE_TYPES8 } from "@typescript-eslint/utils";
1084
+ var RULE_NAME8 = "require-schema-parse-at-boundary";
1085
+ function isJsonParseCall(node) {
1086
+ return node.type === AST_NODE_TYPES8.CallExpression && node.callee.type === AST_NODE_TYPES8.MemberExpression && !node.callee.computed && node.callee.object.type === AST_NODE_TYPES8.Identifier && node.callee.object.name === "JSON" && node.callee.property.type === AST_NODE_TYPES8.Identifier && node.callee.property.name === "parse";
1087
+ }
1088
+ function isAwaitJsonCall(node) {
1089
+ if (node.type !== AST_NODE_TYPES8.AwaitExpression) {
1090
+ return false;
1091
+ }
1092
+ const call = node.argument;
1093
+ return call.type === AST_NODE_TYPES8.CallExpression && call.arguments.length === 0 && call.callee.type === AST_NODE_TYPES8.MemberExpression && !call.callee.computed && call.callee.property.type === AST_NODE_TYPES8.Identifier && call.callee.property.name === "json";
1094
+ }
1095
+ function isShapeClaim(annotation) {
1096
+ if (annotation.type === AST_NODE_TYPES8.TSArrayType) {
1097
+ return true;
1098
+ }
1099
+ if (annotation.type === AST_NODE_TYPES8.TSTypeReference) {
1100
+ return !(annotation.typeName.type === AST_NODE_TYPES8.Identifier && annotation.typeName.name === "const");
1101
+ }
1102
+ return false;
1103
+ }
1104
+ var requireSchemaParseAtBoundaryRule = createRule({
1105
+ name: RULE_NAME8,
1106
+ meta: {
1107
+ type: "problem",
1108
+ docs: {
1109
+ description: "Disallow asserting external boundary data with `as T` instead of parsing it at runtime. Flags `JSON.parse(...) as T` and `(await res.json()) as T`; use a zod/valibot parse."
1110
+ },
1111
+ schema: [],
1112
+ messages: {
1113
+ castedBoundaryData: "Boundary data is asserted with `as` here, not parsed. A cast is unchecked \u2014 validate this with a runtime schema (e.g. `Schema.parse(...)`) so a wire-shape change fails loudly."
1114
+ }
1115
+ },
1116
+ defaultOptions: [],
1117
+ create(context) {
1118
+ return {
1119
+ TSAsExpression(node) {
1120
+ if (!isShapeClaim(node.typeAnnotation)) {
1121
+ return;
1122
+ }
1123
+ const expr = node.expression;
1124
+ if (isJsonParseCall(expr) || isAwaitJsonCall(expr)) {
1125
+ context.report({ node, messageId: "castedBoundaryData" });
1126
+ }
1127
+ }
1128
+ };
1129
+ }
1130
+ });
1131
+
1132
+ // src/rules/restrict-throw-to-taxonomy.ts
1133
+ import { AST_NODE_TYPES as AST_NODE_TYPES9 } from "@typescript-eslint/utils";
1134
+ var RULE_NAME9 = "restrict-throw-to-taxonomy";
1135
+ var DEFAULT_ALLOW = ["Error"];
1136
+ var optionSchema7 = {
1137
+ type: "object",
1138
+ additionalProperties: false,
1139
+ properties: {
1140
+ allow: {
1141
+ type: "array",
1142
+ items: { type: "string" },
1143
+ uniqueItems: true
1144
+ }
1145
+ }
1146
+ };
1147
+ function constructorSimpleName2(node) {
1148
+ const callee = node.callee;
1149
+ if (callee.type === AST_NODE_TYPES9.Identifier) {
1150
+ return callee.name;
1151
+ }
1152
+ if (callee.type === AST_NODE_TYPES9.MemberExpression && !callee.computed && callee.property.type === AST_NODE_TYPES9.Identifier) {
1153
+ return callee.property.name;
1154
+ }
1155
+ return null;
1156
+ }
1157
+ function isNonErrorValue(node) {
1158
+ return node.type === AST_NODE_TYPES9.Literal || node.type === AST_NODE_TYPES9.TemplateLiteral || node.type === AST_NODE_TYPES9.ObjectExpression || node.type === AST_NODE_TYPES9.ArrayExpression;
1159
+ }
1160
+ var restrictThrowToTaxonomyRule = createRule({
1161
+ name: RULE_NAME9,
1162
+ meta: {
1163
+ type: "problem",
1164
+ docs: {
1165
+ description: "Restrict `throw` to an approved error taxonomy. Flags throwing a non-allowlisted error class and throwing a non-Error value (string, object, number, ...)."
1166
+ },
1167
+ schema: [optionSchema7],
1168
+ messages: {
1169
+ disallowedErrorClass: "Throw an error from your taxonomy, not `{{name}}`. Allowed: {{allowed}}. Add `{{name}}` to the `allow` option if it belongs to your taxonomy.",
1170
+ nonErrorThrow: "Throw an Error from your taxonomy, not a bare {{kind}} value. A non-Error throw carries no stack or cause."
1171
+ }
1172
+ },
1173
+ defaultOptions: [{ allow: [...DEFAULT_ALLOW] }],
1174
+ create(context, [options]) {
1175
+ const allow = new Set(options.allow ?? DEFAULT_ALLOW);
1176
+ const allowedList = [...allow].join(", ");
1177
+ return {
1178
+ ThrowStatement(node) {
1179
+ const arg = node.argument;
1180
+ if (arg.type === AST_NODE_TYPES9.NewExpression) {
1181
+ const name = constructorSimpleName2(arg);
1182
+ if (name !== null && !allow.has(name)) {
1183
+ context.report({
1184
+ node: arg,
1185
+ messageId: "disallowedErrorClass",
1186
+ data: { name, allowed: allowedList }
1187
+ });
1188
+ }
1189
+ return;
1190
+ }
1191
+ if (isNonErrorValue(arg)) {
1192
+ const kind = arg.type === AST_NODE_TYPES9.ObjectExpression ? "object" : arg.type === AST_NODE_TYPES9.ArrayExpression ? "array" : "literal";
1193
+ context.report({ node: arg, messageId: "nonErrorThrow", data: { kind } });
1194
+ }
1195
+ }
1196
+ };
1197
+ }
1198
+ });
1199
+
1200
+ // src/rules/schema-enum-field-consistency.ts
1201
+ import { AST_NODE_TYPES as AST_NODE_TYPES10 } from "@typescript-eslint/utils";
1202
+ var RULE_NAME10 = "schema-enum-field-consistency";
1203
+ var MODIFIERS = /* @__PURE__ */ new Set([
1204
+ "optional",
1205
+ "nullable",
1206
+ "nullish",
1207
+ "default",
1208
+ "prefault",
1209
+ "catch",
1210
+ "describe",
1211
+ "meta",
1212
+ "readonly"
1213
+ ]);
1214
+ var ENUM_PRESERVING = /* @__PURE__ */ new Set(["extract", "exclude"]);
1215
+ var OUTPUT_CHANGING = /* @__PURE__ */ new Set(["pipe", "transform"]);
1216
+ var OBJECT_FACTORIES = /* @__PURE__ */ new Set(["object", "strictObject", "looseObject"]);
1217
+ var SHAPE_EXTENDERS = /* @__PURE__ */ new Set(["extend", "safeExtend"]);
1218
+ var ENUM_FACTORIES = /* @__PURE__ */ new Set(["enum", "nativeEnum"]);
1219
+ var UNION = /* @__PURE__ */ new Set(["union"]);
1220
+ var LITERAL = /* @__PURE__ */ new Set(["literal"]);
1221
+ var STRING = /* @__PURE__ */ new Set(["string"]);
1222
+ var optionSchema8 = {
1223
+ type: "object",
1224
+ additionalProperties: false,
1225
+ properties: {
1226
+ zodIdentifiers: { type: "array", items: { type: "string" }, uniqueItems: true },
1227
+ ignoreFields: { type: "array", items: { type: "string" }, uniqueItems: true },
1228
+ enumIdentifierPattern: { type: "string" }
1229
+ }
1230
+ };
1231
+ var OTHER = { kind: "other" };
1232
+ function isEnumOccurrence(occurrence) {
1233
+ return occurrence.kind.kind === "enum";
1234
+ }
1235
+ function methodCall(node) {
1236
+ if (node.type !== AST_NODE_TYPES10.CallExpression) return null;
1237
+ const callee = node.callee;
1238
+ if (callee.type !== AST_NODE_TYPES10.MemberExpression || callee.computed) return null;
1239
+ if (callee.property.type !== AST_NODE_TYPES10.Identifier) return null;
1240
+ return { receiver: callee.object, method: callee.property.name, call: node };
1241
+ }
1242
+ function propertyName(property) {
1243
+ if (property.computed) return null;
1244
+ if (property.key.type === AST_NODE_TYPES10.Identifier) return property.key.name;
1245
+ if (property.key.type === AST_NODE_TYPES10.Literal && typeof property.key.value === "string") {
1246
+ return property.key.value;
1247
+ }
1248
+ return null;
1249
+ }
1250
+ var schemaEnumFieldConsistencyRule = createRule({
1251
+ name: RULE_NAME10,
1252
+ meta: {
1253
+ type: "problem",
1254
+ docs: {
1255
+ description: "Disallow a zod field that is an enum in one object schema of a module from being `z.string()` in another, which widens the wire type every consumer then narrows by hand."
1256
+ },
1257
+ schema: [optionSchema8],
1258
+ messages: {
1259
+ widenedEnumField: "`{{field}}` is `z.string()` here but an enum on line {{line}} of this file. The widened type leaks `string` to every consumer, which then has to narrow or cast it. Use {{suggestion}} instead (and, if the stored data is free text, migrate it first)."
1260
+ }
1261
+ },
1262
+ defaultOptions: [{ zodIdentifiers: ["z"], ignoreFields: [] }],
1263
+ create(context, [options]) {
1264
+ const zodIdentifiers = new Set(options.zodIdentifiers ?? ["z"]);
1265
+ const ignoreFields = new Set(options.ignoreFields ?? []);
1266
+ const enumIdentifierPattern = options.enumIdentifierPattern === void 0 ? null : new RegExp(options.enumIdentifierPattern, "u");
1267
+ const sourceCode = context.sourceCode;
1268
+ const fields = /* @__PURE__ */ new Map();
1269
+ function isZodCall(node, names) {
1270
+ const call = methodCall(node);
1271
+ return call !== null && call.receiver.type === AST_NODE_TYPES10.Identifier && zodIdentifiers.has(call.receiver.name) && names.has(call.method);
1272
+ }
1273
+ function resolveVariable(identifier) {
1274
+ let scope = sourceCode.getScope(identifier);
1275
+ while (scope !== null) {
1276
+ const variable = scope.set.get(identifier.name);
1277
+ if (variable !== void 0) return variable;
1278
+ scope = scope.upper;
1279
+ }
1280
+ return null;
1281
+ }
1282
+ function identifierIsEnum(identifier, seen) {
1283
+ const definition = resolveVariable(identifier)?.defs[0];
1284
+ if (definition === void 0) return false;
1285
+ if (definition.type === "ImportBinding") {
1286
+ return enumIdentifierPattern !== null && enumIdentifierPattern.test(identifier.name);
1287
+ }
1288
+ if (definition.type !== "Variable") return false;
1289
+ const init = definition.node.init;
1290
+ if (init === null || seen.has(init)) return false;
1291
+ return classify(init, /* @__PURE__ */ new Set([...seen, init])).kind === "enum";
1292
+ }
1293
+ function isLiteralUnion(node) {
1294
+ if (!isZodCall(node, UNION)) return false;
1295
+ const members = node.arguments[0];
1296
+ if (members?.type !== AST_NODE_TYPES10.ArrayExpression || members.elements.length === 0) {
1297
+ return false;
1298
+ }
1299
+ return members.elements.every((element) => element !== null && isZodCall(element, LITERAL));
1300
+ }
1301
+ function isMultiLiteral(node) {
1302
+ if (!isZodCall(node, LITERAL)) return false;
1303
+ const value = node.arguments[0];
1304
+ return value?.type === AST_NODE_TYPES10.ArrayExpression && value.elements.length > 1;
1305
+ }
1306
+ function classify(node, seen) {
1307
+ let current = node;
1308
+ for (; ; ) {
1309
+ const call = methodCall(current);
1310
+ if (call === null || !(MODIFIERS.has(call.method) || ENUM_PRESERVING.has(call.method))) {
1311
+ break;
1312
+ }
1313
+ current = call.receiver;
1314
+ }
1315
+ if (current.type === AST_NODE_TYPES10.Identifier) {
1316
+ return identifierIsEnum(current, seen) ? { kind: "enum", identifier: current.name } : OTHER;
1317
+ }
1318
+ if (isZodCall(current, ENUM_FACTORIES) || isLiteralUnion(current) || isMultiLiteral(current)) {
1319
+ return { kind: "enum", identifier: null };
1320
+ }
1321
+ current = node;
1322
+ for (; ; ) {
1323
+ if (isZodCall(current, STRING)) return { kind: "string" };
1324
+ const call = methodCall(current);
1325
+ if (call === null || OUTPUT_CHANGING.has(call.method)) return OTHER;
1326
+ current = call.receiver;
1327
+ }
1328
+ }
1329
+ function collectShape(shape) {
1330
+ if (shape?.type !== AST_NODE_TYPES10.ObjectExpression) return;
1331
+ for (const property of shape.properties) {
1332
+ if (property.type !== AST_NODE_TYPES10.Property) continue;
1333
+ const name = propertyName(property);
1334
+ if (name === null || ignoreFields.has(name)) continue;
1335
+ const kind = classify(property.value, /* @__PURE__ */ new Set());
1336
+ if (kind.kind === "other") continue;
1337
+ const occurrences = fields.get(name) ?? [];
1338
+ occurrences.push({ property, kind });
1339
+ fields.set(name, occurrences);
1340
+ }
1341
+ }
1342
+ return {
1343
+ CallExpression(node) {
1344
+ if (isZodCall(node, OBJECT_FACTORIES)) {
1345
+ collectShape(node.arguments[0]);
1346
+ return;
1347
+ }
1348
+ const call = methodCall(node);
1349
+ if (call !== null && SHAPE_EXTENDERS.has(call.method)) collectShape(node.arguments[0]);
1350
+ },
1351
+ "Program:exit"() {
1352
+ for (const [field, occurrences] of fields) {
1353
+ const enumOccurrence = occurrences.find(isEnumOccurrence);
1354
+ if (enumOccurrence === void 0) continue;
1355
+ const suggestion = enumOccurrence.kind.identifier === null ? "the same enum schema" : `\`${enumOccurrence.kind.identifier}\``;
1356
+ for (const occurrence of occurrences) {
1357
+ if (occurrence.kind.kind !== "string") continue;
1358
+ context.report({
1359
+ node: occurrence.property,
1360
+ messageId: "widenedEnumField",
1361
+ data: {
1362
+ field,
1363
+ line: String(enumOccurrence.property.loc.start.line),
1364
+ suggestion
1365
+ }
1366
+ });
1367
+ }
1368
+ }
1369
+ }
1370
+ };
1371
+ }
1372
+ });
1373
+
309
1374
  // src/rules/wire-message-naming.ts
310
- var RULE_NAME4 = "wire-message-naming";
1375
+ var RULE_NAME11 = "wire-message-naming";
311
1376
  var DEFAULT_ROLE_SUFFIXES = ["Event", "Command", "Query"];
312
- var optionSchema4 = {
1377
+ var optionSchema9 = {
313
1378
  type: "object",
314
1379
  additionalProperties: false,
315
1380
  properties: {
@@ -350,14 +1415,14 @@ function typeLiteralNode(obj) {
350
1415
  return null;
351
1416
  }
352
1417
  var wireMessageNamingRule = createRule({
353
- name: RULE_NAME4,
1418
+ name: RULE_NAME11,
354
1419
  meta: {
355
1420
  type: "problem",
356
1421
  docs: {
357
1422
  description: "A message-schema const ending in a role suffix (default Event/Command/Query) whose zod object declares `type: z.literal(...)` must set that literal to kebab-case(const name minus its role suffix)."
358
1423
  },
359
1424
  fixable: "code",
360
- schema: [optionSchema4],
1425
+ schema: [optionSchema9],
361
1426
  messages: {
362
1427
  typeMismatch: "Wire `type` literal '{{actual}}' for `{{name}}` must be '{{expected}}' \u2014 kebab-case of the const name minus its role suffix."
363
1428
  }
@@ -393,11 +1458,11 @@ var wireMessageNamingRule = createRule({
393
1458
  });
394
1459
 
395
1460
  // src/rules/zod-schema-naming.ts
396
- var RULE_NAME5 = "zod-schema-naming";
1461
+ var RULE_NAME12 = "zod-schema-naming";
397
1462
  var SCHEMA_NAME = /^[A-Z][A-Za-z0-9]*Schema$/;
398
1463
  var SUFFIX = "Schema";
399
1464
  var DEFAULT_ROLE_SUFFIXES2 = [];
400
- var optionSchema5 = {
1465
+ var optionSchema10 = {
401
1466
  type: "object",
402
1467
  additionalProperties: false,
403
1468
  properties: {
@@ -430,13 +1495,13 @@ function rootIdentifierName(node) {
430
1495
  return null;
431
1496
  }
432
1497
  var zodSchemaNamingRule = createRule({
433
- name: RULE_NAME5,
1498
+ name: RULE_NAME12,
434
1499
  meta: {
435
1500
  type: "problem",
436
1501
  docs: {
437
1502
  description: "Every exported zod schema is a PascalCase const suffixed `Schema`, paired with a same-named inferred type (`export type Foo = z.infer<typeof FooSchema>`)."
438
1503
  },
439
- schema: [optionSchema5],
1504
+ schema: [optionSchema10],
440
1505
  messages: {
441
1506
  schemaNaming: "Exported zod schema `{{name}}` must be a PascalCase const ending in `Schema` (e.g. `FooSchema`).",
442
1507
  missingType: "Schema `{{name}}` has no sibling `export type {{base}} = z.infer<typeof {{name}}>`. Export the inferred type instead of hand-authoring a duplicate."
@@ -492,12 +1557,19 @@ var rules = {
492
1557
  "wire-message-naming": wireMessageNamingRule,
493
1558
  "no-error-stringify": noErrorStringifyRule,
494
1559
  "no-direct-process-env": noDirectProcessEnvRule,
495
- "money-must-be-decimal": moneyMustBeDecimalRule
1560
+ "money-must-be-decimal": moneyMustBeDecimalRule,
1561
+ "require-error-cause": requireErrorCauseRule,
1562
+ "restrict-throw-to-taxonomy": restrictThrowToTaxonomyRule,
1563
+ "require-registered-keys": requireRegisteredKeysRule,
1564
+ "env-var-schema-parity": envVarSchemaParityRule,
1565
+ "require-schema-parse-at-boundary": requireSchemaParseAtBoundaryRule,
1566
+ "schema-enum-field-consistency": schemaEnumFieldConsistencyRule,
1567
+ "fetch-must-check-ok": fetchMustCheckOkRule
496
1568
  };
497
1569
 
498
1570
  // src/index.ts
499
1571
  var NAMESPACE = "noctcore-contracts";
500
- var VERSION = "0.1.0";
1572
+ var VERSION = "0.3.0";
501
1573
  var plugin = {
502
1574
  meta: { name: "@noctcore/eslint-plugin-contracts", version: VERSION },
503
1575
  rules,