@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README.md +136 -10
  3. package/dist/{chunk-J7XHAAFJ.js → chunk-UK2U6SMA.js} +14 -2
  4. package/dist/chunk-UK2U6SMA.js.map +1 -0
  5. package/dist/{client-C5sF20f1.d.cts → client-B50KC2aJ.d.cts} +14 -9
  6. package/dist/{client-C5sF20f1.d.ts → client-B50KC2aJ.d.ts} +14 -9
  7. package/dist/connect.cjs +412 -64
  8. package/dist/connect.cjs.map +1 -1
  9. package/dist/connect.d.cts +400 -32
  10. package/dist/connect.d.ts +400 -32
  11. package/dist/connect.js +407 -65
  12. package/dist/connect.js.map +1 -1
  13. package/dist/public.cjs +9 -2
  14. package/dist/public.cjs.map +1 -1
  15. package/dist/public.d.cts +19 -4
  16. package/dist/public.d.ts +19 -4
  17. package/dist/public.js +8 -2
  18. package/dist/public.js.map +1 -1
  19. package/dist/testing.cjs +234 -12
  20. package/dist/testing.cjs.map +1 -1
  21. package/dist/testing.d.cts +122 -27
  22. package/dist/testing.d.ts +122 -27
  23. package/dist/testing.js +230 -12
  24. package/dist/testing.js.map +1 -1
  25. package/dist/{types-C2KGNXEn.d.cts → types-DZYNBCq7.d.cts} +28 -5
  26. package/dist/{types-C2KGNXEn.d.ts → types-DZYNBCq7.d.ts} +28 -5
  27. package/package.json +30 -6
  28. package/src/connect/auth.ts +156 -27
  29. package/src/connect/client.ts +374 -34
  30. package/src/connect/cloud.ts +14 -10
  31. package/src/connect/discovery.ts +66 -32
  32. package/src/connect/encoding.ts +1 -1
  33. package/src/connect/errors.ts +7 -0
  34. package/src/connect/flow.ts +42 -14
  35. package/src/connect/http.ts +42 -0
  36. package/src/connect/hub-types.ts +254 -0
  37. package/src/connect/index.ts +60 -42
  38. package/src/connect/rules.ts +18 -0
  39. package/src/connect/store.ts +9 -5
  40. package/src/connect/types.ts +28 -4
  41. package/src/public/client.ts +14 -2
  42. package/src/public/safe-markdown.ts +13 -0
  43. package/src/public/share-model.ts +6 -2
  44. package/src/public/site-model.ts +5 -3
  45. package/src/testing/auth-server.ts +239 -14
  46. package/src/testing/cloud.ts +3 -4
  47. package/src/testing/fixtures.ts +26 -0
  48. package/src/testing/index.ts +8 -6
  49. package/src/testing/seed.ts +120 -0
  50. 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 text = await res.text();
203
- if (text.length > MAX_METADATA_BYTES) throw new ConnectError("not_a_pod", "Metadata document too large");
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: typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(TIMEOUT_MS) : void 0
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) throw new ConnectError("not_a_pod", `No authorization server metadata (${res.status})`);
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
- const url = new URL(first);
261
- const retry = err instanceof ConnectError && (err.code === "not_a_pod" || err.code === "pod_unreachable") && !url.hostname.startsWith("pod.") && url.pathname === "/" && url.hostname.includes(".");
262
- if (!retry) throw err;
263
- const second = `${url.protocol}//pod.${url.host}`;
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(second, opts);
266
- } catch (err2) {
267
- throw err2 instanceof ConnectError && err2.code === "pod_unsupported" ? err2 : err;
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 && /^[A-Za-z0-9_-]{1,64}$/.test(hint) ? hint : null,
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 && /^[A-Za-z0-9._~-]{16,256}$/.test(state) ? state : null
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 = LOOPBACK_HOSTS2.has(url.hostname);
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 || !STATE_RE.test(state) || !constantTimeEqual(state, tx.state)) {
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
- throw new TypeError(`Not a Hub API path: ${path}`);
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
- const url = new URL(`${podUrl}/api/hub${path}`);
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
- async function request(method, path, body, query) {
612
- const url = hubUrl(connection.podUrl, path, query);
613
- if (connection.accessExpiresAt <= now()) return { ok: false, error: { code: "expired", status: null } };
614
- const headers = { Accept: "application/json", Authorization: `Bearer ${connection.accessToken}` };
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
- const text = await res.text().catch(() => "");
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(connection.podUrl, obj.reviewUrl, proposalId),
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(connection, "proposal.read", opts2.workspaceId)) {
657
- return {
658
- ok: false,
659
- error: { code: "forbidden", status: null, detail: "The connection was not granted proposal.read in that space" }
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(connection.podUrl, null, id)
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: connection.user,
704
- grant: connection.grant,
705
- can: (permission, workspaceId) => can(connection, permission, workspaceId),
706
- spaces: () => spaces(connection),
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 TIMEOUT_MS2 = 1e4;
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: typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(TIMEOUT_MS2) : void 0
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 = await res.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
- out.push({ permission, workspaceId, workspaceName: typeof workspaceName === "string" ? workspaceName : "" });
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:" && LOOPBACK_HOSTS3.has(url.hostname);
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: typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(TIMEOUT_MS2) : void 0
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:" && LOOPBACK.has(url.hostname))) {
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
- if (r.permission.includes("*")) throw new ConnectError("misconfigured", "a permission cannot contain *");
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 && /^[A-Za-z0-9_-]{1,64}$/.test(opts.space) ? opts.space : null,
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 && /^[A-Za-z0-9_-]{1,64}$/.test(space) ? space : null,
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
- async function refreshIfNeeded(request) {
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) return unchanged("kept", connection, e);
1403
- if (e.code === "revoked" || e.code === "expired") return unchanged(e.code, null, e);
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: (connection, opts) => createPodClient(connection, { fetch: config.fetch, now: config.now, ...opts })
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,