@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.
- package/LICENSE +21 -0
- package/README.md +61 -2
- package/dist/admin/api/assets-library-client.d.ts +29 -0
- package/dist/admin/api/assets-library-client.d.ts.map +1 -0
- package/dist/admin/api/assets-library-client.js +70 -0
- package/dist/admin/api/assets-library-client.js.map +1 -0
- package/dist/admin/components/AssetDetailDrawer.d.ts +8 -0
- package/dist/admin/components/AssetDetailDrawer.d.ts.map +1 -0
- package/dist/admin/components/AssetDetailDrawer.js +96 -0
- package/dist/admin/components/AssetDetailDrawer.js.map +1 -0
- package/dist/admin/components/FolderTree.d.ts +13 -0
- package/dist/admin/components/FolderTree.d.ts.map +1 -0
- package/dist/admin/components/FolderTree.js +71 -0
- package/dist/admin/components/FolderTree.js.map +1 -0
- package/dist/admin/index.d.ts +44 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +46 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/LibraryPage.d.ts +9 -0
- package/dist/admin/pages/LibraryPage.d.ts.map +1 -0
- package/dist/admin/pages/LibraryPage.js +163 -0
- package/dist/admin/pages/LibraryPage.js.map +1 -0
- package/dist/backend/entities/asset-folder.entity.d.ts +16 -0
- package/dist/backend/entities/asset-folder.entity.d.ts.map +1 -0
- package/dist/backend/entities/asset-folder.entity.js +57 -0
- package/dist/backend/entities/asset-folder.entity.js.map +1 -0
- package/dist/backend/entities/asset.entity.d.ts +38 -0
- package/dist/backend/entities/asset.entity.d.ts.map +1 -0
- package/dist/backend/entities/asset.entity.js +128 -0
- package/dist/backend/entities/asset.entity.js.map +1 -0
- package/dist/backend/index.d.ts +57 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +150 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/jobs/hard-delete-asset.job.d.ts +18 -0
- package/dist/backend/jobs/hard-delete-asset.job.d.ts.map +1 -0
- package/dist/backend/jobs/hard-delete-asset.job.js +91 -0
- package/dist/backend/jobs/hard-delete-asset.job.js.map +1 -0
- package/dist/backend/plugin.d.ts +47 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +96 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +15 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +189 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.public.d.ts +14 -0
- package/dist/backend/routes.public.d.ts.map +1 -0
- package/dist/backend/routes.public.js +99 -0
- package/dist/backend/routes.public.js.map +1 -0
- package/dist/backend/services/asset-read-port.d.ts +103 -0
- package/dist/backend/services/asset-read-port.d.ts.map +1 -0
- package/dist/backend/services/asset-read-port.js +167 -0
- package/dist/backend/services/asset-read-port.js.map +1 -0
- package/dist/backend/services/assets-library.service.d.ts +76 -0
- package/dist/backend/services/assets-library.service.d.ts.map +1 -0
- package/dist/backend/services/assets-library.service.js +261 -0
- package/dist/backend/services/assets-library.service.js.map +1 -0
- package/dist/backend/services/cms-body-resolver.d.ts +6 -0
- package/dist/backend/services/cms-body-resolver.d.ts.map +1 -0
- package/dist/backend/services/cms-body-resolver.js +44 -0
- package/dist/backend/services/cms-body-resolver.js.map +1 -0
- package/dist/backend/services/folders.service.d.ts +29 -0
- package/dist/backend/services/folders.service.d.ts.map +1 -0
- package/dist/backend/services/folders.service.js +231 -0
- package/dist/backend/services/folders.service.js.map +1 -0
- package/dist/backend/services/hmac.d.ts +28 -0
- package/dist/backend/services/hmac.d.ts.map +1 -0
- package/dist/backend/services/hmac.js +50 -0
- package/dist/backend/services/hmac.js.map +1 -0
- package/dist/backend/services/reference-registry.d.ts +36 -0
- package/dist/backend/services/reference-registry.d.ts.map +1 -0
- package/dist/backend/services/reference-registry.js +63 -0
- package/dist/backend/services/reference-registry.js.map +1 -0
- package/dist/backend/services/storage/adapter-registry.d.ts +77 -0
- package/dist/backend/services/storage/adapter-registry.d.ts.map +1 -0
- package/dist/backend/services/storage/adapter-registry.js +95 -0
- package/dist/backend/services/storage/adapter-registry.js.map +1 -0
- package/dist/backend/services/storage/errors.d.ts +18 -0
- package/dist/backend/services/storage/errors.d.ts.map +1 -0
- package/dist/backend/services/storage/errors.js +30 -0
- package/dist/backend/services/storage/errors.js.map +1 -0
- package/dist/backend/services/storage/gcs-adapter.d.ts +42 -0
- package/dist/backend/services/storage/gcs-adapter.d.ts.map +1 -0
- package/dist/backend/services/storage/gcs-adapter.js +106 -0
- package/dist/backend/services/storage/gcs-adapter.js.map +1 -0
- package/dist/backend/services/storage/legacy-resolver.d.ts +15 -0
- package/dist/backend/services/storage/legacy-resolver.d.ts.map +1 -0
- package/dist/backend/services/storage/legacy-resolver.js +32 -0
- package/dist/backend/services/storage/legacy-resolver.js.map +1 -0
- package/dist/backend/services/storage/local-fs-adapter.d.ts +59 -0
- package/dist/backend/services/storage/local-fs-adapter.d.ts.map +1 -0
- package/dist/backend/services/storage/local-fs-adapter.js +153 -0
- package/dist/backend/services/storage/local-fs-adapter.js.map +1 -0
- package/dist/backend/services/storage/locator.d.ts +5 -0
- package/dist/backend/services/storage/locator.d.ts.map +1 -0
- package/dist/backend/services/storage/locator.js +16 -0
- package/dist/backend/services/storage/locator.js.map +1 -0
- package/dist/backend/services/storage/object-storage-port.d.ts +46 -0
- package/dist/backend/services/storage/object-storage-port.d.ts.map +1 -0
- package/dist/backend/services/storage/object-storage-port.js +78 -0
- package/dist/backend/services/storage/object-storage-port.js.map +1 -0
- package/dist/backend/services/storage/public-url-base.d.ts +50 -0
- package/dist/backend/services/storage/public-url-base.d.ts.map +1 -0
- package/dist/backend/services/storage/public-url-base.js +70 -0
- package/dist/backend/services/storage/public-url-base.js.map +1 -0
- package/dist/backend/services/storage/s3-adapter.d.ts +50 -0
- package/dist/backend/services/storage/s3-adapter.d.ts.map +1 -0
- package/dist/backend/services/storage/s3-adapter.js +110 -0
- package/dist/backend/services/storage/s3-adapter.js.map +1 -0
- package/dist/backend/services/storage/settings-view.d.ts +4 -0
- package/dist/backend/services/storage/settings-view.d.ts.map +1 -0
- package/dist/backend/services/storage/settings-view.js +73 -0
- package/dist/backend/services/storage/settings-view.js.map +1 -0
- package/dist/backend/services/storage/storage-adapter.d.ts +62 -0
- package/dist/backend/services/storage/storage-adapter.d.ts.map +1 -0
- package/dist/backend/services/storage/storage-adapter.js +10 -0
- package/dist/backend/services/storage/storage-adapter.js.map +1 -0
- package/dist/backend/services/upload-pipeline.d.ts +41 -0
- package/dist/backend/services/upload-pipeline.d.ts.map +1 -0
- package/dist/backend/services/upload-pipeline.js +231 -0
- package/dist/backend/services/upload-pipeline.js.map +1 -0
- package/dist/manifest.d.ts +185 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +229 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260505T102206_assets_library_init.d.ts +35 -0
- package/dist/migrations/20260505T102206_assets_library_init.d.ts.map +1 -0
- package/dist/migrations/20260505T102206_assets_library_init.js +132 -0
- package/dist/migrations/20260505T102206_assets_library_init.js.map +1 -0
- package/dist/migrations/index.d.ts +27 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +29 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/assets-library/index.md +125 -0
- package/i18n/en.json +54 -0
- package/i18n/pl.json +54 -0
- package/package.json +99 -3
- 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":"AAAA,yEAAyE;AACzE,2EAA2E;AAC3E,+CAA+C;AAC/C,EAAE;AACF,qEAAqE;AACrE,gFAAgF;AAChF,kBAAkB;AAClB,EAAE;AACF,4EAA4E;AAC5E,+EAA+E;AAC/E,+EAA+E;AAC/E,+EAA+E;AAC/E,yDAAyD;AAEzD,OAAO,EAAE,gBAAgB,EAAmB,MAAM,SAAS,CAAC;AAC5D,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,IAAI,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACjG,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAU5C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,aAAa,CAAC;AA6BrB,MAAM,OAAO,qBAAqB;IAUH;IATpB,IAAI,GAAG,OAAgB,CAAC;IAEjC;;;;OAIG;IACc,UAAU,CAAS;IAEpC,YAA6B,IAA2B;QAA3B,SAAI,GAAJ,IAAI,CAAuB;QACtD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YAC9B,mEAAmE;YACnE,kDAAkD;YAClD,IAAI,CAAC,IAAI,GAAG,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACtE,CAAC;QACD,IAAI,CAAC,UAAU,GAAG,oBAAoB,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,gBAAgB,CAAC,CAAC;IACpF,CAAC;IAED,KAAK,CAAC,SAAS;QACb,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;YACrE,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACxC,IAAI,CAAC,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;gBACrB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,iCAAiC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;YACrF,CAAC;YACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;QACtB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,6DAA6D;YAC7D,MAAM,MAAM,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YAC1D,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAC/B,CAAC;IACH,CAAC;IAED,UAAU,CAAC,KAA6B;QACtC,OAAO,cAAc,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,KAAsB;QAC9B,IAAI,CAAC,KAAK,CAAC,OAAO;YAAE,MAAM,IAAI,mBAAmB,CAAC,0CAA0C,CAAC,CAAC;QAC9F,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC3C,MAAM,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/C,MAAM,GAAG,GAAG,GAAG,GAAG,QAAQ,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;QACtD,IAAI,CAAC;YACH,MAAM,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC;QACvD,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,wCAAwC;YACxC,IAAI,CAAC;gBAAC,MAAM,MAAM,CAAC,GAAG,CAAC,CAAC;YAAC,CAAC;YAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;YACjD,MAAM,IAAI,uBAAuB,CAC/B,6CAA6C,KAAK,CAAC,OAAO,EAAE,EAC5D,CAAC,CACF,CAAC;QACJ,CAAC;QACD,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACzB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,CAAC;gBAAC,MAAM,MAAM,CAAC,GAAG,CAAC,CAAC;YAAC,CAAC;YAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;YACjD,MAAM,IAAI,uBAAuB,CAC/B,iEAAiE,KAAK,CAAC,OAAO,EAAE,EAChF,CAAC,CACF,CAAC;QACJ,CAAC;IACH,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,KAA6B;QAC5C,MAAM,OAAO,GAAG,uBAAuB,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACvD,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC;QAC7B,IAAI,KAAK,CAAC,UAAU,KAAK,QAAQ,EAAE,CAAC;YAClC,OAAO,EAAE,GAAG,EAAE,GAAG,IAAI,gBAAgB,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;QACpE,CAAC;QACD,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC;QACvD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;YACvC,MAAM,IAAI,kBAAkB,CAAC,iDAAiD,GAAG,EAAE,CAAC,CAAC;QACvF,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC;QAChD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,GAAG,CAAC;QACrD,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;QACtD,OAAO;YACL,GAAG,EAAE,GAAG,IAAI,gBAAgB,OAAO,UAAU,KAAK,QAAQ,GAAG,EAAE;YAC/D,SAAS,EAAE,IAAI,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC;SAChC,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,KAA0B;QACnC,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC3C,IAAI,CAAC;YACH,mEAAmE;YACnE,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC;QAClB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,IAAI,uBAAuB,CAAC,4CAA4C,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;QAC1F,CAAC;QACD,OAAO,gBAAgB,CAAC,GAAG,CAAC,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,KAA0B;QACrC,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC3C,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,GAAG,CAAC,CAAC;QACpB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,oEAAoE;YACpE,mEAAmE;YACnE,sEAAsE;YACtE,MAAM,IAAI,uBAAuB,CAC/B,+CAA+C,GAAG,EAAE,EACpD,CAAC,CACF,CAAC;QACJ,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,aAAa;QACjB,WAAW;IACb,CAAC;IAEO,UAAU,CAAC,OAAe;QAChC,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAC5B,MAAM,IAAI,mBAAmB,CAC3B,yDAAyD,OAAO,GAAG,CACpE,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC1C,CAAC;CACF;AAED,MAAM,UAAU,uBAAuB,CAAC,OAAe;IACrD,sEAAsE;IACtE,4EAA4E;IAC5E,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;IAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACjC,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACtD,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClB,MAAM,IAAI,mBAAmB,CAC3B,0DAA0D,OAAO,GAAG,CACrE,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC"}
|
|
@@ -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"}
|