@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/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.granted !== true) {
246
- return { pending, result: { granted: false, values: {} } };
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: { granted: true, values: parsed.values ?? {} }
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/WordKeyButton.tsx
522
+ // src/WordKeyModal.tsx
383
523
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
384
- var sizePx = { sm: 20, md: 28, lg: 38 };
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
- 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,
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
- requestedFields,
444
- onClose: () => setOpen(false),
445
- onSuccess: (u) => {
446
- setOpen(false);
447
- onSuccess(u);
448
- },
449
- onError: (e) => onError?.(e)
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(result.userId, result.isNewUser, result.profileTicket);
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) void completeAuth(p.userId, p.isNewUser, p.profileTicket);
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, b as AuthEvent } from './types-Be6QFOjD.mjs';
2
- export { A as Address, a as WordKeyError, c as WordKeyProfile } from './types-Be6QFOjD.mjs';
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, b as AuthEvent } from './types-Be6QFOjD.js';
2
- export { A as Address, a as WordKeyError, c as WordKeyProfile } from './types-Be6QFOjD.js';
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, AuthEvent as b, WordKeyProfile as c };
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, AuthEvent as b, WordKeyProfile as c };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordkey/sdk",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Add the Word? button to your site in one afternoon. One word. Every door.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "main": "./dist/index.js",