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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +4 -1
  3. package/dist/package.json +100 -0
  4. package/dist/scripts/install/create_root_config.d.mts +3 -0
  5. package/dist/scripts/install/create_root_config.mjs +13 -0
  6. package/dist/scripts/runtime/manage_database.d.mts +6 -0
  7. package/{scripts → dist/scripts}/runtime/manage_database.mjs +6 -8
  8. package/dist/src/application/session.d.mts +96 -0
  9. package/dist/src/application/session.mjs +380 -0
  10. package/dist/src/baseline/artifact.d.mts +119 -0
  11. package/dist/src/baseline/artifact.mjs +522 -0
  12. package/dist/src/baseline/builder.d.mts +19 -0
  13. package/dist/src/baseline/builder.mjs +142 -0
  14. package/dist/src/baseline/input_discovery.d.mts +13 -0
  15. package/dist/src/baseline/input_discovery.mjs +149 -0
  16. package/dist/src/baseline/policy_review.d.mts +56 -0
  17. package/dist/src/baseline/policy_review.mjs +73 -0
  18. package/dist/src/baseline/preparation.d.mts +63 -0
  19. package/dist/src/baseline/preparation.mjs +295 -0
  20. package/dist/src/baseline/privacy_engine.d.mts +162 -0
  21. package/dist/src/baseline/privacy_engine.mjs +1162 -0
  22. package/dist/src/baseline/privacy_structured_json.d.mts +66 -0
  23. package/dist/src/baseline/privacy_structured_json.mjs +464 -0
  24. package/dist/src/baseline/sanitization_policy.d.mts +69 -0
  25. package/dist/src/baseline/sanitization_policy.mjs +227 -0
  26. package/dist/src/baseline/schema_snapshot.d.mts +19 -0
  27. package/dist/src/baseline/schema_snapshot.mjs +79 -0
  28. package/dist/src/cli/arguments.d.mts +43 -0
  29. package/dist/src/cli/arguments.mjs +135 -0
  30. package/dist/src/cli/guided.d.mts +51 -0
  31. package/dist/src/cli/guided.mjs +713 -0
  32. package/dist/src/cli/init_command.d.mts +21 -0
  33. package/dist/src/cli/init_command.mjs +90 -0
  34. package/dist/src/cli/project_commands.d.mts +157 -0
  35. package/dist/src/cli/project_commands.mjs +183 -0
  36. package/dist/src/cli/rehearsal.d.mts +7 -0
  37. package/dist/src/cli/rehearsal.mjs +706 -0
  38. package/dist/src/cli/renderers.d.mts +299 -0
  39. package/dist/src/cli/renderers.mjs +554 -0
  40. package/dist/src/cli/runtime_commands.d.mts +265 -0
  41. package/dist/src/cli/runtime_commands.mjs +599 -0
  42. package/dist/src/cli/source_commands.d.mts +432 -0
  43. package/dist/src/cli/source_commands.mjs +299 -0
  44. package/dist/src/cli/terminal.d.mts +59 -0
  45. package/dist/src/cli/terminal.mjs +230 -0
  46. package/dist/src/identity/claim.d.mts +144 -0
  47. package/dist/src/identity/claim.mjs +947 -0
  48. package/dist/src/identity/provider_configuration.d.mts +37 -0
  49. package/dist/src/identity/provider_configuration.mjs +132 -0
  50. package/dist/src/identity/storage.d.mts +21 -0
  51. package/dist/src/identity/storage.mjs +122 -0
  52. package/dist/src/index.d.mts +4 -0
  53. package/dist/src/index.mjs +3 -0
  54. package/dist/src/project/configuration.d.mts +181 -0
  55. package/dist/src/project/configuration.mjs +958 -0
  56. package/dist/src/project/install_scaffold.d.mts +42 -0
  57. package/dist/src/project/install_scaffold.mjs +263 -0
  58. package/dist/src/project/setup.d.mts +87 -0
  59. package/dist/src/project/setup.mjs +414 -0
  60. package/dist/src/project/support_report.d.mts +88 -0
  61. package/dist/src/project/support_report.mjs +83 -0
  62. package/dist/src/runtime/cleanup.d.mts +85 -0
  63. package/dist/src/runtime/cleanup.mjs +311 -0
  64. package/dist/src/runtime/migration_history.d.mts +51 -0
  65. package/dist/src/runtime/migration_history.mjs +170 -0
  66. package/dist/src/runtime/plan.d.mts +134 -0
  67. package/dist/src/runtime/plan.mjs +636 -0
  68. package/dist/src/runtime/policy.d.mts +66 -0
  69. package/dist/src/runtime/policy.mjs +369 -0
  70. package/dist/src/runtime/restore.d.mts +22 -0
  71. package/dist/src/runtime/restore.mjs +276 -0
  72. package/dist/src/runtime/service_environment.d.mts +11 -0
  73. package/dist/src/runtime/service_environment.mjs +58 -0
  74. package/dist/src/runtime/topology.d.mts +51 -0
  75. package/dist/src/runtime/topology.mjs +128 -0
  76. package/dist/src/shared/diagnostics.d.mts +97 -0
  77. package/dist/src/shared/diagnostics.mjs +170 -0
  78. package/dist/src/shared/human_output.d.mts +2 -0
  79. package/{src → dist/src}/shared/human_output.mjs +1 -3
  80. package/dist/src/shared/node_runtime.d.mts +14 -0
  81. package/dist/src/shared/node_runtime.mjs +25 -0
  82. package/dist/src/shared/operation_guard.d.mts +22 -0
  83. package/dist/src/shared/operation_guard.mjs +150 -0
  84. package/dist/src/shared/process_environment.d.mts +15 -0
  85. package/dist/src/shared/process_environment.mjs +50 -0
  86. package/dist/src/source/access.d.mts +165 -0
  87. package/dist/src/source/access.mjs +602 -0
  88. package/dist/src/source/asset_transfer.d.mts +27 -0
  89. package/dist/src/source/asset_transfer.mjs +202 -0
  90. package/dist/src/source/baseline.d.mts +66 -0
  91. package/dist/src/source/baseline.mjs +372 -0
  92. package/dist/src/source/external_reader.d.mts +42 -0
  93. package/dist/src/source/external_reader.mjs +278 -0
  94. package/dist/src/source/postgresql_access.d.mts +86 -0
  95. package/dist/src/source/postgresql_access.mjs +454 -0
  96. package/dist/src/source/privacy_paths.d.mts +10 -0
  97. package/dist/src/source/privacy_paths.mjs +27 -0
  98. package/dist/src/targets/driver.d.mts +2 -0
  99. package/dist/src/targets/driver.mjs +13 -0
  100. package/dist/src/targets/postgresql.d.mts +5 -0
  101. package/dist/src/targets/postgresql.mjs +641 -0
  102. package/dist/src/targets/supabase.d.mts +8 -0
  103. package/dist/src/targets/supabase.mjs +659 -0
  104. package/dist/src/targets/supabase_environment.d.mts +49 -0
  105. package/dist/src/targets/supabase_environment.mjs +162 -0
  106. package/dist/src/targets/target.d.mts +33 -0
  107. package/dist/src/targets/target.mjs +39 -0
  108. package/docs/README.md +1 -0
  109. package/docs/architecture.md +17 -9
  110. package/docs/configuration.md +49 -8
  111. package/docs/getting-started.md +28 -0
  112. package/docs/index.md +1 -0
  113. package/docs/production-source.md +43 -7
  114. package/docs/releasing.md +5 -3
  115. package/docs/roadmap.md +3 -2
  116. package/docs/sanitization.md +415 -14
  117. package/docs/standalone-workflow.md +12 -6
  118. package/docs/troubleshooting.md +5 -1
  119. package/docs/tutorial.md +5 -0
  120. package/docs/typescript.md +87 -0
  121. package/package.json +35 -29
  122. package/scripts/install/create_root_config.mjs +0 -36
  123. package/src/README.md +0 -17
  124. package/src/application/session.mjs +0 -435
  125. package/src/baseline/artifact.mjs +0 -684
  126. package/src/baseline/builder.mjs +0 -164
  127. package/src/baseline/input_discovery.mjs +0 -155
  128. package/src/baseline/policy_review.mjs +0 -92
  129. package/src/baseline/preparation.mjs +0 -323
  130. package/src/baseline/privacy_engine.mjs +0 -952
  131. package/src/baseline/sanitization_policy.mjs +0 -299
  132. package/src/baseline/schema_snapshot.mjs +0 -113
  133. package/src/cli/arguments.mjs +0 -126
  134. package/src/cli/guided.mjs +0 -814
  135. package/src/cli/init_command.mjs +0 -94
  136. package/src/cli/rehearsal.mjs +0 -970
  137. package/src/cli/renderers.mjs +0 -626
  138. package/src/cli/runtime_commands.mjs +0 -707
  139. package/src/cli/source_commands.mjs +0 -347
  140. package/src/cli/terminal.mjs +0 -275
  141. package/src/identity/claim.mjs +0 -1163
  142. package/src/identity/provider_configuration.mjs +0 -177
  143. package/src/identity/storage.mjs +0 -165
  144. package/src/project/configuration.mjs +0 -1337
  145. package/src/project/install_scaffold.mjs +0 -291
  146. package/src/project/setup.mjs +0 -466
  147. package/src/project/support_report.mjs +0 -105
  148. package/src/runtime/cleanup.mjs +0 -381
  149. package/src/runtime/migration_history.mjs +0 -220
  150. package/src/runtime/plan.mjs +0 -722
  151. package/src/runtime/policy.mjs +0 -438
  152. package/src/runtime/restore.mjs +0 -337
  153. package/src/runtime/service_environment.mjs +0 -82
  154. package/src/runtime/topology.mjs +0 -177
  155. package/src/shared/diagnostics.mjs +0 -207
  156. package/src/shared/node_runtime.mjs +0 -31
  157. package/src/shared/operation_guard.mjs +0 -162
  158. package/src/shared/process_environment.mjs +0 -68
  159. package/src/source/access.mjs +0 -753
  160. package/src/source/asset_transfer.mjs +0 -207
  161. package/src/source/baseline.mjs +0 -482
  162. package/src/source/external_reader.mjs +0 -255
  163. package/src/source/postgresql_access.mjs +0 -581
  164. package/src/source/privacy_paths.mjs +0 -38
  165. package/src/targets/postgresql.mjs +0 -808
  166. package/src/targets/supabase.mjs +0 -840
  167. package/src/targets/supabase_environment.mjs +0 -229
  168. package/src/targets/target.mjs +0 -66
  169. /package/{src/project → types}/configuration.d.mts +0 -0
@@ -0,0 +1,66 @@
1
+ /** Validate and execute the bounded structured-JSON privacy recipes. */
2
+ type PrivacyObject = Record<string, unknown>;
3
+ type DeclarationLike = {
4
+ action: "KEEP" | "PSEUDONYMIZE" | "REPLACE" | "EXCLUDE" | "DERIVE";
5
+ recipe: unknown;
6
+ required?: boolean;
7
+ };
8
+ type DictionaryKeyFormat = "integer" | "positive-integer" | "uuid" | "identifier" | "text";
9
+ type DictionaryKeyRule = {
10
+ format: DictionaryKeyFormat;
11
+ action: "KEEP" | "PSEUDONYMIZE";
12
+ minimum?: number;
13
+ maximum?: number;
14
+ maximumLength?: number;
15
+ recipe?: unknown;
16
+ };
17
+ type JsonBounds = {
18
+ allowNull: boolean;
19
+ maximumBytes: number;
20
+ maximumDepth: number;
21
+ };
22
+ type JsonArrayRecipe = JsonBounds & {
23
+ kind: "json-array";
24
+ maximumItems: number;
25
+ items: DeclarationLike;
26
+ };
27
+ type JsonUnionRecipe = JsonBounds & {
28
+ kind: "json-union";
29
+ maximumItems: number;
30
+ variants: Readonly<Record<string, DeclarationLike>>;
31
+ };
32
+ type JsonDictionaryRecipe = JsonBounds & {
33
+ kind: "json-dictionary";
34
+ maximumItems: number;
35
+ keys: Readonly<DictionaryKeyRule>;
36
+ values: DeclarationLike;
37
+ };
38
+ type JsonObjectRecipe = JsonBounds & {
39
+ kind: "json-object";
40
+ fields: Readonly<Record<string, DeclarationLike>>;
41
+ };
42
+ export type StructuredJsonRecipe = JsonArrayRecipe | JsonUnionRecipe | JsonDictionaryRecipe | JsonObjectRecipe;
43
+ type ValidateDeclaration = (declaration: unknown, label: string, options?: {
44
+ allowRequired?: boolean;
45
+ depth?: number;
46
+ }) => DeclarationLike;
47
+ type ValidatePseudonymRecipe = (recipe: unknown, label: string, depth: number) => unknown;
48
+ export declare const STRUCTURED_JSON_KINDS: Set<string>;
49
+ export declare const validateStructuredJsonRecipe: ({ recipe, label, depth, maximumPolicyDepth, validateDeclaration, validatePseudonymRecipe, }: {
50
+ recipe: PrivacyObject;
51
+ label: string;
52
+ depth: number;
53
+ maximumPolicyDepth: number;
54
+ validateDeclaration: ValidateDeclaration;
55
+ validatePseudonymRecipe: ValidatePseudonymRecipe;
56
+ }) => StructuredJsonRecipe;
57
+ export declare const structuredJsonDeclarations: <T extends DeclarationLike = DeclarationLike>(recipe: unknown) => T[];
58
+ export declare const executeStructuredJsonDeclaration: <T extends DeclarationLike>({ declaration, value, key, path, execute, pseudonymize, }: {
59
+ declaration: T;
60
+ value: unknown;
61
+ key: Uint8Array;
62
+ path: string;
63
+ execute: (declaration: T, value: unknown, path: string) => unknown;
64
+ pseudonymize: (key: Uint8Array, recipe: unknown, value: unknown) => unknown;
65
+ }) => unknown;
66
+ export {};
@@ -0,0 +1,464 @@
1
+ /** Validate and execute the bounded structured-JSON privacy recipes. */
2
+ const JSON_UNION_VARIANTS = new Set([
3
+ "string",
4
+ "number",
5
+ "boolean",
6
+ "array",
7
+ "object",
8
+ ]);
9
+ const DICTIONARY_KEY_FORMATS = new Set([
10
+ "integer",
11
+ "positive-integer",
12
+ "uuid",
13
+ "identifier",
14
+ "text",
15
+ ]);
16
+ const IDENTIFIER = /^[a-z][a-z0-9_]{0,62}$/u;
17
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
18
+ const DEFAULT_MAXIMUM_BYTES = 65_536;
19
+ const DEFAULT_MAXIMUM_DEPTH = 8;
20
+ export const STRUCTURED_JSON_KINDS = new Set([
21
+ "json-object",
22
+ "json-array",
23
+ "json-union",
24
+ "json-dictionary",
25
+ ]);
26
+ const isObject = (value) => value !== null &&
27
+ typeof value === "object" &&
28
+ !Array.isArray(value) &&
29
+ Object.getPrototypeOf(value) === Object.prototype;
30
+ const knownKeys = (value, allowed, label) => {
31
+ for (const key of Object.keys(value)) {
32
+ if (!allowed.includes(key))
33
+ throw new Error(`${label}.${key} is unknown.`);
34
+ }
35
+ };
36
+ const positiveInteger = (value, label, maximum) => {
37
+ if (typeof value !== "number" ||
38
+ !Number.isSafeInteger(value) ||
39
+ value < 1 ||
40
+ value > maximum) {
41
+ throw new Error(`${label} must be an integer from 1 through ${maximum}.`);
42
+ }
43
+ return value;
44
+ };
45
+ const safeInteger = (value, label) => {
46
+ if (typeof value !== "number" || !Number.isSafeInteger(value)) {
47
+ throw new Error(`${label} must be a safe integer.`);
48
+ }
49
+ return value;
50
+ };
51
+ const boundsFor = (recipe, label, maximumPolicyDepth) => ({
52
+ maximumBytes: positiveInteger(recipe.maximumBytes ?? DEFAULT_MAXIMUM_BYTES, `${label}.recipe.maximumBytes`, 16_777_216),
53
+ maximumDepth: positiveInteger(recipe.maximumDepth ?? DEFAULT_MAXIMUM_DEPTH, `${label}.recipe.maximumDepth`, maximumPolicyDepth),
54
+ });
55
+ const classifiesUnknownJsonValue = (declaration) => {
56
+ if (["REPLACE", "EXCLUDE", "PSEUDONYMIZE"].includes(declaration.action)) {
57
+ return true;
58
+ }
59
+ if (declaration.action !== "DERIVE")
60
+ return false;
61
+ if (!isObject(declaration.recipe))
62
+ return false;
63
+ if (declaration.recipe.kind === "approved-owner") {
64
+ return (classifiesUnknownJsonValue(declaration.recipe.approved) &&
65
+ classifiesUnknownJsonValue(declaration.recipe.otherwise));
66
+ }
67
+ if (declaration.recipe.kind === "json-union")
68
+ return true;
69
+ if (declaration.recipe.kind === "json-array") {
70
+ return classifiesUnknownJsonValue(declaration.recipe.items);
71
+ }
72
+ if (declaration.recipe.kind === "json-object") {
73
+ return Object.values(declaration.recipe.fields).every(classifiesUnknownJsonValue);
74
+ }
75
+ if (declaration.recipe.kind === "json-dictionary") {
76
+ return classifiesUnknownJsonValue(declaration.recipe.values);
77
+ }
78
+ return [
79
+ "date-shift",
80
+ "enum",
81
+ "validated-string",
82
+ "path-map",
83
+ "binding-substitute",
84
+ ].includes(String(declaration.recipe.kind));
85
+ };
86
+ const classifiesContainer = (declaration, expectedType) => {
87
+ if (["REPLACE", "EXCLUDE", "PSEUDONYMIZE"].includes(declaration.action)) {
88
+ return true;
89
+ }
90
+ if (declaration.action !== "DERIVE")
91
+ return false;
92
+ if (!isObject(declaration.recipe))
93
+ return false;
94
+ if (declaration.recipe.kind === "approved-owner") {
95
+ return (classifiesContainer(declaration.recipe.approved, expectedType) &&
96
+ classifiesContainer(declaration.recipe.otherwise, expectedType));
97
+ }
98
+ if (declaration.recipe.kind === "json-union")
99
+ return true;
100
+ return expectedType === "array"
101
+ ? declaration.recipe.kind === "json-array" &&
102
+ classifiesUnknownJsonValue(declaration.recipe.items)
103
+ : declaration.recipe.kind === "json-object"
104
+ ? Object.values(declaration.recipe.fields).every(classifiesUnknownJsonValue)
105
+ : declaration.recipe.kind === "json-dictionary" &&
106
+ classifiesUnknownJsonValue(declaration.recipe.values);
107
+ };
108
+ export const validateStructuredJsonRecipe = ({ recipe, label, depth, maximumPolicyDepth, validateDeclaration, validatePseudonymRecipe, }) => {
109
+ const bounds = boundsFor(recipe, label, maximumPolicyDepth);
110
+ if (recipe.allowNull !== undefined && typeof recipe.allowNull !== "boolean") {
111
+ throw new Error(`${label}.recipe.allowNull must be true or false.`);
112
+ }
113
+ if (recipe.kind === "json-array") {
114
+ knownKeys(recipe, [
115
+ "kind",
116
+ "items",
117
+ "allowNull",
118
+ "maximumBytes",
119
+ "maximumDepth",
120
+ "maximumItems",
121
+ ], `${label}.recipe`);
122
+ return Object.freeze({
123
+ kind: "json-array",
124
+ allowNull: recipe.allowNull === true,
125
+ ...bounds,
126
+ maximumItems: positiveInteger(recipe.maximumItems, `${label}.recipe.maximumItems`, 10_000),
127
+ items: validateDeclaration(recipe.items, `${label}.recipe.items`, {
128
+ depth,
129
+ }),
130
+ });
131
+ }
132
+ if (recipe.kind === "json-union") {
133
+ knownKeys(recipe, [
134
+ "kind",
135
+ "variants",
136
+ "allowNull",
137
+ "maximumBytes",
138
+ "maximumDepth",
139
+ "maximumItems",
140
+ ], `${label}.recipe`);
141
+ if (!isObject(recipe.variants) ||
142
+ Object.keys(recipe.variants).length === 0) {
143
+ throw new Error(`${label}.recipe.variants must classify JSON value types.`);
144
+ }
145
+ const variants = Object.fromEntries(Object.entries(recipe.variants).map(([type, declaration]) => {
146
+ if (!JSON_UNION_VARIANTS.has(type)) {
147
+ throw new Error(`${label}.recipe.variants.${type} is unsupported.`);
148
+ }
149
+ const normalized = validateDeclaration(declaration, `${label}.recipe.variants.${type}`, { depth });
150
+ if ((type === "array" || type === "object") &&
151
+ !classifiesContainer(normalized, type)) {
152
+ throw new Error(`${label}.recipe.variants.${type} must classify container contents instead of retaining the whole value.`);
153
+ }
154
+ return [type, normalized];
155
+ }));
156
+ return Object.freeze({
157
+ kind: "json-union",
158
+ allowNull: recipe.allowNull === true,
159
+ ...bounds,
160
+ maximumItems: positiveInteger(recipe.maximumItems, `${label}.recipe.maximumItems`, 10_000),
161
+ variants: Object.freeze(variants),
162
+ });
163
+ }
164
+ if (recipe.kind === "json-dictionary") {
165
+ knownKeys(recipe, [
166
+ "kind",
167
+ "keys",
168
+ "values",
169
+ "allowNull",
170
+ "maximumBytes",
171
+ "maximumDepth",
172
+ "maximumItems",
173
+ ], `${label}.recipe`);
174
+ if (!isObject(recipe.keys)) {
175
+ throw new Error(`${label}.recipe.keys must be an object.`);
176
+ }
177
+ knownKeys(recipe.keys, ["format", "action", "minimum", "maximum", "maximumLength", "recipe"], `${label}.recipe.keys`);
178
+ if (typeof recipe.keys.format !== "string" ||
179
+ !DICTIONARY_KEY_FORMATS.has(recipe.keys.format)) {
180
+ throw new Error(`${label}.recipe.keys.format is unsupported.`);
181
+ }
182
+ if (typeof recipe.keys.action !== "string" ||
183
+ !["KEEP", "PSEUDONYMIZE"].includes(recipe.keys.action)) {
184
+ throw new Error(`${label}.recipe.keys.action must be KEEP or PSEUDONYMIZE.`);
185
+ }
186
+ const keyFormat = recipe.keys.format;
187
+ const keyAction = recipe.keys.action;
188
+ const maximumLength = keyFormat === "text"
189
+ ? positiveInteger(recipe.keys.maximumLength, `${label}.recipe.keys.maximumLength`, 1_024)
190
+ : undefined;
191
+ const integerRange = keyFormat === "integer"
192
+ ? {
193
+ minimum: safeInteger(recipe.keys.minimum, `${label}.recipe.keys.minimum`),
194
+ maximum: safeInteger(recipe.keys.maximum, `${label}.recipe.keys.maximum`),
195
+ }
196
+ : null;
197
+ if (integerRange !== null && integerRange.minimum > integerRange.maximum) {
198
+ throw new Error(`${label}.recipe.keys.minimum must not exceed maximum.`);
199
+ }
200
+ if (keyFormat !== "integer" &&
201
+ (recipe.keys.minimum !== undefined || recipe.keys.maximum !== undefined)) {
202
+ throw new Error(`${label}.recipe.keys.minimum and maximum are supported only for integer keys.`);
203
+ }
204
+ if (keyFormat !== "text" && recipe.keys.maximumLength !== undefined) {
205
+ throw new Error(`${label}.recipe.keys.maximumLength is supported only for text keys.`);
206
+ }
207
+ const keyRecipe = keyAction === "PSEUDONYMIZE"
208
+ ? validatePseudonymRecipe(recipe.keys.recipe, `${label}.recipe.keys`, depth + 1)
209
+ : null;
210
+ if (keyAction === "KEEP" && recipe.keys.recipe !== undefined) {
211
+ throw new Error(`${label}.recipe.keys.recipe is not allowed when keys are kept.`);
212
+ }
213
+ const values = validateDeclaration(recipe.values, `${label}.recipe.values`, { depth });
214
+ if (!classifiesUnknownJsonValue(values)) {
215
+ throw new Error(`${label}.recipe.values must classify value types instead of retaining unknown values.`);
216
+ }
217
+ return Object.freeze({
218
+ kind: "json-dictionary",
219
+ allowNull: recipe.allowNull === true,
220
+ ...bounds,
221
+ maximumItems: positiveInteger(recipe.maximumItems, `${label}.recipe.maximumItems`, 10_000),
222
+ keys: Object.freeze({
223
+ format: keyFormat,
224
+ action: keyAction,
225
+ ...(integerRange === null ? {} : integerRange),
226
+ ...(maximumLength === undefined ? {} : { maximumLength }),
227
+ ...(keyRecipe === null ? {} : { recipe: keyRecipe }),
228
+ }),
229
+ values,
230
+ });
231
+ }
232
+ if (!isObject(recipe.fields) || Object.keys(recipe.fields).length === 0) {
233
+ throw new Error(`${label}.recipe.fields must classify JSON object keys.`);
234
+ }
235
+ knownKeys(recipe, ["kind", "fields", "allowNull", "maximumBytes", "maximumDepth"], `${label}.recipe`);
236
+ const fields = Object.fromEntries(Object.entries(recipe.fields).map(([key, declaration]) => {
237
+ if (!key) {
238
+ throw new Error(`${label}.recipe.fields contains an invalid key.`);
239
+ }
240
+ return [
241
+ key,
242
+ validateDeclaration(declaration, `${label}.recipe.fields.${key}`, {
243
+ allowRequired: true,
244
+ depth,
245
+ }),
246
+ ];
247
+ }));
248
+ return Object.freeze({
249
+ kind: "json-object",
250
+ allowNull: recipe.allowNull === true,
251
+ ...bounds,
252
+ fields: Object.freeze(fields),
253
+ });
254
+ };
255
+ export const structuredJsonDeclarations = (recipe) => {
256
+ if (!isObject(recipe))
257
+ return [];
258
+ if (recipe.kind === "json-object" && isObject(recipe.fields)) {
259
+ return Object.values(recipe.fields);
260
+ }
261
+ if (recipe.kind === "json-array")
262
+ return [recipe.items];
263
+ if (recipe.kind === "json-union" && isObject(recipe.variants)) {
264
+ return Object.values(recipe.variants);
265
+ }
266
+ if (recipe.kind === "json-dictionary")
267
+ return [recipe.values];
268
+ return [];
269
+ };
270
+ const assertJsonBounds = (value, recipe, path) => {
271
+ let bytes = 0;
272
+ const jsonStringBytes = (string) => {
273
+ let size = 2;
274
+ for (let index = 0; index < string.length; index += 1) {
275
+ const code = string.charCodeAt(index);
276
+ if (code === 34 || code === 92)
277
+ size += 2;
278
+ else if (code < 32)
279
+ size += [8, 9, 10, 12, 13].includes(code) ? 2 : 6;
280
+ else if (code >= 0xd800 && code <= 0xdbff) {
281
+ const next = string.charCodeAt(index + 1);
282
+ if (next >= 0xdc00 && next <= 0xdfff) {
283
+ size += 4;
284
+ index += 1;
285
+ }
286
+ else
287
+ size += 6;
288
+ }
289
+ else if (code >= 0xdc00 && code <= 0xdfff)
290
+ size += 6;
291
+ else if (code <= 0x7f)
292
+ size += 1;
293
+ else if (code <= 0x7ff)
294
+ size += 2;
295
+ else
296
+ size += 3;
297
+ }
298
+ return size;
299
+ };
300
+ const visit = (current, depth) => {
301
+ if (depth > recipe.maximumDepth) {
302
+ throw new Error(`${path} exceeds maximumDepth ${recipe.maximumDepth}.`);
303
+ }
304
+ if (typeof current === "string")
305
+ bytes += jsonStringBytes(current);
306
+ else if (current === null)
307
+ bytes += 4;
308
+ else if (typeof current === "number" && !Number.isFinite(current)) {
309
+ throw new Error(`${path} contains an unsupported JSON value.`);
310
+ }
311
+ else if (["number", "boolean"].includes(typeof current)) {
312
+ bytes += String(current).length;
313
+ }
314
+ else if (Array.isArray(current)) {
315
+ if (recipe.kind === "json-array" &&
316
+ current.length > recipe.maximumItems) {
317
+ throw new Error(`${path} exceeds maximumItems ${recipe.maximumItems}.`);
318
+ }
319
+ bytes += 2 + Math.max(0, current.length - 1);
320
+ for (const item of current)
321
+ visit(item, depth + 1);
322
+ }
323
+ else if (isObject(current)) {
324
+ bytes += 2 + Math.max(0, Object.keys(current).length - 1);
325
+ for (const [key, nested] of Object.entries(current)) {
326
+ bytes += jsonStringBytes(key) + 1;
327
+ visit(nested, depth + 1);
328
+ }
329
+ }
330
+ else
331
+ throw new Error(`${path} contains an unsupported JSON value.`);
332
+ if (bytes > recipe.maximumBytes) {
333
+ throw new Error(`${path} exceeds maximumBytes ${recipe.maximumBytes}.`);
334
+ }
335
+ };
336
+ visit(value, 1);
337
+ };
338
+ const jsonValueType = (value) => {
339
+ if (Array.isArray(value))
340
+ return "array";
341
+ if (isObject(value))
342
+ return "object";
343
+ if (typeof value === "string")
344
+ return "string";
345
+ if (typeof value === "number")
346
+ return "number";
347
+ if (typeof value === "boolean")
348
+ return "boolean";
349
+ return null;
350
+ };
351
+ const validateDictionaryKey = (value, rule, path) => {
352
+ if (rule.format === "integer") {
353
+ if (!/^(?:0|-?[1-9][0-9]{0,15})$/u.test(value)) {
354
+ throw new Error(`${path} contains an invalid integer key.`);
355
+ }
356
+ const integer = Number(value);
357
+ if (rule.minimum === undefined || rule.maximum === undefined) {
358
+ throw new Error(`${path} has no reviewed integer-key range.`);
359
+ }
360
+ if (!Number.isSafeInteger(integer) ||
361
+ String(integer) !== value ||
362
+ integer < rule.minimum ||
363
+ integer > rule.maximum) {
364
+ throw new Error(`${path} contains an integer key outside the reviewed range ${rule.minimum} through ${rule.maximum}.`);
365
+ }
366
+ return integer;
367
+ }
368
+ else if (rule.format === "positive-integer") {
369
+ if (!/^[1-9][0-9]{0,18}$/u.test(value)) {
370
+ throw new Error(`${path} contains an invalid positive-integer key.`);
371
+ }
372
+ }
373
+ else if (rule.format === "uuid") {
374
+ if (!UUID.test(value)) {
375
+ throw new Error(`${path} contains an invalid uuid key.`);
376
+ }
377
+ }
378
+ else if (rule.format === "identifier") {
379
+ if (!IDENTIFIER.test(value)) {
380
+ throw new Error(`${path} contains an invalid identifier key.`);
381
+ }
382
+ }
383
+ else if (!value ||
384
+ rule.maximumLength === undefined ||
385
+ value.length > rule.maximumLength ||
386
+ /[\u0000-\u001f\u007f]/u.test(value)) {
387
+ throw new Error(`${path} contains an invalid bounded text key.`);
388
+ }
389
+ return value;
390
+ };
391
+ export const executeStructuredJsonDeclaration = ({ declaration, value, key, path, execute, pseudonymize, }) => {
392
+ if (!isObject(declaration.recipe)) {
393
+ throw new Error(`${path} uses an unsupported structured JSON recipe.`);
394
+ }
395
+ const recipe = declaration.recipe;
396
+ if (value === null && recipe.allowNull)
397
+ return null;
398
+ assertJsonBounds(value, recipe, path);
399
+ if (recipe.kind === "json-union") {
400
+ const type = jsonValueType(value);
401
+ if (type === null) {
402
+ throw new Error(`${path} contains an unsupported JSON value.`);
403
+ }
404
+ if ((Array.isArray(value) || isObject(value)) &&
405
+ Object.keys(value).length > recipe.maximumItems) {
406
+ throw new Error(`${path} exceeds maximumItems ${recipe.maximumItems}.`);
407
+ }
408
+ const variant = recipe.variants[type];
409
+ if (!variant) {
410
+ throw new Error(`${path} has an unreviewed JSON ${type} variant.`);
411
+ }
412
+ return execute(variant, value, path);
413
+ }
414
+ if (recipe.kind === "json-array") {
415
+ if (!Array.isArray(value))
416
+ throw new Error(`${path} must be a JSON array.`);
417
+ return value.flatMap((item, index) => {
418
+ const transformed = execute(recipe.items, item, `${path}[${index}]`);
419
+ return transformed === undefined ? [] : [transformed];
420
+ });
421
+ }
422
+ if (recipe.kind === "json-dictionary") {
423
+ if (!isObject(value)) {
424
+ throw new Error(`${path} must be a JSON dictionary object.`);
425
+ }
426
+ const entries = Object.entries(value);
427
+ if (entries.length > recipe.maximumItems) {
428
+ throw new Error(`${path} exceeds maximumItems ${recipe.maximumItems}.`);
429
+ }
430
+ const transformed = [];
431
+ const transformedKeys = new Set();
432
+ for (const [sourceKey, nestedValue] of entries) {
433
+ const canonicalSourceKey = validateDictionaryKey(sourceKey, recipe.keys, path);
434
+ const destinationKey = recipe.keys.action === "KEEP"
435
+ ? sourceKey
436
+ : String(pseudonymize(key, recipe.keys.recipe, canonicalSourceKey));
437
+ if (transformedKeys.has(destinationKey)) {
438
+ throw new Error(`${path} produced a duplicate transformed JSON key.`);
439
+ }
440
+ transformedKeys.add(destinationKey);
441
+ const nested = execute(recipe.values, nestedValue, `${path}.${sourceKey}`);
442
+ if (nested !== undefined)
443
+ transformed.push([destinationKey, nested]);
444
+ }
445
+ return Object.fromEntries(transformed);
446
+ }
447
+ if (!isObject(value))
448
+ throw new Error(`${path} must be a JSON object.`);
449
+ const actual = Object.keys(value).sort();
450
+ const expected = Object.keys(recipe.fields);
451
+ if (actual.some((field) => !expected.includes(field))) {
452
+ throw new Error(`${path} contains an unclassified JSON key.`);
453
+ }
454
+ const fields = recipe.fields;
455
+ const missing = expected.filter((field) => fields[field].required && !Object.hasOwn(value, field));
456
+ if (missing.length)
457
+ throw new Error(`${path} is missing required JSON keys.`);
458
+ return Object.fromEntries(Object.entries(fields).flatMap(([field, nested]) => {
459
+ if (!Object.hasOwn(value, field))
460
+ return [];
461
+ const transformed = execute(nested, value[field], `${path}.${field}`);
462
+ return transformed === undefined ? [] : [[field, transformed]];
463
+ }));
464
+ };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Generic, fail-closed sanitization policy validation and value operations.
3
+ * Projects own the policy, schema inventory, pseudonym keys, and derivation logic.
4
+ */
5
+ import { type ExecutablePrivacyPolicy } from "./privacy_engine.mjs";
6
+ export declare const SANITIZATION_ACTIONS: Readonly<{
7
+ KEEP: "KEEP";
8
+ PSEUDONYMIZE: "PSEUDONYMIZE";
9
+ REPLACE: "REPLACE";
10
+ EXCLUDE: "EXCLUDE";
11
+ DERIVE: "DERIVE";
12
+ }>;
13
+ export declare const EXCLUDED_VALUE: unique symbol;
14
+ export type SanitizationAction = (typeof SANITIZATION_ACTIONS)[keyof typeof SANITIZATION_ACTIONS];
15
+ export interface LegacySanitizationPolicyColumn extends PolicyObject {
16
+ readonly name: string;
17
+ readonly action: SanitizationAction;
18
+ readonly generated: "ALWAYS" | "NEVER";
19
+ readonly identity: "YES" | "NO";
20
+ readonly foreignKey: Readonly<{
21
+ schema: string;
22
+ table: string;
23
+ column: string;
24
+ }> | null;
25
+ }
26
+ export interface LegacySanitizationPolicyTable {
27
+ readonly schema?: string;
28
+ readonly name: string;
29
+ readonly sourceRows: "STREAM AND SANITIZE" | "EXCLUDE";
30
+ readonly columns: readonly LegacySanitizationPolicyColumn[];
31
+ }
32
+ export interface LegacySanitizationPolicy {
33
+ readonly policyVersion: 1;
34
+ readonly migrationCutoff: string;
35
+ readonly tables: readonly LegacySanitizationPolicyTable[];
36
+ }
37
+ export type RuntimeSanitizationPolicy = LegacySanitizationPolicy | ExecutablePrivacyPolicy;
38
+ type PolicyObject = Record<string, unknown>;
39
+ type SanitizationCallback = (value: unknown, context: Record<string, unknown>) => unknown;
40
+ export declare const normalizeSanitizationAction: (action: unknown) => SanitizationAction;
41
+ export declare const validateRuntimeSanitizationPolicy: (policy: unknown) => RuntimeSanitizationPolicy;
42
+ export declare const readBoundRuntimeSanitizationPolicy: ({ bytes, expectedSha256, }: {
43
+ bytes: Uint8Array | string;
44
+ expectedSha256: string;
45
+ }) => RuntimeSanitizationPolicy;
46
+ export declare const validateSanitizationCoverage: ({ policy, schemaTables, }: {
47
+ policy: unknown;
48
+ schemaTables: unknown;
49
+ }) => Readonly<{
50
+ policyVersion: unknown;
51
+ tableCount: number;
52
+ columnCount: number;
53
+ tables: readonly {
54
+ schema: string;
55
+ columns: {
56
+ name: string;
57
+ action: SanitizationAction;
58
+ }[];
59
+ }[];
60
+ }>;
61
+ export declare const applySanitizationAction: ({ action, value, replace, pseudonymize, derive, context, }: {
62
+ action: unknown;
63
+ value: unknown;
64
+ replace?: SanitizationCallback;
65
+ pseudonymize?: SanitizationCallback;
66
+ derive?: SanitizationCallback;
67
+ context?: Record<string, unknown>;
68
+ }) => unknown | typeof EXCLUDED_VALUE;
69
+ export {};