@bwalletx/connect 0.4.1 → 0.5.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/assets/protocol/401.jpg +0 -0
  3. package/assets/protocol/401.mp4 +0 -0
  4. package/assets/protocol/402.jpg +0 -0
  5. package/assets/protocol/402.mp4 +0 -0
  6. package/assets/protocol/403.jpg +0 -0
  7. package/assets/protocol/403.mp4 +0 -0
  8. package/dist/access-Z6xQwKVF.d.cts +118 -0
  9. package/dist/access-Z6xQwKVF.d.ts +118 -0
  10. package/dist/bwalletx-connect.global.js +17 -17
  11. package/dist/bwalletx-connect.global.js.map +1 -1
  12. package/dist/{chunk-NNMUSWPG.js → chunk-CVLXFNAG.js} +123 -1
  13. package/dist/chunk-CVLXFNAG.js.map +1 -0
  14. package/dist/{chunk-FUQ46RJV.js → chunk-GF6GUDZL.js} +2 -2
  15. package/dist/chunk-MWD4BCO5.js +182 -0
  16. package/dist/chunk-MWD4BCO5.js.map +1 -0
  17. package/dist/{chunk-7NAKOJGZ.js → chunk-QRXFL66W.js} +220 -10
  18. package/dist/chunk-QRXFL66W.js.map +1 -0
  19. package/dist/element.cjs +209 -0
  20. package/dist/element.cjs.map +1 -1
  21. package/dist/element.js +2 -1
  22. package/dist/element.js.map +1 -1
  23. package/dist/index.cjs +292 -11
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +44 -1
  26. package/dist/index.d.ts +44 -1
  27. package/dist/index.js +30 -4
  28. package/dist/index.js.map +1 -1
  29. package/dist/next.cjs +377 -9
  30. package/dist/next.cjs.map +1 -1
  31. package/dist/next.d.cts +52 -2
  32. package/dist/next.d.ts +52 -2
  33. package/dist/next.js +92 -1
  34. package/dist/next.js.map +1 -1
  35. package/dist/react.cjs +209 -0
  36. package/dist/react.cjs.map +1 -1
  37. package/dist/react.js +3 -2
  38. package/dist/react.js.map +1 -1
  39. package/dist/server.cjs +406 -9
  40. package/dist/server.cjs.map +1 -1
  41. package/dist/server.d.cts +111 -1
  42. package/dist/server.d.ts +111 -1
  43. package/dist/server.js +47 -1
  44. package/package.json +10 -9
  45. package/dist/chunk-7NAKOJGZ.js.map +0 -1
  46. package/dist/chunk-NNMUSWPG.js.map +0 -1
  47. /package/dist/{chunk-FUQ46RJV.js.map → chunk-GF6GUDZL.js.map} +0 -0
package/dist/next.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/next.ts"],"sourcesContent":["/**\n * Next.js App Router adapter. Route handlers use standard Request/Response, so they also work in\n * any Fetch-API server (Hono, Remix, Bun, Workers).\n *\n * // lib/bwalletx.ts\n * export const auth = createBwalletXAuth({ origin: process.env.APP_ORIGIN!, store: pgStore({ query }) });\n * // app/api/bwalletx/challenge/route.ts → export const POST = auth.challenge;\n * // app/api/bwalletx/verify/route.ts → export const POST = auth.verify;\n * // app/api/bwalletx/logout/route.ts → export const POST = auth.logout;\n * // app/api/bwalletx/session/route.ts → export const GET = auth.session;\n * // server component: const s = await auth.getSession();\n */\nimport { IdentityLinkConflictError } from './link.js';\nimport { createChallenge, originAllowed, verifySignIn, type NonceStore } from './server.js';\nimport { assertSecret, readCookie, serializeCookie, signSession, verifySession, type SessionPayload } from './session.js';\n\nexport type { SessionPayload } from './session.js';\n\nexport interface VerifiedExtra {\n /** Your user id (e.g. from signInOrLink). Stored in the session as `uid`. */\n uid?: string;\n handle?: string;\n paymail?: string;\n [claim: string]: unknown;\n}\n\nexport interface BwalletXAuthOptions {\n /** YOUR origin from config, e.g. \"https://example.com\". Never from the request. */\n origin: string;\n /** A shared nonce store: redisStore or pgStore. memoryStore only in development. */\n store: NonceStore;\n /** HMAC secret, 32+ chars. Default: process.env[secretEnv]. There is no fallback; requests throw without it. */\n secret?: string;\n /** Env var holding the secret. Default 'BWALLETX_SESSION_SECRET'. */\n secretEnv?: string;\n /** Cookie name. Default 'bwx_session'. */\n cookieName?: string;\n /** Session lifetime in seconds. Default 7 days. */\n maxAge?: number;\n /** Default: true when origin is https. */\n secureCookie?: boolean;\n sameSite?: 'Lax' | 'Strict' | 'None';\n cookieDomain?: string;\n /**\n * Runs after the signature is verified, before the cookie is set: link to your user, look up a\n * handle. Throw IdentityLinkConflictError to answer 409; return false to refuse (403).\n */\n onVerified?: (identityKey: string, req: Request) => Promise<VerifiedExtra | void | false> | VerifiedExtra | void | false;\n}\n\nexport interface BwalletXAuth {\n challenge(req: Request): Promise<Response>;\n verify(req: Request): Promise<Response>;\n logout(req: Request): Promise<Response>;\n session(req: Request): Promise<Response>;\n /** The session, or null. With no argument it reads cookies() from next/headers. */\n getSession(source?: Request | Headers | string | null): Promise<SessionPayload | null>;\n /** Make a cookie for an identity you verified yourself (e.g. after account linking elsewhere). */\n sessionCookie(claims: Omit<SessionPayload, 'iat' | 'exp'>): Promise<string>;\n clearCookie(): string;\n}\n\nconst json = (body: unknown, status = 200, headers: Record<string, string> = {}) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json', 'cache-control': 'no-store', ...headers } });\n\nasync function body(req: Request): Promise<Record<string, unknown> | null> {\n try {\n const b = await req.json();\n return b && typeof b === 'object' ? (b as Record<string, unknown>) : null;\n } catch {\n return null;\n }\n}\n\nexport function createBwalletXAuth(o: BwalletXAuthOptions): BwalletXAuth {\n if (!o.store) throw new Error('createBwalletXAuth needs a nonce store (redisStore, pgStore; memoryStore for dev)');\n const cookieName = o.cookieName ?? 'bwx_session';\n const maxAge = o.maxAge ?? 7 * 24 * 3600;\n const secure = o.secureCookie ?? o.origin.startsWith('https:');\n const secret = () => {\n const s = o.secret ?? (typeof process !== 'undefined' ? process.env?.[o.secretEnv ?? 'BWALLETX_SESSION_SECRET'] : undefined);\n assertSecret(s);\n return s;\n };\n const cookie = (v: string, age: number) => serializeCookie(v, { name: cookieName, maxAge: age, secure, sameSite: o.sameSite, domain: o.cookieDomain });\n const badOrigin = (req: Request) => !originAllowed(req.headers.get('origin'), o.origin);\n\n const fromSource = async (source?: Request | Headers | string | null): Promise<string | null> => {\n if (typeof source === 'string') return readCookie(source, cookieName);\n if (source instanceof Headers) return readCookie(source.get('cookie'), cookieName);\n if (source && typeof (source as Request).headers?.get === 'function') return readCookie((source as Request).headers.get('cookie'), cookieName);\n // Server components / route handlers: Next's cookies(). Imported lazily so the package does not require Next.\n const { cookies } = (await import('next/headers')) as { cookies: () => unknown };\n const jar = (await cookies()) as { get(n: string): { value: string } | undefined };\n return jar.get(cookieName)?.value ?? null;\n };\n\n const auth: BwalletXAuth = {\n async challenge(req) {\n secret();\n if (badOrigin(req)) return json({ error: 'origin not allowed' }, 403);\n const b = await body(req);\n try {\n return json(await createChallenge({ origin: o.origin, identityKey: b?.identityKey, store: o.store }));\n } catch (e) {\n return json({ error: (e as Error).message }, 400);\n }\n },\n async verify(req) {\n const key = secret();\n if (badOrigin(req)) return json({ error: 'origin not allowed' }, 403);\n const b = await body(req);\n const identityKey = await verifySignIn({ origin: o.origin, identityKey: b?.identityKey, nonce: b?.nonce, signature: b?.signature, store: o.store });\n if (!identityKey) return json({ error: 'sign-in failed' }, 401);\n let extra: VerifiedExtra = {};\n try {\n const r = await o.onVerified?.(identityKey, req);\n if (r === false) return json({ error: 'not allowed' }, 403);\n if (r) extra = r;\n } catch (e) {\n if (e instanceof IdentityLinkConflictError) return json({ error: e.message, code: e.code }, 409);\n throw e;\n }\n const token = await signSession({ ...extra, sub: identityKey }, { secret: key, maxAge });\n return json({ identityKey, uid: extra.uid, handle: extra.handle, paymail: extra.paymail }, 200, { 'set-cookie': cookie(token, maxAge) });\n },\n async logout(req) {\n if (badOrigin(req)) return json({ error: 'origin not allowed' }, 403);\n return json({ ok: true }, 200, { 'set-cookie': cookie('', 0) });\n },\n async session(req) {\n const s = await auth.getSession(req);\n return s ? json({ identityKey: s.sub, uid: s.uid, handle: s.handle, paymail: s.paymail, exp: s.exp }) : json({ identityKey: null }, 200);\n },\n async getSession(source) {\n const key = secret();\n return verifySession(await fromSource(source), { secret: key });\n },\n async sessionCookie(claims) {\n return cookie(await signSession(claims, { secret: secret(), maxAge }), maxAge);\n },\n clearCookie: () => cookie('', 0),\n };\n return auth;\n}\n\nexport { IdentityLinkConflictError, linkIdentity, signInOrLink } from './link.js';\nexport { memoryStore, redisStore, pgStore, pgNonceTableSql } from './server.js';\n"],"mappings":"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","names":["body"]}
1
+ {"version":3,"sources":["../src/next.ts"],"sourcesContent":["/**\n * Next.js App Router adapter. Route handlers use standard Request/Response, so they also work in\n * any Fetch-API server (Hono, Remix, Bun, Workers).\n *\n * // lib/bwalletx.ts\n * export const auth = createBwalletXAuth({ origin: process.env.APP_ORIGIN!, store: pgStore({ query }) });\n * // app/api/bwalletx/challenge/route.ts → export const POST = auth.challenge;\n * // app/api/bwalletx/verify/route.ts → export const POST = auth.verify;\n * // app/api/bwalletx/logout/route.ts → export const POST = auth.logout;\n * // app/api/bwalletx/session/route.ts → export const GET = auth.session;\n * // server component: const s = await auth.getSession();\n */\nimport { IdentityLinkConflictError } from './link.js';\nimport { createChallenge, originAllowed, verifySignIn, type NonceStore } from './server.js';\nimport { assertSecret, readCookie, serializeCookie, signSession, verifySession, type SessionPayload } from './session.js';\n\nexport type { SessionPayload } from './session.js';\n\nexport interface VerifiedExtra {\n /** Your user id (e.g. from signInOrLink). Stored in the session as `uid`. */\n uid?: string;\n handle?: string;\n paymail?: string;\n [claim: string]: unknown;\n}\n\nexport interface BwalletXAuthOptions {\n /** YOUR origin from config, e.g. \"https://example.com\". Never from the request. */\n origin: string;\n /** A shared nonce store: redisStore or pgStore. memoryStore only in development. */\n store: NonceStore;\n /** HMAC secret, 32+ chars. Default: process.env[secretEnv]. There is no fallback; requests throw without it. */\n secret?: string;\n /** Env var holding the secret. Default 'BWALLETX_SESSION_SECRET'. */\n secretEnv?: string;\n /** Cookie name. Default 'bwx_session'. */\n cookieName?: string;\n /** Session lifetime in seconds. Default 7 days. */\n maxAge?: number;\n /** Default: true when origin is https. */\n secureCookie?: boolean;\n sameSite?: 'Lax' | 'Strict' | 'None';\n cookieDomain?: string;\n /**\n * Runs after the signature is verified, before the cookie is set: link to your user, look up a\n * handle. Throw IdentityLinkConflictError to answer 409; return false to refuse (403).\n */\n onVerified?: (identityKey: string, req: Request) => Promise<VerifiedExtra | void | false> | VerifiedExtra | void | false;\n}\n\nexport interface BwalletXAuth {\n challenge(req: Request): Promise<Response>;\n verify(req: Request): Promise<Response>;\n logout(req: Request): Promise<Response>;\n session(req: Request): Promise<Response>;\n /** The session, or null. With no argument it reads cookies() from next/headers. */\n getSession(source?: Request | Headers | string | null): Promise<SessionPayload | null>;\n /** Make a cookie for an identity you verified yourself (e.g. after account linking elsewhere). */\n sessionCookie(claims: Omit<SessionPayload, 'iat' | 'exp'>): Promise<string>;\n clearCookie(): string;\n}\n\nconst json = (body: unknown, status = 200, headers: Record<string, string> = {}) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json', 'cache-control': 'no-store', ...headers } });\n\nasync function body(req: Request): Promise<Record<string, unknown> | null> {\n try {\n const b = await req.json();\n return b && typeof b === 'object' ? (b as Record<string, unknown>) : null;\n } catch {\n return null;\n }\n}\n\nexport function createBwalletXAuth(o: BwalletXAuthOptions): BwalletXAuth {\n if (!o.store) throw new Error('createBwalletXAuth needs a nonce store (redisStore, pgStore; memoryStore for dev)');\n const cookieName = o.cookieName ?? 'bwx_session';\n const maxAge = o.maxAge ?? 7 * 24 * 3600;\n const secure = o.secureCookie ?? o.origin.startsWith('https:');\n const secret = () => {\n const s = o.secret ?? (typeof process !== 'undefined' ? process.env?.[o.secretEnv ?? 'BWALLETX_SESSION_SECRET'] : undefined);\n assertSecret(s);\n return s;\n };\n const cookie = (v: string, age: number) => serializeCookie(v, { name: cookieName, maxAge: age, secure, sameSite: o.sameSite, domain: o.cookieDomain });\n const badOrigin = (req: Request) => !originAllowed(req.headers.get('origin'), o.origin);\n\n const fromSource = async (source?: Request | Headers | string | null): Promise<string | null> => {\n if (typeof source === 'string') return readCookie(source, cookieName);\n if (source instanceof Headers) return readCookie(source.get('cookie'), cookieName);\n if (source && typeof (source as Request).headers?.get === 'function') return readCookie((source as Request).headers.get('cookie'), cookieName);\n // Server components / route handlers: Next's cookies(). Imported lazily so the package does not require Next.\n const { cookies } = (await import('next/headers')) as { cookies: () => unknown };\n const jar = (await cookies()) as { get(n: string): { value: string } | undefined };\n return jar.get(cookieName)?.value ?? null;\n };\n\n const auth: BwalletXAuth = {\n async challenge(req) {\n secret();\n if (badOrigin(req)) return json({ error: 'origin not allowed' }, 403);\n const b = await body(req);\n try {\n return json(await createChallenge({ origin: o.origin, identityKey: b?.identityKey, store: o.store }));\n } catch (e) {\n return json({ error: (e as Error).message }, 400);\n }\n },\n async verify(req) {\n const key = secret();\n if (badOrigin(req)) return json({ error: 'origin not allowed' }, 403);\n const b = await body(req);\n const identityKey = await verifySignIn({ origin: o.origin, identityKey: b?.identityKey, nonce: b?.nonce, signature: b?.signature, store: o.store });\n if (!identityKey) return json({ error: 'sign-in failed' }, 401);\n let extra: VerifiedExtra = {};\n try {\n const r = await o.onVerified?.(identityKey, req);\n if (r === false) return json({ error: 'not allowed' }, 403);\n if (r) extra = r;\n } catch (e) {\n if (e instanceof IdentityLinkConflictError) return json({ error: e.message, code: e.code }, 409);\n throw e;\n }\n const token = await signSession({ ...extra, sub: identityKey }, { secret: key, maxAge });\n return json({ identityKey, uid: extra.uid, handle: extra.handle, paymail: extra.paymail }, 200, { 'set-cookie': cookie(token, maxAge) });\n },\n async logout(req) {\n if (badOrigin(req)) return json({ error: 'origin not allowed' }, 403);\n return json({ ok: true }, 200, { 'set-cookie': cookie('', 0) });\n },\n async session(req) {\n const s = await auth.getSession(req);\n return s ? json({ identityKey: s.sub, uid: s.uid, handle: s.handle, paymail: s.paymail, exp: s.exp }) : json({ identityKey: null }, 200);\n },\n async getSession(source) {\n const key = secret();\n return verifySession(await fromSource(source), { secret: key });\n },\n async sessionCookie(claims) {\n return cookie(await signSession(claims, { secret: secret(), maxAge }), maxAge);\n },\n clearCookie: () => cookie('', 0),\n };\n return auth;\n}\n\nexport { IdentityLinkConflictError, linkIdentity, signInOrLink } from './link.js';\nexport { memoryStore, redisStore, pgStore, pgNonceTableSql } from './server.js';\n\n// ── $401/$403 access gate ──────────────────────────────────────────────────────────────────\n\nimport { ACCESS_ERROR_STATUS, AccessError, accessErrorMessage, canonicalJson, normaliseRequirement, type AccessRequirement } from './access.js';\nimport { checkTokenHolding, createAccessChallenge, verifyAccess, type AccessResolver } from './access-server.js';\n\nexport interface BwalletXAccessOptions {\n origin: string;\n store: NonceStore;\n /** The gate. From YOUR config, never the request. */\n require: AccessRequirement;\n resolver: AccessResolver;\n secret?: string;\n secretEnv?: string;\n /** Default 'bwx_access'. Use a different name per gate. */\n cookieName?: string;\n /** Cookie lifetime in seconds. Default 1 day. */\n maxAge?: number;\n /** Re-check token holdings at most this often per process (seconds). Default 300. */\n recheckEvery?: number;\n secureCookie?: boolean;\n sameSite?: 'Lax' | 'Strict' | 'None';\n cookieDomain?: string;\n}\n\nexport type AccessState =\n | { ok: true; identityKey: string; addresses: string[]; checkedAt: number }\n | { ok: false; error: AccessError['code'] | 'no_session'; message: string; have?: string | number; need?: string | number };\n\nexport interface BwalletXAccess {\n challenge(req: Request): Promise<Response>;\n verify(req: Request): Promise<Response>;\n logout(req: Request): Promise<Response>;\n /** GET: the current AccessState as JSON (re-checks holdings when stale). */\n status(req: Request): Promise<Response>;\n /** The access state for a request / cookies(). Re-checks holdings every `recheckEvery` seconds. */\n getAccess(source?: Request | Headers | string | null): Promise<AccessState>;\n readonly require: AccessRequirement;\n}\n\nconst errJson = (e: AccessError) => json(e.toJSON(), ACCESS_ERROR_STATUS[e.code]);\n\n/**\n * A token- or identity-gated area. Routes:\n * app/api/access/challenge/route.ts → export const POST = gate.challenge;\n * app/api/access/verify/route.ts → export const POST = gate.verify;\n * app/api/access/status/route.ts → export const GET = gate.status;\n * page: const a = await gate.getAccess(); if (!a.ok) show the \"get the token\" path.\n */\nexport function createBwalletXAccess(o: BwalletXAccessOptions): BwalletXAccess {\n if (!o.store) throw new Error('createBwalletXAccess needs a nonce store');\n const require = normaliseRequirement(o.require);\n const reqHash = canonicalJson(require);\n const cookieName = o.cookieName ?? 'bwx_access';\n const maxAge = o.maxAge ?? 24 * 3600;\n const recheckMs = (o.recheckEvery ?? 300) * 1000;\n const secure = o.secureCookie ?? o.origin.startsWith('https:');\n const secret = () => {\n const s = o.secret ?? (typeof process !== 'undefined' ? process.env?.[o.secretEnv ?? 'BWALLETX_SESSION_SECRET'] : undefined);\n assertSecret(s);\n return s;\n };\n const cookie = (v: string, age: number) => serializeCookie(v, { name: cookieName, maxAge: age, secure, sameSite: o.sameSite, domain: o.cookieDomain });\n const badOrigin = (req: Request) => !originAllowed(req.headers.get('origin'), o.origin);\n /** sub+addresses → last good check time. Per process; a sale ends access within recheckEvery. */\n const checked = new Map<string, number>();\n\n const readToken = async (source?: Request | Headers | string | null): Promise<string | null> => {\n if (typeof source === 'string') return readCookie(source, cookieName);\n if (source instanceof Headers) return readCookie(source.get('cookie'), cookieName);\n if (source && typeof (source as Request).headers?.get === 'function') return readCookie((source as Request).headers.get('cookie'), cookieName);\n const { cookies } = (await import('next/headers')) as { cookies: () => unknown };\n const jar = (await cookies()) as { get(n: string): { value: string } | undefined };\n return jar.get(cookieName)?.value ?? null;\n };\n\n const gate: BwalletXAccess = {\n require,\n async challenge(req) {\n secret();\n if (badOrigin(req)) return json({ error: 'bad_request', message: 'origin not allowed' }, 403);\n const b = await body(req);\n try {\n return json(await createAccessChallenge({ origin: o.origin, require, store: o.store, identityKey: b?.identityKey }));\n } catch (e) {\n return errJson(e instanceof AccessError ? e : new AccessError('bad_request', (e as Error).message));\n }\n },\n async verify(req) {\n const key = secret();\n if (badOrigin(req)) return json({ error: 'bad_request', message: 'origin not allowed' }, 403);\n const b = await body(req);\n try {\n const g = await verifyAccess({ origin: o.origin, response: b, store: o.store, resolver: o.resolver, require });\n const token = await signSession({ sub: g.identityKey, acc: reqHash, ad: g.addresses }, { secret: key, maxAge });\n checked.set(`${g.identityKey}|${g.addresses.join(',')}`, Date.now());\n return json({ ok: true, identityKey: g.identityKey, have: g.have }, 200, { 'set-cookie': cookie(token, maxAge) });\n } catch (e) {\n if (e instanceof AccessError) return errJson(e);\n throw e;\n }\n },\n async logout(req) {\n if (badOrigin(req)) return json({ error: 'bad_request', message: 'origin not allowed' }, 403);\n return json({ ok: true }, 200, { 'set-cookie': cookie('', 0) });\n },\n async status(req) {\n const a = await gate.getAccess(req);\n return json(a, 200);\n },\n async getAccess(source) {\n const s = await verifySession(await readToken(source), { secret: secret() });\n if (!s || s.acc !== reqHash) return { ok: false, error: 'no_session', message: 'Not signed in to this gate.' };\n const addresses = Array.isArray(s.ad) ? (s.ad as unknown[]).filter((x): x is string => typeof x === 'string') : [];\n const k = `${s.sub}|${addresses.join(',')}`;\n const last = checked.get(k) ?? 0;\n if (require.token && Date.now() - last >= recheckMs) {\n try {\n await checkTokenHolding(require, addresses, o.resolver);\n checked.set(k, Date.now());\n } catch (e) {\n checked.delete(k);\n const ae = e instanceof AccessError ? e : new AccessError('indexer_unavailable', (e as Error).message);\n return { ok: false, error: ae.code, message: ae.message === ae.code ? accessErrorMessage(ae.code, ae) : ae.message, have: ae.have, need: ae.need };\n }\n }\n return { ok: true, identityKey: s.sub, addresses, checkedAt: checked.get(k) ?? last };\n },\n };\n return gate;\n}\n"],"mappings":"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","names":["body","require"]}
package/dist/react.cjs CHANGED
@@ -845,6 +845,191 @@ async function buyToken(source, req) {
845
845
  return res;
846
846
  }
847
847
 
848
+ // src/access-client.ts
849
+ var import_sdk5 = require("@bsv/sdk");
850
+
851
+ // src/access.ts
852
+ var ACCESS_PROTOCOL = [2, "bwalletx access"];
853
+ var MAX_HOLDING_PROOFS = 50;
854
+ var AccessError = class extends Error {
855
+ code;
856
+ have;
857
+ need;
858
+ constructor(code, message, extra = {}) {
859
+ super(message ?? code);
860
+ this.name = "AccessError";
861
+ this.code = code;
862
+ this.have = extra.have;
863
+ this.need = extra.need;
864
+ }
865
+ toJSON() {
866
+ const b = { error: this.code, message: this.message };
867
+ if (this.have !== void 0) b.have = this.have;
868
+ if (this.need !== void 0) b.need = this.need;
869
+ return b;
870
+ }
871
+ };
872
+ function canonicalJson(v) {
873
+ if (v === null) return "null";
874
+ if (typeof v === "string" || typeof v === "boolean") return JSON.stringify(v);
875
+ if (typeof v === "number") {
876
+ if (!Number.isFinite(v)) throw new Error("canonicalJson: non-finite number");
877
+ return JSON.stringify(v);
878
+ }
879
+ if (Array.isArray(v)) return `[${v.map((x) => canonicalJson(x)).join(",")}]`;
880
+ if (typeof v === "object") {
881
+ const o = v;
882
+ const keys = Object.keys(o).filter((k) => o[k] !== void 0).sort();
883
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(",")}}`;
884
+ }
885
+ throw new Error(`canonicalJson: unsupported ${typeof v}`);
886
+ }
887
+ function challengeBytes(challenge) {
888
+ return Array.from(new TextEncoder().encode(challenge));
889
+ }
890
+ var TOKEN_ID = /^bsv21:[0-9a-f]{64}_\d{1,6}$/;
891
+ var RAW = /^[1-9]\d{0,38}$/;
892
+ function normaliseRequirement(r) {
893
+ if (!r || typeof r !== "object") throw new AccessError("bad_request", "require must be an object");
894
+ const req = r;
895
+ const out = {};
896
+ if (req.token !== void 0) {
897
+ const t = req.token;
898
+ if (!t || typeof t !== "object" || typeof t.id !== "string") throw new AccessError("bad_request", "require.token.id is required");
899
+ const id = t.id.trim().toLowerCase();
900
+ if (!TOKEN_ID.test(id)) throw new AccessError("bad_request", "require.token.id must be 'bsv21:<txid>_<vout>'");
901
+ const hasMin = t.min !== void 0;
902
+ const hasBps = t.minBps !== void 0;
903
+ if (hasMin === hasBps) throw new AccessError("bad_request", "require.token needs exactly one of min or minBps");
904
+ out.token = { id };
905
+ if (hasMin) {
906
+ if (typeof t.min !== "string" || !RAW.test(t.min)) throw new AccessError("bad_request", "require.token.min must be a positive integer string (raw units)");
907
+ out.token.min = t.min;
908
+ } else {
909
+ if (!Number.isInteger(t.minBps) || t.minBps < 1 || t.minBps > 1e4) {
910
+ throw new AccessError("bad_request", "require.token.minBps must be an integer 1..10000");
911
+ }
912
+ out.token.minBps = t.minBps;
913
+ }
914
+ }
915
+ if (req.identityLevel !== void 0) {
916
+ if (!Number.isInteger(req.identityLevel) || req.identityLevel < 1 || req.identityLevel > 10) {
917
+ throw new AccessError("bad_request", "require.identityLevel must be an integer 1..10");
918
+ }
919
+ out.identityLevel = req.identityLevel;
920
+ }
921
+ if (req.kyc !== void 0) {
922
+ if (typeof req.kyc !== "boolean") throw new AccessError("bad_request", "require.kyc must be a boolean");
923
+ if (req.kyc) out.kyc = true;
924
+ }
925
+ if (!out.token && !out.identityLevel && !out.kyc) throw new AccessError("bad_request", "require is empty");
926
+ return out;
927
+ }
928
+ function parseAccessChallenge(s) {
929
+ if (typeof s !== "string" || s.length > 4096) throw new AccessError("bad_request", "challenge must be a string");
930
+ let c;
931
+ try {
932
+ c = JSON.parse(s);
933
+ } catch {
934
+ throw new AccessError("bad_request", "challenge is not JSON");
935
+ }
936
+ if (!c || typeof c !== "object" || c.v !== 1) throw new AccessError("bad_request", "unsupported challenge version");
937
+ if (c.kind !== "401" && c.kind !== "402" && c.kind !== "403") throw new AccessError("bad_request", "bad kind");
938
+ if (typeof c.origin !== "string" || typeof c.nonce !== "string" || !/^[A-Za-z0-9_-]{16,64}$/.test(c.nonce)) {
939
+ throw new AccessError("bad_request", "bad origin or nonce");
940
+ }
941
+ if (!Number.isInteger(c.iat) || !Number.isInteger(c.exp) || c.exp <= c.iat) throw new AccessError("bad_request", "bad iat/exp");
942
+ if (c.sub !== void 0 && (typeof c.sub !== "string" || !/^0[23][0-9a-f]{64}$/.test(c.sub))) throw new AccessError("bad_request", "bad sub");
943
+ c.require = normaliseRequirement(c.require);
944
+ if (canonicalJson(c) !== s) throw new AccessError("bad_request", "challenge is not canonical");
945
+ return c;
946
+ }
947
+
948
+ // src/access-client.ts
949
+ var ONESAT = [0, "onesat"];
950
+ var LEGACY_ONESAT = [0, "p 1sat"];
951
+ function toAccessError(e) {
952
+ if (e instanceof AccessError) return e;
953
+ const err = e;
954
+ const msg = String(err?.message ?? e ?? "");
955
+ if (err?.code === "NOT_FOUND") return new AccessError("not_installed", msg);
956
+ if (/reject|denied|declin|cancel|user/i.test(msg) || err?.code === "ERR_USER_REJECTED" || err?.status === 4001) {
957
+ return new AccessError("user_rejected", msg);
958
+ }
959
+ return new AccessError("bad_request", msg || "access failed");
960
+ }
961
+ function throwIfAccessError(body) {
962
+ const b = body;
963
+ if (b && typeof b.error === "string") throw new AccessError(b.error, b.message, { have: b.have, need: b.need });
964
+ }
965
+ function hash160FromP2pkh(script) {
966
+ const m = /^76a914([0-9a-f]{40})88ac$/i.exec(script.slice(-50));
967
+ return m ? m[1].toLowerCase() : null;
968
+ }
969
+ async function proveHoldings(wallet, challenge, tokenId, basket = "bsv21") {
970
+ const bare = tokenId.replace(/^bsv21:/, "");
971
+ const list = await wallet.listOutputs({ basket, include: "locking scripts", includeCustomInstructions: true, limit: 1e4 });
972
+ const byKey = /* @__PURE__ */ new Map();
973
+ const data = challengeBytes(challenge);
974
+ for (const out of list.outputs) {
975
+ if (byKey.size >= MAX_HOLDING_PROOFS) break;
976
+ let ci = {};
977
+ try {
978
+ ci = JSON.parse(out.customInstructions ?? "{}");
979
+ } catch {
980
+ continue;
981
+ }
982
+ if (ci.id !== bare || !ci.keyID) continue;
983
+ const want = out.lockingScript ? hash160FromP2pkh(out.lockingScript) : null;
984
+ const counterparty = ci.counterparty ?? "self";
985
+ for (const protocolID of ci.protocolID ? [ci.protocolID] : [ONESAT, LEGACY_ONESAT]) {
986
+ const { publicKey } = await wallet.getPublicKey({ protocolID, keyID: ci.keyID, counterparty });
987
+ const h = import_sdk5.Utils.toHex(import_sdk5.Hash.hash160(import_sdk5.PublicKey.fromString(publicKey).encode(true)));
988
+ if (want && h !== want) continue;
989
+ const existing = byKey.get(publicKey);
990
+ if (existing) {
991
+ existing.outpoints.push(out.outpoint.replace(".", "_"));
992
+ break;
993
+ }
994
+ const { signature } = await wallet.createSignature({ protocolID, keyID: ci.keyID, counterparty, data });
995
+ byKey.set(publicKey, { pubkey: publicKey, signature: Array.from(signature), outpoints: [out.outpoint.replace(".", "_")] });
996
+ break;
997
+ }
998
+ }
999
+ return [...byKey.values()];
1000
+ }
1001
+ async function requestAccess(o) {
1002
+ let wallet;
1003
+ try {
1004
+ ({ wallet } = await connectBwalletX({ wallet: o.wallet, pairing: o.pairing, discoverMs: o.discoverMs }));
1005
+ } catch (e) {
1006
+ throw e?.code === "NOT_FOUND" ? new AccessError("not_installed") : toAccessError(e);
1007
+ }
1008
+ try {
1009
+ const { publicKey: identityKey } = await wallet.getPublicKey({ identityKey: true });
1010
+ const { challenge } = await o.challenge(identityKey);
1011
+ const c = parseAccessChallenge(challenge);
1012
+ if (c.sub && c.sub !== identityKey.toLowerCase()) throw new AccessError("wrong_account");
1013
+ if (Date.now() / 1e3 >= c.exp) throw new AccessError("expired");
1014
+ const { signature } = await wallet.createSignature({
1015
+ protocolID: ACCESS_PROTOCOL,
1016
+ keyID: c.nonce,
1017
+ counterparty: "anyone",
1018
+ data: challengeBytes(challenge)
1019
+ });
1020
+ const response = { v: 1, challenge, identityKey, signature: Array.from(signature) };
1021
+ if (c.require.token) response.holdings = await proveHoldings(wallet, challenge, c.require.token.id, o.tokenBasket);
1022
+ let verified;
1023
+ if (o.verify) {
1024
+ verified = await o.verify(response);
1025
+ throwIfAccessError(verified);
1026
+ }
1027
+ return { response, verified, wallet };
1028
+ } catch (e) {
1029
+ throw toAccessError(e);
1030
+ }
1031
+ }
1032
+
848
1033
  // src/element.ts
849
1034
  var SIGNIN_TAG = "bwalletx-signin";
850
1035
  var GET_URL = "https://bwalletx.com/get";
@@ -940,6 +1125,10 @@ async function postJson(url, body) {
940
1125
  if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
941
1126
  return data;
942
1127
  }
1128
+ async function postAccess(url, body) {
1129
+ const res = await fetch(url, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
1130
+ return res.json().catch(() => ({ error: "bad_request", message: `HTTP ${res.status}` }));
1131
+ }
943
1132
  var OBSERVED = ["size", "theme", "label", "brand", "subtitle", "loading", "disabled", "full-width", "aria-label", "get-url", "web-url"];
944
1133
  function createClass() {
945
1134
  return class BwalletxSigninElement extends HTMLElement {
@@ -1022,6 +1211,26 @@ function createClass() {
1022
1211
  if (!found) {
1023
1212
  this.#notFound = true;
1024
1213
  this.#emit("bwalletx-not-found");
1214
+ if (this.#attr("require-token")) this.#emit("bwalletx-access-denied", { code: "not_installed", message: "No wallet found" });
1215
+ return;
1216
+ }
1217
+ const requireToken = this.#attr("require-token");
1218
+ if (requireToken) {
1219
+ const verifyUrl2 = this.#attr("verify-url");
1220
+ const min = this.#attr("min");
1221
+ const minBps = this.#attr("min-bps");
1222
+ const hint = { token: { id: requireToken, ...minBps ? { minBps: Number(minBps) } : { min: min ?? "1" } } };
1223
+ try {
1224
+ const a = await requestAccess({
1225
+ wallet: found.wallet,
1226
+ challenge: (identityKey) => postJson(challengeUrl, { identityKey, require: hint }),
1227
+ verify: verifyUrl2 ? (response2) => postAccess(verifyUrl2, response2) : void 0
1228
+ });
1229
+ this.#emit("bwalletx-access-granted", { identityKey: a.response.identityKey, response: a.verified });
1230
+ } catch (e) {
1231
+ const err = e instanceof AccessError ? e : new AccessError("bad_request", String(e));
1232
+ this.#emit("bwalletx-access-denied", { code: err.code, message: err.message, have: err.have, need: err.need });
1233
+ }
1025
1234
  return;
1026
1235
  }
1027
1236
  const r = await signInWithBwalletX({ wallet: found.wallet, challenge: (identityKey) => postJson(challengeUrl, { identityKey }) });