@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/CHANGELOG.md +19 -0
- package/main.js +1180 -335
- package/package.json +1 -1
- package/stackbone-cli-0.9.5.tgz +0 -0
- package/stackbone-cli-0.9.4.tgz +0 -0
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$
|
|
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$
|
|
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$
|
|
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 \"
|
|
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 }`.
|
|
17448
|
-
* defaults to `{ type: 'app' }`; `
|
|
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
|
|
17467
|
-
|
|
17468
|
-
|
|
17469
|
-
|
|
17470
|
-
|
|
17471
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
18741
|
-
|
|
18742
|
-
|
|
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
|
-
|
|
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)
|
|
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) =>
|
|
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(() =>
|
|
19001
|
-
url: () => (downloadingFrom ?? livePage()).url(),
|
|
19002
|
-
title: () => afterDownloads(() =>
|
|
19003
|
-
screenshot: (opts) => afterDownloads(() =>
|
|
19004
|
-
act: (opts) =>
|
|
19005
|
-
const
|
|
19006
|
-
|
|
19007
|
-
|
|
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
|
-
|
|
19010
|
-
|
|
19011
|
-
|
|
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 =
|
|
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.
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
39811
|
+
if (!isRecord$5(resolved)) return resolved;
|
|
39631
39812
|
const items = resolved["items"];
|
|
39632
|
-
if (isRecord$
|
|
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$
|
|
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$
|
|
39902
|
+
if (!isRecord$5(requestBody)) return void 0;
|
|
39722
39903
|
const content = requestBody["content"];
|
|
39723
|
-
if (!isRecord$
|
|
39904
|
+
if (!isRecord$5(content)) return void 0;
|
|
39724
39905
|
const json = content["application/json"];
|
|
39725
|
-
if (isRecord$
|
|
39726
|
-
for (const mediaType of Object.values(content)) if (isRecord$
|
|
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$
|
|
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$
|
|
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$
|
|
39930
|
+
if (!isRecord$5(content)) continue;
|
|
39750
39931
|
const json = content["application/json"];
|
|
39751
|
-
if (isRecord$
|
|
39752
|
-
for (const mediaType of Object.values(content)) if (isRecord$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
42429
|
-
if (!isRecord$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
42740
|
+
if (!isRecord$4(item)) continue;
|
|
42556
42741
|
for (const method of HTTP_METHODS$1) {
|
|
42557
42742
|
const doc = item[method];
|
|
42558
|
-
if (!isRecord$
|
|
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
|
-
|
|
44023
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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:
|
|
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.
|
|
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:
|
|
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
|
|
73315
|
-
*
|
|
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:
|
|
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":
|
|
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
|
-
|
|
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
|
|
76869
|
-
* same one the `/execute` executor uses: introspecting a
|
|
76870
|
-
* reading a provider's own metadata, never an end user
|
|
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
|
|
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
|
-
|
|
77231
|
-
|
|
77232
|
-
|
|
77233
|
-
principal
|
|
77234
|
-
}
|
|
77235
|
-
|
|
77236
|
-
|
|
77237
|
-
}
|
|
77238
|
-
|
|
77239
|
-
|
|
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
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
78352
|
+
principal
|
|
77636
78353
|
}, {
|
|
77637
78354
|
connector: request.connector,
|
|
77638
|
-
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
|
|
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
|
|
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
|
|
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,
|
|
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.
|
|
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
|
|
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
|
|
102327
|
-
description: "Editing in place — instead of delete + re-register — preserves the
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
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
|
|
103379
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
104224
|
+
...principalKey !== void 0 ? { principalKey } : {},
|
|
103391
104225
|
...patch.destination !== void 0 ? { destination: patch.destination } : {}
|
|
103392
104226
|
});
|
|
103393
|
-
if (!
|
|
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.
|
|
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,
|