@stackbone/cli 0.9.4 → 0.9.5

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/main.js CHANGED
@@ -919,17 +919,17 @@ function approvalMetadataOf(input) {
919
919
  }
920
920
  const keyOrder = payloadKeyOrderOf(input.payload);
921
921
  if (keyOrder) labels[PAYLOAD_KEY_ORDER_KEY] = keyOrder;
922
- const metadata = isRecord$6(input.metadata) ? input.metadata : void 0;
922
+ const metadata = isRecord$7(input.metadata) ? input.metadata : void 0;
923
923
  if (!metadata && Object.keys(labels).length === 0) return void 0;
924
924
  return {
925
925
  ...metadata,
926
926
  ...labels
927
927
  };
928
928
  }
929
- var PAYLOAD_KEY_ORDER_KEY, isRecord$6;
929
+ var PAYLOAD_KEY_ORDER_KEY, isRecord$7;
930
930
  var init_approval_key_order = __esmMin((() => {
931
931
  PAYLOAD_KEY_ORDER_KEY = "payloadKeyOrder";
932
- isRecord$6 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
932
+ isRecord$7 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
933
933
  }));
934
934
  //#endregion
935
935
  //#region ../../libs/shared/validators/src/lib/agents/contract.ts
@@ -1850,6 +1850,10 @@ function makeBrokerError(code, message, retryable) {
1850
1850
  function isBrokerError(err) {
1851
1851
  return typeof err === "object" && err !== null && "code" in err && typeof err.code === "string" && BROKER_ERROR_CODES.safeParse(err.code).success;
1852
1852
  }
1853
+ /** Type guard: an `account_required` refusal that carries its accounts. */
1854
+ function isAccountRequiredError(err) {
1855
+ return isBrokerError(err) && err.code === "account_required" && Array.isArray(err.accounts);
1856
+ }
1853
1857
  /**
1854
1858
  * Type guard: is this a broker "needs authorization" error (`no_token` or
1855
1859
  * `user_authorization_required`)? These are the codes the adapter maps onto
@@ -1894,7 +1898,8 @@ var init_errors$1 = __esmMin((() => {
1894
1898
  "access_denied",
1895
1899
  "invalid_grant",
1896
1900
  "precondition_failed",
1897
- "cardinality_mismatch"
1901
+ "cardinality_mismatch",
1902
+ "account_required"
1898
1903
  ];
1899
1904
  BROKER_ERROR_CODES = z.enum([...CONNECTOR_EXECUTION_ERROR_CODES, ...BROKER_SPECIFIC_ERROR_CODES]);
1900
1905
  z.object({
@@ -1940,6 +1945,11 @@ var init_errors$1 = __esmMin((() => {
1940
1945
  precondition_failed: false,
1941
1946
  /** A narrower target is needed; repeating the same write exceeds the same limit. */
1942
1947
  cardinality_mismatch: false,
1948
+ /**
1949
+ * Several accounts and no account named. The same call meets the same accounts;
1950
+ * it has to name one of the ids {@link AccountRequiredError} lists.
1951
+ */
1952
+ account_required: false,
1943
1953
  /** Transport, or the provider answering 5xx. */
1944
1954
  execute_failed: true,
1945
1955
  /** The deadline passed. The next attempt may fit inside it. */
@@ -2257,6 +2267,11 @@ var init_requests = __esmMin((() => {
2257
2267
  connector: z.string(),
2258
2268
  /** WHO the token is for — the cache/grant-key dimension. */
2259
2269
  principal: principalSchema,
2270
+ /**
2271
+ * Which connected account, by its `connection_grant` id (STA3-123). Picks the
2272
+ * grant on a connector that holds several; `account_required` lists the ids.
2273
+ */
2274
+ account: z.string().min(1).optional(),
2260
2275
  /** Scoping claim: which install. HMAC-authenticated but separately asserted. */
2261
2276
  installationId: z.string().optional(),
2262
2277
  /** Scoping claim: which agent (folder) name. */
@@ -2309,7 +2324,9 @@ challenge: authorizationChallengeSchema });
2309
2324
  /** Connector id whose cached token to evict. */
2310
2325
  connector: z.string(),
2311
2326
  /** WHO to evict the cached token for. */
2312
- principal: principalSchema
2327
+ principal: principalSchema,
2328
+ /** Which connected account, by its `connection_grant` id, as on the token request. */
2329
+ account: z.string().min(1).optional()
2313
2330
  });
2314
2331
  }));
2315
2332
  //#endregion
@@ -2721,7 +2738,15 @@ type: fieldsAuthMethodTypeSchema }), catalogAuthMethodBaseSchema.extend({
2721
2738
  * "not classified" rather than "retryable" — the SDK leaves such a failure
2722
2739
  * retryable, which is the safe direction for a transient outage.
2723
2740
  */
2724
- retryable: z.boolean().optional()
2741
+ retryable: z.boolean().optional(),
2742
+ /**
2743
+ * On `account_required` (STA3-123): each account the connector holds, by the
2744
+ * id a call names to pick it. `email` is null for a legacy account.
2745
+ */
2746
+ details: z.object({ accounts: z.array(z.object({
2747
+ id: z.string(),
2748
+ email: z.string().nullable()
2749
+ })) }).optional()
2725
2750
  })
2726
2751
  });
2727
2752
  connectEvictResponseSchema = z.object({
@@ -5569,6 +5594,10 @@ var init_studio_triggers = __esmMin((() => {
5569
5594
  trigger: z.string().min(1),
5570
5595
  /** Which broker grant to poll with. `app` is the integration's own credential. */
5571
5596
  principalKey: z.string().min(1).default("app"),
5597
+ /** The connected account to poll with, by its grant id (STA3-123). Beside the
5598
+ * default `app` it picks that account; on a connector with several, the link
5599
+ * names one or is refused `account_required`. */
5600
+ account: z.string().min(1).optional(),
5572
5601
  ...targetRequestShape,
5573
5602
  mapping: z.string().nullish(),
5574
5603
  /** The visual editor's authoring graph, persisted opaquely — no interpretation. */
@@ -5596,6 +5625,10 @@ var init_studio_triggers = __esmMin((() => {
5596
5625
  });
5597
5626
  });
5598
5627
  patchTriggerSubscriptionRequestSchema = z.object({
5628
+ /** The connected account to poll with, by its grant id (STA3-123). Beside the
5629
+ * default `app` it picks that account; on a connector with several, the link
5630
+ * names one or is refused `account_required`. */
5631
+ account: z.string().min(1).optional(),
5599
5632
  enabled: z.boolean().optional(),
5600
5633
  mapping: z.string().nullish(),
5601
5634
  mappingGraph: z.unknown().optional(),
@@ -5625,7 +5658,7 @@ var init_studio_triggers = __esmMin((() => {
5625
5658
  * same reason.
5626
5659
  */
5627
5660
  resumeFrom: z.enum(["cursor", "now"]).optional()
5628
- }).refine((v) => v.enabled !== void 0 || v.mapping !== void 0 || v.mappingGraph !== void 0 || v.correlationKey !== void 0 || v.conversationKey !== void 0 || v.debounceMs !== void 0 || v.scope !== void 0, { message: "Provide at least one of \"enabled\", \"mapping\", \"mappingGraph\", \"correlationKey\", \"conversationKey\", \"debounceMs\" or \"scope\"." });
5661
+ }).refine((v) => v.enabled !== void 0 || v.mapping !== void 0 || v.mappingGraph !== void 0 || v.correlationKey !== void 0 || v.conversationKey !== void 0 || v.debounceMs !== void 0 || v.scope !== void 0 || v.account !== void 0, { message: "Provide at least one of \"enabled\", \"mapping\", \"mappingGraph\", \"correlationKey\", \"conversationKey\", \"debounceMs\", \"scope\" or \"account\"." });
5629
5662
  previewTriggerMappingRequestSchema = z.object({
5630
5663
  connector: z.string().min(1),
5631
5664
  trigger: z.string().min(1),
@@ -5659,6 +5692,8 @@ var init_studio_triggers = __esmMin((() => {
5659
5692
  param: z.string().min(1),
5660
5693
  /** Whose credential loads them, as on register. No default: each account has its own. */
5661
5694
  principalKey: z.string().min(1),
5695
+ /** The connected account whose credential loads them, by its grant id, as on register. */
5696
+ account: z.string().min(1).optional(),
5662
5697
  /** The values the draft holds so far. */
5663
5698
  scope: z.record(z.string(), z.string()).optional()
5664
5699
  });
@@ -5766,14 +5801,20 @@ var init_studio_destinations = __esmMin((() => {
5766
5801
  outputName: z.string().min(1),
5767
5802
  connector: z.string().min(1),
5768
5803
  principalKey: z.string().min(1).default("app"),
5804
+ /** The connected account to write with, by its grant id (STA3-123). Beside the
5805
+ * default `app` it picks that account; on a connector with several, the binding
5806
+ * names one or is refused `account_required`. */
5807
+ account: z.string().min(1).optional(),
5769
5808
  /** Empty for an action, and DEFAULTED so an action binding need not send the key at all. */
5770
5809
  destination: destinationSchema.default({})
5771
5810
  });
5772
5811
  patchDestinationBindingRequestSchema = z.object({
5773
5812
  connector: z.string().min(1).optional(),
5774
5813
  principalKey: z.string().min(1).optional(),
5814
+ /** The connected account to write with, by its grant id, as on the upsert. */
5815
+ account: z.string().min(1).optional(),
5775
5816
  destination: destinationSchema.optional()
5776
- }).refine((patch) => patch.connector !== void 0 || patch.principalKey !== void 0 || patch.destination !== void 0, { message: "At least one of connector, principalKey or destination is required." });
5817
+ }).refine((patch) => patch.connector !== void 0 || patch.principalKey !== void 0 || patch.account !== void 0 || patch.destination !== void 0, { message: "At least one of connector, principalKey, account or destination is required." });
5777
5818
  parkedOutputReasonSchema = z.enum([
5778
5819
  "unbound",
5779
5820
  "write_failed",
@@ -17444,8 +17485,9 @@ var init_connector_invoker = __esmMin((() => {
17444
17485
  * @param connector The connect connector id registered in Studio.
17445
17486
  * @param operation The operation / tool id to run on that connector.
17446
17487
  * @param args The operation arguments (path/query/body params). Defaults `{}`.
17447
- * @param opts Optional `{ principal, idempotent, echoPath }`. `principal`
17448
- * defaults to `{ type: 'app' }`; `idempotent` is UNSET by
17488
+ * @param opts Optional `{ principal, account, idempotent, echoPath }`.
17489
+ * `principal` defaults to `{ type: 'app' }`; `account` picks one
17490
+ * of the connector's accounts by id; `idempotent` is UNSET by
17449
17491
  * default and only an explicit `false` changes how a failure is
17450
17492
  * retried; `echoPath` records what this write returned so a
17451
17493
  * later delivery of it can be recognised as our own.
@@ -17456,6 +17498,7 @@ async function callConnector(connector, operation, args, opts) {
17456
17498
  operation,
17457
17499
  ...args !== void 0 ? { args } : {},
17458
17500
  ...opts?.principal !== void 0 ? { principal: opts.principal } : {},
17501
+ ...opts?.account !== void 0 ? { account: opts.account } : {},
17459
17502
  ...opts?.idempotent !== void 0 ? { idempotent: opts.idempotent } : {},
17460
17503
  ...opts?.echoPath !== void 0 ? { echoPath: opts.echoPath } : {}
17461
17504
  })).output;
@@ -17463,12 +17506,22 @@ async function callConnector(connector, operation, args, opts) {
17463
17506
  var connectorHandle, connection;
17464
17507
  var init_call_connector = __esmMin((() => {
17465
17508
  init_connector_invoker();
17466
- connectorHandle = (id) => new Proxy(Object.create(null), { get(_target, prop) {
17467
- if (typeof prop !== "string" || prop === "then") return void 0;
17468
- if (prop === "call") return (operation, args, opts) => callConnector(id, operation, args, opts);
17469
- return (args, opts) => callConnector(id, prop, args, opts);
17470
- } });
17471
- connection = ((id) => connectorHandle(id));
17509
+ connectorHandle = (id, defaults = {}) => {
17510
+ const run = (operation, args, opts) => {
17511
+ const ownPrincipal = opts?.principal !== void 0 && opts.principal.type !== "app";
17512
+ const account = opts?.account ?? (ownPrincipal ? void 0 : defaults.account);
17513
+ return callConnector(id, operation, args, {
17514
+ ...opts,
17515
+ ...account !== void 0 ? { account } : {}
17516
+ });
17517
+ };
17518
+ return new Proxy(Object.create(null), { get(_target, prop) {
17519
+ if (typeof prop !== "string" || prop === "then") return void 0;
17520
+ if (prop === "call") return run;
17521
+ return (args, opts) => run(prop, args, opts);
17522
+ } });
17523
+ };
17524
+ connection = ((id, opts) => connectorHandle(id, opts));
17472
17525
  }));
17473
17526
  //#endregion
17474
17527
  //#region ../../libs/sdk/src/surfaces/external/connect/connect-transport.ts
@@ -18448,10 +18501,11 @@ async function resolveBrowserMode(input) {
18448
18501
  const storedMode = field(BROWSER_MODE_ENV);
18449
18502
  const mode = parseMode(storedMode);
18450
18503
  if (mode === void 0 && storedMode !== void 0) return unknownModeFailure(storedMode);
18451
- if (mode === "local") return {
18504
+ const local = () => input.allowLocalBrowser === false ? localBrowserNotAllowedFailure() : {
18452
18505
  ok: true,
18453
18506
  mode: "local"
18454
18507
  };
18508
+ if (mode === "local") return local();
18455
18509
  if (mode === "cdp") {
18456
18510
  if (!cdpUrl) return missingFieldFailure(mode, [CDP_URL_ENV]);
18457
18511
  return {
@@ -18480,10 +18534,7 @@ async function resolveBrowserMode(input) {
18480
18534
  apiKey,
18481
18535
  projectId
18482
18536
  };
18483
- return {
18484
- ok: true,
18485
- mode: "local"
18486
- };
18537
+ return local();
18487
18538
  }
18488
18539
  /**
18489
18540
  * Read the four keys from the agent's own secret store (they are stored as
@@ -18528,6 +18579,17 @@ function unknownModeFailure(raw) {
18528
18579
  reason: `browserTools: "${raw}" is not a browser mode — expected ${BROWSER_MODES.map((m) => `"${m}"`).join(", ")}. Fix ${BROWSER_MODE_ENV} on this box, or pick a browser in the Studio "Browser settings" screen.`
18529
18580
  };
18530
18581
  }
18582
+ /**
18583
+ * The failure a host with no local Chromium (a deployed box) returns for any launch
18584
+ * that would open one. Exported so the Studio "Browser settings" screen reports the
18585
+ * same words the first browser action fails with.
18586
+ */
18587
+ function localBrowserNotAllowedFailure() {
18588
+ return {
18589
+ ok: false,
18590
+ reason: `browserTools: a deployed box has no local browser. Set ${BROWSERBASE_API_KEY_ENV} and ${BROWSERBASE_PROJECT_ID_ENV}, or ${CDP_URL_ENV}, in the Studio "Browser settings" screen.`
18591
+ };
18592
+ }
18531
18593
  /** The loud failure an explicit-but-unsatisfiable mode produces. */
18532
18594
  function missingFieldFailure(mode, missing) {
18533
18595
  return {
@@ -18582,7 +18644,8 @@ function isWsl() {
18582
18644
  * Resolve the Chrome/Chromium executable Stagehand's `chrome-launcher` should launch for a
18583
18645
  * LOCAL browser. Precedence: an explicit `STACKBONE_BROWSER_EXECUTABLE_PATH`, else the
18584
18646
  * workspace's Playwright Chromium (`playwright`'s `chromium.executablePath()`). Returns
18585
- * `undefined` when neither resolves, so `chrome-launcher` falls back to its own detection.
18647
+ * `undefined` when neither resolves, and the launch then fails with
18648
+ * {@link LOCAL_CHROMIUM_MISSING_MESSAGE} instead of letting `chrome-launcher` detect one.
18586
18649
  *
18587
18650
  * Why pin it: `chrome-launcher` auto-detects a SYSTEM Chrome, which on WSL2 is the Windows
18588
18651
  * `chrome.exe` reached through `/mnt/c` — a Windows binary whose CDP port is unreachable
@@ -18737,14 +18800,18 @@ async function downloadFromFileOrigin(context, current, url, maxBytes) {
18737
18800
  }
18738
18801
  /** Reject once the server has sent nothing for {@link DOWNLOAD_STALL_MS}. */
18739
18802
  function withinStallLimit(call, url) {
18740
- let timer;
18741
- const stalled = new Promise((_, reject) => {
18742
- timer = setTimeout(() => {
18743
- const seconds = DOWNLOAD_STALL_MS / 1e3;
18744
- reject(/* @__PURE__ */ new Error(`Could not download ${url}: the server sent nothing for ${seconds} seconds.`));
18745
- }, DOWNLOAD_STALL_MS);
18803
+ return raceTimer(call, DOWNLOAD_STALL_MS, () => {
18804
+ const seconds = DOWNLOAD_STALL_MS / 1e3;
18805
+ throw new Error(`Could not download ${url}: the server sent nothing for ${seconds} seconds.`);
18746
18806
  });
18747
- return Promise.race([call, stalled]).finally(() => clearTimeout(timer));
18807
+ }
18808
+ /** `call`'s outcome, or `late()`'s once `ms` pass without one. */
18809
+ function raceTimer(call, ms, late) {
18810
+ let timer;
18811
+ const timedOut = new Promise((resolve) => {
18812
+ timer = setTimeout(resolve, ms);
18813
+ }).then(late);
18814
+ return Promise.race([call, timedOut]).finally(() => clearTimeout(timer));
18748
18815
  }
18749
18816
  /** Read a CDP `IO` stream to its end, decoding base64 and plain-text chunks alike. */
18750
18817
  async function readCdpStream(send, handle, maxBytes) {
@@ -18831,7 +18898,8 @@ function createStagehandBrowserFactory(options) {
18831
18898
  browserbase: options?.browserbase
18832
18899
  },
18833
18900
  env: process.env,
18834
- readSecrets: readBrowserProviderSecrets
18901
+ readSecrets: readBrowserProviderSecrets,
18902
+ allowLocalBrowser: options?.allowLocalBrowser
18835
18903
  });
18836
18904
  if (!resolved.ok) throw new Error(resolved.reason);
18837
18905
  const cdpUrl = resolved.mode === "cdp" ? resolved.cdpUrl : void 0;
@@ -18851,7 +18919,8 @@ function createStagehandBrowserFactory(options) {
18851
18919
  if (hasInitialState) localBrowserLaunchOptions["storageState"] = initialState;
18852
18920
  if (!cdpUrl) {
18853
18921
  const executablePath = await resolveChromeExecutablePath();
18854
- if (executablePath) localBrowserLaunchOptions["executablePath"] = executablePath;
18922
+ if (!executablePath) throw new Error(LOCAL_CHROMIUM_MISSING_MESSAGE);
18923
+ localBrowserLaunchOptions["executablePath"] = executablePath;
18855
18924
  const args = ["--no-sandbox", "--disable-dev-shm-usage"];
18856
18925
  if (isWsl()) {
18857
18926
  wslProfileDir = await mkdtemp(join(tmpdir(), "stackbone-chromium-"));
@@ -18898,13 +18967,15 @@ function createStagehandBrowserFactory(options) {
18898
18967
  await removeWslProfileDir();
18899
18968
  throw err;
18900
18969
  }
18901
- const pageHandle = stagehand.context.activePage() ?? await stagehand.context.newPage();
18970
+ const pageHandle = cdpUrl ? await stagehand.context.newPage() : stagehand.context.activePage() ?? await stagehand.context.newPage();
18971
+ const tabs = cdpUrl ? ownTabs(pageHandle, stagehand.context) : void 0;
18902
18972
  if (hasInitialState) await injectStorageStateViaCDP(pageHandle, initialState);
18903
18973
  const liveViewUrl = browserbase ? await resolveBrowserbaseLiveViewUrl(stagehand, browserbase, resolveLiveViewUrl) : void 0;
18904
18974
  return {
18905
- page: adaptStagehandPage(stagehand, pageHandle),
18975
+ page: adaptStagehandPage(stagehand, pageHandle, tabs),
18906
18976
  close: async () => {
18907
18977
  try {
18978
+ await tabs?.closeAll();
18908
18979
  await stagehand.close();
18909
18980
  } finally {
18910
18981
  await removeWslProfileDir();
@@ -18912,7 +18983,7 @@ function createStagehandBrowserFactory(options) {
18912
18983
  },
18913
18984
  isLive: async () => {
18914
18985
  try {
18915
- return await probeStagehandLive(stagehand.context.activePage() ?? pageHandle);
18986
+ return await probeStagehandLive(tabs?.current() ?? stagehand.context.activePage() ?? pageHandle);
18916
18987
  } catch {
18917
18988
  return false;
18918
18989
  }
@@ -18969,6 +19040,97 @@ async function resolveBrowserbaseLiveViewUrl(stagehand, credentials, resolve) {
18969
19040
  if (typeof sessionId !== "string" || sessionId.length === 0) return void 0;
18970
19041
  return resolve(sessionId, credentials);
18971
19042
  }
19043
+ function ownTabs(first, context) {
19044
+ const owned = new Set([first]);
19045
+ let current = first;
19046
+ const handled = /* @__PURE__ */ new Set();
19047
+ let acting = Promise.resolve();
19048
+ let closing = false;
19049
+ const openTabs = () => {
19050
+ try {
19051
+ return typeof context.pages === "function" ? new Set(context.pages()) : void 0;
19052
+ } catch {
19053
+ return;
19054
+ }
19055
+ };
19056
+ const currentTab = () => {
19057
+ const open = openTabs();
19058
+ if (open !== void 0 && !open.has(current)) {
19059
+ const fallback = [...owned].reverse().find((tab) => open.has(tab));
19060
+ if (fallback !== void 0) current = fallback;
19061
+ }
19062
+ return current;
19063
+ };
19064
+ const browserSend = async (method, params) => {
19065
+ if (typeof context.conn?.send === "function") return context.conn.send(method, params);
19066
+ const via = [currentTab(), ...owned].find((tab) => typeof tab.sendCDP === "function");
19067
+ if (via?.sendCDP === void 0) throw new Error("This browser exposes no CDP access.");
19068
+ return via.sendCDP(method, params);
19069
+ };
19070
+ const openedByOwned = async () => {
19071
+ try {
19072
+ const ownIds = new Set([...owned].flatMap((tab) => tab.targetId?.() ?? []));
19073
+ if (ownIds.size === 0) return [];
19074
+ const answer = await settleWithin(browserSend("Target.getTargets"), FOLLOW_TAB_CAP_MS, {});
19075
+ const ownedOpener = (id) => id !== void 0 && ownIds.has(id);
19076
+ return (answer.targetInfos ?? []).filter((info) => info.type === "page" && (ownedOpener(info.openerId) || ownedOpener(info.openerFrameId)) && !ownIds.has(info.targetId)).map((info) => info.targetId);
19077
+ } catch {
19078
+ return [];
19079
+ }
19080
+ };
19081
+ const followOpenedTab = async () => {
19082
+ if (closing) return;
19083
+ const opened = new Set((await openedByOwned()).filter((id) => !handled.has(id)));
19084
+ if (opened.size === 0) return;
19085
+ for (const id of opened) handled.add(id);
19086
+ for (let waited = 0;; waited += FOLLOW_TAB_POLL_MS) {
19087
+ if (closing) return;
19088
+ const tab = browserTabs(context).find((page) => opened.has(page.targetId?.() ?? ""));
19089
+ if (tab !== void 0) {
19090
+ owned.add(tab);
19091
+ current = tab;
19092
+ return;
19093
+ }
19094
+ if (waited >= FOLLOW_TAB_CAP_MS) return;
19095
+ await delay(FOLLOW_TAB_POLL_MS);
19096
+ }
19097
+ };
19098
+ return {
19099
+ current: currentTab,
19100
+ afterAct: (act) => {
19101
+ const followed = act.then(async (result) => {
19102
+ await followOpenedTab();
19103
+ return result;
19104
+ });
19105
+ acting = Promise.allSettled([acting, followed]).then(() => void 0);
19106
+ return followed;
19107
+ },
19108
+ closeAll: async () => {
19109
+ closing = true;
19110
+ await settleWithin(acting, FOLLOW_TAB_CAP_MS, void 0);
19111
+ const children = await openedByOwned();
19112
+ await settleWithin(Promise.allSettled([...children.map(async (targetId) => browserSend("Target.closeTarget", { targetId })), ...[...owned].map(async (tab) => tab.close())]), FOLLOW_TAB_CAP_MS, []);
19113
+ }
19114
+ };
19115
+ }
19116
+ /** `call`'s answer, or `fallback` once `ms` pass without one. */
19117
+ function settleWithin(call, ms, fallback) {
19118
+ return raceTimer(call, ms, () => fallback);
19119
+ }
19120
+ /** The tabs of the browser; only the active one when the context cannot list them. */
19121
+ function browserTabs(context) {
19122
+ try {
19123
+ if (typeof context.pages === "function") return context.pages();
19124
+ const active = context.activePage();
19125
+ return active === void 0 ? [] : [active];
19126
+ } catch {
19127
+ return [];
19128
+ }
19129
+ }
19130
+ /** Stagehand's own test for a Zod schema: `extract` reads its options after one. */
19131
+ function isZodSchema(value) {
19132
+ return typeof value === "object" && value !== null && "parse" in value && "safeParse" in value;
19133
+ }
18972
19134
  /**
18973
19135
  * Bridge a Stagehand v3 instance + its CDP-backed page to the options-object
18974
19136
  * {@link BrowserPage} contract the browser tools and the session pool speak.
@@ -18982,11 +19144,14 @@ async function resolveBrowserbaseLiveViewUrl(stagehand, credentials, resolve) {
18982
19144
  * option is forwarded verbatim as the trailing options object. The model is not one of
18983
19145
  * them: it rides the call's async context ({@link withBrowserModel}).
18984
19146
  */
18985
- function adaptStagehandPage(stagehand, initialPage) {
18986
- const livePage = () => stagehand.context.activePage() ?? initialPage;
19147
+ function adaptStagehandPage(stagehand, initialPage, tabs) {
19148
+ const livePage = () => tabs?.current() ?? stagehand.context.activePage() ?? initialPage;
18987
19149
  let downloadsSettled = Promise.resolve();
18988
19150
  let downloadingFrom;
18989
- const afterDownloads = (run) => downloadsSettled.then(run);
19151
+ const afterDownloads = (run) => {
19152
+ const called = tabs?.current();
19153
+ return downloadsSettled.then(() => run(called ?? livePage()));
19154
+ };
18990
19155
  const aiOptions = (opts) => {
18991
19156
  const rest = {};
18992
19157
  for (const [key, value] of Object.entries(opts ?? {})) if (key !== "action" && key !== "instruction" && key !== "schema") rest[key] = value;
@@ -18996,27 +19161,40 @@ function adaptStagehandPage(stagehand, initialPage) {
18996
19161
  const value = (opts ?? {})["instruction"];
18997
19162
  return typeof value === "string" ? value : void 0;
18998
19163
  };
19164
+ const onOwnTab = (opts, tab) => tabs ? {
19165
+ ...aiOptions(opts),
19166
+ page: tab
19167
+ } : aiOptions(opts);
18999
19168
  return {
19000
- goto: (url, opts) => afterDownloads(() => livePage().goto(url, opts)),
19001
- url: () => (downloadingFrom ?? livePage()).url(),
19002
- title: () => afterDownloads(() => livePage().title()),
19003
- screenshot: (opts) => afterDownloads(() => livePage().screenshot(opts)),
19004
- act: (opts) => afterDownloads(() => {
19005
- const action = opts["action"];
19006
- const instruction = typeof action === "string" ? action : instructionOf(opts) ?? "";
19007
- return stagehand.act(instruction, aiOptions(opts));
19169
+ goto: (url, opts) => afterDownloads((tab) => tab.goto(url, opts)),
19170
+ url: () => (tabs?.current() ?? downloadingFrom ?? livePage()).url(),
19171
+ title: () => afterDownloads((tab) => tab.title()),
19172
+ screenshot: (opts) => afterDownloads((tab) => tab.screenshot(opts)),
19173
+ act: (opts) => {
19174
+ const act = afterDownloads((tab) => {
19175
+ const action = opts["action"];
19176
+ const instruction = typeof action === "string" ? action : instructionOf(opts) ?? "";
19177
+ return stagehand.act(instruction, onOwnTab(opts, tab));
19178
+ });
19179
+ return tabs === void 0 ? act : tabs.afterAct(act);
19180
+ },
19181
+ extract: (opts) => afterDownloads((tab) => {
19182
+ const instruction = instructionOf(opts);
19183
+ const schema = (opts ?? {})["schema"];
19184
+ if (tabs === void 0) return stagehand.extract(instruction, schema, aiOptions(opts));
19185
+ if (instruction === void 0) return stagehand.extract(onOwnTab(opts, tab));
19186
+ return isZodSchema(schema) ? stagehand.extract(instruction, schema, onOwnTab(opts, tab)) : stagehand.extract(instruction, onOwnTab(opts, tab));
19008
19187
  }),
19009
- extract: (opts) => afterDownloads(() => stagehand.extract(instructionOf(opts), (opts ?? {})["schema"], aiOptions(opts))),
19010
- observe: (opts) => afterDownloads(() => stagehand.observe(instructionOf(opts), aiOptions(opts))),
19011
- storageState: () => afterDownloads(() => captureStorageStateViaCDP(livePage())),
19012
- applyStorageState: (state) => afterDownloads(() => injectStorageStateViaCDP(livePage(), state)),
19013
- evaluate: (script) => afterDownloads(() => {
19014
- const p = livePage();
19015
- return typeof p.evaluate === "function" ? p.evaluate(script) : void 0;
19188
+ observe: (opts) => afterDownloads((tab) => {
19189
+ const instruction = instructionOf(opts);
19190
+ return tabs !== void 0 && instruction === void 0 ? stagehand.observe(onOwnTab(opts, tab)) : stagehand.observe(instruction, onOwnTab(opts, tab));
19016
19191
  }),
19192
+ storageState: () => afterDownloads((tab) => captureStorageStateViaCDP(tab)),
19193
+ applyStorageState: (state) => afterDownloads((tab) => injectStorageStateViaCDP(tab, state)),
19194
+ evaluate: (script) => afterDownloads((tab) => typeof tab.evaluate === "function" ? tab.evaluate(script) : void 0),
19017
19195
  download: ({ url, maxBytes }) => {
19018
- const download = afterDownloads(async () => {
19019
- downloadingFrom = livePage();
19196
+ const download = afterDownloads(async (tab) => {
19197
+ downloadingFrom = tab;
19020
19198
  try {
19021
19199
  return await downloadFromFileOrigin(stagehand.context, downloadingFrom, url, maxBytes);
19022
19200
  } finally {
@@ -19028,7 +19206,7 @@ function adaptStagehandPage(stagehand, initialPage) {
19028
19206
  }
19029
19207
  };
19030
19208
  }
19031
- var MAX_DOWNLOAD_BYTES, DOWNLOAD_STALL_MS, browserModel, delay, DEFAULT_BROWSER_MODEL, BROWSERBASE_SESSION_TIMEOUT_SECONDS, BROWSERBASE_KEEP_ALIVE_ENV, HEADED_ENV, EXECUTABLE_PATH_ENV, PROBE_RETRY_DELAY_MS, BROWSERBASE_API_BASE$1, defaultResolveLiveViewUrl, defaultImportStagehand, readBrowserProviderSecrets, JSON_FENCE, STAGEHAND_PEER, STAGEHAND_INSTALL_SPEC;
19209
+ var MAX_DOWNLOAD_BYTES, DOWNLOAD_STALL_MS, browserModel, delay, DEFAULT_BROWSER_MODEL, BROWSERBASE_SESSION_TIMEOUT_SECONDS, BROWSERBASE_KEEP_ALIVE_ENV, HEADED_ENV, EXECUTABLE_PATH_ENV, LOCAL_CHROMIUM_MISSING_MESSAGE, PROBE_RETRY_DELAY_MS, BROWSERBASE_API_BASE$1, defaultResolveLiveViewUrl, defaultImportStagehand, readBrowserProviderSecrets, JSON_FENCE, FOLLOW_TAB_CAP_MS, FOLLOW_TAB_POLL_MS, STAGEHAND_PEER, STAGEHAND_INSTALL_SPEC;
19032
19210
  var init_browser_tools = __esmMin((() => {
19033
19211
  init_ambient_client();
19034
19212
  init_browser_profile_state();
@@ -19109,6 +19287,7 @@ var init_browser_tools = __esmMin((() => {
19109
19287
  BROWSERBASE_KEEP_ALIVE_ENV = "STACKBONE_BROWSERBASE_KEEP_ALIVE";
19110
19288
  HEADED_ENV = "STACKBONE_BROWSER_HEADED";
19111
19289
  EXECUTABLE_PATH_ENV = "STACKBONE_BROWSER_EXECUTABLE_PATH";
19290
+ LOCAL_CHROMIUM_MISSING_MESSAGE = `No local Chromium was found on this machine. Install one through Playwright (\`pnpm add playwright@1.59.1\`, then \`pnpm exec playwright install chromium\`) or pin a binary with ${EXECUTABLE_PATH_ENV}.`;
19112
19291
  PROBE_RETRY_DELAY_MS = 150;
19113
19292
  BROWSERBASE_API_BASE$1 = "https://api.browserbase.com";
19114
19293
  defaultResolveLiveViewUrl = async (sessionId, credentials) => {
@@ -19127,6 +19306,8 @@ var init_browser_tools = __esmMin((() => {
19127
19306
  );
19128
19307
  readBrowserProviderSecrets = (names) => stackbone.secrets.getMany(names);
19129
19308
  JSON_FENCE = /^```(?:json)?[^\S\n]*\n([\s\S]*)```$/i;
19309
+ FOLLOW_TAB_CAP_MS = 2e3;
19310
+ FOLLOW_TAB_POLL_MS = 25;
19130
19311
  STAGEHAND_PEER = {
19131
19312
  pkg: "@browserbasehq/stagehand",
19132
19313
  version: "3.6.0"
@@ -20014,14 +20195,14 @@ var init_link_auth_props = __esmMin((() => {
20014
20195
  init_link_auth_props();
20015
20196
  var mailboxPattern = new RegExp(LINK_MAILBOX_PATTERN);
20016
20197
  var GRAPH_ID = /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i;
20017
- var EMAIL = /^[^@]+@[^@.]+(?:\.[^@.]+)+$/;
20198
+ var EMAIL$1 = /^[^@]+@[^@.]+(?:\.[^@.]+)+$/;
20018
20199
  function mailboxOfMapping(mapping) {
20019
20200
  if (!mapping) return null;
20020
20201
  let mailbox = null;
20021
20202
  for (const key of mapping.matchAll(/(?<![\\\w`])"mailbox"\s*:/g)) {
20022
20203
  const value = /^\s*((?:\(\s*)*)"([^"\\\s]+)"((?:\s*\))*)\s*(?=[,}])/.exec(mapping.slice(key.index + key[0].length));
20023
20204
  if (!value || (value[1].match(/\(/g)?.length ?? 0) !== (value[3].match(/\)/g)?.length ?? 0)) return null;
20024
- if (!mailboxPattern.test(value[2]) || !EMAIL.test(value[2]) && !GRAPH_ID.test(value[2])) return null;
20205
+ if (!mailboxPattern.test(value[2]) || !EMAIL$1.test(value[2]) && !GRAPH_ID.test(value[2])) return null;
20025
20206
  if (mailbox !== null && mailbox !== value[2]) return null;
20026
20207
  mailbox = value[2];
20027
20208
  }
@@ -23190,7 +23371,7 @@ var init_role = __esmMin((() => {
23190
23371
  var STACKBONE_CLI_BUILD_VERSION, EMULATOR_CAPABILITY_ROUTES, EMULATOR_CAPABILITIES, buildContractResponse;
23191
23372
  var init_contract = __esmMin((() => {
23192
23373
  init_src$26();
23193
- STACKBONE_CLI_BUILD_VERSION = "0.9.4";
23374
+ STACKBONE_CLI_BUILD_VERSION = "0.9.5";
23194
23375
  EMULATOR_CAPABILITY_ROUTES = {
23195
23376
  "database.postgres_direct": "/api/db",
23196
23377
  "rag.basic": "/api/rag",
@@ -27643,7 +27824,7 @@ function diffObjects(prev, next, basePath) {
27643
27824
  }
27644
27825
  function diffValue(prev, next, path) {
27645
27826
  if (deepEqual$1(prev, next)) return [];
27646
- if (isPlainObject$1(prev) && isPlainObject$1(next)) return diffObjects(prev, next, path);
27827
+ if (isPlainObject$2(prev) && isPlainObject$2(next)) return diffObjects(prev, next, path);
27647
27828
  if (Array.isArray(prev) && Array.isArray(next)) return diffArrays(prev, next, path);
27648
27829
  return [{
27649
27830
  op: "replace",
@@ -27657,11 +27838,11 @@ function diffValue(prev, next, path) {
27657
27838
  function diffState(prev, next) {
27658
27839
  return diffObjects(prev, next, "");
27659
27840
  }
27660
- var EXCLUDED_KEYS, escapeSegment, isPlainObject$1, deepEqual$1;
27841
+ var EXCLUDED_KEYS, escapeSegment, isPlainObject$2, deepEqual$1;
27661
27842
  var init_state_projector = __esmMin((() => {
27662
27843
  EXCLUDED_KEYS = new Set(["messages", "files"]);
27663
27844
  escapeSegment = (key) => key.replace(/~/g, "~0").replace(/\//g, "~1");
27664
- isPlainObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
27845
+ isPlainObject$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
27665
27846
  deepEqual$1 = (a, b) => {
27666
27847
  if (a === b) return true;
27667
27848
  if (typeof a !== typeof b) return false;
@@ -27669,7 +27850,7 @@ var init_state_projector = __esmMin((() => {
27669
27850
  if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
27670
27851
  return a.every((v, i) => deepEqual$1(v, b[i]));
27671
27852
  }
27672
- if (isPlainObject$1(a) && isPlainObject$1(b)) {
27853
+ if (isPlainObject$2(a) && isPlainObject$2(b)) {
27673
27854
  const aKeys = Object.keys(a);
27674
27855
  const bKeys = Object.keys(b);
27675
27856
  if (aKeys.length !== bKeys.length) return false;
@@ -37864,7 +38045,7 @@ function argTargetLabel(target) {
37864
38045
  return argSegments(target).join(".");
37865
38046
  }
37866
38047
  /** A plain JSON object — the only thing a path may traverse on the way down. */
37867
- function isRecord$5(value) {
38048
+ function isRecord$6(value) {
37868
38049
  return typeof value === "object" && value !== null && !Array.isArray(value);
37869
38050
  }
37870
38051
  /**
@@ -37885,7 +38066,7 @@ function setPath(target, path, value) {
37885
38066
  let node = target;
37886
38067
  for (const segment of segments) {
37887
38068
  const existing = node[segment];
37888
- const next = isRecord$5(existing) ? { ...existing } : {};
38069
+ const next = isRecord$6(existing) ? { ...existing } : {};
37889
38070
  node[segment] = next;
37890
38071
  node = next;
37891
38072
  }
@@ -39570,7 +39751,7 @@ var init_extra_query_params = __esmMin((() => {}));
39570
39751
  //#endregion
39571
39752
  //#region ../../libs/shared/connect-registry/src/lib/introspector/openapi/openapi-introspector.ts
39572
39753
  /** A plain JSON object guard — the spec is `unknown`, so every descent re-checks. */
39573
- function isRecord$4(value) {
39754
+ function isRecord$5(value) {
39574
39755
  return typeof value === "object" && value !== null && !Array.isArray(value);
39575
39756
  }
39576
39757
  function asString$2(value) {
@@ -39585,7 +39766,7 @@ function asString$2(value) {
39585
39766
  function firstServerUrl(node) {
39586
39767
  const servers = node["servers"];
39587
39768
  if (!Array.isArray(servers)) return void 0;
39588
- for (const server of servers) if (isRecord$4(server)) {
39769
+ for (const server of servers) if (isRecord$5(server)) {
39589
39770
  const url = asString$2(server["url"]);
39590
39771
  if (url) return url;
39591
39772
  }
@@ -39603,7 +39784,7 @@ function resolveRef(root, ref) {
39603
39784
  const tokens = ref.slice(2).split("/").map((t) => t.replace(/~1/g, "/").replace(/~0/g, "~"));
39604
39785
  let node = root;
39605
39786
  for (const token of tokens) {
39606
- if (!isRecord$4(node)) return void 0;
39787
+ if (!isRecord$5(node)) return void 0;
39607
39788
  node = node[token];
39608
39789
  }
39609
39790
  return node;
@@ -39616,7 +39797,7 @@ function resolveRef(root, ref) {
39616
39797
  */
39617
39798
  function derefParameter(raw, root) {
39618
39799
  let current = raw;
39619
- for (let i = 0; i < 8 && isRecord$4(current) && typeof current["$ref"] === "string"; i++) current = resolveRef(root, current["$ref"]);
39800
+ for (let i = 0; i < 8 && isRecord$5(current) && typeof current["$ref"] === "string"; i++) current = resolveRef(root, current["$ref"]);
39620
39801
  return current;
39621
39802
  }
39622
39803
  /**
@@ -39627,9 +39808,9 @@ function derefParameter(raw, root) {
39627
39808
  */
39628
39809
  function derefSchemaShallow(schema, root) {
39629
39810
  const resolved = derefParameter(schema, root) ?? schema;
39630
- if (!isRecord$4(resolved)) return resolved;
39811
+ if (!isRecord$5(resolved)) return resolved;
39631
39812
  const items = resolved["items"];
39632
- if (isRecord$4(items) && typeof items["$ref"] === "string") {
39813
+ if (isRecord$5(items) && typeof items["$ref"] === "string") {
39633
39814
  const resolvedItems = derefParameter(items, root);
39634
39815
  if (resolvedItems !== void 0) return {
39635
39816
  ...resolved,
@@ -39666,7 +39847,7 @@ function joinBasePath(basePath, path) {
39666
39847
  * small schema fragment from the recognised keywords.
39667
39848
  */
39668
39849
  function normalizeParameter$1(param, source, context, warnings) {
39669
- if (!isRecord$4(param)) {
39850
+ if (!isRecord$5(param)) {
39670
39851
  warnings.push(`${context}: skipped a non-object parameter`);
39671
39852
  return null;
39672
39853
  }
@@ -39718,12 +39899,12 @@ function normalizeParameter$1(param, source, context, warnings) {
39718
39899
  */
39719
39900
  function extractRequestBodyOpenapi(operation, root) {
39720
39901
  const requestBody = derefParameter(operation["requestBody"], root);
39721
- if (!isRecord$4(requestBody)) return void 0;
39902
+ if (!isRecord$5(requestBody)) return void 0;
39722
39903
  const content = requestBody["content"];
39723
- if (!isRecord$4(content)) return void 0;
39904
+ if (!isRecord$5(content)) return void 0;
39724
39905
  const json = content["application/json"];
39725
- if (isRecord$4(json) && json["schema"] !== void 0) return json["schema"];
39726
- for (const mediaType of Object.values(content)) if (isRecord$4(mediaType) && mediaType["schema"] !== void 0) return mediaType["schema"];
39906
+ if (isRecord$5(json) && json["schema"] !== void 0) return json["schema"];
39907
+ for (const mediaType of Object.values(content)) if (isRecord$5(mediaType) && mediaType["schema"] !== void 0) return mediaType["schema"];
39727
39908
  }
39728
39909
  /**
39729
39910
  * Extract the success (200/2xx) response JSON Schema. Handles both dialects:
@@ -39732,7 +39913,7 @@ function extractRequestBodyOpenapi(operation, root) {
39732
39913
  */
39733
39914
  function extractResponseSchema(operation, source) {
39734
39915
  const responses = operation["responses"];
39735
- if (!isRecord$4(responses)) return void 0;
39916
+ if (!isRecord$5(responses)) return void 0;
39736
39917
  const ordered = [
39737
39918
  "200",
39738
39919
  ...Object.keys(responses).filter((c) => c !== "200" && /^2\d\d$/.test(c)).sort(),
@@ -39740,16 +39921,16 @@ function extractResponseSchema(operation, source) {
39740
39921
  ];
39741
39922
  for (const code of ordered) {
39742
39923
  const response = responses[code];
39743
- if (!isRecord$4(response)) continue;
39924
+ if (!isRecord$5(response)) continue;
39744
39925
  if (source === "swagger") {
39745
39926
  if (response["schema"] !== void 0) return response["schema"];
39746
39927
  continue;
39747
39928
  }
39748
39929
  const content = response["content"];
39749
- if (!isRecord$4(content)) continue;
39930
+ if (!isRecord$5(content)) continue;
39750
39931
  const json = content["application/json"];
39751
- if (isRecord$4(json) && json["schema"] !== void 0) return json["schema"];
39752
- for (const mediaType of Object.values(content)) if (isRecord$4(mediaType) && mediaType["schema"] !== void 0) return mediaType["schema"];
39932
+ if (isRecord$5(json) && json["schema"] !== void 0) return json["schema"];
39933
+ for (const mediaType of Object.values(content)) if (isRecord$5(mediaType) && mediaType["schema"] !== void 0) return mediaType["schema"];
39753
39934
  }
39754
39935
  }
39755
39936
  /**
@@ -39775,7 +39956,7 @@ function buildOperation(rawPath, resolvedPath, method, operation, pathLevelParam
39775
39956
  warnings.push(`${context}: skipped a parameter whose $ref did not resolve`);
39776
39957
  continue;
39777
39958
  }
39778
- if (isRecord$4(raw) && source === "swagger") {
39959
+ if (isRecord$5(raw) && source === "swagger") {
39779
39960
  const location = asString$2(raw["in"]);
39780
39961
  if (location === "body") {
39781
39962
  if (bodyParamSchema === void 0 && raw["schema"] !== void 0) {
@@ -39831,7 +40012,7 @@ function buildOperation(rawPath, resolvedPath, method, operation, pathLevelParam
39831
40012
  */
39832
40013
  function readContinuation(operation) {
39833
40014
  const declared = operation["x-stackbone-continuation"];
39834
- if (!isRecord$4(declared)) return void 0;
40015
+ if (!isRecord$5(declared)) return void 0;
39835
40016
  const operationId = asString$2(declared["operationId"]);
39836
40017
  const urlHeader = asString$2(declared["urlHeader"]);
39837
40018
  if (operationId === void 0 || operationId === "") return void 0;
@@ -39851,7 +40032,7 @@ function readContinuation(operation) {
39851
40032
  */
39852
40033
  function introspectOpenApi(document) {
39853
40034
  const warnings = [];
39854
- if (!isRecord$4(document)) {
40035
+ if (!isRecord$5(document)) {
39855
40036
  warnings.push("document is not an object; nothing to introspect");
39856
40037
  return {
39857
40038
  source: "openapi",
@@ -39871,7 +40052,7 @@ function introspectOpenApi(document) {
39871
40052
  const basePath = source === "swagger" ? asString$2(document["basePath"]) : void 0;
39872
40053
  const rootServerUrl = source === "openapi" ? firstServerUrl(document) : void 0;
39873
40054
  const paths = document["paths"];
39874
- if (!isRecord$4(paths)) {
40055
+ if (!isRecord$5(paths)) {
39875
40056
  warnings.push("document has no 'paths' object; no operations extracted");
39876
40057
  return {
39877
40058
  source,
@@ -39882,7 +40063,7 @@ function introspectOpenApi(document) {
39882
40063
  const operations = [];
39883
40064
  const derivedIdWarnings = [];
39884
40065
  for (const [rawPath, pathItem] of Object.entries(paths)) {
39885
- if (!isRecord$4(pathItem)) {
40066
+ if (!isRecord$5(pathItem)) {
39886
40067
  warnings.push(`path '${rawPath}': not an object, skipped`);
39887
40068
  continue;
39888
40069
  }
@@ -39892,7 +40073,7 @@ function introspectOpenApi(document) {
39892
40073
  for (const method of HTTP_METHODS) {
39893
40074
  const operation = pathItem[method];
39894
40075
  if (operation === void 0) continue;
39895
- if (!isRecord$4(operation)) {
40076
+ if (!isRecord$5(operation)) {
39896
40077
  warnings.push(`${method.toUpperCase()} ${rawPath}: operation is not an object, skipped`);
39897
40078
  continue;
39898
40079
  }
@@ -41243,6 +41424,10 @@ var createTriggerSubscriptionsStore = (opts) => {
41243
41424
  assertOpen();
41244
41425
  const sets = [];
41245
41426
  const params = [id];
41427
+ if (patch.principalKey !== void 0) {
41428
+ params.push(patch.principalKey);
41429
+ sets.push(`principal_key = $${params.length}`);
41430
+ }
41246
41431
  if (patch.enabled !== void 0) {
41247
41432
  params.push(patch.enabled);
41248
41433
  sets.push(`enabled = $${params.length}`);
@@ -42323,7 +42508,7 @@ var init_route_registry_generation = __esmMin((() => {
42323
42508
  }));
42324
42509
  //#endregion
42325
42510
  //#region ../../libs/runtime/workspace-runtime/src/lib/operations/catalog.ts
42326
- var JSON_MEDIA_TYPE, operationKey, capabilityGateIndex, PRIMITIVE_TYPES, isRecord$3, mediaTypeEssence, isExactlyJson, jsonSchemaOf$1, collectRefs, LOCAL_REF, resolveLocalRef, unresolvableRef, isBoundedPrimitive, DEFAULT_STYLE, isParameterLocation, normalizeParameter, bearerSatisfies, out, classify, toEntry$1, buildOperationCatalog, createOperationCatalogCache, visibleTo, EMPTY_FILTER_HINT, listOperations, OPERATION_NOT_FOUND, componentsReachedBy, closures, describeOperation;
42511
+ var JSON_MEDIA_TYPE, operationKey, capabilityGateIndex, PRIMITIVE_TYPES, isRecord$4, mediaTypeEssence, isExactlyJson, jsonSchemaOf$1, collectRefs, LOCAL_REF, resolveLocalRef, unresolvableRef, isBoundedPrimitive, DEFAULT_STYLE, isParameterLocation, normalizeParameter, bearerSatisfies, out, classify, toEntry$1, buildOperationCatalog, createOperationCatalogCache, visibleTo, EMPTY_FILTER_HINT, listOperations, OPERATION_NOT_FOUND, componentsReachedBy, closures, describeOperation;
42327
42512
  var init_catalog = __esmMin((() => {
42328
42513
  init_src$26();
42329
42514
  init_mutations_outside_catalog();
@@ -42349,7 +42534,7 @@ var init_catalog = __esmMin((() => {
42349
42534
  "integer",
42350
42535
  "boolean"
42351
42536
  ]);
42352
- isRecord$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
42537
+ isRecord$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
42353
42538
  mediaTypeEssence = (mediaType) => (mediaType.split(";")[0] ?? "").trim().toLowerCase();
42354
42539
  isExactlyJson = (content) => {
42355
42540
  const types = Object.keys(content ?? {}).map(mediaTypeEssence);
@@ -42358,7 +42543,7 @@ var init_catalog = __esmMin((() => {
42358
42543
  jsonSchemaOf$1 = (content) => Object.entries(content).find(([type]) => mediaTypeEssence(type) === JSON_MEDIA_TYPE)?.[1]?.schema;
42359
42544
  collectRefs = (node, into = /* @__PURE__ */ new Set()) => {
42360
42545
  if (Array.isArray(node)) for (const item of node) collectRefs(item, into);
42361
- else if (isRecord$3(node)) for (const [key, value] of Object.entries(node)) if (key === "$ref" && typeof value === "string") into.add(value);
42546
+ else if (isRecord$4(node)) for (const [key, value] of Object.entries(node)) if (key === "$ref" && typeof value === "string") into.add(value);
42362
42547
  else collectRefs(value, into);
42363
42548
  return into;
42364
42549
  };
@@ -42401,7 +42586,7 @@ var init_catalog = __esmMin((() => {
42401
42586
  }
42402
42587
  };
42403
42588
  isBoundedPrimitive = (schema) => {
42404
- if (!isRecord$3(schema) || "$ref" in schema) return false;
42589
+ if (!isRecord$4(schema) || "$ref" in schema) return false;
42405
42590
  const branches = schema["anyOf"] ?? schema["oneOf"];
42406
42591
  if (Array.isArray(branches)) return branches.length > 0 && branches.every(isBoundedPrimitive);
42407
42592
  if (Array.isArray(schema["enum"])) return schema["enum"].every((value) => value === null || [
@@ -42425,8 +42610,8 @@ var init_catalog = __esmMin((() => {
42425
42610
  };
42426
42611
  isParameterLocation = (value) => value === "path" || value === "query" || value === "header";
42427
42612
  normalizeParameter = (raw, components) => {
42428
- const doc = isRecord$3(raw) && typeof raw["$ref"] === "string" ? resolveLocalRef(raw["$ref"], components)?.value : raw;
42429
- if (!isRecord$3(doc)) return void 0;
42613
+ const doc = isRecord$4(raw) && typeof raw["$ref"] === "string" ? resolveLocalRef(raw["$ref"], components)?.value : raw;
42614
+ if (!isRecord$4(doc)) return void 0;
42430
42615
  const parameter = doc;
42431
42616
  if (typeof parameter.name !== "string" || !isParameterLocation(parameter.in)) return void 0;
42432
42617
  if (parameter.content !== void 0 || parameter.allowReserved === true) return void 0;
@@ -42466,7 +42651,7 @@ var init_catalog = __esmMin((() => {
42466
42651
  requestBody = {
42467
42652
  required: doc.requestBody.required === true,
42468
42653
  contentType: JSON_MEDIA_TYPE,
42469
- schema: isRecord$3(schema) ? schema : {}
42654
+ schema: isRecord$4(schema) ? schema : {}
42470
42655
  };
42471
42656
  }
42472
42657
  const responses = {};
@@ -42487,7 +42672,7 @@ var init_catalog = __esmMin((() => {
42487
42672
  responses[status] = {
42488
42673
  description,
42489
42674
  contentType: JSON_MEDIA_TYPE,
42490
- ...isRecord$3(schema) ? { schema } : {}
42675
+ ...isRecord$4(schema) ? { schema } : {}
42491
42676
  };
42492
42677
  }
42493
42678
  if (successes === 0) return out("response_media_type");
@@ -42546,16 +42731,16 @@ var init_catalog = __esmMin((() => {
42546
42731
  contentTypes: operation.contentTypes
42547
42732
  });
42548
42733
  buildOperationCatalog = (document, gates, generation = 0) => {
42549
- const components = Object.fromEntries(Object.entries(document.components ?? {}).filter((entry) => isRecord$3(entry[1])));
42734
+ const components = Object.fromEntries(Object.entries(document.components ?? {}).filter((entry) => isRecord$4(entry[1])));
42550
42735
  const documentSecurity = document.security;
42551
42736
  const seen = /* @__PURE__ */ new Map();
42552
42737
  const eligible = [];
42553
42738
  const excluded = [];
42554
42739
  for (const [path, item] of Object.entries(document.paths ?? {})) {
42555
- if (!isRecord$3(item)) continue;
42740
+ if (!isRecord$4(item)) continue;
42556
42741
  for (const method of HTTP_METHODS$1) {
42557
42742
  const doc = item[method];
42558
- if (!isRecord$3(doc)) continue;
42743
+ if (!isRecord$4(doc)) continue;
42559
42744
  const operation = doc;
42560
42745
  const METHOD = method.toUpperCase();
42561
42746
  const operationId = operation.operationId;
@@ -43879,6 +44064,20 @@ var init_target_input_schema = __esmMin((() => {
43879
44064
  };
43880
44065
  }));
43881
44066
  //#endregion
44067
+ //#region ../../libs/runtime/workspace-runtime/src/lib/connect-inbound/connected-account.ts
44068
+ var isPlainObject$1, enrichWithAccount;
44069
+ var init_connected_account = __esmMin((() => {
44070
+ isPlainObject$1 = (value) => {
44071
+ if (typeof value !== "object" || value === null) return false;
44072
+ const proto = Object.getPrototypeOf(value);
44073
+ return proto === Object.prototype || proto === null;
44074
+ };
44075
+ enrichWithAccount = (payload, account) => account !== null && isPlainObject$1(payload) ? {
44076
+ ...payload,
44077
+ connectedAccount: account
44078
+ } : payload;
44079
+ }));
44080
+ //#endregion
43882
44081
  //#region ../../libs/runtime/workspace-runtime/src/lib/workflow/agent-echo-paths.ts
43883
44082
  var AGENT_ECHO_PATHS, defaultAgentEchoPath, AGENT_ECHO_EVENT_FIELDS, eventField, comparable, agentEchoCandidate, lookupAgentEcho, agentEchoRejectionMessage, agentEchoLookup;
43884
44083
  var init_agent_echo_paths = __esmMin((() => {
@@ -43942,6 +44141,7 @@ var init_event_forwarder = __esmMin((() => {
43942
44141
  init_src$13();
43943
44142
  init_src$7();
43944
44143
  init_target_input_schema();
44144
+ init_connected_account();
43945
44145
  init_agent_echo_paths();
43946
44146
  AGENT_ECHO_LOOKUP_TIMEOUT_MS = 1e4;
43947
44147
  issue = (message) => [{
@@ -44001,6 +44201,14 @@ var init_event_forwarder = __esmMin((() => {
44001
44201
  * path.
44002
44202
  */
44003
44203
  if (claim.outcome === "taken_over" && claim.takeoverCount > 3) return reject("claim_abandoned", `delivery abandoned after ${claim.takeoverCount} claim takeovers (cap 3): every attempt died before it marked the delivery, which is what an item whose forward kills the process looks like. Nothing ran and the link's cursor now advances past it, so the items behind it are delivered. Inspect the payload in trigger_deliveries and re-deliver it by hand once the cause is fixed.`);
44204
+ const readBeforeAttempt = async (what, read) => {
44205
+ try {
44206
+ return await withTimeout(read(), AGENT_ECHO_LOOKUP_TIMEOUT_MS, () => /* @__PURE__ */ new Error(`${what} exceeded ${AGENT_ECHO_LOOKUP_TIMEOUT_MS}ms`));
44207
+ } catch (err) {
44208
+ await bestEffort(() => deps.deliveries.release(subscription.id, event.id), "release");
44209
+ throw err;
44210
+ }
44211
+ };
44004
44212
  /**
44005
44213
  * The agent's own write coming back (STA3-32), asked before anything is attempted
44006
44214
  * so it neither starts the agent nor wakes a parked run. A hit is terminal like
@@ -44019,13 +44227,8 @@ var init_event_forwarder = __esmMin((() => {
44019
44227
  * An event with no id is never asked about, as in S1, so a keyless event is
44020
44228
  * forwarded exactly as it was before this guard.
44021
44229
  */
44022
- let ownEcho = null;
44023
- try {
44024
- if (event.id !== "" && deps.isAgentEcho !== void 0) ownEcho = await withTimeout(deps.isAgentEcho(subscription, event.payload), AGENT_ECHO_LOOKUP_TIMEOUT_MS, () => /* @__PURE__ */ new Error(`agent echo lookup exceeded ${AGENT_ECHO_LOOKUP_TIMEOUT_MS}ms`));
44025
- } catch (err) {
44026
- await bestEffort(() => deps.deliveries.release(subscription.id, event.id), "release");
44027
- throw err;
44028
- }
44230
+ const isAgentEcho = deps.isAgentEcho;
44231
+ const ownEcho = event.id !== "" && isAgentEcho !== void 0 ? await readBeforeAttempt("agent echo lookup", () => isAgentEcho(subscription, event.payload)) : null;
44029
44232
  if (ownEcho !== null) return reject("own_echo", agentEchoRejectionMessage(subscription.connector, ownEcho));
44030
44233
  const start = subscription.targetKind === "workflow" ? deps.start : subscription.targetKind === "agent" ? deps.startAgent : void 0;
44031
44234
  if (!start) return reject("not_found", `This box cannot start a "${subscription.targetKind}" from a trigger link.`);
@@ -44072,13 +44275,26 @@ var init_event_forwarder = __esmMin((() => {
44072
44275
  */
44073
44276
  const lostTheFence = () => ({ status: "claim_pending" });
44074
44277
  const rejectMapping = (message) => reject("mapping_error", message);
44075
- const applied = await mapLinkEvent(subscription, event.payload, deps.limits);
44278
+ /**
44279
+ * The account the link runs as (STA3-123, criterion 9), added before the mapping
44280
+ * and both keys read the event, so a key can name the mailbox too. The echo check
44281
+ * above reads the event as the provider sent it, and so does the sample the tick
44282
+ * keeps.
44283
+ *
44284
+ * A read that fails takes the echo lookup's path above, bound included, and for a
44285
+ * reason of its own: forwarded without its account, the event of a link whose
44286
+ * mapping reads `connectedAccount` fails the workflow's schema, and that
44287
+ * rejection is terminal, so one database blip would drop the mail for good.
44288
+ */
44289
+ const connectedAccountOf = deps.connectedAccountOf;
44290
+ const payload = connectedAccountOf ? enrichWithAccount(event.payload, await readBeforeAttempt("connected account lookup", () => connectedAccountOf(subscription))) : event.payload;
44291
+ const applied = await mapLinkEvent(subscription, payload, deps.limits);
44076
44292
  if (!applied.ok) return rejectMapping(applied.error.message);
44077
44293
  const correlationKey = subscription.targetKind === "workflow" ? subscription.correlationKey?.trim() : void 0;
44078
44294
  if (correlationKey && deps.resume) {
44079
44295
  const compiledKey = compileMapping(correlationKey);
44080
44296
  if (!compiledKey.ok) return rejectMapping(compiledKey.error.message);
44081
- const keyResult = await applyMapping(compiledKey.value, event.payload, deps.limits);
44297
+ const keyResult = await applyMapping(compiledKey.value, payload, deps.limits);
44082
44298
  if (!keyResult.ok) return rejectMapping(keyResult.error.message);
44083
44299
  const token = coerceToken(keyResult.value);
44084
44300
  if (token) {
@@ -44098,7 +44314,7 @@ var init_event_forwarder = __esmMin((() => {
44098
44314
  }
44099
44315
  const conversationKey = subscription.targetKind === "agent" ? subscription.conversationKey?.trim() : void 0;
44100
44316
  let startOptions = conversationKey ? {
44101
- conversationKeyValue: await evaluateConversationKey(conversationKey, subscription.id, event.payload, deps.limits),
44317
+ conversationKeyValue: await evaluateConversationKey(conversationKey, subscription.id, payload, deps.limits),
44102
44318
  ...typeof subscription.debounceMs === "number" && { debounceMs: subscription.debounceMs }
44103
44319
  } : void 0;
44104
44320
  if (subscription.targetKind === "agent" && event.id !== "" && deps.isAgentEcho) {
@@ -50143,6 +50359,12 @@ var createAppHooks = (deps) => {
50143
50359
  captureConnectConnectorRows: (fn) => {
50144
50360
  captured.connectConnectorRows = fn;
50145
50361
  },
50362
+ captureConnectResolveAccountKey: (fn) => {
50363
+ captured.connectResolveAccountKey = fn;
50364
+ },
50365
+ captureConnectConnectedAccountOf: (fn) => {
50366
+ captured.connectConnectedAccountOf = fn;
50367
+ },
50146
50368
  captureConnectSpecCache: (cache) => {
50147
50369
  captured.connectSpecCache = cache;
50148
50370
  },
@@ -52557,6 +52779,203 @@ var init_get_token = __esmMin((() => {
52557
52779
  };
52558
52780
  }));
52559
52781
  //#endregion
52782
+ //#region ../../libs/runtime/connect-broker/src/lib/oauth/default-oauth-exchanger.ts
52783
+ /**
52784
+ * Call a provider endpoint and read its answer as JSON. A non-2xx status or a
52785
+ * body that is not JSON throws what `fail` builds from the detail.
52786
+ * The token endpoint and the userinfo lookup (`account-identity.ts`) both read
52787
+ * a provider's answer through here.
52788
+ */
52789
+ async function fetchProviderJson(fetchImpl, url, init, fail) {
52790
+ const response = await fetchImpl(url, init);
52791
+ const raw = await response.text();
52792
+ if (!response.ok) throw fail(`returned ${response.status}: ${raw.slice(0, 500)}`);
52793
+ try {
52794
+ return JSON.parse(raw);
52795
+ } catch {
52796
+ throw fail("returned a non-JSON body");
52797
+ }
52798
+ }
52799
+ var tokenEndpointResponseSchema, formEncoded, DefaultOAuthExchanger;
52800
+ var init_default_oauth_exchanger = __esmMin((() => {
52801
+ init_src$27();
52802
+ tokenEndpointResponseSchema = z.object({
52803
+ access_token: z.string(),
52804
+ refresh_token: z.string().optional(),
52805
+ expires_in: z.number().optional(),
52806
+ id_token: z.string().optional()
52807
+ });
52808
+ formEncoded = (value) => new URLSearchParams([["", value]]).toString().slice(1);
52809
+ DefaultOAuthExchanger = class {
52810
+ constructor(deps) {
52811
+ this.fetchImpl = deps?.fetch ?? globalThis.fetch;
52812
+ this.now = deps?.now ?? Date.now;
52813
+ this.tokenEndpointAuthMethodOf = deps?.tokenEndpointAuthMethodOf ?? (() => void 0);
52814
+ }
52815
+ async exchangeCode(input) {
52816
+ const client = this.clientAuthentication(input.connector);
52817
+ const { params } = client;
52818
+ params.set("grant_type", "authorization_code");
52819
+ params.set("code", input.code);
52820
+ params.set("redirect_uri", input.redirectUri);
52821
+ if (input.codeVerifier) params.set("code_verifier", input.codeVerifier);
52822
+ return this.post(input.connector.tokenUrl, client, "invalid_grant");
52823
+ }
52824
+ async refresh(input) {
52825
+ const client = this.clientAuthentication(input.connector);
52826
+ client.params.set("grant_type", "refresh_token");
52827
+ client.params.set("refresh_token", input.refreshToken);
52828
+ return this.post(input.connector.tokenUrl, client, "credential_error");
52829
+ }
52830
+ async exchangeClientCredentials(input) {
52831
+ const client = this.clientAuthentication(input.connector);
52832
+ const { params } = client;
52833
+ params.set("grant_type", "client_credentials");
52834
+ const scope = input.scope ?? input.connector.scopes.join(" ");
52835
+ if (scope) params.set("scope", scope);
52836
+ const tokens = await this.post(input.connector.tokenUrl, client, "credential_error");
52837
+ delete tokens.refreshToken;
52838
+ return tokens;
52839
+ }
52840
+ /**
52841
+ * The client's half of a token request. In the body, `client_id` and an
52842
+ * optional `client_secret`. In a Basic header, each form-encoded and then
52843
+ * joined with a colon (RFC 6749 §2.3.1), so a colon in the id cannot be read
52844
+ * as the separator; a public client sends an empty password. Never both.
52845
+ */
52846
+ clientAuthentication(connector) {
52847
+ const params = new URLSearchParams();
52848
+ if (this.tokenEndpointAuthMethodOf(connector) === "client_secret_basic") {
52849
+ const credentials = `${formEncoded(connector.clientId)}:${formEncoded(connector.clientSecret ?? "")}`;
52850
+ return {
52851
+ params,
52852
+ headers: { authorization: `Basic ${Buffer.from(credentials, "utf8").toString("base64")}` }
52853
+ };
52854
+ }
52855
+ params.set("client_id", connector.clientId);
52856
+ if (connector.clientSecret) params.set("client_secret", connector.clientSecret);
52857
+ return {
52858
+ params,
52859
+ headers: {}
52860
+ };
52861
+ }
52862
+ /**
52863
+ * POST the form to the token endpoint and parse the response into
52864
+ * `ExchangedTokens`. A non-2xx response or an unparseable body becomes a
52865
+ * `BrokerError` with the supplied failure code (`invalid_grant` for a code
52866
+ * exchange, `credential_error` for a refresh).
52867
+ */
52868
+ async post(tokenUrl, { params, headers }, failureCode) {
52869
+ if (!tokenUrl) throw makeBrokerError("credential_error", "connector has no tokenUrl configured for the OAuth token exchange");
52870
+ const body = await fetchProviderJson(this.fetchImpl, tokenUrl, {
52871
+ method: "POST",
52872
+ headers: {
52873
+ "content-type": "application/x-www-form-urlencoded",
52874
+ accept: "application/json",
52875
+ ...headers
52876
+ },
52877
+ body: params.toString()
52878
+ }, (detail) => makeBrokerError(failureCode, `token endpoint ${detail}`));
52879
+ const parsed = tokenEndpointResponseSchema.safeParse(body);
52880
+ if (!parsed.success) throw makeBrokerError(failureCode, `token endpoint response missing access_token: ${parsed.error.message}`);
52881
+ const result = { accessToken: parsed.data.access_token };
52882
+ if (parsed.data.refresh_token) result.refreshToken = parsed.data.refresh_token;
52883
+ if (typeof parsed.data.expires_in === "number") result.expiresAt = this.now() + parsed.data.expires_in * 1e3;
52884
+ if (parsed.data.id_token) result.idToken = parsed.data.id_token;
52885
+ return result;
52886
+ }
52887
+ };
52888
+ }));
52889
+ //#endregion
52890
+ //#region ../../libs/runtime/connect-broker/src/lib/oauth/account-identity.ts
52891
+ /**
52892
+ * The email of the Google account behind `tokens`: the `id_token`'s `email`, else
52893
+ * Google's userinfo. Null, with no fetch, for a connector whose token endpoint is
52894
+ * not Google's. Throws `credential_error` when Google's userinfo fails, times
52895
+ * out or names no email.
52896
+ */
52897
+ async function resolveAccountIdentity(input) {
52898
+ if (hostOf(input.connector.tokenUrl) !== GOOGLE_TOKEN_HOST) return null;
52899
+ const claims = input.tokens.idToken ? idTokenClaims(input.tokens.idToken) : null;
52900
+ const claimed = claims ? emailOf(claims) : null;
52901
+ if (claimed) return { email: claimed };
52902
+ const fetchImpl = input.fetch ?? globalThis.fetch;
52903
+ const headers = {
52904
+ authorization: `Bearer ${input.tokens.accessToken}`,
52905
+ accept: "application/json"
52906
+ };
52907
+ let answer;
52908
+ try {
52909
+ answer = await fetchWithTimeout(({ signal }) => fetchProviderJson(fetchImpl, GOOGLE_USERINFO_URL, {
52910
+ method: "GET",
52911
+ headers,
52912
+ signal
52913
+ }, userinfoFailure), input.timeoutMs ?? 1e4, (ms) => /* @__PURE__ */ new Error(`timed out after ${ms}ms`));
52914
+ } catch (err) {
52915
+ throw isBrokerError(err) ? err : userinfoFailure(`did not answer: ${err instanceof Error ? err.message : String(err)}`);
52916
+ }
52917
+ const email = isRecord$3(answer) ? emailOf(answer) : null;
52918
+ if (!email) throw userinfoFailure("answered with no email");
52919
+ return { email };
52920
+ }
52921
+ function hostOf(url) {
52922
+ if (!url) return "";
52923
+ try {
52924
+ return new URL(url).hostname;
52925
+ } catch {
52926
+ return "";
52927
+ }
52928
+ }
52929
+ /** The `email` of an id_token's claims or of Google's userinfo, when it is one. */
52930
+ function emailOf(record) {
52931
+ const value = record["email"];
52932
+ const candidate = typeof value === "string" ? value.trim().toLowerCase() : "";
52933
+ return EMAIL.test(candidate) ? candidate : null;
52934
+ }
52935
+ /**
52936
+ * The claims of a JWS-compact `id_token`, or null when it is not one. The
52937
+ * signature is not checked: the token came straight from the token endpoint over
52938
+ * TLS (OpenID Connect Core 3.1.3.7), and a token that does not decode only means
52939
+ * the email is looked up at Google's userinfo.
52940
+ */
52941
+ function idTokenClaims(idToken) {
52942
+ const segments = idToken.split(".");
52943
+ const payload = segments[1];
52944
+ if (segments.length !== 3 || !payload || !BASE64URL.test(payload)) return null;
52945
+ try {
52946
+ const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
52947
+ return isRecord$3(claims) ? claims : null;
52948
+ } catch {
52949
+ return null;
52950
+ }
52951
+ }
52952
+ var GOOGLE_TOKEN_HOST, GOOGLE_USERINFO_URL, userinfoFailure, EMAIL, isRecord$3, BASE64URL;
52953
+ var init_account_identity = __esmMin((() => {
52954
+ init_src$27();
52955
+ init_src$13();
52956
+ init_default_oauth_exchanger();
52957
+ GOOGLE_TOKEN_HOST = "oauth2.googleapis.com";
52958
+ GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
52959
+ userinfoFailure = (detail) => makeBrokerError("credential_error", `userinfo endpoint ${detail}`);
52960
+ EMAIL = /^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/;
52961
+ isRecord$3 = (value) => typeof value === "object" && value !== null;
52962
+ BASE64URL = /^[A-Za-z0-9_-]+$/;
52963
+ }));
52964
+ //#endregion
52965
+ //#region ../../libs/runtime/connect-broker/src/lib/refresh/logger.ts
52966
+ var consoleRefreshLogger;
52967
+ var init_logger = __esmMin((() => {
52968
+ consoleRefreshLogger = {
52969
+ error: (message, context) => console.error("ERROR", message, context ? JSON.stringify(context) : ""),
52970
+ warn: (message, context) => console.warn("WARN", message, context ? JSON.stringify(context) : ""),
52971
+ info: (message, context) => console.info("INFO", message, context ? JSON.stringify(context) : ""),
52972
+ debug: (message, context) => {
52973
+ if (process.env["STACKBONE_DEBUG"] !== "1") return;
52974
+ console.debug("DEBUG", message, context ? JSON.stringify(context) : "");
52975
+ }
52976
+ };
52977
+ }));
52978
+ //#endregion
52560
52979
  //#region ../../libs/runtime/connect-broker/src/lib/oauth/state.ts
52561
52980
  function base64UrlEncode(bytes) {
52562
52981
  return bytes.toString("base64url");
@@ -52633,16 +53052,25 @@ var init_state = __esmMin((() => {
52633
53052
  }));
52634
53053
  //#endregion
52635
53054
  //#region ../../libs/runtime/connect-broker/src/lib/oauth/oauth-flow.ts
52636
- var DEFAULT_BINDING_TTL_MS, mintCodeVerifier, codeChallengeOf, OAuthFlow;
53055
+ var APP_PRINCIPAL$1, LEGACY_KEY, failureOf$2, DEFAULT_BINDING_TTL_MS, mintCodeVerifier, codeChallengeOf, OAuthFlow;
52637
53056
  var init_oauth_flow = __esmMin((() => {
52638
53057
  init_src$27();
53058
+ init_account_identity();
53059
+ init_logger();
52639
53060
  init_state();
53061
+ APP_PRINCIPAL$1 = { type: "app" };
53062
+ LEGACY_KEY = principalKey(APP_PRINCIPAL$1);
53063
+ failureOf$2 = (err) => err instanceof Error ? {
53064
+ error: err.message,
53065
+ stack: err.stack
53066
+ } : { error: String(err) };
52640
53067
  DEFAULT_BINDING_TTL_MS = 600 * 1e3;
52641
53068
  mintCodeVerifier = () => randomBytes(32).toString("base64url");
52642
53069
  codeChallengeOf = (verifier, method) => method === "S256" ? createHash("sha256").update(verifier).digest("base64url") : verifier;
52643
53070
  OAuthFlow = class {
52644
53071
  constructor(deps) {
52645
53072
  this.deps = deps;
53073
+ this.logger = deps.logger ?? consoleRefreshLogger;
52646
53074
  }
52647
53075
  now() {
52648
53076
  return this.deps.now ? this.deps.now() : Date.now();
@@ -52735,23 +53163,142 @@ var init_oauth_flow = __esmMin((() => {
52735
53163
  redirectUri: this.deps.brokerCallbackUrl,
52736
53164
  ...binding.codeVerifier !== void 0 ? { codeVerifier: binding.codeVerifier } : {}
52737
53165
  });
53166
+ const account = await this.resolveAccount({
53167
+ connector,
53168
+ tokens
53169
+ });
53170
+ const key = account ? principalKey({
53171
+ type: "user",
53172
+ issuer: connector.connectorId,
53173
+ id: account.email
53174
+ }) : state.principalKey;
52738
53175
  const grant = {
52739
53176
  orgId: connector.orgId,
52740
53177
  connector: connector.connectorId,
52741
- principalKey: state.principalKey,
53178
+ principalKey: key,
52742
53179
  accessToken: tokens.accessToken,
52743
53180
  refreshToken: tokens.refreshToken ?? null,
52744
53181
  expiresAt: tokens.expiresAt ?? null,
52745
- externalSubject: null
53182
+ externalSubject: account?.email ?? null
52746
53183
  };
52747
- await this.deps.store.upsertGrant(grant);
53184
+ const fold = account === null ? "none" : await this.foldLegacyGrant(connector, grant);
53185
+ if (fold === "none" && account !== null) await this.upsertAdoptingLegacyLinks(grant);
53186
+ else if (fold !== "folded") await this.deps.store.upsertGrant(grant);
53187
+ const legacyLinksLeft = fold === "kept" ? await this.countLegacyLinks(connector) : void 0;
52748
53188
  return {
52749
53189
  redirectTo: this.resolveConsumerRedirect(binding, state),
52750
53190
  granted: {
52751
53191
  connector: connector.connectorId,
52752
- principalKey: state.principalKey
52753
- }
53192
+ principalKey: key
53193
+ },
53194
+ ...legacyLinksLeft ? { legacyLinksLeft } : {}
53195
+ };
53196
+ }
53197
+ resolveAccount(input) {
53198
+ return (this.deps.resolveAccountIdentity ?? resolveAccountIdentity)(input);
53199
+ }
53200
+ /**
53201
+ * D12: a reconnect of the mailbox the connector's legacy `app` grant belongs to
53202
+ * folds the two grants into one. `kept`, with nothing of the legacy grant
53203
+ * written, for another mailbox, no identity, any failure on the way, or a legacy
53204
+ * grant that changed while its identity was read: the caller keeps both, and the
53205
+ * log says why (D15). `none`, quietly, when there is no legacy grant (every first
53206
+ * connect) or this flow cannot fold.
53207
+ */
53208
+ async foldLegacyGrant(connector, account) {
53209
+ const { store, issueGrantToken, moveAccountLinks } = this.deps;
53210
+ if (!store.foldLegacyGrant || !issueGrantToken || !moveAccountLinks) return "none";
53211
+ const { orgId, connectorId } = connector;
53212
+ let legacyGrantId = null;
53213
+ const keepBoth = (reason, detail = {}) => {
53214
+ this.logger.warn("reconnect kept the legacy app grant beside the account", {
53215
+ orgId,
53216
+ connector: connectorId,
53217
+ legacyGrantId,
53218
+ reason,
53219
+ ...detail
53220
+ });
53221
+ return "kept";
52754
53222
  };
53223
+ try {
53224
+ legacyGrantId = (await store.getGrantTokenRevision(orgId, connectorId, LEGACY_KEY))?.id ?? null;
53225
+ if (legacyGrantId === null) return "none";
53226
+ const accessToken = await issueGrantToken({
53227
+ orgId,
53228
+ principal: APP_PRINCIPAL$1
53229
+ }, connector);
53230
+ const revision = await store.getGrantTokenRevision(orgId, connectorId, LEGACY_KEY);
53231
+ if (revision === null || revision.id !== legacyGrantId) return keepBoth("legacy_changed");
53232
+ const legacy = await this.resolveAccount({
53233
+ connector,
53234
+ tokens: { accessToken }
53235
+ });
53236
+ if (legacy === null) return keepBoth("no_identity");
53237
+ if (legacy.email !== account.externalSubject) return keepBoth("other_mailbox");
53238
+ if (await store.foldLegacyGrant({
53239
+ legacy: {
53240
+ ...revision,
53241
+ principalKey: LEGACY_KEY
53242
+ },
53243
+ account
53244
+ }, moveAccountLinks) === null) return keepBoth("legacy_changed");
53245
+ return "folded";
53246
+ } catch (err) {
53247
+ if (isBrokerError(err)) return keepBoth("broker_error", {
53248
+ code: err.code,
53249
+ error: err.message
53250
+ });
53251
+ this.logger.error("reconnect could not fold the legacy app grant and kept both", {
53252
+ orgId,
53253
+ connector: connectorId,
53254
+ legacyGrantId,
53255
+ reason: "unexpected_error",
53256
+ ...failureOf$2(err)
53257
+ });
53258
+ return "kept";
53259
+ }
53260
+ }
53261
+ /**
53262
+ * D16: an account connected on a connector with no grant takes the links a
53263
+ * revoked grant left under `app`, as the grant that used to land back on `app`
53264
+ * did. With any grant on the connector, only the account's own is written; the
53265
+ * store decides inside the write, so two first connects never both adopt.
53266
+ */
53267
+ async upsertAdoptingLegacyLinks(account) {
53268
+ const { store, moveAccountLinks } = this.deps;
53269
+ if (!store.upsertGrantAdoptingLinks || !moveAccountLinks) {
53270
+ await store.upsertGrant(account);
53271
+ return;
53272
+ }
53273
+ const moved = await store.upsertGrantAdoptingLinks({
53274
+ account,
53275
+ from: LEGACY_KEY
53276
+ }, moveAccountLinks);
53277
+ if (moved && moved.triggers + moved.outputs > 0) this.logger.info("connect adopted the links a revoked grant left under app", {
53278
+ orgId: account.orgId,
53279
+ connector: account.connector,
53280
+ ...moved
53281
+ });
53282
+ }
53283
+ /**
53284
+ * The links still under the legacy `app` grant after a reconnect kept it (D15).
53285
+ * A count that fails is logged and left out: the grant is already stored, and
53286
+ * failing the callback now would spend the single-use `state` for a report.
53287
+ */
53288
+ async countLegacyLinks(connector) {
53289
+ try {
53290
+ return await this.deps.countAccountLinks?.({
53291
+ connector: connector.connectorId,
53292
+ principalKey: LEGACY_KEY
53293
+ });
53294
+ } catch (err) {
53295
+ this.logger.error("reconnect could not count the links left on the legacy app grant", {
53296
+ orgId: connector.orgId,
53297
+ connector: connector.connectorId,
53298
+ ...failureOf$2(err)
53299
+ });
53300
+ return;
53301
+ }
52755
53302
  }
52756
53303
  /**
52757
53304
  * Resolve the CONSUMER redirect target the broker 302s the browser to after a
@@ -52810,7 +53357,12 @@ var init_connect_service = __esmMin((() => {
52810
53357
  brokerCallbackUrl: deps.oauth.brokerCallbackUrl,
52811
53358
  stateSecret: deps.oauth.stateSecret,
52812
53359
  ...deps.oauth.bindingTtlMs !== void 0 ? { bindingTtlMs: deps.oauth.bindingTtlMs } : {},
52813
- now: this.now
53360
+ now: this.now,
53361
+ ...deps.resolveAccountIdentity !== void 0 ? { resolveAccountIdentity: deps.resolveAccountIdentity } : {},
53362
+ issueGrantToken: async (caller, connector) => (await this.buildGetTokenService(connector).getToken(caller, connector.connectorId)).token,
53363
+ ...deps.moveAccountLinks !== void 0 ? { moveAccountLinks: deps.moveAccountLinks } : {},
53364
+ ...deps.countAccountLinks !== void 0 ? { countAccountLinks: deps.countAccountLinks } : {},
53365
+ ...deps.logger !== void 0 ? { logger: deps.logger } : {}
52814
53366
  });
52815
53367
  }
52816
53368
  /**
@@ -53330,11 +53882,11 @@ var init_drizzle_broker_store = __esmMin((() => {
53330
53882
  if (!row) return null;
53331
53883
  return toGrantSummary(row);
53332
53884
  }
53333
- async upsertGrant(record) {
53885
+ async upsertGrant(record, tx) {
53334
53886
  const access = this.encrypt(record.accessToken);
53335
53887
  const refresh = this.encryptNullable(record.refreshToken);
53336
53888
  const expiresAt = record.expiresAt === null ? null : new Date(record.expiresAt);
53337
- const [row] = await this.db.insert(connectionGrantTable).values({
53889
+ const [row] = await (tx ?? this.db).insert(connectionGrantTable).values({
53338
53890
  organizationId: record.orgId,
53339
53891
  connector: record.connector,
53340
53892
  principalKey: record.principalKey,
@@ -53383,6 +53935,66 @@ var init_drizzle_broker_store = __esmMin((() => {
53383
53935
  }).where(and(eq(connectionGrantTable.id, expected.id), eq(connectionGrantTable.organizationId, expected.orgId), eq(connectionGrantTable.connector, expected.connector), eq(connectionGrantTable.principalKey, expected.principalKey), eq(connectionGrantTable.accessTokenNonce, Buffer.from(expected.tokenRevision, "hex")), isNull(connectionGrantTable.materialCiphertext))).returning();
53384
53936
  return row ? this.decryptGrant(row) : null;
53385
53937
  }
53938
+ async foldLegacyGrant(fold, moveLinks) {
53939
+ const { legacy, account } = fold;
53940
+ const access = this.encrypt(account.accessToken);
53941
+ const refresh = this.encryptNullable(account.refreshToken);
53942
+ const credentials = {
53943
+ accessTokenNonce: access.nonce,
53944
+ accessTokenCiphertext: access.ciphertext,
53945
+ refreshTokenNonce: refresh?.nonce ?? null,
53946
+ refreshTokenCiphertext: refresh?.ciphertext ?? null,
53947
+ materialNonce: null,
53948
+ materialCiphertext: null,
53949
+ expiresAt: account.expiresAt === null ? null : new Date(account.expiresAt),
53950
+ externalSubject: account.externalSubject,
53951
+ healthStatus: "active",
53952
+ healthDetail: null,
53953
+ updatedAt: /* @__PURE__ */ new Date()
53954
+ };
53955
+ const ofConnector = and(eq(connectionGrantTable.organizationId, account.orgId), eq(connectionGrantTable.connector, account.connector));
53956
+ const legacyAsRead = and(ofConnector, eq(connectionGrantTable.id, legacy.id), eq(connectionGrantTable.principalKey, legacy.principalKey), eq(connectionGrantTable.accessTokenNonce, Buffer.from(legacy.tokenRevision, "hex")), isNull(connectionGrantTable.materialCiphertext));
53957
+ return this.db.transaction(async (tx) => {
53958
+ await tx.select({ id: connectorTable.id }).from(connectorTable).where(and(eq(connectorTable.organizationId, account.orgId), eq(connectorTable.connectorId, account.connector))).for("update");
53959
+ const [own] = await tx.select({ id: connectionGrantTable.id }).from(connectionGrantTable).where(and(ofConnector, eq(connectionGrantTable.principalKey, account.principalKey))).for("update");
53960
+ let row;
53961
+ if (own) {
53962
+ if ((await tx.delete(connectionGrantTable).where(legacyAsRead).returning({ id: connectionGrantTable.id })).length === 0) return null;
53963
+ [row] = await tx.update(connectionGrantTable).set(credentials).where(eq(connectionGrantTable.id, own.id)).returning();
53964
+ if (!row) throw makeBrokerError("execute_failed", "the account grant vanished mid-fold");
53965
+ } else {
53966
+ [row] = await tx.update(connectionGrantTable).set({
53967
+ principalKey: account.principalKey,
53968
+ ...credentials
53969
+ }).where(legacyAsRead).returning();
53970
+ if (!row) return null;
53971
+ }
53972
+ await moveLinks(tx, {
53973
+ connector: account.connector,
53974
+ from: legacy.principalKey,
53975
+ to: account.principalKey
53976
+ });
53977
+ return this.decryptGrant(row);
53978
+ });
53979
+ }
53980
+ async upsertGrantAdoptingLinks({ account, from }, moveLinks) {
53981
+ return this.db.transaction(async (tx) => {
53982
+ await tx.select({ id: connectorTable.id }).from(connectorTable).where(and(eq(connectorTable.organizationId, account.orgId), eq(connectorTable.connectorId, account.connector))).for("update");
53983
+ const [held] = await tx.select({ id: connectionGrantTable.id }).from(connectionGrantTable).where(and(eq(connectionGrantTable.organizationId, account.orgId), eq(connectionGrantTable.connector, account.connector))).limit(1);
53984
+ await this.upsertGrant(account, tx);
53985
+ if (held) return null;
53986
+ return moveLinks(tx, {
53987
+ connector: account.connector,
53988
+ from,
53989
+ to: account.principalKey
53990
+ });
53991
+ });
53992
+ }
53993
+ async awaitAccountMoves(orgId, connector) {
53994
+ await this.db.transaction(async (tx) => {
53995
+ await tx.select({ id: connectorTable.id }).from(connectorTable).where(and(eq(connectorTable.organizationId, orgId), eq(connectorTable.connectorId, connector))).for("share");
53996
+ });
53997
+ }
53386
53998
  async deleteGrant(orgId, connector, principalKey) {
53387
53999
  await this.db.delete(connectionGrantTable).where(and(eq(connectionGrantTable.organizationId, orgId), eq(connectionGrantTable.connector, connector), eq(connectionGrantTable.principalKey, principalKey)));
53388
54000
  }
@@ -53411,7 +54023,7 @@ var init_drizzle_broker_store = __esmMin((() => {
53411
54023
  healthStatus: input.status,
53412
54024
  healthDetail: input.detail,
53413
54025
  updatedAt: /* @__PURE__ */ new Date()
53414
- }).where(eq(connectionGrantTable.id, input.grantId));
54026
+ }).where(and(eq(connectionGrantTable.id, input.grantId), input.fence ? eq(connectionGrantTable.principalKey, input.fence.principalKey) : void 0, input.fence?.tokenRevision !== void 0 ? eq(connectionGrantTable.accessTokenNonce, Buffer.from(input.fence.tokenRevision, "hex")) : void 0));
53415
54027
  }
53416
54028
  encrypt(plaintext) {
53417
54029
  const envelope = this.cipher.encrypt(plaintext);
@@ -53604,104 +54216,6 @@ var init_purge_unreadable_envelopes = __esmMin((() => {
53604
54216
  };
53605
54217
  }));
53606
54218
  //#endregion
53607
- //#region ../../libs/runtime/connect-broker/src/lib/oauth/default-oauth-exchanger.ts
53608
- var tokenEndpointResponseSchema, formEncoded, DefaultOAuthExchanger;
53609
- var init_default_oauth_exchanger = __esmMin((() => {
53610
- init_src$27();
53611
- tokenEndpointResponseSchema = z.object({
53612
- access_token: z.string(),
53613
- refresh_token: z.string().optional(),
53614
- expires_in: z.number().optional()
53615
- });
53616
- formEncoded = (value) => new URLSearchParams([["", value]]).toString().slice(1);
53617
- DefaultOAuthExchanger = class {
53618
- constructor(deps) {
53619
- this.fetchImpl = deps?.fetch ?? globalThis.fetch;
53620
- this.now = deps?.now ?? Date.now;
53621
- this.tokenEndpointAuthMethodOf = deps?.tokenEndpointAuthMethodOf ?? (() => void 0);
53622
- }
53623
- async exchangeCode(input) {
53624
- const client = this.clientAuthentication(input.connector);
53625
- const { params } = client;
53626
- params.set("grant_type", "authorization_code");
53627
- params.set("code", input.code);
53628
- params.set("redirect_uri", input.redirectUri);
53629
- if (input.codeVerifier) params.set("code_verifier", input.codeVerifier);
53630
- return this.post(input.connector.tokenUrl, client, "invalid_grant");
53631
- }
53632
- async refresh(input) {
53633
- const client = this.clientAuthentication(input.connector);
53634
- client.params.set("grant_type", "refresh_token");
53635
- client.params.set("refresh_token", input.refreshToken);
53636
- return this.post(input.connector.tokenUrl, client, "credential_error");
53637
- }
53638
- async exchangeClientCredentials(input) {
53639
- const client = this.clientAuthentication(input.connector);
53640
- const { params } = client;
53641
- params.set("grant_type", "client_credentials");
53642
- const scope = input.scope ?? input.connector.scopes.join(" ");
53643
- if (scope) params.set("scope", scope);
53644
- const tokens = await this.post(input.connector.tokenUrl, client, "credential_error");
53645
- delete tokens.refreshToken;
53646
- return tokens;
53647
- }
53648
- /**
53649
- * The client's half of a token request. In the body, `client_id` and an
53650
- * optional `client_secret`. In a Basic header, each form-encoded and then
53651
- * joined with a colon (RFC 6749 §2.3.1), so a colon in the id cannot be read
53652
- * as the separator; a public client sends an empty password. Never both.
53653
- */
53654
- clientAuthentication(connector) {
53655
- const params = new URLSearchParams();
53656
- if (this.tokenEndpointAuthMethodOf(connector) === "client_secret_basic") {
53657
- const credentials = `${formEncoded(connector.clientId)}:${formEncoded(connector.clientSecret ?? "")}`;
53658
- return {
53659
- params,
53660
- headers: { authorization: `Basic ${Buffer.from(credentials, "utf8").toString("base64")}` }
53661
- };
53662
- }
53663
- params.set("client_id", connector.clientId);
53664
- if (connector.clientSecret) params.set("client_secret", connector.clientSecret);
53665
- return {
53666
- params,
53667
- headers: {}
53668
- };
53669
- }
53670
- /**
53671
- * POST the form to the token endpoint and parse the response into
53672
- * `ExchangedTokens`. A non-2xx response or an unparseable body becomes a
53673
- * `BrokerError` with the supplied failure code (`invalid_grant` for a code
53674
- * exchange, `credential_error` for a refresh).
53675
- */
53676
- async post(tokenUrl, { params, headers }, failureCode) {
53677
- if (!tokenUrl) throw makeBrokerError("credential_error", "connector has no tokenUrl configured for the OAuth token exchange");
53678
- const response = await this.fetchImpl(tokenUrl, {
53679
- method: "POST",
53680
- headers: {
53681
- "content-type": "application/x-www-form-urlencoded",
53682
- accept: "application/json",
53683
- ...headers
53684
- },
53685
- body: params.toString()
53686
- });
53687
- const raw = await response.text();
53688
- if (!response.ok) throw makeBrokerError(failureCode, `token endpoint returned ${response.status}: ${raw.slice(0, 500)}`);
53689
- let body;
53690
- try {
53691
- body = JSON.parse(raw);
53692
- } catch {
53693
- throw makeBrokerError(failureCode, "token endpoint returned a non-JSON body");
53694
- }
53695
- const parsed = tokenEndpointResponseSchema.safeParse(body);
53696
- if (!parsed.success) throw makeBrokerError(failureCode, `token endpoint response missing access_token: ${parsed.error.message}`);
53697
- const result = { accessToken: parsed.data.access_token };
53698
- if (parsed.data.refresh_token) result.refreshToken = parsed.data.refresh_token;
53699
- if (typeof parsed.data.expires_in === "number") result.expiresAt = this.now() + parsed.data.expires_in * 1e3;
53700
- return result;
53701
- }
53702
- };
53703
- }));
53704
- //#endregion
53705
54219
  //#region ../../libs/runtime/connect-broker/src/lib/cache/in-memory-token-cache.ts
53706
54220
  var createInMemoryTokenCache;
53707
54221
  var init_in_memory_token_cache = __esmMin((() => {
@@ -54291,6 +54805,7 @@ function grantToRefreshable(grant) {
54291
54805
  accessTokenExpiresAt: grant.expiresAt === null ? null : new Date(grant.expiresAt),
54292
54806
  connector: grant.connector,
54293
54807
  principalKey: grant.principalKey,
54808
+ ...grant.tokenRevision !== void 0 ? { tokenRevision: grant.tokenRevision } : {},
54294
54809
  hasRefreshToken: grant.refreshToken !== null
54295
54810
  };
54296
54811
  }
@@ -54329,10 +54844,7 @@ var init_grant_refresh_ports = __esmMin((() => {
54329
54844
  reason: "no refresh token"
54330
54845
  };
54331
54846
  const grant = await this.store.getGrant(grantRef.organizationId, grantRef.connector, grantRef.principalKey);
54332
- if (grant === null) return {
54333
- kind: "permanent",
54334
- reason: "grant not found"
54335
- };
54847
+ if (grant === null) return { kind: "superseded" };
54336
54848
  if (grant.refreshToken === null) return {
54337
54849
  kind: "permanent",
54338
54850
  reason: "no refresh token"
@@ -54376,10 +54888,15 @@ var init_grant_refresh_ports = __esmMin((() => {
54376
54888
  this.setter = setter;
54377
54889
  }
54378
54890
  async setHealth(input) {
54891
+ const grant = input.connection;
54379
54892
  await this.setter.setGrantHealth({
54380
54893
  grantId: input.connectionId,
54381
54894
  status: input.status,
54382
- detail: input.detail
54895
+ detail: input.detail,
54896
+ ...grant ? { fence: {
54897
+ principalKey: grant.principalKey,
54898
+ ...input.status !== "active" && grant.tokenRevision !== void 0 ? { tokenRevision: grant.tokenRevision } : {}
54899
+ } } : {}
54383
54900
  });
54384
54901
  }
54385
54902
  };
@@ -54388,20 +54905,6 @@ var init_grant_refresh_ports = __esmMin((() => {
54388
54905
  return { disabledAutomationIds: [] };
54389
54906
  }
54390
54907
  };
54391
- }));
54392
- //#endregion
54393
- //#region ../../libs/runtime/connect-broker/src/lib/refresh/logger.ts
54394
- var consoleRefreshLogger;
54395
- var init_logger = __esmMin((() => {
54396
- consoleRefreshLogger = {
54397
- error: (message, context) => console.error("ERROR", message, context ? JSON.stringify(context) : ""),
54398
- warn: (message, context) => console.warn("WARN", message, context ? JSON.stringify(context) : ""),
54399
- info: (message, context) => console.info("INFO", message, context ? JSON.stringify(context) : ""),
54400
- debug: (message, context) => {
54401
- if (process.env["STACKBONE_DEBUG"] !== "1") return;
54402
- console.debug("DEBUG", message, context ? JSON.stringify(context) : "");
54403
- }
54404
- };
54405
54908
  })), REFRESH_REVOKED_REASON, ConnectionRefreshProcessor;
54406
54909
  var init_connection_refresh_processor = __esmMin((() => {
54407
54910
  init_logger();
@@ -54447,6 +54950,7 @@ var init_connection_refresh_processor = __esmMin((() => {
54447
54950
  case "refreshed":
54448
54951
  if (connection.healthStatus !== "active") await this.deps.health.setHealth({
54449
54952
  connectionId: connection.id,
54953
+ connection,
54450
54954
  status: "active",
54451
54955
  detail: null
54452
54956
  });
@@ -54457,6 +54961,7 @@ var init_connection_refresh_processor = __esmMin((() => {
54457
54961
  case "transient":
54458
54962
  await this.deps.health.setHealth({
54459
54963
  connectionId: connection.id,
54964
+ connection,
54460
54965
  status: "error",
54461
54966
  detail: outcome.reason
54462
54967
  });
@@ -54468,6 +54973,7 @@ var init_connection_refresh_processor = __esmMin((() => {
54468
54973
  case "permanent": {
54469
54974
  await this.deps.health.setHealth({
54470
54975
  connectionId: connection.id,
54976
+ connection,
54471
54977
  status: "revoked",
54472
54978
  detail: outcome.reason
54473
54979
  });
@@ -65986,6 +66492,34 @@ var createCatalogPieceTriggerRunner = (catalog, files = {}) => createPieceTrigge
65986
66492
  ...files
65987
66493
  });
65988
66494
  //#endregion
66495
+ //#region ../../libs/runtime/workspace-runtime/src/lib/connect/move-account-links.ts
66496
+ var moveAccountLinks = async (tx, { connector, from, to }) => {
66497
+ const triggers = await tx.execute(sql`
66498
+ UPDATE stackbone_platform.trigger_subscriptions
66499
+ SET principal_key = ${to}, poll_revision = poll_revision + 1, updated_at = now()
66500
+ WHERE connector = ${connector} AND principal_key = ${from}`);
66501
+ const outputs = await tx.execute(sql`
66502
+ UPDATE stackbone_platform.workflow_file_output_bindings
66503
+ SET principal_key = ${to}, updated_at = now()
66504
+ WHERE connector = ${connector} AND principal_key = ${from}`);
66505
+ return {
66506
+ triggers: triggers.count,
66507
+ outputs: outputs.count
66508
+ };
66509
+ };
66510
+ var countAccountLinks = (db) => async ({ connector, principalKey }) => {
66511
+ const [row] = await db.execute(sql`
66512
+ SELECT
66513
+ (SELECT count(*)::int FROM stackbone_platform.trigger_subscriptions
66514
+ WHERE connector = ${connector} AND principal_key = ${principalKey}) AS triggers,
66515
+ (SELECT count(*)::int FROM stackbone_platform.workflow_file_output_bindings
66516
+ WHERE connector = ${connector} AND principal_key = ${principalKey}) AS outputs`);
66517
+ return {
66518
+ triggers: row?.triggers ?? 0,
66519
+ outputs: row?.outputs ?? 0
66520
+ };
66521
+ };
66522
+ //#endregion
65989
66523
  //#region ../../libs/runtime/workspace-runtime/src/lib/connect/piece/piece-oauth2-method.ts
65990
66524
  init_src$6();
65991
66525
  /**
@@ -66102,7 +66636,9 @@ var makeContextFactory = (opts) => {
66102
66636
  oauth: {
66103
66637
  brokerCallbackUrl: brokerCallbackBaseUrl,
66104
66638
  stateSecret: deriveBase64Key(opts.encryptionKey)
66105
- }
66639
+ },
66640
+ moveAccountLinks,
66641
+ countAccountLinks: countAccountLinks(db)
66106
66642
  });
66107
66643
  return {
66108
66644
  db,
@@ -73311,8 +73847,10 @@ var probeBrowserProvider = async (resolved, deps = {}) => {
73311
73847
  };
73312
73848
  /**
73313
73849
  * No HTTP here — a `local` launch is reachable exactly when a Chromium binary
73314
- * exists on the box. The workspace's browser peers are an optional install, so
73315
- * "no browser found" is the realistic failure and it names how to fix it.
73850
+ * exists on the machine. Playwright is not a required peer (STA3-125), so "no
73851
+ * browser found" is the realistic failure under `stackbone dev`, and it names how
73852
+ * to fix it. A deployed box never reaches this probe: `resolveProbeTarget` refuses
73853
+ * `local` there first.
73316
73854
  */
73317
73855
  var probeLocalBrowser = async (resolveLocalBrowser, fileExists = defaultFileExists) => {
73318
73856
  try {
@@ -73320,7 +73858,7 @@ var probeLocalBrowser = async (resolveLocalBrowser, fileExists = defaultFileExis
73320
73858
  if (executablePath && await fileExists(executablePath)) return { ok: true };
73321
73859
  return {
73322
73860
  ok: false,
73323
- error: "No local Chromium was found on this box. Install the browser peers (`playwright install chromium`) or pin a binary with STACKBONE_BROWSER_EXECUTABLE_PATH."
73861
+ error: LOCAL_CHROMIUM_MISSING_MESSAGE
73324
73862
  };
73325
73863
  } catch (err) {
73326
73864
  return {
@@ -73423,14 +73961,15 @@ var API_KEY_PRESENT_PLACEHOLDER = "__stored__";
73423
73961
  * Pure by design — it takes the stored values and the environment rather than
73424
73962
  * reading either, so every precedence case is unit-testable without a database.
73425
73963
  */
73426
- var buildBrowserProviderView = async (stored, env) => {
73964
+ var buildBrowserProviderView = async (stored, env, { allowLocalBrowser } = {}) => {
73427
73965
  const parsedMode = browserProviderModeSchema.safeParse(stored.mode);
73428
73966
  const resolution = await resolveBrowserMode({
73429
73967
  env,
73430
73968
  readSecrets: async () => ({
73431
73969
  data: toSecretRecord(stored),
73432
73970
  error: null
73433
- })
73971
+ }),
73972
+ allowLocalBrowser
73434
73973
  });
73435
73974
  return {
73436
73975
  mode: parsedMode.success ? parsedMode.data : null,
@@ -73468,7 +74007,7 @@ var readStored = async (opts) => {
73468
74007
  };
73469
74008
  };
73470
74009
  /** The current browser provider as the screen sees it. */
73471
- var getBrowserProvider = async (opts, env = process.env) => buildBrowserProviderView(await readStored(opts), env);
74010
+ var getBrowserProvider = async (opts, host = {}, env = process.env) => buildBrowserProviderView(await readStored(opts), env, host);
73472
74011
  /**
73473
74012
  * Which stored rows a save GOVERNS, per mode. A write replaces the fields of the
73474
74013
  * mode being saved and leaves the other modes' credentials alone.
@@ -73535,7 +74074,7 @@ var setBrowserProvider = async (opts, body, actor = { userId: null }) => {
73535
74074
  * so a body without one falls back to the STORED key — that is how "test the
73536
74075
  * configuration I saved last week" works without retyping the secret.
73537
74076
  */
73538
- var testBrowserProviderConnection = async (opts, body) => {
74077
+ var testBrowserProviderConnection = async (opts, body, host = {}) => {
73539
74078
  let storedApiKey = null;
73540
74079
  if (body.mode === "browserbase" && !normalize(body.api_key)) try {
73541
74080
  storedApiKey = await readSystemSecret(opts, BROWSERBASE_API_KEY_ENV);
@@ -73545,7 +74084,7 @@ var testBrowserProviderConnection = async (opts, body) => {
73545
74084
  error: `The stored ${BROWSERBASE_API_KEY_ENV} could not be read. Enter the key again to replace it.`
73546
74085
  };
73547
74086
  }
73548
- const target = resolveProbeTarget(body, storedApiKey);
74087
+ const target = resolveProbeTarget(body, storedApiKey, host);
73549
74088
  if (!target.ok) return {
73550
74089
  ok: false,
73551
74090
  error: target.reason
@@ -73558,11 +74097,11 @@ var testBrowserProviderConnection = async (opts, body) => {
73558
74097
  * did not type one (the read masks it, so the screen cannot send it back). An
73559
74098
  * incomplete candidate is refused here rather than probed and misreported.
73560
74099
  */
73561
- var resolveProbeTarget = (body, storedApiKey) => {
73562
- if (body.mode === "local") return {
74100
+ var resolveProbeTarget = (body, storedApiKey, { allowLocalBrowser = true } = {}) => {
74101
+ if (body.mode === "local") return allowLocalBrowser ? {
73563
74102
  ok: true,
73564
74103
  mode: "local"
73565
- };
74104
+ } : localBrowserNotAllowedFailure();
73566
74105
  if (body.mode === "cdp") {
73567
74106
  const cdpUrl = normalize(body.cdp_url);
73568
74107
  return cdpUrl ? {
@@ -73633,8 +74172,12 @@ var browserProviderRequestBody = named("BrowserProviderPutRequest", browserProvi
73633
74172
  * activity log, and it is the only route here that changes anything. Required
73634
74173
  * rather than optional, like the profiles router next door — a build of this
73635
74174
  * surface that silently recorded nothing is the gap this closes.
74175
+ *
74176
+ * `host` is required for the same reason (STA3-125): a deployed box has no local
74177
+ * Chromium, and a router that defaulted to allowing one would report and probe a
74178
+ * browser the box cannot open.
73636
74179
  */
73637
- var createBrowserProviderRouter = (secretsOptions) => {
74180
+ var createBrowserProviderRouter = (secretsOptions, host) => {
73638
74181
  const app = new OpenAPIHono({ defaultHook: invalidPayloadHook });
73639
74182
  app.openapi(createRoute({
73640
74183
  method: "get",
@@ -73647,7 +74190,7 @@ var createBrowserProviderRouter = (secretsOptions) => {
73647
74190
  200: jsonResponse$2(named("BrowserProviderGetResponse", browserProviderGetResponseSchema), "The stored configuration and the mode it resolves to."),
73648
74191
  403: forbiddenResponse("secrets:rotate")
73649
74192
  }
73650
- }), async (c) => c.json(await getBrowserProvider(secretsOptions), 200));
74193
+ }), async (c) => c.json(await getBrowserProvider(secretsOptions, host), 200));
73651
74194
  app.openapi(createRoute({
73652
74195
  method: "put",
73653
74196
  path: "/",
@@ -73663,7 +74206,7 @@ var createBrowserProviderRouter = (secretsOptions) => {
73663
74206
  }
73664
74207
  }), async (c) => {
73665
74208
  await setBrowserProvider(secretsOptions, c.req.valid("json"), { userId: readActorUserId(c) });
73666
- return c.json(await getBrowserProvider(secretsOptions), 200);
74209
+ return c.json(await getBrowserProvider(secretsOptions, host), 200);
73667
74210
  });
73668
74211
  app.openapi(createRoute({
73669
74212
  method: "post",
@@ -73678,7 +74221,7 @@ var createBrowserProviderRouter = (secretsOptions) => {
73678
74221
  400: errorResponse("The body failed its schema (`invalid_payload`)."),
73679
74222
  403: forbiddenResponse("secrets:rotate")
73680
74223
  }
73681
- }), async (c) => c.json(await testBrowserProviderConnection(secretsOptions, c.req.valid("json")), 200));
74224
+ }), async (c) => c.json(await testBrowserProviderConnection(secretsOptions, c.req.valid("json"), host), 200));
73682
74225
  return app;
73683
74226
  };
73684
74227
  //#endregion
@@ -73699,7 +74242,7 @@ var mountBrowser = (app, ctx) => {
73699
74242
  app.route("/api/browser-provider", createBrowserProviderRouter({
73700
74243
  ...ctx.secrets,
73701
74244
  audit: ctx.audit
73702
- }));
74245
+ }, { allowLocalBrowser: ctx.opts.host !== "image" }));
73703
74246
  app.route("/api/browser-profiles", createBrowserProfilesRouter({
73704
74247
  db: ctx.db,
73705
74248
  audit: ctx.audit
@@ -75466,8 +76009,127 @@ var resolveConnectSecrets = (opts) => {
75466
76009
  throw new Error("A box needs its own Connect secrets: pass `connectIdentity` (a deployed image) or `connectSecrets`, or boot a dev box through `startEmulatorServer`, which mints them.");
75467
76010
  };
75468
76011
  //#endregion
76012
+ //#region ../../libs/runtime/workspace-runtime/src/lib/connect/resolve-account.ts
76013
+ /**
76014
+ * The principal a connector call runs as.
76015
+ *
76016
+ * Only `oauth` holds accounts. Every other mode keeps the principal it was given:
76017
+ * `api_key`, `client_credentials` and `none` have no grant, and a `custom` piece's
76018
+ * material sits under `app` and is no account. On those modes an `account` is
76019
+ * ignored, since there is one credential to run as (criterion 10).
76020
+ *
76021
+ * On `oauth`, an explicit `app` reads as "no account named" (D14): every deployed
76022
+ * SDK sends it, and the `/execute` and Studio schemas default to it, so honouring
76023
+ * it would break a one-account box once its grant is keyed by email. Any other
76024
+ * explicit principal wins, and on `oauth` naming one beside an `account` is
76025
+ * `invalid_args`.
76026
+ *
76027
+ * With no account named, the one account is used; none is `no_token`, as the mint
76028
+ * says today; several is `account_required` with each id and email. A legacy `app`
76029
+ * grant is one more account, never a silent default.
76030
+ */
76031
+ async function resolveAccount(store, connector, selection) {
76032
+ if (connector.authMode !== "oauth") return selection.principal ?? { type: "app" };
76033
+ const named = selection.principal?.type === "app" ? void 0 : selection.principal;
76034
+ if (named !== void 0 && selection.account !== void 0) throw makeBrokerError("invalid_args", "name a principal or an account, not both");
76035
+ if (named !== void 0) return named;
76036
+ const grants = (await store.listGrants(connector.orgId)).filter((g) => g.connector === connector.connectorId);
76037
+ if (selection.account !== void 0) {
76038
+ const chosen = grants.find((g) => g.id === selection.account);
76039
+ if (chosen === void 0) throw makeBrokerError("no_token", `no account "${selection.account}" on connector "${connector.connectorId}"`, false);
76040
+ return principalFromKey(chosen.principalKey);
76041
+ }
76042
+ const accounts = defaultCandidates(grants);
76043
+ const [only] = accounts;
76044
+ if (only === void 0) throw makeBrokerError("no_token", `no grant for principal on connector "${connector.connectorId}"`);
76045
+ if (accounts.length > 1) throw accountRequired(connector.connectorId, accounts);
76046
+ return principalFromKey(only.principalKey);
76047
+ }
76048
+ /**
76049
+ * {@link resolveAccount} for a selection boundary on the box, which knows the
76050
+ * connector by id.
76051
+ *
76052
+ * Two cases keep the principal the request carried, so what they answer stays
76053
+ * today's. An unregistered connector: the mint refuses it as
76054
+ * `connector_installation_required`. An OAuth connector with no account yet, when
76055
+ * the request named none: the mint answers `no_token` itself, an evict of nothing
76056
+ * is still `evicted`, and a link or a binding made before the account is
76057
+ * connected is stored as it always was.
76058
+ */
76059
+ async function resolveConnectorAccount(store, orgId, connectorId, selection) {
76060
+ const requested = selection.principal ?? { type: "app" };
76061
+ const connector = await store.getConnector(orgId, connectorId);
76062
+ if (connector === null) return requested;
76063
+ try {
76064
+ return await resolveAccount(store, connector, selection);
76065
+ } catch (err) {
76066
+ if (selection.account === void 0 && isBrokerError(err) && err.code === "no_token") return requested;
76067
+ throw err;
76068
+ }
76069
+ }
76070
+ /** A fold or adoption may have moved the grant after the row's first resolve. */
76071
+ async function followAccountMove(resolveAccountKey, request, row, move) {
76072
+ if (!resolveAccountKey) return row;
76073
+ let key;
76074
+ try {
76075
+ key = await resolveAccountKey(request);
76076
+ } catch {
76077
+ return row;
76078
+ }
76079
+ return key === row.principalKey ? row : await move(row.id, key) ?? row;
76080
+ }
76081
+ /**
76082
+ * The grants a call naming no account may fall to.
76083
+ *
76084
+ * A grant the refresh sweep marked `revoked` cannot mint once its access token
76085
+ * expires. Counting it would turn a box into `account_required` after a reconnect
76086
+ * that could not read its dead legacy grant's identity and so kept both rows. When
76087
+ * every grant is revoked they all stay, so a lone one still reaches the mint and
76088
+ * the caller gets the refusal it gets today.
76089
+ */
76090
+ function defaultCandidates(grants) {
76091
+ const live = grants.filter((g) => g.healthStatus !== "revoked");
76092
+ return live.length > 0 ? live : grants;
76093
+ }
76094
+ /**
76095
+ * The list rides in the message as well as in `accounts`: the message is what
76096
+ * every consumer shows as is (an SDK error, a Studio toast), so the choice reads
76097
+ * the same wherever it surfaces.
76098
+ */
76099
+ function accountRequired(connectorId, grants) {
76100
+ const accounts = grants.map((g) => ({
76101
+ id: g.id,
76102
+ email: g.externalSubject
76103
+ }));
76104
+ const listed = accounts.map((a) => `${a.id} (${a.email ?? "no email"})`).join(", ");
76105
+ return {
76106
+ ...makeBrokerError("account_required", `connector "${connectorId}" holds ${accounts.length} accounts: ${listed}; name one with { account }`),
76107
+ code: "account_required",
76108
+ accounts
76109
+ };
76110
+ }
76111
+ var accountKeyResolver, connectedAccountLookup;
76112
+ var init_resolve_account = __esmMin((() => {
76113
+ init_src$27();
76114
+ accountKeyResolver = (store, orgId) => async (request) => isPrincipalKey(request.principalKey) ? principalKey(await resolveConnectorAccount(store, orgId, request.connector, {
76115
+ principal: principalFromKey(request.principalKey),
76116
+ account: request.account
76117
+ })) : request.principalKey;
76118
+ connectedAccountLookup = (store, orgId) => async ({ connector, principalKey: key }) => {
76119
+ if ((await store.getConnector(orgId, connector))?.authMode !== "oauth") return null;
76120
+ const grants = (await store.listGrants(orgId)).filter((g) => g.connector === connector);
76121
+ if (grants.length === 0) return null;
76122
+ const grant = grants.find((g) => g.principalKey === key);
76123
+ if (grant === void 0) throw new Error(`no connected account under ${key}; it moved`);
76124
+ return {
76125
+ id: grant.id,
76126
+ email: grant.externalSubject
76127
+ };
76128
+ };
76129
+ }));
76130
+ //#endregion
75469
76131
  //#region ../../libs/runtime/workspace-runtime/src/lib/connect/http/broker-refusal.ts
75470
- var statusForBrokerCode, brokerRefusal, answerBrokerRefusal, brokerRefusalResponses;
76132
+ var statusForBrokerCode, brokerRefusal, answerRefusal, answerBrokerRefusal, answerAccountScopedRefusal, answerAccountSelectionRefusal, unknownAccountResponse, accountRequiredResponse, brokerRefusalResponses;
75471
76133
  var init_broker_refusal = __esmMin((() => {
75472
76134
  init_src$27();
75473
76135
  init_route_helpers();
@@ -75482,7 +76144,8 @@ var init_broker_refusal = __esmMin((() => {
75482
76144
  case "invalid_args": return 400;
75483
76145
  case "invalid_grant": return 400;
75484
76146
  case "precondition_failed":
75485
- case "cardinality_mismatch": return 409;
76147
+ case "cardinality_mismatch":
76148
+ case "account_required": return 409;
75486
76149
  case "credential_error":
75487
76150
  case "execute_failed":
75488
76151
  case "timeout":
@@ -75492,15 +76155,18 @@ var init_broker_refusal = __esmMin((() => {
75492
76155
  brokerRefusal = (err) => {
75493
76156
  if (isBrokerError(err)) return {
75494
76157
  status: statusForBrokerCode(err.code),
75495
- body: {
76158
+ body: isAccountRequiredError(err) ? {
76159
+ code: err.code,
76160
+ message: err.message,
76161
+ details: { accounts: err.accounts }
76162
+ } : {
75496
76163
  code: err.code,
75497
76164
  message: err.message
75498
76165
  }
75499
76166
  };
75500
76167
  throw err;
75501
76168
  };
75502
- answerBrokerRefusal = (c, err) => {
75503
- const refusal = brokerRefusal(err);
76169
+ answerRefusal = (c, refusal) => {
75504
76170
  switch (refusal.status) {
75505
76171
  case 400: return c.json(refusal.body, 400);
75506
76172
  case 401: return c.json(refusal.body, 401);
@@ -75508,6 +76174,22 @@ var init_broker_refusal = __esmMin((() => {
75508
76174
  default: return c.json(refusal.body, 502);
75509
76175
  }
75510
76176
  };
76177
+ answerBrokerRefusal = (c, err) => answerRefusal(c, brokerRefusal(err));
76178
+ answerAccountScopedRefusal = (c, err) => {
76179
+ const refusal = brokerRefusal(err);
76180
+ return refusal.status === 409 ? c.json(refusal.body, 409) : answerRefusal(c, refusal);
76181
+ };
76182
+ answerAccountSelectionRefusal = (c, err) => {
76183
+ const refusal = brokerRefusal(err);
76184
+ switch (refusal.status) {
76185
+ case 400: return c.json(refusal.body, 400);
76186
+ case 401: return c.json(refusal.body, 401);
76187
+ case 409: return c.json(refusal.body, 409);
76188
+ default: throw err;
76189
+ }
76190
+ };
76191
+ unknownAccountResponse = (subject) => errorResponse(`${subject} names an \`account\` the connector does not hold (\`no_token\`). A reconnect gives the account a new id: list the connector's grants and name a current one.`);
76192
+ accountRequiredResponse = (subject) => errorResponse(`${subject} names no account and the connector holds several (\`account_required\`). \`details.accounts\` lists each one's \`id\` and \`email\` (\`null\` for a legacy account); name one and retry.`);
75511
76193
  brokerRefusalResponses = (subject) => ({
75512
76194
  401: errorResponse(`${subject} needs an authorization this box does not hold: no grant yet (\`no_token\`), a user who has not consented (\`user_authorization_required\`), a call with no principal (\`principal_required\`) or a provider that refused the identity (\`access_denied\`).`),
75513
76195
  404: errorResponse("The connector is not registered on this box (`connector_installation_required`), or the thing named does not exist (`not_found`)."),
@@ -75579,7 +76261,8 @@ var createConnectCallbackRouter = (opts, sharedContext) => {
75579
76261
  subjectType: "connection",
75580
76262
  payload: {
75581
76263
  connector: result.granted.connector,
75582
- principal: result.granted.principalKey
76264
+ principal: result.granted.principalKey,
76265
+ ...result.legacyLinksLeft ? { legacyLinksLeft: result.legacyLinksLeft } : {}
75583
76266
  }
75584
76267
  }, ctx.db, "the OAuth grant");
75585
76268
  return c.redirect(result.redirectTo, 302);
@@ -76863,11 +77546,13 @@ init_actor();
76863
77546
  init_error_envelopes();
76864
77547
  init_require_capability();
76865
77548
  init_route_helpers();
77549
+ init_resolve_account();
76866
77550
  init_spec_fetch();
76867
77551
  /**
76868
- * The principal the `operations` route mints under. In-process and app-scoped, the
76869
- * same one the `/execute` executor uses: introspecting a registration is the box
76870
- * reading a provider's own metadata, never an end user acting.
77552
+ * The principal the `operations` route asks to mint under. In-process and
77553
+ * app-scoped, the same one the `/execute` executor uses: introspecting a
77554
+ * registration is the box reading a provider's own metadata, never an end user
77555
+ * acting. On an OAuth connector it names no account ({@link specReadPrincipal}).
76871
77556
  *
76872
77557
  * Also the principal a `piece` registration's credential is stored under
76873
77558
  * (PLA-1664): the SDK's connector invoker asks for `app` unless told otherwise,
@@ -76875,6 +77560,16 @@ init_spec_fetch();
76875
77560
  */
76876
77561
  var APP_PRINCIPAL = { type: "app" };
76877
77562
  /**
77563
+ * The principal a provider-served spec is read under: {@link APP_PRINCIPAL} as
77564
+ * the request, which on an OAuth connector names no account, so the one account
77565
+ * it holds reads the spec (STA3-123). A Gmail grant is keyed by its mailbox and
77566
+ * there is no `app` grant to mint. The `operations` mint, the spec cache entry it
77567
+ * writes and the refresh that evicts it all ask here, so the three name the same
77568
+ * account. Every other registration (API key, custom material) keeps `app`.
77569
+ */
77570
+ var specReadPrincipal = (ctx, connectorId) => resolveConnectorAccount(ctx.store, DEV_ORG_ID, connectorId, { principal: APP_PRINCIPAL });
77571
+ var unmintedSpecWarning = (err) => `this connector's spec is served by the provider and needs its credential, which could not be minted — the spec was fetched unauthenticated: ${isBrokerError(err) || err instanceof Error ? err.message : String(err)}`;
77572
+ /**
76878
77573
  * The placement a piece's OAUTH2 row stores (PLA-1774), under either grant
76879
77574
  * (PLA-1838). Its token is read through the refresh-aware token path — an OAuth
76880
77575
  * access pair, or an M2M token minted per expiry — and every `TokenResponse`
@@ -77165,6 +77860,12 @@ var registerConnectorRoutes = (app, { opts, getContext, registry, unavailable, c
77165
77860
  const ctx = await getContext();
77166
77861
  const connector = await ctx.store.getConnector(DEV_ORG_ID, connectorId);
77167
77862
  if (!connector) return c.json(CONNECTOR_NOT_FOUND, 404);
77863
+ let principal = APP_PRINCIPAL;
77864
+ try {
77865
+ principal = await specReadPrincipal(ctx, connectorId);
77866
+ } catch (err) {
77867
+ if (!isAccountRequiredError(err)) throw err;
77868
+ }
77168
77869
  let entry = null;
77169
77870
  try {
77170
77871
  const def = resolveEffectiveDefinition({
@@ -77175,7 +77876,7 @@ var registerConnectorRoutes = (app, { opts, getContext, registry, unavailable, c
77175
77876
  def,
77176
77877
  orgId: DEV_ORG_ID,
77177
77878
  cache: ctx.specCache,
77178
- principal: APP_PRINCIPAL
77879
+ principal
77179
77880
  });
77180
77881
  } catch {}
77181
77882
  if (entry === null) return c.json({
@@ -77227,23 +77928,32 @@ var registerConnectorRoutes = (app, { opts, getContext, registry, unavailable, c
77227
77928
  const specOrigin = def.specSource?.kind ?? "derived";
77228
77929
  let token;
77229
77930
  let mintWarning;
77230
- if (specFetchUsesCredential(def)) try {
77231
- token = await ctx.service.issueToken({
77232
- orgId: DEV_ORG_ID,
77233
- principal: APP_PRINCIPAL
77234
- }, {
77235
- connector: def.id,
77236
- principal: APP_PRINCIPAL
77237
- });
77238
- } catch (err) {
77239
- mintWarning = `this connector's spec is served by the provider and needs its credential, which could not be minted — the spec was fetched unauthenticated: ${err instanceof Error ? err.message : String(err)}`;
77931
+ let principal = APP_PRINCIPAL;
77932
+ if (specFetchUsesCredential(def)) {
77933
+ try {
77934
+ principal = await specReadPrincipal(ctx, connectorId);
77935
+ } catch (err) {
77936
+ if (!isAccountRequiredError(err)) throw err;
77937
+ mintWarning = unmintedSpecWarning(err);
77938
+ }
77939
+ if (mintWarning === void 0) try {
77940
+ token = await ctx.service.issueToken({
77941
+ orgId: DEV_ORG_ID,
77942
+ principal
77943
+ }, {
77944
+ connector: def.id,
77945
+ principal
77946
+ });
77947
+ } catch (err) {
77948
+ mintWarning = unmintedSpecWarning(err);
77949
+ }
77240
77950
  }
77241
77951
  const baseFetch = opts.specFetch ?? defaultSpecFetch;
77242
77952
  const specEntry = specCacheEntryFor({
77243
77953
  def,
77244
77954
  orgId: DEV_ORG_ID,
77245
77955
  cache: ctx.specCache,
77246
- principal: APP_PRINCIPAL
77956
+ principal
77247
77957
  });
77248
77958
  const cached = specEntry === null ? null : createCachingSpecFetch({
77249
77959
  fetch: baseFetch,
@@ -77404,6 +78114,7 @@ var init_support = __esmMin((() => {
77404
78114
  operation: z.string().min(1),
77405
78115
  args: z.record(z.string(), z.unknown()).default({}),
77406
78116
  principal: principalSchema.default({ type: "app" }),
78117
+ account: z.string().min(1).optional(),
77407
78118
  installationId: z.string().min(1).optional(),
77408
78119
  runId: z.string().min(1).optional(),
77409
78120
  idempotent: z.boolean().optional()
@@ -77425,7 +78136,8 @@ var init_support = __esmMin((() => {
77425
78136
  error: {
77426
78137
  code: error.code,
77427
78138
  message: error.message,
77428
- ...error.retryable !== void 0 ? { retryable: error.retryable } : {}
78139
+ ...error.retryable !== void 0 ? { retryable: error.retryable } : {},
78140
+ ...isAccountRequiredError(error) ? { details: { accounts: error.accounts } } : {}
77429
78141
  }
77430
78142
  };
77431
78143
  switch (statusForBrokerCode(error.code)) {
@@ -77485,6 +78197,7 @@ init_src$6();
77485
78197
  init_src$26();
77486
78198
  init_route_helpers();
77487
78199
  init_api_docs();
78200
+ init_resolve_account();
77488
78201
  init_broker_refusal();
77489
78202
  init_support();
77490
78203
  /**
@@ -77534,27 +78247,29 @@ var createConnectTokenRouter = (opts, sharedContext) => {
77534
78247
  security: HMAC_ROUTE_SECURITY,
77535
78248
  middleware: [requireBrokerSignature(opts.hmacSecret)],
77536
78249
  summary: "Mint a credential for a connector",
77537
- description: "The runtime hot path: a `@stackbone/sdk/connect` adapter calls this before a tool call. The response carries `placement` — every slot the credential occupies — so the caller applies it where the OPERATOR configured it rather than guessing. It is the same set `POST /execute` applies inside the box, which is what keeps the two paths from disagreeing about one connector.\n\nThe two consumers read it differently, and only one of them can. `connectHeaders()` builds the request headers itself, so it honours the set: a `header` placement writes the raw token to that header, a `bearer` one writes `Authorization: Bearer`. `connect()` / `withConnect()` cannot — a connection runtime carries only a bearer through `auth.getToken`, and `TokenResult` has no slot for a placement — so that path stays BEARER-ONLY by construction. A non-bearer connector wired through it fails at the provider, not here: a key the provider reads from a header of its own, such as the `apikey` header the built-in `supabase` places beside its bearer, never reaches that header. Use `connectHeaders()` or `POST /execute` for those.\n\nThe signature proves POSSESSION of the workspace secret, not WHICH install is calling, so the body asserts its own scoping claims: `installationId` and `agentName` are read as claims, `connector` and `principal` pick the grant. A connector with no grant answers `no_token` on 401 and the adapter turns that into an authorization prompt.",
77538
- request: { body: jsonBody(named("ConnectTokenRequest", tokenRequestSchema), "Which connector, for which principal, on behalf of which install.") },
78250
+ description: "The runtime hot path: a `@stackbone/sdk/connect` adapter calls this before a tool call. The response carries `placement` — every slot the credential occupies — so the caller applies it where the OPERATOR configured it rather than guessing. It is the same set `POST /execute` applies inside the box, which is what keeps the two paths from disagreeing about one connector.\n\nThe two consumers read it differently, and only one of them can. `connectHeaders()` builds the request headers itself, so it honours the set: a `header` placement writes the raw token to that header, a `bearer` one writes `Authorization: Bearer`. `connect()` / `withConnect()` cannot — a connection runtime carries only a bearer through `auth.getToken`, and `TokenResult` has no slot for a placement — so that path stays BEARER-ONLY by construction. A non-bearer connector wired through it fails at the provider, not here: a key the provider reads from a header of its own, such as the `apikey` header the built-in `supabase` places beside its bearer, never reaches that header. Use `connectHeaders()` or `POST /execute` for those.\n\nThe signature proves POSSESSION of the workspace secret, not WHICH install is calling, so the body asserts its own scoping claims: `installationId` and `agentName` are read as claims, `connector`, `principal` and `account` pick the grant. `account` picks a connected account by its id, as `account_required` lists them; an id the connector does not hold answers `no_token` on 401. On an OAuth connector `app` with no `account` names no account: the one account the connector holds is used, and several answer `account_required` on 409. A connector with no grant answers `no_token` on 401 and the adapter turns that into an authorization prompt.",
78251
+ request: { body: jsonBody(named("ConnectTokenRequest", tokenRequestSchema), "Which connector, for which principal or connected account, on behalf of which install.") },
77539
78252
  responses: {
77540
78253
  200: jsonResponse$2(named("ConnectTokenResponse", connectTokenResponseSchema), "The minted credential. `expiresAt` is absent when the provider declared no expiry."),
77541
78254
  400: errorResponse("The body failed its schema (`invalid_payload`), or the broker refused the request (`invalid_args`, `invalid_grant`)."),
77542
78255
  401: AGENT_WIRE_401,
77543
78256
  404: errorResponse("The connector is not registered on this box (`connector_installation_required`, `not_found`)."),
78257
+ 409: accountRequiredResponse("The mint"),
77544
78258
  502: errorResponse("The provider round-trip failed (`credential_error`, `execute_failed`, `timeout`, `invalid_output`).")
77545
78259
  }
77546
78260
  }), async (c) => {
77547
78261
  const request = c.req.valid("json");
77548
78262
  const ctx = await getContext();
77549
- const caller = {
77550
- orgId: DEV_ORG_ID,
77551
- principal: request.principal,
77552
- ...request.installationId !== void 0 ? { installationId: request.installationId } : {}
77553
- };
77554
78263
  try {
78264
+ const principal = await resolveConnectorAccount(ctx.store, DEV_ORG_ID, request.connector, request);
78265
+ const caller = {
78266
+ orgId: DEV_ORG_ID,
78267
+ principal,
78268
+ ...request.installationId !== void 0 ? { installationId: request.installationId } : {}
78269
+ };
77555
78270
  const minted = await ctx.service.issueToken(caller, {
77556
78271
  connector: request.connector,
77557
- principal: request.principal,
78272
+ principal,
77558
78273
  ...request.installationId !== void 0 ? { installationId: request.installationId } : {},
77559
78274
  ...request.agentName !== void 0 ? { agentName: request.agentName } : {}
77560
78275
  });
@@ -77564,7 +78279,7 @@ var createConnectTokenRouter = (opts, sharedContext) => {
77564
78279
  placement: minted.placement
77565
78280
  }, 200);
77566
78281
  } catch (err) {
77567
- return answerBrokerRefusal(c, err);
78282
+ return answerAccountScopedRefusal(c, err);
77568
78283
  }
77569
78284
  });
77570
78285
  app.openapi(createRoute({
@@ -77579,20 +78294,20 @@ var createConnectTokenRouter = (opts, sharedContext) => {
77579
78294
  responses: {
77580
78295
  200: jsonResponse$2(named("ConnectExecuteSuccess", connectExecuteSuccessSchema), "The operation’s own output, passed through untouched."),
77581
78296
  400: executeRefusal("The body failed its schema (`invalid_payload`), or the broker refused the arguments (`invalid_args`, `invalid_grant`)."),
77582
- 401: executeRefusal("The signature is missing or wrong (`access_denied`), or the connector has no usable authorization (`no_token`, `user_authorization_required`, `principal_required`)."),
78297
+ 401: executeRefusal("The signature is missing or wrong (`access_denied`), or the connector has no usable authorization (`no_token`, `user_authorization_required`, `principal_required`). `no_token` also answers an `account` the connector does not hold."),
77583
78298
  404: executeRefusal("No such connector on this box, or no such operation on it (`not_found`, `connector_installation_required`)."),
77584
- 409: executeRefusal("The provider refused a guarded write: no rows matched (`precondition_failed`), or the requested row limit was exceeded (`cardinality_mismatch`). Terminal: change the guard or narrow the target before retrying."),
78299
+ 409: executeRefusal("The provider refused a guarded write: no rows matched (`precondition_failed`), or the requested row limit was exceeded (`cardinality_mismatch`). Terminal: change the guard or narrow the target before retrying. Or the call names no account and the connector holds several (`account_required`): `error.details.accounts` lists each one’s `id` and `email`; name one with `account` and retry."),
77585
78300
  502: executeRefusal("The provider call failed (`execute_failed`, `timeout`, `credential_error`, `invalid_output`). Upstream, not the box.")
77586
78301
  }
77587
78302
  }), async (c) => {
77588
78303
  const request = c.req.valid("json");
77589
- const executor = buildConnectExecutor(opts, await getContext(), registry, {
77590
- orgId: DEV_ORG_ID,
77591
- principal: request.principal,
77592
- ...request.installationId !== void 0 ? { installationId: request.installationId } : {}
77593
- });
78304
+ const ctx = await getContext();
77594
78305
  try {
77595
- const result = await executor.execute({
78306
+ const result = await buildConnectExecutor(opts, ctx, registry, {
78307
+ orgId: DEV_ORG_ID,
78308
+ principal: await resolveConnectorAccount(ctx.store, DEV_ORG_ID, request.connector, request),
78309
+ ...request.installationId !== void 0 ? { installationId: request.installationId } : {}
78310
+ }).execute({
77596
78311
  orgId: DEV_ORG_ID,
77597
78312
  connectConnectorId: request.connector,
77598
78313
  connectOperationId: request.operation,
@@ -77618,31 +78333,33 @@ var createConnectTokenRouter = (opts, sharedContext) => {
77618
78333
  middleware: [requireBrokerSignature(opts.hmacSecret)],
77619
78334
  summary: "Drop a cached credential",
77620
78335
  description: "The adapter’s 401-recovery hook: a provider rejected a token that has not expired yet, so the cached copy is dropped and the NEXT mint fetches a fresh one. The durable GRANT survives — that is what separates this from `DELETE /api/connect/grants/{grantId}`, which drops the authorization and makes the principal start over.\n\nEvicting a connector with nothing cached is not an error; it answers the same `evicted` either way.",
77621
- request: { body: jsonBody(named("ConnectEvictRequest", evictRequestSchema), "Which connector, for which principal.") },
78336
+ request: { body: jsonBody(named("ConnectEvictRequest", evictRequestSchema), "Which connector, for which principal or connected account.") },
77622
78337
  responses: {
77623
78338
  200: jsonResponse$2(named("ConnectEvictResponse", connectEvictResponseSchema), "The cached credential for that pair is gone. The grant is untouched."),
77624
78339
  400: errorResponse("The body failed its schema (`invalid_payload`), or the broker refused the arguments (`invalid_args`, `invalid_grant`)."),
77625
78340
  401: AGENT_WIRE_401,
77626
78341
  404: errorResponse("The connector is not registered on this box (`connector_installation_required`, `not_found`)."),
78342
+ 409: accountRequiredResponse("The eviction"),
77627
78343
  502: errorResponse("The broker could not reach its own cache (`credential_error`).")
77628
78344
  }
77629
78345
  }), async (c) => {
77630
78346
  const request = c.req.valid("json");
77631
78347
  const ctx = await getContext();
77632
78348
  try {
78349
+ const principal = await resolveConnectorAccount(ctx.store, DEV_ORG_ID, request.connector, request);
77633
78350
  await ctx.service.evict({
77634
78351
  orgId: DEV_ORG_ID,
77635
- principal: request.principal
78352
+ principal
77636
78353
  }, {
77637
78354
  connector: request.connector,
77638
- principal: request.principal
78355
+ principal
77639
78356
  });
77640
78357
  return c.json({
77641
78358
  connector: request.connector,
77642
78359
  status: "evicted"
77643
78360
  }, 200);
77644
78361
  } catch (err) {
77645
- return answerBrokerRefusal(c, err);
78362
+ return answerAccountScopedRefusal(c, err);
77646
78363
  }
77647
78364
  });
77648
78365
  return app;
@@ -77651,6 +78368,7 @@ var createConnectTokenRouter = (opts, sharedContext) => {
77651
78368
  //#region ../../libs/runtime/workspace-runtime/src/lib/connect/http/connect-handlers.ts
77652
78369
  init_src$3();
77653
78370
  init_src$6();
78371
+ init_resolve_account();
77654
78372
  /** The org every registration on this box belongs to. A box is single-tenant, so
77655
78373
  * the intake's spec cache key is scoped by the same id the routes use. */
77656
78374
  var CONNECT_BOX_ORG_ID = DEV_ORG_ID;
@@ -77691,6 +78409,12 @@ var createConnectHandlers = (opts) => {
77691
78409
  const connectorRows = async (connector) => {
77692
78410
  return (await shared()).store.getConnector(DEV_ORG_ID, connector);
77693
78411
  };
78412
+ const resolveAccountKey = async (request) => {
78413
+ const { store } = await shared();
78414
+ await store.awaitAccountMoves?.(DEV_ORG_ID, request.connector);
78415
+ return accountKeyResolver(store, DEV_ORG_ID)(request);
78416
+ };
78417
+ const connectedAccountOf = async (link) => connectedAccountLookup((await shared()).store, DEV_ORG_ID)(link);
77694
78418
  const createRefreshProcessor = async () => {
77695
78419
  const ctx = await shared();
77696
78420
  return createGrantRefreshProcessor({
@@ -77724,6 +78448,8 @@ var createConnectHandlers = (opts) => {
77724
78448
  issueCredentialMaterial,
77725
78449
  evict,
77726
78450
  connectorRows,
78451
+ resolveAccountKey,
78452
+ connectedAccountOf,
77727
78453
  specCache,
77728
78454
  createRefreshProcessor,
77729
78455
  agentWire,
@@ -77800,6 +78526,8 @@ var mountConnect = (app, ctx) => {
77800
78526
  hooks.captureConnectIssueCredentialMaterial?.(connectHandlers.issueCredentialMaterial);
77801
78527
  hooks.captureConnectEvictToken?.(connectHandlers.evict);
77802
78528
  hooks.captureConnectConnectorRows?.(connectHandlers.connectorRows);
78529
+ hooks.captureConnectResolveAccountKey?.(connectHandlers.resolveAccountKey);
78530
+ hooks.captureConnectConnectedAccountOf?.(connectHandlers.connectedAccountOf);
77803
78531
  hooks.captureConnectSpecCache?.(connectHandlers.specCache);
77804
78532
  hooks.captureConnectRefreshProcessorFactory?.(connectHandlers.createRefreshProcessor);
77805
78533
  hooks.captureConnectAgentWire?.(connectHandlers.agentWire);
@@ -98239,6 +98967,7 @@ var init_mount$1 = __esmMin((() => {
98239
98967
  const browserPool = createBrowserSessionPool({
98240
98968
  factory: createStagehandBrowserFactory({
98241
98969
  importStagehand: resolveWorkspaceStagehand(options.projectRoot),
98970
+ allowLocalBrowser: options.host !== "image",
98242
98971
  loadInitialStorageState: async () => {
98243
98972
  const merged = await profileStore.loadMergedStorageState();
98244
98973
  return merged.error ? void 0 : merged.data;
@@ -98394,6 +99123,7 @@ var createDeepAgentBoot = (deps) => {
98394
99123
  deepAgentsProjectRoot = deepAgents.projectRoot;
98395
99124
  plantAmbientBridges = fromApp.plantAmbientBridges;
98396
99125
  const mounted = await mountDeepAgents(app, {
99126
+ host: opts.host,
98397
99127
  projectRoot: deepAgents.projectRoot,
98398
99128
  agents: deepAgents.agents,
98399
99129
  compileAgentModule: deepAgents.compileAgentModule,
@@ -100132,6 +100862,7 @@ var init_runtime_connector_plants = __esmMin((() => {
100132
100862
  connector: args.connector,
100133
100863
  operation: args.operation,
100134
100864
  args: args.args,
100865
+ ...args.account !== void 0 ? { account: args.account } : {},
100135
100866
  ...echo ?? {}
100136
100867
  },
100137
100868
  ...extra,
@@ -101537,7 +102268,8 @@ var init_intake_runtime = __esmMin((() => {
101537
102268
  ...opts.startAgent ? { startAgent: opts.startAgent } : {},
101538
102269
  deliveries: opts.store,
101539
102270
  ...opts.resume ? { resume: opts.resume } : {},
101540
- ...opts.isAgentEcho ? { isAgentEcho: opts.isAgentEcho } : {}
102271
+ ...opts.isAgentEcho ? { isAgentEcho: opts.isAgentEcho } : {},
102272
+ ...opts.connectedAccountOf ? { connectedAccountOf: opts.connectedAccountOf } : {}
101541
102273
  }, subscription, event);
101542
102274
  forwards.add(forwarding);
101543
102275
  forwarding.finally(() => forwards.delete(forwarding)).catch(() => void 0);
@@ -101615,7 +102347,7 @@ var init_intake_runtime = __esmMin((() => {
101615
102347
  }));
101616
102348
  //#endregion
101617
102349
  //#region ../../libs/runtime/workspace-runtime/src/lib/connect-inbound/subscriptions-router.ts
101618
- var TRIGGER_SUBSCRIPTIONS_TAG, looseSubscriptionIdPath, deleteTriggerSubscriptionQuery, mappingCompileError, KEY_REFUSALS, keyCompileRefusal, keyCompileError, keysCompileError, compileKey, targetSchemaRefusal, previewKey, editingWaitOrKey, patchKeysRefusal, linkSampleEvent, namedLink, namedLinkSampleEvent, previewSample, armingMappingRefusal, trimmedScope, VERIFY_TOKEN_PARAM, withGeneratedVerifyToken, scopeValuesRefusal, sameScope, unreachableConnectorRefusal, fencedOperationRefusal, armingCursorSeed, webhookArm, armedWebhookUrl, answerEnableRefusal, armingEnable, transientRefusal, ENABLE_RETRYABLE_RESPONSE, disarmingHook, linkNames, createTriggerSubscriptionsRouter;
102350
+ var TRIGGER_SUBSCRIPTIONS_TAG, looseSubscriptionIdPath, deleteTriggerSubscriptionQuery, mappingCompileError, KEY_REFUSALS, keyCompileRefusal, keyCompileError, keysCompileError, compileKey, targetSchemaRefusal, previewKey, editingWaitOrKey, patchKeysRefusal, linkSampleEvent, namedLink, namedLinkSampleEvent, previewSample, withLinkAccount, armingMappingRefusal, trimmedScope, VERIFY_TOKEN_PARAM, withGeneratedVerifyToken, scopeValuesRefusal, sameScope, unreachableConnectorRefusal, fencedOperationRefusal, armingCursorSeed, webhookArm, armedWebhookUrl, answerEnableRefusal, armingEnable, transientRefusal, ENABLE_RETRYABLE_RESPONSE, disarmingHook, linkNames, createTriggerSubscriptionsRouter;
101619
102351
  var init_subscriptions_router = __esmMin((() => {
101620
102352
  init_src$7();
101621
102353
  init_src$6();
@@ -101623,6 +102355,8 @@ var init_subscriptions_router = __esmMin((() => {
101623
102355
  init_src$3();
101624
102356
  init_src$26();
101625
102357
  init_box_audit();
102358
+ init_broker_refusal();
102359
+ init_resolve_account();
101626
102360
  init_actor();
101627
102361
  init_require_capability();
101628
102362
  init_route_helpers();
@@ -101630,6 +102364,7 @@ var init_subscriptions_router = __esmMin((() => {
101630
102364
  init_webhook_paths();
101631
102365
  init_piece_action_runner();
101632
102366
  init_target_input_schema();
102367
+ init_connected_account();
101633
102368
  init_intake_scheduler();
101634
102369
  TRIGGER_SUBSCRIPTIONS_TAG = "trigger-subscriptions";
101635
102370
  looseSubscriptionIdPath = z.object({ id: z.string().openapi({
@@ -101721,17 +102456,20 @@ var init_subscriptions_router = __esmMin((() => {
101721
102456
  namedLink = async (deps, request) => request.subscriptionId === void 0 ? null : deps.store.get(request.subscriptionId);
101722
102457
  namedLinkSampleEvent = async (deps, request) => linkSampleEvent(await namedLink(deps, request), request.connector, request.trigger);
101723
102458
  previewSample = async (deps, request) => {
101724
- const link = await namedLink(deps, request);
102459
+ const named = await namedLink(deps, request);
102460
+ const link = named !== null && named.connector === request.connector && named.trigger === request.trigger ? named : null;
101725
102461
  const own = linkSampleEvent(link, request.connector, request.trigger);
101726
102462
  if (own !== void 0) return {
101727
102463
  event: own,
101728
- source: "link"
102464
+ source: "link",
102465
+ link
101729
102466
  };
101730
102467
  if (isWebhookTrigger(deps.registry, request.connector, request.trigger)) return {
101731
102468
  event: void 0,
101732
- source: "none"
102469
+ source: "none",
102470
+ link
101733
102471
  };
101734
- if (deps.fetchSampleEvent && link !== null && link.connector === request.connector && link.trigger === request.trigger) {
102472
+ if (deps.fetchSampleEvent && link !== null) {
101735
102473
  const warn = deps.logger?.warn ?? ((message) => console.warn(message));
101736
102474
  let fetched = null;
101737
102475
  try {
@@ -101750,15 +102488,26 @@ var init_subscriptions_router = __esmMin((() => {
101750
102488
  }
101751
102489
  return {
101752
102490
  event: fetched,
101753
- source: "fetched"
102491
+ source: "fetched",
102492
+ link
101754
102493
  };
101755
102494
  }
101756
102495
  }
101757
102496
  return {
101758
102497
  event: resolveTriggerSampleData(deps.registry, request.connector, request.trigger),
101759
- source: "declared"
102498
+ source: "declared",
102499
+ link
101760
102500
  };
101761
102501
  };
102502
+ withLinkAccount = async (deps, link, sample) => {
102503
+ if (link === null || deps.connectedAccountOf === void 0) return sample;
102504
+ try {
102505
+ return enrichWithAccount(sample, await deps.connectedAccountOf(link));
102506
+ } catch (err) {
102507
+ (deps.logger?.warn ?? ((message) => console.warn(message)))(`[trigger-link] could not read the sample's connected account: ${messageOf$3(err)}`);
102508
+ return sample;
102509
+ }
102510
+ };
101762
102511
  armingMappingRefusal = async (deps, link) => {
101763
102512
  if (!deps.resolveTargetInputSchema) return null;
101764
102513
  const schemaPair = deps.resolveTargetInputSchema(link.targetKind, link.targetName);
@@ -101768,7 +102517,7 @@ var init_subscriptions_router = __esmMin((() => {
101768
102517
  if (isEmptyTriggerSample(link.sampleEvent) && isWebhookTrigger(deps.registry, link.connector, link.trigger)) return null;
101769
102518
  const sampleData = isEmptyTriggerSample(link.sampleEvent) ? resolveTriggerSampleData(deps.registry, link.connector, link.trigger) : link.sampleEvent;
101770
102519
  if (isEmptyTriggerSample(sampleData)) return null;
101771
- const applied = await mapLinkEvent(link, sampleData);
102520
+ const applied = await mapLinkEvent(link, await withLinkAccount(deps, link, sampleData));
101772
102521
  if (!applied.ok) {
101773
102522
  if (applied.error.kind === "parse_error") return null;
101774
102523
  return {
@@ -101933,16 +102682,18 @@ var init_subscriptions_router = __esmMin((() => {
101933
102682
  middleware: [requireCapability("config:write")],
101934
102683
  summary: "Link a polling trigger to a workflow or agent",
101935
102684
  description: "Arms a new link and, when it is enabled, its polling repeatable. A link on a piece's webhook trigger arms no repeatable: its `onEnable` registers the link's own `/api/hooks/{subscriptionId}/{token}` URL at the provider, under a token minted for this arm and never returned. Every way a link can be born broken is refused here rather than at tick time: a trigger the registry does not resolve, a trigger missing one of the per-subscription values it declares (a mailbox and a Drive folder are both one of these) or carrying one that does not match the shape the param declares, and a `mapping`, `correlationKey` or `conversationKey` that does not compile as JSONata. A link carries the one key its kind reads: a workflow link resumes a run by `correlationKey`, an agent link continues a conversation by `conversationKey`, and the other kind's key is refused (`invalid_payload`) unless it is blank (null, empty or whitespace). A body that names no `targetKind` is a workflow link here too. An agent link with a `conversationKey` may also carry `debounceMs`, from 1 to 10000 ms: how long it waits after an event for more events of the same conversation, so a burst of chat messages gets one answer. Null or absent answers each event as it arrives. A non-null `debounceMs` is refused (`invalid_payload`) on a workflow link and on an agent link whose `conversationKey` is absent or blank. A link being ARMED is held to more: its `mapping` is run over the trigger's sample event and the result validated against the destination's input schema, so a mapping that could only ever be rejected never arms a poll that would consume events and start nothing. A blank or `null` `mapping` is judged on what the link delivers without one: a workflow link gets the event unchanged; an agent link gets it unchanged when it carries a non-empty text `message`, and otherwise as `{ message }` holding the event as text (a string as is, anything else as JSON), so an agent link with no mapping is not refused for an event whose `message` is missing or not text. Nothing is stored in its place: the stored `mapping` stays `null`. A trigger that declares no sample, or an empty one (`{}`), gives that check nothing to judge, and the link arms; once it has received an event, that event is the sample the check reads on every later patch. A link born with `enabled: false` skips that check — it is made on the patch that turns it on.",
101936
- request: { body: jsonBody(named("RegisterTriggerSubscriptionRequest", registerTriggerSubscriptionRequestSchema), "The capability to poll, the workflow or agent to start, and the optional JSONata in between. `principalKey` defaults to `app` — the integration's own credential rather than a user grant.") },
102685
+ request: { body: jsonBody(named("RegisterTriggerSubscriptionRequest", registerTriggerSubscriptionRequestSchema), "The capability to poll, the workflow or agent to start, and the optional JSONata in between. `principalKey` defaults to `app` — the integration's own credential rather than a user grant. On an OAuth connector `app` names no account: the link is stored under the key of the one account the connector holds, and several answer `account_required`. `account` names one of them by its grant id, as `account_required` lists them, and the link is stored under that account's key.") },
101937
102686
  responses: {
101938
102687
  201: jsonResponse$2(named("TriggerSubscriptionResponse", triggerSubscriptionResponseSchema), "The link as it was stored. Its repeatable is armed when `enabled`."),
101939
102688
  403: forbiddenResponse("config:write"),
101940
- 400: errorResponse("The body failed its schema (`invalid_payload`, including a key the link's kind does not read and a `debounceMs` on a workflow link or on an agent link with no `conversationKey`), or the link would be born broken: `unknown_capability` (no such polling trigger on that integration, including when the integration itself is unknown), `scope_required` (a value the trigger declares in `scopeParams` is missing — the message names which, and a mailbox-scoped trigger reports its mailbox this way too), `invalid_scope` (a value that does not match the pattern its param declares), `invalid_mapping`, `invalid_correlation_key` or `invalid_conversation_key` (the JSONata does not compile). A link being ARMED is also refused when its mapping (or, with none, the default its kind delivers) compiles but cannot fill the destination: `mapping_evaluation_failed` (it throws on the trigger's sample event) or `mapping_cannot_produce_input` (the mapped sample violates the destination's input schema — `details.mappingIssues` names the offending paths), and a piece trigger whose own `onEnable` fails: `trigger_enable_failed`, with the piece's message, or the broker's own code when the link's credential could not be minted (`no_token`, `user_authorization_required`, `invalid_grant`, `credential_error`). A piece's webhook link with no event yet is not judged on its mapping: its trigger's declared sample is another account's example, so the check waits for the link's first event. A piece's webhook trigger is refused `webhook_url_unavailable` on a box that knows no public URL its provider could deliver to (`stackbone dev` with no tunnel). Nothing is stored and nothing is armed."),
102689
+ 400: errorResponse("The body failed its schema (`invalid_payload`, including a key the link's kind does not read and a `debounceMs` on a workflow link or on an agent link with no `conversationKey`), names a `principalKey` other than `app` beside an `account` (`invalid_args`), or the link would be born broken: `unknown_capability` (no such polling trigger on that integration, including when the integration itself is unknown), `scope_required` (a value the trigger declares in `scopeParams` is missing — the message names which, and a mailbox-scoped trigger reports its mailbox this way too), `invalid_scope` (a value that does not match the pattern its param declares), `invalid_mapping`, `invalid_correlation_key` or `invalid_conversation_key` (the JSONata does not compile). A link being ARMED is also refused when its mapping (or, with none, the default its kind delivers) compiles but cannot fill the destination: `mapping_evaluation_failed` (it throws on the trigger's sample event) or `mapping_cannot_produce_input` (the mapped sample violates the destination's input schema — `details.mappingIssues` names the offending paths), and a piece trigger whose own `onEnable` fails: `trigger_enable_failed`, with the piece's message, or the broker's own code when the link's credential could not be minted (`no_token`, `user_authorization_required`, `invalid_grant`, `credential_error`). A piece's webhook link with no event yet is not judged on its mapping: its trigger's declared sample is another account's example, so the check waits for the link's first event. A piece's webhook trigger is refused `webhook_url_unavailable` on a box that knows no public URL its provider could deliver to (`stackbone dev` with no tunnel). Nothing is stored and nothing is armed."),
102690
+ 401: unknownAccountResponse("The link"),
102691
+ 409: accountRequiredResponse("The link"),
101941
102692
  503: ENABLE_RETRYABLE_RESPONSE
101942
102693
  }
101943
102694
  }), async (c) => {
101944
102695
  const parsed = c.req.valid("json");
101945
- const { connector, trigger, principalKey, targetKind, targetName } = parsed;
102696
+ const { connector, trigger, targetKind, targetName } = parsed;
101946
102697
  const scope = withGeneratedVerifyToken(deps, {
101947
102698
  connector,
101948
102699
  trigger
@@ -101961,13 +102712,24 @@ var init_subscriptions_router = __esmMin((() => {
101961
102712
  if (mappingError) return c.json(mappingError, 400);
101962
102713
  const keyError = keysCompileError(parsed);
101963
102714
  if (keyError) return c.json(keyError, 400);
102715
+ let principalKey;
102716
+ try {
102717
+ principalKey = deps.resolveAccountKey ? await deps.resolveAccountKey({
102718
+ connector,
102719
+ principalKey: parsed.principalKey,
102720
+ account: parsed.account
102721
+ }) : parsed.principalKey;
102722
+ } catch (err) {
102723
+ return answerAccountSelectionRefusal(c, err);
102724
+ }
101964
102725
  if (parsed.enabled !== false) {
101965
102726
  const armingRefusal = await armingMappingRefusal(deps, {
101966
102727
  connector,
101967
102728
  trigger,
101968
102729
  targetKind,
101969
102730
  targetName,
101970
- mapping: parsed.mapping ?? null
102731
+ mapping: parsed.mapping ?? null,
102732
+ principalKey
101971
102733
  });
101972
102734
  if (armingRefusal) return c.json(armingRefusal, 400);
101973
102735
  }
@@ -102056,7 +102818,11 @@ var init_subscriptions_router = __esmMin((() => {
102056
102818
  ...cursorColumn !== null ? { cursorColumn } : {},
102057
102819
  ...seed !== null ? { pollCursor: seed } : {}
102058
102820
  };
102059
- const subscription = await deps.store.register(input);
102821
+ const subscription = await followAccountMove(deps.resolveAccountKey, {
102822
+ connector,
102823
+ principalKey: parsed.principalKey,
102824
+ account: parsed.account
102825
+ }, await deps.store.register(input), (id, principalKey) => deps.store.update(id, { principalKey }));
102060
102826
  if (subscription.enabled && !webhook) await deps.scheduler.add(subscription.id);
102061
102827
  await recordCompleted(deps.audit, {
102062
102828
  action: "trigger.create",
@@ -102072,7 +102838,7 @@ var init_subscriptions_router = __esmMin((() => {
102072
102838
  path: "/preview",
102073
102839
  tags: [TRIGGER_SUBSCRIPTIONS_TAG],
102074
102840
  summary: "Dry-run a candidate mapping over the trigger sample event",
102075
- description: "Compiles the JSONata, evaluates it over the sample event, then validates the result against the destination input JSON Schema: the workflow's declared schema, or an agent's required `message` string with at least one character. Whitespace is valid. No mapping is written, no repeatable armed, no delivery journalled and no cursor moved, so it is the safe way to author a mapping before saving one.\n\nThe sample event is the trigger descriptor's own, unless `subscriptionId` names a link on the same trigger. A link that has received an event is evaluated over its first real event (`sampleSource: link`). A link with none yet is evaluated over the newest event its provider holds, read once without claiming or delivering it; that event is kept as the link's sample and echoed back as `sampleEvent` (`sampleSource: fetched`). When that read finds nothing or fails (an empty mailbox, an expired credential, a timeout), the answer is over the descriptor's sample (`sampleSource: declared`), never an error.\n\nA webhook trigger has no feed to read and its descriptor's sample comes from another account, so until the named link receives its first event nothing is evaluated: the answer is `{ output: null, issues: [], sampleSource: none }`, with no key and no verdict on the mapping. The same holds when no `subscriptionId` is given. A key or a destination input schema that does not compile is still refused with its 400, since it is wrong on any event.\n\nA blank or `null` `mapping` maps the way the link delivers: on a workflow it is the identity mapping and the sample event comes back unchanged; on an agent the sample event comes back unchanged when it carries a non-empty text `message`, and otherwise as `{ message }` holding the event as text (a string as is, anything else as JSON). `output` is whatever the expression produced, which may be a scalar or an array as easily as an object.\n\nThe key the link's kind reads is evaluated over the same sample event: `correlationKey` on a workflow link (the run a real event would resume), `conversationKey` on an agent link (the conversation it would land in). As on register, a non-blank key of the other kind is refused (`invalid_payload`); a blank one (null, empty or whitespace) is no key.\n\nPresent only on a box with an inbound-trigger intake armed.",
102841
+ description: "Compiles the JSONata, evaluates it over the sample event, then validates the result against the destination input JSON Schema: the workflow's declared schema, or an agent's required `message` string with at least one character. Whitespace is valid. No mapping is written, no repeatable armed, no delivery journalled and no cursor moved, so it is the safe way to author a mapping before saving one.\n\nThe sample event is the trigger descriptor's own, unless `subscriptionId` names a link on the same trigger. A link that has received an event is evaluated over its first real event (`sampleSource: link`). A link with none yet is evaluated over the newest event its provider holds, read once without claiming or delivering it; that event is kept as the link's sample and echoed back as `sampleEvent` (`sampleSource: fetched`). When that read finds nothing or fails (an empty mailbox, an expired credential, a timeout), the answer is over the descriptor's sample (`sampleSource: declared`), never an error. Whichever it is, when the named link runs as a connected account the sample is evaluated with that account added as `connectedAccount: { id, email }`, as delivery adds it to every event; `sampleEvent` is echoed as the provider sent it.\n\nA webhook trigger has no feed to read and its descriptor's sample comes from another account, so until the named link receives its first event nothing is evaluated: the answer is `{ output: null, issues: [], sampleSource: none }`, with no key and no verdict on the mapping. The same holds when no `subscriptionId` is given. A key or a destination input schema that does not compile is still refused with its 400, since it is wrong on any event.\n\nA blank or `null` `mapping` maps the way the link delivers: on a workflow it is the identity mapping and the sample event comes back unchanged; on an agent the sample event comes back unchanged when it carries a non-empty text `message`, and otherwise as `{ message }` holding the event as text (a string as is, anything else as JSON). `output` is whatever the expression produced, which may be a scalar or an array as easily as an object.\n\nThe key the link's kind reads is evaluated over the same sample event: `correlationKey` on a workflow link (the run a real event would resume), `conversationKey` on an agent link (the conversation it would land in). As on register, a non-blank key of the other kind is refused (`invalid_payload`); a blank one (null, empty or whitespace) is no key.\n\nPresent only on a box with an inbound-trigger intake armed.",
102076
102842
  request: { body: jsonBody(named("PreviewTriggerMappingRequest", previewTriggerMappingRequestSchema), "The capability to read the sample event from, the workflow or agent to validate against, and the candidate `mapping` (plus the optional `correlationKey` or `conversationKey`) to try.") },
102077
102843
  responses: {
102078
102844
  200: jsonResponse$2(named("TriggerMappingPreview", triggerMappingPreviewResponseSchema), "The mapped output, plus one issue per field the target schema rejected. An empty `issues` with a 200 IS the passing dry-run, except under `sampleSource: none`: a webhook trigger with no event yet, where nothing was evaluated and `output` is `null`. Otherwise `sampleSource` says which sample event was evaluated over, and `sampleEvent` is present only when it was `fetched`. `correlationKey` and `conversationKey` are each present only when the request carried a non-blank expression for it, and `null` when that expression matched nothing."),
@@ -102122,7 +102888,7 @@ var init_subscriptions_router = __esmMin((() => {
102122
102888
  sampleSource: sample.source
102123
102889
  }, 200);
102124
102890
  }
102125
- const sampleData = sample.event;
102891
+ const sampleData = await withLinkAccount(deps, sample.link, sample.event);
102126
102892
  const source = {
102127
102893
  sampleSource: sample.source,
102128
102894
  ...sample.source === "fetched" ? { sampleEvent: sample.event } : {}
@@ -102231,19 +102997,21 @@ var init_subscriptions_router = __esmMin((() => {
102231
102997
  tags: [TRIGGER_SUBSCRIPTIONS_TAG],
102232
102998
  middleware: [requireCapability("config:write")],
102233
102999
  summary: "List a provider-filled scope param's options for one principal",
102234
- description: "Runs the options loader of a scope param the capability flags `dynamicOptions: true` (a piece dropdown, such as WhatsApp's business number) through the credential of `principalKey`, and answers the values it may take. Each `value` is the text a link stores under the param and is sent back as is on register: a string the piece answered stays that string, and anything else is its JSON. Writes nothing and arms nothing.\n\nPresent only on a box with an inbound-trigger intake armed.",
102235
- request: { body: jsonBody(named("TriggerScopeOptionsRequest", triggerScopeOptionsRequestSchema), "The capability, the param to load, the principal whose credential loads it, and the scope drafted so far.") },
103000
+ description: "Runs the options loader of a scope param the capability flags `dynamicOptions: true` (a piece dropdown, such as WhatsApp's business number) through the credential of `principalKey`, and answers the values it may take. `principalKey` and `account` are resolved as register resolves them: on an OAuth connector `app` names no account, so the options load with the one account the connector holds, and several answer `account_required`; `account` names one of them by its grant id. Each `value` is the text a link stores under the param and is sent back as is on register: a string the piece answered stays that string, and anything else is its JSON. Writes nothing and arms nothing.\n\nPresent only on a box with an inbound-trigger intake armed.",
103001
+ request: { body: jsonBody(named("TriggerScopeOptionsRequest", triggerScopeOptionsRequestSchema), "The capability, the param to load, the principal or connected account whose credential loads it, and the scope drafted so far.") },
102236
103002
  responses: {
102237
103003
  200: jsonResponse$2(named("TriggerScopeOptions", triggerScopeOptionsResponseSchema), "The options, in the piece's order, with its `placeholder` and `disabled` when it gave them. An empty list is a legitimate answer: the account has none."),
102238
103004
  403: forbiddenResponse("config:write"),
102239
- 400: errorResponse("The body failed its schema (`invalid_payload`), the param is typed rather than filled by the provider (`not_dynamic`), or the options could not be loaded for a reason somebody has to fix first: the broker's own code when the principal's credential could not be issued (`no_token`, `user_authorization_required`, `invalid_grant`, `credential_error`), `scope_options_failed` otherwise, with the piece's message."),
103005
+ 400: errorResponse("The body failed its schema (`invalid_payload`), names a `principalKey` other than `app` beside an `account` (`invalid_args`), the param is typed rather than filled by the provider (`not_dynamic`), or the options could not be loaded for a reason somebody has to fix first: the broker's own code when the principal's credential could not be issued (`no_token`, `user_authorization_required`, `invalid_grant`, `credential_error`), `scope_options_failed` otherwise, with the piece's message."),
103006
+ 401: unknownAccountResponse("The options request"),
102240
103007
  404: errorResponse("No such trigger on that integration (`unknown_capability`), or the trigger declares no scope param by that name (`unknown_scope_param`)."),
103008
+ 409: accountRequiredResponse("The options request"),
102241
103009
  501: errorResponse("This runtime wired no options loader (`scope_options_unavailable`)."),
102242
103010
  503: errorResponse("Loading failed in a way the same request may clear later: the piece bundle or the credential issuer could not be reached, the provider answered 5xx, or it did not answer within the deadline. The code is the broker's when the credential could not be issued, `scope_options_failed` otherwise.")
102243
103011
  }
102244
103012
  }), async (c) => {
102245
103013
  const parsed = c.req.valid("json");
102246
- const { connector, trigger, param, principalKey } = parsed;
103014
+ const { connector, trigger, param } = parsed;
102247
103015
  if (!resolveTriggerPoll(deps.registry, connector, trigger)) return c.json({
102248
103016
  code: "unknown_capability",
102249
103017
  message: `No polling trigger "${trigger}" for integration "${connector}".`
@@ -102262,6 +103030,16 @@ var init_subscriptions_router = __esmMin((() => {
102262
103030
  code: "scope_options_unavailable",
102263
103031
  message: "Loading scope options is not available on this runtime."
102264
103032
  }, 501);
103033
+ let principalKey;
103034
+ try {
103035
+ principalKey = deps.resolveAccountKey ? await deps.resolveAccountKey({
103036
+ connector,
103037
+ principalKey: parsed.principalKey,
103038
+ account: parsed.account
103039
+ }) : parsed.principalKey;
103040
+ } catch (err) {
103041
+ return answerAccountSelectionRefusal(c, err);
103042
+ }
102265
103043
  const answered = await deps.scopeOptions({
102266
103044
  connector,
102267
103045
  trigger,
@@ -102323,8 +103101,8 @@ var init_subscriptions_router = __esmMin((() => {
102323
103101
  path: "/{id}",
102324
103102
  tags: [TRIGGER_SUBSCRIPTIONS_TAG],
102325
103103
  middleware: [requireCapability("config:write")],
102326
- summary: "Toggle a link, or edit its mapping or its scope in place",
102327
- description: "Editing in place — instead of delete + re-register — preserves the polling cursor and the delivery journal, so fixing a mapping does NOT re-drain the inbox. An explicit `mappingGraph: null` is the code-mode detach.\n\nA link on a piece's webhook trigger is registered at its provider while it is on. Switching it off runs `onDisable` and clears its token, so the URL the provider held answers 404; switching it on, or changing its scope, registers it again under a new token. `enabled: true` sent to one that is already on registers nothing more.\n\nAt least one of `enabled`, `mapping`, `mappingGraph`, `correlationKey`, `conversationKey`, `debounceMs` or `scope` must be present; a body with none of them is refused (`resumeFrom` alone does not count — it qualifies an arming rather than being an edit of its own). The repeatable is re-asserted against the resulting `enabled` state, so a mapping-only patch neither arms nor disarms anything.\n\nA link carries the one key its kind reads: `correlationKey` on a workflow link, `conversationKey` on an agent link. The body names no kind, so the stored link decides, and a non-blank key of the other kind is refused (`invalid_payload`).\n\n`debounceMs` (1 to 10000 ms; `null` clears it) is how long an agent link waits after an event for more events of the same conversation, so a burst of chat messages gets one answer. The stored link decides it too: a non-null wait is refused (`invalid_payload`) on a workflow link and on an agent link left with no `conversationKey`, and so is a patch that clears or blanks the `conversationKey` while a wait is still set. Clear both in the same patch.\n\n`resumeFrom` decides what an ARMING does with the events sitting above the cursor. A cursor is a bookmark, not a clock, so a link that stopped — because its connection was revoked, or because somebody switched it off — resumes where it last read and delivers everything since, at once, timestamped now. `cursor` (the default, and what every link did before this field) keeps that. `now` replaces the bookmark with one minted against the provider this instant, so the window is dropped on purpose. NEITHER APPLIES TO A LINK ARMED FOR THE FIRST TIME: registration mints no bookmark, so there is nothing to keep and the link starts from now whatever you ask for. Anything written between registering it and arming it is not delivered, and `resumeFrom: \"cursor\"` on such a link is refused (`no_cursor_to_resume_from`) rather than quietly meaning `now` (PLA-1304). The choice lives HERE and not at revoke time because at revoke time nobody knows how long the revocation will last, which is the one fact that decides it.\n\n`scope` is the other field whose edit moves the cursor. It exists so a mistyped `owner`, `repo` or folder id can be corrected without deleting the link and losing its journal. A cursor only means something inside the scope it was minted in, so changing the scope REPLACES it: a link that stays armed is reseeded \"from now\" on the new scope, a link that is off has it cleared and the patch that arms it seeds it fresh. Re-sending the scope already stored changes nothing and moves no cursor.",
103104
+ summary: "Toggle a link, or edit its mapping, scope or account in place",
103105
+ description: "Editing in place — instead of delete + re-register — preserves the link id and delivery journal. Fixing a mapping keeps the polling cursor, so it does NOT re-drain the inbox. An explicit `mappingGraph: null` is the code-mode detach.\n\nA link on a piece's webhook trigger is registered at its provider while it is on. Switching it off runs `onDisable` and clears its token, so the URL the provider held answers 404; switching it on, or changing its scope, registers it again under a new token. `enabled: true` sent to one that is already on registers nothing more.\n\nAt least one of `enabled`, `mapping`, `mappingGraph`, `correlationKey`, `conversationKey`, `debounceMs`, `scope` or `account` must be present; a body with none of them is refused (`resumeFrom` alone does not count — it qualifies an arming rather than being an edit of its own). The repeatable is re-asserted against the resulting `enabled` state, so a mapping-only patch neither arms nor disarms anything.\n\nA link carries the one key its kind reads: `correlationKey` on a workflow link, `conversationKey` on an agent link. The body names no kind, so the stored link decides, and a non-blank key of the other kind is refused (`invalid_payload`).\n\n`debounceMs` (1 to 10000 ms; `null` clears it) is how long an agent link waits after an event for more events of the same conversation, so a burst of chat messages gets one answer. The stored link decides it too: a non-null wait is refused (`invalid_payload`) on a workflow link and on an agent link left with no `conversationKey`, and so is a patch that clears or blanks the `conversationKey` while a wait is still set. Clear both in the same patch.\n\n`resumeFrom` decides what an ARMING does with the events sitting above the cursor. A cursor is a bookmark, not a clock, so a link that stopped — because its connection was revoked, or because somebody switched it off — resumes where it last read and delivers everything since, at once, timestamped now. `cursor` (the default, and what every link did before this field) keeps that. `now` replaces the bookmark with one minted against the provider this instant, so the window is dropped on purpose. NEITHER APPLIES TO A LINK ARMED FOR THE FIRST TIME: registration mints no bookmark, so there is nothing to keep and the link starts from now whatever you ask for. Anything written between registering it and arming it is not delivered, and `resumeFrom: \"cursor\"` on such a link is refused (`no_cursor_to_resume_from`) rather than quietly meaning `now` (PLA-1304). The choice lives HERE and not at revoke time because at revoke time nobody knows how long the revocation will last, which is the one fact that decides it.\n\n`scope` is the other field whose edit moves the cursor. It exists so a mistyped `owner`, `repo` or folder id can be corrected without deleting the link and losing its journal. A cursor only means something inside the scope it was minted in, so changing the scope REPLACES it: a link that stays armed is reseeded \"from now\" on the new scope, a link that is off has it cleared and the patch that arms it seeds it fresh. Re-sending the scope already stored changes nothing and moves no cursor. `account` names the connected account by its grant id, as on register. A cursor belongs to one mailbox, so moving to another account replaces it like a scope change. The id, mapping, keys and delivery journal stay in place.",
102328
103106
  request: {
102329
103107
  params: looseSubscriptionIdPath,
102330
103108
  body: jsonBody(named("PatchTriggerSubscriptionRequest", patchTriggerSubscriptionRequestSchema), "The knobs a link can change after registration. Every field is optional on its own, but the body may not be empty.")
@@ -102332,7 +103110,9 @@ var init_subscriptions_router = __esmMin((() => {
102332
103110
  responses: {
102333
103111
  200: jsonResponse$2(named("TriggerSubscriptionResponse", triggerSubscriptionResponseSchema), "The link as it now stands."),
102334
103112
  403: forbiddenResponse("config:write"),
102335
- 400: errorResponse("The body failed its schema — including the \"provide at least one field\" rule — (`invalid_payload`), it gives the link the key of the other kind (`invalid_payload`), it would leave a `debounceMs` on a workflow link or on an agent link with no `conversationKey` (`invalid_payload`), or the new JSONata does not compile (`invalid_mapping`, `invalid_correlation_key`, `invalid_conversation_key`). A patch that TURNS THE LINK ON is refused on the mapping it would leave behind when that mapping cannot fill the destination, a blank or `null` one judged on the default its kind delivers, as on register (`mapping_evaluation_failed`, `mapping_cannot_produce_input` — `details.mappingIssues` names the offending paths), and when a piece trigger's own `onEnable` fails (`trigger_enable_failed`, with the piece's message, or the broker's own code when the link's credential could not be minted), or when a piece's webhook trigger has no public URL to register (`webhook_url_unavailable`). A piece's webhook link that has received no event yet is not judged on its mapping; from its first event on, every save that leaves it armed is judged on that event. A `scope` edit is refused on the same two checks the register path runs (`scope_required`, `invalid_scope`), and — when the link stays armed — on the connection fence the new scope resolves onto (`operation_not_permitted`) and on a connector no request could reach (`connector_installation_required`). `resumeFrom` on a body that does not turn the link on is `invalid_payload`, `resumeFrom: \"cursor\"` on a link that has never polled is `no_cursor_to_resume_from` (it has no bookmark to resume from), and `resumeFrom: \"now\"` when no cursor can be minted for \"now\" is `cursor_seed_failed` — clearing the stored one instead would replay the provider's whole history. Nothing is written, `enabled` is left untouched, the previous expression stands and the cursor is not moved."),
103113
+ 400: errorResponse("The body failed its schema — including the \"provide at least one field\" rule — (`invalid_payload`), account selection is invalid (`invalid_args`), it gives the link the key of the other kind (`invalid_payload`), it would leave a `debounceMs` on a workflow link or on an agent link with no `conversationKey` (`invalid_payload`), or the new JSONata does not compile (`invalid_mapping`, `invalid_correlation_key`, `invalid_conversation_key`). A patch that TURNS THE LINK ON is refused on the mapping it would leave behind when that mapping cannot fill the destination, a blank or `null` one judged on the default its kind delivers, as on register (`mapping_evaluation_failed`, `mapping_cannot_produce_input` — `details.mappingIssues` names the offending paths), and when a piece trigger's own `onEnable` fails (`trigger_enable_failed`, with the piece's message, or the broker's own code when the link's credential could not be minted), or when a piece's webhook trigger has no public URL to register (`webhook_url_unavailable`). A piece's webhook link that has received no event yet is not judged on its mapping; from its first event on, every save that leaves it armed is judged on that event. A `scope` edit is refused on the same two checks the register path runs (`scope_required`, `invalid_scope`), and — when the link stays armed — on the connection fence the new scope resolves onto (`operation_not_permitted`) and on a connector no request could reach (`connector_installation_required`). An account change runs those connection checks against the new account too. `resumeFrom` on a body that does not turn the link on is `invalid_payload`, `resumeFrom: \"cursor\"` on a link that has never polled is `no_cursor_to_resume_from` (it has no bookmark to resume from), and `resumeFrom: \"now\"` when no cursor can be minted for \"now\" is `cursor_seed_failed` — clearing the stored one instead would replay the provider's whole history. Nothing is written, `enabled` is left untouched, the previous expression stands and the cursor is not moved."),
103114
+ 401: unknownAccountResponse("The link"),
103115
+ 409: accountRequiredResponse("The link"),
102336
103116
  404: errorResponse("This box holds no link with that id (`not_found`)."),
102337
103117
  503: ENABLE_RETRYABLE_RESPONSE
102338
103118
  }
@@ -102344,7 +103124,8 @@ var init_subscriptions_router = __esmMin((() => {
102344
103124
  const editingMapping = parsed.mapping !== void 0 || parsed.mappingGraph !== void 0;
102345
103125
  const editingScope = parsed.scope !== void 0;
102346
103126
  const disarming = parsed.enabled === false;
102347
- const needsRow = arming || editingMapping || editingScope || disarming;
103127
+ const editingAccount = parsed.account !== void 0;
103128
+ const needsRow = arming || editingMapping || editingScope || editingAccount || disarming;
102348
103129
  const stored = needsRow || parsed.correlationKey?.trim() || editingWaitOrKey(parsed) ? await deps.store.get(c.req.param("id")) : null;
102349
103130
  const keysRefusal = stored ? patchKeysRefusal(stored, parsed) : null;
102350
103131
  if (keysRefusal) return c.json({
@@ -102358,6 +103139,18 @@ var init_subscriptions_router = __esmMin((() => {
102358
103139
  message: "\"resumeFrom\" only applies to a patch that turns the link on. Send it together with \"enabled\": true."
102359
103140
  }, 400);
102360
103141
  const existing = needsRow ? stored : null;
103142
+ const accountSelection = existing && editingAccount ? {
103143
+ connector: existing.connector,
103144
+ principalKey: "app",
103145
+ account: parsed.account
103146
+ } : void 0;
103147
+ let principalKey = existing?.principalKey ?? "app";
103148
+ if (accountSelection && deps.resolveAccountKey) try {
103149
+ principalKey = await deps.resolveAccountKey(accountSelection);
103150
+ } catch (err) {
103151
+ return answerAccountSelectionRefusal(c, err);
103152
+ }
103153
+ const accountChanged = existing !== null && principalKey !== existing.principalKey;
102361
103154
  const trimmed = editingScope ? trimmedScope(parsed.scope) : void 0;
102362
103155
  const scope = existing && trimmed !== void 0 ? withGeneratedVerifyToken(deps, existing, trimmed) : trimmed;
102363
103156
  if (existing && scope !== void 0) {
@@ -102377,12 +103170,13 @@ var init_subscriptions_router = __esmMin((() => {
102377
103170
  targetKind: existing.targetKind,
102378
103171
  targetName: existing.targetName,
102379
103172
  mapping: parsed.mapping !== void 0 ? parsed.mapping ?? null : existing.mapping,
103173
+ principalKey,
102380
103174
  sampleEvent: existing.sampleEvent
102381
103175
  });
102382
103176
  if (armingRefusal) return c.json(armingRefusal, 400);
102383
103177
  }
102384
103178
  const effectiveScope = scope !== void 0 ? scope : existing?.scope ?? null;
102385
- const rescoping = scopeChanged && willBeArmed;
103179
+ const rescoping = (scopeChanged || accountChanged) && willBeArmed;
102386
103180
  if ((arming || rescoping) && existing) {
102387
103181
  const unreachable = await unreachableConnectorRefusal(deps, existing.connector);
102388
103182
  if (unreachable) return c.json(unreachable, 400);
@@ -102405,6 +103199,7 @@ var init_subscriptions_router = __esmMin((() => {
102405
103199
  if (triggerColumnCursor(deps.registry, existing.connector, existing.trigger) === null) {
102406
103200
  seed = await armingCursorSeed(deps, {
102407
103201
  ...existing,
103202
+ principalKey,
102408
103203
  scope: effectiveScope
102409
103204
  });
102410
103205
  if (webhook) {
@@ -102415,7 +103210,7 @@ var init_subscriptions_router = __esmMin((() => {
102415
103210
  const enabled = await armingEnable(deps, {
102416
103211
  connector: existing.connector,
102417
103212
  trigger: existing.trigger,
102418
- principalKey: existing.principalKey,
103213
+ principalKey,
102419
103214
  scope: effectiveScope,
102420
103215
  ...arm ? { webhookUrl: arm.url } : {}
102421
103216
  });
@@ -102425,7 +103220,7 @@ var init_subscriptions_router = __esmMin((() => {
102425
103220
  const prepared = await deps.prepareColumnCursor({
102426
103221
  connector: existing.connector,
102427
103222
  trigger: existing.trigger,
102428
- principalKey: existing.principalKey,
103223
+ principalKey,
102429
103224
  scope: effectiveScope,
102430
103225
  cursorColumn: existing.cursorColumn ?? null
102431
103226
  });
@@ -102442,10 +103237,11 @@ var init_subscriptions_router = __esmMin((() => {
102442
103237
  message: "This link cannot be started \"from now\": no cursor can be minted for it right now, and clearing the stored one would replay the provider's whole history. It stays where it was — retry, or arm it with \"resumeFrom\": \"cursor\"."
102443
103238
  }, 400);
102444
103239
  const disarm = disarming || rescoping ? await disarmingHook(deps, existing) : null;
102445
- const subscription = await deps.store.update(c.req.param("id"), {
103240
+ const updated = await deps.store.update(c.req.param("id"), {
103241
+ ...accountChanged ? { principalKey } : {},
102446
103242
  ...parsed.enabled !== void 0 ? { enabled: parsed.enabled } : {},
102447
103243
  ...scope !== void 0 ? { scope } : {},
102448
- ...scopeChanged || skippingBacklog || arm ? { resetCursorTo: seed } : seed !== null ? { seedCursorIfUnset: seed } : {},
103244
+ ...scopeChanged || accountChanged || skippingBacklog || arm ? { resetCursorTo: seed } : seed !== null ? { seedCursorIfUnset: seed } : {},
102449
103245
  ...arm ? { webhookToken: arm.token } : webhook && disarming ? { webhookToken: null } : {},
102450
103246
  ...parsed.mapping !== void 0 ? { mapping: parsed.mapping } : {},
102451
103247
  ...parsed.mappingGraph !== void 0 ? { mappingGraph: parsed.mappingGraph } : {},
@@ -102453,10 +103249,11 @@ var init_subscriptions_router = __esmMin((() => {
102453
103249
  ...parsed.conversationKey !== void 0 ? { conversationKey: parsed.conversationKey } : {},
102454
103250
  ...parsed.debounceMs !== void 0 ? { debounceMs: parsed.debounceMs } : {}
102455
103251
  });
102456
- if (!subscription) return c.json({
103252
+ if (!updated) return c.json({
102457
103253
  code: "not_found",
102458
103254
  message: "Trigger link not found."
102459
103255
  }, 404);
103256
+ const subscription = accountSelection && accountChanged ? await followAccountMove(deps.resolveAccountKey, accountSelection, updated, (id, key) => deps.store.update(id, { principalKey: key })) : updated;
102460
103257
  const action = parsed.enabled === void 0 ? "trigger.update" : parsed.enabled ? "trigger.enable" : "trigger.disable";
102461
103258
  await recordCompleted(deps.audit, {
102462
103259
  action,
@@ -103187,6 +103984,8 @@ var init_destinations_router = __esmMin((() => {
103187
103984
  init_actor();
103188
103985
  init_route_helpers();
103189
103986
  init_api_docs();
103987
+ init_broker_refusal();
103988
+ init_resolve_account();
103190
103989
  init_support();
103191
103990
  init_output_writer();
103192
103991
  init_action_caller();
@@ -103329,17 +104128,39 @@ var init_destinations_router = __esmMin((() => {
103329
104128
  path: "/bindings",
103330
104129
  tags: [DESTINATIONS_TAG],
103331
104130
  summary: "Bind a declared output to a destination",
103332
- description: "Decides how one of a workflow's declarations reaches the outside. WHICH rules apply comes from what the workspace declared the name as, never from the body: a `file` needs a connector that can file and a destination it accepts; an `action` needs a connector this box knows and NO destination, because the operator picks the connection and nothing else. A name nothing declares takes the file rules.\n\nAn UPSERT on the (workflow, output) pair: binding an output that is already bound replaces the binding and keeps its id, because the later decision is the one that holds. `principalKey` defaults to `app`.\n\nThe connector must be one `GET /api/destinations/capabilities` lists — a name this box does not know, or one with no file writer, is refused here rather than days later on a run nobody is watching — and `principalKey` must be a grant the broker can resolve.\n\n`destination` is stored as the operator sent it; the one shape rule is an `id` string, which is what the connector's writer receives. Every other key is display metadata the box never reads.",
104131
+ description: "Decides how one of a workflow's declarations reaches the outside. WHICH rules apply comes from what the workspace declared the name as, never from the body: a `file` needs a connector that can file and a destination it accepts; an `action` needs a connector this box knows and NO destination, because the operator picks the connection and nothing else. A name nothing declares takes the file rules.\n\nAn UPSERT on the (workflow, output) pair: binding an output that is already bound replaces the binding and keeps its id, because the later decision is the one that holds. `principalKey` defaults to `app`. On an OAuth connector `app` names no account: the binding is stored under the key of the one account the connector holds, and several answer `account_required`. `account` names one of them by its grant id, as `account_required` lists them, and the binding is stored under that account's key.\n\nThe connector must be one `GET /api/destinations/capabilities` lists — a name this box does not know, or one with no file writer, is refused here rather than days later on a run nobody is watching — and `principalKey` must be a grant the broker can resolve.\n\n`destination` is stored as the operator sent it; the one shape rule is an `id` string, which is what the connector's writer receives. Every other key is display metadata the box never reads.",
103333
104132
  request: { body: jsonBody(named("UpsertDestinationBindingRequest", upsertDestinationBindingRequestSchema), "The output to bind, the connector and grant to write through, and where.") },
103334
104133
  responses: {
103335
104134
  201: jsonResponse$2(named("DestinationBindingResponse", destinationBindingResponseSchema), "The binding as it was stored."),
103336
- 400: errorResponse("The body failed its schema (`invalid_payload`), names a connector this box does not know or that cannot file a document (`unknown_connector`, `connector_cannot_file`), names a grant the broker cannot resolve (`invalid_principal_key`), or sends a destination the declaration does not accept — missing or malformed for a `file` (`destination_required`, `invalid_destination`), present at all for an `action` (`destination_not_accepted`).")
104135
+ 400: errorResponse("The body failed its schema (`invalid_payload`), names a connector this box does not know or that cannot file a document (`unknown_connector`, `connector_cannot_file`), names a grant the broker cannot resolve (`invalid_principal_key`), names a `principalKey` other than `app` beside an `account` (`invalid_args`), or sends a destination the declaration does not accept — missing or malformed for a `file` (`destination_required`, `invalid_destination`), present at all for an `action` (`destination_not_accepted`)."),
104136
+ 401: unknownAccountResponse("The binding"),
104137
+ 409: accountRequiredResponse("The binding")
103337
104138
  }
103338
104139
  }), async (c) => {
103339
104140
  const parsed = c.req.valid("json");
103340
104141
  const refusal = deps.declaredKind?.(parsed.workflowName, parsed.outputName) === "action" ? unknownConnectorRefusal(deps.definitions, parsed.connector) ?? principalKeyRefusal(parsed.principalKey) ?? actionDestinationRefusal(parsed.destination) : connectorRefusal(deps.definitions, parsed.connector) ?? principalKeyRefusal(parsed.principalKey) ?? destinationRefusal(deps.definitions, parsed.connector, parsed.destination);
103341
104142
  if (refusal) return c.json(refusal, 400);
103342
- const binding = await deps.store.upsertBinding(parsed);
104143
+ let principalKey;
104144
+ try {
104145
+ principalKey = deps.resolveAccountKey ? await deps.resolveAccountKey({
104146
+ connector: parsed.connector,
104147
+ principalKey: parsed.principalKey,
104148
+ account: parsed.account
104149
+ }) : parsed.principalKey;
104150
+ } catch (err) {
104151
+ return answerAccountSelectionRefusal(c, err);
104152
+ }
104153
+ const binding = await followAccountMove(deps.resolveAccountKey, {
104154
+ connector: parsed.connector,
104155
+ principalKey: parsed.principalKey,
104156
+ account: parsed.account
104157
+ }, await deps.store.upsertBinding({
104158
+ workflowName: parsed.workflowName,
104159
+ outputName: parsed.outputName,
104160
+ connector: parsed.connector,
104161
+ principalKey,
104162
+ destination: parsed.destination
104163
+ }), (id, key) => deps.store.updateBinding(id, { principalKey: key }));
103343
104164
  await recordCompleted(deps.audit, {
103344
104165
  action: "destination.bind",
103345
104166
  actorUserId: readActorUserId(c),
@@ -103368,15 +104189,17 @@ var init_destinations_router = __esmMin((() => {
103368
104189
  path: "/bindings/{id}",
103369
104190
  tags: [DESTINATIONS_TAG],
103370
104191
  summary: "Move a bound output, or re-point it at another connection",
103371
- description: "Edits the destination, the connector or the grant in place; the (workflow, output) pair a binding is keyed by never changes. At least one of `connector`, `principalKey` or `destination` must be present.",
104192
+ description: "Edits the destination, the connector or the grant in place; the (workflow, output) pair a binding is keyed by never changes. At least one of `connector`, `principalKey`, `account` or `destination` must be present. A re-point resolves the account the way the bind does: `app` on an OAuth connector names no account, and `account` names one by its grant id, stored under that account's key.",
103372
104193
  request: {
103373
104194
  params: looseBindingIdPath,
103374
104195
  body: jsonBody(named("PatchDestinationBindingRequest", patchDestinationBindingRequestSchema), "The fields to change.")
103375
104196
  },
103376
104197
  responses: {
103377
104198
  200: jsonResponse$2(named("DestinationBindingResponse", destinationBindingResponseSchema), "The binding after the edit."),
103378
- 400: errorResponse("The body failed its schema (`invalid_payload`), carried none of the three editable fields, named a connector this box cannot file through (`unknown_connector`, `connector_cannot_file`), named a grant the broker cannot resolve (`invalid_principal_key`), or sent a destination the declaration does not accept (`destination_required`, `invalid_destination`, `destination_not_accepted`)."),
103379
- 404: errorResponse("This box holds no binding with that id (`not_found`).")
104199
+ 400: errorResponse("The body failed its schema (`invalid_payload`), carried none of the four editable fields, named a connector this box cannot file through (`unknown_connector`, `connector_cannot_file`), named a grant the broker cannot resolve (`invalid_principal_key`), named a `principalKey` other than `app` beside an `account` (`invalid_args`), or sent a destination the declaration does not accept (`destination_required`, `invalid_destination`, `destination_not_accepted`)."),
104200
+ 401: unknownAccountResponse("The re-point"),
104201
+ 404: errorResponse("This box holds no binding with that id (`not_found`)."),
104202
+ 409: accountRequiredResponse("The re-point")
103380
104203
  }
103381
104204
  }), async (c) => {
103382
104205
  const patch = c.req.valid("json");
@@ -103385,12 +104208,24 @@ var init_destinations_router = __esmMin((() => {
103385
104208
  const connector = patch.connector ?? existing?.connector;
103386
104209
  const fieldRefusal = (patch.connector === void 0 ? null : kind === "action" ? unknownConnectorRefusal(deps.definitions, patch.connector) : connectorRefusal(deps.definitions, patch.connector)) ?? (patch.principalKey !== void 0 ? principalKeyRefusal(patch.principalKey) : null) ?? (patch.destination === void 0 ? null : kind === "action" ? actionDestinationRefusal(patch.destination) : connector === void 0 ? null : destinationRefusal(deps.definitions, connector, patch.destination));
103387
104210
  if (fieldRefusal) return c.json(fieldRefusal, 400);
103388
- const binding = await deps.store.updateBinding(c.req.param("id"), {
104211
+ let principalKey = patch.principalKey;
104212
+ const accountSelection = existing && (patch.connector !== void 0 && patch.connector !== existing.connector || patch.principalKey !== void 0 || patch.account !== void 0) ? {
104213
+ connector: connector ?? existing.connector,
104214
+ principalKey: patch.principalKey ?? (patch.account !== void 0 ? "app" : existing.principalKey),
104215
+ account: patch.account
104216
+ } : void 0;
104217
+ if (accountSelection && deps.resolveAccountKey) try {
104218
+ principalKey = await deps.resolveAccountKey(accountSelection);
104219
+ } catch (err) {
104220
+ return answerAccountSelectionRefusal(c, err);
104221
+ }
104222
+ const updated = await deps.store.updateBinding(c.req.param("id"), {
103389
104223
  ...patch.connector !== void 0 ? { connector: patch.connector } : {},
103390
- ...patch.principalKey !== void 0 ? { principalKey: patch.principalKey } : {},
104224
+ ...principalKey !== void 0 ? { principalKey } : {},
103391
104225
  ...patch.destination !== void 0 ? { destination: patch.destination } : {}
103392
104226
  });
103393
- if (!binding) return c.json(NOT_FOUND, 404);
104227
+ if (!updated) return c.json(NOT_FOUND, 404);
104228
+ const binding = accountSelection ? await followAccountMove(deps.resolveAccountKey, accountSelection, updated, (id, key) => deps.store.updateBinding(id, { principalKey: key })) : updated;
103394
104229
  await recordCompleted(deps.audit, {
103395
104230
  action: "destination.rebind",
103396
104231
  actorUserId: readActorUserId(c),
@@ -103594,6 +104429,7 @@ var init_runtime_subsystems = __esmMin((() => {
103594
104429
  ...intake.connectorRows ? { connectorRows: intake.connectorRows } : {},
103595
104430
  ...intake.specCache ? { specCache: intake.specCache } : {},
103596
104431
  ...intake.orgId !== void 0 ? { orgId: intake.orgId } : {},
104432
+ ...intake.connectedAccountOf ? { connectedAccountOf: intake.connectedAccountOf } : {},
103597
104433
  startDeps: ctx.startDeps,
103598
104434
  startAgent: ctx.startAgent,
103599
104435
  registry: intake.registry,
@@ -103616,6 +104452,8 @@ var init_runtime_subsystems = __esmMin((() => {
103616
104452
  webhookUrl: intakeRuntime.webhookUrl,
103617
104453
  prepareColumnCursor: intakeRuntime.prepareColumnCursor,
103618
104454
  ...intake.connectorRows ? { connectorRows: intake.connectorRows } : {},
104455
+ ...intake.resolveAccountKey ? { resolveAccountKey: intake.resolveAccountKey } : {},
104456
+ ...intake.connectedAccountOf ? { connectedAccountOf: intake.connectedAccountOf } : {},
103619
104457
  audit: intake.audit,
103620
104458
  db: intake.db,
103621
104459
  resolveTargetInputSchema: createTargetInputSchemaResolver({
@@ -103652,7 +104490,8 @@ var init_runtime_subsystems = __esmMin((() => {
103652
104490
  audit: intake.audit,
103653
104491
  db: intake.db,
103654
104492
  definitions: intake.registry,
103655
- ...destinations.declaredKind ? { declaredKind: destinations.declaredKind } : {}
104493
+ ...destinations.declaredKind ? { declaredKind: destinations.declaredKind } : {},
104494
+ ...intake.resolveAccountKey ? { resolveAccountKey: intake.resolveAccountKey } : {}
103656
104495
  }));
103657
104496
  }
103658
104497
  try {
@@ -104523,6 +105362,8 @@ var createWorkflowRuntimeBoot = (deps) => {
104523
105362
  const issueCredentialMaterial = deps.connectIssueCredentialMaterial?.();
104524
105363
  const pieceTriggers = deps.connectPieceTriggers?.();
104525
105364
  const connectorRows = deps.connectConnectorRows();
105365
+ const resolveAccountKey = deps.connectResolveAccountKey?.();
105366
+ const connectedAccountOf = deps.connectConnectedAccountOf?.();
104526
105367
  const connectSpecCache = deps.connectSpecCache();
104527
105368
  const connectAgentWire = deps.connectAgentWire();
104528
105369
  workflowRuntimeHandle = await mountWorkflowRuntime(target, {
@@ -104561,6 +105402,8 @@ var createWorkflowRuntimeBoot = (deps) => {
104561
105402
  ...pieceTriggers ? { pieceTriggers } : {},
104562
105403
  registry: deps.connectRegistry,
104563
105404
  ...connectorRows ? { connectorRows } : {},
105405
+ ...resolveAccountKey ? { resolveAccountKey } : {},
105406
+ ...connectedAccountOf ? { connectedAccountOf } : {},
104564
105407
  ...connectSpecCache ? { specCache: connectSpecCache } : {},
104565
105408
  orgId: CONNECT_BOX_ORG_ID,
104566
105409
  audit: deps.audit,
@@ -105089,6 +105932,8 @@ var bootEmulatorServer = async (opts, ledger) => {
105089
105932
  connectIssueCredentialMaterial: () => captured.connectIssueCredentialMaterial,
105090
105933
  connectPieceTriggers: () => seededOpts.connectPieceTriggers,
105091
105934
  connectConnectorRows: () => captured.connectConnectorRows,
105935
+ connectResolveAccountKey: () => captured.connectResolveAccountKey,
105936
+ connectConnectedAccountOf: () => captured.connectConnectedAccountOf,
105092
105937
  connectSpecCache: () => captured.connectSpecCache,
105093
105938
  connectAgentWire: () => captured.connectAgentWire,
105094
105939
  recurringJobExecutions: cadences.executions,
@@ -105641,7 +106486,7 @@ var calls_workflow_ts_default = "import { z } from \"zod\";\nimport { streamDeep
105641
106486
  var plain_workflow_ts_default = "import { z } from \"zod\";\n\n/**\n * Workflow \"{{name}}\" — a durable pipeline.\n *\n * Each `\"use step\"` is a durable checkpoint: kill the runtime mid-run and it\n * resumes from the last completed step instead of replaying everything.\n */\n\n// THE contract — sibling exports next to the bare workflow function. The build\n// harvests these (by convention) into the manifest + a live validator. The\n// parameter type is DERIVED from `inputSchema` and the RETURN type from\n// `outputSchema` (both `z.infer`), so neither side can drift from the contract\n// the build harvests. Without the return annotation the schema is published to\n// Studio and the generated types and never compared to what the body returns\n// (PLA-1272).\nexport const inputSchema = z.object({\n message: z.string().describe(\"The input to process\"),\n});\n\nexport const outputSchema = z.object({\n message: z.string(),\n processed: z.boolean(),\n});\n\ntype {{Name}}Input = z.infer<typeof inputSchema>;\n\nexport async function {{export}}(input: {{Name}}Input): Promise<z.infer<typeof outputSchema>> {\n \"use workflow\";\n\n const result = await processMessage(input); // step 1 — durable checkpoint\n return result;\n}\n\nasync function processMessage(input: {{Name}}Input) {\n \"use step\";\n // Replace this with the deterministic work of your step (DB write, HTTP call,\n // an LLM call, …). Returning a value records it as the step's checkpoint.\n return { message: input.message, processed: true };\n}\n";
105642
106487
  //#endregion
105643
106488
  //#region src/services/scaffold/template-loader.ts
105644
- var SDK_TEMPLATE_VERSION = "0.10.4";
106489
+ var SDK_TEMPLATE_VERSION = "0.10.5";
105645
106490
  var PREFIX = "./templates/";
105646
106491
  var templates = new Map(Object.entries(/* @__PURE__ */ Object.assign({
105647
106492
  "./templates/deep-agents/index.ts.tmpl": index_ts_default,