@rehearsal-db/core 0.1.0-beta.14 → 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 +51 -1
- package/README.md +1 -0
- package/dist/package.json +101 -0
- package/dist/scripts/install/create_root_config.d.mts +3 -0
- package/dist/scripts/install/create_root_config.mjs +13 -0
- package/dist/scripts/runtime/manage_database.d.mts +6 -0
- package/{scripts → dist/scripts}/runtime/manage_database.mjs +6 -8
- package/dist/src/application/session.d.mts +96 -0
- package/dist/src/application/session.mjs +380 -0
- package/dist/src/baseline/artifact.d.mts +119 -0
- package/dist/src/baseline/artifact.mjs +522 -0
- package/dist/src/baseline/builder.d.mts +19 -0
- package/dist/src/baseline/builder.mjs +142 -0
- package/dist/src/baseline/input_discovery.d.mts +13 -0
- package/dist/src/baseline/input_discovery.mjs +149 -0
- package/dist/src/baseline/policy_review.d.mts +56 -0
- package/dist/src/baseline/policy_review.mjs +73 -0
- package/dist/src/baseline/preparation.d.mts +63 -0
- package/dist/src/baseline/preparation.mjs +295 -0
- 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 -0
- package/dist/src/baseline/privacy_engine.mjs +4 -0
- 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_structured_json.d.mts +66 -0
- package/dist/src/baseline/privacy_structured_json.mjs +464 -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/baseline/sanitization_policy.d.mts +69 -0
- package/dist/src/baseline/sanitization_policy.mjs +227 -0
- package/dist/src/baseline/schema_snapshot.d.mts +19 -0
- package/dist/src/baseline/schema_snapshot.mjs +79 -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 +38 -0
- package/dist/src/cli/arguments.mjs +128 -0
- 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 +53 -0
- package/dist/src/cli/guided.mjs +719 -0
- 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 +21 -0
- package/dist/src/cli/init_command.mjs +90 -0
- 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 +159 -0
- package/dist/src/cli/project_commands.mjs +183 -0
- package/dist/src/cli/rehearsal.d.mts +7 -0
- package/dist/src/cli/rehearsal.mjs +190 -0
- package/dist/src/cli/renderers.d.mts +299 -0
- package/dist/src/cli/renderers.mjs +554 -0
- 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 +269 -0
- package/dist/src/cli/runtime_commands.mjs +601 -0
- 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 +436 -0
- package/dist/src/cli/source_commands.mjs +301 -0
- package/dist/src/cli/terminal.d.mts +59 -0
- package/dist/src/cli/terminal.mjs +230 -0
- package/dist/src/identity/claim.d.mts +5 -0
- package/dist/src/identity/claim.mjs +4 -0
- 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/identity/provider_configuration.d.mts +37 -0
- package/dist/src/identity/provider_configuration.mjs +132 -0
- package/dist/src/identity/storage.d.mts +21 -0
- package/dist/src/identity/storage.mjs +122 -0
- package/dist/src/index.d.mts +4 -0
- package/dist/src/index.mjs +3 -0
- package/dist/src/project/configuration.d.mts +13 -0
- package/dist/src/project/configuration.mjs +9 -0
- 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/install_scaffold.d.mts +42 -0
- package/dist/src/project/install_scaffold.mjs +263 -0
- package/dist/src/project/project_detection.d.mts +14 -0
- package/dist/src/project/project_detection.mjs +47 -0
- package/dist/src/project/setup.d.mts +87 -0
- package/dist/src/project/setup.mjs +414 -0
- package/dist/src/project/support_report.d.mts +88 -0
- package/dist/src/project/support_report.mjs +83 -0
- package/dist/src/runtime/cleanup.d.mts +85 -0
- package/dist/src/runtime/cleanup.mjs +311 -0
- package/dist/src/runtime/lifecycle_engine.d.mts +63 -0
- package/dist/src/runtime/lifecycle_engine.mjs +114 -0
- package/dist/src/runtime/migration_history.d.mts +51 -0
- package/dist/src/runtime/migration_history.mjs +170 -0
- package/dist/src/runtime/plan.d.mts +134 -0
- package/dist/src/runtime/plan.mjs +637 -0
- package/dist/src/runtime/policy.d.mts +66 -0
- package/dist/src/runtime/policy.mjs +369 -0
- package/dist/src/runtime/restore.d.mts +22 -0
- package/dist/src/runtime/restore.mjs +276 -0
- package/dist/src/runtime/service_environment.d.mts +11 -0
- package/dist/src/runtime/service_environment.mjs +58 -0
- package/dist/src/runtime/topology.d.mts +51 -0
- package/dist/src/runtime/topology.mjs +128 -0
- package/dist/src/shared/diagnostics.d.mts +97 -0
- package/dist/src/shared/diagnostics.mjs +170 -0
- package/dist/src/shared/human_output.d.mts +2 -0
- package/{src → dist/src}/shared/human_output.mjs +1 -3
- package/dist/src/shared/node_runtime.d.mts +14 -0
- package/dist/src/shared/node_runtime.mjs +25 -0
- package/dist/src/shared/operation_guard.d.mts +22 -0
- package/dist/src/shared/operation_guard.mjs +150 -0
- package/dist/src/shared/process_environment.d.mts +15 -0
- package/dist/src/shared/process_environment.mjs +50 -0
- package/dist/src/source/access.d.mts +165 -0
- package/dist/src/source/access.mjs +602 -0
- package/dist/src/source/asset_transfer.d.mts +27 -0
- package/dist/src/source/asset_transfer.mjs +202 -0
- package/dist/src/source/baseline.d.mts +66 -0
- package/dist/src/source/baseline.mjs +372 -0
- package/dist/src/source/external_reader.d.mts +42 -0
- package/dist/src/source/external_reader.mjs +278 -0
- package/dist/src/source/postgresql_access.d.mts +86 -0
- package/dist/src/source/postgresql_access.mjs +454 -0
- package/dist/src/source/privacy_paths.d.mts +10 -0
- package/dist/src/source/privacy_paths.mjs +27 -0
- package/dist/src/targets/driver.d.mts +2 -0
- package/dist/src/targets/driver.mjs +13 -0
- package/dist/src/targets/postgresql.d.mts +5 -0
- package/dist/src/targets/postgresql.mjs +620 -0
- package/dist/src/targets/supabase.d.mts +8 -0
- package/dist/src/targets/supabase.mjs +627 -0
- package/dist/src/targets/supabase_environment.d.mts +49 -0
- package/dist/src/targets/supabase_environment.mjs +162 -0
- package/dist/src/targets/target.d.mts +33 -0
- package/dist/src/targets/target.mjs +39 -0
- package/docs/README.md +1 -0
- package/docs/architecture.md +133 -10
- package/docs/configuration.md +9 -3
- package/docs/index.md +1 -0
- package/docs/releasing.md +8 -3
- package/docs/typescript.md +134 -0
- package/package.json +36 -29
- package/types/configuration.d.mts +5 -0
- package/scripts/install/create_root_config.mjs +0 -19
- package/src/README.md +0 -17
- package/src/application/session.mjs +0 -435
- package/src/baseline/artifact.mjs +0 -684
- package/src/baseline/builder.mjs +0 -164
- package/src/baseline/input_discovery.mjs +0 -155
- package/src/baseline/policy_review.mjs +0 -92
- package/src/baseline/preparation.mjs +0 -323
- package/src/baseline/privacy_engine.mjs +0 -1399
- package/src/baseline/privacy_structured_json.mjs +0 -554
- package/src/baseline/sanitization_policy.mjs +0 -299
- package/src/baseline/schema_snapshot.mjs +0 -113
- package/src/cli/arguments.mjs +0 -126
- package/src/cli/guided.mjs +0 -814
- package/src/cli/init_command.mjs +0 -94
- package/src/cli/rehearsal.mjs +0 -972
- package/src/cli/renderers.mjs +0 -638
- package/src/cli/runtime_commands.mjs +0 -707
- package/src/cli/source_commands.mjs +0 -355
- package/src/cli/terminal.mjs +0 -275
- package/src/identity/claim.mjs +0 -1165
- package/src/identity/provider_configuration.mjs +0 -177
- package/src/identity/storage.mjs +0 -165
- package/src/project/configuration.d.mts +0 -164
- package/src/project/configuration.mjs +0 -1337
- package/src/project/install_scaffold.mjs +0 -325
- package/src/project/setup.mjs +0 -474
- package/src/project/support_report.mjs +0 -105
- package/src/runtime/cleanup.mjs +0 -381
- package/src/runtime/migration_history.mjs +0 -220
- package/src/runtime/plan.mjs +0 -722
- package/src/runtime/policy.mjs +0 -438
- package/src/runtime/restore.mjs +0 -337
- package/src/runtime/service_environment.mjs +0 -82
- package/src/runtime/topology.mjs +0 -177
- package/src/shared/diagnostics.mjs +0 -207
- package/src/shared/node_runtime.mjs +0 -31
- package/src/shared/operation_guard.mjs +0 -162
- package/src/shared/process_environment.mjs +0 -68
- package/src/source/access.mjs +0 -798
- package/src/source/asset_transfer.mjs +0 -259
- package/src/source/baseline.mjs +0 -482
- package/src/source/external_reader.mjs +0 -368
- package/src/source/postgresql_access.mjs +0 -638
- package/src/source/privacy_paths.mjs +0 -38
- package/src/targets/postgresql.mjs +0 -808
- package/src/targets/supabase.mjs +0 -840
- package/src/targets/supabase_environment.mjs +0 -229
- package/src/targets/target.mjs +0 -66
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
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";
|
|
@@ -0,0 +1,4 @@
|
|
|
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";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Create and verify the owner-only secret used by deterministic privacy rules. */
|
|
2
|
+
export declare const PRIVACY_KEY_BYTES = 32;
|
|
3
|
+
export declare const createPrivacyKey: (path: string) => Promise<{
|
|
4
|
+
fingerprint: string;
|
|
5
|
+
}>;
|
|
6
|
+
export declare const readPrivacyKey: (path: string) => Promise<Buffer>;
|
|
7
|
+
export declare const assertPrivacyKeyFingerprint: (key: Uint8Array, expected: unknown) => void;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Create and verify the owner-only secret used by deterministic privacy rules. */
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { chmod, mkdir, open, readFile, stat } from "node:fs/promises";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
export const PRIVACY_KEY_BYTES = 32;
|
|
6
|
+
const HEX_64 = /^[a-f0-9]{64}$/u;
|
|
7
|
+
export const createPrivacyKey = async (path) => {
|
|
8
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
9
|
+
const key = randomBytes(PRIVACY_KEY_BYTES);
|
|
10
|
+
const handle = await open(path, "wx", 0o600);
|
|
11
|
+
try {
|
|
12
|
+
await handle.writeFile(`${key.toString("base64")}\n`);
|
|
13
|
+
await handle.sync();
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
await handle.close();
|
|
17
|
+
}
|
|
18
|
+
await chmod(path, 0o600);
|
|
19
|
+
return { fingerprint: createHash("sha256").update(key).digest("hex") };
|
|
20
|
+
};
|
|
21
|
+
export const readPrivacyKey = async (path) => {
|
|
22
|
+
const details = await stat(path);
|
|
23
|
+
if (!details.isFile() || (details.mode & 0o077) !== 0) {
|
|
24
|
+
throw new Error("Privacy key must be an owner-only regular file.");
|
|
25
|
+
}
|
|
26
|
+
const encoded = (await readFile(path, "utf8")).trim();
|
|
27
|
+
const key = Buffer.from(encoded, "base64");
|
|
28
|
+
if (key.length !== PRIVACY_KEY_BYTES ||
|
|
29
|
+
!/^[A-Za-z0-9+/]+={0,2}$/u.test(encoded)) {
|
|
30
|
+
throw new Error("Privacy key file is invalid.");
|
|
31
|
+
}
|
|
32
|
+
return key;
|
|
33
|
+
};
|
|
34
|
+
export const assertPrivacyKeyFingerprint = (key, expected) => {
|
|
35
|
+
if (typeof expected !== "string" || !HEX_64.test(expected)) {
|
|
36
|
+
throw new Error("Privacy key fingerprint is invalid.");
|
|
37
|
+
}
|
|
38
|
+
const actual = createHash("sha256").update(key).digest("hex");
|
|
39
|
+
if (actual !== expected) {
|
|
40
|
+
throw new Error("Privacy key does not match the reviewed baseline receipt.");
|
|
41
|
+
}
|
|
42
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Resolve reviewed bindings and execute the dependency-aware row privacy pipeline. */
|
|
2
|
+
import type { PrivacyEngine } from "./privacy_contract.mjs";
|
|
3
|
+
export declare const createPrivacyEngine: ({ policy, key, environment, }: {
|
|
4
|
+
policy: unknown;
|
|
5
|
+
key: Uint8Array | string;
|
|
6
|
+
environment?: NodeJS.ProcessEnv;
|
|
7
|
+
}) => Readonly<PrivacyEngine>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/** Resolve reviewed bindings and execute the dependency-aware row privacy pipeline. */
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { PRIVACY_KEY_BYTES } from "./privacy_key.mjs";
|
|
4
|
+
import { validateExecutablePrivacyPolicy } from "./privacy_policy.mjs";
|
|
5
|
+
import { executePrivacyDeclaration } from "./privacy_rule_registry.mjs";
|
|
6
|
+
import { remapPrivacyPath } from "./privacy_transforms.mjs";
|
|
7
|
+
import { IDENTIFIER, isPrivacyObject } from "./privacy_validation_shared.mjs";
|
|
8
|
+
export const createPrivacyEngine = ({ policy, key, environment = process.env, }) => {
|
|
9
|
+
const normalized = validateExecutablePrivacyPolicy(policy);
|
|
10
|
+
const secret = Buffer.isBuffer(key) ? key : Buffer.from(key ?? "");
|
|
11
|
+
if (secret.length < PRIVACY_KEY_BYTES) {
|
|
12
|
+
throw new Error(`Privacy key must contain at least ${PRIVACY_KEY_BYTES} bytes.`);
|
|
13
|
+
}
|
|
14
|
+
const bindingValues = new Map(Object.entries(normalized.bindings).map(([name, binding]) => {
|
|
15
|
+
const value = environment[binding.environmentVariable];
|
|
16
|
+
if (typeof value !== "string" || !value) {
|
|
17
|
+
throw new Error(`Privacy binding ${name} requires ${binding.environmentVariable}.`);
|
|
18
|
+
}
|
|
19
|
+
const receipt = createHash("sha256").update(value).digest("hex");
|
|
20
|
+
if (receipt !== binding.approvedValueSha256) {
|
|
21
|
+
throw new Error(`Privacy binding ${name} does not match its review receipt.`);
|
|
22
|
+
}
|
|
23
|
+
return [name, value];
|
|
24
|
+
}));
|
|
25
|
+
const tables = new Map(normalized.tables.map((table) => [
|
|
26
|
+
table.schema === "public" ? table.name : `${table.schema}.${table.name}`,
|
|
27
|
+
table,
|
|
28
|
+
]));
|
|
29
|
+
return Object.freeze({
|
|
30
|
+
keyFingerprint: createHash("sha256").update(secret).digest("hex"),
|
|
31
|
+
remapPath({ mapping: mappingName, value, }) {
|
|
32
|
+
const mapping = normalized.pathMappings[mappingName];
|
|
33
|
+
if (!mapping) {
|
|
34
|
+
throw new Error(`Privacy path mapping ${mappingName} is not declared.`);
|
|
35
|
+
}
|
|
36
|
+
return remapPrivacyPath({
|
|
37
|
+
key: secret,
|
|
38
|
+
mapping,
|
|
39
|
+
bindingValue: bindingValues.get(mapping.binding),
|
|
40
|
+
value,
|
|
41
|
+
label: `Privacy path mapping ${mappingName}`,
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
sanitize(record) {
|
|
45
|
+
if (!isPrivacyObject(record) ||
|
|
46
|
+
typeof (record.schema ?? "public") !== "string" ||
|
|
47
|
+
!IDENTIFIER.test(String(record.schema ?? "public")) ||
|
|
48
|
+
typeof record.table !== "string" ||
|
|
49
|
+
!IDENTIFIER.test(record.table) ||
|
|
50
|
+
!isPrivacyObject(record.row)) {
|
|
51
|
+
throw new Error("Source record has an invalid privacy-engine shape.");
|
|
52
|
+
}
|
|
53
|
+
const schema = String(record.schema ?? "public");
|
|
54
|
+
const rowSource = record.row;
|
|
55
|
+
const relation = schema === "public" ? record.table : `${schema}.${record.table}`;
|
|
56
|
+
const table = tables.get(relation);
|
|
57
|
+
if (!table) {
|
|
58
|
+
throw new Error(`Privacy policy does not classify table ${relation}.`);
|
|
59
|
+
}
|
|
60
|
+
if (table.sourceRows === "EXCLUDE")
|
|
61
|
+
return null;
|
|
62
|
+
const actual = Object.keys(rowSource).sort();
|
|
63
|
+
const expected = table.columns.map((column) => column.name).sort();
|
|
64
|
+
if (actual.join("\0") !== expected.join("\0")) {
|
|
65
|
+
throw new Error(`Source table ${relation} contains an unclassified or missing column.`);
|
|
66
|
+
}
|
|
67
|
+
let ownerApproved;
|
|
68
|
+
if (table.ownerBinding) {
|
|
69
|
+
const approvedValue = bindingValues.get(table.ownerBinding.binding);
|
|
70
|
+
const owners = table.ownerBinding.columns.map((column) => rowSource[column]);
|
|
71
|
+
for (const owner of owners) {
|
|
72
|
+
if (owner === null) {
|
|
73
|
+
if (table.ownerBinding.nullBehavior !== "otherwise") {
|
|
74
|
+
throw new Error(`${relation} has an invalid owner binding value.`);
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (!["string", "number"].includes(typeof owner) ||
|
|
79
|
+
owner === "" ||
|
|
80
|
+
(typeof owner === "number" && !Number.isFinite(owner))) {
|
|
81
|
+
throw new Error(`${relation} has an invalid owner binding value.`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
ownerApproved = owners.some((owner) => owner !== null && String(owner) === approvedValue);
|
|
85
|
+
}
|
|
86
|
+
const sanitizedColumns = new Map();
|
|
87
|
+
const resolvingColumns = new Set();
|
|
88
|
+
const sanitizeColumn = (columnName) => {
|
|
89
|
+
if (sanitizedColumns.has(columnName)) {
|
|
90
|
+
return sanitizedColumns.get(columnName);
|
|
91
|
+
}
|
|
92
|
+
if (resolvingColumns.has(columnName)) {
|
|
93
|
+
throw new Error(`${relation} contains a cyclic digest dependency.`);
|
|
94
|
+
}
|
|
95
|
+
const column = table.columns.find((entry) => entry.name === columnName);
|
|
96
|
+
if (!column) {
|
|
97
|
+
throw new Error(`${relation} digest references unknown column ${columnName}.`);
|
|
98
|
+
}
|
|
99
|
+
resolvingColumns.add(columnName);
|
|
100
|
+
try {
|
|
101
|
+
const transformed = executePrivacyDeclaration({
|
|
102
|
+
declaration: column,
|
|
103
|
+
value: rowSource[column.name],
|
|
104
|
+
key: secret,
|
|
105
|
+
path: `${relation}.${column.name}`,
|
|
106
|
+
ownerApproved,
|
|
107
|
+
pathMappings: normalized.pathMappings,
|
|
108
|
+
bindingValues,
|
|
109
|
+
resolveInput: sanitizeColumn,
|
|
110
|
+
});
|
|
111
|
+
sanitizedColumns.set(columnName, transformed);
|
|
112
|
+
return transformed;
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
resolvingColumns.delete(columnName);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const row = Object.fromEntries(table.columns.flatMap((column) => {
|
|
119
|
+
const transformed = sanitizeColumn(column.name);
|
|
120
|
+
return transformed === undefined ? [] : [[column.name, transformed]];
|
|
121
|
+
}));
|
|
122
|
+
return schema === "public"
|
|
123
|
+
? { table: record.table, row }
|
|
124
|
+
: { schema, table: record.table, row };
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate and normalize the complete reviewed privacy policy version 2.
|
|
3
|
+
*/
|
|
4
|
+
import type { ExecutablePrivacyPolicy } from "./privacy_contract.mjs";
|
|
5
|
+
export type { ExecutablePrivacyPolicy, PrivacyColumn, PrivacyEngine, PrivacyForeignKey, PrivacySourceRecord, PrivacyTable, } from "./privacy_contract.mjs";
|
|
6
|
+
export declare const validateExecutablePrivacyPolicy: (policy: unknown) => ExecutablePrivacyPolicy;
|