@stackbone/cli 0.2.7 → 0.2.8
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 +47 -0
- package/main.js +1005 -406
- package/package.json +2 -1
- package/stackbone-cli-0.2.8.tgz +0 -0
- package/stackbone-cli-0.2.7.tgz +0 -0
package/main.js
CHANGED
|
@@ -27,6 +27,7 @@ import { Hono } from "hono";
|
|
|
27
27
|
import { cors } from "hono/cors";
|
|
28
28
|
import Ajv from "ajv/dist/2020.js";
|
|
29
29
|
import addFormats from "ajv-formats";
|
|
30
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
30
31
|
import { streamSSE } from "hono/streaming";
|
|
31
32
|
import PgQueryFactory from "pg-query-emscripten";
|
|
32
33
|
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
|
@@ -473,7 +474,7 @@ function requireVersion(contract) {
|
|
|
473
474
|
* never falls through to a misleading "capability missing" message when the
|
|
474
475
|
* actual problem is a stale datapath. Pure, never throws.
|
|
475
476
|
*/
|
|
476
|
-
function requireCapability(capability, contract) {
|
|
477
|
+
function requireCapability$1(capability, contract) {
|
|
477
478
|
const versionCheck = requireVersion(contract);
|
|
478
479
|
if (versionCheck.error) return versionCheck;
|
|
479
480
|
if (!contract.capabilities.includes(capability)) return {
|
|
@@ -507,6 +508,7 @@ var init_contract = __esmMin((() => {
|
|
|
507
508
|
"ai.openrouter",
|
|
508
509
|
"prompts.basic"
|
|
509
510
|
];
|
|
511
|
+
new Set(CAPABILITIES);
|
|
510
512
|
MODULE_CAPABILITIES = {
|
|
511
513
|
database: "database.postgres_direct",
|
|
512
514
|
rag: "rag.basic",
|
|
@@ -522,7 +524,7 @@ var init_contract = __esmMin((() => {
|
|
|
522
524
|
contractResponseSchema = z.object({
|
|
523
525
|
version: z.number().int().min(1),
|
|
524
526
|
minSupported: z.number().int().min(1),
|
|
525
|
-
capabilities: z.array(z.
|
|
527
|
+
capabilities: z.array(z.string()),
|
|
526
528
|
build: z.object({
|
|
527
529
|
name: contractBuildNameSchema,
|
|
528
530
|
version: z.string().min(1)
|
|
@@ -2777,7 +2779,7 @@ var init_contract_handshake = __esmMin((() => {
|
|
|
2777
2779
|
*/
|
|
2778
2780
|
function assertCapability(module, contract) {
|
|
2779
2781
|
const capability = MODULE_CAPABILITIES[module];
|
|
2780
|
-
const result = requireCapability(capability, contract);
|
|
2782
|
+
const result = requireCapability$1(capability, contract);
|
|
2781
2783
|
if (result.error) return err$1(result.error);
|
|
2782
2784
|
return {
|
|
2783
2785
|
data: void 0,
|
|
@@ -5503,11 +5505,11 @@ function isUnsupportedModelError(error) {
|
|
|
5503
5505
|
if (error.code === "ai_validation_error") return true;
|
|
5504
5506
|
return false;
|
|
5505
5507
|
}
|
|
5506
|
-
var DEFAULT_BATCH_SIZE$1, DEFAULT_CONCURRENCY, Embedder;
|
|
5508
|
+
var DEFAULT_BATCH_SIZE$1, DEFAULT_CONCURRENCY$1, Embedder;
|
|
5507
5509
|
var init_embedder = __esmMin((() => {
|
|
5508
5510
|
init_result$1();
|
|
5509
5511
|
DEFAULT_BATCH_SIZE$1 = 128;
|
|
5510
|
-
DEFAULT_CONCURRENCY = 4;
|
|
5512
|
+
DEFAULT_CONCURRENCY$1 = 4;
|
|
5511
5513
|
Embedder = class {
|
|
5512
5514
|
constructor(options) {
|
|
5513
5515
|
const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE$1;
|
|
@@ -5515,7 +5517,7 @@ var init_embedder = __esmMin((() => {
|
|
|
5515
5517
|
this._ai = options.ai;
|
|
5516
5518
|
this._model = options.model;
|
|
5517
5519
|
this._batchSize = batchSize;
|
|
5518
|
-
this._concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
|
|
5520
|
+
this._concurrency = options.concurrency ?? DEFAULT_CONCURRENCY$1;
|
|
5519
5521
|
}
|
|
5520
5522
|
get model() {
|
|
5521
5523
|
return this._model;
|
|
@@ -8744,6 +8746,169 @@ var createSessionsStore = (opts) => {
|
|
|
8744
8746
|
};
|
|
8745
8747
|
};
|
|
8746
8748
|
//#endregion
|
|
8749
|
+
//#region ../../libs/runtime/workspace-runtime/src/lib/connect-inbound/trigger-subscriptions-store.ts
|
|
8750
|
+
init_store_pool();
|
|
8751
|
+
init_uuid();
|
|
8752
|
+
var SUBSCRIPTION_COLUMNS = "id, connector, trigger_id, principal_key, workflow_name, mapping, mapping_graph, correlation_key, enabled, mailbox";
|
|
8753
|
+
var rowToSubscription = (row) => ({
|
|
8754
|
+
id: row.id,
|
|
8755
|
+
connector: row.connector,
|
|
8756
|
+
trigger: row.trigger_id,
|
|
8757
|
+
principalKey: row.principal_key,
|
|
8758
|
+
workflowName: row.workflow_name,
|
|
8759
|
+
mapping: row.mapping,
|
|
8760
|
+
mappingGraph: row.mapping_graph ?? null,
|
|
8761
|
+
correlationKey: row.correlation_key ?? null,
|
|
8762
|
+
enabled: row.enabled,
|
|
8763
|
+
mailbox: row.mailbox ?? null
|
|
8764
|
+
});
|
|
8765
|
+
var createTriggerSubscriptionsStore = (opts) => {
|
|
8766
|
+
const { pool, assertOpen, close } = createStorePool("TriggerSubscriptionsStore", opts.connectionString);
|
|
8767
|
+
return {
|
|
8768
|
+
async register(input) {
|
|
8769
|
+
assertOpen();
|
|
8770
|
+
const id = randomUUID();
|
|
8771
|
+
const { rows } = await pool.query(`INSERT INTO stackbone_platform.trigger_subscriptions
|
|
8772
|
+
(id, connector, trigger_id, principal_key, workflow_name, mapping, mapping_graph, correlation_key, enabled, mailbox)
|
|
8773
|
+
VALUES ($1::uuid, $2, $3, $4, $5, $6, $7::jsonb, $8, $9, $10)
|
|
8774
|
+
RETURNING ${SUBSCRIPTION_COLUMNS}`, [
|
|
8775
|
+
id,
|
|
8776
|
+
input.connector,
|
|
8777
|
+
input.trigger,
|
|
8778
|
+
input.principalKey,
|
|
8779
|
+
input.workflowName,
|
|
8780
|
+
input.mapping ?? null,
|
|
8781
|
+
input.mappingGraph == null ? null : JSON.stringify(input.mappingGraph),
|
|
8782
|
+
input.correlationKey ?? null,
|
|
8783
|
+
input.enabled ?? true,
|
|
8784
|
+
input.mailbox ?? null
|
|
8785
|
+
]);
|
|
8786
|
+
return rowToSubscription(rows[0]);
|
|
8787
|
+
},
|
|
8788
|
+
async list() {
|
|
8789
|
+
assertOpen();
|
|
8790
|
+
const { rows } = await pool.query(`SELECT ${SUBSCRIPTION_COLUMNS}
|
|
8791
|
+
FROM stackbone_platform.trigger_subscriptions
|
|
8792
|
+
ORDER BY created_at DESC, id DESC`);
|
|
8793
|
+
return rows.map(rowToSubscription);
|
|
8794
|
+
},
|
|
8795
|
+
async get(id) {
|
|
8796
|
+
if (!isUuid(id)) return null;
|
|
8797
|
+
assertOpen();
|
|
8798
|
+
const { rows } = await pool.query(`SELECT ${SUBSCRIPTION_COLUMNS}
|
|
8799
|
+
FROM stackbone_platform.trigger_subscriptions
|
|
8800
|
+
WHERE id = $1::uuid`, [id]);
|
|
8801
|
+
return rows[0] ? rowToSubscription(rows[0]) : null;
|
|
8802
|
+
},
|
|
8803
|
+
async update(id, patch) {
|
|
8804
|
+
if (!isUuid(id)) return null;
|
|
8805
|
+
assertOpen();
|
|
8806
|
+
const sets = [];
|
|
8807
|
+
const params = [id];
|
|
8808
|
+
if (patch.enabled !== void 0) {
|
|
8809
|
+
params.push(patch.enabled);
|
|
8810
|
+
sets.push(`enabled = $${params.length}`);
|
|
8811
|
+
}
|
|
8812
|
+
if (patch.mapping !== void 0) {
|
|
8813
|
+
params.push(patch.mapping);
|
|
8814
|
+
sets.push(`mapping = $${params.length}`);
|
|
8815
|
+
}
|
|
8816
|
+
if (patch.mappingGraph !== void 0) {
|
|
8817
|
+
params.push(patch.mappingGraph == null ? null : JSON.stringify(patch.mappingGraph));
|
|
8818
|
+
sets.push(`mapping_graph = $${params.length}::jsonb`);
|
|
8819
|
+
}
|
|
8820
|
+
if (patch.correlationKey !== void 0) {
|
|
8821
|
+
params.push(patch.correlationKey);
|
|
8822
|
+
sets.push(`correlation_key = $${params.length}`);
|
|
8823
|
+
}
|
|
8824
|
+
if (sets.length === 0) return this.get(id);
|
|
8825
|
+
const { rows } = await pool.query(`UPDATE stackbone_platform.trigger_subscriptions
|
|
8826
|
+
SET ${sets.join(", ")}, updated_at = now()
|
|
8827
|
+
WHERE id = $1::uuid
|
|
8828
|
+
RETURNING ${SUBSCRIPTION_COLUMNS}`, params);
|
|
8829
|
+
return rows[0] ? rowToSubscription(rows[0]) : null;
|
|
8830
|
+
},
|
|
8831
|
+
async remove(id) {
|
|
8832
|
+
if (!isUuid(id)) return false;
|
|
8833
|
+
assertOpen();
|
|
8834
|
+
await pool.query("DELETE FROM stackbone_platform.trigger_deliveries WHERE subscription_id = $1::uuid", [id]);
|
|
8835
|
+
return ((await pool.query("DELETE FROM stackbone_platform.trigger_subscriptions WHERE id = $1::uuid", [id])).rowCount ?? 0) > 0;
|
|
8836
|
+
},
|
|
8837
|
+
async listActive() {
|
|
8838
|
+
assertOpen();
|
|
8839
|
+
const { rows } = await pool.query(`SELECT ${SUBSCRIPTION_COLUMNS}
|
|
8840
|
+
FROM stackbone_platform.trigger_subscriptions
|
|
8841
|
+
WHERE enabled
|
|
8842
|
+
ORDER BY created_at ASC, id ASC`);
|
|
8843
|
+
return rows.map(rowToSubscription);
|
|
8844
|
+
},
|
|
8845
|
+
async readCursor(subscriptionId) {
|
|
8846
|
+
assertOpen();
|
|
8847
|
+
const { rows } = await pool.query("SELECT poll_cursor FROM stackbone_platform.trigger_subscriptions WHERE id = $1::uuid", [subscriptionId]);
|
|
8848
|
+
return rows[0]?.poll_cursor ?? null;
|
|
8849
|
+
},
|
|
8850
|
+
async saveCursor({ subscriptionId, cursor, lastPolledAt }) {
|
|
8851
|
+
assertOpen();
|
|
8852
|
+
await pool.query(`UPDATE stackbone_platform.trigger_subscriptions
|
|
8853
|
+
SET poll_cursor = $2::jsonb, last_polled_at = $3::timestamptz, updated_at = now()
|
|
8854
|
+
WHERE id = $1::uuid`, [
|
|
8855
|
+
subscriptionId,
|
|
8856
|
+
cursor == null ? null : JSON.stringify(cursor),
|
|
8857
|
+
lastPolledAt.toISOString()
|
|
8858
|
+
]);
|
|
8859
|
+
},
|
|
8860
|
+
async claim(subscriptionId, providerItemId) {
|
|
8861
|
+
assertOpen();
|
|
8862
|
+
const { rows } = await pool.query(`INSERT INTO stackbone_platform.trigger_deliveries
|
|
8863
|
+
(id, subscription_id, provider_item_id, status)
|
|
8864
|
+
VALUES ($1::uuid, $2::uuid, $3, 'claimed')
|
|
8865
|
+
ON CONFLICT (subscription_id, provider_item_id) DO NOTHING
|
|
8866
|
+
RETURNING id`, [
|
|
8867
|
+
randomUUID(),
|
|
8868
|
+
subscriptionId,
|
|
8869
|
+
providerItemId
|
|
8870
|
+
]);
|
|
8871
|
+
return rows.length > 0;
|
|
8872
|
+
},
|
|
8873
|
+
async markStarted(subscriptionId, providerItemId, runId) {
|
|
8874
|
+
assertOpen();
|
|
8875
|
+
await pool.query(`UPDATE stackbone_platform.trigger_deliveries
|
|
8876
|
+
SET status = 'started', run_id = $3, updated_at = now()
|
|
8877
|
+
WHERE subscription_id = $1::uuid AND provider_item_id = $2`, [
|
|
8878
|
+
subscriptionId,
|
|
8879
|
+
providerItemId,
|
|
8880
|
+
runId
|
|
8881
|
+
]);
|
|
8882
|
+
},
|
|
8883
|
+
async markResumed(subscriptionId, providerItemId, runId) {
|
|
8884
|
+
assertOpen();
|
|
8885
|
+
await pool.query(`UPDATE stackbone_platform.trigger_deliveries
|
|
8886
|
+
SET status = 'resumed', run_id = COALESCE($3, run_id), updated_at = now()
|
|
8887
|
+
WHERE subscription_id = $1::uuid AND provider_item_id = $2`, [
|
|
8888
|
+
subscriptionId,
|
|
8889
|
+
providerItemId,
|
|
8890
|
+
runId ?? null
|
|
8891
|
+
]);
|
|
8892
|
+
},
|
|
8893
|
+
async markRejected(subscriptionId, providerItemId, issues) {
|
|
8894
|
+
assertOpen();
|
|
8895
|
+
await pool.query(`UPDATE stackbone_platform.trigger_deliveries
|
|
8896
|
+
SET status = 'rejected', issues = $3::jsonb, updated_at = now()
|
|
8897
|
+
WHERE subscription_id = $1::uuid AND provider_item_id = $2`, [
|
|
8898
|
+
subscriptionId,
|
|
8899
|
+
providerItemId,
|
|
8900
|
+
JSON.stringify(issues)
|
|
8901
|
+
]);
|
|
8902
|
+
},
|
|
8903
|
+
async release(subscriptionId, providerItemId) {
|
|
8904
|
+
assertOpen();
|
|
8905
|
+
await pool.query(`DELETE FROM stackbone_platform.trigger_deliveries
|
|
8906
|
+
WHERE subscription_id = $1::uuid AND provider_item_id = $2`, [subscriptionId, providerItemId]);
|
|
8907
|
+
},
|
|
8908
|
+
close
|
|
8909
|
+
};
|
|
8910
|
+
};
|
|
8911
|
+
//#endregion
|
|
8747
8912
|
//#region ../../libs/runtime/workspace-runtime/src/lib/services/event-bus.ts
|
|
8748
8913
|
var createEventBus = () => {
|
|
8749
8914
|
const listeners = /* @__PURE__ */ new Set();
|
|
@@ -9003,21 +9168,26 @@ var deepEqual$1 = (a, b) => {
|
|
|
9003
9168
|
return aKeys.every((key) => deepEqual$1(a[key], b[key]));
|
|
9004
9169
|
};
|
|
9005
9170
|
//#endregion
|
|
9006
|
-
//#region ../../libs/runtime/workspace-runtime/src/lib/services/dev-hmac-secret.ts
|
|
9007
|
-
var DEV_HMAC_SECRET = "dev-stackbone-local-emulator-hmac-secret";
|
|
9008
|
-
//#endregion
|
|
9009
9171
|
//#region ../../libs/runtime/workspace-runtime/src/lib/server/request-signature-middleware.ts
|
|
9010
9172
|
init_src$11();
|
|
9173
|
+
var PROXY_CHALLENGE_PATH = "/api/proxy-challenge";
|
|
9011
9174
|
var DEFAULT_PUBLIC_PREFIXES = [
|
|
9012
9175
|
"/api/contract",
|
|
9013
9176
|
"/api/health",
|
|
9014
9177
|
"/live",
|
|
9015
9178
|
"/health",
|
|
9016
|
-
|
|
9179
|
+
PROXY_CHALLENGE_PATH,
|
|
9180
|
+
"/api/connect/callback",
|
|
9181
|
+
"/api/connect/connected",
|
|
9182
|
+
"/api/connect/token",
|
|
9183
|
+
"/api/connect/execute",
|
|
9184
|
+
"/api/connect/evict",
|
|
9017
9185
|
"/connect/v1",
|
|
9018
9186
|
"/openai",
|
|
9019
9187
|
"/anthropic",
|
|
9020
|
-
"/agui"
|
|
9188
|
+
"/agui",
|
|
9189
|
+
"/.well-known/workflow/v1",
|
|
9190
|
+
"/api/workflows/hooks"
|
|
9021
9191
|
];
|
|
9022
9192
|
var isPublicPath = (pathname, prefixes) => {
|
|
9023
9193
|
if (pathname === "/") return true;
|
|
@@ -9054,9 +9224,73 @@ var createRequestSignatureVerifier = (options) => {
|
|
|
9054
9224
|
};
|
|
9055
9225
|
};
|
|
9056
9226
|
//#endregion
|
|
9227
|
+
//#region ../../libs/runtime/workspace-runtime/src/lib/server/jwt-verifier-middleware.ts
|
|
9228
|
+
var JWT_DEFAULT_PUBLIC_PREFIXES = DEFAULT_PUBLIC_PREFIXES;
|
|
9229
|
+
var JWKS_COOLDOWN_MS = 3e4;
|
|
9230
|
+
var JWKS_CACHE_MAX_AGE_MS = 10 * 6e4;
|
|
9231
|
+
/** The Hono context variable the verified identity is stashed under. */
|
|
9232
|
+
var VERIFIED_IDENTITY_VAR = "stackboneVerifiedIdentity";
|
|
9233
|
+
/** Read the identity a successful JWT verification stashed on the context. */
|
|
9234
|
+
var readVerifiedIdentity = (c) => c.get(VERIFIED_IDENTITY_VAR);
|
|
9235
|
+
var BEARER_RE = /^Bearer\s+(.+)$/i;
|
|
9236
|
+
var extractBearer = (header) => {
|
|
9237
|
+
if (!header) return null;
|
|
9238
|
+
return BEARER_RE.exec(header.trim())?.[1]?.trim() || null;
|
|
9239
|
+
};
|
|
9240
|
+
var extractIdentity = (payload) => {
|
|
9241
|
+
const userId = typeof payload.sub === "string" && payload.sub ? payload.sub : null;
|
|
9242
|
+
if (!userId) return null;
|
|
9243
|
+
const org = typeof payload["org"] === "string" ? payload["org"] : void 0;
|
|
9244
|
+
const role = typeof payload["role"] === "string" ? payload["role"] : void 0;
|
|
9245
|
+
const install = typeof payload["install"] === "string" ? payload["install"] : void 0;
|
|
9246
|
+
return {
|
|
9247
|
+
userId,
|
|
9248
|
+
...org !== void 0 ? { org } : {},
|
|
9249
|
+
...role !== void 0 ? { role } : {},
|
|
9250
|
+
...install !== void 0 ? { install } : {}
|
|
9251
|
+
};
|
|
9252
|
+
};
|
|
9253
|
+
/**
|
|
9254
|
+
* Build the Hono middleware that gates the box's surface behind a valid asymmetric
|
|
9255
|
+
* identity JWT. Mount it right after CORS (like the HMAC verifier) so the preflight
|
|
9256
|
+
* is answered by `hono/cors` and every gated route sees a verified caller.
|
|
9257
|
+
*/
|
|
9258
|
+
var createJwtVerifier = (options) => {
|
|
9259
|
+
const publicPrefixes = options.publicPrefixes ?? JWT_DEFAULT_PUBLIC_PREFIXES;
|
|
9260
|
+
const getKey = options.createJwks?.() ?? createRemoteJWKSet(new URL(options.jwksUrl), {
|
|
9261
|
+
cooldownDuration: JWKS_COOLDOWN_MS,
|
|
9262
|
+
cacheMaxAge: JWKS_CACHE_MAX_AGE_MS
|
|
9263
|
+
});
|
|
9264
|
+
const reject = (c) => c.json({
|
|
9265
|
+
code: "invalid_token",
|
|
9266
|
+
message: "Identity token is missing, expired, or invalid."
|
|
9267
|
+
}, 401);
|
|
9268
|
+
return async (c, next) => {
|
|
9269
|
+
if (c.req.method === "OPTIONS") return next();
|
|
9270
|
+
if (isPublicPath(new URL(c.req.url).pathname, publicPrefixes)) return next();
|
|
9271
|
+
const token = extractBearer(c.req.header("authorization"));
|
|
9272
|
+
if (!token) return reject(c);
|
|
9273
|
+
let payload;
|
|
9274
|
+
try {
|
|
9275
|
+
({payload} = await jwtVerify(token, getKey, {
|
|
9276
|
+
issuer: options.issuer,
|
|
9277
|
+
audience: options.audience
|
|
9278
|
+
}));
|
|
9279
|
+
} catch {
|
|
9280
|
+
return reject(c);
|
|
9281
|
+
}
|
|
9282
|
+
const identity = extractIdentity(payload);
|
|
9283
|
+
if (!identity) return reject(c);
|
|
9284
|
+
if (identity.org !== options.expectedOrg) return reject(c);
|
|
9285
|
+
if (identity.install !== void 0 && identity.install !== options.expectedInstall) return reject(c);
|
|
9286
|
+
c.set(VERIFIED_IDENTITY_VAR, identity);
|
|
9287
|
+
return next();
|
|
9288
|
+
};
|
|
9289
|
+
};
|
|
9290
|
+
//#endregion
|
|
9057
9291
|
//#region ../../libs/runtime/workspace-runtime/src/lib/server/contract.ts
|
|
9058
9292
|
init_src$13();
|
|
9059
|
-
var STACKBONE_CLI_BUILD_VERSION = "0.2.
|
|
9293
|
+
var STACKBONE_CLI_BUILD_VERSION = "0.2.8";
|
|
9060
9294
|
/**
|
|
9061
9295
|
* Capabilities the local emulator advertises on `GET /api/contract`, derived
|
|
9062
9296
|
* from the route registry above so the advertised set and the mounted routes
|
|
@@ -9698,9 +9932,63 @@ var init_src$6 = __esmMin((() => {
|
|
|
9698
9932
|
//#region ../../libs/runtime/workspace-runtime/src/lib/handlers/actor.ts
|
|
9699
9933
|
init_src$11();
|
|
9700
9934
|
function readActorUserId(c) {
|
|
9935
|
+
const verified = readVerifiedIdentity(c);
|
|
9936
|
+
if (verified?.userId) return verified.userId;
|
|
9701
9937
|
return c.req.header("x-stackbone-actor-user-id") || null;
|
|
9702
9938
|
}
|
|
9703
9939
|
//#endregion
|
|
9940
|
+
//#region ../../libs/runtime/workspace-runtime/src/lib/handlers/require-capability.ts
|
|
9941
|
+
var ROLES_WITH_CAPABILITY = {
|
|
9942
|
+
"config:write": new Set([
|
|
9943
|
+
"owner",
|
|
9944
|
+
"admin",
|
|
9945
|
+
"member"
|
|
9946
|
+
]),
|
|
9947
|
+
"hitl:decide": new Set([
|
|
9948
|
+
"owner",
|
|
9949
|
+
"admin",
|
|
9950
|
+
"approver"
|
|
9951
|
+
]),
|
|
9952
|
+
"rag:write": new Set([
|
|
9953
|
+
"owner",
|
|
9954
|
+
"admin",
|
|
9955
|
+
"member"
|
|
9956
|
+
]),
|
|
9957
|
+
"runs:invoke": new Set([
|
|
9958
|
+
"owner",
|
|
9959
|
+
"admin",
|
|
9960
|
+
"member"
|
|
9961
|
+
]),
|
|
9962
|
+
"db:query": new Set([
|
|
9963
|
+
"owner",
|
|
9964
|
+
"admin",
|
|
9965
|
+
"member"
|
|
9966
|
+
]),
|
|
9967
|
+
"secrets:rotate": new Set(["owner", "admin"]),
|
|
9968
|
+
"secrets:reveal": new Set(["owner", "admin"]),
|
|
9969
|
+
"connections:manage": new Set(["owner", "admin"])
|
|
9970
|
+
};
|
|
9971
|
+
/**
|
|
9972
|
+
* Build a Hono middleware that requires the caller's verified identity to grant
|
|
9973
|
+
* `capability`. A pass-through when there is no verified identity (proxy / dev /
|
|
9974
|
+
* self-host datapaths — see the module comment). On the browser-direct datapath a
|
|
9975
|
+
* missing/unknown role, or a role the matrix does not grant the capability, is a
|
|
9976
|
+
* 403 (fail-closed) before the handler runs.
|
|
9977
|
+
*/
|
|
9978
|
+
var requireCapability = (capability) => {
|
|
9979
|
+
const allowed = ROLES_WITH_CAPABILITY[capability];
|
|
9980
|
+
return async (c, next) => {
|
|
9981
|
+
const identity = readVerifiedIdentity(c);
|
|
9982
|
+
if (!identity) return next();
|
|
9983
|
+
const role = identity.role;
|
|
9984
|
+
if (!role || !allowed.has(role)) return c.json({
|
|
9985
|
+
code: "forbidden",
|
|
9986
|
+
message: `This action requires the ${capability} capability.`
|
|
9987
|
+
}, 403);
|
|
9988
|
+
return next();
|
|
9989
|
+
};
|
|
9990
|
+
};
|
|
9991
|
+
//#endregion
|
|
9704
9992
|
//#region ../../libs/runtime/workspace-runtime/src/lib/handlers/prompts-handlers.ts
|
|
9705
9993
|
init_src$10();
|
|
9706
9994
|
init_src$13();
|
|
@@ -9800,7 +10088,7 @@ var createPromptsRouter = (opts) => {
|
|
|
9800
10088
|
ORDER BY p.key
|
|
9801
10089
|
LIMIT 200`)).rows.map((row) => rowToPrompt(row, row.current_version, row.content)) };
|
|
9802
10090
|
})));
|
|
9803
|
-
app.post("/", async (c) => {
|
|
10091
|
+
app.post("/", requireCapability("config:write"), async (c) => {
|
|
9804
10092
|
const raw = await c.req.json().catch(() => ({}));
|
|
9805
10093
|
const parsed = createStudioPromptRequestSchema.safeParse(raw);
|
|
9806
10094
|
if (!parsed.success) return c.json(invalidPayload$1(parsed.error.issues), 400);
|
|
@@ -9864,7 +10152,7 @@ var createPromptsRouter = (opts) => {
|
|
|
9864
10152
|
return c.json(rowToPrompt(head, head.current_version, head.content));
|
|
9865
10153
|
});
|
|
9866
10154
|
});
|
|
9867
|
-
app.put("/:key", async (c) => {
|
|
10155
|
+
app.put("/:key", requireCapability("config:write"), async (c) => {
|
|
9868
10156
|
const keyParsed = studioPromptKeyParamSchema.safeParse(c.req.param("key"));
|
|
9869
10157
|
if (!keyParsed.success) return c.json(notFound, 404);
|
|
9870
10158
|
const raw = await c.req.json().catch(() => ({}));
|
|
@@ -9897,7 +10185,7 @@ var createPromptsRouter = (opts) => {
|
|
|
9897
10185
|
return updated ? c.json(rowToPrompt(updated, updated.current_version, updated.content)) : c.json(notFound, 404);
|
|
9898
10186
|
});
|
|
9899
10187
|
});
|
|
9900
|
-
app.delete("/:key", async (c) => {
|
|
10188
|
+
app.delete("/:key", requireCapability("config:write"), async (c) => {
|
|
9901
10189
|
const keyParsed = studioPromptKeyParamSchema.safeParse(c.req.param("key"));
|
|
9902
10190
|
if (!keyParsed.success) return c.json(notFound, 404);
|
|
9903
10191
|
return withClient$2(opts, async (client) => {
|
|
@@ -9924,7 +10212,7 @@ var createPromptsRouter = (opts) => {
|
|
|
9924
10212
|
return c.json({ items: res.rows.map(rowToVersionItem) });
|
|
9925
10213
|
});
|
|
9926
10214
|
});
|
|
9927
|
-
app.post("/:key/rollback", async (c) => {
|
|
10215
|
+
app.post("/:key/rollback", requireCapability("config:write"), async (c) => {
|
|
9928
10216
|
const keyParsed = studioPromptKeyParamSchema.safeParse(c.req.param("key"));
|
|
9929
10217
|
if (!keyParsed.success) return c.json(notFound, 404);
|
|
9930
10218
|
const raw = await c.req.json().catch(() => ({}));
|
|
@@ -10169,7 +10457,7 @@ var createStorageRouter = (storageOptions) => {
|
|
|
10169
10457
|
const response = await listObjects$1(storageOptions, bucketParsed.data, queryParsed.data);
|
|
10170
10458
|
return c.json(response);
|
|
10171
10459
|
});
|
|
10172
|
-
app.post("/buckets/:bucket/upload", async (c) => {
|
|
10460
|
+
app.post("/buckets/:bucket/upload", requireCapability("runs:invoke"), async (c) => {
|
|
10173
10461
|
const bucketParsed = studioStorageBucketParamSchema.safeParse(c.req.param("bucket"));
|
|
10174
10462
|
if (!bucketParsed.success) return c.json({
|
|
10175
10463
|
code: "bucket_not_found",
|
|
@@ -10199,7 +10487,7 @@ var createStorageRouter = (storageOptions) => {
|
|
|
10199
10487
|
const response = await uploadObject$1(storageOptions, bucketParsed.data, keyParsed.data, buffer, file.type || void 0);
|
|
10200
10488
|
return c.json(response);
|
|
10201
10489
|
});
|
|
10202
|
-
app.post("/buckets/:bucket/folders", async (c) => {
|
|
10490
|
+
app.post("/buckets/:bucket/folders", requireCapability("runs:invoke"), async (c) => {
|
|
10203
10491
|
const bucketParsed = studioStorageBucketParamSchema.safeParse(c.req.param("bucket"));
|
|
10204
10492
|
if (!bucketParsed.success) return c.json({
|
|
10205
10493
|
code: "bucket_not_found",
|
|
@@ -10227,7 +10515,7 @@ var createStorageRouter = (storageOptions) => {
|
|
|
10227
10515
|
throw err;
|
|
10228
10516
|
}
|
|
10229
10517
|
});
|
|
10230
|
-
app.delete("/buckets/:bucket/objects/:key", async (c) => {
|
|
10518
|
+
app.delete("/buckets/:bucket/objects/:key", requireCapability("runs:invoke"), async (c) => {
|
|
10231
10519
|
const bucketParsed = studioStorageBucketParamSchema.safeParse(c.req.param("bucket"));
|
|
10232
10520
|
if (!bucketParsed.success) return c.json({
|
|
10233
10521
|
code: "bucket_not_found",
|
|
@@ -10414,11 +10702,11 @@ init_src$13();
|
|
|
10414
10702
|
*/
|
|
10415
10703
|
var createSecretsRouter = (secretsOptions) => {
|
|
10416
10704
|
const app = new Hono();
|
|
10417
|
-
app.get("/", async (c) => {
|
|
10705
|
+
app.get("/", requireCapability("secrets:rotate"), async (c) => {
|
|
10418
10706
|
const response = await listSecrets$1(secretsOptions);
|
|
10419
10707
|
return c.json(response);
|
|
10420
10708
|
});
|
|
10421
|
-
app.post("/", async (c) => {
|
|
10709
|
+
app.post("/", requireCapability("secrets:rotate"), async (c) => {
|
|
10422
10710
|
const raw = await c.req.json().catch(() => ({}));
|
|
10423
10711
|
const parsed = createStudioSecretRequestSchema.safeParse(raw);
|
|
10424
10712
|
if (!parsed.success) return c.json({
|
|
@@ -10437,7 +10725,7 @@ var createSecretsRouter = (secretsOptions) => {
|
|
|
10437
10725
|
throw err;
|
|
10438
10726
|
}
|
|
10439
10727
|
});
|
|
10440
|
-
app.delete("/:name", async (c) => {
|
|
10728
|
+
app.delete("/:name", requireCapability("secrets:rotate"), async (c) => {
|
|
10441
10729
|
const parsed = studioSecretNameParamSchema.safeParse(c.req.param("name"));
|
|
10442
10730
|
if (!parsed.success) return c.json({
|
|
10443
10731
|
code: "studio_secret_not_found",
|
|
@@ -10461,7 +10749,7 @@ var createSecretsRouter = (secretsOptions) => {
|
|
|
10461
10749
|
throw err;
|
|
10462
10750
|
}
|
|
10463
10751
|
});
|
|
10464
|
-
app.post("/:name/reveal", async (c) => {
|
|
10752
|
+
app.post("/:name/reveal", requireCapability("secrets:reveal"), async (c) => {
|
|
10465
10753
|
const parsed = studioSecretNameParamSchema.safeParse(c.req.param("name"));
|
|
10466
10754
|
if (!parsed.success) return c.json({
|
|
10467
10755
|
code: "studio_secret_not_found",
|
|
@@ -10684,7 +10972,7 @@ var createConfigRouter = (agentConfigOptions) => {
|
|
|
10684
10972
|
const response = await getAgentConfig(agentConfigOptions);
|
|
10685
10973
|
return c.json(response);
|
|
10686
10974
|
});
|
|
10687
|
-
app.put("/", async (c) => {
|
|
10975
|
+
app.put("/", requireCapability("config:write"), async (c) => {
|
|
10688
10976
|
const raw = await c.req.json().catch(() => ({}));
|
|
10689
10977
|
const parsed = studioAgentConfigPutRequestSchema.safeParse(raw);
|
|
10690
10978
|
if (!parsed.success) return c.json({
|
|
@@ -10708,7 +10996,7 @@ var createConfigRouter = (agentConfigOptions) => {
|
|
|
10708
10996
|
const response = await listAgentConfigVersions(agentConfigOptions);
|
|
10709
10997
|
return c.json(response);
|
|
10710
10998
|
});
|
|
10711
|
-
app.post("/rollback/:version", async (c) => {
|
|
10999
|
+
app.post("/rollback/:version", requireCapability("config:write"), async (c) => {
|
|
10712
11000
|
const parsed = studioAgentConfigVersionParamSchema.safeParse(c.req.param("version"));
|
|
10713
11001
|
if (!parsed.success) return c.json({
|
|
10714
11002
|
code: "agent_config_version_not_found",
|
|
@@ -11110,7 +11398,7 @@ var createRunsRouter = (deps) => {
|
|
|
11110
11398
|
throw err;
|
|
11111
11399
|
}
|
|
11112
11400
|
});
|
|
11113
|
-
app.post("/:id/retry", async (c) => {
|
|
11401
|
+
app.post("/:id/retry", requireCapability("runs:invoke"), async (c) => {
|
|
11114
11402
|
const result = await deps.invocations.retry(c.req.param("id"));
|
|
11115
11403
|
if (!result) return c.json({
|
|
11116
11404
|
code: "run_not_found",
|
|
@@ -11131,7 +11419,7 @@ var createRunsRouter = (deps) => {
|
|
|
11131
11419
|
});
|
|
11132
11420
|
return c.json({ run: result.run }, 202);
|
|
11133
11421
|
});
|
|
11134
|
-
app.post("/:id/cancel", async (c) => {
|
|
11422
|
+
app.post("/:id/cancel", requireCapability("runs:invoke"), async (c) => {
|
|
11135
11423
|
const result = await deps.invocations.cancel(c.req.param("id"));
|
|
11136
11424
|
if (!result) return c.json({
|
|
11137
11425
|
code: "run_not_found",
|
|
@@ -16587,7 +16875,7 @@ var createApprovalsRouter = (deps) => {
|
|
|
16587
16875
|
}, 404);
|
|
16588
16876
|
return c.json(detail);
|
|
16589
16877
|
});
|
|
16590
|
-
app.post("/:id/decide", async (c) => {
|
|
16878
|
+
app.post("/:id/decide", requireCapability("hitl:decide"), async (c) => {
|
|
16591
16879
|
const raw = await c.req.json().catch(() => ({}));
|
|
16592
16880
|
const parsed = decideApprovalRequestSchema.safeParse(raw);
|
|
16593
16881
|
if (!parsed.success) return c.json({
|
|
@@ -16759,11 +17047,11 @@ init_src$13();
|
|
|
16759
17047
|
var createDbExplorerRouter = (deps) => {
|
|
16760
17048
|
const app = new Hono();
|
|
16761
17049
|
const dbExplorerOptions = { connectionString: deps.connectionString };
|
|
16762
|
-
app.get("/schemas", async (c) => {
|
|
17050
|
+
app.get("/schemas", requireCapability("db:query"), async (c) => {
|
|
16763
17051
|
const response = await listSchemas(dbExplorerOptions);
|
|
16764
17052
|
return c.json(response);
|
|
16765
17053
|
});
|
|
16766
|
-
app.get("/tables/:schema/:table", async (c) => {
|
|
17054
|
+
app.get("/tables/:schema/:table", requireCapability("db:query"), async (c) => {
|
|
16767
17055
|
const paramsParsed = dbBrowseParamsSchema.safeParse({
|
|
16768
17056
|
schema: c.req.param("schema"),
|
|
16769
17057
|
table: c.req.param("table")
|
|
@@ -16797,7 +17085,7 @@ var createDbExplorerRouter = (deps) => {
|
|
|
16797
17085
|
throw err;
|
|
16798
17086
|
}
|
|
16799
17087
|
});
|
|
16800
|
-
app.post("/query", async (c) => {
|
|
17088
|
+
app.post("/query", requireCapability("db:query"), async (c) => {
|
|
16801
17089
|
const raw = await c.req.json().catch(() => ({}));
|
|
16802
17090
|
const parsed = dbQueryRequestSchema.safeParse(raw);
|
|
16803
17091
|
if (!parsed.success) return c.json({
|
|
@@ -17010,11 +17298,11 @@ var deleteBrowserProfile = async (opts, origin, identity) => withClient(opts, as
|
|
|
17010
17298
|
var createBrowserProfilesRouter = (deps) => {
|
|
17011
17299
|
const app = new Hono();
|
|
17012
17300
|
const browserProfilesOptions = { connectionString: deps.connectionString };
|
|
17013
|
-
app.get("/", async (c) => {
|
|
17301
|
+
app.get("/", requireCapability("secrets:rotate"), async (c) => {
|
|
17014
17302
|
const response = await listBrowserProfiles(browserProfilesOptions);
|
|
17015
17303
|
return c.json(response);
|
|
17016
17304
|
});
|
|
17017
|
-
app.delete("/", async (c) => {
|
|
17305
|
+
app.delete("/", requireCapability("secrets:rotate"), async (c) => {
|
|
17018
17306
|
const origin = c.req.query("origin");
|
|
17019
17307
|
const identity = c.req.query("identity") ?? "agent";
|
|
17020
17308
|
if (!origin) return c.json({
|
|
@@ -17973,7 +18261,7 @@ var createRagRouter = (deps) => {
|
|
|
17973
18261
|
next_cursor: page.nextCursor
|
|
17974
18262
|
});
|
|
17975
18263
|
});
|
|
17976
|
-
app.post("/jobs/:jobId/cancel", async (c) => {
|
|
18264
|
+
app.post("/jobs/:jobId/cancel", requireCapability("rag:write"), async (c) => {
|
|
17977
18265
|
const result = await deps.invocations.cancel(c.req.param("jobId"));
|
|
17978
18266
|
if (!result) return c.json({
|
|
17979
18267
|
code: "rag_job_not_found",
|
|
@@ -17999,7 +18287,7 @@ var createRagRouter = (deps) => {
|
|
|
17999
18287
|
return trigger(input);
|
|
18000
18288
|
} } : {}
|
|
18001
18289
|
});
|
|
18002
|
-
app.post("/collections", async (c) => {
|
|
18290
|
+
app.post("/collections", requireCapability("rag:write"), async (c) => {
|
|
18003
18291
|
const raw = await c.req.json().catch(() => ({}));
|
|
18004
18292
|
const parsed = createRagCollectionRequestSchema.safeParse(raw);
|
|
18005
18293
|
if (!parsed.success) return c.json({
|
|
@@ -18013,7 +18301,7 @@ var createRagRouter = (deps) => {
|
|
|
18013
18301
|
return mapRagWriteError(c, err);
|
|
18014
18302
|
}
|
|
18015
18303
|
});
|
|
18016
|
-
app.delete("/collections/:name", async (c) => {
|
|
18304
|
+
app.delete("/collections/:name", requireCapability("rag:write"), async (c) => {
|
|
18017
18305
|
const nameParsed = ragCollectionNameParamSchema.safeParse(c.req.param("name"));
|
|
18018
18306
|
if (!nameParsed.success) return c.json({
|
|
18019
18307
|
code: "rag_collection_not_found",
|
|
@@ -18025,7 +18313,7 @@ var createRagRouter = (deps) => {
|
|
|
18025
18313
|
return mapRagWriteError(c, err);
|
|
18026
18314
|
}
|
|
18027
18315
|
});
|
|
18028
|
-
app.post("/collections/:name/docs", async (c) => {
|
|
18316
|
+
app.post("/collections/:name/docs", requireCapability("rag:write"), async (c) => {
|
|
18029
18317
|
const nameParsed = ragCollectionNameParamSchema.safeParse(c.req.param("name"));
|
|
18030
18318
|
if (!nameParsed.success) return c.json({
|
|
18031
18319
|
code: "rag_collection_not_found",
|
|
@@ -18067,7 +18355,7 @@ var createRagRouter = (deps) => {
|
|
|
18067
18355
|
return mapRagWriteError(c, err);
|
|
18068
18356
|
}
|
|
18069
18357
|
});
|
|
18070
|
-
app.delete("/collections/:name/docs/:docId", async (c) => {
|
|
18358
|
+
app.delete("/collections/:name/docs/:docId", requireCapability("rag:write"), async (c) => {
|
|
18071
18359
|
const nameParsed = ragCollectionNameParamSchema.safeParse(c.req.param("name"));
|
|
18072
18360
|
if (!nameParsed.success) return c.json({
|
|
18073
18361
|
code: "rag_collection_not_found",
|
|
@@ -18080,7 +18368,7 @@ var createRagRouter = (deps) => {
|
|
|
18080
18368
|
return mapRagWriteError(c, err);
|
|
18081
18369
|
}
|
|
18082
18370
|
});
|
|
18083
|
-
app.post("/collections/:name/docs/:docId/download", async (c) => {
|
|
18371
|
+
app.post("/collections/:name/docs/:docId/download", requireCapability("rag:write"), async (c) => {
|
|
18084
18372
|
const nameParsed = ragCollectionNameParamSchema.safeParse(c.req.param("name"));
|
|
18085
18373
|
if (!nameParsed.success) return c.json({
|
|
18086
18374
|
code: "rag_collection_not_found",
|
|
@@ -18093,7 +18381,7 @@ var createRagRouter = (deps) => {
|
|
|
18093
18381
|
return mapRagWriteError(c, err);
|
|
18094
18382
|
}
|
|
18095
18383
|
});
|
|
18096
|
-
app.post("/jobs/:jobId/retry", async (c) => {
|
|
18384
|
+
app.post("/jobs/:jobId/retry", requireCapability("rag:write"), async (c) => {
|
|
18097
18385
|
const parsed = ragJobIdParamSchema.safeParse({ jobId: c.req.param("jobId") });
|
|
18098
18386
|
if (!parsed.success) return c.json({
|
|
18099
18387
|
code: "rag_job_not_found",
|
|
@@ -19353,6 +19641,24 @@ var init_drizzle_broker_store = __esmMin((() => {
|
|
|
19353
19641
|
async deleteGrant(orgId, connector, principalKey) {
|
|
19354
19642
|
await this.db.delete(connectionGrantTable).where(and(eq(connectionGrantTable.organizationId, orgId), eq(connectionGrantTable.connector, connector), eq(connectionGrantTable.principalKey, principalKey)));
|
|
19355
19643
|
}
|
|
19644
|
+
async listRefreshableGrants() {
|
|
19645
|
+
const rows = await this.db.select().from(connectionGrantTable);
|
|
19646
|
+
const grants = [];
|
|
19647
|
+
for (const row of rows) try {
|
|
19648
|
+
grants.push({
|
|
19649
|
+
...this.decryptGrant(row),
|
|
19650
|
+
healthStatus: row.healthStatus
|
|
19651
|
+
});
|
|
19652
|
+
} catch {}
|
|
19653
|
+
return grants;
|
|
19654
|
+
}
|
|
19655
|
+
async setGrantHealth(input) {
|
|
19656
|
+
await this.db.update(connectionGrantTable).set({
|
|
19657
|
+
healthStatus: input.status,
|
|
19658
|
+
healthDetail: input.detail,
|
|
19659
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
19660
|
+
}).where(eq(connectionGrantTable.id, input.grantId));
|
|
19661
|
+
}
|
|
19356
19662
|
encrypt(plaintext) {
|
|
19357
19663
|
const envelope = this.cipher.encrypt(plaintext);
|
|
19358
19664
|
return {
|
|
@@ -19492,6 +19798,384 @@ var init_default_oauth_exchanger = __esmMin((() => {
|
|
|
19492
19798
|
};
|
|
19493
19799
|
}));
|
|
19494
19800
|
//#endregion
|
|
19801
|
+
//#region ../../libs/platform/connect-broker/src/lib/refresh/grant-refresh-ports.ts
|
|
19802
|
+
/** Project a decrypted grant onto the refreshable shape the sweep consumes. */
|
|
19803
|
+
function grantToRefreshable(grant) {
|
|
19804
|
+
return {
|
|
19805
|
+
id: grant.id ?? `${grant.orgId}:${grant.connector}:${grant.principalKey}`,
|
|
19806
|
+
organizationId: grant.orgId,
|
|
19807
|
+
connectorId: grant.connector,
|
|
19808
|
+
healthStatus: grant.healthStatus,
|
|
19809
|
+
accessTokenExpiresAt: grant.expiresAt === null ? null : new Date(grant.expiresAt),
|
|
19810
|
+
connector: grant.connector,
|
|
19811
|
+
principalKey: grant.principalKey,
|
|
19812
|
+
hasRefreshToken: grant.refreshToken !== null
|
|
19813
|
+
};
|
|
19814
|
+
}
|
|
19815
|
+
function asHealthStatus(value) {
|
|
19816
|
+
return value;
|
|
19817
|
+
}
|
|
19818
|
+
/**
|
|
19819
|
+
* Whether a refresh failure message signals a PERMANENT grant loss (the grant is
|
|
19820
|
+
* gone and a refresh will never succeed) vs a transient provider/network blip.
|
|
19821
|
+
* OAuth2 returns `invalid_grant` for a revoked/expired refresh token.
|
|
19822
|
+
*/
|
|
19823
|
+
function isPermanentRefreshFailure(message) {
|
|
19824
|
+
const lowered = message.toLowerCase();
|
|
19825
|
+
return lowered.includes("invalid_grant") || lowered.includes("invalid grant") || lowered.includes("access_denied") || lowered.includes("unauthorized_client");
|
|
19826
|
+
}
|
|
19827
|
+
var GrantRefreshableConnectionLoader, GrantConnectionRefresher, GrantConnectionHealthWriter, NoopGrantCascadeDisabler;
|
|
19828
|
+
var init_grant_refresh_ports = __esmMin((() => {
|
|
19829
|
+
GrantRefreshableConnectionLoader = class {
|
|
19830
|
+
constructor(lister) {
|
|
19831
|
+
this.lister = lister;
|
|
19832
|
+
}
|
|
19833
|
+
async listRefreshable() {
|
|
19834
|
+
return (await this.lister.listRefreshableGrants()).filter((g) => asHealthStatus(g.healthStatus) !== "revoked").map(grantToRefreshable);
|
|
19835
|
+
}
|
|
19836
|
+
};
|
|
19837
|
+
GrantConnectionRefresher = class {
|
|
19838
|
+
constructor(store, exchanger) {
|
|
19839
|
+
this.store = store;
|
|
19840
|
+
this.exchanger = exchanger;
|
|
19841
|
+
}
|
|
19842
|
+
async refresh(connection) {
|
|
19843
|
+
const grantRef = connection;
|
|
19844
|
+
if (grantRef.hasRefreshToken === false) return {
|
|
19845
|
+
kind: "permanent",
|
|
19846
|
+
reason: "no refresh token"
|
|
19847
|
+
};
|
|
19848
|
+
const grant = await this.store.getGrant(grantRef.organizationId, grantRef.connector, grantRef.principalKey);
|
|
19849
|
+
if (grant === null) return {
|
|
19850
|
+
kind: "permanent",
|
|
19851
|
+
reason: "grant not found"
|
|
19852
|
+
};
|
|
19853
|
+
if (grant.refreshToken === null) return {
|
|
19854
|
+
kind: "permanent",
|
|
19855
|
+
reason: "no refresh token"
|
|
19856
|
+
};
|
|
19857
|
+
const connector = await this.store.getConnector(grant.orgId, grant.connector);
|
|
19858
|
+
if (connector === null) return {
|
|
19859
|
+
kind: "permanent",
|
|
19860
|
+
reason: "connector not registered"
|
|
19861
|
+
};
|
|
19862
|
+
try {
|
|
19863
|
+
const tokens = await this.exchanger.refresh({
|
|
19864
|
+
connector,
|
|
19865
|
+
refreshToken: grant.refreshToken
|
|
19866
|
+
});
|
|
19867
|
+
await this.store.upsertGrant({
|
|
19868
|
+
...grant,
|
|
19869
|
+
accessToken: tokens.accessToken,
|
|
19870
|
+
refreshToken: tokens.refreshToken ?? grant.refreshToken,
|
|
19871
|
+
expiresAt: tokens.expiresAt ?? null
|
|
19872
|
+
});
|
|
19873
|
+
return { kind: "refreshed" };
|
|
19874
|
+
} catch (err) {
|
|
19875
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
19876
|
+
if (isPermanentRefreshFailure(message)) return {
|
|
19877
|
+
kind: "permanent",
|
|
19878
|
+
reason: message
|
|
19879
|
+
};
|
|
19880
|
+
return {
|
|
19881
|
+
kind: "transient",
|
|
19882
|
+
reason: message
|
|
19883
|
+
};
|
|
19884
|
+
}
|
|
19885
|
+
}
|
|
19886
|
+
};
|
|
19887
|
+
GrantConnectionHealthWriter = class {
|
|
19888
|
+
constructor(setter) {
|
|
19889
|
+
this.setter = setter;
|
|
19890
|
+
}
|
|
19891
|
+
async setHealth(input) {
|
|
19892
|
+
await this.setter.setGrantHealth({
|
|
19893
|
+
grantId: input.connectionId,
|
|
19894
|
+
status: input.status,
|
|
19895
|
+
detail: input.detail
|
|
19896
|
+
});
|
|
19897
|
+
}
|
|
19898
|
+
};
|
|
19899
|
+
NoopGrantCascadeDisabler = class {
|
|
19900
|
+
async disableDependents() {
|
|
19901
|
+
return { disabledAutomationIds: [] };
|
|
19902
|
+
}
|
|
19903
|
+
};
|
|
19904
|
+
}));
|
|
19905
|
+
//#endregion
|
|
19906
|
+
//#region ../../libs/platform/connect-broker/src/lib/refresh/logger.ts
|
|
19907
|
+
var consoleRefreshLogger;
|
|
19908
|
+
var init_logger = __esmMin((() => {
|
|
19909
|
+
consoleRefreshLogger = {
|
|
19910
|
+
error: (message, context) => console.error("ERROR", message, context ? JSON.stringify(context) : ""),
|
|
19911
|
+
warn: (message, context) => console.warn("WARN", message, context ? JSON.stringify(context) : ""),
|
|
19912
|
+
info: (message, context) => console.info("INFO", message, context ? JSON.stringify(context) : ""),
|
|
19913
|
+
debug: (message, context) => console.debug("DEBUG", message, context ? JSON.stringify(context) : "")
|
|
19914
|
+
};
|
|
19915
|
+
})), REFRESH_REVOKED_REASON, ConnectionRefreshProcessor;
|
|
19916
|
+
var init_connection_refresh_processor = __esmMin((() => {
|
|
19917
|
+
init_logger();
|
|
19918
|
+
REFRESH_REVOKED_REASON = "connection token revoked";
|
|
19919
|
+
ConnectionRefreshProcessor = class {
|
|
19920
|
+
constructor(deps) {
|
|
19921
|
+
this.deps = deps;
|
|
19922
|
+
this.leadMs = deps.leadMs ?? 3e5;
|
|
19923
|
+
this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
19924
|
+
this.logger = deps.logger ?? consoleRefreshLogger;
|
|
19925
|
+
}
|
|
19926
|
+
/**
|
|
19927
|
+
* Decide whether a connection's token is due for a proactive refresh. A NULL
|
|
19928
|
+
* expiry counts as due (PRD: a provider that omitted `expires_in` with no
|
|
19929
|
+
* fallback would otherwise never refresh). Otherwise due when the expiry is
|
|
19930
|
+
* within `leadMs` of now.
|
|
19931
|
+
*/
|
|
19932
|
+
isRefreshDue(connection) {
|
|
19933
|
+
if (connection.accessTokenExpiresAt === null) return true;
|
|
19934
|
+
return connection.accessTokenExpiresAt.getTime() - this.now().getTime() <= this.leadMs;
|
|
19935
|
+
}
|
|
19936
|
+
/** Sweep every refreshable connection once. Returns one result per connection. */
|
|
19937
|
+
async sweep() {
|
|
19938
|
+
const connections = await this.deps.connections.listRefreshable();
|
|
19939
|
+
const results = [];
|
|
19940
|
+
for (const connection of connections) results.push(await this.process(connection));
|
|
19941
|
+
return results;
|
|
19942
|
+
}
|
|
19943
|
+
/** Run the state machine for ONE connection. */
|
|
19944
|
+
async process(connection) {
|
|
19945
|
+
if (!this.isRefreshDue(connection)) return {
|
|
19946
|
+
connectionId: connection.id,
|
|
19947
|
+
action: "skipped",
|
|
19948
|
+
reason: "token-valid"
|
|
19949
|
+
};
|
|
19950
|
+
const outcome = await this.deps.refresher.refresh(connection);
|
|
19951
|
+
switch (outcome.kind) {
|
|
19952
|
+
case "refreshed":
|
|
19953
|
+
if (connection.healthStatus !== "active") await this.deps.health.setHealth({
|
|
19954
|
+
connectionId: connection.id,
|
|
19955
|
+
status: "active",
|
|
19956
|
+
detail: null
|
|
19957
|
+
});
|
|
19958
|
+
return {
|
|
19959
|
+
connectionId: connection.id,
|
|
19960
|
+
action: "refreshed"
|
|
19961
|
+
};
|
|
19962
|
+
case "transient":
|
|
19963
|
+
await this.deps.health.setHealth({
|
|
19964
|
+
connectionId: connection.id,
|
|
19965
|
+
status: "error",
|
|
19966
|
+
detail: outcome.reason
|
|
19967
|
+
});
|
|
19968
|
+
return {
|
|
19969
|
+
connectionId: connection.id,
|
|
19970
|
+
action: "transient-failure",
|
|
19971
|
+
reason: outcome.reason
|
|
19972
|
+
};
|
|
19973
|
+
case "permanent": {
|
|
19974
|
+
await this.deps.health.setHealth({
|
|
19975
|
+
connectionId: connection.id,
|
|
19976
|
+
status: "revoked",
|
|
19977
|
+
detail: outcome.reason
|
|
19978
|
+
});
|
|
19979
|
+
const { disabledAutomationIds } = await this.deps.cascade.disableDependents({
|
|
19980
|
+
connectionId: connection.id,
|
|
19981
|
+
reason: REFRESH_REVOKED_REASON
|
|
19982
|
+
});
|
|
19983
|
+
this.logger.warn("oauth refresh: connection revoked, cascaded disable", {
|
|
19984
|
+
connectionId: connection.id,
|
|
19985
|
+
connectorId: connection.connectorId,
|
|
19986
|
+
disabled: disabledAutomationIds.length,
|
|
19987
|
+
reason: outcome.reason
|
|
19988
|
+
});
|
|
19989
|
+
return {
|
|
19990
|
+
connectionId: connection.id,
|
|
19991
|
+
action: "revoked",
|
|
19992
|
+
reason: outcome.reason,
|
|
19993
|
+
disabledAutomationIds
|
|
19994
|
+
};
|
|
19995
|
+
}
|
|
19996
|
+
}
|
|
19997
|
+
}
|
|
19998
|
+
};
|
|
19999
|
+
})), CONNECTION_REFRESH_QUEUE, CONNECTION_REFRESH_JOB_NAME, CONNECTION_REFRESH_SCHEDULER_ID;
|
|
20000
|
+
var init_constants = __esmMin((() => {
|
|
20001
|
+
CONNECTION_REFRESH_QUEUE = "connector-token-refresh";
|
|
20002
|
+
CONNECTION_REFRESH_JOB_NAME = "refresh-sweep";
|
|
20003
|
+
CONNECTION_REFRESH_SCHEDULER_ID = "connector-token-refresh:sweep";
|
|
20004
|
+
}));
|
|
20005
|
+
//#endregion
|
|
20006
|
+
//#region ../../libs/platform/connect-broker/src/lib/refresh/connection-refresh-scheduler.ts
|
|
20007
|
+
var ConnectionRefreshScheduler;
|
|
20008
|
+
var init_connection_refresh_scheduler = __esmMin((() => {
|
|
20009
|
+
init_logger();
|
|
20010
|
+
init_constants();
|
|
20011
|
+
ConnectionRefreshScheduler = class {
|
|
20012
|
+
constructor(options) {
|
|
20013
|
+
this.logger = options.logger ?? consoleRefreshLogger;
|
|
20014
|
+
this.queue = new Queue(CONNECTION_REFRESH_QUEUE, { connection: options.connection });
|
|
20015
|
+
this.queue.on("error", (err) => {
|
|
20016
|
+
this.logger.error(`connection refresh scheduler queue error: ${err.message}`, { stack: err.stack });
|
|
20017
|
+
});
|
|
20018
|
+
this.intervalMs = options.intervalMs ?? 9e5;
|
|
20019
|
+
this.attempts = options.attempts ?? 3;
|
|
20020
|
+
this.backoffDelayMs = options.backoffDelayMs ?? 3e4;
|
|
20021
|
+
}
|
|
20022
|
+
/**
|
|
20023
|
+
* Ensure the single repeatable sweep exists. Idempotent — a re-`ensure()` on
|
|
20024
|
+
* boot upserts the one scheduler in place, so a restart never stacks a second
|
|
20025
|
+
* sweep.
|
|
20026
|
+
*/
|
|
20027
|
+
async ensure() {
|
|
20028
|
+
const repeat = { every: this.intervalMs };
|
|
20029
|
+
await this.queue.upsertJobScheduler(CONNECTION_REFRESH_SCHEDULER_ID, repeat, {
|
|
20030
|
+
name: CONNECTION_REFRESH_JOB_NAME,
|
|
20031
|
+
opts: {
|
|
20032
|
+
attempts: this.attempts,
|
|
20033
|
+
backoff: {
|
|
20034
|
+
type: "exponential",
|
|
20035
|
+
delay: this.backoffDelayMs
|
|
20036
|
+
},
|
|
20037
|
+
removeOnComplete: true,
|
|
20038
|
+
removeOnFail: {
|
|
20039
|
+
age: 1440 * 60,
|
|
20040
|
+
count: 1e3
|
|
20041
|
+
}
|
|
20042
|
+
}
|
|
20043
|
+
});
|
|
20044
|
+
}
|
|
20045
|
+
/** Remove the repeatable sweep. Idempotent. */
|
|
20046
|
+
async remove() {
|
|
20047
|
+
return { removed: await this.queue.removeJobScheduler(CONNECTION_REFRESH_SCHEDULER_ID) };
|
|
20048
|
+
}
|
|
20049
|
+
async close() {
|
|
20050
|
+
await this.queue.close();
|
|
20051
|
+
}
|
|
20052
|
+
};
|
|
20053
|
+
}));
|
|
20054
|
+
//#endregion
|
|
20055
|
+
//#region ../../libs/platform/connect-broker/src/lib/refresh/connection-refresh-worker.ts
|
|
20056
|
+
var DEFAULT_CONCURRENCY, ConnectionRefreshWorker;
|
|
20057
|
+
var init_connection_refresh_worker = __esmMin((() => {
|
|
20058
|
+
init_logger();
|
|
20059
|
+
init_constants();
|
|
20060
|
+
DEFAULT_CONCURRENCY = 1;
|
|
20061
|
+
ConnectionRefreshWorker = class {
|
|
20062
|
+
constructor(options) {
|
|
20063
|
+
this.options = options;
|
|
20064
|
+
this.logger = options.logger ?? consoleRefreshLogger;
|
|
20065
|
+
this.worker = new Worker(CONNECTION_REFRESH_QUEUE, async (job) => this.process(job), {
|
|
20066
|
+
connection: options.connection,
|
|
20067
|
+
concurrency: options.concurrency ?? DEFAULT_CONCURRENCY
|
|
20068
|
+
});
|
|
20069
|
+
this.worker.on("error", (err) => {
|
|
20070
|
+
this.logger.error(`connection refresh worker error: ${err.message}`, { stack: err.stack });
|
|
20071
|
+
});
|
|
20072
|
+
this.worker.on("failed", (job, err) => {
|
|
20073
|
+
if (job && job.attemptsMade < (job.opts.attempts ?? 1)) return;
|
|
20074
|
+
this.logger.warn("connection refresh: sweep exhausted", {
|
|
20075
|
+
attempts: job?.attemptsMade ?? null,
|
|
20076
|
+
reason: err.message
|
|
20077
|
+
});
|
|
20078
|
+
});
|
|
20079
|
+
}
|
|
20080
|
+
async waitUntilReady() {
|
|
20081
|
+
await this.worker.waitUntilReady();
|
|
20082
|
+
}
|
|
20083
|
+
async close(force = false) {
|
|
20084
|
+
await this.worker.close(force);
|
|
20085
|
+
}
|
|
20086
|
+
async process(_job) {
|
|
20087
|
+
const results = await this.options.processor.sweep();
|
|
20088
|
+
const revoked = results.filter((r) => r.action === "revoked").length;
|
|
20089
|
+
const transient = results.filter((r) => r.action === "transient-failure").length;
|
|
20090
|
+
const refreshed = results.filter((r) => r.action === "refreshed").length;
|
|
20091
|
+
this.logger.warn("connection refresh: sweep complete", {
|
|
20092
|
+
total: results.length,
|
|
20093
|
+
refreshed,
|
|
20094
|
+
transient,
|
|
20095
|
+
revoked
|
|
20096
|
+
});
|
|
20097
|
+
return results;
|
|
20098
|
+
}
|
|
20099
|
+
};
|
|
20100
|
+
}));
|
|
20101
|
+
//#endregion
|
|
20102
|
+
//#region ../../libs/platform/connect-broker/src/lib/refresh/grant-refresh-runtime.ts
|
|
20103
|
+
/**
|
|
20104
|
+
* Build the broker's grant-backed refresh processor: the generic
|
|
20105
|
+
* `ConnectionRefreshProcessor` with the three grant-backed ports plumbed in. The
|
|
20106
|
+
* caller supplies the store + exchanger (the broker's own seams) and the
|
|
20107
|
+
* store-side listing + health-writing queries.
|
|
20108
|
+
*/
|
|
20109
|
+
function createGrantRefreshProcessor(deps) {
|
|
20110
|
+
return new ConnectionRefreshProcessor({
|
|
20111
|
+
connections: new GrantRefreshableConnectionLoader(deps.lister),
|
|
20112
|
+
refresher: new GrantConnectionRefresher(deps.store, deps.exchanger),
|
|
20113
|
+
health: new GrantConnectionHealthWriter(deps.healthSetter),
|
|
20114
|
+
cascade: new NoopGrantCascadeDisabler(),
|
|
20115
|
+
...deps.leadMs !== void 0 ? { leadMs: deps.leadMs } : {},
|
|
20116
|
+
...deps.now !== void 0 ? { now: deps.now } : {},
|
|
20117
|
+
...deps.logger !== void 0 ? { logger: deps.logger } : {}
|
|
20118
|
+
});
|
|
20119
|
+
}
|
|
20120
|
+
/**
|
|
20121
|
+
* Build the broker's refresh SCHEDULER (the single repeatable sweep). A thin
|
|
20122
|
+
* re-instantiation of the connection-refresh scheduler — NOT a fork — so the
|
|
20123
|
+
* cadence / retry / backoff behaviour stays shared.
|
|
20124
|
+
*/
|
|
20125
|
+
function createGrantRefreshScheduler(options) {
|
|
20126
|
+
return new ConnectionRefreshScheduler(options);
|
|
20127
|
+
}
|
|
20128
|
+
/**
|
|
20129
|
+
* Build the broker's refresh WORKER, draining the sweep queue and running the
|
|
20130
|
+
* grant-backed processor on each tick. A thin re-instantiation of the
|
|
20131
|
+
* connection-refresh worker.
|
|
20132
|
+
*/
|
|
20133
|
+
function createGrantRefreshWorker(options) {
|
|
20134
|
+
return new ConnectionRefreshWorker({
|
|
20135
|
+
connection: options.connection,
|
|
20136
|
+
processor: options.processor,
|
|
20137
|
+
...options.concurrency !== void 0 ? { concurrency: options.concurrency } : {},
|
|
20138
|
+
...options.logger !== void 0 ? { logger: options.logger } : {}
|
|
20139
|
+
});
|
|
20140
|
+
}
|
|
20141
|
+
/**
|
|
20142
|
+
* Arm the box's proactive grant-refresh runtime IN ONE CALL (STA-265): build the
|
|
20143
|
+
* single repeatable {@link createGrantRefreshScheduler | sweep scheduler}, start the
|
|
20144
|
+
* {@link createGrantRefreshWorker | worker} that drains it, and `ensure()` the
|
|
20145
|
+
* repeatable exists. Idempotent across boots — the scheduler upserts the ONE
|
|
20146
|
+
* repeatable in place, so a re-arm never stacks a second sweep.
|
|
20147
|
+
*
|
|
20148
|
+
* This is the seam the box composition root (`emulator-server`, which runs the
|
|
20149
|
+
* agent box in cloud BYOC + self-host + the emulator) calls when a Redis URL is
|
|
20150
|
+
* present. The processor is built from the SAME box `DrizzleBrokerStore` the token
|
|
20151
|
+
* hot path uses, so every refresh reads/writes the box `connection_grant` rows —
|
|
20152
|
+
* never the control-plane DB.
|
|
20153
|
+
*/
|
|
20154
|
+
async function createGrantRefreshRuntime(deps) {
|
|
20155
|
+
const scheduler = createGrantRefreshScheduler({
|
|
20156
|
+
connection: deps.connection,
|
|
20157
|
+
...deps.intervalMs !== void 0 ? { intervalMs: deps.intervalMs } : {},
|
|
20158
|
+
...deps.logger !== void 0 ? { logger: deps.logger } : {}
|
|
20159
|
+
});
|
|
20160
|
+
const worker = createGrantRefreshWorker({
|
|
20161
|
+
connection: deps.connection,
|
|
20162
|
+
processor: deps.processor,
|
|
20163
|
+
...deps.concurrency !== void 0 ? { concurrency: deps.concurrency } : {},
|
|
20164
|
+
...deps.logger !== void 0 ? { logger: deps.logger } : {}
|
|
20165
|
+
});
|
|
20166
|
+
await scheduler.ensure();
|
|
20167
|
+
return { async close() {
|
|
20168
|
+
await worker.close().catch(() => void 0);
|
|
20169
|
+
await scheduler.close().catch(() => void 0);
|
|
20170
|
+
} };
|
|
20171
|
+
}
|
|
20172
|
+
var init_grant_refresh_runtime = __esmMin((() => {
|
|
20173
|
+
init_connection_refresh_processor();
|
|
20174
|
+
init_connection_refresh_scheduler();
|
|
20175
|
+
init_connection_refresh_worker();
|
|
20176
|
+
init_grant_refresh_ports();
|
|
20177
|
+
}));
|
|
20178
|
+
//#endregion
|
|
19495
20179
|
//#region ../../libs/shared/connect-registry/src/lib/triggers/trigger-descriptor.ts
|
|
19496
20180
|
var triggerCursorSchema, triggerListStepSchema, triggerItemStepSchema, declarativePollSchema, strategyPollSchema, pollSpecSchema, triggerDescriptorSchema;
|
|
19497
20181
|
var init_trigger_descriptor = __esmMin((() => {
|
|
@@ -21043,6 +21727,8 @@ var init_src$2 = __esmMin((() => {
|
|
|
21043
21727
|
init_src$4();
|
|
21044
21728
|
init_src$11();
|
|
21045
21729
|
init_schema();
|
|
21730
|
+
init_grant_refresh_ports();
|
|
21731
|
+
init_grant_refresh_runtime();
|
|
21046
21732
|
init_connect_action_executor();
|
|
21047
21733
|
init_spec_introspection();
|
|
21048
21734
|
init_connector_definition_from_record();
|
|
@@ -21378,6 +22064,7 @@ var makeContextFactory = (opts) => {
|
|
|
21378
22064
|
}
|
|
21379
22065
|
}),
|
|
21380
22066
|
bindings,
|
|
22067
|
+
exchanger,
|
|
21381
22068
|
brokerCallbackBaseUrl,
|
|
21382
22069
|
async close() {
|
|
21383
22070
|
await sql.end({ timeout: 5 }).catch(() => void 0);
|
|
@@ -21398,7 +22085,7 @@ var createConnectRouter = (opts, sharedContext) => {
|
|
|
21398
22085
|
const getContext = sharedContext ?? makeContextFactory(opts);
|
|
21399
22086
|
const registry = opts.registry ?? createRegistryStore();
|
|
21400
22087
|
const app = new Hono();
|
|
21401
|
-
app.post("/connectors", async (c) => {
|
|
22088
|
+
app.post("/connectors", requireCapability("connections:manage"), async (c) => {
|
|
21402
22089
|
const raw = await c.req.json().catch(() => ({}));
|
|
21403
22090
|
const parsed = registerConnectorRequestSchema.safeParse(raw);
|
|
21404
22091
|
if (!parsed.success) return c.json(invalidPayload(parsed.error.issues), 400);
|
|
@@ -21434,10 +22121,10 @@ var createConnectRouter = (opts, sharedContext) => {
|
|
|
21434
22121
|
displayName: persisted.displayName
|
|
21435
22122
|
});
|
|
21436
22123
|
});
|
|
21437
|
-
app.get("/catalog", (c) => {
|
|
22124
|
+
app.get("/catalog", requireCapability("connections:manage"), (c) => {
|
|
21438
22125
|
return c.json({ items: registry.list().map(connectorDefToCatalogEntry) });
|
|
21439
22126
|
});
|
|
21440
|
-
app.get("/connectors", async (c) => {
|
|
22127
|
+
app.get("/connectors", requireCapability("connections:manage"), async (c) => {
|
|
21441
22128
|
const rows = await (await getContext()).sql`
|
|
21442
22129
|
SELECT id, provider, connector_id, client_id, client_secret_ciphertext,
|
|
21443
22130
|
authorize_url, token_url, scopes, auth_mode, placement, display_name
|
|
@@ -21446,7 +22133,7 @@ var createConnectRouter = (opts, sharedContext) => {
|
|
|
21446
22133
|
ORDER BY provider`;
|
|
21447
22134
|
return c.json({ items: rows.map((row) => connectorRowToResponse(row, registry)) });
|
|
21448
22135
|
});
|
|
21449
|
-
app.delete("/connectors/:connectorId", async (c) => {
|
|
22136
|
+
app.delete("/connectors/:connectorId", requireCapability("connections:manage"), async (c) => {
|
|
21450
22137
|
const connectorId = c.req.param("connectorId");
|
|
21451
22138
|
if ((await (await getContext()).sql`
|
|
21452
22139
|
DELETE FROM connector
|
|
@@ -21460,7 +22147,7 @@ var createConnectRouter = (opts, sharedContext) => {
|
|
|
21460
22147
|
status: "deleted"
|
|
21461
22148
|
});
|
|
21462
22149
|
});
|
|
21463
|
-
app.get("/connectors/:connectorId/operations", async (c) => {
|
|
22150
|
+
app.get("/connectors/:connectorId/operations", requireCapability("connections:manage"), async (c) => {
|
|
21464
22151
|
const connectorId = c.req.param("connectorId");
|
|
21465
22152
|
const connector = await (await getContext()).store.getConnector(DEV_ORG_ID, connectorId);
|
|
21466
22153
|
if (!connector) return c.json({
|
|
@@ -21484,7 +22171,7 @@ var createConnectRouter = (opts, sharedContext) => {
|
|
|
21484
22171
|
}, 200);
|
|
21485
22172
|
}
|
|
21486
22173
|
});
|
|
21487
|
-
app.post("/connectors/:connectorId/test", async (c) => {
|
|
22174
|
+
app.post("/connectors/:connectorId/test", requireCapability("connections:manage"), async (c) => {
|
|
21488
22175
|
const connectorId = c.req.param("connectorId");
|
|
21489
22176
|
const ctx = await getContext();
|
|
21490
22177
|
const connector = await ctx.store.getConnector(DEV_ORG_ID, connectorId);
|
|
@@ -21515,7 +22202,7 @@ var createConnectRouter = (opts, sharedContext) => {
|
|
|
21515
22202
|
}
|
|
21516
22203
|
});
|
|
21517
22204
|
app.get("/connected", (c) => c.html("<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\" /><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" /><title>Connected</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;background:#0b0b0c;color:#e7e7ea;display:flex;min-height:100vh;align-items:center;justify-content:center;margin:0}.card{text-align:center;max-width:28rem;padding:2rem}.tick{font-size:2.5rem}h1{font-size:1.1rem;margin:.75rem 0 .25rem}p{color:#a1a1aa;font-size:.9rem;line-height:1.5}</style></head><body><div class=\"card\"><div class=\"tick\">✓</div><h1>Account connected</h1><p>The credential is ready. You can close this tab and return to Studio — it now shows the grant under Connections → Grants.</p></div></body></html>"));
|
|
21518
|
-
app.post("/connectors/:connectorId/authorize", async (c) => {
|
|
22205
|
+
app.post("/connectors/:connectorId/authorize", requireCapability("connections:manage"), async (c) => {
|
|
21519
22206
|
const connectorId = c.req.param("connectorId");
|
|
21520
22207
|
const body = await c.req.json().catch(() => ({}));
|
|
21521
22208
|
const agentName = typeof body.agentName === "string" && body.agentName ? body.agentName : void 0;
|
|
@@ -21543,7 +22230,7 @@ var createConnectRouter = (opts, sharedContext) => {
|
|
|
21543
22230
|
return brokerErrorResponse(c, err);
|
|
21544
22231
|
}
|
|
21545
22232
|
});
|
|
21546
|
-
app.get("/grants", async (c) => {
|
|
22233
|
+
app.get("/grants", requireCapability("connections:manage"), async (c) => {
|
|
21547
22234
|
const rows = await (await getContext()).sql`
|
|
21548
22235
|
SELECT id, connector, principal_key, expires_at, external_subject, health_status
|
|
21549
22236
|
FROM connection_grant
|
|
@@ -21551,7 +22238,7 @@ var createConnectRouter = (opts, sharedContext) => {
|
|
|
21551
22238
|
ORDER BY connector, principal_key`;
|
|
21552
22239
|
return c.json({ items: rows.map(grantRowToResponse) });
|
|
21553
22240
|
});
|
|
21554
|
-
app.delete("/grants/:grantId", async (c) => {
|
|
22241
|
+
app.delete("/grants/:grantId", requireCapability("connections:manage"), async (c) => {
|
|
21555
22242
|
const grantId = c.req.param("grantId");
|
|
21556
22243
|
const ctx = await getContext();
|
|
21557
22244
|
const row = (await ctx.sql`
|
|
@@ -21792,13 +22479,23 @@ var createConnectHandlers = (opts) => {
|
|
|
21792
22479
|
principal: request.principal
|
|
21793
22480
|
});
|
|
21794
22481
|
};
|
|
22482
|
+
const createRefreshProcessor = async () => {
|
|
22483
|
+
const ctx = await shared();
|
|
22484
|
+
return createGrantRefreshProcessor({
|
|
22485
|
+
store: ctx.store,
|
|
22486
|
+
exchanger: ctx.exchanger,
|
|
22487
|
+
lister: { listRefreshableGrants: () => ctx.store.listRefreshableGrants() },
|
|
22488
|
+
healthSetter: { setGrantHealth: (input) => ctx.store.setGrantHealth(input) }
|
|
22489
|
+
});
|
|
22490
|
+
};
|
|
21795
22491
|
return {
|
|
21796
22492
|
management: createConnectRouter(opts, shared),
|
|
21797
22493
|
callback: createConnectCallbackRouter(opts, shared),
|
|
21798
22494
|
consumerCallback: createConnectConsumerCallbackRouter(opts, shared),
|
|
21799
22495
|
token: createConnectTokenRouter(opts, shared),
|
|
21800
22496
|
issueToken,
|
|
21801
|
-
evict
|
|
22497
|
+
evict,
|
|
22498
|
+
createRefreshProcessor
|
|
21802
22499
|
};
|
|
21803
22500
|
};
|
|
21804
22501
|
var mergedCallbackParams = async (req) => {
|
|
@@ -28806,8 +29503,9 @@ var init_workflow_start_service = __esmMin((() => {
|
|
|
28806
29503
|
const resolved = await startResolvedWorkflow(deps, name, input);
|
|
28807
29504
|
if (resolved.queued) throw new Error(`Cannot await workflow "${name}": it was enqueued behind an active serialized run.`);
|
|
28808
29505
|
const { run } = resolved;
|
|
28809
|
-
|
|
28810
|
-
|
|
29506
|
+
const returnValue = run.returnValue;
|
|
29507
|
+
if (!returnValue) throw new Error(`Cannot await workflow "${name}": the runtime did not expose the run's return value (start() must resolve to a WDK Run with a \`returnValue\`).`);
|
|
29508
|
+
const rawOutput = await returnValue;
|
|
28811
29509
|
const outputValidator = await loadValidator(deps, name, "output");
|
|
28812
29510
|
if (outputValidator) {
|
|
28813
29511
|
const verdict = await validateWorkflowInput(outputValidator, rawOutput);
|
|
@@ -31595,8 +32293,10 @@ var init_mount = __esmMin((() => {
|
|
|
31595
32293
|
//#endregion
|
|
31596
32294
|
//#region ../../libs/runtime/workspace-runtime/src/lib/server/emulator-server.ts
|
|
31597
32295
|
init_src$13();
|
|
32296
|
+
init_src$11();
|
|
31598
32297
|
init_src$9();
|
|
31599
32298
|
init_src$6();
|
|
32299
|
+
init_src$2();
|
|
31600
32300
|
init_registry$1();
|
|
31601
32301
|
init_resume_interrupt();
|
|
31602
32302
|
/**
|
|
@@ -31636,6 +32336,23 @@ var buildEmulatorApp = (opts, hooks = {}) => {
|
|
|
31636
32336
|
exposeHeaders: ["X-Stackbone-Run-Id"]
|
|
31637
32337
|
}));
|
|
31638
32338
|
if (opts.proxyAuth?.secret) app.use("*", createRequestSignatureVerifier({ secret: opts.proxyAuth.secret }));
|
|
32339
|
+
if (opts.jwtAuth) app.use("*", createJwtVerifier(opts.jwtAuth));
|
|
32340
|
+
if (opts.proxyChallenge?.secret) {
|
|
32341
|
+
const challengeSecret = opts.proxyChallenge.secret;
|
|
32342
|
+
app.get(PROXY_CHALLENGE_PATH, (c) => {
|
|
32343
|
+
const url = new URL(c.req.url);
|
|
32344
|
+
const ok = verifyStackboneSignature({
|
|
32345
|
+
secret: challengeSecret,
|
|
32346
|
+
header: c.req.header(STACKBONE_SIGNATURE_HEADER),
|
|
32347
|
+
payload: proxyRequestPayload({
|
|
32348
|
+
method: "GET",
|
|
32349
|
+
path: url.pathname + url.search,
|
|
32350
|
+
body: ""
|
|
32351
|
+
})
|
|
32352
|
+
});
|
|
32353
|
+
return c.json({ ok }, ok ? 200 : 401);
|
|
32354
|
+
});
|
|
32355
|
+
}
|
|
31639
32356
|
app.get("/api/contract", (c) => c.json(buildContractResponse()));
|
|
31640
32357
|
app.get("/", (c) => c.html(renderStudioStub({
|
|
31641
32358
|
agentSlug: opts.agent.slug,
|
|
@@ -31746,12 +32463,15 @@ var buildEmulatorApp = (opts, hooks = {}) => {
|
|
|
31746
32463
|
app.route("/api/prompts", createPromptsRouter({ connectionString: opts.injection.services.postgresUrl }));
|
|
31747
32464
|
const connectHandlers = createConnectHandlers({
|
|
31748
32465
|
connectionString: opts.injection.services.postgresUrl,
|
|
31749
|
-
hmacSecret:
|
|
32466
|
+
hmacSecret: opts.connectIdentity?.hmacSecret ?? "dev-stackbone-local-emulator-hmac-secret",
|
|
32467
|
+
...opts.connectIdentity?.encryptionKey ? { encryptionKey: opts.connectIdentity.encryptionKey } : {},
|
|
31750
32468
|
...opts.connectRegistry ? { registry: opts.connectRegistry } : {},
|
|
32469
|
+
...opts.connectCallbackBaseUrl ? { brokerCallbackBaseUrl: `${opts.connectCallbackBaseUrl.replace(/\/+$/, "")}/api/connect/callback` } : {},
|
|
31751
32470
|
...hooks.onWorkflowHookResume ? { onConnectionResume: (token) => hooks.onWorkflowHookResume?.(token, { status: "connected" }) } : {}
|
|
31752
32471
|
});
|
|
31753
32472
|
hooks.captureConnectIssueToken?.(connectHandlers.issueToken);
|
|
31754
32473
|
hooks.captureConnectEvictToken?.(connectHandlers.evict);
|
|
32474
|
+
hooks.captureConnectRefreshProcessorFactory?.(connectHandlers.createRefreshProcessor);
|
|
31755
32475
|
app.route("/api/connect/callback", connectHandlers.callback);
|
|
31756
32476
|
app.route("/connect/v1/callback", connectHandlers.consumerCallback);
|
|
31757
32477
|
app.route("/api/connect", connectHandlers.token);
|
|
@@ -31824,7 +32544,7 @@ var startEmulatorServer = async (opts) => {
|
|
|
31824
32544
|
body: JSON.stringify(payload ?? {})
|
|
31825
32545
|
})).catch((err) => console.warn(`[workflow-hook-resume] decide-driven resume for token "${token}" failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
31826
32546
|
};
|
|
31827
|
-
const connectProjectRoot = opts.workflow?.projectRoot;
|
|
32547
|
+
const connectProjectRoot = opts.workflow?.projectRoot ?? opts.deepAgents?.projectRoot;
|
|
31828
32548
|
const onConnectLog = opts.onConnectLog ?? ((level, message) => console[level === "warn" ? "warn" : "debug"](`[connect-registry] ${message}`));
|
|
31829
32549
|
const seededRegistry = opts.connectRegistry ?? await seedConnectorRegistry(connectProjectRoot, onConnectLog);
|
|
31830
32550
|
const seededOpts = {
|
|
@@ -31833,6 +32553,8 @@ var startEmulatorServer = async (opts) => {
|
|
|
31833
32553
|
};
|
|
31834
32554
|
let connectIssueToken;
|
|
31835
32555
|
let connectEvictToken;
|
|
32556
|
+
let connectRefreshProcessorFactory;
|
|
32557
|
+
let grantRefreshRuntime;
|
|
31836
32558
|
let deepAgentRegistry;
|
|
31837
32559
|
let deepAgentsProjectRoot;
|
|
31838
32560
|
const deepAgentLog = opts.onDeepAgentLog ? {
|
|
@@ -31871,6 +32593,9 @@ var startEmulatorServer = async (opts) => {
|
|
|
31871
32593
|
captureConnectEvictToken: (fn) => {
|
|
31872
32594
|
connectEvictToken = fn;
|
|
31873
32595
|
},
|
|
32596
|
+
captureConnectRefreshProcessorFactory: (fn) => {
|
|
32597
|
+
connectRefreshProcessorFactory = fn;
|
|
32598
|
+
},
|
|
31874
32599
|
getDeepAgentRegistry: () => deepAgentRegistry
|
|
31875
32600
|
});
|
|
31876
32601
|
appHolder.app = app;
|
|
@@ -31907,6 +32632,19 @@ var startEmulatorServer = async (opts) => {
|
|
|
31907
32632
|
};
|
|
31908
32633
|
serialSupportHolder.support = workflowRuntimeHandle.serialSupport;
|
|
31909
32634
|
}
|
|
32635
|
+
if (opts.connectRefreshRedisUrl && connectRefreshProcessorFactory) {
|
|
32636
|
+
const redisUrl = opts.connectRefreshRedisUrl;
|
|
32637
|
+
const buildRefreshProcessor = connectRefreshProcessorFactory;
|
|
32638
|
+
try {
|
|
32639
|
+
const processor = await buildRefreshProcessor();
|
|
32640
|
+
grantRefreshRuntime = await createGrantRefreshRuntime({
|
|
32641
|
+
connection: redisConnection(redisUrl),
|
|
32642
|
+
processor
|
|
32643
|
+
});
|
|
32644
|
+
} catch (err) {
|
|
32645
|
+
console.warn(`[grant-refresh] failed to arm the proactive refresh runtime: ${err instanceof Error ? err.message : String(err)}`);
|
|
32646
|
+
}
|
|
32647
|
+
}
|
|
31910
32648
|
if (!opts.workflow) {
|
|
31911
32649
|
const fallbackDiscovery = () => ({
|
|
31912
32650
|
agents: deepAgentManifestEntries(deepAgentRegistry),
|
|
@@ -31929,9 +32667,9 @@ var startEmulatorServer = async (opts) => {
|
|
|
31929
32667
|
createApproval: (input) => opts.approvals.create(input),
|
|
31930
32668
|
decideStudioApproval: (threadId, decision) => retireStudioApprovalForThread(opts.approvals, threadId, decision),
|
|
31931
32669
|
connectTransport: {
|
|
31932
|
-
hmacSecret:
|
|
32670
|
+
hmacSecret: opts.connectIdentity?.hmacSecret ?? "dev-stackbone-local-emulator-hmac-secret",
|
|
31933
32671
|
brokerBaseUrl: `http://127.0.0.1:${opts.port}`,
|
|
31934
|
-
installationId:
|
|
32672
|
+
installationId: opts.connectIdentity?.installationId ?? "local-dev"
|
|
31935
32673
|
},
|
|
31936
32674
|
logger: deepAgentLog
|
|
31937
32675
|
});
|
|
@@ -31973,6 +32711,7 @@ var startEmulatorServer = async (opts) => {
|
|
|
31973
32711
|
workflowRunSweep?.stop();
|
|
31974
32712
|
workflowStepUnsubscribe?.();
|
|
31975
32713
|
await workflowRuntimeHandle?.close().catch(() => void 0);
|
|
32714
|
+
await grantRefreshRuntime?.close().catch(() => void 0);
|
|
31976
32715
|
if (opts.workflow) await opts.workflow.world.close().catch(() => void 0);
|
|
31977
32716
|
await deepAgentRegistry?.close().catch(() => void 0);
|
|
31978
32717
|
await new Promise((resolve) => {
|
|
@@ -32024,169 +32763,6 @@ var buildDevServices = (ports) => ({
|
|
|
32024
32763
|
var buildDevRedisUrl = (redisPort) => `redis://127.0.0.1:${redisPort}`;
|
|
32025
32764
|
var DEV_AGENT_SECRET_KEY = createHash("sha256").update(STUDIO_LOCAL_DEV_ENCRYPTION_KEY).digest("base64");
|
|
32026
32765
|
//#endregion
|
|
32027
|
-
//#region ../../libs/runtime/workspace-runtime/src/lib/connect-inbound/trigger-subscriptions-store.ts
|
|
32028
|
-
init_store_pool();
|
|
32029
|
-
init_uuid();
|
|
32030
|
-
var SUBSCRIPTION_COLUMNS = "id, connector, trigger_id, principal_key, workflow_name, mapping, mapping_graph, correlation_key, enabled, mailbox";
|
|
32031
|
-
var rowToSubscription = (row) => ({
|
|
32032
|
-
id: row.id,
|
|
32033
|
-
connector: row.connector,
|
|
32034
|
-
trigger: row.trigger_id,
|
|
32035
|
-
principalKey: row.principal_key,
|
|
32036
|
-
workflowName: row.workflow_name,
|
|
32037
|
-
mapping: row.mapping,
|
|
32038
|
-
mappingGraph: row.mapping_graph ?? null,
|
|
32039
|
-
correlationKey: row.correlation_key ?? null,
|
|
32040
|
-
enabled: row.enabled,
|
|
32041
|
-
mailbox: row.mailbox ?? null
|
|
32042
|
-
});
|
|
32043
|
-
var createTriggerSubscriptionsStore = (opts) => {
|
|
32044
|
-
const { pool, assertOpen, close } = createStorePool("TriggerSubscriptionsStore", opts.connectionString);
|
|
32045
|
-
return {
|
|
32046
|
-
async register(input) {
|
|
32047
|
-
assertOpen();
|
|
32048
|
-
const id = randomUUID();
|
|
32049
|
-
const { rows } = await pool.query(`INSERT INTO stackbone_platform.trigger_subscriptions
|
|
32050
|
-
(id, connector, trigger_id, principal_key, workflow_name, mapping, mapping_graph, correlation_key, enabled, mailbox)
|
|
32051
|
-
VALUES ($1::uuid, $2, $3, $4, $5, $6, $7::jsonb, $8, $9, $10)
|
|
32052
|
-
RETURNING ${SUBSCRIPTION_COLUMNS}`, [
|
|
32053
|
-
id,
|
|
32054
|
-
input.connector,
|
|
32055
|
-
input.trigger,
|
|
32056
|
-
input.principalKey,
|
|
32057
|
-
input.workflowName,
|
|
32058
|
-
input.mapping ?? null,
|
|
32059
|
-
input.mappingGraph == null ? null : JSON.stringify(input.mappingGraph),
|
|
32060
|
-
input.correlationKey ?? null,
|
|
32061
|
-
input.enabled ?? true,
|
|
32062
|
-
input.mailbox ?? null
|
|
32063
|
-
]);
|
|
32064
|
-
return rowToSubscription(rows[0]);
|
|
32065
|
-
},
|
|
32066
|
-
async list() {
|
|
32067
|
-
assertOpen();
|
|
32068
|
-
const { rows } = await pool.query(`SELECT ${SUBSCRIPTION_COLUMNS}
|
|
32069
|
-
FROM stackbone_platform.trigger_subscriptions
|
|
32070
|
-
ORDER BY created_at DESC, id DESC`);
|
|
32071
|
-
return rows.map(rowToSubscription);
|
|
32072
|
-
},
|
|
32073
|
-
async get(id) {
|
|
32074
|
-
if (!isUuid(id)) return null;
|
|
32075
|
-
assertOpen();
|
|
32076
|
-
const { rows } = await pool.query(`SELECT ${SUBSCRIPTION_COLUMNS}
|
|
32077
|
-
FROM stackbone_platform.trigger_subscriptions
|
|
32078
|
-
WHERE id = $1::uuid`, [id]);
|
|
32079
|
-
return rows[0] ? rowToSubscription(rows[0]) : null;
|
|
32080
|
-
},
|
|
32081
|
-
async update(id, patch) {
|
|
32082
|
-
if (!isUuid(id)) return null;
|
|
32083
|
-
assertOpen();
|
|
32084
|
-
const sets = [];
|
|
32085
|
-
const params = [id];
|
|
32086
|
-
if (patch.enabled !== void 0) {
|
|
32087
|
-
params.push(patch.enabled);
|
|
32088
|
-
sets.push(`enabled = $${params.length}`);
|
|
32089
|
-
}
|
|
32090
|
-
if (patch.mapping !== void 0) {
|
|
32091
|
-
params.push(patch.mapping);
|
|
32092
|
-
sets.push(`mapping = $${params.length}`);
|
|
32093
|
-
}
|
|
32094
|
-
if (patch.mappingGraph !== void 0) {
|
|
32095
|
-
params.push(patch.mappingGraph == null ? null : JSON.stringify(patch.mappingGraph));
|
|
32096
|
-
sets.push(`mapping_graph = $${params.length}::jsonb`);
|
|
32097
|
-
}
|
|
32098
|
-
if (patch.correlationKey !== void 0) {
|
|
32099
|
-
params.push(patch.correlationKey);
|
|
32100
|
-
sets.push(`correlation_key = $${params.length}`);
|
|
32101
|
-
}
|
|
32102
|
-
if (sets.length === 0) return this.get(id);
|
|
32103
|
-
const { rows } = await pool.query(`UPDATE stackbone_platform.trigger_subscriptions
|
|
32104
|
-
SET ${sets.join(", ")}, updated_at = now()
|
|
32105
|
-
WHERE id = $1::uuid
|
|
32106
|
-
RETURNING ${SUBSCRIPTION_COLUMNS}`, params);
|
|
32107
|
-
return rows[0] ? rowToSubscription(rows[0]) : null;
|
|
32108
|
-
},
|
|
32109
|
-
async remove(id) {
|
|
32110
|
-
if (!isUuid(id)) return false;
|
|
32111
|
-
assertOpen();
|
|
32112
|
-
await pool.query("DELETE FROM stackbone_platform.trigger_deliveries WHERE subscription_id = $1::uuid", [id]);
|
|
32113
|
-
return ((await pool.query("DELETE FROM stackbone_platform.trigger_subscriptions WHERE id = $1::uuid", [id])).rowCount ?? 0) > 0;
|
|
32114
|
-
},
|
|
32115
|
-
async listActive() {
|
|
32116
|
-
assertOpen();
|
|
32117
|
-
const { rows } = await pool.query(`SELECT ${SUBSCRIPTION_COLUMNS}
|
|
32118
|
-
FROM stackbone_platform.trigger_subscriptions
|
|
32119
|
-
WHERE enabled
|
|
32120
|
-
ORDER BY created_at ASC, id ASC`);
|
|
32121
|
-
return rows.map(rowToSubscription);
|
|
32122
|
-
},
|
|
32123
|
-
async readCursor(subscriptionId) {
|
|
32124
|
-
assertOpen();
|
|
32125
|
-
const { rows } = await pool.query("SELECT poll_cursor FROM stackbone_platform.trigger_subscriptions WHERE id = $1::uuid", [subscriptionId]);
|
|
32126
|
-
return rows[0]?.poll_cursor ?? null;
|
|
32127
|
-
},
|
|
32128
|
-
async saveCursor({ subscriptionId, cursor, lastPolledAt }) {
|
|
32129
|
-
assertOpen();
|
|
32130
|
-
await pool.query(`UPDATE stackbone_platform.trigger_subscriptions
|
|
32131
|
-
SET poll_cursor = $2::jsonb, last_polled_at = $3::timestamptz, updated_at = now()
|
|
32132
|
-
WHERE id = $1::uuid`, [
|
|
32133
|
-
subscriptionId,
|
|
32134
|
-
cursor == null ? null : JSON.stringify(cursor),
|
|
32135
|
-
lastPolledAt.toISOString()
|
|
32136
|
-
]);
|
|
32137
|
-
},
|
|
32138
|
-
async claim(subscriptionId, providerItemId) {
|
|
32139
|
-
assertOpen();
|
|
32140
|
-
const { rows } = await pool.query(`INSERT INTO stackbone_platform.trigger_deliveries
|
|
32141
|
-
(id, subscription_id, provider_item_id, status)
|
|
32142
|
-
VALUES ($1::uuid, $2::uuid, $3, 'claimed')
|
|
32143
|
-
ON CONFLICT (subscription_id, provider_item_id) DO NOTHING
|
|
32144
|
-
RETURNING id`, [
|
|
32145
|
-
randomUUID(),
|
|
32146
|
-
subscriptionId,
|
|
32147
|
-
providerItemId
|
|
32148
|
-
]);
|
|
32149
|
-
return rows.length > 0;
|
|
32150
|
-
},
|
|
32151
|
-
async markStarted(subscriptionId, providerItemId, runId) {
|
|
32152
|
-
assertOpen();
|
|
32153
|
-
await pool.query(`UPDATE stackbone_platform.trigger_deliveries
|
|
32154
|
-
SET status = 'started', run_id = $3, updated_at = now()
|
|
32155
|
-
WHERE subscription_id = $1::uuid AND provider_item_id = $2`, [
|
|
32156
|
-
subscriptionId,
|
|
32157
|
-
providerItemId,
|
|
32158
|
-
runId
|
|
32159
|
-
]);
|
|
32160
|
-
},
|
|
32161
|
-
async markResumed(subscriptionId, providerItemId, runId) {
|
|
32162
|
-
assertOpen();
|
|
32163
|
-
await pool.query(`UPDATE stackbone_platform.trigger_deliveries
|
|
32164
|
-
SET status = 'resumed', run_id = COALESCE($3, run_id), updated_at = now()
|
|
32165
|
-
WHERE subscription_id = $1::uuid AND provider_item_id = $2`, [
|
|
32166
|
-
subscriptionId,
|
|
32167
|
-
providerItemId,
|
|
32168
|
-
runId ?? null
|
|
32169
|
-
]);
|
|
32170
|
-
},
|
|
32171
|
-
async markRejected(subscriptionId, providerItemId, issues) {
|
|
32172
|
-
assertOpen();
|
|
32173
|
-
await pool.query(`UPDATE stackbone_platform.trigger_deliveries
|
|
32174
|
-
SET status = 'rejected', issues = $3::jsonb, updated_at = now()
|
|
32175
|
-
WHERE subscription_id = $1::uuid AND provider_item_id = $2`, [
|
|
32176
|
-
subscriptionId,
|
|
32177
|
-
providerItemId,
|
|
32178
|
-
JSON.stringify(issues)
|
|
32179
|
-
]);
|
|
32180
|
-
},
|
|
32181
|
-
async release(subscriptionId, providerItemId) {
|
|
32182
|
-
assertOpen();
|
|
32183
|
-
await pool.query(`DELETE FROM stackbone_platform.trigger_deliveries
|
|
32184
|
-
WHERE subscription_id = $1::uuid AND provider_item_id = $2`, [subscriptionId, providerItemId]);
|
|
32185
|
-
},
|
|
32186
|
-
close
|
|
32187
|
-
};
|
|
32188
|
-
};
|
|
32189
|
-
//#endregion
|
|
32190
32766
|
//#region src/services/scaffold/file-plan.ts
|
|
32191
32767
|
/**
|
|
32192
32768
|
* Concatenate plans into one. On a duplicate path the FIRST plan wins, so a
|
|
@@ -32299,7 +32875,7 @@ var calls_workflow_ts_default = "import { z } from \"zod\";\nimport { streamDeep
|
|
|
32299
32875
|
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` (`z.infer`), so there is no\n// second type that drifts.\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) {\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";
|
|
32300
32876
|
//#endregion
|
|
32301
32877
|
//#region src/services/scaffold/template-loader.ts
|
|
32302
|
-
var SDK_TEMPLATE_VERSION = "0.2.
|
|
32878
|
+
var SDK_TEMPLATE_VERSION = "0.2.6";
|
|
32303
32879
|
var PREFIX = "./templates/";
|
|
32304
32880
|
var templates = new Map(Object.entries(/* @__PURE__ */ Object.assign({
|
|
32305
32881
|
"./templates/deep-agents/index.ts.tmpl": index_ts_default,
|
|
@@ -33619,7 +34195,7 @@ var runValidateContract = async (ctx, args, deps = {}) => {
|
|
|
33619
34195
|
const versionCheck = requireVersion(contract);
|
|
33620
34196
|
if (versionCheck.error) violations.push(versionCheck.error.message);
|
|
33621
34197
|
for (const capability of manifest.capabilities) {
|
|
33622
|
-
const check = requireCapability(capability, contract);
|
|
34198
|
+
const check = requireCapability$1(capability, contract);
|
|
33623
34199
|
if (check.error) violations.push(check.error.message);
|
|
33624
34200
|
}
|
|
33625
34201
|
const ok = violations.length === 0;
|
|
@@ -35863,12 +36439,32 @@ async function suggest(sourceSample, targetSchema, sourceSchema) {
|
|
|
35863
36439
|
'from the source event. Return STRICT JSON of the shape ' +
|
|
35864
36440
|
'{ "suggestions": [ { "targetPath": "<dotted path in the target>", "jsonata": "<JSONata expression over the source>", "confidence": <0..1>, "note": "<short reason>" } ] }. ' +
|
|
35865
36441
|
'Only include a suggestion when you are reasonably confident. Omit fields you cannot map. ' +
|
|
35866
|
-
'Return an empty suggestions array if nothing maps
|
|
35867
|
-
|
|
36442
|
+
'Return an empty suggestions array if nothing maps.\\n' +
|
|
36443
|
+
// Without this the model writes paths relative to the JSON envelope below
|
|
36444
|
+
// ("sourceSample.subject"), which evaluates to nothing against a real event.
|
|
36445
|
+
'CRITICAL — each expression is evaluated with THE SOURCE EVENT ITSELF as the root ' +
|
|
36446
|
+
'context. Write the path exactly as it appears inside the event: for an event ' +
|
|
36447
|
+
'{ "subject": "Hi", "from": { "address": "a@b.c" } } the expressions are "subject" and ' +
|
|
36448
|
+
'"from.address". NEVER prefix a path with "sourceSample", "event", "input" or any ' +
|
|
36449
|
+
'other wrapper name — those are labels in the prompt below, not part of the data.\\n' +
|
|
36450
|
+
'Return the BARE field reference by default. Do not change case, trim, reformat or ' +
|
|
36451
|
+
'otherwise decorate a value on your own initiative: copying "subject" into a plain ' +
|
|
36452
|
+
'string field is "subject", not $uppercase(subject). Transform only when the TARGET ' +
|
|
36453
|
+
'field asks for it — a different type, a declared format or enum, or its own ' +
|
|
36454
|
+
'description spelling out the shape it wants — and then use ONLY built-in JSONata ' +
|
|
36455
|
+
'functions, which all start with "$" (e.g. $substring, $join, $number). Never invent ' +
|
|
36456
|
+
'a function name.';
|
|
36457
|
+
const parts = [
|
|
36458
|
+
'SOURCE EVENT SAMPLE (the root context your expressions run against):',
|
|
36459
|
+
JSON.stringify(sourceSample, null, 2),
|
|
36460
|
+
'',
|
|
36461
|
+
'TARGET WORKFLOW INPUT SCHEMA (what targetPath addresses):',
|
|
36462
|
+
JSON.stringify(targetSchema, null, 2),
|
|
36463
|
+
];
|
|
35868
36464
|
if (sourceSchema) {
|
|
35869
|
-
|
|
36465
|
+
parts.push('', 'SOURCE EVENT JSON SCHEMA:', JSON.stringify(sourceSchema, null, 2));
|
|
35870
36466
|
}
|
|
35871
|
-
const user = '
|
|
36467
|
+
const user = parts.join('\\n');
|
|
35872
36468
|
|
|
35873
36469
|
const result = await stackbone.ai.chat.completions.create({
|
|
35874
36470
|
model: MAPPING_SUGGEST_MODEL,
|
|
@@ -37650,6 +38246,7 @@ var startDevSession = async (input) => {
|
|
|
37650
38246
|
onConnectLog: (level, message) => input.ctx.logger[level === "warn" ? "warn" : "debug"]({ stage: "connect" }, message),
|
|
37651
38247
|
onDeepAgentLog: (level, message) => input.ctx.logger[level]({ stage: "deep-agents" }, message),
|
|
37652
38248
|
...workflowRuntime ? { workflow: workflowRuntime } : {},
|
|
38249
|
+
connectRefreshRedisUrl: devRedisUrl,
|
|
37653
38250
|
deepAgents: {
|
|
37654
38251
|
projectRoot: input.projectRoot,
|
|
37655
38252
|
agents: workspace.deepAgents ?? []
|
|
@@ -38242,7 +38839,7 @@ Available topics:
|
|
|
38242
38839
|
cli/agent-protocol The HTTP contract a Stackbone workspace exposes: OpenAI-compatible, Anthropic-compatible and AG-UI chat, durable workflows, and health.
|
|
38243
38840
|
cli/local-development How stackbone dev boots your agents and durable workflows locally, injects env vars and tunnels to cloud Studio.
|
|
38244
38841
|
cli/agent-yaml How a Stackbone project is declared — workspace discovery by convention (deep-agents/<name>/index.ts + workflows/), the optional agent.yaml manifest, and the optional stackbone.config.ts override.
|
|
38245
|
-
cli/troubleshooting Diagnose and fix the common stackbone dev and
|
|
38842
|
+
cli/troubleshooting Diagnose and fix the common stackbone dev and agent-runtime failures, keyed on the error codes the CLI actually emits.
|
|
38246
38843
|
|
|
38247
38844
|
Run \`stackbone docs <topic>\` to view one.
|
|
38248
38845
|
`,
|
|
@@ -38262,9 +38859,10 @@ A Stackbone project is two things working together:
|
|
|
38262
38859
|
approvals, built on the [Workflow SDK](https://workflow-sdk.dev/docs).
|
|
38263
38860
|
Steps run once, are persisted, and retry on failure.
|
|
38264
38861
|
|
|
38265
|
-
You author both with the ambient \`stackbone\` client
|
|
38266
|
-
\`stackbone dev
|
|
38267
|
-
|
|
38862
|
+
You author both with the ambient \`stackbone\` client and run them locally with
|
|
38863
|
+
\`stackbone dev\`. To go live you deploy the agent's container image into your
|
|
38864
|
+
own cloud and register it with \`stackbone link\`. This page gets you to a
|
|
38865
|
+
running agent on your machine.
|
|
38268
38866
|
|
|
38269
38867
|
## Prerequisites
|
|
38270
38868
|
|
|
@@ -38568,8 +39166,8 @@ workspace is read straight from two folders:
|
|
|
38568
39166
|
\`<camelCase(name)>Workflow\` function (e.g. \`qualify-lead.workflow.ts\` exports
|
|
38569
39167
|
\`qualifyLeadWorkflow\`).
|
|
38570
39168
|
|
|
38571
|
-
\`stackbone dev\`
|
|
38572
|
-
|
|
39169
|
+
\`stackbone dev\` reads this convention. An optional \`stackbone.config.ts\`
|
|
39170
|
+
(default-exporting \`defineWorkspace({ workflows })\`)
|
|
38573
39171
|
is an **override** for workflows only: when it exists it wins over the
|
|
38574
39172
|
convention scan for workflows; agents are always discovered from
|
|
38575
39173
|
\`deep-agents/\`. Most projects need no \`stackbone.config.ts\` at all.
|
|
@@ -38656,19 +39254,17 @@ control plane, the one \`stackbone init\`, \`stackbone link\` and
|
|
|
38656
39254
|
stored per environment in \`~/.stackbone/credentials.json\`, so logging
|
|
38657
39255
|
into a different control plane keeps its own selection.
|
|
38658
39256
|
|
|
38659
|
-
|
|
38660
|
-
|
|
38661
|
-
|
|
38662
|
-
organization is to switch first, then create the project there:
|
|
39257
|
+
An already-created project is **not** affected by this setting: it stays with
|
|
39258
|
+
the organization recorded in its \`.stackbone/project.json\`. So the way to work
|
|
39259
|
+
in a specific organization is to switch first, then create the project there:
|
|
38663
39260
|
|
|
38664
39261
|
\`\`\`bash
|
|
38665
39262
|
stackbone organization use # interactive picker (TTY)
|
|
38666
39263
|
stackbone organization use acme # switch straight to a slug
|
|
38667
39264
|
|
|
38668
|
-
#
|
|
39265
|
+
# Create a workspace inside a chosen org:
|
|
38669
39266
|
stackbone organization use acme # 1. make "acme" active
|
|
38670
39267
|
stackbone init my-app --with agent # 2. the new workspace is registered in "acme"
|
|
38671
|
-
stackbone publish # 3. publishes to "acme" (from project.json)
|
|
38672
39268
|
\`\`\`
|
|
38673
39269
|
|
|
38674
39270
|
- With a \`slug\` argument it switches directly (works in CI and \`--json\`).
|
|
@@ -38723,7 +39319,7 @@ rely on it.
|
|
|
38723
39319
|
Scaffold a new workspace. \`init\` is **workspace-first** and **links the
|
|
38724
39320
|
workspace** to your organization: it registers the workspace's identity in
|
|
38725
39321
|
the control plane and writes a \`.stackbone/project.json\` (so \`dev\`,
|
|
38726
|
-
\`
|
|
39322
|
+
\`link\` and the management commands know which org to talk to). You
|
|
38727
39323
|
must be signed in (\`stackbone login\`) first, there is no offline \`init\`.
|
|
38728
39324
|
Alongside the link it writes the workspace shell.
|
|
38729
39325
|
|
|
@@ -38882,16 +39478,56 @@ workspace), \`1\` generic (a name collision without \`--force\`, or an unknown
|
|
|
38882
39478
|
|
|
38883
39479
|
### \`stackbone link\`
|
|
38884
39480
|
|
|
38885
|
-
Link the current directory to an existing organization + agent
|
|
38886
|
-
\`.stackbone/project.json\`,
|
|
39481
|
+
Link the current directory to an existing organization + agent, and register
|
|
39482
|
+
the **deployment** you run that agent on. Writes \`.stackbone/project.json\`,
|
|
39483
|
+
patches \`.gitignore\` and re-runs the
|
|
38887
39484
|
[Stackbone agent skills install](/docs/coding-agents/overview) (same
|
|
38888
39485
|
best-effort step \`init\` performs). Does **not** scaffold starter files,
|
|
38889
39486
|
use \`init\` for that.
|
|
38890
39487
|
|
|
38891
|
-
|
|
38892
|
-
|
|
38893
|
-
|
|
38894
|
-
|
|
39488
|
+
You host the agent yourself: you build and deploy the container image into
|
|
39489
|
+
your own cloud, then tell Stackbone where it lives. That is what the three
|
|
39490
|
+
required flags do. \`--url\` is the public URL the container answers on,
|
|
39491
|
+
\`--secret\` is the \`HMAC_SECRET\` you set on the container (it must match
|
|
39492
|
+
exactly), and \`--tag\` is the image tag you deployed. The tag is the agent's
|
|
39493
|
+
version: there is no separate build step to promote.
|
|
39494
|
+
|
|
39495
|
+
\`\`\`sh
|
|
39496
|
+
stackbone link \\
|
|
39497
|
+
--agent support \\
|
|
39498
|
+
--url https://support.acme.example.com \\
|
|
39499
|
+
--secret "$STACKBONE_HMAC_SECRET" \\
|
|
39500
|
+
--tag 1.4.0
|
|
39501
|
+
\`\`\`
|
|
39502
|
+
|
|
39503
|
+
All three flags are validated before any network call, so a missing one fails
|
|
39504
|
+
immediately instead of half-linking the project. Re-running \`link\` for the same
|
|
39505
|
+
agent updates the existing deployment (the new URL, secret and tag win) rather
|
|
39506
|
+
than creating a second one, so it is safe to run on every deploy.
|
|
39507
|
+
|
|
39508
|
+
| Flag | Type | Description |
|
|
39509
|
+
| ---------- | ------- | ------------------------------------------------------------------------------------------------ |
|
|
39510
|
+
| \`--agent\` | string | Agent slug to link to. Required in non-interactive mode (CI, \`--json\`, \`-y\`). |
|
|
39511
|
+
| \`--url\` | string | **Required.** Public URL where the deployed agent is reachable. |
|
|
39512
|
+
| \`--secret\` | string | **Required.** The \`HMAC_SECRET\` you set on the deployed container. Must match it exactly. |
|
|
39513
|
+
| \`--tag\` | string | **Required.** Tag of the image you deployed (a semver or a digest). This is the agent's version. |
|
|
39514
|
+
| \`--force\` | boolean | Overwrite an existing \`.stackbone/project.json\`. |
|
|
39515
|
+
|
|
39516
|
+
**The deploy then link flow.** You generate an HMAC secret, set it as the
|
|
39517
|
+
\`HMAC_SECRET\` env var on the container, start the container, then run
|
|
39518
|
+
\`stackbone link --agent <slug> --url <url> --secret <value> --tag <tag>\`. The control plane signs
|
|
39519
|
+
every request it later proxies to your box with this secret, and your box
|
|
39520
|
+
verifies the signature, so the two values have to be identical.
|
|
39521
|
+
|
|
39522
|
+
**\`link\` proves the secret before it saves anything.** Before it registers the
|
|
39523
|
+
deployment, \`link\` signs a challenge request to your container and your container
|
|
39524
|
+
verifies it against its own \`HMAC_SECRET\`. A match saves the secret (encrypted at
|
|
39525
|
+
rest). A mismatch stops with an actionable error and saves nothing, so a typo
|
|
39526
|
+
fails at \`link\` time instead of quietly breaking the proxy later. If your box is
|
|
39527
|
+
unreachable, \`link\` says so and saves nothing.
|
|
39528
|
+
|
|
39529
|
+
If you registered a deployment before this check existed, re-run \`stackbone link\`
|
|
39530
|
+
once to validate and re-save the secret.
|
|
38895
39531
|
|
|
38896
39532
|
**JSON payload**
|
|
38897
39533
|
|
|
@@ -38901,6 +39537,13 @@ use \`init\` for that.
|
|
|
38901
39537
|
"agent": { "id": "...", "slug": "...", "name": "..." },
|
|
38902
39538
|
"organization_id": "...",
|
|
38903
39539
|
"local_dev_installation": { "id": "...", "organization_slug": "..." },
|
|
39540
|
+
"deployment": {
|
|
39541
|
+
"id": "...",
|
|
39542
|
+
"url": "https://support.acme.example.com",
|
|
39543
|
+
"image_tag": "1.4.0",
|
|
39544
|
+
"status": "...",
|
|
39545
|
+
"last_active_at": "2026-07-21T09:00:00Z" /* or null before the first call */,
|
|
39546
|
+
},
|
|
38904
39547
|
"target_dir": "/abs/path",
|
|
38905
39548
|
"files_written": [".stackbone/project.json", ".gitignore"],
|
|
38906
39549
|
"skills_install": {
|
|
@@ -39307,61 +39950,13 @@ Classify each migration as \`applied\`, \`pending\` or \`drifted\`
|
|
|
39307
39950
|
**Exit codes**: \`0\` ok (regardless of drift), \`3\` no project, \`1\`
|
|
39308
39951
|
generic (cannot reach the DB).
|
|
39309
39952
|
|
|
39310
|
-
##
|
|
39311
|
-
|
|
39312
|
-
### \`stackbone publish\`
|
|
39313
|
-
|
|
39314
|
-
Package the project into a publishable artifact. \`publish\` reads the
|
|
39315
|
-
**workspace** by convention, every \`deep-agents/<name>/index.ts\` (or an
|
|
39316
|
-
explicit \`stackbone.config.ts\` override for workflows), and compiles
|
|
39317
|
-
every agent and every durable workflow into a verifiable tar at
|
|
39318
|
-
\`dist/stackbone/workspace-bundle.tar\`.
|
|
39319
|
-
|
|
39320
|
-
Native dependencies (\`.node\` add-ons) are rejected up front, the runtime image
|
|
39321
|
-
is pure-JS, so a native bundle would crash at boot.
|
|
39322
|
-
|
|
39323
|
-
| Flag | Type | Default | Description |
|
|
39324
|
-
| -------- | ------- | ------- | -------------------------------------------------------- |
|
|
39325
|
-
| \`--json\` | boolean | \`false\` | Emit structured JSON envelopes instead of the pretty UI. |
|
|
39326
|
-
|
|
39327
|
-
Run \`stackbone publish --help\` for the full reference.
|
|
39328
|
-
|
|
39329
|
-
#### Workspace (discovered by convention)
|
|
39330
|
-
|
|
39331
|
-
Compiles every agent and every durable workflow **on your machine** and packs
|
|
39332
|
-
them into a tar so cold boot only extracts, it never recompiles. In order:
|
|
39333
|
-
|
|
39334
|
-
1. Load the workspace and its agents + workflows.
|
|
39335
|
-
2. **Native-deps gate, per agent**: a \`.node\` add-on aborts with the offending
|
|
39336
|
-
package name.
|
|
39337
|
-
3. Compile each agent to a prebuilt ESM module, keeping the agent-authoring
|
|
39338
|
-
library and its model provider packages external so one copy of each
|
|
39339
|
-
resolves from the runtime image at boot.
|
|
39340
|
-
4. **SDK-external guardrail, per agent**: an agent that inlined \`@stackbone/sdk\`
|
|
39341
|
-
aborts (a second copy of the SDK would break per-run logging).
|
|
39342
|
-
5. Compile the workflows.
|
|
39343
|
-
6. Derive the \`.well-known/workspace.json\` manifest, pack the tar and compute its
|
|
39344
|
-
SHA-256 digest.
|
|
39953
|
+
## Going to production
|
|
39345
39954
|
|
|
39346
|
-
|
|
39347
|
-
|
|
39348
|
-
|
|
39349
|
-
|
|
39350
|
-
|
|
39351
|
-
|
|
39352
|
-
**JSON payload (workspace)**
|
|
39353
|
-
|
|
39354
|
-
\`\`\`jsonc
|
|
39355
|
-
{
|
|
39356
|
-
"schema_version": 1,
|
|
39357
|
-
"kind": "workspace",
|
|
39358
|
-
"digest": "<sha256>",
|
|
39359
|
-
"sizeBytes": 1234567,
|
|
39360
|
-
"agents": ["support", "billing"],
|
|
39361
|
-
"workflows": ["onboarding"],
|
|
39362
|
-
"tar": "dist/stackbone/workspace-bundle.tar",
|
|
39363
|
-
}
|
|
39364
|
-
\`\`\`
|
|
39955
|
+
There is no publish command. You run the agent in **your own cloud**: build
|
|
39956
|
+
the container image, deploy it wherever you host services, then register it
|
|
39957
|
+
with [\`stackbone link\`](#stackbone-link) so Stackbone knows its URL, its HMAC
|
|
39958
|
+
secret and the image tag it is running. The tag is the agent's version, so
|
|
39959
|
+
shipping a new version means deploying a new image and re-running \`link\`.
|
|
39365
39960
|
|
|
39366
39961
|
## Inline help
|
|
39367
39962
|
|
|
@@ -39464,15 +40059,14 @@ Lives under \`~/.stackbone/\`:
|
|
|
39464
40059
|
|
|
39465
40060
|
Lives at the project root:
|
|
39466
40061
|
|
|
39467
|
-
| Path | Tracked? | What it stores
|
|
39468
|
-
| --------------------------------------- | ------------------------------------------------ |
|
|
39469
|
-
| \`stackbone.config.ts\` | **Optional** | An _optional_ workflow override. By default \`stackbone dev\`
|
|
39470
|
-
| \`.stackbone/project.json\` | **No** (gitignored by \`stackbone init\`) | \`{ schemaVersion, organizationId, agentId, controlPlaneUrl }\`. Identifies which organization this workspace points at.
|
|
39471
|
-
| \`.stackbone/*.d.ts\` | **No** (gitignored) | Generated editor types — \`config.d.ts\`, \`agents.d.ts\`, \`connect.d.ts\` — that give the ambient \`stackbone\` client typed autocompletion. Regenerated by \`stackbone dev\`; \`config.d.ts\` also by \`stackbone config types\` (no control plane needed).
|
|
39472
|
-
| \`stackbone.config.json\` | **Yes** | \`{ studio: { corsOrigins } }\` and other team-shared knobs. Reviewable in PRs. **Different file** from \`stackbone.config.ts\` above — the \`.json\` holds CLI/Studio knobs, the \`.ts\` holds the workspace manifest.
|
|
39473
|
-
| \`agent.yaml\` | **Yes** | Manifest. See [agent.yaml reference](/docs/cli/agent-yaml).
|
|
39474
|
-
|
|
|
39475
|
-
| \`.claude/\`, \`.cursor/\`, \`.windsurf/\`, … | **No** (gitignored by \`stackbone init\` / \`link\`) | Per-coding-agent skill directories the upstream \`skills\` registry materialises. See [Agent skills overview](/docs/coding-agents/overview). |
|
|
40062
|
+
| Path | Tracked? | What it stores |
|
|
40063
|
+
| --------------------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
40064
|
+
| \`stackbone.config.ts\` | **Optional** | An _optional_ workflow override. By default \`stackbone dev\` **derives** the workspace registry by convention (scanning every \`deep-agents/<name>/index.ts\` and every \`workflows/<name>.workflow.ts\` file), so most projects need no config file at all. When this file is present (default-exporting \`defineWorkspace({ workflows })\` from \`@stackbone/sdk\`) its \`workflows\` **win** over the convention scan; agents are always discovered from \`deep-agents/\`. See [Workflows](/docs/workflows/overview). |
|
|
40065
|
+
| \`.stackbone/project.json\` | **No** (gitignored by \`stackbone init\`) | \`{ schemaVersion, organizationId, agentId, controlPlaneUrl }\`. Identifies which organization this workspace points at. |
|
|
40066
|
+
| \`.stackbone/*.d.ts\` | **No** (gitignored) | Generated editor types — \`config.d.ts\`, \`agents.d.ts\`, \`connect.d.ts\` — that give the ambient \`stackbone\` client typed autocompletion. Regenerated by \`stackbone dev\`; \`config.d.ts\` also by \`stackbone config types\` (no control plane needed). |
|
|
40067
|
+
| \`stackbone.config.json\` | **Yes** | \`{ studio: { corsOrigins } }\` and other team-shared knobs. Reviewable in PRs. **Different file** from \`stackbone.config.ts\` above — the \`.json\` holds CLI/Studio knobs, the \`.ts\` holds the workspace manifest. |
|
|
40068
|
+
| \`agent.yaml\` | **Yes** | Manifest. See [agent.yaml reference](/docs/cli/agent-yaml). |
|
|
40069
|
+
| \`.claude/\`, \`.cursor/\`, \`.windsurf/\`, … | **No** (gitignored by \`stackbone init\` / \`link\`) | Per-coding-agent skill directories the upstream \`skills\` registry materialises. See [Agent skills overview](/docs/coding-agents/overview). |
|
|
39476
40070
|
|
|
39477
40071
|
## Output contract
|
|
39478
40072
|
|
|
@@ -40144,8 +40738,7 @@ them down in reverse order:
|
|
|
40144
40738
|
### How the workspace is discovered
|
|
40145
40739
|
|
|
40146
40740
|
A **workspace** is discovered by convention from the files on disk, you do
|
|
40147
|
-
not maintain a registry. \`stackbone dev\`
|
|
40148
|
-
project the same way:
|
|
40741
|
+
not maintain a registry. \`stackbone dev\` scans your project this way:
|
|
40149
40742
|
|
|
40150
40743
|
- **Agents**: every folder under \`deep-agents/\` that contains an \`index.ts\`
|
|
40151
40744
|
exporting an agent definition. The folder name is both the agent name and
|
|
@@ -40421,8 +41014,8 @@ the protocol version. If your CLI is older than \`minSupported\` the UI shows a
|
|
|
40421
41014
|
> in only when you need to name your workflows explicitly, and it wins over
|
|
40422
41015
|
> the convention scan for workflows.
|
|
40423
41016
|
>
|
|
40424
|
-
> \`stackbone dev\`
|
|
40425
|
-
>
|
|
41017
|
+
> \`stackbone dev\` reads the convention: it
|
|
41018
|
+
> discovers the workspace from every \`deep-agents/<name>/index.ts\` on disk
|
|
40426
41019
|
> (agents are always discovered this way; \`stackbone.config.ts\` can only
|
|
40427
41020
|
> override the workflow list). When present, \`agent.yaml\` is validated
|
|
40428
41021
|
> strictly and belongs under version control.
|
|
@@ -40430,8 +41023,8 @@ the protocol version. If your CLI is older than \`minSupported\` the UI shows a
|
|
|
40430
41023
|
## Workspace discovery by convention
|
|
40431
41024
|
|
|
40432
41025
|
The runtime derives the workspace from the files on disk, there is no
|
|
40433
|
-
hand-maintained registry. \`stackbone dev\`
|
|
40434
|
-
|
|
41026
|
+
hand-maintained registry. \`stackbone dev\` reads the same two conventions the
|
|
41027
|
+
runtime does:
|
|
40435
41028
|
|
|
40436
41029
|
- **Agents**: every folder under \`deep-agents/\` that contains an \`index.ts\`
|
|
40437
41030
|
entry file. The folder name is both the agent's identity and the \`model\` a
|
|
@@ -40512,17 +41105,6 @@ your editor. They are derived artifacts, git-ignore them:
|
|
|
40512
41105
|
| \`.stackbone/connect.d.ts\` | \`stackbone.connection('<id>')\` and its connector methods. |
|
|
40513
41106
|
| \`.stackbone/config.d.ts\` | \`stackbone.config.get('<key>')\` typed from your config schema. |
|
|
40514
41107
|
|
|
40515
|
-
### What \`stackbone publish\` produces
|
|
40516
|
-
|
|
40517
|
-
For a workspace, \`stackbone publish\` compiles every agent and every workflow on
|
|
40518
|
-
your machine and packs them into a single tar at
|
|
40519
|
-
\`dist/stackbone/workspace-bundle.tar\`, next to a \`workspace-bundle.json\` pointer
|
|
40520
|
-
(digest, size, the agent + workflow names). Inside the tar is a
|
|
40521
|
-
\`.well-known/workspace.json\` manifest the runtime reads at boot to know which
|
|
40522
|
-
agents and workflows to launch. The published bundle is pure JavaScript, native
|
|
40523
|
-
add-ons are rejected up front, so publish fails early if a dependency is not
|
|
40524
|
-
pure-JS.
|
|
40525
|
-
|
|
40526
41108
|
## \`agent.yaml\`: the optional workspace manifest
|
|
40527
41109
|
|
|
40528
41110
|
A workspace may carry a single \`agent.yaml\` at its root describing project-wide
|
|
@@ -40554,9 +41136,11 @@ Human-readable workspace name. Must be a non-empty string.
|
|
|
40554
41136
|
|
|
40555
41137
|
### \`version\` (optional)
|
|
40556
41138
|
|
|
40557
|
-
A display-only label
|
|
40558
|
-
|
|
40559
|
-
|
|
41139
|
+
A display-only label (e.g. \`v0.2.0\`). Never validated as semver, never made
|
|
41140
|
+
unique, it just renders on the install detail page. The version that actually
|
|
41141
|
+
identifies what is running is the image tag you register with
|
|
41142
|
+
\`stackbone link --tag\`, see
|
|
41143
|
+
[Commands → \`stackbone link\`](/docs/cli/commands#stackbone-link).
|
|
40560
41144
|
|
|
40561
41145
|
\`\`\`yaml
|
|
40562
41146
|
version: v0.2.0
|
|
@@ -40711,11 +41295,10 @@ on disk, plus every \`workflows/<name>.workflow.ts\` (or an explicit
|
|
|
40711
41295
|
\`stackbone.config.ts\` override for workflows). The optional root \`agent.yaml\`
|
|
40712
41296
|
supplies the project-wide database/runtime knobs when it is present.
|
|
40713
41297
|
|
|
40714
|
-
| Command
|
|
40715
|
-
|
|
|
40716
|
-
| \`stackbone dev\`
|
|
40717
|
-
| \`stackbone
|
|
40718
|
-
| \`stackbone db ...\` | The root \`agent.yaml\` \`database\` block, when present. |
|
|
41298
|
+
| Command | What it reads |
|
|
41299
|
+
| ------------------ | ------------------------------------------------------------- |
|
|
41300
|
+
| \`stackbone dev\` | The discovered workspace, boots every agent + every workflow. |
|
|
41301
|
+
| \`stackbone db ...\` | The root \`agent.yaml\` \`database\` block, when present. |
|
|
40719
41302
|
|
|
40720
41303
|
Every manifest is yours to edit and belongs under version control, the control
|
|
40721
41304
|
plane never writes to them.
|
|
@@ -40885,68 +41468,22 @@ schema\`) is exercised through \`stackbone dev\` today. Pointed at an installati
|
|
|
40885
41468
|
that does not serve the workflow routes, those commands come back empty rather
|
|
40886
41469
|
than erroring.
|
|
40887
41470
|
|
|
40888
|
-
##
|
|
40889
|
-
|
|
40890
|
-
\`stackbone publish\` compiles your whole workspace **once**, on your machine,
|
|
40891
|
-
into a single bundle. Every guardrail below aborts _before_ a bundle is
|
|
40892
|
-
produced — a failed publish ships **nothing**, which is the point: a publish
|
|
40893
|
-
error is always better than a crash-loop in production. Each abort prints the
|
|
40894
|
-
exact fix; here is what each one means.
|
|
40895
|
-
|
|
40896
|
-
### \`@stackbone/sdk\` inlined into an agent build
|
|
40897
|
-
|
|
40898
|
-
> \`stackbone publish failed: deep agent "<name>" inlined @stackbone/sdk into its build.\`
|
|
40899
|
-
|
|
40900
|
-
A workspace agent must keep \`@stackbone/sdk\` **external**, so it resolves to a
|
|
40901
|
-
single copy at runtime. A second copy gives the agent its own ambient context
|
|
40902
|
-
and per-run logs arrive empty. \`defineDeepAgent\` from \`@stackbone/sdk/deep\`
|
|
40903
|
-
keeps the SDK external for you automatically; this error only appears if
|
|
40904
|
-
something in your build pipeline forces it to inline anyway (for example a
|
|
40905
|
-
bundler config outside \`stackbone publish\` re-processing the output). Remove
|
|
40906
|
-
whatever is re-bundling \`@stackbone/sdk\` and re-publish.
|
|
40907
|
-
|
|
40908
|
-
### Native dependencies rejected
|
|
40909
|
-
|
|
40910
|
-
> \`stackbone publish failed: native dependencies detected in deep agent "<name>".\`
|
|
40911
|
-
|
|
40912
|
-
The workspace runtime runs pure-JS agents only. A dependency with native
|
|
40913
|
-
bindings (\`.node\` files) is rejected with the offending package name. Remove the
|
|
40914
|
-
native dependency from that agent.
|
|
40915
|
-
|
|
40916
|
-
### No agents declared
|
|
40917
|
-
|
|
40918
|
-
> \`This workspace declares no deep agents (none found under deep-agents/*/index.ts) — a workspace bundle needs at least one deep agent.\`
|
|
40919
|
-
|
|
40920
|
-
The workspace has no agents, there is no \`deep-agents/<name>/index.ts\` to
|
|
40921
|
-
discover. A workspace bundle needs at least one, so add one (for example
|
|
40922
|
-
\`stackbone add agent <name>\`, which scaffolds \`deep-agents/<name>/\`) and
|
|
40923
|
-
re-publish.
|
|
40924
|
-
|
|
40925
|
-
### An agent's build failed
|
|
40926
|
-
|
|
40927
|
-
> \`esbuild failed for deep agent "<name>": …\`
|
|
40928
|
-
|
|
40929
|
-
One agent's \`index.ts\` failed to bundle. The message carries the underlying
|
|
40930
|
-
esbuild error (a syntax error, a missing import); fix it and re-publish.
|
|
40931
|
-
|
|
40932
|
-
### Incompatible \`@stackbone/sdk\`
|
|
41471
|
+
## Incompatible \`@stackbone/sdk\`
|
|
40933
41472
|
|
|
40934
41473
|
> \`@stackbone/sdk@<version> is not compatible with this CLI, which supports >=0.1.0 <0.2.0 (built against 0.1.0).\`
|
|
40935
41474
|
|
|
40936
|
-
Each CLI build supports one minor line of \`@stackbone/sdk\`. \`stackbone
|
|
40937
|
-
|
|
40938
|
-
|
|
41475
|
+
Each CLI build supports one minor line of \`@stackbone/sdk\`. \`stackbone dev\`
|
|
41476
|
+
checks the version installed in your project on boot and warns when it falls
|
|
41477
|
+
outside that line, then keeps the local loop running. Pin the SDK to a
|
|
40939
41478
|
supported version and reinstall:
|
|
40940
41479
|
|
|
40941
41480
|
\`\`\`sh
|
|
40942
41481
|
pnpm add -E @stackbone/sdk@<version>
|
|
40943
41482
|
\`\`\`
|
|
40944
41483
|
|
|
40945
|
-
|
|
40946
|
-
|
|
40947
|
-
|
|
40948
|
-
exactly (no caret), so a stray \`pnpm update\` cannot silently move it off the
|
|
40949
|
-
supported line.
|
|
41484
|
+
If the SDK is not installed at all, the message tells you to run \`pnpm install\`
|
|
41485
|
+
first instead. A scaffolded project pins the SDK exactly (no caret), so a stray
|
|
41486
|
+
\`pnpm update\` cannot silently move it off the supported line.
|
|
40950
41487
|
|
|
40951
41488
|
## Checking a deployed workspace's health
|
|
40952
41489
|
|
|
@@ -41173,7 +41710,7 @@ engine behind [Vercel Workflows](https://vercel.com/docs/workflows)), so a
|
|
|
41173
41710
|
run survives crashes and redeploys and can pause for months.
|
|
41174
41711
|
|
|
41175
41712
|
You don't register workflows by hand. The workspace is **discovered by
|
|
41176
|
-
convention**: \`stackbone dev\` and
|
|
41713
|
+
convention**: \`stackbone dev\` and the deployed runtime scan your project and
|
|
41177
41714
|
pick up every agent folder under \`deep-agents/\` that has an \`index.ts\`, and
|
|
41178
41715
|
every workflow file at \`workflows/<name>.workflow.ts\`. A workflow's name is
|
|
41179
41716
|
the file basename without the \`.workflow.ts\` suffix, and its exported
|
|
@@ -41468,7 +42005,7 @@ suppressed.** Production agents should leave this flag unset.
|
|
|
41468
42005
|
- **[Logging & observability](/docs/sdk/observability)** — the run timeline is
|
|
41469
42006
|
captured for you.
|
|
41470
42007
|
- **[CLI reference](/docs/cli/commands)** — \`stackbone dev\`,
|
|
41471
|
-
\`stackbone
|
|
42008
|
+
\`stackbone link\`, \`stackbone workflows\`, \`stackbone runs\`,
|
|
41472
42009
|
\`stackbone hitl\`.
|
|
41473
42010
|
`,
|
|
41474
42011
|
"sdk/ai": `# stackbone.ai
|
|
@@ -42929,9 +43466,10 @@ subpath imports it directly.
|
|
|
42929
43466
|
- **Records the approval** so the run shows up in the inbox. The row is
|
|
42930
43467
|
tied to its run, so you can find it from the run in the dashboard or
|
|
42931
43468
|
with \`stackbone hitl list\`.
|
|
42932
|
-
- **Pauses the run** on a hook
|
|
42933
|
-
|
|
42934
|
-
|
|
43469
|
+
- **Pauses the run** on a hook with its own resume key. The SDK assigns
|
|
43470
|
+
that key per run, so you don't invent one. The pause is durable: the
|
|
43471
|
+
hook's state lives in Redis, so the run survives a crash, a cold
|
|
43472
|
+
start, or a redeploy and resumes deterministically.
|
|
42935
43473
|
- **Races the decision against a timeout.** If a human decides first,
|
|
42936
43474
|
the run resumes with that decision. If the \`timeout\` elapses first,
|
|
42937
43475
|
the \`fallback\` (\`'approve'\` or \`'reject'\`) is applied instead.
|
|
@@ -42955,7 +43493,6 @@ import { requestApproval } from '@stackbone/sdk/workflow';
|
|
|
42955
43493
|
export const inputSchema = z.object({
|
|
42956
43494
|
orderId: z.string(),
|
|
42957
43495
|
amount: z.number().positive(),
|
|
42958
|
-
approvalToken: z.string(),
|
|
42959
43496
|
});
|
|
42960
43497
|
|
|
42961
43498
|
export const outputSchema = z.object({
|
|
@@ -42968,7 +43505,7 @@ export async function refundWorkflow(input: z.infer<typeof inputSchema>) {
|
|
|
42968
43505
|
'use workflow';
|
|
42969
43506
|
|
|
42970
43507
|
const decision = await requestApproval({
|
|
42971
|
-
token
|
|
43508
|
+
// No \`token\`: the SDK assigns a unique resume key for this run.
|
|
42972
43509
|
topic: 'refund',
|
|
42973
43510
|
payload: { orderId: input.orderId, amount: input.amount },
|
|
42974
43511
|
title: 'Approve this refund?',
|
|
@@ -43002,12 +43539,12 @@ crash, a redeploy — is handled by the durable pause.
|
|
|
43002
43539
|
|
|
43003
43540
|
| Field | Type | Notes |
|
|
43004
43541
|
| ---------- | ----------------------- | ----------------------------------------------------------------------------------------- |
|
|
43005
|
-
| \`token\` | \`string\` | **Required.** The resume key — stable and unguessable, unique per approval within a run. |
|
|
43006
43542
|
| \`topic\` | \`string\` | **Required.** Approval category, shown in the inbox (e.g. \`'refund'\`, \`'deploy'\`). |
|
|
43007
43543
|
| \`payload\` | \`T\` | **Required.** What the reviewer looks at when deciding. |
|
|
43008
43544
|
| \`title\` | \`string\` | Optional. Human-readable title surfaced in the inbox and run view. |
|
|
43009
43545
|
| \`timeout\` | \`string \\| number\` | Optional. ISO 8601 duration (\`'24h'\`, \`'15m'\`) or milliseconds. Races the human decision. |
|
|
43010
43546
|
| \`fallback\` | \`'approve' \\| 'reject'\` | Optional. Applied when the timeout wins the race. Defaults to \`'reject'\`. |
|
|
43547
|
+
| \`token\` | \`string\` | Optional. Your own resume key. Omit it: the SDK assigns a unique one per run. |
|
|
43011
43548
|
|
|
43012
43549
|
It returns an \`ApprovalDecision\`:
|
|
43013
43550
|
|
|
@@ -43023,6 +43560,21 @@ Branch on \`status\`. Use \`timedOut\` if you want to treat a fallback
|
|
|
43023
43560
|
differently from a real human decision (for example, to alert that
|
|
43024
43561
|
nobody responded in time).
|
|
43025
43562
|
|
|
43563
|
+
### Should you pass your own \`token\`?
|
|
43564
|
+
|
|
43565
|
+
Almost never. Leave \`token\` out and the SDK generates a resume key that
|
|
43566
|
+
is unique to that run, so a retry, or a second run started while the
|
|
43567
|
+
first approval is still pending, can't collide with a key another live
|
|
43568
|
+
run already holds. Two gates in the same run each get their own key too.
|
|
43569
|
+
|
|
43570
|
+
Pass an explicit \`token\` only when something outside the run has to
|
|
43571
|
+
rebuild the same key to resume it, and then make sure it is unique per
|
|
43572
|
+
approval per run. Reusing a key another active run still holds is the
|
|
43573
|
+
one case that can't resume: the approval falls back instead of waiting
|
|
43574
|
+
for a person. If what you actually want is for an inbound reply to wake
|
|
43575
|
+
this exact run, use a raw hook rather than \`requestApproval()\`. See
|
|
43576
|
+
[Wait for a reply](#wait-for-a-reply).
|
|
43577
|
+
|
|
43026
43578
|
## Deciding
|
|
43027
43579
|
|
|
43028
43580
|
When the workflow pauses, the approval lands in the inbox. A reviewer
|
|
@@ -43063,10 +43615,10 @@ Common patterns:
|
|
|
43063
43615
|
|
|
43064
43616
|
\`\`\`ts
|
|
43065
43617
|
// High-risk: auto-reject if no one looks within an hour.
|
|
43066
|
-
await requestApproval({
|
|
43618
|
+
await requestApproval({ topic, payload, timeout: '1h', fallback: 'reject' });
|
|
43067
43619
|
|
|
43068
43620
|
// Low-risk: auto-approve after a grace period if no one objects.
|
|
43069
|
-
await requestApproval({
|
|
43621
|
+
await requestApproval({ topic, payload, timeout: '15m', fallback: 'approve' });
|
|
43070
43622
|
\`\`\`
|
|
43071
43623
|
|
|
43072
43624
|
## Escape hatch: custom hooks
|
|
@@ -43092,10 +43644,11 @@ docs for hook and replay semantics.
|
|
|
43092
43644
|
|
|
43093
43645
|
A common shape is not approve/reject at all. You send someone a message,
|
|
43094
43646
|
and you want the **same run** to wake up when they reply. \`requestApproval()\`
|
|
43095
|
-
does not fit here,
|
|
43096
|
-
\`{ status, payload, timedOut }
|
|
43097
|
-
|
|
43098
|
-
|
|
43647
|
+
does not fit here, for two reasons: it models a decision and returns
|
|
43648
|
+
\`{ status, payload, timedOut }\`, and its resume key is auto-generated, so no
|
|
43649
|
+
incoming event can be matched against it. A reply is free-form content and
|
|
43650
|
+
needs a key you choose, so drop down to a raw hook. A resumed hook returns
|
|
43651
|
+
whatever payload woke it, which is the reply itself.
|
|
43099
43652
|
|
|
43100
43653
|
Park the hook on a \`token\` you control, and make that token the
|
|
43101
43654
|
conversation's own id: the email thread id the provider hands back when you
|
|
@@ -43731,8 +44284,8 @@ the resume path.
|
|
|
43731
44284
|
## The workspace registry
|
|
43732
44285
|
|
|
43733
44286
|
The CLI and runtime discover your workspace **by convention**, from the
|
|
43734
|
-
files on disk, with no registry to hand-maintain. \`stackbone dev\` and
|
|
43735
|
-
|
|
44287
|
+
files on disk, with no registry to hand-maintain. \`stackbone dev\` and the
|
|
44288
|
+
deployed runtime both read the same convention:
|
|
43736
44289
|
|
|
43737
44290
|
- **Agents**: every folder under \`deep-agents/\` with an \`index.ts\` entry
|
|
43738
44291
|
file. The folder name is both the agent's identity and the \`model\` a
|
|
@@ -43768,9 +44321,9 @@ export default defineWorkspace({
|
|
|
43768
44321
|
});
|
|
43769
44322
|
\`\`\`
|
|
43770
44323
|
|
|
43771
|
-
When present, this file is what \`stackbone dev\`, \`stackbone
|
|
43772
|
-
|
|
43773
|
-
|
|
44324
|
+
When present, this file is what \`stackbone dev\`, \`stackbone workflows list\`,
|
|
44325
|
+
and the typed autocompletion below read instead of scanning the filesystem for
|
|
44326
|
+
workflows.
|
|
43774
44327
|
|
|
43775
44328
|
## How the CLI connects to the SDK
|
|
43776
44329
|
|
|
@@ -44351,8 +44904,8 @@ Like \`stackbone.secrets\` and \`stackbone.config\`, this surface is
|
|
|
44351
44904
|
directly, so it is not gated on the contract handshake. The only
|
|
44352
44905
|
prerequisite is that the prompts schema has been applied. Those tables
|
|
44353
44906
|
are created when your project's migrations run — which happens
|
|
44354
|
-
automatically when you boot your project with \`stackbone dev
|
|
44355
|
-
when
|
|
44907
|
+
automatically when you boot your project with \`stackbone dev\`, and again
|
|
44908
|
+
when the deployed runtime starts. A not-yet-migrated agent surfaces
|
|
44356
44909
|
\`prompts_not_configured\` until then.
|
|
44357
44910
|
|
|
44358
44911
|
## API
|
|
@@ -46098,10 +46651,51 @@ var createInitCommand = (ctx) => defineCommand({
|
|
|
46098
46651
|
});
|
|
46099
46652
|
//#endregion
|
|
46100
46653
|
//#region src/services/deployments.service.ts
|
|
46654
|
+
init_src$11();
|
|
46101
46655
|
init_src$13();
|
|
46102
46656
|
var registerDeployment = async (ctx, body) => {
|
|
46103
46657
|
return parseResponse$2(deploymentResponseSchema, await ctx.http.post("/api/v1/deployments", body), "POST /api/v1/deployments");
|
|
46104
46658
|
};
|
|
46659
|
+
var verifyDeploymentSecret = async (args) => {
|
|
46660
|
+
const fetchImpl = args.fetchImpl ?? fetch;
|
|
46661
|
+
let challengeUrl;
|
|
46662
|
+
try {
|
|
46663
|
+
challengeUrl = new URL(PROXY_CHALLENGE_PATH, args.url).toString();
|
|
46664
|
+
} catch {
|
|
46665
|
+
throw new StackboneCliError({
|
|
46666
|
+
code: "generic",
|
|
46667
|
+
message: `--url is not a valid URL: ${args.url}`,
|
|
46668
|
+
suggestion: "Pass the full public URL of the deployed agent, e.g. https://agent.example.com"
|
|
46669
|
+
});
|
|
46670
|
+
}
|
|
46671
|
+
const header = buildStackboneSignatureHeader({
|
|
46672
|
+
secrets: [args.secret],
|
|
46673
|
+
payload: proxyRequestPayload({
|
|
46674
|
+
method: "GET",
|
|
46675
|
+
path: PROXY_CHALLENGE_PATH,
|
|
46676
|
+
body: ""
|
|
46677
|
+
})
|
|
46678
|
+
});
|
|
46679
|
+
let response;
|
|
46680
|
+
try {
|
|
46681
|
+
response = await fetchImpl(challengeUrl, {
|
|
46682
|
+
method: "GET",
|
|
46683
|
+
headers: { "stackbone-signature": header }
|
|
46684
|
+
});
|
|
46685
|
+
} catch (cause) {
|
|
46686
|
+
throw new StackboneCliError({
|
|
46687
|
+
code: "generic",
|
|
46688
|
+
message: `Could not reach the deployed container at ${args.url} to verify --secret`,
|
|
46689
|
+
suggestion: "Check the --url is the public address of the running container and that it is reachable.",
|
|
46690
|
+
cause
|
|
46691
|
+
});
|
|
46692
|
+
}
|
|
46693
|
+
if (!response.ok) throw new StackboneCliError({
|
|
46694
|
+
code: "generic",
|
|
46695
|
+
message: `The --secret does not match the HMAC_SECRET configured on the deployed container at ${args.url}`,
|
|
46696
|
+
suggestion: "Set the same value the container was started with (its HMAC_SECRET env), then re-run `stackbone link`."
|
|
46697
|
+
});
|
|
46698
|
+
};
|
|
46105
46699
|
//#endregion
|
|
46106
46700
|
//#region src/commands/link/link.command.ts
|
|
46107
46701
|
var requireLinkField = (value, flag, description) => {
|
|
@@ -46168,6 +46762,11 @@ var runLink = async (ctx, args) => {
|
|
|
46168
46762
|
const activeOrg = await getActiveOrganization(ctx);
|
|
46169
46763
|
const agent = await getAgentByOwnerAndSlug(ctx, activeOrg.slug, chosenSlug);
|
|
46170
46764
|
if (!ctx.flags.json) log.step(`Linking ${targetDir} to ${agent.slug}`);
|
|
46765
|
+
if (!ctx.flags.json) log.step("Verifying the --secret against the deployed container");
|
|
46766
|
+
await verifyDeploymentSecret({
|
|
46767
|
+
url,
|
|
46768
|
+
secret
|
|
46769
|
+
});
|
|
46171
46770
|
const [localDev, deployment] = await Promise.all([upsertLocalDevInstallation(ctx, {
|
|
46172
46771
|
creatorOrganizationSlug: activeOrg.slug,
|
|
46173
46772
|
agentTemplateSlug: agent.slug
|