@synap-core/sdk 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -7
- package/dist/{client-CVUT1Mcf.d.cts → client-C5sF20f1.d.cts} +1 -1
- package/dist/{client-CVUT1Mcf.d.ts → client-C5sF20f1.d.ts} +1 -1
- package/dist/connect.cjs +397 -116
- package/dist/connect.cjs.map +1 -1
- package/dist/connect.d.cts +220 -15
- package/dist/connect.d.ts +220 -15
- package/dist/connect.js +389 -116
- package/dist/connect.js.map +1 -1
- package/dist/public.d.cts +2 -2
- package/dist/public.d.ts +2 -2
- package/dist/testing.cjs +138 -11
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +92 -4
- package/dist/testing.d.ts +92 -4
- package/dist/testing.js +134 -11
- package/dist/testing.js.map +1 -1
- package/dist/{types-DpqPNaaa.d.cts → types-C2KGNXEn.d.cts} +20 -1
- package/dist/{types-DpqPNaaa.d.ts → types-C2KGNXEn.d.ts} +20 -1
- package/package.json +8 -8
- package/src/connect/auth.ts +161 -31
- package/src/connect/client.ts +107 -1
- package/src/connect/cloud.ts +206 -0
- package/src/connect/discovery.ts +15 -14
- package/src/connect/errors.ts +1 -1
- package/src/connect/flow.ts +41 -1
- package/src/connect/index.ts +39 -11
- package/src/connect/seal.ts +1 -1
- package/src/connect/store.ts +41 -3
- package/src/connect/types.ts +20 -0
- package/src/testing/auth-server.ts +144 -12
- package/src/testing/cloud.ts +84 -0
- package/src/testing/index.ts +3 -1
package/dist/connect.cjs
CHANGED
|
@@ -20,19 +20,23 @@ 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,
|
|
37
41
|
connectionExpiresAt: () => connectionExpiresAt,
|
|
38
42
|
cookieNames: () => cookieNames,
|
|
@@ -43,15 +47,19 @@ __export(connect_exports, {
|
|
|
43
47
|
handleCallback: () => handleCallback,
|
|
44
48
|
isBrowserRuntime: () => isBrowserRuntime,
|
|
45
49
|
isConnectError: () => isConnectError,
|
|
50
|
+
isPrefetchOrAsset: () => isPrefetchOrAsset,
|
|
51
|
+
markerCookie: () => markerCookie,
|
|
46
52
|
openTransaction: () => openTransaction,
|
|
47
53
|
parseConnectErrorCode: () => parseConnectErrorCode,
|
|
48
54
|
parseTokenResponse: () => parseTokenResponse,
|
|
49
55
|
pkceChallenge: () => pkceChallenge,
|
|
56
|
+
readMarker: () => readMarker,
|
|
50
57
|
refreshConnection: () => refreshConnection,
|
|
51
58
|
reviewUrlFor: () => reviewUrlFor,
|
|
52
59
|
revokeConnection: () => revokeConnection,
|
|
53
60
|
safeNextPath: () => safeNextPath,
|
|
54
61
|
seal: () => seal,
|
|
62
|
+
spaces: () => spaces,
|
|
55
63
|
startCloudConnect: () => startCloudConnect,
|
|
56
64
|
startConnect: () => startConnect,
|
|
57
65
|
unseal: () => unseal
|
|
@@ -260,12 +268,6 @@ async function discoverPod(input, opts = {}) {
|
|
|
260
268
|
}
|
|
261
269
|
}
|
|
262
270
|
}
|
|
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");
|
|
268
|
-
}
|
|
269
271
|
function safeNextPath(value, appOrigin) {
|
|
270
272
|
if (!value || value.length > 2048) return null;
|
|
271
273
|
if (value.startsWith("/") && !value.startsWith("//") && !value.startsWith("/\\")) {
|
|
@@ -295,10 +297,14 @@ function acceptLaunch(url, appOrigin, opts = {}) {
|
|
|
295
297
|
}
|
|
296
298
|
}
|
|
297
299
|
const hint = params.get("synap_space_hint");
|
|
300
|
+
const state = params.get("state");
|
|
298
301
|
return {
|
|
299
302
|
pod,
|
|
300
303
|
next: safeNextPath(params.get("target_link_uri"), appOrigin),
|
|
301
|
-
spaceHint: hint && /^[A-Za-z0-9_-]{1,64}$/.test(hint) ? hint : null
|
|
304
|
+
spaceHint: hint && /^[A-Za-z0-9_-]{1,64}$/.test(hint) ? hint : null,
|
|
305
|
+
// A browser launch never carries a state: any well-formed one is a Synap Cloud return
|
|
306
|
+
// (a pick: iss + state; a cancel: state + error=access_denied, no iss).
|
|
307
|
+
cloudState: state && /^[A-Za-z0-9._~-]{16,256}$/.test(state) ? state : null
|
|
302
308
|
};
|
|
303
309
|
}
|
|
304
310
|
|
|
@@ -368,10 +374,199 @@ function serializeCookie(name, value, attrs) {
|
|
|
368
374
|
return parts.join("; ");
|
|
369
375
|
}
|
|
370
376
|
|
|
377
|
+
// src/connect/seal.ts
|
|
378
|
+
var VERSION = "v1";
|
|
379
|
+
var IV_BYTES = 12;
|
|
380
|
+
var TAG_BYTES = 16;
|
|
381
|
+
var MIN_SECRET_LENGTH = 32;
|
|
382
|
+
var SALT = utf8("synap-connect/v1");
|
|
383
|
+
var keyCache = /* @__PURE__ */ new Map();
|
|
384
|
+
function secretsOf(secret) {
|
|
385
|
+
const list = typeof secret === "string" ? [secret] : secret;
|
|
386
|
+
if (list.length === 0 || list.some((s) => typeof s !== "string" || s.length < MIN_SECRET_LENGTH)) {
|
|
387
|
+
throw new ConnectError(
|
|
388
|
+
"misconfigured",
|
|
389
|
+
`The app session secret must be at least ${MIN_SECRET_LENGTH} characters (set SYNAP_APP_SESSION_SECRET on the server).`
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
return list;
|
|
393
|
+
}
|
|
394
|
+
function assertAppSecret(secret) {
|
|
395
|
+
secretsOf(secret);
|
|
396
|
+
}
|
|
397
|
+
function keyFor(secret, purpose) {
|
|
398
|
+
const cacheKey = `${purpose}\0${secret}`;
|
|
399
|
+
let key = keyCache.get(cacheKey);
|
|
400
|
+
if (!key) {
|
|
401
|
+
key = crypto.subtle.importKey("raw", utf8(secret), "HKDF", false, ["deriveKey"]).then(
|
|
402
|
+
(base) => crypto.subtle.deriveKey(
|
|
403
|
+
{ name: "HKDF", hash: "SHA-256", salt: SALT, info: utf8(purpose) },
|
|
404
|
+
base,
|
|
405
|
+
{ name: "AES-GCM", length: 256 },
|
|
406
|
+
false,
|
|
407
|
+
["encrypt", "decrypt"]
|
|
408
|
+
)
|
|
409
|
+
);
|
|
410
|
+
keyCache.set(cacheKey, key);
|
|
411
|
+
}
|
|
412
|
+
return key;
|
|
413
|
+
}
|
|
414
|
+
async function seal(value, opts) {
|
|
415
|
+
const [current] = secretsOf(opts.secret);
|
|
416
|
+
const iv = randomBytes(IV_BYTES);
|
|
417
|
+
const plaintext = utf8(JSON.stringify({ exp: opts.expiresAt, data: value }));
|
|
418
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
419
|
+
{ name: "AES-GCM", iv, additionalData: utf8(opts.purpose) },
|
|
420
|
+
await keyFor(current, opts.purpose),
|
|
421
|
+
plaintext
|
|
422
|
+
);
|
|
423
|
+
const out = new Uint8Array(IV_BYTES + ciphertext.byteLength);
|
|
424
|
+
out.set(iv, 0);
|
|
425
|
+
out.set(new Uint8Array(ciphertext), IV_BYTES);
|
|
426
|
+
return `${VERSION}.${base64url(out)}`;
|
|
427
|
+
}
|
|
428
|
+
async function unseal(sealed, opts) {
|
|
429
|
+
const secrets = secretsOf(opts.secret);
|
|
430
|
+
const dot = sealed.indexOf(".");
|
|
431
|
+
if (dot < 0 || sealed.slice(0, dot) !== VERSION) return { ok: false, reason: "invalid" };
|
|
432
|
+
const bytes = fromBase64url(sealed.slice(dot + 1));
|
|
433
|
+
if (!bytes || bytes.length < IV_BYTES + TAG_BYTES) return { ok: false, reason: "invalid" };
|
|
434
|
+
const iv = bytes.slice(0, IV_BYTES);
|
|
435
|
+
const ciphertext = bytes.slice(IV_BYTES);
|
|
436
|
+
for (const secret of secrets) {
|
|
437
|
+
let plaintext;
|
|
438
|
+
try {
|
|
439
|
+
plaintext = await crypto.subtle.decrypt(
|
|
440
|
+
{ name: "AES-GCM", iv, additionalData: utf8(opts.purpose) },
|
|
441
|
+
await keyFor(secret, opts.purpose),
|
|
442
|
+
ciphertext
|
|
443
|
+
);
|
|
444
|
+
} catch {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
let parsed;
|
|
448
|
+
try {
|
|
449
|
+
parsed = JSON.parse(fromUtf8(new Uint8Array(plaintext)));
|
|
450
|
+
} catch {
|
|
451
|
+
return { ok: false, reason: "invalid" };
|
|
452
|
+
}
|
|
453
|
+
if (typeof parsed.exp !== "number") return { ok: false, reason: "invalid" };
|
|
454
|
+
if (parsed.exp <= opts.now) return { ok: false, reason: "expired" };
|
|
455
|
+
return { ok: true, value: parsed.data, expiresAt: parsed.exp };
|
|
456
|
+
}
|
|
457
|
+
return { ok: false, reason: "invalid" };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/connect/cloud.ts
|
|
461
|
+
var CLOUD_TRANSACTION_TTL_MS = 10 * 60 * 1e3;
|
|
462
|
+
var STATE_RE = /^[A-Za-z0-9._~-]{16,256}$/;
|
|
463
|
+
var LOOPBACK_HOSTS2 = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
464
|
+
function cloudBaseUrl(cloudUrl, opts = {}) {
|
|
465
|
+
let url;
|
|
466
|
+
try {
|
|
467
|
+
url = new URL(cloudUrl.trim());
|
|
468
|
+
} catch {
|
|
469
|
+
throw new ConnectError("misconfigured", "cloudUrl is not a URL");
|
|
470
|
+
}
|
|
471
|
+
const loopback = LOOPBACK_HOSTS2.has(url.hostname);
|
|
472
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback && opts.allowInsecureLoopback)) {
|
|
473
|
+
throw new ConnectError("misconfigured", "cloudUrl must be https (http only on localhost in development)");
|
|
474
|
+
}
|
|
475
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
476
|
+
throw new ConnectError("misconfigured", "cloudUrl has no credentials, query or fragment");
|
|
477
|
+
}
|
|
478
|
+
return `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
|
|
479
|
+
}
|
|
480
|
+
function cloudConnectAvailable(config) {
|
|
481
|
+
if (!config?.cloudUrl) return false;
|
|
482
|
+
try {
|
|
483
|
+
cloudBaseUrl(config.cloudUrl, config);
|
|
484
|
+
return true;
|
|
485
|
+
} catch {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function startCloudConnect(opts) {
|
|
490
|
+
if (!cloudConnectAvailable(opts)) {
|
|
491
|
+
throw new ConnectError("cp_unavailable", "cp: not configured \u2014 pass cloudUrl (the Control Plane URL) to offer Synap Cloud");
|
|
492
|
+
}
|
|
493
|
+
if (!opts.secret) throw new ConnectError("misconfigured", "startCloudConnect needs the app secret to seal its state");
|
|
494
|
+
const base = cloudBaseUrl(opts.cloudUrl, opts);
|
|
495
|
+
const secret = opts.secret;
|
|
496
|
+
return (async () => {
|
|
497
|
+
const now = (opts.now ?? Date.now)();
|
|
498
|
+
const state = randomToken();
|
|
499
|
+
const url = new URL(`${base}/connect/pod`);
|
|
500
|
+
url.searchParams.set("client_id", opts.clientId);
|
|
501
|
+
url.searchParams.set("state", state);
|
|
502
|
+
const tx = {
|
|
503
|
+
v: 1,
|
|
504
|
+
state,
|
|
505
|
+
clientId: opts.clientId,
|
|
506
|
+
cloudUrl: base,
|
|
507
|
+
next: opts.next ?? null,
|
|
508
|
+
spaceHint: opts.spaceHint ?? null,
|
|
509
|
+
prompt: opts.prompt === "consent" ? "consent" : null,
|
|
510
|
+
createdAt: now
|
|
511
|
+
};
|
|
512
|
+
const expiresAt = now + CLOUD_TRANSACTION_TTL_MS;
|
|
513
|
+
const transaction = await seal(tx, { secret, purpose: "synap-connect/cloud", expiresAt });
|
|
514
|
+
return { url: url.href, transaction, expiresAt };
|
|
515
|
+
})();
|
|
516
|
+
}
|
|
517
|
+
async function acceptCloudReturn(opts) {
|
|
518
|
+
if (!opts.transaction) throw new ConnectError("expired_transaction", "No Synap Cloud sign-in in progress in this browser");
|
|
519
|
+
const opened = await unseal(opts.transaction, {
|
|
520
|
+
secret: opts.secret,
|
|
521
|
+
purpose: "synap-connect/cloud",
|
|
522
|
+
now: (opts.now ?? Date.now)()
|
|
523
|
+
});
|
|
524
|
+
if (!opened.ok) {
|
|
525
|
+
throw new ConnectError(
|
|
526
|
+
"expired_transaction",
|
|
527
|
+
opened.reason === "expired" ? "The Synap Cloud sign-in is older than 10 minutes" : "The Synap Cloud cookie is not one this app sealed"
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
const tx = opened.value;
|
|
531
|
+
if (!tx || tx.v !== 1 || typeof tx.state !== "string") {
|
|
532
|
+
throw new ConnectError("expired_transaction", "The Synap Cloud cookie is not one this app sealed");
|
|
533
|
+
}
|
|
534
|
+
if (tx.clientId !== opts.clientId) {
|
|
535
|
+
throw new ConnectError("misconfigured", "The Synap Cloud sign-in was started for another client_id");
|
|
536
|
+
}
|
|
537
|
+
const params = new URL(opts.url).searchParams;
|
|
538
|
+
const state = params.get("state");
|
|
539
|
+
if (state === null || !STATE_RE.test(state) || !constantTimeEqual(state, tx.state)) {
|
|
540
|
+
throw new ConnectError("mismatch", "The Synap Cloud answer does not belong to the sign-in this browser started");
|
|
541
|
+
}
|
|
542
|
+
const error = params.get("error");
|
|
543
|
+
if (error !== null) {
|
|
544
|
+
throw new ConnectError(fromOAuthError(error, "authorize"), `Synap Cloud answered ${error}`, { oauthError: error });
|
|
545
|
+
}
|
|
546
|
+
const iss = params.get("iss");
|
|
547
|
+
if (!iss) throw new ConnectError("mismatch", "The Synap Cloud answer names no pod");
|
|
548
|
+
return {
|
|
549
|
+
pod: canonicalPodUrl(iss, opts),
|
|
550
|
+
next: tx.next,
|
|
551
|
+
spaceHint: tx.spaceHint,
|
|
552
|
+
prompt: tx.prompt === "consent" ? "consent" : null
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
371
556
|
// src/connect/client.ts
|
|
372
557
|
function can(connection, permission, workspaceId) {
|
|
373
558
|
return connection.grant.some((g) => g.permission === permission && (workspaceId === void 0 || g.workspaceId === workspaceId));
|
|
374
559
|
}
|
|
560
|
+
function spaces(connection) {
|
|
561
|
+
const bySpace = /* @__PURE__ */ new Map();
|
|
562
|
+
for (const g of connection.grant) {
|
|
563
|
+
const space = bySpace.get(g.workspaceId) ?? { workspaceId: g.workspaceId, workspaceName: g.workspaceName, permissions: [] };
|
|
564
|
+
if (!space.workspaceName && g.workspaceName) space.workspaceName = g.workspaceName;
|
|
565
|
+
if (!space.permissions.includes(g.permission)) space.permissions.push(g.permission);
|
|
566
|
+
bySpace.set(g.workspaceId, space);
|
|
567
|
+
}
|
|
568
|
+
return [...bySpace.values()];
|
|
569
|
+
}
|
|
375
570
|
var SAFE_PATH = /^\/(?!\/)[A-Za-z0-9\-._~!$&'()*+,;=:@%/]*$/;
|
|
376
571
|
function hubUrl(podUrl, path, query) {
|
|
377
572
|
if (!SAFE_PATH.test(path) || /(^|\/)\.\.?(\/|$)/.test(path) || path.includes("\\")) {
|
|
@@ -457,11 +652,59 @@ function createPodClient(connection, opts = {}) {
|
|
|
457
652
|
}
|
|
458
653
|
return { ok: true, status: "applied", data: result.data };
|
|
459
654
|
}
|
|
655
|
+
async function listPendingProposals(opts2 = {}) {
|
|
656
|
+
if (!can(connection, "proposal.read", opts2.workspaceId)) {
|
|
657
|
+
return {
|
|
658
|
+
ok: false,
|
|
659
|
+
error: { code: "forbidden", status: null, detail: "The connection was not granted proposal.read in that space" }
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
const limit = Math.min(Math.max(Math.floor(opts2.limit ?? 20), 1), 200);
|
|
663
|
+
const offset = Math.max(Math.floor(opts2.offset ?? 0), 0);
|
|
664
|
+
const result = await request("GET", "/proposals", void 0, {
|
|
665
|
+
status: "pending",
|
|
666
|
+
view: "basic",
|
|
667
|
+
limit,
|
|
668
|
+
offset,
|
|
669
|
+
workspaceId: opts2.workspaceId
|
|
670
|
+
});
|
|
671
|
+
if (!result.ok) return result;
|
|
672
|
+
const obj = result.data && typeof result.data === "object" ? result.data : {};
|
|
673
|
+
if (!Array.isArray(obj.proposals) || typeof obj.total !== "number") {
|
|
674
|
+
return { ok: false, error: { code: "server", status: 200, detail: "The pod's /proposals answer has no proposals or total" } };
|
|
675
|
+
}
|
|
676
|
+
const str = (v) => typeof v === "string" && v ? v : null;
|
|
677
|
+
const proposals = [];
|
|
678
|
+
for (const row of obj.proposals) {
|
|
679
|
+
if (!row || typeof row !== "object") continue;
|
|
680
|
+
const r = row;
|
|
681
|
+
const id = str(r.id);
|
|
682
|
+
if (!id) continue;
|
|
683
|
+
proposals.push({
|
|
684
|
+
id,
|
|
685
|
+
proposalType: str(r.proposalType) ?? "",
|
|
686
|
+
targetType: str(r.targetType) ?? "",
|
|
687
|
+
targetId: str(r.targetId),
|
|
688
|
+
workspaceId: str(r.workspaceId),
|
|
689
|
+
createdAt: str(r.createdAt) ?? "",
|
|
690
|
+
agentUserId: str(r.agentUserId),
|
|
691
|
+
sessionId: str(r.sessionId),
|
|
692
|
+
...str(r.summary) ? { summary: str(r.summary) } : {},
|
|
693
|
+
reviewUrl: reviewUrlFor(connection.podUrl, null, id)
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
return {
|
|
697
|
+
ok: true,
|
|
698
|
+
data: { total: obj.total, proposals, hasMore: obj.hasMore === true || offset + proposals.length < obj.total }
|
|
699
|
+
};
|
|
700
|
+
}
|
|
460
701
|
return {
|
|
461
702
|
podUrl: connection.podUrl,
|
|
462
703
|
user: connection.user,
|
|
463
704
|
grant: connection.grant,
|
|
464
705
|
can: (permission, workspaceId) => can(connection, permission, workspaceId),
|
|
706
|
+
spaces: () => spaces(connection),
|
|
707
|
+
listPendingProposals,
|
|
465
708
|
get(path, query) {
|
|
466
709
|
return request("GET", path, void 0, query);
|
|
467
710
|
},
|
|
@@ -480,89 +723,6 @@ function createPodClient(connection, opts = {}) {
|
|
|
480
723
|
};
|
|
481
724
|
}
|
|
482
725
|
|
|
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
726
|
// src/connect/flow.ts
|
|
567
727
|
var TRANSACTION_TTL_MS = 10 * 60 * 1e3;
|
|
568
728
|
var DEFAULT_REFRESH_IDLE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
@@ -597,6 +757,7 @@ async function startConnect(opts) {
|
|
|
597
757
|
url.searchParams.set("code_challenge_method", "S256");
|
|
598
758
|
if (resource) url.searchParams.set("resource", resource);
|
|
599
759
|
if (opts.spaceHint) url.searchParams.set("synap_space_hint", opts.spaceHint);
|
|
760
|
+
if (opts.prompt === "consent") url.searchParams.set("prompt", "consent");
|
|
600
761
|
const tx = {
|
|
601
762
|
v: 1,
|
|
602
763
|
issuer: issuer.issuer,
|
|
@@ -719,6 +880,23 @@ function parseGrant(value) {
|
|
|
719
880
|
}
|
|
720
881
|
return out;
|
|
721
882
|
}
|
|
883
|
+
var APP_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
884
|
+
var LOOPBACK_HOSTS3 = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
885
|
+
function parseApp(value) {
|
|
886
|
+
if (!value || typeof value !== "object") return {};
|
|
887
|
+
const { id, manageUrl } = value;
|
|
888
|
+
const out = {};
|
|
889
|
+
if (typeof id === "string" && APP_ID_RE.test(id)) out.appId = id;
|
|
890
|
+
if (typeof manageUrl === "string" && manageUrl.length <= 2048) {
|
|
891
|
+
try {
|
|
892
|
+
const url = new URL(manageUrl);
|
|
893
|
+
const secure = url.protocol === "https:" || url.protocol === "http:" && LOOPBACK_HOSTS3.has(url.hostname);
|
|
894
|
+
if (secure && !url.username && !url.password) out.manageUrl = url.href;
|
|
895
|
+
} catch {
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return out;
|
|
899
|
+
}
|
|
722
900
|
function parseUser(value) {
|
|
723
901
|
if (!value || typeof value !== "object") return null;
|
|
724
902
|
const { id, name } = value;
|
|
@@ -753,6 +931,11 @@ function parseTokenResponse(body, ctx) {
|
|
|
753
931
|
user,
|
|
754
932
|
connectedAt: ctx.previous?.connectedAt ?? ctx.now
|
|
755
933
|
};
|
|
934
|
+
const app = parseApp(body.synap_app);
|
|
935
|
+
const appId = app.appId ?? ctx.previous?.appId;
|
|
936
|
+
const manageUrl = app.manageUrl ?? ctx.previous?.manageUrl;
|
|
937
|
+
if (appId !== void 0) connection.appId = appId;
|
|
938
|
+
if (manageUrl !== void 0) connection.manageUrl = manageUrl;
|
|
756
939
|
if (typeof refresh_token === "string" && refresh_token !== "") {
|
|
757
940
|
connection.refreshToken = refresh_token;
|
|
758
941
|
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;
|
|
@@ -830,7 +1013,25 @@ async function revokeConnection(connection, opts = {}) {
|
|
|
830
1013
|
|
|
831
1014
|
// src/connect/store.ts
|
|
832
1015
|
function cookieNames(secure) {
|
|
833
|
-
return secure ? { session: "__Host-synap_session", transaction: "__Host-synap_tx" } : { session: "synap_session", transaction: "synap_tx" };
|
|
1016
|
+
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" };
|
|
1017
|
+
}
|
|
1018
|
+
var MARKER_TTL_SEC = 400 * 24 * 60 * 60;
|
|
1019
|
+
function markerCookie(podUrl, opts) {
|
|
1020
|
+
return serializeCookie(opts.name ?? cookieNames(opts.secure).marker, base64url(utf8(podUrl)), {
|
|
1021
|
+
maxAgeSec: MARKER_TTL_SEC,
|
|
1022
|
+
secure: opts.secure
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
function readMarker(cookieHeader, opts) {
|
|
1026
|
+
const raw = parseCookies(cookieHeader).get(opts.name ?? cookieNames(opts.secure).marker);
|
|
1027
|
+
const bytes = raw ? fromBase64url(raw) : null;
|
|
1028
|
+
if (!bytes || bytes.length > 512) return null;
|
|
1029
|
+
const podUrl = fromUtf8(bytes);
|
|
1030
|
+
try {
|
|
1031
|
+
return canonicalPodUrl(podUrl, opts) === podUrl ? podUrl : null;
|
|
1032
|
+
} catch {
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
834
1035
|
}
|
|
835
1036
|
var CHUNK_SIZE = 3800;
|
|
836
1037
|
var MAX_CHUNKS = 4;
|
|
@@ -924,6 +1125,7 @@ var DEFAULT_PATHS = {
|
|
|
924
1125
|
connect: "/api/synap/connect",
|
|
925
1126
|
callback: "/api/synap/callback",
|
|
926
1127
|
disconnect: "/api/synap/disconnect",
|
|
1128
|
+
cloud: "/api/synap/cloud",
|
|
927
1129
|
connectPage: "/connect",
|
|
928
1130
|
resultPage: "/connect/result",
|
|
929
1131
|
afterConnect: "/"
|
|
@@ -1015,7 +1217,7 @@ async function readForm(request) {
|
|
|
1015
1217
|
}
|
|
1016
1218
|
function isPrefetchOrAsset(request) {
|
|
1017
1219
|
const h = request.headers;
|
|
1018
|
-
if (h.get("next-router-prefetch") || h.get("x-middleware-prefetch")) return true;
|
|
1220
|
+
if (h.get("next-router-prefetch") || h.get("next-router-segment-prefetch") || h.get("x-middleware-prefetch")) return true;
|
|
1019
1221
|
if (/prefetch/i.test(h.get("purpose") ?? "") || /prefetch/i.test(h.get("sec-purpose") ?? "")) return true;
|
|
1020
1222
|
const path = new URL(request.url).pathname;
|
|
1021
1223
|
return path.startsWith("/_next/") || /\.[A-Za-z0-9]{1,8}$/.test(path);
|
|
@@ -1026,6 +1228,7 @@ function createPodAppAuth(config) {
|
|
|
1026
1228
|
const { origin, secure } = appOriginOf(config.appUrl);
|
|
1027
1229
|
const paths = { ...DEFAULT_PATHS, ...config.paths };
|
|
1028
1230
|
const metadata = buildClientMetadata({ ...config, paths });
|
|
1231
|
+
const cloudUrl = config.cloudUrl?.trim() ? cloudBaseUrl(config.cloudUrl, config) : null;
|
|
1029
1232
|
const clientId = metadata.client_id;
|
|
1030
1233
|
const redirectUri = metadata.redirect_uris[0];
|
|
1031
1234
|
const permissions = config.requests.map((r) => r.permission);
|
|
@@ -1042,6 +1245,37 @@ function createPodAppAuth(config) {
|
|
|
1042
1245
|
return e;
|
|
1043
1246
|
};
|
|
1044
1247
|
const clearTx = serializeCookie(names.transaction, "", { maxAgeSec: 0, secure });
|
|
1248
|
+
const clearCloud = serializeCookie(names.cloud, "", { maxAgeSec: 0, secure });
|
|
1249
|
+
const clearMarker = serializeCookie(names.marker, "", { maxAgeSec: 0, secure });
|
|
1250
|
+
const marker = (podUrl) => markerCookie(podUrl, { secure, name: names.marker });
|
|
1251
|
+
const asRequest = (source) => source instanceof Request ? source : new Request(origin, { headers: source ? { cookie: source } : {} });
|
|
1252
|
+
const lapsedIn = (request) => {
|
|
1253
|
+
const podUrl = readMarker(request.headers.get("cookie"), { secure, name: names.marker, allowInsecureLoopback: config.allowInsecureLoopback });
|
|
1254
|
+
return podUrl ? { podUrl } : null;
|
|
1255
|
+
};
|
|
1256
|
+
async function toPod(opts, extra = []) {
|
|
1257
|
+
const started = await startConnect({
|
|
1258
|
+
...flow,
|
|
1259
|
+
pod: opts.pod,
|
|
1260
|
+
clientId,
|
|
1261
|
+
redirectUri,
|
|
1262
|
+
permissions,
|
|
1263
|
+
secret: config.secret,
|
|
1264
|
+
resource: config.resource,
|
|
1265
|
+
spaceHint: opts.space && /^[A-Za-z0-9_-]{1,64}$/.test(opts.space) ? opts.space : null,
|
|
1266
|
+
next: opts.next,
|
|
1267
|
+
prompt: opts.prompt,
|
|
1268
|
+
allowInsecureLoopback: config.allowInsecureLoopback
|
|
1269
|
+
});
|
|
1270
|
+
return redirect(started.authorizeUrl, [
|
|
1271
|
+
...extra,
|
|
1272
|
+
serializeCookie(names.transaction, started.transaction, { maxAgeSec: TRANSACTION_TTL_MS / 1e3, secure })
|
|
1273
|
+
]);
|
|
1274
|
+
}
|
|
1275
|
+
async function liveConnectionTo(request, pod) {
|
|
1276
|
+
const existing = await store.read(request);
|
|
1277
|
+
return Boolean(existing && existing.accessExpiresAt > now() && existing.podUrl === canonicalPodUrl(pod, config));
|
|
1278
|
+
}
|
|
1045
1279
|
async function clientMetadata() {
|
|
1046
1280
|
return new Response(JSON.stringify(metadata), {
|
|
1047
1281
|
status: 200,
|
|
@@ -1055,26 +1289,10 @@ function createPodAppAuth(config) {
|
|
|
1055
1289
|
const pod = formField(form, "pod")?.slice(0, 512) ?? "";
|
|
1056
1290
|
const next = safeNextPath(formField(form, "next"), origin) ?? paths.afterConnect;
|
|
1057
1291
|
const space = formField(form, "space");
|
|
1292
|
+
const prompt = formField(form, "prompt") === "consent" ? "consent" : null;
|
|
1058
1293
|
try {
|
|
1059
|
-
|
|
1060
|
-
|
|
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
|
-
]);
|
|
1294
|
+
if (!prompt && await liveConnectionTo(request, pod)) return redirect(new URL(next, origin).href);
|
|
1295
|
+
return await toPod({ pod, next, space, prompt });
|
|
1078
1296
|
} catch (err) {
|
|
1079
1297
|
const e = fail(err, "connect");
|
|
1080
1298
|
return redirect(withQuery(origin, paths.connectPage, { error: e.code, pod }));
|
|
@@ -1093,7 +1311,7 @@ function createPodAppAuth(config) {
|
|
|
1093
1311
|
secret: config.secret
|
|
1094
1312
|
});
|
|
1095
1313
|
const cookies = await store.write(connection, request);
|
|
1096
|
-
return redirect(new URL(next ?? paths.afterConnect, origin).href, [clearTx, ...cookies]);
|
|
1314
|
+
return redirect(new URL(next ?? paths.afterConnect, origin).href, [clearTx, ...cookies, marker(connection.podUrl)]);
|
|
1097
1315
|
} catch (err) {
|
|
1098
1316
|
const e = fail(err, "callback");
|
|
1099
1317
|
return redirect(withQuery(origin, paths.resultPage, { error: e.code }), [clearTx]);
|
|
@@ -1108,7 +1326,53 @@ function createPodAppAuth(config) {
|
|
|
1108
1326
|
const { revoked } = await revokeConnection(connection, flow);
|
|
1109
1327
|
if (!revoked) report(new ConnectError("pod_unreachable", "The pod did not confirm the revocation; the cookie is cleared anyway"), "disconnect");
|
|
1110
1328
|
}
|
|
1111
|
-
return redirect(new URL(next, origin).href, await store.clear(request));
|
|
1329
|
+
return redirect(new URL(next, origin).href, [...await store.clear(request), clearMarker]);
|
|
1330
|
+
}
|
|
1331
|
+
async function cloudConnect(request) {
|
|
1332
|
+
if (request.method !== "POST") return new Response("Method Not Allowed", { status: 405, headers: { Allow: "POST" } });
|
|
1333
|
+
if (!sameOrigin(request, origin)) return new Response("Forbidden", { status: 403 });
|
|
1334
|
+
const form = await readForm(request);
|
|
1335
|
+
const next = safeNextPath(formField(form, "next"), origin) ?? paths.afterConnect;
|
|
1336
|
+
const space = formField(form, "space");
|
|
1337
|
+
try {
|
|
1338
|
+
const started = await startCloudConnect({
|
|
1339
|
+
cloudUrl,
|
|
1340
|
+
allowInsecureLoopback: config.allowInsecureLoopback,
|
|
1341
|
+
clientId,
|
|
1342
|
+
secret: config.secret,
|
|
1343
|
+
next,
|
|
1344
|
+
spaceHint: space && /^[A-Za-z0-9_-]{1,64}$/.test(space) ? space : null,
|
|
1345
|
+
prompt: formField(form, "prompt") === "consent" ? "consent" : null,
|
|
1346
|
+
now
|
|
1347
|
+
});
|
|
1348
|
+
return redirect(started.url, [
|
|
1349
|
+
serializeCookie(names.cloud, started.transaction, { maxAgeSec: CLOUD_TRANSACTION_TTL_MS / 1e3, secure })
|
|
1350
|
+
]);
|
|
1351
|
+
} catch (err) {
|
|
1352
|
+
const e = fail(err, "connect");
|
|
1353
|
+
return redirect(withQuery(origin, paths.connectPage, { error: e.code }));
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
async function cloudReturn(request) {
|
|
1357
|
+
if (request.method !== "GET") return new Response("Method Not Allowed", { status: 405, headers: { Allow: "GET" } });
|
|
1358
|
+
let pod = null;
|
|
1359
|
+
try {
|
|
1360
|
+
const back = await acceptCloudReturn({
|
|
1361
|
+
url: request.url,
|
|
1362
|
+
transaction: parseCookies(request.headers.get("cookie")).get(names.cloud) ?? null,
|
|
1363
|
+
clientId,
|
|
1364
|
+
secret: config.secret,
|
|
1365
|
+
allowInsecureLoopback: config.allowInsecureLoopback,
|
|
1366
|
+
now
|
|
1367
|
+
});
|
|
1368
|
+
pod = back.pod;
|
|
1369
|
+
const next = back.next ?? paths.afterConnect;
|
|
1370
|
+
if (!back.prompt && await liveConnectionTo(request, back.pod)) return redirect(new URL(next, origin).href, [clearCloud]);
|
|
1371
|
+
return await toPod({ pod: back.pod, next, space: back.spaceHint, prompt: back.prompt }, [clearCloud]);
|
|
1372
|
+
} catch (err) {
|
|
1373
|
+
const e = fail(err, "connect");
|
|
1374
|
+
return redirect(withQuery(origin, paths.connectPage, { error: e.code, pod: pod ? new URL(pod).host : null }), [clearCloud]);
|
|
1375
|
+
}
|
|
1112
1376
|
}
|
|
1113
1377
|
async function refreshIfNeeded(request) {
|
|
1114
1378
|
const connection = await store.read(request);
|
|
@@ -1119,7 +1383,10 @@ function createPodAppAuth(config) {
|
|
|
1119
1383
|
cookieHeader: null,
|
|
1120
1384
|
...error ? { error } : {}
|
|
1121
1385
|
});
|
|
1122
|
-
if (!connection)
|
|
1386
|
+
if (!connection) {
|
|
1387
|
+
const lapsed = lapsedIn(request);
|
|
1388
|
+
return lapsed ? { ...unchanged("none", null), lapsed } : unchanged("none", null);
|
|
1389
|
+
}
|
|
1123
1390
|
if (isPrefetchOrAsset(request)) return unchanged("skipped", connection);
|
|
1124
1391
|
const t = now();
|
|
1125
1392
|
const accessLives = connection.accessExpiresAt > t;
|
|
@@ -1144,6 +1411,7 @@ function createPodAppAuth(config) {
|
|
|
1144
1411
|
const cleared = await store.clear(request).catch(() => []);
|
|
1145
1412
|
return { status: "expired", connection: null, setCookies: cleared, cookieHeader: null, error: e };
|
|
1146
1413
|
}
|
|
1414
|
+
setCookies = [...setCookies, marker(next.podUrl)];
|
|
1147
1415
|
return {
|
|
1148
1416
|
status: "refreshed",
|
|
1149
1417
|
connection: next,
|
|
@@ -1156,8 +1424,13 @@ function createPodAppAuth(config) {
|
|
|
1156
1424
|
redirectUri,
|
|
1157
1425
|
metadata,
|
|
1158
1426
|
paths,
|
|
1159
|
-
|
|
1160
|
-
|
|
1427
|
+
cloudConnectAvailable: cloudUrl !== null,
|
|
1428
|
+
handlers: { clientMetadata, connect, callback, disconnect, cloudConnect, cloudReturn },
|
|
1429
|
+
readConnection: (source) => store.read(asRequest(source)),
|
|
1430
|
+
async readLapsed(source) {
|
|
1431
|
+
const request = asRequest(source);
|
|
1432
|
+
return await store.read(request) ? null : lapsedIn(request);
|
|
1433
|
+
},
|
|
1161
1434
|
refreshIfNeeded,
|
|
1162
1435
|
client: (connection, opts) => createPodClient(connection, { fetch: config.fetch, now: config.now, ...opts })
|
|
1163
1436
|
};
|
|
@@ -1167,19 +1440,23 @@ function createPodAppAuth(config) {
|
|
|
1167
1440
|
assertServerRuntime();
|
|
1168
1441
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1169
1442
|
0 && (module.exports = {
|
|
1443
|
+
CLOUD_TRANSACTION_TTL_MS,
|
|
1170
1444
|
CONNECT_ERROR_CODES,
|
|
1171
1445
|
ConnectError,
|
|
1172
1446
|
DEFAULT_PATHS,
|
|
1173
1447
|
DEFAULT_REFRESH_IDLE_MS,
|
|
1448
|
+
MARKER_TTL_SEC,
|
|
1174
1449
|
MIN_SECRET_LENGTH,
|
|
1175
1450
|
REFRESH_WINDOW_MS,
|
|
1176
1451
|
ROTATION_MEMORY_MS,
|
|
1177
1452
|
TRANSACTION_TTL_MS,
|
|
1453
|
+
acceptCloudReturn,
|
|
1178
1454
|
acceptLaunch,
|
|
1179
1455
|
applySetCookies,
|
|
1180
1456
|
buildClientMetadata,
|
|
1181
1457
|
can,
|
|
1182
1458
|
canonicalPodUrl,
|
|
1459
|
+
cloudBaseUrl,
|
|
1183
1460
|
cloudConnectAvailable,
|
|
1184
1461
|
connectionExpiresAt,
|
|
1185
1462
|
cookieNames,
|
|
@@ -1190,15 +1467,19 @@ assertServerRuntime();
|
|
|
1190
1467
|
handleCallback,
|
|
1191
1468
|
isBrowserRuntime,
|
|
1192
1469
|
isConnectError,
|
|
1470
|
+
isPrefetchOrAsset,
|
|
1471
|
+
markerCookie,
|
|
1193
1472
|
openTransaction,
|
|
1194
1473
|
parseConnectErrorCode,
|
|
1195
1474
|
parseTokenResponse,
|
|
1196
1475
|
pkceChallenge,
|
|
1476
|
+
readMarker,
|
|
1197
1477
|
refreshConnection,
|
|
1198
1478
|
reviewUrlFor,
|
|
1199
1479
|
revokeConnection,
|
|
1200
1480
|
safeNextPath,
|
|
1201
1481
|
seal,
|
|
1482
|
+
spaces,
|
|
1202
1483
|
startCloudConnect,
|
|
1203
1484
|
startConnect,
|
|
1204
1485
|
unseal
|