@happyvertical/smrt-assets 0.37.1 → 0.37.3

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