@chaindaddy/js 0.0.0-stage → 0.2.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.
package/dist/index.js ADDED
@@ -0,0 +1,410 @@
1
+ // src/core.ts
2
+ var VERSION = true ? "0.2.0" : "0.0.0";
3
+ function b64url(bytes) {
4
+ let s = "";
5
+ for (const b of bytes) s += String.fromCharCode(b);
6
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
7
+ }
8
+ function randomToken() {
9
+ return b64url(crypto.getRandomValues(new Uint8Array(32)));
10
+ }
11
+ function listen(type, origin, onMessage, channel) {
12
+ const accept = (data) => {
13
+ if (data && typeof data === "object" && data.type === type) {
14
+ try {
15
+ onMessage(data);
16
+ } catch {
17
+ }
18
+ }
19
+ };
20
+ const onWindow = (e) => {
21
+ if (e.origin === origin) accept(e.data);
22
+ };
23
+ window.addEventListener("message", onWindow);
24
+ let bc = null;
25
+ if (channel && typeof BroadcastChannel === "function" && origin === location.origin) {
26
+ try {
27
+ bc = new BroadcastChannel(channel);
28
+ bc.onmessage = (e) => accept(e.data);
29
+ } catch {
30
+ bc = null;
31
+ }
32
+ }
33
+ return () => {
34
+ window.removeEventListener("message", onWindow);
35
+ bc?.close();
36
+ };
37
+ }
38
+ var store = {
39
+ get(key) {
40
+ try {
41
+ return sessionStorage.getItem(key);
42
+ } catch {
43
+ return null;
44
+ }
45
+ },
46
+ set(key, value) {
47
+ try {
48
+ sessionStorage.setItem(key, value);
49
+ } catch {
50
+ }
51
+ },
52
+ remove(key) {
53
+ try {
54
+ sessionStorage.removeItem(key);
55
+ } catch {
56
+ }
57
+ }
58
+ };
59
+
60
+ // src/signin.ts
61
+ var DEFAULT_ISSUER = "https://api.chaindaddy.io";
62
+ var AUTHORIZE = {
63
+ "https://api.chaindaddy.io": "https://chaindaddy.io/_oauth/authorize",
64
+ "https://staging-api.chaindaddy.io": "https://staging.chaindaddy.io/_oauth/authorize"
65
+ };
66
+ var POPUP_NAME = "cd-signin";
67
+ var MESSAGE = "cd:signin";
68
+ var STORAGE_KEY = "cd.signin";
69
+ async function challengeFor(verifier) {
70
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
71
+ return b64url(new Uint8Array(digest));
72
+ }
73
+ function withOpenID(scope = "openid") {
74
+ const parts = scope.split(/\s+/).filter(Boolean);
75
+ return (parts.includes("openid") ? parts : ["openid", ...parts]).join(" ");
76
+ }
77
+ async function authorizeURL(o, p, authorize) {
78
+ const q = new URLSearchParams({
79
+ response_type: "code",
80
+ client_id: p.clientId,
81
+ redirect_uri: p.redirectUri,
82
+ scope: withOpenID(o.scope),
83
+ state: p.state,
84
+ nonce: p.nonce,
85
+ code_challenge: await challengeFor(p.verifier),
86
+ code_challenge_method: "S256"
87
+ });
88
+ if (o.claims) q.set("claims", typeof o.claims === "string" ? o.claims : JSON.stringify(o.claims));
89
+ return `${authorize}?${q}`;
90
+ }
91
+ function result(p, code) {
92
+ return { code, codeVerifier: p.verifier, state: p.state, nonce: p.nonce, issuer: p.issuer, clientId: p.clientId, redirectUri: p.redirectUri };
93
+ }
94
+ function signIn(o) {
95
+ const issuer = (o.issuer ?? DEFAULT_ISSUER).replace(/\/$/, "");
96
+ const authorize = o.authorizeUrl ?? AUTHORIZE[issuer];
97
+ if (!authorize) return Promise.reject(new Error("authorizeUrl is required for this issuer"));
98
+ if (!o.clientId || !o.redirectUri) return Promise.reject(new Error("clientId and redirectUri are required"));
99
+ const p = { state: randomToken(), nonce: randomToken(), verifier: randomToken(), issuer, clientId: o.clientId, redirectUri: o.redirectUri };
100
+ store.set(STORAGE_KEY, JSON.stringify(p));
101
+ const popup = window.open("", POPUP_NAME, "popup,width=480,height=720");
102
+ return (async () => {
103
+ const url = await authorizeURL(o, p, authorize);
104
+ if (!popup) {
105
+ location.assign(url);
106
+ return null;
107
+ }
108
+ popup.location.href = url;
109
+ const callbackOrigin = new URL(o.redirectUri, location.href).origin;
110
+ return new Promise((resolve, reject) => {
111
+ const stop = listen(MESSAGE, callbackOrigin, (m) => {
112
+ if (m.state !== p.state) return;
113
+ stop();
114
+ store.remove(STORAGE_KEY);
115
+ if (typeof m.error === "string" && m.error) reject(new Error(m.error));
116
+ else if (m.iss !== issuer) reject(new Error("the answer came from another issuer"));
117
+ else if (typeof m.code !== "string" || !m.code) reject(new Error("the answer carried no code"));
118
+ else resolve(result(p, m.code));
119
+ }, POPUP_NAME);
120
+ o.signal?.addEventListener("abort", () => {
121
+ stop();
122
+ reject(new Error("aborted"));
123
+ });
124
+ });
125
+ })();
126
+ }
127
+ async function handleCallback() {
128
+ const q = new URLSearchParams(location.search);
129
+ const message = { type: MESSAGE, code: q.get("code"), state: q.get("state"), iss: q.get("iss"), error: q.get("error") };
130
+ if (window.name === POPUP_NAME) {
131
+ try {
132
+ window.opener?.postMessage(message, location.origin);
133
+ } catch {
134
+ }
135
+ try {
136
+ const bc = new BroadcastChannel(POPUP_NAME);
137
+ bc.postMessage(message);
138
+ bc.close();
139
+ } catch {
140
+ }
141
+ window.close();
142
+ return null;
143
+ }
144
+ const raw = store.get(STORAGE_KEY);
145
+ store.remove(STORAGE_KEY);
146
+ const p = raw ? JSON.parse(raw) : null;
147
+ if (!p || message.state !== p.state) throw new Error("the sign-in state does not match: start again");
148
+ if (message.error) throw new Error(message.error);
149
+ if (message.iss !== p.issuer) throw new Error("the answer came from another issuer");
150
+ if (!message.code) throw new Error("the answer carried no code");
151
+ return result(p, message.code);
152
+ }
153
+ async function exchangeCode(r) {
154
+ const res = await fetch(`${r.issuer}/api/v2/oauth/token`, {
155
+ method: "POST",
156
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
157
+ body: new URLSearchParams({ grant_type: "authorization_code", code: r.code, redirect_uri: r.redirectUri, client_id: r.clientId, code_verifier: r.codeVerifier })
158
+ });
159
+ const body = await res.json();
160
+ if (!res.ok) throw new Error(`${body.error ?? res.status}: ${body.error_description ?? ""}`.trim());
161
+ return body;
162
+ }
163
+
164
+ // src/button.ts
165
+ var CROWN = `<svg viewBox="0 0 24 24" fill="#FFD700" width="20" height="20" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"><path d="M5 16L3 5l5.5 5L12 4l3.5 6L21 5l-2 11H5z" stroke="#B8860B" stroke-width="0.5"/><path d="M5 18h14v2H5v-2z" stroke="#B8860B" stroke-width="0.5"/></svg>`;
166
+ var STYLE = `
167
+ :host { display: inline-block; }
168
+ button { display: inline-flex; align-items: center; justify-content: center; gap: 10px; box-sizing: border-box;
169
+ min-height: 40px; padding: 0 16px; border-radius: 10px; font: 600 15px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
170
+ cursor: pointer; border: 1px solid #d0d5dd; background: #ffffff; color: #101828; }
171
+ button[data-theme="dark"] { background: #101828; color: #ffffff; border-color: #101828; }
172
+ button:focus-visible { outline: 2px solid #f59e0b; outline-offset: 2px; }
173
+ button:disabled { opacity: .6; cursor: default; }
174
+ svg { flex: none; }
175
+ `;
176
+ function defineSignInButton() {
177
+ if (typeof customElements === "undefined" || customElements.get("cd-signin-button")) return;
178
+ class CdSigninButton extends HTMLElement {
179
+ constructor() {
180
+ super();
181
+ this.root = this.attachShadow({ mode: "open" });
182
+ }
183
+ connectedCallback() {
184
+ this.render();
185
+ }
186
+ attributeChangedCallback() {
187
+ if (this.isConnected) this.render();
188
+ }
189
+ render() {
190
+ const label = this.getAttribute("text") === "continue" ? "Continue with Chain Daddy" : "Sign in with Chain Daddy";
191
+ const theme = this.getAttribute("theme") === "dark" ? "dark" : "light";
192
+ this.root.innerHTML = `<style>${STYLE}</style><button type="button" part="button" data-theme="${theme}">${CROWN}<span></span></button>`;
193
+ const button = this.root.querySelector("button");
194
+ button.querySelector("span").textContent = label;
195
+ button.addEventListener("click", () => this.start());
196
+ }
197
+ start() {
198
+ const attr = (n) => this.getAttribute(n) || void 0;
199
+ signIn({
200
+ clientId: attr("client-id") ?? "",
201
+ redirectUri: attr("redirect-uri") ?? "",
202
+ scope: attr("scope"),
203
+ claims: attr("claims"),
204
+ issuer: attr("issuer"),
205
+ authorizeUrl: attr("authorize-url")
206
+ }).then(
207
+ (r) => r && this.dispatchEvent(new CustomEvent("cd-signin", { detail: r, bubbles: true, composed: true })),
208
+ (e) => this.dispatchEvent(new CustomEvent("cd-signin-error", { detail: { message: String(e?.message ?? e) }, bubbles: true, composed: true }))
209
+ );
210
+ }
211
+ }
212
+ CdSigninButton.observedAttributes = ["theme", "text"];
213
+ customElements.define("cd-signin-button", CdSigninButton);
214
+ }
215
+
216
+ // src/embed.ts
217
+ var PROD_ORIGIN = "https://embed.chaindaddy.io";
218
+ var STAGING_ORIGIN = "https://embed-staging.chaindaddy.io";
219
+ var READY_TIMEOUT_MS = 8e3;
220
+ var MAX_HEIGHT = 4e3;
221
+ var THEME_TOKENS = ["--cd-accent", "--cd-surface", "--cd-text", "--cd-text-muted", "--cd-border"];
222
+ function embedOriginFor(scriptSrc) {
223
+ try {
224
+ if (scriptSrc && new URL(scriptSrc).hostname === "js.staging.chaindaddy.io") return STAGING_ORIGIN;
225
+ } catch {
226
+ }
227
+ return PROD_ORIGIN;
228
+ }
229
+ var STYLE2 = `
230
+ :host { display: block; }
231
+ iframe { display: block; width: 100%; border: 0; height: 1px; opacity: 0; color-scheme: normal; }
232
+ :host([data-cd-ready]) iframe { opacity: 1; }
233
+ :host([data-cd-ready]) slot { display: none; }
234
+ :host([data-cd-unavailable]) iframe { display: none; }
235
+ `;
236
+ var BUY_STYLE = `
237
+ :host { display: inline-block; }
238
+ button { box-sizing: border-box; min-height: 40px; padding: 0 16px; border-radius: 10px; cursor: pointer;
239
+ font: 600 15px/1 system-ui, -apple-system, "Segoe UI", sans-serif;
240
+ border: 1px solid var(--cd-border, #d0d5dd); background: var(--cd-accent, #f59e0b); color: var(--cd-text, #101828); }
241
+ button:focus-visible { outline: 2px solid var(--cd-accent, #f59e0b); outline-offset: 2px; }
242
+ `;
243
+ function defineEmbedElements(options = {}) {
244
+ try {
245
+ if (typeof customElements === "undefined" || typeof HTMLElement === "undefined") return;
246
+ const origin = options.origin === STAGING_ORIGIN ? STAGING_ORIGIN : PROD_ORIGIN;
247
+ class CdFrame extends HTMLElement {
248
+ constructor() {
249
+ super(...arguments);
250
+ this.kind = "app";
251
+ this.frame = null;
252
+ this.onMessage = (event) => this.receive(event);
253
+ }
254
+ connectedCallback() {
255
+ try {
256
+ if (!this.shadowRoot) this.attachShadow({ mode: "open" });
257
+ this.mount();
258
+ } catch {
259
+ this.unavailable();
260
+ }
261
+ }
262
+ disconnectedCallback() {
263
+ try {
264
+ window.removeEventListener("message", this.onMessage);
265
+ clearTimeout(this.timer);
266
+ } catch {
267
+ }
268
+ }
269
+ attributeChangedCallback() {
270
+ try {
271
+ if (this.isConnected && this.shadowRoot) this.mount();
272
+ } catch {
273
+ this.unavailable();
274
+ }
275
+ }
276
+ /** The frame's address, or null when the element does not name enough. */
277
+ src() {
278
+ const crown = (this.getAttribute("crown") ?? "").trim();
279
+ const chain = (this.getAttribute("chain") ?? "").trim();
280
+ const app = (this.getAttribute("app") ?? "").trim();
281
+ if (!/^[0-9]{1,18}$/.test(crown) || !chain || this.kind === "app" && !app) return null;
282
+ const q = new URLSearchParams({ crown, chain });
283
+ if (this.kind === "app") q.set("app", app);
284
+ else q.set("kind", "coin");
285
+ q.set("mode", this.mode());
286
+ q.set("v", "1");
287
+ return `${origin}/v1/frame?${q.toString()}`;
288
+ }
289
+ mode() {
290
+ const attr = this.getAttribute("mode");
291
+ if (attr === "light" || attr === "dark") return attr;
292
+ try {
293
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
294
+ } catch {
295
+ return "light";
296
+ }
297
+ }
298
+ mount() {
299
+ const root = this.shadowRoot;
300
+ window.removeEventListener("message", this.onMessage);
301
+ clearTimeout(this.timer);
302
+ this.removeAttribute("data-cd-ready");
303
+ this.removeAttribute("data-cd-unavailable");
304
+ const src = this.src();
305
+ if (!src) {
306
+ root.innerHTML = "<slot></slot>";
307
+ this.frame = null;
308
+ return;
309
+ }
310
+ root.innerHTML = `<style>${STYLE2}</style><slot></slot>`;
311
+ const frame = document.createElement("iframe");
312
+ frame.setAttribute("loading", "lazy");
313
+ frame.setAttribute("referrerpolicy", "strict-origin-when-cross-origin");
314
+ frame.setAttribute("allow", "clipboard-write; fullscreen");
315
+ frame.setAttribute("title", this.kind === "coin" ? "Coin on Chain Daddy" : "App on Chain Daddy");
316
+ frame.src = src;
317
+ root.appendChild(frame);
318
+ this.frame = frame;
319
+ window.addEventListener("message", this.onMessage);
320
+ this.timer = setTimeout(() => {
321
+ if (!this.hasAttribute("data-cd-ready")) this.unavailable();
322
+ }, READY_TIMEOUT_MS);
323
+ }
324
+ receive(event) {
325
+ try {
326
+ if (event.origin !== origin || !this.frame || !event.source || event.source !== this.frame.contentWindow) return;
327
+ const data = event.data;
328
+ if (!data || typeof data !== "object") return;
329
+ if (data.type === "cd:ready") {
330
+ clearTimeout(this.timer);
331
+ this.removeAttribute("data-cd-unavailable");
332
+ this.setAttribute("data-cd-ready", "");
333
+ this.sendTheme();
334
+ this.dispatchEvent(new CustomEvent("cd:ready"));
335
+ } else if (data.type === "cd:resize") {
336
+ const h = Number(data.height);
337
+ if (Number.isFinite(h)) this.frame.style.height = `${Math.max(1, Math.min(MAX_HEIGHT, Math.ceil(h)))}px`;
338
+ } else if (data.type === "cd:unavailable") {
339
+ this.unavailable();
340
+ }
341
+ } catch {
342
+ }
343
+ }
344
+ /** The page's --cd-* colours, for an app that takes them. */
345
+ sendTheme() {
346
+ const tokens = {};
347
+ const style = getComputedStyle(this);
348
+ for (const token of THEME_TOKENS) {
349
+ const value = style.getPropertyValue(token).trim();
350
+ if (value) tokens[token] = value;
351
+ }
352
+ this.frame?.contentWindow?.postMessage({ type: "cd:theme", tokens }, origin);
353
+ }
354
+ unavailable() {
355
+ try {
356
+ clearTimeout(this.timer);
357
+ this.removeAttribute("data-cd-ready");
358
+ if (this.hasAttribute("data-cd-unavailable")) return;
359
+ this.setAttribute("data-cd-unavailable", "");
360
+ this.dispatchEvent(new CustomEvent("cd:unavailable"));
361
+ } catch {
362
+ }
363
+ }
364
+ }
365
+ CdFrame.observedAttributes = ["crown", "chain", "app", "mode"];
366
+ class CdApp extends CdFrame {
367
+ }
368
+ class CdCoin extends CdFrame {
369
+ constructor() {
370
+ super(...arguments);
371
+ this.kind = "coin";
372
+ }
373
+ }
374
+ class CdBuy extends HTMLElement {
375
+ connectedCallback() {
376
+ try {
377
+ const link = this.querySelector("a[href]");
378
+ const href = link ? new URL(link.getAttribute("href") ?? "", document.baseURI) : null;
379
+ if (!href || href.protocol !== "https:") return;
380
+ const root = this.shadowRoot ?? this.attachShadow({ mode: "open" });
381
+ root.innerHTML = `<style>${BUY_STYLE}</style><button type="button" part="button"></button>`;
382
+ const button = root.querySelector("button");
383
+ button.textContent = (link.textContent ?? "").trim() || "Buy on Chain Daddy";
384
+ button.addEventListener("click", () => {
385
+ try {
386
+ if (!window.open(href.toString(), "cd-buy", "popup,width=460,height=760")) window.location.assign(href.toString());
387
+ } catch {
388
+ }
389
+ });
390
+ } catch {
391
+ }
392
+ }
393
+ }
394
+ for (const [name, ctor] of [["cd-app", CdApp], ["cd-coin", CdCoin], ["cd-buy", CdBuy]]) {
395
+ if (!customElements.get(name)) customElements.define(name, ctor);
396
+ }
397
+ } catch {
398
+ }
399
+ }
400
+ export {
401
+ DEFAULT_ISSUER,
402
+ VERSION,
403
+ challengeFor,
404
+ defineEmbedElements,
405
+ defineSignInButton,
406
+ embedOriginFor,
407
+ exchangeCode,
408
+ handleCallback,
409
+ signIn
410
+ };
@@ -0,0 +1,57 @@
1
+ export declare const DEFAULT_ISSUER = "https://api.chaindaddy.io";
2
+ export interface SignInOptions {
3
+ /** Your client id: the URL of your client.json, or a registered cdc_ id. */
4
+ clientId: string;
5
+ /** Your callback page, exactly as your client lists it. It must be on this page's origin. */
6
+ redirectUri: string;
7
+ /** Space-separated scopes; openid is added when missing. Default "openid". */
8
+ scope?: string;
9
+ /** The OIDC `claims` request (holdings of your coin, crowns): an object or its JSON. */
10
+ claims?: object | string;
11
+ /** Default https://api.chaindaddy.io. */
12
+ issuer?: string;
13
+ /** The consent page; derived from the issuer for Chain Daddy's own. */
14
+ authorizeUrl?: string;
15
+ /** Stops waiting. Closing the popup does not: a closed-looking popup is not a cancel. */
16
+ signal?: AbortSignal;
17
+ }
18
+ /** What your server exchanges at the token endpoint (or exchangeCode does here). */
19
+ export interface SignInResult {
20
+ code: string;
21
+ codeVerifier: string;
22
+ state: string;
23
+ nonce: string;
24
+ issuer: string;
25
+ clientId: string;
26
+ redirectUri: string;
27
+ }
28
+ /** RFC 7636 S256: base64url(SHA-256(verifier)). */
29
+ export declare function challengeFor(verifier: string): Promise<string>;
30
+ /**
31
+ * Starts a sign-in. Call it from a click handler: the popup opens at once,
32
+ * before anything is awaited, or a browser blocks it. Resolves with the code
33
+ * once the visitor approves; resolves null when the popup was blocked and the
34
+ * page is redirecting instead (your callback page then calls handleCallback).
35
+ * Rejects when they cancel (access_denied) or the answer is not for this
36
+ * request.
37
+ */
38
+ export declare function signIn(o: SignInOptions): Promise<SignInResult | null>;
39
+ /**
40
+ * Runs on your callback page. In the sign-in popup it hands the answer to the
41
+ * page that opened it (by postMessage to this origin, and by a same-origin
42
+ * BroadcastChannel in case the opener link was cut), closes, and resolves
43
+ * null. After a full-page redirect it checks the answer against the request
44
+ * this tab stored and resolves with the code, or rejects.
45
+ */
46
+ export declare function handleCallback(): Promise<SignInResult | null>;
47
+ /**
48
+ * Redeems the code at the token endpoint from the browser (a public client:
49
+ * no secret). Send the id_token to your server and verify it there; do not
50
+ * trust it here.
51
+ */
52
+ export declare function exchangeCode(r: SignInResult): Promise<{
53
+ id_token: string;
54
+ access_token: string;
55
+ expires_in: number;
56
+ scope: string;
57
+ }>;
package/dist/signin.js ADDED
@@ -0,0 +1,168 @@
1
+ // src/core.ts
2
+ function b64url(bytes) {
3
+ let s = "";
4
+ for (const b of bytes) s += String.fromCharCode(b);
5
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
6
+ }
7
+ function randomToken() {
8
+ return b64url(crypto.getRandomValues(new Uint8Array(32)));
9
+ }
10
+ function listen(type, origin, onMessage, channel) {
11
+ const accept = (data) => {
12
+ if (data && typeof data === "object" && data.type === type) {
13
+ try {
14
+ onMessage(data);
15
+ } catch {
16
+ }
17
+ }
18
+ };
19
+ const onWindow = (e) => {
20
+ if (e.origin === origin) accept(e.data);
21
+ };
22
+ window.addEventListener("message", onWindow);
23
+ let bc = null;
24
+ if (channel && typeof BroadcastChannel === "function" && origin === location.origin) {
25
+ try {
26
+ bc = new BroadcastChannel(channel);
27
+ bc.onmessage = (e) => accept(e.data);
28
+ } catch {
29
+ bc = null;
30
+ }
31
+ }
32
+ return () => {
33
+ window.removeEventListener("message", onWindow);
34
+ bc?.close();
35
+ };
36
+ }
37
+ var store = {
38
+ get(key) {
39
+ try {
40
+ return sessionStorage.getItem(key);
41
+ } catch {
42
+ return null;
43
+ }
44
+ },
45
+ set(key, value) {
46
+ try {
47
+ sessionStorage.setItem(key, value);
48
+ } catch {
49
+ }
50
+ },
51
+ remove(key) {
52
+ try {
53
+ sessionStorage.removeItem(key);
54
+ } catch {
55
+ }
56
+ }
57
+ };
58
+
59
+ // src/signin.ts
60
+ var DEFAULT_ISSUER = "https://api.chaindaddy.io";
61
+ var AUTHORIZE = {
62
+ "https://api.chaindaddy.io": "https://chaindaddy.io/_oauth/authorize",
63
+ "https://staging-api.chaindaddy.io": "https://staging.chaindaddy.io/_oauth/authorize"
64
+ };
65
+ var POPUP_NAME = "cd-signin";
66
+ var MESSAGE = "cd:signin";
67
+ var STORAGE_KEY = "cd.signin";
68
+ async function challengeFor(verifier) {
69
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
70
+ return b64url(new Uint8Array(digest));
71
+ }
72
+ function withOpenID(scope = "openid") {
73
+ const parts = scope.split(/\s+/).filter(Boolean);
74
+ return (parts.includes("openid") ? parts : ["openid", ...parts]).join(" ");
75
+ }
76
+ async function authorizeURL(o, p, authorize) {
77
+ const q = new URLSearchParams({
78
+ response_type: "code",
79
+ client_id: p.clientId,
80
+ redirect_uri: p.redirectUri,
81
+ scope: withOpenID(o.scope),
82
+ state: p.state,
83
+ nonce: p.nonce,
84
+ code_challenge: await challengeFor(p.verifier),
85
+ code_challenge_method: "S256"
86
+ });
87
+ if (o.claims) q.set("claims", typeof o.claims === "string" ? o.claims : JSON.stringify(o.claims));
88
+ return `${authorize}?${q}`;
89
+ }
90
+ function result(p, code) {
91
+ return { code, codeVerifier: p.verifier, state: p.state, nonce: p.nonce, issuer: p.issuer, clientId: p.clientId, redirectUri: p.redirectUri };
92
+ }
93
+ function signIn(o) {
94
+ const issuer = (o.issuer ?? DEFAULT_ISSUER).replace(/\/$/, "");
95
+ const authorize = o.authorizeUrl ?? AUTHORIZE[issuer];
96
+ if (!authorize) return Promise.reject(new Error("authorizeUrl is required for this issuer"));
97
+ if (!o.clientId || !o.redirectUri) return Promise.reject(new Error("clientId and redirectUri are required"));
98
+ const p = { state: randomToken(), nonce: randomToken(), verifier: randomToken(), issuer, clientId: o.clientId, redirectUri: o.redirectUri };
99
+ store.set(STORAGE_KEY, JSON.stringify(p));
100
+ const popup = window.open("", POPUP_NAME, "popup,width=480,height=720");
101
+ return (async () => {
102
+ const url = await authorizeURL(o, p, authorize);
103
+ if (!popup) {
104
+ location.assign(url);
105
+ return null;
106
+ }
107
+ popup.location.href = url;
108
+ const callbackOrigin = new URL(o.redirectUri, location.href).origin;
109
+ return new Promise((resolve, reject) => {
110
+ const stop = listen(MESSAGE, callbackOrigin, (m) => {
111
+ if (m.state !== p.state) return;
112
+ stop();
113
+ store.remove(STORAGE_KEY);
114
+ if (typeof m.error === "string" && m.error) reject(new Error(m.error));
115
+ else if (m.iss !== issuer) reject(new Error("the answer came from another issuer"));
116
+ else if (typeof m.code !== "string" || !m.code) reject(new Error("the answer carried no code"));
117
+ else resolve(result(p, m.code));
118
+ }, POPUP_NAME);
119
+ o.signal?.addEventListener("abort", () => {
120
+ stop();
121
+ reject(new Error("aborted"));
122
+ });
123
+ });
124
+ })();
125
+ }
126
+ async function handleCallback() {
127
+ const q = new URLSearchParams(location.search);
128
+ const message = { type: MESSAGE, code: q.get("code"), state: q.get("state"), iss: q.get("iss"), error: q.get("error") };
129
+ if (window.name === POPUP_NAME) {
130
+ try {
131
+ window.opener?.postMessage(message, location.origin);
132
+ } catch {
133
+ }
134
+ try {
135
+ const bc = new BroadcastChannel(POPUP_NAME);
136
+ bc.postMessage(message);
137
+ bc.close();
138
+ } catch {
139
+ }
140
+ window.close();
141
+ return null;
142
+ }
143
+ const raw = store.get(STORAGE_KEY);
144
+ store.remove(STORAGE_KEY);
145
+ const p = raw ? JSON.parse(raw) : null;
146
+ if (!p || message.state !== p.state) throw new Error("the sign-in state does not match: start again");
147
+ if (message.error) throw new Error(message.error);
148
+ if (message.iss !== p.issuer) throw new Error("the answer came from another issuer");
149
+ if (!message.code) throw new Error("the answer carried no code");
150
+ return result(p, message.code);
151
+ }
152
+ async function exchangeCode(r) {
153
+ const res = await fetch(`${r.issuer}/api/v2/oauth/token`, {
154
+ method: "POST",
155
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
156
+ body: new URLSearchParams({ grant_type: "authorization_code", code: r.code, redirect_uri: r.redirectUri, client_id: r.clientId, code_verifier: r.codeVerifier })
157
+ });
158
+ const body = await res.json();
159
+ if (!res.ok) throw new Error(`${body.error ?? res.status}: ${body.error_description ?? ""}`.trim());
160
+ return body;
161
+ }
162
+ export {
163
+ DEFAULT_ISSUER,
164
+ challengeFor,
165
+ exchangeCode,
166
+ handleCallback,
167
+ signIn
168
+ };