@tidecloak/create-nextjs 0.13.31 → 0.13.32

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tidecloak/create-nextjs",
3
- "version": "0.13.31",
3
+ "version": "0.13.32",
4
4
  "type": "module",
5
5
  "description": "Scaffold a TideCloak-ready Next.js app with optional IAM setup and working auth - start building instantly with a live example",
6
6
  "bin": {
@@ -0,0 +1,35 @@
1
+ import fs from "fs"
2
+ import path from "path"
3
+
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+ // Tide DPoP resource-server endpoint.
6
+ //
7
+ // DPoP is enabled by default, and the SDK's DPoP flow loads this page from your
8
+ // own origin at:
9
+ // /tide_dpop/iss/<issuer-hex>/aud/<client-hex>/tide_dpop_auth.html
10
+ //
11
+ // This catch-all route serves the single bundled `public/tide_dpop_auth.html`
12
+ // for any such path, with the two response headers Tide requires:
13
+ // • Content-Security-Policy — the sha256 hashes pin the file's inline
14
+ // script/style (so only that exact code runs).
15
+ // • Allow-CSP-From: * — lets the ORK embed this page cross-origin.
16
+ // ─────────────────────────────────────────────────────────────────────────────
17
+
18
+ const htmlPath = path.join(process.cwd(), "public", "tide_dpop_auth.html")
19
+
20
+ const CSP =
21
+ "default-src 'self'; " +
22
+ "script-src 'self' 'sha256-utc6UrebuHOyLd/2aiMXS/p1EDy9UZBDe/XEMKDw9Mc='; " +
23
+ "style-src 'self' 'sha256-1tYy8m3c1KLuGI2eID9TfLkc50Y+iSPJMpI7n/apN/w=' 'sha256-F7OJTdJYct4J+cQfuJUoDauitndqt8pAc8EbA8gwDPU='"
24
+
25
+ export async function GET() {
26
+ const html = fs.readFileSync(htmlPath, "utf-8")
27
+ return new Response(html, {
28
+ status: 200,
29
+ headers: {
30
+ "Content-Type": "text/html",
31
+ "Content-Security-Policy": CSP,
32
+ "Allow-CSP-From": "*",
33
+ },
34
+ })
35
+ }
@@ -12,6 +12,6 @@
12
12
  "next": "16.x",
13
13
  "react": "19.x",
14
14
  "react-dom": "19.x",
15
- "@tidecloak/nextjs": "^0.13.31"
15
+ "@tidecloak/nextjs": "^0.13.32"
16
16
  }
17
17
  }
@@ -0,0 +1,238 @@
1
+ <html>
2
+
3
+ <head>
4
+ <style>
5
+ body {
6
+ margin: 0;
7
+ display: flex;
8
+ align-items: center;
9
+ justify-content: center;
10
+ height: 100vh;
11
+ background: #f6f6f7;
12
+ font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
13
+ }
14
+
15
+ .status {
16
+ text-align: center;
17
+ opacity: 0;
18
+ }
19
+
20
+ .status.visible {
21
+ opacity: 1;
22
+ }
23
+
24
+ .checkmark-circle {
25
+ width: 72px;
26
+ height: 72px;
27
+ margin: 0 auto 16px;
28
+ }
29
+
30
+ .checkmark-circle circle {
31
+ fill: none;
32
+ stroke: #039855;
33
+ stroke-width: 3;
34
+ stroke-dasharray: 201;
35
+ stroke-dashoffset: 201;
36
+ }
37
+
38
+ .checkmark-circle path {
39
+ fill: none;
40
+ stroke: #039855;
41
+ stroke-width: 3;
42
+ stroke-linecap: round;
43
+ stroke-linejoin: round;
44
+ stroke-dasharray: 50;
45
+ stroke-dashoffset: 50;
46
+ }
47
+
48
+ .status.visible .checkmark-circle circle {
49
+ animation: circle-draw 0.5s ease forwards;
50
+ }
51
+
52
+ .status.visible .checkmark-circle path {
53
+ animation: check-draw 0.3s 0.4s ease forwards;
54
+ }
55
+
56
+ .status.visible .status-text {
57
+ animation: fade-in 0.3s 0.6s ease forwards;
58
+ }
59
+
60
+ .status-text {
61
+ font-size: 15px;
62
+ color: #464647;
63
+ margin: 0;
64
+ opacity: 0;
65
+ }
66
+
67
+ @keyframes circle-draw {
68
+ to {
69
+ stroke-dashoffset: 0;
70
+ }
71
+ }
72
+
73
+ @keyframes check-draw {
74
+ to {
75
+ stroke-dashoffset: 0;
76
+ }
77
+ }
78
+
79
+ @keyframes fade-in {
80
+ to {
81
+ opacity: 1;
82
+ }
83
+ }
84
+ </style>
85
+ </head>
86
+
87
+ <body>
88
+ <div class="status" id="status">
89
+ <svg class="checkmark-circle" viewBox="0 0 72 72">
90
+ <circle cx="36" cy="36" r="32" />
91
+ <path d="M22 36 l10 10 l18 -20" />
92
+ </svg>
93
+ <p class="status-text">Session verified</p>
94
+ </div>
95
+ <script>
96
+ const thisVersion = 1;
97
+
98
+ const hexToStr = (hex) => {
99
+ if (!hex || hex.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hex)) throw Error("Invalid hex string");
100
+ const bytes = new Uint8Array(hex.length / 2);
101
+ for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
102
+ return new TextDecoder().decode(bytes);
103
+ };
104
+
105
+ const createDbName = async (issuer, clientId) => {
106
+ const encoder = new TextEncoder();
107
+ const issuerHash = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(issuer))).slice(0, 8))
108
+ .map(b => b.toString(16).padStart(2, '0')).join('');
109
+ const clientHash = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(clientId))).slice(0, 8))
110
+ .map(b => b.toString(16).padStart(2, '0')).join('');
111
+ return `dpop:${issuerHash}:${clientHash}`;
112
+ };
113
+
114
+ // Determine target window: popup has window.opener, iframe has window.parent
115
+ const isPopup = !!window.opener;
116
+ const targetWindow = window.opener || window.parent;
117
+
118
+ if (!targetWindow || targetWindow === window) {
119
+ document.body.textContent = "Error: No parent or opener window found.";
120
+ throw Error("No target window available for postMessage");
121
+ }
122
+
123
+ const urlParams = new URL(window.location.href).searchParams;
124
+ const enclaveVersion = urlParams.get("version");
125
+ if (!enclaveVersion) throw Error("No enclave version provided in url");
126
+ let openerOrigin = urlParams.get("openerOrigin");
127
+ if (!openerOrigin) throw Error("No opener origin provided in url");
128
+ openerOrigin = new URL(decodeURIComponent(openerOrigin)).origin;
129
+
130
+ const showSuccess = () => {
131
+ document.getElementById('status').classList.add('visible');
132
+ };
133
+
134
+ const version1Flow = async () => {
135
+ const paths = new URL(window.location.href).pathname.split("/");
136
+ const iss = hexToStr(paths[paths.indexOf("iss") + 1]);
137
+ const aud = hexToStr(paths[paths.indexOf("aud") + 1]);
138
+ const dbname = await createDbName(iss, aud);
139
+ let dpopKey;
140
+ try {
141
+ // Chrome 125+: requestStorageAccess({ indexedDB: true }) returns a handle
142
+ // whose .indexedDB factory points directly at the unpartitioned bucket.
143
+ // If permission was already granted (within 30 days) this resolves
144
+ // automatically with no prompt.
145
+ //
146
+ // In popup mode we have first-party context so we can access IndexedDB directly.
147
+ let idbFactory;
148
+ if (isPopup) {
149
+ idbFactory = window.indexedDB;
150
+ } else {
151
+ const handle = await document.requestStorageAccess({ indexedDB: true });
152
+ idbFactory = handle.indexedDB;
153
+ }
154
+
155
+ const db = await new Promise((resolve, reject) => {
156
+ const req = idbFactory.open(dbname);
157
+ req.onsuccess = () => resolve(req.result);
158
+ req.onerror = () => reject(req.error);
159
+ });
160
+ dpopKey = await new Promise((resolve, reject) => {
161
+ const tx = db.transaction("main", "readonly");
162
+ const store = tx.objectStore("main");
163
+ const req = store.get("dpopState");
164
+ req.onsuccess = () => resolve(req.result);
165
+ req.onerror = () => reject(req.error);
166
+ });
167
+ db.close();
168
+ if (!dpopKey || !dpopKey.keys) throw Error("No DPoP key found in IndexedDB");
169
+ } catch (ex) {
170
+ const isStorageBlocked =
171
+ ex?.name === "NotAllowedError" ||
172
+ (ex instanceof DOMException && (
173
+ ex.name === "SecurityError" ||
174
+ ex.name === "UnknownError" ||
175
+ ex.message?.includes("not a known object store name")
176
+ ));
177
+ const detail = isStorageBlocked
178
+ ? "Browser is blocking third-party storage. Please allow storage access or disable strict tracking protection."
179
+ : "Failed to access DPoP key from IndexedDB.";
180
+ targetWindow.postMessage({ type: "error", message: "db failed", detail }, openerOrigin);
181
+ document.body.textContent = detail;
182
+ throw ex;
183
+ }
184
+
185
+ const challenge = () => new Promise((resolve) => {
186
+ const handler = (event) => {
187
+ if (event.origin !== openerOrigin) return;
188
+ if (event.data.type === "challenge") {
189
+ resolve(event.data.challenge);
190
+ window.removeEventListener("message", handler);
191
+ }
192
+ };
193
+ window.addEventListener("message", handler, false);
194
+ });
195
+
196
+ const pre_challenge = challenge();
197
+
198
+ targetWindow.postMessage({ type: "pageLoaded", message: "yay" }, openerOrigin);
199
+
200
+ const challengeResp = await pre_challenge;
201
+ if (typeof challengeResp !== "string" || challengeResp?.length != 64) throw Error("Unexpect challenge format");
202
+
203
+ const te = new TextEncoder();
204
+ const signatureInput = te.encode("dpop-auth-challenge:" + challengeResp);
205
+ const algName = dpopKey.keys.privateKey.algorithm.name;
206
+ const signParams = algName === "ECDSA"
207
+ ? { name: "ECDSA", hash: { "P-256": "SHA-256", "P-384": "SHA-384", "P-521": "SHA-512" }[dpopKey.keys.privateKey.algorithm.namedCurve] }
208
+ : { name: algName };
209
+ const signature = await crypto.subtle.sign(signParams, dpopKey.keys.privateKey, signatureInput);
210
+
211
+ const publicJwk = await crypto.subtle.exportKey("jwk", dpopKey.keys.publicKey);
212
+
213
+ targetWindow.postMessage({
214
+ type: "challenge response",
215
+ message: {
216
+ dpop_public: JSON.stringify({ crv: publicJwk.crv, kty: publicJwk.kty, x: publicJwk.x, y: publicJwk.y }),
217
+ signature: btoa(String.fromCharCode(...new Uint8Array(signature)))
218
+ }
219
+ }, openerOrigin);
220
+
221
+ showSuccess();
222
+ };
223
+
224
+ const versionFlows = new Map([[1, version1Flow]]);
225
+ const latestFlow = version1Flow;
226
+
227
+ (async () => {
228
+ if (Number(enclaveVersion) >= thisVersion) await latestFlow();
229
+ else {
230
+ const flow = versionFlows.get(Number(enclaveVersion));
231
+ if (!flow) throw Error("Cannot find supported enclave version: " + enclaveVersion);
232
+ await flow();
233
+ }
234
+ })();
235
+ </script>
236
+ </body>
237
+
238
+ </html>
@@ -0,0 +1,35 @@
1
+ import fs from "fs"
2
+ import path from "path"
3
+
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+ // Tide DPoP resource-server endpoint.
6
+ //
7
+ // DPoP is enabled by default, and the SDK's DPoP flow loads this page from your
8
+ // own origin at:
9
+ // /tide_dpop/iss/<issuer-hex>/aud/<client-hex>/tide_dpop_auth.html
10
+ //
11
+ // This catch-all route serves the single bundled `public/tide_dpop_auth.html`
12
+ // for any such path, with the two response headers Tide requires:
13
+ // • Content-Security-Policy — the sha256 hashes pin the file's inline
14
+ // script/style (so only that exact code runs).
15
+ // • Allow-CSP-From: * — lets the ORK embed this page cross-origin.
16
+ // ─────────────────────────────────────────────────────────────────────────────
17
+
18
+ const htmlPath = path.join(process.cwd(), "public", "tide_dpop_auth.html")
19
+
20
+ const CSP =
21
+ "default-src 'self'; " +
22
+ "script-src 'self' 'sha256-utc6UrebuHOyLd/2aiMXS/p1EDy9UZBDe/XEMKDw9Mc='; " +
23
+ "style-src 'self' 'sha256-1tYy8m3c1KLuGI2eID9TfLkc50Y+iSPJMpI7n/apN/w=' 'sha256-F7OJTdJYct4J+cQfuJUoDauitndqt8pAc8EbA8gwDPU='"
24
+
25
+ export async function GET(): Promise<Response> {
26
+ const html = fs.readFileSync(htmlPath, "utf-8")
27
+ return new Response(html, {
28
+ status: 200,
29
+ headers: {
30
+ "Content-Type": "text/html",
31
+ "Content-Security-Policy": CSP,
32
+ "Allow-CSP-From": "*",
33
+ },
34
+ })
35
+ }
@@ -12,7 +12,7 @@
12
12
  "next": "16.x",
13
13
  "react": "19.x",
14
14
  "react-dom": "19.x",
15
- "@tidecloak/nextjs": "^0.13.31"
15
+ "@tidecloak/nextjs": "^0.13.32"
16
16
  },
17
17
  "devDependencies": {
18
18
  "@types/node": "24.0.13",
@@ -0,0 +1,238 @@
1
+ <html>
2
+
3
+ <head>
4
+ <style>
5
+ body {
6
+ margin: 0;
7
+ display: flex;
8
+ align-items: center;
9
+ justify-content: center;
10
+ height: 100vh;
11
+ background: #f6f6f7;
12
+ font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
13
+ }
14
+
15
+ .status {
16
+ text-align: center;
17
+ opacity: 0;
18
+ }
19
+
20
+ .status.visible {
21
+ opacity: 1;
22
+ }
23
+
24
+ .checkmark-circle {
25
+ width: 72px;
26
+ height: 72px;
27
+ margin: 0 auto 16px;
28
+ }
29
+
30
+ .checkmark-circle circle {
31
+ fill: none;
32
+ stroke: #039855;
33
+ stroke-width: 3;
34
+ stroke-dasharray: 201;
35
+ stroke-dashoffset: 201;
36
+ }
37
+
38
+ .checkmark-circle path {
39
+ fill: none;
40
+ stroke: #039855;
41
+ stroke-width: 3;
42
+ stroke-linecap: round;
43
+ stroke-linejoin: round;
44
+ stroke-dasharray: 50;
45
+ stroke-dashoffset: 50;
46
+ }
47
+
48
+ .status.visible .checkmark-circle circle {
49
+ animation: circle-draw 0.5s ease forwards;
50
+ }
51
+
52
+ .status.visible .checkmark-circle path {
53
+ animation: check-draw 0.3s 0.4s ease forwards;
54
+ }
55
+
56
+ .status.visible .status-text {
57
+ animation: fade-in 0.3s 0.6s ease forwards;
58
+ }
59
+
60
+ .status-text {
61
+ font-size: 15px;
62
+ color: #464647;
63
+ margin: 0;
64
+ opacity: 0;
65
+ }
66
+
67
+ @keyframes circle-draw {
68
+ to {
69
+ stroke-dashoffset: 0;
70
+ }
71
+ }
72
+
73
+ @keyframes check-draw {
74
+ to {
75
+ stroke-dashoffset: 0;
76
+ }
77
+ }
78
+
79
+ @keyframes fade-in {
80
+ to {
81
+ opacity: 1;
82
+ }
83
+ }
84
+ </style>
85
+ </head>
86
+
87
+ <body>
88
+ <div class="status" id="status">
89
+ <svg class="checkmark-circle" viewBox="0 0 72 72">
90
+ <circle cx="36" cy="36" r="32" />
91
+ <path d="M22 36 l10 10 l18 -20" />
92
+ </svg>
93
+ <p class="status-text">Session verified</p>
94
+ </div>
95
+ <script>
96
+ const thisVersion = 1;
97
+
98
+ const hexToStr = (hex) => {
99
+ if (!hex || hex.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hex)) throw Error("Invalid hex string");
100
+ const bytes = new Uint8Array(hex.length / 2);
101
+ for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
102
+ return new TextDecoder().decode(bytes);
103
+ };
104
+
105
+ const createDbName = async (issuer, clientId) => {
106
+ const encoder = new TextEncoder();
107
+ const issuerHash = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(issuer))).slice(0, 8))
108
+ .map(b => b.toString(16).padStart(2, '0')).join('');
109
+ const clientHash = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(clientId))).slice(0, 8))
110
+ .map(b => b.toString(16).padStart(2, '0')).join('');
111
+ return `dpop:${issuerHash}:${clientHash}`;
112
+ };
113
+
114
+ // Determine target window: popup has window.opener, iframe has window.parent
115
+ const isPopup = !!window.opener;
116
+ const targetWindow = window.opener || window.parent;
117
+
118
+ if (!targetWindow || targetWindow === window) {
119
+ document.body.textContent = "Error: No parent or opener window found.";
120
+ throw Error("No target window available for postMessage");
121
+ }
122
+
123
+ const urlParams = new URL(window.location.href).searchParams;
124
+ const enclaveVersion = urlParams.get("version");
125
+ if (!enclaveVersion) throw Error("No enclave version provided in url");
126
+ let openerOrigin = urlParams.get("openerOrigin");
127
+ if (!openerOrigin) throw Error("No opener origin provided in url");
128
+ openerOrigin = new URL(decodeURIComponent(openerOrigin)).origin;
129
+
130
+ const showSuccess = () => {
131
+ document.getElementById('status').classList.add('visible');
132
+ };
133
+
134
+ const version1Flow = async () => {
135
+ const paths = new URL(window.location.href).pathname.split("/");
136
+ const iss = hexToStr(paths[paths.indexOf("iss") + 1]);
137
+ const aud = hexToStr(paths[paths.indexOf("aud") + 1]);
138
+ const dbname = await createDbName(iss, aud);
139
+ let dpopKey;
140
+ try {
141
+ // Chrome 125+: requestStorageAccess({ indexedDB: true }) returns a handle
142
+ // whose .indexedDB factory points directly at the unpartitioned bucket.
143
+ // If permission was already granted (within 30 days) this resolves
144
+ // automatically with no prompt.
145
+ //
146
+ // In popup mode we have first-party context so we can access IndexedDB directly.
147
+ let idbFactory;
148
+ if (isPopup) {
149
+ idbFactory = window.indexedDB;
150
+ } else {
151
+ const handle = await document.requestStorageAccess({ indexedDB: true });
152
+ idbFactory = handle.indexedDB;
153
+ }
154
+
155
+ const db = await new Promise((resolve, reject) => {
156
+ const req = idbFactory.open(dbname);
157
+ req.onsuccess = () => resolve(req.result);
158
+ req.onerror = () => reject(req.error);
159
+ });
160
+ dpopKey = await new Promise((resolve, reject) => {
161
+ const tx = db.transaction("main", "readonly");
162
+ const store = tx.objectStore("main");
163
+ const req = store.get("dpopState");
164
+ req.onsuccess = () => resolve(req.result);
165
+ req.onerror = () => reject(req.error);
166
+ });
167
+ db.close();
168
+ if (!dpopKey || !dpopKey.keys) throw Error("No DPoP key found in IndexedDB");
169
+ } catch (ex) {
170
+ const isStorageBlocked =
171
+ ex?.name === "NotAllowedError" ||
172
+ (ex instanceof DOMException && (
173
+ ex.name === "SecurityError" ||
174
+ ex.name === "UnknownError" ||
175
+ ex.message?.includes("not a known object store name")
176
+ ));
177
+ const detail = isStorageBlocked
178
+ ? "Browser is blocking third-party storage. Please allow storage access or disable strict tracking protection."
179
+ : "Failed to access DPoP key from IndexedDB.";
180
+ targetWindow.postMessage({ type: "error", message: "db failed", detail }, openerOrigin);
181
+ document.body.textContent = detail;
182
+ throw ex;
183
+ }
184
+
185
+ const challenge = () => new Promise((resolve) => {
186
+ const handler = (event) => {
187
+ if (event.origin !== openerOrigin) return;
188
+ if (event.data.type === "challenge") {
189
+ resolve(event.data.challenge);
190
+ window.removeEventListener("message", handler);
191
+ }
192
+ };
193
+ window.addEventListener("message", handler, false);
194
+ });
195
+
196
+ const pre_challenge = challenge();
197
+
198
+ targetWindow.postMessage({ type: "pageLoaded", message: "yay" }, openerOrigin);
199
+
200
+ const challengeResp = await pre_challenge;
201
+ if (typeof challengeResp !== "string" || challengeResp?.length != 64) throw Error("Unexpect challenge format");
202
+
203
+ const te = new TextEncoder();
204
+ const signatureInput = te.encode("dpop-auth-challenge:" + challengeResp);
205
+ const algName = dpopKey.keys.privateKey.algorithm.name;
206
+ const signParams = algName === "ECDSA"
207
+ ? { name: "ECDSA", hash: { "P-256": "SHA-256", "P-384": "SHA-384", "P-521": "SHA-512" }[dpopKey.keys.privateKey.algorithm.namedCurve] }
208
+ : { name: algName };
209
+ const signature = await crypto.subtle.sign(signParams, dpopKey.keys.privateKey, signatureInput);
210
+
211
+ const publicJwk = await crypto.subtle.exportKey("jwk", dpopKey.keys.publicKey);
212
+
213
+ targetWindow.postMessage({
214
+ type: "challenge response",
215
+ message: {
216
+ dpop_public: JSON.stringify({ crv: publicJwk.crv, kty: publicJwk.kty, x: publicJwk.x, y: publicJwk.y }),
217
+ signature: btoa(String.fromCharCode(...new Uint8Array(signature)))
218
+ }
219
+ }, openerOrigin);
220
+
221
+ showSuccess();
222
+ };
223
+
224
+ const versionFlows = new Map([[1, version1Flow]]);
225
+ const latestFlow = version1Flow;
226
+
227
+ (async () => {
228
+ if (Number(enclaveVersion) >= thisVersion) await latestFlow();
229
+ else {
230
+ const flow = versionFlows.get(Number(enclaveVersion));
231
+ if (!flow) throw Error("Cannot find supported enclave version: " + enclaveVersion);
232
+ await flow();
233
+ }
234
+ })();
235
+ </script>
236
+ </body>
237
+
238
+ </html>