@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 +3 -2
- package/dist/client.js +21 -18
- package/dist/pagesProxy.d.ts +7 -0
- package/dist/pagesProxy.js +12 -3
- package/dist/toolResult.d.ts +11 -0
- package/dist/toolResult.js +27 -0
- package/package.json +1 -1
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
|
|
28
|
-
* patron out
|
|
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
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
}
|
package/dist/pagesProxy.d.ts
CHANGED
|
@@ -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>;
|
package/dist/pagesProxy.js
CHANGED
|
@@ -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") {
|
package/dist/toolResult.d.ts
CHANGED
|
@@ -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;
|
package/dist/toolResult.js
CHANGED
|
@@ -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.
|
|
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",
|