@rehearsal-db/core 0.1.0-beta.15 → 0.1.0-beta.17

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 (113) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/dist/package.json +10 -9
  3. package/dist/src/application/interactive_task.d.mts +30 -0
  4. package/dist/src/application/interactive_task.mjs +83 -0
  5. package/dist/src/application/session.d.mts +1 -0
  6. package/dist/src/baseline/privacy_contract.d.mts +147 -0
  7. package/dist/src/baseline/privacy_contract.mjs +2 -0
  8. package/dist/src/baseline/privacy_dependency_graph.d.mts +7 -0
  9. package/dist/src/baseline/privacy_dependency_graph.mjs +110 -0
  10. package/dist/src/baseline/privacy_engine.d.mts +5 -162
  11. package/dist/src/baseline/privacy_engine.mjs +4 -1162
  12. package/dist/src/baseline/privacy_key.d.mts +7 -0
  13. package/dist/src/baseline/privacy_key.mjs +42 -0
  14. package/dist/src/baseline/privacy_pipeline.d.mts +7 -0
  15. package/dist/src/baseline/privacy_pipeline.mjs +127 -0
  16. package/dist/src/baseline/privacy_policy.d.mts +6 -0
  17. package/dist/src/baseline/privacy_policy.mjs +237 -0
  18. package/dist/src/baseline/privacy_recipe_validation.d.mts +5 -0
  19. package/dist/src/baseline/privacy_recipe_validation.mjs +289 -0
  20. package/dist/src/baseline/privacy_rule_registry.d.mts +18 -0
  21. package/dist/src/baseline/privacy_rule_registry.mjs +143 -0
  22. package/dist/src/baseline/privacy_transforms.d.mts +18 -0
  23. package/dist/src/baseline/privacy_transforms.mjs +142 -0
  24. package/dist/src/baseline/privacy_validation_shared.d.mts +31 -0
  25. package/dist/src/baseline/privacy_validation_shared.mjs +127 -0
  26. package/dist/src/cli/application_command_handlers.d.mts +8 -0
  27. package/dist/src/cli/application_command_handlers.mjs +52 -0
  28. package/dist/src/cli/arguments.d.mts +0 -5
  29. package/dist/src/cli/arguments.mjs +2 -7
  30. package/dist/src/cli/basic_command_handlers.d.mts +4 -0
  31. package/dist/src/cli/basic_command_handlers.mjs +57 -0
  32. package/dist/src/cli/cli_commands.d.mts +14 -0
  33. package/dist/src/cli/cli_commands.mjs +34 -0
  34. package/dist/src/cli/command_contract.d.mts +57 -0
  35. package/dist/src/cli/command_contract.mjs +39 -0
  36. package/dist/src/cli/command_output.d.mts +12 -0
  37. package/dist/src/cli/command_output.mjs +20 -0
  38. package/dist/src/cli/command_registry.d.mts +7 -0
  39. package/dist/src/cli/command_registry.mjs +14 -0
  40. package/dist/src/cli/guided.d.mts +3 -1
  41. package/dist/src/cli/guided.mjs +21 -7
  42. package/dist/src/cli/identity_command_handlers.d.mts +12 -0
  43. package/dist/src/cli/identity_command_handlers.mjs +239 -0
  44. package/dist/src/cli/identity_renderer.d.mts +8 -0
  45. package/dist/src/cli/identity_renderer.mjs +33 -0
  46. package/dist/src/cli/init_command.d.mts +1 -1
  47. package/dist/src/cli/inspection_command_handlers.d.mts +3 -0
  48. package/dist/src/cli/inspection_command_handlers.mjs +79 -0
  49. package/dist/src/cli/project_command_handlers.d.mts +11 -0
  50. package/dist/src/cli/project_command_handlers.mjs +92 -0
  51. package/dist/src/cli/project_commands.d.mts +131 -5
  52. package/dist/src/cli/project_commands.mjs +17 -8
  53. package/dist/src/cli/rehearsal.d.mts +2 -2
  54. package/dist/src/cli/rehearsal.mjs +51 -567
  55. package/dist/src/cli/runtime_action_handlers.d.mts +12 -0
  56. package/dist/src/cli/runtime_action_handlers.mjs +201 -0
  57. package/dist/src/cli/runtime_commands.d.mts +32 -3
  58. package/dist/src/cli/runtime_commands.mjs +33 -13
  59. package/dist/src/cli/source_command_handlers.d.mts +8 -0
  60. package/dist/src/cli/source_command_handlers.mjs +62 -0
  61. package/dist/src/cli/source_commands.d.mts +5 -1
  62. package/dist/src/cli/source_commands.mjs +3 -1
  63. package/dist/src/identity/claim.d.mts +5 -144
  64. package/dist/src/identity/claim.mjs +4 -947
  65. package/dist/src/identity/claim_contract.d.mts +135 -0
  66. package/dist/src/identity/claim_contract.mjs +6 -0
  67. package/dist/src/identity/claim_executor.d.mts +10 -0
  68. package/dist/src/identity/claim_executor.mjs +358 -0
  69. package/dist/src/identity/claim_identity_lookup.d.mts +23 -0
  70. package/dist/src/identity/claim_identity_lookup.mjs +134 -0
  71. package/dist/src/identity/claim_plan.d.mts +8 -0
  72. package/dist/src/identity/claim_plan.mjs +61 -0
  73. package/dist/src/identity/claim_policy.d.mts +3 -0
  74. package/dist/src/identity/claim_policy.mjs +377 -0
  75. package/dist/src/identity/claim_sql.d.mts +10 -0
  76. package/dist/src/identity/claim_sql.mjs +46 -0
  77. package/dist/src/identity/claim_verification.d.mts +13 -0
  78. package/dist/src/identity/claim_verification.mjs +35 -0
  79. package/dist/src/identity/connect.d.mts +30 -0
  80. package/dist/src/identity/connect.mjs +111 -0
  81. package/dist/src/index.d.mts +1 -1
  82. package/dist/src/project/configuration.d.mts +11 -179
  83. package/dist/src/project/configuration.mjs +7 -956
  84. package/dist/src/project/configuration_contract.d.mts +125 -0
  85. package/dist/src/project/configuration_contract.mjs +27 -0
  86. package/dist/src/project/configuration_discovery.d.mts +7 -0
  87. package/dist/src/project/configuration_discovery.mjs +37 -0
  88. package/dist/src/project/configuration_normalization.d.mts +107 -0
  89. package/dist/src/project/configuration_normalization.mjs +302 -0
  90. package/dist/src/project/configuration_rendering.d.mts +14 -0
  91. package/dist/src/project/configuration_rendering.mjs +226 -0
  92. package/dist/src/project/configuration_resolution.d.mts +125 -0
  93. package/dist/src/project/configuration_resolution.mjs +81 -0
  94. package/dist/src/project/configuration_schema.d.mts +22 -0
  95. package/dist/src/project/configuration_schema.mjs +160 -0
  96. package/dist/src/project/configuration_validation.d.mts +22 -0
  97. package/dist/src/project/configuration_validation.mjs +136 -0
  98. package/dist/src/project/project_detection.d.mts +14 -0
  99. package/dist/src/project/project_detection.mjs +47 -0
  100. package/dist/src/runtime/lifecycle_engine.d.mts +63 -0
  101. package/dist/src/runtime/lifecycle_engine.mjs +114 -0
  102. package/dist/src/runtime/plan.mjs +2 -1
  103. package/dist/src/targets/postgresql.mjs +142 -163
  104. package/dist/src/targets/supabase.mjs +142 -174
  105. package/docs/architecture.md +120 -3
  106. package/docs/commands.md +18 -15
  107. package/docs/configuration.md +1 -0
  108. package/docs/releasing.md +8 -3
  109. package/docs/runtime-policies.md +12 -3
  110. package/docs/troubleshooting.md +17 -0
  111. package/docs/typescript.md +57 -10
  112. package/package.json +10 -9
  113. package/types/configuration.d.mts +5 -164
package/CHANGELOG.md CHANGED
@@ -5,6 +5,49 @@ a Changelog, and versions will follow Semantic Versioning after the package exis
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## [0.1.0-beta.17] - 2026-10-08
9
+
10
+ ### Added
11
+
12
+ - `rehearsal identity connect` and the guided home screen now keep the sandbox app
13
+ available during ordinary provider sign-in, detect exactly one approved verified local
14
+ identity, show a redacted copied-account preview, and apply the existing exact claim
15
+ after one deliberate confirmation. The app is reopened automatically with an honest
16
+ fresh-session instruction; explicit `identity plan` and `identity claim` commands remain
17
+ available for scripts.
18
+
19
+ ## [0.1.0-beta.16] - 2026-10-08
20
+
21
+ ### Changed
22
+
23
+ - The CLI now routes guided and scripted requests through focused typed command
24
+ handlers and one result/output contract. The executable retains only process
25
+ lifecycle, project locking, guided-loop coordination, and top-level error handling;
26
+ command-session caches no longer live in public parsed flags.
27
+ - Configuration now passes through separate discovery, raw-schema validation,
28
+ normalization, project-root path resolution, project detection, and starter-rendering
29
+ modules behind the existing import path. Public declarations come from the same
30
+ TypeScript source, including the new `NormalizedRehearsalConfig` type.
31
+ - Installed-consumer verification now shares one scenario catalog, artifact installer,
32
+ isolated workspace boundary, process boundary, and persistent application-session
33
+ helper. The complete release gate runs every scenario against one immutable tarball
34
+ and records its exact version, SHA-256, tool environment, commands, timings, and
35
+ results.
36
+ - Identity policy validation, deterministic claim planning, SQL generation, local Auth
37
+ lookup, pure verification, and transactional execution now live in separate typed
38
+ stages behind the unchanged public claim API. Generated reference SQL is independently
39
+ testable, while only the executor can mutate the local database or Storage runtime.
40
+ - PostgreSQL and Supabase now share one typed runtime lifecycle engine for command
41
+ dispatch, active-baseline loading, migration confirmation and receipts, runtime
42
+ markers, and rollback. Candidate receipts are bound to their exact database target,
43
+ while each driver retains its own ownership proof and database-specific behavior.
44
+ - Privacy policy types, recipe normalization, dependency validation, deterministic
45
+ transforms, structured JSON, rule dispatch, row execution, and key handling now live
46
+ in focused typed stages behind the unchanged public privacy import. An explicit closed
47
+ registry covers every supported action and derivation without widening policy v2.
48
+
49
+ ## [0.1.0-beta.15] - 2026-10-08
50
+
8
51
  ### Added
9
52
 
10
53
  - A strict, no-emit TypeScript gate now checks the first shared runtime utilities and
@@ -482,7 +525,10 @@ a Changelog, and versions will follow Semantic Versioning after the package exis
482
525
  publication uses short-lived trusted OIDC, and every release tag must already exist on
483
526
  protected `main`.
484
527
 
485
- [Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.12...HEAD
528
+ [Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.15...HEAD
529
+ [0.1.0-beta.15]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.14...v0.1.0-beta.15
530
+ [0.1.0-beta.14]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.13...v0.1.0-beta.14
531
+ [0.1.0-beta.13]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.12...v0.1.0-beta.13
486
532
  [0.1.0-beta.12]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.11...v0.1.0-beta.12
487
533
  [0.1.0-beta.11]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.10...v0.1.0-beta.11
488
534
  [0.1.0-beta.10]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.9...v0.1.0-beta.10
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rehearsal-db/core",
3
- "version": "0.1.0-beta.15",
3
+ "version": "0.1.0-beta.17",
4
4
  "private": false,
5
5
  "description": "Safely rehearse PostgreSQL and Supabase migrations against sanitized, production-shaped data.",
6
6
  "repository": {
@@ -60,16 +60,17 @@
60
60
  "test:unit": "npm run build && vitest run tests/unit",
61
61
  "test:integration": "npm run build && vitest run tests/integration tests/contracts",
62
62
  "test:docs": "npm run build && vitest run tests/contracts/documentationCommands.test.mjs",
63
- "test:fixture": "npm run build && node scripts/verification/fixtures/supabase.mjs",
64
- "test:fixture:postgresql": "npm run build && node scripts/verification/fixtures/postgresql.mjs",
65
- "test:fixture:dependent": "npm run build && node scripts/verification/fixtures/dependent.mjs",
66
- "test:fixture:onboarding": "npm run build && node scripts/verification/fixtures/onboarding.mjs",
67
- "test:fixture:typescript": "npm run build && node scripts/verification/fixtures/typescript.mjs",
68
- "test:fixture:standalone": "npm run build && node scripts/verification/fixtures/standalone.mjs",
69
- "test:fixture:privacy": "npm run build && node scripts/verification/fixtures/privacy.mjs",
63
+ "test:fixture": "npm run build && node scripts/verification/run_scenario.mjs supabase",
64
+ "test:fixture:postgresql": "npm run build && node scripts/verification/run_scenario.mjs postgresql",
65
+ "test:fixture:dependent": "npm run build && node scripts/verification/run_scenario.mjs dependent-target",
66
+ "test:fixture:onboarding": "npm run build && node scripts/verification/run_scenario.mjs onboarding",
67
+ "test:fixture:typescript": "npm run build && node scripts/verification/run_scenario.mjs typescript",
68
+ "test:fixture:standalone": "npm run build && node scripts/verification/run_scenario.mjs standalone",
69
+ "test:fixture:privacy": "npm run build && node scripts/verification/run_scenario.mjs privacy",
70
+ "release:verify": "node scripts/verification/release_gate.mjs",
70
71
  "check": "npm run check:syntax && npm run typecheck && npm run test && npm run docs:build && npm run format:check && npm run package:audit",
71
72
  "check:syntax": "find scripts tests -type f -name '*.mjs' -exec node --check {} +",
72
- "typecheck": "tsc --project tsconfig.json",
73
+ "typecheck": "npm run build && tsc --project tsconfig.json",
73
74
  "format": "prettier --write .",
74
75
  "format:check": "prettier --check .",
75
76
  "package:audit": "node scripts/verification/audit_package.mjs"
@@ -0,0 +1,30 @@
1
+ /** Run identity discovery while an owned local application remains available. */
2
+ import type { EventEmitter } from "node:events";
3
+ import { type ApplicationDiagnostics, type ApplicationSession, type ApplicationSessionSignal } from "./session.mjs";
4
+ type SignalTarget = Pick<EventEmitter, "prependListener" | "removeListener">;
5
+ interface SessionInput {
6
+ readonly isTTY?: boolean;
7
+ prependListener(event: "data", listener: (chunk: Buffer | string) => void): unknown;
8
+ removeListener(event: "data", listener: (chunk: Buffer | string) => void): unknown;
9
+ }
10
+ /**
11
+ * A terminal interruption aborts the task and retires the owned process group.
12
+ * A completed task leaves the session running so its caller can deliberately
13
+ * stop, reuse, or hand it back to the persistent application holder.
14
+ */
15
+ export declare const runApplicationSessionTask: <Value>({ session, task, signalTarget, input, onReady, }: {
16
+ session: ApplicationSession;
17
+ task: (signal: AbortSignal) => Promise<Value>;
18
+ signalTarget?: SignalTarget;
19
+ input?: SessionInput;
20
+ onReady?: (ready: ApplicationSession["ready"]) => void;
21
+ }) => Promise<Readonly<{
22
+ completed: true;
23
+ value: Value;
24
+ diagnostics: ApplicationDiagnostics;
25
+ }> | Readonly<{
26
+ completed: false;
27
+ signal: ApplicationSessionSignal;
28
+ diagnostics: ApplicationDiagnostics;
29
+ }>>;
30
+ export {};
@@ -0,0 +1,83 @@
1
+ /** Run identity discovery while an owned local application remains available. */
2
+ import { summarizeProjectCommandFailure, } from "./session.mjs";
3
+ const SESSION_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP", "SIGTSTP"];
4
+ /**
5
+ * A terminal interruption aborts the task and retires the owned process group.
6
+ * A completed task leaves the session running so its caller can deliberately
7
+ * stop, reuse, or hand it back to the persistent application holder.
8
+ */
9
+ export const runApplicationSessionTask = async ({ session, task, signalTarget = process, input = process.stdin, onReady = () => undefined, }) => {
10
+ if (!session ||
11
+ typeof session.wait !== "function" ||
12
+ typeof session.stop !== "function") {
13
+ throw new Error("A started application session is required.");
14
+ }
15
+ let finishInterruption = () => undefined;
16
+ const interruption = new Promise((resolve) => {
17
+ finishInterruption = resolve;
18
+ });
19
+ const handlers = new Map(SESSION_SIGNALS.map((signal) => [
20
+ signal,
21
+ () => finishInterruption({ kind: "interrupted", signal }),
22
+ ]));
23
+ for (const [signal, handler] of handlers) {
24
+ signalTarget.prependListener(signal, handler);
25
+ }
26
+ const onInput = (chunk) => {
27
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
28
+ if (bytes.includes(0x03)) {
29
+ finishInterruption({ kind: "interrupted", signal: "SIGINT" });
30
+ }
31
+ else if (bytes.includes(0x1a)) {
32
+ finishInterruption({ kind: "interrupted", signal: "SIGTSTP" });
33
+ }
34
+ };
35
+ if (input?.isTTY)
36
+ input.prependListener("data", onInput);
37
+ const abortController = new AbortController();
38
+ try {
39
+ onReady(session.ready);
40
+ const outcome = await Promise.race([
41
+ interruption,
42
+ task(abortController.signal).then((value) => ({
43
+ kind: "completed",
44
+ value,
45
+ })),
46
+ session.wait().then((exit) => ({ kind: "exited", ...exit })),
47
+ ]);
48
+ if (outcome.kind === "exited") {
49
+ throw new Error(`Application exited before identity connection finished (${summarizeProjectCommandFailure({
50
+ status: outcome.status,
51
+ signal: outcome.signal,
52
+ stdoutByteCount: session.diagnostics().stdoutBytes,
53
+ stderrByteCount: session.diagnostics().stderrBytes,
54
+ })}).`);
55
+ }
56
+ if (outcome.kind === "interrupted") {
57
+ abortController.abort(new Error(`Identity connection was interrupted by ${outcome.signal}.`));
58
+ await session.stop();
59
+ return Object.freeze({
60
+ completed: false,
61
+ signal: outcome.signal,
62
+ diagnostics: session.diagnostics(),
63
+ });
64
+ }
65
+ return Object.freeze({
66
+ completed: true,
67
+ value: outcome.value,
68
+ diagnostics: session.diagnostics(),
69
+ });
70
+ }
71
+ catch (error) {
72
+ abortController.abort(error);
73
+ await session.stop();
74
+ throw error;
75
+ }
76
+ finally {
77
+ for (const [signal, handler] of handlers) {
78
+ signalTarget.removeListener(signal, handler);
79
+ }
80
+ if (input?.isTTY)
81
+ input.removeListener("data", onInput);
82
+ }
83
+ };
@@ -3,6 +3,7 @@ import { spawn } from "node:child_process";
3
3
  import type { EventEmitter } from "node:events";
4
4
  declare const SESSION_SIGNALS: string[];
5
5
  type SessionSignal = (typeof SESSION_SIGNALS)[number];
6
+ export type ApplicationSessionSignal = SessionSignal;
6
7
  export interface ApplicationReadiness {
7
8
  readonly url: string;
8
9
  readonly expectedStatus?: number;
@@ -0,0 +1,147 @@
1
+ /** Typed, normalized contract shared by privacy validation and execution stages. */
2
+ export type PrivacyAction = "KEEP" | "PSEUDONYMIZE" | "REPLACE" | "EXCLUDE" | "DERIVE";
3
+ export type PrivacyObject = Record<string, unknown>;
4
+ export interface PseudonymRecipe {
5
+ format: "uuid" | "email" | "text" | "integer" | "hex" | "gtin" | "url";
6
+ namespace: string;
7
+ maxLength?: number;
8
+ length?: number;
9
+ allowedLengths?: readonly number[];
10
+ origin?: string;
11
+ }
12
+ export interface ConstantRecipe {
13
+ kind: "constant";
14
+ value: unknown;
15
+ }
16
+ export interface DateShiftRecipe {
17
+ kind: "date-shift";
18
+ days: number;
19
+ representation: "iso-string" | "epoch-milliseconds";
20
+ group?: string;
21
+ }
22
+ export interface EnumRecipe {
23
+ kind: "enum";
24
+ allowNull: boolean;
25
+ values: readonly (string | number | boolean)[];
26
+ }
27
+ export interface ValidatedStringRecipe {
28
+ kind: "validated-string";
29
+ format: "portable-code";
30
+ maximumBytes: number;
31
+ allowNull: boolean;
32
+ }
33
+ export interface DigestRecipe {
34
+ kind: "digest";
35
+ format: "hex";
36
+ length: number;
37
+ namespace: string;
38
+ inputs: readonly string[];
39
+ }
40
+ export interface BindingSubstituteRecipe {
41
+ kind: "binding-substitute";
42
+ binding: string;
43
+ format: "uuid";
44
+ namespace: string;
45
+ maximumBytes: number;
46
+ }
47
+ export interface PathMapRecipe {
48
+ kind: "path-map";
49
+ mapping: string;
50
+ }
51
+ export interface ApprovedOwnerRecipe {
52
+ kind: "approved-owner";
53
+ approved: NormalizedDeclaration;
54
+ otherwise: NormalizedDeclaration;
55
+ }
56
+ export interface JsonArrayRecipe {
57
+ kind: "json-array";
58
+ allowNull: boolean;
59
+ maximumBytes: number;
60
+ maximumDepth: number;
61
+ maximumItems: number;
62
+ items: NormalizedDeclaration;
63
+ }
64
+ export interface JsonObjectRecipe {
65
+ kind: "json-object";
66
+ allowNull: boolean;
67
+ maximumBytes: number;
68
+ maximumDepth: number;
69
+ fields: Readonly<Record<string, NormalizedDeclaration>>;
70
+ }
71
+ export interface JsonUnionRecipe {
72
+ kind: "json-union";
73
+ allowNull: boolean;
74
+ maximumBytes: number;
75
+ maximumDepth: number;
76
+ maximumItems: number;
77
+ variants: Readonly<Record<string, NormalizedDeclaration>>;
78
+ }
79
+ export interface JsonDictionaryRecipe {
80
+ kind: "json-dictionary";
81
+ allowNull: boolean;
82
+ maximumBytes: number;
83
+ maximumDepth: number;
84
+ maximumItems: number;
85
+ keys: Readonly<Record<string, unknown>>;
86
+ values: NormalizedDeclaration;
87
+ }
88
+ export type DerivationRecipe = DateShiftRecipe | EnumRecipe | ValidatedStringRecipe | DigestRecipe | BindingSubstituteRecipe | PathMapRecipe | ApprovedOwnerRecipe | JsonArrayRecipe | JsonObjectRecipe | JsonUnionRecipe | JsonDictionaryRecipe;
89
+ export type NormalizedRecipe = PseudonymRecipe | ConstantRecipe | DerivationRecipe | null;
90
+ export interface NormalizedDeclaration {
91
+ action: PrivacyAction;
92
+ recipe: NormalizedRecipe;
93
+ required?: boolean;
94
+ }
95
+ export interface PrivacyBinding {
96
+ environmentVariable: string;
97
+ approvedValueSha256: string;
98
+ }
99
+ export interface PrivacyPathMapping extends PseudonymRecipe {
100
+ binding: string;
101
+ format: "uuid" | "text";
102
+ maximumBytes: number;
103
+ maximumSegments: number;
104
+ }
105
+ export interface PrivacyForeignKey {
106
+ readonly schema: string;
107
+ readonly table: string;
108
+ readonly column: string;
109
+ }
110
+ export interface PrivacyColumn extends NormalizedDeclaration {
111
+ name: string;
112
+ generated: "ALWAYS" | "NEVER";
113
+ identity: "YES" | "NO";
114
+ foreignKey: PrivacyForeignKey | null;
115
+ }
116
+ export interface PrivacyTable {
117
+ schema: string;
118
+ name: string;
119
+ sourceRows: "STREAM AND SANITIZE" | "EXCLUDE";
120
+ ownerBinding: Readonly<{
121
+ binding: string;
122
+ columns: readonly string[];
123
+ match: "any";
124
+ nullBehavior?: "otherwise";
125
+ }> | null;
126
+ columns: readonly PrivacyColumn[];
127
+ }
128
+ export interface ExecutablePrivacyPolicy {
129
+ readonly policyVersion: 2;
130
+ readonly migrationCutoff: string;
131
+ readonly bindings: Readonly<Record<string, PrivacyBinding>>;
132
+ readonly pathMappings: Readonly<Record<string, PrivacyPathMapping>>;
133
+ readonly tables: readonly PrivacyTable[];
134
+ }
135
+ export interface PrivacySourceRecord {
136
+ schema?: string;
137
+ table: string;
138
+ row: Record<string, unknown>;
139
+ }
140
+ export interface PrivacyEngine {
141
+ readonly keyFingerprint: string;
142
+ remapPath(input: {
143
+ mapping: string;
144
+ value: string;
145
+ }): string;
146
+ sanitize(record: unknown): PrivacySourceRecord | null;
147
+ }
@@ -0,0 +1,2 @@
1
+ /** Typed, normalized contract shared by privacy validation and execution stages. */
2
+ export {};
@@ -0,0 +1,7 @@
1
+ /** Validate cross-rule bindings, dependencies, and shared deterministic groups. */
2
+ import type { PrivacyBinding, PrivacyPathMapping, PrivacyTable } from "./privacy_contract.mjs";
3
+ export declare const validatePrivacyDependencyGraph: ({ tables, bindings, pathMappings, }: {
4
+ tables: readonly PrivacyTable[];
5
+ bindings: Readonly<Record<string, PrivacyBinding>>;
6
+ pathMappings: Readonly<Record<string, PrivacyPathMapping>>;
7
+ }) => void;
@@ -0,0 +1,110 @@
1
+ /** Validate cross-rule bindings, dependencies, and shared deterministic groups. */
2
+ import { structuredJsonDeclarations } from "./privacy_structured_json.mjs";
3
+ const usesApprovedOwner = (declaration) => (declaration.recipe !== null &&
4
+ "kind" in declaration.recipe &&
5
+ declaration.recipe.kind === "approved-owner") ||
6
+ structuredJsonDeclarations(declaration.recipe).some(usesApprovedOwner);
7
+ const visitRecipes = (declaration, visit) => {
8
+ visit(declaration.recipe);
9
+ if (declaration.recipe !== null &&
10
+ "kind" in declaration.recipe &&
11
+ declaration.recipe.kind === "approved-owner") {
12
+ visitRecipes(declaration.recipe.approved, visit);
13
+ visitRecipes(declaration.recipe.otherwise, visit);
14
+ }
15
+ else {
16
+ structuredJsonDeclarations(declaration.recipe).forEach((nested) => visitRecipes(nested, visit));
17
+ }
18
+ };
19
+ const digestInputs = (declaration) => {
20
+ if (declaration.recipe !== null &&
21
+ "kind" in declaration.recipe &&
22
+ declaration.recipe.kind === "digest") {
23
+ return [...declaration.recipe.inputs];
24
+ }
25
+ if (declaration.recipe !== null &&
26
+ "kind" in declaration.recipe &&
27
+ declaration.recipe.kind === "approved-owner") {
28
+ return [
29
+ ...digestInputs(declaration.recipe.approved),
30
+ ...digestInputs(declaration.recipe.otherwise),
31
+ ];
32
+ }
33
+ return structuredJsonDeclarations(declaration.recipe).flatMap(digestInputs);
34
+ };
35
+ const validateTableDependencies = ({ table, index, bindings, pathMappings, }) => {
36
+ const label = `policy.tables[${index}]`;
37
+ if (table.columns.some(usesApprovedOwner) && !table.ownerBinding) {
38
+ throw new Error(`${label} uses approved-owner without ownerBinding.`);
39
+ }
40
+ for (const column of table.columns) {
41
+ visitRecipes(column, (recipe) => {
42
+ if (recipe &&
43
+ "kind" in recipe &&
44
+ recipe.kind === "path-map" &&
45
+ !Object.hasOwn(pathMappings, recipe.mapping)) {
46
+ throw new Error(`${label} references unknown path mapping ${recipe.mapping}.`);
47
+ }
48
+ if (recipe !== null &&
49
+ "kind" in recipe &&
50
+ recipe.kind === "binding-substitute" &&
51
+ !Object.hasOwn(bindings, recipe.binding)) {
52
+ throw new Error(`${label} references unknown privacy binding ${recipe.binding}.`);
53
+ }
54
+ });
55
+ }
56
+ const dependencies = new Map(table.columns.map((column) => [column.name, []]));
57
+ for (const column of table.columns) {
58
+ const inputs = [...new Set(digestInputs(column))];
59
+ for (const input of inputs) {
60
+ const inputColumn = table.columns.find((entry) => entry.name === input);
61
+ if (!inputColumn) {
62
+ throw new Error(`${label}.${column.name} digest references unknown column ${input}.`);
63
+ }
64
+ if (inputColumn.action === "EXCLUDE") {
65
+ throw new Error(`${label}.${column.name} digest input ${input} is excluded instead of sanitized.`);
66
+ }
67
+ }
68
+ dependencies.set(column.name, inputs);
69
+ }
70
+ const resolved = new Set();
71
+ const resolving = new Set();
72
+ const visitDependency = (columnName) => {
73
+ if (resolved.has(columnName))
74
+ return;
75
+ if (resolving.has(columnName)) {
76
+ throw new Error(`${label} contains a cyclic digest dependency.`);
77
+ }
78
+ resolving.add(columnName);
79
+ for (const input of dependencies.get(columnName) ?? []) {
80
+ visitDependency(input);
81
+ }
82
+ resolving.delete(columnName);
83
+ resolved.add(columnName);
84
+ };
85
+ table.columns.forEach((column) => visitDependency(column.name));
86
+ };
87
+ export const validatePrivacyDependencyGraph = ({ tables, bindings, pathMappings, }) => {
88
+ tables.forEach((table, index) => validateTableDependencies({ table, index, bindings, pathMappings }));
89
+ const dateShiftGroups = new Map();
90
+ const validateDateShiftGroup = (declaration) => {
91
+ const recipe = declaration.recipe;
92
+ if (!recipe || !("kind" in recipe))
93
+ return;
94
+ if (recipe.kind === "date-shift" && recipe.group) {
95
+ const existing = dateShiftGroups.get(recipe.group);
96
+ if (existing !== undefined && existing !== recipe.days) {
97
+ throw new Error(`Privacy date-shift group ${recipe.group} must use one days value.`);
98
+ }
99
+ dateShiftGroups.set(recipe.group, recipe.days);
100
+ }
101
+ if (recipe.kind === "approved-owner") {
102
+ validateDateShiftGroup(recipe.approved);
103
+ validateDateShiftGroup(recipe.otherwise);
104
+ }
105
+ else {
106
+ structuredJsonDeclarations(recipe).forEach(validateDateShiftGroup);
107
+ }
108
+ };
109
+ tables.forEach((table) => table.columns.forEach(validateDateShiftGroup));
110
+ };
@@ -1,162 +1,5 @@
1
- /**
2
- * Execute reviewed privacy policy version 2 without project-owned callbacks.
3
- * The engine is intentionally small and fail-closed: unsupported shapes and
4
- * recipes must be reviewed and added here instead of evaluated as code.
5
- */
6
- type PrivacyAction = "KEEP" | "PSEUDONYMIZE" | "REPLACE" | "EXCLUDE" | "DERIVE";
7
- interface PseudonymRecipe {
8
- format: "uuid" | "email" | "text" | "integer" | "hex" | "gtin" | "url";
9
- namespace: string;
10
- maxLength?: number;
11
- length?: number;
12
- allowedLengths?: readonly number[];
13
- origin?: string;
14
- }
15
- interface ConstantRecipe {
16
- kind: "constant";
17
- value: unknown;
18
- }
19
- interface DateShiftRecipe {
20
- kind: "date-shift";
21
- days: number;
22
- representation: "iso-string" | "epoch-milliseconds";
23
- group?: string;
24
- }
25
- interface EnumRecipe {
26
- kind: "enum";
27
- allowNull: boolean;
28
- values: readonly (string | number | boolean)[];
29
- }
30
- interface ValidatedStringRecipe {
31
- kind: "validated-string";
32
- format: "portable-code";
33
- maximumBytes: number;
34
- allowNull: boolean;
35
- }
36
- interface DigestRecipe {
37
- kind: "digest";
38
- format: "hex";
39
- length: number;
40
- namespace: string;
41
- inputs: readonly string[];
42
- }
43
- interface BindingSubstituteRecipe {
44
- kind: "binding-substitute";
45
- binding: string;
46
- format: "uuid";
47
- namespace: string;
48
- maximumBytes: number;
49
- }
50
- interface PathMapRecipe {
51
- kind: "path-map";
52
- mapping: string;
53
- }
54
- interface ApprovedOwnerRecipe {
55
- kind: "approved-owner";
56
- approved: NormalizedDeclaration;
57
- otherwise: NormalizedDeclaration;
58
- }
59
- interface JsonArrayRecipe {
60
- kind: "json-array";
61
- allowNull: boolean;
62
- maximumBytes: number;
63
- maximumDepth: number;
64
- maximumItems: number;
65
- items: NormalizedDeclaration;
66
- }
67
- interface JsonObjectRecipe {
68
- kind: "json-object";
69
- allowNull: boolean;
70
- maximumBytes: number;
71
- maximumDepth: number;
72
- fields: Readonly<Record<string, NormalizedDeclaration>>;
73
- }
74
- interface JsonUnionRecipe {
75
- kind: "json-union";
76
- allowNull: boolean;
77
- maximumBytes: number;
78
- maximumDepth: number;
79
- maximumItems: number;
80
- variants: Readonly<Record<string, NormalizedDeclaration>>;
81
- }
82
- interface JsonDictionaryRecipe {
83
- kind: "json-dictionary";
84
- allowNull: boolean;
85
- maximumBytes: number;
86
- maximumDepth: number;
87
- maximumItems: number;
88
- keys: Readonly<Record<string, unknown>>;
89
- values: NormalizedDeclaration;
90
- }
91
- type DerivationRecipe = DateShiftRecipe | EnumRecipe | ValidatedStringRecipe | DigestRecipe | BindingSubstituteRecipe | PathMapRecipe | ApprovedOwnerRecipe | JsonArrayRecipe | JsonObjectRecipe | JsonUnionRecipe | JsonDictionaryRecipe;
92
- type NormalizedRecipe = PseudonymRecipe | ConstantRecipe | DerivationRecipe | null;
93
- interface NormalizedDeclaration {
94
- action: PrivacyAction;
95
- recipe: NormalizedRecipe;
96
- required?: boolean;
97
- }
98
- interface PrivacyBinding {
99
- environmentVariable: string;
100
- approvedValueSha256: string;
101
- }
102
- interface PrivacyPathMapping extends PseudonymRecipe {
103
- binding: string;
104
- format: "uuid" | "text";
105
- maximumBytes: number;
106
- maximumSegments: number;
107
- }
108
- export interface PrivacyForeignKey {
109
- readonly schema: string;
110
- readonly table: string;
111
- readonly column: string;
112
- }
113
- export interface PrivacyColumn extends NormalizedDeclaration {
114
- name: string;
115
- generated: "ALWAYS" | "NEVER";
116
- identity: "YES" | "NO";
117
- foreignKey: PrivacyForeignKey | null;
118
- }
119
- export interface PrivacyTable {
120
- schema: string;
121
- name: string;
122
- sourceRows: "STREAM AND SANITIZE" | "EXCLUDE";
123
- ownerBinding: Readonly<{
124
- binding: string;
125
- columns: readonly string[];
126
- match: "any";
127
- nullBehavior?: "otherwise";
128
- }> | null;
129
- columns: readonly PrivacyColumn[];
130
- }
131
- export interface ExecutablePrivacyPolicy {
132
- readonly policyVersion: 2;
133
- readonly migrationCutoff: string;
134
- readonly bindings: Readonly<Record<string, PrivacyBinding>>;
135
- readonly pathMappings: Readonly<Record<string, PrivacyPathMapping>>;
136
- readonly tables: readonly PrivacyTable[];
137
- }
138
- export declare const validateExecutablePrivacyPolicy: (policy: unknown) => ExecutablePrivacyPolicy;
139
- export interface PrivacySourceRecord {
140
- schema?: string;
141
- table: string;
142
- row: Record<string, unknown>;
143
- }
144
- export interface PrivacyEngine {
145
- readonly keyFingerprint: string;
146
- remapPath(input: {
147
- mapping: string;
148
- value: string;
149
- }): string;
150
- sanitize(record: unknown): PrivacySourceRecord | null;
151
- }
152
- export declare const createPrivacyEngine: ({ policy, key, environment, }: {
153
- policy: unknown;
154
- key: Uint8Array | string;
155
- environment?: NodeJS.ProcessEnv;
156
- }) => Readonly<PrivacyEngine>;
157
- export declare const createPrivacyKey: (path: string) => Promise<{
158
- fingerprint: string;
159
- }>;
160
- export declare const readPrivacyKey: (path: string) => Promise<Buffer>;
161
- export declare const assertPrivacyKeyFingerprint: (key: Uint8Array, expected: unknown) => void;
162
- export {};
1
+ /** Stable public facade for the versioned, fail-closed privacy pipeline. */
2
+ export { validateExecutablePrivacyPolicy } from "./privacy_policy.mjs";
3
+ export { createPrivacyEngine } from "./privacy_pipeline.mjs";
4
+ export { assertPrivacyKeyFingerprint, createPrivacyKey, readPrivacyKey, } from "./privacy_key.mjs";
5
+ export type { ExecutablePrivacyPolicy, PrivacyColumn, PrivacyEngine, PrivacyForeignKey, PrivacySourceRecord, PrivacyTable, } from "./privacy_policy.mjs";