@orangecheck/auth-client 2.33.4 → 2.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/appearance.mjs +23 -0
- package/dist/appearance.mjs.map +1 -0
- package/dist/bond.mjs +20 -0
- package/dist/bond.mjs.map +1 -0
- package/dist/chunk-4E7GJ5IB.mjs +1184 -0
- package/dist/chunk-4E7GJ5IB.mjs.map +1 -0
- package/dist/chunk-7ST4Q5KR.mjs +346 -0
- package/dist/chunk-7ST4Q5KR.mjs.map +1 -0
- package/dist/chunk-7UYPMTAV.mjs +652 -0
- package/dist/chunk-7UYPMTAV.mjs.map +1 -0
- package/dist/chunk-BYAF2P2L.mjs +253 -0
- package/dist/chunk-BYAF2P2L.mjs.map +1 -0
- package/dist/chunk-CYR7J4UY.mjs +118 -0
- package/dist/chunk-CYR7J4UY.mjs.map +1 -0
- package/dist/chunk-H2TXOCXH.mjs +23 -0
- package/dist/chunk-H2TXOCXH.mjs.map +1 -0
- package/dist/chunk-HZ4FJMF3.mjs +490 -0
- package/dist/chunk-HZ4FJMF3.mjs.map +1 -0
- package/dist/chunk-TJJNTITX.mjs +187 -0
- package/dist/chunk-TJJNTITX.mjs.map +1 -0
- package/dist/chunk-UFRU3I3D.mjs +913 -0
- package/dist/chunk-UFRU3I3D.mjs.map +1 -0
- package/dist/chunk-VRLFZAGK.mjs +37 -0
- package/dist/chunk-VRLFZAGK.mjs.map +1 -0
- package/dist/chunk-XCZVSE7M.mjs +40 -0
- package/dist/chunk-XCZVSE7M.mjs.map +1 -0
- package/dist/components.mjs +19 -0
- package/dist/components.mjs.map +1 -0
- package/dist/index.js +77 -67
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +112 -4001
- package/dist/index.mjs.map +1 -1
- package/dist/linked-identities.mjs +41 -0
- package/dist/linked-identities.mjs.map +1 -0
- package/dist/provider.mjs +17 -0
- package/dist/provider.mjs.map +1 -0
- package/dist/return-to.mjs +18 -0
- package/dist/return-to.mjs.map +1 -0
- package/dist/signin.mjs +23 -0
- package/dist/signin.mjs.map +1 -0
- package/dist/sudo.mjs +16 -0
- package/dist/sudo.mjs.map +1 -0
- package/dist/tab-session.mjs +29 -0
- package/dist/tab-session.mjs.map +1 -0
- package/dist/types.mjs +17 -0
- package/dist/types.mjs.map +1 -0
- package/dist/webauthn.mjs +15 -0
- package/dist/webauthn.mjs.map +1 -0
- package/package.json +2 -1
- package/dist/index.d.mts +0 -303
package/dist/index.mjs
CHANGED
|
@@ -1,4003 +1,114 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
function installTabLinkDecorator(authOrigin) {
|
|
115
|
-
if (typeof window === "undefined" || typeof document === "undefined") return () => {
|
|
116
|
-
};
|
|
117
|
-
const onActivate = (e) => {
|
|
118
|
-
try {
|
|
119
|
-
const pin = readTabSession();
|
|
120
|
-
if (!pin) return;
|
|
121
|
-
const target = e.target;
|
|
122
|
-
if (!(target instanceof Element)) return;
|
|
123
|
-
const anchor = target.closest("a[href]");
|
|
124
|
-
if (!anchor || anchor.hasAttribute("download")) return;
|
|
125
|
-
const raw = anchor.getAttribute("href");
|
|
126
|
-
if (!raw || raw.includes(`${TAB_ACCOUNT_HINT_KEY}=`)) return;
|
|
127
|
-
let dest;
|
|
128
|
-
try {
|
|
129
|
-
dest = new URL(anchor.href);
|
|
130
|
-
} catch {
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
if (dest.hash) return;
|
|
134
|
-
if (!isFamilyUrl(dest, authOrigin)) return;
|
|
135
|
-
if (!opensWithoutTabPin(e, anchor, dest)) return;
|
|
136
|
-
anchor.setAttribute("href", `${raw}#${TAB_ACCOUNT_HINT_KEY}=${pin.didOc}`);
|
|
137
|
-
} catch {
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
window.addEventListener("click", onActivate, true);
|
|
141
|
-
window.addEventListener("auxclick", onActivate, true);
|
|
142
|
-
return () => {
|
|
143
|
-
window.removeEventListener("click", onActivate, true);
|
|
144
|
-
window.removeEventListener("auxclick", onActivate, true);
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
async function consumeTabAccountHint(authOrigin) {
|
|
148
|
-
if (typeof window === "undefined") return null;
|
|
149
|
-
const frag = window.location.hash.replace(/^#/, "");
|
|
150
|
-
const m = OC_AS_HINT_RE.exec(frag);
|
|
151
|
-
if (!m) return null;
|
|
152
|
-
const did = m[1];
|
|
153
|
-
try {
|
|
154
|
-
const url = new URL(window.location.href);
|
|
155
|
-
url.hash = "";
|
|
156
|
-
window.history.replaceState(window.history.state, "", url.toString());
|
|
157
|
-
} catch {
|
|
158
|
-
}
|
|
159
|
-
const existing = readTabSession();
|
|
160
|
-
if (existing && existing.didOc === did) return did;
|
|
161
|
-
try {
|
|
162
|
-
const res = await fetch(`${authOrigin}/api/auth/tab`, {
|
|
163
|
-
method: "POST",
|
|
164
|
-
credentials: "include",
|
|
165
|
-
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
|
166
|
-
body: JSON.stringify({ did_oc: did })
|
|
167
|
-
});
|
|
168
|
-
if (!res.ok) return null;
|
|
169
|
-
const body = await res.json();
|
|
170
|
-
if (body.ok && typeof body.token === "string" && body.account?.did_oc === did) {
|
|
171
|
-
writeTabSession({ token: body.token, didOc: did });
|
|
172
|
-
return did;
|
|
173
|
-
}
|
|
174
|
-
} catch {
|
|
175
|
-
}
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// src/return-to.ts
|
|
180
|
-
var PREFIX = "oc:return-fragment:";
|
|
181
|
-
var HOLD_MS = 1e4;
|
|
182
|
-
var FRAGMENT_TTL_MS = 15 * 60 * 1e3;
|
|
183
|
-
function store() {
|
|
184
|
-
try {
|
|
185
|
-
return window.sessionStorage;
|
|
186
|
-
} catch {
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
function stripFragment(target) {
|
|
191
|
-
const cut = target.indexOf("#");
|
|
192
|
-
return cut < 0 ? target : target.slice(0, cut);
|
|
193
|
-
}
|
|
194
|
-
function keep(pathAndQuery, hash) {
|
|
195
|
-
if (hash.length < 2) return;
|
|
196
|
-
try {
|
|
197
|
-
store()?.setItem(PREFIX + pathAndQuery, JSON.stringify({ hash, at: Date.now() }));
|
|
198
|
-
} catch {
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
function withoutFragment(target) {
|
|
202
|
-
if (typeof window !== "undefined" && target.includes("#")) {
|
|
203
|
-
try {
|
|
204
|
-
const u = new URL(target, window.location.href);
|
|
205
|
-
if (u.origin === window.location.origin) keep(u.pathname + u.search, u.hash);
|
|
206
|
-
} catch {
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
return stripFragment(target);
|
|
210
|
-
}
|
|
211
|
-
function returnToHere() {
|
|
212
|
-
if (typeof window === "undefined") return "/";
|
|
213
|
-
const { pathname, search, hash } = window.location;
|
|
214
|
-
return withoutFragment(pathname + search + hash);
|
|
215
|
-
}
|
|
216
|
-
function keepFragmentOnReturnLinks() {
|
|
217
|
-
if (typeof document === "undefined") return () => {
|
|
218
|
-
};
|
|
219
|
-
const onClick = (e) => {
|
|
220
|
-
const { pathname, search, hash, href } = window.location;
|
|
221
|
-
if (hash.length < 2 || e.defaultPrevented) return;
|
|
222
|
-
const link = e.target?.closest?.("a[href]");
|
|
223
|
-
if (!link) return;
|
|
224
|
-
try {
|
|
225
|
-
const to = new URL(link.getAttribute("href") ?? "", href);
|
|
226
|
-
const back = to.searchParams.get("return_to");
|
|
227
|
-
if (!back) return;
|
|
228
|
-
const here = new URL(back, href);
|
|
229
|
-
if (here.origin === window.location.origin && here.pathname === pathname && here.search === search) {
|
|
230
|
-
keep(pathname + search, hash);
|
|
231
|
-
}
|
|
232
|
-
} catch {
|
|
233
|
-
}
|
|
234
|
-
};
|
|
235
|
-
document.addEventListener("click", onClick, true);
|
|
236
|
-
return () => document.removeEventListener("click", onClick, true);
|
|
237
|
-
}
|
|
238
|
-
function restoreReturnFragment() {
|
|
239
|
-
if (typeof window === "undefined") return false;
|
|
240
|
-
const s = store();
|
|
241
|
-
if (!s) return false;
|
|
242
|
-
const { pathname, search, hash } = window.location;
|
|
243
|
-
const key = PREFIX + pathname + search;
|
|
244
|
-
let raw;
|
|
245
|
-
try {
|
|
246
|
-
raw = s.getItem(key);
|
|
247
|
-
if (raw === null) return false;
|
|
248
|
-
s.removeItem(key);
|
|
249
|
-
} catch {
|
|
250
|
-
return false;
|
|
251
|
-
}
|
|
252
|
-
if (hash) return false;
|
|
253
|
-
try {
|
|
254
|
-
const kept = JSON.parse(raw);
|
|
255
|
-
if (typeof kept.hash !== "string" || !kept.hash.startsWith("#")) return false;
|
|
256
|
-
if (typeof kept.at !== "number" || Date.now() - kept.at > FRAGMENT_TTL_MS) return false;
|
|
257
|
-
const restored = pathname + search + kept.hash;
|
|
258
|
-
window.history.replaceState(window.history.state, "", restored);
|
|
259
|
-
holdFragment(pathname + search, kept.hash, restored);
|
|
260
|
-
return true;
|
|
261
|
-
} catch {
|
|
262
|
-
return false;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
function holdFragment(pathAndQuery, hash, restored) {
|
|
266
|
-
const until = Date.now() + HOLD_MS;
|
|
267
|
-
const check = () => {
|
|
268
|
-
const l = window.location;
|
|
269
|
-
if (l.pathname + l.search !== pathAndQuery || l.hash === hash) {
|
|
270
|
-
if (l.pathname + l.search === pathAndQuery && Date.now() < until) {
|
|
271
|
-
window.setTimeout(check, 50);
|
|
272
|
-
}
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
if (l.hash && l.hash !== TAB_ADOPT_HASH) return;
|
|
276
|
-
try {
|
|
277
|
-
window.history.replaceState(window.history.state, "", restored);
|
|
278
|
-
} catch {
|
|
279
|
-
}
|
|
280
|
-
};
|
|
281
|
-
window.setTimeout(check, 0);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// src/types.ts
|
|
285
|
-
var DISPLAY_IDENTITY_KINDS = ["did", "btc", "email", "nostr"];
|
|
286
|
-
var DEFAULT_CONFIG = {
|
|
287
|
-
authOrigin: "https://ochk.io",
|
|
288
|
-
signInPath: "/signin",
|
|
289
|
-
mePath: "/api/auth/me",
|
|
290
|
-
logoutPath: "/api/auth/logout",
|
|
291
|
-
multiAccount: true
|
|
1
|
+
import {
|
|
2
|
+
useStepUpAuth,
|
|
3
|
+
useWebAuthnList,
|
|
4
|
+
useWebAuthnRegister
|
|
5
|
+
} from "./chunk-BYAF2P2L.mjs";
|
|
6
|
+
import {
|
|
7
|
+
OcBond,
|
|
8
|
+
bondFloorsText,
|
|
9
|
+
fetchOcBond
|
|
10
|
+
} from "./chunk-HZ4FJMF3.mjs";
|
|
11
|
+
import {
|
|
12
|
+
OcAccountChip,
|
|
13
|
+
OcAccountPill,
|
|
14
|
+
OcAddressInput,
|
|
15
|
+
OcSignInButton,
|
|
16
|
+
useOcAddressSuggestion
|
|
17
|
+
} from "./chunk-7UYPMTAV.mjs";
|
|
18
|
+
import {
|
|
19
|
+
OcSessionProvider,
|
|
20
|
+
useOcSession,
|
|
21
|
+
useOptionalOcSession
|
|
22
|
+
} from "./chunk-7ST4Q5KR.mjs";
|
|
23
|
+
import {
|
|
24
|
+
OcSignIn,
|
|
25
|
+
familyReturnTarget,
|
|
26
|
+
safeReturnTo
|
|
27
|
+
} from "./chunk-4E7GJ5IB.mjs";
|
|
28
|
+
import {
|
|
29
|
+
OcLinkedIdentities,
|
|
30
|
+
fetchOcLinkedIdentities
|
|
31
|
+
} from "./chunk-UFRU3I3D.mjs";
|
|
32
|
+
import {
|
|
33
|
+
SUDO_ACCOUNT_MISMATCH_MESSAGE,
|
|
34
|
+
handleSudoRequired,
|
|
35
|
+
isSudoAccountMismatch,
|
|
36
|
+
redirectToSudo
|
|
37
|
+
} from "./chunk-XCZVSE7M.mjs";
|
|
38
|
+
import "./chunk-H2TXOCXH.mjs";
|
|
39
|
+
import {
|
|
40
|
+
DEFAULT_CONFIG,
|
|
41
|
+
DISPLAY_IDENTITY_KINDS,
|
|
42
|
+
buildAddAccountUrl,
|
|
43
|
+
buildSignInUrl
|
|
44
|
+
} from "./chunk-VRLFZAGK.mjs";
|
|
45
|
+
import {
|
|
46
|
+
FRAGMENT_TTL_MS,
|
|
47
|
+
keepFragmentOnReturnLinks,
|
|
48
|
+
restoreReturnFragment,
|
|
49
|
+
returnToHere,
|
|
50
|
+
stripFragment,
|
|
51
|
+
withoutFragment
|
|
52
|
+
} from "./chunk-CYR7J4UY.mjs";
|
|
53
|
+
import {
|
|
54
|
+
TAB_ACCOUNT_HINT_KEY,
|
|
55
|
+
TAB_ADOPT_HASH,
|
|
56
|
+
TAB_SESSION_HEADER,
|
|
57
|
+
TAB_SESSION_STORAGE_KEY,
|
|
58
|
+
clearTabSession,
|
|
59
|
+
consumeTabAccountHint,
|
|
60
|
+
consumeTabAdoptMarker,
|
|
61
|
+
installTabFetchInterceptor,
|
|
62
|
+
installTabLinkDecorator,
|
|
63
|
+
readTabSession,
|
|
64
|
+
tabSessionHeader,
|
|
65
|
+
writeTabSession
|
|
66
|
+
} from "./chunk-TJJNTITX.mjs";
|
|
67
|
+
export {
|
|
68
|
+
DEFAULT_CONFIG,
|
|
69
|
+
DISPLAY_IDENTITY_KINDS,
|
|
70
|
+
FRAGMENT_TTL_MS,
|
|
71
|
+
OcAccountChip,
|
|
72
|
+
OcAccountPill,
|
|
73
|
+
OcAddressInput,
|
|
74
|
+
OcBond,
|
|
75
|
+
OcLinkedIdentities,
|
|
76
|
+
OcSessionProvider,
|
|
77
|
+
OcSignIn,
|
|
78
|
+
OcSignInButton,
|
|
79
|
+
SUDO_ACCOUNT_MISMATCH_MESSAGE,
|
|
80
|
+
TAB_ACCOUNT_HINT_KEY,
|
|
81
|
+
TAB_ADOPT_HASH,
|
|
82
|
+
TAB_SESSION_HEADER,
|
|
83
|
+
TAB_SESSION_STORAGE_KEY,
|
|
84
|
+
bondFloorsText,
|
|
85
|
+
buildAddAccountUrl,
|
|
86
|
+
buildSignInUrl,
|
|
87
|
+
clearTabSession,
|
|
88
|
+
consumeTabAccountHint,
|
|
89
|
+
consumeTabAdoptMarker,
|
|
90
|
+
familyReturnTarget,
|
|
91
|
+
fetchOcBond,
|
|
92
|
+
fetchOcLinkedIdentities,
|
|
93
|
+
handleSudoRequired,
|
|
94
|
+
installTabFetchInterceptor,
|
|
95
|
+
installTabLinkDecorator,
|
|
96
|
+
isSudoAccountMismatch,
|
|
97
|
+
keepFragmentOnReturnLinks,
|
|
98
|
+
readTabSession,
|
|
99
|
+
redirectToSudo,
|
|
100
|
+
restoreReturnFragment,
|
|
101
|
+
returnToHere,
|
|
102
|
+
safeReturnTo,
|
|
103
|
+
stripFragment,
|
|
104
|
+
tabSessionHeader,
|
|
105
|
+
useOcAddressSuggestion,
|
|
106
|
+
useOcSession,
|
|
107
|
+
useOptionalOcSession,
|
|
108
|
+
useStepUpAuth,
|
|
109
|
+
useWebAuthnList,
|
|
110
|
+
useWebAuthnRegister,
|
|
111
|
+
withoutFragment,
|
|
112
|
+
writeTabSession
|
|
292
113
|
};
|
|
293
|
-
function resolveConfig(cfg) {
|
|
294
|
-
return { ...DEFAULT_CONFIG, ...cfg ?? {} };
|
|
295
|
-
}
|
|
296
|
-
function buildSignInUrl(cfg, returnTo) {
|
|
297
|
-
const base = `${cfg.authOrigin}${cfg.signInPath}`;
|
|
298
|
-
if (!returnTo) return base;
|
|
299
|
-
const u = new URL(base);
|
|
300
|
-
u.searchParams.set("return_to", stripFragment(returnTo));
|
|
301
|
-
return u.toString();
|
|
302
|
-
}
|
|
303
|
-
function buildAddAccountUrl(cfg, returnTo) {
|
|
304
|
-
const u = new URL(buildSignInUrl(cfg, returnTo));
|
|
305
|
-
u.searchParams.set("add", "1");
|
|
306
|
-
return u.toString();
|
|
307
|
-
}
|
|
308
|
-
var SessionContext = React4.createContext(null);
|
|
309
|
-
function normalizeDisplayIdentity(raw, didOc) {
|
|
310
|
-
const di = raw.display_identity ?? raw.displayIdentity;
|
|
311
|
-
if (di && typeof di === "object" && typeof di.value === "string" && di.value.length > 0 && typeof di.kind === "string" && DISPLAY_IDENTITY_KINDS.includes(di.kind)) {
|
|
312
|
-
return { kind: di.kind, value: di.value };
|
|
313
|
-
}
|
|
314
|
-
return { kind: "did", value: didOc };
|
|
315
|
-
}
|
|
316
|
-
function normalizeRosterEntry(raw) {
|
|
317
|
-
const didOc = raw.did_oc ?? raw.didOc;
|
|
318
|
-
if (!didOc) return null;
|
|
319
|
-
const di = raw.display_identity ?? raw.displayIdentity;
|
|
320
|
-
const displayIdentity = di && typeof di === "object" && typeof di.value === "string" && di.value.length > 0 && typeof di.kind === "string" && DISPLAY_IDENTITY_KINDS.includes(di.kind) ? { kind: di.kind, value: di.value } : { kind: "did", value: didOc };
|
|
321
|
-
return {
|
|
322
|
-
didOc,
|
|
323
|
-
displayName: raw.display_name ?? raw.displayName ?? null,
|
|
324
|
-
primaryBtc: raw.primary_btc ?? raw.primaryBtc ?? null,
|
|
325
|
-
displayIdentity,
|
|
326
|
-
lastSeenAt: raw.last_seen_at ?? raw.lastSeenAt ?? null
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
async function fetchHostRoster(cfg) {
|
|
330
|
-
try {
|
|
331
|
-
const res = await fetch(`${cfg.authOrigin}${cfg.mePath}`, {
|
|
332
|
-
method: "GET",
|
|
333
|
-
credentials: "include",
|
|
334
|
-
headers: { Accept: "application/json", ...tabSessionHeader() }
|
|
335
|
-
});
|
|
336
|
-
if (!res.ok) return [];
|
|
337
|
-
const body = await res.json();
|
|
338
|
-
return Array.isArray(body.roster) ? body.roster.map(normalizeRosterEntry).filter((r) => r !== null) : [];
|
|
339
|
-
} catch {
|
|
340
|
-
return [];
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
function normalizeAccount(raw) {
|
|
344
|
-
if (!raw) return null;
|
|
345
|
-
const didOc = raw.did_oc ?? raw.didOc;
|
|
346
|
-
const accountId = raw.id ?? raw.account_id ?? raw.accountId;
|
|
347
|
-
if (!didOc || !accountId) return null;
|
|
348
|
-
return {
|
|
349
|
-
accountId,
|
|
350
|
-
didOc,
|
|
351
|
-
primaryBtc: raw.primary_btc ?? raw.primaryBtc ?? null,
|
|
352
|
-
hasEmail: raw.has_email ?? raw.hasEmail ?? false,
|
|
353
|
-
displayName: raw.display_name ?? raw.displayName ?? null,
|
|
354
|
-
nostrNpub: raw.nostr_npub ?? raw.nostrNpub ?? null,
|
|
355
|
-
homeFederation: raw.home_federation_slug ?? raw.homeFederation ?? null,
|
|
356
|
-
signingMethod: raw.signing_method ?? raw.signingMethod ?? null,
|
|
357
|
-
isOwner: Boolean(raw.is_owner ?? raw.isOwner ?? false),
|
|
358
|
-
displayIdentity: normalizeDisplayIdentity(raw, didOc)
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
function OcSessionProvider({
|
|
362
|
-
children,
|
|
363
|
-
config,
|
|
364
|
-
defaultReturnTo
|
|
365
|
-
}) {
|
|
366
|
-
const cfg = React4.useMemo(() => resolveConfig(config), [config]);
|
|
367
|
-
React4.useState(() => {
|
|
368
|
-
if (typeof window !== "undefined") {
|
|
369
|
-
consumeTabAdoptMarker();
|
|
370
|
-
restoreReturnFragment();
|
|
371
|
-
}
|
|
372
|
-
return null;
|
|
373
|
-
});
|
|
374
|
-
const [account, setAccount] = React4.useState(null);
|
|
375
|
-
const [roster, setRoster] = React4.useState([]);
|
|
376
|
-
const [status, setStatus] = React4.useState("loading");
|
|
377
|
-
const [error, setError] = React4.useState(null);
|
|
378
|
-
const [tabPinned, setTabPinned] = React4.useState(false);
|
|
379
|
-
const pinInFlightRef = React4.useRef(false);
|
|
380
|
-
const pinThisTab = React4.useCallback(
|
|
381
|
-
async (didOc) => {
|
|
382
|
-
if (!cfg.multiAccount || pinInFlightRef.current || readTabSession()) return;
|
|
383
|
-
pinInFlightRef.current = true;
|
|
384
|
-
try {
|
|
385
|
-
const res = await fetch(`${cfg.authOrigin}/api/auth/tab`, {
|
|
386
|
-
method: "POST",
|
|
387
|
-
credentials: "include",
|
|
388
|
-
headers: { Accept: "application/json" }
|
|
389
|
-
});
|
|
390
|
-
if (!res.ok) return;
|
|
391
|
-
const body = await res.json();
|
|
392
|
-
if (body.ok && typeof body.token === "string" && body.account?.did_oc === didOc) {
|
|
393
|
-
writeTabSession({ token: body.token, didOc });
|
|
394
|
-
setTabPinned(true);
|
|
395
|
-
}
|
|
396
|
-
} catch {
|
|
397
|
-
} finally {
|
|
398
|
-
pinInFlightRef.current = false;
|
|
399
|
-
}
|
|
400
|
-
},
|
|
401
|
-
[cfg.authOrigin, cfg.multiAccount]
|
|
402
|
-
);
|
|
403
|
-
const refresh = React4.useCallback(async () => {
|
|
404
|
-
if (typeof window === "undefined") return;
|
|
405
|
-
try {
|
|
406
|
-
let pin = readTabSession();
|
|
407
|
-
let res = await fetch(cfg.mePath, {
|
|
408
|
-
method: "GET",
|
|
409
|
-
credentials: "include",
|
|
410
|
-
headers: { Accept: "application/json", ...tabSessionHeader() }
|
|
411
|
-
});
|
|
412
|
-
if (res.status === 401 && pin) {
|
|
413
|
-
clearTabSession();
|
|
414
|
-
pin = null;
|
|
415
|
-
setTabPinned(false);
|
|
416
|
-
res = await fetch(cfg.mePath, {
|
|
417
|
-
method: "GET",
|
|
418
|
-
credentials: "include",
|
|
419
|
-
headers: { Accept: "application/json" }
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
if (res.status === 401) {
|
|
423
|
-
setAccount(null);
|
|
424
|
-
setRoster([]);
|
|
425
|
-
setStatus("anonymous");
|
|
426
|
-
setError(null);
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
if (!res.ok) {
|
|
430
|
-
setStatus("error");
|
|
431
|
-
setError(new Error(`me endpoint returned ${res.status}`));
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
const body = await res.json();
|
|
435
|
-
const acct = normalizeAccount(body.account);
|
|
436
|
-
if (acct && pin && acct.didOc !== pin.didOc) {
|
|
437
|
-
clearTabSession();
|
|
438
|
-
pin = null;
|
|
439
|
-
}
|
|
440
|
-
setTabPinned(pin !== null);
|
|
441
|
-
const rosterEntries = Array.isArray(body.roster) ? body.roster.map(normalizeRosterEntry).filter((r) => r !== null) : [];
|
|
442
|
-
setAccount(acct);
|
|
443
|
-
setRoster(rosterEntries);
|
|
444
|
-
setStatus(acct ? "authenticated" : "anonymous");
|
|
445
|
-
setError(null);
|
|
446
|
-
if (acct && !pin) void pinThisTab(acct.didOc);
|
|
447
|
-
const currentOrigin = typeof window !== "undefined" ? window.location.origin : null;
|
|
448
|
-
if (cfg.multiAccount && acct && rosterEntries.length === 0 && currentOrigin !== null && !cfg.authOrigin.startsWith(currentOrigin)) {
|
|
449
|
-
const peers = await fetchHostRoster(cfg);
|
|
450
|
-
if (peers.length > 0) setRoster(peers);
|
|
451
|
-
}
|
|
452
|
-
} catch (err) {
|
|
453
|
-
setStatus("error");
|
|
454
|
-
setError(err instanceof Error ? err : new Error(String(err)));
|
|
455
|
-
}
|
|
456
|
-
}, [cfg, pinThisTab]);
|
|
457
|
-
React4.useEffect(() => {
|
|
458
|
-
let cancelled = false;
|
|
459
|
-
consumeTabAdoptMarker();
|
|
460
|
-
void (async () => {
|
|
461
|
-
if (cfg.multiAccount) await consumeTabAccountHint(cfg.authOrigin);
|
|
462
|
-
if (!cancelled) void refresh();
|
|
463
|
-
})();
|
|
464
|
-
return () => {
|
|
465
|
-
cancelled = true;
|
|
466
|
-
};
|
|
467
|
-
}, [refresh, cfg.authOrigin, cfg.multiAccount]);
|
|
468
|
-
React4.useEffect(() => keepFragmentOnReturnLinks(), []);
|
|
469
|
-
React4.useEffect(
|
|
470
|
-
() => cfg.multiAccount ? installTabFetchInterceptor(cfg.authOrigin) : void 0,
|
|
471
|
-
[cfg.authOrigin, cfg.multiAccount]
|
|
472
|
-
);
|
|
473
|
-
React4.useEffect(
|
|
474
|
-
() => cfg.multiAccount ? installTabLinkDecorator(cfg.authOrigin) : void 0,
|
|
475
|
-
[cfg.authOrigin, cfg.multiAccount]
|
|
476
|
-
);
|
|
477
|
-
const signOut = React4.useCallback(
|
|
478
|
-
async (opts) => {
|
|
479
|
-
const scope = opts?.scope ?? "all";
|
|
480
|
-
try {
|
|
481
|
-
const url = new URL(`${cfg.authOrigin}${cfg.logoutPath}`);
|
|
482
|
-
if (scope === "current") url.searchParams.set("scope", "current");
|
|
483
|
-
const res = await fetch(url.toString(), {
|
|
484
|
-
method: "POST",
|
|
485
|
-
credentials: "include",
|
|
486
|
-
// Per-tab · carry the pin so scope='current' signs out
|
|
487
|
-
// the account THIS tab is operating as, not whichever
|
|
488
|
-
// account the shared cookie happens to point at.
|
|
489
|
-
headers: { ...tabSessionHeader() },
|
|
490
|
-
// `keepalive` lets the logout round-trip complete even if
|
|
491
|
-
// the caller hard-navigates away in the same tick (e.g.
|
|
492
|
-
// `<OcAccountMenu>` redirects home immediately on sign-out).
|
|
493
|
-
// Without it the in-flight request is cancelled on unload
|
|
494
|
-
// and the `.ochk.io` cookie may never get cleared.
|
|
495
|
-
keepalive: true
|
|
496
|
-
});
|
|
497
|
-
clearTabSession();
|
|
498
|
-
setTabPinned(false);
|
|
499
|
-
if (scope === "current" && res.ok) {
|
|
500
|
-
await refresh();
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
} catch {
|
|
504
|
-
clearTabSession();
|
|
505
|
-
setTabPinned(false);
|
|
506
|
-
}
|
|
507
|
-
setAccount(null);
|
|
508
|
-
setRoster([]);
|
|
509
|
-
setStatus("anonymous");
|
|
510
|
-
},
|
|
511
|
-
[cfg.authOrigin, cfg.logoutPath, refresh]
|
|
512
|
-
);
|
|
513
|
-
const switchAccount = React4.useCallback(
|
|
514
|
-
async (didOc) => {
|
|
515
|
-
if (typeof window === "undefined") return;
|
|
516
|
-
const res = await fetch(`${cfg.authOrigin}/api/auth/switch`, {
|
|
517
|
-
method: "POST",
|
|
518
|
-
credentials: "include",
|
|
519
|
-
headers: { "Content-Type": "application/json", ...tabSessionHeader() },
|
|
520
|
-
body: JSON.stringify({ did_oc: didOc })
|
|
521
|
-
});
|
|
522
|
-
if (!res.ok) {
|
|
523
|
-
let reason = `http_${res.status}`;
|
|
524
|
-
try {
|
|
525
|
-
const body = await res.json();
|
|
526
|
-
if (body.reason) reason = body.reason;
|
|
527
|
-
} catch {
|
|
528
|
-
}
|
|
529
|
-
throw new Error(`[@orangecheck/auth-client] switchAccount failed: ${reason}`);
|
|
530
|
-
}
|
|
531
|
-
try {
|
|
532
|
-
const body = await res.json();
|
|
533
|
-
if (typeof body.token === "string" && body.account?.did_oc) {
|
|
534
|
-
writeTabSession({ token: body.token, didOc: body.account.did_oc });
|
|
535
|
-
setTabPinned(true);
|
|
536
|
-
}
|
|
537
|
-
} catch {
|
|
538
|
-
}
|
|
539
|
-
await refresh();
|
|
540
|
-
},
|
|
541
|
-
[cfg.authOrigin, refresh]
|
|
542
|
-
);
|
|
543
|
-
const addAccountUrl = React4.useCallback(
|
|
544
|
-
(returnTo) => {
|
|
545
|
-
const rt = returnTo ?? (typeof window !== "undefined" ? window.location.href : void 0);
|
|
546
|
-
return buildAddAccountUrl(cfg, rt);
|
|
547
|
-
},
|
|
548
|
-
[cfg]
|
|
549
|
-
);
|
|
550
|
-
const setDisplayIdentity = React4.useCallback(
|
|
551
|
-
async (kind) => {
|
|
552
|
-
if (typeof window === "undefined") return;
|
|
553
|
-
const res = await fetch(`${cfg.authOrigin}/api/auth/account`, {
|
|
554
|
-
method: "PATCH",
|
|
555
|
-
credentials: "include",
|
|
556
|
-
headers: { "Content-Type": "application/json", ...tabSessionHeader() },
|
|
557
|
-
body: JSON.stringify({ display_identity: kind })
|
|
558
|
-
});
|
|
559
|
-
if (!res.ok) {
|
|
560
|
-
let reason = `http_${res.status}`;
|
|
561
|
-
try {
|
|
562
|
-
const body = await res.json();
|
|
563
|
-
if (body.reason) reason = body.reason;
|
|
564
|
-
} catch {
|
|
565
|
-
}
|
|
566
|
-
throw new Error(`[@orangecheck/auth-client] setDisplayIdentity failed: ${reason}`);
|
|
567
|
-
}
|
|
568
|
-
try {
|
|
569
|
-
const body = await res.json();
|
|
570
|
-
const pin = readTabSession();
|
|
571
|
-
if (pin && typeof body.token === "string" && body.account?.did_oc === pin.didOc) {
|
|
572
|
-
writeTabSession({ token: body.token, didOc: pin.didOc });
|
|
573
|
-
}
|
|
574
|
-
} catch {
|
|
575
|
-
}
|
|
576
|
-
await refresh();
|
|
577
|
-
},
|
|
578
|
-
[cfg.authOrigin, refresh]
|
|
579
|
-
);
|
|
580
|
-
const value = React4.useMemo(() => {
|
|
581
|
-
const returnTo = defaultReturnTo ?? (typeof window !== "undefined" ? window.location.href : void 0);
|
|
582
|
-
return {
|
|
583
|
-
status,
|
|
584
|
-
account,
|
|
585
|
-
roster,
|
|
586
|
-
tabPinned,
|
|
587
|
-
error,
|
|
588
|
-
refresh,
|
|
589
|
-
signOut,
|
|
590
|
-
switchAccount,
|
|
591
|
-
addAccountUrl,
|
|
592
|
-
setDisplayIdentity,
|
|
593
|
-
signInUrl: buildSignInUrl(cfg, returnTo)
|
|
594
|
-
};
|
|
595
|
-
}, [
|
|
596
|
-
status,
|
|
597
|
-
account,
|
|
598
|
-
roster,
|
|
599
|
-
tabPinned,
|
|
600
|
-
error,
|
|
601
|
-
refresh,
|
|
602
|
-
signOut,
|
|
603
|
-
switchAccount,
|
|
604
|
-
addAccountUrl,
|
|
605
|
-
setDisplayIdentity,
|
|
606
|
-
cfg,
|
|
607
|
-
defaultReturnTo
|
|
608
|
-
]);
|
|
609
|
-
return /* @__PURE__ */ jsx(SessionContext.Provider, { value, children });
|
|
610
|
-
}
|
|
611
|
-
function useOcSession() {
|
|
612
|
-
const ctx = React4.useContext(SessionContext);
|
|
613
|
-
if (!ctx) {
|
|
614
|
-
throw new Error(
|
|
615
|
-
"[@orangecheck/auth-client] useOcSession() must be called inside <OcSessionProvider>"
|
|
616
|
-
);
|
|
617
|
-
}
|
|
618
|
-
return ctx;
|
|
619
|
-
}
|
|
620
|
-
function useOptionalOcSession() {
|
|
621
|
-
return React4.useContext(SessionContext);
|
|
622
|
-
}
|
|
623
|
-
function shortenAddress(addr) {
|
|
624
|
-
if (addr.length <= 12) return addr;
|
|
625
|
-
return `${addr.slice(0, 6)}\u2026${addr.slice(-4)}`;
|
|
626
|
-
}
|
|
627
|
-
function shortenAddressMid(addr) {
|
|
628
|
-
if (addr.length <= 16) return addr;
|
|
629
|
-
return `${addr.slice(0, 8)}\u2026${addr.slice(-6)}`;
|
|
630
|
-
}
|
|
631
|
-
function isPrefixOf(value, target) {
|
|
632
|
-
return target.toLowerCase().startsWith(value.toLowerCase());
|
|
633
|
-
}
|
|
634
|
-
var listboxIdCounter = 0;
|
|
635
|
-
function useUniqueId(prefix) {
|
|
636
|
-
const [id] = React4.useState(() => `${prefix}-${++listboxIdCounter}`);
|
|
637
|
-
return id;
|
|
638
|
-
}
|
|
639
|
-
function OcAccountChip({
|
|
640
|
-
dashboardUrl = "https://ochk.io/dashboard",
|
|
641
|
-
signInUrl: signInUrlOverride,
|
|
642
|
-
signInLabel = "sign in",
|
|
643
|
-
className,
|
|
644
|
-
triggerClassName,
|
|
645
|
-
popoverClassName,
|
|
646
|
-
menuItemClassName
|
|
647
|
-
}) {
|
|
648
|
-
const { status, account, signInUrl: sessionSignInUrl, signOut } = useOcSession();
|
|
649
|
-
const signInUrl = signInUrlOverride ?? sessionSignInUrl;
|
|
650
|
-
const [open, setOpen] = React4.useState(false);
|
|
651
|
-
const wrapRef = React4.useRef(null);
|
|
652
|
-
React4.useEffect(() => {
|
|
653
|
-
if (!open) return;
|
|
654
|
-
function handleClickOutside(e) {
|
|
655
|
-
if (wrapRef.current && !wrapRef.current.contains(e.target)) {
|
|
656
|
-
setOpen(false);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
function handleEscape(e) {
|
|
660
|
-
if (e.key === "Escape") setOpen(false);
|
|
661
|
-
}
|
|
662
|
-
document.addEventListener("mousedown", handleClickOutside);
|
|
663
|
-
document.addEventListener("keydown", handleEscape);
|
|
664
|
-
return () => {
|
|
665
|
-
document.removeEventListener("mousedown", handleClickOutside);
|
|
666
|
-
document.removeEventListener("keydown", handleEscape);
|
|
667
|
-
};
|
|
668
|
-
}, [open]);
|
|
669
|
-
if (status === "loading") return null;
|
|
670
|
-
if (status !== "authenticated" || !account) {
|
|
671
|
-
return /* @__PURE__ */ jsx(
|
|
672
|
-
"a",
|
|
673
|
-
{
|
|
674
|
-
href: signInUrl,
|
|
675
|
-
className: triggerClassName ?? className,
|
|
676
|
-
"data-oc-account-chip-signin": "",
|
|
677
|
-
children: signInLabel
|
|
678
|
-
}
|
|
679
|
-
);
|
|
680
|
-
}
|
|
681
|
-
return /* @__PURE__ */ jsxs(
|
|
682
|
-
"div",
|
|
683
|
-
{
|
|
684
|
-
ref: wrapRef,
|
|
685
|
-
className,
|
|
686
|
-
"data-oc-account-chip": "",
|
|
687
|
-
style: { position: "relative", display: "inline-block" },
|
|
688
|
-
children: [
|
|
689
|
-
/* @__PURE__ */ jsxs(
|
|
690
|
-
"button",
|
|
691
|
-
{
|
|
692
|
-
type: "button",
|
|
693
|
-
onClick: () => setOpen((v) => !v),
|
|
694
|
-
"aria-haspopup": "menu",
|
|
695
|
-
"aria-expanded": open,
|
|
696
|
-
"aria-label": `Signed in as ${account.didOc}. Open account menu.`,
|
|
697
|
-
className: triggerClassName,
|
|
698
|
-
"data-oc-account-chip-trigger": "",
|
|
699
|
-
style: triggerClassName ? void 0 : {
|
|
700
|
-
display: "inline-flex",
|
|
701
|
-
alignItems: "center",
|
|
702
|
-
gap: "0.4rem",
|
|
703
|
-
padding: "0.25rem 0.625rem",
|
|
704
|
-
borderRadius: "9999px",
|
|
705
|
-
border: "1px solid var(--border, #27272a)",
|
|
706
|
-
background: "color-mix(in oklch, var(--muted, #27272a) 40%, transparent)",
|
|
707
|
-
color: "var(--foreground, #fafafa)",
|
|
708
|
-
fontFamily: "ui-monospace, SFMono-Regular, monospace",
|
|
709
|
-
fontSize: 12,
|
|
710
|
-
cursor: "pointer"
|
|
711
|
-
},
|
|
712
|
-
children: [
|
|
713
|
-
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: { color: "#22c55e", fontSize: 8 }, children: "\u25CF" }),
|
|
714
|
-
/* @__PURE__ */ jsx("span", { children: account.displayName ?? shortenAddress(account.didOc) }),
|
|
715
|
-
/* @__PURE__ */ jsx(
|
|
716
|
-
"svg",
|
|
717
|
-
{
|
|
718
|
-
width: "9",
|
|
719
|
-
height: "9",
|
|
720
|
-
viewBox: "0 0 10 10",
|
|
721
|
-
"aria-hidden": "true",
|
|
722
|
-
style: {
|
|
723
|
-
transition: "transform 120ms",
|
|
724
|
-
transform: open ? "rotate(180deg)" : "rotate(0)",
|
|
725
|
-
opacity: 0.6
|
|
726
|
-
},
|
|
727
|
-
children: /* @__PURE__ */ jsx(
|
|
728
|
-
"path",
|
|
729
|
-
{
|
|
730
|
-
d: "M2 4 L5 7 L8 4",
|
|
731
|
-
stroke: "currentColor",
|
|
732
|
-
strokeWidth: "1.5",
|
|
733
|
-
fill: "none",
|
|
734
|
-
strokeLinecap: "round"
|
|
735
|
-
}
|
|
736
|
-
)
|
|
737
|
-
}
|
|
738
|
-
)
|
|
739
|
-
]
|
|
740
|
-
}
|
|
741
|
-
),
|
|
742
|
-
open && /* @__PURE__ */ jsxs(
|
|
743
|
-
"div",
|
|
744
|
-
{
|
|
745
|
-
role: "menu",
|
|
746
|
-
"aria-label": "Account menu",
|
|
747
|
-
className: popoverClassName,
|
|
748
|
-
"data-oc-account-chip-popover": "",
|
|
749
|
-
style: popoverClassName ? { position: "absolute", zIndex: 50, top: "calc(100% + 6px)", right: 0 } : {
|
|
750
|
-
position: "absolute",
|
|
751
|
-
zIndex: 50,
|
|
752
|
-
top: "calc(100% + 6px)",
|
|
753
|
-
right: 0,
|
|
754
|
-
minWidth: 240,
|
|
755
|
-
background: "var(--popover, #0a0a0a)",
|
|
756
|
-
color: "var(--popover-foreground, #fafafa)",
|
|
757
|
-
border: "1px solid var(--border, #27272a)",
|
|
758
|
-
boxShadow: "0 10px 32px -4px rgba(0,0,0,0.35)",
|
|
759
|
-
fontFamily: "ui-monospace, SFMono-Regular, monospace"
|
|
760
|
-
},
|
|
761
|
-
children: [
|
|
762
|
-
/* @__PURE__ */ jsxs(
|
|
763
|
-
"div",
|
|
764
|
-
{
|
|
765
|
-
"data-oc-account-chip-header": "",
|
|
766
|
-
style: {
|
|
767
|
-
padding: "0.75rem 0.75rem 0.625rem",
|
|
768
|
-
borderBottom: "1px solid var(--border, #27272a)"
|
|
769
|
-
},
|
|
770
|
-
children: [
|
|
771
|
-
/* @__PURE__ */ jsx(
|
|
772
|
-
"div",
|
|
773
|
-
{
|
|
774
|
-
"data-oc-account-chip-label": "",
|
|
775
|
-
style: {
|
|
776
|
-
color: "var(--primary, #f97316)",
|
|
777
|
-
fontSize: 10,
|
|
778
|
-
letterSpacing: "0.18em",
|
|
779
|
-
textTransform: "uppercase",
|
|
780
|
-
marginBottom: 4
|
|
781
|
-
},
|
|
782
|
-
children: "\xA7 signed in"
|
|
783
|
-
}
|
|
784
|
-
),
|
|
785
|
-
/* @__PURE__ */ jsx(
|
|
786
|
-
"div",
|
|
787
|
-
{
|
|
788
|
-
"data-oc-account-chip-address": "",
|
|
789
|
-
style: {
|
|
790
|
-
color: "var(--popover-foreground, #fafafa)",
|
|
791
|
-
fontSize: 11,
|
|
792
|
-
wordBreak: "break-all",
|
|
793
|
-
lineHeight: 1.35
|
|
794
|
-
},
|
|
795
|
-
children: account.didOc
|
|
796
|
-
}
|
|
797
|
-
),
|
|
798
|
-
account.displayName ? /* @__PURE__ */ jsx(
|
|
799
|
-
"div",
|
|
800
|
-
{
|
|
801
|
-
style: {
|
|
802
|
-
color: "var(--muted-foreground, #a1a1aa)",
|
|
803
|
-
fontSize: 10,
|
|
804
|
-
marginTop: 4,
|
|
805
|
-
letterSpacing: "0.06em"
|
|
806
|
-
},
|
|
807
|
-
children: account.displayName
|
|
808
|
-
}
|
|
809
|
-
) : null
|
|
810
|
-
]
|
|
811
|
-
}
|
|
812
|
-
),
|
|
813
|
-
/* @__PURE__ */ jsxs("div", { role: "none", style: { padding: "0.25rem" }, children: [
|
|
814
|
-
dashboardUrl ? /* @__PURE__ */ jsxs(
|
|
815
|
-
"a",
|
|
816
|
-
{
|
|
817
|
-
role: "menuitem",
|
|
818
|
-
href: dashboardUrl,
|
|
819
|
-
onClick: () => setOpen(false),
|
|
820
|
-
className: menuItemClassName,
|
|
821
|
-
"data-oc-account-chip-item": "",
|
|
822
|
-
style: menuItemClassName ? void 0 : {
|
|
823
|
-
display: "flex",
|
|
824
|
-
alignItems: "center",
|
|
825
|
-
gap: "0.5rem",
|
|
826
|
-
padding: "0.5rem 0.625rem",
|
|
827
|
-
fontSize: 12,
|
|
828
|
-
color: "var(--popover-foreground, #fafafa)",
|
|
829
|
-
textDecoration: "none",
|
|
830
|
-
cursor: "pointer"
|
|
831
|
-
},
|
|
832
|
-
children: [
|
|
833
|
-
/* @__PURE__ */ jsx(
|
|
834
|
-
"span",
|
|
835
|
-
{
|
|
836
|
-
"aria-hidden": "true",
|
|
837
|
-
style: { color: "var(--muted-foreground, #a1a1aa)" },
|
|
838
|
-
children: "\u2192"
|
|
839
|
-
}
|
|
840
|
-
),
|
|
841
|
-
/* @__PURE__ */ jsx("span", { style: { flex: 1 }, children: "dashboard" }),
|
|
842
|
-
/* @__PURE__ */ jsx(
|
|
843
|
-
"span",
|
|
844
|
-
{
|
|
845
|
-
style: {
|
|
846
|
-
color: "var(--muted-foreground, #a1a1aa)",
|
|
847
|
-
fontSize: 10
|
|
848
|
-
},
|
|
849
|
-
children: "\u2197"
|
|
850
|
-
}
|
|
851
|
-
)
|
|
852
|
-
]
|
|
853
|
-
}
|
|
854
|
-
) : null,
|
|
855
|
-
/* @__PURE__ */ jsxs(
|
|
856
|
-
"button",
|
|
857
|
-
{
|
|
858
|
-
type: "button",
|
|
859
|
-
role: "menuitem",
|
|
860
|
-
onClick: () => {
|
|
861
|
-
setOpen(false);
|
|
862
|
-
void signOut();
|
|
863
|
-
},
|
|
864
|
-
className: menuItemClassName,
|
|
865
|
-
"data-oc-account-chip-item": "",
|
|
866
|
-
"data-oc-account-chip-signout": "",
|
|
867
|
-
style: menuItemClassName ? void 0 : {
|
|
868
|
-
display: "flex",
|
|
869
|
-
alignItems: "center",
|
|
870
|
-
gap: "0.5rem",
|
|
871
|
-
width: "100%",
|
|
872
|
-
padding: "0.5rem 0.625rem",
|
|
873
|
-
fontSize: 12,
|
|
874
|
-
color: "var(--popover-foreground, #fafafa)",
|
|
875
|
-
background: "transparent",
|
|
876
|
-
border: 0,
|
|
877
|
-
fontFamily: "inherit",
|
|
878
|
-
textAlign: "left",
|
|
879
|
-
cursor: "pointer"
|
|
880
|
-
},
|
|
881
|
-
children: [
|
|
882
|
-
/* @__PURE__ */ jsx(
|
|
883
|
-
"span",
|
|
884
|
-
{
|
|
885
|
-
"aria-hidden": "true",
|
|
886
|
-
style: { color: "var(--muted-foreground, #a1a1aa)" },
|
|
887
|
-
children: "\u2192"
|
|
888
|
-
}
|
|
889
|
-
),
|
|
890
|
-
/* @__PURE__ */ jsx("span", { style: { flex: 1 }, children: "sign out" })
|
|
891
|
-
]
|
|
892
|
-
}
|
|
893
|
-
)
|
|
894
|
-
] })
|
|
895
|
-
]
|
|
896
|
-
}
|
|
897
|
-
)
|
|
898
|
-
]
|
|
899
|
-
}
|
|
900
|
-
);
|
|
901
|
-
}
|
|
902
|
-
function OcSignInButton({
|
|
903
|
-
label = "sign in",
|
|
904
|
-
eager = false,
|
|
905
|
-
className,
|
|
906
|
-
signInUrl: signInUrlOverride,
|
|
907
|
-
...rest
|
|
908
|
-
}) {
|
|
909
|
-
const { status, signInUrl: sessionSignInUrl } = useOcSession();
|
|
910
|
-
const href = signInUrlOverride ?? sessionSignInUrl;
|
|
911
|
-
if (status === "authenticated") return null;
|
|
912
|
-
if (!eager && status === "loading") return null;
|
|
913
|
-
return /* @__PURE__ */ jsx(
|
|
914
|
-
"a",
|
|
915
|
-
{
|
|
916
|
-
...rest,
|
|
917
|
-
href,
|
|
918
|
-
className,
|
|
919
|
-
"data-oc-sign-in-button": "",
|
|
920
|
-
children: label
|
|
921
|
-
}
|
|
922
|
-
);
|
|
923
|
-
}
|
|
924
|
-
function OcAccountPill({
|
|
925
|
-
dashboardUrl,
|
|
926
|
-
render,
|
|
927
|
-
className,
|
|
928
|
-
...rest
|
|
929
|
-
}) {
|
|
930
|
-
const { status, account, signOut } = useOcSession();
|
|
931
|
-
if (status !== "authenticated" || !account) return null;
|
|
932
|
-
const label = render ? render({ address: account.didOc, displayName: account.displayName }) : account.displayName ?? shortenAddress(account.didOc);
|
|
933
|
-
return /* @__PURE__ */ jsxs(
|
|
934
|
-
"div",
|
|
935
|
-
{
|
|
936
|
-
...rest,
|
|
937
|
-
className,
|
|
938
|
-
"data-oc-account-pill": "",
|
|
939
|
-
style: { display: "inline-flex", alignItems: "center", gap: "0.5rem", ...rest.style ?? {} },
|
|
940
|
-
children: [
|
|
941
|
-
dashboardUrl ? /* @__PURE__ */ jsx("a", { href: dashboardUrl, children: label }) : /* @__PURE__ */ jsx("span", { children: label }),
|
|
942
|
-
/* @__PURE__ */ jsx(
|
|
943
|
-
"button",
|
|
944
|
-
{
|
|
945
|
-
type: "button",
|
|
946
|
-
onClick: () => {
|
|
947
|
-
void signOut();
|
|
948
|
-
},
|
|
949
|
-
"aria-label": "Sign out",
|
|
950
|
-
style: {
|
|
951
|
-
background: "none",
|
|
952
|
-
border: "none",
|
|
953
|
-
cursor: "pointer",
|
|
954
|
-
color: "inherit",
|
|
955
|
-
font: "inherit",
|
|
956
|
-
padding: 0
|
|
957
|
-
},
|
|
958
|
-
children: "sign out"
|
|
959
|
-
}
|
|
960
|
-
)
|
|
961
|
-
]
|
|
962
|
-
}
|
|
963
|
-
);
|
|
964
|
-
}
|
|
965
|
-
function accountAddress(account) {
|
|
966
|
-
if (account?.primaryBtc) return account.primaryBtc;
|
|
967
|
-
return account?.displayIdentity?.kind === "btc" ? account.displayIdentity.value : null;
|
|
968
|
-
}
|
|
969
|
-
function useOcAddressSuggestion(options) {
|
|
970
|
-
const {
|
|
971
|
-
value,
|
|
972
|
-
onValueChange,
|
|
973
|
-
suggestionLabel = "use your address",
|
|
974
|
-
popoverClassName,
|
|
975
|
-
suggestionClassName
|
|
976
|
-
} = options;
|
|
977
|
-
const { status, account } = useOcSession();
|
|
978
|
-
const sessionAddress = status === "authenticated" ? accountAddress(account) : null;
|
|
979
|
-
const [open, setOpen] = React4.useState(false);
|
|
980
|
-
const [highlighted, setHighlighted] = React4.useState(false);
|
|
981
|
-
const blurTimer = React4.useRef(null);
|
|
982
|
-
const listboxId = useUniqueId("oc-addr-listbox");
|
|
983
|
-
const optionId = `${listboxId}-opt`;
|
|
984
|
-
const valueMatchesSession = sessionAddress != null && value.toLowerCase() === sessionAddress.toLowerCase();
|
|
985
|
-
const canSuggest = sessionAddress != null && sessionAddress.length > 0 && !valueMatchesSession && isPrefixOf(value, sessionAddress);
|
|
986
|
-
const showPopover = open && canSuggest;
|
|
987
|
-
function selectSuggestion() {
|
|
988
|
-
if (!sessionAddress) return;
|
|
989
|
-
onValueChange(sessionAddress);
|
|
990
|
-
setOpen(false);
|
|
991
|
-
setHighlighted(false);
|
|
992
|
-
}
|
|
993
|
-
React4.useEffect(() => {
|
|
994
|
-
return () => {
|
|
995
|
-
if (blurTimer.current != null) window.clearTimeout(blurTimer.current);
|
|
996
|
-
};
|
|
997
|
-
}, []);
|
|
998
|
-
const inputProps = {
|
|
999
|
-
onFocus: () => {
|
|
1000
|
-
if (blurTimer.current != null) {
|
|
1001
|
-
window.clearTimeout(blurTimer.current);
|
|
1002
|
-
blurTimer.current = null;
|
|
1003
|
-
}
|
|
1004
|
-
setOpen(true);
|
|
1005
|
-
},
|
|
1006
|
-
onBlur: () => {
|
|
1007
|
-
blurTimer.current = window.setTimeout(() => {
|
|
1008
|
-
setOpen(false);
|
|
1009
|
-
setHighlighted(false);
|
|
1010
|
-
}, 120);
|
|
1011
|
-
},
|
|
1012
|
-
onKeyDown: (e) => {
|
|
1013
|
-
if (!showPopover) return;
|
|
1014
|
-
if (e.key === "ArrowDown") {
|
|
1015
|
-
e.preventDefault();
|
|
1016
|
-
setHighlighted(true);
|
|
1017
|
-
} else if (e.key === "ArrowUp") {
|
|
1018
|
-
e.preventDefault();
|
|
1019
|
-
setHighlighted(false);
|
|
1020
|
-
} else if (e.key === "Escape") {
|
|
1021
|
-
e.preventDefault();
|
|
1022
|
-
setOpen(false);
|
|
1023
|
-
setHighlighted(false);
|
|
1024
|
-
} else if (e.key === "Enter" && highlighted) {
|
|
1025
|
-
e.preventDefault();
|
|
1026
|
-
selectSuggestion();
|
|
1027
|
-
}
|
|
1028
|
-
},
|
|
1029
|
-
role: "combobox",
|
|
1030
|
-
"aria-haspopup": "listbox",
|
|
1031
|
-
"aria-expanded": showPopover,
|
|
1032
|
-
"aria-controls": showPopover ? listboxId : void 0,
|
|
1033
|
-
"aria-activedescendant": highlighted ? optionId : void 0,
|
|
1034
|
-
autoComplete: "off",
|
|
1035
|
-
spellCheck: false
|
|
1036
|
-
};
|
|
1037
|
-
const popover = showPopover && sessionAddress ? /* @__PURE__ */ jsx(
|
|
1038
|
-
"div",
|
|
1039
|
-
{
|
|
1040
|
-
id: listboxId,
|
|
1041
|
-
role: "listbox",
|
|
1042
|
-
"data-oc-address-popover": "",
|
|
1043
|
-
className: popoverClassName,
|
|
1044
|
-
style: {
|
|
1045
|
-
position: "absolute",
|
|
1046
|
-
zIndex: 50,
|
|
1047
|
-
top: "calc(100% + 4px)",
|
|
1048
|
-
left: 0,
|
|
1049
|
-
right: 0
|
|
1050
|
-
},
|
|
1051
|
-
onMouseDown: (e) => e.preventDefault(),
|
|
1052
|
-
children: /* @__PURE__ */ jsxs(
|
|
1053
|
-
"button",
|
|
1054
|
-
{
|
|
1055
|
-
type: "button",
|
|
1056
|
-
id: optionId,
|
|
1057
|
-
role: "option",
|
|
1058
|
-
"aria-selected": highlighted,
|
|
1059
|
-
"data-oc-address-suggestion": "",
|
|
1060
|
-
"data-highlighted": highlighted ? "" : void 0,
|
|
1061
|
-
className: suggestionClassName,
|
|
1062
|
-
onClick: selectSuggestion,
|
|
1063
|
-
onMouseEnter: () => setHighlighted(true),
|
|
1064
|
-
onMouseLeave: () => setHighlighted(false),
|
|
1065
|
-
style: {
|
|
1066
|
-
display: "flex",
|
|
1067
|
-
alignItems: "center",
|
|
1068
|
-
justifyContent: "space-between",
|
|
1069
|
-
gap: "0.75rem",
|
|
1070
|
-
width: "100%",
|
|
1071
|
-
textAlign: "left",
|
|
1072
|
-
cursor: "pointer",
|
|
1073
|
-
font: "inherit",
|
|
1074
|
-
background: "inherit",
|
|
1075
|
-
color: "inherit",
|
|
1076
|
-
border: 0,
|
|
1077
|
-
padding: "inherit"
|
|
1078
|
-
},
|
|
1079
|
-
children: [
|
|
1080
|
-
/* @__PURE__ */ jsx("span", { "data-oc-address-suggestion-label": "", children: suggestionLabel }),
|
|
1081
|
-
/* @__PURE__ */ jsx(
|
|
1082
|
-
"span",
|
|
1083
|
-
{
|
|
1084
|
-
"data-oc-address-suggestion-value": "",
|
|
1085
|
-
style: { fontFamily: "ui-monospace, monospace" },
|
|
1086
|
-
children: shortenAddressMid(sessionAddress)
|
|
1087
|
-
}
|
|
1088
|
-
)
|
|
1089
|
-
]
|
|
1090
|
-
}
|
|
1091
|
-
)
|
|
1092
|
-
}
|
|
1093
|
-
) : null;
|
|
1094
|
-
return { inputProps, popover };
|
|
1095
|
-
}
|
|
1096
|
-
var OcAddressInput = React4.forwardRef(
|
|
1097
|
-
function OcAddressInput2({
|
|
1098
|
-
value,
|
|
1099
|
-
onValueChange,
|
|
1100
|
-
suggestionLabel = "use your address",
|
|
1101
|
-
wrapperClassName,
|
|
1102
|
-
popoverClassName,
|
|
1103
|
-
suggestionClassName,
|
|
1104
|
-
onFocus,
|
|
1105
|
-
onBlur,
|
|
1106
|
-
onKeyDown,
|
|
1107
|
-
...rest
|
|
1108
|
-
}, forwardedRef) {
|
|
1109
|
-
const { status, account } = useOcSession();
|
|
1110
|
-
const sessionAddress = status === "authenticated" ? accountAddress(account) : null;
|
|
1111
|
-
const [open, setOpen] = React4.useState(false);
|
|
1112
|
-
const [highlighted, setHighlighted] = React4.useState(false);
|
|
1113
|
-
const innerRef = React4.useRef(null);
|
|
1114
|
-
const setRef = (node) => {
|
|
1115
|
-
innerRef.current = node;
|
|
1116
|
-
if (typeof forwardedRef === "function") forwardedRef(node);
|
|
1117
|
-
else if (forwardedRef) forwardedRef.current = node;
|
|
1118
|
-
};
|
|
1119
|
-
const blurTimer = React4.useRef(null);
|
|
1120
|
-
const listboxId = useUniqueId("oc-addr-listbox");
|
|
1121
|
-
const optionId = `${listboxId}-opt`;
|
|
1122
|
-
const valueMatchesSession = sessionAddress != null && value.toLowerCase() === sessionAddress.toLowerCase();
|
|
1123
|
-
const canSuggest = sessionAddress != null && sessionAddress.length > 0 && !valueMatchesSession && isPrefixOf(value, sessionAddress);
|
|
1124
|
-
const showPopover = open && canSuggest;
|
|
1125
|
-
function selectSuggestion() {
|
|
1126
|
-
if (!sessionAddress) return;
|
|
1127
|
-
onValueChange(sessionAddress);
|
|
1128
|
-
setOpen(false);
|
|
1129
|
-
setHighlighted(false);
|
|
1130
|
-
innerRef.current?.focus();
|
|
1131
|
-
}
|
|
1132
|
-
function handleFocus(e) {
|
|
1133
|
-
if (blurTimer.current != null) {
|
|
1134
|
-
window.clearTimeout(blurTimer.current);
|
|
1135
|
-
blurTimer.current = null;
|
|
1136
|
-
}
|
|
1137
|
-
setOpen(true);
|
|
1138
|
-
onFocus?.(e);
|
|
1139
|
-
}
|
|
1140
|
-
function handleBlur(e) {
|
|
1141
|
-
blurTimer.current = window.setTimeout(() => {
|
|
1142
|
-
setOpen(false);
|
|
1143
|
-
setHighlighted(false);
|
|
1144
|
-
}, 120);
|
|
1145
|
-
onBlur?.(e);
|
|
1146
|
-
}
|
|
1147
|
-
function handleKeyDown(e) {
|
|
1148
|
-
if (showPopover) {
|
|
1149
|
-
if (e.key === "ArrowDown") {
|
|
1150
|
-
e.preventDefault();
|
|
1151
|
-
setHighlighted(true);
|
|
1152
|
-
} else if (e.key === "ArrowUp") {
|
|
1153
|
-
e.preventDefault();
|
|
1154
|
-
setHighlighted(false);
|
|
1155
|
-
} else if (e.key === "Escape") {
|
|
1156
|
-
e.preventDefault();
|
|
1157
|
-
setOpen(false);
|
|
1158
|
-
setHighlighted(false);
|
|
1159
|
-
} else if (e.key === "Enter" && highlighted) {
|
|
1160
|
-
e.preventDefault();
|
|
1161
|
-
selectSuggestion();
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
onKeyDown?.(e);
|
|
1165
|
-
}
|
|
1166
|
-
React4.useEffect(() => {
|
|
1167
|
-
return () => {
|
|
1168
|
-
if (blurTimer.current != null) window.clearTimeout(blurTimer.current);
|
|
1169
|
-
};
|
|
1170
|
-
}, []);
|
|
1171
|
-
return /* @__PURE__ */ jsxs(
|
|
1172
|
-
"div",
|
|
1173
|
-
{
|
|
1174
|
-
"data-oc-address-input": "",
|
|
1175
|
-
className: wrapperClassName,
|
|
1176
|
-
style: { position: "relative" },
|
|
1177
|
-
children: [
|
|
1178
|
-
/* @__PURE__ */ jsx(
|
|
1179
|
-
"input",
|
|
1180
|
-
{
|
|
1181
|
-
...rest,
|
|
1182
|
-
ref: setRef,
|
|
1183
|
-
value,
|
|
1184
|
-
onChange: (e) => onValueChange(e.target.value),
|
|
1185
|
-
onFocus: handleFocus,
|
|
1186
|
-
onBlur: handleBlur,
|
|
1187
|
-
onKeyDown: handleKeyDown,
|
|
1188
|
-
role: "combobox",
|
|
1189
|
-
"aria-haspopup": "listbox",
|
|
1190
|
-
"aria-expanded": showPopover,
|
|
1191
|
-
"aria-controls": showPopover ? listboxId : void 0,
|
|
1192
|
-
"aria-activedescendant": highlighted ? optionId : void 0,
|
|
1193
|
-
autoComplete: "off",
|
|
1194
|
-
spellCheck: false
|
|
1195
|
-
}
|
|
1196
|
-
),
|
|
1197
|
-
showPopover && sessionAddress && /* @__PURE__ */ jsx(
|
|
1198
|
-
"div",
|
|
1199
|
-
{
|
|
1200
|
-
id: listboxId,
|
|
1201
|
-
role: "listbox",
|
|
1202
|
-
"data-oc-address-popover": "",
|
|
1203
|
-
className: popoverClassName,
|
|
1204
|
-
style: {
|
|
1205
|
-
position: "absolute",
|
|
1206
|
-
zIndex: 50,
|
|
1207
|
-
top: "calc(100% + 4px)",
|
|
1208
|
-
left: 0,
|
|
1209
|
-
right: 0
|
|
1210
|
-
},
|
|
1211
|
-
onMouseDown: (e) => e.preventDefault(),
|
|
1212
|
-
children: /* @__PURE__ */ jsxs(
|
|
1213
|
-
"button",
|
|
1214
|
-
{
|
|
1215
|
-
type: "button",
|
|
1216
|
-
id: optionId,
|
|
1217
|
-
role: "option",
|
|
1218
|
-
"aria-selected": highlighted,
|
|
1219
|
-
"data-oc-address-suggestion": "",
|
|
1220
|
-
"data-highlighted": highlighted ? "" : void 0,
|
|
1221
|
-
className: suggestionClassName,
|
|
1222
|
-
onClick: selectSuggestion,
|
|
1223
|
-
onMouseEnter: () => setHighlighted(true),
|
|
1224
|
-
onMouseLeave: () => setHighlighted(false),
|
|
1225
|
-
style: {
|
|
1226
|
-
display: "flex",
|
|
1227
|
-
alignItems: "center",
|
|
1228
|
-
justifyContent: "space-between",
|
|
1229
|
-
gap: "0.75rem",
|
|
1230
|
-
width: "100%",
|
|
1231
|
-
textAlign: "left",
|
|
1232
|
-
cursor: "pointer",
|
|
1233
|
-
font: "inherit",
|
|
1234
|
-
background: "inherit",
|
|
1235
|
-
color: "inherit",
|
|
1236
|
-
border: "inherit",
|
|
1237
|
-
padding: "inherit"
|
|
1238
|
-
},
|
|
1239
|
-
children: [
|
|
1240
|
-
/* @__PURE__ */ jsx("span", { "data-oc-address-suggestion-label": "", children: suggestionLabel }),
|
|
1241
|
-
/* @__PURE__ */ jsx(
|
|
1242
|
-
"span",
|
|
1243
|
-
{
|
|
1244
|
-
"data-oc-address-suggestion-value": "",
|
|
1245
|
-
style: { fontFamily: "ui-monospace, monospace" },
|
|
1246
|
-
children: shortenAddressMid(sessionAddress)
|
|
1247
|
-
}
|
|
1248
|
-
)
|
|
1249
|
-
]
|
|
1250
|
-
}
|
|
1251
|
-
)
|
|
1252
|
-
}
|
|
1253
|
-
)
|
|
1254
|
-
]
|
|
1255
|
-
}
|
|
1256
|
-
);
|
|
1257
|
-
}
|
|
1258
|
-
);
|
|
1259
|
-
|
|
1260
|
-
// src/sudo.tsx
|
|
1261
|
-
function redirectToSudo(args = {}) {
|
|
1262
|
-
if (typeof window === "undefined") {
|
|
1263
|
-
throw new Error(
|
|
1264
|
-
"[@orangecheck/auth-client] redirectToSudo() requires a browser environment"
|
|
1265
|
-
);
|
|
1266
|
-
}
|
|
1267
|
-
const cfg = resolveConfig(args.config);
|
|
1268
|
-
const url = new URL("/sudo", cfg.authOrigin);
|
|
1269
|
-
const returnTo = withoutFragment(args.returnTo ?? window.location.href);
|
|
1270
|
-
if (returnTo) url.searchParams.set("return_to", returnTo);
|
|
1271
|
-
if (args.purpose) url.searchParams.set("purpose", args.purpose);
|
|
1272
|
-
window.location.assign(url.toString());
|
|
1273
|
-
}
|
|
1274
|
-
function handleSudoRequired(body, args = {}) {
|
|
1275
|
-
if (body && body.reason === "sudo_required") {
|
|
1276
|
-
redirectToSudo(args);
|
|
1277
|
-
return true;
|
|
1278
|
-
}
|
|
1279
|
-
return false;
|
|
1280
|
-
}
|
|
1281
|
-
var SUDO_ACCOUNT_MISMATCH_MESSAGE = "Switch this tab to that account first \u2014 the confirmation on file is for a different one.";
|
|
1282
|
-
function isSudoAccountMismatch(body) {
|
|
1283
|
-
return body?.reason === "sudo_account_mismatch";
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
// src/appearance.ts
|
|
1287
|
-
var SIGNIN_FONT = "var(--oc-signin-font, ui-monospace, SFMono-Regular, monospace)";
|
|
1288
|
-
var SIGNIN_CASE = "var(--oc-signin-case, uppercase)";
|
|
1289
|
-
var signinTracking = (em) => `var(--oc-signin-tracking, ${em})`;
|
|
1290
|
-
var signinSize = (px) => `calc(${px}px * var(--oc-signin-scale, 1))`;
|
|
1291
|
-
var signinRadius = (px) => `var(--oc-signin-radius, ${px}px)`;
|
|
1292
|
-
var SIGNIN_BUTTON_RADIUS = "var(--oc-signin-button-radius, 6px)";
|
|
1293
|
-
var SIGNIN_STRONG = "var(--oc-signin-strong, 600)";
|
|
1294
|
-
var SIGNIN_ARROW = "var(--oc-signin-arrow, inline)";
|
|
1295
|
-
var signinRoleSize = (role, fallback) => `var(--oc-signin-${role}-size, ${fallback})`;
|
|
1296
|
-
var DEFAULT_AUTH_ORIGIN = "https://ochk.io";
|
|
1297
|
-
async function fetchOcLinkedIdentities(opts) {
|
|
1298
|
-
const authOrigin = opts?.authOrigin ?? DEFAULT_AUTH_ORIGIN;
|
|
1299
|
-
const r = await fetch(`${authOrigin}/api/auth/identities`, {
|
|
1300
|
-
credentials: "include",
|
|
1301
|
-
headers: { ...tabSessionHeader() }
|
|
1302
|
-
});
|
|
1303
|
-
if (r.status === 401) return [];
|
|
1304
|
-
if (!r.ok) throw new Error(`identities fetch failed: http_${r.status}`);
|
|
1305
|
-
const body = await r.json();
|
|
1306
|
-
return body.linked ?? [];
|
|
1307
|
-
}
|
|
1308
|
-
function OcLinkedIdentities({
|
|
1309
|
-
authOrigin = DEFAULT_AUTH_ORIGIN,
|
|
1310
|
-
className,
|
|
1311
|
-
onChange,
|
|
1312
|
-
attestationLink = true
|
|
1313
|
-
}) {
|
|
1314
|
-
const [resp, setResp] = React4.useState(null);
|
|
1315
|
-
const [error, setError] = React4.useState(null);
|
|
1316
|
-
const [showEmailLink, setShowEmailLink] = React4.useState(false);
|
|
1317
|
-
const [showBtcLink, setShowBtcLink] = React4.useState(false);
|
|
1318
|
-
const refresh = React4.useCallback(async () => {
|
|
1319
|
-
setError(null);
|
|
1320
|
-
try {
|
|
1321
|
-
const r = await fetch(`${authOrigin}/api/auth/identities`, {
|
|
1322
|
-
credentials: "include",
|
|
1323
|
-
headers: { ...tabSessionHeader() }
|
|
1324
|
-
});
|
|
1325
|
-
if (!r.ok) {
|
|
1326
|
-
if (r.status === 401) {
|
|
1327
|
-
setError("not signed in");
|
|
1328
|
-
return;
|
|
1329
|
-
}
|
|
1330
|
-
throw new Error(`http_${r.status}`);
|
|
1331
|
-
}
|
|
1332
|
-
setResp(await r.json());
|
|
1333
|
-
} catch (e) {
|
|
1334
|
-
setError(e instanceof Error ? e.message : "load failed");
|
|
1335
|
-
}
|
|
1336
|
-
}, [authOrigin]);
|
|
1337
|
-
React4.useEffect(() => {
|
|
1338
|
-
void refresh();
|
|
1339
|
-
}, [refresh]);
|
|
1340
|
-
const afterChange = React4.useCallback(() => {
|
|
1341
|
-
setShowEmailLink(false);
|
|
1342
|
-
setShowBtcLink(false);
|
|
1343
|
-
void refresh();
|
|
1344
|
-
onChange?.();
|
|
1345
|
-
}, [refresh, onChange]);
|
|
1346
|
-
if (error) {
|
|
1347
|
-
return /* @__PURE__ */ jsxs("div", { className, "data-oc-li": "", style: panelStyle("warn"), role: "alert", children: [
|
|
1348
|
-
/* @__PURE__ */ jsx(SectionLabel, { tone: "warn", children: "\xA7 identities \xB7 load deferred" }),
|
|
1349
|
-
/* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
|
|
1350
|
-
"Couldn't reach the auth host (",
|
|
1351
|
-
error,
|
|
1352
|
-
"). Linking is temporarily unavailable; your signed-in identity still works as normal."
|
|
1353
|
-
] }),
|
|
1354
|
-
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => void refresh(), style: ghostBtnStyle(false), children: "retry" })
|
|
1355
|
-
] });
|
|
1356
|
-
}
|
|
1357
|
-
if (!resp) {
|
|
1358
|
-
return /* @__PURE__ */ jsx("div", { className, "data-oc-li": "", style: panelStyle(), children: /* @__PURE__ */ jsx("span", { style: hintStyle, children: "loading\u2026" }) });
|
|
1359
|
-
}
|
|
1360
|
-
const linked = resp.linked ?? [];
|
|
1361
|
-
const emailRows = linked.filter((i) => i.kind === "email");
|
|
1362
|
-
const btcRows = linked.filter((i) => i.kind === "btc");
|
|
1363
|
-
const hasEmail = emailRows.some((i) => i.value);
|
|
1364
|
-
const hasBtc = btcRows.some((i) => i.value);
|
|
1365
|
-
const mergedFrom = resp.merged_from ?? [];
|
|
1366
|
-
return /* @__PURE__ */ jsxs("div", { className, "data-oc-li": "", style: { display: "grid", gap: 16 }, children: [
|
|
1367
|
-
/* @__PURE__ */ jsxs("div", { style: panelStyle(), children: [
|
|
1368
|
-
/* @__PURE__ */ jsx(SectionLabel, { children: "\xA7 what this is" }),
|
|
1369
|
-
/* @__PURE__ */ jsx("p", { style: bodyStyle, children: "These are the ways you can sign in to this account. An app you sign in to sees one only if you grant it. Linking a second one is also your way back in: lose one, and the other still signs you in." })
|
|
1370
|
-
] }),
|
|
1371
|
-
mergedFrom.length > 0 && /* @__PURE__ */ jsx(ConsolidationPanel, { entries: mergedFrom }),
|
|
1372
|
-
/* @__PURE__ */ jsxs("div", { style: panelStyle("plain"), "data-oc-li-rows": "", children: [
|
|
1373
|
-
/* @__PURE__ */ jsx(
|
|
1374
|
-
IdentityGroup,
|
|
1375
|
-
{
|
|
1376
|
-
kind: "email",
|
|
1377
|
-
title: "email",
|
|
1378
|
-
rows: emailRows,
|
|
1379
|
-
authOrigin,
|
|
1380
|
-
onChange: afterChange,
|
|
1381
|
-
addCta: hasEmail ? null : "link an email",
|
|
1382
|
-
onAdd: () => {
|
|
1383
|
-
setShowEmailLink((v) => !v);
|
|
1384
|
-
setShowBtcLink(false);
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
),
|
|
1388
|
-
/* @__PURE__ */ jsx("div", { style: { borderTop: `1px solid ${V.border}` } }),
|
|
1389
|
-
/* @__PURE__ */ jsx(
|
|
1390
|
-
IdentityGroup,
|
|
1391
|
-
{
|
|
1392
|
-
kind: "btc",
|
|
1393
|
-
title: "bitcoin address",
|
|
1394
|
-
rows: btcRows,
|
|
1395
|
-
authOrigin,
|
|
1396
|
-
onChange: afterChange,
|
|
1397
|
-
addCta: hasBtc ? null : "link a Bitcoin address",
|
|
1398
|
-
onAdd: () => {
|
|
1399
|
-
setShowBtcLink((v) => !v);
|
|
1400
|
-
setShowEmailLink(false);
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
)
|
|
1404
|
-
] }),
|
|
1405
|
-
showEmailLink && /* @__PURE__ */ jsx(
|
|
1406
|
-
EmailLinkForm,
|
|
1407
|
-
{
|
|
1408
|
-
authOrigin,
|
|
1409
|
-
onDone: afterChange,
|
|
1410
|
-
onCancel: () => setShowEmailLink(false)
|
|
1411
|
-
}
|
|
1412
|
-
),
|
|
1413
|
-
showBtcLink && /* @__PURE__ */ jsx(
|
|
1414
|
-
BtcLinkForm,
|
|
1415
|
-
{
|
|
1416
|
-
authOrigin,
|
|
1417
|
-
didOc: resp.did_oc ?? null,
|
|
1418
|
-
onDone: afterChange,
|
|
1419
|
-
onCancel: () => setShowBtcLink(false)
|
|
1420
|
-
}
|
|
1421
|
-
),
|
|
1422
|
-
hasBtc && attestationLink && /* @__PURE__ */ jsx(AttestationLink, {})
|
|
1423
|
-
] });
|
|
1424
|
-
}
|
|
1425
|
-
function IdentityGroup({
|
|
1426
|
-
kind,
|
|
1427
|
-
title,
|
|
1428
|
-
rows,
|
|
1429
|
-
authOrigin,
|
|
1430
|
-
onChange,
|
|
1431
|
-
addCta,
|
|
1432
|
-
onAdd
|
|
1433
|
-
}) {
|
|
1434
|
-
if (rows.length === 0) {
|
|
1435
|
-
return /* @__PURE__ */ jsxs("div", { style: rowStyle, children: [
|
|
1436
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
1437
|
-
/* @__PURE__ */ jsxs("div", { style: rowLabelStyle, children: [
|
|
1438
|
-
"\xA7 ",
|
|
1439
|
-
title
|
|
1440
|
-
] }),
|
|
1441
|
-
/* @__PURE__ */ jsx("div", { style: { ...bodyStyle, marginTop: 4 }, children: "none linked" })
|
|
1442
|
-
] }),
|
|
1443
|
-
addCta && /* @__PURE__ */ jsxs("button", { type: "button", onClick: onAdd, style: primaryChipStyle, children: [
|
|
1444
|
-
"+ ",
|
|
1445
|
-
addCta
|
|
1446
|
-
] })
|
|
1447
|
-
] });
|
|
1448
|
-
}
|
|
1449
|
-
return /* @__PURE__ */ jsx(Fragment, { children: rows.map((r) => /* @__PURE__ */ jsx(
|
|
1450
|
-
IdentityRow,
|
|
1451
|
-
{
|
|
1452
|
-
row: r,
|
|
1453
|
-
kind,
|
|
1454
|
-
authOrigin,
|
|
1455
|
-
onChange
|
|
1456
|
-
},
|
|
1457
|
-
r.id
|
|
1458
|
-
)) });
|
|
1459
|
-
}
|
|
1460
|
-
function IdentityRow({
|
|
1461
|
-
row,
|
|
1462
|
-
kind,
|
|
1463
|
-
authOrigin,
|
|
1464
|
-
onChange
|
|
1465
|
-
}) {
|
|
1466
|
-
const [shown, setShown] = React4.useState(false);
|
|
1467
|
-
const [confirming, setConfirming] = React4.useState(false);
|
|
1468
|
-
const [busy, setBusy] = React4.useState(false);
|
|
1469
|
-
const [err, setErr] = React4.useState(null);
|
|
1470
|
-
const masked = row.value ? mask(row.value, kind) : "\u2014 pending recovery \u2014";
|
|
1471
|
-
const display = shown && row.value ? row.value : masked;
|
|
1472
|
-
async function unlink() {
|
|
1473
|
-
if (row.is_primary) return;
|
|
1474
|
-
setBusy(true);
|
|
1475
|
-
setErr(null);
|
|
1476
|
-
try {
|
|
1477
|
-
const r = await fetch(
|
|
1478
|
-
`${authOrigin}/api/auth/identities?id=${encodeURIComponent(row.id)}`,
|
|
1479
|
-
{ method: "DELETE", credentials: "include" }
|
|
1480
|
-
);
|
|
1481
|
-
if (!r.ok) {
|
|
1482
|
-
const j = await r.json().catch(() => ({}));
|
|
1483
|
-
throw new Error(j.reason ?? `http_${r.status}`);
|
|
1484
|
-
}
|
|
1485
|
-
onChange();
|
|
1486
|
-
} catch (e) {
|
|
1487
|
-
setErr(e instanceof Error ? e.message : "unlink failed");
|
|
1488
|
-
setBusy(false);
|
|
1489
|
-
setConfirming(false);
|
|
1490
|
-
}
|
|
1491
|
-
}
|
|
1492
|
-
return /* @__PURE__ */ jsxs("div", { style: rowStyle, "data-oc-li-row": kind, children: [
|
|
1493
|
-
/* @__PURE__ */ jsxs("div", { style: { minWidth: 0, flex: 1 }, children: [
|
|
1494
|
-
/* @__PURE__ */ jsxs("div", { style: { display: "flex", flexWrap: "wrap", gap: "4px 12px" }, children: [
|
|
1495
|
-
/* @__PURE__ */ jsxs("span", { style: rowLabelStyle, children: [
|
|
1496
|
-
"\xA7 ",
|
|
1497
|
-
kind === "email" ? "email" : "bitcoin address"
|
|
1498
|
-
] }),
|
|
1499
|
-
row.is_primary && /* @__PURE__ */ jsx("span", { style: { ...rowLabelStyle, color: V.primary }, children: "primary \xB7 signup-time" })
|
|
1500
|
-
] }),
|
|
1501
|
-
/* @__PURE__ */ jsx(
|
|
1502
|
-
"div",
|
|
1503
|
-
{
|
|
1504
|
-
style: {
|
|
1505
|
-
marginTop: 4,
|
|
1506
|
-
color: V.foreground,
|
|
1507
|
-
fontFamily: SIGNIN_FONT,
|
|
1508
|
-
fontSize: signinSize(12),
|
|
1509
|
-
wordBreak: "break-all"
|
|
1510
|
-
},
|
|
1511
|
-
children: display
|
|
1512
|
-
}
|
|
1513
|
-
),
|
|
1514
|
-
row.verified_at && /* @__PURE__ */ jsxs("div", { style: { ...hintStyle, marginTop: 4 }, children: [
|
|
1515
|
-
"verified \xB7 ",
|
|
1516
|
-
new Date(row.verified_at).toUTCString()
|
|
1517
|
-
] }),
|
|
1518
|
-
err && /* @__PURE__ */ jsx(ErrorLine, { children: err })
|
|
1519
|
-
] }),
|
|
1520
|
-
/* @__PURE__ */ jsxs("div", { style: { display: "flex", flexShrink: 0, gap: 8, flexWrap: "wrap" }, children: [
|
|
1521
|
-
row.value && /* @__PURE__ */ jsx(
|
|
1522
|
-
"button",
|
|
1523
|
-
{
|
|
1524
|
-
type: "button",
|
|
1525
|
-
onClick: () => setShown((s) => !s),
|
|
1526
|
-
style: ghostBtnStyle(false),
|
|
1527
|
-
children: shown ? "hide" : "show"
|
|
1528
|
-
}
|
|
1529
|
-
),
|
|
1530
|
-
!row.is_primary && !confirming && /* @__PURE__ */ jsx(
|
|
1531
|
-
"button",
|
|
1532
|
-
{
|
|
1533
|
-
type: "button",
|
|
1534
|
-
onClick: () => setConfirming(true),
|
|
1535
|
-
style: dangerBtnStyle(false),
|
|
1536
|
-
children: "unlink"
|
|
1537
|
-
}
|
|
1538
|
-
),
|
|
1539
|
-
!row.is_primary && confirming && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1540
|
-
/* @__PURE__ */ jsx(
|
|
1541
|
-
"button",
|
|
1542
|
-
{
|
|
1543
|
-
type: "button",
|
|
1544
|
-
onClick: () => void unlink(),
|
|
1545
|
-
disabled: busy,
|
|
1546
|
-
style: dangerBtnStyle(busy),
|
|
1547
|
-
children: busy ? "unlinking\u2026" : "confirm unlink"
|
|
1548
|
-
}
|
|
1549
|
-
),
|
|
1550
|
-
/* @__PURE__ */ jsx(
|
|
1551
|
-
"button",
|
|
1552
|
-
{
|
|
1553
|
-
type: "button",
|
|
1554
|
-
onClick: () => setConfirming(false),
|
|
1555
|
-
disabled: busy,
|
|
1556
|
-
style: ghostBtnStyle(busy),
|
|
1557
|
-
children: "cancel"
|
|
1558
|
-
}
|
|
1559
|
-
)
|
|
1560
|
-
] })
|
|
1561
|
-
] })
|
|
1562
|
-
] });
|
|
1563
|
-
}
|
|
1564
|
-
function ConsolidationPanel({ entries }) {
|
|
1565
|
-
return /* @__PURE__ */ jsxs("div", { style: panelStyle("success"), "data-oc-li-consolidated": "", children: [
|
|
1566
|
-
/* @__PURE__ */ jsxs(SectionLabel, { tone: "success", children: [
|
|
1567
|
-
"\xA7 consolidated accounts \xB7 ",
|
|
1568
|
-
entries.length
|
|
1569
|
-
] }),
|
|
1570
|
-
/* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
|
|
1571
|
-
entries.length === 1 ? "A previous OC account" : "Previous OC accounts",
|
|
1572
|
-
" you signed up with, folded into this one under dual proof of control. Historical activity is unioned into this account automatically;",
|
|
1573
|
-
" ",
|
|
1574
|
-
entries.length === 1 ? "it" : "they",
|
|
1575
|
-
" can no longer sign in."
|
|
1576
|
-
] }),
|
|
1577
|
-
/* @__PURE__ */ jsx("ul", { style: { listStyle: "none", margin: "12px 0 0", padding: 0 }, children: entries.map((e) => /* @__PURE__ */ jsxs(
|
|
1578
|
-
"li",
|
|
1579
|
-
{
|
|
1580
|
-
style: {
|
|
1581
|
-
display: "flex",
|
|
1582
|
-
flexWrap: "wrap",
|
|
1583
|
-
gap: 8,
|
|
1584
|
-
padding: "8px 0",
|
|
1585
|
-
borderTop: `1px solid ${V.border}`,
|
|
1586
|
-
fontFamily: SIGNIN_FONT,
|
|
1587
|
-
fontSize: signinSize(11)
|
|
1588
|
-
},
|
|
1589
|
-
children: [
|
|
1590
|
-
/* @__PURE__ */ jsx("code", { style: { color: V.foreground, wordBreak: "break-all" }, children: e.did_oc }),
|
|
1591
|
-
/* @__PURE__ */ jsxs("span", { style: { ...hintStyle, marginLeft: "auto" }, children: [
|
|
1592
|
-
"created ",
|
|
1593
|
-
new Date(e.created_at).toLocaleDateString()
|
|
1594
|
-
] })
|
|
1595
|
-
]
|
|
1596
|
-
},
|
|
1597
|
-
e.did_oc
|
|
1598
|
-
)) })
|
|
1599
|
-
] });
|
|
1600
|
-
}
|
|
1601
|
-
function AttestationLink() {
|
|
1602
|
-
return /* @__PURE__ */ jsxs("div", { style: panelStyle(), "data-oc-li-attest": "", children: [
|
|
1603
|
-
/* @__PURE__ */ jsx(SectionLabel, { children: "\xA7 portable attestation" }),
|
|
1604
|
-
/* @__PURE__ */ jsx("p", { style: bodyStyle, children: "Make your identity links portable: publish a standard OrangeCheck attestation \u2014 one Bitcoin-wallet signature (BIP-322) binding your address to your handles (Nostr, GitHub, \u2026). It is published to Nostr and verifies with zero trust in OrangeCheck, so it outlives any single service." }),
|
|
1605
|
-
/* @__PURE__ */ jsx(
|
|
1606
|
-
"a",
|
|
1607
|
-
{
|
|
1608
|
-
href: "https://attest.ochk.io/create",
|
|
1609
|
-
target: "_blank",
|
|
1610
|
-
rel: "noreferrer",
|
|
1611
|
-
style: { ...primaryChipStyle, textDecoration: "none", display: "inline-block" },
|
|
1612
|
-
"data-oc-li-attest-link": "",
|
|
1613
|
-
children: "open the attestation builder \u2197"
|
|
1614
|
-
}
|
|
1615
|
-
)
|
|
1616
|
-
] });
|
|
1617
|
-
}
|
|
1618
|
-
var ACCOUNT_WAYS_URL = "https://ochk.app/me/account#ways";
|
|
1619
|
-
function LinkPromptStep({
|
|
1620
|
-
method,
|
|
1621
|
-
didOc,
|
|
1622
|
-
authOrigin,
|
|
1623
|
-
onResolved
|
|
1624
|
-
}) {
|
|
1625
|
-
const what = method === "btc" ? "a Bitcoin address" : "an email";
|
|
1626
|
-
return /* @__PURE__ */ jsxs("div", { "data-oc-linkprompt": method, children: [
|
|
1627
|
-
/* @__PURE__ */ jsxs("div", { style: { ...panelStyle("success"), marginBottom: 16 }, children: [
|
|
1628
|
-
/* @__PURE__ */ jsx(SectionLabel, { tone: "success", children: "\xA7 signed in" }),
|
|
1629
|
-
/* @__PURE__ */ jsxs("p", { style: { ...bodyStyle, margin: 0 }, children: [
|
|
1630
|
-
"You chose to also link ",
|
|
1631
|
-
what,
|
|
1632
|
-
". Complete it below \u2014 or skip for now; it is entirely optional and changes nothing about the sign-in you just completed."
|
|
1633
|
-
] })
|
|
1634
|
-
] }),
|
|
1635
|
-
method === "btc" ? /* @__PURE__ */ jsx(
|
|
1636
|
-
BtcLinkForm,
|
|
1637
|
-
{
|
|
1638
|
-
authOrigin,
|
|
1639
|
-
didOc,
|
|
1640
|
-
onDone: onResolved,
|
|
1641
|
-
onCancel: onResolved
|
|
1642
|
-
}
|
|
1643
|
-
) : /* @__PURE__ */ jsx(EmailLinkForm, { authOrigin, onDone: onResolved, onCancel: onResolved }),
|
|
1644
|
-
/* @__PURE__ */ jsxs(
|
|
1645
|
-
"div",
|
|
1646
|
-
{
|
|
1647
|
-
"data-oc-linkprompt-skip": "",
|
|
1648
|
-
style: { marginTop: 14, paddingTop: 14, borderTop: `1px solid ${V.border}` },
|
|
1649
|
-
children: [
|
|
1650
|
-
/* @__PURE__ */ jsx("button", { type: "button", onClick: onResolved, style: ghostBtnStyle(false), children: "skip \u2014 I'll do this later" }),
|
|
1651
|
-
/* @__PURE__ */ jsxs("p", { style: { ...hintStyle, marginTop: 8 }, children: [
|
|
1652
|
-
"Changed your mind? Skipping continues you straight on \u2014 your sign-in is unaffected. You can link ",
|
|
1653
|
-
what,
|
|
1654
|
-
" anytime later from your account page,",
|
|
1655
|
-
" ",
|
|
1656
|
-
/* @__PURE__ */ jsx(
|
|
1657
|
-
"a",
|
|
1658
|
-
{
|
|
1659
|
-
href: ACCOUNT_WAYS_URL,
|
|
1660
|
-
target: "_blank",
|
|
1661
|
-
rel: "noreferrer",
|
|
1662
|
-
style: { color: "inherit" },
|
|
1663
|
-
"data-oc-linkprompt-account": "",
|
|
1664
|
-
children: "ochk.app/me/account"
|
|
1665
|
-
}
|
|
1666
|
-
),
|
|
1667
|
-
"."
|
|
1668
|
-
] })
|
|
1669
|
-
]
|
|
1670
|
-
}
|
|
1671
|
-
)
|
|
1672
|
-
] });
|
|
1673
|
-
}
|
|
1674
|
-
function EmailLinkForm({
|
|
1675
|
-
authOrigin,
|
|
1676
|
-
onDone,
|
|
1677
|
-
onCancel
|
|
1678
|
-
}) {
|
|
1679
|
-
const [step, setStep] = React4.useState("email");
|
|
1680
|
-
const [email, setEmail] = React4.useState("");
|
|
1681
|
-
const [code, setCode] = React4.useState("");
|
|
1682
|
-
const [token, setToken] = React4.useState("");
|
|
1683
|
-
const [busy, setBusy] = React4.useState(false);
|
|
1684
|
-
const [err, setErr] = React4.useState(null);
|
|
1685
|
-
const [transfer, setTransfer] = React4.useState(false);
|
|
1686
|
-
async function start() {
|
|
1687
|
-
setBusy(true);
|
|
1688
|
-
setErr(null);
|
|
1689
|
-
try {
|
|
1690
|
-
const r = await fetch(`${authOrigin}/api/auth/email-otp/start`, {
|
|
1691
|
-
method: "POST",
|
|
1692
|
-
credentials: "include",
|
|
1693
|
-
headers: { "Content-Type": "application/json" },
|
|
1694
|
-
body: JSON.stringify({ email: email.trim().toLowerCase() })
|
|
1695
|
-
});
|
|
1696
|
-
const j = await r.json();
|
|
1697
|
-
if (!r.ok || !j.token) throw new Error(j.reason ?? `http_${r.status}`);
|
|
1698
|
-
setToken(j.token);
|
|
1699
|
-
setStep("code");
|
|
1700
|
-
} catch (e) {
|
|
1701
|
-
setErr(e instanceof Error ? e.message : "could not send code");
|
|
1702
|
-
} finally {
|
|
1703
|
-
setBusy(false);
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
async function verify(confirmTransfer) {
|
|
1707
|
-
setBusy(true);
|
|
1708
|
-
setErr(null);
|
|
1709
|
-
try {
|
|
1710
|
-
const r = await fetch(`${authOrigin}/api/auth/link/email`, {
|
|
1711
|
-
method: "POST",
|
|
1712
|
-
credentials: "include",
|
|
1713
|
-
headers: { "Content-Type": "application/json" },
|
|
1714
|
-
body: JSON.stringify({
|
|
1715
|
-
email: email.trim().toLowerCase(),
|
|
1716
|
-
code: code.trim(),
|
|
1717
|
-
token,
|
|
1718
|
-
...confirmTransfer ? { confirm_transfer: true } : {}
|
|
1719
|
-
})
|
|
1720
|
-
});
|
|
1721
|
-
const j = await r.json();
|
|
1722
|
-
if (r.status === 409 && j.reason === "email_linked_elsewhere") {
|
|
1723
|
-
setTransfer(true);
|
|
1724
|
-
setBusy(false);
|
|
1725
|
-
return;
|
|
1726
|
-
}
|
|
1727
|
-
if (handleSudoRequired(j, { purpose: "link an email" })) return;
|
|
1728
|
-
if (isSudoAccountMismatch(j)) {
|
|
1729
|
-
setErr(SUDO_ACCOUNT_MISMATCH_MESSAGE);
|
|
1730
|
-
setBusy(false);
|
|
1731
|
-
return;
|
|
1732
|
-
}
|
|
1733
|
-
if (!r.ok || !j.ok) throw new Error(j.reason ?? `http_${r.status}`);
|
|
1734
|
-
onDone();
|
|
1735
|
-
} catch (e) {
|
|
1736
|
-
setErr(e instanceof Error ? e.message : "link failed");
|
|
1737
|
-
setBusy(false);
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
return /* @__PURE__ */ jsxs("div", { style: panelStyle("accent"), "data-oc-li-form": "email", children: [
|
|
1741
|
-
/* @__PURE__ */ jsx(SectionLabel, { children: "\xA7 link an email" }),
|
|
1742
|
-
step === "email" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1743
|
-
/* @__PURE__ */ jsx(
|
|
1744
|
-
"input",
|
|
1745
|
-
{
|
|
1746
|
-
type: "email",
|
|
1747
|
-
autoComplete: "email",
|
|
1748
|
-
value: email,
|
|
1749
|
-
onChange: (e) => setEmail(e.target.value),
|
|
1750
|
-
placeholder: "you@example.com",
|
|
1751
|
-
style: inputStyle
|
|
1752
|
-
}
|
|
1753
|
-
),
|
|
1754
|
-
err && /* @__PURE__ */ jsx(ErrorLine, { children: err }),
|
|
1755
|
-
/* @__PURE__ */ jsxs(FormButtons, { children: [
|
|
1756
|
-
/* @__PURE__ */ jsx(
|
|
1757
|
-
"button",
|
|
1758
|
-
{
|
|
1759
|
-
type: "button",
|
|
1760
|
-
onClick: () => void start(),
|
|
1761
|
-
disabled: busy || email.trim().length < 5,
|
|
1762
|
-
style: primaryBtnStyle(busy || email.trim().length < 5),
|
|
1763
|
-
children: busy ? "sending\u2026" : "send code"
|
|
1764
|
-
}
|
|
1765
|
-
),
|
|
1766
|
-
/* @__PURE__ */ jsx("button", { type: "button", onClick: onCancel, style: ghostBtnStyle(busy), children: "cancel" })
|
|
1767
|
-
] })
|
|
1768
|
-
] }) : transfer ? /* @__PURE__ */ jsx(
|
|
1769
|
-
TransferConfirm,
|
|
1770
|
-
{
|
|
1771
|
-
what: "email",
|
|
1772
|
-
busy,
|
|
1773
|
-
onConfirm: () => void verify(true),
|
|
1774
|
-
onCancel,
|
|
1775
|
-
err
|
|
1776
|
-
}
|
|
1777
|
-
) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1778
|
-
/* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
|
|
1779
|
-
"Enter the 6-digit code sent to ",
|
|
1780
|
-
email,
|
|
1781
|
-
"."
|
|
1782
|
-
] }),
|
|
1783
|
-
/* @__PURE__ */ jsx(
|
|
1784
|
-
"input",
|
|
1785
|
-
{
|
|
1786
|
-
type: "text",
|
|
1787
|
-
inputMode: "numeric",
|
|
1788
|
-
autoComplete: "one-time-code",
|
|
1789
|
-
maxLength: 6,
|
|
1790
|
-
value: code,
|
|
1791
|
-
onChange: (e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6)),
|
|
1792
|
-
placeholder: "123456",
|
|
1793
|
-
style: { ...inputStyle, letterSpacing: "0.3em" }
|
|
1794
|
-
}
|
|
1795
|
-
),
|
|
1796
|
-
err && /* @__PURE__ */ jsx(ErrorLine, { children: err }),
|
|
1797
|
-
/* @__PURE__ */ jsxs(FormButtons, { children: [
|
|
1798
|
-
/* @__PURE__ */ jsx(
|
|
1799
|
-
"button",
|
|
1800
|
-
{
|
|
1801
|
-
type: "button",
|
|
1802
|
-
onClick: () => void verify(false),
|
|
1803
|
-
disabled: busy || code.length !== 6,
|
|
1804
|
-
style: primaryBtnStyle(busy || code.length !== 6),
|
|
1805
|
-
children: busy ? "linking\u2026" : "verify + link"
|
|
1806
|
-
}
|
|
1807
|
-
),
|
|
1808
|
-
/* @__PURE__ */ jsx(
|
|
1809
|
-
"button",
|
|
1810
|
-
{
|
|
1811
|
-
type: "button",
|
|
1812
|
-
onClick: () => setStep("email"),
|
|
1813
|
-
disabled: busy,
|
|
1814
|
-
style: ghostBtnStyle(busy),
|
|
1815
|
-
children: "back"
|
|
1816
|
-
}
|
|
1817
|
-
)
|
|
1818
|
-
] })
|
|
1819
|
-
] })
|
|
1820
|
-
] });
|
|
1821
|
-
}
|
|
1822
|
-
function BtcLinkForm({
|
|
1823
|
-
authOrigin,
|
|
1824
|
-
didOc,
|
|
1825
|
-
onDone,
|
|
1826
|
-
onCancel
|
|
1827
|
-
}) {
|
|
1828
|
-
const [step, setStep] = React4.useState("request");
|
|
1829
|
-
const [addr, setAddr] = React4.useState("");
|
|
1830
|
-
const [challenge, setChallenge] = React4.useState("");
|
|
1831
|
-
const [busy, setBusy] = React4.useState(false);
|
|
1832
|
-
const [err, setErr] = React4.useState(null);
|
|
1833
|
-
const [pendingSig, setPendingSig] = React4.useState(null);
|
|
1834
|
-
async function getChallenge() {
|
|
1835
|
-
if (!didOc) {
|
|
1836
|
-
setErr("session not loaded yet \xB7 refresh and retry");
|
|
1837
|
-
return;
|
|
1838
|
-
}
|
|
1839
|
-
setBusy(true);
|
|
1840
|
-
setErr(null);
|
|
1841
|
-
try {
|
|
1842
|
-
const r = await fetch(
|
|
1843
|
-
`${authOrigin}/api/challenge?addr=${encodeURIComponent(addr)}&purpose=link&audience=${encodeURIComponent(didOc)}`
|
|
1844
|
-
);
|
|
1845
|
-
const j = await r.json();
|
|
1846
|
-
if (!r.ok || !j.message) throw new Error(j.reason ?? `http_${r.status}`);
|
|
1847
|
-
setChallenge(j.message);
|
|
1848
|
-
setStep("sign");
|
|
1849
|
-
} catch (e) {
|
|
1850
|
-
setErr(e instanceof Error ? e.message : "challenge failed");
|
|
1851
|
-
} finally {
|
|
1852
|
-
setBusy(false);
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
async function postLink(sig, confirmTransfer) {
|
|
1856
|
-
setBusy(true);
|
|
1857
|
-
setErr(null);
|
|
1858
|
-
try {
|
|
1859
|
-
const r = await fetch(`${authOrigin}/api/auth/link/btc`, {
|
|
1860
|
-
method: "POST",
|
|
1861
|
-
credentials: "include",
|
|
1862
|
-
headers: { "Content-Type": "application/json" },
|
|
1863
|
-
body: JSON.stringify({
|
|
1864
|
-
message: challenge,
|
|
1865
|
-
signature: sig,
|
|
1866
|
-
...confirmTransfer ? { confirm_transfer: true } : {}
|
|
1867
|
-
})
|
|
1868
|
-
});
|
|
1869
|
-
const j = await r.json();
|
|
1870
|
-
if (r.status === 409 && j.reason === "address_linked_elsewhere") {
|
|
1871
|
-
setPendingSig(sig);
|
|
1872
|
-
setBusy(false);
|
|
1873
|
-
return;
|
|
1874
|
-
}
|
|
1875
|
-
if (handleSudoRequired(j, { purpose: "link a Bitcoin address" })) return;
|
|
1876
|
-
if (isSudoAccountMismatch(j)) {
|
|
1877
|
-
setErr(SUDO_ACCOUNT_MISMATCH_MESSAGE);
|
|
1878
|
-
setBusy(false);
|
|
1879
|
-
return;
|
|
1880
|
-
}
|
|
1881
|
-
if (!r.ok || !j.ok) throw new Error(j.reason ?? `http_${r.status}`);
|
|
1882
|
-
onDone();
|
|
1883
|
-
} catch (e) {
|
|
1884
|
-
setErr(e instanceof Error ? e.message : "link failed");
|
|
1885
|
-
setBusy(false);
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
return /* @__PURE__ */ jsxs("div", { style: panelStyle("accent"), "data-oc-li-form": "btc", children: [
|
|
1889
|
-
/* @__PURE__ */ jsx(SectionLabel, { children: "\xA7 link a Bitcoin address" }),
|
|
1890
|
-
step === "request" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1891
|
-
/* @__PURE__ */ jsx("p", { style: bodyStyle, children: "Enter a Bitcoin address you control. We generate a challenge and your wallet signs it (BIP-322)." }),
|
|
1892
|
-
/* @__PURE__ */ jsx(
|
|
1893
|
-
"input",
|
|
1894
|
-
{
|
|
1895
|
-
type: "text",
|
|
1896
|
-
value: addr,
|
|
1897
|
-
onChange: (e) => setAddr(e.target.value.trim()),
|
|
1898
|
-
placeholder: "bc1q\u2026",
|
|
1899
|
-
autoCapitalize: "none",
|
|
1900
|
-
spellCheck: false,
|
|
1901
|
-
style: { ...inputStyle, fontFamily: SIGNIN_FONT }
|
|
1902
|
-
}
|
|
1903
|
-
),
|
|
1904
|
-
err && /* @__PURE__ */ jsx(ErrorLine, { children: err }),
|
|
1905
|
-
/* @__PURE__ */ jsxs(FormButtons, { children: [
|
|
1906
|
-
/* @__PURE__ */ jsx(
|
|
1907
|
-
"button",
|
|
1908
|
-
{
|
|
1909
|
-
type: "button",
|
|
1910
|
-
onClick: () => void getChallenge(),
|
|
1911
|
-
disabled: busy || addr.length < 14,
|
|
1912
|
-
style: primaryBtnStyle(busy || addr.length < 14),
|
|
1913
|
-
children: busy ? "requesting\u2026" : "get challenge"
|
|
1914
|
-
}
|
|
1915
|
-
),
|
|
1916
|
-
/* @__PURE__ */ jsx("button", { type: "button", onClick: onCancel, style: ghostBtnStyle(busy), children: "cancel" })
|
|
1917
|
-
] })
|
|
1918
|
-
] }) : pendingSig ? /* @__PURE__ */ jsx(
|
|
1919
|
-
TransferConfirm,
|
|
1920
|
-
{
|
|
1921
|
-
what: "bitcoin address",
|
|
1922
|
-
busy,
|
|
1923
|
-
onConfirm: () => void postLink(pendingSig, true),
|
|
1924
|
-
onCancel,
|
|
1925
|
-
err
|
|
1926
|
-
}
|
|
1927
|
-
) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1928
|
-
/* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
|
|
1929
|
-
"Sign the challenge with the wallet that controls",
|
|
1930
|
-
" ",
|
|
1931
|
-
/* @__PURE__ */ jsx("span", { style: { fontFamily: SIGNIN_FONT, wordBreak: "break-all" }, children: addr }),
|
|
1932
|
-
"."
|
|
1933
|
-
] }),
|
|
1934
|
-
/* @__PURE__ */ jsx(
|
|
1935
|
-
LazyWalletButton,
|
|
1936
|
-
{
|
|
1937
|
-
address: addr,
|
|
1938
|
-
message: challenge,
|
|
1939
|
-
showManual: true,
|
|
1940
|
-
layout: "list",
|
|
1941
|
-
heading: null,
|
|
1942
|
-
onSigned: (sig) => void postLink(sig, false),
|
|
1943
|
-
onError: (e) => setErr(e?.message ?? "wallet rejected")
|
|
1944
|
-
}
|
|
1945
|
-
),
|
|
1946
|
-
err && /* @__PURE__ */ jsx(ErrorLine, { children: err }),
|
|
1947
|
-
/* @__PURE__ */ jsx(FormButtons, { children: /* @__PURE__ */ jsx(
|
|
1948
|
-
"button",
|
|
1949
|
-
{
|
|
1950
|
-
type: "button",
|
|
1951
|
-
onClick: () => {
|
|
1952
|
-
setStep("request");
|
|
1953
|
-
setErr(null);
|
|
1954
|
-
},
|
|
1955
|
-
disabled: busy,
|
|
1956
|
-
style: ghostBtnStyle(busy),
|
|
1957
|
-
children: "back"
|
|
1958
|
-
}
|
|
1959
|
-
) })
|
|
1960
|
-
] })
|
|
1961
|
-
] });
|
|
1962
|
-
}
|
|
1963
|
-
function TransferConfirm({
|
|
1964
|
-
what,
|
|
1965
|
-
busy,
|
|
1966
|
-
onConfirm,
|
|
1967
|
-
onCancel,
|
|
1968
|
-
err
|
|
1969
|
-
}) {
|
|
1970
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1971
|
-
/* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
|
|
1972
|
-
"This ",
|
|
1973
|
-
what,
|
|
1974
|
-
" is already linked to another OrangeCheck account. You've just proven control of it \u2014 you can transfer the link to this account under dual proof of control. The other account loses this sign-in path; its history stays verifiable. Continue?"
|
|
1975
|
-
] }),
|
|
1976
|
-
err && /* @__PURE__ */ jsx(ErrorLine, { children: err }),
|
|
1977
|
-
/* @__PURE__ */ jsxs(FormButtons, { children: [
|
|
1978
|
-
/* @__PURE__ */ jsx(
|
|
1979
|
-
"button",
|
|
1980
|
-
{
|
|
1981
|
-
type: "button",
|
|
1982
|
-
onClick: onConfirm,
|
|
1983
|
-
disabled: busy,
|
|
1984
|
-
style: dangerBtnStyle(busy),
|
|
1985
|
-
children: busy ? "transferring\u2026" : "transfer link here"
|
|
1986
|
-
}
|
|
1987
|
-
),
|
|
1988
|
-
/* @__PURE__ */ jsx("button", { type: "button", onClick: onCancel, disabled: busy, style: ghostBtnStyle(busy), children: "cancel" })
|
|
1989
|
-
] })
|
|
1990
|
-
] });
|
|
1991
|
-
}
|
|
1992
|
-
function LazyWalletButton(props) {
|
|
1993
|
-
const [Comp, setComp] = React4.useState(null);
|
|
1994
|
-
const [failed, setFailed] = React4.useState(false);
|
|
1995
|
-
React4.useEffect(() => {
|
|
1996
|
-
let alive = true;
|
|
1997
|
-
import('@orangecheck/wallet-adapter/react').then((m) => {
|
|
1998
|
-
if (alive) {
|
|
1999
|
-
setComp(
|
|
2000
|
-
() => m.OcWalletButton
|
|
2001
|
-
);
|
|
2002
|
-
}
|
|
2003
|
-
}).catch(() => {
|
|
2004
|
-
if (alive) setFailed(true);
|
|
2005
|
-
});
|
|
2006
|
-
return () => {
|
|
2007
|
-
alive = false;
|
|
2008
|
-
};
|
|
2009
|
-
}, []);
|
|
2010
|
-
if (failed) {
|
|
2011
|
-
return /* @__PURE__ */ jsx(ErrorLine, { children: "could not load wallet support \xB7 refresh to try again" });
|
|
2012
|
-
}
|
|
2013
|
-
if (!Comp) return /* @__PURE__ */ jsx("span", { style: hintStyle, children: "loading wallet options\u2026" });
|
|
2014
|
-
return /* @__PURE__ */ jsx(Comp, { ...props });
|
|
2015
|
-
}
|
|
2016
|
-
function mask(value, kind) {
|
|
2017
|
-
if (kind === "email") {
|
|
2018
|
-
const [user, host] = value.split("@");
|
|
2019
|
-
if (!user || !host) return "\u2022\u2022\u2022\u2022";
|
|
2020
|
-
const m = user.length <= 2 ? "\u2022".repeat(user.length) : user[0] + "\u2022".repeat(user.length - 2) + user.slice(-1);
|
|
2021
|
-
return `${m}@${host}`;
|
|
2022
|
-
}
|
|
2023
|
-
if (value.length <= 12) return "\u2022".repeat(value.length);
|
|
2024
|
-
return `${value.slice(0, 6)}\u2026${value.slice(-4)}`;
|
|
2025
|
-
}
|
|
2026
|
-
function SectionLabel({
|
|
2027
|
-
children,
|
|
2028
|
-
tone = "primary"
|
|
2029
|
-
}) {
|
|
2030
|
-
const color = tone === "success" ? V.success : tone === "warn" ? V.warn : V.primary;
|
|
2031
|
-
return /* @__PURE__ */ jsx(
|
|
2032
|
-
"div",
|
|
2033
|
-
{
|
|
2034
|
-
style: {
|
|
2035
|
-
color,
|
|
2036
|
-
fontFamily: SIGNIN_FONT,
|
|
2037
|
-
fontSize: signinSize(10),
|
|
2038
|
-
letterSpacing: signinTracking("0.18em"),
|
|
2039
|
-
textTransform: SIGNIN_CASE,
|
|
2040
|
-
marginBottom: 8
|
|
2041
|
-
},
|
|
2042
|
-
children
|
|
2043
|
-
}
|
|
2044
|
-
);
|
|
2045
|
-
}
|
|
2046
|
-
function ErrorLine({ children }) {
|
|
2047
|
-
return /* @__PURE__ */ jsx(
|
|
2048
|
-
"p",
|
|
2049
|
-
{
|
|
2050
|
-
role: "alert",
|
|
2051
|
-
style: {
|
|
2052
|
-
margin: "8px 0 0",
|
|
2053
|
-
color: V.destructive,
|
|
2054
|
-
fontFamily: SIGNIN_FONT,
|
|
2055
|
-
fontSize: signinSize(11)
|
|
2056
|
-
},
|
|
2057
|
-
children
|
|
2058
|
-
}
|
|
2059
|
-
);
|
|
2060
|
-
}
|
|
2061
|
-
function FormButtons({ children }) {
|
|
2062
|
-
return /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 8, marginTop: 12 }, children });
|
|
2063
|
-
}
|
|
2064
|
-
var V = {
|
|
2065
|
-
primary: "var(--primary, #f97316)",
|
|
2066
|
-
foreground: "var(--foreground, #fafafa)",
|
|
2067
|
-
muted: "var(--muted-foreground, #a1a1aa)",
|
|
2068
|
-
background: "var(--background, #0a0a0a)",
|
|
2069
|
-
card: "var(--card, #111113)",
|
|
2070
|
-
border: "var(--border, #27272a)",
|
|
2071
|
-
destructive: "var(--destructive, #ef4444)",
|
|
2072
|
-
success: "var(--success, #22c55e)",
|
|
2073
|
-
warn: "var(--warning, #eab308)"
|
|
2074
|
-
};
|
|
2075
|
-
function panelStyle(tone = "default") {
|
|
2076
|
-
const base = {
|
|
2077
|
-
background: V.card,
|
|
2078
|
-
border: `1px solid ${V.border}`,
|
|
2079
|
-
padding: "1.25rem"
|
|
2080
|
-
};
|
|
2081
|
-
if (tone === "plain") return { ...base, padding: 0 };
|
|
2082
|
-
if (tone === "accent") return { ...base, borderColor: V.primary, background: "var(--card, #111113)" };
|
|
2083
|
-
if (tone === "success") return { ...base, borderColor: V.success };
|
|
2084
|
-
if (tone === "warn") return { ...base, borderColor: V.warn };
|
|
2085
|
-
return base;
|
|
2086
|
-
}
|
|
2087
|
-
var bodyStyle = {
|
|
2088
|
-
color: V.muted,
|
|
2089
|
-
fontSize: 13,
|
|
2090
|
-
lineHeight: 1.55,
|
|
2091
|
-
margin: "0 0 12px"
|
|
2092
|
-
};
|
|
2093
|
-
var hintStyle = {
|
|
2094
|
-
color: V.muted,
|
|
2095
|
-
fontFamily: SIGNIN_FONT,
|
|
2096
|
-
fontSize: signinSize(10.5),
|
|
2097
|
-
opacity: 0.8
|
|
2098
|
-
};
|
|
2099
|
-
var rowStyle = {
|
|
2100
|
-
display: "flex",
|
|
2101
|
-
flexWrap: "wrap",
|
|
2102
|
-
alignItems: "center",
|
|
2103
|
-
justifyContent: "space-between",
|
|
2104
|
-
gap: 12,
|
|
2105
|
-
padding: "1rem"
|
|
2106
|
-
};
|
|
2107
|
-
var rowLabelStyle = {
|
|
2108
|
-
color: V.foreground,
|
|
2109
|
-
fontFamily: SIGNIN_FONT,
|
|
2110
|
-
fontSize: signinSize(10.5),
|
|
2111
|
-
letterSpacing: signinTracking("0.12em"),
|
|
2112
|
-
textTransform: SIGNIN_CASE,
|
|
2113
|
-
opacity: 0.8
|
|
2114
|
-
};
|
|
2115
|
-
var inputStyle = {
|
|
2116
|
-
width: "100%",
|
|
2117
|
-
padding: "0.55rem 0.75rem",
|
|
2118
|
-
background: V.background,
|
|
2119
|
-
color: V.foreground,
|
|
2120
|
-
border: `1px solid ${V.border}`,
|
|
2121
|
-
borderRadius: signinRadius(6),
|
|
2122
|
-
fontFamily: SIGNIN_FONT,
|
|
2123
|
-
// 16px — never lower · suppresses iOS Safari focus auto-zoom.
|
|
2124
|
-
fontSize: 16,
|
|
2125
|
-
outline: "none",
|
|
2126
|
-
boxSizing: "border-box"
|
|
2127
|
-
};
|
|
2128
|
-
function btnBase(disabled) {
|
|
2129
|
-
return {
|
|
2130
|
-
padding: "0.5rem 0.875rem",
|
|
2131
|
-
borderRadius: SIGNIN_BUTTON_RADIUS,
|
|
2132
|
-
fontFamily: SIGNIN_FONT,
|
|
2133
|
-
fontSize: signinSize(10.5),
|
|
2134
|
-
letterSpacing: signinTracking("0.12em"),
|
|
2135
|
-
textTransform: SIGNIN_CASE,
|
|
2136
|
-
cursor: disabled ? "not-allowed" : "pointer",
|
|
2137
|
-
opacity: disabled ? 0.5 : 1
|
|
2138
|
-
};
|
|
2139
|
-
}
|
|
2140
|
-
function primaryBtnStyle(disabled) {
|
|
2141
|
-
return {
|
|
2142
|
-
...btnBase(disabled),
|
|
2143
|
-
background: V.primary,
|
|
2144
|
-
color: "var(--primary-foreground, #0a0a0a)",
|
|
2145
|
-
border: `1px solid ${V.primary}`,
|
|
2146
|
-
fontWeight: 600
|
|
2147
|
-
};
|
|
2148
|
-
}
|
|
2149
|
-
function ghostBtnStyle(disabled) {
|
|
2150
|
-
return {
|
|
2151
|
-
...btnBase(disabled),
|
|
2152
|
-
background: V.background,
|
|
2153
|
-
color: V.foreground,
|
|
2154
|
-
border: `1px solid ${V.border}`
|
|
2155
|
-
};
|
|
2156
|
-
}
|
|
2157
|
-
function dangerBtnStyle(disabled) {
|
|
2158
|
-
return {
|
|
2159
|
-
...btnBase(disabled),
|
|
2160
|
-
background: "transparent",
|
|
2161
|
-
color: V.destructive,
|
|
2162
|
-
border: `1px solid ${V.destructive}`
|
|
2163
|
-
};
|
|
2164
|
-
}
|
|
2165
|
-
var primaryChipStyle = {
|
|
2166
|
-
...btnBase(false),
|
|
2167
|
-
background: "transparent",
|
|
2168
|
-
color: V.primary,
|
|
2169
|
-
border: `1px solid ${V.primary}`
|
|
2170
|
-
};
|
|
2171
|
-
function isSameOriginPath(candidate, origin) {
|
|
2172
|
-
try {
|
|
2173
|
-
return new URL(candidate, origin).origin === origin;
|
|
2174
|
-
} catch {
|
|
2175
|
-
return false;
|
|
2176
|
-
}
|
|
2177
|
-
}
|
|
2178
|
-
function resolutionOrigin() {
|
|
2179
|
-
return typeof window === "undefined" ? "https://ochk.io" : window.location.origin;
|
|
2180
|
-
}
|
|
2181
|
-
function safeReturnTo(input) {
|
|
2182
|
-
const candidate = input ?? "/";
|
|
2183
|
-
if (typeof candidate !== "string") return "/";
|
|
2184
|
-
if (!candidate.startsWith("/")) return "/";
|
|
2185
|
-
return isSameOriginPath(candidate, resolutionOrigin()) ? candidate : "/";
|
|
2186
|
-
}
|
|
2187
|
-
function familyReturnTarget(input) {
|
|
2188
|
-
if (typeof input !== "string" || input.length === 0) return void 0;
|
|
2189
|
-
if (input.startsWith("/") && isSameOriginPath(input, resolutionOrigin())) {
|
|
2190
|
-
return input;
|
|
2191
|
-
}
|
|
2192
|
-
try {
|
|
2193
|
-
const u = new URL(input);
|
|
2194
|
-
if (u.protocol !== "https:") return void 0;
|
|
2195
|
-
const host = u.hostname.toLowerCase();
|
|
2196
|
-
if (host === "ochk.io" || host.endsWith(".ochk.io")) return u.toString();
|
|
2197
|
-
} catch {
|
|
2198
|
-
}
|
|
2199
|
-
return void 0;
|
|
2200
|
-
}
|
|
2201
|
-
function hardNavigate(target) {
|
|
2202
|
-
if (typeof window === "undefined") return;
|
|
2203
|
-
window.location.assign(target);
|
|
2204
|
-
}
|
|
2205
|
-
var SIGNIN_ERRORS = {
|
|
2206
|
-
bad_request: "That doesn't look like a Bitcoin address.",
|
|
2207
|
-
invalid_challenge: "That signature doesn't match this address. Sign with the wallet that holds it.",
|
|
2208
|
-
sig_invalid: "That signature doesn't match this address. Sign with the wallet that holds it.",
|
|
2209
|
-
sig_unsupported_scheme: "We can't check this wallet's signature format yet. Try another wallet, or use email.",
|
|
2210
|
-
expired: "That request timed out. Try again for a fresh one.",
|
|
2211
|
-
not_yet_valid: "That request is not valid yet. Check your device clock, then try again.",
|
|
2212
|
-
nonce_mismatch: "That request didn't match. Try again for a fresh one.",
|
|
2213
|
-
audience_mismatch: "That request didn't match this site. Reload the page and try again.",
|
|
2214
|
-
purpose_mismatch: "That request didn't match. Try again for a fresh one.",
|
|
2215
|
-
malformed: "That signature couldn't be read. Copy it again, whole.",
|
|
2216
|
-
cross_site_blocked: "Your browser blocked this request. Reload the page and try again.",
|
|
2217
|
-
rate_limited: "Too many tries. Wait a minute, then try again.",
|
|
2218
|
-
invalid_email: "That email address doesn't look right.",
|
|
2219
|
-
mailer_unavailable: "We couldn't send the email right now. Try again in a minute.",
|
|
2220
|
-
mailer_error: "We couldn't send the email right now. Try again in a minute.",
|
|
2221
|
-
wrong_code: "That code isn't right. Check the newest email from us.",
|
|
2222
|
-
replay: "That code was already used. Send a new one.",
|
|
2223
|
-
invalid_token: "That code is no longer valid. Send a new one.",
|
|
2224
|
-
email_mismatch: "That code was sent to a different address. Send a new one."
|
|
2225
|
-
};
|
|
2226
|
-
var EMAIL_ERRORS = {
|
|
2227
|
-
expired: "That code expired. Send a new one."
|
|
2228
|
-
};
|
|
2229
|
-
var SUBMIT_ERRORS = {
|
|
2230
|
-
bad_request: "That signature couldn't be read. Copy it again, whole."
|
|
2231
|
-
};
|
|
2232
|
-
var NETWORK_ERROR = "We couldn't reach the server. Check your connection and try again.";
|
|
2233
|
-
function humanSigninError(reason, path, step = "challenge") {
|
|
2234
|
-
if (!/^[a-z0-9_]+$/.test(reason)) return reason;
|
|
2235
|
-
if (path === "wallet" && step === "submit" && SUBMIT_ERRORS[reason]) return SUBMIT_ERRORS[reason];
|
|
2236
|
-
if (path === "email" && EMAIL_ERRORS[reason]) return EMAIL_ERRORS[reason];
|
|
2237
|
-
return SIGNIN_ERRORS[reason] ?? (path === "email" ? "That didn't work. Try again, or send a new code." : "That didn't work. Try again.");
|
|
2238
|
-
}
|
|
2239
|
-
function failureMessage(err, path, step) {
|
|
2240
|
-
if (err instanceof TypeError || err instanceof SyntaxError) return NETWORK_ERROR;
|
|
2241
|
-
return humanSigninError(err instanceof Error ? err.message : "failed", path, step);
|
|
2242
|
-
}
|
|
2243
|
-
var BECH32_ADDRESS = /^(bc1|tb1|bcrt1)[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{8,87}$/i;
|
|
2244
|
-
var BASE58_ADDRESS = /^[123mn][1-9A-HJ-NP-Za-km-z]{25,34}$/;
|
|
2245
|
-
function looksLikeBitcoinAddress(input) {
|
|
2246
|
-
const s = input.trim();
|
|
2247
|
-
return BECH32_ADDRESS.test(s) || BASE58_ADDRESS.test(s);
|
|
2248
|
-
}
|
|
2249
|
-
function OcSignIn({
|
|
2250
|
-
audience,
|
|
2251
|
-
returnTo,
|
|
2252
|
-
onSuccess,
|
|
2253
|
-
resolveReturnTo,
|
|
2254
|
-
linkPrompt: linkPromptProp = true,
|
|
2255
|
-
add: addProp,
|
|
2256
|
-
authOrigin = "https://ochk.io",
|
|
2257
|
-
initialPath,
|
|
2258
|
-
providersFirst = true,
|
|
2259
|
-
paths,
|
|
2260
|
-
className,
|
|
2261
|
-
mode,
|
|
2262
|
-
providerHref
|
|
2263
|
-
}) {
|
|
2264
|
-
const walletEnabled = paths?.wallet ?? true;
|
|
2265
|
-
const emailEnabled = paths?.email ?? true;
|
|
2266
|
-
const firstParty = mode === "first-party";
|
|
2267
|
-
const credentials = firstParty ? "omit" : "include";
|
|
2268
|
-
const linkPrompt = linkPromptProp && !firstParty;
|
|
2269
|
-
const [resolvedReturn, setResolvedReturn] = React4.useState(
|
|
2270
|
-
() => familyReturnTarget(returnTo) ?? safeReturnTo(returnTo)
|
|
2271
|
-
);
|
|
2272
|
-
React4.useEffect(() => {
|
|
2273
|
-
const fromProp = familyReturnTarget(returnTo);
|
|
2274
|
-
if (fromProp) {
|
|
2275
|
-
setResolvedReturn(fromProp);
|
|
2276
|
-
return;
|
|
2277
|
-
}
|
|
2278
|
-
if (typeof window === "undefined") return;
|
|
2279
|
-
const q = new URLSearchParams(window.location.search);
|
|
2280
|
-
const fromQuery = familyReturnTarget(q.get("return_to")) ?? familyReturnTarget(q.get("next"));
|
|
2281
|
-
setResolvedReturn(fromQuery ?? "/");
|
|
2282
|
-
}, [returnTo]);
|
|
2283
|
-
const [addMode, setAddMode] = React4.useState(Boolean(addProp) && !firstParty);
|
|
2284
|
-
React4.useEffect(() => {
|
|
2285
|
-
if (typeof window === "undefined") return;
|
|
2286
|
-
if (firstParty) {
|
|
2287
|
-
setAddMode(false);
|
|
2288
|
-
return;
|
|
2289
|
-
}
|
|
2290
|
-
if (addProp !== void 0) {
|
|
2291
|
-
setAddMode(Boolean(addProp));
|
|
2292
|
-
return;
|
|
2293
|
-
}
|
|
2294
|
-
setAddMode(new URLSearchParams(window.location.search).get("add") === "1");
|
|
2295
|
-
}, [addProp, firstParty]);
|
|
2296
|
-
const [path, setPath] = React4.useState(
|
|
2297
|
-
initialPath ?? (providersFirst ? "email" : "wallet")
|
|
2298
|
-
);
|
|
2299
|
-
const [signedIn, setSignedIn] = React4.useState(null);
|
|
2300
|
-
const [linkAlso, setLinkAlso] = React4.useState(false);
|
|
2301
|
-
const [oauthError, setOauthError] = React4.useState(false);
|
|
2302
|
-
React4.useEffect(() => {
|
|
2303
|
-
if (typeof window === "undefined") return;
|
|
2304
|
-
if (new URLSearchParams(window.location.search).has("oauth_error")) {
|
|
2305
|
-
setOauthError(true);
|
|
2306
|
-
}
|
|
2307
|
-
}, []);
|
|
2308
|
-
const navigate = React4.useCallback(
|
|
2309
|
-
async (account) => {
|
|
2310
|
-
if (resolveReturnTo) {
|
|
2311
|
-
try {
|
|
2312
|
-
const resolved = familyReturnTarget(await resolveReturnTo(account));
|
|
2313
|
-
hardNavigate(resolved ?? resolvedReturn);
|
|
2314
|
-
return;
|
|
2315
|
-
} catch {
|
|
2316
|
-
}
|
|
2317
|
-
}
|
|
2318
|
-
hardNavigate(resolvedReturn);
|
|
2319
|
-
},
|
|
2320
|
-
[resolveReturnTo, resolvedReturn]
|
|
2321
|
-
);
|
|
2322
|
-
const handleSuccess = React4.useCallback(
|
|
2323
|
-
async (account, token, via) => {
|
|
2324
|
-
const proceed = () => {
|
|
2325
|
-
clearTabSession();
|
|
2326
|
-
if (onSuccess) onSuccess(account, token);
|
|
2327
|
-
else void navigate(account);
|
|
2328
|
-
};
|
|
2329
|
-
if (!linkPrompt || !linkAlso) {
|
|
2330
|
-
proceed();
|
|
2331
|
-
return;
|
|
2332
|
-
}
|
|
2333
|
-
try {
|
|
2334
|
-
const meRes = await fetch(`${authOrigin}/api/auth/me`, {
|
|
2335
|
-
credentials: "include",
|
|
2336
|
-
headers: { Accept: "application/json" }
|
|
2337
|
-
});
|
|
2338
|
-
const me = meRes.ok ? await meRes.json() : null;
|
|
2339
|
-
const didOc = me?.account?.did_oc;
|
|
2340
|
-
if (didOc) {
|
|
2341
|
-
setSignedIn({
|
|
2342
|
-
method: via === "email" ? "btc" : "email",
|
|
2343
|
-
didOc,
|
|
2344
|
-
proceed
|
|
2345
|
-
});
|
|
2346
|
-
return;
|
|
2347
|
-
}
|
|
2348
|
-
} catch {
|
|
2349
|
-
}
|
|
2350
|
-
proceed();
|
|
2351
|
-
},
|
|
2352
|
-
[onSuccess, linkPrompt, linkAlso, navigate, authOrigin]
|
|
2353
|
-
);
|
|
2354
|
-
if (!walletEnabled && !emailEnabled) {
|
|
2355
|
-
return /* @__PURE__ */ jsxs(
|
|
2356
|
-
"div",
|
|
2357
|
-
{
|
|
2358
|
-
className,
|
|
2359
|
-
"data-oc-signin": "",
|
|
2360
|
-
style: {
|
|
2361
|
-
padding: "1rem",
|
|
2362
|
-
border: "1px solid var(--border, #27272a)",
|
|
2363
|
-
color: "var(--foreground, #fafafa)",
|
|
2364
|
-
fontFamily: SIGNIN_FONT,
|
|
2365
|
-
fontSize: signinSize(12)
|
|
2366
|
-
},
|
|
2367
|
-
children: [
|
|
2368
|
-
"no signin paths enabled \u2014 set ",
|
|
2369
|
-
/* @__PURE__ */ jsx("code", { children: "paths.wallet" }),
|
|
2370
|
-
" or",
|
|
2371
|
-
" ",
|
|
2372
|
-
/* @__PURE__ */ jsx("code", { children: "paths.email" }),
|
|
2373
|
-
" to ",
|
|
2374
|
-
/* @__PURE__ */ jsx("code", { children: "true" })
|
|
2375
|
-
]
|
|
2376
|
-
}
|
|
2377
|
-
);
|
|
2378
|
-
}
|
|
2379
|
-
if (signedIn) {
|
|
2380
|
-
return /* @__PURE__ */ jsx("div", { className, "data-oc-signin": "", children: /* @__PURE__ */ jsx(
|
|
2381
|
-
LinkPromptStep,
|
|
2382
|
-
{
|
|
2383
|
-
method: signedIn.method,
|
|
2384
|
-
didOc: signedIn.didOc,
|
|
2385
|
-
authOrigin,
|
|
2386
|
-
onResolved: signedIn.proceed
|
|
2387
|
-
}
|
|
2388
|
-
) });
|
|
2389
|
-
}
|
|
2390
|
-
const showWallet = walletEnabled && (path === "wallet" || !emailEnabled);
|
|
2391
|
-
const showEmail = emailEnabled && (path === "email" || !walletEnabled);
|
|
2392
|
-
const bothEnabled = walletEnabled && emailEnabled;
|
|
2393
|
-
const activeMethod = !emailEnabled ? "wallet" : !walletEnabled ? "email" : path;
|
|
2394
|
-
return /* @__PURE__ */ jsxs("div", { className, "data-oc-signin": "", children: [
|
|
2395
|
-
oauthError && /* @__PURE__ */ jsx(
|
|
2396
|
-
"div",
|
|
2397
|
-
{
|
|
2398
|
-
"data-oc-signin-oauth-error": "",
|
|
2399
|
-
style: {
|
|
2400
|
-
marginBottom: 14,
|
|
2401
|
-
padding: "0.6rem 0.75rem",
|
|
2402
|
-
border: "1px solid var(--destructive, #ef4444)",
|
|
2403
|
-
borderRadius: signinRadius(6),
|
|
2404
|
-
color: "var(--destructive, #ef4444)",
|
|
2405
|
-
fontFamily: SIGNIN_FONT,
|
|
2406
|
-
fontSize: signinRoleSize("text", signinSize(11))
|
|
2407
|
-
},
|
|
2408
|
-
children: "That sign-in didn't finish. Try again, or use another way below."
|
|
2409
|
-
}
|
|
2410
|
-
),
|
|
2411
|
-
addMode && /* @__PURE__ */ jsxs(
|
|
2412
|
-
"div",
|
|
2413
|
-
{
|
|
2414
|
-
"data-oc-signin-add-mode": "",
|
|
2415
|
-
style: {
|
|
2416
|
-
marginBottom: 14,
|
|
2417
|
-
padding: "0.6rem 0.75rem",
|
|
2418
|
-
border: "1px solid var(--primary, #f97316)",
|
|
2419
|
-
borderLeftWidth: 3,
|
|
2420
|
-
borderRadius: signinRadius(4),
|
|
2421
|
-
background: "color-mix(in srgb, var(--primary, #f97316) 8%, transparent)",
|
|
2422
|
-
color: "var(--foreground, #fafafa)",
|
|
2423
|
-
fontFamily: SIGNIN_FONT,
|
|
2424
|
-
fontSize: signinRoleSize("text", signinSize(11)),
|
|
2425
|
-
lineHeight: 1.6
|
|
2426
|
-
},
|
|
2427
|
-
children: [
|
|
2428
|
-
/* @__PURE__ */ jsx(
|
|
2429
|
-
"strong",
|
|
2430
|
-
{
|
|
2431
|
-
style: {
|
|
2432
|
-
color: "var(--primary, #f97316)",
|
|
2433
|
-
letterSpacing: signinTracking("0.12em"),
|
|
2434
|
-
textTransform: SIGNIN_CASE,
|
|
2435
|
-
fontSize: signinSize(10),
|
|
2436
|
-
display: "block",
|
|
2437
|
-
marginBottom: 3
|
|
2438
|
-
},
|
|
2439
|
-
children: "\xA7 adding another account"
|
|
2440
|
-
}
|
|
2441
|
-
),
|
|
2442
|
-
"Your current OrangeCheck account stays signed in. The new account joins your browser's roster; you can switch between them anytime from the account menu."
|
|
2443
|
-
]
|
|
2444
|
-
}
|
|
2445
|
-
),
|
|
2446
|
-
providersFirst && /* @__PURE__ */ jsx(
|
|
2447
|
-
ProviderSignIn,
|
|
2448
|
-
{
|
|
2449
|
-
authOrigin,
|
|
2450
|
-
returnTo: resolvedReturn,
|
|
2451
|
-
add: addMode,
|
|
2452
|
-
credentials,
|
|
2453
|
-
firstParty,
|
|
2454
|
-
providerHref,
|
|
2455
|
-
first: true
|
|
2456
|
-
}
|
|
2457
|
-
),
|
|
2458
|
-
bothEnabled && /* @__PURE__ */ jsx(
|
|
2459
|
-
"div",
|
|
2460
|
-
{
|
|
2461
|
-
role: "tablist",
|
|
2462
|
-
"aria-label": "signin method",
|
|
2463
|
-
"data-oc-signin-tabs": "",
|
|
2464
|
-
style: {
|
|
2465
|
-
display: "flex",
|
|
2466
|
-
gap: 8,
|
|
2467
|
-
marginBottom: 16,
|
|
2468
|
-
borderBottom: "1px solid var(--border, #27272a)"
|
|
2469
|
-
},
|
|
2470
|
-
children: providersFirst ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2471
|
-
/* @__PURE__ */ jsx(
|
|
2472
|
-
SigninTab,
|
|
2473
|
-
{
|
|
2474
|
-
active: path === "email",
|
|
2475
|
-
onClick: () => setPath("email"),
|
|
2476
|
-
children: "Email"
|
|
2477
|
-
}
|
|
2478
|
-
),
|
|
2479
|
-
/* @__PURE__ */ jsx(
|
|
2480
|
-
SigninTab,
|
|
2481
|
-
{
|
|
2482
|
-
active: path === "wallet",
|
|
2483
|
-
onClick: () => setPath("wallet"),
|
|
2484
|
-
children: "Bitcoin wallet"
|
|
2485
|
-
}
|
|
2486
|
-
)
|
|
2487
|
-
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2488
|
-
/* @__PURE__ */ jsx(
|
|
2489
|
-
SigninTab,
|
|
2490
|
-
{
|
|
2491
|
-
active: path === "wallet",
|
|
2492
|
-
onClick: () => setPath("wallet"),
|
|
2493
|
-
children: "Bitcoin wallet"
|
|
2494
|
-
}
|
|
2495
|
-
),
|
|
2496
|
-
/* @__PURE__ */ jsx(
|
|
2497
|
-
SigninTab,
|
|
2498
|
-
{
|
|
2499
|
-
active: path === "email",
|
|
2500
|
-
onClick: () => setPath("email"),
|
|
2501
|
-
children: "Email"
|
|
2502
|
-
}
|
|
2503
|
-
)
|
|
2504
|
-
] })
|
|
2505
|
-
}
|
|
2506
|
-
),
|
|
2507
|
-
/* @__PURE__ */ jsxs("div", { "data-oc-signin-panel": "", role: "tabpanel", children: [
|
|
2508
|
-
showWallet && /* @__PURE__ */ jsx(
|
|
2509
|
-
WalletFlow,
|
|
2510
|
-
{
|
|
2511
|
-
authOrigin,
|
|
2512
|
-
audience,
|
|
2513
|
-
add: addMode,
|
|
2514
|
-
credentials,
|
|
2515
|
-
onSuccess: (a, t) => void handleSuccess(a, t, "wallet")
|
|
2516
|
-
}
|
|
2517
|
-
),
|
|
2518
|
-
showEmail && /* @__PURE__ */ jsx(
|
|
2519
|
-
EmailFlow,
|
|
2520
|
-
{
|
|
2521
|
-
authOrigin,
|
|
2522
|
-
add: addMode,
|
|
2523
|
-
credentials,
|
|
2524
|
-
onSuccess: (a, t) => void handleSuccess(a, t, "email")
|
|
2525
|
-
}
|
|
2526
|
-
)
|
|
2527
|
-
] }),
|
|
2528
|
-
!providersFirst && /* @__PURE__ */ jsx(
|
|
2529
|
-
ProviderSignIn,
|
|
2530
|
-
{
|
|
2531
|
-
authOrigin,
|
|
2532
|
-
returnTo: resolvedReturn,
|
|
2533
|
-
add: addMode,
|
|
2534
|
-
credentials,
|
|
2535
|
-
firstParty,
|
|
2536
|
-
providerHref
|
|
2537
|
-
}
|
|
2538
|
-
),
|
|
2539
|
-
linkPrompt && /* @__PURE__ */ jsxs("label", { "data-oc-signin-linkalso": "", style: linkAlsoStyle, children: [
|
|
2540
|
-
/* @__PURE__ */ jsx(
|
|
2541
|
-
"input",
|
|
2542
|
-
{
|
|
2543
|
-
type: "checkbox",
|
|
2544
|
-
checked: linkAlso,
|
|
2545
|
-
onChange: (e) => setLinkAlso(e.target.checked),
|
|
2546
|
-
style: { margin: 0, accentColor: "var(--primary, #f97316)" }
|
|
2547
|
-
}
|
|
2548
|
-
),
|
|
2549
|
-
/* @__PURE__ */ jsxs("span", { children: [
|
|
2550
|
-
"After signing in, also link",
|
|
2551
|
-
" ",
|
|
2552
|
-
activeMethod === "email" ? "a Bitcoin wallet" : "an email",
|
|
2553
|
-
" ",
|
|
2554
|
-
/* @__PURE__ */ jsx("span", { children: "\u2014 optional, one more signature." })
|
|
2555
|
-
] })
|
|
2556
|
-
] })
|
|
2557
|
-
] });
|
|
2558
|
-
}
|
|
2559
|
-
function SigninTab({
|
|
2560
|
-
active,
|
|
2561
|
-
onClick,
|
|
2562
|
-
children
|
|
2563
|
-
}) {
|
|
2564
|
-
return /* @__PURE__ */ jsx(
|
|
2565
|
-
"button",
|
|
2566
|
-
{
|
|
2567
|
-
type: "button",
|
|
2568
|
-
role: "tab",
|
|
2569
|
-
"aria-selected": active,
|
|
2570
|
-
onClick,
|
|
2571
|
-
"data-oc-signin-tab": active ? "active" : "inactive",
|
|
2572
|
-
style: {
|
|
2573
|
-
minHeight: 44,
|
|
2574
|
-
padding: "0.6rem 0.875rem",
|
|
2575
|
-
background: "transparent",
|
|
2576
|
-
border: "none",
|
|
2577
|
-
borderBottom: active ? "2px solid var(--primary, #f97316)" : "2px solid transparent",
|
|
2578
|
-
color: active ? "var(--foreground, #fafafa)" : "var(--muted-foreground, #a1a1aa)",
|
|
2579
|
-
fontFamily: SIGNIN_FONT,
|
|
2580
|
-
fontSize: signinRoleSize("label", signinSize(11)),
|
|
2581
|
-
letterSpacing: signinTracking("0.12em"),
|
|
2582
|
-
textTransform: SIGNIN_CASE,
|
|
2583
|
-
cursor: "pointer",
|
|
2584
|
-
marginBottom: -1
|
|
2585
|
-
},
|
|
2586
|
-
children
|
|
2587
|
-
}
|
|
2588
|
-
);
|
|
2589
|
-
}
|
|
2590
|
-
function ProviderIcon({ id }) {
|
|
2591
|
-
const common = {
|
|
2592
|
-
width: 14,
|
|
2593
|
-
height: 14,
|
|
2594
|
-
viewBox: "0 0 24 24",
|
|
2595
|
-
fill: "currentColor",
|
|
2596
|
-
"aria-hidden": true,
|
|
2597
|
-
style: { flex: "0 0 auto" }
|
|
2598
|
-
};
|
|
2599
|
-
if (id === "google") {
|
|
2600
|
-
return /* @__PURE__ */ jsx("svg", { ...common, children: /* @__PURE__ */ jsx("path", { d: "M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" }) });
|
|
2601
|
-
}
|
|
2602
|
-
if (id === "github") {
|
|
2603
|
-
return /* @__PURE__ */ jsx("svg", { ...common, children: /* @__PURE__ */ jsx("path", { d: "M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" }) });
|
|
2604
|
-
}
|
|
2605
|
-
return null;
|
|
2606
|
-
}
|
|
2607
|
-
function ProviderSignIn({
|
|
2608
|
-
authOrigin,
|
|
2609
|
-
returnTo,
|
|
2610
|
-
add,
|
|
2611
|
-
credentials,
|
|
2612
|
-
firstParty,
|
|
2613
|
-
providerHref,
|
|
2614
|
-
first = false
|
|
2615
|
-
}) {
|
|
2616
|
-
const [providers, setProviders] = React4.useState(null);
|
|
2617
|
-
const [origin, setOrigin] = React4.useState("");
|
|
2618
|
-
const hidden = firstParty && !providerHref;
|
|
2619
|
-
React4.useEffect(() => {
|
|
2620
|
-
setOrigin(window.location.origin);
|
|
2621
|
-
if (hidden) return;
|
|
2622
|
-
let cancelled = false;
|
|
2623
|
-
fetch(`${authOrigin}/api/auth/providers`, { credentials }).then((r) => r.ok ? r.json() : null).then((body) => {
|
|
2624
|
-
if (!cancelled) setProviders(body?.providers ?? []);
|
|
2625
|
-
}).catch(() => {
|
|
2626
|
-
if (!cancelled) setProviders([]);
|
|
2627
|
-
});
|
|
2628
|
-
return () => {
|
|
2629
|
-
cancelled = true;
|
|
2630
|
-
};
|
|
2631
|
-
}, [authOrigin, credentials, hidden]);
|
|
2632
|
-
if (hidden) return null;
|
|
2633
|
-
if (providers !== null && providers.length === 0) return null;
|
|
2634
|
-
const providerReturnTo = stripFragment(
|
|
2635
|
-
returnTo.startsWith("/") ? origin ? `${origin}${returnTo}` : returnTo : returnTo
|
|
2636
|
-
);
|
|
2637
|
-
const line = { flex: 1, height: 1, background: "var(--border, #27272a)" };
|
|
2638
|
-
const BLOCK_GAP = 16;
|
|
2639
|
-
const BUTTON_GAP = 8;
|
|
2640
|
-
const divider = /* @__PURE__ */ jsxs(
|
|
2641
|
-
"div",
|
|
2642
|
-
{
|
|
2643
|
-
style: {
|
|
2644
|
-
display: "flex",
|
|
2645
|
-
alignItems: "center",
|
|
2646
|
-
gap: 10,
|
|
2647
|
-
// Symmetric in both orderings: in `first` mode the divider
|
|
2648
|
-
// owns the gap on both sides; at the bottom the container's
|
|
2649
|
-
// marginTop owns the gap above, so the divider only pads below.
|
|
2650
|
-
margin: first ? `${BLOCK_GAP}px 0` : `0 0 ${BLOCK_GAP}px`,
|
|
2651
|
-
color: "var(--muted-foreground, #a1a1aa)",
|
|
2652
|
-
fontFamily: SIGNIN_FONT,
|
|
2653
|
-
fontSize: signinRoleSize("hint", signinSize(10)),
|
|
2654
|
-
letterSpacing: signinTracking("0.16em"),
|
|
2655
|
-
textTransform: SIGNIN_CASE
|
|
2656
|
-
},
|
|
2657
|
-
children: [
|
|
2658
|
-
/* @__PURE__ */ jsx("span", { style: line }),
|
|
2659
|
-
"or",
|
|
2660
|
-
/* @__PURE__ */ jsx("span", { style: line })
|
|
2661
|
-
]
|
|
2662
|
-
}
|
|
2663
|
-
);
|
|
2664
|
-
const buttonStyle = (i) => ({
|
|
2665
|
-
display: "flex",
|
|
2666
|
-
alignItems: "center",
|
|
2667
|
-
justifyContent: "center",
|
|
2668
|
-
gap: 10,
|
|
2669
|
-
boxSizing: "border-box",
|
|
2670
|
-
width: "100%",
|
|
2671
|
-
minHeight: 44,
|
|
2672
|
-
marginTop: i === 0 ? 0 : BUTTON_GAP,
|
|
2673
|
-
padding: "0.6rem 0.875rem",
|
|
2674
|
-
border: "1px solid var(--border, #27272a)",
|
|
2675
|
-
borderRadius: SIGNIN_BUTTON_RADIUS,
|
|
2676
|
-
background: "transparent",
|
|
2677
|
-
color: "var(--muted-foreground, #a1a1aa)",
|
|
2678
|
-
fontFamily: SIGNIN_FONT,
|
|
2679
|
-
fontSize: signinSize(12),
|
|
2680
|
-
textDecoration: "none"
|
|
2681
|
-
});
|
|
2682
|
-
if (providers === null) {
|
|
2683
|
-
if (!first) return null;
|
|
2684
|
-
return /* @__PURE__ */ jsxs("div", { "data-oc-signin-providers": "", "aria-hidden": "true", children: [
|
|
2685
|
-
[0, 1].map((i) => /* @__PURE__ */ jsx("div", { style: { ...buttonStyle(i), visibility: "hidden" }, children: "\xA0" }, i)),
|
|
2686
|
-
divider
|
|
2687
|
-
] });
|
|
2688
|
-
}
|
|
2689
|
-
const buttons = providers.map((p, i) => /* @__PURE__ */ jsxs(
|
|
2690
|
-
"a",
|
|
2691
|
-
{
|
|
2692
|
-
href: providerHref ? providerHref(p.id) : `${authOrigin}/api/auth/${p.id}/start?return_to=${encodeURIComponent(
|
|
2693
|
-
providerReturnTo
|
|
2694
|
-
)}${add ? "&add=1" : ""}`,
|
|
2695
|
-
"data-oc-signin-provider": p.id,
|
|
2696
|
-
style: buttonStyle(i),
|
|
2697
|
-
children: [
|
|
2698
|
-
/* @__PURE__ */ jsx(ProviderIcon, { id: p.id }),
|
|
2699
|
-
/* @__PURE__ */ jsx("span", { children: p.label })
|
|
2700
|
-
]
|
|
2701
|
-
},
|
|
2702
|
-
p.id
|
|
2703
|
-
));
|
|
2704
|
-
return /* @__PURE__ */ jsx(
|
|
2705
|
-
"div",
|
|
2706
|
-
{
|
|
2707
|
-
"data-oc-signin-providers": "",
|
|
2708
|
-
style: first ? { marginBottom: 0 } : { marginTop: BLOCK_GAP },
|
|
2709
|
-
children: first ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2710
|
-
buttons,
|
|
2711
|
-
divider
|
|
2712
|
-
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2713
|
-
divider,
|
|
2714
|
-
buttons
|
|
2715
|
-
] })
|
|
2716
|
-
}
|
|
2717
|
-
);
|
|
2718
|
-
}
|
|
2719
|
-
async function loadAdapter() {
|
|
2720
|
-
return await import('@orangecheck/wallet-adapter');
|
|
2721
|
-
}
|
|
2722
|
-
async function detectBrowserWallets() {
|
|
2723
|
-
return (await loadAdapter()).detectWallets().filter((w) => w.detected && w.id !== "manual").map((w) => ({ id: w.id, name: w.name }));
|
|
2724
|
-
}
|
|
2725
|
-
function WalletFlow({
|
|
2726
|
-
authOrigin,
|
|
2727
|
-
audience,
|
|
2728
|
-
add,
|
|
2729
|
-
credentials,
|
|
2730
|
-
onSuccess
|
|
2731
|
-
}) {
|
|
2732
|
-
const uid = React4.useId();
|
|
2733
|
-
const ids = {
|
|
2734
|
-
address: `${uid}-address`,
|
|
2735
|
-
wallet: `${uid}-wallet`,
|
|
2736
|
-
message: `${uid}-message`,
|
|
2737
|
-
signature: `${uid}-signature`
|
|
2738
|
-
};
|
|
2739
|
-
const [address, setAddress] = React4.useState("");
|
|
2740
|
-
const [error, setError] = React4.useState(null);
|
|
2741
|
-
const [submitting, setSubmitting] = React4.useState(false);
|
|
2742
|
-
const [wallets, setWallets] = React4.useState(null);
|
|
2743
|
-
const [walletId, setWalletId] = React4.useState(null);
|
|
2744
|
-
const [manual, setManual] = React4.useState(null);
|
|
2745
|
-
const [pasted, setPasted] = React4.useState("");
|
|
2746
|
-
const [copied, setCopied] = React4.useState("idle");
|
|
2747
|
-
React4.useEffect(() => {
|
|
2748
|
-
let cancelled = false;
|
|
2749
|
-
detectBrowserWallets().then((found) => {
|
|
2750
|
-
if (cancelled) return;
|
|
2751
|
-
setWallets(found);
|
|
2752
|
-
setWalletId(found[0]?.id ?? null);
|
|
2753
|
-
}).catch(() => {
|
|
2754
|
-
if (!cancelled) setWallets([]);
|
|
2755
|
-
});
|
|
2756
|
-
return () => {
|
|
2757
|
-
cancelled = true;
|
|
2758
|
-
};
|
|
2759
|
-
}, []);
|
|
2760
|
-
async function fetchChallenge(addr) {
|
|
2761
|
-
const res = await fetch(
|
|
2762
|
-
`${authOrigin}/api/challenge?addr=${encodeURIComponent(addr)}&audience=${encodeURIComponent(audience)}&purpose=login`
|
|
2763
|
-
);
|
|
2764
|
-
const json = await res.json();
|
|
2765
|
-
if (!res.ok || !json.message || !json.nonce) {
|
|
2766
|
-
throw new Error(json.error ?? "challenge_failed");
|
|
2767
|
-
}
|
|
2768
|
-
return { message: json.message, nonce: json.nonce };
|
|
2769
|
-
}
|
|
2770
|
-
async function submitSignature(message, nonce, signature, scheme) {
|
|
2771
|
-
const res = await fetch(`${authOrigin}/api/auth/signin`, {
|
|
2772
|
-
method: "POST",
|
|
2773
|
-
credentials,
|
|
2774
|
-
headers: { "Content-Type": "application/json" },
|
|
2775
|
-
body: JSON.stringify({
|
|
2776
|
-
message,
|
|
2777
|
-
signature,
|
|
2778
|
-
...scheme ? { scheme } : {},
|
|
2779
|
-
expectedNonce: nonce,
|
|
2780
|
-
expectedAudience: audience,
|
|
2781
|
-
expectedPurpose: "login",
|
|
2782
|
-
// Multi-account add-mode · the auth host preserves the current
|
|
2783
|
-
// roster_id when set, instead of minting a fresh one.
|
|
2784
|
-
...add ? { add: true } : {}
|
|
2785
|
-
})
|
|
2786
|
-
});
|
|
2787
|
-
const json = await res.json();
|
|
2788
|
-
if (!res.ok || !("ok" in json) || !json.ok || !json.account) {
|
|
2789
|
-
throw new Error(
|
|
2790
|
-
"reason" in json && json.reason || "error" in json && json.error || "verify_failed"
|
|
2791
|
-
);
|
|
2792
|
-
}
|
|
2793
|
-
onSuccess(json.account, json.token);
|
|
2794
|
-
}
|
|
2795
|
-
async function signIn(e) {
|
|
2796
|
-
e.preventDefault();
|
|
2797
|
-
const addr = address.trim();
|
|
2798
|
-
if (!addr) {
|
|
2799
|
-
setError("Paste your Bitcoin address.");
|
|
2800
|
-
return;
|
|
2801
|
-
}
|
|
2802
|
-
if (!looksLikeBitcoinAddress(addr)) {
|
|
2803
|
-
setError(humanSigninError("bad_request", "wallet"));
|
|
2804
|
-
return;
|
|
2805
|
-
}
|
|
2806
|
-
setError(null);
|
|
2807
|
-
setSubmitting(true);
|
|
2808
|
-
let step = "challenge";
|
|
2809
|
-
try {
|
|
2810
|
-
let id = walletId;
|
|
2811
|
-
if (wallets === null) {
|
|
2812
|
-
const found = await detectBrowserWallets().catch(() => []);
|
|
2813
|
-
setWallets(found);
|
|
2814
|
-
id = found[0]?.id ?? null;
|
|
2815
|
-
setWalletId(id);
|
|
2816
|
-
}
|
|
2817
|
-
const challenge = await fetchChallenge(addr);
|
|
2818
|
-
if (!id) {
|
|
2819
|
-
setManual(challenge);
|
|
2820
|
-
setSubmitting(false);
|
|
2821
|
-
return;
|
|
2822
|
-
}
|
|
2823
|
-
const signer = (await loadAdapter()).getSigner(id, { address: addr });
|
|
2824
|
-
const signature = await signer(challenge.message);
|
|
2825
|
-
step = "submit";
|
|
2826
|
-
await submitSignature(challenge.message, challenge.nonce, signature, "bip322");
|
|
2827
|
-
} catch (err) {
|
|
2828
|
-
setError(failureMessage(err, "wallet", step));
|
|
2829
|
-
setSubmitting(false);
|
|
2830
|
-
}
|
|
2831
|
-
}
|
|
2832
|
-
async function signInManual(e) {
|
|
2833
|
-
e.preventDefault();
|
|
2834
|
-
if (!manual) return;
|
|
2835
|
-
const sig = pasted.trim();
|
|
2836
|
-
if (!sig) {
|
|
2837
|
-
setError("Paste the signature your wallet gave you.");
|
|
2838
|
-
return;
|
|
2839
|
-
}
|
|
2840
|
-
setError(null);
|
|
2841
|
-
setSubmitting(true);
|
|
2842
|
-
try {
|
|
2843
|
-
await submitSignature(manual.message, manual.nonce, sig, void 0);
|
|
2844
|
-
} catch (err) {
|
|
2845
|
-
setError(failureMessage(err, "wallet", "submit"));
|
|
2846
|
-
setSubmitting(false);
|
|
2847
|
-
}
|
|
2848
|
-
}
|
|
2849
|
-
async function copyMessage() {
|
|
2850
|
-
if (!manual) return;
|
|
2851
|
-
try {
|
|
2852
|
-
await navigator.clipboard.writeText(manual.message);
|
|
2853
|
-
setCopied("copied");
|
|
2854
|
-
} catch {
|
|
2855
|
-
setCopied("failed");
|
|
2856
|
-
}
|
|
2857
|
-
}
|
|
2858
|
-
if (manual) {
|
|
2859
|
-
return /* @__PURE__ */ jsxs("form", { onSubmit: signInManual, "data-oc-signin-wallet-manual": "", children: [
|
|
2860
|
-
/* @__PURE__ */ jsxs(FlowHeader, { children: [
|
|
2861
|
-
"In your wallet, use \u201Csign message\u201D (BIP-322) with ",
|
|
2862
|
-
address.trim(),
|
|
2863
|
-
", sign the text below, and paste the signature back here. Nothing is spent."
|
|
2864
|
-
] }),
|
|
2865
|
-
/* @__PURE__ */ jsx(Label, { htmlFor: ids.message, children: "Message to sign" }),
|
|
2866
|
-
/* @__PURE__ */ jsx(
|
|
2867
|
-
"textarea",
|
|
2868
|
-
{
|
|
2869
|
-
id: ids.message,
|
|
2870
|
-
readOnly: true,
|
|
2871
|
-
value: manual.message,
|
|
2872
|
-
rows: 5,
|
|
2873
|
-
onFocus: (e) => e.currentTarget.select(),
|
|
2874
|
-
style: { ...inputStyle2, resize: "vertical" }
|
|
2875
|
-
}
|
|
2876
|
-
),
|
|
2877
|
-
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => void copyMessage(), style: linkButtonStyle, children: copied === "copied" ? "Copied" : "Copy message" }),
|
|
2878
|
-
copied === "failed" && /* @__PURE__ */ jsx(Hint, { children: "Couldn't copy here. Select the text above and copy it." }),
|
|
2879
|
-
/* @__PURE__ */ jsxs("div", { style: { marginTop: 14 }, children: [
|
|
2880
|
-
/* @__PURE__ */ jsx(Label, { htmlFor: ids.signature, children: "Signature" }),
|
|
2881
|
-
/* @__PURE__ */ jsx(
|
|
2882
|
-
"textarea",
|
|
2883
|
-
{
|
|
2884
|
-
id: ids.signature,
|
|
2885
|
-
value: pasted,
|
|
2886
|
-
onChange: (e) => {
|
|
2887
|
-
setPasted(e.target.value);
|
|
2888
|
-
if (error) setError(null);
|
|
2889
|
-
},
|
|
2890
|
-
rows: 3,
|
|
2891
|
-
placeholder: "paste the signature",
|
|
2892
|
-
spellCheck: false,
|
|
2893
|
-
autoFocus: true,
|
|
2894
|
-
"aria-invalid": error ? true : void 0,
|
|
2895
|
-
style: { ...inputStyle2, resize: "vertical" }
|
|
2896
|
-
}
|
|
2897
|
-
)
|
|
2898
|
-
] }),
|
|
2899
|
-
error && /* @__PURE__ */ jsx(ErrorLine2, { children: error }),
|
|
2900
|
-
/* @__PURE__ */ jsx(
|
|
2901
|
-
"button",
|
|
2902
|
-
{
|
|
2903
|
-
type: "submit",
|
|
2904
|
-
disabled: submitting || !pasted.trim(),
|
|
2905
|
-
style: submitStyle(submitting || !pasted.trim()),
|
|
2906
|
-
children: submitting ? "Checking\u2026" : /* @__PURE__ */ jsx(Submit, { children: "Sign me in" })
|
|
2907
|
-
}
|
|
2908
|
-
),
|
|
2909
|
-
/* @__PURE__ */ jsx(
|
|
2910
|
-
"button",
|
|
2911
|
-
{
|
|
2912
|
-
type: "button",
|
|
2913
|
-
onClick: () => {
|
|
2914
|
-
setManual(null);
|
|
2915
|
-
setPasted("");
|
|
2916
|
-
setError(null);
|
|
2917
|
-
setCopied("idle");
|
|
2918
|
-
},
|
|
2919
|
-
style: linkButtonStyle,
|
|
2920
|
-
children: "Use a different address"
|
|
2921
|
-
}
|
|
2922
|
-
)
|
|
2923
|
-
] });
|
|
2924
|
-
}
|
|
2925
|
-
const noBrowserWallet = wallets !== null && wallets.length === 0;
|
|
2926
|
-
return /* @__PURE__ */ jsxs("form", { onSubmit: signIn, "data-oc-signin-wallet": "", children: [
|
|
2927
|
-
/* @__PURE__ */ jsx(FlowHeader, { children: "Your wallet signs a short message to prove you hold the address. Nothing is spent and your key never leaves the wallet. Works with browser wallets, or any wallet that can sign a message, like Sparrow." }),
|
|
2928
|
-
/* @__PURE__ */ jsx(Label, { htmlFor: ids.address, children: "Bitcoin address" }),
|
|
2929
|
-
/* @__PURE__ */ jsx(
|
|
2930
|
-
"input",
|
|
2931
|
-
{
|
|
2932
|
-
id: ids.address,
|
|
2933
|
-
type: "text",
|
|
2934
|
-
value: address,
|
|
2935
|
-
onChange: (e) => {
|
|
2936
|
-
setAddress(e.target.value);
|
|
2937
|
-
if (error) setError(null);
|
|
2938
|
-
},
|
|
2939
|
-
placeholder: "bc1q\u2026 \xB7 3\u2026 \xB7 1\u2026",
|
|
2940
|
-
autoComplete: "off",
|
|
2941
|
-
autoCapitalize: "none",
|
|
2942
|
-
spellCheck: false,
|
|
2943
|
-
required: true,
|
|
2944
|
-
"aria-invalid": error ? true : void 0,
|
|
2945
|
-
style: inputStyle2
|
|
2946
|
-
}
|
|
2947
|
-
),
|
|
2948
|
-
wallets && wallets.length > 1 && /* @__PURE__ */ jsxs("div", { style: { marginTop: 12 }, children: [
|
|
2949
|
-
/* @__PURE__ */ jsx(Label, { htmlFor: ids.wallet, children: "Sign with" }),
|
|
2950
|
-
/* @__PURE__ */ jsx(
|
|
2951
|
-
"select",
|
|
2952
|
-
{
|
|
2953
|
-
id: ids.wallet,
|
|
2954
|
-
value: walletId ?? "",
|
|
2955
|
-
onChange: (e) => setWalletId(e.target.value),
|
|
2956
|
-
style: inputStyle2,
|
|
2957
|
-
children: wallets.map((w) => /* @__PURE__ */ jsx("option", { value: w.id, children: w.name }, w.id))
|
|
2958
|
-
}
|
|
2959
|
-
)
|
|
2960
|
-
] }),
|
|
2961
|
-
error && /* @__PURE__ */ jsx(ErrorLine2, { children: error }),
|
|
2962
|
-
/* @__PURE__ */ jsx(
|
|
2963
|
-
"button",
|
|
2964
|
-
{
|
|
2965
|
-
type: "submit",
|
|
2966
|
-
disabled: submitting || !address.trim(),
|
|
2967
|
-
style: submitStyle(submitting || !address.trim()),
|
|
2968
|
-
children: submitting ? "Waiting for wallet\u2026" : noBrowserWallet ? /* @__PURE__ */ jsx(Submit, { children: "Get the message to sign" }) : /* @__PURE__ */ jsx(Submit, { children: "Sign in with wallet" })
|
|
2969
|
-
}
|
|
2970
|
-
),
|
|
2971
|
-
noBrowserWallet && /* @__PURE__ */ jsx(Hint, { children: "No browser wallet found here. You can sign with any wallet and paste." })
|
|
2972
|
-
] });
|
|
2973
|
-
}
|
|
2974
|
-
function EmailFlow({ authOrigin, add, credentials, onSuccess }) {
|
|
2975
|
-
const uid = React4.useId();
|
|
2976
|
-
const [stage, setStage] = React4.useState("enter");
|
|
2977
|
-
const [email, setEmail] = React4.useState("");
|
|
2978
|
-
const [emailError, setEmailError] = React4.useState(null);
|
|
2979
|
-
const [code, setCode] = React4.useState("");
|
|
2980
|
-
const [codeError, setCodeError] = React4.useState(null);
|
|
2981
|
-
const [token, setToken] = React4.useState(null);
|
|
2982
|
-
const [submitting, setSubmitting] = React4.useState(false);
|
|
2983
|
-
const [resent, setResent] = React4.useState(false);
|
|
2984
|
-
async function sendCode(address) {
|
|
2985
|
-
try {
|
|
2986
|
-
const res = await fetch(`${authOrigin}/api/auth/email-otp/start`, {
|
|
2987
|
-
method: "POST",
|
|
2988
|
-
credentials,
|
|
2989
|
-
headers: { "Content-Type": "application/json" },
|
|
2990
|
-
body: JSON.stringify({ email: address })
|
|
2991
|
-
});
|
|
2992
|
-
const json = await res.json();
|
|
2993
|
-
if (!res.ok || !json.token) return humanSigninError(json.reason ?? "server_error", "email");
|
|
2994
|
-
setToken(json.token);
|
|
2995
|
-
return null;
|
|
2996
|
-
} catch {
|
|
2997
|
-
return NETWORK_ERROR;
|
|
2998
|
-
}
|
|
2999
|
-
}
|
|
3000
|
-
async function start(e) {
|
|
3001
|
-
e.preventDefault();
|
|
3002
|
-
const trimmed = email.trim().toLowerCase();
|
|
3003
|
-
if (!trimmed || !/.+@.+\..+/.test(trimmed)) {
|
|
3004
|
-
setEmailError(humanSigninError("invalid_email", "email"));
|
|
3005
|
-
return;
|
|
3006
|
-
}
|
|
3007
|
-
setEmailError(null);
|
|
3008
|
-
setSubmitting(true);
|
|
3009
|
-
const err = await sendCode(trimmed);
|
|
3010
|
-
setSubmitting(false);
|
|
3011
|
-
if (err) {
|
|
3012
|
-
setEmailError(err);
|
|
3013
|
-
return;
|
|
3014
|
-
}
|
|
3015
|
-
setEmail(trimmed);
|
|
3016
|
-
setStage("code");
|
|
3017
|
-
}
|
|
3018
|
-
async function resend() {
|
|
3019
|
-
setCodeError(null);
|
|
3020
|
-
setResent(false);
|
|
3021
|
-
setSubmitting(true);
|
|
3022
|
-
const err = await sendCode(email);
|
|
3023
|
-
setSubmitting(false);
|
|
3024
|
-
setCode("");
|
|
3025
|
-
if (err) setCodeError(err);
|
|
3026
|
-
else setResent(true);
|
|
3027
|
-
}
|
|
3028
|
-
async function verify(e) {
|
|
3029
|
-
e.preventDefault();
|
|
3030
|
-
if (!token) return;
|
|
3031
|
-
if (code.length !== 6) {
|
|
3032
|
-
setCodeError("The code is 6 digits.");
|
|
3033
|
-
return;
|
|
3034
|
-
}
|
|
3035
|
-
setCodeError(null);
|
|
3036
|
-
setSubmitting(true);
|
|
3037
|
-
try {
|
|
3038
|
-
const res = await fetch(`${authOrigin}/api/auth/email-otp/verify`, {
|
|
3039
|
-
method: "POST",
|
|
3040
|
-
credentials,
|
|
3041
|
-
headers: { "Content-Type": "application/json" },
|
|
3042
|
-
body: JSON.stringify({
|
|
3043
|
-
email,
|
|
3044
|
-
code,
|
|
3045
|
-
token,
|
|
3046
|
-
// Multi-account add-mode · see WalletFlow.
|
|
3047
|
-
...add ? { add: true } : {}
|
|
3048
|
-
})
|
|
3049
|
-
});
|
|
3050
|
-
const json = await res.json();
|
|
3051
|
-
if (!res.ok || !("ok" in json) || !json.ok || !json.account) {
|
|
3052
|
-
throw new Error(
|
|
3053
|
-
"reason" in json && json.reason || "error" in json && json.error || "server_error"
|
|
3054
|
-
);
|
|
3055
|
-
}
|
|
3056
|
-
onSuccess(json.account, json.token);
|
|
3057
|
-
} catch (err) {
|
|
3058
|
-
setCodeError(failureMessage(err, "email", "submit"));
|
|
3059
|
-
} finally {
|
|
3060
|
-
setSubmitting(false);
|
|
3061
|
-
}
|
|
3062
|
-
}
|
|
3063
|
-
if (stage === "enter") {
|
|
3064
|
-
return /* @__PURE__ */ jsxs("form", { onSubmit: start, "data-oc-signin-email": "", children: [
|
|
3065
|
-
/* @__PURE__ */ jsx(FlowHeader, { children: "We email you a 6-digit code. No password, and no wallet needed to start. Add a Bitcoin address any time: a way in that only you hold." }),
|
|
3066
|
-
/* @__PURE__ */ jsx(Label, { htmlFor: `${uid}-email`, children: "Email" }),
|
|
3067
|
-
/* @__PURE__ */ jsx(
|
|
3068
|
-
"input",
|
|
3069
|
-
{
|
|
3070
|
-
id: `${uid}-email`,
|
|
3071
|
-
type: "email",
|
|
3072
|
-
autoComplete: "email",
|
|
3073
|
-
value: email,
|
|
3074
|
-
onChange: (e) => {
|
|
3075
|
-
setEmail(e.target.value);
|
|
3076
|
-
if (emailError) setEmailError(null);
|
|
3077
|
-
},
|
|
3078
|
-
placeholder: "you@example.com",
|
|
3079
|
-
required: true,
|
|
3080
|
-
"aria-invalid": emailError ? true : void 0,
|
|
3081
|
-
style: inputStyle2
|
|
3082
|
-
}
|
|
3083
|
-
),
|
|
3084
|
-
emailError && /* @__PURE__ */ jsx(ErrorLine2, { children: emailError }),
|
|
3085
|
-
/* @__PURE__ */ jsx("button", { type: "submit", disabled: submitting, style: submitStyle(submitting), children: submitting ? "Sending\u2026" : /* @__PURE__ */ jsx(Submit, { children: "Send one-time code" }) })
|
|
3086
|
-
] });
|
|
3087
|
-
}
|
|
3088
|
-
return /* @__PURE__ */ jsxs("form", { onSubmit: verify, "data-oc-signin-email-verify": "", children: [
|
|
3089
|
-
/* @__PURE__ */ jsxs(FlowHeader, { children: [
|
|
3090
|
-
resent ? "Sent a new code to " : "Sent a 6-digit code to ",
|
|
3091
|
-
/* @__PURE__ */ jsx("span", { style: { color: "var(--foreground, #fafafa)" }, children: email }),
|
|
3092
|
-
". It expires in 10 minutes."
|
|
3093
|
-
] }),
|
|
3094
|
-
/* @__PURE__ */ jsx(Label, { htmlFor: `${uid}-code`, children: "One-time code" }),
|
|
3095
|
-
/* @__PURE__ */ jsx(
|
|
3096
|
-
"input",
|
|
3097
|
-
{
|
|
3098
|
-
id: `${uid}-code`,
|
|
3099
|
-
type: "text",
|
|
3100
|
-
inputMode: "numeric",
|
|
3101
|
-
pattern: "\\d{6}",
|
|
3102
|
-
maxLength: 6,
|
|
3103
|
-
autoComplete: "one-time-code",
|
|
3104
|
-
value: code,
|
|
3105
|
-
onChange: (e) => {
|
|
3106
|
-
setCode(e.target.value.replace(/\D/g, "").slice(0, 6));
|
|
3107
|
-
if (codeError) setCodeError(null);
|
|
3108
|
-
},
|
|
3109
|
-
placeholder: "6 digits",
|
|
3110
|
-
required: true,
|
|
3111
|
-
autoFocus: true,
|
|
3112
|
-
"aria-invalid": codeError ? true : void 0,
|
|
3113
|
-
style: { ...inputStyle2, letterSpacing: "0.4em", fontSize: 16 }
|
|
3114
|
-
}
|
|
3115
|
-
),
|
|
3116
|
-
codeError && /* @__PURE__ */ jsx(ErrorLine2, { children: codeError }),
|
|
3117
|
-
/* @__PURE__ */ jsx(
|
|
3118
|
-
"button",
|
|
3119
|
-
{
|
|
3120
|
-
type: "submit",
|
|
3121
|
-
disabled: submitting || code.length !== 6,
|
|
3122
|
-
style: submitStyle(submitting || code.length !== 6),
|
|
3123
|
-
children: submitting ? "Verifying\u2026" : /* @__PURE__ */ jsx(Submit, { children: "Verify \xB7 sign me in" })
|
|
3124
|
-
}
|
|
3125
|
-
),
|
|
3126
|
-
/* @__PURE__ */ jsxs("div", { children: [
|
|
3127
|
-
/* @__PURE__ */ jsx(
|
|
3128
|
-
"button",
|
|
3129
|
-
{
|
|
3130
|
-
type: "button",
|
|
3131
|
-
onClick: () => void resend(),
|
|
3132
|
-
disabled: submitting,
|
|
3133
|
-
style: linkButtonStyle,
|
|
3134
|
-
children: "Send a new code"
|
|
3135
|
-
}
|
|
3136
|
-
),
|
|
3137
|
-
/* @__PURE__ */ jsx(
|
|
3138
|
-
"button",
|
|
3139
|
-
{
|
|
3140
|
-
type: "button",
|
|
3141
|
-
onClick: () => {
|
|
3142
|
-
setStage("enter");
|
|
3143
|
-
setCode("");
|
|
3144
|
-
setToken(null);
|
|
3145
|
-
setResent(false);
|
|
3146
|
-
},
|
|
3147
|
-
style: linkButtonStyle,
|
|
3148
|
-
children: "Use a different email"
|
|
3149
|
-
}
|
|
3150
|
-
)
|
|
3151
|
-
] })
|
|
3152
|
-
] });
|
|
3153
|
-
}
|
|
3154
|
-
function Submit({ children }) {
|
|
3155
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3156
|
-
children,
|
|
3157
|
-
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", style: { display: SIGNIN_ARROW }, children: " \u2192" })
|
|
3158
|
-
] });
|
|
3159
|
-
}
|
|
3160
|
-
function FlowHeader({
|
|
3161
|
-
label,
|
|
3162
|
-
children
|
|
3163
|
-
}) {
|
|
3164
|
-
return /* @__PURE__ */ jsxs("div", { style: { marginBottom: 16 }, children: [
|
|
3165
|
-
label && /* @__PURE__ */ jsx(
|
|
3166
|
-
"div",
|
|
3167
|
-
{
|
|
3168
|
-
style: {
|
|
3169
|
-
color: "var(--primary, #f97316)",
|
|
3170
|
-
fontFamily: SIGNIN_FONT,
|
|
3171
|
-
fontSize: signinSize(10),
|
|
3172
|
-
letterSpacing: signinTracking("0.18em"),
|
|
3173
|
-
textTransform: SIGNIN_CASE,
|
|
3174
|
-
marginBottom: 6
|
|
3175
|
-
},
|
|
3176
|
-
children: label
|
|
3177
|
-
}
|
|
3178
|
-
),
|
|
3179
|
-
/* @__PURE__ */ jsx(
|
|
3180
|
-
"p",
|
|
3181
|
-
{
|
|
3182
|
-
style: {
|
|
3183
|
-
color: "var(--muted-foreground, #a1a1aa)",
|
|
3184
|
-
fontSize: signinRoleSize("text", "13px"),
|
|
3185
|
-
lineHeight: 1.55,
|
|
3186
|
-
margin: 0
|
|
3187
|
-
},
|
|
3188
|
-
children
|
|
3189
|
-
}
|
|
3190
|
-
)
|
|
3191
|
-
] });
|
|
3192
|
-
}
|
|
3193
|
-
var linkButtonStyle = {
|
|
3194
|
-
display: "inline-flex",
|
|
3195
|
-
alignItems: "center",
|
|
3196
|
-
minHeight: 44,
|
|
3197
|
-
marginTop: 4,
|
|
3198
|
-
marginRight: 16,
|
|
3199
|
-
background: "transparent",
|
|
3200
|
-
border: "none",
|
|
3201
|
-
color: "var(--muted-foreground, #a1a1aa)",
|
|
3202
|
-
fontFamily: SIGNIN_FONT,
|
|
3203
|
-
fontSize: signinRoleSize("button", signinSize(11)),
|
|
3204
|
-
letterSpacing: signinTracking("0.12em"),
|
|
3205
|
-
textTransform: SIGNIN_CASE,
|
|
3206
|
-
cursor: "pointer",
|
|
3207
|
-
padding: 0
|
|
3208
|
-
};
|
|
3209
|
-
function Label({
|
|
3210
|
-
htmlFor,
|
|
3211
|
-
children
|
|
3212
|
-
}) {
|
|
3213
|
-
return /* @__PURE__ */ jsx(
|
|
3214
|
-
"label",
|
|
3215
|
-
{
|
|
3216
|
-
htmlFor,
|
|
3217
|
-
style: {
|
|
3218
|
-
display: "block",
|
|
3219
|
-
color: "var(--foreground, #fafafa)",
|
|
3220
|
-
fontFamily: SIGNIN_FONT,
|
|
3221
|
-
fontSize: signinRoleSize("label", signinSize(11)),
|
|
3222
|
-
letterSpacing: signinTracking("0.12em"),
|
|
3223
|
-
textTransform: SIGNIN_CASE,
|
|
3224
|
-
marginBottom: 6
|
|
3225
|
-
},
|
|
3226
|
-
children
|
|
3227
|
-
}
|
|
3228
|
-
);
|
|
3229
|
-
}
|
|
3230
|
-
var inputStyle2 = {
|
|
3231
|
-
width: "100%",
|
|
3232
|
-
padding: "0.55rem 0.75rem",
|
|
3233
|
-
background: "var(--background, #0a0a0a)",
|
|
3234
|
-
color: "var(--foreground, #fafafa)",
|
|
3235
|
-
border: "1px solid var(--input, #27272a)",
|
|
3236
|
-
borderRadius: signinRadius(6),
|
|
3237
|
-
fontFamily: SIGNIN_FONT,
|
|
3238
|
-
// 16px — never below it. A sub-16px font-size makes iOS Safari
|
|
3239
|
-
// auto-zoom the viewport on focus; the family rule is >=16px on
|
|
3240
|
-
// every form field. Desktop reads fine at 16 too.
|
|
3241
|
-
fontSize: 16,
|
|
3242
|
-
outline: "none"
|
|
3243
|
-
};
|
|
3244
|
-
var linkAlsoStyle = {
|
|
3245
|
-
display: "flex",
|
|
3246
|
-
alignItems: "center",
|
|
3247
|
-
// The whole row is the checkbox's hit area; the 13px box alone was not.
|
|
3248
|
-
minHeight: 44,
|
|
3249
|
-
gap: 8,
|
|
3250
|
-
marginTop: 16,
|
|
3251
|
-
paddingTop: 14,
|
|
3252
|
-
borderTop: "1px solid var(--border, #27272a)",
|
|
3253
|
-
color: "var(--muted-foreground, #a1a1aa)",
|
|
3254
|
-
fontFamily: SIGNIN_FONT,
|
|
3255
|
-
fontSize: signinRoleSize("text", signinSize(12)),
|
|
3256
|
-
lineHeight: 1.5,
|
|
3257
|
-
cursor: "pointer"
|
|
3258
|
-
};
|
|
3259
|
-
function submitStyle(disabled) {
|
|
3260
|
-
return {
|
|
3261
|
-
marginTop: 12,
|
|
3262
|
-
padding: "0.6rem 1rem",
|
|
3263
|
-
background: "var(--primary, #f97316)",
|
|
3264
|
-
color: "var(--primary-foreground, #0a0a0a)",
|
|
3265
|
-
border: "none",
|
|
3266
|
-
borderRadius: SIGNIN_BUTTON_RADIUS,
|
|
3267
|
-
fontFamily: SIGNIN_FONT,
|
|
3268
|
-
fontSize: signinRoleSize("button", signinSize(11)),
|
|
3269
|
-
letterSpacing: signinTracking("0.14em"),
|
|
3270
|
-
textTransform: SIGNIN_CASE,
|
|
3271
|
-
fontWeight: SIGNIN_STRONG,
|
|
3272
|
-
cursor: disabled ? "not-allowed" : "pointer",
|
|
3273
|
-
opacity: disabled ? 0.5 : 1,
|
|
3274
|
-
width: "100%"
|
|
3275
|
-
};
|
|
3276
|
-
}
|
|
3277
|
-
function ErrorLine2({ children }) {
|
|
3278
|
-
return /* @__PURE__ */ jsx(
|
|
3279
|
-
"p",
|
|
3280
|
-
{
|
|
3281
|
-
role: "alert",
|
|
3282
|
-
style: {
|
|
3283
|
-
marginTop: 6,
|
|
3284
|
-
marginBottom: 0,
|
|
3285
|
-
color: "var(--destructive, #ef4444)",
|
|
3286
|
-
fontFamily: SIGNIN_FONT,
|
|
3287
|
-
fontSize: signinRoleSize("hint", signinSize(11)),
|
|
3288
|
-
letterSpacing: "0.04em"
|
|
3289
|
-
},
|
|
3290
|
-
children
|
|
3291
|
-
}
|
|
3292
|
-
);
|
|
3293
|
-
}
|
|
3294
|
-
function Hint({ children }) {
|
|
3295
|
-
return /* @__PURE__ */ jsx(
|
|
3296
|
-
"p",
|
|
3297
|
-
{
|
|
3298
|
-
style: {
|
|
3299
|
-
marginTop: 12,
|
|
3300
|
-
marginBottom: 0,
|
|
3301
|
-
color: "var(--muted-foreground, #a1a1aa)",
|
|
3302
|
-
fontFamily: SIGNIN_FONT,
|
|
3303
|
-
fontSize: signinRoleSize("hint", "10.5px"),
|
|
3304
|
-
lineHeight: 1.55
|
|
3305
|
-
},
|
|
3306
|
-
children
|
|
3307
|
-
}
|
|
3308
|
-
);
|
|
3309
|
-
}
|
|
3310
|
-
var DEFAULT_AUTH_ORIGIN2 = "https://ochk.io";
|
|
3311
|
-
var MIN_CONFIRMATIONS = 6;
|
|
3312
|
-
var MIN_SATS = 1e3;
|
|
3313
|
-
var n = (v) => v.toLocaleString("en-US");
|
|
3314
|
-
function bondFloorsText(f) {
|
|
3315
|
-
const sats = f.sats_floor > 0 ? `at least ${n(f.sats_floor)} sats` : "under 1,000 sats";
|
|
3316
|
-
const days = f.days_floor > 0 ? `held ${f.days_floor} days or more` : "held under 7 days";
|
|
3317
|
-
return `${sats}, ${days}`;
|
|
3318
|
-
}
|
|
3319
|
-
var short = (outpoint) => {
|
|
3320
|
-
const [txid = "", vout = ""] = outpoint.split(":");
|
|
3321
|
-
return `${txid.slice(0, 8)}\u2026${txid.slice(-4)}:${vout}`;
|
|
3322
|
-
};
|
|
3323
|
-
var usable = (c) => c.sats >= MIN_SATS && c.age_blocks >= MIN_CONFIRMATIONS;
|
|
3324
|
-
var dustText = (count) => `${n(count)} ${count === 1 ? "coin" : "coins"} under ${n(MIN_SATS)} sats, too small to back an account`;
|
|
3325
|
-
var REFUSAL = {
|
|
3326
|
-
invalid_address: "that isn't a Bitcoin address we can read",
|
|
3327
|
-
chain_unavailable: "couldn't read the chain just now \xB7 nothing changed, try again in a minute",
|
|
3328
|
-
sig_invalid: "that signature isn't from this coin's address \xB7 sign with the wallet that holds it",
|
|
3329
|
-
bond_expired: "the message expired \xB7 start again",
|
|
3330
|
-
bond_not_yet_valid: "your device's clock looks wrong \xB7 check it and start again",
|
|
3331
|
-
bond_subject_mismatch: "that message was for another account \xB7 start again",
|
|
3332
|
-
bond_malformed: "the signed message changed on the way \xB7 start again",
|
|
3333
|
-
bond_window_too_long: "that message's signing window is too long \xB7 start again",
|
|
3334
|
-
coin_not_found: "that coin doesn't exist on-chain",
|
|
3335
|
-
coin_address_mismatch: "that coin isn't at this address",
|
|
3336
|
-
coin_unconfirmed: "that coin isn't confirmed yet",
|
|
3337
|
-
coin_too_recent: "that coin needs six confirmations first, about an hour",
|
|
3338
|
-
coin_spent: "that coin has been spent \xB7 pick one still in your wallet",
|
|
3339
|
-
coin_too_small: `that coin is under ${n(MIN_SATS)} sats, too small to back an account`,
|
|
3340
|
-
coin_backs_another_account: "that coin already backs another orangecheck account",
|
|
3341
|
-
coin_backed_another_recently: "that coin backed another account until recently \xB7 it can back this one 30 days after it was released",
|
|
3342
|
-
not_authenticated: "your session ended \xB7 sign in again",
|
|
3343
|
-
rate_limited: "too many tries \xB7 wait a minute, then try again"
|
|
3344
|
-
};
|
|
3345
|
-
var bondRefusal = (reason) => reason && REFUSAL[reason] || "your backing didn't save \xB7 nothing changed, try again";
|
|
3346
|
-
var smallStyle = { ...bodyStyle, fontSize: 12, margin: 0 };
|
|
3347
|
-
var explainerStyle = {
|
|
3348
|
-
...hintStyle,
|
|
3349
|
-
fontSize: 12,
|
|
3350
|
-
lineHeight: 1.55,
|
|
3351
|
-
opacity: 1,
|
|
3352
|
-
margin: 0
|
|
3353
|
-
};
|
|
3354
|
-
var fieldLabelStyle = {
|
|
3355
|
-
display: "block",
|
|
3356
|
-
color: V.foreground,
|
|
3357
|
-
fontSize: 13,
|
|
3358
|
-
fontWeight: 500,
|
|
3359
|
-
margin: "0 0 4px"
|
|
3360
|
-
};
|
|
3361
|
-
var addressStyle = { wordBreak: "break-all", color: V.foreground };
|
|
3362
|
-
function coinStyle(picked, young) {
|
|
3363
|
-
return {
|
|
3364
|
-
background: V.card,
|
|
3365
|
-
border: `1px ${young ? "dashed" : "solid"} ${picked ? V.primary : V.border}`,
|
|
3366
|
-
padding: "0.75rem",
|
|
3367
|
-
display: "flex",
|
|
3368
|
-
gap: 10,
|
|
3369
|
-
alignItems: "flex-start",
|
|
3370
|
-
cursor: young ? "not-allowed" : "pointer"
|
|
3371
|
-
};
|
|
3372
|
-
}
|
|
3373
|
-
async function fetchOcBond(opts) {
|
|
3374
|
-
const r = await fetch(`${opts?.authOrigin ?? DEFAULT_AUTH_ORIGIN2}/api/auth/bond`, {
|
|
3375
|
-
credentials: "include"
|
|
3376
|
-
});
|
|
3377
|
-
const j = await r.json().catch(() => ({}));
|
|
3378
|
-
if (!r.ok || !j.ok) throw new Error(`http_${r.status}`);
|
|
3379
|
-
return j.bond ?? null;
|
|
3380
|
-
}
|
|
3381
|
-
function OcBond({
|
|
3382
|
-
authOrigin = DEFAULT_AUTH_ORIGIN2,
|
|
3383
|
-
onChange,
|
|
3384
|
-
className
|
|
3385
|
-
}) {
|
|
3386
|
-
const [bond, setBond] = React4.useState(void 0);
|
|
3387
|
-
const [loadErr, setLoadErr] = React4.useState(false);
|
|
3388
|
-
const [nonce, setNonce] = React4.useState(0);
|
|
3389
|
-
const [adding, setAdding] = React4.useState(false);
|
|
3390
|
-
const [confirming, setConfirming] = React4.useState(false);
|
|
3391
|
-
const [busy, setBusy] = React4.useState(false);
|
|
3392
|
-
const [err, setErr] = React4.useState(null);
|
|
3393
|
-
React4.useEffect(() => {
|
|
3394
|
-
let alive = true;
|
|
3395
|
-
fetchOcBond({ authOrigin }).then(
|
|
3396
|
-
(b) => alive && setBond(b),
|
|
3397
|
-
() => alive && setLoadErr(true)
|
|
3398
|
-
);
|
|
3399
|
-
return () => {
|
|
3400
|
-
alive = false;
|
|
3401
|
-
};
|
|
3402
|
-
}, [authOrigin, nonce]);
|
|
3403
|
-
const call = React4.useCallback(
|
|
3404
|
-
async (method, body) => {
|
|
3405
|
-
const r = await fetch(`${authOrigin}/api/auth/bond`, {
|
|
3406
|
-
method,
|
|
3407
|
-
credentials: "include",
|
|
3408
|
-
headers: body ? { "Content-Type": "application/json" } : void 0,
|
|
3409
|
-
body: body ? JSON.stringify(body) : void 0
|
|
3410
|
-
});
|
|
3411
|
-
const j = await r.json().catch(() => ({}));
|
|
3412
|
-
const pin = readTabSession();
|
|
3413
|
-
if (pin && r.ok && typeof j.token === "string") {
|
|
3414
|
-
writeTabSession({ token: j.token, didOc: pin.didOc });
|
|
3415
|
-
}
|
|
3416
|
-
return { ok: r.ok && j.ok === true, ...j };
|
|
3417
|
-
},
|
|
3418
|
-
[authOrigin]
|
|
3419
|
-
);
|
|
3420
|
-
async function release() {
|
|
3421
|
-
setBusy(true);
|
|
3422
|
-
setErr(null);
|
|
3423
|
-
const j = await call("DELETE").catch(() => ({ ok: false, reason: "network" }));
|
|
3424
|
-
setBusy(false);
|
|
3425
|
-
if (!j.ok) return setErr(bondRefusal(j.reason));
|
|
3426
|
-
setConfirming(false);
|
|
3427
|
-
setBond(null);
|
|
3428
|
-
onChange?.(null);
|
|
3429
|
-
}
|
|
3430
|
-
if (loadErr) {
|
|
3431
|
-
return /* @__PURE__ */ jsxs("div", { className, "data-oc-bond": "", style: panelStyle("warn"), role: "alert", children: [
|
|
3432
|
-
/* @__PURE__ */ jsx("p", { style: bodyStyle, children: "couldn't read your backing \xB7 nothing was changed" }),
|
|
3433
|
-
/* @__PURE__ */ jsx(
|
|
3434
|
-
"button",
|
|
3435
|
-
{
|
|
3436
|
-
type: "button",
|
|
3437
|
-
style: ghostBtnStyle(false),
|
|
3438
|
-
onClick: () => {
|
|
3439
|
-
setLoadErr(false);
|
|
3440
|
-
setNonce((x) => x + 1);
|
|
3441
|
-
},
|
|
3442
|
-
children: "try again"
|
|
3443
|
-
}
|
|
3444
|
-
)
|
|
3445
|
-
] });
|
|
3446
|
-
}
|
|
3447
|
-
if (bond === void 0) {
|
|
3448
|
-
return /* @__PURE__ */ jsx("div", { className, "data-oc-bond": "", style: panelStyle(), children: /* @__PURE__ */ jsx("span", { style: hintStyle, children: "reading your backing\u2026" }) });
|
|
3449
|
-
}
|
|
3450
|
-
return /* @__PURE__ */ jsxs("div", { className, "data-oc-bond": "", style: { display: "grid", gap: 16 }, children: [
|
|
3451
|
-
bond && !adding ? /* @__PURE__ */ jsxs("div", { style: panelStyle("success"), children: [
|
|
3452
|
-
/* @__PURE__ */ jsx(SectionLabel, { tone: "success", children: "\xA7 backed by bitcoin" }),
|
|
3453
|
-
/* @__PURE__ */ jsxs("p", { style: { ...bodyStyle, color: V.foreground }, children: [
|
|
3454
|
-
"apps see a coin ",
|
|
3455
|
-
bondFloorsText(bond),
|
|
3456
|
-
"."
|
|
3457
|
-
] }),
|
|
3458
|
-
/* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
|
|
3459
|
-
"coin ",
|
|
3460
|
-
short(bond.outpoint),
|
|
3461
|
-
", ",
|
|
3462
|
-
n(bond.sats),
|
|
3463
|
-
" sats \xB7 last found unspent at block ",
|
|
3464
|
-
n(bond.checked_height),
|
|
3465
|
-
" \xB7 only you and orangecheck see this line"
|
|
3466
|
-
] }),
|
|
3467
|
-
confirming ? /* @__PURE__ */ jsxs("div", { role: "group", "aria-label": "stop backing", "data-oc-bond-confirm": "", children: [
|
|
3468
|
-
/* @__PURE__ */ jsx("p", { style: { ...bodyStyle, color: V.foreground, margin: 0 }, children: "stop backing? apps that ask for a backed account may stop letting you in." }),
|
|
3469
|
-
/* @__PURE__ */ jsxs(FormButtons, { children: [
|
|
3470
|
-
/* @__PURE__ */ jsx(
|
|
3471
|
-
"button",
|
|
3472
|
-
{
|
|
3473
|
-
type: "button",
|
|
3474
|
-
style: dangerBtnStyle(busy),
|
|
3475
|
-
disabled: busy,
|
|
3476
|
-
onClick: () => void release(),
|
|
3477
|
-
children: "stop backing"
|
|
3478
|
-
}
|
|
3479
|
-
),
|
|
3480
|
-
/* @__PURE__ */ jsx(
|
|
3481
|
-
"button",
|
|
3482
|
-
{
|
|
3483
|
-
type: "button",
|
|
3484
|
-
style: ghostBtnStyle(busy),
|
|
3485
|
-
disabled: busy,
|
|
3486
|
-
autoFocus: true,
|
|
3487
|
-
onClick: () => {
|
|
3488
|
-
setErr(null);
|
|
3489
|
-
setConfirming(false);
|
|
3490
|
-
},
|
|
3491
|
-
children: "keep"
|
|
3492
|
-
}
|
|
3493
|
-
)
|
|
3494
|
-
] })
|
|
3495
|
-
] }) : /* @__PURE__ */ jsxs(FormButtons, { children: [
|
|
3496
|
-
/* @__PURE__ */ jsx(
|
|
3497
|
-
"button",
|
|
3498
|
-
{
|
|
3499
|
-
type: "button",
|
|
3500
|
-
style: ghostBtnStyle(false),
|
|
3501
|
-
onClick: () => {
|
|
3502
|
-
setErr(null);
|
|
3503
|
-
setAdding(true);
|
|
3504
|
-
},
|
|
3505
|
-
children: "use another coin"
|
|
3506
|
-
}
|
|
3507
|
-
),
|
|
3508
|
-
/* @__PURE__ */ jsx(
|
|
3509
|
-
"button",
|
|
3510
|
-
{
|
|
3511
|
-
type: "button",
|
|
3512
|
-
style: dangerBtnStyle(false),
|
|
3513
|
-
onClick: () => setConfirming(true),
|
|
3514
|
-
children: "stop backing"
|
|
3515
|
-
}
|
|
3516
|
-
)
|
|
3517
|
-
] }),
|
|
3518
|
-
err ? /* @__PURE__ */ jsx(ErrorLine, { children: err }) : null
|
|
3519
|
-
] }) : null,
|
|
3520
|
-
!bond && !adding ? /* @__PURE__ */ jsxs("div", { style: panelStyle(), children: [
|
|
3521
|
-
/* @__PURE__ */ jsx(SectionLabel, { children: "\xA7 not backed" }),
|
|
3522
|
-
/* @__PURE__ */ jsx("p", { style: bodyStyle, children: "your account works everywhere as it is. some apps let in only accounts backed by a coin you hold. it stays in your wallet, and apps never see which coin." }),
|
|
3523
|
-
/* @__PURE__ */ jsx(
|
|
3524
|
-
"button",
|
|
3525
|
-
{
|
|
3526
|
-
type: "button",
|
|
3527
|
-
style: primaryBtnStyle(false),
|
|
3528
|
-
onClick: () => setAdding(true),
|
|
3529
|
-
children: "back your account"
|
|
3530
|
-
}
|
|
3531
|
-
)
|
|
3532
|
-
] }) : null,
|
|
3533
|
-
adding ? /* @__PURE__ */ jsx(
|
|
3534
|
-
BondForm,
|
|
3535
|
-
{
|
|
3536
|
-
call,
|
|
3537
|
-
onCancel: () => setAdding(false),
|
|
3538
|
-
onDone: (b) => {
|
|
3539
|
-
setBond(b);
|
|
3540
|
-
setAdding(false);
|
|
3541
|
-
onChange?.(b);
|
|
3542
|
-
}
|
|
3543
|
-
}
|
|
3544
|
-
) : null,
|
|
3545
|
-
/* @__PURE__ */ jsx("p", { style: explainerStyle, children: "Apps see only rough floors, like \u201Cat least 100,000 sats, held 30 days or more\u201D, and a number unique to that app. Never the coin, your address or your balance. An app can block that number; only moving the coin gets past it, and a moved coin starts its age again." })
|
|
3546
|
-
] });
|
|
3547
|
-
}
|
|
3548
|
-
function BondForm({
|
|
3549
|
-
call,
|
|
3550
|
-
onCancel,
|
|
3551
|
-
onDone
|
|
3552
|
-
}) {
|
|
3553
|
-
const [address, setAddress] = React4.useState("");
|
|
3554
|
-
const [coins, setCoins] = React4.useState(null);
|
|
3555
|
-
const [chosen, setChosen] = React4.useState("");
|
|
3556
|
-
const [message, setMessage] = React4.useState(null);
|
|
3557
|
-
const [busy, setBusy] = React4.useState(false);
|
|
3558
|
-
const [err, setErr] = React4.useState(null);
|
|
3559
|
-
const id = React4.useId();
|
|
3560
|
-
const addr = address.trim();
|
|
3561
|
-
const canFind = addr.length >= 14;
|
|
3562
|
-
async function run(fn) {
|
|
3563
|
-
setBusy(true);
|
|
3564
|
-
setErr(null);
|
|
3565
|
-
try {
|
|
3566
|
-
await fn();
|
|
3567
|
-
} catch {
|
|
3568
|
-
setErr(bondRefusal("chain_unavailable"));
|
|
3569
|
-
} finally {
|
|
3570
|
-
setBusy(false);
|
|
3571
|
-
}
|
|
3572
|
-
}
|
|
3573
|
-
const findCoins = () => run(async () => {
|
|
3574
|
-
const j = await call("POST", { action: "coins", address: addr });
|
|
3575
|
-
if (!j.ok) return setErr(bondRefusal(j.reason));
|
|
3576
|
-
const list = j.coins ?? [];
|
|
3577
|
-
setCoins(list);
|
|
3578
|
-
setChosen(list.find(usable)?.outpoint ?? "");
|
|
3579
|
-
});
|
|
3580
|
-
const getMessage = () => run(async () => {
|
|
3581
|
-
const j = await call("POST", { action: "message", address: addr, outpoint: chosen });
|
|
3582
|
-
if (!j.ok || typeof j.message !== "string") return setErr(bondRefusal(j.reason));
|
|
3583
|
-
setMessage(j.message);
|
|
3584
|
-
});
|
|
3585
|
-
const submit = (signature) => run(async () => {
|
|
3586
|
-
const j = await call("POST", { action: "bond", message, signature });
|
|
3587
|
-
if (!j.ok || !j.bond) return setErr(bondRefusal(j.reason));
|
|
3588
|
-
onDone(j.bond);
|
|
3589
|
-
});
|
|
3590
|
-
function back() {
|
|
3591
|
-
setErr(null);
|
|
3592
|
-
if (message) setMessage(null);
|
|
3593
|
-
else setCoins(null);
|
|
3594
|
-
}
|
|
3595
|
-
const error = err ? /* @__PURE__ */ jsx(ErrorLine, { children: err }) : null;
|
|
3596
|
-
const backButton = /* @__PURE__ */ jsx("button", { type: "button", style: ghostBtnStyle(busy), disabled: busy, onClick: back, children: "back" });
|
|
3597
|
-
const cancelButton = /* @__PURE__ */ jsx("button", { type: "button", style: ghostBtnStyle(busy), disabled: busy, onClick: onCancel, children: "cancel" });
|
|
3598
|
-
let step;
|
|
3599
|
-
if (message) {
|
|
3600
|
-
step = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3601
|
-
/* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
|
|
3602
|
-
"sign with the wallet that holds ",
|
|
3603
|
-
/* @__PURE__ */ jsx("span", { style: addressStyle, children: addr }),
|
|
3604
|
-
". signing spends nothing, and the coin stays in your wallet."
|
|
3605
|
-
] }),
|
|
3606
|
-
/* @__PURE__ */ jsx(
|
|
3607
|
-
LazyWalletButton,
|
|
3608
|
-
{
|
|
3609
|
-
address: addr,
|
|
3610
|
-
message,
|
|
3611
|
-
showManual: true,
|
|
3612
|
-
onSigned: (sig) => void submit(sig),
|
|
3613
|
-
onError: (e) => setErr(e.message ?? "the wallet refused to sign")
|
|
3614
|
-
}
|
|
3615
|
-
),
|
|
3616
|
-
error,
|
|
3617
|
-
/* @__PURE__ */ jsxs(FormButtons, { children: [
|
|
3618
|
-
backButton,
|
|
3619
|
-
cancelButton
|
|
3620
|
-
] })
|
|
3621
|
-
] });
|
|
3622
|
-
} else if (coins) {
|
|
3623
|
-
const eligible = coins.filter((c) => c.sats >= MIN_SATS);
|
|
3624
|
-
const dust = coins.length - eligible.length;
|
|
3625
|
-
step = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3626
|
-
coins.length === 0 ? /* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
|
|
3627
|
-
"no confirmed coins at that address \xB7 send at least ",
|
|
3628
|
-
n(MIN_SATS),
|
|
3629
|
-
" sats there and wait for six confirmations, about an hour. the longer a coin sits, the more it says."
|
|
3630
|
-
] }) : eligible.length === 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3631
|
-
/* @__PURE__ */ jsx("p", { style: { ...bodyStyle, color: V.foreground }, children: dustText(dust) }),
|
|
3632
|
-
/* @__PURE__ */ jsxs("p", { style: bodyStyle, children: [
|
|
3633
|
-
"send at least ",
|
|
3634
|
-
n(MIN_SATS),
|
|
3635
|
-
" sats there in one payment and wait for six confirmations, about an hour. coins aren't added together."
|
|
3636
|
-
] })
|
|
3637
|
-
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3638
|
-
/* @__PURE__ */ jsx("p", { id: `${id}-pick`, style: bodyStyle, children: "pick one coin \xB7 coins aren't added together" }),
|
|
3639
|
-
/* @__PURE__ */ jsx(
|
|
3640
|
-
"div",
|
|
3641
|
-
{
|
|
3642
|
-
role: "radiogroup",
|
|
3643
|
-
"aria-labelledby": `${id}-pick`,
|
|
3644
|
-
style: { display: "grid", gap: 8 },
|
|
3645
|
-
children: eligible.map((c) => {
|
|
3646
|
-
const young = c.age_blocks < MIN_CONFIRMATIONS;
|
|
3647
|
-
return /* @__PURE__ */ jsxs(
|
|
3648
|
-
"label",
|
|
3649
|
-
{
|
|
3650
|
-
style: coinStyle(chosen === c.outpoint, young),
|
|
3651
|
-
children: [
|
|
3652
|
-
/* @__PURE__ */ jsx(
|
|
3653
|
-
"input",
|
|
3654
|
-
{
|
|
3655
|
-
type: "radio",
|
|
3656
|
-
name: "oc-bond-coin",
|
|
3657
|
-
value: c.outpoint,
|
|
3658
|
-
checked: chosen === c.outpoint,
|
|
3659
|
-
disabled: young,
|
|
3660
|
-
onChange: () => setChosen(c.outpoint),
|
|
3661
|
-
style: { accentColor: V.primary, margin: "2px 0 0" }
|
|
3662
|
-
}
|
|
3663
|
-
),
|
|
3664
|
-
/* @__PURE__ */ jsxs("span", { style: { display: "grid", gap: 2, minWidth: 0 }, children: [
|
|
3665
|
-
/* @__PURE__ */ jsxs(
|
|
3666
|
-
"span",
|
|
3667
|
-
{
|
|
3668
|
-
style: {
|
|
3669
|
-
color: young ? V.muted : V.foreground,
|
|
3670
|
-
fontSize: 13
|
|
3671
|
-
},
|
|
3672
|
-
children: [
|
|
3673
|
-
n(c.sats),
|
|
3674
|
-
" sats, held",
|
|
3675
|
-
" ",
|
|
3676
|
-
Math.floor(c.age_blocks / 144),
|
|
3677
|
-
" days"
|
|
3678
|
-
]
|
|
3679
|
-
}
|
|
3680
|
-
),
|
|
3681
|
-
/* @__PURE__ */ jsx("span", { style: smallStyle, children: young ? "too recent: needs six confirmations" : `apps would see ${bondFloorsText(c)}` }),
|
|
3682
|
-
/* @__PURE__ */ jsx(
|
|
3683
|
-
"span",
|
|
3684
|
-
{
|
|
3685
|
-
style: {
|
|
3686
|
-
...smallStyle,
|
|
3687
|
-
fontFamily: SIGNIN_FONT,
|
|
3688
|
-
wordBreak: "break-all"
|
|
3689
|
-
},
|
|
3690
|
-
children: short(c.outpoint)
|
|
3691
|
-
}
|
|
3692
|
-
)
|
|
3693
|
-
] })
|
|
3694
|
-
]
|
|
3695
|
-
},
|
|
3696
|
-
c.outpoint
|
|
3697
|
-
);
|
|
3698
|
-
})
|
|
3699
|
-
}
|
|
3700
|
-
),
|
|
3701
|
-
dust > 0 ? /* @__PURE__ */ jsx("p", { style: { ...smallStyle, marginTop: 8 }, children: dustText(dust) }) : null
|
|
3702
|
-
] }),
|
|
3703
|
-
error,
|
|
3704
|
-
/* @__PURE__ */ jsxs(FormButtons, { children: [
|
|
3705
|
-
eligible.length > 0 ? /* @__PURE__ */ jsx(
|
|
3706
|
-
"button",
|
|
3707
|
-
{
|
|
3708
|
-
type: "button",
|
|
3709
|
-
style: primaryBtnStyle(busy || !chosen),
|
|
3710
|
-
disabled: busy || !chosen,
|
|
3711
|
-
onClick: () => void getMessage(),
|
|
3712
|
-
children: "continue"
|
|
3713
|
-
}
|
|
3714
|
-
) : null,
|
|
3715
|
-
backButton,
|
|
3716
|
-
cancelButton
|
|
3717
|
-
] })
|
|
3718
|
-
] });
|
|
3719
|
-
} else {
|
|
3720
|
-
step = /* @__PURE__ */ jsxs(
|
|
3721
|
-
"form",
|
|
3722
|
-
{
|
|
3723
|
-
style: { margin: 0 },
|
|
3724
|
-
onSubmit: (e) => {
|
|
3725
|
-
e.preventDefault();
|
|
3726
|
-
if (canFind && !busy) void findCoins();
|
|
3727
|
-
},
|
|
3728
|
-
children: [
|
|
3729
|
-
/* @__PURE__ */ jsx("label", { htmlFor: `${id}-address`, style: fieldLabelStyle, children: "bitcoin address that holds the coin" }),
|
|
3730
|
-
/* @__PURE__ */ jsx("p", { id: `${id}-advice`, style: { ...bodyStyle, margin: "0 0 8px" }, children: "use one that holds nothing else of yours: orangecheck learns its address and amount. apps never do." }),
|
|
3731
|
-
/* @__PURE__ */ jsx(
|
|
3732
|
-
"input",
|
|
3733
|
-
{
|
|
3734
|
-
id: `${id}-address`,
|
|
3735
|
-
"aria-describedby": `${id}-advice`,
|
|
3736
|
-
style: inputStyle,
|
|
3737
|
-
value: address,
|
|
3738
|
-
onChange: (e) => setAddress(e.target.value),
|
|
3739
|
-
placeholder: "bc1q\u2026",
|
|
3740
|
-
spellCheck: false,
|
|
3741
|
-
autoComplete: "off",
|
|
3742
|
-
autoCapitalize: "off"
|
|
3743
|
-
}
|
|
3744
|
-
),
|
|
3745
|
-
error,
|
|
3746
|
-
/* @__PURE__ */ jsxs(FormButtons, { children: [
|
|
3747
|
-
/* @__PURE__ */ jsx(
|
|
3748
|
-
"button",
|
|
3749
|
-
{
|
|
3750
|
-
type: "submit",
|
|
3751
|
-
style: primaryBtnStyle(busy || !canFind),
|
|
3752
|
-
disabled: busy || !canFind,
|
|
3753
|
-
children: "find its coins"
|
|
3754
|
-
}
|
|
3755
|
-
),
|
|
3756
|
-
cancelButton
|
|
3757
|
-
] })
|
|
3758
|
-
]
|
|
3759
|
-
}
|
|
3760
|
-
);
|
|
3761
|
-
}
|
|
3762
|
-
return /* @__PURE__ */ jsxs("div", { style: panelStyle("accent"), "data-oc-bond-form": "", children: [
|
|
3763
|
-
/* @__PURE__ */ jsx(SectionLabel, { children: "\xA7 back your account" }),
|
|
3764
|
-
step
|
|
3765
|
-
] });
|
|
3766
|
-
}
|
|
3767
|
-
function resolveAuthOrigin(opts) {
|
|
3768
|
-
if (!opts) return resolveConfig(void 0).authOrigin;
|
|
3769
|
-
return resolveConfig(opts).authOrigin;
|
|
3770
|
-
}
|
|
3771
|
-
function isAbortError(err) {
|
|
3772
|
-
if (!(err instanceof Error)) return false;
|
|
3773
|
-
return /NotAllowed|aborted|cancel|user cancelled/i.test(err.message);
|
|
3774
|
-
}
|
|
3775
|
-
function useWebAuthnRegister(opts) {
|
|
3776
|
-
const authOrigin = resolveAuthOrigin(opts);
|
|
3777
|
-
const [status, setStatus] = React4.useState("idle");
|
|
3778
|
-
const [error, setError] = React4.useState(null);
|
|
3779
|
-
const reset = React4.useCallback(() => {
|
|
3780
|
-
setStatus("idle");
|
|
3781
|
-
setError(null);
|
|
3782
|
-
}, []);
|
|
3783
|
-
const register = React4.useCallback(
|
|
3784
|
-
async (args) => {
|
|
3785
|
-
setError(null);
|
|
3786
|
-
setStatus("requesting-options");
|
|
3787
|
-
try {
|
|
3788
|
-
const optsRes = await fetch(`${authOrigin}/api/auth/webauthn/register/options`, {
|
|
3789
|
-
method: "POST",
|
|
3790
|
-
credentials: "include",
|
|
3791
|
-
headers: { "Content-Type": "application/json" },
|
|
3792
|
-
body: JSON.stringify({ label: args?.label })
|
|
3793
|
-
});
|
|
3794
|
-
const optsBody = await optsRes.json();
|
|
3795
|
-
if (!optsRes.ok || !optsBody.ok || !optsBody.options || !optsBody.challenge_token) {
|
|
3796
|
-
const reason = optsBody.reason ?? `options_failed_${optsRes.status}`;
|
|
3797
|
-
setError(new Error(reason));
|
|
3798
|
-
setStatus("error");
|
|
3799
|
-
return { ok: false, reason };
|
|
3800
|
-
}
|
|
3801
|
-
setStatus("authenticating");
|
|
3802
|
-
let attestation;
|
|
3803
|
-
try {
|
|
3804
|
-
attestation = await startRegistration({ optionsJSON: optsBody.options });
|
|
3805
|
-
} catch (e) {
|
|
3806
|
-
const reason = isAbortError(e) ? "cancelled" : e.message;
|
|
3807
|
-
setError(new Error(reason));
|
|
3808
|
-
setStatus("error");
|
|
3809
|
-
return { ok: false, reason };
|
|
3810
|
-
}
|
|
3811
|
-
setStatus("verifying");
|
|
3812
|
-
const verifyRes = await fetch(`${authOrigin}/api/auth/webauthn/register/verify`, {
|
|
3813
|
-
method: "POST",
|
|
3814
|
-
credentials: "include",
|
|
3815
|
-
headers: { "Content-Type": "application/json" },
|
|
3816
|
-
body: JSON.stringify({
|
|
3817
|
-
challenge_token: optsBody.challenge_token,
|
|
3818
|
-
response: attestation,
|
|
3819
|
-
label: args?.label
|
|
3820
|
-
})
|
|
3821
|
-
});
|
|
3822
|
-
const verifyBody = await verifyRes.json();
|
|
3823
|
-
if (!verifyRes.ok || !verifyBody.ok || !verifyBody.credential) {
|
|
3824
|
-
const reason = verifyBody.reason ?? `verify_failed_${verifyRes.status}`;
|
|
3825
|
-
setError(new Error(reason));
|
|
3826
|
-
setStatus("error");
|
|
3827
|
-
return { ok: false, reason };
|
|
3828
|
-
}
|
|
3829
|
-
setStatus("success");
|
|
3830
|
-
return { ok: true, credential: verifyBody.credential };
|
|
3831
|
-
} catch (e) {
|
|
3832
|
-
const reason = e instanceof Error ? e.message : String(e);
|
|
3833
|
-
setError(new Error(reason));
|
|
3834
|
-
setStatus("error");
|
|
3835
|
-
return { ok: false, reason };
|
|
3836
|
-
}
|
|
3837
|
-
},
|
|
3838
|
-
[authOrigin]
|
|
3839
|
-
);
|
|
3840
|
-
return { status, error, register, reset };
|
|
3841
|
-
}
|
|
3842
|
-
function useWebAuthnList(opts) {
|
|
3843
|
-
const authOrigin = resolveAuthOrigin(opts);
|
|
3844
|
-
const { status: sessionStatus } = useOcSession();
|
|
3845
|
-
const [credentials, setCredentials] = React4.useState([]);
|
|
3846
|
-
const [status, setStatus] = React4.useState("loading");
|
|
3847
|
-
const [error, setError] = React4.useState(null);
|
|
3848
|
-
const refetch = React4.useCallback(async () => {
|
|
3849
|
-
setError(null);
|
|
3850
|
-
if (sessionStatus !== "authenticated") {
|
|
3851
|
-
setCredentials([]);
|
|
3852
|
-
setStatus("ready");
|
|
3853
|
-
return;
|
|
3854
|
-
}
|
|
3855
|
-
setStatus("loading");
|
|
3856
|
-
try {
|
|
3857
|
-
const r = await fetch(`${authOrigin}/api/auth/webauthn/credentials`, {
|
|
3858
|
-
credentials: "include"
|
|
3859
|
-
});
|
|
3860
|
-
if (r.status === 401) {
|
|
3861
|
-
setCredentials([]);
|
|
3862
|
-
setStatus("ready");
|
|
3863
|
-
return;
|
|
3864
|
-
}
|
|
3865
|
-
const j = await r.json();
|
|
3866
|
-
if (!j.ok || !j.credentials) {
|
|
3867
|
-
throw new Error(j.reason ?? "list_failed");
|
|
3868
|
-
}
|
|
3869
|
-
setCredentials(j.credentials);
|
|
3870
|
-
setStatus("ready");
|
|
3871
|
-
} catch (e) {
|
|
3872
|
-
setError(e instanceof Error ? e : new Error(String(e)));
|
|
3873
|
-
setStatus("error");
|
|
3874
|
-
}
|
|
3875
|
-
}, [authOrigin, sessionStatus]);
|
|
3876
|
-
React4.useEffect(() => {
|
|
3877
|
-
void refetch();
|
|
3878
|
-
}, [refetch]);
|
|
3879
|
-
const rename = React4.useCallback(
|
|
3880
|
-
async (id, label) => {
|
|
3881
|
-
try {
|
|
3882
|
-
const r = await fetch(`${authOrigin}/api/auth/webauthn/credentials/${id}`, {
|
|
3883
|
-
method: "PATCH",
|
|
3884
|
-
credentials: "include",
|
|
3885
|
-
headers: { "Content-Type": "application/json" },
|
|
3886
|
-
body: JSON.stringify({ label })
|
|
3887
|
-
});
|
|
3888
|
-
const j = await r.json();
|
|
3889
|
-
if (!j.ok || !j.credential) {
|
|
3890
|
-
const reason = j.reason ?? `rename_failed_${r.status}`;
|
|
3891
|
-
setError(new Error(reason));
|
|
3892
|
-
return { ok: false, reason };
|
|
3893
|
-
}
|
|
3894
|
-
await refetch();
|
|
3895
|
-
return { ok: true, credential: j.credential };
|
|
3896
|
-
} catch (e) {
|
|
3897
|
-
const reason = e instanceof Error ? e.message : String(e);
|
|
3898
|
-
setError(new Error(reason));
|
|
3899
|
-
return { ok: false, reason };
|
|
3900
|
-
}
|
|
3901
|
-
},
|
|
3902
|
-
[authOrigin, refetch]
|
|
3903
|
-
);
|
|
3904
|
-
const remove = React4.useCallback(
|
|
3905
|
-
async (id) => {
|
|
3906
|
-
try {
|
|
3907
|
-
const r = await fetch(`${authOrigin}/api/auth/webauthn/credentials/${id}`, {
|
|
3908
|
-
method: "DELETE",
|
|
3909
|
-
credentials: "include"
|
|
3910
|
-
});
|
|
3911
|
-
const j = await r.json();
|
|
3912
|
-
if (!j.ok) {
|
|
3913
|
-
const reason = j.reason ?? `delete_failed_${r.status}`;
|
|
3914
|
-
setError(new Error(reason));
|
|
3915
|
-
return { ok: false, reason };
|
|
3916
|
-
}
|
|
3917
|
-
await refetch();
|
|
3918
|
-
return { ok: true };
|
|
3919
|
-
} catch (e) {
|
|
3920
|
-
const reason = e instanceof Error ? e.message : String(e);
|
|
3921
|
-
setError(new Error(reason));
|
|
3922
|
-
return { ok: false, reason };
|
|
3923
|
-
}
|
|
3924
|
-
},
|
|
3925
|
-
[authOrigin, refetch]
|
|
3926
|
-
);
|
|
3927
|
-
return { status, credentials, error, refetch, rename, remove };
|
|
3928
|
-
}
|
|
3929
|
-
function useStepUpAuth(opts) {
|
|
3930
|
-
const authOrigin = resolveAuthOrigin(opts);
|
|
3931
|
-
const { refresh } = useOcSession();
|
|
3932
|
-
const [status, setStatus] = React4.useState("idle");
|
|
3933
|
-
const [error, setError] = React4.useState(null);
|
|
3934
|
-
const reset = React4.useCallback(() => {
|
|
3935
|
-
setStatus("idle");
|
|
3936
|
-
setError(null);
|
|
3937
|
-
}, []);
|
|
3938
|
-
const stepUp = React4.useCallback(
|
|
3939
|
-
async (args) => {
|
|
3940
|
-
setError(null);
|
|
3941
|
-
setStatus("requesting-options");
|
|
3942
|
-
try {
|
|
3943
|
-
const optsRes = await fetch(`${authOrigin}/api/auth/webauthn/assertion/options`, {
|
|
3944
|
-
method: "POST",
|
|
3945
|
-
credentials: "include",
|
|
3946
|
-
headers: { "Content-Type": "application/json" },
|
|
3947
|
-
body: JSON.stringify({ purpose: args.purpose })
|
|
3948
|
-
});
|
|
3949
|
-
const optsBody = await optsRes.json();
|
|
3950
|
-
if (!optsRes.ok || !optsBody.ok || !optsBody.options || !optsBody.challenge_token) {
|
|
3951
|
-
const reason = optsBody.reason ?? `options_failed_${optsRes.status}`;
|
|
3952
|
-
setError(new Error(reason));
|
|
3953
|
-
setStatus("error");
|
|
3954
|
-
return { ok: false, reason };
|
|
3955
|
-
}
|
|
3956
|
-
setStatus("authenticating");
|
|
3957
|
-
let assertion;
|
|
3958
|
-
try {
|
|
3959
|
-
assertion = await startAuthentication({ optionsJSON: optsBody.options });
|
|
3960
|
-
} catch (e) {
|
|
3961
|
-
const reason = isAbortError(e) ? "cancelled" : e.message;
|
|
3962
|
-
setError(new Error(reason));
|
|
3963
|
-
setStatus("error");
|
|
3964
|
-
return { ok: false, reason };
|
|
3965
|
-
}
|
|
3966
|
-
setStatus("verifying");
|
|
3967
|
-
const verifyRes = await fetch(
|
|
3968
|
-
`${authOrigin}/api/auth/webauthn/assertion/verify`,
|
|
3969
|
-
{
|
|
3970
|
-
method: "POST",
|
|
3971
|
-
credentials: "include",
|
|
3972
|
-
headers: { "Content-Type": "application/json" },
|
|
3973
|
-
body: JSON.stringify({
|
|
3974
|
-
challenge_token: optsBody.challenge_token,
|
|
3975
|
-
response: assertion
|
|
3976
|
-
})
|
|
3977
|
-
}
|
|
3978
|
-
);
|
|
3979
|
-
const verifyBody = await verifyRes.json();
|
|
3980
|
-
if (!verifyRes.ok || !verifyBody.ok || typeof verifyBody.step_up_at !== "number") {
|
|
3981
|
-
const reason = verifyBody.reason ?? `verify_failed_${verifyRes.status}`;
|
|
3982
|
-
setError(new Error(reason));
|
|
3983
|
-
setStatus("error");
|
|
3984
|
-
return { ok: false, reason };
|
|
3985
|
-
}
|
|
3986
|
-
await refresh();
|
|
3987
|
-
setStatus("success");
|
|
3988
|
-
return { ok: true, step_up_at: verifyBody.step_up_at };
|
|
3989
|
-
} catch (e) {
|
|
3990
|
-
const reason = e instanceof Error ? e.message : String(e);
|
|
3991
|
-
setError(new Error(reason));
|
|
3992
|
-
setStatus("error");
|
|
3993
|
-
return { ok: false, reason };
|
|
3994
|
-
}
|
|
3995
|
-
},
|
|
3996
|
-
[authOrigin, refresh]
|
|
3997
|
-
);
|
|
3998
|
-
return { status, error, stepUp, reset };
|
|
3999
|
-
}
|
|
4000
|
-
|
|
4001
|
-
export { DEFAULT_CONFIG, DISPLAY_IDENTITY_KINDS, FRAGMENT_TTL_MS, OcAccountChip, OcAccountPill, OcAddressInput, OcBond, OcLinkedIdentities, OcSessionProvider, OcSignIn, OcSignInButton, SUDO_ACCOUNT_MISMATCH_MESSAGE, TAB_ACCOUNT_HINT_KEY, TAB_ADOPT_HASH, TAB_SESSION_HEADER, TAB_SESSION_STORAGE_KEY, bondFloorsText, buildAddAccountUrl, buildSignInUrl, clearTabSession, consumeTabAccountHint, consumeTabAdoptMarker, familyReturnTarget, fetchOcBond, fetchOcLinkedIdentities, handleSudoRequired, installTabFetchInterceptor, installTabLinkDecorator, isSudoAccountMismatch, keepFragmentOnReturnLinks, readTabSession, redirectToSudo, restoreReturnFragment, returnToHere, safeReturnTo, stripFragment, tabSessionHeader, useOcAddressSuggestion, useOcSession, useOptionalOcSession, useStepUpAuth, useWebAuthnList, useWebAuthnRegister, withoutFragment, writeTabSession };
|
|
4002
|
-
//# sourceMappingURL=index.mjs.map
|
|
4003
114
|
//# sourceMappingURL=index.mjs.map
|