@synap-core/sdk 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +133 -0
- package/README.md +136 -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 +412 -64
- package/dist/connect.cjs.map +1 -1
- package/dist/connect.d.cts +400 -32
- package/dist/connect.d.ts +400 -32
- package/dist/connect.js +407 -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 +234 -12
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +122 -27
- package/dist/testing.d.ts +122 -27
- package/dist/testing.js +230 -12
- 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 +156 -27
- package/src/connect/client.ts +374 -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 +254 -0
- package/src/connect/index.ts +60 -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 +239 -14
- package/src/testing/cloud.ts +3 -4
- package/src/testing/fixtures.ts +26 -0
- package/src/testing/index.ts +8 -6
- package/src/testing/seed.ts +120 -0
- package/dist/chunk-J7XHAAFJ.js.map +0 -1
package/dist/connect.cjs
CHANGED
|
@@ -38,6 +38,7 @@ __export(connect_exports, {
|
|
|
38
38
|
canonicalPodUrl: () => canonicalPodUrl,
|
|
39
39
|
cloudBaseUrl: () => cloudBaseUrl,
|
|
40
40
|
cloudConnectAvailable: () => cloudConnectAvailable,
|
|
41
|
+
connectionActsAs: () => connectionActsAs,
|
|
41
42
|
connectionExpiresAt: () => connectionExpiresAt,
|
|
42
43
|
cookieNames: () => cookieNames,
|
|
43
44
|
createPodAppAuth: () => createPodAppAuth,
|
|
@@ -45,6 +46,7 @@ __export(connect_exports, {
|
|
|
45
46
|
createSealedCookieStore: () => createSealedCookieStore,
|
|
46
47
|
discoverPod: () => discoverPod,
|
|
47
48
|
handleCallback: () => handleCallback,
|
|
49
|
+
humanSlots: () => humanSlots,
|
|
48
50
|
isBrowserRuntime: () => isBrowserRuntime,
|
|
49
51
|
isConnectError: () => isConnectError,
|
|
50
52
|
isPrefetchOrAsset: () => isPrefetchOrAsset,
|
|
@@ -107,6 +109,7 @@ var CONNECT_ERROR_CODES = [
|
|
|
107
109
|
"rate_limited",
|
|
108
110
|
"expired",
|
|
109
111
|
"revoked",
|
|
112
|
+
"pod_moved",
|
|
110
113
|
"server_error"
|
|
111
114
|
];
|
|
112
115
|
function parseConnectErrorCode(value) {
|
|
@@ -142,11 +145,52 @@ function retryAfterSeconds(header, now) {
|
|
|
142
145
|
return Math.max(0, Math.ceil((at - now) / 1e3));
|
|
143
146
|
}
|
|
144
147
|
|
|
148
|
+
// src/connect/http.ts
|
|
149
|
+
function timeoutSignal(ms) {
|
|
150
|
+
return typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(ms) : void 0;
|
|
151
|
+
}
|
|
152
|
+
async function readCapped(res, maxBytes) {
|
|
153
|
+
const declared = Number(res.headers.get("content-length"));
|
|
154
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
155
|
+
await res.body?.cancel().catch(() => {
|
|
156
|
+
});
|
|
157
|
+
return { ok: false, reason: "too_large" };
|
|
158
|
+
}
|
|
159
|
+
if (!res.body) return { ok: true, text: "" };
|
|
160
|
+
const reader = res.body.getReader();
|
|
161
|
+
const decoder2 = new TextDecoder();
|
|
162
|
+
let total = 0;
|
|
163
|
+
let text = "";
|
|
164
|
+
try {
|
|
165
|
+
for (; ; ) {
|
|
166
|
+
const { done, value } = await reader.read();
|
|
167
|
+
if (done) break;
|
|
168
|
+
total += value.byteLength;
|
|
169
|
+
if (total > maxBytes) {
|
|
170
|
+
await reader.cancel().catch(() => {
|
|
171
|
+
});
|
|
172
|
+
return { ok: false, reason: "too_large" };
|
|
173
|
+
}
|
|
174
|
+
text += decoder2.decode(value, { stream: true });
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
return { ok: false, reason: "unreadable" };
|
|
178
|
+
}
|
|
179
|
+
return { ok: true, text: text + decoder2.decode() };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/connect/rules.ts
|
|
183
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
184
|
+
var SPACE_HINT_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
185
|
+
var CLOUD_STATE_RE = /^[A-Za-z0-9._~-]{16,256}$/;
|
|
186
|
+
function isIpLiteral(hostname) {
|
|
187
|
+
return hostname.startsWith("[") || /^\d{1,3}(\.\d{1,3}){3}$/.test(hostname);
|
|
188
|
+
}
|
|
189
|
+
|
|
145
190
|
// src/connect/discovery.ts
|
|
146
191
|
var METADATA_PATH = "/.well-known/oauth-authorization-server";
|
|
147
192
|
var TIMEOUT_MS = 5e3;
|
|
148
193
|
var MAX_METADATA_BYTES = 64 * 1024;
|
|
149
|
-
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
150
194
|
function isPrivateIpv4(host) {
|
|
151
195
|
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
152
196
|
if (!m) return false;
|
|
@@ -199,10 +243,12 @@ function sameOriginEndpoint(value, issuer) {
|
|
|
199
243
|
}
|
|
200
244
|
}
|
|
201
245
|
async function readJsonCapped(res) {
|
|
202
|
-
const
|
|
203
|
-
if (
|
|
246
|
+
const read = await readCapped(res, MAX_METADATA_BYTES);
|
|
247
|
+
if (!read.ok) {
|
|
248
|
+
throw read.reason === "too_large" ? new ConnectError("not_a_pod", "Metadata document too large") : new ConnectError("pod_unreachable", "The metadata answer broke off");
|
|
249
|
+
}
|
|
204
250
|
try {
|
|
205
|
-
return JSON.parse(text);
|
|
251
|
+
return JSON.parse(read.text);
|
|
206
252
|
} catch {
|
|
207
253
|
throw new ConnectError("not_a_pod", "Metadata is not JSON");
|
|
208
254
|
}
|
|
@@ -215,18 +261,27 @@ async function discoverIssuer(issuer, opts) {
|
|
|
215
261
|
method: "GET",
|
|
216
262
|
headers: { Accept: "application/json" },
|
|
217
263
|
redirect: "manual",
|
|
218
|
-
signal:
|
|
264
|
+
signal: timeoutSignal(TIMEOUT_MS)
|
|
219
265
|
});
|
|
220
266
|
} catch {
|
|
221
267
|
throw new ConnectError("pod_unreachable", "The pod did not answer");
|
|
222
268
|
}
|
|
269
|
+
if (res.status >= 300 && res.status < 400) {
|
|
270
|
+
await res.body?.cancel().catch(() => {
|
|
271
|
+
});
|
|
272
|
+
throw new ConnectError("pod_moved", `The address answered ${res.status}: the pod moved (its new address is not followed)`);
|
|
273
|
+
}
|
|
223
274
|
if (res.status === 429) {
|
|
224
275
|
throw new ConnectError("rate_limited", "The pod asked to slow down", {
|
|
225
276
|
retryAfterSec: retryAfterSeconds(res.headers.get("retry-after"), (opts.now ?? Date.now)())
|
|
226
277
|
});
|
|
227
278
|
}
|
|
228
279
|
if (res.status >= 500) throw new ConnectError("pod_unreachable", `The pod answered ${res.status}`);
|
|
229
|
-
if (res.status !== 200)
|
|
280
|
+
if (res.status !== 200) {
|
|
281
|
+
await res.body?.cancel().catch(() => {
|
|
282
|
+
});
|
|
283
|
+
throw new ConnectError("not_a_pod", `No authorization server metadata (${res.status})`);
|
|
284
|
+
}
|
|
230
285
|
const doc = await readJsonCapped(res);
|
|
231
286
|
if (!doc || typeof doc !== "object" || Array.isArray(doc)) throw new ConnectError("not_a_pod", "Metadata is not an object");
|
|
232
287
|
const m = doc;
|
|
@@ -252,21 +307,38 @@ async function discoverIssuer(issuer, opts) {
|
|
|
252
307
|
refreshSupported: list(m.grant_types_supported)?.includes("refresh_token") ?? false
|
|
253
308
|
};
|
|
254
309
|
}
|
|
310
|
+
var CLOUD_DOMAIN = "synap.live";
|
|
311
|
+
var CLOUD_POD_ZONE = `pod.${CLOUD_DOMAIN}`;
|
|
312
|
+
var POD_NAME_RE = /^[a-z0-9]([a-z0-9-]{0,30}[a-z0-9])?$/;
|
|
313
|
+
function fallbackIssuers(first) {
|
|
314
|
+
const url = new URL(first);
|
|
315
|
+
const host = url.hostname;
|
|
316
|
+
if (url.pathname !== "/" || !host.includes(".") || host.endsWith(`.${CLOUD_POD_ZONE}`)) return [];
|
|
317
|
+
const label = host.endsWith(`.${CLOUD_DOMAIN}`) ? host.slice(0, -CLOUD_DOMAIN.length - 1) : null;
|
|
318
|
+
if (label !== null && POD_NAME_RE.test(label)) {
|
|
319
|
+
return [`${url.protocol}//${label}.${CLOUD_POD_ZONE}`, `${url.protocol}//pod.${url.host}`];
|
|
320
|
+
}
|
|
321
|
+
return host.startsWith("pod.") ? [] : [`${url.protocol}//pod.${url.host}`];
|
|
322
|
+
}
|
|
255
323
|
async function discoverPod(input, opts = {}) {
|
|
256
324
|
const first = canonicalPodUrl(input, opts);
|
|
325
|
+
let firstError;
|
|
257
326
|
try {
|
|
258
327
|
return await discoverIssuer(first, opts);
|
|
259
328
|
} catch (err) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
329
|
+
firstError = err;
|
|
330
|
+
}
|
|
331
|
+
const retryable = (err) => err instanceof ConnectError && (err.code === "not_a_pod" || err.code === "pod_unreachable" || err.code === "pod_moved");
|
|
332
|
+
if (!retryable(firstError)) throw firstError;
|
|
333
|
+
let tooOld = null;
|
|
334
|
+
for (const candidate of fallbackIssuers(first)) {
|
|
264
335
|
try {
|
|
265
|
-
return await discoverIssuer(
|
|
266
|
-
} catch (
|
|
267
|
-
|
|
336
|
+
return await discoverIssuer(candidate, opts);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
if (err instanceof ConnectError && err.code === "pod_unsupported") tooOld ??= err;
|
|
268
339
|
}
|
|
269
340
|
}
|
|
341
|
+
throw tooOld ?? firstError;
|
|
270
342
|
}
|
|
271
343
|
function safeNextPath(value, appOrigin) {
|
|
272
344
|
if (!value || value.length > 2048) return null;
|
|
@@ -301,10 +373,10 @@ function acceptLaunch(url, appOrigin, opts = {}) {
|
|
|
301
373
|
return {
|
|
302
374
|
pod,
|
|
303
375
|
next: safeNextPath(params.get("target_link_uri"), appOrigin),
|
|
304
|
-
spaceHint: hint &&
|
|
376
|
+
spaceHint: hint && SPACE_HINT_RE.test(hint) ? hint : null,
|
|
305
377
|
// A browser launch never carries a state: any well-formed one is a Synap Cloud return
|
|
306
378
|
// (a pick: iss + state; a cancel: state + error=access_denied, no iss).
|
|
307
|
-
cloudState: state &&
|
|
379
|
+
cloudState: state && CLOUD_STATE_RE.test(state) ? state : null
|
|
308
380
|
};
|
|
309
381
|
}
|
|
310
382
|
|
|
@@ -459,8 +531,6 @@ async function unseal(sealed, opts) {
|
|
|
459
531
|
|
|
460
532
|
// src/connect/cloud.ts
|
|
461
533
|
var CLOUD_TRANSACTION_TTL_MS = 10 * 60 * 1e3;
|
|
462
|
-
var STATE_RE = /^[A-Za-z0-9._~-]{16,256}$/;
|
|
463
|
-
var LOOPBACK_HOSTS2 = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
464
534
|
function cloudBaseUrl(cloudUrl, opts = {}) {
|
|
465
535
|
let url;
|
|
466
536
|
try {
|
|
@@ -468,7 +538,7 @@ function cloudBaseUrl(cloudUrl, opts = {}) {
|
|
|
468
538
|
} catch {
|
|
469
539
|
throw new ConnectError("misconfigured", "cloudUrl is not a URL");
|
|
470
540
|
}
|
|
471
|
-
const loopback =
|
|
541
|
+
const loopback = LOOPBACK_HOSTS.has(url.hostname);
|
|
472
542
|
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback && opts.allowInsecureLoopback)) {
|
|
473
543
|
throw new ConnectError("misconfigured", "cloudUrl must be https (http only on localhost in development)");
|
|
474
544
|
}
|
|
@@ -536,7 +606,7 @@ async function acceptCloudReturn(opts) {
|
|
|
536
606
|
}
|
|
537
607
|
const params = new URL(opts.url).searchParams;
|
|
538
608
|
const state = params.get("state");
|
|
539
|
-
if (state === null || !
|
|
609
|
+
if (state === null || !CLOUD_STATE_RE.test(state) || !constantTimeEqual(state, tx.state)) {
|
|
540
610
|
throw new ConnectError("mismatch", "The Synap Cloud answer does not belong to the sign-in this browser started");
|
|
541
611
|
}
|
|
542
612
|
const error = params.get("error");
|
|
@@ -554,6 +624,12 @@ async function acceptCloudReturn(opts) {
|
|
|
554
624
|
}
|
|
555
625
|
|
|
556
626
|
// src/connect/client.ts
|
|
627
|
+
function connectionActsAs(connection) {
|
|
628
|
+
return connection.actsAs === "person" ? "person" : "agent";
|
|
629
|
+
}
|
|
630
|
+
function humanSlots(session) {
|
|
631
|
+
return session.expectedOutputs.filter((slot) => slot.owner === "human" && slot.status !== "done" && !slot.retiredAt);
|
|
632
|
+
}
|
|
557
633
|
function can(connection, permission, workspaceId) {
|
|
558
634
|
return connection.grant.some((g) => g.permission === permission && (workspaceId === void 0 || g.workspaceId === workspaceId));
|
|
559
635
|
}
|
|
@@ -562,23 +638,50 @@ function spaces(connection) {
|
|
|
562
638
|
for (const g of connection.grant) {
|
|
563
639
|
const space = bySpace.get(g.workspaceId) ?? { workspaceId: g.workspaceId, workspaceName: g.workspaceName, permissions: [] };
|
|
564
640
|
if (!space.workspaceName && g.workspaceName) space.workspaceName = g.workspaceName;
|
|
641
|
+
if (!space.templateSlug && g.templateSlug) space.templateSlug = g.templateSlug;
|
|
565
642
|
if (!space.permissions.includes(g.permission)) space.permissions.push(g.permission);
|
|
566
643
|
bySpace.set(g.workspaceId, space);
|
|
567
644
|
}
|
|
568
645
|
return [...bySpace.values()];
|
|
569
646
|
}
|
|
647
|
+
var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
648
|
+
var PROPOSAL_IDS_MAX = 100;
|
|
570
649
|
var SAFE_PATH = /^\/(?!\/)[A-Za-z0-9\-._~!$&'()*+,;=:@%/]*$/;
|
|
650
|
+
var TIMEOUT_MS2 = 1e4;
|
|
651
|
+
var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
571
652
|
function hubUrl(podUrl, path, query) {
|
|
572
|
-
if (!SAFE_PATH.test(path) || /(^|\/)\.\.?(\/|$)/.test(path) || path.includes("\\"))
|
|
573
|
-
|
|
653
|
+
if (!SAFE_PATH.test(path) || /%2e/i.test(path) || /(^|\/)\.\.?(\/|$)/.test(path) || path.includes("\\")) return null;
|
|
654
|
+
let url;
|
|
655
|
+
try {
|
|
656
|
+
url = new URL(`${podUrl}/api/hub${path}`);
|
|
657
|
+
} catch {
|
|
658
|
+
return null;
|
|
574
659
|
}
|
|
575
|
-
|
|
660
|
+
if (url.origin !== new URL(podUrl).origin || !(url.pathname === "/api/hub" || url.pathname.startsWith("/api/hub/"))) return null;
|
|
576
661
|
for (const [key, value] of Object.entries(query ?? {})) {
|
|
577
662
|
if (value === void 0 || value === null) continue;
|
|
578
663
|
for (const v of Array.isArray(value) ? value : [value]) url.searchParams.append(key, String(v));
|
|
579
664
|
}
|
|
580
665
|
return url;
|
|
581
666
|
}
|
|
667
|
+
function renewalError(err) {
|
|
668
|
+
const e = isConnectError(err) ? err : null;
|
|
669
|
+
const detail = e?.message ?? (err instanceof Error ? err.message : "The connection could not be renewed");
|
|
670
|
+
switch (e?.code) {
|
|
671
|
+
case "revoked":
|
|
672
|
+
return { code: "unauthorized", status: 401, detail };
|
|
673
|
+
case "expired":
|
|
674
|
+
return { code: "expired", status: null, detail };
|
|
675
|
+
case "rate_limited":
|
|
676
|
+
return { code: "rate_limited", status: null, detail, ...e.retryAfterSec !== void 0 ? { retryAfterSec: e.retryAfterSec } : {} };
|
|
677
|
+
case "pod_unreachable":
|
|
678
|
+
return { code: "pod_unreachable", status: null, detail };
|
|
679
|
+
case "pod_moved":
|
|
680
|
+
return { code: "pod_moved", status: null, detail };
|
|
681
|
+
default:
|
|
682
|
+
return { code: "server", status: null, detail };
|
|
683
|
+
}
|
|
684
|
+
}
|
|
582
685
|
function errorFor(status, body, retryAfter, now) {
|
|
583
686
|
const obj = body && typeof body === "object" ? body : {};
|
|
584
687
|
const detail = typeof obj.error === "string" ? obj.error : typeof obj.message === "string" ? obj.message : void 0;
|
|
@@ -593,6 +696,7 @@ function errorFor(status, body, retryAfter, now) {
|
|
|
593
696
|
return { code: "rate_limited", ...base, ...retryAfterSec !== void 0 ? { retryAfterSec } : {} };
|
|
594
697
|
}
|
|
595
698
|
if (status === 502 || status === 503 || status === 504) return { code: "pod_unreachable", ...base };
|
|
699
|
+
if (status >= 300 && status < 400) return { code: "pod_moved", ...base };
|
|
596
700
|
return { code: "server", ...base };
|
|
597
701
|
}
|
|
598
702
|
function reviewUrlFor(podUrl, reviewUrl, proposalId) {
|
|
@@ -605,13 +709,37 @@ function reviewUrlFor(podUrl, reviewUrl, proposalId) {
|
|
|
605
709
|
}
|
|
606
710
|
return proposalId ? `${podUrl}/open/${encodeURIComponent(proposalId)}` : podUrl;
|
|
607
711
|
}
|
|
712
|
+
function refused(code, reason, detail) {
|
|
713
|
+
return { ok: false, error: { code, status: null, detail, reason } };
|
|
714
|
+
}
|
|
715
|
+
function asObject(value) {
|
|
716
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
717
|
+
}
|
|
718
|
+
function hasId(value) {
|
|
719
|
+
return asObject(value) !== null && typeof value.id === "string";
|
|
720
|
+
}
|
|
608
721
|
function createPodClient(connection, opts = {}) {
|
|
609
722
|
const now = opts.now ?? Date.now;
|
|
610
723
|
const doFetch = () => opts.fetch ?? fetch;
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
724
|
+
let current = connection;
|
|
725
|
+
let renewal = null;
|
|
726
|
+
function renewOnce(refused2) {
|
|
727
|
+
if (current !== refused2) return Promise.resolve({ ok: true, data: current });
|
|
728
|
+
renewal ??= opts.renew(refused2).then(
|
|
729
|
+
(next) => {
|
|
730
|
+
current = next;
|
|
731
|
+
return { ok: true, data: next };
|
|
732
|
+
},
|
|
733
|
+
(err) => ({ ok: false, error: renewalError(err) })
|
|
734
|
+
);
|
|
735
|
+
return renewal;
|
|
736
|
+
}
|
|
737
|
+
async function request(method, path, body, query, retried = false) {
|
|
738
|
+
const sent = current;
|
|
739
|
+
const url = hubUrl(sent.podUrl, path, query);
|
|
740
|
+
if (!url) return refused("invalid", "invalid_input", "Not a Hub API path: encode each path segment (encodeURIComponent)");
|
|
741
|
+
if (sent.accessExpiresAt <= now()) return { ok: false, error: { code: "expired", status: null } };
|
|
742
|
+
const headers = { Accept: "application/json", Authorization: `Bearer ${sent.accessToken}` };
|
|
615
743
|
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
616
744
|
let res;
|
|
617
745
|
try {
|
|
@@ -619,21 +747,33 @@ function createPodClient(connection, opts = {}) {
|
|
|
619
747
|
method,
|
|
620
748
|
headers,
|
|
621
749
|
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
622
|
-
redirect: "manual"
|
|
750
|
+
redirect: "manual",
|
|
751
|
+
signal: timeoutSignal(opts.timeoutMs ?? TIMEOUT_MS2)
|
|
623
752
|
});
|
|
624
753
|
} catch {
|
|
625
754
|
return { ok: false, error: { code: "pod_unreachable", status: null } };
|
|
626
755
|
}
|
|
756
|
+
const read2 = await readCapped(res, opts.maxResponseBytes ?? MAX_RESPONSE_BYTES);
|
|
757
|
+
if (!read2.ok) {
|
|
758
|
+
return read2.reason === "too_large" ? { ok: false, error: { code: "server", status: res.status, detail: "The pod's answer is larger than maxResponseBytes" } } : { ok: false, error: { code: "pod_unreachable", status: res.status, detail: "The pod's answer broke off" } };
|
|
759
|
+
}
|
|
627
760
|
let data = null;
|
|
628
|
-
|
|
629
|
-
if (text) {
|
|
761
|
+
if (read2.text) {
|
|
630
762
|
try {
|
|
631
|
-
data = JSON.parse(text);
|
|
763
|
+
data = JSON.parse(read2.text);
|
|
632
764
|
} catch {
|
|
633
765
|
data = null;
|
|
634
766
|
}
|
|
635
767
|
}
|
|
636
768
|
if (res.status >= 200 && res.status < 300) return { ok: true, data };
|
|
769
|
+
if (res.status === 401 && sent.refreshToken) {
|
|
770
|
+
if (!opts.renew) return { ok: false, error: { ...errorFor(401, data, null, now()), refreshable: true } };
|
|
771
|
+
if (!retried) {
|
|
772
|
+
const renewed = await renewOnce(sent);
|
|
773
|
+
if (!renewed.ok) return renewed;
|
|
774
|
+
return request(method, path, body, query, true);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
637
777
|
return { ok: false, error: errorFor(res.status, data, res.headers.get("retry-after"), now()) };
|
|
638
778
|
}
|
|
639
779
|
async function write(method, path, body) {
|
|
@@ -646,18 +786,23 @@ function createPodClient(connection, opts = {}) {
|
|
|
646
786
|
ok: true,
|
|
647
787
|
status: "proposed",
|
|
648
788
|
proposalId,
|
|
649
|
-
reviewUrl: reviewUrlFor(
|
|
789
|
+
reviewUrl: reviewUrlFor(current.podUrl, obj.reviewUrl, proposalId),
|
|
650
790
|
data: result.data
|
|
651
791
|
};
|
|
652
792
|
}
|
|
653
793
|
return { ok: true, status: "applied", data: result.data };
|
|
654
794
|
}
|
|
655
795
|
async function listPendingProposals(opts2 = {}) {
|
|
656
|
-
if (!can(
|
|
657
|
-
return
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
796
|
+
if (!can(current, "proposal.read", opts2.workspaceId)) {
|
|
797
|
+
return refused("forbidden", "not_granted", "The connection was not granted proposal.read in that space");
|
|
798
|
+
}
|
|
799
|
+
let only = null;
|
|
800
|
+
if (opts2.proposalIds !== void 0) {
|
|
801
|
+
if (opts2.proposalIds.length > PROPOSAL_IDS_MAX || opts2.proposalIds.some((id) => !ID_RE.test(id))) {
|
|
802
|
+
return refused("invalid", "invalid_input", `proposalIds must be at most ${PROPOSAL_IDS_MAX} proposal ids`);
|
|
803
|
+
}
|
|
804
|
+
if (opts2.proposalIds.length === 0) return { ok: true, data: { total: 0, proposals: [], hasMore: false } };
|
|
805
|
+
only = new Set(opts2.proposalIds);
|
|
661
806
|
}
|
|
662
807
|
const limit = Math.min(Math.max(Math.floor(opts2.limit ?? 20), 1), 200);
|
|
663
808
|
const offset = Math.max(Math.floor(opts2.offset ?? 0), 0);
|
|
@@ -666,7 +811,8 @@ function createPodClient(connection, opts = {}) {
|
|
|
666
811
|
view: "basic",
|
|
667
812
|
limit,
|
|
668
813
|
offset,
|
|
669
|
-
workspaceId: opts2.workspaceId
|
|
814
|
+
workspaceId: opts2.workspaceId,
|
|
815
|
+
...only ? { proposalIds: [...only].join(",") } : {}
|
|
670
816
|
});
|
|
671
817
|
if (!result.ok) return result;
|
|
672
818
|
const obj = result.data && typeof result.data === "object" ? result.data : {};
|
|
@@ -679,7 +825,7 @@ function createPodClient(connection, opts = {}) {
|
|
|
679
825
|
if (!row || typeof row !== "object") continue;
|
|
680
826
|
const r = row;
|
|
681
827
|
const id = str(r.id);
|
|
682
|
-
if (!id) continue;
|
|
828
|
+
if (!id || only && !only.has(id)) continue;
|
|
683
829
|
proposals.push({
|
|
684
830
|
id,
|
|
685
831
|
proposalType: str(r.proposalType) ?? "",
|
|
@@ -690,7 +836,7 @@ function createPodClient(connection, opts = {}) {
|
|
|
690
836
|
agentUserId: str(r.agentUserId),
|
|
691
837
|
sessionId: str(r.sessionId),
|
|
692
838
|
...str(r.summary) ? { summary: str(r.summary) } : {},
|
|
693
|
-
reviewUrl: reviewUrlFor(
|
|
839
|
+
reviewUrl: reviewUrlFor(current.podUrl, null, id)
|
|
694
840
|
});
|
|
695
841
|
}
|
|
696
842
|
return {
|
|
@@ -698,13 +844,131 @@ function createPodClient(connection, opts = {}) {
|
|
|
698
844
|
data: { total: obj.total, proposals, hasMore: obj.hasMore === true || offset + proposals.length < obj.total }
|
|
699
845
|
};
|
|
700
846
|
}
|
|
847
|
+
async function read(path, query, shape) {
|
|
848
|
+
const result = await request("GET", path, void 0, query);
|
|
849
|
+
if (!result.ok) return result;
|
|
850
|
+
const data = shape(result.data);
|
|
851
|
+
if (data === null) return { ok: false, error: { code: "server", status: 200, detail: `The pod's ${path.split("/")[1]} answer is not the expected shape` } };
|
|
852
|
+
return { ok: true, data };
|
|
853
|
+
}
|
|
854
|
+
const badId = (what) => refused("invalid", "invalid_input", `Not a ${what} id`);
|
|
855
|
+
const clamp = (n, min, max) => n === void 0 ? void 0 : Math.min(Math.max(Math.floor(n), min), max);
|
|
856
|
+
function listEntities(opts2 = {}) {
|
|
857
|
+
return read("/entities", {
|
|
858
|
+
workspaceId: opts2.workspaceId,
|
|
859
|
+
profileSlug: opts2.profileSlug,
|
|
860
|
+
limit: clamp(opts2.limit, 1, 1e3),
|
|
861
|
+
offset: clamp(opts2.offset, 0, Number.MAX_SAFE_INTEGER),
|
|
862
|
+
sort: opts2.sort,
|
|
863
|
+
includePodWide: opts2.includePodWide
|
|
864
|
+
}, (data) => Array.isArray(data) ? data.filter(hasId) : null);
|
|
865
|
+
}
|
|
866
|
+
async function getEntity(id) {
|
|
867
|
+
if (!ID_RE.test(id)) return badId("entity");
|
|
868
|
+
return read(`/entities/${id}`, void 0, (data) => hasId(data) ? data : null);
|
|
869
|
+
}
|
|
870
|
+
async function getEntityConnections(id, opts2 = {}) {
|
|
871
|
+
if (!ID_RE.test(id)) return badId("entity");
|
|
872
|
+
return read(`/entities/${id}/connections`, { workspaceId: opts2.workspaceId, limit: clamp(opts2.limit, 1, 200) }, (data) => {
|
|
873
|
+
const obj = asObject(data);
|
|
874
|
+
if (!obj || !Array.isArray(obj.connections)) return null;
|
|
875
|
+
const connections = obj.connections.filter(
|
|
876
|
+
(c) => !!c && typeof c === "object" && typeof c.entityId === "string"
|
|
877
|
+
);
|
|
878
|
+
const counts = asObject(obj.counts) ?? {};
|
|
879
|
+
const count = (key, source) => typeof counts[key] === "number" ? counts[key] : connections.filter((c) => c.source === source).length;
|
|
880
|
+
return {
|
|
881
|
+
connections,
|
|
882
|
+
counts: {
|
|
883
|
+
total: typeof counts.total === "number" ? counts.total : connections.length,
|
|
884
|
+
graph: count("graph", "graph"),
|
|
885
|
+
structural: count("structural", "property"),
|
|
886
|
+
threads: count("threads", "thread"),
|
|
887
|
+
contextChannels: count("contextChannels", "context_channel"),
|
|
888
|
+
focusSessions: count("focusSessions", "focus_session")
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
async function getDocument(id, opts2 = {}) {
|
|
894
|
+
if (!ID_RE.test(id)) return badId("document");
|
|
895
|
+
return read(`/documents/${id}`, { format: opts2.format }, (data) => {
|
|
896
|
+
const doc = asObject(asObject(data)?.document);
|
|
897
|
+
if (!doc || typeof doc.id !== "string" || typeof doc.content !== "string") return null;
|
|
898
|
+
return { ...doc, sections: Array.isArray(doc.sections) ? doc.sections : [] };
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
async function listFocusSessions(opts2) {
|
|
902
|
+
if (!opts2?.workspaceId && !opts2?.projectId) return refused("invalid", "invalid_input", "workspaceId or projectId is required");
|
|
903
|
+
return read("/focus-sessions", {
|
|
904
|
+
workspaceId: opts2.workspaceId,
|
|
905
|
+
projectId: opts2.projectId,
|
|
906
|
+
status: opts2.status,
|
|
907
|
+
limit: clamp(opts2.limit, 1, 50),
|
|
908
|
+
lens: opts2.lens,
|
|
909
|
+
kind: opts2.kind,
|
|
910
|
+
subjectEntityId: opts2.subjectEntityId
|
|
911
|
+
}, (data) => {
|
|
912
|
+
if (!Array.isArray(data)) return null;
|
|
913
|
+
return data.filter(hasId).map((row) => ({
|
|
914
|
+
...row,
|
|
915
|
+
expectedOutputs: Array.isArray(row.expectedOutputs) ? row.expectedOutputs.filter((s) => !!s && typeof s === "object" && typeof s.label === "string") : []
|
|
916
|
+
}));
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
async function getFileUrl(entityId) {
|
|
920
|
+
if (!ID_RE.test(entityId)) return badId("file");
|
|
921
|
+
return read(`/files/${entityId}/url`, void 0, (data) => {
|
|
922
|
+
const obj = asObject(data);
|
|
923
|
+
if (!obj || typeof obj.url !== "string" || typeof obj.expiresAt !== "string") return null;
|
|
924
|
+
return { url: obj.url, expiresAt: obj.expiresAt, mimeType: typeof obj.mimeType === "string" ? obj.mimeType : null };
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
async function approveProposal(id, opts2 = {}) {
|
|
928
|
+
if (connectionActsAs(current) !== "person") {
|
|
929
|
+
return refused("forbidden", "agent_cannot_approve", "An agent connection can never approve: approval is the person's");
|
|
930
|
+
}
|
|
931
|
+
if (!can(current, "proposal.approve", opts2.workspaceId)) {
|
|
932
|
+
return refused("forbidden", "not_granted", "The connection was not granted proposal.approve in that space");
|
|
933
|
+
}
|
|
934
|
+
if (!ID_RE.test(id)) return badId("proposal");
|
|
935
|
+
const result = await request("POST", `/proposals/${id}/approve`, {});
|
|
936
|
+
if (!result.ok) return result;
|
|
937
|
+
const obj = asObject(result.data);
|
|
938
|
+
if (!obj || typeof obj.proposalId !== "string") {
|
|
939
|
+
return { ok: false, error: { code: "server", status: 200, detail: "The pod's approve answer names no proposalId" } };
|
|
940
|
+
}
|
|
941
|
+
return {
|
|
942
|
+
ok: true,
|
|
943
|
+
data: {
|
|
944
|
+
proposalId: obj.proposalId,
|
|
945
|
+
...typeof obj.primaryId === "string" ? { primaryId: obj.primaryId } : {},
|
|
946
|
+
...typeof obj.linked === "number" ? { linked: obj.linked } : {},
|
|
947
|
+
proposal: asObject(obj.proposal)
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
}
|
|
701
951
|
return {
|
|
702
952
|
podUrl: connection.podUrl,
|
|
703
|
-
user
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
953
|
+
get user() {
|
|
954
|
+
return current.user;
|
|
955
|
+
},
|
|
956
|
+
get grant() {
|
|
957
|
+
return current.grant;
|
|
958
|
+
},
|
|
959
|
+
get actsAs() {
|
|
960
|
+
return connectionActsAs(current);
|
|
961
|
+
},
|
|
962
|
+
can: (permission, workspaceId) => can(current, permission, workspaceId),
|
|
963
|
+
spaces: () => spaces(current),
|
|
707
964
|
listPendingProposals,
|
|
965
|
+
listEntities,
|
|
966
|
+
getEntity,
|
|
967
|
+
getEntityConnections,
|
|
968
|
+
getDocument,
|
|
969
|
+
listFocusSessions,
|
|
970
|
+
getFileUrl,
|
|
971
|
+
approveProposal,
|
|
708
972
|
get(path, query) {
|
|
709
973
|
return request("GET", path, void 0, query);
|
|
710
974
|
},
|
|
@@ -727,7 +991,8 @@ function createPodClient(connection, opts = {}) {
|
|
|
727
991
|
var TRANSACTION_TTL_MS = 10 * 60 * 1e3;
|
|
728
992
|
var DEFAULT_REFRESH_IDLE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
729
993
|
var DEFAULT_ACCESS_TTL_SEC = 3600;
|
|
730
|
-
var
|
|
994
|
+
var TIMEOUT_MS3 = 1e4;
|
|
995
|
+
var MAX_TOKEN_RESPONSE_BYTES = 64 * 1024;
|
|
731
996
|
var PERMISSION_RE = /^[a-z][a-z0-9_-]*(\.[a-z0-9_-]+)+$/i;
|
|
732
997
|
function assertPermissions(permissions) {
|
|
733
998
|
const unique = [...new Set(permissions)];
|
|
@@ -845,20 +1110,29 @@ async function tokenRequest(endpoint, body, during, opts) {
|
|
|
845
1110
|
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
|
|
846
1111
|
body: body.toString(),
|
|
847
1112
|
redirect: "manual",
|
|
848
|
-
signal:
|
|
1113
|
+
signal: timeoutSignal(TIMEOUT_MS3)
|
|
849
1114
|
});
|
|
850
1115
|
} catch {
|
|
851
1116
|
throw new ConnectError("pod_unreachable", "The pod's token endpoint did not answer");
|
|
852
1117
|
}
|
|
1118
|
+
if (res.status >= 300 && res.status < 400) {
|
|
1119
|
+
await res.body?.cancel().catch(() => {
|
|
1120
|
+
});
|
|
1121
|
+
throw new ConnectError("pod_moved", `The token endpoint answered ${res.status}: the pod moved; connect again with its new address`);
|
|
1122
|
+
}
|
|
853
1123
|
if (res.status === 429) {
|
|
854
1124
|
throw new ConnectError("rate_limited", "The pod asked to slow down", {
|
|
855
1125
|
retryAfterSec: retryAfterSeconds(res.headers.get("retry-after"), (opts.now ?? Date.now)())
|
|
856
1126
|
});
|
|
857
1127
|
}
|
|
858
1128
|
if (res.status >= 500) throw new ConnectError("pod_unreachable", `The token endpoint answered ${res.status}`);
|
|
1129
|
+
const read = await readCapped(res, MAX_TOKEN_RESPONSE_BYTES);
|
|
1130
|
+
if (!read.ok) {
|
|
1131
|
+
throw read.reason === "too_large" ? new ConnectError("server_error", "The token endpoint's answer is too large") : new ConnectError("pod_unreachable", "The token endpoint's answer broke off");
|
|
1132
|
+
}
|
|
859
1133
|
let json;
|
|
860
1134
|
try {
|
|
861
|
-
json =
|
|
1135
|
+
json = JSON.parse(read.text);
|
|
862
1136
|
} catch {
|
|
863
1137
|
throw new ConnectError("server_error", `The token endpoint answered ${res.status} without JSON`);
|
|
864
1138
|
}
|
|
@@ -874,14 +1148,18 @@ function parseGrant(value) {
|
|
|
874
1148
|
const out = [];
|
|
875
1149
|
for (const g of value) {
|
|
876
1150
|
if (!g || typeof g !== "object") return null;
|
|
877
|
-
const { permission, workspaceId, workspaceName } = g;
|
|
1151
|
+
const { permission, workspaceId, workspaceName, templateSlug } = g;
|
|
878
1152
|
if (typeof permission !== "string" || typeof workspaceId !== "string") return null;
|
|
879
|
-
|
|
1153
|
+
const row = { permission, workspaceId, workspaceName: typeof workspaceName === "string" ? workspaceName : "" };
|
|
1154
|
+
if (typeof templateSlug === "string" && templateSlug.length > 0 && templateSlug.length <= 200) row.templateSlug = templateSlug;
|
|
1155
|
+
out.push(row);
|
|
880
1156
|
}
|
|
881
1157
|
return out;
|
|
882
1158
|
}
|
|
1159
|
+
function parseActsAs(value) {
|
|
1160
|
+
return value === "person" || value === "agent" ? value : void 0;
|
|
1161
|
+
}
|
|
883
1162
|
var APP_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
884
|
-
var LOOPBACK_HOSTS3 = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
885
1163
|
function parseApp(value) {
|
|
886
1164
|
if (!value || typeof value !== "object") return {};
|
|
887
1165
|
const { id, manageUrl } = value;
|
|
@@ -890,7 +1168,7 @@ function parseApp(value) {
|
|
|
890
1168
|
if (typeof manageUrl === "string" && manageUrl.length <= 2048) {
|
|
891
1169
|
try {
|
|
892
1170
|
const url = new URL(manageUrl);
|
|
893
|
-
const secure = url.protocol === "https:" || url.protocol === "http:" &&
|
|
1171
|
+
const secure = url.protocol === "https:" || url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname);
|
|
894
1172
|
if (secure && !url.username && !url.password) out.manageUrl = url.href;
|
|
895
1173
|
} catch {
|
|
896
1174
|
}
|
|
@@ -936,6 +1214,8 @@ function parseTokenResponse(body, ctx) {
|
|
|
936
1214
|
const manageUrl = app.manageUrl ?? ctx.previous?.manageUrl;
|
|
937
1215
|
if (appId !== void 0) connection.appId = appId;
|
|
938
1216
|
if (manageUrl !== void 0) connection.manageUrl = manageUrl;
|
|
1217
|
+
const actsAs = parseActsAs(body.synap_acts_as);
|
|
1218
|
+
if (actsAs !== void 0) connection.actsAs = actsAs;
|
|
939
1219
|
if (typeof refresh_token === "string" && refresh_token !== "") {
|
|
940
1220
|
connection.refreshToken = refresh_token;
|
|
941
1221
|
const idle = typeof refresh_token_expires_in === "number" && Number.isFinite(refresh_token_expires_in) && refresh_token_expires_in > 0 ? refresh_token_expires_in * 1e3 : DEFAULT_REFRESH_IDLE_MS;
|
|
@@ -1001,7 +1281,9 @@ async function revokeConnection(connection, opts = {}) {
|
|
|
1001
1281
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1002
1282
|
body: new URLSearchParams({ token, token_type_hint: hint, client_id: connection.clientId }).toString(),
|
|
1003
1283
|
redirect: "manual",
|
|
1004
|
-
signal:
|
|
1284
|
+
signal: timeoutSignal(TIMEOUT_MS3)
|
|
1285
|
+
});
|
|
1286
|
+
await res.body?.cancel().catch(() => {
|
|
1005
1287
|
});
|
|
1006
1288
|
if (res.status !== 200) revoked = false;
|
|
1007
1289
|
} catch {
|
|
@@ -1043,6 +1325,7 @@ function pack(c) {
|
|
|
1043
1325
|
for (const g of c.grant) {
|
|
1044
1326
|
const row = bySpace.get(g.workspaceId) ?? [g.workspaceId, g.workspaceName, []];
|
|
1045
1327
|
row[2].push(g.permission);
|
|
1328
|
+
if (g.templateSlug && row[3] === void 0) row[3] = g.templateSlug;
|
|
1046
1329
|
bySpace.set(g.workspaceId, row);
|
|
1047
1330
|
}
|
|
1048
1331
|
const { grant: _grant, ...rest } = c;
|
|
@@ -1052,7 +1335,7 @@ function unpack(s) {
|
|
|
1052
1335
|
if (!s || typeof s.podUrl !== "string" || typeof s.accessToken !== "string" || !Array.isArray(s.g)) return null;
|
|
1053
1336
|
const { g, ...rest } = s;
|
|
1054
1337
|
const grant = g.flatMap(
|
|
1055
|
-
([workspaceId, workspaceName, permissions]) => permissions.map((permission) => ({ permission, workspaceId, workspaceName }))
|
|
1338
|
+
([workspaceId, workspaceName, permissions, templateSlug]) => permissions.map((permission) => ({ permission, workspaceId, workspaceName, ...typeof templateSlug === "string" ? { templateSlug } : {} }))
|
|
1056
1339
|
);
|
|
1057
1340
|
return { ...rest, grant };
|
|
1058
1341
|
}
|
|
@@ -1131,9 +1414,30 @@ var DEFAULT_PATHS = {
|
|
|
1131
1414
|
afterConnect: "/"
|
|
1132
1415
|
};
|
|
1133
1416
|
var REFRESH_WINDOW_MS = 5 * 60 * 1e3;
|
|
1134
|
-
var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
1135
1417
|
var TEMPLATE_SLUG_RE = /^[a-z0-9][a-z0-9._/-]{0,99}$/;
|
|
1136
1418
|
var MAX_METADATA_BYTES2 = 5 * 1024;
|
|
1419
|
+
var MAX_REQUESTS = 32;
|
|
1420
|
+
var KEPT_RETRY_MS = 6e4;
|
|
1421
|
+
var GRANT_ACTIONS = ["read", "create", "update", "delete"];
|
|
1422
|
+
var SYNAP_APP_REVIEW_PERMISSIONS = ["proposal.approve", "proposal.reject"];
|
|
1423
|
+
var PERMISSION_SEGMENT_RE = /^[a-z0-9][a-z0-9_-]*$/;
|
|
1424
|
+
function isSynapAppOrigin(origin) {
|
|
1425
|
+
return /^https:\/\/[a-z0-9]([a-z0-9-]*[a-z0-9])?\.synap\.live$/.test(origin);
|
|
1426
|
+
}
|
|
1427
|
+
function assertAppPermission(permission, origin) {
|
|
1428
|
+
const segs = permission.split(".");
|
|
1429
|
+
const fail = (why) => {
|
|
1430
|
+
throw new ConnectError("misconfigured", `requests: "${permission}" ${why}`);
|
|
1431
|
+
};
|
|
1432
|
+
if (segs.length < 2 || segs.length > 3 || segs.some((seg) => !PERMISSION_SEGMENT_RE.test(seg))) {
|
|
1433
|
+
fail("is not <subject>[.<kind>].<action> in lowercase (no *)");
|
|
1434
|
+
}
|
|
1435
|
+
if (segs.length === 3 && segs[0] !== "entity") fail(`names a kind, but "${segs[0]}" has none (only entity does)`);
|
|
1436
|
+
if (SYNAP_APP_REVIEW_PERMISSIONS.includes(permission) && isSynapAppOrigin(origin)) return;
|
|
1437
|
+
if (!GRANT_ACTIONS.includes(segs[segs.length - 1])) {
|
|
1438
|
+
fail(`asks for "${segs[segs.length - 1]}", which an app cannot be granted (read, create, update, delete)`);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1137
1441
|
function appOriginOf(appUrl) {
|
|
1138
1442
|
let url;
|
|
1139
1443
|
try {
|
|
@@ -1142,8 +1446,11 @@ function appOriginOf(appUrl) {
|
|
|
1142
1446
|
throw new ConnectError("misconfigured", "appUrl is not a URL");
|
|
1143
1447
|
}
|
|
1144
1448
|
const secure = url.protocol === "https:";
|
|
1145
|
-
if (!secure && !(url.protocol === "http:" &&
|
|
1146
|
-
throw new ConnectError("misconfigured", "appUrl must be https (http only on localhost)");
|
|
1449
|
+
if (!secure && !(url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname))) {
|
|
1450
|
+
throw new ConnectError("misconfigured", "appUrl must be https (http only on localhost, for the SDK's mock pod)");
|
|
1451
|
+
}
|
|
1452
|
+
if (secure && isIpLiteral(url.hostname)) {
|
|
1453
|
+
throw new ConnectError("misconfigured", "appUrl must use a domain name: the pod refuses an IP address client_id");
|
|
1147
1454
|
}
|
|
1148
1455
|
if (url.pathname !== "/" || url.search || url.hash || url.username || url.password) {
|
|
1149
1456
|
throw new ConnectError("misconfigured", "appUrl is an origin: no path, query, fragment or credentials");
|
|
@@ -1161,9 +1468,12 @@ function buildClientMetadata(opts) {
|
|
|
1161
1468
|
for (const [name, path] of Object.entries(paths)) assertPath(name, path);
|
|
1162
1469
|
const clientName = opts.clientName.trim();
|
|
1163
1470
|
if (!clientName || clientName.length > 100) throw new ConnectError("misconfigured", "clientName must be 1-100 characters");
|
|
1471
|
+
if (opts.requests.length === 0 || opts.requests.length > MAX_REQUESTS) {
|
|
1472
|
+
throw new ConnectError("misconfigured", `requests must hold 1-${MAX_REQUESTS} entries (duplicates count)`);
|
|
1473
|
+
}
|
|
1164
1474
|
assertPermissions(opts.requests.map((r) => r.permission));
|
|
1165
1475
|
for (const r of opts.requests) {
|
|
1166
|
-
|
|
1476
|
+
assertAppPermission(r.permission, origin);
|
|
1167
1477
|
if (typeof r.reason !== "string" || !r.reason.trim() || r.reason.length > 200) {
|
|
1168
1478
|
throw new ConnectError("misconfigured", `the reason for ${r.permission} must be 1-200 characters`);
|
|
1169
1479
|
}
|
|
@@ -1262,7 +1572,7 @@ function createPodAppAuth(config) {
|
|
|
1262
1572
|
permissions,
|
|
1263
1573
|
secret: config.secret,
|
|
1264
1574
|
resource: config.resource,
|
|
1265
|
-
spaceHint: opts.space &&
|
|
1575
|
+
spaceHint: opts.space && SPACE_HINT_RE.test(opts.space) ? opts.space : null,
|
|
1266
1576
|
next: opts.next,
|
|
1267
1577
|
prompt: opts.prompt,
|
|
1268
1578
|
allowInsecureLoopback: config.allowInsecureLoopback
|
|
@@ -1341,7 +1651,7 @@ function createPodAppAuth(config) {
|
|
|
1341
1651
|
clientId,
|
|
1342
1652
|
secret: config.secret,
|
|
1343
1653
|
next,
|
|
1344
|
-
spaceHint: space &&
|
|
1654
|
+
spaceHint: space && SPACE_HINT_RE.test(space) ? space : null,
|
|
1345
1655
|
prompt: formField(form, "prompt") === "consent" ? "consent" : null,
|
|
1346
1656
|
now
|
|
1347
1657
|
});
|
|
@@ -1374,7 +1684,8 @@ function createPodAppAuth(config) {
|
|
|
1374
1684
|
return redirect(withQuery(origin, paths.connectPage, { error: e.code, pod: pod ? new URL(pod).host : null }), [clearCloud]);
|
|
1375
1685
|
}
|
|
1376
1686
|
}
|
|
1377
|
-
|
|
1687
|
+
const keptFailures = /* @__PURE__ */ new Map();
|
|
1688
|
+
async function refreshIfNeeded(request, opts = {}) {
|
|
1378
1689
|
const connection = await store.read(request);
|
|
1379
1690
|
const unchanged = (status, conn, error) => ({
|
|
1380
1691
|
status,
|
|
@@ -1389,18 +1700,27 @@ function createPodAppAuth(config) {
|
|
|
1389
1700
|
}
|
|
1390
1701
|
if (isPrefetchOrAsset(request)) return unchanged("skipped", connection);
|
|
1391
1702
|
const t = now();
|
|
1392
|
-
const accessLives = connection.accessExpiresAt > t;
|
|
1393
|
-
if (connection.accessExpiresAt - t > REFRESH_WINDOW_MS) return unchanged("fresh", connection);
|
|
1703
|
+
const accessLives = !opts.force && connection.accessExpiresAt > t;
|
|
1704
|
+
if (!opts.force && connection.accessExpiresAt - t > REFRESH_WINDOW_MS) return unchanged("fresh", connection);
|
|
1394
1705
|
if (!connection.refreshToken) {
|
|
1395
1706
|
return accessLives ? unchanged("fresh", connection) : unchanged("expired", null, new ConnectError("expired", "No refresh token; connect again"));
|
|
1396
1707
|
}
|
|
1708
|
+
for (const [token, f] of keptFailures) if (t - f.at >= KEPT_RETRY_MS) keptFailures.delete(token);
|
|
1709
|
+
const keptFailure = accessLives ? keptFailures.get(connection.refreshToken) : void 0;
|
|
1710
|
+
if (keptFailure) return unchanged("kept", connection, keptFailure.error);
|
|
1397
1711
|
let next;
|
|
1398
1712
|
try {
|
|
1399
1713
|
next = await refreshConnection(connection, flow);
|
|
1400
1714
|
} catch (err) {
|
|
1401
1715
|
const e = fail(err, "refresh");
|
|
1402
|
-
if (accessLives)
|
|
1403
|
-
|
|
1716
|
+
if (accessLives) {
|
|
1717
|
+
keptFailures.set(connection.refreshToken, { error: e, at: t });
|
|
1718
|
+
return unchanged("kept", connection, e);
|
|
1719
|
+
}
|
|
1720
|
+
if (e.code === "revoked" || e.code === "expired" || e.code === "pod_moved") {
|
|
1721
|
+
const cleared = await store.clear(request).catch(() => []);
|
|
1722
|
+
return { status: e.code === "revoked" ? "revoked" : "expired", connection: null, setCookies: cleared, cookieHeader: applySetCookies(request.headers.get("cookie"), cleared), error: e };
|
|
1723
|
+
}
|
|
1404
1724
|
return unchanged("unavailable", null, e);
|
|
1405
1725
|
}
|
|
1406
1726
|
let setCookies;
|
|
@@ -1419,6 +1739,24 @@ function createPodAppAuth(config) {
|
|
|
1419
1739
|
cookieHeader: applySetCookies(request.headers.get("cookie"), setCookies)
|
|
1420
1740
|
};
|
|
1421
1741
|
}
|
|
1742
|
+
function renewer(persist, source) {
|
|
1743
|
+
return async (connection) => {
|
|
1744
|
+
let next;
|
|
1745
|
+
try {
|
|
1746
|
+
next = await refreshConnection(connection, flow);
|
|
1747
|
+
} catch (err) {
|
|
1748
|
+
throw fail(err, "refresh");
|
|
1749
|
+
}
|
|
1750
|
+
try {
|
|
1751
|
+
const cookies = await store.write(next, asRequest(source ?? null));
|
|
1752
|
+
await persist([...cookies, marker(next.podUrl)]);
|
|
1753
|
+
} catch (err) {
|
|
1754
|
+
fail(err, "refresh");
|
|
1755
|
+
throw new ConnectError("expired", "The renewed connection could not be stored; connect again");
|
|
1756
|
+
}
|
|
1757
|
+
return next;
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1422
1760
|
return {
|
|
1423
1761
|
clientId,
|
|
1424
1762
|
redirectUri,
|
|
@@ -1432,7 +1770,15 @@ function createPodAppAuth(config) {
|
|
|
1432
1770
|
return await store.read(request) ? null : lapsedIn(request);
|
|
1433
1771
|
},
|
|
1434
1772
|
refreshIfNeeded,
|
|
1435
|
-
client
|
|
1773
|
+
client(connection, opts = {}) {
|
|
1774
|
+
const { persist, request, ...clientOpts } = opts;
|
|
1775
|
+
return createPodClient(connection, {
|
|
1776
|
+
fetch: config.fetch,
|
|
1777
|
+
now: config.now,
|
|
1778
|
+
...persist ? { renew: renewer(persist, request) } : {},
|
|
1779
|
+
...clientOpts
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1436
1782
|
};
|
|
1437
1783
|
}
|
|
1438
1784
|
|
|
@@ -1458,6 +1804,7 @@ assertServerRuntime();
|
|
|
1458
1804
|
canonicalPodUrl,
|
|
1459
1805
|
cloudBaseUrl,
|
|
1460
1806
|
cloudConnectAvailable,
|
|
1807
|
+
connectionActsAs,
|
|
1461
1808
|
connectionExpiresAt,
|
|
1462
1809
|
cookieNames,
|
|
1463
1810
|
createPodAppAuth,
|
|
@@ -1465,6 +1812,7 @@ assertServerRuntime();
|
|
|
1465
1812
|
createSealedCookieStore,
|
|
1466
1813
|
discoverPod,
|
|
1467
1814
|
handleCallback,
|
|
1815
|
+
humanSlots,
|
|
1468
1816
|
isBrowserRuntime,
|
|
1469
1817
|
isConnectError,
|
|
1470
1818
|
isPrefetchOrAsset,
|