@tollbooth-dpyc/web 1.8.0 → 1.8.1

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
@@ -24,8 +24,9 @@ type ProofExpiredListener = (message: string) => void;
24
24
  export declare function onProofExpired(cb: ProofExpiredListener): () => void;
25
25
  export interface CallOptions {
26
26
  /**
27
- * A non-essential call — diagnostics, personalization. It never logs the
28
- * patron out, whatever it gets back.
27
+ * A non-essential call — diagnostics, personalization, a board polled all
28
+ * match. A soft proof miss here never logs the patron out; a stored grant
29
+ * the service refused for good (expired, revoked) still does.
29
30
  */
30
31
  bestEffort?: boolean;
31
32
  timeoutMs?: number;
package/dist/client.js CHANGED
@@ -17,7 +17,7 @@ import { clearStoredProof, forgetRecentLogin, getStoredNpub, getStoredProof, } f
17
17
  import { signInlineProof } from "./inlineProof.js";
18
18
  import { isTransportFailure, NetworkError } from "./networkError.js";
19
19
  import { clearSessionNsec, hasSessionNsec, sessionNsecNpub } from "./sessionNsec.js";
20
- import { errorText, looksFailed, proofBounceMessage, readToolResult, } from "./toolResult.js";
20
+ import { errorText, deadGrantMessage, looksFailed, proofBounceMessage, readToolResult, } from "./toolResult.js";
21
21
  /**
22
22
  * Standard tools whose wheel signature takes no npub/proof envelope. Pydantic
23
23
  * strict mode rejects unexpected kwargs, so they must not get one.
@@ -77,14 +77,14 @@ function proofFor(tool) {
77
77
  const npub = getStoredNpub();
78
78
  const sessionNpub = hasSessionNsec() ? sessionNsecNpub() : null;
79
79
  if (sessionNpub && sessionNpub === npub)
80
- return signInlineProof(toolName(tool));
80
+ return { token: signInlineProof(toolName(tool)), inline: true };
81
81
  if (sessionNpub)
82
82
  clearSessionNsec();
83
83
  }
84
84
  catch {
85
85
  /* fall through to the cached token */
86
86
  }
87
- return getStoredProof();
87
+ return { token: getStoredProof(), inline: false };
88
88
  }
89
89
  export { isNetworkError, NetworkError } from "./networkError.js";
90
90
  /** A paid call bounced for a lapsed or missing proof. The gate re-arms sign-in. */
@@ -131,9 +131,10 @@ export async function callToolWithContent(tool, args = {}, opts = {}) {
131
131
  if (!quiet)
132
132
  debugPush("call", `${name}(${redact(JSON.stringify(args)).slice(0, 140)})`);
133
133
  const bootstrap = STANDARD_BOOTSTRAP.includes(tool) || cfg.extraBootstrapTools.includes(tool);
134
- const merged = bootstrap
135
- ? { ...args }
136
- : { npub: getStoredNpub(), dpop_token: proofFor(tool), ...args };
134
+ const proof = bootstrap ? null : proofFor(tool);
135
+ const merged = proof
136
+ ? { npub: getStoredNpub(), dpop_token: proof.token, ...args }
137
+ : { ...args };
137
138
  let result;
138
139
  try {
139
140
  const c = await getClient();
@@ -163,18 +164,20 @@ export async function callToolWithContent(tool, args = {}, opts = {}) {
163
164
  const tail = images.length ? ` +${images.length} image` : "";
164
165
  debugPush(looksFailed(payload) ? "error" : "result", `${name} → ${redact(String(preview)).slice(0, 220)}${tail}`);
165
166
  }
166
- if (!opts.bestEffort) {
167
- const bounced = proofBounceMessage(payload);
168
- if (bounced !== null) {
169
- // The token just refused is the same one the recent-login shortcut
170
- // would replay, so it goes too, or a returning patron re-bounces.
171
- const npub = getStoredNpub();
172
- clearStoredProof();
173
- if (npub)
174
- forgetRecentLogin(npub);
175
- emitProofExpired(bounced);
176
- throw new ProofRequiredError(bounced);
177
- }
167
+ // A stored grant the service refused for good signs the patron out even
168
+ // from a best-effort call: that is not a blip, and staying "signed in" on a
169
+ // dead grant leaves every page failing with nothing to tell them why.
170
+ const bounced = (opts.bestEffort ? null : proofBounceMessage(payload)) ??
171
+ (proof && !proof.inline && proof.token ? deadGrantMessage(payload) : null);
172
+ if (bounced !== null) {
173
+ // The token just refused is the same one the recent-login shortcut
174
+ // would replay, so it goes too, or a returning patron re-bounces.
175
+ const npub = getStoredNpub();
176
+ clearStoredProof();
177
+ if (npub)
178
+ forgetRecentLogin(npub);
179
+ emitProofExpired(bounced);
180
+ throw new ProofRequiredError(bounced);
178
181
  }
179
182
  return { data: payload, images };
180
183
  }
@@ -33,5 +33,16 @@ export declare function readToolResult(result: RawToolResult): ReadResult;
33
33
  * silently never fired. Normalised here, once.
34
34
  */
35
35
  export declare function proofBounceMessage(payload: unknown): string | null;
36
+ /**
37
+ * A stored proof grant the service refused for good: `{success: false,
38
+ * error_code: proof_invalid, reason}` where the reason is about the grant —
39
+ * expired, revoked, issued for another npub or operator, malformed. Retrying
40
+ * with it can never succeed, so the patron is signed out.
41
+ *
42
+ * Only meaningful when the call carried the stored grant. A session key's
43
+ * inline proof shares the code and even the word `expired` (outside its
44
+ * window), but it is signed afresh for the next call and loses nothing.
45
+ */
46
+ export declare function deadGrantMessage(payload: unknown): string | null;
36
47
  /** Whether a payload reports failure, for the debug log's colouring. */
37
48
  export declare function looksFailed(payload: unknown): boolean;
@@ -51,6 +51,33 @@ export function proofBounceMessage(payload) {
51
51
  }
52
52
  return null;
53
53
  }
54
+ /**
55
+ * Grant refusals that say nothing about the grant: the service could not
56
+ * establish its own identity, or could not read its revocation list. Both are
57
+ * the service's fault and retryable, so the patron stays signed in.
58
+ */
59
+ const GRANT_STILL_GOOD = new Set(["operator_unknown", "revocations_unavailable"]);
60
+ /**
61
+ * A stored proof grant the service refused for good: `{success: false,
62
+ * error_code: proof_invalid, reason}` where the reason is about the grant —
63
+ * expired, revoked, issued for another npub or operator, malformed. Retrying
64
+ * with it can never succeed, so the patron is signed out.
65
+ *
66
+ * Only meaningful when the call carried the stored grant. A session key's
67
+ * inline proof shares the code and even the word `expired` (outside its
68
+ * window), but it is signed afresh for the next call and loses nothing.
69
+ */
70
+ export function deadGrantMessage(payload) {
71
+ if (!payload || typeof payload !== "object")
72
+ return null;
73
+ const p = payload;
74
+ const code = String(p.error_code ?? "").toLowerCase();
75
+ if (p.success !== false || code !== "proof_invalid")
76
+ return null;
77
+ if (GRANT_STILL_GOOD.has(String(p.reason ?? "").toLowerCase()))
78
+ return null;
79
+ return String(p.error ?? "Your sign-in has expired.");
80
+ }
54
81
  /** Whether a payload reports failure, for the debug log's colouring. */
55
82
  export function looksFailed(payload) {
56
83
  if (!payload || typeof payload !== "object")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tollbooth-dpyc/web",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
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",