@rehearsal-db/core 0.1.0-beta.13 → 0.1.0-beta.14
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 +76 -0
- package/README.md +3 -1
- package/docs/configuration.md +40 -5
- package/docs/getting-started.md +28 -0
- package/docs/production-source.md +43 -7
- package/docs/releasing.md +5 -3
- package/docs/roadmap.md +3 -2
- package/docs/sanitization.md +415 -14
- package/docs/standalone-workflow.md +12 -6
- package/docs/troubleshooting.md +5 -1
- package/docs/tutorial.md +5 -0
- package/package.json +1 -1
- package/scripts/install/create_root_config.mjs +5 -22
- package/src/baseline/privacy_engine.mjs +660 -213
- package/src/baseline/privacy_structured_json.mjs +554 -0
- package/src/cli/rehearsal.mjs +3 -1
- package/src/cli/renderers.mjs +12 -0
- package/src/cli/source_commands.mjs +11 -3
- package/src/identity/claim.mjs +23 -21
- package/src/project/install_scaffold.mjs +35 -1
- package/src/project/setup.mjs +9 -1
- package/src/source/access.mjs +50 -5
- package/src/source/asset_transfer.mjs +139 -87
- package/src/source/external_reader.mjs +136 -23
- package/src/source/postgresql_access.mjs +80 -23
package/docs/sanitization.md
CHANGED
|
@@ -39,11 +39,13 @@ Two policy versions exist:
|
|
|
39
39
|
- Version 2 is required by `baseline refresh`. It adds bounded declarative recipes so
|
|
40
40
|
Rehearsal can sanitize without project callbacks.
|
|
41
41
|
|
|
42
|
-
Version 2 supports keyed UUID, email, text,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
Version 2 supports keyed UUID, email, text, integer, fixed-width hexadecimal, and GTIN
|
|
43
|
+
pseudonyms; digests derived from named sanitized columns; explicit constant replacement;
|
|
44
|
+
bounded and grouped date shifting; identity-aware path mapping;
|
|
45
|
+
approved-owner conditionals; exhaustively classified JSON objects; bounded JSON arrays;
|
|
46
|
+
reviewed JSON type unions; and runtime-keyed JSON dictionaries. Unknown tables, columns,
|
|
47
|
+
nested keys, formats, recipes, variants, or missing required fields fail closed. It never
|
|
48
|
+
evaluates JavaScript, regular expressions, or SQL from the policy.
|
|
47
49
|
|
|
48
50
|
## Approved-owner values
|
|
49
51
|
|
|
@@ -100,16 +102,42 @@ node -e 'const {createHash}=require("node:crypto"); process.stdout.write(createH
|
|
|
100
102
|
```
|
|
101
103
|
|
|
102
104
|
Every conditional table names its owner column. Missing bindings, hash mismatches,
|
|
103
|
-
missing owner columns,
|
|
105
|
+
missing owner columns, unsupported owner values, and conditionals without an owner
|
|
104
106
|
binding are refused. The owner column must use `PSEUDONYMIZE`, preventing a conditional
|
|
105
|
-
policy from retaining raw account IDs.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
policy from retaining raw account IDs.
|
|
108
|
+
|
|
109
|
+
When ownership may come through several relationships, replace `column` with an explicit
|
|
110
|
+
bounded any-of declaration:
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"ownerBinding": {
|
|
115
|
+
"binding": "approved-owner",
|
|
116
|
+
"columns": ["target_user_id", "actor_user_id"],
|
|
117
|
+
"match": "any",
|
|
118
|
+
"nullBehavior": "otherwise"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Every listed column must exist and be pseudonymized. One matching value selects the
|
|
124
|
+
`approved` branch. `nullBehavior: "otherwise"` explicitly treats a null value as a
|
|
125
|
+
non-match; it never grants owner access. Without that declaration, encountering null is
|
|
126
|
+
an error. Empty strings, non-finite numbers (`NaN`, positive infinity, or negative
|
|
127
|
+
infinity), objects, arrays, booleans, missing columns, duplicate declarations, and
|
|
128
|
+
unsupported matching modes remain errors. Rehearsal validates every declared owner
|
|
129
|
+
column before selecting either branch, even when another owner column matches. A
|
|
130
|
+
single-column binding may also declare `nullBehavior: "otherwise"` when that column is
|
|
131
|
+
reviewed as nullable.
|
|
132
|
+
|
|
133
|
+
`identity` describes PostgreSQL's generated SQL identity metadata, not whether a value
|
|
134
|
+
identifies a person: ordinary UUID primary keys use `"NO"`, while a genuine
|
|
135
|
+
`GENERATED ... AS IDENTITY` column uses `"YES"`. Rehearsal checks the restored PostgreSQL
|
|
136
|
+
schema for a real backing sequence before attempting a sequence reset, so UUID identity
|
|
137
|
+
values never receive numeric sequence operations. Other owners take the declared
|
|
138
|
+
`otherwise` path; every other identity-bearing field still needs its own pseudonymization
|
|
139
|
+
declaration. Supply the binding only to the local server-side Rehearsal process; do not
|
|
140
|
+
put it in the policy, baseline, or tracked files.
|
|
113
141
|
|
|
114
142
|
## Identity-aware Storage paths
|
|
115
143
|
|
|
@@ -173,6 +201,180 @@ offset. This preserves their order and duration:
|
|
|
173
201
|
Apply the same recipe to `ends_at`. Rehearsal refuses a group that declares conflicting
|
|
174
202
|
`days` values. Leave out `group` when a date should shift independently.
|
|
175
203
|
|
|
204
|
+
`date-shift` returns an ISO string by default, preserving the existing behavior. When
|
|
205
|
+
the reviewed application model stores a date as numeric epoch milliseconds, declare
|
|
206
|
+
that representation explicitly:
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{
|
|
210
|
+
"action": "DERIVE",
|
|
211
|
+
"recipe": {
|
|
212
|
+
"kind": "date-shift",
|
|
213
|
+
"days": 30,
|
|
214
|
+
"group": "saved-recipe-window",
|
|
215
|
+
"representation": "epoch-milliseconds"
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
This form accepts only finite safe integers that JavaScript can represent as dates and
|
|
221
|
+
returns a numeric epoch-millisecond value. It preserves `null`, refuses strings,
|
|
222
|
+
fractions, non-finite numbers, unsafe integers and values outside the supported date
|
|
223
|
+
range. ISO-string and epoch-millisecond declarations may share a group; they receive the
|
|
224
|
+
same stable offset while each retains its declared output representation. The only
|
|
225
|
+
supported representation names are `iso-string` and `epoch-milliseconds`.
|
|
226
|
+
|
|
227
|
+
## Constraint-preserving identifiers and fingerprints
|
|
228
|
+
|
|
229
|
+
Use a keyed `hex` pseudonym when one source value must remain a lowercase 32- or
|
|
230
|
+
64-character hexadecimal value:
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
{
|
|
234
|
+
"name": "source_fingerprint",
|
|
235
|
+
"action": "PSEUDONYMIZE",
|
|
236
|
+
"recipe": {
|
|
237
|
+
"format": "hex",
|
|
238
|
+
"namespace": "source-fingerprint",
|
|
239
|
+
"length": 64
|
|
240
|
+
},
|
|
241
|
+
"generated": "NEVER",
|
|
242
|
+
"identity": "NO",
|
|
243
|
+
"foreignKey": null
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Use a `digest` derivation when the fingerprint must describe other sanitized fields.
|
|
248
|
+
Inputs are column names, evaluated in the listed order after their own privacy recipes.
|
|
249
|
+
The existing raw fingerprint is ignored:
|
|
250
|
+
|
|
251
|
+
```json
|
|
252
|
+
{
|
|
253
|
+
"name": "evidence_fingerprint",
|
|
254
|
+
"action": "DERIVE",
|
|
255
|
+
"recipe": {
|
|
256
|
+
"kind": "digest",
|
|
257
|
+
"format": "hex",
|
|
258
|
+
"namespace": "evidence-fingerprint",
|
|
259
|
+
"length": 32,
|
|
260
|
+
"inputs": ["sanitized_owner_id", "evidence_state"]
|
|
261
|
+
},
|
|
262
|
+
"generated": "NEVER",
|
|
263
|
+
"identity": "NO",
|
|
264
|
+
"foreignKey": null
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Digest inputs must exist, must not be excluded, and cannot form a dependency cycle.
|
|
269
|
+
Rehearsal supports only 32- and 64-character hexadecimal output. The namespace and local
|
|
270
|
+
privacy key separate otherwise identical declarations.
|
|
271
|
+
|
|
272
|
+
Use the `gtin` pseudonym format for a normalized GTIN whose length and check digit must
|
|
273
|
+
remain valid:
|
|
274
|
+
|
|
275
|
+
```json
|
|
276
|
+
{
|
|
277
|
+
"name": "barcode",
|
|
278
|
+
"action": "PSEUDONYMIZE",
|
|
279
|
+
"recipe": { "format": "gtin", "namespace": "product-barcode", "length": 14 },
|
|
280
|
+
"generated": "NEVER",
|
|
281
|
+
"identity": "NO",
|
|
282
|
+
"foreignKey": null
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Supported GTIN lengths are 8, 12, 13, and 14. Non-null input must be a normalized string
|
|
287
|
+
of exactly that length with a valid check digit; malformed input stops preparation. Null
|
|
288
|
+
stays null. Rehearsal creates a deterministic keyed payload and recalculates the check
|
|
289
|
+
digit, so related fields using the same namespace, length, source value, and key remain
|
|
290
|
+
aligned.
|
|
291
|
+
|
|
292
|
+
When one reviewed field legitimately contains more than one GTIN length, replace
|
|
293
|
+
`"length": 14` with an explicit allowlist such as `"allowedLengths": [12, 14]`. The two
|
|
294
|
+
keys are mutually exclusive. Each source value must match one allowed length, and its
|
|
295
|
+
result retains that exact length. Empty, duplicated, or unsupported allowlist entries
|
|
296
|
+
are refused. This also works inside structured JSON declarations; a JSON union is not
|
|
297
|
+
needed merely to distinguish GTIN string lengths.
|
|
298
|
+
|
|
299
|
+
Use the `url` pseudonym format when a sanitized field must remain an HTTPS URL:
|
|
300
|
+
|
|
301
|
+
```json
|
|
302
|
+
{
|
|
303
|
+
"name": "evidence_reference",
|
|
304
|
+
"action": "PSEUDONYMIZE",
|
|
305
|
+
"recipe": {
|
|
306
|
+
"format": "url",
|
|
307
|
+
"namespace": "evidence-url",
|
|
308
|
+
"origin": "https://127.0.0.1:58432",
|
|
309
|
+
"maxLength": 500
|
|
310
|
+
},
|
|
311
|
+
"generated": "NEVER",
|
|
312
|
+
"identity": "NO",
|
|
313
|
+
"foreignKey": null
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
`origin` is required and must be an HTTPS loopback origin using `127.0.0.1`, `::1`, or
|
|
318
|
+
`localhost`. Credentials, paths, queries, fragments, non-HTTPS schemes, and non-local
|
|
319
|
+
hosts are refused. The value must be the canonical origin itself, optionally followed by
|
|
320
|
+
one `/`; dot segments and encoded dot segments are rejected before URL normalization.
|
|
321
|
+
`maxLength` is also required, cannot exceed 2,048 characters, and must fit the complete
|
|
322
|
+
generated URL.
|
|
323
|
+
|
|
324
|
+
Non-null input must be a valid HTTPS URL without surrounding or embedded whitespace.
|
|
325
|
+
Rehearsal hashes the entire source value with the local privacy key and namespace, then
|
|
326
|
+
emits only the reviewed origin and a 32-character pseudonymous path. It never copies the
|
|
327
|
+
source host, credentials, path, query, token, or fragment. Null remains null. Equal
|
|
328
|
+
inputs using the same namespace, key, and origin correspond across ordinary columns and
|
|
329
|
+
structured JSON; different inputs and rotated keys produce different paths.
|
|
330
|
+
|
|
331
|
+
The same declaration can be used for items inside bounded `json-array` recipes, which
|
|
332
|
+
covers PostgreSQL text-array projections represented by the source pipeline. Each array
|
|
333
|
+
and object still needs its ordinary item, depth, and byte bounds.
|
|
334
|
+
|
|
335
|
+
### Retained text containing a reviewed identity
|
|
336
|
+
|
|
337
|
+
Use `binding-substitute` when permitted text may contain the exact value of a reviewed
|
|
338
|
+
UUID binding. This keeps the surrounding text but replaces every occurrence of that UUID
|
|
339
|
+
with the same keyed pseudonym used by a `uuid` declaration sharing its namespace:
|
|
340
|
+
|
|
341
|
+
```json
|
|
342
|
+
{
|
|
343
|
+
"action": "DERIVE",
|
|
344
|
+
"recipe": {
|
|
345
|
+
"kind": "approved-owner",
|
|
346
|
+
"approved": {
|
|
347
|
+
"action": "DERIVE",
|
|
348
|
+
"recipe": {
|
|
349
|
+
"kind": "binding-substitute",
|
|
350
|
+
"binding": "approved-owner",
|
|
351
|
+
"format": "uuid",
|
|
352
|
+
"namespace": "account-id",
|
|
353
|
+
"maximumBytes": 256
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
"otherwise": {
|
|
357
|
+
"action": "PSEUDONYMIZE",
|
|
358
|
+
"recipe": {
|
|
359
|
+
"format": "text",
|
|
360
|
+
"namespace": "food-description",
|
|
361
|
+
"maxLength": 128
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
The named binding must already exist in `policy.bindings`, and its environment value must
|
|
369
|
+
match the tracked SHA-256 review receipt. For this recipe the reviewed value must be a
|
|
370
|
+
UUID. `maximumBytes` is required and applies before and after replacement. Null remains
|
|
371
|
+
null; other input must be text. Ordinary text is unchanged, literal UUID occurrences are
|
|
372
|
+
all replaced, and the raw identity is never stored in policy.
|
|
373
|
+
|
|
374
|
+
This is intentionally not a general search-and-replace facility: there are no regular
|
|
375
|
+
expressions, arbitrary patterns, replacement strings, or callbacks. Pair it with an
|
|
376
|
+
`approved-owner` branch when only the reviewed owner's surrounding text may be retained.
|
|
377
|
+
|
|
176
378
|
## Structured JSON
|
|
177
379
|
|
|
178
380
|
`json-object` fields are required by default. Set `"required": false` on an explicitly
|
|
@@ -211,6 +413,205 @@ the same declarations, and every nested identity uses an explicit pseudonym reci
|
|
|
211
413
|
}
|
|
212
414
|
```
|
|
213
415
|
|
|
416
|
+
### JSON values with several reviewed types
|
|
417
|
+
|
|
418
|
+
Use `json-union` when one JSON column legitimately stores more than one root type. Every
|
|
419
|
+
accepted type has its own declaration. Null is rejected unless `allowNull` is explicitly
|
|
420
|
+
true. Arrays and objects cannot use whole-value `KEEP`; their contents must be classified
|
|
421
|
+
through `json-array`, `json-object`, `json-dictionary`, another bounded union, or a safe
|
|
422
|
+
replacement or pseudonymization.
|
|
423
|
+
|
|
424
|
+
```json
|
|
425
|
+
{
|
|
426
|
+
"action": "DERIVE",
|
|
427
|
+
"recipe": {
|
|
428
|
+
"kind": "json-union",
|
|
429
|
+
"allowNull": true,
|
|
430
|
+
"maximumItems": 25,
|
|
431
|
+
"maximumDepth": 5,
|
|
432
|
+
"maximumBytes": 8192,
|
|
433
|
+
"variants": {
|
|
434
|
+
"string": { "action": "KEEP" },
|
|
435
|
+
"number": { "action": "KEEP" },
|
|
436
|
+
"array": {
|
|
437
|
+
"action": "DERIVE",
|
|
438
|
+
"recipe": {
|
|
439
|
+
"kind": "json-array",
|
|
440
|
+
"maximumItems": 25,
|
|
441
|
+
"items": {
|
|
442
|
+
"action": "REPLACE",
|
|
443
|
+
"recipe": { "kind": "constant", "value": "Synthetic item" }
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
"object": {
|
|
448
|
+
"action": "DERIVE",
|
|
449
|
+
"recipe": {
|
|
450
|
+
"kind": "json-object",
|
|
451
|
+
"fields": {
|
|
452
|
+
"status": { "action": "KEEP" },
|
|
453
|
+
"note": {
|
|
454
|
+
"action": "REPLACE",
|
|
455
|
+
"required": false,
|
|
456
|
+
"recipe": { "kind": "constant", "value": "Synthetic note" }
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Supported variant names are `string`, `number`, `boolean`, `array`, and `object`.
|
|
467
|
+
Undeclared types, non-finite numbers, excessive bytes, depth or items, and unclassified
|
|
468
|
+
object fields fail closed.
|
|
469
|
+
|
|
470
|
+
### Reviewed scalar domains
|
|
471
|
+
|
|
472
|
+
Use `enum` when a structural string, number, or boolean must remain exact, but only a
|
|
473
|
+
small reviewed set is valid. For example, an image mode can retain its application
|
|
474
|
+
meaning without allowing arbitrary text through:
|
|
475
|
+
|
|
476
|
+
```json
|
|
477
|
+
{
|
|
478
|
+
"action": "DERIVE",
|
|
479
|
+
"recipe": {
|
|
480
|
+
"kind": "enum",
|
|
481
|
+
"values": ["contain", "cover", "custom"],
|
|
482
|
+
"allowNull": true
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
The recipe accepts one through 128 distinct values. Each value must be a JSON string,
|
|
488
|
+
finite number, or boolean; strings are limited to 256 UTF-8 bytes. `null` is accepted
|
|
489
|
+
only when `allowNull` is true. Runtime values match by both type and exact value, so the
|
|
490
|
+
number `1` does not authorize the string `"1"`. Rejected values are never included in
|
|
491
|
+
the error message.
|
|
492
|
+
|
|
493
|
+
An enum can be used directly or inside reviewed objects, arrays, dictionary values, and
|
|
494
|
+
`approved-owner` branches. It validates structure; it is not a place to list private
|
|
495
|
+
data, credentials, tokens, user-entered prose, or identifiers.
|
|
496
|
+
|
|
497
|
+
### Open structural codes
|
|
498
|
+
|
|
499
|
+
Use `validated-string` when an open set of structural codes must remain exact and an
|
|
500
|
+
enum would incorrectly freeze the domain. The built-in `portable-code` format accepts
|
|
501
|
+
case-preserving ASCII letters, digits, `_`, and `-`, starting with a letter or digit:
|
|
502
|
+
|
|
503
|
+
```json
|
|
504
|
+
{
|
|
505
|
+
"action": "DERIVE",
|
|
506
|
+
"recipe": {
|
|
507
|
+
"kind": "validated-string",
|
|
508
|
+
"format": "portable-code",
|
|
509
|
+
"maximumBytes": 64,
|
|
510
|
+
"allowNull": true
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
`maximumBytes` is required and may be 1 through 1,024. Dots, whitespace, slashes,
|
|
516
|
+
colons, control characters, non-ASCII text, wrong JSON types, and over-limit strings
|
|
517
|
+
are rejected without printing the input. This preserves codes such as `PROCNT`, `203`,
|
|
518
|
+
or `energy-kcal_100g`, while refusing JWT-shaped strings. Arbitrary regular expressions
|
|
519
|
+
are not supported.
|
|
520
|
+
|
|
521
|
+
Like enums, validated strings compose inside reviewed objects, arrays, dictionary
|
|
522
|
+
values, and `approved-owner` branches. Use them only for documented structural code
|
|
523
|
+
domains—not prose, URLs, paths, credentials, secrets, personal data, or identifiers
|
|
524
|
+
whose legitimate alphabet requires other characters.
|
|
525
|
+
|
|
526
|
+
### JSON dictionaries with runtime keys
|
|
527
|
+
|
|
528
|
+
Use `json-dictionary` for a reviewed object whose keys come from runtime data. This is
|
|
529
|
+
different from `json-object`, where every field name is known in advance. A dictionary
|
|
530
|
+
requires a key format, an explicit key action, one value declaration, and item, depth and
|
|
531
|
+
byte bounds:
|
|
532
|
+
|
|
533
|
+
```json
|
|
534
|
+
{
|
|
535
|
+
"action": "DERIVE",
|
|
536
|
+
"recipe": {
|
|
537
|
+
"kind": "json-dictionary",
|
|
538
|
+
"maximumItems": 100,
|
|
539
|
+
"maximumDepth": 3,
|
|
540
|
+
"maximumBytes": 8192,
|
|
541
|
+
"keys": { "format": "positive-integer", "action": "KEEP" },
|
|
542
|
+
"values": {
|
|
543
|
+
"action": "DERIVE",
|
|
544
|
+
"recipe": {
|
|
545
|
+
"kind": "json-union",
|
|
546
|
+
"maximumItems": 1,
|
|
547
|
+
"variants": { "number": { "action": "KEEP" } }
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
Key formats are `integer`, `positive-integer`, `uuid`, `identifier`, and bounded `text`.
|
|
555
|
+
Use `integer` when runtime keys may be positive, negative, or zero. It requires inclusive
|
|
556
|
+
safe-integer `minimum` and `maximum` bounds, accepts only canonical decimal keys, and
|
|
557
|
+
preserves their exact spelling:
|
|
558
|
+
|
|
559
|
+
```json
|
|
560
|
+
{
|
|
561
|
+
"keys": {
|
|
562
|
+
"format": "integer",
|
|
563
|
+
"action": "KEEP",
|
|
564
|
+
"minimum": -9007199254740991,
|
|
565
|
+
"maximum": 9007199254740991
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
The reviewed range decides whether zero is allowed. Leading zeroes, `-0`, a leading `+`,
|
|
571
|
+
fractions, exponents, unsafe integers, and unrelated text are rejected. Use
|
|
572
|
+
`positive-integer` for canonical decimal keys starting at 1. Text keys must declare
|
|
573
|
+
`maximumLength`. Every key explicitly uses either `KEEP` or `PSEUDONYMIZE`.
|
|
574
|
+
Identity-bearing keys should use `PSEUDONYMIZE` with the ordinary keyed pseudonym recipe:
|
|
575
|
+
|
|
576
|
+
```json
|
|
577
|
+
{
|
|
578
|
+
"keys": {
|
|
579
|
+
"format": "uuid",
|
|
580
|
+
"action": "PSEUDONYMIZE",
|
|
581
|
+
"recipe": { "format": "uuid", "namespace": "account-id" }
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
When a dictionary is keyed by the same numeric identity stored in a column or nested
|
|
587
|
+
numeric field, use `format: "integer"` for both pseudonym recipes and reuse the exact
|
|
588
|
+
namespace:
|
|
589
|
+
|
|
590
|
+
```json
|
|
591
|
+
{
|
|
592
|
+
"keys": {
|
|
593
|
+
"format": "integer",
|
|
594
|
+
"action": "PSEUDONYMIZE",
|
|
595
|
+
"minimum": -9007199254740991,
|
|
596
|
+
"maximum": 9007199254740991,
|
|
597
|
+
"recipe": { "format": "integer", "namespace": "external:food-id" }
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
Rehearsal validates the object key as canonical decimal text, converts it to its safe
|
|
603
|
+
integer identity, and then applies the keyed mapping. Therefore the source number `203`
|
|
604
|
+
and reviewed dictionary key `"203"` receive the same pseudonym. Positive, negative, and
|
|
605
|
+
reviewed zero identities correspond; namespace changes and privacy-key rotation still
|
|
606
|
+
produce separate mappings. This normalization applies only to explicitly typed
|
|
607
|
+
`integer` dictionary keys. It does not weaken the key syntax or range checks.
|
|
608
|
+
|
|
609
|
+
Invalid keys, duplicate transformed keys, excessive entries, unclassified surrounding
|
|
610
|
+
object fields, and values rejected by the declared value recipe stop preparation. A
|
|
611
|
+
dictionary never infers that runtime keys are safe. Its value declaration also cannot
|
|
612
|
+
retain an unknown JSON shape wholesale; use a bounded union when a scalar value should be
|
|
613
|
+
kept exactly.
|
|
614
|
+
|
|
214
615
|
The package still exports `validateSanitizationCoverage` and
|
|
215
616
|
`applySanitizationAction` for existing version 1 preparation tools:
|
|
216
617
|
|
|
@@ -52,8 +52,12 @@ The default managed workflow creates and verifies its own temporary PostgreSQL r
|
|
|
52
52
|
External mode accepts a provider-managed reader only when its complete non-system
|
|
53
53
|
readable-column surface exactly matches the reviewed export views and migration ledger.
|
|
54
54
|
It also verifies canonical view-definition fingerprints, `security_barrier`, ownership,
|
|
55
|
-
write denial,
|
|
56
|
-
|
|
55
|
+
write denial, security-definer denial, and a short database-enforced expiration. An
|
|
56
|
+
external login can declare a bounded exact list of provider-owned permission groups;
|
|
57
|
+
each must be non-login, non-privileged, directly inherited, unable to be assumed or
|
|
58
|
+
delegated, and free of nested membership. Undeclared or unsafe role membership fails as
|
|
59
|
+
an actionable source-authorization refusal. Rehearsal never claims ownership or
|
|
60
|
+
revocation of provider-managed objects.
|
|
57
61
|
|
|
58
62
|
## Snapshot and privacy guarantees
|
|
59
63
|
|
|
@@ -78,10 +82,12 @@ and records a SHA-256 in the immutable baseline. Transfer refuses an object that
|
|
|
78
82
|
exceeds its declared limit, or falls outside an approved bucket and prefix.
|
|
79
83
|
|
|
80
84
|
Privacy policy version 2 is executable without project sanitizer code. It
|
|
81
|
-
supports exact keep, exclusion, keyed
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
supports exact keep, exclusion, keyed and constraint-preserving pseudonyms (including
|
|
86
|
+
safe loopback HTTPS replacements), bounded reviewed-identity substitution inside
|
|
87
|
+
retained text,
|
|
88
|
+
constants, named sanitized-input digests, and a small set of documented
|
|
89
|
+
derivations. Policies classify every selected field and supported nested JSON
|
|
90
|
+
key. A new or unknown field blocks refresh until the policy is reviewed again.
|
|
85
91
|
|
|
86
92
|
The pseudonym key is an owner-only local secret. It is not stored in the
|
|
87
93
|
baseline or source control. Receipts bind the policy bytes, schema shape,
|
package/docs/troubleshooting.md
CHANGED
|
@@ -32,7 +32,11 @@ npx rehearsal
|
|
|
32
32
|
|
|
33
33
|
Choose **Set the stage** and approve the preview. For pnpm and Yarn, use this guided path;
|
|
34
34
|
automatic root generation is currently tested with npm. An existing supported config is
|
|
35
|
-
kept in place, including `infrastructure/rehearsal/rehearsal.config.mjs`.
|
|
35
|
+
kept in place, including `infrastructure/rehearsal/rehearsal.config.mjs`. When npm shows
|
|
36
|
+
lifecycle output, the install message names the active path and explains when that
|
|
37
|
+
existing file prevented `rehearsal.config.mjs` from being generated. You can always run
|
|
38
|
+
`npx rehearsal init --json` and read `data.destination`. To relocate it safely, follow
|
|
39
|
+
[Move an existing config to the project root](configuration.md#move-an-existing-config-to-the-project-root).
|
|
36
40
|
|
|
37
41
|
## Setup says this Node.js version is unsupported
|
|
38
42
|
|
package/docs/tutorial.md
CHANGED
|
@@ -106,3 +106,8 @@ npm run test:fixture
|
|
|
106
106
|
```
|
|
107
107
|
|
|
108
108
|
The PostgreSQL equivalent is `npm run test:fixture:postgresql`.
|
|
109
|
+
|
|
110
|
+
The release workflow also runs `npm run test:fixture:onboarding` from the packed package.
|
|
111
|
+
That check creates fresh Supabase and PostgreSQL consumers, verifies beta configuration
|
|
112
|
+
preservation, and exercises safe refusal and recovery for partial setup, occupied ports,
|
|
113
|
+
malformed configuration, and missing credentials.
|
package/package.json
CHANGED
|
@@ -2,33 +2,16 @@
|
|
|
2
2
|
/** Create safe project-root Rehearsal scaffolding during a local install. */
|
|
3
3
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
createInstalledRehearsalScaffold,
|
|
7
|
+
renderInstalledRehearsalScaffold,
|
|
8
|
+
} from "../../src/project/install_scaffold.mjs";
|
|
6
9
|
|
|
7
10
|
const packageRoot = fileURLToPath(new URL("../..", import.meta.url));
|
|
8
11
|
|
|
9
12
|
try {
|
|
10
13
|
const result = await createInstalledRehearsalScaffold({ packageRoot });
|
|
11
|
-
|
|
12
|
-
const changed = result.files.filter((file) => file.action !== "unchanged");
|
|
13
|
-
console.log(
|
|
14
|
-
[
|
|
15
|
-
"Rehearsal is ready in this project.",
|
|
16
|
-
...(changed.length
|
|
17
|
-
? [
|
|
18
|
-
...changed.map(
|
|
19
|
-
(file) =>
|
|
20
|
-
` ${file.action === "create" ? "+" : "~"} ${file.path}`,
|
|
21
|
-
),
|
|
22
|
-
]
|
|
23
|
-
: [" Existing Rehearsal configuration preserved."]),
|
|
24
|
-
"Next: review rehearsal.config.mjs, then run npx rehearsal.",
|
|
25
|
-
].join("\n"),
|
|
26
|
-
);
|
|
27
|
-
} else {
|
|
28
|
-
console.log(
|
|
29
|
-
`Rehearsal did not create project files: ${result.reason}.\nNext: run npx rehearsal from the application directory.`,
|
|
30
|
-
);
|
|
31
|
-
}
|
|
14
|
+
console.log(renderInstalledRehearsalScaffold(result));
|
|
32
15
|
} catch (error) {
|
|
33
16
|
console.warn(
|
|
34
17
|
`Rehearsal could not safely create project files during installation: ${String(error?.message ?? error)}\nNext: run npx rehearsal from the application directory.`,
|