@happyvertical/smrt-assets 0.30.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 (119) hide show
  1. package/AGENTS.md +78 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +136 -0
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/asset-association.d.ts +16 -0
  8. package/dist/asset-association.d.ts.map +1 -0
  9. package/dist/asset-associations.d.ts +27 -0
  10. package/dist/asset-associations.d.ts.map +1 -0
  11. package/dist/asset-capabilities.d.ts +137 -0
  12. package/dist/asset-capabilities.d.ts.map +1 -0
  13. package/dist/asset-conventions.d.ts +76 -0
  14. package/dist/asset-conventions.d.ts.map +1 -0
  15. package/dist/asset-metafield.d.ts +27 -0
  16. package/dist/asset-metafield.d.ts.map +1 -0
  17. package/dist/asset-metafields.d.ts +27 -0
  18. package/dist/asset-metafields.d.ts.map +1 -0
  19. package/dist/asset-runtime.d.ts +218 -0
  20. package/dist/asset-runtime.d.ts.map +1 -0
  21. package/dist/asset-serving.d.ts +146 -0
  22. package/dist/asset-serving.d.ts.map +1 -0
  23. package/dist/asset-status.d.ts +15 -0
  24. package/dist/asset-status.d.ts.map +1 -0
  25. package/dist/asset-statuses.d.ts +25 -0
  26. package/dist/asset-statuses.d.ts.map +1 -0
  27. package/dist/asset-store.d.ts +200 -0
  28. package/dist/asset-store.d.ts.map +1 -0
  29. package/dist/asset-type.d.ts +15 -0
  30. package/dist/asset-type.d.ts.map +1 -0
  31. package/dist/asset-types.d.ts +28 -0
  32. package/dist/asset-types.d.ts.map +1 -0
  33. package/dist/asset.d.ts +158 -0
  34. package/dist/asset.d.ts.map +1 -0
  35. package/dist/assets.d.ts +125 -0
  36. package/dist/assets.d.ts.map +1 -0
  37. package/dist/folder.d.ts +16 -0
  38. package/dist/folder.d.ts.map +1 -0
  39. package/dist/folders.d.ts +45 -0
  40. package/dist/folders.d.ts.map +1 -0
  41. package/dist/index.d.ts +21 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +2285 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/manifest.json +4079 -0
  46. package/dist/media-bundle-persistence.d.ts +99 -0
  47. package/dist/media-bundle-persistence.d.ts.map +1 -0
  48. package/dist/owned-asset-helpers.d.ts +20 -0
  49. package/dist/owned-asset-helpers.d.ts.map +1 -0
  50. package/dist/playground.d.ts +2 -0
  51. package/dist/playground.d.ts.map +1 -0
  52. package/dist/playground.js +127 -0
  53. package/dist/playground.js.map +1 -0
  54. package/dist/smrt-knowledge.json +1922 -0
  55. package/dist/svelte/ActionBar.svelte +203 -0
  56. package/dist/svelte/ActionBar.svelte.d.ts +5 -0
  57. package/dist/svelte/ActionBar.svelte.d.ts.map +1 -0
  58. package/dist/svelte/AssetDetail.svelte +521 -0
  59. package/dist/svelte/AssetDetail.svelte.d.ts +35 -0
  60. package/dist/svelte/AssetDetail.svelte.d.ts.map +1 -0
  61. package/dist/svelte/AssetGrid.svelte +351 -0
  62. package/dist/svelte/AssetGrid.svelte.d.ts +5 -0
  63. package/dist/svelte/AssetGrid.svelte.d.ts.map +1 -0
  64. package/dist/svelte/AssetList.svelte +436 -0
  65. package/dist/svelte/AssetList.svelte.d.ts +5 -0
  66. package/dist/svelte/AssetList.svelte.d.ts.map +1 -0
  67. package/dist/svelte/AssetManager.svelte +381 -0
  68. package/dist/svelte/AssetManager.svelte.d.ts +5 -0
  69. package/dist/svelte/AssetManager.svelte.d.ts.map +1 -0
  70. package/dist/svelte/AssetToolbar.svelte +388 -0
  71. package/dist/svelte/AssetToolbar.svelte.d.ts +5 -0
  72. package/dist/svelte/AssetToolbar.svelte.d.ts.map +1 -0
  73. package/dist/svelte/CreateAssetModal.svelte +373 -0
  74. package/dist/svelte/CreateAssetModal.svelte.d.ts +19 -0
  75. package/dist/svelte/CreateAssetModal.svelte.d.ts.map +1 -0
  76. package/dist/svelte/__tests__/ActionBar.test.js +72 -0
  77. package/dist/svelte/__tests__/AssetDetail.test.js +57 -0
  78. package/dist/svelte/__tests__/AssetGrid.test.js +69 -0
  79. package/dist/svelte/__tests__/AssetList.test.js +72 -0
  80. package/dist/svelte/__tests__/AssetManager.test.js +21 -0
  81. package/dist/svelte/__tests__/AssetManagerRoute.test.js +16 -0
  82. package/dist/svelte/__tests__/AssetToolbar.test.js +39 -0
  83. package/dist/svelte/__tests__/CreateAssetModal.test.js +42 -0
  84. package/dist/svelte/i18n.d.ts +76 -0
  85. package/dist/svelte/i18n.d.ts.map +1 -0
  86. package/dist/svelte/i18n.js +87 -0
  87. package/dist/svelte/index.d.ts +19 -0
  88. package/dist/svelte/index.d.ts.map +1 -0
  89. package/dist/svelte/index.js +30 -0
  90. package/dist/svelte/playground/AssetDetailPreview.svelte +131 -0
  91. package/dist/svelte/playground/AssetDetailPreview.svelte.d.ts +8 -0
  92. package/dist/svelte/playground/AssetDetailPreview.svelte.d.ts.map +1 -0
  93. package/dist/svelte/playground/CreateAssetModalPreview.svelte +151 -0
  94. package/dist/svelte/playground/CreateAssetModalPreview.svelte.d.ts +4 -0
  95. package/dist/svelte/playground/CreateAssetModalPreview.svelte.d.ts.map +1 -0
  96. package/dist/svelte/playground.d.ts +60 -0
  97. package/dist/svelte/playground.d.ts.map +1 -0
  98. package/dist/svelte/playground.js +93 -0
  99. package/dist/svelte/routes/AssetManagerRoute.svelte +209 -0
  100. package/dist/svelte/routes/AssetManagerRoute.svelte.d.ts +4 -0
  101. package/dist/svelte/routes/AssetManagerRoute.svelte.d.ts.map +1 -0
  102. package/dist/svelte/routes/index.d.ts +2 -0
  103. package/dist/svelte/routes/index.d.ts.map +1 -0
  104. package/dist/svelte/routes/index.js +1 -0
  105. package/dist/svelte/routes/shared.d.ts +25 -0
  106. package/dist/svelte/routes/shared.d.ts.map +1 -0
  107. package/dist/svelte/routes/shared.js +31 -0
  108. package/dist/svelte/types.d.ts +179 -0
  109. package/dist/svelte/types.d.ts.map +1 -0
  110. package/dist/svelte/types.js +6 -0
  111. package/dist/types.d.ts +80 -0
  112. package/dist/types.d.ts.map +1 -0
  113. package/dist/types.js +2 -0
  114. package/dist/types.js.map +1 -0
  115. package/dist/ui.d.ts +10 -0
  116. package/dist/ui.d.ts.map +1 -0
  117. package/dist/ui.js +85 -0
  118. package/dist/ui.js.map +1 -0
  119. package/package.json +102 -0
package/dist/index.js ADDED
@@ -0,0 +1,2285 @@
1
+ import { ObjectRegistry, foreignKey, smrt, SmrtPolymorphicAssociation, SmrtJunction, SmrtObject, crossPackageRef, SmrtCollection, SmrtHierarchical } from "@happyvertical/smrt-core";
2
+ import { Tag } from "@happyvertical/smrt-tags";
3
+ import { tenantId, TenantScoped, withSystemContext } from "@happyvertical/smrt-tenancy";
4
+ import { getFilesystem, FileNotFoundError } from "@happyvertical/files";
5
+ ObjectRegistry.registerPackageManifest(
6
+ new URL("./manifest.json", import.meta.url)
7
+ );
8
+ var __defProp$3 = Object.defineProperty;
9
+ var __getOwnPropDesc$6 = Object.getOwnPropertyDescriptor;
10
+ var __decorateClass$6 = (decorators, target, key, kind) => {
11
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$6(target, key) : target;
12
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
13
+ if (decorator = decorators[i])
14
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
15
+ if (kind && result) __defProp$3(target, key, result);
16
+ return result;
17
+ };
18
+ let AssetAssociation = class extends SmrtPolymorphicAssociation {
19
+ tenantId = null;
20
+ assetId = "";
21
+ constructor(options = {}) {
22
+ super(options);
23
+ if (options.assetId) this.assetId = options.assetId;
24
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
25
+ }
26
+ };
27
+ __decorateClass$6([
28
+ tenantId({ nullable: true })
29
+ ], AssetAssociation.prototype, "tenantId", 2);
30
+ __decorateClass$6([
31
+ foreignKey("Asset", { required: true })
32
+ ], AssetAssociation.prototype, "assetId", 2);
33
+ AssetAssociation = __decorateClass$6([
34
+ TenantScoped({ mode: "optional" }),
35
+ smrt({
36
+ conflictColumns: ["asset_id", "meta_type", "meta_id", "role"],
37
+ api: { include: ["list", "get", "create", "delete"] },
38
+ mcp: { include: ["list", "get", "create"] },
39
+ cli: true
40
+ })
41
+ ], AssetAssociation);
42
+ var __defProp$2 = Object.defineProperty;
43
+ var __getOwnPropDesc$5 = Object.getOwnPropertyDescriptor;
44
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
45
+ var __decorateClass$5 = (decorators, target, key, kind) => {
46
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$5(target, key) : target;
47
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
48
+ if (decorator = decorators[i])
49
+ result = decorator(result) || result;
50
+ return result;
51
+ };
52
+ var __publicField = (obj, key, value) => __defNormalProp(obj, key + "", value);
53
+ let AssetAssociationCollection = class extends SmrtJunction {
54
+ // Composite left = (metaType, metaId); right = assetId.
55
+ // `leftField` placeholder satisfies the abstract base — the overrides below
56
+ // always handle the composite key explicitly.
57
+ leftField = "metaId";
58
+ rightField = "assetId";
59
+ /**
60
+ * List associations for a polymorphic owner.
61
+ *
62
+ * Composite left key — pass both halves.
63
+ */
64
+ // @ts-expect-error — diverges from SmrtJunction.byLeft(leftId) by arity; see class docstring.
65
+ async byLeft(metaType, metaId, opts = {}) {
66
+ return await this.list({
67
+ // Spread opts first so the fixed polymorphic owner keys always win.
68
+ where: { ...opts, metaType, metaId },
69
+ orderBy: "sort_order ASC"
70
+ });
71
+ }
72
+ /**
73
+ * Create an association. Composite left (metaType, metaId) precedes right (assetId).
74
+ */
75
+ // @ts-expect-error — diverges from SmrtJunction.attach(leftId, rightId) by arity.
76
+ async attach(metaType, metaId, assetId, opts = {}) {
77
+ return await this.create({
78
+ // Spread opts first so the fixed key fields always win.
79
+ ...opts,
80
+ assetId,
81
+ metaType,
82
+ metaId
83
+ });
84
+ }
85
+ /**
86
+ * Delete matching associations. Composite left (metaType, metaId) precedes right (assetId).
87
+ */
88
+ // @ts-expect-error — diverges from SmrtJunction.detach(leftId, rightId) by arity.
89
+ async detach(metaType, metaId, assetId, opts = {}) {
90
+ const links = await this.list({
91
+ where: { ...opts, metaType, metaId, assetId }
92
+ });
93
+ for (const link of links) {
94
+ await link.delete();
95
+ }
96
+ }
97
+ /**
98
+ * Replace all associations for a polymorphic owner with the given asset IDs.
99
+ * Not transactional — see `SmrtJunction.setLinks` for caveats.
100
+ */
101
+ // @ts-expect-error — diverges from SmrtJunction.setLinks(leftId, rightIds) by arity.
102
+ async setLinks(metaType, metaId, assetIds, opts = {}) {
103
+ const snapshotOpts = { ...opts };
104
+ delete snapshotOpts.assetId;
105
+ const existing = await this.list({
106
+ where: { ...snapshotOpts, metaType, metaId }
107
+ });
108
+ for (const link of existing) {
109
+ await link.delete();
110
+ }
111
+ const positionKey = this.positionField;
112
+ for (let i = 0; i < assetIds.length; i++) {
113
+ const rowOpts = { ...opts };
114
+ if (positionKey && rowOpts[positionKey] === void 0) {
115
+ rowOpts[positionKey] = i;
116
+ }
117
+ await this.attach(metaType, metaId, assetIds[i], rowOpts);
118
+ }
119
+ }
120
+ };
121
+ __publicField(AssetAssociationCollection, "_itemClass", AssetAssociation);
122
+ AssetAssociationCollection = __decorateClass$5([
123
+ smrt()
124
+ ], AssetAssociationCollection);
125
+ var __getOwnPropDesc$4 = Object.getOwnPropertyDescriptor;
126
+ var __decorateClass$4 = (decorators, target, key, kind) => {
127
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$4(target, key) : target;
128
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
129
+ if (decorator = decorators[i])
130
+ result = decorator(result) || result;
131
+ return result;
132
+ };
133
+ let AssetStatus = class extends SmrtObject {
134
+ // slug is inherited as an accessor from SmrtObject
135
+ name = "";
136
+ // Display name (e.g., 'Draft', 'Published')
137
+ description = "";
138
+ // Optional description
139
+ constructor(options = {}) {
140
+ super(options);
141
+ if (options.slug) this.slug = options.slug;
142
+ if (options.name) this.name = options.name;
143
+ if (options.description) this.description = options.description;
144
+ }
145
+ /**
146
+ * Get asset status by slug
147
+ *
148
+ * @param slug - The slug to search for
149
+ * @returns AssetStatus instance or null
150
+ */
151
+ static async getBySlug(_slug) {
152
+ return null;
153
+ }
154
+ };
155
+ AssetStatus = __decorateClass$4([
156
+ smrt({
157
+ tableStrategy: "sti",
158
+ api: { include: ["list", "get", "create", "update", "delete"] },
159
+ mcp: { include: ["list", "get", "create"] },
160
+ cli: true
161
+ })
162
+ ], AssetStatus);
163
+ var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
164
+ var __decorateClass$3 = (decorators, target, key, kind) => {
165
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
166
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
167
+ if (decorator = decorators[i])
168
+ result = decorator(result) || result;
169
+ return result;
170
+ };
171
+ let AssetType = class extends SmrtObject {
172
+ // slug is inherited as an accessor from SmrtObject
173
+ name = "";
174
+ // Display name (e.g., 'Image', 'Video')
175
+ description = "";
176
+ // Optional description
177
+ constructor(options = {}) {
178
+ super(options);
179
+ if (options.slug) this.slug = options.slug;
180
+ if (options.name) this.name = options.name;
181
+ if (options.description) this.description = options.description;
182
+ }
183
+ /**
184
+ * Get asset type by slug
185
+ *
186
+ * @param slug - The slug to search for
187
+ * @returns AssetType instance or null
188
+ */
189
+ static async getBySlug(_slug) {
190
+ return null;
191
+ }
192
+ };
193
+ AssetType = __decorateClass$3([
194
+ smrt({
195
+ tableStrategy: "sti",
196
+ api: { include: ["list", "get", "create", "update", "delete"] },
197
+ mcp: { include: ["list", "get", "create"] },
198
+ cli: true
199
+ })
200
+ ], AssetType);
201
+ var __defProp$1 = Object.defineProperty;
202
+ var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
203
+ var __decorateClass$2 = (decorators, target, key, kind) => {
204
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
205
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
206
+ if (decorator = decorators[i])
207
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
208
+ if (kind && result) __defProp$1(target, key, result);
209
+ return result;
210
+ };
211
+ function parseAssetRecord(value) {
212
+ if (!value) return {};
213
+ if (typeof value !== "string") return value;
214
+ if (!value.trim()) return {};
215
+ try {
216
+ const parsed = JSON.parse(value);
217
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
218
+ } catch {
219
+ return {};
220
+ }
221
+ }
222
+ function stringifyAssetRecord(value) {
223
+ if (!value || Object.keys(value).length === 0) return "";
224
+ return JSON.stringify(value);
225
+ }
226
+ let Asset = class extends SmrtObject {
227
+ tenantId = null;
228
+ // Core fields
229
+ name = "";
230
+ // User-friendly name
231
+ // slug is inherited as an accessor from SmrtObject
232
+ sourceUri = "";
233
+ // URI to the actual file (e.g., 's3://bucket/key', 'file:///path')
234
+ mimeType = "";
235
+ // MIME type (e.g., 'image/jpeg', 'video/mp4')
236
+ description = "";
237
+ // Optional description
238
+ metadata = "";
239
+ // JSON metadata owned by SMRT asset processors
240
+ version = 1;
241
+ primaryVersionId = null;
242
+ // Points to first version's ID
243
+ typeSlug = "";
244
+ // FK to AssetType.slug
245
+ statusSlug = "";
246
+ ownerProfileId = null;
247
+ sourceAssetId = null;
248
+ folderId = null;
249
+ // FK to Folder.id
250
+ // Provenance fields
251
+ sourceType = "";
252
+ // 'local', 'shutterstock', 'google-photos', 'upstream-smrt'
253
+ externalId = "";
254
+ // Original ID in upstream source
255
+ externalRefs = "";
256
+ // JSON map of provider references, keyed by provider slug
257
+ // Timestamps
258
+ createdAt = /* @__PURE__ */ new Date();
259
+ updatedAt = /* @__PURE__ */ new Date();
260
+ constructor(options = {}) {
261
+ super(options);
262
+ if (options.name) this.name = options.name;
263
+ if (options.slug) this.slug = options.slug;
264
+ if (options.sourceUri) this.sourceUri = options.sourceUri;
265
+ if (options.mimeType) this.mimeType = options.mimeType;
266
+ if (options.description) this.description = options.description;
267
+ if (options.metadata !== void 0)
268
+ this.metadata = typeof options.metadata === "string" ? options.metadata : stringifyAssetRecord(options.metadata);
269
+ if (options.version !== void 0) this.version = options.version;
270
+ if (options.primaryVersionId !== void 0)
271
+ this.primaryVersionId = options.primaryVersionId;
272
+ if (options.typeSlug) this.typeSlug = options.typeSlug;
273
+ if (options.statusSlug) this.statusSlug = options.statusSlug;
274
+ if (options.ownerProfileId !== void 0)
275
+ this.ownerProfileId = options.ownerProfileId;
276
+ if (options.sourceAssetId !== void 0)
277
+ this.sourceAssetId = options.sourceAssetId;
278
+ if (options.folderId !== void 0) this.folderId = options.folderId;
279
+ if (options.sourceType) this.sourceType = options.sourceType;
280
+ if (options.externalId) this.externalId = options.externalId;
281
+ if (options.externalRefs !== void 0)
282
+ this.externalRefs = typeof options.externalRefs === "string" ? options.externalRefs : stringifyAssetRecord(options.externalRefs);
283
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
284
+ if (options.createdAt) this.createdAt = options.createdAt;
285
+ if (options.updatedAt) this.updatedAt = options.updatedAt;
286
+ }
287
+ getMetadata() {
288
+ return parseAssetRecord(this.metadata);
289
+ }
290
+ setMetadata(metadata) {
291
+ this.metadata = stringifyAssetRecord(metadata);
292
+ }
293
+ mergeMetadata(metadata) {
294
+ this.setMetadata({
295
+ ...this.getMetadata(),
296
+ ...metadata
297
+ });
298
+ }
299
+ getExternalRefs() {
300
+ const refs = parseAssetRecord(this.externalRefs);
301
+ const normalized = {};
302
+ for (const [provider, value] of Object.entries(refs)) {
303
+ if (value && typeof value === "object" && !Array.isArray(value)) {
304
+ normalized[provider] = {
305
+ ...value,
306
+ provider
307
+ };
308
+ }
309
+ }
310
+ return normalized;
311
+ }
312
+ getExternalRef(provider) {
313
+ return this.getExternalRefs()[provider] ?? null;
314
+ }
315
+ setExternalRef(provider, reference) {
316
+ this.externalRefs = stringifyAssetRecord({
317
+ ...this.getExternalRefs(),
318
+ [provider]: {
319
+ ...this.getExternalRef(provider) ?? {},
320
+ ...reference,
321
+ provider: reference.provider ?? provider
322
+ }
323
+ });
324
+ }
325
+ /**
326
+ * Get all tags for this asset from @happyvertical/smrt-tags
327
+ *
328
+ * @returns Array of Tag instances from @happyvertical/smrt-tags package
329
+ */
330
+ async getTags() {
331
+ const db = this.db;
332
+ const rows = await db.list("asset_tags", {
333
+ where: { asset_id: this.id }
334
+ });
335
+ const tags = [];
336
+ for (const row of rows) {
337
+ const tag = await Tag.getBySlug(row.tag_slug);
338
+ if (tag) tags.push(tag);
339
+ }
340
+ return tags;
341
+ }
342
+ /**
343
+ * Check if this asset has a specific tag
344
+ *
345
+ * @param tagSlug - The slug of the tag to check
346
+ * @returns True if the asset has this tag
347
+ */
348
+ async hasTag(tagSlug) {
349
+ const db = this.db;
350
+ const rows = await db.list("asset_tags", {
351
+ where: { asset_id: this.id, tag_slug: tagSlug }
352
+ });
353
+ return rows.length > 0;
354
+ }
355
+ /**
356
+ * Resolve the AssetCollection lazily. Going through `ObjectRegistry`
357
+ * mirrors the pattern used by `SmrtHierarchical._hierarchyCollection`
358
+ * so source/derivative lookups inherit tenant scoping and ORM
359
+ * hydration without hard-coding an import of `./assets` (which would
360
+ * create a module-import cycle).
361
+ *
362
+ * The return type is `SmrtCollection<Asset>` (the framework base, type-
363
+ * only import from core) rather than the concrete `AssetCollection` —
364
+ * importing the concrete class is what would create the cycle, but the
365
+ * base-class shape gives callers full `.get()` / `.list()` type
366
+ * safety here.
367
+ *
368
+ * R5-canon: hardcode the base Asset's qualified key so a different
369
+ * package also registering a class called `Asset` can't be picked
370
+ * by `findClass`'s multi-strategy fallback. Crucially we DON'T
371
+ * resolve via `this.constructor._smrtQualifiedName` — for an STI
372
+ * subclass like `Image`, that would yield the Image collection
373
+ * (which auto-filters `_meta_type = '...:Image'` on `get`/`list`),
374
+ * and `getSource()` / `getDerivatives()` would miss cross-type
375
+ * derivation links (Image derived from a plain Asset, etc.).
376
+ * `sourceAssetId` is a base-table derivation link, so it always
377
+ * resolves through the base Asset collection.
378
+ */
379
+ async _assetCollection() {
380
+ return await ObjectRegistry.getCollection(
381
+ "@happyvertical/smrt-assets:Asset",
382
+ this.options
383
+ );
384
+ }
385
+ /**
386
+ * Get the source asset this one was derived from, if any.
387
+ *
388
+ * Renamed from `getParent` in R3-D. The relationship is "I was produced
389
+ * from that asset" (e.g. a thumbnail's source is its original image),
390
+ * not a structural-hierarchy parent.
391
+ *
392
+ * Goes through the AssetCollection so tenant interceptors and ORM
393
+ * hydration apply — important because a tenant-scoped consumer with
394
+ * cross-tenant derivative chains would otherwise return assets from
395
+ * tenants the caller cannot see, and a raw `db.get` returns
396
+ * snake_case rows that leave camelCase props (e.g. `sourceUri`) at
397
+ * their constructor defaults.
398
+ *
399
+ * @returns Source Asset instance, or null if this asset has no source
400
+ */
401
+ async getSource() {
402
+ if (!this.sourceAssetId) return null;
403
+ const collection = await this._assetCollection();
404
+ return await collection.get({ id: this.sourceAssetId });
405
+ }
406
+ /**
407
+ * Get all assets derived from this one (e.g. thumbnails, variants,
408
+ * transcodes, AI edits).
409
+ *
410
+ * Renamed from `getChildren` in R3-D to match the derivation
411
+ * semantics. Goes through the AssetCollection so tenant interceptors
412
+ * and ORM hydration apply (see `getSource` for why this matters —
413
+ * the pre-R3-D `getChildren` used raw `db.list`, which both bypassed
414
+ * tenant scoping and dropped camelCase property hydration; that
415
+ * latent breakage is fixed here).
416
+ *
417
+ * @returns Array of derivative Asset instances
418
+ */
419
+ async getDerivatives() {
420
+ if (!this.id) return [];
421
+ const collection = await this._assetCollection();
422
+ return await collection.list({
423
+ where: { sourceAssetId: this.id }
424
+ });
425
+ }
426
+ /**
427
+ * Get the type of this asset
428
+ *
429
+ * @returns AssetType instance or null
430
+ */
431
+ async getType() {
432
+ if (!this.typeSlug) return null;
433
+ return await AssetType.getBySlug(this.typeSlug);
434
+ }
435
+ /**
436
+ * Get the status of this asset
437
+ *
438
+ * @returns AssetStatus instance or null
439
+ */
440
+ async getStatus() {
441
+ if (!this.statusSlug) return null;
442
+ return await AssetStatus.getBySlug(this.statusSlug);
443
+ }
444
+ /**
445
+ * Get all associations for this asset
446
+ *
447
+ * @returns Array of AssetAssociation instances
448
+ */
449
+ async getAssociations() {
450
+ const associations = await AssetAssociationCollection.create({
451
+ db: this.db
452
+ });
453
+ return await associations.byRight(this.id);
454
+ }
455
+ /**
456
+ * Associate this asset with a target object
457
+ *
458
+ * @param metaType - Target class name or qualified name (e.g., 'Article' or '@pkg:Article')
459
+ * @param metaId - Target object ID
460
+ * @param role - Association role (default: 'default')
461
+ * @returns The created AssetAssociation
462
+ */
463
+ async associateWith(metaType, metaId, role = "default") {
464
+ const associations = await AssetAssociationCollection.create({
465
+ db: this.db
466
+ });
467
+ return await associations.attach(metaType, metaId, this.id, { role });
468
+ }
469
+ /**
470
+ * Get asset by slug
471
+ *
472
+ * @param slug - The slug to search for
473
+ * @returns Asset instance or null
474
+ */
475
+ static async getBySlug(_slug) {
476
+ return null;
477
+ }
478
+ };
479
+ __decorateClass$2([
480
+ tenantId({ nullable: true })
481
+ ], Asset.prototype, "tenantId", 2);
482
+ __decorateClass$2([
483
+ foreignKey("Asset")
484
+ ], Asset.prototype, "primaryVersionId", 2);
485
+ __decorateClass$2([
486
+ crossPackageRef("@happyvertical/smrt-profiles:Profile")
487
+ ], Asset.prototype, "ownerProfileId", 2);
488
+ __decorateClass$2([
489
+ foreignKey("Asset")
490
+ ], Asset.prototype, "sourceAssetId", 2);
491
+ __decorateClass$2([
492
+ foreignKey("Folder")
493
+ ], Asset.prototype, "folderId", 2);
494
+ Asset = __decorateClass$2([
495
+ TenantScoped({ mode: "optional" }),
496
+ smrt({
497
+ tableStrategy: "sti",
498
+ api: { include: ["list", "get", "create", "update", "delete"] },
499
+ mcp: { include: ["list", "get", "create", "update"] },
500
+ cli: true
501
+ })
502
+ ], Asset);
503
+ class AssetCapabilityUnavailableError extends Error {
504
+ constructor(capability, message = `No asset capability provider is registered for ${capability}.`) {
505
+ super(message);
506
+ this.capability = capability;
507
+ this.name = "AssetCapabilityUnavailableError";
508
+ }
509
+ capability;
510
+ }
511
+ class AssetCapabilitySkippedError extends Error {
512
+ constructor(capability, message) {
513
+ super(message);
514
+ this.capability = capability;
515
+ this.name = "AssetCapabilitySkippedError";
516
+ }
517
+ capability;
518
+ }
519
+ const ASSET_ROLES = {
520
+ SOURCE_DOCUMENT: "source_document",
521
+ DOCUMENT_IMAGE: "document_image",
522
+ THUMBNAIL: "thumbnail",
523
+ ASSET_VARIANT: "asset_variant",
524
+ PROOF: "proof",
525
+ DERIVATION_SOURCE: "derivation_source",
526
+ ATTACHMENT: "attachment",
527
+ HERO: "hero"
528
+ };
529
+ const ASSET_METADATA_KEYS = {
530
+ EXTRACTION_STATUS: "extractionStatus",
531
+ EXTRACTION_ERROR: "extractionError",
532
+ EXTRACTED_AT: "extractedAt",
533
+ SOURCE_URL: "sourceUrl",
534
+ SOURCE_HASH: "sourceHash",
535
+ PAGE_NUMBER: "pageNumber"
536
+ };
537
+ const ASSET_EXTRACTION_STATUS = {
538
+ PENDING: "pending",
539
+ RUNNING: "running",
540
+ SUCCEEDED: "succeeded",
541
+ FAILED: "failed"
542
+ };
543
+ var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
544
+ var __decorateClass$1 = (decorators, target, key, kind) => {
545
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
546
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
547
+ if (decorator = decorators[i])
548
+ result = decorator(result) || result;
549
+ return result;
550
+ };
551
+ let AssetMetafield = class extends SmrtObject {
552
+ // slug is inherited as an accessor from SmrtObject
553
+ name = "";
554
+ // Display name (e.g., 'Width', 'Height')
555
+ validation = "";
556
+ // JSON validation rules stored as text
557
+ constructor(options = {}) {
558
+ super(options);
559
+ if (options.slug) this.slug = options.slug;
560
+ if (options.name) this.name = options.name;
561
+ if (options.validation) this.validation = options.validation;
562
+ }
563
+ /**
564
+ * Get validation rules as parsed object
565
+ *
566
+ * @returns Parsed validation object or empty object if no validation
567
+ */
568
+ getValidation() {
569
+ if (!this.validation) return {};
570
+ try {
571
+ return JSON.parse(this.validation);
572
+ } catch {
573
+ return {};
574
+ }
575
+ }
576
+ /**
577
+ * Set validation rules from object
578
+ *
579
+ * @param rules - Validation rules object
580
+ */
581
+ setValidation(rules) {
582
+ this.validation = JSON.stringify(rules);
583
+ }
584
+ /**
585
+ * Get asset metafield by slug
586
+ *
587
+ * @param slug - The slug to search for
588
+ * @returns AssetMetafield instance or null
589
+ */
590
+ static async getBySlug(_slug) {
591
+ return null;
592
+ }
593
+ };
594
+ AssetMetafield = __decorateClass$1([
595
+ smrt({
596
+ tableStrategy: "sti",
597
+ api: { include: ["list", "get", "create", "update", "delete"] },
598
+ mcp: { include: ["list", "get", "create"] },
599
+ cli: true
600
+ })
601
+ ], AssetMetafield);
602
+ class AssetMetafieldCollection extends SmrtCollection {
603
+ static _itemClass = AssetMetafield;
604
+ /**
605
+ * Get or create an asset metafield by slug
606
+ *
607
+ * @param slug - The metafield slug
608
+ * @param name - The display name (defaults to slug)
609
+ * @param validation - Optional validation rules (JSON string or object)
610
+ * @returns The existing or newly created AssetMetafield
611
+ */
612
+ async getOrCreate(slug, name, validation) {
613
+ const existing = await this.list({ where: { slug }, limit: 1 });
614
+ if (existing.length > 0) {
615
+ return existing[0];
616
+ }
617
+ const validationString = typeof validation === "string" ? validation : validation ? JSON.stringify(validation) : "";
618
+ return await this.create({
619
+ slug,
620
+ name: name || slug,
621
+ validation: validationString
622
+ });
623
+ }
624
+ /**
625
+ * Initialize common asset metafields
626
+ *
627
+ * Creates standard metafields with validation rules:
628
+ * - width (integer, min: 0)
629
+ * - height (integer, min: 0)
630
+ * - duration (number, min: 0)
631
+ * - size (integer, min: 0)
632
+ * - author (string)
633
+ * - copyright (string)
634
+ */
635
+ async initializeCommonMetafields() {
636
+ await this.getOrCreate("width", "Width", {
637
+ type: "integer",
638
+ min: 0,
639
+ description: "Width in pixels"
640
+ });
641
+ await this.getOrCreate("height", "Height", {
642
+ type: "integer",
643
+ min: 0,
644
+ description: "Height in pixels"
645
+ });
646
+ await this.getOrCreate("duration", "Duration", {
647
+ type: "number",
648
+ min: 0,
649
+ description: "Duration in seconds"
650
+ });
651
+ await this.getOrCreate("size", "File Size", {
652
+ type: "integer",
653
+ min: 0,
654
+ description: "File size in bytes"
655
+ });
656
+ await this.getOrCreate("author", "Author", {
657
+ type: "string",
658
+ description: "Content creator"
659
+ });
660
+ await this.getOrCreate("copyright", "Copyright", {
661
+ type: "string",
662
+ description: "Copyright notice"
663
+ });
664
+ }
665
+ }
666
+ const MIME_TO_EXT = {
667
+ "image/png": "png",
668
+ "image/jpeg": "jpg",
669
+ "image/webp": "webp",
670
+ "image/gif": "gif",
671
+ "image/svg+xml": "svg",
672
+ "video/mp4": "mp4",
673
+ "video/webm": "webm",
674
+ "audio/wav": "wav",
675
+ "audio/mpeg": "mp3",
676
+ "audio/ogg": "ogg",
677
+ "audio/flac": "flac",
678
+ "application/pdf": "pdf",
679
+ "application/json": "json",
680
+ "text/plain": "txt"
681
+ };
682
+ function serializeStoreMetadata(metadata) {
683
+ if (metadata === void 0) return void 0;
684
+ if (metadata === null) return "";
685
+ if (typeof metadata === "string") return metadata;
686
+ return JSON.stringify(metadata);
687
+ }
688
+ function normalizeProviderOptions(providerOrBasePath) {
689
+ if (typeof providerOrBasePath === "string") {
690
+ return { type: "local", basePath: providerOrBasePath };
691
+ }
692
+ return providerOrBasePath;
693
+ }
694
+ class AssetStore {
695
+ constructor(providerOrBasePath, collection, options = {}) {
696
+ this.collection = collection;
697
+ this.fsOptions = normalizeProviderOptions(providerOrBasePath);
698
+ this.resolver = options.resolver;
699
+ }
700
+ collection;
701
+ fs = null;
702
+ fsOptions;
703
+ fsCache = /* @__PURE__ */ new Map();
704
+ resolver;
705
+ /** The base path for local storage, or empty string for non-local providers */
706
+ get basePath() {
707
+ return this.fsOptions.basePath ?? "";
708
+ }
709
+ /**
710
+ * Initialize the filesystem adapter.
711
+ * Must be called before any file operations.
712
+ */
713
+ async initialize() {
714
+ this.fs = await getFilesystem(this.fsOptions);
715
+ this.fsCache.set(this.providerCacheKey(this.fsOptions), this.fs);
716
+ return this;
717
+ }
718
+ /**
719
+ * Get the initialized filesystem (throws if not initialized)
720
+ */
721
+ getFs() {
722
+ if (!this.fs) {
723
+ throw new Error("AssetStore not initialized. Call initialize() first.");
724
+ }
725
+ return this.fs;
726
+ }
727
+ providerCacheKey(providerOptions) {
728
+ return JSON.stringify(providerOptions);
729
+ }
730
+ async getFilesystemForOptions(providerOptions) {
731
+ const key = this.providerCacheKey(providerOptions);
732
+ const cached = this.fsCache.get(key);
733
+ if (cached) return cached;
734
+ const filesystem = await getFilesystem(providerOptions);
735
+ this.fsCache.set(key, filesystem);
736
+ return filesystem;
737
+ }
738
+ /**
739
+ * Build a sourceUri for the given file path based on the provider type.
740
+ */
741
+ buildSourceUri(filePath) {
742
+ return AssetStore.buildSourceUriForProvider(filePath, this.fsOptions);
743
+ }
744
+ static buildSourceUriForProvider(filePath, providerOptions) {
745
+ const type = providerOptions.type;
746
+ if (type === "s3") {
747
+ const bucket = providerOptions.bucket ?? "";
748
+ return `s3://${bucket}/${filePath}`;
749
+ }
750
+ const base = providerOptions.basePath ?? "";
751
+ return base ? `file://${base}/${filePath}` : `file://${filePath}`;
752
+ }
753
+ static providerRelativePath(filePath, providerOptions) {
754
+ const base = providerOptions.basePath ?? "";
755
+ return base && filePath.startsWith(base) ? filePath.slice(base.length + 1) : filePath;
756
+ }
757
+ static requireAssetId(asset) {
758
+ if (!asset.id) {
759
+ throw new Error("Asset must be saved before storing file data.");
760
+ }
761
+ return asset.id;
762
+ }
763
+ async resolveStorage(request) {
764
+ const defaultFilesystem = this.getFs();
765
+ const resolution = this.resolver ? await this.resolver({
766
+ ...request,
767
+ defaultProviderOptions: this.fsOptions,
768
+ defaultFilesystem
769
+ }) : void 0;
770
+ if (request.operation === "write" && resolution?.filesystem && !resolution.providerOptions && !resolution.sourceUri) {
771
+ throw new Error(
772
+ "Asset storage resolver must return providerOptions or sourceUri when overriding filesystem for write operations."
773
+ );
774
+ }
775
+ const providerOptions = resolution?.providerOptions ? normalizeProviderOptions(resolution.providerOptions) : this.fsOptions;
776
+ const path = AssetStore.providerRelativePath(
777
+ resolution?.path ?? request.path,
778
+ providerOptions
779
+ );
780
+ const sourceUri = resolution?.sourceUri ?? (resolution?.path || resolution?.providerOptions ? AssetStore.buildSourceUriForProvider(path, providerOptions) : request.sourceUri);
781
+ const filesystem = resolution?.filesystem ?? (providerOptions === this.fsOptions ? defaultFilesystem : await this.getFilesystemForOptions(providerOptions));
782
+ return {
783
+ filesystem,
784
+ providerOptions,
785
+ path,
786
+ sourceUri
787
+ };
788
+ }
789
+ async writeAssetData(asset, data, opts) {
790
+ const ext = MIME_TO_EXT[opts.mimeType] ?? "bin";
791
+ const typeSlug = opts.typeSlug ?? "file";
792
+ const assetId = AssetStore.requireAssetId(asset);
793
+ const filePath = `${typeSlug}/${assetId}.${ext}`;
794
+ const sourceUri = this.buildSourceUri(filePath);
795
+ const target = await this.resolveStorage({
796
+ operation: "write",
797
+ asset,
798
+ path: filePath,
799
+ sourceUri,
800
+ mimeType: opts.mimeType,
801
+ typeSlug
802
+ });
803
+ await target.filesystem.write(target.path, data, { createParents: true });
804
+ return target.sourceUri;
805
+ }
806
+ /**
807
+ * Write file data for an existing Asset record (no DB record created).
808
+ *
809
+ * Use this when you've already created the record (e.g., via a
810
+ * collection.create()) and only need to persist the file data.
811
+ *
812
+ * @param asset - The existing asset to write data for
813
+ * @param data - File data as a Buffer
814
+ * @param opts - Storage options (mimeType required)
815
+ * @returns The sourceUri for the written file
816
+ */
817
+ async storeFile(asset, data, opts) {
818
+ return this.writeAssetData(asset, data, opts);
819
+ }
820
+ /**
821
+ * Write buffer to disk and create an Asset record.
822
+ *
823
+ * @param name - Human-readable name for the asset
824
+ * @param data - File data as a Buffer
825
+ * @param opts - Storage options (mimeType required)
826
+ * @returns Created Asset instance
827
+ */
828
+ async store(name, data, opts) {
829
+ const typeSlug = opts.typeSlug ?? "file";
830
+ const asset = await this.collection.create({
831
+ name,
832
+ mimeType: opts.mimeType,
833
+ typeSlug,
834
+ statusSlug: opts.statusSlug ?? "active",
835
+ sourceAssetId: opts.sourceAssetId ?? null,
836
+ description: opts.description ?? "",
837
+ sourceType: opts.sourceType ?? "",
838
+ externalId: opts.externalId ?? "",
839
+ metadata: serializeStoreMetadata(opts.metadata) ?? "",
840
+ sourceUri: ""
841
+ // Will be updated after file write
842
+ });
843
+ try {
844
+ asset.sourceUri = await this.writeAssetData(asset, data, {
845
+ mimeType: opts.mimeType,
846
+ typeSlug
847
+ });
848
+ } catch (err) {
849
+ await asset.delete();
850
+ throw err;
851
+ }
852
+ await asset.save();
853
+ return asset;
854
+ }
855
+ /**
856
+ * Store a new version of an existing asset.
857
+ *
858
+ * @param asset - The existing asset to version
859
+ * @param data - File data for the new version
860
+ * @param opts - Optional overrides for store options
861
+ * @returns The newly created version Asset
862
+ */
863
+ async storeVersion(asset, data, opts = {}) {
864
+ const primaryVersionId = asset.primaryVersionId ?? AssetStore.requireAssetId(asset);
865
+ const { metadata, ...versionOptions } = opts;
866
+ const versionUpdates = { ...versionOptions };
867
+ const serializedMetadata = serializeStoreMetadata(metadata);
868
+ if (serializedMetadata !== void 0) {
869
+ versionUpdates.metadata = serializedMetadata;
870
+ } else {
871
+ delete versionUpdates.metadata;
872
+ }
873
+ const newVersion = await this.collection.createNewVersion(
874
+ primaryVersionId,
875
+ "",
876
+ // sourceUri will be set by store
877
+ versionUpdates
878
+ );
879
+ const mimeType = opts.mimeType ?? asset.mimeType;
880
+ const typeSlug = opts.typeSlug ?? asset.typeSlug ?? "file";
881
+ try {
882
+ newVersion.sourceUri = await this.writeAssetData(newVersion, data, {
883
+ mimeType,
884
+ typeSlug
885
+ });
886
+ } catch (err) {
887
+ await newVersion.delete();
888
+ throw err;
889
+ }
890
+ if (opts.mimeType) newVersion.mimeType = opts.mimeType;
891
+ await newVersion.save();
892
+ return newVersion;
893
+ }
894
+ /**
895
+ * Read file data for a specific version of an asset.
896
+ *
897
+ * @param asset - The asset (any version in the chain)
898
+ * @param version - The version number to read
899
+ * @returns File data as a Buffer
900
+ */
901
+ async readVersion(asset, version) {
902
+ const primaryVersionId = asset.primaryVersionId ?? AssetStore.requireAssetId(asset);
903
+ const versions = await this.collection.listVersions(primaryVersionId);
904
+ const targetVersion = versions.find((v) => v.version === version);
905
+ if (!targetVersion) {
906
+ throw new Error(
907
+ `Version ${version} not found for asset ${primaryVersionId}`
908
+ );
909
+ }
910
+ return this.read(targetVersion);
911
+ }
912
+ /**
913
+ * Read file data from an Asset's sourceUri.
914
+ *
915
+ * @param asset - Asset to read data for
916
+ * @returns File data as a Buffer
917
+ */
918
+ async read(asset) {
919
+ const filePath = AssetStore.pathFromUri(asset.sourceUri);
920
+ const target = await this.resolveStorage({
921
+ operation: "read",
922
+ asset,
923
+ path: AssetStore.providerRelativePath(filePath, this.fsOptions),
924
+ sourceUri: asset.sourceUri
925
+ });
926
+ return await target.filesystem.read(target.path, { raw: true });
927
+ }
928
+ /**
929
+ * Read file by asset ID.
930
+ *
931
+ * @param id - Asset ID to look up
932
+ * @returns Object with data Buffer and Asset, or null if not found
933
+ */
934
+ async readById(id) {
935
+ const asset = await this.collection.get({ id });
936
+ if (!asset) return null;
937
+ try {
938
+ const data = await this.read(asset);
939
+ return { data, asset };
940
+ } catch {
941
+ return null;
942
+ }
943
+ }
944
+ /**
945
+ * Delete file from disk and remove the Asset record.
946
+ *
947
+ * @param asset - Asset to remove
948
+ */
949
+ async remove(asset) {
950
+ if (asset.sourceUri) {
951
+ const filePath = AssetStore.pathFromUri(asset.sourceUri);
952
+ const target = await this.resolveStorage({
953
+ operation: "delete",
954
+ asset,
955
+ path: AssetStore.providerRelativePath(filePath, this.fsOptions),
956
+ sourceUri: asset.sourceUri
957
+ });
958
+ try {
959
+ await target.filesystem.delete(target.path);
960
+ } catch (err) {
961
+ if (!(err instanceof FileNotFoundError)) {
962
+ throw err;
963
+ }
964
+ }
965
+ }
966
+ await asset.delete();
967
+ }
968
+ /**
969
+ * Extract filesystem path from a sourceUri.
970
+ * Handles file://, s3://, and plain paths.
971
+ *
972
+ * @param sourceUri - Asset sourceUri (e.g., 'file:///path/to/file.mp4', 's3://bucket/key')
973
+ * @returns Filesystem path
974
+ */
975
+ static pathFromUri(sourceUri) {
976
+ if (sourceUri.startsWith("file://")) {
977
+ return sourceUri.slice(7);
978
+ }
979
+ if (sourceUri.startsWith("s3://")) {
980
+ const withoutScheme = sourceUri.slice(5);
981
+ const slashIdx = withoutScheme.indexOf("/");
982
+ return slashIdx >= 0 ? withoutScheme.slice(slashIdx + 1) : withoutScheme;
983
+ }
984
+ return sourceUri;
985
+ }
986
+ }
987
+ class AssetCollection extends SmrtCollection {
988
+ static _itemClass = Asset;
989
+ // ─────────────────────────────────────────────────────────────────────────────
990
+ // Tenant-Aware Query Methods
991
+ // ─────────────────────────────────────────────────────────────────────────────
992
+ /**
993
+ * Find all assets belonging to a specific tenant
994
+ *
995
+ * @param tenantId - The tenant ID to filter by
996
+ * @returns Array of assets belonging to this tenant
997
+ */
998
+ async findByTenant(tenantId2) {
999
+ return await this.list({ where: { tenantId: tenantId2 } });
1000
+ }
1001
+ /**
1002
+ * Find all global assets (assets without a tenant)
1003
+ *
1004
+ * @returns Array of global assets
1005
+ */
1006
+ async findGlobal() {
1007
+ return await this.list({ where: { tenantId: null } });
1008
+ }
1009
+ /**
1010
+ * Find assets belonging to a tenant plus all global assets
1011
+ *
1012
+ * @param tenantId - The tenant ID to include
1013
+ * @returns Array of tenant-specific and global assets
1014
+ */
1015
+ async findWithGlobals(tenantId2) {
1016
+ return await this.query(
1017
+ `SELECT * FROM ${this.tableName} WHERE tenant_id = ? OR tenant_id IS NULL`,
1018
+ [tenantId2]
1019
+ );
1020
+ }
1021
+ /**
1022
+ * Add a tag to an asset (uses @smrt/tags)
1023
+ *
1024
+ * @param assetId - The asset ID to tag
1025
+ * @param tagSlug - The tag slug from @smrt/tags
1026
+ */
1027
+ async addTag(assetId, tagSlug) {
1028
+ const db = this.db;
1029
+ await db.upsert("asset_tags", ["asset_id", "tag_slug"], {
1030
+ asset_id: assetId,
1031
+ tag_slug: tagSlug,
1032
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
1033
+ });
1034
+ }
1035
+ /**
1036
+ * Remove a tag from an asset
1037
+ *
1038
+ * @param assetId - The asset ID
1039
+ * @param tagSlug - The tag slug to remove
1040
+ */
1041
+ async removeTag(assetId, tagSlug) {
1042
+ const db = this.db;
1043
+ await db.delete("asset_tags", {
1044
+ asset_id: assetId,
1045
+ tag_slug: tagSlug
1046
+ });
1047
+ }
1048
+ /**
1049
+ * Get all assets with a specific tag
1050
+ *
1051
+ * @param tagSlug - The tag slug to filter by
1052
+ * @returns Array of assets with this tag
1053
+ */
1054
+ async getByTag(tagSlug) {
1055
+ const db = this.db;
1056
+ const rows = await db.list("asset_tags", { tag_slug: tagSlug });
1057
+ const assets = [];
1058
+ for (const row of rows) {
1059
+ const asset = await this.get({ id: row.asset_id });
1060
+ if (asset) assets.push(asset);
1061
+ }
1062
+ return assets;
1063
+ }
1064
+ /**
1065
+ * Get assets by type
1066
+ *
1067
+ * @param typeSlug - The asset type slug (e.g., 'image', 'video')
1068
+ * @returns Array of assets matching the type
1069
+ */
1070
+ async getByType(typeSlug) {
1071
+ return await this.list({ where: { typeSlug } });
1072
+ }
1073
+ /**
1074
+ * Get assets by status
1075
+ *
1076
+ * @param statusSlug - The asset status slug (e.g., 'published', 'draft')
1077
+ * @returns Array of assets matching the status
1078
+ */
1079
+ async getByStatus(statusSlug) {
1080
+ return await this.list({ where: { statusSlug } });
1081
+ }
1082
+ /**
1083
+ * Get assets by owner
1084
+ *
1085
+ * @param ownerProfileId - The profile ID of the owner
1086
+ * @returns Array of assets owned by this profile
1087
+ */
1088
+ async getByOwner(ownerProfileId) {
1089
+ return await this.list({ where: { ownerProfileId } });
1090
+ }
1091
+ /**
1092
+ * Create a new version of an existing asset
1093
+ *
1094
+ * @param primaryVersionId - The primary version ID (first version's ID)
1095
+ * @param newSourceUri - The new source URI for this version
1096
+ * @param updates - Optional additional updates
1097
+ * @returns The newly created asset version
1098
+ */
1099
+ async createNewVersion(primaryVersionId, newSourceUri, updates = {}) {
1100
+ const versions = await this.listVersions(primaryVersionId);
1101
+ if (versions.length === 0) {
1102
+ throw new Error(
1103
+ `No asset found with primary version ID: ${primaryVersionId}`
1104
+ );
1105
+ }
1106
+ versions.sort((a, b) => b.version - a.version);
1107
+ const latestVersion = versions[0];
1108
+ const newVersionNumber = latestVersion.version + 1;
1109
+ const primary = versions.find((v) => v.id === primaryVersionId) ?? versions[versions.length - 1];
1110
+ const baseSlug = String(primary?.slug ?? "") || "asset";
1111
+ const takenInChain = new Set(
1112
+ versions.map((v) => v.slug).filter(Boolean)
1113
+ );
1114
+ let candidate = `${baseSlug}-v${newVersionNumber}`;
1115
+ for (let attempt = 1; ; attempt += 1) {
1116
+ const collides = takenInChain.has(candidate) || await this.get({ slug: candidate }) !== null;
1117
+ if (!collides) break;
1118
+ candidate = `${baseSlug}-v${newVersionNumber}-${attempt}`;
1119
+ }
1120
+ return await this.create({
1121
+ ...latestVersion,
1122
+ id: void 0,
1123
+ // Generate new ID
1124
+ slug: candidate,
1125
+ sourceUri: newSourceUri,
1126
+ version: newVersionNumber,
1127
+ primaryVersionId,
1128
+ createdAt: /* @__PURE__ */ new Date(),
1129
+ updatedAt: /* @__PURE__ */ new Date(),
1130
+ ...updates
1131
+ });
1132
+ }
1133
+ /**
1134
+ * Get the latest version of an asset
1135
+ *
1136
+ * @param primaryVersionId - The primary version ID
1137
+ * @returns The latest version or null
1138
+ */
1139
+ async getLatestVersion(primaryVersionId) {
1140
+ const versions = await this.listVersions(primaryVersionId);
1141
+ if (versions.length === 0) return null;
1142
+ versions.sort((a, b) => b.version - a.version);
1143
+ return versions[0];
1144
+ }
1145
+ /**
1146
+ * List all versions of an asset
1147
+ *
1148
+ * @param primaryVersionId - The primary version ID
1149
+ * @returns Array of all asset versions, ordered by version number
1150
+ */
1151
+ async listVersions(primaryVersionId) {
1152
+ const [chained, primary] = await Promise.all([
1153
+ this.list({ where: { primaryVersionId } }),
1154
+ this.get({ id: primaryVersionId })
1155
+ ]);
1156
+ const byId = /* @__PURE__ */ new Map();
1157
+ for (const asset of [...primary ? [primary] : [], ...chained]) {
1158
+ if (asset?.id) byId.set(asset.id, asset);
1159
+ }
1160
+ const assets = Array.from(byId.values());
1161
+ assets.sort((a, b) => a.version - b.version);
1162
+ return assets;
1163
+ }
1164
+ /**
1165
+ * Get derivative assets of a source asset.
1166
+ *
1167
+ * Renamed from `getChildren(parentId)` in R3-D to match the rename of
1168
+ * the underlying column (`parent_id` → `source_asset_id`) and method
1169
+ * (`Asset.getChildren` → `Asset.getDerivatives`).
1170
+ *
1171
+ * @param sourceAssetId - The source asset ID
1172
+ * @returns Array of derivative assets
1173
+ */
1174
+ async getDerivatives(sourceAssetId) {
1175
+ return await this.list({ where: { sourceAssetId } });
1176
+ }
1177
+ /**
1178
+ * Get assets by MIME type pattern
1179
+ *
1180
+ * @param mimePattern - MIME type pattern (e.g., 'image/*', 'video/mp4')
1181
+ * @returns Array of matching assets
1182
+ */
1183
+ async getByMimeType(mimePattern) {
1184
+ const pattern = mimePattern.replace("*", "%");
1185
+ return await this.list({
1186
+ where: { "mimeType like": pattern }
1187
+ });
1188
+ }
1189
+ /**
1190
+ * Rollback to a previous version by creating a new version with the target's content.
1191
+ * Does NOT delete intermediate versions (safe rollback).
1192
+ *
1193
+ * @param primaryVersionId - The primary version ID of the version chain
1194
+ * @param targetVersion - The version number to rollback to
1195
+ * @returns The newly created asset version with content copied from target
1196
+ */
1197
+ async rollbackToVersion(primaryVersionId, targetVersion) {
1198
+ const versions = await this.listVersions(primaryVersionId);
1199
+ const target = versions.find((v) => v.version === targetVersion);
1200
+ if (!target) {
1201
+ throw new Error(
1202
+ `Version ${targetVersion} not found for asset ${primaryVersionId}`
1203
+ );
1204
+ }
1205
+ return await this.createNewVersion(primaryVersionId, target.sourceUri, {
1206
+ description: `Rollback to version ${targetVersion}`
1207
+ });
1208
+ }
1209
+ /**
1210
+ * Get assets in a specific folder
1211
+ *
1212
+ * @param folderId - The folder ID to list contents for
1213
+ * @returns Array of assets in this folder
1214
+ */
1215
+ async getByFolder(folderId) {
1216
+ return await this.list({ where: { folderId } });
1217
+ }
1218
+ }
1219
+ class AssetRuntime {
1220
+ constructor(collection, associations, store, capabilityProviders = []) {
1221
+ this.collection = collection;
1222
+ this.associations = associations;
1223
+ this.store = store;
1224
+ this.capabilityProviders = [...capabilityProviders];
1225
+ }
1226
+ collection;
1227
+ associations;
1228
+ store;
1229
+ capabilityProviders;
1230
+ registerCapabilityProvider(provider) {
1231
+ this.capabilityProviders.push(provider);
1232
+ return this;
1233
+ }
1234
+ providersFor(capability) {
1235
+ const providers = this.capabilityProviders.filter(
1236
+ (candidate) => typeof candidate[capability] === "function"
1237
+ );
1238
+ if (providers.length === 0) {
1239
+ throw new AssetCapabilityUnavailableError(capability);
1240
+ }
1241
+ return providers;
1242
+ }
1243
+ async processAsset(asset, input = {}) {
1244
+ let skipped = null;
1245
+ for (const provider of this.providersFor("processAsset")) {
1246
+ const processAsset = provider.processAsset;
1247
+ if (!processAsset) continue;
1248
+ try {
1249
+ return await processAsset({
1250
+ runtime: this,
1251
+ asset,
1252
+ ...input
1253
+ });
1254
+ } catch (cause) {
1255
+ if (cause instanceof AssetCapabilitySkippedError) {
1256
+ skipped = cause;
1257
+ continue;
1258
+ }
1259
+ throw cause;
1260
+ }
1261
+ }
1262
+ throw new AssetCapabilityUnavailableError("processAsset", skipped?.message);
1263
+ }
1264
+ async ensureVariant(asset, request) {
1265
+ let skipped = null;
1266
+ for (const provider of this.providersFor("ensureVariant")) {
1267
+ const ensureVariant = provider.ensureVariant;
1268
+ if (!ensureVariant) continue;
1269
+ try {
1270
+ return await ensureVariant({
1271
+ runtime: this,
1272
+ asset,
1273
+ request
1274
+ });
1275
+ } catch (cause) {
1276
+ if (cause instanceof AssetCapabilitySkippedError) {
1277
+ skipped = cause;
1278
+ continue;
1279
+ }
1280
+ throw cause;
1281
+ }
1282
+ }
1283
+ throw new AssetCapabilityUnavailableError(
1284
+ "ensureVariant",
1285
+ skipped?.message
1286
+ );
1287
+ }
1288
+ async searchNearbyAssets(input) {
1289
+ let skipped = null;
1290
+ for (const provider of this.providersFor("searchNearbyAssets")) {
1291
+ const searchNearbyAssets = provider.searchNearbyAssets;
1292
+ if (!searchNearbyAssets) continue;
1293
+ try {
1294
+ return await searchNearbyAssets({
1295
+ runtime: this,
1296
+ ...input
1297
+ });
1298
+ } catch (cause) {
1299
+ if (cause instanceof AssetCapabilitySkippedError) {
1300
+ skipped = cause;
1301
+ continue;
1302
+ }
1303
+ throw cause;
1304
+ }
1305
+ }
1306
+ throw new AssetCapabilityUnavailableError(
1307
+ "searchNearbyAssets",
1308
+ skipped?.message
1309
+ );
1310
+ }
1311
+ async syncExternalAsset(asset, input = {}) {
1312
+ let skipped = null;
1313
+ for (const provider of this.providersFor("syncExternalAsset")) {
1314
+ const syncExternalAsset = provider.syncExternalAsset;
1315
+ if (!syncExternalAsset) continue;
1316
+ try {
1317
+ return await syncExternalAsset({
1318
+ runtime: this,
1319
+ asset,
1320
+ ...input
1321
+ });
1322
+ } catch (cause) {
1323
+ if (cause instanceof AssetCapabilitySkippedError) {
1324
+ skipped = cause;
1325
+ continue;
1326
+ }
1327
+ throw cause;
1328
+ }
1329
+ }
1330
+ throw new AssetCapabilityUnavailableError(
1331
+ "syncExternalAsset",
1332
+ skipped?.message
1333
+ );
1334
+ }
1335
+ async submitAssetWorkflow(asset, input) {
1336
+ let skipped = null;
1337
+ for (const provider of this.providersFor("submitAssetWorkflow")) {
1338
+ const submitAssetWorkflow = provider.submitAssetWorkflow;
1339
+ if (!submitAssetWorkflow) continue;
1340
+ try {
1341
+ return await submitAssetWorkflow({
1342
+ runtime: this,
1343
+ asset,
1344
+ ...input
1345
+ });
1346
+ } catch (cause) {
1347
+ if (cause instanceof AssetCapabilitySkippedError) {
1348
+ skipped = cause;
1349
+ continue;
1350
+ }
1351
+ throw cause;
1352
+ }
1353
+ }
1354
+ throw new AssetCapabilityUnavailableError(
1355
+ "submitAssetWorkflow",
1356
+ skipped?.message
1357
+ );
1358
+ }
1359
+ /**
1360
+ * Create a new source asset with both a record and bytes on disk.
1361
+ *
1362
+ * This is the same as `AssetStore.store()`, but exposed on the runtime
1363
+ * so callers only need one handle.
1364
+ */
1365
+ storeSourceAsset(name, data, opts) {
1366
+ return this.store.store(name, data, opts);
1367
+ }
1368
+ /**
1369
+ * Create a derivative of `source`, persist its bytes, and optionally
1370
+ * record a provenance association.
1371
+ *
1372
+ * The new asset's `sourceAssetId` always points at `source.id`. When
1373
+ * `linkAssociation` is true (the default), the runtime also writes
1374
+ * an `AssetAssociation` so queries by role (e.g. "all `document_image`
1375
+ * derivatives for this `source_document`") work without scanning
1376
+ * `source_asset_id` chains.
1377
+ */
1378
+ async storeDerivedAsset(source, name, data, opts) {
1379
+ if (!source.id) {
1380
+ throw new Error(
1381
+ "storeDerivedAsset: source asset must be persisted (missing id)"
1382
+ );
1383
+ }
1384
+ const {
1385
+ role: rawRole,
1386
+ linkAssociation = true,
1387
+ derivativeMetaType = "Asset",
1388
+ ...storeOpts
1389
+ } = opts;
1390
+ const role = rawRole ?? ASSET_ROLES.DERIVATION_SOURCE;
1391
+ const derived = await this.store.store(name, data, {
1392
+ ...storeOpts,
1393
+ sourceAssetId: source.id
1394
+ });
1395
+ if (linkAssociation && derived.id) {
1396
+ await this.associations.attach(
1397
+ derivativeMetaType,
1398
+ derived.id,
1399
+ source.id,
1400
+ { role }
1401
+ );
1402
+ }
1403
+ return derived;
1404
+ }
1405
+ /**
1406
+ * Record a provenance association between an existing source asset
1407
+ * and an existing derivative asset without touching bytes.
1408
+ */
1409
+ async linkDerivation(source, derivative, opts = {}) {
1410
+ if (!source.id || !derivative.id) {
1411
+ throw new Error(
1412
+ "linkDerivation: both source and derivative must be persisted (missing id)"
1413
+ );
1414
+ }
1415
+ const role = opts.role ?? ASSET_ROLES.DERIVATION_SOURCE;
1416
+ const derivativeMetaType = opts.derivativeMetaType ?? "Asset";
1417
+ return this.associations.attach(
1418
+ derivativeMetaType,
1419
+ derivative.id,
1420
+ source.id,
1421
+ { role }
1422
+ );
1423
+ }
1424
+ /**
1425
+ * Update the standard extraction-status metadata on an asset's
1426
+ * `description` JSON sidecar. This is a thin convenience over the
1427
+ * convention in `asset-conventions.ts` — callers that store
1428
+ * metadata elsewhere can ignore it.
1429
+ *
1430
+ * **How existing descriptions are handled**:
1431
+ * - Empty / unset → fresh JSON object.
1432
+ * - Valid JSON object → merged into; existing keys preserved.
1433
+ * - Free-form prose or non-object JSON → preserved under the
1434
+ * reserved `text` key of the resulting object (e.g.
1435
+ * `{ text: "original prose", extractionStatus: "..." }`). No prose
1436
+ * is discarded.
1437
+ *
1438
+ * Callers that already use `text` for something else, or that need
1439
+ * an entirely separate metadata surface, should either round-trip
1440
+ * the JSON themselves or skip this helper — its only job is the
1441
+ * `extractionStatus` / `extractionError` / `extractedAt` triple.
1442
+ *
1443
+ * Error handling: when `status` transitions away from `failed`
1444
+ * without a new `extra.error`, the stale `extractionError` is
1445
+ * cleared so downstream consumers don't misread the current state.
1446
+ * When `status === 'succeeded'`, `extractedAt` is stamped to now
1447
+ * unless the caller provides one.
1448
+ */
1449
+ async setExtractionStatus(asset, status, extra = {}) {
1450
+ const existing = parseDescriptionSidecar(asset.description);
1451
+ const next = {
1452
+ ...existing,
1453
+ extractionStatus: status
1454
+ };
1455
+ if (extra.error !== void 0) {
1456
+ next.extractionError = extra.error;
1457
+ } else if (status !== "failed") {
1458
+ delete next.extractionError;
1459
+ }
1460
+ if (status === "succeeded") {
1461
+ next.extractedAt = (extra.extractedAt ?? /* @__PURE__ */ new Date()).toISOString();
1462
+ }
1463
+ asset.description = JSON.stringify(next);
1464
+ await asset.save();
1465
+ }
1466
+ }
1467
+ async function createAssetRuntime(options) {
1468
+ const collection = options.collection ?? await AssetCollection.create({ db: options.db });
1469
+ const associations = options.associations ?? await AssetAssociationCollection.create({ db: options.db });
1470
+ const store = await new AssetStore(
1471
+ options.storage,
1472
+ collection,
1473
+ options.storeOptions
1474
+ ).initialize();
1475
+ return new AssetRuntime(
1476
+ collection,
1477
+ associations,
1478
+ store,
1479
+ options.capabilityProviders
1480
+ );
1481
+ }
1482
+ function parseDescriptionSidecar(description) {
1483
+ if (!description) return {};
1484
+ try {
1485
+ const parsed = JSON.parse(description);
1486
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1487
+ return parsed;
1488
+ }
1489
+ return { text: description };
1490
+ } catch {
1491
+ return { text: description };
1492
+ }
1493
+ }
1494
+ const DEFAULT_REMOTE_MAX_BYTES = 50 * 1024 * 1024;
1495
+ const DEFAULT_REMOTE_TIMEOUT_MS = 1e4;
1496
+ const MAX_REMOTE_REDIRECTS = 5;
1497
+ const INLINE_SAFE_MIME_TYPES = /* @__PURE__ */ new Set([
1498
+ "application/json",
1499
+ "application/pdf",
1500
+ "audio/aac",
1501
+ "audio/mpeg",
1502
+ "audio/ogg",
1503
+ "audio/wav",
1504
+ "audio/webm",
1505
+ "image/avif",
1506
+ "image/gif",
1507
+ "image/jpeg",
1508
+ "image/png",
1509
+ "image/webp",
1510
+ "text/csv",
1511
+ "text/plain",
1512
+ "video/mp4",
1513
+ "video/ogg",
1514
+ "video/webm"
1515
+ ]);
1516
+ function normalizeMimeType(raw) {
1517
+ return (raw.split(";")[0] ?? "").trim().toLowerCase();
1518
+ }
1519
+ function isInlineSafeMimeType(contentType) {
1520
+ return INLINE_SAFE_MIME_TYPES.has(normalizeMimeType(contentType));
1521
+ }
1522
+ function isRemoteUri(uri) {
1523
+ return /^https?:\/\//i.test(uri);
1524
+ }
1525
+ function isBlockedHost(hostname) {
1526
+ let host = hostname.trim().toLowerCase();
1527
+ if (host.startsWith("[") && host.endsWith("]")) {
1528
+ host = host.slice(1, -1);
1529
+ }
1530
+ host = host.replace(/\.+$/, "");
1531
+ if (host === "" || host === "localhost" || host.endsWith(".localhost")) {
1532
+ return true;
1533
+ }
1534
+ if (host === "169.254.169.254" || host === "metadata.google.internal" || host === "metadata" || host === "100.100.100.200") {
1535
+ return true;
1536
+ }
1537
+ const ipv4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
1538
+ if (ipv4) {
1539
+ const octets = ipv4.slice(1).map((o) => Number(o));
1540
+ if (octets.some((o) => o > 255)) return true;
1541
+ return isBlockedIpv4(octets);
1542
+ }
1543
+ if (host.includes(":")) {
1544
+ return isBlockedIpv6(host);
1545
+ }
1546
+ return false;
1547
+ }
1548
+ function isBlockedIpv4(octets) {
1549
+ const [a, b] = octets;
1550
+ if (a === 0) return true;
1551
+ if (a === 10) return true;
1552
+ if (a === 127) return true;
1553
+ if (a === 169 && b === 254) return true;
1554
+ if (a === 172 && b >= 16 && b <= 31) return true;
1555
+ if (a === 192 && b === 168) return true;
1556
+ if (a === 100 && b >= 64 && b <= 127) return true;
1557
+ if (a >= 224) return true;
1558
+ return false;
1559
+ }
1560
+ function expandIpv6(host) {
1561
+ let h = host.toLowerCase();
1562
+ const pct = h.indexOf("%");
1563
+ if (pct !== -1) h = h.slice(0, pct);
1564
+ let tailHextets = [];
1565
+ const lastColon = h.lastIndexOf(":");
1566
+ const tail = lastColon === -1 ? "" : h.slice(lastColon + 1);
1567
+ if (tail.includes(".")) {
1568
+ const m = tail.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
1569
+ if (!m) return null;
1570
+ const octets = m.slice(1).map((o) => Number(o));
1571
+ if (octets.some((o) => o > 255)) return null;
1572
+ tailHextets = [octets[0] << 8 | octets[1], octets[2] << 8 | octets[3]];
1573
+ h = h.slice(0, lastColon);
1574
+ if (h === ":") h = "::";
1575
+ }
1576
+ const parseHextets = (parts) => {
1577
+ const out = [];
1578
+ for (const p of parts) {
1579
+ if (p === "" || !/^[0-9a-f]{1,4}$/.test(p)) return null;
1580
+ out.push(Number.parseInt(p, 16));
1581
+ }
1582
+ return out;
1583
+ };
1584
+ const doubleColon = h.indexOf("::");
1585
+ let head;
1586
+ let midTail;
1587
+ if (doubleColon !== -1) {
1588
+ if (h.indexOf("::", doubleColon + 1) !== -1) return null;
1589
+ const before = h.slice(0, doubleColon);
1590
+ const after = h.slice(doubleColon + 2);
1591
+ head = parseHextets(before === "" ? [] : before.split(":"));
1592
+ midTail = parseHextets(after === "" ? [] : after.split(":"));
1593
+ } else {
1594
+ head = parseHextets(h === "" ? [] : h.split(":"));
1595
+ midTail = [];
1596
+ }
1597
+ if (head === null || midTail === null) return null;
1598
+ const explicit = [...head, ...midTail, ...tailHextets];
1599
+ if (doubleColon !== -1) {
1600
+ if (explicit.length > 7) return null;
1601
+ const fill = 8 - explicit.length;
1602
+ return [...head, ...new Array(fill).fill(0), ...midTail, ...tailHextets];
1603
+ }
1604
+ if (explicit.length !== 8) return null;
1605
+ return explicit;
1606
+ }
1607
+ function isBlockedIpv6(host) {
1608
+ const hextets = expandIpv6(host);
1609
+ if (hextets === null) return true;
1610
+ const [h0, h1, h2, h3, h4, h5, h6, h7] = hextets;
1611
+ const embeddedV4 = () => [
1612
+ h6 >> 8 & 255,
1613
+ h6 & 255,
1614
+ h7 >> 8 & 255,
1615
+ h7 & 255
1616
+ ];
1617
+ if (h0 === 0 && h1 === 0 && h2 === 0 && h3 === 0 && h4 === 0 && h5 === 0) {
1618
+ if (h6 === 0 && h7 === 0) return true;
1619
+ if (h6 === 0 && h7 === 1) return true;
1620
+ return isBlockedIpv4(embeddedV4());
1621
+ }
1622
+ if (h0 === 0 && h1 === 0 && h2 === 0 && h3 === 0 && h4 === 0 && h5 === 65535) {
1623
+ return isBlockedIpv4(embeddedV4());
1624
+ }
1625
+ if ((h0 & 65472) === 65152) return true;
1626
+ if ((h0 & 65472) === 65216) return true;
1627
+ if ((h0 & 65024) === 64512) return true;
1628
+ if ((h0 & 65280) === 65280) return true;
1629
+ return false;
1630
+ }
1631
+ function assertSafeRemoteUrl(rawUrl) {
1632
+ let url;
1633
+ try {
1634
+ url = new URL(rawUrl);
1635
+ } catch {
1636
+ throw new AssetServeError("Invalid remote asset URL", 502);
1637
+ }
1638
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
1639
+ throw new AssetServeError("Unsupported remote asset scheme", 502);
1640
+ }
1641
+ if (isBlockedHost(url.hostname)) {
1642
+ throw new AssetServeError("Remote asset host not allowed", 502);
1643
+ }
1644
+ return url;
1645
+ }
1646
+ async function fetchRemoteAsset(startUrl, fetchImpl, maxBytes, timeoutMs) {
1647
+ let current = assertSafeRemoteUrl(startUrl);
1648
+ for (let hop = 0; hop <= MAX_REMOTE_REDIRECTS; hop++) {
1649
+ const controller = new AbortController();
1650
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1651
+ let response;
1652
+ try {
1653
+ response = await fetchImpl(current.toString(), {
1654
+ redirect: "manual",
1655
+ signal: controller.signal
1656
+ });
1657
+ } finally {
1658
+ clearTimeout(timer);
1659
+ }
1660
+ if (response.status >= 300 && response.status < 400) {
1661
+ const location = response.headers.get("location");
1662
+ if (!location) {
1663
+ throw new AssetServeError(
1664
+ "Remote asset redirect missing Location",
1665
+ 502
1666
+ );
1667
+ }
1668
+ current = assertSafeRemoteUrl(new URL(location, current).toString());
1669
+ continue;
1670
+ }
1671
+ if (!response.ok) {
1672
+ throw new AssetServeError(
1673
+ `Remote asset upstream returned ${response.status}`,
1674
+ 502
1675
+ );
1676
+ }
1677
+ const data = await readBodyWithCap(response, maxBytes);
1678
+ return {
1679
+ data,
1680
+ contentType: response.headers.get("content-type") ?? void 0
1681
+ };
1682
+ }
1683
+ throw new AssetServeError("Too many remote asset redirects", 502);
1684
+ }
1685
+ async function readBodyWithCap(response, maxBytes) {
1686
+ const declared = response.headers.get("content-length");
1687
+ if (declared && Number(declared) > maxBytes) {
1688
+ throw new AssetServeError("Remote asset exceeds size limit", 502);
1689
+ }
1690
+ const body = response.body;
1691
+ if (body && typeof body.getReader === "function") {
1692
+ const reader = body.getReader();
1693
+ const chunks = [];
1694
+ let total = 0;
1695
+ while (true) {
1696
+ const { done, value } = await reader.read();
1697
+ if (done) break;
1698
+ if (value) {
1699
+ total += value.byteLength;
1700
+ if (total > maxBytes) {
1701
+ await reader.cancel().catch(() => {
1702
+ });
1703
+ throw new AssetServeError("Remote asset exceeds size limit", 502);
1704
+ }
1705
+ chunks.push(value);
1706
+ }
1707
+ }
1708
+ return Buffer.concat(chunks.map((c) => Buffer.from(c)));
1709
+ }
1710
+ const buf = Buffer.from(await response.arrayBuffer());
1711
+ if (buf.byteLength > maxBytes) {
1712
+ throw new AssetServeError("Remote asset exceeds size limit", 502);
1713
+ }
1714
+ return buf;
1715
+ }
1716
+ async function resolveAssetForServing(options) {
1717
+ const {
1718
+ runtime,
1719
+ asset: assetOrId,
1720
+ tenantId: tenantId2,
1721
+ canAccess,
1722
+ disposition: _disposition
1723
+ } = options;
1724
+ const asset = typeof assetOrId === "string" ? await runtime.collection.get({ id: assetOrId }) : assetOrId;
1725
+ if (!asset) {
1726
+ throw new AssetServeError("Asset not found", 404);
1727
+ }
1728
+ if (tenantId2 !== void 0 && asset.tenantId !== null && asset.tenantId !== tenantId2) {
1729
+ throw new AssetServeError("Asset not visible to this tenant", 403);
1730
+ }
1731
+ if (canAccess) {
1732
+ const allowed = await canAccess(asset);
1733
+ if (!allowed) {
1734
+ throw new AssetServeError("Asset access denied", 403);
1735
+ }
1736
+ }
1737
+ const remoteMode = options.remoteMode ?? "error";
1738
+ let data;
1739
+ let remoteContentType;
1740
+ if (isRemoteUri(asset.sourceUri)) {
1741
+ if (remoteMode === "error") {
1742
+ throw new AssetServeError("Remote asset not supported", 500);
1743
+ }
1744
+ if (remoteMode === "redirect") {
1745
+ throw new RedirectToSourceUri(asset.sourceUri);
1746
+ }
1747
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
1748
+ if (!fetchImpl) {
1749
+ throw new AssetServeError(
1750
+ "No fetch implementation available for remote asset",
1751
+ 500
1752
+ );
1753
+ }
1754
+ const maxBytes = options.remoteMaxBytes ?? DEFAULT_REMOTE_MAX_BYTES;
1755
+ const timeoutMs = options.remoteTimeoutMs ?? DEFAULT_REMOTE_TIMEOUT_MS;
1756
+ try {
1757
+ const result = await fetchRemoteAsset(
1758
+ asset.sourceUri,
1759
+ fetchImpl,
1760
+ maxBytes,
1761
+ timeoutMs
1762
+ );
1763
+ data = result.data;
1764
+ remoteContentType = result.contentType;
1765
+ } catch (err) {
1766
+ if (err instanceof AssetServeError) throw err;
1767
+ console.error("serveAsset: remote fetch failed", err);
1768
+ throw new AssetServeError("Failed to fetch remote asset", 502);
1769
+ }
1770
+ } else {
1771
+ try {
1772
+ data = await runtime.store.read(asset);
1773
+ } catch (err) {
1774
+ console.error("serveAsset: store read failed", err);
1775
+ throw new AssetServeError(
1776
+ "Failed to read asset bytes",
1777
+ 500,
1778
+ "Failed to read asset bytes"
1779
+ );
1780
+ }
1781
+ }
1782
+ const contentType = asset.mimeType || remoteContentType || "application/octet-stream";
1783
+ const filename = options.filename ?? deriveFilename(asset);
1784
+ return {
1785
+ asset,
1786
+ data,
1787
+ contentType,
1788
+ filename,
1789
+ size: data.byteLength
1790
+ };
1791
+ }
1792
+ class RedirectToSourceUri extends Error {
1793
+ constructor(location) {
1794
+ super("Asset stored at remote URL");
1795
+ this.location = location;
1796
+ this.name = "RedirectToSourceUri";
1797
+ }
1798
+ location;
1799
+ }
1800
+ async function serveAsset(options) {
1801
+ const Ctor = options.responseCtor ?? globalThis.Response;
1802
+ if (!Ctor) {
1803
+ throw new Error(
1804
+ "serveAsset: no Response constructor available. Pass options.responseCtor or run on Node 18+."
1805
+ );
1806
+ }
1807
+ try {
1808
+ const { data, contentType, filename, size } = await resolveAssetForServing(options);
1809
+ const requestedDisposition = options.disposition ?? "inline";
1810
+ const disposition = requestedDisposition === "attachment" || isInlineSafeMimeType(contentType) ? requestedDisposition : "attachment";
1811
+ const PROTECTED_HEADERS = /* @__PURE__ */ new Set([
1812
+ "content-type",
1813
+ "content-length",
1814
+ "content-disposition",
1815
+ "x-content-type-options"
1816
+ ]);
1817
+ const headers = {};
1818
+ for (const [name, value] of Object.entries(options.headers ?? {})) {
1819
+ if (!PROTECTED_HEADERS.has(name.toLowerCase())) {
1820
+ headers[name] = value;
1821
+ }
1822
+ }
1823
+ headers["Content-Type"] = contentType;
1824
+ headers["Content-Length"] = String(size);
1825
+ headers["Content-Disposition"] = buildContentDisposition(
1826
+ disposition,
1827
+ filename
1828
+ );
1829
+ headers["X-Content-Type-Options"] = "nosniff";
1830
+ return new Ctor(
1831
+ data,
1832
+ {
1833
+ status: 200,
1834
+ headers
1835
+ }
1836
+ );
1837
+ } catch (err) {
1838
+ if (err instanceof RedirectToSourceUri) {
1839
+ return new Ctor(null, {
1840
+ status: 302,
1841
+ headers: { Location: err.location }
1842
+ });
1843
+ }
1844
+ if (err instanceof AssetServeError) {
1845
+ if (err.status >= 500) {
1846
+ console.error("serveAsset: request failed", err.status, err.message);
1847
+ }
1848
+ return new Ctor(err.clientMessage, {
1849
+ status: err.status,
1850
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
1851
+ });
1852
+ }
1853
+ console.error("serveAsset: unexpected error", err);
1854
+ return new Ctor("Internal error serving asset", {
1855
+ status: 500,
1856
+ headers: { "Content-Type": "text/plain; charset=utf-8" }
1857
+ });
1858
+ }
1859
+ }
1860
+ function defaultClientMessage(status) {
1861
+ switch (status) {
1862
+ case 403:
1863
+ return "Forbidden";
1864
+ case 404:
1865
+ return "Not found";
1866
+ case 502:
1867
+ return "Bad gateway";
1868
+ default:
1869
+ return "Internal error serving asset";
1870
+ }
1871
+ }
1872
+ class AssetServeError extends Error {
1873
+ constructor(message, status, clientMessage) {
1874
+ super(message);
1875
+ this.status = status;
1876
+ this.name = "AssetServeError";
1877
+ this.clientMessage = clientMessage ?? defaultClientMessage(status);
1878
+ }
1879
+ status;
1880
+ /** Opaque body safe to return to the HTTP client. */
1881
+ clientMessage;
1882
+ }
1883
+ function deriveFilename(asset) {
1884
+ if (asset.name) return asset.name;
1885
+ const fromUri = extractBasename(asset.sourceUri);
1886
+ if (fromUri) return fromUri;
1887
+ return asset.id ?? "asset";
1888
+ }
1889
+ function extractBasename(uri) {
1890
+ if (!uri) return "";
1891
+ const noScheme = uri.replace(/^[a-z][a-z0-9+\-.]*:\/\//i, "");
1892
+ const last = noScheme.split("/").pop() ?? "";
1893
+ return last;
1894
+ }
1895
+ function sanitizeDispositionFilename(filename) {
1896
+ let stripped = "";
1897
+ for (const ch of filename) {
1898
+ const code = ch.charCodeAt(0);
1899
+ if (code > 31 && code !== 127) {
1900
+ stripped += ch;
1901
+ }
1902
+ }
1903
+ const safe = stripped.replace(/["\\/]/g, "_").trim();
1904
+ return safe || "asset";
1905
+ }
1906
+ function buildContentDisposition(disposition, filename) {
1907
+ const sanitized = sanitizeDispositionFilename(filename);
1908
+ const encoded = encodeURIComponent(sanitized);
1909
+ return `${disposition}; filename="${sanitized}"; filename*=UTF-8''${encoded}`;
1910
+ }
1911
+ class AssetStatusCollection extends SmrtCollection {
1912
+ static _itemClass = AssetStatus;
1913
+ /**
1914
+ * Get or create an asset status by slug
1915
+ *
1916
+ * @param slug - The asset status slug
1917
+ * @param name - The display name (defaults to slug)
1918
+ * @param description - Optional description
1919
+ * @returns The existing or newly created AssetStatus
1920
+ */
1921
+ async getOrCreate(slug, name, description) {
1922
+ const existing = await this.list({ where: { slug }, limit: 1 });
1923
+ if (existing.length > 0) {
1924
+ return existing[0];
1925
+ }
1926
+ return await this.create({
1927
+ slug,
1928
+ name: name || slug,
1929
+ description
1930
+ });
1931
+ }
1932
+ /**
1933
+ * Initialize common asset statuses
1934
+ *
1935
+ * Creates standard asset statuses if they don't exist:
1936
+ * - draft
1937
+ * - published
1938
+ * - archived
1939
+ * - deleted
1940
+ */
1941
+ async initializeCommonStatuses() {
1942
+ await this.getOrCreate("draft", "Draft", "Work in progress, not yet ready");
1943
+ await this.getOrCreate("published", "Published", "Live and available");
1944
+ await this.getOrCreate(
1945
+ "archived",
1946
+ "Archived",
1947
+ "No longer active but preserved"
1948
+ );
1949
+ await this.getOrCreate("deleted", "Deleted", "Marked for deletion");
1950
+ }
1951
+ }
1952
+ class AssetTypeCollection extends SmrtCollection {
1953
+ static _itemClass = AssetType;
1954
+ /**
1955
+ * Get or create an asset type by slug
1956
+ *
1957
+ * @param slug - The asset type slug
1958
+ * @param name - The display name (defaults to slug)
1959
+ * @param description - Optional description
1960
+ * @returns The existing or newly created AssetType
1961
+ */
1962
+ async getOrCreate(slug, name, description) {
1963
+ const existing = await this.list({ where: { slug }, limit: 1 });
1964
+ if (existing.length > 0) {
1965
+ return existing[0];
1966
+ }
1967
+ return await this.create({
1968
+ slug,
1969
+ name: name || slug,
1970
+ description
1971
+ });
1972
+ }
1973
+ /**
1974
+ * Initialize common asset types
1975
+ *
1976
+ * Creates standard asset types if they don't exist:
1977
+ * - image
1978
+ * - video
1979
+ * - document
1980
+ * - audio
1981
+ *
1982
+ * Note: `folder` was removed in R3-D. Folders are no longer an asset
1983
+ * subtype — they live on their own `folders` table. See `folder.ts`.
1984
+ */
1985
+ async initializeCommonTypes() {
1986
+ await this.getOrCreate(
1987
+ "image",
1988
+ "Image",
1989
+ "Image files (JPEG, PNG, GIF, etc.)"
1990
+ );
1991
+ await this.getOrCreate(
1992
+ "video",
1993
+ "Video",
1994
+ "Video files (MP4, AVI, MOV, etc.)"
1995
+ );
1996
+ await this.getOrCreate(
1997
+ "document",
1998
+ "Document",
1999
+ "Document files (PDF, DOCX, TXT, etc.)"
2000
+ );
2001
+ await this.getOrCreate(
2002
+ "audio",
2003
+ "Audio",
2004
+ "Audio files (MP3, WAV, AAC, etc.)"
2005
+ );
2006
+ }
2007
+ }
2008
+ var __defProp = Object.defineProperty;
2009
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
2010
+ var __decorateClass = (decorators, target, key, kind) => {
2011
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
2012
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
2013
+ if (decorator = decorators[i])
2014
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
2015
+ if (kind && result) __defProp(target, key, result);
2016
+ return result;
2017
+ };
2018
+ let Folder = class extends SmrtHierarchical {
2019
+ tenantId = null;
2020
+ // Core fields. `parentId` is inherited from SmrtHierarchical.
2021
+ name = "";
2022
+ // `slug` is inherited as an accessor from SmrtObject.
2023
+ description = "";
2024
+ ownerProfileId = null;
2025
+ // Timestamps
2026
+ createdAt = /* @__PURE__ */ new Date();
2027
+ updatedAt = /* @__PURE__ */ new Date();
2028
+ constructor(options = {}) {
2029
+ super(options);
2030
+ if (options.name !== void 0) this.name = options.name;
2031
+ if (options.slug !== void 0) this.slug = options.slug;
2032
+ if (options.description !== void 0)
2033
+ this.description = options.description;
2034
+ if (options.parentId !== void 0)
2035
+ this.parentId = options.parentId ?? null;
2036
+ if (options.ownerProfileId !== void 0)
2037
+ this.ownerProfileId = options.ownerProfileId;
2038
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
2039
+ if (options.createdAt) this.createdAt = options.createdAt;
2040
+ if (options.updatedAt) this.updatedAt = options.updatedAt;
2041
+ }
2042
+ // Hierarchy traversal (getParent / getChildren / getAncestors /
2043
+ // getDescendants / getHierarchy / moveTo) provided by SmrtHierarchical.
2044
+ /**
2045
+ * Look up a folder by slug.
2046
+ */
2047
+ static async getBySlug(_slug) {
2048
+ return null;
2049
+ }
2050
+ };
2051
+ __decorateClass([
2052
+ tenantId({ nullable: true })
2053
+ ], Folder.prototype, "tenantId", 2);
2054
+ __decorateClass([
2055
+ crossPackageRef("@happyvertical/smrt-profiles:Profile")
2056
+ ], Folder.prototype, "ownerProfileId", 2);
2057
+ Folder = __decorateClass([
2058
+ TenantScoped({ mode: "optional" }),
2059
+ smrt({
2060
+ api: { include: ["list", "get", "create", "update", "delete"] },
2061
+ mcp: { include: ["list", "get", "create", "update"] },
2062
+ cli: true
2063
+ })
2064
+ ], Folder);
2065
+ class FolderCollection extends SmrtCollection {
2066
+ static _itemClass = Folder;
2067
+ /**
2068
+ * Get the folder tree starting from an optional root.
2069
+ *
2070
+ * When `rootId` is omitted, returns top-level folders (those with no
2071
+ * parent). When `rootId` is provided, returns all descendants of that
2072
+ * folder via the inherited SmrtHierarchical BFS traversal — same
2073
+ * cycle-safe, one-query-per-depth behaviour as Place/Event.
2074
+ *
2075
+ * @param rootId - Optional root folder ID; if omitted, returns top-level folders
2076
+ * @returns Array of folders (flat list; use parentId to reconstruct tree)
2077
+ */
2078
+ async getTree(rootId) {
2079
+ if (!rootId) {
2080
+ return await this.list({
2081
+ where: { parentId: null },
2082
+ orderBy: "name ASC"
2083
+ });
2084
+ }
2085
+ const result = [];
2086
+ const queue = [rootId];
2087
+ const visited = /* @__PURE__ */ new Set([rootId]);
2088
+ while (queue.length > 0) {
2089
+ const currentId = queue.shift();
2090
+ const children = await this.list({
2091
+ where: { parentId: currentId },
2092
+ orderBy: "name ASC"
2093
+ });
2094
+ for (const child of children) {
2095
+ if (child.id && !visited.has(child.id)) {
2096
+ visited.add(child.id);
2097
+ result.push(child);
2098
+ queue.push(child.id);
2099
+ }
2100
+ }
2101
+ }
2102
+ return result;
2103
+ }
2104
+ /**
2105
+ * Get the path from root to a given folder (ancestors + self).
2106
+ *
2107
+ * @param folderId - The folder ID to get the path for
2108
+ * @returns Array of folders from root to the given folder (inclusive)
2109
+ */
2110
+ async getPath(folderId) {
2111
+ const folder = await this.get({ id: folderId });
2112
+ if (!folder) return [];
2113
+ const ancestors = await folder.getAncestors();
2114
+ return [...ancestors, folder];
2115
+ }
2116
+ /**
2117
+ * Get all assets that are direct children of a folder.
2118
+ *
2119
+ * Folder membership is recorded on `Asset.folderId`, not on the folder
2120
+ * itself, so this is delegated to the asset collection.
2121
+ *
2122
+ * @param folderId - The folder ID
2123
+ * @param assetCollection - An AssetCollection instance for querying
2124
+ * @returns Array of assets in this folder
2125
+ */
2126
+ async getContents(folderId, assetCollection) {
2127
+ return await assetCollection.list({
2128
+ where: { folderId }
2129
+ });
2130
+ }
2131
+ /**
2132
+ * Move an asset into a folder (or out, by passing `null`).
2133
+ *
2134
+ * @param asset - The asset to move
2135
+ * @param folderId - The target folder ID (or null to move to root)
2136
+ */
2137
+ async moveAsset(asset, folderId) {
2138
+ asset.folderId = folderId;
2139
+ await asset.save();
2140
+ }
2141
+ }
2142
+ async function persistMediaBundleInspection(adapter, inspection, options = {}) {
2143
+ const warnings = [...inspection.warnings];
2144
+ const primary = await adapter.upsertAsset({
2145
+ file: inspection.primary,
2146
+ role: "primary",
2147
+ typeSlug: options.primaryTypeSlug,
2148
+ metadata: inspection.metadata,
2149
+ inspection
2150
+ });
2151
+ const supportAssetIds = [];
2152
+ for (const support of inspection.supportFiles) {
2153
+ const visibility = support.visibility ?? options.supportVisibility ?? "hidden-retained";
2154
+ if (visibility === "drop-after-extract") continue;
2155
+ const supportAsset = await adapter.upsertAsset({
2156
+ file: support.file,
2157
+ role: "support",
2158
+ typeSlug: options.supportTypeSlug,
2159
+ parentAssetId: primary.id,
2160
+ relationship: support.relationship,
2161
+ visibility,
2162
+ metadata: support.metadata ?? {},
2163
+ inspection
2164
+ });
2165
+ supportAssetIds.push(supportAsset.id);
2166
+ await adapter.associateSupportFile?.({
2167
+ primaryAssetId: primary.id,
2168
+ supportAssetId: supportAsset.id,
2169
+ relationship: support.relationship,
2170
+ visibility,
2171
+ metadata: support.metadata
2172
+ });
2173
+ }
2174
+ let metadataArtifactId;
2175
+ if (options.writeMetadataArtifact !== false && adapter.writeMetadataArtifact) {
2176
+ const artifact = await adapter.writeMetadataArtifact({
2177
+ primaryAssetId: primary.id,
2178
+ inspection
2179
+ });
2180
+ metadataArtifactId = artifact?.id;
2181
+ }
2182
+ let gpsTrackPointCount = 0;
2183
+ const gpsTrack = inspection.metadata.gpsTrack ?? [];
2184
+ if (options.writeGpsTrack !== false && gpsTrack.length > 0) {
2185
+ if (adapter.replaceGpsTrack) {
2186
+ gpsTrackPointCount = await adapter.replaceGpsTrack(primary.id, gpsTrack);
2187
+ } else {
2188
+ warnings.push("adapter cannot persist gps track");
2189
+ }
2190
+ }
2191
+ return {
2192
+ primaryAssetId: primary.id,
2193
+ supportAssetIds,
2194
+ metadataArtifactId,
2195
+ gpsTrackPointCount,
2196
+ warnings
2197
+ };
2198
+ }
2199
+ const OWNED_ASSET_RELATIONSHIP_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
2200
+ function assertValidOwnedAssetRelationship(relationship) {
2201
+ if (!OWNED_ASSET_RELATIONSHIP_PATTERN.test(relationship)) {
2202
+ throw new Error(
2203
+ `Invalid relationship type "${relationship}"; must start with a letter or underscore and contain only letters, digits, and underscores`
2204
+ );
2205
+ }
2206
+ }
2207
+ function assertValidOwnedAssetSortOrder(sortOrder) {
2208
+ if (!Number.isInteger(sortOrder) || sortOrder < 0 || sortOrder > 2147483647) {
2209
+ throw new Error(
2210
+ `Invalid sortOrder "${sortOrder}"; must be a non-negative integer`
2211
+ );
2212
+ }
2213
+ }
2214
+ async function resolveOwnedAssetsById(db, assetIds, tenantId2) {
2215
+ if (assetIds.length === 0) {
2216
+ return [];
2217
+ }
2218
+ const assets = await AssetCollection.create({ db });
2219
+ const resolved = tenantId2 ? await withSystemContext(async () => assets.listByIds(assetIds)) : await assets.listByIds(assetIds);
2220
+ const visibleAssets = tenantId2 ? resolved.filter(
2221
+ (asset) => asset.tenantId === tenantId2 || asset.tenantId === null
2222
+ ) : resolved;
2223
+ const assetsById = new Map(
2224
+ visibleAssets.filter((asset) => asset.id).map((asset) => [asset.id, asset])
2225
+ );
2226
+ return assetIds.map((assetId) => assetsById.get(assetId)).filter(Boolean);
2227
+ }
2228
+ async function getOwnerRecord(collection, ownerId) {
2229
+ return collection.get({ id: ownerId });
2230
+ }
2231
+ async function getOwnedAssetsFromCollection(collection, ownerId, relationship) {
2232
+ const owner = await getOwnerRecord(collection, ownerId);
2233
+ if (!owner) {
2234
+ return [];
2235
+ }
2236
+ return owner.getAssets(relationship);
2237
+ }
2238
+ async function addOwnedAssetFromCollection(collection, ownerType, ownerId, asset, relationship = "attachment", sortOrder = 0) {
2239
+ const owner = await getOwnerRecord(collection, ownerId);
2240
+ if (!owner) {
2241
+ throw new Error(`${ownerType} '${ownerId}' not found`);
2242
+ }
2243
+ await owner.addAsset(asset, relationship, sortOrder);
2244
+ }
2245
+ async function removeOwnedAssetFromCollection(collection, ownerType, ownerId, assetId, relationship) {
2246
+ const owner = await getOwnerRecord(collection, ownerId);
2247
+ if (!owner) {
2248
+ throw new Error(`${ownerType} '${ownerId}' not found`);
2249
+ }
2250
+ await owner.removeAsset(assetId, relationship);
2251
+ }
2252
+ export {
2253
+ ASSET_EXTRACTION_STATUS,
2254
+ ASSET_METADATA_KEYS,
2255
+ ASSET_ROLES,
2256
+ Asset,
2257
+ AssetAssociation,
2258
+ AssetAssociationCollection,
2259
+ AssetCapabilitySkippedError,
2260
+ AssetCapabilityUnavailableError,
2261
+ AssetCollection,
2262
+ AssetMetafield,
2263
+ AssetMetafieldCollection,
2264
+ AssetRuntime,
2265
+ AssetServeError,
2266
+ AssetStatus,
2267
+ AssetStatusCollection,
2268
+ AssetStore,
2269
+ AssetType,
2270
+ AssetTypeCollection,
2271
+ Folder,
2272
+ FolderCollection,
2273
+ OWNED_ASSET_RELATIONSHIP_PATTERN,
2274
+ addOwnedAssetFromCollection,
2275
+ assertValidOwnedAssetRelationship,
2276
+ assertValidOwnedAssetSortOrder,
2277
+ createAssetRuntime,
2278
+ getOwnedAssetsFromCollection,
2279
+ persistMediaBundleInspection,
2280
+ removeOwnedAssetFromCollection,
2281
+ resolveAssetForServing,
2282
+ resolveOwnedAssetsById,
2283
+ serveAsset
2284
+ };
2285
+ //# sourceMappingURL=index.js.map