@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.js CHANGED
@@ -1,15 +1,19 @@
1
1
  import {
2
+ CLOUD_STATE_RE,
3
+ LOOPBACK_HOSTS,
4
+ SPACE_HINT_RE,
2
5
  base64url,
3
6
  constantTimeEqual,
4
7
  fromBase64url,
5
8
  fromUtf8,
9
+ isIpLiteral,
6
10
  parseCookies,
7
11
  pkceChallenge,
8
12
  randomBytes,
9
13
  randomToken,
10
14
  serializeCookie,
11
15
  utf8
12
- } from "./chunk-J7XHAAFJ.js";
16
+ } from "./chunk-UK2U6SMA.js";
13
17
 
14
18
  // src/connect/guard.ts
15
19
  var BROWSER_REFUSAL = "@synap-core/sdk/connect is server-only: it holds the pod's tokens and the app secret. Import it from route handlers, proxy.ts or server code, never from a client component.";
@@ -52,6 +56,7 @@ var CONNECT_ERROR_CODES = [
52
56
  "rate_limited",
53
57
  "expired",
54
58
  "revoked",
59
+ "pod_moved",
55
60
  "server_error"
56
61
  ];
57
62
  function parseConnectErrorCode(value) {
@@ -87,11 +92,44 @@ function retryAfterSeconds(header, now) {
87
92
  return Math.max(0, Math.ceil((at - now) / 1e3));
88
93
  }
89
94
 
95
+ // src/connect/http.ts
96
+ function timeoutSignal(ms) {
97
+ return typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(ms) : void 0;
98
+ }
99
+ async function readCapped(res, maxBytes) {
100
+ const declared = Number(res.headers.get("content-length"));
101
+ if (Number.isFinite(declared) && declared > maxBytes) {
102
+ await res.body?.cancel().catch(() => {
103
+ });
104
+ return { ok: false, reason: "too_large" };
105
+ }
106
+ if (!res.body) return { ok: true, text: "" };
107
+ const reader = res.body.getReader();
108
+ const decoder = new TextDecoder();
109
+ let total = 0;
110
+ let text = "";
111
+ try {
112
+ for (; ; ) {
113
+ const { done, value } = await reader.read();
114
+ if (done) break;
115
+ total += value.byteLength;
116
+ if (total > maxBytes) {
117
+ await reader.cancel().catch(() => {
118
+ });
119
+ return { ok: false, reason: "too_large" };
120
+ }
121
+ text += decoder.decode(value, { stream: true });
122
+ }
123
+ } catch {
124
+ return { ok: false, reason: "unreadable" };
125
+ }
126
+ return { ok: true, text: text + decoder.decode() };
127
+ }
128
+
90
129
  // src/connect/discovery.ts
91
130
  var METADATA_PATH = "/.well-known/oauth-authorization-server";
92
131
  var TIMEOUT_MS = 5e3;
93
132
  var MAX_METADATA_BYTES = 64 * 1024;
94
- var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
95
133
  function isPrivateIpv4(host) {
96
134
  const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
97
135
  if (!m) return false;
@@ -144,10 +182,12 @@ function sameOriginEndpoint(value, issuer) {
144
182
  }
145
183
  }
146
184
  async function readJsonCapped(res) {
147
- const text = await res.text();
148
- if (text.length > MAX_METADATA_BYTES) throw new ConnectError("not_a_pod", "Metadata document too large");
185
+ const read = await readCapped(res, MAX_METADATA_BYTES);
186
+ if (!read.ok) {
187
+ throw read.reason === "too_large" ? new ConnectError("not_a_pod", "Metadata document too large") : new ConnectError("pod_unreachable", "The metadata answer broke off");
188
+ }
149
189
  try {
150
- return JSON.parse(text);
190
+ return JSON.parse(read.text);
151
191
  } catch {
152
192
  throw new ConnectError("not_a_pod", "Metadata is not JSON");
153
193
  }
@@ -160,18 +200,27 @@ async function discoverIssuer(issuer, opts) {
160
200
  method: "GET",
161
201
  headers: { Accept: "application/json" },
162
202
  redirect: "manual",
163
- signal: typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(TIMEOUT_MS) : void 0
203
+ signal: timeoutSignal(TIMEOUT_MS)
164
204
  });
165
205
  } catch {
166
206
  throw new ConnectError("pod_unreachable", "The pod did not answer");
167
207
  }
208
+ if (res.status >= 300 && res.status < 400) {
209
+ await res.body?.cancel().catch(() => {
210
+ });
211
+ throw new ConnectError("pod_moved", `The address answered ${res.status}: the pod moved (its new address is not followed)`);
212
+ }
168
213
  if (res.status === 429) {
169
214
  throw new ConnectError("rate_limited", "The pod asked to slow down", {
170
215
  retryAfterSec: retryAfterSeconds(res.headers.get("retry-after"), (opts.now ?? Date.now)())
171
216
  });
172
217
  }
173
218
  if (res.status >= 500) throw new ConnectError("pod_unreachable", `The pod answered ${res.status}`);
174
- if (res.status !== 200) throw new ConnectError("not_a_pod", `No authorization server metadata (${res.status})`);
219
+ if (res.status !== 200) {
220
+ await res.body?.cancel().catch(() => {
221
+ });
222
+ throw new ConnectError("not_a_pod", `No authorization server metadata (${res.status})`);
223
+ }
175
224
  const doc = await readJsonCapped(res);
176
225
  if (!doc || typeof doc !== "object" || Array.isArray(doc)) throw new ConnectError("not_a_pod", "Metadata is not an object");
177
226
  const m = doc;
@@ -197,21 +246,38 @@ async function discoverIssuer(issuer, opts) {
197
246
  refreshSupported: list(m.grant_types_supported)?.includes("refresh_token") ?? false
198
247
  };
199
248
  }
249
+ var CLOUD_DOMAIN = "synap.live";
250
+ var CLOUD_POD_ZONE = `pod.${CLOUD_DOMAIN}`;
251
+ var POD_NAME_RE = /^[a-z0-9]([a-z0-9-]{0,30}[a-z0-9])?$/;
252
+ function fallbackIssuers(first) {
253
+ const url = new URL(first);
254
+ const host = url.hostname;
255
+ if (url.pathname !== "/" || !host.includes(".") || host.endsWith(`.${CLOUD_POD_ZONE}`)) return [];
256
+ const label = host.endsWith(`.${CLOUD_DOMAIN}`) ? host.slice(0, -CLOUD_DOMAIN.length - 1) : null;
257
+ if (label !== null && POD_NAME_RE.test(label)) {
258
+ return [`${url.protocol}//${label}.${CLOUD_POD_ZONE}`, `${url.protocol}//pod.${url.host}`];
259
+ }
260
+ return host.startsWith("pod.") ? [] : [`${url.protocol}//pod.${url.host}`];
261
+ }
200
262
  async function discoverPod(input, opts = {}) {
201
263
  const first = canonicalPodUrl(input, opts);
264
+ let firstError;
202
265
  try {
203
266
  return await discoverIssuer(first, opts);
204
267
  } catch (err) {
205
- const url = new URL(first);
206
- const retry = err instanceof ConnectError && (err.code === "not_a_pod" || err.code === "pod_unreachable") && !url.hostname.startsWith("pod.") && url.pathname === "/" && url.hostname.includes(".");
207
- if (!retry) throw err;
208
- const second = `${url.protocol}//pod.${url.host}`;
268
+ firstError = err;
269
+ }
270
+ const retryable = (err) => err instanceof ConnectError && (err.code === "not_a_pod" || err.code === "pod_unreachable" || err.code === "pod_moved");
271
+ if (!retryable(firstError)) throw firstError;
272
+ let tooOld = null;
273
+ for (const candidate of fallbackIssuers(first)) {
209
274
  try {
210
- return await discoverIssuer(second, opts);
211
- } catch (err2) {
212
- throw err2 instanceof ConnectError && err2.code === "pod_unsupported" ? err2 : err;
275
+ return await discoverIssuer(candidate, opts);
276
+ } catch (err) {
277
+ if (err instanceof ConnectError && err.code === "pod_unsupported") tooOld ??= err;
213
278
  }
214
279
  }
280
+ throw tooOld ?? firstError;
215
281
  }
216
282
  function safeNextPath(value, appOrigin) {
217
283
  if (!value || value.length > 2048) return null;
@@ -246,10 +312,10 @@ function acceptLaunch(url, appOrigin, opts = {}) {
246
312
  return {
247
313
  pod,
248
314
  next: safeNextPath(params.get("target_link_uri"), appOrigin),
249
- spaceHint: hint && /^[A-Za-z0-9_-]{1,64}$/.test(hint) ? hint : null,
315
+ spaceHint: hint && SPACE_HINT_RE.test(hint) ? hint : null,
250
316
  // A browser launch never carries a state: any well-formed one is a Synap Cloud return
251
317
  // (a pick: iss + state; a cancel: state + error=access_denied, no iss).
252
- cloudState: state && /^[A-Za-z0-9._~-]{16,256}$/.test(state) ? state : null
318
+ cloudState: state && CLOUD_STATE_RE.test(state) ? state : null
253
319
  };
254
320
  }
255
321
 
@@ -338,8 +404,6 @@ async function unseal(sealed, opts) {
338
404
 
339
405
  // src/connect/cloud.ts
340
406
  var CLOUD_TRANSACTION_TTL_MS = 10 * 60 * 1e3;
341
- var STATE_RE = /^[A-Za-z0-9._~-]{16,256}$/;
342
- var LOOPBACK_HOSTS2 = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
343
407
  function cloudBaseUrl(cloudUrl, opts = {}) {
344
408
  let url;
345
409
  try {
@@ -347,7 +411,7 @@ function cloudBaseUrl(cloudUrl, opts = {}) {
347
411
  } catch {
348
412
  throw new ConnectError("misconfigured", "cloudUrl is not a URL");
349
413
  }
350
- const loopback = LOOPBACK_HOSTS2.has(url.hostname);
414
+ const loopback = LOOPBACK_HOSTS.has(url.hostname);
351
415
  if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback && opts.allowInsecureLoopback)) {
352
416
  throw new ConnectError("misconfigured", "cloudUrl must be https (http only on localhost in development)");
353
417
  }
@@ -415,7 +479,7 @@ async function acceptCloudReturn(opts) {
415
479
  }
416
480
  const params = new URL(opts.url).searchParams;
417
481
  const state = params.get("state");
418
- if (state === null || !STATE_RE.test(state) || !constantTimeEqual(state, tx.state)) {
482
+ if (state === null || !CLOUD_STATE_RE.test(state) || !constantTimeEqual(state, tx.state)) {
419
483
  throw new ConnectError("mismatch", "The Synap Cloud answer does not belong to the sign-in this browser started");
420
484
  }
421
485
  const error = params.get("error");
@@ -433,6 +497,12 @@ async function acceptCloudReturn(opts) {
433
497
  }
434
498
 
435
499
  // src/connect/client.ts
500
+ function connectionActsAs(connection) {
501
+ return connection.actsAs === "person" ? "person" : "agent";
502
+ }
503
+ function humanSlots(session) {
504
+ return session.expectedOutputs.filter((slot) => slot.owner === "human" && slot.status !== "done" && !slot.retiredAt);
505
+ }
436
506
  function can(connection, permission, workspaceId) {
437
507
  return connection.grant.some((g) => g.permission === permission && (workspaceId === void 0 || g.workspaceId === workspaceId));
438
508
  }
@@ -441,23 +511,50 @@ function spaces(connection) {
441
511
  for (const g of connection.grant) {
442
512
  const space = bySpace.get(g.workspaceId) ?? { workspaceId: g.workspaceId, workspaceName: g.workspaceName, permissions: [] };
443
513
  if (!space.workspaceName && g.workspaceName) space.workspaceName = g.workspaceName;
514
+ if (!space.templateSlug && g.templateSlug) space.templateSlug = g.templateSlug;
444
515
  if (!space.permissions.includes(g.permission)) space.permissions.push(g.permission);
445
516
  bySpace.set(g.workspaceId, space);
446
517
  }
447
518
  return [...bySpace.values()];
448
519
  }
520
+ var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
521
+ var PROPOSAL_IDS_MAX = 100;
449
522
  var SAFE_PATH = /^\/(?!\/)[A-Za-z0-9\-._~!$&'()*+,;=:@%/]*$/;
523
+ var TIMEOUT_MS2 = 1e4;
524
+ var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
450
525
  function hubUrl(podUrl, path, query) {
451
- if (!SAFE_PATH.test(path) || /(^|\/)\.\.?(\/|$)/.test(path) || path.includes("\\")) {
452
- throw new TypeError(`Not a Hub API path: ${path}`);
526
+ if (!SAFE_PATH.test(path) || /%2e/i.test(path) || /(^|\/)\.\.?(\/|$)/.test(path) || path.includes("\\")) return null;
527
+ let url;
528
+ try {
529
+ url = new URL(`${podUrl}/api/hub${path}`);
530
+ } catch {
531
+ return null;
453
532
  }
454
- const url = new URL(`${podUrl}/api/hub${path}`);
533
+ if (url.origin !== new URL(podUrl).origin || !(url.pathname === "/api/hub" || url.pathname.startsWith("/api/hub/"))) return null;
455
534
  for (const [key, value] of Object.entries(query ?? {})) {
456
535
  if (value === void 0 || value === null) continue;
457
536
  for (const v of Array.isArray(value) ? value : [value]) url.searchParams.append(key, String(v));
458
537
  }
459
538
  return url;
460
539
  }
540
+ function renewalError(err) {
541
+ const e = isConnectError(err) ? err : null;
542
+ const detail = e?.message ?? (err instanceof Error ? err.message : "The connection could not be renewed");
543
+ switch (e?.code) {
544
+ case "revoked":
545
+ return { code: "unauthorized", status: 401, detail };
546
+ case "expired":
547
+ return { code: "expired", status: null, detail };
548
+ case "rate_limited":
549
+ return { code: "rate_limited", status: null, detail, ...e.retryAfterSec !== void 0 ? { retryAfterSec: e.retryAfterSec } : {} };
550
+ case "pod_unreachable":
551
+ return { code: "pod_unreachable", status: null, detail };
552
+ case "pod_moved":
553
+ return { code: "pod_moved", status: null, detail };
554
+ default:
555
+ return { code: "server", status: null, detail };
556
+ }
557
+ }
461
558
  function errorFor(status, body, retryAfter, now) {
462
559
  const obj = body && typeof body === "object" ? body : {};
463
560
  const detail = typeof obj.error === "string" ? obj.error : typeof obj.message === "string" ? obj.message : void 0;
@@ -472,6 +569,7 @@ function errorFor(status, body, retryAfter, now) {
472
569
  return { code: "rate_limited", ...base, ...retryAfterSec !== void 0 ? { retryAfterSec } : {} };
473
570
  }
474
571
  if (status === 502 || status === 503 || status === 504) return { code: "pod_unreachable", ...base };
572
+ if (status >= 300 && status < 400) return { code: "pod_moved", ...base };
475
573
  return { code: "server", ...base };
476
574
  }
477
575
  function reviewUrlFor(podUrl, reviewUrl, proposalId) {
@@ -484,13 +582,37 @@ function reviewUrlFor(podUrl, reviewUrl, proposalId) {
484
582
  }
485
583
  return proposalId ? `${podUrl}/open/${encodeURIComponent(proposalId)}` : podUrl;
486
584
  }
585
+ function refused(code, reason, detail) {
586
+ return { ok: false, error: { code, status: null, detail, reason } };
587
+ }
588
+ function asObject(value) {
589
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
590
+ }
591
+ function hasId(value) {
592
+ return asObject(value) !== null && typeof value.id === "string";
593
+ }
487
594
  function createPodClient(connection, opts = {}) {
488
595
  const now = opts.now ?? Date.now;
489
596
  const doFetch = () => opts.fetch ?? fetch;
490
- async function request(method, path, body, query) {
491
- const url = hubUrl(connection.podUrl, path, query);
492
- if (connection.accessExpiresAt <= now()) return { ok: false, error: { code: "expired", status: null } };
493
- const headers = { Accept: "application/json", Authorization: `Bearer ${connection.accessToken}` };
597
+ let current = connection;
598
+ let renewal = null;
599
+ function renewOnce(refused2) {
600
+ if (current !== refused2) return Promise.resolve({ ok: true, data: current });
601
+ renewal ??= opts.renew(refused2).then(
602
+ (next) => {
603
+ current = next;
604
+ return { ok: true, data: next };
605
+ },
606
+ (err) => ({ ok: false, error: renewalError(err) })
607
+ );
608
+ return renewal;
609
+ }
610
+ async function request(method, path, body, query, retried = false) {
611
+ const sent = current;
612
+ const url = hubUrl(sent.podUrl, path, query);
613
+ if (!url) return refused("invalid", "invalid_input", "Not a Hub API path: encode each path segment (encodeURIComponent)");
614
+ if (sent.accessExpiresAt <= now()) return { ok: false, error: { code: "expired", status: null } };
615
+ const headers = { Accept: "application/json", Authorization: `Bearer ${sent.accessToken}` };
494
616
  if (body !== void 0) headers["Content-Type"] = "application/json";
495
617
  let res;
496
618
  try {
@@ -498,21 +620,33 @@ function createPodClient(connection, opts = {}) {
498
620
  method,
499
621
  headers,
500
622
  body: body === void 0 ? void 0 : JSON.stringify(body),
501
- redirect: "manual"
623
+ redirect: "manual",
624
+ signal: timeoutSignal(opts.timeoutMs ?? TIMEOUT_MS2)
502
625
  });
503
626
  } catch {
504
627
  return { ok: false, error: { code: "pod_unreachable", status: null } };
505
628
  }
629
+ const read2 = await readCapped(res, opts.maxResponseBytes ?? MAX_RESPONSE_BYTES);
630
+ if (!read2.ok) {
631
+ 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" } };
632
+ }
506
633
  let data = null;
507
- const text = await res.text().catch(() => "");
508
- if (text) {
634
+ if (read2.text) {
509
635
  try {
510
- data = JSON.parse(text);
636
+ data = JSON.parse(read2.text);
511
637
  } catch {
512
638
  data = null;
513
639
  }
514
640
  }
515
641
  if (res.status >= 200 && res.status < 300) return { ok: true, data };
642
+ if (res.status === 401 && sent.refreshToken) {
643
+ if (!opts.renew) return { ok: false, error: { ...errorFor(401, data, null, now()), refreshable: true } };
644
+ if (!retried) {
645
+ const renewed = await renewOnce(sent);
646
+ if (!renewed.ok) return renewed;
647
+ return request(method, path, body, query, true);
648
+ }
649
+ }
516
650
  return { ok: false, error: errorFor(res.status, data, res.headers.get("retry-after"), now()) };
517
651
  }
518
652
  async function write(method, path, body) {
@@ -525,18 +659,23 @@ function createPodClient(connection, opts = {}) {
525
659
  ok: true,
526
660
  status: "proposed",
527
661
  proposalId,
528
- reviewUrl: reviewUrlFor(connection.podUrl, obj.reviewUrl, proposalId),
662
+ reviewUrl: reviewUrlFor(current.podUrl, obj.reviewUrl, proposalId),
529
663
  data: result.data
530
664
  };
531
665
  }
532
666
  return { ok: true, status: "applied", data: result.data };
533
667
  }
534
668
  async function listPendingProposals(opts2 = {}) {
535
- if (!can(connection, "proposal.read", opts2.workspaceId)) {
536
- return {
537
- ok: false,
538
- error: { code: "forbidden", status: null, detail: "The connection was not granted proposal.read in that space" }
539
- };
669
+ if (!can(current, "proposal.read", opts2.workspaceId)) {
670
+ return refused("forbidden", "not_granted", "The connection was not granted proposal.read in that space");
671
+ }
672
+ let only = null;
673
+ if (opts2.proposalIds !== void 0) {
674
+ if (opts2.proposalIds.length > PROPOSAL_IDS_MAX || opts2.proposalIds.some((id) => !ID_RE.test(id))) {
675
+ return refused("invalid", "invalid_input", `proposalIds must be at most ${PROPOSAL_IDS_MAX} proposal ids`);
676
+ }
677
+ if (opts2.proposalIds.length === 0) return { ok: true, data: { total: 0, proposals: [], hasMore: false } };
678
+ only = new Set(opts2.proposalIds);
540
679
  }
541
680
  const limit = Math.min(Math.max(Math.floor(opts2.limit ?? 20), 1), 200);
542
681
  const offset = Math.max(Math.floor(opts2.offset ?? 0), 0);
@@ -545,7 +684,8 @@ function createPodClient(connection, opts = {}) {
545
684
  view: "basic",
546
685
  limit,
547
686
  offset,
548
- workspaceId: opts2.workspaceId
687
+ workspaceId: opts2.workspaceId,
688
+ ...only ? { proposalIds: [...only].join(",") } : {}
549
689
  });
550
690
  if (!result.ok) return result;
551
691
  const obj = result.data && typeof result.data === "object" ? result.data : {};
@@ -558,7 +698,7 @@ function createPodClient(connection, opts = {}) {
558
698
  if (!row || typeof row !== "object") continue;
559
699
  const r = row;
560
700
  const id = str(r.id);
561
- if (!id) continue;
701
+ if (!id || only && !only.has(id)) continue;
562
702
  proposals.push({
563
703
  id,
564
704
  proposalType: str(r.proposalType) ?? "",
@@ -569,7 +709,7 @@ function createPodClient(connection, opts = {}) {
569
709
  agentUserId: str(r.agentUserId),
570
710
  sessionId: str(r.sessionId),
571
711
  ...str(r.summary) ? { summary: str(r.summary) } : {},
572
- reviewUrl: reviewUrlFor(connection.podUrl, null, id)
712
+ reviewUrl: reviewUrlFor(current.podUrl, null, id)
573
713
  });
574
714
  }
575
715
  return {
@@ -577,13 +717,131 @@ function createPodClient(connection, opts = {}) {
577
717
  data: { total: obj.total, proposals, hasMore: obj.hasMore === true || offset + proposals.length < obj.total }
578
718
  };
579
719
  }
720
+ async function read(path, query, shape) {
721
+ const result = await request("GET", path, void 0, query);
722
+ if (!result.ok) return result;
723
+ const data = shape(result.data);
724
+ if (data === null) return { ok: false, error: { code: "server", status: 200, detail: `The pod's ${path.split("/")[1]} answer is not the expected shape` } };
725
+ return { ok: true, data };
726
+ }
727
+ const badId = (what) => refused("invalid", "invalid_input", `Not a ${what} id`);
728
+ const clamp = (n, min, max) => n === void 0 ? void 0 : Math.min(Math.max(Math.floor(n), min), max);
729
+ function listEntities(opts2 = {}) {
730
+ return read("/entities", {
731
+ workspaceId: opts2.workspaceId,
732
+ profileSlug: opts2.profileSlug,
733
+ limit: clamp(opts2.limit, 1, 1e3),
734
+ offset: clamp(opts2.offset, 0, Number.MAX_SAFE_INTEGER),
735
+ sort: opts2.sort,
736
+ includePodWide: opts2.includePodWide
737
+ }, (data) => Array.isArray(data) ? data.filter(hasId) : null);
738
+ }
739
+ async function getEntity(id) {
740
+ if (!ID_RE.test(id)) return badId("entity");
741
+ return read(`/entities/${id}`, void 0, (data) => hasId(data) ? data : null);
742
+ }
743
+ async function getEntityConnections(id, opts2 = {}) {
744
+ if (!ID_RE.test(id)) return badId("entity");
745
+ return read(`/entities/${id}/connections`, { workspaceId: opts2.workspaceId, limit: clamp(opts2.limit, 1, 200) }, (data) => {
746
+ const obj = asObject(data);
747
+ if (!obj || !Array.isArray(obj.connections)) return null;
748
+ const connections = obj.connections.filter(
749
+ (c) => !!c && typeof c === "object" && typeof c.entityId === "string"
750
+ );
751
+ const counts = asObject(obj.counts) ?? {};
752
+ const count = (key, source) => typeof counts[key] === "number" ? counts[key] : connections.filter((c) => c.source === source).length;
753
+ return {
754
+ connections,
755
+ counts: {
756
+ total: typeof counts.total === "number" ? counts.total : connections.length,
757
+ graph: count("graph", "graph"),
758
+ structural: count("structural", "property"),
759
+ threads: count("threads", "thread"),
760
+ contextChannels: count("contextChannels", "context_channel"),
761
+ focusSessions: count("focusSessions", "focus_session")
762
+ }
763
+ };
764
+ });
765
+ }
766
+ async function getDocument(id, opts2 = {}) {
767
+ if (!ID_RE.test(id)) return badId("document");
768
+ return read(`/documents/${id}`, { format: opts2.format }, (data) => {
769
+ const doc = asObject(asObject(data)?.document);
770
+ if (!doc || typeof doc.id !== "string" || typeof doc.content !== "string") return null;
771
+ return { ...doc, sections: Array.isArray(doc.sections) ? doc.sections : [] };
772
+ });
773
+ }
774
+ async function listFocusSessions(opts2) {
775
+ if (!opts2?.workspaceId && !opts2?.projectId) return refused("invalid", "invalid_input", "workspaceId or projectId is required");
776
+ return read("/focus-sessions", {
777
+ workspaceId: opts2.workspaceId,
778
+ projectId: opts2.projectId,
779
+ status: opts2.status,
780
+ limit: clamp(opts2.limit, 1, 50),
781
+ lens: opts2.lens,
782
+ kind: opts2.kind,
783
+ subjectEntityId: opts2.subjectEntityId
784
+ }, (data) => {
785
+ if (!Array.isArray(data)) return null;
786
+ return data.filter(hasId).map((row) => ({
787
+ ...row,
788
+ expectedOutputs: Array.isArray(row.expectedOutputs) ? row.expectedOutputs.filter((s) => !!s && typeof s === "object" && typeof s.label === "string") : []
789
+ }));
790
+ });
791
+ }
792
+ async function getFileUrl(entityId) {
793
+ if (!ID_RE.test(entityId)) return badId("file");
794
+ return read(`/files/${entityId}/url`, void 0, (data) => {
795
+ const obj = asObject(data);
796
+ if (!obj || typeof obj.url !== "string" || typeof obj.expiresAt !== "string") return null;
797
+ return { url: obj.url, expiresAt: obj.expiresAt, mimeType: typeof obj.mimeType === "string" ? obj.mimeType : null };
798
+ });
799
+ }
800
+ async function approveProposal(id, opts2 = {}) {
801
+ if (connectionActsAs(current) !== "person") {
802
+ return refused("forbidden", "agent_cannot_approve", "An agent connection can never approve: approval is the person's");
803
+ }
804
+ if (!can(current, "proposal.approve", opts2.workspaceId)) {
805
+ return refused("forbidden", "not_granted", "The connection was not granted proposal.approve in that space");
806
+ }
807
+ if (!ID_RE.test(id)) return badId("proposal");
808
+ const result = await request("POST", `/proposals/${id}/approve`, {});
809
+ if (!result.ok) return result;
810
+ const obj = asObject(result.data);
811
+ if (!obj || typeof obj.proposalId !== "string") {
812
+ return { ok: false, error: { code: "server", status: 200, detail: "The pod's approve answer names no proposalId" } };
813
+ }
814
+ return {
815
+ ok: true,
816
+ data: {
817
+ proposalId: obj.proposalId,
818
+ ...typeof obj.primaryId === "string" ? { primaryId: obj.primaryId } : {},
819
+ ...typeof obj.linked === "number" ? { linked: obj.linked } : {},
820
+ proposal: asObject(obj.proposal)
821
+ }
822
+ };
823
+ }
580
824
  return {
581
825
  podUrl: connection.podUrl,
582
- user: connection.user,
583
- grant: connection.grant,
584
- can: (permission, workspaceId) => can(connection, permission, workspaceId),
585
- spaces: () => spaces(connection),
826
+ get user() {
827
+ return current.user;
828
+ },
829
+ get grant() {
830
+ return current.grant;
831
+ },
832
+ get actsAs() {
833
+ return connectionActsAs(current);
834
+ },
835
+ can: (permission, workspaceId) => can(current, permission, workspaceId),
836
+ spaces: () => spaces(current),
586
837
  listPendingProposals,
838
+ listEntities,
839
+ getEntity,
840
+ getEntityConnections,
841
+ getDocument,
842
+ listFocusSessions,
843
+ getFileUrl,
844
+ approveProposal,
587
845
  get(path, query) {
588
846
  return request("GET", path, void 0, query);
589
847
  },
@@ -606,7 +864,8 @@ function createPodClient(connection, opts = {}) {
606
864
  var TRANSACTION_TTL_MS = 10 * 60 * 1e3;
607
865
  var DEFAULT_REFRESH_IDLE_MS = 30 * 24 * 60 * 60 * 1e3;
608
866
  var DEFAULT_ACCESS_TTL_SEC = 3600;
609
- var TIMEOUT_MS2 = 1e4;
867
+ var TIMEOUT_MS3 = 1e4;
868
+ var MAX_TOKEN_RESPONSE_BYTES = 64 * 1024;
610
869
  var PERMISSION_RE = /^[a-z][a-z0-9_-]*(\.[a-z0-9_-]+)+$/i;
611
870
  function assertPermissions(permissions) {
612
871
  const unique = [...new Set(permissions)];
@@ -724,20 +983,29 @@ async function tokenRequest(endpoint, body, during, opts) {
724
983
  headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
725
984
  body: body.toString(),
726
985
  redirect: "manual",
727
- signal: typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(TIMEOUT_MS2) : void 0
986
+ signal: timeoutSignal(TIMEOUT_MS3)
728
987
  });
729
988
  } catch {
730
989
  throw new ConnectError("pod_unreachable", "The pod's token endpoint did not answer");
731
990
  }
991
+ if (res.status >= 300 && res.status < 400) {
992
+ await res.body?.cancel().catch(() => {
993
+ });
994
+ throw new ConnectError("pod_moved", `The token endpoint answered ${res.status}: the pod moved; connect again with its new address`);
995
+ }
732
996
  if (res.status === 429) {
733
997
  throw new ConnectError("rate_limited", "The pod asked to slow down", {
734
998
  retryAfterSec: retryAfterSeconds(res.headers.get("retry-after"), (opts.now ?? Date.now)())
735
999
  });
736
1000
  }
737
1001
  if (res.status >= 500) throw new ConnectError("pod_unreachable", `The token endpoint answered ${res.status}`);
1002
+ const read = await readCapped(res, MAX_TOKEN_RESPONSE_BYTES);
1003
+ if (!read.ok) {
1004
+ 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");
1005
+ }
738
1006
  let json;
739
1007
  try {
740
- json = await res.json();
1008
+ json = JSON.parse(read.text);
741
1009
  } catch {
742
1010
  throw new ConnectError("server_error", `The token endpoint answered ${res.status} without JSON`);
743
1011
  }
@@ -753,14 +1021,18 @@ function parseGrant(value) {
753
1021
  const out = [];
754
1022
  for (const g of value) {
755
1023
  if (!g || typeof g !== "object") return null;
756
- const { permission, workspaceId, workspaceName } = g;
1024
+ const { permission, workspaceId, workspaceName, templateSlug } = g;
757
1025
  if (typeof permission !== "string" || typeof workspaceId !== "string") return null;
758
- out.push({ permission, workspaceId, workspaceName: typeof workspaceName === "string" ? workspaceName : "" });
1026
+ const row = { permission, workspaceId, workspaceName: typeof workspaceName === "string" ? workspaceName : "" };
1027
+ if (typeof templateSlug === "string" && templateSlug.length > 0 && templateSlug.length <= 200) row.templateSlug = templateSlug;
1028
+ out.push(row);
759
1029
  }
760
1030
  return out;
761
1031
  }
1032
+ function parseActsAs(value) {
1033
+ return value === "person" || value === "agent" ? value : void 0;
1034
+ }
762
1035
  var APP_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
763
- var LOOPBACK_HOSTS3 = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
764
1036
  function parseApp(value) {
765
1037
  if (!value || typeof value !== "object") return {};
766
1038
  const { id, manageUrl } = value;
@@ -769,7 +1041,7 @@ function parseApp(value) {
769
1041
  if (typeof manageUrl === "string" && manageUrl.length <= 2048) {
770
1042
  try {
771
1043
  const url = new URL(manageUrl);
772
- const secure = url.protocol === "https:" || url.protocol === "http:" && LOOPBACK_HOSTS3.has(url.hostname);
1044
+ const secure = url.protocol === "https:" || url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname);
773
1045
  if (secure && !url.username && !url.password) out.manageUrl = url.href;
774
1046
  } catch {
775
1047
  }
@@ -815,6 +1087,8 @@ function parseTokenResponse(body, ctx) {
815
1087
  const manageUrl = app.manageUrl ?? ctx.previous?.manageUrl;
816
1088
  if (appId !== void 0) connection.appId = appId;
817
1089
  if (manageUrl !== void 0) connection.manageUrl = manageUrl;
1090
+ const actsAs = parseActsAs(body.synap_acts_as);
1091
+ if (actsAs !== void 0) connection.actsAs = actsAs;
818
1092
  if (typeof refresh_token === "string" && refresh_token !== "") {
819
1093
  connection.refreshToken = refresh_token;
820
1094
  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;
@@ -880,7 +1154,9 @@ async function revokeConnection(connection, opts = {}) {
880
1154
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
881
1155
  body: new URLSearchParams({ token, token_type_hint: hint, client_id: connection.clientId }).toString(),
882
1156
  redirect: "manual",
883
- signal: typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(TIMEOUT_MS2) : void 0
1157
+ signal: timeoutSignal(TIMEOUT_MS3)
1158
+ });
1159
+ await res.body?.cancel().catch(() => {
884
1160
  });
885
1161
  if (res.status !== 200) revoked = false;
886
1162
  } catch {
@@ -922,6 +1198,7 @@ function pack(c) {
922
1198
  for (const g of c.grant) {
923
1199
  const row = bySpace.get(g.workspaceId) ?? [g.workspaceId, g.workspaceName, []];
924
1200
  row[2].push(g.permission);
1201
+ if (g.templateSlug && row[3] === void 0) row[3] = g.templateSlug;
925
1202
  bySpace.set(g.workspaceId, row);
926
1203
  }
927
1204
  const { grant: _grant, ...rest } = c;
@@ -931,7 +1208,7 @@ function unpack(s) {
931
1208
  if (!s || typeof s.podUrl !== "string" || typeof s.accessToken !== "string" || !Array.isArray(s.g)) return null;
932
1209
  const { g, ...rest } = s;
933
1210
  const grant = g.flatMap(
934
- ([workspaceId, workspaceName, permissions]) => permissions.map((permission) => ({ permission, workspaceId, workspaceName }))
1211
+ ([workspaceId, workspaceName, permissions, templateSlug]) => permissions.map((permission) => ({ permission, workspaceId, workspaceName, ...typeof templateSlug === "string" ? { templateSlug } : {} }))
935
1212
  );
936
1213
  return { ...rest, grant };
937
1214
  }
@@ -1010,9 +1287,30 @@ var DEFAULT_PATHS = {
1010
1287
  afterConnect: "/"
1011
1288
  };
1012
1289
  var REFRESH_WINDOW_MS = 5 * 60 * 1e3;
1013
- var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
1014
1290
  var TEMPLATE_SLUG_RE = /^[a-z0-9][a-z0-9._/-]{0,99}$/;
1015
1291
  var MAX_METADATA_BYTES2 = 5 * 1024;
1292
+ var MAX_REQUESTS = 32;
1293
+ var KEPT_RETRY_MS = 6e4;
1294
+ var GRANT_ACTIONS = ["read", "create", "update", "delete"];
1295
+ var SYNAP_APP_REVIEW_PERMISSIONS = ["proposal.approve", "proposal.reject"];
1296
+ var PERMISSION_SEGMENT_RE = /^[a-z0-9][a-z0-9_-]*$/;
1297
+ function isSynapAppOrigin(origin) {
1298
+ return /^https:\/\/[a-z0-9]([a-z0-9-]*[a-z0-9])?\.synap\.live$/.test(origin);
1299
+ }
1300
+ function assertAppPermission(permission, origin) {
1301
+ const segs = permission.split(".");
1302
+ const fail = (why) => {
1303
+ throw new ConnectError("misconfigured", `requests: "${permission}" ${why}`);
1304
+ };
1305
+ if (segs.length < 2 || segs.length > 3 || segs.some((seg) => !PERMISSION_SEGMENT_RE.test(seg))) {
1306
+ fail("is not <subject>[.<kind>].<action> in lowercase (no *)");
1307
+ }
1308
+ if (segs.length === 3 && segs[0] !== "entity") fail(`names a kind, but "${segs[0]}" has none (only entity does)`);
1309
+ if (SYNAP_APP_REVIEW_PERMISSIONS.includes(permission) && isSynapAppOrigin(origin)) return;
1310
+ if (!GRANT_ACTIONS.includes(segs[segs.length - 1])) {
1311
+ fail(`asks for "${segs[segs.length - 1]}", which an app cannot be granted (read, create, update, delete)`);
1312
+ }
1313
+ }
1016
1314
  function appOriginOf(appUrl) {
1017
1315
  let url;
1018
1316
  try {
@@ -1021,8 +1319,11 @@ function appOriginOf(appUrl) {
1021
1319
  throw new ConnectError("misconfigured", "appUrl is not a URL");
1022
1320
  }
1023
1321
  const secure = url.protocol === "https:";
1024
- if (!secure && !(url.protocol === "http:" && LOOPBACK.has(url.hostname))) {
1025
- throw new ConnectError("misconfigured", "appUrl must be https (http only on localhost)");
1322
+ if (!secure && !(url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname))) {
1323
+ throw new ConnectError("misconfigured", "appUrl must be https (http only on localhost, for the SDK's mock pod)");
1324
+ }
1325
+ if (secure && isIpLiteral(url.hostname)) {
1326
+ throw new ConnectError("misconfigured", "appUrl must use a domain name: the pod refuses an IP address client_id");
1026
1327
  }
1027
1328
  if (url.pathname !== "/" || url.search || url.hash || url.username || url.password) {
1028
1329
  throw new ConnectError("misconfigured", "appUrl is an origin: no path, query, fragment or credentials");
@@ -1040,9 +1341,12 @@ function buildClientMetadata(opts) {
1040
1341
  for (const [name, path] of Object.entries(paths)) assertPath(name, path);
1041
1342
  const clientName = opts.clientName.trim();
1042
1343
  if (!clientName || clientName.length > 100) throw new ConnectError("misconfigured", "clientName must be 1-100 characters");
1344
+ if (opts.requests.length === 0 || opts.requests.length > MAX_REQUESTS) {
1345
+ throw new ConnectError("misconfigured", `requests must hold 1-${MAX_REQUESTS} entries (duplicates count)`);
1346
+ }
1043
1347
  assertPermissions(opts.requests.map((r) => r.permission));
1044
1348
  for (const r of opts.requests) {
1045
- if (r.permission.includes("*")) throw new ConnectError("misconfigured", "a permission cannot contain *");
1349
+ assertAppPermission(r.permission, origin);
1046
1350
  if (typeof r.reason !== "string" || !r.reason.trim() || r.reason.length > 200) {
1047
1351
  throw new ConnectError("misconfigured", `the reason for ${r.permission} must be 1-200 characters`);
1048
1352
  }
@@ -1141,7 +1445,7 @@ function createPodAppAuth(config) {
1141
1445
  permissions,
1142
1446
  secret: config.secret,
1143
1447
  resource: config.resource,
1144
- spaceHint: opts.space && /^[A-Za-z0-9_-]{1,64}$/.test(opts.space) ? opts.space : null,
1448
+ spaceHint: opts.space && SPACE_HINT_RE.test(opts.space) ? opts.space : null,
1145
1449
  next: opts.next,
1146
1450
  prompt: opts.prompt,
1147
1451
  allowInsecureLoopback: config.allowInsecureLoopback
@@ -1220,7 +1524,7 @@ function createPodAppAuth(config) {
1220
1524
  clientId,
1221
1525
  secret: config.secret,
1222
1526
  next,
1223
- spaceHint: space && /^[A-Za-z0-9_-]{1,64}$/.test(space) ? space : null,
1527
+ spaceHint: space && SPACE_HINT_RE.test(space) ? space : null,
1224
1528
  prompt: formField(form, "prompt") === "consent" ? "consent" : null,
1225
1529
  now
1226
1530
  });
@@ -1253,7 +1557,8 @@ function createPodAppAuth(config) {
1253
1557
  return redirect(withQuery(origin, paths.connectPage, { error: e.code, pod: pod ? new URL(pod).host : null }), [clearCloud]);
1254
1558
  }
1255
1559
  }
1256
- async function refreshIfNeeded(request) {
1560
+ const keptFailures = /* @__PURE__ */ new Map();
1561
+ async function refreshIfNeeded(request, opts = {}) {
1257
1562
  const connection = await store.read(request);
1258
1563
  const unchanged = (status, conn, error) => ({
1259
1564
  status,
@@ -1268,18 +1573,27 @@ function createPodAppAuth(config) {
1268
1573
  }
1269
1574
  if (isPrefetchOrAsset(request)) return unchanged("skipped", connection);
1270
1575
  const t = now();
1271
- const accessLives = connection.accessExpiresAt > t;
1272
- if (connection.accessExpiresAt - t > REFRESH_WINDOW_MS) return unchanged("fresh", connection);
1576
+ const accessLives = !opts.force && connection.accessExpiresAt > t;
1577
+ if (!opts.force && connection.accessExpiresAt - t > REFRESH_WINDOW_MS) return unchanged("fresh", connection);
1273
1578
  if (!connection.refreshToken) {
1274
1579
  return accessLives ? unchanged("fresh", connection) : unchanged("expired", null, new ConnectError("expired", "No refresh token; connect again"));
1275
1580
  }
1581
+ for (const [token, f] of keptFailures) if (t - f.at >= KEPT_RETRY_MS) keptFailures.delete(token);
1582
+ const keptFailure = accessLives ? keptFailures.get(connection.refreshToken) : void 0;
1583
+ if (keptFailure) return unchanged("kept", connection, keptFailure.error);
1276
1584
  let next;
1277
1585
  try {
1278
1586
  next = await refreshConnection(connection, flow);
1279
1587
  } catch (err) {
1280
1588
  const e = fail(err, "refresh");
1281
- if (accessLives) return unchanged("kept", connection, e);
1282
- if (e.code === "revoked" || e.code === "expired") return unchanged(e.code, null, e);
1589
+ if (accessLives) {
1590
+ keptFailures.set(connection.refreshToken, { error: e, at: t });
1591
+ return unchanged("kept", connection, e);
1592
+ }
1593
+ if (e.code === "revoked" || e.code === "expired" || e.code === "pod_moved") {
1594
+ const cleared = await store.clear(request).catch(() => []);
1595
+ return { status: e.code === "revoked" ? "revoked" : "expired", connection: null, setCookies: cleared, cookieHeader: applySetCookies(request.headers.get("cookie"), cleared), error: e };
1596
+ }
1283
1597
  return unchanged("unavailable", null, e);
1284
1598
  }
1285
1599
  let setCookies;
@@ -1298,6 +1612,24 @@ function createPodAppAuth(config) {
1298
1612
  cookieHeader: applySetCookies(request.headers.get("cookie"), setCookies)
1299
1613
  };
1300
1614
  }
1615
+ function renewer(persist, source) {
1616
+ return async (connection) => {
1617
+ let next;
1618
+ try {
1619
+ next = await refreshConnection(connection, flow);
1620
+ } catch (err) {
1621
+ throw fail(err, "refresh");
1622
+ }
1623
+ try {
1624
+ const cookies = await store.write(next, asRequest(source ?? null));
1625
+ await persist([...cookies, marker(next.podUrl)]);
1626
+ } catch (err) {
1627
+ fail(err, "refresh");
1628
+ throw new ConnectError("expired", "The renewed connection could not be stored; connect again");
1629
+ }
1630
+ return next;
1631
+ };
1632
+ }
1301
1633
  return {
1302
1634
  clientId,
1303
1635
  redirectUri,
@@ -1311,7 +1643,15 @@ function createPodAppAuth(config) {
1311
1643
  return await store.read(request) ? null : lapsedIn(request);
1312
1644
  },
1313
1645
  refreshIfNeeded,
1314
- client: (connection, opts) => createPodClient(connection, { fetch: config.fetch, now: config.now, ...opts })
1646
+ client(connection, opts = {}) {
1647
+ const { persist, request, ...clientOpts } = opts;
1648
+ return createPodClient(connection, {
1649
+ fetch: config.fetch,
1650
+ now: config.now,
1651
+ ...persist ? { renew: renewer(persist, request) } : {},
1652
+ ...clientOpts
1653
+ });
1654
+ }
1315
1655
  };
1316
1656
  }
1317
1657
 
@@ -1336,6 +1676,7 @@ export {
1336
1676
  canonicalPodUrl,
1337
1677
  cloudBaseUrl,
1338
1678
  cloudConnectAvailable,
1679
+ connectionActsAs,
1339
1680
  connectionExpiresAt,
1340
1681
  cookieNames,
1341
1682
  createPodAppAuth,
@@ -1343,6 +1684,7 @@ export {
1343
1684
  createSealedCookieStore,
1344
1685
  discoverPod,
1345
1686
  handleCallback,
1687
+ humanSlots,
1346
1688
  isBrowserRuntime,
1347
1689
  isConnectError,
1348
1690
  isPrefetchOrAsset,