@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 +12 -1
- package/dist/popup.d.mts +13 -1
- package/dist/popup.d.ts +13 -1
- package/dist/popup.js +13 -8
- package/dist/popup.js.map +1 -1
- package/dist/popup.mjs +13 -9
- package/dist/popup.mjs.map +1 -1
- package/dist/server.d.mts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +29 -15
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +29 -15
- package/dist/server.mjs.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
package/dist/popup.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/popup.ts"],"names":[],"mappings":";AA0BA,IAAM,oBAAA,GAAuB,oBAAA;AAC7B,IAAM,kBAAA,GAAqB,eAAA;AAC3B,IAAM,gBAAA,GAAmB,mDAAA;AA4DlB,SAAS,YAAA,CAAa,OAAA,GAA+B,EAAC,EAAkC;AAC3F,EAAA,MAAM,WAAA,GAAc,QAAQ,WAAA,IAAe,oBAAA;AAC3C,EAAA,MAAM,SAAA,GAAY,QAAQ,SAAA,IAAa,kBAAA;AACvC,EAAA,MAAM,QAAA,GAAW,QAAQ,QAAA,IAAY,gBAAA;AAErC,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,KAAY;AAC5B,IAAA,IAAI,OAAO,WAAW,WAAA,EAAa;AAC/B,MAAA,OAAA,CAAQ,IAAI,CAAA;AACZ,MAAA;AAAA,IACJ;AAEA,IAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,SAAA,EAAW,WAAW,CAAA;AAC1C,IAAA,GAAA,CAAI,YAAA,CAAa,GAAA,CAAI,eAAA,EAAiB,MAAA,CAAO,SAAS,MAAM,CAAA;AAC5D,IAAA,MAAM,QAAQ,MAAA,CAAO,IAAA,CAAK,IAAI,QAAA,EAAS,EAAG,aAAa,QAAQ,CAAA;AAC/D,IAAA,IAAI,CAAC,KAAA,EAAO;AAGR,MAAA,OAAA,CAAQ,IAAI,CAAA;AACZ,MAAA;AAAA,IACJ;AACA,IAAA,KAAA,CAAM,KAAA,EAAM;AAEZ,IAAA,IAAI,OAAA,GAAU,KAAA;AACd,IAAA,MAAM,UAAU,MAAM;AAClB,MAAA,MAAA,CAAO,mBAAA,CAAoB,WAAW,SAAS,CAAA;AAC/C,MAAA,aAAA,CAAc,WAAW,CAAA;AACzB,MAAA,IAAI,QAAQ,MAAA,EAAQ,OAAA,CAAQ,MAAA,CAAO,mBAAA,CAAoB,SAAS,OAAO,CAAA;AAAA,IAC3E,CAAA;AAEA,IAAA,SAAS,OAAO,KAAA,EAA6B;AACzC,MAAA,IAAI,OAAA,EAAS;AACb,MAAA,OAAA,GAAU,IAAA;AACV,MAAA,OAAA,EAAQ;AACR,MAAA,IAAI;AACA,QAAA,IAAI,KAAA,IAAS,CAAC,KAAA,CAAM,MAAA,QAAc,KAAA,EAAM;AAAA,MAC5C,CAAA,CAAA,MAAQ;AAAA,MAER;AACA,MAAA,OAAA,CAAQ,KAAK,CAAA;AAAA,IACjB;AAEA,IAAA,SAAS,UAAU,KAAA,EAAqB;AAKpC,MAAA,IAAI,KAAA,CAAM,WAAW,WAAA,EAAa;AAClC,MAAA,IAAI,KAAA,CAAM,WAAW,KAAA,EAAO;AAC5B,MAAA,MAAM,OAAO,KAAA,CAAM,IAAA;AAGnB,MAAA,IAAI,CAAC,IAAA,IAAQ,IAAA,CAAK,IAAA,KAAS,mBAAA,EAAqB;AAChD,MAAA,IAAI,CAAC,IAAA,CAAK,OAAA,IAAW,CAAC,KAAK,KAAA,EAAO;AAE9B,QAAA,MAAA,CAAO,IAAI,CAAA;AACX,QAAA;AAAA,MACJ;AACA,MAAA,MAAA,CAAO,EAAE,OAAA,EAAS,IAAA,CAAK,SAAS,KAAA,EAAO,IAAA,CAAK,OAAO,CAAA;AAAA,IACvD;AAEA,IAAA,SAAS,OAAA,GAAU;AACf,MAAA,MAAA,CAAO,IAAI,CAAA;AAAA,IACf;AAEA,IAAA,MAAA,CAAO,gBAAA,CAAiB,WAAW,SAAS,CAAA;AAC5C,IAAA,IAAI,QAAQ,MAAA,EAAQ;AAChB,MAAA,IAAI,OAAA,CAAQ,OAAO,OAAA,EAAS;AACxB,QAAA,MAAA,CAAO,IAAI,CAAA;AACX,QAAA;AAAA,MACJ;AACA,MAAA,OAAA,CAAQ,MAAA,CAAO,gBAAA,CAAiB,OAAA,EAAS,OAAO,CAAA;AAAA,IACpD;AAGA,IAAA,MAAM,WAAA,GAAc,YAAY,MAAM;AAClC,MAAA,IAAI,KAAA,IAAS,KAAA,CAAM,MAAA,EAAQ,MAAA,CAAO,IAAI,CAAA;AAAA,IAC1C,GAAG,GAAG,CAAA;AAAA,EACV,CAAC,CAAA;AACL","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(
|
|
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
|
|
656
|
+
const slot = `${base} ${key}`;
|
|
657
|
+
const hit = policies.get(slot);
|
|
643
658
|
const now = Date.now();
|
|
644
|
-
|
|
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
|
|
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(
|
|
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(
|
|
673
|
+
unknownKeys.set(slot, Date.now());
|
|
659
674
|
return UNKNOWN_PROJECT;
|
|
660
675
|
}
|
|
661
|
-
unknownKeys.delete(
|
|
676
|
+
unknownKeys.delete(slot);
|
|
662
677
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
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(
|
|
670
|
-
return
|
|
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) {
|