@its-not-rocket-science/ananke 0.1.69 → 0.5.4
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/CHANGELOG.md +288 -0
- package/README.md +150 -432
- package/STABLE_API.md +113 -490
- package/conformance/README.md +46 -44
- package/conformance/lockstep-sequence.json +395 -395
- package/conformance/phase-order.json +21 -0
- package/conformance/replay-parity.json +65 -65
- package/conformance/state-hash-regression.json +39 -0
- package/conformance/state-hash.json +23 -22
- package/dist/src/conformance.d.ts +8 -2
- package/dist/src/content/index.d.ts +3 -0
- package/dist/src/content/index.js +2 -0
- package/dist/src/content/injector.d.ts +19 -0
- package/dist/src/content/injector.js +61 -0
- package/dist/src/content/loader.d.ts +3 -0
- package/dist/src/content/loader.js +170 -0
- package/dist/src/content/types.d.ts +56 -0
- package/dist/src/content/types.js +1 -0
- package/dist/src/content/validator.d.ts +6 -0
- package/dist/src/content/validator.js +14 -0
- package/dist/src/content-pack.d.ts +3 -2
- package/dist/src/content-pack.js +205 -10
- package/dist/src/data-governance.d.ts +73 -0
- package/dist/src/data-governance.js +188 -0
- package/dist/src/debug.d.ts +6 -0
- package/dist/src/debug.js +9 -0
- package/dist/src/determinism.d.ts +12 -0
- package/dist/src/determinism.js +93 -0
- package/dist/src/dialogue.d.ts +41 -0
- package/dist/src/dialogue.js +113 -2
- package/dist/src/economy.d.ts +97 -3
- package/dist/src/economy.js +191 -37
- package/dist/src/history/autosave.d.ts +19 -0
- package/dist/src/history/autosave.js +98 -0
- package/dist/src/history/timetravel.d.ts +28 -0
- package/dist/src/history/timetravel.js +61 -0
- package/dist/src/index.d.ts +19 -31
- package/dist/src/index.js +20 -38
- package/dist/src/model3d.js +63 -2
- package/dist/src/narrative/combat-logger.d.ts +25 -0
- package/dist/src/narrative/combat-logger.js +97 -0
- package/dist/src/narrative/plausibility.d.ts +30 -0
- package/dist/src/narrative/plausibility.js +70 -0
- package/dist/src/narrative-layer.d.ts +2 -0
- package/dist/src/narrative-layer.js +2 -0
- package/dist/src/navigation/causal-chain.d.ts +26 -0
- package/dist/src/navigation/causal-chain.js +74 -0
- package/dist/src/netcode.d.ts +1 -1
- package/dist/src/netcode.js +1 -1
- package/dist/src/performance/adaptive-tick.d.ts +23 -0
- package/dist/src/performance/adaptive-tick.js +50 -0
- package/dist/src/plugins/loader.d.ts +23 -0
- package/dist/src/plugins/loader.js +86 -0
- package/dist/src/plugins/registry.d.ts +11 -0
- package/dist/src/plugins/registry.js +28 -0
- package/dist/src/plugins/types.d.ts +31 -0
- package/dist/src/plugins/types.js +7 -0
- package/dist/src/replay.js +5 -2
- package/dist/src/scenario.js +2 -1
- package/dist/src/serialization/binary.d.ts +5 -0
- package/dist/src/serialization/binary.js +193 -0
- package/dist/src/session.d.ts +115 -0
- package/dist/src/session.js +228 -0
- package/dist/src/sim/ai/decide.js +1 -1
- package/dist/src/sim/ai/personality.js +2 -2
- package/dist/src/sim/bodyplan.d.ts +30 -0
- package/dist/src/sim/context.d.ts +2 -0
- package/dist/src/sim/kernel.js +80 -1294
- package/dist/src/sim/normalization.d.ts +12 -0
- package/dist/src/sim/normalization.js +92 -0
- package/dist/src/sim/resolvers/attack-resolver.d.ts +30 -0
- package/dist/src/sim/resolvers/attack-resolver.js +310 -0
- package/dist/src/sim/resolvers/capability-resolver.d.ts +15 -0
- package/dist/src/sim/resolvers/capability-resolver.js +270 -0
- package/dist/src/sim/resolvers/grapple-resolver.d.ts +28 -0
- package/dist/src/sim/resolvers/grapple-resolver.js +70 -0
- package/dist/src/sim/resolvers/shoot-resolver.d.ts +27 -0
- package/dist/src/sim/resolvers/shoot-resolver.js +212 -0
- package/dist/src/sim/resolvers/treat-resolver.d.ts +16 -0
- package/dist/src/sim/resolvers/treat-resolver.js +77 -0
- package/dist/src/sim/step/apply/intents.d.ts +6 -0
- package/dist/src/sim/step/apply/intents.js +83 -0
- package/dist/src/sim/step/kernel-pipeline.d.ts +9 -0
- package/dist/src/sim/step/kernel-pipeline.js +15 -0
- package/dist/src/sim/step/phases/capability-phase.d.ts +8 -0
- package/dist/src/sim/step/phases/capability-phase.js +65 -0
- package/dist/src/sim/step/phases/cooldowns-phase.d.ts +3 -0
- package/dist/src/sim/step/phases/cooldowns-phase.js +37 -0
- package/dist/src/sim/step/phases/prepare-phase.d.ts +10 -0
- package/dist/src/sim/step/phases/prepare-phase.js +33 -0
- package/dist/src/sim/step/pipeline.d.ts +2 -0
- package/dist/src/sim/step/pipeline.js +15 -0
- package/dist/src/sim/step/resolvers/impact-resolver.d.ts +9 -0
- package/dist/src/sim/step/resolvers/impact-resolver.js +55 -0
- package/dist/src/sim/step/world-phases.d.ts +7 -0
- package/dist/src/sim/step/world-phases.js +15 -0
- package/dist/src/sim/step/world-step-context.d.ts +23 -0
- package/dist/src/sim/step/world-step-context.js +1 -0
- package/dist/src/sim/systemic-toxicology.js +1 -1
- package/dist/src/sim/team.js +2 -2
- package/dist/src/sim/testing.js +4 -3
- package/dist/src/sim/toxicology.d.ts +1 -1
- package/dist/src/sim/toxicology.js +1 -1
- package/dist/src/sim/world.d.ts +17 -24
- package/dist/src/snapshot.js +3 -6
- package/dist/src/tier2.d.ts +19 -0
- package/dist/src/tier2.js +21 -0
- package/dist/src/tier3.d.ts +10 -0
- package/dist/src/tier3.js +12 -0
- package/dist/src/version.d.ts +6 -0
- package/dist/src/version.js +8 -0
- package/dist/src/wasm/bridge.d.ts +14 -0
- package/dist/src/wasm/bridge.js +84 -0
- package/dist/src/wasm-kernel.d.ts +2 -0
- package/dist/src/wasm-kernel.js +1 -0
- package/dist/src/world-evolution-backend/engine.d.ts +4 -0
- package/dist/src/world-evolution-backend/engine.js +347 -0
- package/dist/src/world-evolution-backend/host-schema.d.ts +145 -0
- package/dist/src/world-evolution-backend/host-schema.js +375 -0
- package/dist/src/world-evolution-backend/index.d.ts +1 -0
- package/dist/src/world-evolution-backend/index.js +1 -0
- package/dist/src/world-evolution-backend/open-world-host-adapter.d.ts +96 -0
- package/dist/src/world-evolution-backend/open-world-host-adapter.js +208 -0
- package/dist/src/world-evolution-backend/profiles.d.ts +4 -0
- package/dist/src/world-evolution-backend/profiles.js +136 -0
- package/dist/src/world-evolution-backend/public.d.ts +11 -0
- package/dist/src/world-evolution-backend/public.js +7 -0
- package/dist/src/world-evolution-backend/reproducibility.d.ts +8 -0
- package/dist/src/world-evolution-backend/reproducibility.js +35 -0
- package/dist/src/world-evolution-backend/timeline.d.ts +6 -0
- package/dist/src/world-evolution-backend/timeline.js +337 -0
- package/dist/src/world-evolution-backend/types.d.ts +138 -0
- package/dist/src/world-evolution-backend/types.js +1 -0
- package/dist/src/world-evolution-host-backend.d.ts +51 -0
- package/dist/src/world-evolution-host-backend.js +97 -0
- package/dist/src/world-evolution.d.ts +201 -0
- package/dist/src/world-evolution.js +543 -0
- package/dist/src/world-factory.js +2 -1
- package/dist/tools/pack-cli.js +55 -2
- package/docs/bridge-contract.md +140 -332
- package/docs/completion-evidence-dashboard.md +63 -0
- package/docs/host-contract.md +86 -310
- package/docs/host-world-evolution-schema.md +155 -0
- package/docs/integration-primer.md +29 -22
- package/docs/migration-monolith-to-modular.md +102 -120
- package/docs/package-architecture.md +116 -323
- package/docs/plugins/README.md +42 -0
- package/docs/plugins/hooks.md +22 -0
- package/docs/plugins/publishing.md +29 -0
- package/docs/plugins/security-model.md +273 -0
- package/docs/project-overview.md +30 -30
- package/docs/release-dashboard.md +47 -0
- package/docs/session-api.md +127 -0
- package/docs/stable-api-manifest.json +52 -0
- package/docs/trust-dashboard.md +37 -0
- package/docs/versioning.md +256 -245
- package/docs/wire-protocol.md +140 -209
- package/docs/world-evolution-backend.md +200 -0
- package/docs/world-evolution-engine.md +150 -0
- package/docs/world-evolution-host-backend.md +70 -0
- package/docs/world-evolution-orchestration.md +248 -0
- package/package.json +125 -6
- package/schema/content-pack.schema.json +85 -0
- package/schema/plugin.schema.json +57 -0
- package/schema/world-evolution-input.schema.json +196 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export interface ContentPackRegistry {
|
|
2
|
+
compatRange?: string;
|
|
3
|
+
[key: string]: unknown;
|
|
4
|
+
}
|
|
5
|
+
export interface WeaponDamageProfile {
|
|
6
|
+
surfaceFrac: number;
|
|
7
|
+
internalFrac: number;
|
|
8
|
+
structuralFrac: number;
|
|
9
|
+
bleedFactor: number;
|
|
10
|
+
penetrationBias: number;
|
|
11
|
+
}
|
|
12
|
+
export interface ContentWeapon {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
mass_kg: number;
|
|
16
|
+
damage: WeaponDamageProfile;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
export interface ContentArmour {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
mass_kg: number;
|
|
23
|
+
resist_J: number;
|
|
24
|
+
protectedDamageMul: number;
|
|
25
|
+
coverageByRegion: Record<string, number>;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
export interface ContentArchetype {
|
|
29
|
+
id: string;
|
|
30
|
+
base?: string;
|
|
31
|
+
overrides?: Record<string, unknown>;
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
}
|
|
34
|
+
export interface ContentTerrain {
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
tags?: string[];
|
|
38
|
+
tractionMul?: number;
|
|
39
|
+
cover?: number;
|
|
40
|
+
[key: string]: unknown;
|
|
41
|
+
}
|
|
42
|
+
export interface ContentPack {
|
|
43
|
+
$schema?: string;
|
|
44
|
+
name: string;
|
|
45
|
+
version: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
registry?: ContentPackRegistry;
|
|
48
|
+
weapons?: ContentWeapon[];
|
|
49
|
+
armour?: ContentArmour[];
|
|
50
|
+
archetypes?: ContentArchetype[];
|
|
51
|
+
terrain?: ContentTerrain[];
|
|
52
|
+
}
|
|
53
|
+
export interface ContentPackValidationError {
|
|
54
|
+
path: string;
|
|
55
|
+
message: string;
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ContentPack, ContentPackValidationError } from "./types.js";
|
|
2
|
+
export interface ContentValidationResult {
|
|
3
|
+
schemaErrors: ContentPackValidationError[];
|
|
4
|
+
semanticWarnings: ContentPackValidationError[];
|
|
5
|
+
}
|
|
6
|
+
export declare function runContentSemanticChecks(pack: ContentPack): ContentPackValidationError[];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function runContentSemanticChecks(pack) {
|
|
2
|
+
const warnings = [];
|
|
3
|
+
(pack.weapons ?? []).forEach((weapon, idx) => {
|
|
4
|
+
const damage = weapon.damage;
|
|
5
|
+
const roughDamage = (damage.surfaceFrac + damage.internalFrac + damage.structuralFrac) * 1000;
|
|
6
|
+
if (roughDamage > 1000) {
|
|
7
|
+
warnings.push({
|
|
8
|
+
path: `$.weapons[${idx}].damage`,
|
|
9
|
+
message: `weapon damage > 1000 is likely wrong (score=${roughDamage.toFixed(2)})`,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
return warnings;
|
|
14
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { WorldState } from "./sim/world.js";
|
|
2
|
-
|
|
3
|
-
export declare const ANANKE_ENGINE_VERSION = "0.1.69";
|
|
2
|
+
export { ANANKE_ENGINE_VERSION } from "./version.js";
|
|
4
3
|
/** A single actionable validation failure from `validatePack`. */
|
|
5
4
|
export interface PackValidationError {
|
|
6
5
|
/** JSONPath-style location, e.g. `"$.weapons[2].mass_kg"`. */
|
|
@@ -135,6 +134,8 @@ export declare function semverSatisfies(version: string, range: string): boolean
|
|
|
135
134
|
* @returns Array of `PackValidationError`. Empty means valid.
|
|
136
135
|
*/
|
|
137
136
|
export declare function validatePack(manifest: unknown): PackValidationError[];
|
|
137
|
+
/** Compute SHA-256 for a manifest's canonical JSON with `registry.checksum` blanked. */
|
|
138
|
+
export declare function computePackChecksum(manifest: AnankePackManifest): string;
|
|
138
139
|
/**
|
|
139
140
|
* Validate and load a pack manifest into the active catalogues.
|
|
140
141
|
*
|
package/dist/src/content-pack.js
CHANGED
|
@@ -13,19 +13,19 @@ import { registerWeapon, registerArmour, registerArchetype } from "./catalog.js"
|
|
|
13
13
|
import { validateScenario, loadScenario } from "./scenario.js";
|
|
14
14
|
import { hashMod } from "./modding.js";
|
|
15
15
|
import { registerWorldArchetype, registerWorldItem } from "./world-factory.js";
|
|
16
|
-
// ──
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
export const ANANKE_ENGINE_VERSION = "0.1.69";
|
|
16
|
+
// ── Runtime version constant ─────────────────────────────────────────────────────
|
|
17
|
+
export { ANANKE_ENGINE_VERSION } from "./version.js";
|
|
18
|
+
import { ANANKE_ENGINE_VERSION } from "./version.js";
|
|
20
19
|
// ── Semver utilities ──────────────────────────────────────────────────────────
|
|
21
20
|
// Lightweight range evaluator — no external dependencies.
|
|
22
21
|
// Supports: >=X.Y.Z >X.Y.Z <=X.Y.Z <X.Y.Z =X.Y.Z ^X.Y.Z ~X.Y.Z
|
|
23
22
|
// Short forms X.Y and X treated as X.Y.0 and X.0.0 respectively.
|
|
24
23
|
// Compound ranges (space-separated) require all constraints to match.
|
|
25
24
|
function parseSemverTuple(v) {
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
25
|
+
const raw = v.trim().replace(/^v/, "");
|
|
26
|
+
if (!/^\d+(\.\d+){0,2}$/.test(raw))
|
|
28
27
|
return null;
|
|
28
|
+
const parts = raw.split(".").map(Number);
|
|
29
29
|
const [major = 0, minor = 0, patch = 0] = parts;
|
|
30
30
|
return [major, minor, patch];
|
|
31
31
|
}
|
|
@@ -44,13 +44,46 @@ export function semverSatisfies(version, range) {
|
|
|
44
44
|
const ver = parseSemverTuple(version);
|
|
45
45
|
if (!ver)
|
|
46
46
|
return false;
|
|
47
|
-
const
|
|
47
|
+
const trimmed = range.trim();
|
|
48
|
+
if (trimmed.length === 0)
|
|
49
|
+
return false;
|
|
50
|
+
const constraints = trimmed.split(/\s+/);
|
|
48
51
|
for (const constraint of constraints) {
|
|
52
|
+
if (constraint.length === 0)
|
|
53
|
+
return false;
|
|
49
54
|
if (!evalConstraint(ver, constraint.trim()))
|
|
50
55
|
return false;
|
|
51
56
|
}
|
|
52
57
|
return true;
|
|
53
58
|
}
|
|
59
|
+
function isSemverRange(range) {
|
|
60
|
+
const trimmed = range.trim();
|
|
61
|
+
if (trimmed.length === 0)
|
|
62
|
+
return false;
|
|
63
|
+
const constraints = trimmed.split(/\s+/);
|
|
64
|
+
for (const constraint of constraints) {
|
|
65
|
+
if (constraint.length === 0)
|
|
66
|
+
return false;
|
|
67
|
+
if (constraint.startsWith("^") || constraint.startsWith("~")) {
|
|
68
|
+
if (parseSemverTuple(constraint.slice(1)) === null)
|
|
69
|
+
return false;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (constraint.startsWith(">=") ||
|
|
73
|
+
constraint.startsWith("<=") ||
|
|
74
|
+
constraint.startsWith(">") ||
|
|
75
|
+
constraint.startsWith("<") ||
|
|
76
|
+
constraint.startsWith("=")) {
|
|
77
|
+
const offset = constraint.startsWith(">=") || constraint.startsWith("<=") ? 2 : 1;
|
|
78
|
+
if (parseSemverTuple(constraint.slice(offset)) === null)
|
|
79
|
+
return false;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (parseSemverTuple(constraint) === null)
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
54
87
|
function evalConstraint(ver, c) {
|
|
55
88
|
// Caret: ^X.Y.Z — compatible within the leftmost non-zero component.
|
|
56
89
|
// ^1.2.3 → >=1.2.3 <2.0.0 (major locked when major > 0)
|
|
@@ -125,9 +158,23 @@ export function validatePack(manifest) {
|
|
|
125
158
|
}
|
|
126
159
|
// Required: version (semver-ish)
|
|
127
160
|
if (typeof m["version"] !== "string" ||
|
|
128
|
-
|
|
161
|
+
parseSemverTuple(m["version"]) === null) {
|
|
129
162
|
errors.push({ path: "$.version", message: 'must be a semver string like "1.0.0" or "1.0"' });
|
|
130
163
|
}
|
|
164
|
+
if (m["anankeVersion"] !== undefined) {
|
|
165
|
+
if (typeof m["anankeVersion"] !== "string") {
|
|
166
|
+
errors.push({ path: "$.anankeVersion", message: "must be a semver range string" });
|
|
167
|
+
}
|
|
168
|
+
else if (!isSemverRange(m["anankeVersion"])) {
|
|
169
|
+
errors.push({ path: "$.anankeVersion", message: "must be a valid semver range expression" });
|
|
170
|
+
}
|
|
171
|
+
else if (!semverSatisfies(ANANKE_ENGINE_VERSION, m["anankeVersion"])) {
|
|
172
|
+
errors.push({
|
|
173
|
+
path: "$.anankeVersion",
|
|
174
|
+
message: `engine version ${ANANKE_ENGINE_VERSION} does not satisfy range "${m["anankeVersion"]}"`,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
131
178
|
// Optional: registry block
|
|
132
179
|
if (m["registry"] !== undefined) {
|
|
133
180
|
if (typeof m["registry"] !== "object" || m["registry"] === null || Array.isArray(m["registry"])) {
|
|
@@ -140,6 +187,9 @@ export function validatePack(manifest) {
|
|
|
140
187
|
if (typeof reg["compatRange"] !== "string") {
|
|
141
188
|
errors.push({ path: "$.registry.compatRange", message: "must be a string" });
|
|
142
189
|
}
|
|
190
|
+
else if (!isSemverRange(reg["compatRange"])) {
|
|
191
|
+
errors.push({ path: "$.registry.compatRange", message: "must be a valid semver range expression" });
|
|
192
|
+
}
|
|
143
193
|
else if (!semverSatisfies(ANANKE_ENGINE_VERSION, reg["compatRange"])) {
|
|
144
194
|
errors.push({
|
|
145
195
|
path: "$.registry.compatRange",
|
|
@@ -205,20 +255,39 @@ export function validatePack(manifest) {
|
|
|
205
255
|
}
|
|
206
256
|
// Validate weapon entries
|
|
207
257
|
if (Array.isArray(m["weapons"])) {
|
|
258
|
+
const ids = new Set();
|
|
208
259
|
for (let i = 0; i < m["weapons"].length; i++) {
|
|
209
260
|
const w = m["weapons"][i];
|
|
210
261
|
errors.push(...validateWeaponEntry(w, i));
|
|
262
|
+
if (typeof w === "object" && w !== null) {
|
|
263
|
+
const id = w["id"];
|
|
264
|
+
if (typeof id === "string") {
|
|
265
|
+
if (ids.has(id))
|
|
266
|
+
errors.push({ path: `$.weapons[${i}].id`, message: `duplicate weapon id "${id}"` });
|
|
267
|
+
ids.add(id);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
211
270
|
}
|
|
212
271
|
}
|
|
213
272
|
// Validate armour entries
|
|
214
273
|
if (Array.isArray(m["armour"])) {
|
|
274
|
+
const ids = new Set();
|
|
215
275
|
for (let i = 0; i < m["armour"].length; i++) {
|
|
216
276
|
const a = m["armour"][i];
|
|
217
277
|
errors.push(...validateArmourEntry(a, i));
|
|
278
|
+
if (typeof a === "object" && a !== null) {
|
|
279
|
+
const id = a["id"];
|
|
280
|
+
if (typeof id === "string") {
|
|
281
|
+
if (ids.has(id))
|
|
282
|
+
errors.push({ path: `$.armour[${i}].id`, message: `duplicate armour id "${id}"` });
|
|
283
|
+
ids.add(id);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
218
286
|
}
|
|
219
287
|
}
|
|
220
288
|
// Validate archetype entries (minimal — full validation is in registerArchetype)
|
|
221
289
|
if (Array.isArray(m["archetypes"])) {
|
|
290
|
+
const ids = new Set();
|
|
222
291
|
for (let i = 0; i < m["archetypes"].length; i++) {
|
|
223
292
|
const arch = m["archetypes"][i];
|
|
224
293
|
if (typeof arch !== "object" || arch === null) {
|
|
@@ -229,19 +298,130 @@ export function validatePack(manifest) {
|
|
|
229
298
|
if (typeof o["id"] !== "string" || o["id"].trim() === "") {
|
|
230
299
|
errors.push({ path: `$.archetypes[${i}].id`, message: "must be a non-empty string" });
|
|
231
300
|
}
|
|
301
|
+
else {
|
|
302
|
+
if (ids.has(o["id"]))
|
|
303
|
+
errors.push({ path: `$.archetypes[${i}].id`, message: `duplicate archetype id "${o["id"]}"` });
|
|
304
|
+
ids.add(o["id"]);
|
|
305
|
+
}
|
|
232
306
|
}
|
|
233
307
|
}
|
|
234
308
|
// Validate scenario entries via existing validateScenario
|
|
235
309
|
if (Array.isArray(m["scenarios"])) {
|
|
310
|
+
const ids = new Set();
|
|
236
311
|
for (let i = 0; i < m["scenarios"].length; i++) {
|
|
237
|
-
const
|
|
312
|
+
const scenario = m["scenarios"][i];
|
|
313
|
+
if (typeof scenario === "object" && scenario !== null) {
|
|
314
|
+
const id = scenario["id"];
|
|
315
|
+
if (typeof id === "string") {
|
|
316
|
+
if (ids.has(id))
|
|
317
|
+
errors.push({ path: `$.scenarios[${i}].id`, message: `duplicate scenario id "${id}"` });
|
|
318
|
+
ids.add(id);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const scenErrors = validateScenario(scenario);
|
|
238
322
|
for (const msg of scenErrors) {
|
|
239
323
|
errors.push({ path: `$.scenarios[${i}]`, message: msg });
|
|
240
324
|
}
|
|
241
325
|
}
|
|
242
326
|
}
|
|
327
|
+
const checksum = m["registry"]?.["checksum"];
|
|
328
|
+
if (typeof checksum === "string" && /^[0-9a-f]{64}$/.test(checksum)) {
|
|
329
|
+
const computed = computePackChecksum(m);
|
|
330
|
+
if (computed !== checksum) {
|
|
331
|
+
errors.push({
|
|
332
|
+
path: "$.registry.checksum",
|
|
333
|
+
message: `checksum mismatch: expected "${checksum}" but computed "${computed}"`,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
243
337
|
return errors;
|
|
244
338
|
}
|
|
339
|
+
/** Compute SHA-256 for a manifest's canonical JSON with `registry.checksum` blanked. */
|
|
340
|
+
export function computePackChecksum(manifest) {
|
|
341
|
+
const clone = JSON.parse(JSON.stringify(manifest));
|
|
342
|
+
const registry = clone["registry"];
|
|
343
|
+
if (typeof registry === "object" && registry !== null && !Array.isArray(registry)) {
|
|
344
|
+
registry["checksum"] = "";
|
|
345
|
+
}
|
|
346
|
+
return sha256Hex(JSON.stringify(clone));
|
|
347
|
+
}
|
|
348
|
+
/** Portable SHA-256 (sync) used by pack checksum validation in both Node and browser/worker runtimes. */
|
|
349
|
+
function sha256Hex(input) {
|
|
350
|
+
const bytes = new TextEncoder().encode(input);
|
|
351
|
+
const K = [
|
|
352
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
353
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
354
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
355
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
356
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
357
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
358
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
359
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
360
|
+
];
|
|
361
|
+
const H = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
|
|
362
|
+
const paddedLength = (((bytes.length + 9 + 63) >> 6) << 6);
|
|
363
|
+
const data = new Uint8Array(paddedLength);
|
|
364
|
+
data.set(bytes);
|
|
365
|
+
data[bytes.length] = 0x80;
|
|
366
|
+
const bitLenHi = Math.floor((bytes.length * 8) / 0x100000000);
|
|
367
|
+
const bitLenLo = (bytes.length * 8) >>> 0;
|
|
368
|
+
data[paddedLength - 8] = (bitLenHi >>> 24) & 0xff;
|
|
369
|
+
data[paddedLength - 7] = (bitLenHi >>> 16) & 0xff;
|
|
370
|
+
data[paddedLength - 6] = (bitLenHi >>> 8) & 0xff;
|
|
371
|
+
data[paddedLength - 5] = bitLenHi & 0xff;
|
|
372
|
+
data[paddedLength - 4] = (bitLenLo >>> 24) & 0xff;
|
|
373
|
+
data[paddedLength - 3] = (bitLenLo >>> 16) & 0xff;
|
|
374
|
+
data[paddedLength - 2] = (bitLenLo >>> 8) & 0xff;
|
|
375
|
+
data[paddedLength - 1] = bitLenLo & 0xff;
|
|
376
|
+
const w = new Uint32Array(64);
|
|
377
|
+
for (let i = 0; i < data.length; i += 64) {
|
|
378
|
+
for (let t = 0; t < 16; t += 1) {
|
|
379
|
+
const j = i + (t << 2);
|
|
380
|
+
w[t] = ((data[j] << 24) | (data[j + 1] << 16) | (data[j + 2] << 8) | data[j + 3]) >>> 0;
|
|
381
|
+
}
|
|
382
|
+
for (let t = 16; t < 64; t += 1) {
|
|
383
|
+
const s0 = (rotR(w[t - 15], 7) ^ rotR(w[t - 15], 18) ^ (w[t - 15] >>> 3)) >>> 0;
|
|
384
|
+
const s1 = (rotR(w[t - 2], 17) ^ rotR(w[t - 2], 19) ^ (w[t - 2] >>> 10)) >>> 0;
|
|
385
|
+
w[t] = (w[t - 16] + s0 + w[t - 7] + s1) >>> 0;
|
|
386
|
+
}
|
|
387
|
+
let a = H[0];
|
|
388
|
+
let b = H[1];
|
|
389
|
+
let c = H[2];
|
|
390
|
+
let d = H[3];
|
|
391
|
+
let e = H[4];
|
|
392
|
+
let f = H[5];
|
|
393
|
+
let g = H[6];
|
|
394
|
+
let h = H[7];
|
|
395
|
+
for (let t = 0; t < 64; t += 1) {
|
|
396
|
+
const S1 = (rotR(e, 6) ^ rotR(e, 11) ^ rotR(e, 25)) >>> 0;
|
|
397
|
+
const ch = ((e & f) ^ (~e & g)) >>> 0;
|
|
398
|
+
const temp1 = (h + S1 + ch + K[t] + w[t]) >>> 0;
|
|
399
|
+
const S0 = (rotR(a, 2) ^ rotR(a, 13) ^ rotR(a, 22)) >>> 0;
|
|
400
|
+
const maj = ((a & b) ^ (a & c) ^ (b & c)) >>> 0;
|
|
401
|
+
const temp2 = (S0 + maj) >>> 0;
|
|
402
|
+
h = g;
|
|
403
|
+
g = f;
|
|
404
|
+
f = e;
|
|
405
|
+
e = (d + temp1) >>> 0;
|
|
406
|
+
d = c;
|
|
407
|
+
c = b;
|
|
408
|
+
b = a;
|
|
409
|
+
a = (temp1 + temp2) >>> 0;
|
|
410
|
+
}
|
|
411
|
+
H[0] = (H[0] + a) >>> 0;
|
|
412
|
+
H[1] = (H[1] + b) >>> 0;
|
|
413
|
+
H[2] = (H[2] + c) >>> 0;
|
|
414
|
+
H[3] = (H[3] + d) >>> 0;
|
|
415
|
+
H[4] = (H[4] + e) >>> 0;
|
|
416
|
+
H[5] = (H[5] + f) >>> 0;
|
|
417
|
+
H[6] = (H[6] + g) >>> 0;
|
|
418
|
+
H[7] = (H[7] + h) >>> 0;
|
|
419
|
+
}
|
|
420
|
+
return H.map((n) => n.toString(16).padStart(8, "0")).join("");
|
|
421
|
+
}
|
|
422
|
+
function rotR(x, n) {
|
|
423
|
+
return (x >>> n) | (x << (32 - n));
|
|
424
|
+
}
|
|
245
425
|
function validateWeaponEntry(w, i) {
|
|
246
426
|
const errors = [];
|
|
247
427
|
if (typeof w !== "object" || w === null) {
|
|
@@ -297,9 +477,22 @@ function validateArmourEntry(a, i) {
|
|
|
297
477
|
*/
|
|
298
478
|
export function loadPack(manifest) {
|
|
299
479
|
const packId = `${manifest.name}@${manifest.version}`;
|
|
480
|
+
const fingerprint = hashMod(manifest);
|
|
300
481
|
// Already loaded — return stored summary
|
|
301
482
|
const existing = _packs.get(packId);
|
|
302
483
|
if (existing !== undefined) {
|
|
484
|
+
if (existing.fingerprint !== fingerprint) {
|
|
485
|
+
return {
|
|
486
|
+
packId,
|
|
487
|
+
registeredIds: [],
|
|
488
|
+
scenarioIds: [],
|
|
489
|
+
fingerprint: existing.fingerprint,
|
|
490
|
+
errors: [{
|
|
491
|
+
path: "$",
|
|
492
|
+
message: `pack "${packId}" already loaded with different content fingerprint (${existing.fingerprint} != ${fingerprint})`,
|
|
493
|
+
}],
|
|
494
|
+
};
|
|
495
|
+
}
|
|
303
496
|
return {
|
|
304
497
|
packId,
|
|
305
498
|
registeredIds: existing.registeredIds,
|
|
@@ -359,7 +552,9 @@ export function loadPack(manifest) {
|
|
|
359
552
|
scenarioMap.set(id, scen);
|
|
360
553
|
scenarioIds.push(id);
|
|
361
554
|
}
|
|
362
|
-
|
|
555
|
+
if (loadErrors.length > 0) {
|
|
556
|
+
return { packId, registeredIds, scenarioIds: [], fingerprint, errors: loadErrors };
|
|
557
|
+
}
|
|
363
558
|
_packs.set(packId, {
|
|
364
559
|
manifest,
|
|
365
560
|
registeredIds,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export type RegionId = string;
|
|
2
|
+
export type TenantId = string;
|
|
3
|
+
export interface RetentionPolicy {
|
|
4
|
+
/**
|
|
5
|
+
* Default time-to-live in milliseconds for records.
|
|
6
|
+
*/
|
|
7
|
+
defaultTtlMs: number;
|
|
8
|
+
}
|
|
9
|
+
export interface DataEnvelope {
|
|
10
|
+
id: string;
|
|
11
|
+
tenantId: TenantId;
|
|
12
|
+
regionId: RegionId;
|
|
13
|
+
createdAtMs: number;
|
|
14
|
+
expiresAtMs: number;
|
|
15
|
+
legalHold: boolean;
|
|
16
|
+
piiDetected: boolean;
|
|
17
|
+
piiFindings: PIIFinding[];
|
|
18
|
+
payload: unknown;
|
|
19
|
+
}
|
|
20
|
+
export interface PIIFinding {
|
|
21
|
+
path: string;
|
|
22
|
+
kind: "email" | "phone" | "ssn" | "creditCard" | "sensitiveKey";
|
|
23
|
+
}
|
|
24
|
+
export interface AccessEvent {
|
|
25
|
+
timestampMs: number;
|
|
26
|
+
actor: string;
|
|
27
|
+
action: "write" | "read" | "list" | "legal_hold_set" | "legal_hold_released" | "purge";
|
|
28
|
+
tenantId: TenantId;
|
|
29
|
+
regionId: RegionId;
|
|
30
|
+
recordId?: string;
|
|
31
|
+
details?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface IngestOptions {
|
|
34
|
+
nowMs?: number;
|
|
35
|
+
ttlMs?: number;
|
|
36
|
+
redactSensitiveFields?: boolean;
|
|
37
|
+
actor?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface ReadOptions {
|
|
40
|
+
nowMs?: number;
|
|
41
|
+
actor?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface ListOptions {
|
|
44
|
+
nowMs?: number;
|
|
45
|
+
actor?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface LegalHoldOptions {
|
|
48
|
+
actor?: string;
|
|
49
|
+
nowMs?: number;
|
|
50
|
+
}
|
|
51
|
+
export interface TenancyModel {
|
|
52
|
+
isolation: "tenant-region-partition";
|
|
53
|
+
partitionKey: "regionId/tenantId";
|
|
54
|
+
replication: "none_cross_region";
|
|
55
|
+
accessAudit: "immutable_append_only";
|
|
56
|
+
}
|
|
57
|
+
export declare function buildTenancyModel(): TenancyModel;
|
|
58
|
+
export declare class DataGovernanceStore {
|
|
59
|
+
private readonly retention;
|
|
60
|
+
private readonly records;
|
|
61
|
+
private readonly byPartition;
|
|
62
|
+
private readonly accessLog;
|
|
63
|
+
constructor(retention: RetentionPolicy);
|
|
64
|
+
ingest(tenantId: TenantId, regionId: RegionId, recordId: string, payload: unknown, options?: IngestOptions): DataEnvelope;
|
|
65
|
+
get(tenantId: TenantId, regionId: RegionId, recordId: string, options?: ReadOptions): DataEnvelope | undefined;
|
|
66
|
+
listByPartition(tenantId: TenantId, regionId: RegionId, options?: ListOptions): DataEnvelope[];
|
|
67
|
+
setLegalHold(tenantId: TenantId, regionId: RegionId, recordId: string, options?: LegalHoldOptions): boolean;
|
|
68
|
+
releaseLegalHold(tenantId: TenantId, regionId: RegionId, recordId: string, options?: LegalHoldOptions): boolean;
|
|
69
|
+
purgeExpired(nowMs: number, actor?: string): number;
|
|
70
|
+
getAccessLog(): readonly AccessEvent[];
|
|
71
|
+
private updateLegalHold;
|
|
72
|
+
private log;
|
|
73
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
export function buildTenancyModel() {
|
|
2
|
+
return {
|
|
3
|
+
isolation: "tenant-region-partition",
|
|
4
|
+
partitionKey: "regionId/tenantId",
|
|
5
|
+
replication: "none_cross_region",
|
|
6
|
+
accessAudit: "immutable_append_only",
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
const SENSITIVE_KEY_PATTERN = /(email|phone|ssn|social|taxid|credit|card|password|secret|token|passport)/i;
|
|
10
|
+
const EMAIL_PATTERN = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i;
|
|
11
|
+
const PHONE_PATTERN = /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/;
|
|
12
|
+
const SSN_PATTERN = /\b\d{3}-\d{2}-\d{4}\b/;
|
|
13
|
+
const CREDIT_CARD_PATTERN = /\b(?:\d[ -]*?){13,19}\b/;
|
|
14
|
+
function detectFindingsFromString(path, value) {
|
|
15
|
+
const findings = [];
|
|
16
|
+
if (EMAIL_PATTERN.test(value))
|
|
17
|
+
findings.push({ path, kind: "email" });
|
|
18
|
+
if (PHONE_PATTERN.test(value))
|
|
19
|
+
findings.push({ path, kind: "phone" });
|
|
20
|
+
if (SSN_PATTERN.test(value))
|
|
21
|
+
findings.push({ path, kind: "ssn" });
|
|
22
|
+
if (CREDIT_CARD_PATTERN.test(value))
|
|
23
|
+
findings.push({ path, kind: "creditCard" });
|
|
24
|
+
return findings;
|
|
25
|
+
}
|
|
26
|
+
function scanValue(path, value, redactSensitiveFields) {
|
|
27
|
+
if (typeof value === "string") {
|
|
28
|
+
const findings = detectFindingsFromString(path, value);
|
|
29
|
+
return {
|
|
30
|
+
findings,
|
|
31
|
+
redactedValue: redactSensitiveFields && findings.length > 0 ? "[REDACTED]" : value,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
const findings = [];
|
|
36
|
+
const redacted = value.map((entry, idx) => {
|
|
37
|
+
const child = scanValue(`${path}[${idx}]`, entry, redactSensitiveFields);
|
|
38
|
+
findings.push(...child.findings);
|
|
39
|
+
return child.redactedValue;
|
|
40
|
+
});
|
|
41
|
+
return { findings, redactedValue: redacted };
|
|
42
|
+
}
|
|
43
|
+
if (value !== null && typeof value === "object") {
|
|
44
|
+
const findings = [];
|
|
45
|
+
const out = {};
|
|
46
|
+
for (const [key, childValue] of Object.entries(value)) {
|
|
47
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
48
|
+
const keyIsSensitive = SENSITIVE_KEY_PATTERN.test(key);
|
|
49
|
+
if (keyIsSensitive) {
|
|
50
|
+
findings.push({ path: childPath, kind: "sensitiveKey" });
|
|
51
|
+
}
|
|
52
|
+
if (keyIsSensitive && redactSensitiveFields) {
|
|
53
|
+
out[key] = "[REDACTED]";
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const child = scanValue(childPath, childValue, redactSensitiveFields);
|
|
57
|
+
findings.push(...child.findings);
|
|
58
|
+
out[key] = child.redactedValue;
|
|
59
|
+
}
|
|
60
|
+
return { findings, redactedValue: out };
|
|
61
|
+
}
|
|
62
|
+
return { findings: [], redactedValue: value };
|
|
63
|
+
}
|
|
64
|
+
function makeRecordKey(tenantId, regionId, id) {
|
|
65
|
+
return `${regionId}::${tenantId}::${id}`;
|
|
66
|
+
}
|
|
67
|
+
export class DataGovernanceStore {
|
|
68
|
+
retention;
|
|
69
|
+
records = new Map();
|
|
70
|
+
byPartition = new Map();
|
|
71
|
+
accessLog = [];
|
|
72
|
+
constructor(retention) {
|
|
73
|
+
if (retention.defaultTtlMs <= 0) {
|
|
74
|
+
throw new Error("defaultTtlMs must be > 0");
|
|
75
|
+
}
|
|
76
|
+
this.retention = retention;
|
|
77
|
+
}
|
|
78
|
+
ingest(tenantId, regionId, recordId, payload, options = {}) {
|
|
79
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
80
|
+
const ttlMs = options.ttlMs ?? this.retention.defaultTtlMs;
|
|
81
|
+
const redactSensitiveFields = options.redactSensitiveFields ?? true;
|
|
82
|
+
const actor = options.actor ?? "system";
|
|
83
|
+
const scanned = scanValue("", payload, redactSensitiveFields);
|
|
84
|
+
const envelope = {
|
|
85
|
+
id: recordId,
|
|
86
|
+
tenantId,
|
|
87
|
+
regionId,
|
|
88
|
+
createdAtMs: nowMs,
|
|
89
|
+
expiresAtMs: nowMs + ttlMs,
|
|
90
|
+
legalHold: false,
|
|
91
|
+
piiDetected: scanned.findings.length > 0,
|
|
92
|
+
piiFindings: scanned.findings,
|
|
93
|
+
payload: scanned.redactedValue,
|
|
94
|
+
};
|
|
95
|
+
const key = makeRecordKey(tenantId, regionId, recordId);
|
|
96
|
+
this.records.set(key, envelope);
|
|
97
|
+
const partition = `${regionId}/${tenantId}`;
|
|
98
|
+
let set = this.byPartition.get(partition);
|
|
99
|
+
if (!set) {
|
|
100
|
+
set = new Set();
|
|
101
|
+
this.byPartition.set(partition, set);
|
|
102
|
+
}
|
|
103
|
+
set.add(key);
|
|
104
|
+
this.log({ timestampMs: nowMs, actor, action: "write", tenantId, regionId, recordId });
|
|
105
|
+
return envelope;
|
|
106
|
+
}
|
|
107
|
+
get(tenantId, regionId, recordId, options = {}) {
|
|
108
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
109
|
+
const actor = options.actor ?? "system";
|
|
110
|
+
const key = makeRecordKey(tenantId, regionId, recordId);
|
|
111
|
+
const record = this.records.get(key);
|
|
112
|
+
this.log({ timestampMs: nowMs, actor, action: "read", tenantId, regionId, recordId });
|
|
113
|
+
if (!record)
|
|
114
|
+
return undefined;
|
|
115
|
+
if (!record.legalHold && record.expiresAtMs <= nowMs)
|
|
116
|
+
return undefined;
|
|
117
|
+
return record;
|
|
118
|
+
}
|
|
119
|
+
listByPartition(tenantId, regionId, options = {}) {
|
|
120
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
121
|
+
const actor = options.actor ?? "system";
|
|
122
|
+
this.log({ timestampMs: nowMs, actor, action: "list", tenantId, regionId });
|
|
123
|
+
const partition = `${regionId}/${tenantId}`;
|
|
124
|
+
const keys = this.byPartition.get(partition);
|
|
125
|
+
if (!keys)
|
|
126
|
+
return [];
|
|
127
|
+
const out = [];
|
|
128
|
+
for (const key of keys) {
|
|
129
|
+
const rec = this.records.get(key);
|
|
130
|
+
if (!rec)
|
|
131
|
+
continue;
|
|
132
|
+
if (!rec.legalHold && rec.expiresAtMs <= nowMs)
|
|
133
|
+
continue;
|
|
134
|
+
out.push(rec);
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
setLegalHold(tenantId, regionId, recordId, options = {}) {
|
|
139
|
+
return this.updateLegalHold(tenantId, regionId, recordId, true, options);
|
|
140
|
+
}
|
|
141
|
+
releaseLegalHold(tenantId, regionId, recordId, options = {}) {
|
|
142
|
+
return this.updateLegalHold(tenantId, regionId, recordId, false, options);
|
|
143
|
+
}
|
|
144
|
+
purgeExpired(nowMs, actor = "system") {
|
|
145
|
+
let purged = 0;
|
|
146
|
+
for (const [key, record] of this.records.entries()) {
|
|
147
|
+
if (record.legalHold || record.expiresAtMs > nowMs)
|
|
148
|
+
continue;
|
|
149
|
+
this.records.delete(key);
|
|
150
|
+
const partition = `${record.regionId}/${record.tenantId}`;
|
|
151
|
+
this.byPartition.get(partition)?.delete(key);
|
|
152
|
+
this.log({
|
|
153
|
+
timestampMs: nowMs,
|
|
154
|
+
actor,
|
|
155
|
+
action: "purge",
|
|
156
|
+
tenantId: record.tenantId,
|
|
157
|
+
regionId: record.regionId,
|
|
158
|
+
recordId: record.id,
|
|
159
|
+
});
|
|
160
|
+
purged += 1;
|
|
161
|
+
}
|
|
162
|
+
return purged;
|
|
163
|
+
}
|
|
164
|
+
getAccessLog() {
|
|
165
|
+
return this.accessLog;
|
|
166
|
+
}
|
|
167
|
+
updateLegalHold(tenantId, regionId, recordId, state, options) {
|
|
168
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
169
|
+
const actor = options.actor ?? "system";
|
|
170
|
+
const key = makeRecordKey(tenantId, regionId, recordId);
|
|
171
|
+
const rec = this.records.get(key);
|
|
172
|
+
if (!rec)
|
|
173
|
+
return false;
|
|
174
|
+
rec.legalHold = state;
|
|
175
|
+
this.log({
|
|
176
|
+
timestampMs: nowMs,
|
|
177
|
+
actor,
|
|
178
|
+
action: state ? "legal_hold_set" : "legal_hold_released",
|
|
179
|
+
tenantId,
|
|
180
|
+
regionId,
|
|
181
|
+
recordId,
|
|
182
|
+
});
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
log(event) {
|
|
186
|
+
this.accessLog.push(event);
|
|
187
|
+
}
|
|
188
|
+
}
|