@wordkey/sdk 1.0.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.mjs ADDED
@@ -0,0 +1,821 @@
1
+ import {
2
+ DEFAULT_API_URL,
3
+ FIELD_LABELS
4
+ } from "./chunk-JWCQBSIQ.mjs";
5
+
6
+ // src/WordKeyButton.tsx
7
+ import { useEffect, useMemo, useRef, useState } from "react";
8
+
9
+ // src/fields.ts
10
+ var STANDARD = {
11
+ name: { key: "name", label: FIELD_LABELS.name, type: "text", isStandard: true },
12
+ email: { key: "email", label: FIELD_LABELS.email, type: "email", isStandard: true },
13
+ phone: { key: "phone", label: FIELD_LABELS.phone, type: "tel", isStandard: true },
14
+ shipping_address: {
15
+ key: "shipping_address",
16
+ label: FIELD_LABELS.shipping_address,
17
+ type: "address",
18
+ isStandard: true
19
+ }
20
+ };
21
+ function resolveFields(requested) {
22
+ return requested.map((f) => {
23
+ if (typeof f === "string") {
24
+ const base = STANDARD[f];
25
+ if (base) return { ...base, required: false };
26
+ return { key: f, label: f, type: "text", required: false, isStandard: false };
27
+ }
28
+ return {
29
+ key: f.key,
30
+ label: f.label,
31
+ type: f.type,
32
+ required: f.required ?? false,
33
+ options: f.options,
34
+ isStandard: false
35
+ };
36
+ });
37
+ }
38
+
39
+ // src/auth.ts
40
+ var PROTOCOL_SALT = "wordkey.v1.protocol.salt";
41
+ async function hashWord(word) {
42
+ const data = new TextEncoder().encode(word.toLowerCase().trim() + PROTOCOL_SALT);
43
+ const digest = await crypto.subtle.digest("SHA-256", data);
44
+ return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
45
+ }
46
+ function generateHolderSecret() {
47
+ const bytes = new Uint8Array(32);
48
+ crypto.getRandomValues(bytes);
49
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
50
+ }
51
+ var DEVICE_STORE = "wordkey.device_id";
52
+ function getDeviceId() {
53
+ if (typeof window === "undefined") return "server";
54
+ try {
55
+ const existing = localStorage.getItem(DEVICE_STORE);
56
+ if (existing) return existing;
57
+ const bytes = new Uint8Array(16);
58
+ crypto.getRandomValues(bytes);
59
+ const id = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
60
+ localStorage.setItem(DEVICE_STORE, id);
61
+ return id;
62
+ } catch {
63
+ return "ephemeral";
64
+ }
65
+ }
66
+ var DeviceApprovalRequiredError = class extends Error {
67
+ code = "device_approval_required";
68
+ constructor(message) {
69
+ super(message);
70
+ this.name = "DeviceApprovalRequiredError";
71
+ }
72
+ };
73
+ async function verifyWord(apiUrl = DEFAULT_API_URL, publishableKey, word) {
74
+ const wordHash = await hashWord(word);
75
+ const res = await fetch(`${apiUrl}/api/auth/verify`, {
76
+ method: "POST",
77
+ headers: { "Content-Type": "application/json" },
78
+ body: JSON.stringify({ wordHash, deviceId: getDeviceId(), publishableKey })
79
+ });
80
+ const json = await res.json();
81
+ if (!res.ok) {
82
+ if (json?.error?.code === "device_approval_required") {
83
+ throw new DeviceApprovalRequiredError(
84
+ json?.error?.message ?? "Approve this sign-in from your phone."
85
+ );
86
+ }
87
+ throw new Error(json?.error?.message ?? "Authentication failed");
88
+ }
89
+ return json;
90
+ }
91
+
92
+ // src/profile-popup.ts
93
+ var POPUP_TIMEOUT_MS = 5 * 60 * 1e3;
94
+ var PENDING_STORE = "wordkey.pending_share";
95
+ var RESULT_HASH = "#wordkey_result=";
96
+ function b64urlEncode(bytes) {
97
+ let bin = "";
98
+ for (const b of bytes) bin += String.fromCharCode(b);
99
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
100
+ }
101
+ function b64urlDecode(s) {
102
+ const b64 = s.replace(/-/g, "+").replace(/_/g, "/");
103
+ const bin = atob(b64 + "=".repeat((4 - b64.length % 4) % 4));
104
+ const out = new Uint8Array(bin.length);
105
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
106
+ return out;
107
+ }
108
+ function encodeJson(value) {
109
+ return b64urlEncode(new TextEncoder().encode(JSON.stringify(value)));
110
+ }
111
+ function decodeJson(s) {
112
+ try {
113
+ return JSON.parse(new TextDecoder().decode(b64urlDecode(s)));
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+ function newRequestId() {
119
+ const bytes = new Uint8Array(16);
120
+ crypto.getRandomValues(bytes);
121
+ return b64urlEncode(bytes);
122
+ }
123
+ function openConsentWindow(apiUrl) {
124
+ const w = 440;
125
+ const h = 660;
126
+ const left = Math.max(0, (window.screenX ?? 0) + (window.outerWidth - w) / 2);
127
+ const top = Math.max(0, (window.screenY ?? 0) + (window.outerHeight - h) / 3);
128
+ try {
129
+ return window.open(
130
+ `${apiUrl}/authorize`,
131
+ "wordkey-authorize",
132
+ `popup=yes,width=${w},height=${h},left=${left},top=${top}`
133
+ );
134
+ } catch {
135
+ return null;
136
+ }
137
+ }
138
+ function runPopupFlow(input2) {
139
+ const { popup, apiOrigin, requestId, signal } = input2;
140
+ return new Promise((resolve) => {
141
+ let settled = false;
142
+ const cleanup = () => {
143
+ window.removeEventListener("message", onMsg);
144
+ clearInterval(closedPoll);
145
+ clearTimeout(timer);
146
+ signal?.removeEventListener("abort", onAbort);
147
+ };
148
+ const finish = (r) => {
149
+ if (settled) return;
150
+ settled = true;
151
+ cleanup();
152
+ resolve(r);
153
+ };
154
+ const onAbort = () => {
155
+ try {
156
+ popup.close();
157
+ } catch {
158
+ }
159
+ finish({ granted: false, values: {}, cancelled: true });
160
+ };
161
+ const onMsg = (e) => {
162
+ if (e.origin !== apiOrigin || e.source !== popup) return;
163
+ const data = e.data;
164
+ if (!data || typeof data !== "object") return;
165
+ if (data.type === "wordkey:authorize:ready") {
166
+ popup.postMessage(
167
+ {
168
+ type: "wordkey:authorize:init",
169
+ requestId,
170
+ publishableKey: input2.publishableKey,
171
+ ticket: input2.ticket,
172
+ fields: input2.fields
173
+ },
174
+ apiOrigin
175
+ );
176
+ }
177
+ if (data.type === "wordkey:profile:result" && data.requestId === requestId) {
178
+ finish({
179
+ granted: data.granted === true,
180
+ values: data.values ?? {}
181
+ });
182
+ }
183
+ };
184
+ window.addEventListener("message", onMsg);
185
+ const closedPoll = setInterval(() => {
186
+ if (popup.closed) finish({ granted: false, values: {}, closed: true });
187
+ }, 400);
188
+ const timer = setTimeout(
189
+ () => finish({ granted: false, values: {} }),
190
+ POPUP_TIMEOUT_MS
191
+ );
192
+ if (signal) {
193
+ if (signal.aborted) onAbort();
194
+ else signal.addEventListener("abort", onAbort);
195
+ }
196
+ });
197
+ }
198
+ async function beginRedirectShare(input2) {
199
+ const pair = await crypto.subtle.generateKey(
200
+ { name: "ECDH", namedCurve: "P-256" },
201
+ true,
202
+ ["deriveKey"]
203
+ );
204
+ const publicJwk = await crypto.subtle.exportKey("jwk", pair.publicKey);
205
+ const privateKeyJwk = await crypto.subtle.exportKey("jwk", pair.privateKey);
206
+ const pending = {
207
+ requestId: input2.requestId,
208
+ userId: input2.userId,
209
+ isNewUser: input2.isNewUser,
210
+ fields: input2.fields,
211
+ privateKeyJwk
212
+ };
213
+ sessionStorage.setItem(PENDING_STORE, JSON.stringify(pending));
214
+ const returnUrl = window.location.href.split("#")[0];
215
+ const request = {
216
+ v: 1,
217
+ requestId: input2.requestId,
218
+ publishableKey: input2.publishableKey,
219
+ ticket: input2.ticket,
220
+ fields: input2.fields,
221
+ origin: window.location.origin,
222
+ returnUrl,
223
+ sdkPub: publicJwk
224
+ };
225
+ window.location.assign(`${input2.apiUrl}/authorize#wkreq=${encodeJson(request)}`);
226
+ }
227
+ async function consumePendingRedirect() {
228
+ if (typeof window === "undefined") return null;
229
+ const hash = window.location.hash;
230
+ if (!hash.startsWith(RESULT_HASH)) return null;
231
+ const stored = sessionStorage.getItem(PENDING_STORE);
232
+ const envelope = decodeJson(
233
+ hash.slice(RESULT_HASH.length)
234
+ );
235
+ history.replaceState(null, "", window.location.pathname + window.location.search);
236
+ if (!stored || !envelope) return null;
237
+ let pending;
238
+ try {
239
+ pending = JSON.parse(stored);
240
+ } catch {
241
+ return null;
242
+ }
243
+ if (envelope.requestId !== pending.requestId) return null;
244
+ sessionStorage.removeItem(PENDING_STORE);
245
+ if (envelope.granted !== true) {
246
+ return { pending, result: { granted: false, values: {} } };
247
+ }
248
+ if (!envelope.epk || !envelope.iv || !envelope.ct) return null;
249
+ try {
250
+ const privateKey = await crypto.subtle.importKey(
251
+ "jwk",
252
+ pending.privateKeyJwk,
253
+ { name: "ECDH", namedCurve: "P-256" },
254
+ false,
255
+ ["deriveKey"]
256
+ );
257
+ const peer = await crypto.subtle.importKey(
258
+ "jwk",
259
+ envelope.epk,
260
+ { name: "ECDH", namedCurve: "P-256" },
261
+ false,
262
+ []
263
+ );
264
+ const key = await crypto.subtle.deriveKey(
265
+ { name: "ECDH", public: peer },
266
+ privateKey,
267
+ { name: "AES-GCM", length: 256 },
268
+ false,
269
+ ["decrypt"]
270
+ );
271
+ const plain = await crypto.subtle.decrypt(
272
+ { name: "AES-GCM", iv: b64urlDecode(envelope.iv) },
273
+ key,
274
+ b64urlDecode(envelope.ct)
275
+ );
276
+ const parsed = JSON.parse(new TextDecoder().decode(plain));
277
+ return {
278
+ pending,
279
+ result: { granted: true, values: parsed.values ?? {} }
280
+ };
281
+ } catch {
282
+ return null;
283
+ }
284
+ }
285
+
286
+ // src/fonts.ts
287
+ var loaded = false;
288
+ function ensureFont() {
289
+ if (loaded || typeof document === "undefined") return;
290
+ loaded = true;
291
+ const href = "https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@600&display=swap";
292
+ const link = document.createElement("link");
293
+ link.rel = "stylesheet";
294
+ link.href = href;
295
+ link.media = "print";
296
+ link.onload = () => {
297
+ link.media = "all";
298
+ };
299
+ document.head.appendChild(link);
300
+ }
301
+
302
+ // src/theme.ts
303
+ var C = {
304
+ navy: "#0B1529",
305
+ navy2: "#111E38",
306
+ navy3: "#1A2B50",
307
+ purple: "#7C3AED",
308
+ purpleL: "#A78BFA",
309
+ purpleD: "#5B21B6",
310
+ cream: "#F4F4F0",
311
+ muted: "#7A8BAD",
312
+ teal: "#0D9488",
313
+ red: "#EF4444",
314
+ border: "rgba(124,58,237,0.2)"
315
+ };
316
+ var FONT_DISPLAY = "'Cormorant Garamond', Georgia, 'Times New Roman', serif";
317
+ var FONT_BODY = "'DM Sans', system-ui, -apple-system, Segoe UI, Roboto, sans-serif";
318
+ var overlay = {
319
+ position: "fixed",
320
+ inset: 0,
321
+ zIndex: 2147483e3,
322
+ display: "flex",
323
+ alignItems: "center",
324
+ justifyContent: "center",
325
+ background: "rgba(0,0,0,0.7)",
326
+ backdropFilter: "blur(4px)",
327
+ padding: 16,
328
+ fontFamily: FONT_BODY
329
+ };
330
+ var panel = {
331
+ width: "100%",
332
+ maxWidth: 420,
333
+ background: C.navy2,
334
+ border: `1px solid ${C.border}`,
335
+ borderRadius: 20,
336
+ boxShadow: "0 20px 60px rgba(0,0,0,0.5)",
337
+ padding: 28,
338
+ color: C.cream,
339
+ boxSizing: "border-box",
340
+ maxHeight: "90vh",
341
+ overflowY: "auto"
342
+ };
343
+ var input = {
344
+ width: "100%",
345
+ height: 46,
346
+ borderRadius: 12,
347
+ border: `1px solid ${C.border}`,
348
+ background: C.navy,
349
+ color: C.cream,
350
+ padding: "0 14px",
351
+ fontSize: 15,
352
+ fontFamily: FONT_BODY,
353
+ boxSizing: "border-box",
354
+ outline: "none"
355
+ };
356
+ function primaryBtn(disabled) {
357
+ return {
358
+ flex: 1,
359
+ height: 46,
360
+ borderRadius: 12,
361
+ border: "none",
362
+ background: disabled ? "#3a2d63" : C.purple,
363
+ color: C.cream,
364
+ fontSize: 15,
365
+ fontWeight: 600,
366
+ cursor: disabled ? "not-allowed" : "pointer",
367
+ fontFamily: FONT_BODY
368
+ };
369
+ }
370
+ var ghostBtn = {
371
+ flex: 1,
372
+ height: 46,
373
+ borderRadius: 12,
374
+ border: `1px solid ${C.border}`,
375
+ background: "transparent",
376
+ color: C.cream,
377
+ fontSize: 15,
378
+ cursor: "pointer",
379
+ fontFamily: FONT_BODY
380
+ };
381
+
382
+ // src/WordKeyButton.tsx
383
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
384
+ var sizePx = { sm: 20, md: 28, lg: 38 };
385
+ function WordKeyButton({
386
+ publishableKey,
387
+ onSuccess,
388
+ onError,
389
+ size = "md",
390
+ label = "Word?",
391
+ requestedFields,
392
+ apiUrl = DEFAULT_API_URL
393
+ }) {
394
+ const [open, setOpen] = useState(false);
395
+ useEffect(() => {
396
+ ensureFont();
397
+ }, []);
398
+ const resumed = useRef(false);
399
+ useEffect(() => {
400
+ if (resumed.current) return;
401
+ resumed.current = true;
402
+ void consumePendingRedirect().then((r) => {
403
+ if (!r) return;
404
+ const profile = buildProfile(
405
+ r.pending.fields,
406
+ r.result.granted ? r.result.values : {}
407
+ );
408
+ onSuccess({
409
+ id: r.pending.userId,
410
+ isNewUser: r.pending.isNewUser,
411
+ authenticatedAt: (/* @__PURE__ */ new Date()).toISOString(),
412
+ profile
413
+ });
414
+ });
415
+ }, []);
416
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
417
+ /* @__PURE__ */ jsxs(
418
+ "button",
419
+ {
420
+ onClick: () => setOpen(true),
421
+ style: {
422
+ background: "transparent",
423
+ border: "none",
424
+ padding: 0,
425
+ cursor: "pointer",
426
+ fontFamily: FONT_DISPLAY,
427
+ fontWeight: 600,
428
+ fontSize: sizePx[size],
429
+ lineHeight: 1
430
+ },
431
+ "aria-label": label,
432
+ children: [
433
+ /* @__PURE__ */ jsx("span", { style: { color: C.purple }, children: "Word" }),
434
+ /* @__PURE__ */ jsx("span", { style: { color: C.purpleL }, children: "?" })
435
+ ]
436
+ }
437
+ ),
438
+ open && /* @__PURE__ */ jsx(
439
+ AuthModal,
440
+ {
441
+ publishableKey,
442
+ apiUrl,
443
+ requestedFields,
444
+ onClose: () => setOpen(false),
445
+ onSuccess: (u) => {
446
+ setOpen(false);
447
+ onSuccess(u);
448
+ },
449
+ onError: (e) => onError?.(e)
450
+ }
451
+ )
452
+ ] });
453
+ }
454
+ function AuthModal({
455
+ publishableKey,
456
+ apiUrl,
457
+ requestedFields,
458
+ onClose,
459
+ onSuccess,
460
+ onError
461
+ }) {
462
+ const fields = useMemo(
463
+ () => requestedFields ? resolveFields(requestedFields) : [],
464
+ [requestedFields]
465
+ );
466
+ const apiOrigin = useMemo(() => {
467
+ try {
468
+ return new URL(apiUrl).origin;
469
+ } catch {
470
+ return apiUrl;
471
+ }
472
+ }, [apiUrl]);
473
+ const [step, setStep] = useState("word");
474
+ const [word, setWord] = useState("");
475
+ const [error, setError] = useState(null);
476
+ const abortRef = useRef(null);
477
+ if (!abortRef.current) abortRef.current = new AbortController();
478
+ useEffect(() => () => abortRef.current?.abort(), []);
479
+ const [approvalQr, setApprovalQr] = useState(null);
480
+ const [approvalUrl, setApprovalUrl] = useState(null);
481
+ const pendingApproval = useRef(null);
482
+ async function authenticate() {
483
+ setError(null);
484
+ setStep("authenticating");
485
+ try {
486
+ const result = await verifyWord(apiUrl, publishableKey, word);
487
+ await completeAuth(result.userId, result.isNewUser, result.profileTicket);
488
+ } catch (e) {
489
+ if (e instanceof DeviceApprovalRequiredError) {
490
+ await startDeviceApproval();
491
+ return;
492
+ }
493
+ const message = e.message;
494
+ setError(message);
495
+ setStep("error");
496
+ onError({ code: "auth_failed", message });
497
+ }
498
+ }
499
+ async function completeAuth(userId, isNewUser, profileTicket) {
500
+ const finish = (profile) => onSuccess({
501
+ id: userId,
502
+ isNewUser,
503
+ authenticatedAt: (/* @__PURE__ */ new Date()).toISOString(),
504
+ profile
505
+ });
506
+ if (fields.length === 0) {
507
+ finish();
508
+ return;
509
+ }
510
+ const requestId = newRequestId();
511
+ const popup = openConsentWindow(apiUrl);
512
+ if (!popup) {
513
+ await beginRedirectShare({
514
+ apiUrl,
515
+ requestId,
516
+ publishableKey,
517
+ ticket: profileTicket,
518
+ fields,
519
+ userId,
520
+ isNewUser
521
+ });
522
+ return;
523
+ }
524
+ setStep("sharing");
525
+ const result = await runPopupFlow({
526
+ popup,
527
+ apiOrigin,
528
+ requestId,
529
+ publishableKey,
530
+ ticket: profileTicket,
531
+ fields,
532
+ signal: abortRef.current?.signal
533
+ });
534
+ if (result.cancelled) return;
535
+ finish(buildProfile(fields, result.granted ? result.values : {}));
536
+ }
537
+ async function startDeviceApproval() {
538
+ setStep("device-approval");
539
+ setError(null);
540
+ try {
541
+ const holderSecret = generateHolderSecret();
542
+ const res = await fetch(`${apiUrl}/api/auth/device-approval/create`, {
543
+ method: "POST",
544
+ headers: { "Content-Type": "application/json" },
545
+ body: JSON.stringify({
546
+ publishableKey,
547
+ deviceId: getDeviceId(),
548
+ holderSecret
549
+ })
550
+ });
551
+ const data = await res.json();
552
+ if (!res.ok) {
553
+ throw new Error(data?.error?.message ?? "Could not start phone approval");
554
+ }
555
+ setApprovalUrl(data.approveUrl);
556
+ try {
557
+ const QRCode = await import("qrcode");
558
+ setApprovalQr(
559
+ await QRCode.toDataURL(data.approveUrl, {
560
+ margin: 1,
561
+ color: { dark: "#A78BFA", light: "#0B1529" }
562
+ })
563
+ );
564
+ } catch {
565
+ }
566
+ void pollDeviceApproval(data.sessionKey, holderSecret);
567
+ } catch (e) {
568
+ const message = e.message;
569
+ setError(message);
570
+ setStep("error");
571
+ onError({ code: "device_approval_failed", message });
572
+ }
573
+ }
574
+ async function pollDeviceApproval(sessionKey, holderSecret) {
575
+ for (let i = 0; i < 80; i++) {
576
+ await new Promise((r) => setTimeout(r, 1500));
577
+ let data;
578
+ try {
579
+ const res = await fetch(
580
+ `${apiUrl}/api/auth/device-approval/status/${sessionKey}`,
581
+ { headers: { "X-WordKey-Approval-Secret": holderSecret } }
582
+ );
583
+ data = await res.json();
584
+ } catch {
585
+ continue;
586
+ }
587
+ if (data.status === "approved" && data.token) {
588
+ const claims = decodeToken(data.token);
589
+ const userId = claims?.userId ?? "";
590
+ const isNewUser = claims?.isNewUser ?? false;
591
+ if (fields.length === 0) {
592
+ await completeAuth(userId, isNewUser, data.profileTicket);
593
+ } else {
594
+ pendingApproval.current = {
595
+ userId,
596
+ isNewUser,
597
+ profileTicket: data.profileTicket
598
+ };
599
+ setStep("approved");
600
+ }
601
+ return;
602
+ }
603
+ if (data.status === "expired") {
604
+ setError("The approval request expired. Try again.");
605
+ setStep("error");
606
+ onError({
607
+ code: "device_approval_expired",
608
+ message: "Approval request expired"
609
+ });
610
+ return;
611
+ }
612
+ }
613
+ setError("Approval timed out. Try again.");
614
+ setStep("error");
615
+ onError({ code: "device_approval_timeout", message: "Approval timed out" });
616
+ }
617
+ return /* @__PURE__ */ jsx("div", { style: overlay, role: "dialog", "aria-modal": "true", onClick: onClose, children: /* @__PURE__ */ jsxs("div", { style: panel, onClick: (e) => e.stopPropagation(), children: [
618
+ /* @__PURE__ */ jsx(Header, {}),
619
+ step === "word" && /* @__PURE__ */ jsxs(Fragment, { children: [
620
+ /* @__PURE__ */ jsx("p", { style: { color: C.muted, marginTop: 0, fontSize: 14 }, children: "Enter your word to continue." }),
621
+ /* @__PURE__ */ jsx(
622
+ WordInput,
623
+ {
624
+ autoFocus: true,
625
+ value: word,
626
+ onChange: (e) => setWord(e.target.value),
627
+ onKeyDown: (e) => {
628
+ if (e.key === "Enter" && word.trim().length >= 3)
629
+ void authenticate();
630
+ },
631
+ placeholder: "Your word",
632
+ style: { ...input, textTransform: "lowercase" }
633
+ }
634
+ ),
635
+ error && /* @__PURE__ */ jsx(ErrorText, { children: error }),
636
+ /* @__PURE__ */ jsxs(Row, { children: [
637
+ /* @__PURE__ */ jsx("button", { style: ghostBtn, onClick: onClose, children: "Cancel" }),
638
+ /* @__PURE__ */ jsx(
639
+ "button",
640
+ {
641
+ style: primaryBtn(word.trim().length < 3),
642
+ disabled: word.trim().length < 3,
643
+ onClick: () => void authenticate(),
644
+ children: "Continue"
645
+ }
646
+ )
647
+ ] }),
648
+ /* @__PURE__ */ jsx(Footer, {})
649
+ ] }),
650
+ step === "authenticating" && /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", padding: "24px 0" }, children: [
651
+ /* @__PURE__ */ jsx(Spinner, {}),
652
+ /* @__PURE__ */ jsx("p", { style: { color: C.muted }, children: "Verifying\u2026" })
653
+ ] }),
654
+ step === "sharing" && /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", padding: "12px 0" }, children: [
655
+ /* @__PURE__ */ jsx(Spinner, {}),
656
+ /* @__PURE__ */ jsx("h3", { style: { fontFamily: FONT_DISPLAY, fontSize: 22, margin: "4px 0" }, children: "Finish in the WordKey window" }),
657
+ /* @__PURE__ */ jsx("p", { style: { color: C.muted, fontSize: 13 }, children: "Review what this site will receive in the window that just opened. Closing it signs you in without sharing your details." })
658
+ ] }),
659
+ step === "device-approval" && /* @__PURE__ */ jsxs("div", { style: { textAlign: "center" }, children: [
660
+ /* @__PURE__ */ jsx("h3", { style: { fontFamily: FONT_DISPLAY, fontSize: 24, margin: "4px 0" }, children: "Approve on your phone" }),
661
+ /* @__PURE__ */ jsx("p", { style: { color: C.muted, fontSize: 14, marginTop: 4 }, children: "This device isn't recognized. Scan with the phone where your word lives, then approve." }),
662
+ approvalQr ? (
663
+ // eslint-disable-next-line @next/next/no-img-element
664
+ /* @__PURE__ */ jsx(
665
+ "img",
666
+ {
667
+ src: approvalQr,
668
+ alt: "Scan to approve on your phone",
669
+ width: 180,
670
+ height: 180,
671
+ style: { margin: "16px auto", display: "block", borderRadius: 12 }
672
+ }
673
+ )
674
+ ) : /* @__PURE__ */ jsx("div", { style: { padding: "24px 0" }, children: /* @__PURE__ */ jsx(Spinner, {}) }),
675
+ approvalUrl && /* @__PURE__ */ jsx(
676
+ "a",
677
+ {
678
+ href: approvalUrl,
679
+ target: "_blank",
680
+ rel: "noopener noreferrer",
681
+ style: { color: C.purpleL, fontSize: 12, wordBreak: "break-all" },
682
+ children: "Or open the approval link"
683
+ }
684
+ ),
685
+ /* @__PURE__ */ jsx("p", { style: { color: C.muted, fontSize: 12, marginTop: 12 }, children: "Waiting for approval\u2026" }),
686
+ error && /* @__PURE__ */ jsx(ErrorText, { children: error }),
687
+ /* @__PURE__ */ jsx(Row, { children: /* @__PURE__ */ jsx("button", { style: ghostBtn, onClick: onClose, children: "Cancel" }) })
688
+ ] }),
689
+ step === "approved" && /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", padding: "12px 0" }, children: [
690
+ /* @__PURE__ */ jsx("span", { style: { fontSize: 34, color: C.teal }, children: "\u2713" }),
691
+ /* @__PURE__ */ jsx("h3", { style: { fontFamily: FONT_DISPLAY, fontSize: 24, margin: "8px 0 4px" }, children: "Approved on your phone" }),
692
+ /* @__PURE__ */ jsx("p", { style: { color: C.muted, fontSize: 14 }, children: "Continue to review what this site will receive." }),
693
+ /* @__PURE__ */ jsxs(Row, { children: [
694
+ /* @__PURE__ */ jsx("button", { style: ghostBtn, onClick: onClose, children: "Cancel" }),
695
+ /* @__PURE__ */ jsx(
696
+ "button",
697
+ {
698
+ style: primaryBtn(false),
699
+ onClick: () => {
700
+ const p = pendingApproval.current;
701
+ if (p) void completeAuth(p.userId, p.isNewUser, p.profileTicket);
702
+ },
703
+ children: "Continue"
704
+ }
705
+ )
706
+ ] })
707
+ ] }),
708
+ step === "error" && /* @__PURE__ */ jsxs(Fragment, { children: [
709
+ /* @__PURE__ */ jsx(ErrorText, { children: error }),
710
+ /* @__PURE__ */ jsxs(Row, { children: [
711
+ /* @__PURE__ */ jsx("button", { style: ghostBtn, onClick: onClose, children: "Close" }),
712
+ /* @__PURE__ */ jsx(
713
+ "button",
714
+ {
715
+ style: primaryBtn(false),
716
+ onClick: () => {
717
+ setError(null);
718
+ setStep("word");
719
+ },
720
+ children: "Try again"
721
+ }
722
+ )
723
+ ] })
724
+ ] })
725
+ ] }) });
726
+ }
727
+ function WordInput({
728
+ style,
729
+ ...props
730
+ }) {
731
+ const [masked, setMasked] = useState(false);
732
+ useEffect(() => {
733
+ try {
734
+ if (typeof CSS !== "undefined" && CSS.supports("-webkit-text-security", "disc")) {
735
+ setMasked(true);
736
+ }
737
+ } catch {
738
+ }
739
+ }, []);
740
+ return /* @__PURE__ */ jsx(
741
+ "input",
742
+ {
743
+ type: masked ? "text" : "password",
744
+ name: "word",
745
+ autoComplete: "off",
746
+ autoCapitalize: "none",
747
+ autoCorrect: "off",
748
+ spellCheck: false,
749
+ inputMode: "text",
750
+ style: masked ? { ...style, WebkitTextSecurity: "disc" } : style,
751
+ ...props
752
+ }
753
+ );
754
+ }
755
+ function valuePresent(v) {
756
+ if (v === void 0) return false;
757
+ if (typeof v === "string") return v.trim().length > 0;
758
+ return Object.values(v).some((x) => x && String(x).trim().length > 0);
759
+ }
760
+ function decodeToken(token) {
761
+ try {
762
+ const part = token.split(".")[1];
763
+ if (!part) return null;
764
+ const b64 = part.replace(/-/g, "+").replace(/_/g, "/");
765
+ const json = typeof atob === "function" ? atob(b64) : Buffer.from(b64, "base64").toString("utf8");
766
+ return JSON.parse(json);
767
+ } catch {
768
+ return null;
769
+ }
770
+ }
771
+ function buildProfile(fields, granted) {
772
+ const profile = {};
773
+ const custom = {};
774
+ for (const f of fields) {
775
+ const v = granted[f.key];
776
+ if (!valuePresent(v)) continue;
777
+ if (f.key === "name" && typeof v === "string") profile.name = v;
778
+ else if (f.key === "email" && typeof v === "string") profile.email = v;
779
+ else if (f.key === "phone" && typeof v === "string") profile.phone = v;
780
+ else if (f.key === "shipping_address" && typeof v === "object")
781
+ profile.shippingAddress = v;
782
+ else if (typeof v === "string") custom[f.key] = v;
783
+ }
784
+ if (Object.keys(custom).length) profile.custom = custom;
785
+ return profile;
786
+ }
787
+ function Header() {
788
+ return /* @__PURE__ */ jsx("div", { style: { marginBottom: 14 }, children: /* @__PURE__ */ jsxs("span", { style: { fontFamily: FONT_DISPLAY, fontSize: 28, fontWeight: 600 }, children: [
789
+ /* @__PURE__ */ jsx("span", { style: { color: C.purple }, children: "Word" }),
790
+ /* @__PURE__ */ jsx("span", { style: { color: C.purpleL }, children: "?" })
791
+ ] }) });
792
+ }
793
+ function Footer() {
794
+ return /* @__PURE__ */ jsx("p", { style: { marginTop: 16, fontSize: 11, color: C.muted, textAlign: "center" }, children: "Secured by WordKey \xB7 one word, every door" });
795
+ }
796
+ function Row({ children }) {
797
+ return /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 10, marginTop: 20 }, children });
798
+ }
799
+ function ErrorText({ children }) {
800
+ return /* @__PURE__ */ jsx("p", { style: { color: C.red, fontSize: 13, marginTop: 12 }, children });
801
+ }
802
+ function Spinner() {
803
+ return /* @__PURE__ */ jsx(
804
+ "div",
805
+ {
806
+ style: {
807
+ width: 32,
808
+ height: 32,
809
+ margin: "0 auto 12px",
810
+ border: `3px solid ${C.border}`,
811
+ borderTopColor: C.purpleL,
812
+ borderRadius: "50%",
813
+ animation: "wk-spin 0.8s linear infinite"
814
+ },
815
+ children: /* @__PURE__ */ jsx("style", { children: "@keyframes wk-spin{to{transform:rotate(360deg)}}" })
816
+ }
817
+ );
818
+ }
819
+ export {
820
+ WordKeyButton
821
+ };