@fungi.computer/veil 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.
Files changed (35) hide show
  1. package/dist/connections/cleanup.d.ts +21 -0
  2. package/dist/connections/cleanup.js +66 -0
  3. package/dist/connections/contract.d.ts +153 -7
  4. package/dist/connections/contract.js +42 -2
  5. package/dist/connections/core.d.ts +55 -5
  6. package/dist/connections/core.js +365 -106
  7. package/dist/connections/discover.d.ts +3 -0
  8. package/dist/connections/discover.js +23 -0
  9. package/dist/connections/egress.js +3 -2
  10. package/dist/connections/index.d.ts +1 -0
  11. package/dist/connections/index.js +1 -0
  12. package/dist/connections/internal.d.ts +78 -0
  13. package/dist/connections/internal.js +66 -0
  14. package/dist/connections/mcp-agent.d.ts +37 -0
  15. package/dist/connections/mcp-agent.js +75 -0
  16. package/dist/connections/nango.d.ts +30 -0
  17. package/dist/connections/nango.js +136 -0
  18. package/dist/connections/oauth-flow.d.ts +81 -0
  19. package/dist/connections/oauth-flow.js +322 -0
  20. package/dist/connections/oauth-provider.d.ts +31 -0
  21. package/dist/connections/oauth-provider.js +96 -0
  22. package/dist/connections/oauth.d.ts +93 -0
  23. package/dist/connections/oauth.js +239 -0
  24. package/dist/connections/openapi.d.ts +36 -0
  25. package/dist/connections/openapi.js +120 -0
  26. package/dist/connections/sealed.js +23 -4
  27. package/dist/connections/source.d.ts +17 -0
  28. package/dist/connections/source.js +111 -0
  29. package/dist/connections/update.d.ts +41 -0
  30. package/dist/connections/update.js +81 -0
  31. package/dist/gatekeeper.d.ts +21 -1
  32. package/dist/gatekeeper.js +74 -12
  33. package/dist/nango.d.ts +41 -0
  34. package/dist/nango.js +6 -6
  35. package/package.json +11 -1
@@ -0,0 +1,322 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Schema from "effect/Schema";
3
+ import { assertPublicHostName } from "../model-key-network.js";
4
+ import { ConnectionsError, identifier, } from "./contract.js";
5
+ import { decode, io, sync, } from "./internal.js";
6
+ import { finishStaticAuthorization, refreshStaticTokens, startStaticAuthorization, } from "./oauth-provider.js";
7
+ import { finishAuthorization, oauthBundleJson, oauthClientJson, refreshTokens, startAuthorization, tokenExpired, } from "./oauth.js";
8
+ /** Discovery, sign-in, token refresh and the registered-client record for OAuth connections. */
9
+ export function createOAuthFlow(deps) {
10
+ const { ownerId, custody, store } = deps;
11
+ const address = (row) => ({
12
+ ownerId,
13
+ connectionId: row.id,
14
+ credential: row.credential,
15
+ });
16
+ const oauthPorts = () => {
17
+ const ports = deps.oauth;
18
+ const open = custody.open;
19
+ if (ports === undefined || open === undefined)
20
+ throw new ConnectionsError("unsupported");
21
+ return { ...ports, open };
22
+ };
23
+ /** Every discovery, registration and token request meets the same destination rules. */
24
+ const guardedFetch = async (url, init) => {
25
+ const ports = oauthPorts();
26
+ const request = new Request(url, { ...init, redirect: "error" });
27
+ const target = new URL(request.url);
28
+ if (target.protocol !== "https:" || target.username || target.password)
29
+ throw new ConnectionsError("denied");
30
+ try {
31
+ assertPublicHostName(target.hostname);
32
+ deps.policy.destination(target);
33
+ }
34
+ catch (cause) {
35
+ throw new ConnectionsError("denied", { cause });
36
+ }
37
+ return ports.fetch(request);
38
+ };
39
+ /** The operator's registered provider by id; a bundle naming one this host no longer holds cannot be used. */
40
+ const staticProvider = (id) => {
41
+ if (id === undefined)
42
+ return undefined;
43
+ const found = deps.oauth?.providers?.[id];
44
+ if (found === undefined)
45
+ throw new ConnectionsError("reconnect_required");
46
+ return found;
47
+ };
48
+ const clientAddress = () => ({
49
+ ownerId,
50
+ connectionId: "oauth-clients",
51
+ credential: { kind: "oauth" },
52
+ });
53
+ /** One registered client per authorization server, sealed in custody. */
54
+ const registry = {
55
+ lookup: (issuer) => Effect.runPromise(Effect.gen(function* () {
56
+ const found = yield* sync(() => store.clientLocator(issuer));
57
+ if (found === undefined)
58
+ return undefined;
59
+ const raw = yield* sync(oauthPorts).pipe(Effect.flatMap((ports) => ports.open(clientAddress(), found)));
60
+ return yield* sync(() => decode(oauthClientJson, raw));
61
+ })),
62
+ save: (issuer, client) => Effect.runPromise(Effect.gen(function* () {
63
+ const locator = yield* custody.put(clientAddress(), Schema.encodeSync(oauthClientJson)(client));
64
+ yield* sync(() => {
65
+ store.saveClientLocator(issuer, locator);
66
+ });
67
+ })),
68
+ };
69
+ const readBundle = (row, locator) => sync(oauthPorts).pipe(Effect.flatMap((ports) => ports.open(address(row), locator)), Effect.flatMap((raw) => Effect.try({
70
+ try: () => Schema.decodeUnknownSync(oauthBundleJson)(raw),
71
+ catch: (cause) => new ConnectionsError("reconnect_required", { cause }),
72
+ })));
73
+ /** The custodian runs the authorization: we hold only the page to send the owner to. */
74
+ const beginCustody = (actor, draft, credential, endpoint, fingerprint, openapi) => Effect.gen(function* () {
75
+ const connect = custody.connect;
76
+ const integrationId = credential.provider;
77
+ if (connect === undefined || integrationId === undefined)
78
+ return yield* Effect.fail(new ConnectionsError("unsupported"));
79
+ const connectionId = deps.ids();
80
+ const state = `${ownerId}.${deps.ids()}`;
81
+ yield* sync(() => decode(identifier, connectionId));
82
+ const session = yield* connect({ ownerId, connectionId, credential }, { state, actor, integrationId });
83
+ const stored = {
84
+ id: connectionId,
85
+ name: draft.name,
86
+ credential,
87
+ locator: null,
88
+ state: "pending",
89
+ authorityVersion: 1,
90
+ version: 1,
91
+ surface: {
92
+ kind: openapi === null ? "mcp" : "openapi",
93
+ url: endpoint,
94
+ table: openapi?.table ?? null,
95
+ digest: openapi?.digest ?? null,
96
+ label: draft.label ?? draft.name,
97
+ },
98
+ };
99
+ return yield* sync(() => {
100
+ store.recordPending({
101
+ actor,
102
+ connection: stored,
103
+ state,
104
+ attemptLocator: null,
105
+ issuer: `custody:${integrationId}`,
106
+ requestId: draft.requestId,
107
+ fingerprint,
108
+ authorizeUrl: session.connectUrl,
109
+ });
110
+ return { connectionId, authorizeUrl: session.connectUrl };
111
+ });
112
+ });
113
+ /**
114
+ * The custodian says an authorization finished. The host has already
115
+ * verified its signature; the state is single use and expiring, and the
116
+ * connection is not live until this settles it.
117
+ */
118
+ const completeCustody = (input) => Effect.gen(function* () {
119
+ const claimed = yield* sync(() => store.claimCustody(input.state));
120
+ if (claimed.expired)
121
+ return yield* Effect.fail(new ConnectionsError("denied"));
122
+ if (claimed.attempt.issuer !== `custody:${input.integrationId}`)
123
+ return yield* Effect.fail(new ConnectionsError("denied"));
124
+ const row = yield* sync(() => store.connection(claimed.attempt.connectionId));
125
+ const activated = yield* sync(() => store.activate(row, claimed.attempt, input.locator));
126
+ if (!activated) {
127
+ yield* sync(() => {
128
+ store.discard(row.id, input.locator, row.credential);
129
+ });
130
+ return yield* Effect.fail(new ConnectionsError("revoked"));
131
+ }
132
+ yield* deps.sweep();
133
+ return { connectionId: row.id };
134
+ });
135
+ const begin = (actor, draft, credential, endpoint, fingerprint,
136
+ /** The decoded operation table of a REST connection, when that is what signs in. */
137
+ openapi) => Effect.gen(function* () {
138
+ const ports = yield* sync(oauthPorts);
139
+ const connectionId = deps.ids();
140
+ const state = `${ownerId}.${deps.ids()}`;
141
+ yield* sync(() => decode(identifier, connectionId));
142
+ let client;
143
+ if (credential.clientId !== undefined) {
144
+ client = { client_id: credential.clientId };
145
+ if (draft.secret !== undefined)
146
+ client.client_secret = draft.secret;
147
+ }
148
+ const started = yield* io(() => {
149
+ const provider = staticProvider(credential.provider);
150
+ return provider === undefined
151
+ ? startAuthorization({
152
+ serverUrl: endpoint,
153
+ scope: credential.scope,
154
+ client,
155
+ callbackUrl: ports.callbackUrl,
156
+ state,
157
+ registry,
158
+ fetch: guardedFetch,
159
+ })
160
+ : startStaticAuthorization({
161
+ providerId: credential.provider ?? "",
162
+ provider,
163
+ scope: credential.scope,
164
+ callbackUrl: ports.callbackUrl,
165
+ state,
166
+ });
167
+ });
168
+ const attemptLocator = yield* custody.put({ ownerId, connectionId, credential }, Schema.encodeSync(oauthBundleJson)(started.bundle));
169
+ const stored = {
170
+ id: connectionId,
171
+ name: draft.name,
172
+ credential,
173
+ locator: null,
174
+ state: "pending",
175
+ authorityVersion: 1,
176
+ version: 1,
177
+ surface: {
178
+ kind: openapi === null ? "mcp" : "openapi",
179
+ url: endpoint,
180
+ table: openapi?.table ?? null,
181
+ digest: openapi?.digest ?? null,
182
+ label: draft.label ?? draft.name,
183
+ },
184
+ };
185
+ return yield* sync(() => {
186
+ store.recordPending({
187
+ actor,
188
+ connection: stored,
189
+ state,
190
+ attemptLocator,
191
+ issuer: started.bundle.authorizationServerUrl,
192
+ requestId: draft.requestId,
193
+ fingerprint,
194
+ authorizeUrl: started.authorizeUrl,
195
+ });
196
+ return { connectionId, authorizeUrl: started.authorizeUrl };
197
+ }).pipe(Effect.tapError(() => sync(() => {
198
+ store.discard(connectionId, attemptLocator, credential);
199
+ })));
200
+ });
201
+ /**
202
+ * The browser's return from the authorization server. The state is single
203
+ * use, expiring and bound to the actor who began; the SDK checks the issuer
204
+ * and PKCE before the code is redeemed.
205
+ */
206
+ const complete = (actor, input) => Effect.gen(function* () {
207
+ const ports = yield* sync(oauthPorts);
208
+ const claimed = yield* sync(() => store.claim(actor, input));
209
+ if (claimed.expired)
210
+ return yield* Effect.fail(new ConnectionsError("denied"));
211
+ const { attempt, wanted } = claimed;
212
+ const row = yield* sync(() => store.connection(attempt.connectionId));
213
+ const attemptLocator = attempt.pkceVerifierSealed ?? "";
214
+ const settled = Effect.gen(function* () {
215
+ const bundle = yield* readBundle(row, attemptLocator);
216
+ const provider = staticProvider(bundle.provider);
217
+ const next = yield* io(() => provider === undefined
218
+ ? finishAuthorization({
219
+ serverUrl: row.surface.url,
220
+ callbackUrl: ports.callbackUrl,
221
+ bundle,
222
+ code: wanted.code,
223
+ iss: wanted.iss,
224
+ now: deps.clock(),
225
+ fetch: guardedFetch,
226
+ })
227
+ : finishStaticAuthorization({
228
+ provider,
229
+ bundle,
230
+ callbackUrl: ports.callbackUrl,
231
+ code: wanted.code,
232
+ iss: wanted.iss,
233
+ now: deps.clock(),
234
+ fetch: guardedFetch,
235
+ }));
236
+ return yield* custody.put(address(row), Schema.encodeSync(oauthBundleJson)(next));
237
+ }).pipe(Effect.tapError(() => sync(() => {
238
+ store.fail(attempt);
239
+ })));
240
+ const locator = yield* settled;
241
+ const activated = yield* sync(() => store.activate(row, attempt, locator));
242
+ if (!activated) {
243
+ // Revoked while the code was in flight: nothing may come back to life.
244
+ yield* sync(() => {
245
+ store.discard(row.id, locator, row.credential);
246
+ });
247
+ return yield* Effect.fail(new ConnectionsError("revoked"));
248
+ }
249
+ yield* deps.sweep();
250
+ return { connectionId: row.id };
251
+ });
252
+ /** One refresh at a time per connection; later callers share its outcome. */
253
+ const flights = new Map();
254
+ const refreshLocator = (id, stale) => {
255
+ const running = flights.get(id);
256
+ if (running !== undefined)
257
+ return running;
258
+ const flight = Effect.runPromise(refreshOnce(id, stale)).finally(() => {
259
+ flights.delete(id);
260
+ });
261
+ flights.set(id, flight);
262
+ return flight;
263
+ };
264
+ const refreshOnce = (id, stale) => Effect.gen(function* () {
265
+ const row = yield* sync(() => store.connection(id));
266
+ if (row.state === "revoked")
267
+ return yield* Effect.fail(new ConnectionsError("revoked"));
268
+ if (row.state !== "active" || row.locator === null)
269
+ return yield* Effect.fail(new ConnectionsError("reconnect_required"));
270
+ // A flight that just ended already replaced the token this caller saw.
271
+ if (row.locator !== stale)
272
+ return row.locator;
273
+ const held = row.locator;
274
+ const bundle = yield* readBundle(row, held);
275
+ const provider = staticProvider(bundle.provider);
276
+ const next = yield* io(() => provider === undefined
277
+ ? refreshTokens({ bundle, now: deps.clock(), fetch: guardedFetch })
278
+ : refreshStaticTokens({
279
+ provider,
280
+ bundle,
281
+ now: deps.clock(),
282
+ fetch: guardedFetch,
283
+ }));
284
+ // Persist the rotated token before anything uses it.
285
+ const locator = yield* custody.put(address(row), Schema.encodeSync(oauthBundleJson)(next));
286
+ const outcome = yield* sync(() => store.swap(row, held, locator));
287
+ if (outcome !== "applied") {
288
+ // Revoked or replaced during the refresh: the new token never lives.
289
+ yield* sync(() => {
290
+ store.discard(id, locator, row.credential);
291
+ });
292
+ return yield* Effect.fail(new ConnectionsError(outcome === "revoked" ? "revoked" : "conflict"));
293
+ }
294
+ return locator;
295
+ });
296
+ const unauthorized = (response) => io(async () => {
297
+ await response.body?.cancel();
298
+ });
299
+ /** Send with the live token: refresh early when it is about to expire, and once on a 401. */
300
+ const send = (row, request) => Effect.gen(function* () {
301
+ let locator = row.locator;
302
+ if (locator === null)
303
+ return yield* Effect.fail(new ConnectionsError("reconnect_required"));
304
+ const bundle = yield* readBundle(row, locator);
305
+ if (tokenExpired(bundle, deps.clock())) {
306
+ const stale = locator;
307
+ locator = yield* io(() => refreshLocator(row.id, stale));
308
+ }
309
+ const first = yield* custody.inject(address(row), locator, request.clone());
310
+ if (first.status !== 401)
311
+ return first;
312
+ yield* unauthorized(first);
313
+ const stale = locator;
314
+ const fresh = yield* io(() => refreshLocator(row.id, stale));
315
+ const second = yield* custody.inject(address(row), fresh, request.clone());
316
+ if (second.status !== 401)
317
+ return second;
318
+ yield* unauthorized(second);
319
+ return yield* Effect.fail(new ConnectionsError("reconnect_required"));
320
+ });
321
+ return { begin, beginCustody, complete, completeCustody, send };
322
+ }
@@ -0,0 +1,31 @@
1
+ import type { FetchLike } from "@modelcontextprotocol/client";
2
+ import { type StaticOAuthProvider } from "./contract.js";
3
+ import type { OAuthBundle } from "./oauth.js";
4
+ /** PKCE redirect for an operator-registered provider. */
5
+ export declare function startStaticAuthorization(input: {
6
+ providerId: string;
7
+ provider: StaticOAuthProvider;
8
+ scope: string | undefined;
9
+ callbackUrl: string;
10
+ state: string;
11
+ }): Promise<{
12
+ authorizeUrl: string;
13
+ bundle: OAuthBundle;
14
+ }>;
15
+ /** Redeem the code; the SDK validates `iss` against the provider's issuer. */
16
+ export declare function finishStaticAuthorization(input: {
17
+ provider: StaticOAuthProvider;
18
+ bundle: OAuthBundle;
19
+ callbackUrl: string;
20
+ code: string;
21
+ iss: string | undefined;
22
+ now: number;
23
+ fetch: FetchLike;
24
+ }): Promise<OAuthBundle>;
25
+ /** One refresh grant against the provider's own token endpoint. */
26
+ export declare function refreshStaticTokens(input: {
27
+ provider: StaticOAuthProvider;
28
+ bundle: OAuthBundle;
29
+ now: number;
30
+ fetch: FetchLike;
31
+ }): Promise<OAuthBundle>;
@@ -0,0 +1,96 @@
1
+ import { ConnectionsError } from "./contract.js";
2
+ const metadataOf = (provider) => ({
3
+ issuer: provider.issuer,
4
+ authorization_endpoint: provider.authorizationEndpoint,
5
+ token_endpoint: provider.tokenEndpoint,
6
+ response_types_supported: ["code"],
7
+ code_challenge_methods_supported: ["S256"],
8
+ token_endpoint_auth_methods_supported: [
9
+ provider.client.client_secret === undefined ? "none" : "client_secret_post",
10
+ ],
11
+ });
12
+ const clientOf = (client) => client.client_secret === undefined
13
+ ? { client_id: client.client_id }
14
+ : { client_id: client.client_id, client_secret: client.client_secret };
15
+ /** PKCE redirect for an operator-registered provider. */
16
+ export async function startStaticAuthorization(input) {
17
+ const sdk = await import("@modelcontextprotocol/client");
18
+ const scope = input.scope ?? input.provider.scope;
19
+ const request = {
20
+ metadata: metadataOf(input.provider),
21
+ clientInformation: clientOf(input.provider.client),
22
+ redirectUrl: input.callbackUrl,
23
+ state: input.state,
24
+ };
25
+ if (scope !== undefined)
26
+ request.scope = scope;
27
+ const started = await sdk.startAuthorization(input.provider.issuer, request);
28
+ return {
29
+ authorizeUrl: started.authorizationUrl.href,
30
+ bundle: {
31
+ authorizationServerUrl: input.provider.issuer,
32
+ provider: input.providerId,
33
+ client: input.provider.client,
34
+ codeVerifier: started.codeVerifier,
35
+ resource: undefined,
36
+ tokens: undefined,
37
+ },
38
+ };
39
+ }
40
+ /** Redeem the code; the SDK validates `iss` against the provider's issuer. */
41
+ export async function finishStaticAuthorization(input) {
42
+ const { bundle } = input;
43
+ if (bundle.codeVerifier === undefined)
44
+ throw new ConnectionsError("denied");
45
+ const sdk = await import("@modelcontextprotocol/client");
46
+ const request = {
47
+ metadata: metadataOf(input.provider),
48
+ clientInformation: clientOf(input.provider.client),
49
+ authorizationCode: input.code,
50
+ codeVerifier: bundle.codeVerifier,
51
+ redirectUri: input.callbackUrl,
52
+ fetchFn: input.fetch,
53
+ };
54
+ if (input.iss !== undefined)
55
+ request.iss = input.iss;
56
+ const tokens = await sdk.exchangeAuthorization(input.provider.issuer, request);
57
+ return {
58
+ ...bundle,
59
+ codeVerifier: undefined,
60
+ tokens: {
61
+ access_token: tokens.access_token,
62
+ refresh_token: tokens.refresh_token,
63
+ expires_in: tokens.expires_in,
64
+ obtainedAt: input.now,
65
+ },
66
+ };
67
+ }
68
+ /** One refresh grant against the provider's own token endpoint. */
69
+ export async function refreshStaticTokens(input) {
70
+ const held = input.bundle.tokens?.refresh_token;
71
+ if (held === undefined)
72
+ throw new ConnectionsError("reconnect_required");
73
+ const sdk = await import("@modelcontextprotocol/client");
74
+ try {
75
+ const fresh = await sdk.refreshAuthorization(input.provider.issuer, {
76
+ metadata: metadataOf(input.provider),
77
+ clientInformation: clientOf(input.provider.client),
78
+ refreshToken: held,
79
+ fetchFn: input.fetch,
80
+ });
81
+ return {
82
+ ...input.bundle,
83
+ tokens: {
84
+ access_token: fresh.access_token,
85
+ refresh_token: fresh.refresh_token ?? held,
86
+ expires_in: fresh.expires_in,
87
+ obtainedAt: input.now,
88
+ },
89
+ };
90
+ }
91
+ catch (cause) {
92
+ if (sdk.OAuthError.isInstance(cause) && cause.code === "invalid_grant")
93
+ throw new ConnectionsError("reconnect_required", { cause });
94
+ throw cause;
95
+ }
96
+ }
@@ -0,0 +1,93 @@
1
+ import type { FetchLike } from "@modelcontextprotocol/client";
2
+ import * as Schema from "effect/Schema";
3
+ declare const clientSchema: Schema.Struct<{
4
+ readonly client_id: Schema.String;
5
+ readonly client_secret: Schema.optional<Schema.String>;
6
+ }>;
7
+ /**
8
+ * Everything custody seals for one OAuth connection or attempt: the client,
9
+ * the authorization server it was issued by, the PKCE verifier while an
10
+ * attempt is pending, and the token set once it completes.
11
+ */
12
+ declare const oauthBundleSchema: Schema.Struct<{
13
+ readonly authorizationServerUrl: Schema.String;
14
+ /** The operator-registered provider this was issued by, when there is one. */
15
+ readonly provider: Schema.optional<Schema.String>;
16
+ readonly resource: Schema.optional<Schema.String>;
17
+ readonly client: Schema.Struct<{
18
+ readonly client_id: Schema.String;
19
+ readonly client_secret: Schema.optional<Schema.String>;
20
+ }>;
21
+ readonly codeVerifier: Schema.optional<Schema.String>;
22
+ readonly tokens: Schema.optional<Schema.Struct<{
23
+ readonly access_token: Schema.String;
24
+ readonly refresh_token: Schema.optional<Schema.String>;
25
+ /** Seconds the access token lives, as the authorization server said. */
26
+ readonly expires_in: Schema.optional<Schema.Number>;
27
+ readonly obtainedAt: Schema.Number;
28
+ }>>;
29
+ }>;
30
+ export type OAuthBundle = typeof oauthBundleSchema.Type;
31
+ export type OAuthClient = typeof clientSchema.Type;
32
+ export declare const oauthClientJson: Schema.fromJsonString<Schema.Struct<{
33
+ readonly client_id: Schema.String;
34
+ readonly client_secret: Schema.optional<Schema.String>;
35
+ }>>;
36
+ export declare const oauthBundleJson: Schema.fromJsonString<Schema.Struct<{
37
+ readonly authorizationServerUrl: Schema.String;
38
+ /** The operator-registered provider this was issued by, when there is one. */
39
+ readonly provider: Schema.optional<Schema.String>;
40
+ readonly resource: Schema.optional<Schema.String>;
41
+ readonly client: Schema.Struct<{
42
+ readonly client_id: Schema.String;
43
+ readonly client_secret: Schema.optional<Schema.String>;
44
+ }>;
45
+ readonly codeVerifier: Schema.optional<Schema.String>;
46
+ readonly tokens: Schema.optional<Schema.Struct<{
47
+ readonly access_token: Schema.String;
48
+ readonly refresh_token: Schema.optional<Schema.String>;
49
+ /** Seconds the access token lives, as the authorization server said. */
50
+ readonly expires_in: Schema.optional<Schema.Number>;
51
+ readonly obtainedAt: Schema.Number;
52
+ }>>;
53
+ }>>;
54
+ /** Registered clients are reused for every connection to the same issuer. */
55
+ export type ClientRegistry = Readonly<{
56
+ lookup: (issuer: string) => Promise<OAuthClient | undefined>;
57
+ save: (issuer: string, client: OAuthClient) => Promise<void>;
58
+ }>;
59
+ export declare const tokenExpired: (bundle: OAuthBundle, now: number) => boolean;
60
+ export type OAuthStart = Readonly<{
61
+ serverUrl: string;
62
+ scope: string | undefined;
63
+ client: OAuthClient | undefined;
64
+ callbackUrl: string;
65
+ state: string;
66
+ registry: ClientRegistry;
67
+ fetch: FetchLike;
68
+ }>;
69
+ /** Discovery, registration and PKCE through the SDK; returns the redirect and the sealed attempt. */
70
+ export declare function startAuthorization(input: OAuthStart): Promise<{
71
+ authorizeUrl: string;
72
+ bundle: OAuthBundle;
73
+ }>;
74
+ /** Redeem the code; the SDK validates `iss` and PKCE. The issuer must be the one the attempt began with. */
75
+ export declare function finishAuthorization(input: {
76
+ serverUrl: string;
77
+ callbackUrl: string;
78
+ bundle: OAuthBundle;
79
+ code: string;
80
+ iss: string | undefined;
81
+ now: number;
82
+ fetch: FetchLike;
83
+ }): Promise<OAuthBundle>;
84
+ /**
85
+ * One refresh grant. A rotated refresh token replaces the old one; the caller
86
+ * persists the returned bundle before using the new access token.
87
+ */
88
+ export declare function refreshTokens(input: {
89
+ bundle: OAuthBundle;
90
+ now: number;
91
+ fetch: FetchLike;
92
+ }): Promise<OAuthBundle>;
93
+ export {};