@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,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate and normalize the complete reviewed privacy policy version 2.
|
|
3
|
+
*/
|
|
4
|
+
import { validatePrivacyDependencyGraph } from "./privacy_dependency_graph.mjs";
|
|
5
|
+
import { validatePrivacyRecipe } from "./privacy_recipe_validation.mjs";
|
|
6
|
+
import { ACTIONS, BINDING_NAME, DEFAULT_PATH_MAXIMUM_BYTES, DEFAULT_PATH_MAXIMUM_SEGMENTS, ENVIRONMENT_KEY, HEX_64, assertKnownKeys as knownKeys, isPrivacyObject as isObject, validateIdentifier as identifier, validateNonEmpty as nonEmpty, validatePositiveInteger as positiveInteger, } from "./privacy_validation_shared.mjs";
|
|
7
|
+
export const validateExecutablePrivacyPolicy = (policy) => {
|
|
8
|
+
if (!isObject(policy))
|
|
9
|
+
throw new Error("Privacy policy must be an object.");
|
|
10
|
+
knownKeys(policy, [
|
|
11
|
+
"policyVersion",
|
|
12
|
+
"draft",
|
|
13
|
+
"migrationCutoff",
|
|
14
|
+
"bindings",
|
|
15
|
+
"pathMappings",
|
|
16
|
+
"tables",
|
|
17
|
+
], "policy");
|
|
18
|
+
if (policy.policyVersion !== 2) {
|
|
19
|
+
throw new Error("Executable privacy policies must use policyVersion 2.");
|
|
20
|
+
}
|
|
21
|
+
if (policy.draft === true) {
|
|
22
|
+
throw new Error("The privacy policy is still a REVIEW REQUIRED draft.");
|
|
23
|
+
}
|
|
24
|
+
if (typeof policy.migrationCutoff !== "string" ||
|
|
25
|
+
!/^\d{14}$/u.test(policy.migrationCutoff)) {
|
|
26
|
+
throw new Error("Privacy policy migrationCutoff must be a timestamp version.");
|
|
27
|
+
}
|
|
28
|
+
if (!Array.isArray(policy.tables) || policy.tables.length === 0) {
|
|
29
|
+
throw new Error("Privacy policy must classify at least one table.");
|
|
30
|
+
}
|
|
31
|
+
if (policy.bindings !== undefined && !isObject(policy.bindings)) {
|
|
32
|
+
throw new Error("policy.bindings must be an object.");
|
|
33
|
+
}
|
|
34
|
+
const bindings = Object.fromEntries(Object.entries(policy.bindings ?? {}).map(([name, binding]) => {
|
|
35
|
+
if (!BINDING_NAME.test(name) || !isObject(binding)) {
|
|
36
|
+
throw new Error("policy.bindings contains an invalid binding.");
|
|
37
|
+
}
|
|
38
|
+
knownKeys(binding, ["environmentVariable", "approvedValueSha256"], `policy.bindings.${name}`);
|
|
39
|
+
if (typeof binding.environmentVariable !== "string" ||
|
|
40
|
+
!ENVIRONMENT_KEY.test(binding.environmentVariable)) {
|
|
41
|
+
throw new Error(`policy.bindings.${name}.environmentVariable is invalid.`);
|
|
42
|
+
}
|
|
43
|
+
if (typeof binding.approvedValueSha256 !== "string" ||
|
|
44
|
+
!HEX_64.test(binding.approvedValueSha256)) {
|
|
45
|
+
throw new Error(`policy.bindings.${name}.approvedValueSha256 must be a lowercase SHA-256 receipt.`);
|
|
46
|
+
}
|
|
47
|
+
return [
|
|
48
|
+
name,
|
|
49
|
+
Object.freeze({
|
|
50
|
+
environmentVariable: binding.environmentVariable,
|
|
51
|
+
approvedValueSha256: binding.approvedValueSha256,
|
|
52
|
+
}),
|
|
53
|
+
];
|
|
54
|
+
}));
|
|
55
|
+
if (policy.pathMappings !== undefined && !isObject(policy.pathMappings)) {
|
|
56
|
+
throw new Error("policy.pathMappings must be an object.");
|
|
57
|
+
}
|
|
58
|
+
const pathMappings = Object.fromEntries(Object.entries(policy.pathMappings ?? {}).map(([name, mapping]) => {
|
|
59
|
+
const label = `policy.pathMappings.${name}`;
|
|
60
|
+
if (!BINDING_NAME.test(name) || !isObject(mapping)) {
|
|
61
|
+
throw new Error("policy.pathMappings contains an invalid mapping.");
|
|
62
|
+
}
|
|
63
|
+
knownKeys(mapping, [
|
|
64
|
+
"binding",
|
|
65
|
+
"format",
|
|
66
|
+
"namespace",
|
|
67
|
+
"maxLength",
|
|
68
|
+
"maximumBytes",
|
|
69
|
+
"maximumSegments",
|
|
70
|
+
], label);
|
|
71
|
+
if (typeof mapping.binding !== "string" ||
|
|
72
|
+
!Object.hasOwn(bindings, mapping.binding)) {
|
|
73
|
+
throw new Error(`${label}.binding references an unknown binding.`);
|
|
74
|
+
}
|
|
75
|
+
if (typeof mapping.format !== "string" ||
|
|
76
|
+
!["uuid", "text"].includes(mapping.format)) {
|
|
77
|
+
throw new Error(`${label}.format must be uuid or text.`);
|
|
78
|
+
}
|
|
79
|
+
const normalized = {
|
|
80
|
+
binding: mapping.binding,
|
|
81
|
+
format: mapping.format,
|
|
82
|
+
namespace: nonEmpty(mapping.namespace, `${label}.namespace`),
|
|
83
|
+
maximumBytes: positiveInteger(mapping.maximumBytes ?? DEFAULT_PATH_MAXIMUM_BYTES, `${label}.maximumBytes`, 16_384),
|
|
84
|
+
maximumSegments: positiveInteger(mapping.maximumSegments ?? DEFAULT_PATH_MAXIMUM_SEGMENTS, `${label}.maximumSegments`, 256),
|
|
85
|
+
};
|
|
86
|
+
if (mapping.format === "text") {
|
|
87
|
+
return [
|
|
88
|
+
name,
|
|
89
|
+
Object.freeze({
|
|
90
|
+
...normalized,
|
|
91
|
+
maxLength: positiveInteger(mapping.maxLength, `${label}.maxLength`, 1_024),
|
|
92
|
+
}),
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
else if (mapping.maxLength !== undefined) {
|
|
96
|
+
throw new Error(`${label}.maxLength is supported only for text.`);
|
|
97
|
+
}
|
|
98
|
+
return [name, Object.freeze(normalized)];
|
|
99
|
+
}));
|
|
100
|
+
const names = new Set();
|
|
101
|
+
const tables = policy.tables.map((table, tableIndex) => {
|
|
102
|
+
const label = `policy.tables[${tableIndex}]`;
|
|
103
|
+
if (!isObject(table))
|
|
104
|
+
throw new Error(`${label} must be an object.`);
|
|
105
|
+
knownKeys(table, ["schema", "name", "sourceRows", "ownerBinding", "columns"], label);
|
|
106
|
+
const schema = identifier(table.schema ?? "public", `${label}.schema`);
|
|
107
|
+
const name = identifier(table.name, `${label}.name`);
|
|
108
|
+
const key = schema === "public" ? name : `${schema}.${name}`;
|
|
109
|
+
if (names.has(key))
|
|
110
|
+
throw new Error(`Privacy policy duplicates table ${key}.`);
|
|
111
|
+
names.add(key);
|
|
112
|
+
if (typeof table.sourceRows !== "string" ||
|
|
113
|
+
!["STREAM AND SANITIZE", "EXCLUDE"].includes(table.sourceRows)) {
|
|
114
|
+
throw new Error(`${label}.sourceRows is unsupported.`);
|
|
115
|
+
}
|
|
116
|
+
if (!Array.isArray(table.columns) || table.columns.length === 0) {
|
|
117
|
+
throw new Error(`${label}.columns must not be empty.`);
|
|
118
|
+
}
|
|
119
|
+
const sourceRows = table.sourceRows;
|
|
120
|
+
const columnNames = new Set();
|
|
121
|
+
const columns = table.columns.map((column, columnIndex) => {
|
|
122
|
+
const columnLabel = `${label}.columns[${columnIndex}]`;
|
|
123
|
+
if (!isObject(column))
|
|
124
|
+
throw new Error(`${columnLabel} must be an object.`);
|
|
125
|
+
knownKeys(column, ["name", "action", "recipe", "generated", "identity", "foreignKey"], columnLabel);
|
|
126
|
+
const columnName = identifier(column.name, `${columnLabel}.name`);
|
|
127
|
+
if (columnNames.has(columnName)) {
|
|
128
|
+
throw new Error(`Privacy policy duplicates column ${name}.${columnName}.`);
|
|
129
|
+
}
|
|
130
|
+
columnNames.add(columnName);
|
|
131
|
+
const action = String(column.action ?? "").toUpperCase();
|
|
132
|
+
if (!ACTIONS.has(action))
|
|
133
|
+
throw new Error(`${columnLabel}.action is unsupported.`);
|
|
134
|
+
if (typeof column.generated !== "string" ||
|
|
135
|
+
!["ALWAYS", "NEVER"].includes(column.generated)) {
|
|
136
|
+
throw new Error(`${columnLabel}.generated must be ALWAYS or NEVER.`);
|
|
137
|
+
}
|
|
138
|
+
if (typeof column.identity !== "string" ||
|
|
139
|
+
!["YES", "NO"].includes(column.identity)) {
|
|
140
|
+
throw new Error(`${columnLabel}.identity must be YES or NO.`);
|
|
141
|
+
}
|
|
142
|
+
if (column.foreignKey !== null) {
|
|
143
|
+
if (!isObject(column.foreignKey)) {
|
|
144
|
+
throw new Error(`${columnLabel}.foreignKey must be null or an object.`);
|
|
145
|
+
}
|
|
146
|
+
knownKeys(column.foreignKey, ["schema", "table", "column"], `${columnLabel}.foreignKey`);
|
|
147
|
+
identifier(column.foreignKey.schema, `${columnLabel}.foreignKey.schema`);
|
|
148
|
+
identifier(column.foreignKey.table, `${columnLabel}.foreignKey.table`);
|
|
149
|
+
identifier(column.foreignKey.column, `${columnLabel}.foreignKey.column`);
|
|
150
|
+
}
|
|
151
|
+
return Object.freeze({
|
|
152
|
+
...column,
|
|
153
|
+
name: columnName,
|
|
154
|
+
action: action,
|
|
155
|
+
recipe: validatePrivacyRecipe(action, column.recipe, columnLabel),
|
|
156
|
+
generated: column.generated,
|
|
157
|
+
identity: column.identity,
|
|
158
|
+
foreignKey: column.foreignKey,
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
let ownerBinding = null;
|
|
162
|
+
if (table.ownerBinding !== undefined && table.ownerBinding !== null) {
|
|
163
|
+
if (!isObject(table.ownerBinding)) {
|
|
164
|
+
throw new Error(`${label}.ownerBinding must be an object.`);
|
|
165
|
+
}
|
|
166
|
+
knownKeys(table.ownerBinding, ["binding", "column", "columns", "match", "nullBehavior"], `${label}.ownerBinding`);
|
|
167
|
+
const binding = nonEmpty(table.ownerBinding.binding, `${label}.ownerBinding.binding`);
|
|
168
|
+
const hasColumn = table.ownerBinding.column !== undefined;
|
|
169
|
+
const hasColumns = table.ownerBinding.columns !== undefined;
|
|
170
|
+
if (hasColumn === hasColumns) {
|
|
171
|
+
throw new Error(`${label}.ownerBinding must declare exactly one of column or columns.`);
|
|
172
|
+
}
|
|
173
|
+
let ownerColumns;
|
|
174
|
+
if (hasColumn) {
|
|
175
|
+
if (table.ownerBinding.match !== undefined) {
|
|
176
|
+
throw new Error(`${label}.ownerBinding.match is supported only with columns.`);
|
|
177
|
+
}
|
|
178
|
+
ownerColumns = [
|
|
179
|
+
identifier(table.ownerBinding.column, `${label}.ownerBinding.column`),
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
if (!Array.isArray(table.ownerBinding.columns) ||
|
|
184
|
+
table.ownerBinding.columns.length === 0 ||
|
|
185
|
+
table.ownerBinding.columns.length > 16) {
|
|
186
|
+
throw new Error(`${label}.ownerBinding.columns must contain 1 through 16 columns.`);
|
|
187
|
+
}
|
|
188
|
+
if (table.ownerBinding.match !== "any") {
|
|
189
|
+
throw new Error(`${label}.ownerBinding.match must be any when columns are used.`);
|
|
190
|
+
}
|
|
191
|
+
ownerColumns = table.ownerBinding.columns.map((column, index) => identifier(column, `${label}.ownerBinding.columns[${index}]`));
|
|
192
|
+
if (new Set(ownerColumns).size !== ownerColumns.length) {
|
|
193
|
+
throw new Error(`${label}.ownerBinding.columns must not contain duplicates.`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (table.ownerBinding.nullBehavior !== undefined &&
|
|
197
|
+
table.ownerBinding.nullBehavior !== "otherwise") {
|
|
198
|
+
throw new Error(`${label}.ownerBinding.nullBehavior must be otherwise when declared.`);
|
|
199
|
+
}
|
|
200
|
+
if (!Object.hasOwn(bindings, binding)) {
|
|
201
|
+
throw new Error(`${label}.ownerBinding references an unknown binding.`);
|
|
202
|
+
}
|
|
203
|
+
for (const column of ownerColumns) {
|
|
204
|
+
if (!columnNames.has(column)) {
|
|
205
|
+
throw new Error(`${label}.ownerBinding references an unknown column.`);
|
|
206
|
+
}
|
|
207
|
+
const ownerColumn = columns.find((entry) => entry.name === column);
|
|
208
|
+
if (!ownerColumn || ownerColumn.action !== "PSEUDONYMIZE") {
|
|
209
|
+
throw new Error(`${label}.ownerBinding columns must be explicitly pseudonymized.`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
ownerBinding = Object.freeze({
|
|
213
|
+
binding,
|
|
214
|
+
columns: Object.freeze(ownerColumns),
|
|
215
|
+
match: "any",
|
|
216
|
+
...(table.ownerBinding.nullBehavior === undefined
|
|
217
|
+
? {}
|
|
218
|
+
: { nullBehavior: table.ownerBinding.nullBehavior }),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return Object.freeze({
|
|
222
|
+
schema,
|
|
223
|
+
name,
|
|
224
|
+
sourceRows,
|
|
225
|
+
ownerBinding,
|
|
226
|
+
columns: Object.freeze(columns),
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
validatePrivacyDependencyGraph({ tables, bindings, pathMappings });
|
|
230
|
+
return Object.freeze({
|
|
231
|
+
policyVersion: 2,
|
|
232
|
+
migrationCutoff: policy.migrationCutoff,
|
|
233
|
+
bindings: Object.freeze(bindings),
|
|
234
|
+
pathMappings: Object.freeze(pathMappings),
|
|
235
|
+
tables: Object.freeze(tables),
|
|
236
|
+
});
|
|
237
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate and normalize reviewed privacy recipes for policy version 2.
|
|
3
|
+
*/
|
|
4
|
+
import type { NormalizedRecipe, PrivacyAction } from "./privacy_contract.mjs";
|
|
5
|
+
export declare const validatePrivacyRecipe: (action: PrivacyAction, recipe: unknown, label: string, depth?: number) => NormalizedRecipe;
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate and normalize reviewed privacy recipes for policy version 2.
|
|
3
|
+
*/
|
|
4
|
+
import { validateStructuredJsonRecipe } from "./privacy_structured_json.mjs";
|
|
5
|
+
import { ACTIONS, BINDING_NAME, DATE_REPRESENTATIONS, DERIVATION_KINDS, GTIN_LENGTHS, HEX_LENGTHS, MAXIMUM_ENUM_VALUES, MAXIMUM_POLICY_DEPTH, MAXIMUM_TEXT_SUBSTITUTION_BYTES, MAXIMUM_URL_LENGTH, PSEUDONYM_FORMATS, URL_PATH_PREFIX, URL_TOKEN_LENGTH, assertKnownKeys as knownKeys, isPrivacyObject as isObject, validateHttpsLoopbackOrigin as httpsLoopbackOrigin, validateIdentifier as identifier, validateNonEmpty as nonEmpty, validatePositiveInteger as positiveInteger, validateReviewedScalar as reviewedScalar, } from "./privacy_validation_shared.mjs";
|
|
6
|
+
const validateNestedDeclaration = (declaration, label, { allowRequired = false, depth = 0, } = {}) => {
|
|
7
|
+
if (!isObject(declaration))
|
|
8
|
+
throw new Error(`${label} must be an object.`);
|
|
9
|
+
knownKeys(declaration, allowRequired ? ["action", "recipe", "required"] : ["action", "recipe"], label);
|
|
10
|
+
if (allowRequired &&
|
|
11
|
+
declaration.required !== undefined &&
|
|
12
|
+
typeof declaration.required !== "boolean") {
|
|
13
|
+
throw new Error(`${label}.required must be true or false.`);
|
|
14
|
+
}
|
|
15
|
+
const action = String(declaration.action ?? "").toUpperCase();
|
|
16
|
+
if (!ACTIONS.has(action))
|
|
17
|
+
throw new Error(`${label}.action is unsupported.`);
|
|
18
|
+
return Object.freeze({
|
|
19
|
+
action: action,
|
|
20
|
+
recipe: validatePrivacyRecipe(action, declaration.recipe, label, depth + 1),
|
|
21
|
+
...(allowRequired ? { required: declaration.required !== false } : {}),
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
export const validatePrivacyRecipe = (action, recipe, label, depth = 0) => {
|
|
25
|
+
if (depth > MAXIMUM_POLICY_DEPTH) {
|
|
26
|
+
throw new Error(`${label}.recipe exceeds the supported nesting depth.`);
|
|
27
|
+
}
|
|
28
|
+
if (action === "KEEP" || action === "EXCLUDE") {
|
|
29
|
+
if (recipe !== undefined && recipe !== null) {
|
|
30
|
+
throw new Error(`${label}.recipe is not allowed for ${action}.`);
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (!isObject(recipe)) {
|
|
35
|
+
throw new Error(`${label}.recipe is required for ${action}.`);
|
|
36
|
+
}
|
|
37
|
+
if (action === "PSEUDONYMIZE") {
|
|
38
|
+
knownKeys(recipe, [
|
|
39
|
+
"format",
|
|
40
|
+
"namespace",
|
|
41
|
+
"maxLength",
|
|
42
|
+
"length",
|
|
43
|
+
"allowedLengths",
|
|
44
|
+
"origin",
|
|
45
|
+
], `${label}.recipe`);
|
|
46
|
+
if (typeof recipe.format !== "string" ||
|
|
47
|
+
!PSEUDONYM_FORMATS.has(recipe.format)) {
|
|
48
|
+
throw new Error(`${label}.recipe.format is unsupported.`);
|
|
49
|
+
}
|
|
50
|
+
const normalized = {
|
|
51
|
+
format: recipe.format,
|
|
52
|
+
namespace: nonEmpty(recipe.namespace, `${label}.recipe.namespace`),
|
|
53
|
+
};
|
|
54
|
+
if (recipe.format === "text") {
|
|
55
|
+
normalized.maxLength = positiveInteger(recipe.maxLength, `${label}.recipe.maxLength`, 1_024);
|
|
56
|
+
}
|
|
57
|
+
else if (recipe.format === "url") {
|
|
58
|
+
normalized.origin = httpsLoopbackOrigin(recipe.origin, `${label}.recipe.origin`);
|
|
59
|
+
normalized.maxLength = positiveInteger(recipe.maxLength, `${label}.recipe.maxLength`, MAXIMUM_URL_LENGTH);
|
|
60
|
+
const outputLength = normalized.origin.length + URL_PATH_PREFIX.length + URL_TOKEN_LENGTH;
|
|
61
|
+
if (normalized.maxLength < outputLength) {
|
|
62
|
+
throw new Error(`${label}.recipe.maxLength must allow the complete pseudonymous URL (${outputLength} characters).`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else if (recipe.maxLength !== undefined) {
|
|
66
|
+
throw new Error(`${label}.recipe.maxLength is supported only for text and url pseudonyms.`);
|
|
67
|
+
}
|
|
68
|
+
if (recipe.format === "hex") {
|
|
69
|
+
if (typeof recipe.length !== "number" ||
|
|
70
|
+
!HEX_LENGTHS.has(recipe.length)) {
|
|
71
|
+
throw new Error(`${label}.recipe.length must be 32 or 64 for hex.`);
|
|
72
|
+
}
|
|
73
|
+
normalized.length = recipe.length;
|
|
74
|
+
}
|
|
75
|
+
else if (recipe.format === "gtin") {
|
|
76
|
+
const hasLength = Object.hasOwn(recipe, "length");
|
|
77
|
+
const hasAllowedLengths = Object.hasOwn(recipe, "allowedLengths");
|
|
78
|
+
if (hasLength === hasAllowedLengths) {
|
|
79
|
+
throw new Error(`${label}.recipe must declare exactly one of length or allowedLengths for gtin.`);
|
|
80
|
+
}
|
|
81
|
+
if (hasLength) {
|
|
82
|
+
if (typeof recipe.length !== "number" ||
|
|
83
|
+
!GTIN_LENGTHS.has(recipe.length)) {
|
|
84
|
+
throw new Error(`${label}.recipe.length must be 8, 12, 13, or 14 for gtin.`);
|
|
85
|
+
}
|
|
86
|
+
normalized.length = recipe.length;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
if (!Array.isArray(recipe.allowedLengths) ||
|
|
90
|
+
recipe.allowedLengths.length === 0) {
|
|
91
|
+
throw new Error(`${label}.recipe.allowedLengths must contain reviewed GTIN lengths 8, 12, 13, or 14.`);
|
|
92
|
+
}
|
|
93
|
+
const allowedLengths = recipe.allowedLengths.map((length) => {
|
|
94
|
+
if (typeof length !== "number" || !GTIN_LENGTHS.has(length)) {
|
|
95
|
+
throw new Error(`${label}.recipe.allowedLengths must contain reviewed GTIN lengths 8, 12, 13, or 14.`);
|
|
96
|
+
}
|
|
97
|
+
return length;
|
|
98
|
+
});
|
|
99
|
+
if (new Set(allowedLengths).size !== allowedLengths.length) {
|
|
100
|
+
throw new Error(`${label}.recipe.allowedLengths must not contain duplicates.`);
|
|
101
|
+
}
|
|
102
|
+
normalized.allowedLengths = Object.freeze(allowedLengths);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else if (recipe.length !== undefined ||
|
|
106
|
+
recipe.allowedLengths !== undefined ||
|
|
107
|
+
(recipe.origin !== undefined && recipe.format !== "url")) {
|
|
108
|
+
throw new Error(`${label}.recipe shape controls do not match its pseudonym format.`);
|
|
109
|
+
}
|
|
110
|
+
return Object.freeze(normalized);
|
|
111
|
+
}
|
|
112
|
+
if (action === "REPLACE") {
|
|
113
|
+
knownKeys(recipe, ["kind", "value"], `${label}.recipe`);
|
|
114
|
+
if (recipe.kind !== "constant" || !Object.hasOwn(recipe, "value")) {
|
|
115
|
+
throw new Error(`${label}.recipe must be an explicit constant.`);
|
|
116
|
+
}
|
|
117
|
+
if (typeof recipe.value === "function" || recipe.value === undefined) {
|
|
118
|
+
throw new Error(`${label}.recipe.value must be JSON-compatible.`);
|
|
119
|
+
}
|
|
120
|
+
JSON.stringify(recipe.value);
|
|
121
|
+
return Object.freeze({ kind: "constant", value: recipe.value });
|
|
122
|
+
}
|
|
123
|
+
knownKeys(recipe, [
|
|
124
|
+
"kind",
|
|
125
|
+
"days",
|
|
126
|
+
"group",
|
|
127
|
+
"representation",
|
|
128
|
+
"format",
|
|
129
|
+
"length",
|
|
130
|
+
"namespace",
|
|
131
|
+
"inputs",
|
|
132
|
+
"mapping",
|
|
133
|
+
"binding",
|
|
134
|
+
"fields",
|
|
135
|
+
"items",
|
|
136
|
+
"variants",
|
|
137
|
+
"keys",
|
|
138
|
+
"values",
|
|
139
|
+
"approved",
|
|
140
|
+
"otherwise",
|
|
141
|
+
"allowNull",
|
|
142
|
+
"maximumBytes",
|
|
143
|
+
"maximumDepth",
|
|
144
|
+
"maximumItems",
|
|
145
|
+
], `${label}.recipe`);
|
|
146
|
+
if (typeof recipe.kind !== "string" || !DERIVATION_KINDS.has(recipe.kind)) {
|
|
147
|
+
throw new Error(`${label}.recipe.kind is unsupported.`);
|
|
148
|
+
}
|
|
149
|
+
if (recipe.kind === "enum") {
|
|
150
|
+
if (Object.keys(recipe).some((key) => !["kind", "values", "allowNull"].includes(key))) {
|
|
151
|
+
throw new Error(`${label}.recipe contains fields not used by enum.`);
|
|
152
|
+
}
|
|
153
|
+
if (!Array.isArray(recipe.values) ||
|
|
154
|
+
recipe.values.length === 0 ||
|
|
155
|
+
recipe.values.length > MAXIMUM_ENUM_VALUES) {
|
|
156
|
+
throw new Error(`${label}.recipe.values must contain 1 through ${MAXIMUM_ENUM_VALUES} reviewed scalars.`);
|
|
157
|
+
}
|
|
158
|
+
if (recipe.allowNull !== undefined &&
|
|
159
|
+
typeof recipe.allowNull !== "boolean") {
|
|
160
|
+
throw new Error(`${label}.recipe.allowNull must be true or false.`);
|
|
161
|
+
}
|
|
162
|
+
const values = Array.from(recipe.values, (value, index) => reviewedScalar(value, `${label}.recipe.values[${index}]`));
|
|
163
|
+
const identities = values.map((value) => JSON.stringify([typeof value, value]));
|
|
164
|
+
if (new Set(identities).size !== identities.length) {
|
|
165
|
+
throw new Error(`${label}.recipe.values must not contain duplicates.`);
|
|
166
|
+
}
|
|
167
|
+
return Object.freeze({
|
|
168
|
+
kind: "enum",
|
|
169
|
+
allowNull: recipe.allowNull === true,
|
|
170
|
+
values: Object.freeze(values),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
if (recipe.kind === "validated-string") {
|
|
174
|
+
if (Object.keys(recipe).some((key) => !["kind", "format", "maximumBytes", "allowNull"].includes(key))) {
|
|
175
|
+
throw new Error(`${label}.recipe contains fields not used by validated-string.`);
|
|
176
|
+
}
|
|
177
|
+
if (recipe.format !== "portable-code") {
|
|
178
|
+
throw new Error(`${label}.recipe.format must be portable-code for validated-string.`);
|
|
179
|
+
}
|
|
180
|
+
const maximumBytes = positiveInteger(recipe.maximumBytes, `${label}.recipe.maximumBytes`, 1_024);
|
|
181
|
+
if (recipe.allowNull !== undefined &&
|
|
182
|
+
typeof recipe.allowNull !== "boolean") {
|
|
183
|
+
throw new Error(`${label}.recipe.allowNull must be true or false.`);
|
|
184
|
+
}
|
|
185
|
+
return Object.freeze({
|
|
186
|
+
kind: "validated-string",
|
|
187
|
+
format: "portable-code",
|
|
188
|
+
maximumBytes,
|
|
189
|
+
allowNull: recipe.allowNull === true,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
if (recipe.kind === "digest") {
|
|
193
|
+
if (Object.keys(recipe).some((key) => !["kind", "format", "length", "namespace", "inputs"].includes(key))) {
|
|
194
|
+
throw new Error(`${label}.recipe contains fields not used by digest.`);
|
|
195
|
+
}
|
|
196
|
+
if (recipe.format !== "hex") {
|
|
197
|
+
throw new Error(`${label}.recipe.format must be hex for digest.`);
|
|
198
|
+
}
|
|
199
|
+
if (typeof recipe.length !== "number" || !HEX_LENGTHS.has(recipe.length)) {
|
|
200
|
+
throw new Error(`${label}.recipe.length must be 32 or 64 for digest.`);
|
|
201
|
+
}
|
|
202
|
+
if (!Array.isArray(recipe.inputs) ||
|
|
203
|
+
recipe.inputs.length === 0 ||
|
|
204
|
+
recipe.inputs.length > 32) {
|
|
205
|
+
throw new Error(`${label}.recipe.inputs must contain 1 through 32 columns.`);
|
|
206
|
+
}
|
|
207
|
+
const inputs = recipe.inputs.map((input, index) => identifier(input, `${label}.recipe.inputs[${index}]`));
|
|
208
|
+
if (new Set(inputs).size !== inputs.length) {
|
|
209
|
+
throw new Error(`${label}.recipe.inputs must not contain duplicates.`);
|
|
210
|
+
}
|
|
211
|
+
return Object.freeze({
|
|
212
|
+
kind: "digest",
|
|
213
|
+
format: "hex",
|
|
214
|
+
length: recipe.length,
|
|
215
|
+
namespace: nonEmpty(recipe.namespace, `${label}.recipe.namespace`),
|
|
216
|
+
inputs: Object.freeze(inputs),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
if (recipe.kind === "date-shift") {
|
|
220
|
+
const days = positiveInteger(recipe.days, `${label}.recipe.days`, 3_650);
|
|
221
|
+
if (Object.keys(recipe).some((key) => !["kind", "days", "group", "representation"].includes(key))) {
|
|
222
|
+
throw new Error(`${label}.recipe contains fields not used by date-shift.`);
|
|
223
|
+
}
|
|
224
|
+
if (recipe.group !== undefined &&
|
|
225
|
+
(typeof recipe.group !== "string" || !BINDING_NAME.test(recipe.group))) {
|
|
226
|
+
throw new Error(`${label}.recipe.group is invalid.`);
|
|
227
|
+
}
|
|
228
|
+
const representation = recipe.representation === undefined
|
|
229
|
+
? "iso-string"
|
|
230
|
+
: recipe.representation;
|
|
231
|
+
if (typeof representation !== "string" ||
|
|
232
|
+
!DATE_REPRESENTATIONS.has(representation)) {
|
|
233
|
+
throw new Error(`${label}.recipe.representation must be iso-string or epoch-milliseconds.`);
|
|
234
|
+
}
|
|
235
|
+
return Object.freeze({
|
|
236
|
+
kind: "date-shift",
|
|
237
|
+
days,
|
|
238
|
+
representation: representation,
|
|
239
|
+
...(recipe.group === undefined ? {} : { group: recipe.group }),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
if (recipe.kind === "path-map") {
|
|
243
|
+
if (Object.keys(recipe).some((key) => !["kind", "mapping"].includes(key))) {
|
|
244
|
+
throw new Error(`${label}.recipe contains fields not used by path-map.`);
|
|
245
|
+
}
|
|
246
|
+
if (typeof recipe.mapping !== "string" ||
|
|
247
|
+
!BINDING_NAME.test(recipe.mapping)) {
|
|
248
|
+
throw new Error(`${label}.recipe.mapping is invalid.`);
|
|
249
|
+
}
|
|
250
|
+
return Object.freeze({ kind: "path-map", mapping: recipe.mapping });
|
|
251
|
+
}
|
|
252
|
+
if (recipe.kind === "binding-substitute") {
|
|
253
|
+
if (Object.keys(recipe).some((key) => !["kind", "binding", "format", "namespace", "maximumBytes"].includes(key))) {
|
|
254
|
+
throw new Error(`${label}.recipe contains fields not used by binding-substitute.`);
|
|
255
|
+
}
|
|
256
|
+
if (typeof recipe.binding !== "string" ||
|
|
257
|
+
!BINDING_NAME.test(recipe.binding)) {
|
|
258
|
+
throw new Error(`${label}.recipe.binding is invalid.`);
|
|
259
|
+
}
|
|
260
|
+
if (recipe.format !== "uuid") {
|
|
261
|
+
throw new Error(`${label}.recipe.format must be uuid.`);
|
|
262
|
+
}
|
|
263
|
+
return Object.freeze({
|
|
264
|
+
kind: "binding-substitute",
|
|
265
|
+
binding: recipe.binding,
|
|
266
|
+
format: "uuid",
|
|
267
|
+
namespace: nonEmpty(recipe.namespace, `${label}.recipe.namespace`),
|
|
268
|
+
maximumBytes: positiveInteger(recipe.maximumBytes, `${label}.recipe.maximumBytes`, MAXIMUM_TEXT_SUBSTITUTION_BYTES),
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
if (recipe.kind === "approved-owner") {
|
|
272
|
+
if (Object.keys(recipe).some((key) => !["kind", "approved", "otherwise"].includes(key))) {
|
|
273
|
+
throw new Error(`${label}.recipe contains fields not used by approved-owner.`);
|
|
274
|
+
}
|
|
275
|
+
return Object.freeze({
|
|
276
|
+
kind: "approved-owner",
|
|
277
|
+
approved: validateNestedDeclaration(recipe.approved, `${label}.recipe.approved`, { depth }),
|
|
278
|
+
otherwise: validateNestedDeclaration(recipe.otherwise, `${label}.recipe.otherwise`, { depth }),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return validateStructuredJsonRecipe({
|
|
282
|
+
recipe,
|
|
283
|
+
label,
|
|
284
|
+
depth,
|
|
285
|
+
maximumPolicyDepth: MAXIMUM_POLICY_DEPTH,
|
|
286
|
+
validateDeclaration: validateNestedDeclaration,
|
|
287
|
+
validatePseudonymRecipe: (nestedRecipe, nestedLabel, nestedDepth) => validatePrivacyRecipe("PSEUDONYMIZE", nestedRecipe, nestedLabel, nestedDepth),
|
|
288
|
+
});
|
|
289
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Explicit typed registry for every normalized privacy action and derivation rule. */
|
|
2
|
+
import type { DerivationRecipe, NormalizedDeclaration, PrivacyAction, PrivacyPathMapping } from "./privacy_contract.mjs";
|
|
3
|
+
interface PrivacyExecutionContext {
|
|
4
|
+
readonly declaration: NormalizedDeclaration;
|
|
5
|
+
readonly value: unknown;
|
|
6
|
+
readonly key: Uint8Array;
|
|
7
|
+
readonly path: string;
|
|
8
|
+
readonly ownerApproved: boolean | undefined;
|
|
9
|
+
readonly pathMappings: Readonly<Record<string, PrivacyPathMapping>>;
|
|
10
|
+
readonly bindingValues: ReadonlyMap<string, string>;
|
|
11
|
+
readonly resolveInput: (name: string) => unknown;
|
|
12
|
+
}
|
|
13
|
+
type ActionHandler = (context: PrivacyExecutionContext) => unknown;
|
|
14
|
+
type DerivationHandler = (recipe: DerivationRecipe, context: PrivacyExecutionContext) => unknown;
|
|
15
|
+
export declare const PRIVACY_DERIVATION_RULES: Readonly<Record<DerivationRecipe["kind"], DerivationHandler>>;
|
|
16
|
+
export declare const PRIVACY_ACTION_RULES: Readonly<Record<PrivacyAction, ActionHandler>>;
|
|
17
|
+
export declare const executePrivacyDeclaration: (context: PrivacyExecutionContext) => unknown;
|
|
18
|
+
export {};
|