@synap-core/sdk 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
- import { P as PublicForm, a as PublicShare, b as PublicRecord } from './client-CVUT1Mcf.cjs';
2
- import { P as PodGrant, a as PodUser } from './types-DpqPNaaa.cjs';
1
+ import { P as PublicForm, g as PublicShare, e as PublicRecord } from './client-C5sF20f1.cjs';
2
+ import { P as PodGrant, a as PodUser, C as ClientMetadataDocument } from './types-C2KGNXEn.cjs';
3
3
 
4
4
  /**
5
5
  * Demo data for the mock pod. Shapes are the `@synap-core/sdk/public` wire
@@ -44,6 +44,18 @@ interface MockAuthServerOptions {
44
44
  refreshIdleSec?: number;
45
45
  /** Send `refresh_token_expires_in` with each refresh token. Default false (the pod does not say). */
46
46
  advertiseRefreshExpiry?: boolean;
47
+ /** The reuse grace, seconds (PB3b). Default 30 (the pod's); 0 = off (a pod before PB3b). */
48
+ refreshGraceSec?: number;
49
+ /**
50
+ * `synap_app` on token responses: the connection's id and its page on the
51
+ * pod console. Default `MOCK_APP`; null = a pod that does not send it.
52
+ */
53
+ app?: {
54
+ id: string;
55
+ manageUrl: string | null;
56
+ } | null;
57
+ /** Entities `GET /entities` answers (with `workspaceId` / `profileSlug` filters). */
58
+ entities?: Array<Record<string, unknown>>;
47
59
  grant?: PodGrant[];
48
60
  user?: PodUser;
49
61
  /** Override or remove (undefined) metadata fields: a pod too old, a lying issuer. */
@@ -70,6 +82,27 @@ interface AuthorizeDecision {
70
82
  /** Override `state` on the answer: a forged answer. */
71
83
  state?: string;
72
84
  }
85
+ /** A proposal a governed write filed (`GET /proposals` lists the pending ones). */
86
+ interface MockProposal {
87
+ id: string;
88
+ proposalType: string;
89
+ targetType: string;
90
+ targetId: string | null;
91
+ status: "pending" | "approved" | "rejected";
92
+ workspaceId: string | null;
93
+ createdAt: string;
94
+ correlationId: string | null;
95
+ sessionId: string | null;
96
+ agentUserId: string | null;
97
+ summary?: string;
98
+ }
99
+ /** The connection's `synap_app` the mock pod answers by default (backend `appTokenResponse`). */
100
+ declare const MOCK_APP: {
101
+ id: string;
102
+ manageUrl: string;
103
+ };
104
+ /** The app's agent on the mock pod: who files its proposals. */
105
+ declare const MOCK_APP_AGENT_ID = "agent_mock_app";
73
106
  interface MockAuthServer {
74
107
  issuer: string;
75
108
  fetch: typeof fetch;
@@ -80,6 +113,18 @@ interface MockAuthServer {
80
113
  path: string;
81
114
  body: unknown;
82
115
  }>;
116
+ /** Proposals filed by proposed writes, in order. */
117
+ proposals: MockProposal[];
118
+ /** The person approved or rejected one: it leaves the pending list. */
119
+ settleProposal(id: string, status: "approved" | "rejected"): void;
120
+ /** Another instance of the app rotated `token` (its own refresh): returns the pair the pod answered it. */
121
+ redeemElsewhere(token: string): Promise<{
122
+ access_token: string;
123
+ refresh_token: string;
124
+ expires_in: number;
125
+ }>;
126
+ /** Refresh redeems answered within the reuse grace (the same pair again). */
127
+ graceAnswers: number;
83
128
  /** Stand-in for the consent screen: validates the request like /authorize, returns where the pod redirects. */
84
129
  authorize(authorizeUrl: string, decision?: AuthorizeDecision): string;
85
130
  /** Mark a refresh token used elsewhere: presenting it now is a REUSE (family + live key revoked). */
@@ -96,6 +141,49 @@ interface MockAuthServer {
96
141
  }
97
142
  declare function createMockAuthServer(options?: MockAuthServerOptions): MockAuthServer;
98
143
 
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
+ /** A placeholder Control Plane origin; never contacted. */
163
+ declare const MOCK_CLOUD_URL = "https://cp.mock-synap.test";
164
+ interface MockCloud {
165
+ url: string;
166
+ /** The chooser URLs the app sent browsers to. */
167
+ requests: string[];
168
+ /**
169
+ * The person picks `pod` on the chooser: validates the request like the CP,
170
+ * returns where the CP redirects the browser. Throws on a request the CP
171
+ * refuses (shown as a page there, never redirected).
172
+ */
173
+ choose(location: string, opts: {
174
+ pod: string;
175
+ metadata: Pick<ClientMetadataDocument, "client_id" | "initiate_login_uri">;
176
+ }): string;
177
+ /** The person cancels on the chooser: `<initiate_login_uri>?state=…&error=access_denied`, no `iss`. */
178
+ cancel(location: string, opts: {
179
+ metadata: Pick<ClientMetadataDocument, "client_id" | "initiate_login_uri">;
180
+ }): string;
181
+ }
182
+ declare function createMockCloud(options?: {
183
+ url?: string;
184
+ allowInsecureLoopback?: boolean;
185
+ }): MockCloud;
186
+
99
187
  /**
100
188
  * @synap-core/sdk/testing — a mock pod: a fake `fetch` that answers the pod's
101
189
  * public doors (forms, shares, sites) from fixtures, with no network. Inject
@@ -115,7 +203,7 @@ declare function createMockAuthServer(options?: MockAuthServerOptions): MockAuth
115
203
  * anything else the uniform 404
116
204
  *
117
205
  * Pod-app sign-in (`@synap-core/sdk/connect`): `createMockAuthServer()`, see
118
- * ./auth-server.ts.
206
+ * ./auth-server.ts; Synap Cloud's pod chooser: `createMockCloud()`, ./cloud.ts.
119
207
  */
120
208
 
121
209
  /** A placeholder origin; never contacted. */
@@ -178,4 +266,4 @@ interface MockPod {
178
266
  }
179
267
  declare function createMockPod(options?: MockPodOptions): MockPod;
180
268
 
181
- export { type AuthorizeDecision, MOCK_AUTH_ISSUER, 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 MockCollection, type MockHubFailure, type MockPod, type MockPodOptions, type MockRequest, type MockSite, type MockSubmission, createMockAuthServer, createMockPod };
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 };
package/dist/testing.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { P as PublicForm, a as PublicShare, b as PublicRecord } from './client-CVUT1Mcf.js';
2
- import { P as PodGrant, a as PodUser } from './types-DpqPNaaa.js';
1
+ import { P as PublicForm, g as PublicShare, e as PublicRecord } from './client-C5sF20f1.js';
2
+ import { P as PodGrant, a as PodUser, C as ClientMetadataDocument } from './types-C2KGNXEn.js';
3
3
 
4
4
  /**
5
5
  * Demo data for the mock pod. Shapes are the `@synap-core/sdk/public` wire
@@ -44,6 +44,18 @@ interface MockAuthServerOptions {
44
44
  refreshIdleSec?: number;
45
45
  /** Send `refresh_token_expires_in` with each refresh token. Default false (the pod does not say). */
46
46
  advertiseRefreshExpiry?: boolean;
47
+ /** The reuse grace, seconds (PB3b). Default 30 (the pod's); 0 = off (a pod before PB3b). */
48
+ refreshGraceSec?: number;
49
+ /**
50
+ * `synap_app` on token responses: the connection's id and its page on the
51
+ * pod console. Default `MOCK_APP`; null = a pod that does not send it.
52
+ */
53
+ app?: {
54
+ id: string;
55
+ manageUrl: string | null;
56
+ } | null;
57
+ /** Entities `GET /entities` answers (with `workspaceId` / `profileSlug` filters). */
58
+ entities?: Array<Record<string, unknown>>;
47
59
  grant?: PodGrant[];
48
60
  user?: PodUser;
49
61
  /** Override or remove (undefined) metadata fields: a pod too old, a lying issuer. */
@@ -70,6 +82,27 @@ interface AuthorizeDecision {
70
82
  /** Override `state` on the answer: a forged answer. */
71
83
  state?: string;
72
84
  }
85
+ /** A proposal a governed write filed (`GET /proposals` lists the pending ones). */
86
+ interface MockProposal {
87
+ id: string;
88
+ proposalType: string;
89
+ targetType: string;
90
+ targetId: string | null;
91
+ status: "pending" | "approved" | "rejected";
92
+ workspaceId: string | null;
93
+ createdAt: string;
94
+ correlationId: string | null;
95
+ sessionId: string | null;
96
+ agentUserId: string | null;
97
+ summary?: string;
98
+ }
99
+ /** The connection's `synap_app` the mock pod answers by default (backend `appTokenResponse`). */
100
+ declare const MOCK_APP: {
101
+ id: string;
102
+ manageUrl: string;
103
+ };
104
+ /** The app's agent on the mock pod: who files its proposals. */
105
+ declare const MOCK_APP_AGENT_ID = "agent_mock_app";
73
106
  interface MockAuthServer {
74
107
  issuer: string;
75
108
  fetch: typeof fetch;
@@ -80,6 +113,18 @@ interface MockAuthServer {
80
113
  path: string;
81
114
  body: unknown;
82
115
  }>;
116
+ /** Proposals filed by proposed writes, in order. */
117
+ proposals: MockProposal[];
118
+ /** The person approved or rejected one: it leaves the pending list. */
119
+ settleProposal(id: string, status: "approved" | "rejected"): void;
120
+ /** Another instance of the app rotated `token` (its own refresh): returns the pair the pod answered it. */
121
+ redeemElsewhere(token: string): Promise<{
122
+ access_token: string;
123
+ refresh_token: string;
124
+ expires_in: number;
125
+ }>;
126
+ /** Refresh redeems answered within the reuse grace (the same pair again). */
127
+ graceAnswers: number;
83
128
  /** Stand-in for the consent screen: validates the request like /authorize, returns where the pod redirects. */
84
129
  authorize(authorizeUrl: string, decision?: AuthorizeDecision): string;
85
130
  /** Mark a refresh token used elsewhere: presenting it now is a REUSE (family + live key revoked). */
@@ -96,6 +141,49 @@ interface MockAuthServer {
96
141
  }
97
142
  declare function createMockAuthServer(options?: MockAuthServerOptions): MockAuthServer;
98
143
 
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
+ /** A placeholder Control Plane origin; never contacted. */
163
+ declare const MOCK_CLOUD_URL = "https://cp.mock-synap.test";
164
+ interface MockCloud {
165
+ url: string;
166
+ /** The chooser URLs the app sent browsers to. */
167
+ requests: string[];
168
+ /**
169
+ * The person picks `pod` on the chooser: validates the request like the CP,
170
+ * returns where the CP redirects the browser. Throws on a request the CP
171
+ * refuses (shown as a page there, never redirected).
172
+ */
173
+ choose(location: string, opts: {
174
+ pod: string;
175
+ metadata: Pick<ClientMetadataDocument, "client_id" | "initiate_login_uri">;
176
+ }): string;
177
+ /** The person cancels on the chooser: `<initiate_login_uri>?state=…&error=access_denied`, no `iss`. */
178
+ cancel(location: string, opts: {
179
+ metadata: Pick<ClientMetadataDocument, "client_id" | "initiate_login_uri">;
180
+ }): string;
181
+ }
182
+ declare function createMockCloud(options?: {
183
+ url?: string;
184
+ allowInsecureLoopback?: boolean;
185
+ }): MockCloud;
186
+
99
187
  /**
100
188
  * @synap-core/sdk/testing — a mock pod: a fake `fetch` that answers the pod's
101
189
  * public doors (forms, shares, sites) from fixtures, with no network. Inject
@@ -115,7 +203,7 @@ declare function createMockAuthServer(options?: MockAuthServerOptions): MockAuth
115
203
  * anything else the uniform 404
116
204
  *
117
205
  * Pod-app sign-in (`@synap-core/sdk/connect`): `createMockAuthServer()`, see
118
- * ./auth-server.ts.
206
+ * ./auth-server.ts; Synap Cloud's pod chooser: `createMockCloud()`, ./cloud.ts.
119
207
  */
120
208
 
121
209
  /** A placeholder origin; never contacted. */
@@ -178,4 +266,4 @@ interface MockPod {
178
266
  }
179
267
  declare function createMockPod(options?: MockPodOptions): MockPod;
180
268
 
181
- export { type AuthorizeDecision, MOCK_AUTH_ISSUER, 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 MockCollection, type MockHubFailure, type MockPod, type MockPodOptions, type MockRequest, type MockSite, type MockSubmission, createMockAuthServer, createMockPod };
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 };
package/dist/testing.js CHANGED
@@ -105,6 +105,8 @@ var MOCK_GRANT = [
105
105
  { permission: "entity.post.create", workspaceId: "ws_mock_studio", workspaceName: "Studio" }
106
106
  ];
107
107
  var MOCK_USER = { id: "user_mock_ada", name: "Ada Lovelace" };
108
+ var MOCK_APP = { id: "app_mock_studio", manageUrl: "https://pod-admin.mock-synap.test/apps/app_mock_studio" };
109
+ var MOCK_APP_AGENT_ID = "agent_mock_app";
108
110
  function json(status, body, headers = {}) {
109
111
  return new Response(JSON.stringify(body), {
110
112
  status,
@@ -127,7 +129,12 @@ function createMockAuthServer(options = {}) {
127
129
  const refreshTokens = /* @__PURE__ */ new Map();
128
130
  const requests = [];
129
131
  const writes = [];
132
+ const proposals = [];
133
+ const entities = [...options.entities ?? []];
134
+ const graceMs = (options.refreshGraceSec ?? 30) * 1e3;
135
+ const app = options.app === void 0 ? MOCK_APP : options.app;
130
136
  let proposalSeq = 0;
137
+ let graceAnswers = 0;
131
138
  const metadata = () => {
132
139
  const base = {
133
140
  issuer,
@@ -155,18 +162,24 @@ function createMockAuthServer(options = {}) {
155
162
  for (const r of refreshTokens.values()) if (r.family === family) r.revoked = true;
156
163
  for (const a of access.values()) if (a.family === family) a.revoked = true;
157
164
  };
158
- const tokenBody = (scope, clientId, family) => {
159
- const accessToken = `synap_hub_mock_${randomToken()}`;
160
- access.set(accessToken, { expiresAt: now() + accessTtl * 1e3, revoked: false, family: refresh ? family : null });
165
+ const tokenBody = (scope, clientId, family, pair) => {
166
+ const accessToken = pair?.accessToken ?? `synap_hub_mock_${randomToken()}`;
167
+ if (!pair) access.set(accessToken, { expiresAt: now() + accessTtl * 1e3, revoked: false, family: refresh ? family : null, scope });
168
+ const row = access.get(accessToken);
161
169
  const body = {
162
170
  access_token: accessToken,
163
171
  token_type: "Bearer",
164
- expires_in: accessTtl,
172
+ // As the pod: what the key has left, never an assumed hour.
173
+ expires_in: Math.max(0, Math.floor((row.expiresAt - now()) / 1e3)),
165
174
  scope: scope.join(" "),
166
175
  synap_grant: grant.filter((g) => scope.includes(g.permission)),
167
- synap_user: user
176
+ synap_user: user,
177
+ ...app ? { synap_app: app } : {}
168
178
  };
169
- if (refresh) {
179
+ if (pair) {
180
+ body.refresh_token = pair.refreshToken;
181
+ if (options.advertiseRefreshExpiry) body.refresh_token_expires_in = Math.floor((refreshTokens.get(pair.refreshToken).idleUntil - now()) / 1e3);
182
+ } else if (refresh) {
170
183
  const refreshToken = randomToken();
171
184
  refreshTokens.set(refreshToken, { clientId, family, idleUntil: now() + refreshIdle * 1e3, used: false, revoked: false, scope });
172
185
  body.refresh_token = refreshToken;
@@ -198,6 +211,13 @@ function createMockAuthServer(options = {}) {
198
211
  return oauthError("invalid_grant", "Invalid, expired, revoked or already-used refresh token");
199
212
  }
200
213
  if (row.used) {
214
+ const successor = row.successor ? refreshTokens.get(row.successor) : void 0;
215
+ const key = row.pairAccess ? access.get(row.pairAccess) : void 0;
216
+ const inGrace = graceMs > 0 && row.usedAt !== void 0 && now() - row.usedAt < graceMs;
217
+ if (inGrace && successor && !successor.used && !successor.revoked && key && !key.revoked && key.expiresAt > now()) {
218
+ graceAnswers++;
219
+ return json(200, tokenBody(row.scope, row.clientId, row.family, { accessToken: row.pairAccess, refreshToken: row.successor }));
220
+ }
201
221
  reuses++;
202
222
  revokeFamily(row.family);
203
223
  return oauthError("invalid_grant", "Invalid, expired, revoked or already-used refresh token");
@@ -208,11 +228,16 @@ function createMockAuthServer(options = {}) {
208
228
  return oauthError("invalid_grant", "Invalid, expired, revoked or already-used refresh token");
209
229
  }
210
230
  row.used = true;
211
- return json(200, tokenBody(row.scope, row.clientId, row.family));
231
+ row.usedAt = now();
232
+ for (const a of access.values()) if (a.family === row.family) a.revoked = true;
233
+ const body = tokenBody(row.scope, row.clientId, row.family);
234
+ row.successor = body.refresh_token;
235
+ row.pairAccess = body.access_token;
236
+ return json(200, body);
212
237
  }
213
238
  return oauthError("unsupported_grant_type", "Unsupported grant_type");
214
239
  }
215
- function hub(method, path, headers, body) {
240
+ function hub(method, path, query, headers, body) {
216
241
  const failure2 = options.failures?.[path];
217
242
  if (failure2 === "unreachable") return "reject";
218
243
  const bearer = /^Bearer (.+)$/.exec(headers.authorization ?? "")?.[1];
@@ -234,11 +259,43 @@ function createMockAuthServer(options = {}) {
234
259
  return json(403, { error: "This door is not available to a space-scoped key", code: "GRANT_DOOR_UNSUPPORTED" });
235
260
  }
236
261
  if (method === "GET" && path === "/users/me") return json(200, user);
237
- if (method === "GET" && path === "/entities") return json(200, { entities: [], total: 0 });
262
+ if (method === "GET" && path === "/entities") {
263
+ const ws = query.get("workspaceId");
264
+ const kind = query.get("profileSlug");
265
+ const offset = Number(query.get("offset") ?? 0) || 0;
266
+ const limit = Number(query.get("limit") ?? 50) || 50;
267
+ const rows = entities.filter((e) => (!ws || e.workspaceId === ws) && (!kind || e.profileSlug === kind));
268
+ return json(200, rows.slice(offset, offset + limit));
269
+ }
270
+ if (method === "GET" && path === "/proposals") {
271
+ const readable = new Set(grant.filter((g) => g.permission === "proposal.read" && row.scope.includes(g.permission)).map((g) => g.workspaceId));
272
+ const status = query.get("status") ?? "pending";
273
+ const ws = query.get("workspaceId");
274
+ const limit = Math.min(Math.max(Number(query.get("limit") ?? 50) || 50, 1), 200);
275
+ const offset = Number(query.get("offset") ?? 0) || 0;
276
+ const rows = proposals.filter((p) => p.workspaceId !== null && readable.has(p.workspaceId)).filter((p) => (status === "all" || p.status === status) && (!ws || p.workspaceId === ws)).reverse();
277
+ const page = rows.slice(offset, offset + limit);
278
+ return json(200, { proposals: page, limit, offset, total: rows.length, hasMore: offset + page.length < rows.length });
279
+ }
238
280
  if (method !== "GET") {
239
281
  writes.push({ method, path, body });
282
+ const fields = body && typeof body === "object" ? body : {};
283
+ const workspaceId = typeof fields.workspaceId === "string" ? fields.workspaceId : null;
240
284
  if ((options.writeMode ?? "proposed") === "proposed") {
241
285
  const proposalId = `prop_mock_${++proposalSeq}`;
286
+ proposals.push({
287
+ id: proposalId,
288
+ proposalType: method === "POST" ? "create" : method === "DELETE" ? "delete" : "update",
289
+ targetType: path.split("/")[1]?.replace(/s$/, "") ?? "entity",
290
+ targetId: null,
291
+ status: "pending",
292
+ workspaceId,
293
+ createdAt: new Date(now()).toISOString(),
294
+ correlationId: null,
295
+ sessionId: null,
296
+ agentUserId: MOCK_APP_AGENT_ID,
297
+ ...typeof fields.title === "string" ? { summary: `Create "${fields.title}"` } : {}
298
+ });
242
299
  return json(200, {
243
300
  status: "proposed",
244
301
  message: "Sent for review",
@@ -246,7 +303,9 @@ function createMockAuthServer(options = {}) {
246
303
  reviewUrl: `${issuer}/open/${proposalId}`
247
304
  });
248
305
  }
249
- return json(200, { status: "created", id: `ent_mock_${writes.length}` });
306
+ const id = `ent_mock_${writes.length}`;
307
+ if (method === "POST" && path === "/entities") entities.push({ id, ...fields });
308
+ return json(200, { status: "created", id });
250
309
  }
251
310
  return json(404, { error: "Not found" });
252
311
  }
@@ -285,7 +344,7 @@ function createMockAuthServer(options = {}) {
285
344
  } catch {
286
345
  body = rawBody;
287
346
  }
288
- const res = hub(method, hubPath, headers, body);
347
+ const res = hub(method, hubPath, url.searchParams, headers, body);
289
348
  if (res === "reject") throw new TypeError("Failed to fetch: mock pod unreachable");
290
349
  return res;
291
350
  }
@@ -296,6 +355,20 @@ function createMockAuthServer(options = {}) {
296
355
  fetch: mockFetch,
297
356
  requests,
298
357
  writes,
358
+ proposals,
359
+ settleProposal(id, status) {
360
+ const p = proposals.find((x) => x.id === id);
361
+ if (p) p.status = status;
362
+ },
363
+ async redeemElsewhere(t) {
364
+ const row = refreshTokens.get(t);
365
+ const res = await token({ grant_type: "refresh_token", refresh_token: t, client_id: row?.clientId ?? "" });
366
+ if (res.status !== 200) throw new Error(`redeemElsewhere: the pod answered ${res.status}`);
367
+ return await res.json();
368
+ },
369
+ get graceAnswers() {
370
+ return graceAnswers;
371
+ },
299
372
  authorize(authorizeUrl, decision = {}) {
300
373
  const url = new URL(authorizeUrl);
301
374
  if (`${url.origin}${url.pathname}` !== `${issuer}/authorize`) throw new Error(`Not this pod's /authorize: ${authorizeUrl}`);
@@ -351,6 +424,52 @@ function createMockAuthServer(options = {}) {
351
424
  };
352
425
  }
353
426
 
427
+ // src/testing/cloud.ts
428
+ var MOCK_CLOUD_URL = "https://cp.mock-synap.test";
429
+ var STATE_RE = /^[A-Za-z0-9._~-]{16,256}$/;
430
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
431
+ function createMockCloud(options = {}) {
432
+ const base = (options.url ?? MOCK_CLOUD_URL).replace(/\/+$/, "");
433
+ const requests = [];
434
+ return {
435
+ url: base,
436
+ requests,
437
+ choose(location, { pod, metadata }) {
438
+ const back = check(location, metadata);
439
+ back.searchParams.set("iss", pod);
440
+ back.searchParams.set("state", new URL(location).searchParams.get("state"));
441
+ return back.href;
442
+ },
443
+ cancel(location, { metadata }) {
444
+ const back = check(location, metadata);
445
+ back.searchParams.set("state", new URL(location).searchParams.get("state"));
446
+ back.searchParams.set("error", "access_denied");
447
+ return back.href;
448
+ }
449
+ };
450
+ function check(location, metadata) {
451
+ requests.push(location);
452
+ const url = new URL(location);
453
+ if (`${url.origin}${url.pathname}` !== `${base}/connect/pod`) throw new Error(`Not this CP's /connect/pod: ${location}`);
454
+ const clientId = url.searchParams.get("client_id");
455
+ const state = url.searchParams.get("state");
456
+ if (!clientId || !cimdUrlAccepted(clientId)) throw new Error("Chooser refused: client_id must be an https CIMD URL");
457
+ if (!state || !STATE_RE.test(state)) throw new Error("Chooser refused: state is missing or malformed");
458
+ if (clientId !== metadata.client_id) throw new Error("Chooser refused: the client metadata is not this client_id's");
459
+ return new URL(metadata.initiate_login_uri);
460
+ }
461
+ function cimdUrlAccepted(clientId) {
462
+ let url;
463
+ try {
464
+ url = new URL(clientId);
465
+ } catch {
466
+ return false;
467
+ }
468
+ if (url.protocol === "https:") return true;
469
+ return url.protocol === "http:" && options.allowInsecureLoopback === true && LOOPBACK_HOSTS.has(url.hostname);
470
+ }
471
+ }
472
+
354
473
  // src/testing/index.ts
355
474
  var MOCK_POD_URL = "https://mock.synap.invalid";
356
475
  var MOCK_TOKENS = { form: "demo-form", share: "demo", site: "demo-site" };
@@ -598,7 +717,10 @@ function createMockPod(options = {}) {
598
717
  };
599
718
  }
600
719
  export {
720
+ MOCK_APP,
721
+ MOCK_APP_AGENT_ID,
601
722
  MOCK_AUTH_ISSUER,
723
+ MOCK_CLOUD_URL,
602
724
  MOCK_FAILURE_TOKENS,
603
725
  MOCK_FORM,
604
726
  MOCK_GRANT,
@@ -609,6 +731,7 @@ export {
609
731
  MOCK_TOKENS,
610
732
  MOCK_USER,
611
733
  createMockAuthServer,
734
+ createMockCloud,
612
735
  createMockPod
613
736
  };
614
737
  //# sourceMappingURL=testing.js.map