@tollbooth-dpyc/web 1.1.0 → 1.2.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/client.d.ts CHANGED
@@ -28,6 +28,14 @@ export interface CallOptions {
28
28
  * patron out, whatever it gets back.
29
29
  */
30
30
  bestEffort?: boolean;
31
+ /**
32
+ * A background read — a badge, a poll — that must not send the patron to
33
+ * the sign-in gate. A proof bounce still throws `ProofRequiredError`, but
34
+ * the session is left as it is and `onProofExpired` is not told; the next
35
+ * call the patron makes themselves meets the lapse and re-arms sign-in.
36
+ * Opt-in: by default a bounce signs the patron out and re-arms the gate.
37
+ */
38
+ quietProof?: boolean;
31
39
  timeoutMs?: number;
32
40
  }
33
41
  export interface ToolAnswer<T> {
package/dist/client.js CHANGED
@@ -31,6 +31,7 @@ const STANDARD_BOOTSTRAP = [
31
31
  "publish_nostr_profile",
32
32
  "get_operator_onboarding_status",
33
33
  "check_authority_balance",
34
+ "get_pricing_model",
34
35
  "list_canonical_identities",
35
36
  "check_proof_status",
36
37
  "get_patron_onboarding_status",
@@ -165,6 +166,8 @@ export async function callToolWithContent(tool, args = {}, opts = {}) {
165
166
  if (!opts.bestEffort) {
166
167
  const bounced = proofBounceMessage(payload);
167
168
  if (bounced !== null) {
169
+ if (opts.quietProof)
170
+ throw new ProofRequiredError(bounced);
168
171
  // The token just refused is the same one the recent-login shortcut
169
172
  // would replay, so it goes too, or a returning patron re-bounces.
170
173
  const npub = getStoredNpub();
package/dist/debugLog.js CHANGED
@@ -28,9 +28,12 @@ const REDACTED = "[redacted]";
28
28
  const SECRET_FIELD = "(?:nsec|ncryptsec|dpop_token|proof|poison|password|passphrase|secret|token|authorization|api_?key|private_?key|client_secret|[a-z0-9_]*_(?:nsec|token|secret|password|passphrase|api_?key|private_?key))";
29
29
  // `"dpop_token":"…"`, `\"dpop_token\":\"…\"` (JSON inside a JSON string),
30
30
  // `dpop_token=…` (a query string) and `dpop_token: '…'` (a Python repr). A
31
- // quoted value is taken whole, spaces and all; a bare one up to a delimiter.
31
+ // quoted value is taken whole — spaces, escaped quotes and the other kind of
32
+ // quote included — up to its own closing quote or the end of a truncated line;
33
+ // a bare one up to a delimiter. Stopping at the first quote of either kind let
34
+ // the tail of a secret holding `'` or `\"` through.
32
35
  function fieldValue(names) {
33
- return new RegExp(`((?:\\\\*["'])?\\b${names}(?:\\\\*["'])?\\s*[:=]\\s*)(?:(\\\\*["'])[^"'\\\\]*|[^"'\\\\,}\\]&\\s]+)`, "gi");
36
+ return new RegExp(`((?:\\\\*["'])?\\b${names}(?:\\\\*["'])?\\s*[:=]\\s*)(?:(\\\\*["'])(?:\\\\.|\\\\$|(?!\\2)[^\\\\])*?(?=\\2|$)|[^"'\\\\,}\\]&\\s]+)`, "gi");
34
37
  }
35
38
  const FIELD_VALUE = fieldValue(SECRET_FIELD);
36
39
  // update_patron_credential / update_operator_credential carry the secret as
@@ -5,6 +5,14 @@
5
5
  * runtime tool name, e.g. "chart_snapshot_display"), and `created_at` (within
6
6
  * about 60 seconds of server time). The wheel verifies the Schnorr signature
7
7
  * inline — no relay round trip, no cached phrase.
8
+ *
9
+ * Every proof also carries a random `nonce` tag, exactly as the wheel's own
10
+ * `create_proof` does. Without it, two calls to the same tool in the same
11
+ * wall-clock second sign byte-identical events, their ids collide, and the
12
+ * verifier's replay guard refuses the second. The nonce is signed but inert:
13
+ * `verify_proof` reads only the `u` tag.
8
14
  */
15
+ /** 16 random bytes as 32 lowercase hex chars — Python's `secrets.token_hex(16)`. */
16
+ export declare function proofNonce(): string;
9
17
  /** The JSON-stringified signed event, exactly what `verify_proof` expects. */
10
18
  export declare function signInlineProof(runtimeToolName: string, secretKey?: Uint8Array): string;
@@ -5,16 +5,30 @@
5
5
  * runtime tool name, e.g. "chart_snapshot_display"), and `created_at` (within
6
6
  * about 60 seconds of server time). The wheel verifies the Schnorr signature
7
7
  * inline — no relay round trip, no cached phrase.
8
+ *
9
+ * Every proof also carries a random `nonce` tag, exactly as the wheel's own
10
+ * `create_proof` does. Without it, two calls to the same tool in the same
11
+ * wall-clock second sign byte-identical events, their ids collide, and the
12
+ * verifier's replay guard refuses the second. The nonce is signed but inert:
13
+ * `verify_proof` reads only the `u` tag.
8
14
  */
9
15
  import { finalizeEvent } from "nostr-tools";
10
16
  import { getSessionNsecBytes } from "./sessionNsec.js";
11
17
  const PROOF_KIND = 27235;
18
+ /** 16 random bytes as 32 lowercase hex chars — Python's `secrets.token_hex(16)`. */
19
+ export function proofNonce() {
20
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
21
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
22
+ }
12
23
  /** The JSON-stringified signed event, exactly what `verify_proof` expects. */
13
24
  export function signInlineProof(runtimeToolName, secretKey) {
14
25
  const signed = finalizeEvent({
15
26
  kind: PROOF_KIND,
16
27
  created_at: Math.floor(Date.now() / 1000),
17
- tags: [["u", runtimeToolName]],
28
+ tags: [
29
+ ["u", runtimeToolName],
30
+ ["nonce", proofNonce()],
31
+ ],
18
32
  content: "",
19
33
  }, secretKey ?? getSessionNsecBytes());
20
34
  return JSON.stringify(signed);
@@ -12,6 +12,8 @@ export interface Session {
12
12
  canSign: boolean;
13
13
  /** Set when a cached proof lapses: a calm prompt to sign in again. */
14
14
  notice: string;
15
+ /** Clear the notice without signing in — the patron read it and moved on. */
16
+ dismissNotice: () => void;
15
17
  /** Call after a successful sign-in so the whole app notices. */
16
18
  refresh: () => void;
17
19
  signOut: () => void;
@@ -28,6 +28,7 @@ export function useSession() {
28
28
  setState(read());
29
29
  setNotice("");
30
30
  }, []);
31
+ const dismissNotice = useCallback(() => setNotice(""), []);
31
32
  useEffect(() => onProofExpired(() => {
32
33
  // A tab that can sign for THIS npub lost nothing.
33
34
  if (canSignFor(currentClaim()))
@@ -35,5 +36,5 @@ export function useSession() {
35
36
  setNotice("Your session lapsed while you were away. Sign in again to carry on.");
36
37
  setState(read());
37
38
  }), []);
38
- return { ...state, notice, refresh, signOut };
39
+ return { ...state, notice, dismissNotice, refresh, signOut };
39
40
  }
@@ -2,14 +2,45 @@
2
2
  * Typed wrappers for the tools every operator gets from
3
3
  * `register_standard_tools` in the tollbooth-dpyc wheel.
4
4
  */
5
+ /** What a patron must supply beyond the npub proof, per the wheel's `patron_auth_block`. */
6
+ export interface PatronAuth {
7
+ mode: "oauth" | "secure_courier" | "none";
8
+ patron_credentials_required: boolean;
9
+ /** mode "oauth": the provider's credential service. */
10
+ oauth_service?: string;
11
+ /** mode "secure_courier": the service a patron delivers secrets to. */
12
+ credential_service?: string;
13
+ }
14
+ /** The wheel's `service_status`: health and build facts, free and unauthenticated. */
5
15
  export interface ServiceStatus {
16
+ success?: boolean;
17
+ service?: string;
18
+ slug?: string;
19
+ version?: string;
20
+ tollbooth_dpyc_version?: string;
21
+ vault_configured?: boolean;
22
+ courier_has_vault?: boolean;
6
23
  operator_npub_hash?: string;
24
+ process_id?: number;
25
+ /** Deploy metadata from the environment (commit, build time, …), lower-cased keys. */
26
+ build_info?: Record<string, string> | null;
27
+ patron_auth?: PatronAuth;
28
+ /** Only on a server that runs background jobs. */
29
+ async_jobs?: {
30
+ docket_url_set: boolean;
31
+ backend: string;
32
+ durable_across_recycles: boolean;
33
+ };
34
+ durable_jobs?: {
35
+ modal_app: string | null;
36
+ detached_executor_active: boolean;
37
+ detached_executor_resolved: boolean;
38
+ detached_executor_error: string | null;
39
+ last_dispatch_error: string | null;
40
+ dispatching: boolean;
41
+ };
7
42
  lifecycle?: string;
8
43
  message?: string;
9
- version?: string;
10
- tollbooth_dpyc_version?: string;
11
- service?: string;
12
- slug?: string;
13
44
  }
14
45
  export declare function serviceStatus(): Promise<ServiceStatus>;
15
46
  export interface NpubProofResult {
@@ -75,24 +106,49 @@ export interface CheckBalanceResult {
75
106
  calls: number;
76
107
  api_sats: number;
77
108
  }>;
109
+ invoice_summary?: {
110
+ total_invoices: number;
111
+ settled_count: number;
112
+ pending_count: number;
113
+ total_real_sats: number;
114
+ total_api_sats_credited: number;
115
+ };
78
116
  error?: string;
79
117
  error_code?: string;
80
118
  }
81
119
  export declare function checkBalance(): Promise<CheckBalanceResult>;
120
+ export interface ConstraintEffect {
121
+ type: string;
122
+ message?: string;
123
+ [key: string]: unknown;
124
+ }
125
+ /** The wheel's `check_price` preview. */
82
126
  export interface CheckPriceResult {
83
127
  success: boolean;
84
128
  tool_id?: string;
85
- base_cost_api_sats?: number;
86
- effective_cost_api_sats?: number;
129
+ tool_name?: string;
130
+ /** "flat" | "flat+multipliers" | "percent" */
131
+ pricing_type?: string;
132
+ base_cost_api_sats?: number | null;
133
+ effective_cost_api_sats?: number | null;
134
+ rate_percent?: number;
135
+ rate_param?: string;
136
+ min_cost_sats?: number;
137
+ multipliers?: Record<string, unknown>;
138
+ constraints_enabled?: boolean;
139
+ constraint_effects?: ConstraintEffect[];
140
+ hint?: string;
87
141
  error?: string;
88
142
  error_code?: string;
89
143
  }
90
144
  /**
91
145
  * The fare for one tool in api_sats, as the live pricing model resolves it,
92
- * constraints included — keyed by the tool's frozen UUID. Null when unreadable:
93
- * show the answer without a price rather than invent one.
146
+ * constraints included — keyed by the tool's frozen UUID (or its capability
147
+ * name). `toolKwargs` are the call's own parameters, for a tool priced by
148
+ * them (ad valorem, or a multiplier on a categorical argument). Null when
149
+ * unreadable: show the answer without a price rather than invent one.
94
150
  */
95
- export declare function checkPrice(toolId: string): Promise<number | null>;
151
+ export declare function checkPrice(toolId: string, toolKwargs?: Record<string, unknown>): Promise<number | null>;
96
152
  export interface PurchaseCreditsResult {
97
153
  success?: boolean;
98
154
  invoice_id?: string;
@@ -112,6 +168,10 @@ export interface CheckPaymentResult {
112
168
  invoice_id?: string;
113
169
  credits_granted?: number;
114
170
  balance_api_sats?: number;
171
+ amount_sats?: number;
172
+ /** false: the credit could not be written; nothing was granted. */
173
+ persisted?: boolean;
174
+ source?: string;
115
175
  error?: string;
116
176
  error_code?: string;
117
177
  }
@@ -235,3 +295,149 @@ export interface PublishNostrProfileResult {
235
295
  }
236
296
  /** Relay a kind-0 the BROWSER signed. The wheel verifies it and fans it out. */
237
297
  export declare function publishNostrProfile(npub: string, signedEvent: string): Promise<PublishNostrProfileResult>;
298
+ /** Every lifecycle `session_status` reports. */
299
+ export type SessionLifecycle = "ready" | "warming_up" | "misconfigured" | "quota_exceeded" | "not_registered" | "no_identity";
300
+ /** The patron's stored upstream OAuth token, when the operator has a provider. */
301
+ export interface UpstreamOAuth {
302
+ service: string;
303
+ has_access_token: boolean;
304
+ has_refresh_token: boolean;
305
+ refresh_enabled: boolean;
306
+ access_token_expires_at?: number;
307
+ access_token_expires_in_seconds?: number;
308
+ }
309
+ export interface SessionStatusResult {
310
+ success?: boolean;
311
+ lifecycle?: SessionLifecycle;
312
+ message?: string;
313
+ operator_npub?: string;
314
+ /** Why it is not ready, from the failing layer. */
315
+ detail?: string;
316
+ operator_credential_service?: string;
317
+ patron_credential_service?: string;
318
+ upstream_oauth?: UpstreamOAuth;
319
+ /** The vault could not answer — not the same as "never authorized". */
320
+ upstream_oauth_unreadable?: string;
321
+ error?: string;
322
+ error_code?: string;
323
+ }
324
+ /**
325
+ * Is the operator ready to serve? Free, no proof. With `patronNpub`, also the
326
+ * patron's upstream OAuth token expiry, so a page can refresh ahead of time.
327
+ */
328
+ export declare function sessionStatus(patronNpub?: string): Promise<SessionStatusResult>;
329
+ export interface OnboardingItem {
330
+ field: string;
331
+ /** "identity" | "authority" | "secret" */
332
+ category: string;
333
+ /** "configured" | "missing" | "unknown" */
334
+ status: string;
335
+ how?: string;
336
+ lifecycle?: string;
337
+ delivered_at?: string | null;
338
+ }
339
+ /** The wheel's `get_operator_onboarding_status`: what the operator still has to set up. */
340
+ export interface OperatorOnboardingStatus {
341
+ ready?: boolean;
342
+ configured?: OnboardingItem[];
343
+ missing?: OnboardingItem[];
344
+ optional_missing?: OnboardingItem[];
345
+ summary?: string;
346
+ bootstrap_error?: string;
347
+ vault_ok?: boolean;
348
+ /** Set when the credential vault could not be read: nothing is known either way. */
349
+ vault_situation?: string;
350
+ credential_greeting?: string | null;
351
+ credential_service?: string | null;
352
+ operator_name?: string;
353
+ error?: string;
354
+ error_code?: string;
355
+ }
356
+ export declare function getOperatorOnboardingStatus(): Promise<OperatorOnboardingStatus>;
357
+ /**
358
+ * The operator's own balance at its Authority — what certifies patron top-ups.
359
+ * The Authority's ledger, in the same shape as a patron's `check_balance`.
360
+ */
361
+ export declare function checkAuthorityBalance(): Promise<CheckBalanceResult>;
362
+ export interface PricingStep {
363
+ id: string;
364
+ type: string;
365
+ params?: Record<string, unknown>;
366
+ patron_npubs?: string[];
367
+ }
368
+ export interface ToolPrice {
369
+ tool_id: string;
370
+ tool_name: string;
371
+ price_sats: number;
372
+ category: string;
373
+ intent: string;
374
+ priced: boolean;
375
+ price_type?: string;
376
+ price_formula?: unknown;
377
+ min_cost?: number;
378
+ max_cost?: number;
379
+ multipliers?: Record<string, unknown>;
380
+ chain?: PricingStep[];
381
+ }
382
+ /** The wheel's `get_pricing_model`. `status` is "ok" or "error". */
383
+ export interface PricingModelResult {
384
+ status?: string;
385
+ model_id?: string | null;
386
+ name?: string | null;
387
+ is_active?: boolean | null;
388
+ tools?: ToolPrice[] | null;
389
+ tranche_lifetime?: {
390
+ target_usage_pct: number;
391
+ ttl_days?: number;
392
+ min_days?: number;
393
+ max_days?: number;
394
+ };
395
+ error?: string;
396
+ }
397
+ /** The operator's active pricing model. Free, no proof. */
398
+ export declare function getPricingModel(): Promise<PricingModelResult>;
399
+ export interface CanonicalIdentity {
400
+ tool_id: string;
401
+ mcp_name: string;
402
+ category: string;
403
+ intent: string;
404
+ capability: string;
405
+ registered: boolean;
406
+ reason?: string;
407
+ }
408
+ /** The wheel's `list_canonical_identities`: every tool's frozen UUID and wire name. */
409
+ export interface CanonicalIdentitiesResult {
410
+ success?: boolean;
411
+ operator_npub?: string;
412
+ count?: number;
413
+ unregistered_count?: number;
414
+ tools?: CanonicalIdentity[];
415
+ unregistered?: Array<{
416
+ mcp_name: string;
417
+ reason: string;
418
+ [key: string]: unknown;
419
+ }>;
420
+ error?: string;
421
+ }
422
+ export declare function listCanonicalIdentities(): Promise<CanonicalIdentitiesResult>;
423
+ export interface PatronCredentialFieldsResult {
424
+ success?: boolean;
425
+ fields?: string[];
426
+ /** Per field, when it was delivered; null for secrets vaulted before stamps. */
427
+ delivered_at?: Record<string, string | null>;
428
+ count?: number;
429
+ error?: string;
430
+ error_code?: string;
431
+ }
432
+ export interface PatronCredentialWriteResult {
433
+ success?: boolean;
434
+ message?: string;
435
+ error?: string;
436
+ error_code?: string;
437
+ }
438
+ /** The names of the patron's stored credential fields — never their values. */
439
+ export declare function getPatronCredentialFields(): Promise<PatronCredentialFieldsResult>;
440
+ /** Set one credential field, leaving the others as they are. */
441
+ export declare function updatePatronCredential(field: string, value: string): Promise<PatronCredentialWriteResult>;
442
+ /** Remove one credential field, leaving the others as they are. */
443
+ export declare function deletePatronCredential(field: string): Promise<PatronCredentialWriteResult>;
@@ -39,11 +39,16 @@ export function checkBalance() {
39
39
  }
40
40
  /**
41
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.
42
+ * constraints included — keyed by the tool's frozen UUID (or its capability
43
+ * name). `toolKwargs` are the call's own parameters, for a tool priced by
44
+ * them (ad valorem, or a multiplier on a categorical argument). Null when
45
+ * unreadable: show the answer without a price rather than invent one.
44
46
  */
45
- export async function checkPrice(toolId) {
46
- const r = await callTool("check_price", { tool_id: toolId });
47
+ export async function checkPrice(toolId, toolKwargs) {
48
+ const r = await callTool("check_price", {
49
+ tool_id: toolId,
50
+ ...(toolKwargs ? { tool_kwargs: JSON.stringify(toolKwargs) } : {}),
51
+ });
47
52
  const v = r.effective_cost_api_sats ?? r.base_cost_api_sats;
48
53
  return typeof v === "number" ? v : null;
49
54
  }
@@ -78,3 +83,40 @@ export function publishNostrProfile(npub, signedEvent) {
78
83
  signed_event: signedEvent,
79
84
  });
80
85
  }
86
+ /**
87
+ * Is the operator ready to serve? Free, no proof. With `patronNpub`, also the
88
+ * patron's upstream OAuth token expiry, so a page can refresh ahead of time.
89
+ */
90
+ export function sessionStatus(patronNpub) {
91
+ return callTool("session_status", patronNpub ? { patron_npub: patronNpub } : {}, { bestEffort: true });
92
+ }
93
+ export function getOperatorOnboardingStatus() {
94
+ return callTool("get_operator_onboarding_status", {}, { bestEffort: true });
95
+ }
96
+ /**
97
+ * The operator's own balance at its Authority — what certifies patron top-ups.
98
+ * The Authority's ledger, in the same shape as a patron's `check_balance`.
99
+ */
100
+ export function checkAuthorityBalance() {
101
+ return callTool("check_authority_balance", {}, { bestEffort: true });
102
+ }
103
+ /** The operator's active pricing model. Free, no proof. */
104
+ export function getPricingModel() {
105
+ return callTool("get_pricing_model", {});
106
+ }
107
+ export function listCanonicalIdentities() {
108
+ return callTool("list_canonical_identities", {});
109
+ }
110
+ /** The names of the patron's stored credential fields — never their values. */
111
+ export function getPatronCredentialFields() {
112
+ return callTool("get_patron_credential_fields", {});
113
+ }
114
+ /** Set one credential field, leaving the others as they are. */
115
+ export function updatePatronCredential(field, value) {
116
+ // `field` first: the log's scrubber keys on it to hide `value`.
117
+ return callTool("update_patron_credential", { field, value });
118
+ }
119
+ /** Remove one credential field, leaving the others as they are. */
120
+ export function deletePatronCredential(field) {
121
+ return callTool("delete_patron_credential", { field });
122
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tollbooth-dpyc/web",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Shared browser SDK for Tollbooth DPYC operator front ends: npub sign-in, proofs, MCP client, profile and wallet",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Lonnie VanZandt",