@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
package/dist/testing.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { P as PublicForm,
|
|
2
|
-
import { P as PodGrant, a as PodUser, C as ClientMetadataDocument } from './types-
|
|
1
|
+
import { P as PublicForm, h as PublicShare, f as PublicRecord } from './client-B50KC2aJ.cjs';
|
|
2
|
+
import { P as PodGrant, a as PodUser, b as PodActsAs, C as ClientMetadataDocument } from './types-DZYNBCq7.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Demo data for the mock pod. Shapes are the `@synap-core/sdk/public` wire
|
|
@@ -8,6 +8,12 @@ import { P as PodGrant, a as PodUser, C as ClientMetadataDocument } from './type
|
|
|
8
8
|
|
|
9
9
|
declare const MOCK_FORM: PublicForm;
|
|
10
10
|
declare const MOCK_SHARE: PublicShare;
|
|
11
|
+
/**
|
|
12
|
+
* A published HTML document (an estimate), as the pod serves an `html` body: a
|
|
13
|
+
* page renders it only inside `<iframe sandbox="" srcdoc>`, so the script below
|
|
14
|
+
* never runs.
|
|
15
|
+
*/
|
|
16
|
+
declare const MOCK_HTML_SHARE: PublicShare;
|
|
11
17
|
/** One collection of a mock site: its door config and its live records. */
|
|
12
18
|
interface MockCollection {
|
|
13
19
|
/** Default sort keys (`-` = descending); also the sort keys the door accepts. */
|
|
@@ -29,6 +35,139 @@ interface MockSite {
|
|
|
29
35
|
*/
|
|
30
36
|
declare const MOCK_SITE: MockSite;
|
|
31
37
|
|
|
38
|
+
/**
|
|
39
|
+
* The mock pod's data as ONE JSON-serialisable shape, so an app's mock mode,
|
|
40
|
+
* smoke and screenshots replay a recorded snapshot (one file) instead of
|
|
41
|
+
* fixtures built in app code:
|
|
42
|
+
*
|
|
43
|
+
* import { readFileSync } from "node:fs";
|
|
44
|
+
* const seed = parseMockPodSeed(readFileSync("mock/pod-seed.json", "utf8"));
|
|
45
|
+
* const pod = createMockAuthServer({ seed });
|
|
46
|
+
*
|
|
47
|
+
* Rows are the pod's wire shapes (see `@synap-core/sdk/connect` hub-types):
|
|
48
|
+
* `entities` as `GET /entities` answers them, `sessions` as `GET
|
|
49
|
+
* /focus-sessions` rows (missing columns get the pod's defaults), `documents`
|
|
50
|
+
* as `GET /documents/:id` (`sections` and `diagnostics` answered empty),
|
|
51
|
+
* `relations` as graph edges for `GET /entities/:id/connections`, `proposals`
|
|
52
|
+
* as `GET /proposals?view=basic` rows, `spaces` name each granted space's
|
|
53
|
+
* template (`synap_grant[].templateSlug` on the token response).
|
|
54
|
+
*
|
|
55
|
+
* An entity's roles ride on its row as `facets`, the shape `GET /entities?
|
|
56
|
+
* includeFacets=true` answers them (`{ facetId, slug, properties, status }`),
|
|
57
|
+
* plus the seed-only space each is stamped in, so a seed row reads like a pod
|
|
58
|
+
* row read with includeFacets. The mock derives `facetSlugs` and the reach rule
|
|
59
|
+
* from them and serves neither field unless asked, as the pod.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/** One role an entity wears (backend `FacetRowAnnotation`, plus the space it is stamped in). */
|
|
63
|
+
interface MockSeedFacet {
|
|
64
|
+
facetId: string;
|
|
65
|
+
/** The role's slug, e.g. `lead`. */
|
|
66
|
+
slug: string;
|
|
67
|
+
/** The facet's overlay properties. Default `{}`. */
|
|
68
|
+
properties?: Record<string, unknown>;
|
|
69
|
+
/** Its lifecycle status. Default null. */
|
|
70
|
+
status?: string | null;
|
|
71
|
+
/**
|
|
72
|
+
* Seed only, never served: the space the role is in, i.e. the space whose
|
|
73
|
+
* members see the facet (stamped there, or a pod-wide facet of a role granted
|
|
74
|
+
* to it). A role in a granted space puts a pod-wide entity in the app's
|
|
75
|
+
* reach; a workspace lens sees the roles of that space only.
|
|
76
|
+
*/
|
|
77
|
+
workspaceId: string;
|
|
78
|
+
/**
|
|
79
|
+
* Seed only, never served: the role is SHARED, i.e. stored pod-wide (null
|
|
80
|
+
* `workspaceId`) and granted to `workspaceId`, which then means "the space it
|
|
81
|
+
* is granted to". Default false = stamped in `workspaceId`. The pod treats the
|
|
82
|
+
* two alike for reach, except `includePodWide=false` on a space lens, which
|
|
83
|
+
* drops a pod-wide row reached only through a shared role.
|
|
84
|
+
*/
|
|
85
|
+
sharedRole?: boolean;
|
|
86
|
+
}
|
|
87
|
+
interface MockSeedEntity {
|
|
88
|
+
id: string;
|
|
89
|
+
profileSlug?: string;
|
|
90
|
+
workspaceId?: string | null;
|
|
91
|
+
title?: string | null;
|
|
92
|
+
properties?: Record<string, unknown>;
|
|
93
|
+
/**
|
|
94
|
+
* The roles (facets) it wears, in the wire shape plus the space each is in
|
|
95
|
+
* (`MockSeedFacet`). SEED INPUT ONLY: the pod sends them only to
|
|
96
|
+
* `includeFacets`, and so does the mock. A pod-wide entity (`workspaceId:
|
|
97
|
+
* null`) with a role in a granted space is readable by the app, as a member
|
|
98
|
+
* of that space sees it. Absent = none.
|
|
99
|
+
*/
|
|
100
|
+
facets?: MockSeedFacet[];
|
|
101
|
+
/**
|
|
102
|
+
* @deprecated Use `facets`; removed in 2.0. Shorthand for `facets` entries
|
|
103
|
+
* with no id and no overlay: `parseMockPodSeed` turns each into a facet (a
|
|
104
|
+
* generated `facetId`, empty properties, a null status). A role stated in
|
|
105
|
+
* both is refused.
|
|
106
|
+
*/
|
|
107
|
+
seedRoles?: Array<{
|
|
108
|
+
profileSlug: string;
|
|
109
|
+
workspaceId: string;
|
|
110
|
+
}>;
|
|
111
|
+
[key: string]: unknown;
|
|
112
|
+
}
|
|
113
|
+
interface MockSeedSession {
|
|
114
|
+
id: string;
|
|
115
|
+
workspaceId?: string | null;
|
|
116
|
+
projectId?: string | null;
|
|
117
|
+
goal?: string;
|
|
118
|
+
status?: string;
|
|
119
|
+
/** Slots; `owner: "human"` ones are what waits on the person. */
|
|
120
|
+
expectedOutputs?: Array<Record<string, unknown>>;
|
|
121
|
+
/** The entity it is about: listed among that entity's connections. */
|
|
122
|
+
subjectEntityId?: string | null;
|
|
123
|
+
/** ISO 8601; the list is newest first by it. */
|
|
124
|
+
startedAt?: string;
|
|
125
|
+
[key: string]: unknown;
|
|
126
|
+
}
|
|
127
|
+
interface MockSeedDocument {
|
|
128
|
+
id: string;
|
|
129
|
+
title: string;
|
|
130
|
+
content: string;
|
|
131
|
+
type?: string;
|
|
132
|
+
language?: string | null;
|
|
133
|
+
workspaceId?: string | null;
|
|
134
|
+
revision?: number;
|
|
135
|
+
version?: number;
|
|
136
|
+
createdAt?: string;
|
|
137
|
+
updatedAt?: string;
|
|
138
|
+
}
|
|
139
|
+
interface MockSeedRelation {
|
|
140
|
+
id: string;
|
|
141
|
+
sourceEntityId: string;
|
|
142
|
+
targetEntityId: string;
|
|
143
|
+
type: string;
|
|
144
|
+
createdAt?: string;
|
|
145
|
+
}
|
|
146
|
+
interface MockSeedSpace {
|
|
147
|
+
id: string;
|
|
148
|
+
name: string;
|
|
149
|
+
/** The template it came from (e.g. `content-os`); absent = made by hand. */
|
|
150
|
+
templateSlug?: string;
|
|
151
|
+
}
|
|
152
|
+
interface MockPodSeed {
|
|
153
|
+
entities?: MockSeedEntity[];
|
|
154
|
+
sessions?: MockSeedSession[];
|
|
155
|
+
documents?: MockSeedDocument[];
|
|
156
|
+
relations?: MockSeedRelation[];
|
|
157
|
+
/** Missing fields default to a pending `create` of an `entity`, with null ids and the seed's load time. */
|
|
158
|
+
proposals?: Array<Partial<MockProposal> & {
|
|
159
|
+
id: string;
|
|
160
|
+
}>;
|
|
161
|
+
spaces?: MockSeedSpace[];
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Read a seed (JSON text or an already-parsed value) and check its shape:
|
|
165
|
+
* every collection optional, each an array of objects carrying its required
|
|
166
|
+
* string fields; unknown top-level keys are refused (a typo would otherwise
|
|
167
|
+
* seed nothing). Throws `TypeError` naming the first bad path.
|
|
168
|
+
*/
|
|
169
|
+
declare function parseMockPodSeed(input: string | unknown): MockPodSeed;
|
|
170
|
+
|
|
32
171
|
/** A placeholder pod origin; never contacted. */
|
|
33
172
|
declare const MOCK_AUTH_ISSUER = "https://pod.mock-synap.test";
|
|
34
173
|
declare const MOCK_GRANT: PodGrant[];
|
|
@@ -36,7 +175,7 @@ declare const MOCK_USER: PodUser;
|
|
|
36
175
|
type MockHubFailure = "rate_limited" | "forbidden" | "door_unsupported" | "unreachable" | "server" | "unavailable";
|
|
37
176
|
interface MockAuthServerOptions {
|
|
38
177
|
issuer?: string;
|
|
39
|
-
/** Issue rotating refresh tokens
|
|
178
|
+
/** Issue rotating refresh tokens. Default true; false = a pod without refresh tokens. */
|
|
40
179
|
refresh?: boolean;
|
|
41
180
|
/** Default 3600 (the pod's 1 h). */
|
|
42
181
|
accessTtlSec?: number;
|
|
@@ -44,7 +183,7 @@ interface MockAuthServerOptions {
|
|
|
44
183
|
refreshIdleSec?: number;
|
|
45
184
|
/** Send `refresh_token_expires_in` with each refresh token. Default false (the pod does not say). */
|
|
46
185
|
advertiseRefreshExpiry?: boolean;
|
|
47
|
-
/** The reuse grace, seconds
|
|
186
|
+
/** The reuse grace, seconds. Default 30 (the pod's); 0 = off (a pod without the grace). */
|
|
48
187
|
refreshGraceSec?: number;
|
|
49
188
|
/**
|
|
50
189
|
* `synap_app` on token responses: the connection's id and its page on the
|
|
@@ -60,8 +199,22 @@ interface MockAuthServerOptions {
|
|
|
60
199
|
user?: PodUser;
|
|
61
200
|
/** Override or remove (undefined) metadata fields: a pod too old, a lying issuer. */
|
|
62
201
|
metadata?: Record<string, unknown>;
|
|
63
|
-
/**
|
|
202
|
+
/**
|
|
203
|
+
* Hub writes: "proposed" (ask-first, the default for an agent token) or
|
|
204
|
+
* "applied" (the default for `actsAs: "person"`).
|
|
205
|
+
*/
|
|
64
206
|
writeMode?: "proposed" | "applied";
|
|
207
|
+
/**
|
|
208
|
+
* `synap_acts_as` on token responses. Omitted = not sent (a pod that does
|
|
209
|
+
* not name it; the SDK reads an agent connection).
|
|
210
|
+
*/
|
|
211
|
+
actsAs?: PodActsAs;
|
|
212
|
+
/**
|
|
213
|
+
* The pod's data as one JSON shape (`parseMockPodSeed`). `seed.entities` come
|
|
214
|
+
* before `entities`; `seed.spaces` put a `templateSlug` on the grant rows of
|
|
215
|
+
* those spaces.
|
|
216
|
+
*/
|
|
217
|
+
seed?: MockPodSeed;
|
|
65
218
|
/** Hub paths (after /api/hub) that answer one failure. */
|
|
66
219
|
failures?: Record<string, MockHubFailure>;
|
|
67
220
|
now?: () => number;
|
|
@@ -113,8 +266,10 @@ interface MockAuthServer {
|
|
|
113
266
|
path: string;
|
|
114
267
|
body: unknown;
|
|
115
268
|
}>;
|
|
116
|
-
/** Proposals filed by proposed writes, in order. */
|
|
269
|
+
/** Proposals: the seeded ones, then those filed by proposed writes, in order. */
|
|
117
270
|
proposals: MockProposal[];
|
|
271
|
+
/** Proposals approved through `POST /proposals/:id/approve`, in order. */
|
|
272
|
+
approvals: string[];
|
|
118
273
|
/** The person approved or rejected one: it leaves the pending list. */
|
|
119
274
|
settleProposal(id: string, status: "approved" | "rejected"): void;
|
|
120
275
|
/** Another instance of the app rotated `token` (its own refresh): returns the pair the pod answered it. */
|
|
@@ -135,30 +290,18 @@ interface MockAuthServer {
|
|
|
135
290
|
reuses: number;
|
|
136
291
|
/** The person revoked the app: every token stops working. */
|
|
137
292
|
revokeAll(): void;
|
|
293
|
+
/**
|
|
294
|
+
* The live access keys stop working, the refresh tokens do not: what a
|
|
295
|
+
* narrow (the slot's key is revoked at once) or a pod-side "Revoke key"
|
|
296
|
+
* does. The next refresh mints a new key.
|
|
297
|
+
*/
|
|
298
|
+
revokeAccessKeys(): void;
|
|
138
299
|
/** Live (unrevoked) refresh tokens. */
|
|
139
300
|
liveRefreshTokens(): string[];
|
|
140
301
|
reset(): void;
|
|
141
302
|
}
|
|
142
303
|
declare function createMockAuthServer(options?: MockAuthServerOptions): MockAuthServer;
|
|
143
304
|
|
|
144
|
-
/**
|
|
145
|
-
* A mock Synap Cloud pod chooser (Control Plane `GET /connect/pod`, wave C1)
|
|
146
|
-
* for "Continue with Synap Cloud" tests and a kit's mock mode. No network:
|
|
147
|
-
* `choose()` stands in for the person signing in to the CP and picking a pod.
|
|
148
|
-
*
|
|
149
|
-
* Wire behaviour mirrors the CP route (doc 18 §1.1): it accepts only
|
|
150
|
-
* `client_id` (an https CIMD URL; http on loopback only with
|
|
151
|
-
* `allowInsecureLoopback`, for a kit's mock mode on http://localhost) and
|
|
152
|
-
* `state` (`^[A-Za-z0-9._~-]{16,256}$`),
|
|
153
|
-
* takes the return URL from the app's client metadata (`initiate_login_uri`),
|
|
154
|
-
* never from the query, and answers `<initiate_login_uri>?iss=<pod issuer>&state=<state>`.
|
|
155
|
-
* It never sees a code or a token.
|
|
156
|
-
*
|
|
157
|
-
* const cloud = createMockCloud();
|
|
158
|
-
* createPodAppAuth({ ...config, cloudUrl: cloud.url });
|
|
159
|
-
* cloud.choose(location, { pod: mockPod.issuer, metadata: auth.metadata }) → the app's return URL
|
|
160
|
-
*/
|
|
161
|
-
|
|
162
305
|
/** A placeholder Control Plane origin; never contacted. */
|
|
163
306
|
declare const MOCK_CLOUD_URL = "https://cp.mock-synap.test";
|
|
164
307
|
interface MockCloud {
|
|
@@ -198,12 +341,12 @@ declare function createMockCloud(options?: {
|
|
|
198
341
|
* `credentials: "include"` request is refused (it rejects, as a CORS failure
|
|
199
342
|
* does); with `{ browser: true }` a non-safelisted request header is too.
|
|
200
343
|
*
|
|
201
|
-
* tokens MOCK_TOKENS.form / .share / .site the demo form,
|
|
344
|
+
* tokens MOCK_TOKENS.form / .share / .htmlShare / .site the demo form, shares (markdown, html) and site
|
|
202
345
|
* tokens MOCK_FAILURE_TOKENS.* one failure on every door
|
|
203
346
|
* anything else the uniform 404
|
|
204
347
|
*
|
|
205
348
|
* Pod-app sign-in (`@synap-core/sdk/connect`): `createMockAuthServer()`, see
|
|
206
|
-
* ./auth-server.ts; Synap Cloud's pod chooser: `createMockCloud()`, ./cloud.ts.
|
|
349
|
+
* ./auth-server.ts, seeded from one JSON shape (`parseMockPodSeed`, ./seed.ts); Synap Cloud's pod chooser: `createMockCloud()`, ./cloud.ts.
|
|
207
350
|
*/
|
|
208
351
|
|
|
209
352
|
/** A placeholder origin; never contacted. */
|
|
@@ -211,6 +354,7 @@ declare const MOCK_POD_URL = "https://mock.synap.invalid";
|
|
|
211
354
|
declare const MOCK_TOKENS: {
|
|
212
355
|
readonly form: "demo-form";
|
|
213
356
|
readonly share: "demo";
|
|
357
|
+
readonly htmlShare: "demo-html";
|
|
214
358
|
readonly site: "demo-site";
|
|
215
359
|
};
|
|
216
360
|
/** The `Retry-After` the `rateLimitedRetryAfter` token sends. */
|
|
@@ -266,4 +410,4 @@ interface MockPod {
|
|
|
266
410
|
}
|
|
267
411
|
declare function createMockPod(options?: MockPodOptions): MockPod;
|
|
268
412
|
|
|
269
|
-
export { type AuthorizeDecision, MOCK_APP, MOCK_APP_AGENT_ID, MOCK_AUTH_ISSUER, MOCK_CLOUD_URL, MOCK_FAILURE_TOKENS, MOCK_FORM, MOCK_GRANT, MOCK_POD_URL, MOCK_RETRY_AFTER_SEC, MOCK_SHARE, MOCK_SITE, MOCK_TOKENS, MOCK_USER, type MockAuthRequest, type MockAuthServer, type MockAuthServerOptions, type MockCloud, type MockCollection, type MockHubFailure, type MockPod, type MockPodOptions, type MockProposal, type MockRequest, type MockSite, type MockSubmission, createMockAuthServer, createMockCloud, createMockPod };
|
|
413
|
+
export { type AuthorizeDecision, MOCK_APP, MOCK_APP_AGENT_ID, MOCK_AUTH_ISSUER, MOCK_CLOUD_URL, MOCK_FAILURE_TOKENS, MOCK_FORM, MOCK_GRANT, MOCK_HTML_SHARE, MOCK_POD_URL, MOCK_RETRY_AFTER_SEC, MOCK_SHARE, MOCK_SITE, MOCK_TOKENS, MOCK_USER, type MockAuthRequest, type MockAuthServer, type MockAuthServerOptions, type MockCloud, type MockCollection, type MockHubFailure, type MockPod, type MockPodOptions, type MockPodSeed, type MockProposal, type MockRequest, type MockSeedDocument, type MockSeedEntity, type MockSeedFacet, type MockSeedRelation, type MockSeedSession, type MockSeedSpace, type MockSite, type MockSubmission, createMockAuthServer, createMockCloud, createMockPod, parseMockPodSeed };
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { P as PublicForm,
|
|
2
|
-
import { P as PodGrant, a as PodUser, C as ClientMetadataDocument } from './types-
|
|
1
|
+
import { P as PublicForm, h as PublicShare, f as PublicRecord } from './client-B50KC2aJ.js';
|
|
2
|
+
import { P as PodGrant, a as PodUser, b as PodActsAs, C as ClientMetadataDocument } from './types-DZYNBCq7.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Demo data for the mock pod. Shapes are the `@synap-core/sdk/public` wire
|
|
@@ -8,6 +8,12 @@ import { P as PodGrant, a as PodUser, C as ClientMetadataDocument } from './type
|
|
|
8
8
|
|
|
9
9
|
declare const MOCK_FORM: PublicForm;
|
|
10
10
|
declare const MOCK_SHARE: PublicShare;
|
|
11
|
+
/**
|
|
12
|
+
* A published HTML document (an estimate), as the pod serves an `html` body: a
|
|
13
|
+
* page renders it only inside `<iframe sandbox="" srcdoc>`, so the script below
|
|
14
|
+
* never runs.
|
|
15
|
+
*/
|
|
16
|
+
declare const MOCK_HTML_SHARE: PublicShare;
|
|
11
17
|
/** One collection of a mock site: its door config and its live records. */
|
|
12
18
|
interface MockCollection {
|
|
13
19
|
/** Default sort keys (`-` = descending); also the sort keys the door accepts. */
|
|
@@ -29,6 +35,139 @@ interface MockSite {
|
|
|
29
35
|
*/
|
|
30
36
|
declare const MOCK_SITE: MockSite;
|
|
31
37
|
|
|
38
|
+
/**
|
|
39
|
+
* The mock pod's data as ONE JSON-serialisable shape, so an app's mock mode,
|
|
40
|
+
* smoke and screenshots replay a recorded snapshot (one file) instead of
|
|
41
|
+
* fixtures built in app code:
|
|
42
|
+
*
|
|
43
|
+
* import { readFileSync } from "node:fs";
|
|
44
|
+
* const seed = parseMockPodSeed(readFileSync("mock/pod-seed.json", "utf8"));
|
|
45
|
+
* const pod = createMockAuthServer({ seed });
|
|
46
|
+
*
|
|
47
|
+
* Rows are the pod's wire shapes (see `@synap-core/sdk/connect` hub-types):
|
|
48
|
+
* `entities` as `GET /entities` answers them, `sessions` as `GET
|
|
49
|
+
* /focus-sessions` rows (missing columns get the pod's defaults), `documents`
|
|
50
|
+
* as `GET /documents/:id` (`sections` and `diagnostics` answered empty),
|
|
51
|
+
* `relations` as graph edges for `GET /entities/:id/connections`, `proposals`
|
|
52
|
+
* as `GET /proposals?view=basic` rows, `spaces` name each granted space's
|
|
53
|
+
* template (`synap_grant[].templateSlug` on the token response).
|
|
54
|
+
*
|
|
55
|
+
* An entity's roles ride on its row as `facets`, the shape `GET /entities?
|
|
56
|
+
* includeFacets=true` answers them (`{ facetId, slug, properties, status }`),
|
|
57
|
+
* plus the seed-only space each is stamped in, so a seed row reads like a pod
|
|
58
|
+
* row read with includeFacets. The mock derives `facetSlugs` and the reach rule
|
|
59
|
+
* from them and serves neither field unless asked, as the pod.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/** One role an entity wears (backend `FacetRowAnnotation`, plus the space it is stamped in). */
|
|
63
|
+
interface MockSeedFacet {
|
|
64
|
+
facetId: string;
|
|
65
|
+
/** The role's slug, e.g. `lead`. */
|
|
66
|
+
slug: string;
|
|
67
|
+
/** The facet's overlay properties. Default `{}`. */
|
|
68
|
+
properties?: Record<string, unknown>;
|
|
69
|
+
/** Its lifecycle status. Default null. */
|
|
70
|
+
status?: string | null;
|
|
71
|
+
/**
|
|
72
|
+
* Seed only, never served: the space the role is in, i.e. the space whose
|
|
73
|
+
* members see the facet (stamped there, or a pod-wide facet of a role granted
|
|
74
|
+
* to it). A role in a granted space puts a pod-wide entity in the app's
|
|
75
|
+
* reach; a workspace lens sees the roles of that space only.
|
|
76
|
+
*/
|
|
77
|
+
workspaceId: string;
|
|
78
|
+
/**
|
|
79
|
+
* Seed only, never served: the role is SHARED, i.e. stored pod-wide (null
|
|
80
|
+
* `workspaceId`) and granted to `workspaceId`, which then means "the space it
|
|
81
|
+
* is granted to". Default false = stamped in `workspaceId`. The pod treats the
|
|
82
|
+
* two alike for reach, except `includePodWide=false` on a space lens, which
|
|
83
|
+
* drops a pod-wide row reached only through a shared role.
|
|
84
|
+
*/
|
|
85
|
+
sharedRole?: boolean;
|
|
86
|
+
}
|
|
87
|
+
interface MockSeedEntity {
|
|
88
|
+
id: string;
|
|
89
|
+
profileSlug?: string;
|
|
90
|
+
workspaceId?: string | null;
|
|
91
|
+
title?: string | null;
|
|
92
|
+
properties?: Record<string, unknown>;
|
|
93
|
+
/**
|
|
94
|
+
* The roles (facets) it wears, in the wire shape plus the space each is in
|
|
95
|
+
* (`MockSeedFacet`). SEED INPUT ONLY: the pod sends them only to
|
|
96
|
+
* `includeFacets`, and so does the mock. A pod-wide entity (`workspaceId:
|
|
97
|
+
* null`) with a role in a granted space is readable by the app, as a member
|
|
98
|
+
* of that space sees it. Absent = none.
|
|
99
|
+
*/
|
|
100
|
+
facets?: MockSeedFacet[];
|
|
101
|
+
/**
|
|
102
|
+
* @deprecated Use `facets`; removed in 2.0. Shorthand for `facets` entries
|
|
103
|
+
* with no id and no overlay: `parseMockPodSeed` turns each into a facet (a
|
|
104
|
+
* generated `facetId`, empty properties, a null status). A role stated in
|
|
105
|
+
* both is refused.
|
|
106
|
+
*/
|
|
107
|
+
seedRoles?: Array<{
|
|
108
|
+
profileSlug: string;
|
|
109
|
+
workspaceId: string;
|
|
110
|
+
}>;
|
|
111
|
+
[key: string]: unknown;
|
|
112
|
+
}
|
|
113
|
+
interface MockSeedSession {
|
|
114
|
+
id: string;
|
|
115
|
+
workspaceId?: string | null;
|
|
116
|
+
projectId?: string | null;
|
|
117
|
+
goal?: string;
|
|
118
|
+
status?: string;
|
|
119
|
+
/** Slots; `owner: "human"` ones are what waits on the person. */
|
|
120
|
+
expectedOutputs?: Array<Record<string, unknown>>;
|
|
121
|
+
/** The entity it is about: listed among that entity's connections. */
|
|
122
|
+
subjectEntityId?: string | null;
|
|
123
|
+
/** ISO 8601; the list is newest first by it. */
|
|
124
|
+
startedAt?: string;
|
|
125
|
+
[key: string]: unknown;
|
|
126
|
+
}
|
|
127
|
+
interface MockSeedDocument {
|
|
128
|
+
id: string;
|
|
129
|
+
title: string;
|
|
130
|
+
content: string;
|
|
131
|
+
type?: string;
|
|
132
|
+
language?: string | null;
|
|
133
|
+
workspaceId?: string | null;
|
|
134
|
+
revision?: number;
|
|
135
|
+
version?: number;
|
|
136
|
+
createdAt?: string;
|
|
137
|
+
updatedAt?: string;
|
|
138
|
+
}
|
|
139
|
+
interface MockSeedRelation {
|
|
140
|
+
id: string;
|
|
141
|
+
sourceEntityId: string;
|
|
142
|
+
targetEntityId: string;
|
|
143
|
+
type: string;
|
|
144
|
+
createdAt?: string;
|
|
145
|
+
}
|
|
146
|
+
interface MockSeedSpace {
|
|
147
|
+
id: string;
|
|
148
|
+
name: string;
|
|
149
|
+
/** The template it came from (e.g. `content-os`); absent = made by hand. */
|
|
150
|
+
templateSlug?: string;
|
|
151
|
+
}
|
|
152
|
+
interface MockPodSeed {
|
|
153
|
+
entities?: MockSeedEntity[];
|
|
154
|
+
sessions?: MockSeedSession[];
|
|
155
|
+
documents?: MockSeedDocument[];
|
|
156
|
+
relations?: MockSeedRelation[];
|
|
157
|
+
/** Missing fields default to a pending `create` of an `entity`, with null ids and the seed's load time. */
|
|
158
|
+
proposals?: Array<Partial<MockProposal> & {
|
|
159
|
+
id: string;
|
|
160
|
+
}>;
|
|
161
|
+
spaces?: MockSeedSpace[];
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Read a seed (JSON text or an already-parsed value) and check its shape:
|
|
165
|
+
* every collection optional, each an array of objects carrying its required
|
|
166
|
+
* string fields; unknown top-level keys are refused (a typo would otherwise
|
|
167
|
+
* seed nothing). Throws `TypeError` naming the first bad path.
|
|
168
|
+
*/
|
|
169
|
+
declare function parseMockPodSeed(input: string | unknown): MockPodSeed;
|
|
170
|
+
|
|
32
171
|
/** A placeholder pod origin; never contacted. */
|
|
33
172
|
declare const MOCK_AUTH_ISSUER = "https://pod.mock-synap.test";
|
|
34
173
|
declare const MOCK_GRANT: PodGrant[];
|
|
@@ -36,7 +175,7 @@ declare const MOCK_USER: PodUser;
|
|
|
36
175
|
type MockHubFailure = "rate_limited" | "forbidden" | "door_unsupported" | "unreachable" | "server" | "unavailable";
|
|
37
176
|
interface MockAuthServerOptions {
|
|
38
177
|
issuer?: string;
|
|
39
|
-
/** Issue rotating refresh tokens
|
|
178
|
+
/** Issue rotating refresh tokens. Default true; false = a pod without refresh tokens. */
|
|
40
179
|
refresh?: boolean;
|
|
41
180
|
/** Default 3600 (the pod's 1 h). */
|
|
42
181
|
accessTtlSec?: number;
|
|
@@ -44,7 +183,7 @@ interface MockAuthServerOptions {
|
|
|
44
183
|
refreshIdleSec?: number;
|
|
45
184
|
/** Send `refresh_token_expires_in` with each refresh token. Default false (the pod does not say). */
|
|
46
185
|
advertiseRefreshExpiry?: boolean;
|
|
47
|
-
/** The reuse grace, seconds
|
|
186
|
+
/** The reuse grace, seconds. Default 30 (the pod's); 0 = off (a pod without the grace). */
|
|
48
187
|
refreshGraceSec?: number;
|
|
49
188
|
/**
|
|
50
189
|
* `synap_app` on token responses: the connection's id and its page on the
|
|
@@ -60,8 +199,22 @@ interface MockAuthServerOptions {
|
|
|
60
199
|
user?: PodUser;
|
|
61
200
|
/** Override or remove (undefined) metadata fields: a pod too old, a lying issuer. */
|
|
62
201
|
metadata?: Record<string, unknown>;
|
|
63
|
-
/**
|
|
202
|
+
/**
|
|
203
|
+
* Hub writes: "proposed" (ask-first, the default for an agent token) or
|
|
204
|
+
* "applied" (the default for `actsAs: "person"`).
|
|
205
|
+
*/
|
|
64
206
|
writeMode?: "proposed" | "applied";
|
|
207
|
+
/**
|
|
208
|
+
* `synap_acts_as` on token responses. Omitted = not sent (a pod that does
|
|
209
|
+
* not name it; the SDK reads an agent connection).
|
|
210
|
+
*/
|
|
211
|
+
actsAs?: PodActsAs;
|
|
212
|
+
/**
|
|
213
|
+
* The pod's data as one JSON shape (`parseMockPodSeed`). `seed.entities` come
|
|
214
|
+
* before `entities`; `seed.spaces` put a `templateSlug` on the grant rows of
|
|
215
|
+
* those spaces.
|
|
216
|
+
*/
|
|
217
|
+
seed?: MockPodSeed;
|
|
65
218
|
/** Hub paths (after /api/hub) that answer one failure. */
|
|
66
219
|
failures?: Record<string, MockHubFailure>;
|
|
67
220
|
now?: () => number;
|
|
@@ -113,8 +266,10 @@ interface MockAuthServer {
|
|
|
113
266
|
path: string;
|
|
114
267
|
body: unknown;
|
|
115
268
|
}>;
|
|
116
|
-
/** Proposals filed by proposed writes, in order. */
|
|
269
|
+
/** Proposals: the seeded ones, then those filed by proposed writes, in order. */
|
|
117
270
|
proposals: MockProposal[];
|
|
271
|
+
/** Proposals approved through `POST /proposals/:id/approve`, in order. */
|
|
272
|
+
approvals: string[];
|
|
118
273
|
/** The person approved or rejected one: it leaves the pending list. */
|
|
119
274
|
settleProposal(id: string, status: "approved" | "rejected"): void;
|
|
120
275
|
/** Another instance of the app rotated `token` (its own refresh): returns the pair the pod answered it. */
|
|
@@ -135,30 +290,18 @@ interface MockAuthServer {
|
|
|
135
290
|
reuses: number;
|
|
136
291
|
/** The person revoked the app: every token stops working. */
|
|
137
292
|
revokeAll(): void;
|
|
293
|
+
/**
|
|
294
|
+
* The live access keys stop working, the refresh tokens do not: what a
|
|
295
|
+
* narrow (the slot's key is revoked at once) or a pod-side "Revoke key"
|
|
296
|
+
* does. The next refresh mints a new key.
|
|
297
|
+
*/
|
|
298
|
+
revokeAccessKeys(): void;
|
|
138
299
|
/** Live (unrevoked) refresh tokens. */
|
|
139
300
|
liveRefreshTokens(): string[];
|
|
140
301
|
reset(): void;
|
|
141
302
|
}
|
|
142
303
|
declare function createMockAuthServer(options?: MockAuthServerOptions): MockAuthServer;
|
|
143
304
|
|
|
144
|
-
/**
|
|
145
|
-
* A mock Synap Cloud pod chooser (Control Plane `GET /connect/pod`, wave C1)
|
|
146
|
-
* for "Continue with Synap Cloud" tests and a kit's mock mode. No network:
|
|
147
|
-
* `choose()` stands in for the person signing in to the CP and picking a pod.
|
|
148
|
-
*
|
|
149
|
-
* Wire behaviour mirrors the CP route (doc 18 §1.1): it accepts only
|
|
150
|
-
* `client_id` (an https CIMD URL; http on loopback only with
|
|
151
|
-
* `allowInsecureLoopback`, for a kit's mock mode on http://localhost) and
|
|
152
|
-
* `state` (`^[A-Za-z0-9._~-]{16,256}$`),
|
|
153
|
-
* takes the return URL from the app's client metadata (`initiate_login_uri`),
|
|
154
|
-
* never from the query, and answers `<initiate_login_uri>?iss=<pod issuer>&state=<state>`.
|
|
155
|
-
* It never sees a code or a token.
|
|
156
|
-
*
|
|
157
|
-
* const cloud = createMockCloud();
|
|
158
|
-
* createPodAppAuth({ ...config, cloudUrl: cloud.url });
|
|
159
|
-
* cloud.choose(location, { pod: mockPod.issuer, metadata: auth.metadata }) → the app's return URL
|
|
160
|
-
*/
|
|
161
|
-
|
|
162
305
|
/** A placeholder Control Plane origin; never contacted. */
|
|
163
306
|
declare const MOCK_CLOUD_URL = "https://cp.mock-synap.test";
|
|
164
307
|
interface MockCloud {
|
|
@@ -198,12 +341,12 @@ declare function createMockCloud(options?: {
|
|
|
198
341
|
* `credentials: "include"` request is refused (it rejects, as a CORS failure
|
|
199
342
|
* does); with `{ browser: true }` a non-safelisted request header is too.
|
|
200
343
|
*
|
|
201
|
-
* tokens MOCK_TOKENS.form / .share / .site the demo form,
|
|
344
|
+
* tokens MOCK_TOKENS.form / .share / .htmlShare / .site the demo form, shares (markdown, html) and site
|
|
202
345
|
* tokens MOCK_FAILURE_TOKENS.* one failure on every door
|
|
203
346
|
* anything else the uniform 404
|
|
204
347
|
*
|
|
205
348
|
* Pod-app sign-in (`@synap-core/sdk/connect`): `createMockAuthServer()`, see
|
|
206
|
-
* ./auth-server.ts; Synap Cloud's pod chooser: `createMockCloud()`, ./cloud.ts.
|
|
349
|
+
* ./auth-server.ts, seeded from one JSON shape (`parseMockPodSeed`, ./seed.ts); Synap Cloud's pod chooser: `createMockCloud()`, ./cloud.ts.
|
|
207
350
|
*/
|
|
208
351
|
|
|
209
352
|
/** A placeholder origin; never contacted. */
|
|
@@ -211,6 +354,7 @@ declare const MOCK_POD_URL = "https://mock.synap.invalid";
|
|
|
211
354
|
declare const MOCK_TOKENS: {
|
|
212
355
|
readonly form: "demo-form";
|
|
213
356
|
readonly share: "demo";
|
|
357
|
+
readonly htmlShare: "demo-html";
|
|
214
358
|
readonly site: "demo-site";
|
|
215
359
|
};
|
|
216
360
|
/** The `Retry-After` the `rateLimitedRetryAfter` token sends. */
|
|
@@ -266,4 +410,4 @@ interface MockPod {
|
|
|
266
410
|
}
|
|
267
411
|
declare function createMockPod(options?: MockPodOptions): MockPod;
|
|
268
412
|
|
|
269
|
-
export { type AuthorizeDecision, MOCK_APP, MOCK_APP_AGENT_ID, MOCK_AUTH_ISSUER, MOCK_CLOUD_URL, MOCK_FAILURE_TOKENS, MOCK_FORM, MOCK_GRANT, MOCK_POD_URL, MOCK_RETRY_AFTER_SEC, MOCK_SHARE, MOCK_SITE, MOCK_TOKENS, MOCK_USER, type MockAuthRequest, type MockAuthServer, type MockAuthServerOptions, type MockCloud, type MockCollection, type MockHubFailure, type MockPod, type MockPodOptions, type MockProposal, type MockRequest, type MockSite, type MockSubmission, createMockAuthServer, createMockCloud, createMockPod };
|
|
413
|
+
export { type AuthorizeDecision, MOCK_APP, MOCK_APP_AGENT_ID, MOCK_AUTH_ISSUER, MOCK_CLOUD_URL, MOCK_FAILURE_TOKENS, MOCK_FORM, MOCK_GRANT, MOCK_HTML_SHARE, MOCK_POD_URL, MOCK_RETRY_AFTER_SEC, MOCK_SHARE, MOCK_SITE, MOCK_TOKENS, MOCK_USER, type MockAuthRequest, type MockAuthServer, type MockAuthServerOptions, type MockCloud, type MockCollection, type MockHubFailure, type MockPod, type MockPodOptions, type MockPodSeed, type MockProposal, type MockRequest, type MockSeedDocument, type MockSeedEntity, type MockSeedFacet, type MockSeedRelation, type MockSeedSession, type MockSeedSpace, type MockSite, type MockSubmission, createMockAuthServer, createMockCloud, createMockPod, parseMockPodSeed };
|