@rehearsal-db/core 0.1.0-beta.15 → 0.1.0-beta.17
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 +47 -1
- package/dist/package.json +10 -9
- package/dist/src/application/interactive_task.d.mts +30 -0
- package/dist/src/application/interactive_task.mjs +83 -0
- package/dist/src/application/session.d.mts +1 -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 -162
- package/dist/src/baseline/privacy_engine.mjs +4 -1162
- package/dist/src/baseline/privacy_key.d.mts +7 -0
- package/dist/src/baseline/privacy_key.mjs +42 -0
- package/dist/src/baseline/privacy_pipeline.d.mts +7 -0
- package/dist/src/baseline/privacy_pipeline.mjs +127 -0
- package/dist/src/baseline/privacy_policy.d.mts +6 -0
- package/dist/src/baseline/privacy_policy.mjs +237 -0
- package/dist/src/baseline/privacy_recipe_validation.d.mts +5 -0
- package/dist/src/baseline/privacy_recipe_validation.mjs +289 -0
- package/dist/src/baseline/privacy_rule_registry.d.mts +18 -0
- package/dist/src/baseline/privacy_rule_registry.mjs +143 -0
- package/dist/src/baseline/privacy_transforms.d.mts +18 -0
- package/dist/src/baseline/privacy_transforms.mjs +142 -0
- package/dist/src/baseline/privacy_validation_shared.d.mts +31 -0
- package/dist/src/baseline/privacy_validation_shared.mjs +127 -0
- package/dist/src/cli/application_command_handlers.d.mts +8 -0
- package/dist/src/cli/application_command_handlers.mjs +52 -0
- package/dist/src/cli/arguments.d.mts +0 -5
- package/dist/src/cli/arguments.mjs +2 -7
- package/dist/src/cli/basic_command_handlers.d.mts +4 -0
- package/dist/src/cli/basic_command_handlers.mjs +57 -0
- package/dist/src/cli/cli_commands.d.mts +14 -0
- package/dist/src/cli/cli_commands.mjs +34 -0
- package/dist/src/cli/command_contract.d.mts +57 -0
- package/dist/src/cli/command_contract.mjs +39 -0
- package/dist/src/cli/command_output.d.mts +12 -0
- package/dist/src/cli/command_output.mjs +20 -0
- package/dist/src/cli/command_registry.d.mts +7 -0
- package/dist/src/cli/command_registry.mjs +14 -0
- package/dist/src/cli/guided.d.mts +3 -1
- package/dist/src/cli/guided.mjs +21 -7
- package/dist/src/cli/identity_command_handlers.d.mts +12 -0
- package/dist/src/cli/identity_command_handlers.mjs +239 -0
- package/dist/src/cli/identity_renderer.d.mts +8 -0
- package/dist/src/cli/identity_renderer.mjs +33 -0
- package/dist/src/cli/init_command.d.mts +1 -1
- package/dist/src/cli/inspection_command_handlers.d.mts +3 -0
- package/dist/src/cli/inspection_command_handlers.mjs +79 -0
- package/dist/src/cli/project_command_handlers.d.mts +11 -0
- package/dist/src/cli/project_command_handlers.mjs +92 -0
- package/dist/src/cli/project_commands.d.mts +131 -5
- package/dist/src/cli/project_commands.mjs +17 -8
- package/dist/src/cli/rehearsal.d.mts +2 -2
- package/dist/src/cli/rehearsal.mjs +51 -567
- package/dist/src/cli/runtime_action_handlers.d.mts +12 -0
- package/dist/src/cli/runtime_action_handlers.mjs +201 -0
- package/dist/src/cli/runtime_commands.d.mts +32 -3
- package/dist/src/cli/runtime_commands.mjs +33 -13
- package/dist/src/cli/source_command_handlers.d.mts +8 -0
- package/dist/src/cli/source_command_handlers.mjs +62 -0
- package/dist/src/cli/source_commands.d.mts +5 -1
- package/dist/src/cli/source_commands.mjs +3 -1
- package/dist/src/identity/claim.d.mts +5 -144
- package/dist/src/identity/claim.mjs +4 -947
- package/dist/src/identity/claim_contract.d.mts +135 -0
- package/dist/src/identity/claim_contract.mjs +6 -0
- package/dist/src/identity/claim_executor.d.mts +10 -0
- package/dist/src/identity/claim_executor.mjs +358 -0
- package/dist/src/identity/claim_identity_lookup.d.mts +23 -0
- package/dist/src/identity/claim_identity_lookup.mjs +134 -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/connect.d.mts +30 -0
- package/dist/src/identity/connect.mjs +111 -0
- package/dist/src/index.d.mts +1 -1
- package/dist/src/project/configuration.d.mts +11 -179
- package/dist/src/project/configuration.mjs +7 -956
- package/dist/src/project/configuration_contract.d.mts +125 -0
- package/dist/src/project/configuration_contract.mjs +27 -0
- package/dist/src/project/configuration_discovery.d.mts +7 -0
- package/dist/src/project/configuration_discovery.mjs +37 -0
- package/dist/src/project/configuration_normalization.d.mts +107 -0
- package/dist/src/project/configuration_normalization.mjs +302 -0
- package/dist/src/project/configuration_rendering.d.mts +14 -0
- package/dist/src/project/configuration_rendering.mjs +226 -0
- package/dist/src/project/configuration_resolution.d.mts +125 -0
- package/dist/src/project/configuration_resolution.mjs +81 -0
- package/dist/src/project/configuration_schema.d.mts +22 -0
- package/dist/src/project/configuration_schema.mjs +160 -0
- package/dist/src/project/configuration_validation.d.mts +22 -0
- package/dist/src/project/configuration_validation.mjs +136 -0
- package/dist/src/project/project_detection.d.mts +14 -0
- package/dist/src/project/project_detection.mjs +47 -0
- package/dist/src/runtime/lifecycle_engine.d.mts +63 -0
- package/dist/src/runtime/lifecycle_engine.mjs +114 -0
- package/dist/src/runtime/plan.mjs +2 -1
- package/dist/src/targets/postgresql.mjs +142 -163
- package/dist/src/targets/supabase.mjs +142 -174
- package/docs/architecture.md +120 -3
- package/docs/commands.md +18 -15
- package/docs/configuration.md +1 -0
- package/docs/releasing.md +8 -3
- package/docs/runtime-policies.md +12 -3
- package/docs/troubleshooting.md +17 -0
- package/docs/typescript.md +57 -10
- package/package.json +10 -9
- package/types/configuration.d.mts +5 -164
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,49 @@ a Changelog, and versions will follow Semantic Versioning after the package exis
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
+
## [0.1.0-beta.17] - 2026-10-08
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `rehearsal identity connect` and the guided home screen now keep the sandbox app
|
|
13
|
+
available during ordinary provider sign-in, detect exactly one approved verified local
|
|
14
|
+
identity, show a redacted copied-account preview, and apply the existing exact claim
|
|
15
|
+
after one deliberate confirmation. The app is reopened automatically with an honest
|
|
16
|
+
fresh-session instruction; explicit `identity plan` and `identity claim` commands remain
|
|
17
|
+
available for scripts.
|
|
18
|
+
|
|
19
|
+
## [0.1.0-beta.16] - 2026-10-08
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- The CLI now routes guided and scripted requests through focused typed command
|
|
24
|
+
handlers and one result/output contract. The executable retains only process
|
|
25
|
+
lifecycle, project locking, guided-loop coordination, and top-level error handling;
|
|
26
|
+
command-session caches no longer live in public parsed flags.
|
|
27
|
+
- Configuration now passes through separate discovery, raw-schema validation,
|
|
28
|
+
normalization, project-root path resolution, project detection, and starter-rendering
|
|
29
|
+
modules behind the existing import path. Public declarations come from the same
|
|
30
|
+
TypeScript source, including the new `NormalizedRehearsalConfig` type.
|
|
31
|
+
- Installed-consumer verification now shares one scenario catalog, artifact installer,
|
|
32
|
+
isolated workspace boundary, process boundary, and persistent application-session
|
|
33
|
+
helper. The complete release gate runs every scenario against one immutable tarball
|
|
34
|
+
and records its exact version, SHA-256, tool environment, commands, timings, and
|
|
35
|
+
results.
|
|
36
|
+
- Identity policy validation, deterministic claim planning, SQL generation, local Auth
|
|
37
|
+
lookup, pure verification, and transactional execution now live in separate typed
|
|
38
|
+
stages behind the unchanged public claim API. Generated reference SQL is independently
|
|
39
|
+
testable, while only the executor can mutate the local database or Storage runtime.
|
|
40
|
+
- PostgreSQL and Supabase now share one typed runtime lifecycle engine for command
|
|
41
|
+
dispatch, active-baseline loading, migration confirmation and receipts, runtime
|
|
42
|
+
markers, and rollback. Candidate receipts are bound to their exact database target,
|
|
43
|
+
while each driver retains its own ownership proof and database-specific behavior.
|
|
44
|
+
- Privacy policy types, recipe normalization, dependency validation, deterministic
|
|
45
|
+
transforms, structured JSON, rule dispatch, row execution, and key handling now live
|
|
46
|
+
in focused typed stages behind the unchanged public privacy import. An explicit closed
|
|
47
|
+
registry covers every supported action and derivation without widening policy v2.
|
|
48
|
+
|
|
49
|
+
## [0.1.0-beta.15] - 2026-10-08
|
|
50
|
+
|
|
8
51
|
### Added
|
|
9
52
|
|
|
10
53
|
- A strict, no-emit TypeScript gate now checks the first shared runtime utilities and
|
|
@@ -482,7 +525,10 @@ a Changelog, and versions will follow Semantic Versioning after the package exis
|
|
|
482
525
|
publication uses short-lived trusted OIDC, and every release tag must already exist on
|
|
483
526
|
protected `main`.
|
|
484
527
|
|
|
485
|
-
[Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.
|
|
528
|
+
[Unreleased]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.15...HEAD
|
|
529
|
+
[0.1.0-beta.15]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.14...v0.1.0-beta.15
|
|
530
|
+
[0.1.0-beta.14]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.13...v0.1.0-beta.14
|
|
531
|
+
[0.1.0-beta.13]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.12...v0.1.0-beta.13
|
|
486
532
|
[0.1.0-beta.12]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.11...v0.1.0-beta.12
|
|
487
533
|
[0.1.0-beta.11]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.10...v0.1.0-beta.11
|
|
488
534
|
[0.1.0-beta.10]: https://github.com/Ddupasquier/rehearsal-db/compare/v0.1.0-beta.9...v0.1.0-beta.10
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rehearsal-db/core",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.17",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Safely rehearse PostgreSQL and Supabase migrations against sanitized, production-shaped data.",
|
|
6
6
|
"repository": {
|
|
@@ -60,16 +60,17 @@
|
|
|
60
60
|
"test:unit": "npm run build && vitest run tests/unit",
|
|
61
61
|
"test:integration": "npm run build && vitest run tests/integration tests/contracts",
|
|
62
62
|
"test:docs": "npm run build && vitest run tests/contracts/documentationCommands.test.mjs",
|
|
63
|
-
"test:fixture": "npm run build && node scripts/verification/
|
|
64
|
-
"test:fixture:postgresql": "npm run build && node scripts/verification/
|
|
65
|
-
"test:fixture:dependent": "npm run build && node scripts/verification/
|
|
66
|
-
"test:fixture:onboarding": "npm run build && node scripts/verification/
|
|
67
|
-
"test:fixture:typescript": "npm run build && node scripts/verification/
|
|
68
|
-
"test:fixture:standalone": "npm run build && node scripts/verification/
|
|
69
|
-
"test:fixture:privacy": "npm run build && node scripts/verification/
|
|
63
|
+
"test:fixture": "npm run build && node scripts/verification/run_scenario.mjs supabase",
|
|
64
|
+
"test:fixture:postgresql": "npm run build && node scripts/verification/run_scenario.mjs postgresql",
|
|
65
|
+
"test:fixture:dependent": "npm run build && node scripts/verification/run_scenario.mjs dependent-target",
|
|
66
|
+
"test:fixture:onboarding": "npm run build && node scripts/verification/run_scenario.mjs onboarding",
|
|
67
|
+
"test:fixture:typescript": "npm run build && node scripts/verification/run_scenario.mjs typescript",
|
|
68
|
+
"test:fixture:standalone": "npm run build && node scripts/verification/run_scenario.mjs standalone",
|
|
69
|
+
"test:fixture:privacy": "npm run build && node scripts/verification/run_scenario.mjs privacy",
|
|
70
|
+
"release:verify": "node scripts/verification/release_gate.mjs",
|
|
70
71
|
"check": "npm run check:syntax && npm run typecheck && npm run test && npm run docs:build && npm run format:check && npm run package:audit",
|
|
71
72
|
"check:syntax": "find scripts tests -type f -name '*.mjs' -exec node --check {} +",
|
|
72
|
-
"typecheck": "tsc --project tsconfig.json",
|
|
73
|
+
"typecheck": "npm run build && tsc --project tsconfig.json",
|
|
73
74
|
"format": "prettier --write .",
|
|
74
75
|
"format:check": "prettier --check .",
|
|
75
76
|
"package:audit": "node scripts/verification/audit_package.mjs"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Run identity discovery while an owned local application remains available. */
|
|
2
|
+
import type { EventEmitter } from "node:events";
|
|
3
|
+
import { type ApplicationDiagnostics, type ApplicationSession, type ApplicationSessionSignal } from "./session.mjs";
|
|
4
|
+
type SignalTarget = Pick<EventEmitter, "prependListener" | "removeListener">;
|
|
5
|
+
interface SessionInput {
|
|
6
|
+
readonly isTTY?: boolean;
|
|
7
|
+
prependListener(event: "data", listener: (chunk: Buffer | string) => void): unknown;
|
|
8
|
+
removeListener(event: "data", listener: (chunk: Buffer | string) => void): unknown;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A terminal interruption aborts the task and retires the owned process group.
|
|
12
|
+
* A completed task leaves the session running so its caller can deliberately
|
|
13
|
+
* stop, reuse, or hand it back to the persistent application holder.
|
|
14
|
+
*/
|
|
15
|
+
export declare const runApplicationSessionTask: <Value>({ session, task, signalTarget, input, onReady, }: {
|
|
16
|
+
session: ApplicationSession;
|
|
17
|
+
task: (signal: AbortSignal) => Promise<Value>;
|
|
18
|
+
signalTarget?: SignalTarget;
|
|
19
|
+
input?: SessionInput;
|
|
20
|
+
onReady?: (ready: ApplicationSession["ready"]) => void;
|
|
21
|
+
}) => Promise<Readonly<{
|
|
22
|
+
completed: true;
|
|
23
|
+
value: Value;
|
|
24
|
+
diagnostics: ApplicationDiagnostics;
|
|
25
|
+
}> | Readonly<{
|
|
26
|
+
completed: false;
|
|
27
|
+
signal: ApplicationSessionSignal;
|
|
28
|
+
diagnostics: ApplicationDiagnostics;
|
|
29
|
+
}>>;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** Run identity discovery while an owned local application remains available. */
|
|
2
|
+
import { summarizeProjectCommandFailure, } from "./session.mjs";
|
|
3
|
+
const SESSION_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP", "SIGTSTP"];
|
|
4
|
+
/**
|
|
5
|
+
* A terminal interruption aborts the task and retires the owned process group.
|
|
6
|
+
* A completed task leaves the session running so its caller can deliberately
|
|
7
|
+
* stop, reuse, or hand it back to the persistent application holder.
|
|
8
|
+
*/
|
|
9
|
+
export const runApplicationSessionTask = async ({ session, task, signalTarget = process, input = process.stdin, onReady = () => undefined, }) => {
|
|
10
|
+
if (!session ||
|
|
11
|
+
typeof session.wait !== "function" ||
|
|
12
|
+
typeof session.stop !== "function") {
|
|
13
|
+
throw new Error("A started application session is required.");
|
|
14
|
+
}
|
|
15
|
+
let finishInterruption = () => undefined;
|
|
16
|
+
const interruption = new Promise((resolve) => {
|
|
17
|
+
finishInterruption = resolve;
|
|
18
|
+
});
|
|
19
|
+
const handlers = new Map(SESSION_SIGNALS.map((signal) => [
|
|
20
|
+
signal,
|
|
21
|
+
() => finishInterruption({ kind: "interrupted", signal }),
|
|
22
|
+
]));
|
|
23
|
+
for (const [signal, handler] of handlers) {
|
|
24
|
+
signalTarget.prependListener(signal, handler);
|
|
25
|
+
}
|
|
26
|
+
const onInput = (chunk) => {
|
|
27
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
28
|
+
if (bytes.includes(0x03)) {
|
|
29
|
+
finishInterruption({ kind: "interrupted", signal: "SIGINT" });
|
|
30
|
+
}
|
|
31
|
+
else if (bytes.includes(0x1a)) {
|
|
32
|
+
finishInterruption({ kind: "interrupted", signal: "SIGTSTP" });
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
if (input?.isTTY)
|
|
36
|
+
input.prependListener("data", onInput);
|
|
37
|
+
const abortController = new AbortController();
|
|
38
|
+
try {
|
|
39
|
+
onReady(session.ready);
|
|
40
|
+
const outcome = await Promise.race([
|
|
41
|
+
interruption,
|
|
42
|
+
task(abortController.signal).then((value) => ({
|
|
43
|
+
kind: "completed",
|
|
44
|
+
value,
|
|
45
|
+
})),
|
|
46
|
+
session.wait().then((exit) => ({ kind: "exited", ...exit })),
|
|
47
|
+
]);
|
|
48
|
+
if (outcome.kind === "exited") {
|
|
49
|
+
throw new Error(`Application exited before identity connection finished (${summarizeProjectCommandFailure({
|
|
50
|
+
status: outcome.status,
|
|
51
|
+
signal: outcome.signal,
|
|
52
|
+
stdoutByteCount: session.diagnostics().stdoutBytes,
|
|
53
|
+
stderrByteCount: session.diagnostics().stderrBytes,
|
|
54
|
+
})}).`);
|
|
55
|
+
}
|
|
56
|
+
if (outcome.kind === "interrupted") {
|
|
57
|
+
abortController.abort(new Error(`Identity connection was interrupted by ${outcome.signal}.`));
|
|
58
|
+
await session.stop();
|
|
59
|
+
return Object.freeze({
|
|
60
|
+
completed: false,
|
|
61
|
+
signal: outcome.signal,
|
|
62
|
+
diagnostics: session.diagnostics(),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return Object.freeze({
|
|
66
|
+
completed: true,
|
|
67
|
+
value: outcome.value,
|
|
68
|
+
diagnostics: session.diagnostics(),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
abortController.abort(error);
|
|
73
|
+
await session.stop();
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
for (const [signal, handler] of handlers) {
|
|
78
|
+
signalTarget.removeListener(signal, handler);
|
|
79
|
+
}
|
|
80
|
+
if (input?.isTTY)
|
|
81
|
+
input.removeListener("data", onInput);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
@@ -3,6 +3,7 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
import type { EventEmitter } from "node:events";
|
|
4
4
|
declare const SESSION_SIGNALS: string[];
|
|
5
5
|
type SessionSignal = (typeof SESSION_SIGNALS)[number];
|
|
6
|
+
export type ApplicationSessionSignal = SessionSignal;
|
|
6
7
|
export interface ApplicationReadiness {
|
|
7
8
|
readonly url: string;
|
|
8
9
|
readonly expectedStatus?: number;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/** Typed, normalized contract shared by privacy validation and execution stages. */
|
|
2
|
+
export type PrivacyAction = "KEEP" | "PSEUDONYMIZE" | "REPLACE" | "EXCLUDE" | "DERIVE";
|
|
3
|
+
export type PrivacyObject = Record<string, unknown>;
|
|
4
|
+
export interface PseudonymRecipe {
|
|
5
|
+
format: "uuid" | "email" | "text" | "integer" | "hex" | "gtin" | "url";
|
|
6
|
+
namespace: string;
|
|
7
|
+
maxLength?: number;
|
|
8
|
+
length?: number;
|
|
9
|
+
allowedLengths?: readonly number[];
|
|
10
|
+
origin?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ConstantRecipe {
|
|
13
|
+
kind: "constant";
|
|
14
|
+
value: unknown;
|
|
15
|
+
}
|
|
16
|
+
export interface DateShiftRecipe {
|
|
17
|
+
kind: "date-shift";
|
|
18
|
+
days: number;
|
|
19
|
+
representation: "iso-string" | "epoch-milliseconds";
|
|
20
|
+
group?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface EnumRecipe {
|
|
23
|
+
kind: "enum";
|
|
24
|
+
allowNull: boolean;
|
|
25
|
+
values: readonly (string | number | boolean)[];
|
|
26
|
+
}
|
|
27
|
+
export interface ValidatedStringRecipe {
|
|
28
|
+
kind: "validated-string";
|
|
29
|
+
format: "portable-code";
|
|
30
|
+
maximumBytes: number;
|
|
31
|
+
allowNull: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface DigestRecipe {
|
|
34
|
+
kind: "digest";
|
|
35
|
+
format: "hex";
|
|
36
|
+
length: number;
|
|
37
|
+
namespace: string;
|
|
38
|
+
inputs: readonly string[];
|
|
39
|
+
}
|
|
40
|
+
export interface BindingSubstituteRecipe {
|
|
41
|
+
kind: "binding-substitute";
|
|
42
|
+
binding: string;
|
|
43
|
+
format: "uuid";
|
|
44
|
+
namespace: string;
|
|
45
|
+
maximumBytes: number;
|
|
46
|
+
}
|
|
47
|
+
export interface PathMapRecipe {
|
|
48
|
+
kind: "path-map";
|
|
49
|
+
mapping: string;
|
|
50
|
+
}
|
|
51
|
+
export interface ApprovedOwnerRecipe {
|
|
52
|
+
kind: "approved-owner";
|
|
53
|
+
approved: NormalizedDeclaration;
|
|
54
|
+
otherwise: NormalizedDeclaration;
|
|
55
|
+
}
|
|
56
|
+
export interface JsonArrayRecipe {
|
|
57
|
+
kind: "json-array";
|
|
58
|
+
allowNull: boolean;
|
|
59
|
+
maximumBytes: number;
|
|
60
|
+
maximumDepth: number;
|
|
61
|
+
maximumItems: number;
|
|
62
|
+
items: NormalizedDeclaration;
|
|
63
|
+
}
|
|
64
|
+
export interface JsonObjectRecipe {
|
|
65
|
+
kind: "json-object";
|
|
66
|
+
allowNull: boolean;
|
|
67
|
+
maximumBytes: number;
|
|
68
|
+
maximumDepth: number;
|
|
69
|
+
fields: Readonly<Record<string, NormalizedDeclaration>>;
|
|
70
|
+
}
|
|
71
|
+
export interface JsonUnionRecipe {
|
|
72
|
+
kind: "json-union";
|
|
73
|
+
allowNull: boolean;
|
|
74
|
+
maximumBytes: number;
|
|
75
|
+
maximumDepth: number;
|
|
76
|
+
maximumItems: number;
|
|
77
|
+
variants: Readonly<Record<string, NormalizedDeclaration>>;
|
|
78
|
+
}
|
|
79
|
+
export interface JsonDictionaryRecipe {
|
|
80
|
+
kind: "json-dictionary";
|
|
81
|
+
allowNull: boolean;
|
|
82
|
+
maximumBytes: number;
|
|
83
|
+
maximumDepth: number;
|
|
84
|
+
maximumItems: number;
|
|
85
|
+
keys: Readonly<Record<string, unknown>>;
|
|
86
|
+
values: NormalizedDeclaration;
|
|
87
|
+
}
|
|
88
|
+
export type DerivationRecipe = DateShiftRecipe | EnumRecipe | ValidatedStringRecipe | DigestRecipe | BindingSubstituteRecipe | PathMapRecipe | ApprovedOwnerRecipe | JsonArrayRecipe | JsonObjectRecipe | JsonUnionRecipe | JsonDictionaryRecipe;
|
|
89
|
+
export type NormalizedRecipe = PseudonymRecipe | ConstantRecipe | DerivationRecipe | null;
|
|
90
|
+
export interface NormalizedDeclaration {
|
|
91
|
+
action: PrivacyAction;
|
|
92
|
+
recipe: NormalizedRecipe;
|
|
93
|
+
required?: boolean;
|
|
94
|
+
}
|
|
95
|
+
export interface PrivacyBinding {
|
|
96
|
+
environmentVariable: string;
|
|
97
|
+
approvedValueSha256: string;
|
|
98
|
+
}
|
|
99
|
+
export interface PrivacyPathMapping extends PseudonymRecipe {
|
|
100
|
+
binding: string;
|
|
101
|
+
format: "uuid" | "text";
|
|
102
|
+
maximumBytes: number;
|
|
103
|
+
maximumSegments: number;
|
|
104
|
+
}
|
|
105
|
+
export interface PrivacyForeignKey {
|
|
106
|
+
readonly schema: string;
|
|
107
|
+
readonly table: string;
|
|
108
|
+
readonly column: string;
|
|
109
|
+
}
|
|
110
|
+
export interface PrivacyColumn extends NormalizedDeclaration {
|
|
111
|
+
name: string;
|
|
112
|
+
generated: "ALWAYS" | "NEVER";
|
|
113
|
+
identity: "YES" | "NO";
|
|
114
|
+
foreignKey: PrivacyForeignKey | null;
|
|
115
|
+
}
|
|
116
|
+
export interface PrivacyTable {
|
|
117
|
+
schema: string;
|
|
118
|
+
name: string;
|
|
119
|
+
sourceRows: "STREAM AND SANITIZE" | "EXCLUDE";
|
|
120
|
+
ownerBinding: Readonly<{
|
|
121
|
+
binding: string;
|
|
122
|
+
columns: readonly string[];
|
|
123
|
+
match: "any";
|
|
124
|
+
nullBehavior?: "otherwise";
|
|
125
|
+
}> | null;
|
|
126
|
+
columns: readonly PrivacyColumn[];
|
|
127
|
+
}
|
|
128
|
+
export interface ExecutablePrivacyPolicy {
|
|
129
|
+
readonly policyVersion: 2;
|
|
130
|
+
readonly migrationCutoff: string;
|
|
131
|
+
readonly bindings: Readonly<Record<string, PrivacyBinding>>;
|
|
132
|
+
readonly pathMappings: Readonly<Record<string, PrivacyPathMapping>>;
|
|
133
|
+
readonly tables: readonly PrivacyTable[];
|
|
134
|
+
}
|
|
135
|
+
export interface PrivacySourceRecord {
|
|
136
|
+
schema?: string;
|
|
137
|
+
table: string;
|
|
138
|
+
row: Record<string, unknown>;
|
|
139
|
+
}
|
|
140
|
+
export interface PrivacyEngine {
|
|
141
|
+
readonly keyFingerprint: string;
|
|
142
|
+
remapPath(input: {
|
|
143
|
+
mapping: string;
|
|
144
|
+
value: string;
|
|
145
|
+
}): string;
|
|
146
|
+
sanitize(record: unknown): PrivacySourceRecord | null;
|
|
147
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Validate cross-rule bindings, dependencies, and shared deterministic groups. */
|
|
2
|
+
import type { PrivacyBinding, PrivacyPathMapping, PrivacyTable } from "./privacy_contract.mjs";
|
|
3
|
+
export declare const validatePrivacyDependencyGraph: ({ tables, bindings, pathMappings, }: {
|
|
4
|
+
tables: readonly PrivacyTable[];
|
|
5
|
+
bindings: Readonly<Record<string, PrivacyBinding>>;
|
|
6
|
+
pathMappings: Readonly<Record<string, PrivacyPathMapping>>;
|
|
7
|
+
}) => void;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/** Validate cross-rule bindings, dependencies, and shared deterministic groups. */
|
|
2
|
+
import { structuredJsonDeclarations } from "./privacy_structured_json.mjs";
|
|
3
|
+
const usesApprovedOwner = (declaration) => (declaration.recipe !== null &&
|
|
4
|
+
"kind" in declaration.recipe &&
|
|
5
|
+
declaration.recipe.kind === "approved-owner") ||
|
|
6
|
+
structuredJsonDeclarations(declaration.recipe).some(usesApprovedOwner);
|
|
7
|
+
const visitRecipes = (declaration, visit) => {
|
|
8
|
+
visit(declaration.recipe);
|
|
9
|
+
if (declaration.recipe !== null &&
|
|
10
|
+
"kind" in declaration.recipe &&
|
|
11
|
+
declaration.recipe.kind === "approved-owner") {
|
|
12
|
+
visitRecipes(declaration.recipe.approved, visit);
|
|
13
|
+
visitRecipes(declaration.recipe.otherwise, visit);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
structuredJsonDeclarations(declaration.recipe).forEach((nested) => visitRecipes(nested, visit));
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const digestInputs = (declaration) => {
|
|
20
|
+
if (declaration.recipe !== null &&
|
|
21
|
+
"kind" in declaration.recipe &&
|
|
22
|
+
declaration.recipe.kind === "digest") {
|
|
23
|
+
return [...declaration.recipe.inputs];
|
|
24
|
+
}
|
|
25
|
+
if (declaration.recipe !== null &&
|
|
26
|
+
"kind" in declaration.recipe &&
|
|
27
|
+
declaration.recipe.kind === "approved-owner") {
|
|
28
|
+
return [
|
|
29
|
+
...digestInputs(declaration.recipe.approved),
|
|
30
|
+
...digestInputs(declaration.recipe.otherwise),
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
return structuredJsonDeclarations(declaration.recipe).flatMap(digestInputs);
|
|
34
|
+
};
|
|
35
|
+
const validateTableDependencies = ({ table, index, bindings, pathMappings, }) => {
|
|
36
|
+
const label = `policy.tables[${index}]`;
|
|
37
|
+
if (table.columns.some(usesApprovedOwner) && !table.ownerBinding) {
|
|
38
|
+
throw new Error(`${label} uses approved-owner without ownerBinding.`);
|
|
39
|
+
}
|
|
40
|
+
for (const column of table.columns) {
|
|
41
|
+
visitRecipes(column, (recipe) => {
|
|
42
|
+
if (recipe &&
|
|
43
|
+
"kind" in recipe &&
|
|
44
|
+
recipe.kind === "path-map" &&
|
|
45
|
+
!Object.hasOwn(pathMappings, recipe.mapping)) {
|
|
46
|
+
throw new Error(`${label} references unknown path mapping ${recipe.mapping}.`);
|
|
47
|
+
}
|
|
48
|
+
if (recipe !== null &&
|
|
49
|
+
"kind" in recipe &&
|
|
50
|
+
recipe.kind === "binding-substitute" &&
|
|
51
|
+
!Object.hasOwn(bindings, recipe.binding)) {
|
|
52
|
+
throw new Error(`${label} references unknown privacy binding ${recipe.binding}.`);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const dependencies = new Map(table.columns.map((column) => [column.name, []]));
|
|
57
|
+
for (const column of table.columns) {
|
|
58
|
+
const inputs = [...new Set(digestInputs(column))];
|
|
59
|
+
for (const input of inputs) {
|
|
60
|
+
const inputColumn = table.columns.find((entry) => entry.name === input);
|
|
61
|
+
if (!inputColumn) {
|
|
62
|
+
throw new Error(`${label}.${column.name} digest references unknown column ${input}.`);
|
|
63
|
+
}
|
|
64
|
+
if (inputColumn.action === "EXCLUDE") {
|
|
65
|
+
throw new Error(`${label}.${column.name} digest input ${input} is excluded instead of sanitized.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
dependencies.set(column.name, inputs);
|
|
69
|
+
}
|
|
70
|
+
const resolved = new Set();
|
|
71
|
+
const resolving = new Set();
|
|
72
|
+
const visitDependency = (columnName) => {
|
|
73
|
+
if (resolved.has(columnName))
|
|
74
|
+
return;
|
|
75
|
+
if (resolving.has(columnName)) {
|
|
76
|
+
throw new Error(`${label} contains a cyclic digest dependency.`);
|
|
77
|
+
}
|
|
78
|
+
resolving.add(columnName);
|
|
79
|
+
for (const input of dependencies.get(columnName) ?? []) {
|
|
80
|
+
visitDependency(input);
|
|
81
|
+
}
|
|
82
|
+
resolving.delete(columnName);
|
|
83
|
+
resolved.add(columnName);
|
|
84
|
+
};
|
|
85
|
+
table.columns.forEach((column) => visitDependency(column.name));
|
|
86
|
+
};
|
|
87
|
+
export const validatePrivacyDependencyGraph = ({ tables, bindings, pathMappings, }) => {
|
|
88
|
+
tables.forEach((table, index) => validateTableDependencies({ table, index, bindings, pathMappings }));
|
|
89
|
+
const dateShiftGroups = new Map();
|
|
90
|
+
const validateDateShiftGroup = (declaration) => {
|
|
91
|
+
const recipe = declaration.recipe;
|
|
92
|
+
if (!recipe || !("kind" in recipe))
|
|
93
|
+
return;
|
|
94
|
+
if (recipe.kind === "date-shift" && recipe.group) {
|
|
95
|
+
const existing = dateShiftGroups.get(recipe.group);
|
|
96
|
+
if (existing !== undefined && existing !== recipe.days) {
|
|
97
|
+
throw new Error(`Privacy date-shift group ${recipe.group} must use one days value.`);
|
|
98
|
+
}
|
|
99
|
+
dateShiftGroups.set(recipe.group, recipe.days);
|
|
100
|
+
}
|
|
101
|
+
if (recipe.kind === "approved-owner") {
|
|
102
|
+
validateDateShiftGroup(recipe.approved);
|
|
103
|
+
validateDateShiftGroup(recipe.otherwise);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
structuredJsonDeclarations(recipe).forEach(validateDateShiftGroup);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
tables.forEach((table) => table.columns.forEach(validateDateShiftGroup));
|
|
110
|
+
};
|
|
@@ -1,162 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
type PrivacyAction = "KEEP" | "PSEUDONYMIZE" | "REPLACE" | "EXCLUDE" | "DERIVE";
|
|
7
|
-
interface PseudonymRecipe {
|
|
8
|
-
format: "uuid" | "email" | "text" | "integer" | "hex" | "gtin" | "url";
|
|
9
|
-
namespace: string;
|
|
10
|
-
maxLength?: number;
|
|
11
|
-
length?: number;
|
|
12
|
-
allowedLengths?: readonly number[];
|
|
13
|
-
origin?: string;
|
|
14
|
-
}
|
|
15
|
-
interface ConstantRecipe {
|
|
16
|
-
kind: "constant";
|
|
17
|
-
value: unknown;
|
|
18
|
-
}
|
|
19
|
-
interface DateShiftRecipe {
|
|
20
|
-
kind: "date-shift";
|
|
21
|
-
days: number;
|
|
22
|
-
representation: "iso-string" | "epoch-milliseconds";
|
|
23
|
-
group?: string;
|
|
24
|
-
}
|
|
25
|
-
interface EnumRecipe {
|
|
26
|
-
kind: "enum";
|
|
27
|
-
allowNull: boolean;
|
|
28
|
-
values: readonly (string | number | boolean)[];
|
|
29
|
-
}
|
|
30
|
-
interface ValidatedStringRecipe {
|
|
31
|
-
kind: "validated-string";
|
|
32
|
-
format: "portable-code";
|
|
33
|
-
maximumBytes: number;
|
|
34
|
-
allowNull: boolean;
|
|
35
|
-
}
|
|
36
|
-
interface DigestRecipe {
|
|
37
|
-
kind: "digest";
|
|
38
|
-
format: "hex";
|
|
39
|
-
length: number;
|
|
40
|
-
namespace: string;
|
|
41
|
-
inputs: readonly string[];
|
|
42
|
-
}
|
|
43
|
-
interface BindingSubstituteRecipe {
|
|
44
|
-
kind: "binding-substitute";
|
|
45
|
-
binding: string;
|
|
46
|
-
format: "uuid";
|
|
47
|
-
namespace: string;
|
|
48
|
-
maximumBytes: number;
|
|
49
|
-
}
|
|
50
|
-
interface PathMapRecipe {
|
|
51
|
-
kind: "path-map";
|
|
52
|
-
mapping: string;
|
|
53
|
-
}
|
|
54
|
-
interface ApprovedOwnerRecipe {
|
|
55
|
-
kind: "approved-owner";
|
|
56
|
-
approved: NormalizedDeclaration;
|
|
57
|
-
otherwise: NormalizedDeclaration;
|
|
58
|
-
}
|
|
59
|
-
interface JsonArrayRecipe {
|
|
60
|
-
kind: "json-array";
|
|
61
|
-
allowNull: boolean;
|
|
62
|
-
maximumBytes: number;
|
|
63
|
-
maximumDepth: number;
|
|
64
|
-
maximumItems: number;
|
|
65
|
-
items: NormalizedDeclaration;
|
|
66
|
-
}
|
|
67
|
-
interface JsonObjectRecipe {
|
|
68
|
-
kind: "json-object";
|
|
69
|
-
allowNull: boolean;
|
|
70
|
-
maximumBytes: number;
|
|
71
|
-
maximumDepth: number;
|
|
72
|
-
fields: Readonly<Record<string, NormalizedDeclaration>>;
|
|
73
|
-
}
|
|
74
|
-
interface JsonUnionRecipe {
|
|
75
|
-
kind: "json-union";
|
|
76
|
-
allowNull: boolean;
|
|
77
|
-
maximumBytes: number;
|
|
78
|
-
maximumDepth: number;
|
|
79
|
-
maximumItems: number;
|
|
80
|
-
variants: Readonly<Record<string, NormalizedDeclaration>>;
|
|
81
|
-
}
|
|
82
|
-
interface JsonDictionaryRecipe {
|
|
83
|
-
kind: "json-dictionary";
|
|
84
|
-
allowNull: boolean;
|
|
85
|
-
maximumBytes: number;
|
|
86
|
-
maximumDepth: number;
|
|
87
|
-
maximumItems: number;
|
|
88
|
-
keys: Readonly<Record<string, unknown>>;
|
|
89
|
-
values: NormalizedDeclaration;
|
|
90
|
-
}
|
|
91
|
-
type DerivationRecipe = DateShiftRecipe | EnumRecipe | ValidatedStringRecipe | DigestRecipe | BindingSubstituteRecipe | PathMapRecipe | ApprovedOwnerRecipe | JsonArrayRecipe | JsonObjectRecipe | JsonUnionRecipe | JsonDictionaryRecipe;
|
|
92
|
-
type NormalizedRecipe = PseudonymRecipe | ConstantRecipe | DerivationRecipe | null;
|
|
93
|
-
interface NormalizedDeclaration {
|
|
94
|
-
action: PrivacyAction;
|
|
95
|
-
recipe: NormalizedRecipe;
|
|
96
|
-
required?: boolean;
|
|
97
|
-
}
|
|
98
|
-
interface PrivacyBinding {
|
|
99
|
-
environmentVariable: string;
|
|
100
|
-
approvedValueSha256: string;
|
|
101
|
-
}
|
|
102
|
-
interface PrivacyPathMapping extends PseudonymRecipe {
|
|
103
|
-
binding: string;
|
|
104
|
-
format: "uuid" | "text";
|
|
105
|
-
maximumBytes: number;
|
|
106
|
-
maximumSegments: number;
|
|
107
|
-
}
|
|
108
|
-
export interface PrivacyForeignKey {
|
|
109
|
-
readonly schema: string;
|
|
110
|
-
readonly table: string;
|
|
111
|
-
readonly column: string;
|
|
112
|
-
}
|
|
113
|
-
export interface PrivacyColumn extends NormalizedDeclaration {
|
|
114
|
-
name: string;
|
|
115
|
-
generated: "ALWAYS" | "NEVER";
|
|
116
|
-
identity: "YES" | "NO";
|
|
117
|
-
foreignKey: PrivacyForeignKey | null;
|
|
118
|
-
}
|
|
119
|
-
export interface PrivacyTable {
|
|
120
|
-
schema: string;
|
|
121
|
-
name: string;
|
|
122
|
-
sourceRows: "STREAM AND SANITIZE" | "EXCLUDE";
|
|
123
|
-
ownerBinding: Readonly<{
|
|
124
|
-
binding: string;
|
|
125
|
-
columns: readonly string[];
|
|
126
|
-
match: "any";
|
|
127
|
-
nullBehavior?: "otherwise";
|
|
128
|
-
}> | null;
|
|
129
|
-
columns: readonly PrivacyColumn[];
|
|
130
|
-
}
|
|
131
|
-
export interface ExecutablePrivacyPolicy {
|
|
132
|
-
readonly policyVersion: 2;
|
|
133
|
-
readonly migrationCutoff: string;
|
|
134
|
-
readonly bindings: Readonly<Record<string, PrivacyBinding>>;
|
|
135
|
-
readonly pathMappings: Readonly<Record<string, PrivacyPathMapping>>;
|
|
136
|
-
readonly tables: readonly PrivacyTable[];
|
|
137
|
-
}
|
|
138
|
-
export declare const validateExecutablePrivacyPolicy: (policy: unknown) => ExecutablePrivacyPolicy;
|
|
139
|
-
export interface PrivacySourceRecord {
|
|
140
|
-
schema?: string;
|
|
141
|
-
table: string;
|
|
142
|
-
row: Record<string, unknown>;
|
|
143
|
-
}
|
|
144
|
-
export interface PrivacyEngine {
|
|
145
|
-
readonly keyFingerprint: string;
|
|
146
|
-
remapPath(input: {
|
|
147
|
-
mapping: string;
|
|
148
|
-
value: string;
|
|
149
|
-
}): string;
|
|
150
|
-
sanitize(record: unknown): PrivacySourceRecord | null;
|
|
151
|
-
}
|
|
152
|
-
export declare const createPrivacyEngine: ({ policy, key, environment, }: {
|
|
153
|
-
policy: unknown;
|
|
154
|
-
key: Uint8Array | string;
|
|
155
|
-
environment?: NodeJS.ProcessEnv;
|
|
156
|
-
}) => Readonly<PrivacyEngine>;
|
|
157
|
-
export declare const createPrivacyKey: (path: string) => Promise<{
|
|
158
|
-
fingerprint: string;
|
|
159
|
-
}>;
|
|
160
|
-
export declare const readPrivacyKey: (path: string) => Promise<Buffer>;
|
|
161
|
-
export declare const assertPrivacyKeyFingerprint: (key: Uint8Array, expected: unknown) => void;
|
|
162
|
-
export {};
|
|
1
|
+
/** Stable public facade for the versioned, fail-closed privacy pipeline. */
|
|
2
|
+
export { validateExecutablePrivacyPolicy } from "./privacy_policy.mjs";
|
|
3
|
+
export { createPrivacyEngine } from "./privacy_pipeline.mjs";
|
|
4
|
+
export { assertPrivacyKeyFingerprint, createPrivacyKey, readPrivacyKey, } from "./privacy_key.mjs";
|
|
5
|
+
export type { ExecutablePrivacyPolicy, PrivacyColumn, PrivacyEngine, PrivacyForeignKey, PrivacySourceRecord, PrivacyTable, } from "./privacy_policy.mjs";
|