@synap-core/sdk 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +215 -0
  2. package/README.md +152 -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 +419 -64
  8. package/dist/connect.cjs.map +1 -1
  9. package/dist/connect.d.cts +416 -32
  10. package/dist/connect.d.ts +416 -32
  11. package/dist/connect.js +414 -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 +350 -15
  20. package/dist/testing.cjs.map +1 -1
  21. package/dist/testing.d.cts +171 -27
  22. package/dist/testing.d.ts +171 -27
  23. package/dist/testing.js +346 -15
  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 +169 -27
  29. package/src/connect/client.ts +377 -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 +271 -0
  37. package/src/connect/index.ts +61 -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 +350 -17
  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/roles.ts +27 -0
  50. package/src/testing/seed.ts +196 -0
  51. 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,134 @@ 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
+ facetSlug: opts2.facetSlug,
865
+ // Sent only when asked: the default request stays the 1.5.0 one.
866
+ includeFacets: opts2.includeFacets ? true : void 0
867
+ }, (data) => Array.isArray(data) ? data.filter(hasId) : null);
868
+ }
869
+ async function getEntity(id) {
870
+ if (!ID_RE.test(id)) return badId("entity");
871
+ return read(`/entities/${id}`, void 0, (data) => hasId(data) ? data : null);
872
+ }
873
+ async function getEntityConnections(id, opts2 = {}) {
874
+ if (!ID_RE.test(id)) return badId("entity");
875
+ return read(`/entities/${id}/connections`, { workspaceId: opts2.workspaceId, limit: clamp(opts2.limit, 1, 200) }, (data) => {
876
+ const obj = asObject(data);
877
+ if (!obj || !Array.isArray(obj.connections)) return null;
878
+ const connections = obj.connections.filter(
879
+ (c) => !!c && typeof c === "object" && typeof c.entityId === "string"
880
+ );
881
+ const counts = asObject(obj.counts) ?? {};
882
+ const count = (key, source) => typeof counts[key] === "number" ? counts[key] : connections.filter((c) => c.source === source).length;
883
+ return {
884
+ connections,
885
+ counts: {
886
+ total: typeof counts.total === "number" ? counts.total : connections.length,
887
+ graph: count("graph", "graph"),
888
+ structural: count("structural", "property"),
889
+ threads: count("threads", "thread"),
890
+ contextChannels: count("contextChannels", "context_channel"),
891
+ focusSessions: count("focusSessions", "focus_session")
892
+ }
893
+ };
894
+ });
895
+ }
896
+ async function getDocument(id, opts2 = {}) {
897
+ if (!ID_RE.test(id)) return badId("document");
898
+ return read(`/documents/${id}`, { format: opts2.format }, (data) => {
899
+ const doc = asObject(asObject(data)?.document);
900
+ if (!doc || typeof doc.id !== "string" || typeof doc.content !== "string") return null;
901
+ return { ...doc, sections: Array.isArray(doc.sections) ? doc.sections : [] };
902
+ });
903
+ }
904
+ async function listFocusSessions(opts2) {
905
+ if (!opts2?.workspaceId && !opts2?.projectId) return refused("invalid", "invalid_input", "workspaceId or projectId is required");
906
+ return read("/focus-sessions", {
907
+ workspaceId: opts2.workspaceId,
908
+ projectId: opts2.projectId,
909
+ status: opts2.status,
910
+ limit: clamp(opts2.limit, 1, 50),
911
+ lens: opts2.lens,
912
+ kind: opts2.kind,
913
+ subjectEntityId: opts2.subjectEntityId
914
+ }, (data) => {
915
+ if (!Array.isArray(data)) return null;
916
+ return data.filter(hasId).map((row) => ({
917
+ ...row,
918
+ expectedOutputs: Array.isArray(row.expectedOutputs) ? row.expectedOutputs.filter((s) => !!s && typeof s === "object" && typeof s.label === "string") : []
919
+ }));
920
+ });
921
+ }
922
+ async function getFileUrl(entityId) {
923
+ if (!ID_RE.test(entityId)) return badId("file");
924
+ return read(`/files/${entityId}/url`, void 0, (data) => {
925
+ const obj = asObject(data);
926
+ if (!obj || typeof obj.url !== "string" || typeof obj.expiresAt !== "string") return null;
927
+ return { url: obj.url, expiresAt: obj.expiresAt, mimeType: typeof obj.mimeType === "string" ? obj.mimeType : null };
928
+ });
929
+ }
930
+ async function approveProposal(id, opts2 = {}) {
931
+ if (connectionActsAs(current) !== "person") {
932
+ return refused("forbidden", "agent_cannot_approve", "An agent connection can never approve: approval is the person's");
933
+ }
934
+ if (!can(current, "proposal.approve", opts2.workspaceId)) {
935
+ return refused("forbidden", "not_granted", "The connection was not granted proposal.approve in that space");
936
+ }
937
+ if (!ID_RE.test(id)) return badId("proposal");
938
+ const result = await request("POST", `/proposals/${id}/approve`, {});
939
+ if (!result.ok) return result;
940
+ const obj = asObject(result.data);
941
+ if (!obj || typeof obj.proposalId !== "string") {
942
+ return { ok: false, error: { code: "server", status: 200, detail: "The pod's approve answer names no proposalId" } };
943
+ }
944
+ return {
945
+ ok: true,
946
+ data: {
947
+ proposalId: obj.proposalId,
948
+ ...typeof obj.primaryId === "string" ? { primaryId: obj.primaryId } : {},
949
+ ...typeof obj.linked === "number" ? { linked: obj.linked } : {},
950
+ proposal: asObject(obj.proposal)
951
+ }
952
+ };
953
+ }
701
954
  return {
702
955
  podUrl: connection.podUrl,
703
- user: connection.user,
704
- grant: connection.grant,
705
- can: (permission, workspaceId) => can(connection, permission, workspaceId),
706
- spaces: () => spaces(connection),
956
+ get user() {
957
+ return current.user;
958
+ },
959
+ get grant() {
960
+ return current.grant;
961
+ },
962
+ get actsAs() {
963
+ return connectionActsAs(current);
964
+ },
965
+ can: (permission, workspaceId) => can(current, permission, workspaceId),
966
+ spaces: () => spaces(current),
707
967
  listPendingProposals,
968
+ listEntities,
969
+ getEntity,
970
+ getEntityConnections,
971
+ getDocument,
972
+ listFocusSessions,
973
+ getFileUrl,
974
+ approveProposal,
708
975
  get(path, query) {
709
976
  return request("GET", path, void 0, query);
710
977
  },
@@ -727,7 +994,8 @@ function createPodClient(connection, opts = {}) {
727
994
  var TRANSACTION_TTL_MS = 10 * 60 * 1e3;
728
995
  var DEFAULT_REFRESH_IDLE_MS = 30 * 24 * 60 * 60 * 1e3;
729
996
  var DEFAULT_ACCESS_TTL_SEC = 3600;
730
- var TIMEOUT_MS2 = 1e4;
997
+ var TIMEOUT_MS3 = 1e4;
998
+ var MAX_TOKEN_RESPONSE_BYTES = 64 * 1024;
731
999
  var PERMISSION_RE = /^[a-z][a-z0-9_-]*(\.[a-z0-9_-]+)+$/i;
732
1000
  function assertPermissions(permissions) {
733
1001
  const unique = [...new Set(permissions)];
@@ -845,20 +1113,29 @@ async function tokenRequest(endpoint, body, during, opts) {
845
1113
  headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
846
1114
  body: body.toString(),
847
1115
  redirect: "manual",
848
- signal: typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(TIMEOUT_MS2) : void 0
1116
+ signal: timeoutSignal(TIMEOUT_MS3)
849
1117
  });
850
1118
  } catch {
851
1119
  throw new ConnectError("pod_unreachable", "The pod's token endpoint did not answer");
852
1120
  }
1121
+ if (res.status >= 300 && res.status < 400) {
1122
+ await res.body?.cancel().catch(() => {
1123
+ });
1124
+ throw new ConnectError("pod_moved", `The token endpoint answered ${res.status}: the pod moved; connect again with its new address`);
1125
+ }
853
1126
  if (res.status === 429) {
854
1127
  throw new ConnectError("rate_limited", "The pod asked to slow down", {
855
1128
  retryAfterSec: retryAfterSeconds(res.headers.get("retry-after"), (opts.now ?? Date.now)())
856
1129
  });
857
1130
  }
858
1131
  if (res.status >= 500) throw new ConnectError("pod_unreachable", `The token endpoint answered ${res.status}`);
1132
+ const read = await readCapped(res, MAX_TOKEN_RESPONSE_BYTES);
1133
+ if (!read.ok) {
1134
+ 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");
1135
+ }
859
1136
  let json;
860
1137
  try {
861
- json = await res.json();
1138
+ json = JSON.parse(read.text);
862
1139
  } catch {
863
1140
  throw new ConnectError("server_error", `The token endpoint answered ${res.status} without JSON`);
864
1141
  }
@@ -874,14 +1151,18 @@ function parseGrant(value) {
874
1151
  const out = [];
875
1152
  for (const g of value) {
876
1153
  if (!g || typeof g !== "object") return null;
877
- const { permission, workspaceId, workspaceName } = g;
1154
+ const { permission, workspaceId, workspaceName, templateSlug } = g;
878
1155
  if (typeof permission !== "string" || typeof workspaceId !== "string") return null;
879
- out.push({ permission, workspaceId, workspaceName: typeof workspaceName === "string" ? workspaceName : "" });
1156
+ const row = { permission, workspaceId, workspaceName: typeof workspaceName === "string" ? workspaceName : "" };
1157
+ if (typeof templateSlug === "string" && templateSlug.length > 0 && templateSlug.length <= 200) row.templateSlug = templateSlug;
1158
+ out.push(row);
880
1159
  }
881
1160
  return out;
882
1161
  }
1162
+ function parseActsAs(value) {
1163
+ return value === "person" || value === "agent" ? value : void 0;
1164
+ }
883
1165
  var APP_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
884
- var LOOPBACK_HOSTS3 = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
885
1166
  function parseApp(value) {
886
1167
  if (!value || typeof value !== "object") return {};
887
1168
  const { id, manageUrl } = value;
@@ -890,7 +1171,7 @@ function parseApp(value) {
890
1171
  if (typeof manageUrl === "string" && manageUrl.length <= 2048) {
891
1172
  try {
892
1173
  const url = new URL(manageUrl);
893
- const secure = url.protocol === "https:" || url.protocol === "http:" && LOOPBACK_HOSTS3.has(url.hostname);
1174
+ const secure = url.protocol === "https:" || url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname);
894
1175
  if (secure && !url.username && !url.password) out.manageUrl = url.href;
895
1176
  } catch {
896
1177
  }
@@ -936,6 +1217,8 @@ function parseTokenResponse(body, ctx) {
936
1217
  const manageUrl = app.manageUrl ?? ctx.previous?.manageUrl;
937
1218
  if (appId !== void 0) connection.appId = appId;
938
1219
  if (manageUrl !== void 0) connection.manageUrl = manageUrl;
1220
+ const actsAs = parseActsAs(body.synap_acts_as);
1221
+ if (actsAs !== void 0) connection.actsAs = actsAs;
939
1222
  if (typeof refresh_token === "string" && refresh_token !== "") {
940
1223
  connection.refreshToken = refresh_token;
941
1224
  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 +1284,9 @@ async function revokeConnection(connection, opts = {}) {
1001
1284
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
1002
1285
  body: new URLSearchParams({ token, token_type_hint: hint, client_id: connection.clientId }).toString(),
1003
1286
  redirect: "manual",
1004
- signal: typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(TIMEOUT_MS2) : void 0
1287
+ signal: timeoutSignal(TIMEOUT_MS3)
1288
+ });
1289
+ await res.body?.cancel().catch(() => {
1005
1290
  });
1006
1291
  if (res.status !== 200) revoked = false;
1007
1292
  } catch {
@@ -1043,6 +1328,7 @@ function pack(c) {
1043
1328
  for (const g of c.grant) {
1044
1329
  const row = bySpace.get(g.workspaceId) ?? [g.workspaceId, g.workspaceName, []];
1045
1330
  row[2].push(g.permission);
1331
+ if (g.templateSlug && row[3] === void 0) row[3] = g.templateSlug;
1046
1332
  bySpace.set(g.workspaceId, row);
1047
1333
  }
1048
1334
  const { grant: _grant, ...rest } = c;
@@ -1052,7 +1338,7 @@ function unpack(s) {
1052
1338
  if (!s || typeof s.podUrl !== "string" || typeof s.accessToken !== "string" || !Array.isArray(s.g)) return null;
1053
1339
  const { g, ...rest } = s;
1054
1340
  const grant = g.flatMap(
1055
- ([workspaceId, workspaceName, permissions]) => permissions.map((permission) => ({ permission, workspaceId, workspaceName }))
1341
+ ([workspaceId, workspaceName, permissions, templateSlug]) => permissions.map((permission) => ({ permission, workspaceId, workspaceName, ...typeof templateSlug === "string" ? { templateSlug } : {} }))
1056
1342
  );
1057
1343
  return { ...rest, grant };
1058
1344
  }
@@ -1131,9 +1417,34 @@ var DEFAULT_PATHS = {
1131
1417
  afterConnect: "/"
1132
1418
  };
1133
1419
  var REFRESH_WINDOW_MS = 5 * 60 * 1e3;
1134
- var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
1135
1420
  var TEMPLATE_SLUG_RE = /^[a-z0-9][a-z0-9._/-]{0,99}$/;
1136
1421
  var MAX_METADATA_BYTES2 = 5 * 1024;
1422
+ var MAX_REQUESTS = 32;
1423
+ var KEPT_RETRY_MS = 6e4;
1424
+ var GRANT_ACTIONS = ["read", "create", "update", "delete"];
1425
+ var SYNAP_APP_REVIEW_PERMISSIONS = ["proposal.approve", "proposal.reject"];
1426
+ var PERMISSION_SEGMENT_RE = /^[a-z0-9][a-z0-9_-]*$/;
1427
+ function isSynapAppOrigin(origin) {
1428
+ return /^https:\/\/[a-z0-9]([a-z0-9-]*[a-z0-9])?\.synap\.live$/.test(origin);
1429
+ }
1430
+ function isMockModeOrigin(origin) {
1431
+ const url = new URL(origin);
1432
+ return url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname);
1433
+ }
1434
+ function assertAppPermission(permission, origin) {
1435
+ const segs = permission.split(".");
1436
+ const fail = (why) => {
1437
+ throw new ConnectError("misconfigured", `requests: "${permission}" ${why}`);
1438
+ };
1439
+ if (segs.length < 2 || segs.length > 3 || segs.some((seg) => !PERMISSION_SEGMENT_RE.test(seg))) {
1440
+ fail("is not <subject>[.<kind>].<action> in lowercase (no *)");
1441
+ }
1442
+ if (segs.length === 3 && segs[0] !== "entity") fail(`names a kind, but "${segs[0]}" has none (only entity does)`);
1443
+ if (SYNAP_APP_REVIEW_PERMISSIONS.includes(permission) && (isSynapAppOrigin(origin) || isMockModeOrigin(origin))) return;
1444
+ if (!GRANT_ACTIONS.includes(segs[segs.length - 1])) {
1445
+ fail(`asks for "${segs[segs.length - 1]}", which an app cannot be granted (read, create, update, delete)`);
1446
+ }
1447
+ }
1137
1448
  function appOriginOf(appUrl) {
1138
1449
  let url;
1139
1450
  try {
@@ -1142,8 +1453,11 @@ function appOriginOf(appUrl) {
1142
1453
  throw new ConnectError("misconfigured", "appUrl is not a URL");
1143
1454
  }
1144
1455
  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)");
1456
+ if (!secure && !(url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname))) {
1457
+ throw new ConnectError("misconfigured", "appUrl must be https (http only on localhost, for the SDK's mock pod)");
1458
+ }
1459
+ if (secure && isIpLiteral(url.hostname)) {
1460
+ throw new ConnectError("misconfigured", "appUrl must use a domain name: the pod refuses an IP address client_id");
1147
1461
  }
1148
1462
  if (url.pathname !== "/" || url.search || url.hash || url.username || url.password) {
1149
1463
  throw new ConnectError("misconfigured", "appUrl is an origin: no path, query, fragment or credentials");
@@ -1161,9 +1475,12 @@ function buildClientMetadata(opts) {
1161
1475
  for (const [name, path] of Object.entries(paths)) assertPath(name, path);
1162
1476
  const clientName = opts.clientName.trim();
1163
1477
  if (!clientName || clientName.length > 100) throw new ConnectError("misconfigured", "clientName must be 1-100 characters");
1478
+ if (opts.requests.length === 0 || opts.requests.length > MAX_REQUESTS) {
1479
+ throw new ConnectError("misconfigured", `requests must hold 1-${MAX_REQUESTS} entries (duplicates count)`);
1480
+ }
1164
1481
  assertPermissions(opts.requests.map((r) => r.permission));
1165
1482
  for (const r of opts.requests) {
1166
- if (r.permission.includes("*")) throw new ConnectError("misconfigured", "a permission cannot contain *");
1483
+ assertAppPermission(r.permission, origin);
1167
1484
  if (typeof r.reason !== "string" || !r.reason.trim() || r.reason.length > 200) {
1168
1485
  throw new ConnectError("misconfigured", `the reason for ${r.permission} must be 1-200 characters`);
1169
1486
  }
@@ -1262,7 +1579,7 @@ function createPodAppAuth(config) {
1262
1579
  permissions,
1263
1580
  secret: config.secret,
1264
1581
  resource: config.resource,
1265
- spaceHint: opts.space && /^[A-Za-z0-9_-]{1,64}$/.test(opts.space) ? opts.space : null,
1582
+ spaceHint: opts.space && SPACE_HINT_RE.test(opts.space) ? opts.space : null,
1266
1583
  next: opts.next,
1267
1584
  prompt: opts.prompt,
1268
1585
  allowInsecureLoopback: config.allowInsecureLoopback
@@ -1341,7 +1658,7 @@ function createPodAppAuth(config) {
1341
1658
  clientId,
1342
1659
  secret: config.secret,
1343
1660
  next,
1344
- spaceHint: space && /^[A-Za-z0-9_-]{1,64}$/.test(space) ? space : null,
1661
+ spaceHint: space && SPACE_HINT_RE.test(space) ? space : null,
1345
1662
  prompt: formField(form, "prompt") === "consent" ? "consent" : null,
1346
1663
  now
1347
1664
  });
@@ -1374,7 +1691,8 @@ function createPodAppAuth(config) {
1374
1691
  return redirect(withQuery(origin, paths.connectPage, { error: e.code, pod: pod ? new URL(pod).host : null }), [clearCloud]);
1375
1692
  }
1376
1693
  }
1377
- async function refreshIfNeeded(request) {
1694
+ const keptFailures = /* @__PURE__ */ new Map();
1695
+ async function refreshIfNeeded(request, opts = {}) {
1378
1696
  const connection = await store.read(request);
1379
1697
  const unchanged = (status, conn, error) => ({
1380
1698
  status,
@@ -1389,18 +1707,27 @@ function createPodAppAuth(config) {
1389
1707
  }
1390
1708
  if (isPrefetchOrAsset(request)) return unchanged("skipped", connection);
1391
1709
  const t = now();
1392
- const accessLives = connection.accessExpiresAt > t;
1393
- if (connection.accessExpiresAt - t > REFRESH_WINDOW_MS) return unchanged("fresh", connection);
1710
+ const accessLives = !opts.force && connection.accessExpiresAt > t;
1711
+ if (!opts.force && connection.accessExpiresAt - t > REFRESH_WINDOW_MS) return unchanged("fresh", connection);
1394
1712
  if (!connection.refreshToken) {
1395
1713
  return accessLives ? unchanged("fresh", connection) : unchanged("expired", null, new ConnectError("expired", "No refresh token; connect again"));
1396
1714
  }
1715
+ for (const [token, f] of keptFailures) if (t - f.at >= KEPT_RETRY_MS) keptFailures.delete(token);
1716
+ const keptFailure = accessLives ? keptFailures.get(connection.refreshToken) : void 0;
1717
+ if (keptFailure) return unchanged("kept", connection, keptFailure.error);
1397
1718
  let next;
1398
1719
  try {
1399
1720
  next = await refreshConnection(connection, flow);
1400
1721
  } catch (err) {
1401
1722
  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);
1723
+ if (accessLives) {
1724
+ keptFailures.set(connection.refreshToken, { error: e, at: t });
1725
+ return unchanged("kept", connection, e);
1726
+ }
1727
+ if (e.code === "revoked" || e.code === "expired" || e.code === "pod_moved") {
1728
+ const cleared = await store.clear(request).catch(() => []);
1729
+ return { status: e.code === "revoked" ? "revoked" : "expired", connection: null, setCookies: cleared, cookieHeader: applySetCookies(request.headers.get("cookie"), cleared), error: e };
1730
+ }
1404
1731
  return unchanged("unavailable", null, e);
1405
1732
  }
1406
1733
  let setCookies;
@@ -1419,6 +1746,24 @@ function createPodAppAuth(config) {
1419
1746
  cookieHeader: applySetCookies(request.headers.get("cookie"), setCookies)
1420
1747
  };
1421
1748
  }
1749
+ function renewer(persist, source) {
1750
+ return async (connection) => {
1751
+ let next;
1752
+ try {
1753
+ next = await refreshConnection(connection, flow);
1754
+ } catch (err) {
1755
+ throw fail(err, "refresh");
1756
+ }
1757
+ try {
1758
+ const cookies = await store.write(next, asRequest(source ?? null));
1759
+ await persist([...cookies, marker(next.podUrl)]);
1760
+ } catch (err) {
1761
+ fail(err, "refresh");
1762
+ throw new ConnectError("expired", "The renewed connection could not be stored; connect again");
1763
+ }
1764
+ return next;
1765
+ };
1766
+ }
1422
1767
  return {
1423
1768
  clientId,
1424
1769
  redirectUri,
@@ -1432,7 +1777,15 @@ function createPodAppAuth(config) {
1432
1777
  return await store.read(request) ? null : lapsedIn(request);
1433
1778
  },
1434
1779
  refreshIfNeeded,
1435
- client: (connection, opts) => createPodClient(connection, { fetch: config.fetch, now: config.now, ...opts })
1780
+ client(connection, opts = {}) {
1781
+ const { persist, request, ...clientOpts } = opts;
1782
+ return createPodClient(connection, {
1783
+ fetch: config.fetch,
1784
+ now: config.now,
1785
+ ...persist ? { renew: renewer(persist, request) } : {},
1786
+ ...clientOpts
1787
+ });
1788
+ }
1436
1789
  };
1437
1790
  }
1438
1791
 
@@ -1458,6 +1811,7 @@ assertServerRuntime();
1458
1811
  canonicalPodUrl,
1459
1812
  cloudBaseUrl,
1460
1813
  cloudConnectAvailable,
1814
+ connectionActsAs,
1461
1815
  connectionExpiresAt,
1462
1816
  cookieNames,
1463
1817
  createPodAppAuth,
@@ -1465,6 +1819,7 @@ assertServerRuntime();
1465
1819
  createSealedCookieStore,
1466
1820
  discoverPod,
1467
1821
  handleCallback,
1822
+ humanSlots,
1468
1823
  isBrowserRuntime,
1469
1824
  isConnectError,
1470
1825
  isPrefetchOrAsset,