@tidecloak/js 0.14.33-staging → 0.14.34-staging
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 +12 -0
- package/dist/cjs/lib/tidecloak.js +19 -15
- package/dist/cjs/lib/tidecloak.js.map +1 -1
- package/dist/cjs/src/IAMService.js +44 -11
- package/dist/cjs/src/IAMService.js.map +1 -1
- package/dist/cjs/src/dpopRoute.js +30 -0
- package/dist/cjs/src/dpopRoute.js.map +1 -0
- package/dist/cjs/src/dpopServer.js +77 -0
- package/dist/cjs/src/dpopServer.js.map +1 -0
- package/dist/cjs/src/generated/dpopAuthAsset.js +7 -0
- package/dist/cjs/src/generated/dpopAuthAsset.js.map +1 -0
- package/dist/cjs/src/policy-react.js +48 -16
- package/dist/cjs/src/policy-react.js.map +1 -1
- package/dist/cjs/src/policy.css +1 -1
- package/dist/cjs/src/utils/dpopAuthPage.js +69 -0
- package/dist/cjs/src/utils/dpopAuthPage.js.map +1 -0
- package/dist/cjs/src/utils/index.js +3 -1
- package/dist/cjs/src/utils/index.js.map +1 -1
- package/dist/cjs/src/vite.js +43 -0
- package/dist/cjs/src/vite.js.map +1 -0
- package/dist/esm/lib/tidecloak.js +19 -15
- package/dist/esm/lib/tidecloak.js.map +1 -1
- package/dist/esm/src/IAMService.js +45 -12
- package/dist/esm/src/IAMService.js.map +1 -1
- package/dist/esm/src/dpopRoute.js +27 -0
- package/dist/esm/src/dpopRoute.js.map +1 -0
- package/dist/esm/src/dpopServer.js +68 -0
- package/dist/esm/src/dpopServer.js.map +1 -0
- package/dist/esm/src/generated/dpopAuthAsset.js +4 -0
- package/dist/esm/src/generated/dpopAuthAsset.js.map +1 -0
- package/dist/esm/src/policy-react.js +44 -2
- package/dist/esm/src/policy-react.js.map +1 -1
- package/dist/esm/src/policy.css +1 -1
- package/dist/esm/src/utils/dpopAuthPage.js +63 -0
- package/dist/esm/src/utils/dpopAuthPage.js.map +1 -0
- package/dist/esm/src/utils/index.js +1 -0
- package/dist/esm/src/utils/index.js.map +1 -1
- package/dist/esm/src/vite.js +40 -0
- package/dist/esm/src/vite.js.map +1 -0
- package/dist/tide_dpop_auth.html +238 -0
- package/dist/types/IAMService.d.ts +21 -8
- package/dist/types/dpopRoute.d.ts +11 -0
- package/dist/types/dpopServer.d.ts +35 -0
- package/dist/types/generated/dpopAuthAsset.d.ts +2 -0
- package/dist/types/policy-react.d.ts +40 -1
- package/dist/types/src/IAMService.d.ts +21 -8
- package/dist/types/src/dpopRoute.d.ts +11 -0
- package/dist/types/src/dpopServer.d.ts +35 -0
- package/dist/types/src/generated/dpopAuthAsset.d.ts +2 -0
- package/dist/types/src/policy-react.d.ts +40 -1
- package/dist/types/src/utils/dpopAuthPage.d.ts +22 -0
- package/dist/types/src/utils/index.d.ts +1 -0
- package/dist/types/src/vite.d.ts +13 -0
- package/dist/types/utils/dpopAuthPage.d.ts +22 -0
- package/dist/types/utils/index.d.ts +1 -0
- package/dist/types/vite.d.ts +13 -0
- package/docs/FRONT_CHANNEL.md +227 -0
- package/docs/HYBRID_MODE.md +185 -0
- package/docs/NATIVE_MODE.md +284 -0
- package/lib/README.md +834 -0
- package/package.json +24 -13
- package/tide_dpop_auth.html +238 -0
|
@@ -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>
|
|
@@ -10,12 +10,14 @@ import TideCloak, { RequestEnclave } from "../lib/tidecloak.js";
|
|
|
10
10
|
* ---
|
|
11
11
|
* ## Front-channel Mode
|
|
12
12
|
*
|
|
13
|
+
* Handlers are called with the event name first, then the event's arguments.
|
|
14
|
+
*
|
|
13
15
|
* Usage A: pass an onReady callback directly
|
|
14
16
|
* ```js
|
|
15
17
|
* import { IAMService } from '@tidecloak/js';
|
|
16
|
-
* import tidecloakConfig from './
|
|
18
|
+
* import tidecloakConfig from './tidecloak.json';
|
|
17
19
|
*
|
|
18
|
-
* IAMService.initIAM(tidecloakConfig, authenticated => {
|
|
20
|
+
* IAMService.initIAM(tidecloakConfig, (_event, authenticated) => {
|
|
19
21
|
* if (!authenticated) IAMService.doLogin();
|
|
20
22
|
* }).catch(console.error);
|
|
21
23
|
* ```
|
|
@@ -23,8 +25,8 @@ import TideCloak, { RequestEnclave } from "../lib/tidecloak.js";
|
|
|
23
25
|
* Usage B: attach multiple listeners, then init
|
|
24
26
|
* ```js
|
|
25
27
|
* IAMService
|
|
26
|
-
* .on('ready', auth => console.log('ready', auth))
|
|
27
|
-
* .on('authError', err => console.error('Auth failed', err));
|
|
28
|
+
* .on('ready', (_event, auth) => console.log('ready', auth))
|
|
29
|
+
* .on('authError', (_event, err) => console.error('Auth failed', err));
|
|
28
30
|
*
|
|
29
31
|
* await IAMService.initIAM(tidecloakConfig);
|
|
30
32
|
* ```
|
|
@@ -85,6 +87,7 @@ import TideCloak, { RequestEnclave } from "../lib/tidecloak.js";
|
|
|
85
87
|
*
|
|
86
88
|
* ---
|
|
87
89
|
* ## Events
|
|
90
|
+
* Every handler receives `(eventName, ...args)`.
|
|
88
91
|
* - `ready` - Emitted when initialization completes (with authenticated boolean)
|
|
89
92
|
* - `initError` - Emitted when initialization fails
|
|
90
93
|
* - `authSuccess` - Emitted on successful authentication
|
|
@@ -146,7 +149,8 @@ declare class IAMService {
|
|
|
146
149
|
_frontchannelInitPromise: Promise<boolean> | null;
|
|
147
150
|
constructor();
|
|
148
151
|
/**
|
|
149
|
-
* Register an event listener.
|
|
152
|
+
* Register an event listener. The handler is called as `handler(event, ...args)`,
|
|
153
|
+
* so the event name comes first, e.g. `on('ready', (_event, authenticated) => ...)`.
|
|
150
154
|
* @param {'ready'|'initError'|'authSuccess'|'authError'|'authRefreshSuccess'|'authRefreshError'|'logout'|'tokenExpired'} event
|
|
151
155
|
* @param {Function} handler
|
|
152
156
|
* @returns {this}
|
|
@@ -181,7 +185,7 @@ declare class IAMService {
|
|
|
181
185
|
* Initialize the TideCloak SSO client with silent SSO check.
|
|
182
186
|
* In hybrid mode, handles the redirect callback if present.
|
|
183
187
|
* @param {Object} config - TideCloak configuration object.
|
|
184
|
-
* @param {Function} [onReady] - Optional
|
|
188
|
+
* @param {Function} [onReady] - Optional 'ready' handler, called as `(event, authenticated)`.
|
|
185
189
|
* @returns {Promise<boolean>} true if authenticated, else false.
|
|
186
190
|
*/
|
|
187
191
|
initIAM(config: Object, onReady?: Function): Promise<boolean>;
|
|
@@ -294,6 +298,13 @@ declare class IAMService {
|
|
|
294
298
|
* @throws {Error} In hybrid mode (tokens are server-side)
|
|
295
299
|
*/
|
|
296
300
|
getValueFromIDToken(key: string): any;
|
|
301
|
+
/**
|
|
302
|
+
* Alias of {@link IAMService#getValueFromIDToken}, with the same casing as the React context.
|
|
303
|
+
* @param {string} key - The name of the claim to retrieve from the ID token's payload.
|
|
304
|
+
* @returns {*} Custom claim from ID token
|
|
305
|
+
* @throws {Error} In hybrid mode (tokens are server-side)
|
|
306
|
+
*/
|
|
307
|
+
getValueFromIdToken(key: string): any;
|
|
297
308
|
/**
|
|
298
309
|
* Refreshes token if expired or about to expire.
|
|
299
310
|
* @returns {Promise<boolean>}
|
|
@@ -317,8 +328,9 @@ declare class IAMService {
|
|
|
317
328
|
/**
|
|
318
329
|
* Encrypt data via adapter.
|
|
319
330
|
* Not available in hybrid mode (encryption requires client-side doken).
|
|
331
|
+
* In native mode, passing a decryption policy throws.
|
|
320
332
|
* @param {{ data: string | Uint8Array, tags: string[] }[]} data - Array of objects to encrypt
|
|
321
|
-
* @param {Uint8Array} decryption_policy Optional policy to protect the encrypted data
|
|
333
|
+
* @param {Uint8Array} [decryption_policy] Optional policy to protect the encrypted data
|
|
322
334
|
* @returns {Promise<(string | Uint8Array)[]>} Array of encrypted values
|
|
323
335
|
*/
|
|
324
336
|
doEncrypt(data: {
|
|
@@ -328,8 +340,9 @@ declare class IAMService {
|
|
|
328
340
|
/**
|
|
329
341
|
* Decrypt data via adapter.
|
|
330
342
|
* Not available in hybrid mode (decryption requires client-side doken).
|
|
343
|
+
* In native mode, passing a decryption policy throws.
|
|
331
344
|
* @param {{ encrypted: string | Uint8Array, tags: string[] }[]} data - Array of objects to decrypt
|
|
332
|
-
* @param {Uint8Array} decryption_policy Optional policy
|
|
345
|
+
* @param {Uint8Array} [decryption_policy] Optional policy the data was encrypted under
|
|
333
346
|
* @returns {Promise<(string | Uint8Array)[]>} Array of decrypted values
|
|
334
347
|
*/
|
|
335
348
|
doDecrypt(data: {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {Object} [options]
|
|
3
|
+
* @param {Object} [options.config] TideCloak adapter config (tidecloak.json). Used to
|
|
4
|
+
* reject requests for another realm or client. Omitted or empty, it serves any.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createDpopRoute({ config }?: {
|
|
7
|
+
config?: Object;
|
|
8
|
+
}): {
|
|
9
|
+
GET: (request: any) => Response;
|
|
10
|
+
HEAD: (request: any) => Response;
|
|
11
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { DPOP_AUTH_HTML, DPOP_AUTH_CSP } from './generated/dpopAuthAsset.js';
|
|
2
|
+
export { DPOP_AUTH_HTML, DPOP_AUTH_CSP };
|
|
3
|
+
export declare function dpopAuthHeaders(): {
|
|
4
|
+
'Content-Type': string;
|
|
5
|
+
'Content-Security-Policy': string;
|
|
6
|
+
'Allow-CSP-From': string;
|
|
7
|
+
'Cache-Control': string;
|
|
8
|
+
};
|
|
9
|
+
export declare function hexToUtf8(hex: any): string | null;
|
|
10
|
+
export declare function dpopAuthPathFor(issuer: any, clientId: any): string;
|
|
11
|
+
/**
|
|
12
|
+
* Issuer and client id to accept, from a TideCloak adapter config.
|
|
13
|
+
* Either is undefined when the config does not carry it (e.g. the `{}` placeholder).
|
|
14
|
+
*/
|
|
15
|
+
export declare function expectationsFromConfig(config?: {}): {
|
|
16
|
+
issuer: string | undefined;
|
|
17
|
+
client: any;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* The response for a request path, as `{ status, headers, body }`, or null when
|
|
21
|
+
* the path is not the DPoP page. Omitted expectations are not checked.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveDpopAuthRequest({ pathname, expectedIssuer, expectedClient, method }: {
|
|
24
|
+
expectedClient: any;
|
|
25
|
+
expectedIssuer: any;
|
|
26
|
+
method?: string | undefined;
|
|
27
|
+
pathname: any;
|
|
28
|
+
}): {
|
|
29
|
+
status: any;
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': string;
|
|
32
|
+
'Cache-Control': string;
|
|
33
|
+
};
|
|
34
|
+
body: any;
|
|
35
|
+
} | null;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const DPOP_AUTH_HTML = "<html>\n\n<head>\n <style>\n body {\n margin: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100vh;\n background: #f6f6f7;\n font-family: -apple-system, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n }\n\n .status {\n text-align: center;\n opacity: 0;\n }\n\n .status.visible {\n opacity: 1;\n }\n\n .checkmark-circle {\n width: 72px;\n height: 72px;\n margin: 0 auto 16px;\n }\n\n .checkmark-circle circle {\n fill: none;\n stroke: #039855;\n stroke-width: 3;\n stroke-dasharray: 201;\n stroke-dashoffset: 201;\n }\n\n .checkmark-circle path {\n fill: none;\n stroke: #039855;\n stroke-width: 3;\n stroke-linecap: round;\n stroke-linejoin: round;\n stroke-dasharray: 50;\n stroke-dashoffset: 50;\n }\n\n .status.visible .checkmark-circle circle {\n animation: circle-draw 0.5s ease forwards;\n }\n\n .status.visible .checkmark-circle path {\n animation: check-draw 0.3s 0.4s ease forwards;\n }\n\n .status.visible .status-text {\n animation: fade-in 0.3s 0.6s ease forwards;\n }\n\n .status-text {\n font-size: 15px;\n color: #464647;\n margin: 0;\n opacity: 0;\n }\n\n @keyframes circle-draw {\n to {\n stroke-dashoffset: 0;\n }\n }\n\n @keyframes check-draw {\n to {\n stroke-dashoffset: 0;\n }\n }\n\n @keyframes fade-in {\n to {\n opacity: 1;\n }\n }\n </style>\n</head>\n\n<body>\n <div class=\"status\" id=\"status\">\n <svg class=\"checkmark-circle\" viewBox=\"0 0 72 72\">\n <circle cx=\"36\" cy=\"36\" r=\"32\" />\n <path d=\"M22 36 l10 10 l18 -20\" />\n </svg>\n <p class=\"status-text\">Session verified</p>\n </div>\n <script>\n const thisVersion = 1;\n\n const hexToStr = (hex) => {\n if (!hex || hex.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hex)) throw Error(\"Invalid hex string\");\n const bytes = new Uint8Array(hex.length / 2);\n for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(hex.substr(i * 2, 2), 16);\n return new TextDecoder().decode(bytes);\n };\n\n const createDbName = async (issuer, clientId) => {\n const encoder = new TextEncoder();\n const issuerHash = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(issuer))).slice(0, 8))\n .map(b => b.toString(16).padStart(2, '0')).join('');\n const clientHash = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(clientId))).slice(0, 8))\n .map(b => b.toString(16).padStart(2, '0')).join('');\n return `dpop:${issuerHash}:${clientHash}`;\n };\n\n // Determine target window: popup has window.opener, iframe has window.parent\n const isPopup = !!window.opener;\n const targetWindow = window.opener || window.parent;\n\n if (!targetWindow || targetWindow === window) {\n document.body.textContent = \"Error: No parent or opener window found.\";\n throw Error(\"No target window available for postMessage\");\n }\n\n const urlParams = new URL(window.location.href).searchParams;\n const enclaveVersion = urlParams.get(\"version\");\n if (!enclaveVersion) throw Error(\"No enclave version provided in url\");\n let openerOrigin = urlParams.get(\"openerOrigin\");\n if (!openerOrigin) throw Error(\"No opener origin provided in url\");\n openerOrigin = new URL(decodeURIComponent(openerOrigin)).origin;\n\n const showSuccess = () => {\n document.getElementById('status').classList.add('visible');\n };\n\n const version1Flow = async () => {\n const paths = new URL(window.location.href).pathname.split(\"/\");\n const iss = hexToStr(paths[paths.indexOf(\"iss\") + 1]);\n const aud = hexToStr(paths[paths.indexOf(\"aud\") + 1]);\n const dbname = await createDbName(iss, aud);\n let dpopKey;\n try {\n // Chrome 125+: requestStorageAccess({ indexedDB: true }) returns a handle\n // whose .indexedDB factory points directly at the unpartitioned bucket.\n // If permission was already granted (within 30 days) this resolves\n // automatically with no prompt.\n //\n // In popup mode we have first-party context so we can access IndexedDB directly.\n let idbFactory;\n if (isPopup) {\n idbFactory = window.indexedDB;\n } else {\n const handle = await document.requestStorageAccess({ indexedDB: true });\n idbFactory = handle.indexedDB;\n }\n\n const db = await new Promise((resolve, reject) => {\n const req = idbFactory.open(dbname);\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n dpopKey = await new Promise((resolve, reject) => {\n const tx = db.transaction(\"main\", \"readonly\");\n const store = tx.objectStore(\"main\");\n const req = store.get(\"dpopState\");\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n db.close();\n if (!dpopKey || !dpopKey.keys) throw Error(\"No DPoP key found in IndexedDB\");\n } catch (ex) {\n const isStorageBlocked =\n ex?.name === \"NotAllowedError\" ||\n (ex instanceof DOMException && (\n ex.name === \"SecurityError\" ||\n ex.name === \"UnknownError\" ||\n ex.message?.includes(\"not a known object store name\")\n ));\n const detail = isStorageBlocked\n ? \"Browser is blocking third-party storage. Please allow storage access or disable strict tracking protection.\"\n : \"Failed to access DPoP key from IndexedDB.\";\n targetWindow.postMessage({ type: \"error\", message: \"db failed\", detail }, openerOrigin);\n document.body.textContent = detail;\n throw ex;\n }\n\n const challenge = () => new Promise((resolve) => {\n const handler = (event) => {\n if (event.origin !== openerOrigin) return;\n if (event.data.type === \"challenge\") {\n resolve(event.data.challenge);\n window.removeEventListener(\"message\", handler);\n }\n };\n window.addEventListener(\"message\", handler, false);\n });\n\n const pre_challenge = challenge();\n\n targetWindow.postMessage({ type: \"pageLoaded\", message: \"yay\" }, openerOrigin);\n\n const challengeResp = await pre_challenge;\n if (typeof challengeResp !== \"string\" || challengeResp?.length != 64) throw Error(\"Unexpect challenge format\");\n\n const te = new TextEncoder();\n const signatureInput = te.encode(\"dpop-auth-challenge:\" + challengeResp);\n const algName = dpopKey.keys.privateKey.algorithm.name;\n const signParams = algName === \"ECDSA\"\n ? { name: \"ECDSA\", hash: { \"P-256\": \"SHA-256\", \"P-384\": \"SHA-384\", \"P-521\": \"SHA-512\" }[dpopKey.keys.privateKey.algorithm.namedCurve] }\n : { name: algName };\n const signature = await crypto.subtle.sign(signParams, dpopKey.keys.privateKey, signatureInput);\n\n const publicJwk = await crypto.subtle.exportKey(\"jwk\", dpopKey.keys.publicKey);\n\n targetWindow.postMessage({\n type: \"challenge response\",\n message: {\n dpop_public: JSON.stringify({ crv: publicJwk.crv, kty: publicJwk.kty, x: publicJwk.x, y: publicJwk.y }),\n signature: btoa(String.fromCharCode(...new Uint8Array(signature)))\n }\n }, openerOrigin);\n\n showSuccess();\n };\n\n const versionFlows = new Map([[1, version1Flow]]);\n const latestFlow = version1Flow;\n\n (async () => {\n if (Number(enclaveVersion) >= thisVersion) await latestFlow();\n else {\n const flow = versionFlows.get(Number(enclaveVersion));\n if (!flow) throw Error(\"Cannot find supported enclave version: \" + enclaveVersion);\n await flow();\n }\n })();\n </script>\n</body>\n\n</html>";
|
|
2
|
+
export declare const DPOP_AUTH_CSP = "default-src 'self'; script-src 'self' 'sha256-utc6UrebuHOyLd/2aiMXS/p1EDy9UZBDe/XEMKDw9Mc='; style-src 'self' 'sha256-F7OJTdJYct4J+cQfuJUoDauitndqt8pAc8EbA8gwDPU='";
|
|
@@ -1 +1,40 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated The policy editor is no longer part of @tidecloak/js. This entry
|
|
3
|
+
* point is kept so existing imports still resolve; every component throws when
|
|
4
|
+
* rendered. The @tidecloak/policy package it used to re-export was never
|
|
5
|
+
* published to npm.
|
|
6
|
+
*/
|
|
7
|
+
export type Policy = any;
|
|
8
|
+
export type PolicyBlock = any;
|
|
9
|
+
export type Model = any;
|
|
10
|
+
export type ModelField = any;
|
|
11
|
+
export type Claim = any;
|
|
12
|
+
/** @typedef {any} Policy */
|
|
13
|
+
/** @typedef {any} PolicyBlock */
|
|
14
|
+
/** @typedef {any} Model */
|
|
15
|
+
/** @typedef {any} ModelField */
|
|
16
|
+
/** @typedef {any} Claim */
|
|
17
|
+
/**
|
|
18
|
+
* @deprecated The policy editor has moved out of @tidecloak/js.
|
|
19
|
+
* @param {any} _props
|
|
20
|
+
* @returns {never}
|
|
21
|
+
*/
|
|
22
|
+
export declare function PolicyBuilder(_props: any): never;
|
|
23
|
+
/**
|
|
24
|
+
* @deprecated The policy editor has moved out of @tidecloak/js.
|
|
25
|
+
* @param {any} _props
|
|
26
|
+
* @returns {never}
|
|
27
|
+
*/
|
|
28
|
+
export declare function PolicyCanvas(_props: any): never;
|
|
29
|
+
/**
|
|
30
|
+
* @deprecated The policy editor has moved out of @tidecloak/js.
|
|
31
|
+
* @param {any} _props
|
|
32
|
+
* @returns {never}
|
|
33
|
+
*/
|
|
34
|
+
export declare function PropertiesPanel(_props: any): never;
|
|
35
|
+
/**
|
|
36
|
+
* @deprecated The policy editor has moved out of @tidecloak/js.
|
|
37
|
+
* @param {any} _props
|
|
38
|
+
* @returns {never}
|
|
39
|
+
*/
|
|
40
|
+
export declare function BlockPalette(_props: any): never;
|
|
@@ -10,12 +10,14 @@ import TideCloak, { RequestEnclave } from "../lib/tidecloak.js";
|
|
|
10
10
|
* ---
|
|
11
11
|
* ## Front-channel Mode
|
|
12
12
|
*
|
|
13
|
+
* Handlers are called with the event name first, then the event's arguments.
|
|
14
|
+
*
|
|
13
15
|
* Usage A: pass an onReady callback directly
|
|
14
16
|
* ```js
|
|
15
17
|
* import { IAMService } from '@tidecloak/js';
|
|
16
|
-
* import tidecloakConfig from './
|
|
18
|
+
* import tidecloakConfig from './tidecloak.json';
|
|
17
19
|
*
|
|
18
|
-
* IAMService.initIAM(tidecloakConfig, authenticated => {
|
|
20
|
+
* IAMService.initIAM(tidecloakConfig, (_event, authenticated) => {
|
|
19
21
|
* if (!authenticated) IAMService.doLogin();
|
|
20
22
|
* }).catch(console.error);
|
|
21
23
|
* ```
|
|
@@ -23,8 +25,8 @@ import TideCloak, { RequestEnclave } from "../lib/tidecloak.js";
|
|
|
23
25
|
* Usage B: attach multiple listeners, then init
|
|
24
26
|
* ```js
|
|
25
27
|
* IAMService
|
|
26
|
-
* .on('ready', auth => console.log('ready', auth))
|
|
27
|
-
* .on('authError', err => console.error('Auth failed', err));
|
|
28
|
+
* .on('ready', (_event, auth) => console.log('ready', auth))
|
|
29
|
+
* .on('authError', (_event, err) => console.error('Auth failed', err));
|
|
28
30
|
*
|
|
29
31
|
* await IAMService.initIAM(tidecloakConfig);
|
|
30
32
|
* ```
|
|
@@ -85,6 +87,7 @@ import TideCloak, { RequestEnclave } from "../lib/tidecloak.js";
|
|
|
85
87
|
*
|
|
86
88
|
* ---
|
|
87
89
|
* ## Events
|
|
90
|
+
* Every handler receives `(eventName, ...args)`.
|
|
88
91
|
* - `ready` - Emitted when initialization completes (with authenticated boolean)
|
|
89
92
|
* - `initError` - Emitted when initialization fails
|
|
90
93
|
* - `authSuccess` - Emitted on successful authentication
|
|
@@ -146,7 +149,8 @@ declare class IAMService {
|
|
|
146
149
|
_frontchannelInitPromise: Promise<boolean> | null;
|
|
147
150
|
constructor();
|
|
148
151
|
/**
|
|
149
|
-
* Register an event listener.
|
|
152
|
+
* Register an event listener. The handler is called as `handler(event, ...args)`,
|
|
153
|
+
* so the event name comes first, e.g. `on('ready', (_event, authenticated) => ...)`.
|
|
150
154
|
* @param {'ready'|'initError'|'authSuccess'|'authError'|'authRefreshSuccess'|'authRefreshError'|'logout'|'tokenExpired'} event
|
|
151
155
|
* @param {Function} handler
|
|
152
156
|
* @returns {this}
|
|
@@ -181,7 +185,7 @@ declare class IAMService {
|
|
|
181
185
|
* Initialize the TideCloak SSO client with silent SSO check.
|
|
182
186
|
* In hybrid mode, handles the redirect callback if present.
|
|
183
187
|
* @param {Object} config - TideCloak configuration object.
|
|
184
|
-
* @param {Function} [onReady] - Optional
|
|
188
|
+
* @param {Function} [onReady] - Optional 'ready' handler, called as `(event, authenticated)`.
|
|
185
189
|
* @returns {Promise<boolean>} true if authenticated, else false.
|
|
186
190
|
*/
|
|
187
191
|
initIAM(config: Object, onReady?: Function): Promise<boolean>;
|
|
@@ -294,6 +298,13 @@ declare class IAMService {
|
|
|
294
298
|
* @throws {Error} In hybrid mode (tokens are server-side)
|
|
295
299
|
*/
|
|
296
300
|
getValueFromIDToken(key: string): any;
|
|
301
|
+
/**
|
|
302
|
+
* Alias of {@link IAMService#getValueFromIDToken}, with the same casing as the React context.
|
|
303
|
+
* @param {string} key - The name of the claim to retrieve from the ID token's payload.
|
|
304
|
+
* @returns {*} Custom claim from ID token
|
|
305
|
+
* @throws {Error} In hybrid mode (tokens are server-side)
|
|
306
|
+
*/
|
|
307
|
+
getValueFromIdToken(key: string): any;
|
|
297
308
|
/**
|
|
298
309
|
* Refreshes token if expired or about to expire.
|
|
299
310
|
* @returns {Promise<boolean>}
|
|
@@ -317,8 +328,9 @@ declare class IAMService {
|
|
|
317
328
|
/**
|
|
318
329
|
* Encrypt data via adapter.
|
|
319
330
|
* Not available in hybrid mode (encryption requires client-side doken).
|
|
331
|
+
* In native mode, passing a decryption policy throws.
|
|
320
332
|
* @param {{ data: string | Uint8Array, tags: string[] }[]} data - Array of objects to encrypt
|
|
321
|
-
* @param {Uint8Array} decryption_policy Optional policy to protect the encrypted data
|
|
333
|
+
* @param {Uint8Array} [decryption_policy] Optional policy to protect the encrypted data
|
|
322
334
|
* @returns {Promise<(string | Uint8Array)[]>} Array of encrypted values
|
|
323
335
|
*/
|
|
324
336
|
doEncrypt(data: {
|
|
@@ -328,8 +340,9 @@ declare class IAMService {
|
|
|
328
340
|
/**
|
|
329
341
|
* Decrypt data via adapter.
|
|
330
342
|
* Not available in hybrid mode (decryption requires client-side doken).
|
|
343
|
+
* In native mode, passing a decryption policy throws.
|
|
331
344
|
* @param {{ encrypted: string | Uint8Array, tags: string[] }[]} data - Array of objects to decrypt
|
|
332
|
-
* @param {Uint8Array} decryption_policy Optional policy
|
|
345
|
+
* @param {Uint8Array} [decryption_policy] Optional policy the data was encrypted under
|
|
333
346
|
* @returns {Promise<(string | Uint8Array)[]>} Array of decrypted values
|
|
334
347
|
*/
|
|
335
348
|
doDecrypt(data: {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {Object} [options]
|
|
3
|
+
* @param {Object} [options.config] TideCloak adapter config (tidecloak.json). Used to
|
|
4
|
+
* reject requests for another realm or client. Omitted or empty, it serves any.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createDpopRoute({ config }?: {
|
|
7
|
+
config?: Object;
|
|
8
|
+
}): {
|
|
9
|
+
GET: (request: any) => Response;
|
|
10
|
+
HEAD: (request: any) => Response;
|
|
11
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { DPOP_AUTH_HTML, DPOP_AUTH_CSP } from './generated/dpopAuthAsset.js';
|
|
2
|
+
export { DPOP_AUTH_HTML, DPOP_AUTH_CSP };
|
|
3
|
+
export declare function dpopAuthHeaders(): {
|
|
4
|
+
'Content-Type': string;
|
|
5
|
+
'Content-Security-Policy': string;
|
|
6
|
+
'Allow-CSP-From': string;
|
|
7
|
+
'Cache-Control': string;
|
|
8
|
+
};
|
|
9
|
+
export declare function hexToUtf8(hex: any): string | null;
|
|
10
|
+
export declare function dpopAuthPathFor(issuer: any, clientId: any): string;
|
|
11
|
+
/**
|
|
12
|
+
* Issuer and client id to accept, from a TideCloak adapter config.
|
|
13
|
+
* Either is undefined when the config does not carry it (e.g. the `{}` placeholder).
|
|
14
|
+
*/
|
|
15
|
+
export declare function expectationsFromConfig(config?: {}): {
|
|
16
|
+
issuer: string | undefined;
|
|
17
|
+
client: any;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* The response for a request path, as `{ status, headers, body }`, or null when
|
|
21
|
+
* the path is not the DPoP page. Omitted expectations are not checked.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveDpopAuthRequest({ pathname, expectedIssuer, expectedClient, method }: {
|
|
24
|
+
expectedClient: any;
|
|
25
|
+
expectedIssuer: any;
|
|
26
|
+
method?: string | undefined;
|
|
27
|
+
pathname: any;
|
|
28
|
+
}): {
|
|
29
|
+
status: any;
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': string;
|
|
32
|
+
'Cache-Control': string;
|
|
33
|
+
};
|
|
34
|
+
body: any;
|
|
35
|
+
} | null;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const DPOP_AUTH_HTML = "<html>\n\n<head>\n <style>\n body {\n margin: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100vh;\n background: #f6f6f7;\n font-family: -apple-system, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n }\n\n .status {\n text-align: center;\n opacity: 0;\n }\n\n .status.visible {\n opacity: 1;\n }\n\n .checkmark-circle {\n width: 72px;\n height: 72px;\n margin: 0 auto 16px;\n }\n\n .checkmark-circle circle {\n fill: none;\n stroke: #039855;\n stroke-width: 3;\n stroke-dasharray: 201;\n stroke-dashoffset: 201;\n }\n\n .checkmark-circle path {\n fill: none;\n stroke: #039855;\n stroke-width: 3;\n stroke-linecap: round;\n stroke-linejoin: round;\n stroke-dasharray: 50;\n stroke-dashoffset: 50;\n }\n\n .status.visible .checkmark-circle circle {\n animation: circle-draw 0.5s ease forwards;\n }\n\n .status.visible .checkmark-circle path {\n animation: check-draw 0.3s 0.4s ease forwards;\n }\n\n .status.visible .status-text {\n animation: fade-in 0.3s 0.6s ease forwards;\n }\n\n .status-text {\n font-size: 15px;\n color: #464647;\n margin: 0;\n opacity: 0;\n }\n\n @keyframes circle-draw {\n to {\n stroke-dashoffset: 0;\n }\n }\n\n @keyframes check-draw {\n to {\n stroke-dashoffset: 0;\n }\n }\n\n @keyframes fade-in {\n to {\n opacity: 1;\n }\n }\n </style>\n</head>\n\n<body>\n <div class=\"status\" id=\"status\">\n <svg class=\"checkmark-circle\" viewBox=\"0 0 72 72\">\n <circle cx=\"36\" cy=\"36\" r=\"32\" />\n <path d=\"M22 36 l10 10 l18 -20\" />\n </svg>\n <p class=\"status-text\">Session verified</p>\n </div>\n <script>\n const thisVersion = 1;\n\n const hexToStr = (hex) => {\n if (!hex || hex.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hex)) throw Error(\"Invalid hex string\");\n const bytes = new Uint8Array(hex.length / 2);\n for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(hex.substr(i * 2, 2), 16);\n return new TextDecoder().decode(bytes);\n };\n\n const createDbName = async (issuer, clientId) => {\n const encoder = new TextEncoder();\n const issuerHash = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(issuer))).slice(0, 8))\n .map(b => b.toString(16).padStart(2, '0')).join('');\n const clientHash = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(clientId))).slice(0, 8))\n .map(b => b.toString(16).padStart(2, '0')).join('');\n return `dpop:${issuerHash}:${clientHash}`;\n };\n\n // Determine target window: popup has window.opener, iframe has window.parent\n const isPopup = !!window.opener;\n const targetWindow = window.opener || window.parent;\n\n if (!targetWindow || targetWindow === window) {\n document.body.textContent = \"Error: No parent or opener window found.\";\n throw Error(\"No target window available for postMessage\");\n }\n\n const urlParams = new URL(window.location.href).searchParams;\n const enclaveVersion = urlParams.get(\"version\");\n if (!enclaveVersion) throw Error(\"No enclave version provided in url\");\n let openerOrigin = urlParams.get(\"openerOrigin\");\n if (!openerOrigin) throw Error(\"No opener origin provided in url\");\n openerOrigin = new URL(decodeURIComponent(openerOrigin)).origin;\n\n const showSuccess = () => {\n document.getElementById('status').classList.add('visible');\n };\n\n const version1Flow = async () => {\n const paths = new URL(window.location.href).pathname.split(\"/\");\n const iss = hexToStr(paths[paths.indexOf(\"iss\") + 1]);\n const aud = hexToStr(paths[paths.indexOf(\"aud\") + 1]);\n const dbname = await createDbName(iss, aud);\n let dpopKey;\n try {\n // Chrome 125+: requestStorageAccess({ indexedDB: true }) returns a handle\n // whose .indexedDB factory points directly at the unpartitioned bucket.\n // If permission was already granted (within 30 days) this resolves\n // automatically with no prompt.\n //\n // In popup mode we have first-party context so we can access IndexedDB directly.\n let idbFactory;\n if (isPopup) {\n idbFactory = window.indexedDB;\n } else {\n const handle = await document.requestStorageAccess({ indexedDB: true });\n idbFactory = handle.indexedDB;\n }\n\n const db = await new Promise((resolve, reject) => {\n const req = idbFactory.open(dbname);\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n dpopKey = await new Promise((resolve, reject) => {\n const tx = db.transaction(\"main\", \"readonly\");\n const store = tx.objectStore(\"main\");\n const req = store.get(\"dpopState\");\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n db.close();\n if (!dpopKey || !dpopKey.keys) throw Error(\"No DPoP key found in IndexedDB\");\n } catch (ex) {\n const isStorageBlocked =\n ex?.name === \"NotAllowedError\" ||\n (ex instanceof DOMException && (\n ex.name === \"SecurityError\" ||\n ex.name === \"UnknownError\" ||\n ex.message?.includes(\"not a known object store name\")\n ));\n const detail = isStorageBlocked\n ? \"Browser is blocking third-party storage. Please allow storage access or disable strict tracking protection.\"\n : \"Failed to access DPoP key from IndexedDB.\";\n targetWindow.postMessage({ type: \"error\", message: \"db failed\", detail }, openerOrigin);\n document.body.textContent = detail;\n throw ex;\n }\n\n const challenge = () => new Promise((resolve) => {\n const handler = (event) => {\n if (event.origin !== openerOrigin) return;\n if (event.data.type === \"challenge\") {\n resolve(event.data.challenge);\n window.removeEventListener(\"message\", handler);\n }\n };\n window.addEventListener(\"message\", handler, false);\n });\n\n const pre_challenge = challenge();\n\n targetWindow.postMessage({ type: \"pageLoaded\", message: \"yay\" }, openerOrigin);\n\n const challengeResp = await pre_challenge;\n if (typeof challengeResp !== \"string\" || challengeResp?.length != 64) throw Error(\"Unexpect challenge format\");\n\n const te = new TextEncoder();\n const signatureInput = te.encode(\"dpop-auth-challenge:\" + challengeResp);\n const algName = dpopKey.keys.privateKey.algorithm.name;\n const signParams = algName === \"ECDSA\"\n ? { name: \"ECDSA\", hash: { \"P-256\": \"SHA-256\", \"P-384\": \"SHA-384\", \"P-521\": \"SHA-512\" }[dpopKey.keys.privateKey.algorithm.namedCurve] }\n : { name: algName };\n const signature = await crypto.subtle.sign(signParams, dpopKey.keys.privateKey, signatureInput);\n\n const publicJwk = await crypto.subtle.exportKey(\"jwk\", dpopKey.keys.publicKey);\n\n targetWindow.postMessage({\n type: \"challenge response\",\n message: {\n dpop_public: JSON.stringify({ crv: publicJwk.crv, kty: publicJwk.kty, x: publicJwk.x, y: publicJwk.y }),\n signature: btoa(String.fromCharCode(...new Uint8Array(signature)))\n }\n }, openerOrigin);\n\n showSuccess();\n };\n\n const versionFlows = new Map([[1, version1Flow]]);\n const latestFlow = version1Flow;\n\n (async () => {\n if (Number(enclaveVersion) >= thisVersion) await latestFlow();\n else {\n const flow = versionFlows.get(Number(enclaveVersion));\n if (!flow) throw Error(\"Cannot find supported enclave version: \" + enclaveVersion);\n await flow();\n }\n })();\n </script>\n</body>\n\n</html>";
|
|
2
|
+
export declare const DPOP_AUTH_CSP = "default-src 'self'; script-src 'self' 'sha256-utc6UrebuHOyLd/2aiMXS/p1EDy9UZBDe/XEMKDw9Mc='; style-src 'self' 'sha256-F7OJTdJYct4J+cQfuJUoDauitndqt8pAc8EbA8gwDPU='";
|
|
@@ -1 +1,40 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated The policy editor is no longer part of @tidecloak/js. This entry
|
|
3
|
+
* point is kept so existing imports still resolve; every component throws when
|
|
4
|
+
* rendered. The @tidecloak/policy package it used to re-export was never
|
|
5
|
+
* published to npm.
|
|
6
|
+
*/
|
|
7
|
+
export type Policy = any;
|
|
8
|
+
export type PolicyBlock = any;
|
|
9
|
+
export type Model = any;
|
|
10
|
+
export type ModelField = any;
|
|
11
|
+
export type Claim = any;
|
|
12
|
+
/** @typedef {any} Policy */
|
|
13
|
+
/** @typedef {any} PolicyBlock */
|
|
14
|
+
/** @typedef {any} Model */
|
|
15
|
+
/** @typedef {any} ModelField */
|
|
16
|
+
/** @typedef {any} Claim */
|
|
17
|
+
/**
|
|
18
|
+
* @deprecated The policy editor has moved out of @tidecloak/js.
|
|
19
|
+
* @param {any} _props
|
|
20
|
+
* @returns {never}
|
|
21
|
+
*/
|
|
22
|
+
export declare function PolicyBuilder(_props: any): never;
|
|
23
|
+
/**
|
|
24
|
+
* @deprecated The policy editor has moved out of @tidecloak/js.
|
|
25
|
+
* @param {any} _props
|
|
26
|
+
* @returns {never}
|
|
27
|
+
*/
|
|
28
|
+
export declare function PolicyCanvas(_props: any): never;
|
|
29
|
+
/**
|
|
30
|
+
* @deprecated The policy editor has moved out of @tidecloak/js.
|
|
31
|
+
* @param {any} _props
|
|
32
|
+
* @returns {never}
|
|
33
|
+
*/
|
|
34
|
+
export declare function PropertiesPanel(_props: any): never;
|
|
35
|
+
/**
|
|
36
|
+
* @deprecated The policy editor has moved out of @tidecloak/js.
|
|
37
|
+
* @param {any} _props
|
|
38
|
+
* @returns {never}
|
|
39
|
+
*/
|
|
40
|
+
export declare function BlockPalette(_props: any): never;
|