@bygghammar/cms-platform 0.91.1 → 0.92.1

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.
@@ -0,0 +1,203 @@
1
+ /**
2
+ * The canonical form contract v3 hashes, written twice on purpose.
3
+ *
4
+ * A review plan is produced by the command-line tool and applied by the
5
+ * database. Both sides have to answer "is this still the plan that was
6
+ * reviewed?" the same way, and neither can see the other's code — the tool
7
+ * runs on an operator's laptop against a database that may be several releases
8
+ * old. So the agreement is a *format*, defined once in prose, implemented here
9
+ * and mirrored in SQL by `cms_canonical_json`, with a parity test that hashes
10
+ * one corpus on both sides and compares every answer.
11
+ *
12
+ * `JSON.stringify` cannot be that format. It preserves insertion order, so two
13
+ * objects that are equal produce different text, and the digest of a plan would
14
+ * depend on the order a query happened to return its columns in.
15
+ *
16
+ * The format:
17
+ *
18
+ * null, true, false literally, lowercase
19
+ * strings the JSON string literal, minimal escapes
20
+ * numbers whole numbers only, no exponent, no negative zero
21
+ * arrays [a,b,c] — order preserved, no spaces
22
+ * objects {"a":1,"b":2} — keys ascending by code point
23
+ *
24
+ * Two restrictions carry their weight. Fractional numbers are refused because
25
+ * PostgreSQL holds them as `numeric` and JavaScript as a double, and no
26
+ * spelling of 0.1 is the same value in both — a digest containing one could
27
+ * never be *proved* equal, only observed to match so far. Object keys are held
28
+ * to ASCII because PostgreSQL orders text by byte and JavaScript orders it by
29
+ * UTF-16 code unit, and above the basic plane those two orders disagree. Every
30
+ * key contract v3 digests is a literal in this repository, so the restriction
31
+ * costs nothing and turns a hope into a guarantee.
32
+ */
33
+
34
+ /** Values the canonical form accepts. Anything else is refused, loudly. */
35
+ export type DigestValue =
36
+ | null
37
+ | boolean
38
+ | number
39
+ | string
40
+ | readonly DigestValue[]
41
+ | { readonly [key: string]: DigestValue };
42
+
43
+ /**
44
+ * A value that cannot be digested.
45
+ *
46
+ * Its own class because callers act on it: the CLI turns it into "this plan
47
+ * contains something the database could not have produced" rather than letting
48
+ * a `TypeError` surface as a crash.
49
+ */
50
+ export class CanonicalJsonError extends Error {
51
+ readonly path: string;
52
+
53
+ constructor(message: string, path: string) {
54
+ super(path ? `${message} (at ${path || "$"})` : message);
55
+ this.name = "CanonicalJsonError";
56
+ this.path = path;
57
+ }
58
+ }
59
+
60
+ /** Mirrors the pattern `cms_canonical_json` enforces on object keys. */
61
+ const KEY_PATTERN = /^[A-Za-z0-9_.:-]+$/;
62
+
63
+ /** Both sides can name every integer in this range exactly, and none beyond it. */
64
+ const MAX_EXACT_INTEGER = 9007199254740991;
65
+
66
+ /**
67
+ * A lone surrogate is a string JavaScript can hold and PostgreSQL cannot: it is
68
+ * not valid UTF-8, so `jsonb` refuses it. Refusing it here keeps the two sides
69
+ * describing the same set of inputs rather than the same subset of one.
70
+ */
71
+ function hasLoneSurrogate(text: string): boolean {
72
+ for (let i = 0; i < text.length; i += 1) {
73
+ const code = text.charCodeAt(i);
74
+ if (code >= 0xd800 && code <= 0xdbff) {
75
+ const next = text.charCodeAt(i + 1);
76
+ if (Number.isNaN(next) || next < 0xdc00 || next > 0xdfff) return true;
77
+ i += 1;
78
+ } else if (code >= 0xdc00 && code <= 0xdfff) {
79
+ return true;
80
+ }
81
+ }
82
+ return false;
83
+ }
84
+
85
+ function isPlainObject(value: object): boolean {
86
+ const proto = Object.getPrototypeOf(value);
87
+ return proto === Object.prototype || proto === null;
88
+ }
89
+
90
+ function write(value: unknown, path: string): string {
91
+ if (value === null) return "null";
92
+
93
+ switch (typeof value) {
94
+ case "boolean":
95
+ return value ? "true" : "false";
96
+
97
+ case "number": {
98
+ if (!Number.isFinite(value)) {
99
+ throw new CanonicalJsonError(`${String(value)} is not a finite number`, path);
100
+ }
101
+ if (!Number.isInteger(value)) {
102
+ throw new CanonicalJsonError(
103
+ `${String(value)} is fractional, and digest inputs may only contain whole numbers`,
104
+ path,
105
+ );
106
+ }
107
+ if (Math.abs(value) > MAX_EXACT_INTEGER) {
108
+ throw new CanonicalJsonError(
109
+ `${String(value)} is outside the range both sides can represent exactly (±${MAX_EXACT_INTEGER})`,
110
+ path,
111
+ );
112
+ }
113
+ // `String(-0)` is "0", which is what the SQL side produces too.
114
+ return String(value);
115
+ }
116
+
117
+ case "string": {
118
+ if (hasLoneSurrogate(value)) {
119
+ throw new CanonicalJsonError(
120
+ "string contains an unpaired surrogate, which PostgreSQL cannot store",
121
+ path,
122
+ );
123
+ }
124
+ return JSON.stringify(value);
125
+ }
126
+
127
+ case "undefined":
128
+ throw new CanonicalJsonError("undefined is not a JSON value", path);
129
+
130
+ case "bigint":
131
+ throw new CanonicalJsonError("bigint is not a JSON value", path);
132
+
133
+ case "function":
134
+ case "symbol":
135
+ throw new CanonicalJsonError(`${typeof value} is not a JSON value`, path);
136
+ }
137
+
138
+ if (Array.isArray(value)) {
139
+ const parts: string[] = [];
140
+ for (let i = 0; i < value.length; i += 1) {
141
+ parts.push(write(value[i], `${path}[${i}]`));
142
+ }
143
+ return `[${parts.join(",")}]`;
144
+ }
145
+
146
+ const object = value as object;
147
+ if (!isPlainObject(object)) {
148
+ const name = object.constructor?.name ?? "object";
149
+ throw new CanonicalJsonError(
150
+ `${name} is not a plain object; digest inputs carry data, not instances`,
151
+ path,
152
+ );
153
+ }
154
+
155
+ const keys = Object.keys(object).sort();
156
+ const parts: string[] = [];
157
+ for (const key of keys) {
158
+ if (!KEY_PATTERN.test(key)) {
159
+ throw new CanonicalJsonError(
160
+ `object key ${JSON.stringify(key)} must be ASCII letters, digits, underscore, dot, colon or hyphen`,
161
+ path,
162
+ );
163
+ }
164
+ const entry = (object as Record<string, unknown>)[key];
165
+ // An absent key and a key holding `undefined` are the same fact, and the
166
+ // database has no way to spell the second one. `JSON.stringify` drops it
167
+ // silently; dropping it silently here would mean two plans that differ
168
+ // digest the same.
169
+ if (entry === undefined) {
170
+ throw new CanonicalJsonError(
171
+ `object key ${JSON.stringify(key)} holds undefined; omit the key instead`,
172
+ path,
173
+ );
174
+ }
175
+ parts.push(`${JSON.stringify(key)}:${write(entry, path ? `${path}.${key}` : key)}`);
176
+ }
177
+ return `{${parts.join(",")}}`;
178
+ }
179
+
180
+ /** The canonical text for `value`. Throws `CanonicalJsonError` if it has none. */
181
+ export function canonicalJson(value: DigestValue): string {
182
+ return write(value, "");
183
+ }
184
+
185
+ const encoder = new TextEncoder();
186
+
187
+ function hex(bytes: ArrayBuffer): string {
188
+ return Array.from(new Uint8Array(bytes))
189
+ .map((byte) => byte.toString(16).padStart(2, "0"))
190
+ .join("");
191
+ }
192
+
193
+ /**
194
+ * SHA-256 of the canonical text, lowercase hex — the same string
195
+ * `cms_digest(jsonb)` returns for the same value.
196
+ *
197
+ * Async because it uses Web Crypto, which every runtime this package targets
198
+ * has and none of them offers synchronously.
199
+ */
200
+ export async function digestJson(value: DigestValue): Promise<string> {
201
+ const bytes = encoder.encode(canonicalJson(value));
202
+ return hex(await crypto.subtle.digest("SHA-256", bytes.slice().buffer as ArrayBuffer));
203
+ }
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Canonical migration catalog. GENERATED FILE — do not edit by hand.
3
+ *
4
+ * Regenerate with `bun run manifest` after adding, editing or removing a file
5
+ * under `migrations/`. CI runs `bun run manifest:check`, which renders this
6
+ * file again and fails when the result differs, so a migration cannot land
7
+ * without the catalog that describes it.
8
+ *
9
+ * This is what the package ships, never what a database has applied. The
10
+ * digests are of the files' bytes as published: a site holding a migration by
11
+ * the right name and the wrong contents is a different problem from a site
12
+ * missing it, and a list of names alone cannot tell an operator which they have.
13
+ *
14
+ * The accessors below are part of the generated text so that this module needs
15
+ * no imports of its own, which is what lets `ops/migration-smoke.mjs` load it
16
+ * directly under a plain `node`. Every other list of migration names in the
17
+ * package is derived from here: `SCHEMA_MIGRATIONS` in `../platform/contract`
18
+ * and in `../server/schema-drift`, and the named slices beside them.
19
+ */
20
+
21
+ export type CanonicalMigration = {
22
+ /** File name under `@bygghammar/cms-platform/migrations`. */
23
+ readonly name: string;
24
+ /** SHA-256 of the file's bytes, lowercase hex. */
25
+ readonly sha256: string;
26
+ };
27
+
28
+ /** Every canonical migration this package ships, oldest first. */
29
+ export const MIGRATION_MANIFEST: readonly CanonicalMigration[] = [
30
+ {
31
+ name: "20260728113741_f9744b19-078b-46e5-b00f-31c84aaf71ab.sql",
32
+ sha256: "afbdb034ddb11db5b394d3d2e227032ffbd00d666d270143279f632d6891be52",
33
+ },
34
+ {
35
+ name: "20260728113757_4156a886-21fb-4a1f-9c12-acb7bb145102.sql",
36
+ sha256: "c69f45d23cc496ab211971540790e85e789faa3803564e65caa1eb1c4744e35f",
37
+ },
38
+ {
39
+ name: "20260728113823_6e0237e1-99e3-4e1a-8a68-804b7bcdbbb8.sql",
40
+ sha256: "a174ab188b17426fafe1260d3efafbb760e138fa6cbafb50be1ed2ccf186a0d4",
41
+ },
42
+ {
43
+ name: "20260728115305_506d92b4-129f-4d2b-961f-cf76e592677e.sql",
44
+ sha256: "41e047be09010a797381532b973b2e583d1f82c87ed70cfa525f420ea9405553",
45
+ },
46
+ {
47
+ name: "20260728115320_717d4447-f433-4a1d-89d2-fc8f028d4ac6.sql",
48
+ sha256: "d51ea99f405efa3749429c0a8536e0be9fbd464795741dfb9c837e94beaca2df",
49
+ },
50
+ {
51
+ name: "20260728115347_c77f5a56-19db-4117-bb60-9ad692b817ab.sql",
52
+ sha256: "2730f1bdde50f9ae3a7cc9e03cccc66a5117a2cce1ce323902b32dd3a59da2e3",
53
+ },
54
+ {
55
+ name: "20260728132052_b692dcfe-a944-4a19-afd0-187f7600feb8.sql",
56
+ sha256: "fdea3be37894f6711572d132f7ef73bf5ba9f1692cba568b1e5fb274adc59815",
57
+ },
58
+ {
59
+ name: "20260728183318_05b05a47-ba05-4a66-ab8f-69f2688fea84.sql",
60
+ sha256: "e2205f3068511b2b3abb6045b69019c93f5485c1033868c42a1597c160615041",
61
+ },
62
+ {
63
+ name: "20260728193035_84b68d27-c3f3-4516-a3a9-15fc6f476163.sql",
64
+ sha256: "57c0a945cb58739501f90c7bf031627323665b615f01145e197158081a984618",
65
+ },
66
+ {
67
+ name: "20260728193707_8438ee2d-d589-4048-86e2-464fd209af80.sql",
68
+ sha256: "51395b9e5f5e2d4aa0885d43fd9f87851d5cd15d4103f7cb669819c6c740f7e5",
69
+ },
70
+ {
71
+ name: "20260729055312_08862d7e-0279-4d4f-8709-a59b9f068c55.sql",
72
+ sha256: "df857ab362eae6fdc28b277fe9839fc5e3fad493c89936c014c05747dc2d8a5c",
73
+ },
74
+ {
75
+ name: "20260729055348_418b7e36-4877-4304-831f-290ca73e56f3.sql",
76
+ sha256: "9db9f62b65088391619c48c114b790f3290038c8456d08ff7b347325af9a70d1",
77
+ },
78
+ {
79
+ name: "20260729055358_20b467e2-4efd-42c1-bd34-773ed6b77fc5.sql",
80
+ sha256: "2e28867a81056c300f598551f3be57df2d80eb59cb110af7c6cf639802e84363",
81
+ },
82
+ {
83
+ name: "20260729061039_306859eb-a38c-44ef-af1a-6c02a8fd84db.sql",
84
+ sha256: "708323f91501c041edcc39ba5bf6f57c3099b914be56c0a6fd9b75eb3b00de89",
85
+ },
86
+ {
87
+ name: "20260730065600_54c25861-3355-4600-a72f-3d67b457ecb1.sql",
88
+ sha256: "e1d20aa50731f4bc3e04d34bc3b1fbf5cfeb139e88f1d8fad5d790ae84f298cb",
89
+ },
90
+ {
91
+ name: "20260730071745_d4904314-15ff-4c58-ac66-85e7371e04b5.sql",
92
+ sha256: "f71f12dffb54a53b6ce81a4e578601776476cbf7654a86f44d57a9b587622a2b",
93
+ },
94
+ {
95
+ name: "20260730071914_48163aa3-830e-4dcc-bdd4-c35be282ab87.sql",
96
+ sha256: "de6c90c866baf4f4709909dc259c25b7203f225cd35faf931f9c814b228c8a69",
97
+ },
98
+ {
99
+ name: "20260730085949_bcfc3270-ea80-4617-86a3-2555ed34a280.sql",
100
+ sha256: "a35f1fc477662db7963e21110ba855f59ccea775068d0ddac721ff3ba4f71ede",
101
+ },
102
+ {
103
+ name: "20260730095256_c977c30e-b285-4041-8804-c0bd72b6d9b3.sql",
104
+ sha256: "3fa9fa8539b9657dc5eaddcf67615857938767081a1e29fad91084363e87d097",
105
+ },
106
+ {
107
+ name: "20260730100057_e341b887-eb0e-4a74-850e-0ee5ffb70d7a.sql",
108
+ sha256: "9558fd29329c840bb01197236b95f5079462be74ce0e0fd8299bc9444d079a43",
109
+ },
110
+ {
111
+ name: "20260730103703_d5c4e1b7-1e2c-4dd0-be4e-185c159ade69.sql",
112
+ sha256: "e7dec3c226316e361b887fb57364d03211b387d6f4ffb2f6f90176e2d796059b",
113
+ },
114
+ {
115
+ name: "20260730105635_3557b7da-919e-48db-8c92-8221d68d3cf9.sql",
116
+ sha256: "3ea4d412bc328a229fa4d41a2fc11e923ff933291f004e12488ea47724b72b34",
117
+ },
118
+ {
119
+ name: "20260730120308_12ff8cfb-43f9-4070-abde-61531ad3b75f.sql",
120
+ sha256: "745c590c691b8ddf9fa69d7407ee8ea9f16cbaade679e64343dee763d5c96675",
121
+ },
122
+ {
123
+ name: "20260730125024_1baf8926-12a2-48f4-8edc-7309941309e9.sql",
124
+ sha256: "db4004be7ea2f2d5fdfbf60b23775a399ebc4585176d99715cdfdb01594214e3",
125
+ },
126
+ {
127
+ name: "20260730173510_feb974d1-510e-4f52-9e20-5fd0582d8952.sql",
128
+ sha256: "4f655f5a571570225755683eee4ddb9fa19d2030c3d93ef9e23d44e69eceac60",
129
+ },
130
+ {
131
+ name: "20260730175409_9f60743f-f5e1-418c-a700-c91fb3704db8.sql",
132
+ sha256: "07e2e91b0dbe78b9be0995419aa8d6862574e74a875023969a485b053920fb8a",
133
+ },
134
+ {
135
+ name: "20260730181224_1c52ff16-c7a2-4ef1-a0a8-e493c2c9106f.sql",
136
+ sha256: "dcbd6605f888d5915b00fd7627025a7a4d30d7fe7729891545408769dbb4597a",
137
+ },
138
+ {
139
+ name: "20260730183129_933adb75-f985-4d82-ad5d-213b69fd42e5.sql",
140
+ sha256: "3dc3f5f9f8f3f705933de522b26c2b4d6259c069adb1a83fc8e6e94fdcad058b",
141
+ },
142
+ {
143
+ name: "20260730183918_7bc7a827-2100-49f1-bc6e-24509249350b.sql",
144
+ sha256: "27604b05d6cfcbb50a216c3e0bc9b0cb735254f1db6541f3dd6d30b634ead502",
145
+ },
146
+ {
147
+ name: "20260730202628_7ceade5e-644c-4bc2-91b8-594133329f31.sql",
148
+ sha256: "731cd859ce049d18990e19518151487ae638304d06b3c0b72a1a610f41e87441",
149
+ },
150
+ {
151
+ name: "20260730211056_57f42049-00b4-473b-a2cd-65a13570b8d2.sql",
152
+ sha256: "5b4b8206a80d604eb2e1d3ead880cb35612fdc27c6aec7142997b467a70f5ced",
153
+ },
154
+ {
155
+ name: "20260730214340_98af2e55-9031-4c11-9659-c47a37a8d6b7.sql",
156
+ sha256: "7140e202773ff6cc8f6b4086e8a44d3367d0b3cffced590667e18ab1dc8ea700",
157
+ },
158
+ {
159
+ name: "20260731091630_8a6f28ce-5833-4901-ab17-27648cef32ec.sql",
160
+ sha256: "01b71ae008885583ca9f737d17f5be2b48ff10c5a182f413065d34bf95186e37",
161
+ },
162
+ {
163
+ name: "20260731094716_4c68a1df-83ed-4ba8-8019-fa2fe2b9681e.sql",
164
+ sha256: "b23ecc1434b3c8769aa41919fea55d0c2dd5520dd0012294def8252270a9fe5a",
165
+ },
166
+ {
167
+ name: "20260731191655_fef97c3c-1397-47b4-8b56-afb020298e97.sql",
168
+ sha256: "842c19d4dd427e0681f72009d8162cce1a14d54e62665ea65d1a45dda9e8a313",
169
+ },
170
+ {
171
+ name: "20260731192212_95772a2f-19a1-4013-b463-2eb161e96737.sql",
172
+ sha256: "13dfff42c79d8f1907f9c5f5d2cdb03971943b1763fc31866f599713cb05dc46",
173
+ },
174
+ {
175
+ name: "20260731192815_c490653e-1232-4ad9-a150-3c83d2b90f5e.sql",
176
+ sha256: "4b88e2e9eff1a37a10b912912f1c96bb15393ca78541317530fe336f2bb51853",
177
+ },
178
+ {
179
+ name: "20260731193135_7966e740-5e86-4b14-8e1d-5119069666ba.sql",
180
+ sha256: "3cbafd3f06f5ed1fbc9a58bff9c5444ff52641b7dd3ca154059d6650a9727bee",
181
+ },
182
+ {
183
+ name: "20260731193245_599790d0-ca5e-4b5d-a3e7-d69b2a79e1a3.sql",
184
+ sha256: "0ed68fa7bbfce33aecb42552a8ba489bd1f079ed56bf1fc0ec9b6aac0504a11a",
185
+ },
186
+ {
187
+ name: "20260731212959_a748cb0d-4dc7-45dc-be90-f6bacbfab26c.sql",
188
+ sha256: "757f72f75b797dfcd494764223801e9d57acf4f7cf706c7d29d7afc839d4e734",
189
+ },
190
+ {
191
+ name: "20260801003057_1195f4c1-fb54-42dc-9718-638a25e6a82e.sql",
192
+ sha256: "e216bed54942b2f6bc0b6489ffe85bc955d5ceff077e52411d6cfc638ff1278c",
193
+ },
194
+ {
195
+ name: "20260801003300_fc591960-cb41-41cf-837d-2eef23b29ca9.sql",
196
+ sha256: "67c195b1b7ceffd30799b2b388a9ce3b97e7b9c06c42d97bf3f76802608a4de4",
197
+ },
198
+ {
199
+ name: "20260802151445_a56b0023-eaf7-41e2-a7fb-d4beddfc587c.sql",
200
+ sha256: "9801304f48179a0bb1a48d24414f94bba606704420cc3b5226a75fc997a0ccd2",
201
+ },
202
+ {
203
+ name: "20260802202621_d661af8a-8513-4b54-b375-f963ae824c8f.sql",
204
+ sha256: "a24330ff778dd2d02afd2aefe89378ea328b2678e03bd5928050f82132afc08f",
205
+ },
206
+ {
207
+ name: "20260802202702_c30cd837-744c-4641-85f1-edd3a7ebbcb6.sql",
208
+ sha256: "777fa5e80c5ca35ceb179b998ae37c1027e952a8b3c8043636c26e324b76c60a",
209
+ },
210
+ {
211
+ name: "20260802202720_4418d18d-c851-42dc-9345-78fc330128ae.sql",
212
+ sha256: "98f83c27dd7fd168095a5ba2e5c5548044b8740754491721d6f04bd49aaeedb2",
213
+ },
214
+ {
215
+ name: "20260803132514_78ba1f53-c85b-4457-96b1-7d49c5c8b494.sql",
216
+ sha256: "33b9f0f387be611efe68497a68727c0b90af50fc51e86eaf7318de3f48582c48",
217
+ },
218
+ {
219
+ name: "20260803181201_28c8aa09-baa8-431c-8338-90b1af9c8172.sql",
220
+ sha256: "f1b9befc75e0a23963288e1cdae0d135bfdcd4d77144e5cbeea9e76075ed2425",
221
+ },
222
+ {
223
+ name: "20260803201518_6e8a6028-b70f-4a7d-8bc9-95c86304e7ae.sql",
224
+ sha256: "4b03dfb3f9ef0f113da737150c5c8dda98a52b8ff6c738261b9e79c8be370b37",
225
+ },
226
+ {
227
+ name: "20260803201535_32a9a213-cb92-4035-b8cd-8211ca34af68.sql",
228
+ sha256: "4b03dfb3f9ef0f113da737150c5c8dda98a52b8ff6c738261b9e79c8be370b37",
229
+ },
230
+ {
231
+ name: "20260811103505_29df753d-14fa-4a05-8fea-f4207785fba3.sql",
232
+ sha256: "8f6e191dda1c203009fff2597a6751eff5b63e4ae9c846b31ccc2956b416b182",
233
+ },
234
+ {
235
+ name: "20260811130318_7cb95e95-9baa-40af-8f3f-de1d4e09cec4.sql",
236
+ sha256: "3fb8028377ce02dbb76baf13f4cffaf00f4618512ba127bc350857047f3c4ecc",
237
+ },
238
+ {
239
+ name: "20260818040722_1bdf1b8f-7fb0-4ae8-8ecf-73f1fadff3c2.sql",
240
+ sha256: "929600b9b5638f43615ed500bb3e095f745dd4951ad6795db26fbecdbd77b5ee",
241
+ },
242
+ {
243
+ name: "20260818040808_69a5e832-57e8-4aa7-aff8-7fd7777faa7c.sql",
244
+ sha256: "01ecbe30eccbea8539ed299f5eff4f1bb75fa95e155d1038153d3c241baebe5a",
245
+ },
246
+ {
247
+ name: "20260818040839_e6523163-bbf5-4a39-9233-91759059569a.sql",
248
+ sha256: "54203cc892a0093638e460821924d0261c51a63546516cc73d116d2222bba54c",
249
+ },
250
+ {
251
+ name: "20260902090000_dc072b25-8467-46ae-aef3-0ccdf40aa7eb.sql",
252
+ sha256: "aace4aa95e12cd655c109eb7c452c0fe65d445da6ac2f7648e94fcb57bec7251",
253
+ },
254
+ {
255
+ name: "20260904090000_c54603f8-a155-40f8-a86c-54434aa5c0ae.sql",
256
+ sha256: "082a25cb6f6be624cee5d245b196aee07b16cc4fa89892ed6bc527353a6b3f49",
257
+ },
258
+ {
259
+ name: "20260904140000_9f2a7c41-06d5-4d6b-8b4e-1c7a5d3e9b20.sql",
260
+ sha256: "cbb19c39fe238fb965458d81a35ffa3a063bed659d60606d580609a97fc1a9b8",
261
+ },
262
+ {
263
+ name: "20260904160000_0d565000-2839-4738-aacc-537d7090398f.sql",
264
+ sha256: "617f4d09014c664670928fe097438462464fc0aa6c0619474389dc5208ab86fa",
265
+ },
266
+ {
267
+ name: "20260905120000_da2db0b2-6048-4aa8-8e4f-13ee33e9cc24.sql",
268
+ sha256: "9dde032772a5824a4fb6a700223ac40144a30a7c23a83b3570af5b57f2c870a3",
269
+ },
270
+ ];
271
+
272
+ /** The same catalog as filenames, which is what applied-migration tooling records. */
273
+ export const MIGRATION_NAMES: readonly string[] = MIGRATION_MANIFEST.map((m) => m.name);
274
+
275
+ /**
276
+ * The digest this package expects a canonical migration's bytes to have.
277
+ *
278
+ * `undefined` for a name the package does not ship — a site-local migration,
279
+ * or one from a newer release. Advisory, like the rest of the manifest: it says
280
+ * what was published under that name, and a site comparing its own file against
281
+ * it learns whether it has the same file or merely the same name.
282
+ */
283
+ export function migrationDigest(name: string): string | undefined {
284
+ return MIGRATION_MANIFEST.find((m) => m.name === name)?.sha256;
285
+ }
286
+
287
+ /**
288
+ * A named part of the catalog, returned in applied order.
289
+ *
290
+ * Code that has to name specific migrations — the publication functions a
291
+ * failed RPC tells an operator to install, the files a smoke run holds back —
292
+ * says which ones and gets the catalog's own ordering back, so a caller cannot
293
+ * hand an operator two migrations in the wrong order by listing them that way.
294
+ *
295
+ * Naming a file the package does not ship is a packaging fault rather than a
296
+ * runtime condition: this catalog is written from the directory, the package's
297
+ * own suite resolves every slice, and CI regenerates and compares, so it cannot
298
+ * reach a published package. It throws because a catalog that does not contain
299
+ * a migration it names is not one a site should take upgrade instructions from.
300
+ */
301
+ export function migrationSlice(what: string, names: readonly string[]): readonly string[] {
302
+ const missing = names.filter((name) => !MIGRATION_NAMES.includes(name));
303
+ if (missing.length > 0) {
304
+ throw new Error(
305
+ `@bygghammar/cms-platform: the ${what} migrations name ${missing.join(", ")}, ` +
306
+ "which this package does not ship. Regenerate src/platform/migration-manifest.ts.",
307
+ );
308
+ }
309
+ return MIGRATION_NAMES.filter((name) => names.includes(name));
310
+ }