@tollbooth-dpyc/web 0.1.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 (52) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +69 -0
  3. package/dist/avatar.d.ts +20 -0
  4. package/dist/avatar.js +54 -0
  5. package/dist/client.d.ts +42 -0
  6. package/dist/client.js +169 -0
  7. package/dist/config.d.ts +39 -0
  8. package/dist/config.js +33 -0
  9. package/dist/debugLog.d.ts +18 -0
  10. package/dist/debugLog.js +29 -0
  11. package/dist/identity.d.ts +43 -0
  12. package/dist/identity.js +106 -0
  13. package/dist/index.d.ts +12 -0
  14. package/dist/index.js +11 -0
  15. package/dist/inlineProof.d.ts +10 -0
  16. package/dist/inlineProof.js +21 -0
  17. package/dist/nostrProfile.d.ts +13 -0
  18. package/dist/nostrProfile.js +54 -0
  19. package/dist/pagesProxy.d.ts +22 -0
  20. package/dist/pagesProxy.js +54 -0
  21. package/dist/profilePresentation.d.ts +17 -0
  22. package/dist/profilePresentation.js +32 -0
  23. package/dist/react/Avatar.d.ts +6 -0
  24. package/dist/react/Avatar.js +7 -0
  25. package/dist/react/AvatarPicker.d.ts +9 -0
  26. package/dist/react/AvatarPicker.js +64 -0
  27. package/dist/react/NostrProfilePanel.d.ts +8 -0
  28. package/dist/react/NostrProfilePanel.js +116 -0
  29. package/dist/react/NpubGate.d.ts +16 -0
  30. package/dist/react/NpubGate.js +159 -0
  31. package/dist/react/WalletCard.d.ts +11 -0
  32. package/dist/react/WalletCard.js +82 -0
  33. package/dist/react/index.d.ts +6 -0
  34. package/dist/react/index.js +6 -0
  35. package/dist/react/ui.d.ts +9 -0
  36. package/dist/react/ui.js +9 -0
  37. package/dist/react/useSession.d.ts +19 -0
  38. package/dist/react/useSession.js +39 -0
  39. package/dist/sessionNsec.d.ts +23 -0
  40. package/dist/sessionNsec.js +59 -0
  41. package/dist/signInSituation.d.ts +22 -0
  42. package/dist/signInSituation.js +42 -0
  43. package/dist/signedIn.d.ts +26 -0
  44. package/dist/signedIn.js +26 -0
  45. package/dist/standardTools.d.ts +134 -0
  46. package/dist/standardTools.js +68 -0
  47. package/dist/storage.d.ts +14 -0
  48. package/dist/storage.js +46 -0
  49. package/dist/toolResult.d.ts +37 -0
  50. package/dist/toolResult.js +60 -0
  51. package/package.json +60 -0
  52. package/theme.css +47 -0
@@ -0,0 +1,26 @@
1
+ /**
2
+ * What "signed in" means. One definition, because there were two.
3
+ *
4
+ * An npub is public — it is on every note its owner ever wrote — so the npub
5
+ * alone is a claim, never a credential. A shell that read "we know an npub" as
6
+ * "signed in" let anyone who asked for a DM and never answered it in, as
7
+ * themselves, unproven. Two things can back the claim:
8
+ *
9
+ * - a **cached DM proof**, won by answering a Nostr challenge, or
10
+ * - a **session nsec** in this tab, which signs a fresh proof per call — and
11
+ * only counts for the npub it actually derives to.
12
+ *
13
+ * Pure, so it is testable without a browser.
14
+ */
15
+ export interface Claim {
16
+ /** The npub the interface is showing, proven or not. */
17
+ npub: string;
18
+ /** A cached DM proof token, if one was won and has not lapsed. */
19
+ proof: string;
20
+ /** The npub this tab's session key derives to, if it holds one. */
21
+ sessionNpub: string | null;
22
+ }
23
+ /** True only when the claimed npub is backed by something. */
24
+ export declare function isProven({ npub, proof, sessionNpub }: Claim): boolean;
25
+ /** True when this tab can sign a fresh proof for the npub it is claiming. */
26
+ export declare function canSignFor({ npub, sessionNpub }: Claim): boolean;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * What "signed in" means. One definition, because there were two.
3
+ *
4
+ * An npub is public — it is on every note its owner ever wrote — so the npub
5
+ * alone is a claim, never a credential. A shell that read "we know an npub" as
6
+ * "signed in" let anyone who asked for a DM and never answered it in, as
7
+ * themselves, unproven. Two things can back the claim:
8
+ *
9
+ * - a **cached DM proof**, won by answering a Nostr challenge, or
10
+ * - a **session nsec** in this tab, which signs a fresh proof per call — and
11
+ * only counts for the npub it actually derives to.
12
+ *
13
+ * Pure, so it is testable without a browser.
14
+ */
15
+ /** True only when the claimed npub is backed by something. */
16
+ export function isProven({ npub, proof, sessionNpub }) {
17
+ if (!npub)
18
+ return false;
19
+ if (proof)
20
+ return true;
21
+ return sessionNpub === npub;
22
+ }
23
+ /** True when this tab can sign a fresh proof for the npub it is claiming. */
24
+ export function canSignFor({ npub, sessionNpub }) {
25
+ return Boolean(npub) && sessionNpub === npub;
26
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Typed wrappers for the tools every operator gets from
3
+ * `register_standard_tools` in the tollbooth-dpyc wheel.
4
+ */
5
+ export interface ServiceStatus {
6
+ operator_npub_hash?: string;
7
+ lifecycle?: string;
8
+ message?: string;
9
+ version?: string;
10
+ tollbooth_dpyc_version?: string;
11
+ service?: string;
12
+ slug?: string;
13
+ }
14
+ export declare function serviceStatus(): Promise<ServiceStatus>;
15
+ export interface NpubProofResult {
16
+ success?: boolean;
17
+ proven_npub?: string;
18
+ status?: string;
19
+ message?: string;
20
+ dpop_token?: string;
21
+ expires_in_seconds?: number;
22
+ expires_at?: string;
23
+ error?: string;
24
+ error_code?: string;
25
+ }
26
+ /**
27
+ * Step 1 of DM sign-in: a Secure Courier challenge DM to the npub. Free.
28
+ *
29
+ * `verifyAt` is the RFC 8628 verification venue — the place THIS app shows the
30
+ * session phrase. The DM names it so the human can cross-check that the code
31
+ * in the DM is the one on their own screen; an impostor firing the same tool
32
+ * from elsewhere cannot make the patron's open tab show the attacker's code.
33
+ */
34
+ export declare function requestNpubProof(patronNpub: string, verifyAt?: string, reason?: string): Promise<NpubProofResult>;
35
+ /**
36
+ * Step 2: drain DMs for the signed reply to step 1. Call only after the human
37
+ * says they replied — it is destructive, so never poll it.
38
+ */
39
+ export declare function receiveNpubProof(patronNpub: string, dpopToken: string): Promise<NpubProofResult>;
40
+ export interface ProofStatusResult {
41
+ success?: boolean;
42
+ status?: "valid" | "expired" | "unknown" | string;
43
+ expires_in_seconds?: number | null;
44
+ error?: string;
45
+ error_code?: string;
46
+ }
47
+ export declare function checkProofStatus(patronNpub: string, dpopToken: string): Promise<ProofStatusResult>;
48
+ export interface CheckBalanceResult {
49
+ success?: boolean;
50
+ balance_api_sats?: number;
51
+ total_deposited_api_sats?: number;
52
+ total_consumed_api_sats?: number;
53
+ next_expiration_iso?: string;
54
+ expiring_within_24h_sats?: number;
55
+ error?: string;
56
+ error_code?: string;
57
+ }
58
+ export declare function checkBalance(): Promise<CheckBalanceResult>;
59
+ export interface CheckPriceResult {
60
+ success: boolean;
61
+ tool_id?: string;
62
+ base_cost_api_sats?: number;
63
+ effective_cost_api_sats?: number;
64
+ error?: string;
65
+ error_code?: string;
66
+ }
67
+ /**
68
+ * The fare for one tool in api_sats, as the live pricing model resolves it,
69
+ * constraints included — keyed by the tool's frozen UUID. Null when unreadable:
70
+ * show the answer without a price rather than invent one.
71
+ */
72
+ export declare function checkPrice(toolId: string): Promise<number | null>;
73
+ export interface PurchaseCreditsResult {
74
+ success?: boolean;
75
+ invoice_id?: string;
76
+ checkout_link?: string;
77
+ lightning_invoice?: string;
78
+ payment_request?: string;
79
+ expires_at?: string;
80
+ amount_sats?: number;
81
+ error?: string;
82
+ error_code?: string;
83
+ }
84
+ export declare function purchaseCredits(sats: number): Promise<PurchaseCreditsResult>;
85
+ export interface CheckPaymentResult {
86
+ success?: boolean;
87
+ status?: "New" | "Processing" | "Settled" | "Expired" | "Invalid" | string;
88
+ message?: string;
89
+ invoice_id?: string;
90
+ credits_granted?: number;
91
+ balance_api_sats?: number;
92
+ error?: string;
93
+ error_code?: string;
94
+ }
95
+ export declare function checkPayment(invoiceId: string): Promise<CheckPaymentResult>;
96
+ export interface AccountStatementResult {
97
+ success?: boolean;
98
+ balance_api_sats?: number;
99
+ total_deposited_api_sats?: number;
100
+ total_consumed_api_sats?: number;
101
+ total_expired_api_sats?: number;
102
+ today_usage?: Record<string, {
103
+ calls: number;
104
+ api_sats: number;
105
+ }>;
106
+ error?: string;
107
+ }
108
+ export declare function getAccountStatement(days?: number): Promise<AccountStatementResult>;
109
+ export interface Kind0 {
110
+ name?: string;
111
+ display_name?: string;
112
+ about?: string;
113
+ picture?: string;
114
+ banner?: string;
115
+ nip05?: string;
116
+ website?: string;
117
+ lud16?: string;
118
+ }
119
+ export interface GetNostrProfileResult {
120
+ success: boolean;
121
+ npub?: string;
122
+ profile?: Kind0;
123
+ error?: string;
124
+ }
125
+ export declare function getNostrProfile(npub: string): Promise<GetNostrProfileResult>;
126
+ export interface PublishNostrProfileResult {
127
+ success: boolean;
128
+ ok?: number;
129
+ total?: number;
130
+ errors?: string[];
131
+ error?: string;
132
+ }
133
+ /** Relay a kind-0 the BROWSER signed. The wheel verifies it and fans it out. */
134
+ export declare function publishNostrProfile(npub: string, signedEvent: string): Promise<PublishNostrProfileResult>;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Typed wrappers for the tools every operator gets from
3
+ * `register_standard_tools` in the tollbooth-dpyc wheel.
4
+ */
5
+ import { callTool } from "./client.js";
6
+ export function serviceStatus() {
7
+ return callTool("service_status", {}, { bestEffort: true });
8
+ }
9
+ /**
10
+ * Step 1 of DM sign-in: a Secure Courier challenge DM to the npub. Free.
11
+ *
12
+ * `verifyAt` is the RFC 8628 verification venue — the place THIS app shows the
13
+ * session phrase. The DM names it so the human can cross-check that the code
14
+ * in the DM is the one on their own screen; an impostor firing the same tool
15
+ * from elsewhere cannot make the patron's open tab show the attacker's code.
16
+ */
17
+ export function requestNpubProof(patronNpub, verifyAt, reason) {
18
+ return callTool("request_npub_proof", {
19
+ patron_npub: patronNpub,
20
+ ...(verifyAt ? { verify_at: verifyAt } : {}),
21
+ ...(reason ? { reason } : {}),
22
+ });
23
+ }
24
+ /**
25
+ * Step 2: drain DMs for the signed reply to step 1. Call only after the human
26
+ * says they replied — it is destructive, so never poll it.
27
+ */
28
+ export function receiveNpubProof(patronNpub, dpopToken) {
29
+ return callTool("receive_npub_proof", {
30
+ patron_npub: patronNpub,
31
+ dpop_token: dpopToken,
32
+ });
33
+ }
34
+ export function checkProofStatus(patronNpub, dpopToken) {
35
+ return callTool("check_proof_status", { patron_npub: patronNpub, dpop_token: dpopToken }, { bestEffort: true });
36
+ }
37
+ export function checkBalance() {
38
+ return callTool("check_balance", {});
39
+ }
40
+ /**
41
+ * The fare for one tool in api_sats, as the live pricing model resolves it,
42
+ * constraints included — keyed by the tool's frozen UUID. Null when unreadable:
43
+ * show the answer without a price rather than invent one.
44
+ */
45
+ export async function checkPrice(toolId) {
46
+ const r = await callTool("check_price", { tool_id: toolId });
47
+ const v = r.effective_cost_api_sats ?? r.base_cost_api_sats;
48
+ return typeof v === "number" ? v : null;
49
+ }
50
+ export function purchaseCredits(sats) {
51
+ return callTool("purchase_credits", { amount_sats: sats });
52
+ }
53
+ export function checkPayment(invoiceId) {
54
+ return callTool("check_payment", { invoice_id: invoiceId });
55
+ }
56
+ export function getAccountStatement(days = 30) {
57
+ return callTool("account_statement", { days });
58
+ }
59
+ export function getNostrProfile(npub) {
60
+ return callTool("get_nostr_profile", { npub });
61
+ }
62
+ /** Relay a kind-0 the BROWSER signed. The wheel verifies it and fans it out. */
63
+ export function publishNostrProfile(npub, signedEvent) {
64
+ return callTool("publish_nostr_profile", {
65
+ npub,
66
+ signed_event: signedEvent,
67
+ });
68
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * localStorage, read as the uncertainty it is.
3
+ *
4
+ * It is absent when a page is rendered off a browser, and merely touching it
5
+ * raises in a browser with site data blocked or in some privacy modes. An
6
+ * identity helper that takes the page down over a setting the visitor chose is
7
+ * not worth having, so a failure reads as "nothing stored" and the app carries
8
+ * on asking them to sign in.
9
+ */
10
+ /** A key under this site's prefix, e.g. "chart:patron_npub:v1". */
11
+ export declare function key(name: string): string;
12
+ export declare function readStored(name: string): string;
13
+ export declare function writeStored(name: string, value: string): void;
14
+ export declare function removeStored(name: string): void;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * localStorage, read as the uncertainty it is.
3
+ *
4
+ * It is absent when a page is rendered off a browser, and merely touching it
5
+ * raises in a browser with site data blocked or in some privacy modes. An
6
+ * identity helper that takes the page down over a setting the visitor chose is
7
+ * not worth having, so a failure reads as "nothing stored" and the app carries
8
+ * on asking them to sign in.
9
+ */
10
+ import { tollboothConfig } from "./config.js";
11
+ function store() {
12
+ try {
13
+ return globalThis.localStorage ?? null;
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ /** A key under this site's prefix, e.g. "chart:patron_npub:v1". */
20
+ export function key(name) {
21
+ return `${tollboothConfig().storagePrefix}:${name}`;
22
+ }
23
+ export function readStored(name) {
24
+ try {
25
+ return store()?.getItem(key(name)) ?? "";
26
+ }
27
+ catch {
28
+ return "";
29
+ }
30
+ }
31
+ export function writeStored(name, value) {
32
+ try {
33
+ store()?.setItem(key(name), value);
34
+ }
35
+ catch {
36
+ /* a visitor who blocks site data simply signs in again next time */
37
+ }
38
+ }
39
+ export function removeStored(name) {
40
+ try {
41
+ store()?.removeItem(key(name));
42
+ }
43
+ catch {
44
+ /* nothing to remove from a store we cannot reach */
45
+ }
46
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Turning an MCP tool result into what a page wants. Pure, so it is tested
3
+ * without a server.
4
+ */
5
+ export interface ContentBlock {
6
+ type: string;
7
+ text?: string;
8
+ data?: string;
9
+ mimeType?: string;
10
+ }
11
+ export interface RawToolResult {
12
+ isError?: boolean;
13
+ content?: ContentBlock[];
14
+ structuredContent?: unknown;
15
+ }
16
+ export interface ImageBlock {
17
+ mimeType: string;
18
+ /** Base64, as the server sent it. */
19
+ data: string;
20
+ }
21
+ export interface ReadResult {
22
+ /** The structured answer, else the first text block parsed as JSON, else that text. */
23
+ payload: unknown;
24
+ images: ImageBlock[];
25
+ }
26
+ /** The text a failed call carries, or a plain fallback. */
27
+ export declare function errorText(result: RawToolResult): string;
28
+ export declare function readToolResult(result: RawToolResult): ReadResult;
29
+ /**
30
+ * A soft proof failure: `{success: false, error_code: proof_required |
31
+ * proof_refresh_needed}` with no isError flag. The wheel's codes are lowercase
32
+ * snake_case; an earlier uppercase comparison never matched, so the bounce
33
+ * silently never fired. Normalised here, once.
34
+ */
35
+ export declare function proofBounceMessage(payload: unknown): string | null;
36
+ /** Whether a payload reports failure, for the debug log's colouring. */
37
+ export declare function looksFailed(payload: unknown): boolean;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Turning an MCP tool result into what a page wants. Pure, so it is tested
3
+ * without a server.
4
+ */
5
+ /** The text a failed call carries, or a plain fallback. */
6
+ export function errorText(result) {
7
+ return (result.content ?? [])
8
+ .filter((b) => b.type === "text" && typeof b.text === "string")
9
+ .map((b) => String(b.text))
10
+ .join("\n") || "Tool call failed";
11
+ }
12
+ export function readToolResult(result) {
13
+ const content = result.content ?? [];
14
+ const images = content
15
+ .filter((b) => b.type === "image" && typeof b.data === "string")
16
+ .map((b) => ({ mimeType: b.mimeType || "image/png", data: String(b.data) }));
17
+ let payload;
18
+ if (result.structuredContent !== undefined) {
19
+ payload = result.structuredContent;
20
+ }
21
+ else {
22
+ const text = content.find((b) => b.type === "text");
23
+ if (text) {
24
+ const raw = String(text.text ?? "");
25
+ try {
26
+ payload = JSON.parse(raw);
27
+ }
28
+ catch {
29
+ payload = raw;
30
+ }
31
+ }
32
+ else {
33
+ payload = images.length ? {} : result;
34
+ }
35
+ }
36
+ return { payload, images };
37
+ }
38
+ /**
39
+ * A soft proof failure: `{success: false, error_code: proof_required |
40
+ * proof_refresh_needed}` with no isError flag. The wheel's codes are lowercase
41
+ * snake_case; an earlier uppercase comparison never matched, so the bounce
42
+ * silently never fired. Normalised here, once.
43
+ */
44
+ export function proofBounceMessage(payload) {
45
+ if (!payload || typeof payload !== "object")
46
+ return null;
47
+ const p = payload;
48
+ const code = String(p.error_code ?? "").toLowerCase();
49
+ if (p.success === false && (code === "proof_required" || code === "proof_refresh_needed")) {
50
+ return String(p.error ?? "Sign-in required.");
51
+ }
52
+ return null;
53
+ }
54
+ /** Whether a payload reports failure, for the debug log's colouring. */
55
+ export function looksFailed(payload) {
56
+ if (!payload || typeof payload !== "object")
57
+ return false;
58
+ const p = payload;
59
+ return p.success === false || p.ok === false || Boolean(p.error);
60
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@tollbooth-dpyc/web",
3
+ "version": "0.1.0",
4
+ "description": "Shared browser SDK for Tollbooth DPYC operator front ends: npub sign-in, proofs, MCP client, profile and wallet",
5
+ "license": "Apache-2.0",
6
+ "author": "Lonnie VanZandt",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/lonniev/tollbooth-web.git"
10
+ },
11
+ "type": "module",
12
+ "sideEffects": [
13
+ "*.css"
14
+ ],
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "default": "./dist/index.js"
19
+ },
20
+ "./react": {
21
+ "types": "./dist/react/index.d.ts",
22
+ "default": "./dist/react/index.js"
23
+ },
24
+ "./pages-proxy": {
25
+ "types": "./dist/pagesProxy.d.ts",
26
+ "default": "./dist/pagesProxy.js"
27
+ },
28
+ "./theme.css": "./theme.css"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "theme.css",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsc -p tsconfig.json",
38
+ "typecheck": "tsc -p tsconfig.json --noEmit",
39
+ "test": "node --experimental-strip-types --test 'src/**/*.test.ts'",
40
+ "prepublishOnly": "npm test && npm run build"
41
+ },
42
+ "peerDependencies": {
43
+ "@modelcontextprotocol/sdk": "^1.29.0",
44
+ "lucide-react": ">=1.0.0",
45
+ "nostr-tools": "^2.23.0",
46
+ "react": "^19.0.0"
47
+ },
48
+ "devDependencies": {
49
+ "@modelcontextprotocol/sdk": "^1.30.1",
50
+ "@types/react": "^19.2.17",
51
+ "lucide-react": "^1.47.0",
52
+ "nostr-tools": "^2.25.2",
53
+ "react": "^19.3.0",
54
+ "typescript": "^7.0.2"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public",
58
+ "provenance": true
59
+ }
60
+ }
package/theme.css ADDED
@@ -0,0 +1,47 @@
1
+ /*
2
+ * Theme tokens for @tollbooth-dpyc/web components.
3
+ *
4
+ * Components use only these variables for colour, so a site re-skins them by
5
+ * redefining the variables after importing this file. Tailwind must also scan
6
+ * the package for class names:
7
+ *
8
+ * @import "tailwindcss";
9
+ * @import "@tollbooth-dpyc/web/theme.css";
10
+ * @source "../node_modules/@tollbooth-dpyc/web/dist";
11
+ */
12
+
13
+ :root {
14
+ --tb-accent: #2f6fde;
15
+ --tb-on-accent: #ffffff;
16
+ --tb-surface: #ffffff;
17
+ --tb-surface-2: #f3f5f8;
18
+ --tb-ink: #14181f;
19
+ --tb-muted: #5b6573;
20
+ --tb-line: #d7dce3;
21
+ --tb-ok: #1f7a4a;
22
+ --tb-warn-bg: #fff7e6;
23
+ --tb-warn-line: #f0d08a;
24
+ --tb-warn-ink: #7a5200;
25
+ --tb-err-bg: #fdecec;
26
+ --tb-err-line: #f2b8b8;
27
+ --tb-err-ink: #9b1c1c;
28
+ }
29
+
30
+ @media (prefers-color-scheme: dark) {
31
+ :root:not([data-theme="light"]) {
32
+ --tb-accent: #7aa7ff;
33
+ --tb-on-accent: #0b1220;
34
+ --tb-surface: #151a21;
35
+ --tb-surface-2: #1d232c;
36
+ --tb-ink: #e6ebf2;
37
+ --tb-muted: #9aa5b4;
38
+ --tb-line: #2b333e;
39
+ --tb-ok: #5fd39a;
40
+ --tb-warn-bg: #2a2110;
41
+ --tb-warn-line: #5c4717;
42
+ --tb-warn-ink: #f2cf7a;
43
+ --tb-err-bg: #2c1515;
44
+ --tb-err-line: #5e2525;
45
+ --tb-err-ink: #f4a3a3;
46
+ }
47
+ }