@rynx-ai/daemon 0.1.11-beta.30 → 0.1.11-beta.32

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/plugin-channel-lark",
3
- "version": "0.1.11-beta.30",
3
+ "version": "0.1.11-beta.32",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rynx-ai/rynx.git",
@@ -0,0 +1,9 @@
1
+ export interface ChromeForTestingFetchInit {
2
+ headers?: HeadersInit;
3
+ redirect?: RequestRedirect;
4
+ signal?: AbortSignal | null;
5
+ }
6
+ /** HTTP behavior shared by Chrome for Testing metadata and artifact downloads. */
7
+ export declare function fetchChromeForTesting(url: string, init?: ChromeForTestingFetchInit): Promise<Response>;
8
+ /** Preserve nested network diagnostics instead of reporting only `fetch failed`. */
9
+ export declare function formatChromeForTestingNetworkError(error: unknown): string;
@@ -0,0 +1,58 @@
1
+ import { EnvHttpProxyAgent, fetch as undiciFetch } from "undici";
2
+ import { installedDaemonVersion } from "./entry-path.js";
3
+ const CONNECT_TIMEOUT_MS = 30_000;
4
+ const REQUEST_TIMEOUT_MS = 120_000;
5
+ let dispatcher;
6
+ /** HTTP behavior shared by Chrome for Testing metadata and artifact downloads. */
7
+ export async function fetchChromeForTesting(url, init = {}) {
8
+ const headers = new Headers(init.headers);
9
+ if (!headers.has("user-agent")) {
10
+ headers.set("user-agent", `rynx/${installedDaemonVersion()}`);
11
+ }
12
+ const timeoutSignal = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
13
+ const signal = init.signal
14
+ ? AbortSignal.any([init.signal, timeoutSignal])
15
+ : timeoutSignal;
16
+ const response = await undiciFetch(url, {
17
+ dispatcher: dispatcher ??= createDispatcher(),
18
+ headers: Object.fromEntries(headers.entries()),
19
+ redirect: init.redirect,
20
+ signal,
21
+ });
22
+ return response;
23
+ }
24
+ /** Preserve nested network diagnostics instead of reporting only `fetch failed`. */
25
+ export function formatChromeForTestingNetworkError(error) {
26
+ const messages = [];
27
+ const seen = new Set();
28
+ let current = error;
29
+ while (current instanceof Error && !seen.has(current) && messages.length < 8) {
30
+ seen.add(current);
31
+ if (current.message && messages.at(-1) !== current.message)
32
+ messages.push(current.message);
33
+ current = current.cause;
34
+ }
35
+ return messages.join(": ") || String(error);
36
+ }
37
+ function createDispatcher() {
38
+ const allProxy = firstEnvironmentValue("all_proxy", "ALL_PROXY");
39
+ const httpProxy = firstEnvironmentValue("http_proxy", "HTTP_PROXY") ?? allProxy;
40
+ const httpsProxy = firstEnvironmentValue("https_proxy", "HTTPS_PROXY") ?? httpProxy;
41
+ const noProxy = firstEnvironmentValue("no_proxy", "NO_PROXY");
42
+ return new EnvHttpProxyAgent({
43
+ connectTimeout: CONNECT_TIMEOUT_MS,
44
+ headersTimeout: REQUEST_TIMEOUT_MS,
45
+ bodyTimeout: REQUEST_TIMEOUT_MS,
46
+ ...(httpProxy ? { httpProxy } : {}),
47
+ ...(httpsProxy ? { httpsProxy } : {}),
48
+ ...(noProxy ? { noProxy } : {}),
49
+ });
50
+ }
51
+ function firstEnvironmentValue(...names) {
52
+ for (const name of names) {
53
+ const value = process.env[name];
54
+ if (value)
55
+ return value;
56
+ }
57
+ return undefined;
58
+ }
@@ -7,6 +7,7 @@
7
7
  * store computes and records the archive digest when it installs these bytes.
8
8
  */
9
9
  import { resolveChromeForTestingPlatform, } from "./chrome-for-testing-store.js";
10
+ import { fetchChromeForTesting, formatChromeForTestingNetworkError, } from "./chrome-for-testing-http.js";
10
11
  const MANIFEST_BASE_URL = "https://googlechromelabs.github.io/chrome-for-testing/";
11
12
  const CHANNEL_MANIFEST_URL = `${MANIFEST_BASE_URL}last-known-good-versions-with-downloads.json`;
12
13
  const ARTIFACT_BASE_URL = "https://storage.googleapis.com/chrome-for-testing-public/";
@@ -53,11 +54,17 @@ function validateSelector(selector) {
53
54
  }
54
55
  async function fetchManifest(manifestUrl, fetchMetadata, signal) {
55
56
  assertManifestUrl(manifestUrl);
56
- const response = await fetchMetadata(manifestUrl, {
57
- signal,
58
- redirect: "error",
59
- headers: { accept: "application/json" },
60
- });
57
+ let response;
58
+ try {
59
+ response = await fetchMetadata(manifestUrl, {
60
+ signal,
61
+ redirect: "error",
62
+ headers: { accept: "application/json" },
63
+ });
64
+ }
65
+ catch (error) {
66
+ throw new Error(`Chrome for Testing metadata request failed: ${formatChromeForTestingNetworkError(error)}`, { cause: error });
67
+ }
61
68
  if (!response.ok) {
62
69
  throw new Error(`Chrome for Testing metadata request failed: HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`);
63
70
  }
@@ -210,5 +217,5 @@ function isRecord(value) {
210
217
  return typeof value === "object" && value !== null && !Array.isArray(value);
211
218
  }
212
219
  async function defaultFetchMetadata(url, init) {
213
- return fetch(url, init);
220
+ return fetchChromeForTesting(url, init);
214
221
  }
@@ -48,6 +48,8 @@ export interface ChromeForTestingArtifactStoreOptions {
48
48
  arch?: string;
49
49
  fetchArtifact?: ChromeForTestingArtifactFetcher;
50
50
  extractZip?: ChromeForTestingZipExtractor;
51
+ /** Injectable only so download retries can be tested without real delays. */
52
+ retryDelay?: (attempt: number, signal?: AbortSignal) => Promise<void>;
51
53
  maxArchiveBytes?: number;
52
54
  now?: () => Date;
53
55
  /** Defaults to ~/.rynx/browser/profiles/headless-chromium. */
@@ -6,12 +6,14 @@
6
6
  * explicitly pinned build or an already-resolved official release.
7
7
  */
8
8
  import { createHash } from "node:crypto";
9
- import { accessSync, chmodSync, closeSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync, writeSync, } from "node:fs";
9
+ import { accessSync, chmodSync, closeSync, existsSync, ftruncateSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync, writeSync, } from "node:fs";
10
10
  import { constants as fsConstants } from "node:fs";
11
11
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
12
12
  import { DatabaseSync } from "node:sqlite";
13
+ import { setTimeout as delay } from "node:timers/promises";
13
14
  import extractZipArchive from "@electron-internal/extract-zip";
14
15
  import { rynxHome } from "@rynx-ai/core";
16
+ import { fetchChromeForTesting, formatChromeForTestingNetworkError, } from "./chrome-for-testing-http.js";
15
17
  import { hasSqlitePrimaryCode, SQLITE_BUSY, SQLITE_LOCKED } from "./sqlite.js";
16
18
  const BUILD_OWNER_FILE = ".rynx-cft-owner.json";
17
19
  const STAGING_OWNER_FILE = ".rynx-cft-staging-owner.json";
@@ -20,8 +22,9 @@ const STORE_LOCK_FILE = ".rynx-cft-store.sqlite";
20
22
  const MAX_METADATA_BYTES = 8 * 1024;
21
23
  const DEFAULT_MAX_ARCHIVE_BYTES = 1_024 * 1_024 * 1_024;
22
24
  const DEFAULT_STAGING_MAX_AGE_MS = 24 * 60 * 60 * 1_000;
23
- const DOWNLOAD_PROGRESS_PERCENT_STEP = 10;
25
+ const DOWNLOAD_PROGRESS_PERCENT_STEP = 5;
24
26
  const DOWNLOAD_PROGRESS_BYTES_STEP = 16 * 1_024 * 1_024;
27
+ const DOWNLOAD_MAX_ATTEMPTS = 3;
25
28
  const STAGING_NAME_PATTERN = /^install-([A-Za-z0-9]{6})$/;
26
29
  const HEADLESS_PROFILE_NAME_PATTERN = /^session-([a-f0-9]{64})$/;
27
30
  const VERSION_PATTERN = /^\d{1,6}\.\d{1,6}\.\d{1,6}\.\d{1,6}$/;
@@ -68,6 +71,7 @@ export function createChromeForTestingArtifactStore(options = {}) {
68
71
  const rootDir = resolve(options.rootDir ?? join(rynxHome(), "cache", "browser", "chrome-for-testing"));
69
72
  const fetchArtifact = options.fetchArtifact ?? defaultFetchArtifact;
70
73
  const extractZip = options.extractZip ?? defaultExtractZip;
74
+ const retryDelay = options.retryDelay ?? defaultDownloadRetryDelay;
71
75
  const maxArchiveBytes = options.maxArchiveBytes ?? DEFAULT_MAX_ARCHIVE_BYTES;
72
76
  const now = options.now ?? (() => new Date());
73
77
  const headlessProfileRoot = resolve(options.headlessProfileRoot ?? join(rynxHome(), "browser", "profiles", "headless-chromium"));
@@ -139,6 +143,7 @@ export function createChromeForTestingArtifactStore(options = {}) {
139
143
  maxArchiveBytes,
140
144
  signal: installOptions.signal,
141
145
  onProgress: installOptions.onProgress,
146
+ retryDelay,
142
147
  });
143
148
  throwIfAborted(installOptions.signal);
144
149
  const build = { ...release, sha256 };
@@ -450,9 +455,54 @@ function validateDownloadMetadata(input, platform) {
450
455
  return { version: input.version, platform: input.platform, url: url.href };
451
456
  }
452
457
  async function downloadArchive(options) {
453
- const response = await options.fetchArtifact(options.url, { signal: options.signal });
458
+ const descriptor = openSync(options.destination, "wx", 0o600);
459
+ try {
460
+ for (let attempt = 1; attempt <= DOWNLOAD_MAX_ATTEMPTS; attempt += 1) {
461
+ try {
462
+ if (attempt > 1)
463
+ ftruncateSync(descriptor, 0);
464
+ return await downloadArchiveAttempt(options, descriptor);
465
+ }
466
+ catch (error) {
467
+ throwIfAborted(options.signal);
468
+ if (!(error instanceof RetryableDownloadError) || attempt === DOWNLOAD_MAX_ATTEMPTS) {
469
+ throw error;
470
+ }
471
+ options.onProgress?.(`Chrome for Testing 下载失败,正在重试 (${attempt + 1}/${DOWNLOAD_MAX_ATTEMPTS})`);
472
+ try {
473
+ await options.retryDelay(attempt, options.signal);
474
+ }
475
+ catch (delayError) {
476
+ throwIfAborted(options.signal);
477
+ throw delayError;
478
+ }
479
+ }
480
+ }
481
+ }
482
+ finally {
483
+ closeSync(descriptor);
484
+ }
485
+ throw new Error("Chrome for Testing download failed");
486
+ }
487
+ async function downloadArchiveAttempt(options, descriptor) {
488
+ let response;
489
+ try {
490
+ response = await options.fetchArtifact(options.url, { signal: options.signal });
491
+ }
492
+ catch (error) {
493
+ throw new RetryableDownloadError(`Chrome for Testing download failed: ${formatChromeForTestingNetworkError(error)}`, { cause: error });
494
+ }
454
495
  if (!response.ok) {
455
- throw new Error(`Chrome for Testing download failed: HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`);
496
+ try {
497
+ await response.body?.cancel();
498
+ }
499
+ catch {
500
+ // The status code remains the useful failure when response cleanup fails.
501
+ }
502
+ const error = new Error(`Chrome for Testing download failed: HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}`);
503
+ if (response.status >= 500)
504
+ throw new RetryableDownloadError(error.message, { cause: error });
505
+ throw error;
456
506
  }
457
507
  if (!response.body)
458
508
  throw new Error("Chrome for Testing download returned no response body");
@@ -461,15 +511,20 @@ async function downloadArchive(options) {
461
511
  throw new Error(`Chrome for Testing archive exceeds ${options.maxArchiveBytes} bytes`);
462
512
  }
463
513
  const hash = createHash("sha256");
464
- const descriptor = openSync(options.destination, "wx", 0o600);
465
514
  let bytes = 0;
466
- let nextPercent = DOWNLOAD_PROGRESS_PERCENT_STEP;
515
+ let lastReportedPercent = 0;
467
516
  let nextBytes = DOWNLOAD_PROGRESS_BYTES_STEP;
468
517
  const reader = response.body.getReader();
469
518
  try {
470
519
  while (true) {
471
520
  throwIfAborted(options.signal);
472
- const next = await reader.read();
521
+ let next;
522
+ try {
523
+ next = await reader.read();
524
+ }
525
+ catch (error) {
526
+ throw new RetryableDownloadError(`Chrome for Testing download failed: ${formatChromeForTestingNetworkError(error)}`, { cause: error });
527
+ }
473
528
  if (next.done)
474
529
  break;
475
530
  const chunk = Buffer.from(next.value);
@@ -478,24 +533,24 @@ async function downloadArchive(options) {
478
533
  throw new Error(`Chrome for Testing archive exceeds ${options.maxArchiveBytes} bytes`);
479
534
  }
480
535
  hash.update(chunk);
481
- writeAll(descriptor, chunk);
536
+ writeAll(descriptor, chunk, bytes - chunk.byteLength);
482
537
  if (length !== undefined && length > 0) {
483
538
  const completed = Math.min(100, Math.floor(bytes * 100 / length));
484
- while (nextPercent <= completed) {
485
- options.onProgress?.(`Chrome for Testing 下载进度:${nextPercent}%`);
486
- nextPercent += DOWNLOAD_PROGRESS_PERCENT_STEP;
539
+ if (completed >= lastReportedPercent + DOWNLOAD_PROGRESS_PERCENT_STEP) {
540
+ lastReportedPercent = completed;
541
+ options.onProgress?.(`Chrome for Testing 下载进度:${formatDownloadMegabytes(bytes)} / ` +
542
+ `${formatDownloadMegabytes(length)} (${completed}%)`);
487
543
  }
488
544
  }
489
545
  else {
490
546
  while (nextBytes <= bytes) {
491
- options.onProgress?.(`Chrome for Testing 已下载:${formatDownloadBytes(nextBytes)}`);
547
+ options.onProgress?.(`Chrome for Testing 已下载:${formatDownloadMegabytes(nextBytes)}`);
492
548
  nextBytes += DOWNLOAD_PROGRESS_BYTES_STEP;
493
549
  }
494
550
  }
495
551
  }
496
552
  }
497
553
  finally {
498
- closeSync(descriptor);
499
554
  reader.releaseLock();
500
555
  }
501
556
  const actual = hash.digest("hex");
@@ -504,13 +559,18 @@ async function downloadArchive(options) {
504
559
  }
505
560
  return actual;
506
561
  }
507
- function formatDownloadBytes(bytes) {
508
- return `${Math.floor(bytes / (1_024 * 1_024))} MiB`;
562
+ class RetryableDownloadError extends Error {
563
+ }
564
+ async function defaultDownloadRetryDelay(attempt, signal) {
565
+ await delay(2 ** attempt * 1_000, undefined, { signal });
566
+ }
567
+ function formatDownloadMegabytes(bytes) {
568
+ return `${(bytes / (1_024 * 1_024)).toFixed(1)} MB`;
509
569
  }
510
- function writeAll(descriptor, value) {
570
+ function writeAll(descriptor, value, position) {
511
571
  let offset = 0;
512
572
  while (offset < value.byteLength) {
513
- const written = writeSync(descriptor, value, offset, value.byteLength - offset);
573
+ const written = writeSync(descriptor, value, offset, value.byteLength - offset, position + offset);
514
574
  if (written <= 0)
515
575
  throw new Error("Chrome for Testing archive write made no progress");
516
576
  offset += written;
@@ -841,7 +901,7 @@ function throwIfAborted(signal) {
841
901
  throw new Error("Chrome for Testing installation aborted");
842
902
  }
843
903
  async function defaultFetchArtifact(url, init) {
844
- return fetch(url, { signal: init.signal, redirect: "follow" });
904
+ return fetchChromeForTesting(url, { signal: init.signal, redirect: "follow" });
845
905
  }
846
906
  async function defaultExtractZip(archivePath, destination) {
847
907
  await extractZipArchive(archivePath, { dir: destination });
@@ -17,8 +17,8 @@ import { REMOTE_RUNTIME_CORE_PROTOCOL } from "@rynx-ai/protocol/remote-runtime";
17
17
  import { RUNTIME_BROWSER_INSPECT_MAX_MESSAGE_BYTES, RUNTIME_BROWSER_INSPECT_SEMANTIC_CAPABILITY, } from "@rynx-ai/protocol/runtime-browser-inspect";
18
18
  import { RUNTIME_BROWSER_SURFACE_SEMANTIC_CAPABILITY } from "@rynx-ai/protocol/runtime-browser-surface";
19
19
  import { RUNTIME_EMULATOR_SURFACE_SEMANTIC_CAPABILITY } from "@rynx-ai/protocol/runtime-emulator-surface";
20
- import { createDaemonRuntimeHost, createDesktopFirstSessionBrowserHost, createRunnerSessionTerminalHost, createSessionRuntimeServices, DesktopBrowserHostAbsentError, DesktopBrowserHostRegistry, DirectRuntimeConnectionHub, DirectRuntimeBrowserInspectHostError, MachineSessionService, MachineSessionServiceFailure, RemoteRuntimeDispatcher, SessionBrowserService, SessionBrowserServiceError, SessionBrowserSurfaceCoordinator, SessionEmulatorService, startServer, startDirectRuntimeServer, } from "@rynx-ai/server";
21
- import { authorizeSessionPortalGrant, redeemSessionPortalTicket, } from "./session-portal-grant-store.js";
20
+ import { createDaemonRuntimeHost, createDesktopFirstSessionBrowserHost, createRunnerSessionTerminalHost, createSessionRuntimeServices, DesktopBrowserHostAbsentError, DesktopBrowserHostRegistry, DirectRuntimeConnectionHub, DirectRuntimeBrowserInspectHostError, MachineSessionService, MachineSessionServiceFailure, RemoteRuntimeDispatcher, SessionPendingInputIndex, SessionBrowserService, SessionBrowserServiceError, SessionBrowserSurfaceCoordinator, SessionEmulatorService, startServer, startDirectRuntimeServer, } from "@rynx-ai/server";
21
+ import { authorizeSessionPortalGrant, inspectSessionPortalGrant, onSessionPortalGrantRevoked, redeemSessionPortalTicket, revokeEmbeddedWritableSessionPortalGrants, } from "./session-portal-grant-store.js";
22
22
  import { APP_BROWSER_HOST_BOOTSTRAP_TYPE, AppBrowserHostSupervisor, parseAppBrowserHostLaunchConfig, } from "./app-browser-host-supervisor.js";
23
23
  import { buildControlDeps, listControlAgentModels } from "./control-deps.js";
24
24
  import { createBrowserArtifactManagementService } from "./browser-artifact-management.js";
@@ -311,7 +311,9 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
311
311
  "semantic.session.interaction.v1",
312
312
  "semantic.project.manage.v1",
313
313
  "semantic.provider-cli.status.v1",
314
- ...(runtimeBrowserSupported ? ["semantic.browser.v1"] : []),
314
+ ...(runtimeBrowserSupported
315
+ ? ["semantic.browser.v1", "semantic.browser.request-headers.v1"]
316
+ : []),
315
317
  ...(runtimeBrowserSupported && directHostingSupported
316
318
  ? [
317
319
  RUNTIME_BROWSER_SURFACE_SEMANTIC_CAPABILITY,
@@ -387,6 +389,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
387
389
  };
388
390
  };
389
391
  let machineSessions;
392
+ let sessionBrowsers;
390
393
  const pluginRuntime = {
391
394
  bindHostServices: (services) => pluginSupervisor.bindHostServices({
392
395
  ...services,
@@ -396,6 +399,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
396
399
  publicKey: installationIdentity.publicKey,
397
400
  sign: installationIdentity.sign,
398
401
  },
402
+ sessionBrowserRequestHeaders: sessionBrowsers,
399
403
  sessionLaunch: {
400
404
  resolve: async (selection) => {
401
405
  const workspace = await resolveSessionWorkspace(runtimeProjects, selection.projectId, selection.managedCwd, selection.workspace);
@@ -434,6 +438,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
434
438
  const directConnections = new DirectRuntimeConnectionHub();
435
439
  const sessionLog = new SqliteSessionLogStore();
436
440
  const sessionResources = new SqliteSessionResourceStore();
441
+ const sessionPendingInputs = new SessionPendingInputIndex();
437
442
  const sessionForks = new SqliteSessionForkStore();
438
443
  const runtimeProjects = new SqliteRuntimeProjectStore();
439
444
  const providerClis = new ProviderCliService();
@@ -446,6 +451,11 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
446
451
  const sessionRuntimeServices = createSessionRuntimeServices({
447
452
  config: appConfig,
448
453
  sessionLog,
454
+ pendingInputs: sessionPendingInputs,
455
+ onPendingInputPersisted: (input) => sessionResources.markMessageMirrored(input.sessionId, input.clientMessageId, {
456
+ responseId: input.responseId,
457
+ messageItemId: input.messageItemId,
458
+ }),
449
459
  sessionContextProvider: browserRunnerSessionContexts,
450
460
  admissionOpen,
451
461
  admissionReserve: reserveAdmission,
@@ -455,7 +465,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
455
465
  const sessionBrowserHost = appBrowserHostSupervisor
456
466
  ? createSupervisedAppSessionBrowserHost(durableDesktopBrowserHost, appBrowserHostSupervisor)
457
467
  : createDesktopFirstSessionBrowserHost(durableDesktopBrowserHost, createHeadlessBrowserHost());
458
- const sessionBrowsers = new SessionBrowserService({
468
+ sessionBrowsers = new SessionBrowserService({
459
469
  host: sessionBrowserHost,
460
470
  sessions: {
461
471
  exists: async (sessionId) => Boolean(sessionRegistry.get(sessionId) ??
@@ -556,6 +566,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
556
566
  list: () => control.listAgents(),
557
567
  },
558
568
  pendingMessages: sessionPendingMessageStore,
569
+ pendingInputs: sessionPendingInputs,
559
570
  resources: sessionResources,
560
571
  forks: sessionForks,
561
572
  execution: {
@@ -769,6 +780,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
769
780
  status: async () => asDaemonChromeInspectionStatus(await initializedChromeInspectionManager.status()),
770
781
  configure: async (input) => asDaemonChromeInspectionStatus(await initializedChromeInspectionManager.configure(input)),
771
782
  };
783
+ revokeEmbeddedWritableSessionPortalGrants();
772
784
  server = await startServer({
773
785
  config: appConfig,
774
786
  control,
@@ -801,8 +813,10 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
801
813
  daemonInstanceId,
802
814
  },
803
815
  sessionPortalAuthorization: {
804
- redeem: redeemSessionPortalTicket,
816
+ redeem: (ticket, embedOrigin) => redeemSessionPortalTicket(ticket, new Date(), embedOrigin),
805
817
  authorize: authorizeSessionPortalGrant,
818
+ inspect: inspectSessionPortalGrant,
819
+ subscribeRevocations: onSessionPortalGrantRevoked,
806
820
  ownsSession: sessionBelongsToPlugin,
807
821
  },
808
822
  });
package/dist/db.js CHANGED
@@ -282,6 +282,17 @@ function migrate(conn) {
282
282
  );
283
283
  CREATE INDEX IF NOT EXISTS idx_session_portal_grants_expires
284
284
  ON session_portal_grants(expires_at);
285
+ CREATE TABLE IF NOT EXISTS session_timeline_projections (
286
+ plugin_id TEXT NOT NULL,
287
+ session_id TEXT NOT NULL,
288
+ projection_id TEXT NOT NULL,
289
+ request_hash TEXT NOT NULL,
290
+ response_id TEXT NOT NULL,
291
+ item_id TEXT NOT NULL UNIQUE,
292
+ state TEXT NOT NULL DEFAULT 'pending' CHECK(state IN ('pending', 'completed')),
293
+ created_at TEXT NOT NULL,
294
+ PRIMARY KEY(plugin_id, session_id, projection_id)
295
+ );
285
296
  CREATE TABLE IF NOT EXISTS runtime_pairing_claims (
286
297
  claim_id TEXT PRIMARY KEY,
287
298
  secret_hash TEXT NOT NULL,
@@ -359,6 +370,17 @@ function migrate(conn) {
359
370
  addColumnIfMissing(conn, "session_message_operations", "response_id", "TEXT");
360
371
  addColumnIfMissing(conn, "session_message_operations", "message_item_id", "TEXT");
361
372
  addColumnIfMissing(conn, "session_message_operations", "execution_snapshot", "TEXT");
373
+ addColumnIfMissing(conn, "session_portal_tickets", "grant_handle", "TEXT");
374
+ addColumnIfMissing(conn, "session_portal_tickets", "embed_origin", "TEXT");
375
+ addColumnIfMissing(conn, "session_portal_grants", "grant_handle", "TEXT");
376
+ addColumnIfMissing(conn, "session_portal_grants", "embed_origin", "TEXT");
377
+ addColumnIfMissing(conn, "session_timeline_projections", "state", "TEXT NOT NULL DEFAULT 'pending'");
378
+ conn.exec(`
379
+ CREATE INDEX IF NOT EXISTS idx_session_portal_tickets_grant_handle
380
+ ON session_portal_tickets(plugin_id, grant_handle);
381
+ CREATE INDEX IF NOT EXISTS idx_session_portal_grants_grant_handle
382
+ ON session_portal_grants(plugin_id, grant_handle);
383
+ `);
362
384
  }
363
385
  /** Rebuild the original image-only table without losing committed resources. */
364
386
  function migrateSessionResourceFiles(conn) {