@tollbooth-dpyc/web 2.0.0 → 2.1.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/holder.d.ts +7 -15
- package/dist/holder.js +19 -31
- package/dist/react/NpubGate.js +6 -24
- package/package.json +1 -1
package/dist/holder.d.ts
CHANGED
|
@@ -5,26 +5,18 @@
|
|
|
5
5
|
* the connection's OAuth tokens and names the npub on each call.
|
|
6
6
|
*
|
|
7
7
|
* Becoming a holder is the OAuth Authorization Code flow every MCP client
|
|
8
|
-
* runs: register,
|
|
9
|
-
* at once, since a holder holds nothing until a DM proves an npub
|
|
10
|
-
* and
|
|
11
|
-
*
|
|
8
|
+
* runs, done by fetch: register, ask the operator's `/authorize` for a code
|
|
9
|
+
* (it approves at once, since a holder holds nothing until a DM proves an npub
|
|
10
|
+
* into it, and answers a page in JSON), and exchange the code. The page never
|
|
11
|
+
* leaves itself. Tokens refresh on their own; a refresh token is single-use,
|
|
12
|
+
* so each refresh stores the pair it returns.
|
|
12
13
|
*/
|
|
13
14
|
/** True when this site connects as a holder (its operator supports it). */
|
|
14
15
|
export declare function holderEnabled(): boolean;
|
|
15
16
|
/** True when this tab already is a holder. */
|
|
16
17
|
export declare function isHolder(): boolean;
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
* sends it straight back. `resume` comes back from `completeHolderConnect`.
|
|
20
|
-
*/
|
|
21
|
-
export declare function connectAsHolder(resume: string): Promise<void>;
|
|
22
|
-
/**
|
|
23
|
-
* On return from the operator: exchange the code, store the tokens, tidy the
|
|
24
|
-
* address bar, and hand back what the sign-in was doing. Null when this page
|
|
25
|
-
* load is not a return.
|
|
26
|
-
*/
|
|
27
|
-
export declare function completeHolderConnect(): Promise<string | null>;
|
|
18
|
+
/** Become a holder: register, take a code from the operator by fetch, and exchange it. */
|
|
19
|
+
export declare function connectAsHolder(): Promise<void>;
|
|
28
20
|
/** The bearer for the next request, refreshed when it is about to lapse; null when not a holder. */
|
|
29
21
|
export declare function holderBearer(): Promise<string | null>;
|
|
30
22
|
/** Disconnect: the operator forgets this holder and every npub it held. */
|
package/dist/holder.js
CHANGED
|
@@ -5,15 +5,15 @@
|
|
|
5
5
|
* the connection's OAuth tokens and names the npub on each call.
|
|
6
6
|
*
|
|
7
7
|
* Becoming a holder is the OAuth Authorization Code flow every MCP client
|
|
8
|
-
* runs: register,
|
|
9
|
-
* at once, since a holder holds nothing until a DM proves an npub
|
|
10
|
-
* and
|
|
11
|
-
*
|
|
8
|
+
* runs, done by fetch: register, ask the operator's `/authorize` for a code
|
|
9
|
+
* (it approves at once, since a holder holds nothing until a DM proves an npub
|
|
10
|
+
* into it, and answers a page in JSON), and exchange the code. The page never
|
|
11
|
+
* leaves itself. Tokens refresh on their own; a refresh token is single-use,
|
|
12
|
+
* so each refresh stores the pair it returns.
|
|
12
13
|
*/
|
|
13
14
|
import { tollboothConfig } from "./config.js";
|
|
14
15
|
import { readStored, removeStored, writeStored } from "./storage.js";
|
|
15
16
|
const TOKENS = "holder:v1";
|
|
16
|
-
const PENDING = "holder_pending:v1";
|
|
17
17
|
/** Refresh this long before the access token lapses. */
|
|
18
18
|
const REFRESH_EARLY_MS = 60_000;
|
|
19
19
|
function operator() {
|
|
@@ -26,6 +26,7 @@ function operator() {
|
|
|
26
26
|
export function holderEnabled() {
|
|
27
27
|
return Boolean(tollboothConfig().operatorUrl);
|
|
28
28
|
}
|
|
29
|
+
/** The address the code is issued to. Registered and named, never visited. */
|
|
29
30
|
function redirectUri() {
|
|
30
31
|
return `${globalThis.location.origin}/`;
|
|
31
32
|
}
|
|
@@ -85,16 +86,11 @@ async function token(form, clientId) {
|
|
|
85
86
|
export function isHolder() {
|
|
86
87
|
return readTokens() !== null;
|
|
87
88
|
}
|
|
88
|
-
/**
|
|
89
|
-
|
|
90
|
-
* sends it straight back. `resume` comes back from `completeHolderConnect`.
|
|
91
|
-
*/
|
|
92
|
-
export async function connectAsHolder(resume) {
|
|
89
|
+
/** Become a holder: register, take a code from the operator by fetch, and exchange it. */
|
|
90
|
+
export async function connectAsHolder() {
|
|
93
91
|
const clientId = readTokens()?.clientId || (await register());
|
|
94
92
|
const verifier = randomUrlSafe(48);
|
|
95
93
|
const state = randomUrlSafe(16);
|
|
96
|
-
const pending = { clientId, verifier, state, resume };
|
|
97
|
-
globalThis.sessionStorage.setItem(PENDING, JSON.stringify(pending));
|
|
98
94
|
const url = new URL(`${operator()}/authorize`);
|
|
99
95
|
url.search = new URLSearchParams({
|
|
100
96
|
response_type: "code",
|
|
@@ -105,26 +101,18 @@ export async function connectAsHolder(resume) {
|
|
|
105
101
|
code_challenge_method: "S256",
|
|
106
102
|
resource: `${operator()}/mcp`,
|
|
107
103
|
}).toString();
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
export async function completeHolderConnect() {
|
|
116
|
-
const params = new URLSearchParams(globalThis.location.search);
|
|
117
|
-
const code = params.get("code");
|
|
118
|
-
const raw = globalThis.sessionStorage.getItem(PENDING);
|
|
119
|
-
if (!code || !raw)
|
|
120
|
-
return null;
|
|
121
|
-
globalThis.sessionStorage.removeItem(PENDING);
|
|
122
|
-
const pending = JSON.parse(raw);
|
|
123
|
-
globalThis.history.replaceState(null, "", globalThis.location.pathname);
|
|
124
|
-
if (params.get("state") !== pending.state)
|
|
104
|
+
const r = await fetch(url, { headers: { Accept: "application/json" } });
|
|
105
|
+
const answer = await r.json().catch(() => ({}));
|
|
106
|
+
if (!r.ok || typeof answer.redirect_uri !== "string") {
|
|
107
|
+
throw new Error(answer.error_description || `The service would not connect this page (${r.status}).`);
|
|
108
|
+
}
|
|
109
|
+
const issued = new URL(answer.redirect_uri).searchParams;
|
|
110
|
+
if (issued.get("state") !== state)
|
|
125
111
|
throw new Error("The service's reply did not match this sign-in.");
|
|
126
|
-
|
|
127
|
-
|
|
112
|
+
const code = issued.get("code");
|
|
113
|
+
if (!code)
|
|
114
|
+
throw new Error(issued.get("error_description") || "The service did not connect this page.");
|
|
115
|
+
writeTokens(await token({ grant_type: "authorization_code", code, redirect_uri: redirectUri(), code_verifier: verifier }, clientId));
|
|
128
116
|
}
|
|
129
117
|
/** The bearer for the next request, refreshed when it is about to lapse; null when not a holder. */
|
|
130
118
|
export async function holderBearer() {
|
package/dist/react/NpubGate.js
CHANGED
|
@@ -19,7 +19,7 @@ import { useEffect, useRef, useState } from "react";
|
|
|
19
19
|
import { generateSecretKey, getPublicKey, nip19 } from "nostr-tools";
|
|
20
20
|
import { tollboothConfig } from "../config.js";
|
|
21
21
|
import { forgetRecentLogin, getLastTypedNpub, getValidRecentLogins, recordRecentLogin, setLastTypedNpub, setStoredNpub, } from "../identity.js";
|
|
22
|
-
import {
|
|
22
|
+
import { connectAsHolder, holderEnabled, isHolder } from "../holder.js";
|
|
23
23
|
import { setSessionNsec } from "../sessionNsec.js";
|
|
24
24
|
import { readSignInFailure } from "../signInSituation.js";
|
|
25
25
|
import { receiveNpubProof, requestNpubProof } from "../standardTools.js";
|
|
@@ -109,24 +109,8 @@ export default function NpubGate({ onLogin, startFresh = false, operatorHash, no
|
|
|
109
109
|
setError(`Couldn't read that nsec: ${e.message}`);
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
-
|
|
113
|
-
|
|
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;
|
|
112
|
+
async function begin() {
|
|
113
|
+
const npubToProve = trimmed;
|
|
130
114
|
if (!(npubToProve.startsWith("npub1") && npubToProve.length >= 60)) {
|
|
131
115
|
setError("Enter a valid npub1… key.");
|
|
132
116
|
return;
|
|
@@ -135,15 +119,13 @@ export default function NpubGate({ onLogin, startFresh = false, operatorHash, no
|
|
|
135
119
|
setError("");
|
|
136
120
|
try {
|
|
137
121
|
// The proof will stay with the operator, so this tab connects to it
|
|
138
|
-
// first
|
|
122
|
+
// first, by fetch, and the DM goes out.
|
|
139
123
|
if (!holderEnabled()) {
|
|
140
124
|
setError("This site signs in with a key only — paste an nsec, or make one.");
|
|
141
125
|
return;
|
|
142
126
|
}
|
|
143
|
-
if (!isHolder())
|
|
144
|
-
await connectAsHolder(
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
127
|
+
if (!isHolder())
|
|
128
|
+
await connectAsHolder();
|
|
147
129
|
const r = await requestNpubProof(npubToProve, globalThis.location.origin, `You requested to sign in to ${appName} (${globalThis.location.host}).`);
|
|
148
130
|
if (r.error)
|
|
149
131
|
return setError(r.error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tollbooth-dpyc/web",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.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",
|