@blamejs/blamejs-shop 0.0.70 → 0.0.75

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 (46) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/lib/announcement-bar.js +753 -0
  3. package/lib/banner-ab-tests.js +806 -0
  4. package/lib/bin-locations.js +791 -0
  5. package/lib/blog-articles.js +1173 -0
  6. package/lib/carrier-accounts.js +805 -0
  7. package/lib/cart-recovery.js +1133 -0
  8. package/lib/category-navigation.js +934 -0
  9. package/lib/consent-ledger.js +539 -0
  10. package/lib/customer-impersonation.js +743 -0
  11. package/lib/customer-merge.js +879 -0
  12. package/lib/demand-forecast.js +1121 -0
  13. package/lib/dispute-resolution.js +886 -0
  14. package/lib/email-ab-tests.js +918 -0
  15. package/lib/email-engagement-score.js +649 -0
  16. package/lib/event-log.js +713 -0
  17. package/lib/fulfillment-sla.js +791 -0
  18. package/lib/index.js +42 -0
  19. package/lib/inventory-audits.js +852 -0
  20. package/lib/line-gift-wrap.js +430 -0
  21. package/lib/loyalty-earn-rules.js +786 -0
  22. package/lib/marketing-budget.js +792 -0
  23. package/lib/operator-activity-feed.js +977 -0
  24. package/lib/operator-approvals.js +942 -0
  25. package/lib/operator-help-center.js +1020 -0
  26. package/lib/operator-inbox.js +889 -0
  27. package/lib/operator-sessions.js +701 -0
  28. package/lib/order-exchanges.js +602 -0
  29. package/lib/product-compare.js +804 -0
  30. package/lib/pwa-manifest.js +1005 -0
  31. package/lib/referral-leaderboard.js +612 -0
  32. package/lib/sales-tax-filings.js +807 -0
  33. package/lib/search-ranking.js +859 -0
  34. package/lib/shipping-insurance.js +757 -0
  35. package/lib/shrinkage-report.js +1182 -0
  36. package/lib/sidebar-widgets.js +952 -0
  37. package/lib/smart-restocking.js +1048 -0
  38. package/lib/split-shipments.js +7 -1
  39. package/lib/stock-receipts.js +834 -0
  40. package/lib/subscription-analytics.js +1032 -0
  41. package/lib/suggestion-box.js +921 -0
  42. package/lib/tax-remittance.js +625 -0
  43. package/lib/vendor-invoices.js +1021 -0
  44. package/lib/winback-campaigns.js +1350 -0
  45. package/lib/wishlist-digest.js +1133 -0
  46. package/package.json +1 -1
@@ -0,0 +1,713 @@
1
+ "use strict";
2
+ /**
3
+ * @module shop.eventLog
4
+ * @title Event log — universal append-only application event stream
5
+ *
6
+ * @intro
7
+ * Cross-cutting record of "something happened" across the
8
+ * application — domain event published, background job dispatched,
9
+ * cache invalidated, webhook received, feature flag flipped, secret
10
+ * rotated. Distinct from:
11
+ *
12
+ * analytics — customer behavioural events.
13
+ * operatorAuditLog — cryptographically chained operator
14
+ * mutations.
15
+ * errorLog — HTTP 4xx/5xx response events.
16
+ *
17
+ * `eventLog` is the catch-all stream that doesn't fit those three
18
+ * shapes: anything an operator might want to grep across after the
19
+ * fact when reconstructing what the system did. Every row is keyed
20
+ * on a compact set of dimensions (`kind`, `subject_kind`+
21
+ * `subject_id`, `actor_kind`+`actor_id`, `severity`, `source`,
22
+ * `occurred_at`) so the read paths stay deterministic; the optional
23
+ * `payload` blob carries forensic detail that's never indexed.
24
+ *
25
+ * **Drop-silent on bad input — by design.** `recordEvent` is wired
26
+ * directly into every callsite that emits an event; throwing inside
27
+ * it would crash whichever request / job / hook triggered the
28
+ * write. The write side resolves every malformed input to a silent
29
+ * drop and returns `{ dropped: true, reason }`. Read-side methods
30
+ * (`query`, `tail`, `metricsForKind`, `topKinds`, `purgeOlderThan`)
31
+ * THROW — those are dashboard / operator-tool entry points with a
32
+ * human on the other end of the stack trace.
33
+ *
34
+ * Surface:
35
+ *
36
+ * recordEvent({
37
+ * kind, subject_kind, subject_id,
38
+ * actor_kind?, actor_id?,
39
+ * payload?, severity?, source?,
40
+ * })
41
+ * → { id, occurred_at } on success
42
+ * → { dropped: true, reason } on bad input (no throw)
43
+ *
44
+ * query({
45
+ * kind?, subject?, actor?, severity_min?,
46
+ * from?, to?, cursor?, limit?,
47
+ * })
48
+ * → { rows, next_cursor }
49
+ * HMAC-tagged opaque cursor over (occurred_at, id) — newest
50
+ * first. `subject` is `{ kind, id }`; `actor` is `{ kind?, id }`
51
+ * (id alone is sufficient because actor_id is independently
52
+ * indexed). `severity_min` is one of debug/info/warning/critical
53
+ * and includes every rung at or above it.
54
+ *
55
+ * tail({ poll_ms?, max_events? })
56
+ * → { rows, observed_at }
57
+ * Snapshot read of the most recent rows for the operator
58
+ * tail-style monitoring console. Not a streaming subscription —
59
+ * the caller polls on its own schedule (`poll_ms` is a hint the
60
+ * UI consumes, not a side effect the primitive enforces).
61
+ *
62
+ * metricsForKind({ kind, from, to })
63
+ * → { kind, total, by_severity, by_source, first_at, last_at }
64
+ * Roll-up for a single kind across a window. `by_severity` and
65
+ * `by_source` are flat `{ <bucket>: count }` maps.
66
+ *
67
+ * topKinds({ from, to, limit })
68
+ * → [{ kind, count }]
69
+ * Most-frequent kinds across the window, count DESC then kind
70
+ * ASC for deterministic ordering.
71
+ *
72
+ * purgeOlderThan({ days, exclude_critical? })
73
+ * → { deleted }
74
+ * Retention sweep. Drops every row whose `occurred_at` is older
75
+ * than `now - days * 86400000`. `exclude_critical: true`
76
+ * preserves critical rows past the retention window so they
77
+ * remain available for after-the-fact incident review.
78
+ *
79
+ * Composition:
80
+ * - `b.uuid.v7` — every row id, sortable by insertion
81
+ * order.
82
+ * - `b.pagination.encodeCursor` / `.decodeCursor` — opaque HMAC-
83
+ * tagged keyset cursor over
84
+ * (occurred_at, id).
85
+ *
86
+ * @primitive eventLog
87
+ * @related shop.analytics, shop.operatorAuditLog, shop.errorLog
88
+ */
89
+
90
+ var bShop;
91
+ function _b() {
92
+ if (!bShop) bShop = require("./index");
93
+ return bShop.framework;
94
+ }
95
+
96
+ // ---- constants ---------------------------------------------------------
97
+
98
+ var SEVERITIES = ["debug", "info", "warning", "critical"];
99
+
100
+ // String length bounds. The dimensions are operator-grepped; oversize
101
+ // inputs are nearly always producer bugs that, if persisted, would
102
+ // either truncate-to-garbage or break the index scan path. The drop-
103
+ // silent gate refuses rather than mangles.
104
+ var MAX_KIND_LEN = 128;
105
+ var MAX_SUBJECT_KIND_LEN = 64;
106
+ var MAX_SUBJECT_ID_LEN = 128;
107
+ var MAX_ACTOR_KIND_LEN = 64;
108
+ var MAX_ACTOR_ID_LEN = 128;
109
+ var MAX_SOURCE_LEN = 128;
110
+ var MAX_PAYLOAD_BYTES = 64 * 1024;
111
+
112
+ var MAX_LIST_LIMIT = 500;
113
+ var DEFAULT_LIST_LIMIT = 100;
114
+ var MAX_TAIL_EVENTS = 500;
115
+ var DEFAULT_TAIL_EVENTS = 50;
116
+ var MIN_POLL_MS = 250;
117
+ var DEFAULT_POLL_MS = 2000;
118
+ var MAX_POLL_MS = 60000;
119
+ var MAX_PURGE_DAYS = 3650;
120
+
121
+ // Identifier shapes — kind / subject_kind / actor_kind / source share
122
+ // the snake-case-plus-dot vocabulary every other primitive in the
123
+ // framework already uses. Concrete ids (subject_id, actor_id) carry
124
+ // a wider character class because the upstream producer might hand
125
+ // in a UUID, a slug, a hex digest, or a CDN-cache key.
126
+ var KIND_RE = /^[a-z][a-z0-9._-]{0,127}$/;
127
+ var ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:\-]{0,127}$/;
128
+ var SOURCE_RE = /^[a-z0-9][a-z0-9._\-/]{0,127}$/;
129
+
130
+ // Cursor order key matches the (occurred_at DESC, id DESC) keyset
131
+ // the query scan reads. Encoded into every cursor so a cursor minted
132
+ // by `query` can't be replayed against a method with a different
133
+ // order key (HMAC verification rejects the orderKey mismatch).
134
+ var CURSOR_ORDER_KEY = ["occurred_at", "id"];
135
+
136
+ // ---- monotonic clock ---------------------------------------------------
137
+ //
138
+ // Wall-clock can stall on a hot loop (multiple recordEvent calls in
139
+ // the same millisecond) and on coarse-grained virtualised hosts. The
140
+ // monotonic shim keeps every per-factory `occurred_at` strictly
141
+ // increasing — every subsequent call observes a timestamp at least
142
+ // 1ms greater than the previous one. Sibling primitives use the same
143
+ // shape; the (occurred_at, id) keyset cursor + the topKinds / tail
144
+ // scans rely on strict monotonicity to break ties deterministically.
145
+
146
+ var _lastTs = 0;
147
+ function _now() {
148
+ var t = Date.now();
149
+ if (t <= _lastTs) { t = _lastTs + 1; }
150
+ _lastTs = t;
151
+ return t;
152
+ }
153
+
154
+ // ---- write-side drop-silent helpers ------------------------------------
155
+
156
+ function _isInt(n) { return typeof n === "number" && Number.isInteger(n); }
157
+ function _isNonNegative(n) { return _isInt(n) && n >= 0; }
158
+ function _isPositive(n) { return _isInt(n) && n > 0; }
159
+
160
+ function _isBoundedKind(v, max, re) {
161
+ if (typeof v !== "string") return false;
162
+ if (v.length === 0 || v.length > max) return false;
163
+ return re.test(v);
164
+ }
165
+
166
+ function _isOptionalBoundedKind(v, max, re) {
167
+ if (v == null) return true;
168
+ return _isBoundedKind(v, max, re);
169
+ }
170
+
171
+ function _isSeverity(v) {
172
+ return typeof v === "string" && SEVERITIES.indexOf(v) !== -1;
173
+ }
174
+
175
+ // ---- read-side validators (THROW — operator-facing) --------------------
176
+
177
+ function _epochMsOpt(n, label) {
178
+ if (n == null) return null;
179
+ if (!_isNonNegative(n)) {
180
+ throw new TypeError("eventLog: " + label + " must be a non-negative integer (epoch-ms) when provided");
181
+ }
182
+ return n;
183
+ }
184
+
185
+ function _limit(n, label, max, def) {
186
+ if (n == null) return def;
187
+ if (!_isInt(n) || n < 1 || n > max) {
188
+ throw new TypeError("eventLog: " + label + " must be an integer in [1, " + max + "]");
189
+ }
190
+ return n;
191
+ }
192
+
193
+ function _severityMin(s) {
194
+ if (s == null) return null;
195
+ if (!_isSeverity(s)) {
196
+ throw new TypeError("eventLog: severity_min must be one of " + SEVERITIES.join(", "));
197
+ }
198
+ return s;
199
+ }
200
+
201
+ function _readKind(v, label) {
202
+ if (typeof v !== "string" || !v.length || v.length > MAX_KIND_LEN || !KIND_RE.test(v)) {
203
+ throw new TypeError("eventLog: " + label + " must match /[a-z][a-z0-9._-]*/ (≤ " + MAX_KIND_LEN + " chars)");
204
+ }
205
+ return v;
206
+ }
207
+
208
+ function _readSubject(v) {
209
+ if (v == null) return null;
210
+ if (typeof v !== "object") {
211
+ throw new TypeError("eventLog: subject must be an object { kind, id } when provided");
212
+ }
213
+ if (typeof v.kind !== "string" || !v.kind.length || v.kind.length > MAX_SUBJECT_KIND_LEN || !KIND_RE.test(v.kind)) {
214
+ throw new TypeError("eventLog: subject.kind must match /[a-z][a-z0-9._-]*/ (≤ " + MAX_SUBJECT_KIND_LEN + " chars)");
215
+ }
216
+ if (typeof v.id !== "string" || !v.id.length || v.id.length > MAX_SUBJECT_ID_LEN || !ID_RE.test(v.id)) {
217
+ throw new TypeError("eventLog: subject.id must match /[A-Za-z0-9][A-Za-z0-9._:-]*/ (≤ " + MAX_SUBJECT_ID_LEN + " chars)");
218
+ }
219
+ return { kind: v.kind, id: v.id };
220
+ }
221
+
222
+ function _readActor(v) {
223
+ if (v == null) return null;
224
+ if (typeof v !== "object") {
225
+ throw new TypeError("eventLog: actor must be an object { kind?, id } when provided");
226
+ }
227
+ if (typeof v.id !== "string" || !v.id.length || v.id.length > MAX_ACTOR_ID_LEN || !ID_RE.test(v.id)) {
228
+ throw new TypeError("eventLog: actor.id must match /[A-Za-z0-9][A-Za-z0-9._:-]*/ (≤ " + MAX_ACTOR_ID_LEN + " chars)");
229
+ }
230
+ var out = { id: v.id };
231
+ if (v.kind != null) {
232
+ if (typeof v.kind !== "string" || !v.kind.length || v.kind.length > MAX_ACTOR_KIND_LEN || !KIND_RE.test(v.kind)) {
233
+ throw new TypeError("eventLog: actor.kind must match /[a-z][a-z0-9._-]*/ (≤ " + MAX_ACTOR_KIND_LEN + " chars)");
234
+ }
235
+ out.kind = v.kind;
236
+ }
237
+ return out;
238
+ }
239
+
240
+ // `severity_min` filter expands to the set of rungs at or above the
241
+ // given severity, in the SEVERITIES ordering. critical >= warning >=
242
+ // info >= debug.
243
+ function _severitiesAtOrAbove(min) {
244
+ if (min == null) return null;
245
+ var idx = SEVERITIES.indexOf(min);
246
+ return SEVERITIES.slice(idx);
247
+ }
248
+
249
+ // ---- row hydration ------------------------------------------------------
250
+
251
+ function _hydrate(r) {
252
+ if (!r) return null;
253
+ var payload = null;
254
+ if (r.payload_json != null) {
255
+ try { payload = JSON.parse(r.payload_json); }
256
+ catch (_e) { payload = null; } // drop-silent — corrupted blob still surfaces the row, just without payload
257
+ }
258
+ return {
259
+ id: r.id,
260
+ kind: r.kind,
261
+ subject_kind: r.subject_kind,
262
+ subject_id: r.subject_id,
263
+ actor_kind: r.actor_kind == null ? null : r.actor_kind,
264
+ actor_id: r.actor_id == null ? null : r.actor_id,
265
+ payload: payload,
266
+ severity: r.severity,
267
+ source: r.source == null ? null : r.source,
268
+ occurred_at: Number(r.occurred_at),
269
+ };
270
+ }
271
+
272
+ // ---- factory ------------------------------------------------------------
273
+
274
+ function create(opts) {
275
+ opts = opts || {};
276
+ var query = opts.query;
277
+ if (!query) {
278
+ query = function (sql, params) { return _b().externalDb.query(sql, params); };
279
+ }
280
+
281
+ // HMAC-tagged cursor secret. Production wiring MUST pass an
282
+ // operator-managed secret derived from the deployment's bridge
283
+ // secret (e.g. `b.crypto.namespaceHash("event-log-cursor", ...)`).
284
+ // Dev fallback is a stable placeholder so the suite can run against
285
+ // an in-memory factory without pre-arranged secrets.
286
+ if (typeof opts.cursorSecret !== "string" || !opts.cursorSecret.length) {
287
+ if (process.env.NODE_ENV === "production") {
288
+ throw new Error("eventLog.create: opts.cursorSecret is required in production");
289
+ }
290
+ opts.cursorSecret = "event-log-cursor-secret-dev-only";
291
+ }
292
+ var cursorSecret = opts.cursorSecret;
293
+
294
+ function _decodeCursor(cursor, label) {
295
+ if (cursor == null) return null;
296
+ if (typeof cursor !== "string") {
297
+ throw new TypeError("eventLog." + label + ": cursor must be an opaque string or null");
298
+ }
299
+ try {
300
+ var state = _b().pagination.decodeCursor(cursor, cursorSecret);
301
+ if (JSON.stringify(state.orderKey) !== JSON.stringify(CURSOR_ORDER_KEY)) {
302
+ throw new TypeError("eventLog." + label + ": cursor orderKey mismatch");
303
+ }
304
+ if (!Array.isArray(state.vals) || state.vals.length !== 2) {
305
+ throw new TypeError("eventLog." + label + ": cursor vals shape mismatch");
306
+ }
307
+ var occurredAt = state.vals[0];
308
+ var rowId = state.vals[1];
309
+ if (!Number.isInteger(occurredAt) || occurredAt < 0) {
310
+ throw new TypeError("eventLog." + label + ": cursor occurred_at must be a non-negative integer");
311
+ }
312
+ if (typeof rowId !== "string" || !rowId.length) {
313
+ throw new TypeError("eventLog." + label + ": cursor id must be a non-empty string");
314
+ }
315
+ return { occurred_at: occurredAt, id: rowId };
316
+ } catch (e) {
317
+ if (e instanceof TypeError) throw e;
318
+ throw new TypeError("eventLog." + label + ": cursor — " + (e && e.message || "malformed"));
319
+ }
320
+ }
321
+
322
+ function _encodeNext(rows, limit) {
323
+ if (rows.length < limit) return null;
324
+ var last = rows[rows.length - 1];
325
+ return _b().pagination.encodeCursor({
326
+ orderKey: CURSOR_ORDER_KEY,
327
+ vals: [last.occurred_at, last.id],
328
+ forward: true,
329
+ }, cursorSecret);
330
+ }
331
+
332
+ // -- recordEvent --------------------------------------------------------
333
+ //
334
+ // Drop-silent on bad input. Returns `{ id, occurred_at }` on success
335
+ // and `{ dropped: true, reason }` on every refusal so a caller that
336
+ // wants to log the drop (smoke tests, debug builds) can see why.
337
+ // Production callers ignore the return value entirely; the drop is
338
+ // silent from the operator dashboard's perspective.
339
+
340
+ async function recordEvent(input) {
341
+ try {
342
+ if (!input || typeof input !== "object") {
343
+ return { dropped: true, reason: "input must be an object" };
344
+ }
345
+
346
+ var kind = input.kind;
347
+ if (!_isBoundedKind(kind, MAX_KIND_LEN, KIND_RE)) {
348
+ return { dropped: true, reason: "kind must match /[a-z][a-z0-9._-]*/ (≤ " + MAX_KIND_LEN + " chars)" };
349
+ }
350
+
351
+ var subjectKind = input.subject_kind;
352
+ if (!_isBoundedKind(subjectKind, MAX_SUBJECT_KIND_LEN, KIND_RE)) {
353
+ return { dropped: true, reason: "subject_kind must match /[a-z][a-z0-9._-]*/ (≤ " + MAX_SUBJECT_KIND_LEN + " chars)" };
354
+ }
355
+
356
+ var subjectId = input.subject_id;
357
+ if (!_isBoundedKind(subjectId, MAX_SUBJECT_ID_LEN, ID_RE)) {
358
+ return { dropped: true, reason: "subject_id must match /[A-Za-z0-9][A-Za-z0-9._:-]*/ (≤ " + MAX_SUBJECT_ID_LEN + " chars)" };
359
+ }
360
+
361
+ var actorKindRaw = input.actor_kind == null ? null : input.actor_kind;
362
+ if (!_isOptionalBoundedKind(actorKindRaw, MAX_ACTOR_KIND_LEN, KIND_RE)) {
363
+ return { dropped: true, reason: "actor_kind must match /[a-z][a-z0-9._-]*/ (≤ " + MAX_ACTOR_KIND_LEN + " chars) when provided" };
364
+ }
365
+ var actorIdRaw = input.actor_id == null ? null : input.actor_id;
366
+ if (!_isOptionalBoundedKind(actorIdRaw, MAX_ACTOR_ID_LEN, ID_RE)) {
367
+ return { dropped: true, reason: "actor_id must match /[A-Za-z0-9][A-Za-z0-9._:-]*/ (≤ " + MAX_ACTOR_ID_LEN + " chars) when provided" };
368
+ }
369
+ // actor_kind without actor_id is a producer bug — the kind
370
+ // describes the id, and dangling-kind rows confuse the actor-
371
+ // indexed scans.
372
+ if (actorKindRaw != null && actorIdRaw == null) {
373
+ return { dropped: true, reason: "actor_kind provided without actor_id" };
374
+ }
375
+
376
+ var severity = input.severity == null ? "info" : input.severity;
377
+ if (!_isSeverity(severity)) {
378
+ return { dropped: true, reason: "severity must be one of " + SEVERITIES.join(", ") };
379
+ }
380
+
381
+ var sourceRaw = input.source == null ? null : input.source;
382
+ if (!_isOptionalBoundedKind(sourceRaw, MAX_SOURCE_LEN, SOURCE_RE)) {
383
+ return { dropped: true, reason: "source must match /[a-z0-9][a-z0-9._\\-/]*/ (≤ " + MAX_SOURCE_LEN + " chars) when provided" };
384
+ }
385
+
386
+ // Payload is optional + opaque. JSON.stringify refuses BigInt /
387
+ // circular references / functions; bound the size so a runaway
388
+ // producer can't fill the row store with a single megabyte blob.
389
+ var payloadJson = null;
390
+ if (input.payload != null) {
391
+ try { payloadJson = JSON.stringify(input.payload); }
392
+ catch (_e) {
393
+ return { dropped: true, reason: "payload is not JSON-serialisable" };
394
+ }
395
+ if (payloadJson == null) {
396
+ return { dropped: true, reason: "payload is not JSON-serialisable" };
397
+ }
398
+ if (Buffer.byteLength(payloadJson, "utf8") > MAX_PAYLOAD_BYTES) {
399
+ return { dropped: true, reason: "payload JSON exceeds " + MAX_PAYLOAD_BYTES + " bytes" };
400
+ }
401
+ }
402
+
403
+ var occurredAt = _now();
404
+ var id = _b().uuid.v7();
405
+
406
+ await query(
407
+ "INSERT INTO event_log " +
408
+ "(id, kind, subject_kind, subject_id, actor_kind, actor_id, " +
409
+ " payload_json, severity, source, occurred_at) " +
410
+ "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
411
+ [
412
+ id, kind, subjectKind, subjectId,
413
+ actorKindRaw, actorIdRaw,
414
+ payloadJson, severity, sourceRaw,
415
+ occurredAt,
416
+ ],
417
+ );
418
+ return { id: id, occurred_at: occurredAt };
419
+ } catch (_e) {
420
+ // Drop-silent on any unexpected failure — a database hiccup at
421
+ // event-record time must not crash whichever request / job
422
+ // triggered the write. The aggregate dashboards already render
423
+ // "no data" gracefully so an observed gap is the operator's
424
+ // signal to check the DB.
425
+ return { dropped: true, reason: "internal" };
426
+ }
427
+ }
428
+
429
+ // -- query --------------------------------------------------------------
430
+ //
431
+ // HMAC-paginated event stream. `kind`, `subject`, `actor`,
432
+ // `severity_min`, `from`, `to` narrow the scan; `cursor` + `limit`
433
+ // paginate it. Cursor is an opaque base64url string the caller
434
+ // round-trips verbatim.
435
+
436
+ async function queryFn(input) {
437
+ if (input == null) input = {};
438
+ if (typeof input !== "object") {
439
+ throw new TypeError("eventLog.query: input must be an object");
440
+ }
441
+ var kind = input.kind == null ? null : _readKind(input.kind, "kind");
442
+ var subject = _readSubject(input.subject);
443
+ var actor = _readActor(input.actor);
444
+ var severityMin = _severityMin(input.severity_min);
445
+ var fromTs = _epochMsOpt(input.from, "from");
446
+ var toTs = _epochMsOpt(input.to, "to");
447
+ if (fromTs != null && toTs != null && fromTs > toTs) {
448
+ throw new TypeError("eventLog.query: from must be <= to");
449
+ }
450
+ var limit = _limit(input.limit, "limit", MAX_LIST_LIMIT, DEFAULT_LIST_LIMIT);
451
+ var cursor = _decodeCursor(input.cursor, "query");
452
+
453
+ var clauses = [];
454
+ var params = [];
455
+ var idx = 1;
456
+ if (kind != null) { clauses.push("kind = ?" + idx); params.push(kind); idx += 1; }
457
+ if (subject != null) {
458
+ clauses.push("subject_kind = ?" + idx); params.push(subject.kind); idx += 1;
459
+ clauses.push("subject_id = ?" + idx); params.push(subject.id); idx += 1;
460
+ }
461
+ if (actor != null) {
462
+ clauses.push("actor_id = ?" + idx); params.push(actor.id); idx += 1;
463
+ if (actor.kind != null) {
464
+ clauses.push("actor_kind = ?" + idx); params.push(actor.kind); idx += 1;
465
+ }
466
+ }
467
+ if (severityMin != null) {
468
+ var rungs = _severitiesAtOrAbove(severityMin);
469
+ var placeholders = [];
470
+ for (var s = 0; s < rungs.length; s += 1) {
471
+ placeholders.push("?" + idx);
472
+ params.push(rungs[s]);
473
+ idx += 1;
474
+ }
475
+ clauses.push("severity IN (" + placeholders.join(", ") + ")");
476
+ }
477
+ if (fromTs != null) { clauses.push("occurred_at >= ?" + idx); params.push(fromTs); idx += 1; }
478
+ if (toTs != null) { clauses.push("occurred_at <= ?" + idx); params.push(toTs); idx += 1; }
479
+ if (cursor != null) {
480
+ clauses.push("(occurred_at < ?" + idx + " OR (occurred_at = ?" + idx + " AND id < ?" + (idx + 1) + "))");
481
+ params.push(cursor.occurred_at);
482
+ params.push(cursor.id);
483
+ idx += 2;
484
+ }
485
+
486
+ var sql = "SELECT id, kind, subject_kind, subject_id, actor_kind, actor_id, " +
487
+ "payload_json, severity, source, occurred_at FROM event_log";
488
+ if (clauses.length) sql += " WHERE " + clauses.join(" AND ");
489
+ sql += " ORDER BY occurred_at DESC, id DESC LIMIT ?" + idx;
490
+ params.push(limit + 1);
491
+
492
+ var rs = await query(sql, params);
493
+ var raw = rs.rows;
494
+ var page = raw.slice(0, limit);
495
+ var rows = [];
496
+ for (var i = 0; i < page.length; i += 1) rows.push(_hydrate(page[i]));
497
+ var nextCursor = null;
498
+ if (raw.length > limit) {
499
+ var last = rows[rows.length - 1];
500
+ nextCursor = _b().pagination.encodeCursor({
501
+ orderKey: CURSOR_ORDER_KEY,
502
+ vals: [last.occurred_at, last.id],
503
+ forward: true,
504
+ }, cursorSecret);
505
+ }
506
+ return { rows: rows, next_cursor: nextCursor };
507
+ }
508
+
509
+ // -- tail ---------------------------------------------------------------
510
+ //
511
+ // Snapshot read of the most recent rows. Operator's monitoring
512
+ // console polls this on its own schedule. The `poll_ms` hint is
513
+ // echoed back as `observed_poll_ms` so the UI knows what cadence
514
+ // the primitive validated.
515
+
516
+ async function tail(input) {
517
+ if (input == null || typeof input !== "object") {
518
+ throw new TypeError("eventLog.tail: input must be an object");
519
+ }
520
+ var pollMs;
521
+ if (input.poll_ms == null) {
522
+ pollMs = DEFAULT_POLL_MS;
523
+ } else if (_isInt(input.poll_ms) && input.poll_ms >= MIN_POLL_MS && input.poll_ms <= MAX_POLL_MS) {
524
+ pollMs = input.poll_ms;
525
+ } else {
526
+ throw new TypeError("eventLog.tail: poll_ms must be an integer in [" + MIN_POLL_MS + ", " + MAX_POLL_MS + "]");
527
+ }
528
+ var maxEvents = _limit(input.max_events, "max_events", MAX_TAIL_EVENTS, DEFAULT_TAIL_EVENTS);
529
+
530
+ var observedAt = _now();
531
+ var rs = await query(
532
+ "SELECT id, kind, subject_kind, subject_id, actor_kind, actor_id, " +
533
+ "payload_json, severity, source, occurred_at FROM event_log " +
534
+ "ORDER BY occurred_at DESC, id DESC LIMIT ?1",
535
+ [maxEvents],
536
+ );
537
+ var rows = [];
538
+ for (var i = 0; i < rs.rows.length; i += 1) rows.push(_hydrate(rs.rows[i]));
539
+ return {
540
+ rows: rows,
541
+ observed_at: observedAt,
542
+ observed_poll_ms: pollMs,
543
+ };
544
+ }
545
+
546
+ // -- metricsForKind -----------------------------------------------------
547
+ //
548
+ // Roll-up for a single kind across [from, to]. by_severity and
549
+ // by_source are flat `{ <bucket>: count }` maps so the operator
550
+ // dashboard renders both panels off one query.
551
+
552
+ async function metricsForKind(input) {
553
+ if (!input || typeof input !== "object") {
554
+ throw new TypeError("eventLog.metricsForKind: input must be an object");
555
+ }
556
+ var kind = _readKind(input.kind, "kind");
557
+ var fromTs = _epochMsOpt(input.from, "from");
558
+ var toTs = _epochMsOpt(input.to, "to");
559
+ if (fromTs == null) {
560
+ throw new TypeError("eventLog.metricsForKind: from is required");
561
+ }
562
+ if (toTs == null) {
563
+ throw new TypeError("eventLog.metricsForKind: to is required");
564
+ }
565
+ if (fromTs > toTs) {
566
+ throw new TypeError("eventLog.metricsForKind: from must be <= to");
567
+ }
568
+
569
+ var rs = await query(
570
+ "SELECT severity, source, occurred_at FROM event_log " +
571
+ "WHERE kind = ?1 AND occurred_at >= ?2 AND occurred_at <= ?3",
572
+ [kind, fromTs, toTs],
573
+ );
574
+
575
+ var bySeverity = { debug: 0, info: 0, warning: 0, critical: 0 };
576
+ var bySource = {};
577
+ var total = 0;
578
+ var firstAt = null;
579
+ var lastAt = null;
580
+ for (var i = 0; i < rs.rows.length; i += 1) {
581
+ var r = rs.rows[i];
582
+ total += 1;
583
+ if (bySeverity[r.severity] != null) bySeverity[r.severity] += 1;
584
+ var src = r.source == null ? "(none)" : r.source;
585
+ bySource[src] = (bySource[src] || 0) + 1;
586
+ var ts = Number(r.occurred_at);
587
+ if (firstAt == null || ts < firstAt) firstAt = ts;
588
+ if (lastAt == null || ts > lastAt) lastAt = ts;
589
+ }
590
+ return {
591
+ kind: kind,
592
+ from: fromTs,
593
+ to: toTs,
594
+ total: total,
595
+ by_severity: bySeverity,
596
+ by_source: bySource,
597
+ first_at: firstAt,
598
+ last_at: lastAt,
599
+ };
600
+ }
601
+
602
+ // -- topKinds -----------------------------------------------------------
603
+
604
+ async function topKinds(input) {
605
+ if (!input || typeof input !== "object") {
606
+ throw new TypeError("eventLog.topKinds: input must be an object");
607
+ }
608
+ var fromTs = _epochMsOpt(input.from, "from");
609
+ var toTs = _epochMsOpt(input.to, "to");
610
+ if (fromTs == null) {
611
+ throw new TypeError("eventLog.topKinds: from is required");
612
+ }
613
+ if (toTs == null) {
614
+ throw new TypeError("eventLog.topKinds: to is required");
615
+ }
616
+ if (fromTs > toTs) {
617
+ throw new TypeError("eventLog.topKinds: from must be <= to");
618
+ }
619
+ var limit = _limit(input.limit, "limit", MAX_LIST_LIMIT, DEFAULT_LIST_LIMIT);
620
+
621
+ var rs = await query(
622
+ "SELECT kind, COUNT(*) AS c FROM event_log " +
623
+ "WHERE occurred_at >= ?1 AND occurred_at <= ?2 " +
624
+ "GROUP BY kind ORDER BY c DESC, kind ASC LIMIT ?3",
625
+ [fromTs, toTs, limit],
626
+ );
627
+ var out = [];
628
+ for (var i = 0; i < rs.rows.length; i += 1) {
629
+ out.push({
630
+ kind: rs.rows[i].kind,
631
+ count: Number(rs.rows[i].c) || 0,
632
+ });
633
+ }
634
+ return out;
635
+ }
636
+
637
+ // -- purgeOlderThan -----------------------------------------------------
638
+ //
639
+ // Retention sweep. Drops every row whose `occurred_at` is older
640
+ // than `now - days * 86400000`. `exclude_critical: true` preserves
641
+ // critical-rung rows past the retention window so the next incident
642
+ // review still has the high-signal events to grep over.
643
+
644
+ async function purgeOlderThan(input) {
645
+ if (!input || typeof input !== "object") {
646
+ throw new TypeError("eventLog.purgeOlderThan: input must be an object");
647
+ }
648
+ var days = input.days;
649
+ if (!_isPositive(days) || days > MAX_PURGE_DAYS) {
650
+ throw new TypeError("eventLog.purgeOlderThan: days must be an integer in [1, " + MAX_PURGE_DAYS + "]");
651
+ }
652
+ var excludeCritical = !!input.exclude_critical;
653
+ var cutoff = _now() - days * 86400000;
654
+
655
+ var sql, params;
656
+ if (excludeCritical) {
657
+ sql = "DELETE FROM event_log WHERE occurred_at < ?1 AND severity != 'critical'";
658
+ params = [cutoff];
659
+ } else {
660
+ sql = "DELETE FROM event_log WHERE occurred_at < ?1";
661
+ params = [cutoff];
662
+ }
663
+ var rs = await query(sql, params);
664
+ return { deleted: Number(rs.rowCount) || 0 };
665
+ }
666
+
667
+ return {
668
+ SEVERITIES: SEVERITIES.slice(),
669
+ MAX_KIND_LEN: MAX_KIND_LEN,
670
+ MAX_LIST_LIMIT: MAX_LIST_LIMIT,
671
+ DEFAULT_LIST_LIMIT: DEFAULT_LIST_LIMIT,
672
+ MAX_TAIL_EVENTS: MAX_TAIL_EVENTS,
673
+ DEFAULT_TAIL_EVENTS: DEFAULT_TAIL_EVENTS,
674
+ MIN_POLL_MS: MIN_POLL_MS,
675
+ DEFAULT_POLL_MS: DEFAULT_POLL_MS,
676
+ MAX_POLL_MS: MAX_POLL_MS,
677
+ MAX_PURGE_DAYS: MAX_PURGE_DAYS,
678
+
679
+ recordEvent: recordEvent,
680
+ query: queryFn,
681
+ tail: tail,
682
+ metricsForKind: metricsForKind,
683
+ topKinds: topKinds,
684
+ purgeOlderThan: purgeOlderThan,
685
+ };
686
+ }
687
+
688
+ // Async run() entry point — invoked by harnesses that load the
689
+ // primitive as a unit. Builds a default factory against `b.externalDb`
690
+ // so callers don't need to know about the peer injection surface to
691
+ // verify the module loads cleanly.
692
+ async function run() {
693
+ var instance = create({});
694
+ return {
695
+ ok: true,
696
+ surface: Object.keys(instance),
697
+ };
698
+ }
699
+
700
+ module.exports = {
701
+ create: create,
702
+ run: run,
703
+ SEVERITIES: SEVERITIES.slice(),
704
+ MAX_KIND_LEN: MAX_KIND_LEN,
705
+ MAX_LIST_LIMIT: MAX_LIST_LIMIT,
706
+ DEFAULT_LIST_LIMIT: DEFAULT_LIST_LIMIT,
707
+ MAX_TAIL_EVENTS: MAX_TAIL_EVENTS,
708
+ DEFAULT_TAIL_EVENTS: DEFAULT_TAIL_EVENTS,
709
+ MIN_POLL_MS: MIN_POLL_MS,
710
+ DEFAULT_POLL_MS: DEFAULT_POLL_MS,
711
+ MAX_POLL_MS: MAX_POLL_MS,
712
+ MAX_PURGE_DAYS: MAX_PURGE_DAYS,
713
+ };