@ductape/mcp 0.1.61 → 0.2.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 +14 -0
- package/README.md +18 -0
- package/dist/index.js +654 -18
- package/docs/TOOLS.md +14 -0
- package/package.json +7 -1
- package/scripts/check-frontend-analytics-guidance.mjs +0 -99
- package/src/index.ts +0 -4318
- package/src/proxy-client.ts +0 -172
- package/tsconfig.json +0 -17
package/src/index.ts
DELETED
|
@@ -1,4318 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Ductape MCP Server
|
|
4
|
-
*
|
|
5
|
-
* Exposes Ductape SDK operations as MCP tools by calling the backend proxy.
|
|
6
|
-
*
|
|
7
|
-
* Authentication:
|
|
8
|
-
* Set DUCTAPE_PUBLISHABLE_KEY in the MCP server's env config to avoid passing
|
|
9
|
-
* publishable_key on every tool call. Per-call publishable_key still overrides
|
|
10
|
-
* the env var when provided.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { createRequire } from 'module';
|
|
14
|
-
import { execSync } from 'child_process';
|
|
15
|
-
import { z } from 'zod';
|
|
16
|
-
import {
|
|
17
|
-
executeViaProxy,
|
|
18
|
-
generateExecutablePayload,
|
|
19
|
-
getAssetSchemas,
|
|
20
|
-
type SDKModule,
|
|
21
|
-
} from './proxy-client.js';
|
|
22
|
-
|
|
23
|
-
const MODULES: SDKModule[] = [
|
|
24
|
-
'product', 'app', 'databases', 'graph', 'webhooks', 'notifications',
|
|
25
|
-
'events', 'messageBrokers', 'storage', 'vector', 'caches', 'sessions', 'quotas',
|
|
26
|
-
'actions', 'features', 'jobs', 'logs', 'resilience', 'health', 'fallback', 'secrets',
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// ─── Exhaustive SDK Method & Params Reference ────────────────────────────────
|
|
32
|
-
// Built from a complete read of sdk/ts/src/index.ts (Ductape class public API).
|
|
33
|
-
// Each entry follows: [module].[method] → params array signature.
|
|
34
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
const METHOD_DOCS = `
|
|
37
|
-
━━━ TOOL SELECTION GUIDE ━━━
|
|
38
|
-
|
|
39
|
-
There are THREE categories of operations. Use the right tool for each:
|
|
40
|
-
|
|
41
|
-
0. ADMINISTRATIVE OPERATIONS (create/update products, apps, environments, cloud connections…)
|
|
42
|
-
These require an access key and CANNOT be done via ductape_execute (publishable key only).
|
|
43
|
-
→ Use ductape_cli instead. Examples:
|
|
44
|
-
ductape_cli("products list")
|
|
45
|
-
ductape_cli("products create --name \\"My Product\\" --tag my-product")
|
|
46
|
-
ductape_cli("cloud connections list")
|
|
47
|
-
ductape_cli("link --product my-product --env dev")
|
|
48
|
-
If the CLI is not installed, ductape_cli will return install instructions automatically.
|
|
49
|
-
NOTE: Environments and app actions are configured in the Workbench UI — there are no CLI commands for them.
|
|
50
|
-
|
|
51
|
-
DECLARATIVE SYNC (apply sessions, notifications, events from code; run DB migrations)
|
|
52
|
-
→ Also use ductape_cli. The project must be linked first (ductape init --link).
|
|
53
|
-
ductape_cli("apply") # sync all: sessions + notifications + events
|
|
54
|
-
ductape_cli("apply sessions") # sessions only
|
|
55
|
-
ductape_cli("apply notifications") # notifications only
|
|
56
|
-
ductape_cli("apply events") # event brokers only
|
|
57
|
-
ductape_cli("apply --dry-run") # preview without changes
|
|
58
|
-
ductape_cli("db schema generate") # diff schema.json → write migration files
|
|
59
|
-
ductape_cli("db schema generate --destructive") # also generate drop operations
|
|
60
|
-
ductape_cli("db migrate") # apply pending migration files
|
|
61
|
-
ductape_cli("db migrate --dry-run") # preview without applying
|
|
62
|
-
ductape_cli("db migrate --env prd") # apply to a specific environment
|
|
63
|
-
ductape_cli("db migrate status") # show applied vs pending migrations
|
|
64
|
-
ductape_cli("db migrate status --json") # machine-readable status
|
|
65
|
-
ductape_cli("db migrate rollback") # roll back last migration
|
|
66
|
-
ductape_cli("db migrate rollback -n 3") # roll back last 3 migrations
|
|
67
|
-
|
|
68
|
-
DATABASE SCHEMA FILE — ductape/database/schema.json
|
|
69
|
-
─────────────────────────────────────────────────────────────────────────
|
|
70
|
-
IMPORTANT: "db schema generate" READS schema.json and emits migration files.
|
|
71
|
-
It does NOT create schema.json from scratch. You must write this file first.
|
|
72
|
-
|
|
73
|
-
If schema.json does not exist, "db schema generate" now auto-creates it and
|
|
74
|
-
attempts to hydrate it from the cloud (pass --db <tag> to enable cloud hydration):
|
|
75
|
-
ductape_cli("db schema generate --db statecraft-core")
|
|
76
|
-
If the cloud database is empty, schema.json will be created as [] and you must
|
|
77
|
-
populate it before running the command again.
|
|
78
|
-
|
|
79
|
-
FILE FORMAT — ductape/database/schema.json is a JSON array:
|
|
80
|
-
[
|
|
81
|
-
{
|
|
82
|
-
"db": "<database-component-tag>",
|
|
83
|
-
"tables": {
|
|
84
|
-
"<collection_name>": {
|
|
85
|
-
"<field_name>": {
|
|
86
|
-
"type": "string|number|boolean|date|datetime|timestamp|uuid|object|array|json|text|decimal|bigint|smallint|double|binary|blob|time",
|
|
87
|
-
"required": true, // false by default (nullable)
|
|
88
|
-
"unique": true, // unique constraint on this field
|
|
89
|
-
"index": true, // create an index on this field (use with unique for unique index)
|
|
90
|
-
"default": "value", // default value ("now"/"NOW" → CURRENT_TIMESTAMP)
|
|
91
|
-
"maxlength": 255, // max length for string fields
|
|
92
|
-
"enum": ["a", "b", "c"], // restrict to enum values (sets type to string automatically)
|
|
93
|
-
"primaryKey": true, // mark as primary key
|
|
94
|
-
"autoGenerate": true, // auto-generate value on insert
|
|
95
|
-
"float": true // treat number as float (use with type: "number")
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
]
|
|
101
|
-
|
|
102
|
-
SINGLE-FIELD INDEX: set index: true (+ unique: true for unique constraint)
|
|
103
|
-
"matchId": { "type": "string", "required": true, "unique": true, "index": true }
|
|
104
|
-
|
|
105
|
-
COMPOSITE INDEX: schema.json does not support composite indexes.
|
|
106
|
-
After running db schema generate, add a manual migration file in
|
|
107
|
-
ductape/migrations/<db-tag>/ with type "createIndex" and a fields array:
|
|
108
|
-
{
|
|
109
|
-
"tag": "create_map_events_match_seq_idx",
|
|
110
|
-
"name": "Composite unique index on matchId+sequence",
|
|
111
|
-
"up": [{
|
|
112
|
-
"type": "createIndex",
|
|
113
|
-
"collection": "map_events",
|
|
114
|
-
"name": "map_events_matchId_sequence_idx",
|
|
115
|
-
"fields": [{ "name": "matchId" }, { "name": "sequence" }],
|
|
116
|
-
"unique": true,
|
|
117
|
-
"ifNotExists": true
|
|
118
|
-
}],
|
|
119
|
-
"down": [{ "type": "dropIndex", "collection": "map_events", "name": "map_events_matchId_sequence_idx", "ifExists": true }],
|
|
120
|
-
"createdAt": "<ISO timestamp>"
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
WORKFLOW:
|
|
124
|
-
1. Create/edit ductape/database/schema.json with your collection definitions
|
|
125
|
-
2. ductape_cli("db schema generate --db <tag>") → writes migration files
|
|
126
|
-
3. ductape_cli("db migrate --db <tag>") → applies pending migrations
|
|
127
|
-
4. Add manual migration files for composite/partial indexes not expressible in schema.json
|
|
128
|
-
|
|
129
|
-
1. ASSET CREATION / UPDATE (create, update, add, register… for ANY asset type)
|
|
130
|
-
ALL creation and update operations require an access key and CANNOT go through ductape_execute.
|
|
131
|
-
→ Use ductape_cli for every create/update operation. Examples:
|
|
132
|
-
ductape_cli("apps create --name \\"Email Service\\" --description \\"Transactional email\\"")
|
|
133
|
-
ductape_cli("apps list")
|
|
134
|
-
ductape_cli("resources storage list")
|
|
135
|
-
ductape_cli("resources database create -f db-config.json")
|
|
136
|
-
This applies to: products, apps, and resources (databases, storage, caches, etc.),
|
|
137
|
-
cloud connections, and secrets. Environments, app actions, auths, quotas, fallbacks,
|
|
138
|
-
jobs, and healthchecks are configured in the Workbench UI. Features have no CLI create
|
|
139
|
-
command because their definitions are code-first through features.define.
|
|
140
|
-
|
|
141
|
-
⚠ MULTI-ENV REQUIREMENT — applies to ALL product assets (storage, database, cache,
|
|
142
|
-
messageBroker, graph, vector, and any other resource with an envs array):
|
|
143
|
-
The envs array MUST contain one entry for EVERY environment defined on the product.
|
|
144
|
-
import-persist and provision-persist only generate a draft for ONE env at a time.
|
|
145
|
-
Calling import-persist once will fail with MISSING_ENV_COVERAGE because the single-env
|
|
146
|
-
draft does not cover all product environments. Use import-persist-all or
|
|
147
|
-
provision-persist-all instead — they accept an array of per-env inputs, import/provision
|
|
148
|
-
each env in parallel, merge the drafts, and persist once with full env coverage.
|
|
149
|
-
|
|
150
|
-
BEFORE constructing any resource file or initiating import/provision:
|
|
151
|
-
1. Run: ductape_cli("products environments list <product_tag> --json")
|
|
152
|
-
2. Note every slug returned (e.g. dev, snd, prd).
|
|
153
|
-
3. Collect connection details or confirmation for EACH slug from the user.
|
|
154
|
-
Do not proceed until you have details for every env.
|
|
155
|
-
|
|
156
|
-
Use import-persist-all / provision-persist-all (not the single-env forms) whenever
|
|
157
|
-
you have details for all envs. The file must be a JSON array — one object per env,
|
|
158
|
-
same format as a single import-persist/provision-persist input, all sharing the same
|
|
159
|
-
product and component tag. Example:
|
|
160
|
-
[{"cloud":"gcp-snd","service":"gcs","type":"storage","product":"my-prod",
|
|
161
|
-
"component":"assets","env":"snd","resource":"snd-bucket"},
|
|
162
|
-
{"cloud":"gcp-prd","service":"gcs","type":"storage","product":"my-prod",
|
|
163
|
-
"component":"assets","env":"prd","resource":"prd-bucket"}]
|
|
164
|
-
→ ductape_cli("cloud resources import-persist-all -f all-envs.json --json")
|
|
165
|
-
|
|
166
|
-
Sharing one resource across multiple envs is allowed but must be confirmed per env:
|
|
167
|
-
- Ask the user explicitly: "Should <env> use the same <resource> as <other_env>?"
|
|
168
|
-
- Never assume shared configuration without confirmation.
|
|
169
|
-
- For cloud-linked envs, set config.cloud to the connection tag for that env and
|
|
170
|
-
omit raw credentials; the cloud connection must exist for that env too.
|
|
171
|
-
|
|
172
|
-
2. RUNTIME OPERATIONS (run, dispatch, execute, start, send, produce, query, insert, update, delete…)
|
|
173
|
-
The "input" field shape is product- and operation-specific — it is NOT derivable from Joi validators.
|
|
174
|
-
It is defined by how the product's action/feature/session/quota/etc. was configured in Ductape.
|
|
175
|
-
→ ALWAYS call ductape_generate_payload first to get the canonical payload template, EXCEPT for
|
|
176
|
-
messaging (produce/consume/dispatch) — see the Events section for why.
|
|
177
|
-
→ The template shows you exactly which input keys are expected and their types/defaults.
|
|
178
|
-
→ Then fill in the values and pass the completed payload to ductape_execute.
|
|
179
|
-
→ Applies to: actions, features, sessions, notifications, databases, storage, graphs, vectors,
|
|
180
|
-
quotas, fallbacks, jobs, and any other operation that executes against a pre-configured schema.
|
|
181
|
-
|
|
182
|
-
Skipping ductape_generate_payload for applicable runtime operations will produce incorrect or empty input payloads.
|
|
183
|
-
Exception: messaging produce/consume/dispatch — the producer defines the schema, so infer from context instead.
|
|
184
|
-
|
|
185
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
186
|
-
|
|
187
|
-
━━━ NESTJS INTEGRATION — use @ductape/nestjs ━━━
|
|
188
|
-
|
|
189
|
-
When the target application is a NestJS service or controller, always use @ductape/nestjs
|
|
190
|
-
instead of instantiating @ductape/sdk directly. It provides NestJS DI integration,
|
|
191
|
-
global interceptors, decorators, and type-safe resource handles.
|
|
192
|
-
|
|
193
|
-
SETUP — register once in AppModule:
|
|
194
|
-
|
|
195
|
-
╔══════════════════════════════════════════════════════════════════════════╗
|
|
196
|
-
║ redisUrl IS REQUIRED TO USE ANY *.dispatch() ║
|
|
197
|
-
║ ║
|
|
198
|
-
║ Every dispatch() call (actions, features, events, databases, storage, ║
|
|
199
|
-
║ graph, notifications, quotas, fallback) enqueues jobs ║
|
|
200
|
-
║ via BullMQ over Redis. Without redisUrl the call throws at runtime: ║
|
|
201
|
-
║ "Queues not configured. dispatch() requires a queue connection." ║
|
|
202
|
-
║ ║
|
|
203
|
-
║ *.run(), events.produce(), and @Events.Consumer do NOT need Redis. ║
|
|
204
|
-
║ Only dispatch() does — and it is non-negotiable. ║
|
|
205
|
-
╚══════════════════════════════════════════════════════════════════════════╝
|
|
206
|
-
|
|
207
|
-
import { DuctapeModule } from '@ductape/nestjs';
|
|
208
|
-
|
|
209
|
-
@Module({
|
|
210
|
-
imports: [
|
|
211
|
-
DuctapeModule.forIntegration({
|
|
212
|
-
accessKey: process.env.DUCTAPE_ACCESS_KEY,
|
|
213
|
-
product: 'my-product',
|
|
214
|
-
env: process.env.NODE_ENV === 'production' ? 'prd' : 'snd',
|
|
215
|
-
redisUrl: process.env.DUCTAPE_REDIS_URL, // required — no dispatch() works without this
|
|
216
|
-
}),
|
|
217
|
-
],
|
|
218
|
-
})
|
|
219
|
-
export class AppModule {}
|
|
220
|
-
|
|
221
|
-
// Async (e.g. pulling from ConfigService):
|
|
222
|
-
DuctapeModule.forRootAsync({
|
|
223
|
-
imports: [ConfigModule],
|
|
224
|
-
inject: [ConfigService],
|
|
225
|
-
useFactory: (cfg: ConfigService) => ({
|
|
226
|
-
accessKey: cfg.get('DUCTAPE_ACCESS_KEY'),
|
|
227
|
-
product: cfg.get('DUCTAPE_PRODUCT'),
|
|
228
|
-
env: cfg.get('DUCTAPE_ENV'),
|
|
229
|
-
redisUrl: cfg.get('DUCTAPE_REDIS_URL'), // required — no dispatch() works without this
|
|
230
|
-
}),
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
Environment variable (add to .env and deployment secrets):
|
|
234
|
-
DUCTAPE_REDIS_URL=redis://localhost:6379 # local dev
|
|
235
|
-
DUCTAPE_REDIS_URL=rediss://:<password>@host:6380 # managed Redis (TLS)
|
|
236
|
-
|
|
237
|
-
INJECTING IN SERVICES AND CONTROLLERS — use @InjectContext():
|
|
238
|
-
|
|
239
|
-
import { InjectContext, DuctapeContext } from '@ductape/nestjs';
|
|
240
|
-
|
|
241
|
-
@Injectable()
|
|
242
|
-
export class MatchService {
|
|
243
|
-
constructor(@InjectContext() private readonly ductape: DuctapeContext) {}
|
|
244
|
-
|
|
245
|
-
async publishMatchState(matchId: string, state: object) {
|
|
246
|
-
await this.ductape.sdk.events.produce({
|
|
247
|
-
product: 'my-product',
|
|
248
|
-
env: 'prd',
|
|
249
|
-
event: 'statecraft-events:match-state',
|
|
250
|
-
message: { matchId, ...state },
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async queryDatabase(env: string) {
|
|
255
|
-
const db = await this.ductape.database('core-db', { env });
|
|
256
|
-
return db.query({ table: 'matches', where: { active: true } });
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
AVAILABLE ON DuctapeContext:
|
|
261
|
-
|
|
262
|
-
ctx.sdk — full @ductape/sdk instance (events, sessions, etc.)
|
|
263
|
-
ctx.database(tag, overrides?) — connected DatabaseHandle (query, insert, update, delete)
|
|
264
|
-
ctx.storage(tag, overrides?) — StorageHandle (upload, download, remove)
|
|
265
|
-
ctx.cache(tag, overrides?) — CacheHandle (get, set, delete)
|
|
266
|
-
ctx.graph(tag, overrides?) — GraphHandle
|
|
267
|
-
ctx.vector(tag, overrides?) — VectorHandle
|
|
268
|
-
ctx.agent(tag, overrides?) — AgentHandle
|
|
269
|
-
ctx.warehouse(overrides?) — WarehouseHandle
|
|
270
|
-
ctx.cloudTiers(query?) — query available cloud resource tiers and pricing
|
|
271
|
-
ctx.runJob({ product, env, event, input? }) — trigger a product job
|
|
272
|
-
|
|
273
|
-
CONTROLLER DECORATORS:
|
|
274
|
-
|
|
275
|
-
@Product('my-product') — override default product at class or method level
|
|
276
|
-
@Env('prd') — override default env
|
|
277
|
-
@ApiRun({ app, action }) — bind a controller method to an app action (interceptor runs it automatically)
|
|
278
|
-
@ApiConfig({ app, ... }) — shared app credentials at class level
|
|
279
|
-
@Webhook.Register(...) — register a webhook consumer URL
|
|
280
|
-
@Webhook.Consumer(...) — mark inbound handler for forwarded webhook payloads
|
|
281
|
-
|
|
282
|
-
FOR MESSAGING (events.produce / events.consume / events.dispatch):
|
|
283
|
-
|
|
284
|
-
In @ductape/nestjs, inject DuctapeContextService (not raw Ductape):
|
|
285
|
-
constructor(private readonly ductape: DuctapeContextService) {}
|
|
286
|
-
|
|
287
|
-
Produce immediately (method decorator — returns payload as message):
|
|
288
|
-
@Events.Produce({ event: 'broker-tag:topic-tag' })
|
|
289
|
-
emitOrderCreated(payload: { orderId: string }) { return payload; }
|
|
290
|
-
|
|
291
|
-
Dispatch (scheduled or immediate) — dispatch() CANNOT be used without redisUrl in forIntegration:
|
|
292
|
-
@Events.Dispatch({ broker: 'order-events', event: 'order-events:order-created', schedule: { every: 60000 } })
|
|
293
|
-
dispatchHeartbeat(payload: { message: { ping: boolean } }) { return payload; }
|
|
294
|
-
|
|
295
|
-
Dispatch with dynamic schedule — method returns { message, schedule?, retries? }:
|
|
296
|
-
@Events.Dispatch({ broker: 'statecraft-events', event: 'statecraft-events:boundary-due' })
|
|
297
|
-
scheduleBoundary(match: MatchLifecycle) {
|
|
298
|
-
return {
|
|
299
|
-
message: buildBoundaryCommand(match),
|
|
300
|
-
schedule: { start_at: match.nextBoundaryAt },
|
|
301
|
-
retries: 5,
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
// method return takes precedence over decorator schedule; use sdk.events.dispatch() directly
|
|
305
|
-
// when even the broker/event must vary at call time.
|
|
306
|
-
// ALL *.dispatch() calls require redisUrl — BullMQ over Redis. Without it dispatch throws.
|
|
307
|
-
|
|
308
|
-
Consume (method decorator — method is called for each incoming message):
|
|
309
|
-
@Events.Consumer({ event: 'order-events:order-created' })
|
|
310
|
-
async onOrderCreated(message: { orderId: string; total: number }) {
|
|
311
|
-
await this.processOrder(message);
|
|
312
|
-
// return to ack; throw to nack
|
|
313
|
-
}
|
|
314
|
-
// DuctapeEventsConsumerService wires this up automatically at module init.
|
|
315
|
-
// No manual onModuleInit needed when using the decorator.
|
|
316
|
-
|
|
317
|
-
Low-level SDK access (for produce only — not needed for consume with the decorator):
|
|
318
|
-
await this.ductape.sdk.events.produce({
|
|
319
|
-
product: 'my-product', env: 'prd',
|
|
320
|
-
event: 'broker-tag:topic-tag',
|
|
321
|
-
message: { ... },
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
SPECIALIZED MODULES (for injecting handles directly without @InjectContext):
|
|
325
|
-
|
|
326
|
-
DuctapeDatabaseModule.forTags(['core-db']) → inject with @Database('core-db')
|
|
327
|
-
DuctapeStorageModule.forTags(['assets']) → inject with @Storage('assets')
|
|
328
|
-
DuctapeCacheModule, DuctapeGraphModule, DuctapeVectorModule — same pattern
|
|
329
|
-
|
|
330
|
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
331
|
-
|
|
332
|
-
ALL params are passed as a JSON array in positional order matching the SDK signature.
|
|
333
|
-
|
|
334
|
-
━━━ MODULE: product ━━━
|
|
335
|
-
IMPORTANT: ALL product.* methods require the access key and will return 403 with a publishable key.
|
|
336
|
-
Use ductape_cli for ALL product operations — never ductape_execute:
|
|
337
|
-
ductape_cli("products get --tag <tag> --json") ← fetch product + full inventory
|
|
338
|
-
ductape_cli("products components list --tag <tag> --json") ← compact non-secret inventory
|
|
339
|
-
ductape_cli("products components get --tag <tag> --type notifications --json")
|
|
340
|
-
ductape_cli("products components get --tag <tag> --type events --json")
|
|
341
|
-
ductape_cli("products create --name <name> --tag <tag>")
|
|
342
|
-
ductape_cli("products environments list <tag> --json")
|
|
343
|
-
ductape_cli("products environments get <tag> <slug> --json")
|
|
344
|
-
ductape_cli("products apps list --product <id> --json")
|
|
345
|
-
|
|
346
|
-
SDK method signatures (for reference, admin key only):
|
|
347
|
-
product.create [data: { name, description, tag?, envs?: [{slug, name}] }]
|
|
348
|
-
product.fetch [product_tag: string]
|
|
349
|
-
product.update [product_tag: string, data: { name?: string, description?: string }]
|
|
350
|
-
product.environments.create [product_tag, data: { slug, env_name, description, active? }]
|
|
351
|
-
product.environments.list [product_tag]
|
|
352
|
-
product.environments.fetch [product_tag, slug]
|
|
353
|
-
product.apps.add [product_tag, app: { access_tag, envs: [{ app_env_slug, product_env_slug, variables?, auth? }] }]
|
|
354
|
-
product.apps.list [product_tag]
|
|
355
|
-
product.apps.fetch [product_tag, access_tag]
|
|
356
|
-
|
|
357
|
-
━━━ MODULE: app ━━━
|
|
358
|
-
app.create [data: { app_name: string, description: string, unique?: boolean }]
|
|
359
|
-
app.fetch [app_tag]
|
|
360
|
-
app.update [app_tag, data: { app_name?: string, description?: string, logo?: string, colors?: { primary?: string, secondary?: string, accent?: string, background?: string }, require_whitelist?: boolean, aboutText?: string, aboutHTML?: string, request_type?: string }]
|
|
361
|
-
app.init [app_tag]
|
|
362
|
-
|
|
363
|
-
━━━ MODULE: actions (app actions) ━━━
|
|
364
|
-
actions.create [app_tag, data: { tag: string, name: string, resource: string, method: "GET"|"POST"|"PUT"|"PATCH"|"DELETE", description?: string, body?: object, params?: object, query?: object, headers?: object, response?: { name?: string, status_code: number, success: boolean, body: object, response_format: "json"|"xml"|"form" } }]
|
|
365
|
-
actions.update [app_tag, action_tag, data: { resource?: string, method?: "GET"|"POST"|"PUT"|"PATCH"|"DELETE", description?: string, request_type?: "json"|"xml"|"form", body?: object, query?: object, params?: object, headers?: object, response?: { name?: string, success: boolean, body: object, response_format: "json"|"xml"|"form", status_code: number } }]
|
|
366
|
-
actions.fetch [app_tag, action_tag]
|
|
367
|
-
actions.list [app_tag]
|
|
368
|
-
actions.run [{ product, env, app, action, input: { "body:fieldName": value, ... } }] ← CALL ductape_generate_payload FIRST (operation_family="action", method="run", targets={app, action})
|
|
369
|
-
actions.dispatch [{ product, env, app, action, input, retries?, session?, cache?, schedule?: { start_at?, cron?, every?, limit?, tz? } }] ← CALL ductape_generate_payload FIRST (operation_family="action", method="dispatch") — requires redisUrl in ductape initialization
|
|
370
|
-
|
|
371
|
-
━━━ MODULE: auths ━━━
|
|
372
|
-
auths.create [app_tag, data: { tag: string, name: string, setup_type: "header"|"bearer"|"basic"|"oauth2"|"apikey", expiry: number, period: "seconds"|"minutes"|"hours"|"days", description: string, action_tag?: string }]
|
|
373
|
-
auths.update [app_tag, auth_tag, data: { name?: string, setup_type?: "header"|"bearer"|"basic"|"oauth2"|"apikey", expiry?: number, period?: "seconds"|"minutes"|"hours"|"days", description?: string, action_tag?: string }]
|
|
374
|
-
auths.fetch [app_tag, auth_tag]
|
|
375
|
-
auths.list [app_tag]
|
|
376
|
-
|
|
377
|
-
━━━ MODULE: webhooks ━━━
|
|
378
|
-
webhooks.create [app_tag, data: { tag: string, name: string, description: string, envs?: [{ slug: string, registration_url?: string, method?: "GET"|"POST"|"PUT"|"PATCH", sample?: object }] }]
|
|
379
|
-
webhooks.update [app_tag, webhook_tag, data: { name?: string, description?: string, envs?: [{ slug: string, registration_url?: string, method?: "GET"|"POST"|"PUT"|"PATCH", sample?: object }] }]
|
|
380
|
-
webhooks.fetch [app_tag, webhook_tag]
|
|
381
|
-
webhooks.list [app_tag]
|
|
382
|
-
webhooks.events.create [app_tag, data: { tag: string, name: string, selector: string, description: string, sample: object }]
|
|
383
|
-
webhooks.events.update [app_tag, event_tag, data: { name?: string, selector?: string, selectorValue?: string, description?: string, sample?: object }]
|
|
384
|
-
webhooks.events.fetch [app_tag, event_tag]
|
|
385
|
-
webhooks.events.list [app_tag, webhook_tag]
|
|
386
|
-
|
|
387
|
-
━━━ MODULE: sessions ━━━
|
|
388
|
-
sessions.create [product_tag, data: { tag: string, name: string, description?: string, expiry: number, period: "seconds"|"minutes"|"hours"|"days", selector: string, schema: Record<string, unknown> }]
|
|
389
|
-
← selector MUST be in the format "$Session{fieldName}" (e.g. "$Session{playerId}"). Plain dot-paths are rejected.
|
|
390
|
-
← schema is SAMPLE DATA — actual example values, not type declarations. The value at the selector path must be a primitive (string|number|boolean), not an object.
|
|
391
|
-
sessions.update [product_tag, session_tag, data: { name?: string, description?: string, expiry?: number, period?: "seconds"|"minutes"|"hours"|"days", selector?: string, schema?: object }]
|
|
392
|
-
sessions.fetch [product_tag, session_tag]
|
|
393
|
-
sessions.list [product_tag]
|
|
394
|
-
sessions.delete [product_tag, session_tag]
|
|
395
|
-
sessions.start [{ product, env, tag, data: { key: value } }] ← CALL ductape_generate_payload FIRST (operation_family="session", method="start", targets={tag}) to discover the data field shape
|
|
396
|
-
sessions.verify [{ product, env, tag, token }]
|
|
397
|
-
sessions.refresh [{ product, env, tag, refreshToken }]
|
|
398
|
-
sessions.revoke [{ product, env, tag, sessionId?, identifier? }]
|
|
399
|
-
sessions.listActive [{ product, env, tag, identifier?, page?, limit? }]
|
|
400
|
-
sessions.fetchUsers [{ product, session, env?, page?, limit? }]
|
|
401
|
-
sessions.fetchUserDetails [{ product, session, identifier, env? }]
|
|
402
|
-
sessions.fetchDashboard [{ product, session, env? }]
|
|
403
|
-
|
|
404
|
-
━━━ MODULE: quotas ━━━
|
|
405
|
-
quotas.create [product_tag, data: {
|
|
406
|
-
tag: string, // unique identifier
|
|
407
|
-
name?: string, // display name
|
|
408
|
-
description?: string,
|
|
409
|
-
|
|
410
|
-
// INPUT SCHEMA (top-level): declares what fields the quota accepts when called at runtime.
|
|
411
|
-
// These are the fields callers will pass to quotas.run / quotas.dispatch.
|
|
412
|
-
// Construct this yourself — it is a schema declaration, not a runtime value.
|
|
413
|
-
input: {
|
|
414
|
-
fieldName: {
|
|
415
|
-
type: "string"|"number"|"boolean"|"object"|"array",
|
|
416
|
-
required?: boolean,
|
|
417
|
-
description?: string
|
|
418
|
-
}
|
|
419
|
-
},
|
|
420
|
-
|
|
421
|
-
options: [ // list of providers tried in order
|
|
422
|
-
{
|
|
423
|
-
provider?: string, // friendly name for this provider slot
|
|
424
|
-
app: string, // app tag whose action to call
|
|
425
|
-
type: "action",
|
|
426
|
-
event: string, // action tag on that app
|
|
427
|
-
quota: number, // max allowed uses for this provider
|
|
428
|
-
uses?: number, // current use count (usually 0 at creation)
|
|
429
|
-
|
|
430
|
-
// OPTIONS INPUT (per provider): maps the quota's declared input fields → the underlying action's fields.
|
|
431
|
-
// ← CALL ductape_generate_payload (operation_family="action", method="run", targets={app, event})
|
|
432
|
-
// to discover what fields the action accepts, then wire them with "$Input{fieldName}" references.
|
|
433
|
-
input: { "body:field": "$Input{fieldName}" },
|
|
434
|
-
|
|
435
|
-
output: {}, // expected output shape (can be {})
|
|
436
|
-
retries: number,
|
|
437
|
-
healthcheck?: string, // optional healthcheck tag to gate this provider
|
|
438
|
-
check_interval?: number // ms between health polls
|
|
439
|
-
}
|
|
440
|
-
]
|
|
441
|
-
}]
|
|
442
|
-
quotas.update [product_tag, quota_tag, data: { name?: string, description?: string, options?: array }]
|
|
443
|
-
quotas.fetch [product_tag, quota_tag]
|
|
444
|
-
quotas.list [product_tag]
|
|
445
|
-
quotas.delete [product_tag, quota_tag]
|
|
446
|
-
quotas.run [{ product: string, env: string, tag: string, input: { fieldName: value }, session?: string, cache?: string }] ← CALL ductape_generate_payload FIRST (operation_family="quota", method="run", targets={tag})
|
|
447
|
-
quotas.dispatch [{ product: string, env: string, tag: string, input: object, session?: string, cache?: string, schedule?: { cron?: string, delay?: number, at?: string } }] ← CALL ductape_generate_payload FIRST (operation_family="quota", method="dispatch") — requires redisUrl in ductape initialization
|
|
448
|
-
|
|
449
|
-
━━━ MODULE: fallback ━━━
|
|
450
|
-
fallback.create [product_tag, data: {
|
|
451
|
-
tag: string,
|
|
452
|
-
name?: string,
|
|
453
|
-
description?: string,
|
|
454
|
-
|
|
455
|
-
// INPUT SCHEMA (top-level): declares what fields the fallback accepts when called at runtime.
|
|
456
|
-
// These are the fields callers will pass to fallback.run / fallback.dispatch.
|
|
457
|
-
// Construct this yourself — it is a schema declaration, not a runtime value.
|
|
458
|
-
input: {
|
|
459
|
-
fieldName: {
|
|
460
|
-
type: "string"|"number"|"boolean"|"object"|"array",
|
|
461
|
-
required?: boolean,
|
|
462
|
-
description?: string
|
|
463
|
-
}
|
|
464
|
-
},
|
|
465
|
-
|
|
466
|
-
options: [ // ordered list: primary first, then fallback(s)
|
|
467
|
-
{
|
|
468
|
-
provider?: string, // friendly name e.g. "primary", "backup"
|
|
469
|
-
app: string, // app tag
|
|
470
|
-
type: "action",
|
|
471
|
-
event: string, // action tag
|
|
472
|
-
|
|
473
|
-
// OPTIONS INPUT (per provider): maps the fallback's declared input fields → the underlying action's fields.
|
|
474
|
-
// ← CALL ductape_generate_payload (operation_family="action", method="run", targets={app, event})
|
|
475
|
-
// to discover what fields the action accepts, then wire them with "$Input{fieldName}" references.
|
|
476
|
-
input: { "body:field": "$Input{fieldName}" },
|
|
477
|
-
|
|
478
|
-
output: {},
|
|
479
|
-
retries: number,
|
|
480
|
-
healthcheck?: string,
|
|
481
|
-
check_interval?: number
|
|
482
|
-
}
|
|
483
|
-
]
|
|
484
|
-
}]
|
|
485
|
-
fallback.update [product_tag, fallback_tag, data: { name?: string, description?: string, options?: array }]
|
|
486
|
-
fallback.fetch [product_tag, fallback_tag]
|
|
487
|
-
fallback.list [product_tag]
|
|
488
|
-
fallback.delete [product_tag, fallback_tag]
|
|
489
|
-
fallback.run [{ product: string, env: string, tag: string, input: { fieldName: value }, session?: string, cache?: string }] ← CALL ductape_generate_payload FIRST (operation_family="fallback", method="run", targets={tag})
|
|
490
|
-
fallback.dispatch [{ product: string, env: string, tag: string, input: object, session?: string, cache?: string, schedule?: { cron?: string, delay?: number, at?: string } }] ← CALL ductape_generate_payload FIRST (operation_family="fallback", method="dispatch") — requires redisUrl in ductape initialization
|
|
491
|
-
|
|
492
|
-
━━━ MODULE: health ━━━
|
|
493
|
-
health.create [product_tag, data: { tag: string, name: string, description?: string, app?: string, event?: string, probe?: { type: "app"|"database"|"feature", app?: string, event?: string, input?: object }, interval: number, retries: number, envs: [{ slug: string, input?: object }], onFailure?: { notifications?: [{ notification: string, message: string, channels: { email?: { recipients: string[] }, push?: { recipients?: string[] }, sms?: { recipients: string[] } } }], webhooks?: [{ url: string, method?: "GET"|"POST", headers?: object, body?: object }] } }]
|
|
494
|
-
health.update [product_tag, health_tag, data: { name?: string, description?: string, app?: string, event?: string, probe?: { type: "app"|"database"|"feature", app?: string, event?: string, input?: object }, interval?: number, retries?: number, envs?: [{ slug: string, input?: object }], onFailure?: object }]
|
|
495
|
-
health.fetch [product_tag, health_tag]
|
|
496
|
-
health.list [product_tag]
|
|
497
|
-
health.delete [product_tag, health_tag]
|
|
498
|
-
health.status [{ product, env, tag }]
|
|
499
|
-
health.run [{ product, env, tag }]
|
|
500
|
-
health.check [{ product, env, tag }]
|
|
501
|
-
|
|
502
|
-
━━━ MODULE: notifications ━━━
|
|
503
|
-
notifications.create [product_tag, data: { tag: string, name: string, type?: "email"|"push"|"sms"|"callback" }]
|
|
504
|
-
notifications.update [product_tag, notif_tag, data: { name?: string, type?: "email"|"push"|"sms"|"callback" }]
|
|
505
|
-
notifications.fetch [product_tag, notif_tag]
|
|
506
|
-
notifications.list [product_tag]
|
|
507
|
-
notifications.delete [product_tag, notif_tag]
|
|
508
|
-
notifications.messages.create [product_tag, data: { tag: string, name: string, description?: string, push_notification?: { title: string, body: string, data?: object }, email?: { subject: string, template: string }, callback?: object, sms?: string }]
|
|
509
|
-
notifications.messages.update [product_tag, msg_tag, data: { name?: string, description?: string, push_notification?: { title: string, body: string, data?: object }, email?: { subject: string, template: string }, callback?: object, sms?: string }]
|
|
510
|
-
notifications.messages.fetch [product_tag, msg_tag]
|
|
511
|
-
notifications.messages.list [product_tag, notification_tag]
|
|
512
|
-
notifications.send [{ product, env, event, input: { ... } }] ← CALL ductape_generate_payload FIRST (operation_family="notification", method="send", targets={notification})
|
|
513
|
-
notifications.email.send [{ product, env, notification, input: { ... }, session?, cache? }] ← CALL ductape_generate_payload FIRST (operation_family="notification", method="email.send")
|
|
514
|
-
notifications.push.send [{ product, env, notification, input: { ... }, session?, cache? }] ← CALL ductape_generate_payload FIRST (operation_family="notification", method="push.send")
|
|
515
|
-
notifications.sms.send [{ product, env, notification, input: { ... }, session?, cache? }] ← CALL ductape_generate_payload FIRST (operation_family="notification", method="sms.send")
|
|
516
|
-
notifications.callback.send [{ product, env, notification, input: { ... }, session?, cache? }] ← CALL ductape_generate_payload FIRST (operation_family="notification", method="callback.send")
|
|
517
|
-
notifications.slack.send [{ product, env, notification, input: { text?, blocks?, channel? }, session?, cache? }]
|
|
518
|
-
notifications.discord.send [{ product, env, notification, input: { content?, embeds? }, session?, cache? }]
|
|
519
|
-
notifications.dispatch [{ product, env, notification, event, input, retries?, session?, cache?, schedule?: { cron?, every?, start_at? } }] ← CALL ductape_generate_payload FIRST (operation_family="notification", method="dispatch") — requires redisUrl in ductape initialization
|
|
520
|
-
notifications.getMessages [{ product_tag?, env?, notification_tag?, status?, type?, start_date?, end_date?, page?, limit? }]
|
|
521
|
-
|
|
522
|
-
━━━ MODULE: events (alias: messageBrokers — both accepted; events matches the TS SDK naming) ━━━
|
|
523
|
-
events.create [{ product: string, tag: string, name: string, description?: string, type: "kafka"|"rabbitmq"|"redis"|"sqs", envs: [{ slug: string, connection_url: string }] }]
|
|
524
|
-
events.update [product_tag, broker_tag, data: { name?: string, description?: string, type?: "kafka"|"rabbitmq"|"redis"|"sqs", envs?: [{ slug: string, connection_url: string }] }]
|
|
525
|
-
events.fetch [product_tag, broker_tag]
|
|
526
|
-
events.list [product_tag]
|
|
527
|
-
events.delete [product_tag, broker_tag]
|
|
528
|
-
events.topics.create ← FORBIDDEN with publishable key. Use ductape_cli instead:
|
|
529
|
-
ductape_cli("events topics create -f topic.json")
|
|
530
|
-
topic.json: { tag: "broker-tag:topic-tag", name, description?, sample?, idempotent?, queueUrls?: [{ env_slug, url }] }
|
|
531
|
-
← Always required before consuming. For SQS: must include queueUrls per env.
|
|
532
|
-
← For Pub/Sub, Kafka, RabbitMQ, Redis, NATS: the first produce call auto-registers the topic,
|
|
533
|
-
but you should still create it explicitly so consumers can subscribe before any produce occurs.
|
|
534
|
-
events.topics.update ← FORBIDDEN with publishable key. Use ductape_cli:
|
|
535
|
-
ductape_cli("events topics update --tag broker:topic -f patch.json")
|
|
536
|
-
events.topics.delete ← FORBIDDEN with publishable key. Use ductape_cli:
|
|
537
|
-
ductape_cli("events topics delete --tag broker:topic")
|
|
538
|
-
events.topics.fetch [product_tag, topic_tag] ← safe via ductape_execute
|
|
539
|
-
events.topics.list [product_tag, broker_tag] ← safe via ductape_execute
|
|
540
|
-
events.produce [{ product, env, event: "broker_tag:topic_tag", message: { key: value }, session?, cache? }]
|
|
541
|
-
events.consume [{ product, env, event: "broker_tag:topic_tag", callback: "function_ref" }]
|
|
542
|
-
events.dispatch [{ product, env, event: "broker_tag:topic_tag", input: { message }, retries?, session?, cache?, schedule?: { cron?, every?, start_at? } }] — event is always the fully-qualified "broker:topic" string; do NOT pass broker separately — requires redisUrl in ductape initialization
|
|
543
|
-
events.messages.query [{ product, env, brokerTag, topicTag?, producerTag?, consumerTag?, status?, startDate?, endDate?, page?, limit? }]
|
|
544
|
-
events.messages.getProducers [{ product, env, brokerTag, topicTag?, page?, limit? }]
|
|
545
|
-
events.messages.getConsumers [{ product, env, brokerTag, topicTag?, page?, limit? }]
|
|
546
|
-
events.messages.getDeadLetters [{ product, env, brokerTag, topicTag?, consumerTag?, startDate?, endDate?, page?, limit? }]
|
|
547
|
-
events.messages.getStats [{ product, env, brokerTag }]
|
|
548
|
-
events.messages.getDashboard [{ product, env, brokerTag }]
|
|
549
|
-
|
|
550
|
-
━━━ MODULE: storage ━━━
|
|
551
|
-
storage.create [{ product: string, tag: string, name: string, description?: string, envs: [{ slug: string, type: "aws"|"azure"|"gcp", config: { bucket?: string, region?: string, accessKeyId?: string, secretAccessKey?: string, containerName?: string, connectionString?: string, projectId?: string, keyFilename?: string } }] }]
|
|
552
|
-
storage.update [product_tag, storage_tag, data: { name?: string, description?: string, envs?: [{ slug: string, type: "aws"|"azure"|"gcp", config: { bucket?: string, region?: string, accessKeyId?: string, secretAccessKey?: string } }] }]
|
|
553
|
-
storage.fetch [product_tag, storage_tag]
|
|
554
|
-
storage.list [product_tag]
|
|
555
|
-
storage.delete [product_tag, storage_tag]
|
|
556
|
-
storage.upload [{ product, env, storage, fileName, buffer: string|Buffer, mimeType? }]
|
|
557
|
-
storage.download [{ product, env, storage, fileName }]
|
|
558
|
-
storage.remove [{ product, env, storage, fileName }]
|
|
559
|
-
storage.listFiles [{ product, env, storage, prefix?, limit?, continuationToken? }]
|
|
560
|
-
storage.getSignedUrl [{ product, env, storage, fileName, expiresIn?: number, action?: "read"|"write" }]
|
|
561
|
-
storage.stats [{ product, env, storage, prefix?, session?, cache? }]
|
|
562
|
-
storage.testConnection [{ product, env, storage }]
|
|
563
|
-
storage.files.upload [{ product, env, storage, fileName, buffer, mimeType? }]
|
|
564
|
-
storage.files.download [{ product, env, storage, fileName }]
|
|
565
|
-
storage.files.delete [{ product, env, storage, fileName }]
|
|
566
|
-
storage.files.list [{ product, env, storage, prefix?, limit?, continuationToken? }]
|
|
567
|
-
storage.files.getSignedUrl [{ product, env, storage, fileName, expiresIn?, action? }]
|
|
568
|
-
storage.dispatch [{ product, env, storage, operation, input, retries?, session?, cache?, schedule? }] ← CALL ductape_generate_payload FIRST (operation_family="storage", method="dispatch", targets={storage}) — requires redisUrl in ductape initialization
|
|
569
|
-
|
|
570
|
-
━━━ MODULE: databases ━━━
|
|
571
|
-
databases.create [{ product, tag, name, description?, type: "mongodb"|"postgresql"|"mysql"|"sqlite",
|
|
572
|
-
envs: [{ slug, connection_url?,
|
|
573
|
-
cloud?, ← workspace cloud-connection tag (omit connection_url when using this)
|
|
574
|
-
authMode?, ← "cloud_connection" when using cloud field
|
|
575
|
-
dbName?, ← REQUIRED for MongoDB cloud connections — the database name inside the cluster (e.g. "myapp_snd")
|
|
576
|
-
instance?, ← RDS/Cloud SQL instance id (provision flow only)
|
|
577
|
-
region? }] }]
|
|
578
|
-
MONGODB CLOUD CONNECTION RULE: when type="mongodb" and cloud is set, dbName is MANDATORY.
|
|
579
|
-
Without dbName the Atlas API returns a URL with no database name and the adapter will throw
|
|
580
|
-
at migrate/connect time. Provide one dbName per env — they typically differ per environment:
|
|
581
|
-
e.g. { slug:"snd", cloud:"atlas-conn", authMode:"cloud_connection", dbName:"myapp_snd" }
|
|
582
|
-
{ slug:"prd", cloud:"atlas-conn", authMode:"cloud_connection", dbName:"myapp_prd" }
|
|
583
|
-
databases.register [product_tag, data: IProductDatabase]
|
|
584
|
-
databases.list [product_tag]
|
|
585
|
-
databases.fetch [product_tag, database_tag]
|
|
586
|
-
databases.updateDatabase [product_tag, database_tag, data: { name?, description?, type?,
|
|
587
|
-
envs?: [{ slug, connection_url?, cloud?, authMode?, dbName?, instance?, region? }] }]
|
|
588
|
-
databases.connect [{ product, env, database }]
|
|
589
|
-
databases.testConnection [{ product, env, database }]
|
|
590
|
-
databases.disconnect []
|
|
591
|
-
databases.query [{ product, env, database, table, where?: {field: value}, select?: string[], orderBy?: [{field, order:"ASC"|"DESC"}], limit?, offset?, session?, cache? }] ← CALL ductape_generate_payload FIRST (operation_family="database", method="query", targets={database, table})
|
|
592
|
-
databases.insert [{ product, env, database, table, data: {key: value}|{key:value}[], returning?, session?, cache? }] ← CALL ductape_generate_payload FIRST (operation_family="database", method="insert", targets={database, table})
|
|
593
|
-
databases.update [{ product, env, database, table, data: {key:value}, where: {field: value}, returning?, session?, cache? }] ← CALL ductape_generate_payload FIRST (operation_family="database", method="update", targets={database, table})
|
|
594
|
-
databases.delete [{ product, env, database, table, where: {field: value}, returning?, session?, cache? }] ← CALL ductape_generate_payload FIRST (operation_family="database", method="delete", targets={database, table})
|
|
595
|
-
databases.upsert [{ product, env, database, table, data: {key:value}, conflictKeys: string[], returning?, session?, cache? }] ← CALL ductape_generate_payload FIRST (operation_family="database", method="upsert", targets={database, table})
|
|
596
|
-
NOTE on write operations (insert/update/delete/upsert): if the proxy returns "Authentication failed",
|
|
597
|
-
the publishable key does not have write access for that database. Write permissions are configured
|
|
598
|
-
in Workbench → Tokens → Publishable Key. An access key is required for write operations that are
|
|
599
|
-
not explicitly enabled for the publishable key. In that case the operation must be performed
|
|
600
|
-
server-side using a full Ductape SDK instance initialized with an access key.
|
|
601
|
-
NOTE: ductape_generate_payload for databases returns:
|
|
602
|
-
payload.input — ready-to-use input with real field names in where/data, plus session and cache inside input
|
|
603
|
-
meta.schema_context.database.fields — { fieldName: { type, required, description?, sample? } } for the table
|
|
604
|
-
meta.schema_context.database.available_tables — list of all tables that have configured actions
|
|
605
|
-
meta.schema_warnings — array of warning strings; if it contains "No table schema discovered..." it means no
|
|
606
|
-
migrations or schema have been synced to the server yet. To fix this, tell the user:
|
|
607
|
-
"Run: ductape db schema push --db <database_tag> --env <env_slug>
|
|
608
|
-
This reads the live table schema from the database and syncs it to the Ductape server,
|
|
609
|
-
enabling accurate field guidance for AI operations. Once complete, retry your request."
|
|
610
|
-
databases.count [{ product, env, database, entity, where? }]
|
|
611
|
-
databases.sum [{ product, env, database, entity, field, where? }]
|
|
612
|
-
databases.avg [{ product, env, database, entity, field, where? }]
|
|
613
|
-
databases.min [{ product, env, database, entity, field, where? }]
|
|
614
|
-
databases.max [{ product, env, database, entity, field, where? }]
|
|
615
|
-
databases.aggregate [{ product, env, database, entity, aggregations: [{type:"count"|"sum"|"avg"|"min"|"max", field?, alias}], where?, groupBy? }]
|
|
616
|
-
databases.schema.create [collection_name, definition: { fieldName: "string"|"number"|"boolean"|"date"|{type, required?, unique?, default?} }, options?]
|
|
617
|
-
databases.schema.drop [collection_name, options?]
|
|
618
|
-
databases.schema.addField [collection, fieldName, definition]
|
|
619
|
-
databases.schema.dropField [collection, fieldName]
|
|
620
|
-
databases.schema.renameField [collection, oldName, newName]
|
|
621
|
-
databases.schema.modifyField [collection, fieldName, changes: Partial<{type, required, unique, default}>]
|
|
622
|
-
databases.schema.createIndex [collection, fields: string[]|[{field, order}], options?: {unique?, name?}]
|
|
623
|
-
databases.schema.dropIndex [collection, indexName]
|
|
624
|
-
databases.schema.list [schemaName?]
|
|
625
|
-
databases.schema.describe [collection_name]
|
|
626
|
-
databases.schema.indexes [collection_name]
|
|
627
|
-
databases.migration.create [{ product, database, data: { name, tag, description?, value: { up: string[], down: string[] } } }]
|
|
628
|
-
databases.migration.update [{ product, tag, data: { name?, description?, value? } }]
|
|
629
|
-
databases.migration.fetch [{ product, tag }]
|
|
630
|
-
databases.migration.list [{ product, database }]
|
|
631
|
-
databases.migration.delete [{ product, tag }]
|
|
632
|
-
databases.migration.run [migrations, options?]
|
|
633
|
-
databases.migration.rollback [migrations, count?]
|
|
634
|
-
databases.migration.history []
|
|
635
|
-
databases.migration.status [migrations]
|
|
636
|
-
databases.action.create [{ product, database, data: { tag, name, description?, type:"sql"|"nosql", query } }]
|
|
637
|
-
databases.action.update [{ product: string, tag: string, data: { name?: string, description?: string, query?: string } }]
|
|
638
|
-
databases.action.fetch [action_tag]
|
|
639
|
-
databases.action.list [database_tag]
|
|
640
|
-
databases.action.delete [action_tag]
|
|
641
|
-
databases.action.dispatch [{ product, env, database, action, input, schedule? }] ← CALL ductape_generate_payload FIRST (operation_family="database", method="dispatch", targets={database: "db_tag", table: "table_name"}) — requires redisUrl in ductape initialization
|
|
642
|
-
databases.dispatch [{ product, env, database, action, input, schedule? }] ← CALL ductape_generate_payload FIRST (operation_family="database", method="dispatch", targets={database: "db_tag", table: "table_name"}) — requires redisUrl in ductape initialization
|
|
643
|
-
databases.beginTransaction [{ product, env, database, isolationLevel?: "READ_COMMITTED"|"REPEATABLE_READ"|"SERIALIZABLE" }]
|
|
644
|
-
→ returns a transaction object; pass it to insert/update/delete/upsert/query calls as the last argument.
|
|
645
|
-
Commit with: transaction.commit() Rollback with: transaction.rollback()
|
|
646
|
-
BEFORE using transactions: ask the user which database type and tier they are running.
|
|
647
|
-
Transaction support varies by database — call ductape_docs({ topic: "transactions" }) for the full matrix.
|
|
648
|
-
databases.transaction [{ product, env, database, isolationLevel? }, async (tx) => { … }]
|
|
649
|
-
→ managed transaction: commits automatically on success, rolls back on error.
|
|
650
|
-
Prefer this over beginTransaction when the callback is self-contained.
|
|
651
|
-
See ductape_docs({ topic: "transactions" }) for database/tier compatibility before proceeding.
|
|
652
|
-
|
|
653
|
-
━━━ MODULE: graph ━━━
|
|
654
|
-
graph.create [{ product, tag, name, description?, type: "neo4j"|"nebula"|"arangodb", envs: [{slug, connection_url, username?, password?}] }]
|
|
655
|
-
graph.fetch [product_tag, graph_tag]
|
|
656
|
-
graph.list [product_tag?]
|
|
657
|
-
graph.update [product_tag, graph_tag, data: { name?: string, description?: string, type?: "neo4j"|"neptune"|"arangodb"|"memgraph", envs?: [{ slug: string, connection_url: string, username?: string, password?: string, database?: string, graphName?: string, region?: string }] }]
|
|
658
|
-
graph.delete [graph_tag, product_tag?]
|
|
659
|
-
graph.connect [{ product, env, graph }]
|
|
660
|
-
graph.testConnection [config]
|
|
661
|
-
graph.createNode [{ labels: string[], properties: { key: value } }, transaction?]
|
|
662
|
-
graph.findNodes [{ labels?: string[], where?: { key: value }, limit?, skip? }, transaction?]
|
|
663
|
-
graph.findNodeById [id: string|number, transaction?]
|
|
664
|
-
graph.updateNode [{ id, properties: { key: value } }, transaction?]
|
|
665
|
-
graph.deleteNode [{ id, detach?: boolean }, transaction?]
|
|
666
|
-
graph.mergeNode [{ labels: string[], matchProps: { key: value }, setProps?: { key: value } }, transaction?]
|
|
667
|
-
graph.addLabels [{ id, labels: string[] }, transaction?]
|
|
668
|
-
graph.removeLabels [{ id, labels: string[] }, transaction?]
|
|
669
|
-
graph.setLabels [{ id, labels: string[] }, transaction?]
|
|
670
|
-
graph.createRelationship [{ fromId, toId, type: string, properties?: { key: value } }, transaction?]
|
|
671
|
-
graph.findRelationships [{ type?: string, where?, limit? }, transaction?]
|
|
672
|
-
graph.findRelationshipById [id, transaction?]
|
|
673
|
-
graph.updateRelationship [{ id, properties: { key: value } }, transaction?]
|
|
674
|
-
graph.deleteRelationship [{ id }, transaction?]
|
|
675
|
-
graph.mergeRelationship [{ fromId, toId, type, matchProps?, setProps? }, transaction?]
|
|
676
|
-
graph.traverse [{ startId, direction?: "in"|"out"|"both", relationshipTypes?: string[], maxDepth?, where? }, transaction?]
|
|
677
|
-
graph.shortestPath [{ fromId, toId, relationshipType?, maxDepth? }, transaction?]
|
|
678
|
-
graph.allPaths [{ fromId, toId, relationshipType?, maxDepth? }, transaction?]
|
|
679
|
-
graph.getNeighborhood [{ id, depth?, relationshipTypes? }, transaction?]
|
|
680
|
-
graph.findConnectedComponents [{ labels? }, transaction?]
|
|
681
|
-
graph.query [cypher_query: string, params?: { key: value }, transaction?]
|
|
682
|
-
graph.getStatistics [transaction?]
|
|
683
|
-
graph.countNodes [labels?: string[], where?, transaction?]
|
|
684
|
-
graph.countRelationships [types?: string[], where?, transaction?]
|
|
685
|
-
graph.fullTextSearch [{ index, query, limit? }, transaction?]
|
|
686
|
-
graph.vectorSearch [{ index, vector: number[], topK? }, transaction?]
|
|
687
|
-
graph.createNodeIndex [{ label, field, type?: "btree"|"fulltext"|"vector" }]
|
|
688
|
-
graph.createNodeConstraint [{ label, field, type: "unique"|"exists" }]
|
|
689
|
-
graph.createRelationshipIndex [{ type, field }]
|
|
690
|
-
graph.listIndexes []
|
|
691
|
-
graph.listConstraints []
|
|
692
|
-
graph.dropIndex [name: string]
|
|
693
|
-
graph.dropConstraint [name: string]
|
|
694
|
-
graph.listLabels []
|
|
695
|
-
graph.listRelationshipTypes []
|
|
696
|
-
graph.createAction [{ graphTag, tag, name, query, params? }, productTag?]
|
|
697
|
-
graph.listActions [graphTag?, productTag?]
|
|
698
|
-
graph.getAction [actionTag, graphTag?, productTag?]
|
|
699
|
-
graph.updateAction [actionTag, updates, graphTag?, productTag?]
|
|
700
|
-
graph.deleteAction [actionTag, graphTag?, productTag?]
|
|
701
|
-
graph.beginTransaction [options?]
|
|
702
|
-
graph.commitTransaction [transaction]
|
|
703
|
-
graph.rollbackTransaction [transaction]
|
|
704
|
-
graph.dispatch [data] ← CALL ductape_generate_payload FIRST (operation_family="graph", method="dispatch", targets={graph}) — requires redisUrl in ductape initialization
|
|
705
|
-
|
|
706
|
-
━━━ MODULE: vector ━━━
|
|
707
|
-
vector.create [{ product, tag, name, description?, provider: "pinecone"|"qdrant"|"weaviate", dimensions: number, metric?: "cosine"|"euclidean"|"dotproduct", envs: [{slug, api_key, environment?}] }]
|
|
708
|
-
vector.update [{ product: string, tag: string, data: { name?: string, description?: string, type?: "pinecone"|"qdrant"|"weaviate"|"chroma"|"milvus"|"pgvector"|"memory", dimensions?: number, metric?: "cosine"|"euclidean"|"dotproduct", envs?: [{ slug: string, endpoint?: string, apiKey?: string, region?: string, index?: string, namespace?: string }] } }]
|
|
709
|
-
vector.fetch [{ product, tag }]
|
|
710
|
-
vector.list [{ product }]
|
|
711
|
-
vector.delete [{ product, tag }]
|
|
712
|
-
vector.connect [{ product, env, vector }]
|
|
713
|
-
vector.disconnect [{ product, env, vector }]
|
|
714
|
-
vector.query [{ product, env, tag, vector: number[], topK?: number, filter?, namespace?, includeValues?, includeMetadata? }]
|
|
715
|
-
vector.upsert [{ product, env, tag, vectors: [{id, values: number[], metadata?}], namespace? }]
|
|
716
|
-
vector.fetchVectors [{ product, env, vector, ids: string[], namespace? }]
|
|
717
|
-
vector.deleteVectors [{ product, env, tag, ids: string[], namespace?, deleteAll? }]
|
|
718
|
-
vector.findSimilar [{ product, env, vector, values: number[], topK?, filter?, namespace?, includeValues?, includeMetadata? }]
|
|
719
|
-
vector.upsertOne [{ product, env, tag, id, values: number[], metadata?, namespace?, session? }]
|
|
720
|
-
vector.fetchOne [{ product, env, vector, id, namespace? }]
|
|
721
|
-
vector.updateVector [{ product, env, vector, id, values?, setMetadata?, mergeMetadata?, namespace? }]
|
|
722
|
-
vector.updateMetadata [{ product, env, vector, id, metadata: { key: value }, merge?, namespace? }]
|
|
723
|
-
vector.deleteByIds [{ product, env, vector, ids: string[], namespace? }]
|
|
724
|
-
vector.deleteAll [{ product, env, vector, namespace? }]
|
|
725
|
-
vector.listVectors [{ product, env, vector, namespace?, prefix?, limit?, cursor? }]
|
|
726
|
-
vector.listAllVectors [{ product, env, vector, namespace?, prefix? }]
|
|
727
|
-
vector.listNamespaces [{ product, env, vector }]
|
|
728
|
-
vector.deleteNamespace [{ product, env, vector, namespace }]
|
|
729
|
-
vector.describeIndex [{ product, env, vector }]
|
|
730
|
-
vector.getStats [{ product, env, vector }]
|
|
731
|
-
vector.createIndex [{ product, env, vector, name, dimensions, metric?, replicas?, shards? }]
|
|
732
|
-
vector.deleteIndex [{ product, env, vector, name }]
|
|
733
|
-
vector.listIndexes [{ product, env, vector }]
|
|
734
|
-
vector.count [{ product, env, vector, namespace? }]
|
|
735
|
-
|
|
736
|
-
━━━ MODULE: features ━━━
|
|
737
|
-
Feature definitions are code-first. Use features.define in application source; do not call
|
|
738
|
-
administrative create/update/delete methods through ductape_execute.
|
|
739
|
-
features.fetch [product_tag, feature_tag]
|
|
740
|
-
features.fetchAll [product_tag]
|
|
741
|
-
|
|
742
|
-
features.define [{
|
|
743
|
-
product?: string,
|
|
744
|
-
tag: string,
|
|
745
|
-
name: string,
|
|
746
|
-
description?: string,
|
|
747
|
-
input?: { fieldName: { type: string, required?: boolean } },
|
|
748
|
-
output?: object,
|
|
749
|
-
signals?: { signalName: { input?: object } },
|
|
750
|
-
queries?: { queryName: { handler?: function } },
|
|
751
|
-
options?: { timeout?: number, retries?: number },
|
|
752
|
-
envs?: [{ slug: string, active?: boolean }],
|
|
753
|
-
recordInput?: object, // sample input for step-recording (run once during define)
|
|
754
|
-
recordScenarios?: object[], // multiple recording scenarios for branching
|
|
755
|
-
branchOverrides?: object, // force step results during recording to reach later branches
|
|
756
|
-
handler: async (ctx) => {
|
|
757
|
-
// ctx.input – typed feature input
|
|
758
|
-
// ctx.step(tag, fn, rollback?, opts?) – define a durable step
|
|
759
|
-
// ctx.api.run({ app, event, input }) – call an app action
|
|
760
|
-
// ctx.database.query/insert/update/delete({ database, event, ... })
|
|
761
|
-
// ctx.graph.execute({ graph, action, input })
|
|
762
|
-
// ctx.notification.send/email/push/sms({ notification, event, ... })
|
|
763
|
-
// ctx.storage.upload/download({ storage, event, input })
|
|
764
|
-
// ctx.events.produce({ event: "broker:topic", message: {} })
|
|
765
|
-
// ctx.quota.execute({ quota, input })
|
|
766
|
-
// ctx.fallback.execute({ fallback, input })
|
|
767
|
-
// ctx.healthcheck.getStatus(tag)
|
|
768
|
-
// ctx.sleep(ms|"1h30m")
|
|
769
|
-
// ctx.waitForSignal("signal-name", { timeout? })
|
|
770
|
-
// ctx.setState(key, value) / ctx.getState(key)
|
|
771
|
-
// ctx.feature(childId, childTag, childInput) – child feature
|
|
772
|
-
}
|
|
773
|
-
}]
|
|
774
|
-
|
|
775
|
-
features.execute [{ ← CALL ductape_generate_payload FIRST (operation_family="features", method="execute", targets={tag}) to discover the input field shape
|
|
776
|
-
product: string,
|
|
777
|
-
env: string,
|
|
778
|
-
tag: string,
|
|
779
|
-
input: { fieldName: value },
|
|
780
|
-
session?: string, // "session_tag:jwt_token"
|
|
781
|
-
idempotency_key?: string,
|
|
782
|
-
cache?: string,
|
|
783
|
-
retries?: number,
|
|
784
|
-
timeout?: number
|
|
785
|
-
}]
|
|
786
|
-
|
|
787
|
-
features.dispatch [{ ← CALL ductape_generate_payload FIRST (operation_family="features", method="dispatch", targets={feature}) — requires redisUrl in ductape initialization
|
|
788
|
-
product: string,
|
|
789
|
-
env: string,
|
|
790
|
-
feature: string,
|
|
791
|
-
input: { fieldName: value },
|
|
792
|
-
schedule?: { start_at?: number|string, cron?: string, every?: number, limit?: number, endDate?: number|string, tz?: string },
|
|
793
|
-
session?: string,
|
|
794
|
-
cache?: string,
|
|
795
|
-
retries?: number
|
|
796
|
-
}]
|
|
797
|
-
|
|
798
|
-
features.signal [{ product, env, feature_id, signal: string, payload?: object }]
|
|
799
|
-
features.query [{ product, env, feature_id, query: string, params?: object }]
|
|
800
|
-
features.status [executionId: string]
|
|
801
|
-
features.cancel [executionId: string, reason?: string]
|
|
802
|
-
features.replay [executionId: string, options?: object]
|
|
803
|
-
features.restart [executionId: string]
|
|
804
|
-
features.resume [executionId: string]
|
|
805
|
-
features.replayFromStep [executionId: string, stepTag: string]
|
|
806
|
-
features.history [executionId: string]
|
|
807
|
-
features.stepDetail [executionId: string, stepTag: string]
|
|
808
|
-
features.relatedExecutions [executionId: string]
|
|
809
|
-
features.compare [executionId1: string, executionId2: string]
|
|
810
|
-
|
|
811
|
-
━━━ MODULE: caches ━━━
|
|
812
|
-
caches.create [product_tag, data: { name: string, tag: string, description?: string, expiry: number }]
|
|
813
|
-
← expiry is in MILLISECONDS (e.g. 3600000 = 1 hour, 86400000 = 1 day). No type or envs — Ductape manages the store.
|
|
814
|
-
caches.update [product_tag, cache_tag, data: { name?: string, description?: string, expiry?: number }]
|
|
815
|
-
caches.fetch [product_tag, cache_tag]
|
|
816
|
-
caches.list [product_tag]
|
|
817
|
-
caches.delete [product_tag, cache_tag]
|
|
818
|
-
caches.get [{ key: string }]
|
|
819
|
-
caches.set [{ product, cache, key, value: string, componentTag?, componentType?, expiry?: string (ISO 8601 absolute timestamp — NOT a duration; e.g. new Date(Date.now()+3600000).toISOString() for 1h TTL), env }]
|
|
820
|
-
caches.clear [{ key: string }]
|
|
821
|
-
caches.clearAll [{ product, cache, env? }]
|
|
822
|
-
caches.fetchValues [{ product, cache, env?, page?, limit?, expiryFilter?: "all"|"expiring"|"permanent"|"expired" }]
|
|
823
|
-
caches.fetchDashboard [{ product, cache, env? }]
|
|
824
|
-
|
|
825
|
-
━━━ MODULE: jobs ━━━
|
|
826
|
-
jobs.create [product_tag, data: { tag, name, description?, type: "action"|"notification"|"storage"|"database"|"publish", app?, event?, executions?, intervals?, start_at? }]
|
|
827
|
-
jobs.update [product_tag, job_tag, data: { name?: string, description?: string, type?: "action"|"notification"|"storage"|"database"|"publish", app?: string, event?: string, executions?: number, intervals?: number, start_at?: number }]
|
|
828
|
-
jobs.fetch [product_tag, job_tag]
|
|
829
|
-
jobs.list [product_tag]
|
|
830
|
-
jobs.delete [product_tag, job_tag]
|
|
831
|
-
jobs.get [jobId: string]
|
|
832
|
-
jobs.listJobs [{ product?, status?, limit?, page? }]
|
|
833
|
-
jobs.cancel [jobId, { reason? }?]
|
|
834
|
-
jobs.cancelMany [{ product?, status? }]
|
|
835
|
-
jobs.pause [jobId]
|
|
836
|
-
jobs.pauseMany [{ product?, recurring? }]
|
|
837
|
-
jobs.resume [jobId]
|
|
838
|
-
jobs.resumeMany [{ product?, status? }]
|
|
839
|
-
jobs.retry [jobId, { delay? }?]
|
|
840
|
-
jobs.retryMany [{ status?, from? }]
|
|
841
|
-
jobs.reschedule [jobId, { start_at?, cron?, every? }]
|
|
842
|
-
jobs.getHistory [jobId, { limit? }?]
|
|
843
|
-
jobs.getStats [{ product?, env?, from?, to? }?]
|
|
844
|
-
|
|
845
|
-
━━━ MODULE: secrets ━━━
|
|
846
|
-
secrets.create [{ key, value, description?, token_type?: "api"|"password"|"certificate", scope?: string[], envs?: string[], expires_at?: number }]
|
|
847
|
-
secrets.update [key, { value?, description?, scope?, envs?, expires_at? }]
|
|
848
|
-
secrets.fetch [key]
|
|
849
|
-
secrets.list []
|
|
850
|
-
secrets.delete [key]
|
|
851
|
-
secrets.revoke [key]
|
|
852
|
-
secrets.exists [key]
|
|
853
|
-
secrets.resolve [value, { env?, app? }?]
|
|
854
|
-
secrets.validate [value]
|
|
855
|
-
|
|
856
|
-
━━━ MODULE: logs ━━━
|
|
857
|
-
logs.init [product_tag?, app_tag?] (at least one required)
|
|
858
|
-
logs.fetch [{ product_id?, app_id?, env?, level?: "info"|"warn"|"error"|"debug", start_date?, end_date?, page?, limit? }]
|
|
859
|
-
|
|
860
|
-
━━━ TOOL: ductape_schema ━━━
|
|
861
|
-
Use this tool to dynamically discover the full field manifest for Ductape asset creation and update operations,
|
|
862
|
-
derived live from the SDK's Joi validators. No arguments are required to get everything.
|
|
863
|
-
|
|
864
|
-
Arguments:
|
|
865
|
-
module?: "app" | "product" (optional — omit to get both modules + all enums)
|
|
866
|
-
|
|
867
|
-
Returns:
|
|
868
|
-
When module is omitted:
|
|
869
|
-
{
|
|
870
|
-
modules: {
|
|
871
|
-
app: { [method: string]: { fields: { [field]: FieldDef } } },
|
|
872
|
-
product: { [method: string]: { fields: { [field]: FieldDef } } }
|
|
873
|
-
},
|
|
874
|
-
enums: {
|
|
875
|
-
HttpMethods, AuthTypes, DataFormats, DataTypes, StatusCodes, TokenPeriods,
|
|
876
|
-
DatabaseTypes, AppComponents, ProductComponents, Categories, DefaultTypes,
|
|
877
|
-
EventTypes, InputsTypes, PublicStates
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
When module="app" or module="product":
|
|
882
|
-
{ module: string, methods: { [method]: { fields: { [field]: FieldDef } } }, enums: { ... } }
|
|
883
|
-
|
|
884
|
-
FieldDef shape:
|
|
885
|
-
{
|
|
886
|
-
type: string, // "string" | "number" | "boolean" | "object" | "array" | "alternatives" | "any"
|
|
887
|
-
required: boolean,
|
|
888
|
-
enum?: (string | number | boolean)[], // present when field is restricted to specific values
|
|
889
|
-
fields?: { [key]: FieldDef }, // present for nested objects
|
|
890
|
-
items?: FieldDef, // present for arrays (describes each element)
|
|
891
|
-
oneOf?: FieldDef[], // present for alternatives (.try(...))
|
|
892
|
-
minLength?: number, maxLength?: number, // string length constraints
|
|
893
|
-
min?: number, max?: number, // number range constraints
|
|
894
|
-
pattern?: string // string pattern regex
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
Example method keys for app module:
|
|
898
|
-
"create", "update", "environments.create", "environments.update",
|
|
899
|
-
"actions.create", "actions.update", "auths.create", "auths.update",
|
|
900
|
-
"variables.create", "variables.update", "constants.create", "constants.update",
|
|
901
|
-
"webhooks.create", "webhooks.update", "webhooks.events.create", "webhooks.events.update"
|
|
902
|
-
|
|
903
|
-
Example method keys for product module:
|
|
904
|
-
"create", "environments.create", "environments.update", "apps.add", "apps.update",
|
|
905
|
-
"databases.create", "databases.update", "notifications.create", "notifications.update",
|
|
906
|
-
"caches.create", "caches.update", "jobs.create", "jobs.update",
|
|
907
|
-
"messageBrokers.create", "messageBrokers.update", "fallbacks.create", "fallbacks.update",
|
|
908
|
-
"graphs.create", "graphs.update", "vectors.create", "vectors.update",
|
|
909
|
-
"sessions.create", "sessions.update", "healthchecks.create",
|
|
910
|
-
"quotas.create", "quotas.update", "functions.create", "functions.update",
|
|
911
|
-
"agents.create", "agents.update", "models.create", "models.update",
|
|
912
|
-
"storage.create", "storage.update"
|
|
913
|
-
|
|
914
|
-
When to use this tool:
|
|
915
|
-
- Before constructing an administrative asset file for ductape_cli or Workbench — use this to discover required fields
|
|
916
|
-
- To enumerate valid enum values (e.g. which DatabaseTypes or AuthTypes are accepted)
|
|
917
|
-
- To understand nested object shapes without reading SDK docs
|
|
918
|
-
`;
|
|
919
|
-
|
|
920
|
-
const executeInputSchema = z.object({
|
|
921
|
-
publishable_key: z.string().optional().describe('Your workspace publishable key. Omit if DUCTAPE_PUBLISHABLE_KEY is set in the MCP server env config.'),
|
|
922
|
-
module: z.enum(MODULES as [string, ...string[]]).describe(
|
|
923
|
-
'SDK module to target. Options: ' + MODULES.join(', ') + '.'
|
|
924
|
-
),
|
|
925
|
-
method: z.string().describe(
|
|
926
|
-
'The SDK method to call. Use dot-notation for nested methods (e.g. "environments.create", "schema.addField", "messages.query").\n' +
|
|
927
|
-
'Full reference: see the METHOD_DOCS embedded in the params description.'
|
|
928
|
-
),
|
|
929
|
-
params: z.array(z.any()).default([]).describe(METHOD_DOCS),
|
|
930
|
-
});
|
|
931
|
-
|
|
932
|
-
const payloadGenerateInputSchema = z.object({
|
|
933
|
-
publishable_key: z.string().optional().describe('Your workspace publishable key. Omit if DUCTAPE_PUBLISHABLE_KEY is set in the MCP server env config.'),
|
|
934
|
-
product_tag: z.string().describe('The product tag (e.g. "my-product"). Identifies which product configuration to read.'),
|
|
935
|
-
env_slug: z.string().describe('The environment slug (e.g. "dev", "prod"). Determines which env-specific values are used.'),
|
|
936
|
-
operation_family: z.string().describe(
|
|
937
|
-
'The operation category. One of: "action", "features", "database", "graph", "vector", "storage", ' +
|
|
938
|
-
'"notification", "messaging", "quota", "fallback", "healthcheck", "session", "cache".'
|
|
939
|
-
),
|
|
940
|
-
method: z.string().describe(
|
|
941
|
-
'The method within the operation family. E.g. "run", "dispatch", "execute", "start", "send", "produce", ' +
|
|
942
|
-
'"query", "insert", "update", "delete". Must match an allowed method for the given operation_family.'
|
|
943
|
-
),
|
|
944
|
-
targets: z.record(z.any()).optional().describe(
|
|
945
|
-
'Identifies the specific operation to generate a payload for. ' +
|
|
946
|
-
'For actions: { app: "app_tag", action: "action_tag" }. ' +
|
|
947
|
-
'For features: { feature: "feature_tag" }. ' +
|
|
948
|
-
'For databases: { database: "db_tag", table: "table_or_collection_name" }. ' +
|
|
949
|
-
' Providing table is strongly recommended — the generator scans all actions configured for that table, ' +
|
|
950
|
-
' aggregates field definitions (name, type, required, sample value), and returns them in ' +
|
|
951
|
-
' meta.schema_context.database.fields. The where/data placeholders in the payload are also ' +
|
|
952
|
-
' pre-filled with the real field names. meta.schema_context.database.available_tables lists ' +
|
|
953
|
-
' every table the database has actions configured for, so you can discover table names first. ' +
|
|
954
|
-
'For graphs: { graph: "graph_tag", node_label?: "NodeLabel", edge_type?: "REL_TYPE" }. ' +
|
|
955
|
-
' node_label and edge_type pre-fill the Cypher template; omit to get generic placeholders. ' +
|
|
956
|
-
' meta.schema_context.graph.type tells you the engine (neo4j, neptune, etc.). ' +
|
|
957
|
-
'For vectors: { vector: "vector_tag", namespace?: "ns" }. ' +
|
|
958
|
-
' meta.schema_context.vector surfaces dimensions, metric, and index so you know what size ' +
|
|
959
|
-
' embedding to pass and which distance function is used. ' +
|
|
960
|
-
'For sessions: { session: "session_tag" }. ' +
|
|
961
|
-
'For notifications: { notification: "notif_tag" }. ' +
|
|
962
|
-
'For quotas/fallbacks: { tag: "resource_tag" }. ' +
|
|
963
|
-
'For storage: { storage: "storage_tag" }. ' +
|
|
964
|
-
'For messaging: { broker: "broker_tag", topic?: "topic_tag" }.'
|
|
965
|
-
),
|
|
966
|
-
include_session: z.boolean().optional().describe(
|
|
967
|
-
'Include a session placeholder inside the generated input object. ' +
|
|
968
|
-
'Defaults to false for execution_context="system" and true otherwise. ' +
|
|
969
|
-
'The placeholder is named "<session_tag_token>" to indicate it expects the runtime JWT, not the tag name.'
|
|
970
|
-
),
|
|
971
|
-
execution_context: z.enum(['user', 'delegated', 'system']).optional().default('user').describe(
|
|
972
|
-
'Actor intent for this runtime operation. "user" means an active request initiated by the authenticated user; ' +
|
|
973
|
-
'"delegated" means work acting on behalf of a user with a short-lived delegated identity or application-owned ' +
|
|
974
|
-
'immutable actor context; "system" means intentionally unattributed background work. This drives session warnings.'
|
|
975
|
-
),
|
|
976
|
-
include_cache: z.boolean().optional().describe(
|
|
977
|
-
'Include a cache tag inside the generated payload. Defaults to false; set true only when the runtime call should use cache explicitly.'
|
|
978
|
-
),
|
|
979
|
-
schema_mode: z.enum(['strict', 'best_effort']).optional().default('best_effort').describe(
|
|
980
|
-
'"strict" — fail if any required field cannot be resolved. ' +
|
|
981
|
-
'"best_effort" — fill what is known, leave unknowns as null/placeholder. Use best_effort when exploring.'
|
|
982
|
-
),
|
|
983
|
-
input_hint: z.record(z.any()).optional().describe(
|
|
984
|
-
'Optional. Partial input values you already know. These are merged into the generated payload template ' +
|
|
985
|
-
'so the result is pre-filled. Use this to get a payload with known values already substituted.'
|
|
986
|
-
),
|
|
987
|
-
});
|
|
988
|
-
|
|
989
|
-
const snippetGenerateInputSchema = payloadGenerateInputSchema.extend({
|
|
990
|
-
language: z.enum(['typescript', 'python']).default('typescript'),
|
|
991
|
-
});
|
|
992
|
-
|
|
993
|
-
const schemaInputSchema = z.object({
|
|
994
|
-
module: z.enum(['app', 'product']).optional().describe(
|
|
995
|
-
'Optional. Scope the result to a single module ("app" or "product"). Omit to get the full manifest including enums.'
|
|
996
|
-
),
|
|
997
|
-
});
|
|
998
|
-
|
|
999
|
-
function toPrettyJson(value: unknown): string {
|
|
1000
|
-
return JSON.stringify(value ?? {}, null, 2);
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
const ALLOWED_SNIPPET_METHODS: Record<string, string[]> = {
|
|
1004
|
-
action: ['run', 'dispatch', 'execute'],
|
|
1005
|
-
features: ['run', 'dispatch', 'execute'],
|
|
1006
|
-
database: ['dispatch', 'find', 'insert', 'update', 'delete', 'query', 'upsert', 'count', 'aggregate'],
|
|
1007
|
-
graph: ['dispatch', 'find', 'insert', 'update', 'delete', 'query', 'execute'],
|
|
1008
|
-
vector: ['query', 'find', 'findSimilar', 'upsert', 'insert', 'delete', 'dispatch'],
|
|
1009
|
-
storage: ['dispatch', 'upload', 'download', 'remove', 'listFiles', 'getSignedUrl', 'stats'],
|
|
1010
|
-
notification: ['dispatch', 'send', 'email.send', 'push.send', 'sms.send', 'callback.send', 'slack.send', 'discord.send'],
|
|
1011
|
-
messaging: ['dispatch', 'send', 'publish', 'produce', 'consume'],
|
|
1012
|
-
broker: ['dispatch', 'send', 'publish', 'produce', 'consume'],
|
|
1013
|
-
quota: ['run', 'dispatch', 'check', 'consume'],
|
|
1014
|
-
fallback: ['run', 'dispatch'],
|
|
1015
|
-
healthcheck: ['run', 'check', 'status'],
|
|
1016
|
-
health: ['run', 'check', 'status'],
|
|
1017
|
-
session: ['start', 'verify', 'refresh', 'revoke', 'listActive'],
|
|
1018
|
-
cache: ['get', 'set', 'clear', 'clearAll', 'dispatch', 'fetchValues'],
|
|
1019
|
-
};
|
|
1020
|
-
|
|
1021
|
-
function ensureSupportedSnippetOperation(operationFamily: string, method: string): void {
|
|
1022
|
-
const family = String(operationFamily || '').toLowerCase();
|
|
1023
|
-
const m = String(method || '');
|
|
1024
|
-
const allowed = ALLOWED_SNIPPET_METHODS[family] || [];
|
|
1025
|
-
if (!allowed.includes(m)) {
|
|
1026
|
-
const supportedFamilies = Object.keys(ALLOWED_SNIPPET_METHODS).sort().join(', ');
|
|
1027
|
-
throw new Error(
|
|
1028
|
-
`Unsupported snippet operation "${family}.${m}". Allowed methods for "${family}": ${allowed.join(', ') || '(none)'}.\n` +
|
|
1029
|
-
`Supported families: ${supportedFamilies}`,
|
|
1030
|
-
);
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
function resolveSdkCallPath(operationFamily: string, method: string): string {
|
|
1035
|
-
const family = String(operationFamily || '').toLowerCase();
|
|
1036
|
-
const m = String(method || '').toLowerCase();
|
|
1037
|
-
|
|
1038
|
-
if (family === 'action') return `actions.${m}`;
|
|
1039
|
-
if (family === 'features' || family === 'feature') return `features.${m}`;
|
|
1040
|
-
if (family === 'database') return m === 'dispatch' ? 'databases.dispatch' : `databases.${m}`;
|
|
1041
|
-
if (family === 'graph') return m === 'dispatch' ? 'graph.dispatch' : `graph.${m}`;
|
|
1042
|
-
if (family === 'vector') return `vector.${m}`;
|
|
1043
|
-
if (family === 'storage') return m === 'dispatch' ? 'storage.dispatch' : `storage.${m}`;
|
|
1044
|
-
if (family === 'notification') return m === 'dispatch' ? 'notifications.dispatch' : `notifications.${m}`;
|
|
1045
|
-
if (family === 'messaging' || family === 'broker' || family === 'events' || family === 'event') {
|
|
1046
|
-
if (m === 'dispatch') return 'events.dispatch';
|
|
1047
|
-
if (m === 'send' || m === 'publish' || m === 'produce') return 'events.produce';
|
|
1048
|
-
return `events.${m}`;
|
|
1049
|
-
}
|
|
1050
|
-
if (family === 'quota') return `quotas.${m}`;
|
|
1051
|
-
if (family === 'fallback') return `fallback.${m}`;
|
|
1052
|
-
if (family === 'healthcheck' || family === 'health') return `health.${m}`;
|
|
1053
|
-
if (family === 'session') return `sessions.${m}`;
|
|
1054
|
-
if (family === 'cache') return m === 'dispatch' ? 'caches.dispatch' : `caches.${m}`;
|
|
1055
|
-
return `${family}.${m}`;
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
function buildSdkInvocationArgs(payload: Record<string, unknown>): Record<string, unknown> {
|
|
1059
|
-
const input = (payload?.input as Record<string, unknown>) || {};
|
|
1060
|
-
return {
|
|
1061
|
-
product: payload?.product_tag,
|
|
1062
|
-
env: payload?.env,
|
|
1063
|
-
...input,
|
|
1064
|
-
...(payload?.session ? { session: payload.session } : {}),
|
|
1065
|
-
...(payload?.cache ? { cache: payload.cache } : {}),
|
|
1066
|
-
};
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
function operationAcceptsSession(operationFamily: string, method: string): boolean {
|
|
1070
|
-
const family = operationFamily.toLowerCase();
|
|
1071
|
-
const sessionFamilies = new Set([
|
|
1072
|
-
'action', 'features', 'feature', 'database', 'graph', 'vector', 'storage',
|
|
1073
|
-
'notification', 'messaging', 'broker', 'events', 'event', 'quota', 'fallback',
|
|
1074
|
-
]);
|
|
1075
|
-
if (!sessionFamilies.has(family)) return false;
|
|
1076
|
-
return !['consume', 'status', 'check', 'fetch', 'list'].includes(method.toLowerCase());
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
function addSessionAwarenessMetadata(
|
|
1080
|
-
generated: any,
|
|
1081
|
-
args: z.infer<typeof payloadGenerateInputSchema>,
|
|
1082
|
-
): any {
|
|
1083
|
-
const acceptsSession = operationAcceptsSession(args.operation_family, args.method);
|
|
1084
|
-
const executionContext = args.execution_context ?? 'user';
|
|
1085
|
-
const payloadHasSession = Boolean(generated?.payload?.session || generated?.payload?.input?.session);
|
|
1086
|
-
const sessionRequested = args.include_session ?? executionContext !== 'system';
|
|
1087
|
-
const warnings: string[] = [];
|
|
1088
|
-
|
|
1089
|
-
if (acceptsSession && executionContext !== 'system' && (!sessionRequested || !payloadHasSession)) {
|
|
1090
|
-
warnings.push(
|
|
1091
|
-
`Session attribution is missing for a ${executionContext}-context operation. ` +
|
|
1092
|
-
'Pass the original full session token for immediate user work, or an approved delegated identity/immutable actor context for delayed work.',
|
|
1093
|
-
);
|
|
1094
|
-
}
|
|
1095
|
-
if (executionContext === 'system' && payloadHasSession) {
|
|
1096
|
-
warnings.push(
|
|
1097
|
-
'This operation is marked system-context but contains a session placeholder. Remove it unless the work is actually user/delegated.',
|
|
1098
|
-
);
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
generated.meta = {
|
|
1102
|
-
...(generated?.meta ?? {}),
|
|
1103
|
-
session_awareness: {
|
|
1104
|
-
accepts_session: acceptsSession,
|
|
1105
|
-
execution_context: executionContext,
|
|
1106
|
-
appears_user_context: executionContext === 'user',
|
|
1107
|
-
session_requested: sessionRequested,
|
|
1108
|
-
session_present_in_payload: payloadHasSession,
|
|
1109
|
-
intentionally_system_context: executionContext === 'system',
|
|
1110
|
-
warnings,
|
|
1111
|
-
},
|
|
1112
|
-
};
|
|
1113
|
-
return generated;
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
function buildTypeScriptSnippet(
|
|
1117
|
-
payload: Record<string, unknown>,
|
|
1118
|
-
operationFamily: string,
|
|
1119
|
-
method: string,
|
|
1120
|
-
): string {
|
|
1121
|
-
const callPath = resolveSdkCallPath(operationFamily, method);
|
|
1122
|
-
const invocationArgs = buildSdkInvocationArgs(payload);
|
|
1123
|
-
return `import Ductape from "@ductape/sdk";
|
|
1124
|
-
|
|
1125
|
-
const ductape = new Ductape({
|
|
1126
|
-
workspace_id: process.env.DUCTAPE_WORKSPACE_ID!,
|
|
1127
|
-
user_id: process.env.DUCTAPE_USER_ID!,
|
|
1128
|
-
public_key: process.env.DUCTAPE_PUBLIC_KEY!,
|
|
1129
|
-
});
|
|
1130
|
-
|
|
1131
|
-
async function run() {
|
|
1132
|
-
const payload = ${toPrettyJson(payload)};
|
|
1133
|
-
const args = ${toPrettyJson(invocationArgs)};
|
|
1134
|
-
const result = await ductape.${callPath}(args);
|
|
1135
|
-
return { payload, result };
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
run().catch(console.error);
|
|
1139
|
-
`;
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
function buildPythonSnippet(
|
|
1143
|
-
payload: Record<string, unknown>,
|
|
1144
|
-
operationFamily: string,
|
|
1145
|
-
method: string,
|
|
1146
|
-
): string {
|
|
1147
|
-
const callPath = resolveSdkCallPath(operationFamily, method);
|
|
1148
|
-
const invocationArgs = buildSdkInvocationArgs(payload);
|
|
1149
|
-
return `from ductape import Ductape
|
|
1150
|
-
import os
|
|
1151
|
-
import json
|
|
1152
|
-
|
|
1153
|
-
ductape = Ductape(
|
|
1154
|
-
workspace_id=os.environ.get("DUCTAPE_WORKSPACE_ID"),
|
|
1155
|
-
user_id=os.environ.get("DUCTAPE_USER_ID"),
|
|
1156
|
-
public_key=os.environ.get("DUCTAPE_PUBLIC_KEY"),
|
|
1157
|
-
)
|
|
1158
|
-
|
|
1159
|
-
def run():
|
|
1160
|
-
payload = ${toPrettyJson(payload)}
|
|
1161
|
-
args = ${toPrettyJson(invocationArgs)}
|
|
1162
|
-
result = ductape.${callPath}(args)
|
|
1163
|
-
return {"payload": payload, "result": result}
|
|
1164
|
-
|
|
1165
|
-
if __name__ == "__main__":
|
|
1166
|
-
print(json.dumps(run(), indent=2))
|
|
1167
|
-
`;
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
function buildSnippet(
|
|
1171
|
-
language: 'typescript' | 'python',
|
|
1172
|
-
payload: Record<string, unknown>,
|
|
1173
|
-
operationFamily: string,
|
|
1174
|
-
method: string,
|
|
1175
|
-
): string {
|
|
1176
|
-
return language === 'python'
|
|
1177
|
-
? buildPythonSnippet(payload, operationFamily, method)
|
|
1178
|
-
: buildTypeScriptSnippet(payload, operationFamily, method);
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
// ─── CLI helpers ─────────────────────────────────────────────────────────────
|
|
1183
|
-
|
|
1184
|
-
// Per-process cache: avoids re-running whoami / workspaces use on every call.
|
|
1185
|
-
let authState: 'unknown' | 'ok' | 'none' = 'unknown';
|
|
1186
|
-
let workspaceSynced = false;
|
|
1187
|
-
|
|
1188
|
-
const ADMIN_SUBCOMMANDS = [
|
|
1189
|
-
'login', 'logout', 'whoami',
|
|
1190
|
-
'profiles',
|
|
1191
|
-
'workspaces',
|
|
1192
|
-
'link', 'unlink', 'init',
|
|
1193
|
-
'products', 'apps',
|
|
1194
|
-
'resources',
|
|
1195
|
-
'notifications',
|
|
1196
|
-
'events',
|
|
1197
|
-
'cloud',
|
|
1198
|
-
'secrets',
|
|
1199
|
-
'generate',
|
|
1200
|
-
'apply',
|
|
1201
|
-
'db',
|
|
1202
|
-
'graph',
|
|
1203
|
-
];
|
|
1204
|
-
|
|
1205
|
-
function checkCli(): { available: boolean; version?: string } {
|
|
1206
|
-
try {
|
|
1207
|
-
const out = execSync('ductape --version', {
|
|
1208
|
-
encoding: 'utf8',
|
|
1209
|
-
timeout: 5000,
|
|
1210
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1211
|
-
env: cliEnvironment(),
|
|
1212
|
-
}).trim();
|
|
1213
|
-
return { available: true, version: out || 'unknown' };
|
|
1214
|
-
} catch {
|
|
1215
|
-
return { available: false };
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
function checkLoginState(): 'ok' | 'none' {
|
|
1220
|
-
try {
|
|
1221
|
-
// `ductape whoami` only reports whether a local credentials file exists. It does not validate
|
|
1222
|
-
// the stored token, so an expired token would be cached as authenticated and fail later with 401.
|
|
1223
|
-
// Use a harmless authenticated read to validate the credential against the API.
|
|
1224
|
-
execSync('ductape workspaces list --json', {
|
|
1225
|
-
encoding: 'utf8',
|
|
1226
|
-
timeout: 10000,
|
|
1227
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1228
|
-
env: cliEnvironment(),
|
|
1229
|
-
});
|
|
1230
|
-
authState = 'ok';
|
|
1231
|
-
return 'ok';
|
|
1232
|
-
} catch {
|
|
1233
|
-
authState = 'none';
|
|
1234
|
-
return 'none';
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
function syncWorkspace(): void {
|
|
1239
|
-
const target = process.env.DUCTAPE_WORKSPACE;
|
|
1240
|
-
workspaceSynced = true; // mark done regardless so we don't retry on every call
|
|
1241
|
-
if (!target) return;
|
|
1242
|
-
try {
|
|
1243
|
-
execSync(`ductape workspaces use "${target}"`, {
|
|
1244
|
-
encoding: 'utf8',
|
|
1245
|
-
timeout: 10000,
|
|
1246
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1247
|
-
env: cliEnvironment(),
|
|
1248
|
-
});
|
|
1249
|
-
} catch {
|
|
1250
|
-
// best-effort; if it fails the user will see workspace-mismatch errors on subsequent commands
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
function runCli(command: string): { success: boolean; output: string } {
|
|
1255
|
-
const first = command.trim().split(/\s+/)[0];
|
|
1256
|
-
if (!ADMIN_SUBCOMMANDS.includes(first)) {
|
|
1257
|
-
return {
|
|
1258
|
-
success: false,
|
|
1259
|
-
output: `Subcommand "${first}" is not in the allowed admin subcommand list. Allowed: ${ADMIN_SUBCOMMANDS.join(', ')}.`,
|
|
1260
|
-
};
|
|
1261
|
-
}
|
|
1262
|
-
// Auto-inject --workspace on login if DUCTAPE_WORKSPACE is set and caller hasn't specified one
|
|
1263
|
-
let finalCommand = command;
|
|
1264
|
-
const ws = process.env.DUCTAPE_WORKSPACE;
|
|
1265
|
-
if (first === 'login' && ws && !command.includes('--workspace') && !command.includes('--skip-workspace-select')) {
|
|
1266
|
-
finalCommand = `${command} --workspace "${ws}"`;
|
|
1267
|
-
}
|
|
1268
|
-
try {
|
|
1269
|
-
const output = execSync(`ductape ${finalCommand}`, {
|
|
1270
|
-
encoding: 'utf8',
|
|
1271
|
-
// Must exceed the proxy's operation timeout so stderr can preserve the structured timeout
|
|
1272
|
-
// instead of this wrapper killing the CLI first and reducing it to "(no data)".
|
|
1273
|
-
timeout: 90000,
|
|
1274
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1275
|
-
env: cliEnvironment(),
|
|
1276
|
-
});
|
|
1277
|
-
return { success: true, output: output.trim() };
|
|
1278
|
-
} catch (err: any) {
|
|
1279
|
-
const msg = (err.stderr || err.stdout || err.message || String(err)).trim();
|
|
1280
|
-
if (/\bHTTP 401\b|unauthori[sz]ed|invalid token|token expired/i.test(msg)) {
|
|
1281
|
-
// Distinguish a genuinely expired login from a command-specific endpoint/auth bug.
|
|
1282
|
-
// A valid workspace read proves the CLI session and selected workspace are authenticated.
|
|
1283
|
-
if (checkLoginState() === 'ok') {
|
|
1284
|
-
return {
|
|
1285
|
-
success: false,
|
|
1286
|
-
output: [
|
|
1287
|
-
'The Ductape CLI session and active workspace are authenticated, but this command endpoint returned HTTP 401.',
|
|
1288
|
-
'Do not ask the user to log in again and do not retry through ductape_execute.',
|
|
1289
|
-
'This indicates a CLI command routing or endpoint authorization problem.',
|
|
1290
|
-
'',
|
|
1291
|
-
'Update the Ductape CLI to the latest version and retry the same ductape_cli command.',
|
|
1292
|
-
`Original command: ductape ${command}`,
|
|
1293
|
-
].join('\n'),
|
|
1294
|
-
};
|
|
1295
|
-
}
|
|
1296
|
-
authState = 'none';
|
|
1297
|
-
workspaceSynced = false;
|
|
1298
|
-
return {
|
|
1299
|
-
success: false,
|
|
1300
|
-
output: [
|
|
1301
|
-
'Ductape CLI authentication expired or was revoked (HTTP 401).',
|
|
1302
|
-
'This administrative operation correctly used ductape_cli; do not retry it through ductape_execute.',
|
|
1303
|
-
'',
|
|
1304
|
-
'Re-authenticate locally in a trusted terminal:',
|
|
1305
|
-
' ductape login',
|
|
1306
|
-
'or use an OAuth flow:',
|
|
1307
|
-
' ductape login --browser google',
|
|
1308
|
-
' ductape login --browser github',
|
|
1309
|
-
'',
|
|
1310
|
-
'Do not paste a password, OAuth callback token, or stored CLI credential into an agent prompt.',
|
|
1311
|
-
'After login succeeds, retry the same ductape_cli command.',
|
|
1312
|
-
].join('\n'),
|
|
1313
|
-
};
|
|
1314
|
-
}
|
|
1315
|
-
return { success: false, output: msg };
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
/**
|
|
1320
|
-
* MCP is publishable-key-only. The standalone CLI may manage its own access-key
|
|
1321
|
-
* credentials, but an access key present in the MCP host environment is never
|
|
1322
|
-
* forwarded into CLI subprocesses.
|
|
1323
|
-
*/
|
|
1324
|
-
function cliEnvironment(): NodeJS.ProcessEnv {
|
|
1325
|
-
const environment = { ...process.env };
|
|
1326
|
-
delete environment.DUCTAPE_ACCESS_KEY;
|
|
1327
|
-
return environment;
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
const docsInputSchema = z.object({
|
|
1331
|
-
topic: z.string().describe(
|
|
1332
|
-
'Feature topic to look up. Supported: ' +
|
|
1333
|
-
'transactions, presave, triggers, aggregations, migrations, indexes, performance, actions, ' +
|
|
1334
|
-
'graphs, storage, cloud, vector, warehouse, secrets, apps, products, sessions, caches, ' +
|
|
1335
|
-
'notifications, resilience, features, events, logs, frontend, frontend-analytics, client, react, vue',
|
|
1336
|
-
),
|
|
1337
|
-
});
|
|
1338
|
-
|
|
1339
|
-
const DOCS: Record<string, string> = {
|
|
1340
|
-
frontend: `
|
|
1341
|
-
DUCTAPE FRONTEND SDK GUIDE
|
|
1342
|
-
|
|
1343
|
-
Choose one integration package:
|
|
1344
|
-
React 17+ — npm install @ductape/react
|
|
1345
|
-
Provider and hooks built on @ductape/client.
|
|
1346
|
-
Continue with ductape_docs({ topic: "react" }).
|
|
1347
|
-
Vue 3+ — npm install @ductape/vue
|
|
1348
|
-
Plugin and composables built on @ductape/client.
|
|
1349
|
-
Continue with ductape_docs({ topic: "vue" }).
|
|
1350
|
-
Other UI — npm install @ductape/client
|
|
1351
|
-
frameworks Use directly with Svelte, Angular, vanilla JavaScript, or a custom adapter.
|
|
1352
|
-
Continue with ductape_docs({ topic: "client" }).
|
|
1353
|
-
|
|
1354
|
-
Do not install @ductape/client separately when using @ductape/react or @ductape/vue unless the
|
|
1355
|
-
application also needs direct access to a client API not exposed by the framework package. Both
|
|
1356
|
-
framework packages wrap @ductape/client and expose the underlying client through useDuctape().
|
|
1357
|
-
|
|
1358
|
-
SHARED APPLICATION LIFECYCLE
|
|
1359
|
-
1. Create one client at the application root using a browser-safe publishableKey, proxy baseUrl,
|
|
1360
|
-
default product tag, and environment slug.
|
|
1361
|
-
2. Connect the real-time client only in the browser. In React use DuctapeProvider autoConnect or
|
|
1362
|
-
connect() after mount; in Vue use createDuctape({ autoConnect: true }) or connect() in onMounted.
|
|
1363
|
-
3. Authenticate the player with the sessions API and retain the returned session token according
|
|
1364
|
-
to the application's security policy. Refresh it before expiry (the React/Vue packages provide
|
|
1365
|
-
useSessionAutoRefresh) and revoke it on logout.
|
|
1366
|
-
4. Pass the session value when connecting a broker that requires player-scoped authorization.
|
|
1367
|
-
5. Subscribe through the framework hook/composable or the client service. Always unsubscribe on
|
|
1368
|
-
component teardown; the framework integrations do this automatically for declarative hooks.
|
|
1369
|
-
6. The underlying client reconnects its WebSocket and restores active subscriptions. Observe
|
|
1370
|
-
connectionState to render disconnected/reconnecting UI; do not create duplicate subscriptions.
|
|
1371
|
-
7. Disconnect resource sessions and the root client when the owning application scope is torn down.
|
|
1372
|
-
|
|
1373
|
-
PRODUCT ANALYTICS
|
|
1374
|
-
Frontend product analytics complements backend session propagation; neither replaces the other.
|
|
1375
|
-
Continue with ductape_docs({ topic: "frontend-analytics" }) for identity lifecycle, pageviews,
|
|
1376
|
-
custom events, privacy masking, hidden-state safety, trace correlation, and package-version checks.
|
|
1377
|
-
|
|
1378
|
-
AUTHENTICATION AND SECURITY
|
|
1379
|
-
- Use publishableKey in browser applications. Never ship workspace private keys or privileged
|
|
1380
|
-
access keys in frontend bundles.
|
|
1381
|
-
- A Ductape player session and the real-time transport connection are separate: authenticate or
|
|
1382
|
-
refresh the session, then use that session when opening player-scoped broker resources.
|
|
1383
|
-
- For SSR, create/connect the real-time client only on the browser side.
|
|
1384
|
-
|
|
1385
|
-
MIGRATING A CUSTOM WRAPPER
|
|
1386
|
-
- Keep @ductape/client when the wrapper implements application-specific projection or state logic.
|
|
1387
|
-
- For React, move root connection ownership to DuctapeProvider, replace imperative subscriptions
|
|
1388
|
-
with useBroker/useBrokerSubscription, and keep projection reduction in an application hook.
|
|
1389
|
-
- For Vue, move root ownership to createDuctape(), replace imperative subscriptions with
|
|
1390
|
-
useBroker/useBrokerSubscription, and keep projection reduction in an application composable.
|
|
1391
|
-
- Verify session handoff, initial loading state, error state, reconnect UI, subscription cleanup,
|
|
1392
|
-
and duplicate-event behavior with a live end-to-end run before removing the old wrapper.
|
|
1393
|
-
`.trim(),
|
|
1394
|
-
|
|
1395
|
-
transactions: `
|
|
1396
|
-
DUCTAPE DATABASE TRANSACTIONS
|
|
1397
|
-
|
|
1398
|
-
Ask the user which database type and tier they are using before advising on transaction support.
|
|
1399
|
-
|
|
1400
|
-
Support matrix:
|
|
1401
|
-
PostgreSQL — full ACID; all isolation levels (READ_UNCOMMITTED, READ_COMMITTED,
|
|
1402
|
-
REPEATABLE_READ, SERIALIZABLE). Always supported.
|
|
1403
|
-
MySQL — full ACID; same isolation levels as PostgreSQL.
|
|
1404
|
-
MongoDB — multi-document transactions require a replica set.
|
|
1405
|
-
Atlas M10+ (dedicated): supported.
|
|
1406
|
-
Atlas M0/M2/M5 (free/shared): NOT supported — all writes are
|
|
1407
|
-
single-document atomic only. Confirm tier with user before proceeding.
|
|
1408
|
-
Self-hosted: supported if replica set mode is enabled.
|
|
1409
|
-
Cassandra — does NOT support ACID transactions. Lightweight transactions (LWT)
|
|
1410
|
-
only for single-partition compare-and-set.
|
|
1411
|
-
DynamoDB — transactional API supported (TransactWrite) but NOT via Ductape's
|
|
1412
|
-
transaction abstraction; use single operations with condition expressions.
|
|
1413
|
-
|
|
1414
|
-
Usage patterns:
|
|
1415
|
-
// Managed (preferred) — auto-commit on success, auto-rollback on error:
|
|
1416
|
-
await ductape.database.transaction(
|
|
1417
|
-
{ product, env, database, isolationLevel: 'READ_COMMITTED' },
|
|
1418
|
-
async (tx) => {
|
|
1419
|
-
await ductape.database.insert({ ..., transaction: tx });
|
|
1420
|
-
await ductape.database.update({ ..., transaction: tx });
|
|
1421
|
-
}
|
|
1422
|
-
);
|
|
1423
|
-
|
|
1424
|
-
// Manual — commit/rollback yourself:
|
|
1425
|
-
const tx = await ductape.database.beginTransaction({ product, env, database });
|
|
1426
|
-
try {
|
|
1427
|
-
await ductape.database.insert({ ..., transaction: tx });
|
|
1428
|
-
await tx.commit();
|
|
1429
|
-
} catch (e) {
|
|
1430
|
-
await tx.rollback();
|
|
1431
|
-
throw e;
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
Via ductape_execute:
|
|
1435
|
-
ductape_execute("databases.beginTransaction", [{ product, env, database, isolationLevel? }])
|
|
1436
|
-
→ returns a transaction reference you can pass to subsequent insert/update/delete calls.
|
|
1437
|
-
`.trim(),
|
|
1438
|
-
|
|
1439
|
-
presave: `
|
|
1440
|
-
DUCTAPE PRE-SAVE HOOKS
|
|
1441
|
-
|
|
1442
|
-
Pre-save operations transform or validate data before it is written to the database.
|
|
1443
|
-
Pass a preSave array to insert or update operations. Operations run in priority order.
|
|
1444
|
-
|
|
1445
|
-
Available operations (PreSaveOperationType):
|
|
1446
|
-
encrypt — AES-256 encrypt field (use for PII, secrets)
|
|
1447
|
-
hash — one-way hash (bcrypt/argon2 for passwords)
|
|
1448
|
-
mask — mask sensitive data (e.g. **** **** **** 1234)
|
|
1449
|
-
trim — strip leading/trailing whitespace
|
|
1450
|
-
lowercase — convert string to lowercase
|
|
1451
|
-
uppercase — convert string to uppercase
|
|
1452
|
-
sanitize — strip HTML/script tags
|
|
1453
|
-
validate — throw if field value fails a rule
|
|
1454
|
-
transform — apply a custom mapping function
|
|
1455
|
-
uuid — generate a UUID for the field
|
|
1456
|
-
slug — generate a URL slug from another field
|
|
1457
|
-
timestamp — set field to current timestamp (ISO 8601)
|
|
1458
|
-
round — round number to N decimal places
|
|
1459
|
-
clamp — clamp number to a min/max range
|
|
1460
|
-
truncate — truncate string to max length
|
|
1461
|
-
default — set a default value if null/undefined
|
|
1462
|
-
normalizePhone — normalise phone number format
|
|
1463
|
-
normalizeEmail — lowercase + trim email
|
|
1464
|
-
parseJson — parse JSON string to object
|
|
1465
|
-
stringifyJson — serialise object to JSON string
|
|
1466
|
-
compute — derive field value from other fields (runs before validate)
|
|
1467
|
-
|
|
1468
|
-
Execution order is fixed regardless of declaration order:
|
|
1469
|
-
default → compute → validate → trim → lowercase/uppercase/normalize* →
|
|
1470
|
-
sanitize → truncate → slug → round/clamp → parseJson/stringifyJson → uuid/timestamp → transform
|
|
1471
|
-
|
|
1472
|
-
Example:
|
|
1473
|
-
await ductape.database.insert({
|
|
1474
|
-
product, env, database: 'users', entity: 'users',
|
|
1475
|
-
data: { email: ' User@Example.com ', password: 'secret', username: 'My App User' },
|
|
1476
|
-
preSave: [
|
|
1477
|
-
{ field: 'email', operation: 'normalizeEmail' },
|
|
1478
|
-
{ field: 'password', operation: 'hash' },
|
|
1479
|
-
{ field: 'username', operation: 'slug', target: 'slug' },
|
|
1480
|
-
],
|
|
1481
|
-
});
|
|
1482
|
-
`.trim(),
|
|
1483
|
-
|
|
1484
|
-
triggers: `
|
|
1485
|
-
DUCTAPE DATABASE TRIGGERS
|
|
1486
|
-
|
|
1487
|
-
Triggers fire automatically in response to database write events.
|
|
1488
|
-
Defined via the Workbench UI or databases.trigger.create (admin SDK).
|
|
1489
|
-
|
|
1490
|
-
Events (TriggerEvent):
|
|
1491
|
-
beforeInsert / afterInsert
|
|
1492
|
-
beforeUpdate / afterUpdate
|
|
1493
|
-
beforeDelete / afterDelete
|
|
1494
|
-
beforeWrite / afterWrite (any write)
|
|
1495
|
-
|
|
1496
|
-
Timing (TriggerTiming):
|
|
1497
|
-
sync — block the write until trigger completes (adds latency)
|
|
1498
|
-
async — fire and forget (default for notifications, cache, broker)
|
|
1499
|
-
queued — enqueue for background processing
|
|
1500
|
-
|
|
1501
|
-
Action types (TriggerActionType):
|
|
1502
|
-
database.* — insert/update/delete/query another collection
|
|
1503
|
-
storage.* — upload/delete/copy a file
|
|
1504
|
-
notification.* — email/SMS/push/callback
|
|
1505
|
-
broker.publish — publish to a message broker topic
|
|
1506
|
-
cache.* — set/invalidate/delete a cache key
|
|
1507
|
-
feature.* — execute or dispatch a Ductape feature
|
|
1508
|
-
action.execute — call an app action
|
|
1509
|
-
agent.run — run an AI agent
|
|
1510
|
-
quota.run / fallback.run / healthcheck.run
|
|
1511
|
-
vector.upsert / vector.delete
|
|
1512
|
-
session.revoke
|
|
1513
|
-
log.create
|
|
1514
|
-
custom.function / custom.http
|
|
1515
|
-
|
|
1516
|
-
All triggers support an optional condition (field comparisons) to gate execution.
|
|
1517
|
-
All triggers support retry config: { maxAttempts, delay, backoff: "fixed"|"exponential" }.
|
|
1518
|
-
|
|
1519
|
-
Triggers are admin-only configuration — they cannot be set at runtime via ductape_execute.
|
|
1520
|
-
Use ductape_cli with the Workbench or databases.trigger.create via the admin SDK.
|
|
1521
|
-
`.trim(),
|
|
1522
|
-
|
|
1523
|
-
aggregations: `
|
|
1524
|
-
DUCTAPE DATABASE AGGREGATIONS
|
|
1525
|
-
|
|
1526
|
-
databases.aggregate [{ product, env, database, entity, aggregations, where?, groupBy? }]
|
|
1527
|
-
|
|
1528
|
-
aggregations array — each entry:
|
|
1529
|
-
{ type: "count"|"sum"|"avg"|"min"|"max", field?: string, alias: string }
|
|
1530
|
-
|
|
1531
|
-
Example — total revenue grouped by category:
|
|
1532
|
-
ductape_execute("databases.aggregate", [{
|
|
1533
|
-
product: "my-product",
|
|
1534
|
-
env: "prd",
|
|
1535
|
-
database: "sales-db",
|
|
1536
|
-
entity: "orders",
|
|
1537
|
-
aggregations: [
|
|
1538
|
-
{ type: "sum", field: "amount", alias: "totalRevenue" },
|
|
1539
|
-
{ type: "count", alias: "orderCount" },
|
|
1540
|
-
{ type: "avg", field: "amount", alias: "avgOrderValue" },
|
|
1541
|
-
],
|
|
1542
|
-
groupBy: ["category"],
|
|
1543
|
-
where: { status: { $eq: "completed" } },
|
|
1544
|
-
}])
|
|
1545
|
-
|
|
1546
|
-
Supported where operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $like, $ilike
|
|
1547
|
-
groupBy: array of field names to group by (omit for global aggregation)
|
|
1548
|
-
`.trim(),
|
|
1549
|
-
|
|
1550
|
-
migrations: `
|
|
1551
|
-
DUCTAPE DATABASE MIGRATIONS
|
|
1552
|
-
|
|
1553
|
-
Migrations are versioned SQL/NoSQL schema change scripts managed per database component.
|
|
1554
|
-
|
|
1555
|
-
Create migrations through the project migration files and ductape_cli, never ductape_execute.
|
|
1556
|
-
|
|
1557
|
-
Use the access-key administrative CLI for running, rolling back, and inspecting migrations:
|
|
1558
|
-
ductape_cli("db migrate") // run pending
|
|
1559
|
-
ductape_cli("db migrate rollback") // roll back last
|
|
1560
|
-
ductape_cli("db migrate rollback -n 3")
|
|
1561
|
-
|
|
1562
|
-
MongoDB: migrations run as raw Mongo shell commands in the up/down arrays.
|
|
1563
|
-
`.trim(),
|
|
1564
|
-
|
|
1565
|
-
indexes: `
|
|
1566
|
-
DUCTAPE DATABASE INDEXES
|
|
1567
|
-
|
|
1568
|
-
Create or update indexes through migration files and ductape_cli("db migrate"), not the
|
|
1569
|
-
publishable-key runtime proxy. Example definitions may use ["field1", "field2"] with
|
|
1570
|
-
{ unique: true, name: "idx_email_unique" }, or ordered fields such as
|
|
1571
|
-
[{ field: "createdAt", order: "DESC" }, { field: "userId", order: "ASC" }].
|
|
1572
|
-
|
|
1573
|
-
Drop and inspect indexes through migration/CLI tooling or the database administration surface,
|
|
1574
|
-
not ductape_execute.
|
|
1575
|
-
|
|
1576
|
-
Performance guidance:
|
|
1577
|
-
- Index fields used in WHERE clauses, JOIN conditions, and ORDER BY.
|
|
1578
|
-
- Compound indexes: put high-cardinality fields first.
|
|
1579
|
-
- Unique indexes enforce uniqueness at the database level.
|
|
1580
|
-
- MongoDB: compound indexes support prefix queries; order matters.
|
|
1581
|
-
- PostgreSQL/MySQL: B-tree indexes by default; use partial indexes for sparse columns.
|
|
1582
|
-
- Avoid over-indexing writes-heavy collections — each index slows inserts/updates.
|
|
1583
|
-
|
|
1584
|
-
Before adding an index ask the user which fields they query/filter on and expected data volume.
|
|
1585
|
-
`.trim(),
|
|
1586
|
-
|
|
1587
|
-
performance: `
|
|
1588
|
-
DUCTAPE DATABASE PERFORMANCE GUIDANCE
|
|
1589
|
-
|
|
1590
|
-
1. Queries
|
|
1591
|
-
- Always pass select: ["field1", "field2"] to avoid fetching unused columns.
|
|
1592
|
-
- Use limit + offset for pagination; add an index on the ORDER BY field.
|
|
1593
|
-
- Use count/sum/avg/aggregate instead of fetching all rows and computing in code.
|
|
1594
|
-
|
|
1595
|
-
2. Indexes (see ductape_docs({ topic: "indexes" }) for syntax)
|
|
1596
|
-
- Index every WHERE / ORDER BY field.
|
|
1597
|
-
- Compound index order: equality fields first, range fields last.
|
|
1598
|
-
- For MongoDB: covered queries (index holds all projected fields) skip the document read.
|
|
1599
|
-
|
|
1600
|
-
3. Connections
|
|
1601
|
-
- The SDK pools connections per product+env+database triple.
|
|
1602
|
-
- Call databases.connect once per process; the pool reuses the connection.
|
|
1603
|
-
- databases.disconnect releases the pool — call only on shutdown.
|
|
1604
|
-
|
|
1605
|
-
4. Transactions (see ductape_docs({ topic: "transactions" }))
|
|
1606
|
-
- Keep transactions short; long-lived transactions block row locks (PostgreSQL/MySQL).
|
|
1607
|
-
- For MongoDB on M10+, use sessions for multi-document writes — not for reads.
|
|
1608
|
-
|
|
1609
|
-
5. Caching
|
|
1610
|
-
- For read-heavy, rarely-changing data use caches.get before databases.query.
|
|
1611
|
-
- Invalidate cache keys in an afterWrite trigger (see ductape_docs({ topic: "triggers" })).
|
|
1612
|
-
`.trim(),
|
|
1613
|
-
|
|
1614
|
-
actions: `
|
|
1615
|
-
DUCTAPE DATABASE ACTIONS
|
|
1616
|
-
|
|
1617
|
-
A database action is a saved query or mutation (SQL string or NoSQL command) stored
|
|
1618
|
-
on the Ductape product and executed by tag at runtime.
|
|
1619
|
-
|
|
1620
|
-
Create/update actions in Workbench (administrative access), never through ductape_execute.
|
|
1621
|
-
|
|
1622
|
-
Dispatch an action at runtime:
|
|
1623
|
-
→ CALL ductape_generate_payload FIRST to get the canonical input shape.
|
|
1624
|
-
ductape_execute("databases.action.dispatch", [{
|
|
1625
|
-
product: "my-product",
|
|
1626
|
-
env: "prd",
|
|
1627
|
-
database: "core-db",
|
|
1628
|
-
action: "get-active-users",
|
|
1629
|
-
input: { status: "active" },
|
|
1630
|
-
}])
|
|
1631
|
-
|
|
1632
|
-
List actions for a database:
|
|
1633
|
-
ductape_execute("databases.action.list", ["database_tag"])
|
|
1634
|
-
|
|
1635
|
-
Fetch:
|
|
1636
|
-
ductape_execute("databases.action.fetch", ["action_tag"])
|
|
1637
|
-
Update/delete are administrative and must be performed in Workbench.
|
|
1638
|
-
|
|
1639
|
-
Actions are the preferred way to encapsulate complex or reused queries — they can be
|
|
1640
|
-
scheduled, dispatched with retries, and audited via logs.
|
|
1641
|
-
`.trim(),
|
|
1642
|
-
|
|
1643
|
-
graphs: `
|
|
1644
|
-
DUCTAPE GRAPH DATABASES
|
|
1645
|
-
|
|
1646
|
-
Supported engines: neo4j | neptune | cosmos-gremlin | spanner-graph | arangodb | memgraph
|
|
1647
|
-
|
|
1648
|
-
Registration (admin — ductape_cli):
|
|
1649
|
-
ductape_cli("resources graphs create -f graph.json")
|
|
1650
|
-
File: { name, tag, type, envs: [{ slug, connection_url, username?, password?, graphName?, region? }] }
|
|
1651
|
-
Sensitive fields (connection_url, username, password) are auto-wrapped as $Secret{...} when
|
|
1652
|
-
a productTag is supplied — do NOT pre-wrap them yourself.
|
|
1653
|
-
|
|
1654
|
-
Node operations:
|
|
1655
|
-
graph.createNode [{ labels: string[], properties: { key: value } }, transaction?]
|
|
1656
|
-
graph.findNodes [{ labels?, where?, limit?, skip? }, transaction?]
|
|
1657
|
-
graph.findNodeById [id, transaction?]
|
|
1658
|
-
graph.updateNode [{ id, properties }, transaction?]
|
|
1659
|
-
graph.deleteNode [{ id, detach?: boolean }, transaction?]
|
|
1660
|
-
graph.mergeNode [{ labels, matchProps, setProps? }, transaction?]
|
|
1661
|
-
graph.addLabels / removeLabels / setLabels [{ id, labels }, transaction?]
|
|
1662
|
-
|
|
1663
|
-
Relationship operations:
|
|
1664
|
-
graph.createRelationship [{ fromId, toId, type, properties? }, transaction?]
|
|
1665
|
-
graph.findRelationships [{ type?, where?, limit? }, transaction?]
|
|
1666
|
-
graph.updateRelationship [{ id, properties }, transaction?]
|
|
1667
|
-
graph.deleteRelationship [{ id }, transaction?]
|
|
1668
|
-
graph.mergeRelationship [{ fromId, toId, type, matchProps?, setProps? }, transaction?]
|
|
1669
|
-
|
|
1670
|
-
Traversal:
|
|
1671
|
-
graph.traverse [{ startId, direction: "in"|"out"|"both", relationshipTypes?, maxDepth?, where? }]
|
|
1672
|
-
graph.shortestPath [{ fromId, toId, relationshipType?, maxDepth? }]
|
|
1673
|
-
graph.allPaths [{ fromId, toId, relationshipType?, maxDepth? }]
|
|
1674
|
-
graph.getNeighborhood [{ id, depth?, relationshipTypes? }]
|
|
1675
|
-
graph.findConnectedComponents [{ labels? }]
|
|
1676
|
-
|
|
1677
|
-
Raw query (provider-native Cypher/Gremlin):
|
|
1678
|
-
graph.query [cypher_query: string, params?: { key: value }, transaction?]
|
|
1679
|
-
|
|
1680
|
-
Full-text and vector search (where supported):
|
|
1681
|
-
graph.fullTextSearch [{ index, query, limit? }]
|
|
1682
|
-
graph.vectorSearch [{ index, vector: number[], topK? }]
|
|
1683
|
-
|
|
1684
|
-
Transactions:
|
|
1685
|
-
graph.beginTransaction [options?: { isolationLevel?, timeout? }] → transaction object
|
|
1686
|
-
graph.commitTransaction [transaction]
|
|
1687
|
-
graph.rollbackTransaction [transaction]
|
|
1688
|
-
Or use withTransaction (managed — auto-commit/rollback):
|
|
1689
|
-
graph.withTransaction [graphTag, env, callback, options?]
|
|
1690
|
-
Transaction support varies by engine — ask the user which engine they use before enabling.
|
|
1691
|
-
neo4j: full ACID; neptune: limited; arangodb: multi-document; memgraph: full ACID.
|
|
1692
|
-
|
|
1693
|
-
Schema management:
|
|
1694
|
-
graph.createNodeIndex [{ label, field, type: "btree"|"fulltext"|"vector" }]
|
|
1695
|
-
graph.createNodeConstraint [{ label, field, type: "unique"|"exists" }]
|
|
1696
|
-
graph.dropIndex / dropConstraint / listIndexes / listConstraints
|
|
1697
|
-
|
|
1698
|
-
Saved actions (parameterized queries stored on the product):
|
|
1699
|
-
graph.createAction [{ graphTag, tag, name, query, params? }, productTag?]
|
|
1700
|
-
graph.listActions [graphTag?, productTag?]
|
|
1701
|
-
graph.dispatch [data] ← call ductape_generate_payload FIRST
|
|
1702
|
-
|
|
1703
|
-
Supported index types: btree | fulltext | vector | range | point | text
|
|
1704
|
-
Supported constraint types: UNIQUE | EXISTS | NODE_KEY
|
|
1705
|
-
`.trim(),
|
|
1706
|
-
|
|
1707
|
-
storage: `
|
|
1708
|
-
DUCTAPE FILE STORAGE
|
|
1709
|
-
|
|
1710
|
-
Supported providers: aws (S3) | gcp (GCS) | azure (Blob Storage)
|
|
1711
|
-
|
|
1712
|
-
Registration (admin — ductape_cli):
|
|
1713
|
-
ductape_cli("resources storage create -f storage.json")
|
|
1714
|
-
File: { name, tag, envs: [{ slug, type: "aws"|"gcp"|"azure", config: { ... } }] }
|
|
1715
|
-
For cloud-linked envs: set config.cloud to the connection tag; omit raw credentials.
|
|
1716
|
-
For all envs in the product must be covered — see ductape_docs({ topic: "cloud" }) for
|
|
1717
|
-
import-persist-all workflow when connecting existing buckets.
|
|
1718
|
-
|
|
1719
|
-
Operations (runtime):
|
|
1720
|
-
storage.upload [{ product, env, storage, fileName, buffer: string|Buffer, mimeType? }]
|
|
1721
|
-
storage.download [{ product, env, storage, fileName }] → { content, size, mimeType }
|
|
1722
|
-
storage.remove [{ product, env, storage, fileName }]
|
|
1723
|
-
storage.listFiles [{ product, env, storage, prefix?, limit?, continuationToken? }]
|
|
1724
|
-
storage.getSignedUrl [{ product, env, storage, fileName, expiresIn?: number, action?: "read"|"write" }]
|
|
1725
|
-
storage.stats [{ product, env, storage, prefix?, cache? }] → file counts by type
|
|
1726
|
-
storage.testConnection [{ product, env, storage }]
|
|
1727
|
-
|
|
1728
|
-
Background dispatch (fire-and-forget):
|
|
1729
|
-
storage.dispatch [{ product, env, storage, operation, input, schedule? }]
|
|
1730
|
-
→ CALL ductape_generate_payload FIRST (operation_family="storage", method="dispatch")
|
|
1731
|
-
→ Returns { jobId, status: "queued" } immediately; actual operation runs in background.
|
|
1732
|
-
→ schedule: { start_at?, cron?, every?, limit?, endDate?, tz? }
|
|
1733
|
-
|
|
1734
|
-
Caching:
|
|
1735
|
-
download, listFiles, getSignedUrl, and stats all support a cache option.
|
|
1736
|
-
Pass cache: "cache_tag" to check the product cache before hitting the provider.
|
|
1737
|
-
Cached results are stored fire-and-forget on miss.
|
|
1738
|
-
|
|
1739
|
-
Notes:
|
|
1740
|
-
- buffer can be a Node.js Buffer (server) or a base64 string (JSON proxy clients).
|
|
1741
|
-
- Cloud-linked configs inject runtime credentials via the cloud connection; no plaintext keys stored.
|
|
1742
|
-
- $Secret{} references in config are resolved at runtime if cloud-link resolution fails.
|
|
1743
|
-
`.trim(),
|
|
1744
|
-
|
|
1745
|
-
cloud: `
|
|
1746
|
-
DUCTAPE CLOUD CONNECTIONS
|
|
1747
|
-
|
|
1748
|
-
Cloud connections link your workspace to an external cloud provider so Ductape can manage
|
|
1749
|
-
resources (databases, storage, brokers, graphs, vectors) on your behalf.
|
|
1750
|
-
|
|
1751
|
-
Supported providers: aws | gcp | azure | mongodb_atlas | neo4j_aura
|
|
1752
|
-
Auth modes: iam_role | oauth | service_principal | workload_identity | api_key
|
|
1753
|
-
|
|
1754
|
-
Create a connection (admin — ductape_cli):
|
|
1755
|
-
ductape_cli("cloud connections create --provider aws --name my-aws-conn")
|
|
1756
|
-
Returns: external_id, trust_policy, setup_instructions, setup_url
|
|
1757
|
-
AWS: the caller must create an IAM role using the returned trust policy, then complete:
|
|
1758
|
-
ductape_cli("cloud connections complete <tag> --role-arn arn:aws:iam::...")
|
|
1759
|
-
GCP: complete with project_id, service_account_email, service_account_json
|
|
1760
|
-
Azure: complete with tenant_id, subscription_id, client_id, client_secret, default_location
|
|
1761
|
-
Atlas: complete with atlas_public_key, atlas_private_key
|
|
1762
|
-
Aura (Neo4j): complete with aura_client_id, aura_client_secret, aura_instance_id
|
|
1763
|
-
|
|
1764
|
-
Validate a connection:
|
|
1765
|
-
ductape_cli("cloud connections validate <tag>")
|
|
1766
|
-
→ { valid, status, message?, tested_at? }
|
|
1767
|
-
|
|
1768
|
-
List / fetch / delete:
|
|
1769
|
-
ductape_cli("cloud connections list")
|
|
1770
|
-
ductape_cli("cloud connections fetch <tag>")
|
|
1771
|
-
|
|
1772
|
-
Discover resources on a connection:
|
|
1773
|
-
ductape_cli("cloud resources list -f query.json --json")
|
|
1774
|
-
File: { cloud: "<tag>", service: "gcs"|"s3"|"blob"|"rds"|"atlas-cluster"|..., region? }
|
|
1775
|
-
→ returns list of available resources (buckets, clusters, instances, etc.)
|
|
1776
|
-
|
|
1777
|
-
Import an existing resource and register it on the product:
|
|
1778
|
-
Use import-persist-all for multi-env products (required):
|
|
1779
|
-
ductape_cli("cloud resources import-persist-all -f all-envs.json --json")
|
|
1780
|
-
File is a JSON ARRAY — one entry per env, same product + component tag across all entries.
|
|
1781
|
-
Each entry: { cloud, service, type, product, component, env, resource, region?, dbName? }
|
|
1782
|
-
Supported service identifiers: s3, gcs, blob, rds, postgresql, cloudsql, sqs, pubsub,
|
|
1783
|
-
servicebus, neptune, cosmos-gremlin, opensearch, azure-search, atlas-cluster, aura-instance
|
|
1784
|
-
|
|
1785
|
-
Provision a brand-new resource and register it:
|
|
1786
|
-
ductape_cli("cloud resources provision-persist-all -f all-envs.json --json")
|
|
1787
|
-
Additional per-entry fields: tier, region/location, waitForReady.
|
|
1788
|
-
List available tiers first: ductape_cli("cloud tiers --provider aws --type database --db-type postgresql --json")
|
|
1789
|
-
NEVER infer region, tier, or cost — always list and confirm with the user first.
|
|
1790
|
-
Atlas and Neo4j Aura are IMPORT-ONLY — provision is not supported for these providers.
|
|
1791
|
-
|
|
1792
|
-
VPC connector (private networking):
|
|
1793
|
-
ductape_cli("cloud connections vpc update <tag> --vpc-id vpc-xxx --subnet-ids subnet-1,subnet-2")
|
|
1794
|
-
ductape_cli("cloud connections vpc status <tag>")
|
|
1795
|
-
VPC connector enables Ductape to reach resources in a private VPC; agent_status:
|
|
1796
|
-
pending → connected → disconnected
|
|
1797
|
-
|
|
1798
|
-
Scopes a connection can cover: storage | broker | database | graph | vector | cache
|
|
1799
|
-
|
|
1800
|
-
Component types accepted by import/provision:
|
|
1801
|
-
storage | messageBrokers | databases | graphs | vectors | caches
|
|
1802
|
-
`.trim(),
|
|
1803
|
-
|
|
1804
|
-
vector: `
|
|
1805
|
-
DUCTAPE VECTOR DATABASES
|
|
1806
|
-
|
|
1807
|
-
Supported adapters: pinecone | qdrant | weaviate | opensearch | azure-search | vertex-vector-search
|
|
1808
|
-
(chroma, milvus, pgvector are declared but not yet implemented — do not use them)
|
|
1809
|
-
|
|
1810
|
-
Registration (admin — ductape_cli):
|
|
1811
|
-
ductape_cli("resources vectors create -f vector.json")
|
|
1812
|
-
File: { name, tag, type, dimensions: number, metric?: "cosine"|"euclidean"|"dotproduct",
|
|
1813
|
-
envs: [{ slug, endpoint?, apiKey?, region?, index?, namespace? }] }
|
|
1814
|
-
|
|
1815
|
-
Runtime operations:
|
|
1816
|
-
vector.upsert [{ product, env, tag, vectors: [{id, values: number[], metadata?}], namespace? }]
|
|
1817
|
-
vector.upsertOne [{ product, env, tag, id, values: number[], metadata?, namespace? }]
|
|
1818
|
-
vector.query [{ product, env, tag, vector: number[], topK?, filter?, namespace?,
|
|
1819
|
-
includeValues?, includeMetadata? }]
|
|
1820
|
-
vector.findSimilar [{ product, env, vector, values: number[], topK?, filter?, namespace? }]
|
|
1821
|
-
vector.fetchOne [{ product, env, vector, id, namespace? }]
|
|
1822
|
-
vector.fetchVectors [{ product, env, vector, ids: string[], namespace? }]
|
|
1823
|
-
vector.updateVector [{ product, env, vector, id, values?, setMetadata?, mergeMetadata?, namespace? }]
|
|
1824
|
-
vector.updateMetadata [{ product, env, vector, id, metadata: { key: value }, merge?, namespace? }]
|
|
1825
|
-
merge: true → deep-merges metadata rather than replacing it
|
|
1826
|
-
vector.deleteByIds [{ product, env, vector, ids: string[], namespace? }]
|
|
1827
|
-
vector.deleteAll [{ product, env, vector, namespace? }]
|
|
1828
|
-
vector.count [{ product, env, vector, namespace? }]
|
|
1829
|
-
|
|
1830
|
-
Namespace management:
|
|
1831
|
-
vector.listNamespaces [{ product, env, vector }]
|
|
1832
|
-
vector.deleteNamespace [{ product, env, vector, namespace }]
|
|
1833
|
-
|
|
1834
|
-
Listing (paginated):
|
|
1835
|
-
vector.listVectors [{ product, env, vector, namespace?, prefix?, limit?, cursor? }]
|
|
1836
|
-
vector.listAllVectors [{ product, env, vector, namespace?, prefix? }]
|
|
1837
|
-
listAllVectors auto-paginates until cursor exhausted — avoid on large indexes.
|
|
1838
|
-
|
|
1839
|
-
Index management:
|
|
1840
|
-
vector.describeIndex [{ product, env, vector }]
|
|
1841
|
-
vector.getStats [{ product, env, vector }] → totalVectorCount per namespace
|
|
1842
|
-
vector.createIndex [{ product, env, vector, name, dimensions, metric?, replicas?, shards? }]
|
|
1843
|
-
vector.deleteIndex [{ product, env, vector, name }]
|
|
1844
|
-
vector.listIndexes [{ product, env, vector }]
|
|
1845
|
-
|
|
1846
|
-
Distance metrics: cosine | euclidean | dotproduct | manhattan | hamming
|
|
1847
|
-
Index types: flat | ivf | hnsw | pq | ivf_pq | annoy
|
|
1848
|
-
Feature flags per adapter: metadata_filtering | namespaces | hybrid_search | batch_operations |
|
|
1849
|
-
index_management | vector_updates | sparse_vectors | multi_vector | aggregations
|
|
1850
|
-
|
|
1851
|
-
Call vector.supportsFeature(feature) or vector.getSupportedFeatures() to check what the
|
|
1852
|
-
chosen adapter supports before using a feature.
|
|
1853
|
-
`.trim(),
|
|
1854
|
-
|
|
1855
|
-
warehouse: `
|
|
1856
|
-
DUCTAPE WAREHOUSE
|
|
1857
|
-
|
|
1858
|
-
The Warehouse is a unified query layer over the three structured data stores: Database, Graph, and Vector.
|
|
1859
|
-
It provides a single API for cross-store operations without having to address each service separately.
|
|
1860
|
-
|
|
1861
|
-
Warehouse is available on the SDK instance as ductape.warehouse (TypeScript) or Warehouse (C#/Go/Java).
|
|
1862
|
-
It is NOT a separate Ductape resource — it wraps the existing database, graph, and vector components
|
|
1863
|
-
that are already registered on the product.
|
|
1864
|
-
|
|
1865
|
-
Key use cases:
|
|
1866
|
-
- Run a relational query, a graph traversal, and a vector similarity search in a single call.
|
|
1867
|
-
- Build recommendation pipelines: graph neighbors → vector re-rank → database hydration.
|
|
1868
|
-
- Federated search across multiple store types with a single await.
|
|
1869
|
-
|
|
1870
|
-
Usage (TypeScript SDK):
|
|
1871
|
-
const result = await ductape.warehouse.query({
|
|
1872
|
-
product: "my-product",
|
|
1873
|
-
env: "prd",
|
|
1874
|
-
database: { tag: "core-db", entity: "products", where: { active: { $eq: true } }, select: ["id", "name"] },
|
|
1875
|
-
graph: { tag: "reco-graph", startId: userId, direction: "out", maxDepth: 2 },
|
|
1876
|
-
vector: { tag: "embedding-store", vector: queryEmbedding, topK: 10 },
|
|
1877
|
-
});
|
|
1878
|
-
|
|
1879
|
-
result.database → relational rows
|
|
1880
|
-
result.graph → traversal nodes/relationships
|
|
1881
|
-
result.vector → similarity hits with scores
|
|
1882
|
-
|
|
1883
|
-
Warehouse context is set at SDK init time (env + workspaceId) and shared across all three stores.
|
|
1884
|
-
Underlying store calls use the same auth and product context; individual store errors are surfaced per store.
|
|
1885
|
-
|
|
1886
|
-
Before using Warehouse:
|
|
1887
|
-
1. Ensure the database, graph, and vector components are registered on the product.
|
|
1888
|
-
2. Confirm the env slug exists on all three stores.
|
|
1889
|
-
3. Each store can be queried independently — pass only the fields for the stores you need.
|
|
1890
|
-
|
|
1891
|
-
Warehouse does not support writes — use the individual service APIs (database.insert, graph.createNode,
|
|
1892
|
-
vector.upsert) for mutations.
|
|
1893
|
-
`.trim(),
|
|
1894
|
-
|
|
1895
|
-
secrets: `
|
|
1896
|
-
DUCTAPE SECRETS
|
|
1897
|
-
|
|
1898
|
-
Secrets are workspace-level encrypted key-value pairs. They are referenced in resource configs,
|
|
1899
|
-
connection URLs, and any string field using the $Secret{KEY_NAME} syntax.
|
|
1900
|
-
|
|
1901
|
-
Create/update secrets with ductape_cli or Workbench (administrative access), never ductape_execute.
|
|
1902
|
-
Definition fields include key, value, description, token_type, scope, envs, and expires_at.
|
|
1903
|
-
The server never receives the plaintext value. Encryption uses the workspace private key.
|
|
1904
|
-
|
|
1905
|
-
Fetch / resolve:
|
|
1906
|
-
ductape_execute("secrets.fetch", ["STRIPE_API_KEY"]) → decrypted string value
|
|
1907
|
-
ductape_execute("secrets.exists", ["STRIPE_API_KEY"]) → boolean
|
|
1908
|
-
ductape_execute("secrets.validate", ["$Secret{STRIPE_API_KEY}"]) → { valid, missingKeys, existingKeys }
|
|
1909
|
-
ductape_execute("secrets.resolve", ["$Secret{STRIPE_API_KEY}", { env: "prd" }]) → resolved string
|
|
1910
|
-
|
|
1911
|
-
$Secret{} reference syntax:
|
|
1912
|
-
- Embed anywhere a string is accepted: "postgresql://$Secret{DB_USER}:$Secret{DB_PASS}@host/db"
|
|
1913
|
-
- Resolved at runtime before the value is used by the consuming service (storage, broker, graph, etc.)
|
|
1914
|
-
- The in-memory cache stores the encrypted form only; decryption happens on each cache hit.
|
|
1915
|
-
- Cache TTL: 5 minutes. Clear with: secrets.clearCache()
|
|
1916
|
-
|
|
1917
|
-
Lifecycle mutations (revoke/delete/update) are administrative: use ductape_cli or Workbench.
|
|
1918
|
-
|
|
1919
|
-
List all secrets (keys only — values are not returned in list):
|
|
1920
|
-
ductape_execute("secrets.list", [])
|
|
1921
|
-
|
|
1922
|
-
Important:
|
|
1923
|
-
- Secrets are workspace-scoped, not product-scoped. scope[] and envs[] control access.
|
|
1924
|
-
- Other services (storage, broker, graph, etc.) resolve $Secret{} references automatically
|
|
1925
|
-
using the singleton secrets service — no manual resolution needed in most cases.
|
|
1926
|
-
- Never log or return resolved secret values to end users.
|
|
1927
|
-
`.trim(),
|
|
1928
|
-
|
|
1929
|
-
apps: `
|
|
1930
|
-
DUCTAPE APPS
|
|
1931
|
-
|
|
1932
|
-
WHAT AN APP IS:
|
|
1933
|
-
A Ductape App is a pre-configured, versioned API integration definition. It is NOT a generic HTTP
|
|
1934
|
-
client, NOT a job scheduler, and NOT anything you can call without registering first.
|
|
1935
|
-
|
|
1936
|
-
An App must be fully set up in Ductape before any code can use it:
|
|
1937
|
-
1. The App record must be created (name, tag, description)
|
|
1938
|
-
2. Environments must be added (each environment slug → base URL for that stage)
|
|
1939
|
-
3. Auth scheme must be configured (how outbound requests authenticate: apikey, bearer, OAuth2, etc.)
|
|
1940
|
-
4. Action endpoints must be defined (each action = one HTTP endpoint spec: method, path, body/query/header shape, response shape)
|
|
1941
|
-
5. The App must be connected to the product (product.apps.add) and its envs mapped
|
|
1942
|
-
|
|
1943
|
-
ONLY after all five steps can any code call:
|
|
1944
|
-
ctx.api.run({ app: '<app_tag>', event: '<action_tag>', input: { ... } }) ← in a feature handler
|
|
1945
|
-
actions.run([{ product, env, app: '<app_tag>', action: '<action_tag>', input }]) ← at runtime
|
|
1946
|
-
|
|
1947
|
-
ctx.api (alias for ctx.action) is NOT a generic HTTP call. It ONLY invokes a pre-registered
|
|
1948
|
-
Ductape App Action. If the App or action tag does not exist in the product, the call will fail.
|
|
1949
|
-
actions.dispatch is the same as actions.run but scheduled as a background job — it still requires
|
|
1950
|
-
a registered App. There is no way to dispatch a job to an arbitrary URL via ctx.api or actions.dispatch.
|
|
1951
|
-
|
|
1952
|
-
If a feature step needs to call an external service and no App is registered for it yet:
|
|
1953
|
-
→ Flag it as "App to create" in your plan (STEP 4 of the feature design workflow)
|
|
1954
|
-
→ Create the App and all its actions first (see below)
|
|
1955
|
-
→ Only then write the ctx.api.run call
|
|
1956
|
-
|
|
1957
|
-
An app is a versioned API integration definition. It contains environments (base URLs), actions
|
|
1958
|
-
(individual endpoint specs), auth schemes, webhooks, variables, and constants.
|
|
1959
|
-
|
|
1960
|
-
Create an app (admin — ductape_cli):
|
|
1961
|
-
ductape_cli("apps create --name \\"Email Service\\" --description \\"Transactional email\\"")
|
|
1962
|
-
ductape_cli("app.init", ["app_tag"]) → loads app into builder state
|
|
1963
|
-
|
|
1964
|
-
Import from a file:
|
|
1965
|
-
ductape_cli("apps import <file.json> -t postman|openapi")
|
|
1966
|
-
Supports Postman v2.1 collection and OpenAPI 3.0 spec.
|
|
1967
|
-
|
|
1968
|
-
Manage environments (base URLs per stage) in Workbench; this currently has no CLI command.
|
|
1969
|
-
|
|
1970
|
-
Discover apps in a product and their actions (ALWAYS do this before writing any ctx.api.run call):
|
|
1971
|
-
Step 1 — list apps connected to the product:
|
|
1972
|
-
ductape_cli("products get --tag <product_tag> --json") → full product document; check apps[]
|
|
1973
|
-
ductape_cli("products apps list --product <product_id> --json") → apps[] with access_tag, envs
|
|
1974
|
-
Step 2 — list actions in an app:
|
|
1975
|
-
ductape_execute("actions.list", [app_tag]) → returns all action tags + names
|
|
1976
|
-
Step 3 — fetch the input schema for an action:
|
|
1977
|
-
ductape_execute("actions.fetch", [app_tag, action_tag])
|
|
1978
|
-
→ returns { body: {fieldName: {type, required}}, params: {}, query: {}, headers: {} }
|
|
1979
|
-
OR: call ductape_generate_payload (operation_family="action", method="run",
|
|
1980
|
-
targets={app: "app_tag", action: "action_tag"}) to get the exact resolved payload shape
|
|
1981
|
-
Step 4 — call ductape_schema({ module: "app" }) if you need the JSON schema for creating/updating
|
|
1982
|
-
app resources (not for runtime input — use actions.fetch or ductape_generate_payload for that)
|
|
1983
|
-
NEVER assume action input field names. Always fetch the action definition first.
|
|
1984
|
-
|
|
1985
|
-
Manage actions (individual API endpoints) in Workbench or import an OpenAPI/Postman file.
|
|
1986
|
-
Creation/update are administrative and must never use ductape_execute.
|
|
1987
|
-
ductape_execute("actions.list", [app_tag])
|
|
1988
|
-
ductape_execute("actions.fetch", [app_tag, action_tag])
|
|
1989
|
-
|
|
1990
|
-
Action input — flat input format:
|
|
1991
|
-
Fields are resolved to the correct location (body/params/query/headers) by matching the action schema.
|
|
1992
|
-
For ambiguous keys, use explicit prefixes:
|
|
1993
|
-
input: { amount: 1000 } → auto-resolved (body.amount if body field exists)
|
|
1994
|
-
input: { "body:amount": 1000 } → explicit body
|
|
1995
|
-
input: { "params:id": "user_123" } → route parameter
|
|
1996
|
-
input: { "query:limit": 10 } → query string
|
|
1997
|
-
input: { "headers:X-Idempotency-Key": "..." } → request header
|
|
1998
|
-
Always use ductape_generate_payload or actions.fetch to know the exact field names — never guess.
|
|
1999
|
-
|
|
2000
|
-
Run an action at runtime:
|
|
2001
|
-
→ CALL ductape_generate_payload FIRST (operation_family="action", method="run", targets={app, action})
|
|
2002
|
-
ductape_execute("actions.run", [{ product, env, app, action, input: { "body:field": value } }])
|
|
2003
|
-
ductape_execute("actions.dispatch", [{ product, env, app, action, input, schedule? }])
|
|
2004
|
-
|
|
2005
|
-
Auth schemes (how the app authenticates outbound requests):
|
|
2006
|
-
Setup types: header | bearer | basic | oauth2 | apikey
|
|
2007
|
-
Configure auth in Workbench (administrative).
|
|
2008
|
-
ductape_execute("auths.list", [app_tag])
|
|
2009
|
-
|
|
2010
|
-
Webhooks (inbound events from the external service):
|
|
2011
|
-
Configure webhooks and webhook events in Workbench (administrative).
|
|
2012
|
-
|
|
2013
|
-
Variables (per-env mutable values) and Constants (fixed values):
|
|
2014
|
-
Configure variables and constants in Workbench (administrative).
|
|
2015
|
-
|
|
2016
|
-
Connecting an app to a product (after creation):
|
|
2017
|
-
NOTE: All product.* module methods require the access key and CANNOT use ductape_execute.
|
|
2018
|
-
Use ductape_cli for all product-level operations.
|
|
2019
|
-
|
|
2020
|
-
FULL FLOW to make an app callable from a product:
|
|
2021
|
-
1. Create the app: ductape_cli("apps create --name \\"Service Name\\" --description \\"...\\"")
|
|
2022
|
-
2. Add environments: Workbench
|
|
2023
|
-
3. Configure auth: Workbench
|
|
2024
|
-
4. Define actions: Workbench
|
|
2025
|
-
OR import: ductape_cli("apps import <file.json> -t postman|openapi")
|
|
2026
|
-
5. Connect to product: Requires the product_id (from ductape_cli("products get --tag <tag> --json")).
|
|
2027
|
-
There is no CLI command for this step — the SDK product.apps.add method requires
|
|
2028
|
-
an access key which only the backend can provide. Connect via Workbench.
|
|
2029
|
-
6. Verify: ductape_cli("products get --tag <product_tag> --json") → check apps[] contains the app
|
|
2030
|
-
ductape_execute("actions.list", [app_tag]) → verify actions are registered
|
|
2031
|
-
`.trim(),
|
|
2032
|
-
|
|
2033
|
-
products: `
|
|
2034
|
-
DUCTAPE PRODUCTS
|
|
2035
|
-
|
|
2036
|
-
A product is the top-level namespace for all Ductape infrastructure: apps, databases, graphs,
|
|
2037
|
-
vectors, storage, brokers, sessions, caches, notifications, resilience, features, jobs, and envs.
|
|
2038
|
-
Every SDK service call resolves within a product context.
|
|
2039
|
-
|
|
2040
|
-
IMPORTANT: The product module requires the access key. ALL product operations must use ductape_cli,
|
|
2041
|
-
not ductape_execute (which only accepts the publishable key and will return 403 for product.*).
|
|
2042
|
-
|
|
2043
|
-
Create a product:
|
|
2044
|
-
ductape_cli("products create --name \\"My App\\" --tag my-app")
|
|
2045
|
-
|
|
2046
|
-
Environments — every resource's envs array MUST cover all product env slugs:
|
|
2047
|
-
ductape_cli("products environments list <product_tag> --json")
|
|
2048
|
-
ductape_cli("products environments get <product_tag> <slug> --json")
|
|
2049
|
-
BEFORE registering any resource, always run environments list and collect all slugs.
|
|
2050
|
-
|
|
2051
|
-
Fetch / update:
|
|
2052
|
-
ductape_cli("products get --tag <product_tag> --json")
|
|
2053
|
-
ductape_cli("products get --id <product_id> --json")
|
|
2054
|
-
|
|
2055
|
-
Connect apps to a product:
|
|
2056
|
-
See ductape_docs({ topic: "apps" }) for product.apps.add / product.apps.list.
|
|
2057
|
-
|
|
2058
|
-
Resource registration (all via ductape_cli or ductape_execute — see per-topic docs):
|
|
2059
|
-
databases → ductape_docs({ topic: "transactions" })
|
|
2060
|
-
storage → ductape_docs({ topic: "storage" })
|
|
2061
|
-
graphs → ductape_docs({ topic: "graphs" })
|
|
2062
|
-
vectors → ductape_docs({ topic: "vector" })
|
|
2063
|
-
events → ductape_docs({ topic: "events" })
|
|
2064
|
-
caches → ductape_docs({ topic: "caches" })
|
|
2065
|
-
notifications → ductape_docs({ topic: "notifications" })
|
|
2066
|
-
sessions → ductape_docs({ topic: "sessions" })
|
|
2067
|
-
frontend analytics → ductape_docs({ topic: "frontend-analytics" })
|
|
2068
|
-
resilience → ductape_docs({ topic: "resilience" })
|
|
2069
|
-
features → ductape_docs({ topic: "features" })
|
|
2070
|
-
|
|
2071
|
-
Product structure (IProduct fields):
|
|
2072
|
-
_id, workspace_id, name, tag, description, private_key,
|
|
2073
|
-
apps[], envs[], databases[], graphs[], vectors[], storage[], messageBrokers[],
|
|
2074
|
-
caches[], sessions[], notifications[], quota[], fallback[], healthchecks[],
|
|
2075
|
-
workflows[] (features), models[], agents[], jobs[]
|
|
2076
|
-
|
|
2077
|
-
Bootstrap (single API call returning product context + component config + private key):
|
|
2078
|
-
Each service makes a single bootstrap call at first use; results are cached in BootstrapCache
|
|
2079
|
-
(Redis when available). This avoids repeated round-trips in high-frequency paths.
|
|
2080
|
-
`.trim(),
|
|
2081
|
-
|
|
2082
|
-
sessions: `
|
|
2083
|
-
DUCTAPE SESSIONS
|
|
2084
|
-
|
|
2085
|
-
A session is a named JWT schema on a product. It defines:
|
|
2086
|
-
- tag / name — unique identifier and display name
|
|
2087
|
-
- expiry + period — how long each issued JWT is valid (duration, not an absolute date)
|
|
2088
|
-
- selector — MUST be in the format "$Session{fieldName}" where fieldName is the key
|
|
2089
|
-
in the schema that is the PRIMARY user identifier (e.g. "$Session{playerId}").
|
|
2090
|
-
Plain dot-paths like "playerId" are REJECTED by the validator.
|
|
2091
|
-
This field becomes the lookup key for revoke, list, and analytics.
|
|
2092
|
-
- schema — SAMPLE DATA showing example values for each field embedded in the JWT.
|
|
2093
|
-
This is NOT a type declaration. Use actual example values.
|
|
2094
|
-
The value at the selector path must be a primitive (string/number/boolean),
|
|
2095
|
-
not an object or array.
|
|
2096
|
-
|
|
2097
|
-
IMPORTANT — schema is sample data, not type declarations:
|
|
2098
|
-
CORRECT: schema: { playerId: "player_abc123", username: "Alice", role: "player" }
|
|
2099
|
-
INCORRECT: schema: { playerId: { type: "string", required: true } } ← WILL FAIL
|
|
2100
|
-
|
|
2101
|
-
IMPORTANT — selector must be "$Session{fieldName}" format:
|
|
2102
|
-
CORRECT: selector: "$Session{playerId}"
|
|
2103
|
-
INCORRECT: selector: "playerId" ← WILL FAIL with "Selector should be in the format $Session{...}{key}"
|
|
2104
|
-
|
|
2105
|
-
Example definition (configure via declarative apply or Workbench, not ductape_execute):
|
|
2106
|
-
{
|
|
2107
|
-
tag: "player-session",
|
|
2108
|
-
name: "Player Session",
|
|
2109
|
-
expiry: 24,
|
|
2110
|
-
period: "hours",
|
|
2111
|
-
selector: "$Session{playerId}", // $Session{} wrapper required
|
|
2112
|
-
schema: {
|
|
2113
|
-
playerId: "player_abc123", // sample value — primitive required at selector path
|
|
2114
|
-
username: "ShadowBlade",
|
|
2115
|
-
role: "player",
|
|
2116
|
-
accountId: "acct_xyz",
|
|
2117
|
-
},
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
Runtime — create a session (sign a JWT):
|
|
2121
|
-
→ CALL ductape_generate_payload FIRST (operation_family="session", method="start", targets={tag})
|
|
2122
|
-
to discover the exact data field names accepted for this session tag.
|
|
2123
|
-
ductape_execute("sessions.start", [{ product, env, tag: "player-session",
|
|
2124
|
-
data: {
|
|
2125
|
-
playerId: "player_abc123", // must match selector path — used as the revocation/lookup key
|
|
2126
|
-
username: "ShadowBlade",
|
|
2127
|
-
role: "player",
|
|
2128
|
-
accountId: "acct_xyz",
|
|
2129
|
-
} }])
|
|
2130
|
-
→ returns token: "player-session:eyJ..." ← format is always "session_tag:jwt"
|
|
2131
|
-
The token embeds all schema fields in the JWT payload, signed with the product private key.
|
|
2132
|
-
|
|
2133
|
-
Verify a token:
|
|
2134
|
-
ductape_execute("sessions.verify", [{ product, env, tag: "user-session", token: "user-session:eyJ..." }])
|
|
2135
|
-
→ decodes JWT + checks revocation blacklist; throws if revoked or expired
|
|
2136
|
-
|
|
2137
|
-
Refresh (atomic token rotation):
|
|
2138
|
-
ductape_execute("sessions.refresh", [{ product, env, tag: "user-session", refreshToken: "..." }])
|
|
2139
|
-
Old refresh token is invalidated server-side atomically — no partial state possible.
|
|
2140
|
-
The refresh token is AES-encrypted JSON (NOT a JWT).
|
|
2141
|
-
|
|
2142
|
-
Revoke:
|
|
2143
|
-
ductape_execute("sessions.revoke", [{ product, env, tag, sessionId?, identifier? }])
|
|
2144
|
-
Requires at least one of sessionId or identifier.
|
|
2145
|
-
Revocation is enforced via a ProcessorAPI blacklist check on every verify() call.
|
|
2146
|
-
|
|
2147
|
-
Analytics:
|
|
2148
|
-
sessions.listActive [{ product, env, tag, identifier?, page?, limit? }]
|
|
2149
|
-
sessions.fetchUsers [{ product, session, env?, page?, limit? }]
|
|
2150
|
-
sessions.fetchUserDetails [{ product, session, identifier, env? }]
|
|
2151
|
-
sessions.fetchDashboard [{ product, session, env? }]
|
|
2152
|
-
→ { DAU, WAU, MAU, activityTimeline, peakHours, environmentBreakdown, avgSessionDuration }
|
|
2153
|
-
|
|
2154
|
-
Token format: "session_tag:jwt_token" — pass this format verbatim wherever session is expected.
|
|
2155
|
-
JWT is signed with the product private key (not a symmetric shared secret).
|
|
2156
|
-
|
|
2157
|
-
SESSION PROPAGATION
|
|
2158
|
-
Extract the original full "session_tag:jwt" token once at the HTTP/transport boundary and retain
|
|
2159
|
-
it separately from verified/decoded claims. Claims are authorization data; they are not a token.
|
|
2160
|
-
Store a request ActorContext in a NestJS request-scoped provider or AsyncLocalStorage:
|
|
2161
|
-
type ActorContext =
|
|
2162
|
-
| { kind: "user"; session: string; actorId: string }
|
|
2163
|
-
| { kind: "delegated"; delegatedIdentity: string; actorId: string; initiatedAt: string }
|
|
2164
|
-
| { kind: "system"; reason: string };
|
|
2165
|
-
Pass ActorContext explicitly through service boundaries. At each immediate Ductape runtime call,
|
|
2166
|
-
pass actor.kind === "user" ? actor.session : the SDK-approved delegated identity. Put only
|
|
2167
|
-
non-secret actor metadata (actorId, kind, initiatedAt, correlationId) in Event envelopes.
|
|
2168
|
-
Feature execution should receive the same explicit actor classification. Never log, serialize
|
|
2169
|
-
into business payloads, or attach the raw session/refresh token to traces or error messages.
|
|
2170
|
-
|
|
2171
|
-
SECURITY BOUNDARY FOR DURABLE WORK
|
|
2172
|
-
The current SDK accepts a session string on dispatch, but it does not expose an immutable
|
|
2173
|
-
attribution snapshot API, an expired-token attribution contract, or a general delegated-session
|
|
2174
|
-
issuer. Therefore do NOT persist reusable JWTs indefinitely or assume an expired initiating token
|
|
2175
|
-
will remain valid when delayed work executes. Pass the full token only for immediate work within
|
|
2176
|
-
its validity window. For delayed/recurring work, classify execution as system-context, or use an
|
|
2177
|
-
application-owned immutable actor-context envelope plus a short-lived delegated identity issued
|
|
2178
|
-
by an explicitly approved auth design. Until the SDK provides that design, actor metadata is for
|
|
2179
|
-
audit/correlation and must not be treated as authorization.
|
|
2180
|
-
|
|
2181
|
-
Distinguish contexts deliberately:
|
|
2182
|
-
user — active request; original valid full token is required when the operation accepts it
|
|
2183
|
-
delegated — delayed/on-behalf-of work; approved short-lived credential + immutable actor metadata
|
|
2184
|
-
system — scheduler/maintenance with no user authority; omit session and record a reason
|
|
2185
|
-
|
|
2186
|
-
Backend propagation attributes component activity to an actor, but it does not record frontend
|
|
2187
|
-
pageviews, navigation, UI intent, funnels, or client failures. Continue immediately with
|
|
2188
|
-
ductape_docs({ topic: "frontend-analytics" }); both layers are required for a complete picture.
|
|
2189
|
-
`.trim(),
|
|
2190
|
-
|
|
2191
|
-
caches: `
|
|
2192
|
-
DUCTAPE CACHES
|
|
2193
|
-
|
|
2194
|
-
Caches are product-level Redis (or in-memory) stores for temporary key-value data with optional TTL.
|
|
2195
|
-
|
|
2196
|
-
Registration (admin — ductape_cli):
|
|
2197
|
-
ductape_cli("resources caches create -f cache.json")
|
|
2198
|
-
File: { name, tag, description?, expiry: <milliseconds> }
|
|
2199
|
-
No type or envs — Ductape manages the store infrastructure.
|
|
2200
|
-
expiry is in MILLISECONDS: 3600000 = 1 hour, 86400000 = 1 day, 604800000 = 1 week.
|
|
2201
|
-
|
|
2202
|
-
Operations:
|
|
2203
|
-
caches.set [{ product, cache, key, value: string, expiry?: string (ISO 8601), env }]
|
|
2204
|
-
expiry is an ABSOLUTE TIMESTAMP (not a duration).
|
|
2205
|
-
To expire in 1 hour: expiry = new Date(Date.now() + 3600_000).toISOString()
|
|
2206
|
-
To expire in 24 hours: expiry = new Date(Date.now() + 86400_000).toISOString()
|
|
2207
|
-
To never expire: omit expiry entirely.
|
|
2208
|
-
Via MCP: pass an ISO 8601 string e.g. "2026-07-17T12:00:00.000Z"
|
|
2209
|
-
→ Writes to Redis synchronously, then fires remote API write in background (non-blocking).
|
|
2210
|
-
caches.get [{ key: string }]
|
|
2211
|
-
→ Checks Redis first; on miss falls through to remote API; on hit from API, populates Redis.
|
|
2212
|
-
→ Enforces TTL by comparing the stored expiry timestamp against current time (client-side check).
|
|
2213
|
-
caches.clear [{ key: string }]
|
|
2214
|
-
→ Deletes from Redis and from remote API.
|
|
2215
|
-
caches.clearAll [{ product, cache, env? }]
|
|
2216
|
-
→ Bulk delete via remote API only. Redis may still have stale keys until naturally evicted.
|
|
2217
|
-
caches.fetchValues [{ product, cache, env?, page?, limit?,
|
|
2218
|
-
expiryFilter?: "all"|"expiring"|"permanent"|"expired" }]
|
|
2219
|
-
caches.fetchDashboard [{ product, cache, env? }]
|
|
2220
|
-
→ { totalValues, activeValues, expiredValues, totalSize }
|
|
2221
|
-
|
|
2222
|
-
Tier architecture (three tiers applied automatically):
|
|
2223
|
-
Tier 1: in-process Map (SDK metadata cache, 5-min TTL — not for user data)
|
|
2224
|
-
Tier 2: Redis hash (hSet/hGetAll) with optional EXPIRE
|
|
2225
|
-
Tier 3: Remote Ductape API
|
|
2226
|
-
|
|
2227
|
-
Practical examples:
|
|
2228
|
-
// Cache a player leaderboard for 5 minutes:
|
|
2229
|
-
{ product, cache: "leaderboard-cache", key: "top-100", value: JSON.stringify(rows),
|
|
2230
|
-
expiry: new Date(Date.now() + 300_000).toISOString(), env: "prd" }
|
|
2231
|
-
|
|
2232
|
-
// Cache a session token lookup for 1 hour:
|
|
2233
|
-
{ product, cache: "session-cache", key: "player:u_123", value: token,
|
|
2234
|
-
expiry: new Date(Date.now() + 3_600_000).toISOString(), env: "prd" }
|
|
2235
|
-
|
|
2236
|
-
Important:
|
|
2237
|
-
- Redis is optional; without it all reads/writes go through the remote API.
|
|
2238
|
-
- expiry is stored as a Date field, not a Redis TTL — the expiry check happens client-side on read.
|
|
2239
|
-
- clearAll only clears via the remote API bulk endpoint; Redis may retain stale entries until evicted.
|
|
2240
|
-
- Cache entries are stored as Redis hashes (not plain strings).
|
|
2241
|
-
- Other services (storage, graph, notifications, sessions) also use CacheManager internally —
|
|
2242
|
-
configure a shared Redis URL at SDK init to share the pool.
|
|
2243
|
-
`.trim(),
|
|
2244
|
-
|
|
2245
|
-
notifications: `
|
|
2246
|
-
DUCTAPE NOTIFICATIONS
|
|
2247
|
-
|
|
2248
|
-
Notifications send messages across multiple channels: email, SMS, push, or HTTP callback.
|
|
2249
|
-
|
|
2250
|
-
ADMINISTRATION — CLI (never ductape_execute)
|
|
2251
|
-
ductape_cli("resources notifications create -f notification.json")
|
|
2252
|
-
ductape_cli("resources notifications get --tag <notification-tag> --json")
|
|
2253
|
-
ductape_cli("resources notifications list --json")
|
|
2254
|
-
ductape_cli("resources notifications update --tag <notification-tag> -f patch.json")
|
|
2255
|
-
ductape_cli("resources notifications delete --tag <notification-tag>")
|
|
2256
|
-
ductape_cli("notifications messages create -f message.json")
|
|
2257
|
-
ductape_cli("notifications messages list --notification <notification-tag> --json")
|
|
2258
|
-
ductape_cli("notifications messages get --tag <notification:message> --json")
|
|
2259
|
-
ductape_cli("notifications messages update --tag <notification:message> -f patch.json")
|
|
2260
|
-
Declarative alternative: ductape/notifications.json then ductape_cli("apply notifications").
|
|
2261
|
-
The file MUST be a top-level JSON array. Each item is a notification definition and may contain
|
|
2262
|
-
a nested "messages" array. {"notifications":[],"messages":[]} is not a valid envelope.
|
|
2263
|
-
|
|
2264
|
-
Notification definition:
|
|
2265
|
-
{
|
|
2266
|
-
tag: "welcome-email", // component tags cannot contain ":"
|
|
2267
|
-
name: "Welcome Email",
|
|
2268
|
-
description: "Transactional welcome messages",
|
|
2269
|
-
envs: [{
|
|
2270
|
-
slug: "prd",
|
|
2271
|
-
emails: {
|
|
2272
|
-
provider: "smtp",
|
|
2273
|
-
smtp: {
|
|
2274
|
-
host: "smtp.example.com",
|
|
2275
|
-
port: "$Secret{smtp-port}",
|
|
2276
|
-
sender_email: "hello@example.com",
|
|
2277
|
-
auth: { user: "$Secret{smtp-user}", pass: "$Secret{smtp-password}" },
|
|
2278
|
-
secure: true
|
|
2279
|
-
}
|
|
2280
|
-
}
|
|
2281
|
-
}],
|
|
2282
|
-
messages: []
|
|
2283
|
-
}
|
|
2284
|
-
|
|
2285
|
-
Create a message template through declarative apply or Workbench:
|
|
2286
|
-
{
|
|
2287
|
-
tag: "welcome-email:default", // message tags use component-tag:message-tag
|
|
2288
|
-
name: "Default welcome",
|
|
2289
|
-
description: "Sent after account creation",
|
|
2290
|
-
push_notification: {
|
|
2291
|
-
title: "Welcome, {{name}}!",
|
|
2292
|
-
body: "Thanks for signing up.",
|
|
2293
|
-
data: {}
|
|
2294
|
-
},
|
|
2295
|
-
email: {
|
|
2296
|
-
subject: "Welcome, {{name}}!",
|
|
2297
|
-
template: "Hi {{name}}, thanks for signing up."
|
|
2298
|
-
}
|
|
2299
|
-
}
|
|
2300
|
-
|
|
2301
|
-
TAG AND SMTP RULES
|
|
2302
|
-
Component tag: "game-alerts-critical" (no colon).
|
|
2303
|
-
Message tag: "game-alerts-critical:match-launch" (one colon separator).
|
|
2304
|
-
Do not add "notification", "subject", or "body" at message root.
|
|
2305
|
-
SMTP requires emails.smtp.sender_email.
|
|
2306
|
-
emails.smtp.secure is a boolean and cannot be a $Secret{...} string.
|
|
2307
|
-
Credential strings such as auth.user and auth.pass may use $Secret{...}.
|
|
2308
|
-
|
|
2309
|
-
Send at runtime (one channel at a time):
|
|
2310
|
-
→ CALL ductape_generate_payload FIRST (operation_family="notification", method="email.send")
|
|
2311
|
-
notifications.email.send [{ product, env, notification, input: { recipients, subject?, template? } }]
|
|
2312
|
-
notifications.push.send [{ product, env, notification, input: { device_tokens, title?, body?, data? } }]
|
|
2313
|
-
notifications.sms.send [{ product, env, notification, input: { recipients, body? } }]
|
|
2314
|
-
notifications.callback.send [{ product, env, notification, input: { query?, headers?, params?, body? } }]
|
|
2315
|
-
notifications.slack.send [{ product, env, notification, input: { text?, blocks?, channel? } }]
|
|
2316
|
-
notifications.discord.send [{ product, env, notification, input: { content?, embeds? } }]
|
|
2317
|
-
|
|
2318
|
-
Multi-channel send (all channels in parallel):
|
|
2319
|
-
notifications.send [{ product, env, event: "notif_tag:message_tag", input: { ... } }]
|
|
2320
|
-
→ success is true if at least one channel succeeds; channel failures do not abort siblings.
|
|
2321
|
-
|
|
2322
|
-
Background dispatch with scheduling:
|
|
2323
|
-
notifications.dispatch [{ product, env, notification, event, input,
|
|
2324
|
-
schedule?: { start_at?, cron?, every?, limit?, endDate?, tz? } }]
|
|
2325
|
-
→ Returns { job_id, status: "queued", scheduled_at, recurring, next_run_at }
|
|
2326
|
-
|
|
2327
|
-
Query delivery logs:
|
|
2328
|
-
notifications.getMessages [{ product_tag?, env?, notification_tag?, status?,
|
|
2329
|
-
type?, start_date?, end_date?, page?, limit? }]
|
|
2330
|
-
Status values: pending | sent | failed | reprocessing
|
|
2331
|
-
|
|
2332
|
-
Supported providers:
|
|
2333
|
-
Email: smtp | mailgun | sendgrid | postmark | brevo
|
|
2334
|
-
SMS: twilio | nexmo | plivo | other
|
|
2335
|
-
Push: firebase | expo
|
|
2336
|
-
Callback: any HTTP endpoint
|
|
2337
|
-
|
|
2338
|
-
Each notification env may configure push_notifications, emails, sms, callbacks, slack, and discord.
|
|
2339
|
-
Email providers: smtp, sendgrid, mailgun, postmark, brevo.
|
|
2340
|
-
SMS providers: twilio, nexmo/vonage, plivo.
|
|
2341
|
-
|
|
2342
|
-
FIREBASE THROUGH A GCP CLOUD CONNECTION
|
|
2343
|
-
Complete and validate a GCP cloud connection with a service account authorized for Firebase
|
|
2344
|
-
Cloud Messaging. Then configure the notification env without copying service-account JSON:
|
|
2345
|
-
{
|
|
2346
|
-
"slug": "snd",
|
|
2347
|
-
"push_notifications": {
|
|
2348
|
-
"type": "firebase",
|
|
2349
|
-
"cloud": "gcp-connection-tag",
|
|
2350
|
-
"authMode": "cloud_connection",
|
|
2351
|
-
"databaseUrl": "https://<project>.firebaseio.com"
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
The SDK requests current GCP credentials from the cloud connection at send time. Never put the
|
|
2355
|
-
service-account private key in the notification file. Expo does not use a GCP cloud connection.
|
|
2356
|
-
The recommended cloud connection scope is "notifications". It expresses Ductape capability/UI
|
|
2357
|
-
intent; creating a notification does not prove Google-side FCM permission. Runtime delivery
|
|
2358
|
-
requires fcm.googleapis.com and roles/firebasecloudmessaging.admin, so validate the connection
|
|
2359
|
-
and perform a delivery test.
|
|
2360
|
-
Notification tag and message tag are ALWAYS passed together as "notification_tag:message_tag".
|
|
2361
|
-
`.trim(),
|
|
2362
|
-
|
|
2363
|
-
resilience: `
|
|
2364
|
-
DUCTAPE RESILIENCE
|
|
2365
|
-
|
|
2366
|
-
Resilience covers three mechanisms: quotas (rate-limited provider pools), fallbacks (automatic
|
|
2367
|
-
provider switching), and healthchecks (continuous probe monitoring with failure actions).
|
|
2368
|
-
|
|
2369
|
-
CONFIGURATION BOUNDARY
|
|
2370
|
-
Quotas, fallbacks, and health checks are administrative product configuration. Configure them in
|
|
2371
|
-
Workbench (or a future access-key administrative tool explicitly documented for the asset).
|
|
2372
|
-
Never route administrative create/update methods through ductape_execute: its publishable-key
|
|
2373
|
-
runtime proxy will fail.
|
|
2374
|
-
|
|
2375
|
-
QUOTAS — rate-limited multi-provider pools:
|
|
2376
|
-
Workbench definition shape:
|
|
2377
|
-
{
|
|
2378
|
-
tag: "sms-quota",
|
|
2379
|
-
name: "SMS Provider Pool",
|
|
2380
|
-
input: { to: { type: "string", required: true }, message: { type: "string" } },
|
|
2381
|
-
options: [
|
|
2382
|
-
{ provider: "twilio", app: "twilio-app", type: "action", event: "send-sms",
|
|
2383
|
-
quota: 1000, uses: 0, retries: 2,
|
|
2384
|
-
input: { "body:to": "$Input{to}", "body:message": "$Input{message}" },
|
|
2385
|
-
output: {} },
|
|
2386
|
-
{ provider: "nexmo", app: "nexmo-app", type: "action", event: "send-sms",
|
|
2387
|
-
quota: 500, uses: 0, retries: 1,
|
|
2388
|
-
input: { "body:to": "$Input{to}", "body:body": "$Input{message}" },
|
|
2389
|
-
output: {} },
|
|
2390
|
-
],
|
|
2391
|
-
}
|
|
2392
|
-
Providers are tried in order until quota is not exhausted.
|
|
2393
|
-
quotas.run [{ product, env, tag, input }] ← CALL ductape_generate_payload FIRST
|
|
2394
|
-
quotas.dispatch [{ product, env, tag, input, schedule? }]
|
|
2395
|
-
|
|
2396
|
-
FALLBACKS — automatic provider switching on failure:
|
|
2397
|
-
Same schema as quotas but options are ordered: primary first, then fallback(s).
|
|
2398
|
-
Primary is used first; on failure, the next provider is tried automatically.
|
|
2399
|
-
Configure in Workbench: { tag, name, input: { ... }, options: [...] }
|
|
2400
|
-
fallback.run [{ product, env, tag, input }] ← CALL ductape_generate_payload FIRST
|
|
2401
|
-
fallback.dispatch [{ product, env, tag, input, schedule? }]
|
|
2402
|
-
|
|
2403
|
-
HEALTHCHECKS — continuous probe with failure notifications:
|
|
2404
|
-
Workbench definition shape:
|
|
2405
|
-
{
|
|
2406
|
-
tag: "payment-health",
|
|
2407
|
-
name: "Payment Service Health",
|
|
2408
|
-
probe: { type: "app", app: "stripe-app", event: "ping" },
|
|
2409
|
-
interval: 30000, // ms between checks
|
|
2410
|
-
retries: 3,
|
|
2411
|
-
envs: [{ slug: "prd", input: {} }],
|
|
2412
|
-
onFailure: {
|
|
2413
|
-
notifications: [{ notification: "ops-alerts", message: "payment-down",
|
|
2414
|
-
channels: { email: { recipients: ["ops@example.com"] } } }],
|
|
2415
|
-
webhooks: [{ url: "https://hooks.example.com/alert", method: "POST" }],
|
|
2416
|
-
},
|
|
2417
|
-
}
|
|
2418
|
-
health.run [{ product, env, tag }] → triggers an immediate probe
|
|
2419
|
-
health.check [{ product, env, tag }] → same as run
|
|
2420
|
-
health.status [{ product, env, tag }] → current health status
|
|
2421
|
-
|
|
2422
|
-
Probe types: app | database | feature | graph | message_broker | storage
|
|
2423
|
-
Failure actions: notification channels, HTTP webhooks, and/or message broker emit — all can
|
|
2424
|
-
be configured simultaneously on the same healthcheck.
|
|
2425
|
-
Input template references: $Input{field} → maps declared input to the probe's action input.
|
|
2426
|
-
Provider status: available | unavailable
|
|
2427
|
-
|
|
2428
|
-
DECISION MATRIX
|
|
2429
|
-
Rate/capacity allocation across providers → quota
|
|
2430
|
-
Equivalent provider after operational failure → fallback
|
|
2431
|
-
Detect failure before routing provider traffic → health check
|
|
2432
|
-
Transient failure of one operation → bounded retry + idempotency policy
|
|
2433
|
-
Multi-step business recovery/compensation → Feature
|
|
2434
|
-
Database atomicity → database transaction, not Feature rollback
|
|
2435
|
-
Scheduled single operation → that component's dispatch
|
|
2436
|
-
Scheduled multi-step process → Feature dispatch
|
|
2437
|
-
|
|
2438
|
-
COMBINING MECHANISMS
|
|
2439
|
-
A health check may keep an unhealthy provider out of a fallback/quota pool; the pool controls
|
|
2440
|
-
provider selection; each operation may use bounded retries and an idempotency key; a Feature
|
|
2441
|
-
coordinates business steps and compensation around those resilient operations. Do not stack them
|
|
2442
|
-
reflexively: each layer needs a distinct failure it owns, bounded retry budgets, and observable
|
|
2443
|
-
terminal behavior. Database transactions remain the atomicity boundary for related DB writes.
|
|
2444
|
-
`.trim(),
|
|
2445
|
-
|
|
2446
|
-
'frontend-analytics': `
|
|
2447
|
-
DUCTAPE FRONTEND PRODUCT ANALYTICS AND SESSION ACTIVITY
|
|
2448
|
-
|
|
2449
|
-
Frontend analytics and backend session attribution are complementary; neither replaces the other.
|
|
2450
|
-
|
|
2451
|
-
BACKEND OPERATION ATTRIBUTION
|
|
2452
|
-
Immediate user-initiated backend work should pass the original full Ductape session token to every
|
|
2453
|
-
database, Event, Feature, notification, storage, graph, vector, action, and other runtime operation
|
|
2454
|
-
that accepts session. This attributes immediate server work to the authenticated actor.
|
|
2455
|
-
For delayed/durable work, do not persist raw JWTs indefinitely. Use system context, or an approved
|
|
2456
|
-
delegated actor design with immutable non-secret actor metadata. Actor metadata supports audit and
|
|
2457
|
-
correlation; it is not authorization. Intentional system/background work should remain sessionless.
|
|
2458
|
-
|
|
2459
|
-
FRONTEND PRODUCT ANALYTICS
|
|
2460
|
-
Use the browser Analytics service for anonymous visits, authenticated page views, navigation,
|
|
2461
|
-
UI interactions, funnels, client errors, realtime state, and product-surface engagement.
|
|
2462
|
-
|
|
2463
|
-
IMPORTANT:
|
|
2464
|
-
Passing session to backend component calls does not replace frontend analytics.
|
|
2465
|
-
Calling analytics.identify(sessionToken) and tracking frontend events does not replace backend
|
|
2466
|
-
session propagation. A complete activity picture requires both.
|
|
2467
|
-
|
|
2468
|
-
VERSION AND DOCUMENTATION PRECEDENCE
|
|
2469
|
-
1. Installed package types and exports determine what can be called now.
|
|
2470
|
-
2. Version-matched package documentation explains intended usage.
|
|
2471
|
-
3. Current docs/docs/Frontend describes the latest supported design.
|
|
2472
|
-
4. If they disagree, report the mismatch; never fabricate compatibility.
|
|
2473
|
-
|
|
2474
|
-
Inspect package.json plus package exports/type declarations before recommending framework hooks.
|
|
2475
|
-
The current repository exports useAnalytics from @ductape/react and @ductape/vue. Their installed
|
|
2476
|
-
hook/composable exposes track, pageview, identify, visitorId, enableAutoCapture, and flush.
|
|
2477
|
-
clearSession and disableAutoCapture are available on client.analytics but are not currently
|
|
2478
|
-
returned by those framework wrappers. Use useDuctape().client.analytics for those calls, or
|
|
2479
|
-
recommend a package version that exports them; do not generate a hook method that does not exist.
|
|
2480
|
-
|
|
2481
|
-
CLIENT API — IDENTIFY AFTER AUTHENTICATION
|
|
2482
|
-
ductape.analytics.identify(sessionToken);
|
|
2483
|
-
|
|
2484
|
-
sessionToken is the complete value returned by Ductape, in "player-session:jwt" format.
|
|
2485
|
-
It is NOT a player ID, session tag, session ID, decoded claims, or refresh token.
|
|
2486
|
-
identify links subsequent frontend analytics to the authenticated Ductape session and lets
|
|
2487
|
-
supported analytics correlate anonymous pre-login activity with authenticated activity.
|
|
2488
|
-
|
|
2489
|
-
LOGOUT / ACCOUNT SWITCHING
|
|
2490
|
-
await ductape.analytics.flush();
|
|
2491
|
-
ductape.analytics.clearSession();
|
|
2492
|
-
|
|
2493
|
-
Clear analytics identity when logout or revocation succeeds, refresh fails irrecoverably, local
|
|
2494
|
-
authentication is removed, or a different user is about to authenticate in the same browser.
|
|
2495
|
-
Removing only the application token can leave later anonymous/next-user events associated with
|
|
2496
|
-
the previous analytics identity. Flush is best-effort; browser shutdown does not guarantee it.
|
|
2497
|
-
|
|
2498
|
-
CUSTOM EVENTS
|
|
2499
|
-
await ductape.analytics.track({
|
|
2500
|
-
event: 'order_submitted',
|
|
2501
|
-
traceId,
|
|
2502
|
-
properties: { matchId, orderType },
|
|
2503
|
-
});
|
|
2504
|
-
|
|
2505
|
-
IAnalyticsTrackOptions:
|
|
2506
|
-
event: string
|
|
2507
|
-
properties?: Record<string, unknown>
|
|
2508
|
-
session?: string
|
|
2509
|
-
product?: string
|
|
2510
|
-
env?: string
|
|
2511
|
-
traceId?: string
|
|
2512
|
-
context?: { url?, path?, referrer?, locale?, screen?: { width, height } }
|
|
2513
|
-
|
|
2514
|
-
identify establishes the default analytics session. An individual event may explicitly provide
|
|
2515
|
-
session. Normally use product/env from client configuration. traceId correlates frontend intent
|
|
2516
|
-
with backend logs, Events, Features, and order processing.
|
|
2517
|
-
|
|
2518
|
-
PAGE VIEWS
|
|
2519
|
-
await ductape.analytics.pageview({
|
|
2520
|
-
path: location.pathname,
|
|
2521
|
-
title: document.title,
|
|
2522
|
-
properties: { matchId, screen: 'governance' },
|
|
2523
|
-
});
|
|
2524
|
-
|
|
2525
|
-
IAnalyticsPageviewOptions:
|
|
2526
|
-
path?: string
|
|
2527
|
-
title?: string
|
|
2528
|
-
session?: string
|
|
2529
|
-
product?: string
|
|
2530
|
-
env?: string
|
|
2531
|
-
properties?: Record<string, unknown>
|
|
2532
|
-
|
|
2533
|
-
AUTO-CAPTURE — OPT IN ONLY AFTER A PRIVACY AUDIT
|
|
2534
|
-
const stopAutoCapture = ductape.analytics.enableAutoCapture({
|
|
2535
|
-
session: () => authSession?.token,
|
|
2536
|
-
clicks: false,
|
|
2537
|
-
pageviews: true,
|
|
2538
|
-
maskTextSelectors: [
|
|
2539
|
-
'[data-private]',
|
|
2540
|
-
'[data-secret]',
|
|
2541
|
-
'[data-player-message]',
|
|
2542
|
-
'[data-intelligence-report]',
|
|
2543
|
-
],
|
|
2544
|
-
});
|
|
2545
|
-
stopAutoCapture(); // or ductape.analytics.disableAutoCapture()
|
|
2546
|
-
|
|
2547
|
-
Mark sensitive UI with data-private/data-secret attributes. Text masking may not mask attributes,
|
|
2548
|
-
IDs, URLs, element names, custom properties, console errors, or network errors. Inspect actual
|
|
2549
|
-
payloads before production. For hidden-information products, begin with reviewed automatic
|
|
2550
|
-
pageviews, clicks disabled, and preferred custom named events.
|
|
2551
|
-
|
|
2552
|
-
VISITOR ID
|
|
2553
|
-
const visitorId = ductape.analytics.getVisitorId();
|
|
2554
|
-
A visitor ID is anonymous analytics identity, not authentication or authorization.
|
|
2555
|
-
|
|
2556
|
-
FRONTEND SESSION LIFECYCLE
|
|
2557
|
-
Before login:
|
|
2558
|
-
- Track anonymous pageviews/onboarding; do not invent a session.
|
|
2559
|
-
After login/registration:
|
|
2560
|
-
- Store the session securely, identify(fullSessionToken), track success, start authenticated
|
|
2561
|
-
pageviews, and pass the same token to user-context realtime/component operations.
|
|
2562
|
-
After refresh:
|
|
2563
|
-
- Replace the old token, identify(newSessionToken), update realtime connections/subscriptions,
|
|
2564
|
-
and use the refreshed token for future backend operations.
|
|
2565
|
-
Logout:
|
|
2566
|
-
- Optionally track logout_initiated, flush, revoke, disconnect realtime, clearSession, then
|
|
2567
|
-
remove local authentication.
|
|
2568
|
-
Refresh failure/revocation:
|
|
2569
|
-
- Disconnect user-context clients, clearSession, clear local auth, navigate to authentication,
|
|
2570
|
-
and track only anonymous events afterward.
|
|
2571
|
-
|
|
2572
|
-
REACT ROUTE TRACKING (verify installed exports first)
|
|
2573
|
-
import { useAnalytics, useDuctape } from '@ductape/react';
|
|
2574
|
-
import { useEffect } from 'react';
|
|
2575
|
-
import { useLocation } from 'react-router-dom';
|
|
2576
|
-
|
|
2577
|
-
function ProductAnalytics({ sessionToken }: { sessionToken?: string }) {
|
|
2578
|
-
const analytics = useAnalytics();
|
|
2579
|
-
const { client } = useDuctape();
|
|
2580
|
-
const location = useLocation();
|
|
2581
|
-
useEffect(() => {
|
|
2582
|
-
if (sessionToken) analytics.identify(sessionToken);
|
|
2583
|
-
else client.analytics.clearSession();
|
|
2584
|
-
}, [analytics, client, sessionToken]);
|
|
2585
|
-
useEffect(() => {
|
|
2586
|
-
void analytics.pageview({ path: location.pathname, title: document.title });
|
|
2587
|
-
}, [analytics, location.pathname]);
|
|
2588
|
-
return null;
|
|
2589
|
-
}
|
|
2590
|
-
|
|
2591
|
-
SAFE EVENT TAXONOMY
|
|
2592
|
-
Define stable names centrally; do not invent variants throughout components.
|
|
2593
|
-
Authentication:
|
|
2594
|
-
registration_started, registration_completed, registration_failed,
|
|
2595
|
-
login_started, login_completed, login_failed, session_refreshed,
|
|
2596
|
-
session_refresh_failed, logout_completed
|
|
2597
|
-
Match:
|
|
2598
|
-
match_list_viewed, match_creation_started, match_created, match_joined, lobby_viewed,
|
|
2599
|
-
player_marked_ready, match_preparation_started, world_loaded, match_reconnected,
|
|
2600
|
-
match_completed, endgame_viewed
|
|
2601
|
-
Orders:
|
|
2602
|
-
order_form_opened, order_previewed, order_submission_started, order_submitted,
|
|
2603
|
-
order_submission_failed, order_cancelled, boundary_result_viewed
|
|
2604
|
-
Realtime:
|
|
2605
|
-
realtime_connect_started, realtime_connected, realtime_disconnected,
|
|
2606
|
-
realtime_reconnect_attempted, realtime_subscription_failed, projection_refresh_failed,
|
|
2607
|
-
client_error
|
|
2608
|
-
Funnels:
|
|
2609
|
-
tutorial_started, tutorial_step_completed, tutorial_abandoned, first_match_created,
|
|
2610
|
-
first_order_submitted, first_boundary_viewed, first_match_completed
|
|
2611
|
-
|
|
2612
|
-
Safe properties include matchId, orderType, screen, tick, result, and errorCategory.
|
|
2613
|
-
|
|
2614
|
-
HIDDEN AND SENSITIVE DATA — NEVER SEND TO ANALYTICS
|
|
2615
|
-
Do not track exact hidden formations, operative/handler identities, secret operation payloads,
|
|
2616
|
-
false-report truth markers, undiscovered evasion details, invisible treaties, canonical hidden
|
|
2617
|
-
map state, private messages, passwords, authorization headers, session/refresh tokens in event
|
|
2618
|
-
properties, or full errors/records that may contain secrets. Analytics must observe usage, not
|
|
2619
|
-
become a hidden-state side channel.
|
|
2620
|
-
|
|
2621
|
-
CORRELATION
|
|
2622
|
-
Frontend: create traceId = crypto.randomUUID(), track intent with traceId, and send traceId with
|
|
2623
|
-
the actual request. Backend logs the same traceId and propagates session to Ductape operations.
|
|
2624
|
-
Track completion with the same traceId.
|
|
2625
|
-
|
|
2626
|
-
traceId = correlation
|
|
2627
|
-
session = actor attribution
|
|
2628
|
-
idempotencyKey = duplicate prevention
|
|
2629
|
-
matchId/orderId = domain identity
|
|
2630
|
-
These values are not interchangeable.
|
|
2631
|
-
|
|
2632
|
-
OWNERSHIP BOUNDARIES
|
|
2633
|
-
Analytics is never an authoritative order, authorization proof, or gameplay source of truth.
|
|
2634
|
-
Analytics failure must not block or alter gameplay. The server verifies sessions independently;
|
|
2635
|
-
authoritative database and Event streams remain the source of truth. Prefer non-blocking
|
|
2636
|
-
analytics except explicit best-effort flushes at safe lifecycle transitions.
|
|
2637
|
-
|
|
2638
|
-
FRONTEND PROJECT AUDIT
|
|
2639
|
-
Inspect installed @ductape/client/react/vue versions and actual exports; client/provider setup;
|
|
2640
|
-
identify after login and refresh; clearSession on logout/account switch; SPA pageviews; auto-
|
|
2641
|
-
capture and masking; taxonomy consistency; sensitive custom properties; and shared trace IDs.
|
|
2642
|
-
Report capability as Present/Missing/Partial/Not applicable with concrete findings.
|
|
2643
|
-
|
|
2644
|
-
WHEN “SESSION ACTIVITY IS MISSING FROM THE DASHBOARD”
|
|
2645
|
-
Investigate both tracks before blaming the SDK.
|
|
2646
|
-
Backend: start/verify format, correct env, session on database/Event/Feature/notification/etc.,
|
|
2647
|
-
and intentional background sessionlessness.
|
|
2648
|
-
Frontend: identify after login/refresh, clearSession on logout, pageviews/custom events, flush,
|
|
2649
|
-
correct publishable key/product/env, installed API compatibility, browser failures, and privacy
|
|
2650
|
-
controls that may suppress events.
|
|
2651
|
-
|
|
2652
|
-
PAYLOAD RECIPES
|
|
2653
|
-
Anonymous:
|
|
2654
|
-
await ductape.analytics.pageview({ path: window.location.pathname, title: document.title });
|
|
2655
|
-
Authenticated:
|
|
2656
|
-
ductape.analytics.identify(playerSessionToken);
|
|
2657
|
-
await ductape.analytics.track({
|
|
2658
|
-
event: 'match_created', session: playerSessionToken, traceId, properties: { matchId },
|
|
2659
|
-
});
|
|
2660
|
-
Logout:
|
|
2661
|
-
await ductape.analytics.flush();
|
|
2662
|
-
ductape.analytics.clearSession();
|
|
2663
|
-
`.trim(),
|
|
2664
|
-
|
|
2665
|
-
features: `
|
|
2666
|
-
DUCTAPE FEATURES
|
|
2667
|
-
|
|
2668
|
-
A feature is an orchestrated workflow of durable steps. Steps can call app actions, database
|
|
2669
|
-
operations, graph queries, storage uploads, notifications, broker publishes, child features,
|
|
2670
|
-
quotas, fallbacks, and more. Features support rollback, signals, checkpoints, and sleep.
|
|
2671
|
-
|
|
2672
|
-
━━━ AI DESIGN WORKFLOW — follow this process every time a user asks you to build or plan a feature ━━━
|
|
2673
|
-
|
|
2674
|
-
ALWAYS use features.define (code-first). NEVER use features.create.
|
|
2675
|
-
The feature handler is real code written into the project's source files — find the project's
|
|
2676
|
-
language and framework, write the feature into an appropriate file in the codebase, and call
|
|
2677
|
-
features.define from there. Do not generate inline snippets and stop — write the actual file.
|
|
2678
|
-
|
|
2679
|
-
STEP 1 — UNDERSTAND the goal
|
|
2680
|
-
Ask clarifying questions if the user's intent is unclear. Do not start designing until you
|
|
2681
|
-
understand: what the feature does, what it returns, what can fail and how failures should behave.
|
|
2682
|
-
|
|
2683
|
-
STEP 2 — INVENTORY existing Ductape components
|
|
2684
|
-
Call ductape_cli("products get --tag <product_tag> --json") to read the full product document.
|
|
2685
|
-
(product.* requires the access key — never use ductape_execute for product reads, it will return 403)
|
|
2686
|
-
Note what already exists:
|
|
2687
|
-
- databases[] → available for ctx.database.insert/query/update/delete steps
|
|
2688
|
-
- apps[] → available for ctx.api.run steps ONLY if the App is fully registered:
|
|
2689
|
-
(a) App record exists, (b) environments defined with base URLs,
|
|
2690
|
-
(c) auth scheme configured, (d) action endpoints defined,
|
|
2691
|
-
(e) connected to the product via product.apps.add.
|
|
2692
|
-
Check app.events[] for action tags. If no app exists for a service the
|
|
2693
|
-
feature needs to call, flag it as "App to create" in the plan — do NOT
|
|
2694
|
-
assume ctx.api can call any URL or schedule any job without a registered App.
|
|
2695
|
-
- notifications[] → available for ctx.notification.email/sms/push steps
|
|
2696
|
-
- storage[] → available for ctx.storage.upload/download steps
|
|
2697
|
-
- messageBrokers[] → available for ctx.events.produce steps
|
|
2698
|
-
- graphs[] → available for ctx.graph steps
|
|
2699
|
-
- features[] → can be called as child features via ctx.feature.execute()
|
|
2700
|
-
- caches[], sessions[]
|
|
2701
|
-
Do NOT assume a component or event tag exists — verify from the product before using it.
|
|
2702
|
-
Do NOT treat ctx.api as a generic HTTP call or job scheduler. It requires a registered App.
|
|
2703
|
-
|
|
2704
|
-
STEP 3 — PLAN each step
|
|
2705
|
-
For every logical step:
|
|
2706
|
-
a. Identify which existing component handles it, or flag it as needing creation.
|
|
2707
|
-
If a step calls an external service, it MUST go through a registered Ductape App.
|
|
2708
|
-
If no App for that service exists in the product → mark it "App to create: <service name>".
|
|
2709
|
-
DO NOT plan a raw HTTP call, a direct dispatch to a URL, or any workaround in place of a missing App.
|
|
2710
|
-
b. Note how inputs flow: ctx.input fields, or return values from earlier steps (plain JS variables — no special notation needed)
|
|
2711
|
-
c. Decide if a rollback handler is needed (e.g. charge → refund on later failure)
|
|
2712
|
-
d. Decide allow_fail: true for non-critical steps (email, analytics, audit logs)
|
|
2713
|
-
|
|
2714
|
-
STEP 4 — PRESENT the plan and get approval BEFORE writing any code or creating anything
|
|
2715
|
-
Show the user:
|
|
2716
|
-
Feature: <name> (<tag>)
|
|
2717
|
-
Input fields: list them with types
|
|
2718
|
-
Steps (in order):
|
|
2719
|
-
1. <step-tag> — <what it does> — <component> [rollback: yes/no] [allow_fail: yes/no]
|
|
2720
|
-
2. ...
|
|
2721
|
-
Return value: describe what the handler returns
|
|
2722
|
-
Components to create: list anything missing with a short description
|
|
2723
|
-
Ask: "Should I proceed? Shall I create the missing components first?"
|
|
2724
|
-
Wait for confirmation before writing code or calling any create tool.
|
|
2725
|
-
|
|
2726
|
-
STEP 5 — CREATE missing components (only with user approval)
|
|
2727
|
-
Administrative assets must never be created through ductape_execute. Use ductape_cli for products,
|
|
2728
|
-
apps, supported resources, broker topics, cloud connections, secrets, and declarative apply flows.
|
|
2729
|
-
App actions, auths, quotas, fallbacks, health checks, and other administrative assets for which
|
|
2730
|
-
the CLI has no command must be configured in Workbench. Feature definitions are the exception:
|
|
2731
|
-
they are code-first via features.define, are registered by application boot/runtime initialization,
|
|
2732
|
-
and cannot be created with the CLI. Do not generate a Workbench-only or publishable-key
|
|
2733
|
-
create/update call for a Feature. For a missing database action, configure it in Workbench, then verify it exists.
|
|
2734
|
-
For a missing child feature, recursively apply this same workflow.
|
|
2735
|
-
Tell the user what you are about to create before each tool call.
|
|
2736
|
-
|
|
2737
|
-
STEP 6 — WRITE the feature into the project codebase
|
|
2738
|
-
- Locate or create an appropriate file in the project (e.g. src/features/feature-name.ts, features/feature_name.py)
|
|
2739
|
-
- Use features.define with an async handler
|
|
2740
|
-
- Step results are plain variables — just await ctx.step(...) and use the return value in the next step
|
|
2741
|
-
- No special notation needed: const user = await ctx.step('create-user', async () => { ... }); then use user.id directly in the next step
|
|
2742
|
-
- Write rollback handlers inline as the third argument to ctx.step()
|
|
2743
|
-
- Return a plain object as the feature's output
|
|
2744
|
-
|
|
2745
|
-
STEP 7 — HANDLE conditionals, loops, and branching (when applicable)
|
|
2746
|
-
Branch on step result (early return in handler):
|
|
2747
|
-
→ Add branchOverrides: { stepTag: { field: value } } so all branches are recorded
|
|
2748
|
-
→ At runtime the executor evaluates the real result and skips or runs later steps accordingly
|
|
2749
|
-
Loop over input array:
|
|
2750
|
-
→ Add recordInput: { items: [{ id: "1" }, { id: "2" }] } so the loop runs during recording
|
|
2751
|
-
→ Use unique step tags per iteration (e.g. "process-" + item.id)
|
|
2752
|
-
Switch/if-else on feature input values:
|
|
2753
|
-
→ Add recordScenarios: [{ type: "a" }, { type: "b" }] — handler runs once per scenario
|
|
2754
|
-
→ Only the scenario whose input matches runs at execution time
|
|
2755
|
-
|
|
2756
|
-
STEP 8 — SET rollbacks for reversible steps
|
|
2757
|
-
Any step that allocates a resource should undo it if a later step fails.
|
|
2758
|
-
ctx.api.run requires a pre-registered Ductape App — 'stripe' below is the tag of a registered App:
|
|
2759
|
-
const charge = await ctx.step(
|
|
2760
|
-
'charge',
|
|
2761
|
-
async () => ctx.api.run({ app: 'stripe', event: 'create-charge', input: { amount: ctx.input.amount } }),
|
|
2762
|
-
async (result) => ctx.api.run({ app: 'stripe', event: 'refund', input: { chargeId: result.id } })
|
|
2763
|
-
);
|
|
2764
|
-
|
|
2765
|
-
Step types: action | database | graph | notification | storage | produce | quota | fallback |
|
|
2766
|
-
vector | child_feature | sleep | wait_for_signal | checkpoint
|
|
2767
|
-
|
|
2768
|
-
Define a feature (write this into the project's source files — do NOT use features.create):
|
|
2769
|
-
// src/features/onboard-user.ts (or the equivalent path/language for the project)
|
|
2770
|
-
await ductape.features.define({
|
|
2771
|
-
tag: "onboard-user",
|
|
2772
|
-
name: "Onboard User",
|
|
2773
|
-
handler: async (ctx) => {
|
|
2774
|
-
const account = await ctx.step("create-account", async () =>
|
|
2775
|
-
ctx.database.insert({ database: "core-db", event: "insert-user",
|
|
2776
|
-
data: { userId: ctx.input.userId, email: ctx.input.email } }),
|
|
2777
|
-
async (result) => ctx.database.delete({ database: "core-db", event: "delete-user",
|
|
2778
|
-
where: { id: result.id } }) // rollback
|
|
2779
|
-
);
|
|
2780
|
-
|
|
2781
|
-
await ctx.step("send-welcome", async () =>
|
|
2782
|
-
ctx.notification.email({ notification: "welcome-email", event: "welcome",
|
|
2783
|
-
recipients: [ctx.input.email], subject: {}, template: {} }),
|
|
2784
|
-
null,
|
|
2785
|
-
{ allow_fail: true }
|
|
2786
|
-
);
|
|
2787
|
-
|
|
2788
|
-
return { userId: account.id };
|
|
2789
|
-
},
|
|
2790
|
-
});
|
|
2791
|
-
|
|
2792
|
-
Execute at runtime:
|
|
2793
|
-
→ CALL ductape_generate_payload FIRST (operation_family="features", method="execute", targets={tag})
|
|
2794
|
-
features.execute [{ product, env, tag, input: { userId: "u_123", email: "..." },
|
|
2795
|
-
idempotency_key?: string, retries?: number, timeout?: number }]
|
|
2796
|
-
|
|
2797
|
-
Background dispatch with scheduling:
|
|
2798
|
-
features.dispatch [{ product, env, feature, input,
|
|
2799
|
-
schedule?: { start_at?, cron?, every?, limit?, endDate?, tz? } }]
|
|
2800
|
-
→ Returns { job_id, status: "queued"|"scheduled", scheduled_at, next_run_at }
|
|
2801
|
-
|
|
2802
|
-
Execution management:
|
|
2803
|
-
features.status [executionId] → { status, current_step, completed_steps, output, error }
|
|
2804
|
-
features.cancel [executionId, reason?] → triggers rollback of running steps
|
|
2805
|
-
features.replay [executionId, options?] → re-run with original input
|
|
2806
|
-
features.restart [executionId] → re-run with new input
|
|
2807
|
-
features.resume [executionId] → continue from a checkpoint
|
|
2808
|
-
features.replayFromStep [executionId, stepTag] → replay from a specific step
|
|
2809
|
-
features.history [executionId] → { events[], checkpoints[], replays[], restarts[] }
|
|
2810
|
-
features.stepDetail [executionId, stepTag] → per-step input/output/error/timing
|
|
2811
|
-
|
|
2812
|
-
Signals and queries (for long-running features):
|
|
2813
|
-
features.signal [{ product, env, feature_id, signal: "payment-confirmed", payload? }]
|
|
2814
|
-
features.query [{ product, env, feature_id, query: "current-status", params? }]
|
|
2815
|
-
|
|
2816
|
-
Rollback strategies: reverse_all | reverse_critical | compensate | none
|
|
2817
|
-
Feature statuses: pending | running | completed | failed | rolled_back | rolling_back | paused
|
|
2818
|
-
|
|
2819
|
-
━━━ FEATURE RECORDING SEMANTICS ━━━
|
|
2820
|
-
|
|
2821
|
-
When you call features.define({ handler }), the handler runs TWICE:
|
|
2822
|
-
|
|
2823
|
-
1. RECORDING PHASE (at define time) — handler is called with a RecordingContext.
|
|
2824
|
-
All ctx.step() calls return lightweight proxy objects, not real data.
|
|
2825
|
-
This phase captures the step graph: which steps exist, their types, tags, and declared
|
|
2826
|
-
inputs/outputs. No real API calls, DB queries, or side effects occur.
|
|
2827
|
-
Arbitrary JS code OUTSIDE ctx.step() ALSO runs during recording — with proxy values.
|
|
2828
|
-
For loops: use recordInput so the handler sees sample data and all iterations are recorded.
|
|
2829
|
-
For branches: use branchOverrides so each path is captured.
|
|
2830
|
-
|
|
2831
|
-
2. EXECUTION PHASE (at runtime) — handler is called with a real ExecutionContext.
|
|
2832
|
-
ctx.step() actually executes. All real Ductape component calls happen.
|
|
2833
|
-
Arbitrary JS logic (math, string ops, conditionals on step results) runs for real.
|
|
2834
|
-
|
|
2835
|
-
Implication: put all meaningful business logic INSIDE ctx.step() handlers, not in the
|
|
2836
|
-
outer handler body. Code in the outer body runs during recording with proxy values and
|
|
2837
|
-
may behave unexpectedly (e.g. typeof proxy === 'object' is true but .someField is a proxy).
|
|
2838
|
-
|
|
2839
|
-
Features do NOT execute arbitrary NestJS or server code directly. A feature handler can only
|
|
2840
|
-
call Ductape component primitives (ctx.api, ctx.database, ctx.notification, etc.) as steps.
|
|
2841
|
-
To invoke internal application business logic, produce a broker event from a feature step
|
|
2842
|
-
(ctx.events.produce in the currently published SDK) and consume it in your NestJS service.
|
|
2843
|
-
ctx.publish is deprecated; do not use it. Do not assume a ctx.messaging alias exists unless the
|
|
2844
|
-
installed SDK types explicitly expose it.
|
|
2845
|
-
|
|
2846
|
-
━━━ ORCHESTRATION DECISION RULE ━━━
|
|
2847
|
-
|
|
2848
|
-
One component operation at a future time:
|
|
2849
|
-
→ use that component's own dispatch method
|
|
2850
|
-
e.g. ductape.events.dispatch({ ..., schedule: { start_at: ... } })
|
|
2851
|
-
e.g. ductape.api.dispatch({ ..., schedule: { start_at: ... } })
|
|
2852
|
-
e.g. ductape.database.dispatch({ ..., schedule: { start_at: ... } })
|
|
2853
|
-
|
|
2854
|
-
Several durable Ductape component operations in sequence (with rollback / retry / state):
|
|
2855
|
-
→ define a Feature, then features.dispatch to schedule it
|
|
2856
|
-
|
|
2857
|
-
Invoke internal application business logic (your own NestJS/backend service code):
|
|
2858
|
-
→ produce a broker event (ctx.events.produce inside a Feature, or ductape.events.produce outside it)
|
|
2859
|
-
→ follow ductape_docs({ topic: "events" }) and use the canonical NestJS decorator:
|
|
2860
|
-
@Events.Consumer({ event: "broker-tag:topic-tag" })
|
|
2861
|
-
async handle(message: MessageShape) { /* injected-service business logic; throw to nack */ }
|
|
2862
|
-
→ DuctapeModule auto-registers the decorated consumer; no manual onModuleInit is needed
|
|
2863
|
-
→ events.consume() remains a supported lower-level alternative for plain TypeScript/Node.js
|
|
2864
|
-
→ your service method runs with full access to DI, DB transactions, etc.
|
|
2865
|
-
Do NOT create an App Action just to call your own service over HTTP.
|
|
2866
|
-
|
|
2867
|
-
Invoke an external/public HTTP service:
|
|
2868
|
-
→ create a Ductape App (register base URL, auth, action endpoints) then use ctx.api.run
|
|
2869
|
-
→ requires the App to be fully registered and connected to the product first
|
|
2870
|
-
`.trim(),
|
|
2871
|
-
|
|
2872
|
-
events: `
|
|
2873
|
-
DUCTAPE EVENTS (MESSAGE BROKERS)
|
|
2874
|
-
|
|
2875
|
-
ARCHITECTURE — always two separate steps:
|
|
2876
|
-
Step 1: Register the BROKER COMPONENT (establishes the connection to the broker service).
|
|
2877
|
-
The broker's envs[] holds connection credentials and host/project info, NOT topics.
|
|
2878
|
-
Step 2: Create TOPIC DEFINITIONS on that broker (unlimited; each is a named subject or queue).
|
|
2879
|
-
Topics are separate from the broker registration and added after.
|
|
2880
|
-
A single broker component can have as many topics as needed.
|
|
2881
|
-
|
|
2882
|
-
━━━ SUPPORTED BROKER TYPES ━━━
|
|
2883
|
-
|
|
2884
|
-
Cloud-managed (provision OR import via cloud connection):
|
|
2885
|
-
GCP Pub/Sub → service: "pubsub", type in envs: "google_pubsub"
|
|
2886
|
-
AWS SQS → service: "sqs", type in envs: "aws_sqs"
|
|
2887
|
-
Azure SvcBus → service: "servicebus", type in envs: "azure_servicebus"
|
|
2888
|
-
|
|
2889
|
-
Self-hosted (import-only — supply connection URL manually):
|
|
2890
|
-
Kafka → type in envs: "kafka"
|
|
2891
|
-
RabbitMQ → type in envs: "rabbitmq"
|
|
2892
|
-
Redis → type in envs: "redis"
|
|
2893
|
-
NATS → type in envs: "nats"
|
|
2894
|
-
|
|
2895
|
-
━━━ STEP 1A: REGISTER VIA CLOUD CONNECTION (cloud-managed brokers) ━━━
|
|
2896
|
-
|
|
2897
|
-
Provision (create a NEW resource in the cloud):
|
|
2898
|
-
ductape_cli("cloud resources provision-persist-all -f brokers.json --json")
|
|
2899
|
-
File: JSON ARRAY — one entry per env, same product + component tag across all entries.
|
|
2900
|
-
type field: "messageBrokers" (exact — not "messagebrokers" or "events")
|
|
2901
|
-
|
|
2902
|
-
GCP Pub/Sub — creates a new Pub/Sub topic in GCP, stores credentials in secrets:
|
|
2903
|
-
Cost: GCP Pub/Sub is usage-based — no upfront cost, no tier selection required.
|
|
2904
|
-
You pay per GB of data published/subscribed (first 10 GB/month free).
|
|
2905
|
-
It is safe to provision without user approval of a fixed monthly cost.
|
|
2906
|
-
[{"cloud":"gcp-snd","service":"pubsub","type":"messageBrokers",
|
|
2907
|
-
"product":"my-product","component":"notifications-broker","env":"snd",
|
|
2908
|
-
"topicName":"my-product-notifications-snd"},
|
|
2909
|
-
{"cloud":"gcp-prd","service":"pubsub","type":"messageBrokers",
|
|
2910
|
-
"product":"my-product","component":"notifications-broker","env":"prd",
|
|
2911
|
-
"topicName":"my-product-notifications-prd"}]
|
|
2912
|
-
If topicName is omitted a timestamped name is generated — always supply it explicitly.
|
|
2913
|
-
|
|
2914
|
-
AWS SQS — creates a new SQS queue per env:
|
|
2915
|
-
Cost: SQS is usage-based — no upfront cost, no tier selection required.
|
|
2916
|
-
First 1 million requests/month free; $0.40 per million after that.
|
|
2917
|
-
It is safe to provision without user approval of a fixed monthly cost.
|
|
2918
|
-
[{"cloud":"aws-snd","service":"sqs","type":"messageBrokers",
|
|
2919
|
-
"product":"my-product","component":"notifications-broker","env":"snd",
|
|
2920
|
-
"queueName":"my-product-notifications-snd"},
|
|
2921
|
-
{"cloud":"aws-prd","service":"sqs","type":"messageBrokers",
|
|
2922
|
-
"product":"my-product","component":"notifications-broker","env":"prd",
|
|
2923
|
-
"queueName":"my-product-notifications-prd"}]
|
|
2924
|
-
|
|
2925
|
-
Azure Service Bus — creates a namespace + queue per env:
|
|
2926
|
-
Cost: Azure Service Bus has TIERED pricing — confirm the tier with the user before provisioning.
|
|
2927
|
-
Basic: queues only, ~$0.05/million operations. No topics/subscriptions.
|
|
2928
|
-
Standard: queues + topics, ~$10/month base + $0.10/million operations.
|
|
2929
|
-
Premium: dedicated capacity, starts ~$677/month. Not needed for standard workloads.
|
|
2930
|
-
DO NOT provision Azure Service Bus without confirming the tier with the user.
|
|
2931
|
-
[{"cloud":"azure-snd","service":"servicebus","type":"messageBrokers",
|
|
2932
|
-
"product":"my-product","component":"notifications-broker","env":"snd",
|
|
2933
|
-
"namespaceName":"myproduct-snd","queueName":"notifications"},
|
|
2934
|
-
{"cloud":"azure-prd","service":"servicebus","type":"messageBrokers",
|
|
2935
|
-
"product":"my-product","component":"notifications-broker","env":"prd",
|
|
2936
|
-
"namespaceName":"myproduct-prd","queueName":"notifications"}]
|
|
2937
|
-
|
|
2938
|
-
Import (register an EXISTING cloud resource):
|
|
2939
|
-
Same as above but use import-persist-all and supply "resource" (the existing resource name/ID):
|
|
2940
|
-
ductape_cli("cloud resources import-persist-all -f brokers.json --json")
|
|
2941
|
-
Each entry: { cloud, service, type: "messageBrokers", product, component, env, resource: "<id>" }
|
|
2942
|
-
|
|
2943
|
-
IMPORTANT: Never share one cloud resource (topic/queue) across snd and prd envs — use
|
|
2944
|
-
separate resources per env to avoid mixing sandbox and production events.
|
|
2945
|
-
|
|
2946
|
-
━━━ STEP 1B: REGISTER SELF-HOSTED BROKER (no cloud connection needed) ━━━
|
|
2947
|
-
|
|
2948
|
-
ductape_cli("resources events create -f broker.json")
|
|
2949
|
-
File: {
|
|
2950
|
-
name: string, tag: string, description?: string,
|
|
2951
|
-
envs: [
|
|
2952
|
-
{
|
|
2953
|
-
slug: "snd",
|
|
2954
|
-
type: "kafka"|"rabbitmq"|"redis"|"nats",
|
|
2955
|
-
config: <see config shapes below>
|
|
2956
|
-
},
|
|
2957
|
-
{ slug: "prd", type: "kafka", config: { ... } }
|
|
2958
|
-
]
|
|
2959
|
-
}
|
|
2960
|
-
|
|
2961
|
-
Config shapes per type:
|
|
2962
|
-
kafka: { brokers: ["host:9092"], clientId: "my-app", groupId?: "...",
|
|
2963
|
-
ssl?: true, sasl?: { mechanism: "plain", username, password } }
|
|
2964
|
-
rabbitmq: { url: "amqp://user:pass@host:5672/vhost" }
|
|
2965
|
-
redis: { host: "...", port: 6379, password?: "..." }
|
|
2966
|
-
nats: { servers: ["nats://host:4222"], token?: "...", user?: "...", pass?: "...", tls?: true }
|
|
2967
|
-
|
|
2968
|
-
━━━ STEP 2: DEFINE TOPICS ━━━
|
|
2969
|
-
|
|
2970
|
-
Topics MUST be defined before any consumer can subscribe to them.
|
|
2971
|
-
Producing to a topic also calls ensureTopicRegistered in the background — but DO NOT rely on
|
|
2972
|
-
auto-registration for consume paths. Always create topics explicitly.
|
|
2973
|
-
|
|
2974
|
-
IMPORTANT: events.topics.create requires an access key (admin operation).
|
|
2975
|
-
Use ductape_cli — NOT ductape_execute — to create topics.
|
|
2976
|
-
|
|
2977
|
-
Write a topic.json file, then:
|
|
2978
|
-
ductape_cli("events topics create -f topic.json")
|
|
2979
|
-
|
|
2980
|
-
topic.json schema:
|
|
2981
|
-
{
|
|
2982
|
-
"tag": "order-events:order-created", // ALWAYS "broker-tag:topic-tag" — full event string
|
|
2983
|
-
"name": "Order Created",
|
|
2984
|
-
"description": "...", // optional
|
|
2985
|
-
"sample": { "orderId": "string", "total": 0 }, // expected message shape
|
|
2986
|
-
"idempotent": false, // optional — deduplicates by idempotency_key when true
|
|
2987
|
-
// AWS SQS only — per-env queue URL:
|
|
2988
|
-
"queueUrls": [
|
|
2989
|
-
{ "env_slug": "snd", "url": "https://sqs.us-east-1.amazonaws.com/123/queue-snd" },
|
|
2990
|
-
{ "env_slug": "prd", "url": "https://sqs.us-east-1.amazonaws.com/123/queue-prd" }
|
|
2991
|
-
]
|
|
2992
|
-
}
|
|
2993
|
-
|
|
2994
|
-
Other topic operations (all require access key via ductape_cli):
|
|
2995
|
-
ductape_cli("events topics list --tag order-events") → list topics for a broker
|
|
2996
|
-
ductape_cli("events topics get --tag order-events:order-created")
|
|
2997
|
-
ductape_cli("events topics update --tag order-events:order-created -f patch.json")
|
|
2998
|
-
ductape_cli("events topics delete --tag order-events:order-created")
|
|
2999
|
-
|
|
3000
|
-
Read-only fetches (safe with publishable key via ductape_execute):
|
|
3001
|
-
ductape_execute("events.fetch", [product_tag, "broker-tag"]) → includes topics[]
|
|
3002
|
-
|
|
3003
|
-
━━━ STEP 3: PRODUCE — WRITTEN IN APPLICATION CODE ━━━
|
|
3004
|
-
|
|
3005
|
-
There is NO admin command to declare a producer. Producers are auto-registered by the SDK on
|
|
3006
|
-
the first produce call — you do not pre-declare them.
|
|
3007
|
-
Do NOT call ductape_generate_payload for messaging. The producer owns the schema.
|
|
3008
|
-
Infer the message shape from context, present it to the user for approval, then implement.
|
|
3009
|
-
|
|
3010
|
-
GENERAL BACKEND (TypeScript/Node.js — not NestJS):
|
|
3011
|
-
import Ductape from '@ductape/sdk';
|
|
3012
|
-
// produce() does not need redis_url.
|
|
3013
|
-
// dispatch() requires redis_url in the Ductape initialization options — it throws at runtime without it.
|
|
3014
|
-
const ductape = new Ductape({
|
|
3015
|
-
accessKey: process.env.DUCTAPE_ACCESS_KEY,
|
|
3016
|
-
redis_url: process.env.DUCTAPE_REDIS_URL, // required for any dispatch(); omit only if never dispatching
|
|
3017
|
-
});
|
|
3018
|
-
await ductape.events.produce({
|
|
3019
|
-
product: "my-product",
|
|
3020
|
-
env: "prd",
|
|
3021
|
-
event: "broker-tag:topic-tag", // always colon-separated
|
|
3022
|
-
message: { key: value },
|
|
3023
|
-
session?: "session-tag:jwt", // optional — traces message to a user session
|
|
3024
|
-
});
|
|
3025
|
-
// Idempotent publish (deduplicates — prevents double-processing on retries):
|
|
3026
|
-
await ductape.events.publishIdempotent({
|
|
3027
|
-
product, env, event, message,
|
|
3028
|
-
idempotencyKey: "order-123-charge", // stable key unique to this logical operation
|
|
3029
|
-
idempotencyTtl?: 86400, // seconds; default 86400 (24h)
|
|
3030
|
-
});
|
|
3031
|
-
|
|
3032
|
-
NESTJS — initialization + method decorators:
|
|
3033
|
-
// AppModule — redisUrl is required whenever any *.dispatch() is used:
|
|
3034
|
-
DuctapeModule.forRootAsync({
|
|
3035
|
-
useFactory: () => ({
|
|
3036
|
-
accessKey: process.env.DUCTAPE_ACCESS_KEY,
|
|
3037
|
-
product: 'my-product',
|
|
3038
|
-
env: process.env.NODE_ENV === 'production' ? 'prd' : 'snd',
|
|
3039
|
-
redisUrl: process.env.DUCTAPE_REDIS_URL, // required — dispatch() throws without this
|
|
3040
|
-
}),
|
|
3041
|
-
});
|
|
3042
|
-
// Environment: DUCTAPE_REDIS_URL=redis://localhost:6379 (local) or rediss://:<pw>@host:6380 (managed)
|
|
3043
|
-
// produce() and @Events.Consumer do NOT need DUCTAPE_REDIS_URL — only dispatch() does.
|
|
3044
|
-
|
|
3045
|
-
import { Events } from '@ductape/nestjs';
|
|
3046
|
-
@Injectable() export class OrdersService {
|
|
3047
|
-
// Immediate produce — method returns the message payload:
|
|
3048
|
-
@Events.Produce({ event: 'order-events:order-created' })
|
|
3049
|
-
emitOrderCreated(payload: { orderId: string; total: number }) { return payload; }
|
|
3050
|
-
|
|
3051
|
-
// Dispatch with static schedule — requires redisUrl in DuctapeModule initialization:
|
|
3052
|
-
@Events.Dispatch({ broker: 'order-events', event: 'order-events:reminder-due',
|
|
3053
|
-
schedule: { every: 86400000 } })
|
|
3054
|
-
scheduleReminder(payload: { message: { orderId: string } }) { return payload; }
|
|
3055
|
-
|
|
3056
|
-
// Dispatch with dynamic schedule (known at call time) — method returns { message, schedule?, retries? }:
|
|
3057
|
-
@Events.Dispatch({ broker: 'order-events', event: 'order-events:fulfillment-due' })
|
|
3058
|
-
scheduleFulfillment(order: Order) {
|
|
3059
|
-
return {
|
|
3060
|
-
message: { orderId: order.id, items: order.items },
|
|
3061
|
-
schedule: { start_at: order.expectedAt },
|
|
3062
|
-
retries: 3,
|
|
3063
|
-
};
|
|
3064
|
-
}
|
|
3065
|
-
// Called as: await this.ordersService.scheduleFulfillment(order);
|
|
3066
|
-
// When method return has a 'message' key, schedule/retries from return take precedence over decorator config.
|
|
3067
|
-
// For even more control (dynamic broker/event), use sdk.events.dispatch() directly.
|
|
3068
|
-
}
|
|
3069
|
-
|
|
3070
|
-
CLIENT-SIDE (browser — publishable key):
|
|
3071
|
-
Clients CAN produce messages using a publishable key + session token.
|
|
3072
|
-
Only produce to topics whose schema is safe for client authorship.
|
|
3073
|
-
NEVER allow clients to produce to topics that trigger privileged server-side operations
|
|
3074
|
-
(payments, admin actions, state mutations) — those must go through a backend endpoint first.
|
|
3075
|
-
import { createClient } from '@ductape/client';
|
|
3076
|
-
// Equivalent supported import: import Ductape from '@ductape/client';
|
|
3077
|
-
const ductape = createClient({
|
|
3078
|
-
publishableKey: 'pk_...',
|
|
3079
|
-
env: 'prd',
|
|
3080
|
-
product: 'my-product',
|
|
3081
|
-
});
|
|
3082
|
-
await ductape.connect();
|
|
3083
|
-
await ductape.brokers.connect({
|
|
3084
|
-
broker: 'user-events',
|
|
3085
|
-
session: 'user-session:eyJ...',
|
|
3086
|
-
});
|
|
3087
|
-
await ductape.brokers.publish({
|
|
3088
|
-
topic: 'user-action',
|
|
3089
|
-
message: { action: "button-click", screen: "dashboard" },
|
|
3090
|
-
session: "user-session:eyJ...", // REQUIRED for client-side produce
|
|
3091
|
-
});
|
|
3092
|
-
|
|
3093
|
-
SCHEDULED DISPATCH (background job):
|
|
3094
|
-
ductape_execute("events.dispatch", [{
|
|
3095
|
-
product, env,
|
|
3096
|
-
event: "order-events:reminder-due", // fully-qualified "broker-tag:topic-tag" — no separate broker field
|
|
3097
|
-
input: { message: { orderId: "123" } },
|
|
3098
|
-
retries?: 3,
|
|
3099
|
-
session?: "session-tag:jwt",
|
|
3100
|
-
schedule?: {
|
|
3101
|
-
start_at?: 1735689600000, // Unix ms or ISO string
|
|
3102
|
-
cron?: "0 9 * * *", // recurring cron
|
|
3103
|
-
every?: 86400000, // recurring interval ms
|
|
3104
|
-
limit?: 10, // max repetitions
|
|
3105
|
-
endDate?: "2026-12-31",
|
|
3106
|
-
tz?: "America/New_York",
|
|
3107
|
-
},
|
|
3108
|
-
}])
|
|
3109
|
-
Returns: { job_id, status: "scheduled"|"queued", scheduled_at, recurring, next_run_at? }
|
|
3110
|
-
|
|
3111
|
-
━━━ STEP 4: CONSUME — WRITTEN IN APPLICATION CODE ━━━
|
|
3112
|
-
|
|
3113
|
-
ACK BEHAVIOR (automatic):
|
|
3114
|
-
- Callback returns successfully → message is acknowledged (ack)
|
|
3115
|
-
- Callback throws → message is tracked as failed; broker nacks/retries per provider behavior
|
|
3116
|
-
- After max retries → message moves to dead-letter queue (DLQ)
|
|
3117
|
-
There is no manual ack API. Acknowledgement is implicit from callback outcome.
|
|
3118
|
-
|
|
3119
|
-
CONSUMER GROUPS (Kafka-specific):
|
|
3120
|
-
Consumer groups are set in the broker's envs[].config.groupId (at broker registration time).
|
|
3121
|
-
All service instances sharing the same groupId form a consumer group and share partition load.
|
|
3122
|
-
To configure: set groupId in the kafka config when creating/updating the broker.
|
|
3123
|
-
|
|
3124
|
-
CONCURRENCY:
|
|
3125
|
-
Ductape has no per-consumer concurrency setting. Concurrency is determined by:
|
|
3126
|
-
- Number of running service instances (horizontal scale)
|
|
3127
|
-
- Broker-level partition count (Kafka) or visibility timeout (SQS)
|
|
3128
|
-
Run multiple instances of your service to scale consumption.
|
|
3129
|
-
|
|
3130
|
-
GENERAL BACKEND (TypeScript/Node.js — not NestJS):
|
|
3131
|
-
await ductape.events.consume({
|
|
3132
|
-
product: "my-product",
|
|
3133
|
-
env: "prd",
|
|
3134
|
-
event: "order-events:order-created", // ALWAYS "broker-tag:topic-tag"
|
|
3135
|
-
callback: async (message) => {
|
|
3136
|
-
// Throw to nack. Return to ack.
|
|
3137
|
-
await processOrder(message as { orderId: string; total: number });
|
|
3138
|
-
},
|
|
3139
|
-
});
|
|
3140
|
-
|
|
3141
|
-
NESTJS — use @Events.Consumer decorator (preferred):
|
|
3142
|
-
import { Events } from '@ductape/nestjs';
|
|
3143
|
-
@Injectable()
|
|
3144
|
-
export class OrderConsumerService {
|
|
3145
|
-
@Events.Consumer({ event: 'order-events:order-created' })
|
|
3146
|
-
async onOrderCreated(message: { orderId: string; total: number }) {
|
|
3147
|
-
await this.processOrder(message);
|
|
3148
|
-
// return to ack; throw to nack
|
|
3149
|
-
}
|
|
3150
|
-
private async processOrder(msg: { orderId: string; total: number }) { /* ... */ }
|
|
3151
|
-
}
|
|
3152
|
-
// DuctapeEventsConsumerService (auto-registered by DuctapeModule) wires this up at startup.
|
|
3153
|
-
// No manual onModuleInit needed.
|
|
3154
|
-
|
|
3155
|
-
// If you need to override product/env for a specific consumer:
|
|
3156
|
-
@Events.Consumer({ event: 'order-events:order-created', product: 'my-product', env: 'prd' })
|
|
3157
|
-
|
|
3158
|
-
CLIENT-SIDE: Clients CANNOT consume. Event consumption is always server-side only.
|
|
3159
|
-
This is the key distinction between server topics (produce + consume) and client-observable
|
|
3160
|
-
topics (produce from client, consume on server). Never set up a consumer in browser code.
|
|
3161
|
-
|
|
3162
|
-
DEAD-LETTER QUEUE (DLQ):
|
|
3163
|
-
Messages whose callbacks consistently throw are automatically moved to the DLQ.
|
|
3164
|
-
Query: ductape_execute("events.messages.getDeadLetters",
|
|
3165
|
-
[{ product, env, brokerTag, topicTag?, consumerTag?, limit? }])
|
|
3166
|
-
Reprocess: ductape_execute("events.reprocessDLQ",
|
|
3167
|
-
[{ product, env, brokerTag, topicTag?, messageIds?, limit? }])
|
|
3168
|
-
Replay: ductape_execute("events.replayEvent",
|
|
3169
|
-
[{ product, env, eventId, force? }])
|
|
3170
|
-
|
|
3171
|
-
━━━ OBSERVABILITY ━━━
|
|
3172
|
-
|
|
3173
|
-
events.messages.query [{ product, env, brokerTag, topicTag?, status?, page?, limit? }]
|
|
3174
|
-
events.messages.getStats [{ product, env, brokerTag }]
|
|
3175
|
-
events.messages.getDashboard [{ product, env, brokerTag }]
|
|
3176
|
-
events.messages.getDeadLetters [{ product, env, brokerTag, topicTag?, consumerTag?, limit? }]
|
|
3177
|
-
events.messages.getProducers [{ product, env, brokerTag, topicTag?, page?, limit? }]
|
|
3178
|
-
events.messages.getConsumers [{ product, env, brokerTag, topicTag?, page?, limit? }]
|
|
3179
|
-
events.replayEvent [{ product, env, eventId, force? }]
|
|
3180
|
-
events.reprocessDLQ [{ product, env, brokerTag, topicTag?, messageIds?, limit? }]
|
|
3181
|
-
events.checkIdempotency [{ product, env, brokerTag, idempotency_key }]
|
|
3182
|
-
`.trim(),
|
|
3183
|
-
|
|
3184
|
-
logs: `
|
|
3185
|
-
DUCTAPE LOGS
|
|
3186
|
-
|
|
3187
|
-
Logs record every SDK operation — actions, features, database queries, notifications, sessions,
|
|
3188
|
-
brokers, storage, cache hits, graph, vector, and resilience events.
|
|
3189
|
-
|
|
3190
|
-
Fetch logs (runtime — ductape_execute):
|
|
3191
|
-
ductape_execute("logs.fetch", [{
|
|
3192
|
-
app_id?: string, // query by app (requires component: "app")
|
|
3193
|
-
product_id?: string, // query by product (requires component: "product")
|
|
3194
|
-
env?: string,
|
|
3195
|
-
start_date?: string, // ISO 8601
|
|
3196
|
-
end_date?: string,
|
|
3197
|
-
page?: number,
|
|
3198
|
-
limit?: number,
|
|
3199
|
-
}])
|
|
3200
|
-
component "app" or "product" is required — there is no default.
|
|
3201
|
-
When component="app", tag is only valid with type: "actions".
|
|
3202
|
-
|
|
3203
|
-
Log data fields (ILogData):
|
|
3204
|
-
process_id, product_tag, env, type (LogEventType), status, data (encrypted if private key set)
|
|
3205
|
-
message?, parent_tag?, child_tag?, app_id?, action?, method?,
|
|
3206
|
-
cache_tag?, cache_key?, cache_status (true = hit),
|
|
3207
|
-
start, end, latency (auto-calculated from start/end),
|
|
3208
|
-
session_user_id?, session_id?, session_tag?, visitor_id?,
|
|
3209
|
-
ip_address?, language?, data_encrypted?, successful_execution?, failed_execution?
|
|
3210
|
-
|
|
3211
|
-
Log event types (LogEventTypes):
|
|
3212
|
-
notifications | push | email | sms | callbacks | slack | discord |
|
|
3213
|
-
database_actions | actions | functions | storage | webhook | jobs |
|
|
3214
|
-
message_broker | producer | consumer | quota | fallback | database_migration |
|
|
3215
|
-
feature | feature_step | database | graph | session | vector | cache | frontend
|
|
3216
|
-
|
|
3217
|
-
Log statuses: success | fail | waiting | processing
|
|
3218
|
-
|
|
3219
|
-
Encryption:
|
|
3220
|
-
When workspace_private_key is provided at SDK init, each log entry's data field is
|
|
3221
|
-
AES-encrypted client-side before transmission. data_encrypted: true is set on the entry.
|
|
3222
|
-
The backend can decrypt when returning logs to the Workbench.
|
|
3223
|
-
|
|
3224
|
-
Emit a log manually (SDK only — not via MCP):
|
|
3225
|
-
logs.add({ process_id, product_tag, env, type, status, data, message?, ... })
|
|
3226
|
-
logs.publish() → flushes all buffered entries to the API in one call
|
|
3227
|
-
|
|
3228
|
-
Notes:
|
|
3229
|
-
- Every SDK service (storage, broker, feature, graph, notifications, sessions, vector, cache,
|
|
3230
|
-
resilience) emits its own logs automatically — manual add/publish is only needed for custom logs.
|
|
3231
|
-
- language defaults to "typescript" so the backend knows which SDK emitted the entry.
|
|
3232
|
-
- Logs are batched in memory and sent in a single publish() call per operation.
|
|
3233
|
-
`.trim(),
|
|
3234
|
-
|
|
3235
|
-
client: `
|
|
3236
|
-
DUCTAPE CLIENT SDK (@ductape/client)
|
|
3237
|
-
|
|
3238
|
-
The client SDK is for frontend applications (React, Vue, Svelte, vanilla JS). All HTTP
|
|
3239
|
-
operations are routed through the Ductape proxy using a publishableKey. WebSocket subscriptions
|
|
3240
|
-
connect to the proxy's /realtime gateway. Never use this package on the server — use @ductape/sdk
|
|
3241
|
-
for server-side code.
|
|
3242
|
-
|
|
3243
|
-
INSTALL
|
|
3244
|
-
npm install @ductape/client
|
|
3245
|
-
|
|
3246
|
-
INITIALIZATION
|
|
3247
|
-
import { createClient } from '@ductape/client';
|
|
3248
|
-
// or: import Ductape from '@ductape/client';
|
|
3249
|
-
|
|
3250
|
-
const ductape = createClient({
|
|
3251
|
-
publishableKey: 'pk_live_...', // from Workbench → Settings → Publishable Keys
|
|
3252
|
-
baseUrl: 'https://your-proxy.example.com', // your deployed proxy URL
|
|
3253
|
-
product: 'my-product', // default product tag (can be overridden per call)
|
|
3254
|
-
env: 'prd', // default environment slug
|
|
3255
|
-
});
|
|
3256
|
-
|
|
3257
|
-
When publishableKey is set:
|
|
3258
|
-
- HTTP calls go to baseUrl/proxy/v1/sdk-proxy/execute
|
|
3259
|
-
- WebSocket connects to wss://your-proxy.example.com/realtime?token=pk_live_...
|
|
3260
|
-
- No server secret is exposed to the browser
|
|
3261
|
-
|
|
3262
|
-
Use accessKey only for server-side or trusted environments — never in a browser bundle.
|
|
3263
|
-
Override wsUrl to point to a custom WebSocket endpoint.
|
|
3264
|
-
|
|
3265
|
-
REAL-TIME CONNECTION
|
|
3266
|
-
await ductape.connect(); // opens WebSocket; required before any .subscribe() call
|
|
3267
|
-
ductape.disconnect(); // closes WebSocket and clears all subscriptions
|
|
3268
|
-
ductape.isConnected // boolean
|
|
3269
|
-
ductape.connectionState // 'disconnected' | 'connecting' | 'connected' | 'reconnecting'
|
|
3270
|
-
ductape.onConnectionChange(cb) // listen to state changes; returns unsubscribe fn
|
|
3271
|
-
|
|
3272
|
-
SERVICES
|
|
3273
|
-
ductape.databases DatabaseService — CRUD + real-time query subscriptions
|
|
3274
|
-
ductape.features FeatureService — execute features, subscribe to execution status
|
|
3275
|
-
ductape.agents AgentService — run AI agents, subscribe to output stream
|
|
3276
|
-
ductape.vectors VectorService — vector upsert / search
|
|
3277
|
-
ductape.graphs GraphService — graph queries + subscriptions
|
|
3278
|
-
ductape.brokers BrokerService — publish / subscribe to message brokers
|
|
3279
|
-
ductape.sessions SessionService — verify, refresh, revoke sessions
|
|
3280
|
-
ductape.resilience ResilienceService — subscribe to healthcheck status
|
|
3281
|
-
ductape.storage StorageService — upload / download files
|
|
3282
|
-
ductape.warehouse WarehouseService — cross-store federated queries
|
|
3283
|
-
ductape.cache CacheService — key-value cache get / set / clear
|
|
3284
|
-
ductape.api ApiService — run app actions with OAuth credential support
|
|
3285
|
-
ductape.notifications NotificationsService — send push / email / SMS / callback
|
|
3286
|
-
ductape.analytics AnalyticsService — track pageviews, clicks, custom events
|
|
3287
|
-
ductape.workflows (deprecated alias → features)
|
|
3288
|
-
ductape.actions (deprecated alias → api)
|
|
3289
|
-
|
|
3290
|
-
DATABASE SERVICE
|
|
3291
|
-
await ductape.databases.connect({ database: 'core-db' });
|
|
3292
|
-
const { data } = await ductape.databases.query({ entity: 'users', where: { active: true } });
|
|
3293
|
-
await ductape.databases.insert({ entity: 'users', data: { name: 'Alice', email: 'a@b.com' } });
|
|
3294
|
-
await ductape.databases.update({ entity: 'users', where: { id: 1 }, data: { name: 'Bob' } });
|
|
3295
|
-
await ductape.databases.delete({ entity: 'users', where: { id: 1 } });
|
|
3296
|
-
await ductape.databases.upsert({ entity: 'users', data: { email: 'a@b.com' }, conflict: ['email'] });
|
|
3297
|
-
const total = await ductape.databases.count({ entity: 'users', where: { active: true } });
|
|
3298
|
-
await ductape.databases.raw({ query: 'SELECT * FROM users WHERE id = $1', params: [1] });
|
|
3299
|
-
await ductape.databases.transaction(async (tx) => {
|
|
3300
|
-
await tx.insert({ entity: 'orders', data: { ... } });
|
|
3301
|
-
await tx.update({ entity: 'inventory', where: { ... }, data: { ... } });
|
|
3302
|
-
});
|
|
3303
|
-
await ductape.databases.disconnect();
|
|
3304
|
-
|
|
3305
|
-
Real-time subscription (ductape.connect() must be called first):
|
|
3306
|
-
const sub = ductape.databases.subscribe(
|
|
3307
|
-
{ entity: 'orders', where: { status: 'pending' } },
|
|
3308
|
-
(rows) => console.log('live update', rows),
|
|
3309
|
-
);
|
|
3310
|
-
sub.unsubscribe(); // stop receiving updates
|
|
3311
|
-
|
|
3312
|
-
FEATURE SERVICE
|
|
3313
|
-
const { executionId } = await ductape.features.execute({ feature: 'onboard-user', input: { userId: 'u_1' } });
|
|
3314
|
-
const status = await ductape.features.status({ feature: 'onboard-user', executionId });
|
|
3315
|
-
await ductape.features.cancel({ feature: 'onboard-user', executionId, reason: 'user request' });
|
|
3316
|
-
await ductape.features.signal({ feature: 'onboard-user', executionId, signal: 'payment-confirmed' });
|
|
3317
|
-
const history = await ductape.features.history({ feature: 'onboard-user', executionId });
|
|
3318
|
-
const { executions } = await ductape.features.list({ status: 'running', limit: 20 });
|
|
3319
|
-
|
|
3320
|
-
Real-time status subscription:
|
|
3321
|
-
const sub = ductape.features.subscribe(
|
|
3322
|
-
{ feature: 'onboard-user', executionId },
|
|
3323
|
-
(events) => {
|
|
3324
|
-
const ev = events[0]; // { executionId, feature, status, currentStep, output, error }
|
|
3325
|
-
console.log(ev.status, ev.currentStep);
|
|
3326
|
-
if (ev.status === 'completed' || ev.status === 'failed') sub.unsubscribe();
|
|
3327
|
-
},
|
|
3328
|
-
);
|
|
3329
|
-
|
|
3330
|
-
AGENT SERVICE
|
|
3331
|
-
const result = await ductape.agents.run({ agent: 'support-bot', input: { message: 'Help!' } });
|
|
3332
|
-
const sub = ductape.agents.subscribe(
|
|
3333
|
-
{ agent: 'support-bot', executionId: result.executionId },
|
|
3334
|
-
(events) => console.log('chunk', events[0]),
|
|
3335
|
-
);
|
|
3336
|
-
sub.unsubscribe();
|
|
3337
|
-
|
|
3338
|
-
BROKER SERVICE
|
|
3339
|
-
await ductape.brokers.connect({ broker: 'notifications-broker', session: 'user-session:eyJ...' });
|
|
3340
|
-
await ductape.brokers.publish({ topic: 'chat.message', message: { text: 'Hello', userId: 'u_1' } });
|
|
3341
|
-
const sub = ductape.brokers.subscribe(
|
|
3342
|
-
{ topic: 'chat.message' },
|
|
3343
|
-
(msgs) => {
|
|
3344
|
-
const m = msgs[0]; // { topic, message, headers?, key?, timestamp, offset?, partition? }
|
|
3345
|
-
console.log('new message', m.message);
|
|
3346
|
-
},
|
|
3347
|
-
);
|
|
3348
|
-
sub.unsubscribe();
|
|
3349
|
-
await ductape.brokers.disconnect();
|
|
3350
|
-
|
|
3351
|
-
Passing session scopes delivery to a specific end-user so the server applies session-level
|
|
3352
|
-
authorization. Without session, messages for the whole product+env are delivered.
|
|
3353
|
-
|
|
3354
|
-
GRAPH SERVICE
|
|
3355
|
-
const nodes = await ductape.graphs.findNodes({ labels: ['User'], where: { active: true } });
|
|
3356
|
-
const sub = ductape.graphs.subscribe({ labels: ['Order'] }, (nodes) => console.log(nodes));
|
|
3357
|
-
sub.unsubscribe();
|
|
3358
|
-
|
|
3359
|
-
RESILIENCE SERVICE (health subscriptions)
|
|
3360
|
-
const sub = ductape.resilience.subscribe(
|
|
3361
|
-
{ tag: 'payment-health' },
|
|
3362
|
-
(events) => console.log('health change', events[0]),
|
|
3363
|
-
);
|
|
3364
|
-
sub.unsubscribe();
|
|
3365
|
-
|
|
3366
|
-
SESSIONS SERVICE
|
|
3367
|
-
const { valid } = await ductape.sessions.verify({ token: 'user-session:eyJ...' });
|
|
3368
|
-
await ductape.sessions.revoke({ token: 'user-session:eyJ...' });
|
|
3369
|
-
const { token } = await ductape.sessions.refresh({ refreshToken: '...' });
|
|
3370
|
-
|
|
3371
|
-
ANALYTICS SERVICE
|
|
3372
|
-
ductape.analytics.identify('player-session:eyJ...'); // full Ductape session token
|
|
3373
|
-
await ductape.analytics.pageview({ path: '/dashboard', title: document.title });
|
|
3374
|
-
await ductape.analytics.track({
|
|
3375
|
-
event: 'button_clicked',
|
|
3376
|
-
properties: { button: 'sign-up' },
|
|
3377
|
-
});
|
|
3378
|
-
await ductape.analytics.flush();
|
|
3379
|
-
ductape.analytics.clearSession(); // logout/account switch
|
|
3380
|
-
See ductape_docs({ topic: "frontend-analytics" }) before enabling auto-capture.
|
|
3381
|
-
|
|
3382
|
-
FRAMEWORK-SPECIFIC PACKAGES
|
|
3383
|
-
For React and Vue projects, use the dedicated packages instead of managing the client manually:
|
|
3384
|
-
@ductape/react — DuctapeProvider + hooks (useDatabaseQuery, useFeatureSubscription, etc.)
|
|
3385
|
-
See: ductape_docs({ topic: "react" })
|
|
3386
|
-
@ductape/vue — createDuctape() plugin + composables (useDatabaseQuery, useFeatureSubscription, etc.)
|
|
3387
|
-
See: ductape_docs({ topic: "vue" })
|
|
3388
|
-
|
|
3389
|
-
Use @ductape/client directly only for vanilla JS, Svelte, Angular, or custom integrations.
|
|
3390
|
-
|
|
3391
|
-
IMPORTANT
|
|
3392
|
-
- Call ductape.connect() before any .subscribe() — it throws if not connected.
|
|
3393
|
-
- Always clean up (sub.unsubscribe()) in component teardown to prevent memory leaks.
|
|
3394
|
-
- The client auto-resubscribes after a WebSocket reconnect — no manual retry needed.
|
|
3395
|
-
- publishableKey is safe in browser bundles; it grants only proxy-authorized operations.
|
|
3396
|
-
- For SSR, skip connect() on the server; call it only client-side (useEffect / onMounted).
|
|
3397
|
-
- The client SDK does NOT expose secrets, access keys, or workspace admin operations.
|
|
3398
|
-
- Pass session (format session_tag:jwt) to broker connect/subscribe for per-user scoping.
|
|
3399
|
-
`.trim(),
|
|
3400
|
-
|
|
3401
|
-
react: `
|
|
3402
|
-
DUCTAPE REACT (@ductape/react)
|
|
3403
|
-
|
|
3404
|
-
React hooks and context provider for Ductape. Wraps @ductape/client.
|
|
3405
|
-
Requires react >= 17.
|
|
3406
|
-
|
|
3407
|
-
INSTALL
|
|
3408
|
-
npm install @ductape/react
|
|
3409
|
-
|
|
3410
|
-
SETUP — wrap your app root with DuctapeProvider
|
|
3411
|
-
import { DuctapeProvider } from '@ductape/react';
|
|
3412
|
-
|
|
3413
|
-
function App() {
|
|
3414
|
-
return (
|
|
3415
|
-
<DuctapeProvider
|
|
3416
|
-
config={{
|
|
3417
|
-
publishableKey: import.meta.env.VITE_PUBLISHABLE_KEY,
|
|
3418
|
-
baseUrl: import.meta.env.VITE_PROXY_URL,
|
|
3419
|
-
product: 'my-product',
|
|
3420
|
-
env: 'prd',
|
|
3421
|
-
}}
|
|
3422
|
-
autoConnect={false}
|
|
3423
|
-
>
|
|
3424
|
-
<MyApp />
|
|
3425
|
-
</DuctapeProvider>
|
|
3426
|
-
);
|
|
3427
|
-
}
|
|
3428
|
-
|
|
3429
|
-
DuctapeProvider props:
|
|
3430
|
-
config IDuctapeClientConfig — publishableKey (or accessKey), baseUrl, product, env
|
|
3431
|
-
autoConnect boolean (default false) — connect WebSocket automatically on mount
|
|
3432
|
-
onConnected () => void
|
|
3433
|
-
onDisconnected () => void
|
|
3434
|
-
onError (error: Error) => void
|
|
3435
|
-
onConnectionChange (state: ConnectionState) => void
|
|
3436
|
-
|
|
3437
|
-
CORE HOOKS
|
|
3438
|
-
useDuctape() → { client, isConnected, connectionState, connect, disconnect, isReady }
|
|
3439
|
-
useDuctapeContext() → same; throws if called outside DuctapeProvider
|
|
3440
|
-
|
|
3441
|
-
DATABASE HOOKS
|
|
3442
|
-
useDatabase(database)
|
|
3443
|
-
→ { isConnected, isConnecting, error, connect, disconnect }
|
|
3444
|
-
Call connect() in a useEffect to open the database session.
|
|
3445
|
-
|
|
3446
|
-
useDatabaseQuery(key, queryOptions, hookOptions?)
|
|
3447
|
-
→ { data: IQueryResult<T>, isLoading, error, refetch }
|
|
3448
|
-
key: string | string[] — deduplication key
|
|
3449
|
-
queryOptions: { table, where?, select?, limit?, offset?, sort? }
|
|
3450
|
-
|
|
3451
|
-
useDatabaseInsert(hookOptions?) → { mutate({ table, data }), isLoading, error, data }
|
|
3452
|
-
useDatabaseUpdate(hookOptions?) → { mutate({ table, where, data }), isLoading, error, data }
|
|
3453
|
-
useDatabaseDelete(hookOptions?) → { mutate({ table, where }), isLoading, error, data }
|
|
3454
|
-
|
|
3455
|
-
useDatabaseSubscription(subscribeOptions, hookOptions?)
|
|
3456
|
-
→ { data: T[] | null, isSubscribed, error, unsubscribe, resubscribe }
|
|
3457
|
-
subscribeOptions: { table, where?, select? }
|
|
3458
|
-
Requires autoConnect: true (or manual connect()) and databases.connect() before subscribing.
|
|
3459
|
-
|
|
3460
|
-
FEATURE HOOKS
|
|
3461
|
-
useFeatureExecute(hookOptions?)
|
|
3462
|
-
→ { mutate({ feature, input?, product?, env? }), isLoading, error, data }
|
|
3463
|
-
useFeatureStatus(statusInput, hookOptions?)
|
|
3464
|
-
→ { data: FeatureStatus, isLoading, error, refetch }
|
|
3465
|
-
statusInput: { feature, executionId, product?, env? }
|
|
3466
|
-
useFeatureSubscription(subscribeOptions, hookOptions?)
|
|
3467
|
-
→ { data: FeatureStatusEvent[], isSubscribed, error, unsubscribe, resubscribe }
|
|
3468
|
-
subscribeOptions: { feature, executionId, product?, env? }
|
|
3469
|
-
useFeatureSignal(hookOptions?) → { mutate({ feature, executionId, signal, data? }), isLoading }
|
|
3470
|
-
useFeatureCancel(hookOptions?) → { mutate({ feature, executionId, reason? }), isLoading }
|
|
3471
|
-
|
|
3472
|
-
AGENT HOOKS
|
|
3473
|
-
useAgentRun(hookOptions?)
|
|
3474
|
-
→ { mutate({ tag, input?, sessionId? }), isLoading, error, data: IAgentExecutionResult }
|
|
3475
|
-
useAgentStream(tag, input?, hookOptions?)
|
|
3476
|
-
→ { events, content, isStreaming, isComplete, error, start, stop, reset }
|
|
3477
|
-
content: accumulated string from all stream text events
|
|
3478
|
-
useAgentStatus(statusInput, hookOptions?) → { data, isLoading, error, refetch }
|
|
3479
|
-
useAgentSignal(hookOptions?) → { mutate, isLoading, error }
|
|
3480
|
-
|
|
3481
|
-
BROKER HOOKS
|
|
3482
|
-
useBroker()
|
|
3483
|
-
→ { isConnected, isConnecting, error, connect, disconnect }
|
|
3484
|
-
connect({ broker, session?, product?, env? }) forwards the complete options object to
|
|
3485
|
-
@ductape/client. Pass session for player-scoped authorization.
|
|
3486
|
-
useBrokerPublish(hookOptions?)
|
|
3487
|
-
→ { mutate({ topic, message, headers?, key? }), isLoading, error, data }
|
|
3488
|
-
useBrokerSubscription(subscribeOptions, hookOptions?)
|
|
3489
|
-
→ { data: BrokerMessage[], isSubscribed, error, unsubscribe, resubscribe }
|
|
3490
|
-
subscribeOptions: { broker?, topic, group?, session?, product?, env? }
|
|
3491
|
-
|
|
3492
|
-
GRAPH HOOKS
|
|
3493
|
-
useGraph(graphTag) → { isConnected, isConnecting, error, connect, disconnect }
|
|
3494
|
-
useCreateNode(hookOptions?) → { mutate({ labels, properties }), isLoading, error, data }
|
|
3495
|
-
useUpdateNode(hookOptions?) → { mutate({ id, properties }), isLoading, error, data }
|
|
3496
|
-
useDeleteNode(hookOptions?) → { mutate({ id, detach? }), isLoading, error, data }
|
|
3497
|
-
useQueryNodes(key, options, hookOptions?) → { data, isLoading, error }
|
|
3498
|
-
useCreateRelationship(hookOptions?) → { mutate, isLoading, error, data }
|
|
3499
|
-
useDeleteRelationship(hookOptions?) → { mutate, isLoading, error }
|
|
3500
|
-
useQueryRelationships(key, options, hookOptions?) → { data, isLoading, error }
|
|
3501
|
-
useTraverse(key, options, hookOptions?) → { data, isLoading, error }
|
|
3502
|
-
useShortestPath(key, options, hookOptions?) → { data, isLoading, error }
|
|
3503
|
-
useGraphSubscription(subscribeOptions, hookOptions?)
|
|
3504
|
-
→ { data, isSubscribed, error, unsubscribe, resubscribe }
|
|
3505
|
-
|
|
3506
|
-
VECTOR HOOKS
|
|
3507
|
-
useVector(vectorTag) → { isConnected, isConnecting, error, connect, disconnect }
|
|
3508
|
-
useVectorQuery(key, options, hookOptions?) → { data, isLoading, error }
|
|
3509
|
-
useVectorSearch(key, options, hookOptions?) → { data, isLoading, error }
|
|
3510
|
-
useVectorUpsert(hookOptions?) → { mutate({ id, values, metadata? }), isLoading, error, data }
|
|
3511
|
-
useVectorDelete(hookOptions?) → { mutate({ ids }), isLoading, error }
|
|
3512
|
-
useVectorFetch(key, options, hookOptions?) → { data: VectorFetchResult, isLoading, error }
|
|
3513
|
-
useVectorStats(key, options, hookOptions?) → { data: VectorStatsResult, isLoading, error }
|
|
3514
|
-
useVectorNamespaces(key, options, hookOptions?) → { data, isLoading, error }
|
|
3515
|
-
|
|
3516
|
-
STORAGE HOOKS
|
|
3517
|
-
useUpload(hookOptions?)
|
|
3518
|
-
→ { upload({ storage, fileName, buffer, mimeType? }), isLoading, progress, error, data }
|
|
3519
|
-
progress: 0–100 number
|
|
3520
|
-
useDownload(hookOptions?) → { mutate, isLoading, error, data }
|
|
3521
|
-
useStorageDelete(hookOptions?) → { mutate, isLoading, error }
|
|
3522
|
-
useListFiles(key, options, hookOptions?) → { data, isLoading, error }
|
|
3523
|
-
useSignedUrl(key, options, hookOptions?) → { data: string, isLoading, error }
|
|
3524
|
-
|
|
3525
|
-
SESSION HOOKS
|
|
3526
|
-
useSessionStart(hookOptions?) → { mutate, isLoading, error, data: { token, refreshToken } }
|
|
3527
|
-
useSessionVerify(hookOptions?) → { mutate, isLoading, error, data }
|
|
3528
|
-
useSessionRefresh(hookOptions?) → { mutate, isLoading, error, data }
|
|
3529
|
-
useSessionRevoke(hookOptions?) → { mutate, isLoading, error }
|
|
3530
|
-
useSessionRevokeAll(hookOptions?) → { mutate, isLoading, error }
|
|
3531
|
-
useSessionList(key, options, hookOptions?) → { data, isLoading, error }
|
|
3532
|
-
useSessionAutoRefresh(config)
|
|
3533
|
-
→ { token, isRefreshing }; auto-refreshes before expiry; no manual call needed
|
|
3534
|
-
|
|
3535
|
-
CACHE HOOKS
|
|
3536
|
-
useCacheGet(key, options, hookOptions?) → { data, isLoading, error }
|
|
3537
|
-
useCacheSet(hookOptions?) → { mutate({ key, value, expiry? }), isLoading, error }
|
|
3538
|
-
useCacheDelete(hookOptions?) → { mutate({ key }), isLoading, error }
|
|
3539
|
-
useCacheExists(key, options, hookOptions?) → { data: boolean, isLoading, error }
|
|
3540
|
-
useCacheGetMany / useCacheSetMany / useCacheDeleteMany
|
|
3541
|
-
|
|
3542
|
-
RESILIENCE HOOKS
|
|
3543
|
-
useHealthStatus(key, options, hookOptions?)
|
|
3544
|
-
→ { data: HealthStatusResult, isLoading, error, refetch }
|
|
3545
|
-
useHealthSubscription(subscribeOptions, hookOptions?)
|
|
3546
|
-
→ { data: HealthChangeEvent[], isSubscribed, error, unsubscribe, resubscribe }
|
|
3547
|
-
useQuotaCheck(hookOptions?) → { mutate, isLoading, error, data: QuotaCheckResult }
|
|
3548
|
-
useQuotaStatus(key, options, hookOptions?) → { data, isLoading, error }
|
|
3549
|
-
useQuotaSubscription(subscribeOptions, hookOptions?) → { data, isSubscribed, error }
|
|
3550
|
-
|
|
3551
|
-
WAREHOUSE HOOKS
|
|
3552
|
-
useWarehouseQuery / useWarehouseSelect / useWarehouseInsert
|
|
3553
|
-
useWarehouseUpdate / useWarehouseDelete / useWarehouseUpsert / useWarehouseTransaction
|
|
3554
|
-
|
|
3555
|
-
ACTIONS HOOKS
|
|
3556
|
-
useActions(app) → { isConnected, connect, disconnect }
|
|
3557
|
-
useActionRun(hookOptions?) → { mutate, isLoading, error, data }
|
|
3558
|
-
|
|
3559
|
-
ANALYTICS HOOK
|
|
3560
|
-
useAnalytics() → { pageview, track, identify, visitorId, enableAutoCapture, flush }
|
|
3561
|
-
The currently installed hook does NOT return clearSession or disableAutoCapture.
|
|
3562
|
-
Use useDuctape().client.analytics.clearSession()/disableAutoCapture(), or verify a newer installed
|
|
3563
|
-
package export before generating those hook calls. See ductape_docs({ topic: "frontend-analytics" }).
|
|
3564
|
-
|
|
3565
|
-
hookOptions pattern (applies to all hooks):
|
|
3566
|
-
enabled? boolean — false skips auto-fetch/subscribe
|
|
3567
|
-
onSuccess? (data) => void
|
|
3568
|
-
onError? (error: Error) => void
|
|
3569
|
-
Subscriptions also accept: onData, onSubscribe, onUnsubscribe
|
|
3570
|
-
|
|
3571
|
-
EXAMPLE
|
|
3572
|
-
// main.tsx
|
|
3573
|
-
import { DuctapeProvider } from '@ductape/react';
|
|
3574
|
-
root.render(
|
|
3575
|
-
<DuctapeProvider config={{ publishableKey: 'pk_...', baseUrl: '...', product: 'my-app', env: 'prd' }} autoConnect>
|
|
3576
|
-
<App />
|
|
3577
|
-
</DuctapeProvider>
|
|
3578
|
-
);
|
|
3579
|
-
|
|
3580
|
-
// UsersList.tsx
|
|
3581
|
-
import { useDatabaseQuery } from '@ductape/react';
|
|
3582
|
-
function UsersList() {
|
|
3583
|
-
const { data, isLoading } = useDatabaseQuery('users', { table: 'users', limit: 20 });
|
|
3584
|
-
if (isLoading) return <p>Loading...</p>;
|
|
3585
|
-
return <ul>{data?.rows.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
|
|
3586
|
-
}
|
|
3587
|
-
|
|
3588
|
-
// FeatureTracker.tsx
|
|
3589
|
-
import { useFeatureSubscription } from '@ductape/react';
|
|
3590
|
-
function FeatureTracker({ executionId }) {
|
|
3591
|
-
const { data: events } = useFeatureSubscription(
|
|
3592
|
-
{ feature: 'onboard-user', executionId },
|
|
3593
|
-
{ onData: (evs) => console.log('status:', evs[0].status) },
|
|
3594
|
-
);
|
|
3595
|
-
return <p>{events?.[0]?.status ?? 'waiting'}</p>;
|
|
3596
|
-
}
|
|
3597
|
-
|
|
3598
|
-
DEPRECATED ALIASES
|
|
3599
|
-
useWorkflowExecute → useFeatureExecute
|
|
3600
|
-
useWorkflowStatus → useFeatureStatus
|
|
3601
|
-
useWorkflowSubscription → useFeatureSubscription
|
|
3602
|
-
useWorkflowSignal → useFeatureSignal
|
|
3603
|
-
useWorkflowCancel → useFeatureCancel
|
|
3604
|
-
`.trim(),
|
|
3605
|
-
|
|
3606
|
-
vue: `
|
|
3607
|
-
DUCTAPE VUE (@ductape/vue)
|
|
3608
|
-
|
|
3609
|
-
Vue 3 composables and plugin for Ductape. Wraps @ductape/client.
|
|
3610
|
-
Requires vue >= 3.
|
|
3611
|
-
|
|
3612
|
-
INSTALL
|
|
3613
|
-
npm install @ductape/vue
|
|
3614
|
-
|
|
3615
|
-
SETUP — install the plugin at app root
|
|
3616
|
-
// main.ts
|
|
3617
|
-
import { createApp } from 'vue';
|
|
3618
|
-
import { createDuctape } from '@ductape/vue';
|
|
3619
|
-
import App from './App.vue';
|
|
3620
|
-
|
|
3621
|
-
const app = createApp(App);
|
|
3622
|
-
|
|
3623
|
-
app.use(createDuctape({
|
|
3624
|
-
publishableKey: import.meta.env.VITE_PUBLISHABLE_KEY,
|
|
3625
|
-
baseUrl: import.meta.env.VITE_PROXY_URL,
|
|
3626
|
-
product: 'my-product',
|
|
3627
|
-
env: 'prd',
|
|
3628
|
-
autoConnect: false, // true = connect WebSocket when plugin installs
|
|
3629
|
-
}));
|
|
3630
|
-
|
|
3631
|
-
app.mount('#app');
|
|
3632
|
-
|
|
3633
|
-
Plugin options (extends IDuctapeClientConfig):
|
|
3634
|
-
publishableKey / accessKey — auth
|
|
3635
|
-
baseUrl — proxy URL
|
|
3636
|
-
product, env — defaults for all composables
|
|
3637
|
-
autoConnect — boolean (default false)
|
|
3638
|
-
|
|
3639
|
-
The client is provided via inject(DUCTAPE_INJECTION_KEY) and is available on
|
|
3640
|
-
this.$ductape in Options API components.
|
|
3641
|
-
|
|
3642
|
-
CORE COMPOSABLE
|
|
3643
|
-
useDuctape()
|
|
3644
|
-
→ { client, isReady, isConnected: Ref<boolean>, connectionState: Ref<ConnectionState>,
|
|
3645
|
-
error: Ref<Error|null>, connect, disconnect }
|
|
3646
|
-
|
|
3647
|
-
DATABASE COMPOSABLES
|
|
3648
|
-
useDatabase(database)
|
|
3649
|
-
→ { isConnected, isConnecting, error, connect, disconnect } — all Refs
|
|
3650
|
-
Call connect() in onMounted to open the database session.
|
|
3651
|
-
|
|
3652
|
-
useDatabaseQuery(key, queryOptions, composableOptions?)
|
|
3653
|
-
→ { data: Ref<IQueryResult<T>|null>, isLoading: Ref<boolean>, error: Ref<Error|null>, refetch }
|
|
3654
|
-
|
|
3655
|
-
useDatabaseInsert(composableOptions?) → { mutate({ table, data }), isLoading, error, data }
|
|
3656
|
-
useDatabaseUpdate(composableOptions?) → { mutate({ table, where, data }), isLoading, error, data }
|
|
3657
|
-
useDatabaseDelete(composableOptions?) → { mutate({ table, where }), isLoading, error, data }
|
|
3658
|
-
|
|
3659
|
-
useDatabaseSubscription(subscribeOptions, composableOptions?)
|
|
3660
|
-
→ { data: Ref<T[]|null>, isSubscribed: Ref<boolean>, error, unsubscribe, resubscribe }
|
|
3661
|
-
subscribeOptions: { table, where?, select? }
|
|
3662
|
-
|
|
3663
|
-
FEATURE COMPOSABLES
|
|
3664
|
-
useFeatureExecute(composableOptions?)
|
|
3665
|
-
→ { mutate({ feature, input?, product?, env? }), isLoading, error, data }
|
|
3666
|
-
useFeatureStatus(statusInput, composableOptions?)
|
|
3667
|
-
→ { data: Ref<FeatureStatus|null>, isLoading, error, refetch }
|
|
3668
|
-
useFeatureSubscription(subscribeOptions, composableOptions?)
|
|
3669
|
-
→ { data: Ref<FeatureStatusEvent[]|null>, isSubscribed, error, unsubscribe, resubscribe }
|
|
3670
|
-
subscribeOptions: { feature, executionId, product?, env? }
|
|
3671
|
-
useFeatureSignal(composableOptions?) → { mutate, isLoading, error }
|
|
3672
|
-
useFeatureCancel(composableOptions?) → { mutate, isLoading, error }
|
|
3673
|
-
|
|
3674
|
-
AGENT COMPOSABLES
|
|
3675
|
-
useAgentRun(composableOptions?)
|
|
3676
|
-
→ { mutate({ tag, input?, sessionId? }), isLoading, error, data }
|
|
3677
|
-
useAgentStream(tag, input?, composableOptions?)
|
|
3678
|
-
→ { events, content, isStreaming, isComplete, error, start, stop, reset }
|
|
3679
|
-
content: Ref<string> accumulated from stream text events
|
|
3680
|
-
useAgentStatus(statusInput, composableOptions?) → { data, isLoading, error, refetch }
|
|
3681
|
-
useAgentSignal(composableOptions?) → { mutate, isLoading, error }
|
|
3682
|
-
|
|
3683
|
-
BROKER COMPOSABLES
|
|
3684
|
-
useBroker(broker, options?)
|
|
3685
|
-
→ { isConnected, isConnecting, error, connect, disconnect }
|
|
3686
|
-
useBrokerPublish(composableOptions?)
|
|
3687
|
-
→ { mutate({ topic, message, headers?, key? }), isLoading, error, data }
|
|
3688
|
-
useBrokerSubscription(subscribeOptions, composableOptions?)
|
|
3689
|
-
→ { data: Ref<BrokerMessage[]|null>, isSubscribed, error, unsubscribe, resubscribe }
|
|
3690
|
-
subscribeOptions: { topic, group? }
|
|
3691
|
-
|
|
3692
|
-
GRAPH COMPOSABLES
|
|
3693
|
-
useGraph(graphTag) → { isConnected, isConnecting, error, connect, disconnect }
|
|
3694
|
-
useCreateNode / useUpdateNode / useDeleteNode / useQueryNodes
|
|
3695
|
-
useCreateRelationship / useDeleteRelationship / useQueryRelationships
|
|
3696
|
-
useTraverse / useShortestPath
|
|
3697
|
-
useGraphSubscription(subscribeOptions, composableOptions?)
|
|
3698
|
-
→ { data, isSubscribed, error, unsubscribe, resubscribe }
|
|
3699
|
-
|
|
3700
|
-
VECTOR COMPOSABLES
|
|
3701
|
-
useVector(vectorTag) → { isConnected, isConnecting, error, connect, disconnect }
|
|
3702
|
-
useVectorQuery / useVectorSearch / useVectorUpsert / useVectorDelete
|
|
3703
|
-
useVectorFetch / useVectorStats / useVectorNamespaces
|
|
3704
|
-
|
|
3705
|
-
STORAGE COMPOSABLES
|
|
3706
|
-
useUpload(composableOptions?)
|
|
3707
|
-
→ { upload({ storage, fileName, buffer, mimeType? }), isLoading, progress: Ref<number>, error, data }
|
|
3708
|
-
useDownload / useStorageDelete / useListFiles / useSignedUrl
|
|
3709
|
-
|
|
3710
|
-
SESSION COMPOSABLES
|
|
3711
|
-
useSessionStart / useSessionVerify / useSessionRefresh
|
|
3712
|
-
useSessionRevoke / useSessionRevokeAll / useSessionList
|
|
3713
|
-
useSessionAutoRefresh(config) → { token: Ref<string|null>, isRefreshing: Ref<boolean> }
|
|
3714
|
-
|
|
3715
|
-
CACHE COMPOSABLES
|
|
3716
|
-
useCacheGet / useCacheSet / useCacheDelete / useCacheExists
|
|
3717
|
-
useCacheGetMany / useCacheSetMany / useCacheDeleteMany
|
|
3718
|
-
|
|
3719
|
-
RESILIENCE COMPOSABLES
|
|
3720
|
-
useHealthStatus / useHealthSubscription
|
|
3721
|
-
useQuotaRun / useQuotaCheck / useQuotaStatus / useQuotaSubscription
|
|
3722
|
-
useFallbackRun
|
|
3723
|
-
|
|
3724
|
-
WAREHOUSE COMPOSABLES
|
|
3725
|
-
useWarehouseQuery / useWarehouseSelect / useWarehouseInsert
|
|
3726
|
-
useWarehouseUpdate / useWarehouseDelete / useWarehouseUpsert / useWarehouseTransaction
|
|
3727
|
-
|
|
3728
|
-
ACTIONS COMPOSABLES
|
|
3729
|
-
useActions(app) → { isConnected, connect, disconnect }
|
|
3730
|
-
useActionRun(composableOptions?) → { mutate, isLoading, error, data }
|
|
3731
|
-
|
|
3732
|
-
ANALYTICS COMPOSABLE
|
|
3733
|
-
useAnalytics() → { pageview, track, identify, visitorId, enableAutoCapture, flush }
|
|
3734
|
-
The currently installed composable does NOT return clearSession or disableAutoCapture.
|
|
3735
|
-
Use useDuctape().client.analytics.clearSession()/disableAutoCapture(), or verify a newer installed
|
|
3736
|
-
package export before generating those composable calls. See ductape_docs({ topic: "frontend-analytics" }).
|
|
3737
|
-
|
|
3738
|
-
composableOptions pattern (applies to all composables):
|
|
3739
|
-
enabled? boolean — false skips auto-fetch/subscribe
|
|
3740
|
-
onSuccess? (data) => void
|
|
3741
|
-
onError? (error: Error) => void
|
|
3742
|
-
Subscriptions also accept: onData, onSubscribe, onUnsubscribe
|
|
3743
|
-
|
|
3744
|
-
EXAMPLE
|
|
3745
|
-
<!-- UsersList.vue -->
|
|
3746
|
-
<script setup>
|
|
3747
|
-
import { useDatabaseQuery } from '@ductape/vue';
|
|
3748
|
-
const { data, isLoading, error } = useDatabaseQuery('users', { table: 'users', limit: 20 });
|
|
3749
|
-
</script>
|
|
3750
|
-
<template>
|
|
3751
|
-
<div v-if="isLoading">Loading...</div>
|
|
3752
|
-
<div v-else-if="error">{{ error.message }}</div>
|
|
3753
|
-
<ul v-else>
|
|
3754
|
-
<li v-for="user in data?.rows" :key="user.id">{{ user.name }}</li>
|
|
3755
|
-
</ul>
|
|
3756
|
-
</template>
|
|
3757
|
-
|
|
3758
|
-
<!-- FeatureTracker.vue -->
|
|
3759
|
-
<script setup>
|
|
3760
|
-
import { useFeatureSubscription } from '@ductape/vue';
|
|
3761
|
-
const props = defineProps(['executionId']);
|
|
3762
|
-
const { data: events, isSubscribed } = useFeatureSubscription(
|
|
3763
|
-
{ feature: 'onboard-user', executionId: props.executionId },
|
|
3764
|
-
);
|
|
3765
|
-
</script>
|
|
3766
|
-
<template>
|
|
3767
|
-
<p>{{ events?.[0]?.status ?? 'waiting' }}</p>
|
|
3768
|
-
</template>
|
|
3769
|
-
|
|
3770
|
-
DEPRECATED ALIASES
|
|
3771
|
-
useWorkflowExecute → useFeatureExecute
|
|
3772
|
-
useWorkflowStatus → useFeatureStatus
|
|
3773
|
-
useWorkflowSubscription → useFeatureSubscription
|
|
3774
|
-
useWorkflowSignal → useFeatureSignal
|
|
3775
|
-
useWorkflowCancel → useFeatureCancel
|
|
3776
|
-
`.trim(),
|
|
3777
|
-
};
|
|
3778
|
-
|
|
3779
|
-
const docsHandler = async (args: { topic: string }) => {
|
|
3780
|
-
const key = args.topic.toLowerCase().trim();
|
|
3781
|
-
const doc = DOCS[key];
|
|
3782
|
-
if (!doc) {
|
|
3783
|
-
const available = Object.keys(DOCS).join(', ');
|
|
3784
|
-
return {
|
|
3785
|
-
content: [{ type: 'text' as const, text: `Unknown topic "${args.topic}". Available topics: ${available}` }],
|
|
3786
|
-
};
|
|
3787
|
-
}
|
|
3788
|
-
return { content: [{ type: 'text' as const, text: doc }] };
|
|
3789
|
-
};
|
|
3790
|
-
|
|
3791
|
-
const cliInputSchema = z.object({
|
|
3792
|
-
command: z.string().describe(
|
|
3793
|
-
'The ductape CLI command to run, without the leading "ductape" word. ' +
|
|
3794
|
-
'Examples: "products list", "products create --name \\"My Product\\" --tag my-product", ' +
|
|
3795
|
-
'"apps list", "apps create -f app.json", "resources storage list", ' +
|
|
3796
|
-
'"events topics create -f topic.json", "events topics list --tag broker-tag", ' +
|
|
3797
|
-
'"cloud connections list", "link --product my-product --env dev".\n\n' +
|
|
3798
|
-
'Use this tool for administrative operations: creating or updating products, apps, ' +
|
|
3799
|
-
'resources (databases, storage, caches…), event broker topics, cloud connections, secrets, ' +
|
|
3800
|
-
'and for apply/migrate workflows.\n\n' +
|
|
3801
|
-
'Note: environments, app actions, quotas, fallbacks, and jobs are configured in the ' +
|
|
3802
|
-
'Workbench UI. Features also have no CLI creation command: define them in application code ' +
|
|
3803
|
-
'with features.define so application boot/runtime registration makes them available.\n\n' +
|
|
3804
|
-
'The CLI uses the user\'s local logged-in session (ductape login) — no key is required.',
|
|
3805
|
-
),
|
|
3806
|
-
});
|
|
3807
|
-
|
|
3808
|
-
async function loadMcpSdk(): Promise<{
|
|
3809
|
-
McpServer: new (info: { name: string; version: string }) => any;
|
|
3810
|
-
StdioServerTransport: new () => any;
|
|
3811
|
-
}> {
|
|
3812
|
-
try {
|
|
3813
|
-
const [{ McpServer }, { StdioServerTransport }] = await Promise.all([
|
|
3814
|
-
import('@modelcontextprotocol/sdk/server/mcp.js'),
|
|
3815
|
-
import('@modelcontextprotocol/sdk/server/stdio.js'),
|
|
3816
|
-
]);
|
|
3817
|
-
if (McpServer && StdioServerTransport) {
|
|
3818
|
-
return { McpServer, StdioServerTransport };
|
|
3819
|
-
}
|
|
3820
|
-
} catch {
|
|
3821
|
-
// fall through to v2 alpha package
|
|
3822
|
-
}
|
|
3823
|
-
|
|
3824
|
-
try {
|
|
3825
|
-
const sdk = await import('@modelcontextprotocol/server');
|
|
3826
|
-
if (sdk.McpServer && sdk.StdioServerTransport) {
|
|
3827
|
-
return { McpServer: sdk.McpServer, StdioServerTransport: sdk.StdioServerTransport };
|
|
3828
|
-
}
|
|
3829
|
-
} catch {
|
|
3830
|
-
// fall through
|
|
3831
|
-
}
|
|
3832
|
-
|
|
3833
|
-
console.error(
|
|
3834
|
-
'Failed to load MCP SDK. Install: npm install @modelcontextprotocol/sdk zod\n' +
|
|
3835
|
-
'Or v2 alpha: npm install @modelcontextprotocol/server zod @cfworker/json-schema',
|
|
3836
|
-
);
|
|
3837
|
-
process.exit(1);
|
|
3838
|
-
throw new Error('MCP SDK not available');
|
|
3839
|
-
}
|
|
3840
|
-
|
|
3841
|
-
function handleCliFlags(): boolean {
|
|
3842
|
-
const arg = process.argv[2];
|
|
3843
|
-
if (arg === '--version' || arg === '-v') {
|
|
3844
|
-
const { version } = createRequire(import.meta.url)('../package.json') as { version: string };
|
|
3845
|
-
process.stdout.write(version + '\n');
|
|
3846
|
-
return true;
|
|
3847
|
-
}
|
|
3848
|
-
if (arg === '--help' || arg === '-h') {
|
|
3849
|
-
process.stdout.write(
|
|
3850
|
-
'ductape-mcp — Ductape MCP server\n\n' +
|
|
3851
|
-
'Usage: ductape-mcp\n\n' +
|
|
3852
|
-
'The server communicates over stdio and is meant to be spawned by an MCP\n' +
|
|
3853
|
-
'client (Cursor, Claude Desktop, Claude Code). Run it directly only to\n' +
|
|
3854
|
-
'verify the binary is working.\n\n' +
|
|
3855
|
-
'Options:\n' +
|
|
3856
|
-
' --version, -v Print version and exit\n' +
|
|
3857
|
-
' --help, -h Print this message and exit\n\n' +
|
|
3858
|
-
'Environment:\n' +
|
|
3859
|
-
' DUCTAPE_PUBLISHABLE_KEY Your workspace publishable key. Set this in\n' +
|
|
3860
|
-
' the MCP client env config to avoid passing\n' +
|
|
3861
|
-
' publishable_key on every tool call.\n',
|
|
3862
|
-
);
|
|
3863
|
-
return true;
|
|
3864
|
-
}
|
|
3865
|
-
return false;
|
|
3866
|
-
}
|
|
3867
|
-
|
|
3868
|
-
async function main() {
|
|
3869
|
-
const { McpServer, StdioServerTransport } = await loadMcpSdk();
|
|
3870
|
-
const server = new McpServer({ name: 'ductape-mcp', version: '0.1.0' });
|
|
3871
|
-
const transport = new StdioServerTransport();
|
|
3872
|
-
|
|
3873
|
-
const cliHandler = async (args: { command: string }) => {
|
|
3874
|
-
const cli = checkCli();
|
|
3875
|
-
if (!cli.available) {
|
|
3876
|
-
return {
|
|
3877
|
-
content: [{
|
|
3878
|
-
type: 'text',
|
|
3879
|
-
text: [
|
|
3880
|
-
'The Ductape CLI is not installed or not in PATH.',
|
|
3881
|
-
'',
|
|
3882
|
-
'Install it with:',
|
|
3883
|
-
' npm install --global @ductape/cli',
|
|
3884
|
-
'',
|
|
3885
|
-
'Then log in:',
|
|
3886
|
-
' ductape login',
|
|
3887
|
-
'',
|
|
3888
|
-
'After logging in, retry this operation.',
|
|
3889
|
-
].join('\n'),
|
|
3890
|
-
}],
|
|
3891
|
-
isError: true,
|
|
3892
|
-
};
|
|
3893
|
-
}
|
|
3894
|
-
|
|
3895
|
-
const firstWord = args.command.trim().split(/\s+/)[0];
|
|
3896
|
-
const isAuthCommand = firstWord === 'login' || firstWord === 'logout';
|
|
3897
|
-
|
|
3898
|
-
if (!isAuthCommand) {
|
|
3899
|
-
// Cache successful authentication, but re-check a missing/expired session on every call.
|
|
3900
|
-
// The user may complete `ductape login` in another terminal while this MCP process remains
|
|
3901
|
-
// alive; caching "none" would otherwise make the MCP blind to the newly written session.
|
|
3902
|
-
if (authState === 'unknown' || authState === 'none') {
|
|
3903
|
-
checkLoginState();
|
|
3904
|
-
}
|
|
3905
|
-
if (authState === 'none') {
|
|
3906
|
-
const wsFlag = process.env.DUCTAPE_WORKSPACE ? ` --workspace "${process.env.DUCTAPE_WORKSPACE}"` : '';
|
|
3907
|
-
return {
|
|
3908
|
-
content: [{
|
|
3909
|
-
type: 'text',
|
|
3910
|
-
text: [
|
|
3911
|
-
'The Ductape CLI has no remotely valid authenticated session.',
|
|
3912
|
-
'',
|
|
3913
|
-
'Administrative reads and writes must remain on ductape_cli; do not fall back to ductape_execute.',
|
|
3914
|
-
'',
|
|
3915
|
-
'Ask the user to re-authenticate in a trusted local terminal:',
|
|
3916
|
-
` ductape login${wsFlag}`,
|
|
3917
|
-
'or:',
|
|
3918
|
-
` ductape login --browser google${wsFlag}`,
|
|
3919
|
-
` ductape login --browser github${wsFlag}`,
|
|
3920
|
-
'',
|
|
3921
|
-
'Never ask the user to paste their password, OAuth callback token, or stored CLI credential into the agent.',
|
|
3922
|
-
'After login succeeds, retry the original ductape_cli command.',
|
|
3923
|
-
].join('\n'),
|
|
3924
|
-
}],
|
|
3925
|
-
isError: true,
|
|
3926
|
-
};
|
|
3927
|
-
}
|
|
3928
|
-
// Sync to the configured workspace once per process (best-effort)
|
|
3929
|
-
if (!workspaceSynced) {
|
|
3930
|
-
syncWorkspace();
|
|
3931
|
-
}
|
|
3932
|
-
}
|
|
3933
|
-
|
|
3934
|
-
const result = runCli(args.command);
|
|
3935
|
-
|
|
3936
|
-
// Update cached state after auth commands
|
|
3937
|
-
if (firstWord === 'login' && result.success) {
|
|
3938
|
-
authState = 'ok';
|
|
3939
|
-
workspaceSynced = false; // re-sync workspace after fresh login
|
|
3940
|
-
}
|
|
3941
|
-
if (firstWord === 'logout' && result.success) {
|
|
3942
|
-
authState = 'none';
|
|
3943
|
-
workspaceSynced = false;
|
|
3944
|
-
}
|
|
3945
|
-
|
|
3946
|
-
return {
|
|
3947
|
-
content: [{ type: 'text', text: result.output || '(no output)' }],
|
|
3948
|
-
...(result.success ? {} : { isError: true }),
|
|
3949
|
-
};
|
|
3950
|
-
};
|
|
3951
|
-
|
|
3952
|
-
const executeHandler = async (args: { publishable_key?: string; module: SDKModule; method: string; params: unknown[] }) => {
|
|
3953
|
-
try {
|
|
3954
|
-
const runtimeMutationMethods: Partial<Record<SDKModule, Set<string>>> = {
|
|
3955
|
-
databases: new Set(['insert', 'update', 'delete', 'upsert']),
|
|
3956
|
-
graph: new Set(['insert', 'update', 'delete']),
|
|
3957
|
-
vector: new Set(['insert', 'upsert', 'upsertOne', 'delete']),
|
|
3958
|
-
sessions: new Set(['revoke']),
|
|
3959
|
-
};
|
|
3960
|
-
const isRuntimeDataMutation = runtimeMutationMethods[args.module]?.has(args.method) === true;
|
|
3961
|
-
const isAdministrativeMutation =
|
|
3962
|
-
(/^migration\./.test(args.method) ||
|
|
3963
|
-
/^schema\.(create|drop|add|remove|update)/.test(args.method) ||
|
|
3964
|
-
/(^|\.)(create|update|delete|configure|add|remove|revoke)$/.test(args.method)) &&
|
|
3965
|
-
!isRuntimeDataMutation;
|
|
3966
|
-
if (isAdministrativeMutation) {
|
|
3967
|
-
throw new Error(
|
|
3968
|
-
`Administrative operation "${args.module}.${args.method}" is blocked in ductape_execute. ` +
|
|
3969
|
-
'Use ductape_cli when that resource is supported by the CLI; otherwise configure it in Workbench. ' +
|
|
3970
|
-
'The runtime proxy uses a publishable key and cannot administer platform assets.',
|
|
3971
|
-
);
|
|
3972
|
-
}
|
|
3973
|
-
const key = args.publishable_key || process.env.DUCTAPE_PUBLISHABLE_KEY;
|
|
3974
|
-
if (!key) {
|
|
3975
|
-
throw new Error(
|
|
3976
|
-
'Runtime authentication is missing. Set DUCTAPE_PUBLISHABLE_KEY in the MCP server environment ' +
|
|
3977
|
-
'or pass publishable_key to ductape_execute. The MCP server never accepts access keys.',
|
|
3978
|
-
);
|
|
3979
|
-
}
|
|
3980
|
-
// The TS SDK uses ductape.events.* for broker operations; the backend proxy uses messageBrokers.
|
|
3981
|
-
const proxyModule: SDKModule = args.module === 'events' ? 'messageBrokers' : args.module;
|
|
3982
|
-
|
|
3983
|
-
// Normalize events.dispatch params: event must be fully-qualified "broker:topic".
|
|
3984
|
-
// If an agent passes both broker and event="broker:topic", strip broker and de-duplicate.
|
|
3985
|
-
let params = args.params;
|
|
3986
|
-
if ((args.module === 'events' || args.module === 'messageBrokers') && args.method === 'dispatch') {
|
|
3987
|
-
params = params.map((p) => {
|
|
3988
|
-
if (p && typeof p === 'object' && !Array.isArray(p)) {
|
|
3989
|
-
const obj = p as Record<string, unknown>;
|
|
3990
|
-
const broker = typeof obj['broker'] === 'string' ? obj['broker'] : '';
|
|
3991
|
-
const event = typeof obj['event'] === 'string' ? obj['event'] : '';
|
|
3992
|
-
if (broker && event) {
|
|
3993
|
-
// Strip broker prefix if event is already "broker:..." to avoid "broker:broker:topic"
|
|
3994
|
-
const normalized = event.startsWith(broker + ':') ? event : `${broker}:${event}`;
|
|
3995
|
-
const { broker: _removed, ...rest } = obj;
|
|
3996
|
-
return { ...rest, event: normalized };
|
|
3997
|
-
}
|
|
3998
|
-
}
|
|
3999
|
-
return p;
|
|
4000
|
-
});
|
|
4001
|
-
}
|
|
4002
|
-
|
|
4003
|
-
let result: unknown;
|
|
4004
|
-
try {
|
|
4005
|
-
result = await executeViaProxy(key, proxyModule, args.method, params);
|
|
4006
|
-
} catch (error) {
|
|
4007
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
4008
|
-
if (/authentication failed|unauthorized|invalid.*key/i.test(message)) {
|
|
4009
|
-
throw new Error(
|
|
4010
|
-
'The DUCTAPE_PUBLISHABLE_KEY was rejected by the runtime proxy. ' +
|
|
4011
|
-
'Use a publishable key for the same workspace/product. The MCP server never accepts or forwards access keys.',
|
|
4012
|
-
);
|
|
4013
|
-
}
|
|
4014
|
-
throw error;
|
|
4015
|
-
}
|
|
4016
|
-
return { content: [{ type: 'text', text: JSON.stringify(result ?? null, null, 2) }] };
|
|
4017
|
-
} catch (err) {
|
|
4018
|
-
let message = err instanceof Error ? err.message : String(err);
|
|
4019
|
-
const isAuthFailed = /authentication failed/i.test(message);
|
|
4020
|
-
const isDbWrite = args.module === 'databases' && /^(insert|update|delete|upsert)$/.test(args.method);
|
|
4021
|
-
if (isAuthFailed && isDbWrite) {
|
|
4022
|
-
message += '\n\nThe publishable key does not have write access for this database operation. ' +
|
|
4023
|
-
'Enable it in Workbench → Tokens → Publishable Key. ' +
|
|
4024
|
-
'If write access cannot be granted to the publishable key, perform this operation server-side using a full Ductape SDK instance initialized with an access key.';
|
|
4025
|
-
}
|
|
4026
|
-
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
4027
|
-
}
|
|
4028
|
-
};
|
|
4029
|
-
|
|
4030
|
-
const payloadGenerateHandler = async (
|
|
4031
|
-
args: z.infer<typeof payloadGenerateInputSchema>,
|
|
4032
|
-
) => {
|
|
4033
|
-
try {
|
|
4034
|
-
const key = args.publishable_key || process.env.DUCTAPE_PUBLISHABLE_KEY;
|
|
4035
|
-
if (!key) {
|
|
4036
|
-
throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
|
|
4037
|
-
}
|
|
4038
|
-
const includeSession = args.include_session ?? args.execution_context !== 'system';
|
|
4039
|
-
const result = addSessionAwarenessMetadata(
|
|
4040
|
-
await generateExecutablePayload({ ...args, include_session: includeSession, publishable_key: key }),
|
|
4041
|
-
args,
|
|
4042
|
-
);
|
|
4043
|
-
|
|
4044
|
-
let text = JSON.stringify(result ?? null, null, 2);
|
|
4045
|
-
|
|
4046
|
-
if (args.operation_family === 'database') {
|
|
4047
|
-
const meta = (result as any)?.meta ?? {};
|
|
4048
|
-
const fields = meta.schema_context?.database?.fields ?? {};
|
|
4049
|
-
const warnings: string[] = Array.isArray(meta.schema_warnings) ? meta.schema_warnings : [];
|
|
4050
|
-
const noSchema = Object.keys(fields).length === 0 || warnings.some((w: string) => w.includes('No table schema'));
|
|
4051
|
-
if (noSchema) {
|
|
4052
|
-
const dbTag = (args.targets as any)?.database_tag ?? '<database_tag>';
|
|
4053
|
-
const envSlug = (args as any).env_slug ?? '<env_slug>';
|
|
4054
|
-
text += `\n\n--- SCHEMA NOT FOUND ---\nNo table schema is available for database "${dbTag}" in environment "${envSlug}".\nTell the user:\n Run: ductape db schema push --db ${dbTag} --env ${envSlug}\n This reads the live schema from the database and syncs it to the Ductape server so the AI can provide accurate field guidance.\n Once complete, retry the operation.\n------------------------`;
|
|
4055
|
-
}
|
|
4056
|
-
}
|
|
4057
|
-
|
|
4058
|
-
return { content: [{ type: 'text', text }] };
|
|
4059
|
-
} catch (err) {
|
|
4060
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4061
|
-
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
4062
|
-
}
|
|
4063
|
-
};
|
|
4064
|
-
|
|
4065
|
-
const snippetGenerateHandler = async (
|
|
4066
|
-
args: z.infer<typeof snippetGenerateInputSchema>,
|
|
4067
|
-
) => {
|
|
4068
|
-
try {
|
|
4069
|
-
const key = args.publishable_key || process.env.DUCTAPE_PUBLISHABLE_KEY;
|
|
4070
|
-
if (!key) {
|
|
4071
|
-
throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
|
|
4072
|
-
}
|
|
4073
|
-
ensureSupportedSnippetOperation(args.operation_family, args.method);
|
|
4074
|
-
const includeSession = args.include_session ?? args.execution_context !== 'system';
|
|
4075
|
-
const generated = addSessionAwarenessMetadata(
|
|
4076
|
-
await generateExecutablePayload({ ...args, include_session: includeSession, publishable_key: key }),
|
|
4077
|
-
args,
|
|
4078
|
-
);
|
|
4079
|
-
const payload = (generated as any)?.payload ?? {};
|
|
4080
|
-
const snippet = buildSnippet(args.language, payload, args.operation_family, args.method);
|
|
4081
|
-
return {
|
|
4082
|
-
content: [
|
|
4083
|
-
{
|
|
4084
|
-
type: 'text',
|
|
4085
|
-
text: JSON.stringify(
|
|
4086
|
-
{
|
|
4087
|
-
payload: generated,
|
|
4088
|
-
snippet,
|
|
4089
|
-
},
|
|
4090
|
-
null,
|
|
4091
|
-
2,
|
|
4092
|
-
),
|
|
4093
|
-
},
|
|
4094
|
-
],
|
|
4095
|
-
};
|
|
4096
|
-
} catch (err) {
|
|
4097
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4098
|
-
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
4099
|
-
}
|
|
4100
|
-
};
|
|
4101
|
-
|
|
4102
|
-
const schemaHandler = async (args: { module?: 'app' | 'product' }) => {
|
|
4103
|
-
try {
|
|
4104
|
-
const data = await getAssetSchemas(args.module);
|
|
4105
|
-
return { content: [{ type: 'text', text: JSON.stringify(data ?? null, null, 2) }] };
|
|
4106
|
-
} catch (err) {
|
|
4107
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4108
|
-
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
4109
|
-
}
|
|
4110
|
-
};
|
|
4111
|
-
|
|
4112
|
-
if (typeof server.registerTool === 'function') {
|
|
4113
|
-
server.registerTool(
|
|
4114
|
-
'ductape_execute',
|
|
4115
|
-
{
|
|
4116
|
-
title: 'Ductape SDK Execute',
|
|
4117
|
-
description:
|
|
4118
|
-
'Execute a Ductape SDK RUNTIME operation via the backend proxy (publishable key).\n\n' +
|
|
4119
|
-
'ONLY use this tool for runtime operations: run, dispatch, execute, start, send, produce, query, insert, update, delete.\n' +
|
|
4120
|
-
'Do NOT use this for creating or updating any asset (product, app, action, environment, database, storage, etc.) — ' +
|
|
4121
|
-
'those require an access key. Use ductape_cli for all creation and update operations.\n\n' +
|
|
4122
|
-
'Two-step rule for runtime operations:\n' +
|
|
4123
|
-
' 1. Call ductape_generate_payload first to get the canonical payload template.\n' +
|
|
4124
|
-
' This reveals the exact "input" field keys — they are product/env/operation-specific.\n' +
|
|
4125
|
-
' 2. Fill in the values from the template, then call ductape_execute.',
|
|
4126
|
-
inputSchema: executeInputSchema,
|
|
4127
|
-
},
|
|
4128
|
-
executeHandler,
|
|
4129
|
-
);
|
|
4130
|
-
server.registerTool(
|
|
4131
|
-
'ductape_generate_payload',
|
|
4132
|
-
{
|
|
4133
|
-
title: 'Ductape Payload Generator',
|
|
4134
|
-
description:
|
|
4135
|
-
'Generate the canonical input payload template for a runtime SDK operation.\n\n' +
|
|
4136
|
-
'CALL THIS BEFORE ductape_execute when you need to run, dispatch, execute, start, send, produce, ' +
|
|
4137
|
-
'or otherwise trigger any Ductape operation that takes an "input" field.\n\n' +
|
|
4138
|
-
'The "input" field shape is defined by how each action/feature/session/quota/etc. was configured ' +
|
|
4139
|
-
'in the product — it cannot be inferred from the SDK schema alone. This tool returns the exact ' +
|
|
4140
|
-
'field names, types, and placeholder values for that specific operation in that specific environment.\n\n' +
|
|
4141
|
-
'Returns: { payload: { product, env, input: { fieldName: placeholder, ... }, session?, cache? }, meta: { ... } }',
|
|
4142
|
-
inputSchema: payloadGenerateInputSchema,
|
|
4143
|
-
},
|
|
4144
|
-
payloadGenerateHandler,
|
|
4145
|
-
);
|
|
4146
|
-
server.registerTool(
|
|
4147
|
-
'ductape_generate_snippet',
|
|
4148
|
-
{
|
|
4149
|
-
title: 'Ductape Snippet Generator',
|
|
4150
|
-
description: 'Generate canonical payload and ready TypeScript/Python snippet for engineers',
|
|
4151
|
-
inputSchema: snippetGenerateInputSchema,
|
|
4152
|
-
},
|
|
4153
|
-
snippetGenerateHandler,
|
|
4154
|
-
);
|
|
4155
|
-
server.registerTool(
|
|
4156
|
-
'ductape_schema',
|
|
4157
|
-
{
|
|
4158
|
-
title: 'Ductape Asset Schema',
|
|
4159
|
-
description:
|
|
4160
|
-
'Returns the full field manifest for Ductape asset creation/update methods, ' +
|
|
4161
|
-
'derived live from the SDK Joi validators. Includes field types, required flags, ' +
|
|
4162
|
-
'enum values, nested structures, and all enum constants. ' +
|
|
4163
|
-
'Pass module="app" or module="product" to scope the result.\n\n' +
|
|
4164
|
-
'ALWAYS call this before constructing a file for "resources <type> create" or any cloud ' +
|
|
4165
|
-
'import/provision operation — field shapes are not guessable from context.\n\n' +
|
|
4166
|
-
'Conditional fields: some fields are returned as oneOf (an array of variant shapes). ' +
|
|
4167
|
-
'For example, storage.create envs[].config is oneOf [awsConfig, gcpConfig, azureConfig] — ' +
|
|
4168
|
-
'pick the variant whose fields match the envs[].type value (aws/gcp/azure). ' +
|
|
4169
|
-
'Fields inside oneOf variants are context-dependent and should all be treated as optional ' +
|
|
4170
|
-
'unless the chosen variant explicitly marks them required.',
|
|
4171
|
-
inputSchema: schemaInputSchema,
|
|
4172
|
-
},
|
|
4173
|
-
schemaHandler,
|
|
4174
|
-
);
|
|
4175
|
-
server.registerTool(
|
|
4176
|
-
'ductape_docs',
|
|
4177
|
-
{
|
|
4178
|
-
title: 'Ductape Feature Docs',
|
|
4179
|
-
description:
|
|
4180
|
-
'Look up detailed documentation for a specific Ductape SDK feature.\n\n' +
|
|
4181
|
-
'Call this when you need guidance on how a feature works before using it — ' +
|
|
4182
|
-
'especially for features that require configuration decisions (tier, isolation level, ' +
|
|
4183
|
-
'index strategy, operation types) that should be confirmed with the user first.\n\n' +
|
|
4184
|
-
'Available topics: transactions, presave, triggers, aggregations, migrations, indexes, performance, actions, ' +
|
|
4185
|
-
'graphs, storage, cloud, vector, warehouse, secrets, apps, products, sessions, caches, ' +
|
|
4186
|
-
'notifications, resilience, features, events, logs, frontend, frontend-analytics, client, react, vue',
|
|
4187
|
-
inputSchema: docsInputSchema,
|
|
4188
|
-
},
|
|
4189
|
-
docsHandler,
|
|
4190
|
-
);
|
|
4191
|
-
server.registerTool(
|
|
4192
|
-
'ductape_cli',
|
|
4193
|
-
{
|
|
4194
|
-
title: 'Ductape CLI',
|
|
4195
|
-
description:
|
|
4196
|
-
'Run a Ductape CLI command for administrative operations.\n\n' +
|
|
4197
|
-
'USE THIS TOOL for any operation that creates or modifies platform configuration:\n' +
|
|
4198
|
-
' - Creating or updating products (products create/update) and apps (apps create/update)\n' +
|
|
4199
|
-
' - Importing an app from a Postman v2.1 or OpenAPI 3.0 file: "apps import <file> -t postman|openapi"\n' +
|
|
4200
|
-
' - Managing resources via "resources <type> <verb>" (databases, storage, caches…)\n' +
|
|
4201
|
-
' ALWAYS call ductape_schema(module="product") first to get the exact field shape before\n' +
|
|
4202
|
-
' constructing a file. Storage envs[].config is conditional on envs[].type (aws/gcp/azure)\n' +
|
|
4203
|
-
' and returned as oneOf — pick the variant matching the type.\n' +
|
|
4204
|
-
' REQUIRED: before building any resource file, run\n' +
|
|
4205
|
-
' ductape_cli("products environments list <product_tag> --json")\n' +
|
|
4206
|
-
' and include an envs entry for EVERY environment returned. Omitting any env causes a\n' +
|
|
4207
|
-
' validation error. If the user has not supplied connection details for every env, ask\n' +
|
|
4208
|
-
' them explicitly. Confirm per-env before reusing the same resource across environments.\n' +
|
|
4209
|
-
' - Three distinct storage registration flows — choose the right one:\n' +
|
|
4210
|
-
' 1. Manual credentials (bucket already exists, you supply keys directly):\n' +
|
|
4211
|
-
' resources storage create -f storage.json\n' +
|
|
4212
|
-
' File shape: { name, tag, envs: [{ slug, type: "aws"|"gcp"|"azure", config: { bucketName, ...creds } }] }\n' +
|
|
4213
|
-
' For cloud-linked envs set config.cloud to the connection tag and omit raw credentials.\n' +
|
|
4214
|
-
' 2. Import existing cloud buckets via cloud connections (no provisioning):\n' +
|
|
4215
|
-
' cloud resources import-persist-all -f import-all.json --json\n' +
|
|
4216
|
-
' File is a JSON ARRAY — one entry per env, all with the same product and component tag.\n' +
|
|
4217
|
-
' Each entry: cloud (connection tag), service (e.g. "gcs","s3"), type ("storage"),\n' +
|
|
4218
|
-
' product, component (new tag), env, resource (bucket name/identifier).\n' +
|
|
4219
|
-
' Example (two envs): [{"cloud":"gcp-snd","service":"gcs","type":"storage",\n' +
|
|
4220
|
-
' "product":"my-product","component":"assets","env":"snd","resource":"snd-bucket"},\n' +
|
|
4221
|
-
' {"cloud":"gcp-prd","service":"gcs","type":"storage","product":"my-product",\n' +
|
|
4222
|
-
' "component":"assets","env":"prd","resource":"prd-bucket"}].\n' +
|
|
4223
|
-
' All envs are imported in parallel, drafts merged, then the asset is created once.\n' +
|
|
4224
|
-
' Use import-persist (single-env form) only when the product has exactly one environment.\n' +
|
|
4225
|
-
' If import-persist-all fails, the error reports all create/update failures — do not retry blindly.\n' +
|
|
4226
|
-
' 3. Provision brand-new buckets AND register them (see provision-persist-all docs below).\n' +
|
|
4227
|
-
' - Managing cloud connections and cloud-linked resources\n' +
|
|
4228
|
-
' - Discovering cloud tiers: "cloud tiers --provider <provider> --type <database|storage> [--db-type <type>] --json"\n' +
|
|
4229
|
-
' IMPORTANT: `cloud tiers` has no `list` verb. Use only a tier returned for the selected provider/type.\n' +
|
|
4230
|
-
' - Provisioning and persisting cloud resources:\n' +
|
|
4231
|
-
' For a product with multiple environments always use provision-persist-all (not provision-persist):\n' +
|
|
4232
|
-
' cloud resources provision-persist-all -f file.json --json\n' +
|
|
4233
|
-
' File is a JSON ARRAY — one entry per env, all with the same product and component tag.\n' +
|
|
4234
|
-
' Each entry requires: cloud (connection tag), service (provider service), type (Ductape component type),\n' +
|
|
4235
|
-
' product (product tag), component (new component tag), env (existing environment slug).\n' +
|
|
4236
|
-
' Provider parameters such as region, location, bucketName, dbName, tier, and waitForReady\n' +
|
|
4237
|
-
' may be supplied per entry in the file.\n' +
|
|
4238
|
-
' Example (two envs): [{"cloud":"gcp-snd","service":"gcs","type":"storage",\n' +
|
|
4239
|
-
' "product":"product-tag","component":"assets","env":"snd","location":"us-central1"},\n' +
|
|
4240
|
-
' {"cloud":"gcp-prd","service":"gcs","type":"storage","product":"product-tag",\n' +
|
|
4241
|
-
' "component":"assets","env":"prd","location":"us-central1"}].\n' +
|
|
4242
|
-
' Use provision-persist (single-env) only when the product has exactly one environment.\n' +
|
|
4243
|
-
' Use provision alone when you only need the provider resource without registering an asset.\n' +
|
|
4244
|
-
' Tier discovery does not by itself prove that a connection supports provisioning that service:\n' +
|
|
4245
|
-
' if provisioning reports an unsupported provider/service pair, stop and report it.\n' +
|
|
4246
|
-
' Never infer an environment, provider service, region, tier, or cost. List/verify each before provisioning.\n' +
|
|
4247
|
-
' - Known provisioning limitations: MongoDB Atlas (M0 free tier) and Neo4j Aura cannot be provisioned via\n' +
|
|
4248
|
-
' Ductape — they are import-only. Only AWS, GCP, and Azure managed services support provision-persist.\n' +
|
|
4249
|
-
' Supported service/component pairs include: rds→database, aurora→database, gcs→storage, s3→storage,\n' +
|
|
4250
|
-
' azure-blob→storage, cloud-sql→database, neptune→graph, opensearch→vector. Attempting an unsupported pair\n' +
|
|
4251
|
-
' will return an error; do not retry with a different tier — report the limitation to the user.\n' +
|
|
4252
|
-
' - Atlas (MongoDB Atlas) import flow — service identifier is "atlas-cluster" (required, not optional):\n' +
|
|
4253
|
-
' IMPORTANT: dbName is mandatory for every MongoDB env — see "MONGODB CLOUD CONNECTION RULE" in the\n' +
|
|
4254
|
-
' databases module above. The SDK enforces this at create time; omitting it throws a validation error.\n' +
|
|
4255
|
-
' Step 1 — discover the cluster name:\n' +
|
|
4256
|
-
' ductape_cli("cloud resources list -f /tmp/atlas-list.json --json")\n' +
|
|
4257
|
-
' File: {"cloud": "<atlas-connection-tag>", "service": "atlas-cluster"}\n' +
|
|
4258
|
-
' Returns a list of clusters; note the "name" field (this is your resource identifier).\n' +
|
|
4259
|
-
' Step 2 — check if the database component already exists:\n' +
|
|
4260
|
-
' ductape_cli("resources databases list <product_tag> --json")\n' +
|
|
4261
|
-
' If a component already uses the same Atlas cluster, do NOT re-import — instead update it:\n' +
|
|
4262
|
-
' use ductape_cli resource update when supported; otherwise update it in Workbench\n' +
|
|
4263
|
-
' Add or change the dbName in the env\'s connection_url to switch databases on the same cluster.\n' +
|
|
4264
|
-
' Step 3 — import (only if no existing component uses this cluster):\n' +
|
|
4265
|
-
' Use import-persist-all with one entry per product env. Required fields per entry:\n' +
|
|
4266
|
-
' cloud (connection tag), service: "atlas-cluster", type: "databases",\n' +
|
|
4267
|
-
' product, component (new tag), env, resource (cluster name from Step 1),\n' +
|
|
4268
|
-
' dbName (the MongoDB database name to connect to — required for Atlas).\n' +
|
|
4269
|
-
' Example: [{"cloud":"atlas-tag","service":"atlas-cluster","type":"databases",\n' +
|
|
4270
|
-
' "product":"my-product","component":"core-db","env":"snd","resource":"Cluster0","dbName":"myapp_snd"},\n' +
|
|
4271
|
-
' {"cloud":"atlas-tag","service":"atlas-cluster","type":"databases",\n' +
|
|
4272
|
-
' "product":"my-product","component":"core-db","env":"prd","resource":"Cluster0","dbName":"myapp_prd"}]\n' +
|
|
4273
|
-
' - Message broker / event broker import:\n' +
|
|
4274
|
-
' CLI accepts these aliases for the messageBrokers module: events, event, broker, brokers, message-brokers.\n' +
|
|
4275
|
-
' List existing brokers: ductape_cli("resources events list --json")\n' +
|
|
4276
|
-
' GCP Pub/Sub service identifier is "pubsub". AWS SQS is "sqs". Azure Service Bus is "servicebus".\n' +
|
|
4277
|
-
' Message brokers are import-only (no provision-persist). Import flow is the same as storage.\n' +
|
|
4278
|
-
' type field = "messageBrokers" (not "messagebrokers" or "events").\n' +
|
|
4279
|
-
' After importing, create topics first with ductape_cli("events topics create -f topic.json") — SQS requires explicit topic creation with queueUrls. For other providers, topics auto-register on first produce but should still be created explicitly before any consumer subscribes.\n' +
|
|
4280
|
-
' - Listing workspaces, products, focused product components, secrets\n' +
|
|
4281
|
-
' Prefer "products components list --tag <tag> --json" for compact inventory; use\n' +
|
|
4282
|
-
' "products components get --tag <tag> --type notifications|events --json" for focused detail.\n' +
|
|
4283
|
-
' - Managing notification components and message templates through "resources notifications" and "notifications messages"\n' +
|
|
4284
|
-
' - Linking a project folder: "link --product <tag> --env <slug>"\n' +
|
|
4285
|
-
' - Syncing sessions/notifications/events: "apply" or "apply sessions" etc.\n' +
|
|
4286
|
-
' - Running database migrations: "db migrate", "db schema generate"\n\n' +
|
|
4287
|
-
'NOTE: Environments, app actions, auths, quotas, fallbacks, and jobs are configured ' +
|
|
4288
|
-
'in the Workbench UI. Features have no CLI creation command because definitions are ' +
|
|
4289
|
-
'code-first through features.define and registered by the application runtime.\n\n' +
|
|
4290
|
-
'DO NOT use ductape_execute for admin operations — it uses a publishable key which only ' +
|
|
4291
|
-
'covers runtime operations. Administrative operations will fail with "Authentication failed".\n\n' +
|
|
4292
|
-
'The CLI uses the user\'s local logged-in session (ductape login). ' +
|
|
4293
|
-
'If the CLI is not installed, this tool will return install instructions automatically.',
|
|
4294
|
-
inputSchema: cliInputSchema,
|
|
4295
|
-
},
|
|
4296
|
-
cliHandler,
|
|
4297
|
-
);
|
|
4298
|
-
} else if (typeof server.tool === 'function') {
|
|
4299
|
-
server.tool('ductape_execute', executeInputSchema.shape, executeHandler);
|
|
4300
|
-
server.tool('ductape_generate_payload', payloadGenerateInputSchema.shape, payloadGenerateHandler);
|
|
4301
|
-
server.tool('ductape_generate_snippet', snippetGenerateInputSchema.shape, snippetGenerateHandler);
|
|
4302
|
-
server.tool('ductape_schema', schemaInputSchema.shape, schemaHandler);
|
|
4303
|
-
server.tool('ductape_docs', docsInputSchema.shape, docsHandler);
|
|
4304
|
-
server.tool('ductape_cli', cliInputSchema.shape, cliHandler);
|
|
4305
|
-
} else {
|
|
4306
|
-
console.error('MCP server does not expose .registerTool() or .tool()');
|
|
4307
|
-
process.exit(1);
|
|
4308
|
-
}
|
|
4309
|
-
|
|
4310
|
-
await server.connect(transport);
|
|
4311
|
-
}
|
|
4312
|
-
|
|
4313
|
-
if (!handleCliFlags()) {
|
|
4314
|
-
main().catch((err) => {
|
|
4315
|
-
console.error(err);
|
|
4316
|
-
process.exit(1);
|
|
4317
|
-
});
|
|
4318
|
-
}
|