@synap-core/sdk 1.3.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 (51) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README.md +212 -12
  3. package/dist/{chunk-J7XHAAFJ.js → chunk-UK2U6SMA.js} +14 -2
  4. package/dist/chunk-UK2U6SMA.js.map +1 -0
  5. package/dist/{client-CVUT1Mcf.d.cts → client-B50KC2aJ.d.cts} +14 -9
  6. package/dist/{client-CVUT1Mcf.d.ts → client-B50KC2aJ.d.ts} +14 -9
  7. package/dist/connect.cjs +790 -161
  8. package/dist/connect.cjs.map +1 -1
  9. package/dist/connect.d.cts +610 -37
  10. package/dist/connect.d.ts +610 -37
  11. package/dist/connect.js +777 -162
  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 +366 -17
  20. package/dist/testing.cjs.map +1 -1
  21. package/dist/testing.d.cts +190 -7
  22. package/dist/testing.d.ts +190 -7
  23. package/dist/testing.js +358 -17
  24. package/dist/testing.js.map +1 -1
  25. package/dist/{types-DpqPNaaa.d.cts → types-DZYNBCq7.d.cts} +47 -5
  26. package/dist/{types-DpqPNaaa.d.ts → types-DZYNBCq7.d.ts} +47 -5
  27. package/package.json +36 -12
  28. package/src/connect/auth.ts +314 -55
  29. package/src/connect/client.ts +472 -26
  30. package/src/connect/cloud.ts +210 -0
  31. package/src/connect/discovery.ts +79 -44
  32. package/src/connect/encoding.ts +1 -1
  33. package/src/connect/errors.ts +8 -1
  34. package/src/connect/flow.ts +80 -12
  35. package/src/connect/http.ts +42 -0
  36. package/src/connect/hub-types.ts +254 -0
  37. package/src/connect/index.ts +69 -23
  38. package/src/connect/rules.ts +18 -0
  39. package/src/connect/seal.ts +1 -1
  40. package/src/connect/store.ts +50 -8
  41. package/src/connect/types.ts +48 -4
  42. package/src/public/client.ts +14 -2
  43. package/src/public/safe-markdown.ts +13 -0
  44. package/src/public/share-model.ts +6 -2
  45. package/src/public/site-model.ts +5 -3
  46. package/src/testing/auth-server.ts +377 -20
  47. package/src/testing/cloud.ts +83 -0
  48. package/src/testing/fixtures.ts +26 -0
  49. package/src/testing/index.ts +10 -6
  50. package/src/testing/seed.ts +120 -0
  51. package/dist/chunk-J7XHAAFJ.js.map +0 -1
package/dist/connect.cjs CHANGED
@@ -20,20 +20,25 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/connect/index.ts
21
21
  var connect_exports = {};
22
22
  __export(connect_exports, {
23
+ CLOUD_TRANSACTION_TTL_MS: () => CLOUD_TRANSACTION_TTL_MS,
23
24
  CONNECT_ERROR_CODES: () => CONNECT_ERROR_CODES,
24
25
  ConnectError: () => ConnectError,
25
26
  DEFAULT_PATHS: () => DEFAULT_PATHS,
26
27
  DEFAULT_REFRESH_IDLE_MS: () => DEFAULT_REFRESH_IDLE_MS,
28
+ MARKER_TTL_SEC: () => MARKER_TTL_SEC,
27
29
  MIN_SECRET_LENGTH: () => MIN_SECRET_LENGTH,
28
30
  REFRESH_WINDOW_MS: () => REFRESH_WINDOW_MS,
29
31
  ROTATION_MEMORY_MS: () => ROTATION_MEMORY_MS,
30
32
  TRANSACTION_TTL_MS: () => TRANSACTION_TTL_MS,
33
+ acceptCloudReturn: () => acceptCloudReturn,
31
34
  acceptLaunch: () => acceptLaunch,
32
35
  applySetCookies: () => applySetCookies,
33
36
  buildClientMetadata: () => buildClientMetadata,
34
37
  can: () => can,
35
38
  canonicalPodUrl: () => canonicalPodUrl,
39
+ cloudBaseUrl: () => cloudBaseUrl,
36
40
  cloudConnectAvailable: () => cloudConnectAvailable,
41
+ connectionActsAs: () => connectionActsAs,
37
42
  connectionExpiresAt: () => connectionExpiresAt,
38
43
  cookieNames: () => cookieNames,
39
44
  createPodAppAuth: () => createPodAppAuth,
@@ -41,17 +46,22 @@ __export(connect_exports, {
41
46
  createSealedCookieStore: () => createSealedCookieStore,
42
47
  discoverPod: () => discoverPod,
43
48
  handleCallback: () => handleCallback,
49
+ humanSlots: () => humanSlots,
44
50
  isBrowserRuntime: () => isBrowserRuntime,
45
51
  isConnectError: () => isConnectError,
52
+ isPrefetchOrAsset: () => isPrefetchOrAsset,
53
+ markerCookie: () => markerCookie,
46
54
  openTransaction: () => openTransaction,
47
55
  parseConnectErrorCode: () => parseConnectErrorCode,
48
56
  parseTokenResponse: () => parseTokenResponse,
49
57
  pkceChallenge: () => pkceChallenge,
58
+ readMarker: () => readMarker,
50
59
  refreshConnection: () => refreshConnection,
51
60
  reviewUrlFor: () => reviewUrlFor,
52
61
  revokeConnection: () => revokeConnection,
53
62
  safeNextPath: () => safeNextPath,
54
63
  seal: () => seal,
64
+ spaces: () => spaces,
55
65
  startCloudConnect: () => startCloudConnect,
56
66
  startConnect: () => startConnect,
57
67
  unseal: () => unseal
@@ -99,6 +109,7 @@ var CONNECT_ERROR_CODES = [
99
109
  "rate_limited",
100
110
  "expired",
101
111
  "revoked",
112
+ "pod_moved",
102
113
  "server_error"
103
114
  ];
104
115
  function parseConnectErrorCode(value) {
@@ -134,11 +145,52 @@ function retryAfterSeconds(header, now) {
134
145
  return Math.max(0, Math.ceil((at - now) / 1e3));
135
146
  }
136
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
+
137
190
  // src/connect/discovery.ts
138
191
  var METADATA_PATH = "/.well-known/oauth-authorization-server";
139
192
  var TIMEOUT_MS = 5e3;
140
193
  var MAX_METADATA_BYTES = 64 * 1024;
141
- var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
142
194
  function isPrivateIpv4(host) {
143
195
  const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
144
196
  if (!m) return false;
@@ -191,10 +243,12 @@ function sameOriginEndpoint(value, issuer) {
191
243
  }
192
244
  }
193
245
  async function readJsonCapped(res) {
194
- const text = await res.text();
195
- 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
+ }
196
250
  try {
197
- return JSON.parse(text);
251
+ return JSON.parse(read.text);
198
252
  } catch {
199
253
  throw new ConnectError("not_a_pod", "Metadata is not JSON");
200
254
  }
@@ -207,18 +261,27 @@ async function discoverIssuer(issuer, opts) {
207
261
  method: "GET",
208
262
  headers: { Accept: "application/json" },
209
263
  redirect: "manual",
210
- signal: typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(TIMEOUT_MS) : void 0
264
+ signal: timeoutSignal(TIMEOUT_MS)
211
265
  });
212
266
  } catch {
213
267
  throw new ConnectError("pod_unreachable", "The pod did not answer");
214
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
+ }
215
274
  if (res.status === 429) {
216
275
  throw new ConnectError("rate_limited", "The pod asked to slow down", {
217
276
  retryAfterSec: retryAfterSeconds(res.headers.get("retry-after"), (opts.now ?? Date.now)())
218
277
  });
219
278
  }
220
279
  if (res.status >= 500) throw new ConnectError("pod_unreachable", `The pod answered ${res.status}`);
221
- 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
+ }
222
285
  const doc = await readJsonCapped(res);
223
286
  if (!doc || typeof doc !== "object" || Array.isArray(doc)) throw new ConnectError("not_a_pod", "Metadata is not an object");
224
287
  const m = doc;
@@ -244,27 +307,38 @@ async function discoverIssuer(issuer, opts) {
244
307
  refreshSupported: list(m.grant_types_supported)?.includes("refresh_token") ?? false
245
308
  };
246
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
+ }
247
323
  async function discoverPod(input, opts = {}) {
248
324
  const first = canonicalPodUrl(input, opts);
325
+ let firstError;
249
326
  try {
250
327
  return await discoverIssuer(first, opts);
251
328
  } catch (err) {
252
- const url = new URL(first);
253
- const retry = err instanceof ConnectError && (err.code === "not_a_pod" || err.code === "pod_unreachable") && !url.hostname.startsWith("pod.") && url.pathname === "/" && url.hostname.includes(".");
254
- if (!retry) throw err;
255
- 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)) {
256
335
  try {
257
- return await discoverIssuer(second, opts);
258
- } catch (err2) {
259
- 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;
260
339
  }
261
340
  }
262
- }
263
- function cloudConnectAvailable() {
264
- return false;
265
- }
266
- function startCloudConnect(_opts) {
267
- throw new ConnectError("cp_unavailable", "cp: not available \u2014 connecting through Synap Cloud is not built yet; use a pod address");
341
+ throw tooOld ?? firstError;
268
342
  }
269
343
  function safeNextPath(value, appOrigin) {
270
344
  if (!value || value.length > 2048) return null;
@@ -295,10 +369,14 @@ function acceptLaunch(url, appOrigin, opts = {}) {
295
369
  }
296
370
  }
297
371
  const hint = params.get("synap_space_hint");
372
+ const state = params.get("state");
298
373
  return {
299
374
  pod,
300
375
  next: safeNextPath(params.get("target_link_uri"), appOrigin),
301
- spaceHint: hint && /^[A-Za-z0-9_-]{1,64}$/.test(hint) ? hint : null
376
+ spaceHint: hint && SPACE_HINT_RE.test(hint) ? hint : null,
377
+ // A browser launch never carries a state: any well-formed one is a Synap Cloud return
378
+ // (a pick: iss + state; a cancel: state + error=access_denied, no iss).
379
+ cloudState: state && CLOUD_STATE_RE.test(state) ? state : null
302
380
  };
303
381
  }
304
382
 
@@ -368,22 +446,242 @@ function serializeCookie(name, value, attrs) {
368
446
  return parts.join("; ");
369
447
  }
370
448
 
449
+ // src/connect/seal.ts
450
+ var VERSION = "v1";
451
+ var IV_BYTES = 12;
452
+ var TAG_BYTES = 16;
453
+ var MIN_SECRET_LENGTH = 32;
454
+ var SALT = utf8("synap-connect/v1");
455
+ var keyCache = /* @__PURE__ */ new Map();
456
+ function secretsOf(secret) {
457
+ const list = typeof secret === "string" ? [secret] : secret;
458
+ if (list.length === 0 || list.some((s) => typeof s !== "string" || s.length < MIN_SECRET_LENGTH)) {
459
+ throw new ConnectError(
460
+ "misconfigured",
461
+ `The app session secret must be at least ${MIN_SECRET_LENGTH} characters (set SYNAP_APP_SESSION_SECRET on the server).`
462
+ );
463
+ }
464
+ return list;
465
+ }
466
+ function assertAppSecret(secret) {
467
+ secretsOf(secret);
468
+ }
469
+ function keyFor(secret, purpose) {
470
+ const cacheKey = `${purpose}\0${secret}`;
471
+ let key = keyCache.get(cacheKey);
472
+ if (!key) {
473
+ key = crypto.subtle.importKey("raw", utf8(secret), "HKDF", false, ["deriveKey"]).then(
474
+ (base) => crypto.subtle.deriveKey(
475
+ { name: "HKDF", hash: "SHA-256", salt: SALT, info: utf8(purpose) },
476
+ base,
477
+ { name: "AES-GCM", length: 256 },
478
+ false,
479
+ ["encrypt", "decrypt"]
480
+ )
481
+ );
482
+ keyCache.set(cacheKey, key);
483
+ }
484
+ return key;
485
+ }
486
+ async function seal(value, opts) {
487
+ const [current] = secretsOf(opts.secret);
488
+ const iv = randomBytes(IV_BYTES);
489
+ const plaintext = utf8(JSON.stringify({ exp: opts.expiresAt, data: value }));
490
+ const ciphertext = await crypto.subtle.encrypt(
491
+ { name: "AES-GCM", iv, additionalData: utf8(opts.purpose) },
492
+ await keyFor(current, opts.purpose),
493
+ plaintext
494
+ );
495
+ const out = new Uint8Array(IV_BYTES + ciphertext.byteLength);
496
+ out.set(iv, 0);
497
+ out.set(new Uint8Array(ciphertext), IV_BYTES);
498
+ return `${VERSION}.${base64url(out)}`;
499
+ }
500
+ async function unseal(sealed, opts) {
501
+ const secrets = secretsOf(opts.secret);
502
+ const dot = sealed.indexOf(".");
503
+ if (dot < 0 || sealed.slice(0, dot) !== VERSION) return { ok: false, reason: "invalid" };
504
+ const bytes = fromBase64url(sealed.slice(dot + 1));
505
+ if (!bytes || bytes.length < IV_BYTES + TAG_BYTES) return { ok: false, reason: "invalid" };
506
+ const iv = bytes.slice(0, IV_BYTES);
507
+ const ciphertext = bytes.slice(IV_BYTES);
508
+ for (const secret of secrets) {
509
+ let plaintext;
510
+ try {
511
+ plaintext = await crypto.subtle.decrypt(
512
+ { name: "AES-GCM", iv, additionalData: utf8(opts.purpose) },
513
+ await keyFor(secret, opts.purpose),
514
+ ciphertext
515
+ );
516
+ } catch {
517
+ continue;
518
+ }
519
+ let parsed;
520
+ try {
521
+ parsed = JSON.parse(fromUtf8(new Uint8Array(plaintext)));
522
+ } catch {
523
+ return { ok: false, reason: "invalid" };
524
+ }
525
+ if (typeof parsed.exp !== "number") return { ok: false, reason: "invalid" };
526
+ if (parsed.exp <= opts.now) return { ok: false, reason: "expired" };
527
+ return { ok: true, value: parsed.data, expiresAt: parsed.exp };
528
+ }
529
+ return { ok: false, reason: "invalid" };
530
+ }
531
+
532
+ // src/connect/cloud.ts
533
+ var CLOUD_TRANSACTION_TTL_MS = 10 * 60 * 1e3;
534
+ function cloudBaseUrl(cloudUrl, opts = {}) {
535
+ let url;
536
+ try {
537
+ url = new URL(cloudUrl.trim());
538
+ } catch {
539
+ throw new ConnectError("misconfigured", "cloudUrl is not a URL");
540
+ }
541
+ const loopback = LOOPBACK_HOSTS.has(url.hostname);
542
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback && opts.allowInsecureLoopback)) {
543
+ throw new ConnectError("misconfigured", "cloudUrl must be https (http only on localhost in development)");
544
+ }
545
+ if (url.username || url.password || url.search || url.hash) {
546
+ throw new ConnectError("misconfigured", "cloudUrl has no credentials, query or fragment");
547
+ }
548
+ return `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
549
+ }
550
+ function cloudConnectAvailable(config) {
551
+ if (!config?.cloudUrl) return false;
552
+ try {
553
+ cloudBaseUrl(config.cloudUrl, config);
554
+ return true;
555
+ } catch {
556
+ return false;
557
+ }
558
+ }
559
+ function startCloudConnect(opts) {
560
+ if (!cloudConnectAvailable(opts)) {
561
+ throw new ConnectError("cp_unavailable", "cp: not configured \u2014 pass cloudUrl (the Control Plane URL) to offer Synap Cloud");
562
+ }
563
+ if (!opts.secret) throw new ConnectError("misconfigured", "startCloudConnect needs the app secret to seal its state");
564
+ const base = cloudBaseUrl(opts.cloudUrl, opts);
565
+ const secret = opts.secret;
566
+ return (async () => {
567
+ const now = (opts.now ?? Date.now)();
568
+ const state = randomToken();
569
+ const url = new URL(`${base}/connect/pod`);
570
+ url.searchParams.set("client_id", opts.clientId);
571
+ url.searchParams.set("state", state);
572
+ const tx = {
573
+ v: 1,
574
+ state,
575
+ clientId: opts.clientId,
576
+ cloudUrl: base,
577
+ next: opts.next ?? null,
578
+ spaceHint: opts.spaceHint ?? null,
579
+ prompt: opts.prompt === "consent" ? "consent" : null,
580
+ createdAt: now
581
+ };
582
+ const expiresAt = now + CLOUD_TRANSACTION_TTL_MS;
583
+ const transaction = await seal(tx, { secret, purpose: "synap-connect/cloud", expiresAt });
584
+ return { url: url.href, transaction, expiresAt };
585
+ })();
586
+ }
587
+ async function acceptCloudReturn(opts) {
588
+ if (!opts.transaction) throw new ConnectError("expired_transaction", "No Synap Cloud sign-in in progress in this browser");
589
+ const opened = await unseal(opts.transaction, {
590
+ secret: opts.secret,
591
+ purpose: "synap-connect/cloud",
592
+ now: (opts.now ?? Date.now)()
593
+ });
594
+ if (!opened.ok) {
595
+ throw new ConnectError(
596
+ "expired_transaction",
597
+ opened.reason === "expired" ? "The Synap Cloud sign-in is older than 10 minutes" : "The Synap Cloud cookie is not one this app sealed"
598
+ );
599
+ }
600
+ const tx = opened.value;
601
+ if (!tx || tx.v !== 1 || typeof tx.state !== "string") {
602
+ throw new ConnectError("expired_transaction", "The Synap Cloud cookie is not one this app sealed");
603
+ }
604
+ if (tx.clientId !== opts.clientId) {
605
+ throw new ConnectError("misconfigured", "The Synap Cloud sign-in was started for another client_id");
606
+ }
607
+ const params = new URL(opts.url).searchParams;
608
+ const state = params.get("state");
609
+ if (state === null || !CLOUD_STATE_RE.test(state) || !constantTimeEqual(state, tx.state)) {
610
+ throw new ConnectError("mismatch", "The Synap Cloud answer does not belong to the sign-in this browser started");
611
+ }
612
+ const error = params.get("error");
613
+ if (error !== null) {
614
+ throw new ConnectError(fromOAuthError(error, "authorize"), `Synap Cloud answered ${error}`, { oauthError: error });
615
+ }
616
+ const iss = params.get("iss");
617
+ if (!iss) throw new ConnectError("mismatch", "The Synap Cloud answer names no pod");
618
+ return {
619
+ pod: canonicalPodUrl(iss, opts),
620
+ next: tx.next,
621
+ spaceHint: tx.spaceHint,
622
+ prompt: tx.prompt === "consent" ? "consent" : null
623
+ };
624
+ }
625
+
371
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
+ }
372
633
  function can(connection, permission, workspaceId) {
373
634
  return connection.grant.some((g) => g.permission === permission && (workspaceId === void 0 || g.workspaceId === workspaceId));
374
635
  }
636
+ function spaces(connection) {
637
+ const bySpace = /* @__PURE__ */ new Map();
638
+ for (const g of connection.grant) {
639
+ const space = bySpace.get(g.workspaceId) ?? { workspaceId: g.workspaceId, workspaceName: g.workspaceName, permissions: [] };
640
+ if (!space.workspaceName && g.workspaceName) space.workspaceName = g.workspaceName;
641
+ if (!space.templateSlug && g.templateSlug) space.templateSlug = g.templateSlug;
642
+ if (!space.permissions.includes(g.permission)) space.permissions.push(g.permission);
643
+ bySpace.set(g.workspaceId, space);
644
+ }
645
+ return [...bySpace.values()];
646
+ }
647
+ var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
648
+ var PROPOSAL_IDS_MAX = 100;
375
649
  var SAFE_PATH = /^\/(?!\/)[A-Za-z0-9\-._~!$&'()*+,;=:@%/]*$/;
650
+ var TIMEOUT_MS2 = 1e4;
651
+ var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
376
652
  function hubUrl(podUrl, path, query) {
377
- if (!SAFE_PATH.test(path) || /(^|\/)\.\.?(\/|$)/.test(path) || path.includes("\\")) {
378
- 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;
379
659
  }
380
- 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;
381
661
  for (const [key, value] of Object.entries(query ?? {})) {
382
662
  if (value === void 0 || value === null) continue;
383
663
  for (const v of Array.isArray(value) ? value : [value]) url.searchParams.append(key, String(v));
384
664
  }
385
665
  return url;
386
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
+ }
387
685
  function errorFor(status, body, retryAfter, now) {
388
686
  const obj = body && typeof body === "object" ? body : {};
389
687
  const detail = typeof obj.error === "string" ? obj.error : typeof obj.message === "string" ? obj.message : void 0;
@@ -398,6 +696,7 @@ function errorFor(status, body, retryAfter, now) {
398
696
  return { code: "rate_limited", ...base, ...retryAfterSec !== void 0 ? { retryAfterSec } : {} };
399
697
  }
400
698
  if (status === 502 || status === 503 || status === 504) return { code: "pod_unreachable", ...base };
699
+ if (status >= 300 && status < 400) return { code: "pod_moved", ...base };
401
700
  return { code: "server", ...base };
402
701
  }
403
702
  function reviewUrlFor(podUrl, reviewUrl, proposalId) {
@@ -410,13 +709,37 @@ function reviewUrlFor(podUrl, reviewUrl, proposalId) {
410
709
  }
411
710
  return proposalId ? `${podUrl}/open/${encodeURIComponent(proposalId)}` : podUrl;
412
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
+ }
413
721
  function createPodClient(connection, opts = {}) {
414
722
  const now = opts.now ?? Date.now;
415
723
  const doFetch = () => opts.fetch ?? fetch;
416
- async function request(method, path, body, query) {
417
- const url = hubUrl(connection.podUrl, path, query);
418
- if (connection.accessExpiresAt <= now()) return { ok: false, error: { code: "expired", status: null } };
419
- 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}` };
420
743
  if (body !== void 0) headers["Content-Type"] = "application/json";
421
744
  let res;
422
745
  try {
@@ -424,21 +747,33 @@ function createPodClient(connection, opts = {}) {
424
747
  method,
425
748
  headers,
426
749
  body: body === void 0 ? void 0 : JSON.stringify(body),
427
- redirect: "manual"
750
+ redirect: "manual",
751
+ signal: timeoutSignal(opts.timeoutMs ?? TIMEOUT_MS2)
428
752
  });
429
753
  } catch {
430
754
  return { ok: false, error: { code: "pod_unreachable", status: null } };
431
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
+ }
432
760
  let data = null;
433
- const text = await res.text().catch(() => "");
434
- if (text) {
761
+ if (read2.text) {
435
762
  try {
436
- data = JSON.parse(text);
763
+ data = JSON.parse(read2.text);
437
764
  } catch {
438
765
  data = null;
439
766
  }
440
767
  }
441
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
+ }
442
777
  return { ok: false, error: errorFor(res.status, data, res.headers.get("retry-after"), now()) };
443
778
  }
444
779
  async function write(method, path, body) {
@@ -451,17 +786,189 @@ function createPodClient(connection, opts = {}) {
451
786
  ok: true,
452
787
  status: "proposed",
453
788
  proposalId,
454
- reviewUrl: reviewUrlFor(connection.podUrl, obj.reviewUrl, proposalId),
789
+ reviewUrl: reviewUrlFor(current.podUrl, obj.reviewUrl, proposalId),
455
790
  data: result.data
456
791
  };
457
792
  }
458
793
  return { ok: true, status: "applied", data: result.data };
459
794
  }
795
+ async function listPendingProposals(opts2 = {}) {
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);
806
+ }
807
+ const limit = Math.min(Math.max(Math.floor(opts2.limit ?? 20), 1), 200);
808
+ const offset = Math.max(Math.floor(opts2.offset ?? 0), 0);
809
+ const result = await request("GET", "/proposals", void 0, {
810
+ status: "pending",
811
+ view: "basic",
812
+ limit,
813
+ offset,
814
+ workspaceId: opts2.workspaceId,
815
+ ...only ? { proposalIds: [...only].join(",") } : {}
816
+ });
817
+ if (!result.ok) return result;
818
+ const obj = result.data && typeof result.data === "object" ? result.data : {};
819
+ if (!Array.isArray(obj.proposals) || typeof obj.total !== "number") {
820
+ return { ok: false, error: { code: "server", status: 200, detail: "The pod's /proposals answer has no proposals or total" } };
821
+ }
822
+ const str = (v) => typeof v === "string" && v ? v : null;
823
+ const proposals = [];
824
+ for (const row of obj.proposals) {
825
+ if (!row || typeof row !== "object") continue;
826
+ const r = row;
827
+ const id = str(r.id);
828
+ if (!id || only && !only.has(id)) continue;
829
+ proposals.push({
830
+ id,
831
+ proposalType: str(r.proposalType) ?? "",
832
+ targetType: str(r.targetType) ?? "",
833
+ targetId: str(r.targetId),
834
+ workspaceId: str(r.workspaceId),
835
+ createdAt: str(r.createdAt) ?? "",
836
+ agentUserId: str(r.agentUserId),
837
+ sessionId: str(r.sessionId),
838
+ ...str(r.summary) ? { summary: str(r.summary) } : {},
839
+ reviewUrl: reviewUrlFor(current.podUrl, null, id)
840
+ });
841
+ }
842
+ return {
843
+ ok: true,
844
+ data: { total: obj.total, proposals, hasMore: obj.hasMore === true || offset + proposals.length < obj.total }
845
+ };
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
+ }
460
951
  return {
461
952
  podUrl: connection.podUrl,
462
- user: connection.user,
463
- grant: connection.grant,
464
- can: (permission, workspaceId) => can(connection, permission, workspaceId),
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),
964
+ listPendingProposals,
965
+ listEntities,
966
+ getEntity,
967
+ getEntityConnections,
968
+ getDocument,
969
+ listFocusSessions,
970
+ getFileUrl,
971
+ approveProposal,
465
972
  get(path, query) {
466
973
  return request("GET", path, void 0, query);
467
974
  },
@@ -480,94 +987,12 @@ function createPodClient(connection, opts = {}) {
480
987
  };
481
988
  }
482
989
 
483
- // src/connect/seal.ts
484
- var VERSION = "v1";
485
- var IV_BYTES = 12;
486
- var TAG_BYTES = 16;
487
- var MIN_SECRET_LENGTH = 32;
488
- var SALT = utf8("synap-connect/v1");
489
- var keyCache = /* @__PURE__ */ new Map();
490
- function secretsOf(secret) {
491
- const list = typeof secret === "string" ? [secret] : secret;
492
- if (list.length === 0 || list.some((s) => typeof s !== "string" || s.length < MIN_SECRET_LENGTH)) {
493
- throw new ConnectError(
494
- "misconfigured",
495
- `The app session secret must be at least ${MIN_SECRET_LENGTH} characters (set SYNAP_APP_SESSION_SECRET on the server).`
496
- );
497
- }
498
- return list;
499
- }
500
- function assertAppSecret(secret) {
501
- secretsOf(secret);
502
- }
503
- function keyFor(secret, purpose) {
504
- const cacheKey = `${purpose}\0${secret}`;
505
- let key = keyCache.get(cacheKey);
506
- if (!key) {
507
- key = crypto.subtle.importKey("raw", utf8(secret), "HKDF", false, ["deriveKey"]).then(
508
- (base) => crypto.subtle.deriveKey(
509
- { name: "HKDF", hash: "SHA-256", salt: SALT, info: utf8(purpose) },
510
- base,
511
- { name: "AES-GCM", length: 256 },
512
- false,
513
- ["encrypt", "decrypt"]
514
- )
515
- );
516
- keyCache.set(cacheKey, key);
517
- }
518
- return key;
519
- }
520
- async function seal(value, opts) {
521
- const [current] = secretsOf(opts.secret);
522
- const iv = randomBytes(IV_BYTES);
523
- const plaintext = utf8(JSON.stringify({ exp: opts.expiresAt, data: value }));
524
- const ciphertext = await crypto.subtle.encrypt(
525
- { name: "AES-GCM", iv, additionalData: utf8(opts.purpose) },
526
- await keyFor(current, opts.purpose),
527
- plaintext
528
- );
529
- const out = new Uint8Array(IV_BYTES + ciphertext.byteLength);
530
- out.set(iv, 0);
531
- out.set(new Uint8Array(ciphertext), IV_BYTES);
532
- return `${VERSION}.${base64url(out)}`;
533
- }
534
- async function unseal(sealed, opts) {
535
- const secrets = secretsOf(opts.secret);
536
- const dot = sealed.indexOf(".");
537
- if (dot < 0 || sealed.slice(0, dot) !== VERSION) return { ok: false, reason: "invalid" };
538
- const bytes = fromBase64url(sealed.slice(dot + 1));
539
- if (!bytes || bytes.length < IV_BYTES + TAG_BYTES) return { ok: false, reason: "invalid" };
540
- const iv = bytes.slice(0, IV_BYTES);
541
- const ciphertext = bytes.slice(IV_BYTES);
542
- for (const secret of secrets) {
543
- let plaintext;
544
- try {
545
- plaintext = await crypto.subtle.decrypt(
546
- { name: "AES-GCM", iv, additionalData: utf8(opts.purpose) },
547
- await keyFor(secret, opts.purpose),
548
- ciphertext
549
- );
550
- } catch {
551
- continue;
552
- }
553
- let parsed;
554
- try {
555
- parsed = JSON.parse(fromUtf8(new Uint8Array(plaintext)));
556
- } catch {
557
- return { ok: false, reason: "invalid" };
558
- }
559
- if (typeof parsed.exp !== "number") return { ok: false, reason: "invalid" };
560
- if (parsed.exp <= opts.now) return { ok: false, reason: "expired" };
561
- return { ok: true, value: parsed.data, expiresAt: parsed.exp };
562
- }
563
- return { ok: false, reason: "invalid" };
564
- }
565
-
566
990
  // src/connect/flow.ts
567
991
  var TRANSACTION_TTL_MS = 10 * 60 * 1e3;
568
992
  var DEFAULT_REFRESH_IDLE_MS = 30 * 24 * 60 * 60 * 1e3;
569
993
  var DEFAULT_ACCESS_TTL_SEC = 3600;
570
- var TIMEOUT_MS2 = 1e4;
994
+ var TIMEOUT_MS3 = 1e4;
995
+ var MAX_TOKEN_RESPONSE_BYTES = 64 * 1024;
571
996
  var PERMISSION_RE = /^[a-z][a-z0-9_-]*(\.[a-z0-9_-]+)+$/i;
572
997
  function assertPermissions(permissions) {
573
998
  const unique = [...new Set(permissions)];
@@ -597,6 +1022,7 @@ async function startConnect(opts) {
597
1022
  url.searchParams.set("code_challenge_method", "S256");
598
1023
  if (resource) url.searchParams.set("resource", resource);
599
1024
  if (opts.spaceHint) url.searchParams.set("synap_space_hint", opts.spaceHint);
1025
+ if (opts.prompt === "consent") url.searchParams.set("prompt", "consent");
600
1026
  const tx = {
601
1027
  v: 1,
602
1028
  issuer: issuer.issuer,
@@ -684,20 +1110,29 @@ async function tokenRequest(endpoint, body, during, opts) {
684
1110
  headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
685
1111
  body: body.toString(),
686
1112
  redirect: "manual",
687
- signal: typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(TIMEOUT_MS2) : void 0
1113
+ signal: timeoutSignal(TIMEOUT_MS3)
688
1114
  });
689
1115
  } catch {
690
1116
  throw new ConnectError("pod_unreachable", "The pod's token endpoint did not answer");
691
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
+ }
692
1123
  if (res.status === 429) {
693
1124
  throw new ConnectError("rate_limited", "The pod asked to slow down", {
694
1125
  retryAfterSec: retryAfterSeconds(res.headers.get("retry-after"), (opts.now ?? Date.now)())
695
1126
  });
696
1127
  }
697
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
+ }
698
1133
  let json;
699
1134
  try {
700
- json = await res.json();
1135
+ json = JSON.parse(read.text);
701
1136
  } catch {
702
1137
  throw new ConnectError("server_error", `The token endpoint answered ${res.status} without JSON`);
703
1138
  }
@@ -713,9 +1148,30 @@ function parseGrant(value) {
713
1148
  const out = [];
714
1149
  for (const g of value) {
715
1150
  if (!g || typeof g !== "object") return null;
716
- const { permission, workspaceId, workspaceName } = g;
1151
+ const { permission, workspaceId, workspaceName, templateSlug } = g;
717
1152
  if (typeof permission !== "string" || typeof workspaceId !== "string") return null;
718
- 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);
1156
+ }
1157
+ return out;
1158
+ }
1159
+ function parseActsAs(value) {
1160
+ return value === "person" || value === "agent" ? value : void 0;
1161
+ }
1162
+ var APP_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
1163
+ function parseApp(value) {
1164
+ if (!value || typeof value !== "object") return {};
1165
+ const { id, manageUrl } = value;
1166
+ const out = {};
1167
+ if (typeof id === "string" && APP_ID_RE.test(id)) out.appId = id;
1168
+ if (typeof manageUrl === "string" && manageUrl.length <= 2048) {
1169
+ try {
1170
+ const url = new URL(manageUrl);
1171
+ const secure = url.protocol === "https:" || url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname);
1172
+ if (secure && !url.username && !url.password) out.manageUrl = url.href;
1173
+ } catch {
1174
+ }
719
1175
  }
720
1176
  return out;
721
1177
  }
@@ -753,6 +1209,13 @@ function parseTokenResponse(body, ctx) {
753
1209
  user,
754
1210
  connectedAt: ctx.previous?.connectedAt ?? ctx.now
755
1211
  };
1212
+ const app = parseApp(body.synap_app);
1213
+ const appId = app.appId ?? ctx.previous?.appId;
1214
+ const manageUrl = app.manageUrl ?? ctx.previous?.manageUrl;
1215
+ if (appId !== void 0) connection.appId = appId;
1216
+ if (manageUrl !== void 0) connection.manageUrl = manageUrl;
1217
+ const actsAs = parseActsAs(body.synap_acts_as);
1218
+ if (actsAs !== void 0) connection.actsAs = actsAs;
756
1219
  if (typeof refresh_token === "string" && refresh_token !== "") {
757
1220
  connection.refreshToken = refresh_token;
758
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;
@@ -818,7 +1281,9 @@ async function revokeConnection(connection, opts = {}) {
818
1281
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
819
1282
  body: new URLSearchParams({ token, token_type_hint: hint, client_id: connection.clientId }).toString(),
820
1283
  redirect: "manual",
821
- signal: typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(TIMEOUT_MS2) : void 0
1284
+ signal: timeoutSignal(TIMEOUT_MS3)
1285
+ });
1286
+ await res.body?.cancel().catch(() => {
822
1287
  });
823
1288
  if (res.status !== 200) revoked = false;
824
1289
  } catch {
@@ -830,7 +1295,25 @@ async function revokeConnection(connection, opts = {}) {
830
1295
 
831
1296
  // src/connect/store.ts
832
1297
  function cookieNames(secure) {
833
- return secure ? { session: "__Host-synap_session", transaction: "__Host-synap_tx" } : { session: "synap_session", transaction: "synap_tx" };
1298
+ return secure ? { session: "__Host-synap_session", transaction: "__Host-synap_tx", cloud: "__Host-synap_cloud", marker: "__Host-synap_seen" } : { session: "synap_session", transaction: "synap_tx", cloud: "synap_cloud", marker: "synap_seen" };
1299
+ }
1300
+ var MARKER_TTL_SEC = 400 * 24 * 60 * 60;
1301
+ function markerCookie(podUrl, opts) {
1302
+ return serializeCookie(opts.name ?? cookieNames(opts.secure).marker, base64url(utf8(podUrl)), {
1303
+ maxAgeSec: MARKER_TTL_SEC,
1304
+ secure: opts.secure
1305
+ });
1306
+ }
1307
+ function readMarker(cookieHeader, opts) {
1308
+ const raw = parseCookies(cookieHeader).get(opts.name ?? cookieNames(opts.secure).marker);
1309
+ const bytes = raw ? fromBase64url(raw) : null;
1310
+ if (!bytes || bytes.length > 512) return null;
1311
+ const podUrl = fromUtf8(bytes);
1312
+ try {
1313
+ return canonicalPodUrl(podUrl, opts) === podUrl ? podUrl : null;
1314
+ } catch {
1315
+ return null;
1316
+ }
834
1317
  }
835
1318
  var CHUNK_SIZE = 3800;
836
1319
  var MAX_CHUNKS = 4;
@@ -842,6 +1325,7 @@ function pack(c) {
842
1325
  for (const g of c.grant) {
843
1326
  const row = bySpace.get(g.workspaceId) ?? [g.workspaceId, g.workspaceName, []];
844
1327
  row[2].push(g.permission);
1328
+ if (g.templateSlug && row[3] === void 0) row[3] = g.templateSlug;
845
1329
  bySpace.set(g.workspaceId, row);
846
1330
  }
847
1331
  const { grant: _grant, ...rest } = c;
@@ -851,7 +1335,7 @@ function unpack(s) {
851
1335
  if (!s || typeof s.podUrl !== "string" || typeof s.accessToken !== "string" || !Array.isArray(s.g)) return null;
852
1336
  const { g, ...rest } = s;
853
1337
  const grant = g.flatMap(
854
- ([workspaceId, workspaceName, permissions]) => permissions.map((permission) => ({ permission, workspaceId, workspaceName }))
1338
+ ([workspaceId, workspaceName, permissions, templateSlug]) => permissions.map((permission) => ({ permission, workspaceId, workspaceName, ...typeof templateSlug === "string" ? { templateSlug } : {} }))
855
1339
  );
856
1340
  return { ...rest, grant };
857
1341
  }
@@ -924,14 +1408,36 @@ var DEFAULT_PATHS = {
924
1408
  connect: "/api/synap/connect",
925
1409
  callback: "/api/synap/callback",
926
1410
  disconnect: "/api/synap/disconnect",
1411
+ cloud: "/api/synap/cloud",
927
1412
  connectPage: "/connect",
928
1413
  resultPage: "/connect/result",
929
1414
  afterConnect: "/"
930
1415
  };
931
1416
  var REFRESH_WINDOW_MS = 5 * 60 * 1e3;
932
- var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
933
1417
  var TEMPLATE_SLUG_RE = /^[a-z0-9][a-z0-9._/-]{0,99}$/;
934
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
+ }
935
1441
  function appOriginOf(appUrl) {
936
1442
  let url;
937
1443
  try {
@@ -940,8 +1446,11 @@ function appOriginOf(appUrl) {
940
1446
  throw new ConnectError("misconfigured", "appUrl is not a URL");
941
1447
  }
942
1448
  const secure = url.protocol === "https:";
943
- if (!secure && !(url.protocol === "http:" && LOOPBACK.has(url.hostname))) {
944
- 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");
945
1454
  }
946
1455
  if (url.pathname !== "/" || url.search || url.hash || url.username || url.password) {
947
1456
  throw new ConnectError("misconfigured", "appUrl is an origin: no path, query, fragment or credentials");
@@ -959,9 +1468,12 @@ function buildClientMetadata(opts) {
959
1468
  for (const [name, path] of Object.entries(paths)) assertPath(name, path);
960
1469
  const clientName = opts.clientName.trim();
961
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
+ }
962
1474
  assertPermissions(opts.requests.map((r) => r.permission));
963
1475
  for (const r of opts.requests) {
964
- if (r.permission.includes("*")) throw new ConnectError("misconfigured", "a permission cannot contain *");
1476
+ assertAppPermission(r.permission, origin);
965
1477
  if (typeof r.reason !== "string" || !r.reason.trim() || r.reason.length > 200) {
966
1478
  throw new ConnectError("misconfigured", `the reason for ${r.permission} must be 1-200 characters`);
967
1479
  }
@@ -1015,7 +1527,7 @@ async function readForm(request) {
1015
1527
  }
1016
1528
  function isPrefetchOrAsset(request) {
1017
1529
  const h = request.headers;
1018
- if (h.get("next-router-prefetch") || h.get("x-middleware-prefetch")) return true;
1530
+ if (h.get("next-router-prefetch") || h.get("next-router-segment-prefetch") || h.get("x-middleware-prefetch")) return true;
1019
1531
  if (/prefetch/i.test(h.get("purpose") ?? "") || /prefetch/i.test(h.get("sec-purpose") ?? "")) return true;
1020
1532
  const path = new URL(request.url).pathname;
1021
1533
  return path.startsWith("/_next/") || /\.[A-Za-z0-9]{1,8}$/.test(path);
@@ -1026,6 +1538,7 @@ function createPodAppAuth(config) {
1026
1538
  const { origin, secure } = appOriginOf(config.appUrl);
1027
1539
  const paths = { ...DEFAULT_PATHS, ...config.paths };
1028
1540
  const metadata = buildClientMetadata({ ...config, paths });
1541
+ const cloudUrl = config.cloudUrl?.trim() ? cloudBaseUrl(config.cloudUrl, config) : null;
1029
1542
  const clientId = metadata.client_id;
1030
1543
  const redirectUri = metadata.redirect_uris[0];
1031
1544
  const permissions = config.requests.map((r) => r.permission);
@@ -1042,6 +1555,37 @@ function createPodAppAuth(config) {
1042
1555
  return e;
1043
1556
  };
1044
1557
  const clearTx = serializeCookie(names.transaction, "", { maxAgeSec: 0, secure });
1558
+ const clearCloud = serializeCookie(names.cloud, "", { maxAgeSec: 0, secure });
1559
+ const clearMarker = serializeCookie(names.marker, "", { maxAgeSec: 0, secure });
1560
+ const marker = (podUrl) => markerCookie(podUrl, { secure, name: names.marker });
1561
+ const asRequest = (source) => source instanceof Request ? source : new Request(origin, { headers: source ? { cookie: source } : {} });
1562
+ const lapsedIn = (request) => {
1563
+ const podUrl = readMarker(request.headers.get("cookie"), { secure, name: names.marker, allowInsecureLoopback: config.allowInsecureLoopback });
1564
+ return podUrl ? { podUrl } : null;
1565
+ };
1566
+ async function toPod(opts, extra = []) {
1567
+ const started = await startConnect({
1568
+ ...flow,
1569
+ pod: opts.pod,
1570
+ clientId,
1571
+ redirectUri,
1572
+ permissions,
1573
+ secret: config.secret,
1574
+ resource: config.resource,
1575
+ spaceHint: opts.space && SPACE_HINT_RE.test(opts.space) ? opts.space : null,
1576
+ next: opts.next,
1577
+ prompt: opts.prompt,
1578
+ allowInsecureLoopback: config.allowInsecureLoopback
1579
+ });
1580
+ return redirect(started.authorizeUrl, [
1581
+ ...extra,
1582
+ serializeCookie(names.transaction, started.transaction, { maxAgeSec: TRANSACTION_TTL_MS / 1e3, secure })
1583
+ ]);
1584
+ }
1585
+ async function liveConnectionTo(request, pod) {
1586
+ const existing = await store.read(request);
1587
+ return Boolean(existing && existing.accessExpiresAt > now() && existing.podUrl === canonicalPodUrl(pod, config));
1588
+ }
1045
1589
  async function clientMetadata() {
1046
1590
  return new Response(JSON.stringify(metadata), {
1047
1591
  status: 200,
@@ -1055,26 +1599,10 @@ function createPodAppAuth(config) {
1055
1599
  const pod = formField(form, "pod")?.slice(0, 512) ?? "";
1056
1600
  const next = safeNextPath(formField(form, "next"), origin) ?? paths.afterConnect;
1057
1601
  const space = formField(form, "space");
1602
+ const prompt = formField(form, "prompt") === "consent" ? "consent" : null;
1058
1603
  try {
1059
- const existing = await store.read(request);
1060
- if (existing && existing.accessExpiresAt > now() && existing.podUrl === canonicalPodUrl(pod, config)) {
1061
- return redirect(new URL(next, origin).href);
1062
- }
1063
- const started = await startConnect({
1064
- ...flow,
1065
- pod,
1066
- clientId,
1067
- redirectUri,
1068
- permissions,
1069
- secret: config.secret,
1070
- resource: config.resource,
1071
- spaceHint: space && /^[A-Za-z0-9_-]{1,64}$/.test(space) ? space : null,
1072
- next,
1073
- allowInsecureLoopback: config.allowInsecureLoopback
1074
- });
1075
- return redirect(started.authorizeUrl, [
1076
- serializeCookie(names.transaction, started.transaction, { maxAgeSec: TRANSACTION_TTL_MS / 1e3, secure })
1077
- ]);
1604
+ if (!prompt && await liveConnectionTo(request, pod)) return redirect(new URL(next, origin).href);
1605
+ return await toPod({ pod, next, space, prompt });
1078
1606
  } catch (err) {
1079
1607
  const e = fail(err, "connect");
1080
1608
  return redirect(withQuery(origin, paths.connectPage, { error: e.code, pod }));
@@ -1093,7 +1621,7 @@ function createPodAppAuth(config) {
1093
1621
  secret: config.secret
1094
1622
  });
1095
1623
  const cookies = await store.write(connection, request);
1096
- return redirect(new URL(next ?? paths.afterConnect, origin).href, [clearTx, ...cookies]);
1624
+ return redirect(new URL(next ?? paths.afterConnect, origin).href, [clearTx, ...cookies, marker(connection.podUrl)]);
1097
1625
  } catch (err) {
1098
1626
  const e = fail(err, "callback");
1099
1627
  return redirect(withQuery(origin, paths.resultPage, { error: e.code }), [clearTx]);
@@ -1108,9 +1636,56 @@ function createPodAppAuth(config) {
1108
1636
  const { revoked } = await revokeConnection(connection, flow);
1109
1637
  if (!revoked) report(new ConnectError("pod_unreachable", "The pod did not confirm the revocation; the cookie is cleared anyway"), "disconnect");
1110
1638
  }
1111
- return redirect(new URL(next, origin).href, await store.clear(request));
1639
+ return redirect(new URL(next, origin).href, [...await store.clear(request), clearMarker]);
1640
+ }
1641
+ async function cloudConnect(request) {
1642
+ if (request.method !== "POST") return new Response("Method Not Allowed", { status: 405, headers: { Allow: "POST" } });
1643
+ if (!sameOrigin(request, origin)) return new Response("Forbidden", { status: 403 });
1644
+ const form = await readForm(request);
1645
+ const next = safeNextPath(formField(form, "next"), origin) ?? paths.afterConnect;
1646
+ const space = formField(form, "space");
1647
+ try {
1648
+ const started = await startCloudConnect({
1649
+ cloudUrl,
1650
+ allowInsecureLoopback: config.allowInsecureLoopback,
1651
+ clientId,
1652
+ secret: config.secret,
1653
+ next,
1654
+ spaceHint: space && SPACE_HINT_RE.test(space) ? space : null,
1655
+ prompt: formField(form, "prompt") === "consent" ? "consent" : null,
1656
+ now
1657
+ });
1658
+ return redirect(started.url, [
1659
+ serializeCookie(names.cloud, started.transaction, { maxAgeSec: CLOUD_TRANSACTION_TTL_MS / 1e3, secure })
1660
+ ]);
1661
+ } catch (err) {
1662
+ const e = fail(err, "connect");
1663
+ return redirect(withQuery(origin, paths.connectPage, { error: e.code }));
1664
+ }
1112
1665
  }
1113
- async function refreshIfNeeded(request) {
1666
+ async function cloudReturn(request) {
1667
+ if (request.method !== "GET") return new Response("Method Not Allowed", { status: 405, headers: { Allow: "GET" } });
1668
+ let pod = null;
1669
+ try {
1670
+ const back = await acceptCloudReturn({
1671
+ url: request.url,
1672
+ transaction: parseCookies(request.headers.get("cookie")).get(names.cloud) ?? null,
1673
+ clientId,
1674
+ secret: config.secret,
1675
+ allowInsecureLoopback: config.allowInsecureLoopback,
1676
+ now
1677
+ });
1678
+ pod = back.pod;
1679
+ const next = back.next ?? paths.afterConnect;
1680
+ if (!back.prompt && await liveConnectionTo(request, back.pod)) return redirect(new URL(next, origin).href, [clearCloud]);
1681
+ return await toPod({ pod: back.pod, next, space: back.spaceHint, prompt: back.prompt }, [clearCloud]);
1682
+ } catch (err) {
1683
+ const e = fail(err, "connect");
1684
+ return redirect(withQuery(origin, paths.connectPage, { error: e.code, pod: pod ? new URL(pod).host : null }), [clearCloud]);
1685
+ }
1686
+ }
1687
+ const keptFailures = /* @__PURE__ */ new Map();
1688
+ async function refreshIfNeeded(request, opts = {}) {
1114
1689
  const connection = await store.read(request);
1115
1690
  const unchanged = (status, conn, error) => ({
1116
1691
  status,
@@ -1119,21 +1694,33 @@ function createPodAppAuth(config) {
1119
1694
  cookieHeader: null,
1120
1695
  ...error ? { error } : {}
1121
1696
  });
1122
- if (!connection) return unchanged("none", null);
1697
+ if (!connection) {
1698
+ const lapsed = lapsedIn(request);
1699
+ return lapsed ? { ...unchanged("none", null), lapsed } : unchanged("none", null);
1700
+ }
1123
1701
  if (isPrefetchOrAsset(request)) return unchanged("skipped", connection);
1124
1702
  const t = now();
1125
- const accessLives = connection.accessExpiresAt > t;
1126
- 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);
1127
1705
  if (!connection.refreshToken) {
1128
1706
  return accessLives ? unchanged("fresh", connection) : unchanged("expired", null, new ConnectError("expired", "No refresh token; connect again"));
1129
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);
1130
1711
  let next;
1131
1712
  try {
1132
1713
  next = await refreshConnection(connection, flow);
1133
1714
  } catch (err) {
1134
1715
  const e = fail(err, "refresh");
1135
- if (accessLives) return unchanged("kept", connection, e);
1136
- 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
+ }
1137
1724
  return unchanged("unavailable", null, e);
1138
1725
  }
1139
1726
  let setCookies;
@@ -1144,6 +1731,7 @@ function createPodAppAuth(config) {
1144
1731
  const cleared = await store.clear(request).catch(() => []);
1145
1732
  return { status: "expired", connection: null, setCookies: cleared, cookieHeader: null, error: e };
1146
1733
  }
1734
+ setCookies = [...setCookies, marker(next.podUrl)];
1147
1735
  return {
1148
1736
  status: "refreshed",
1149
1737
  connection: next,
@@ -1151,15 +1739,46 @@ function createPodAppAuth(config) {
1151
1739
  cookieHeader: applySetCookies(request.headers.get("cookie"), setCookies)
1152
1740
  };
1153
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
+ }
1154
1760
  return {
1155
1761
  clientId,
1156
1762
  redirectUri,
1157
1763
  metadata,
1158
1764
  paths,
1159
- handlers: { clientMetadata, connect, callback, disconnect },
1160
- readConnection: (source) => store.read(source instanceof Request ? source : new Request(origin, { headers: source ? { cookie: source } : {} })),
1765
+ cloudConnectAvailable: cloudUrl !== null,
1766
+ handlers: { clientMetadata, connect, callback, disconnect, cloudConnect, cloudReturn },
1767
+ readConnection: (source) => store.read(asRequest(source)),
1768
+ async readLapsed(source) {
1769
+ const request = asRequest(source);
1770
+ return await store.read(request) ? null : lapsedIn(request);
1771
+ },
1161
1772
  refreshIfNeeded,
1162
- 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
+ }
1163
1782
  };
1164
1783
  }
1165
1784
 
@@ -1167,20 +1786,25 @@ function createPodAppAuth(config) {
1167
1786
  assertServerRuntime();
1168
1787
  // Annotate the CommonJS export names for ESM import in node:
1169
1788
  0 && (module.exports = {
1789
+ CLOUD_TRANSACTION_TTL_MS,
1170
1790
  CONNECT_ERROR_CODES,
1171
1791
  ConnectError,
1172
1792
  DEFAULT_PATHS,
1173
1793
  DEFAULT_REFRESH_IDLE_MS,
1794
+ MARKER_TTL_SEC,
1174
1795
  MIN_SECRET_LENGTH,
1175
1796
  REFRESH_WINDOW_MS,
1176
1797
  ROTATION_MEMORY_MS,
1177
1798
  TRANSACTION_TTL_MS,
1799
+ acceptCloudReturn,
1178
1800
  acceptLaunch,
1179
1801
  applySetCookies,
1180
1802
  buildClientMetadata,
1181
1803
  can,
1182
1804
  canonicalPodUrl,
1805
+ cloudBaseUrl,
1183
1806
  cloudConnectAvailable,
1807
+ connectionActsAs,
1184
1808
  connectionExpiresAt,
1185
1809
  cookieNames,
1186
1810
  createPodAppAuth,
@@ -1188,17 +1812,22 @@ assertServerRuntime();
1188
1812
  createSealedCookieStore,
1189
1813
  discoverPod,
1190
1814
  handleCallback,
1815
+ humanSlots,
1191
1816
  isBrowserRuntime,
1192
1817
  isConnectError,
1818
+ isPrefetchOrAsset,
1819
+ markerCookie,
1193
1820
  openTransaction,
1194
1821
  parseConnectErrorCode,
1195
1822
  parseTokenResponse,
1196
1823
  pkceChallenge,
1824
+ readMarker,
1197
1825
  refreshConnection,
1198
1826
  reviewUrlFor,
1199
1827
  revokeConnection,
1200
1828
  safeNextPath,
1201
1829
  seal,
1830
+ spaces,
1202
1831
  startCloudConnect,
1203
1832
  startConnect,
1204
1833
  unseal