@synap-core/sdk 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +154 -0
- package/dist/chunk-J7XHAAFJ.js +79 -0
- package/dist/chunk-J7XHAAFJ.js.map +1 -0
- package/dist/connect.browser.cjs +22 -0
- package/dist/connect.browser.cjs.map +1 -0
- package/dist/connect.browser.d.cts +2 -0
- package/dist/connect.browser.d.ts +2 -0
- package/dist/connect.browser.js +5 -0
- package/dist/connect.browser.js.map +1 -0
- package/dist/connect.cjs +1487 -0
- package/dist/connect.cjs.map +1 -0
- package/dist/connect.d.cts +647 -0
- package/dist/connect.d.ts +647 -0
- package/dist/connect.js +1365 -0
- package/dist/connect.js.map +1 -0
- package/dist/testing.cjs +431 -19
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +160 -1
- package/dist/testing.d.ts +160 -1
- package/dist/testing.js +404 -19
- package/dist/testing.js.map +1 -1
- package/dist/types-C2KGNXEn.d.cts +95 -0
- package/dist/types-C2KGNXEn.d.ts +95 -0
- package/package.json +18 -10
- package/src/connect/auth.ts +535 -0
- package/src/connect/browser.ts +10 -0
- package/src/connect/client.ts +299 -0
- package/src/connect/cloud.ts +206 -0
- package/src/connect/discovery.ts +263 -0
- package/src/connect/encoding.ts +93 -0
- package/src/connect/errors.ts +115 -0
- package/src/connect/flow.ts +489 -0
- package/src/connect/guard.ts +25 -0
- package/src/connect/index.ts +87 -0
- package/src/connect/seal.ts +119 -0
- package/src/connect/store.ts +189 -0
- package/src/connect/types.ts +100 -0
- package/src/testing/auth-server.ts +512 -0
- package/src/testing/cloud.ts +84 -0
- package/src/testing/index.ts +7 -0
package/dist/testing.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
pkceChallenge,
|
|
3
|
+
randomToken
|
|
4
|
+
} from "./chunk-J7XHAAFJ.js";
|
|
5
|
+
|
|
1
6
|
// src/testing/fixtures.ts
|
|
2
7
|
var MOCK_FORM = {
|
|
3
8
|
title: "Get in touch",
|
|
@@ -93,6 +98,378 @@ var MOCK_SITE = {
|
|
|
93
98
|
}
|
|
94
99
|
};
|
|
95
100
|
|
|
101
|
+
// src/testing/auth-server.ts
|
|
102
|
+
var MOCK_AUTH_ISSUER = "https://pod.mock-synap.test";
|
|
103
|
+
var MOCK_GRANT = [
|
|
104
|
+
{ permission: "entity.post.read", workspaceId: "ws_mock_studio", workspaceName: "Studio" },
|
|
105
|
+
{ permission: "entity.post.create", workspaceId: "ws_mock_studio", workspaceName: "Studio" }
|
|
106
|
+
];
|
|
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";
|
|
110
|
+
function json(status, body, headers = {}) {
|
|
111
|
+
return new Response(JSON.stringify(body), {
|
|
112
|
+
status,
|
|
113
|
+
headers: { "Content-Type": "application/json", "Cache-Control": "no-store", ...headers }
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function oauthError(error, description, status = 400) {
|
|
117
|
+
return json(status, { error, error_description: description });
|
|
118
|
+
}
|
|
119
|
+
function createMockAuthServer(options = {}) {
|
|
120
|
+
const issuer = options.issuer ?? MOCK_AUTH_ISSUER;
|
|
121
|
+
const now = options.now ?? Date.now;
|
|
122
|
+
const refresh = options.refresh ?? true;
|
|
123
|
+
const accessTtl = options.accessTtlSec ?? 3600;
|
|
124
|
+
const refreshIdle = options.refreshIdleSec ?? 30 * 24 * 3600;
|
|
125
|
+
const grant = options.grant ?? MOCK_GRANT;
|
|
126
|
+
const user = options.user ?? MOCK_USER;
|
|
127
|
+
const codes = /* @__PURE__ */ new Map();
|
|
128
|
+
const access = /* @__PURE__ */ new Map();
|
|
129
|
+
const refreshTokens = /* @__PURE__ */ new Map();
|
|
130
|
+
const requests = [];
|
|
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;
|
|
136
|
+
let proposalSeq = 0;
|
|
137
|
+
let graceAnswers = 0;
|
|
138
|
+
const metadata = () => {
|
|
139
|
+
const base = {
|
|
140
|
+
issuer,
|
|
141
|
+
authorization_endpoint: `${issuer}/authorize`,
|
|
142
|
+
token_endpoint: `${issuer}/token`,
|
|
143
|
+
registration_endpoint: `${issuer}/register`,
|
|
144
|
+
revocation_endpoint: `${issuer}/revoke`,
|
|
145
|
+
revocation_endpoint_auth_methods_supported: ["none"],
|
|
146
|
+
response_types_supported: ["code"],
|
|
147
|
+
grant_types_supported: refresh ? ["authorization_code", "refresh_token"] : ["authorization_code"],
|
|
148
|
+
code_challenge_methods_supported: ["S256"],
|
|
149
|
+
token_endpoint_auth_methods_supported: ["none"],
|
|
150
|
+
authorization_response_iss_parameter_supported: true,
|
|
151
|
+
client_id_metadata_document_supported: true
|
|
152
|
+
};
|
|
153
|
+
for (const [k, v] of Object.entries(options.metadata ?? {})) {
|
|
154
|
+
if (v === void 0) delete base[k];
|
|
155
|
+
else base[k] = v;
|
|
156
|
+
}
|
|
157
|
+
return base;
|
|
158
|
+
};
|
|
159
|
+
let loseNextRace = false;
|
|
160
|
+
let reuses = 0;
|
|
161
|
+
const revokeFamily = (family) => {
|
|
162
|
+
for (const r of refreshTokens.values()) if (r.family === family) r.revoked = true;
|
|
163
|
+
for (const a of access.values()) if (a.family === family) a.revoked = true;
|
|
164
|
+
};
|
|
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);
|
|
169
|
+
const body = {
|
|
170
|
+
access_token: accessToken,
|
|
171
|
+
token_type: "Bearer",
|
|
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)),
|
|
174
|
+
scope: scope.join(" "),
|
|
175
|
+
synap_grant: grant.filter((g) => scope.includes(g.permission)),
|
|
176
|
+
synap_user: user,
|
|
177
|
+
...app ? { synap_app: app } : {}
|
|
178
|
+
};
|
|
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) {
|
|
183
|
+
const refreshToken = randomToken();
|
|
184
|
+
refreshTokens.set(refreshToken, { clientId, family, idleUntil: now() + refreshIdle * 1e3, used: false, revoked: false, scope });
|
|
185
|
+
body.refresh_token = refreshToken;
|
|
186
|
+
if (options.advertiseRefreshExpiry) body.refresh_token_expires_in = refreshIdle;
|
|
187
|
+
}
|
|
188
|
+
return body;
|
|
189
|
+
};
|
|
190
|
+
async function token(form) {
|
|
191
|
+
if (form.grant_type === "authorization_code") {
|
|
192
|
+
const row = form.code ? codes.get(form.code) : void 0;
|
|
193
|
+
if (form.code) codes.delete(form.code);
|
|
194
|
+
if (!row) return oauthError("invalid_grant", "Invalid, expired, or already-redeemed authorization code");
|
|
195
|
+
if (row.clientId !== form.client_id) return oauthError("invalid_grant", "client_id does not match the code");
|
|
196
|
+
if (row.redirectUri !== form.redirect_uri) return oauthError("invalid_grant", "redirect_uri mismatch");
|
|
197
|
+
if (!form.code_verifier || await pkceChallenge(form.code_verifier) !== row.challenge) {
|
|
198
|
+
return oauthError("invalid_grant", "PKCE verification failed");
|
|
199
|
+
}
|
|
200
|
+
if (form.resource !== void 0 && row.resource && form.resource !== row.resource) {
|
|
201
|
+
return oauthError("invalid_target", "resource does not match the authorization request");
|
|
202
|
+
}
|
|
203
|
+
for (const r of refreshTokens.values()) if (r.clientId === row.clientId) revokeFamily(r.family);
|
|
204
|
+
return json(200, tokenBody(row.scope, row.clientId, randomToken()));
|
|
205
|
+
}
|
|
206
|
+
if (form.grant_type === "refresh_token") {
|
|
207
|
+
if (!refresh) return oauthError("unsupported_grant_type", 'Only grant_type "authorization_code" is supported');
|
|
208
|
+
if (!form.refresh_token || !form.client_id) return oauthError("invalid_request", "refresh_token and client_id are required");
|
|
209
|
+
const row = refreshTokens.get(form.refresh_token);
|
|
210
|
+
if (!row || row.clientId !== form.client_id || row.revoked) {
|
|
211
|
+
return oauthError("invalid_grant", "Invalid, expired, revoked or already-used refresh token");
|
|
212
|
+
}
|
|
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
|
+
}
|
|
221
|
+
reuses++;
|
|
222
|
+
revokeFamily(row.family);
|
|
223
|
+
return oauthError("invalid_grant", "Invalid, expired, revoked or already-used refresh token");
|
|
224
|
+
}
|
|
225
|
+
if (row.idleUntil <= now()) return oauthError("invalid_grant", "Invalid, expired, revoked or already-used refresh token");
|
|
226
|
+
if (loseNextRace) {
|
|
227
|
+
loseNextRace = false;
|
|
228
|
+
return oauthError("invalid_grant", "Invalid, expired, revoked or already-used refresh token");
|
|
229
|
+
}
|
|
230
|
+
row.used = true;
|
|
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);
|
|
237
|
+
}
|
|
238
|
+
return oauthError("unsupported_grant_type", "Unsupported grant_type");
|
|
239
|
+
}
|
|
240
|
+
function hub(method, path, query, headers, body) {
|
|
241
|
+
const failure2 = options.failures?.[path];
|
|
242
|
+
if (failure2 === "unreachable") return "reject";
|
|
243
|
+
const bearer = /^Bearer (.+)$/.exec(headers.authorization ?? "")?.[1];
|
|
244
|
+
const row = bearer ? access.get(bearer) : void 0;
|
|
245
|
+
if (!row || row.revoked || row.expiresAt <= now()) return json(401, { error: "Unauthorized" });
|
|
246
|
+
switch (failure2) {
|
|
247
|
+
case "rate_limited":
|
|
248
|
+
return json(429, { error: "Too many requests" }, { "Retry-After": "30" });
|
|
249
|
+
case "forbidden":
|
|
250
|
+
return json(403, { error: "Forbidden" });
|
|
251
|
+
case "door_unsupported":
|
|
252
|
+
return json(403, { error: "This door is not available to a space-scoped key", code: "GRANT_DOOR_UNSUPPORTED" });
|
|
253
|
+
case "server":
|
|
254
|
+
return json(500, { error: "Internal error" });
|
|
255
|
+
case "unavailable":
|
|
256
|
+
return json(503, { error: "Unavailable" });
|
|
257
|
+
}
|
|
258
|
+
if (path.startsWith("/knowledge/") || path === "/entities/retrieve" || path === "/memory/search") {
|
|
259
|
+
return json(403, { error: "This door is not available to a space-scoped key", code: "GRANT_DOOR_UNSUPPORTED" });
|
|
260
|
+
}
|
|
261
|
+
if (method === "GET" && path === "/users/me") return json(200, user);
|
|
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
|
+
}
|
|
280
|
+
if (method !== "GET") {
|
|
281
|
+
writes.push({ method, path, body });
|
|
282
|
+
const fields = body && typeof body === "object" ? body : {};
|
|
283
|
+
const workspaceId = typeof fields.workspaceId === "string" ? fields.workspaceId : null;
|
|
284
|
+
if ((options.writeMode ?? "proposed") === "proposed") {
|
|
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
|
+
});
|
|
299
|
+
return json(200, {
|
|
300
|
+
status: "proposed",
|
|
301
|
+
message: "Sent for review",
|
|
302
|
+
proposalId,
|
|
303
|
+
reviewUrl: `${issuer}/open/${proposalId}`
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
const id = `ent_mock_${writes.length}`;
|
|
307
|
+
if (method === "POST" && path === "/entities") entities.push({ id, ...fields });
|
|
308
|
+
return json(200, { status: "created", id });
|
|
309
|
+
}
|
|
310
|
+
return json(404, { error: "Not found" });
|
|
311
|
+
}
|
|
312
|
+
const mockFetch = async (input, init) => {
|
|
313
|
+
const request = typeof input === "object" && "method" in input ? input : null;
|
|
314
|
+
const url = new URL(typeof input === "string" ? input : input instanceof URL ? input.href : input.url);
|
|
315
|
+
const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
|
|
316
|
+
const headers = {};
|
|
317
|
+
new Headers(init?.headers ?? request?.headers).forEach((value, name) => {
|
|
318
|
+
headers[name.toLowerCase()] = value;
|
|
319
|
+
});
|
|
320
|
+
const rawBody = typeof init?.body === "string" ? init.body : request ? await request.clone().text() : "";
|
|
321
|
+
const isForm = (headers["content-type"] ?? "").includes("application/x-www-form-urlencoded");
|
|
322
|
+
const form = isForm ? Object.fromEntries(new URLSearchParams(rawBody)) : null;
|
|
323
|
+
requests.push({ method, url: url.href, headers, form });
|
|
324
|
+
if (url.origin !== new URL(issuer).origin) throw new TypeError(`Failed to fetch: ${url.origin} is not the mock pod`);
|
|
325
|
+
const path = url.pathname;
|
|
326
|
+
if (method === "GET" && path === "/.well-known/oauth-authorization-server") return json(200, metadata());
|
|
327
|
+
if (method === "POST" && path === "/token") return token(form ?? {});
|
|
328
|
+
if (method === "POST" && path === "/revoke") {
|
|
329
|
+
const t = form?.token ?? "";
|
|
330
|
+
const r = refreshTokens.get(t);
|
|
331
|
+
if (r && form?.client_id && form.client_id !== r.clientId) {
|
|
332
|
+
return oauthError("unauthorized_client", "The token was not issued to this client");
|
|
333
|
+
}
|
|
334
|
+
if (r) revokeFamily(r.family);
|
|
335
|
+
const a = access.get(t);
|
|
336
|
+
if (a) a.revoked = true;
|
|
337
|
+
return new Response(null, { status: 200 });
|
|
338
|
+
}
|
|
339
|
+
const hubPath = /^\/api\/hub(\/.*)$/.exec(path)?.[1];
|
|
340
|
+
if (hubPath) {
|
|
341
|
+
let body = void 0;
|
|
342
|
+
try {
|
|
343
|
+
body = rawBody ? JSON.parse(rawBody) : void 0;
|
|
344
|
+
} catch {
|
|
345
|
+
body = rawBody;
|
|
346
|
+
}
|
|
347
|
+
const res = hub(method, hubPath, url.searchParams, headers, body);
|
|
348
|
+
if (res === "reject") throw new TypeError("Failed to fetch: mock pod unreachable");
|
|
349
|
+
return res;
|
|
350
|
+
}
|
|
351
|
+
return json(404, { error: "Not found" });
|
|
352
|
+
};
|
|
353
|
+
return {
|
|
354
|
+
issuer,
|
|
355
|
+
fetch: mockFetch,
|
|
356
|
+
requests,
|
|
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
|
+
},
|
|
372
|
+
authorize(authorizeUrl, decision = {}) {
|
|
373
|
+
const url = new URL(authorizeUrl);
|
|
374
|
+
if (`${url.origin}${url.pathname}` !== `${issuer}/authorize`) throw new Error(`Not this pod's /authorize: ${authorizeUrl}`);
|
|
375
|
+
const q = url.searchParams;
|
|
376
|
+
const redirectUri = q.get("redirect_uri");
|
|
377
|
+
const clientId = q.get("client_id");
|
|
378
|
+
if (!clientId || !redirectUri) throw new Error("Authorization request rejected: client_id and redirect_uri are required");
|
|
379
|
+
const answer = new URL(redirectUri);
|
|
380
|
+
const state = decision.state ?? q.get("state");
|
|
381
|
+
const finish = (fields) => {
|
|
382
|
+
for (const [k, v] of Object.entries(fields)) answer.searchParams.set(k, v);
|
|
383
|
+
if (state !== null) answer.searchParams.set("state", state);
|
|
384
|
+
const iss = decision.iss === void 0 ? issuer : decision.iss;
|
|
385
|
+
if (iss !== null) answer.searchParams.set("iss", iss);
|
|
386
|
+
return answer.href;
|
|
387
|
+
};
|
|
388
|
+
if (q.get("response_type") !== "code") return finish({ error: "unsupported_response_type" });
|
|
389
|
+
if (q.get("code_challenge_method") !== "S256" || !q.get("code_challenge")) {
|
|
390
|
+
return finish({ error: "invalid_request", error_description: "PKCE S256 is required" });
|
|
391
|
+
}
|
|
392
|
+
const resource = q.get("resource");
|
|
393
|
+
if (resource !== null && resource !== `${issuer}/api/hub` && resource !== `${issuer}/mcp`) {
|
|
394
|
+
return finish({ error: "invalid_target" });
|
|
395
|
+
}
|
|
396
|
+
const scope = (q.get("scope") ?? "").split(/\s+/).filter(Boolean);
|
|
397
|
+
if (scope.length === 0) return finish({ error: "invalid_scope" });
|
|
398
|
+
if (decision.deny) return finish({ error: "access_denied" });
|
|
399
|
+
const code = randomToken();
|
|
400
|
+
codes.set(code, { clientId, redirectUri, challenge: q.get("code_challenge"), resource, scope });
|
|
401
|
+
return finish({ code });
|
|
402
|
+
},
|
|
403
|
+
consumeRefreshToken(t) {
|
|
404
|
+
const row = refreshTokens.get(t);
|
|
405
|
+
if (row) row.used = true;
|
|
406
|
+
},
|
|
407
|
+
loseNextRefreshRace() {
|
|
408
|
+
loseNextRace = true;
|
|
409
|
+
},
|
|
410
|
+
get reuses() {
|
|
411
|
+
return reuses;
|
|
412
|
+
},
|
|
413
|
+
revokeAll() {
|
|
414
|
+
for (const row of access.values()) row.revoked = true;
|
|
415
|
+
for (const row of refreshTokens.values()) row.revoked = true;
|
|
416
|
+
},
|
|
417
|
+
liveRefreshTokens() {
|
|
418
|
+
return [...refreshTokens].filter(([, r]) => !r.used && !r.revoked).map(([t]) => t);
|
|
419
|
+
},
|
|
420
|
+
reset() {
|
|
421
|
+
requests.length = 0;
|
|
422
|
+
writes.length = 0;
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
}
|
|
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
|
+
|
|
96
473
|
// src/testing/index.ts
|
|
97
474
|
var MOCK_POD_URL = "https://mock.synap.invalid";
|
|
98
475
|
var MOCK_TOKENS = { form: "demo-form", share: "demo", site: "demo-site" };
|
|
@@ -127,7 +504,7 @@ var MAX_OFFSET = 1e4;
|
|
|
127
504
|
var MAX_LIMIT = 100;
|
|
128
505
|
var MAX_SORT_KEYS = 3;
|
|
129
506
|
var PUBLISHED_AT = "publishedAt";
|
|
130
|
-
function
|
|
507
|
+
function json2(status, body, headers = {}) {
|
|
131
508
|
return new Response(JSON.stringify(body), {
|
|
132
509
|
status,
|
|
133
510
|
headers: { "Content-Type": "application/json", "Cache-Control": "no-cache", ...headers }
|
|
@@ -150,15 +527,15 @@ function etagMatches(header, etag) {
|
|
|
150
527
|
function failure(token) {
|
|
151
528
|
switch (token) {
|
|
152
529
|
case MOCK_FAILURE_TOKENS.rateLimited:
|
|
153
|
-
return
|
|
530
|
+
return json2(429, { error: "Too many requests", retryAfter: "5 minutes" });
|
|
154
531
|
case MOCK_FAILURE_TOKENS.rateLimitedRetryAfter:
|
|
155
|
-
return
|
|
532
|
+
return json2(429, { error: "Too many requests" }, { "Retry-After": String(MOCK_RETRY_AFTER_SEC) });
|
|
156
533
|
case MOCK_FAILURE_TOKENS.doorsDisabled:
|
|
157
|
-
return
|
|
534
|
+
return json2(403, DOORS_DISABLED);
|
|
158
535
|
case MOCK_FAILURE_TOKENS.serverError:
|
|
159
|
-
return
|
|
536
|
+
return json2(500, { error: "Internal error" });
|
|
160
537
|
case MOCK_FAILURE_TOKENS.unavailable:
|
|
161
|
-
return
|
|
538
|
+
return json2(503, { received: false });
|
|
162
539
|
case MOCK_FAILURE_TOKENS.unreachable:
|
|
163
540
|
return "reject";
|
|
164
541
|
default:
|
|
@@ -233,17 +610,17 @@ function cached(view, ifNoneMatch) {
|
|
|
233
610
|
if (etagMatches(ifNoneMatch, etag)) {
|
|
234
611
|
return new Response(null, { status: 304, headers: { ETag: etag, "Cache-Control": "no-cache" } });
|
|
235
612
|
}
|
|
236
|
-
return
|
|
613
|
+
return json2(200, view, { ETag: etag });
|
|
237
614
|
}
|
|
238
615
|
function siteDoor(site, rest, params, ifNoneMatch) {
|
|
239
616
|
if (rest.length === 0) return cached(manifest(site), ifNoneMatch);
|
|
240
617
|
if (rest[0] !== "collections" || rest.length > 3 || !Object.hasOwn(site.collections, rest[1])) {
|
|
241
|
-
return
|
|
618
|
+
return json2(404, NOT_FOUND);
|
|
242
619
|
}
|
|
243
620
|
const c = site.collections[rest[1]];
|
|
244
621
|
if (rest.length === 2) {
|
|
245
622
|
const q = parseQuery(params, c);
|
|
246
|
-
if (typeof q === "string") return
|
|
623
|
+
if (typeof q === "string") return json2(400, { error: q });
|
|
247
624
|
const sorted = [...c.records].sort(compareRecords(q.sort));
|
|
248
625
|
const page = {
|
|
249
626
|
items: sorted.slice(q.offset, q.offset + q.limit).map(summary),
|
|
@@ -255,10 +632,10 @@ function siteDoor(site, rest, params, ifNoneMatch) {
|
|
|
255
632
|
}
|
|
256
633
|
const address = rest[2];
|
|
257
634
|
const byPublicId = PUBLIC_ID_RE.test(address);
|
|
258
|
-
if (!byPublicId && !(address.length <= SLUG_MAX_LENGTH && SLUG_RE.test(address))) return
|
|
635
|
+
if (!byPublicId && !(address.length <= SLUG_MAX_LENGTH && SLUG_RE.test(address))) return json2(404, NOT_FOUND);
|
|
259
636
|
const record = c.records.find((r) => byPublicId ? r.publicId === address : r.slug === address);
|
|
260
|
-
if (!record) return
|
|
261
|
-
if (params.has("embed")) return
|
|
637
|
+
if (!record) return json2(404, NOT_FOUND);
|
|
638
|
+
if (params.has("embed")) return json2(400, { error: "embed is not available" });
|
|
262
639
|
return cached(record, ifNoneMatch);
|
|
263
640
|
}
|
|
264
641
|
function invalidKeys(form, body) {
|
|
@@ -295,12 +672,12 @@ function createMockPod(options = {}) {
|
|
|
295
672
|
if (preflighted.length > 0) throw new TypeError(`Failed to fetch: preflight refuses ${preflighted.join(", ")}`);
|
|
296
673
|
}
|
|
297
674
|
const m = url.pathname.match(/^\/api\/hub\/public\/(forms|shares|sites)\/(.+)$/);
|
|
298
|
-
if (!m) return
|
|
675
|
+
if (!m) return json2(404, NOT_FOUND);
|
|
299
676
|
let segments;
|
|
300
677
|
try {
|
|
301
678
|
segments = m[2].split("/").map(decodeURIComponent);
|
|
302
679
|
} catch {
|
|
303
|
-
return
|
|
680
|
+
return json2(404, NOT_FOUND);
|
|
304
681
|
}
|
|
305
682
|
const [token, ...rest] = segments;
|
|
306
683
|
const failed = failure(token);
|
|
@@ -318,15 +695,15 @@ function createMockPod(options = {}) {
|
|
|
318
695
|
const invalid = form ? invalidKeys(form, body) : [];
|
|
319
696
|
const status = invalid.length > 0 ? 422 : 202;
|
|
320
697
|
submissions.push({ token, body, status });
|
|
321
|
-
return status === 422 ?
|
|
698
|
+
return status === 422 ? json2(422, { received: false, invalid }) : json2(202, { received: true });
|
|
322
699
|
}
|
|
323
|
-
if (method !== "GET") return
|
|
324
|
-
if (door === "forms" && rest.length === 0 && Object.hasOwn(forms, token)) return
|
|
325
|
-
if (door === "shares" && rest.length === 0 && Object.hasOwn(shares, token)) return
|
|
700
|
+
if (method !== "GET") return json2(404, NOT_FOUND);
|
|
701
|
+
if (door === "forms" && rest.length === 0 && Object.hasOwn(forms, token)) return json2(200, forms[token]);
|
|
702
|
+
if (door === "shares" && rest.length === 0 && Object.hasOwn(shares, token)) return json2(200, shares[token]);
|
|
326
703
|
if (door === "sites" && Object.hasOwn(sites, token)) {
|
|
327
704
|
return siteDoor(sites[token], rest, url.searchParams, headers["if-none-match"] ?? null);
|
|
328
705
|
}
|
|
329
|
-
return
|
|
706
|
+
return json2(404, NOT_FOUND);
|
|
330
707
|
};
|
|
331
708
|
return {
|
|
332
709
|
podUrl: MOCK_POD_URL,
|
|
@@ -340,13 +717,21 @@ function createMockPod(options = {}) {
|
|
|
340
717
|
};
|
|
341
718
|
}
|
|
342
719
|
export {
|
|
720
|
+
MOCK_APP,
|
|
721
|
+
MOCK_APP_AGENT_ID,
|
|
722
|
+
MOCK_AUTH_ISSUER,
|
|
723
|
+
MOCK_CLOUD_URL,
|
|
343
724
|
MOCK_FAILURE_TOKENS,
|
|
344
725
|
MOCK_FORM,
|
|
726
|
+
MOCK_GRANT,
|
|
345
727
|
MOCK_POD_URL,
|
|
346
728
|
MOCK_RETRY_AFTER_SEC,
|
|
347
729
|
MOCK_SHARE,
|
|
348
730
|
MOCK_SITE,
|
|
349
731
|
MOCK_TOKENS,
|
|
732
|
+
MOCK_USER,
|
|
733
|
+
createMockAuthServer,
|
|
734
|
+
createMockCloud,
|
|
350
735
|
createMockPod
|
|
351
736
|
};
|
|
352
737
|
//# sourceMappingURL=testing.js.map
|