@byokit/connect 0.4.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.5.0 (2026-10-17)
6
+
7
+ - 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."
8
+
5
9
  ## 0.4.0 (2026-10-12)
6
10
 
7
11
  - Dependency update: pins @byokit/secrets 0.5.1.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @byokit/connect
2
2
 
3
- Connect a person's Google, Drive, Notion, Canva or other remote MCP account.
3
+ Connect a person's online files, mail, calendar, notes, designs, code or other remote account.
4
4
  Sign-in opens the provider's own page. Credentials stay in the keystore the app
5
5
  passes, scoped to that person on their device. There is no account pool, proxy,
6
6
  ambient credential lookup, logging or extra opt-in switch.
@@ -129,6 +129,35 @@ sanitized diagnostic `cause` as `ConnectError`. A secret is required. This check
129
129
  client authentication only; it does not check consent-screen publishing, enabled
130
130
  APIs, redirect registration or scopes. Tests use injected fake endpoints only.
131
131
 
132
+ ## Connect GitHub with a device code
133
+
134
+ ```ts
135
+ import { connect, deviceSignIn } from '@byokit/connect';
136
+ import type { Keystore } from '@byokit/secrets';
137
+
138
+ declare const store: Keystore;
139
+ const options = { store, person: 'Umer', client: { id: 'your-github-oauth-app-client-id' }, scopes: ['repo'] };
140
+ const flow = await deviceSignIn('github', options);
141
+ // Show flow.userCode and flow.verificationUri (github.com/login/device);
142
+ // the person types the code there on any device.
143
+ await flow.finish(); // Resolves once they approve; call flow.cancel() when the sheet closes.
144
+
145
+ const github = connect('github', options);
146
+ await github.connected(); // true
147
+ ```
148
+
149
+ `deviceSignIn(provider, options)` starts an RFC 8628 device sign-in and returns
150
+ `{ userCode, verificationUri, expiresAt, finish, cancel }`; `expiresAt` is Unix
151
+ milliseconds. It needs the app's own OAuth client ID with device flow enabled;
152
+ there is no house GitHub client, and no `redirectUri`. `finish()` polls at the
153
+ provider's interval, adds 5 seconds on `slow_down`, and keeps waiting through a
154
+ poll that cannot get through. Only an approved code stores a grant, in the same
155
+ keystore record `connect()` reads, so `connected()`, `grant()` and `token()`
156
+ work afterwards. An expired code rejects with `ConnectError` code `expired`; a
157
+ person pressing Cancel on GitHub, or `cancel()`, rejects with `declined`. Both
158
+ store nothing. Any provider with `device: { code, token }` endpoints works the
159
+ same way.
160
+
132
161
  ## Loopback on a computer
133
162
 
134
163
  ```ts
@@ -330,7 +359,7 @@ sh scripts/test.sh 'packages/connect/test/*.test.ts'
330
359
  ```
331
360
 
332
361
  `packages/connect/test/journey-connect.test.ts` drives the built package the way a
333
- host app does — `connect`/`signIn`/`token`/`mcp`, the loopback listener, the house
362
+ host app does — `connect`/`signIn`/`deviceSignIn`/`token`/`mcp`, the loopback listener, the house
334
363
  Google client, `MailReader`/`MailSender` and `MailHistory`. Fake providers and a real
335
364
  loopback callback only. It covers PKCE/state and exact-callback binding, per-person
336
365
  isolation, refresh races and revocation, discovery/DCR/resource binding, rich MCP
package/dist/connect.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
2
  import { type ProviderId } from './providers.ts';
3
- import type { ConnectOptions, OAuthClient, Provider, SignIn, McpOptions, Grant, ClientVerification } from './types.ts';
3
+ import type { ConnectOptions, OAuthClient, Provider, SignIn, DeviceSignIn, McpOptions, Grant, ClientVerification } from './types.ts';
4
4
  /** One person's connection on one device; the host owns the store and browser. */
5
5
  export declare class Connection {
6
6
  readonly provider: Readonly<Provider>;
@@ -23,6 +23,9 @@ export declare class Connection {
23
23
  disconnect(): Promise<void>;
24
24
  /** Starts a fresh browser sign-in. No opt-in flag or server-held account is required. */
25
25
  signIn(): Promise<SignIn>;
26
+ /** RFC 8628 device sign-in with the app's own client: show `userCode` at `verificationUri`, then await `finish()`.
27
+ * The poll follows the token endpoint: pending waits, slow_down adds 5 s, and an unreachable poll waits for the next. */
28
+ deviceSignIn(): Promise<DeviceSignIn>;
26
29
  private register;
27
30
  private exchange;
28
31
  /** For trusted app code (mail/calendar/API calls). Never display or log this value. */
@@ -31,3 +34,4 @@ export declare class Connection {
31
34
  mcp(options?: McpOptions): Promise<Client>;
32
35
  }
33
36
  export declare function connect(provider: ProviderId | Provider | string, options: ConnectOptions): Connection;
37
+ export declare function deviceSignIn(provider: ProviderId | Provider, options: ConnectOptions): Promise<DeviceSignIn>;
package/dist/connect.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import WORDS from './words.json' with { type: 'json' };
2
2
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
3
3
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
4
- import { ConnectError, providerCause } from "./errors.js";
5
- import { discover } from "./discovery.js";
6
- import { endpoint, redirect, request, json } from "./http.js";
7
- import { providers } from "./providers.js";
4
+ import { ConnectError, providerCause } from './errors.js';
5
+ import { discover } from './discovery.js';
6
+ import { endpoint, redirect, request, json } from './http.js';
7
+ import { providers } from './providers.js';
8
8
  // Across handles, only the same store + person + connection share work. No global token cache.
9
9
  const stores = new WeakMap();
10
10
  function slotFor(store, key) {
@@ -35,6 +35,9 @@ function validClient(client) {
35
35
  throw new ConnectError('configuration');
36
36
  return { id: client.id, secret: client.secret, authMethod: client.authMethod };
37
37
  }
38
+ function wait(ms, signal) {
39
+ return new Promise(resolve => { const timer = setTimeout(resolve, ms); signal.addEventListener('abort', () => { clearTimeout(timer); resolve(); }, { once: true }); });
40
+ }
38
41
  function validTime(value) { return value === undefined || (typeof value === 'number' && Number.isFinite(value) && value >= 0); }
39
42
  /** One person's connection on one device; the host owns the store and browser. */
40
43
  export class Connection {
@@ -49,16 +52,18 @@ export class Connection {
49
52
  : target;
50
53
  this.provider = Object.freeze({ ...provider,
51
54
  oauth: provider.oauth && Object.freeze({ ...provider.oauth }),
55
+ device: provider.device && Object.freeze({ ...provider.device }),
52
56
  scopes: provider.scopes && Object.freeze([...provider.scopes]),
53
57
  extra: provider.extra && Object.freeze({ ...provider.extra }),
54
58
  });
55
59
  if (!options.person || !this.provider.id)
56
60
  throw new ConnectError('configuration');
57
- redirect(options.redirectUri);
61
+ if (options.redirectUri !== undefined)
62
+ redirect(options.redirectUri);
58
63
  if (this.provider.mcpUrl)
59
64
  endpoint(this.provider.mcpUrl);
60
65
  this.options = { ...options, scopes: options.scopes && Object.freeze([...options.scopes]), client: options.client && { ...options.client } };
61
- this.key = `byokit.connect:${JSON.stringify([options.person, this.provider.id, this.provider.mcpUrl ?? '', this.provider.oauth?.token ?? this.provider.issuer ?? '', options.client?.id ?? ''])}`;
66
+ this.key = `byokit.connect:${JSON.stringify([options.person, this.provider.id, this.provider.mcpUrl ?? '', this.provider.oauth?.token ?? this.provider.device?.token ?? this.provider.issuer ?? '', options.client?.id ?? ''])}`;
62
67
  this.slot = slotFor(options.store, this.key);
63
68
  }
64
69
  storeKey() {
@@ -120,6 +125,9 @@ export class Connection {
120
125
  }
121
126
  /** Starts a fresh browser sign-in. No opt-in flag or server-held account is required. */
122
127
  async signIn() {
128
+ const uri = this.options.redirectUri;
129
+ if (!uri)
130
+ throw new ConnectError('configuration');
123
131
  const epoch = ++this.slot.epoch;
124
132
  const info = await discover(this.provider, this.fetcher, this.timeout);
125
133
  const scopes = this.options.scopes ?? info.scopes;
@@ -128,7 +136,6 @@ export class Connection {
128
136
  throw new ConnectError('configuration');
129
137
  const state = random(), verifier = random();
130
138
  const challenge = base64url(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))));
131
- const uri = this.options.redirectUri;
132
139
  const url = endpoint(info.endpoints.authorize);
133
140
  const fields = { ...this.provider.extra, response_type: 'code', client_id: client.id, redirect_uri: uri, state,
134
141
  code_challenge: challenge, code_challenge_method: 'S256', ...(info.resource ? { resource: info.resource } : {}) };
@@ -181,6 +188,68 @@ export class Connection {
181
188
  });
182
189
  } };
183
190
  }
191
+ /** RFC 8628 device sign-in with the app's own client: show `userCode` at `verificationUri`, then await `finish()`.
192
+ * The poll follows the token endpoint: pending waits, slow_down adds 5 s, and an unreachable poll waits for the next. */
193
+ async deviceSignIn() {
194
+ const device = this.provider.device;
195
+ if (!device || !this.options.client)
196
+ throw new ConnectError('configuration');
197
+ const client = validClient(this.options.client);
198
+ const epoch = ++this.slot.epoch;
199
+ const scopes = this.options.scopes ?? this.provider.scopes ?? [];
200
+ const info = { endpoints: { authorize: endpoint(device.code).href, token: endpoint(device.token).href }, scopes };
201
+ const response = await request(this.fetcher, device.code, { method: 'POST',
202
+ headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },
203
+ body: new URLSearchParams({ client_id: client.id, ...(scopes.length ? { scope: scopes.join(' ') } : {}) }) }, this.timeout);
204
+ const m = await json(response, 'token');
205
+ if (!response.ok || m.error)
206
+ throw new ConnectError('token', response.status, providerCause(m, [client.id, client.secret ?? '']));
207
+ const lives = Number(m.expires_in), seconds = m.interval === undefined ? 5 : Number(m.interval);
208
+ if (typeof m.device_code !== 'string' || !m.device_code || typeof m.user_code !== 'string' || !m.user_code || typeof m.verification_uri !== 'string' ||
209
+ !Number.isFinite(lives) || lives <= 0 || !Number.isFinite(seconds) || seconds < 0)
210
+ throw new ConnectError('token');
211
+ const code = m.device_code, verificationUri = endpoint(m.verification_uri).href, expiresAt = this.now + lives * 1000;
212
+ const stop = new AbortController();
213
+ let interval = Math.max(1000, seconds * 1000), done;
214
+ const ended = () => stop.signal.aborted || epoch !== this.slot.epoch;
215
+ const poll = async () => {
216
+ for (;;) {
217
+ if (ended())
218
+ throw new ConnectError('declined');
219
+ if (this.now >= expiresAt)
220
+ throw new ConnectError('expired');
221
+ const tokens = await this.exchange(info, client, { grant_type: 'urn:ietf:params:oauth:grant-type:device_code', device_code: code }).catch((error) => {
222
+ if (!(error instanceof ConnectError))
223
+ throw error;
224
+ const answer = error.cause?.error;
225
+ if (answer === 'expired_token')
226
+ throw new ConnectError('expired', error.status, error.cause);
227
+ if (answer === 'access_denied')
228
+ throw new ConnectError('declined', error.status, error.cause);
229
+ if (answer === 'slow_down')
230
+ interval += 5000;
231
+ // A poll that cannot get through waits for the next, as a phone cuts a backgrounded app's network.
232
+ else if (answer !== 'authorization_pending' && error.code !== 'network')
233
+ throw error;
234
+ return undefined;
235
+ });
236
+ if (tokens)
237
+ return serial(this.slot, async () => {
238
+ if (ended())
239
+ throw new ConnectError('declined');
240
+ // GitHub lists granted scopes with commas; RFC 6749 with spaces.
241
+ if (tokens.scope !== undefined && !scopes.every(sc => tokens.scope.split(/[\s,]+/).includes(sc)))
242
+ throw new ConnectError('scope');
243
+ const key = await this.storeKey();
244
+ if (ended())
245
+ throw new ConnectError('declined');
246
+ await this.options.store.set(key, JSON.stringify({ ...info, client, tokens }));
247
+ });
248
+ await wait(interval, stop.signal);
249
+ }
250
+ };
251
+ return { userCode: m.user_code, verificationUri, expiresAt, finish: () => done ??= poll(), cancel: () => stop.abort() };
252
+ }
184
253
  async register(info) {
185
254
  if (!info.endpoints.register)
186
255
  throw new ConnectError('configuration');
@@ -307,3 +376,4 @@ export class Connection {
307
376
  }
308
377
  }
309
378
  export function connect(provider, options) { return new Connection(provider, options); }
379
+ export function deviceSignIn(provider, options) { return connect(provider, options).deviceSignIn(); }
package/dist/discovery.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { checkResourceAllowed } from '@modelcontextprotocol/sdk/shared/auth-utils.js';
2
- import { ConnectError } from "./errors.js";
3
- import { endpoint, json, request, strings } from "./http.js";
2
+ import { ConnectError } from './errors.js';
3
+ import { endpoint, json, request, strings } from './http.js';
4
4
  export async function discover(provider, fetcher, timeout) {
5
5
  if (provider.oauth) {
6
6
  for (const url of [provider.oauth.authorize, provider.oauth.token, provider.oauth.register].filter((x) => !!x))
package/dist/errors.js CHANGED
@@ -17,7 +17,7 @@ export function providerCause(body, secrets) {
17
17
  }
18
18
  return text.replace(/https?:\/\/[^\s<>"']+/gi, '[redacted]').replace(/(?:Bearer|Basic)\s+[^\s,;]+/gi, '[redacted]');
19
19
  };
20
- const standard = ['invalid_request', 'invalid_client', 'invalid_grant', 'unauthorized_client', 'unsupported_grant_type', 'invalid_scope', 'access_denied', 'server_error', 'temporarily_unavailable', 'invalid_client_metadata', 'invalid_redirect_uri'];
20
+ const standard = ['invalid_request', 'invalid_client', 'invalid_grant', 'unauthorized_client', 'unsupported_grant_type', 'invalid_scope', 'access_denied', 'server_error', 'temporarily_unavailable', 'invalid_client_metadata', 'invalid_redirect_uri', 'authorization_pending', 'slow_down', 'expired_token'];
21
21
  return Object.freeze({ error: standard.includes(body.error) && !values.includes(body.error) ? body.error : redact(body.error), ...(typeof body.error_description === 'string' ? { error_description: redact(body.error_description) } : {}) });
22
22
  }
23
23
  /** Plain message and optional sanitized OAuth cause; never includes raw response bodies. */
package/dist/history.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Owner-bound mail history and export. The credential (a Connection) stays the
2
2
  // only authentication; every call names its principal and anything but the bound
3
3
  // owner is denied before any provider fetch. This module never sends.
4
- import { MailReader, MailError } from "./mail.js";
4
+ import { MailReader, MailError } from './mail.js';
5
5
  export class HistoryError extends Error {
6
6
  code;
7
7
  status;
package/dist/http.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ConnectError } from "./errors.js";
1
+ import { ConnectError } from './errors.js';
2
2
  /** Remote secrets go over HTTPS; loopback HTTP is useful for local servers and tests. */
3
3
  export function endpoint(raw) {
4
4
  let u;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { connect, Connection } from './connect.ts';
1
+ export { connect, deviceSignIn, Connection } from './connect.ts';
2
2
  export { providers } from './providers.ts';
3
3
  export type { ProviderId } from './providers.ts';
4
4
  export { ConnectError } from './errors.ts';
@@ -7,6 +7,6 @@ export { MailHistory, HistoryError } from './history.ts';
7
7
  export type { HistoryFormat, HistoryErrorCode, MailHistoryOptions, HistoryQuery, HistoryExportQuery } from './history.ts';
8
8
  export type { MailErrorCode, MailCredential, MailReaderOptions, MailEnvelope, MailBody, MailMessage, MailPage, MailListOptions, MailGetOptions, MailSenderOptions, MailDraft, OutgoingMail, MailApproval, MailSendOptions, SentMail } from './mail.ts';
9
9
  export type { ConnectErrorCode, ProviderErrorCause } from './errors.ts';
10
- export type { Provider, OAuthClient, OAuthEndpoints, ConnectOptions, SignIn, McpOptions, Grant, ClientVerification } from './types.ts';
10
+ export type { Provider, OAuthClient, OAuthEndpoints, ConnectOptions, SignIn, DeviceSignIn, McpOptions, Grant, ClientVerification } from './types.ts';
11
11
  export { Client } from '@modelcontextprotocol/sdk/client/index.js';
12
12
  export * from '@modelcontextprotocol/sdk/types.js';
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- export { connect, Connection } from "./connect.js";
2
- export { providers } from "./providers.js";
3
- export { ConnectError } from "./errors.js";
4
- export { MailReader, MailSender, MailError } from "./mail.js";
5
- export { MailHistory, HistoryError } from "./history.js";
1
+ export { connect, deviceSignIn, Connection } from './connect.js';
2
+ export { providers } from './providers.js';
3
+ export { ConnectError } from './errors.js';
4
+ export { MailReader, MailSender, MailError } from './mail.js';
5
+ export { MailHistory, HistoryError } from './history.js';
6
6
  // Expose the pinned engine for typed protocol pass-through without a second SDK pin.
7
7
  export { Client } from '@modelcontextprotocol/sdk/client/index.js';
8
8
  export * from '@modelcontextprotocol/sdk/types.js';
package/dist/node.js CHANGED
@@ -2,8 +2,8 @@ import WORDS from './words.json' with { type: 'json' };
2
2
  import { createServer } from 'node:http';
3
3
  import { constants } from 'node:fs';
4
4
  import { open } from 'node:fs/promises';
5
- import { connect } from "./connect.js";
6
- import { ConnectError } from "./errors.js";
5
+ import { connect } from './connect.js';
6
+ import { ConnectError } from './errors.js';
7
7
  /** One-shot loopback listener on an OS-assigned port; closes on completion, cancel or timeout. */
8
8
  export async function connectLoopback(target, options) {
9
9
  let finish;
@@ -1,7 +1,7 @@
1
1
  export declare const providers: {
2
2
  readonly google: {
3
- readonly id: "google";
4
- readonly name: "Google";
3
+ readonly id: 'google';
4
+ readonly name: 'Google';
5
5
  readonly oauth: {
6
6
  authorize: string;
7
7
  token: string;
@@ -13,8 +13,8 @@ export declare const providers: {
13
13
  };
14
14
  };
15
15
  readonly drive: {
16
- readonly id: "drive";
17
- readonly name: "Google Drive";
16
+ readonly id: 'drive';
17
+ readonly name: 'Google Drive';
18
18
  readonly oauth: {
19
19
  authorize: string;
20
20
  token: string;
@@ -24,11 +24,11 @@ export declare const providers: {
24
24
  access_type: string;
25
25
  prompt: string;
26
26
  };
27
- readonly mcpUrl: "https://drivemcp.googleapis.com/mcp/v1";
27
+ readonly mcpUrl: 'https://drivemcp.googleapis.com/mcp/v1';
28
28
  };
29
29
  readonly gmail: {
30
- readonly id: "gmail";
31
- readonly name: "Gmail";
30
+ readonly id: 'gmail';
31
+ readonly name: 'Gmail';
32
32
  readonly oauth: {
33
33
  authorize: string;
34
34
  token: string;
@@ -40,8 +40,8 @@ export declare const providers: {
40
40
  };
41
41
  };
42
42
  readonly calendar: {
43
- readonly id: "calendar";
44
- readonly name: "Google Calendar";
43
+ readonly id: 'calendar';
44
+ readonly name: 'Google Calendar';
45
45
  readonly oauth: {
46
46
  authorize: string;
47
47
  token: string;
@@ -53,14 +53,22 @@ export declare const providers: {
53
53
  };
54
54
  };
55
55
  readonly notion: {
56
- readonly id: "notion";
57
- readonly name: "Notion";
58
- readonly mcpUrl: "https://mcp.notion.com/mcp";
56
+ readonly id: 'notion';
57
+ readonly name: 'Notion';
58
+ readonly mcpUrl: 'https://mcp.notion.com/mcp';
59
59
  };
60
60
  readonly canva: {
61
- readonly id: "canva";
62
- readonly name: "Canva";
63
- readonly mcpUrl: "https://mcp.canva.com/mcp";
61
+ readonly id: 'canva';
62
+ readonly name: 'Canva';
63
+ readonly mcpUrl: 'https://mcp.canva.com/mcp';
64
+ };
65
+ readonly github: {
66
+ readonly id: 'github';
67
+ readonly name: 'GitHub';
68
+ readonly device: {
69
+ readonly code: 'https://github.com/login/device/code';
70
+ readonly token: 'https://github.com/login/oauth/access_token';
71
+ };
64
72
  };
65
73
  };
66
74
  export type ProviderId = keyof typeof providers;
package/dist/providers.js CHANGED
@@ -7,4 +7,5 @@ export const providers = {
7
7
  calendar: { id: 'calendar', name: 'Google Calendar', oauth: googleOAuth, scopes: ['https://www.googleapis.com/auth/calendar.events'], extra: googleExtra },
8
8
  notion: { id: 'notion', name: 'Notion', mcpUrl: 'https://mcp.notion.com/mcp' },
9
9
  canva: { id: 'canva', name: 'Canva', mcpUrl: 'https://mcp.canva.com/mcp' },
10
+ github: { id: 'github', name: 'GitHub', device: { code: 'https://github.com/login/device/code', token: 'https://github.com/login/oauth/access_token' } },
10
11
  };
package/dist/types.d.ts CHANGED
@@ -17,6 +17,11 @@ export interface Provider {
17
17
  mcpUrl?: string;
18
18
  issuer?: string;
19
19
  oauth?: OAuthEndpoints;
20
+ /** RFC 8628 device authorization: the person types a code on any device instead of returning to a redirect. */
21
+ device?: {
22
+ code: string;
23
+ token: string;
24
+ };
20
25
  scopes?: readonly string[];
21
26
  /** Provider-specific authorization parameters; PKCE/state cannot be overridden. */
22
27
  extra?: Readonly<Record<string, string>>;
@@ -26,8 +31,8 @@ export interface ConnectOptions {
26
31
  store: Keystore;
27
32
  /** Distinguishes connections belonging to different people sharing a store. */
28
33
  person: string;
29
- /** HTTPS, loopback HTTP, or the host's registered native app URI. */
30
- redirectUri: string;
34
+ /** HTTPS, loopback HTTP, or the host's registered native app URI. Needed by `signIn()`, not by `deviceSignIn()`. */
35
+ redirectUri?: string;
31
36
  client?: OAuthClient;
32
37
  clientName?: string;
33
38
  scopes?: readonly string[];
@@ -43,6 +48,16 @@ export interface SignIn {
43
48
  finish(callback: string | URL): Promise<void>;
44
49
  cancel(): void;
45
50
  }
51
+ export interface DeviceSignIn {
52
+ /** Show this code and `verificationUri`; the person types the code there on any device. */
53
+ readonly userCode: string;
54
+ readonly verificationUri: string;
55
+ /** Unix milliseconds when the code stops working. */
56
+ readonly expiresAt: number;
57
+ /** Waits for the person; stores the grant only when they approve. The same promise on every call. */
58
+ finish(): Promise<void>;
59
+ cancel(): void;
60
+ }
46
61
  export interface McpOptions {
47
62
  clientInfo?: {
48
63
  name: string;
package/dist/words.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "discovery": "Could not find the app’s sign-in page.",
5
5
  "registration": "The app could not set up sign-in. Try again.",
6
6
  "callback": "This sign-in link does not match. Start again.",
7
- "expired": "This sign-in link has expired. Start again.",
7
+ "expired": "This sign-in has expired. Start again.",
8
8
  "declined": "Nothing was connected. Try again whenever you like.",
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.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byokit/connect",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
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",