@rehearsal-db/core 0.1.0-beta.14 → 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 (162) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +1 -0
  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 +9 -3
  111. package/docs/index.md +1 -0
  112. package/docs/typescript.md +87 -0
  113. package/package.json +35 -29
  114. package/scripts/install/create_root_config.mjs +0 -19
  115. package/src/README.md +0 -17
  116. package/src/application/session.mjs +0 -435
  117. package/src/baseline/artifact.mjs +0 -684
  118. package/src/baseline/builder.mjs +0 -164
  119. package/src/baseline/input_discovery.mjs +0 -155
  120. package/src/baseline/policy_review.mjs +0 -92
  121. package/src/baseline/preparation.mjs +0 -323
  122. package/src/baseline/privacy_engine.mjs +0 -1399
  123. package/src/baseline/privacy_structured_json.mjs +0 -554
  124. package/src/baseline/sanitization_policy.mjs +0 -299
  125. package/src/baseline/schema_snapshot.mjs +0 -113
  126. package/src/cli/arguments.mjs +0 -126
  127. package/src/cli/guided.mjs +0 -814
  128. package/src/cli/init_command.mjs +0 -94
  129. package/src/cli/rehearsal.mjs +0 -972
  130. package/src/cli/renderers.mjs +0 -638
  131. package/src/cli/runtime_commands.mjs +0 -707
  132. package/src/cli/source_commands.mjs +0 -355
  133. package/src/cli/terminal.mjs +0 -275
  134. package/src/identity/claim.mjs +0 -1165
  135. package/src/identity/provider_configuration.mjs +0 -177
  136. package/src/identity/storage.mjs +0 -165
  137. package/src/project/configuration.mjs +0 -1337
  138. package/src/project/install_scaffold.mjs +0 -325
  139. package/src/project/setup.mjs +0 -474
  140. package/src/project/support_report.mjs +0 -105
  141. package/src/runtime/cleanup.mjs +0 -381
  142. package/src/runtime/migration_history.mjs +0 -220
  143. package/src/runtime/plan.mjs +0 -722
  144. package/src/runtime/policy.mjs +0 -438
  145. package/src/runtime/restore.mjs +0 -337
  146. package/src/runtime/service_environment.mjs +0 -82
  147. package/src/runtime/topology.mjs +0 -177
  148. package/src/shared/diagnostics.mjs +0 -207
  149. package/src/shared/node_runtime.mjs +0 -31
  150. package/src/shared/operation_guard.mjs +0 -162
  151. package/src/shared/process_environment.mjs +0 -68
  152. package/src/source/access.mjs +0 -798
  153. package/src/source/asset_transfer.mjs +0 -259
  154. package/src/source/baseline.mjs +0 -482
  155. package/src/source/external_reader.mjs +0 -368
  156. package/src/source/postgresql_access.mjs +0 -638
  157. package/src/source/privacy_paths.mjs +0 -38
  158. package/src/targets/postgresql.mjs +0 -808
  159. package/src/targets/supabase.mjs +0 -840
  160. package/src/targets/supabase_environment.mjs +0 -229
  161. package/src/targets/target.mjs +0 -66
  162. /package/{src/project → types}/configuration.d.mts +0 -0
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Purpose: Write, verify, and atomically activate immutable sanitized Rehearsal
3
+ * baseline generations. Do not run directly; this module is reusable script
4
+ * infrastructure.
5
+ */
6
+ import { type MigrationLedgerEntry } from "../runtime/migration_history.mjs";
7
+ type ByteChunk = string | Uint8Array;
8
+ type ByteStream = Iterable<ByteChunk> | AsyncIterable<ByteChunk>;
9
+ export interface BaselineRecord {
10
+ readonly schema?: string;
11
+ readonly table: string;
12
+ readonly row: Readonly<Record<string, unknown>>;
13
+ }
14
+ export interface BaselineMigrationFile {
15
+ readonly filename: string;
16
+ readonly content: string;
17
+ readonly fileSha256?: string;
18
+ readonly version?: string;
19
+ readonly name?: string;
20
+ }
21
+ export interface BaselineStorageAsset {
22
+ readonly bucket: string;
23
+ readonly objectPath: string;
24
+ readonly contentType?: string;
25
+ readonly content: Buffer | ByteStream;
26
+ }
27
+ export interface BaselineMetadata extends Record<string, unknown> {
28
+ readonly migrationCutoff: string;
29
+ readonly migrationHistorySha256: string;
30
+ readonly sanitizationPolicySha256: string;
31
+ readonly sourceMigrationHistory?: readonly MigrationLedgerEntry[];
32
+ }
33
+ interface ContentReceipt {
34
+ readonly sha256: string;
35
+ readonly bytes: number;
36
+ }
37
+ interface StorageAssetReceipt extends ContentReceipt {
38
+ readonly bucket: string;
39
+ readonly objectPath: string;
40
+ readonly contentType: string;
41
+ }
42
+ export interface BaselineManifest extends BaselineMetadata {
43
+ readonly formatVersion: 1 | 2;
44
+ readonly generationId: string;
45
+ readonly rowCount: number;
46
+ readonly tableCounts: Readonly<Record<string, number>>;
47
+ readonly files: Readonly<Record<string, Readonly<{
48
+ sha256: string;
49
+ rows?: number;
50
+ bytes?: number;
51
+ }>>>;
52
+ readonly storageAssets: Readonly<Record<string, StorageAssetReceipt>>;
53
+ readonly migrations: Readonly<Record<string, Readonly<{
54
+ sha256: string;
55
+ }>>>;
56
+ }
57
+ export interface ActiveBaselinePaths {
58
+ readonly artifactRoot: string;
59
+ readonly generationDirectory: string;
60
+ readonly dataPath: string;
61
+ readonly manifestPath: string;
62
+ readonly migrationsDirectory: string;
63
+ readonly schemaPath: string;
64
+ }
65
+ export interface BaselinePrunePlan {
66
+ readonly retained: string[];
67
+ readonly removed: string[];
68
+ }
69
+ export declare const createBaselineGenerationId: ({ now, }?: {
70
+ now?: Date;
71
+ }) => string;
72
+ export declare const createAndActivateBaseline: ({ artifactRoot, generationId, records, metadata, expectedTables, migrationFiles, assets, schemaSql, activate, }: {
73
+ artifactRoot: string;
74
+ generationId?: string;
75
+ records: Iterable<unknown> | AsyncIterable<unknown>;
76
+ metadata: BaselineMetadata;
77
+ expectedTables?: readonly string[];
78
+ migrationFiles?: readonly BaselineMigrationFile[] | Promise<readonly BaselineMigrationFile[]> | (() => readonly BaselineMigrationFile[] | Promise<readonly BaselineMigrationFile[]>);
79
+ assets?: Iterable<BaselineStorageAsset> | AsyncIterable<BaselineStorageAsset>;
80
+ schemaSql?: string | null;
81
+ activate?: boolean;
82
+ }) => Promise<BaselineManifest>;
83
+ export declare const activateBaselineGeneration: ({ artifactRoot, generationId, }: {
84
+ artifactRoot: string;
85
+ generationId: string;
86
+ }) => Promise<BaselineManifest>;
87
+ export declare const removeStagedBaselineGeneration: ({ artifactRoot, generationId, }: {
88
+ artifactRoot: string;
89
+ generationId: string;
90
+ }) => Promise<void>;
91
+ export declare const resolveActiveBaselinePaths: ({ artifactRoot, }: {
92
+ artifactRoot: string;
93
+ }) => Promise<ActiveBaselinePaths>;
94
+ export declare const verifyActiveBaseline: ({ artifactRoot, }: {
95
+ artifactRoot: string;
96
+ }) => Promise<BaselineManifest>;
97
+ export declare const listIncompleteBaselineBuilds: ({ artifactRoot, }: {
98
+ artifactRoot: string;
99
+ }) => Promise<string[]>;
100
+ export declare const planBaselineGenerationPrune: ({ artifactRoot, retain, }: {
101
+ artifactRoot: string;
102
+ retain?: number;
103
+ }) => Promise<BaselinePrunePlan>;
104
+ export declare const planBaselineReplacementPrune: ({ artifactRoot, retain, }: {
105
+ artifactRoot: string;
106
+ retain?: number;
107
+ }) => Promise<{
108
+ retainedAfterReplacement: string[];
109
+ removedAfterReplacement: string[];
110
+ }>;
111
+ export declare const pruneBaselineGenerations: ({ artifactRoot, retain, expectedRemoved, }: {
112
+ artifactRoot: string;
113
+ retain?: number;
114
+ expectedRemoved?: readonly string[];
115
+ }) => Promise<BaselinePrunePlan>;
116
+ export declare const removeBaselineArtifactRoot: ({ artifactRoot, }: {
117
+ artifactRoot: string;
118
+ }) => Promise<void>;
119
+ export {};
@@ -0,0 +1,522 @@
1
+ /**
2
+ * Purpose: Write, verify, and atomically activate immutable sanitized Rehearsal
3
+ * baseline generations. Do not run directly; this module is reusable script
4
+ * infrastructure.
5
+ */
6
+ import { createHash, randomBytes } from "node:crypto";
7
+ import { chmod, lstat, mkdir, open, readFile, readdir, readlink, rename, rm, symlink, } from "node:fs/promises";
8
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
9
+ import { createMigrationReplayReceipt, } from "../runtime/migration_history.mjs";
10
+ const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
11
+ const hasErrorCode = (error, code) => isObject(error) && error.code === code;
12
+ const isBaselineRecord = (value) => isObject(value) &&
13
+ (value.schema === undefined ||
14
+ (typeof value.schema === "string" &&
15
+ /^[a-z][a-z0-9_]{0,62}$/u.test(value.schema))) &&
16
+ typeof value.table === "string" &&
17
+ /^[a-z][a-z0-9_]{0,62}$/u.test(value.table) &&
18
+ isObject(value.row);
19
+ const isByteStream = (value) => value !== null &&
20
+ value !== undefined &&
21
+ (Symbol.iterator in Object(value) || Symbol.asyncIterator in Object(value));
22
+ const parseManifest = (source) => {
23
+ const parsed = JSON.parse(source);
24
+ if (!isObject(parsed)) {
25
+ throw new Error("A Rehearsal baseline manifest must be an object.");
26
+ }
27
+ return parsed;
28
+ };
29
+ const generationPattern = /^\d{8}T\d{6}Z-[a-f0-9]{12}$/u;
30
+ const buildPrefix = ".building-";
31
+ const generationsDirectoryName = "generations";
32
+ const currentLinkName = "current";
33
+ const migrationNamePattern = /^\d{14}_[a-z0-9_]+\.sql$/u;
34
+ const storageBucketPattern = /^[a-z0-9][a-z0-9.-]{0,99}$/u;
35
+ const maximumAssetBytes = 50 * 1024 * 1024;
36
+ const maximumTotalAssetBytes = 2 * 1024 * 1024 * 1024;
37
+ const relationPattern = /^(?:[a-z][a-z0-9_]{0,62}\.)?[a-z][a-z0-9_]{0,62}$/u;
38
+ const relationKey = (schema, table) => schema === "public" ? table : `${schema}.${table}`;
39
+ const canonicalJson = (value) => `${JSON.stringify(value, null, "\t")}\n`;
40
+ const assertGenerationId = (value) => {
41
+ if (typeof value !== "string") {
42
+ throw new Error("Invalid Rehearsal baseline generation identifier.");
43
+ }
44
+ if (!generationPattern.test(value)) {
45
+ throw new Error(`Invalid Rehearsal baseline generation identifier: ${value}`);
46
+ }
47
+ return value;
48
+ };
49
+ const assertArtifactRoot = (artifactRoot) => {
50
+ const resolved = resolve(artifactRoot);
51
+ if (basename(resolved) !== ".rehearsal" || resolved === sep) {
52
+ throw new Error(`Refusing an unsafe Rehearsal artifact root: ${artifactRoot}`);
53
+ }
54
+ return resolved;
55
+ };
56
+ const pathInside = (parent, child) => {
57
+ const childRelative = relative(parent, child);
58
+ return (childRelative !== "" &&
59
+ !childRelative.startsWith(`..${sep}`) &&
60
+ childRelative !== ".." &&
61
+ !childRelative.startsWith(sep));
62
+ };
63
+ const assertInsideRoot = (root, target) => {
64
+ if (!pathInside(root, target)) {
65
+ throw new Error(`Rehearsal artifact path escaped its root: ${target}`);
66
+ }
67
+ return target;
68
+ };
69
+ const writeImmutableFile = async (path, content) => {
70
+ const handle = await open(path, "wx", 0o600);
71
+ try {
72
+ await handle.writeFile(content);
73
+ await handle.sync();
74
+ }
75
+ finally {
76
+ await handle.close();
77
+ }
78
+ await chmod(path, 0o400);
79
+ };
80
+ const assetRelativePath = (bucket, objectPath) => {
81
+ if (!storageBucketPattern.test(bucket)) {
82
+ throw new Error(`Invalid Rehearsal Storage bucket: ${bucket}.`);
83
+ }
84
+ const segments = objectPath.split("/");
85
+ if (!objectPath ||
86
+ segments.some((segment) => !segment || segment === "." || segment === "..")) {
87
+ throw new Error(`Invalid Rehearsal Storage object path: ${objectPath}.`);
88
+ }
89
+ return join("storage", encodeURIComponent(bucket), ...segments.map(encodeURIComponent));
90
+ };
91
+ const verifyNoSymlink = async (path, label) => {
92
+ try {
93
+ const stats = await lstat(path);
94
+ if (stats.isSymbolicLink()) {
95
+ throw new Error(`${label} may not be a symbolic link.`);
96
+ }
97
+ }
98
+ catch (error) {
99
+ if (!hasErrorCode(error, "ENOENT"))
100
+ throw error;
101
+ }
102
+ };
103
+ const makeArtifactTreeWritable = async (path) => {
104
+ await chmod(path, 0o700).catch(() => undefined);
105
+ for (const entry of await readdir(path, { withFileTypes: true }).catch(() => [])) {
106
+ const child = join(path, entry.name);
107
+ if (entry.isDirectory())
108
+ await makeArtifactTreeWritable(child);
109
+ else if (!entry.isSymbolicLink())
110
+ await chmod(child, 0o600);
111
+ }
112
+ };
113
+ export const createBaselineGenerationId = ({ now = new Date(), } = {}) => {
114
+ const timestamp = now
115
+ .toISOString()
116
+ .replace(/\.\d{3}Z$/u, "Z")
117
+ .replaceAll("-", "")
118
+ .replaceAll(":", "");
119
+ return `${timestamp}-${randomBytes(6).toString("hex")}`;
120
+ };
121
+ export const createAndActivateBaseline = async ({ artifactRoot, generationId = createBaselineGenerationId(), records, metadata, expectedTables = [], migrationFiles = [], assets = [], schemaSql = null, activate = true, }) => {
122
+ const root = assertArtifactRoot(artifactRoot);
123
+ assertGenerationId(generationId);
124
+ if (!Array.isArray(expectedTables) ||
125
+ expectedTables.some((name) => !relationPattern.test(name)) ||
126
+ new Set(expectedTables).size !== expectedTables.length) {
127
+ throw new Error("Rehearsal expected table names are invalid or duplicated.");
128
+ }
129
+ const expectedTableSet = new Set(expectedTables);
130
+ await verifyNoSymlink(root, "The Rehearsal artifact root");
131
+ await mkdir(root, { recursive: true, mode: 0o700 });
132
+ const generationsRoot = assertInsideRoot(root, join(root, generationsDirectoryName));
133
+ await verifyNoSymlink(generationsRoot, "The Rehearsal generations directory");
134
+ await mkdir(generationsRoot, { recursive: true, mode: 0o700 });
135
+ const finalDirectory = assertInsideRoot(root, join(generationsRoot, generationId));
136
+ const buildDirectory = assertInsideRoot(root, join(root, `${buildPrefix}${generationId}-${randomBytes(4).toString("hex")}`));
137
+ const dataPath = join(buildDirectory, "sanitized-data.ndjson");
138
+ let dataHandle = null;
139
+ let finalCreated = false;
140
+ let rowCount = 0;
141
+ const tableCounts = new Map(expectedTables.map((name) => [name, 0]));
142
+ const dataHash = createHash("sha256");
143
+ try {
144
+ await mkdir(buildDirectory, { mode: 0o700 });
145
+ dataHandle = await open(dataPath, "wx", 0o600);
146
+ for await (const record of records) {
147
+ if (!isBaselineRecord(record)) {
148
+ throw new Error("A sanitized baseline record has an invalid shape.");
149
+ }
150
+ const key = relationKey(record.schema ?? "public", record.table);
151
+ if (expectedTableSet.size && !expectedTableSet.has(key)) {
152
+ throw new Error(`The baseline stream exposed unexpected table ${key}.`);
153
+ }
154
+ const line = `${JSON.stringify(record)}\n`;
155
+ dataHash.update(line);
156
+ await dataHandle.write(line, null, "utf8");
157
+ rowCount += 1;
158
+ tableCounts.set(key, (tableCounts.get(key) ?? 0) + 1);
159
+ }
160
+ await dataHandle.sync();
161
+ await dataHandle.close();
162
+ dataHandle = null;
163
+ await chmod(dataPath, 0o400);
164
+ let schemaReceipt;
165
+ if (schemaSql !== null) {
166
+ if (typeof schemaSql !== "string" || schemaSql.length === 0) {
167
+ throw new Error("A Rehearsal production schema snapshot is invalid.");
168
+ }
169
+ const schemaPath = join(buildDirectory, "production-schema.sql");
170
+ await writeImmutableFile(schemaPath, schemaSql);
171
+ schemaReceipt = {
172
+ sha256: createHash("sha256").update(schemaSql).digest("hex"),
173
+ bytes: Buffer.byteLength(schemaSql),
174
+ };
175
+ }
176
+ const resolvedMigrationFiles = typeof migrationFiles === "function"
177
+ ? await migrationFiles()
178
+ : await migrationFiles;
179
+ if (!Array.isArray(resolvedMigrationFiles)) {
180
+ throw new Error("Rehearsal migration source must be an array.");
181
+ }
182
+ if (!/^[a-f0-9]{64}$/u.test(metadata.migrationHistorySha256 ?? "")) {
183
+ throw new Error("A Rehearsal baseline requires a migration-history SHA-256.");
184
+ }
185
+ if (metadata.sourceMigrationHistory) {
186
+ const migrationReceipt = createMigrationReplayReceipt({
187
+ files: resolvedMigrationFiles,
188
+ ledger: metadata.sourceMigrationHistory,
189
+ });
190
+ if (migrationReceipt.historySha256 !== metadata.migrationHistorySha256) {
191
+ throw new Error("The Rehearsal baseline migration-history SHA-256 is invalid.");
192
+ }
193
+ }
194
+ const migrationDirectory = join(buildDirectory, "migrations");
195
+ await mkdir(migrationDirectory, { mode: 0o700 });
196
+ const migrationManifest = {};
197
+ for (const migration of resolvedMigrationFiles) {
198
+ if (!migration ||
199
+ !migrationNamePattern.test(migration.filename) ||
200
+ typeof migration.content !== "string" ||
201
+ Object.hasOwn(migrationManifest, migration.filename)) {
202
+ throw new Error("A Rehearsal migration bundle entry is invalid.");
203
+ }
204
+ const contentHash = createHash("sha256")
205
+ .update(migration.content)
206
+ .digest("hex");
207
+ if (migration.fileSha256 && migration.fileSha256 !== contentHash) {
208
+ throw new Error(`Rehearsal migration source checksum mismatch for ${migration.filename}.`);
209
+ }
210
+ await writeImmutableFile(join(migrationDirectory, migration.filename), migration.content);
211
+ migrationManifest[migration.filename] = { sha256: contentHash };
212
+ }
213
+ await chmod(migrationDirectory, 0o500);
214
+ const assetManifest = {};
215
+ let totalAssetBytes = 0;
216
+ for await (const asset of assets) {
217
+ if (!asset ||
218
+ typeof asset.bucket !== "string" ||
219
+ typeof asset.objectPath !== "string" ||
220
+ (!Buffer.isBuffer(asset.content) && !isByteStream(asset.content))) {
221
+ throw new Error("A Rehearsal Storage asset has an invalid shape.");
222
+ }
223
+ const relativePath = assetRelativePath(asset.bucket, asset.objectPath);
224
+ if (Object.hasOwn(assetManifest, relativePath)) {
225
+ throw new Error(`Duplicate Rehearsal Storage asset: ${relativePath}.`);
226
+ }
227
+ const target = assertInsideRoot(buildDirectory, join(buildDirectory, relativePath));
228
+ await mkdir(dirname(target), { recursive: true, mode: 0o700 });
229
+ const handle = await open(target, "wx", 0o600);
230
+ const hash = createHash("sha256");
231
+ let bytes = 0;
232
+ try {
233
+ const chunks = Buffer.isBuffer(asset.content)
234
+ ? [asset.content]
235
+ : asset.content;
236
+ for await (const chunk of chunks) {
237
+ const buffer = Buffer.from(chunk);
238
+ bytes += buffer.length;
239
+ totalAssetBytes += buffer.length;
240
+ if (bytes > maximumAssetBytes ||
241
+ totalAssetBytes > maximumTotalAssetBytes) {
242
+ throw new Error("The Rehearsal Storage asset boundary was exceeded.");
243
+ }
244
+ hash.update(buffer);
245
+ await handle.write(buffer);
246
+ }
247
+ await handle.sync();
248
+ }
249
+ finally {
250
+ await handle.close();
251
+ }
252
+ await chmod(target, 0o400);
253
+ assetManifest[relativePath] = {
254
+ sha256: hash.digest("hex"),
255
+ bytes,
256
+ bucket: asset.bucket,
257
+ objectPath: asset.objectPath,
258
+ contentType: asset.contentType ?? "application/octet-stream",
259
+ };
260
+ }
261
+ const manifest = {
262
+ ...metadata,
263
+ formatVersion: schemaReceipt ? 2 : 1,
264
+ generationId,
265
+ rowCount,
266
+ tableCounts: Object.fromEntries([...tableCounts].sort()),
267
+ files: {
268
+ "sanitized-data.ndjson": {
269
+ sha256: dataHash.digest("hex"),
270
+ rows: rowCount,
271
+ },
272
+ ...(schemaReceipt ? { "production-schema.sql": schemaReceipt } : {}),
273
+ },
274
+ storageAssets: assetManifest,
275
+ migrations: migrationManifest,
276
+ };
277
+ await writeImmutableFile(join(buildDirectory, "baseline-manifest.json"), canonicalJson(manifest));
278
+ await rename(buildDirectory, finalDirectory);
279
+ finalCreated = true;
280
+ await chmod(finalDirectory, 0o500);
281
+ if (activate) {
282
+ await activateBaselineGeneration({
283
+ artifactRoot: root,
284
+ generationId,
285
+ });
286
+ }
287
+ return manifest;
288
+ }
289
+ catch (error) {
290
+ let cleanupFailure;
291
+ if (dataHandle)
292
+ await dataHandle.close().catch(() => undefined);
293
+ await makeArtifactTreeWritable(buildDirectory).catch(() => undefined);
294
+ await rm(buildDirectory, { recursive: true, force: true }).catch((cleanupError) => {
295
+ cleanupFailure = cleanupError;
296
+ });
297
+ if (finalCreated) {
298
+ await makeArtifactTreeWritable(finalDirectory).catch(() => undefined);
299
+ await rm(finalDirectory, { recursive: true, force: true }).catch((cleanupError) => {
300
+ cleanupFailure ??= cleanupError;
301
+ });
302
+ }
303
+ if (cleanupFailure &&
304
+ error &&
305
+ (typeof error === "object" || typeof error === "function")) {
306
+ try {
307
+ Object.defineProperty(error, "cleanupError", {
308
+ value: cleanupFailure,
309
+ enumerable: false,
310
+ });
311
+ }
312
+ catch {
313
+ // Preserve the preparation failure even when its Error is immutable.
314
+ }
315
+ }
316
+ throw error;
317
+ }
318
+ };
319
+ export const activateBaselineGeneration = async ({ artifactRoot, generationId, }) => {
320
+ const root = assertArtifactRoot(artifactRoot);
321
+ assertGenerationId(generationId);
322
+ const generationDirectory = assertInsideRoot(root, join(root, generationsDirectoryName, generationId));
323
+ const manifest = parseManifest(await readFile(join(generationDirectory, "baseline-manifest.json"), "utf8"));
324
+ if (manifest.generationId !== generationId) {
325
+ throw new Error("The staged baseline identifier does not match its manifest.");
326
+ }
327
+ const temporaryLink = assertInsideRoot(root, join(root, `.current-${randomBytes(4).toString("hex")}`));
328
+ await symlink(join(generationsDirectoryName, generationId), temporaryLink);
329
+ await rename(temporaryLink, join(root, currentLinkName));
330
+ return manifest;
331
+ };
332
+ export const removeStagedBaselineGeneration = async ({ artifactRoot, generationId, }) => {
333
+ const root = assertArtifactRoot(artifactRoot);
334
+ assertGenerationId(generationId);
335
+ try {
336
+ const active = await resolveActiveBaselinePaths({ artifactRoot: root });
337
+ if (basename(active.generationDirectory) === generationId) {
338
+ throw new Error("Refusing to remove the active Rehearsal baseline generation.");
339
+ }
340
+ }
341
+ catch (error) {
342
+ if (!hasErrorCode(error, "ENOENT"))
343
+ throw error;
344
+ }
345
+ const target = assertInsideRoot(root, join(root, generationsDirectoryName, generationId));
346
+ await verifyNoSymlink(target, "A staged Rehearsal baseline generation");
347
+ await makeArtifactTreeWritable(target);
348
+ await rm(target, { recursive: true, force: true });
349
+ };
350
+ export const resolveActiveBaselinePaths = async ({ artifactRoot, }) => {
351
+ const root = assertArtifactRoot(artifactRoot);
352
+ const currentTarget = await readlink(join(root, currentLinkName));
353
+ const generationDirectory = resolve(root, currentTarget);
354
+ assertInsideRoot(root, generationDirectory);
355
+ if (dirname(generationDirectory) !== join(root, generationsDirectoryName)) {
356
+ throw new Error("The active Rehearsal baseline points outside generations.");
357
+ }
358
+ return {
359
+ artifactRoot: root,
360
+ generationDirectory,
361
+ dataPath: join(generationDirectory, "sanitized-data.ndjson"),
362
+ manifestPath: join(generationDirectory, "baseline-manifest.json"),
363
+ migrationsDirectory: join(generationDirectory, "migrations"),
364
+ schemaPath: join(generationDirectory, "production-schema.sql"),
365
+ };
366
+ };
367
+ export const verifyActiveBaseline = async ({ artifactRoot, }) => {
368
+ const paths = await resolveActiveBaselinePaths({ artifactRoot });
369
+ const { generationDirectory } = paths;
370
+ const manifest = parseManifest(await readFile(paths.manifestPath, "utf8"));
371
+ assertGenerationId(manifest.generationId);
372
+ if (basename(generationDirectory) !== manifest.generationId) {
373
+ throw new Error("The active Rehearsal baseline identifier does not match its path.");
374
+ }
375
+ const data = await readFile(paths.dataPath);
376
+ const actualHash = createHash("sha256").update(data).digest("hex");
377
+ if (actualHash !== manifest.files?.["sanitized-data.ndjson"]?.sha256) {
378
+ throw new Error("The active Rehearsal baseline data checksum is invalid.");
379
+ }
380
+ if (manifest.files?.["production-schema.sql"]) {
381
+ const schema = await readFile(paths.schemaPath);
382
+ const receipt = manifest.files["production-schema.sql"];
383
+ if (receipt === undefined || receipt.bytes === undefined) {
384
+ throw new Error("The active Rehearsal production schema receipt is invalid.");
385
+ }
386
+ if (schema.length !== receipt.bytes ||
387
+ createHash("sha256").update(schema).digest("hex") !== receipt.sha256) {
388
+ throw new Error("The active Rehearsal production schema checksum is invalid.");
389
+ }
390
+ }
391
+ const migrationDirectory = paths.migrationsDirectory;
392
+ const expectedMigrationNames = Object.keys(manifest.migrations ?? {}).sort();
393
+ const actualMigrationNames = (await readdir(migrationDirectory)).sort();
394
+ if (expectedMigrationNames.join("\0") !== actualMigrationNames.join("\0")) {
395
+ throw new Error("The active Rehearsal migration bundle is not exact.");
396
+ }
397
+ for (const filename of expectedMigrationNames) {
398
+ if (!migrationNamePattern.test(filename)) {
399
+ throw new Error("The active Rehearsal migration manifest is invalid.");
400
+ }
401
+ const migrationHash = createHash("sha256")
402
+ .update(await readFile(join(migrationDirectory, filename)))
403
+ .digest("hex");
404
+ if (migrationHash !== manifest.migrations[filename]?.sha256) {
405
+ throw new Error(`The active Rehearsal migration checksum is invalid for ${filename}.`);
406
+ }
407
+ }
408
+ for (const [relativePath, receipt] of Object.entries(manifest.storageAssets ?? {})) {
409
+ if (relativePath !== assetRelativePath(receipt.bucket, receipt.objectPath)) {
410
+ throw new Error("The active Rehearsal Storage manifest path is invalid.");
411
+ }
412
+ const assetPath = assertInsideRoot(generationDirectory, join(generationDirectory, relativePath));
413
+ const asset = await readFile(assetPath);
414
+ if (asset.length !== receipt.bytes ||
415
+ createHash("sha256").update(asset).digest("hex") !== receipt.sha256) {
416
+ throw new Error(`The active Rehearsal Storage checksum is invalid for ${relativePath}.`);
417
+ }
418
+ }
419
+ return manifest;
420
+ };
421
+ export const listIncompleteBaselineBuilds = async ({ artifactRoot, }) => {
422
+ const root = assertArtifactRoot(artifactRoot);
423
+ try {
424
+ return (await readdir(root)).filter((name) => name.startsWith(buildPrefix));
425
+ }
426
+ catch (error) {
427
+ if (hasErrorCode(error, "ENOENT"))
428
+ return [];
429
+ throw error;
430
+ }
431
+ };
432
+ export const planBaselineGenerationPrune = async ({ artifactRoot, retain = 2, }) => {
433
+ const root = assertArtifactRoot(artifactRoot);
434
+ if (!Number.isSafeInteger(retain) || retain < 1) {
435
+ throw new Error("Rehearsal baseline retention must be a positive integer.");
436
+ }
437
+ const active = await resolveActiveBaselinePaths({ artifactRoot: root });
438
+ const generationsRoot = join(root, generationsDirectoryName);
439
+ await verifyNoSymlink(generationsRoot, "The Rehearsal generations directory");
440
+ const entries = await readdir(generationsRoot, { withFileTypes: true });
441
+ for (const entry of entries) {
442
+ if (!entry.isDirectory() ||
443
+ entry.isSymbolicLink() ||
444
+ !generationPattern.test(entry.name)) {
445
+ throw new Error(`Refusing to prune an unexpected Rehearsal generation entry: ${entry.name}`);
446
+ }
447
+ }
448
+ const activeName = basename(active.generationDirectory);
449
+ const retained = new Set([
450
+ activeName,
451
+ ...entries
452
+ .map((entry) => entry.name)
453
+ .sort()
454
+ .reverse(),
455
+ ]
456
+ .filter((name, index, values) => values.indexOf(name) === index)
457
+ .slice(0, retain));
458
+ const removed = entries
459
+ .map((entry) => entry.name)
460
+ .filter((name) => !retained.has(name))
461
+ .sort();
462
+ return { retained: [...retained], removed };
463
+ };
464
+ export const planBaselineReplacementPrune = async ({ artifactRoot, retain = 2, }) => {
465
+ const root = assertArtifactRoot(artifactRoot);
466
+ if (!Number.isSafeInteger(retain) || retain < 1) {
467
+ throw new Error("Rehearsal baseline retention must be a positive integer.");
468
+ }
469
+ await resolveActiveBaselinePaths({ artifactRoot: root });
470
+ const generationsRoot = join(root, generationsDirectoryName);
471
+ await verifyNoSymlink(generationsRoot, "The Rehearsal generations directory");
472
+ const entries = await readdir(generationsRoot, { withFileTypes: true });
473
+ for (const entry of entries) {
474
+ if (!entry.isDirectory() ||
475
+ entry.isSymbolicLink() ||
476
+ !generationPattern.test(entry.name)) {
477
+ throw new Error(`Refusing to plan replacement around an unexpected Rehearsal generation entry: ${entry.name}`);
478
+ }
479
+ }
480
+ const current = entries
481
+ .map((entry) => entry.name)
482
+ .sort()
483
+ .reverse();
484
+ const retained = current.slice(0, Math.max(0, retain - 1));
485
+ const retainedSet = new Set(retained);
486
+ return {
487
+ retainedAfterReplacement: retained,
488
+ removedAfterReplacement: current
489
+ .filter((name) => !retainedSet.has(name))
490
+ .sort(),
491
+ };
492
+ };
493
+ export const pruneBaselineGenerations = async ({ artifactRoot, retain = 2, expectedRemoved, }) => {
494
+ const root = assertArtifactRoot(artifactRoot);
495
+ const plan = await planBaselineGenerationPrune({
496
+ artifactRoot: root,
497
+ retain,
498
+ });
499
+ const generationsRoot = join(root, generationsDirectoryName);
500
+ if (expectedRemoved &&
501
+ (expectedRemoved.length !== plan.removed.length ||
502
+ expectedRemoved.some((name, index) => name !== plan.removed[index]))) {
503
+ throw new Error("Rehearsal baseline generations changed after the cleanup preview.");
504
+ }
505
+ for (const name of plan.removed) {
506
+ const active = await resolveActiveBaselinePaths({ artifactRoot: root });
507
+ if (basename(active.generationDirectory) === name) {
508
+ throw new Error("Refusing to prune the active Rehearsal baseline generation.");
509
+ }
510
+ const target = assertInsideRoot(generationsRoot, join(generationsRoot, name));
511
+ await verifyNoSymlink(target, "A Rehearsal baseline generation");
512
+ await makeArtifactTreeWritable(target);
513
+ await rm(target, { recursive: true, force: true });
514
+ }
515
+ return plan;
516
+ };
517
+ export const removeBaselineArtifactRoot = async ({ artifactRoot, }) => {
518
+ const root = assertArtifactRoot(artifactRoot);
519
+ await verifyNoSymlink(root, "The Rehearsal artifact root");
520
+ await makeArtifactTreeWritable(root);
521
+ await rm(root, { recursive: true, force: true });
522
+ };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Build a synthetic baseline from explicit project-owned local files. This module does
3
+ * not connect to a hosted source or decide which values are safe.
4
+ */
5
+ interface SyntheticBaselineOptions {
6
+ readonly projectRoot?: string;
7
+ readonly configPath?: string;
8
+ readonly recordsPath: string;
9
+ readonly ledgerPath: string;
10
+ readonly assetsPath?: string;
11
+ }
12
+ export declare const createSyntheticBaselineFromFiles: ({ projectRoot, configPath, recordsPath, ledgerPath, assetsPath, }: SyntheticBaselineOptions) => Promise<{
13
+ generationId: string;
14
+ migrationCutoff: string;
15
+ migrationCount: number;
16
+ rowCount: number;
17
+ tableCount: number;
18
+ }>;
19
+ export {};