@synap-core/sdk 1.2.0 → 1.4.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.
@@ -0,0 +1,647 @@
1
+ import { b as PodConnection, P as PodGrant, c as PodSpace, C as ClientMetadataDocument, A as AppRequest, d as PodIssuer } from './types-C2KGNXEn.cjs';
2
+ export { a as PodUser } from './types-C2KGNXEn.cjs';
3
+
4
+ /**
5
+ * The one error type of `@synap-core/sdk/connect`. `code` is the contract a UI
6
+ * branches on; `message` and `detail` are for the developer's logs, never for
7
+ * the person (a sign-in failure page says one sentence per code, never why).
8
+ */
9
+ type ConnectErrorCode =
10
+ /** The typed pod address is not a usable URL. */
11
+ "invalid_pod_address"
12
+ /** Something answered, but it is not a Synap pod (no AS metadata). */
13
+ | "not_a_pod"
14
+ /** The pod did not answer (network, timeout, 5xx). */
15
+ | "pod_unreachable"
16
+ /** A Synap pod, but too old for app sign-in (no CIMD, no `iss`, no S256). */
17
+ | "pod_unsupported"
18
+ /** Connecting through Synap Cloud is not configured for this app (no Control Plane URL). */
19
+ | "cp_unavailable"
20
+ /** The person said no on the consent screen. */
21
+ | "denied"
22
+ /** No sign-in in progress here, or it is older than 10 minutes. */
23
+ | "expired_transaction"
24
+ /** The answer does not belong to the sign-in this browser started (`state` or `iss`). */
25
+ | "mismatch"
26
+ /** The app's own setup is wrong (client metadata, scopes, secret, redirect). */
27
+ | "misconfigured"
28
+ /** The pod asked to slow down; see `retryAfterSec`. */
29
+ | "rate_limited"
30
+ /** The connection lapsed (refresh token idle past its life, or none was issued). */
31
+ | "expired"
32
+ /** The pod refused the connection's refresh token while it should still live. */
33
+ | "revoked"
34
+ /** The pod answered something the SDK cannot use. */
35
+ | "server_error";
36
+ declare class ConnectError extends Error {
37
+ readonly code: ConnectErrorCode;
38
+ /** Seconds to wait, from `Retry-After`, when `code` is `rate_limited`. */
39
+ readonly retryAfterSec?: number;
40
+ /** The OAuth error code the pod sent, when there was one (logs only). */
41
+ readonly oauthError?: string;
42
+ constructor(code: ConnectErrorCode, message: string, extra?: {
43
+ retryAfterSec?: number;
44
+ oauthError?: string;
45
+ });
46
+ }
47
+ declare function isConnectError(err: unknown): err is ConnectError;
48
+ declare const CONNECT_ERROR_CODES: readonly ConnectErrorCode[];
49
+ /** Parse an untrusted `?error=` value back into a code (null when unknown). */
50
+ declare function parseConnectErrorCode(value: string | null | undefined): ConnectErrorCode | null;
51
+
52
+ type PodErrorCode = "expired" | "unauthorized" | "forbidden" | "door_unsupported" | "not_found" | "invalid" | "conflict" | "rate_limited" | "pod_unreachable" | "server";
53
+ interface PodError {
54
+ code: PodErrorCode;
55
+ /** The HTTP status, or null when nothing was answered. */
56
+ status: number | null;
57
+ retryAfterSec?: number;
58
+ /** The pod's own error text, for logs only. */
59
+ detail?: string;
60
+ }
61
+ type PodResult<T> = {
62
+ ok: true;
63
+ data: T;
64
+ } | {
65
+ ok: false;
66
+ error: PodError;
67
+ };
68
+ type PodWriteResult<T> = {
69
+ ok: true;
70
+ status: "applied";
71
+ data: T;
72
+ } | {
73
+ ok: true;
74
+ status: "proposed";
75
+ proposalId: string | null;
76
+ reviewUrl: string;
77
+ data: T;
78
+ } | {
79
+ ok: false;
80
+ error: PodError;
81
+ };
82
+ type QueryValue = string | number | boolean | null | undefined | ReadonlyArray<string | number | boolean>;
83
+ /** One pending proposal, the pod's summary row (`GET /proposals?view=basic`). */
84
+ interface PendingProposal {
85
+ id: string;
86
+ proposalType: string;
87
+ targetType: string;
88
+ targetId: string | null;
89
+ workspaceId: string | null;
90
+ /** ISO 8601, as the pod sent it. */
91
+ createdAt: string;
92
+ /** The agent that filed it (this app's agent, or another one), when known. */
93
+ agentUserId: string | null;
94
+ sessionId: string | null;
95
+ summary?: string;
96
+ /** Where the person reviews it: the pod's deep link. */
97
+ reviewUrl: string;
98
+ }
99
+ interface PendingProposals {
100
+ /** Every pending proposal the connection can see, not only this page (the pod COUNTs it). */
101
+ total: number;
102
+ proposals: PendingProposal[];
103
+ hasMore: boolean;
104
+ }
105
+ interface ListPendingProposalsOptions {
106
+ /** One granted space only. */
107
+ workspaceId?: string;
108
+ /** 1-200, default 20. */
109
+ limit?: number;
110
+ offset?: number;
111
+ }
112
+ interface PodClientOptions {
113
+ fetch?: typeof fetch;
114
+ now?: () => number;
115
+ }
116
+ interface PodClient {
117
+ podUrl: string;
118
+ user: PodConnection["user"];
119
+ grant: PodGrant[];
120
+ /** Whether the connection was granted `permission` (in `workspaceId` when given). */
121
+ can(permission: string, workspaceId?: string): boolean;
122
+ /** The granted spaces, each with its permissions (`spaces(connection)`). */
123
+ spaces(): PodSpace[];
124
+ /**
125
+ * "Waiting for review: N". The person's pending proposals in the spaces this
126
+ * connection may read proposals in (any agent's, not only this app's), from
127
+ * the pod's seamed `GET /proposals` door. Needs the `proposal.read`
128
+ * permission (ask for it in `requests`): without it the pod would answer an
129
+ * empty list, so this answers `forbidden` instead, without a request.
130
+ */
131
+ listPendingProposals(opts?: ListPendingProposalsOptions): Promise<PodResult<PendingProposals>>;
132
+ get<T = unknown>(path: string, query?: Record<string, QueryValue>): Promise<PodResult<T>>;
133
+ post<T = unknown>(path: string, body?: unknown): Promise<PodWriteResult<T>>;
134
+ patch<T = unknown>(path: string, body?: unknown): Promise<PodWriteResult<T>>;
135
+ put<T = unknown>(path: string, body?: unknown): Promise<PodWriteResult<T>>;
136
+ delete<T = unknown>(path: string, body?: unknown): Promise<PodWriteResult<T>>;
137
+ }
138
+ /** Whether `connection` was granted `permission` (in `workspaceId` when given). Exact match: grants carry no wildcard. */
139
+ declare function can(connection: Pick<PodConnection, "grant">, permission: string, workspaceId?: string): boolean;
140
+ /**
141
+ * The grant by space, in the grant's order, each permission once. The pod
142
+ * sends one row per permission per space; a space switcher wants one per space.
143
+ */
144
+ declare function spaces(connection: Pick<PodConnection, "grant">): PodSpace[];
145
+ /**
146
+ * Where to review a proposal: the pod's answer, resolved against the pod and
147
+ * kept only on the pod's own origin; else the pod's deep link `/open/<id>`.
148
+ */
149
+ declare function reviewUrlFor(podUrl: string, reviewUrl: unknown, proposalId: string | null): string;
150
+ declare function createPodClient(connection: PodConnection, opts?: PodClientOptions): PodClient;
151
+
152
+ type SealPurpose = "synap-connect/transaction" | "synap-connect/session" | "synap-connect/cloud";
153
+ /** The app secret: one value, or several to rotate (the first seals, all open). */
154
+ type AppSecret = string | readonly string[];
155
+ declare const MIN_SECRET_LENGTH = 32;
156
+ declare function seal(value: unknown, opts: {
157
+ secret: AppSecret;
158
+ purpose: SealPurpose;
159
+ expiresAt: number;
160
+ }): Promise<string>;
161
+ type Opened<T> = {
162
+ ok: true;
163
+ value: T;
164
+ expiresAt: number;
165
+ } | {
166
+ ok: false;
167
+ reason: "invalid" | "expired";
168
+ };
169
+ /**
170
+ * `invalid` = not ours (malformed, tampered, other secret, other purpose);
171
+ * `expired` = ours, but past its `exp`.
172
+ */
173
+ declare function unseal<T = unknown>(sealed: string, opts: {
174
+ secret: AppSecret;
175
+ purpose: SealPurpose;
176
+ now: number;
177
+ }): Promise<Opened<T>>;
178
+
179
+ interface ConnectionStore {
180
+ /** The request's connection, or null (absent, tampered, foreign, expired). */
181
+ read(request: Request): Promise<PodConnection | null>;
182
+ /** Persist; returns the Set-Cookie header values to send. */
183
+ write(connection: PodConnection, request: Request): Promise<string[]>;
184
+ /** Forget; returns the Set-Cookie header values to send. */
185
+ clear(request: Request): Promise<string[]>;
186
+ }
187
+ interface CookieNames {
188
+ session: string;
189
+ transaction: string;
190
+ /** A Synap Cloud sign-in in progress (sealed state, 10 minutes). */
191
+ cloud: string;
192
+ /** "This browser was connected to <pod>": the pod's URL only, no secret (`markerCookie`). */
193
+ marker: string;
194
+ }
195
+ /** `__Host-` names on https (Secure, Path=/, no Domain: bound to this exact host). */
196
+ declare function cookieNames(secure: boolean): CookieNames;
197
+ /** The marker outlives the connection: as long as browsers keep a cookie (400 days). */
198
+ declare const MARKER_TTL_SEC: number;
199
+ /**
200
+ * The marker cookie: which pod this browser was connected to, so a connection
201
+ * that lapsed (idle past the refresh token's life, its sealed cookie gone)
202
+ * reads as "expired, connect again to <pod>" rather than "never connected".
203
+ * It holds the pod's public URL and NOTHING else: no token, no user, nothing
204
+ * sealed. A forged value can only pre-fill the address the person confirms.
205
+ */
206
+ declare function markerCookie(podUrl: string, opts: {
207
+ secure: boolean;
208
+ name?: string;
209
+ }): string;
210
+ /** The marker's pod URL, or null (absent, or not a canonical pod address). */
211
+ declare function readMarker(cookieHeader: string | null, opts: {
212
+ secure: boolean;
213
+ name?: string;
214
+ allowInsecureLoopback?: boolean;
215
+ }): string | null;
216
+ /** When the stored value stops being useful. */
217
+ declare function connectionExpiresAt(c: PodConnection): number;
218
+ interface SealedCookieStoreOptions {
219
+ secret: AppSecret;
220
+ /** True on https (the default when the app URL is https). */
221
+ secure: boolean;
222
+ name?: string;
223
+ now?: () => number;
224
+ }
225
+ declare function createSealedCookieStore(opts: SealedCookieStoreOptions): ConnectionStore;
226
+ /**
227
+ * Apply Set-Cookie values to a request's Cookie header, so code that runs
228
+ * later IN THE SAME REQUEST (a server component after `proxy.ts`) reads the
229
+ * refreshed value. A `Max-Age=0` removes the cookie.
230
+ */
231
+ declare function applySetCookies(cookieHeader: string | null, setCookies: readonly string[]): string;
232
+
233
+ interface PodAppPaths {
234
+ /** The client metadata document; its absolute URL is the client_id. */
235
+ metadata: string;
236
+ connect: string;
237
+ callback: string;
238
+ disconnect: string;
239
+ /** Synap Cloud: POST starts it, GET is the pod chooser's return (forwarded from the connect page). */
240
+ cloud: string;
241
+ /** The app's connect page (also the CIMD `initiate_login_uri`). */
242
+ connectPage: string;
243
+ /** Where a failed callback lands, with `?error=<code>`. */
244
+ resultPage: string;
245
+ /** Where a sign-in lands when it named no `next`. */
246
+ afterConnect: string;
247
+ }
248
+ declare const DEFAULT_PATHS: PodAppPaths;
249
+ /** Refresh only inside the last 5 minutes of the access token's life. */
250
+ declare const REFRESH_WINDOW_MS: number;
251
+ interface PodAppAuthConfig {
252
+ /** The app's origin, e.g. https://studio.example.com (http only on localhost). */
253
+ appUrl: string;
254
+ /** Shown on the pod's consent screen (at most 100 characters). */
255
+ clientName: string;
256
+ /** What the app asks for, each with the reason the consent screen shows. */
257
+ requests: readonly AppRequest[];
258
+ /** Workspace-template slugs the consent may offer to create (`synap_templates`). */
259
+ templates?: readonly string[];
260
+ /** SERVER-ONLY secret, at least 32 characters (SYNAP_APP_SESSION_SECRET). Several to rotate. */
261
+ secret: AppSecret;
262
+ paths?: Partial<PodAppPaths>;
263
+ /** Defaults to the sealed session cookie. */
264
+ store?: ConnectionStore;
265
+ /** RFC 8707 resource asked for (default the Hub API). */
266
+ resource?: "api/hub" | "mcp" | null;
267
+ /**
268
+ * The Synap Control Plane's URL (e.g. https://api.synap.live): offers
269
+ * "Continue with Synap Cloud" (`cloudConnectAvailable`). Unset = address only.
270
+ */
271
+ cloudUrl?: string | null;
272
+ /** Accept pods (and a Control Plane) on http://localhost (development against a local pod). */
273
+ allowInsecureLoopback?: boolean;
274
+ /** Developer log of every refused sign-in; messages never carry a token. Default: console.warn. */
275
+ onError?: (error: ConnectError, where: "connect" | "callback" | "disconnect" | "refresh") => void;
276
+ fetch?: typeof fetch;
277
+ now?: () => number;
278
+ }
279
+ type RefreshStatus =
280
+ /** No connection in this request. */
281
+ "none"
282
+ /** Prefetch or static asset: nothing refreshed (the connection is read as is). */
283
+ | "skipped"
284
+ /** Valid beyond the refresh window, or no refresh token to use. */
285
+ | "fresh"
286
+ /** Rotated: send `setCookies`, forward `cookieHeader`. */
287
+ | "refreshed"
288
+ /** The refresh failed (a lost rotation race, the pod busy) but the access token still works: keep it, set nothing. */
289
+ | "kept"
290
+ /** The access token is spent and cannot be renewed: connect again. */
291
+ | "expired"
292
+ /** The pod refused the refresh token (access removed): connect again. */
293
+ | "revoked"
294
+ /** The pod could not be reached and the access token is spent: try later. */
295
+ | "unavailable";
296
+ interface RefreshOutcome {
297
+ status: RefreshStatus;
298
+ /** A usable connection, or null. */
299
+ connection: PodConnection | null;
300
+ /** Set-Cookie values to append to the response. */
301
+ setCookies: string[];
302
+ /** The request's Cookie header with the refreshed value, to forward to the render (null = unchanged). */
303
+ cookieHeader: string | null;
304
+ error?: ConnectError;
305
+ /**
306
+ * With `none`: this browser WAS connected to that pod and the connection
307
+ * lapsed (its sealed cookie is gone, idle past its life). Say "expired,
308
+ * connect again" with the pod pre-filled, not "connect your pod".
309
+ */
310
+ lapsed?: {
311
+ podUrl: string;
312
+ };
313
+ }
314
+ interface PodAppAuth {
315
+ clientId: string;
316
+ redirectUri: string;
317
+ metadata: ClientMetadataDocument;
318
+ paths: PodAppPaths;
319
+ /** Whether "Continue with Synap Cloud" can be offered (`cloudUrl` is configured). */
320
+ cloudConnectAvailable: boolean;
321
+ handlers: {
322
+ clientMetadata(request: Request): Promise<Response>;
323
+ connect(request: Request): Promise<Response>;
324
+ callback(request: Request): Promise<Response>;
325
+ disconnect(request: Request): Promise<Response>;
326
+ /** POST: start a Synap Cloud sign-in (same-origin, form { next?, space?, prompt? }). */
327
+ cloudConnect(request: Request): Promise<Response>;
328
+ /** GET: the Synap Cloud pod chooser's return (`?iss&state`), checked against this browser's cookie. */
329
+ cloudReturn(request: Request): Promise<Response>;
330
+ };
331
+ /**
332
+ * The stored connection (no refresh, no expiry check), or null. Takes the
333
+ * request, or just its Cookie header (a server component has no Request:
334
+ * pass `(await headers()).get("cookie")`).
335
+ */
336
+ readConnection(source: Request | string | null): Promise<PodConnection | null>;
337
+ /**
338
+ * The pod this browser WAS connected to, when no connection is stored any
339
+ * more (it lapsed); null when one is stored or this browser never connected
340
+ * (or disconnected). Same sources as `readConnection`.
341
+ */
342
+ readLapsed(source: Request | string | null): Promise<{
343
+ podUrl: string;
344
+ } | null>;
345
+ /** For `proxy.ts` / a route handler: rotate near expiry, lost-race safe. */
346
+ refreshIfNeeded(request: Request): Promise<RefreshOutcome>;
347
+ /** A Hub REST client for the connection. */
348
+ client(connection: PodConnection, opts?: PodClientOptions): PodClient;
349
+ }
350
+ /** The CIMD document (spec 17 §6.1), validated the way the pod will validate it. */
351
+ declare function buildClientMetadata(opts: {
352
+ appUrl: string;
353
+ clientName: string;
354
+ requests: readonly AppRequest[];
355
+ templates?: readonly string[];
356
+ paths?: Partial<PodAppPaths>;
357
+ }): ClientMetadataDocument;
358
+ /**
359
+ * A prefetch or a static asset: `refreshIfNeeded` answers `skipped` for these.
360
+ *
361
+ * LIMIT (Next.js 16 `proxy.ts` on the Node runtime): Next strips its router
362
+ * headers (`next-router-prefetch`, `rsc`, …, `FLIGHT_HEADERS` in
363
+ * next/dist/server/web/adapter.js) and the `_rsc` query BEFORE the proxy runs,
364
+ * so a router prefetch is invisible here. Skip prefetches in the proxy
365
+ * MATCHER, which still sees the raw headers:
366
+ *
367
+ * export const config = { matcher: [{ source: "/((?!_next/static|_next/image|.*\\.\\w+$).*)",
368
+ * missing: [{ type: "header", key: "next-router-prefetch" }, { type: "header", key: "purpose", value: "prefetch" }] }] };
369
+ *
370
+ * Route handlers and other frameworks see the headers, and this still checks them.
371
+ */
372
+ declare function isPrefetchOrAsset(request: Request): boolean;
373
+ declare function createPodAppAuth(config: PodAppAuthConfig): PodAppAuth;
374
+
375
+ interface DiscoverOptions {
376
+ fetch?: typeof fetch;
377
+ /** Allow `http://localhost` (and 127.0.0.1, [::1]) pods: development only. */
378
+ allowInsecureLoopback?: boolean;
379
+ now?: () => number;
380
+ }
381
+ /**
382
+ * The canonical issuer for a typed address, or a ConnectError. Adds `https://`
383
+ * when no scheme is given; drops a trailing slash.
384
+ */
385
+ declare function canonicalPodUrl(input: string, opts?: {
386
+ allowInsecureLoopback?: boolean;
387
+ }): string;
388
+ /**
389
+ * A typed pod address (or a launch `iss`) → its validated authorization server.
390
+ * Tries the address, then `pod.<host>` (so "antoine.synap.live" finds
391
+ * "pod.antoine.synap.live").
392
+ */
393
+ declare function discoverPod(input: string, opts?: DiscoverOptions): Promise<PodIssuer>;
394
+ /**
395
+ * A same-origin path to return to after connecting, or null. Accepts a
396
+ * relative path (never `//host` or `/\host`) or an absolute URL on the app's
397
+ * own origin; anything else is dropped (no open redirect).
398
+ */
399
+ declare function safeNextPath(value: string | null | undefined, appOrigin: string): string | null;
400
+ interface LaunchHint {
401
+ /** The pod issuer the launcher named (`iss`), to pre-fill and CONFIRM (T6), never to auto-connect. */
402
+ pod: string | null;
403
+ /** Where to land after connecting (`target_link_uri`), same origin only. */
404
+ next: string | null;
405
+ /** A space to preselect on consent (`synap_space_hint`). */
406
+ spaceHint: string | null;
407
+ /**
408
+ * The `state` of a Synap Cloud return (the CP pod chooser answers
409
+ * `initiate_login_uri?iss=…&state=…`, or `?state=…&error=access_denied` when
410
+ * the person cancelled), shape-checked only. NOT verified here:
411
+ * send the request's query to the cloud handler (GET `paths.cloud`), which
412
+ * checks it against this browser's sealed cookie. Never act on `pod` because
413
+ * a state is present.
414
+ */
415
+ cloudState: string | null;
416
+ }
417
+ /**
418
+ * Read a third-party-initiated login (OIDC Core §4, spec 17 §7.2): the browser
419
+ * opens the app's `initiate_login_uri?iss=…&target_link_uri=…`. One parser for
420
+ * the browser launch and the Synap Cloud chooser's return (`cloudState`).
421
+ */
422
+ declare function acceptLaunch(url: string | URL, appOrigin: string, opts?: {
423
+ allowInsecureLoopback?: boolean;
424
+ }): LaunchHint;
425
+
426
+ /** How long a Synap Cloud sign-in may take, from the click to the pod chooser's return. */
427
+ declare const CLOUD_TRANSACTION_TTL_MS: number;
428
+ /** A sealed `CloudTransaction`: opaque, safe to put in an HttpOnly cookie. */
429
+ type SealedCloudTransaction = string & {
430
+ readonly __sealedCloudTransaction: true;
431
+ };
432
+ /** What a started Synap Cloud sign-in remembers (sealed; never sent to the CP). */
433
+ interface CloudTransaction {
434
+ v: 1;
435
+ state: string;
436
+ clientId: string;
437
+ /** The CP base it was sent to. */
438
+ cloudUrl: string;
439
+ next: string | null;
440
+ spaceHint: string | null;
441
+ prompt: "consent" | null;
442
+ createdAt: number;
443
+ }
444
+ interface CloudConnectConfig {
445
+ /** The Control Plane's base URL, e.g. https://api.synap.live. Unset = no Synap Cloud option. */
446
+ cloudUrl?: string | null;
447
+ /** Accept a CP on http://localhost (development). */
448
+ allowInsecureLoopback?: boolean;
449
+ }
450
+ /** The CP base (no trailing slash), or a ConnectError (`misconfigured`). */
451
+ declare function cloudBaseUrl(cloudUrl: string, opts?: {
452
+ allowInsecureLoopback?: boolean;
453
+ }): string;
454
+ /** Whether "Continue with Synap Cloud" can be offered: a valid Control Plane URL is configured. */
455
+ declare function cloudConnectAvailable(config?: CloudConnectConfig | null): boolean;
456
+ interface StartCloudConnectOptions extends CloudConnectConfig {
457
+ /** The app's client_id (its CIMD URL): the CP reads `initiate_login_uri` from it. */
458
+ clientId: string;
459
+ /** SERVER-ONLY app secret that seals the transaction. */
460
+ secret?: AppSecret;
461
+ /** A same-origin path to land on afterwards (validate it first: `safeNextPath`). */
462
+ next?: string | null;
463
+ /** A space to preselect on the pod's consent. */
464
+ spaceHint?: string | null;
465
+ /** Show the pod's consent again even if this browser is already connected to the pod the person picks. */
466
+ prompt?: "consent" | null;
467
+ /** @deprecated Ignored: the SDK always draws its own 32-byte state. */
468
+ state?: string;
469
+ now?: () => number;
470
+ }
471
+ interface StartedCloudConnect {
472
+ /** Redirect the browser here (303): the CP's pod chooser. */
473
+ url: string;
474
+ /** Store in an HttpOnly cookie until the CP returns. */
475
+ transaction: SealedCloudTransaction;
476
+ /** ms since epoch. */
477
+ expiresAt: number;
478
+ }
479
+ /**
480
+ * Start a Synap Cloud sign-in. Throws `cp_unavailable` at once when no CP URL
481
+ * is configured (as 1.3 always did), `misconfigured` without a secret.
482
+ */
483
+ declare function startCloudConnect(opts: StartCloudConnectOptions): Promise<StartedCloudConnect>;
484
+ interface AcceptCloudReturnOptions {
485
+ /** The URL the CP sent the browser back to (`?iss=…&state=…`). */
486
+ url: string | URL;
487
+ transaction: string | null | undefined;
488
+ clientId: string;
489
+ secret: AppSecret;
490
+ allowInsecureLoopback?: boolean;
491
+ now?: () => number;
492
+ }
493
+ interface CloudReturn {
494
+ /** The pod the person picked (canonical issuer), still to be discovered and validated. */
495
+ pod: string;
496
+ next: string | null;
497
+ spaceHint: string | null;
498
+ prompt: "consent" | null;
499
+ }
500
+ /**
501
+ * Check the CP's return against the sealed transaction of THIS browser.
502
+ * Throws `expired_transaction` (none, foreign, tampered or older than 10
503
+ * minutes), `mismatch` (the state is not this browser's, or no pod is named),
504
+ * `denied` (the person cancelled on the chooser: `error=access_denied`, no
505
+ * `iss`; nothing is started), `invalid_pod_address`.
506
+ * `state` is compared BEFORE anything else in the query is used.
507
+ */
508
+ declare function acceptCloudReturn(opts: AcceptCloudReturnOptions): Promise<CloudReturn>;
509
+
510
+ /** How long a started sign-in stays valid. */
511
+ declare const TRANSACTION_TTL_MS: number;
512
+ /** The refresh token's idle life when the pod does not say (spec 17 F5: 30 days). */
513
+ declare const DEFAULT_REFRESH_IDLE_MS: number;
514
+ /** A sealed `ConnectTransaction`: opaque, safe to put in an HttpOnly cookie. */
515
+ type SealedTransaction = string & {
516
+ readonly __sealedTransaction: true;
517
+ };
518
+ /** What a started sign-in remembers (sealed; never sent to the pod). */
519
+ interface ConnectTransaction {
520
+ v: 1;
521
+ issuer: string;
522
+ tokenEndpoint: string;
523
+ revocationEndpoint: string | null;
524
+ clientId: string;
525
+ redirectUri: string;
526
+ state: string;
527
+ verifier: string;
528
+ resource: string | null;
529
+ /** A same-origin path to land on afterwards. */
530
+ next: string | null;
531
+ createdAt: number;
532
+ }
533
+ interface FlowOptions {
534
+ fetch?: typeof fetch;
535
+ now?: () => number;
536
+ }
537
+ interface StartConnectOptions extends FlowOptions {
538
+ /** A validated issuer (`discoverPod`), or an address / launch `iss` to discover. */
539
+ pod: PodIssuer | string;
540
+ clientId: string;
541
+ redirectUri: string;
542
+ /** Grant-grammar permissions, each declared in the app's client metadata. */
543
+ permissions: readonly string[];
544
+ secret: AppSecret;
545
+ /** RFC 8707 resource: the pod's Hub API (default) or its MCP endpoint; null = none. */
546
+ resource?: "api/hub" | "mcp" | null;
547
+ /** A space to preselect on consent. */
548
+ spaceHint?: string | null;
549
+ /** A same-origin path to land on afterwards (validate it first: `safeNextPath`). */
550
+ next?: string | null;
551
+ /**
552
+ * `"consent"`: the person asked to see the consent screen again (to widen or
553
+ * narrow what the app may touch). Sent as OIDC `prompt=consent`; the sign-in
554
+ * is otherwise the same (fresh PKCE verifier and state).
555
+ */
556
+ prompt?: "consent" | null;
557
+ allowInsecureLoopback?: boolean;
558
+ }
559
+ interface StartedConnect {
560
+ /** Redirect the browser here (303). */
561
+ authorizeUrl: string;
562
+ /** Store in an HttpOnly cookie until the callback. */
563
+ transaction: SealedTransaction;
564
+ /** ms since epoch. */
565
+ expiresAt: number;
566
+ issuer: PodIssuer;
567
+ }
568
+ declare function startConnect(opts: StartConnectOptions): Promise<StartedConnect>;
569
+ /** Open a sealed transaction; refuses tampered, foreign and stale ones. */
570
+ declare function openTransaction(sealed: string | null | undefined, opts: {
571
+ secret: AppSecret;
572
+ now?: () => number;
573
+ }): Promise<ConnectTransaction>;
574
+ interface HandleCallbackOptions extends FlowOptions {
575
+ /** The full URL the pod redirected the browser to. */
576
+ callbackUrl: string | URL;
577
+ transaction: string | null | undefined;
578
+ clientId: string;
579
+ redirectUri: string;
580
+ secret: AppSecret;
581
+ }
582
+ interface CallbackResult {
583
+ connection: PodConnection;
584
+ /** The same-origin path the sign-in was started for. */
585
+ next: string | null;
586
+ }
587
+ declare function handleCallback(opts: HandleCallbackOptions): Promise<CallbackResult>;
588
+ /**
589
+ * A token response → a connection. On a refresh, `previous` supplies what the
590
+ * response may omit (grant, user, app). `expires_in` is read as sent: a refresh
591
+ * answered within the pod's reuse grace (PB3b) hands back the live key the
592
+ * first redeem got, with less than an hour left. A refresh response WITHOUT a new
593
+ * refresh_token drops the old one: with single-use rotation, resending a
594
+ * consumed token reads as theft and revokes the whole family.
595
+ */
596
+ declare function parseTokenResponse(body: Record<string, unknown>, ctx: {
597
+ podUrl: string;
598
+ clientId: string;
599
+ tokenEndpoint: string;
600
+ revocationEndpoint: string | null;
601
+ now: number;
602
+ previous?: PodConnection;
603
+ }): PodConnection;
604
+ /**
605
+ * How long a rotation's result is remembered for its SPENT refresh token.
606
+ * The pod (PB3) refuses a concurrent second redeem softly, but a token
607
+ * presented AGAIN after its use is a reuse: the whole family and the live
608
+ * access key are revoked. A browser easily sends that replay: parallel
609
+ * requests, a second tab, a response it had not stored yet, all still
610
+ * carrying the old cookie. Within this window such a request gets the
611
+ * rotation's result (and re-sends the new cookie) instead of reaching the pod.
612
+ */
613
+ declare const ROTATION_MEMORY_MS: number;
614
+ /**
615
+ * Exchange the refresh token for a new access token and the rotated refresh
616
+ * token (PB3: single-use, 30 days from issue, reach re-derived by the pod).
617
+ *
618
+ * SINGLE-FLIGHT per connection within this process: concurrent callers share
619
+ * one request, and for `ROTATION_MEMORY_MS` after it a caller still holding
620
+ * the spent token gets the same result, so this process never replays a
621
+ * spent token. Across processes nothing here can coordinate: a server-side
622
+ * `ConnectionStore` (one row per connection) is the way to rule that out.
623
+ * The caller must PERSIST the result before using its access token
624
+ * (`refreshIfNeeded` does).
625
+ *
626
+ * Throws `expired` (no refresh token, or past its idle life), `revoked`
627
+ * (invalid_grant while it should live: spent, revoked, the app removed or
628
+ * narrowed to nothing, or a concurrent redeem lost), `misconfigured`
629
+ * (invalid_client, unauthorized_client), `rate_limited`, `pod_unreachable`,
630
+ * `server_error`.
631
+ */
632
+ declare function refreshConnection(connection: PodConnection, opts?: FlowOptions): Promise<PodConnection>;
633
+ /**
634
+ * RFC 7009, best effort: the refresh token (the pod retires its whole family
635
+ * and the live access key) then the access token (for a pod without refresh).
636
+ * Never throws; `revoked` says whether the pod confirmed.
637
+ */
638
+ declare function revokeConnection(connection: PodConnection, opts?: FlowOptions): Promise<{
639
+ revoked: boolean;
640
+ }>;
641
+
642
+ /** RFC 7636 §4.2: `BASE64URL(SHA256(ASCII(code_verifier)))`. */
643
+ declare function pkceChallenge(verifier: string): Promise<string>;
644
+
645
+ declare function isBrowserRuntime(): boolean;
646
+
647
+ export { type AcceptCloudReturnOptions, AppRequest, type AppSecret, CLOUD_TRANSACTION_TTL_MS, CONNECT_ERROR_CODES, type CallbackResult, ClientMetadataDocument, type CloudConnectConfig, type CloudReturn, type CloudTransaction, ConnectError, type ConnectErrorCode, type ConnectTransaction, type ConnectionStore, type CookieNames, DEFAULT_PATHS, DEFAULT_REFRESH_IDLE_MS, type DiscoverOptions, type FlowOptions, type HandleCallbackOptions, type LaunchHint, type ListPendingProposalsOptions, MARKER_TTL_SEC, MIN_SECRET_LENGTH, type Opened, type PendingProposal, type PendingProposals, type PodAppAuth, type PodAppAuthConfig, type PodAppPaths, type PodClient, type PodClientOptions, PodConnection, type PodError, type PodErrorCode, PodGrant, PodIssuer, type PodResult, PodSpace, type PodWriteResult, type QueryValue, REFRESH_WINDOW_MS, ROTATION_MEMORY_MS, type RefreshOutcome, type RefreshStatus, type SealPurpose, type SealedCloudTransaction, type SealedCookieStoreOptions, type SealedTransaction, type StartCloudConnectOptions, type StartConnectOptions, type StartedCloudConnect, type StartedConnect, TRANSACTION_TTL_MS, acceptCloudReturn, acceptLaunch, applySetCookies, buildClientMetadata, can, canonicalPodUrl, cloudBaseUrl, cloudConnectAvailable, connectionExpiresAt, cookieNames, createPodAppAuth, createPodClient, createSealedCookieStore, discoverPod, handleCallback, isBrowserRuntime, isConnectError, isPrefetchOrAsset, markerCookie, openTransaction, parseConnectErrorCode, parseTokenResponse, pkceChallenge, readMarker, refreshConnection, reviewUrlFor, revokeConnection, safeNextPath, seal, spaces, startCloudConnect, startConnect, unseal };