@directive-run/core 1.12.0 → 1.13.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/adapter-utils.cjs +1 -1
- package/dist/adapter-utils.d.cts +2 -2
- package/dist/adapter-utils.d.ts +2 -2
- package/dist/adapter-utils.js +1 -1
- package/dist/adapter-utils.js.map +1 -1
- package/dist/audit-ledger-Dc6hAXam.d.cts +378 -0
- package/dist/audit-ledger-dxvslGi3.d.ts +378 -0
- package/dist/chunk-2FF6QGOA.js +2 -0
- package/dist/chunk-2FF6QGOA.js.map +1 -0
- package/dist/chunk-4MNQDXH7.cjs +3 -0
- package/dist/chunk-4MNQDXH7.cjs.map +1 -0
- package/dist/chunk-644QZVTT.js +16 -0
- package/dist/{chunk-26Z5VNPZ.js.map → chunk-644QZVTT.js.map} +1 -1
- package/dist/chunk-ENZEHIL7.cjs +3 -0
- package/dist/chunk-ENZEHIL7.cjs.map +1 -0
- package/dist/chunk-I722BZA5.js +7 -0
- package/dist/chunk-I722BZA5.js.map +1 -0
- package/dist/chunk-IXRS4LM4.cjs +2 -0
- package/dist/chunk-IXRS4LM4.cjs.map +1 -0
- package/dist/chunk-NPX5EKPP.cjs +16 -0
- package/dist/{chunk-EX3XG667.cjs.map → chunk-NPX5EKPP.cjs.map} +1 -1
- package/dist/chunk-PA6VC32N.js +2 -0
- package/dist/chunk-PA6VC32N.js.map +1 -0
- package/dist/chunk-PXRV64PA.js +3 -0
- package/dist/chunk-PXRV64PA.js.map +1 -0
- package/dist/chunk-R2GHSCTR.js +3 -0
- package/dist/chunk-R2GHSCTR.js.map +1 -0
- package/dist/chunk-T4TRJEJN.cjs +2 -0
- package/dist/chunk-T4TRJEJN.cjs.map +1 -0
- package/dist/chunk-X7G7UBXU.cjs +7 -0
- package/dist/chunk-X7G7UBXU.cjs.map +1 -0
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +214 -391
- package/dist/index.d.ts +214 -391
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/internals.cjs +1 -1
- package/dist/internals.d.cts +5 -5
- package/dist/internals.d.ts +5 -5
- package/dist/internals.js +1 -1
- package/dist/plugins/index.cjs +2 -2
- package/dist/plugins/index.cjs.map +1 -1
- package/dist/plugins/index.d.cts +2 -2
- package/dist/plugins/index.d.ts +2 -2
- package/dist/plugins/index.js +1 -1
- package/dist/plugins/index.js.map +1 -1
- package/dist/{plugins-Ykl_sAPE.d.ts → plugins-BIzXaYbg.d.cts} +15 -1
- package/dist/{plugins-Ykl_sAPE.d.cts → plugins-BIzXaYbg.d.ts} +15 -1
- package/dist/predicate-Bnx3LN7P.d.cts +655 -0
- package/dist/predicate-BxQVf0ug.d.ts +655 -0
- package/dist/system-A6VYKLVF.js +2 -0
- package/dist/{system-VZWB6WXX.js.map → system-A6VYKLVF.js.map} +1 -1
- package/dist/system-CDJMD5O5.cjs +2 -0
- package/dist/{system-GK3NSFQH.cjs.map → system-CDJMD5O5.cjs.map} +1 -1
- package/dist/testing.cjs +1 -1
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/dist/testing.js.map +1 -1
- package/dist/{utils-BnQajqPu.d.cts → utils-Mg55IerF.d.cts} +27 -1
- package/dist/{utils-BnQajqPu.d.ts → utils-Mg55IerF.d.ts} +27 -1
- package/dist/worker.cjs +1 -1
- package/dist/worker.d.cts +1 -1
- package/dist/worker.d.ts +1 -1
- package/dist/worker.js +1 -1
- package/package.json +1 -1
- package/dist/audit-ledger-9IElAHH9.d.ts +0 -205
- package/dist/audit-ledger-qMjEBqiP.d.cts +0 -205
- package/dist/chunk-26Z5VNPZ.js +0 -16
- package/dist/chunk-4VZOZWXM.cjs +0 -2
- package/dist/chunk-4VZOZWXM.cjs.map +0 -1
- package/dist/chunk-7NMXRATK.cjs +0 -3
- package/dist/chunk-7NMXRATK.cjs.map +0 -1
- package/dist/chunk-7TSYQEN3.js +0 -2
- package/dist/chunk-7TSYQEN3.js.map +0 -1
- package/dist/chunk-EOLY64E6.cjs +0 -3
- package/dist/chunk-EOLY64E6.cjs.map +0 -1
- package/dist/chunk-EX3XG667.cjs +0 -16
- package/dist/chunk-N4KTCKOI.cjs +0 -7
- package/dist/chunk-N4KTCKOI.cjs.map +0 -1
- package/dist/chunk-T6IJUWYR.js +0 -3
- package/dist/chunk-T6IJUWYR.js.map +0 -1
- package/dist/chunk-TPOKS4RY.js +0 -3
- package/dist/chunk-TPOKS4RY.js.map +0 -1
- package/dist/chunk-TZHC4E6S.js +0 -7
- package/dist/chunk-TZHC4E6S.js.map +0 -1
- package/dist/helpers-D2pfb6vT.d.ts +0 -235
- package/dist/helpers-hh6UanB1.d.cts +0 -235
- package/dist/system-GK3NSFQH.cjs +0 -2
- package/dist/system-VZWB6WXX.js +0 -2
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { F as FactPredicate, C as ClauseResult, P as Plugin, M as ModuleSchema } from './plugins-BIzXaYbg.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* createAuditLedger — append-only, queryable, hash-chained
|
|
5
|
+
* (djb2; SHA-256 reserved for v2) audit of every state change. For
|
|
6
|
+
* forensics and "show me why this user got that decision."
|
|
7
|
+
*
|
|
8
|
+
* Captures (per observation event):
|
|
9
|
+
*
|
|
10
|
+
* - `constraint.evaluate` → { whenSpec, whenExplain, active, whenSource? }
|
|
11
|
+
* - `resolver.write.rejected` (rejection + summary kinds)
|
|
12
|
+
* - `fact.change` → { key, prior, next }
|
|
13
|
+
* - `resolver.complete` → { resolverId, requirementId, duration }
|
|
14
|
+
* - `system.init` / `system.start` / `system.stop` / `system.destroy`
|
|
15
|
+
* - `system.snapshot` / `system.history.navigate` (lifecycle markers)
|
|
16
|
+
* - `system.truncated` (ring-buffer overflow marker)
|
|
17
|
+
* - `system.entry-erased` / `system.subject-erased` (GDPR Art.17 stub)
|
|
18
|
+
*
|
|
19
|
+
* Hash chain: each entry stores `prevHash` — the djb2 (`hashObject`)
|
|
20
|
+
* hash of the previous entry's stable-stringified payload. Tampering
|
|
21
|
+
* with any entry's payload breaks the next entry's `prevHash` link —
|
|
22
|
+
* visible in `verify()`. v1 ships sync djb2 only; `verify({ strong: true })`
|
|
23
|
+
* is reserved for v2 (SHA-256) and throws today.
|
|
24
|
+
*
|
|
25
|
+
* PII redaction: by default, fact keys whose meta carries the `pii`
|
|
26
|
+
* tag (via `system.meta.byTag("pii")`) have their values replaced with
|
|
27
|
+
* `"[redacted]"` in `whenExplain.actual`, `fact.change.prior`,
|
|
28
|
+
* `fact.change.next`, and the cached `whenSpec` operands. Opt out with
|
|
29
|
+
* `capturePII: true`.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/** Hash algorithm tag — bumped if canonicalization or hash function changes. */
|
|
33
|
+
declare const HASH_ALGO: "djb2-1";
|
|
34
|
+
/**
|
|
35
|
+
* Entry schema version. Bumped if `AuditEntry` field shape changes in
|
|
36
|
+
* a way that breaks back-compat parsers. Persisted on every entry so
|
|
37
|
+
* exports remain self-describing across library upgrades. (F-5)
|
|
38
|
+
*/
|
|
39
|
+
declare const SCHEMA_VERSION: 1;
|
|
40
|
+
/**
|
|
41
|
+
* Private sentinel stamped onto tombstone entries by {@link createAuditLedger.erase}.
|
|
42
|
+
* Never exported, never serialized — `verify()` checks for it before
|
|
43
|
+
* accepting a `system.entry-erased` entry as a legitimate chain break.
|
|
44
|
+
*
|
|
45
|
+
* (N7) Without this, a caller holding a raw `AuditLedgerSink` reference
|
|
46
|
+
* could write `{ kind: "system.entry-erased", … }` directly into the
|
|
47
|
+
* sink to mask real tampering as legitimate erasure. The sentinel
|
|
48
|
+
* raises the bar so only the in-process ledger plugin (which lives in
|
|
49
|
+
* this module's closure) can mint a valid tombstone.
|
|
50
|
+
*/
|
|
51
|
+
declare const LEDGER_INTERNAL_TOKEN: unique symbol;
|
|
52
|
+
type AuditEntryKind = "constraint.evaluate" | "resolver.write.rejected" | "fact.change" | "resolver.complete" | "resolver.error" | "system.init" | "system.start" | "system.stop" | "system.destroy" | "system.snapshot" | "system.history.navigate" | "system.truncated" | "system.entry-erased" | "system.subject-erased";
|
|
53
|
+
interface AuditEntryBase {
|
|
54
|
+
/** Monotonic sequence number, starting at 0. */
|
|
55
|
+
readonly seq: number;
|
|
56
|
+
/** Wall-clock timestamp (ms epoch). */
|
|
57
|
+
readonly ts: number;
|
|
58
|
+
/** Discriminator. */
|
|
59
|
+
readonly kind: AuditEntryKind;
|
|
60
|
+
/** Hash of the previous entry's full payload. null on the genesis entry. */
|
|
61
|
+
readonly prevHash: string | null;
|
|
62
|
+
/**
|
|
63
|
+
* Hash algorithm tag identifying the canonicalization + hash
|
|
64
|
+
* function in use. Bumped if the algorithm or canonical form
|
|
65
|
+
* changes, so exports remain verifiable across versions.
|
|
66
|
+
*/
|
|
67
|
+
readonly hashAlgo: typeof HASH_ALGO;
|
|
68
|
+
/**
|
|
69
|
+
* Entry schema version — bumped if any `AuditEntry` field shape
|
|
70
|
+
* changes in a way that breaks back-compat. Pair with `hashAlgo`
|
|
71
|
+
* when migrating older exports. (F-5)
|
|
72
|
+
*/
|
|
73
|
+
readonly schemaVersion: typeof SCHEMA_VERSION;
|
|
74
|
+
/**
|
|
75
|
+
* Private sentinel — present (and equal to the in-module token) only
|
|
76
|
+
* on legitimate tombstones minted by `ledger.erase()`. Filtered out
|
|
77
|
+
* of all public read paths (`query`, `recent`, `toJSON`, etc.) so
|
|
78
|
+
* consumers never see or copy it. (N7)
|
|
79
|
+
*
|
|
80
|
+
* NOT serialized. NOT exported. Forging this from outside the module
|
|
81
|
+
* is impossible without the symbol reference; `verify()` rejects any
|
|
82
|
+
* `system.entry-erased` entry that lacks it.
|
|
83
|
+
*
|
|
84
|
+
* @internal
|
|
85
|
+
*/
|
|
86
|
+
readonly __internal?: typeof LEDGER_INTERNAL_TOKEN;
|
|
87
|
+
}
|
|
88
|
+
type AuditEntry = (AuditEntryBase & {
|
|
89
|
+
kind: "constraint.evaluate";
|
|
90
|
+
constraintId: string;
|
|
91
|
+
active: boolean;
|
|
92
|
+
/** Cached at ledger start from `system.inspect().constraints[].whenSpec`. Refreshed on `register()`/`assign()`/`unregister()`. May be undefined for function-form constraints (see `whenSource`). PII operands redacted unless `capturePII: true`. */
|
|
93
|
+
whenSpec?: FactPredicate<unknown>;
|
|
94
|
+
whenExplain?: readonly ClauseResult[];
|
|
95
|
+
/**
|
|
96
|
+
* For function-form constraints (no `whenSpec`), a tamper-evident
|
|
97
|
+
* identity for the function. We DO NOT capture the raw source —
|
|
98
|
+
* closures routinely reference secrets, API keys, or PII (e.g.
|
|
99
|
+
* `if (apiKey === "sk-live-xxx")`) and a preview would leak them
|
|
100
|
+
* into the audit log. Instead, we capture a djb2 hash of the
|
|
101
|
+
* stringified function (`hashObject(String(fn))`). Auditors can
|
|
102
|
+
* detect "the function changed between deploys" by comparing
|
|
103
|
+
* hashes across entries, without ever seeing the function body.
|
|
104
|
+
*
|
|
105
|
+
* Informational only — NOT replayable. (N5, M22)
|
|
106
|
+
*/
|
|
107
|
+
whenSource?: {
|
|
108
|
+
kind: "function";
|
|
109
|
+
sourceHash: string;
|
|
110
|
+
};
|
|
111
|
+
}) | (AuditEntryBase & {
|
|
112
|
+
kind: "resolver.write.rejected";
|
|
113
|
+
rejection: "rejection" | "summary";
|
|
114
|
+
resolverId: string;
|
|
115
|
+
requirementId: string;
|
|
116
|
+
reason: string;
|
|
117
|
+
fact?: string;
|
|
118
|
+
expected?: unknown;
|
|
119
|
+
actual?: unknown;
|
|
120
|
+
dropped?: number;
|
|
121
|
+
}) | (AuditEntryBase & {
|
|
122
|
+
kind: "fact.change";
|
|
123
|
+
key: string;
|
|
124
|
+
prior: unknown;
|
|
125
|
+
next: unknown;
|
|
126
|
+
}) | (AuditEntryBase & {
|
|
127
|
+
kind: "resolver.complete";
|
|
128
|
+
resolverId: string;
|
|
129
|
+
requirementId: string;
|
|
130
|
+
duration: number;
|
|
131
|
+
}) | (AuditEntryBase & {
|
|
132
|
+
kind: "resolver.error";
|
|
133
|
+
resolverId: string;
|
|
134
|
+
requirementId: string;
|
|
135
|
+
error: string;
|
|
136
|
+
}) | (AuditEntryBase & {
|
|
137
|
+
kind: "system.init" | "system.start" | "system.stop" | "system.destroy";
|
|
138
|
+
}) | (AuditEntryBase & {
|
|
139
|
+
kind: "system.snapshot";
|
|
140
|
+
snapshotId: number;
|
|
141
|
+
trigger: string;
|
|
142
|
+
}) | (AuditEntryBase & {
|
|
143
|
+
kind: "system.history.navigate";
|
|
144
|
+
from: number;
|
|
145
|
+
to: number;
|
|
146
|
+
}) | (AuditEntryBase & {
|
|
147
|
+
kind: "system.truncated";
|
|
148
|
+
droppedSeq: number;
|
|
149
|
+
droppedCount: number;
|
|
150
|
+
}) | (AuditEntryBase & {
|
|
151
|
+
kind: "system.entry-erased";
|
|
152
|
+
originalKind: AuditEntryKind;
|
|
153
|
+
erasedAt: number;
|
|
154
|
+
}) | (AuditEntryBase & {
|
|
155
|
+
kind: "system.subject-erased";
|
|
156
|
+
/**
|
|
157
|
+
* djb2 hash of the filter (via `hashObject(filter)`). PII-safe —
|
|
158
|
+
* the raw filter values never land in the ledger. Pair with
|
|
159
|
+
* `filterShape` to see which filter fields were used. (N2)
|
|
160
|
+
*/
|
|
161
|
+
filterHash: string;
|
|
162
|
+
/**
|
|
163
|
+
* Stripped-values shape of the filter — captures WHICH fields were
|
|
164
|
+
* present without recording their values. (N2)
|
|
165
|
+
*/
|
|
166
|
+
filterShape: {
|
|
167
|
+
factPath: boolean;
|
|
168
|
+
constraintId: boolean;
|
|
169
|
+
kind: AuditEntryKind | readonly AuditEntryKind[] | undefined;
|
|
170
|
+
changedBetween: "[range]" | undefined;
|
|
171
|
+
};
|
|
172
|
+
erased: number;
|
|
173
|
+
});
|
|
174
|
+
interface QueryFilter {
|
|
175
|
+
/** Exact-match fact path. */
|
|
176
|
+
factPath?: string;
|
|
177
|
+
/** Filter by constraint id. */
|
|
178
|
+
constraintId?: string;
|
|
179
|
+
/** Filter by entry kind. */
|
|
180
|
+
kind?: AuditEntryKind | readonly AuditEntryKind[];
|
|
181
|
+
/** Time range as `[startMs, endMs]`, ISO strings, or epoch numbers. */
|
|
182
|
+
changedBetween?: [string | number | Date, string | number | Date];
|
|
183
|
+
/** Maximum entries returned. Default 1000. */
|
|
184
|
+
limit?: number;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Verify result — chain valid OR a break with full context for tamper visualization.
|
|
188
|
+
*
|
|
189
|
+
* Erased entries (via `ledger.erase()`) appear as legitimate chain breaks —
|
|
190
|
+
* `verify()` reports them in `erasedSeqs` and continues the walk from the
|
|
191
|
+
* tombstone's own hash. Real tamper still surfaces as `valid: false`.
|
|
192
|
+
*
|
|
193
|
+
* Forged tombstones (a caller writes `kind: "system.entry-erased"`
|
|
194
|
+
* directly via `sink.write()` to mask tamper as erasure) are detected:
|
|
195
|
+
* legitimate tombstones carry an in-module sentinel that forgeries
|
|
196
|
+
* cannot mint, so `verify()` reports them as tamper. (N7)
|
|
197
|
+
*/
|
|
198
|
+
type VerifyResult = {
|
|
199
|
+
valid: true;
|
|
200
|
+
entryCount: number;
|
|
201
|
+
/**
|
|
202
|
+
* Seq numbers of entries legitimately broken by `erase()`
|
|
203
|
+
* tombstones. NOT timestamps — each entry pairs this seq with
|
|
204
|
+
* the per-entry `system.entry-erased.erasedAt` (ms epoch) for
|
|
205
|
+
* the timestamp. Empty unless the chain contains erasures.
|
|
206
|
+
* (N1 + M1; renamed from `erasedAt` in R3)
|
|
207
|
+
*/
|
|
208
|
+
erasedSeqs?: number[];
|
|
209
|
+
} | {
|
|
210
|
+
valid: false;
|
|
211
|
+
brokenAt: number;
|
|
212
|
+
expectedHash: string;
|
|
213
|
+
actualHash: string;
|
|
214
|
+
entry: AuditEntry;
|
|
215
|
+
/**
|
|
216
|
+
* Human-readable reason for the break — populated for cases
|
|
217
|
+
* where the cause is more specific than "hash mismatch" (e.g.
|
|
218
|
+
* tombstone forgery detected via missing sentinel).
|
|
219
|
+
*/
|
|
220
|
+
reason?: string;
|
|
221
|
+
};
|
|
222
|
+
interface AuditLedgerSink {
|
|
223
|
+
write(entry: AuditEntry): void;
|
|
224
|
+
query(filter: QueryFilter): readonly AuditEntry[];
|
|
225
|
+
recent(n: number): readonly AuditEntry[];
|
|
226
|
+
forFact(path: string, opts?: {
|
|
227
|
+
limit?: number;
|
|
228
|
+
}): readonly AuditEntry[];
|
|
229
|
+
forConstraint(id: string, opts?: {
|
|
230
|
+
limit?: number;
|
|
231
|
+
}): readonly AuditEntry[];
|
|
232
|
+
toJSON(): {
|
|
233
|
+
entries: readonly AuditEntry[];
|
|
234
|
+
capturedAt: number;
|
|
235
|
+
};
|
|
236
|
+
clear(): void;
|
|
237
|
+
destroy(): void;
|
|
238
|
+
/**
|
|
239
|
+
* Replace matching entries with tombstones IN PLACE (preserving seq +
|
|
240
|
+
* prevHash so the hash chain still verifies). v1 implementation
|
|
241
|
+
* matches on the same `QueryFilter` shape used by `query()`.
|
|
242
|
+
* Returns the count of entries replaced.
|
|
243
|
+
*
|
|
244
|
+
* WARNING: erases only from this sink. Any external copies (toJSON
|
|
245
|
+
* exports, downstream pipelines) must be erased separately.
|
|
246
|
+
*/
|
|
247
|
+
erase?(filter: QueryFilter, tombstoneFactory: (e: AuditEntry) => AuditEntry): number;
|
|
248
|
+
/**
|
|
249
|
+
* Optional hook fired by the sink BEFORE shifting the oldest entry
|
|
250
|
+
* out of a bounded ring buffer. The ledger plugin uses this to emit
|
|
251
|
+
* a `system.truncated` marker so an auditor sees that the log was
|
|
252
|
+
* truncated and where. (M23)
|
|
253
|
+
*/
|
|
254
|
+
onTruncate?(handler: (droppedSeq: number, droppedCount: number) => void): void;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* In-memory bounded ring-buffer sink. Drops oldest entries past
|
|
258
|
+
* `capacity` (default 10,000). Use this as the default sink for dev,
|
|
259
|
+
* tests, and StackBlitz demos.
|
|
260
|
+
*/
|
|
261
|
+
declare function memorySink(opts?: {
|
|
262
|
+
capacity?: number;
|
|
263
|
+
}): AuditLedgerSink;
|
|
264
|
+
interface AuditLedgerOptions {
|
|
265
|
+
/** Sink to write entries to. Default: in-memory ring buffer (capacity 10k). */
|
|
266
|
+
sink?: AuditLedgerSink;
|
|
267
|
+
/**
|
|
268
|
+
* Whether to capture raw fact values (`prior`/`next` on fact.change,
|
|
269
|
+
* `actual` in whenExplain). Default `false` — PII-tagged facts are
|
|
270
|
+
* redacted by default. Set `true` to opt out of redaction.
|
|
271
|
+
*/
|
|
272
|
+
capturePII?: boolean;
|
|
273
|
+
/**
|
|
274
|
+
* Optional caller-supplied redactor. Runs AFTER the default
|
|
275
|
+
* pii-tag-based redaction. Useful for additional sanitization.
|
|
276
|
+
*/
|
|
277
|
+
redact?: (entry: AuditEntry) => AuditEntry;
|
|
278
|
+
}
|
|
279
|
+
interface AuditLedger {
|
|
280
|
+
/** The plugin to pass to `createSystem({ plugins: [...] })`. */
|
|
281
|
+
readonly plugin: Plugin<ModuleSchema>;
|
|
282
|
+
/** Query entries matching the filter. */
|
|
283
|
+
query(filter?: QueryFilter): readonly AuditEntry[];
|
|
284
|
+
/** Most recent N entries (chronological). */
|
|
285
|
+
recent(n: number): readonly AuditEntry[];
|
|
286
|
+
/** All entries that touch this fact path (exact match). */
|
|
287
|
+
forFact(path: string, opts?: {
|
|
288
|
+
limit?: number;
|
|
289
|
+
}): readonly AuditEntry[];
|
|
290
|
+
/** All entries for this constraint id. */
|
|
291
|
+
forConstraint(id: string, opts?: {
|
|
292
|
+
limit?: number;
|
|
293
|
+
}): readonly AuditEntry[];
|
|
294
|
+
/** Full ledger snapshot for export / serialization. */
|
|
295
|
+
toJSON(): {
|
|
296
|
+
entries: readonly AuditEntry[];
|
|
297
|
+
capturedAt: number;
|
|
298
|
+
};
|
|
299
|
+
/**
|
|
300
|
+
* Walk the hash chain genesis → tip. Returns `{ valid: true }` iff
|
|
301
|
+
* every entry's `prevHash` matches the (sync, djb2-based) hash of
|
|
302
|
+
* the previous entry. On break, returns the index of the first
|
|
303
|
+
* broken link plus the expected vs actual hashes — feed into a
|
|
304
|
+
* "TAMPERED" visualization.
|
|
305
|
+
*
|
|
306
|
+
* Erased entries (via `ledger.erase()`) appear as legitimate chain
|
|
307
|
+
* breaks — `verify()` reports them in `erasedSeqs` and continues
|
|
308
|
+
* the walk from the tombstone's actual hash. Real tamper still
|
|
309
|
+
* surfaces as `valid: false`. (N1 + M1)
|
|
310
|
+
*
|
|
311
|
+
* Forged tombstones — `kind: "system.entry-erased"` entries written
|
|
312
|
+
* directly via `sink.write()` to mask tamper — are detected as
|
|
313
|
+
* forgery. Legitimate tombstones carry an in-module sentinel that
|
|
314
|
+
* forgeries cannot mint. (N7)
|
|
315
|
+
*
|
|
316
|
+
* v1 ships sync djb2 only. `verify({ strong: true })` is reserved
|
|
317
|
+
* for v2 (SHA-256) and THROWS today — there is no silent fallback.
|
|
318
|
+
* Call `verify()` (no args) for tamper detection.
|
|
319
|
+
*/
|
|
320
|
+
verify(opts?: {
|
|
321
|
+
strong?: boolean;
|
|
322
|
+
}): VerifyResult;
|
|
323
|
+
/**
|
|
324
|
+
* Per-subject erasure (GDPR Art. 17 stub). Replaces matching entries
|
|
325
|
+
* in this sink with `system.entry-erased` tombstones (preserving
|
|
326
|
+
* seq + prevHash so verify() can resync), then appends a chained
|
|
327
|
+
* `system.subject-erased` marker entry that summarises the erasure.
|
|
328
|
+
*
|
|
329
|
+
* Returns `{ erased, markerEntry }` — `markerEntry` is the chained
|
|
330
|
+
* `system.subject-erased` summary (the N per-entry tombstones live
|
|
331
|
+
* in the sink, not on the return value). (M7)
|
|
332
|
+
*
|
|
333
|
+
* When `erased === 0` (filter matched nothing), `markerEntry` is
|
|
334
|
+
* `null` and no marker is emitted into the chain — avoids polluting
|
|
335
|
+
* the audit trail with empty "erased: 0" records. (MAJOR-3)
|
|
336
|
+
*
|
|
337
|
+
* WARNING: v1 erases only from THIS sink. External copies (toJSON
|
|
338
|
+
* exports, downstream pipelines, persisted backups) must be erased
|
|
339
|
+
* separately. (C8)
|
|
340
|
+
*/
|
|
341
|
+
erase(filter: QueryFilter): {
|
|
342
|
+
erased: number;
|
|
343
|
+
markerEntry: AuditEntry | null;
|
|
344
|
+
};
|
|
345
|
+
/** Empty the sink. */
|
|
346
|
+
clear(): void;
|
|
347
|
+
/** Unsubscribe + drop the sink. */
|
|
348
|
+
destroy(): void;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Create an audit ledger that subscribes to the given system's
|
|
352
|
+
* observation stream. Returns a `Plugin` to install + a query/verify
|
|
353
|
+
* API for the ledger.
|
|
354
|
+
*
|
|
355
|
+
* @example
|
|
356
|
+
* ```ts
|
|
357
|
+
* import { createAuditLedger } from "@directive-run/core/plugins";
|
|
358
|
+
*
|
|
359
|
+
* const ledger = createAuditLedger();
|
|
360
|
+
* const system = createSystem({ module, plugins: [ledger.plugin] });
|
|
361
|
+
* system.start();
|
|
362
|
+
*
|
|
363
|
+
* // Six months later — auditor asks "what changed cart-total in March?"
|
|
364
|
+
* ledger.query({
|
|
365
|
+
* factPath: "cartTotal",
|
|
366
|
+
* changedBetween: ["2026-03-01", "2026-04-01"],
|
|
367
|
+
* });
|
|
368
|
+
*
|
|
369
|
+
* // Verify nobody tampered with the ledger
|
|
370
|
+
* const verdict = await ledger.verify();
|
|
371
|
+
* if (!verdict.valid) {
|
|
372
|
+
* console.error("Tamper at entry", verdict.brokenAt);
|
|
373
|
+
* }
|
|
374
|
+
* ```
|
|
375
|
+
*/
|
|
376
|
+
declare function createAuditLedger(opts?: AuditLedgerOptions): AuditLedger;
|
|
377
|
+
|
|
378
|
+
export { type AuditEntry as A, type QueryFilter as Q, type VerifyResult as V, type AuditEntryKind as a, type AuditLedger as b, type AuditLedgerOptions as c, type AuditLedgerSink as d, createAuditLedger as e, memorySink as m };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import {a,e}from'./chunk-PXRV64PA.js';var g=new Set(["$eq","$ne","$in","$nin","$exists","$gt","$gte","$lt","$lte","$between","$matches","$startsWith","$endsWith","$contains","$changed"]),Q=new Set(["$all","$any","$not"]);var T=new Set(["number","string","boolean","bigint","date","unknown"]);function P(e){if(!e)return {kind:"unknown"};let n=/^Branded<(.+)>$/.exec(e);return n?{kind:"branded",inner:P(n[1])}:e.endsWith(" | null")||e.endsWith(" | undefined")?{...P(e.replace(/ \| (null|undefined)$/,"")),nullable:true}:T.has(e)?{kind:e}:/^(email|uuid|url|cuid|datetime|iso\b)/i.test(e)?{kind:"string"}:e.includes(" | ")?{kind:"union",members:e.split(" | ").map(P)}:e==="array"?{kind:"array",element:{kind:"unknown"}}:e==="object"?{kind:"object",shape:{}}:e==="record"?{kind:"record",value:{kind:"unknown"}}:e==="tuple"?{kind:"tuple",elements:[]}:e==="union"?{kind:"union",members:[]}:{kind:"unknown"}}function z(e){if(e==null)return {kind:"unknown"};if(typeof e=="function")return a&&console.warn("[Directive] getKind: received a function \u2014 did you forget () on a t.* builder? Example: write `t.number()`, not `t.number`."),{kind:"unknown"};if(typeof e!="object")return {kind:"unknown"};let n=e,r;try{r=n._kind;}catch{return {kind:"unknown"}}if(r)return r;let i;try{i=n._typeName;}catch{return {kind:"unknown"}}return P(i)}function H(e){let n=new Map;if(!e||typeof e!="object")return n;let r=e,t="facts"in r&&r.facts&&typeof r.facts=="object"?r.facts:r;for(let[a,s]of Object.entries(t)){if(!s||typeof s!="object")continue;let o;try{o=z(s);}catch{continue}if(n.set(a,o),o.kind==="object")for(let[c,l]of Object.entries(o.shape))n.set(`${a}.${c}`,l);}return n.size===0&&a&&console.warn("[Directive] getSchemaFieldKinds: schema appears empty (no introspectable keys). Did you pass the module instead of its schema? Pass `myModule.schema`, not `myModule`."),n}var k=["$eq","$ne","$in","$nin","$exists"],D=["$gt","$gte","$lt","$lte","$between"],W=["$matches","$startsWith","$endsWith","$contains"];function w(e){switch(e.kind){case "number":case "bigint":case "date":return [...k,...D];case "string":return [...k,...D,...W];case "boolean":case "unknown":return k;case "array":return [...k,"$contains"];case "object":case "record":case "tuple":return k;case "literal":case "enum":return w(e.primitive==="number"?{kind:"number"}:e.primitive==="boolean"?{kind:"boolean"}:e.primitive==="null"?{kind:"unknown"}:{kind:"string"});case "branded":return w(e.inner);case "union":{if(e.members.length===0)return k;let n=e.members.map(t=>new Set(w(t))),r=n[0],i=[];for(let t of r)n.every(a=>a.has(t))&&i.push(t);return i}default:{return k}}}function X(){return Array.from(g)}var O=64;function u(e){return typeof e!="object"||e===null||Array.isArray(e)?false:!(e instanceof Date)&&!(e instanceof RegExp)}function x(e){if(typeof e!="object"||e===null||Array.isArray(e))return false;let n=Object.getPrototypeOf(e);return n===Object.prototype||n===null}function E(e){if(!u(e))return false;let n=0,r=false;for(let i of Object.keys(e)){if(i.startsWith("$"))r=true,g.has(i)||f(`predicate: unknown operator "${i}" \u2014 looks like a typo. Known operators: ${[...g].join(", ")}`);else if(r||n===0)return false;n++;}return r?n>0:false}function re(e){return e===null?false:Array.isArray(e)?e.every(n=>x(n)&&"fact"in n&&"op"in n):x(e)}function h(e,n,r="",i=new WeakSet,t=0){if(t>O){a&&console.warn(`[Directive] predicate depth limit (${O}) exceeded \u2014 flatten the predicate or split it into multiple constraints. If this is unexpected, check for a cyclic spec object.`),n.bail?.("depth");return}if(Array.isArray(e)){e.forEach((s,o)=>{if(!u(s))return;let c=s;if(typeof c.fact=="string"&&typeof c.op=="string"){let l=r?`${r}[${o}]`:`[${o}]`;n.operator?.(r?`${r}.${c.fact}`:c.fact,c.op,c.value,`${l}.value`);}});return}if(!u(e))return;if(i.has(e)){a&&console.warn("[Directive] walkPredicate: cyclic predicate spec"),n.bail?.("cycle");return}i.add(e);let a$1=e;for(let s of ["$all","$any","$not"])if(s in a$1){if(n.combinator?.(s)===false)return;let c=s==="$not"?[a$1.$not]:a$1[s]??[];for(let l of c)h(l,n,r,i,t+1);return}for(let s of Object.keys(a$1)){let o=r?`${r}.${s}`:s;if(s.startsWith("$")){n.strayOperatorKey?.(s,o);continue}let c=a$1[s];if(E(c)){let l=c;for(let p of Object.keys(l))n.operator?.(o,p,l[p],`${o}.${p}`);continue}if(x(c)){if(n.nested?.(s)===false)continue;h(c,n,o,i,t+1);continue}n.literal?.(o,c);}}function ie(e){if(!x(e))return false;let n=false;return h(e,{operator(){n=true;},literal(){n=true;},combinator(){n=true;},strayOperatorKey(){n=true;},bail(){n=true;}}),!n}function I(e){return u(e)&&Object.hasOwn(e,"$template")&&typeof e.$template=="string"}function oe(e,n=""){function r(t,a,s,o){if(typeof t=="bigint")throw new Error(`[Directive] validatePredicate: bigint operand at "${a}" is not JSON-serializable (JSON.stringify throws on bigint).`);if(t instanceof Set)throw new Error(`[Directive] validatePredicate: Set operand at "${a}" is not JSON-serializable (serializes to {} and loses all members).`);if(t instanceof Map)throw new Error(`[Directive] validatePredicate: Map operand at "${a}" is not JSON-serializable (serializes to {} and loses all entries).`);if(t instanceof RegExp)throw new Error(`[Directive] validatePredicate: RegExp operand at "${a}" is not JSON-serializable (a regex lost to JSON.parse becomes {}). Only a direct $matches operand may be a RegExp.`);if(!(t===null||typeof t!="object")&&!(o>O)&&!s.has(t)){if(s.add(t),Array.isArray(t)){t.forEach((c,l)=>{r(c,`${a}[${l}]`,s,o+1);});return}for(let c of Object.keys(t))r(t[c],a?`${a}.${c}`:c,s,o+1);}}function i(t,a,s){if(typeof t=="bigint")throw new Error(`[Directive] validatePredicate: bigint operand at "${s}" is not JSON-serializable (JSON.stringify throws on bigint).`);if(t instanceof Set)throw new Error(`[Directive] validatePredicate: Set operand at "${s}" is not JSON-serializable (serializes to {} and loses all members).`);if(t instanceof Map)throw new Error(`[Directive] validatePredicate: Map operand at "${s}" is not JSON-serializable (serializes to {} and loses all entries).`);if(a==="$matches"&&!(t instanceof RegExp))throw new Error(`[Directive] validatePredicate: $matches operand at "${s}" must be a RegExp; got ${t===null?"null":typeof t}. A regex lost to JSON.parse becomes {} \u2014 reify with new RegExp(pattern, flags) before installing.`);if(Array.isArray(t))t.forEach((o,c)=>{r(o,`${s}[${c}]`,new WeakSet,1);});else if(x(t))for(let o of Object.keys(t))r(t[o],`${s}.${o}`,new WeakSet,1);}if(e instanceof Set)throw new Error(`[Directive] validatePredicate: Set operand${n?` at "${n}"`:""} is not JSON-serializable (serializes to {} and loses all members).`);if(e instanceof Map)throw new Error(`[Directive] validatePredicate: Map operand${n?` at "${n}"`:""} is not JSON-serializable (serializes to {} and loses all entries).`);h(e,{operator(t,a,s,o){i(s,a,n?`${n}.${o}`:o);},literal(t,a){i(a,"",n?`${n}.${t}`:t);}});}function M(e){return typeof e!="string"||e.length===0?false:!!(/\)(?:[+*]\??|\{\d+,?\d*\})\s*[+*?]\s*[+*]/.test(e)||/\(([^()]*?[+*]|[^()]*?\{\d+,?\d*\})[^()]*\)\s*[+*]/.test(e)||/\(\??:?([^()|]+)\|\1\)\s*[+*]/.test(e))}function se(e,n,r={}){let i=[],t=0,a=r.maxOperatorCount,s=r.maxArrayOperandLength;return h(e,{operator(o,c,l,p){if(t++,a!==void 0&&t>a){i.some($=>$.reason.includes("maxOperatorCount"))||i.push({path:o,op:c,reason:`Predicate exceeds maxOperatorCount=${a} \u2014 too many clauses (DoS guard).`});return}if(s!==void 0&&(c==="$in"||c==="$nin")&&Array.isArray(l)&&l.length>s){i.push({path:o,op:c,reason:`Operator ${c} operand exceeds maxArrayOperandLength=${s} (got ${l.length}) \u2014 too large for a query planner.`});return}if(c==="$matches"){let $;if(l instanceof RegExp?$=l.source:typeof l=="string"&&($=l),$!==void 0&&M($)){i.push({path:o,op:c,reason:`Operator $matches operand at "${o}" contains nested quantifiers (ReDoS risk: ${JSON.stringify($)}). Reject untrusted regex or rewrite without nested + / *.`});return}}let m=n.get(o);if(!m){i.push({path:o,op:c,reason:`Unknown fact "${o}" \u2014 not in schema. Known facts: ${n.size===0?"(empty schema)":Array.from(n.keys()).join(", ")}`});return}let y=w(m);y.includes(c)||i.push({path:o,op:c,kind:m,allowedOps:y,reason:`Operator "${c}" is not allowed on fact "${o}" of kind "${m.kind}". Allowed operators for this kind: ${y.join(", ")}.`});},literal(o,c){t++,n.has(o)||i.push({path:o,op:"$eq",reason:`Unknown fact "${o}" \u2014 not in schema.`});},strayOperatorKey(o,c){i.push({path:c,op:o,reason:`Stray operator key "${o}" at "${c}" \u2014 operators must live inside a fact's operator object, not at the predicate top level.`});}}),i.length===0?{ok:true,operatorCount:t}:{ok:false,errors:i,operatorCount:t}}function _(){return {ids:new WeakMap,next:{v:1},pairs:new Set}}function v(e,n){let r=e.ids.get(n);return r===void 0&&(r=e.next.v++,e.ids.set(n,r)),r}function d(e,n,r){if(Object.is(e,n))return true;if(e instanceof Date&&n instanceof Date)return e.getTime()===n.getTime();if(typeof e!="object"||typeof n!="object"||e===null||n===null)return false;let i=r??_(),t=`${v(i,e)}:${v(i,n)}`;if(i.pairs.has(t))return true;if(i.pairs.add(t),Array.isArray(e)||Array.isArray(n))return !Array.isArray(e)||!Array.isArray(n)||e.length!==n.length?false:e.every((o,c)=>d(o,n[c],i));if(e instanceof Set||n instanceof Set){if(!(e instanceof Set)||!(n instanceof Set)||e.size!==n.size)return false;let o=[...n];return [...e].every(c=>o.some(l=>d(c,l,i)))}if(e instanceof Map||n instanceof Map){if(!(e instanceof Map)||!(n instanceof Map)||e.size!==n.size)return false;let o=[...n.entries()],c=new Array(o.length).fill(false);for(let[l,p]of e){let m=false;for(let y=0;y<o.length;y++){if(c[y])continue;let[$,N]=o[y];if(d(l,$,i)&&d(p,N,i)){c[y]=true,m=true;break}}if(!m)return false}return true}let a=Object.keys(e),s=Object.keys(n);return a.length!==s.length?false:a.every(o=>Object.hasOwn(n,o)&&d(e[o],n[o],i))}function j(e){if(e instanceof Date)return e.getTime();if(typeof e=="number"||typeof e=="bigint"||typeof e=="string")return e}function A(e,n,r){let i=j(n),t=j(r);if(i===void 0||t===void 0||typeof i!=typeof t)return false;switch(e){case "$gt":return i>t;case "$gte":return i>=t;case "$lt":return i<t;case "$lte":return i<=t;default:return false}}function R(e,n,r,i){switch(e){case "$eq":return d(n,r);case "$ne":return !d(n,r);case "$in":return Array.isArray(r)&&r.some(t=>d(n,t));case "$nin":return Array.isArray(r)&&!r.some(t=>d(n,t));case "$exists":return r===(n!==void 0);case "$changed":return !d(n,i);case "$gt":case "$gte":case "$lt":case "$lte":return A(e,n,r);case "$between":{if(!Array.isArray(r)||r.length!==2)return false;let t=j(r[0]),a=j(r[1]);return t!==void 0&&a!==void 0&&typeof t==typeof a&&t>a?(f("$between: reversed pair \u2014 [min, max] required"),false):A("$gte",n,r[0])&&A("$lte",n,r[1])}case "$matches":{if(!(r instanceof RegExp))throw new Error("[Directive] $matches: operand must be a RegExp (string operands are no longer accepted; pass /pattern/flags directly).");return typeof n!="string"?false:r.test(n)}case "$startsWith":return typeof n!="string"?false:n.startsWith(String(r));case "$endsWith":return typeof n!="string"?false:n.endsWith(String(r));case "$contains":return typeof n=="string"?n.includes(String(r)):Array.isArray(n)?n.some(t=>d(t,r)):n instanceof Set?n.has(r):false;default:return false}}function f(e){a&&console.warn(`[Directive] ${e}`);}function q(e,n,r,i){if(E(e)){let t=Object.keys(e);t.length>1&&f(`predicate: operator object has ${t.length} operators (${t.join(", ")}) \u2014 write the array form or $all instead. The runtime ANDs them as a best-effort fallback.`);for(let a of t)if(!R(a,n,e[a],r))return false;return true}return u(e)?S(e,u(n)?n:Object.create(null),u(r)?r:void 0,i+1):d(n,e)}function S(e,n,r,i=0){if(i>O)return f(`predicate depth limit (${O}) exceeded \u2014 flatten the predicate or split it into multiple constraints. If this is unexpected, check for a cyclic spec object.`),false;if(Array.isArray(e))return e.every(t=>{if(!u(t))return false;let{fact:a,op:s,value:o}=t;return R(s,n?.[a],o,r?.[a])});if(!u(e))return !!e;if("$all"in e)return e.$all.every(t=>S(t,n,r,i+1));if("$any"in e)return e.$any.some(t=>S(t,n,r,i+1));if("$not"in e)return !S(e.$not,n,r,i+1);for(let t of Object.keys(e)){if(g.has(t))return f(`predicate: operator "${t}" mixed with fact keys \u2014 wrap operators in a per-fact object`),false;if(!q(e[t],n?.[t],r?.[t],i))return false}return true}function V(e,n,r,i=""){let t=[];if(Array.isArray(e)){for(let a of e){if(!u(a))continue;let{fact:s,op:o,value:c}=a,l=n?.[s];t.push({path:i+s,op:o,expected:c,actual:l,pass:R(o,l,c,r?.[s])});}return t}if(!u(e))return t;for(let a of ["$all","$any","$not"])if(a in e){let s=a==="$not"?[e.$not]:e[a],o=[];for(let p of s)o.push(...V(p,n,r,i));let c=o.filter(p=>p.pass).length,l;return a==="$all"?l=o.length===0||c===o.length:a==="$any"?l=o.length>0&&c>0:l=!o.every(p=>p.pass),t.push({path:i||a,op:a,expected:s.length,actual:c,pass:l,children:o}),t}for(let a of Object.keys(e)){if(g.has(a))continue;let s=e[a],o=n?.[a],c=i+a;if(E(s))for(let l of Object.keys(s))t.push({path:c,op:l,expected:s[l],actual:o,pass:R(l,o,s[l],r?.[a])});else u(s)?t.push(...V(s,u(o)?o:Object.create(null),u(r?.[a])?r?.[a]:void 0,`${c}.`)):t.push({path:c,op:"$eq",expected:s,actual:o,pass:d(o,s)});}return t}var F=new WeakMap;function ce(e){if(e===null||typeof e!="object")throw new Error(`[Directive] memoizePredicate: predicate must be a plain object or array; got ${typeof e}`);let n=F.get(e);if(n)return n;let r=(i,t)=>S(e,i,t);return F.set(e,r),r}function le(e,n=""){let r=new Set;return h(e,{operator(i){r.add(n+i);},literal(i){r.add(n+i);},strayOperatorKey(i){g.has(i)||f(`extractDeps: unknown operator "${i}" \u2014 skipping. Known operators: ${[...g].join(", ")}`);}}),r}var C=/^[A-Za-z_][A-Za-z0-9_]*$/;function J(e){return typeof e=="symbol"||e==null?"":String(e)}function B(e,n){return typeof e=="symbol"?(f("template: cannot interpolate a symbol value \u2014 using empty string"),""):e===void 0?(f(`template: ${n?`key "${n}" is `:""}undefined \u2014 using empty string`),""):e===null?(f(`template: ${n?`key "${n}" is `:""}null \u2014 using empty string`),""):String(e)}function L(e,n){let r=e.$template,i="",t=0;for(;t<r.length;){if(r[t]==="$"&&r[t+1]==="$"&&r[t+2]==="{"){i+="${",t+=3;continue}if(r[t]==="$"&&r[t+1]==="{"){let a=r.indexOf("}",t+2);if(a===-1){f(`template: unterminated "\${" in ${JSON.stringify(r)}`),i+=r.slice(t);break}let s=r.slice(t+2,a);if(!C.test(s))f(`template: invalid placeholder "\${${s}}" \u2014 not an identifier`);else {let o=n!=null&&Object.hasOwn(n,s),c=o?n[s]:void 0;o?i+=B(c,s):(f(`template: unknown key "${s}"`),i+=J(c));}t=a+1;continue}i+=r[t],t++;}return i}function ue(e){let n=new Set,r=e.$template,i=0;for(;i<r.length;){if(r[i]==="$"&&r[i+1]==="$"&&r[i+2]==="{"){i+=3;continue}if(r[i]==="$"&&r[i+1]==="{"){let t=r.indexOf("}",i+2);if(t===-1)break;let a=r.slice(i+2,t);C.test(a)&&n.add(a),i=t+1;continue}i++;}return n}function de(e$1,n){return e$1.map(r=>e(n?.[r])).join("|")}function fe(e,n,r){let i=e.$set,t=r??{};for(let a of Object.keys(i)){let s=i[a];if(I(s))n[a]=L(s,t);else if(u(s)&&Object.hasOwn(s,"$ref")&&typeof s.$ref=="string"){let o=s.$ref;Object.hasOwn(t,o)||f(`applyPatch: $ref "${o}" is missing from event payload \u2014 assigning undefined to fact "${a}"`),n[a]=t[o];}else n[a]=s;}}export{g as a,Q as b,z as c,H as d,w as e,X as f,O as g,re as h,h as i,ie as j,I as k,oe as l,M as m,se as n,S as o,V as p,ce as q,le as r,L as s,ue as t,de as u,fe as v};//# sourceMappingURL=chunk-2FF6QGOA.js.map
|
|
2
|
+
//# sourceMappingURL=chunk-2FF6QGOA.js.map
|