@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,59 @@
1
+ import { type ReadStream } from 'node:fs';
2
+ import type { StorageAdapter, StorageAdapterSelfCheck, StorageNewLocatorInput, StoragePutInput, StorageResolveUrlInput, StorageResolveUrlOutput } from './storage-adapter.js';
3
+ import type { HmacSigner } from '../hmac.js';
4
+ export interface LocalFsAdapterOptions {
5
+ /** Filesystem root that holds every asset. */
6
+ baseDir: string;
7
+ /**
8
+ * Public-facing URL prefix (no trailing slash), from
9
+ * `assets.local.public_url_base`. Blank on every deployment that never set
10
+ * it — see {@link LocalFsAdapterOptions.publicApiBaseUrl}.
11
+ */
12
+ publicUrlBase: string;
13
+ /**
14
+ * This deployment's resolved public API origin — D-223's fallback base, used
15
+ * when `publicUrlBase` is blank.
16
+ *
17
+ * **Required rather than optional.** An omission here is a host-relative URL
18
+ * inside an e-mail, a push payload or a partner's feed, which is precisely
19
+ * the silent, safe-path-is-the-tested-path shape this module removed once
20
+ * before (feature 072, T092, `requireAdmin`).
21
+ */
22
+ publicApiBaseUrl: string;
23
+ /** TTL for private URL signatures, in seconds. */
24
+ privateUrlTtlSec: number;
25
+ /** HMAC signer for private URLs. */
26
+ signer: HmacSigner;
27
+ /** Now-source — injectable for tests. */
28
+ now?: () => Date;
29
+ }
30
+ export declare class LocalFsStorageAdapter implements StorageAdapter {
31
+ private readonly opts;
32
+ readonly code: "local";
33
+ /**
34
+ * The origin-and-prefix every URL this adapter builds starts with, decided
35
+ * once at construction: the operator's configured base when there is one,
36
+ * this deployment's public API origin when there is not (D-223).
37
+ */
38
+ private readonly publicBase;
39
+ constructor(opts: LocalFsAdapterOptions);
40
+ selfCheck(): Promise<StorageAdapterSelfCheck>;
41
+ newLocator(input: StorageNewLocatorInput): string;
42
+ put(input: StoragePutInput): Promise<void>;
43
+ resolveUrl(input: StorageResolveUrlInput): Promise<StorageResolveUrlOutput>;
44
+ open(input: {
45
+ locator: string;
46
+ }): Promise<ReadStream>;
47
+ delete(input: {
48
+ locator: string;
49
+ }): Promise<void>;
50
+ /**
51
+ * Visibility on local-FS is enforced per-request by routes.public.ts (it
52
+ * reads the asset's current visibility live and demands a token for
53
+ * private). No server-side state to flip — no-op.
54
+ */
55
+ setVisibility(): Promise<void>;
56
+ private toAbsolute;
57
+ }
58
+ export declare function parseAssetIdFromLocator(locator: string): string;
59
+ //# sourceMappingURL=local-fs-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-fs-adapter.d.ts","sourceRoot":"","sources":["../../../../src/backend/services/storage/local-fs-adapter.ts"],"names":[],"mappings":"AAcA,OAAO,EAAoB,KAAK,UAAU,EAAE,MAAM,SAAS,CAAC;AAM5D,OAAO,KAAK,EACV,cAAc,EACd,uBAAuB,EACvB,sBAAsB,EACtB,eAAe,EACf,sBAAsB,EACtB,uBAAuB,EACxB,MAAM,sBAAsB,CAAC;AAG9B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAO7C,MAAM,WAAW,qBAAqB;IACpC,8CAA8C;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;;;;;;;OAQG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB,kDAAkD;IAClD,gBAAgB,EAAE,MAAM,CAAC;IACzB,oCAAoC;IACpC,MAAM,EAAE,UAAU,CAAC;IACnB,yCAAyC;IACzC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,qBAAa,qBAAsB,YAAW,cAAc;IAU9C,OAAO,CAAC,QAAQ,CAAC,IAAI;IATjC,QAAQ,CAAC,IAAI,EAAG,OAAO,CAAU;IAEjC;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;gBAEP,IAAI,EAAE,qBAAqB;IASlD,SAAS,IAAI,OAAO,CAAC,uBAAuB,CAAC;IAenD,UAAU,CAAC,KAAK,EAAE,sBAAsB,GAAG,MAAM;IAI3C,GAAG,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IA0B1C,UAAU,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAmB3E,IAAI,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,UAAU,CAAC;IAWrD,MAAM,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAevD;;;;OAIG;IACG,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAIpC,OAAO,CAAC,UAAU;CAQnB;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAY/D"}
@@ -0,0 +1,153 @@
1
+ // Local filesystem storage adapter. Files live under a configurable base
2
+ // directory (settings: `assets.local.baseDir`, default `var/assets`) using
3
+ // the sharded path scheme from research.md R5.
4
+ //
5
+ // Public URLs are stable (`<base>/assets/file/<assetId>`), served by
6
+ // routes.public.ts. Private URLs append `?token=<hmac>&exp=<unix>` validated by
7
+ // the same route.
8
+ //
9
+ // `<base>` is absolute since D-223: `assets.local.public_url_base` when the
10
+ // operator set one, this deployment's resolved public API origin when they did
11
+ // not. It used to be the empty string in the second case, which made every URL
12
+ // this adapter produced host-relative and left four composition-root sites and
13
+ // two frontend helpers compensating for it, disagreeing.
14
+ import { createReadStream } from 'node:fs';
15
+ import { mkdir, rename, stat, unlink, access, constants as fsConstants } from 'node:fs/promises';
16
+ import { dirname, join, isAbsolute } from 'node:path';
17
+ import { pipeline } from 'node:stream/promises';
18
+ import { createWriteStream } from 'node:fs';
19
+ import { computeLocator } from './locator.js';
20
+ import { resolvePublicUrlBase } from './public-url-base.js';
21
+ import { BackendUnavailableError, ConfigurationError, LocatorMissingError, } from './errors.js';
22
+ export class LocalFsStorageAdapter {
23
+ opts;
24
+ code = 'local';
25
+ /**
26
+ * The origin-and-prefix every URL this adapter builds starts with, decided
27
+ * once at construction: the operator's configured base when there is one,
28
+ * this deployment's public API origin when there is not (D-223).
29
+ */
30
+ publicBase;
31
+ constructor(opts) {
32
+ this.opts = opts;
33
+ if (!isAbsolute(opts.baseDir)) {
34
+ // Resolve relative to process cwd at construction time so we never
35
+ // accidentally write outside the configured tree.
36
+ this.opts = { ...opts, baseDir: join(process.cwd(), opts.baseDir) };
37
+ }
38
+ this.publicBase = resolvePublicUrlBase(opts.publicUrlBase, opts.publicApiBaseUrl);
39
+ }
40
+ async selfCheck() {
41
+ try {
42
+ await access(this.opts.baseDir, fsConstants.W_OK | fsConstants.R_OK);
43
+ const s = await stat(this.opts.baseDir);
44
+ if (!s.isDirectory()) {
45
+ return { ok: false, reason: `Base path is not a directory: ${this.opts.baseDir}` };
46
+ }
47
+ return { ok: true };
48
+ }
49
+ catch (e) {
50
+ // ENOENT → try to create it; that's a recoverable misconfig.
51
+ const reason = e instanceof Error ? e.message : String(e);
52
+ return { ok: false, reason };
53
+ }
54
+ }
55
+ newLocator(input) {
56
+ return computeLocator(input);
57
+ }
58
+ async put(input) {
59
+ if (!input.locator)
60
+ throw new LocatorMissingError('LocalFsStorageAdapter.put: empty locator');
61
+ const abs = this.toAbsolute(input.locator);
62
+ await mkdir(dirname(abs), { recursive: true });
63
+ const tmp = `${abs}.tmp-${process.pid}-${Date.now()}`;
64
+ try {
65
+ await pipeline(input.stream, createWriteStream(tmp));
66
+ }
67
+ catch (e) {
68
+ // Best-effort cleanup of the temp file.
69
+ try {
70
+ await unlink(tmp);
71
+ }
72
+ catch { /* ignore */ }
73
+ throw new BackendUnavailableError(`LocalFsStorageAdapter.put: failed writing ${input.locator}`, e);
74
+ }
75
+ try {
76
+ await rename(tmp, abs);
77
+ }
78
+ catch (e) {
79
+ try {
80
+ await unlink(tmp);
81
+ }
82
+ catch { /* ignore */ }
83
+ throw new BackendUnavailableError(`LocalFsStorageAdapter.put: failed renaming temp into place at ${input.locator}`, e);
84
+ }
85
+ }
86
+ async resolveUrl(input) {
87
+ const assetId = parseAssetIdFromLocator(input.locator);
88
+ const base = this.publicBase;
89
+ if (input.visibility === 'public') {
90
+ return { url: `${base}/assets/file/${assetId}`, expiresAt: null };
91
+ }
92
+ const ttl = input.ttlSec ?? this.opts.privateUrlTtlSec;
93
+ if (!Number.isInteger(ttl) || ttl <= 0) {
94
+ throw new ConfigurationError(`LocalFsStorageAdapter.resolveUrl: invalid TTL ${ttl}`);
95
+ }
96
+ const now = this.opts.now ?? (() => new Date());
97
+ const exp = Math.floor(now().getTime() / 1000) + ttl;
98
+ const token = this.opts.signer.sign({ assetId, exp });
99
+ return {
100
+ url: `${base}/assets/file/${assetId}?token=${token}&exp=${exp}`,
101
+ expiresAt: new Date(exp * 1000),
102
+ };
103
+ }
104
+ async open(input) {
105
+ const abs = this.toAbsolute(input.locator);
106
+ try {
107
+ // stat first so a missing file produces a recognizable error path.
108
+ await stat(abs);
109
+ }
110
+ catch (e) {
111
+ throw new BackendUnavailableError(`LocalFsStorageAdapter.open: missing file ${abs}`, e);
112
+ }
113
+ return createReadStream(abs);
114
+ }
115
+ async delete(input) {
116
+ const abs = this.toAbsolute(input.locator);
117
+ try {
118
+ await unlink(abs);
119
+ }
120
+ catch (e) {
121
+ // ENOENT is treated as failure here — the worker (FR-031) needs the
122
+ // backend to confirm removal; if the file is already gone we still
123
+ // need an explicit signal. Caller decides whether that's recoverable.
124
+ throw new BackendUnavailableError(`LocalFsStorageAdapter.delete: cannot remove ${abs}`, e);
125
+ }
126
+ }
127
+ /**
128
+ * Visibility on local-FS is enforced per-request by routes.public.ts (it
129
+ * reads the asset's current visibility live and demands a token for
130
+ * private). No server-side state to flip — no-op.
131
+ */
132
+ async setVisibility() {
133
+ /* no-op */
134
+ }
135
+ toAbsolute(locator) {
136
+ if (locator.startsWith('/')) {
137
+ throw new LocatorMissingError(`LocalFsStorageAdapter: locator must be relative, got "${locator}"`);
138
+ }
139
+ return join(this.opts.baseDir, locator);
140
+ }
141
+ }
142
+ export function parseAssetIdFromLocator(locator) {
143
+ // Locators produced by computeLocator look like `aa/bb/<uuid>.<ext>`.
144
+ // We extract the basename's UUID portion (everything before the first dot).
145
+ const last = locator.split('/').pop() ?? '';
146
+ const dotIdx = last.indexOf('.');
147
+ const id = dotIdx >= 0 ? last.slice(0, dotIdx) : last;
148
+ if (id.length < 4) {
149
+ throw new LocatorMissingError(`parseAssetIdFromLocator: cannot extract asset id from "${locator}"`);
150
+ }
151
+ return id;
152
+ }
153
+ //# sourceMappingURL=local-fs-adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-fs-adapter.js","sourceRoot":"","sources":["../../../../src/backend/services/storage/local-fs-adapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export declare function computeLocator(input: {
2
+ assetId: string;
3
+ originalFilename: string;
4
+ }): string;
5
+ //# sourceMappingURL=locator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locator.d.ts","sourceRoot":"","sources":["../../../../src/backend/services/storage/locator.ts"],"names":[],"mappings":"AAMA,wBAAgB,cAAc,CAAC,KAAK,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAU3F"}
@@ -0,0 +1,16 @@
1
+ // Pure helper: shard the asset UUID into a directory path of the form
2
+ // `<aa>/<bb>/<assetId>.<ext>` so neither the local-FS leaf directories nor
3
+ // the cloud bucket key space ever bunches up. See research.md R5.
4
+ import { extname } from 'node:path';
5
+ export function computeLocator(input) {
6
+ const id = input.assetId.toLowerCase();
7
+ if (id.length < 4) {
8
+ throw new Error(`computeLocator: assetId too short ("${input.assetId}")`);
9
+ }
10
+ const aa = id.slice(0, 2);
11
+ const bb = id.slice(2, 4);
12
+ const rawExt = extname(input.originalFilename).slice(1);
13
+ const ext = rawExt.length > 0 ? rawExt : 'bin';
14
+ return `${aa}/${bb}/${id}.${ext}`;
15
+ }
16
+ //# sourceMappingURL=locator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locator.js","sourceRoot":"","sources":["../../../../src/backend/services/storage/locator.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,2EAA2E;AAC3E,kEAAkE;AAElE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,MAAM,UAAU,cAAc,CAAC,KAAoD;IACjF,MAAM,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;IACvC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CAAC,uCAAuC,KAAK,CAAC,OAAO,IAAI,CAAC,CAAC;IAC5E,CAAC;IACD,MAAM,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1B,MAAM,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1B,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACxD,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;IAC/C,OAAO,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,GAAG,EAAE,CAAC;AACpC,CAAC"}
@@ -0,0 +1,46 @@
1
+ import type { ObjectStorageBackendCode, ObjectStoragePort, ObjectStore } from '@endora-commerce/contracts';
2
+ import type { StorageAdapter } from './storage-adapter.js';
3
+ /**
4
+ * `objectStoragePort` — the byte store this module's storage configuration
5
+ * already describes, published for the modules that store their own objects in
6
+ * it (`specs/110-instance-repository/` T118c).
7
+ *
8
+ * It is a **mapping and nothing else**: the in-process `StorageAdapter` SPI
9
+ * cannot be published as it stands, three ways, and each of the three is what
10
+ * one line of this file answers.
11
+ *
12
+ * 1. `put` and `open` name `NodeJS.ReadableStream`, and `@endora-commerce/contracts`
13
+ * is compiled by `@endora-commerce/admin-kit` with `types: ["vite/client"]`
14
+ * — no `@types/node`, so naming that namespace fails a consumer's build.
15
+ * {@link AssetUploadStream} and {@link AssetByteStream} are the structural
16
+ * shapes that survive it, and `Readable.from` is the one line that bridges
17
+ * the write direction. The read direction needs no bridge: a Node stream
18
+ * already satisfies the structural shape.
19
+ * 2. `setVisibility?` is **optional**, which D-97.3 refuses outright on a
20
+ * published port — `lazyPort`'s proxy answers every property with a
21
+ * function, so feature detection through one is impossible by construction.
22
+ * It is absent here, and not by omission: an object a consumer stores under
23
+ * its own prefix is written `private` and served by that consumer's own
24
+ * route, so there is no visibility to flip.
25
+ * 3. `getForBackend` answered `StorageAdapter | LegacyAssetResolver`, and both
26
+ * consumers of the byte surface probed for `open` at the call site to find
27
+ * out which they had. Here it is **total**: `legacy` is not an object store
28
+ * — it is a resolver for pre-013 rows whose URL this platform did not issue
29
+ * — so it is not in {@link ObjectStorageBackendCode} and there is no branch
30
+ * left to probe for.
31
+ *
32
+ * `newLocator`, `resolveUrl` and `selfCheck` are absent for the same reason:
33
+ * they are the *library's* questions. A consumer of this port computes its own
34
+ * locator under its own prefix, serves its objects itself, and has no
35
+ * administrative screen to report a backend's health on.
36
+ */
37
+ export declare class ObjectStorageAdapter implements ObjectStoragePort {
38
+ private readonly registry;
39
+ constructor(registry: {
40
+ getActive(): Promise<StorageAdapter>;
41
+ getForBackend(backend: ObjectStorageBackendCode): Promise<StorageAdapter>;
42
+ });
43
+ getActive(): Promise<ObjectStore>;
44
+ getForBackend(backend: ObjectStorageBackendCode): Promise<ObjectStore>;
45
+ }
46
+ //# sourceMappingURL=object-storage-port.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"object-storage-port.d.ts","sourceRoot":"","sources":["../../../../src/backend/services/storage/object-storage-port.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,wBAAwB,EACxB,iBAAiB,EAEjB,WAAW,EACZ,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,qBAAa,oBAAqB,YAAW,iBAAiB;IAE1D,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE;QACzB,SAAS,IAAI,OAAO,CAAC,cAAc,CAAC,CAAC;QACrC,aAAa,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;KAC3E;IAGG,SAAS,IAAI,OAAO,CAAC,WAAW,CAAC;IAIjC,aAAa,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,WAAW,CAAC;CAG7E"}
@@ -0,0 +1,78 @@
1
+ import { Readable } from 'node:stream';
2
+ /**
3
+ * `objectStoragePort` — the byte store this module's storage configuration
4
+ * already describes, published for the modules that store their own objects in
5
+ * it (`specs/110-instance-repository/` T118c).
6
+ *
7
+ * It is a **mapping and nothing else**: the in-process `StorageAdapter` SPI
8
+ * cannot be published as it stands, three ways, and each of the three is what
9
+ * one line of this file answers.
10
+ *
11
+ * 1. `put` and `open` name `NodeJS.ReadableStream`, and `@endora-commerce/contracts`
12
+ * is compiled by `@endora-commerce/admin-kit` with `types: ["vite/client"]`
13
+ * — no `@types/node`, so naming that namespace fails a consumer's build.
14
+ * {@link AssetUploadStream} and {@link AssetByteStream} are the structural
15
+ * shapes that survive it, and `Readable.from` is the one line that bridges
16
+ * the write direction. The read direction needs no bridge: a Node stream
17
+ * already satisfies the structural shape.
18
+ * 2. `setVisibility?` is **optional**, which D-97.3 refuses outright on a
19
+ * published port — `lazyPort`'s proxy answers every property with a
20
+ * function, so feature detection through one is impossible by construction.
21
+ * It is absent here, and not by omission: an object a consumer stores under
22
+ * its own prefix is written `private` and served by that consumer's own
23
+ * route, so there is no visibility to flip.
24
+ * 3. `getForBackend` answered `StorageAdapter | LegacyAssetResolver`, and both
25
+ * consumers of the byte surface probed for `open` at the call site to find
26
+ * out which they had. Here it is **total**: `legacy` is not an object store
27
+ * — it is a resolver for pre-013 rows whose URL this platform did not issue
28
+ * — so it is not in {@link ObjectStorageBackendCode} and there is no branch
29
+ * left to probe for.
30
+ *
31
+ * `newLocator`, `resolveUrl` and `selfCheck` are absent for the same reason:
32
+ * they are the *library's* questions. A consumer of this port computes its own
33
+ * locator under its own prefix, serves its objects itself, and has no
34
+ * administrative screen to report a backend's health on.
35
+ */
36
+ export class ObjectStorageAdapter {
37
+ registry;
38
+ constructor(registry) {
39
+ this.registry = registry;
40
+ }
41
+ async getActive() {
42
+ return wrap(await this.registry.getActive());
43
+ }
44
+ async getForBackend(backend) {
45
+ return wrap(await this.registry.getForBackend(backend));
46
+ }
47
+ }
48
+ function wrap(adapter) {
49
+ // `code` is `StorageBackendCode` on the SPI and `legacy` is unrepresentable
50
+ // here: `getActive` builds one of the three real backends and throws on
51
+ // anything else, and `getForBackend` is overloaded so that the legacy arm is
52
+ // reachable only for the literal `'legacy'`, which this port cannot name.
53
+ const code = adapter.code;
54
+ return {
55
+ code,
56
+ async put(input) {
57
+ await adapter.put({
58
+ locator: input.locator,
59
+ mimeType: input.mimeType,
60
+ visibility: input.visibility,
61
+ // The one bridge, and it is here rather than in the caller because the
62
+ // caller is the one that must not name `node:stream`'s types across a
63
+ // package boundary. A Node `Readable` passed in comes back out of
64
+ // `Readable.from` as itself in every practical sense — it is iterated,
65
+ // not copied.
66
+ stream: Readable.from(input.stream),
67
+ sizeBytes: input.sizeBytes,
68
+ });
69
+ },
70
+ async open(input) {
71
+ return adapter.open(input);
72
+ },
73
+ async delete(input) {
74
+ await adapter.delete(input);
75
+ },
76
+ };
77
+ }
78
+ //# sourceMappingURL=object-storage-port.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"object-storage-port.js","sourceRoot":"","sources":["../../../../src/backend/services/storage/object-storage-port.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAUvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,OAAO,oBAAoB;IAEZ;IADnB,YACmB,QAGhB;QAHgB,aAAQ,GAAR,QAAQ,CAGxB;IACA,CAAC;IAEJ,KAAK,CAAC,SAAS;QACb,OAAO,IAAI,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,OAAiC;QACnD,OAAO,IAAI,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;IAC1D,CAAC;CACF;AAED,SAAS,IAAI,CAAC,OAAuB;IACnC,4EAA4E;IAC5E,wEAAwE;IACxE,6EAA6E;IAC7E,0EAA0E;IAC1E,MAAM,IAAI,GAAG,OAAO,CAAC,IAAgC,CAAC;IACtD,OAAO;QACL,IAAI;QACJ,KAAK,CAAC,GAAG,CAAC,KAA4B;YACpC,MAAM,OAAO,CAAC,GAAG,CAAC;gBAChB,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,QAAQ,EAAE,KAAK,CAAC,QAAQ;gBACxB,UAAU,EAAE,KAAK,CAAC,UAAU;gBAC5B,uEAAuE;gBACvE,sEAAsE;gBACtE,kEAAkE;gBAClE,uEAAuE;gBACvE,cAAc;gBACd,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;gBACnC,SAAS,EAAE,KAAK,CAAC,SAAS;aAC3B,CAAC,CAAC;QACL,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,KAA0B;YACnC,OAAO,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC7B,CAAC;QACD,KAAK,CAAC,MAAM,CAAC,KAA0B;YACrC,MAAM,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC9B,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The base every public asset URL this module produces is built on — D-223.
3
+ *
4
+ * **`assets_library` resolves the public API origin itself and every URL it
5
+ * produces is absolute.** One question used to have five answers in this tree:
6
+ * `absolutizePublicUrl` at two composition-root sites reading
7
+ * `BACKEND_PUBLIC_URL` first, `configuredPublicApiBaseUrl` plus a hand-written
8
+ * join at a third reading `PUBLIC_API_BASE_URL` first and silently skipping
9
+ * signed URLs, nothing at all at a fourth, and a helper in each of the two
10
+ * frontends. Every one of them existed to compensate for a setting documented
11
+ * as *"leave blank to use the request host"* — correct only for a browser on
12
+ * the API host, and wrong for an e-mail, a push payload, a partner's feed
13
+ * reader and a storefront on another origin. The module that builds the URL is
14
+ * the only party that knows which adapter produced it and whether it is signed,
15
+ * so it is the one that decides.
16
+ *
17
+ * **The fallback lives in the adapter's construction and not in the setting's
18
+ * default** (D-223, rejected alternative three): a setting default cannot read
19
+ * the environment at manifest-declaration time, and an operator who set the
20
+ * value explicitly must keep winning.
21
+ */
22
+ /**
23
+ * Make one asset URL absolute against `apiOrigin`.
24
+ *
25
+ * Everything after the path travels untouched, which is the property the signed
26
+ * local-FS form depends on: `?token=…&exp=…` is validated over the asset id and
27
+ * the expiry, so a rebase that rewrote the query would produce a 403 rather
28
+ * than an image. An empty origin returns the URL unchanged — there is nothing
29
+ * to build on, and a wrong-but-plausible origin is worse than a relative URL.
30
+ */
31
+ export declare function absolutizeAssetUrl(url: string, apiOrigin: string): string;
32
+ /**
33
+ * The public URL base an adapter serves from, in the one precedence order every
34
+ * adapter in this module uses:
35
+ *
36
+ * 1. the operator's configured base, when it names its own origin — it wins,
37
+ * always, which is D-223's third rejected alternative stated as code;
38
+ * 2. the operator's configured base, when it is a path — rebased onto the
39
+ * resolved API origin, so a configured value cannot make the result
40
+ * relative either;
41
+ * 3. blank — the caller's own default, which for the cloud adapters is the
42
+ * bucket's absolute origin and for local-FS is the API origin itself.
43
+ *
44
+ * A blank `assets.s3.public_base_url` therefore keeps resolving to the bucket
45
+ * rather than to this API: substituting the API origin there would point every
46
+ * image at a host that does not serve the bytes. D-223's invariant is that the
47
+ * URL is absolute, and the cloud adapters' own defaults already are.
48
+ */
49
+ export declare function resolvePublicUrlBase(configured: string | undefined, apiOrigin: string): string;
50
+ //# sourceMappingURL=public-url-base.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-url-base.d.ts","sourceRoot":"","sources":["../../../../src/backend/services/storage/public-url-base.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAYH;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAKzE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,SAAS,EAAE,MAAM,GAChB,MAAM,CAIR"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The base every public asset URL this module produces is built on — D-223.
3
+ *
4
+ * **`assets_library` resolves the public API origin itself and every URL it
5
+ * produces is absolute.** One question used to have five answers in this tree:
6
+ * `absolutizePublicUrl` at two composition-root sites reading
7
+ * `BACKEND_PUBLIC_URL` first, `configuredPublicApiBaseUrl` plus a hand-written
8
+ * join at a third reading `PUBLIC_API_BASE_URL` first and silently skipping
9
+ * signed URLs, nothing at all at a fourth, and a helper in each of the two
10
+ * frontends. Every one of them existed to compensate for a setting documented
11
+ * as *"leave blank to use the request host"* — correct only for a browser on
12
+ * the API host, and wrong for an e-mail, a push payload, a partner's feed
13
+ * reader and a storefront on another origin. The module that builds the URL is
14
+ * the only party that knows which adapter produced it and whether it is signed,
15
+ * so it is the one that decides.
16
+ *
17
+ * **The fallback lives in the adapter's construction and not in the setting's
18
+ * default** (D-223, rejected alternative three): a setting default cannot read
19
+ * the environment at manifest-declaration time, and an operator who set the
20
+ * value explicitly must keep winning.
21
+ */
22
+ /** A URL that already names its own origin — a scheme, or protocol-relative. */
23
+ function namesItsOwnOrigin(url) {
24
+ return url.startsWith('//') || /^[a-z][a-z\d+.-]*:/i.test(url);
25
+ }
26
+ /** Drop the trailing slashes every caller here concatenates a path onto. */
27
+ function withoutTrailingSlash(value) {
28
+ return value.replace(/\/+$/, '');
29
+ }
30
+ /**
31
+ * Make one asset URL absolute against `apiOrigin`.
32
+ *
33
+ * Everything after the path travels untouched, which is the property the signed
34
+ * local-FS form depends on: `?token=…&exp=…` is validated over the asset id and
35
+ * the expiry, so a rebase that rewrote the query would produce a 403 rather
36
+ * than an image. An empty origin returns the URL unchanged — there is nothing
37
+ * to build on, and a wrong-but-plausible origin is worse than a relative URL.
38
+ */
39
+ export function absolutizeAssetUrl(url, apiOrigin) {
40
+ if (url === '' || namesItsOwnOrigin(url))
41
+ return url;
42
+ const origin = withoutTrailingSlash(apiOrigin.trim());
43
+ if (origin === '')
44
+ return url;
45
+ return url.startsWith('/') ? `${origin}${url}` : `${origin}/${url}`;
46
+ }
47
+ /**
48
+ * The public URL base an adapter serves from, in the one precedence order every
49
+ * adapter in this module uses:
50
+ *
51
+ * 1. the operator's configured base, when it names its own origin — it wins,
52
+ * always, which is D-223's third rejected alternative stated as code;
53
+ * 2. the operator's configured base, when it is a path — rebased onto the
54
+ * resolved API origin, so a configured value cannot make the result
55
+ * relative either;
56
+ * 3. blank — the caller's own default, which for the cloud adapters is the
57
+ * bucket's absolute origin and for local-FS is the API origin itself.
58
+ *
59
+ * A blank `assets.s3.public_base_url` therefore keeps resolving to the bucket
60
+ * rather than to this API: substituting the API origin there would point every
61
+ * image at a host that does not serve the bytes. D-223's invariant is that the
62
+ * URL is absolute, and the cloud adapters' own defaults already are.
63
+ */
64
+ export function resolvePublicUrlBase(configured, apiOrigin) {
65
+ const trimmed = (configured ?? '').trim();
66
+ if (trimmed === '')
67
+ return withoutTrailingSlash(apiOrigin.trim());
68
+ return withoutTrailingSlash(absolutizeAssetUrl(trimmed, apiOrigin));
69
+ }
70
+ //# sourceMappingURL=public-url-base.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-url-base.js","sourceRoot":"","sources":["../../../../src/backend/services/storage/public-url-base.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,gFAAgF;AAChF,SAAS,iBAAiB,CAAC,GAAW;IACpC,OAAO,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,qBAAqB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACjE,CAAC;AAED,4EAA4E;AAC5E,SAAS,oBAAoB,CAAC,KAAa;IACzC,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AACnC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAW,EAAE,SAAiB;IAC/D,IAAI,GAAG,KAAK,EAAE,IAAI,iBAAiB,CAAC,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC;IACrD,MAAM,MAAM,GAAG,oBAAoB,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;IACtD,IAAI,MAAM,KAAK,EAAE;QAAE,OAAO,GAAG,CAAC;IAC9B,OAAO,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,GAAG,EAAE,CAAC;AACtE,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,oBAAoB,CAClC,UAA8B,EAC9B,SAAiB;IAEjB,MAAM,OAAO,GAAG,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC1C,IAAI,OAAO,KAAK,EAAE;QAAE,OAAO,oBAAoB,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC;IAClE,OAAO,oBAAoB,CAAC,kBAAkB,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;AACtE,CAAC"}
@@ -0,0 +1,50 @@
1
+ import type { StorageAdapter, StorageAdapterSelfCheck, StorageNewLocatorInput, StoragePutInput, StorageResolveUrlInput, StorageResolveUrlOutput } from './storage-adapter.js';
2
+ export interface S3AdapterConfig {
3
+ bucket: string;
4
+ region: string;
5
+ accessKeyId: string;
6
+ secretAccessKey: string;
7
+ endpoint?: string;
8
+ prefix?: string;
9
+ /**
10
+ * `assets.s3.public_base_url` — a CDN or a custom domain in front of the
11
+ * bucket. Blank means "serve from the bucket's own origin", which is already
12
+ * absolute; see {@link S3AdapterConfig.publicApiBaseUrl}.
13
+ */
14
+ publicBaseUrl?: string;
15
+ /**
16
+ * This deployment's resolved public API origin — D-223's fallback base.
17
+ *
18
+ * It is consulted here for one case: a `publicBaseUrl` the operator wrote as
19
+ * a **path** (`/media`), which would otherwise make this adapter produce a
20
+ * host-relative URL. A *blank* one keeps resolving to the bucket, because
21
+ * pointing an S3 object at this API's origin would name a host that does not
22
+ * serve those bytes — D-223's invariant is that the URL is absolute, and the
23
+ * bucket URL already is.
24
+ */
25
+ publicApiBaseUrl: string;
26
+ privateUrlTtlSec: number;
27
+ }
28
+ export declare class S3StorageAdapter implements StorageAdapter {
29
+ private readonly cfg;
30
+ readonly code: "s3";
31
+ private readonly client;
32
+ /** The absolute base public object URLs are built on — decided once (D-223). */
33
+ private readonly publicBase;
34
+ constructor(cfg: S3AdapterConfig);
35
+ selfCheck(): Promise<StorageAdapterSelfCheck>;
36
+ newLocator(input: StorageNewLocatorInput): string;
37
+ put(input: StoragePutInput): Promise<void>;
38
+ resolveUrl(input: StorageResolveUrlInput): Promise<StorageResolveUrlOutput>;
39
+ open(input: {
40
+ locator: string;
41
+ }): Promise<NodeJS.ReadableStream>;
42
+ delete(input: {
43
+ locator: string;
44
+ }): Promise<void>;
45
+ setVisibility(input: {
46
+ locator: string;
47
+ visibility: 'public' | 'private';
48
+ }): Promise<void>;
49
+ }
50
+ //# sourceMappingURL=s3-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"s3-adapter.d.ts","sourceRoot":"","sources":["../../../../src/backend/services/storage/s3-adapter.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EACV,cAAc,EACd,uBAAuB,EACvB,sBAAsB,EACtB,eAAe,EACf,sBAAsB,EACtB,uBAAuB,EACxB,MAAM,sBAAsB,CAAC;AAS9B,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;;;OASG;IACH,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,qBAAa,gBAAiB,YAAW,cAAc;IAMzC,OAAO,CAAC,QAAQ,CAAC,GAAG;IALhC,QAAQ,CAAC,IAAI,EAAG,IAAI,CAAU;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;IAClC,gFAAgF;IAChF,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;gBAEP,GAAG,EAAE,eAAe;IAwB3C,SAAS,IAAI,OAAO,CAAC,uBAAuB,CAAC;IAUnD,UAAU,CAAC,KAAK,EAAE,sBAAsB,GAAG,MAAM;IAM3C,GAAG,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAqB1C,UAAU,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAgB3E,IAAI,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,cAAc,CAAC;IAUhE,MAAM,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAajD,aAAa,CAAC,KAAK,EAAE;QACzB,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,QAAQ,GAAG,SAAS,CAAC;KAClC,GAAG,OAAO,CAAC,IAAI,CAAC;CAgBlB"}