aboutsummaryrefslogtreecommitdiffstats
path: root/src/lib
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-02-16 11:51:25 -0800
committerAdam Soltys <[email protected]>2026-02-16 11:51:25 -0800
commite39bf4f977bba2952a7de4a307b14c5ec862a3a7 (patch)
tree1eac038796d376ad073eae6246bf43f15454bd66 /src/lib
parentfe104c2364de35bcdb2a10ca469cd25c12b04289 (diff)
downloadcoinos-ui-e39bf4f977bba2952a7de4a307b14c5ec862a3a7.tar.gz
coinos-ui-e39bf4f977bba2952a7de4a307b14c5ec862a3a7.zip
passkey auth
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/ark.ts47
-rw-r--r--src/lib/passkey.ts14
-rw-r--r--src/lib/utils.ts38
3 files changed, 68 insertions, 31 deletions
diff --git a/src/lib/ark.ts b/src/lib/ark.ts
index a90236da..a3939a40 100644
--- a/src/lib/ark.ts
+++ b/src/lib/ark.ts
@@ -52,10 +52,13 @@ export const getVtxos = async () => {
return wallet.getVtxos({ spendableOnly: false });
};
-export const syncTransactions = async (aid: string) => {
+const _syncTransactions = async (aid: string) => {
const wallet = await getWallet();
if (!wallet) return;
+ const walletAddr = await wallet.getAddress();
+ console.log("arkSync: wallet address", walletAddr);
+
const transactions: any[] = [];
const history = await wallet.getTransactionHistory();
@@ -91,9 +94,17 @@ export const syncTransactions = async (aid: string) => {
});
}
- if (!transactions.length) return;
+ const bal = await wallet.getBalance();
+ const balance = (bal.available || 0) + (bal.preconfirmed || 0);
- return post("/post/ark/sync", { transactions, aid });
+ return post("/post/ark/sync", { transactions, aid, balance });
+};
+
+export const syncTransactions = async (aid: string) => {
+ if (arkSending) return;
+ const p = _syncTransactions(aid);
+ syncPromise = p;
+ try { return await p; } finally { if (syncPromise === p) syncPromise = null; }
};
export const vtxoKey = (v: { txid: string; vout: number }) => `${v.txid}:${v.vout}`;
@@ -113,8 +124,9 @@ export const addProcessedVtxos = (keys: string[]) => {
localStorage.setItem("processedArkVtxos", JSON.stringify(arr));
};
-// Flag to suppress notifyIncomingFunds during sends
+// Flag to suppress sync during sends
export let arkSending = false;
+let syncPromise: Promise<any> | null = null;
const withTimeout = <T>(promise: Promise<T>, ms: number, label: string) =>
Promise.race([
@@ -128,23 +140,22 @@ export const sendArk = async (address: string, amount: number) => {
const wallet = await getWallet();
if (!wallet) throw new Error("Ark wallet not available");
- // Refresh expired VTXOs before attempting to send
- const balance = await wallet.getBalance();
- if (balance.available < amount && balance.recoverable > 0) {
- console.log("ark send: refreshing expired VTXOs before send");
- await refresh();
- }
-
- const freshBalance = await wallet.getBalance();
- if (freshBalance.available < amount) {
- throw new Error(`Insufficient funds: ${freshBalance.available} available, need ${amount}`);
- }
-
- // Suppress incoming notifications during send to prevent
- // change VTXOs from being credited as incoming payments
+ // Suppress sync polling and wait for any in-flight sync to finish
arkSending = true;
+ if (syncPromise) await syncPromise.catch(() => {});
try {
+ // Refresh expired VTXOs before attempting to send
+ const balance = await wallet.getBalance();
+ if (balance.available < amount && balance.recoverable > 0) {
+ console.log("ark send: refreshing expired VTXOs before send");
+ await refresh();
+ }
+
+ const freshBalance = await wallet.getBalance();
+ if (freshBalance.available < amount) {
+ throw new Error(`Insufficient funds: ${freshBalance.available} available, need ${amount}`);
+ }
let txid;
try {
txid = await withTimeout(
diff --git a/src/lib/passkey.ts b/src/lib/passkey.ts
new file mode 100644
index 00000000..228b254e
--- /dev/null
+++ b/src/lib/passkey.ts
@@ -0,0 +1,14 @@
+import { startRegistration, startAuthentication } from "@simplewebauthn/browser";
+import { post } from "$lib/utils";
+
+export async function registerPasskey() {
+ const options = await post("/passkey/register/options", {});
+ const registration = await startRegistration({ optionsJSON: options });
+ await post("/passkey/register/verify", registration);
+}
+
+export async function loginWithPasskey() {
+ const { challengeId, ...options } = await post("/passkey/login/options", {});
+ const authentication = await startAuthentication({ optionsJSON: options });
+ return { credential: authentication, challengeId };
+}
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index a96db03d..28b63038 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -508,29 +508,41 @@ export const register = async (
};
if (ip) headers["cf-connecting-ip"] = ip;
- let sk;
+ let sk, token;
try {
- ({ sk } = await post("/register", { user }, headers));
+ ({ sk, token } = await post("/register", { user }, headers));
} catch (e) {
({ message: error } = e as Error);
}
- try {
- await login(user, cookies, ip, host, extraHeaders);
+ if (user.password) {
+ try {
+ await login(user, cookies, ip, host, extraHeaders);
+ error = null;
+ } catch (e) {
+ const { message } = e as Error;
+ error ||= message;
+ }
+ } else if (token) {
+ const maxAge = 380 * 24 * 60 * 60;
+ const expires = new Date();
+ expires.setSeconds(expires.getSeconds() + maxAge);
+ const opts = { path: "/", expires };
+ cookies.set("username", user.username, opts);
+ cookies.set("token", token, opts);
error = null;
- } catch (e) {
- const { message } = e as Error;
- error ||= message;
}
const expires = new Date();
expires.setSeconds(expires.getSeconds() + 21000000);
- cookies.set("sk", sk, {
- path: "/",
- expires,
- httpOnly: false,
- sameSite: "lax",
- });
+ if (sk) {
+ cookies.set("sk", sk, {
+ path: "/",
+ expires,
+ httpOnly: false,
+ sameSite: "lax",
+ });
+ }
if (error) return svelteFail(400, { error });
redirect(303, loginRedirect || `/${user.username}`);