@orangecheck/me-client 0.35.0 → 0.36.1

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 CHANGED
@@ -270,7 +270,18 @@ button.addEventListener('click', async () => {
270
270
  });
271
271
  ```
272
272
 
273
- `signInWithOc()` MUST be called inside a user-gesture handler (browsers block `window.open` outside of gestures). Returns `{ account, token }` on success, `null` on cancel / popup block / abort. Pins both `event.origin` AND `event.source === popup` on the postMessage handler so a same-origin attacker can't settle the promise.
273
+ `signInWithOc()` MUST be called inside a user-gesture handler (browsers block `window.open` outside of gestures). Returns `{ account, token }` on success, `null` on cancel / popup block / abort.
274
+
275
+ To tell those apart, `signInWithOcOutcome()` (0.36.0 or later) resolves with a `status`:
276
+
277
+ ```ts
278
+ import { signInWithOcOutcome } from '@orangecheck/me-client/popup';
279
+
280
+ const outcome = await signInWithOcOutcome();
281
+ if (outcome.status === 'blocked') showHint('Allow pop-ups for this site, then try again.');
282
+ if (outcome.status === 'signed_in') await keepSession(outcome.token);
283
+ // 'closed' (the person closed the window), 'aborted' (your signal), 'failed' (not a browser, or a malformed answer)
284
+ ``` Pins both `event.origin` AND `event.source === popup` on the postMessage handler so a same-origin attacker can't settle the promise.
274
285
 
275
286
  ## Custom origin
276
287
 
package/dist/popup.d.mts CHANGED
@@ -20,6 +20,18 @@ interface SignInWithOcOptions {
20
20
  features?: string;
21
21
  signal?: AbortSignal;
22
22
  }
23
+ type OcPopupOutcome = ({
24
+ status: 'signed_in';
25
+ } & OcPopupResult) | {
26
+ status: 'blocked';
27
+ } | {
28
+ status: 'closed';
29
+ } | {
30
+ status: 'aborted';
31
+ } | {
32
+ status: 'failed';
33
+ };
34
+ declare function signInWithOcOutcome(options?: SignInWithOcOptions): Promise<OcPopupOutcome>;
23
35
  declare function signInWithOc(options?: SignInWithOcOptions): Promise<OcPopupResult | null>;
24
36
 
25
- export { type OcPopupAccount, type OcPopupResult, type SignInWithOcOptions, signInWithOc };
37
+ export { type OcPopupAccount, type OcPopupOutcome, type OcPopupResult, type SignInWithOcOptions, signInWithOc, signInWithOcOutcome };
package/dist/popup.d.ts CHANGED
@@ -20,6 +20,18 @@ interface SignInWithOcOptions {
20
20
  features?: string;
21
21
  signal?: AbortSignal;
22
22
  }
23
+ type OcPopupOutcome = ({
24
+ status: 'signed_in';
25
+ } & OcPopupResult) | {
26
+ status: 'blocked';
27
+ } | {
28
+ status: 'closed';
29
+ } | {
30
+ status: 'aborted';
31
+ } | {
32
+ status: 'failed';
33
+ };
34
+ declare function signInWithOcOutcome(options?: SignInWithOcOptions): Promise<OcPopupOutcome>;
23
35
  declare function signInWithOc(options?: SignInWithOcOptions): Promise<OcPopupResult | null>;
24
36
 
25
- export { type OcPopupAccount, type OcPopupResult, type SignInWithOcOptions, signInWithOc };
37
+ export { type OcPopupAccount, type OcPopupOutcome, type OcPopupResult, type SignInWithOcOptions, signInWithOc, signInWithOcOutcome };
package/dist/popup.js CHANGED
@@ -4,20 +4,20 @@
4
4
  var DEFAULT_POPUP_ORIGIN = "https://me.ochk.io";
5
5
  var DEFAULT_POPUP_PATH = "/popup/signin";
6
6
  var DEFAULT_FEATURES = "width=480,height=720,resizable=yes,scrollbars=yes";
7
- function signInWithOc(options = {}) {
7
+ function signInWithOcOutcome(options = {}) {
8
8
  const popupOrigin = options.popupOrigin ?? DEFAULT_POPUP_ORIGIN;
9
9
  const popupPath = options.popupPath ?? DEFAULT_POPUP_PATH;
10
10
  const features = options.features ?? DEFAULT_FEATURES;
11
11
  return new Promise((resolve) => {
12
12
  if (typeof window === "undefined") {
13
- resolve(null);
13
+ resolve({ status: "failed" });
14
14
  return;
15
15
  }
16
16
  const url = new URL(popupPath, popupOrigin);
17
17
  url.searchParams.set("opener_origin", window.location.origin);
18
18
  const popup = window.open(url.toString(), "oc-signin", features);
19
19
  if (!popup) {
20
- resolve(null);
20
+ resolve({ status: "blocked" });
21
21
  return;
22
22
  }
23
23
  popup.focus();
@@ -43,28 +43,33 @@ function signInWithOc(options = {}) {
43
43
  const data = event.data;
44
44
  if (!data || data.type !== "oc-signin-success") return;
45
45
  if (!data.account || !data.token) {
46
- settle(null);
46
+ settle({ status: "failed" });
47
47
  return;
48
48
  }
49
- settle({ account: data.account, token: data.token });
49
+ settle({ status: "signed_in", account: data.account, token: data.token });
50
50
  }
51
51
  function onAbort() {
52
- settle(null);
52
+ settle({ status: "aborted" });
53
53
  }
54
54
  window.addEventListener("message", onMessage);
55
55
  if (options.signal) {
56
56
  if (options.signal.aborted) {
57
- settle(null);
57
+ settle({ status: "aborted" });
58
58
  return;
59
59
  }
60
60
  options.signal.addEventListener("abort", onAbort);
61
61
  }
62
62
  const closedTimer = setInterval(() => {
63
- if (popup && popup.closed) settle(null);
63
+ if (popup && popup.closed) settle({ status: "closed" });
64
64
  }, 500);
65
65
  });
66
66
  }
67
+ async function signInWithOc(options = {}) {
68
+ const outcome = await signInWithOcOutcome(options);
69
+ return outcome.status === "signed_in" ? { account: outcome.account, token: outcome.token } : null;
70
+ }
67
71
 
68
72
  exports.signInWithOc = signInWithOc;
73
+ exports.signInWithOcOutcome = signInWithOcOutcome;
69
74
  //# sourceMappingURL=popup.js.map
70
75
  //# sourceMappingURL=popup.js.map
package/dist/popup.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/popup.ts"],"names":[],"mappings":"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","file":"popup.js","sourcesContent":["/**\n * @orangecheck/me-client/popup\n *\n * Browser-side helper that opens the OC sign-in popup, listens for the\n * postMessage from me.ochk.io, and resolves with the verified session.\n *\n * import { signInWithOc } from '@orangecheck/me-client/popup';\n *\n * document.querySelector('#oc-signin')!.addEventListener('click', async () => {\n * const result = await signInWithOc();\n * if (!result) return;\n * // Your server verifies it and keeps it in an httpOnly cookie.\n * const r = await fetch('/api/auth/session', {\n * method: 'POST',\n * headers: { 'Content-Type': 'application/json' },\n * body: JSON.stringify({ token: result.token }),\n * });\n * if (r.ok) location.assign('/app');\n * });\n *\n * Why this lives in a separate entry point: it's browser-only (uses\n * window.open / addEventListener) and integrators that only need the\n * server-side verifier shouldn't pull popup orchestration into their\n * server bundle.\n */\n\nconst DEFAULT_POPUP_ORIGIN = 'https://me.ochk.io';\nconst DEFAULT_POPUP_PATH = '/popup/signin';\nconst DEFAULT_FEATURES = 'width=480,height=720,resizable=yes,scrollbars=yes';\n\nexport interface OcPopupAccount {\n id?: string;\n /** Opaque public identity · `did:oc:<32-hex>`. Stable across linking\n * events. The canonical identifier post auth-refactor (Dec 2025\n * +). */\n did_oc?: string;\n /** Primary linked Bitcoin address, if the user has one and is\n * surfacing it (same-origin family integrators get it inline;\n * cross-domain integrators need the bitcoin_address scope). */\n primary_btc?: string | null;\n /** Whether the user has a primary email linked (plaintext stays\n * on auth host; only the boolean reaches integrators). */\n has_email?: boolean;\n /** Legacy fields kept for back-compat with pre-refactor consumers.\n * Prefer did_oc + primary_btc. Removed in a future major. */\n address?: string;\n btc_address?: string;\n display_name?: string | null;\n nostr_npub?: string | null;\n wallet_provisioned?: boolean;\n email?: string;\n}\n\nexport interface OcPopupResult {\n /** Account payload from the auth host, unsigned: trust only the\n * verified token. `did_oc` is the account's opaque identifier, the\n * same at every site; the email and Bitcoin address stay private\n * unless the user grants a scope. */\n account: OcPopupAccount;\n /** The session JWT, bound to your origin. Hand it once to your own\n * server, which verifies it and keeps it in an httpOnly cookie; never\n * in localStorage, where any script on the page can read it. Family\n * sites (*.ochk.io) can ignore it: the Domain=.ochk.io cookie rides\n * home on its own. */\n token: string;\n}\n\nexport interface SignInWithOcOptions {\n /** Origin of the OC popup page. Defaults to https://me.ochk.io.\n * Override only for staging / preview environments. */\n popupOrigin?: string;\n /** Path on the popup origin. Defaults to /popup/signin. */\n popupPath?: string;\n /** window.open features string. Defaults to a 480x720 resizable\n * popup with scrollbars. */\n features?: string;\n /** AbortSignal that cancels the wait and closes the popup. */\n signal?: AbortSignal;\n}\n\n/**\n * Open the OC signin popup and resolve with the session result. Returns\n * `null` if the user closed the popup without completing signin, or if\n * the AbortSignal fired.\n *\n * MUST be called inside a user-gesture handler (e.g. a button onClick).\n * Browsers block `window.open` outside of user gestures.\n */\nexport function signInWithOc(options: SignInWithOcOptions = {}): Promise<OcPopupResult | null> {\n const popupOrigin = options.popupOrigin ?? DEFAULT_POPUP_ORIGIN;\n const popupPath = options.popupPath ?? DEFAULT_POPUP_PATH;\n const features = options.features ?? DEFAULT_FEATURES;\n\n return new Promise((resolve) => {\n if (typeof window === 'undefined') {\n resolve(null);\n return;\n }\n\n const url = new URL(popupPath, popupOrigin);\n url.searchParams.set('opener_origin', window.location.origin);\n const popup = window.open(url.toString(), 'oc-signin', features);\n if (!popup) {\n // Popup blocked. Caller can detect a null result and fall\n // back to a full-page redirect if desired.\n resolve(null);\n return;\n }\n popup.focus();\n\n let settled = false;\n const cleanup = () => {\n window.removeEventListener('message', onMessage);\n clearInterval(closedTimer);\n if (options.signal) options.signal.removeEventListener('abort', onAbort);\n };\n\n function settle(value: OcPopupResult | null) {\n if (settled) return;\n settled = true;\n cleanup();\n try {\n if (popup && !popup.closed) popup.close();\n } catch {\n /* popup may have closed itself first */\n }\n resolve(value);\n }\n\n function onMessage(event: MessageEvent) {\n // Origin pinning is necessary but not sufficient — same-origin\n // pages other than our popup can post the same shape. Pin both\n // origin AND source window so only the popup we opened can\n // settle this promise.\n if (event.origin !== popupOrigin) return;\n if (event.source !== popup) return;\n const data = event.data as\n | { type?: string; account?: OcPopupAccount; token?: string }\n | null;\n if (!data || data.type !== 'oc-signin-success') return;\n if (!data.account || !data.token) {\n // Malformed payload — treat as not-signed-in.\n settle(null);\n return;\n }\n settle({ account: data.account, token: data.token });\n }\n\n function onAbort() {\n settle(null);\n }\n\n window.addEventListener('message', onMessage);\n if (options.signal) {\n if (options.signal.aborted) {\n settle(null);\n return;\n }\n options.signal.addEventListener('abort', onAbort);\n }\n\n // Detect the user closing the popup without completing signin.\n const closedTimer = setInterval(() => {\n if (popup && popup.closed) settle(null);\n }, 500);\n });\n}\n"]}
1
+ {"version":3,"sources":["../src/popup.ts"],"names":[],"mappings":"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","file":"popup.js","sourcesContent":["/**\n * @orangecheck/me-client/popup\n *\n * Browser-side helper that opens the OC sign-in popup, listens for the\n * postMessage from me.ochk.io, and resolves with the verified session.\n *\n * import { signInWithOc } from '@orangecheck/me-client/popup';\n *\n * document.querySelector('#oc-signin')!.addEventListener('click', async () => {\n * const result = await signInWithOc();\n * if (!result) return;\n * // Your server verifies it and keeps it in an httpOnly cookie.\n * const r = await fetch('/api/auth/session', {\n * method: 'POST',\n * headers: { 'Content-Type': 'application/json' },\n * body: JSON.stringify({ token: result.token }),\n * });\n * if (r.ok) location.assign('/app');\n * });\n *\n * Why this lives in a separate entry point: it's browser-only (uses\n * window.open / addEventListener) and integrators that only need the\n * server-side verifier shouldn't pull popup orchestration into their\n * server bundle.\n */\n\nconst DEFAULT_POPUP_ORIGIN = 'https://me.ochk.io';\nconst DEFAULT_POPUP_PATH = '/popup/signin';\nconst DEFAULT_FEATURES = 'width=480,height=720,resizable=yes,scrollbars=yes';\n\nexport interface OcPopupAccount {\n id?: string;\n /** Opaque public identity · `did:oc:<32-hex>`. Stable across linking\n * events. The canonical identifier post auth-refactor (Dec 2025\n * +). */\n did_oc?: string;\n /** Primary linked Bitcoin address, if the user has one and is\n * surfacing it (same-origin family integrators get it inline;\n * cross-domain integrators need the bitcoin_address scope). */\n primary_btc?: string | null;\n /** Whether the user has a primary email linked (plaintext stays\n * on auth host; only the boolean reaches integrators). */\n has_email?: boolean;\n /** Legacy fields kept for back-compat with pre-refactor consumers.\n * Prefer did_oc + primary_btc. Removed in a future major. */\n address?: string;\n btc_address?: string;\n display_name?: string | null;\n nostr_npub?: string | null;\n wallet_provisioned?: boolean;\n email?: string;\n}\n\nexport interface OcPopupResult {\n /** Account payload from the auth host, unsigned: trust only the\n * verified token. `did_oc` is the account's opaque identifier, the\n * same at every site; the email and Bitcoin address stay private\n * unless the user grants a scope. */\n account: OcPopupAccount;\n /** The session JWT, bound to your origin. Hand it once to your own\n * server, which verifies it and keeps it in an httpOnly cookie; never\n * in localStorage, where any script on the page can read it. Family\n * sites (*.ochk.io) can ignore it: the Domain=.ochk.io cookie rides\n * home on its own. */\n token: string;\n}\n\nexport interface SignInWithOcOptions {\n /** Origin of the OC popup page. Defaults to https://me.ochk.io.\n * Override only for staging / preview environments. */\n popupOrigin?: string;\n /** Path on the popup origin. Defaults to /popup/signin. */\n popupPath?: string;\n /** window.open features string. Defaults to a 480x720 resizable\n * popup with scrollbars. */\n features?: string;\n /** AbortSignal that cancels the wait and closes the popup. */\n signal?: AbortSignal;\n}\n\n/**\n * How a sign-in popup ended:\n * - `signed_in` · the person signed in; `account` and `token` as from signInWithOc.\n * - `blocked` · the browser refused to open the window. Ask the person to\n * allow pop-ups for your site, then try again from a click.\n * - `closed` · the person closed the window without signing in.\n * - `aborted` · your AbortSignal fired.\n * - `failed` · not in a browser, or the popup's answer was malformed.\n */\nexport type OcPopupOutcome =\n | ({ status: 'signed_in' } & OcPopupResult)\n | { status: 'blocked' }\n | { status: 'closed' }\n | { status: 'aborted' }\n | { status: 'failed' };\n\n/**\n * Open the OC signin popup and resolve with how it ended (OcPopupOutcome),\n * so a blocked window and a closed one can be told apart.\n *\n * MUST be called inside a user-gesture handler (e.g. a button onClick).\n * Browsers block `window.open` outside of user gestures.\n */\nexport function signInWithOcOutcome(options: SignInWithOcOptions = {}): Promise<OcPopupOutcome> {\n const popupOrigin = options.popupOrigin ?? DEFAULT_POPUP_ORIGIN;\n const popupPath = options.popupPath ?? DEFAULT_POPUP_PATH;\n const features = options.features ?? DEFAULT_FEATURES;\n\n return new Promise((resolve) => {\n if (typeof window === 'undefined') {\n resolve({ status: 'failed' });\n return;\n }\n\n const url = new URL(popupPath, popupOrigin);\n url.searchParams.set('opener_origin', window.location.origin);\n const popup = window.open(url.toString(), 'oc-signin', features);\n if (!popup) {\n resolve({ status: 'blocked' });\n return;\n }\n popup.focus();\n\n let settled = false;\n const cleanup = () => {\n window.removeEventListener('message', onMessage);\n clearInterval(closedTimer);\n if (options.signal) options.signal.removeEventListener('abort', onAbort);\n };\n\n function settle(value: OcPopupOutcome) {\n if (settled) return;\n settled = true;\n cleanup();\n try {\n if (popup && !popup.closed) popup.close();\n } catch {\n /* popup may have closed itself first */\n }\n resolve(value);\n }\n\n function onMessage(event: MessageEvent) {\n // Origin pinning is necessary but not sufficient — same-origin\n // pages other than our popup can post the same shape. Pin both\n // origin AND source window so only the popup we opened can\n // settle this promise.\n if (event.origin !== popupOrigin) return;\n if (event.source !== popup) return;\n const data = event.data as\n | { type?: string; account?: OcPopupAccount; token?: string }\n | null;\n if (!data || data.type !== 'oc-signin-success') return;\n if (!data.account || !data.token) {\n settle({ status: 'failed' });\n return;\n }\n settle({ status: 'signed_in', account: data.account, token: data.token });\n }\n\n function onAbort() {\n settle({ status: 'aborted' });\n }\n\n window.addEventListener('message', onMessage);\n if (options.signal) {\n if (options.signal.aborted) {\n settle({ status: 'aborted' });\n return;\n }\n options.signal.addEventListener('abort', onAbort);\n }\n\n // Detect the person closing the popup without completing signin.\n const closedTimer = setInterval(() => {\n if (popup && popup.closed) settle({ status: 'closed' });\n }, 500);\n });\n}\n\n/**\n * Open the OC signin popup and resolve with the session result. Returns\n * `null` if the user closed the popup without completing signin, if the\n * browser blocked it, or if the AbortSignal fired. To tell those apart,\n * use signInWithOcOutcome.\n *\n * MUST be called inside a user-gesture handler (e.g. a button onClick).\n * Browsers block `window.open` outside of user gestures.\n */\nexport async function signInWithOc(options: SignInWithOcOptions = {}): Promise<OcPopupResult | null> {\n const outcome = await signInWithOcOutcome(options);\n return outcome.status === 'signed_in' ? { account: outcome.account, token: outcome.token } : null;\n}\n"]}
package/dist/popup.mjs CHANGED
@@ -2,20 +2,20 @@
2
2
  var DEFAULT_POPUP_ORIGIN = "https://me.ochk.io";
3
3
  var DEFAULT_POPUP_PATH = "/popup/signin";
4
4
  var DEFAULT_FEATURES = "width=480,height=720,resizable=yes,scrollbars=yes";
5
- function signInWithOc(options = {}) {
5
+ function signInWithOcOutcome(options = {}) {
6
6
  const popupOrigin = options.popupOrigin ?? DEFAULT_POPUP_ORIGIN;
7
7
  const popupPath = options.popupPath ?? DEFAULT_POPUP_PATH;
8
8
  const features = options.features ?? DEFAULT_FEATURES;
9
9
  return new Promise((resolve) => {
10
10
  if (typeof window === "undefined") {
11
- resolve(null);
11
+ resolve({ status: "failed" });
12
12
  return;
13
13
  }
14
14
  const url = new URL(popupPath, popupOrigin);
15
15
  url.searchParams.set("opener_origin", window.location.origin);
16
16
  const popup = window.open(url.toString(), "oc-signin", features);
17
17
  if (!popup) {
18
- resolve(null);
18
+ resolve({ status: "blocked" });
19
19
  return;
20
20
  }
21
21
  popup.focus();
@@ -41,28 +41,32 @@ function signInWithOc(options = {}) {
41
41
  const data = event.data;
42
42
  if (!data || data.type !== "oc-signin-success") return;
43
43
  if (!data.account || !data.token) {
44
- settle(null);
44
+ settle({ status: "failed" });
45
45
  return;
46
46
  }
47
- settle({ account: data.account, token: data.token });
47
+ settle({ status: "signed_in", account: data.account, token: data.token });
48
48
  }
49
49
  function onAbort() {
50
- settle(null);
50
+ settle({ status: "aborted" });
51
51
  }
52
52
  window.addEventListener("message", onMessage);
53
53
  if (options.signal) {
54
54
  if (options.signal.aborted) {
55
- settle(null);
55
+ settle({ status: "aborted" });
56
56
  return;
57
57
  }
58
58
  options.signal.addEventListener("abort", onAbort);
59
59
  }
60
60
  const closedTimer = setInterval(() => {
61
- if (popup && popup.closed) settle(null);
61
+ if (popup && popup.closed) settle({ status: "closed" });
62
62
  }, 500);
63
63
  });
64
64
  }
65
+ async function signInWithOc(options = {}) {
66
+ const outcome = await signInWithOcOutcome(options);
67
+ return outcome.status === "signed_in" ? { account: outcome.account, token: outcome.token } : null;
68
+ }
65
69
 
66
- export { signInWithOc };
70
+ export { signInWithOc, signInWithOcOutcome };
67
71
  //# sourceMappingURL=popup.mjs.map
68
72
  //# sourceMappingURL=popup.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/popup.ts"],"names":[],"mappings":"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","file":"popup.mjs","sourcesContent":["/**\n * @orangecheck/me-client/popup\n *\n * Browser-side helper that opens the OC sign-in popup, listens for the\n * postMessage from me.ochk.io, and resolves with the verified session.\n *\n * import { signInWithOc } from '@orangecheck/me-client/popup';\n *\n * document.querySelector('#oc-signin')!.addEventListener('click', async () => {\n * const result = await signInWithOc();\n * if (!result) return;\n * // Your server verifies it and keeps it in an httpOnly cookie.\n * const r = await fetch('/api/auth/session', {\n * method: 'POST',\n * headers: { 'Content-Type': 'application/json' },\n * body: JSON.stringify({ token: result.token }),\n * });\n * if (r.ok) location.assign('/app');\n * });\n *\n * Why this lives in a separate entry point: it's browser-only (uses\n * window.open / addEventListener) and integrators that only need the\n * server-side verifier shouldn't pull popup orchestration into their\n * server bundle.\n */\n\nconst DEFAULT_POPUP_ORIGIN = 'https://me.ochk.io';\nconst DEFAULT_POPUP_PATH = '/popup/signin';\nconst DEFAULT_FEATURES = 'width=480,height=720,resizable=yes,scrollbars=yes';\n\nexport interface OcPopupAccount {\n id?: string;\n /** Opaque public identity · `did:oc:<32-hex>`. Stable across linking\n * events. The canonical identifier post auth-refactor (Dec 2025\n * +). */\n did_oc?: string;\n /** Primary linked Bitcoin address, if the user has one and is\n * surfacing it (same-origin family integrators get it inline;\n * cross-domain integrators need the bitcoin_address scope). */\n primary_btc?: string | null;\n /** Whether the user has a primary email linked (plaintext stays\n * on auth host; only the boolean reaches integrators). */\n has_email?: boolean;\n /** Legacy fields kept for back-compat with pre-refactor consumers.\n * Prefer did_oc + primary_btc. Removed in a future major. */\n address?: string;\n btc_address?: string;\n display_name?: string | null;\n nostr_npub?: string | null;\n wallet_provisioned?: boolean;\n email?: string;\n}\n\nexport interface OcPopupResult {\n /** Account payload from the auth host, unsigned: trust only the\n * verified token. `did_oc` is the account's opaque identifier, the\n * same at every site; the email and Bitcoin address stay private\n * unless the user grants a scope. */\n account: OcPopupAccount;\n /** The session JWT, bound to your origin. Hand it once to your own\n * server, which verifies it and keeps it in an httpOnly cookie; never\n * in localStorage, where any script on the page can read it. Family\n * sites (*.ochk.io) can ignore it: the Domain=.ochk.io cookie rides\n * home on its own. */\n token: string;\n}\n\nexport interface SignInWithOcOptions {\n /** Origin of the OC popup page. Defaults to https://me.ochk.io.\n * Override only for staging / preview environments. */\n popupOrigin?: string;\n /** Path on the popup origin. Defaults to /popup/signin. */\n popupPath?: string;\n /** window.open features string. Defaults to a 480x720 resizable\n * popup with scrollbars. */\n features?: string;\n /** AbortSignal that cancels the wait and closes the popup. */\n signal?: AbortSignal;\n}\n\n/**\n * Open the OC signin popup and resolve with the session result. Returns\n * `null` if the user closed the popup without completing signin, or if\n * the AbortSignal fired.\n *\n * MUST be called inside a user-gesture handler (e.g. a button onClick).\n * Browsers block `window.open` outside of user gestures.\n */\nexport function signInWithOc(options: SignInWithOcOptions = {}): Promise<OcPopupResult | null> {\n const popupOrigin = options.popupOrigin ?? DEFAULT_POPUP_ORIGIN;\n const popupPath = options.popupPath ?? DEFAULT_POPUP_PATH;\n const features = options.features ?? DEFAULT_FEATURES;\n\n return new Promise((resolve) => {\n if (typeof window === 'undefined') {\n resolve(null);\n return;\n }\n\n const url = new URL(popupPath, popupOrigin);\n url.searchParams.set('opener_origin', window.location.origin);\n const popup = window.open(url.toString(), 'oc-signin', features);\n if (!popup) {\n // Popup blocked. Caller can detect a null result and fall\n // back to a full-page redirect if desired.\n resolve(null);\n return;\n }\n popup.focus();\n\n let settled = false;\n const cleanup = () => {\n window.removeEventListener('message', onMessage);\n clearInterval(closedTimer);\n if (options.signal) options.signal.removeEventListener('abort', onAbort);\n };\n\n function settle(value: OcPopupResult | null) {\n if (settled) return;\n settled = true;\n cleanup();\n try {\n if (popup && !popup.closed) popup.close();\n } catch {\n /* popup may have closed itself first */\n }\n resolve(value);\n }\n\n function onMessage(event: MessageEvent) {\n // Origin pinning is necessary but not sufficient — same-origin\n // pages other than our popup can post the same shape. Pin both\n // origin AND source window so only the popup we opened can\n // settle this promise.\n if (event.origin !== popupOrigin) return;\n if (event.source !== popup) return;\n const data = event.data as\n | { type?: string; account?: OcPopupAccount; token?: string }\n | null;\n if (!data || data.type !== 'oc-signin-success') return;\n if (!data.account || !data.token) {\n // Malformed payload — treat as not-signed-in.\n settle(null);\n return;\n }\n settle({ account: data.account, token: data.token });\n }\n\n function onAbort() {\n settle(null);\n }\n\n window.addEventListener('message', onMessage);\n if (options.signal) {\n if (options.signal.aborted) {\n settle(null);\n return;\n }\n options.signal.addEventListener('abort', onAbort);\n }\n\n // Detect the user closing the popup without completing signin.\n const closedTimer = setInterval(() => {\n if (popup && popup.closed) settle(null);\n }, 500);\n });\n}\n"]}
1
+ {"version":3,"sources":["../src/popup.ts"],"names":[],"mappings":"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","file":"popup.mjs","sourcesContent":["/**\n * @orangecheck/me-client/popup\n *\n * Browser-side helper that opens the OC sign-in popup, listens for the\n * postMessage from me.ochk.io, and resolves with the verified session.\n *\n * import { signInWithOc } from '@orangecheck/me-client/popup';\n *\n * document.querySelector('#oc-signin')!.addEventListener('click', async () => {\n * const result = await signInWithOc();\n * if (!result) return;\n * // Your server verifies it and keeps it in an httpOnly cookie.\n * const r = await fetch('/api/auth/session', {\n * method: 'POST',\n * headers: { 'Content-Type': 'application/json' },\n * body: JSON.stringify({ token: result.token }),\n * });\n * if (r.ok) location.assign('/app');\n * });\n *\n * Why this lives in a separate entry point: it's browser-only (uses\n * window.open / addEventListener) and integrators that only need the\n * server-side verifier shouldn't pull popup orchestration into their\n * server bundle.\n */\n\nconst DEFAULT_POPUP_ORIGIN = 'https://me.ochk.io';\nconst DEFAULT_POPUP_PATH = '/popup/signin';\nconst DEFAULT_FEATURES = 'width=480,height=720,resizable=yes,scrollbars=yes';\n\nexport interface OcPopupAccount {\n id?: string;\n /** Opaque public identity · `did:oc:<32-hex>`. Stable across linking\n * events. The canonical identifier post auth-refactor (Dec 2025\n * +). */\n did_oc?: string;\n /** Primary linked Bitcoin address, if the user has one and is\n * surfacing it (same-origin family integrators get it inline;\n * cross-domain integrators need the bitcoin_address scope). */\n primary_btc?: string | null;\n /** Whether the user has a primary email linked (plaintext stays\n * on auth host; only the boolean reaches integrators). */\n has_email?: boolean;\n /** Legacy fields kept for back-compat with pre-refactor consumers.\n * Prefer did_oc + primary_btc. Removed in a future major. */\n address?: string;\n btc_address?: string;\n display_name?: string | null;\n nostr_npub?: string | null;\n wallet_provisioned?: boolean;\n email?: string;\n}\n\nexport interface OcPopupResult {\n /** Account payload from the auth host, unsigned: trust only the\n * verified token. `did_oc` is the account's opaque identifier, the\n * same at every site; the email and Bitcoin address stay private\n * unless the user grants a scope. */\n account: OcPopupAccount;\n /** The session JWT, bound to your origin. Hand it once to your own\n * server, which verifies it and keeps it in an httpOnly cookie; never\n * in localStorage, where any script on the page can read it. Family\n * sites (*.ochk.io) can ignore it: the Domain=.ochk.io cookie rides\n * home on its own. */\n token: string;\n}\n\nexport interface SignInWithOcOptions {\n /** Origin of the OC popup page. Defaults to https://me.ochk.io.\n * Override only for staging / preview environments. */\n popupOrigin?: string;\n /** Path on the popup origin. Defaults to /popup/signin. */\n popupPath?: string;\n /** window.open features string. Defaults to a 480x720 resizable\n * popup with scrollbars. */\n features?: string;\n /** AbortSignal that cancels the wait and closes the popup. */\n signal?: AbortSignal;\n}\n\n/**\n * How a sign-in popup ended:\n * - `signed_in` · the person signed in; `account` and `token` as from signInWithOc.\n * - `blocked` · the browser refused to open the window. Ask the person to\n * allow pop-ups for your site, then try again from a click.\n * - `closed` · the person closed the window without signing in.\n * - `aborted` · your AbortSignal fired.\n * - `failed` · not in a browser, or the popup's answer was malformed.\n */\nexport type OcPopupOutcome =\n | ({ status: 'signed_in' } & OcPopupResult)\n | { status: 'blocked' }\n | { status: 'closed' }\n | { status: 'aborted' }\n | { status: 'failed' };\n\n/**\n * Open the OC signin popup and resolve with how it ended (OcPopupOutcome),\n * so a blocked window and a closed one can be told apart.\n *\n * MUST be called inside a user-gesture handler (e.g. a button onClick).\n * Browsers block `window.open` outside of user gestures.\n */\nexport function signInWithOcOutcome(options: SignInWithOcOptions = {}): Promise<OcPopupOutcome> {\n const popupOrigin = options.popupOrigin ?? DEFAULT_POPUP_ORIGIN;\n const popupPath = options.popupPath ?? DEFAULT_POPUP_PATH;\n const features = options.features ?? DEFAULT_FEATURES;\n\n return new Promise((resolve) => {\n if (typeof window === 'undefined') {\n resolve({ status: 'failed' });\n return;\n }\n\n const url = new URL(popupPath, popupOrigin);\n url.searchParams.set('opener_origin', window.location.origin);\n const popup = window.open(url.toString(), 'oc-signin', features);\n if (!popup) {\n resolve({ status: 'blocked' });\n return;\n }\n popup.focus();\n\n let settled = false;\n const cleanup = () => {\n window.removeEventListener('message', onMessage);\n clearInterval(closedTimer);\n if (options.signal) options.signal.removeEventListener('abort', onAbort);\n };\n\n function settle(value: OcPopupOutcome) {\n if (settled) return;\n settled = true;\n cleanup();\n try {\n if (popup && !popup.closed) popup.close();\n } catch {\n /* popup may have closed itself first */\n }\n resolve(value);\n }\n\n function onMessage(event: MessageEvent) {\n // Origin pinning is necessary but not sufficient — same-origin\n // pages other than our popup can post the same shape. Pin both\n // origin AND source window so only the popup we opened can\n // settle this promise.\n if (event.origin !== popupOrigin) return;\n if (event.source !== popup) return;\n const data = event.data as\n | { type?: string; account?: OcPopupAccount; token?: string }\n | null;\n if (!data || data.type !== 'oc-signin-success') return;\n if (!data.account || !data.token) {\n settle({ status: 'failed' });\n return;\n }\n settle({ status: 'signed_in', account: data.account, token: data.token });\n }\n\n function onAbort() {\n settle({ status: 'aborted' });\n }\n\n window.addEventListener('message', onMessage);\n if (options.signal) {\n if (options.signal.aborted) {\n settle({ status: 'aborted' });\n return;\n }\n options.signal.addEventListener('abort', onAbort);\n }\n\n // Detect the person closing the popup without completing signin.\n const closedTimer = setInterval(() => {\n if (popup && popup.closed) settle({ status: 'closed' });\n }, 500);\n });\n}\n\n/**\n * Open the OC signin popup and resolve with the session result. Returns\n * `null` if the user closed the popup without completing signin, if the\n * browser blocked it, or if the AbortSignal fired. To tell those apart,\n * use signInWithOcOutcome.\n *\n * MUST be called inside a user-gesture handler (e.g. a button onClick).\n * Browsers block `window.open` outside of user gestures.\n */\nexport async function signInWithOc(options: SignInWithOcOptions = {}): Promise<OcPopupResult | null> {\n const outcome = await signInWithOcOutcome(options);\n return outcome.status === 'signed_in' ? { account: outcome.account, token: outcome.token } : null;\n}\n"]}
package/dist/server.d.mts CHANGED
@@ -5,6 +5,7 @@ export { A as ActivityAttestation, a as ActivityAttestationBundle, b as Activity
5
5
  interface OcSessionOptions extends VerifyOcOptions {
6
6
  method?: string;
7
7
  projectKey?: string;
8
+ meOrigin?: string;
8
9
  revocation?: RevocationCheck | false;
9
10
  }
10
11
  declare function _resetSessionChecksForTests(): void;
@@ -34,7 +35,6 @@ interface WithOcAuthOptions extends OcSessionOptions {
34
35
  maxAgeSecs?: number;
35
36
  };
36
37
  projectKey?: string;
37
- meOrigin?: string;
38
38
  }
39
39
  declare const BACK_ACCOUNT_URL = "https://ochk.app/me/account#backing";
40
40
  declare function withOcAuth<Req extends NextApiRequestLike = NextApiRequestLike, Res extends NextApiResponseLike = NextApiResponseLike>(handler: (req: WithOcSession<Req>, res: Res) => unknown, options?: WithOcAuthOptions): (req: Req, res: Res) => Promise<unknown>;
package/dist/server.d.ts CHANGED
@@ -5,6 +5,7 @@ export { A as ActivityAttestation, a as ActivityAttestationBundle, b as Activity
5
5
  interface OcSessionOptions extends VerifyOcOptions {
6
6
  method?: string;
7
7
  projectKey?: string;
8
+ meOrigin?: string;
8
9
  revocation?: RevocationCheck | false;
9
10
  }
10
11
  declare function _resetSessionChecksForTests(): void;
@@ -34,7 +35,6 @@ interface WithOcAuthOptions extends OcSessionOptions {
34
35
  maxAgeSecs?: number;
35
36
  };
36
37
  projectKey?: string;
37
- meOrigin?: string;
38
38
  }
39
39
  declare const BACK_ACCOUNT_URL = "https://ochk.app/me/account#backing";
40
40
  declare function withOcAuth<Req extends NextApiRequestLike = NextApiRequestLike, Res extends NextApiResponseLike = NextApiResponseLike>(handler: (req: WithOcSession<Req>, res: Res) => unknown, options?: WithOcAuthOptions): (req: Req, res: Res) => Promise<unknown>;
package/dist/server.js CHANGED
@@ -575,11 +575,14 @@ var warnedUnchecked = false;
575
575
  function revocationFor(options) {
576
576
  if (options.revocation === false) return null;
577
577
  if (options.revocation) return options.revocation;
578
- const key = options.projectKey ?? "";
578
+ const key = `${options.meOrigin ?? ""} ${options.projectKey ?? ""}`;
579
579
  if (!sessionChecks.has(key)) {
580
580
  sessionChecks.set(
581
581
  key,
582
- authCore.integratorRevocationCheck(options.projectKey ? { projectKey: options.projectKey } : {})
582
+ authCore.integratorRevocationCheck({
583
+ ...options.projectKey ? { projectKey: options.projectKey } : {},
584
+ ...options.meOrigin ? { meOrigin: options.meOrigin } : {}
585
+ })
583
586
  );
584
587
  }
585
588
  const check = sessionChecks.get(key);
@@ -636,38 +639,49 @@ var unknownKeys = /* @__PURE__ */ new Map();
636
639
  var UNKNOWN_PROJECT = /* @__PURE__ */ Symbol("unknown_project");
637
640
  var POLICY_TTL_MS = 6e4;
638
641
  var POLICY_RETRY_MS = 1e4;
642
+ var POLICY_MAX_STALE_MS = 6e5;
639
643
  var policies = /* @__PURE__ */ new Map();
644
+ var isFloor = (n) => Number.isSafeInteger(n) && n >= 0;
645
+ function readRequirement(body) {
646
+ const b = body;
647
+ if (!b || typeof b !== "object" || b.ok !== true || !("require_bond" in b)) return void 0;
648
+ if (b.require_bond === null) return null;
649
+ const r = b.require_bond;
650
+ if (!r || typeof r !== "object" || !isFloor(r.min_sats) || !isFloor(r.min_days)) return void 0;
651
+ return { minSats: r.min_sats, minDays: r.min_days };
652
+ }
640
653
  async function projectRequirement(options) {
654
+ const base = (options.meOrigin ?? "https://me.ochk.io").replace(/\/$/, "");
641
655
  const key = options.projectKey;
642
- const hit = policies.get(key);
656
+ const slot = `${base} ${key}`;
657
+ const hit = policies.get(slot);
643
658
  const now = Date.now();
644
- if (unknownKeys.has(key) && now - unknownKeys.get(key) < POLICY_TTL_MS) return UNKNOWN_PROJECT;
659
+ const standing = () => hit?.at && now - hit.at < POLICY_MAX_STALE_MS ? hit.need : void 0;
660
+ if (unknownKeys.has(slot) && now - unknownKeys.get(slot) < POLICY_TTL_MS) return UNKNOWN_PROJECT;
645
661
  if (hit && now - hit.at < POLICY_TTL_MS) return hit.need;
646
- if (hit?.failedAt && now - hit.failedAt < POLICY_RETRY_MS) return hit.at ? hit.need : void 0;
662
+ if (hit?.failedAt && now - hit.failedAt < POLICY_RETRY_MS) return standing();
647
663
  try {
648
- const base = options.meOrigin ?? "https://me.ochk.io";
649
664
  const r = await fetch(`${base}/api/integrator/${encodeURIComponent(key)}/sign-in`, {
650
665
  signal: AbortSignal.timeout(4e3)
651
666
  });
652
667
  if (r.status === 404) {
653
- if (!unknownKeys.has(key)) {
668
+ if (!unknownKeys.has(slot)) {
654
669
  console.error(
655
670
  `[@orangecheck/me-client] projectKey "${key}" is not a project. Copy yours from https://ochk.app/me/projects.`
656
671
  );
657
672
  }
658
- unknownKeys.set(key, Date.now());
673
+ unknownKeys.set(slot, Date.now());
659
674
  return UNKNOWN_PROJECT;
660
675
  }
661
- unknownKeys.delete(key);
676
+ unknownKeys.delete(slot);
662
677
  if (!r.ok) throw new Error(`HTTP ${r.status}`);
663
- const j = await r.json();
664
- const req = j.require_bond ?? null;
665
- const need = req ? { minSats: req.min_sats, minDays: req.min_days } : null;
666
- policies.set(key, { at: Date.now(), need });
678
+ const need = readRequirement(await r.json());
679
+ if (need === void 0) throw new Error("unexpected body");
680
+ policies.set(slot, { at: Date.now(), need });
667
681
  return need;
668
682
  } catch {
669
- policies.set(key, { at: hit?.at ?? 0, need: hit?.need ?? null, failedAt: Date.now() });
670
- return hit?.at ? hit.need : void 0;
683
+ policies.set(slot, { at: hit?.at ?? 0, need: hit?.need ?? null, failedAt: Date.now() });
684
+ return standing();
671
685
  }
672
686
  }
673
687
  async function bondNeed(options) {