@wordkey/sdk 1.0.0 → 1.1.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/README.md +72 -6
- package/dist/index.d.mts +41 -4
- package/dist/index.d.ts +41 -4
- package/dist/index.js +359 -66
- package/dist/index.mjs +356 -64
- package/dist/server.d.mts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/{types-Be6QFOjD.d.mts → types-WFjkfsy6.d.mts} +17 -1
- package/dist/{types-Be6QFOjD.d.ts → types-WFjkfsy6.d.ts} +17 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -4,6 +4,9 @@ import {
|
|
|
4
4
|
} from "./chunk-JWCQBSIQ.mjs";
|
|
5
5
|
|
|
6
6
|
// src/WordKeyButton.tsx
|
|
7
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
8
|
+
|
|
9
|
+
// src/WordKeyModal.tsx
|
|
7
10
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
8
11
|
|
|
9
12
|
// src/fields.ts
|
|
@@ -70,6 +73,17 @@ var DeviceApprovalRequiredError = class extends Error {
|
|
|
70
73
|
this.name = "DeviceApprovalRequiredError";
|
|
71
74
|
}
|
|
72
75
|
};
|
|
76
|
+
function decodeToken(token) {
|
|
77
|
+
try {
|
|
78
|
+
const part = token.split(".")[1];
|
|
79
|
+
if (!part) return null;
|
|
80
|
+
const b64 = part.replace(/-/g, "+").replace(/_/g, "/");
|
|
81
|
+
const json = typeof atob === "function" ? atob(b64) : Buffer.from(b64, "base64").toString("utf8");
|
|
82
|
+
return JSON.parse(json);
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
73
87
|
async function verifyWord(apiUrl = DEFAULT_API_URL, publishableKey, word) {
|
|
74
88
|
const wordHash = await hashWord(word);
|
|
75
89
|
const res = await fetch(`${apiUrl}/api/auth/verify`, {
|
|
@@ -204,9 +218,11 @@ async function beginRedirectShare(input2) {
|
|
|
204
218
|
const publicJwk = await crypto.subtle.exportKey("jwk", pair.publicKey);
|
|
205
219
|
const privateKeyJwk = await crypto.subtle.exportKey("jwk", pair.privateKey);
|
|
206
220
|
const pending = {
|
|
221
|
+
kind: "login",
|
|
207
222
|
requestId: input2.requestId,
|
|
208
223
|
userId: input2.userId,
|
|
209
224
|
isNewUser: input2.isNewUser,
|
|
225
|
+
token: input2.token,
|
|
210
226
|
fields: input2.fields,
|
|
211
227
|
privateKeyJwk
|
|
212
228
|
};
|
|
@@ -242,10 +258,12 @@ async function consumePendingRedirect() {
|
|
|
242
258
|
}
|
|
243
259
|
if (envelope.requestId !== pending.requestId) return null;
|
|
244
260
|
sessionStorage.removeItem(PENDING_STORE);
|
|
245
|
-
if (envelope.
|
|
246
|
-
|
|
261
|
+
if (!envelope.epk || !envelope.iv || !envelope.ct) {
|
|
262
|
+
if (envelope.granted !== true) {
|
|
263
|
+
return { pending, result: { granted: false, values: {} } };
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
247
266
|
}
|
|
248
|
-
if (!envelope.epk || !envelope.iv || !envelope.ct) return null;
|
|
249
267
|
try {
|
|
250
268
|
const privateKey = await crypto.subtle.importKey(
|
|
251
269
|
"jwk",
|
|
@@ -276,13 +294,135 @@ async function consumePendingRedirect() {
|
|
|
276
294
|
const parsed = JSON.parse(new TextDecoder().decode(plain));
|
|
277
295
|
return {
|
|
278
296
|
pending,
|
|
279
|
-
result: {
|
|
297
|
+
result: {
|
|
298
|
+
granted: envelope.granted === true,
|
|
299
|
+
values: parsed.values ?? {}
|
|
300
|
+
},
|
|
301
|
+
signup: typeof parsed.signup?.token === "string" ? { token: parsed.signup.token } : void 0
|
|
280
302
|
};
|
|
281
303
|
} catch {
|
|
282
304
|
return null;
|
|
283
305
|
}
|
|
284
306
|
}
|
|
285
307
|
|
|
308
|
+
// src/signup-popup.ts
|
|
309
|
+
var SIGNUP_TIMEOUT_MS = 20 * 60 * 1e3;
|
|
310
|
+
function openSignupWindow(apiUrl, publishableKey) {
|
|
311
|
+
const w = 440;
|
|
312
|
+
const h = 700;
|
|
313
|
+
const left = Math.max(0, (window.screenX ?? 0) + (window.outerWidth - w) / 2);
|
|
314
|
+
const top = Math.max(0, (window.screenY ?? 0) + (window.outerHeight - h) / 3);
|
|
315
|
+
try {
|
|
316
|
+
return window.open(
|
|
317
|
+
`${apiUrl}/welcome?wk_sdk=1&business=${encodeURIComponent(publishableKey)}`,
|
|
318
|
+
"wordkey-signup",
|
|
319
|
+
`popup=yes,width=${w},height=${h},left=${left},top=${top}`
|
|
320
|
+
);
|
|
321
|
+
} catch {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function runSignupHandshake(input2) {
|
|
326
|
+
const { popup, apiOrigin, requestId, signal } = input2;
|
|
327
|
+
return new Promise((resolve) => {
|
|
328
|
+
let settled = false;
|
|
329
|
+
const cleanup = () => {
|
|
330
|
+
window.removeEventListener("message", onMsg);
|
|
331
|
+
clearInterval(closedPoll);
|
|
332
|
+
clearTimeout(timer);
|
|
333
|
+
signal?.removeEventListener("abort", onAbort);
|
|
334
|
+
};
|
|
335
|
+
const finish = (r) => {
|
|
336
|
+
if (settled) return;
|
|
337
|
+
settled = true;
|
|
338
|
+
cleanup();
|
|
339
|
+
resolve(r);
|
|
340
|
+
};
|
|
341
|
+
const onAbort = () => {
|
|
342
|
+
try {
|
|
343
|
+
popup.close();
|
|
344
|
+
} catch {
|
|
345
|
+
}
|
|
346
|
+
finish({ ok: false, cancelled: true });
|
|
347
|
+
};
|
|
348
|
+
const onMsg = (e) => {
|
|
349
|
+
if (e.origin !== apiOrigin || e.source !== popup) return;
|
|
350
|
+
const data = e.data;
|
|
351
|
+
if (!data || typeof data !== "object") return;
|
|
352
|
+
if (data.type === "wordkey:signup:ready") {
|
|
353
|
+
popup.postMessage(
|
|
354
|
+
{
|
|
355
|
+
type: "wordkey:signup:init",
|
|
356
|
+
requestId,
|
|
357
|
+
publishableKey: input2.publishableKey,
|
|
358
|
+
sdkDeviceId: input2.sdkDeviceId,
|
|
359
|
+
fields: input2.fields
|
|
360
|
+
},
|
|
361
|
+
apiOrigin
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
if (data.type === "wordkey:signup:result" && data.requestId === requestId) {
|
|
365
|
+
finish({
|
|
366
|
+
ok: data.ok === true && typeof data.token === "string",
|
|
367
|
+
token: typeof data.token === "string" ? data.token : void 0,
|
|
368
|
+
profileTicket: typeof data.profileTicket === "string" ? data.profileTicket : void 0
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
window.addEventListener("message", onMsg);
|
|
373
|
+
const closedPoll = setInterval(() => {
|
|
374
|
+
if (popup.closed) finish({ ok: false, closed: true });
|
|
375
|
+
}, 400);
|
|
376
|
+
const timer = setTimeout(
|
|
377
|
+
() => finish({ ok: false, closed: true }),
|
|
378
|
+
SIGNUP_TIMEOUT_MS
|
|
379
|
+
);
|
|
380
|
+
if (signal) {
|
|
381
|
+
if (signal.aborted) onAbort();
|
|
382
|
+
else signal.addEventListener("abort", onAbort);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
async function beginSignupRedirect(input2) {
|
|
387
|
+
const pair = await crypto.subtle.generateKey(
|
|
388
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
389
|
+
true,
|
|
390
|
+
["deriveKey"]
|
|
391
|
+
);
|
|
392
|
+
const publicJwk = await crypto.subtle.exportKey("jwk", pair.publicKey);
|
|
393
|
+
const privateKeyJwk = await crypto.subtle.exportKey("jwk", pair.privateKey);
|
|
394
|
+
const pending = {
|
|
395
|
+
kind: "signup",
|
|
396
|
+
requestId: input2.requestId,
|
|
397
|
+
fields: input2.fields,
|
|
398
|
+
privateKeyJwk
|
|
399
|
+
};
|
|
400
|
+
sessionStorage.setItem(PENDING_STORE, JSON.stringify(pending));
|
|
401
|
+
const returnUrl = window.location.href.split("#")[0];
|
|
402
|
+
const request = {
|
|
403
|
+
v: 1,
|
|
404
|
+
requestId: input2.requestId,
|
|
405
|
+
publishableKey: input2.publishableKey,
|
|
406
|
+
origin: window.location.origin,
|
|
407
|
+
returnUrl,
|
|
408
|
+
sdkPub: publicJwk,
|
|
409
|
+
sdkDeviceId: input2.sdkDeviceId,
|
|
410
|
+
fields: input2.fields
|
|
411
|
+
};
|
|
412
|
+
const b64 = encodeSignupRequest(request);
|
|
413
|
+
window.location.assign(
|
|
414
|
+
`${input2.apiUrl}/welcome?wk_sdk=1&business=${encodeURIComponent(
|
|
415
|
+
input2.publishableKey
|
|
416
|
+
)}#wksignup=${b64}`
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
function encodeSignupRequest(value) {
|
|
420
|
+
const bytes = new TextEncoder().encode(JSON.stringify(value));
|
|
421
|
+
let bin = "";
|
|
422
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
423
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
424
|
+
}
|
|
425
|
+
|
|
286
426
|
// src/fonts.ts
|
|
287
427
|
var loaded = false;
|
|
288
428
|
function ensureFont() {
|
|
@@ -379,19 +519,18 @@ var ghostBtn = {
|
|
|
379
519
|
fontFamily: FONT_BODY
|
|
380
520
|
};
|
|
381
521
|
|
|
382
|
-
// src/
|
|
522
|
+
// src/WordKeyModal.tsx
|
|
383
523
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
384
|
-
|
|
385
|
-
function WordKeyButton({
|
|
524
|
+
function WordKeyModal({
|
|
386
525
|
publishableKey,
|
|
526
|
+
open,
|
|
527
|
+
onClose,
|
|
387
528
|
onSuccess,
|
|
529
|
+
mode = "login",
|
|
388
530
|
onError,
|
|
389
|
-
size = "md",
|
|
390
|
-
label = "Word?",
|
|
391
531
|
requestedFields,
|
|
392
532
|
apiUrl = DEFAULT_API_URL
|
|
393
533
|
}) {
|
|
394
|
-
const [open, setOpen] = useState(false);
|
|
395
534
|
useEffect(() => {
|
|
396
535
|
ensureFont();
|
|
397
536
|
}, []);
|
|
@@ -401,55 +540,151 @@ function WordKeyButton({
|
|
|
401
540
|
resumed.current = true;
|
|
402
541
|
void consumePendingRedirect().then((r) => {
|
|
403
542
|
if (!r) return;
|
|
543
|
+
if (r.pending.kind === "signup") {
|
|
544
|
+
const token = r.signup?.token;
|
|
545
|
+
if (!token) {
|
|
546
|
+
onError?.({
|
|
547
|
+
code: "signup_incomplete",
|
|
548
|
+
message: "Signup did not complete. Please try again."
|
|
549
|
+
});
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
const claims = decodeToken(token);
|
|
553
|
+
onSuccess({
|
|
554
|
+
id: claims?.userId ?? "",
|
|
555
|
+
isNewUser: claims?.isNewUser ?? true,
|
|
556
|
+
authenticatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
557
|
+
token,
|
|
558
|
+
profile: r.pending.fields.length ? buildProfile(r.pending.fields, r.result.granted ? r.result.values : {}) : void 0
|
|
559
|
+
});
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
404
562
|
const profile = buildProfile(
|
|
405
563
|
r.pending.fields,
|
|
406
564
|
r.result.granted ? r.result.values : {}
|
|
407
565
|
);
|
|
408
566
|
onSuccess({
|
|
409
|
-
id: r.pending.userId,
|
|
410
|
-
isNewUser: r.pending.isNewUser,
|
|
567
|
+
id: r.pending.userId ?? "",
|
|
568
|
+
isNewUser: r.pending.isNewUser ?? false,
|
|
411
569
|
authenticatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
570
|
+
token: r.pending.token,
|
|
412
571
|
profile
|
|
413
572
|
});
|
|
414
573
|
});
|
|
415
574
|
}, []);
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
575
|
+
if (!open) return null;
|
|
576
|
+
const common = {
|
|
577
|
+
publishableKey,
|
|
578
|
+
apiUrl,
|
|
579
|
+
requestedFields,
|
|
580
|
+
onClose,
|
|
581
|
+
onSuccess: (u) => {
|
|
582
|
+
onClose();
|
|
583
|
+
onSuccess(u);
|
|
584
|
+
},
|
|
585
|
+
onError: (e) => onError?.(e)
|
|
586
|
+
};
|
|
587
|
+
return mode === "signup" ? /* @__PURE__ */ jsx(SignupModal, { ...common }) : /* @__PURE__ */ jsx(AuthModal, { ...common });
|
|
588
|
+
}
|
|
589
|
+
function SignupModal({
|
|
590
|
+
publishableKey,
|
|
591
|
+
apiUrl,
|
|
592
|
+
requestedFields,
|
|
593
|
+
onClose,
|
|
594
|
+
onSuccess,
|
|
595
|
+
onError
|
|
596
|
+
}) {
|
|
597
|
+
const fields = useMemo(
|
|
598
|
+
() => requestedFields ? resolveFields(requestedFields) : [],
|
|
599
|
+
[requestedFields]
|
|
600
|
+
);
|
|
601
|
+
const apiOrigin = useMemo(() => {
|
|
602
|
+
try {
|
|
603
|
+
return new URL(apiUrl).origin;
|
|
604
|
+
} catch {
|
|
605
|
+
return apiUrl;
|
|
606
|
+
}
|
|
607
|
+
}, [apiUrl]);
|
|
608
|
+
const [failed, setFailed] = useState(null);
|
|
609
|
+
const abortRef = useRef(null);
|
|
610
|
+
if (!abortRef.current) abortRef.current = new AbortController();
|
|
611
|
+
useEffect(() => () => abortRef.current?.abort(), []);
|
|
612
|
+
const started = useRef(false);
|
|
613
|
+
useEffect(() => {
|
|
614
|
+
if (started.current) return;
|
|
615
|
+
started.current = true;
|
|
616
|
+
const popup = openSignupWindow(apiUrl, publishableKey);
|
|
617
|
+
if (!popup) {
|
|
618
|
+
void beginSignupRedirect({
|
|
442
619
|
apiUrl,
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
620
|
+
requestId: newRequestId(),
|
|
621
|
+
publishableKey,
|
|
622
|
+
sdkDeviceId: getDeviceId(),
|
|
623
|
+
fields
|
|
624
|
+
});
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const requestId = newRequestId();
|
|
628
|
+
void (async () => {
|
|
629
|
+
const hs = await runSignupHandshake({
|
|
630
|
+
popup,
|
|
631
|
+
apiOrigin,
|
|
632
|
+
requestId,
|
|
633
|
+
publishableKey,
|
|
634
|
+
sdkDeviceId: getDeviceId(),
|
|
635
|
+
fields,
|
|
636
|
+
signal: abortRef.current?.signal
|
|
637
|
+
});
|
|
638
|
+
if (hs.cancelled) return;
|
|
639
|
+
if (hs.closed) {
|
|
640
|
+
onClose();
|
|
641
|
+
return;
|
|
450
642
|
}
|
|
451
|
-
|
|
452
|
-
|
|
643
|
+
if (!hs.ok || !hs.token) {
|
|
644
|
+
setFailed("Signup could not be completed. Please try again.");
|
|
645
|
+
onError({
|
|
646
|
+
code: "signup_failed",
|
|
647
|
+
message: "Signup could not be completed"
|
|
648
|
+
});
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const claims = decodeToken(hs.token);
|
|
652
|
+
const finish = (profile) => onSuccess({
|
|
653
|
+
id: claims?.userId ?? "",
|
|
654
|
+
isNewUser: claims?.isNewUser ?? true,
|
|
655
|
+
authenticatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
656
|
+
token: hs.token,
|
|
657
|
+
profile
|
|
658
|
+
});
|
|
659
|
+
if (fields.length === 0) {
|
|
660
|
+
finish();
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const share = await runPopupFlow({
|
|
664
|
+
popup,
|
|
665
|
+
apiOrigin,
|
|
666
|
+
requestId,
|
|
667
|
+
publishableKey,
|
|
668
|
+
ticket: hs.profileTicket,
|
|
669
|
+
fields,
|
|
670
|
+
signal: abortRef.current?.signal
|
|
671
|
+
});
|
|
672
|
+
if (share.cancelled) return;
|
|
673
|
+
finish(buildProfile(fields, share.granted ? share.values : {}));
|
|
674
|
+
})();
|
|
675
|
+
}, []);
|
|
676
|
+
return /* @__PURE__ */ jsx("div", { style: overlay, role: "dialog", "aria-modal": "true", onClick: onClose, children: /* @__PURE__ */ jsxs("div", { style: panel, onClick: (e) => e.stopPropagation(), children: [
|
|
677
|
+
/* @__PURE__ */ jsx(Header, {}),
|
|
678
|
+
failed ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
679
|
+
/* @__PURE__ */ jsx(ErrorText, { children: failed }),
|
|
680
|
+
/* @__PURE__ */ jsx(Row, { children: /* @__PURE__ */ jsx("button", { style: ghostBtn, onClick: onClose, children: "Close" }) })
|
|
681
|
+
] }) : /* @__PURE__ */ jsxs("div", { style: { textAlign: "center", padding: "12px 0" }, children: [
|
|
682
|
+
/* @__PURE__ */ jsx(Spinner, {}),
|
|
683
|
+
/* @__PURE__ */ jsx("h3", { style: { fontFamily: FONT_DISPLAY, fontSize: 22, margin: "4px 0" }, children: "Get your word in the WordKey window" }),
|
|
684
|
+
/* @__PURE__ */ jsx("p", { style: { color: C.muted, fontSize: 13 }, children: "Finish setting up in the window that just opened. Closing it cancels \u2014 nothing is created without you." }),
|
|
685
|
+
/* @__PURE__ */ jsx(Row, { children: /* @__PURE__ */ jsx("button", { style: ghostBtn, onClick: onClose, children: "Cancel" }) })
|
|
686
|
+
] })
|
|
687
|
+
] }) });
|
|
453
688
|
}
|
|
454
689
|
function AuthModal({
|
|
455
690
|
publishableKey,
|
|
@@ -484,7 +719,12 @@ function AuthModal({
|
|
|
484
719
|
setStep("authenticating");
|
|
485
720
|
try {
|
|
486
721
|
const result = await verifyWord(apiUrl, publishableKey, word);
|
|
487
|
-
await completeAuth(
|
|
722
|
+
await completeAuth(
|
|
723
|
+
result.userId,
|
|
724
|
+
result.isNewUser,
|
|
725
|
+
result.token,
|
|
726
|
+
result.profileTicket
|
|
727
|
+
);
|
|
488
728
|
} catch (e) {
|
|
489
729
|
if (e instanceof DeviceApprovalRequiredError) {
|
|
490
730
|
await startDeviceApproval();
|
|
@@ -496,11 +736,12 @@ function AuthModal({
|
|
|
496
736
|
onError({ code: "auth_failed", message });
|
|
497
737
|
}
|
|
498
738
|
}
|
|
499
|
-
async function completeAuth(userId, isNewUser, profileTicket) {
|
|
739
|
+
async function completeAuth(userId, isNewUser, token, profileTicket) {
|
|
500
740
|
const finish = (profile) => onSuccess({
|
|
501
741
|
id: userId,
|
|
502
742
|
isNewUser,
|
|
503
743
|
authenticatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
744
|
+
token,
|
|
504
745
|
profile
|
|
505
746
|
});
|
|
506
747
|
if (fields.length === 0) {
|
|
@@ -517,7 +758,8 @@ function AuthModal({
|
|
|
517
758
|
ticket: profileTicket,
|
|
518
759
|
fields,
|
|
519
760
|
userId,
|
|
520
|
-
isNewUser
|
|
761
|
+
isNewUser,
|
|
762
|
+
token
|
|
521
763
|
});
|
|
522
764
|
return;
|
|
523
765
|
}
|
|
@@ -589,11 +831,12 @@ function AuthModal({
|
|
|
589
831
|
const userId = claims?.userId ?? "";
|
|
590
832
|
const isNewUser = claims?.isNewUser ?? false;
|
|
591
833
|
if (fields.length === 0) {
|
|
592
|
-
await completeAuth(userId, isNewUser, data.profileTicket);
|
|
834
|
+
await completeAuth(userId, isNewUser, data.token, data.profileTicket);
|
|
593
835
|
} else {
|
|
594
836
|
pendingApproval.current = {
|
|
595
837
|
userId,
|
|
596
838
|
isNewUser,
|
|
839
|
+
token: data.token,
|
|
597
840
|
profileTicket: data.profileTicket
|
|
598
841
|
};
|
|
599
842
|
setStep("approved");
|
|
@@ -698,7 +941,8 @@ function AuthModal({
|
|
|
698
941
|
style: primaryBtn(false),
|
|
699
942
|
onClick: () => {
|
|
700
943
|
const p = pendingApproval.current;
|
|
701
|
-
if (p)
|
|
944
|
+
if (p)
|
|
945
|
+
void completeAuth(p.userId, p.isNewUser, p.token, p.profileTicket);
|
|
702
946
|
},
|
|
703
947
|
children: "Continue"
|
|
704
948
|
}
|
|
@@ -757,17 +1001,6 @@ function valuePresent(v) {
|
|
|
757
1001
|
if (typeof v === "string") return v.trim().length > 0;
|
|
758
1002
|
return Object.values(v).some((x) => x && String(x).trim().length > 0);
|
|
759
1003
|
}
|
|
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
1004
|
function buildProfile(fields, granted) {
|
|
772
1005
|
const profile = {};
|
|
773
1006
|
const custom = {};
|
|
@@ -816,6 +1049,65 @@ function Spinner() {
|
|
|
816
1049
|
}
|
|
817
1050
|
);
|
|
818
1051
|
}
|
|
1052
|
+
|
|
1053
|
+
// src/WordKeyButton.tsx
|
|
1054
|
+
import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1055
|
+
var sizePx = { sm: 20, md: 28, lg: 38 };
|
|
1056
|
+
function WordKeyButton({
|
|
1057
|
+
publishableKey,
|
|
1058
|
+
onSuccess,
|
|
1059
|
+
onError,
|
|
1060
|
+
size = "md",
|
|
1061
|
+
label,
|
|
1062
|
+
mode = "login",
|
|
1063
|
+
requestedFields,
|
|
1064
|
+
apiUrl = DEFAULT_API_URL
|
|
1065
|
+
}) {
|
|
1066
|
+
const [open, setOpen] = useState2(false);
|
|
1067
|
+
useEffect2(() => {
|
|
1068
|
+
ensureFont();
|
|
1069
|
+
}, []);
|
|
1070
|
+
return /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
1071
|
+
/* @__PURE__ */ jsx2(
|
|
1072
|
+
"button",
|
|
1073
|
+
{
|
|
1074
|
+
onClick: () => setOpen(true),
|
|
1075
|
+
style: {
|
|
1076
|
+
background: "transparent",
|
|
1077
|
+
border: "none",
|
|
1078
|
+
padding: 0,
|
|
1079
|
+
cursor: "pointer",
|
|
1080
|
+
fontFamily: FONT_DISPLAY,
|
|
1081
|
+
fontWeight: 600,
|
|
1082
|
+
fontSize: sizePx[size],
|
|
1083
|
+
lineHeight: 1
|
|
1084
|
+
},
|
|
1085
|
+
"aria-label": label ?? (mode === "signup" ? "Get your word" : "Word?"),
|
|
1086
|
+
children: mode === "signup" ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
1087
|
+
/* @__PURE__ */ jsx2("span", { style: { color: C.purple }, children: "Get your " }),
|
|
1088
|
+
/* @__PURE__ */ jsx2("span", { style: { color: C.purpleL }, children: "word" })
|
|
1089
|
+
] }) : /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
1090
|
+
/* @__PURE__ */ jsx2("span", { style: { color: C.purple }, children: "Word" }),
|
|
1091
|
+
/* @__PURE__ */ jsx2("span", { style: { color: C.purpleL }, children: "?" })
|
|
1092
|
+
] })
|
|
1093
|
+
}
|
|
1094
|
+
),
|
|
1095
|
+
/* @__PURE__ */ jsx2(
|
|
1096
|
+
WordKeyModal,
|
|
1097
|
+
{
|
|
1098
|
+
publishableKey,
|
|
1099
|
+
open,
|
|
1100
|
+
mode,
|
|
1101
|
+
onClose: () => setOpen(false),
|
|
1102
|
+
onSuccess,
|
|
1103
|
+
onError,
|
|
1104
|
+
requestedFields,
|
|
1105
|
+
apiUrl
|
|
1106
|
+
}
|
|
1107
|
+
)
|
|
1108
|
+
] });
|
|
1109
|
+
}
|
|
819
1110
|
export {
|
|
820
|
-
WordKeyButton
|
|
1111
|
+
WordKeyButton,
|
|
1112
|
+
WordKeyModal
|
|
821
1113
|
};
|
package/dist/server.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { W as WordKeyUser,
|
|
2
|
-
export { A as Address, a as WordKeyError,
|
|
1
|
+
import { W as WordKeyUser, c as AuthEvent } from './types-WFjkfsy6.mjs';
|
|
2
|
+
export { A as Address, a as WordKeyError, d as WordKeyProfile } from './types-WFjkfsy6.mjs';
|
|
3
3
|
|
|
4
4
|
interface WordKeyConfig {
|
|
5
5
|
/** Your SECRET key (wk_sec_...). Server-only — never ship it to a browser. */
|
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { W as WordKeyUser,
|
|
2
|
-
export { A as Address, a as WordKeyError,
|
|
1
|
+
import { W as WordKeyUser, c as AuthEvent } from './types-WFjkfsy6.js';
|
|
2
|
+
export { A as Address, a as WordKeyError, d as WordKeyProfile } from './types-WFjkfsy6.js';
|
|
3
3
|
|
|
4
4
|
interface WordKeyConfig {
|
|
5
5
|
/** Your SECRET key (wk_sec_...). Server-only — never ship it to a browser. */
|
|
@@ -33,9 +33,25 @@ interface WordKeyUser {
|
|
|
33
33
|
/** True on the user's first-ever authentication (i.e. account creation). */
|
|
34
34
|
isNewUser: boolean;
|
|
35
35
|
authenticatedAt: string;
|
|
36
|
+
/**
|
|
37
|
+
* Single-use verification token. Send it to your backend and call
|
|
38
|
+
* `wk.verify(token)` there — the id/isNewUser fields on this object are
|
|
39
|
+
* client-side claims and MUST NOT be trusted for anything security-relevant
|
|
40
|
+
* until your server has verified this token. Always present on users
|
|
41
|
+
* delivered by WordKeyButton onSuccess (SDK >= 1.0.1); absent on the result
|
|
42
|
+
* of `wk.verify` itself, which spends the token.
|
|
43
|
+
*/
|
|
44
|
+
token?: string;
|
|
36
45
|
/** Present only when requestedFields were granted; unfilled fields omitted. */
|
|
37
46
|
profile?: WordKeyProfile;
|
|
38
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* The two entry points of the protocol. "login" is Word? — a returning user
|
|
50
|
+
* types the word they already have. "signup" is Get-your-word — WordKey's
|
|
51
|
+
* first-party onboarding assigns a word, enrolls the device, and signs the
|
|
52
|
+
* new user in to your site in one flow. Both deliver the same WordKeyUser.
|
|
53
|
+
*/
|
|
54
|
+
type WordKeyMode = "login" | "signup";
|
|
39
55
|
interface WordKeyError {
|
|
40
56
|
code: string;
|
|
41
57
|
message: string;
|
|
@@ -46,4 +62,4 @@ interface AuthEvent {
|
|
|
46
62
|
at: string | null;
|
|
47
63
|
}
|
|
48
64
|
|
|
49
|
-
export type { Address as A, CustomField as C, ProfileField as P, RequestedField as R, WordKeyUser as W, WordKeyError as a,
|
|
65
|
+
export type { Address as A, CustomField as C, ProfileField as P, RequestedField as R, WordKeyUser as W, WordKeyError as a, WordKeyMode as b, AuthEvent as c, WordKeyProfile as d };
|
|
@@ -33,9 +33,25 @@ interface WordKeyUser {
|
|
|
33
33
|
/** True on the user's first-ever authentication (i.e. account creation). */
|
|
34
34
|
isNewUser: boolean;
|
|
35
35
|
authenticatedAt: string;
|
|
36
|
+
/**
|
|
37
|
+
* Single-use verification token. Send it to your backend and call
|
|
38
|
+
* `wk.verify(token)` there — the id/isNewUser fields on this object are
|
|
39
|
+
* client-side claims and MUST NOT be trusted for anything security-relevant
|
|
40
|
+
* until your server has verified this token. Always present on users
|
|
41
|
+
* delivered by WordKeyButton onSuccess (SDK >= 1.0.1); absent on the result
|
|
42
|
+
* of `wk.verify` itself, which spends the token.
|
|
43
|
+
*/
|
|
44
|
+
token?: string;
|
|
36
45
|
/** Present only when requestedFields were granted; unfilled fields omitted. */
|
|
37
46
|
profile?: WordKeyProfile;
|
|
38
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* The two entry points of the protocol. "login" is Word? — a returning user
|
|
50
|
+
* types the word they already have. "signup" is Get-your-word — WordKey's
|
|
51
|
+
* first-party onboarding assigns a word, enrolls the device, and signs the
|
|
52
|
+
* new user in to your site in one flow. Both deliver the same WordKeyUser.
|
|
53
|
+
*/
|
|
54
|
+
type WordKeyMode = "login" | "signup";
|
|
39
55
|
interface WordKeyError {
|
|
40
56
|
code: string;
|
|
41
57
|
message: string;
|
|
@@ -46,4 +62,4 @@ interface AuthEvent {
|
|
|
46
62
|
at: string | null;
|
|
47
63
|
}
|
|
48
64
|
|
|
49
|
-
export type { Address as A, CustomField as C, ProfileField as P, RequestedField as R, WordKeyUser as W, WordKeyError as a,
|
|
65
|
+
export type { Address as A, CustomField as C, ProfileField as P, RequestedField as R, WordKeyUser as W, WordKeyError as a, WordKeyMode as b, AuthEvent as c, WordKeyProfile as d };
|