@rehearsal-db/core 0.1.0-beta.15 → 0.1.0-beta.16
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 +34 -1
- package/dist/package.json +10 -9
- package/dist/src/baseline/privacy_contract.d.mts +147 -0
- package/dist/src/baseline/privacy_contract.mjs +2 -0
- package/dist/src/baseline/privacy_dependency_graph.d.mts +7 -0
- package/dist/src/baseline/privacy_dependency_graph.mjs +110 -0
- package/dist/src/baseline/privacy_engine.d.mts +5 -162
- package/dist/src/baseline/privacy_engine.mjs +4 -1162
- package/dist/src/baseline/privacy_key.d.mts +7 -0
- package/dist/src/baseline/privacy_key.mjs +42 -0
- package/dist/src/baseline/privacy_pipeline.d.mts +7 -0
- package/dist/src/baseline/privacy_pipeline.mjs +127 -0
- package/dist/src/baseline/privacy_policy.d.mts +6 -0
- package/dist/src/baseline/privacy_policy.mjs +237 -0
- package/dist/src/baseline/privacy_recipe_validation.d.mts +5 -0
- package/dist/src/baseline/privacy_recipe_validation.mjs +289 -0
- package/dist/src/baseline/privacy_rule_registry.d.mts +18 -0
- package/dist/src/baseline/privacy_rule_registry.mjs +143 -0
- package/dist/src/baseline/privacy_transforms.d.mts +18 -0
- package/dist/src/baseline/privacy_transforms.mjs +142 -0
- package/dist/src/baseline/privacy_validation_shared.d.mts +31 -0
- package/dist/src/baseline/privacy_validation_shared.mjs +127 -0
- package/dist/src/cli/application_command_handlers.d.mts +8 -0
- package/dist/src/cli/application_command_handlers.mjs +52 -0
- package/dist/src/cli/arguments.d.mts +0 -5
- package/dist/src/cli/arguments.mjs +0 -7
- package/dist/src/cli/basic_command_handlers.d.mts +4 -0
- package/dist/src/cli/basic_command_handlers.mjs +56 -0
- package/dist/src/cli/cli_commands.d.mts +14 -0
- package/dist/src/cli/cli_commands.mjs +32 -0
- package/dist/src/cli/command_contract.d.mts +57 -0
- package/dist/src/cli/command_contract.mjs +39 -0
- package/dist/src/cli/command_output.d.mts +12 -0
- package/dist/src/cli/command_output.mjs +20 -0
- package/dist/src/cli/command_registry.d.mts +7 -0
- package/dist/src/cli/command_registry.mjs +14 -0
- package/dist/src/cli/guided.d.mts +3 -1
- package/dist/src/cli/guided.mjs +13 -7
- package/dist/src/cli/identity_command_handlers.d.mts +8 -0
- package/dist/src/cli/identity_command_handlers.mjs +43 -0
- package/dist/src/cli/init_command.d.mts +1 -1
- package/dist/src/cli/inspection_command_handlers.d.mts +3 -0
- package/dist/src/cli/inspection_command_handlers.mjs +79 -0
- package/dist/src/cli/project_command_handlers.d.mts +11 -0
- package/dist/src/cli/project_command_handlers.mjs +92 -0
- package/dist/src/cli/project_commands.d.mts +6 -4
- package/dist/src/cli/project_commands.mjs +6 -6
- package/dist/src/cli/rehearsal.d.mts +2 -2
- package/dist/src/cli/rehearsal.mjs +51 -567
- package/dist/src/cli/runtime_action_handlers.d.mts +12 -0
- package/dist/src/cli/runtime_action_handlers.mjs +201 -0
- package/dist/src/cli/runtime_commands.d.mts +7 -3
- package/dist/src/cli/runtime_commands.mjs +10 -8
- package/dist/src/cli/source_command_handlers.d.mts +8 -0
- package/dist/src/cli/source_command_handlers.mjs +62 -0
- package/dist/src/cli/source_commands.d.mts +5 -1
- package/dist/src/cli/source_commands.mjs +3 -1
- package/dist/src/identity/claim.d.mts +5 -144
- package/dist/src/identity/claim.mjs +4 -947
- package/dist/src/identity/claim_contract.d.mts +135 -0
- package/dist/src/identity/claim_contract.mjs +6 -0
- package/dist/src/identity/claim_executor.d.mts +10 -0
- package/dist/src/identity/claim_executor.mjs +358 -0
- package/dist/src/identity/claim_identity_lookup.d.mts +17 -0
- package/dist/src/identity/claim_identity_lookup.mjs +125 -0
- package/dist/src/identity/claim_plan.d.mts +8 -0
- package/dist/src/identity/claim_plan.mjs +61 -0
- package/dist/src/identity/claim_policy.d.mts +3 -0
- package/dist/src/identity/claim_policy.mjs +377 -0
- package/dist/src/identity/claim_sql.d.mts +10 -0
- package/dist/src/identity/claim_sql.mjs +46 -0
- package/dist/src/identity/claim_verification.d.mts +13 -0
- package/dist/src/identity/claim_verification.mjs +35 -0
- package/dist/src/index.d.mts +1 -1
- package/dist/src/project/configuration.d.mts +11 -179
- package/dist/src/project/configuration.mjs +7 -956
- package/dist/src/project/configuration_contract.d.mts +125 -0
- package/dist/src/project/configuration_contract.mjs +27 -0
- package/dist/src/project/configuration_discovery.d.mts +7 -0
- package/dist/src/project/configuration_discovery.mjs +37 -0
- package/dist/src/project/configuration_normalization.d.mts +107 -0
- package/dist/src/project/configuration_normalization.mjs +302 -0
- package/dist/src/project/configuration_rendering.d.mts +14 -0
- package/dist/src/project/configuration_rendering.mjs +224 -0
- package/dist/src/project/configuration_resolution.d.mts +125 -0
- package/dist/src/project/configuration_resolution.mjs +81 -0
- package/dist/src/project/configuration_schema.d.mts +22 -0
- package/dist/src/project/configuration_schema.mjs +160 -0
- package/dist/src/project/configuration_validation.d.mts +22 -0
- package/dist/src/project/configuration_validation.mjs +136 -0
- package/dist/src/project/project_detection.d.mts +14 -0
- package/dist/src/project/project_detection.mjs +47 -0
- package/dist/src/runtime/lifecycle_engine.d.mts +63 -0
- package/dist/src/runtime/lifecycle_engine.mjs +114 -0
- package/dist/src/runtime/plan.mjs +2 -1
- package/dist/src/targets/postgresql.mjs +142 -163
- package/dist/src/targets/supabase.mjs +142 -174
- package/docs/architecture.md +118 -3
- package/docs/releasing.md +8 -3
- package/docs/typescript.md +57 -10
- package/package.json +10 -9
- package/types/configuration.d.mts +5 -164
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,36 @@ a Changelog, and versions will follow Semantic Versioning after the package exis
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
- The CLI now routes guided and scripted requests through focused typed command
|
|
11
|
+
handlers and one result/output contract. The executable retains only process
|
|
12
|
+
lifecycle, project locking, guided-loop coordination, and top-level error handling;
|
|
13
|
+
command-session caches no longer live in public parsed flags.
|
|
14
|
+
- Configuration now passes through separate discovery, raw-schema validation,
|
|
15
|
+
normalization, project-root path resolution, project detection, and starter-rendering
|
|
16
|
+
modules behind the existing import path. Public declarations come from the same
|
|
17
|
+
TypeScript source, including the new `NormalizedRehearsalConfig` type.
|
|
18
|
+
- Installed-consumer verification now shares one scenario catalog, artifact installer,
|
|
19
|
+
isolated workspace boundary, process boundary, and persistent application-session
|
|
20
|
+
helper. The complete release gate runs every scenario against one immutable tarball
|
|
21
|
+
and records its exact version, SHA-256, tool environment, commands, timings, and
|
|
22
|
+
results.
|
|
23
|
+
- Identity policy validation, deterministic claim planning, SQL generation, local Auth
|
|
24
|
+
lookup, pure verification, and transactional execution now live in separate typed
|
|
25
|
+
stages behind the unchanged public claim API. Generated reference SQL is independently
|
|
26
|
+
testable, while only the executor can mutate the local database or Storage runtime.
|
|
27
|
+
- PostgreSQL and Supabase now share one typed runtime lifecycle engine for command
|
|
28
|
+
dispatch, active-baseline loading, migration confirmation and receipts, runtime
|
|
29
|
+
markers, and rollback. Candidate receipts are bound to their exact database target,
|
|
30
|
+
while each driver retains its own ownership proof and database-specific behavior.
|
|
31
|
+
- Privacy policy types, recipe normalization, dependency validation, deterministic
|
|
32
|
+
transforms, structured JSON, rule dispatch, row execution, and key handling now live
|
|
33
|
+
in focused typed stages behind the unchanged public privacy import. An explicit closed
|
|
34
|
+
registry covers every supported action and derivation without widening policy v2.
|
|
35
|
+
|
|
36
|
+
## [0.1.0-beta.15] - 2026-10-08
|
|
37
|
+
|
|
8
38
|
### Added
|
|
9
39
|
|
|
10
40
|
- A strict, no-emit TypeScript gate now checks the first shared runtime utilities and
|
|
@@ -482,7 +512,10 @@ a Changelog, and versions will follow Semantic Versioning after the package exis
|
|
|
482
512
|
publication uses short-lived trusted OIDC, and every release tag must already exist on
|
|
483
513
|
protected `main`.
|
|
484
514
|
|
|
485
|
-
[Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.
|
|
515
|
+
[Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.15...HEAD
|
|
516
|
+
[0.1.0-beta.15]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.14...v0.1.0-beta.15
|
|
517
|
+
[0.1.0-beta.14]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.13...v0.1.0-beta.14
|
|
518
|
+
[0.1.0-beta.13]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.12...v0.1.0-beta.13
|
|
486
519
|
[0.1.0-beta.12]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.11...v0.1.0-beta.12
|
|
487
520
|
[0.1.0-beta.11]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.10...v0.1.0-beta.11
|
|
488
521
|
[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.
|
|
3
|
+
"version": "0.1.0-beta.16",
|
|
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/
|
|
64
|
-
"test:fixture:postgresql": "npm run build && node scripts/verification/
|
|
65
|
-
"test:fixture:dependent": "npm run build && node scripts/verification/
|
|
66
|
-
"test:fixture:onboarding": "npm run build && node scripts/verification/
|
|
67
|
-
"test:fixture:typescript": "npm run build && node scripts/verification/
|
|
68
|
-
"test:fixture:standalone": "npm run build && node scripts/verification/
|
|
69
|
-
"test:fixture:privacy": "npm run build && node scripts/verification/
|
|
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,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,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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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";
|