@byokit/connect 0.1.0 → 0.2.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 +5 -0
- package/README.md +38 -4
- package/SECURITY.md +5 -2
- package/dist/connect.d.ts +6 -1
- package/dist/connect.js +57 -9
- package/dist/errors.d.ts +9 -2
- package/dist/errors.js +23 -3
- package/dist/index.d.ts +2 -2
- package/dist/types.d.ts +23 -0
- package/dist/words.json +5 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.2.0 (2026-10-01)
|
|
6
|
+
|
|
7
|
+
- FIX: Preserve provider refresh-token lifetimes and sanitized OAuth error causes for setup guidance.
|
|
8
|
+
- Add a client-details check without a user grant, with plain outcomes for accepted, rejected and uncertain replies.
|
|
9
|
+
|
|
5
10
|
## 0.1.0 (2026-10-01)
|
|
6
11
|
|
|
7
12
|
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ import type { Keystore } from '@byokit/secrets';
|
|
|
24
24
|
declare const store: Keystore;
|
|
25
25
|
const drive = connect('drive', {
|
|
26
26
|
store,
|
|
27
|
-
person: '
|
|
27
|
+
person: 'Umer',
|
|
28
28
|
redirectUri: 'https://your-app.example/connect/callback',
|
|
29
29
|
client: { id: 'your-google-client-id', secret: 'your-google-client-secret' },
|
|
30
30
|
});
|
|
@@ -60,6 +60,37 @@ parameters. Options `scopes` override the preset/discovered scopes; security
|
|
|
60
60
|
parameters (`state`, PKCE, response type, client and redirect) cannot be overridden
|
|
61
61
|
through `extra`. A sign-in with explicitly incomplete granted scopes is not saved.
|
|
62
62
|
|
|
63
|
+
## Check app details and inspect grant lifetime
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import type { Connection } from '@byokit/connect';
|
|
67
|
+
|
|
68
|
+
declare const drive: Connection;
|
|
69
|
+
declare const enteredClientId: string;
|
|
70
|
+
declare const enteredClientSecret: string;
|
|
71
|
+
|
|
72
|
+
const check = await drive.verifyClient(); // Uses options.client.
|
|
73
|
+
// Or check newly entered details without changing the saved connection:
|
|
74
|
+
const entered = await drive.verifyClient({ id: enteredClientId, secret: enteredClientSecret });
|
|
75
|
+
const grant = await drive.grant(); // Trusted app code only; contains credentials.
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`grant()` returns a snapshot or `null` and makes no provider call. Times are Unix
|
|
79
|
+
milliseconds. `refreshTokenExpiresIn` preserves the provider's
|
|
80
|
+
`refresh_token_expires_in` seconds; `refreshTokenExpiresAt` is its absolute deadline.
|
|
81
|
+
Sign-in/refresh responses calculate the deadline when received, and omitted refresh
|
|
82
|
+
lifetime fields retain the previous deadline. Expired refresh grants require
|
|
83
|
+
sign-in without sending another refresh request.
|
|
84
|
+
|
|
85
|
+
`verifyClient(client?)` uses the selected token endpoint with a deliberately
|
|
86
|
+
nonexistent refresh grant, without reading or changing the person's grant. For
|
|
87
|
+
Google, `invalid_client` yields `outcome: 'invalid'`; a 400 `invalid_grant` yields
|
|
88
|
+
`'valid'`; network failures and all other replies yield `'inconclusive'`. Each
|
|
89
|
+
outcome includes a plain `message`; failed provider replies may carry the same
|
|
90
|
+
sanitized diagnostic `cause` as `ConnectError`. A secret is required. This checks
|
|
91
|
+
client authentication only; it does not check consent-screen publishing, enabled
|
|
92
|
+
APIs, redirect registration or scopes. Tests use injected fake endpoints only.
|
|
93
|
+
|
|
63
94
|
## Loopback on a computer
|
|
64
95
|
|
|
65
96
|
```ts
|
|
@@ -70,7 +101,7 @@ declare const store: Keystore;
|
|
|
70
101
|
declare const yourApp: { openExternal(url: string): Promise<void> };
|
|
71
102
|
|
|
72
103
|
const attempt = await connectLoopback('notion', {
|
|
73
|
-
store, person: '
|
|
104
|
+
store, person: 'Umer',
|
|
74
105
|
open: url => yourApp.openExternal(url),
|
|
75
106
|
});
|
|
76
107
|
await attempt.done;
|
|
@@ -91,7 +122,7 @@ import type { Keystore } from '@byokit/secrets';
|
|
|
91
122
|
declare const store: Keystore;
|
|
92
123
|
|
|
93
124
|
const notion = connect('notion', {
|
|
94
|
-
store, person: '
|
|
125
|
+
store, person: 'Umer', redirectUri: 'https://your-app.example/connect/callback',
|
|
95
126
|
});
|
|
96
127
|
// Sign in once, then reconstruct this handle across app restarts.
|
|
97
128
|
const mcp = await notion.mcp({
|
|
@@ -131,7 +162,10 @@ failure; an expired token is never returned. Tokens without `expires_in` remain
|
|
|
131
162
|
valid until rejected, then refresh if possible. No background timer is needed.
|
|
132
163
|
|
|
133
164
|
`ConnectError.code` is a typed app diagnostic and `message` is a plain sentence.
|
|
134
|
-
OAuth errors
|
|
165
|
+
OAuth errors expose an optional typed `cause` with `error` and
|
|
166
|
+
`error_description`, sanitized against supplied/returned credentials and URLs.
|
|
167
|
+
The message remains a plain sentence. Treat provider descriptions as untrusted
|
|
168
|
+
text; they are diagnostics for the host, not ready-to-render UI. No errors or
|
|
135
169
|
tokens are logged. MCP results/errors are private application content and should
|
|
136
170
|
be treated accordingly. `fetch`, `now`, request and flow timeouts are injectable.
|
|
137
171
|
|
package/SECURITY.md
CHANGED
|
@@ -7,8 +7,11 @@ server pool or account sharing is provided. Connection keys are fixed-length SHA
|
|
|
7
7
|
resource, issuer/token endpoint and configured client ID. Long identities therefore
|
|
8
8
|
fit every keystore backend without exposing those identities in keyring labels.
|
|
9
9
|
|
|
10
|
-
Treat `token()` as trusted app code: never display or log
|
|
11
|
-
not log credentials or include provider response bodies in OAuth
|
|
10
|
+
Treat `token()` and `grant()` as trusted app code: never display or log their results.
|
|
11
|
+
The kit does not log credentials or include raw provider response bodies in OAuth
|
|
12
|
+
errors. OAuth causes retain only `error` and `error_description`, with supplied and
|
|
13
|
+
returned credentials (including URL/form encodings), URLs and authorization headers
|
|
14
|
+
redacted. Treat descriptions as untrusted diagnostic text, never as HTML. The full
|
|
12
15
|
MCP client can return private content and server error messages; handle those as
|
|
13
16
|
private app data. Close each MCP client when finished.
|
|
14
17
|
|
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, Provider, SignIn, McpOptions } from './types.ts';
|
|
3
|
+
import type { ConnectOptions, OAuthClient, Provider, SignIn, 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>;
|
|
@@ -14,6 +14,11 @@ export declare class Connection {
|
|
|
14
14
|
private get timeout();
|
|
15
15
|
private get now();
|
|
16
16
|
private saved;
|
|
17
|
+
/** Snapshot for trusted app code, including the provider's refresh lifetime. */
|
|
18
|
+
grant(): Promise<Grant | null>;
|
|
19
|
+
/** Google checks app credentials before rejecting this deliberately nonexistent refresh grant.
|
|
20
|
+
* Only invalid_grant confirms the details; other responses cannot prove validity. */
|
|
21
|
+
verifyClient(input?: OAuthClient): Promise<ClientVerification>;
|
|
17
22
|
connected(): Promise<boolean>;
|
|
18
23
|
disconnect(): Promise<void>;
|
|
19
24
|
/** Starts a fresh browser sign-in. No opt-in flag or server-held account is required. */
|
package/dist/connect.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import WORDS from './words.json' with { type: 'json' };
|
|
1
2
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
3
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
3
|
-
import { ConnectError } from "./errors.js";
|
|
4
|
+
import { ConnectError, providerCause } from "./errors.js";
|
|
4
5
|
import { discover } from "./discovery.js";
|
|
5
6
|
import { endpoint, redirect, request, json } from "./http.js";
|
|
6
7
|
import { providers } from "./providers.js";
|
|
@@ -26,6 +27,15 @@ function serial(slot, action) {
|
|
|
26
27
|
}
|
|
27
28
|
function base64url(bytes) { return btoa(String.fromCharCode(...bytes)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); }
|
|
28
29
|
function random() { return base64url(crypto.getRandomValues(new Uint8Array(32))); }
|
|
30
|
+
function validClient(client) {
|
|
31
|
+
if (!client || typeof client.id !== 'string' || !client.id.trim() ||
|
|
32
|
+
(client.secret !== undefined && (typeof client.secret !== 'string' || !client.secret)) ||
|
|
33
|
+
(client.authMethod !== undefined && !['none', 'client_secret_post', 'client_secret_basic'].includes(client.authMethod)) ||
|
|
34
|
+
(client.authMethod?.startsWith('client_secret_') && !client.secret))
|
|
35
|
+
throw new ConnectError('configuration');
|
|
36
|
+
return { id: client.id, secret: client.secret, authMethod: client.authMethod };
|
|
37
|
+
}
|
|
38
|
+
function validTime(value) { return value === undefined || (typeof value === 'number' && Number.isFinite(value) && value >= 0); }
|
|
29
39
|
/** One person's connection on one device; the host owns the store and browser. */
|
|
30
40
|
export class Connection {
|
|
31
41
|
provider;
|
|
@@ -73,6 +83,36 @@ export class Connection {
|
|
|
73
83
|
throw new ConnectError('token');
|
|
74
84
|
}
|
|
75
85
|
}
|
|
86
|
+
/** Snapshot for trusted app code, including the provider's refresh lifetime. */
|
|
87
|
+
async grant() {
|
|
88
|
+
const saved = await this.saved();
|
|
89
|
+
if (!saved)
|
|
90
|
+
return null;
|
|
91
|
+
const t = saved.tokens;
|
|
92
|
+
return { accessToken: t.access, refreshToken: t.refresh, expiresAt: t.expires, scope: t.scope,
|
|
93
|
+
refreshTokenExpiresIn: t.refreshExpiresIn, refreshTokenExpiresAt: t.refreshExpiresAt };
|
|
94
|
+
}
|
|
95
|
+
/** Google checks app credentials before rejecting this deliberately nonexistent refresh grant.
|
|
96
|
+
* Only invalid_grant confirms the details; other responses cannot prove validity. */
|
|
97
|
+
async verifyClient(input) {
|
|
98
|
+
const client = validClient((input ?? this.options.client));
|
|
99
|
+
if (!client.secret || client.authMethod === 'none')
|
|
100
|
+
throw new ConnectError('configuration');
|
|
101
|
+
try {
|
|
102
|
+
const info = await discover(this.provider, this.fetcher, this.timeout);
|
|
103
|
+
await this.exchange(info, client, { grant_type: 'refresh_token', refresh_token: 'byokit-client-check-not-a-grant' });
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (!(error instanceof ConnectError))
|
|
107
|
+
throw error;
|
|
108
|
+
if (error.cause?.error === 'invalid_client' && (error.status === 400 || error.status === 401))
|
|
109
|
+
return { outcome: 'invalid', message: WORDS.verification.invalid, cause: error.cause };
|
|
110
|
+
if (error.cause?.error === 'invalid_grant' && error.status === 400)
|
|
111
|
+
return { outcome: 'valid', message: WORDS.verification.valid };
|
|
112
|
+
return { outcome: 'inconclusive', message: WORDS.verification.inconclusive, cause: error.cause };
|
|
113
|
+
}
|
|
114
|
+
return { outcome: 'inconclusive', message: WORDS.verification.inconclusive };
|
|
115
|
+
}
|
|
76
116
|
async connected() { return !!await this.saved(); }
|
|
77
117
|
async disconnect() {
|
|
78
118
|
++this.slot.epoch;
|
|
@@ -122,7 +162,7 @@ export class Connection {
|
|
|
122
162
|
throw new ConnectError('callback');
|
|
123
163
|
consumed = true;
|
|
124
164
|
if (returned.searchParams.has('error'))
|
|
125
|
-
throw new ConnectError('declined');
|
|
165
|
+
throw new ConnectError('declined', undefined, providerCause(Object.fromEntries(returned.searchParams), [state, verifier, client.id, client.secret ?? '', ...returned.searchParams.getAll('code')]));
|
|
126
166
|
if (returned.searchParams.getAll('code').length !== 1 || !returned.searchParams.get('code'))
|
|
127
167
|
throw new ConnectError('callback');
|
|
128
168
|
await serial(this.slot, async () => {
|
|
@@ -148,11 +188,9 @@ export class Connection {
|
|
|
148
188
|
method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
149
189
|
body: JSON.stringify({ client_name: this.options.clientName ?? 'BYOKit', redirect_uris: [this.options.redirectUri], grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], token_endpoint_auth_method: 'none' }),
|
|
150
190
|
}, this.timeout);
|
|
151
|
-
if (!response.ok) {
|
|
152
|
-
await response.body?.cancel();
|
|
153
|
-
throw new ConnectError('registration', response.status);
|
|
154
|
-
}
|
|
155
191
|
const m = await json(response, 'registration');
|
|
192
|
+
if (!response.ok || m.error)
|
|
193
|
+
throw new ConnectError('registration', response.status, providerCause(m, [this.options.clientName ?? 'BYOKit', this.options.redirectUri]));
|
|
156
194
|
if (typeof m.client_id !== 'string' || !m.client_id || (m.token_endpoint_auth_method !== undefined && m.token_endpoint_auth_method !== 'none'))
|
|
157
195
|
throw new ConnectError('registration');
|
|
158
196
|
return { id: m.client_id, authMethod: 'none' };
|
|
@@ -172,15 +210,20 @@ export class Connection {
|
|
|
172
210
|
const m = await json(response, 'token');
|
|
173
211
|
if (!response.ok || m.error) {
|
|
174
212
|
// Only an explicit invalid grant proves revocation. Transient failures preserve the sign-in.
|
|
213
|
+
const cause = providerCause(m, [client.id, client.secret ?? '', previous?.access ?? '', previous?.refresh ?? '', ...Object.values(fields)]);
|
|
175
214
|
if (m.error === 'invalid_grant' && previous)
|
|
176
|
-
throw new ConnectError('signin', response.status);
|
|
177
|
-
throw new ConnectError('token', response.status);
|
|
215
|
+
throw new ConnectError('signin', response.status, cause);
|
|
216
|
+
throw new ConnectError('token', response.status, cause);
|
|
178
217
|
}
|
|
179
218
|
if (typeof m.access_token !== 'string' || !m.access_token || typeof m.token_type !== 'string' || m.token_type.toLowerCase() !== 'bearer' || (m.expires_in !== undefined && (!Number.isFinite(Number(m.expires_in)) || Number(m.expires_in) < 0)))
|
|
180
219
|
throw new ConnectError('token');
|
|
220
|
+
if (m.refresh_token_expires_in !== undefined && (!validTime(m.refresh_token_expires_in) || !Number.isFinite(this.now + Number(m.refresh_token_expires_in) * 1000)))
|
|
221
|
+
throw new ConnectError('token');
|
|
181
222
|
return { access: m.access_token, refresh: typeof m.refresh_token === 'string' ? m.refresh_token : previous?.refresh,
|
|
182
223
|
expires: m.expires_in === undefined ? undefined : this.now + Number(m.expires_in) * 1000,
|
|
183
|
-
scope: typeof m.scope === 'string' ? m.scope : previous?.scope
|
|
224
|
+
scope: typeof m.scope === 'string' ? m.scope : previous?.scope,
|
|
225
|
+
refreshExpiresIn: typeof m.refresh_token_expires_in === 'number' ? m.refresh_token_expires_in : previous?.refreshExpiresIn,
|
|
226
|
+
refreshExpiresAt: typeof m.refresh_token_expires_in === 'number' ? this.now + m.refresh_token_expires_in * 1000 : previous?.refreshExpiresAt };
|
|
184
227
|
}
|
|
185
228
|
/** For trusted app code (mail/calendar/API calls). Never display or log this value. */
|
|
186
229
|
async token(rejectedAccessToken) {
|
|
@@ -202,6 +245,11 @@ export class Connection {
|
|
|
202
245
|
return old.access;
|
|
203
246
|
throw new ConnectError('signin');
|
|
204
247
|
}
|
|
248
|
+
if (old.refreshExpiresAt !== undefined && old.refreshExpiresAt <= this.now) {
|
|
249
|
+
if (!force && old.expires !== undefined && old.expires > this.now)
|
|
250
|
+
return old.access;
|
|
251
|
+
throw new ConnectError('signin');
|
|
252
|
+
}
|
|
205
253
|
try {
|
|
206
254
|
const tokens = await this.exchange(saved, saved.client, { grant_type: 'refresh_token', refresh_token: old.refresh }, old);
|
|
207
255
|
if (epoch !== this.slot.epoch)
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
export type ConnectErrorCode = 'configuration' | 'discovery' | 'registration' | 'callback' | 'expired' | 'declined' | 'scope' | 'token' | 'signin' | 'network';
|
|
2
|
-
|
|
2
|
+
export interface ProviderErrorCause {
|
|
3
|
+
readonly error: string;
|
|
4
|
+
readonly error_description?: string;
|
|
5
|
+
}
|
|
6
|
+
/** Preserve OAuth diagnostics, removing credentials supplied to or returned by the provider. */
|
|
7
|
+
export declare function providerCause(body: Record<string, unknown>, secrets: readonly string[]): ProviderErrorCause | undefined;
|
|
8
|
+
/** Plain message and optional sanitized OAuth cause; never includes raw response bodies. */
|
|
3
9
|
export declare class ConnectError extends Error {
|
|
4
10
|
readonly code: ConnectErrorCode;
|
|
5
11
|
readonly status?: number;
|
|
6
|
-
|
|
12
|
+
readonly cause?: ProviderErrorCause;
|
|
13
|
+
constructor(code: ConnectErrorCode, status?: number, cause?: ProviderErrorCause);
|
|
7
14
|
}
|
package/dist/errors.js
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
import WORDS from './words.json' with { type: 'json' };
|
|
2
2
|
const words = WORDS.errors;
|
|
3
|
-
/**
|
|
3
|
+
/** Preserve OAuth diagnostics, removing credentials supplied to or returned by the provider. */
|
|
4
|
+
export function providerCause(body, secrets) {
|
|
5
|
+
if (typeof body.error !== 'string')
|
|
6
|
+
return undefined;
|
|
7
|
+
const values = [...secrets, ...['access_token', 'refresh_token', 'id_token', 'client_secret', 'client_id'].map(k => body[k]).filter((v) => typeof v === 'string')];
|
|
8
|
+
const redact = (text) => {
|
|
9
|
+
for (const value of values.filter(Boolean).sort((a, b) => b.length - a.length)) {
|
|
10
|
+
const variants = new Set([value, new URLSearchParams({ v: value }).toString().slice(2)]);
|
|
11
|
+
try {
|
|
12
|
+
variants.add(encodeURIComponent(value));
|
|
13
|
+
}
|
|
14
|
+
catch { /* Malformed Unicode still gets literal/form redaction. */ }
|
|
15
|
+
for (const encoded of variants)
|
|
16
|
+
text = text.split(encoded).join('[redacted]');
|
|
17
|
+
}
|
|
18
|
+
return text.replace(/https?:\/\/[^\s<>"']+/gi, '[redacted]').replace(/(?:Bearer|Basic)\s+[^\s,;]+/gi, '[redacted]');
|
|
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'];
|
|
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
|
+
}
|
|
23
|
+
/** Plain message and optional sanitized OAuth cause; never includes raw response bodies. */
|
|
4
24
|
export class ConnectError extends Error {
|
|
5
25
|
code;
|
|
6
26
|
status;
|
|
7
|
-
constructor(code, status) {
|
|
8
|
-
super(words[code]);
|
|
27
|
+
constructor(code, status, cause) {
|
|
28
|
+
super(words[code], cause ? { cause } : undefined);
|
|
9
29
|
this.name = 'ConnectError';
|
|
10
30
|
this.code = code;
|
|
11
31
|
this.status = status;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { connect, 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';
|
|
5
|
-
export type { ConnectErrorCode } from './errors.ts';
|
|
6
|
-
export type { Provider, OAuthClient, OAuthEndpoints, ConnectOptions, SignIn, McpOptions } from './types.ts';
|
|
5
|
+
export type { ConnectErrorCode, ProviderErrorCause } from './errors.ts';
|
|
6
|
+
export type { Provider, OAuthClient, OAuthEndpoints, ConnectOptions, SignIn, McpOptions, Grant, ClientVerification } from './types.ts';
|
|
7
7
|
export { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
8
8
|
export * from '@modelcontextprotocol/sdk/types.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -53,3 +53,26 @@ export interface McpOptions {
|
|
|
53
53
|
/** Register sampling/elicitation/request/notification handlers before connecting. */
|
|
54
54
|
configure?: (client: import('@modelcontextprotocol/sdk/client/index.js').Client) => void;
|
|
55
55
|
}
|
|
56
|
+
/** Trusted app data only: never display or log credentials. Times are Unix milliseconds. */
|
|
57
|
+
export interface Grant {
|
|
58
|
+
accessToken: string;
|
|
59
|
+
refreshToken?: string;
|
|
60
|
+
expiresAt?: number;
|
|
61
|
+
scope?: string;
|
|
62
|
+
/** The provider's refresh_token_expires_in value, in seconds. */
|
|
63
|
+
refreshTokenExpiresIn?: number;
|
|
64
|
+
/** Absolute deadline; unlike the duration, this does not restart on restore. */
|
|
65
|
+
refreshTokenExpiresAt?: number;
|
|
66
|
+
}
|
|
67
|
+
export type ClientVerification = {
|
|
68
|
+
outcome: 'valid';
|
|
69
|
+
message: string;
|
|
70
|
+
} | {
|
|
71
|
+
outcome: 'invalid';
|
|
72
|
+
message: string;
|
|
73
|
+
cause?: import('./errors.ts').ProviderErrorCause;
|
|
74
|
+
} | {
|
|
75
|
+
outcome: 'inconclusive';
|
|
76
|
+
message: string;
|
|
77
|
+
cause?: import('./errors.ts').ProviderErrorCause;
|
|
78
|
+
};
|
package/dist/words.json
CHANGED
|
@@ -17,5 +17,10 @@
|
|
|
17
17
|
"finishing": "Sign-in is already finishing. Return to the app.",
|
|
18
18
|
"connected": "Connected. You can return to the app.",
|
|
19
19
|
"failed": "Sign-in did not finish. Return to the app and try again."
|
|
20
|
+
},
|
|
21
|
+
"verification": {
|
|
22
|
+
"valid": "Your app details work.",
|
|
23
|
+
"invalid": "Check your app details and try again.",
|
|
24
|
+
"inconclusive": "Your app details could not be checked. Try again later."
|
|
20
25
|
}
|
|
21
26
|
}
|