@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 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
- ## JOSHUA Alignment
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 { DatabaseResult, RevealCollectionConfig, RevealDeleteOptions, RevealDocument } from '../../types/index.js';
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,cAAc,EACd,sBAAsB,EACtB,mBAAmB,EACnB,cAAc,EAEf,MAAM,sBAAsB,CAAC;AAoC9B,wBAAsB,cAAc,CAClC,MAAM,EAAE,sBAAsB,EAC9B,EAAE,EAAE;IACF,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;CACvE,GAAG,IAAI,EACR,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,cAAc,CAAC,CAoBzB"}
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
- // Any truthy result (true, WhereClause) = allowed for single-document operations
26
- return !!result;
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 allowed = await evaluateDeleteAccess(config, options);
32
- if (!allowed) {
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,EAEd,MAAM,sBAAsB,CAAC;AAI9B,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,CAwHhC"}
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
- // If result is a WhereClause (row-level filter), we fetch the doc first then verify.
26
- // For findByID, we check after fetch whether the doc matches the access filter.
27
- // Boolean true = allow, WhereClause = post-fetch filter (handled below after query).
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,EACpB,MAAM,sBAAsB,CAAC;AAiF9B,wBAAsB,MAAM,CAC1B,MAAM,EAAE,sBAAsB,EAC9B,EAAE,EAAE,wBAAwB,GAAG,IAAI,EACnC,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,cAAc,CAAC,CAkMzB"}
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
- // Any truthy result (true, WhereClause) = allowed for single-document operations
56
- return !!result;
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 allowed = await evaluateUpdateAccess(config, options);
62
- if (!allowed) {
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-rvui'` when both are
193
- * advertised (RVUI_PAYMENTS_ENABLED + receiving wallet set) and `'usdc-only'`
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' | 'usdc-rvui'): void;
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 includes Coinbase facilitator
200
- * round-trip for USDC OR Solana RPC + safeguards pipeline for RVUI).
197
+ * observes the duration histogram (latency = Coinbase facilitator round-trip
198
+ * for USDC).
201
199
  */
202
- export declare function trackX402PaymentVerify(route: string, scheme: 'exact' | 'solana-spl', result: 'valid' | 'invalid', durationMs: number): void;
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; Solana RPC + safeguards for RVUI)', [0.05, 0.1, 0.5, 1, 5, 10, 30], ['scheme']),
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-rvui'` when both are
399
- * advertised (RVUI_PAYMENTS_ENABLED + receiving wallet set) and `'usdc-only'`
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 includes Coinbase facilitator
408
- * round-trip for USDC OR Solana RPC + safeguards pipeline for RVUI).
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;AAEH,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAC5E,OAAO,EAAY,KAAK,GAAG,EAAE,MAAM,OAAO,CAAC;AAG3C,YAAY,EAAE,qBAAqB,EAAE,CAAC;AAKtC,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC;AAyBlC,UAAU,qBAAsB,SAAQ,qBAAqB;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,qBAAqB,EAAE,CAAC;IACnC,SAAS,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,IAAI,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;IACzC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,kBAAmB,SAAQ,qBAAqB;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE;QACP,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,QAAQ,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;QACjC,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,GAAG,CAAC,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAA;SAAE,CAAC;KAC7C,CAAC;CACH;AAmBD,UAAU,mBAAoB,SAAQ,qBAAqB;IACzD,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,0BAA0B,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CACrE,SAAQ,qBAAqB;IAC7B,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,CAAC,GAAG;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CACnC;AAMD,MAAM,WAAW,wBAAwB;IACvC,IAAI,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAMD,UAAU,gBAAgB;IACxB,4BAA4B;IAC5B,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,mBAAmB,EAAE,KAAK,EAAE,MAAM,KAAK,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC;IAC/E,2BAA2B;IAC3B,UAAU,CAAC,EAAE,CACX,IAAI,EAAE,kBAAkB,EACxB,QAAQ,EAAE,GAAG,CAAC,OAAO,GAAG,IAAI,EAC5B,KAAK,EAAE,MAAM,KACV,GAAG,CAAC,OAAO,CAAC;IACjB,4BAA4B;IAC5B,WAAW,CAAC,EAAE,CACZ,IAAI,EAAE;QACJ,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,EACD,KAAK,EAAE,MAAM,KACV,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC;CACzB;AAqSD;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,qBAAqB,GAAG,IAAI,GAAG,SAAS,EAC9C,OAAO,CAAC,EAAE,gBAAgB,GACzB,GAAG,CAAC,OAAO,GAAG,IAAI,CAMpB;AAED;;;;;;;;;GASG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,qBAAqB,GAAG,IAAI,GAAG,SAAS,GAC7C,OAAO,CAAC,MAAM,CAAC,CA2CjB;AAMD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,wBAAwB,kDAYhF;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,yBAAyB,kDAYlF;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,qBAAqB,GAAG,IAAI,GAAG,SAAS,CAAC;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,mBAAmB,EAAE,KAAK,EAAE,MAAM,KAAK,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC;IAC/E,UAAU,CAAC,EAAE,CACX,IAAI,EAAE,kBAAkB,EACxB,QAAQ,EAAE,GAAG,CAAC,OAAO,GAAG,IAAI,EAC5B,KAAK,EAAE,MAAM,KACV,GAAG,CAAC,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,CACZ,IAAI,EAAE;QACJ,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,EACD,KAAK,EAAE,MAAM,KACV,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC;CACzB;AAED,wBAAgB,eAAe,CAAC,EAC9B,IAAI,EACJ,SAAS,EACT,WAAW,EACX,UAAU,EACV,WAAW,GACZ,EAAE,oBAAoB,kDAgBtB;AAED,eAAe,eAAe,CAAC"}
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
- import { isSafeUrl, sanitizeUrl } from '@revealui/security';
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
@@ -1,6 +1,13 @@
1
1
  /**
2
- * Security & Compliance - re-exported from @revealui/security
3
- * New code should import directly from '@revealui/security'.
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;;;GAGG;AACH,cAAc,oBAAoB,CAAC"}
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"}
@@ -1,5 +1,12 @@
1
1
  /**
2
- * Security & Compliance - re-exported from @revealui/security
3
- * New code should import directly from '@revealui/security'.
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
+ }
@@ -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":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC"}
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"}
@@ -1,2 +1,44 @@
1
- // Re-export storage adapters
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.7.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
- "@electric-sql/pglite": "^0.4.4",
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.20.0",
25
+ "pg": "^8.21.0",
25
26
  "yjs": "^13.6.30",
26
27
  "zod": "^4.4.3",
27
- "@revealui/cache": "0.1.5",
28
- "@revealui/contracts": "0.5.0",
28
+ "@revealui/cache": "0.2.1",
29
+ "@revealui/contracts": "0.6.0",
29
30
  "@revealui/resilience": "0.2.4",
30
- "@revealui/security": "0.3.1",
31
+ "@revealui/security": "0.4.0",
31
32
  "@revealui/utils": "0.3.5"
32
33
  },
33
34
  "devDependencies": {