@revealui/core 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +57 -5
  2. package/dist/client/admin/components/AdminDashboard.d.ts +7 -1
  3. package/dist/client/admin/components/AdminDashboard.d.ts.map +1 -1
  4. package/dist/client/admin/components/AdminDashboard.js +11 -11
  5. package/dist/client/admin/components/CollectionList.d.ts +2 -1
  6. package/dist/client/admin/components/CollectionList.d.ts.map +1 -1
  7. package/dist/client/admin/components/CollectionList.js +5 -5
  8. package/dist/client/admin/components/DocumentForm.d.ts +2 -1
  9. package/dist/client/admin/components/DocumentForm.d.ts.map +1 -1
  10. package/dist/client/admin/components/DocumentForm.js +16 -16
  11. package/dist/client/admin/components/GlobalForm.d.ts +2 -1
  12. package/dist/client/admin/components/GlobalForm.d.ts.map +1 -1
  13. package/dist/client/admin/components/GlobalForm.js +3 -3
  14. package/dist/client/admin/context/ServerFunctionContext.d.ts +1 -1
  15. package/dist/client/admin/context/ServerFunctionContext.d.ts.map +1 -1
  16. package/dist/client/admin/layout.d.ts +1 -1
  17. package/dist/client/admin/layout.d.ts.map +1 -1
  18. package/dist/client/admin/layout.js +5 -1
  19. package/dist/client/admin/page.d.ts +2 -2
  20. package/dist/client/admin/page.d.ts.map +1 -1
  21. package/dist/client/admin/page.js +10 -3
  22. package/dist/client/richtext/RichTextEditor.d.ts +1 -1
  23. package/dist/client/richtext/RichTextEditor.d.ts.map +1 -1
  24. package/dist/client/richtext/plugins/CollaborationPlugin.d.ts +1 -1
  25. package/dist/client/richtext/plugins/CollaborationPlugin.d.ts.map +1 -1
  26. package/dist/client/richtext/plugins/ToolbarPlugin.d.ts +1 -1
  27. package/dist/client/richtext/plugins/ToolbarPlugin.d.ts.map +1 -1
  28. package/dist/collections/operations/sqlAdapter.d.ts.map +1 -1
  29. package/dist/collections/operations/sqlAdapter.js +7 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/license.d.ts +20 -0
  34. package/dist/license.d.ts.map +1 -1
  35. package/dist/license.js +30 -0
  36. package/dist/nextjs/withRevealUI.d.ts +7 -2
  37. package/dist/nextjs/withRevealUI.d.ts.map +1 -1
  38. package/dist/nextjs/withRevealUI.js +9 -99
  39. package/dist/observability/health-check.d.ts.map +1 -1
  40. package/dist/observability/health-check.js +8 -1
  41. package/dist/revforge-license.d.ts +4 -2
  42. package/dist/revforge-license.d.ts.map +1 -1
  43. package/dist/revforge-license.js +21 -3
  44. package/dist/richtext/exports/server/rsc.d.ts +3 -3
  45. package/dist/richtext/exports/server/rsc.d.ts.map +1 -1
  46. package/dist/server/index.d.ts +1 -1
  47. package/dist/server/index.d.ts.map +1 -1
  48. package/dist/server/index.js +1 -1
  49. package/dist/storage/_sigv4.d.ts +76 -0
  50. package/dist/storage/_sigv4.d.ts.map +1 -0
  51. package/dist/storage/_sigv4.js +134 -0
  52. package/dist/storage/_xml.d.ts +26 -0
  53. package/dist/storage/_xml.d.ts.map +1 -0
  54. package/dist/storage/_xml.js +75 -0
  55. package/dist/storage/index.d.ts +7 -6
  56. package/dist/storage/index.d.ts.map +1 -1
  57. package/dist/storage/index.js +17 -15
  58. package/dist/storage/object-storage.d.ts +41 -0
  59. package/dist/storage/object-storage.d.ts.map +1 -0
  60. package/dist/storage/{vercel-blob.js → object-storage.js} +38 -18
  61. package/dist/storage/r2.d.ts +14 -2
  62. package/dist/storage/r2.d.ts.map +1 -1
  63. package/dist/storage/r2.js +107 -44
  64. package/dist/storage/vercel-blob-provider.d.ts +28 -0
  65. package/dist/storage/vercel-blob-provider.d.ts.map +1 -0
  66. package/dist/storage/vercel-blob-provider.js +88 -0
  67. package/package.json +22 -23
  68. package/dist/storage/vercel-blob.d.ts +0 -17
  69. package/dist/storage/vercel-blob.d.ts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAEvE,OAAO,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAE9E,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AAEjE,cAAc,6BAA6B,CAAC;AAQ5C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAEvE,OAAO,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAE9E,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AAEjE,cAAc,6BAA6B,CAAC;AAQ5C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC"}
@@ -15,7 +15,7 @@
15
15
  export { createRESTHandlers, handleRESTRequest } from '../api/rest.js';
16
16
  // Server-only database/storage
17
17
  export { universalPostgresAdapter } from '../database/universal-postgres.js';
18
- export { vercelBlobStorage } from '../storage/vercel-blob.js';
18
+ export { objectStorage } from '../storage/object-storage.js';
19
19
  // Server-only logger with request context
20
20
  export { createLogger, logger } from '../utils/logger-server.js';
21
21
  // Request context utilities (server-only - uses async_hooks)
@@ -0,0 +1,76 @@
1
+ /**
2
+ * AWS Signature Version 4 signer for the Cloudflare R2 (S3-compatible) API.
3
+ *
4
+ * Replaces the request signing that @aws-sdk/client-s3 used to do for us.
5
+ * Uses only node:crypto — no external dependencies, no regex.
6
+ *
7
+ * The core (`computeSigV4`) is verified byte-for-byte against the official AWS
8
+ * SigV4 test-suite `get-vanilla` vector in __tests__/_sigv4.test.ts.
9
+ *
10
+ * Server-only. Do NOT import from client-side code or edge runtime.
11
+ */
12
+ /** Hex SHA-256 of the empty string — the payload hash for body-less requests. */
13
+ export declare const EMPTY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
14
+ /** Hex SHA-256 of arbitrary bytes (request payload hashing). */
15
+ export declare function sha256Hex(data: string | Uint8Array): string;
16
+ /**
17
+ * RFC 3986 percent-encoding per the AWS SigV4 spec. `encodeSlash` is false for
18
+ * path segments (slashes are separators) and true for query keys/values.
19
+ */
20
+ export declare function awsUriEncode(input: string, encodeSlash: boolean): string;
21
+ /** Inputs to the verified SigV4 core. All path/header values must be final. */
22
+ export interface CanonicalInput {
23
+ method: string;
24
+ /** Already AWS-URI-encoded path (slashes preserved). */
25
+ canonicalPath: string;
26
+ /** Raw (unencoded) query pairs; encoded + sorted by the signer. */
27
+ query: [string, string][];
28
+ /** Exact header set to sign (any case; lowercased + trimmed internally). */
29
+ headersToSign: Record<string, string>;
30
+ payloadHash: string;
31
+ amzDate: string;
32
+ region: string;
33
+ service: string;
34
+ accessKeyId: string;
35
+ secretAccessKey: string;
36
+ }
37
+ export interface SigV4Result {
38
+ signature: string;
39
+ signedHeaders: string;
40
+ scope: string;
41
+ canonicalQuery: string;
42
+ }
43
+ /**
44
+ * The verified SigV4 computation: canonical request → string-to-sign → signing
45
+ * key → signature. Verified against AWS's official `get-vanilla` test vector.
46
+ */
47
+ export declare function computeSigV4(input: CanonicalInput): SigV4Result;
48
+ /** Inputs for an R2/S3 request signature. */
49
+ export interface SignS3Input {
50
+ method: 'GET' | 'PUT' | 'DELETE';
51
+ accountId: string;
52
+ bucket: string;
53
+ /** Object key; omit for bucket-level operations (e.g. ListObjectsV2). */
54
+ key?: string;
55
+ /** Query params; `undefined` values are dropped before signing. */
56
+ query?: Record<string, string | undefined>;
57
+ region: string;
58
+ accessKeyId: string;
59
+ secretAccessKey: string;
60
+ /** Hex SHA-256 of the body, or EMPTY_SHA256 for body-less requests. */
61
+ payloadHash: string;
62
+ /** Extra headers to send + sign (content-type, cache-control, x-amz-meta-*). */
63
+ extraHeaders?: Record<string, string>;
64
+ now: Date;
65
+ }
66
+ export interface SignedS3Request {
67
+ url: string;
68
+ headers: Record<string, string>;
69
+ }
70
+ /**
71
+ * Build a signed R2 request: a path-style URL + AWS4-HMAC-SHA256 Authorization
72
+ * header. The `host` header is signed but not returned — fetch/undici sets it
73
+ * from the URL (same value), and `Host` is a forbidden header to set manually.
74
+ */
75
+ export declare function signS3Request(input: SignS3Input): SignedS3Request;
76
+ //# sourceMappingURL=_sigv4.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_sigv4.d.ts","sourceRoot":"","sources":["../../src/storage/_sigv4.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAOH,iFAAiF;AACjF,eAAO,MAAM,YAAY,qEAAqE,CAAC;AAE/F,gEAAgE;AAChE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAE3D;AAQD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,GAAG,MAAM,CAaxE;AAQD,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,aAAa,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;IAC1B,4EAA4E;IAC5E,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,cAAc,GAAG,WAAW,CAkC/D;AAED,6CAA6C;AAC7C,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,KAAK,GAAG,KAAK,GAAG,QAAQ,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,uEAAuE;IACvE,WAAW,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,GAAG,EAAE,IAAI,CAAC;CACX;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,WAAW,GAAG,eAAe,CAmDjE"}
@@ -0,0 +1,134 @@
1
+ /**
2
+ * AWS Signature Version 4 signer for the Cloudflare R2 (S3-compatible) API.
3
+ *
4
+ * Replaces the request signing that @aws-sdk/client-s3 used to do for us.
5
+ * Uses only node:crypto — no external dependencies, no regex.
6
+ *
7
+ * The core (`computeSigV4`) is verified byte-for-byte against the official AWS
8
+ * SigV4 test-suite `get-vanilla` vector in __tests__/_sigv4.test.ts.
9
+ *
10
+ * Server-only. Do NOT import from client-side code or edge runtime.
11
+ */
12
+ import { createHash, createHmac } from 'node:crypto';
13
+ const ALGORITHM = 'AWS4-HMAC-SHA256';
14
+ const S3_SERVICE = 's3';
15
+ /** Hex SHA-256 of the empty string — the payload hash for body-less requests. */
16
+ export const EMPTY_SHA256 = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
17
+ /** Hex SHA-256 of arbitrary bytes (request payload hashing). */
18
+ export function sha256Hex(data) {
19
+ return createHash('sha256').update(data).digest('hex');
20
+ }
21
+ function hmac(key, data) {
22
+ return createHmac('sha256', key).update(data, 'utf8').digest();
23
+ }
24
+ const UNRESERVED = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~';
25
+ /**
26
+ * RFC 3986 percent-encoding per the AWS SigV4 spec. `encodeSlash` is false for
27
+ * path segments (slashes are separators) and true for query keys/values.
28
+ */
29
+ export function awsUriEncode(input, encodeSlash) {
30
+ let out = '';
31
+ for (const byte of new TextEncoder().encode(input)) {
32
+ const ch = String.fromCharCode(byte);
33
+ if (UNRESERVED.includes(ch)) {
34
+ out += ch;
35
+ }
36
+ else if (ch === '/' && !encodeSlash) {
37
+ out += '/';
38
+ }
39
+ else {
40
+ out += `%${byte.toString(16).toUpperCase().padStart(2, '0')}`;
41
+ }
42
+ }
43
+ return out;
44
+ }
45
+ function toAmzDate(now) {
46
+ const iso = now.toISOString(); // e.g. 2026-05-18T10:00:00.000Z
47
+ const amzDate = `${iso.slice(0, 4)}${iso.slice(5, 7)}${iso.slice(8, 10)}T${iso.slice(11, 13)}${iso.slice(14, 16)}${iso.slice(17, 19)}Z`;
48
+ return { amzDate, dateStamp: amzDate.slice(0, 8) };
49
+ }
50
+ /**
51
+ * The verified SigV4 computation: canonical request → string-to-sign → signing
52
+ * key → signature. Verified against AWS's official `get-vanilla` test vector.
53
+ */
54
+ export function computeSigV4(input) {
55
+ const lowered = new Map();
56
+ for (const [name, value] of Object.entries(input.headersToSign)) {
57
+ lowered.set(name.toLowerCase(), value.trim());
58
+ }
59
+ const names = [...lowered.keys()].sort();
60
+ const canonicalHeaders = names.map((n) => `${n}:${lowered.get(n)}\n`).join('');
61
+ const signedHeaders = names.join(';');
62
+ const canonicalQuery = [...input.query]
63
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
64
+ .map(([k, v]) => `${awsUriEncode(k, true)}=${awsUriEncode(v, true)}`)
65
+ .join('&');
66
+ const canonicalRequest = [
67
+ input.method,
68
+ input.canonicalPath,
69
+ canonicalQuery,
70
+ canonicalHeaders,
71
+ signedHeaders,
72
+ input.payloadHash,
73
+ ].join('\n');
74
+ const dateStamp = input.amzDate.slice(0, 8);
75
+ const scope = `${dateStamp}/${input.region}/${input.service}/aws4_request`;
76
+ const stringToSign = [ALGORITHM, input.amzDate, scope, sha256Hex(canonicalRequest)].join('\n');
77
+ const kDate = hmac(`AWS4${input.secretAccessKey}`, dateStamp);
78
+ const kRegion = hmac(kDate, input.region);
79
+ const kService = hmac(kRegion, input.service);
80
+ const kSigning = hmac(kService, 'aws4_request');
81
+ const signature = createHmac('sha256', kSigning).update(stringToSign, 'utf8').digest('hex');
82
+ return { signature, signedHeaders, scope, canonicalQuery };
83
+ }
84
+ /**
85
+ * Build a signed R2 request: a path-style URL + AWS4-HMAC-SHA256 Authorization
86
+ * header. The `host` header is signed but not returned — fetch/undici sets it
87
+ * from the URL (same value), and `Host` is a forbidden header to set manually.
88
+ */
89
+ export function signS3Request(input) {
90
+ const host = `${input.accountId}.r2.cloudflarestorage.com`;
91
+ const { amzDate } = toAmzDate(input.now);
92
+ const encodedKey = (input.key ?? '')
93
+ .split('/')
94
+ .map((segment) => awsUriEncode(segment, false))
95
+ .join('/');
96
+ const canonicalPath = input.key !== undefined
97
+ ? `/${awsUriEncode(input.bucket, false)}/${encodedKey}`
98
+ : `/${awsUriEncode(input.bucket, false)}`;
99
+ const query = [];
100
+ for (const [k, v] of Object.entries(input.query ?? {})) {
101
+ if (v !== undefined)
102
+ query.push([k, v]);
103
+ }
104
+ const headersToSign = {
105
+ host,
106
+ 'x-amz-date': amzDate,
107
+ 'x-amz-content-sha256': input.payloadHash,
108
+ };
109
+ for (const [k, v] of Object.entries(input.extraHeaders ?? {})) {
110
+ headersToSign[k.toLowerCase()] = v;
111
+ }
112
+ const { signature, signedHeaders, scope, canonicalQuery } = computeSigV4({
113
+ method: input.method,
114
+ canonicalPath,
115
+ query,
116
+ headersToSign,
117
+ payloadHash: input.payloadHash,
118
+ amzDate,
119
+ region: input.region,
120
+ service: S3_SERVICE,
121
+ accessKeyId: input.accessKeyId,
122
+ secretAccessKey: input.secretAccessKey,
123
+ });
124
+ const headers = {
125
+ 'x-amz-date': amzDate,
126
+ 'x-amz-content-sha256': input.payloadHash,
127
+ authorization: `${ALGORITHM} Credential=${input.accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`,
128
+ };
129
+ for (const [k, v] of Object.entries(input.extraHeaders ?? {})) {
130
+ headers[k.toLowerCase()] = v;
131
+ }
132
+ const url = `https://${host}${canonicalPath}${canonicalQuery ? `?${canonicalQuery}` : ''}`;
133
+ return { url, headers };
134
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Minimal, dependency-free readers for the two S3/R2 XML responses we consume:
3
+ * ListObjectsV2 results and `<Error>` bodies. Replaces the XML codec that
4
+ * @aws-sdk/client-s3 used to run for us.
5
+ *
6
+ * No regex (M2): a tag's text can never contain a raw `<` (XML escapes it to
7
+ * `&lt;`), so `indexOf`-based tag scanning is unambiguous.
8
+ */
9
+ export interface ListedObject {
10
+ key: string;
11
+ size: number;
12
+ lastModified: Date;
13
+ }
14
+ export interface ParsedList {
15
+ objects: ListedObject[];
16
+ isTruncated: boolean;
17
+ nextContinuationToken?: string;
18
+ }
19
+ /** Parse an S3 ListObjectsV2 XML response into the fields the R2 provider needs. */
20
+ export declare function parseListObjectsV2(xml: string): ParsedList;
21
+ /** Extract `<Code>`/`<Message>` from an S3 `<Error>` response for diagnostics. */
22
+ export declare function s3ErrorFields(xml: string): {
23
+ code?: string;
24
+ message?: string;
25
+ };
26
+ //# sourceMappingURL=_xml.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_xml.d.ts","sourceRoot":"","sources":["../../src/storage/_xml.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA6BH,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,WAAW,EAAE,OAAO,CAAC;IACrB,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,oFAAoF;AACpF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CA6B1D;AAED,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAK9E"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Minimal, dependency-free readers for the two S3/R2 XML responses we consume:
3
+ * ListObjectsV2 results and `<Error>` bodies. Replaces the XML codec that
4
+ * @aws-sdk/client-s3 used to run for us.
5
+ *
6
+ * No regex (M2): a tag's text can never contain a raw `<` (XML escapes it to
7
+ * `&lt;`), so `indexOf`-based tag scanning is unambiguous.
8
+ */
9
+ /** First `<tag>…</tag>` text content at or after `from`, or undefined. */
10
+ function firstTagValue(xml, tag, from = 0) {
11
+ const open = `<${tag}>`;
12
+ const start = xml.indexOf(open, from);
13
+ if (start === -1)
14
+ return undefined;
15
+ const contentStart = start + open.length;
16
+ const end = xml.indexOf(`</${tag}>`, contentStart);
17
+ if (end === -1)
18
+ return undefined;
19
+ return xml.slice(contentStart, end);
20
+ }
21
+ /** Decode the five predefined XML entities (no regex). `&amp;` is decoded last. */
22
+ function decodeXmlEntities(value) {
23
+ if (!value.includes('&'))
24
+ return value;
25
+ return value
26
+ .split('&lt;')
27
+ .join('<')
28
+ .split('&gt;')
29
+ .join('>')
30
+ .split('&quot;')
31
+ .join('"')
32
+ .split('&apos;')
33
+ .join("'")
34
+ .split('&amp;')
35
+ .join('&');
36
+ }
37
+ /** Parse an S3 ListObjectsV2 XML response into the fields the R2 provider needs. */
38
+ export function parseListObjectsV2(xml) {
39
+ const objects = [];
40
+ const contentsOpen = '<Contents>';
41
+ const contentsClose = '</Contents>';
42
+ let cursor = 0;
43
+ for (;;) {
44
+ const start = xml.indexOf(contentsOpen, cursor);
45
+ if (start === -1)
46
+ break;
47
+ const end = xml.indexOf(contentsClose, start);
48
+ if (end === -1)
49
+ break;
50
+ const block = xml.slice(start + contentsOpen.length, end);
51
+ const key = firstTagValue(block, 'Key');
52
+ if (key !== undefined) {
53
+ const lastModified = firstTagValue(block, 'LastModified');
54
+ objects.push({
55
+ key: decodeXmlEntities(key),
56
+ size: Number(firstTagValue(block, 'Size') ?? '0') || 0,
57
+ lastModified: lastModified ? new Date(lastModified) : new Date(0),
58
+ });
59
+ }
60
+ cursor = end + contentsClose.length;
61
+ }
62
+ const token = firstTagValue(xml, 'NextContinuationToken');
63
+ return {
64
+ objects,
65
+ isTruncated: firstTagValue(xml, 'IsTruncated') === 'true',
66
+ nextContinuationToken: token !== undefined ? decodeXmlEntities(token) : undefined,
67
+ };
68
+ }
69
+ /** Extract `<Code>`/`<Message>` from an S3 `<Error>` response for diagnostics. */
70
+ export function s3ErrorFields(xml) {
71
+ return {
72
+ code: firstTagValue(xml, 'Code'),
73
+ message: firstTagValue(xml, 'Message'),
74
+ };
75
+ }
@@ -1,18 +1,19 @@
1
1
  import type { StorageConfig, StorageProvider } from './types.js';
2
2
  export { createMockProvider } from './mock.js';
3
+ export type { ObjectStorageConfig } from './object-storage.js';
4
+ export { objectStorage } from './object-storage.js';
3
5
  export { createR2Provider } from './r2.js';
4
6
  export type { ListItem, ListOptions, ListResult, PutOptions, PutResult, R2Config, StorageConfig, StorageProvider, VercelBlobConfig, } from './types.js';
5
- export { vercelBlobStorage } from './vercel-blob.js';
7
+ export { createVercelBlobProvider } from './vercel-blob-provider.js';
6
8
  /**
7
9
  * Construct a StorageProvider from a tagged config.
8
10
  *
9
11
  * Per `feedback_pluggable_provider_pattern`: explicit "no-X" opt-in — unknown
10
- * or unsupported tags throw with a clear message; no silent fallback.
12
+ * tags throw with a clear message; no silent fallback.
11
13
  *
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`.
14
+ * Supported tags: 'r2' (canonical), 'vercel-blob' (legacy migration-window
15
+ * fallback), and 'mock' (tests). The provider-agnostic `objectStorage` Payload
16
+ * plugin (object-storage.ts) adapts any of these to apps/admin's upload path.
16
17
  */
17
18
  export declare function createStorage(config: StorageConfig): StorageProvider;
18
19
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
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
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/storage/index.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAGjE,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAK/D,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAC3C,YAAY,EACV,QAAQ,EACR,WAAW,EACX,UAAU,EACV,UAAU,EACV,SAAS,EACT,QAAQ,EACR,aAAa,EACb,eAAe,EACf,gBAAgB,GACjB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAErE;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,eAAe,CAgBpE"}
@@ -2,43 +2,45 @@
2
2
  // GAP-208 — introduced 2026-05-18 alongside Vercel Blob → Cloudflare R2 swap.
3
3
  //
4
4
  // Phase 1 (#959): types.ts + index.ts type re-exports.
5
- // Phase 2a (this PR): r2 + mock providers + createStorage factory.
5
+ // Phase 2a (#959 follow-up): r2 + mock providers + createStorage factory.
6
+ // Phase 2b (this PR): vercel-blob StorageProvider + apps/server media-route
7
+ // cutover. R2 is canonical; Vercel Blob is the migration-window fallback.
6
8
  import { createMockProvider } from './mock.js';
7
9
  import { createR2Provider } from './r2.js';
10
+ import { createVercelBlobProvider } from './vercel-blob-provider.js';
8
11
  export { createMockProvider } from './mock.js';
12
+ // Provider-agnostic Payload-style upload plugin — adapts any StorageProvider
13
+ // (R2 canonical, Vercel Blob fallback, mock) to the engine's collection upload
14
+ // adapter. Used by apps/admin/revealui.config.ts. GAP-208 Phase 4 replaced the
15
+ // provider-specific `vercelBlobStorage` plugin with this.
16
+ export { objectStorage } from './object-storage.js';
9
17
  // Provider factories — exported so callers can construct a provider directly
10
18
  // when they already know which one they want (skipping the discriminated-union
11
19
  // factory below).
12
20
  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.
15
- export { vercelBlobStorage } from './vercel-blob.js';
21
+ export { createVercelBlobProvider } from './vercel-blob-provider.js';
16
22
  /**
17
23
  * Construct a StorageProvider from a tagged config.
18
24
  *
19
25
  * Per `feedback_pluggable_provider_pattern`: explicit "no-X" opt-in — unknown
20
- * or unsupported tags throw with a clear message; no silent fallback.
26
+ * tags throw with a clear message; no silent fallback.
21
27
  *
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`.
28
+ * Supported tags: 'r2' (canonical), 'vercel-blob' (legacy migration-window
29
+ * fallback), and 'mock' (tests). The provider-agnostic `objectStorage` Payload
30
+ * plugin (object-storage.ts) adapts any of these to apps/admin's upload path.
26
31
  */
27
32
  export function createStorage(config) {
28
33
  switch (config.provider) {
29
34
  case 'r2':
30
35
  return createR2Provider(config.r2);
36
+ case 'vercel-blob':
37
+ return createVercelBlobProvider(config.vercelBlob);
31
38
  case 'mock':
32
39
  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
40
  default: {
39
41
  const _exhaustive = config;
40
42
  throw new Error(`createStorage: unknown provider tag. Got ${JSON.stringify(_exhaustive)}. ` +
41
- "Valid tags: 'r2', 'mock', 'vercel-blob' (Phase 2b).");
43
+ "Valid tags: 'r2', 'vercel-blob', 'mock'.");
42
44
  }
43
45
  }
44
46
  }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Object-storage upload plugin (provider-agnostic).
3
+ *
4
+ * Adapts any `StorageProvider` (Cloudflare R2 — canonical — Vercel Blob, mock,
5
+ * or a future backend) to the RevealUI engine's collection upload-adapter
6
+ * interface, so `apps/admin/revealui.config.ts` stores media through the
7
+ * configured object-storage backend. Replaces the provider-specific
8
+ * `vercelBlobStorage` Payload plugin retired in GAP-208 Phase 4.
9
+ *
10
+ * The backend is resolved through a lazy `resolveProvider` thunk — invoked
11
+ * (and memoized) on the first upload/delete, never at config-build time — so a
12
+ * consumer reading validated config (e.g. `@revealui/config`'s `config.storage`)
13
+ * does NOT force env validation when the admin config module is imported during
14
+ * `next build`, tests, or boot. This mirrors `apps/server` `getMediaStorage()`,
15
+ * which reads `config.storage` lazily per request.
16
+ *
17
+ * WARNING: This module is server-only.
18
+ * Do NOT import in client-side code or edge runtime.
19
+ */
20
+ import type { Plugin } from '../types/index.js';
21
+ import type { StorageProvider } from './types.js';
22
+ export interface ObjectStorageConfig {
23
+ /** When `false`, the plugin is a no-op (no upload config applied). Defaults to enabled. */
24
+ enabled?: boolean;
25
+ /** Map of collection slug → whether to attach upload handling. */
26
+ collections?: {
27
+ [key: string]: boolean;
28
+ };
29
+ /**
30
+ * Lazily resolves the backend the upload adapter writes through. Called once
31
+ * (memoized) on the first upload/delete — never at config build — so reading
32
+ * validated config (e.g. `@revealui/config`'s `config.storage`) does not force
33
+ * env validation at module load. Should throw a clear, actionable error when
34
+ * no backend is configured (fail-fast; never silently no-op an upload).
35
+ */
36
+ resolveProvider: () => StorageProvider;
37
+ /** Key prefix for stored objects. Defaults to `'uploads'`. */
38
+ prefix?: string;
39
+ }
40
+ export declare function objectStorage(config: ObjectStorageConfig): Plugin;
41
+ //# sourceMappingURL=object-storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"object-storage.d.ts","sourceRoot":"","sources":["../../src/storage/object-storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,MAAM,WAAW,mBAAmB;IAClC,2FAA2F;IAC3F,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kEAAkE;IAClE,WAAW,CAAC,EAAE;QACZ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;IACF;;;;;;OAMG;IACH,eAAe,EAAE,MAAM,eAAe,CAAC;IACvC,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,mBAAmB,GAAG,MAAM,CAiGjE"}
@@ -1,13 +1,35 @@
1
1
  /**
2
- * Vercel Blob Storage Plugin
2
+ * Object-storage upload plugin (provider-agnostic).
3
+ *
4
+ * Adapts any `StorageProvider` (Cloudflare R2 — canonical — Vercel Blob, mock,
5
+ * or a future backend) to the RevealUI engine's collection upload-adapter
6
+ * interface, so `apps/admin/revealui.config.ts` stores media through the
7
+ * configured object-storage backend. Replaces the provider-specific
8
+ * `vercelBlobStorage` Payload plugin retired in GAP-208 Phase 4.
9
+ *
10
+ * The backend is resolved through a lazy `resolveProvider` thunk — invoked
11
+ * (and memoized) on the first upload/delete, never at config-build time — so a
12
+ * consumer reading validated config (e.g. `@revealui/config`'s `config.storage`)
13
+ * does NOT force env validation when the admin config module is imported during
14
+ * `next build`, tests, or boot. This mirrors `apps/server` `getMediaStorage()`,
15
+ * which reads `config.storage` lazily per request.
3
16
  *
4
17
  * WARNING: This module is server-only.
5
18
  * Do NOT import in client-side code or edge runtime.
6
19
  */
7
- import { del, put } from '@vercel/blob';
8
20
  import { defaultLogger } from '../instance/logger.js';
9
- export function vercelBlobStorage(config) {
21
+ export function objectStorage(config) {
10
22
  const prefix = config.prefix || 'uploads';
23
+ // Memoize the provider so the underlying client (S3 / Blob) is constructed
24
+ // once per process, not per upload — and so config is read lazily, on first
25
+ // use, rather than at config-build time.
26
+ let cachedProvider;
27
+ const getProvider = () => {
28
+ if (!cachedProvider) {
29
+ cachedProvider = config.resolveProvider();
30
+ }
31
+ return cachedProvider;
32
+ };
11
33
  return (incomingConfig) => {
12
34
  if (config.enabled === false) {
13
35
  return incomingConfig;
@@ -26,25 +48,21 @@ export function vercelBlobStorage(config) {
26
48
  crop: true,
27
49
  adapters: [
28
50
  {
29
- name: 'vercel-blob',
51
+ name: 'object-storage',
30
52
  adapter: {
31
- name: 'vercel-blob',
53
+ name: 'object-storage',
32
54
  generateURL: (file) => {
33
55
  return `${prefix}/${file.filename}`;
34
56
  },
35
57
  upload: async (file) => {
36
58
  try {
37
- const filePath = `${prefix}/${collection.slug}/${Date.now()}-${file.name}`;
38
- if (!config.token) {
39
- throw new Error('Vercel blob token is required but not configured');
40
- }
41
- const blob = await put(filePath, file.data, {
59
+ const key = `${prefix}/${collection.slug}/${Date.now()}-${file.name}`;
60
+ const result = await getProvider().put(key, file.data, {
42
61
  access: 'public',
43
- token: config.token,
44
- addRandomSuffix: false,
62
+ contentType: file.mimetype,
45
63
  });
46
64
  return {
47
- url: blob.url,
65
+ url: result.url,
48
66
  filename: file.name,
49
67
  filesize: file.size,
50
68
  mimeType: file.mimetype,
@@ -53,20 +71,22 @@ export function vercelBlobStorage(config) {
53
71
  };
54
72
  }
55
73
  catch (error) {
56
- defaultLogger.error('Vercel Blob upload error:', error);
74
+ defaultLogger.error('Object storage upload error:', error);
57
75
  throw error;
58
76
  }
59
77
  },
60
78
  delete: async (filename) => {
61
79
  try {
62
- // Extract the blob URL from filename or construct it
63
- const blobUrl = filename.startsWith('http')
80
+ // Payload may pass a full URL (preferred providers extract
81
+ // the storage key from it) or a bare filename; mirror the
82
+ // legacy plugin's prefix-prepend for the bare-filename case.
83
+ const keyOrUrl = filename.startsWith('http')
64
84
  ? filename
65
85
  : `${prefix}/${filename}`;
66
- await del(blobUrl);
86
+ await getProvider().del(keyOrUrl);
67
87
  }
68
88
  catch (error) {
69
- defaultLogger.error('Vercel Blob delete error:', error);
89
+ defaultLogger.error('Object storage delete error:', error);
70
90
  throw error;
71
91
  }
72
92
  },
@@ -1,12 +1,24 @@
1
1
  /**
2
2
  * Cloudflare R2 implementation of StorageProvider.
3
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/.
4
+ * R2 is S3-compatible. This is a native client: AWS SigV4 request signing
5
+ * (./_sigv4.ts, verified against AWS's official test vector) over global
6
+ * `fetch`, plus a small XML reader for ListObjectsV2 (./_xml.ts). No SDK
7
+ * dependency — only node:crypto + fetch.
8
+ *
9
+ * Path-style addressing is used for the S3 API calls
10
+ * (https://<account>.r2.cloudflarestorage.com/<bucket>/<key>); this has no
11
+ * effect on the public object URLs, which are built from `publicBaseUrl`.
6
12
  *
7
13
  * GAP-208 Phase 2a (2026-05-18). Phase 1 (interface + types) shipped in #959.
14
+ * Native client (dropping @aws-sdk/client-s3) landed 2026-06-09.
8
15
  *
9
16
  * Server-only. Do NOT import from client-side code or edge runtime.
17
+ *
18
+ * Known limitation (unchanged from the SDK version): request bodies are
19
+ * buffered fully in memory (see toUint8Array) so the payload can be hashed for
20
+ * SigV4. Streaming uploads (STREAMING-AWS4-HMAC-SHA256-PAYLOAD) are a future
21
+ * enhancement; presigned/private URLs remain Phase 2b.
10
22
  */
11
23
  import type { R2Config, StorageProvider } from './types.js';
12
24
  export declare function createR2Provider(config: R2Config): StorageProvider;
@@ -1 +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"}
1
+ {"version":3,"file":"r2.d.ts","sourceRoot":"","sources":["../../src/storage/r2.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAKH,OAAO,KAAK,EAMV,QAAQ,EACR,eAAe,EAChB,MAAM,YAAY,CAAC;AAkKpB,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,QAAQ,GAAG,eAAe,CAElE"}