@revealui/core 0.7.0 → 0.9.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/README.md +50 -6
- package/dist/collections/operations/delete.d.ts +2 -4
- package/dist/collections/operations/delete.d.ts.map +1 -1
- package/dist/collections/operations/delete.js +21 -4
- package/dist/collections/operations/findById.d.ts.map +1 -1
- package/dist/collections/operations/findById.js +18 -3
- package/dist/collections/operations/sqlAdapter.d.ts.map +1 -1
- package/dist/collections/operations/sqlAdapter.js +7 -1
- package/dist/collections/operations/update.d.ts.map +1 -1
- package/dist/collections/operations/update.js +21 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/license.d.ts +20 -0
- package/dist/license.d.ts.map +1 -1
- package/dist/license.js +30 -0
- package/dist/observability/health-check.d.ts.map +1 -1
- package/dist/observability/health-check.js +8 -1
- package/dist/observability/metrics.d.ts +6 -16
- package/dist/observability/metrics.d.ts.map +1 -1
- package/dist/observability/metrics.js +5 -10
- package/dist/richtext/exports/server/rsc.d.ts +1 -1
- package/dist/richtext/exports/server/rsc.d.ts.map +1 -1
- package/dist/richtext/exports/server/rsc.js +6 -1
- package/dist/security/index.d.ts +9 -2
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +9 -2
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/storage/_helpers.d.ts +14 -0
- package/dist/storage/_helpers.d.ts.map +1 -0
- package/dist/storage/_helpers.js +40 -0
- package/dist/storage/index.d.ts +18 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +46 -2
- package/dist/storage/mock.d.ts +39 -0
- package/dist/storage/mock.d.ts.map +1 -0
- package/dist/storage/mock.js +91 -0
- package/dist/storage/object-storage.d.ts +41 -0
- package/dist/storage/object-storage.d.ts.map +1 -0
- package/dist/storage/{vercel-blob.js → object-storage.js} +38 -18
- package/dist/storage/r2.d.ts +13 -0
- package/dist/storage/r2.d.ts.map +1 -0
- package/dist/storage/r2.js +117 -0
- package/dist/storage/types.d.ts +110 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +16 -0
- package/dist/storage/vercel-blob-provider.d.ts +28 -0
- package/dist/storage/vercel-blob-provider.d.ts.map +1 -0
- package/dist/storage/vercel-blob-provider.js +88 -0
- package/package.json +8 -7
- package/dist/storage/vercel-blob.d.ts +0 -17
- package/dist/storage/vercel-blob.d.ts.map +0 -1
package/dist/security/index.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Security & Compliance -
|
|
3
|
-
*
|
|
2
|
+
* Security & Compliance - full server-side surface.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the client-safe `@revealui/security` barrel AND the server-only
|
|
5
|
+
* `@revealui/security/server` entry (auth/gdpr/audit/ssrf — node: built-ins),
|
|
6
|
+
* so server code can pull any security symbol from one place. Because it
|
|
7
|
+
* re-exports `./server`, THIS entry is server-only: do not import
|
|
8
|
+
* `@revealui/core/security` from client/RSC code (use `@revealui/security` or
|
|
9
|
+
* `@revealui/security/sanitize` there).
|
|
4
10
|
*/
|
|
5
11
|
export * from '@revealui/security';
|
|
12
|
+
export * from '@revealui/security/server';
|
package/dist/server/index.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
export { createRESTHandlers, handleRESTRequest } from '../api/rest.js';
|
|
15
15
|
export { universalPostgresAdapter } from '../database/universal-postgres.js';
|
|
16
|
-
export {
|
|
16
|
+
export { objectStorage } from '../storage/object-storage.js';
|
|
17
17
|
export type { LogContext, Logger, LogLevel } from '../utils/logger-server.js';
|
|
18
18
|
export { createLogger, logger } from '../utils/logger-server.js';
|
|
19
19
|
export * from '../utils/request-context.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAEvE,OAAO,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAEvE,OAAO,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAE9E,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AAEjE,cAAc,6BAA6B,CAAC;AAQ5C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/server/index.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
export { createRESTHandlers, handleRESTRequest } from '../api/rest.js';
|
|
16
16
|
// Server-only database/storage
|
|
17
17
|
export { universalPostgresAdapter } from '../database/universal-postgres.js';
|
|
18
|
-
export {
|
|
18
|
+
export { objectStorage } from '../storage/object-storage.js';
|
|
19
19
|
// Server-only logger with request context
|
|
20
20
|
export { createLogger, logger } from '../utils/logger-server.js';
|
|
21
21
|
// Request context utilities (server-only - uses async_hooks)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal helpers shared across storage providers.
|
|
3
|
+
*
|
|
4
|
+
* Underscore prefix denotes private-to-storage/; not re-exported from
|
|
5
|
+
* packages/core/src/storage/index.ts.
|
|
6
|
+
*
|
|
7
|
+
* GAP-208 Phase 2a (2026-05-18).
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Normalize the four accepted body types into a Uint8Array so providers
|
|
11
|
+
* can compute size + hand a known buffer to their underlying SDKs.
|
|
12
|
+
*/
|
|
13
|
+
export declare function toUint8Array(data: Blob | ArrayBuffer | Uint8Array | ReadableStream<Uint8Array>): Promise<Uint8Array>;
|
|
14
|
+
//# sourceMappingURL=_helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"_helpers.d.ts","sourceRoot":"","sources":["../../src/storage/_helpers.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;GAGG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,IAAI,GAAG,WAAW,GAAG,UAAU,GAAG,cAAc,CAAC,UAAU,CAAC,GACjE,OAAO,CAAC,UAAU,CAAC,CA0BrB"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal helpers shared across storage providers.
|
|
3
|
+
*
|
|
4
|
+
* Underscore prefix denotes private-to-storage/; not re-exported from
|
|
5
|
+
* packages/core/src/storage/index.ts.
|
|
6
|
+
*
|
|
7
|
+
* GAP-208 Phase 2a (2026-05-18).
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Normalize the four accepted body types into a Uint8Array so providers
|
|
11
|
+
* can compute size + hand a known buffer to their underlying SDKs.
|
|
12
|
+
*/
|
|
13
|
+
export async function toUint8Array(data) {
|
|
14
|
+
if (data instanceof Uint8Array) {
|
|
15
|
+
return data;
|
|
16
|
+
}
|
|
17
|
+
if (data instanceof ArrayBuffer) {
|
|
18
|
+
return new Uint8Array(data);
|
|
19
|
+
}
|
|
20
|
+
if (data instanceof Blob) {
|
|
21
|
+
return new Uint8Array(await data.arrayBuffer());
|
|
22
|
+
}
|
|
23
|
+
const reader = data.getReader();
|
|
24
|
+
const chunks = [];
|
|
25
|
+
let total = 0;
|
|
26
|
+
while (true) {
|
|
27
|
+
const { done, value } = await reader.read();
|
|
28
|
+
if (done)
|
|
29
|
+
break;
|
|
30
|
+
chunks.push(value);
|
|
31
|
+
total += value.byteLength;
|
|
32
|
+
}
|
|
33
|
+
const merged = new Uint8Array(total);
|
|
34
|
+
let offset = 0;
|
|
35
|
+
for (const chunk of chunks) {
|
|
36
|
+
merged.set(chunk, offset);
|
|
37
|
+
offset += chunk.byteLength;
|
|
38
|
+
}
|
|
39
|
+
return merged;
|
|
40
|
+
}
|
package/dist/storage/index.d.ts
CHANGED
|
@@ -1,2 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
import type { StorageConfig, StorageProvider } from './types.js';
|
|
2
|
+
export { createMockProvider } from './mock.js';
|
|
3
|
+
export type { ObjectStorageConfig } from './object-storage.js';
|
|
4
|
+
export { objectStorage } from './object-storage.js';
|
|
5
|
+
export { createR2Provider } from './r2.js';
|
|
6
|
+
export type { ListItem, ListOptions, ListResult, PutOptions, PutResult, R2Config, StorageConfig, StorageProvider, VercelBlobConfig, } from './types.js';
|
|
7
|
+
export { createVercelBlobProvider } from './vercel-blob-provider.js';
|
|
8
|
+
/**
|
|
9
|
+
* Construct a StorageProvider from a tagged config.
|
|
10
|
+
*
|
|
11
|
+
* Per `feedback_pluggable_provider_pattern`: explicit "no-X" opt-in — unknown
|
|
12
|
+
* tags throw with a clear message; no silent fallback.
|
|
13
|
+
*
|
|
14
|
+
* Supported tags: 'r2' (canonical), 'vercel-blob' (legacy migration-window
|
|
15
|
+
* fallback), and 'mock' (tests). The provider-agnostic `objectStorage` Payload
|
|
16
|
+
* plugin (object-storage.ts) adapts any of these to apps/admin's upload path.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createStorage(config: StorageConfig): StorageProvider;
|
|
2
19
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/storage/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/storage/index.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAGjE,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAK/D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAC3C,YAAY,EACV,QAAQ,EACR,WAAW,EACX,UAAU,EACV,UAAU,EACV,SAAS,EACT,QAAQ,EACR,aAAa,EACb,eAAe,EACf,gBAAgB,GACjB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAErE;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,eAAe,CAgBpE"}
|
package/dist/storage/index.js
CHANGED
|
@@ -1,2 +1,46 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
1
|
+
// Storage abstraction (StorageProvider interface + per-provider configs + factory).
|
|
2
|
+
// GAP-208 — introduced 2026-05-18 alongside Vercel Blob → Cloudflare R2 swap.
|
|
3
|
+
//
|
|
4
|
+
// Phase 1 (#959): types.ts + index.ts type re-exports.
|
|
5
|
+
// Phase 2a (#959 follow-up): r2 + mock providers + createStorage factory.
|
|
6
|
+
// Phase 2b (this PR): vercel-blob StorageProvider + apps/server media-route
|
|
7
|
+
// cutover. R2 is canonical; Vercel Blob is the migration-window fallback.
|
|
8
|
+
import { createMockProvider } from './mock.js';
|
|
9
|
+
import { createR2Provider } from './r2.js';
|
|
10
|
+
import { createVercelBlobProvider } from './vercel-blob-provider.js';
|
|
11
|
+
export { createMockProvider } from './mock.js';
|
|
12
|
+
// Provider-agnostic Payload-style upload plugin — adapts any StorageProvider
|
|
13
|
+
// (R2 canonical, Vercel Blob fallback, mock) to the engine's collection upload
|
|
14
|
+
// adapter. Used by apps/admin/revealui.config.ts. GAP-208 Phase 4 replaced the
|
|
15
|
+
// provider-specific `vercelBlobStorage` plugin with this.
|
|
16
|
+
export { objectStorage } from './object-storage.js';
|
|
17
|
+
// Provider factories — exported so callers can construct a provider directly
|
|
18
|
+
// when they already know which one they want (skipping the discriminated-union
|
|
19
|
+
// factory below).
|
|
20
|
+
export { createR2Provider } from './r2.js';
|
|
21
|
+
export { createVercelBlobProvider } from './vercel-blob-provider.js';
|
|
22
|
+
/**
|
|
23
|
+
* Construct a StorageProvider from a tagged config.
|
|
24
|
+
*
|
|
25
|
+
* Per `feedback_pluggable_provider_pattern`: explicit "no-X" opt-in — unknown
|
|
26
|
+
* tags throw with a clear message; no silent fallback.
|
|
27
|
+
*
|
|
28
|
+
* Supported tags: 'r2' (canonical), 'vercel-blob' (legacy migration-window
|
|
29
|
+
* fallback), and 'mock' (tests). The provider-agnostic `objectStorage` Payload
|
|
30
|
+
* plugin (object-storage.ts) adapts any of these to apps/admin's upload path.
|
|
31
|
+
*/
|
|
32
|
+
export function createStorage(config) {
|
|
33
|
+
switch (config.provider) {
|
|
34
|
+
case 'r2':
|
|
35
|
+
return createR2Provider(config.r2);
|
|
36
|
+
case 'vercel-blob':
|
|
37
|
+
return createVercelBlobProvider(config.vercelBlob);
|
|
38
|
+
case 'mock':
|
|
39
|
+
return createMockProvider();
|
|
40
|
+
default: {
|
|
41
|
+
const _exhaustive = config;
|
|
42
|
+
throw new Error(`createStorage: unknown provider tag. Got ${JSON.stringify(_exhaustive)}. ` +
|
|
43
|
+
"Valid tags: 'r2', 'vercel-blob', 'mock'.");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory mock implementation of StorageProvider for tests.
|
|
3
|
+
*
|
|
4
|
+
* Per `feedback_pluggable_provider_pattern`: every provider abstraction ships
|
|
5
|
+
* a mock so consumer tests don't need network access or real cloud creds.
|
|
6
|
+
*
|
|
7
|
+
* Stores objects in a Map keyed by storage key. URLs use a synthetic
|
|
8
|
+
* `mock://` scheme so callers can distinguish mock results from real ones
|
|
9
|
+
* without parsing host names.
|
|
10
|
+
*
|
|
11
|
+
* GAP-208 Phase 2a (2026-05-18).
|
|
12
|
+
*/
|
|
13
|
+
import type { ListOptions, ListResult, PutOptions, PutResult, StorageProvider } from './types.js';
|
|
14
|
+
interface MockEntry {
|
|
15
|
+
body: Uint8Array;
|
|
16
|
+
contentType: string;
|
|
17
|
+
cacheControl?: string;
|
|
18
|
+
metadata?: Record<string, string>;
|
|
19
|
+
uploadedAt: Date;
|
|
20
|
+
}
|
|
21
|
+
declare class MockProvider implements StorageProvider {
|
|
22
|
+
readonly provider = "mock";
|
|
23
|
+
private readonly store;
|
|
24
|
+
put(key: string, data: Blob | ArrayBuffer | Uint8Array | ReadableStream<Uint8Array>, opts?: PutOptions): Promise<PutResult>;
|
|
25
|
+
del(keyOrUrl: string): Promise<void>;
|
|
26
|
+
list(opts?: ListOptions): Promise<ListResult>;
|
|
27
|
+
/** Test helper — read the raw bytes back. Not part of StorageProvider. */
|
|
28
|
+
read(key: string): Uint8Array | undefined;
|
|
29
|
+
/** Test helper — full entry inspection. Not part of StorageProvider. */
|
|
30
|
+
inspect(key: string): MockEntry | undefined;
|
|
31
|
+
/** Test helper — count of stored objects. Not part of StorageProvider. */
|
|
32
|
+
size(): number;
|
|
33
|
+
/** Test helper — wipe all stored objects. Not part of StorageProvider. */
|
|
34
|
+
clear(): void;
|
|
35
|
+
private extractKey;
|
|
36
|
+
}
|
|
37
|
+
export declare function createMockProvider(): MockProvider;
|
|
38
|
+
export type { MockProvider };
|
|
39
|
+
//# sourceMappingURL=mock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock.d.ts","sourceRoot":"","sources":["../../src/storage/mock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAEV,WAAW,EACX,UAAU,EACV,UAAU,EACV,SAAS,EACT,eAAe,EAChB,MAAM,YAAY,CAAC;AAMpB,UAAU,SAAS;IACjB,IAAI,EAAE,UAAU,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,UAAU,EAAE,IAAI,CAAC;CAClB;AAED,cAAM,YAAa,YAAW,eAAe;IAC3C,QAAQ,CAAC,QAAQ,UAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IAEhD,GAAG,CACP,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,IAAI,GAAG,WAAW,GAAG,UAAU,GAAG,cAAc,CAAC,UAAU,CAAC,EAClE,IAAI,CAAC,EAAE,UAAU,GAChB,OAAO,CAAC,SAAS,CAAC;IAiBf,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKpC,IAAI,CAAC,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAgCnD,0EAA0E;IAC1E,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAIzC,wEAAwE;IACxE,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAI3C,0EAA0E;IAC1E,IAAI,IAAI,MAAM;IAId,0EAA0E;IAC1E,KAAK,IAAI,IAAI;IAIb,OAAO,CAAC,UAAU;CAMnB;AAED,wBAAgB,kBAAkB,IAAI,YAAY,CAEjD;AAED,YAAY,EAAE,YAAY,EAAE,CAAC"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory mock implementation of StorageProvider for tests.
|
|
3
|
+
*
|
|
4
|
+
* Per `feedback_pluggable_provider_pattern`: every provider abstraction ships
|
|
5
|
+
* a mock so consumer tests don't need network access or real cloud creds.
|
|
6
|
+
*
|
|
7
|
+
* Stores objects in a Map keyed by storage key. URLs use a synthetic
|
|
8
|
+
* `mock://` scheme so callers can distinguish mock results from real ones
|
|
9
|
+
* without parsing host names.
|
|
10
|
+
*
|
|
11
|
+
* GAP-208 Phase 2a (2026-05-18).
|
|
12
|
+
*/
|
|
13
|
+
import { toUint8Array } from './_helpers.js';
|
|
14
|
+
const PROVIDER_TAG = 'mock';
|
|
15
|
+
const MOCK_URL_SCHEME = 'mock://storage';
|
|
16
|
+
const DEFAULT_LIST_LIMIT = 1000;
|
|
17
|
+
class MockProvider {
|
|
18
|
+
provider = PROVIDER_TAG;
|
|
19
|
+
store = new Map();
|
|
20
|
+
async put(key, data, opts) {
|
|
21
|
+
const body = await toUint8Array(data);
|
|
22
|
+
this.store.set(key, {
|
|
23
|
+
body,
|
|
24
|
+
contentType: opts?.contentType ?? 'application/octet-stream',
|
|
25
|
+
cacheControl: opts?.cacheControl,
|
|
26
|
+
metadata: opts?.metadata,
|
|
27
|
+
uploadedAt: new Date(),
|
|
28
|
+
});
|
|
29
|
+
return {
|
|
30
|
+
key,
|
|
31
|
+
url: `${MOCK_URL_SCHEME}/${key}`,
|
|
32
|
+
size: body.byteLength,
|
|
33
|
+
provider: PROVIDER_TAG,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
async del(keyOrUrl) {
|
|
37
|
+
const key = this.extractKey(keyOrUrl);
|
|
38
|
+
this.store.delete(key);
|
|
39
|
+
}
|
|
40
|
+
async list(opts) {
|
|
41
|
+
const prefix = opts?.prefix ?? '';
|
|
42
|
+
const limit = opts?.limit ?? DEFAULT_LIST_LIMIT;
|
|
43
|
+
const startAfter = opts?.cursor;
|
|
44
|
+
const allKeys = Array.from(this.store.keys())
|
|
45
|
+
.filter((key) => key.startsWith(prefix))
|
|
46
|
+
.sort();
|
|
47
|
+
const startIndex = startAfter ? allKeys.findIndex((k) => k > startAfter) : 0;
|
|
48
|
+
const effectiveStart = startIndex < 0 ? allKeys.length : startIndex;
|
|
49
|
+
const page = allKeys.slice(effectiveStart, effectiveStart + limit);
|
|
50
|
+
const items = page.map((key) => {
|
|
51
|
+
const entry = this.store.get(key);
|
|
52
|
+
if (!entry) {
|
|
53
|
+
throw new Error(`mock: store mutated during list iteration (${key})`);
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
key,
|
|
57
|
+
url: `${MOCK_URL_SCHEME}/${key}`,
|
|
58
|
+
size: entry.body.byteLength,
|
|
59
|
+
uploadedAt: entry.uploadedAt,
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
const hasMore = effectiveStart + limit < allKeys.length;
|
|
63
|
+
const nextCursor = hasMore ? page[page.length - 1] : undefined;
|
|
64
|
+
return { items, cursor: nextCursor, hasMore };
|
|
65
|
+
}
|
|
66
|
+
/** Test helper — read the raw bytes back. Not part of StorageProvider. */
|
|
67
|
+
read(key) {
|
|
68
|
+
return this.store.get(key)?.body;
|
|
69
|
+
}
|
|
70
|
+
/** Test helper — full entry inspection. Not part of StorageProvider. */
|
|
71
|
+
inspect(key) {
|
|
72
|
+
return this.store.get(key);
|
|
73
|
+
}
|
|
74
|
+
/** Test helper — count of stored objects. Not part of StorageProvider. */
|
|
75
|
+
size() {
|
|
76
|
+
return this.store.size;
|
|
77
|
+
}
|
|
78
|
+
/** Test helper — wipe all stored objects. Not part of StorageProvider. */
|
|
79
|
+
clear() {
|
|
80
|
+
this.store.clear();
|
|
81
|
+
}
|
|
82
|
+
extractKey(keyOrUrl) {
|
|
83
|
+
if (keyOrUrl.startsWith(`${MOCK_URL_SCHEME}/`)) {
|
|
84
|
+
return keyOrUrl.slice(MOCK_URL_SCHEME.length + 1);
|
|
85
|
+
}
|
|
86
|
+
return keyOrUrl;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function createMockProvider() {
|
|
90
|
+
return new MockProvider();
|
|
91
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Object-storage upload plugin (provider-agnostic).
|
|
3
|
+
*
|
|
4
|
+
* Adapts any `StorageProvider` (Cloudflare R2 — canonical — Vercel Blob, mock,
|
|
5
|
+
* or a future backend) to the RevealUI engine's collection upload-adapter
|
|
6
|
+
* interface, so `apps/admin/revealui.config.ts` stores media through the
|
|
7
|
+
* configured object-storage backend. Replaces the provider-specific
|
|
8
|
+
* `vercelBlobStorage` Payload plugin retired in GAP-208 Phase 4.
|
|
9
|
+
*
|
|
10
|
+
* The backend is resolved through a lazy `resolveProvider` thunk — invoked
|
|
11
|
+
* (and memoized) on the first upload/delete, never at config-build time — so a
|
|
12
|
+
* consumer reading validated config (e.g. `@revealui/config`'s `config.storage`)
|
|
13
|
+
* does NOT force env validation when the admin config module is imported during
|
|
14
|
+
* `next build`, tests, or boot. This mirrors `apps/server` `getMediaStorage()`,
|
|
15
|
+
* which reads `config.storage` lazily per request.
|
|
16
|
+
*
|
|
17
|
+
* WARNING: This module is server-only.
|
|
18
|
+
* Do NOT import in client-side code or edge runtime.
|
|
19
|
+
*/
|
|
20
|
+
import type { Plugin } from '../types/index.js';
|
|
21
|
+
import type { StorageProvider } from './types.js';
|
|
22
|
+
export interface ObjectStorageConfig {
|
|
23
|
+
/** When `false`, the plugin is a no-op (no upload config applied). Defaults to enabled. */
|
|
24
|
+
enabled?: boolean;
|
|
25
|
+
/** Map of collection slug → whether to attach upload handling. */
|
|
26
|
+
collections?: {
|
|
27
|
+
[key: string]: boolean;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Lazily resolves the backend the upload adapter writes through. Called once
|
|
31
|
+
* (memoized) on the first upload/delete — never at config build — so reading
|
|
32
|
+
* validated config (e.g. `@revealui/config`'s `config.storage`) does not force
|
|
33
|
+
* env validation at module load. Should throw a clear, actionable error when
|
|
34
|
+
* no backend is configured (fail-fast; never silently no-op an upload).
|
|
35
|
+
*/
|
|
36
|
+
resolveProvider: () => StorageProvider;
|
|
37
|
+
/** Key prefix for stored objects. Defaults to `'uploads'`. */
|
|
38
|
+
prefix?: string;
|
|
39
|
+
}
|
|
40
|
+
export declare function objectStorage(config: ObjectStorageConfig): Plugin;
|
|
41
|
+
//# sourceMappingURL=object-storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"object-storage.d.ts","sourceRoot":"","sources":["../../src/storage/object-storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,WAAW,mBAAmB;IAClC,2FAA2F;IAC3F,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kEAAkE;IAClE,WAAW,CAAC,EAAE;QACZ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;IACF;;;;;;OAMG;IACH,eAAe,EAAE,MAAM,eAAe,CAAC;IACvC,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,mBAAmB,GAAG,MAAM,CAiGjE"}
|
|
@@ -1,13 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Object-storage upload plugin (provider-agnostic).
|
|
3
|
+
*
|
|
4
|
+
* Adapts any `StorageProvider` (Cloudflare R2 — canonical — Vercel Blob, mock,
|
|
5
|
+
* or a future backend) to the RevealUI engine's collection upload-adapter
|
|
6
|
+
* interface, so `apps/admin/revealui.config.ts` stores media through the
|
|
7
|
+
* configured object-storage backend. Replaces the provider-specific
|
|
8
|
+
* `vercelBlobStorage` Payload plugin retired in GAP-208 Phase 4.
|
|
9
|
+
*
|
|
10
|
+
* The backend is resolved through a lazy `resolveProvider` thunk — invoked
|
|
11
|
+
* (and memoized) on the first upload/delete, never at config-build time — so a
|
|
12
|
+
* consumer reading validated config (e.g. `@revealui/config`'s `config.storage`)
|
|
13
|
+
* does NOT force env validation when the admin config module is imported during
|
|
14
|
+
* `next build`, tests, or boot. This mirrors `apps/server` `getMediaStorage()`,
|
|
15
|
+
* which reads `config.storage` lazily per request.
|
|
3
16
|
*
|
|
4
17
|
* WARNING: This module is server-only.
|
|
5
18
|
* Do NOT import in client-side code or edge runtime.
|
|
6
19
|
*/
|
|
7
|
-
import { del, put } from '@vercel/blob';
|
|
8
20
|
import { defaultLogger } from '../instance/logger.js';
|
|
9
|
-
export function
|
|
21
|
+
export function objectStorage(config) {
|
|
10
22
|
const prefix = config.prefix || 'uploads';
|
|
23
|
+
// Memoize the provider so the underlying client (S3 / Blob) is constructed
|
|
24
|
+
// once per process, not per upload — and so config is read lazily, on first
|
|
25
|
+
// use, rather than at config-build time.
|
|
26
|
+
let cachedProvider;
|
|
27
|
+
const getProvider = () => {
|
|
28
|
+
if (!cachedProvider) {
|
|
29
|
+
cachedProvider = config.resolveProvider();
|
|
30
|
+
}
|
|
31
|
+
return cachedProvider;
|
|
32
|
+
};
|
|
11
33
|
return (incomingConfig) => {
|
|
12
34
|
if (config.enabled === false) {
|
|
13
35
|
return incomingConfig;
|
|
@@ -26,25 +48,21 @@ export function vercelBlobStorage(config) {
|
|
|
26
48
|
crop: true,
|
|
27
49
|
adapters: [
|
|
28
50
|
{
|
|
29
|
-
name: '
|
|
51
|
+
name: 'object-storage',
|
|
30
52
|
adapter: {
|
|
31
|
-
name: '
|
|
53
|
+
name: 'object-storage',
|
|
32
54
|
generateURL: (file) => {
|
|
33
55
|
return `${prefix}/${file.filename}`;
|
|
34
56
|
},
|
|
35
57
|
upload: async (file) => {
|
|
36
58
|
try {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
throw new Error('Vercel blob token is required but not configured');
|
|
40
|
-
}
|
|
41
|
-
const blob = await put(filePath, file.data, {
|
|
59
|
+
const key = `${prefix}/${collection.slug}/${Date.now()}-${file.name}`;
|
|
60
|
+
const result = await getProvider().put(key, file.data, {
|
|
42
61
|
access: 'public',
|
|
43
|
-
|
|
44
|
-
addRandomSuffix: false,
|
|
62
|
+
contentType: file.mimetype,
|
|
45
63
|
});
|
|
46
64
|
return {
|
|
47
|
-
url:
|
|
65
|
+
url: result.url,
|
|
48
66
|
filename: file.name,
|
|
49
67
|
filesize: file.size,
|
|
50
68
|
mimeType: file.mimetype,
|
|
@@ -53,20 +71,22 @@ export function vercelBlobStorage(config) {
|
|
|
53
71
|
};
|
|
54
72
|
}
|
|
55
73
|
catch (error) {
|
|
56
|
-
defaultLogger.error('
|
|
74
|
+
defaultLogger.error('Object storage upload error:', error);
|
|
57
75
|
throw error;
|
|
58
76
|
}
|
|
59
77
|
},
|
|
60
78
|
delete: async (filename) => {
|
|
61
79
|
try {
|
|
62
|
-
//
|
|
63
|
-
|
|
80
|
+
// Payload may pass a full URL (preferred — providers extract
|
|
81
|
+
// the storage key from it) or a bare filename; mirror the
|
|
82
|
+
// legacy plugin's prefix-prepend for the bare-filename case.
|
|
83
|
+
const keyOrUrl = filename.startsWith('http')
|
|
64
84
|
? filename
|
|
65
85
|
: `${prefix}/${filename}`;
|
|
66
|
-
await del(
|
|
86
|
+
await getProvider().del(keyOrUrl);
|
|
67
87
|
}
|
|
68
88
|
catch (error) {
|
|
69
|
-
defaultLogger.error('
|
|
89
|
+
defaultLogger.error('Object storage delete error:', error);
|
|
70
90
|
throw error;
|
|
71
91
|
}
|
|
72
92
|
},
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare R2 implementation of StorageProvider.
|
|
3
|
+
*
|
|
4
|
+
* R2 is S3-compatible — uses @aws-sdk/client-s3 with a custom endpoint and
|
|
5
|
+
* region='auto'. See https://developers.cloudflare.com/r2/api/s3/api/.
|
|
6
|
+
*
|
|
7
|
+
* GAP-208 Phase 2a (2026-05-18). Phase 1 (interface + types) shipped in #959.
|
|
8
|
+
*
|
|
9
|
+
* Server-only. Do NOT import from client-side code or edge runtime.
|
|
10
|
+
*/
|
|
11
|
+
import type { R2Config, StorageProvider } from './types.js';
|
|
12
|
+
export declare function createR2Provider(config: R2Config): StorageProvider;
|
|
13
|
+
//# sourceMappingURL=r2.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"r2.d.ts","sourceRoot":"","sources":["../../src/storage/r2.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AASH,OAAO,KAAK,EAMV,QAAQ,EACR,eAAe,EAChB,MAAM,YAAY,CAAC;AAsHpB,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,QAAQ,GAAG,eAAe,CAElE"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare R2 implementation of StorageProvider.
|
|
3
|
+
*
|
|
4
|
+
* R2 is S3-compatible — uses @aws-sdk/client-s3 with a custom endpoint and
|
|
5
|
+
* region='auto'. See https://developers.cloudflare.com/r2/api/s3/api/.
|
|
6
|
+
*
|
|
7
|
+
* GAP-208 Phase 2a (2026-05-18). Phase 1 (interface + types) shipped in #959.
|
|
8
|
+
*
|
|
9
|
+
* Server-only. Do NOT import from client-side code or edge runtime.
|
|
10
|
+
*/
|
|
11
|
+
import { DeleteObjectCommand, ListObjectsV2Command, PutObjectCommand, S3Client, } from '@aws-sdk/client-s3';
|
|
12
|
+
import { toUint8Array } from './_helpers.js';
|
|
13
|
+
const PROVIDER_TAG = 'r2';
|
|
14
|
+
const DEFAULT_CONTENT_TYPE = 'application/octet-stream';
|
|
15
|
+
const DEFAULT_LIST_LIMIT = 1000;
|
|
16
|
+
class R2Provider {
|
|
17
|
+
provider = PROVIDER_TAG;
|
|
18
|
+
client;
|
|
19
|
+
bucket;
|
|
20
|
+
publicBaseUrl;
|
|
21
|
+
constructor(config) {
|
|
22
|
+
if (!config.publicBaseUrl) {
|
|
23
|
+
throw new Error('R2 provider requires R2Config.publicBaseUrl. Presigned/private URLs ' +
|
|
24
|
+
'are not supported in Phase 2a. Set publicBaseUrl to either a bound ' +
|
|
25
|
+
"custom domain (e.g. 'https://media.revealui.com') or the R2 dev URL " +
|
|
26
|
+
"('https://<account-id>.r2.cloudflarestorage.com/<bucket>').");
|
|
27
|
+
}
|
|
28
|
+
this.bucket = config.bucket;
|
|
29
|
+
this.publicBaseUrl = stripTrailingSlash(config.publicBaseUrl);
|
|
30
|
+
this.client = new S3Client({
|
|
31
|
+
region: 'auto',
|
|
32
|
+
endpoint: `https://${config.accountId}.r2.cloudflarestorage.com`,
|
|
33
|
+
credentials: {
|
|
34
|
+
accessKeyId: config.accessKeyId,
|
|
35
|
+
secretAccessKey: config.secretAccessKey,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async put(key, data, opts) {
|
|
40
|
+
if (opts?.access === 'private') {
|
|
41
|
+
throw new Error("R2 provider Phase 2a does not support access: 'private'. All objects " +
|
|
42
|
+
'are written under the publicBaseUrl. Presigned/private-URL support ' +
|
|
43
|
+
'lands in Phase 2b.');
|
|
44
|
+
}
|
|
45
|
+
const body = await toUint8Array(data);
|
|
46
|
+
await this.client.send(new PutObjectCommand({
|
|
47
|
+
Bucket: this.bucket,
|
|
48
|
+
Key: key,
|
|
49
|
+
Body: body,
|
|
50
|
+
ContentType: opts?.contentType ?? DEFAULT_CONTENT_TYPE,
|
|
51
|
+
ContentLength: body.byteLength,
|
|
52
|
+
CacheControl: opts?.cacheControl,
|
|
53
|
+
Metadata: opts?.metadata,
|
|
54
|
+
}));
|
|
55
|
+
return {
|
|
56
|
+
key,
|
|
57
|
+
url: `${this.publicBaseUrl}/${key}`,
|
|
58
|
+
size: body.byteLength,
|
|
59
|
+
provider: PROVIDER_TAG,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async del(keyOrUrl) {
|
|
63
|
+
const key = this.extractKey(keyOrUrl);
|
|
64
|
+
await this.client.send(new DeleteObjectCommand({
|
|
65
|
+
Bucket: this.bucket,
|
|
66
|
+
Key: key,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
async list(opts) {
|
|
70
|
+
const response = await this.client.send(new ListObjectsV2Command({
|
|
71
|
+
Bucket: this.bucket,
|
|
72
|
+
Prefix: opts?.prefix,
|
|
73
|
+
MaxKeys: opts?.limit ?? DEFAULT_LIST_LIMIT,
|
|
74
|
+
ContinuationToken: opts?.cursor,
|
|
75
|
+
}));
|
|
76
|
+
const items = (response.Contents ?? []).flatMap((entry) => {
|
|
77
|
+
if (!entry.Key)
|
|
78
|
+
return [];
|
|
79
|
+
return [
|
|
80
|
+
{
|
|
81
|
+
key: entry.Key,
|
|
82
|
+
url: `${this.publicBaseUrl}/${entry.Key}`,
|
|
83
|
+
size: entry.Size ?? 0,
|
|
84
|
+
uploadedAt: entry.LastModified ?? new Date(0),
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
});
|
|
88
|
+
return {
|
|
89
|
+
items,
|
|
90
|
+
cursor: response.NextContinuationToken,
|
|
91
|
+
hasMore: response.IsTruncated === true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
extractKey(keyOrUrl) {
|
|
95
|
+
const parsed = tryParseUrl(keyOrUrl);
|
|
96
|
+
if (!parsed) {
|
|
97
|
+
return keyOrUrl;
|
|
98
|
+
}
|
|
99
|
+
// URL form — strip the leading slash from the pathname.
|
|
100
|
+
return parsed.pathname.startsWith('/') ? parsed.pathname.slice(1) : parsed.pathname;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export function createR2Provider(config) {
|
|
104
|
+
return new R2Provider(config);
|
|
105
|
+
}
|
|
106
|
+
// ── helpers ────────────────────────────────────────────────────────────────
|
|
107
|
+
function tryParseUrl(value) {
|
|
108
|
+
try {
|
|
109
|
+
return new URL(value);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function stripTrailingSlash(value) {
|
|
116
|
+
return value.endsWith('/') ? value.slice(0, -1) : value;
|
|
117
|
+
}
|