@revealui/core 0.7.0 → 0.8.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 +1 -1
- 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/update.d.ts.map +1 -1
- package/dist/collections/operations/update.js +21 -4
- 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/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 +16 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +43 -1
- 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/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/package.json +7 -6
package/README.md
CHANGED
|
@@ -159,7 +159,7 @@ pnpm dev
|
|
|
159
159
|
- **Not** for standalone UI components - use `@revealui/presentation`
|
|
160
160
|
- **Not** for raw database queries - use `@revealui/db` directly
|
|
161
161
|
|
|
162
|
-
##
|
|
162
|
+
## Design Principles
|
|
163
163
|
|
|
164
164
|
- **Sovereign**: Self-hosted runtime engine - no vendor dependency for content management, auth, or storage
|
|
165
165
|
- **Unified**: One `buildConfig()` call wires collections, globals, plugins, security, and feature gates into a single configuration
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Deletes a document by ID with access control enforcement.
|
|
5
5
|
*/
|
|
6
|
-
import type {
|
|
7
|
-
export declare function deleteDocument(config: RevealCollectionConfig, db:
|
|
8
|
-
query: (query: string, values?: unknown[]) => Promise<DatabaseResult>;
|
|
9
|
-
} | null, options: RevealDeleteOptions): Promise<RevealDocument>;
|
|
6
|
+
import type { QueryableDatabaseAdapter, RevealCollectionConfig, RevealDeleteOptions, RevealDocument } from '../../types/index.js';
|
|
7
|
+
export declare function deleteDocument(config: RevealCollectionConfig, db: QueryableDatabaseAdapter | null, options: RevealDeleteOptions): Promise<RevealDocument>;
|
|
10
8
|
//# sourceMappingURL=delete.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"delete.d.ts","sourceRoot":"","sources":["../../../src/collections/operations/delete.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,
|
|
1
|
+
{"version":3,"file":"delete.d.ts","sourceRoot":"","sources":["../../../src/collections/operations/delete.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,wBAAwB,EACxB,sBAAsB,EACtB,mBAAmB,EACnB,cAAc,EAGf,MAAM,sBAAsB,CAAC;AAsC9B,wBAAsB,cAAc,CAClC,MAAM,EAAE,sBAAsB,EAC9B,EAAE,EAAE,wBAAwB,GAAG,IAAI,EACnC,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,cAAc,CAAC,CAkCzB"}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Deletes a document by ID with access control enforcement.
|
|
5
5
|
*/
|
|
6
|
+
import { find } from './find.js';
|
|
6
7
|
import { deleteByIdQuery } from './sqlAdapter.js';
|
|
7
8
|
/**
|
|
8
9
|
* Evaluate a collection's access.delete function.
|
|
@@ -22,16 +23,32 @@ async function evaluateDeleteAccess(config, options) {
|
|
|
22
23
|
const result = await deleteAccess({ req, id: options.id });
|
|
23
24
|
if (result === false)
|
|
24
25
|
return false;
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
if (result === true)
|
|
27
|
+
return true;
|
|
28
|
+
// A WhereClause = row-level access: allowed only for rows matching the filter.
|
|
29
|
+
return result;
|
|
27
30
|
}
|
|
28
31
|
export async function deleteDocument(config, db, options) {
|
|
29
32
|
const { id } = options;
|
|
30
33
|
// --- Access control enforcement ---
|
|
31
|
-
const
|
|
32
|
-
if (
|
|
34
|
+
const access = await evaluateDeleteAccess(config, options);
|
|
35
|
+
if (access === false) {
|
|
33
36
|
throw new Error('Access denied: insufficient permissions to delete this document');
|
|
34
37
|
}
|
|
38
|
+
if (access !== true) {
|
|
39
|
+
// Row-level access: confirm the target row matches the ownership filter
|
|
40
|
+
// before deleting, so delete() cannot remove a row outside the caller's
|
|
41
|
+
// scope. Reuse find()'s AND-merge; overrideAccess skips re-evaluation.
|
|
42
|
+
const scoped = await find(config, db, {
|
|
43
|
+
where: { and: [{ id: { equals: id } }, access] },
|
|
44
|
+
limit: 1,
|
|
45
|
+
req: options.req,
|
|
46
|
+
overrideAccess: true,
|
|
47
|
+
});
|
|
48
|
+
if (scoped.docs.length === 0) {
|
|
49
|
+
throw new Error('Access denied: insufficient permissions to delete this document');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
35
52
|
if (db?.query) {
|
|
36
53
|
// Dynamic collection storage is quarantined in sqlAdapter.ts until this
|
|
37
54
|
// layer is redesigned around typed tables that Drizzle can model directly.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"findById.d.ts","sourceRoot":"","sources":["../../../src/collections/operations/findById.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EACV,YAAY,EACZ,wBAAwB,EACxB,sBAAsB,EACtB,cAAc,EACd,aAAa,
|
|
1
|
+
{"version":3,"file":"findById.d.ts","sourceRoot":"","sources":["../../../src/collections/operations/findById.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EACV,YAAY,EACZ,wBAAwB,EACxB,sBAAsB,EACtB,cAAc,EACd,aAAa,EAGd,MAAM,sBAAsB,CAAC;AAK9B,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,sBAAsB,EAC9B,EAAE,EAAE,wBAAwB,GAAG,IAAI,EACnC,OAAO,EAAE;IACP,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,GACA,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAsIhC"}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { afterRead } from '../../fields/hooks/afterRead/index.js';
|
|
7
7
|
import { deserializeJsonFields } from '../../utils/json-parsing.js';
|
|
8
|
+
import { find } from './find.js';
|
|
8
9
|
import { selectByIdQuery } from './sqlAdapter.js';
|
|
9
10
|
export async function findByID(config, db, options) {
|
|
10
11
|
const { id, depth = 0, req, populate: populateOption } = options;
|
|
@@ -22,9 +23,23 @@ export async function findByID(config, db, options) {
|
|
|
22
23
|
const result = await readAccess({ req, id });
|
|
23
24
|
if (result === false)
|
|
24
25
|
return null;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
if (result !== true) {
|
|
27
|
+
// Row-level access: a WhereClause means "allowed only for rows matching
|
|
28
|
+
// this filter". Confirm THIS row matches before returning it — otherwise
|
|
29
|
+
// findByID hands back rows the ownership rule was meant to scope out
|
|
30
|
+
// (cross-tenant / unpublished-draft leakage). Reuse find()'s AND-merge so
|
|
31
|
+
// the filter runs through the same proven path the list view uses (both
|
|
32
|
+
// storage adapters); overrideAccess skips re-evaluating the rule.
|
|
33
|
+
const scoped = await find(config, db, {
|
|
34
|
+
where: { and: [{ id: { equals: id } }, result] },
|
|
35
|
+
limit: 1,
|
|
36
|
+
depth,
|
|
37
|
+
req,
|
|
38
|
+
...(populateOption !== undefined ? { populate: populateOption } : {}),
|
|
39
|
+
overrideAccess: true,
|
|
40
|
+
});
|
|
41
|
+
return scoped.docs[0] ?? null;
|
|
42
|
+
}
|
|
28
43
|
}
|
|
29
44
|
}
|
|
30
45
|
if (db?.collectionStorage?.findByID) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../src/collections/operations/update.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EACV,wBAAwB,EACxB,sBAAsB,EACtB,cAAc,EAGd,mBAAmB,
|
|
1
|
+
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../src/collections/operations/update.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,KAAK,EACV,wBAAwB,EACxB,sBAAsB,EACtB,cAAc,EAGd,mBAAmB,EAEpB,MAAM,sBAAsB,CAAC;AAmF9B,wBAAsB,MAAM,CAC1B,MAAM,EAAE,sBAAsB,EAC9B,EAAE,EAAE,wBAAwB,GAAG,IAAI,EACnC,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,cAAc,CAAC,CAgNzB"}
|
|
@@ -8,6 +8,7 @@ import { defaultLogger } from '../../instance/logger.js';
|
|
|
8
8
|
import { collectJsonFields, serializeValueForDatabase } from '../../utils/json-parsing.js';
|
|
9
9
|
import { flattenFields, isJsonFieldType } from '../../utils/type-guards.js';
|
|
10
10
|
import { runBeforeFieldHooks } from './fieldHooks.js';
|
|
11
|
+
import { find } from './find.js';
|
|
11
12
|
import { findByID } from './findById.js';
|
|
12
13
|
import { checkExistsByIdQuery, selectJsonByIdQuery, updateByIdQuery, updateByIdWithVersionQuery, } from './sqlAdapter.js';
|
|
13
14
|
/**
|
|
@@ -52,16 +53,32 @@ async function evaluateUpdateAccess(config, options) {
|
|
|
52
53
|
const result = await updateAccess({ req, id: options.id, data: options.data });
|
|
53
54
|
if (result === false)
|
|
54
55
|
return false;
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
if (result === true)
|
|
57
|
+
return true;
|
|
58
|
+
// A WhereClause = row-level access: allowed only for rows matching the filter.
|
|
59
|
+
return result;
|
|
57
60
|
}
|
|
58
61
|
export async function update(config, db, options) {
|
|
59
62
|
const { id, data } = options;
|
|
60
63
|
// --- Access control enforcement ---
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
64
|
+
const access = await evaluateUpdateAccess(config, options);
|
|
65
|
+
if (access === false) {
|
|
63
66
|
throw new Error('Access denied: insufficient permissions to update this document');
|
|
64
67
|
}
|
|
68
|
+
if (access !== true) {
|
|
69
|
+
// Row-level access: confirm the target row matches the ownership filter
|
|
70
|
+
// before mutating, so update() cannot modify a row outside the caller's
|
|
71
|
+
// scope. Reuse find()'s AND-merge; overrideAccess skips re-evaluation.
|
|
72
|
+
const scoped = await find(config, db, {
|
|
73
|
+
where: { and: [{ id: { equals: options.id } }, access] },
|
|
74
|
+
limit: 1,
|
|
75
|
+
req: options.req,
|
|
76
|
+
overrideAccess: true,
|
|
77
|
+
});
|
|
78
|
+
if (scoped.docs.length === 0) {
|
|
79
|
+
throw new Error('Access denied: insufficient permissions to update this document');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
65
82
|
// Run beforeValidate field hooks before validation so they can transform values.
|
|
66
83
|
await runBeforeFieldHooks(config, data, 'update', 'beforeValidate', undefined, options.req);
|
|
67
84
|
// Validate email format if email field is being updated
|
|
@@ -165,7 +165,6 @@ export declare const appMetrics: {
|
|
|
165
165
|
x402PaymentRequiredTotal: Counter;
|
|
166
166
|
x402PaymentVerifyTotal: Counter;
|
|
167
167
|
x402PaymentVerifyDuration: Histogram;
|
|
168
|
-
x402SafeguardRejectionTotal: Counter;
|
|
169
168
|
};
|
|
170
169
|
/**
|
|
171
170
|
* Track HTTP request
|
|
@@ -189,25 +188,16 @@ export declare function trackError(type: string, severity?: 'low' | 'medium' | '
|
|
|
189
188
|
export declare function updateActiveConnections(type: string, delta: number): void;
|
|
190
189
|
/**
|
|
191
190
|
* Track an x402 HTTP 402 emission at a route. Call when the route returns
|
|
192
|
-
* 402 with `X-PAYMENT-REQUIRED`. `currency` is `'usdc-
|
|
193
|
-
*
|
|
194
|
-
* otherwise.
|
|
191
|
+
* 402 with `X-PAYMENT-REQUIRED`. `currency` is always `'usdc-only'` — USDC on
|
|
192
|
+
* Base is the sole settlement currency.
|
|
195
193
|
*/
|
|
196
|
-
export declare function trackX402PaymentRequired(route: string, currency: 'usdc-only'
|
|
194
|
+
export declare function trackX402PaymentRequired(route: string, currency: 'usdc-only'): void;
|
|
197
195
|
/**
|
|
198
196
|
* Track an x402 payment verification. Increments the result counter and
|
|
199
|
-
* observes the duration histogram (latency
|
|
200
|
-
*
|
|
197
|
+
* observes the duration histogram (latency = Coinbase facilitator round-trip
|
|
198
|
+
* for USDC).
|
|
201
199
|
*/
|
|
202
|
-
export declare function trackX402PaymentVerify(route: string, scheme: 'exact'
|
|
203
|
-
/**
|
|
204
|
-
* Track an RVUI safeguards pipeline rejection. Called from
|
|
205
|
-
* `verifyRvuiPayment` when `validatePayment` returns disallowed. Reasons
|
|
206
|
-
* are mapped from the safeguard's free-text reason at the call site to
|
|
207
|
-
* keep wording coupling low.
|
|
208
|
-
*/
|
|
209
|
-
export type X402SafeguardRejectionReason = 'duplicate-tx' | 'circuit-breaker' | 'single-payment-cap' | 'wallet-rate-limit' | 'discount-cap' | 'unknown';
|
|
210
|
-
export declare function trackX402SafeguardRejection(reason: X402SafeguardRejectionReason): void;
|
|
200
|
+
export declare function trackX402PaymentVerify(route: string, scheme: 'exact', result: 'valid' | 'invalid', durationMs: number): void;
|
|
211
201
|
/**
|
|
212
202
|
* Update memory usage
|
|
213
203
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/observability/metrics.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../src/observability/metrics.ts"],"names":[],"mappings":"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"}
|
|
@@ -358,8 +358,7 @@ export const appMetrics = {
|
|
|
358
358
|
// x402 micropayments (GAP-149 PR 5)
|
|
359
359
|
x402PaymentRequiredTotal: metrics.counter('x402_payment_required_total', 'HTTP 402 Payment Required responses emitted by route + currency advertised', ['route', 'currency']),
|
|
360
360
|
x402PaymentVerifyTotal: metrics.counter('x402_payment_verify_total', 'x402 payment verification attempts by route, scheme, and result', ['route', 'scheme', 'result']),
|
|
361
|
-
x402PaymentVerifyDuration: metrics.histogram('x402_payment_verify_duration_seconds', 'x402 payment verification latency (Coinbase facilitator round-trip for USDC
|
|
362
|
-
x402SafeguardRejectionTotal: metrics.counter('x402_safeguard_rejection_total', 'RVUI safeguards pipeline rejections by reason (replay / cap / rate-limit / discount-cap / circuit-breaker)', ['reason']),
|
|
361
|
+
x402PaymentVerifyDuration: metrics.histogram('x402_payment_verify_duration_seconds', 'x402 payment verification latency (Coinbase facilitator round-trip for USDC)', [0.05, 0.1, 0.5, 1, 5, 10, 30], ['scheme']),
|
|
363
362
|
};
|
|
364
363
|
/**
|
|
365
364
|
* Track HTTP request
|
|
@@ -395,25 +394,21 @@ export function updateActiveConnections(type, delta) {
|
|
|
395
394
|
}
|
|
396
395
|
/**
|
|
397
396
|
* Track an x402 HTTP 402 emission at a route. Call when the route returns
|
|
398
|
-
* 402 with `X-PAYMENT-REQUIRED`. `currency` is `'usdc-
|
|
399
|
-
*
|
|
400
|
-
* otherwise.
|
|
397
|
+
* 402 with `X-PAYMENT-REQUIRED`. `currency` is always `'usdc-only'` — USDC on
|
|
398
|
+
* Base is the sole settlement currency.
|
|
401
399
|
*/
|
|
402
400
|
export function trackX402PaymentRequired(route, currency) {
|
|
403
401
|
appMetrics.x402PaymentRequiredTotal.inc(1, { route, currency });
|
|
404
402
|
}
|
|
405
403
|
/**
|
|
406
404
|
* Track an x402 payment verification. Increments the result counter and
|
|
407
|
-
* observes the duration histogram (latency
|
|
408
|
-
*
|
|
405
|
+
* observes the duration histogram (latency = Coinbase facilitator round-trip
|
|
406
|
+
* for USDC).
|
|
409
407
|
*/
|
|
410
408
|
export function trackX402PaymentVerify(route, scheme, result, durationMs) {
|
|
411
409
|
appMetrics.x402PaymentVerifyTotal.inc(1, { route, scheme, result });
|
|
412
410
|
appMetrics.x402PaymentVerifyDuration.observe(durationMs / 1000, { scheme });
|
|
413
411
|
}
|
|
414
|
-
export function trackX402SafeguardRejection(reason) {
|
|
415
|
-
appMetrics.x402SafeguardRejectionTotal.inc(1, { reason });
|
|
416
|
-
}
|
|
417
412
|
/**
|
|
418
413
|
* Update memory usage
|
|
419
414
|
*/
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Provides server-side rendering components for Lexical content.
|
|
5
5
|
* Converts Lexical JSON state to React elements without requiring a browser.
|
|
6
6
|
*/
|
|
7
|
-
import { isSafeUrl, sanitizeUrl } from '@revealui/security';
|
|
7
|
+
import { isSafeUrl, sanitizeUrl } from '@revealui/security/sanitize';
|
|
8
8
|
import type { SerializedEditorState, SerializedLexicalNode } from 'lexical';
|
|
9
9
|
import { type JSX } from 'react';
|
|
10
10
|
export type { SerializedEditorState };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rsc.d.ts","sourceRoot":"","sources":["../../../../src/richtext/exports/server/rsc.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"rsc.d.ts","sourceRoot":"","sources":["../../../../src/richtext/exports/server/rsc.tsx"],"names":[],"mappings":"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"}
|
|
@@ -5,7 +5,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
5
5
|
* Provides server-side rendering components for Lexical content.
|
|
6
6
|
* Converts Lexical JSON state to React elements without requiring a browser.
|
|
7
7
|
*/
|
|
8
|
-
|
|
8
|
+
// Import from the client-safe ./sanitize subpath, NOT the @revealui/security
|
|
9
|
+
// barrel: the barrel statically re-exports auth.ts/gdpr.ts, whose top-level
|
|
10
|
+
// `import 'node:crypto'` crashes this module's client/RSC bundle (blank render
|
|
11
|
+
// on routes that load rich text, e.g. marketing /blog). ./sanitize imports only
|
|
12
|
+
// parse5 and is browser-safe.
|
|
13
|
+
import { isSafeUrl, sanitizeUrl } from '@revealui/security/sanitize';
|
|
9
14
|
import { Fragment } from 'react';
|
|
10
15
|
// URL sanitization is owned by @revealui/security (single source of truth
|
|
11
16
|
// for every untrusted-string sink across RevFleet). Re-exported here so
|
package/dist/security/index.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
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';
|
|
6
13
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/security/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/security/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,cAAc,oBAAoB,CAAC;AACnC,cAAc,2BAA2B,CAAC"}
|
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';
|
|
@@ -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,18 @@
|
|
|
1
|
+
import type { StorageConfig, StorageProvider } from './types.js';
|
|
2
|
+
export { createMockProvider } from './mock.js';
|
|
3
|
+
export { createR2Provider } from './r2.js';
|
|
4
|
+
export type { ListItem, ListOptions, ListResult, PutOptions, PutResult, R2Config, StorageConfig, StorageProvider, VercelBlobConfig, } from './types.js';
|
|
1
5
|
export { vercelBlobStorage } from './vercel-blob.js';
|
|
6
|
+
/**
|
|
7
|
+
* Construct a StorageProvider from a tagged config.
|
|
8
|
+
*
|
|
9
|
+
* Per `feedback_pluggable_provider_pattern`: explicit "no-X" opt-in — unknown
|
|
10
|
+
* or unsupported tags throw with a clear message; no silent fallback.
|
|
11
|
+
*
|
|
12
|
+
* Supported tags in Phase 2a: 'r2', 'mock'. The 'vercel-blob' tag is reserved
|
|
13
|
+
* for Phase 2b (StorageProvider impl over @vercel/blob); during the migration
|
|
14
|
+
* window, consumers needing Vercel Blob should use the legacy `vercelBlobStorage`
|
|
15
|
+
* Payload plugin via `revealui.config.ts`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createStorage(config: StorageConfig): StorageProvider;
|
|
2
18
|
//# 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":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAEjE,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAK/C,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;AAIpB,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,eAAe,CAqBpE"}
|
package/dist/storage/index.js
CHANGED
|
@@ -1,2 +1,44 @@
|
|
|
1
|
-
//
|
|
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 (this PR): r2 + mock providers + createStorage factory.
|
|
6
|
+
import { createMockProvider } from './mock.js';
|
|
7
|
+
import { createR2Provider } from './r2.js';
|
|
8
|
+
export { createMockProvider } from './mock.js';
|
|
9
|
+
// Provider factories — exported so callers can construct a provider directly
|
|
10
|
+
// when they already know which one they want (skipping the discriminated-union
|
|
11
|
+
// factory below).
|
|
12
|
+
export { createR2Provider } from './r2.js';
|
|
13
|
+
// Legacy Payload-style plugin — kept during GAP-208 migration so existing
|
|
14
|
+
// `apps/admin/revealui.config.ts` keeps working. Dropped in Phase 4 cleanup.
|
|
2
15
|
export { vercelBlobStorage } from './vercel-blob.js';
|
|
16
|
+
/**
|
|
17
|
+
* Construct a StorageProvider from a tagged config.
|
|
18
|
+
*
|
|
19
|
+
* Per `feedback_pluggable_provider_pattern`: explicit "no-X" opt-in — unknown
|
|
20
|
+
* or unsupported tags throw with a clear message; no silent fallback.
|
|
21
|
+
*
|
|
22
|
+
* Supported tags in Phase 2a: 'r2', 'mock'. The 'vercel-blob' tag is reserved
|
|
23
|
+
* for Phase 2b (StorageProvider impl over @vercel/blob); during the migration
|
|
24
|
+
* window, consumers needing Vercel Blob should use the legacy `vercelBlobStorage`
|
|
25
|
+
* Payload plugin via `revealui.config.ts`.
|
|
26
|
+
*/
|
|
27
|
+
export function createStorage(config) {
|
|
28
|
+
switch (config.provider) {
|
|
29
|
+
case 'r2':
|
|
30
|
+
return createR2Provider(config.r2);
|
|
31
|
+
case 'mock':
|
|
32
|
+
return createMockProvider();
|
|
33
|
+
case 'vercel-blob':
|
|
34
|
+
throw new Error("createStorage: provider 'vercel-blob' is not yet implemented as a " +
|
|
35
|
+
'StorageProvider (lands in GAP-208 Phase 2b). For the migration ' +
|
|
36
|
+
'window, use the legacy `vercelBlobStorage` Payload plugin in ' +
|
|
37
|
+
"revealui.config.ts, or switch to provider: 'r2'.");
|
|
38
|
+
default: {
|
|
39
|
+
const _exhaustive = config;
|
|
40
|
+
throw new Error(`createStorage: unknown provider tag. Got ${JSON.stringify(_exhaustive)}. ` +
|
|
41
|
+
"Valid tags: 'r2', 'mock', 'vercel-blob' (Phase 2b).");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -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,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
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-agnostic storage interface for the RevealUI fleet.
|
|
3
|
+
*
|
|
4
|
+
* Per `feedback_revealui_agnosticism_principle` + `feedback_pluggable_provider_pattern`:
|
|
5
|
+
* slim interface, tag + factory, explicit "no-X" opt-in, mock provider for tests.
|
|
6
|
+
*
|
|
7
|
+
* Providers implement this interface; consumers (apps/server media routes,
|
|
8
|
+
* apps/admin health probes, packages/core Payload-style plugin shims) depend
|
|
9
|
+
* only on this contract. Switching providers becomes a config change.
|
|
10
|
+
*
|
|
11
|
+
* GAP-208 introduces this abstraction during the Vercel Blob → Cloudflare R2
|
|
12
|
+
* swap (2026-05-18). The vercel-blob Payload-style plugin (vercel-blob.ts)
|
|
13
|
+
* stays in place during migration for backward compat; once consumers migrate
|
|
14
|
+
* to the interface, the legacy plugin is dropped.
|
|
15
|
+
*/
|
|
16
|
+
export interface StorageProvider {
|
|
17
|
+
/** Upload binary data. Returns a fully-qualified URL the value is reachable at. */
|
|
18
|
+
put(key: string, data: Blob | ArrayBuffer | Uint8Array | ReadableStream<Uint8Array>, opts?: PutOptions): Promise<PutResult>;
|
|
19
|
+
/** Delete by either provider URL or storage key. Best-effort; missing keys are not errors. */
|
|
20
|
+
del(keyOrUrl: string): Promise<void>;
|
|
21
|
+
/** List items, optionally filtered by key prefix. Used by health probes + admin browsing. */
|
|
22
|
+
list(opts?: ListOptions): Promise<ListResult>;
|
|
23
|
+
/** Provider tag (e.g. "r2", "vercel-blob", "mock") — exposed so consumers can adapt URL handling. */
|
|
24
|
+
readonly provider: string;
|
|
25
|
+
}
|
|
26
|
+
export interface PutOptions {
|
|
27
|
+
/** MIME type — written to the object's Content-Type header. Defaults to "application/octet-stream". */
|
|
28
|
+
contentType?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Public read access (no auth required for GET on the returned URL).
|
|
31
|
+
* Defaults to "public" because the current media-upload flow needs public CDN URLs.
|
|
32
|
+
* Providers that don't support public access (e.g. private-bucket-only configs)
|
|
33
|
+
* MUST throw when access: "public" is requested.
|
|
34
|
+
*/
|
|
35
|
+
access?: 'public' | 'private';
|
|
36
|
+
/** Cache-Control header value. Provider may apply a sensible default if omitted. */
|
|
37
|
+
cacheControl?: string;
|
|
38
|
+
/** Custom user-metadata. Keys are lowercased; values are strings. Limits vary by provider. */
|
|
39
|
+
metadata?: Record<string, string>;
|
|
40
|
+
}
|
|
41
|
+
export interface PutResult {
|
|
42
|
+
/** The storage key the object was written to (provider-relative, e.g. "media/uuid.jpg"). */
|
|
43
|
+
key: string;
|
|
44
|
+
/** Fully-qualified URL the object is reachable at for GET. */
|
|
45
|
+
url: string;
|
|
46
|
+
/** Storage size in bytes. Always populated by every provider. */
|
|
47
|
+
size: number;
|
|
48
|
+
/** Provider tag that wrote this object (matches StorageProvider.provider). */
|
|
49
|
+
provider: string;
|
|
50
|
+
}
|
|
51
|
+
export interface ListOptions {
|
|
52
|
+
/** Filter to keys starting with this prefix. */
|
|
53
|
+
prefix?: string;
|
|
54
|
+
/** Maximum items per page. Providers cap this internally (typically 1000). */
|
|
55
|
+
limit?: number;
|
|
56
|
+
/** Opaque pagination cursor from a previous ListResult. */
|
|
57
|
+
cursor?: string;
|
|
58
|
+
}
|
|
59
|
+
export interface ListResult {
|
|
60
|
+
items: ListItem[];
|
|
61
|
+
/** Next-page cursor if hasMore is true; undefined when listing is exhausted. */
|
|
62
|
+
cursor?: string;
|
|
63
|
+
hasMore: boolean;
|
|
64
|
+
}
|
|
65
|
+
export interface ListItem {
|
|
66
|
+
key: string;
|
|
67
|
+
url: string;
|
|
68
|
+
size: number;
|
|
69
|
+
uploadedAt: Date;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Discriminated union over `provider`. Adding a new provider means:
|
|
73
|
+
* 1. add a tag here + the per-provider config shape
|
|
74
|
+
* 2. add a case in the createStorage factory
|
|
75
|
+
* 3. implement the StorageProvider interface in a new file
|
|
76
|
+
*
|
|
77
|
+
* No silent fallback per `feedback_pluggable_provider_pattern`: unknown tags
|
|
78
|
+
* throw with a clear "valid tags: ..." message.
|
|
79
|
+
*/
|
|
80
|
+
export type StorageConfig = {
|
|
81
|
+
provider: 'r2';
|
|
82
|
+
r2: R2Config;
|
|
83
|
+
} | {
|
|
84
|
+
provider: 'vercel-blob';
|
|
85
|
+
vercelBlob: VercelBlobConfig;
|
|
86
|
+
} | {
|
|
87
|
+
provider: 'mock';
|
|
88
|
+
};
|
|
89
|
+
export interface R2Config {
|
|
90
|
+
/** Cloudflare account ID (visible at top of the R2 dashboard). */
|
|
91
|
+
accountId: string;
|
|
92
|
+
/** R2 API token Access Key ID (NOT the global account API token). */
|
|
93
|
+
accessKeyId: string;
|
|
94
|
+
/** R2 API token Secret Access Key (paired with accessKeyId). */
|
|
95
|
+
secretAccessKey: string;
|
|
96
|
+
/** Bucket name (per-account-globally-unique within R2). */
|
|
97
|
+
bucket: string;
|
|
98
|
+
/**
|
|
99
|
+
* Optional public base URL for objects (e.g. "https://media.revealui.com" if a
|
|
100
|
+
* custom domain is bound, or "https://<account-id>.r2.cloudflarestorage.com/<bucket>"
|
|
101
|
+
* for the dev URL). When set, PutResult.url uses this base. When omitted, PutResult.url
|
|
102
|
+
* falls back to a presigned URL (private bucket pattern).
|
|
103
|
+
*/
|
|
104
|
+
publicBaseUrl?: string;
|
|
105
|
+
}
|
|
106
|
+
export interface VercelBlobConfig {
|
|
107
|
+
/** vercel_blob_rw_* token from the Vercel Blob dashboard. */
|
|
108
|
+
token: string;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/storage/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,MAAM,WAAW,eAAe;IAC9B,mFAAmF;IACnF,GAAG,CACD,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,CAAC;IAEtB,8FAA8F;IAC9F,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAErC,6FAA6F;IAC7F,IAAI,CAAC,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAE9C,qGAAqG;IACrG,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAID,MAAM,WAAW,UAAU;IACzB,uGAAuG;IACvG,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IAE9B,oFAAoF;IACpF,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,SAAS;IACxB,4FAA4F;IAC5F,GAAG,EAAE,MAAM,CAAC;IAEZ,8DAA8D;IAC9D,GAAG,EAAE,MAAM,CAAC;IAEZ,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IAEb,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAC;CAClB;AAID,MAAM,WAAW,WAAW;IAC1B,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,QAAQ,EAAE,CAAC;IAElB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,IAAI,CAAC;CAClB;AAID;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,EAAE,IAAI,CAAC;IAAC,EAAE,EAAE,QAAQ,CAAA;CAAE,GAChC;IAAE,QAAQ,EAAE,aAAa,CAAC;IAAC,UAAU,EAAE,gBAAgB,CAAA;CAAE,GACzD;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzB,MAAM,WAAW,QAAQ;IACvB,kEAAkE;IAClE,SAAS,EAAE,MAAM,CAAC;IAElB,qEAAqE;IACrE,WAAW,EAAE,MAAM,CAAC;IAEpB,gEAAgE;IAChE,eAAe,EAAE,MAAM,CAAC;IAExB,2DAA2D;IAC3D,MAAM,EAAE,MAAM,CAAC;IAEf;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;CACf"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-agnostic storage interface for the RevealUI fleet.
|
|
3
|
+
*
|
|
4
|
+
* Per `feedback_revealui_agnosticism_principle` + `feedback_pluggable_provider_pattern`:
|
|
5
|
+
* slim interface, tag + factory, explicit "no-X" opt-in, mock provider for tests.
|
|
6
|
+
*
|
|
7
|
+
* Providers implement this interface; consumers (apps/server media routes,
|
|
8
|
+
* apps/admin health probes, packages/core Payload-style plugin shims) depend
|
|
9
|
+
* only on this contract. Switching providers becomes a config change.
|
|
10
|
+
*
|
|
11
|
+
* GAP-208 introduces this abstraction during the Vercel Blob → Cloudflare R2
|
|
12
|
+
* swap (2026-05-18). The vercel-blob Payload-style plugin (vercel-blob.ts)
|
|
13
|
+
* stays in place during migration for backward compat; once consumers migrate
|
|
14
|
+
* to the interface, the legacy plugin is dropped.
|
|
15
|
+
*/
|
|
16
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revealui/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "RevealUI's runtime engine — admin UI, REST API, auth, rich text, plugin system, and access control. The heart of the open-source platform.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@
|
|
7
|
+
"@aws-sdk/client-s3": "^3.1049.0",
|
|
8
|
+
"@electric-sql/pglite": "^0.4.5",
|
|
8
9
|
"@lexical/clipboard": "^0.44.0",
|
|
9
10
|
"@lexical/code": "^0.44.0",
|
|
10
11
|
"@lexical/html": "^0.44.0",
|
|
@@ -21,13 +22,13 @@
|
|
|
21
22
|
"dataloader": "^2.2.3",
|
|
22
23
|
"jose": "^6.2.3",
|
|
23
24
|
"lexical": "^0.44.0",
|
|
24
|
-
"pg": "^8.
|
|
25
|
+
"pg": "^8.21.0",
|
|
25
26
|
"yjs": "^13.6.30",
|
|
26
27
|
"zod": "^4.4.3",
|
|
27
|
-
"@revealui/cache": "0.1
|
|
28
|
-
"@revealui/contracts": "0.
|
|
28
|
+
"@revealui/cache": "0.2.1",
|
|
29
|
+
"@revealui/contracts": "0.6.0",
|
|
29
30
|
"@revealui/resilience": "0.2.4",
|
|
30
|
-
"@revealui/security": "0.
|
|
31
|
+
"@revealui/security": "0.4.0",
|
|
31
32
|
"@revealui/utils": "0.3.5"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|