@oliphaunt/wasix-ts 0.1.0

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 (169) hide show
  1. package/ARCHITECTURE.md +655 -0
  2. package/CHANGELOG.md +33 -0
  3. package/LICENSE +21 -0
  4. package/README.md +404 -0
  5. package/THIRD_PARTY_NOTICES.md +20 -0
  6. package/lib/archive.d.ts +21 -0
  7. package/lib/archive.js +336 -0
  8. package/lib/asset-source.d.ts +4 -0
  9. package/lib/asset-source.js +15 -0
  10. package/lib/byte-channel.d.ts +27 -0
  11. package/lib/byte-channel.js +170 -0
  12. package/lib/client-common.d.ts +6 -0
  13. package/lib/client-common.js +36 -0
  14. package/lib/client.d.ts +7 -0
  15. package/lib/client.js +24 -0
  16. package/lib/database-root.d.ts +19 -0
  17. package/lib/database-root.js +139 -0
  18. package/lib/database.d.ts +135 -0
  19. package/lib/database.js +1039 -0
  20. package/lib/descriptor-validation.d.ts +10 -0
  21. package/lib/descriptor-validation.js +75 -0
  22. package/lib/direct-client-common.d.ts +50 -0
  23. package/lib/direct-client-common.js +717 -0
  24. package/lib/direct-client.d.ts +4 -0
  25. package/lib/direct-client.js +13 -0
  26. package/lib/direct.node.d.ts +2 -0
  27. package/lib/direct.node.js +2 -0
  28. package/lib/errors.d.ts +25 -0
  29. package/lib/errors.js +34 -0
  30. package/lib/extension-descriptor.d.ts +14 -0
  31. package/lib/extension-descriptor.js +419 -0
  32. package/lib/extensions.d.ts +68 -0
  33. package/lib/extensions.js +769 -0
  34. package/lib/host/LICENSE +21 -0
  35. package/lib/host/index.d.mts +123 -0
  36. package/lib/host/index.mjs +11 -0
  37. package/lib/host/provenance.json +16 -0
  38. package/lib/host/wasmer_js_bg.wasm +0 -0
  39. package/lib/host/worker.mjs +11 -0
  40. package/lib/host-runtime.d.ts +5 -0
  41. package/lib/host-runtime.js +13 -0
  42. package/lib/icu-descriptor.d.ts +3 -0
  43. package/lib/icu-descriptor.js +92 -0
  44. package/lib/index.bun.d.ts +2 -0
  45. package/lib/index.bun.js +2 -0
  46. package/lib/index.d.ts +2 -0
  47. package/lib/index.deno.d.ts +2 -0
  48. package/lib/index.deno.js +2 -0
  49. package/lib/index.js +2 -0
  50. package/lib/index.node.d.ts +2 -0
  51. package/lib/index.node.js +2 -0
  52. package/lib/internal-common.d.ts +12 -0
  53. package/lib/internal-common.js +274 -0
  54. package/lib/internal.d.ts +5 -0
  55. package/lib/internal.js +42 -0
  56. package/lib/internal.node.d.ts +5 -0
  57. package/lib/internal.node.js +8 -0
  58. package/lib/native-addon.d.ts +111 -0
  59. package/lib/native-addon.js +223 -0
  60. package/lib/native-server.d.ts +21 -0
  61. package/lib/native-server.js +109 -0
  62. package/lib/native-session.d.ts +60 -0
  63. package/lib/native-session.js +565 -0
  64. package/lib/node-actor.d.ts +7 -0
  65. package/lib/node-actor.js +10 -0
  66. package/lib/node-client-common.d.ts +9 -0
  67. package/lib/node-client-common.js +35 -0
  68. package/lib/node-client.d.ts +4 -0
  69. package/lib/node-client.js +13 -0
  70. package/lib/node-direct.d.ts +7 -0
  71. package/lib/node-direct.js +10 -0
  72. package/lib/node-worker-options.d.ts +5 -0
  73. package/lib/node-worker-options.js +65 -0
  74. package/lib/node-worker-port.d.ts +4 -0
  75. package/lib/node-worker-port.js +116 -0
  76. package/lib/node-worker.d.ts +1 -0
  77. package/lib/node-worker.js +38 -0
  78. package/lib/pgwire-connection.d.ts +60 -0
  79. package/lib/pgwire-connection.js +528 -0
  80. package/lib/pgwire.d.ts +3 -0
  81. package/lib/pgwire.js +105 -0
  82. package/lib/physical-archive.d.ts +29 -0
  83. package/lib/physical-archive.js +527 -0
  84. package/lib/protocol.d.ts +1 -0
  85. package/lib/protocol.js +1 -0
  86. package/lib/public.d.ts +4 -0
  87. package/lib/public.js +3 -0
  88. package/lib/query.d.ts +1 -0
  89. package/lib/query.js +1 -0
  90. package/lib/rpc.d.ts +203 -0
  91. package/lib/rpc.js +84 -0
  92. package/lib/runtime-descriptor.d.ts +3 -0
  93. package/lib/runtime-descriptor.js +79 -0
  94. package/lib/server.node.d.ts +1 -0
  95. package/lib/server.node.js +1 -0
  96. package/lib/startup-config.d.ts +2 -0
  97. package/lib/startup-config.js +19 -0
  98. package/lib/storage/bun.d.ts +6 -0
  99. package/lib/storage/bun.js +6 -0
  100. package/lib/storage/deno.d.ts +7 -0
  101. package/lib/storage/deno.js +7 -0
  102. package/lib/storage/incremental-storage.d.ts +25 -0
  103. package/lib/storage/incremental-storage.js +154 -0
  104. package/lib/storage/indexed-db-provider.d.ts +40 -0
  105. package/lib/storage/indexed-db-provider.js +259 -0
  106. package/lib/storage/indexed-db.d.ts +9 -0
  107. package/lib/storage/indexed-db.js +11 -0
  108. package/lib/storage/node.d.ts +10 -0
  109. package/lib/storage/node.js +13 -0
  110. package/lib/storage/opfs-pool.d.ts +31 -0
  111. package/lib/storage/opfs-pool.js +1271 -0
  112. package/lib/storage/opfs-provider.d.ts +4 -0
  113. package/lib/storage/opfs-provider.js +257 -0
  114. package/lib/storage/opfs.d.ts +8 -0
  115. package/lib/storage/opfs.js +10 -0
  116. package/lib/storage/restore-cleanup.d.ts +4 -0
  117. package/lib/storage/restore-cleanup.js +22 -0
  118. package/lib/storage/web-lock.d.ts +2 -0
  119. package/lib/storage/web-lock.js +64 -0
  120. package/lib/storage-provider.d.ts +47 -0
  121. package/lib/storage-provider.js +141 -0
  122. package/lib/storage-snapshot.d.ts +44 -0
  123. package/lib/storage-snapshot.js +274 -0
  124. package/lib/storage.d.ts +46 -0
  125. package/lib/storage.js +83 -0
  126. package/lib/tool-runtime.d.ts +43 -0
  127. package/lib/tool-runtime.js +93 -0
  128. package/lib/tool-worker-common.d.ts +48 -0
  129. package/lib/tool-worker-common.js +97 -0
  130. package/lib/tool-worker.d.ts +1 -0
  131. package/lib/tool-worker.js +10 -0
  132. package/lib/types.d.ts +230 -0
  133. package/lib/types.js +1 -0
  134. package/lib/wasix-runtime.d.ts +24 -0
  135. package/lib/wasix-runtime.js +186 -0
  136. package/lib/worker-client.d.ts +4 -0
  137. package/lib/worker-client.js +45 -0
  138. package/lib/worker-dispatch.d.ts +11 -0
  139. package/lib/worker-dispatch.js +174 -0
  140. package/lib/worker-entry.bun.d.ts +2 -0
  141. package/lib/worker-entry.bun.js +2 -0
  142. package/lib/worker-entry.d.ts +2 -0
  143. package/lib/worker-entry.deno.d.ts +2 -0
  144. package/lib/worker-entry.deno.js +2 -0
  145. package/lib/worker-entry.js +2 -0
  146. package/lib/worker-entry.node.d.ts +2 -0
  147. package/lib/worker-entry.node.js +2 -0
  148. package/lib/worker-node-client.d.ts +4 -0
  149. package/lib/worker-node-client.js +55 -0
  150. package/lib/worker-rpc.d.ts +36 -0
  151. package/lib/worker-rpc.js +422 -0
  152. package/lib/worker-transfer.d.ts +6 -0
  153. package/lib/worker-transfer.js +11 -0
  154. package/lib/worker.d.ts +1 -0
  155. package/lib/worker.js +21 -0
  156. package/lib/zstd.d.ts +4 -0
  157. package/lib/zstd.js +12 -0
  158. package/node_modules/@oliphaunt/js-core/README.md +7 -0
  159. package/node_modules/@oliphaunt/js-core/dist/commonjs/protocol.d.ts +1 -0
  160. package/node_modules/@oliphaunt/js-core/dist/commonjs/protocol.js +22 -0
  161. package/node_modules/@oliphaunt/js-core/dist/commonjs/query.d.ts +255 -0
  162. package/node_modules/@oliphaunt/js-core/dist/commonjs/query.js +2068 -0
  163. package/node_modules/@oliphaunt/js-core/dist/module/package.json +3 -0
  164. package/node_modules/@oliphaunt/js-core/dist/module/protocol.d.ts +1 -0
  165. package/node_modules/@oliphaunt/js-core/dist/module/protocol.js +19 -0
  166. package/node_modules/@oliphaunt/js-core/dist/module/query.d.ts +255 -0
  167. package/node_modules/@oliphaunt/js-core/dist/module/query.js +2039 -0
  168. package/node_modules/@oliphaunt/js-core/package.json +21 -0
  169. package/package.json +122 -0
@@ -0,0 +1,769 @@
1
+ import { clusterSeedMount, decompressIfNeeded, extractTar, layoutRuntimeSupport, loadAsset, } from './archive.js';
2
+ import { WASIX_PHYSICAL_IDENTITY } from './storage-provider.js';
3
+ const decoder = new TextDecoder('utf-8', { fatal: true });
4
+ const SQL_NAME = /^[a-z0-9][a-z0-9_-]*$/;
5
+ const SHA256 = /^[a-f0-9]{64}$/;
6
+ const SHARED_PRELOAD_LIBRARIES = 'shared_preload_libraries';
7
+ /**
8
+ * Loads and verifies one exact runtime/ICU/extension closure. The matching
9
+ * cluster seed stays lazy until an exclusively leased storage provider reports
10
+ * a new root. Selection materializes exact artifacts and required startup
11
+ * configuration; database-local installation remains explicit application SQL.
12
+ */
13
+ export async function prepareWasixRuntime(options) {
14
+ const descriptor = options.runtime;
15
+ const profile = options.icu === undefined ? 'standard' : 'icu';
16
+ const seedArchiveDescriptor = options.icu?.clusterSeedArchive ?? descriptor.standardSeedArchive;
17
+ const seedManifestDescriptor = options.icu?.clusterSeedManifest ?? descriptor.standardSeedManifest;
18
+ const prefetchedSeedAssets = options.storage.kind === 'memory'
19
+ ? loadClusterSeedAssets(profile, seedArchiveDescriptor, seedManifestDescriptor)
20
+ : undefined;
21
+ // Memory storage is always new, so overlap its required seed download with
22
+ // the runtime closure. Persistent providers must inspect storage first.
23
+ void prefetchedSeedAssets?.catch(() => undefined);
24
+ const [manifestBytes, runtimeBytes, icuDataBytes] = await Promise.all([
25
+ loadAsset(descriptor.manifest.source, 'WASIX asset manifest'),
26
+ loadAsset(descriptor.runtimeArchive.source, 'WASIX runtime archive'),
27
+ options.icu === undefined
28
+ ? Promise.resolve(undefined)
29
+ : loadAsset(options.icu.dataArchive.source, 'WASIX ICU data archive'),
30
+ ]);
31
+ await Promise.all([
32
+ assertDeclaredAssetBytes(manifestBytes, descriptor.manifest.size, descriptor.manifest.sha256, 'WASIX asset manifest'),
33
+ assertDeclaredAssetBytes(runtimeBytes, descriptor.runtimeArchive.size, descriptor.runtimeArchive.sha256, 'WASIX runtime archive'),
34
+ ...(options.icu === undefined || icuDataBytes === undefined
35
+ ? []
36
+ : [
37
+ assertDeclaredAssetBytes(icuDataBytes, options.icu.dataArchive.size, options.icu.dataArchive.sha256, 'WASIX ICU data archive'),
38
+ ]),
39
+ ]);
40
+ const manifest = parseWasixAssetManifest(manifestBytes);
41
+ assertRuntimeDescriptorMatchesManifest(descriptor, manifest);
42
+ const selectedSeed = manifest['cluster-seeds'][profile];
43
+ assertRuntimeArchiveMatchesManifest(seedArchiveDescriptor, selectedSeed, `WASIX ${profile} cluster seed archive`);
44
+ if (options.icu !== undefined) {
45
+ assertIcuDescriptorMatchesRuntime(options.runtime, options.icu, manifest);
46
+ }
47
+ const eagerClusterSeed = prefetchedSeedAssets === undefined
48
+ ? undefined
49
+ : loadClusterSeed(options, manifest, selectedSeed, profile, seedArchiveDescriptor, seedManifestDescriptor, prefetchedSeedAssets);
50
+ void eagerClusterSeed?.catch(() => undefined);
51
+ const runtime = extractTar(decompressIfNeeded(runtimeBytes));
52
+ if (options.icu !== undefined && icuDataBytes !== undefined) {
53
+ const icuArchive = extractTar(decompressIfNeeded(icuDataBytes));
54
+ overlayIcuArchive(runtime, icuArchive);
55
+ }
56
+ const layout = layoutRuntimeSupport(runtime);
57
+ assertExtensionCarriersCompatible(descriptor, manifest, options.extensionCarriers);
58
+ const resolved = resolveWasixExtensions(manifest, options.extensionCarriers, options.extensions);
59
+ assertExactCarrierClosure(resolved.extensions, options.extensionCarriers);
60
+ const loaded = await Promise.all(resolved.extensions.map(async (extension) => {
61
+ if (extension['unresolved-imports'].length > 0) {
62
+ throw new Error(`WASIX extension '${extension['sql-name']}' carrier has unresolved imports`);
63
+ }
64
+ const carrier = Object.hasOwn(options.extensionCarriers, extension['sql-name'])
65
+ ? options.extensionCarriers[extension['sql-name']]
66
+ : undefined;
67
+ if (carrier === undefined) {
68
+ throw new Error(`selected WASIX extension '${extension['sql-name']}' requires exact carrier ${extension.archive}`);
69
+ }
70
+ const bytes = await loadAsset(carrier.source, `WASIX extension ${extension['sql-name']} from ${carrier.product}@${carrier.version}`);
71
+ if (bytes.length !== extension.size) {
72
+ throw new Error(`WASIX extension '${extension['sql-name']}' carrier size mismatch: expected ${extension.size}, received ${bytes.length}`);
73
+ }
74
+ await assertSha256(bytes, extension.sha256, `WASIX extension '${extension['sql-name']}' carrier`);
75
+ return [extension, extractTar(decompressIfNeeded(bytes))];
76
+ }));
77
+ for (const [extension, archive] of loaded) {
78
+ overlayExtensionArchive(layout, archive, extension);
79
+ }
80
+ return {
81
+ layout,
82
+ loadClusterSeed: lazyClusterSeedLoader(options, manifest, selectedSeed, profile, seedArchiveDescriptor, seedManifestDescriptor, eagerClusterSeed),
83
+ moduleSha256: manifest.runtime['module-sha256'],
84
+ catalogProfile: profile,
85
+ icuEnabled: options.icu !== undefined,
86
+ startupGUCs: mergeExtensionStartupGUCs(options.startupGUCs, resolved.extensions),
87
+ physicalIdentity: WASIX_PHYSICAL_IDENTITY,
88
+ };
89
+ }
90
+ function lazyClusterSeedLoader(options, manifest, selectedSeed, profile, archiveDescriptor, manifestDescriptor, eager) {
91
+ let cached = eager;
92
+ const forgetRejectedAttempt = (attempt) => {
93
+ void attempt.catch(() => {
94
+ if (cached === attempt)
95
+ cached = undefined;
96
+ });
97
+ };
98
+ if (cached !== undefined)
99
+ forgetRejectedAttempt(cached);
100
+ return () => {
101
+ if (cached === undefined) {
102
+ const attempt = loadClusterSeed(options, manifest, selectedSeed, profile, archiveDescriptor, manifestDescriptor);
103
+ cached = attempt;
104
+ forgetRejectedAttempt(attempt);
105
+ }
106
+ return cached;
107
+ };
108
+ }
109
+ function loadClusterSeedAssets(profile, archiveDescriptor, manifestDescriptor) {
110
+ return Promise.all([
111
+ loadAsset(archiveDescriptor.source, `WASIX ${profile} cluster seed`),
112
+ loadAsset(manifestDescriptor.source, `WASIX ${profile} cluster seed manifest`),
113
+ ]);
114
+ }
115
+ async function loadClusterSeed(options, manifest, selectedSeed, profile, archiveDescriptor, manifestDescriptor, assets) {
116
+ const [archiveBytes, manifestBytes] = await (assets ??
117
+ loadClusterSeedAssets(profile, archiveDescriptor, manifestDescriptor));
118
+ await Promise.all([
119
+ assertDeclaredAssetBytes(archiveBytes, archiveDescriptor.size, archiveDescriptor.sha256, `WASIX ${profile} cluster seed`),
120
+ assertDeclaredAssetBytes(manifestBytes, manifestDescriptor.size, manifestDescriptor.sha256, `WASIX ${profile} cluster seed manifest`),
121
+ ]);
122
+ const seedManifest = parseClusterSeedManifest(manifestBytes, profile);
123
+ verifyClusterSeedIdentity(options, manifest, selectedSeed, seedManifest, archiveDescriptor);
124
+ const seed = extractTar(decompressIfNeeded(archiveBytes));
125
+ verifyPostgresIdentity(manifest, selectedSeed, seed.files.get('PG_VERSION'));
126
+ return clusterSeedMount(seed);
127
+ }
128
+ /** Require the package-authored archive identities to match the canonical manifest. */
129
+ export function assertRuntimeDescriptorMatchesManifest(descriptor, manifest) {
130
+ assertToolRuntimeDescriptorMatchesManifest(descriptor, manifest);
131
+ assertRuntimeArchiveMatchesManifest(descriptor.standardSeedArchive, manifest['cluster-seeds'].standard, 'WASIX standard cluster seed archive');
132
+ }
133
+ /** Verify only the runtime assets consumed by PostgreSQL frontend tools. */
134
+ export function assertToolRuntimeDescriptorMatchesManifest(descriptor, manifest) {
135
+ assertRuntimeArchiveMatchesManifest(descriptor.runtimeArchive, manifest.runtime, 'WASIX runtime archive');
136
+ }
137
+ export function assertExactCarrierClosure(resolved, carriers) {
138
+ const expected = new Set(resolved.map((extension) => extension['sql-name']));
139
+ const actual = new Set(Object.keys(carriers));
140
+ const missing = [...expected].filter((sqlName) => !actual.has(sqlName)).sort();
141
+ const unexpected = [...actual].filter((sqlName) => !expected.has(sqlName)).sort();
142
+ if (missing.length > 0 || unexpected.length > 0) {
143
+ throw new Error('WASIX extension carrier closure does not match imported dependency resolution' +
144
+ `${missing.length > 0 ? `; missing ${missing.join(', ')}` : ''}` +
145
+ `${unexpected.length > 0 ? `; unexpected ${unexpected.join(', ')}` : ''}`);
146
+ }
147
+ }
148
+ export function parseWasixAssetManifest(bytes) {
149
+ let parsed;
150
+ try {
151
+ parsed = JSON.parse(decodeUtf8(bytes));
152
+ }
153
+ catch (error) {
154
+ throw new Error(`WASIX asset manifest is not valid UTF-8 JSON: ${describeError(error)}`);
155
+ }
156
+ const manifest = requireObject(parsed, 'WASIX asset manifest');
157
+ if (manifest['format-version'] !== 2) {
158
+ throw new Error('WASIX asset manifest must use format-version 2');
159
+ }
160
+ const runtime = requireObject(manifest.runtime, 'WASIX asset manifest runtime');
161
+ requireAssetPath(runtime.archive, 'WASIX runtime archive');
162
+ requireSha256(runtime.sha256, 'WASIX runtime archive');
163
+ if (runtime.size !== undefined) {
164
+ requireSafeInteger(runtime.size, 'WASIX runtime archive size');
165
+ }
166
+ requireSha256(runtime['module-sha256'], 'WASIX runtime module');
167
+ requireString(runtime['postgres-version'], 'WASIX runtime PostgreSQL version');
168
+ const link = requireObject(runtime.link, 'WASIX runtime link metadata');
169
+ const exports = requireArray(link.exports, 'WASIX runtime exports');
170
+ for (const [index, value] of exports.entries()) {
171
+ const entry = requireObject(value, `WASIX runtime export ${index}`);
172
+ requireString(entry.name, `WASIX runtime export ${index} name`);
173
+ requireString(entry.kind, `WASIX runtime export ${index} kind`);
174
+ }
175
+ const seeds = requireObject(manifest['cluster-seeds'], 'WASIX asset manifest cluster-seeds');
176
+ requireExactKeys(seeds, ['icu', 'standard'], 'WASIX asset manifest cluster-seeds');
177
+ for (const profile of ['standard', 'icu']) {
178
+ const seed = requireObject(seeds[profile], `WASIX ${profile} cluster seed`);
179
+ const expectedRole = profile === 'standard' ? 'cluster-seed-standard' : 'cluster-seed-icu';
180
+ if (seed['catalog-profile'] !== profile || seed['artifact-role'] !== expectedRole) {
181
+ throw new Error(`WASIX ${profile} cluster seed has a mismatched profile or artifact role`);
182
+ }
183
+ requireAssetPath(seed.archive, `WASIX ${profile} cluster seed archive`);
184
+ requireAssetPath(seed.manifest, `WASIX ${profile} cluster seed manifest`);
185
+ requireSha256(seed.sha256, `WASIX ${profile} cluster seed archive`);
186
+ requireSafeInteger(seed.size, `WASIX ${profile} cluster seed archive size`);
187
+ requireSha256(seed['runtime-module-sha256'], `WASIX ${profile} seed runtime module`);
188
+ requireString(seed['postgres-version'], `WASIX ${profile} seed PostgreSQL version`);
189
+ requireString(seed['source-fingerprint'], `WASIX ${profile} seed source fingerprint`);
190
+ if (seed['physical-format'] !== 'wasix-pg18-v1' ||
191
+ seed['compatibility-key'] !== 'wasix-pg18-datum32-v1') {
192
+ throw new Error(`WASIX ${profile} cluster seed has an incompatible physical identity`);
193
+ }
194
+ if (profile === 'icu') {
195
+ requireSha256(seed['icu-data-tree-sha256'], 'WASIX ICU seed data tree');
196
+ }
197
+ else if (seed['icu-data-tree-sha256'] !== undefined) {
198
+ throw new Error('WASIX standard cluster seed must not identify ICU data');
199
+ }
200
+ }
201
+ requireString(manifest['source-fingerprint'], 'WASIX asset source fingerprint');
202
+ const runtimeSupport = requireArray(manifest['runtime-support'], 'WASIX runtime-support entries');
203
+ for (const [index, value] of runtimeSupport.entries()) {
204
+ const support = requireObject(value, `WASIX runtime-support entry ${index}`);
205
+ requireSqlName(support.name, `WASIX runtime-support entry ${index} name`);
206
+ requireInstallPath(support.path, `WASIX runtime-support entry ${index} path`);
207
+ requireSha256(support.sha256, `WASIX runtime-support entry ${index}`);
208
+ }
209
+ const extensions = requireArray(manifest.extensions, 'WASIX extension entries');
210
+ if (extensions.length !== 0) {
211
+ throw new Error('WASIX core asset manifest must not contain extension rows; import extension carriers explicitly');
212
+ }
213
+ return parsed;
214
+ }
215
+ export function resolveWasixExtensions(manifest, carriers, requested) {
216
+ const runtimeSupport = new Set(manifest['runtime-support'].map((entry) => entry.name));
217
+ const bySqlName = new Map(Object.entries(carriers).map(([sqlName, carrier]) => {
218
+ if (carrier.sqlName !== sqlName) {
219
+ throw new Error(`WASIX extension carrier map key '${sqlName}' does not match carrier SQL name '${carrier.sqlName}'`);
220
+ }
221
+ if (runtimeSupport.has(sqlName)) {
222
+ throw new Error(`WASIX extension carrier '${sqlName}' cannot replace runtime-provided support`);
223
+ }
224
+ return [sqlName, extensionFromCarrier(carrier)];
225
+ }));
226
+ const visiting = new Set();
227
+ const visited = new Set();
228
+ const runtimeDependencies = new Set();
229
+ const resolved = [];
230
+ const visit = (extension) => {
231
+ const sqlName = extension['sql-name'];
232
+ if (visited.has(sqlName)) {
233
+ return;
234
+ }
235
+ if (extension.lifecycle['shared-memory-required']) {
236
+ throw new Error(`selected WASIX extension '${sqlName}' requires shared-memory behavior that the @oliphaunt/wasix-ts host has not qualified`);
237
+ }
238
+ if (visiting.has(sqlName)) {
239
+ throw new Error(`cyclic WASIX extension dependency involving '${sqlName}'`);
240
+ }
241
+ visiting.add(sqlName);
242
+ for (const dependency of extension.dependencies) {
243
+ if (runtimeSupport.has(dependency)) {
244
+ runtimeDependencies.add(dependency);
245
+ }
246
+ else {
247
+ const dependencyExtension = bySqlName.get(dependency);
248
+ if (dependencyExtension !== undefined) {
249
+ visit(dependencyExtension);
250
+ continue;
251
+ }
252
+ throw new Error(`selected WASIX extension '${sqlName}' depends on unavailable extension '${dependency}'`);
253
+ }
254
+ }
255
+ visiting.delete(sqlName);
256
+ visited.add(sqlName);
257
+ resolved.push(extension);
258
+ };
259
+ for (const sqlName of [...new Set(requested)].sort()) {
260
+ requireSqlName(sqlName, 'selected WASIX extension');
261
+ const extension = bySqlName.get(sqlName);
262
+ if (extension === undefined) {
263
+ throw new Error(`selected WASIX extension '${sqlName}' has no imported carrier`);
264
+ }
265
+ visit(extension);
266
+ }
267
+ return {
268
+ extensions: resolved,
269
+ runtimeDependencies: [...runtimeDependencies].sort(),
270
+ };
271
+ }
272
+ function extensionFromCarrier(carrier) {
273
+ const lifecycle = carrier.install.lifecycle;
274
+ return {
275
+ name: carrier.install.name,
276
+ 'sql-name': carrier.sqlName,
277
+ archive: carrier.archive,
278
+ sha256: carrier.sha256,
279
+ size: carrier.size,
280
+ 'native-module': carrier.install.nativeModule,
281
+ 'native-modules': carrier.install.nativeModules,
282
+ dependencies: carrier.install.dependencies,
283
+ 'load-order': carrier.install.loadOrder,
284
+ lifecycle: {
285
+ 'create-extension': lifecycle.createExtension,
286
+ ...(lifecycle.createSchema === undefined ? {} : { 'create-schema': lifecycle.createSchema }),
287
+ 'load-sql': lifecycle.loadSql,
288
+ 'post-create-sql': lifecycle.postCreateSql,
289
+ 'startup-config': lifecycle.startupConfig,
290
+ 'shared-memory-required': lifecycle.sharedMemoryRequired,
291
+ },
292
+ 'installed-files': carrier.install.installedFiles,
293
+ 'unresolved-imports': carrier.install.unresolvedImports,
294
+ };
295
+ }
296
+ export function assertExtensionCarriersCompatible(runtime, manifest, carriers) {
297
+ const postgresMajor = manifest.runtime['postgres-version'].split('.')[0];
298
+ for (const carrier of Object.values(carriers)) {
299
+ const compatibility = carrier.compatibility;
300
+ if (compatibility.extensionRuntimeContract !== 'oliphaunt-extension-runtime-contract-v1') {
301
+ throw new Error(`WASIX extension '${carrier.sqlName}' has an unsupported extension runtime contract`);
302
+ }
303
+ if (compatibility.wasixRuntimeProduct !== runtime.product ||
304
+ compatibility.wasixRuntimeVersion !== runtime.version) {
305
+ throw new Error(`WASIX extension '${carrier.sqlName}' targets ${compatibility.wasixRuntimeProduct}@${compatibility.wasixRuntimeVersion}, not ${runtime.product}@${runtime.version}`);
306
+ }
307
+ if (compatibility.postgresMajor !== postgresMajor) {
308
+ throw new Error(`WASIX extension '${carrier.sqlName}' targets PostgreSQL ${compatibility.postgresMajor}, not ${postgresMajor}`);
309
+ }
310
+ }
311
+ const coreExports = new Set(manifest.runtime.link.exports
312
+ .filter((entry) => entry.kind === 'func' || entry.kind === 'global')
313
+ .map((entry) => entry.name));
314
+ for (const carrier of Object.values(carriers)) {
315
+ const missing = carrier.install.coreExportsRequired.filter((name) => !coreExports.has(name));
316
+ if (missing.length > 0) {
317
+ throw new Error(`WASIX extension '${carrier.sqlName}' requires exports absent from the selected core runtime: ${missing.join(', ')}`);
318
+ }
319
+ }
320
+ }
321
+ export function overlayExtensionArchive(layout, archive, extension) {
322
+ const sqlName = extension['sql-name'];
323
+ const expected = new Set(extension['installed-files']);
324
+ if (expected.size !== extension['installed-files'].length) {
325
+ throw new Error(`WASIX extension '${sqlName}' manifest repeats installed file paths`);
326
+ }
327
+ const actual = new Set(archive.files.keys());
328
+ const missing = [...expected].filter((path) => !actual.has(path));
329
+ const unexpected = [...actual].filter((path) => !expected.has(path));
330
+ if (missing.length > 0 || unexpected.length > 0) {
331
+ throw new Error(`WASIX extension '${sqlName}' archive contents do not match installed-files` +
332
+ `${missing.length > 0 ? `; missing ${missing.join(', ')}` : ''}` +
333
+ `${unexpected.length > 0 ? `; unexpected ${unexpected.join(', ')}` : ''}`);
334
+ }
335
+ for (const [path, bytes] of archive.files) {
336
+ const { mountPath, relative } = extensionMountTarget(path, sqlName);
337
+ const mount = layout.mounts[mountPath];
338
+ if (mount === undefined) {
339
+ throw new Error(`WASIX runtime is missing extension mount ${mountPath}`);
340
+ }
341
+ if (Object.hasOwn(mount.files, relative)) {
342
+ throw new Error(`WASIX extension '${sqlName}' collides with installed file ${path}`);
343
+ }
344
+ mount.files[relative] = bytes;
345
+ }
346
+ for (const path of archive.directories) {
347
+ if (path === 'lib' || path === 'share') {
348
+ continue;
349
+ }
350
+ const { mountPath, relative } = extensionMountTarget(path, sqlName, true);
351
+ const mount = layout.mounts[mountPath];
352
+ if (mount === undefined) {
353
+ throw new Error(`WASIX runtime is missing extension mount ${mountPath}`);
354
+ }
355
+ if (!mount.directories.includes(relative)) {
356
+ mount.directories.push(relative);
357
+ }
358
+ }
359
+ }
360
+ export function mergeExtensionStartupGUCs(configured, extensions) {
361
+ const merged = { ...configured };
362
+ const sharedPreloads = [];
363
+ const seenSharedPreloads = new Set();
364
+ appendCsv(merged[SHARED_PRELOAD_LIBRARIES], sharedPreloads, seenSharedPreloads);
365
+ for (const extension of extensions) {
366
+ for (const assignment of extension.lifecycle['startup-config']) {
367
+ const equals = assignment.indexOf('=');
368
+ const name = assignment.slice(0, equals).trim();
369
+ const value = assignment.slice(equals + 1).trim();
370
+ if (equals <= 0 || !/^[A-Za-z][A-Za-z0-9_.]*$/.test(name) || value.length === 0) {
371
+ throw new Error(`WASIX extension '${extension['sql-name']}' has invalid startup config '${assignment}'`);
372
+ }
373
+ if (name === SHARED_PRELOAD_LIBRARIES) {
374
+ appendCsv(value, sharedPreloads, seenSharedPreloads);
375
+ continue;
376
+ }
377
+ const existing = merged[name];
378
+ if (existing !== undefined && existing !== value) {
379
+ throw new Error(`WASIX extension '${extension['sql-name']}' requires ${name}=${value}, but the caller configured ${name}=${existing}`);
380
+ }
381
+ merged[name] = value;
382
+ }
383
+ }
384
+ if (sharedPreloads.length > 0) {
385
+ merged[SHARED_PRELOAD_LIBRARIES] = sharedPreloads.join(',');
386
+ }
387
+ return merged;
388
+ }
389
+ function parseClusterSeedManifest(bytes, expectedProfile) {
390
+ let parsed;
391
+ try {
392
+ parsed = JSON.parse(decodeUtf8(bytes));
393
+ }
394
+ catch (error) {
395
+ throw new Error(`WASIX ${expectedProfile} cluster seed manifest is not valid UTF-8 JSON: ${describeError(error)}`);
396
+ }
397
+ const label = `WASIX ${expectedProfile} cluster seed manifest`;
398
+ const root = requireObject(parsed, label);
399
+ requireExactKeys(root, [
400
+ 'archive',
401
+ 'artifactRole',
402
+ 'catalogProfile',
403
+ 'extensions',
404
+ 'icu',
405
+ 'initProfile',
406
+ 'requiredRuntimeFeatures',
407
+ 'runtime',
408
+ 'schema',
409
+ 'source',
410
+ ], label);
411
+ assertClusterSeedProfileContract(root, expectedProfile);
412
+ requireString(root.initProfile, `${label} init profile`);
413
+ const runtime = requireObject(root.runtime, `${label} runtime`);
414
+ requireExactKeys(runtime, [
415
+ 'compatibilityKey',
416
+ 'consumerSha256',
417
+ 'engineFamily',
418
+ 'initdbSha256',
419
+ 'physicalFormat',
420
+ 'postgresMajor',
421
+ 'producerSha256',
422
+ 'product',
423
+ 'version',
424
+ ], `${label} runtime`);
425
+ for (const field of [
426
+ 'product',
427
+ 'version',
428
+ 'engineFamily',
429
+ 'physicalFormat',
430
+ 'compatibilityKey',
431
+ ]) {
432
+ requireString(runtime[field], `${label} runtime ${field}`);
433
+ }
434
+ requirePositiveInteger(runtime.postgresMajor, `${label} runtime PostgreSQL major`);
435
+ for (const field of ['consumerSha256', 'producerSha256', 'initdbSha256']) {
436
+ requireSha256(runtime[field], `${label} runtime ${field}`);
437
+ }
438
+ const source = requireObject(root.source, `${label} source`);
439
+ requireExactKeys(source, ['catalogVersion', 'fingerprint', 'lane', 'producer'], `${label} source`);
440
+ for (const field of ['catalogVersion', 'fingerprint', 'lane', 'producer']) {
441
+ requireString(source[field], `${label} source ${field}`);
442
+ }
443
+ const archive = requireObject(root.archive, `${label} archive`);
444
+ requireExactKeys(archive, ['compressedBytes', 'directories', 'expandedBytes', 'path', 'regularFiles', 'sha256'], `${label} archive`);
445
+ requireAssetPath(archive.path, `${label} archive path`);
446
+ requireSha256(archive.sha256, `${label} archive`);
447
+ for (const field of [
448
+ 'compressedBytes',
449
+ 'directories',
450
+ 'expandedBytes',
451
+ 'regularFiles',
452
+ ]) {
453
+ requirePositiveInteger(archive[field], `${label} archive ${field}`);
454
+ }
455
+ const extensions = requireObject(root.extensions, `${label} extensions`);
456
+ requireExactKeys(extensions, ['selected', 'startupConfiguration'], `${label} extensions`);
457
+ const selected = requireStringArray(extensions.selected, `${label} selected extensions`);
458
+ const startupConfiguration = requireStringArray(extensions.startupConfiguration, `${label} startup configuration`);
459
+ if (selected.length !== 0 || startupConfiguration.length !== 0) {
460
+ throw new Error(`${label} must be extension-free`);
461
+ }
462
+ return parsed;
463
+ }
464
+ /** @internal Validate the host-independent standard/ICU seed profile contract. */
465
+ export function assertClusterSeedProfileContract(value, expectedProfile) {
466
+ const label = `WASIX ${expectedProfile} cluster seed manifest`;
467
+ const root = requireObject(value, label);
468
+ if (root.schema !== 'oliphaunt-cluster-seed-v1') {
469
+ throw new Error(`${label} has an unsupported schema`);
470
+ }
471
+ if (root.catalogProfile !== expectedProfile) {
472
+ throw new Error(`${label} profile mismatch: expected ${expectedProfile}, got ${String(root.catalogProfile)}`);
473
+ }
474
+ const expectedRole = expectedProfile === 'standard' ? 'cluster-seed-standard' : 'cluster-seed-icu';
475
+ if (root.artifactRole !== expectedRole) {
476
+ throw new Error(`${label} profile mismatch: expected role ${expectedRole}, got ${String(root.artifactRole)}`);
477
+ }
478
+ const requiredFeatures = requireStringArray(root.requiredRuntimeFeatures, `${label} required features`);
479
+ if (expectedProfile === 'standard') {
480
+ if (requiredFeatures.length !== 0 || root.icu !== null) {
481
+ throw new Error(`${label} must not require or identify ICU data`);
482
+ }
483
+ return;
484
+ }
485
+ if (requiredFeatures.length !== 1 || requiredFeatures[0] !== 'icu') {
486
+ throw new Error(`${label} must require exactly the ICU runtime feature`);
487
+ }
488
+ const icu = requireObject(root.icu, `${label} ICU identity`);
489
+ requireExactKeys(icu, [
490
+ 'artifactRole',
491
+ 'dataForm',
492
+ 'dataTreeSha256',
493
+ 'dataVersion',
494
+ 'sourceCommit',
495
+ 'upstreamVersion',
496
+ ], `${label} ICU identity`);
497
+ for (const field of [
498
+ 'artifactRole',
499
+ 'dataForm',
500
+ 'dataVersion',
501
+ 'sourceCommit',
502
+ 'upstreamVersion',
503
+ ]) {
504
+ requireString(icu[field], `${label} ICU ${field}`);
505
+ }
506
+ requireSha256(icu.dataTreeSha256, `${label} ICU data tree`);
507
+ if (icu.artifactRole !== 'icu-data' ||
508
+ icu.upstreamVersion !== '76.1' ||
509
+ icu.dataVersion !== '76.1' ||
510
+ icu.dataForm !== 'files-le') {
511
+ throw new Error(`${label} has an incompatible ICU identity`);
512
+ }
513
+ }
514
+ /** @internal Verify that a separately distributed ICU carrier belongs to this runtime. */
515
+ export function assertIcuDescriptorMatchesRuntime(runtime, icu, manifest) {
516
+ if (icu.compatibility.runtimeProduct !== runtime.product ||
517
+ icu.compatibility.runtimeVersion !== runtime.version) {
518
+ throw new Error(`WASIX ICU carrier targets ${icu.compatibility.runtimeProduct}@${icu.compatibility.runtimeVersion}, not ${runtime.product}@${runtime.version}`);
519
+ }
520
+ if (icu.compatibility.dataTreeSha256 !== manifest['cluster-seeds'].icu['icu-data-tree-sha256']) {
521
+ throw new Error('WASIX ICU data and ICU cluster seed identify different logical data trees');
522
+ }
523
+ }
524
+ function verifyClusterSeedIdentity(options, outer, selected, seed, archive) {
525
+ const profile = seed.catalogProfile;
526
+ if (seed.runtime.product !== options.runtime.product ||
527
+ seed.runtime.version !== options.runtime.version ||
528
+ seed.runtime.engineFamily !== 'wasix' ||
529
+ seed.runtime.physicalFormat !== 'wasix-pg18-v1' ||
530
+ seed.runtime.compatibilityKey !== 'wasix-pg18-datum32-v1' ||
531
+ seed.runtime.postgresMajor !== 18) {
532
+ throw new Error(`WASIX ${profile} cluster seed has an incompatible runtime identity`);
533
+ }
534
+ if (seed.runtime.consumerSha256 !== seed.runtime.producerSha256 ||
535
+ seed.runtime.consumerSha256 !== outer.runtime['module-sha256'] ||
536
+ seed.runtime.consumerSha256 !== selected['runtime-module-sha256']) {
537
+ throw new Error(`WASIX ${profile} cluster seed was produced by a different runtime module`);
538
+ }
539
+ if (seed.source.fingerprint !== outer['source-fingerprint'] ||
540
+ seed.source.fingerprint !== selected['source-fingerprint']) {
541
+ throw new Error(`WASIX ${profile} cluster seed has a different source fingerprint`);
542
+ }
543
+ if (seed.archive.path !== archive.archive ||
544
+ seed.archive.path !== selected.archive ||
545
+ seed.archive.sha256 !== archive.sha256 ||
546
+ seed.archive.sha256 !== selected.sha256 ||
547
+ seed.archive.compressedBytes !== archive.size ||
548
+ seed.archive.compressedBytes !== selected.size) {
549
+ throw new Error(`WASIX ${profile} cluster seed archive identity is inconsistent`);
550
+ }
551
+ if (seed.archive.path !== `cluster-seeds/${profile}.tar.zst`) {
552
+ throw new Error(`WASIX ${profile} cluster seed has a non-canonical archive path`);
553
+ }
554
+ if (profile === 'icu') {
555
+ const icu = options.icu;
556
+ if (icu === undefined ||
557
+ seed.icu === null ||
558
+ seed.icu.artifactRole !== 'icu-data' ||
559
+ seed.icu.upstreamVersion !== icu.compatibility.dataVersion ||
560
+ seed.icu.dataVersion !== icu.compatibility.dataVersion ||
561
+ seed.icu.dataForm !== icu.compatibility.dataForm ||
562
+ seed.icu.dataTreeSha256 !== icu.compatibility.dataTreeSha256) {
563
+ throw new Error('WASIX ICU cluster seed does not match the selected ICU data carrier');
564
+ }
565
+ }
566
+ }
567
+ /** @internal Validate and add exact-archive-verified ICU files to the runtime tree. */
568
+ export function overlayIcuArchive(runtime, icu) {
569
+ const prefix = 'share/icu/';
570
+ const rows = [];
571
+ const directories = [];
572
+ let hasDataFile = false;
573
+ for (const [path, bytes] of icu.files) {
574
+ if (!path.startsWith(prefix) || path.length === prefix.length) {
575
+ throw new Error(`WASIX ICU data archive contains a file outside share/icu: ${path}`);
576
+ }
577
+ const relative = path.slice(prefix.length);
578
+ if (relative.split('/').some((segment) => segment.startsWith('icudt')))
579
+ hasDataFile = true;
580
+ rows.push({ path: relative, bytes });
581
+ const target = `oliphaunt/${path}`;
582
+ if (runtime.files.has(target) || runtime.directories.has(target)) {
583
+ throw new Error(`WASIX ICU data collides with runtime path ${target}`);
584
+ }
585
+ }
586
+ if (rows.length === 0 || !hasDataFile) {
587
+ throw new Error('WASIX ICU data archive contains no ICU data files under share/icu');
588
+ }
589
+ for (const path of icu.directories) {
590
+ if (path !== 'share' && path !== 'share/icu' && !path.startsWith(prefix)) {
591
+ throw new Error(`WASIX ICU data archive contains a directory outside share/icu: ${path}`);
592
+ }
593
+ if (path === 'share')
594
+ continue;
595
+ const target = `oliphaunt/${path}`;
596
+ if (runtime.files.has(target)) {
597
+ throw new Error(`WASIX ICU data collides with runtime path ${target}`);
598
+ }
599
+ directories.push(target);
600
+ }
601
+ for (const { path, bytes } of rows)
602
+ runtime.files.set(`oliphaunt/share/icu/${path}`, bytes);
603
+ for (const path of directories)
604
+ runtime.directories.add(path);
605
+ }
606
+ function assertRuntimeArchiveMatchesManifest(descriptor, manifest, label) {
607
+ if (descriptor.archive !== manifest.archive) {
608
+ throw new Error(`${label} path does not match the canonical manifest: expected ${manifest.archive}, received ${descriptor.archive}`);
609
+ }
610
+ if (descriptor.sha256 !== manifest.sha256) {
611
+ throw new Error(`${label} SHA-256 does not match the canonical manifest`);
612
+ }
613
+ if (manifest.size !== undefined && descriptor.size !== manifest.size) {
614
+ throw new Error(`${label} size does not match the canonical manifest: expected ${manifest.size}, received ${descriptor.size}`);
615
+ }
616
+ }
617
+ async function assertDeclaredAssetBytes(bytes, expectedSize, expectedSha256, label) {
618
+ if (bytes.length !== expectedSize) {
619
+ throw new Error(`${label} size mismatch: expected ${expectedSize}, received ${bytes.length}`);
620
+ }
621
+ await assertSha256(bytes, expectedSha256, label);
622
+ }
623
+ function verifyPostgresIdentity(manifest, seed, pgVersionBytes) {
624
+ if (pgVersionBytes === undefined) {
625
+ throw new Error('WASIX cluster seed is missing PG_VERSION');
626
+ }
627
+ const pgVersion = decodeUtf8(pgVersionBytes).trim();
628
+ const runtimeMajor = manifest.runtime['postgres-version'].split('.')[0];
629
+ const seedMajor = seed['postgres-version'].split('.')[0];
630
+ if (pgVersion !== runtimeMajor || pgVersion !== seedMajor) {
631
+ throw new Error(`WASIX runtime/cluster seed PostgreSQL major mismatch: runtime ${runtimeMajor}, seed ${seedMajor}, PG_VERSION ${pgVersion}`);
632
+ }
633
+ }
634
+ function decodeUtf8(bytes) {
635
+ const input = typeof SharedArrayBuffer !== 'undefined' && bytes.buffer instanceof SharedArrayBuffer
636
+ ? Uint8Array.from(bytes)
637
+ : bytes;
638
+ return decoder.decode(input);
639
+ }
640
+ /** @internal Shared verification for separately carried WASIX tool modules. */
641
+ export async function assertSha256(bytes, expected, label) {
642
+ if (globalThis.crypto?.subtle === undefined) {
643
+ throw new Error(`Web Crypto is required to verify ${label}`);
644
+ }
645
+ const actual = await sha256Hex(bytes);
646
+ if (actual !== expected) {
647
+ throw new Error(`${label} SHA-256 mismatch: expected ${expected}, received ${actual}`);
648
+ }
649
+ }
650
+ async function sha256Hex(bytes) {
651
+ if (globalThis.crypto?.subtle === undefined) {
652
+ throw new Error('Web Crypto is required to calculate SHA-256');
653
+ }
654
+ const source = bytes.buffer instanceof ArrayBuffer
655
+ ? new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength)
656
+ : bytes.slice().buffer;
657
+ const digest = new Uint8Array(await globalThis.crypto.subtle.digest('SHA-256', source));
658
+ return [...digest].map((byte) => byte.toString(16).padStart(2, '0')).join('');
659
+ }
660
+ function extensionMountTarget(path, sqlName, directory = false) {
661
+ const allowed = [
662
+ 'lib/postgresql/',
663
+ 'share/proj/',
664
+ 'share/postgresql/extension/',
665
+ 'share/postgresql/tsearch_data/',
666
+ ];
667
+ const allowedDirectory = new Set([
668
+ 'lib/postgresql',
669
+ 'share/proj',
670
+ 'share/postgresql',
671
+ 'share/postgresql/extension',
672
+ 'share/postgresql/tsearch_data',
673
+ ]);
674
+ if (!allowed.some((prefix) => path.startsWith(prefix)) &&
675
+ !(directory && allowedDirectory.has(path))) {
676
+ throw new Error(`WASIX extension '${sqlName}' contains non-canonical install path ${path}`);
677
+ }
678
+ const slash = path.indexOf('/');
679
+ return {
680
+ mountPath: path.slice(0, slash) === 'lib' ? '/lib' : '/share',
681
+ relative: path.slice(slash + 1),
682
+ };
683
+ }
684
+ function appendCsv(value, ordered, seen) {
685
+ for (const item of value?.split(',') ?? []) {
686
+ const trimmed = item.trim();
687
+ if (trimmed.length > 0 && !seen.has(trimmed)) {
688
+ seen.add(trimmed);
689
+ ordered.push(trimmed);
690
+ }
691
+ }
692
+ }
693
+ function requireObject(value, label) {
694
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
695
+ throw new Error(`${label} must be an object`);
696
+ }
697
+ return value;
698
+ }
699
+ function requireArray(value, label) {
700
+ if (!Array.isArray(value)) {
701
+ throw new Error(`${label} must be an array`);
702
+ }
703
+ return value;
704
+ }
705
+ function requireExactKeys(value, expected, label) {
706
+ const actual = Object.keys(value).sort();
707
+ const canonical = [...expected].sort();
708
+ if (actual.length !== canonical.length || actual.some((key, index) => key !== canonical[index])) {
709
+ const missing = canonical.filter((key) => !Object.hasOwn(value, key));
710
+ const unexpected = actual.filter((key) => !canonical.includes(key));
711
+ throw new Error(`${label} fields do not match the contract` +
712
+ `${missing.length > 0 ? `; missing ${missing.join(', ')}` : ''}` +
713
+ `${unexpected.length > 0 ? `; unexpected ${unexpected.join(', ')}` : ''}`);
714
+ }
715
+ }
716
+ function requireStringArray(value, label) {
717
+ const values = requireArray(value, label);
718
+ return values.map((entry, index) => requireString(entry, `${label} entry ${index}`));
719
+ }
720
+ function requireString(value, label) {
721
+ if (typeof value !== 'string' || value.length === 0 || value.includes('\0')) {
722
+ throw new Error(`${label} must be a non-empty string without NUL bytes`);
723
+ }
724
+ return value;
725
+ }
726
+ function requireSqlName(value, label) {
727
+ const name = requireString(value, label);
728
+ if (!SQL_NAME.test(name)) {
729
+ throw new Error(`${label} must be a portable PostgreSQL extension name`);
730
+ }
731
+ return name;
732
+ }
733
+ function requireSha256(value, label) {
734
+ const hash = requireString(value, `${label} SHA-256`);
735
+ if (!SHA256.test(hash)) {
736
+ throw new Error(`${label} SHA-256 must be 64 lowercase hexadecimal characters`);
737
+ }
738
+ return hash;
739
+ }
740
+ function requireSafeInteger(value, label) {
741
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
742
+ throw new Error(`${label} must be a non-negative safe integer`);
743
+ }
744
+ return value;
745
+ }
746
+ function requirePositiveInteger(value, label) {
747
+ const integer = requireSafeInteger(value, label);
748
+ if (integer === 0)
749
+ throw new Error(`${label} must be positive`);
750
+ return integer;
751
+ }
752
+ function requireAssetPath(value, label) {
753
+ const path = requireString(value, label);
754
+ const segments = path.replaceAll('\\', '/').split('/');
755
+ if (path.startsWith('/') ||
756
+ path.includes('\\') ||
757
+ segments.some((segment) => segment.length === 0 || segment === '.' || segment === '..')) {
758
+ throw new Error(`${label} must be a safe relative asset path`);
759
+ }
760
+ return path;
761
+ }
762
+ function requireInstallPath(value, label) {
763
+ const path = requireAssetPath(value, label);
764
+ extensionMountTarget(path, label);
765
+ return path;
766
+ }
767
+ function describeError(error) {
768
+ return error instanceof Error ? error.message : String(error);
769
+ }