@rehearsal-db/core 0.1.0-beta.1 → 0.1.0-beta.11

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 (72) hide show
  1. package/CHANGELOG.md +271 -1
  2. package/COMPATIBILITY.md +26 -0
  3. package/README.md +150 -346
  4. package/SECURITY.md +2 -1
  5. package/SUPPORT.md +5 -0
  6. package/docs/README.md +38 -0
  7. package/docs/adapters.md +20 -6
  8. package/docs/architecture.md +58 -0
  9. package/docs/baselines.md +102 -22
  10. package/docs/commands.md +202 -33
  11. package/docs/configuration.md +288 -13
  12. package/docs/getting-started.md +114 -181
  13. package/docs/glossary.md +11 -8
  14. package/docs/index.md +47 -0
  15. package/docs/production-source.md +176 -33
  16. package/docs/releasing.md +28 -38
  17. package/docs/roadmap.md +41 -0
  18. package/docs/runtime-policies.md +253 -0
  19. package/docs/sanitization.md +144 -8
  20. package/docs/security-model.md +34 -9
  21. package/docs/standalone-workflow.md +109 -0
  22. package/docs/troubleshooting.md +127 -2
  23. package/docs/tutorial.md +70 -59
  24. package/package.json +39 -23
  25. package/scripts/install/create_root_config.mjs +36 -0
  26. package/scripts/runtime/manage_database.mjs +18 -0
  27. package/src/README.md +17 -0
  28. package/src/application/session.mjs +435 -0
  29. package/{scripts/lib/rehearsal/baseline_artifact.mjs → src/baseline/artifact.mjs} +142 -17
  30. package/{scripts/lib/rehearsal/baseline_builder.mjs → src/baseline/builder.mjs} +19 -10
  31. package/src/baseline/input_discovery.mjs +155 -0
  32. package/src/baseline/policy_review.mjs +92 -0
  33. package/src/baseline/preparation.mjs +323 -0
  34. package/src/baseline/privacy_engine.mjs +741 -0
  35. package/{scripts/lib/rehearsal → src/baseline}/sanitization_policy.mjs +133 -3
  36. package/{scripts/lib/rehearsal → src/baseline}/schema_snapshot.mjs +1 -1
  37. package/src/cli/arguments.mjs +123 -0
  38. package/src/cli/guided.mjs +812 -0
  39. package/src/cli/init_command.mjs +77 -0
  40. package/src/cli/rehearsal.mjs +952 -0
  41. package/src/cli/renderers.mjs +589 -0
  42. package/src/cli/runtime_commands.mjs +624 -0
  43. package/src/cli/source_commands.mjs +326 -0
  44. package/src/cli/terminal.mjs +275 -0
  45. package/src/identity/claim.mjs +1163 -0
  46. package/src/identity/storage.mjs +165 -0
  47. package/{scripts/lib/rehearsal → src/project}/configuration.d.mts +72 -4
  48. package/src/project/configuration.mjs +1201 -0
  49. package/src/project/install_scaffold.mjs +291 -0
  50. package/src/project/setup.mjs +484 -0
  51. package/src/project/support_report.mjs +105 -0
  52. package/src/runtime/cleanup.mjs +381 -0
  53. package/{scripts/lib/rehearsal → src/runtime}/plan.mjs +118 -20
  54. package/src/runtime/policy.mjs +438 -0
  55. package/{scripts/lib/rehearsal/runtime_restore.mjs → src/runtime/restore.mjs} +46 -27
  56. package/src/runtime/topology.mjs +177 -0
  57. package/{scripts/lib/rehearsal → src/shared}/diagnostics.mjs +10 -3
  58. package/src/shared/human_output.mjs +4 -0
  59. package/src/shared/operation_guard.mjs +162 -0
  60. package/{scripts/lib/rehearsal → src/shared}/process_environment.mjs +5 -1
  61. package/src/source/access.mjs +578 -0
  62. package/src/source/asset_transfer.mjs +177 -0
  63. package/src/source/baseline.mjs +446 -0
  64. package/src/source/postgresql_access.mjs +480 -0
  65. package/src/targets/postgresql.mjs +808 -0
  66. package/{scripts/operations/database/manage_rehearsal_database.mjs → src/targets/supabase.mjs} +66 -20
  67. package/{scripts/lib/environment/local_supabase.mjs → src/targets/supabase_environment.mjs} +60 -28
  68. package/src/targets/target.mjs +66 -0
  69. package/scripts/lib/rehearsal/configuration.mjs +0 -559
  70. package/scripts/operations/rehearsal/rehearsal_cli.mjs +0 -565
  71. /package/{scripts/lib/rehearsal → src/runtime}/migration_history.mjs +0 -0
  72. /package/{scripts/lib/rehearsal → src/runtime}/service_environment.mjs +0 -0
@@ -3,6 +3,9 @@
3
3
  * Projects own the policy, schema inventory, pseudonym keys, and derivation logic.
4
4
  */
5
5
 
6
+ import { createHash } from "node:crypto";
7
+ import { validateExecutablePrivacyPolicy } from "./privacy_engine.mjs";
8
+
6
9
  export const SANITIZATION_ACTIONS = Object.freeze({
7
10
  KEEP: "KEEP",
8
11
  PSEUDONYMIZE: "PSEUDONYMIZE",
@@ -31,6 +34,11 @@ const assertIdentifier = (value, label) => {
31
34
  }
32
35
  return value;
33
36
  };
37
+ const tableSchema = (table) => table.schema ?? "public";
38
+ const tableKey = (table) =>
39
+ tableSchema(table) === "public"
40
+ ? table.name
41
+ : `${tableSchema(table)}.${table.name}`;
34
42
 
35
43
  export const normalizeSanitizationAction = (action) => {
36
44
  const normalized = actionAliases.get(
@@ -44,12 +52,134 @@ export const normalizeSanitizationAction = (action) => {
44
52
  return normalized;
45
53
  };
46
54
 
55
+ export const validateRuntimeSanitizationPolicy = (policy) => {
56
+ if (!policy || typeof policy !== "object" || Array.isArray(policy)) {
57
+ throw new Error("A Rehearsal sanitization policy object is required.");
58
+ }
59
+ if (policy.draft === true) {
60
+ throw new Error(
61
+ "The sanitization policy is still a REVIEW REQUIRED draft.",
62
+ );
63
+ }
64
+ if (policy.policyVersion === 2) {
65
+ return validateExecutablePrivacyPolicy(policy);
66
+ }
67
+ if (policy.policyVersion !== 1) {
68
+ throw new Error("The sanitization policy must use policyVersion 1 or 2.");
69
+ }
70
+ if (!/^\d{14}$/u.test(policy.migrationCutoff ?? "")) {
71
+ throw new Error(
72
+ "The sanitization policy must declare a timestamp migrationCutoff.",
73
+ );
74
+ }
75
+ if (!Array.isArray(policy.tables) || policy.tables.length === 0) {
76
+ throw new Error("The sanitization policy does not list tables.");
77
+ }
78
+ const tables = new Set();
79
+ for (const table of policy.tables) {
80
+ const schemaName = assertIdentifier(
81
+ tableSchema(table),
82
+ "Sanitization policy table schema",
83
+ );
84
+ const tableName = assertIdentifier(
85
+ table?.name,
86
+ "Sanitization policy table name",
87
+ );
88
+ const key =
89
+ schemaName === "public" ? tableName : `${schemaName}.${tableName}`;
90
+ if (tables.has(key)) {
91
+ throw new Error(`Sanitization policy duplicates table ${key}.`);
92
+ }
93
+ tables.add(key);
94
+ if (!["STREAM AND SANITIZE", "EXCLUDE"].includes(table.sourceRows)) {
95
+ throw new Error(
96
+ `Sanitization policy table ${tableName} has an invalid sourceRows decision.`,
97
+ );
98
+ }
99
+ if (!Array.isArray(table.columns) || table.columns.length === 0) {
100
+ throw new Error(
101
+ `Sanitization policy table ${tableName} must list its columns.`,
102
+ );
103
+ }
104
+ const columns = new Set();
105
+ for (const column of table.columns) {
106
+ const columnName = assertIdentifier(
107
+ column?.name,
108
+ `Sanitization policy column in ${tableName}`,
109
+ );
110
+ if (columns.has(columnName)) {
111
+ throw new Error(
112
+ `Sanitization policy duplicates column ${tableName}.${columnName}.`,
113
+ );
114
+ }
115
+ columns.add(columnName);
116
+ normalizeSanitizationAction(column.action);
117
+ if (!["ALWAYS", "NEVER"].includes(column.generated)) {
118
+ throw new Error(
119
+ `Sanitization policy column ${tableName}.${columnName} must classify generated as ALWAYS or NEVER.`,
120
+ );
121
+ }
122
+ if (!["YES", "NO"].includes(column.identity)) {
123
+ throw new Error(
124
+ `Sanitization policy column ${tableName}.${columnName} must classify identity as YES or NO.`,
125
+ );
126
+ }
127
+ if (column.foreignKey !== null) {
128
+ if (
129
+ !column.foreignKey ||
130
+ typeof column.foreignKey !== "object" ||
131
+ Array.isArray(column.foreignKey)
132
+ ) {
133
+ throw new Error(
134
+ `Sanitization policy column ${tableName}.${columnName} has an invalid foreignKey decision.`,
135
+ );
136
+ }
137
+ assertIdentifier(
138
+ column.foreignKey.schema,
139
+ `Foreign-key schema for ${tableName}.${columnName}`,
140
+ );
141
+ assertIdentifier(
142
+ column.foreignKey.table,
143
+ `Foreign-key table for ${tableName}.${columnName}`,
144
+ );
145
+ assertIdentifier(
146
+ column.foreignKey.column,
147
+ `Foreign-key column for ${tableName}.${columnName}`,
148
+ );
149
+ }
150
+ }
151
+ }
152
+ return policy;
153
+ };
154
+
155
+ export const readBoundRuntimeSanitizationPolicy = ({
156
+ bytes,
157
+ expectedSha256,
158
+ }) => {
159
+ const source = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
160
+ const policy = validateRuntimeSanitizationPolicy(
161
+ JSON.parse(source.toString("utf8")),
162
+ );
163
+ const actualSha256 = createHash("sha256").update(source).digest("hex");
164
+ if (actualSha256 !== expectedSha256) {
165
+ throw new Error(
166
+ "The active sanitization policy does not match the reviewed baseline policy checksum.",
167
+ );
168
+ }
169
+ return policy;
170
+ };
171
+
47
172
  const indexTables = (tables, label) => {
48
173
  if (!Array.isArray(tables)) throw new Error(`${label} must be an array.`);
49
174
  const index = new Map();
50
175
  for (const table of tables) {
176
+ const schema = assertIdentifier(
177
+ tableSchema(table),
178
+ `${label} table schema`,
179
+ );
51
180
  const name = assertIdentifier(table?.name, `${label} table name`);
52
- if (index.has(name)) throw new Error(`${label} duplicates table ${name}.`);
181
+ const key = schema === "public" ? name : `${schema}.${name}`;
182
+ if (index.has(key)) throw new Error(`${label} duplicates table ${key}.`);
53
183
  if (!Array.isArray(table.columns)) {
54
184
  throw new Error(`${label} table ${name} must list its columns.`);
55
185
  }
@@ -64,7 +194,7 @@ const indexTables = (tables, label) => {
64
194
  }
65
195
  columns.set(columnName, column);
66
196
  }
67
- index.set(name, { table, columns });
197
+ index.set(key, { table: { ...table, schema }, columns });
68
198
  }
69
199
  return index;
70
200
  };
@@ -107,7 +237,7 @@ export const validateSanitizationCoverage = ({ policy, schemaTables }) => {
107
237
  }
108
238
  normalizedTables.push({
109
239
  ...policyEntry.table,
110
- name: tableName,
240
+ schema: tableSchema(policyEntry.table),
111
241
  columns: normalizedColumns,
112
242
  });
113
243
  }
@@ -7,7 +7,7 @@ import { spawnSync } from "node:child_process";
7
7
  import { createHash } from "node:crypto";
8
8
  import { chmod, mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
9
9
  import { basename, join, resolve, sep } from "node:path";
10
- import { createCleanProcessEnvironment } from "./process_environment.mjs";
10
+ import { createCleanProcessEnvironment } from "../shared/process_environment.mjs";
11
11
 
12
12
  const maximumSchemaBytes = 64 * 1024 * 1024;
13
13
 
@@ -0,0 +1,123 @@
1
+ /** Parse and classify the stable Rehearsal command-line contract. */
2
+
3
+ export const parseArguments = (arguments_) => {
4
+ const flags = {
5
+ json: false,
6
+ help: false,
7
+ verbosity: "normal",
8
+ dryRun: false,
9
+ write: false,
10
+ plain: false,
11
+ configPath: undefined,
12
+ confirmation: undefined,
13
+ recordsPath: undefined,
14
+ ledgerPath: undefined,
15
+ assetsPath: undefined,
16
+ target: undefined,
17
+ includeRuntime: false,
18
+ includeImages: false,
19
+ cleanupConfirmation: undefined,
20
+ sourceAccessConfirmation: undefined,
21
+ sourceRetirementConfirmation: undefined,
22
+ refreshConfirmation: undefined,
23
+ identityConfirmation: undefined,
24
+ identityName: undefined,
25
+ runtimeTopology: undefined,
26
+ targetConfirmations: undefined,
27
+ exitGuidedSession: false,
28
+ };
29
+ const positionals = [];
30
+ for (const argument of arguments_) {
31
+ if (argument === "--json") flags.json = true;
32
+ else if (argument === "--help" || argument === "-h") flags.help = true;
33
+ else if (argument === "--verbose") flags.verbosity = "verbose";
34
+ else if (argument === "--debug") flags.verbosity = "debug";
35
+ else if (argument === "--dry-run") flags.dryRun = true;
36
+ else if (argument === "--write") flags.write = true;
37
+ else if (argument === "--plain") flags.plain = true;
38
+ else if (argument.startsWith("--config="))
39
+ flags.configPath = argument.slice("--config=".length);
40
+ else if (argument.startsWith("--confirm-candidates="))
41
+ flags.confirmation = argument.slice("--confirm-candidates=".length);
42
+ else if (argument.startsWith("--confirm-cleanup="))
43
+ flags.cleanupConfirmation = argument.slice("--confirm-cleanup=".length);
44
+ else if (argument.startsWith("--confirm-source-access="))
45
+ flags.sourceAccessConfirmation = argument.slice(
46
+ "--confirm-source-access=".length,
47
+ );
48
+ else if (argument.startsWith("--confirm-source-retirement="))
49
+ flags.sourceRetirementConfirmation = argument.slice(
50
+ "--confirm-source-retirement=".length,
51
+ );
52
+ else if (argument.startsWith("--confirm-refresh="))
53
+ flags.refreshConfirmation = argument.slice("--confirm-refresh=".length);
54
+ else if (argument.startsWith("--confirm-identity="))
55
+ flags.identityConfirmation = argument.slice("--confirm-identity=".length);
56
+ else if (argument.startsWith("--identity="))
57
+ flags.identityName = argument.slice("--identity=".length);
58
+ else if (argument.startsWith("--records="))
59
+ flags.recordsPath = argument.slice("--records=".length);
60
+ else if (argument.startsWith("--ledger="))
61
+ flags.ledgerPath = argument.slice("--ledger=".length);
62
+ else if (argument.startsWith("--assets="))
63
+ flags.assetsPath = argument.slice("--assets=".length);
64
+ else if (argument.startsWith("--target="))
65
+ flags.target = argument.slice("--target=".length);
66
+ else if (argument === "--include-runtime") flags.includeRuntime = true;
67
+ else if (argument === "--include-images") flags.includeImages = true;
68
+ else if (argument.startsWith("--"))
69
+ throw new Error(`Unknown Rehearsal option: ${argument}.`);
70
+ else positionals.push(argument);
71
+ }
72
+ return { flags, positionals };
73
+ };
74
+
75
+ export const resetGuidedFlags = (flags) => {
76
+ Object.assign(flags, {
77
+ dryRun: false,
78
+ write: false,
79
+ confirmation: undefined,
80
+ recordsPath: undefined,
81
+ ledgerPath: undefined,
82
+ assetsPath: undefined,
83
+ target: undefined,
84
+ includeRuntime: false,
85
+ includeImages: false,
86
+ cleanupConfirmation: undefined,
87
+ sourceAccessConfirmation: undefined,
88
+ sourceRetirementConfirmation: undefined,
89
+ refreshConfirmation: undefined,
90
+ identityConfirmation: undefined,
91
+ identityName: undefined,
92
+ runtimeTopology: undefined,
93
+ targetConfirmations: undefined,
94
+ exitGuidedSession: false,
95
+ setupPlan: undefined,
96
+ preparationPlan: undefined,
97
+ cleanupPlan: undefined,
98
+ });
99
+ };
100
+
101
+ export const commandMutatesProject = ({ command, flags, guided }) => {
102
+ if (guided) return true;
103
+ if (["setup", "init", "baseline prepare", "privacy key"].includes(command))
104
+ return flags.write;
105
+ if (command === "cleanup") return flags.write;
106
+ if (command === "source apply")
107
+ return Boolean(flags.sourceAccessConfirmation);
108
+ if (command === "source retire")
109
+ return Boolean(flags.sourceRetirementConfirmation);
110
+ if (command === "refresh") return Boolean(flags.refreshConfirmation);
111
+ if (command === "identity claim") return Boolean(flags.identityConfirmation);
112
+ if (command === "run") return !flags.dryRun;
113
+ return [
114
+ "baseline create",
115
+ "baseline refresh",
116
+ "open",
117
+ "start",
118
+ "migrate",
119
+ "reset",
120
+ "stop",
121
+ "discard",
122
+ ].includes(command);
123
+ };