@tollbooth-dpyc/web 1.8.2 → 2.0.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/README.md +3 -2
- package/dist/client.d.ts +6 -6
- package/dist/client.js +24 -44
- package/dist/config.d.ts +8 -1
- package/dist/config.js +1 -0
- package/dist/debugLog.d.ts +1 -1
- package/dist/debugLog.js +6 -6
- package/dist/funding.d.ts +5 -3
- package/dist/funding.js +4 -2
- package/dist/holder.d.ts +31 -0
- package/dist/holder.js +162 -0
- package/dist/identity.d.ts +14 -10
- package/dist/identity.js +25 -21
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/pagesProxy.d.ts +4 -5
- package/dist/pagesProxy.js +4 -5
- package/dist/react/FundingStatus.js +10 -4
- package/dist/react/NpubGate.d.ts +4 -1
- package/dist/react/NpubGate.js +49 -21
- package/dist/requestSigning.d.ts +25 -0
- package/dist/requestSigning.js +71 -0
- package/dist/signedIn.d.ts +8 -7
- package/dist/signedIn.js +7 -10
- package/dist/standardTools.d.ts +12 -6
- package/dist/standardTools.js +9 -6
- package/dist/toolResult.d.ts +9 -9
- package/dist/toolResult.js +10 -10
- package/package.json +1 -1
- package/dist/inlineProof.d.ts +0 -18
- package/dist/inlineProof.js +0 -35
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ export { callTool, callToolWithContent, isNetworkError, NetworkError, onProofExp
|
|
|
3
3
|
export * from "./identity.js";
|
|
4
4
|
export * from "./standardTools.js";
|
|
5
5
|
export { clearSessionNsec, getSessionNsec, getSessionNsecBytes, hasSessionNsec, sessionNsecNpub, setSessionNsec, } from "./sessionNsec.js";
|
|
6
|
-
export {
|
|
6
|
+
export { signRequest } from "./requestSigning.js";
|
|
7
7
|
export { sessionKeyClaimVisible } from "./sessionKeyClaim.js";
|
|
8
8
|
export { handOffMode, shareOrDownload } from "./shareFile.js";
|
|
9
9
|
export { canSignFor, isProven } from "./signedIn.js";
|
package/dist/pagesProxy.d.ts
CHANGED
|
@@ -12,11 +12,10 @@
|
|
|
12
12
|
* Only the headers MCP needs are forwarded upstream. Cookies and anything else
|
|
13
13
|
* the browser attached stay behind.
|
|
14
14
|
*
|
|
15
|
-
* Every request upstream carries
|
|
16
|
-
*
|
|
17
|
-
* anonymous caller,
|
|
18
|
-
*
|
|
19
|
-
* sends it, and it is forwarded untouched.
|
|
15
|
+
* Every request upstream carries who it may act as. An operator refuses a
|
|
16
|
+
* request with no `Authorization`, so a page that neither signs (`Nostr`)
|
|
17
|
+
* nor holds a holder token is an anonymous caller, and the proxy says so with
|
|
18
|
+
* `Bearer anonymous`. A request that carries either is forwarded untouched.
|
|
20
19
|
*/
|
|
21
20
|
export declare const CORS_HEADERS: Record<string, string>;
|
|
22
21
|
export declare const ANONYMOUS_BEARER = "Bearer anonymous";
|
package/dist/pagesProxy.js
CHANGED
|
@@ -12,11 +12,10 @@
|
|
|
12
12
|
* Only the headers MCP needs are forwarded upstream. Cookies and anything else
|
|
13
13
|
* the browser attached stay behind.
|
|
14
14
|
*
|
|
15
|
-
* Every request upstream carries
|
|
16
|
-
*
|
|
17
|
-
* anonymous caller,
|
|
18
|
-
*
|
|
19
|
-
* sends it, and it is forwarded untouched.
|
|
15
|
+
* Every request upstream carries who it may act as. An operator refuses a
|
|
16
|
+
* request with no `Authorization`, so a page that neither signs (`Nostr`)
|
|
17
|
+
* nor holds a holder token is an anonymous caller, and the proxy says so with
|
|
18
|
+
* `Bearer anonymous`. A request that carries either is forwarded untouched.
|
|
20
19
|
*/
|
|
21
20
|
export const CORS_HEADERS = {
|
|
22
21
|
"Access-Control-Allow-Origin": "*",
|
|
@@ -69,11 +69,17 @@ async function proofKind() {
|
|
|
69
69
|
const claim = currentClaim();
|
|
70
70
|
if (canSignFor(claim))
|
|
71
71
|
return { kind: "session_nsec" };
|
|
72
|
-
if (!claim.
|
|
72
|
+
if (!claim.held)
|
|
73
73
|
return { kind: "none" };
|
|
74
|
-
const s = await checkProofStatus(claim.npub
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
const s = await checkProofStatus(claim.npub).catch(() => null);
|
|
75
|
+
return { kind: "held", status: heldStatus(s?.status), expiresInSec: s?.expires_in_seconds ?? null };
|
|
76
|
+
}
|
|
77
|
+
function heldStatus(status) {
|
|
78
|
+
if (status === "valid" || status === "not_held")
|
|
79
|
+
return status;
|
|
80
|
+
if (status === "expired" || status === "revoked" || status === "invalid")
|
|
81
|
+
return "lapsed";
|
|
82
|
+
return "unknown";
|
|
77
83
|
}
|
|
78
84
|
/** The patron's own health: sign-in proof and credit balance. */
|
|
79
85
|
export function PatronFundingStatus({ thresholds, siteRows, heading = "Account health", ...rest }) {
|
package/dist/react/NpubGate.d.ts
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* begin → request_npub_proof → awaiting reply → receive_npub_proof → app
|
|
5
5
|
*
|
|
6
|
-
* Success is not a `verified` flag but
|
|
6
|
+
* Success is not a `verified` flag but the operator saying it now holds the
|
|
7
|
+
* npub for this tab's connection (`held_by_connection`). A DM sign-in therefore
|
|
8
|
+
* needs a holder connection first; a site without `operatorUrl` offers the
|
|
9
|
+
* session key only.
|
|
7
10
|
*
|
|
8
11
|
* Around the card: the site's `welcome` (its own words about what this place
|
|
9
12
|
* is for), the network's register line, and — below — `SignInSteps`, which
|
package/dist/react/NpubGate.js
CHANGED
|
@@ -4,7 +4,10 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
4
4
|
*
|
|
5
5
|
* begin → request_npub_proof → awaiting reply → receive_npub_proof → app
|
|
6
6
|
*
|
|
7
|
-
* Success is not a `verified` flag but
|
|
7
|
+
* Success is not a `verified` flag but the operator saying it now holds the
|
|
8
|
+
* npub for this tab's connection (`held_by_connection`). A DM sign-in therefore
|
|
9
|
+
* needs a holder connection first; a site without `operatorUrl` offers the
|
|
10
|
+
* session key only.
|
|
8
11
|
*
|
|
9
12
|
* Around the card: the site's `welcome` (its own words about what this place
|
|
10
13
|
* is for), the network's register line, and — below — `SignInSteps`, which
|
|
@@ -15,7 +18,8 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
15
18
|
import { useEffect, useRef, useState } from "react";
|
|
16
19
|
import { generateSecretKey, getPublicKey, nip19 } from "nostr-tools";
|
|
17
20
|
import { tollboothConfig } from "../config.js";
|
|
18
|
-
import { forgetRecentLogin, getLastTypedNpub, getValidRecentLogins, recordRecentLogin, setLastTypedNpub, setStoredNpub,
|
|
21
|
+
import { forgetRecentLogin, getLastTypedNpub, getValidRecentLogins, recordRecentLogin, setLastTypedNpub, setStoredNpub, } from "../identity.js";
|
|
22
|
+
import { completeHolderConnect, connectAsHolder, holderEnabled, isHolder } from "../holder.js";
|
|
19
23
|
import { setSessionNsec } from "../sessionNsec.js";
|
|
20
24
|
import { readSignInFailure } from "../signInSituation.js";
|
|
21
25
|
import { receiveNpubProof, requestNpubProof } from "../standardTools.js";
|
|
@@ -53,7 +57,7 @@ export default function NpubGate({ onLogin, startFresh = false, operatorHash, no
|
|
|
53
57
|
// Prefilled from what was typed last, NOT from the stored identity.
|
|
54
58
|
const [value, setValue] = useState(getLastTypedNpub());
|
|
55
59
|
const [stage, setStage] = useState("begin");
|
|
56
|
-
const [
|
|
60
|
+
const [nonce, setNonce] = useState("");
|
|
57
61
|
const [busy, setBusy] = useState(false);
|
|
58
62
|
const [error, setError] = useState("");
|
|
59
63
|
const [note, setNote] = useState("");
|
|
@@ -75,8 +79,7 @@ export default function NpubGate({ onLogin, startFresh = false, operatorHash, no
|
|
|
75
79
|
const valid = isNsec || isNpub;
|
|
76
80
|
function reuseRecent(entry) {
|
|
77
81
|
setStoredNpub(entry.npub);
|
|
78
|
-
|
|
79
|
-
recordRecentLogin(entry.npub, entry.proof, Math.floor((entry.expiresAt - Date.now()) / 1000));
|
|
82
|
+
recordRecentLogin(entry.npub, Math.floor((entry.expiresAt - Date.now()) / 1000));
|
|
80
83
|
onLogin();
|
|
81
84
|
}
|
|
82
85
|
function forget(npub) {
|
|
@@ -106,23 +109,50 @@ export default function NpubGate({ onLogin, startFresh = false, operatorHash, no
|
|
|
106
109
|
setError(`Couldn't read that nsec: ${e.message}`);
|
|
107
110
|
}
|
|
108
111
|
}
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
// Back from connecting to the operator: carry on with the DM it was for.
|
|
113
|
+
const resumed = useRef(false);
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (resumed.current)
|
|
116
|
+
return;
|
|
117
|
+
resumed.current = true;
|
|
118
|
+
completeHolderConnect()
|
|
119
|
+
.then((npub) => {
|
|
120
|
+
if (npub) {
|
|
121
|
+
setValue(npub);
|
|
122
|
+
void begin(npub);
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
.catch((e) => setError(e.message));
|
|
126
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
127
|
+
}, []);
|
|
128
|
+
async function begin(npubArg) {
|
|
129
|
+
const npubToProve = npubArg ?? trimmed;
|
|
130
|
+
if (!(npubToProve.startsWith("npub1") && npubToProve.length >= 60)) {
|
|
111
131
|
setError("Enter a valid npub1… key.");
|
|
112
132
|
return;
|
|
113
133
|
}
|
|
114
134
|
setBusy(true);
|
|
115
135
|
setError("");
|
|
116
136
|
try {
|
|
117
|
-
|
|
137
|
+
// The proof will stay with the operator, so this tab connects to it
|
|
138
|
+
// first; the browser comes straight back here and the DM goes out.
|
|
139
|
+
if (!holderEnabled()) {
|
|
140
|
+
setError("This site signs in with a key only — paste an nsec, or make one.");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (!isHolder()) {
|
|
144
|
+
await connectAsHolder(npubToProve);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const r = await requestNpubProof(npubToProve, globalThis.location.origin, `You requested to sign in to ${appName} (${globalThis.location.host}).`);
|
|
118
148
|
if (r.error)
|
|
119
149
|
return setError(r.error);
|
|
120
|
-
if (!r.
|
|
150
|
+
if (!r.nonce)
|
|
121
151
|
return setError("The service did not return a session phrase. Try again.");
|
|
122
152
|
// Remembered for the field only. The identity is written by `finish()`,
|
|
123
153
|
// once the human has answered the DM.
|
|
124
|
-
setLastTypedNpub(
|
|
125
|
-
|
|
154
|
+
setLastTypedNpub(npubToProve);
|
|
155
|
+
setNonce(r.nonce);
|
|
126
156
|
setStage("awaiting");
|
|
127
157
|
}
|
|
128
158
|
catch (e) {
|
|
@@ -137,20 +167,18 @@ export default function NpubGate({ onLogin, startFresh = false, operatorHash, no
|
|
|
137
167
|
setError("");
|
|
138
168
|
setStage("checking");
|
|
139
169
|
try {
|
|
140
|
-
const r = await receiveNpubProof(trimmed,
|
|
141
|
-
if (r.error) {
|
|
142
|
-
setError(r.error);
|
|
170
|
+
const r = await receiveNpubProof(trimmed, nonce);
|
|
171
|
+
if (r.error || !r.held_by_connection) {
|
|
172
|
+
setError(r.error || "The service did not keep the proof for this page. Try again.");
|
|
143
173
|
setStage("awaiting");
|
|
144
174
|
return;
|
|
145
175
|
}
|
|
146
|
-
const token = r.dpop_token || pendingProof;
|
|
147
176
|
const npub = r.proven_npub || trimmed;
|
|
148
177
|
setStoredNpub(npub);
|
|
149
|
-
setStoredProof(token);
|
|
150
178
|
if (r.expires_in_seconds && r.expires_in_seconds > 0) {
|
|
151
|
-
recordRecentLogin(npub,
|
|
179
|
+
recordRecentLogin(npub, r.expires_in_seconds);
|
|
152
180
|
const h = Math.floor(r.expires_in_seconds / 3600);
|
|
153
|
-
setNote(`Proof
|
|
181
|
+
setNote(`Proof held for ~${h > 0 ? `${h}h` : `${Math.round(r.expires_in_seconds / 60)}m`}.`);
|
|
154
182
|
}
|
|
155
183
|
onLogin();
|
|
156
184
|
}
|
|
@@ -164,10 +192,10 @@ export default function NpubGate({ onLogin, startFresh = false, operatorHash, no
|
|
|
164
192
|
}
|
|
165
193
|
function reset() {
|
|
166
194
|
setStage("begin");
|
|
167
|
-
|
|
195
|
+
setNonce("");
|
|
168
196
|
setError("");
|
|
169
197
|
}
|
|
170
|
-
return (_jsxs("div", { className: cx("max-w-md mx-auto mt-12 px-4", c.root), children: [_jsxs("h1", { className: "text-xl font-semibold mb-1", children: ["Sign in to ", appName] }), welcome && _jsx("div", { className: cx("text-sm mb-3", c.welcome), children: welcome }), _jsxs("p", { className: cx(`text-sm mb-5 ${muted}`, c.register), children: [appName, " is an operator on the Tollbooth DPYC\u2122 network. DPYC\u2122 \u2014 Don't Pester Your Customer\u2122: no email, no password, no KYC. Your Nostr key is your identity."] }), notice && _jsx("div", { className: `mb-5 ${warnBox}`, children: notice }), stage === "begin" && recents.length > 0 && (_jsxs("div", { className: "mb-5", children: [_jsx("div", { className: `text-xs uppercase tracking-wider mb-2 ${muted}`, children: "Recent identities" }), _jsx("div", { className: "space-y-1.5", children: recents.map((e) => (_jsxs("div", { className: "flex gap-1.5", children: [_jsxs("button", { type: "button", onClick: () => reuseRecent(e), disabled: busy, title: `Re-enter as ${e.npub} on the
|
|
198
|
+
return (_jsxs("div", { className: cx("max-w-md mx-auto mt-12 px-4", c.root), children: [_jsxs("h1", { className: "text-xl font-semibold mb-1", children: ["Sign in to ", appName] }), welcome && _jsx("div", { className: cx("text-sm mb-3", c.welcome), children: welcome }), _jsxs("p", { className: cx(`text-sm mb-5 ${muted}`, c.register), children: [appName, " is an operator on the Tollbooth DPYC\u2122 network. DPYC\u2122 \u2014 Don't Pester Your Customer\u2122: no email, no password, no KYC. Your Nostr key is your identity."] }), notice && _jsx("div", { className: `mb-5 ${warnBox}`, children: notice }), stage === "begin" && recents.length > 0 && (_jsxs("div", { className: "mb-5", children: [_jsx("div", { className: `text-xs uppercase tracking-wider mb-2 ${muted}`, children: "Recent identities" }), _jsx("div", { className: "space-y-1.5", children: recents.map((e) => (_jsxs("div", { className: "flex gap-1.5", children: [_jsxs("button", { type: "button", onClick: () => reuseRecent(e), disabled: busy, title: `Re-enter as ${e.npub} on the proof the service holds`, className: `${card} flex-1 flex items-center justify-between px-3 py-2.5 text-left hover:border-[var(--tb-accent)]`, children: [_jsxs("span", { className: "font-mono text-xs truncate", children: [e.npub.slice(0, 12), "\u2026", e.npub.slice(-6)] }), _jsxs("span", { className: `text-xs shrink-0 ml-2 ${muted}`, children: [ttl(e.expiresAt), " left"] })] }), _jsx("button", { type: "button", onClick: () => forget(e.npub), disabled: busy, title: "Forget this identity", "aria-label": "Forget this identity", className: `w-9 rounded-lg border border-[var(--tb-line)] ${muted}`, children: "\u00D7" })] }, e.npub))) })] })), _jsxs("div", { className: `${card} p-4 space-y-3`, children: [stage === "begin" ? (_jsxs(_Fragment, { children: [_jsx("label", { className: `block text-xs uppercase tracking-wider ${muted}`, htmlFor: "tb-npub-field", children: "Enter npub (secure) or nsec (convenient)" }), _jsx("input", { id: "tb-npub-field", "data-tour": "tb.gate.npub", type: isNsec ? "password" : "text", value: value, onChange: (e) => {
|
|
171
199
|
setValue(e.target.value);
|
|
172
200
|
setGeneratedHint(false);
|
|
173
201
|
}, onKeyDown: (e) => {
|
|
@@ -177,7 +205,7 @@ export default function NpubGate({ onLogin, startFresh = false, operatorHash, no
|
|
|
177
205
|
signInWithNsec();
|
|
178
206
|
else
|
|
179
207
|
void begin();
|
|
180
|
-
}, placeholder: "npub1\u2026", spellCheck: false, autoCapitalize: "off", autoCorrect: "off", className: input }), generatedHint && (_jsxs("div", { className: warnBox, children: ["New key generated. ", _jsx("b", { children: "Save this nsec" }), " somewhere safe \u2014 it's the only copy and it lives only in this browser."] })), _jsx("button", { type: "button", onClick: () => (isNsec ? signInWithNsec() : void begin()), disabled: !valid || busy, className: primary, "data-tour": "tb.gate.send", children: busy ? "Sending…" : isNpub ? "Send message" : "Sign in" }), isNsec && _jsx("p", { className: `text-xs ${muted}`, children: "Your nsec stays in this browser and signs each call." }), isNpub && (_jsx("p", { className: `text-xs ${muted}`, children: "We send a message to your npub. Approve it from your Nostr client \u2014 your signature is the proof." })), _jsx("p", { className: `text-xs pt-1 ${muted}`, children: "Need a new identity?" }), _jsx("button", { type: "button", onClick: generateKey, className: ghost, children: "Generate a new key" })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: `${warnBox} space-y-1.5`, children: [_jsx("div", { className: "font-medium", children: "Message sent \u2014 open your Nostr client." }), _jsx("div", { children: "Reply with any text. Your signature on that reply is the proof." }), operatorHash && (_jsxs("div", { children: ["Sender fingerprint: ", _jsxs("span", { className: "font-mono", children: ["\uD83D\uDD12 ", operatorHash] })] }))] }),
|
|
208
|
+
}, placeholder: "npub1\u2026", spellCheck: false, autoCapitalize: "off", autoCorrect: "off", className: input }), generatedHint && (_jsxs("div", { className: warnBox, children: ["New key generated. ", _jsx("b", { children: "Save this nsec" }), " somewhere safe \u2014 it's the only copy and it lives only in this browser."] })), _jsx("button", { type: "button", onClick: () => (isNsec ? signInWithNsec() : void begin()), disabled: !valid || busy, className: primary, "data-tour": "tb.gate.send", children: busy ? "Sending…" : isNpub ? "Send message" : "Sign in" }), isNsec && _jsx("p", { className: `text-xs ${muted}`, children: "Your nsec stays in this browser and signs each call." }), isNpub && (_jsx("p", { className: `text-xs ${muted}`, children: "We send a message to your npub. Approve it from your Nostr client \u2014 your signature is the proof." })), _jsx("p", { className: `text-xs pt-1 ${muted}`, children: "Need a new identity?" }), _jsx("button", { type: "button", onClick: generateKey, className: ghost, children: "Generate a new key" })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: `${warnBox} space-y-1.5`, children: [_jsx("div", { className: "font-medium", children: "Message sent \u2014 open your Nostr client." }), _jsx("div", { children: "Reply with any text. Your signature on that reply is the proof." }), operatorHash && (_jsxs("div", { children: ["Sender fingerprint: ", _jsxs("span", { className: "font-mono", children: ["\uD83D\uDD12 ", operatorHash] })] }))] }), nonce && (_jsxs("div", { className: "rounded-lg p-3 text-xs border border-[var(--tb-accent)] space-y-2", children: [_jsx("div", { className: `uppercase tracking-wider text-[10px] ${muted}`, children: "Confirmation code" }), _jsx("div", { className: "font-mono text-base select-all", children: nonce }), _jsxs("div", { className: `leading-relaxed ${muted}`, children: ["The same code appears in the message. ", _jsx("b", { children: "Approve it only if the codes match." }), " If they differ \u2014 or the message points you somewhere other than this site \u2014 do not reply."] })] })), _jsx("button", { type: "button", onClick: () => void finish(), disabled: busy, className: primary, "data-tour": "tb.gate.verify", children: busy ? "Checking…" : "I've replied — verify" }), _jsx("button", { type: "button", onClick: () => void begin(), disabled: busy, className: ghost, children: "Resend" }), _jsx("button", { type: "button", onClick: reset, disabled: busy, className: `w-full text-xs py-1.5 ${muted}`, children: "Use a different npub" })] })), error && _jsx(Trouble, { raw: error, appName: appName }), note && _jsx("div", { className: `text-xs text-center italic ${muted}`, children: note })] }), steps && _jsx(SignInSteps, { stage: gateStage(stage, busy), classNames: { ...STEPS_LOOK, ...c.steps } }), links && _jsx(SignInLinks, { credit: linksCredit, classNames: { ...LINKS_LOOK, ...c.links } })] }));
|
|
181
209
|
}
|
|
182
210
|
/** A failure, read to the person: the lead first, the service's own words underneath. */
|
|
183
211
|
function Trouble({ raw, appName }) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every request a page sends while it holds the signed-in npub's key is signed
|
|
3
|
+
* with that key (NIP-98): an `Authorization: Nostr` event bound to the method,
|
|
4
|
+
* the URL and the sha256 of the body. Whoever holds the key needs no grant, and
|
|
5
|
+
* no tool call carries a proof in its arguments.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* 16 random bytes as 32 lowercase hex chars. Without it two requests signed in
|
|
9
|
+
* the same wall-clock second could be byte-identical, their ids would collide,
|
|
10
|
+
* and the server's replay guard would refuse the second.
|
|
11
|
+
*/
|
|
12
|
+
export declare function proofNonce(): string;
|
|
13
|
+
/** The `Authorization` value for one request, signed by `secretKey`. */
|
|
14
|
+
export declare function signRequest(method: string, url: string, body: string, secretKey: Uint8Array): Promise<string>;
|
|
15
|
+
/**
|
|
16
|
+
* True when this tab holds the key of the npub that is signed in. A key left
|
|
17
|
+
* from a previous identity is evicted rather than allowed to sign.
|
|
18
|
+
*/
|
|
19
|
+
export declare function signsForSignedInNpub(): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* `fetch` for the MCP transport: the request carries who it may act as — this
|
|
22
|
+
* tab's key when it holds one, else this tab's holder connection, else nobody
|
|
23
|
+
* (the site's proxy then marks it anonymous).
|
|
24
|
+
*/
|
|
25
|
+
export declare const signingFetch: typeof fetch;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every request a page sends while it holds the signed-in npub's key is signed
|
|
3
|
+
* with that key (NIP-98): an `Authorization: Nostr` event bound to the method,
|
|
4
|
+
* the URL and the sha256 of the body. Whoever holds the key needs no grant, and
|
|
5
|
+
* no tool call carries a proof in its arguments.
|
|
6
|
+
*/
|
|
7
|
+
import { finalizeEvent } from "nostr-tools";
|
|
8
|
+
import { holderBearer } from "./holder.js";
|
|
9
|
+
import { getStoredNpub } from "./identity.js";
|
|
10
|
+
import { clearSessionNsec, getSessionNsecBytes, hasSessionNsec, sessionNsecNpub } from "./sessionNsec.js";
|
|
11
|
+
const PROOF_KIND = 27235;
|
|
12
|
+
/**
|
|
13
|
+
* 16 random bytes as 32 lowercase hex chars. Without it two requests signed in
|
|
14
|
+
* the same wall-clock second could be byte-identical, their ids would collide,
|
|
15
|
+
* and the server's replay guard would refuse the second.
|
|
16
|
+
*/
|
|
17
|
+
export function proofNonce() {
|
|
18
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
19
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
20
|
+
}
|
|
21
|
+
async function sha256Hex(body) {
|
|
22
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(body));
|
|
23
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
24
|
+
}
|
|
25
|
+
/** The `Authorization` value for one request, signed by `secretKey`. */
|
|
26
|
+
export async function signRequest(method, url, body, secretKey) {
|
|
27
|
+
const event = finalizeEvent({
|
|
28
|
+
kind: PROOF_KIND,
|
|
29
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
30
|
+
tags: [
|
|
31
|
+
["u", url],
|
|
32
|
+
["method", method.toUpperCase()],
|
|
33
|
+
["payload", await sha256Hex(body)],
|
|
34
|
+
["nonce", proofNonce()],
|
|
35
|
+
],
|
|
36
|
+
content: "",
|
|
37
|
+
}, secretKey);
|
|
38
|
+
return `Nostr ${btoa(JSON.stringify(event))}`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* True when this tab holds the key of the npub that is signed in. A key left
|
|
42
|
+
* from a previous identity is evicted rather than allowed to sign.
|
|
43
|
+
*/
|
|
44
|
+
export function signsForSignedInNpub() {
|
|
45
|
+
const sessionNpub = hasSessionNsec() ? sessionNsecNpub() : null;
|
|
46
|
+
if (!sessionNpub)
|
|
47
|
+
return false;
|
|
48
|
+
if (sessionNpub === getStoredNpub())
|
|
49
|
+
return true;
|
|
50
|
+
clearSessionNsec();
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* `fetch` for the MCP transport: the request carries who it may act as — this
|
|
55
|
+
* tab's key when it holds one, else this tab's holder connection, else nobody
|
|
56
|
+
* (the site's proxy then marks it anonymous).
|
|
57
|
+
*/
|
|
58
|
+
export const signingFetch = async (input, init) => {
|
|
59
|
+
const headers = new Headers(init?.headers);
|
|
60
|
+
if (signsForSignedInNpub()) {
|
|
61
|
+
const request = new Request(input, init);
|
|
62
|
+
const body = typeof init?.body === "string" ? init.body : "";
|
|
63
|
+
headers.set("Authorization", await signRequest(request.method, request.url, body, getSessionNsecBytes()));
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const bearer = await holderBearer();
|
|
67
|
+
if (bearer)
|
|
68
|
+
headers.set("Authorization", `Bearer ${bearer}`);
|
|
69
|
+
}
|
|
70
|
+
return fetch(input, { ...init, headers });
|
|
71
|
+
};
|
package/dist/signedIn.d.ts
CHANGED
|
@@ -4,23 +4,24 @@
|
|
|
4
4
|
* An npub is public — it is on every note its owner ever wrote — so the npub
|
|
5
5
|
* alone is a claim, never a credential. A shell that read "we know an npub" as
|
|
6
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:
|
|
7
|
+
* themselves, unproven. Two things can back the claim, both on the connection:
|
|
8
8
|
*
|
|
9
|
-
* - a **
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* - a **DM proof**, won by answering a Nostr challenge — held by the operator
|
|
10
|
+
* for this tab's holder connection, or
|
|
11
|
+
* - a **session nsec** in this tab, which signs every request — and only
|
|
12
|
+
* counts for the npub it actually derives to.
|
|
12
13
|
*
|
|
13
14
|
* Pure, so it is testable without a browser.
|
|
14
15
|
*/
|
|
15
16
|
export interface Claim {
|
|
16
17
|
/** The npub the interface is showing, proven or not. */
|
|
17
18
|
npub: string;
|
|
18
|
-
/**
|
|
19
|
-
|
|
19
|
+
/** True when this tab's holder connection holds the npub, proven by DM. */
|
|
20
|
+
held: boolean;
|
|
20
21
|
/** The npub this tab's session key derives to, if it holds one. */
|
|
21
22
|
sessionNpub: string | null;
|
|
22
23
|
}
|
|
23
24
|
/** True only when the claimed npub is backed by something. */
|
|
24
|
-
export declare function isProven({ npub,
|
|
25
|
+
export declare function isProven({ npub, held, sessionNpub }: Claim): boolean;
|
|
25
26
|
/** True when this tab can sign a fresh proof for the npub it is claiming. */
|
|
26
27
|
export declare function canSignFor({ npub, sessionNpub }: Claim): boolean;
|
package/dist/signedIn.js
CHANGED
|
@@ -4,21 +4,18 @@
|
|
|
4
4
|
* An npub is public — it is on every note its owner ever wrote — so the npub
|
|
5
5
|
* alone is a claim, never a credential. A shell that read "we know an npub" as
|
|
6
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:
|
|
7
|
+
* themselves, unproven. Two things can back the claim, both on the connection:
|
|
8
8
|
*
|
|
9
|
-
* - a **
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* - a **DM proof**, won by answering a Nostr challenge — held by the operator
|
|
10
|
+
* for this tab's holder connection, or
|
|
11
|
+
* - a **session nsec** in this tab, which signs every request — and only
|
|
12
|
+
* counts for the npub it actually derives to.
|
|
12
13
|
*
|
|
13
14
|
* Pure, so it is testable without a browser.
|
|
14
15
|
*/
|
|
15
16
|
/** True only when the claimed npub is backed by something. */
|
|
16
|
-
export function isProven({ npub,
|
|
17
|
-
|
|
18
|
-
return false;
|
|
19
|
-
if (proof)
|
|
20
|
-
return true;
|
|
21
|
-
return sessionNpub === npub;
|
|
17
|
+
export function isProven({ npub, held, sessionNpub }) {
|
|
18
|
+
return Boolean(npub) && (held || sessionNpub === npub);
|
|
22
19
|
}
|
|
23
20
|
/** True when this tab can sign a fresh proof for the npub it is claiming. */
|
|
24
21
|
export function canSignFor({ npub, sessionNpub }) {
|
package/dist/standardTools.d.ts
CHANGED
|
@@ -53,7 +53,10 @@ export interface NpubProofResult {
|
|
|
53
53
|
proven_npub?: string;
|
|
54
54
|
status?: string;
|
|
55
55
|
message?: string;
|
|
56
|
-
|
|
56
|
+
/** From `request_npub_proof`: the phrase the DM shows; it names the reply. */
|
|
57
|
+
nonce?: string;
|
|
58
|
+
/** From `receive_npub_proof`: the operator now holds the npub for this tab's connection. */
|
|
59
|
+
held_by_connection?: boolean;
|
|
57
60
|
expires_in_seconds?: number;
|
|
58
61
|
expires_at?: string;
|
|
59
62
|
error?: string;
|
|
@@ -69,18 +72,21 @@ export interface NpubProofResult {
|
|
|
69
72
|
*/
|
|
70
73
|
export declare function requestNpubProof(patronNpub: string, verifyAt?: string, reason?: string, opts?: CallOptions): Promise<NpubProofResult>;
|
|
71
74
|
/**
|
|
72
|
-
* Step 2: drain DMs for the signed reply to step 1
|
|
73
|
-
*
|
|
75
|
+
* Step 2: drain DMs for the signed reply to step 1, naming it by the `nonce`
|
|
76
|
+
* step 1 returned. The operator keeps the proof for this tab's holder
|
|
77
|
+
* connection. Call only after the human says they replied — it is
|
|
78
|
+
* destructive, so never poll it.
|
|
74
79
|
*/
|
|
75
|
-
export declare function receiveNpubProof(patronNpub: string,
|
|
80
|
+
export declare function receiveNpubProof(patronNpub: string, nonce: string, opts?: CallOptions): Promise<NpubProofResult>;
|
|
76
81
|
export interface ProofStatusResult {
|
|
77
82
|
success?: boolean;
|
|
78
|
-
status?: "valid" | "expired" | "
|
|
83
|
+
status?: "valid" | "expired" | "invalid" | "revoked" | "not_held" | string;
|
|
79
84
|
expires_in_seconds?: number | null;
|
|
80
85
|
error?: string;
|
|
81
86
|
error_code?: string;
|
|
82
87
|
}
|
|
83
|
-
|
|
88
|
+
/** Whether this tab's connection proves `patronNpub`, and until when. */
|
|
89
|
+
export declare function checkProofStatus(patronNpub: string, opts?: CallOptions): Promise<ProofStatusResult>;
|
|
84
90
|
/** One credit tranche: a purchase (or grant) and what is left of it. */
|
|
85
91
|
export interface CreditTranche {
|
|
86
92
|
id: string;
|
package/dist/standardTools.js
CHANGED
|
@@ -26,14 +26,17 @@ export function requestNpubProof(patronNpub, verifyAt, reason, opts) {
|
|
|
26
26
|
}, opts);
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* Step 2: drain DMs for the signed reply to step 1
|
|
30
|
-
*
|
|
29
|
+
* Step 2: drain DMs for the signed reply to step 1, naming it by the `nonce`
|
|
30
|
+
* step 1 returned. The operator keeps the proof for this tab's holder
|
|
31
|
+
* connection. Call only after the human says they replied — it is
|
|
32
|
+
* destructive, so never poll it.
|
|
31
33
|
*/
|
|
32
|
-
export function receiveNpubProof(patronNpub,
|
|
33
|
-
return callTool("receive_npub_proof", { patron_npub: patronNpub,
|
|
34
|
+
export function receiveNpubProof(patronNpub, nonce, opts) {
|
|
35
|
+
return callTool("receive_npub_proof", { patron_npub: patronNpub, nonce }, opts);
|
|
34
36
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
/** Whether this tab's connection proves `patronNpub`, and until when. */
|
|
38
|
+
export function checkProofStatus(patronNpub, opts) {
|
|
39
|
+
return callTool("check_proof_status", { patron_npub: patronNpub }, { bestEffort: true, ...opts });
|
|
37
40
|
}
|
|
38
41
|
export function checkBalance(opts) {
|
|
39
42
|
return callTool("check_balance", {}, opts);
|
package/dist/toolResult.d.ts
CHANGED
|
@@ -27,21 +27,21 @@ export interface ReadResult {
|
|
|
27
27
|
export declare function errorText(result: RawToolResult): string;
|
|
28
28
|
export declare function readToolResult(result: RawToolResult): ReadResult;
|
|
29
29
|
/**
|
|
30
|
-
* A soft proof failure: `{success: false, error_code: proof_required
|
|
31
|
-
*
|
|
32
|
-
* snake_case; an earlier uppercase comparison never matched, so the
|
|
33
|
-
* silently never fired. Normalised here, once.
|
|
30
|
+
* A soft proof failure: `{success: false, error_code: proof_required}` with no
|
|
31
|
+
* isError flag — the connection does not prove the npub. The wheel's codes are
|
|
32
|
+
* lowercase snake_case; an earlier uppercase comparison never matched, so the
|
|
33
|
+
* bounce silently never fired. Normalised here, once.
|
|
34
34
|
*/
|
|
35
35
|
export declare function proofBounceMessage(payload: unknown): string | null;
|
|
36
36
|
/**
|
|
37
|
-
* A
|
|
37
|
+
* A held proof grant the service refused for good: `{success: false,
|
|
38
38
|
* error_code: proof_invalid, reason}` where the reason is about the grant —
|
|
39
39
|
* expired, revoked, issued for another npub or operator, malformed. Retrying
|
|
40
|
-
*
|
|
40
|
+
* on it can never succeed, so the patron is signed out.
|
|
41
41
|
*
|
|
42
|
-
* Only meaningful when the call
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* Only meaningful when the call rode this tab's holder. A signed request
|
|
43
|
+
* shares the code and even the word `expired` (outside its window), but it is
|
|
44
|
+
* signed afresh for the next call and loses nothing.
|
|
45
45
|
*/
|
|
46
46
|
export declare function deadGrantMessage(payload: unknown): string | null;
|
|
47
47
|
/** Whether a payload reports failure, for the debug log's colouring. */
|
package/dist/toolResult.js
CHANGED
|
@@ -36,17 +36,17 @@ export function readToolResult(result) {
|
|
|
36
36
|
return { payload, images };
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
|
-
* A soft proof failure: `{success: false, error_code: proof_required
|
|
40
|
-
*
|
|
41
|
-
* snake_case; an earlier uppercase comparison never matched, so the
|
|
42
|
-
* silently never fired. Normalised here, once.
|
|
39
|
+
* A soft proof failure: `{success: false, error_code: proof_required}` with no
|
|
40
|
+
* isError flag — the connection does not prove the npub. The wheel's codes are
|
|
41
|
+
* lowercase snake_case; an earlier uppercase comparison never matched, so the
|
|
42
|
+
* bounce silently never fired. Normalised here, once.
|
|
43
43
|
*/
|
|
44
44
|
export function proofBounceMessage(payload) {
|
|
45
45
|
if (!payload || typeof payload !== "object")
|
|
46
46
|
return null;
|
|
47
47
|
const p = payload;
|
|
48
48
|
const code = String(p.error_code ?? "").toLowerCase();
|
|
49
|
-
if (p.success === false &&
|
|
49
|
+
if (p.success === false && code === "proof_required") {
|
|
50
50
|
return String(p.error ?? "Sign-in required.");
|
|
51
51
|
}
|
|
52
52
|
return null;
|
|
@@ -58,14 +58,14 @@ export function proofBounceMessage(payload) {
|
|
|
58
58
|
*/
|
|
59
59
|
const GRANT_STILL_GOOD = new Set(["operator_unknown", "revocations_unavailable"]);
|
|
60
60
|
/**
|
|
61
|
-
* A
|
|
61
|
+
* A held proof grant the service refused for good: `{success: false,
|
|
62
62
|
* error_code: proof_invalid, reason}` where the reason is about the grant —
|
|
63
63
|
* expired, revoked, issued for another npub or operator, malformed. Retrying
|
|
64
|
-
*
|
|
64
|
+
* on it can never succeed, so the patron is signed out.
|
|
65
65
|
*
|
|
66
|
-
* Only meaningful when the call
|
|
67
|
-
*
|
|
68
|
-
*
|
|
66
|
+
* Only meaningful when the call rode this tab's holder. A signed request
|
|
67
|
+
* shares the code and even the word `expired` (outside its window), but it is
|
|
68
|
+
* signed afresh for the next call and loses nothing.
|
|
69
69
|
*/
|
|
70
70
|
export function deadGrantMessage(payload) {
|
|
71
71
|
if (!payload || typeof payload !== "object")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tollbooth-dpyc/web",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.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",
|
package/dist/inlineProof.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Inline kind-27235 identity proof — Tactic 2 in the wheel's
|
|
3
|
-
* `identity_proof.verify_proof`. A fresh signed event per paid call, bound to:
|
|
4
|
-
* the sender pubkey (must match the claimed npub), the `u` tag (must match the
|
|
5
|
-
* runtime tool name, e.g. "chart_snapshot_display"), and `created_at` (within
|
|
6
|
-
* about 60 seconds of server time). The wheel verifies the Schnorr signature
|
|
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.
|
|
14
|
-
*/
|
|
15
|
-
/** 16 random bytes as 32 lowercase hex chars — Python's `secrets.token_hex(16)`. */
|
|
16
|
-
export declare function proofNonce(): string;
|
|
17
|
-
/** The JSON-stringified signed event, exactly what `verify_proof` expects. */
|
|
18
|
-
export declare function signInlineProof(runtimeToolName: string, secretKey?: Uint8Array): string;
|
package/dist/inlineProof.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Inline kind-27235 identity proof — Tactic 2 in the wheel's
|
|
3
|
-
* `identity_proof.verify_proof`. A fresh signed event per paid call, bound to:
|
|
4
|
-
* the sender pubkey (must match the claimed npub), the `u` tag (must match the
|
|
5
|
-
* runtime tool name, e.g. "chart_snapshot_display"), and `created_at` (within
|
|
6
|
-
* about 60 seconds of server time). The wheel verifies the Schnorr signature
|
|
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.
|
|
14
|
-
*/
|
|
15
|
-
import { finalizeEvent } from "nostr-tools";
|
|
16
|
-
import { getSessionNsecBytes } from "./sessionNsec.js";
|
|
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
|
-
}
|
|
23
|
-
/** The JSON-stringified signed event, exactly what `verify_proof` expects. */
|
|
24
|
-
export function signInlineProof(runtimeToolName, secretKey) {
|
|
25
|
-
const signed = finalizeEvent({
|
|
26
|
-
kind: PROOF_KIND,
|
|
27
|
-
created_at: Math.floor(Date.now() / 1000),
|
|
28
|
-
tags: [
|
|
29
|
-
["u", runtimeToolName],
|
|
30
|
-
["nonce", proofNonce()],
|
|
31
|
-
],
|
|
32
|
-
content: "",
|
|
33
|
-
}, secretKey ?? getSessionNsecBytes());
|
|
34
|
-
return JSON.stringify(signed);
|
|
35
|
-
}
|