@synap-core/sdk 1.4.0 → 1.5.1
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 +215 -0
- package/README.md +152 -10
- package/dist/{chunk-J7XHAAFJ.js → chunk-UK2U6SMA.js} +14 -2
- package/dist/chunk-UK2U6SMA.js.map +1 -0
- package/dist/{client-C5sF20f1.d.cts → client-B50KC2aJ.d.cts} +14 -9
- package/dist/{client-C5sF20f1.d.ts → client-B50KC2aJ.d.ts} +14 -9
- package/dist/connect.cjs +419 -64
- package/dist/connect.cjs.map +1 -1
- package/dist/connect.d.cts +416 -32
- package/dist/connect.d.ts +416 -32
- package/dist/connect.js +414 -65
- 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 +350 -15
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +171 -27
- package/dist/testing.d.ts +171 -27
- package/dist/testing.js +346 -15
- package/dist/testing.js.map +1 -1
- package/dist/{types-C2KGNXEn.d.cts → types-DZYNBCq7.d.cts} +28 -5
- package/dist/{types-C2KGNXEn.d.ts → types-DZYNBCq7.d.ts} +28 -5
- package/package.json +30 -6
- package/src/connect/auth.ts +169 -27
- package/src/connect/client.ts +377 -34
- package/src/connect/cloud.ts +14 -10
- package/src/connect/discovery.ts +66 -32
- package/src/connect/encoding.ts +1 -1
- package/src/connect/errors.ts +7 -0
- package/src/connect/flow.ts +42 -14
- package/src/connect/http.ts +42 -0
- package/src/connect/hub-types.ts +271 -0
- package/src/connect/index.ts +61 -42
- package/src/connect/rules.ts +18 -0
- package/src/connect/store.ts +9 -5
- package/src/connect/types.ts +28 -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 +350 -17
- package/src/testing/cloud.ts +3 -4
- package/src/testing/fixtures.ts +26 -0
- package/src/testing/index.ts +8 -6
- package/src/testing/roles.ts +27 -0
- package/src/testing/seed.ts +196 -0
- package/dist/chunk-J7XHAAFJ.js.map +0 -1
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire shapes of the Hub REST doors `createPodClient`'s typed methods read.
|
|
3
|
+
*
|
|
4
|
+
* DRIFT RISK: hand-written from synap-backend main (2026-10-22), each from the
|
|
5
|
+
* route itself, never from `@synap-core/hub-rest-client` alone:
|
|
6
|
+
* - entities: `routers/hub-protocol/rest/entities.ts` (`GET /entities`,
|
|
7
|
+
* `GET /entities/:id` → `entityToWire`, `_codecs/entity.ts`
|
|
8
|
+
* `WireEntitySchema`; `GET /entities/:id/connections` → `relations.ts`
|
|
9
|
+
* `getConnections`, `services/object-graph/entity-connections.ts`);
|
|
10
|
+
* - documents: `rest/documents.ts` `GET /documents/:id` →
|
|
11
|
+
* `services/document-patch/read-document.ts` `projectAgentDocument`;
|
|
12
|
+
* - focus sessions: `rest/focus-sessions.ts` `GET /focus-sessions` (raw rows +
|
|
13
|
+
* `triage` + `kind`), slots `services/focus-sessions/update-session.ts`
|
|
14
|
+
* `expectedOutputWireSchema`;
|
|
15
|
+
* - files: `rest/entities.ts` `GET /files/:id/url` → `file-upload.ts`
|
|
16
|
+
* `signFileEntityUrl`;
|
|
17
|
+
* - approve: `rest/proposals.ts` `POST /proposals/:id/approve`.
|
|
18
|
+
* Fields the SDK does not read stay on the objects (index signatures): the pod
|
|
19
|
+
* sends more than is typed here. The mock pod (`@synap-core/sdk/testing`)
|
|
20
|
+
* answers these shapes; `src/__tests__/connect.typed.test.ts` drives both.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** One entity, as `GET /entities` and `GET /entities/:id` answer it (`WireEntitySchema`). */
|
|
24
|
+
export interface PodEntity {
|
|
25
|
+
id: string;
|
|
26
|
+
profileSlug: string;
|
|
27
|
+
/** Deprecated alias of `profileSlug`, still sent. */
|
|
28
|
+
type?: string;
|
|
29
|
+
title?: string | null;
|
|
30
|
+
preview?: string | null;
|
|
31
|
+
description?: string | null;
|
|
32
|
+
properties: Record<string, unknown>;
|
|
33
|
+
systemData?: Record<string, unknown>;
|
|
34
|
+
userId: string;
|
|
35
|
+
workspaceId?: string | null;
|
|
36
|
+
documentId?: string | null;
|
|
37
|
+
version?: number;
|
|
38
|
+
/** ISO 8601. */
|
|
39
|
+
createdAt?: string;
|
|
40
|
+
updatedAt?: string;
|
|
41
|
+
/** The roles it wears, with `includeFacets`; absent when it wears none. */
|
|
42
|
+
facetSlugs?: string[];
|
|
43
|
+
/** Each role's overlay, with `includeFacets`; absent when it wears none. */
|
|
44
|
+
facets?: PodEntityFacet[];
|
|
45
|
+
[key: string]: unknown;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** One role an entity wears (`FacetRowAnnotation`). */
|
|
49
|
+
export interface PodEntityFacet {
|
|
50
|
+
facetId: string;
|
|
51
|
+
/** The role, e.g. `lead`. */
|
|
52
|
+
slug: string;
|
|
53
|
+
properties: Record<string, unknown>;
|
|
54
|
+
status: string | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ListEntitiesOptions {
|
|
58
|
+
/** One granted space. Omitted: every space the connection can read. */
|
|
59
|
+
workspaceId?: string;
|
|
60
|
+
/** One kind, e.g. `post`. */
|
|
61
|
+
profileSlug?: string;
|
|
62
|
+
/** 1-1000 (the pod's bounds); the pod's default is 20. */
|
|
63
|
+
limit?: number;
|
|
64
|
+
offset?: number;
|
|
65
|
+
/** e.g. `updatedAt:desc`. */
|
|
66
|
+
sort?: string;
|
|
67
|
+
/** With `workspaceId`: also the person's pod-wide rows (the pod's default: true). */
|
|
68
|
+
includePodWide?: boolean;
|
|
69
|
+
/** Only the entities wearing this role, e.g. `lead`. */
|
|
70
|
+
facetSlug?: string;
|
|
71
|
+
/** Put `facetSlugs` and `facets` on each row that wears a role. */
|
|
72
|
+
includeFacets?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type PodConnectionSource = "graph" | "property" | "thread" | "context_channel" | "focus_session";
|
|
76
|
+
|
|
77
|
+
/** One edge of `GET /entities/:id/connections` (`EntityConnection`). */
|
|
78
|
+
export interface PodEntityConnection {
|
|
79
|
+
/** The other end: an entity, or the channel / session for those sources. */
|
|
80
|
+
entityId: string;
|
|
81
|
+
/** The other entity's row (title, `type` = its kind, workspaceId, facetSlugs, …); null for channels and sessions. */
|
|
82
|
+
entity: {
|
|
83
|
+
title?: string | null;
|
|
84
|
+
type?: string | null;
|
|
85
|
+
workspaceId?: string | null;
|
|
86
|
+
facetSlugs?: string[] | null;
|
|
87
|
+
[key: string]: unknown;
|
|
88
|
+
} | null;
|
|
89
|
+
label: string;
|
|
90
|
+
direction: "outgoing" | "incoming" | "structural";
|
|
91
|
+
source: PodConnectionSource;
|
|
92
|
+
relationId?: string;
|
|
93
|
+
relationType?: string;
|
|
94
|
+
storedAs?: "link";
|
|
95
|
+
propertySlug?: string;
|
|
96
|
+
propertyLabel?: string;
|
|
97
|
+
channelId?: string;
|
|
98
|
+
channelRelationshipType?: string;
|
|
99
|
+
channelTitle?: string | null;
|
|
100
|
+
channelWorkspaceId?: string | null;
|
|
101
|
+
focusSessionId?: string;
|
|
102
|
+
focusSessionGoal?: string;
|
|
103
|
+
focusSessionStatus?: string;
|
|
104
|
+
focusSessionWorkspaceId?: string | null;
|
|
105
|
+
/** ISO 8601, or null (a property link has no date). */
|
|
106
|
+
createdAt?: string | null;
|
|
107
|
+
[key: string]: unknown;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface PodEntityConnections {
|
|
111
|
+
connections: PodEntityConnection[];
|
|
112
|
+
counts: {
|
|
113
|
+
total: number;
|
|
114
|
+
graph: number;
|
|
115
|
+
structural: number;
|
|
116
|
+
threads: number;
|
|
117
|
+
contextChannels: number;
|
|
118
|
+
focusSessions: number;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface GetEntityConnectionsOptions {
|
|
123
|
+
/** A space lens; omitted = everything the connection can read. */
|
|
124
|
+
workspaceId?: string;
|
|
125
|
+
/** Per source, 1-200 (the pod's default: 50). */
|
|
126
|
+
limit?: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A document's top-level section (`listDocumentSections`). */
|
|
130
|
+
export interface PodDocumentSection {
|
|
131
|
+
id: string;
|
|
132
|
+
/** Who owns it (a person's section is not an agent's to change). */
|
|
133
|
+
owner: string;
|
|
134
|
+
title: string | null;
|
|
135
|
+
author: string | null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** `GET /documents/:id` → `document` (`projectAgentDocument`). */
|
|
139
|
+
export interface PodDocument {
|
|
140
|
+
id: string;
|
|
141
|
+
title: string;
|
|
142
|
+
/** `markdown`, `text`, `html`, … */
|
|
143
|
+
type: string;
|
|
144
|
+
language: string | null;
|
|
145
|
+
workspaceId: string | null;
|
|
146
|
+
format: "raw" | "readable";
|
|
147
|
+
/** Markdown; `readable` replaces embeds by their fallback text. */
|
|
148
|
+
content: string;
|
|
149
|
+
/** Pass back as `baseRevision` on a guarded edit. */
|
|
150
|
+
revision: number;
|
|
151
|
+
version: number;
|
|
152
|
+
sections: PodDocumentSection[];
|
|
153
|
+
/** What will not render; null when the check could not run (`diagnosticsError`). */
|
|
154
|
+
diagnostics: unknown[] | null;
|
|
155
|
+
diagnosticsError?: string;
|
|
156
|
+
updatedAt: string;
|
|
157
|
+
createdAt: string;
|
|
158
|
+
[key: string]: unknown;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface GetDocumentOptions {
|
|
162
|
+
/** `raw` (the stored markdown, the pod's default) or `readable`. */
|
|
163
|
+
format?: "raw" | "readable";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** `focus_sessions.status` (`@synap-core/types/focus-sessions` SESSION_STATUSES). */
|
|
167
|
+
export type PodSessionStatus = "active" | "paused" | "forming" | "scheduled" | "closed" | "failed" | "cancelled" | "stale";
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* One declared deliverable of a session (`expectedOutputWireSchema`). A slot
|
|
171
|
+
* with `owner: "human"` is waiting on the person: `blockedReason` and `why` say
|
|
172
|
+
* for what, `ask` how they can answer (confirm, choose, form, act, provide).
|
|
173
|
+
*/
|
|
174
|
+
export interface PodSessionSlot {
|
|
175
|
+
kind: string;
|
|
176
|
+
label: string;
|
|
177
|
+
key?: string;
|
|
178
|
+
optional?: boolean;
|
|
179
|
+
status?: "pending" | "done";
|
|
180
|
+
/** Absent = `agent`. */
|
|
181
|
+
owner?: "human" | "agent";
|
|
182
|
+
blockedReason?: string;
|
|
183
|
+
why?: string;
|
|
184
|
+
/** ISO 8601, stamped by the pod with `owner: "human"`. */
|
|
185
|
+
owedSince?: string;
|
|
186
|
+
/** How the person can answer (`@synap-core/types/ask`), or null. */
|
|
187
|
+
ask?: unknown;
|
|
188
|
+
answer?: {
|
|
189
|
+
text: string;
|
|
190
|
+
messageId: string | null;
|
|
191
|
+
answeredBy: string;
|
|
192
|
+
answeredAt: string;
|
|
193
|
+
question?: string;
|
|
194
|
+
value?: unknown;
|
|
195
|
+
[key: string]: unknown;
|
|
196
|
+
};
|
|
197
|
+
retiredAt?: string;
|
|
198
|
+
[key: string]: unknown;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** One row of `GET /focus-sessions`. `expectedOutputs` is always an array here (the SDK drops anything else). */
|
|
202
|
+
export interface PodFocusSession {
|
|
203
|
+
id: string;
|
|
204
|
+
workspaceId: string | null;
|
|
205
|
+
projectId: string | null;
|
|
206
|
+
userId: string;
|
|
207
|
+
title: string | null;
|
|
208
|
+
goal: string;
|
|
209
|
+
status: PodSessionStatus | (string & {});
|
|
210
|
+
templateId: string | null;
|
|
211
|
+
expectedOutputs: PodSessionSlot[];
|
|
212
|
+
channelId: string | null;
|
|
213
|
+
progress: number | null;
|
|
214
|
+
currentStage: string | null;
|
|
215
|
+
agentIds: string[];
|
|
216
|
+
closedAt: string | null;
|
|
217
|
+
metadata: unknown;
|
|
218
|
+
startedAt: string;
|
|
219
|
+
createdAt: string;
|
|
220
|
+
updatedAt: string;
|
|
221
|
+
/** Projected by the list door: `work`, `run` (a playbook or automation ran it) or `receipt`. */
|
|
222
|
+
kind?: "work" | "run" | "receipt";
|
|
223
|
+
/** Projected by the list door: an agent-started session the person has not accepted yet. */
|
|
224
|
+
triage?: { pending: boolean; acceptedAt: string | null; acceptedBy: string | null };
|
|
225
|
+
[key: string]: unknown;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export interface ListFocusSessionsOptions {
|
|
229
|
+
/** A space; `workspaceId` or `projectId` is required (the pod's rule). */
|
|
230
|
+
workspaceId?: string;
|
|
231
|
+
projectId?: string;
|
|
232
|
+
/** Default `all` (the pod's default on this door). */
|
|
233
|
+
status?: PodSessionStatus | "all";
|
|
234
|
+
/** 1-50 (the pod's bounds); the pod's default is 20. */
|
|
235
|
+
limit?: number;
|
|
236
|
+
/** Triage lens: `all` (default here), `default` (hides unaccepted agent sessions), `triage`. */
|
|
237
|
+
lens?: "all" | "default" | "triage";
|
|
238
|
+
/** Population lens: `all` (default here), `work`, `run`, `receipt`. */
|
|
239
|
+
kind?: "all" | "work" | "run" | "receipt";
|
|
240
|
+
/** Sessions about one thing. */
|
|
241
|
+
subjectEntityId?: string;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** `GET /files/:id/url`: a signed GET for a file entity's blob. */
|
|
245
|
+
export interface PodFileUrl {
|
|
246
|
+
/** Served as an `attachment`, with the stored type. */
|
|
247
|
+
url: string;
|
|
248
|
+
/** ISO 8601; the signature's end (about an hour). */
|
|
249
|
+
expiresAt: string;
|
|
250
|
+
/** Decide drawability on this, never on the URL; null when unknown. */
|
|
251
|
+
mimeType: string | null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** `POST /proposals/:id/approve`. */
|
|
255
|
+
export interface PodApproval {
|
|
256
|
+
proposalId: string;
|
|
257
|
+
/** The row the approval produced or linked, when it names one. */
|
|
258
|
+
primaryId?: string;
|
|
259
|
+
linked?: number;
|
|
260
|
+
/** The proposal after the approval (status, execution result), when the pod could read it back. */
|
|
261
|
+
proposal: Record<string, unknown> | null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export interface ApproveProposalOptions {
|
|
265
|
+
/**
|
|
266
|
+
* The proposal's space, when known: refused locally unless the connection
|
|
267
|
+
* holds `proposal.approve` there. Omitted: needs it in some space, and the
|
|
268
|
+
* pod checks the proposal's own space.
|
|
269
|
+
*/
|
|
270
|
+
workspaceId?: string;
|
|
271
|
+
}
|
package/src/connect/index.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@synap-core/sdk/connect` — pod-app sign-in, SERVER-ONLY
|
|
3
|
-
* doc 18 § SDK): an app connects to a person's own pod as an OAuth 2.1 public
|
|
2
|
+
* `@synap-core/sdk/connect` — pod-app sign-in, SERVER-ONLY: an app connects to a person's own pod as an OAuth 2.1 public
|
|
4
3
|
* client (CIMD client_id, PKCE S256, `iss`-checked answers), keeps the pod
|
|
5
4
|
* token on its server in a sealed HttpOnly cookie, refreshes it in `proxy.ts`,
|
|
6
5
|
* and reads/writes the pod's Hub API with typed results.
|
|
7
6
|
*
|
|
8
|
-
* Framework-free and Web-standard (Request/Response, WebCrypto): Node
|
|
7
|
+
* Framework-free and Web-standard (Request/Response, WebCrypto): Node 20+,
|
|
9
8
|
* edge runtimes, workers. Throws when loaded in a browser (./guard.ts).
|
|
10
9
|
*
|
|
11
10
|
* Invariant (pinned by src/__tests__/connect.guard.test.ts): every file of
|
|
@@ -15,20 +14,65 @@ import { assertServerRuntime } from "./guard";
|
|
|
15
14
|
|
|
16
15
|
assertServerRuntime();
|
|
17
16
|
|
|
17
|
+
// What an app on createPodAppAuth uses.
|
|
18
18
|
export { createPodAppAuth, buildClientMetadata, isPrefetchOrAsset, DEFAULT_PATHS, REFRESH_WINDOW_MS } from "./auth";
|
|
19
|
-
export type { PodAppAuth, PodAppAuthConfig, PodAppPaths, RefreshOutcome, RefreshStatus } from "./auth";
|
|
20
|
-
export {
|
|
21
|
-
export type {
|
|
22
|
-
export {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
export type { PodAppAuth, PodAppAuthConfig, PodAppClientOptions, PodAppPaths, RefreshOptions, RefreshOutcome, RefreshStatus } from "./auth";
|
|
20
|
+
export { acceptLaunch, safeNextPath } from "./discovery";
|
|
21
|
+
export type { LaunchHint } from "./discovery";
|
|
22
|
+
export { cloudConnectAvailable } from "./cloud";
|
|
23
|
+
export type { CloudConnectConfig } from "./cloud";
|
|
24
|
+
export { createPodClient, can, spaces, reviewUrlFor, connectionActsAs, humanSlots } from "./client";
|
|
25
|
+
export type {
|
|
26
|
+
ListPendingProposalsOptions,
|
|
27
|
+
PendingProposal,
|
|
28
|
+
PendingProposals,
|
|
29
|
+
PodClient,
|
|
30
|
+
PodClientOptions,
|
|
31
|
+
PodError,
|
|
32
|
+
PodErrorCode,
|
|
33
|
+
PodRefusalReason,
|
|
34
|
+
PodResult,
|
|
35
|
+
PodWriteResult,
|
|
36
|
+
QueryValue,
|
|
37
|
+
} from "./client";
|
|
38
|
+
export type {
|
|
39
|
+
ApproveProposalOptions,
|
|
40
|
+
GetDocumentOptions,
|
|
41
|
+
GetEntityConnectionsOptions,
|
|
42
|
+
ListEntitiesOptions,
|
|
43
|
+
ListFocusSessionsOptions,
|
|
44
|
+
PodApproval,
|
|
45
|
+
PodConnectionSource,
|
|
46
|
+
PodDocument,
|
|
47
|
+
PodDocumentSection,
|
|
48
|
+
PodEntity,
|
|
49
|
+
PodEntityConnection,
|
|
50
|
+
PodEntityConnections,
|
|
51
|
+
PodEntityFacet,
|
|
52
|
+
PodFileUrl,
|
|
53
|
+
PodFocusSession,
|
|
54
|
+
PodSessionSlot,
|
|
55
|
+
PodSessionStatus,
|
|
56
|
+
} from "./hub-types";
|
|
57
|
+
export { ConnectError, isConnectError, parseConnectErrorCode, CONNECT_ERROR_CODES } from "./errors";
|
|
58
|
+
export type { ConnectErrorCode } from "./errors";
|
|
59
|
+
export { isBrowserRuntime } from "./guard";
|
|
60
|
+
export type { AppRequest, ClientMetadataDocument, PodActsAs, PodConnection, PodGrant, PodIssuer, PodSpace, PodUser } from "./types";
|
|
61
|
+
export type { ConnectionStore } from "./store";
|
|
62
|
+
export { createSealedCookieStore } from "./store";
|
|
63
|
+
export type { SealedCookieStoreOptions } from "./store";
|
|
64
|
+
export type { AppSecret } from "./seal";
|
|
65
|
+
|
|
66
|
+
// Building blocks: what createPodAppAuth is made of, for another framework or
|
|
67
|
+
// store. INTERNAL, not the supported surface: an app on createPodAppAuth needs
|
|
68
|
+
// none of them, and no new export is added here. They stay exported only for
|
|
69
|
+
// 1.x semver; 2.0 moves them to their own entry. (ConnectionStore and
|
|
70
|
+
// createSealedCookieStore above are the supported custom-store seam.)
|
|
71
|
+
export { discoverPod, canonicalPodUrl } from "./discovery";
|
|
72
|
+
export type { DiscoverOptions } from "./discovery";
|
|
73
|
+
export { startCloudConnect, acceptCloudReturn, cloudBaseUrl, CLOUD_TRANSACTION_TTL_MS } from "./cloud";
|
|
29
74
|
export type {
|
|
30
75
|
AcceptCloudReturnOptions,
|
|
31
|
-
CloudConnectConfig,
|
|
32
76
|
CloudReturn,
|
|
33
77
|
CloudTransaction,
|
|
34
78
|
SealedCloudTransaction,
|
|
@@ -55,33 +99,8 @@ export type {
|
|
|
55
99
|
StartConnectOptions,
|
|
56
100
|
StartedConnect,
|
|
57
101
|
} from "./flow";
|
|
58
|
-
export {
|
|
59
|
-
export type {
|
|
60
|
-
ListPendingProposalsOptions,
|
|
61
|
-
PendingProposal,
|
|
62
|
-
PendingProposals,
|
|
63
|
-
PodClient,
|
|
64
|
-
PodClientOptions,
|
|
65
|
-
PodError,
|
|
66
|
-
PodErrorCode,
|
|
67
|
-
PodResult,
|
|
68
|
-
PodWriteResult,
|
|
69
|
-
QueryValue,
|
|
70
|
-
} from "./client";
|
|
71
|
-
export {
|
|
72
|
-
createSealedCookieStore,
|
|
73
|
-
cookieNames,
|
|
74
|
-
applySetCookies,
|
|
75
|
-
connectionExpiresAt,
|
|
76
|
-
markerCookie,
|
|
77
|
-
readMarker,
|
|
78
|
-
MARKER_TTL_SEC,
|
|
79
|
-
} from "./store";
|
|
80
|
-
export type { ConnectionStore, CookieNames, SealedCookieStoreOptions } from "./store";
|
|
102
|
+
export { cookieNames, applySetCookies, connectionExpiresAt, markerCookie, readMarker, MARKER_TTL_SEC } from "./store";
|
|
103
|
+
export type { CookieNames } from "./store";
|
|
81
104
|
export { seal, unseal, MIN_SECRET_LENGTH } from "./seal";
|
|
82
|
-
export type {
|
|
105
|
+
export type { Opened, SealPurpose } from "./seal";
|
|
83
106
|
export { pkceChallenge } from "./encoding";
|
|
84
|
-
export { ConnectError, isConnectError, parseConnectErrorCode, CONNECT_ERROR_CODES } from "./errors";
|
|
85
|
-
export type { ConnectErrorCode } from "./errors";
|
|
86
|
-
export { isBrowserRuntime } from "./guard";
|
|
87
|
-
export type { AppRequest, ClientMetadataDocument, PodConnection, PodGrant, PodIssuer, PodSpace, PodUser } from "./types";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape rules several files of this entry (and the mock pod) apply, in
|
|
3
|
+
* one place, so a hint or a state accepted in one door is accepted in all.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Hosts that count as this machine (WHATWG keeps IPv6 brackets: "[::1]"). */
|
|
7
|
+
export const LOOPBACK_HOSTS: ReadonlySet<string> = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
8
|
+
|
|
9
|
+
/** A `synap_space_hint`: a space id (uuid, short id), never a path or a query. */
|
|
10
|
+
export const SPACE_HINT_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
11
|
+
|
|
12
|
+
/** The `state` the Synap Cloud pod chooser accepts and echoes; ours is 43 base64url characters. */
|
|
13
|
+
export const CLOUD_STATE_RE = /^[A-Za-z0-9._~-]{16,256}$/;
|
|
14
|
+
|
|
15
|
+
/** An IP literal host (dotted IPv4 or bracketed IPv6), as WHATWG URL normalises them. */
|
|
16
|
+
export function isIpLiteral(hostname: string): boolean {
|
|
17
|
+
return hostname.startsWith("[") || /^\d{1,3}(\.\d{1,3}){3}$/.test(hostname);
|
|
18
|
+
}
|
package/src/connect/store.ts
CHANGED
|
@@ -79,16 +79,20 @@ function chunkName(name: string, i: number): string {
|
|
|
79
79
|
return i === 0 ? name : `${name}.${i}`;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/** One space of the sealed grant; the template slug only when the pod named one (cookies written by 1.4 have none). */
|
|
83
|
+
type StoredSpace = [workspaceId: string, workspaceName: string, permissions: string[], templateSlug?: string];
|
|
84
|
+
|
|
82
85
|
/** The sealed form: the grant grouped per space (it repeats the space otherwise). */
|
|
83
86
|
interface StoredConnection extends Omit<PodConnection, "grant"> {
|
|
84
|
-
g:
|
|
87
|
+
g: StoredSpace[];
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
function pack(c: PodConnection): StoredConnection {
|
|
88
|
-
const bySpace = new Map<string,
|
|
91
|
+
const bySpace = new Map<string, StoredSpace>();
|
|
89
92
|
for (const g of c.grant) {
|
|
90
|
-
const row = bySpace.get(g.workspaceId) ?? [g.workspaceId, g.workspaceName, []];
|
|
93
|
+
const row: StoredSpace = bySpace.get(g.workspaceId) ?? [g.workspaceId, g.workspaceName, []];
|
|
91
94
|
row[2].push(g.permission);
|
|
95
|
+
if (g.templateSlug && row[3] === undefined) row[3] = g.templateSlug;
|
|
92
96
|
bySpace.set(g.workspaceId, row);
|
|
93
97
|
}
|
|
94
98
|
const { grant: _grant, ...rest } = c;
|
|
@@ -98,8 +102,8 @@ function pack(c: PodConnection): StoredConnection {
|
|
|
98
102
|
function unpack(s: StoredConnection): PodConnection | null {
|
|
99
103
|
if (!s || typeof s.podUrl !== "string" || typeof s.accessToken !== "string" || !Array.isArray(s.g)) return null;
|
|
100
104
|
const { g, ...rest } = s;
|
|
101
|
-
const grant: PodGrant[] = g.flatMap(([workspaceId, workspaceName, permissions]) =>
|
|
102
|
-
permissions.map((permission) => ({ permission, workspaceId, workspaceName })),
|
|
105
|
+
const grant: PodGrant[] = g.flatMap(([workspaceId, workspaceName, permissions, templateSlug]) =>
|
|
106
|
+
permissions.map((permission) => ({ permission, workspaceId, workspaceName, ...(typeof templateSlug === "string" ? { templateSlug } : {}) })),
|
|
103
107
|
);
|
|
104
108
|
return { ...rest, grant };
|
|
105
109
|
}
|
package/src/connect/types.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* DRIFT RISK: hand-written from the pod's OAuth AS (synap-backend
|
|
5
5
|
* `routers/oauth/routes.ts` /token for a CIMD client, `protocol.ts`
|
|
6
|
-
* `buildAuthorizationServerMetadata
|
|
7
|
-
* `src/__tests__/connect.contract.test.ts` copies the token
|
|
8
|
-
* backend changes, update both.
|
|
6
|
+
* `buildAuthorizationServerMetadata`). The contract test
|
|
7
|
+
* `src/__tests__/connect.contract.test.ts` holds copies of the pod's token and
|
|
8
|
+
* metadata answers; when the backend changes them, update both.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
/** One permission the app asks for, with the sentence the consent screen shows. */
|
|
@@ -21,8 +21,25 @@ export interface PodGrant {
|
|
|
21
21
|
permission: string;
|
|
22
22
|
workspaceId: string;
|
|
23
23
|
workspaceName: string;
|
|
24
|
+
/**
|
|
25
|
+
* The template the space was made from (`synap_grant[].templateSlug`, e.g.
|
|
26
|
+
* `content-os`), when the pod names one: absent for a space made by hand
|
|
27
|
+
* and from pods that do not send it.
|
|
28
|
+
*/
|
|
29
|
+
templateSlug?: string;
|
|
24
30
|
}
|
|
25
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Who the connection's token acts as (`synap_acts_as` of the token response):
|
|
34
|
+
* - `"person"`: an official Synap app; the token is held by the person who
|
|
35
|
+
* consented, so their clicks write as them, directly, approvals included
|
|
36
|
+
* (where they granted `proposal.approve`);
|
|
37
|
+
* - `"agent"`: every other app; the token is the app's own ask-first agent's,
|
|
38
|
+
* its writes become proposals the person reviews, and it can never approve.
|
|
39
|
+
* A pod that does not say is `"agent"`.
|
|
40
|
+
*/
|
|
41
|
+
export type PodActsAs = "person" | "agent";
|
|
42
|
+
|
|
26
43
|
export interface PodUser {
|
|
27
44
|
id: string;
|
|
28
45
|
name: string;
|
|
@@ -75,6 +92,11 @@ export interface PodConnection {
|
|
|
75
92
|
* never guess one.
|
|
76
93
|
*/
|
|
77
94
|
manageUrl?: string;
|
|
95
|
+
/**
|
|
96
|
+
* Who the token acts as, as the latest token response said
|
|
97
|
+
* (`synap_acts_as`). Absent = `"agent"` (read it with `connectionActsAs`).
|
|
98
|
+
*/
|
|
99
|
+
actsAs?: PodActsAs;
|
|
78
100
|
}
|
|
79
101
|
|
|
80
102
|
/** One space of the grant, its permissions together (`spaces(connection)`). */
|
|
@@ -83,9 +105,11 @@ export interface PodSpace {
|
|
|
83
105
|
/** "" when the pod sent no name. */
|
|
84
106
|
workspaceName: string;
|
|
85
107
|
permissions: string[];
|
|
108
|
+
/** The template the space was made from, when the pod names one (`PodGrant.templateSlug`). */
|
|
109
|
+
templateSlug?: string;
|
|
86
110
|
}
|
|
87
111
|
|
|
88
|
-
/** The
|
|
112
|
+
/** The client metadata document (CIMD) an app serves at its client_id URL. */
|
|
89
113
|
export interface ClientMetadataDocument {
|
|
90
114
|
client_id: string;
|
|
91
115
|
client_name: string;
|
package/src/public/client.ts
CHANGED
|
@@ -34,12 +34,24 @@
|
|
|
34
34
|
|
|
35
35
|
// ── Wire types (hand-written; see DRIFT RISK) ────────────────────────────────
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* A published body, in the format the pod serves it (backend
|
|
39
|
+
* `PublicBodyFormatSchema`): `markdown` is text to parse with
|
|
40
|
+
* `parseSafeMarkdown`; `html` is a whole HTML document (an estimate, a report)
|
|
41
|
+
* that a page renders ONLY inside `<iframe sandbox="" srcdoc>`, never in its
|
|
42
|
+
* own DOM. `null`: nothing pinned, or a body that is not text (pdf, docx).
|
|
43
|
+
*/
|
|
44
|
+
export interface PublicBody {
|
|
45
|
+
format: "markdown" | "html";
|
|
46
|
+
content: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
37
49
|
export interface PublicShare {
|
|
38
50
|
resourceType: "entity";
|
|
39
51
|
/** Present only when the publisher allowlisted `title`. */
|
|
40
52
|
title?: string;
|
|
41
53
|
properties: Record<string, string | number | boolean>;
|
|
42
|
-
body:
|
|
54
|
+
body: PublicBody | null;
|
|
43
55
|
/** Publication day, YYYY-MM-DD. */
|
|
44
56
|
publishedOn: string;
|
|
45
57
|
}
|
|
@@ -136,7 +148,7 @@ export interface PublicCollectionPage {
|
|
|
136
148
|
|
|
137
149
|
/** One published record, with its pinned body. */
|
|
138
150
|
export interface PublicRecord extends PublicRecordSummary {
|
|
139
|
-
body:
|
|
151
|
+
body: PublicBody | null;
|
|
140
152
|
}
|
|
141
153
|
|
|
142
154
|
/** The collection door's query bounds (backend `parseCollectionQuery`). */
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* Everything else is plain text. Extend here — never by rendering HTML.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import type { PublicBody } from "./client";
|
|
16
|
+
|
|
15
17
|
export type Inline =
|
|
16
18
|
| { type: "text"; text: string }
|
|
17
19
|
| { type: "strong"; text: string }
|
|
@@ -112,3 +114,14 @@ export function parseSafeMarkdown(source: string): Block[] {
|
|
|
112
114
|
flush();
|
|
113
115
|
return blocks;
|
|
114
116
|
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* A published body as a page renders it: markdown -> safe blocks; html -> the
|
|
120
|
+
* document for a sandboxed frame, never parsed as markdown (its tags would show
|
|
121
|
+
* as text). One rule for the share page and the site record page.
|
|
122
|
+
*/
|
|
123
|
+
export function bodyOf(body: PublicBody | null | undefined): { blocks: Block[]; html: string | null } {
|
|
124
|
+
if (!body) return { blocks: [], html: null };
|
|
125
|
+
if (body.format === "html") return { blocks: [], html: body.content };
|
|
126
|
+
return { blocks: parseSafeMarkdown(body.content), html: null };
|
|
127
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* is honest (a network blip) — it never says anything about the token.
|
|
9
9
|
*/
|
|
10
10
|
import type { PublicReadError, PublicResult, PublicShare } from "./client";
|
|
11
|
-
import {
|
|
11
|
+
import { bodyOf, type Block } from "./safe-markdown";
|
|
12
12
|
import { formatPublicValue, type FormatOptions } from "./site-model";
|
|
13
13
|
|
|
14
14
|
export const UNAVAILABLE_TITLE = "This link isn't available";
|
|
@@ -20,7 +20,10 @@ export type SharePageModel =
|
|
|
20
20
|
title: string | null;
|
|
21
21
|
/** Property rows exactly as the publisher allowlisted them. */
|
|
22
22
|
properties: Array<{ key: string; value: string }>;
|
|
23
|
+
/** A markdown body as safe blocks (`[]` for an html body or none). */
|
|
23
24
|
blocks: Block[];
|
|
25
|
+
/** An html body: render ONLY as `<iframe sandbox="" srcdoc>`. `null` otherwise. */
|
|
26
|
+
html: string | null;
|
|
24
27
|
publishedOn: string;
|
|
25
28
|
};
|
|
26
29
|
|
|
@@ -41,7 +44,8 @@ export function toSharePageModel(
|
|
|
41
44
|
key,
|
|
42
45
|
value: formatPublicValue(value, opts),
|
|
43
46
|
})),
|
|
44
|
-
|
|
47
|
+
...bodyOf(share.body),
|
|
45
48
|
publishedOn: share.publishedOn,
|
|
46
49
|
};
|
|
47
50
|
}
|
|
51
|
+
|
package/src/public/site-model.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
type PublicRecordSummary,
|
|
15
15
|
type PublicValue,
|
|
16
16
|
} from "./client";
|
|
17
|
-
import {
|
|
17
|
+
import { bodyOf, type Block } from "./safe-markdown";
|
|
18
18
|
|
|
19
19
|
export interface FormatOptions {
|
|
20
20
|
/** BCP 47 locale (default `en`). */
|
|
@@ -197,8 +197,10 @@ export interface RecordViewModel {
|
|
|
197
197
|
slug: string | null;
|
|
198
198
|
/** Rows exactly as the publisher allowlisted them, formatted. */
|
|
199
199
|
properties: Array<{ key: string; value: string }>;
|
|
200
|
-
/**
|
|
200
|
+
/** A markdown body as safe blocks (`[]` for an html body or none). */
|
|
201
201
|
blocks: Block[];
|
|
202
|
+
/** An html body: render ONLY as `<iframe sandbox="" srcdoc>`. `null` otherwise. */
|
|
203
|
+
html: string | null;
|
|
202
204
|
publishedOn: string;
|
|
203
205
|
}
|
|
204
206
|
|
|
@@ -211,7 +213,7 @@ export function toRecordViewModel(record: PublicRecord, opts: FormatOptions = {}
|
|
|
211
213
|
key,
|
|
212
214
|
value: formatPublicValue(value, opts),
|
|
213
215
|
})),
|
|
214
|
-
|
|
216
|
+
...bodyOf(record.body),
|
|
215
217
|
publishedOn: record.publishedOn,
|
|
216
218
|
};
|
|
217
219
|
}
|