@openkey/sdk 0.12.0-beta.0 → 0.12.0-beta.2

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.d.mts CHANGED
@@ -126,6 +126,8 @@ interface SignOutAcknowledgement {
126
126
  interface OpenKeyError {
127
127
  code: 'USER_CANCELLED' | 'POPUP_BLOCKED' | 'TIMEOUT' | 'NO_KEY' | 'UNAUTHORIZED' | 'UNKNOWN' | 'STATE_MISMATCH';
128
128
  message: string;
129
+ /** On `POPUP_BLOCKED`: `'embedded-webview'` when the page runs in an app WebView that cannot open popups. */
130
+ reason?: 'embedded-webview';
129
131
  }
130
132
  /** OAuth configuration for third-party apps */
131
133
  interface OAuthConfig {
@@ -388,6 +390,12 @@ declare class OpenKey {
388
390
  * the SDK never holds an OpenKey session token. Local SDK state is cleared
389
391
  * before opening the widget, including when remote revocation is
390
392
  * unavailable.
393
+ *
394
+ * Rejects without waiting for the timeout when the widget cannot be shown
395
+ * (code `POPUP_BLOCKED`): the iframe did not load and the popup fallback is
396
+ * blocked, or the app is an embedded WebView that cannot open popups.
397
+ * Remote revocation did not happen then, so clearing any app session is the
398
+ * caller's decision.
391
399
  */
392
400
  signOut(opts?: {
393
401
  mode?: OpenKeyMode;
package/dist/index.d.ts CHANGED
@@ -126,6 +126,8 @@ interface SignOutAcknowledgement {
126
126
  interface OpenKeyError {
127
127
  code: 'USER_CANCELLED' | 'POPUP_BLOCKED' | 'TIMEOUT' | 'NO_KEY' | 'UNAUTHORIZED' | 'UNKNOWN' | 'STATE_MISMATCH';
128
128
  message: string;
129
+ /** On `POPUP_BLOCKED`: `'embedded-webview'` when the page runs in an app WebView that cannot open popups. */
130
+ reason?: 'embedded-webview';
129
131
  }
130
132
  /** OAuth configuration for third-party apps */
131
133
  interface OAuthConfig {
@@ -388,6 +390,12 @@ declare class OpenKey {
388
390
  * the SDK never holds an OpenKey session token. Local SDK state is cleared
389
391
  * before opening the widget, including when remote revocation is
390
392
  * unavailable.
393
+ *
394
+ * Rejects without waiting for the timeout when the widget cannot be shown
395
+ * (code `POPUP_BLOCKED`): the iframe did not load and the popup fallback is
396
+ * blocked, or the app is an embedded WebView that cannot open popups.
397
+ * Remote revocation did not happen then, so clearing any app session is the
398
+ * caller's decision.
391
399
  */
392
400
  signOut(opts?: {
393
401
  mode?: OpenKeyMode;
package/dist/index.js CHANGED
@@ -37,9 +37,9 @@ module.exports = __toCommonJS(index_exports);
37
37
  var y = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
38
38
  function u(e) {
39
39
  let n = "", t = e.length;
40
- for (let r = 0; r < t; r += 3) {
41
- let i = e[r], s = r + 1 < t ? e[r + 1] : 0, a = r + 2 < t ? e[r + 2] : 0, c = i << 16 | s << 8 | a;
42
- n += y[c >> 18 & 63], n += y[c >> 12 & 63], n += r + 1 < t ? y[c >> 6 & 63] : "", n += r + 2 < t ? y[c & 63] : "";
40
+ for (let i = 0; i < t; i += 3) {
41
+ let r = e[i], s = i + 1 < t ? e[i + 1] : 0, a = i + 2 < t ? e[i + 2] : 0, c = r << 16 | s << 8 | a;
42
+ n += y[c >> 18 & 63], n += y[c >> 12 & 63], n += i + 1 < t ? y[c >> 6 & 63] : "", n += i + 2 < t ? y[c & 63] : "";
43
43
  }
44
44
  return n.replace(/\+/g, "-").replace(/\//g, "_");
45
45
  }
@@ -48,20 +48,20 @@ function B() {
48
48
  return crypto.getRandomValues(e), u(e);
49
49
  }
50
50
  async function I(e) {
51
- let t = new TextEncoder().encode(e), r = await crypto.subtle.digest("SHA-256", t);
52
- return new Uint8Array(r);
51
+ let t = new TextEncoder().encode(e), i = await crypto.subtle.digest("SHA-256", t);
52
+ return new Uint8Array(i);
53
53
  }
54
54
  async function W(e, n) {
55
- let r = await (n ?? I)(e);
56
- return u(r);
55
+ let i = await (n ?? I)(e);
56
+ return u(i);
57
57
  }
58
58
  function j() {
59
59
  let e = new Uint8Array(16);
60
60
  return crypto.getRandomValues(e), u(e);
61
61
  }
62
62
  function G(e) {
63
- let { host: n, clientId: t, redirectUri: r, codeChallenge: i, state: s, scopes: a, authorizePath: c = "/api/auth/oauth2/authorize" } = e, d = new URL(c, n);
64
- return d.searchParams.set("client_id", t), d.searchParams.set("redirect_uri", r), d.searchParams.set("response_type", "code"), d.searchParams.set("scope", a.join(" ")), d.searchParams.set("state", s), d.searchParams.set("code_challenge", i), d.searchParams.set("code_challenge_method", "S256"), d.toString();
63
+ let { host: n, clientId: t, redirectUri: i, codeChallenge: r, state: s, scopes: a, authorizePath: c = "/api/auth/oauth2/authorize" } = e, d = new URL(c, n);
64
+ return d.searchParams.set("client_id", t), d.searchParams.set("redirect_uri", i), d.searchParams.set("response_type", "code"), d.searchParams.set("scope", a.join(" ")), d.searchParams.set("state", s), d.searchParams.set("code_challenge", r), d.searchParams.set("code_challenge_method", "S256"), d.toString();
65
65
  }
66
66
 
67
67
  // src/nostr.ts
@@ -265,6 +265,33 @@ var OpenKeyNostr = class {
265
265
  }
266
266
  };
267
267
 
268
+ // src/popup-capability.ts
269
+ function isEmbeddedAppWebView(env) {
270
+ const target = env ?? (typeof window === "undefined" ? void 0 : window);
271
+ if (!target) return false;
272
+ if (target.Capacitor?.isNativePlatform?.() === true) return true;
273
+ if (target.ReactNativeWebView) return true;
274
+ const userAgent = target.navigator?.userAgent ?? "";
275
+ if (/\bAndroid\b[^)]*;\s*wv\b/.test(userAgent)) return true;
276
+ if (/\b(iPhone|iPad|iPod)\b/.test(userAgent) && /AppleWebKit/.test(userAgent) && !/Safari\//.test(userAgent)) {
277
+ return true;
278
+ }
279
+ return false;
280
+ }
281
+ function isUsablePopup(popup) {
282
+ return !!popup && popup.closed !== true;
283
+ }
284
+ function popupUnavailableError(embeddedWebView) {
285
+ if (embeddedWebView) {
286
+ return {
287
+ code: "POPUP_BLOCKED",
288
+ message: "Popups are not available in this embedded app WebView. Make sure the OpenKey host is allowed by frame-src, or use native sign-in.",
289
+ reason: "embedded-webview"
290
+ };
291
+ }
292
+ return { code: "POPUP_BLOCKED", message: "Popup was blocked. Please allow popups for this site." };
293
+ }
294
+
268
295
  // src/index.ts
269
296
  function passkeysQueryFlag(passkeysSupported) {
270
297
  return passkeysSupported ? "" : "&passkeys=false";
@@ -686,6 +713,12 @@ var OpenKey = class {
686
713
  * the SDK never holds an OpenKey session token. Local SDK state is cleared
687
714
  * before opening the widget, including when remote revocation is
688
715
  * unavailable.
716
+ *
717
+ * Rejects without waiting for the timeout when the widget cannot be shown
718
+ * (code `POPUP_BLOCKED`): the iframe did not load and the popup fallback is
719
+ * blocked, or the app is an embedded WebView that cannot open popups.
720
+ * Remote revocation did not happen then, so clearing any app session is the
721
+ * caller's decision.
689
722
  */
690
723
  async signOut(opts) {
691
724
  const requestId = `ok-signout-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
@@ -1176,19 +1209,19 @@ var OpenKey = class {
1176
1209
  openOAuthPopup(url, state, redirectUri, resolve, reject) {
1177
1210
  const left = window.screenX + (window.outerWidth - POPUP_WIDTH) / 2;
1178
1211
  const top = window.screenY + (window.outerHeight - POPUP_HEIGHT) / 2;
1179
- this.popup = window.open(
1212
+ const popup = window.open(
1180
1213
  url,
1181
1214
  "openkey-oauth",
1182
1215
  `width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},popup=true`
1183
1216
  );
1184
- if (!this.popup) {
1185
- showToast("Popup was blocked. Please allow popups for this site.", "error");
1186
- reject({
1187
- code: "POPUP_BLOCKED",
1188
- message: "Popup was blocked. Please allow popups or use redirect mode."
1189
- });
1217
+ if (!isUsablePopup(popup)) {
1218
+ this.popup = null;
1219
+ const embedded = isEmbeddedAppWebView();
1220
+ if (!embedded) showToast("Popup was blocked. Please allow popups for this site.", "error");
1221
+ reject(embedded ? popupUnavailableError(true) : { code: "POPUP_BLOCKED", message: "Popup was blocked. Please allow popups or use redirect mode." });
1190
1222
  return;
1191
1223
  }
1224
+ this.popup = popup;
1192
1225
  const redirectHost = new URL(redirectUri).origin;
1193
1226
  const pollInterval = setInterval(() => {
1194
1227
  try {
@@ -1372,6 +1405,11 @@ var OpenKey = class {
1372
1405
  const readyTimeout = setTimeout(() => {
1373
1406
  if (readyReceived || settled) return;
1374
1407
  cleanup();
1408
+ if (isEmbeddedAppWebView()) {
1409
+ console.warn("OpenKey: iframe did not load in an embedded app WebView, which cannot open a popup. Add frame-src https://openkey.so to your CSP.");
1410
+ settle(() => reject(popupUnavailableError(true)));
1411
+ return;
1412
+ }
1375
1413
  console.warn("OpenKey: iframe blocked by CSP, falling back to popup. Add frame-src https://openkey.so to your CSP.");
1376
1414
  showToast();
1377
1415
  const popupUrl = `${this.host}/widget/${action}?origin=${origin}${passkeysQueryFlag(this.passkeysSupported)}`;
@@ -1470,15 +1508,14 @@ var OpenKey = class {
1470
1508
  `openkey-${action}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
1471
1509
  `width=${POPUP_WIDTH},height=${POPUP_HEIGHT},left=${left},top=${top},popup=true`
1472
1510
  );
1473
- this.popup = popup;
1474
- if (!popup) {
1475
- showToast("Popup was blocked. Please allow popups for this site.", "error");
1476
- reject({
1477
- code: "POPUP_BLOCKED",
1478
- message: "Popup was blocked. Please allow popups for this site."
1479
- });
1511
+ if (!isUsablePopup(popup)) {
1512
+ this.popup = null;
1513
+ const embedded = isEmbeddedAppWebView();
1514
+ if (!embedded) showToast("Popup was blocked. Please allow popups for this site.", "error");
1515
+ reject(popupUnavailableError(embedded));
1480
1516
  return;
1481
1517
  }
1518
+ this.popup = popup;
1482
1519
  let settled = false;
1483
1520
  let cancel;
1484
1521
  const cleanup = () => {