@happyvertical/smrt-cli 0.53.3 → 0.53.4

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 (64) hide show
  1. package/AGENTS.md +1 -0
  2. package/README.md +40 -0
  3. package/agents/app-commands.md +93 -0
  4. package/bin/smrt-web.mjs +19 -0
  5. package/bin/smrt.js +18 -12
  6. package/dist/{ApiKey-BZmbOTnL-DchyhIPA.js → ApiKey-BZmbOTnL-DPPF6Swn.js} +2 -2
  7. package/dist/ApiKeyCollection-Bfb_nw81-B1-2YKj8.js +2 -0
  8. package/dist/AuditLogCollection-B1YonCAP-DP4PCCw-.js +2 -0
  9. package/dist/{NostrIdentity-n70E0vck-CLGPdJ1W.js → NostrIdentity-n70E0vck-D60BSg6H.js} +3 -3
  10. package/dist/NostrIdentityCollection-Ca74dkaL-C-v57o9N.js +2 -0
  11. package/dist/{OidcIdentity-Cb1bxreM-DPBTZddF.js → OidcIdentity-Cb1bxreM-B2eV2ahq.js} +2 -2
  12. package/dist/OidcIdentityCollection-TV8VdK69-BodvOtPJ.js +2 -0
  13. package/dist/ProfileAssetCollection-B9hacUJa-B7PLe0ld.js +2 -0
  14. package/dist/{ProfileCollection-DuJT7Mum-BUYQQ9xZ.js → ProfileCollection-DuJT7Mum-B3Uhqphs.js} +26 -26
  15. package/dist/ProfileLinkCollection-4uNURz32-CLY1cBnn.js +2 -0
  16. package/dist/{ProfileMetadataCollection-DQhbfEfa-BQTogfVX.js → ProfileMetadataCollection-DQhbfEfa-Bzv-E4ep.js} +1 -1
  17. package/dist/{ProfileMetafieldCollection-CoBuM_5G-BXtoIP02.js → ProfileMetafieldCollection-CoBuM_5G-D-VqQuY1.js} +1 -1
  18. package/dist/{ProfileRelationshipCollection-DUVgaIqf-CNMliUF3.js → ProfileRelationshipCollection-DUVgaIqf-gEkDBmkh.js} +1 -1
  19. package/dist/{ProfileRelationshipTermCollection-DtA9OM23-DPOMtMr7.js → ProfileRelationshipTermCollection-DtA9OM23-DI_awdol.js} +1 -1
  20. package/dist/{ProfileRelationshipType-BRiRka6t-xzRfZRFG.js → ProfileRelationshipType-BRiRka6t-Ci-qvFhm.js} +1 -1
  21. package/dist/{ProfileRelationshipTypeCollection-BdXwd7Kf-Co3PdH01.js → ProfileRelationshipTypeCollection-BdXwd7Kf-CaQMLH9f.js} +1 -1
  22. package/dist/app/cli.d.ts +44 -0
  23. package/dist/app/cli.d.ts.map +1 -0
  24. package/dist/app/errors.d.ts +22 -0
  25. package/dist/app/errors.d.ts.map +1 -0
  26. package/dist/app/identity.d.ts +24 -0
  27. package/dist/app/identity.d.ts.map +1 -0
  28. package/dist/app/index.d.ts +19 -0
  29. package/dist/app/index.d.ts.map +1 -0
  30. package/dist/app/launchers.d.ts +40 -0
  31. package/dist/app/launchers.d.ts.map +1 -0
  32. package/dist/app/operations.d.ts +28 -0
  33. package/dist/app/operations.d.ts.map +1 -0
  34. package/dist/app/portability-assets.d.ts +151 -0
  35. package/dist/app/portability-assets.d.ts.map +1 -0
  36. package/dist/app/portability.d.ts +117 -0
  37. package/dist/app/portability.d.ts.map +1 -0
  38. package/dist/app/process-record.d.ts +54 -0
  39. package/dist/app/process-record.d.ts.map +1 -0
  40. package/dist/app/runtime.d.ts +83 -0
  41. package/dist/app/runtime.d.ts.map +1 -0
  42. package/dist/app.d.ts +1 -0
  43. package/dist/app.js +4 -0
  44. package/dist/cli-BRFsobey.js +1275 -0
  45. package/dist/cli-generator.d.ts.map +1 -1
  46. package/dist/commands/utilities.d.ts.map +1 -1
  47. package/dist/{commands-B8N38deG.js → commands-dFz1e9A6.js} +17 -288
  48. package/dist/{dist-BBQyIRsO.js → dist-Berwe80z.js} +4 -4
  49. package/dist/{dist-CL9KGm1j.js → dist-C2BqYVu7.js} +27 -27
  50. package/dist/{dist-C8BOgli4.js → dist-D4BNILLN.js} +3 -3
  51. package/dist/{dist-B62O-xH6.js → dist-EJ63xUEp.js} +2 -2
  52. package/dist/index.js +14 -9
  53. package/dist/mcp-apps-packaging-CixRnxz_.js +286 -0
  54. package/dist/{oidcProvisioningPrimitives-D9OYu5-F-l8afZs4F.js → oidcProvisioningPrimitives-D9OYu5-F-B-kYsBTM.js} +4 -4
  55. package/dist/portability-DmQWSSXA.js +859 -0
  56. package/dist/resolveIdentity-B-PK6RwU-BmAU1Kse.js +2 -0
  57. package/package.json +14 -9
  58. package/dist/ApiKeyCollection-Bfb_nw81-DR5XF_SZ.js +0 -2
  59. package/dist/AuditLogCollection-B1YonCAP-PbzALncL.js +0 -2
  60. package/dist/NostrIdentityCollection-Ca74dkaL-C2r-uDlC.js +0 -2
  61. package/dist/OidcIdentityCollection-TV8VdK69-2fZROnih.js +0 -2
  62. package/dist/ProfileAssetCollection-B9hacUJa-D33sifgF.js +0 -2
  63. package/dist/ProfileLinkCollection-4uNURz32-CHDTinax.js +0 -2
  64. package/dist/resolveIdentity-B-PK6RwU-B306Ch1I.js +0 -2
@@ -0,0 +1,859 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
2
+ import { T as assertExternalArtifactPath, j as errorCode } from "./cli-BRFsobey.js";
3
+ import { closeSync, constants, existsSync, fstatSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, writeFileSync } from "node:fs";
4
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { createHash, randomBytes } from "node:crypto";
7
+ //#region src/app/portability-assets.ts
8
+ /**
9
+ * Filesystem asset portability for logical export/import bundles.
10
+ *
11
+ * Ported unchanged from the template's `scripts/smrt-portability-assets.mjs`:
12
+ * the bundle format, limits, sanitized failure codes, staging journal, and
13
+ * crash-recovery rules are a persisted contract.
14
+ */
15
+ var ASSET_MANIFEST_SCHEMA_VERSION = 1;
16
+ var MAX_ASSET_COUNT = 1e3;
17
+ var MAX_ASSET_BYTES = 64 * 1024 * 1024;
18
+ var MAX_TOTAL_ASSET_BYTES = 256 * 1024 * 1024;
19
+ var MAX_BUNDLE_BYTES = 512 * 1024 * 1024;
20
+ var ASSET_URI_PREFIX = "smrt-bundle://asset/";
21
+ var DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
22
+ var BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
23
+ var MAX_BASE64_ASSET_LENGTH = Math.ceil(MAX_ASSET_BYTES / 3) * 4;
24
+ /** Sanitized asset-portability failure with a stable kebab-case `code`. */
25
+ var AssetPortabilityError = class extends Error {
26
+ code;
27
+ constructor(code) {
28
+ super(`Asset portability validation failed (${code}).`);
29
+ this.name = "AssetPortabilityError";
30
+ this.code = code;
31
+ }
32
+ };
33
+ function fail(code) {
34
+ return new AssetPortabilityError(code);
35
+ }
36
+ function isSanitizedFailure(error) {
37
+ return !!error && typeof error === "object" && typeof error.code === "string" && /^[a-z][a-z0-9-]*$/.test(error.code);
38
+ }
39
+ function sha256(value) {
40
+ return createHash("sha256").update(value).digest("hex");
41
+ }
42
+ /** `sha256:<hex>` digest of bytes. */
43
+ function digestBytes(value) {
44
+ return `sha256:${sha256(value)}`;
45
+ }
46
+ function isInside(parent, child) {
47
+ const nested = relative(parent, child);
48
+ return nested !== "" && nested !== ".." && !nested.startsWith(`..${sep}`);
49
+ }
50
+ function assertOutsideSource(sourceRoot, candidate, code) {
51
+ const source = realpathSync(sourceRoot);
52
+ const resolved = resolve(candidate);
53
+ if (resolved === source || isInside(source, resolved) || isInside(resolved, source)) throw fail(code);
54
+ return resolved;
55
+ }
56
+ function assertRealDirectory(path, code) {
57
+ try {
58
+ const details = lstatSync(path);
59
+ if (details.isSymbolicLink() || !details.isDirectory()) throw fail(code);
60
+ if (realpathSync(path) !== resolve(path)) throw fail(code);
61
+ return resolve(path);
62
+ } catch (error) {
63
+ if (isSanitizedFailure(error)) throw error;
64
+ throw fail(code);
65
+ }
66
+ }
67
+ /** Validate an asset root: absolute, outside the source tree, real directory. */
68
+ function assertAssetRoot({ sourceRoot, assetRoot, allowMissing = false }) {
69
+ if (typeof assetRoot !== "string" || !isAbsolute(assetRoot)) throw fail("asset-root-required");
70
+ const root = assertOutsideSource(sourceRoot, assetRoot, "unsafe-asset-root");
71
+ assertRealDirectory(dirname(root), "unsafe-asset-root");
72
+ if (existsSync(root)) assertRealDirectory(root, "unsafe-asset-root");
73
+ else if (!allowMissing) throw fail("asset-root-missing");
74
+ return root;
75
+ }
76
+ function normalizePayloadPath(value) {
77
+ if (typeof value !== "string" || value.length === 0 || value.length > 512 || value.includes("\\") || value.includes("\0") || value.startsWith("/") || value.split("/").some((part) => part === "" || part === "." || part === "..")) throw fail("unsafe-payload-path");
78
+ return value;
79
+ }
80
+ function normalizeDestinationPath(value) {
81
+ const normalized = normalizePayloadPath(value);
82
+ if (!/^portable\/[a-f0-9]{64}$/.test(normalized)) throw fail("unsafe-destination-path");
83
+ return normalized;
84
+ }
85
+ function portableUri(key) {
86
+ return `${ASSET_URI_PREFIX}${encodeURIComponent(key)}`;
87
+ }
88
+ function payloadPathForKey(key) {
89
+ return `payloads/${sha256(key)}`;
90
+ }
91
+ function destinationPathForKey(key) {
92
+ return `portable/${sha256(key)}`;
93
+ }
94
+ function readBoundedDescriptor(descriptor, expectedSize, changedCode) {
95
+ const bytes = Buffer.alloc(expectedSize);
96
+ let offset = 0;
97
+ while (offset < expectedSize) {
98
+ const count = readSync(descriptor, bytes, offset, expectedSize - offset, null);
99
+ if (count === 0) throw fail(changedCode);
100
+ offset += count;
101
+ }
102
+ if (readSync(descriptor, Buffer.allocUnsafe(1), 0, 1, null) !== 0) throw fail(changedCode);
103
+ return bytes;
104
+ }
105
+ function readRegularFileUnchecked(path, approvedRoot, onOpened) {
106
+ const lexicalPath = resolve(path);
107
+ if (!isInside(approvedRoot, lexicalPath)) throw fail("asset-outside-root");
108
+ const before = lstatSync(lexicalPath);
109
+ if (before.isSymbolicLink() || !before.isFile()) throw fail("asset-not-regular");
110
+ if (before.size > 67108864) throw fail("asset-too-large");
111
+ if (!isInside(approvedRoot, realpathSync(lexicalPath))) throw fail("asset-outside-root");
112
+ const noFollow = constants.O_NOFOLLOW ?? 0;
113
+ const descriptor = openSync(lexicalPath, constants.O_RDONLY | noFollow);
114
+ try {
115
+ const opened = fstatSync(descriptor);
116
+ if (!opened.isFile() || opened.size !== before.size) throw fail("asset-changed-during-read");
117
+ onOpened?.();
118
+ const openedPath = statSync(lexicalPath);
119
+ if (!isInside(approvedRoot, realpathSync(lexicalPath)) || openedPath.dev !== opened.dev || openedPath.ino !== opened.ino) throw fail("asset-changed-during-read");
120
+ const bytes = readBoundedDescriptor(descriptor, opened.size, "asset-changed-during-read");
121
+ const after = fstatSync(descriptor);
122
+ const afterPath = statSync(lexicalPath);
123
+ if (after.dev !== opened.dev || after.ino !== opened.ino || after.size !== opened.size || bytes.byteLength !== opened.size || !isInside(approvedRoot, realpathSync(lexicalPath)) || afterPath.dev !== opened.dev || afterPath.ino !== opened.ino) throw fail("asset-changed-during-read");
124
+ return bytes;
125
+ } finally {
126
+ closeSync(descriptor);
127
+ }
128
+ }
129
+ function readRegularFile(path, approvedRoot, onOpened) {
130
+ try {
131
+ return readRegularFileUnchecked(path, approvedRoot, onOpened);
132
+ } catch (error) {
133
+ if (isSanitizedFailure(error)) throw error;
134
+ throw fail("asset-changed-during-read");
135
+ }
136
+ }
137
+ function pathFromSourceUri(sourceUri) {
138
+ if (typeof sourceUri !== "string" || !sourceUri.startsWith("file://")) throw fail("unsupported-asset-source");
139
+ try {
140
+ return fileURLToPath(sourceUri);
141
+ } catch {
142
+ throw fail("unsupported-asset-source");
143
+ }
144
+ }
145
+ function assetRows(tables) {
146
+ const table = tables.find((candidate) => candidate?.name === "assets");
147
+ if (!table) return [];
148
+ if (!Array.isArray(table.rows) || !table.columns?.includes("id") || !table.columns?.includes("source_uri")) throw fail("invalid-asset-table");
149
+ return table.rows;
150
+ }
151
+ /** Replace provider paths with logical keys while collecting verified payloads. */
152
+ function collectFilesystemAssets({ tables, sourceRoot, assetRoot, onSourceOpened }) {
153
+ const rows = assetRows(tables);
154
+ if (rows.length === 0) return {
155
+ schemaVersion: 1,
156
+ adapter: "filesystem",
157
+ entries: [],
158
+ payloads: []
159
+ };
160
+ if (rows.length > 1e3) throw fail("too-many-assets");
161
+ const root = assertAssetRoot({
162
+ sourceRoot,
163
+ assetRoot
164
+ });
165
+ const keys = /* @__PURE__ */ new Set();
166
+ const payloadPaths = /* @__PURE__ */ new Set();
167
+ const entries = [];
168
+ const payloads = [];
169
+ let totalBytes = 0;
170
+ for (const row of rows) {
171
+ if (!row?.source_uri) continue;
172
+ const key = String(row.id || "");
173
+ if (!key || key.length > 512 || keys.has(key)) throw fail("duplicate-asset-key");
174
+ keys.add(key);
175
+ const payloadPath = payloadPathForKey(key);
176
+ if (payloadPaths.has(payloadPath)) throw fail("duplicate-payload-path");
177
+ payloadPaths.add(payloadPath);
178
+ const bytes = readRegularFile(pathFromSourceUri(row.source_uri), root, onSourceOpened);
179
+ totalBytes += bytes.byteLength;
180
+ if (totalBytes > 268435456) throw fail("asset-total-too-large");
181
+ const contentDigest = digestBytes(bytes);
182
+ entries.push({
183
+ key,
184
+ byteLength: bytes.byteLength,
185
+ contentDigest,
186
+ payloadPath
187
+ });
188
+ payloads.push({
189
+ path: payloadPath,
190
+ encoding: "base64",
191
+ data: bytes.toString("base64")
192
+ });
193
+ row.source_uri = portableUri(key);
194
+ }
195
+ return {
196
+ schemaVersion: 1,
197
+ adapter: "filesystem",
198
+ entries,
199
+ payloads
200
+ };
201
+ }
202
+ function decodePayload(payload, expectedByteLength) {
203
+ const candidate = payload;
204
+ if (candidate?.encoding !== "base64" || typeof candidate.data !== "string" || candidate.data.length > MAX_BASE64_ASSET_LENGTH || !BASE64_PATTERN.test(candidate.data)) throw fail("invalid-asset-payload");
205
+ const data = candidate.data;
206
+ const padding = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
207
+ if (data.length / 4 * 3 - padding !== expectedByteLength) throw fail("asset-size-mismatch");
208
+ const bytes = Buffer.from(data, "base64");
209
+ if (bytes.toString("base64") !== data) throw fail("invalid-asset-payload");
210
+ return bytes;
211
+ }
212
+ /** Validate every manifest/payload relationship and hydrate destination URIs. */
213
+ function verifyFilesystemAssets({ assetBundle, tables, sourceRoot, assetRoot }) {
214
+ const bundle = assetBundle;
215
+ if (bundle?.schemaVersion !== 1 || bundle?.adapter !== "filesystem" || !Array.isArray(bundle.entries) || !Array.isArray(bundle.payloads)) throw fail("unsupported-asset-manifest");
216
+ if (bundle.entries.length > 1e3) throw fail("too-many-assets");
217
+ if (bundle.entries.length !== bundle.payloads.length) throw fail("asset-payload-count-mismatch");
218
+ const rows = assetRows(tables);
219
+ const rowByKey = /* @__PURE__ */ new Map();
220
+ for (const row of rows) {
221
+ if (!row?.source_uri) continue;
222
+ const key = String(row.id || "");
223
+ if (!key || rowByKey.has(key) || row.source_uri !== portableUri(key)) throw fail("invalid-asset-reference");
224
+ rowByKey.set(key, row);
225
+ }
226
+ const payloadByPath = /* @__PURE__ */ new Map();
227
+ for (const payload of bundle.payloads) {
228
+ const path = normalizePayloadPath(payload?.path);
229
+ if (payloadByPath.has(path)) throw fail("duplicate-payload-path");
230
+ payloadByPath.set(path, payload);
231
+ }
232
+ const keys = /* @__PURE__ */ new Set();
233
+ const paths = /* @__PURE__ */ new Set();
234
+ const verified = [];
235
+ let totalBytes = 0;
236
+ for (const rawEntry of bundle.entries) {
237
+ const entry = rawEntry;
238
+ const key = typeof entry?.key === "string" ? entry.key : "";
239
+ const payloadPath = normalizePayloadPath(entry?.payloadPath);
240
+ if (!key || key.length > 512 || keys.has(key)) throw fail("duplicate-asset-key");
241
+ if (paths.has(payloadPath)) throw fail("duplicate-payload-path");
242
+ keys.add(key);
243
+ paths.add(payloadPath);
244
+ const byteLength = entry?.byteLength;
245
+ if (!Number.isSafeInteger(byteLength) || byteLength < 0 || byteLength > 67108864) throw fail("invalid-asset-size");
246
+ if (!DIGEST_PATTERN.test(entry?.contentDigest || "")) throw fail("invalid-asset-digest");
247
+ totalBytes += byteLength;
248
+ if (totalBytes > 268435456) throw fail("asset-total-too-large");
249
+ const payload = payloadByPath.get(payloadPath);
250
+ if (!payload) throw fail("missing-asset-payload");
251
+ const bytes = decodePayload(payload, byteLength);
252
+ if (bytes.byteLength !== byteLength) throw fail("asset-size-mismatch");
253
+ if (digestBytes(bytes) !== entry?.contentDigest) throw fail("asset-digest-mismatch");
254
+ if (!rowByKey.has(key)) throw fail("unreferenced-asset-payload");
255
+ verified.push({
256
+ key,
257
+ bytes,
258
+ contentDigest: entry.contentDigest,
259
+ relativePath: destinationPathForKey(key)
260
+ });
261
+ }
262
+ if (keys.size !== rowByKey.size) throw fail("missing-asset-entry");
263
+ if (payloadByPath.size !== paths.size) throw fail("unreferenced-asset-payload");
264
+ if (verified.length === 0) return {
265
+ root: null,
266
+ entries: []
267
+ };
268
+ const root = assertAssetRoot({
269
+ sourceRoot,
270
+ assetRoot,
271
+ allowMissing: true
272
+ });
273
+ for (const entry of verified) rowByKey.get(entry.key).source_uri = pathToFileURL(join(root, entry.relativePath)).href;
274
+ return {
275
+ root,
276
+ entries: verified
277
+ };
278
+ }
279
+ function journalPath(stateRoot, appId) {
280
+ return join(stateRoot, `.smrt-asset-import-${sha256(appId).slice(0, 16)}.json`);
281
+ }
282
+ function writeJournal(path, journal) {
283
+ const temporary = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
284
+ try {
285
+ writeFileSync(temporary, `${JSON.stringify(journal)}\n`, {
286
+ flag: "wx",
287
+ mode: 384
288
+ });
289
+ renameSync(temporary, path);
290
+ } finally {
291
+ rmSync(temporary, { force: true });
292
+ }
293
+ }
294
+ function assertEmptyAssetRoot(root) {
295
+ if (!existsSync(root)) return;
296
+ assertRealDirectory(root, "unsafe-asset-root");
297
+ if (readdirSync(root).length !== 0) throw fail("non-empty-asset-target");
298
+ }
299
+ /** Stage verified bytes without publishing anything into the destination root. */
300
+ function stageFilesystemAssets({ verified, stateRoot, appId, bundleDigest }) {
301
+ if (verified.entries.length === 0 || !verified.root) return null;
302
+ assertEmptyAssetRoot(verified.root);
303
+ const stageRoot = join(dirname(verified.root), `.smrt-asset-stage-${process.pid}-${randomBytes(8).toString("hex")}`);
304
+ const path = journalPath(stateRoot, appId);
305
+ if (existsSync(path)) throw fail("asset-recovery-required");
306
+ const journal = {
307
+ schemaVersion: 1,
308
+ application: appId,
309
+ bundleDigest,
310
+ assetRoot: verified.root,
311
+ stageRoot,
312
+ phase: "staging",
313
+ rootExisted: existsSync(verified.root),
314
+ entries: verified.entries.map(({ relativePath, contentDigest }) => ({
315
+ relativePath,
316
+ contentDigest
317
+ }))
318
+ };
319
+ writeJournal(path, journal);
320
+ try {
321
+ mkdirSync(stageRoot, { mode: 448 });
322
+ for (const entry of verified.entries) {
323
+ const destination = join(stageRoot, entry.relativePath);
324
+ mkdirSync(dirname(destination), {
325
+ recursive: true,
326
+ mode: 448
327
+ });
328
+ writeFileSync(destination, entry.bytes, {
329
+ flag: "wx",
330
+ mode: 384
331
+ });
332
+ if (digestBytes(readFileSync(destination)) !== entry.contentDigest) throw fail("staged-asset-digest-mismatch");
333
+ }
334
+ return {
335
+ path,
336
+ journal
337
+ };
338
+ } catch (error) {
339
+ rmSync(stageRoot, {
340
+ recursive: true,
341
+ force: true
342
+ });
343
+ rmSync(path, { force: true });
344
+ throw error;
345
+ }
346
+ }
347
+ /** Publish the complete verified tree with one rename that never follows the target. */
348
+ function publishFilesystemAssets(staged, options = {}) {
349
+ if (!staged) return;
350
+ const { journal, path } = staged;
351
+ assertEmptyAssetRoot(journal.assetRoot);
352
+ journal.phase = "publishing";
353
+ writeJournal(path, journal);
354
+ if (existsSync(journal.assetRoot)) rmdirSync(journal.assetRoot);
355
+ try {
356
+ options.beforeRename?.();
357
+ renameSync(journal.stageRoot, journal.assetRoot);
358
+ } catch (error) {
359
+ if (journal.rootExisted && !existsSync(journal.assetRoot)) mkdirSync(journal.assetRoot, { mode: 448 });
360
+ throw error;
361
+ }
362
+ journal.phase = "published";
363
+ writeJournal(path, journal);
364
+ verifyPublishedTree(journal);
365
+ }
366
+ function verifyPublishedEntry(journal, entry, approvedRoot, { allowMissing = false, verifyDigest = true } = {}) {
367
+ const destination = join(journal.assetRoot, normalizeDestinationPath(entry.relativePath));
368
+ if (!existsSync(destination)) {
369
+ if (allowMissing) return false;
370
+ throw fail("missing-published-asset");
371
+ }
372
+ const bytes = readRegularFile(destination, approvedRoot);
373
+ if (verifyDigest && digestBytes(bytes) !== entry.contentDigest) throw fail("published-asset-digest-mismatch");
374
+ return true;
375
+ }
376
+ function verifyPublishedTree(journal, { verifyDigest = true } = {}) {
377
+ try {
378
+ const approvedRoot = assertRealDirectory(journal.assetRoot, "unsafe-published-asset");
379
+ const rootEntries = readdirSync(approvedRoot);
380
+ if (rootEntries.length !== 1 || rootEntries[0] !== "portable") throw fail("unexpected-published-asset");
381
+ const portableDirectory = join(approvedRoot, "portable");
382
+ assertRealDirectory(portableDirectory, "unsafe-published-asset");
383
+ const expected = new Set(journal.entries.map((entry) => normalizeDestinationPath(entry.relativePath).slice(9)));
384
+ const actual = readdirSync(portableDirectory);
385
+ if (actual.length !== expected.size || actual.some((name) => !expected.has(name))) throw fail("unexpected-published-asset");
386
+ for (const entry of journal.entries) verifyPublishedEntry(journal, entry, approvedRoot, { verifyDigest });
387
+ } catch (error) {
388
+ if (isSanitizedFailure(error)) throw error;
389
+ throw fail("unsafe-published-asset");
390
+ }
391
+ }
392
+ /** Re-verify the published tree against the staged journal. */
393
+ function verifyPublishedFilesystemAssets(staged) {
394
+ if (staged) verifyPublishedTree(staged.journal);
395
+ }
396
+ function removePublishedTree(journal) {
397
+ const quarantine = `${journal.stageRoot}.rollback-${randomBytes(6).toString("hex")}`;
398
+ renameSync(journal.assetRoot, quarantine);
399
+ const quarantinedJournal = {
400
+ ...journal,
401
+ assetRoot: quarantine
402
+ };
403
+ try {
404
+ verifyPublishedTree(quarantinedJournal, { verifyDigest: false });
405
+ } catch {
406
+ if (journal.rootExisted) mkdirSync(journal.assetRoot, { mode: 448 });
407
+ return {
408
+ quarantined: true,
409
+ path: quarantine
410
+ };
411
+ }
412
+ rmSync(quarantine, { recursive: true });
413
+ if (journal.rootExisted) mkdirSync(journal.assetRoot, { mode: 448 });
414
+ return { quarantined: false };
415
+ }
416
+ /** Record in the journal that this bundle's database transaction committed. */
417
+ function markFilesystemAssetsCommitted(staged) {
418
+ if (!staged) return;
419
+ staged.journal.committedAt = (/* @__PURE__ */ new Date()).toISOString();
420
+ writeJournal(staged.path, staged.journal);
421
+ }
422
+ /**
423
+ * True when an import journal for `appId` exists, belongs to `bundleDigest`,
424
+ * and records a committed database transaction.
425
+ */
426
+ function hasCommittedImportJournal({ stateRoot, appId, bundleDigest }) {
427
+ const path = journalPath(stateRoot, appId);
428
+ try {
429
+ const details = lstatSync(path);
430
+ if (details.isSymbolicLink() || !details.isFile()) return false;
431
+ const journal = JSON.parse(readFileSync(path, "utf8"));
432
+ return journal?.application === appId && journal.bundleDigest === bundleDigest && typeof journal.committedAt === "string";
433
+ } catch {
434
+ return false;
435
+ }
436
+ }
437
+ /** Verify the published tree and drop the journal. */
438
+ function finishFilesystemAssets(staged) {
439
+ if (!staged) return;
440
+ verifyPublishedTree(staged.journal);
441
+ rmSync(staged.path, { force: true });
442
+ }
443
+ /** Undo a staged or published import, restoring an empty target. */
444
+ function rollbackFilesystemAssets(staged) {
445
+ if (!staged) return;
446
+ if (existsSync(staged.journal.stageRoot)) rmSync(staged.journal.stageRoot, { recursive: true });
447
+ else if (existsSync(staged.journal.assetRoot)) removePublishedTree(staged.journal);
448
+ if (staged.journal.rootExisted && !existsSync(staged.journal.assetRoot)) mkdirSync(staged.journal.assetRoot, { mode: 448 });
449
+ rmSync(staged.path, { force: true });
450
+ }
451
+ /** Recover a prior crash only when the same bundle and exact target state prove ownership. */
452
+ function recoverFilesystemAssets({ stateRoot, appId, bundleDigest, assetRoot, targetState }) {
453
+ const path = journalPath(stateRoot, appId);
454
+ if (!existsSync(path)) return "none";
455
+ const details = lstatSync(path);
456
+ if (details.isSymbolicLink() || !details.isFile() || (details.mode & 63) !== 0) throw fail("unsafe-asset-recovery-journal");
457
+ let journal;
458
+ try {
459
+ journal = JSON.parse(readFileSync(path, "utf8"));
460
+ } catch {
461
+ throw fail("invalid-asset-recovery-journal");
462
+ }
463
+ if (journal?.schemaVersion !== 1 || journal.application !== appId || journal.bundleDigest !== bundleDigest || resolve(journal.assetRoot || "") !== resolve(assetRoot || "") || !Array.isArray(journal.entries) || ![
464
+ "staging",
465
+ "publishing",
466
+ "published"
467
+ ].includes(journal.phase) || dirname(resolve(journal.stageRoot || "")) !== dirname(resolve(assetRoot || "")) || !String(journal.stageRoot || "").includes(".smrt-asset-stage-")) throw fail("asset-recovery-mismatch");
468
+ if (targetState === "complete") {
469
+ if (existsSync(journal.stageRoot)) throw fail("asset-recovery-target-mismatch");
470
+ verifyPublishedTree(journal);
471
+ rmSync(path, { force: true });
472
+ return "complete";
473
+ }
474
+ if (targetState !== "empty") throw fail("asset-recovery-target-mismatch");
475
+ if (existsSync(journal.stageRoot)) rmSync(journal.stageRoot, { recursive: true });
476
+ else if (existsSync(journal.assetRoot)) removePublishedTree(journal);
477
+ if (journal.rootExisted && !existsSync(journal.assetRoot)) mkdirSync(journal.assetRoot, { mode: 448 });
478
+ rmSync(path, { force: true });
479
+ return "retry";
480
+ }
481
+ /**
482
+ * Read an import bundle only when it is a private (mode 0600), regular,
483
+ * non-symlinked file that does not change while it is read.
484
+ */
485
+ function readSensitiveBundle(path, options = {}) {
486
+ let details;
487
+ let initialRealPath;
488
+ try {
489
+ details = lstatSync(path);
490
+ initialRealPath = realpathSync(path);
491
+ } catch {
492
+ throw fail("unsafe-bundle-file");
493
+ }
494
+ if (details.isSymbolicLink() || !details.isFile() || initialRealPath !== resolve(path) || details.size > 536870912 || process.platform !== "win32" && (details.mode & 511) !== 384) throw fail("unsafe-bundle-file");
495
+ const noFollow = constants.O_NOFOLLOW ?? 0;
496
+ let descriptor;
497
+ try {
498
+ descriptor = openSync(path, constants.O_RDONLY | noFollow);
499
+ } catch {
500
+ throw fail("bundle-changed-during-read");
501
+ }
502
+ try {
503
+ const opened = fstatSync(descriptor);
504
+ if (opened.dev !== details.dev || opened.ino !== details.ino || opened.size !== details.size || !opened.isFile()) throw fail("bundle-changed-during-read");
505
+ options.onOpened?.();
506
+ const bytes = readBoundedDescriptor(descriptor, opened.size, "bundle-changed-during-read");
507
+ const after = fstatSync(descriptor);
508
+ let afterPath;
509
+ let finalRealPath;
510
+ try {
511
+ afterPath = statSync(path);
512
+ finalRealPath = realpathSync(path);
513
+ } catch {
514
+ throw fail("bundle-changed-during-read");
515
+ }
516
+ if (after.dev !== opened.dev || after.ino !== opened.ino || after.size !== opened.size || afterPath.dev !== opened.dev || afterPath.ino !== opened.ino || finalRealPath !== resolve(path)) throw fail("bundle-changed-during-read");
517
+ return bytes.toString("utf8");
518
+ } finally {
519
+ closeSync(descriptor);
520
+ }
521
+ }
522
+ /** Digest identifying one serialized bundle for crash recovery. */
523
+ function bundleContentDigest(serialized) {
524
+ return digestBytes(Buffer.from(serialized));
525
+ }
526
+ //#endregion
527
+ //#region src/app/portability.ts
528
+ /**
529
+ * Logical application export/import (schemaVersion 2 bundles).
530
+ *
531
+ * Ported unchanged from the template's `scripts/smrt-portability.mjs`: the
532
+ * bundle shape, non-portable table/column rules, parent-first import plan,
533
+ * empty-target rule, and asset staging/recovery are a persisted contract.
534
+ */
535
+ var portability_exports = /* @__PURE__ */ __exportAll({
536
+ executeImportPlan: () => executeImportPlan,
537
+ exportApplication: () => exportApplication,
538
+ importApplication: () => importApplication,
539
+ importedRowsMatchBundle: () => importedRowsMatchBundle,
540
+ manifestTables: () => manifestTables,
541
+ planImportTables: () => planImportTables,
542
+ serializeExportBundle: () => serializeExportBundle,
543
+ validateImportBundle: () => validateImportBundle
544
+ });
545
+ var SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
546
+ var NON_PORTABLE_TABLE_PATTERN = /(?:^|_)(?:api_keys?|audit_logs?|auth_approve_limits?|auth_requests?|bootstrap|credentials?|magic_link_tokens?|secrets?|sessions?|tokens?)(?:_|$)/;
547
+ var NON_PORTABLE_COLUMN_PATTERN = /(?:^|_)(?:api_key|ciphertext|cookie|credential|encrypted|encryption|password|private_key|privkey|secret|token)(?:_|$)/;
548
+ function quoteIdentifier(value) {
549
+ if (!SAFE_IDENTIFIER.test(value)) throw new Error("The generated manifest contains an unsafe SQL identifier.");
550
+ return `"${value}"`;
551
+ }
552
+ function containsNonPortableRows(table) {
553
+ return NON_PORTABLE_TABLE_PATTERN.test(table.name) || table.columns.some((column) => NON_PORTABLE_COLUMN_PATTERN.test(column));
554
+ }
555
+ /** Read the application tables from `<sourceRoot>/.smrt/manifest.json`. */
556
+ function manifestTables(sourceRoot) {
557
+ const manifestPath = join(sourceRoot, ".smrt", "manifest.json");
558
+ if (!existsSync(manifestPath)) throw new Error("Build the application before exporting or importing data.");
559
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
560
+ const tables = /* @__PURE__ */ new Map();
561
+ for (const rawDefinition of Object.values(manifest.objects || {})) {
562
+ const definition = rawDefinition;
563
+ const schema = definition?.schema;
564
+ if (definition?.className?.endsWith("Collection")) continue;
565
+ if (!schema?.tableName || schema.tableName.startsWith("_smrt_")) continue;
566
+ const columnDefinitions = schema.columns || {};
567
+ const columns = Object.keys(columnDefinitions);
568
+ if (columns.length === 0) continue;
569
+ const foreignKeys = Object.entries(columnDefinitions).flatMap(([column, columnDefinition]) => columnDefinition.foreignKey?.table ? [{
570
+ column,
571
+ referencesTable: columnDefinition.foreignKey.table
572
+ }] : []);
573
+ const primaryKeys = Object.entries(columnDefinitions).filter(([, columnDefinition]) => columnDefinition.primaryKey).map(([column]) => column);
574
+ tables.set(schema.tableName, {
575
+ name: schema.tableName,
576
+ columns,
577
+ columnDefinitions,
578
+ foreignKeys,
579
+ primaryKeys
580
+ });
581
+ }
582
+ return [...tables.values()].sort((left, right) => left.name.localeCompare(right.name));
583
+ }
584
+ /** Parent-first insertion plan with nullable cycle edges deferred to updates. */
585
+ function planImportTables(tables) {
586
+ const remaining = new Map(tables.map((table) => [table.name, table]));
587
+ const inserted = /* @__PURE__ */ new Set();
588
+ const plan = [];
589
+ while (remaining.size > 0) {
590
+ const table = [...remaining.values()].filter((table) => table.foreignKeys.every((foreignKey) => foreignKey.referencesTable === table.name || !remaining.has(foreignKey.referencesTable) || inserted.has(foreignKey.referencesTable))).sort((left, right) => left.name.localeCompare(right.name))[0] || [...remaining.values()].sort((left, right) => left.name.localeCompare(right.name))[0];
591
+ const deferredColumns = new Set(table.foreignKeys.filter((foreignKey) => foreignKey.referencesTable === table.name || remaining.has(foreignKey.referencesTable)).map((foreignKey) => foreignKey.column));
592
+ plan.push({
593
+ ...table,
594
+ deferredColumns
595
+ });
596
+ inserted.add(table.name);
597
+ remaining.delete(table.name);
598
+ }
599
+ return plan;
600
+ }
601
+ async function withDatabase(context, callback) {
602
+ const options = {
603
+ type: context.runtime.providers.database.engine,
604
+ url: context.env.DATABASE_URL
605
+ };
606
+ if (context.runtime.profile === "local") options.secureFile = {
607
+ driver: "node:sqlite",
608
+ custody: "trusted-parent",
609
+ root: context.paths?.root
610
+ };
611
+ const db = await (context.openDatabase ?? (async (databaseOptions) => {
612
+ const { getDatabase } = await import("@happyvertical/sql");
613
+ return getDatabase(databaseOptions);
614
+ }))(options);
615
+ try {
616
+ return await callback(db);
617
+ } finally {
618
+ await db.close?.();
619
+ }
620
+ }
621
+ /** Reject bundles that do not exactly match the generated schema. */
622
+ function validateImportBundle(bundle, expected) {
623
+ const providedNames = bundle.tables.map((table) => table?.name);
624
+ if (providedNames.length !== expected.size || new Set(providedNames).size !== expected.size || providedNames.some((name) => !expected.has(name))) throw new Error("The export does not contain the complete application schema.");
625
+ for (const rawExported of bundle.tables) {
626
+ const exported = rawExported;
627
+ const table = expected.get(exported.name);
628
+ if (!table || !Array.isArray(exported.rows) || !Array.isArray(exported.columns)) throw new Error("The export does not match the generated application schema.");
629
+ if (exported.columns.length !== table.columns.length || new Set(exported.columns).size !== table.columns.length || exported.columns.some((column) => !table.columns.includes(column))) throw new Error(`The export schema does not match ${table.name}.`);
630
+ for (const row of exported.rows) {
631
+ if (!row || typeof row !== "object" || Array.isArray(row)) throw new Error(`The export has an invalid row for ${table.name}.`);
632
+ const columns = Object.keys(row);
633
+ if (columns.length !== table.columns.length || columns.some((column) => !table.columns.includes(column))) throw new Error(`The export has incomplete columns for ${table.name}.`);
634
+ }
635
+ }
636
+ }
637
+ /** JSON cannot represent SQLite's lossless BigInt results; decimal strings do. */
638
+ function serializeExportBundle(bundle) {
639
+ return JSON.stringify(bundle, (_key, value) => typeof value === "bigint" ? value.toString() : value, 2);
640
+ }
641
+ /** Write a logical export bundle without replacing an existing file. */
642
+ async function exportApplication(context) {
643
+ const tables = manifestTables(context.sourceRoot);
644
+ let bundle;
645
+ await withDatabase(context, async (db) => {
646
+ if (typeof db.transaction !== "function") throw new Error("Logical export requires transactional database support.");
647
+ bundle = await db.transaction(async (tx) => {
648
+ if (context.runtime.providers.database.engine === "postgres") await tx.query("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY");
649
+ const exported = {
650
+ schemaVersion: 2,
651
+ application: context.appId,
652
+ profile: context.runtime.profile,
653
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
654
+ tables: []
655
+ };
656
+ for (const table of tables) exported.tables.push({
657
+ name: table.name,
658
+ columns: table.columns,
659
+ rows: containsNonPortableRows(table) ? [] : (await tx.query(`SELECT ${table.columns.map(quoteIdentifier).join(", ")} FROM ${quoteIdentifier(table.name)}`)).rows
660
+ });
661
+ exported.assets = collectFilesystemAssets({
662
+ tables: exported.tables,
663
+ sourceRoot: context.sourceRoot,
664
+ assetRoot: context.assetRoot || context.paths?.assets
665
+ });
666
+ return exported;
667
+ });
668
+ });
669
+ const exportedBundle = bundle;
670
+ const outputPath = assertExternalArtifactPath({
671
+ sourceRoot: context.sourceRoot,
672
+ path: context.path || join(context.stateRoot, "exports", `${context.appId}-${Date.now()}.json`),
673
+ label: "Export destination"
674
+ });
675
+ mkdirSync(dirname(outputPath), {
676
+ recursive: true,
677
+ mode: 448
678
+ });
679
+ const temporaryPath = join(dirname(outputPath), `.${basename(outputPath)}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`);
680
+ const serialized = `${serializeExportBundle(exportedBundle)}\n`;
681
+ if (Buffer.byteLength(serialized) > 536870912) throw new Error("The portability bundle exceeds the supported size limit.");
682
+ try {
683
+ writeFileSync(temporaryPath, serialized, {
684
+ flag: "wx",
685
+ mode: 384
686
+ });
687
+ try {
688
+ linkSync(temporaryPath, outputPath);
689
+ } catch (error) {
690
+ if (errorCode(error) === "EEXIST") throw new Error(`Export destination already exists: ${outputPath}`);
691
+ throw error;
692
+ }
693
+ } finally {
694
+ rmSync(temporaryPath, { force: true });
695
+ }
696
+ return {
697
+ path: outputPath,
698
+ tableCount: exportedBundle.tables.length,
699
+ assetsIncluded: true,
700
+ assetCount: exportedBundle.assets.entries.length
701
+ };
702
+ }
703
+ /** Execute a prevalidated parent-first import against one database transaction. */
704
+ async function executeImportPlan(tx, plan, exportedByName) {
705
+ const deferredUpdates = [];
706
+ let rowCount = 0;
707
+ for (const table of plan) {
708
+ const exported = exportedByName.get(table.name);
709
+ const count = await tx.query(`SELECT COUNT(*) AS count FROM ${quoteIdentifier(table.name)}`);
710
+ if (Number(count.rows[0]?.count || 0) !== 0) throw new Error(`Import target table ${table.name} is not empty.`);
711
+ for (const row of exported.rows) {
712
+ const columns = Object.keys(row);
713
+ await tx.query(`INSERT INTO ${quoteIdentifier(table.name)} (${columns.map(quoteIdentifier).join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`, ...columns.map((column) => table.deferredColumns.has(column) ? null : row[column]));
714
+ for (const column of table.deferredColumns) if (row[column] != null) deferredUpdates.push({
715
+ table,
716
+ column,
717
+ value: row[column],
718
+ primaryKey: table.primaryKeys[0],
719
+ primaryValue: row[table.primaryKeys[0]]
720
+ });
721
+ rowCount += 1;
722
+ }
723
+ }
724
+ for (const update of deferredUpdates) await tx.query(`UPDATE ${quoteIdentifier(update.table.name)} SET ${quoteIdentifier(update.column)} = ? WHERE ${quoteIdentifier(update.primaryKey)} = ?`, update.value, update.primaryValue);
725
+ return rowCount;
726
+ }
727
+ async function inspectImportTarget(db, plan, exportedByName) {
728
+ let empty = true;
729
+ let complete = true;
730
+ for (const table of plan) {
731
+ const count = await db.query(`SELECT COUNT(*) AS count FROM ${quoteIdentifier(table.name)}`);
732
+ const actual = Number(count.rows[0]?.count || 0);
733
+ const expected = exportedByName.get(table.name).rows.length;
734
+ if (actual !== 0) empty = false;
735
+ if (actual !== expected) complete = false;
736
+ }
737
+ if (empty) return "empty";
738
+ if (complete) return "complete";
739
+ return "dirty";
740
+ }
741
+ function canonicalValue(value) {
742
+ if (value === null || value === void 0) return null;
743
+ if (typeof value === "boolean") return value ? "1" : "0";
744
+ if (typeof value === "number" || typeof value === "bigint") return String(value);
745
+ if (value instanceof Date) return value.toISOString();
746
+ if (value instanceof Uint8Array) return Buffer.from(value).toString("base64");
747
+ if (typeof value === "object") return JSON.stringify(value);
748
+ return String(value);
749
+ }
750
+ function canonicalRows(rows, columns) {
751
+ return rows.map((row) => JSON.stringify(columns.map((column) => canonicalValue(row[column])))).sort();
752
+ }
753
+ /**
754
+ * True when every planned table holds exactly the bundle's rows (as a
755
+ * multiset over the bundle's columns). This is the evidence an interrupted
756
+ * import's recovery needs when no commit marker was recorded: matching
757
+ * per-table counts alone do not prove the rows are this bundle's.
758
+ */
759
+ async function importedRowsMatchBundle(db, plan, exportedByName) {
760
+ for (const table of plan) {
761
+ const expected = exportedByName.get(table.name).rows;
762
+ const left = canonicalRows((await db.query(`SELECT ${table.columns.map(quoteIdentifier).join(", ")} FROM ${quoteIdentifier(table.name)}`)).rows, table.columns);
763
+ const right = canonicalRows(expected, table.columns);
764
+ if (left.length !== right.length) return false;
765
+ for (let index = 0; index < left.length; index += 1) if (left[index] !== right[index]) return false;
766
+ }
767
+ return true;
768
+ }
769
+ /** Import a logical bundle into an empty application database. */
770
+ async function importApplication(context) {
771
+ if (!context.path) throw new Error("Usage: pnpm app:import -- /absolute/path/export.json");
772
+ const serialized = readSensitiveBundle(context.path);
773
+ const bundle = JSON.parse(serialized);
774
+ if (bundle.schemaVersion === 1) throw new Error("Database-only logical export bundles are not importable; create a new asset-aware export.");
775
+ if (bundle.schemaVersion !== 2 || !Array.isArray(bundle.tables)) throw new Error("Unsupported logical export bundle.");
776
+ if (bundle.application !== context.appId) throw new Error("The export belongs to a different application.");
777
+ const tables = bundle.tables;
778
+ const expected = new Map(manifestTables(context.sourceRoot).map((table) => [table.name, table]));
779
+ validateImportBundle({ tables }, expected);
780
+ const exportedByName = new Map(tables.map((table) => [table.name, table]));
781
+ const plan = planImportTables([...expected.values()]);
782
+ for (const table of plan) {
783
+ const exported = exportedByName.get(table.name);
784
+ for (const column of table.deferredColumns) if (table.columnDefinitions[column]?.notNull && exported.rows.some((row) => row[column] != null)) throw new Error(`Import cannot safely defer required cyclic reference ${table.name}.${column}.`);
785
+ if (table.deferredColumns.size > 0 && table.primaryKeys.length !== 1) throw new Error(`Import cannot update cyclic references for ${table.name} without one primary key.`);
786
+ }
787
+ const assetRoot = context.assetRoot || context.paths?.assets || null;
788
+ const verifiedAssets = verifyFilesystemAssets({
789
+ assetBundle: bundle.assets,
790
+ tables,
791
+ sourceRoot: context.sourceRoot,
792
+ assetRoot
793
+ });
794
+ const bundleDigest = bundleContentDigest(serialized);
795
+ const totalRows = () => tables.reduce((count, table) => count + table.rows.length, 0);
796
+ let rowCount = 0;
797
+ await withDatabase(context, async (db) => {
798
+ if (typeof db.transaction !== "function") throw new Error("Logical import requires transactional database support.");
799
+ const executor = db;
800
+ let targetState = await inspectImportTarget(executor, plan, exportedByName);
801
+ if (verifiedAssets.root && targetState === "complete" && !hasCommittedImportJournal({
802
+ stateRoot: context.stateRoot,
803
+ appId: context.appId,
804
+ bundleDigest
805
+ }) && !await importedRowsMatchBundle(executor, plan, exportedByName)) targetState = "dirty";
806
+ if (verifiedAssets.root) {
807
+ const recovery = recoverFilesystemAssets({
808
+ stateRoot: context.stateRoot,
809
+ appId: context.appId,
810
+ bundleDigest,
811
+ assetRoot: verifiedAssets.root,
812
+ targetState
813
+ });
814
+ if (recovery === "complete") {
815
+ rowCount = totalRows();
816
+ return;
817
+ }
818
+ if (recovery === "retry") targetState = await inspectImportTarget(executor, plan, exportedByName);
819
+ }
820
+ if (targetState !== "empty") throw new Error("Import target is not empty.");
821
+ const staged = stageFilesystemAssets({
822
+ verified: verifiedAssets,
823
+ stateRoot: context.stateRoot,
824
+ appId: context.appId,
825
+ bundleDigest
826
+ });
827
+ let committed = false;
828
+ try {
829
+ await context.onImportPhase?.("assets-staged");
830
+ await db.transaction(async (tx) => {
831
+ publishFilesystemAssets(staged);
832
+ await context.onImportPhase?.("assets-published");
833
+ rowCount = await executeImportPlan(tx, plan, exportedByName);
834
+ await context.onImportPhase?.("database-staged");
835
+ verifyPublishedFilesystemAssets(staged);
836
+ });
837
+ committed = true;
838
+ markFilesystemAssetsCommitted(staged);
839
+ finishFilesystemAssets(staged);
840
+ } catch (error) {
841
+ const stateAfterFailure = await inspectImportTarget(executor, plan, exportedByName);
842
+ if (stateAfterFailure === "complete" && (committed || await importedRowsMatchBundle(executor, plan, exportedByName))) {
843
+ finishFilesystemAssets(staged);
844
+ rowCount = totalRows();
845
+ return;
846
+ }
847
+ if (stateAfterFailure === "empty") rollbackFilesystemAssets(staged);
848
+ throw error;
849
+ }
850
+ });
851
+ return {
852
+ path: context.path,
853
+ rowCount,
854
+ assetsIncluded: true,
855
+ assetCount: bundle.assets.entries.length
856
+ };
857
+ }
858
+ //#endregion
859
+ export { publishFilesystemAssets as C, stageFilesystemAssets as D, rollbackFilesystemAssets as E, verifyFilesystemAssets as O, markFilesystemAssetsCommitted as S, recoverFilesystemAssets as T, bundleContentDigest as _, manifestTables as a, finishFilesystemAssets as b, serializeExportBundle as c, AssetPortabilityError as d, MAX_ASSET_BYTES as f, assertAssetRoot as g, MAX_TOTAL_ASSET_BYTES as h, importedRowsMatchBundle as i, verifyPublishedFilesystemAssets as k, validateImportBundle as l, MAX_BUNDLE_BYTES as m, exportApplication as n, planImportTables as o, MAX_ASSET_COUNT as p, importApplication as r, portability_exports as s, executeImportPlan as t, ASSET_MANIFEST_SCHEMA_VERSION as u, collectFilesystemAssets as v, readSensitiveBundle as w, hasCommittedImportJournal as x, digestBytes as y };