@synap-core/sdk 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +133 -0
- package/README.md +212 -12
- package/dist/{chunk-J7XHAAFJ.js → chunk-UK2U6SMA.js} +14 -2
- package/dist/chunk-UK2U6SMA.js.map +1 -0
- package/dist/{client-CVUT1Mcf.d.cts → client-B50KC2aJ.d.cts} +14 -9
- package/dist/{client-CVUT1Mcf.d.ts → client-B50KC2aJ.d.ts} +14 -9
- package/dist/connect.cjs +790 -161
- package/dist/connect.cjs.map +1 -1
- package/dist/connect.d.cts +610 -37
- package/dist/connect.d.ts +610 -37
- package/dist/connect.js +777 -162
- package/dist/connect.js.map +1 -1
- package/dist/public.cjs +9 -2
- package/dist/public.cjs.map +1 -1
- package/dist/public.d.cts +19 -4
- package/dist/public.d.ts +19 -4
- package/dist/public.js +8 -2
- package/dist/public.js.map +1 -1
- package/dist/testing.cjs +366 -17
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +190 -7
- package/dist/testing.d.ts +190 -7
- package/dist/testing.js +358 -17
- package/dist/testing.js.map +1 -1
- package/dist/{types-DpqPNaaa.d.cts → types-DZYNBCq7.d.cts} +47 -5
- package/dist/{types-DpqPNaaa.d.ts → types-DZYNBCq7.d.ts} +47 -5
- package/package.json +36 -12
- package/src/connect/auth.ts +314 -55
- package/src/connect/client.ts +472 -26
- package/src/connect/cloud.ts +210 -0
- package/src/connect/discovery.ts +79 -44
- package/src/connect/encoding.ts +1 -1
- package/src/connect/errors.ts +8 -1
- package/src/connect/flow.ts +80 -12
- package/src/connect/http.ts +42 -0
- package/src/connect/hub-types.ts +254 -0
- package/src/connect/index.ts +69 -23
- package/src/connect/rules.ts +18 -0
- package/src/connect/seal.ts +1 -1
- package/src/connect/store.ts +50 -8
- package/src/connect/types.ts +48 -4
- package/src/public/client.ts +14 -2
- package/src/public/safe-markdown.ts +13 -0
- package/src/public/share-model.ts +6 -2
- package/src/public/site-model.ts +5 -3
- package/src/testing/auth-server.ts +377 -20
- package/src/testing/cloud.ts +83 -0
- package/src/testing/fixtures.ts +26 -0
- package/src/testing/index.ts +10 -6
- package/src/testing/seed.ts +120 -0
- package/dist/chunk-J7XHAAFJ.js.map +0 -1
package/dist/connect.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { a as PodUser } from './types-
|
|
1
|
+
import { c as PodConnection, P as PodGrant, b as PodActsAs, d as PodSpace, C as ClientMetadataDocument, A as AppRequest, e as PodIssuer } from './types-DZYNBCq7.js';
|
|
2
|
+
export { a as PodUser } from './types-DZYNBCq7.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The one error type of `@synap-core/sdk/connect`. `code` is the contract a UI
|
|
@@ -15,7 +15,7 @@ type ConnectErrorCode =
|
|
|
15
15
|
| "pod_unreachable"
|
|
16
16
|
/** A Synap pod, but too old for app sign-in (no CIMD, no `iss`, no S256). */
|
|
17
17
|
| "pod_unsupported"
|
|
18
|
-
/** Connecting through Synap Cloud is not
|
|
18
|
+
/** Connecting through Synap Cloud is not configured for this app (no Control Plane URL). */
|
|
19
19
|
| "cp_unavailable"
|
|
20
20
|
/** The person said no on the consent screen. */
|
|
21
21
|
| "denied"
|
|
@@ -31,6 +31,12 @@ type ConnectErrorCode =
|
|
|
31
31
|
| "expired"
|
|
32
32
|
/** The pod refused the connection's refresh token while it should still live. */
|
|
33
33
|
| "revoked"
|
|
34
|
+
/**
|
|
35
|
+
* The pod answered a redirect where it must answer itself (a moved pod
|
|
36
|
+
* answers 308 from its old name for a while): connect again with its new
|
|
37
|
+
* address. The new address is never taken from the redirect.
|
|
38
|
+
*/
|
|
39
|
+
| "pod_moved"
|
|
34
40
|
/** The pod answered something the SDK cannot use. */
|
|
35
41
|
| "server_error";
|
|
36
42
|
declare class ConnectError extends Error {
|
|
@@ -49,7 +55,260 @@ declare const CONNECT_ERROR_CODES: readonly ConnectErrorCode[];
|
|
|
49
55
|
/** Parse an untrusted `?error=` value back into a code (null when unknown). */
|
|
50
56
|
declare function parseConnectErrorCode(value: string | null | undefined): ConnectErrorCode | null;
|
|
51
57
|
|
|
52
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Wire shapes of the Hub REST doors `createPodClient`'s typed methods read.
|
|
60
|
+
*
|
|
61
|
+
* DRIFT RISK: hand-written from synap-backend main (2026-10-22), each from the
|
|
62
|
+
* route itself, never from `@synap-core/hub-rest-client` alone:
|
|
63
|
+
* - entities: `routers/hub-protocol/rest/entities.ts` (`GET /entities`,
|
|
64
|
+
* `GET /entities/:id` → `entityToWire`, `_codecs/entity.ts`
|
|
65
|
+
* `WireEntitySchema`; `GET /entities/:id/connections` → `relations.ts`
|
|
66
|
+
* `getConnections`, `services/object-graph/entity-connections.ts`);
|
|
67
|
+
* - documents: `rest/documents.ts` `GET /documents/:id` →
|
|
68
|
+
* `services/document-patch/read-document.ts` `projectAgentDocument`;
|
|
69
|
+
* - focus sessions: `rest/focus-sessions.ts` `GET /focus-sessions` (raw rows +
|
|
70
|
+
* `triage` + `kind`), slots `services/focus-sessions/update-session.ts`
|
|
71
|
+
* `expectedOutputWireSchema`;
|
|
72
|
+
* - files: `rest/entities.ts` `GET /files/:id/url` → `file-upload.ts`
|
|
73
|
+
* `signFileEntityUrl`;
|
|
74
|
+
* - approve: `rest/proposals.ts` `POST /proposals/:id/approve`.
|
|
75
|
+
* Fields the SDK does not read stay on the objects (index signatures): the pod
|
|
76
|
+
* sends more than is typed here. The mock pod (`@synap-core/sdk/testing`)
|
|
77
|
+
* answers these shapes; `src/__tests__/connect.typed.test.ts` drives both.
|
|
78
|
+
*/
|
|
79
|
+
/** One entity, as `GET /entities` and `GET /entities/:id` answer it (`WireEntitySchema`). */
|
|
80
|
+
interface PodEntity {
|
|
81
|
+
id: string;
|
|
82
|
+
profileSlug: string;
|
|
83
|
+
/** Deprecated alias of `profileSlug`, still sent. */
|
|
84
|
+
type?: string;
|
|
85
|
+
title?: string | null;
|
|
86
|
+
preview?: string | null;
|
|
87
|
+
description?: string | null;
|
|
88
|
+
properties: Record<string, unknown>;
|
|
89
|
+
systemData?: Record<string, unknown>;
|
|
90
|
+
userId: string;
|
|
91
|
+
workspaceId?: string | null;
|
|
92
|
+
documentId?: string | null;
|
|
93
|
+
version?: number;
|
|
94
|
+
/** ISO 8601. */
|
|
95
|
+
createdAt?: string;
|
|
96
|
+
updatedAt?: string;
|
|
97
|
+
[key: string]: unknown;
|
|
98
|
+
}
|
|
99
|
+
interface ListEntitiesOptions {
|
|
100
|
+
/** One granted space. Omitted: every space the connection can read. */
|
|
101
|
+
workspaceId?: string;
|
|
102
|
+
/** One kind, e.g. `post`. */
|
|
103
|
+
profileSlug?: string;
|
|
104
|
+
/** 1-1000 (the pod's bounds); the pod's default is 20. */
|
|
105
|
+
limit?: number;
|
|
106
|
+
offset?: number;
|
|
107
|
+
/** e.g. `updatedAt:desc`. */
|
|
108
|
+
sort?: string;
|
|
109
|
+
/** With `workspaceId`: also the person's pod-wide rows (the pod's default: true). */
|
|
110
|
+
includePodWide?: boolean;
|
|
111
|
+
}
|
|
112
|
+
type PodConnectionSource = "graph" | "property" | "thread" | "context_channel" | "focus_session";
|
|
113
|
+
/** One edge of `GET /entities/:id/connections` (`EntityConnection`). */
|
|
114
|
+
interface PodEntityConnection {
|
|
115
|
+
/** The other end: an entity, or the channel / session for those sources. */
|
|
116
|
+
entityId: string;
|
|
117
|
+
/** The other entity's row (title, `type` = its kind, workspaceId, facetSlugs, …); null for channels and sessions. */
|
|
118
|
+
entity: {
|
|
119
|
+
title?: string | null;
|
|
120
|
+
type?: string | null;
|
|
121
|
+
workspaceId?: string | null;
|
|
122
|
+
facetSlugs?: string[] | null;
|
|
123
|
+
[key: string]: unknown;
|
|
124
|
+
} | null;
|
|
125
|
+
label: string;
|
|
126
|
+
direction: "outgoing" | "incoming" | "structural";
|
|
127
|
+
source: PodConnectionSource;
|
|
128
|
+
relationId?: string;
|
|
129
|
+
relationType?: string;
|
|
130
|
+
storedAs?: "link";
|
|
131
|
+
propertySlug?: string;
|
|
132
|
+
propertyLabel?: string;
|
|
133
|
+
channelId?: string;
|
|
134
|
+
channelRelationshipType?: string;
|
|
135
|
+
channelTitle?: string | null;
|
|
136
|
+
channelWorkspaceId?: string | null;
|
|
137
|
+
focusSessionId?: string;
|
|
138
|
+
focusSessionGoal?: string;
|
|
139
|
+
focusSessionStatus?: string;
|
|
140
|
+
focusSessionWorkspaceId?: string | null;
|
|
141
|
+
/** ISO 8601, or null (a property link has no date). */
|
|
142
|
+
createdAt?: string | null;
|
|
143
|
+
[key: string]: unknown;
|
|
144
|
+
}
|
|
145
|
+
interface PodEntityConnections {
|
|
146
|
+
connections: PodEntityConnection[];
|
|
147
|
+
counts: {
|
|
148
|
+
total: number;
|
|
149
|
+
graph: number;
|
|
150
|
+
structural: number;
|
|
151
|
+
threads: number;
|
|
152
|
+
contextChannels: number;
|
|
153
|
+
focusSessions: number;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
interface GetEntityConnectionsOptions {
|
|
157
|
+
/** A space lens; omitted = everything the connection can read. */
|
|
158
|
+
workspaceId?: string;
|
|
159
|
+
/** Per source, 1-200 (the pod's default: 50). */
|
|
160
|
+
limit?: number;
|
|
161
|
+
}
|
|
162
|
+
/** A document's top-level section (`listDocumentSections`). */
|
|
163
|
+
interface PodDocumentSection {
|
|
164
|
+
id: string;
|
|
165
|
+
/** Who owns it (a person's section is not an agent's to change). */
|
|
166
|
+
owner: string;
|
|
167
|
+
title: string | null;
|
|
168
|
+
author: string | null;
|
|
169
|
+
}
|
|
170
|
+
/** `GET /documents/:id` → `document` (`projectAgentDocument`). */
|
|
171
|
+
interface PodDocument {
|
|
172
|
+
id: string;
|
|
173
|
+
title: string;
|
|
174
|
+
/** `markdown`, `text`, `html`, … */
|
|
175
|
+
type: string;
|
|
176
|
+
language: string | null;
|
|
177
|
+
workspaceId: string | null;
|
|
178
|
+
format: "raw" | "readable";
|
|
179
|
+
/** Markdown; `readable` replaces embeds by their fallback text. */
|
|
180
|
+
content: string;
|
|
181
|
+
/** Pass back as `baseRevision` on a guarded edit. */
|
|
182
|
+
revision: number;
|
|
183
|
+
version: number;
|
|
184
|
+
sections: PodDocumentSection[];
|
|
185
|
+
/** What will not render; null when the check could not run (`diagnosticsError`). */
|
|
186
|
+
diagnostics: unknown[] | null;
|
|
187
|
+
diagnosticsError?: string;
|
|
188
|
+
updatedAt: string;
|
|
189
|
+
createdAt: string;
|
|
190
|
+
[key: string]: unknown;
|
|
191
|
+
}
|
|
192
|
+
interface GetDocumentOptions {
|
|
193
|
+
/** `raw` (the stored markdown, the pod's default) or `readable`. */
|
|
194
|
+
format?: "raw" | "readable";
|
|
195
|
+
}
|
|
196
|
+
/** `focus_sessions.status` (`@synap-core/types/focus-sessions` SESSION_STATUSES). */
|
|
197
|
+
type PodSessionStatus = "active" | "paused" | "forming" | "scheduled" | "closed" | "failed" | "cancelled" | "stale";
|
|
198
|
+
/**
|
|
199
|
+
* One declared deliverable of a session (`expectedOutputWireSchema`). A slot
|
|
200
|
+
* with `owner: "human"` is waiting on the person: `blockedReason` and `why` say
|
|
201
|
+
* for what, `ask` how they can answer (confirm, choose, form, act, provide).
|
|
202
|
+
*/
|
|
203
|
+
interface PodSessionSlot {
|
|
204
|
+
kind: string;
|
|
205
|
+
label: string;
|
|
206
|
+
key?: string;
|
|
207
|
+
optional?: boolean;
|
|
208
|
+
status?: "pending" | "done";
|
|
209
|
+
/** Absent = `agent`. */
|
|
210
|
+
owner?: "human" | "agent";
|
|
211
|
+
blockedReason?: string;
|
|
212
|
+
why?: string;
|
|
213
|
+
/** ISO 8601, stamped by the pod with `owner: "human"`. */
|
|
214
|
+
owedSince?: string;
|
|
215
|
+
/** How the person can answer (`@synap-core/types/ask`), or null. */
|
|
216
|
+
ask?: unknown;
|
|
217
|
+
answer?: {
|
|
218
|
+
text: string;
|
|
219
|
+
messageId: string | null;
|
|
220
|
+
answeredBy: string;
|
|
221
|
+
answeredAt: string;
|
|
222
|
+
question?: string;
|
|
223
|
+
value?: unknown;
|
|
224
|
+
[key: string]: unknown;
|
|
225
|
+
};
|
|
226
|
+
retiredAt?: string;
|
|
227
|
+
[key: string]: unknown;
|
|
228
|
+
}
|
|
229
|
+
/** One row of `GET /focus-sessions`. `expectedOutputs` is always an array here (the SDK drops anything else). */
|
|
230
|
+
interface PodFocusSession {
|
|
231
|
+
id: string;
|
|
232
|
+
workspaceId: string | null;
|
|
233
|
+
projectId: string | null;
|
|
234
|
+
userId: string;
|
|
235
|
+
title: string | null;
|
|
236
|
+
goal: string;
|
|
237
|
+
status: PodSessionStatus | (string & {});
|
|
238
|
+
templateId: string | null;
|
|
239
|
+
expectedOutputs: PodSessionSlot[];
|
|
240
|
+
channelId: string | null;
|
|
241
|
+
progress: number | null;
|
|
242
|
+
currentStage: string | null;
|
|
243
|
+
agentIds: string[];
|
|
244
|
+
closedAt: string | null;
|
|
245
|
+
metadata: unknown;
|
|
246
|
+
startedAt: string;
|
|
247
|
+
createdAt: string;
|
|
248
|
+
updatedAt: string;
|
|
249
|
+
/** Projected by the list door: `work`, `run` (a playbook or automation ran it) or `receipt`. */
|
|
250
|
+
kind?: "work" | "run" | "receipt";
|
|
251
|
+
/** Projected by the list door: an agent-started session the person has not accepted yet. */
|
|
252
|
+
triage?: {
|
|
253
|
+
pending: boolean;
|
|
254
|
+
acceptedAt: string | null;
|
|
255
|
+
acceptedBy: string | null;
|
|
256
|
+
};
|
|
257
|
+
[key: string]: unknown;
|
|
258
|
+
}
|
|
259
|
+
interface ListFocusSessionsOptions {
|
|
260
|
+
/** A space; `workspaceId` or `projectId` is required (the pod's rule). */
|
|
261
|
+
workspaceId?: string;
|
|
262
|
+
projectId?: string;
|
|
263
|
+
/** Default `all` (the pod's default on this door). */
|
|
264
|
+
status?: PodSessionStatus | "all";
|
|
265
|
+
/** 1-50 (the pod's bounds); the pod's default is 20. */
|
|
266
|
+
limit?: number;
|
|
267
|
+
/** Triage lens: `all` (default here), `default` (hides unaccepted agent sessions), `triage`. */
|
|
268
|
+
lens?: "all" | "default" | "triage";
|
|
269
|
+
/** Population lens: `all` (default here), `work`, `run`, `receipt`. */
|
|
270
|
+
kind?: "all" | "work" | "run" | "receipt";
|
|
271
|
+
/** Sessions about one thing. */
|
|
272
|
+
subjectEntityId?: string;
|
|
273
|
+
}
|
|
274
|
+
/** `GET /files/:id/url`: a signed GET for a file entity's blob. */
|
|
275
|
+
interface PodFileUrl {
|
|
276
|
+
/** Served as an `attachment`, with the stored type. */
|
|
277
|
+
url: string;
|
|
278
|
+
/** ISO 8601; the signature's end (about an hour). */
|
|
279
|
+
expiresAt: string;
|
|
280
|
+
/** Decide drawability on this, never on the URL; null when unknown. */
|
|
281
|
+
mimeType: string | null;
|
|
282
|
+
}
|
|
283
|
+
/** `POST /proposals/:id/approve`. */
|
|
284
|
+
interface PodApproval {
|
|
285
|
+
proposalId: string;
|
|
286
|
+
/** The row the approval produced or linked, when it names one. */
|
|
287
|
+
primaryId?: string;
|
|
288
|
+
linked?: number;
|
|
289
|
+
/** The proposal after the approval (status, execution result), when the pod could read it back. */
|
|
290
|
+
proposal: Record<string, unknown> | null;
|
|
291
|
+
}
|
|
292
|
+
interface ApproveProposalOptions {
|
|
293
|
+
/**
|
|
294
|
+
* The proposal's space, when known: refused locally unless the connection
|
|
295
|
+
* holds `proposal.approve` there. Omitted: needs it in some space, and the
|
|
296
|
+
* pod checks the proposal's own space.
|
|
297
|
+
*/
|
|
298
|
+
workspaceId?: string;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
type PodErrorCode = "expired" | "unauthorized" | "forbidden" | "door_unsupported" | "not_found" | "invalid" | "conflict" | "rate_limited" | "pod_unreachable"
|
|
302
|
+
/** The pod answered a redirect (a moved pod answers 308 from its old name): connect again at its new address. */
|
|
303
|
+
| "pod_moved" | "server";
|
|
304
|
+
/**
|
|
305
|
+
* Why the SDK refused before sending anything (`status: null`):
|
|
306
|
+
* - `agent_cannot_approve`: an agent connection asked to approve (the pod
|
|
307
|
+
* never lets an agent credential approve; `code: "forbidden"`);
|
|
308
|
+
* - `not_granted`: the connection holds no permission for it (`forbidden`);
|
|
309
|
+
* - `invalid_input`: an id, a path or an option the pod would refuse (`invalid`).
|
|
310
|
+
*/
|
|
311
|
+
type PodRefusalReason = "agent_cannot_approve" | "not_granted" | "invalid_input";
|
|
53
312
|
interface PodError {
|
|
54
313
|
code: PodErrorCode;
|
|
55
314
|
/** The HTTP status, or null when nothing was answered. */
|
|
@@ -57,6 +316,17 @@ interface PodError {
|
|
|
57
316
|
retryAfterSec?: number;
|
|
58
317
|
/** The pod's own error text, for logs only. */
|
|
59
318
|
detail?: string;
|
|
319
|
+
/** Set when the SDK refused locally (1.5). */
|
|
320
|
+
reason?: PodRefusalReason;
|
|
321
|
+
/**
|
|
322
|
+
* On `unauthorized` (1.5): the pod refused the access token, the connection
|
|
323
|
+
* still holds a refresh token, and this client could not renew it (no
|
|
324
|
+
* `renew`). A forced refresh may restore it (a narrow, or a pod-side "Revoke
|
|
325
|
+
* key", ends the key, not the connection): run `refreshIfNeeded(request,
|
|
326
|
+
* { force: true })` where cookies can be set. Absent = the pod refused the
|
|
327
|
+
* connection itself: connect again.
|
|
328
|
+
*/
|
|
329
|
+
refreshable?: true;
|
|
60
330
|
}
|
|
61
331
|
type PodResult<T> = {
|
|
62
332
|
ok: true;
|
|
@@ -80,24 +350,121 @@ type PodWriteResult<T> = {
|
|
|
80
350
|
error: PodError;
|
|
81
351
|
};
|
|
82
352
|
type QueryValue = string | number | boolean | null | undefined | ReadonlyArray<string | number | boolean>;
|
|
353
|
+
/** One pending proposal, the pod's summary row (`GET /proposals?view=basic`). */
|
|
354
|
+
interface PendingProposal {
|
|
355
|
+
id: string;
|
|
356
|
+
proposalType: string;
|
|
357
|
+
targetType: string;
|
|
358
|
+
targetId: string | null;
|
|
359
|
+
workspaceId: string | null;
|
|
360
|
+
/** ISO 8601, as the pod sent it. */
|
|
361
|
+
createdAt: string;
|
|
362
|
+
/** The agent that filed it (this app's agent, or another one), when known. */
|
|
363
|
+
agentUserId: string | null;
|
|
364
|
+
sessionId: string | null;
|
|
365
|
+
summary?: string;
|
|
366
|
+
/** Where the person reviews it: the pod's deep link. */
|
|
367
|
+
reviewUrl: string;
|
|
368
|
+
}
|
|
369
|
+
interface PendingProposals {
|
|
370
|
+
/** Every pending proposal the connection can see, not only this page (the pod COUNTs it). */
|
|
371
|
+
total: number;
|
|
372
|
+
proposals: PendingProposal[];
|
|
373
|
+
hasMore: boolean;
|
|
374
|
+
}
|
|
375
|
+
interface ListPendingProposalsOptions {
|
|
376
|
+
/** One granted space only. */
|
|
377
|
+
workspaceId?: string;
|
|
378
|
+
/** 1-200, default 20. */
|
|
379
|
+
limit?: number;
|
|
380
|
+
offset?: number;
|
|
381
|
+
/**
|
|
382
|
+
* Only these proposals (at most 100 ids; the pod's `proposalIds` filter,
|
|
383
|
+
* pods that do not filter are filtered here). An empty list matches nothing (answered without a request).
|
|
384
|
+
*/
|
|
385
|
+
proposalIds?: readonly string[];
|
|
386
|
+
}
|
|
83
387
|
interface PodClientOptions {
|
|
84
388
|
fetch?: typeof fetch;
|
|
85
389
|
now?: () => number;
|
|
390
|
+
/** Per Hub request, default 10 000 ms; a request past it is `pod_unreachable`. */
|
|
391
|
+
timeoutMs?: number;
|
|
392
|
+
/** The most a Hub answer may weigh, default 8 MiB; a bigger one is `server` (the body is not read further). */
|
|
393
|
+
maxResponseBytes?: number;
|
|
394
|
+
/**
|
|
395
|
+
* Renew the connection after the pod refused its access token (401) while it
|
|
396
|
+
* holds a refresh token: rotate it, PERSIST the result (the old refresh token
|
|
397
|
+
* is spent once the pod answers), then return it; throw the refresh's
|
|
398
|
+
* `ConnectError` when it fails. The client calls it at most once (concurrent
|
|
399
|
+
* 401s share the call), retries the refused request once with the result, and
|
|
400
|
+
* uses it from then on. `PodAppAuth.client(connection, { persist })` builds
|
|
401
|
+
* one. Without it a 401 stays `unauthorized` (with `refreshable`).
|
|
402
|
+
*/
|
|
403
|
+
renew?: (connection: PodConnection) => Promise<PodConnection>;
|
|
86
404
|
}
|
|
87
405
|
interface PodClient {
|
|
88
406
|
podUrl: string;
|
|
89
|
-
|
|
90
|
-
|
|
407
|
+
/** The connection's, or the renewed one's after a 401 (`renew`). */
|
|
408
|
+
readonly user: PodConnection["user"];
|
|
409
|
+
readonly grant: PodGrant[];
|
|
410
|
+
/** Who the token acts as (`connectionActsAs`): `"agent"` unless the pod said `"person"`. */
|
|
411
|
+
readonly actsAs: PodActsAs;
|
|
91
412
|
/** Whether the connection was granted `permission` (in `workspaceId` when given). */
|
|
92
413
|
can(permission: string, workspaceId?: string): boolean;
|
|
414
|
+
/** The granted spaces, each with its permissions (`spaces(connection)`). */
|
|
415
|
+
spaces(): PodSpace[];
|
|
416
|
+
/**
|
|
417
|
+
* "Waiting for review: N". The person's pending proposals in the spaces this
|
|
418
|
+
* connection may read proposals in (any agent's, not only this app's), from
|
|
419
|
+
* the pod's seamed `GET /proposals` door. Needs the `proposal.read`
|
|
420
|
+
* permission (ask for it in `requests`): without it the pod would answer an
|
|
421
|
+
* empty list, so this answers `forbidden` instead, without a request.
|
|
422
|
+
*/
|
|
423
|
+
listPendingProposals(opts?: ListPendingProposalsOptions): Promise<PodResult<PendingProposals>>;
|
|
424
|
+
/** `GET /entities` (no full-text `q`: a space-scoped token may list, never search). */
|
|
425
|
+
listEntities(opts?: ListEntitiesOptions): Promise<PodResult<PodEntity[]>>;
|
|
426
|
+
/** `GET /entities/:id`; `not_found` when it does not exist or is not readable. */
|
|
427
|
+
getEntity(id: string): Promise<PodResult<PodEntity>>;
|
|
428
|
+
/** `GET /entities/:id/connections`: its relations, property links, threads and sessions. */
|
|
429
|
+
getEntityConnections(id: string, opts?: GetEntityConnectionsOptions): Promise<PodResult<PodEntityConnections>>;
|
|
430
|
+
/** `GET /documents/:id`: content, revision, sections. */
|
|
431
|
+
getDocument(id: string, opts?: GetDocumentOptions): Promise<PodResult<PodDocument>>;
|
|
432
|
+
/**
|
|
433
|
+
* `GET /focus-sessions` in a space (or a project), newest first, with their
|
|
434
|
+
* slots (`expectedOutputs`; `humanSlots(session)` = what waits on the person).
|
|
435
|
+
* `invalid` without a request when neither `workspaceId` nor `projectId` is given.
|
|
436
|
+
*/
|
|
437
|
+
listFocusSessions(opts: ListFocusSessionsOptions): Promise<PodResult<PodFocusSession[]>>;
|
|
438
|
+
/** `GET /files/:id/url`: a signed download URL for a file entity (about an hour). */
|
|
439
|
+
getFileUrl(entityId: string): Promise<PodResult<PodFileUrl>>;
|
|
440
|
+
/**
|
|
441
|
+
* `POST /proposals/:id/approve`, as the person. Only a PERSON connection
|
|
442
|
+
* (`actsAs: "person"`) holding `proposal.approve` can: an agent connection
|
|
443
|
+
* gets `forbidden` / `agent_cannot_approve` and NOTHING is sent (the pod
|
|
444
|
+
* never lets an agent approve); without the permission, `forbidden` /
|
|
445
|
+
* `not_granted`, also without a request.
|
|
446
|
+
*/
|
|
447
|
+
approveProposal(id: string, opts?: ApproveProposalOptions): Promise<PodResult<PodApproval>>;
|
|
93
448
|
get<T = unknown>(path: string, query?: Record<string, QueryValue>): Promise<PodResult<T>>;
|
|
94
449
|
post<T = unknown>(path: string, body?: unknown): Promise<PodWriteResult<T>>;
|
|
95
450
|
patch<T = unknown>(path: string, body?: unknown): Promise<PodWriteResult<T>>;
|
|
96
451
|
put<T = unknown>(path: string, body?: unknown): Promise<PodWriteResult<T>>;
|
|
97
452
|
delete<T = unknown>(path: string, body?: unknown): Promise<PodWriteResult<T>>;
|
|
98
453
|
}
|
|
454
|
+
/** Who `connection`'s token acts as: `"person"` only when the pod said so, else `"agent"`. */
|
|
455
|
+
declare function connectionActsAs(connection: Pick<PodConnection, "actsAs">): PodActsAs;
|
|
456
|
+
/**
|
|
457
|
+
* The slots of `session` that wait on the person (Home's "needs you"):
|
|
458
|
+
* `owner: "human"`, not done, not retired.
|
|
459
|
+
*/
|
|
460
|
+
declare function humanSlots(session: Pick<PodFocusSession, "expectedOutputs">): PodSessionSlot[];
|
|
99
461
|
/** Whether `connection` was granted `permission` (in `workspaceId` when given). Exact match: grants carry no wildcard. */
|
|
100
462
|
declare function can(connection: Pick<PodConnection, "grant">, permission: string, workspaceId?: string): boolean;
|
|
463
|
+
/**
|
|
464
|
+
* The grant by space, in the grant's order, each permission once. The pod
|
|
465
|
+
* sends one row per permission per space; a space switcher wants one per space.
|
|
466
|
+
*/
|
|
467
|
+
declare function spaces(connection: Pick<PodConnection, "grant">): PodSpace[];
|
|
101
468
|
/**
|
|
102
469
|
* Where to review a proposal: the pod's answer, resolved against the pod and
|
|
103
470
|
* kept only on the pod's own origin; else the pod's deep link `/open/<id>`.
|
|
@@ -105,7 +472,7 @@ declare function can(connection: Pick<PodConnection, "grant">, permission: strin
|
|
|
105
472
|
declare function reviewUrlFor(podUrl: string, reviewUrl: unknown, proposalId: string | null): string;
|
|
106
473
|
declare function createPodClient(connection: PodConnection, opts?: PodClientOptions): PodClient;
|
|
107
474
|
|
|
108
|
-
type SealPurpose = "synap-connect/transaction" | "synap-connect/session";
|
|
475
|
+
type SealPurpose = "synap-connect/transaction" | "synap-connect/session" | "synap-connect/cloud";
|
|
109
476
|
/** The app secret: one value, or several to rotate (the first seals, all open). */
|
|
110
477
|
type AppSecret = string | readonly string[];
|
|
111
478
|
declare const MIN_SECRET_LENGTH = 32;
|
|
@@ -143,9 +510,32 @@ interface ConnectionStore {
|
|
|
143
510
|
interface CookieNames {
|
|
144
511
|
session: string;
|
|
145
512
|
transaction: string;
|
|
513
|
+
/** A Synap Cloud sign-in in progress (sealed state, 10 minutes). */
|
|
514
|
+
cloud: string;
|
|
515
|
+
/** "This browser was connected to <pod>": the pod's URL only, no secret (`markerCookie`). */
|
|
516
|
+
marker: string;
|
|
146
517
|
}
|
|
147
518
|
/** `__Host-` names on https (Secure, Path=/, no Domain: bound to this exact host). */
|
|
148
519
|
declare function cookieNames(secure: boolean): CookieNames;
|
|
520
|
+
/** The marker outlives the connection: as long as browsers keep a cookie (400 days). */
|
|
521
|
+
declare const MARKER_TTL_SEC: number;
|
|
522
|
+
/**
|
|
523
|
+
* The marker cookie: which pod this browser was connected to, so a connection
|
|
524
|
+
* that lapsed (idle past the refresh token's life, its sealed cookie gone)
|
|
525
|
+
* reads as "expired, connect again to <pod>" rather than "never connected".
|
|
526
|
+
* It holds the pod's public URL and NOTHING else: no token, no user, nothing
|
|
527
|
+
* sealed. A forged value can only pre-fill the address the person confirms.
|
|
528
|
+
*/
|
|
529
|
+
declare function markerCookie(podUrl: string, opts: {
|
|
530
|
+
secure: boolean;
|
|
531
|
+
name?: string;
|
|
532
|
+
}): string;
|
|
533
|
+
/** The marker's pod URL, or null (absent, or not a canonical pod address). */
|
|
534
|
+
declare function readMarker(cookieHeader: string | null, opts: {
|
|
535
|
+
secure: boolean;
|
|
536
|
+
name?: string;
|
|
537
|
+
allowInsecureLoopback?: boolean;
|
|
538
|
+
}): string | null;
|
|
149
539
|
/** When the stored value stops being useful. */
|
|
150
540
|
declare function connectionExpiresAt(c: PodConnection): number;
|
|
151
541
|
interface SealedCookieStoreOptions {
|
|
@@ -169,6 +559,8 @@ interface PodAppPaths {
|
|
|
169
559
|
connect: string;
|
|
170
560
|
callback: string;
|
|
171
561
|
disconnect: string;
|
|
562
|
+
/** Synap Cloud: POST starts it, GET is the pod chooser's return (forwarded from the connect page). */
|
|
563
|
+
cloud: string;
|
|
172
564
|
/** The app's connect page (also the CIMD `initiate_login_uri`). */
|
|
173
565
|
connectPage: string;
|
|
174
566
|
/** Where a failed callback lands, with `?error=<code>`. */
|
|
@@ -180,7 +572,12 @@ declare const DEFAULT_PATHS: PodAppPaths;
|
|
|
180
572
|
/** Refresh only inside the last 5 minutes of the access token's life. */
|
|
181
573
|
declare const REFRESH_WINDOW_MS: number;
|
|
182
574
|
interface PodAppAuthConfig {
|
|
183
|
-
/**
|
|
575
|
+
/**
|
|
576
|
+
* The app's origin, e.g. https://studio.example.com. `http://localhost` is
|
|
577
|
+
* accepted for the SDK's MOCK pod only: every real pod, a local one
|
|
578
|
+
* included, refuses an http client_id and redirect_uri, so an app that signs
|
|
579
|
+
* in to a real pod runs on https (a tunnel in development).
|
|
580
|
+
*/
|
|
184
581
|
appUrl: string;
|
|
185
582
|
/** Shown on the pod's consent screen (at most 100 characters). */
|
|
186
583
|
clientName: string;
|
|
@@ -195,7 +592,16 @@ interface PodAppAuthConfig {
|
|
|
195
592
|
store?: ConnectionStore;
|
|
196
593
|
/** RFC 8707 resource asked for (default the Hub API). */
|
|
197
594
|
resource?: "api/hub" | "mcp" | null;
|
|
198
|
-
/**
|
|
595
|
+
/**
|
|
596
|
+
* The Synap Control Plane's URL (e.g. https://api.synap.live): offers
|
|
597
|
+
* "Continue with Synap Cloud" (`cloudConnectAvailable`). Unset = address only.
|
|
598
|
+
*/
|
|
599
|
+
cloudUrl?: string | null;
|
|
600
|
+
/**
|
|
601
|
+
* Accept a pod (and a Control Plane) on http://localhost: development
|
|
602
|
+
* against the SDK's mock pod (`@synap-core/sdk/testing`). A real local pod
|
|
603
|
+
* still refuses an app that is not on https (see `appUrl`).
|
|
604
|
+
*/
|
|
199
605
|
allowInsecureLoopback?: boolean;
|
|
200
606
|
/** Developer log of every refused sign-in; messages never carry a token. Default: console.warn. */
|
|
201
607
|
onError?: (error: ConnectError, where: "connect" | "callback" | "disconnect" | "refresh") => void;
|
|
@@ -213,9 +619,9 @@ type RefreshStatus =
|
|
|
213
619
|
| "refreshed"
|
|
214
620
|
/** The refresh failed (a lost rotation race, the pod busy) but the access token still works: keep it, set nothing. */
|
|
215
621
|
| "kept"
|
|
216
|
-
/** The access token is spent and cannot be renewed: connect again. */
|
|
622
|
+
/** The access token is spent and cannot be renewed (lapsed, or the pod moved: `error.code`): connect again. The session cookie is cleared (the marker stays). */
|
|
217
623
|
| "expired"
|
|
218
|
-
/** The pod refused the refresh token (access removed): connect again. */
|
|
624
|
+
/** The pod refused the refresh token (access removed): connect again. The session cookie is cleared (the marker stays). */
|
|
219
625
|
| "revoked"
|
|
220
626
|
/** The pod could not be reached and the access token is spent: try later. */
|
|
221
627
|
| "unavailable";
|
|
@@ -228,17 +634,53 @@ interface RefreshOutcome {
|
|
|
228
634
|
/** The request's Cookie header with the refreshed value, to forward to the render (null = unchanged). */
|
|
229
635
|
cookieHeader: string | null;
|
|
230
636
|
error?: ConnectError;
|
|
637
|
+
/**
|
|
638
|
+
* With `none`: this browser WAS connected to that pod and the connection
|
|
639
|
+
* lapsed (its sealed cookie is gone, idle past its life). Say "expired,
|
|
640
|
+
* connect again" with the pod pre-filled, not "connect your pod".
|
|
641
|
+
*/
|
|
642
|
+
lapsed?: {
|
|
643
|
+
podUrl: string;
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
interface RefreshOptions {
|
|
647
|
+
/**
|
|
648
|
+
* Rotate now, whatever the access token's expiry says: the pod refused it
|
|
649
|
+
* (a Hub call answered `unauthorized` with `refreshable`). Needs a refresh
|
|
650
|
+
* token. If the pod refuses the refresh too, the outcome is `revoked` (or
|
|
651
|
+
* `expired`), never `kept`: the access token is known dead.
|
|
652
|
+
*/
|
|
653
|
+
force?: boolean;
|
|
654
|
+
}
|
|
655
|
+
interface PodAppClientOptions extends PodClientOptions {
|
|
656
|
+
/**
|
|
657
|
+
* Where the client may set cookies (a route handler, a server action): after
|
|
658
|
+
* a 401 it then renews the connection ONCE (rotate, seal, `persist` the
|
|
659
|
+
* Set-Cookie values, retry the request once). The rotation spends the old
|
|
660
|
+
* refresh token, so it is never done where its result cannot be stored: a
|
|
661
|
+
* server component passes no `persist`, and a 401 there answers
|
|
662
|
+
* `unauthorized` with `refreshable` (see `RefreshOptions.force`).
|
|
663
|
+
*/
|
|
664
|
+
persist?: (setCookies: string[]) => void | Promise<void>;
|
|
665
|
+
/** The request (or its Cookie header) the connection came from: chunks a larger old cookie left are cleared. */
|
|
666
|
+
request?: Request | string | null;
|
|
231
667
|
}
|
|
232
668
|
interface PodAppAuth {
|
|
233
669
|
clientId: string;
|
|
234
670
|
redirectUri: string;
|
|
235
671
|
metadata: ClientMetadataDocument;
|
|
236
672
|
paths: PodAppPaths;
|
|
673
|
+
/** Whether "Continue with Synap Cloud" can be offered (`cloudUrl` is configured). */
|
|
674
|
+
cloudConnectAvailable: boolean;
|
|
237
675
|
handlers: {
|
|
238
676
|
clientMetadata(request: Request): Promise<Response>;
|
|
239
677
|
connect(request: Request): Promise<Response>;
|
|
240
678
|
callback(request: Request): Promise<Response>;
|
|
241
679
|
disconnect(request: Request): Promise<Response>;
|
|
680
|
+
/** POST: start a Synap Cloud sign-in (same-origin, form { next?, space?, prompt? }). */
|
|
681
|
+
cloudConnect(request: Request): Promise<Response>;
|
|
682
|
+
/** GET: the Synap Cloud pod chooser's return (`?iss&state`), checked against this browser's cookie. */
|
|
683
|
+
cloudReturn(request: Request): Promise<Response>;
|
|
242
684
|
};
|
|
243
685
|
/**
|
|
244
686
|
* The stored connection (no refresh, no expiry check), or null. Takes the
|
|
@@ -246,12 +688,25 @@ interface PodAppAuth {
|
|
|
246
688
|
* pass `(await headers()).get("cookie")`).
|
|
247
689
|
*/
|
|
248
690
|
readConnection(source: Request | string | null): Promise<PodConnection | null>;
|
|
249
|
-
/**
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
691
|
+
/**
|
|
692
|
+
* The pod this browser WAS connected to, when no connection is stored any
|
|
693
|
+
* more (it lapsed); null when one is stored or this browser never connected
|
|
694
|
+
* (or disconnected). Same sources as `readConnection`.
|
|
695
|
+
*/
|
|
696
|
+
readLapsed(source: Request | string | null): Promise<{
|
|
697
|
+
podUrl: string;
|
|
698
|
+
} | null>;
|
|
699
|
+
/** For `proxy.ts` / a route handler: rotate near expiry (or now, `force`), lost-race safe. */
|
|
700
|
+
refreshIfNeeded(request: Request, opts?: RefreshOptions): Promise<RefreshOutcome>;
|
|
701
|
+
/** A Hub REST client for the connection (renewing it after a 401 when it can `persist`). */
|
|
702
|
+
client(connection: PodConnection, opts?: PodAppClientOptions): PodClient;
|
|
253
703
|
}
|
|
254
|
-
/**
|
|
704
|
+
/**
|
|
705
|
+
* The app's client metadata document (its client_id URL serves it). Refuses,
|
|
706
|
+
* at setup, every document the pod would refuse at sign-in: the permissions
|
|
707
|
+
* (`assertAppPermission`), at most 32 requests counted as sent, duplicates
|
|
708
|
+
* included, an IP address origin, the 5 KB cap.
|
|
709
|
+
*/
|
|
255
710
|
declare function buildClientMetadata(opts: {
|
|
256
711
|
appUrl: string;
|
|
257
712
|
clientName: string;
|
|
@@ -259,6 +714,21 @@ declare function buildClientMetadata(opts: {
|
|
|
259
714
|
templates?: readonly string[];
|
|
260
715
|
paths?: Partial<PodAppPaths>;
|
|
261
716
|
}): ClientMetadataDocument;
|
|
717
|
+
/**
|
|
718
|
+
* A prefetch or a static asset: `refreshIfNeeded` answers `skipped` for these.
|
|
719
|
+
*
|
|
720
|
+
* LIMIT (Next.js 16 `proxy.ts` on the Node runtime): Next strips its router
|
|
721
|
+
* headers (`next-router-prefetch`, `rsc`, …, `FLIGHT_HEADERS` in
|
|
722
|
+
* next/dist/server/web/adapter.js) and the `_rsc` query BEFORE the proxy runs,
|
|
723
|
+
* so a router prefetch is invisible here. Skip prefetches in the proxy
|
|
724
|
+
* MATCHER, which still sees the raw headers:
|
|
725
|
+
*
|
|
726
|
+
* export const config = { matcher: [{ source: "/((?!_next/static|_next/image|.*\\.\\w+$).*)",
|
|
727
|
+
* missing: [{ type: "header", key: "next-router-prefetch" }, { type: "header", key: "purpose", value: "prefetch" }] }] };
|
|
728
|
+
*
|
|
729
|
+
* Route handlers and other frameworks see the headers, and this still checks them.
|
|
730
|
+
*/
|
|
731
|
+
declare function isPrefetchOrAsset(request: Request): boolean;
|
|
262
732
|
declare function createPodAppAuth(config: PodAppAuthConfig): PodAppAuth;
|
|
263
733
|
|
|
264
734
|
interface DiscoverOptions {
|
|
@@ -276,17 +746,13 @@ declare function canonicalPodUrl(input: string, opts?: {
|
|
|
276
746
|
}): string;
|
|
277
747
|
/**
|
|
278
748
|
* A typed pod address (or a launch `iss`) → its validated authorization server.
|
|
279
|
-
* Tries the address, then
|
|
280
|
-
* "pod.
|
|
749
|
+
* Tries the address, then the pod names it may stand for: on Synap Cloud,
|
|
750
|
+
* "you.synap.live" finds "you.pod.synap.live" (and the older
|
|
751
|
+
* "pod.you.synap.live"); elsewhere, "example.com" finds "pod.example.com".
|
|
752
|
+
* A redirect is never followed: an old name that answers 308 counts as a
|
|
753
|
+
* miss (`pod_moved` when nothing else answers).
|
|
281
754
|
*/
|
|
282
755
|
declare function discoverPod(input: string, opts?: DiscoverOptions): Promise<PodIssuer>;
|
|
283
|
-
/** Whether "Continue with Synap Cloud" can be offered. False until the CP pod chooser (C1) ships. */
|
|
284
|
-
declare function cloudConnectAvailable(): boolean;
|
|
285
|
-
/** The Synap Cloud pod chooser (C1). Not available yet: always throws `cp_unavailable`. */
|
|
286
|
-
declare function startCloudConnect(_opts: {
|
|
287
|
-
clientId: string;
|
|
288
|
-
state?: string;
|
|
289
|
-
}): never;
|
|
290
756
|
/**
|
|
291
757
|
* A same-origin path to return to after connecting, or null. Accepts a
|
|
292
758
|
* relative path (never `//host` or `/\host`) or an absolute URL on the app's
|
|
@@ -294,25 +760,126 @@ declare function startCloudConnect(_opts: {
|
|
|
294
760
|
*/
|
|
295
761
|
declare function safeNextPath(value: string | null | undefined, appOrigin: string): string | null;
|
|
296
762
|
interface LaunchHint {
|
|
297
|
-
/** The pod issuer the launcher named (`iss`), to pre-fill and CONFIRM
|
|
763
|
+
/** The pod issuer the launcher named (`iss`), to pre-fill and CONFIRM, never to auto-connect. */
|
|
298
764
|
pod: string | null;
|
|
299
765
|
/** Where to land after connecting (`target_link_uri`), same origin only. */
|
|
300
766
|
next: string | null;
|
|
301
|
-
/** A space
|
|
767
|
+
/** A space hint for the pod's consent (`synap_space_hint`); a pod may ignore it. */
|
|
302
768
|
spaceHint: string | null;
|
|
769
|
+
/**
|
|
770
|
+
* The `state` of a Synap Cloud return (the CP pod chooser answers
|
|
771
|
+
* `initiate_login_uri?iss=…&state=…`, or `?state=…&error=access_denied` when
|
|
772
|
+
* the person cancelled), shape-checked only. NOT verified here:
|
|
773
|
+
* send the request's query to the cloud handler (GET `paths.cloud`), which
|
|
774
|
+
* checks it against this browser's sealed cookie. Never act on `pod` because
|
|
775
|
+
* a state is present.
|
|
776
|
+
*/
|
|
777
|
+
cloudState: string | null;
|
|
303
778
|
}
|
|
304
779
|
/**
|
|
305
|
-
* Read a third-party-initiated login (OIDC Core §4
|
|
780
|
+
* Read a third-party-initiated login (OIDC Core §4): the browser
|
|
306
781
|
* opens the app's `initiate_login_uri?iss=…&target_link_uri=…`. One parser for
|
|
307
|
-
* the browser launch and
|
|
782
|
+
* the browser launch and the Synap Cloud chooser's return (`cloudState`).
|
|
308
783
|
*/
|
|
309
784
|
declare function acceptLaunch(url: string | URL, appOrigin: string, opts?: {
|
|
310
785
|
allowInsecureLoopback?: boolean;
|
|
311
786
|
}): LaunchHint;
|
|
312
787
|
|
|
788
|
+
/** How long a Synap Cloud sign-in may take, from the click to the pod chooser's return. */
|
|
789
|
+
declare const CLOUD_TRANSACTION_TTL_MS: number;
|
|
790
|
+
/** A sealed `CloudTransaction`: opaque, safe to put in an HttpOnly cookie. */
|
|
791
|
+
type SealedCloudTransaction = string & {
|
|
792
|
+
readonly __sealedCloudTransaction: true;
|
|
793
|
+
};
|
|
794
|
+
/** What a started Synap Cloud sign-in remembers (sealed; never sent to the CP). */
|
|
795
|
+
interface CloudTransaction {
|
|
796
|
+
v: 1;
|
|
797
|
+
state: string;
|
|
798
|
+
clientId: string;
|
|
799
|
+
/** The CP base it was sent to. */
|
|
800
|
+
cloudUrl: string;
|
|
801
|
+
next: string | null;
|
|
802
|
+
spaceHint: string | null;
|
|
803
|
+
prompt: "consent" | null;
|
|
804
|
+
createdAt: number;
|
|
805
|
+
}
|
|
806
|
+
interface CloudConnectConfig {
|
|
807
|
+
/** The Control Plane's base URL, e.g. https://api.synap.live. Unset = no Synap Cloud option. */
|
|
808
|
+
cloudUrl?: string | null;
|
|
809
|
+
/** Accept a CP on http://localhost (development). */
|
|
810
|
+
allowInsecureLoopback?: boolean;
|
|
811
|
+
}
|
|
812
|
+
/** The CP base (no trailing slash), or a ConnectError (`misconfigured`). */
|
|
813
|
+
declare function cloudBaseUrl(cloudUrl: string, opts?: {
|
|
814
|
+
allowInsecureLoopback?: boolean;
|
|
815
|
+
}): string;
|
|
816
|
+
/** Whether "Continue with Synap Cloud" can be offered: a valid Control Plane URL is configured. */
|
|
817
|
+
declare function cloudConnectAvailable(config?: CloudConnectConfig | null): boolean;
|
|
818
|
+
interface StartCloudConnectOptions extends CloudConnectConfig {
|
|
819
|
+
/** The app's client_id (its CIMD URL): the CP reads `initiate_login_uri` from it. */
|
|
820
|
+
clientId: string;
|
|
821
|
+
/**
|
|
822
|
+
* SERVER-ONLY app secret that seals the transaction. Required: without it the call throws `misconfigured`.
|
|
823
|
+
* Optional in the type only for 1.x semver; required in 2.0.
|
|
824
|
+
*/
|
|
825
|
+
secret?: AppSecret;
|
|
826
|
+
/** A same-origin path to land on afterwards (validate it first: `safeNextPath`). */
|
|
827
|
+
next?: string | null;
|
|
828
|
+
/** A space hint for the pod's consent (`synap_space_hint`); a pod may ignore it. */
|
|
829
|
+
spaceHint?: string | null;
|
|
830
|
+
/** Show the pod's consent again even if this browser is already connected to the pod the person picks. */
|
|
831
|
+
prompt?: "consent" | null;
|
|
832
|
+
/** @deprecated Ignored: the SDK always draws its own 32-byte state. Removed in 2.0. */
|
|
833
|
+
state?: string;
|
|
834
|
+
now?: () => number;
|
|
835
|
+
}
|
|
836
|
+
interface StartedCloudConnect {
|
|
837
|
+
/** Redirect the browser here (303): the CP's pod chooser. */
|
|
838
|
+
url: string;
|
|
839
|
+
/** Store in an HttpOnly cookie until the CP returns. */
|
|
840
|
+
transaction: SealedCloudTransaction;
|
|
841
|
+
/** ms since epoch. */
|
|
842
|
+
expiresAt: number;
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Start a Synap Cloud sign-in. Throws `cp_unavailable` at once when no CP URL
|
|
846
|
+
* is configured, `misconfigured` without a secret: both SYNCHRONOUSLY, before
|
|
847
|
+
* a Promise exists, so wrap the call in try/catch rather than relying on
|
|
848
|
+
* `.catch` (kept from 1.3 for compatibility; 2.0 makes it a plain async
|
|
849
|
+
* function with a required secret).
|
|
850
|
+
*/
|
|
851
|
+
declare function startCloudConnect(opts: StartCloudConnectOptions): Promise<StartedCloudConnect>;
|
|
852
|
+
interface AcceptCloudReturnOptions {
|
|
853
|
+
/** The URL the CP sent the browser back to (`?iss=…&state=…`). */
|
|
854
|
+
url: string | URL;
|
|
855
|
+
transaction: string | null | undefined;
|
|
856
|
+
clientId: string;
|
|
857
|
+
secret: AppSecret;
|
|
858
|
+
allowInsecureLoopback?: boolean;
|
|
859
|
+
now?: () => number;
|
|
860
|
+
}
|
|
861
|
+
interface CloudReturn {
|
|
862
|
+
/** The pod the person picked (canonical issuer), still to be discovered and validated. */
|
|
863
|
+
pod: string;
|
|
864
|
+
next: string | null;
|
|
865
|
+
spaceHint: string | null;
|
|
866
|
+
prompt: "consent" | null;
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Check the CP's return against the sealed transaction of THIS browser.
|
|
870
|
+
* Throws `expired_transaction` (none, foreign, tampered or older than 10
|
|
871
|
+
* minutes), `mismatch` (the state is not this browser's, or no pod is named),
|
|
872
|
+
* `denied` (the person cancelled on the chooser: `error=access_denied`, no
|
|
873
|
+
* `iss`; nothing is started), `invalid_pod_address`.
|
|
874
|
+
* `state` is compared BEFORE anything else in the query is used.
|
|
875
|
+
*/
|
|
876
|
+
declare function acceptCloudReturn(opts: AcceptCloudReturnOptions): Promise<CloudReturn>;
|
|
877
|
+
|
|
878
|
+
declare function isBrowserRuntime(): boolean;
|
|
879
|
+
|
|
313
880
|
/** How long a started sign-in stays valid. */
|
|
314
881
|
declare const TRANSACTION_TTL_MS: number;
|
|
315
|
-
/** The refresh token's idle life when the pod does not say (
|
|
882
|
+
/** The refresh token's idle life when the pod does not say (the pod's: 30 days). */
|
|
316
883
|
declare const DEFAULT_REFRESH_IDLE_MS: number;
|
|
317
884
|
/** A sealed `ConnectTransaction`: opaque, safe to put in an HttpOnly cookie. */
|
|
318
885
|
type SealedTransaction = string & {
|
|
@@ -351,6 +918,12 @@ interface StartConnectOptions extends FlowOptions {
|
|
|
351
918
|
spaceHint?: string | null;
|
|
352
919
|
/** A same-origin path to land on afterwards (validate it first: `safeNextPath`). */
|
|
353
920
|
next?: string | null;
|
|
921
|
+
/**
|
|
922
|
+
* `"consent"`: the person asked to see the consent screen again (to widen or
|
|
923
|
+
* narrow what the app may touch). Sent as OIDC `prompt=consent`; the sign-in
|
|
924
|
+
* is otherwise the same (fresh PKCE verifier and state).
|
|
925
|
+
*/
|
|
926
|
+
prompt?: "consent" | null;
|
|
354
927
|
allowInsecureLoopback?: boolean;
|
|
355
928
|
}
|
|
356
929
|
interface StartedConnect {
|
|
@@ -384,7 +957,9 @@ interface CallbackResult {
|
|
|
384
957
|
declare function handleCallback(opts: HandleCallbackOptions): Promise<CallbackResult>;
|
|
385
958
|
/**
|
|
386
959
|
* A token response → a connection. On a refresh, `previous` supplies what the
|
|
387
|
-
* response may omit (grant, user).
|
|
960
|
+
* response may omit (grant, user, app; never `actsAs`). `expires_in` is read as sent: a refresh
|
|
961
|
+
* answered within the pod's reuse grace hands back the live key the
|
|
962
|
+
* first redeem got, with less than an hour left. A refresh response WITHOUT a new
|
|
388
963
|
* refresh_token drops the old one: with single-use rotation, resending a
|
|
389
964
|
* consumed token reads as theft and revokes the whole family.
|
|
390
965
|
*/
|
|
@@ -398,7 +973,7 @@ declare function parseTokenResponse(body: Record<string, unknown>, ctx: {
|
|
|
398
973
|
}): PodConnection;
|
|
399
974
|
/**
|
|
400
975
|
* How long a rotation's result is remembered for its SPENT refresh token.
|
|
401
|
-
* The pod
|
|
976
|
+
* The pod refuses a concurrent second redeem softly, but a token
|
|
402
977
|
* presented AGAIN after its use is a reuse: the whole family and the live
|
|
403
978
|
* access key are revoked. A browser easily sends that replay: parallel
|
|
404
979
|
* requests, a second tab, a response it had not stored yet, all still
|
|
@@ -408,7 +983,7 @@ declare function parseTokenResponse(body: Record<string, unknown>, ctx: {
|
|
|
408
983
|
declare const ROTATION_MEMORY_MS: number;
|
|
409
984
|
/**
|
|
410
985
|
* Exchange the refresh token for a new access token and the rotated refresh
|
|
411
|
-
* token (
|
|
986
|
+
* token (single-use, 30 days from issue, its reach re-derived by the pod).
|
|
412
987
|
*
|
|
413
988
|
* SINGLE-FLIGHT per connection within this process: concurrent callers share
|
|
414
989
|
* one request, and for `ROTATION_MEMORY_MS` after it a caller still holding
|
|
@@ -422,7 +997,7 @@ declare const ROTATION_MEMORY_MS: number;
|
|
|
422
997
|
* (invalid_grant while it should live: spent, revoked, the app removed or
|
|
423
998
|
* narrowed to nothing, or a concurrent redeem lost), `misconfigured`
|
|
424
999
|
* (invalid_client, unauthorized_client), `rate_limited`, `pod_unreachable`,
|
|
425
|
-
* `server_error`.
|
|
1000
|
+
* `pod_moved` (the token endpoint answered a redirect), `server_error`.
|
|
426
1001
|
*/
|
|
427
1002
|
declare function refreshConnection(connection: PodConnection, opts?: FlowOptions): Promise<PodConnection>;
|
|
428
1003
|
/**
|
|
@@ -437,6 +1012,4 @@ declare function revokeConnection(connection: PodConnection, opts?: FlowOptions)
|
|
|
437
1012
|
/** RFC 7636 §4.2: `BASE64URL(SHA256(ASCII(code_verifier)))`. */
|
|
438
1013
|
declare function pkceChallenge(verifier: string): Promise<string>;
|
|
439
1014
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
export { AppRequest, type AppSecret, CONNECT_ERROR_CODES, type CallbackResult, ClientMetadataDocument, ConnectError, type ConnectErrorCode, type ConnectTransaction, type ConnectionStore, type CookieNames, DEFAULT_PATHS, DEFAULT_REFRESH_IDLE_MS, type DiscoverOptions, type FlowOptions, type HandleCallbackOptions, type LaunchHint, MIN_SECRET_LENGTH, type Opened, type PodAppAuth, type PodAppAuthConfig, type PodAppPaths, type PodClient, type PodClientOptions, PodConnection, type PodError, type PodErrorCode, PodGrant, PodIssuer, type PodResult, type PodWriteResult, type QueryValue, REFRESH_WINDOW_MS, ROTATION_MEMORY_MS, type RefreshOutcome, type RefreshStatus, type SealPurpose, type SealedCookieStoreOptions, type SealedTransaction, type StartConnectOptions, type StartedConnect, TRANSACTION_TTL_MS, acceptLaunch, applySetCookies, buildClientMetadata, can, canonicalPodUrl, cloudConnectAvailable, connectionExpiresAt, cookieNames, createPodAppAuth, createPodClient, createSealedCookieStore, discoverPod, handleCallback, isBrowserRuntime, isConnectError, openTransaction, parseConnectErrorCode, parseTokenResponse, pkceChallenge, refreshConnection, reviewUrlFor, revokeConnection, safeNextPath, seal, startCloudConnect, startConnect, unseal };
|
|
1015
|
+
export { type AcceptCloudReturnOptions, AppRequest, type AppSecret, type ApproveProposalOptions, 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 GetDocumentOptions, type GetEntityConnectionsOptions, type HandleCallbackOptions, type LaunchHint, type ListEntitiesOptions, type ListFocusSessionsOptions, type ListPendingProposalsOptions, MARKER_TTL_SEC, MIN_SECRET_LENGTH, type Opened, type PendingProposal, type PendingProposals, PodActsAs, type PodAppAuth, type PodAppAuthConfig, type PodAppClientOptions, type PodAppPaths, type PodApproval, type PodClient, type PodClientOptions, PodConnection, type PodConnectionSource, type PodDocument, type PodDocumentSection, type PodEntity, type PodEntityConnection, type PodEntityConnections, type PodError, type PodErrorCode, type PodFileUrl, type PodFocusSession, PodGrant, PodIssuer, type PodRefusalReason, type PodResult, type PodSessionSlot, type PodSessionStatus, PodSpace, type PodWriteResult, type QueryValue, REFRESH_WINDOW_MS, ROTATION_MEMORY_MS, type RefreshOptions, 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, connectionActsAs, connectionExpiresAt, cookieNames, createPodAppAuth, createPodClient, createSealedCookieStore, discoverPod, handleCallback, humanSlots, isBrowserRuntime, isConnectError, isPrefetchOrAsset, markerCookie, openTransaction, parseConnectErrorCode, parseTokenResponse, pkceChallenge, readMarker, refreshConnection, reviewUrlFor, revokeConnection, safeNextPath, seal, spaces, startCloudConnect, startConnect, unseal };
|