@crombolo/next-integration 0.0.0-stage → 0.37.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.
@@ -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.37.0",
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.37.0",
73
+ "@crombolo/delivery-client": "0.37.0"
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.