@reprova/sdk 0.5.0 → 0.6.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 (148) hide show
  1. package/README.md +75 -0
  2. package/dist/cjs/context.js +28 -0
  3. package/dist/cjs/distributionBaselinePoller.js +87 -0
  4. package/dist/cjs/distributionDrift.js +170 -0
  5. package/dist/cjs/drizzle.js +204 -0
  6. package/dist/cjs/frameworkAdapter.js +2 -0
  7. package/dist/cjs/httpFetch.js +59 -0
  8. package/dist/cjs/idempotency.js +132 -0
  9. package/dist/cjs/idempotencySettingsPoller.js +71 -0
  10. package/dist/cjs/index.js +101 -0
  11. package/dist/cjs/invariantInference.js +90 -0
  12. package/dist/cjs/invariantRules.js +142 -0
  13. package/dist/cjs/knex.js +105 -0
  14. package/dist/cjs/kysely.js +201 -0
  15. package/dist/cjs/legacyPrisma.js +130 -0
  16. package/dist/cjs/mikroorm.js +107 -0
  17. package/dist/cjs/mongoSchemaReporter.js +85 -0
  18. package/dist/cjs/mongoose.js +335 -0
  19. package/dist/cjs/nPlusOne.js +0 -0
  20. package/dist/cjs/nest.js +136 -0
  21. package/dist/cjs/nextjs.js +180 -0
  22. package/dist/cjs/package.json +1 -0
  23. package/dist/cjs/prisma.js +363 -0
  24. package/dist/cjs/proto.gen.js +4 -0
  25. package/dist/cjs/registry.js +100 -0
  26. package/dist/cjs/retryStorm.js +70 -0
  27. package/dist/cjs/rulesPoller.js +77 -0
  28. package/dist/cjs/sdk.js +1231 -0
  29. package/dist/cjs/sequelize.js +146 -0
  30. package/dist/cjs/shape.js +45 -0
  31. package/dist/cjs/stack.js +51 -0
  32. package/dist/cjs/transport.js +71 -0
  33. package/dist/cjs/typeorm.js +231 -0
  34. package/dist/context.d.ts +18 -0
  35. package/dist/context.d.ts.map +1 -1
  36. package/dist/context.js.map +1 -1
  37. package/dist/distributionBaselinePoller.d.ts +36 -0
  38. package/dist/distributionBaselinePoller.d.ts.map +1 -0
  39. package/dist/distributionBaselinePoller.js +84 -0
  40. package/dist/distributionBaselinePoller.js.map +1 -0
  41. package/dist/distributionDrift.d.ts +41 -0
  42. package/dist/distributionDrift.d.ts.map +1 -0
  43. package/dist/distributionDrift.js +161 -0
  44. package/dist/distributionDrift.js.map +1 -0
  45. package/dist/drizzle.d.ts.map +1 -1
  46. package/dist/drizzle.js +10 -4
  47. package/dist/drizzle.js.map +1 -1
  48. package/dist/frameworkAdapter.d.ts +5 -0
  49. package/dist/frameworkAdapter.d.ts.map +1 -0
  50. package/dist/frameworkAdapter.js +2 -0
  51. package/dist/frameworkAdapter.js.map +1 -0
  52. package/dist/httpFetch.d.ts +13 -0
  53. package/dist/httpFetch.d.ts.map +1 -0
  54. package/dist/httpFetch.js +54 -0
  55. package/dist/httpFetch.js.map +1 -0
  56. package/dist/idempotency.d.ts +26 -0
  57. package/dist/idempotency.d.ts.map +1 -0
  58. package/dist/idempotency.js +124 -0
  59. package/dist/idempotency.js.map +1 -0
  60. package/dist/idempotencySettingsPoller.d.ts +23 -0
  61. package/dist/idempotencySettingsPoller.d.ts.map +1 -0
  62. package/dist/idempotencySettingsPoller.js +68 -0
  63. package/dist/idempotencySettingsPoller.js.map +1 -0
  64. package/dist/index.d.ts +33 -8
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +20 -6
  67. package/dist/index.js.map +1 -1
  68. package/dist/invariantInference.d.ts +28 -0
  69. package/dist/invariantInference.d.ts.map +1 -0
  70. package/dist/invariantInference.js +87 -0
  71. package/dist/invariantInference.js.map +1 -0
  72. package/dist/invariantRules.d.ts +16 -0
  73. package/dist/invariantRules.d.ts.map +1 -0
  74. package/dist/invariantRules.js +104 -0
  75. package/dist/invariantRules.js.map +1 -0
  76. package/dist/knex.d.ts +1 -0
  77. package/dist/knex.d.ts.map +1 -1
  78. package/dist/knex.js +19 -0
  79. package/dist/knex.js.map +1 -1
  80. package/dist/kysely.d.ts +1 -0
  81. package/dist/kysely.d.ts.map +1 -1
  82. package/dist/kysely.js +27 -1
  83. package/dist/kysely.js.map +1 -1
  84. package/dist/legacyPrisma.d.ts +6 -0
  85. package/dist/legacyPrisma.d.ts.map +1 -0
  86. package/dist/legacyPrisma.js +128 -0
  87. package/dist/legacyPrisma.js.map +1 -0
  88. package/dist/mikroorm.d.ts +3 -0
  89. package/dist/mikroorm.d.ts.map +1 -0
  90. package/dist/mikroorm.js +104 -0
  91. package/dist/mikroorm.js.map +1 -0
  92. package/dist/mongoSchemaReporter.d.ts +23 -0
  93. package/dist/mongoSchemaReporter.d.ts.map +1 -0
  94. package/dist/mongoSchemaReporter.js +82 -0
  95. package/dist/mongoSchemaReporter.js.map +1 -0
  96. package/dist/mongoose.d.ts +48 -0
  97. package/dist/mongoose.d.ts.map +1 -0
  98. package/dist/mongoose.js +331 -0
  99. package/dist/mongoose.js.map +1 -0
  100. package/dist/nPlusOne.d.ts +52 -0
  101. package/dist/nPlusOne.d.ts.map +1 -0
  102. package/dist/nPlusOne.js +0 -0
  103. package/dist/nPlusOne.js.map +1 -0
  104. package/dist/nest.d.ts +13 -0
  105. package/dist/nest.d.ts.map +1 -1
  106. package/dist/nest.js +39 -0
  107. package/dist/nest.js.map +1 -1
  108. package/dist/nextjs.d.ts +27 -0
  109. package/dist/nextjs.d.ts.map +1 -0
  110. package/dist/nextjs.js +178 -0
  111. package/dist/nextjs.js.map +1 -0
  112. package/dist/prisma.d.ts +21 -5
  113. package/dist/prisma.d.ts.map +1 -1
  114. package/dist/prisma.js +247 -22
  115. package/dist/prisma.js.map +1 -1
  116. package/dist/proto.gen.d.ts +10 -2
  117. package/dist/proto.gen.d.ts.map +1 -1
  118. package/dist/registry.d.ts +1 -0
  119. package/dist/registry.d.ts.map +1 -1
  120. package/dist/registry.js +24 -0
  121. package/dist/registry.js.map +1 -1
  122. package/dist/retryStorm.d.ts +42 -0
  123. package/dist/retryStorm.d.ts.map +1 -0
  124. package/dist/retryStorm.js +65 -0
  125. package/dist/retryStorm.js.map +1 -0
  126. package/dist/rulesPoller.d.ts +26 -0
  127. package/dist/rulesPoller.d.ts.map +1 -0
  128. package/dist/rulesPoller.js +74 -0
  129. package/dist/rulesPoller.js.map +1 -0
  130. package/dist/sdk.d.ts +48 -7
  131. package/dist/sdk.d.ts.map +1 -1
  132. package/dist/sdk.js +619 -47
  133. package/dist/sdk.js.map +1 -1
  134. package/dist/sequelize.d.ts +1 -0
  135. package/dist/sequelize.d.ts.map +1 -1
  136. package/dist/sequelize.js +41 -3
  137. package/dist/sequelize.js.map +1 -1
  138. package/dist/stack.d.ts.map +1 -1
  139. package/dist/stack.js +6 -3
  140. package/dist/stack.js.map +1 -1
  141. package/dist/transport.d.ts.map +1 -1
  142. package/dist/transport.js +2 -1
  143. package/dist/transport.js.map +1 -1
  144. package/dist/typeorm.d.ts +2 -0
  145. package/dist/typeorm.d.ts.map +1 -1
  146. package/dist/typeorm.js +163 -1
  147. package/dist/typeorm.js.map +1 -1
  148. package/package.json +14 -5
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ // Idempotency/consistency checks: two independent, domain-agnostic
3
+ // detectors for "silent 200" bugs, unlike business-invariant rules — neither
4
+ // needs a dashboard-authored condition tree, since neither depends on
5
+ // knowing anything about the app's data model.
6
+ //
7
+ // 1. Idempotency-key mismatch: the app itself asserts two requests are "the
8
+ // same operation" by sending an identical value in a known header (e.g.
9
+ // Stripe-style Idempotency-Key). If the response differs between them,
10
+ // that's unambiguously a bug — no heuristic tuning needed, the app told
11
+ // us these should match.
12
+ // 2. Duplicate-read mismatch: the SAME GET (route + query + caller
13
+ // identity) repeated within a short window returning a different
14
+ // result. Real false-positive risk exists here (data can legitimately
15
+ // change between two reads), so it's scoped tight: a short window turns
16
+ // it into "did two near-simultaneous reads of what should be the same
17
+ // snapshot disagree" (a race-condition/double-fetch signal), not general
18
+ // staleness detection — and it's skipped entirely when no caller
19
+ // identity is available, since an identity-less GET trivially disagrees
20
+ // across different callers.
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.HashMismatchCache = exports.DuplicateReadMismatch = exports.IdempotencyKeyMismatch = exports.DEFAULT_IDEMPOTENCY_SETTINGS = void 0;
23
+ exports.hashResponseBody = hashResponseBody;
24
+ exports.findIdempotencyKey = findIdempotencyKey;
25
+ exports.extractIdentity = extractIdentity;
26
+ const node_crypto_1 = require("node:crypto");
27
+ // Mirrors the control plane's idempotency_settings table defaults — used
28
+ // before the first successful poll, and if polling never succeeds at all
29
+ // (e.g. an older control plane without this endpoint yet).
30
+ exports.DEFAULT_IDEMPOTENCY_SETTINGS = {
31
+ key_check_enabled: true,
32
+ header_names: ['Idempotency-Key', 'X-Idempotency-Key'],
33
+ ttl_ms: 86_400_000,
34
+ max_keys: 100_000,
35
+ duplicate_read_enabled: false,
36
+ duplicate_read_window_ms: 500,
37
+ };
38
+ // One reusable Error class per detector, mirroring BusinessInvariantViolation
39
+ // — per-endpoint identity comes from capture()'s errorType override, not a
40
+ // per-instance subclass, so many distinct endpoints share one class without
41
+ // collapsing into a single issue.
42
+ class IdempotencyKeyMismatch extends Error {
43
+ }
44
+ exports.IdempotencyKeyMismatch = IdempotencyKeyMismatch;
45
+ class DuplicateReadMismatch extends Error {
46
+ }
47
+ exports.DuplicateReadMismatch = DuplicateReadMismatch;
48
+ // Deterministic regardless of key insertion order, so { a:1, b:2 } and
49
+ // { b:2, a:1 } hash identically — otherwise two genuinely-equivalent
50
+ // responses serialized in a different key order would look like a mismatch
51
+ // that isn't one.
52
+ function stableStringify(value) {
53
+ if (value === null || typeof value !== 'object')
54
+ return JSON.stringify(value);
55
+ if (Array.isArray(value))
56
+ return `[${value.map(stableStringify).join(',')}]`;
57
+ const obj = value;
58
+ const keys = Object.keys(obj).sort();
59
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(',')}}`;
60
+ }
61
+ function hashResponseBody(body) {
62
+ return (0, node_crypto_1.createHash)('sha256').update(stableStringify(body)).digest('hex');
63
+ }
64
+ // Bounded signature -> last-seen-hash cache with TTL + LRU eviction, shared
65
+ // by both detectors — the underlying mechanism ("has this signature's hash
66
+ // changed within its window") is identical for both; only what goes into
67
+ // the signature and how long it's remembered differs. Map iteration order
68
+ // is insertion order, and delete-then-reinsert on every access keeps it in
69
+ // least-recently-used order, so eviction always drops the LRU entry.
70
+ class HashMismatchCache {
71
+ maxKeys;
72
+ ttlMs;
73
+ map = new Map();
74
+ constructor(maxKeys, ttlMs) {
75
+ this.maxKeys = maxKeys;
76
+ this.ttlMs = ttlMs;
77
+ }
78
+ // Settings can change on every poll — reconfigure in place rather than
79
+ // recreating the cache, so a TTL/cap edit doesn't silently drop everything
80
+ // currently tracked.
81
+ configure(maxKeys, ttlMs) {
82
+ this.maxKeys = maxKeys;
83
+ this.ttlMs = ttlMs;
84
+ }
85
+ // Records `hash` for `signature`, returning true if a live (non-expired)
86
+ // prior hash for the same signature exists and differs from this one.
87
+ checkAndRecord(signature, hash, now = Date.now()) {
88
+ const existing = this.map.get(signature);
89
+ this.map.delete(signature); // re-inserted below either way, refreshing LRU order
90
+ const mismatch = !!existing && existing.expiresAt > now && existing.hash !== hash;
91
+ this.map.set(signature, { hash, expiresAt: now + this.ttlMs });
92
+ while (this.map.size > this.maxKeys) {
93
+ const oldest = this.map.keys().next().value;
94
+ if (oldest === undefined)
95
+ break;
96
+ this.map.delete(oldest);
97
+ }
98
+ return mismatch;
99
+ }
100
+ get size() {
101
+ return this.map.size;
102
+ }
103
+ }
104
+ exports.HashMismatchCache = HashMismatchCache;
105
+ // Checks each configured header name case-insensitively — Node normalizes
106
+ // incoming header names to lowercase, and captureHeaders (stack.ts) passes
107
+ // that through unchanged, so ctx.request.headers keys are already lowercase.
108
+ function findIdempotencyKey(headers, headerNames) {
109
+ for (const name of headerNames) {
110
+ const value = headers[name.toLowerCase()];
111
+ if (value)
112
+ return value;
113
+ }
114
+ return undefined;
115
+ }
116
+ // A small, conventional set of claim keys checked in order — not exhaustive,
117
+ // just enough to cover common shapes (Auth0/JWT `sub`, a plain `id`,
118
+ // Passport-style `userId`/`user_id`). Returns undefined, never a guess, when
119
+ // none match: duplicate-read detection is skipped entirely without an
120
+ // identity, rather than risk comparing two different callers' data and
121
+ // calling the difference a bug.
122
+ const IDENTITY_CLAIM_KEYS = ['sub', 'id', 'userId', 'user_id'];
123
+ function extractIdentity(authClaims) {
124
+ if (!authClaims)
125
+ return undefined;
126
+ for (const key of IDENTITY_CLAIM_KEYS) {
127
+ const value = authClaims[key];
128
+ if (typeof value === 'string' || typeof value === 'number')
129
+ return String(value);
130
+ }
131
+ return undefined;
132
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IdempotencySettingsPoller = void 0;
4
+ const idempotency_js_1 = require("./idempotency.js");
5
+ const httpFetch_js_1 = require("./httpFetch.js");
6
+ const WARN_INTERVAL_MS = 60_000;
7
+ // Same start()/stop()/setInterval+unref shape as RulesPoller — periodically
8
+ // fetches the tenant's dashboard-configured idempotency-check settings so a
9
+ // toggle flipped in the dashboard reaches a running process without a
10
+ // redeploy. Falls back to DEFAULT_IDEMPOTENCY_SETTINGS before the first
11
+ // successful poll and keeps the last-known-good settings on any failure,
12
+ // never throwing and never leaving the detectors unconfigured.
13
+ class IdempotencySettingsPoller {
14
+ settings = idempotency_js_1.DEFAULT_IDEMPOTENCY_SETTINGS;
15
+ timer = null;
16
+ lastWarnAt = 0;
17
+ endpoint;
18
+ apiKey;
19
+ intervalMs;
20
+ constructor(opts) {
21
+ this.endpoint = opts.endpoint;
22
+ this.apiKey = opts.apiKey;
23
+ this.intervalMs = opts.intervalMs ?? 60_000;
24
+ }
25
+ start() {
26
+ void this.refresh(); // don't wait a full interval before settings are usable
27
+ this.timer = setInterval(() => void this.refresh(), this.intervalMs);
28
+ if (this.timer.unref)
29
+ this.timer.unref(); // don't block process exit
30
+ }
31
+ stop() {
32
+ if (this.timer) {
33
+ clearInterval(this.timer);
34
+ this.timer = null;
35
+ }
36
+ }
37
+ getSettings() {
38
+ return this.settings;
39
+ }
40
+ async refresh() {
41
+ try {
42
+ const res = await (0, httpFetch_js_1.httpFetch)(this.endpoint, {
43
+ headers: { 'X-Reprova-Key': this.apiKey },
44
+ });
45
+ if (!res.ok) {
46
+ this.warnOnce(`reprova: idempotency settings fetch returned ${res.status}`);
47
+ return;
48
+ }
49
+ this.seed((await res.json()));
50
+ }
51
+ catch (err) {
52
+ this.warnOnce(`reprova: idempotency settings fetch error: ${err.message}`);
53
+ }
54
+ }
55
+ // Applies an already-fetched settings object directly, with no network
56
+ // call — used by replay mode to seed a one-time snapshot the CLI fetched
57
+ // ahead of time (there's no live control plane to poll from inside a
58
+ // replayed app), so checkIdempotencyIfAny (sdk.ts) sees exactly the same
59
+ // shape it would from a real refresh().
60
+ seed(settings) {
61
+ this.settings = settings;
62
+ }
63
+ warnOnce(msg) {
64
+ const now = Date.now();
65
+ if (now - this.lastWarnAt > WARN_INTERVAL_MS) {
66
+ this.lastWarnAt = now;
67
+ console.warn(msg);
68
+ }
69
+ }
70
+ }
71
+ exports.IdempotencySettingsPoller = IdempotencySettingsPoller;
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.InferenceReporter = exports.IdempotencySettingsPoller = exports.hashResponseBody = exports.extractIdentity = exports.findIdempotencyKey = exports.DuplicateReadMismatch = exports.IdempotencyKeyMismatch = exports.HashMismatchCache = exports.DEFAULT_IDEMPOTENCY_SETTINGS = exports.RulesPoller = exports.BusinessInvariantViolation = exports.operationMatchesHook = exports.evaluateInvariant = exports.compileInvariant = exports.PotentialRetryStorm = exports.describeRetryStorm = exports.detectRetryStorm = exports.PotentialNPlusOneQuery = exports.describeNPlusOne = exports.detectNPlusOne = exports.createDrizzleFootprint = exports.createDrizzleCache = exports.createDrizzleLogger = exports.createNextAdapter = exports.createNestContextInterceptor = exports.createNestExceptionFilter = exports.readKnexMigrationId = exports.installKnexHooks = exports.MongoSchemaReporter = exports.discoverMongooseSchema = exports.reprovaMongoosePlugin = exports.readMikroOrmMigrationId = exports.installMikroOrmSubscriber = exports.readTypeOrmMigrationId = exports.installTypeOrmQueryLogger = exports.installTypeOrmSubscriber = exports.readSequelizeMigrationId = exports.installSequelizeHooks = exports.readKyselyMigrationId = exports.createKyselyPlugin = exports.instrumentLegacyPrismaClient = exports.recordPrismaOperation = exports.createPrismaExtension = exports.setAuthClaims = exports.currentContext = exports.contextStorage = exports.replayErrorHandler = exports.forwardAsyncErrors = exports.wrapResponse = exports.Reprova = void 0;
18
+ exports.SDK_VERSION = exports.DistributionBaselinePoller = exports.DistributionDriftReporter = exports.extractTopLevelNumericFields = exports.isDrift = exports.updateWelfordState = exports.newWelfordState = exports.WriteCountDrift = exports.ValueDistributionDrift = void 0;
19
+ var sdk_js_1 = require("./sdk.js");
20
+ Object.defineProperty(exports, "Reprova", { enumerable: true, get: function () { return sdk_js_1.Reprova; } });
21
+ Object.defineProperty(exports, "wrapResponse", { enumerable: true, get: function () { return sdk_js_1.wrapResponse; } });
22
+ Object.defineProperty(exports, "forwardAsyncErrors", { enumerable: true, get: function () { return sdk_js_1.forwardAsyncErrors; } });
23
+ Object.defineProperty(exports, "replayErrorHandler", { enumerable: true, get: function () { return sdk_js_1.replayErrorHandler; } });
24
+ var context_js_1 = require("./context.js");
25
+ Object.defineProperty(exports, "contextStorage", { enumerable: true, get: function () { return context_js_1.contextStorage; } });
26
+ Object.defineProperty(exports, "currentContext", { enumerable: true, get: function () { return context_js_1.currentContext; } });
27
+ Object.defineProperty(exports, "setAuthClaims", { enumerable: true, get: function () { return context_js_1.setAuthClaims; } });
28
+ var prisma_js_1 = require("./prisma.js");
29
+ Object.defineProperty(exports, "createPrismaExtension", { enumerable: true, get: function () { return prisma_js_1.createPrismaExtension; } });
30
+ Object.defineProperty(exports, "recordPrismaOperation", { enumerable: true, get: function () { return prisma_js_1.recordPrismaOperation; } });
31
+ var legacyPrisma_js_1 = require("./legacyPrisma.js");
32
+ Object.defineProperty(exports, "instrumentLegacyPrismaClient", { enumerable: true, get: function () { return legacyPrisma_js_1.instrumentLegacyPrismaClient; } });
33
+ var kysely_js_1 = require("./kysely.js");
34
+ Object.defineProperty(exports, "createKyselyPlugin", { enumerable: true, get: function () { return kysely_js_1.createKyselyPlugin; } });
35
+ Object.defineProperty(exports, "readKyselyMigrationId", { enumerable: true, get: function () { return kysely_js_1.readKyselyMigrationId; } });
36
+ var sequelize_js_1 = require("./sequelize.js");
37
+ Object.defineProperty(exports, "installSequelizeHooks", { enumerable: true, get: function () { return sequelize_js_1.installSequelizeHooks; } });
38
+ Object.defineProperty(exports, "readSequelizeMigrationId", { enumerable: true, get: function () { return sequelize_js_1.readSequelizeMigrationId; } });
39
+ var typeorm_js_1 = require("./typeorm.js");
40
+ Object.defineProperty(exports, "installTypeOrmSubscriber", { enumerable: true, get: function () { return typeorm_js_1.installTypeOrmSubscriber; } });
41
+ Object.defineProperty(exports, "installTypeOrmQueryLogger", { enumerable: true, get: function () { return typeorm_js_1.installTypeOrmQueryLogger; } });
42
+ Object.defineProperty(exports, "readTypeOrmMigrationId", { enumerable: true, get: function () { return typeorm_js_1.readTypeOrmMigrationId; } });
43
+ var mikroorm_js_1 = require("./mikroorm.js");
44
+ Object.defineProperty(exports, "installMikroOrmSubscriber", { enumerable: true, get: function () { return mikroorm_js_1.installMikroOrmSubscriber; } });
45
+ Object.defineProperty(exports, "readMikroOrmMigrationId", { enumerable: true, get: function () { return mikroorm_js_1.readMikroOrmMigrationId; } });
46
+ var mongoose_js_1 = require("./mongoose.js");
47
+ Object.defineProperty(exports, "reprovaMongoosePlugin", { enumerable: true, get: function () { return mongoose_js_1.reprovaMongoosePlugin; } });
48
+ Object.defineProperty(exports, "discoverMongooseSchema", { enumerable: true, get: function () { return mongoose_js_1.discoverMongooseSchema; } });
49
+ var mongoSchemaReporter_js_1 = require("./mongoSchemaReporter.js");
50
+ Object.defineProperty(exports, "MongoSchemaReporter", { enumerable: true, get: function () { return mongoSchemaReporter_js_1.MongoSchemaReporter; } });
51
+ var knex_js_1 = require("./knex.js");
52
+ Object.defineProperty(exports, "installKnexHooks", { enumerable: true, get: function () { return knex_js_1.installKnexHooks; } });
53
+ Object.defineProperty(exports, "readKnexMigrationId", { enumerable: true, get: function () { return knex_js_1.readKnexMigrationId; } });
54
+ var nest_js_1 = require("./nest.js");
55
+ Object.defineProperty(exports, "createNestExceptionFilter", { enumerable: true, get: function () { return nest_js_1.createNestExceptionFilter; } });
56
+ Object.defineProperty(exports, "createNestContextInterceptor", { enumerable: true, get: function () { return nest_js_1.createNestContextInterceptor; } });
57
+ var nextjs_js_1 = require("./nextjs.js");
58
+ Object.defineProperty(exports, "createNextAdapter", { enumerable: true, get: function () { return nextjs_js_1.createNextAdapter; } });
59
+ var drizzle_js_1 = require("./drizzle.js");
60
+ Object.defineProperty(exports, "createDrizzleLogger", { enumerable: true, get: function () { return drizzle_js_1.createDrizzleLogger; } });
61
+ Object.defineProperty(exports, "createDrizzleCache", { enumerable: true, get: function () { return drizzle_js_1.createDrizzleCache; } });
62
+ Object.defineProperty(exports, "createDrizzleFootprint", { enumerable: true, get: function () { return drizzle_js_1.createDrizzleFootprint; } });
63
+ var nPlusOne_js_1 = require("./nPlusOne.js");
64
+ Object.defineProperty(exports, "detectNPlusOne", { enumerable: true, get: function () { return nPlusOne_js_1.detectNPlusOne; } });
65
+ Object.defineProperty(exports, "describeNPlusOne", { enumerable: true, get: function () { return nPlusOne_js_1.describeNPlusOne; } });
66
+ Object.defineProperty(exports, "PotentialNPlusOneQuery", { enumerable: true, get: function () { return nPlusOne_js_1.PotentialNPlusOneQuery; } });
67
+ var retryStorm_js_1 = require("./retryStorm.js");
68
+ Object.defineProperty(exports, "detectRetryStorm", { enumerable: true, get: function () { return retryStorm_js_1.detectRetryStorm; } });
69
+ Object.defineProperty(exports, "describeRetryStorm", { enumerable: true, get: function () { return retryStorm_js_1.describeRetryStorm; } });
70
+ Object.defineProperty(exports, "PotentialRetryStorm", { enumerable: true, get: function () { return retryStorm_js_1.PotentialRetryStorm; } });
71
+ var invariantRules_js_1 = require("./invariantRules.js");
72
+ Object.defineProperty(exports, "compileInvariant", { enumerable: true, get: function () { return invariantRules_js_1.compileInvariant; } });
73
+ Object.defineProperty(exports, "evaluateInvariant", { enumerable: true, get: function () { return invariantRules_js_1.evaluateInvariant; } });
74
+ Object.defineProperty(exports, "operationMatchesHook", { enumerable: true, get: function () { return invariantRules_js_1.operationMatchesHook; } });
75
+ Object.defineProperty(exports, "BusinessInvariantViolation", { enumerable: true, get: function () { return invariantRules_js_1.BusinessInvariantViolation; } });
76
+ var rulesPoller_js_1 = require("./rulesPoller.js");
77
+ Object.defineProperty(exports, "RulesPoller", { enumerable: true, get: function () { return rulesPoller_js_1.RulesPoller; } });
78
+ var idempotency_js_1 = require("./idempotency.js");
79
+ Object.defineProperty(exports, "DEFAULT_IDEMPOTENCY_SETTINGS", { enumerable: true, get: function () { return idempotency_js_1.DEFAULT_IDEMPOTENCY_SETTINGS; } });
80
+ Object.defineProperty(exports, "HashMismatchCache", { enumerable: true, get: function () { return idempotency_js_1.HashMismatchCache; } });
81
+ Object.defineProperty(exports, "IdempotencyKeyMismatch", { enumerable: true, get: function () { return idempotency_js_1.IdempotencyKeyMismatch; } });
82
+ Object.defineProperty(exports, "DuplicateReadMismatch", { enumerable: true, get: function () { return idempotency_js_1.DuplicateReadMismatch; } });
83
+ Object.defineProperty(exports, "findIdempotencyKey", { enumerable: true, get: function () { return idempotency_js_1.findIdempotencyKey; } });
84
+ Object.defineProperty(exports, "extractIdentity", { enumerable: true, get: function () { return idempotency_js_1.extractIdentity; } });
85
+ Object.defineProperty(exports, "hashResponseBody", { enumerable: true, get: function () { return idempotency_js_1.hashResponseBody; } });
86
+ var idempotencySettingsPoller_js_1 = require("./idempotencySettingsPoller.js");
87
+ Object.defineProperty(exports, "IdempotencySettingsPoller", { enumerable: true, get: function () { return idempotencySettingsPoller_js_1.IdempotencySettingsPoller; } });
88
+ var invariantInference_js_1 = require("./invariantInference.js");
89
+ Object.defineProperty(exports, "InferenceReporter", { enumerable: true, get: function () { return invariantInference_js_1.InferenceReporter; } });
90
+ var distributionDrift_js_1 = require("./distributionDrift.js");
91
+ Object.defineProperty(exports, "ValueDistributionDrift", { enumerable: true, get: function () { return distributionDrift_js_1.ValueDistributionDrift; } });
92
+ Object.defineProperty(exports, "WriteCountDrift", { enumerable: true, get: function () { return distributionDrift_js_1.WriteCountDrift; } });
93
+ Object.defineProperty(exports, "newWelfordState", { enumerable: true, get: function () { return distributionDrift_js_1.newWelfordState; } });
94
+ Object.defineProperty(exports, "updateWelfordState", { enumerable: true, get: function () { return distributionDrift_js_1.updateWelfordState; } });
95
+ Object.defineProperty(exports, "isDrift", { enumerable: true, get: function () { return distributionDrift_js_1.isDrift; } });
96
+ Object.defineProperty(exports, "extractTopLevelNumericFields", { enumerable: true, get: function () { return distributionDrift_js_1.extractTopLevelNumericFields; } });
97
+ Object.defineProperty(exports, "DistributionDriftReporter", { enumerable: true, get: function () { return distributionDrift_js_1.DistributionDriftReporter; } });
98
+ var distributionBaselinePoller_js_1 = require("./distributionBaselinePoller.js");
99
+ Object.defineProperty(exports, "DistributionBaselinePoller", { enumerable: true, get: function () { return distributionBaselinePoller_js_1.DistributionBaselinePoller; } });
100
+ __exportStar(require("./proto.gen.js"), exports);
101
+ exports.SDK_VERSION = '0.1.0';
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ // Invariant inference, v1 slice: instead of hand-authoring a
3
+ // business-invariant-rule condition tree, passively observe whether a
4
+ // same-named numeric field on a parent row equals the sum of that same
5
+ // field across an already-included to-many relation (Invoice.total vs
6
+ // sum(lineItems.total)) — and report pass/fail counts to the control plane.
7
+ // Once a candidate has been observed enough times at a high enough pass
8
+ // rate, the dashboard offers it as a one-click "confirm" that creates a
9
+ // normal business_invariant_rule — no new evaluation mechanism needed once
10
+ // confirmed, it becomes an ordinary dashboard-authored rule.
11
+ //
12
+ // Deliberately scoped down from the general idea: only the same-field-name
13
+ // shape (not every numeric-field-pair combination, which would be
14
+ // combinatorial and noisy), only `sum` (no `count`), and no dashboard-
15
+ // configurable thresholds yet — see PROGRESS.md for the fuller design this
16
+ // is a first slice of.
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.InferenceReporter = void 0;
19
+ const httpFetch_js_1 = require("./httpFetch.js");
20
+ const WARN_INTERVAL_MS = 60_000;
21
+ // Reports observation deltas, not cumulative totals — the control plane
22
+ // owns the running (checked_count, held_count) per candidate; the SDK only
23
+ // ever adds to it. That makes a dropped/failed flush harmless to correctness
24
+ // (the next flush's delta is unaffected by a prior one failing) at the cost
25
+ // of only ever undercounting on a genuine send failure, never overcounting.
26
+ class InferenceReporter {
27
+ counts = new Map();
28
+ timer = null;
29
+ lastWarnAt = 0;
30
+ endpoint;
31
+ apiKey;
32
+ flushIntervalMs;
33
+ constructor(opts) {
34
+ this.endpoint = opts.endpoint;
35
+ this.apiKey = opts.apiKey;
36
+ this.flushIntervalMs = opts.flushIntervalMs ?? 30_000;
37
+ }
38
+ start() {
39
+ this.timer = setInterval(() => void this.flush(), this.flushIntervalMs);
40
+ if (this.timer.unref)
41
+ this.timer.unref(); // don't block process exit
42
+ }
43
+ stop() {
44
+ if (this.timer) {
45
+ clearInterval(this.timer);
46
+ this.timer = null;
47
+ }
48
+ }
49
+ record(model, field, relation, held) {
50
+ const key = `${model}\x00${field}\x00${relation}`;
51
+ let c = this.counts.get(key);
52
+ if (!c) {
53
+ c = { model, field, relation, checked: 0, held: 0 };
54
+ this.counts.set(key, c);
55
+ }
56
+ c.checked++;
57
+ if (held)
58
+ c.held++;
59
+ }
60
+ async flush() {
61
+ if (this.counts.size === 0)
62
+ return;
63
+ const observations = [...this.counts.values()];
64
+ this.counts.clear();
65
+ try {
66
+ const res = await (0, httpFetch_js_1.httpFetch)(this.endpoint, {
67
+ method: 'POST',
68
+ headers: {
69
+ 'Content-Type': 'application/json',
70
+ 'X-Reprova-Key': this.apiKey,
71
+ },
72
+ body: JSON.stringify({ observations }),
73
+ });
74
+ if (!res.ok) {
75
+ this.warnOnce(`reprova: inferred-invariant observation report returned ${res.status}`);
76
+ }
77
+ }
78
+ catch (err) {
79
+ this.warnOnce(`reprova: inferred-invariant observation report error: ${err.message}`);
80
+ }
81
+ }
82
+ warnOnce(msg) {
83
+ const now = Date.now();
84
+ if (now - this.lastWarnAt > WARN_INTERVAL_MS) {
85
+ this.lastWarnAt = now;
86
+ console.warn(msg);
87
+ }
88
+ }
89
+ }
90
+ exports.InferenceReporter = InferenceReporter;
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ // Dashboard-authored business-invariant rules: CEL (Common Expression
3
+ // Language) expressions evaluated in-process against real Prisma query
4
+ // results (not just the footprint's PKs/counts — actual field values), so
5
+ // bugs that never throw and never return a 5xx (an invoice total that
6
+ // silently drifted from its line items, negative inventory, an order marked
7
+ // shipped with no shipment row) can still produce a reproduction.
8
+ //
9
+ // Replaces an earlier JSON condition-tree evaluator (see git history) — CEL
10
+ // gives the same same-row-comparison-and-relation-aggregate expressiveness
11
+ // with a much friendlier authoring surface (`total ==
12
+ // lineItems.map(x, x.total).sum()` instead of a nested op/field/right tree),
13
+ // using the actively-maintained, zero-dependency @marcbachmann/cel-js.
14
+ //
15
+ // `expr` is the INVARIANT itself — what should always be true — not the
16
+ // violation condition: a rule fires when `expr` evaluates to exactly
17
+ // `false`. This is the opposite polarity from the old JSON tree (which
18
+ // encoded the violation, e.g. `not: eq(...)`), but reads naturally in CEL
19
+ // and matches how an admin would actually describe the rule out loud.
20
+ //
21
+ // A field or relation the app's own query didn't `include` simply isn't a
22
+ // key on the row object, which CEL reports as a runtime "no such attribute"
23
+ // error rather than `false` — evaluateInvariant treats any evaluation error,
24
+ // and any non-boolean result (an author mistake, e.g. forgetting a
25
+ // comparison operator), as "can't evaluate" and skips, never firing a false
26
+ // violation on data the query simply didn't load. Same principle
27
+ // `invariantInference.ts`/`distributionDrift.ts` use: never guess.
28
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
29
+ if (k2 === undefined) k2 = k;
30
+ var desc = Object.getOwnPropertyDescriptor(m, k);
31
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
32
+ desc = { enumerable: true, get: function() { return m[k]; } };
33
+ }
34
+ Object.defineProperty(o, k2, desc);
35
+ }) : (function(o, m, k, k2) {
36
+ if (k2 === undefined) k2 = k;
37
+ o[k2] = m[k];
38
+ }));
39
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
40
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
41
+ }) : function(o, v) {
42
+ o["default"] = v;
43
+ });
44
+ var __importStar = (this && this.__importStar) || (function () {
45
+ var ownKeys = function(o) {
46
+ ownKeys = Object.getOwnPropertyNames || function (o) {
47
+ var ar = [];
48
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
49
+ return ar;
50
+ };
51
+ return ownKeys(o);
52
+ };
53
+ return function (mod) {
54
+ if (mod && mod.__esModule) return mod;
55
+ var result = {};
56
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
57
+ __setModuleDefault(result, mod);
58
+ return result;
59
+ };
60
+ })();
61
+ Object.defineProperty(exports, "__esModule", { value: true });
62
+ exports.BusinessInvariantViolation = void 0;
63
+ exports.operationMatchesHook = operationMatchesHook;
64
+ exports.compileInvariant = compileInvariant;
65
+ exports.evaluateInvariant = evaluateInvariant;
66
+ let envPromise = null;
67
+ function loadEnv() {
68
+ if (!envPromise) {
69
+ envPromise = Promise.resolve().then(() => __importStar(require('@marcbachmann/cel-js'))).then(({ Environment }) => new Environment({ unlistedVariablesAreDyn: true }).registerFunction({
70
+ name: 'sum',
71
+ receiverType: 'list',
72
+ returnType: 'double',
73
+ handler: (list) => list.reduce((a, b) => a + b, 0),
74
+ params: [],
75
+ }))
76
+ .catch(() => null); // can't load the CEL engine at all — every rule silently never fires, never crashes the host app
77
+ }
78
+ return envPromise;
79
+ }
80
+ const READ_OPS = new Set(['findUnique', 'findUniqueOrThrow', 'findFirst', 'findFirstOrThrow', 'findMany']);
81
+ const WRITE_OPS = new Set(['create', 'createMany', 'update', 'updateMany', 'upsert', 'delete', 'deleteMany']);
82
+ // Whether a Prisma operation name belongs to the read/write class a rule's
83
+ // when_hook asks to run on. Anything neither (e.g. $queryRaw, aggregate,
84
+ // count, groupBy) never matches — a rule needs real field values on a real
85
+ // row, which those don't uniformly provide.
86
+ function operationMatchesHook(op, whenHook) {
87
+ return whenHook === 'after_read' ? READ_OPS.has(op) : WRITE_OPS.has(op);
88
+ }
89
+ // Compiled programs are cached by (rule id, expr) so an edited rule
90
+ // recompiles (new expr under the same id misses the cache) while an
91
+ // unchanged one is parsed exactly once for the life of the process — CEL
92
+ // parsing is not free, and this runs on every matching query result.
93
+ // Invalid CEL syntax caches a `null` miss rather than re-attempting the
94
+ // same broken parse on every request. A CEL-engine load failure (see
95
+ // loadEnv's own doc comment) is deliberately NOT cached here — that's a
96
+ // process-wide condition already memoized by envPromise itself, not a
97
+ // per-rule outcome, so a later rule with valid syntax isn't punished by an
98
+ // earlier one's transient load timing.
99
+ const compileCache = new Map();
100
+ async function compileInvariant(rule) {
101
+ const key = `${rule.id}\x00${rule.expr}`;
102
+ if (compileCache.has(key))
103
+ return compileCache.get(key);
104
+ const env = await loadEnv();
105
+ if (!env)
106
+ return null;
107
+ let compiled;
108
+ try {
109
+ compiled = env.parse(rule.expr);
110
+ }
111
+ catch {
112
+ compiled = null; // invalid CEL — never crash the host app on a bad admin-authored rule
113
+ }
114
+ compileCache.set(key, compiled);
115
+ return compiled;
116
+ }
117
+ // Runs a compiled invariant against one row. Returns `false` only for a
118
+ // genuine, cleanly-evaluated violation; `undefined` for anything else
119
+ // (evaluation error from a missing field/relation, or a non-boolean result
120
+ // from a malformed expression) so the caller never mistakes "couldn't tell"
121
+ // for "violated".
122
+ function evaluateInvariant(compiled, row) {
123
+ let result;
124
+ try {
125
+ result = compiled(row);
126
+ }
127
+ catch {
128
+ return undefined;
129
+ }
130
+ return typeof result === 'boolean' ? result : undefined;
131
+ }
132
+ // One reusable Error class for every rule violation. Per-rule identity comes
133
+ // from capture()'s extra.errorType override (see sdk.ts), not from a
134
+ // per-rule subclass — many rules can share this class without collapsing
135
+ // into one issue, since fingerprinting keys off errorType, not the class
136
+ // name, once errorType is supplied explicitly.
137
+ class BusinessInvariantViolation extends Error {
138
+ constructor(message) {
139
+ super(message);
140
+ }
141
+ }
142
+ exports.BusinessInvariantViolation = BusinessInvariantViolation;
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installKnexHooks = installKnexHooks;
4
+ exports.readKnexMigrationId = readKnexMigrationId;
5
+ const context_js_1 = require("./context.js");
6
+ const OP_BY_METHOD = {
7
+ select: 'select',
8
+ first: 'select',
9
+ pluck: 'select',
10
+ insert: 'insert',
11
+ update: 'update',
12
+ del: 'delete',
13
+ delete: 'delete',
14
+ };
15
+ function isRowArray(v) {
16
+ return Array.isArray(v) && v.length > 0 && typeof v[0] === 'object' && v[0] !== null;
17
+ }
18
+ function distinct(values) {
19
+ const seen = new Set();
20
+ const out = [];
21
+ for (const v of values) {
22
+ if (!seen.has(v)) {
23
+ seen.add(v);
24
+ out.push(v);
25
+ }
26
+ }
27
+ return out;
28
+ }
29
+ function extractRowPks(rows, pkCols) {
30
+ if (pkCols.length === 0)
31
+ return [];
32
+ const out = [];
33
+ for (const row of rows) {
34
+ const vals = pkCols.map((c) => row[c]);
35
+ if (vals.some((v) => v === undefined || v === null))
36
+ continue;
37
+ out.push(vals.map((v) => String(v)).join(':'));
38
+ }
39
+ return distinct(out);
40
+ }
41
+ // Installs a footprint listener on a Knex instance. Also covers Objection.js
42
+ // models bound to this same instance.
43
+ function installKnexHooks(knex, opts = {}) {
44
+ const primaryKey = opts.primaryKey ?? (() => ['id']);
45
+ try {
46
+ knex.on('query-response', (response, obj, builder) => {
47
+ try {
48
+ const ctx = context_js_1.contextStorage.getStore();
49
+ if (!ctx)
50
+ return;
51
+ const method = obj?.method ?? builder?._method;
52
+ const op = method ? OP_BY_METHOD[method] : undefined;
53
+ if (!op)
54
+ return; // schema/DDL/raw — skip
55
+ const table = builder?._single?.table;
56
+ if (typeof table !== 'string' || !table)
57
+ return;
58
+ let pks = [];
59
+ let count = 0;
60
+ if (isRowArray(response)) {
61
+ pks = extractRowPks(response, primaryKey(table));
62
+ count = response.length;
63
+ }
64
+ else if (Array.isArray(response)) {
65
+ // Primitive array — insert returning ids (partial on sqlite/mysql).
66
+ if (op === 'insert')
67
+ pks = distinct(response.filter((v) => v != null).map((v) => String(v)));
68
+ count = response.length;
69
+ }
70
+ else if (typeof response === 'number') {
71
+ count = response; // affected-row count from update/delete
72
+ }
73
+ const entry = {
74
+ model: table,
75
+ op,
76
+ pks,
77
+ count,
78
+ note: count === 0 ? 'returned 0 rows' : undefined,
79
+ resolved: true,
80
+ };
81
+ ctx.footprint.push(entry);
82
+ }
83
+ catch { /* never break the query pipeline */ }
84
+ });
85
+ }
86
+ catch { /* never throw from install */ }
87
+ }
88
+ // Reads the latest applied migration name from Knex's own migration tracking
89
+ // table (created automatically by `knex migrate:latest` / `knex.migrate`) —
90
+ // 'id' and 'name' are Knex's own fixed column names; migrationTableName
91
+ // mirrors the one config option (`migrations.tableName`) apps can change it
92
+ // with, default 'knex_migrations'. Best-effort, same contract as
93
+ // instrumentPrisma's _prisma_migrations read: an absent table or any query
94
+ // error resolves to 'unknown', never throws. Wire it up with:
95
+ // sdk.registerFrameworkAdapter({ resolveMigrationId: () => readKnexMigrationId(knex) })
96
+ async function readKnexMigrationId(knex, migrationTableName = 'knex_migrations') {
97
+ try {
98
+ const row = await knex(migrationTableName).orderBy('id', 'desc').first('name');
99
+ const name = row?.name;
100
+ return name || 'unknown';
101
+ }
102
+ catch {
103
+ return 'unknown';
104
+ }
105
+ }