@endora-commerce/mod-assets-library 0.0.0-stage → 0.100.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -2
  3. package/dist/admin/api/assets-library-client.d.ts +29 -0
  4. package/dist/admin/api/assets-library-client.d.ts.map +1 -0
  5. package/dist/admin/api/assets-library-client.js +70 -0
  6. package/dist/admin/api/assets-library-client.js.map +1 -0
  7. package/dist/admin/components/AssetDetailDrawer.d.ts +8 -0
  8. package/dist/admin/components/AssetDetailDrawer.d.ts.map +1 -0
  9. package/dist/admin/components/AssetDetailDrawer.js +96 -0
  10. package/dist/admin/components/AssetDetailDrawer.js.map +1 -0
  11. package/dist/admin/components/FolderTree.d.ts +13 -0
  12. package/dist/admin/components/FolderTree.d.ts.map +1 -0
  13. package/dist/admin/components/FolderTree.js +71 -0
  14. package/dist/admin/components/FolderTree.js.map +1 -0
  15. package/dist/admin/index.d.ts +44 -0
  16. package/dist/admin/index.d.ts.map +1 -0
  17. package/dist/admin/index.js +46 -0
  18. package/dist/admin/index.js.map +1 -0
  19. package/dist/admin/pages/LibraryPage.d.ts +9 -0
  20. package/dist/admin/pages/LibraryPage.d.ts.map +1 -0
  21. package/dist/admin/pages/LibraryPage.js +163 -0
  22. package/dist/admin/pages/LibraryPage.js.map +1 -0
  23. package/dist/backend/entities/asset-folder.entity.d.ts +16 -0
  24. package/dist/backend/entities/asset-folder.entity.d.ts.map +1 -0
  25. package/dist/backend/entities/asset-folder.entity.js +57 -0
  26. package/dist/backend/entities/asset-folder.entity.js.map +1 -0
  27. package/dist/backend/entities/asset.entity.d.ts +38 -0
  28. package/dist/backend/entities/asset.entity.d.ts.map +1 -0
  29. package/dist/backend/entities/asset.entity.js +128 -0
  30. package/dist/backend/entities/asset.entity.js.map +1 -0
  31. package/dist/backend/index.d.ts +57 -0
  32. package/dist/backend/index.d.ts.map +1 -0
  33. package/dist/backend/index.js +150 -0
  34. package/dist/backend/index.js.map +1 -0
  35. package/dist/backend/jobs/hard-delete-asset.job.d.ts +18 -0
  36. package/dist/backend/jobs/hard-delete-asset.job.d.ts.map +1 -0
  37. package/dist/backend/jobs/hard-delete-asset.job.js +91 -0
  38. package/dist/backend/jobs/hard-delete-asset.job.js.map +1 -0
  39. package/dist/backend/plugin.d.ts +47 -0
  40. package/dist/backend/plugin.d.ts.map +1 -0
  41. package/dist/backend/plugin.js +96 -0
  42. package/dist/backend/plugin.js.map +1 -0
  43. package/dist/backend/routes.admin.d.ts +15 -0
  44. package/dist/backend/routes.admin.d.ts.map +1 -0
  45. package/dist/backend/routes.admin.js +189 -0
  46. package/dist/backend/routes.admin.js.map +1 -0
  47. package/dist/backend/routes.public.d.ts +14 -0
  48. package/dist/backend/routes.public.d.ts.map +1 -0
  49. package/dist/backend/routes.public.js +99 -0
  50. package/dist/backend/routes.public.js.map +1 -0
  51. package/dist/backend/services/asset-read-port.d.ts +103 -0
  52. package/dist/backend/services/asset-read-port.d.ts.map +1 -0
  53. package/dist/backend/services/asset-read-port.js +167 -0
  54. package/dist/backend/services/asset-read-port.js.map +1 -0
  55. package/dist/backend/services/assets-library.service.d.ts +76 -0
  56. package/dist/backend/services/assets-library.service.d.ts.map +1 -0
  57. package/dist/backend/services/assets-library.service.js +261 -0
  58. package/dist/backend/services/assets-library.service.js.map +1 -0
  59. package/dist/backend/services/cms-body-resolver.d.ts +6 -0
  60. package/dist/backend/services/cms-body-resolver.d.ts.map +1 -0
  61. package/dist/backend/services/cms-body-resolver.js +44 -0
  62. package/dist/backend/services/cms-body-resolver.js.map +1 -0
  63. package/dist/backend/services/folders.service.d.ts +29 -0
  64. package/dist/backend/services/folders.service.d.ts.map +1 -0
  65. package/dist/backend/services/folders.service.js +231 -0
  66. package/dist/backend/services/folders.service.js.map +1 -0
  67. package/dist/backend/services/hmac.d.ts +28 -0
  68. package/dist/backend/services/hmac.d.ts.map +1 -0
  69. package/dist/backend/services/hmac.js +50 -0
  70. package/dist/backend/services/hmac.js.map +1 -0
  71. package/dist/backend/services/reference-registry.d.ts +36 -0
  72. package/dist/backend/services/reference-registry.d.ts.map +1 -0
  73. package/dist/backend/services/reference-registry.js +63 -0
  74. package/dist/backend/services/reference-registry.js.map +1 -0
  75. package/dist/backend/services/storage/adapter-registry.d.ts +77 -0
  76. package/dist/backend/services/storage/adapter-registry.d.ts.map +1 -0
  77. package/dist/backend/services/storage/adapter-registry.js +95 -0
  78. package/dist/backend/services/storage/adapter-registry.js.map +1 -0
  79. package/dist/backend/services/storage/errors.d.ts +18 -0
  80. package/dist/backend/services/storage/errors.d.ts.map +1 -0
  81. package/dist/backend/services/storage/errors.js +30 -0
  82. package/dist/backend/services/storage/errors.js.map +1 -0
  83. package/dist/backend/services/storage/gcs-adapter.d.ts +42 -0
  84. package/dist/backend/services/storage/gcs-adapter.d.ts.map +1 -0
  85. package/dist/backend/services/storage/gcs-adapter.js +106 -0
  86. package/dist/backend/services/storage/gcs-adapter.js.map +1 -0
  87. package/dist/backend/services/storage/legacy-resolver.d.ts +15 -0
  88. package/dist/backend/services/storage/legacy-resolver.d.ts.map +1 -0
  89. package/dist/backend/services/storage/legacy-resolver.js +32 -0
  90. package/dist/backend/services/storage/legacy-resolver.js.map +1 -0
  91. package/dist/backend/services/storage/local-fs-adapter.d.ts +59 -0
  92. package/dist/backend/services/storage/local-fs-adapter.d.ts.map +1 -0
  93. package/dist/backend/services/storage/local-fs-adapter.js +153 -0
  94. package/dist/backend/services/storage/local-fs-adapter.js.map +1 -0
  95. package/dist/backend/services/storage/locator.d.ts +5 -0
  96. package/dist/backend/services/storage/locator.d.ts.map +1 -0
  97. package/dist/backend/services/storage/locator.js +16 -0
  98. package/dist/backend/services/storage/locator.js.map +1 -0
  99. package/dist/backend/services/storage/object-storage-port.d.ts +46 -0
  100. package/dist/backend/services/storage/object-storage-port.d.ts.map +1 -0
  101. package/dist/backend/services/storage/object-storage-port.js +78 -0
  102. package/dist/backend/services/storage/object-storage-port.js.map +1 -0
  103. package/dist/backend/services/storage/public-url-base.d.ts +50 -0
  104. package/dist/backend/services/storage/public-url-base.d.ts.map +1 -0
  105. package/dist/backend/services/storage/public-url-base.js +70 -0
  106. package/dist/backend/services/storage/public-url-base.js.map +1 -0
  107. package/dist/backend/services/storage/s3-adapter.d.ts +50 -0
  108. package/dist/backend/services/storage/s3-adapter.d.ts.map +1 -0
  109. package/dist/backend/services/storage/s3-adapter.js +110 -0
  110. package/dist/backend/services/storage/s3-adapter.js.map +1 -0
  111. package/dist/backend/services/storage/settings-view.d.ts +4 -0
  112. package/dist/backend/services/storage/settings-view.d.ts.map +1 -0
  113. package/dist/backend/services/storage/settings-view.js +73 -0
  114. package/dist/backend/services/storage/settings-view.js.map +1 -0
  115. package/dist/backend/services/storage/storage-adapter.d.ts +62 -0
  116. package/dist/backend/services/storage/storage-adapter.d.ts.map +1 -0
  117. package/dist/backend/services/storage/storage-adapter.js +10 -0
  118. package/dist/backend/services/storage/storage-adapter.js.map +1 -0
  119. package/dist/backend/services/upload-pipeline.d.ts +41 -0
  120. package/dist/backend/services/upload-pipeline.d.ts.map +1 -0
  121. package/dist/backend/services/upload-pipeline.js +231 -0
  122. package/dist/backend/services/upload-pipeline.js.map +1 -0
  123. package/dist/manifest.d.ts +185 -0
  124. package/dist/manifest.d.ts.map +1 -0
  125. package/dist/manifest.js +229 -0
  126. package/dist/manifest.js.map +1 -0
  127. package/dist/migrations/20260505T102206_assets_library_init.d.ts +35 -0
  128. package/dist/migrations/20260505T102206_assets_library_init.d.ts.map +1 -0
  129. package/dist/migrations/20260505T102206_assets_library_init.js +132 -0
  130. package/dist/migrations/20260505T102206_assets_library_init.js.map +1 -0
  131. package/dist/migrations/index.d.ts +27 -0
  132. package/dist/migrations/index.d.ts.map +1 -0
  133. package/dist/migrations/index.js +29 -0
  134. package/dist/migrations/index.js.map +1 -0
  135. package/docs/assets-library/index.md +125 -0
  136. package/i18n/en.json +54 -0
  137. package/i18n/pl.json +54 -0
  138. package/package.json +99 -3
  139. package/tailwind.css +14 -0
@@ -0,0 +1,150 @@
1
+ import { resolvePublicApiBaseUrl } from '@endora-commerce/platform/kernel';
2
+ import { assetsLibraryModule } from './plugin.js';
3
+ import { AssetReadService } from './services/asset-read-port.js';
4
+ import { ObjectStorageAdapter } from './services/storage/object-storage-port.js';
5
+ import { AssetFolder } from './entities/asset-folder.entity.js';
6
+ import { Asset } from './entities/asset.entity.js';
7
+ export function registerModule(ctx) {
8
+ ctx.di.register({
9
+ assetsLibrary: ctx
10
+ .asFunction(({ emFactory, auditLogService }) => assetsLibraryModule({
11
+ emFactory,
12
+ // D-223 — this module resolves the deployment's public API origin
13
+ // itself, the idiom `payu`, `tpay` and `inpost` already use for their
14
+ // callback origins, and every URL it produces is built on it when the
15
+ // storage adapter's own configured base is blank. It is read here,
16
+ // inside the factory, so the value is the one this composition boots
17
+ // with; the platform has already refused a production boot that has
18
+ // no origin at all (issue #218), so there is no new failure mode.
19
+ publicApiBaseUrl: resolvePublicApiBaseUrl(),
20
+ // Resolved per check, not captured. Awilix's strict mode refuses a
21
+ // singleton holding the transient `requireAdmin` port, and it is
22
+ // right to: a captured guard keeps admitting requests after `auth`
23
+ // goes away. This indirection is what the gate being non-optional
24
+ // buys — there is one guard, and it is always the live one.
25
+ requireAdmin: (permission) => async (req, reply) => ctx.cradle().requireAdmin(permission)(req, reply),
26
+ auditLog: auditLogService,
27
+ }))
28
+ .singleton(),
29
+ });
30
+ /**
31
+ * The contribution seam, deliberately **ungated** (feature 072, D-39).
32
+ *
33
+ * `catalog`, `cms`, `blog` and `megamenu` push their reference scanners in
34
+ * from `ctx.onBoot`, and boot hooks run regardless of effective state. As a
35
+ * `providePort` this was a transient gate, so switching `assets_library` off
36
+ * made all four hooks throw `MODULE_DISABLED` during composition and the
37
+ * backend stopped starting. A descriptor is inert; `assetsLibraryPort`, which
38
+ * reads and writes assets, is the port and does fail closed. (That sentence
39
+ * named `assetsLibraryService` until T118c, which is the container name the
40
+ * roots contributed rather than a port — it was never gated, and it is gone.)
41
+ *
42
+ * Whether an entry is honoured while its contributor is absent is answered at
43
+ * enumeration — see `services/reference-registry.ts`.
44
+ *
45
+ * `AssetReferenceRegistryPort` is published over this name all the same, which
46
+ * is not a reason to convert it back (issue #192): a published shape and a
47
+ * gated registration are different questions, and only the first is what F4
48
+ * packages.
49
+ */
50
+ ctx.di.register({
51
+ assetReferenceRegistry: ctx
52
+ .asFunction(({ assetsLibrary }) => assetsLibrary.handle.referenceRegistry)
53
+ .singleton(),
54
+ });
55
+ // ---------------------------------------------------------------------------
56
+ // Feature 075, Phase P — the published surface.
57
+ //
58
+ // `assetReadPort` replaces the five `em.findOne(Asset, …)` calls `catalog`
59
+ // and `orders` make to validate an id an admin supplied before attaching it.
60
+ //
61
+ // `assetsLibraryPort` is the narrow face of the library service that
62
+ // `pim_ergonode` uses during an import — upload, read, patch the alternate
63
+ // text, soft-delete what an item stopped pointing at. Since T118c it is also
64
+ // what `cms` resolves to turn an embedded asset id into the detail a
65
+ // storefront response carries.
66
+ //
67
+ // **Phase C has retired the root's entry** (`specs/110-instance-repository/`
68
+ // T118c). This note used to read *"the existing `assetsLibraryService` name is
69
+ // still contributed by a composition root; the root's entry stays until Phase
70
+ // C retires it, and the two resolve the same instance"*. Both roots
71
+ // contributed that name and, by the time it was drained, **no module resolved
72
+ // it**: `pim_ergonode` moved to this port at feature 075's cut and the second,
73
+ // ungated hold on the same service stayed behind. So the drain cost nothing
74
+ // and bought the one thing the duplicate could not have — there is now exactly
75
+ // one way in, and it is gated.
76
+ //
77
+ // What is still a composition's, and why it is not an omission. This note
78
+ // read *"five reaches … `resolveUrl` (three of them, two of those also
79
+ // folding in the host's public API base URL)"* against a tree holding
80
+ // **eight** reaches, **four** of them `resolveUrl` and **three** of those
81
+ // absolutizing. The count is not written down again (D-100):
82
+ // `grep -n 'assetsLibrary\.' backend/src/composition.ts` answers it. What is
83
+ // true of the reaches that remain is that each needs something this port does
84
+ // not publish — `resolveUrl`, or the storage adapters' byte-streaming surface
85
+ // (`invoices`' logo embed, `product_feeds`' artefact writer).
86
+ //
87
+ // **What no reach does any more is absolutize** (D-223). The three that did
88
+ // did it three ways — `absolutizePublicUrl` reading `BACKEND_PUBLIC_URL`
89
+ // first at two of them, `configuredPublicApiBaseUrl` plus a hand-written join
90
+ // reading `PUBLIC_API_BASE_URL` first at the third — while the fourth
91
+ // `resolveUrl` reach did not absolutize at all, so one asset had three
92
+ // answers depending on which consumer asked, and the two composition roots
93
+ // disagreed at two of the sites. This module resolves the origin itself now
94
+ // and every URL it returns is absolute.
95
+ //
96
+ // **`objectStoragePort` is the byte-store answer** (T118c), and it is a
97
+ // sibling of `assetsLibraryPort` rather than four more methods on it: what
98
+ // `product_feeds` borrows is *which bucket this deployment writes to, with
99
+ // which credentials*, and it creates no `Asset` row. `invoices`' logo embed
100
+ // asks the identical question through a composition root and drains onto the
101
+ // same port. The published shape's own doc block carries its retiring
102
+ // condition — this is really an object store, and it lives here because the
103
+ // configuration does.
104
+ //
105
+ // **And the URL answer is `assetReadPort.resolvePublicUrls`, in batch.** This
106
+ // note read *"a consumer that wants a URL for an asset id takes
107
+ // `AssetsLibraryPort.getAsset`, which already carries one"*, and that is the
108
+ // right answer for `cms` resolving a handful of embeds on one page. Measured
109
+ // for a consumer that asks about hundreds at a time, it is not: `getAsset`
110
+ // builds the full detail, and the detail carries the deletion-protection
111
+ // `references` list, which is one query per registered reference descriptor —
112
+ // roughly ten queries per asset against a feed hydration batch of 500
113
+ // products. The batched read is one query and a string build per row, and it
114
+ // is the *rule* that moves with it rather than only the loop: only this
115
+ // module can tell a stable URL from a signed one, so the filter belongs here
116
+ // and the absence is the return type (composition checklist item 7).
117
+ // ---------------------------------------------------------------------------
118
+ ctx.di.providePort('assetReadPort', ctx
119
+ .asFunction(({ emFactory, assetsLibrary }) =>
120
+ // The registry is read per call, never captured: it is rebuilt when
121
+ // the active-backend setting changes, and a URL built by yesterday's
122
+ // adapter points at a bucket this deployment no longer writes to.
123
+ new AssetReadService(emFactory, () => assetsLibrary.handle.adapters))
124
+ .singleton());
125
+ ctx.di.providePort('assetsLibraryPort', ctx
126
+ .asFunction(({ assetsLibrary }) => assetsLibrary.handle.service)
127
+ .singleton());
128
+ ctx.di.providePort('objectStoragePort', ctx
129
+ .asFunction(({ assetsLibrary }) => new ObjectStorageAdapter(assetsLibrary.handle.adapters))
130
+ .singleton());
131
+ ctx.routes(async (app) => {
132
+ await ctx.cradle().assetsLibrary.plugin(app);
133
+ });
134
+ }
135
+ /**
136
+ * The module's persisted entity classes, on the `./backend` subpath, as one
137
+ * array and **no named class export** (D-168).
138
+ *
139
+ * This is the shape the platform reads when the package is *installed*: the
140
+ * boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
141
+ * and the static declaration reader (`scripts/lib/package-declarations.ts`),
142
+ * which is the third source of `check:module-boundary`'s `table→owner` map and
143
+ * the package pass of `check-entity-tenant-classification`. A missing array is
144
+ * answered with `[]` — zero entities registered, no error anywhere.
145
+ */
146
+ export const entities = [
147
+ AssetFolder,
148
+ Asset,
149
+ ];
150
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { AdapterRegistry } from '../services/storage/adapter-registry.js';
3
+ export interface HardDeleteSweepResult {
4
+ hardDeleted: number;
5
+ pendingCleanup: number;
6
+ }
7
+ export declare class HardDeleteAssetWorker {
8
+ private readonly emFactory;
9
+ private readonly adapters;
10
+ constructor(emFactory: () => EntityManager, adapters: AdapterRegistry);
11
+ sweep(now?: Date): Promise<HardDeleteSweepResult>;
12
+ /**
13
+ * Retry pass for rows the worker previously parked in pendingCleanup.
14
+ * Same logic as `sweep` but selects rows with pending_cleanup = true.
15
+ */
16
+ sweepPending(): Promise<HardDeleteSweepResult>;
17
+ }
18
+ //# sourceMappingURL=hard-delete-asset.job.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hard-delete-asset.job.d.ts","sourceRoot":"","sources":["../../../src/backend/jobs/hard-delete-asset.job.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAG/E,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,qBAAqB;IAE9B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBADR,SAAS,EAAE,MAAM,aAAa,EAC9B,QAAQ,EAAE,eAAe;IAGtC,KAAK,CAAC,GAAG,GAAE,IAAiB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAoCnE;;;OAGG;IACG,YAAY,IAAI,OAAO,CAAC,qBAAqB,CAAC;CA8BrD"}
@@ -0,0 +1,91 @@
1
+ // Hard-delete worker — feature 013 / FR-031 / Phase 9 / T100.
2
+ //
3
+ // Periodic sweep that finalises soft-deleted assets:
4
+ // 1. Find every Asset where deletedAt is set, purgeAfterAt has elapsed,
5
+ // and pendingCleanup is false.
6
+ // 2. For each, call adapter.delete(locator). Success → DELETE the row.
7
+ // Failure → set pendingCleanup = true; the row stays soft-deleted and
8
+ // the worker retries on the next tick.
9
+ //
10
+ // `sweep()` is a plain async method so tests can drive it directly without
11
+ // Redis/BullMQ. Production wiring registers it on the existing repeatable
12
+ // queue (omitted here — repeatable scheduling lives in composition.ts).
13
+ import { Asset } from '../entities/asset.entity.js';
14
+ export class HardDeleteAssetWorker {
15
+ emFactory;
16
+ adapters;
17
+ constructor(emFactory, adapters) {
18
+ this.emFactory = emFactory;
19
+ this.adapters = adapters;
20
+ }
21
+ async sweep(now = new Date()) {
22
+ // command-coverage-ignore: retention purge, not an operator write. The
23
+ // auditable decision was the soft delete that set `deletedAt` and
24
+ // `purgeAfterAt`; this pass only executes the retention window that write
25
+ // already committed, with no actor behind it and nothing to undo.
26
+ const em = this.emFactory();
27
+ const due = await em.find(Asset, {
28
+ deletedAt: { $ne: null },
29
+ purgeAfterAt: { $lte: now },
30
+ pendingCleanup: false,
31
+ });
32
+ let hardDeleted = 0;
33
+ let newlyPending = 0;
34
+ for (const a of due) {
35
+ const adapter = await this.adapters.getForBackend(a.storageBackend);
36
+ // Legacy resolver has no delete — skip and just hard-delete the row.
37
+ if (!('delete' in adapter) || typeof adapter.delete !== 'function') {
38
+ em.remove(a);
39
+ hardDeleted += 1;
40
+ continue;
41
+ }
42
+ try {
43
+ await adapter.delete({ locator: a.storageLocator || a.storageUrl });
44
+ em.remove(a);
45
+ hardDeleted += 1;
46
+ }
47
+ catch {
48
+ a.pendingCleanup = true;
49
+ newlyPending += 1;
50
+ }
51
+ }
52
+ if (due.length > 0)
53
+ await em.flush();
54
+ return { hardDeleted, pendingCleanup: newlyPending };
55
+ }
56
+ /**
57
+ * Retry pass for rows the worker previously parked in pendingCleanup.
58
+ * Same logic as `sweep` but selects rows with pending_cleanup = true.
59
+ */
60
+ async sweepPending() {
61
+ // command-coverage-ignore: the retry half of `sweep()` — same already-audited
62
+ // retention decision, re-attempted for rows whose storage delete failed.
63
+ const em = this.emFactory();
64
+ const due = await em.find(Asset, {
65
+ deletedAt: { $ne: null },
66
+ pendingCleanup: true,
67
+ });
68
+ let hardDeleted = 0;
69
+ let stillPending = 0;
70
+ for (const a of due) {
71
+ const adapter = await this.adapters.getForBackend(a.storageBackend);
72
+ if (!('delete' in adapter) || typeof adapter.delete !== 'function') {
73
+ em.remove(a);
74
+ hardDeleted += 1;
75
+ continue;
76
+ }
77
+ try {
78
+ await adapter.delete({ locator: a.storageLocator || a.storageUrl });
79
+ em.remove(a);
80
+ hardDeleted += 1;
81
+ }
82
+ catch {
83
+ stillPending += 1;
84
+ }
85
+ }
86
+ if (due.length > 0)
87
+ await em.flush();
88
+ return { hardDeleted, pendingCleanup: stillPending };
89
+ }
90
+ }
91
+ //# sourceMappingURL=hard-delete-asset.job.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hard-delete-asset.job.js","sourceRoot":"","sources":["../../../src/backend/jobs/hard-delete-asset.job.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { EntityManager } from '@mikro-orm/postgresql';
3
+ import { AdapterRegistry } from './services/storage/adapter-registry.js';
4
+ import { AssetsLibraryService } from './services/assets-library.service.js';
5
+ import { FoldersService } from './services/folders.service.js';
6
+ import { AssetReferenceRegistry } from './services/reference-registry.js';
7
+ import { HmacSigner } from './services/hmac.js';
8
+ import type { AuditPort } from '@endora-commerce/platform/kernel';
9
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
10
+ export interface AssetsLibraryModuleOptions {
11
+ emFactory: () => EntityManager;
12
+ /** HMAC signer for local-FS private URLs. Uses ASSETS_LIBRARY_HMAC_KEY by default. */
13
+ signer?: HmacSigner;
14
+ /**
15
+ * Permission gate factory. **Required** since feature 072 (T092): it used to
16
+ * default to a permissive no-op, so a caller that forgot it got an
17
+ * unguarded asset admin surface — silently, because the safe path is the one
18
+ * every test exercises. The container supplies it now; there is no omission
19
+ * left to be silent about.
20
+ */
21
+ requireAdmin: RequireAdminFactory;
22
+ /**
23
+ * This deployment's resolved public API origin, and the base every URL this
24
+ * module produces falls back to (D-223).
25
+ *
26
+ * **Required**, for the reason the gate above is: an omitted origin is not a
27
+ * failure, it is a host-relative URL in an e-mail, a push payload and a
28
+ * partner's feed — silent, and invisible to every test that reads the URL
29
+ * from the same host that served it. `registerModule` resolves it with the
30
+ * platform's `resolvePublicApiBaseUrl()`, which is the idiom the payment
31
+ * modules already use for their callback origins.
32
+ */
33
+ publicApiBaseUrl: string;
34
+ /** Feature 054 — audits asset/folder writes co-transactionally when provided. */
35
+ auditLog?: AuditPort;
36
+ }
37
+ export interface AssetsLibraryModuleHandle {
38
+ service: AssetsLibraryService;
39
+ folders: FoldersService;
40
+ referenceRegistry: AssetReferenceRegistry;
41
+ adapters: AdapterRegistry;
42
+ }
43
+ export declare function assetsLibraryModule(options: AssetsLibraryModuleOptions): {
44
+ plugin: (app: FastifyInstance) => Promise<void>;
45
+ handle: AssetsLibraryModuleHandle;
46
+ };
47
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/backend/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAG3D,OAAO,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AAC5E,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAMlE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E,MAAM,WAAW,0BAA0B;IACzC,SAAS,EAAE,MAAM,aAAa,CAAC;IAC/B,sFAAsF;IACtF,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB;;;;;;OAMG;IACH,YAAY,EAAE,mBAAmB,CAAC;IAClC;;;;;;;;;;OAUG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,oBAAoB,CAAC;IAC9B,OAAO,EAAE,cAAc,CAAC;IACxB,iBAAiB,EAAE,sBAAsB,CAAC;IAC1C,QAAQ,EAAE,eAAe,CAAC;CAC3B;AAyBD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,0BAA0B,GAAG;IACxE,MAAM,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,MAAM,EAAE,yBAAyB,CAAC;CACnC,CAqEA"}
@@ -0,0 +1,96 @@
1
+ // Assets Library — Fastify plugin / composition root for the module.
2
+ import fastifyMultipart from '@fastify/multipart';
3
+ import { AdapterRegistry } from './services/storage/adapter-registry.js';
4
+ import { AssetsLibraryService } from './services/assets-library.service.js';
5
+ import { FoldersService } from './services/folders.service.js';
6
+ import { AssetReferenceRegistry } from './services/reference-registry.js';
7
+ import { HmacSigner } from './services/hmac.js';
8
+ import { createSettingsView } from './services/storage/settings-view.js';
9
+ import { Setting } from '@endora-commerce/platform/kernel';
10
+ import { SettingValue } from '@endora-commerce/platform/kernel';
11
+ import { registerAssetsLibraryAdminRoutes } from './routes.admin.js';
12
+ import { registerAssetsLibraryPublicRoutes } from './routes.public.js';
13
+ async function loadUploadPolicy(emFactory) {
14
+ const em = emFactory();
15
+ const allowed = await em.findOne(Setting, { code: 'assets.allowed_file_types' });
16
+ const allowedOverride = allowed
17
+ ? await em.findOne(SettingValue, { setting: allowed })
18
+ : null;
19
+ const allowedRaw = allowedOverride
20
+ ? allowedOverride.value
21
+ : allowed?.defaultValue ?? ['*'];
22
+ const allowedTypes = Array.isArray(allowedRaw) ? allowedRaw.map(String) : ['*'];
23
+ const max = await em.findOne(Setting, { code: 'assets.max_file_size_mb' });
24
+ const maxOverride = max ? await em.findOne(SettingValue, { setting: max }) : null;
25
+ const maxRaw = maxOverride ? maxOverride.value : max?.defaultValue ?? 0;
26
+ const maxFileSizeMb = typeof maxRaw === 'number' ? maxRaw : Number(maxRaw) || 0;
27
+ return { allowedTypes, maxFileSizeMb };
28
+ }
29
+ export function assetsLibraryModule(options) {
30
+ let cachedSigner = options.signer;
31
+ const adapters = new AdapterRegistry({
32
+ settings: createSettingsView(options.emFactory),
33
+ publicApiBaseUrl: options.publicApiBaseUrl,
34
+ signer: () => {
35
+ if (!cachedSigner)
36
+ cachedSigner = HmacSigner.fromEnv();
37
+ return cachedSigner;
38
+ },
39
+ });
40
+ const referenceRegistry = new AssetReferenceRegistry();
41
+ const service = new AssetsLibraryService({
42
+ emFactory: options.emFactory,
43
+ adapters,
44
+ referenceRegistry,
45
+ loadUploadPolicy: () => loadUploadPolicy(options.emFactory),
46
+ ...(options.auditLog ? { auditLog: options.auditLog } : {}),
47
+ });
48
+ const folders = new FoldersService(options.emFactory, referenceRegistry, options.auditLog);
49
+ const requireAdmin = options.requireAdmin;
50
+ const plugin = async (app) => {
51
+ // Resolve the upload policy ONCE at registration so the multipart
52
+ // parser's hard cap matches the operator-configured maximum. Without
53
+ // an explicit `fileSize`, @fastify/multipart silently falls back to
54
+ // its 1 MiB default and tears down the file stream on anything larger
55
+ // — which surfaces as a 500 from the awaited adapter pipeline rather
56
+ // than a clean 413 (the symptom that motivated this code).
57
+ //
58
+ // The pipeline's per-request size check still runs against the LIVE
59
+ // setting on every upload, so tightening the cap takes effect without
60
+ // a restart. Loosening it (or switching from a finite cap to "no cap")
61
+ // requires a backend restart for the multipart parser to pick up the
62
+ // new `fileSize` value.
63
+ const initialPolicy = await loadUploadPolicy(options.emFactory);
64
+ const FALLBACK_HARD_CEILING_BYTES = 5 * 1024 * 1024 * 1024; // 5 GiB
65
+ const multipartFileSize = initialPolicy.maxFileSizeMb > 0
66
+ ? initialPolicy.maxFileSizeMb * 1024 * 1024
67
+ : FALLBACK_HARD_CEILING_BYTES;
68
+ // Multipart is registered in a child encapsulation context so other modules'
69
+ // routes are unaffected by its options.
70
+ await app.register(async (childApp) => {
71
+ await childApp.register(fastifyMultipart, {
72
+ // Keep memory bounded; upload-pipeline streams directly to the adapter.
73
+ limits: { files: 1, fields: 10, fileSize: multipartFileSize },
74
+ });
75
+ await registerAssetsLibraryAdminRoutes(childApp, {
76
+ service,
77
+ folders,
78
+ adapters,
79
+ emFactory: options.emFactory,
80
+ requireAdmin,
81
+ });
82
+ });
83
+ await registerAssetsLibraryPublicRoutes(app, {
84
+ service,
85
+ adapters,
86
+ emFactory: options.emFactory,
87
+ signer: () => {
88
+ if (!cachedSigner)
89
+ cachedSigner = HmacSigner.fromEnv();
90
+ return cachedSigner;
91
+ },
92
+ });
93
+ };
94
+ return { plugin, handle: { service, folders, referenceRegistry, adapters } };
95
+ }
96
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/backend/plugin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { EntityManager } from '@mikro-orm/postgresql';
3
+ import type { AssetsLibraryService } from './services/assets-library.service.js';
4
+ import type { FoldersService } from './services/folders.service.js';
5
+ import type { AdapterRegistry } from './services/storage/adapter-registry.js';
6
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
7
+ export interface AdminRoutesDeps {
8
+ service: AssetsLibraryService;
9
+ folders: FoldersService;
10
+ adapters: AdapterRegistry;
11
+ emFactory: () => EntityManager;
12
+ requireAdmin: RequireAdminFactory;
13
+ }
14
+ export declare function registerAssetsLibraryAdminRoutes(app: FastifyInstance, deps: AdminRoutesDeps): Promise<void>;
15
+ //# sourceMappingURL=routes.admin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.admin.d.ts","sourceRoot":"","sources":["../../src/backend/routes.admin.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAC7E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAc3D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AACjF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wCAAwC,CAAC;AAC9E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,oBAAoB,CAAC;IAC9B,OAAO,EAAE,cAAc,CAAC;IACxB,QAAQ,EAAE,eAAe,CAAC;IAC1B,SAAS,EAAE,MAAM,aAAa,CAAC;IAC/B,YAAY,EAAE,mBAAmB,CAAC;CACnC;AAED,wBAAsB,gCAAgC,CACpD,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,IAAI,CAAC,CAmQf"}
@@ -0,0 +1,189 @@
1
+ // Admin HTTP surface for the Assets Library — feature 013 / US1.
2
+ // Mounts under /api/v1/admin/assets/*.
3
+ //
4
+ // Routes implemented in this phase (US1):
5
+ // POST /api/v1/admin/assets — multipart upload
6
+ // GET /api/v1/admin/assets — list (folder filter + pagination)
7
+ // GET /api/v1/admin/assets/:id — detail with references
8
+ // GET /api/v1/admin/assets/:id/url — re-resolve URL on demand
9
+ // PATCH /api/v1/admin/assets/:id — edit metadata + visibility flip
10
+ //
11
+ // Folder CRUD, soft-delete, restore, bulk move, and storage self-check
12
+ // land in subsequent user stories.
13
+ import { ERROR_CODES, patchAssetRequestSchema, listAssetsQuerySchema, createFolderRequestSchema, patchFolderRequestSchema, deleteFolderRequestSchema, moveAssetRequestSchema, moveManyAssetsRequestSchema, } from '@endora-commerce/contracts';
14
+ import { HttpError } from '@endora-commerce/platform/http';
15
+ import { Asset } from './entities/asset.entity.js';
16
+ export async function registerAssetsLibraryAdminRoutes(app, deps) {
17
+ const { service, folders, adapters, emFactory, requireAdmin } = deps;
18
+ // — POST /assets (multipart upload) -----------------------------------------
19
+ app.post('/api/v1/admin/assets', { preHandler: requireAdmin('assets.write') }, async (req, reply) => {
20
+ // Confirm we're handling a multipart request.
21
+ if (!req.isMultipart()) {
22
+ throw new HttpError(400, ERROR_CODES.ASSET_UPLOAD_NO_FILE, 'Upload requires multipart/form-data with a `file` part.');
23
+ }
24
+ // Collect non-file fields once; @fastify/multipart streams parts.
25
+ let folderId = null;
26
+ let label = null;
27
+ let visibility = 'public';
28
+ let asset = null;
29
+ for await (const part of req.parts()) {
30
+ if (part.type === 'file') {
31
+ if (asset) {
32
+ throw new HttpError(400, ERROR_CODES.ASSET_UPLOAD_NO_FILE, 'Only one file per request is allowed.');
33
+ }
34
+ asset = await service.upload({
35
+ filename: part.filename ?? 'unknown',
36
+ declaredMime: part.mimetype ?? 'application/octet-stream',
37
+ stream: part.file,
38
+ // @fastify/multipart does not provide a guaranteed size.
39
+ declaredSize: 0,
40
+ folderId,
41
+ label,
42
+ visibility,
43
+ });
44
+ }
45
+ else if (part.fieldname === 'folderId') {
46
+ folderId = part.value || null;
47
+ }
48
+ else if (part.fieldname === 'label') {
49
+ label = part.value || null;
50
+ }
51
+ else if (part.fieldname === 'visibility') {
52
+ const v = part.value;
53
+ if (v !== 'public' && v !== 'private') {
54
+ throw new HttpError(400, ERROR_CODES.VALIDATION_FAILED, `visibility must be public|private; got "${v}".`);
55
+ }
56
+ visibility = v;
57
+ }
58
+ }
59
+ if (!asset) {
60
+ throw new HttpError(400, ERROR_CODES.ASSET_UPLOAD_NO_FILE, 'Multipart body did not include a `file` part.');
61
+ }
62
+ reply.status(201);
63
+ return { data: asset };
64
+ });
65
+ // — GET /assets (paginated list) -------------------------------------------
66
+ app.get('/api/v1/admin/assets', {
67
+ preHandler: requireAdmin('assets.read'),
68
+ }, async (req) => {
69
+ const parsed = listAssetsQuerySchema.parse(req.query);
70
+ const out = await service.listAssets({
71
+ folderId: parsed.folderId ?? null,
72
+ ...(parsed.q !== undefined ? { q: parsed.q } : {}),
73
+ ...(parsed.mime !== undefined ? { mime: parsed.mime } : {}),
74
+ ...(parsed.visibility !== undefined ? { visibility: parsed.visibility } : {}),
75
+ includeDeleted: parsed.includeDeleted,
76
+ ...(parsed.cursor !== undefined ? { cursor: parsed.cursor } : {}),
77
+ limit: parsed.limit,
78
+ });
79
+ return out;
80
+ });
81
+ // — GET /assets/:id --------------------------------------------------------
82
+ app.get('/api/v1/admin/assets/:id', { preHandler: requireAdmin('assets.read') }, async (req) => {
83
+ const detail = await service.getAsset(req.params.id);
84
+ return { data: detail };
85
+ });
86
+ // — GET /assets/:id/url ----------------------------------------------------
87
+ app.get('/api/v1/admin/assets/:id/url', { preHandler: requireAdmin('assets.read') }, async (req) => {
88
+ const resolved = await service.resolveUrl(req.params.id);
89
+ return {
90
+ data: {
91
+ url: resolved.url,
92
+ expiresAt: resolved.expiresAt ? resolved.expiresAt.toISOString() : null,
93
+ },
94
+ };
95
+ });
96
+ // — DELETE /assets/:id (soft-delete + reference protection) ---------------
97
+ app.delete('/api/v1/admin/assets/:id', { preHandler: requireAdmin('assets.write') }, async (req) => {
98
+ const out = await service.softDelete(req.params.id);
99
+ return {
100
+ data: {
101
+ deletedAt: out.deletedAt.toISOString(),
102
+ purgeAfterAt: out.purgeAfterAt.toISOString(),
103
+ },
104
+ };
105
+ });
106
+ // — POST /assets/:id/restore ----------------------------------------------
107
+ app.post('/api/v1/admin/assets/:id/restore', { preHandler: requireAdmin('assets.write') }, async (req) => {
108
+ const detail = await service.restore(req.params.id);
109
+ return { data: detail };
110
+ });
111
+ // — POST /assets/:id/move --------------------------------------------------
112
+ app.post('/api/v1/admin/assets/:id/move', { preHandler: requireAdmin('assets.write') }, async (req) => {
113
+ const body = moveAssetRequestSchema.parse(req.body);
114
+ const detail = await service.move(req.params.id, body.folderId ?? null);
115
+ return { data: detail };
116
+ });
117
+ // — POST /assets/move-many -------------------------------------------------
118
+ app.post('/api/v1/admin/assets/move-many', { preHandler: requireAdmin('assets.write') }, async (req) => {
119
+ const body = moveManyAssetsRequestSchema.parse(req.body);
120
+ const out = await service.moveMany(body.assetIds, body.folderId ?? null);
121
+ return { data: out };
122
+ });
123
+ // — Storage administration -------------------------------------------------
124
+ app.post('/api/v1/admin/assets/storage/self-check', { preHandler: requireAdmin('assets.write') }, async () => {
125
+ const adapter = await adapters.getActive();
126
+ const result = await adapter.selfCheck();
127
+ return {
128
+ data: { adapter: adapter.code, ...result },
129
+ };
130
+ });
131
+ app.get('/api/v1/admin/assets/storage/state', { preHandler: requireAdmin('assets.read') }, async () => {
132
+ const adapter = await adapters.getActive().catch((err) => {
133
+ return {
134
+ code: 'local',
135
+ // Surface the misconfig as a not-ok self-check rather than 500.
136
+ async selfCheck() {
137
+ return { ok: false, reason: err instanceof Error ? err.message : String(err) };
138
+ },
139
+ };
140
+ });
141
+ const result = await adapter.selfCheck();
142
+ const em = emFactory();
143
+ const pendingCleanupCount = await em.count(Asset, { pendingCleanup: true });
144
+ const softDeletedCount = await em.count(Asset, { deletedAt: { $ne: null } });
145
+ return {
146
+ data: {
147
+ activeAdapter: adapter.code,
148
+ selfCheck: result,
149
+ pendingCleanupCount,
150
+ softDeletedCount,
151
+ },
152
+ };
153
+ });
154
+ // — Folders ----------------------------------------------------------------
155
+ app.get('/api/v1/admin/assets/folders', { preHandler: requireAdmin('assets.read') }, async () => ({ data: await folders.listFolderTree() }));
156
+ app.post('/api/v1/admin/assets/folders', { preHandler: requireAdmin('assets.write') }, async (req, reply) => {
157
+ const body = createFolderRequestSchema.parse(req.body);
158
+ const f = await folders.createFolder(body);
159
+ reply.status(201);
160
+ return { data: f };
161
+ });
162
+ app.patch('/api/v1/admin/assets/folders/:folderId', { preHandler: requireAdmin('assets.write') }, async (req) => {
163
+ const body = patchFolderRequestSchema.parse(req.body);
164
+ const f = await folders.patchFolder(req.params.folderId, {
165
+ ...(body.name !== undefined ? { name: body.name } : {}),
166
+ ...(body.parentId !== undefined ? { parentId: body.parentId } : {}),
167
+ ...(body.position !== undefined ? { position: body.position } : {}),
168
+ });
169
+ return { data: f };
170
+ });
171
+ app.delete('/api/v1/admin/assets/folders/:folderId', { preHandler: requireAdmin('assets.write') }, async (req) => {
172
+ const body = deleteFolderRequestSchema.parse(req.body ?? {});
173
+ const out = await folders.deleteFolder(req.params.folderId, body.ifNonEmpty);
174
+ return { data: out };
175
+ });
176
+ // — PATCH /assets/:id ------------------------------------------------------
177
+ app.patch('/api/v1/admin/assets/:id', { preHandler: requireAdmin('assets.write') }, async (req) => {
178
+ const patch = patchAssetRequestSchema.parse(req.body);
179
+ const detail = await service.patchAsset(req.params.id, {
180
+ ...(patch.filename !== undefined ? { filename: patch.filename } : {}),
181
+ ...(patch.label !== undefined ? { label: patch.label } : {}),
182
+ ...(patch.mimeType !== undefined ? { mimeType: patch.mimeType } : {}),
183
+ ...(patch.visibility !== undefined ? { visibility: patch.visibility } : {}),
184
+ ...(patch.folderId !== undefined ? { folderId: patch.folderId } : {}),
185
+ });
186
+ return { data: detail };
187
+ });
188
+ }
189
+ //# sourceMappingURL=routes.admin.js.map