@tollbooth-dpyc/web 1.8.0 → 1.8.2

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
  }
@@ -11,8 +11,15 @@
11
11
  *
12
12
  * Only the headers MCP needs are forwarded upstream. Cookies and anything else
13
13
  * the browser attached stay behind.
14
+ *
15
+ * Every request upstream carries a bearer. An operator with holder bearer on
16
+ * refuses a request without one; a page that holds no holder token is an
17
+ * anonymous caller, so the proxy says so with `Bearer anonymous` and the page
18
+ * proves itself with `dpop_token` as before. A page that does hold a token
19
+ * sends it, and it is forwarded untouched.
14
20
  */
15
21
  export declare const CORS_HEADERS: Record<string, string>;
22
+ export declare const ANONYMOUS_BEARER = "Bearer anonymous";
16
23
  type Fetch = (input: string, init: RequestInit & {
17
24
  duplex?: string;
18
25
  }) => Promise<Response>;
@@ -11,14 +11,21 @@
11
11
  *
12
12
  * Only the headers MCP needs are forwarded upstream. Cookies and anything else
13
13
  * the browser attached stay behind.
14
+ *
15
+ * Every request upstream carries a bearer. An operator with holder bearer on
16
+ * refuses a request without one; a page that holds no holder token is an
17
+ * anonymous caller, so the proxy says so with `Bearer anonymous` and the page
18
+ * proves itself with `dpop_token` as before. A page that does hold a token
19
+ * sends it, and it is forwarded untouched.
14
20
  */
15
21
  export const CORS_HEADERS = {
16
22
  "Access-Control-Allow-Origin": "*",
17
23
  "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
18
- "Access-Control-Allow-Headers": "content-type, mcp-session-id, accept, last-event-id",
19
- "Access-Control-Expose-Headers": "mcp-session-id",
24
+ "Access-Control-Allow-Headers": "content-type, mcp-session-id, accept, last-event-id, authorization",
25
+ "Access-Control-Expose-Headers": "mcp-session-id, www-authenticate",
20
26
  };
21
- const FORWARDED = new Set(["content-type", "accept", "mcp-session-id", "last-event-id"]);
27
+ const FORWARDED = new Set(["content-type", "accept", "mcp-session-id", "last-event-id", "authorization"]);
28
+ export const ANONYMOUS_BEARER = "Bearer anonymous";
22
29
  export function makeMcpProxy(upstream, fetchImpl = fetch) {
23
30
  const host = new URL(upstream).host;
24
31
  return async function onRequest(context) {
@@ -31,6 +38,8 @@ export function makeMcpProxy(upstream, fetchImpl = fetch) {
31
38
  if (FORWARDED.has(k.toLowerCase()))
32
39
  headers.set(k, v);
33
40
  }
41
+ if (!headers.has("authorization"))
42
+ headers.set("Authorization", ANONYMOUS_BEARER);
34
43
  headers.set("Host", host);
35
44
  const init = { method: req.method, headers };
36
45
  if (req.method !== "GET" && req.method !== "HEAD" && req.method !== "DELETE") {
@@ -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.2",
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",