@crombolo/next-integration 0.0.0-stage → 0.38.1

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.
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Route handlers a Next.js app mounts (story 208): Draft Mode on and off from
3
+ * the CMS's Preview button, revalidation from its webhook, a member's sign-in
4
+ * and sign-out with PKCE, media fetched with the member's token, and the CMS's
5
+ * changes for live reload in development (story 214).
6
+ *
7
+ * ```ts
8
+ * // web/app/api/crombolo/preview/route.ts
9
+ * export const GET = previewHandler(crombolo)
10
+ * ```
11
+ */
12
+ import { draftMode } from 'next/headers';
13
+ import { challengeFor, cookie, PREVIEW_COOKIE, randomToken, readCookie, SESSION_COOKIE, SIGN_IN_COOKIE, sameSecret, seal, sessionSecret, unseal, } from './session.js';
14
+ /** Where member sign-in lives on the CMS. */
15
+ export const MEMBER_SECURITY_PATH = '/crombolo/delivery/api/v1/security/member';
16
+ const CLIENT_ID = 'umbraco-member';
17
+ /** A path on this app, or `/`: never another site, so a link cannot send anyone away. */
18
+ export function localPath(value) {
19
+ if (!value?.startsWith('/') || value.startsWith('//') || value.startsWith('/\\'))
20
+ return '/';
21
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: control characters are what it refuses.
22
+ return /[\u0000-\u001f\u007f]/.test(value) ? '/' : value;
23
+ }
24
+ const secure = (request) => new URL(request.url).protocol === 'https:';
25
+ function redirectTo(location, cookies = [], status = 307) {
26
+ const headers = new Headers({ location, 'cache-control': 'no-store' });
27
+ for (const value of cookies)
28
+ headers.append('set-cookie', value);
29
+ return new Response(null, { status, headers });
30
+ }
31
+ /**
32
+ * `GET ?path=…&token=…`, what the Preview button opens: the token is checked
33
+ * against the CMS, Draft Mode is turned on, and the editor lands on the page.
34
+ */
35
+ export function previewHandler(crombolo) {
36
+ return async (request) => {
37
+ const url = new URL(request.url);
38
+ const token = url.searchParams.get('token');
39
+ const path = localPath(url.searchParams.get('path'));
40
+ if (!token)
41
+ return new Response('A preview token is required.', { status: 401 });
42
+ // There is no endpoint that checks a token; a route lookup with it answers 401 when it is bad.
43
+ const lookup = new URL(`${crombolo.options.url.replace(/\/+$/, '')}${crombolo.options.apiPath ?? '/crombolo/delivery/api/v2'}/route`);
44
+ lookup.searchParams.set('path', path);
45
+ if (crombolo.options.host)
46
+ lookup.searchParams.set('host', crombolo.options.host);
47
+ const checked = await fetch(lookup, {
48
+ headers: {
49
+ preview: 'true',
50
+ 'preview-token': token,
51
+ accept: 'application/json',
52
+ },
53
+ cache: 'no-store',
54
+ });
55
+ if (checked.status === 401 || checked.status === 403)
56
+ return new Response('This preview link has expired. Press Preview again.', { status: 401 });
57
+ (await draftMode()).enable();
58
+ return redirectTo(path, [
59
+ cookie(PREVIEW_COOKIE, token, { secure: secure(request), maxAge: 60 * 60 }),
60
+ ]);
61
+ };
62
+ }
63
+ /** `GET ?path=…`: leaves Draft Mode and forgets the preview token. */
64
+ export function exitPreviewHandler() {
65
+ return async (request) => {
66
+ ;
67
+ (await draftMode()).disable();
68
+ return redirectTo(localPath(new URL(request.url).searchParams.get('path')), [
69
+ cookie(PREVIEW_COOKIE, '', { secure: secure(request), maxAge: 0 }),
70
+ ]);
71
+ };
72
+ }
73
+ /**
74
+ * `POST` from a CMS webhook with `payload = "crombolo"`: checks the shared
75
+ * secret header and clears what holds the item and each of its ancestors.
76
+ */
77
+ export function revalidateHandler(crombolo, options = {}) {
78
+ return async (request) => {
79
+ const secret = options.secret ?? process.env.CROMBOLO_REVALIDATE_SECRET;
80
+ if (!secret)
81
+ return new Response('CROMBOLO_REVALIDATE_SECRET is not set.', { status: 500 });
82
+ if (!(await sameSecret(request.headers.get(options.header ?? 'x-crombolo-secret'), secret)))
83
+ return new Response('Unauthorized', { status: 401 });
84
+ let body;
85
+ try {
86
+ body = (await request.json());
87
+ }
88
+ catch {
89
+ return new Response('Expected the webhook payload as JSON.', { status: 400 });
90
+ }
91
+ const keys = [body.Id, ...(Array.isArray(body.Ancestors) ? body.Ancestors : [])].filter((key) => typeof key === 'string' && /^[0-9a-f-]{36}$/i.test(key));
92
+ if (keys.length === 0)
93
+ return new Response('The payload names no item.', { status: 400 });
94
+ crombolo.revalidate(keys);
95
+ announceRevalidation(keys);
96
+ return Response.json({ revalidated: keys });
97
+ };
98
+ }
99
+ const sessionFrom = (answer) => ({
100
+ accessToken: answer.access_token,
101
+ ...(answer.refresh_token ? { refreshToken: answer.refresh_token } : {}),
102
+ expiresAt: Date.now() + (answer.expires_in ?? 300) * 1000,
103
+ });
104
+ /** Asks the CMS's token endpoint, by authorization code or refresh token. */
105
+ export async function requestTokens(crombolo, form) {
106
+ const response = await fetch(`${crombolo.options.url.replace(/\/+$/, '')}${MEMBER_SECURITY_PATH}/token`, {
107
+ method: 'POST',
108
+ headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },
109
+ body: new URLSearchParams({ client_id: CLIENT_ID, ...form }),
110
+ cache: 'no-store',
111
+ });
112
+ if (!response.ok)
113
+ return undefined;
114
+ return sessionFrom((await response.json()));
115
+ }
116
+ /** Sign-in, its callback, and sign-out, for a member of the site. */
117
+ export function memberHandlers(crombolo, options) {
118
+ const cms = crombolo.options.url.replace(/\/+$/, '');
119
+ const secretOf = () => sessionSecret(crombolo.options.secret ?? process.env.CROMBOLO_NEXT_SECRET);
120
+ return {
121
+ /** `GET ?returnTo=/path`: off to the CMS to sign in. */
122
+ async signIn(request) {
123
+ const state = randomToken();
124
+ const verifier = randomToken(48);
125
+ const pending = {
126
+ state,
127
+ verifier,
128
+ returnTo: localPath(new URL(request.url).searchParams.get('returnTo')),
129
+ };
130
+ const authorize = new URL(`${cms}${MEMBER_SECURITY_PATH}/authorize`);
131
+ authorize.search = new URLSearchParams({
132
+ client_id: CLIENT_ID,
133
+ redirect_uri: options.redirectUri,
134
+ response_type: 'code',
135
+ scope: 'openid offline_access',
136
+ code_challenge: await challengeFor(verifier),
137
+ code_challenge_method: 'S256',
138
+ state,
139
+ }).toString();
140
+ return redirectTo(authorize.toString(), [
141
+ cookie(SIGN_IN_COOKIE, await seal(pending, secretOf()), {
142
+ secure: secure(request),
143
+ maxAge: 10 * 60,
144
+ }),
145
+ ]);
146
+ },
147
+ /** `GET ?code=…&state=…`: the tokens, sealed into the session cookie. */
148
+ async callback(request) {
149
+ const url = new URL(request.url);
150
+ const pending = await unseal(readCookie(request.headers.get('cookie'), SIGN_IN_COOKIE), secretOf());
151
+ const code = url.searchParams.get('code');
152
+ const done = cookie(SIGN_IN_COOKIE, '', { secure: secure(request), maxAge: 0 });
153
+ if (!pending || !code || url.searchParams.get('state') !== pending.state)
154
+ return new Response('This sign-in has expired. Please sign in again.', {
155
+ status: 400,
156
+ headers: { 'set-cookie': done },
157
+ });
158
+ const session = await requestTokens(crombolo, {
159
+ grant_type: 'authorization_code',
160
+ code,
161
+ redirect_uri: options.redirectUri,
162
+ code_verifier: pending.verifier,
163
+ });
164
+ if (!session)
165
+ return new Response('Signing in did not complete. Please try again.', {
166
+ status: 400,
167
+ headers: { 'set-cookie': done },
168
+ });
169
+ return redirectTo(pending.returnTo, [
170
+ done,
171
+ cookie(SESSION_COOKIE, await seal(session, secretOf()), {
172
+ secure: secure(request),
173
+ maxAge: 60 * 60 * 24 * 30,
174
+ }),
175
+ ]);
176
+ },
177
+ /** `GET`: ends the member's tokens and their CMS session, then comes back. */
178
+ async signOut(request) {
179
+ const session = await unseal(readCookie(request.headers.get('cookie'), SESSION_COOKIE), secretOf());
180
+ if (session?.refreshToken)
181
+ await fetch(`${cms}${MEMBER_SECURITY_PATH}/revoke`, {
182
+ method: 'POST',
183
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
184
+ body: new URLSearchParams({ client_id: CLIENT_ID, token: session.refreshToken }),
185
+ cache: 'no-store',
186
+ }).catch(() => undefined);
187
+ const signout = new URL(`${cms}${MEMBER_SECURITY_PATH}/signout`);
188
+ signout.search = new URLSearchParams({
189
+ post_logout_redirect_uri: options.postLogoutRedirectUri,
190
+ }).toString();
191
+ return redirectTo(signout.toString(), [
192
+ cookie(SESSION_COOKIE, '', { secure: secure(request), maxAge: 0 }),
193
+ ]);
194
+ },
195
+ };
196
+ }
197
+ /**
198
+ * `GET /media/…`: the CMS's media file, sent with the member's token when one
199
+ * is signed in, so protected media reaches who may read it. Mount it at
200
+ * `web/app/media/[...path]/route.ts`.
201
+ */
202
+ export function mediaHandler(crombolo) {
203
+ const cms = crombolo.options.url.replace(/\/+$/, '');
204
+ return async (request) => {
205
+ const url = new URL(request.url);
206
+ const headers = new Headers();
207
+ for (const name of ['accept', 'if-none-match', 'if-modified-since', 'range'])
208
+ if (request.headers.has(name))
209
+ headers.set(name, request.headers.get(name));
210
+ const secret = crombolo.options.secret ?? process.env.CROMBOLO_NEXT_SECRET;
211
+ const session = secret && secret.length >= 32
212
+ ? await unseal(readCookie(request.headers.get('cookie'), SESSION_COOKIE), secret)
213
+ : undefined;
214
+ if (session && session.expiresAt > Date.now())
215
+ headers.set('authorization', `Bearer ${session.accessToken}`);
216
+ const answer = await fetch(`${cms}${url.pathname}${url.search}`, {
217
+ headers,
218
+ cache: 'no-store',
219
+ signal: request.signal,
220
+ });
221
+ const out = new Headers();
222
+ for (const name of [
223
+ 'content-type',
224
+ 'content-length',
225
+ 'cache-control',
226
+ 'etag',
227
+ 'last-modified',
228
+ 'content-range',
229
+ 'accept-ranges',
230
+ ])
231
+ if (answer.headers.has(name))
232
+ out.set(name, answer.headers.get(name));
233
+ // An answer to a member's token is theirs alone.
234
+ if (headers.has('authorization'))
235
+ out.set('cache-control', 'private, no-store');
236
+ return new Response(answer.body, { status: answer.status, headers: out });
237
+ };
238
+ }
239
+ /** Where the CMS streams its changes in development. */
240
+ export const CMS_LIVE_EVENTS_PATH = '/crombolo/dev/events';
241
+ /**
242
+ * On the global object: Next.js may bundle this module once per route, and
243
+ * the revalidate route must reach the live route's listeners.
244
+ */
245
+ const REVALIDATED = Symbol.for('crombolo.next.revalidated');
246
+ function revalidationListeners() {
247
+ const holder = globalThis;
248
+ holder[REVALIDATED] ??= new Set();
249
+ return holder[REVALIDATED];
250
+ }
251
+ function announceRevalidation(keys) {
252
+ for (const listener of revalidationListeners())
253
+ listener(keys);
254
+ }
255
+ const encoder = new TextEncoder();
256
+ /**
257
+ * `GET` for `<CromboloLiveReload />` in development: the CMS's change stream,
258
+ * and a `revalidated` event once this app has cleared what a webhook named, so
259
+ * a refresh after it shows the new content. 404 outside development.
260
+ */
261
+ export function liveReloadHandler(crombolo) {
262
+ return async (request) => {
263
+ if (process.env.NODE_ENV !== 'development')
264
+ return new Response('Not Found', { status: 404 });
265
+ const upstream = await fetch(new URL(CMS_LIVE_EVENTS_PATH, crombolo.options.url), {
266
+ headers: { accept: 'text/event-stream' },
267
+ signal: request.signal,
268
+ }).catch(() => undefined);
269
+ if (!upstream?.ok || !upstream.body)
270
+ return new Response('The CMS is not streaming its changes.', { status: 502 });
271
+ const reader = upstream.body.getReader();
272
+ const decoder = new TextDecoder();
273
+ let listener;
274
+ const body = new ReadableStream({
275
+ async start(controller) {
276
+ listener = (keys) => controller.enqueue(encoder.encode(`event: revalidated\ndata: ${JSON.stringify({ keys })}\n\n`));
277
+ revalidationListeners().add(listener);
278
+ // Whole frames only, so a `revalidated` event never lands inside one of the CMS's.
279
+ let buffered = '';
280
+ try {
281
+ for (;;) {
282
+ const { done, value } = await reader.read();
283
+ if (done)
284
+ break;
285
+ buffered += decoder.decode(value, { stream: true });
286
+ const end = buffered.lastIndexOf('\n\n');
287
+ if (end < 0)
288
+ continue;
289
+ controller.enqueue(encoder.encode(buffered.slice(0, end + 2)));
290
+ buffered = buffered.slice(end + 2);
291
+ }
292
+ }
293
+ catch {
294
+ // The page left, or the CMS restarted; the browser reconnects either way.
295
+ }
296
+ finally {
297
+ if (listener)
298
+ revalidationListeners().delete(listener);
299
+ try {
300
+ controller.close();
301
+ }
302
+ catch { }
303
+ }
304
+ },
305
+ cancel() {
306
+ if (listener)
307
+ revalidationListeners().delete(listener);
308
+ void reader.cancel().catch(() => { });
309
+ },
310
+ });
311
+ return new Response(body, {
312
+ headers: { 'content-type': 'text/event-stream; charset=utf-8', 'cache-control': 'no-store' },
313
+ });
314
+ };
315
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Cookies the integration keeps (story 208): a member's tokens and a sign-in in
3
+ * progress, sealed with AES-GCM under `CROMBOLO_NEXT_SECRET`, and the editor's
4
+ * preview token. Web Crypto only, so it runs in Node and at the edge alike.
5
+ */
6
+ export declare const SESSION_COOKIE = "crombolo_member";
7
+ export declare const SIGN_IN_COOKIE = "crombolo_sign_in";
8
+ export declare const PREVIEW_COOKIE = "crombolo_preview";
9
+ export interface MemberSession {
10
+ accessToken: string;
11
+ refreshToken?: string;
12
+ /** When the access token stops working, in milliseconds since the epoch. */
13
+ expiresAt: number;
14
+ }
15
+ export interface SignInState {
16
+ state: string;
17
+ verifier: string;
18
+ returnTo: string;
19
+ }
20
+ /** The secret sessions are sealed with; refused when shorter than 32 characters. */
21
+ export declare function sessionSecret(secret: string | undefined): string;
22
+ /** A value sealed so only a holder of the secret can read or forge it. */
23
+ export declare function seal(value: unknown, secret: string): Promise<string>;
24
+ /** The value back, or undefined when it was tampered with or sealed under another secret. */
25
+ export declare function unseal<T>(sealed: string | undefined, secret: string): Promise<T | undefined>;
26
+ /** A random value for PKCE and `state`. */
27
+ export declare function randomToken(bytes?: number): string;
28
+ /** PKCE's S256 challenge for a verifier. */
29
+ export declare function challengeFor(verifier: string): Promise<string>;
30
+ /** Whether two secrets match, taking the same time whatever they hold. */
31
+ export declare function sameSecret(given: string | null, expected: string): Promise<boolean>;
32
+ /** A cookie's value from a `Cookie` header. */
33
+ export declare function readCookie(header: string | null, name: string): string | undefined;
34
+ /** A `Set-Cookie` value: http-only, `SameSite=Lax`, and `Secure` unless on plain http. */
35
+ export declare function cookie(name: string, value: string, options?: {
36
+ maxAge?: number;
37
+ secure?: boolean;
38
+ path?: string;
39
+ }): string;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Cookies the integration keeps (story 208): a member's tokens and a sign-in in
3
+ * progress, sealed with AES-GCM under `CROMBOLO_NEXT_SECRET`, and the editor's
4
+ * preview token. Web Crypto only, so it runs in Node and at the edge alike.
5
+ */
6
+ export const SESSION_COOKIE = 'crombolo_member';
7
+ export const SIGN_IN_COOKIE = 'crombolo_sign_in';
8
+ export const PREVIEW_COOKIE = 'crombolo_preview';
9
+ /** The secret sessions are sealed with; refused when shorter than 32 characters. */
10
+ export function sessionSecret(secret) {
11
+ if (!secret || secret.length < 32)
12
+ throw new Error('CROMBOLO_NEXT_SECRET must be set to at least 32 characters to keep members signed in.');
13
+ return secret;
14
+ }
15
+ const encoder = new TextEncoder();
16
+ function base64url(bytes) {
17
+ let text = '';
18
+ for (const byte of bytes)
19
+ text += String.fromCharCode(byte);
20
+ return btoa(text).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '');
21
+ }
22
+ function fromBase64url(value) {
23
+ const text = atob(value.replaceAll('-', '+').replaceAll('_', '/'));
24
+ const bytes = new Uint8Array(text.length);
25
+ for (let i = 0; i < text.length; i++)
26
+ bytes[i] = text.charCodeAt(i);
27
+ return bytes;
28
+ }
29
+ async function keyFor(secret) {
30
+ const digest = await crypto.subtle.digest('SHA-256', encoder.encode(secret));
31
+ return crypto.subtle.importKey('raw', digest, 'AES-GCM', false, ['encrypt', 'decrypt']);
32
+ }
33
+ /** A value sealed so only a holder of the secret can read or forge it. */
34
+ export async function seal(value, secret) {
35
+ const iv = crypto.getRandomValues(new Uint8Array(12));
36
+ const sealed = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, await keyFor(secret), encoder.encode(JSON.stringify(value)));
37
+ return `${base64url(iv)}.${base64url(new Uint8Array(sealed))}`;
38
+ }
39
+ /** The value back, or undefined when it was tampered with or sealed under another secret. */
40
+ export async function unseal(sealed, secret) {
41
+ if (!sealed)
42
+ return undefined;
43
+ const [iv, data] = sealed.split('.');
44
+ if (!iv || !data)
45
+ return undefined;
46
+ try {
47
+ const opened = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: fromBase64url(iv) }, await keyFor(secret), fromBase64url(data));
48
+ return JSON.parse(new TextDecoder().decode(opened));
49
+ }
50
+ catch {
51
+ return undefined;
52
+ }
53
+ }
54
+ /** A random value for PKCE and `state`. */
55
+ export function randomToken(bytes = 32) {
56
+ return base64url(crypto.getRandomValues(new Uint8Array(bytes)));
57
+ }
58
+ /** PKCE's S256 challenge for a verifier. */
59
+ export async function challengeFor(verifier) {
60
+ return base64url(new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(verifier))));
61
+ }
62
+ /** Whether two secrets match, taking the same time whatever they hold. */
63
+ export async function sameSecret(given, expected) {
64
+ if (!given)
65
+ return false;
66
+ const [a, b] = await Promise.all([given, expected].map(async (v) => new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(v)))));
67
+ const left = a;
68
+ const right = b;
69
+ let difference = 0;
70
+ for (let i = 0; i < left.length; i++)
71
+ difference |= left[i] ^ right[i];
72
+ return difference === 0;
73
+ }
74
+ /** A cookie's value from a `Cookie` header. */
75
+ export function readCookie(header, name) {
76
+ for (const part of (header ?? '').split(';')) {
77
+ const at = part.indexOf('=');
78
+ if (at > 0 && part.slice(0, at).trim() === name)
79
+ return decodeURIComponent(part.slice(at + 1).trim());
80
+ }
81
+ return undefined;
82
+ }
83
+ /** A `Set-Cookie` value: http-only, `SameSite=Lax`, and `Secure` unless on plain http. */
84
+ export function cookie(name, value, options = {}) {
85
+ const parts = [
86
+ `${name}=${encodeURIComponent(value)}`,
87
+ `Path=${options.path ?? '/'}`,
88
+ 'HttpOnly',
89
+ 'SameSite=Lax',
90
+ ];
91
+ if (options.maxAge !== undefined)
92
+ parts.push(`Max-Age=${Math.max(0, Math.floor(options.maxAge))}`);
93
+ if (options.secure !== false)
94
+ parts.push('Secure');
95
+ return parts.join('; ');
96
+ }
package/package.json CHANGED
@@ -1,6 +1,88 @@
1
1
  {
2
2
  "name": "@crombolo/next-integration",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.38.1",
4
+ "description": "A Next.js App Router front end for a crombolo site: pages, metadata, preview, revalidation, members, images and content components.",
5
+ "license": "MIT",
6
+ "author": "Crombolo",
7
+ "homepage": "https://github.com/kineco-au/crombolo-cms#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/kineco-au/crombolo-cms.git",
11
+ "directory": "packages/next-integration"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/kineco-au/crombolo-cms/issues"
15
+ },
16
+ "keywords": [
17
+ "crombolo",
18
+ "nextjs",
19
+ "headless"
20
+ ],
21
+ "engines": {
22
+ "bun": ">=1.3.0"
23
+ },
24
+ "type": "module",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js",
29
+ "default": "./dist/index.js"
30
+ },
31
+ "./cache-16": {
32
+ "types": "./dist/cache-16.d.ts",
33
+ "import": "./dist/cache-16.js",
34
+ "default": "./dist/cache-16.js"
35
+ },
36
+ "./cache-15": {
37
+ "types": "./dist/cache-15.d.ts",
38
+ "import": "./dist/cache-15.js",
39
+ "default": "./dist/cache-15.js"
40
+ },
41
+ "./route-handlers": {
42
+ "types": "./dist/route-handlers.d.ts",
43
+ "import": "./dist/route-handlers.js",
44
+ "default": "./dist/route-handlers.js"
45
+ },
46
+ "./middleware": {
47
+ "types": "./dist/middleware.d.ts",
48
+ "import": "./dist/middleware.js",
49
+ "default": "./dist/middleware.js"
50
+ },
51
+ "./components": {
52
+ "types": "./dist/components.d.ts",
53
+ "import": "./dist/components.js",
54
+ "default": "./dist/components.js"
55
+ },
56
+ "./client": {
57
+ "types": "./dist/client.d.ts",
58
+ "import": "./dist/client.js",
59
+ "default": "./dist/client.js"
60
+ },
61
+ "./image-loader": {
62
+ "types": "./dist/image-loader.d.ts",
63
+ "import": "./dist/image-loader.js",
64
+ "default": "./dist/image-loader.js"
65
+ }
66
+ },
67
+ "files": [
68
+ "dist",
69
+ "NOTICE"
70
+ ],
71
+ "dependencies": {
72
+ "@crombolo/common": "0.38.1",
73
+ "@crombolo/delivery-client": "0.38.1"
74
+ },
75
+ "peerDependencies": {
76
+ "next": ">=15.5 <17",
77
+ "react": ">=19"
78
+ },
79
+ "devDependencies": {
80
+ "@types/react": "19.3.0",
81
+ "next": "16.4.3",
82
+ "react": "19.3.1",
83
+ "react-dom": "19.3.1"
84
+ },
85
+ "publishConfig": {
86
+ "access": "public"
87
+ }
88
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.