@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.
- package/README.md +154 -0
- package/dist/chunk-J7XHAAFJ.js +79 -0
- package/dist/chunk-J7XHAAFJ.js.map +1 -0
- package/dist/connect.browser.cjs +22 -0
- package/dist/connect.browser.cjs.map +1 -0
- package/dist/connect.browser.d.cts +2 -0
- package/dist/connect.browser.d.ts +2 -0
- package/dist/connect.browser.js +5 -0
- package/dist/connect.browser.js.map +1 -0
- package/dist/connect.cjs +1487 -0
- package/dist/connect.cjs.map +1 -0
- package/dist/connect.d.cts +647 -0
- package/dist/connect.d.ts +647 -0
- package/dist/connect.js +1365 -0
- package/dist/connect.js.map +1 -0
- package/dist/testing.cjs +431 -19
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +160 -1
- package/dist/testing.d.ts +160 -1
- package/dist/testing.js +404 -19
- package/dist/testing.js.map +1 -1
- package/dist/types-C2KGNXEn.d.cts +95 -0
- package/dist/types-C2KGNXEn.d.ts +95 -0
- package/package.json +18 -10
- package/src/connect/auth.ts +535 -0
- package/src/connect/browser.ts +10 -0
- package/src/connect/client.ts +299 -0
- package/src/connect/cloud.ts +206 -0
- package/src/connect/discovery.ts +263 -0
- package/src/connect/encoding.ts +93 -0
- package/src/connect/errors.ts +115 -0
- package/src/connect/flow.ts +489 -0
- package/src/connect/guard.ts +25 -0
- package/src/connect/index.ts +87 -0
- package/src/connect/seal.ts +119 -0
- package/src/connect/store.ts +189 -0
- package/src/connect/types.ts +100 -0
- package/src/testing/auth-server.ts +512 -0
- package/src/testing/cloud.ts +84 -0
- package/src/testing/index.ts +7 -0
|
@@ -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 };
|