@ductape/mcp 0.1.13 → 0.1.15
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/dist/index.js +1259 -4
- package/package.json +1 -1
- package/src/index.ts +1291 -4
package/dist/index.js
CHANGED
|
@@ -155,7 +155,9 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
155
155
|
webhooks.events.list [app_tag, webhook_tag]
|
|
156
156
|
|
|
157
157
|
━━━ MODULE: sessions ━━━
|
|
158
|
-
sessions.create [product_tag, data: { tag: string, name: string, description?: string, expiry: number, period: "seconds"|"minutes"|"hours"|"days", selector: string, schema: { fieldName: "string"|"number"|"boolean" } }]
|
|
158
|
+
sessions.create [product_tag, data: { tag: string, name: string, description?: string, expiry: number, period: "seconds"|"minutes"|"hours"|"days", selector: string, schema: { fieldName: { type: "string"|"number"|"boolean"|"object"|"array", required?: boolean, description?: string } } }]
|
|
159
|
+
← selector is the dot-path of the primary identifier field (e.g. "playerId"); it MUST exist in schema.
|
|
160
|
+
← schema declares the shape of data encrypted in the JWT; actual values are passed at runtime via sessions.start.
|
|
159
161
|
sessions.update [product_tag, session_tag, data: { name?: string, description?: string, expiry?: number, period?: "seconds"|"minutes"|"hours"|"days", selector?: string, schema?: object }]
|
|
160
162
|
sessions.fetch [product_tag, session_tag]
|
|
161
163
|
sessions.list [product_tag]
|
|
@@ -291,8 +293,15 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
291
293
|
messageBrokers.fetch [product_tag, broker_tag]
|
|
292
294
|
messageBrokers.list [product_tag]
|
|
293
295
|
messageBrokers.delete [product_tag, broker_tag]
|
|
294
|
-
messageBrokers.topics.create [product_tag, data: { tag: string, name: string, broker: string,
|
|
295
|
-
|
|
296
|
+
messageBrokers.topics.create [product_tag, data: { tag: string, name: string, broker: string,
|
|
297
|
+
description?: string, sample?: object, idempotent?: boolean,
|
|
298
|
+
queueUrls?: [{ env_slug: string, url: string }] // SQS only: per-env queue URL per topic
|
|
299
|
+
}]
|
|
300
|
+
← A broker can have unlimited topics. Add one per logical event type.
|
|
301
|
+
← For SQS: each topic needs queueUrls mapping env slug → the SQS queue URL for that topic+env.
|
|
302
|
+
← For Pub/Sub and Service Bus: topic name resolves from the tag or broker env config; no queueUrls needed.
|
|
303
|
+
messageBrokers.topics.update [product_tag, topic_tag, data: { name?: string, description?: string,
|
|
304
|
+
sample?: object, idempotent?: boolean, queueUrls?: [{ env_slug: string, url: string }] }]
|
|
296
305
|
messageBrokers.topics.fetch [product_tag, topic_tag]
|
|
297
306
|
messageBrokers.topics.list [product_tag, broker_tag]
|
|
298
307
|
messageBrokers.produce [{ product, env, event: "broker_tag:topic_tag", message: { key: value }, session?, cache? }] ← CALL ductape_generate_payload FIRST (operation_family="messaging", method="produce", targets={broker, event})
|
|
@@ -372,6 +381,15 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
372
381
|
databases.action.delete [action_tag]
|
|
373
382
|
databases.action.dispatch [{ product, env, database, action, input, schedule? }] ← CALL ductape_generate_payload FIRST (operation_family="database", method="dispatch", targets={database, action})
|
|
374
383
|
databases.dispatch [{ product, env, database, action, input, schedule? }] ← CALL ductape_generate_payload FIRST (operation_family="database", method="dispatch")
|
|
384
|
+
databases.beginTransaction [{ product, env, database, isolationLevel?: "READ_COMMITTED"|"REPEATABLE_READ"|"SERIALIZABLE" }]
|
|
385
|
+
→ returns a transaction object; pass it to insert/update/delete/upsert/query calls as the last argument.
|
|
386
|
+
Commit with: transaction.commit() Rollback with: transaction.rollback()
|
|
387
|
+
BEFORE using transactions: ask the user which database type and tier they are running.
|
|
388
|
+
Transaction support varies by database — call ductape_docs({ topic: "transactions" }) for the full matrix.
|
|
389
|
+
databases.transaction [{ product, env, database, isolationLevel? }, async (tx) => { … }]
|
|
390
|
+
→ managed transaction: commits automatically on success, rolls back on error.
|
|
391
|
+
Prefer this over beginTransaction when the callback is self-contained.
|
|
392
|
+
See ductape_docs({ topic: "transactions" }) for database/tier compatibility before proceeding.
|
|
375
393
|
|
|
376
394
|
━━━ MODULE: graph ━━━
|
|
377
395
|
graph.create [{ product, tag, name, description?, type: "neo4j"|"nebula"|"arangodb", envs: [{slug, connection_url, username?, password?}] }]
|
|
@@ -575,7 +593,7 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
575
593
|
caches.list [product_tag]
|
|
576
594
|
caches.delete [product_tag, cache_tag]
|
|
577
595
|
caches.get [{ key: string }]
|
|
578
|
-
caches.set [{ product, cache, key, value: string, componentTag?, componentType?, expiry?: Date }]
|
|
596
|
+
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 }]
|
|
579
597
|
caches.clear [{ key: string }]
|
|
580
598
|
caches.clearAll [{ product, cache, env? }]
|
|
581
599
|
caches.fetchValues [{ product, cache, env?, page?, limit?, expiryFilter?: "all"|"expiring"|"permanent"|"expired" }]
|
|
@@ -914,6 +932,1205 @@ function runCli(command) {
|
|
|
914
932
|
return { success: false, output: msg };
|
|
915
933
|
}
|
|
916
934
|
}
|
|
935
|
+
const docsInputSchema = z.object({
|
|
936
|
+
topic: z.string().describe('Feature topic to look up. Supported: ' +
|
|
937
|
+
'transactions, presave, triggers, aggregations, migrations, indexes, performance, actions, ' +
|
|
938
|
+
'graphs, storage, cloud, vector, warehouse, secrets, apps, products, sessions, caches, ' +
|
|
939
|
+
'notifications, resilience, features, events, logs'),
|
|
940
|
+
});
|
|
941
|
+
const DOCS = {
|
|
942
|
+
transactions: `
|
|
943
|
+
DUCTAPE DATABASE TRANSACTIONS
|
|
944
|
+
|
|
945
|
+
Ask the user which database type and tier they are using before advising on transaction support.
|
|
946
|
+
|
|
947
|
+
Support matrix:
|
|
948
|
+
PostgreSQL — full ACID; all isolation levels (READ_UNCOMMITTED, READ_COMMITTED,
|
|
949
|
+
REPEATABLE_READ, SERIALIZABLE). Always supported.
|
|
950
|
+
MySQL — full ACID; same isolation levels as PostgreSQL.
|
|
951
|
+
MongoDB — multi-document transactions require a replica set.
|
|
952
|
+
Atlas M10+ (dedicated): supported.
|
|
953
|
+
Atlas M0/M2/M5 (free/shared): NOT supported — all writes are
|
|
954
|
+
single-document atomic only. Confirm tier with user before proceeding.
|
|
955
|
+
Self-hosted: supported if replica set mode is enabled.
|
|
956
|
+
Cassandra — does NOT support ACID transactions. Lightweight transactions (LWT)
|
|
957
|
+
only for single-partition compare-and-set.
|
|
958
|
+
DynamoDB — transactional API supported (TransactWrite) but NOT via Ductape's
|
|
959
|
+
transaction abstraction; use single operations with condition expressions.
|
|
960
|
+
|
|
961
|
+
Usage patterns:
|
|
962
|
+
// Managed (preferred) — auto-commit on success, auto-rollback on error:
|
|
963
|
+
await ductape.database.transaction(
|
|
964
|
+
{ product, env, database, isolationLevel: 'READ_COMMITTED' },
|
|
965
|
+
async (tx) => {
|
|
966
|
+
await ductape.database.insert({ ..., transaction: tx });
|
|
967
|
+
await ductape.database.update({ ..., transaction: tx });
|
|
968
|
+
}
|
|
969
|
+
);
|
|
970
|
+
|
|
971
|
+
// Manual — commit/rollback yourself:
|
|
972
|
+
const tx = await ductape.database.beginTransaction({ product, env, database });
|
|
973
|
+
try {
|
|
974
|
+
await ductape.database.insert({ ..., transaction: tx });
|
|
975
|
+
await tx.commit();
|
|
976
|
+
} catch (e) {
|
|
977
|
+
await tx.rollback();
|
|
978
|
+
throw e;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
Via ductape_execute:
|
|
982
|
+
ductape_execute("databases.beginTransaction", [{ product, env, database, isolationLevel? }])
|
|
983
|
+
→ returns a transaction reference you can pass to subsequent insert/update/delete calls.
|
|
984
|
+
`.trim(),
|
|
985
|
+
presave: `
|
|
986
|
+
DUCTAPE PRE-SAVE HOOKS
|
|
987
|
+
|
|
988
|
+
Pre-save operations transform or validate data before it is written to the database.
|
|
989
|
+
Pass a preSave array to insert or update operations. Operations run in priority order.
|
|
990
|
+
|
|
991
|
+
Available operations (PreSaveOperationType):
|
|
992
|
+
encrypt — AES-256 encrypt field (use for PII, secrets)
|
|
993
|
+
hash — one-way hash (bcrypt/argon2 for passwords)
|
|
994
|
+
mask — mask sensitive data (e.g. **** **** **** 1234)
|
|
995
|
+
trim — strip leading/trailing whitespace
|
|
996
|
+
lowercase — convert string to lowercase
|
|
997
|
+
uppercase — convert string to uppercase
|
|
998
|
+
sanitize — strip HTML/script tags
|
|
999
|
+
validate — throw if field value fails a rule
|
|
1000
|
+
transform — apply a custom mapping function
|
|
1001
|
+
uuid — generate a UUID for the field
|
|
1002
|
+
slug — generate a URL slug from another field
|
|
1003
|
+
timestamp — set field to current timestamp (ISO 8601)
|
|
1004
|
+
round — round number to N decimal places
|
|
1005
|
+
clamp — clamp number to a min/max range
|
|
1006
|
+
truncate — truncate string to max length
|
|
1007
|
+
default — set a default value if null/undefined
|
|
1008
|
+
normalizePhone — normalise phone number format
|
|
1009
|
+
normalizeEmail — lowercase + trim email
|
|
1010
|
+
parseJson — parse JSON string to object
|
|
1011
|
+
stringifyJson — serialise object to JSON string
|
|
1012
|
+
compute — derive field value from other fields (runs before validate)
|
|
1013
|
+
|
|
1014
|
+
Execution order is fixed regardless of declaration order:
|
|
1015
|
+
default → compute → validate → trim → lowercase/uppercase/normalize* →
|
|
1016
|
+
sanitize → truncate → slug → round/clamp → parseJson/stringifyJson → uuid/timestamp → transform
|
|
1017
|
+
|
|
1018
|
+
Example:
|
|
1019
|
+
await ductape.database.insert({
|
|
1020
|
+
product, env, database: 'users', entity: 'users',
|
|
1021
|
+
data: { email: ' User@Example.com ', password: 'secret', username: 'My App User' },
|
|
1022
|
+
preSave: [
|
|
1023
|
+
{ field: 'email', operation: 'normalizeEmail' },
|
|
1024
|
+
{ field: 'password', operation: 'hash' },
|
|
1025
|
+
{ field: 'username', operation: 'slug', target: 'slug' },
|
|
1026
|
+
],
|
|
1027
|
+
});
|
|
1028
|
+
`.trim(),
|
|
1029
|
+
triggers: `
|
|
1030
|
+
DUCTAPE DATABASE TRIGGERS
|
|
1031
|
+
|
|
1032
|
+
Triggers fire automatically in response to database write events.
|
|
1033
|
+
Defined via the Workbench UI or databases.trigger.create (admin SDK).
|
|
1034
|
+
|
|
1035
|
+
Events (TriggerEvent):
|
|
1036
|
+
beforeInsert / afterInsert
|
|
1037
|
+
beforeUpdate / afterUpdate
|
|
1038
|
+
beforeDelete / afterDelete
|
|
1039
|
+
beforeWrite / afterWrite (any write)
|
|
1040
|
+
|
|
1041
|
+
Timing (TriggerTiming):
|
|
1042
|
+
sync — block the write until trigger completes (adds latency)
|
|
1043
|
+
async — fire and forget (default for notifications, cache, broker)
|
|
1044
|
+
queued — enqueue for background processing
|
|
1045
|
+
|
|
1046
|
+
Action types (TriggerActionType):
|
|
1047
|
+
database.* — insert/update/delete/query another collection
|
|
1048
|
+
storage.* — upload/delete/copy a file
|
|
1049
|
+
notification.* — email/SMS/push/callback
|
|
1050
|
+
broker.publish — publish to a message broker topic
|
|
1051
|
+
cache.* — set/invalidate/delete a cache key
|
|
1052
|
+
feature.* — execute or dispatch a Ductape feature
|
|
1053
|
+
action.execute — call an app action
|
|
1054
|
+
agent.run — run an AI agent
|
|
1055
|
+
quota.run / fallback.run / healthcheck.run
|
|
1056
|
+
vector.upsert / vector.delete
|
|
1057
|
+
session.revoke
|
|
1058
|
+
log.create
|
|
1059
|
+
custom.function / custom.http
|
|
1060
|
+
|
|
1061
|
+
All triggers support an optional condition (field comparisons) to gate execution.
|
|
1062
|
+
All triggers support retry config: { maxAttempts, delay, backoff: "fixed"|"exponential" }.
|
|
1063
|
+
|
|
1064
|
+
Triggers are admin-only configuration — they cannot be set at runtime via ductape_execute.
|
|
1065
|
+
Use ductape_cli with the Workbench or databases.trigger.create via the admin SDK.
|
|
1066
|
+
`.trim(),
|
|
1067
|
+
aggregations: `
|
|
1068
|
+
DUCTAPE DATABASE AGGREGATIONS
|
|
1069
|
+
|
|
1070
|
+
databases.aggregate [{ product, env, database, entity, aggregations, where?, groupBy? }]
|
|
1071
|
+
|
|
1072
|
+
aggregations array — each entry:
|
|
1073
|
+
{ type: "count"|"sum"|"avg"|"min"|"max", field?: string, alias: string }
|
|
1074
|
+
|
|
1075
|
+
Example — total revenue grouped by category:
|
|
1076
|
+
ductape_execute("databases.aggregate", [{
|
|
1077
|
+
product: "my-product",
|
|
1078
|
+
env: "prd",
|
|
1079
|
+
database: "sales-db",
|
|
1080
|
+
entity: "orders",
|
|
1081
|
+
aggregations: [
|
|
1082
|
+
{ type: "sum", field: "amount", alias: "totalRevenue" },
|
|
1083
|
+
{ type: "count", alias: "orderCount" },
|
|
1084
|
+
{ type: "avg", field: "amount", alias: "avgOrderValue" },
|
|
1085
|
+
],
|
|
1086
|
+
groupBy: ["category"],
|
|
1087
|
+
where: { status: { $eq: "completed" } },
|
|
1088
|
+
}])
|
|
1089
|
+
|
|
1090
|
+
Supported where operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $like, $ilike
|
|
1091
|
+
groupBy: array of field names to group by (omit for global aggregation)
|
|
1092
|
+
`.trim(),
|
|
1093
|
+
migrations: `
|
|
1094
|
+
DUCTAPE DATABASE MIGRATIONS
|
|
1095
|
+
|
|
1096
|
+
Migrations are versioned SQL/NoSQL schema change scripts managed per database component.
|
|
1097
|
+
|
|
1098
|
+
Create a migration:
|
|
1099
|
+
ductape_execute("databases.migration.create", [{
|
|
1100
|
+
product: "my-product",
|
|
1101
|
+
database: "core-db",
|
|
1102
|
+
data: {
|
|
1103
|
+
name: "add users table",
|
|
1104
|
+
tag: "001-add-users",
|
|
1105
|
+
value: {
|
|
1106
|
+
up: ["CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT NOT NULL UNIQUE)"],
|
|
1107
|
+
down: ["DROP TABLE users"],
|
|
1108
|
+
},
|
|
1109
|
+
},
|
|
1110
|
+
}])
|
|
1111
|
+
|
|
1112
|
+
Run migrations (applies all pending):
|
|
1113
|
+
ductape_execute("databases.migration.run", [migrations, { env: "prd" }])
|
|
1114
|
+
|
|
1115
|
+
Rollback:
|
|
1116
|
+
ductape_execute("databases.migration.rollback", [migrations, 1]) // roll back 1
|
|
1117
|
+
|
|
1118
|
+
Check status:
|
|
1119
|
+
ductape_execute("databases.migration.status", [migrations])
|
|
1120
|
+
|
|
1121
|
+
History:
|
|
1122
|
+
ductape_execute("databases.migration.history", [])
|
|
1123
|
+
|
|
1124
|
+
Via CLI:
|
|
1125
|
+
ductape_cli("db migrate") // run pending
|
|
1126
|
+
ductape_cli("db migrate rollback") // roll back last
|
|
1127
|
+
ductape_cli("db migrate rollback -n 3")
|
|
1128
|
+
|
|
1129
|
+
MongoDB: migrations run as raw Mongo shell commands in the up/down arrays.
|
|
1130
|
+
`.trim(),
|
|
1131
|
+
indexes: `
|
|
1132
|
+
DUCTAPE DATABASE INDEXES
|
|
1133
|
+
|
|
1134
|
+
Create an index:
|
|
1135
|
+
ductape_execute("databases.schema.createIndex", [
|
|
1136
|
+
"collection_name",
|
|
1137
|
+
["field1", "field2"], // simple array of field names
|
|
1138
|
+
{ unique: true, name: "idx_email_unique" } // options (optional)
|
|
1139
|
+
])
|
|
1140
|
+
|
|
1141
|
+
// Ordered index:
|
|
1142
|
+
ductape_execute("databases.schema.createIndex", [
|
|
1143
|
+
"orders",
|
|
1144
|
+
[{ field: "createdAt", order: "DESC" }, { field: "userId", order: "ASC" }],
|
|
1145
|
+
{ name: "idx_orders_date_user" }
|
|
1146
|
+
])
|
|
1147
|
+
|
|
1148
|
+
Drop an index:
|
|
1149
|
+
ductape_execute("databases.schema.dropIndex", ["collection_name", "index_name"])
|
|
1150
|
+
|
|
1151
|
+
List indexes on a collection:
|
|
1152
|
+
ductape_execute("databases.schema.indexes", ["collection_name"])
|
|
1153
|
+
|
|
1154
|
+
Performance guidance:
|
|
1155
|
+
- Index fields used in WHERE clauses, JOIN conditions, and ORDER BY.
|
|
1156
|
+
- Compound indexes: put high-cardinality fields first.
|
|
1157
|
+
- Unique indexes enforce uniqueness at the database level.
|
|
1158
|
+
- MongoDB: compound indexes support prefix queries; order matters.
|
|
1159
|
+
- PostgreSQL/MySQL: B-tree indexes by default; use partial indexes for sparse columns.
|
|
1160
|
+
- Avoid over-indexing writes-heavy collections — each index slows inserts/updates.
|
|
1161
|
+
|
|
1162
|
+
Before adding an index ask the user which fields they query/filter on and expected data volume.
|
|
1163
|
+
`.trim(),
|
|
1164
|
+
performance: `
|
|
1165
|
+
DUCTAPE DATABASE PERFORMANCE GUIDANCE
|
|
1166
|
+
|
|
1167
|
+
1. Queries
|
|
1168
|
+
- Always pass select: ["field1", "field2"] to avoid fetching unused columns.
|
|
1169
|
+
- Use limit + offset for pagination; add an index on the ORDER BY field.
|
|
1170
|
+
- Use count/sum/avg/aggregate instead of fetching all rows and computing in code.
|
|
1171
|
+
|
|
1172
|
+
2. Indexes (see ductape_docs({ topic: "indexes" }) for syntax)
|
|
1173
|
+
- Index every WHERE / ORDER BY field.
|
|
1174
|
+
- Compound index order: equality fields first, range fields last.
|
|
1175
|
+
- For MongoDB: covered queries (index holds all projected fields) skip the document read.
|
|
1176
|
+
|
|
1177
|
+
3. Connections
|
|
1178
|
+
- The SDK pools connections per product+env+database triple.
|
|
1179
|
+
- Call databases.connect once per process; the pool reuses the connection.
|
|
1180
|
+
- databases.disconnect releases the pool — call only on shutdown.
|
|
1181
|
+
|
|
1182
|
+
4. Transactions (see ductape_docs({ topic: "transactions" }))
|
|
1183
|
+
- Keep transactions short; long-lived transactions block row locks (PostgreSQL/MySQL).
|
|
1184
|
+
- For MongoDB on M10+, use sessions for multi-document writes — not for reads.
|
|
1185
|
+
|
|
1186
|
+
5. Caching
|
|
1187
|
+
- For read-heavy, rarely-changing data use caches.get before databases.query.
|
|
1188
|
+
- Invalidate cache keys in an afterWrite trigger (see ductape_docs({ topic: "triggers" })).
|
|
1189
|
+
`.trim(),
|
|
1190
|
+
actions: `
|
|
1191
|
+
DUCTAPE DATABASE ACTIONS
|
|
1192
|
+
|
|
1193
|
+
A database action is a saved query or mutation (SQL string or NoSQL command) stored
|
|
1194
|
+
on the Ductape product and executed by tag at runtime.
|
|
1195
|
+
|
|
1196
|
+
Create an action (admin):
|
|
1197
|
+
ductape_execute("databases.action.create", [{
|
|
1198
|
+
product: "my-product",
|
|
1199
|
+
database: "core-db",
|
|
1200
|
+
data: {
|
|
1201
|
+
tag: "get-active-users",
|
|
1202
|
+
name: "Get active users",
|
|
1203
|
+
description: "Returns all users with status=active",
|
|
1204
|
+
type: "sql", // "sql" or "nosql"
|
|
1205
|
+
query: "SELECT * FROM users WHERE status = :status",
|
|
1206
|
+
},
|
|
1207
|
+
}])
|
|
1208
|
+
|
|
1209
|
+
Dispatch an action at runtime:
|
|
1210
|
+
→ CALL ductape_generate_payload FIRST to get the canonical input shape.
|
|
1211
|
+
ductape_execute("databases.action.dispatch", [{
|
|
1212
|
+
product: "my-product",
|
|
1213
|
+
env: "prd",
|
|
1214
|
+
database: "core-db",
|
|
1215
|
+
action: "get-active-users",
|
|
1216
|
+
input: { status: "active" },
|
|
1217
|
+
}])
|
|
1218
|
+
|
|
1219
|
+
List actions for a database:
|
|
1220
|
+
ductape_execute("databases.action.list", ["database_tag"])
|
|
1221
|
+
|
|
1222
|
+
Fetch / update / delete:
|
|
1223
|
+
ductape_execute("databases.action.fetch", ["action_tag"])
|
|
1224
|
+
ductape_execute("databases.action.update", ["my-product", "action_tag", { query: "..." }])
|
|
1225
|
+
ductape_execute("databases.action.delete", ["action_tag"])
|
|
1226
|
+
|
|
1227
|
+
Actions are the preferred way to encapsulate complex or reused queries — they can be
|
|
1228
|
+
scheduled, dispatched with retries, and audited via logs.
|
|
1229
|
+
`.trim(),
|
|
1230
|
+
graphs: `
|
|
1231
|
+
DUCTAPE GRAPH DATABASES
|
|
1232
|
+
|
|
1233
|
+
Supported engines: neo4j | neptune | cosmos-gremlin | spanner-graph | arangodb | memgraph
|
|
1234
|
+
|
|
1235
|
+
Registration (admin — ductape_cli):
|
|
1236
|
+
ductape_cli("resources graphs create -f graph.json")
|
|
1237
|
+
File: { name, tag, type, envs: [{ slug, connection_url, username?, password?, graphName?, region? }] }
|
|
1238
|
+
Sensitive fields (connection_url, username, password) are auto-wrapped as $Secret{...} when
|
|
1239
|
+
a productTag is supplied — do NOT pre-wrap them yourself.
|
|
1240
|
+
|
|
1241
|
+
Node operations:
|
|
1242
|
+
graph.createNode [{ labels: string[], properties: { key: value } }, transaction?]
|
|
1243
|
+
graph.findNodes [{ labels?, where?, limit?, skip? }, transaction?]
|
|
1244
|
+
graph.findNodeById [id, transaction?]
|
|
1245
|
+
graph.updateNode [{ id, properties }, transaction?]
|
|
1246
|
+
graph.deleteNode [{ id, detach?: boolean }, transaction?]
|
|
1247
|
+
graph.mergeNode [{ labels, matchProps, setProps? }, transaction?]
|
|
1248
|
+
graph.addLabels / removeLabels / setLabels [{ id, labels }, transaction?]
|
|
1249
|
+
|
|
1250
|
+
Relationship operations:
|
|
1251
|
+
graph.createRelationship [{ fromId, toId, type, properties? }, transaction?]
|
|
1252
|
+
graph.findRelationships [{ type?, where?, limit? }, transaction?]
|
|
1253
|
+
graph.updateRelationship [{ id, properties }, transaction?]
|
|
1254
|
+
graph.deleteRelationship [{ id }, transaction?]
|
|
1255
|
+
graph.mergeRelationship [{ fromId, toId, type, matchProps?, setProps? }, transaction?]
|
|
1256
|
+
|
|
1257
|
+
Traversal:
|
|
1258
|
+
graph.traverse [{ startId, direction: "in"|"out"|"both", relationshipTypes?, maxDepth?, where? }]
|
|
1259
|
+
graph.shortestPath [{ fromId, toId, relationshipType?, maxDepth? }]
|
|
1260
|
+
graph.allPaths [{ fromId, toId, relationshipType?, maxDepth? }]
|
|
1261
|
+
graph.getNeighborhood [{ id, depth?, relationshipTypes? }]
|
|
1262
|
+
graph.findConnectedComponents [{ labels? }]
|
|
1263
|
+
|
|
1264
|
+
Raw query (provider-native Cypher/Gremlin):
|
|
1265
|
+
graph.query [cypher_query: string, params?: { key: value }, transaction?]
|
|
1266
|
+
|
|
1267
|
+
Full-text and vector search (where supported):
|
|
1268
|
+
graph.fullTextSearch [{ index, query, limit? }]
|
|
1269
|
+
graph.vectorSearch [{ index, vector: number[], topK? }]
|
|
1270
|
+
|
|
1271
|
+
Transactions:
|
|
1272
|
+
graph.beginTransaction [options?: { isolationLevel?, timeout? }] → transaction object
|
|
1273
|
+
graph.commitTransaction [transaction]
|
|
1274
|
+
graph.rollbackTransaction [transaction]
|
|
1275
|
+
Or use withTransaction (managed — auto-commit/rollback):
|
|
1276
|
+
graph.withTransaction [graphTag, env, callback, options?]
|
|
1277
|
+
Transaction support varies by engine — ask the user which engine they use before enabling.
|
|
1278
|
+
neo4j: full ACID; neptune: limited; arangodb: multi-document; memgraph: full ACID.
|
|
1279
|
+
|
|
1280
|
+
Schema management:
|
|
1281
|
+
graph.createNodeIndex [{ label, field, type: "btree"|"fulltext"|"vector" }]
|
|
1282
|
+
graph.createNodeConstraint [{ label, field, type: "unique"|"exists" }]
|
|
1283
|
+
graph.dropIndex / dropConstraint / listIndexes / listConstraints
|
|
1284
|
+
|
|
1285
|
+
Saved actions (parameterized queries stored on the product):
|
|
1286
|
+
graph.createAction [{ graphTag, tag, name, query, params? }, productTag?]
|
|
1287
|
+
graph.listActions [graphTag?, productTag?]
|
|
1288
|
+
graph.dispatch [data] ← call ductape_generate_payload FIRST
|
|
1289
|
+
|
|
1290
|
+
Supported index types: btree | fulltext | vector | range | point | text
|
|
1291
|
+
Supported constraint types: UNIQUE | EXISTS | NODE_KEY
|
|
1292
|
+
`.trim(),
|
|
1293
|
+
storage: `
|
|
1294
|
+
DUCTAPE FILE STORAGE
|
|
1295
|
+
|
|
1296
|
+
Supported providers: aws (S3) | gcp (GCS) | azure (Blob Storage)
|
|
1297
|
+
|
|
1298
|
+
Registration (admin — ductape_cli):
|
|
1299
|
+
ductape_cli("resources storage create -f storage.json")
|
|
1300
|
+
File: { name, tag, envs: [{ slug, type: "aws"|"gcp"|"azure", config: { ... } }] }
|
|
1301
|
+
For cloud-linked envs: set config.cloud to the connection tag; omit raw credentials.
|
|
1302
|
+
For all envs in the product must be covered — see ductape_docs({ topic: "cloud" }) for
|
|
1303
|
+
import-persist-all workflow when connecting existing buckets.
|
|
1304
|
+
|
|
1305
|
+
Operations (runtime):
|
|
1306
|
+
storage.upload [{ product, env, storage, fileName, buffer: string|Buffer, mimeType? }]
|
|
1307
|
+
storage.download [{ product, env, storage, fileName }] → { content, size, mimeType }
|
|
1308
|
+
storage.remove [{ product, env, storage, fileName }]
|
|
1309
|
+
storage.listFiles [{ product, env, storage, prefix?, limit?, continuationToken? }]
|
|
1310
|
+
storage.getSignedUrl [{ product, env, storage, fileName, expiresIn?: number, action?: "read"|"write" }]
|
|
1311
|
+
storage.stats [{ product, env, storage, prefix?, cache? }] → file counts by type
|
|
1312
|
+
storage.testConnection [{ product, env, storage }]
|
|
1313
|
+
|
|
1314
|
+
Background dispatch (fire-and-forget):
|
|
1315
|
+
storage.dispatch [{ product, env, storage, operation, input, schedule? }]
|
|
1316
|
+
→ CALL ductape_generate_payload FIRST (operation_family="storage", method="dispatch")
|
|
1317
|
+
→ Returns { jobId, status: "queued" } immediately; actual operation runs in background.
|
|
1318
|
+
→ schedule: { start_at?, cron?, every?, limit?, endDate?, tz? }
|
|
1319
|
+
|
|
1320
|
+
Caching:
|
|
1321
|
+
download, listFiles, getSignedUrl, and stats all support a cache option.
|
|
1322
|
+
Pass cache: "cache_tag" to check the product cache before hitting the provider.
|
|
1323
|
+
Cached results are stored fire-and-forget on miss.
|
|
1324
|
+
|
|
1325
|
+
Notes:
|
|
1326
|
+
- buffer can be a Node.js Buffer (server) or a base64 string (JSON proxy clients).
|
|
1327
|
+
- Cloud-linked configs inject runtime credentials via the cloud connection; no plaintext keys stored.
|
|
1328
|
+
- $Secret{} references in config are resolved at runtime if cloud-link resolution fails.
|
|
1329
|
+
`.trim(),
|
|
1330
|
+
cloud: `
|
|
1331
|
+
DUCTAPE CLOUD CONNECTIONS
|
|
1332
|
+
|
|
1333
|
+
Cloud connections link your workspace to an external cloud provider so Ductape can manage
|
|
1334
|
+
resources (databases, storage, brokers, graphs, vectors) on your behalf.
|
|
1335
|
+
|
|
1336
|
+
Supported providers: aws | gcp | azure | mongodb_atlas | neo4j_aura
|
|
1337
|
+
Auth modes: iam_role | oauth | service_principal | workload_identity | api_key
|
|
1338
|
+
|
|
1339
|
+
Create a connection (admin — ductape_cli):
|
|
1340
|
+
ductape_cli("cloud connections create --provider aws --name my-aws-conn")
|
|
1341
|
+
Returns: external_id, trust_policy, setup_instructions, setup_url
|
|
1342
|
+
AWS: the caller must create an IAM role using the returned trust policy, then complete:
|
|
1343
|
+
ductape_cli("cloud connections complete <tag> --role-arn arn:aws:iam::...")
|
|
1344
|
+
GCP: complete with project_id, service_account_email, service_account_json
|
|
1345
|
+
Azure: complete with tenant_id, subscription_id, client_id, client_secret, default_location
|
|
1346
|
+
Atlas: complete with atlas_public_key, atlas_private_key
|
|
1347
|
+
Aura (Neo4j): complete with aura_client_id, aura_client_secret, aura_instance_id
|
|
1348
|
+
|
|
1349
|
+
Validate a connection:
|
|
1350
|
+
ductape_cli("cloud connections validate <tag>")
|
|
1351
|
+
→ { valid, status, message?, tested_at? }
|
|
1352
|
+
|
|
1353
|
+
List / fetch / delete:
|
|
1354
|
+
ductape_cli("cloud connections list")
|
|
1355
|
+
ductape_cli("cloud connections fetch <tag>")
|
|
1356
|
+
|
|
1357
|
+
Discover resources on a connection:
|
|
1358
|
+
ductape_cli("cloud resources list -f query.json --json")
|
|
1359
|
+
File: { cloud: "<tag>", service: "gcs"|"s3"|"blob"|"rds"|"atlas-cluster"|..., region? }
|
|
1360
|
+
→ returns list of available resources (buckets, clusters, instances, etc.)
|
|
1361
|
+
|
|
1362
|
+
Import an existing resource and register it on the product:
|
|
1363
|
+
Use import-persist-all for multi-env products (required):
|
|
1364
|
+
ductape_cli("cloud resources import-persist-all -f all-envs.json --json")
|
|
1365
|
+
File is a JSON ARRAY — one entry per env, same product + component tag across all entries.
|
|
1366
|
+
Each entry: { cloud, service, type, product, component, env, resource, region?, dbName? }
|
|
1367
|
+
Supported service identifiers: s3, gcs, blob, rds, postgresql, cloudsql, sqs, pubsub,
|
|
1368
|
+
servicebus, neptune, cosmos-gremlin, opensearch, azure-search, atlas-cluster, aura-instance
|
|
1369
|
+
|
|
1370
|
+
Provision a brand-new resource and register it:
|
|
1371
|
+
ductape_cli("cloud resources provision-persist-all -f all-envs.json --json")
|
|
1372
|
+
Additional per-entry fields: tier, region/location, waitForReady.
|
|
1373
|
+
List available tiers first: ductape_cli("cloud tiers --provider aws --type database --db-type postgresql --json")
|
|
1374
|
+
NEVER infer region, tier, or cost — always list and confirm with the user first.
|
|
1375
|
+
Atlas and Neo4j Aura are IMPORT-ONLY — provision is not supported for these providers.
|
|
1376
|
+
|
|
1377
|
+
VPC connector (private networking):
|
|
1378
|
+
ductape_cli("cloud connections vpc update <tag> --vpc-id vpc-xxx --subnet-ids subnet-1,subnet-2")
|
|
1379
|
+
ductape_cli("cloud connections vpc status <tag>")
|
|
1380
|
+
VPC connector enables Ductape to reach resources in a private VPC; agent_status:
|
|
1381
|
+
pending → connected → disconnected
|
|
1382
|
+
|
|
1383
|
+
Scopes a connection can cover: storage | broker | database | graph | vector | cache
|
|
1384
|
+
|
|
1385
|
+
Component types accepted by import/provision:
|
|
1386
|
+
storage | messageBrokers | databases | graphs | vectors | caches
|
|
1387
|
+
`.trim(),
|
|
1388
|
+
vector: `
|
|
1389
|
+
DUCTAPE VECTOR DATABASES
|
|
1390
|
+
|
|
1391
|
+
Supported adapters: pinecone | qdrant | weaviate | opensearch | azure-search | vertex-vector-search
|
|
1392
|
+
(chroma, milvus, pgvector are declared but not yet implemented — do not use them)
|
|
1393
|
+
|
|
1394
|
+
Registration (admin — ductape_cli):
|
|
1395
|
+
ductape_cli("resources vectors create -f vector.json")
|
|
1396
|
+
File: { name, tag, type, dimensions: number, metric?: "cosine"|"euclidean"|"dotproduct",
|
|
1397
|
+
envs: [{ slug, endpoint?, apiKey?, region?, index?, namespace? }] }
|
|
1398
|
+
|
|
1399
|
+
Runtime operations:
|
|
1400
|
+
vector.upsert [{ product, env, tag, vectors: [{id, values: number[], metadata?}], namespace? }]
|
|
1401
|
+
vector.upsertOne [{ product, env, tag, id, values: number[], metadata?, namespace? }]
|
|
1402
|
+
vector.query [{ product, env, tag, vector: number[], topK?, filter?, namespace?,
|
|
1403
|
+
includeValues?, includeMetadata? }]
|
|
1404
|
+
vector.findSimilar [{ product, env, vector, values: number[], topK?, filter?, namespace? }]
|
|
1405
|
+
vector.fetchOne [{ product, env, vector, id, namespace? }]
|
|
1406
|
+
vector.fetchVectors [{ product, env, vector, ids: string[], namespace? }]
|
|
1407
|
+
vector.updateVector [{ product, env, vector, id, values?, setMetadata?, mergeMetadata?, namespace? }]
|
|
1408
|
+
vector.updateMetadata [{ product, env, vector, id, metadata: { key: value }, merge?, namespace? }]
|
|
1409
|
+
merge: true → deep-merges metadata rather than replacing it
|
|
1410
|
+
vector.deleteByIds [{ product, env, vector, ids: string[], namespace? }]
|
|
1411
|
+
vector.deleteAll [{ product, env, vector, namespace? }]
|
|
1412
|
+
vector.count [{ product, env, vector, namespace? }]
|
|
1413
|
+
|
|
1414
|
+
Namespace management:
|
|
1415
|
+
vector.listNamespaces [{ product, env, vector }]
|
|
1416
|
+
vector.deleteNamespace [{ product, env, vector, namespace }]
|
|
1417
|
+
|
|
1418
|
+
Listing (paginated):
|
|
1419
|
+
vector.listVectors [{ product, env, vector, namespace?, prefix?, limit?, cursor? }]
|
|
1420
|
+
vector.listAllVectors [{ product, env, vector, namespace?, prefix? }]
|
|
1421
|
+
listAllVectors auto-paginates until cursor exhausted — avoid on large indexes.
|
|
1422
|
+
|
|
1423
|
+
Index management:
|
|
1424
|
+
vector.describeIndex [{ product, env, vector }]
|
|
1425
|
+
vector.getStats [{ product, env, vector }] → totalVectorCount per namespace
|
|
1426
|
+
vector.createIndex [{ product, env, vector, name, dimensions, metric?, replicas?, shards? }]
|
|
1427
|
+
vector.deleteIndex [{ product, env, vector, name }]
|
|
1428
|
+
vector.listIndexes [{ product, env, vector }]
|
|
1429
|
+
|
|
1430
|
+
Distance metrics: cosine | euclidean | dotproduct | manhattan | hamming
|
|
1431
|
+
Index types: flat | ivf | hnsw | pq | ivf_pq | annoy
|
|
1432
|
+
Feature flags per adapter: metadata_filtering | namespaces | hybrid_search | batch_operations |
|
|
1433
|
+
index_management | vector_updates | sparse_vectors | multi_vector | aggregations
|
|
1434
|
+
|
|
1435
|
+
Call vector.supportsFeature(feature) or vector.getSupportedFeatures() to check what the
|
|
1436
|
+
chosen adapter supports before using a feature.
|
|
1437
|
+
`.trim(),
|
|
1438
|
+
warehouse: `
|
|
1439
|
+
DUCTAPE WAREHOUSE
|
|
1440
|
+
|
|
1441
|
+
The Warehouse is a unified query layer over the three structured data stores: Database, Graph, and Vector.
|
|
1442
|
+
It provides a single API for cross-store operations without having to address each service separately.
|
|
1443
|
+
|
|
1444
|
+
Warehouse is available on the SDK instance as ductape.warehouse (TypeScript) or Warehouse (C#/Go/Java).
|
|
1445
|
+
It is NOT a separate Ductape resource — it wraps the existing database, graph, and vector components
|
|
1446
|
+
that are already registered on the product.
|
|
1447
|
+
|
|
1448
|
+
Key use cases:
|
|
1449
|
+
- Run a relational query, a graph traversal, and a vector similarity search in a single call.
|
|
1450
|
+
- Build recommendation pipelines: graph neighbors → vector re-rank → database hydration.
|
|
1451
|
+
- Federated search across multiple store types with a single await.
|
|
1452
|
+
|
|
1453
|
+
Usage (TypeScript SDK):
|
|
1454
|
+
const result = await ductape.warehouse.query({
|
|
1455
|
+
product: "my-product",
|
|
1456
|
+
env: "prd",
|
|
1457
|
+
database: { tag: "core-db", entity: "products", where: { active: { $eq: true } }, select: ["id", "name"] },
|
|
1458
|
+
graph: { tag: "reco-graph", startId: userId, direction: "out", maxDepth: 2 },
|
|
1459
|
+
vector: { tag: "embedding-store", vector: queryEmbedding, topK: 10 },
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
result.database → relational rows
|
|
1463
|
+
result.graph → traversal nodes/relationships
|
|
1464
|
+
result.vector → similarity hits with scores
|
|
1465
|
+
|
|
1466
|
+
Warehouse context is set at SDK init time (env + workspaceId) and shared across all three stores.
|
|
1467
|
+
Underlying store calls use the same auth and product context; individual store errors are surfaced per store.
|
|
1468
|
+
|
|
1469
|
+
Before using Warehouse:
|
|
1470
|
+
1. Ensure the database, graph, and vector components are registered on the product.
|
|
1471
|
+
2. Confirm the env slug exists on all three stores.
|
|
1472
|
+
3. Each store can be queried independently — pass only the fields for the stores you need.
|
|
1473
|
+
|
|
1474
|
+
Warehouse does not support writes — use the individual service APIs (database.insert, graph.createNode,
|
|
1475
|
+
vector.upsert) for mutations.
|
|
1476
|
+
`.trim(),
|
|
1477
|
+
secrets: `
|
|
1478
|
+
DUCTAPE SECRETS
|
|
1479
|
+
|
|
1480
|
+
Secrets are workspace-level encrypted key-value pairs. They are referenced in resource configs,
|
|
1481
|
+
connection URLs, and any string field using the $Secret{KEY_NAME} syntax.
|
|
1482
|
+
|
|
1483
|
+
Create (admin — ductape_cli or ductape_execute):
|
|
1484
|
+
ductape_execute("secrets.create", [{
|
|
1485
|
+
key: "STRIPE_API_KEY",
|
|
1486
|
+
value: "sk_live_...", // plaintext — encrypted AES-256-GCM client-side before send
|
|
1487
|
+
description: "Stripe live key",
|
|
1488
|
+
token_type: "api", // "api" | "password" | "certificate"
|
|
1489
|
+
scope: ["my-product"], // which products can read this secret
|
|
1490
|
+
envs: ["prd"], // which env slugs can read this secret
|
|
1491
|
+
expires_at: 1800000000, // optional epoch ms
|
|
1492
|
+
}])
|
|
1493
|
+
The server never receives the plaintext value. Encryption uses the workspace private key.
|
|
1494
|
+
|
|
1495
|
+
Fetch / resolve:
|
|
1496
|
+
ductape_execute("secrets.fetch", ["STRIPE_API_KEY"]) → decrypted string value
|
|
1497
|
+
ductape_execute("secrets.exists", ["STRIPE_API_KEY"]) → boolean
|
|
1498
|
+
ductape_execute("secrets.validate", ["$Secret{STRIPE_API_KEY}"]) → { valid, missingKeys, existingKeys }
|
|
1499
|
+
ductape_execute("secrets.resolve", ["$Secret{STRIPE_API_KEY}", { env: "prd" }]) → resolved string
|
|
1500
|
+
|
|
1501
|
+
$Secret{} reference syntax:
|
|
1502
|
+
- Embed anywhere a string is accepted: "postgresql://$Secret{DB_USER}:$Secret{DB_PASS}@host/db"
|
|
1503
|
+
- Resolved at runtime before the value is used by the consuming service (storage, broker, graph, etc.)
|
|
1504
|
+
- The in-memory cache stores the encrypted form only; decryption happens on each cache hit.
|
|
1505
|
+
- Cache TTL: 5 minutes. Clear with: secrets.clearCache()
|
|
1506
|
+
|
|
1507
|
+
Lifecycle:
|
|
1508
|
+
ductape_execute("secrets.revoke", ["KEY"]) → disables without deleting (recoverable)
|
|
1509
|
+
ductape_execute("secrets.delete", ["KEY"]) → permanent deletion (irreversible)
|
|
1510
|
+
ductape_execute("secrets.update", ["KEY", { value: "new_value", expires_at: ... }])
|
|
1511
|
+
|
|
1512
|
+
List all secrets (keys only — values are not returned in list):
|
|
1513
|
+
ductape_execute("secrets.list", [])
|
|
1514
|
+
|
|
1515
|
+
Important:
|
|
1516
|
+
- Secrets are workspace-scoped, not product-scoped. scope[] and envs[] control access.
|
|
1517
|
+
- Other services (storage, broker, graph, etc.) resolve $Secret{} references automatically
|
|
1518
|
+
using the singleton secrets service — no manual resolution needed in most cases.
|
|
1519
|
+
- Never log or return resolved secret values to end users.
|
|
1520
|
+
`.trim(),
|
|
1521
|
+
apps: `
|
|
1522
|
+
DUCTAPE APPS
|
|
1523
|
+
|
|
1524
|
+
An app is a versioned API integration definition. It contains environments (base URLs), actions
|
|
1525
|
+
(individual endpoint specs), auth schemes, webhooks, variables, and constants.
|
|
1526
|
+
|
|
1527
|
+
Create an app (admin — ductape_cli):
|
|
1528
|
+
ductape_cli("apps create --name \\"Email Service\\" --description \\"Transactional email\\"")
|
|
1529
|
+
ductape_cli("app.init", ["app_tag"]) → loads app into builder state
|
|
1530
|
+
|
|
1531
|
+
Import from a file:
|
|
1532
|
+
ductape_cli("apps import <file.json> -t postman|openapi")
|
|
1533
|
+
Supports Postman v2.1 collection and OpenAPI 3.0 spec.
|
|
1534
|
+
|
|
1535
|
+
Manage environments (base URLs per stage):
|
|
1536
|
+
ductape_execute("app.environments.create", [app_tag, { slug, env_name, base_url }])
|
|
1537
|
+
ductape_execute("app.environments.list", [app_tag])
|
|
1538
|
+
|
|
1539
|
+
Manage actions (individual API endpoints):
|
|
1540
|
+
ductape_execute("actions.create", [app_tag, {
|
|
1541
|
+
tag, name, resource: "/users/{id}", method: "GET"|"POST"|"PUT"|"PATCH"|"DELETE",
|
|
1542
|
+
body?: { fieldName: { type, required? } },
|
|
1543
|
+
params?: { id: { type: "string" } },
|
|
1544
|
+
query?: { filter: { type: "string" } },
|
|
1545
|
+
headers?: { Authorization: { type: "string" } },
|
|
1546
|
+
response?: { status_code: 200, success: true, body: { ... }, response_format: "json" },
|
|
1547
|
+
}])
|
|
1548
|
+
ductape_execute("actions.update", [app_tag, action_tag, data])
|
|
1549
|
+
ductape_execute("actions.list", [app_tag])
|
|
1550
|
+
ductape_execute("actions.fetch", [app_tag, action_tag])
|
|
1551
|
+
|
|
1552
|
+
Run an action at runtime:
|
|
1553
|
+
→ CALL ductape_generate_payload FIRST (operation_family="action", method="run", targets={app, action})
|
|
1554
|
+
ductape_execute("actions.run", [{ product, env, app, action, input: { "body:field": value } }])
|
|
1555
|
+
ductape_execute("actions.dispatch", [{ product, env, app, action, input, schedule? }])
|
|
1556
|
+
|
|
1557
|
+
Auth schemes (how the app authenticates outbound requests):
|
|
1558
|
+
Setup types: header | bearer | basic | oauth2 | apikey
|
|
1559
|
+
ductape_execute("auths.create", [app_tag, { tag, name, setup_type, expiry, period, action_tag? }])
|
|
1560
|
+
ductape_execute("auths.list", [app_tag])
|
|
1561
|
+
|
|
1562
|
+
Webhooks (inbound events from the external service):
|
|
1563
|
+
ductape_execute("webhooks.create", [app_tag, { tag, name, description, envs: [{ slug, registration_url?, method? }] }])
|
|
1564
|
+
ductape_execute("webhooks.events.create", [app_tag, { tag, name, selector, description, sample }])
|
|
1565
|
+
|
|
1566
|
+
Variables (per-env mutable values) and Constants (fixed values):
|
|
1567
|
+
ductape_execute("app.variables.create", [app_tag, { key, value, env_slug }])
|
|
1568
|
+
ductape_execute("app.constants.create", [app_tag, { key, value }])
|
|
1569
|
+
|
|
1570
|
+
Connecting an app to a product (after creation):
|
|
1571
|
+
ductape_execute("product.apps.add", [product_tag, {
|
|
1572
|
+
access_tag: "app_access_tag",
|
|
1573
|
+
envs: [{ app_env_slug: "production", product_env_slug: "prd",
|
|
1574
|
+
variables: [{ key: "BASE_URL", value: "https://api.example.com" }],
|
|
1575
|
+
auth: { auth_tag: "api-key-auth", data: "$Secret{API_KEY}" } }]
|
|
1576
|
+
}])
|
|
1577
|
+
ductape_execute("product.apps.list", [product_tag])
|
|
1578
|
+
ductape_execute("product.apps.fetch", [product_tag, access_tag])
|
|
1579
|
+
`.trim(),
|
|
1580
|
+
products: `
|
|
1581
|
+
DUCTAPE PRODUCTS
|
|
1582
|
+
|
|
1583
|
+
A product is the top-level namespace for all Ductape infrastructure: apps, databases, graphs,
|
|
1584
|
+
vectors, storage, brokers, sessions, caches, notifications, resilience, features, jobs, and envs.
|
|
1585
|
+
Every SDK service call resolves within a product context.
|
|
1586
|
+
|
|
1587
|
+
Create a product:
|
|
1588
|
+
ductape_cli("products create --name \\"My App\\" --tag my-app")
|
|
1589
|
+
ductape_execute("product.create", [{ name: "My App", tag: "my-app",
|
|
1590
|
+
envs: [{ slug: "dev", name: "Development" }, { slug: "prd", name: "Production" }] }])
|
|
1591
|
+
|
|
1592
|
+
Environments — every resource's envs array MUST cover all product env slugs:
|
|
1593
|
+
ductape_execute("product.environments.create", [product_tag, { slug, env_name, description, active? }])
|
|
1594
|
+
ductape_execute("product.environments.list", [product_tag])
|
|
1595
|
+
ductape_execute("product.environments.fetch", [product_tag, slug])
|
|
1596
|
+
BEFORE registering any resource, always run environments.list and collect all slugs.
|
|
1597
|
+
|
|
1598
|
+
Fetch / update:
|
|
1599
|
+
ductape_execute("product.fetch", [product_tag])
|
|
1600
|
+
ductape_execute("product.update", [product_tag, { name?, description? }])
|
|
1601
|
+
|
|
1602
|
+
Connect apps to a product:
|
|
1603
|
+
See ductape_docs({ topic: "apps" }) for product.apps.add / product.apps.list.
|
|
1604
|
+
|
|
1605
|
+
Resource registration (all via ductape_cli or ductape_execute — see per-topic docs):
|
|
1606
|
+
databases → ductape_docs({ topic: "transactions" })
|
|
1607
|
+
storage → ductape_docs({ topic: "storage" })
|
|
1608
|
+
graphs → ductape_docs({ topic: "graphs" })
|
|
1609
|
+
vectors → ductape_docs({ topic: "vector" })
|
|
1610
|
+
events → ductape_docs({ topic: "events" })
|
|
1611
|
+
caches → ductape_docs({ topic: "caches" })
|
|
1612
|
+
notifications → ductape_docs({ topic: "notifications" })
|
|
1613
|
+
sessions → ductape_docs({ topic: "sessions" })
|
|
1614
|
+
resilience → ductape_docs({ topic: "resilience" })
|
|
1615
|
+
features → ductape_docs({ topic: "features" })
|
|
1616
|
+
|
|
1617
|
+
Product structure (IProduct fields):
|
|
1618
|
+
_id, workspace_id, name, tag, description, private_key,
|
|
1619
|
+
apps[], envs[], databases[], graphs[], vectors[], storage[], messageBrokers[],
|
|
1620
|
+
caches[], sessions[], notifications[], quota[], fallback[], healthchecks[],
|
|
1621
|
+
workflows[] (features), models[], agents[], jobs[]
|
|
1622
|
+
|
|
1623
|
+
Bootstrap (single API call returning product context + component config + private key):
|
|
1624
|
+
Each service makes a single bootstrap call at first use; results are cached in BootstrapCache
|
|
1625
|
+
(Redis when available). This avoids repeated round-trips in high-frequency paths.
|
|
1626
|
+
`.trim(),
|
|
1627
|
+
sessions: `
|
|
1628
|
+
DUCTAPE SESSIONS
|
|
1629
|
+
|
|
1630
|
+
A session is a named JWT schema on a product. It defines:
|
|
1631
|
+
- tag / name — unique identifier and display name
|
|
1632
|
+
- expiry + period — how long each issued JWT is valid (duration, not an absolute date)
|
|
1633
|
+
- selector — the dot-path of the field inside the data object that is the PRIMARY
|
|
1634
|
+
user identifier (e.g. "playerId"). This field becomes the lookup key
|
|
1635
|
+
for revoke, list, and analytics. It MUST exist in the schema.
|
|
1636
|
+
- schema — the complete shape of data that will be embedded in the JWT payload.
|
|
1637
|
+
Each field is declared as { type, required?, description? }.
|
|
1638
|
+
This is a TYPE DECLARATION, not sample data. The actual values
|
|
1639
|
+
are passed at runtime when calling sessions.start.
|
|
1640
|
+
|
|
1641
|
+
IMPORTANT — schema field format: { fieldName: { type, required?, description? } }
|
|
1642
|
+
type values: "string" | "number" | "boolean" | "object" | "array"
|
|
1643
|
+
Do NOT use the shorthand { fieldName: "string" } — pass the full object form.
|
|
1644
|
+
|
|
1645
|
+
Example (game product — player identity in JWT):
|
|
1646
|
+
ductape_execute("sessions.create", [product_tag, {
|
|
1647
|
+
tag: "player-session",
|
|
1648
|
+
name: "Player Session",
|
|
1649
|
+
expiry: 24,
|
|
1650
|
+
period: "hours",
|
|
1651
|
+
selector: "playerId", // primary identifier — must be in schema below
|
|
1652
|
+
schema: {
|
|
1653
|
+
playerId: { type: "string", required: true, description: "Unique player ID" },
|
|
1654
|
+
username: { type: "string", required: true, description: "Player display name" },
|
|
1655
|
+
role: { type: "string", required: false, description: "admin | player | guest" },
|
|
1656
|
+
accountId: { type: "string", required: false, description: "Linked account ID" },
|
|
1657
|
+
},
|
|
1658
|
+
}])
|
|
1659
|
+
|
|
1660
|
+
Runtime — create a session (sign a JWT):
|
|
1661
|
+
→ CALL ductape_generate_payload FIRST (operation_family="session", method="start", targets={tag})
|
|
1662
|
+
to discover the exact data field names accepted for this session tag.
|
|
1663
|
+
ductape_execute("sessions.start", [{ product, env, tag: "player-session",
|
|
1664
|
+
data: {
|
|
1665
|
+
playerId: "player_abc123", // must match selector — used as the revocation/lookup key
|
|
1666
|
+
username: "ShadowBlade",
|
|
1667
|
+
role: "player",
|
|
1668
|
+
accountId: "acct_xyz",
|
|
1669
|
+
} }])
|
|
1670
|
+
→ returns token: "player-session:eyJ..." ← format is always "session_tag:jwt"
|
|
1671
|
+
The token embeds all schema fields in the JWT payload, signed with the product private key.
|
|
1672
|
+
|
|
1673
|
+
Verify a token:
|
|
1674
|
+
ductape_execute("sessions.verify", [{ product, env, tag: "user-session", token: "user-session:eyJ..." }])
|
|
1675
|
+
→ decodes JWT + checks revocation blacklist; throws if revoked or expired
|
|
1676
|
+
|
|
1677
|
+
Refresh (atomic token rotation):
|
|
1678
|
+
ductape_execute("sessions.refresh", [{ product, env, tag: "user-session", refreshToken: "..." }])
|
|
1679
|
+
Old refresh token is invalidated server-side atomically — no partial state possible.
|
|
1680
|
+
The refresh token is AES-encrypted JSON (NOT a JWT).
|
|
1681
|
+
|
|
1682
|
+
Revoke:
|
|
1683
|
+
ductape_execute("sessions.revoke", [{ product, env, tag, sessionId?, identifier? }])
|
|
1684
|
+
Requires at least one of sessionId or identifier.
|
|
1685
|
+
Revocation is enforced via a ProcessorAPI blacklist check on every verify() call.
|
|
1686
|
+
|
|
1687
|
+
Analytics:
|
|
1688
|
+
sessions.listActive [{ product, env, tag, identifier?, page?, limit? }]
|
|
1689
|
+
sessions.fetchUsers [{ product, session, env?, page?, limit? }]
|
|
1690
|
+
sessions.fetchUserDetails [{ product, session, identifier, env? }]
|
|
1691
|
+
sessions.fetchDashboard [{ product, session, env? }]
|
|
1692
|
+
→ { DAU, WAU, MAU, activityTimeline, peakHours, environmentBreakdown, avgSessionDuration }
|
|
1693
|
+
|
|
1694
|
+
Token format: "session_tag:jwt_token" — pass this format verbatim wherever session is expected.
|
|
1695
|
+
JWT is signed with the product private key (not a symmetric shared secret).
|
|
1696
|
+
`.trim(),
|
|
1697
|
+
caches: `
|
|
1698
|
+
DUCTAPE CACHES
|
|
1699
|
+
|
|
1700
|
+
Caches are product-level Redis (or in-memory) stores for temporary key-value data with optional TTL.
|
|
1701
|
+
|
|
1702
|
+
Registration (admin — ductape_cli):
|
|
1703
|
+
ductape_cli("resources caches create -f cache.json")
|
|
1704
|
+
File: { name, tag, type: "redis"|"memcached"|"in-memory",
|
|
1705
|
+
envs: [{ slug, connection_url }] }
|
|
1706
|
+
|
|
1707
|
+
Operations:
|
|
1708
|
+
caches.set [{ product, cache, key, value: string, expiry?: string (ISO 8601), env }]
|
|
1709
|
+
expiry is an ABSOLUTE TIMESTAMP (not a duration).
|
|
1710
|
+
To expire in 1 hour: expiry = new Date(Date.now() + 3600_000).toISOString()
|
|
1711
|
+
To expire in 24 hours: expiry = new Date(Date.now() + 86400_000).toISOString()
|
|
1712
|
+
To never expire: omit expiry entirely.
|
|
1713
|
+
Via MCP: pass an ISO 8601 string e.g. "2026-07-17T12:00:00.000Z"
|
|
1714
|
+
→ Writes to Redis synchronously, then fires remote API write in background (non-blocking).
|
|
1715
|
+
caches.get [{ key: string }]
|
|
1716
|
+
→ Checks Redis first; on miss falls through to remote API; on hit from API, populates Redis.
|
|
1717
|
+
→ Enforces TTL by comparing the stored expiry timestamp against current time (client-side check).
|
|
1718
|
+
caches.clear [{ key: string }]
|
|
1719
|
+
→ Deletes from Redis and from remote API.
|
|
1720
|
+
caches.clearAll [{ product, cache, env? }]
|
|
1721
|
+
→ Bulk delete via remote API only. Redis may still have stale keys until naturally evicted.
|
|
1722
|
+
caches.fetchValues [{ product, cache, env?, page?, limit?,
|
|
1723
|
+
expiryFilter?: "all"|"expiring"|"permanent"|"expired" }]
|
|
1724
|
+
caches.fetchDashboard [{ product, cache, env? }]
|
|
1725
|
+
→ { totalValues, activeValues, expiredValues, totalSize }
|
|
1726
|
+
|
|
1727
|
+
Tier architecture (three tiers applied automatically):
|
|
1728
|
+
Tier 1: in-process Map (SDK metadata cache, 5-min TTL — not for user data)
|
|
1729
|
+
Tier 2: Redis hash (hSet/hGetAll) with optional EXPIRE
|
|
1730
|
+
Tier 3: Remote Ductape API
|
|
1731
|
+
|
|
1732
|
+
Practical examples:
|
|
1733
|
+
// Cache a player leaderboard for 5 minutes:
|
|
1734
|
+
{ product, cache: "leaderboard-cache", key: "top-100", value: JSON.stringify(rows),
|
|
1735
|
+
expiry: new Date(Date.now() + 300_000).toISOString(), env: "prd" }
|
|
1736
|
+
|
|
1737
|
+
// Cache a session token lookup for 1 hour:
|
|
1738
|
+
{ product, cache: "session-cache", key: "player:u_123", value: token,
|
|
1739
|
+
expiry: new Date(Date.now() + 3_600_000).toISOString(), env: "prd" }
|
|
1740
|
+
|
|
1741
|
+
Important:
|
|
1742
|
+
- Redis is optional; without it all reads/writes go through the remote API.
|
|
1743
|
+
- expiry is stored as a Date field, not a Redis TTL — the expiry check happens client-side on read.
|
|
1744
|
+
- clearAll only clears via the remote API bulk endpoint; Redis may retain stale entries until evicted.
|
|
1745
|
+
- Cache entries are stored as Redis hashes (not plain strings).
|
|
1746
|
+
- Other services (storage, graph, notifications, sessions) also use CacheManager internally —
|
|
1747
|
+
configure a shared Redis URL at SDK init to share the pool.
|
|
1748
|
+
`.trim(),
|
|
1749
|
+
notifications: `
|
|
1750
|
+
DUCTAPE NOTIFICATIONS
|
|
1751
|
+
|
|
1752
|
+
Notifications send messages across multiple channels: email, SMS, push, or HTTP callback.
|
|
1753
|
+
|
|
1754
|
+
Create a notification (admin — ductape_execute):
|
|
1755
|
+
ductape_execute("notifications.create", [product_tag, {
|
|
1756
|
+
tag: "welcome-email",
|
|
1757
|
+
name: "Welcome Email",
|
|
1758
|
+
type: "email", // optional hint; actual channels configured per env
|
|
1759
|
+
}])
|
|
1760
|
+
|
|
1761
|
+
Create a message template:
|
|
1762
|
+
ductape_execute("notifications.messages.create", [product_tag, {
|
|
1763
|
+
tag: "welcome-email:default", // format: "notification_tag:message_tag"
|
|
1764
|
+
notification: "welcome-email",
|
|
1765
|
+
subject: { template: "Welcome, {{name}}!", data: { name: "" } },
|
|
1766
|
+
body: { template: "Hi {{name}}, thanks for signing up.", data: { name: "" } },
|
|
1767
|
+
}])
|
|
1768
|
+
|
|
1769
|
+
Send at runtime (one channel at a time):
|
|
1770
|
+
→ CALL ductape_generate_payload FIRST (operation_family="notification", method="email.send")
|
|
1771
|
+
notifications.email.send [{ product, env, notification, input: { recipients, subject?, template? } }]
|
|
1772
|
+
notifications.push.send [{ product, env, notification, input: { device_tokens, title?, body?, data? } }]
|
|
1773
|
+
notifications.sms.send [{ product, env, notification, input: { recipients, body? } }]
|
|
1774
|
+
notifications.callback.send [{ product, env, notification, input: { query?, headers?, params?, body? } }]
|
|
1775
|
+
|
|
1776
|
+
Multi-channel send (all channels in parallel):
|
|
1777
|
+
notifications.send [{ product, env, event: "notif_tag:message_tag", input: { ... } }]
|
|
1778
|
+
→ success is true if at least one channel succeeds; channel failures do not abort siblings.
|
|
1779
|
+
|
|
1780
|
+
Background dispatch with scheduling:
|
|
1781
|
+
notifications.dispatch [{ product, env, notification, event, input,
|
|
1782
|
+
schedule?: { start_at?, cron?, every?, limit?, endDate?, tz? } }]
|
|
1783
|
+
→ Returns { job_id, status: "queued", scheduled_at, recurring, next_run_at }
|
|
1784
|
+
|
|
1785
|
+
Query delivery logs:
|
|
1786
|
+
notifications.getMessages [{ product_tag?, env?, notification_tag?, status?,
|
|
1787
|
+
type?, start_date?, end_date?, page?, limit? }]
|
|
1788
|
+
Status values: pending | sent | failed | reprocessing
|
|
1789
|
+
|
|
1790
|
+
Supported providers:
|
|
1791
|
+
Email: smtp | mailgun | sendgrid | postmark | brevo
|
|
1792
|
+
SMS: twilio | nexmo | plivo | other
|
|
1793
|
+
Push: firebase | expo
|
|
1794
|
+
Callback: any HTTP endpoint
|
|
1795
|
+
|
|
1796
|
+
Channel configuration (envs per notification) is done in the Workbench UI, not via CLI.
|
|
1797
|
+
Notification tag and message tag are ALWAYS passed together as "notification_tag:message_tag".
|
|
1798
|
+
`.trim(),
|
|
1799
|
+
resilience: `
|
|
1800
|
+
DUCTAPE RESILIENCE
|
|
1801
|
+
|
|
1802
|
+
Resilience covers three mechanisms: quotas (rate-limited provider pools), fallbacks (automatic
|
|
1803
|
+
provider switching), and healthchecks (continuous probe monitoring with failure actions).
|
|
1804
|
+
|
|
1805
|
+
QUOTAS — rate-limited multi-provider pools:
|
|
1806
|
+
ductape_execute("quotas.create", [product_tag, {
|
|
1807
|
+
tag: "sms-quota",
|
|
1808
|
+
name: "SMS Provider Pool",
|
|
1809
|
+
input: { to: { type: "string", required: true }, message: { type: "string" } },
|
|
1810
|
+
options: [
|
|
1811
|
+
{ provider: "twilio", app: "twilio-app", type: "action", event: "send-sms",
|
|
1812
|
+
quota: 1000, uses: 0, retries: 2,
|
|
1813
|
+
input: { "body:to": "$Input{to}", "body:message": "$Input{message}" },
|
|
1814
|
+
output: {} },
|
|
1815
|
+
{ provider: "nexmo", app: "nexmo-app", type: "action", event: "send-sms",
|
|
1816
|
+
quota: 500, uses: 0, retries: 1,
|
|
1817
|
+
input: { "body:to": "$Input{to}", "body:body": "$Input{message}" },
|
|
1818
|
+
output: {} },
|
|
1819
|
+
],
|
|
1820
|
+
}])
|
|
1821
|
+
Providers are tried in order until quota is not exhausted.
|
|
1822
|
+
quotas.run [{ product, env, tag, input }] ← CALL ductape_generate_payload FIRST
|
|
1823
|
+
quotas.dispatch [{ product, env, tag, input, schedule? }]
|
|
1824
|
+
|
|
1825
|
+
FALLBACKS — automatic provider switching on failure:
|
|
1826
|
+
Same schema as quotas but options are ordered: primary first, then fallback(s).
|
|
1827
|
+
Primary is used first; on failure, the next provider is tried automatically.
|
|
1828
|
+
ductape_execute("fallback.create", [product_tag, { tag, name, input: { ... }, options: [...] }])
|
|
1829
|
+
fallback.run [{ product, env, tag, input }] ← CALL ductape_generate_payload FIRST
|
|
1830
|
+
fallback.dispatch [{ product, env, tag, input, schedule? }]
|
|
1831
|
+
|
|
1832
|
+
HEALTHCHECKS — continuous probe with failure notifications:
|
|
1833
|
+
ductape_execute("health.create", [product_tag, {
|
|
1834
|
+
tag: "payment-health",
|
|
1835
|
+
name: "Payment Service Health",
|
|
1836
|
+
probe: { type: "app", app: "stripe-app", event: "ping" },
|
|
1837
|
+
interval: 30000, // ms between checks
|
|
1838
|
+
retries: 3,
|
|
1839
|
+
envs: [{ slug: "prd", input: {} }],
|
|
1840
|
+
onFailure: {
|
|
1841
|
+
notifications: [{ notification: "ops-alerts", message: "payment-down",
|
|
1842
|
+
channels: { email: { recipients: ["ops@example.com"] } } }],
|
|
1843
|
+
webhooks: [{ url: "https://hooks.example.com/alert", method: "POST" }],
|
|
1844
|
+
},
|
|
1845
|
+
}])
|
|
1846
|
+
health.run [{ product, env, tag }] → triggers an immediate probe
|
|
1847
|
+
health.check [{ product, env, tag }] → same as run
|
|
1848
|
+
health.status [{ product, env, tag }] → current health status
|
|
1849
|
+
|
|
1850
|
+
Probe types: app | database | feature | graph | message_broker | storage
|
|
1851
|
+
Failure actions: notification channels, HTTP webhooks, and/or message broker emit — all can
|
|
1852
|
+
be configured simultaneously on the same healthcheck.
|
|
1853
|
+
Input template references: $Input{field} → maps declared input to the probe's action input.
|
|
1854
|
+
Provider status: available | unavailable
|
|
1855
|
+
`.trim(),
|
|
1856
|
+
features: `
|
|
1857
|
+
DUCTAPE FEATURES
|
|
1858
|
+
|
|
1859
|
+
A feature is an orchestrated workflow of durable steps. Steps can call app actions, database
|
|
1860
|
+
operations, graph queries, storage uploads, notifications, broker publishes, child features,
|
|
1861
|
+
quotas, fallbacks, and more. Features support rollback, signals, checkpoints, and sleep.
|
|
1862
|
+
|
|
1863
|
+
Step types: action | database | graph | notification | storage | produce | quota | fallback |
|
|
1864
|
+
vector | child_feature | sleep | wait_for_signal | checkpoint
|
|
1865
|
+
|
|
1866
|
+
Create a feature (admin — ductape_execute):
|
|
1867
|
+
ductape_execute("features.create", [product_tag, {
|
|
1868
|
+
tag: "onboard-user",
|
|
1869
|
+
name: "Onboard User",
|
|
1870
|
+
input: { userId: { type: "string", required: true }, email: { type: "string" } },
|
|
1871
|
+
steps: [
|
|
1872
|
+
{
|
|
1873
|
+
tag: "create-account",
|
|
1874
|
+
type: "database",
|
|
1875
|
+
database: "core-db",
|
|
1876
|
+
event: "insert-user",
|
|
1877
|
+
input: { "body:userId": "$Input{userId}", "body:email": "$Input{email}" },
|
|
1878
|
+
options: { retries: 2, timeout: 5000 },
|
|
1879
|
+
},
|
|
1880
|
+
{
|
|
1881
|
+
tag: "send-welcome",
|
|
1882
|
+
type: "notification",
|
|
1883
|
+
notification: "welcome-email",
|
|
1884
|
+
event: "welcome-email:default",
|
|
1885
|
+
input: { "body:email": "$Input{email}" },
|
|
1886
|
+
dependsOn: ["create-account"],
|
|
1887
|
+
options: { allow_fail: true }, // welcome email failure won't abort the feature
|
|
1888
|
+
},
|
|
1889
|
+
],
|
|
1890
|
+
}])
|
|
1891
|
+
|
|
1892
|
+
Execute at runtime:
|
|
1893
|
+
→ CALL ductape_generate_payload FIRST (operation_family="features", method="execute", targets={tag})
|
|
1894
|
+
features.execute [{ product, env, tag, input: { userId: "u_123", email: "..." },
|
|
1895
|
+
idempotency_key?: string, retries?: number, timeout?: number }]
|
|
1896
|
+
|
|
1897
|
+
Background dispatch with scheduling:
|
|
1898
|
+
features.dispatch [{ product, env, feature, input,
|
|
1899
|
+
schedule?: { start_at?, cron?, every?, limit?, endDate?, tz? } }]
|
|
1900
|
+
→ Returns { job_id, status: "queued"|"scheduled", scheduled_at, next_run_at }
|
|
1901
|
+
|
|
1902
|
+
Execution management:
|
|
1903
|
+
features.status [executionId] → { status, current_step, completed_steps, output, error }
|
|
1904
|
+
features.cancel [executionId, reason?] → triggers rollback of running steps
|
|
1905
|
+
features.replay [executionId, options?] → re-run with original input
|
|
1906
|
+
features.restart [executionId] → re-run with new input
|
|
1907
|
+
features.resume [executionId] → continue from a checkpoint
|
|
1908
|
+
features.replayFromStep [executionId, stepTag] → replay from a specific step
|
|
1909
|
+
features.history [executionId] → { events[], checkpoints[], replays[], restarts[] }
|
|
1910
|
+
features.stepDetail [executionId, stepTag] → per-step input/output/error/timing
|
|
1911
|
+
|
|
1912
|
+
Signals and queries (for long-running features):
|
|
1913
|
+
features.signal [{ product, env, feature_id, signal: "payment-confirmed", payload? }]
|
|
1914
|
+
features.query [{ product, env, feature_id, query: "current-status", params? }]
|
|
1915
|
+
|
|
1916
|
+
Step input references:
|
|
1917
|
+
$Input{field} → map from feature's declared input
|
|
1918
|
+
$Step{stepTag}{field} → output field from a prior step
|
|
1919
|
+
$StepOutput{field} → current step's own return value
|
|
1920
|
+
$Concat([...parts], delim) → string interpolation
|
|
1921
|
+
|
|
1922
|
+
Rollback strategies: reverse_all | reverse_critical | compensate | none
|
|
1923
|
+
Feature statuses: pending | running | completed | failed | rolled_back | rolling_back | paused
|
|
1924
|
+
|
|
1925
|
+
Code-first (define API — compiles async handler to JSON step schema):
|
|
1926
|
+
features.define({ tag, name, input, handler: async (ctx) => { ... } })
|
|
1927
|
+
ctx provides: ctx.step(), ctx.action.run(), ctx.database.query/insert(),
|
|
1928
|
+
ctx.graph.execute(), ctx.notification.send(), ctx.storage.upload(),
|
|
1929
|
+
ctx.messaging.produce(), ctx.quota.execute(), ctx.fallback.execute(),
|
|
1930
|
+
ctx.sleep(), ctx.waitForSignal(), ctx.setState/getState(), ctx.feature()
|
|
1931
|
+
`.trim(),
|
|
1932
|
+
events: `
|
|
1933
|
+
DUCTAPE EVENTS (MESSAGE BROKERS)
|
|
1934
|
+
|
|
1935
|
+
ARCHITECTURE — always two separate steps:
|
|
1936
|
+
Step 1: Register the BROKER COMPONENT (establishes the connection to the broker service).
|
|
1937
|
+
The broker's envs[] holds connection credentials and host/project info, NOT topics.
|
|
1938
|
+
Step 2: Create TOPIC DEFINITIONS on that broker (unlimited; each is a named subject or queue).
|
|
1939
|
+
Topics are separate from the broker registration and added after.
|
|
1940
|
+
A single broker component can have as many topics as needed.
|
|
1941
|
+
|
|
1942
|
+
━━━ SUPPORTED BROKER TYPES ━━━
|
|
1943
|
+
|
|
1944
|
+
Cloud-managed (provision OR import via cloud connection):
|
|
1945
|
+
GCP Pub/Sub → service: "pubsub", type in envs: "google_pubsub"
|
|
1946
|
+
AWS SQS → service: "sqs", type in envs: "aws_sqs"
|
|
1947
|
+
Azure SvcBus → service: "servicebus", type in envs: "azure_servicebus"
|
|
1948
|
+
|
|
1949
|
+
Self-hosted (import-only — supply connection URL manually):
|
|
1950
|
+
Kafka → type in envs: "kafka"
|
|
1951
|
+
RabbitMQ → type in envs: "rabbitmq"
|
|
1952
|
+
Redis → type in envs: "redis"
|
|
1953
|
+
NATS → type in envs: "nats"
|
|
1954
|
+
|
|
1955
|
+
━━━ STEP 1A: REGISTER VIA CLOUD CONNECTION (cloud-managed brokers) ━━━
|
|
1956
|
+
|
|
1957
|
+
Provision (create a NEW resource in the cloud):
|
|
1958
|
+
ductape_cli("cloud resources provision-persist-all -f brokers.json --json")
|
|
1959
|
+
File: JSON ARRAY — one entry per env, same product + component tag across all entries.
|
|
1960
|
+
type field: "messageBrokers" (exact — not "messagebrokers" or "events")
|
|
1961
|
+
|
|
1962
|
+
GCP Pub/Sub — creates a new Pub/Sub topic in GCP, stores credentials in secrets:
|
|
1963
|
+
[{"cloud":"gcp-snd","service":"pubsub","type":"messageBrokers",
|
|
1964
|
+
"product":"my-product","component":"notifications-broker","env":"snd",
|
|
1965
|
+
"topicName":"my-product-notifications-snd"},
|
|
1966
|
+
{"cloud":"gcp-prd","service":"pubsub","type":"messageBrokers",
|
|
1967
|
+
"product":"my-product","component":"notifications-broker","env":"prd",
|
|
1968
|
+
"topicName":"my-product-notifications-prd"}]
|
|
1969
|
+
If topicName is omitted a timestamped name is generated — always supply it explicitly.
|
|
1970
|
+
|
|
1971
|
+
AWS SQS — creates a new SQS queue per env:
|
|
1972
|
+
[{"cloud":"aws-snd","service":"sqs","type":"messageBrokers",
|
|
1973
|
+
"product":"my-product","component":"notifications-broker","env":"snd",
|
|
1974
|
+
"queueName":"my-product-notifications-snd"},
|
|
1975
|
+
{"cloud":"aws-prd","service":"sqs","type":"messageBrokers",
|
|
1976
|
+
"product":"my-product","component":"notifications-broker","env":"prd",
|
|
1977
|
+
"queueName":"my-product-notifications-prd"}]
|
|
1978
|
+
|
|
1979
|
+
Azure Service Bus — creates a namespace + queue per env:
|
|
1980
|
+
[{"cloud":"azure-snd","service":"servicebus","type":"messageBrokers",
|
|
1981
|
+
"product":"my-product","component":"notifications-broker","env":"snd",
|
|
1982
|
+
"namespaceName":"myproduct-snd","queueName":"notifications"},
|
|
1983
|
+
{"cloud":"azure-prd","service":"servicebus","type":"messageBrokers",
|
|
1984
|
+
"product":"my-product","component":"notifications-broker","env":"prd",
|
|
1985
|
+
"namespaceName":"myproduct-prd","queueName":"notifications"}]
|
|
1986
|
+
|
|
1987
|
+
Import (register an EXISTING cloud resource):
|
|
1988
|
+
Same as above but use import-persist-all and supply "resource" (the existing resource name/ID):
|
|
1989
|
+
ductape_cli("cloud resources import-persist-all -f brokers.json --json")
|
|
1990
|
+
Each entry: { cloud, service, type: "messageBrokers", product, component, env, resource: "<id>" }
|
|
1991
|
+
|
|
1992
|
+
IMPORTANT: Never share one cloud resource (topic/queue) across snd and prd envs — use
|
|
1993
|
+
separate resources per env to avoid mixing sandbox and production events.
|
|
1994
|
+
|
|
1995
|
+
━━━ STEP 1B: REGISTER SELF-HOSTED BROKER (no cloud connection needed) ━━━
|
|
1996
|
+
|
|
1997
|
+
ductape_cli("resources events create -f broker.json")
|
|
1998
|
+
File: {
|
|
1999
|
+
name: string, tag: string, description?: string,
|
|
2000
|
+
envs: [
|
|
2001
|
+
{
|
|
2002
|
+
slug: "snd",
|
|
2003
|
+
type: "kafka"|"rabbitmq"|"redis"|"nats",
|
|
2004
|
+
config: <see config shapes below>
|
|
2005
|
+
},
|
|
2006
|
+
{ slug: "prd", type: "kafka", config: { ... } }
|
|
2007
|
+
]
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
Config shapes per type:
|
|
2011
|
+
kafka: { brokers: ["host:9092"], clientId: "my-app", groupId?: "...",
|
|
2012
|
+
ssl?: true, sasl?: { mechanism: "plain", username, password } }
|
|
2013
|
+
rabbitmq: { url: "amqp://user:pass@host:5672/vhost" }
|
|
2014
|
+
redis: { host: "...", port: 6379, password?: "..." }
|
|
2015
|
+
nats: { servers: ["nats://host:4222"], token?: "...", user?: "...", pass?: "...", tls?: true }
|
|
2016
|
+
|
|
2017
|
+
━━━ STEP 2: ADD TOPIC DEFINITIONS (all broker types — add as many as needed) ━━━
|
|
2018
|
+
|
|
2019
|
+
ductape_execute("messageBrokers.topics.create", [product_tag, {
|
|
2020
|
+
tag: "player-joined",
|
|
2021
|
+
name: "Player Joined",
|
|
2022
|
+
broker: "notifications-broker", // broker component tag
|
|
2023
|
+
description?: string,
|
|
2024
|
+
sample: { playerId: "string", username: "string" }, // example message shape
|
|
2025
|
+
idempotent?: boolean,
|
|
2026
|
+
// SQS only — map per-env queue URLs (each topic can be a different queue):
|
|
2027
|
+
queueUrls?: [
|
|
2028
|
+
{ env_slug: "snd", url: "https://sqs.us-east-1.amazonaws.com/123456/my-product-player-joined-snd" },
|
|
2029
|
+
{ env_slug: "prd", url: "https://sqs.us-east-1.amazonaws.com/123456/my-product-player-joined-prd" }
|
|
2030
|
+
]
|
|
2031
|
+
}])
|
|
2032
|
+
|
|
2033
|
+
For GCP Pub/Sub and Azure Service Bus, the SDK resolves the topic name from the topic tag (or
|
|
2034
|
+
config.topicName on the env config). No extra per-topic URL mapping is needed beyond the tag.
|
|
2035
|
+
For SQS, every topic definition needs queueUrls to point to the specific per-env queue.
|
|
2036
|
+
Repeat this call for each logical event type — there is no limit on number of topics.
|
|
2037
|
+
|
|
2038
|
+
List topics on a broker:
|
|
2039
|
+
ductape_execute("messageBrokers.topics.list", [product_tag, "broker-tag"])
|
|
2040
|
+
|
|
2041
|
+
━━━ RUNTIME: PRODUCE AND CONSUME ━━━
|
|
2042
|
+
|
|
2043
|
+
Produce (publish a message):
|
|
2044
|
+
→ CALL ductape_generate_payload FIRST (operation_family="messaging", method="produce")
|
|
2045
|
+
messageBrokers.produce [{ product, env, event: "broker_tag:topic_tag", message: { key: value } }]
|
|
2046
|
+
Idempotent publish (deduplicates by key, default TTL 86400 s):
|
|
2047
|
+
messageBrokers.publishIdempotent [{ product, env, event, message, idempotency_key, ttl? }]
|
|
2048
|
+
|
|
2049
|
+
Consume (subscribe):
|
|
2050
|
+
messageBrokers.consume [{ product, env, event: "broker_tag:topic_tag",
|
|
2051
|
+
callback: async (message) => { ... } }]
|
|
2052
|
+
Callback errors are re-thrown so the broker can nack/retry.
|
|
2053
|
+
|
|
2054
|
+
Background dispatch with scheduling:
|
|
2055
|
+
messageBrokers.dispatch [{ product, env, broker, event, input: { message },
|
|
2056
|
+
schedule?: { start_at?, cron?, every?, limit?, endDate?, tz? } }]
|
|
2057
|
+
→ CALL ductape_generate_payload FIRST (operation_family="messaging", method="dispatch")
|
|
2058
|
+
|
|
2059
|
+
Event format string: "broker_tag:topic_tag" — always colon-separated.
|
|
2060
|
+
Message payload is AES-encrypted before the tracking API call — tracking never sees plaintext.
|
|
2061
|
+
|
|
2062
|
+
━━━ OBSERVABILITY ━━━
|
|
2063
|
+
|
|
2064
|
+
messageBrokers.messages.query [{ product, env, brokerTag, topicTag?, status?, page?, limit? }]
|
|
2065
|
+
messageBrokers.messages.getStats [{ product, env, brokerTag }]
|
|
2066
|
+
messageBrokers.messages.getDashboard [{ product, env, brokerTag }]
|
|
2067
|
+
messageBrokers.messages.getDeadLetters [{ product, env, brokerTag, topicTag?, limit? }]
|
|
2068
|
+
messageBrokers.replayEvent [{ product, env, eventId, force? }]
|
|
2069
|
+
messageBrokers.reprocessDLQ [{ product, env, brokerTag, topicTag?, messageIds?, limit? }]
|
|
2070
|
+
messageBrokers.checkIdempotency [{ product, env, brokerTag, idempotency_key }]
|
|
2071
|
+
`.trim(),
|
|
2072
|
+
logs: `
|
|
2073
|
+
DUCTAPE LOGS
|
|
2074
|
+
|
|
2075
|
+
Logs record every SDK operation — actions, features, database queries, notifications, sessions,
|
|
2076
|
+
brokers, storage, cache hits, graph, vector, and resilience events.
|
|
2077
|
+
|
|
2078
|
+
Fetch logs (runtime — ductape_execute):
|
|
2079
|
+
ductape_execute("logs.fetch", [{
|
|
2080
|
+
app_id?: string, // query by app (requires component: "app")
|
|
2081
|
+
product_id?: string, // query by product (requires component: "product")
|
|
2082
|
+
env?: string,
|
|
2083
|
+
start_date?: string, // ISO 8601
|
|
2084
|
+
end_date?: string,
|
|
2085
|
+
page?: number,
|
|
2086
|
+
limit?: number,
|
|
2087
|
+
}])
|
|
2088
|
+
component "app" or "product" is required — there is no default.
|
|
2089
|
+
When component="app", tag is only valid with type: "actions".
|
|
2090
|
+
|
|
2091
|
+
Log data fields (ILogData):
|
|
2092
|
+
process_id, product_tag, env, type (LogEventType), status, data (encrypted if private key set)
|
|
2093
|
+
message?, parent_tag?, child_tag?, app_id?, action?, method?,
|
|
2094
|
+
cache_tag?, cache_key?, cache_status (true = hit),
|
|
2095
|
+
start, end, latency (auto-calculated from start/end),
|
|
2096
|
+
session_user_id?, session_id?, session_tag?, visitor_id?,
|
|
2097
|
+
ip_address?, language?, data_encrypted?, successful_execution?, failed_execution?
|
|
2098
|
+
|
|
2099
|
+
Log event types (LogEventTypes):
|
|
2100
|
+
notifications | push | email | sms | callbacks | slack | discord |
|
|
2101
|
+
database_actions | actions | functions | storage | webhook | jobs |
|
|
2102
|
+
message_broker | producer | consumer | quota | fallback | database_migration |
|
|
2103
|
+
feature | feature_step | database | graph | session | vector | cache | frontend
|
|
2104
|
+
|
|
2105
|
+
Log statuses: success | fail | waiting | processing
|
|
2106
|
+
|
|
2107
|
+
Encryption:
|
|
2108
|
+
When workspace_private_key is provided at SDK init, each log entry's data field is
|
|
2109
|
+
AES-encrypted client-side before transmission. data_encrypted: true is set on the entry.
|
|
2110
|
+
The backend can decrypt when returning logs to the Workbench.
|
|
2111
|
+
|
|
2112
|
+
Emit a log manually (SDK only — not via MCP):
|
|
2113
|
+
logs.add({ process_id, product_tag, env, type, status, data, message?, ... })
|
|
2114
|
+
logs.publish() → flushes all buffered entries to the API in one call
|
|
2115
|
+
|
|
2116
|
+
Notes:
|
|
2117
|
+
- Every SDK service (storage, broker, feature, graph, notifications, sessions, vector, cache,
|
|
2118
|
+
resilience) emits its own logs automatically — manual add/publish is only needed for custom logs.
|
|
2119
|
+
- language defaults to "typescript" so the backend knows which SDK emitted the entry.
|
|
2120
|
+
- Logs are batched in memory and sent in a single publish() call per operation.
|
|
2121
|
+
`.trim(),
|
|
2122
|
+
};
|
|
2123
|
+
const docsHandler = async (args) => {
|
|
2124
|
+
const key = args.topic.toLowerCase().trim();
|
|
2125
|
+
const doc = DOCS[key];
|
|
2126
|
+
if (!doc) {
|
|
2127
|
+
const available = Object.keys(DOCS).join(', ');
|
|
2128
|
+
return {
|
|
2129
|
+
content: [{ type: 'text', text: `Unknown topic "${args.topic}". Available topics: ${available}` }],
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
2132
|
+
return { content: [{ type: 'text', text: doc }] };
|
|
2133
|
+
};
|
|
917
2134
|
const cliInputSchema = z.object({
|
|
918
2135
|
command: z.string().describe('The ductape CLI command to run, without the leading "ductape" word. ' +
|
|
919
2136
|
'Examples: "products list", "products create --name \\"My Product\\" --tag my-product", ' +
|
|
@@ -1154,6 +2371,17 @@ async function main() {
|
|
|
1154
2371
|
'unless the chosen variant explicitly marks them required.',
|
|
1155
2372
|
inputSchema: schemaInputSchema,
|
|
1156
2373
|
}, schemaHandler);
|
|
2374
|
+
server.registerTool('ductape_docs', {
|
|
2375
|
+
title: 'Ductape Feature Docs',
|
|
2376
|
+
description: 'Look up detailed documentation for a specific Ductape SDK feature.\n\n' +
|
|
2377
|
+
'Call this when you need guidance on how a feature works before using it — ' +
|
|
2378
|
+
'especially for features that require configuration decisions (tier, isolation level, ' +
|
|
2379
|
+
'index strategy, operation types) that should be confirmed with the user first.\n\n' +
|
|
2380
|
+
'Available topics: transactions, presave, triggers, aggregations, migrations, indexes, performance, actions, ' +
|
|
2381
|
+
'graphs, storage, cloud, vector, warehouse, secrets, apps, products, sessions, caches, ' +
|
|
2382
|
+
'notifications, resilience, features, events, logs',
|
|
2383
|
+
inputSchema: docsInputSchema,
|
|
2384
|
+
}, docsHandler);
|
|
1157
2385
|
server.registerTool('ductape_cli', {
|
|
1158
2386
|
title: 'Ductape CLI',
|
|
1159
2387
|
description: 'Run a Ductape CLI command for administrative operations.\n\n' +
|
|
@@ -1212,6 +2440,32 @@ async function main() {
|
|
|
1212
2440
|
' Supported service/component pairs include: rds→database, aurora→database, gcs→storage, s3→storage,\n' +
|
|
1213
2441
|
' azure-blob→storage, cloud-sql→database, neptune→graph, opensearch→vector. Attempting an unsupported pair\n' +
|
|
1214
2442
|
' will return an error; do not retry with a different tier — report the limitation to the user.\n' +
|
|
2443
|
+
' - Atlas (MongoDB Atlas) import flow — service identifier is "atlas-cluster" (required, not optional):\n' +
|
|
2444
|
+
' Step 1 — discover the cluster name:\n' +
|
|
2445
|
+
' ductape_cli("cloud resources list -f /tmp/atlas-list.json --json")\n' +
|
|
2446
|
+
' File: {"cloud": "<atlas-connection-tag>", "service": "atlas-cluster"}\n' +
|
|
2447
|
+
' Returns a list of clusters; note the "name" field (this is your resource identifier).\n' +
|
|
2448
|
+
' Step 2 — check if the database component already exists:\n' +
|
|
2449
|
+
' ductape_cli("resources databases list <product_tag> --json")\n' +
|
|
2450
|
+
' If a component already uses the same Atlas cluster, do NOT re-import — instead update it:\n' +
|
|
2451
|
+
' ductape_execute("databases.updateDatabase", [product_tag, db_tag, { envs: [...updated envs...] }])\n' +
|
|
2452
|
+
' Add or change the dbName in the env\'s connection_url to switch databases on the same cluster.\n' +
|
|
2453
|
+
' Step 3 — import (only if no existing component uses this cluster):\n' +
|
|
2454
|
+
' Use import-persist-all with one entry per product env. Required fields per entry:\n' +
|
|
2455
|
+
' cloud (connection tag), service: "atlas-cluster", type: "databases",\n' +
|
|
2456
|
+
' product, component (new tag), env, resource (cluster name from Step 1),\n' +
|
|
2457
|
+
' dbName (the MongoDB database name to connect to — required for Atlas).\n' +
|
|
2458
|
+
' Example: [{"cloud":"atlas-tag","service":"atlas-cluster","type":"databases",\n' +
|
|
2459
|
+
' "product":"my-product","component":"core-db","env":"snd","resource":"Cluster0","dbName":"myapp_snd"},\n' +
|
|
2460
|
+
' {"cloud":"atlas-tag","service":"atlas-cluster","type":"databases",\n' +
|
|
2461
|
+
' "product":"my-product","component":"core-db","env":"prd","resource":"Cluster0","dbName":"myapp_prd"}]\n' +
|
|
2462
|
+
' - Message broker / event broker import:\n' +
|
|
2463
|
+
' CLI accepts these aliases for the messageBrokers module: events, event, broker, brokers, message-brokers.\n' +
|
|
2464
|
+
' List existing brokers: ductape_cli("resources events list <product_tag> --json")\n' +
|
|
2465
|
+
' GCP Pub/Sub service identifier is "pubsub". AWS SQS is "sqs". Azure Service Bus is "servicebus".\n' +
|
|
2466
|
+
' Message brokers are import-only (no provision-persist). Import flow is the same as storage.\n' +
|
|
2467
|
+
' type field = "messageBrokers" (not "messagebrokers" or "events").\n' +
|
|
2468
|
+
' After importing, create topics via: ductape_execute("messageBrokers.topics.create", [product_tag, data])\n' +
|
|
1215
2469
|
' - Listing workspaces, products, secrets\n' +
|
|
1216
2470
|
' - Linking a project folder: "link --product <tag> --env <slug>"\n' +
|
|
1217
2471
|
' - Syncing sessions/notifications/events: "apply" or "apply sessions" etc.\n' +
|
|
@@ -1230,6 +2484,7 @@ async function main() {
|
|
|
1230
2484
|
server.tool('ductape_generate_payload', payloadGenerateInputSchema.shape, payloadGenerateHandler);
|
|
1231
2485
|
server.tool('ductape_generate_snippet', snippetGenerateInputSchema.shape, snippetGenerateHandler);
|
|
1232
2486
|
server.tool('ductape_schema', schemaInputSchema.shape, schemaHandler);
|
|
2487
|
+
server.tool('ductape_docs', docsInputSchema.shape, docsHandler);
|
|
1233
2488
|
server.tool('ductape_cli', cliInputSchema.shape, cliHandler);
|
|
1234
2489
|
}
|
|
1235
2490
|
else {
|