@byokit/connect 0.5.0 → 0.5.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/CHANGELOG.md +4 -0
- package/README.md +42 -2
- package/dist/catalog.d.ts +26 -0
- package/dist/catalog.js +76 -0
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/words.json +2 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.5.1 (2026-10-17)
|
|
6
|
+
|
|
7
|
+
- Dependency update: pins @byokit/secrets 0.5.2.
|
|
8
|
+
|
|
5
9
|
## 0.5.0 (2026-10-17)
|
|
6
10
|
|
|
7
11
|
- FEAT: `deviceSignIn(provider, options)` connects a person with an RFC 8628 device code: it returns `{ userCode, verificationUri, expiresAt, finish(), cancel() }`, polls with the app's own OAuth client ID (pending waits, `slow_down` adds 5 s), and stores the grant in the person's keystore only when they approve. An expired code rejects with `ConnectError` `expired`; a person cancelling on the provider's page, or `cancel()`, rejects with `declined`, and neither stores anything. New `github` provider (`device` endpoints on github.com) and `Provider.device`; `ConnectOptions.redirectUri` is now optional (`signIn()` still requires it). RFC 8628 error codes stay readable in `ConnectError.cause`. The `expired` message now reads "This sign-in has expired. Start again."
|
package/README.md
CHANGED
|
@@ -218,6 +218,44 @@ A 401 triggers one refresh/retry; redirects are refused. Close clients before
|
|
|
218
218
|
`disconnect()` to release their streams. Arbitrary unauthenticated MCP servers and
|
|
219
219
|
legacy HTTP+SSE transports are outside this sign-in kit's initial scope.
|
|
220
220
|
|
|
221
|
+
## List apps to connect
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
import { catalog, connect, knownApps, mcpDirectory } from '@byokit/connect';
|
|
225
|
+
import type { Keystore } from '@byokit/secrets';
|
|
226
|
+
|
|
227
|
+
declare const store: Keystore;
|
|
228
|
+
const apps = await catalog([knownApps, mcpDirectory('https://directory.your-app.example/v0/servers?version=latest')]);
|
|
229
|
+
const app = apps[0]; // { id, name, icon?, auth, ...the Provider connect() takes }
|
|
230
|
+
const connection = connect(app, { store, person: 'Umer', redirectUri: 'https://your-app.example/connect/callback' });
|
|
231
|
+
const flow = await connection.signIn(); // Or connectLoopback(app, ...); deviceSignIn(app, ...) when app.auth is 'device'.
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`catalog(sources)` lists the apps of the sources the host picks, in that order; an
|
|
235
|
+
id listed twice keeps its first entry. Nothing is listed, and nothing is read, by
|
|
236
|
+
default. Every listed app is a `Provider`, so it signs in through the same
|
|
237
|
+
`connect()`, `connectLoopback()` and `deviceSignIn()` as above, under the same
|
|
238
|
+
keystore record as the preset of that id. `auth` says how: `oauth` needs the app's
|
|
239
|
+
registered `client`, `discovery` finds sign-in from the app's MCP server and
|
|
240
|
+
registers itself where offered, `device` shows a typed code. `icon` is an HTTPS
|
|
241
|
+
image address from the listing; render it as untrusted content.
|
|
242
|
+
|
|
243
|
+
- `knownApps` lists the kit's presets (`providers`) and reads nothing.
|
|
244
|
+
- `mcpDirectory(url, { fetch?, timeoutMs? })` reads a remote MCP directory that
|
|
245
|
+
answers in the MCP registry's `GET /v0/servers` shape, following
|
|
246
|
+
`metadata.nextCursor` (at most 100 pages), only when listed. It keeps servers
|
|
247
|
+
with a streamable HTTP remote and no address variables or required headers;
|
|
248
|
+
local packages, legacy SSE remotes and key-header servers are left out. The URL
|
|
249
|
+
must be HTTPS (or loopback); for the MCP registry add `version=latest`.
|
|
250
|
+
- Any object with `list(): Promise<CatalogApp[]>` is a source, such as the host's
|
|
251
|
+
own managed connector directory behind its server.
|
|
252
|
+
|
|
253
|
+
A directory that fails or answers in another shape rejects with `ConnectError`
|
|
254
|
+
code `catalog` (with its `status`), an unreachable one with `network`; the whole
|
|
255
|
+
`catalog()` call rejects with it, so list a source on its own to keep the rest.
|
|
256
|
+
The host vouches for the directories it names: signing in to a listed app talks
|
|
257
|
+
to the server that listing names.
|
|
258
|
+
|
|
221
259
|
## Read and search Gmail
|
|
222
260
|
|
|
223
261
|
```ts
|
|
@@ -342,7 +380,8 @@ the connection handle; never persist its returned access token separately.
|
|
|
342
380
|
## Crewhouse adoption
|
|
343
381
|
|
|
344
382
|
Replace the OAuth, token-file and `RemoteMcp` portions of `src/connections.ts` with
|
|
345
|
-
one handle per member/app
|
|
383
|
+
one handle per member/app, listing the apps from `catalog([knownApps])` (add `mcpDirectory(url)`
|
|
384
|
+
or the host's own source to list more). Pass the member's keystore and member ID, the house Google
|
|
346
385
|
client from `googleClientFile` when applicable, and Crewhouse's callback address. Keep
|
|
347
386
|
flow handles in the host's callback routing table until completion/cancellation.
|
|
348
387
|
Crewhouse retains its own connection screen, Google setup guidance and tool gates.
|
|
@@ -359,7 +398,8 @@ sh scripts/test.sh 'packages/connect/test/*.test.ts'
|
|
|
359
398
|
```
|
|
360
399
|
|
|
361
400
|
`packages/connect/test/journey-connect.test.ts` drives the built package the way a
|
|
362
|
-
host app does — `connect`/`signIn`/`deviceSignIn`/`token`/`mcp`, the
|
|
401
|
+
host app does — `connect`/`signIn`/`deviceSignIn`/`token`/`mcp`, `catalog` over the known apps and a
|
|
402
|
+
loopback MCP directory (one listed app connected end to end), the loopback listener, the house
|
|
363
403
|
Google client, `MailReader`/`MailSender` and `MailHistory`. Fake providers and a real
|
|
364
404
|
loopback callback only. It covers PKCE/state and exact-callback binding, per-person
|
|
365
405
|
isolation, refresh races and revocation, discovery/DCR/resource binding, rich MCP
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Provider } from './types.ts';
|
|
2
|
+
/** How a listed app signs in: `oauth` with the app's own registered client, `discovery` from its MCP server
|
|
3
|
+
* (registering itself where the server offers it), `device` with a typed code and the app's own client. */
|
|
4
|
+
export type CatalogAuth = 'oauth' | 'discovery' | 'device';
|
|
5
|
+
/** A listed app is a `Provider`: pass it to `connect()`, `connectLoopback()` or, for `device`, `deviceSignIn()`. */
|
|
6
|
+
export interface CatalogApp extends Provider {
|
|
7
|
+
/** An HTTPS (or loopback) image address from the listing; render it as untrusted content. */
|
|
8
|
+
icon?: string;
|
|
9
|
+
auth: CatalogAuth;
|
|
10
|
+
}
|
|
11
|
+
/** Any source of apps a host can list; the kit ships `knownApps` and `mcpDirectory(url)`. */
|
|
12
|
+
export interface CatalogProvider {
|
|
13
|
+
list(): Promise<readonly CatalogApp[]>;
|
|
14
|
+
}
|
|
15
|
+
export interface DirectoryOptions {
|
|
16
|
+
fetch?: typeof fetch;
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
}
|
|
19
|
+
/** The kit's hand-known apps (`providers`); reads nothing. */
|
|
20
|
+
export declare const knownApps: CatalogProvider;
|
|
21
|
+
/** A remote MCP directory at the caller's HTTPS (or loopback) URL, answering in the MCP registry's `GET /v0/servers`
|
|
22
|
+
* shape and paged by `metadata.nextCursor`. Nothing is read until `list()`. The caller vouches for the directory:
|
|
23
|
+
* signing in to a listed app talks to the server it names. */
|
|
24
|
+
export declare function mcpDirectory(url: string, options?: DirectoryOptions): CatalogProvider;
|
|
25
|
+
/** Lists the apps of the providers the caller picks, in their order; an id listed twice keeps its first entry. */
|
|
26
|
+
export declare function catalog(sources: readonly CatalogProvider[]): Promise<CatalogApp[]>;
|
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { ConnectError } from './errors.js';
|
|
2
|
+
import { endpoint, json, request } from './http.js';
|
|
3
|
+
import { providers } from './providers.js';
|
|
4
|
+
function listed(provider, icon) {
|
|
5
|
+
return { ...provider, ...(icon ? { icon } : {}), auth: provider.device ? 'device' : provider.oauth ? 'oauth' : 'discovery' };
|
|
6
|
+
}
|
|
7
|
+
function web(raw) {
|
|
8
|
+
if (typeof raw !== 'string')
|
|
9
|
+
return undefined;
|
|
10
|
+
try {
|
|
11
|
+
return endpoint(raw).href;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** One MCP registry `server.json` entry, kept only when the kit can sign in to it: a streamable HTTP remote with no
|
|
18
|
+
* address variables or required headers for the person to fill in. */
|
|
19
|
+
function fromServer(server) {
|
|
20
|
+
if (!server || typeof server !== 'object')
|
|
21
|
+
return undefined;
|
|
22
|
+
const { name, title, remotes, icons } = server;
|
|
23
|
+
if (typeof name !== 'string' || !name)
|
|
24
|
+
return undefined;
|
|
25
|
+
const remote = Array.isArray(remotes) ? remotes.find(r => r?.type === 'streamable-http' && typeof r.url === 'string' && !r.url.includes('{') &&
|
|
26
|
+
!(Array.isArray(r.headers) && r.headers.some((h) => h?.isRequired))) : undefined;
|
|
27
|
+
const mcpUrl = web(remote?.url);
|
|
28
|
+
if (!mcpUrl)
|
|
29
|
+
return undefined;
|
|
30
|
+
const icon = Array.isArray(icons) ? icons.map(i => web(i?.src)).find(Boolean) : undefined;
|
|
31
|
+
return listed({ id: name, name: typeof title === 'string' && title ? title : name, mcpUrl }, icon);
|
|
32
|
+
}
|
|
33
|
+
/** The kit's hand-known apps (`providers`); reads nothing. */
|
|
34
|
+
export const knownApps = { list: async () => Object.values(providers).map(provider => listed(provider)) };
|
|
35
|
+
/** A remote MCP directory at the caller's HTTPS (or loopback) URL, answering in the MCP registry's `GET /v0/servers`
|
|
36
|
+
* shape and paged by `metadata.nextCursor`. Nothing is read until `list()`. The caller vouches for the directory:
|
|
37
|
+
* signing in to a listed app talks to the server it names. */
|
|
38
|
+
export function mcpDirectory(url, options = {}) {
|
|
39
|
+
const base = endpoint(url);
|
|
40
|
+
return { list: async () => {
|
|
41
|
+
const apps = [];
|
|
42
|
+
let cursor;
|
|
43
|
+
// ponytail: at most 100 pages per list(), so a directory that keeps paging stays finite; raise it if a real directory outgrows it.
|
|
44
|
+
for (let page = 0; page < 100; page++) {
|
|
45
|
+
const next = new URL(base);
|
|
46
|
+
if (cursor)
|
|
47
|
+
next.searchParams.set('cursor', cursor);
|
|
48
|
+
const response = await request(options.fetch ?? globalThis.fetch, next, { headers: { accept: 'application/json' } }, options.timeoutMs ?? 20_000);
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
await response.body?.cancel();
|
|
51
|
+
throw new ConnectError('catalog', response.status);
|
|
52
|
+
}
|
|
53
|
+
const body = await json(response, 'catalog');
|
|
54
|
+
if (!Array.isArray(body.servers))
|
|
55
|
+
throw new ConnectError('catalog', response.status);
|
|
56
|
+
for (const entry of body.servers) {
|
|
57
|
+
const app = fromServer(entry?.server);
|
|
58
|
+
if (app)
|
|
59
|
+
apps.push(app);
|
|
60
|
+
}
|
|
61
|
+
const more = body.metadata?.nextCursor;
|
|
62
|
+
if (typeof more !== 'string' || !more || more === cursor)
|
|
63
|
+
break;
|
|
64
|
+
cursor = more;
|
|
65
|
+
}
|
|
66
|
+
return apps;
|
|
67
|
+
} };
|
|
68
|
+
}
|
|
69
|
+
/** Lists the apps of the providers the caller picks, in their order; an id listed twice keeps its first entry. */
|
|
70
|
+
export async function catalog(sources) {
|
|
71
|
+
const apps = new Map();
|
|
72
|
+
for (const app of (await Promise.all(sources.map(source => source.list()))).flat())
|
|
73
|
+
if (!apps.has(app.id))
|
|
74
|
+
apps.set(app.id, app);
|
|
75
|
+
return [...apps.values()];
|
|
76
|
+
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ConnectErrorCode = 'configuration' | 'discovery' | 'registration' | 'callback' | 'expired' | 'declined' | 'scope' | 'token' | 'signin' | 'network';
|
|
1
|
+
export type ConnectErrorCode = 'configuration' | 'discovery' | 'registration' | 'callback' | 'expired' | 'declined' | 'scope' | 'token' | 'signin' | 'network' | 'catalog';
|
|
2
2
|
export interface ProviderErrorCause {
|
|
3
3
|
readonly error: string;
|
|
4
4
|
readonly error_description?: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { connect, deviceSignIn, Connection } from './connect.ts';
|
|
2
2
|
export { providers } from './providers.ts';
|
|
3
3
|
export type { ProviderId } from './providers.ts';
|
|
4
|
+
export { catalog, knownApps, mcpDirectory } from './catalog.ts';
|
|
5
|
+
export type { CatalogApp, CatalogAuth, CatalogProvider, DirectoryOptions } from './catalog.ts';
|
|
4
6
|
export { ConnectError } from './errors.ts';
|
|
5
7
|
export { MailReader, MailSender, MailError } from './mail.ts';
|
|
6
8
|
export { MailHistory, HistoryError } from './history.ts';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { connect, deviceSignIn, Connection } from './connect.js';
|
|
2
2
|
export { providers } from './providers.js';
|
|
3
|
+
export { catalog, knownApps, mcpDirectory } from './catalog.js';
|
|
3
4
|
export { ConnectError } from './errors.js';
|
|
4
5
|
export { MailReader, MailSender, MailError } from './mail.js';
|
|
5
6
|
export { MailHistory, HistoryError } from './history.js';
|
package/dist/words.json
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"scope": "Some permissions were left out. Sign in again and select them.",
|
|
10
10
|
"token": "The app did not finish connecting. Try again.",
|
|
11
11
|
"signin": "Sign in to this app again.",
|
|
12
|
-
"network": "Could not reach the app. Check your connection and try again."
|
|
12
|
+
"network": "Could not reach the app. Check your connection and try again.",
|
|
13
|
+
"catalog": "Could not load the list of apps. Try again later."
|
|
13
14
|
},
|
|
14
15
|
"browser": {
|
|
15
16
|
"unavailable": "This page is not available.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byokit/connect",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Connect a person's third-party apps with PKCE, refresh and typed remote MCP.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -37,6 +37,6 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@modelcontextprotocol/sdk": "1.31.0",
|
|
40
|
-
"@byokit/secrets": "0.5.
|
|
40
|
+
"@byokit/secrets": "0.5.2"
|
|
41
41
|
}
|
|
42
42
|
}
|