@stackbone/cli 0.4.1 → 0.5.1
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 +62 -0
- package/main.js +1706 -133
- package/package.json +1 -1
- package/stackbone-cli-0.5.1.tgz +0 -0
- package/stackbone-cli-0.4.1.tgz +0 -0
package/main.js
CHANGED
|
@@ -695,7 +695,17 @@ var init_contract$1 = __esmMin((() => {
|
|
|
695
695
|
}),
|
|
696
696
|
runtimeUrl: z.url().nullable().optional()
|
|
697
697
|
});
|
|
698
|
-
}))
|
|
698
|
+
}));
|
|
699
|
+
//#endregion
|
|
700
|
+
//#region ../../libs/shared/validators/src/lib/agents/deployments.ts
|
|
701
|
+
function protocolOf(raw) {
|
|
702
|
+
try {
|
|
703
|
+
return new URL(raw).protocol;
|
|
704
|
+
} catch {
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
var deploymentStatusSchema, deploymentUrlSchema, deploymentResponseSchema;
|
|
699
709
|
var init_deployments = __esmMin((() => {
|
|
700
710
|
init_organizations();
|
|
701
711
|
deploymentStatusSchema = z.enum([
|
|
@@ -704,11 +714,15 @@ var init_deployments = __esmMin((() => {
|
|
|
704
714
|
"ready",
|
|
705
715
|
"failed"
|
|
706
716
|
]);
|
|
717
|
+
deploymentUrlSchema = z.url().refine((raw) => {
|
|
718
|
+
const protocol = protocolOf(raw);
|
|
719
|
+
return protocol === null || protocol === "https:";
|
|
720
|
+
}, { error: (issue) => `scheme "${protocolOf(String(issue.input))}" is not supported — register the box by its https:// address` });
|
|
707
721
|
z.object({
|
|
708
722
|
organizationSlug: organizationSlugSchema,
|
|
709
723
|
agentSlug: agentSlugSchema,
|
|
710
724
|
installationId: z.uuid(),
|
|
711
|
-
url:
|
|
725
|
+
url: deploymentUrlSchema,
|
|
712
726
|
hmacSecret: z.string().min(1),
|
|
713
727
|
imageTag: z.string().min(1).max(200).optional()
|
|
714
728
|
});
|
|
@@ -9720,9 +9734,11 @@ var init_connector_invoker = __esmMin((() => {
|
|
|
9720
9734
|
* @param connector The connect connector id registered in Studio.
|
|
9721
9735
|
* @param operation The operation / tool id to run on that connector.
|
|
9722
9736
|
* @param args The operation arguments (path/query/body params). Defaults `{}`.
|
|
9723
|
-
* @param opts Optional `{ principal, idempotent }`. `principal`
|
|
9724
|
-
* `{ type: 'app' }`; `idempotent` is UNSET by
|
|
9725
|
-
* an explicit `false` changes how a failure is
|
|
9737
|
+
* @param opts Optional `{ principal, idempotent, echoPath }`. `principal`
|
|
9738
|
+
* defaults to `{ type: 'app' }`; `idempotent` is UNSET by
|
|
9739
|
+
* default and only an explicit `false` changes how a failure is
|
|
9740
|
+
* retried; `echoPath` records what this write returned so a
|
|
9741
|
+
* later delivery of it can be recognised as our own.
|
|
9726
9742
|
*/
|
|
9727
9743
|
async function callConnector(connector, operation, args, opts) {
|
|
9728
9744
|
return (await getConnectorInvoker().invoke({
|
|
@@ -9730,7 +9746,8 @@ async function callConnector(connector, operation, args, opts) {
|
|
|
9730
9746
|
operation,
|
|
9731
9747
|
...args !== void 0 ? { args } : {},
|
|
9732
9748
|
...opts?.principal !== void 0 ? { principal: opts.principal } : {},
|
|
9733
|
-
...opts?.idempotent !== void 0 ? { idempotent: opts.idempotent } : {}
|
|
9749
|
+
...opts?.idempotent !== void 0 ? { idempotent: opts.idempotent } : {},
|
|
9750
|
+
...opts?.echoPath !== void 0 ? { echoPath: opts.echoPath } : {}
|
|
9734
9751
|
})).output;
|
|
9735
9752
|
}
|
|
9736
9753
|
var connectorHandle, connection;
|
|
@@ -14368,6 +14385,29 @@ var createInvocationsStore = (opts) => {
|
|
|
14368
14385
|
]);
|
|
14369
14386
|
return rows[0]?.id ?? null;
|
|
14370
14387
|
},
|
|
14388
|
+
async findEchoRecord(input) {
|
|
14389
|
+
assertOpen();
|
|
14390
|
+
const args = [
|
|
14391
|
+
input.connector,
|
|
14392
|
+
input.value,
|
|
14393
|
+
input.windowMs
|
|
14394
|
+
];
|
|
14395
|
+
let markerScope = "";
|
|
14396
|
+
if (input.workflowName !== null) {
|
|
14397
|
+
args.push(input.workflowName);
|
|
14398
|
+
markerScope = ` AND (input->>'workflowName' = $${args.length} OR input->>'workflowName' IS NULL)`;
|
|
14399
|
+
}
|
|
14400
|
+
const { rows } = await readPool.query(`SELECT bool_or(input->>'echo' = $2) AS matched,
|
|
14401
|
+
bool_or(input->>'echoUnresolved' = 'true'${markerScope}) AS unresolved
|
|
14402
|
+
FROM stackbone_platform.run_steps
|
|
14403
|
+
WHERE type = 'connection'
|
|
14404
|
+
AND input->>'connector' = $1
|
|
14405
|
+
AND created_at > now() - ($3::bigint * interval '1 millisecond')`, args);
|
|
14406
|
+
return {
|
|
14407
|
+
matched: rows[0]?.matched === true,
|
|
14408
|
+
unresolved: rows[0]?.unresolved === true
|
|
14409
|
+
};
|
|
14410
|
+
},
|
|
14371
14411
|
async annotatePromptUse(input) {
|
|
14372
14412
|
if (!isUuid(input.runId)) return;
|
|
14373
14413
|
assertOpen();
|
|
@@ -15699,7 +15739,7 @@ var emulatorCors = (allowlist) => cors({
|
|
|
15699
15739
|
var STACKBONE_CLI_BUILD_VERSION, EMULATOR_CAPABILITY_ROUTES, EMULATOR_CAPABILITIES, buildContractResponse;
|
|
15700
15740
|
var init_contract = __esmMin((() => {
|
|
15701
15741
|
init_src$19();
|
|
15702
|
-
STACKBONE_CLI_BUILD_VERSION = "0.
|
|
15742
|
+
STACKBONE_CLI_BUILD_VERSION = "0.5.1";
|
|
15703
15743
|
EMULATOR_CAPABILITY_ROUTES = {
|
|
15704
15744
|
"database.postgres_direct": "/api/db",
|
|
15705
15745
|
"rag.basic": "/api/rag",
|
|
@@ -30724,6 +30764,37 @@ var init_types$1 = __esmMin((() => {
|
|
|
30724
30764
|
requestBodySchema: z.unknown().optional(),
|
|
30725
30765
|
/** Original Swagger body parameter name; allows body values alongside query filters. */
|
|
30726
30766
|
bodyArgName: z.string().optional(),
|
|
30767
|
+
/**
|
|
30768
|
+
* SEND THIS OPERATION'S BODY AS A FORM, not as JSON. The executor then encodes
|
|
30769
|
+
* the same payload as `application/x-www-form-urlencoded` in the bracket
|
|
30770
|
+
* dialect (`metadata[plan]=pro`, `items[0][price]=p_1`). Absent — the case for
|
|
30771
|
+
* every operation of every other connector — the body is JSON, byte for byte
|
|
30772
|
+
* as before.
|
|
30773
|
+
*
|
|
30774
|
+
* Declared on a document as `x-stackbone-body-encoding: form`, the fifth of the
|
|
30775
|
+
* `x-stackbone-*` extensions and read by the same rules: a per-operation
|
|
30776
|
+
* literal, ignored rather than coerced when it is anything else.
|
|
30777
|
+
*
|
|
30778
|
+
* EXPLICIT, NEVER INFERRED FROM THE SPEC'S CONTENT TYPE. A document that
|
|
30779
|
+
* declares `application/x-www-form-urlencoded` under `requestBody.content` is
|
|
30780
|
+
* saying what the provider ACCEPTS, and plenty of specs list several media
|
|
30781
|
+
* types or the wrong one. Switching the serialisation on that reading would
|
|
30782
|
+
* make a spec edit silently change what goes on the wire, which is the
|
|
30783
|
+
* data-corruption class `openapi-introspector.ts` refuses everywhere else.
|
|
30784
|
+
*
|
|
30785
|
+
* ONE declared value rather than a `json` / `form` pair, because a second
|
|
30786
|
+
* spelling of today's default is a second thing that can be got wrong for no
|
|
30787
|
+
* gain: absent already means JSON and always has.
|
|
30788
|
+
*
|
|
30789
|
+
* Per-OPERATION, like the flags above. Stripe is the first provider we want it
|
|
30790
|
+
* for and its API is form-encoded end to end, but a provider that takes a form
|
|
30791
|
+
* on one endpoint and JSON on the next must not have the whole connector
|
|
30792
|
+
* switched over.
|
|
30793
|
+
*
|
|
30794
|
+
* MULTIPART IS NOT HERE. A body of arbitrary bytes with a caller-chosen
|
|
30795
|
+
* content type is what `rawBody` already does.
|
|
30796
|
+
*/
|
|
30797
|
+
bodyEncoding: z.literal("form").optional(),
|
|
30727
30798
|
responseSchema: z.unknown().optional(),
|
|
30728
30799
|
/**
|
|
30729
30800
|
* This operation's body is BYTES, not JSON. The executor then resolves the
|
|
@@ -30980,7 +31051,7 @@ function hasTopLevelAlternation(pattern) {
|
|
|
30980
31051
|
}
|
|
30981
31052
|
return false;
|
|
30982
31053
|
}
|
|
30983
|
-
var argTargetSchema, triggerCursorSchema, triggerScopeParamSchema, unanchoredPatternReason, triggerListStepSchema, triggerItemStepSchema, declarativePollSchema, strategyPollSchema, pollSpecSchema, triggerDescriptorSchema;
|
|
31054
|
+
var argTargetSchema, triggerCursorSchema, compileAnchored, triggerScopeParamSchema, unanchoredPatternReason, triggerListStepSchema, triggerItemStepSchema, declarativePollSchema, strategyPollSchema, pollSpecSchema, triggerDescriptorSchema;
|
|
30984
31055
|
var init_trigger_descriptor = __esmMin((() => {
|
|
30985
31056
|
init_paths();
|
|
30986
31057
|
argTargetSchema = z.union([z.string(), z.array(z.string()).min(1)], { error: "must be a parameter name (one literal key, dots included) or a non-empty array of path segments to nest it under" });
|
|
@@ -31054,7 +31125,25 @@ var init_trigger_descriptor = __esmMin((() => {
|
|
|
31054
31125
|
* that is a definition promising a margin the data cannot honour, and a
|
|
31055
31126
|
* failed tick on the row beats a protection that silently is not there.
|
|
31056
31127
|
*/
|
|
31057
|
-
lookbackMs: z.number().int().positive().optional()
|
|
31128
|
+
lookbackMs: z.number().int().positive().optional(),
|
|
31129
|
+
/**
|
|
31130
|
+
* The unit this cursor's stamps are counted in, when they are not ISO date
|
|
31131
|
+
* strings. Stripe is the case: an event's `created` is seconds from the
|
|
31132
|
+
* epoch and the list filter takes the same integer.
|
|
31133
|
+
*
|
|
31134
|
+
* DECLARED, never inferred from the value, and that is the whole point of
|
|
31135
|
+
* the field. `timestamp` already admits numbers — a Supabase
|
|
31136
|
+
* `{cursorColumn}` may resolve to an integer `id` — so reading a 10-digit
|
|
31137
|
+
* number as seconds because it looks like seconds would turn those ids into
|
|
31138
|
+
* instants: the future check would start barring them and the lookback
|
|
31139
|
+
* would start subtracting from them, both silently.
|
|
31140
|
+
*
|
|
31141
|
+
* Absent is every trigger written before this, unchanged: the stamp reader
|
|
31142
|
+
* takes the two ISO shapes and nothing else. Its arithmetic lives in
|
|
31143
|
+
* `parseStamp` / `formatStamp` (`stamps.ts`) and nowhere else — the refine
|
|
31144
|
+
* below names the value only to refuse a margin finer than one tick of it.
|
|
31145
|
+
*/
|
|
31146
|
+
unit: z.enum(["unix-seconds"]).optional()
|
|
31058
31147
|
}), z.object({
|
|
31059
31148
|
kind: z.literal("opaque"),
|
|
31060
31149
|
/** Dot-path in the list response to the next opaque token. */
|
|
@@ -31071,6 +31160,13 @@ var init_trigger_descriptor = __esmMin((() => {
|
|
|
31071
31160
|
*/
|
|
31072
31161
|
lookbackMs: z.never({ error: "lookbackMs is only meaningful on a timestamp cursor: an opaque cursor is a token the provider minted, and there is no instant to move back from" }).optional()
|
|
31073
31162
|
})]);
|
|
31163
|
+
compileAnchored = (pattern) => {
|
|
31164
|
+
try {
|
|
31165
|
+
return new RegExp(`^(?:${pattern})$`);
|
|
31166
|
+
} catch {
|
|
31167
|
+
return;
|
|
31168
|
+
}
|
|
31169
|
+
};
|
|
31074
31170
|
triggerScopeParamSchema = z.object({
|
|
31075
31171
|
/**
|
|
31076
31172
|
* The list arg the value is written into, and the key the subscription stores
|
|
@@ -31108,6 +31204,23 @@ var init_trigger_descriptor = __esmMin((() => {
|
|
|
31108
31204
|
/** Placeholder for the field — a real example, not a shape. */
|
|
31109
31205
|
example: z.string().optional(),
|
|
31110
31206
|
/**
|
|
31207
|
+
* Values Studio offers while the operator types. SUGGESTIONS, not options:
|
|
31208
|
+
* the field stays free text and anything `pattern` accepts is still valid.
|
|
31209
|
+
*
|
|
31210
|
+
* The distinction is the whole point. Stripe's `type` takes any of the many
|
|
31211
|
+
* event names its API emits, plus a wildcard family — a closed dropdown could
|
|
31212
|
+
* not express `charge.*` and would be a list too long to read either way.
|
|
31213
|
+
* What an operator actually needs is to be shown that `charge.succeeded`
|
|
31214
|
+
* exists before typing `charge.suceeded`, which passes every pattern anyone
|
|
31215
|
+
* would write and then polls green forever, delivering nothing.
|
|
31216
|
+
*
|
|
31217
|
+
* So this is a spelling aid for a value nothing else can check. A connector
|
|
31218
|
+
* whose scope really IS a closed set should say so with a `pattern` that
|
|
31219
|
+
* alternates over it; that refusal happens at register time, where a refusal
|
|
31220
|
+
* belongs, and this field is not a substitute for it.
|
|
31221
|
+
*/
|
|
31222
|
+
suggestions: z.array(z.string()).optional(),
|
|
31223
|
+
/**
|
|
31111
31224
|
* A poll cannot run without a required value, so the register endpoint refuses
|
|
31112
31225
|
* the link rather than arming one that would fail every tick. An OPTIONAL one
|
|
31113
31226
|
* left blank is simply not sent, which is how a caller asks for the provider's
|
|
@@ -31142,6 +31255,15 @@ var init_trigger_descriptor = __esmMin((() => {
|
|
|
31142
31255
|
}
|
|
31143
31256
|
}, { message: "pattern must be a compilable regular expression" }).optional()
|
|
31144
31257
|
}).superRefine((param, ctx) => {
|
|
31258
|
+
if (param.pattern !== void 0 && param.suggestions !== void 0) {
|
|
31259
|
+
const anchored = compileAnchored(param.pattern);
|
|
31260
|
+
const refused = anchored === void 0 ? [] : param.suggestions.filter((value) => !anchored.test(value));
|
|
31261
|
+
if (refused.length > 0) ctx.addIssue({
|
|
31262
|
+
code: "custom",
|
|
31263
|
+
path: ["suggestions"],
|
|
31264
|
+
message: `every suggestion must satisfy this param's own \`pattern\` (${param.pattern}), or the field offers a value its register endpoint refuses: ${refused.join(", ")}`
|
|
31265
|
+
});
|
|
31266
|
+
}
|
|
31145
31267
|
if (param.template === void 0) return;
|
|
31146
31268
|
if (param.pattern === void 0) {
|
|
31147
31269
|
ctx.addIssue({
|
|
@@ -31191,6 +31313,27 @@ var init_trigger_descriptor = __esmMin((() => {
|
|
|
31191
31313
|
* because this is how EVERY GraphQL provider answers, not a Linear quirk.
|
|
31192
31314
|
*/
|
|
31193
31315
|
errorsPath: z.string().optional(),
|
|
31316
|
+
/**
|
|
31317
|
+
* WHICH END OF THE FEED THIS OPERATION RETURNS FIRST (PLA-1510).
|
|
31318
|
+
*
|
|
31319
|
+
* Documentation only: the engine never sorts, and it delivers `items` in
|
|
31320
|
+
* whatever order the provider gave them. What this changes is the sentence
|
|
31321
|
+
* `scripts/generate-trigger-reference.ts` prints on a trigger's public page,
|
|
31322
|
+
* and through it what an operator planning a backlog drain expects.
|
|
31323
|
+
*
|
|
31324
|
+
* DECLARED, NEVER INFERRED, and never defaulted. The generator used to print
|
|
31325
|
+
* `oldest first` as a literal for every timestamp cursor, which was false for
|
|
31326
|
+
* five of the nine declarative triggers in this catalog — Stripe's event
|
|
31327
|
+
* feed, GitHub's commits and all three Linear triggers come back newest
|
|
31328
|
+
* first. A default would put that same false sentence on any trigger whose
|
|
31329
|
+
* author did not think about it, which is the failure this field exists to
|
|
31330
|
+
* end. Absent, the page says nothing about order, which is the only honest
|
|
31331
|
+
* thing to say about an order nobody established.
|
|
31332
|
+
*
|
|
31333
|
+
* The order matters because the per-tick ceiling is real: a walk that gets
|
|
31334
|
+
* cut off keeps the end it read and passes over the other one.
|
|
31335
|
+
*/
|
|
31336
|
+
order: z.enum(["oldest-first", "newest-first"]).optional(),
|
|
31194
31337
|
/** Page-walk wiring: the request arg that carries the page token + where the
|
|
31195
31338
|
* next token is read from. Absent for non-paged list operations. */
|
|
31196
31339
|
pageToken: z.object({
|
|
@@ -31260,7 +31403,73 @@ var init_trigger_descriptor = __esmMin((() => {
|
|
|
31260
31403
|
* stops on `items.length < stride`, and it also ADVANCES by this, so `0`
|
|
31261
31404
|
* would re-request page one until the bound (PLA-1142). */
|
|
31262
31405
|
stride: z.number().int().positive()
|
|
31263
|
-
}).optional()
|
|
31406
|
+
}).optional(),
|
|
31407
|
+
/**
|
|
31408
|
+
* Paging by the LAST ITEM THE CALLER ALREADY HOLDS: the next page is
|
|
31409
|
+
* addressed by the data, not by a token the provider hands back. Stripe is
|
|
31410
|
+
* the case — `starting_after` takes the id of the last item on the page just
|
|
31411
|
+
* read — and it is the common shape for anything that paginates a
|
|
31412
|
+
* time-ordered feed.
|
|
31413
|
+
*
|
|
31414
|
+
* The engine reads `itemPath` off the LAST element of the page and sends it
|
|
31415
|
+
* as `param` on the next request. Page 0 carries neither, exactly as
|
|
31416
|
+
* `pageToken` sends no token on its first call.
|
|
31417
|
+
*
|
|
31418
|
+
* `hasMorePath` IS REQUIRED HERE, unlike inside `pageToken`, and the
|
|
31419
|
+
* difference is not an oversight. A token walk has the token itself as a
|
|
31420
|
+
* second end signal: a provider that nulls it on the last page ends the walk
|
|
31421
|
+
* with no flag at all. This shape has no such signal — the last item's id is
|
|
31422
|
+
* always there, on the final page as much as on the first — so without a
|
|
31423
|
+
* flag the walk would run to `MAX_PAGES` against the provider's rate limit on
|
|
31424
|
+
* every tick, or stop on an empty page one call too late. A required flag is
|
|
31425
|
+
* also reversible into an optional one; the reverse is not.
|
|
31426
|
+
*
|
|
31427
|
+
* A declared flag that comes back neither `true` nor `false` breaks the walk
|
|
31428
|
+
* WITHOUT draining it, the same rule the token walk follows: the provider
|
|
31429
|
+
* failed to answer, and reading that as "no more" advances the cursor past
|
|
31430
|
+
* pages nobody read.
|
|
31431
|
+
*
|
|
31432
|
+
* Mutually exclusive with the other three shapes.
|
|
31433
|
+
*/
|
|
31434
|
+
pageLastId: z.object({
|
|
31435
|
+
/** Request arg carrying the last item's id, e.g. `starting_after`. */
|
|
31436
|
+
param: argTargetSchema,
|
|
31437
|
+
/**
|
|
31438
|
+
* Dot-path, ON AN ITEM, to the value that fills `param` — `id` for
|
|
31439
|
+
* Stripe. Non-empty, because `getPath(item, '')` is the whole item, which
|
|
31440
|
+
* is never a page address.
|
|
31441
|
+
*/
|
|
31442
|
+
itemPath: z.string().min(1),
|
|
31443
|
+
/**
|
|
31444
|
+
* Dot-path to the boolean saying another page exists — Stripe's
|
|
31445
|
+
* `has_more`. Read BEFORE asking for the next page, and only `true`
|
|
31446
|
+
* continues the walk.
|
|
31447
|
+
*/
|
|
31448
|
+
hasMorePath: z.string().min(1)
|
|
31449
|
+
}).optional(),
|
|
31450
|
+
/**
|
|
31451
|
+
* Where the provider says another page exists, for a list step the engine
|
|
31452
|
+
* CANNOT page — no token in the response, no offset in the request, and no
|
|
31453
|
+
* addressable last item. Stripe USED to be the case here, before `pageLastId`
|
|
31454
|
+
* above existed; it is kept because a provider that reports `has_more` and
|
|
31455
|
+
* exposes no way at all to ask for the next page is still expressible.
|
|
31456
|
+
*
|
|
31457
|
+
* Without this the walk reads one page and reports itself DRAINED, because
|
|
31458
|
+
* the null-token rule is what ends an unpaged walk. The items below that
|
|
31459
|
+
* page are then under the marker the cursor just advanced to, never listed
|
|
31460
|
+
* again, with no warning and a green row — the failure `pageToken`'s own
|
|
31461
|
+
* `hasMorePath` doc calls the worst the intake can have. Declared here, a
|
|
31462
|
+
* page that answers `true` breaks the walk WITHOUT draining it, so the tick
|
|
31463
|
+
* reports it cut off and the loss is at least visible.
|
|
31464
|
+
*
|
|
31465
|
+
* Only for a list step that declares no paging shape: `pageToken` and
|
|
31466
|
+
* `pageLastId` carry their own `hasMorePath`, and `pageNumber` / `pageSkip`
|
|
31467
|
+
* end their walk on a short page and never read this. All are refused below.
|
|
31468
|
+
*
|
|
31469
|
+
* A non-empty path, because `getPath(value, '')` is the whole response —
|
|
31470
|
+
* never a boolean, so an empty path would cut off every tick forever.
|
|
31471
|
+
*/
|
|
31472
|
+
hasMorePath: z.string().min(1).optional()
|
|
31264
31473
|
}).superRefine((list, ctx) => {
|
|
31265
31474
|
const params = (list.scopeParams ?? []).map((param) => param.param);
|
|
31266
31475
|
const duplicated = [...new Set(params.filter((name, i) => params.indexOf(name) !== i))];
|
|
@@ -31272,13 +31481,23 @@ var init_trigger_descriptor = __esmMin((() => {
|
|
|
31272
31481
|
const declared = [
|
|
31273
31482
|
"pageToken",
|
|
31274
31483
|
"pageNumber",
|
|
31275
|
-
"pageSkip"
|
|
31484
|
+
"pageSkip",
|
|
31485
|
+
"pageLastId"
|
|
31276
31486
|
].filter((shape) => list[shape] !== void 0);
|
|
31277
31487
|
if (declared.length > 1) ctx.addIssue({
|
|
31278
31488
|
code: "custom",
|
|
31279
31489
|
path: [declared[1] ?? "pageNumber"],
|
|
31280
31490
|
message: `a list step declares ONE paging shape, and this one declares ${declared.length} (${declared.join(", ")}): the engine would silently use ${declared[0]} and ignore the rest`
|
|
31281
31491
|
});
|
|
31492
|
+
if (list.hasMorePath !== void 0 && declared.length > 0) {
|
|
31493
|
+
const shape = declared[0];
|
|
31494
|
+
const ownsAFlag = shape === "pageToken" || shape === "pageLastId";
|
|
31495
|
+
ctx.addIssue({
|
|
31496
|
+
code: "custom",
|
|
31497
|
+
path: ["hasMorePath"],
|
|
31498
|
+
message: ownsAFlag ? `this list step declares ${shape}, which reads its own hasMorePath and never this one — move the flag inside ${shape}, or drop the paging shape` : `this list step declares ${shape}, which ends its walk on a short page and never reads hasMorePath — drop one of the two`
|
|
31499
|
+
});
|
|
31500
|
+
}
|
|
31282
31501
|
});
|
|
31283
31502
|
triggerItemStepSchema = z.object({
|
|
31284
31503
|
/** The OpenAPI `operationId` that fetches one item's full detail. */
|
|
@@ -31312,6 +31531,11 @@ var init_trigger_descriptor = __esmMin((() => {
|
|
|
31312
31531
|
* absent, the event payload is the raw item verbatim. */
|
|
31313
31532
|
map: z.record(z.string(), z.string()).optional()
|
|
31314
31533
|
}).superRefine((spec, ctx) => {
|
|
31534
|
+
if (spec.cursor.kind === "timestamp" && spec.cursor.unit === "unix-seconds" && spec.cursor.lookbackMs !== void 0 && spec.cursor.lookbackMs % 1e3 !== 0) ctx.addIssue({
|
|
31535
|
+
code: "custom",
|
|
31536
|
+
path: ["cursor", "lookbackMs"],
|
|
31537
|
+
message: `a unix-seconds cursor carries whole seconds, so a lookback of ${spec.cursor.lookbackMs}ms floors to ${Math.floor(spec.cursor.lookbackMs / 1e3)}s on the wire — declare a multiple of 1000`
|
|
31538
|
+
});
|
|
31315
31539
|
const cursor = spec.cursor;
|
|
31316
31540
|
const cursorParam = cursor.kind === "timestamp" ? cursor.param : cursor.requestParam;
|
|
31317
31541
|
const clash = spec.list.scopeParams?.find((param) => sameArgTarget([param.param], cursorParam));
|
|
@@ -32303,8 +32527,8 @@ var init_github_connector = __esmMin((() => {
|
|
|
32303
32527
|
triggers: [{
|
|
32304
32528
|
id: "commit-pushed",
|
|
32305
32529
|
displayName: "New commit by date",
|
|
32306
|
-
description: "Starts one run per commit whose committer date is newer than this link’s cursor. This follows commit dates, not push times: pushing an older local commit or merging existing commits without changing their committer dates can leave them undetected. A poll reads up to 10,000 commits; a larger window can skip its oldest commits. CAUTION: a workflow that writes a file to the watched branch can create a commit that re-triggers itself. Each new commit has a new SHA, so deduplication cannot stop the loop.
|
|
32307
|
-
docs: "Watches commits on the selected branch, or the repository’s default branch when Branch is blank. Starts one run per commit whose committer date is newer than the saved cursor.\n\nThe cursor starts at the time you turn the link on. Push time is not recorded: commits created earlier and pushed later, including existing commits added by a fast-forward merge, can be skipped. Rebasing or squashing can assign new committer dates.\n\nEach poll reads up to 10,000 commits. If more commits match, the oldest may be skipped.\n\nCAUTION: a workflow that writes a file to the watched branch can create a commit that re-triggers itself. Each new commit has a new SHA, so deduplication cannot stop the loop.
|
|
32530
|
+
description: "Starts one run per commit whose committer date is newer than this link’s cursor. This follows commit dates, not push times: pushing an older local commit or merging existing commits without changing their committer dates can leave them undetected. A poll reads up to 10,000 commits; a larger window can skip its oldest commits. CAUTION: a workflow that writes a file to the watched branch can create a commit that re-triggers itself. Each new commit has a new SHA, so deduplication cannot stop the loop. THIS CONNECTOR EXPOSES NO WRITE OPERATION, so the `isOwnEcho` guard has nothing to compare against and the workaround is the branch: write to a separate branch that this link does not watch. A trigger mapping cannot skip an event.",
|
|
32531
|
+
docs: "Watches commits on the selected branch, or the repository’s default branch when Branch is blank. Starts one run per commit whose committer date is newer than the saved cursor.\n\nThe cursor starts at the time you turn the link on. Push time is not recorded: commits created earlier and pushed later, including existing commits added by a fast-forward merge, can be skipped. Rebasing or squashing can assign new committer dates.\n\nEach poll reads up to 10,000 commits. If more commits match, the oldest may be skipped.\n\nCAUTION: a workflow that writes a file to the watched branch can create a commit that re-triggers itself. Each new commit has a new SHA, so deduplication cannot stop the loop. This connector exposes no write operation, so the `isOwnEcho` guard has nothing to compare against, and the workaround is the branch: write to a separate branch that this link does not watch. A trigger mapping cannot skip an event.",
|
|
32308
32532
|
sampleData: {
|
|
32309
32533
|
sha: "11589d462dc2b29714240e03ce310e7b15aa94c3",
|
|
32310
32534
|
url: "https://github.com/stackbone/stackbone/commit/11589d462dc2b29714240e03ce310e7b15aa94c3",
|
|
@@ -32336,6 +32560,7 @@ var init_github_connector = __esmMin((() => {
|
|
|
32336
32560
|
kind: "declarative",
|
|
32337
32561
|
list: {
|
|
32338
32562
|
operationId: "listCommits",
|
|
32563
|
+
order: "newest-first",
|
|
32339
32564
|
args: { "X-GitHub-Api-Version": "2022-11-28" },
|
|
32340
32565
|
pageNumber: {
|
|
32341
32566
|
param: "page",
|
|
@@ -32385,8 +32610,8 @@ var init_github_connector = __esmMin((() => {
|
|
|
32385
32610
|
}, {
|
|
32386
32611
|
id: "issue-updated",
|
|
32387
32612
|
displayName: "Issue updated",
|
|
32388
|
-
description: "Fires on every change to an issue in the repository after the link was turned on, following `updated_at`. CLOSED ISSUES ARE INCLUDED — the link asks for `state=all`, so resolving an issue is a change you receive. PULL REQUESTS ARRIVE HERE TOO: GitHub models every pull request as an issue and this endpoint returns both, and only a `pullRequest` object on the payload tells them apart — it is absent on a real issue. A newly opened issue arrives as well, with `createdAt` equal to `updatedAt`. CAUTION: a workflow that writes back to the issue it received — a comment, a label, a close — bumps `updated_at` and re-triggers itself, and the delivery journal cannot stop it. GitHub sends nothing that says who made a change (`author` is who OPENED the issue), so the guard
|
|
32389
|
-
docs: "Starts a run on every change to an issue in the repository, from the moment the link is turned on. Closed issues count: resolving one is a change you receive. Pull requests arrive here too, because GitHub models each one as an issue. A `pullRequest` object on the event is what tells them apart, and it is absent on a real issue. A newly opened issue arrives as well, with `createdAt` equal to `updatedAt`.\n\nCAUTION: a workflow that writes back to the issue it received (a comment, a label, a close) bumps `updatedAt` and starts itself again. Nothing on the platform stops that loop, and GitHub sends no field saying who made a change (`author` is whoever opened the issue).
|
|
32613
|
+
description: "Fires on every change to an issue in the repository after the link was turned on, following `updated_at`. CLOSED ISSUES ARE INCLUDED — the link asks for `state=all`, so resolving an issue is a change you receive. PULL REQUESTS ARRIVE HERE TOO: GitHub models every pull request as an issue and this endpoint returns both, and only a `pullRequest` object on the payload tells them apart — it is absent on a real issue. A newly opened issue arrives as well, with `createdAt` equal to `updatedAt`. CAUTION: a workflow that writes back to the issue it received — a comment, a label, a close — bumps `updated_at` and re-triggers itself, and the delivery journal cannot stop it. GitHub sends nothing that says who made a change (`author` is who OPENED the issue), and THIS CONNECTOR EXPOSES NO WRITE OPERATION, so the `isOwnEcho` guard has nothing to compare against: whatever wrote the issue did not go through here. Until a write lands on this connector the workaround is outside it. A trigger mapping cannot skip an item; every delivered event starts a run.",
|
|
32614
|
+
docs: "Starts a run on every change to an issue in the repository, from the moment the link is turned on. Closed issues count: resolving one is a change you receive. Pull requests arrive here too, because GitHub models each one as an issue. A `pullRequest` object on the event is what tells them apart, and it is absent on a real issue. A newly opened issue arrives as well, with `createdAt` equal to `updatedAt`.\n\nCAUTION: a workflow that writes back to the issue it received (a comment, a label, a close) bumps `updatedAt` and starts itself again. Nothing on the platform stops that loop, and GitHub sends no field saying who made a change (`author` is whoever opened the issue). This connector exposes no write operation, so the `isOwnEcho` guard has nothing to compare against: whatever wrote the issue did not go through here. Until a write lands on this connector, the workaround is outside it. A mapping cannot skip an event: every delivery starts a run.",
|
|
32390
32615
|
sampleData: {
|
|
32391
32616
|
issueId: 5320382826,
|
|
32392
32617
|
number: 14317,
|
|
@@ -32455,6 +32680,7 @@ var init_github_connector = __esmMin((() => {
|
|
|
32455
32680
|
kind: "declarative",
|
|
32456
32681
|
list: {
|
|
32457
32682
|
operationId: "listIssues",
|
|
32683
|
+
order: "oldest-first",
|
|
32458
32684
|
args: {
|
|
32459
32685
|
"X-GitHub-Api-Version": "2022-11-28",
|
|
32460
32686
|
state: "all",
|
|
@@ -32782,7 +33008,7 @@ var init_google_drive_connector = __esmMin((() => {
|
|
|
32782
33008
|
post: {
|
|
32783
33009
|
operationId: "drive.files.create",
|
|
32784
33010
|
summary: "Create a folder, or a file with metadata but no content",
|
|
32785
|
-
description: "Metadata only — this sends JSON, never bytes. A folder is `mimeType: \"application/vnd.google-apps.folder\"`. To create a file WITH content use `drive.files.upload`, which sends the bytes from object storage against Google's upload host; this operation creates the file empty. If a workflow of yours also watches the destination folder, ask for `
|
|
33011
|
+
description: "Metadata only — this sends JSON, never bytes. A folder is `mimeType: \"application/vnd.google-apps.folder\"`. To create a file WITH content use `drive.files.upload`, which sends the bytes from object storage against Google's upload host; this operation creates the file empty. If a workflow of yours also watches the destination folder, ask for `id` in `fields` and declare `echoPath: 'id'` on the call, then open that workflow with `isOwnEcho('google-drive', event.fileId)`. The trigger keys on the file ID, so the ID is the value to compare — a `modifiedTime` echo would never match it.",
|
|
32786
33012
|
"x-stackbone-version-path": "modifiedTime",
|
|
32787
33013
|
parameters: [FIELDS_PARAM, SUPPORTS_ALL_DRIVES_PARAM],
|
|
32788
33014
|
requestBody: {
|
|
@@ -32852,7 +33078,7 @@ var init_google_drive_connector = __esmMin((() => {
|
|
|
32852
33078
|
patch: {
|
|
32853
33079
|
operationId: "drive.files.update",
|
|
32854
33080
|
summary: "Rename a file, move it between folders, or set its metadata",
|
|
32855
|
-
description: "The move is the two QUERY parameters, not the body: `addParents` and `removeParents`. `name` goes in the JSON body. Both in one call is how a file gets named and filed at once.
|
|
33081
|
+
description: "The move is the two QUERY parameters, not the body: `addParents` and `removeParents`. `name` goes in the JSON body. Both in one call is how a file gets named and filed at once. This one does NOT re-trigger `file-added-to-folder`: that trigger fires once per file, so an edit to a file it already delivered starts nothing. The self-write loop to guard is the one the CREATE and UPLOAD operations open, not this one.",
|
|
32856
33082
|
"x-stackbone-version-path": "modifiedTime",
|
|
32857
33083
|
parameters: [
|
|
32858
33084
|
{
|
|
@@ -33160,8 +33386,8 @@ var init_google_drive_connector = __esmMin((() => {
|
|
|
33160
33386
|
triggers: [{
|
|
33161
33387
|
id: "file-added-to-folder",
|
|
33162
33388
|
displayName: "File added to folder",
|
|
33163
|
-
description: "Starts a run the first time a file in one Drive folder arrives or changes — once per file, so later edits to a file already delivered start nothing. Watches the folder itself, not its subfolders. CAUTION: a file this connector uploads to the watched folder can re-trigger the workflow. Each upload creates a new file id, so deduplication cannot stop the loop. Guard
|
|
33164
|
-
docs: "Starts a run the first time a file in one Drive folder arrives or changes. Once per file: a later edit to a file already delivered starts nothing. It watches the folder itself, not its subfolders.\n\nCAUTION: a file this connector uploads to the watched folder
|
|
33389
|
+
description: "Starts a run the first time a file in one Drive folder arrives or changes — once per file, so later edits to a file already delivered start nothing. Watches the folder itself, not its subfolders. CAUTION: a file this connector uploads to the watched folder can re-trigger the workflow. Each upload creates a new file id, so deduplication cannot stop the loop. Guard INSIDE THE WORKFLOW BODY with `isOwnEcho`, for a file the workflow creates by calling `drive.files.create` or `drive.files.upload` ITSELF: pass `fields: 'id'` and `echoPath: 'id'` on that call, then open the workflow with `if (await isOwnEcho('google-drive', event.fileId)) return;`. NAME `fields` FOR WHATEVER THE ECHO PATH POINTS AT, because Drive answers a partial response by default and a path onto a field it left out resolves to nothing, which makes the guard throw rather than answer. THE `drive.file-write` OUTPUT WRITER CANNOT CARRY AN ECHO PATH, so a workflow that files its output through it is not covered and still needs separate input and output folders. A trigger mapping cannot skip an event.",
|
|
33390
|
+
docs: "Starts a run the first time a file in one Drive folder arrives or changes. Once per file: a later edit to a file already delivered starts nothing. It watches the folder itself, not its subfolders.\n\nCAUTION: a file this connector uploads to the watched folder starts the workflow again. Each upload creates a new file id, so deduplication cannot stop the loop. Guard inside the workflow body with `isOwnEcho`, for a file the workflow creates by calling Drive's own `create` or `upload` operation itself: pass `fields: 'id'` and `echoPath: 'id'` on that call, then open the workflow with `if (await isOwnEcho('google-drive', event.fileId)) return;`. Name `fields` for whatever the echo path points at, because Drive answers a partial response by default, and a path onto a field it left out resolves to nothing, which makes the guard throw rather than answer. The `drive.file-write` output writer cannot carry an echo path, so a workflow that files its output through it is not covered and still needs separate input and output folders. A mapping cannot skip an event: every delivery starts a run.",
|
|
33165
33391
|
sampleData: {
|
|
33166
33392
|
fileId: "1a2B3cD4eF5gH6iJ7kL8mN9oP",
|
|
33167
33393
|
name: "Weekly sync 2026-08-25.pdf",
|
|
@@ -33200,6 +33426,7 @@ var init_google_drive_connector = __esmMin((() => {
|
|
|
33200
33426
|
kind: "declarative",
|
|
33201
33427
|
list: {
|
|
33202
33428
|
operationId: "drive.files.list",
|
|
33429
|
+
order: "oldest-first",
|
|
33203
33430
|
scopeParams: [{
|
|
33204
33431
|
param: "q",
|
|
33205
33432
|
displayName: "Folder ID",
|
|
@@ -33381,8 +33608,8 @@ var init_linear_connector = __esmMin((() => {
|
|
|
33381
33608
|
{
|
|
33382
33609
|
id: "issue-created",
|
|
33383
33610
|
displayName: "Issue created",
|
|
33384
|
-
description: "Fires once per issue created in the workspace after the link was turned on, following `createdAt`. CAUTION: a workflow that creates an issue in this workspace can re-trigger itself. Each new issue has a new id, so deduplication cannot stop the loop. Guard
|
|
33385
|
-
docs: "Starts one run per issue created in the workspace, from the moment the link is turned on.\n\nCAUTION: a workflow that creates an issue in this workspace
|
|
33611
|
+
description: "Fires once per issue created in the workspace after the link was turned on, following `createdAt`. CAUTION: a workflow that creates an issue in this workspace can re-trigger itself. Each new issue has a new id, so deduplication cannot stop the loop. Guard INSIDE THE WORKFLOW BODY with `isOwnEcho`: have the `issueCreate` mutation select `issue { id }`, declare `echoPath: 'data.issueCreate.issue.id'` on the write, then open the workflow with `if (await isOwnEcho('linear', event.issueId)) return;`. It compares the id your own write returned against the one delivered. The connector id you pass is the one you passed to `connection()`. A trigger mapping cannot skip an event.",
|
|
33612
|
+
docs: "Starts one run per issue created in the workspace, from the moment the link is turned on.\n\nCAUTION: a workflow that creates an issue in this workspace starts itself again. Each new issue has a new id, so deduplication cannot stop the loop. Guard inside the workflow body with `isOwnEcho`. The `issueCreate` mutation has to select `issue { id }`, because the guard can only record a value the response carries. Declare `echoPath: 'data.issueCreate.issue.id'` on the write, then open the workflow with `if (await isOwnEcho('linear', event.issueId)) return;`. That compares the id your own write returned against the one you were handed. The connector id you pass to `isOwnEcho` is the same one you passed to `connection()`. A mapping cannot skip an event: every delivery starts a run.",
|
|
33386
33613
|
sampleData: {
|
|
33387
33614
|
issueId: "a1b2c3d4-0000-0000-0000-000000000000",
|
|
33388
33615
|
identifier: "PLA-812",
|
|
@@ -33436,6 +33663,7 @@ var init_linear_connector = __esmMin((() => {
|
|
|
33436
33663
|
kind: "declarative",
|
|
33437
33664
|
list: {
|
|
33438
33665
|
operationId: "graphql",
|
|
33666
|
+
order: "newest-first",
|
|
33439
33667
|
args: {
|
|
33440
33668
|
query: ISSUE_CREATED_DOCUMENT,
|
|
33441
33669
|
variables: { first: 50 }
|
|
@@ -33471,8 +33699,8 @@ var init_linear_connector = __esmMin((() => {
|
|
|
33471
33699
|
{
|
|
33472
33700
|
id: "comment-created",
|
|
33473
33701
|
displayName: "Comment created",
|
|
33474
|
-
description: "Fires once per comment posted on any issue in the workspace after the link was turned on, following `createdAt`. CAUTION: a workflow that replies by posting a comment in this workspace can re-trigger itself. Each new comment has a new id, so deduplication cannot stop the loop. Guard
|
|
33475
|
-
docs: "Starts one run per comment posted on any issue in the workspace, from the moment the link is turned on.\n\nCAUTION: a workflow that replies by posting a comment in this workspace
|
|
33702
|
+
description: "Fires once per comment posted on any issue in the workspace after the link was turned on, following `createdAt`. CAUTION: a workflow that replies by posting a comment in this workspace can re-trigger itself. Each new comment has a new id, so deduplication cannot stop the loop. Guard INSIDE THE WORKFLOW BODY with `isOwnEcho`: have the `commentCreate` mutation select `comment { id }`, declare `echoPath: 'data.commentCreate.comment.id'` on the write, then open the workflow with `if (await isOwnEcho('linear', event.commentId)) return;`. It compares the id your own write returned against the one delivered. A trigger mapping cannot skip an event.",
|
|
33703
|
+
docs: "Starts one run per comment posted on any issue in the workspace, from the moment the link is turned on.\n\nCAUTION: a workflow that replies by posting a comment in this workspace starts itself again. Each new comment has a new id, so deduplication cannot stop the loop. Guard inside the workflow body with `isOwnEcho`. The `commentCreate` mutation has to select `comment { id }`, because the guard can only record a value the response carries. Declare `echoPath: 'data.commentCreate.comment.id'` on the write, then open the workflow with `if (await isOwnEcho('linear', event.commentId)) return;`. That compares the id your own write returned against the one you were handed. A mapping cannot skip an event: every delivery starts a run.",
|
|
33476
33704
|
sampleData: {
|
|
33477
33705
|
commentId: "e5f6a7b8-0000-0000-0000-000000000000",
|
|
33478
33706
|
body: "Shipped on `next`, closing.",
|
|
@@ -33533,6 +33761,7 @@ var init_linear_connector = __esmMin((() => {
|
|
|
33533
33761
|
kind: "declarative",
|
|
33534
33762
|
list: {
|
|
33535
33763
|
operationId: "graphql",
|
|
33764
|
+
order: "newest-first",
|
|
33536
33765
|
args: {
|
|
33537
33766
|
query: COMMENT_CREATED_DOCUMENT,
|
|
33538
33767
|
variables: { first: 50 }
|
|
@@ -33564,8 +33793,8 @@ var init_linear_connector = __esmMin((() => {
|
|
|
33564
33793
|
{
|
|
33565
33794
|
id: "issue-updated",
|
|
33566
33795
|
displayName: "Issue updated",
|
|
33567
|
-
description: "Fires on every change to an issue in the workspace after the link was turned on, following `updatedAt`. A newly created issue arrives here too — compare `createdAt` with `updatedAt` to tell a birth from an edit. `lastActor` / `lastBotActor` carry the actor of the issue's NEWEST HISTORY ENTRY — a `User` when a personal API key made the change, a bot when an OAuth app did.
|
|
33568
|
-
docs: "Starts a run on every change to an issue in the workspace, from the moment the link is turned on. A newly created issue arrives here too, so compare `createdAt` with `updatedAt` to tell a birth from an edit. Archiving is not delivered, because an archived issue stops matching.\n\n`lastActor` and `lastBotActor` carry whoever made the newest change: `lastActor` when a personal API key did it, `lastBotActor` when an OAuth app did.
|
|
33796
|
+
description: "Fires on every change to an issue in the workspace after the link was turned on, following `updatedAt`. A newly created issue arrives here too — compare `createdAt` with `updatedAt` to tell a birth from an edit. `lastActor` / `lastBotActor` carry the actor of the issue's NEWEST HISTORY ENTRY — a `User` when a personal API key made the change, a bot when an OAuth app did. Use them to answer who touched this, for instance to ignore every bot's edits; they are not a self-write guard, because either can arrive null and an `updatedAt` bump that minted no history entry leaves the previous change's actor in place. CAUTION: a workflow that writes back to the issue it received bumps `updatedAt` and re-triggers itself, and the delivery journal cannot stop it. Guard INSIDE THE WORKFLOW BODY with `isOwnEcho`: declare `echoPath: 'data.issueUpdate.issue.updatedAt'` on the write, then open the workflow with `if (await isOwnEcho('linear', event.updatedAt)) return;`. It compares the value your own write returned against the one delivered, so it does not depend on an actor field being fresh. A trigger mapping cannot skip an item; every delivered event starts a run. Write-back can also overwrite a human edit made since the event was read: issueUpdate exposes no version or expected-value precondition. Write only fields reserved for the workflow; reading again before writing does not make the update atomic. Archiving is not delivered, because an archived issue stops matching.",
|
|
33797
|
+
docs: "Starts a run on every change to an issue in the workspace, from the moment the link is turned on. A newly created issue arrives here too, so compare `createdAt` with `updatedAt` to tell a birth from an edit. Archiving is not delivered, because an archived issue stops matching.\n\n`lastActor` and `lastBotActor` carry whoever made the newest change: `lastActor` when a personal API key did it, `lastBotActor` when an OAuth app did. They answer who touched this, which is useful on its own, for instance to ignore edits made by bots. They are not the self-write guard: either can arrive null, and a change that minted no history entry leaves the previous actor in place.\n\nCAUTION: a workflow that writes back to the issue it received bumps `updatedAt` and starts itself again. Nothing on the platform stops that loop. Guard inside the workflow body with `isOwnEcho`. Declare where your write reports the new version, `echoPath: 'data.issueUpdate.issue.updatedAt'`, then open the workflow with `if (await isOwnEcho('linear', event.updatedAt)) return;`. That compares the value your own write returned against the one you were handed, so it holds whether or not an actor field is fresh. A mapping cannot skip an event: every delivery starts a run.\n\nWrite-back can also overwrite a human edit made since the event was read. `issueUpdate` exposes no version or expected-value precondition. Write only fields reserved for the workflow; reading again before writing does not make the update atomic.",
|
|
33569
33798
|
sampleData: {
|
|
33570
33799
|
issueId: "a1b2c3d4-0000-0000-0000-000000000000",
|
|
33571
33800
|
identifier: "PLA-812",
|
|
@@ -33643,6 +33872,7 @@ var init_linear_connector = __esmMin((() => {
|
|
|
33643
33872
|
kind: "declarative",
|
|
33644
33873
|
list: {
|
|
33645
33874
|
operationId: "graphql",
|
|
33875
|
+
order: "newest-first",
|
|
33646
33876
|
args: {
|
|
33647
33877
|
query: ISSUE_UPDATED_DOCUMENT,
|
|
33648
33878
|
variables: { first: 50 }
|
|
@@ -33905,6 +34135,1020 @@ var init_outlook_app_only_connector = __esmMin((() => {
|
|
|
33905
34135
|
};
|
|
33906
34136
|
}));
|
|
33907
34137
|
//#endregion
|
|
34138
|
+
//#region ../../libs/shared/connect-registry/src/lib/catalog/stripe/stripe.connector.ts
|
|
34139
|
+
var IDEMPOTENCY_KEY_PARAM, CHARGE_SCHEMA, STRIPE_CONNECTOR;
|
|
34140
|
+
var init_stripe_connector = __esmMin((() => {
|
|
34141
|
+
IDEMPOTENCY_KEY_PARAM = {
|
|
34142
|
+
name: "Idempotency-Key",
|
|
34143
|
+
in: "header",
|
|
34144
|
+
required: false,
|
|
34145
|
+
description: "A caller-chosen key that stays the same on every retry of the same call — a message id, a run id — and different for a genuinely new one. Stripe replays the first response for 24 hours instead of performing the operation again, and refuses a reused key whose arguments changed. Omitted, a retry is a second write.",
|
|
34146
|
+
schema: { type: "string" }
|
|
34147
|
+
};
|
|
34148
|
+
CHARGE_SCHEMA = {
|
|
34149
|
+
type: "object",
|
|
34150
|
+
properties: {
|
|
34151
|
+
id: { type: "string" },
|
|
34152
|
+
object: { type: "string" },
|
|
34153
|
+
amount: {
|
|
34154
|
+
type: "integer",
|
|
34155
|
+
description: "What was charged, in the smallest unit of `currency`."
|
|
34156
|
+
},
|
|
34157
|
+
amount_captured: { type: "integer" },
|
|
34158
|
+
amount_refunded: { type: "integer" },
|
|
34159
|
+
currency: { type: "string" },
|
|
34160
|
+
customer: {
|
|
34161
|
+
type: ["string", "null"],
|
|
34162
|
+
description: "The customer id, or null for a charge made without one."
|
|
34163
|
+
},
|
|
34164
|
+
description: { type: ["string", "null"] },
|
|
34165
|
+
failure_code: { type: ["string", "null"] },
|
|
34166
|
+
failure_message: { type: ["string", "null"] },
|
|
34167
|
+
paid: { type: "boolean" },
|
|
34168
|
+
refunded: { type: "boolean" },
|
|
34169
|
+
receipt_url: { type: ["string", "null"] },
|
|
34170
|
+
payment_intent: { type: ["string", "null"] },
|
|
34171
|
+
status: {
|
|
34172
|
+
type: "string",
|
|
34173
|
+
description: "One of `succeeded`, `pending` or `failed`."
|
|
34174
|
+
},
|
|
34175
|
+
created: { type: "integer" },
|
|
34176
|
+
livemode: { type: "boolean" },
|
|
34177
|
+
metadata: {
|
|
34178
|
+
type: "object",
|
|
34179
|
+
additionalProperties: true
|
|
34180
|
+
}
|
|
34181
|
+
},
|
|
34182
|
+
required: [
|
|
34183
|
+
"id",
|
|
34184
|
+
"object",
|
|
34185
|
+
"amount",
|
|
34186
|
+
"amount_captured",
|
|
34187
|
+
"amount_refunded",
|
|
34188
|
+
"currency",
|
|
34189
|
+
"created",
|
|
34190
|
+
"livemode",
|
|
34191
|
+
"metadata",
|
|
34192
|
+
"paid",
|
|
34193
|
+
"refunded",
|
|
34194
|
+
"status"
|
|
34195
|
+
]
|
|
34196
|
+
};
|
|
34197
|
+
STRIPE_CONNECTOR = {
|
|
34198
|
+
id: "stripe",
|
|
34199
|
+
displayName: "Stripe",
|
|
34200
|
+
description: "Read a Stripe account — its event feed, its charges, one payment intent, one invoice, one subscription, the dispute on a charge, the balance — and write two things: a payment intent, and a refund. Exposes an `event-received` trigger.",
|
|
34201
|
+
credentialNote: "Create a restricted key (`rk_…`) in your Stripe dashboard with read permission on Events, Charges, Customers, Payment Intents, Balance, Invoices, Subscriptions and Disputes. Tick only the resources your workflows read: a key missing one answers that operation with a 403 and leaves the rest working. If a workflow also creates payment intents or issues refunds, that key needs WRITE on Payment Intents and Refunds too — a read-only key answers those two with a 403.\n\nA full secret key (`sk_…`) works for everything and is the account’s master credential, so it can do a great deal more than this connector asks for.\n\nTwo of the reads follow fields Stripe moved in a recent API version: the subscription behind an invoice, and a subscription’s billing period. Nothing here pins a version, so the account’s own pinned version decides which shape arrives. Check it in the dashboard under Developers before you map those two.\n\nStart with a test-mode key while you wire a workflow up. Test mode and live mode are separate accounts of everything — events, charges, balance — and an event carries `livemode` so a workflow can tell which one it came from.\n\nThe key is held by the broker. It never reaches a workspace or a browser.",
|
|
34202
|
+
logoUrl: "https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=https://stripe.com/&size=64",
|
|
34203
|
+
category: "productivity",
|
|
34204
|
+
protocol: "openapi",
|
|
34205
|
+
baseUrl: "https://api.stripe.com",
|
|
34206
|
+
authMode: "api_key",
|
|
34207
|
+
placement: { kind: "bearer" },
|
|
34208
|
+
scopes: [],
|
|
34209
|
+
specSource: {
|
|
34210
|
+
kind: "inline",
|
|
34211
|
+
document: {
|
|
34212
|
+
openapi: "3.1.0",
|
|
34213
|
+
info: {
|
|
34214
|
+
title: "Stripe API (subset)",
|
|
34215
|
+
version: "2026-08-26.dahlia"
|
|
34216
|
+
},
|
|
34217
|
+
servers: [{ url: "https://api.stripe.com" }],
|
|
34218
|
+
paths: {
|
|
34219
|
+
"/v1/events": { get: {
|
|
34220
|
+
operationId: "listEvents",
|
|
34221
|
+
summary: "List the account's events, newest first",
|
|
34222
|
+
description: "The list endpoint behind the `event-received` trigger. Newest first, and Stripe keeps events for 30 days, so this is a recent window and never the account’s whole history. `created[gte]` is the bracketed spelling of Stripe’s `created` range filter and takes seconds from the epoch rather than an ISO string, the same unit every `created` field in this API is measured in. `type` takes one event name or a family with a `*` wildcard; Stripe also accepts a `types` array, which this document does not declare because one query parameter here carries one value.",
|
|
34223
|
+
parameters: [
|
|
34224
|
+
{
|
|
34225
|
+
name: "created[gte]",
|
|
34226
|
+
in: "query",
|
|
34227
|
+
required: false,
|
|
34228
|
+
description: "Only events created at or after this instant, in seconds from the epoch. Inclusive on purpose — the trigger below explains why the strict `created[gt]` is the wrong filter for a whole-second stamp.",
|
|
34229
|
+
schema: { type: "integer" }
|
|
34230
|
+
},
|
|
34231
|
+
{
|
|
34232
|
+
name: "type",
|
|
34233
|
+
in: "query",
|
|
34234
|
+
required: false,
|
|
34235
|
+
description: "One event name (`charge.succeeded`), or a family with a wildcard (`charge.*`). Omitted, every type the account emits is listed.",
|
|
34236
|
+
schema: { type: "string" }
|
|
34237
|
+
},
|
|
34238
|
+
{
|
|
34239
|
+
name: "limit",
|
|
34240
|
+
in: "query",
|
|
34241
|
+
required: false,
|
|
34242
|
+
description: "Events per page, 1 to 100. Stripe sends 10 when this is absent, so a caller that wants a full page has to ask.",
|
|
34243
|
+
schema: { type: "integer" }
|
|
34244
|
+
},
|
|
34245
|
+
{
|
|
34246
|
+
name: "starting_after",
|
|
34247
|
+
in: "query",
|
|
34248
|
+
required: false,
|
|
34249
|
+
description: "The id of the last event on the previous page. Stripe pages by the id you already hold rather than by a token it hands back, which is why `has_more` is the only thing the response says about a next page.",
|
|
34250
|
+
schema: { type: "string" }
|
|
34251
|
+
}
|
|
34252
|
+
],
|
|
34253
|
+
responses: { "200": {
|
|
34254
|
+
description: "One page of events, newest first.",
|
|
34255
|
+
content: { "application/json": { schema: {
|
|
34256
|
+
type: "object",
|
|
34257
|
+
properties: {
|
|
34258
|
+
object: { type: "string" },
|
|
34259
|
+
url: { type: "string" },
|
|
34260
|
+
has_more: {
|
|
34261
|
+
type: "boolean",
|
|
34262
|
+
description: "Another page exists after this one. Stripe names no next cursor: the caller sends the last id as `starting_after`."
|
|
34263
|
+
},
|
|
34264
|
+
data: {
|
|
34265
|
+
type: "array",
|
|
34266
|
+
items: {
|
|
34267
|
+
type: "object",
|
|
34268
|
+
description: "One event envelope. `data.object` is the API resource the event is about, and its shape follows `type` — a charge for `charge.succeeded`, an invoice for `invoice.created`.",
|
|
34269
|
+
properties: {
|
|
34270
|
+
id: { type: "string" },
|
|
34271
|
+
object: { type: "string" },
|
|
34272
|
+
api_version: { type: ["string", "null"] },
|
|
34273
|
+
created: {
|
|
34274
|
+
type: "integer",
|
|
34275
|
+
description: "Seconds from the epoch. The trigger cursor reads this."
|
|
34276
|
+
},
|
|
34277
|
+
livemode: { type: "boolean" },
|
|
34278
|
+
pending_webhooks: { type: "integer" },
|
|
34279
|
+
type: { type: "string" },
|
|
34280
|
+
request: {
|
|
34281
|
+
type: ["object", "null"],
|
|
34282
|
+
properties: {
|
|
34283
|
+
id: { type: ["string", "null"] },
|
|
34284
|
+
idempotency_key: { type: ["string", "null"] }
|
|
34285
|
+
}
|
|
34286
|
+
},
|
|
34287
|
+
data: {
|
|
34288
|
+
type: "object",
|
|
34289
|
+
properties: {
|
|
34290
|
+
object: {
|
|
34291
|
+
type: "object",
|
|
34292
|
+
additionalProperties: true
|
|
34293
|
+
},
|
|
34294
|
+
previous_attributes: {
|
|
34295
|
+
type: "object",
|
|
34296
|
+
additionalProperties: true,
|
|
34297
|
+
description: "The attributes as they were before the change, on a `*.updated` event only."
|
|
34298
|
+
}
|
|
34299
|
+
},
|
|
34300
|
+
required: ["object"]
|
|
34301
|
+
}
|
|
34302
|
+
},
|
|
34303
|
+
required: [
|
|
34304
|
+
"id",
|
|
34305
|
+
"object",
|
|
34306
|
+
"created",
|
|
34307
|
+
"livemode",
|
|
34308
|
+
"pending_webhooks",
|
|
34309
|
+
"type",
|
|
34310
|
+
"data"
|
|
34311
|
+
]
|
|
34312
|
+
}
|
|
34313
|
+
}
|
|
34314
|
+
},
|
|
34315
|
+
required: [
|
|
34316
|
+
"object",
|
|
34317
|
+
"data",
|
|
34318
|
+
"has_more",
|
|
34319
|
+
"url"
|
|
34320
|
+
]
|
|
34321
|
+
} } }
|
|
34322
|
+
} }
|
|
34323
|
+
} },
|
|
34324
|
+
"/v1/customers": { get: {
|
|
34325
|
+
operationId: "listCustomers",
|
|
34326
|
+
summary: "List customers, filtered by email address",
|
|
34327
|
+
description: "Customers, newest first, and the one hop from a person to their payments: every other read here is keyed by a Stripe id, so a workflow that starts from a mail, a form or a chat message has nothing to look up. Feed an id from here to `listCharges`’ `customer` filter.\n\nA LIST and not a lookup, because an email is not unique on a Stripe account: the same address can sit on several customers, created by different integrations or by a checkout that made a new one each time. A caller that reads `data[0]` and acts on it is refunding whichever row Stripe happened to return first. Zero rows is a normal answer too — the sender may simply not be a customer.\n\nOnly exact addresses match. Stripe does not do partial or case-insensitive matching on this filter; its Search API is a different endpoint with its own query language and is not declared here.",
|
|
34328
|
+
parameters: [
|
|
34329
|
+
{
|
|
34330
|
+
name: "email",
|
|
34331
|
+
in: "query",
|
|
34332
|
+
required: false,
|
|
34333
|
+
description: "Only customers whose address is exactly this one. Omitted, the answer is every customer, newest first.",
|
|
34334
|
+
schema: { type: "string" }
|
|
34335
|
+
},
|
|
34336
|
+
{
|
|
34337
|
+
name: "limit",
|
|
34338
|
+
in: "query",
|
|
34339
|
+
required: false,
|
|
34340
|
+
description: "Customers per page, 1 to 100. Stripe defaults it to 10.",
|
|
34341
|
+
schema: { type: "integer" }
|
|
34342
|
+
},
|
|
34343
|
+
{
|
|
34344
|
+
name: "starting_after",
|
|
34345
|
+
in: "query",
|
|
34346
|
+
required: false,
|
|
34347
|
+
description: "The id of the last customer on the previous page.",
|
|
34348
|
+
schema: { type: "string" }
|
|
34349
|
+
}
|
|
34350
|
+
],
|
|
34351
|
+
responses: { "200": {
|
|
34352
|
+
description: "One page of customers, newest first.",
|
|
34353
|
+
content: { "application/json": { schema: {
|
|
34354
|
+
type: "object",
|
|
34355
|
+
properties: {
|
|
34356
|
+
object: { type: "string" },
|
|
34357
|
+
url: { type: "string" },
|
|
34358
|
+
has_more: { type: "boolean" },
|
|
34359
|
+
data: {
|
|
34360
|
+
type: "array",
|
|
34361
|
+
items: {
|
|
34362
|
+
type: "object",
|
|
34363
|
+
properties: {
|
|
34364
|
+
id: { type: "string" },
|
|
34365
|
+
object: { type: "string" },
|
|
34366
|
+
email: { type: ["string", "null"] },
|
|
34367
|
+
name: { type: ["string", "null"] },
|
|
34368
|
+
created: { type: "integer" },
|
|
34369
|
+
livemode: { type: "boolean" }
|
|
34370
|
+
},
|
|
34371
|
+
required: [
|
|
34372
|
+
"id",
|
|
34373
|
+
"object",
|
|
34374
|
+
"created",
|
|
34375
|
+
"livemode"
|
|
34376
|
+
]
|
|
34377
|
+
}
|
|
34378
|
+
}
|
|
34379
|
+
},
|
|
34380
|
+
required: [
|
|
34381
|
+
"object",
|
|
34382
|
+
"data",
|
|
34383
|
+
"has_more",
|
|
34384
|
+
"url"
|
|
34385
|
+
]
|
|
34386
|
+
} } }
|
|
34387
|
+
} }
|
|
34388
|
+
} },
|
|
34389
|
+
"/v1/charges": { get: {
|
|
34390
|
+
operationId: "listCharges",
|
|
34391
|
+
summary: "List the account's charges, newest first",
|
|
34392
|
+
description: "Charges, newest first. Amounts are in the currency’s smallest unit — 4900 in `eur` is 49.00 euros, and a zero-decimal currency like `jpy` has no such factor at all, so nothing may divide by 100 unconditionally. `created[gte]` takes seconds from the epoch, as everywhere else in this API.",
|
|
34393
|
+
parameters: [
|
|
34394
|
+
{
|
|
34395
|
+
name: "created[gte]",
|
|
34396
|
+
in: "query",
|
|
34397
|
+
required: false,
|
|
34398
|
+
description: "Only charges created at or after this instant, in seconds from the epoch.",
|
|
34399
|
+
schema: { type: "integer" }
|
|
34400
|
+
},
|
|
34401
|
+
{
|
|
34402
|
+
name: "customer",
|
|
34403
|
+
in: "query",
|
|
34404
|
+
required: false,
|
|
34405
|
+
description: "Only charges for this customer id (`cus_…`).",
|
|
34406
|
+
schema: { type: "string" }
|
|
34407
|
+
},
|
|
34408
|
+
{
|
|
34409
|
+
name: "payment_intent",
|
|
34410
|
+
in: "query",
|
|
34411
|
+
required: false,
|
|
34412
|
+
description: "Only charges belonging to this payment intent (`pi_…`).",
|
|
34413
|
+
schema: { type: "string" }
|
|
34414
|
+
},
|
|
34415
|
+
{
|
|
34416
|
+
name: "limit",
|
|
34417
|
+
in: "query",
|
|
34418
|
+
required: false,
|
|
34419
|
+
description: "Charges per page, 1 to 100. Stripe defaults it to 10.",
|
|
34420
|
+
schema: { type: "integer" }
|
|
34421
|
+
},
|
|
34422
|
+
{
|
|
34423
|
+
name: "starting_after",
|
|
34424
|
+
in: "query",
|
|
34425
|
+
required: false,
|
|
34426
|
+
description: "The id of the last charge on the previous page.",
|
|
34427
|
+
schema: { type: "string" }
|
|
34428
|
+
}
|
|
34429
|
+
],
|
|
34430
|
+
responses: { "200": {
|
|
34431
|
+
description: "One page of charges, newest first.",
|
|
34432
|
+
content: { "application/json": { schema: {
|
|
34433
|
+
type: "object",
|
|
34434
|
+
properties: {
|
|
34435
|
+
object: { type: "string" },
|
|
34436
|
+
url: { type: "string" },
|
|
34437
|
+
has_more: { type: "boolean" },
|
|
34438
|
+
data: {
|
|
34439
|
+
type: "array",
|
|
34440
|
+
items: CHARGE_SCHEMA
|
|
34441
|
+
}
|
|
34442
|
+
},
|
|
34443
|
+
required: [
|
|
34444
|
+
"object",
|
|
34445
|
+
"data",
|
|
34446
|
+
"has_more",
|
|
34447
|
+
"url"
|
|
34448
|
+
]
|
|
34449
|
+
} } }
|
|
34450
|
+
} }
|
|
34451
|
+
} },
|
|
34452
|
+
"/v1/charges/{charge}": { get: {
|
|
34453
|
+
operationId: "getCharge",
|
|
34454
|
+
summary: "Retrieve one charge by id",
|
|
34455
|
+
description: "One charge, by the `ch_…` a workflow already holds. The alternative is walking `listCharges` and matching client-side, which works on a quiet account and stops working on a busy one: the charge falls off the page and the caller reports no amount for a charge that exists. The path parameter is named `charge` because that is what Stripe’s own document calls it.\n\nThe body is the same shape `listCharges` returns as a page item, down to the nullable fields — it is one declaration, so a field readable from the list is readable here.",
|
|
34456
|
+
parameters: [{
|
|
34457
|
+
name: "charge",
|
|
34458
|
+
in: "path",
|
|
34459
|
+
required: true,
|
|
34460
|
+
description: "The charge id, e.g. `ch_3PqR2sC0mNqLxYzA0kL1mN2o`.",
|
|
34461
|
+
schema: { type: "string" }
|
|
34462
|
+
}],
|
|
34463
|
+
responses: { "200": {
|
|
34464
|
+
description: "The charge.",
|
|
34465
|
+
content: { "application/json": { schema: CHARGE_SCHEMA } }
|
|
34466
|
+
} }
|
|
34467
|
+
} },
|
|
34468
|
+
"/v1/payment_intents/{intent}": { get: {
|
|
34469
|
+
operationId: "getPaymentIntent",
|
|
34470
|
+
summary: "Retrieve one payment intent by id",
|
|
34471
|
+
description: "One payment intent. The path parameter is named `intent` because that is what Stripe’s own document calls it; the value is the `pi_…` id.\n\n`client_secret` is left out of the schema below, and that is a note rather than a filter: Stripe returns it on every read of an intent and the broker hands the body back as it arrived. It is the value a browser uses to confirm the payment, so a workflow that keeps it has put a bearer credential for one payment into its own output. Leaving it undeclared says do not reach for it; nothing stops you.",
|
|
34472
|
+
parameters: [{
|
|
34473
|
+
name: "intent",
|
|
34474
|
+
in: "path",
|
|
34475
|
+
required: true,
|
|
34476
|
+
description: "The payment intent id, e.g. `pi_3PqR2sC0mNqLxYzA1cD2eF3g`.",
|
|
34477
|
+
schema: { type: "string" }
|
|
34478
|
+
}],
|
|
34479
|
+
responses: { "200": {
|
|
34480
|
+
description: "The payment intent.",
|
|
34481
|
+
content: { "application/json": { schema: {
|
|
34482
|
+
type: "object",
|
|
34483
|
+
properties: {
|
|
34484
|
+
id: { type: "string" },
|
|
34485
|
+
object: { type: "string" },
|
|
34486
|
+
amount: {
|
|
34487
|
+
type: "integer",
|
|
34488
|
+
description: "What was asked for, in the smallest unit of `currency`."
|
|
34489
|
+
},
|
|
34490
|
+
amount_received: { type: "integer" },
|
|
34491
|
+
currency: { type: "string" },
|
|
34492
|
+
customer: { type: ["string", "null"] },
|
|
34493
|
+
description: { type: ["string", "null"] },
|
|
34494
|
+
capture_method: { type: "string" },
|
|
34495
|
+
confirmation_method: { type: "string" },
|
|
34496
|
+
status: {
|
|
34497
|
+
type: "string",
|
|
34498
|
+
description: "One of `requires_payment_method`, `requires_confirmation`, `requires_action`, `processing`, `requires_capture`, `canceled` or `succeeded`."
|
|
34499
|
+
},
|
|
34500
|
+
created: { type: "integer" },
|
|
34501
|
+
livemode: { type: "boolean" },
|
|
34502
|
+
metadata: {
|
|
34503
|
+
type: "object",
|
|
34504
|
+
additionalProperties: true
|
|
34505
|
+
}
|
|
34506
|
+
},
|
|
34507
|
+
required: [
|
|
34508
|
+
"id",
|
|
34509
|
+
"object",
|
|
34510
|
+
"created",
|
|
34511
|
+
"livemode",
|
|
34512
|
+
"status"
|
|
34513
|
+
]
|
|
34514
|
+
} } }
|
|
34515
|
+
} }
|
|
34516
|
+
} },
|
|
34517
|
+
"/v1/balance": { get: {
|
|
34518
|
+
operationId: "getBalance",
|
|
34519
|
+
summary: "Retrieve the account balance",
|
|
34520
|
+
description: "The balance, as arrays and not as one number: `available` and `pending` carry an entry per currency, because a Stripe account can hold several at once. An account that takes only euros still answers a one-element array, so a caller reads the entry whose `currency` it wants rather than the first one.",
|
|
34521
|
+
parameters: [],
|
|
34522
|
+
responses: { "200": {
|
|
34523
|
+
description: "The account balance, one entry per currency.",
|
|
34524
|
+
content: { "application/json": { schema: {
|
|
34525
|
+
type: "object",
|
|
34526
|
+
properties: {
|
|
34527
|
+
object: { type: "string" },
|
|
34528
|
+
livemode: { type: "boolean" },
|
|
34529
|
+
available: {
|
|
34530
|
+
type: "array",
|
|
34531
|
+
description: "Funds that can be paid out now, one entry per currency.",
|
|
34532
|
+
items: {
|
|
34533
|
+
type: "object",
|
|
34534
|
+
properties: {
|
|
34535
|
+
amount: { type: "integer" },
|
|
34536
|
+
currency: { type: "string" }
|
|
34537
|
+
},
|
|
34538
|
+
required: ["amount", "currency"]
|
|
34539
|
+
}
|
|
34540
|
+
},
|
|
34541
|
+
pending: {
|
|
34542
|
+
type: "array",
|
|
34543
|
+
description: "Funds not settled yet, one entry per currency.",
|
|
34544
|
+
items: {
|
|
34545
|
+
type: "object",
|
|
34546
|
+
properties: {
|
|
34547
|
+
amount: { type: "integer" },
|
|
34548
|
+
currency: { type: "string" }
|
|
34549
|
+
},
|
|
34550
|
+
required: ["amount", "currency"]
|
|
34551
|
+
}
|
|
34552
|
+
}
|
|
34553
|
+
},
|
|
34554
|
+
required: [
|
|
34555
|
+
"object",
|
|
34556
|
+
"available",
|
|
34557
|
+
"pending"
|
|
34558
|
+
]
|
|
34559
|
+
} } }
|
|
34560
|
+
} }
|
|
34561
|
+
} },
|
|
34562
|
+
"/v1/invoices/{invoice}": { get: {
|
|
34563
|
+
operationId: "getInvoice",
|
|
34564
|
+
summary: "Retrieve one invoice by id",
|
|
34565
|
+
description: "One invoice, by the `in_…` an event or a workflow already holds. Read it when the snapshot on the event is not enough: an invoice moves through `draft`, `open`, `paid`, `uncollectible` and `void`, and a run that started on `invoice.payment_failed` may be looking at one that has since been paid.\n\nTHE SUBSCRIPTION BEHIND AN INVOICE IS NOT A TOP-LEVEL FIELD. It is `parent.subscription_details.subscription`, and `parent` is null on an invoice nobody subscribed for. THIS DEPENDS ON THE ACCOUNT’S API VERSION: nothing here sends a `Stripe-Version` header, so Stripe serves whatever version the account is pinned to, and an account pinned before `parent` existed answers a top-level `subscription` instead. Read the version in the Stripe dashboard under Developers, and expect the older shape on an account nobody has upgraded. A workflow that wants the subscription reads that path and then calls `getSubscription`.\n\n`lines` is one page of up to ten entries, which is what a read of the invoice returns; an invoice with more has them behind its own endpoint, which this connector does not expose. A workflow that adds up `lines.data` on a longer invoice under-counts, and nothing errors, so check `lines.has_more` before you total anything.",
|
|
34566
|
+
parameters: [{
|
|
34567
|
+
name: "invoice",
|
|
34568
|
+
in: "path",
|
|
34569
|
+
required: true,
|
|
34570
|
+
description: "The invoice id, e.g. `in_1PqR2sC0mNqLxYzA3bC4dE5f`.",
|
|
34571
|
+
schema: { type: "string" }
|
|
34572
|
+
}],
|
|
34573
|
+
responses: { "200": {
|
|
34574
|
+
description: "The invoice.",
|
|
34575
|
+
content: { "application/json": { schema: {
|
|
34576
|
+
type: "object",
|
|
34577
|
+
properties: {
|
|
34578
|
+
id: { type: "string" },
|
|
34579
|
+
object: { type: "string" },
|
|
34580
|
+
number: {
|
|
34581
|
+
type: ["string", "null"],
|
|
34582
|
+
description: "The reference a customer sees. Null until the invoice is finalized."
|
|
34583
|
+
},
|
|
34584
|
+
status: {
|
|
34585
|
+
type: ["string", "null"],
|
|
34586
|
+
description: "One of `draft`, `open`, `paid`, `uncollectible` or `void`."
|
|
34587
|
+
},
|
|
34588
|
+
amount_due: {
|
|
34589
|
+
type: "integer",
|
|
34590
|
+
description: "In the smallest unit of `currency`, like every amount here."
|
|
34591
|
+
},
|
|
34592
|
+
amount_paid: { type: "integer" },
|
|
34593
|
+
amount_remaining: { type: "integer" },
|
|
34594
|
+
total: { type: "integer" },
|
|
34595
|
+
subtotal: { type: "integer" },
|
|
34596
|
+
currency: { type: "string" },
|
|
34597
|
+
customer: { type: ["string", "null"] },
|
|
34598
|
+
customer_email: { type: ["string", "null"] },
|
|
34599
|
+
customer_name: { type: ["string", "null"] },
|
|
34600
|
+
description: { type: ["string", "null"] },
|
|
34601
|
+
collection_method: {
|
|
34602
|
+
type: "string",
|
|
34603
|
+
description: "Either `charge_automatically` or `send_invoice`."
|
|
34604
|
+
},
|
|
34605
|
+
hosted_invoice_url: {
|
|
34606
|
+
type: ["string", "null"],
|
|
34607
|
+
description: "The Stripe-hosted page for this invoice. Null while it is a draft."
|
|
34608
|
+
},
|
|
34609
|
+
invoice_pdf: { type: ["string", "null"] },
|
|
34610
|
+
due_date: { type: ["integer", "null"] },
|
|
34611
|
+
period_start: { type: "integer" },
|
|
34612
|
+
period_end: { type: "integer" },
|
|
34613
|
+
created: { type: "integer" },
|
|
34614
|
+
livemode: { type: "boolean" },
|
|
34615
|
+
metadata: {
|
|
34616
|
+
type: "object",
|
|
34617
|
+
additionalProperties: true
|
|
34618
|
+
},
|
|
34619
|
+
parent: {
|
|
34620
|
+
type: ["object", "null"],
|
|
34621
|
+
description: "What generated this invoice. Null when nothing did.",
|
|
34622
|
+
properties: {
|
|
34623
|
+
type: { type: "string" },
|
|
34624
|
+
subscription_details: {
|
|
34625
|
+
type: ["object", "null"],
|
|
34626
|
+
properties: { subscription: {
|
|
34627
|
+
type: "string",
|
|
34628
|
+
description: "The `sub_…` this invoice bills, which `getSubscription` takes."
|
|
34629
|
+
} }
|
|
34630
|
+
}
|
|
34631
|
+
}
|
|
34632
|
+
},
|
|
34633
|
+
lines: {
|
|
34634
|
+
type: "object",
|
|
34635
|
+
description: "One page of line items, up to ten.",
|
|
34636
|
+
properties: {
|
|
34637
|
+
object: { type: "string" },
|
|
34638
|
+
has_more: { type: "boolean" },
|
|
34639
|
+
data: {
|
|
34640
|
+
type: "array",
|
|
34641
|
+
items: {
|
|
34642
|
+
type: "object",
|
|
34643
|
+
properties: {
|
|
34644
|
+
id: { type: "string" },
|
|
34645
|
+
object: { type: "string" },
|
|
34646
|
+
description: { type: ["string", "null"] },
|
|
34647
|
+
amount: { type: "integer" },
|
|
34648
|
+
currency: { type: "string" },
|
|
34649
|
+
quantity: { type: ["integer", "null"] }
|
|
34650
|
+
},
|
|
34651
|
+
required: [
|
|
34652
|
+
"id",
|
|
34653
|
+
"object",
|
|
34654
|
+
"amount",
|
|
34655
|
+
"currency"
|
|
34656
|
+
]
|
|
34657
|
+
}
|
|
34658
|
+
}
|
|
34659
|
+
},
|
|
34660
|
+
required: [
|
|
34661
|
+
"object",
|
|
34662
|
+
"data",
|
|
34663
|
+
"has_more"
|
|
34664
|
+
]
|
|
34665
|
+
}
|
|
34666
|
+
},
|
|
34667
|
+
required: [
|
|
34668
|
+
"id",
|
|
34669
|
+
"object",
|
|
34670
|
+
"amount_due",
|
|
34671
|
+
"amount_paid",
|
|
34672
|
+
"amount_remaining",
|
|
34673
|
+
"total",
|
|
34674
|
+
"subtotal",
|
|
34675
|
+
"currency",
|
|
34676
|
+
"collection_method",
|
|
34677
|
+
"period_start",
|
|
34678
|
+
"period_end",
|
|
34679
|
+
"created",
|
|
34680
|
+
"livemode",
|
|
34681
|
+
"lines"
|
|
34682
|
+
]
|
|
34683
|
+
} } }
|
|
34684
|
+
} }
|
|
34685
|
+
} },
|
|
34686
|
+
"/v1/subscriptions/{subscription_exposed_id}": { get: {
|
|
34687
|
+
operationId: "getSubscription",
|
|
34688
|
+
summary: "Retrieve one subscription by id",
|
|
34689
|
+
description: "One subscription, by the `sub_…` an event, an invoice or a workflow already holds. The path parameter is named `subscription_exposed_id` because that is what Stripe’s own document calls it; the value is still the `sub_…` id. Read it to answer whether a customer is still paying: `status` is one of `active`, `trialing`, `past_due`, `unpaid`, `canceled`, `incomplete`, `incomplete_expired` or `paused`.\n\nTHE BILLING PERIOD IS ON EACH ITEM, NOT ON THE SUBSCRIPTION. Stripe moved `current_period_start` and `current_period_end` onto the subscription items, because one subscription can bill several prices on different clocks. THIS TOO DEPENDS ON THE ACCOUNT’S API VERSION, for the same reason the invoice’s `parent` does: an account pinned to a version from before the move answers the period on the subscription itself and nothing on the items. A workflow that wants the period reads `items.data[0].current_period_end`, and it should say out loud that it is taking the first item.\n\n`items` IS ONE PAGE. Stripe answers a page of subscription items with `has_more` beside them, and the rest sit behind their own endpoint, which this connector does not expose. A workflow that adds up `items.data` on a subscription carrying more than one page under-counts, and nothing errors, so check `items.has_more` before you total anything.\n\n`cancel_at_period_end` is the field that says a cancellation is scheduled. A subscription with it set is still `active` and still collecting, which is why reading `status` alone answers the wrong question.",
|
|
34690
|
+
parameters: [{
|
|
34691
|
+
name: "subscription_exposed_id",
|
|
34692
|
+
in: "path",
|
|
34693
|
+
required: true,
|
|
34694
|
+
description: "The subscription id, e.g. `sub_1PqR2sC0mNqLxYzA6gH7iJ8k`.",
|
|
34695
|
+
schema: { type: "string" }
|
|
34696
|
+
}],
|
|
34697
|
+
responses: { "200": {
|
|
34698
|
+
description: "The subscription.",
|
|
34699
|
+
content: { "application/json": { schema: {
|
|
34700
|
+
type: "object",
|
|
34701
|
+
properties: {
|
|
34702
|
+
id: { type: "string" },
|
|
34703
|
+
object: { type: "string" },
|
|
34704
|
+
customer: { type: "string" },
|
|
34705
|
+
status: { type: "string" },
|
|
34706
|
+
currency: { type: "string" },
|
|
34707
|
+
collection_method: { type: "string" },
|
|
34708
|
+
description: { type: ["string", "null"] },
|
|
34709
|
+
cancel_at_period_end: { type: "boolean" },
|
|
34710
|
+
cancel_at: { type: ["integer", "null"] },
|
|
34711
|
+
canceled_at: { type: ["integer", "null"] },
|
|
34712
|
+
ended_at: { type: ["integer", "null"] },
|
|
34713
|
+
trial_start: { type: ["integer", "null"] },
|
|
34714
|
+
trial_end: { type: ["integer", "null"] },
|
|
34715
|
+
start_date: { type: "integer" },
|
|
34716
|
+
created: { type: "integer" },
|
|
34717
|
+
latest_invoice: {
|
|
34718
|
+
type: ["string", "null"],
|
|
34719
|
+
description: "The `in_…` of the most recent invoice, which `getInvoice` takes."
|
|
34720
|
+
},
|
|
34721
|
+
livemode: { type: "boolean" },
|
|
34722
|
+
metadata: {
|
|
34723
|
+
type: "object",
|
|
34724
|
+
additionalProperties: true
|
|
34725
|
+
},
|
|
34726
|
+
items: {
|
|
34727
|
+
type: "object",
|
|
34728
|
+
description: "The priced items this subscription bills, and where the billing period lives.",
|
|
34729
|
+
properties: {
|
|
34730
|
+
object: { type: "string" },
|
|
34731
|
+
has_more: { type: "boolean" },
|
|
34732
|
+
data: {
|
|
34733
|
+
type: "array",
|
|
34734
|
+
items: {
|
|
34735
|
+
type: "object",
|
|
34736
|
+
properties: {
|
|
34737
|
+
id: { type: "string" },
|
|
34738
|
+
object: { type: "string" },
|
|
34739
|
+
quantity: { type: "integer" },
|
|
34740
|
+
current_period_start: { type: "integer" },
|
|
34741
|
+
current_period_end: { type: "integer" },
|
|
34742
|
+
price: {
|
|
34743
|
+
type: "object",
|
|
34744
|
+
properties: {
|
|
34745
|
+
id: { type: "string" },
|
|
34746
|
+
currency: { type: "string" },
|
|
34747
|
+
unit_amount: {
|
|
34748
|
+
type: ["integer", "null"],
|
|
34749
|
+
description: "Null on a tiered or metered price, where no single amount exists."
|
|
34750
|
+
},
|
|
34751
|
+
recurring: {
|
|
34752
|
+
type: ["object", "null"],
|
|
34753
|
+
properties: {
|
|
34754
|
+
interval: {
|
|
34755
|
+
type: "string",
|
|
34756
|
+
description: "One of `day`, `week`, `month` or `year`."
|
|
34757
|
+
},
|
|
34758
|
+
interval_count: { type: "integer" }
|
|
34759
|
+
}
|
|
34760
|
+
}
|
|
34761
|
+
},
|
|
34762
|
+
required: ["id", "currency"]
|
|
34763
|
+
}
|
|
34764
|
+
},
|
|
34765
|
+
required: [
|
|
34766
|
+
"id",
|
|
34767
|
+
"object",
|
|
34768
|
+
"current_period_start",
|
|
34769
|
+
"current_period_end",
|
|
34770
|
+
"price"
|
|
34771
|
+
]
|
|
34772
|
+
}
|
|
34773
|
+
}
|
|
34774
|
+
},
|
|
34775
|
+
required: [
|
|
34776
|
+
"object",
|
|
34777
|
+
"data",
|
|
34778
|
+
"has_more"
|
|
34779
|
+
]
|
|
34780
|
+
}
|
|
34781
|
+
},
|
|
34782
|
+
required: [
|
|
34783
|
+
"id",
|
|
34784
|
+
"object",
|
|
34785
|
+
"customer",
|
|
34786
|
+
"status",
|
|
34787
|
+
"currency",
|
|
34788
|
+
"collection_method",
|
|
34789
|
+
"cancel_at_period_end",
|
|
34790
|
+
"start_date",
|
|
34791
|
+
"created",
|
|
34792
|
+
"livemode",
|
|
34793
|
+
"items"
|
|
34794
|
+
]
|
|
34795
|
+
} } }
|
|
34796
|
+
} }
|
|
34797
|
+
} },
|
|
34798
|
+
"/v1/charges/{charge}/dispute": { get: {
|
|
34799
|
+
operationId: "getChargeDispute",
|
|
34800
|
+
summary: "Retrieve the dispute on a charge",
|
|
34801
|
+
description: "The dispute a cardholder raised against a charge, by the `ch_…` the charge event carried. A dispute hangs off its charge rather than standing alone, which is why this reads through the charge and takes no dispute id.\n\nON A `charge.dispute.*` EVENT THE OBJECT IS THE DISPUTE, so the id this operation wants is `data.object.charge` and never `data.object.id`. The latter is a `du_…`, and the call answers 404 with `No dispute for charge: du_…`, a message that points at the wrong mistake.\n\nA CHARGE WITH NO DISPUTE IS AN ERROR AND NOT AN EMPTY ANSWER. Stripe answers 404 with `No dispute for charge: ch_…`, verified against a test account on 2026-09-17. So this is not the way to ask whether a charge was disputed: reach for it on a run started by a `charge.dispute.*` event, or handle the failure.\n\n`evidence_details.due_by` is the deadline, and it is the field the whole operation exists for: miss it and the dispute is lost by default. `is_charge_refundable` says whether refunding is still an option, which usually stops being one once a dispute opens.",
|
|
34802
|
+
parameters: [{
|
|
34803
|
+
name: "charge",
|
|
34804
|
+
in: "path",
|
|
34805
|
+
required: true,
|
|
34806
|
+
description: "The disputed charge id, e.g. `ch_3PqR2sC0mNqLxYzA0kL1mN2o`.",
|
|
34807
|
+
schema: { type: "string" }
|
|
34808
|
+
}],
|
|
34809
|
+
responses: { "200": {
|
|
34810
|
+
description: "The dispute on that charge.",
|
|
34811
|
+
content: { "application/json": { schema: {
|
|
34812
|
+
type: "object",
|
|
34813
|
+
properties: {
|
|
34814
|
+
id: { type: "string" },
|
|
34815
|
+
object: { type: "string" },
|
|
34816
|
+
charge: { type: "string" },
|
|
34817
|
+
payment_intent: { type: ["string", "null"] },
|
|
34818
|
+
amount: {
|
|
34819
|
+
type: "integer",
|
|
34820
|
+
description: "What is being disputed, in the smallest unit of `currency`. It can be less than the charge."
|
|
34821
|
+
},
|
|
34822
|
+
currency: { type: "string" },
|
|
34823
|
+
reason: {
|
|
34824
|
+
type: "string",
|
|
34825
|
+
description: "What the cardholder claimed, such as `fraudulent`, `product_not_received`, `duplicate` or `credit_not_processed`."
|
|
34826
|
+
},
|
|
34827
|
+
status: {
|
|
34828
|
+
type: "string",
|
|
34829
|
+
description: "One of `warning_needs_response`, `warning_under_review`, `warning_closed`, `needs_response`, `under_review`, `won`, `lost` or `prevented`."
|
|
34830
|
+
},
|
|
34831
|
+
is_charge_refundable: { type: "boolean" },
|
|
34832
|
+
created: { type: "integer" },
|
|
34833
|
+
livemode: { type: "boolean" },
|
|
34834
|
+
metadata: {
|
|
34835
|
+
type: "object",
|
|
34836
|
+
additionalProperties: true
|
|
34837
|
+
},
|
|
34838
|
+
evidence_details: {
|
|
34839
|
+
type: "object",
|
|
34840
|
+
properties: {
|
|
34841
|
+
due_by: {
|
|
34842
|
+
type: ["integer", "null"],
|
|
34843
|
+
description: "When evidence has to be submitted. Null while no response is owed."
|
|
34844
|
+
},
|
|
34845
|
+
has_evidence: { type: "boolean" },
|
|
34846
|
+
past_due: { type: "boolean" },
|
|
34847
|
+
submission_count: { type: "integer" }
|
|
34848
|
+
},
|
|
34849
|
+
required: [
|
|
34850
|
+
"has_evidence",
|
|
34851
|
+
"past_due",
|
|
34852
|
+
"submission_count"
|
|
34853
|
+
]
|
|
34854
|
+
}
|
|
34855
|
+
},
|
|
34856
|
+
required: [
|
|
34857
|
+
"id",
|
|
34858
|
+
"object",
|
|
34859
|
+
"charge",
|
|
34860
|
+
"amount",
|
|
34861
|
+
"currency",
|
|
34862
|
+
"reason",
|
|
34863
|
+
"status",
|
|
34864
|
+
"is_charge_refundable",
|
|
34865
|
+
"created",
|
|
34866
|
+
"livemode",
|
|
34867
|
+
"evidence_details"
|
|
34868
|
+
]
|
|
34869
|
+
} } }
|
|
34870
|
+
} }
|
|
34871
|
+
} },
|
|
34872
|
+
"/v1/payment_intents": { post: {
|
|
34873
|
+
operationId: "createPaymentIntent",
|
|
34874
|
+
summary: "Create a payment intent",
|
|
34875
|
+
description: "Creates a payment intent — Stripe’s record of an intent to collect money, which a browser then confirms. `amount` is in the currency’s smallest unit, so 4900 with `eur` is 49.00 euros and a zero-decimal currency like `jpy` has no such factor at all. The intent comes back `requires_payment_method`: creating one moves no money and charges nobody, which is why this operation needs no confirmation step here.\n\n`client_secret` is left out of the response schema, and it is worth knowing exactly how little that does. The schema documents an operation; it never filters a response. `connect-action-executor.ts` parses the provider body and returns it whole, and nothing in the broker reads `responseSchema` at all — so Stripe sends the secret and the workflow receives it, declared or not. Logs are a different story: `@stackbone/redaction` masks any key ending in `secret`, so a logged body loses it. A run’s output is not scrubbed, so a workflow that returns this response persists a bearer credential for one payment. Take the fields you need.\n\nIf a workflow of yours also watches this account’s event feed, declare `echoPath: 'id'` on the call and open that workflow with `isOwnEcho('stripe', event.data.object.id)`: creating an intent raises `payment_intent.created` on the feed, and the `pi_…` id is the value both sides can compare. Without it a link scoped to a payment-intent family, pointed at a workflow that creates intents, starts itself again on every lap.",
|
|
34876
|
+
"x-stackbone-body-encoding": "form",
|
|
34877
|
+
parameters: [IDEMPOTENCY_KEY_PARAM],
|
|
34878
|
+
requestBody: {
|
|
34879
|
+
required: true,
|
|
34880
|
+
content: { "application/x-www-form-urlencoded": { schema: {
|
|
34881
|
+
type: "object",
|
|
34882
|
+
properties: {
|
|
34883
|
+
amount: {
|
|
34884
|
+
type: "integer",
|
|
34885
|
+
description: "How much to collect, in the smallest unit of `currency`. 4900 is 49.00 in a two-decimal currency."
|
|
34886
|
+
},
|
|
34887
|
+
currency: {
|
|
34888
|
+
type: "string",
|
|
34889
|
+
description: "Three-letter ISO code, lowercase, e.g. `eur`."
|
|
34890
|
+
},
|
|
34891
|
+
customer: {
|
|
34892
|
+
type: "string",
|
|
34893
|
+
description: "Optional customer id (`cus_…`) to attach the intent to."
|
|
34894
|
+
},
|
|
34895
|
+
description: {
|
|
34896
|
+
type: "string",
|
|
34897
|
+
description: "Free text shown on the payment in the Stripe dashboard."
|
|
34898
|
+
},
|
|
34899
|
+
metadata: {
|
|
34900
|
+
type: "object",
|
|
34901
|
+
additionalProperties: { type: "string" },
|
|
34902
|
+
description: "Arbitrary key/value pairs stored on the intent, sent as `metadata[key]=value`."
|
|
34903
|
+
}
|
|
34904
|
+
},
|
|
34905
|
+
required: ["amount", "currency"]
|
|
34906
|
+
} } }
|
|
34907
|
+
},
|
|
34908
|
+
responses: { "200": {
|
|
34909
|
+
description: "The payment intent, newly created.",
|
|
34910
|
+
content: { "application/json": { schema: {
|
|
34911
|
+
type: "object",
|
|
34912
|
+
properties: {
|
|
34913
|
+
id: { type: "string" },
|
|
34914
|
+
object: { type: "string" },
|
|
34915
|
+
amount: { type: "integer" },
|
|
34916
|
+
amount_received: { type: "integer" },
|
|
34917
|
+
currency: { type: "string" },
|
|
34918
|
+
customer: { type: ["string", "null"] },
|
|
34919
|
+
description: { type: ["string", "null"] },
|
|
34920
|
+
status: {
|
|
34921
|
+
type: "string",
|
|
34922
|
+
description: "A freshly created intent is `requires_payment_method` until something confirms it."
|
|
34923
|
+
},
|
|
34924
|
+
created: { type: "integer" },
|
|
34925
|
+
livemode: { type: "boolean" },
|
|
34926
|
+
metadata: {
|
|
34927
|
+
type: "object",
|
|
34928
|
+
additionalProperties: true
|
|
34929
|
+
}
|
|
34930
|
+
},
|
|
34931
|
+
required: [
|
|
34932
|
+
"id",
|
|
34933
|
+
"object",
|
|
34934
|
+
"created",
|
|
34935
|
+
"livemode",
|
|
34936
|
+
"status"
|
|
34937
|
+
]
|
|
34938
|
+
} } }
|
|
34939
|
+
} }
|
|
34940
|
+
} },
|
|
34941
|
+
"/v1/refunds": { post: {
|
|
34942
|
+
operationId: "createRefund",
|
|
34943
|
+
summary: "Refund a charge or a payment intent",
|
|
34944
|
+
description: "Gives money back. Name one of `charge` or `payment_intent`, never both — Stripe refuses the pair rather than picking one. Omit `amount` to refund the whole thing; supply it, in the smallest unit, for a partial refund. Two calls are two refunds: nothing about the arguments makes a repeat a no-op, and the second fails only once the charge has nothing left to give back. Send `Idempotency-Key` to make a retry safe — see the parameter. If a workflow of yours also watches this account's event feed, declare `echoPath: 'charge'` on the call and open that workflow with `isOwnEcho('stripe', event.data.object.id)`: a refund raises `charge.refunded` on the feed, and the charge id is the value both sides can compare.\n\n`reason` takes exactly `duplicate`, `fraudulent` or `requested_by_customer`. Any other string is refused, so it is an enum rather than free text.",
|
|
34945
|
+
"x-stackbone-body-encoding": "form",
|
|
34946
|
+
parameters: [IDEMPOTENCY_KEY_PARAM],
|
|
34947
|
+
requestBody: {
|
|
34948
|
+
required: true,
|
|
34949
|
+
content: { "application/x-www-form-urlencoded": { schema: {
|
|
34950
|
+
type: "object",
|
|
34951
|
+
properties: {
|
|
34952
|
+
charge: {
|
|
34953
|
+
type: "string",
|
|
34954
|
+
description: "The charge to refund (`ch_…`). Mutually exclusive with `payment_intent`."
|
|
34955
|
+
},
|
|
34956
|
+
payment_intent: {
|
|
34957
|
+
type: "string",
|
|
34958
|
+
description: "The payment intent to refund (`pi_…`). Mutually exclusive with `charge`."
|
|
34959
|
+
},
|
|
34960
|
+
amount: {
|
|
34961
|
+
type: "integer",
|
|
34962
|
+
description: "How much to give back, in the smallest unit. Omitted refunds the full amount."
|
|
34963
|
+
},
|
|
34964
|
+
reason: {
|
|
34965
|
+
type: "string",
|
|
34966
|
+
enum: [
|
|
34967
|
+
"duplicate",
|
|
34968
|
+
"fraudulent",
|
|
34969
|
+
"requested_by_customer"
|
|
34970
|
+
],
|
|
34971
|
+
description: "Why. Stripe accepts these three strings and no others."
|
|
34972
|
+
},
|
|
34973
|
+
metadata: {
|
|
34974
|
+
type: "object",
|
|
34975
|
+
additionalProperties: { type: "string" },
|
|
34976
|
+
description: "Key/value pairs stored on the refund, as `metadata[key]=value`."
|
|
34977
|
+
}
|
|
34978
|
+
},
|
|
34979
|
+
required: []
|
|
34980
|
+
} } }
|
|
34981
|
+
},
|
|
34982
|
+
responses: { "200": {
|
|
34983
|
+
description: "The refund.",
|
|
34984
|
+
content: { "application/json": { schema: {
|
|
34985
|
+
type: "object",
|
|
34986
|
+
properties: {
|
|
34987
|
+
id: { type: "string" },
|
|
34988
|
+
object: { type: "string" },
|
|
34989
|
+
amount: { type: "integer" },
|
|
34990
|
+
currency: { type: "string" },
|
|
34991
|
+
charge: { type: ["string", "null"] },
|
|
34992
|
+
payment_intent: { type: ["string", "null"] },
|
|
34993
|
+
reason: { type: ["string", "null"] },
|
|
34994
|
+
status: {
|
|
34995
|
+
type: "string",
|
|
34996
|
+
description: "One of `pending`, `succeeded`, `failed` or `canceled`. A card refund in test mode comes back `succeeded` at once."
|
|
34997
|
+
},
|
|
34998
|
+
created: { type: "integer" },
|
|
34999
|
+
metadata: {
|
|
35000
|
+
type: "object",
|
|
35001
|
+
additionalProperties: true
|
|
35002
|
+
}
|
|
35003
|
+
},
|
|
35004
|
+
required: [
|
|
35005
|
+
"id",
|
|
35006
|
+
"object",
|
|
35007
|
+
"created",
|
|
35008
|
+
"status"
|
|
35009
|
+
]
|
|
35010
|
+
} } }
|
|
35011
|
+
} }
|
|
35012
|
+
} }
|
|
35013
|
+
}
|
|
35014
|
+
}
|
|
35015
|
+
},
|
|
35016
|
+
triggers: [{
|
|
35017
|
+
id: "event-received",
|
|
35018
|
+
displayName: "Stripe event",
|
|
35019
|
+
description: "A Stripe event is the account’s own record of something that happened: a charge succeeded, an invoice was created. This link reads that feed and starts one run per event recorded after you turn it on. The event arrives whole — `type` names what happened, and `data.object` carries the thing it happened to.",
|
|
35020
|
+
docs: "Starts a run for each event Stripe records on the account, from the moment you turn the link on. The event arrives whole: `type` names what happened, and the object it happened to rides along with it. That object is a charge on a charge.succeeded event and an invoice on an invoice.created one, so a mapping that reads a charge field belongs on a link scoped to charge events.\n\nEvent type takes one name, or a family with a wildcard such as `charge.*`. Left blank, every type the account emits starts a run.\n\nTest mode and live mode are separate feeds. A test key sees only test events, and every event carries a `livemode` flag saying which feed it came from.\n\nCAUTION: a poll walks page after page until Stripe says there are no more, and stops at 10,000 events. Reaching that ceiling marks the tick cut off, so the loss is on the record rather than silent, but those events do not arrive on a later poll. Only a very busy account gets near it.\n\nCAUTION: Stripe keeps events for 30 days. A link switched off for longer resumes from a bookmark older than anything the feed still holds, and whatever expired in the meantime never arrives.\n\nCAUTION: this connector writes, and a workflow that creates a payment intent or issues a refund through this connection raises events on the very feed this link reads. A refund raises a charge.refunded event, so a link scoped to `charge.*` that also refunds starts itself again, and deduplication cannot stop it because every lap carries a new event id. There are two guards. The cheap one is the scope: keep the writing workflow off the types its own writes raise. That is why the event type field offers no charge or payment intent wildcard, though it still accepts one you type yourself. The other guard is `isOwnEcho` inside the workflow body, which compares a value the delivery carried against one your own write recorded. Declare an echo path on the write so it records the charge id, or the payment intent id, and compare it against the id the event object carries. Compare the resource id and never the event id, which is new on every lap and would match nothing. The `createRefund` and `createPaymentIntent` pages name the exact paths. A mapping cannot skip an event: every delivery starts a run.",
|
|
35021
|
+
sampleData: {
|
|
35022
|
+
id: "evt_1PqR2sC0mNqLxYzA3bK4dEf5",
|
|
35023
|
+
object: "event",
|
|
35024
|
+
api_version: "2026-08-26.dahlia",
|
|
35025
|
+
created: 1757836921,
|
|
35026
|
+
livemode: false,
|
|
35027
|
+
pending_webhooks: 0,
|
|
35028
|
+
request: {
|
|
35029
|
+
id: "req_9fT2kQwZ1aBcDe",
|
|
35030
|
+
idempotency_key: null
|
|
35031
|
+
},
|
|
35032
|
+
type: "charge.succeeded",
|
|
35033
|
+
data: { object: {
|
|
35034
|
+
id: "ch_3PqR2sC0mNqLxYzA0kL1mN2o",
|
|
35035
|
+
object: "charge",
|
|
35036
|
+
amount: 4900,
|
|
35037
|
+
amount_captured: 4900,
|
|
35038
|
+
amount_refunded: 0,
|
|
35039
|
+
currency: "eur",
|
|
35040
|
+
customer: "cus_QhT8kLmNoPqRs",
|
|
35041
|
+
description: "Stackbone Pro, monthly",
|
|
35042
|
+
paid: true,
|
|
35043
|
+
refunded: false,
|
|
35044
|
+
receipt_url: "https://pay.stripe.com/receipts/payment/example",
|
|
35045
|
+
payment_intent: "pi_3PqR2sC0mNqLxYzA1cD2eF3g",
|
|
35046
|
+
status: "succeeded",
|
|
35047
|
+
created: 1757836920,
|
|
35048
|
+
livemode: false,
|
|
35049
|
+
metadata: { workspace: "stackbone-demo" }
|
|
35050
|
+
} }
|
|
35051
|
+
},
|
|
35052
|
+
outputSchema: {
|
|
35053
|
+
type: "object",
|
|
35054
|
+
properties: {
|
|
35055
|
+
id: { type: "string" },
|
|
35056
|
+
object: { type: "string" },
|
|
35057
|
+
api_version: { type: ["string", "null"] },
|
|
35058
|
+
created: {
|
|
35059
|
+
type: "integer",
|
|
35060
|
+
description: "When Stripe recorded the event, in seconds from the epoch."
|
|
35061
|
+
},
|
|
35062
|
+
livemode: {
|
|
35063
|
+
type: "boolean",
|
|
35064
|
+
description: "False for an event from test mode, true for one from live mode."
|
|
35065
|
+
},
|
|
35066
|
+
pending_webhooks: { type: "integer" },
|
|
35067
|
+
type: { type: "string" },
|
|
35068
|
+
request: {
|
|
35069
|
+
type: ["object", "null"],
|
|
35070
|
+
properties: {
|
|
35071
|
+
id: { type: ["string", "null"] },
|
|
35072
|
+
idempotency_key: { type: ["string", "null"] }
|
|
35073
|
+
}
|
|
35074
|
+
},
|
|
35075
|
+
data: {
|
|
35076
|
+
type: "object",
|
|
35077
|
+
properties: {
|
|
35078
|
+
object: {
|
|
35079
|
+
type: "object",
|
|
35080
|
+
additionalProperties: true
|
|
35081
|
+
},
|
|
35082
|
+
previous_attributes: {
|
|
35083
|
+
type: "object",
|
|
35084
|
+
additionalProperties: true
|
|
35085
|
+
}
|
|
35086
|
+
},
|
|
35087
|
+
required: ["object"]
|
|
35088
|
+
}
|
|
35089
|
+
},
|
|
35090
|
+
required: [
|
|
35091
|
+
"id",
|
|
35092
|
+
"object",
|
|
35093
|
+
"created",
|
|
35094
|
+
"livemode",
|
|
35095
|
+
"pending_webhooks",
|
|
35096
|
+
"type",
|
|
35097
|
+
"data"
|
|
35098
|
+
]
|
|
35099
|
+
},
|
|
35100
|
+
poll: {
|
|
35101
|
+
kind: "declarative",
|
|
35102
|
+
list: {
|
|
35103
|
+
operationId: "listEvents",
|
|
35104
|
+
args: { limit: 100 },
|
|
35105
|
+
itemsPath: "data",
|
|
35106
|
+
order: "newest-first",
|
|
35107
|
+
pageLastId: {
|
|
35108
|
+
param: "starting_after",
|
|
35109
|
+
itemPath: "id",
|
|
35110
|
+
hasMorePath: "has_more"
|
|
35111
|
+
},
|
|
35112
|
+
scopeParams: [{
|
|
35113
|
+
param: "type",
|
|
35114
|
+
displayName: "Event type",
|
|
35115
|
+
description: "One event name, or a family with a wildcard: `charge.*`. Blank delivers every type the account emits.",
|
|
35116
|
+
example: "charge.succeeded",
|
|
35117
|
+
required: false,
|
|
35118
|
+
pattern: "^[a-z0-9_.*]+$",
|
|
35119
|
+
suggestions: [
|
|
35120
|
+
"charge.succeeded",
|
|
35121
|
+
"charge.failed",
|
|
35122
|
+
"charge.refunded",
|
|
35123
|
+
"charge.dispute.created",
|
|
35124
|
+
"payment_intent.succeeded",
|
|
35125
|
+
"payment_intent.payment_failed",
|
|
35126
|
+
"payment_intent.canceled",
|
|
35127
|
+
"invoice.paid",
|
|
35128
|
+
"invoice.payment_failed",
|
|
35129
|
+
"invoice.*",
|
|
35130
|
+
"customer.subscription.created",
|
|
35131
|
+
"customer.subscription.updated",
|
|
35132
|
+
"customer.subscription.deleted",
|
|
35133
|
+
"customer.subscription.*",
|
|
35134
|
+
"checkout.session.completed",
|
|
35135
|
+
"payout.paid",
|
|
35136
|
+
"payout.failed"
|
|
35137
|
+
]
|
|
35138
|
+
}]
|
|
35139
|
+
},
|
|
35140
|
+
cursor: {
|
|
35141
|
+
kind: "timestamp",
|
|
35142
|
+
field: "created",
|
|
35143
|
+
param: "created[gte]",
|
|
35144
|
+
unit: "unix-seconds"
|
|
35145
|
+
},
|
|
35146
|
+
dedup: { idPath: "id" }
|
|
35147
|
+
}
|
|
35148
|
+
}]
|
|
35149
|
+
};
|
|
35150
|
+
}));
|
|
35151
|
+
//#endregion
|
|
33908
35152
|
//#region ../../libs/shared/connect-registry/src/lib/catalog/supabase/supabase.connector.ts
|
|
33909
35153
|
var SUPABASE_CONNECTOR;
|
|
33910
35154
|
var init_supabase_connector = __esmMin((() => {
|
|
@@ -33985,8 +35229,8 @@ var init_supabase_connector = __esmMin((() => {
|
|
|
33985
35229
|
triggers: [{
|
|
33986
35230
|
id: "row-inserted",
|
|
33987
35231
|
displayName: "Row inserted or changed",
|
|
33988
|
-
description: "Starts a run for each row that appears past the cursor column, delivered as { table, row }. THE CURSOR COLUMN DECIDES WHAT \"APPEARS\" MEANS. Left at the default `id`, or set to any column written once and never touched again such as `created_at`, this is insert-only: one run per new row, and an edit to an existing row is never delivered. Set to a column your database bumps on every write, such as `updated_at`, this is change detection: EVERY UPDATE TO A ROW STARTS A NEW RUN. CAUTION: on a moving column a
|
|
33989
|
-
docs: "Starts a run for each row that appears past the cursor column, delivered as `{ table, row }`.\n\nThe cursor column decides what \"appears\" means, and it is the only choice that matters here. Left at the default `id`, or set to a column written once and never touched again such as `created_at`, this is insert-only: one run per new row, and an edit to an existing row is never delivered. Set to a column your database bumps on every write, such as `updated_at`, it becomes change detection, and every update to a row starts a new run.\n\nThe table must expose a unique, non-null `id` column for delivery identity. Rows whose cursor value is NULL are ignored.\n\nRows are read in ascending cursor order. A row that becomes visible after the cursor advances, with a value equal to or lower than the highest already seen, is not delivered. Choose values that increase as changes become visible; this also applies to timestamp and date columns.\n\nCAUTION: on a moving column, a workflow that
|
|
35232
|
+
description: "Starts a run for each row that appears past the cursor column, delivered as { table, row }. THE CURSOR COLUMN DECIDES WHAT \"APPEARS\" MEANS. Left at the default `id`, or set to any column written once and never touched again such as `created_at`, this is insert-only: one run per new row, and an edit to an existing row is never delivered. Set to a column your database bumps on every write, such as `updated_at`, this is change detection: EVERY UPDATE TO A ROW STARTS A NEW RUN. CAUTION: a workflow that writes to this table can re-trigger itself, and the delivery journal cannot stop it: on a moving column a write-back bumps that column, and on the default column a workflow that answers each row by inserting another mints a new id. Guard INSIDE THE WORKFLOW BODY with `isOwnEcho`, and MATCH THE VALUE TO THE CURSOR COLUMN. Insert-only, compare the new row's id: `echoPath: '0.id'` on the write against `event.row.id`. Change detection, compare the cursor column itself: `echoPath: '0.updated_at'` against `event.row.updated_at`, with your own column name in place of `updated_at`. COMPARING THE ID ON A MOVING COLUMN IS WRONG — a row's id never changes, so the guard would read every later delivery of that row as your own echo for the whole window and swallow a human's edit with it. The write must also ask for `Prefer: return=representation`: without it PostgREST answers no body, the echo path resolves to nothing, and the guard throws instead of guarding. The table must expose a unique, non-null `id` column for delivery identity. Rows whose cursor value is NULL are ignored. Rows are read in ascending cursor order: a row that becomes visible after the cursor advances, with a value EQUAL TO OR LOWER THAN the highest seen, is not delivered. Choose values that increase as changes become visible; this also applies to timestamp and date columns. A trigger mapping cannot skip an item; every delivered row starts a run.",
|
|
35233
|
+
docs: "Starts a run for each row that appears past the cursor column, delivered as `{ table, row }`.\n\nThe cursor column decides what \"appears\" means, and it is the only choice that matters here. Left at the default `id`, or set to a column written once and never touched again such as `created_at`, this is insert-only: one run per new row, and an edit to an existing row is never delivered. Set to a column your database bumps on every write, such as `updated_at`, it becomes change detection, and every update to a row starts a new run.\n\nThe table must expose a unique, non-null `id` column for delivery identity. Rows whose cursor value is NULL are ignored.\n\nRows are read in ascending cursor order. A row that becomes visible after the cursor advances, with a value equal to or lower than the highest already seen, is not delivered. Choose values that increase as changes become visible; this also applies to timestamp and date columns.\n\nCAUTION: a workflow that writes to this table starts itself again, and nothing on the platform stops that loop: on a moving column a write-back bumps that column, and on the default column a workflow that answers each row by inserting another row mints a new id. Guard inside the workflow body with `isOwnEcho`, and match the value you compare to the cursor column. Insert-only: compare the `id` the delivery carried, with `echoPath: '0.id'` on the write. Change detection: compare the cursor column the delivery carried, with `echoPath: '0.updated_at'` on the write, putting your own column name in place of `updated_at`. Comparing the id on a moving column is wrong, because a row's id never changes, so the guard would read every later delivery of that row as your own echo for the whole window and swallow a human's edit with it. The write also has to ask for `Prefer: return=representation`: without it PostgREST answers no body, the echo path resolves to nothing, and the guard throws instead of guarding. A mapping cannot skip an event: every delivered row starts a run.",
|
|
33990
35234
|
sampleData: {
|
|
33991
35235
|
table: "todos",
|
|
33992
35236
|
row: {
|
|
@@ -34007,6 +35251,7 @@ var init_supabase_connector = __esmMin((() => {
|
|
|
34007
35251
|
kind: "declarative",
|
|
34008
35252
|
list: {
|
|
34009
35253
|
operationId: "GET /{table}",
|
|
35254
|
+
order: "oldest-first",
|
|
34010
35255
|
args: {
|
|
34011
35256
|
order: "{cursorColumn}.asc",
|
|
34012
35257
|
and: "({cursorColumn}.not.is.null)"
|
|
@@ -34120,6 +35365,7 @@ var init_built_in_catalog = __esmMin((() => {
|
|
|
34120
35365
|
init_linear_connector();
|
|
34121
35366
|
init_outlook_connector();
|
|
34122
35367
|
init_outlook_app_only_connector();
|
|
35368
|
+
init_stripe_connector();
|
|
34123
35369
|
init_supabase_connector();
|
|
34124
35370
|
init_telegram_connector();
|
|
34125
35371
|
init_whatsapp_connector();
|
|
@@ -34130,6 +35376,7 @@ var init_built_in_catalog = __esmMin((() => {
|
|
|
34130
35376
|
LINEAR_CONNECTOR,
|
|
34131
35377
|
OUTLOOK_CONNECTOR,
|
|
34132
35378
|
OUTLOOK_APP_ONLY_CONNECTOR,
|
|
35379
|
+
STRIPE_CONNECTOR,
|
|
34133
35380
|
SUPABASE_CONNECTOR,
|
|
34134
35381
|
TELEGRAM_CONNECTOR,
|
|
34135
35382
|
WHATSAPP_CONNECTOR
|
|
@@ -34536,6 +35783,7 @@ function buildOperation(rawPath, resolvedPath, method, operation, pathLevelParam
|
|
|
34536
35783
|
const rawResponse = declaredRawResponse === true || declaredRawResponse === "when-requested" ? declaredRawResponse : void 0;
|
|
34537
35784
|
const declaredVersionPath = operation["x-stackbone-version-path"];
|
|
34538
35785
|
const versionPath = typeof declaredVersionPath === "string" && declaredVersionPath !== "" ? declaredVersionPath : void 0;
|
|
35786
|
+
const bodyEncoding = operation["x-stackbone-body-encoding"] === "form" ? "form" : void 0;
|
|
34539
35787
|
const continuation = readContinuation(operation);
|
|
34540
35788
|
return {
|
|
34541
35789
|
operationId,
|
|
@@ -34547,6 +35795,7 @@ function buildOperation(rawPath, resolvedPath, method, operation, pathLevelParam
|
|
|
34547
35795
|
...parameters.length > 0 ? { parameters } : {},
|
|
34548
35796
|
...requestBodySchema !== void 0 ? { requestBodySchema } : {},
|
|
34549
35797
|
...bodyArgName !== void 0 ? { bodyArgName } : {},
|
|
35798
|
+
...bodyEncoding !== void 0 ? { bodyEncoding } : {},
|
|
34550
35799
|
...responseSchema !== void 0 ? { responseSchema } : {},
|
|
34551
35800
|
...rawBody ? { rawBody } : {},
|
|
34552
35801
|
...rawResponse !== void 0 ? { rawResponse } : {},
|
|
@@ -34780,8 +36029,15 @@ async function mapWithConcurrency(items, limit, fn) {
|
|
|
34780
36029
|
var init_concurrency = __esmMin((() => {}));
|
|
34781
36030
|
//#endregion
|
|
34782
36031
|
//#region ../../libs/shared/connect-registry/src/lib/triggers/stamps.ts
|
|
34783
|
-
/**
|
|
34784
|
-
|
|
36032
|
+
/**
|
|
36033
|
+
* The instant a stamp names, or `null` for anything that is not a stamp.
|
|
36034
|
+
*
|
|
36035
|
+
* `unit` is the cursor's declared unit, passed by every caller that has one.
|
|
36036
|
+
* Omitted — the case for every trigger written before units existed — the two
|
|
36037
|
+
* ISO shapes above are the only ones read, exactly as before.
|
|
36038
|
+
*/
|
|
36039
|
+
function parseStamp(value, unit) {
|
|
36040
|
+
if (unit === "unix-seconds") return parseUnixSeconds(value);
|
|
34785
36041
|
if (typeof value !== "string") return null;
|
|
34786
36042
|
const zoned = EXPLICIT_OFFSET.test(value);
|
|
34787
36043
|
const zoneless = !zoned && ZONELESS_STAMP.test(value);
|
|
@@ -34797,17 +36053,36 @@ function parseStamp(value) {
|
|
|
34797
36053
|
}
|
|
34798
36054
|
};
|
|
34799
36055
|
}
|
|
36056
|
+
/** A stamp counted in seconds from the epoch, as a number or as its digits. */
|
|
36057
|
+
function parseUnixSeconds(value) {
|
|
36058
|
+
const numeric = typeof value === "number";
|
|
36059
|
+
if (!numeric && !(typeof value === "string" && UNIX_SECONDS.test(value))) return null;
|
|
36060
|
+
const seconds = numeric ? value : Number(value);
|
|
36061
|
+
if (!Number.isSafeInteger(seconds) || Math.abs(seconds) >= 0xe8d4a51000) return null;
|
|
36062
|
+
return {
|
|
36063
|
+
epochMs: seconds * 1e3,
|
|
36064
|
+
shape: {
|
|
36065
|
+
unit: "unix-seconds",
|
|
36066
|
+
numeric
|
|
36067
|
+
}
|
|
36068
|
+
};
|
|
36069
|
+
}
|
|
34800
36070
|
/** Write an instant in the shape {@link parseStamp} read. */
|
|
34801
36071
|
function formatStamp(epochMs, shape) {
|
|
36072
|
+
if (shape.unit === "unix-seconds") {
|
|
36073
|
+
const seconds = Math.floor(epochMs / 1e3);
|
|
36074
|
+
return shape.numeric ? seconds : String(seconds);
|
|
36075
|
+
}
|
|
34802
36076
|
const iso = new Date(epochMs).toISOString();
|
|
34803
36077
|
const withFraction = shape.fractional ? iso : iso.replace(/\.\d{3}Z$/, "Z");
|
|
34804
36078
|
if (shape.zoned) return withFraction;
|
|
34805
36079
|
return withFraction.slice(0, -1).replace("T", shape.separator);
|
|
34806
36080
|
}
|
|
34807
|
-
var EXPLICIT_OFFSET, ZONELESS_STAMP;
|
|
36081
|
+
var EXPLICIT_OFFSET, ZONELESS_STAMP, UNIX_SECONDS;
|
|
34808
36082
|
var init_stamps = __esmMin((() => {
|
|
34809
36083
|
EXPLICIT_OFFSET = /(?:Z|[+-]\d{2}:?\d{2})$/i;
|
|
34810
36084
|
ZONELESS_STAMP = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?$/;
|
|
36085
|
+
UNIX_SECONDS = /^\d{1,12}$/;
|
|
34811
36086
|
}));
|
|
34812
36087
|
//#endregion
|
|
34813
36088
|
//#region ../../libs/shared/connect-registry/src/lib/triggers/list-args.ts
|
|
@@ -34845,17 +36120,30 @@ function scopeValue(param, scope) {
|
|
|
34845
36120
|
* same filter the stored one did and has to parse the way that one parsed. An
|
|
34846
36121
|
* offset other than `Z` comes back as `Z`; it names the same instant.
|
|
34847
36122
|
*
|
|
34848
|
-
* @throws on a stored cursor
|
|
34849
|
-
*
|
|
34850
|
-
*
|
|
34851
|
-
*
|
|
34852
|
-
*
|
|
34853
|
-
|
|
34854
|
-
|
|
36123
|
+
* @throws on a stored cursor the reader cannot take, whenever the definition
|
|
36124
|
+
* claimed it could be read: a declared `unit`, a declared `lookbackMs`, or both
|
|
36125
|
+
* (PLA-1506). Refusing to PARSE a value is not the same as refusing to SEND it,
|
|
36126
|
+
* and sending it is the worse of the two. A `unix-seconds` cursor holding a
|
|
36127
|
+
* 13-digit millisecond value goes out verbatim, matches nothing, never advances,
|
|
36128
|
+
* and every tick after it reports `polled` on a green row — the definition's
|
|
36129
|
+
* promise unkept with nothing anywhere to say so. The failed tick puts the value
|
|
36130
|
+
* on the row where an operator sees it.
|
|
36131
|
+
*
|
|
36132
|
+
* A cursor that declares NEITHER is passed through unread, and has to be: an
|
|
36133
|
+
* opaque provider token, a Supabase integer id, the `'12345'` that
|
|
36134
|
+
* `declarative-poll.spec.ts` pins as passing through untouched. Reading is owed
|
|
36135
|
+
* only where the definition said the value is readable.
|
|
36136
|
+
*/
|
|
36137
|
+
function cursorForRequest(cursor, stored) {
|
|
34855
36138
|
if (stored === null || stored === void 0) return stored;
|
|
34856
|
-
if (cursor.kind !== "timestamp"
|
|
34857
|
-
|
|
34858
|
-
|
|
36139
|
+
if (cursor.kind !== "timestamp") return stored;
|
|
36140
|
+
if (cursor.unit === void 0 && cursor.lookbackMs === void 0) return stored;
|
|
36141
|
+
const stamp = parseStamp(stored, cursor.unit);
|
|
36142
|
+
if (stamp === null) {
|
|
36143
|
+
const value = JSON.stringify(stored);
|
|
36144
|
+
throw new Error(cursor.lookbackMs !== void 0 ? `trigger declares a cursor lookback of ${cursor.lookbackMs}ms but the stored cursor ${value} cannot be read as an instant` + (cursor.unit === void 0 ? ", and this cursor declares no unit, so only an ISO date stamp can be moved back" : ` in the declared unit ${cursor.unit}`) + " — there is nothing to subtract the margin from" : `trigger declares a cursor unit of ${cursor.unit} but the stored cursor ${value} cannot be read in it — sending it on would filter for a moment that is not the one it names, match nothing, and leave the marker where it is on a green row`);
|
|
36145
|
+
}
|
|
36146
|
+
if (cursor.lookbackMs === void 0) return stored;
|
|
34859
36147
|
return formatStamp(stamp.epochMs - cursor.lookbackMs, stamp.shape);
|
|
34860
36148
|
}
|
|
34861
36149
|
/**
|
|
@@ -34866,11 +36154,13 @@ function withLookback(cursor, stored) {
|
|
|
34866
36154
|
* failed tick.
|
|
34867
36155
|
* @throws when a scope clause and the cursor clause target one key and the
|
|
34868
36156
|
* cursor declares no `composeWith` to join them with.
|
|
36157
|
+
* @throws when the stored cursor cannot be read in the unit or shape the
|
|
36158
|
+
* definition declared — see {@link cursorForRequest}.
|
|
34869
36159
|
*/
|
|
34870
36160
|
function buildListArgs(spec, input) {
|
|
34871
36161
|
const args = { ...spec.list.args ?? {} };
|
|
34872
36162
|
const cursorParam = spec.cursor.kind === "timestamp" ? spec.cursor.param : spec.cursor.requestParam;
|
|
34873
|
-
const cursor =
|
|
36163
|
+
const cursor = cursorForRequest(spec.cursor, input.cursor);
|
|
34874
36164
|
const cursorValue = cursor === null || cursor === void 0 ? void 0 : spec.cursor.kind === "timestamp" && spec.cursor.cursorTemplate ? fill(spec.cursor.cursorTemplate, "{cursor}", String(cursor)) : cursor;
|
|
34875
36165
|
let collidingClause = void 0;
|
|
34876
36166
|
for (const param of spec.list.scopeParams ?? []) {
|
|
@@ -34895,7 +36185,8 @@ function buildListArgs(spec, input) {
|
|
|
34895
36185
|
} else if (spec.list.pageNumber) {
|
|
34896
36186
|
setPath(args, spec.list.pageNumber.pageSizeParam, spec.list.pageNumber.size);
|
|
34897
36187
|
setPath(args, spec.list.pageNumber.param, page.index + 1);
|
|
34898
|
-
} else if (spec.list.
|
|
36188
|
+
} else if (spec.list.pageLastId && page.lastId !== null && page.lastId !== void 0) setPath(args, spec.list.pageLastId.param, page.lastId);
|
|
36189
|
+
else if (spec.list.pageToken && page.token !== null && page.token !== void 0) setPath(args, spec.list.pageToken.requestParam, page.token);
|
|
34899
36190
|
return args;
|
|
34900
36191
|
}
|
|
34901
36192
|
var init_list_args = __esmMin((() => {
|
|
@@ -35001,10 +36292,22 @@ function firstErrorMessage(errors) {
|
|
|
35001
36292
|
return String(first);
|
|
35002
36293
|
}
|
|
35003
36294
|
}
|
|
35004
|
-
|
|
36295
|
+
/**
|
|
36296
|
+
* The later of two markers. With a `unit` declared both sides are read as
|
|
36297
|
+
* INSTANTS, because the lexical fallback below gets a declared unit wrong every
|
|
36298
|
+
* time: `'1757000000'` sorts under `'2026-…'`, so a unix marker would lose to
|
|
36299
|
+
* the ISO value the seed used to mint and the cursor would never move — the
|
|
36300
|
+
* link green, the poll asking for the same window forever.
|
|
36301
|
+
*/
|
|
36302
|
+
function laterOf(a, b, unit) {
|
|
35005
36303
|
const other = typeof b === "string" || typeof b === "number" ? b : void 0;
|
|
35006
36304
|
if (a === void 0) return other;
|
|
35007
36305
|
if (other === void 0) return a;
|
|
36306
|
+
if (unit !== void 0) {
|
|
36307
|
+
const left = parseStamp(a, unit);
|
|
36308
|
+
const right = parseStamp(other, unit);
|
|
36309
|
+
if (left !== null && right !== null) return left.epochMs > right.epochMs ? a : other;
|
|
36310
|
+
}
|
|
35008
36311
|
if (typeof a === "number" && typeof other === "number") return a > other ? a : other;
|
|
35009
36312
|
return String(a) > String(other) ? a : other;
|
|
35010
36313
|
}
|
|
@@ -35031,14 +36334,15 @@ function laterOf(a, b) {
|
|
|
35031
36334
|
* A provider clock running ahead therefore makes the marker trail by the skew
|
|
35032
36335
|
* and re-list once — which dedup absorbs — rather than stall.
|
|
35033
36336
|
*
|
|
35034
|
-
* Only
|
|
35035
|
-
*
|
|
35036
|
-
* sequence) and is left alone, and
|
|
35037
|
-
*
|
|
35038
|
-
*
|
|
36337
|
+
* Only values `parseStamp` reads are judged: the two date shapes, plus a number
|
|
36338
|
+
* when the cursor DECLARES its unit. An undeclared numeric cursor is ambiguous
|
|
36339
|
+
* (seconds or milliseconds, or a provider's own sequence) and is left alone, and
|
|
36340
|
+
* so is anything that is not a date shape — which is what keeps
|
|
36341
|
+
* `Date.parse('12345')`, the year 12345, from barring a numeric-looking id. A
|
|
36342
|
+
* zoneless stamp is read as UTC; `stamps.ts` says why.
|
|
35039
36343
|
*/
|
|
35040
|
-
function isFuture(value) {
|
|
35041
|
-
const stamp = parseStamp(value);
|
|
36344
|
+
function isFuture(value, unit) {
|
|
36345
|
+
const stamp = parseStamp(value, unit);
|
|
35042
36346
|
return stamp !== null && stamp.epochMs > Date.now();
|
|
35043
36347
|
}
|
|
35044
36348
|
/** The greatest value across items AND across every declared field (ISO strings
|
|
@@ -35046,14 +36350,15 @@ function isFuture(value) {
|
|
|
35046
36350
|
* yet. `undefined` when no item carries a usable value — an item barred from
|
|
35047
36351
|
* moving the marker is still delivered; it is re-listed until its date arrives,
|
|
35048
36352
|
* and dedup absorbs that. */
|
|
35049
|
-
function latestTimestamp(items, field) {
|
|
36353
|
+
function latestTimestamp(items, field, unit) {
|
|
35050
36354
|
const fields = typeof field === "string" ? [field] : field;
|
|
35051
36355
|
let latest;
|
|
35052
36356
|
for (const item of items) for (const path of fields) {
|
|
35053
36357
|
const value = getPath(item, path);
|
|
35054
36358
|
if (typeof value !== "string" && typeof value !== "number") continue;
|
|
35055
|
-
if (
|
|
35056
|
-
|
|
36359
|
+
if (unit !== void 0 && parseStamp(value, unit) === null) continue;
|
|
36360
|
+
if (isFuture(value, unit)) continue;
|
|
36361
|
+
latest = laterOf(value, latest, unit);
|
|
35057
36362
|
}
|
|
35058
36363
|
return latest;
|
|
35059
36364
|
}
|
|
@@ -35067,6 +36372,7 @@ function createDeclarativePoll(spec) {
|
|
|
35067
36372
|
const rawItems = [];
|
|
35068
36373
|
let opaqueNext;
|
|
35069
36374
|
let pageToken;
|
|
36375
|
+
let pageLastId;
|
|
35070
36376
|
let drained = false;
|
|
35071
36377
|
let pagesWalked = 0;
|
|
35072
36378
|
for (let page = 0; page < 100; page += 1) {
|
|
@@ -35076,7 +36382,8 @@ function createDeclarativePoll(spec) {
|
|
|
35076
36382
|
cursor,
|
|
35077
36383
|
page: {
|
|
35078
36384
|
index: page,
|
|
35079
|
-
token: pageToken
|
|
36385
|
+
token: pageToken,
|
|
36386
|
+
lastId: pageLastId
|
|
35080
36387
|
}
|
|
35081
36388
|
});
|
|
35082
36389
|
const response = await ctx.runOperation(spec.list.operationId, args);
|
|
@@ -35098,9 +36405,20 @@ function createDeclarativePoll(spec) {
|
|
|
35098
36405
|
drained = true;
|
|
35099
36406
|
break;
|
|
35100
36407
|
}
|
|
36408
|
+
} else if (spec.list.pageLastId) {
|
|
36409
|
+
const hasMore = getPath(response, spec.list.pageLastId.hasMorePath);
|
|
36410
|
+
if (hasMore === false) {
|
|
36411
|
+
drained = true;
|
|
36412
|
+
break;
|
|
36413
|
+
}
|
|
36414
|
+
if (hasMore !== true) break;
|
|
36415
|
+
const last = Array.isArray(items) && items.length > 0 ? items[items.length - 1] : void 0;
|
|
36416
|
+
const next = last === void 0 ? void 0 : getPath(last, spec.list.pageLastId.itemPath);
|
|
36417
|
+
if (next === null || next === void 0) break;
|
|
36418
|
+
pageLastId = next;
|
|
35101
36419
|
} else {
|
|
35102
36420
|
pageToken = spec.list.pageToken ? getPath(response, spec.list.pageToken.responsePath) : void 0;
|
|
35103
|
-
const hasMorePath = spec.list.pageToken?.hasMorePath;
|
|
36421
|
+
const hasMorePath = spec.list.pageToken?.hasMorePath ?? spec.list.hasMorePath;
|
|
35104
36422
|
const hasMore = hasMorePath === void 0 ? void 0 : getPath(response, hasMorePath);
|
|
35105
36423
|
if (hasMore === false) {
|
|
35106
36424
|
drained = true;
|
|
@@ -35108,6 +36426,7 @@ function createDeclarativePoll(spec) {
|
|
|
35108
36426
|
}
|
|
35109
36427
|
if (hasMorePath !== void 0 && hasMore !== true) break;
|
|
35110
36428
|
if (pageToken === null || pageToken === void 0) {
|
|
36429
|
+
if (spec.list.pageToken === void 0 && hasMorePath !== void 0) break;
|
|
35111
36430
|
drained = true;
|
|
35112
36431
|
break;
|
|
35113
36432
|
}
|
|
@@ -35126,7 +36445,7 @@ function createDeclarativePoll(spec) {
|
|
|
35126
36445
|
id: dedupId(item, spec.dedup),
|
|
35127
36446
|
payload: withScopeFields(spec.map ? mapFields(spec.map, item) : item, spec.list.scopeParams, ctx.scope)
|
|
35128
36447
|
}));
|
|
35129
|
-
const nextCursor = spec.cursor.kind === "opaque" ? opaqueNext ?? cursor ?? null : laterOf(latestTimestamp(items, spec.cursor.field), cursor) ?? null;
|
|
36448
|
+
const nextCursor = spec.cursor.kind === "opaque" ? opaqueNext ?? cursor ?? null : laterOf(latestTimestamp(items, spec.cursor.field, spec.cursor.unit), cursor, spec.cursor.unit) ?? null;
|
|
35130
36449
|
if (!drained) return {
|
|
35131
36450
|
items: events,
|
|
35132
36451
|
nextCursor,
|
|
@@ -35294,6 +36613,7 @@ var init_src$4 = __esmMin((() => {
|
|
|
35294
36613
|
init_trigger_descriptor();
|
|
35295
36614
|
init_declarative_poll();
|
|
35296
36615
|
init_list_args();
|
|
36616
|
+
init_stamps();
|
|
35297
36617
|
init_paths();
|
|
35298
36618
|
init_concurrency();
|
|
35299
36619
|
init_built_in_strategies();
|
|
@@ -36286,6 +37606,70 @@ var init_body_error = __esmMin((() => {
|
|
|
36286
37606
|
init_src$20();
|
|
36287
37607
|
}));
|
|
36288
37608
|
//#endregion
|
|
37609
|
+
//#region ../../libs/runtime/connect-broker/src/lib/executor/form-encoding.ts
|
|
37610
|
+
/**
|
|
37611
|
+
* Encode an object as a form body. Returns the `&`-joined pairs, with no leading
|
|
37612
|
+
* `?` and no trailing separator; an object with nothing to send returns the
|
|
37613
|
+
* empty string.
|
|
37614
|
+
*
|
|
37615
|
+
* The value rules, each of which a provider can tell apart:
|
|
37616
|
+
*
|
|
37617
|
+
* - `null` and `undefined` are OMITTED, key and all. A form body has no null:
|
|
37618
|
+
* the wire can only carry text, so `x=null` would set the field to the
|
|
37619
|
+
* four-letter string. Omitting is the one encoding that means "not sent".
|
|
37620
|
+
* - the empty string is NOT omitted — `x=` is how a form body clears a field,
|
|
37621
|
+
* and a caller that wrote `''` asked for exactly that.
|
|
37622
|
+
* - an empty object or an empty array contributes NO pairs, because it has no
|
|
37623
|
+
* leaf to hang a key off. Same reading as `qs` and as Stripe's own client.
|
|
37624
|
+
* - booleans go out as `true` / `false`, numbers as their digits.
|
|
37625
|
+
*
|
|
37626
|
+
* @param body the arguments to send, already stripped of anything the broker
|
|
37627
|
+
* consumes (see `buildRequestBody`).
|
|
37628
|
+
*/
|
|
37629
|
+
function encodeFormBody(body) {
|
|
37630
|
+
const pairs = [];
|
|
37631
|
+
for (const [key, value] of Object.entries(body)) appendPairs(encodeURIComponent(key), value, pairs);
|
|
37632
|
+
return pairs.join("&");
|
|
37633
|
+
}
|
|
37634
|
+
/**
|
|
37635
|
+
* Walk one value into `pairs`, carrying the key prefix built so far. The prefix
|
|
37636
|
+
* arrives ALREADY ENCODED, which is why the brackets this function adds survive
|
|
37637
|
+
* literally: `encodeURIComponent` would turn `[` into `%5B`, and while every
|
|
37638
|
+
* provider decodes that back, a percent-escaped key is unreadable in a log and
|
|
37639
|
+
* in a spec assertion for no gain.
|
|
37640
|
+
*/
|
|
37641
|
+
function appendPairs(key, value, pairs) {
|
|
37642
|
+
if (value === null || value === void 0) return;
|
|
37643
|
+
if (Array.isArray(value)) {
|
|
37644
|
+
value.forEach((item, index) => appendPairs(`${key}[${index}]`, item, pairs));
|
|
37645
|
+
return;
|
|
37646
|
+
}
|
|
37647
|
+
if (isNestable(value)) {
|
|
37648
|
+
for (const [childKey, childValue] of Object.entries(value)) appendPairs(`${key}[${encodeURIComponent(childKey)}]`, childValue, pairs);
|
|
37649
|
+
return;
|
|
37650
|
+
}
|
|
37651
|
+
pairs.push(`${key}=${encodeURIComponent(String(value))}`);
|
|
37652
|
+
}
|
|
37653
|
+
/**
|
|
37654
|
+
* Is this value a structure to walk into, or a leaf to stringify?
|
|
37655
|
+
*
|
|
37656
|
+
* Only a PLAIN object counts. A `Date`, a `Map`, a class instance — anything
|
|
37657
|
+
* with its own prototype — has no enumerable own entries worth speaking of, so
|
|
37658
|
+
* walking it would emit NOTHING and the field would vanish from the request with
|
|
37659
|
+
* no error anywhere. Stringifying it instead sends `[object Map]`, which the
|
|
37660
|
+
* provider rejects with a 400 naming the field. A loud rejection beats a field
|
|
37661
|
+
* that silently never arrived.
|
|
37662
|
+
*
|
|
37663
|
+
* In practice every body reaching here was parsed from JSON and is plain, so
|
|
37664
|
+
* this branch is a guard against the day something else does.
|
|
37665
|
+
*/
|
|
37666
|
+
function isNestable(value) {
|
|
37667
|
+
if (typeof value !== "object" || value === null) return false;
|
|
37668
|
+
const proto = Object.getPrototypeOf(value);
|
|
37669
|
+
return proto === Object.prototype || proto === null;
|
|
37670
|
+
}
|
|
37671
|
+
var init_form_encoding = __esmMin((() => {}));
|
|
37672
|
+
//#endregion
|
|
36289
37673
|
//#region ../../libs/runtime/connect-broker/src/lib/executor/provider-error-classes.ts
|
|
36290
37674
|
/**
|
|
36291
37675
|
* Classify one failed provider response, or answer `null` for anything the
|
|
@@ -36711,25 +38095,50 @@ function brokerCodeForProviderStatus(status) {
|
|
|
36711
38095
|
}
|
|
36712
38096
|
}
|
|
36713
38097
|
/**
|
|
36714
|
-
* Build the
|
|
36715
|
-
*
|
|
38098
|
+
* Build the request body for an OpenAPI operation. When the operation names a
|
|
38099
|
+
* Swagger body argument containing an object, send that object directly.
|
|
36716
38100
|
* Otherwise, every payload key that is NOT a declared path/query/
|
|
36717
38101
|
* header param becomes the body. Sets `content-type` when a body is produced.
|
|
36718
38102
|
* Returns `undefined` for body-less operations (e.g. GET).
|
|
38103
|
+
*
|
|
38104
|
+
* JSON unless the operation declares `bodyEncoding: 'form'` — see
|
|
38105
|
+
* {@link serialiseRequestBody}. Both exits go through it, because a provider
|
|
38106
|
+
* that only speaks form-encoding speaks it on every endpoint and a Swagger
|
|
38107
|
+
* named body is not the exception.
|
|
38108
|
+
*
|
|
38109
|
+
* Exported for the specs: the encoding is a property of THIS function — which
|
|
38110
|
+
* bytes go out with which `content-type` — and asserting it here is a direct
|
|
38111
|
+
* reading rather than an inference from what a `fetch` stub was handed.
|
|
36719
38112
|
*/
|
|
36720
38113
|
function buildRequestBody(operation, args, headers) {
|
|
36721
38114
|
if (operation.requestBodySchema === void 0) return void 0;
|
|
36722
38115
|
const namedBody = operation.bodyArgName === void 0 ? void 0 : args[operation.bodyArgName];
|
|
36723
|
-
if (namedBody !== null && typeof namedBody === "object")
|
|
36724
|
-
headers["content-type"] = "application/json";
|
|
36725
|
-
return JSON.stringify(namedBody);
|
|
36726
|
-
}
|
|
38116
|
+
if (namedBody !== null && typeof namedBody === "object") return serialiseRequestBody(operation, namedBody, headers);
|
|
36727
38117
|
const consumed = /* @__PURE__ */ new Set();
|
|
36728
38118
|
for (const param of operation.parameters ?? []) if (param.in === "path" || param.in === "query" || param.in === "header") consumed.add(param.name);
|
|
36729
38119
|
for (const arg of brokerConsumedArgs(operation)) consumed.add(arg.name);
|
|
36730
38120
|
const body = {};
|
|
36731
38121
|
for (const [key, value] of Object.entries(args)) if (!consumed.has(key)) body[key] = value;
|
|
36732
38122
|
if (Object.keys(body).length === 0) return void 0;
|
|
38123
|
+
return serialiseRequestBody(operation, body, headers);
|
|
38124
|
+
}
|
|
38125
|
+
/**
|
|
38126
|
+
* Serialise one already-assembled body, and set the `content-type` it goes out
|
|
38127
|
+
* with. The two travel together on purpose: a form body sent under
|
|
38128
|
+
* `application/json` is a provider parsing `amount=2000` as malformed JSON, and
|
|
38129
|
+
* a JSON body under the form header is a provider reading `{"amount":2000}` as
|
|
38130
|
+
* ONE field whose name is that whole string — it answers 200 and stores
|
|
38131
|
+
* nonsense. Neither failure is readable from the operator's side, so the header
|
|
38132
|
+
* is never set anywhere but here.
|
|
38133
|
+
*
|
|
38134
|
+
* `bodyEncoding` is the operation's own declaration (`x-stackbone-body-encoding`
|
|
38135
|
+
* in the document); absent, this is the JSON path exactly as it has always been.
|
|
38136
|
+
*/
|
|
38137
|
+
function serialiseRequestBody(operation, body, headers) {
|
|
38138
|
+
if (operation.bodyEncoding === "form") {
|
|
38139
|
+
headers["content-type"] = "application/x-www-form-urlencoded";
|
|
38140
|
+
return encodeFormBody(body);
|
|
38141
|
+
}
|
|
36733
38142
|
headers["content-type"] = "application/json";
|
|
36734
38143
|
return JSON.stringify(body);
|
|
36735
38144
|
}
|
|
@@ -36955,6 +38364,7 @@ var init_connect_action_executor = __esmMin((() => {
|
|
|
36955
38364
|
init_connection_fence();
|
|
36956
38365
|
init_delete_guard();
|
|
36957
38366
|
init_body_error();
|
|
38367
|
+
init_form_encoding();
|
|
36958
38368
|
init_provider_error_classes();
|
|
36959
38369
|
init_spec_request();
|
|
36960
38370
|
init_operation_continuation();
|
|
@@ -51808,8 +53218,27 @@ var CONNECT_DDL = `
|
|
|
51808
53218
|
* Ensure the connect tables exist in the emulator Postgres. Idempotent. Exported
|
|
51809
53219
|
* so the workspace-server can run it once at boot (and the spec can run it before
|
|
51810
53220
|
* exercising the routes) instead of paying the DDL on every request.
|
|
51811
|
-
|
|
51812
|
-
|
|
53221
|
+
*
|
|
53222
|
+
* A NO-OP WHEN THE CONTROL PLANE OWNS THE SCHEMA (PLA-1286). In the self-host
|
|
53223
|
+
* topology the box and the control plane share ONE Postgres, and both declare
|
|
53224
|
+
* `connection_grant`: db-core's `0008_connect.sql` with a bare `CREATE TABLE`,
|
|
53225
|
+
* this module with `CREATE TABLE IF NOT EXISTS`. Only one of the two survives
|
|
53226
|
+
* arriving second — the migration dies on `relation already exists`, the control
|
|
53227
|
+
* plane restarts, and the self-host smoke reports it as a health-check timeout.
|
|
53228
|
+
*
|
|
53229
|
+
* The DDL here runs LAZILY, on the first request that builds the broker context,
|
|
53230
|
+
* so which of the two lands first is a coin flip decided by traffic. Probing the
|
|
53231
|
+
* database for a control plane cannot settle it: the box answers healthy seconds
|
|
53232
|
+
* before the control plane has written anything at all, so the probe would come
|
|
53233
|
+
* back empty and create the table anyway. So the topology DECLARES the owner
|
|
53234
|
+
* instead of the box guessing it, and a declared owner is the same answer on
|
|
53235
|
+
* every boot.
|
|
53236
|
+
*
|
|
53237
|
+
* `stackbone dev` and BYOC set nothing, have no control plane on their database,
|
|
53238
|
+
* and keep creating their own tables exactly as before.
|
|
53239
|
+
*/
|
|
53240
|
+
var ensureConnectTables = async (connectionString, opts = {}) => {
|
|
53241
|
+
if ((opts.schemaOwner ?? process.env["STACKBONE_PLATFORM_SCHEMA_OWNER"]) === "control-plane") return;
|
|
51813
53242
|
const client = new Client({ connectionString });
|
|
51814
53243
|
await client.connect();
|
|
51815
53244
|
try {
|
|
@@ -51879,47 +53308,53 @@ var deriveBase64Key = (raw) => createHash("sha256").update(raw).digest().toStrin
|
|
|
51879
53308
|
var makeContextFactory = (opts) => {
|
|
51880
53309
|
let cached;
|
|
51881
53310
|
const get = () => {
|
|
51882
|
-
if (!cached)
|
|
51883
|
-
|
|
51884
|
-
|
|
51885
|
-
|
|
51886
|
-
|
|
51887
|
-
|
|
51888
|
-
|
|
51889
|
-
|
|
51890
|
-
|
|
51891
|
-
|
|
51892
|
-
|
|
51893
|
-
|
|
51894
|
-
|
|
51895
|
-
|
|
51896
|
-
|
|
51897
|
-
|
|
51898
|
-
|
|
51899
|
-
|
|
51900
|
-
|
|
51901
|
-
|
|
51902
|
-
|
|
51903
|
-
|
|
51904
|
-
|
|
53311
|
+
if (!cached) {
|
|
53312
|
+
const building = (async () => {
|
|
53313
|
+
await ensureConnectTables(opts.connectionString);
|
|
53314
|
+
const cipher = await createSecretCipher(deriveBase64Key(opts.encryptionKey ?? DEV_CONNECT_ENCRYPTION_KEY));
|
|
53315
|
+
const sql = postgres(opts.connectionString, {
|
|
53316
|
+
max: 4,
|
|
53317
|
+
onnotice: () => void 0
|
|
53318
|
+
});
|
|
53319
|
+
const db = drizzle(sql);
|
|
53320
|
+
const store = new DrizzleBrokerStore(db, cipher);
|
|
53321
|
+
const cache = createInMemoryTokenCache();
|
|
53322
|
+
const specCache = createBoxSpecCache(opts.redisUrl);
|
|
53323
|
+
const bindings = createInMemoryBindingStore();
|
|
53324
|
+
const exchanger = new DefaultOAuthExchanger();
|
|
53325
|
+
const brokerCallbackBaseUrl = opts.brokerCallbackBaseUrl ?? "http://127.0.0.1:4242/api/connect/callback";
|
|
53326
|
+
const service = new ConnectService({
|
|
53327
|
+
store,
|
|
53328
|
+
exchanger,
|
|
53329
|
+
cache,
|
|
53330
|
+
bindings,
|
|
53331
|
+
oauth: {
|
|
53332
|
+
brokerCallbackUrl: brokerCallbackBaseUrl,
|
|
53333
|
+
stateSecret: deriveBase64Key(opts.encryptionKey ?? DEV_CONNECT_ENCRYPTION_KEY)
|
|
53334
|
+
}
|
|
53335
|
+
});
|
|
53336
|
+
return {
|
|
53337
|
+
db,
|
|
53338
|
+
audit: opts.audit,
|
|
53339
|
+
sql,
|
|
53340
|
+
cipher,
|
|
53341
|
+
store,
|
|
53342
|
+
service,
|
|
53343
|
+
bindings,
|
|
53344
|
+
exchanger,
|
|
53345
|
+
specCache: specCache.cache,
|
|
53346
|
+
brokerCallbackBaseUrl,
|
|
53347
|
+
async close() {
|
|
53348
|
+
await sql.end({ timeout: 5 }).catch(() => void 0);
|
|
53349
|
+
await specCache.close();
|
|
53350
|
+
}
|
|
53351
|
+
};
|
|
53352
|
+
})();
|
|
53353
|
+
cached = building;
|
|
53354
|
+
building.catch(() => {
|
|
53355
|
+
if (cached === building) cached = void 0;
|
|
51905
53356
|
});
|
|
51906
|
-
|
|
51907
|
-
db,
|
|
51908
|
-
audit: opts.audit,
|
|
51909
|
-
sql,
|
|
51910
|
-
cipher,
|
|
51911
|
-
store,
|
|
51912
|
-
service,
|
|
51913
|
-
bindings,
|
|
51914
|
-
exchanger,
|
|
51915
|
-
specCache: specCache.cache,
|
|
51916
|
-
brokerCallbackBaseUrl,
|
|
51917
|
-
async close() {
|
|
51918
|
-
await sql.end({ timeout: 5 }).catch(() => void 0);
|
|
51919
|
-
await specCache.close();
|
|
51920
|
-
}
|
|
51921
|
-
};
|
|
51922
|
-
})();
|
|
53357
|
+
}
|
|
51923
53358
|
return cached;
|
|
51924
53359
|
};
|
|
51925
53360
|
const close = async () => {
|
|
@@ -52144,14 +53579,13 @@ var init_trigger_strategies = __esmMin((() => {
|
|
|
52144
53579
|
}));
|
|
52145
53580
|
//#endregion
|
|
52146
53581
|
//#region ../../libs/runtime/workspace-runtime/src/lib/connect-inbound/timestamp-poll-cursor.ts
|
|
52147
|
-
var
|
|
53582
|
+
var readStamp, createTimestampPoll;
|
|
52148
53583
|
var init_timestamp_poll_cursor = __esmMin((() => {
|
|
52149
53584
|
init_src$4();
|
|
52150
|
-
|
|
52151
|
-
|
|
52152
|
-
|
|
52153
|
-
|
|
52154
|
-
return parsed;
|
|
53585
|
+
readStamp = (value, unit) => {
|
|
53586
|
+
const stamp = parseStamp(value, unit);
|
|
53587
|
+
if (stamp === null) throw new Error(unit === void 0 ? "Invalid timestamp cursor: expected an ISO date stamp" : `Invalid timestamp cursor: expected a stamp in ${unit}`);
|
|
53588
|
+
return stamp;
|
|
52155
53589
|
};
|
|
52156
53590
|
createTimestampPoll = (spec) => {
|
|
52157
53591
|
const rawPoll = createDeclarativePoll(spec);
|
|
@@ -52161,8 +53595,10 @@ var init_timestamp_poll_cursor = __esmMin((() => {
|
|
|
52161
53595
|
if (cursor === null || typeof cursor !== "object" || !("kind" in cursor)) return rawPoll(ctx, cursor);
|
|
52162
53596
|
if (cursor.kind !== "timestamp-v1") throw new Error("Invalid timestamp cursor: unknown version");
|
|
52163
53597
|
const stored = cursor;
|
|
52164
|
-
const
|
|
52165
|
-
const
|
|
53598
|
+
const unit = cursorSpec.unit;
|
|
53599
|
+
const value = readStamp(stored.value, unit);
|
|
53600
|
+
const valueEpoch = value.epochMs;
|
|
53601
|
+
const floorEpoch = readStamp(stored.floor, unit).epochMs;
|
|
52166
53602
|
if (valueEpoch < floorEpoch) throw new Error("Invalid timestamp cursor: value precedes arming floor");
|
|
52167
53603
|
const lookbackMs = Math.min(cursorSpec.lookbackMs ?? 0, valueEpoch - floorEpoch);
|
|
52168
53604
|
const result = await createDeclarativePoll({
|
|
@@ -52171,8 +53607,8 @@ var init_timestamp_poll_cursor = __esmMin((() => {
|
|
|
52171
53607
|
...cursorSpec,
|
|
52172
53608
|
lookbackMs
|
|
52173
53609
|
}
|
|
52174
|
-
})(ctx, new Date(valueEpoch).toISOString());
|
|
52175
|
-
const next =
|
|
53610
|
+
})(ctx, unit === void 0 ? new Date(valueEpoch).toISOString() : formatStamp(valueEpoch, value.shape));
|
|
53611
|
+
const next = readStamp(result.nextCursor, unit).epochMs > valueEpoch ? result.nextCursor : stored.value;
|
|
52176
53612
|
return {
|
|
52177
53613
|
...result,
|
|
52178
53614
|
nextCursor: {
|
|
@@ -52335,7 +53771,11 @@ var init_trigger_capability = __esmMin((() => {
|
|
|
52335
53771
|
if (!descriptor || descriptor.poll.kind !== "declarative") return null;
|
|
52336
53772
|
if (descriptor.poll.cursor.kind !== "timestamp") return null;
|
|
52337
53773
|
if (SCOPE_TEMPLATE.test(descriptor.poll.list.operationId)) return null;
|
|
52338
|
-
const
|
|
53774
|
+
const unit = descriptor.poll.cursor.unit;
|
|
53775
|
+
const value = unit === void 0 ? armedAt.toISOString() : formatStamp(armedAt.getTime(), {
|
|
53776
|
+
unit,
|
|
53777
|
+
numeric: true
|
|
53778
|
+
});
|
|
52339
53779
|
return descriptor.poll.cursor.lookbackMs === void 0 ? value : {
|
|
52340
53780
|
kind: "timestamp-v1",
|
|
52341
53781
|
value,
|
|
@@ -65943,6 +67383,7 @@ var init_load_project_runtime = __esmMin((() => {
|
|
|
65943
67383
|
let setActiveStepReader;
|
|
65944
67384
|
let approvalRequest;
|
|
65945
67385
|
let setConnectorInvoker;
|
|
67386
|
+
let setEchoGuardReader;
|
|
65946
67387
|
let connectorInvoker;
|
|
65947
67388
|
let setActionInvoker;
|
|
65948
67389
|
let actionInvoker;
|
|
@@ -65958,6 +67399,7 @@ var init_load_project_runtime = __esmMin((() => {
|
|
|
65958
67399
|
setApprovalRecorder = sdk.setApprovalRecorder;
|
|
65959
67400
|
setActiveStepReader = sdk.setActiveStepReader;
|
|
65960
67401
|
setConnectorInvoker = sdk.setConnectorInvoker;
|
|
67402
|
+
setEchoGuardReader = sdk.setEchoGuardReader;
|
|
65961
67403
|
connectorInvoker = sdk.createConnectInvoker?.();
|
|
65962
67404
|
setActionInvoker = sdk.setActionInvoker;
|
|
65963
67405
|
actionInvoker = sdk.createActionInvoker?.();
|
|
@@ -66015,6 +67457,7 @@ var init_load_project_runtime = __esmMin((() => {
|
|
|
66015
67457
|
setActiveStepReader,
|
|
66016
67458
|
approvalRequest,
|
|
66017
67459
|
setConnectorInvoker,
|
|
67460
|
+
setEchoGuardReader,
|
|
66018
67461
|
connectorInvoker,
|
|
66019
67462
|
setActionInvoker,
|
|
66020
67463
|
actionInvoker,
|
|
@@ -66679,11 +68122,49 @@ var init_runtime_approval_plants = __esmMin((() => {
|
|
|
66679
68122
|
}));
|
|
66680
68123
|
//#endregion
|
|
66681
68124
|
//#region ../../libs/runtime/workspace-runtime/src/lib/workflow/runtime-connector-plants.ts
|
|
66682
|
-
var plantConnectorInvoker, plantActionInvoker;
|
|
68125
|
+
var owningWorkflow, echoOwner, unresolvedEcho, resolveEcho, echoRecordError, plantConnectorInvoker, plantActionInvoker, ECHO_WINDOW_MS, renderEcho, echoGuardUnavailable, plantEchoGuard;
|
|
66683
68126
|
var init_runtime_connector_plants = __esmMin((() => {
|
|
68127
|
+
init_src$4();
|
|
68128
|
+
init_src$13();
|
|
66684
68129
|
init_call_ordinal();
|
|
66685
68130
|
init_monotonic_span();
|
|
66686
68131
|
init_project_runtime();
|
|
68132
|
+
owningWorkflow = () => {
|
|
68133
|
+
const owner = getExecutionOwner();
|
|
68134
|
+
return owner?.kind === "workflow" ? owner.name : void 0;
|
|
68135
|
+
};
|
|
68136
|
+
echoOwner = () => {
|
|
68137
|
+
const workflowName = owningWorkflow();
|
|
68138
|
+
return workflowName !== void 0 ? { workflowName } : {};
|
|
68139
|
+
};
|
|
68140
|
+
unresolvedEcho = (echoPath) => ({
|
|
68141
|
+
echoPath,
|
|
68142
|
+
echo: null,
|
|
68143
|
+
echoUnresolved: true,
|
|
68144
|
+
...echoOwner()
|
|
68145
|
+
});
|
|
68146
|
+
resolveEcho = (echoPath, output, label) => {
|
|
68147
|
+
const owner = echoOwner();
|
|
68148
|
+
const echo = getPath(output, echoPath);
|
|
68149
|
+
if (echo !== void 0 && echo !== null && echo !== "") return {
|
|
68150
|
+
echoPath,
|
|
68151
|
+
echo,
|
|
68152
|
+
...owner
|
|
68153
|
+
};
|
|
68154
|
+
console.warn(`[connector] ${label}: echoPath "${echoPath}" resolved to nothing (missing, null or empty) in the response, so the run step records no echo value and the self-write guard cannot answer for this call`);
|
|
68155
|
+
return {
|
|
68156
|
+
echoPath,
|
|
68157
|
+
echo: null,
|
|
68158
|
+
echoUnresolved: true,
|
|
68159
|
+
...owner
|
|
68160
|
+
};
|
|
68161
|
+
};
|
|
68162
|
+
echoRecordError = (label, cause) => {
|
|
68163
|
+
const error = /* @__PURE__ */ new Error(`[connector] ${label}: the provider call succeeded but its echo record could not be written (${cause instanceof Error ? cause.message : String(cause)}), so nothing can recognise this write when the trigger delivers it back. The step stops here.`);
|
|
68164
|
+
error.code = "echo_record_failed";
|
|
68165
|
+
error.fatal = true;
|
|
68166
|
+
return error;
|
|
68167
|
+
};
|
|
66687
68168
|
plantConnectorInvoker = (rt, ctx) => {
|
|
66688
68169
|
if (!rt.setConnectorInvoker || !rt.connectorInvoker) return;
|
|
66689
68170
|
const baseInvoker = rt.connectorInvoker;
|
|
@@ -66697,14 +68178,15 @@ var init_runtime_connector_plants = __esmMin((() => {
|
|
|
66697
68178
|
const span = startSpan();
|
|
66698
68179
|
const ordinal = stepInfo ? nextConnOrdinal(runId ?? "", stepInfo.stepId, stepInfo.attempt) : 0;
|
|
66699
68180
|
const externalStepId = stepInfo ? `${stepInfo.stepId}::conn::${ordinal}` : randomUUID();
|
|
66700
|
-
const
|
|
66701
|
-
|
|
68181
|
+
const label = `${args.connector}/${args.operation}`;
|
|
68182
|
+
const writeRow = (status, extra, echo) => {
|
|
68183
|
+
if (!runId) return void 0;
|
|
66702
68184
|
const { completedAt, durationMs } = closeSpan(span);
|
|
66703
|
-
recordActivityStep({
|
|
68185
|
+
return recordActivityStep({
|
|
66704
68186
|
runId,
|
|
66705
68187
|
externalStepId,
|
|
66706
68188
|
type: "connection",
|
|
66707
|
-
name:
|
|
68189
|
+
name: label,
|
|
66708
68190
|
status,
|
|
66709
68191
|
startedAt: span.startedAt.toISOString(),
|
|
66710
68192
|
completedAt: completedAt.toISOString(),
|
|
@@ -66712,26 +68194,52 @@ var init_runtime_connector_plants = __esmMin((() => {
|
|
|
66712
68194
|
input: {
|
|
66713
68195
|
connector: args.connector,
|
|
66714
68196
|
operation: args.operation,
|
|
66715
|
-
args: args.args
|
|
68197
|
+
args: args.args,
|
|
68198
|
+
...echo ?? {}
|
|
66716
68199
|
},
|
|
66717
68200
|
...extra,
|
|
66718
68201
|
enclosingExternalStepId: stepInfo?.stepId ?? null
|
|
66719
|
-
})
|
|
68202
|
+
});
|
|
68203
|
+
};
|
|
68204
|
+
const forget = (write) => {
|
|
68205
|
+
write?.catch((err) => {
|
|
66720
68206
|
console.warn(`[connector] run-step write failed for run ${runId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
66721
68207
|
});
|
|
66722
68208
|
};
|
|
68209
|
+
let result;
|
|
66723
68210
|
try {
|
|
66724
|
-
|
|
66725
|
-
record("completed", { output: result.output });
|
|
66726
|
-
return result;
|
|
68211
|
+
result = await baseInvoker.invoke(args);
|
|
66727
68212
|
} catch (err) {
|
|
66728
68213
|
const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
|
|
66729
|
-
|
|
68214
|
+
const echo = typeof args.echoPath === "string" ? unresolvedEcho(args.echoPath) : void 0;
|
|
68215
|
+
if (echo) console.warn(`[connector] ${label}: the call failed after naming an echoPath, so it may have landed with nothing recorded — the run step keeps an unresolved marker and the self-write guard will refuse rather than answer for this connector`);
|
|
68216
|
+
const write = writeRow("failed", { error: {
|
|
66730
68217
|
message: err instanceof Error ? err.message : String(err),
|
|
66731
68218
|
...typeof code === "string" ? { code } : {}
|
|
66732
|
-
} });
|
|
68219
|
+
} }, echo);
|
|
68220
|
+
if (echo) await write?.catch((cause) => {
|
|
68221
|
+
console.warn(`[connector] ${label}: could not record the unresolved echo marker: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
68222
|
+
});
|
|
68223
|
+
else forget(write);
|
|
66733
68224
|
throw err;
|
|
66734
68225
|
}
|
|
68226
|
+
if (typeof args.echoPath !== "string") {
|
|
68227
|
+
forget(writeRow("completed", { output: result.output }));
|
|
68228
|
+
return result;
|
|
68229
|
+
}
|
|
68230
|
+
const echo = resolveEcho(args.echoPath, result.output, label);
|
|
68231
|
+
const write = writeRow("completed", { output: result.output }, echo);
|
|
68232
|
+
if (write === void 0) console.warn(`[connector] ${label}: this call named an echoPath but runs with no invocation context, so no run step records its echo and the self-write guard cannot recognise the delivery this write causes`);
|
|
68233
|
+
if (write !== void 0) {
|
|
68234
|
+
let rowId;
|
|
68235
|
+
try {
|
|
68236
|
+
rowId = await write;
|
|
68237
|
+
} catch (cause) {
|
|
68238
|
+
throw echoRecordError(label, cause);
|
|
68239
|
+
}
|
|
68240
|
+
if (rowId === null) throw echoRecordError(label, /* @__PURE__ */ new Error(`the store wrote no row for run ${runId}`));
|
|
68241
|
+
}
|
|
68242
|
+
return result;
|
|
66735
68243
|
} });
|
|
66736
68244
|
} else rt.setConnectorInvoker(baseInvoker);
|
|
66737
68245
|
};
|
|
@@ -66795,6 +68303,34 @@ var init_runtime_connector_plants = __esmMin((() => {
|
|
|
66795
68303
|
}
|
|
66796
68304
|
} });
|
|
66797
68305
|
};
|
|
68306
|
+
ECHO_WINDOW_MS = 3600 * 1e3;
|
|
68307
|
+
renderEcho = (value) => String(value);
|
|
68308
|
+
echoGuardUnavailable = (connector, workflowName) => {
|
|
68309
|
+
const scope = workflowName ? `this workflow ("${workflowName}"), or a caller with no workflow owner,` : "some caller on this connector";
|
|
68310
|
+
const error = /* @__PURE__ */ new Error(`isOwnEcho("${connector}"): a write by ${scope} inside the last hour named an echoPath that resolved to nothing, so the value it produced was never recorded. The guard cannot tell your own write from somebody else's change. Find the echoPath that resolved to nothing — the runtime warned which one when it wrote the row.`);
|
|
68311
|
+
error.code = "echo_guard_unavailable";
|
|
68312
|
+
return error;
|
|
68313
|
+
};
|
|
68314
|
+
plantEchoGuard = (rt, ctx) => {
|
|
68315
|
+
if (!rt.setEchoGuardReader) return;
|
|
68316
|
+
const findEchoRecord = ctx.invocations?.findEchoRecord?.bind(ctx.invocations);
|
|
68317
|
+
if (!findEchoRecord) {
|
|
68318
|
+
rt.setEchoGuardReader(void 0);
|
|
68319
|
+
return;
|
|
68320
|
+
}
|
|
68321
|
+
rt.setEchoGuardReader({ isOwnEcho: async ({ connector, value }) => {
|
|
68322
|
+
const workflowName = owningWorkflow();
|
|
68323
|
+
const { matched, unresolved } = await findEchoRecord({
|
|
68324
|
+
connector,
|
|
68325
|
+
value: renderEcho(value),
|
|
68326
|
+
workflowName: workflowName ?? null,
|
|
68327
|
+
windowMs: ECHO_WINDOW_MS
|
|
68328
|
+
});
|
|
68329
|
+
if (matched) return true;
|
|
68330
|
+
if (unresolved) throw echoGuardUnavailable(connector, workflowName);
|
|
68331
|
+
return false;
|
|
68332
|
+
} });
|
|
68333
|
+
};
|
|
66798
68334
|
}));
|
|
66799
68335
|
//#endregion
|
|
66800
68336
|
//#region ../../libs/runtime/workspace-runtime/src/lib/workflow/runtime-sdk-plants.ts
|
|
@@ -66858,6 +68394,7 @@ var init_runtime_sdk_plants = __esmMin((() => {
|
|
|
66858
68394
|
plantPromptUseReporter(rt, ctx);
|
|
66859
68395
|
plantApprovalRecorder(rt, ctx);
|
|
66860
68396
|
plantConnectorInvoker(rt, ctx);
|
|
68397
|
+
plantEchoGuard(rt, ctx);
|
|
66861
68398
|
plantActionInvoker(rt, ctx);
|
|
66862
68399
|
return rt;
|
|
66863
68400
|
};
|
|
@@ -66884,6 +68421,7 @@ var init_runtime_loader = __esmMin((() => {
|
|
|
66884
68421
|
rt.setPromptUseReporter?.(void 0);
|
|
66885
68422
|
rt.setApprovalRecorder?.(void 0);
|
|
66886
68423
|
rt.setConnectorInvoker?.(void 0);
|
|
68424
|
+
rt.setEchoGuardReader?.(void 0);
|
|
66887
68425
|
rt.setActionInvoker?.(void 0);
|
|
66888
68426
|
rt.setActiveStepReader?.(void 0);
|
|
66889
68427
|
planted = void 0;
|
|
@@ -67265,7 +68803,8 @@ var init_workflow_http_schemas = __esmMin((() => {
|
|
|
67265
68803
|
displayName: z.string().optional(),
|
|
67266
68804
|
description: z.string().optional(),
|
|
67267
68805
|
example: z.string().optional(),
|
|
67268
|
-
required: z.boolean()
|
|
68806
|
+
required: z.boolean(),
|
|
68807
|
+
suggestions: z.array(z.string()).optional().openapi({ example: ["charge.succeeded", "charge.*"] })
|
|
67269
68808
|
})).openapi({ description: "The per-subscription values this trigger needs, declared by the connector. The register dialog renders a field each and the register call must carry every required one. Empty when the credential is the trigger's whole scope." }),
|
|
67270
68809
|
cursorColumn: z.object({ default: z.string() }).nullable().openapi({
|
|
67271
68810
|
description: "Present when the poll cursor advances on a column the subscription chooses; `default` pre-fills the field.",
|
|
@@ -71117,7 +72656,7 @@ var toWorkflowExport = (name) => `${toCamelCase(name)}Workflow`;
|
|
|
71117
72656
|
var index_ts_default = "import { tool } from \"@langchain/core/tools\";\nimport { z } from \"zod\";\nimport { defineDeepAgent } from \"@stackbone/sdk/deep\";\n\n// A deep agent is a single file: a name, a model, and optional tools /\n// sub-agents. `stackbone dev` discovers it by its folder\n// (`deep-agents/{{name}}/`), builds the LangGraph graph in-process (no server,\n// no port), and serves it over the standard OpenAI + Anthropic chat wire — point\n// any client at the emulator and select this agent with `\"model\": \"{{name}}\"`.\n\n// ITS INSTRUCTION IS NOT IN THIS FILE. Write it in Studio, under the prompt\n// named `{{name}}` (the agent's name is its default prompt key). Until you do,\n// the agent runs with an empty instruction — it answers, it just has no persona\n// yet. To put values into the text, declare them here and reference them in the\n// prompt with the double-brace syntax:\n//\n// instructions: { key: \"{{name}}\", variables: { tone: \"friendly\" } },\n\n// A tiny example tool. Delete it (and the `tools` field below) if your agent\n// needs none, or add more — each is a plain LangChain `tool(...)`.\nconst addNumbers = tool(\n async ({ a, b }: { a: number; b: number }) => String(a + b),\n {\n name: \"add_numbers\",\n description: \"Add two integers and return the sum.\",\n schema: z.object({ a: z.number().int(), b: z.number().int() }),\n },\n);\n\nexport default defineDeepAgent({\n // Must match the folder name: it is who this agent's prompts belong to.\n name: \"{{name}}\",\n model: \"openai/gpt-4o-mini\",\n tools: [addNumbers],\n});\n";
|
|
71118
72657
|
//#endregion
|
|
71119
72658
|
//#region src/services/scaffold/templates/shell/AGENTS.md.tmpl?raw
|
|
71120
|
-
var AGENTS_md_default = "# Agent instructions\n\nThis project is the **{{name}}** Stackbone workspace — a multi-agent project. It\nholds one or more in-process deep agents plus the durable workflows that\norchestrate them:\n\n- `deep-agents/<name>/` — one deep agent per folder (discovered by its `index.ts`).\n Its code is `deep-agents/<name>/index.ts`, which `export default defineDeepAgent({\n name, model, tools })`. Its INSTRUCTION is not in the code: it is written in\n Studio, under the prompt keyed by the agent's name, so changing the wording is an\n edit rather than a deploy. A deep agent is a plain JS module: no server, no port,\n no HTTP code — the emulator builds its LangGraph graph in-process and serves it\n over the standard OpenAI + Anthropic chat wire.\n- `workflows/<name>.workflow.ts` — one durable Workflow SDK workflow per file:\n a plain async function tagged `\"use workflow\"`, with each `\"use step\"` a\n durable checkpoint, and its contract declared as sibling `inputSchema` /\n `outputSchema` exports.\n\n`stackbone dev` discovers every deep agent and workflow by convention — there is no\nregistry file to hand-edit.\n\n## Always use the Stackbone skills\n\n`stackbone init` / `stackbone link` install
|
|
72659
|
+
var AGENTS_md_default = "# Agent instructions\n\nThis project is the **{{name}}** Stackbone workspace — a multi-agent project. It\nholds one or more in-process deep agents plus the durable workflows that\norchestrate them:\n\n- `deep-agents/<name>/` — one deep agent per folder (discovered by its `index.ts`).\n Its code is `deep-agents/<name>/index.ts`, which `export default defineDeepAgent({\n name, model, tools })`. Its INSTRUCTION is not in the code: it is written in\n Studio, under the prompt keyed by the agent's name, so changing the wording is an\n edit rather than a deploy. A deep agent is a plain JS module: no server, no port,\n no HTTP code — the emulator builds its LangGraph graph in-process and serves it\n over the standard OpenAI + Anthropic chat wire.\n- `workflows/<name>.workflow.ts` — one durable Workflow SDK workflow per file:\n a plain async function tagged `\"use workflow\"`, with each `\"use step\"` a\n durable checkpoint, and its contract declared as sibling `inputSchema` /\n `outputSchema` exports.\n\n`stackbone dev` discovers every deep agent and workflow by convention — there is no\nregistry file to hand-edit.\n\n## Always use the Stackbone skills\n\n`stackbone init` / `stackbone link` install seven skills into this workspace for\nevery coding agent you picked (Claude Code, Cursor, Windsurf, Codex, Cline, …), and\nadd the `stackbone-docs` MCP server to that agent's configuration.\n**Use them — do not improvise from memory.** The skills tell you how to work here:\nthe shapes, the order of operations, the rules. The SDK surface, the CLI flags and\nthe error catalog are read from the docs MCP server (`search_docs`, `get_doc`); each\nskill names the page to read before you touch a surface.\n\n| Skill | Use it when… |\n| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `stackbone` | **Editing agent or workflow code.** Writing a `defineDeepAgent`, a `*.workflow.ts`, DB CRUD, storage uploads, LLM calls, RAG, queues, HITL, secrets, or prompts. |\n| `stackbone-cli` | **Running or shipping the workspace.** Scaffolding (`init` / `add` / `link`), the local emulator (`dev`), compiling the deployable bundle (`build`), DB migrations (`db migrate`), reading metadata. |\n| `stackbone-debug` | **Triaging a failure.** An error, a 4xx/5xx, a stuck or timed-out run, a HITL run waiting on approval, a failed deploy, or a quota/billing response. Locate the cause, then fix it. |\n| `stackbone-coder` | **Starting a new piece, feature…** — interviews, then scaffolds it by description. |\n| `stackbone-evals` | **Measuring whether it works.** Turning a Playground exchange into an eval case, a case into a suite, a suite into a run, and comparing two variants of the same suite. |\n| `stackbone-guardrails` | **Putting limits on a running agent.** Blocking a turn, masking data on its way out, or holding it for a person. The rule is state on the box, so it needs no rebuild. |\n| `stackbone-integrations` | **Wiring the workspace to the outside.** Registering a connection, arming a trigger so a workflow starts on its own, and binding what a run outputs to a destination. |\n\n## Grow the workspace\n\n```sh\nstackbone add agent <name> # add a deep agent (alias of add deep-agent)\nstackbone add workflow <name> # add a durable workflow\nstackbone add workflow-agent <name> # add a deep agent + a workflow wired to call it\n```\n\n## Edit a deep agent\n\n- The agent lives in `deep-agents/<name>/index.ts` as `defineDeepAgent({ name,\n model, tools })` from `@stackbone/sdk/deep`. Its tools are plain LangChain\n `tool(...)` values.\n- Its behaviour and tone are its PROMPT, written in Studio and keyed by the agent's\n name. Write text there, never in the code. Pass values into it with\n `instructions: { variables: { ... } }`, and point it at another key with\n `instructions: { key: \"...\" }`. An agent whose prompt is still empty runs and\n answers — it just has no persona yet.\n- Pick the model with a bare id string (resolved through the deployment's **model\n provider**, i.e. whatever OpenAI-compatible endpoint it is pointed at) — e.g.\n `model: \"openai/gpt-4o-mini\"` — or pass a built LangChain model instance.\n- Follow the `stackbone` skill for every `client.*` / `stackbone.*` surface.\n\n## Edit a workflow\n\nA workflow is a bare async function — no class, no decorators:\n\n```ts\nexport const inputSchema = z.object({ message: z.string() });\nexport const outputSchema = z.object({ reply: z.string() });\n\nexport async function myWorkflow(input) {\n \"use workflow\";\n const reply = await askAgent(input.message); // a durable step\n return { reply };\n}\n```\n\nThe `inputSchema` / `outputSchema` exports next to the function ARE the contract.\nTag each durable checkpoint with `\"use step\"`.\n\n## Reaching a deep agent or a connector\n\n- To call a deep agent from a workflow step, use `callDeepAgent` from\n `@stackbone/sdk/workflow` inside a `\"use step\"` — it runs the agent IN-PROCESS\n (no HTTP, no HMAC):\n\n ```ts\n import { callDeepAgent } from '@stackbone/sdk/workflow';\n\n async function askAgent(message: string) {\n \"use step\";\n const { text } = await callDeepAgent('support', message);\n return text;\n }\n ```\n\n- To call a connector directly (typed when its types are generated), use the\n ambient handle:\n\n ```ts\n import { stackbone } from '@stackbone/sdk';\n await stackbone.connection('stub-mail').sendMail({ to, subject, body });\n ```\n\n## Run it locally\n\n```sh\npnpm install\nstackbone dev # runs the workspace against a local World + Studio\n```\n\n**The first run stops for setup.** With no model provider configured, `stackbone\ndev` brings up the local stack and then holds the workflows and agents back,\nprinting a Studio link to finish setup in the browser. Saving there starts the\nworkspace in place, with no restart. To skip that step entirely, export the\nendpoint before booting:\n\n```sh\nexport MODEL_PROVIDER_BASE_URL=https://openrouter.ai/api/v1\nexport MODEL_PROVIDER_API_KEY=sk-... # omit for a keyless local gateway\n```\n\nUse the `stackbone-cli` skill for the emulator, migrations, and building the\nbundle you deploy, and the `stackbone-debug` skill the moment something fails.\n\nThe full SDK and CLI reference lives at https://docs.stackbone.ai, and is\nserved over MCP at https://docs.stackbone.ai/mcp so you can search it from\nhere. Run `stackbone docs` for the connection details.\n";
|
|
71121
72660
|
//#endregion
|
|
71122
72661
|
//#region src/services/scaffold/templates/shell/CLAUDE.md.tmpl?raw
|
|
71123
72662
|
var CLAUDE_md_default = "# CLAUDE.md\n\nThis is the **{{name}}** Stackbone workspace. The full agent-development guidance\nlives in `AGENTS.md` — read it.\n\n@AGENTS.md\n\n## Claude Code note\n\nThe Stackbone skills (`stackbone`, `stackbone-cli`, `stackbone-debug`,\n`stackbone-coder`) are installed for Claude Code under `.claude/`, and the\n`stackbone-docs` MCP server is registered in `.mcp.json`. Invoke the skills\nthrough the Skill tool:\n\n- `stackbone` — write/edit deep agents (`deep-agents/<name>/index.ts`) and\n workflows (`workflows/<name>.workflow.ts`).\n- `stackbone-cli` — run the emulator (`stackbone dev`), migrate the DB, compile\n the deployable bundle (`stackbone build`).\n- `stackbone-debug` — triage any error or stuck run, then return to the skill\n above to apply the fix.\n- `stackbone-coder` — start a new agent or workflow from a one-line idea.\n\nEach skill says how to find the docs page to read before you touch a surface\n(`list_docs`, then `get_doc`); read it instead of guessing a flag or a signature.\n";
|
|
@@ -71153,7 +72692,7 @@ var calls_workflow_ts_default = "import { z } from \"zod\";\nimport { streamDeep
|
|
|
71153
72692
|
var plain_workflow_ts_default = "import { z } from \"zod\";\n\n/**\n * Workflow \"{{name}}\" — a durable pipeline.\n *\n * Each `\"use step\"` is a durable checkpoint: kill the runtime mid-run and it\n * resumes from the last completed step instead of replaying everything.\n */\n\n// THE contract — sibling exports next to the bare workflow function. The build\n// harvests these (by convention) into the manifest + a live validator. The\n// parameter type is DERIVED from `inputSchema` and the RETURN type from\n// `outputSchema` (both `z.infer`), so neither side can drift from the contract\n// the build harvests. Without the return annotation the schema is published to\n// Studio and the generated types and never compared to what the body returns\n// (PLA-1272).\nexport const inputSchema = z.object({\n message: z.string().describe(\"The input to process\"),\n});\n\nexport const outputSchema = z.object({\n message: z.string(),\n processed: z.boolean(),\n});\n\ntype {{Name}}Input = z.infer<typeof inputSchema>;\n\nexport async function {{export}}(input: {{Name}}Input): Promise<z.infer<typeof outputSchema>> {\n \"use workflow\";\n\n const result = await processMessage(input); // step 1 — durable checkpoint\n return result;\n}\n\nasync function processMessage(input: {{Name}}Input) {\n \"use step\";\n // Replace this with the deterministic work of your step (DB write, HTTP call,\n // an LLM call, …). Returning a value records it as the step's checkpoint.\n return { message: input.message, processed: true };\n}\n";
|
|
71154
72693
|
//#endregion
|
|
71155
72694
|
//#region src/services/scaffold/template-loader.ts
|
|
71156
|
-
var SDK_TEMPLATE_VERSION = "0.
|
|
72695
|
+
var SDK_TEMPLATE_VERSION = "0.6.0";
|
|
71157
72696
|
var PREFIX = "./templates/";
|
|
71158
72697
|
var templates = new Map(Object.entries(/* @__PURE__ */ Object.assign({
|
|
71159
72698
|
"./templates/deep-agents/index.ts.tmpl": index_ts_default,
|
|
@@ -75125,7 +76664,7 @@ var REDIS_IMAGE$1 = "redis:7-alpine";
|
|
|
75125
76664
|
* Same image and pin as `stackbone dev`'s compose, so the emulator and the
|
|
75126
76665
|
* deliverable run the identical backend.
|
|
75127
76666
|
*/
|
|
75128
|
-
var MINIO_IMAGE$1 = "minio/minio:RELEASE.2025-03-12T18-04-18Z";
|
|
76667
|
+
var MINIO_IMAGE$1 = "quay.io/minio/minio:RELEASE.2025-03-12T18-04-18Z";
|
|
75129
76668
|
/** Access key the bundled MinIO is created with. The secret is generated per package. */
|
|
75130
76669
|
var MINIO_ACCESS_KEY = "stackbone";
|
|
75131
76670
|
/** Bucket the init container creates, and the agent's `STACKBONE_S3_BUCKET`. */
|
|
@@ -76178,6 +77717,15 @@ ${objectStore}
|
|
|
76178
77717
|
# MinIO addresses buckets by path. A managed bucket that only answers to the
|
|
76179
77718
|
# bucket-as-subdomain form (AWS S3, Railway Buckets) needs this set to false.
|
|
76180
77719
|
STACKBONE_S3_FORCE_PATH_STYLE: \${STACKBONE_S3_FORCE_PATH_STYLE:-true}
|
|
77720
|
+
# PLA-1286 — WHO OWNS THE PLATFORM SCHEMA IN THE DATABASE BELOW. This is the
|
|
77721
|
+
# only topology where the agent and the control plane share one Postgres,
|
|
77722
|
+
# and both of them declare \`connection_grant\`: the control plane through
|
|
77723
|
+
# its db-core migrations, the agent through its own boot DDL. The control
|
|
77724
|
+
# plane's statement is a bare CREATE TABLE and dies if the table is already
|
|
77725
|
+
# there, so the agent stands down here and lets the migrations create them.
|
|
77726
|
+
# Not overridable from .env on purpose: it is a fact about this compose file,
|
|
77727
|
+
# not a preference, and a box that shares no database never sees this line.
|
|
77728
|
+
STACKBONE_PLATFORM_SCHEMA_OWNER: control-plane
|
|
76181
77729
|
${sharedIdentity}
|
|
76182
77730
|
# REPLACES the agent's built-in origin allowlist — it does not add to it. The
|
|
76183
77731
|
# dashboard is a different ORIGIN from the agent (different port), so its
|
|
@@ -78485,7 +80033,7 @@ var readStackboneConfig = async (cwd) => readJsonFile(stackboneConfigPath(cwd),
|
|
|
78485
80033
|
//#region src/dev/compose-template.ts
|
|
78486
80034
|
var POSTGRES_IMAGE = "pgvector/pgvector:pg17";
|
|
78487
80035
|
var REDIS_IMAGE = "redis:7-alpine";
|
|
78488
|
-
var MINIO_IMAGE = "minio/minio:RELEASE.2025-03-12T18-04-18Z";
|
|
80036
|
+
var MINIO_IMAGE = "quay.io/minio/minio:RELEASE.2025-03-12T18-04-18Z";
|
|
78489
80037
|
var PGVECTOR_INIT_SQL = `-- Generated by stackbone dev. Runs once on Postgres first boot via
|
|
78490
80038
|
-- /docker-entrypoint-initdb.d. The queue extension is gone (BullMQ over Redis
|
|
78491
80039
|
-- is the only job engine); only pgvector is enabled here.
|
|
@@ -79028,7 +80576,9 @@ function renderOperation(op) {
|
|
|
79028
80576
|
* time (`call-connector.ts`), and no generated signature declared it — so
|
|
79029
80577
|
* `idempotent` was reachable only through the untyped `.call(...)`, which is not
|
|
79030
80578
|
* the path a creator is steered to. Typed as the whole options object rather than
|
|
79031
|
-
* one field,
|
|
80579
|
+
* one field, which is what carried PLA-1351's `echoPath` in with no change here:
|
|
80580
|
+
* the option landed on `CallConnectorOptions` and every generated signature already
|
|
80581
|
+
* accepted it.
|
|
79032
80582
|
*
|
|
79033
80583
|
* It is ALWAYS second, and `renderParam` is what guarantees that: an operation
|
|
79034
80584
|
* with nothing to put in `args` still prints `args?: undefined` ahead of this. The
|
|
@@ -84151,6 +85701,7 @@ var assertBoxCanBeRegistered = (contract) => {
|
|
|
84151
85701
|
};
|
|
84152
85702
|
//#endregion
|
|
84153
85703
|
//#region src/commands/link/link.command.ts
|
|
85704
|
+
init_src$19();
|
|
84154
85705
|
var requireLinkField = (value, flag, description) => {
|
|
84155
85706
|
const trimmed = (value ?? "").trim();
|
|
84156
85707
|
if (trimmed.length === 0) throw new StackboneCliError({
|
|
@@ -84161,6 +85712,27 @@ var requireLinkField = (value, flag, description) => {
|
|
|
84161
85712
|
return trimmed;
|
|
84162
85713
|
};
|
|
84163
85714
|
/**
|
|
85715
|
+
* PLA-1583 — `link` refuses a plain `http://` box before it touches anything.
|
|
85716
|
+
*
|
|
85717
|
+
* The rule itself comes from `deploymentUrlSchema`, the very schema the control
|
|
85718
|
+
* plane validates the request body with, imported rather than restated: a rule
|
|
85719
|
+
* written twice is a rule the CLI and the API drift apart on, and the CLI is
|
|
85720
|
+
* the caller that used to pass `--url` straight through while only the browser
|
|
85721
|
+
* wizard refused one. What this adds is WHERE the refusal lands — here, before
|
|
85722
|
+
* the handshake dials the box, instead of as a 400 after the operator has
|
|
85723
|
+
* already watched `link` talk to it.
|
|
85724
|
+
*/
|
|
85725
|
+
var requireEncryptedUrl = (url) => {
|
|
85726
|
+
const checked = deploymentUrlSchema.safeParse(url);
|
|
85727
|
+
if (checked.success) return;
|
|
85728
|
+
const refusedScheme = checked.error.issues.find((issue) => issue.code === "custom");
|
|
85729
|
+
throw new StackboneCliError({
|
|
85730
|
+
code: "generic",
|
|
85731
|
+
message: `\`--url ${url}\` cannot be registered: ${refusedScheme?.message ?? checked.error.issues[0]?.message ?? "it is not an https:// address"}`,
|
|
85732
|
+
suggestion: refusedScheme ? "Terminate TLS at your own proxy and pass its https:// address. Callbacks from the control plane and calls from the browser reach the box over this URL; the HMAC secret signs them, it does not encrypt them." : "Pass the full address, scheme and host, like https://box.example.com — a bare host is not enough to know where to call."
|
|
85733
|
+
});
|
|
85734
|
+
};
|
|
85735
|
+
/**
|
|
84164
85736
|
* STA-440 — what `link` tells the operator about the box's browser-identity gate.
|
|
84165
85737
|
* `null` when the box verifies identity: there is nothing to say. The other two
|
|
84166
85738
|
* read differently on purpose — a box that reports it is not verifying is a
|
|
@@ -84263,6 +85835,7 @@ var runLink = async (ctx, args) => {
|
|
|
84263
85835
|
colors: true
|
|
84264
85836
|
}));
|
|
84265
85837
|
const url = requireLinkField(args.url, "url", "the public URL of the deployed agent");
|
|
85838
|
+
requireEncryptedUrl(url);
|
|
84266
85839
|
const secret = requireLinkField(args.secret, "secret", "the HMAC secret the deployed agent authenticates its callbacks with");
|
|
84267
85840
|
const tag = requireLinkField(args.tag, "tag", "the tag of the deep-agents image you deployed");
|
|
84268
85841
|
const agentsArg = readAgentsArg(args.agents);
|