@ductape/mcp 0.1.50 → 0.1.52
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 +167 -129
- package/dist/proxy-client.d.ts +1 -0
- package/dist/proxy-client.d.ts.map +1 -1
- package/package.json +1 -1
- package/scripts/check-frontend-analytics-guidance.mjs +22 -1
- package/src/index.ts +188 -129
- package/src/proxy-client.ts +1 -0
package/dist/index.js
CHANGED
|
@@ -932,7 +932,7 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
932
932
|
"storage.create", "storage.update"
|
|
933
933
|
|
|
934
934
|
When to use this tool:
|
|
935
|
-
- Before
|
|
935
|
+
- Before constructing an administrative asset file for ductape_cli or Workbench — use this to discover required fields
|
|
936
936
|
- To enumerate valid enum values (e.g. which DatabaseTypes or AuthTypes are accepted)
|
|
937
937
|
- To understand nested object shapes without reading SDK docs
|
|
938
938
|
`;
|
|
@@ -973,6 +973,9 @@ const payloadGenerateInputSchema = z.object({
|
|
|
973
973
|
'For messaging: { broker: "broker_tag", topic?: "topic_tag" }.'),
|
|
974
974
|
include_session: z.boolean().optional().default(true).describe('Include a session placeholder inside the generated input object. ' +
|
|
975
975
|
'The placeholder is named "<session_tag_token>" to indicate it expects the runtime JWT, not the tag name.'),
|
|
976
|
+
execution_context: z.enum(['user', 'delegated', 'system']).optional().default('user').describe('Actor intent for this runtime operation. "user" means an active request initiated by the authenticated user; ' +
|
|
977
|
+
'"delegated" means work acting on behalf of a user with a short-lived delegated identity or application-owned ' +
|
|
978
|
+
'immutable actor context; "system" means intentionally unattributed background work. This drives session warnings.'),
|
|
976
979
|
include_cache: z.boolean().optional().default(true).describe('Include the cache tag inside the generated input object so the caller knows which cache to reference for this query.'),
|
|
977
980
|
schema_mode: z.enum(['strict', 'best_effort']).optional().default('best_effort').describe('"strict" — fail if any required field cannot be resolved. ' +
|
|
978
981
|
'"best_effort" — fill what is known, leave unknowns as null/placeholder. Use best_effort when exploring.'),
|
|
@@ -1061,6 +1064,43 @@ function buildSdkInvocationArgs(payload) {
|
|
|
1061
1064
|
...(payload?.cache ? { cache: payload.cache } : {}),
|
|
1062
1065
|
};
|
|
1063
1066
|
}
|
|
1067
|
+
function operationAcceptsSession(operationFamily, method) {
|
|
1068
|
+
const family = operationFamily.toLowerCase();
|
|
1069
|
+
const sessionFamilies = new Set([
|
|
1070
|
+
'action', 'features', 'feature', 'database', 'graph', 'vector', 'storage',
|
|
1071
|
+
'notification', 'messaging', 'broker', 'events', 'event', 'quota', 'fallback',
|
|
1072
|
+
]);
|
|
1073
|
+
if (!sessionFamilies.has(family))
|
|
1074
|
+
return false;
|
|
1075
|
+
return !['consume', 'status', 'check', 'fetch', 'list'].includes(method.toLowerCase());
|
|
1076
|
+
}
|
|
1077
|
+
function addSessionAwarenessMetadata(generated, args) {
|
|
1078
|
+
const acceptsSession = operationAcceptsSession(args.operation_family, args.method);
|
|
1079
|
+
const executionContext = args.execution_context ?? 'user';
|
|
1080
|
+
const payloadHasSession = Boolean(generated?.payload?.session || generated?.payload?.input?.session);
|
|
1081
|
+
const sessionRequested = args.include_session !== false;
|
|
1082
|
+
const warnings = [];
|
|
1083
|
+
if (acceptsSession && executionContext !== 'system' && (!sessionRequested || !payloadHasSession)) {
|
|
1084
|
+
warnings.push(`Session attribution is missing for a ${executionContext}-context operation. ` +
|
|
1085
|
+
'Pass the original full session token for immediate user work, or an approved delegated identity/immutable actor context for delayed work.');
|
|
1086
|
+
}
|
|
1087
|
+
if (executionContext === 'system' && payloadHasSession) {
|
|
1088
|
+
warnings.push('This operation is marked system-context but contains a session placeholder. Remove it unless the work is actually user/delegated.');
|
|
1089
|
+
}
|
|
1090
|
+
generated.meta = {
|
|
1091
|
+
...(generated?.meta ?? {}),
|
|
1092
|
+
session_awareness: {
|
|
1093
|
+
accepts_session: acceptsSession,
|
|
1094
|
+
execution_context: executionContext,
|
|
1095
|
+
appears_user_context: executionContext === 'user',
|
|
1096
|
+
session_requested: sessionRequested,
|
|
1097
|
+
session_present_in_payload: payloadHasSession,
|
|
1098
|
+
intentionally_system_context: executionContext === 'system',
|
|
1099
|
+
warnings,
|
|
1100
|
+
},
|
|
1101
|
+
};
|
|
1102
|
+
return generated;
|
|
1103
|
+
}
|
|
1064
1104
|
function buildTypeScriptSnippet(payload, operationFamily, method) {
|
|
1065
1105
|
const callPath = resolveSdkCallPath(operationFamily, method);
|
|
1066
1106
|
const invocationArgs = buildSdkInvocationArgs(payload);
|
|
@@ -1405,33 +1445,9 @@ DUCTAPE DATABASE MIGRATIONS
|
|
|
1405
1445
|
|
|
1406
1446
|
Migrations are versioned SQL/NoSQL schema change scripts managed per database component.
|
|
1407
1447
|
|
|
1408
|
-
Create
|
|
1409
|
-
ductape_execute("databases.migration.create", [{
|
|
1410
|
-
product: "my-product",
|
|
1411
|
-
database: "core-db",
|
|
1412
|
-
data: {
|
|
1413
|
-
name: "add users table",
|
|
1414
|
-
tag: "001-add-users",
|
|
1415
|
-
value: {
|
|
1416
|
-
up: ["CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT NOT NULL UNIQUE)"],
|
|
1417
|
-
down: ["DROP TABLE users"],
|
|
1418
|
-
},
|
|
1419
|
-
},
|
|
1420
|
-
}])
|
|
1421
|
-
|
|
1422
|
-
Run migrations (applies all pending):
|
|
1423
|
-
ductape_execute("databases.migration.run", [migrations, { env: "prd" }])
|
|
1424
|
-
|
|
1425
|
-
Rollback:
|
|
1426
|
-
ductape_execute("databases.migration.rollback", [migrations, 1]) // roll back 1
|
|
1427
|
-
|
|
1428
|
-
Check status:
|
|
1429
|
-
ductape_execute("databases.migration.status", [migrations])
|
|
1430
|
-
|
|
1431
|
-
History:
|
|
1432
|
-
ductape_execute("databases.migration.history", [])
|
|
1448
|
+
Create migrations through the project migration files and ductape_cli, never ductape_execute.
|
|
1433
1449
|
|
|
1434
|
-
|
|
1450
|
+
Use the access-key administrative CLI for running, rolling back, and inspecting migrations:
|
|
1435
1451
|
ductape_cli("db migrate") // run pending
|
|
1436
1452
|
ductape_cli("db migrate rollback") // roll back last
|
|
1437
1453
|
ductape_cli("db migrate rollback -n 3")
|
|
@@ -1441,25 +1457,13 @@ MongoDB: migrations run as raw Mongo shell commands in the up/down arrays.
|
|
|
1441
1457
|
indexes: `
|
|
1442
1458
|
DUCTAPE DATABASE INDEXES
|
|
1443
1459
|
|
|
1444
|
-
Create
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
{ unique: true, name: "idx_email_unique" } // options (optional)
|
|
1449
|
-
])
|
|
1460
|
+
Create or update indexes through migration files and ductape_cli("db migrate"), not the
|
|
1461
|
+
publishable-key runtime proxy. Example definitions may use ["field1", "field2"] with
|
|
1462
|
+
{ unique: true, name: "idx_email_unique" }, or ordered fields such as
|
|
1463
|
+
[{ field: "createdAt", order: "DESC" }, { field: "userId", order: "ASC" }].
|
|
1450
1464
|
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
"orders",
|
|
1454
|
-
[{ field: "createdAt", order: "DESC" }, { field: "userId", order: "ASC" }],
|
|
1455
|
-
{ name: "idx_orders_date_user" }
|
|
1456
|
-
])
|
|
1457
|
-
|
|
1458
|
-
Drop an index:
|
|
1459
|
-
ductape_execute("databases.schema.dropIndex", ["collection_name", "index_name"])
|
|
1460
|
-
|
|
1461
|
-
List indexes on a collection:
|
|
1462
|
-
ductape_execute("databases.schema.indexes", ["collection_name"])
|
|
1465
|
+
Drop and inspect indexes through migration/CLI tooling or the database administration surface,
|
|
1466
|
+
not ductape_execute.
|
|
1463
1467
|
|
|
1464
1468
|
Performance guidance:
|
|
1465
1469
|
- Index fields used in WHERE clauses, JOIN conditions, and ORDER BY.
|
|
@@ -1503,18 +1507,7 @@ DUCTAPE DATABASE ACTIONS
|
|
|
1503
1507
|
A database action is a saved query or mutation (SQL string or NoSQL command) stored
|
|
1504
1508
|
on the Ductape product and executed by tag at runtime.
|
|
1505
1509
|
|
|
1506
|
-
Create
|
|
1507
|
-
ductape_execute("databases.action.create", [{
|
|
1508
|
-
product: "my-product",
|
|
1509
|
-
database: "core-db",
|
|
1510
|
-
data: {
|
|
1511
|
-
tag: "get-active-users",
|
|
1512
|
-
name: "Get active users",
|
|
1513
|
-
description: "Returns all users with status=active",
|
|
1514
|
-
type: "sql", // "sql" or "nosql"
|
|
1515
|
-
query: "SELECT * FROM users WHERE status = :status",
|
|
1516
|
-
},
|
|
1517
|
-
}])
|
|
1510
|
+
Create/update actions in Workbench (administrative access), never through ductape_execute.
|
|
1518
1511
|
|
|
1519
1512
|
Dispatch an action at runtime:
|
|
1520
1513
|
→ CALL ductape_generate_payload FIRST to get the canonical input shape.
|
|
@@ -1529,10 +1522,9 @@ Dispatch an action at runtime:
|
|
|
1529
1522
|
List actions for a database:
|
|
1530
1523
|
ductape_execute("databases.action.list", ["database_tag"])
|
|
1531
1524
|
|
|
1532
|
-
Fetch
|
|
1525
|
+
Fetch:
|
|
1533
1526
|
ductape_execute("databases.action.fetch", ["action_tag"])
|
|
1534
|
-
|
|
1535
|
-
ductape_execute("databases.action.delete", ["action_tag"])
|
|
1527
|
+
Update/delete are administrative and must be performed in Workbench.
|
|
1536
1528
|
|
|
1537
1529
|
Actions are the preferred way to encapsulate complex or reused queries — they can be
|
|
1538
1530
|
scheduled, dispatched with retries, and audited via logs.
|
|
@@ -1790,16 +1782,8 @@ DUCTAPE SECRETS
|
|
|
1790
1782
|
Secrets are workspace-level encrypted key-value pairs. They are referenced in resource configs,
|
|
1791
1783
|
connection URLs, and any string field using the $Secret{KEY_NAME} syntax.
|
|
1792
1784
|
|
|
1793
|
-
Create
|
|
1794
|
-
|
|
1795
|
-
key: "STRIPE_API_KEY",
|
|
1796
|
-
value: "sk_live_...", // plaintext — encrypted AES-256-GCM client-side before send
|
|
1797
|
-
description: "Stripe live key",
|
|
1798
|
-
token_type: "api", // "api" | "password" | "certificate"
|
|
1799
|
-
scope: ["my-product"], // which products can read this secret
|
|
1800
|
-
envs: ["prd"], // which env slugs can read this secret
|
|
1801
|
-
expires_at: 1800000000, // optional epoch ms
|
|
1802
|
-
}])
|
|
1785
|
+
Create/update secrets with ductape_cli or Workbench (administrative access), never ductape_execute.
|
|
1786
|
+
Definition fields include key, value, description, token_type, scope, envs, and expires_at.
|
|
1803
1787
|
The server never receives the plaintext value. Encryption uses the workspace private key.
|
|
1804
1788
|
|
|
1805
1789
|
Fetch / resolve:
|
|
@@ -1814,10 +1798,7 @@ $Secret{} reference syntax:
|
|
|
1814
1798
|
- The in-memory cache stores the encrypted form only; decryption happens on each cache hit.
|
|
1815
1799
|
- Cache TTL: 5 minutes. Clear with: secrets.clearCache()
|
|
1816
1800
|
|
|
1817
|
-
Lifecycle:
|
|
1818
|
-
ductape_execute("secrets.revoke", ["KEY"]) → disables without deleting (recoverable)
|
|
1819
|
-
ductape_execute("secrets.delete", ["KEY"]) → permanent deletion (irreversible)
|
|
1820
|
-
ductape_execute("secrets.update", ["KEY", { value: "new_value", expires_at: ... }])
|
|
1801
|
+
Lifecycle mutations (revoke/delete/update) are administrative: use ductape_cli or Workbench.
|
|
1821
1802
|
|
|
1822
1803
|
List all secrets (keys only — values are not returned in list):
|
|
1823
1804
|
ductape_execute("secrets.list", [])
|
|
@@ -1867,9 +1848,7 @@ Import from a file:
|
|
|
1867
1848
|
ductape_cli("apps import <file.json> -t postman|openapi")
|
|
1868
1849
|
Supports Postman v2.1 collection and OpenAPI 3.0 spec.
|
|
1869
1850
|
|
|
1870
|
-
Manage environments (base URLs per stage)
|
|
1871
|
-
ductape_execute("app.environments.create", [app_tag, { slug, env_name, base_url }])
|
|
1872
|
-
ductape_execute("app.environments.list", [app_tag])
|
|
1851
|
+
Manage environments (base URLs per stage) in Workbench; this currently has no CLI command.
|
|
1873
1852
|
|
|
1874
1853
|
Discover apps in a product and their actions (ALWAYS do this before writing any ctx.api.run call):
|
|
1875
1854
|
Step 1 — list apps connected to the product:
|
|
@@ -1886,16 +1865,8 @@ Discover apps in a product and their actions (ALWAYS do this before writing any
|
|
|
1886
1865
|
app resources (not for runtime input — use actions.fetch or ductape_generate_payload for that)
|
|
1887
1866
|
NEVER assume action input field names. Always fetch the action definition first.
|
|
1888
1867
|
|
|
1889
|
-
Manage actions (individual API endpoints)
|
|
1890
|
-
|
|
1891
|
-
tag, name, resource: "/users/{id}", method: "GET"|"POST"|"PUT"|"PATCH"|"DELETE",
|
|
1892
|
-
body?: { fieldName: { type, required? } },
|
|
1893
|
-
params?: { id: { type: "string" } },
|
|
1894
|
-
query?: { filter: { type: "string" } },
|
|
1895
|
-
headers?: { Authorization: { type: "string" } },
|
|
1896
|
-
response?: { status_code: 200, success: true, body: { ... }, response_format: "json" },
|
|
1897
|
-
}])
|
|
1898
|
-
ductape_execute("actions.update", [app_tag, action_tag, data])
|
|
1868
|
+
Manage actions (individual API endpoints) in Workbench or import an OpenAPI/Postman file.
|
|
1869
|
+
Creation/update are administrative and must never use ductape_execute.
|
|
1899
1870
|
ductape_execute("actions.list", [app_tag])
|
|
1900
1871
|
ductape_execute("actions.fetch", [app_tag, action_tag])
|
|
1901
1872
|
|
|
@@ -1916,16 +1887,14 @@ Run an action at runtime:
|
|
|
1916
1887
|
|
|
1917
1888
|
Auth schemes (how the app authenticates outbound requests):
|
|
1918
1889
|
Setup types: header | bearer | basic | oauth2 | apikey
|
|
1919
|
-
|
|
1890
|
+
Configure auth in Workbench (administrative).
|
|
1920
1891
|
ductape_execute("auths.list", [app_tag])
|
|
1921
1892
|
|
|
1922
1893
|
Webhooks (inbound events from the external service):
|
|
1923
|
-
|
|
1924
|
-
ductape_execute("webhooks.events.create", [app_tag, { tag, name, selector, description, sample }])
|
|
1894
|
+
Configure webhooks and webhook events in Workbench (administrative).
|
|
1925
1895
|
|
|
1926
1896
|
Variables (per-env mutable values) and Constants (fixed values):
|
|
1927
|
-
|
|
1928
|
-
ductape_execute("app.constants.create", [app_tag, { key, value }])
|
|
1897
|
+
Configure variables and constants in Workbench (administrative).
|
|
1929
1898
|
|
|
1930
1899
|
Connecting an app to a product (after creation):
|
|
1931
1900
|
NOTE: All product.* module methods require the access key and CANNOT use ductape_execute.
|
|
@@ -1933,14 +1902,13 @@ Connecting an app to a product (after creation):
|
|
|
1933
1902
|
|
|
1934
1903
|
FULL FLOW to make an app callable from a product:
|
|
1935
1904
|
1. Create the app: ductape_cli("apps create --name \\"Service Name\\" --description \\"...\\"")
|
|
1936
|
-
2. Add environments:
|
|
1937
|
-
3. Configure auth:
|
|
1938
|
-
4. Define actions:
|
|
1905
|
+
2. Add environments: Workbench
|
|
1906
|
+
3. Configure auth: Workbench
|
|
1907
|
+
4. Define actions: Workbench
|
|
1939
1908
|
OR import: ductape_cli("apps import <file.json> -t postman|openapi")
|
|
1940
1909
|
5. Connect to product: Requires the product_id (from ductape_cli("products get --tag <tag> --json")).
|
|
1941
1910
|
There is no CLI command for this step — the SDK product.apps.add method requires
|
|
1942
|
-
an access key which only the backend can provide.
|
|
1943
|
-
admin-authenticated context, or connect via the Workbench UI.
|
|
1911
|
+
an access key which only the backend can provide. Connect via Workbench.
|
|
1944
1912
|
6. Verify: ductape_cli("products get --tag <product_tag> --json") → check apps[] contains the app
|
|
1945
1913
|
ductape_execute("actions.list", [app_tag]) → verify actions are registered
|
|
1946
1914
|
`.trim(),
|
|
@@ -2015,8 +1983,8 @@ IMPORTANT — selector must be "$Session{fieldName}" format:
|
|
|
2015
1983
|
CORRECT: selector: "$Session{playerId}"
|
|
2016
1984
|
INCORRECT: selector: "playerId" ← WILL FAIL with "Selector should be in the format $Session{...}{key}"
|
|
2017
1985
|
|
|
2018
|
-
Example (
|
|
2019
|
-
|
|
1986
|
+
Example definition (configure via declarative apply or Workbench, not ductape_execute):
|
|
1987
|
+
{
|
|
2020
1988
|
tag: "player-session",
|
|
2021
1989
|
name: "Player Session",
|
|
2022
1990
|
expiry: 24,
|
|
@@ -2028,7 +1996,7 @@ Example (game product — player identity in JWT):
|
|
|
2028
1996
|
role: "player",
|
|
2029
1997
|
accountId: "acct_xyz",
|
|
2030
1998
|
},
|
|
2031
|
-
}
|
|
1999
|
+
}
|
|
2032
2000
|
|
|
2033
2001
|
Runtime — create a session (sign a JWT):
|
|
2034
2002
|
→ CALL ductape_generate_payload FIRST (operation_family="session", method="start", targets={tag})
|
|
@@ -2068,10 +2036,33 @@ Token format: "session_tag:jwt_token" — pass this format verbatim wherever ses
|
|
|
2068
2036
|
JWT is signed with the product private key (not a symmetric shared secret).
|
|
2069
2037
|
|
|
2070
2038
|
SESSION PROPAGATION
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2039
|
+
Extract the original full "session_tag:jwt" token once at the HTTP/transport boundary and retain
|
|
2040
|
+
it separately from verified/decoded claims. Claims are authorization data; they are not a token.
|
|
2041
|
+
Store a request ActorContext in a NestJS request-scoped provider or AsyncLocalStorage:
|
|
2042
|
+
type ActorContext =
|
|
2043
|
+
| { kind: "user"; session: string; actorId: string }
|
|
2044
|
+
| { kind: "delegated"; delegatedIdentity: string; actorId: string; initiatedAt: string }
|
|
2045
|
+
| { kind: "system"; reason: string };
|
|
2046
|
+
Pass ActorContext explicitly through service boundaries. At each immediate Ductape runtime call,
|
|
2047
|
+
pass actor.kind === "user" ? actor.session : the SDK-approved delegated identity. Put only
|
|
2048
|
+
non-secret actor metadata (actorId, kind, initiatedAt, correlationId) in Event envelopes.
|
|
2049
|
+
Feature execution should receive the same explicit actor classification. Never log, serialize
|
|
2050
|
+
into business payloads, or attach the raw session/refresh token to traces or error messages.
|
|
2051
|
+
|
|
2052
|
+
SECURITY BOUNDARY FOR DURABLE WORK
|
|
2053
|
+
The current SDK accepts a session string on dispatch, but it does not expose an immutable
|
|
2054
|
+
attribution snapshot API, an expired-token attribution contract, or a general delegated-session
|
|
2055
|
+
issuer. Therefore do NOT persist reusable JWTs indefinitely or assume an expired initiating token
|
|
2056
|
+
will remain valid when delayed work executes. Pass the full token only for immediate work within
|
|
2057
|
+
its validity window. For delayed/recurring work, classify execution as system-context, or use an
|
|
2058
|
+
application-owned immutable actor-context envelope plus a short-lived delegated identity issued
|
|
2059
|
+
by an explicitly approved auth design. Until the SDK provides that design, actor metadata is for
|
|
2060
|
+
audit/correlation and must not be treated as authorization.
|
|
2061
|
+
|
|
2062
|
+
Distinguish contexts deliberately:
|
|
2063
|
+
user — active request; original valid full token is required when the operation accepts it
|
|
2064
|
+
delegated — delayed/on-behalf-of work; approved short-lived credential + immutable actor metadata
|
|
2065
|
+
system — scheduler/maintenance with no user authority; omit session and record a reason
|
|
2075
2066
|
|
|
2076
2067
|
Backend propagation attributes component activity to an actor, but it does not record frontend
|
|
2077
2068
|
pageviews, navigation, UI intent, funnels, or client failures. Continue immediately with
|
|
@@ -2082,14 +2073,12 @@ DUCTAPE CACHES
|
|
|
2082
2073
|
|
|
2083
2074
|
Caches are product-level Redis (or in-memory) stores for temporary key-value data with optional TTL.
|
|
2084
2075
|
|
|
2085
|
-
Registration (admin — ductape_cli
|
|
2076
|
+
Registration (admin — ductape_cli):
|
|
2086
2077
|
ductape_cli("resources caches create -f cache.json")
|
|
2087
2078
|
File: { name, tag, description?, expiry: <milliseconds> }
|
|
2088
2079
|
No type or envs — Ductape manages the store infrastructure.
|
|
2089
2080
|
expiry is in MILLISECONDS: 3600000 = 1 hour, 86400000 = 1 day, 604800000 = 1 week.
|
|
2090
2081
|
|
|
2091
|
-
SDK: ductape_execute("caches.create", [product_tag, { name, tag, description?, expiry: 3600000 }])
|
|
2092
|
-
|
|
2093
2082
|
Operations:
|
|
2094
2083
|
caches.set [{ product, cache, key, value: string, expiry?: string (ISO 8601), env }]
|
|
2095
2084
|
expiry is an ABSOLUTE TIMESTAMP (not a duration).
|
|
@@ -2137,20 +2126,20 @@ DUCTAPE NOTIFICATIONS
|
|
|
2137
2126
|
|
|
2138
2127
|
Notifications send messages across multiple channels: email, SMS, push, or HTTP callback.
|
|
2139
2128
|
|
|
2140
|
-
Create a notification
|
|
2141
|
-
|
|
2129
|
+
Create a notification through declarative apply or Workbench (administrative), using:
|
|
2130
|
+
{
|
|
2142
2131
|
tag: "welcome-email",
|
|
2143
2132
|
name: "Welcome Email",
|
|
2144
2133
|
type: "email", // optional hint; actual channels configured per env
|
|
2145
|
-
}
|
|
2134
|
+
}
|
|
2146
2135
|
|
|
2147
|
-
Create a message template:
|
|
2148
|
-
|
|
2136
|
+
Create a message template through declarative apply or Workbench:
|
|
2137
|
+
{
|
|
2149
2138
|
tag: "welcome-email:default", // format: "notification_tag:message_tag"
|
|
2150
2139
|
notification: "welcome-email",
|
|
2151
2140
|
subject: { template: "Welcome, {{name}}!", data: { name: "" } },
|
|
2152
2141
|
body: { template: "Hi {{name}}, thanks for signing up.", data: { name: "" } },
|
|
2153
|
-
}
|
|
2142
|
+
}
|
|
2154
2143
|
|
|
2155
2144
|
Send at runtime (one channel at a time):
|
|
2156
2145
|
→ CALL ductape_generate_payload FIRST (operation_family="notification", method="email.send")
|
|
@@ -2188,8 +2177,15 @@ DUCTAPE RESILIENCE
|
|
|
2188
2177
|
Resilience covers three mechanisms: quotas (rate-limited provider pools), fallbacks (automatic
|
|
2189
2178
|
provider switching), and healthchecks (continuous probe monitoring with failure actions).
|
|
2190
2179
|
|
|
2180
|
+
CONFIGURATION BOUNDARY
|
|
2181
|
+
Quotas, fallbacks, and health checks are administrative product configuration. Configure them in
|
|
2182
|
+
Workbench (or a future access-key administrative tool explicitly documented for the asset).
|
|
2183
|
+
Never route administrative create/update methods through ductape_execute: its publishable-key
|
|
2184
|
+
runtime proxy will fail.
|
|
2185
|
+
|
|
2191
2186
|
QUOTAS — rate-limited multi-provider pools:
|
|
2192
|
-
|
|
2187
|
+
Workbench definition shape:
|
|
2188
|
+
{
|
|
2193
2189
|
tag: "sms-quota",
|
|
2194
2190
|
name: "SMS Provider Pool",
|
|
2195
2191
|
input: { to: { type: "string", required: true }, message: { type: "string" } },
|
|
@@ -2203,7 +2199,7 @@ QUOTAS — rate-limited multi-provider pools:
|
|
|
2203
2199
|
input: { "body:to": "$Input{to}", "body:body": "$Input{message}" },
|
|
2204
2200
|
output: {} },
|
|
2205
2201
|
],
|
|
2206
|
-
}
|
|
2202
|
+
}
|
|
2207
2203
|
Providers are tried in order until quota is not exhausted.
|
|
2208
2204
|
quotas.run [{ product, env, tag, input }] ← CALL ductape_generate_payload FIRST
|
|
2209
2205
|
quotas.dispatch [{ product, env, tag, input, schedule? }]
|
|
@@ -2211,12 +2207,13 @@ QUOTAS — rate-limited multi-provider pools:
|
|
|
2211
2207
|
FALLBACKS — automatic provider switching on failure:
|
|
2212
2208
|
Same schema as quotas but options are ordered: primary first, then fallback(s).
|
|
2213
2209
|
Primary is used first; on failure, the next provider is tried automatically.
|
|
2214
|
-
|
|
2210
|
+
Configure in Workbench: { tag, name, input: { ... }, options: [...] }
|
|
2215
2211
|
fallback.run [{ product, env, tag, input }] ← CALL ductape_generate_payload FIRST
|
|
2216
2212
|
fallback.dispatch [{ product, env, tag, input, schedule? }]
|
|
2217
2213
|
|
|
2218
2214
|
HEALTHCHECKS — continuous probe with failure notifications:
|
|
2219
|
-
|
|
2215
|
+
Workbench definition shape:
|
|
2216
|
+
{
|
|
2220
2217
|
tag: "payment-health",
|
|
2221
2218
|
name: "Payment Service Health",
|
|
2222
2219
|
probe: { type: "app", app: "stripe-app", event: "ping" },
|
|
@@ -2228,7 +2225,7 @@ HEALTHCHECKS — continuous probe with failure notifications:
|
|
|
2228
2225
|
channels: { email: { recipients: ["ops@example.com"] } } }],
|
|
2229
2226
|
webhooks: [{ url: "https://hooks.example.com/alert", method: "POST" }],
|
|
2230
2227
|
},
|
|
2231
|
-
}
|
|
2228
|
+
}
|
|
2232
2229
|
health.run [{ product, env, tag }] → triggers an immediate probe
|
|
2233
2230
|
health.check [{ product, env, tag }] → same as run
|
|
2234
2231
|
health.status [{ product, env, tag }] → current health status
|
|
@@ -2238,6 +2235,23 @@ Failure actions: notification channels, HTTP webhooks, and/or message broker emi
|
|
|
2238
2235
|
be configured simultaneously on the same healthcheck.
|
|
2239
2236
|
Input template references: $Input{field} → maps declared input to the probe's action input.
|
|
2240
2237
|
Provider status: available | unavailable
|
|
2238
|
+
|
|
2239
|
+
DECISION MATRIX
|
|
2240
|
+
Rate/capacity allocation across providers → quota
|
|
2241
|
+
Equivalent provider after operational failure → fallback
|
|
2242
|
+
Detect failure before routing provider traffic → health check
|
|
2243
|
+
Transient failure of one operation → bounded retry + idempotency policy
|
|
2244
|
+
Multi-step business recovery/compensation → Feature
|
|
2245
|
+
Database atomicity → database transaction, not Feature rollback
|
|
2246
|
+
Scheduled single operation → that component's dispatch
|
|
2247
|
+
Scheduled multi-step process → Feature dispatch
|
|
2248
|
+
|
|
2249
|
+
COMBINING MECHANISMS
|
|
2250
|
+
A health check may keep an unhealthy provider out of a fallback/quota pool; the pool controls
|
|
2251
|
+
provider selection; each operation may use bounded retries and an idempotency key; a Feature
|
|
2252
|
+
coordinates business steps and compensation around those resilient operations. Do not stack them
|
|
2253
|
+
reflexively: each layer needs a distinct failure it owns, bounded retry budgets, and observable
|
|
2254
|
+
terminal behavior. Database transactions remain the atomicity boundary for related DB writes.
|
|
2241
2255
|
`.trim(),
|
|
2242
2256
|
'frontend-analytics': `
|
|
2243
2257
|
DUCTAPE FRONTEND PRODUCT ANALYTICS AND SESSION ACTIVITY
|
|
@@ -2245,10 +2259,12 @@ DUCTAPE FRONTEND PRODUCT ANALYTICS AND SESSION ACTIVITY
|
|
|
2245
2259
|
Frontend analytics and backend session attribution are complementary; neither replaces the other.
|
|
2246
2260
|
|
|
2247
2261
|
BACKEND OPERATION ATTRIBUTION
|
|
2248
|
-
|
|
2249
|
-
notification, storage, graph, vector, action, and other runtime operation
|
|
2250
|
-
This attributes server work to the authenticated actor.
|
|
2251
|
-
|
|
2262
|
+
Immediate user-initiated backend work should pass the original full Ductape session token to every
|
|
2263
|
+
database, Event, Feature, notification, storage, graph, vector, action, and other runtime operation
|
|
2264
|
+
that accepts session. This attributes immediate server work to the authenticated actor.
|
|
2265
|
+
For delayed/durable work, do not persist raw JWTs indefinitely. Use system context, or an approved
|
|
2266
|
+
delegated actor design with immutable non-secret actor metadata. Actor metadata supports audit and
|
|
2267
|
+
correlation; it is not authorization. Intentional system/background work should remain sessionless.
|
|
2252
2268
|
|
|
2253
2269
|
FRONTEND PRODUCT ANALYTICS
|
|
2254
2270
|
Use the browser Analytics service for anonymous visits, authenticated page views, navigation,
|
|
@@ -2517,9 +2533,11 @@ STEP 4 — PRESENT the plan and get approval BEFORE writing any code or creating
|
|
|
2517
2533
|
Wait for confirmation before writing code or calling any create tool.
|
|
2518
2534
|
|
|
2519
2535
|
STEP 5 — CREATE missing components (only with user approval)
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2536
|
+
Administrative assets must never be created through ductape_execute. Use ductape_cli for products,
|
|
2537
|
+
apps, supported resources, broker topics, cloud connections, secrets, and declarative apply flows.
|
|
2538
|
+
App actions, auths, Features, quotas, fallbacks, health checks, and other assets for which the CLI
|
|
2539
|
+
has no command must be configured in Workbench. Do not generate an impossible publishable-key
|
|
2540
|
+
create/update call. For a missing database action, configure it in Workbench, then verify it exists.
|
|
2523
2541
|
For a missing child feature, recursively apply this same workflow.
|
|
2524
2542
|
Tell the user what you are about to create before each tool call.
|
|
2525
2543
|
|
|
@@ -2643,7 +2661,11 @@ When you call features.define({ handler }), the handler runs TWICE:
|
|
|
2643
2661
|
|
|
2644
2662
|
Invoke internal application business logic (your own NestJS/backend service code):
|
|
2645
2663
|
→ produce a broker event (ctx.messaging.produce or ductape.events.produce)
|
|
2646
|
-
→
|
|
2664
|
+
→ follow ductape_docs({ topic: "events" }) and use the canonical NestJS decorator:
|
|
2665
|
+
@Events.Consumer({ event: "broker-tag:topic-tag" })
|
|
2666
|
+
async handle(message: MessageShape) { /* injected-service business logic; throw to nack */ }
|
|
2667
|
+
→ DuctapeModule auto-registers the decorated consumer; no manual onModuleInit is needed
|
|
2668
|
+
→ events.consume() remains a supported lower-level alternative for plain TypeScript/Node.js
|
|
2647
2669
|
→ your service method runs with full access to DI, DB transactions, etc.
|
|
2648
2670
|
Do NOT create an App Action just to call your own service over HTTP.
|
|
2649
2671
|
|
|
@@ -3689,6 +3711,22 @@ async function main() {
|
|
|
3689
3711
|
};
|
|
3690
3712
|
const executeHandler = async (args) => {
|
|
3691
3713
|
try {
|
|
3714
|
+
const runtimeMutationMethods = {
|
|
3715
|
+
databases: new Set(['insert', 'update', 'delete', 'upsert']),
|
|
3716
|
+
graph: new Set(['insert', 'update', 'delete']),
|
|
3717
|
+
vector: new Set(['insert', 'upsert', 'upsertOne', 'delete']),
|
|
3718
|
+
sessions: new Set(['revoke']),
|
|
3719
|
+
};
|
|
3720
|
+
const isRuntimeDataMutation = runtimeMutationMethods[args.module]?.has(args.method) === true;
|
|
3721
|
+
const isAdministrativeMutation = (/^migration\./.test(args.method) ||
|
|
3722
|
+
/^schema\.(create|drop|add|remove|update)/.test(args.method) ||
|
|
3723
|
+
/(^|\.)(create|update|delete|configure|add|remove|revoke)$/.test(args.method)) &&
|
|
3724
|
+
!isRuntimeDataMutation;
|
|
3725
|
+
if (isAdministrativeMutation) {
|
|
3726
|
+
throw new Error(`Administrative operation "${args.module}.${args.method}" is blocked in ductape_execute. ` +
|
|
3727
|
+
'Use ductape_cli when that resource is supported by the CLI; otherwise configure it in Workbench. ' +
|
|
3728
|
+
'The runtime proxy uses a publishable key and cannot administer platform assets.');
|
|
3729
|
+
}
|
|
3692
3730
|
const key = args.publishable_key || process.env.DUCTAPE_PUBLISHABLE_KEY;
|
|
3693
3731
|
if (!key) {
|
|
3694
3732
|
throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
|
|
@@ -3735,7 +3773,7 @@ async function main() {
|
|
|
3735
3773
|
if (!key) {
|
|
3736
3774
|
throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
|
|
3737
3775
|
}
|
|
3738
|
-
const result = await generateExecutablePayload({ ...args, publishable_key: key });
|
|
3776
|
+
const result = addSessionAwarenessMetadata(await generateExecutablePayload({ ...args, publishable_key: key }), args);
|
|
3739
3777
|
let text = JSON.stringify(result ?? null, null, 2);
|
|
3740
3778
|
if (args.operation_family === 'database') {
|
|
3741
3779
|
const meta = result?.meta ?? {};
|
|
@@ -3762,7 +3800,7 @@ async function main() {
|
|
|
3762
3800
|
throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
|
|
3763
3801
|
}
|
|
3764
3802
|
ensureSupportedSnippetOperation(args.operation_family, args.method);
|
|
3765
|
-
const generated = await generateExecutablePayload({ ...args, publishable_key: key });
|
|
3803
|
+
const generated = addSessionAwarenessMetadata(await generateExecutablePayload({ ...args, publishable_key: key }), args);
|
|
3766
3804
|
const payload = generated?.payload ?? {};
|
|
3767
3805
|
const snippet = buildSnippet(args.language, payload, args.operation_family, args.method);
|
|
3768
3806
|
return {
|
|
@@ -3915,7 +3953,7 @@ async function main() {
|
|
|
3915
3953
|
' Step 2 — check if the database component already exists:\n' +
|
|
3916
3954
|
' ductape_cli("resources databases list <product_tag> --json")\n' +
|
|
3917
3955
|
' If a component already uses the same Atlas cluster, do NOT re-import — instead update it:\n' +
|
|
3918
|
-
'
|
|
3956
|
+
' use ductape_cli resource update when supported; otherwise update it in Workbench\n' +
|
|
3919
3957
|
' Add or change the dbName in the env\'s connection_url to switch databases on the same cluster.\n' +
|
|
3920
3958
|
' Step 3 — import (only if no existing component uses this cluster):\n' +
|
|
3921
3959
|
' Use import-persist-all with one entry per product env. Required fields per entry:\n' +
|
package/dist/proxy-client.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface IGenerateExecutablePayloadRequest {
|
|
|
15
15
|
method: string;
|
|
16
16
|
targets?: Record<string, unknown>;
|
|
17
17
|
include_session?: boolean;
|
|
18
|
+
execution_context?: 'user' | 'delegated' | 'system';
|
|
18
19
|
include_cache?: boolean;
|
|
19
20
|
schema_mode?: 'strict' | 'best_effort';
|
|
20
21
|
input_hint?: Record<string, unknown>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy-client.d.ts","sourceRoot":"","sources":["../src/proxy-client.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,eAAO,MAAM,YAAY,4BAA4B,CAAC;AAEtD,MAAM,MAAM,SAAS,GACjB,SAAS,GACT,KAAK,GACL,WAAW,GACX,OAAO,GACP,UAAU,GACV,eAAe,GACf,gBAAgB,GAChB,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,QAAQ,GACR,SAAS,GACT,UAAU,GACV,MAAM,GACN,MAAM,GACN,YAAY,GACZ,QAAQ,GACR,UAAU,GACV,SAAS,CAAC;AAUd;;GAEG;AACH,wBAAsB,eAAe,CAAC,CAAC,GAAG,OAAO,EAC/C,eAAe,EAAE,MAAM,EACvB,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,OAAO,EAAO,GACrB,OAAO,CAAC,CAAC,CAAC,CAuBZ;AAED,MAAM,WAAW,iCAAiC;IAChD,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,QAAQ,GAAG,aAAa,CAAC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,kCAAkC;IACjD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AASD,wBAAsB,eAAe,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAYvE;AA4BD,wBAAsB,yBAAyB,CAAC,CAAC,GAAG,kCAAkC,EACpF,OAAO,EAAE,iCAAiC,GACzC,OAAO,CAAC,CAAC,CAAC,CAwBZ"}
|
|
1
|
+
{"version":3,"file":"proxy-client.d.ts","sourceRoot":"","sources":["../src/proxy-client.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,eAAO,MAAM,YAAY,4BAA4B,CAAC;AAEtD,MAAM,MAAM,SAAS,GACjB,SAAS,GACT,KAAK,GACL,WAAW,GACX,OAAO,GACP,UAAU,GACV,eAAe,GACf,gBAAgB,GAChB,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,QAAQ,GACR,SAAS,GACT,UAAU,GACV,MAAM,GACN,MAAM,GACN,YAAY,GACZ,QAAQ,GACR,UAAU,GACV,SAAS,CAAC;AAUd;;GAEG;AACH,wBAAsB,eAAe,CAAC,CAAC,GAAG,OAAO,EAC/C,eAAe,EAAE,MAAM,EACvB,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,OAAO,EAAO,GACrB,OAAO,CAAC,CAAC,CAAC,CAuBZ;AAED,MAAM,WAAW,iCAAiC;IAChD,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IACpD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,QAAQ,GAAG,aAAa,CAAC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,kCAAkC;IACjD,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AASD,wBAAsB,eAAe,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAYvE;AA4BD,wBAAsB,yBAAyB,CAAC,CAAC,GAAG,kCAAkC,EACpF,OAAO,EAAE,iCAAiC,GACzC,OAAO,CAAC,CAAC,CAAC,CAwBZ"}
|
package/package.json
CHANGED
|
@@ -34,4 +34,25 @@ assert.doesNotMatch(
|
|
|
34
34
|
'MCP guidance must not use the obsolete identify({ userId, traits }) signature',
|
|
35
35
|
);
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
const safetyChecks = [
|
|
38
|
+
['admin execute calls removed', /ductape_execute\("[^"\n]*(?:create|update)/],
|
|
39
|
+
['obsolete NestJS consumer removed', /events\.consume in onModuleInit/],
|
|
40
|
+
['canonical NestJS consumer present', /@Events\.Consumer\(\{ event:/],
|
|
41
|
+
['request actor context documented', /AsyncLocalStorage[\s\S]*ActorContext/],
|
|
42
|
+
['raw token logging prohibited', /Never log,[\s\S]*raw session\/refresh token/],
|
|
43
|
+
['durable token expiry semantics documented', /does not expose an immutable[\s\S]*expired-token attribution contract/],
|
|
44
|
+
['frontend durable work prohibits persisted JWTs', /For delayed\/durable work, do not persist raw JWTs indefinitely/],
|
|
45
|
+
['resilience decision matrix present', /DECISION MATRIX[\s\S]*Database atomicity/],
|
|
46
|
+
['payload session-awareness metadata present', /session_awareness:[\s\S]*accepts_session/],
|
|
47
|
+
['execute blocks administrative mutations', /Administrative operation.*is blocked in ductape_execute/],
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
for (const [name, pattern] of safetyChecks) {
|
|
51
|
+
if (name === 'admin execute calls removed' || name === 'obsolete NestJS consumer removed') {
|
|
52
|
+
assert.doesNotMatch(source, pattern, `Unsafe MCP guidance remains: ${name}`);
|
|
53
|
+
} else {
|
|
54
|
+
assert.match(source, pattern, `Missing MCP safety guidance: ${name}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log(`MCP guidance: ${checks.length + 1 + safetyChecks.length} acceptance checks passed`);
|
package/src/index.ts
CHANGED
|
@@ -943,7 +943,7 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
943
943
|
"storage.create", "storage.update"
|
|
944
944
|
|
|
945
945
|
When to use this tool:
|
|
946
|
-
- Before
|
|
946
|
+
- Before constructing an administrative asset file for ductape_cli or Workbench — use this to discover required fields
|
|
947
947
|
- To enumerate valid enum values (e.g. which DatabaseTypes or AuthTypes are accepted)
|
|
948
948
|
- To understand nested object shapes without reading SDK docs
|
|
949
949
|
`;
|
|
@@ -998,6 +998,11 @@ const payloadGenerateInputSchema = z.object({
|
|
|
998
998
|
'Include a session placeholder inside the generated input object. ' +
|
|
999
999
|
'The placeholder is named "<session_tag_token>" to indicate it expects the runtime JWT, not the tag name.'
|
|
1000
1000
|
),
|
|
1001
|
+
execution_context: z.enum(['user', 'delegated', 'system']).optional().default('user').describe(
|
|
1002
|
+
'Actor intent for this runtime operation. "user" means an active request initiated by the authenticated user; ' +
|
|
1003
|
+
'"delegated" means work acting on behalf of a user with a short-lived delegated identity or application-owned ' +
|
|
1004
|
+
'immutable actor context; "system" means intentionally unattributed background work. This drives session warnings.'
|
|
1005
|
+
),
|
|
1001
1006
|
include_cache: z.boolean().optional().default(true).describe(
|
|
1002
1007
|
'Include the cache tag inside the generated input object so the caller knows which cache to reference for this query.'
|
|
1003
1008
|
),
|
|
@@ -1091,6 +1096,53 @@ function buildSdkInvocationArgs(payload: Record<string, unknown>): Record<string
|
|
|
1091
1096
|
};
|
|
1092
1097
|
}
|
|
1093
1098
|
|
|
1099
|
+
function operationAcceptsSession(operationFamily: string, method: string): boolean {
|
|
1100
|
+
const family = operationFamily.toLowerCase();
|
|
1101
|
+
const sessionFamilies = new Set([
|
|
1102
|
+
'action', 'features', 'feature', 'database', 'graph', 'vector', 'storage',
|
|
1103
|
+
'notification', 'messaging', 'broker', 'events', 'event', 'quota', 'fallback',
|
|
1104
|
+
]);
|
|
1105
|
+
if (!sessionFamilies.has(family)) return false;
|
|
1106
|
+
return !['consume', 'status', 'check', 'fetch', 'list'].includes(method.toLowerCase());
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function addSessionAwarenessMetadata(
|
|
1110
|
+
generated: any,
|
|
1111
|
+
args: z.infer<typeof payloadGenerateInputSchema>,
|
|
1112
|
+
): any {
|
|
1113
|
+
const acceptsSession = operationAcceptsSession(args.operation_family, args.method);
|
|
1114
|
+
const executionContext = args.execution_context ?? 'user';
|
|
1115
|
+
const payloadHasSession = Boolean(generated?.payload?.session || generated?.payload?.input?.session);
|
|
1116
|
+
const sessionRequested = args.include_session !== false;
|
|
1117
|
+
const warnings: string[] = [];
|
|
1118
|
+
|
|
1119
|
+
if (acceptsSession && executionContext !== 'system' && (!sessionRequested || !payloadHasSession)) {
|
|
1120
|
+
warnings.push(
|
|
1121
|
+
`Session attribution is missing for a ${executionContext}-context operation. ` +
|
|
1122
|
+
'Pass the original full session token for immediate user work, or an approved delegated identity/immutable actor context for delayed work.',
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
if (executionContext === 'system' && payloadHasSession) {
|
|
1126
|
+
warnings.push(
|
|
1127
|
+
'This operation is marked system-context but contains a session placeholder. Remove it unless the work is actually user/delegated.',
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
generated.meta = {
|
|
1132
|
+
...(generated?.meta ?? {}),
|
|
1133
|
+
session_awareness: {
|
|
1134
|
+
accepts_session: acceptsSession,
|
|
1135
|
+
execution_context: executionContext,
|
|
1136
|
+
appears_user_context: executionContext === 'user',
|
|
1137
|
+
session_requested: sessionRequested,
|
|
1138
|
+
session_present_in_payload: payloadHasSession,
|
|
1139
|
+
intentionally_system_context: executionContext === 'system',
|
|
1140
|
+
warnings,
|
|
1141
|
+
},
|
|
1142
|
+
};
|
|
1143
|
+
return generated;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1094
1146
|
function buildTypeScriptSnippet(
|
|
1095
1147
|
payload: Record<string, unknown>,
|
|
1096
1148
|
operationFamily: string,
|
|
@@ -1462,33 +1514,9 @@ DUCTAPE DATABASE MIGRATIONS
|
|
|
1462
1514
|
|
|
1463
1515
|
Migrations are versioned SQL/NoSQL schema change scripts managed per database component.
|
|
1464
1516
|
|
|
1465
|
-
Create
|
|
1466
|
-
ductape_execute("databases.migration.create", [{
|
|
1467
|
-
product: "my-product",
|
|
1468
|
-
database: "core-db",
|
|
1469
|
-
data: {
|
|
1470
|
-
name: "add users table",
|
|
1471
|
-
tag: "001-add-users",
|
|
1472
|
-
value: {
|
|
1473
|
-
up: ["CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT NOT NULL UNIQUE)"],
|
|
1474
|
-
down: ["DROP TABLE users"],
|
|
1475
|
-
},
|
|
1476
|
-
},
|
|
1477
|
-
}])
|
|
1478
|
-
|
|
1479
|
-
Run migrations (applies all pending):
|
|
1480
|
-
ductape_execute("databases.migration.run", [migrations, { env: "prd" }])
|
|
1517
|
+
Create migrations through the project migration files and ductape_cli, never ductape_execute.
|
|
1481
1518
|
|
|
1482
|
-
|
|
1483
|
-
ductape_execute("databases.migration.rollback", [migrations, 1]) // roll back 1
|
|
1484
|
-
|
|
1485
|
-
Check status:
|
|
1486
|
-
ductape_execute("databases.migration.status", [migrations])
|
|
1487
|
-
|
|
1488
|
-
History:
|
|
1489
|
-
ductape_execute("databases.migration.history", [])
|
|
1490
|
-
|
|
1491
|
-
Via CLI:
|
|
1519
|
+
Use the access-key administrative CLI for running, rolling back, and inspecting migrations:
|
|
1492
1520
|
ductape_cli("db migrate") // run pending
|
|
1493
1521
|
ductape_cli("db migrate rollback") // roll back last
|
|
1494
1522
|
ductape_cli("db migrate rollback -n 3")
|
|
@@ -1499,25 +1527,13 @@ MongoDB: migrations run as raw Mongo shell commands in the up/down arrays.
|
|
|
1499
1527
|
indexes: `
|
|
1500
1528
|
DUCTAPE DATABASE INDEXES
|
|
1501
1529
|
|
|
1502
|
-
Create
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
{ unique: true, name: "idx_email_unique" } // options (optional)
|
|
1507
|
-
])
|
|
1530
|
+
Create or update indexes through migration files and ductape_cli("db migrate"), not the
|
|
1531
|
+
publishable-key runtime proxy. Example definitions may use ["field1", "field2"] with
|
|
1532
|
+
{ unique: true, name: "idx_email_unique" }, or ordered fields such as
|
|
1533
|
+
[{ field: "createdAt", order: "DESC" }, { field: "userId", order: "ASC" }].
|
|
1508
1534
|
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
"orders",
|
|
1512
|
-
[{ field: "createdAt", order: "DESC" }, { field: "userId", order: "ASC" }],
|
|
1513
|
-
{ name: "idx_orders_date_user" }
|
|
1514
|
-
])
|
|
1515
|
-
|
|
1516
|
-
Drop an index:
|
|
1517
|
-
ductape_execute("databases.schema.dropIndex", ["collection_name", "index_name"])
|
|
1518
|
-
|
|
1519
|
-
List indexes on a collection:
|
|
1520
|
-
ductape_execute("databases.schema.indexes", ["collection_name"])
|
|
1535
|
+
Drop and inspect indexes through migration/CLI tooling or the database administration surface,
|
|
1536
|
+
not ductape_execute.
|
|
1521
1537
|
|
|
1522
1538
|
Performance guidance:
|
|
1523
1539
|
- Index fields used in WHERE clauses, JOIN conditions, and ORDER BY.
|
|
@@ -1563,18 +1579,7 @@ DUCTAPE DATABASE ACTIONS
|
|
|
1563
1579
|
A database action is a saved query or mutation (SQL string or NoSQL command) stored
|
|
1564
1580
|
on the Ductape product and executed by tag at runtime.
|
|
1565
1581
|
|
|
1566
|
-
Create
|
|
1567
|
-
ductape_execute("databases.action.create", [{
|
|
1568
|
-
product: "my-product",
|
|
1569
|
-
database: "core-db",
|
|
1570
|
-
data: {
|
|
1571
|
-
tag: "get-active-users",
|
|
1572
|
-
name: "Get active users",
|
|
1573
|
-
description: "Returns all users with status=active",
|
|
1574
|
-
type: "sql", // "sql" or "nosql"
|
|
1575
|
-
query: "SELECT * FROM users WHERE status = :status",
|
|
1576
|
-
},
|
|
1577
|
-
}])
|
|
1582
|
+
Create/update actions in Workbench (administrative access), never through ductape_execute.
|
|
1578
1583
|
|
|
1579
1584
|
Dispatch an action at runtime:
|
|
1580
1585
|
→ CALL ductape_generate_payload FIRST to get the canonical input shape.
|
|
@@ -1589,10 +1594,9 @@ Dispatch an action at runtime:
|
|
|
1589
1594
|
List actions for a database:
|
|
1590
1595
|
ductape_execute("databases.action.list", ["database_tag"])
|
|
1591
1596
|
|
|
1592
|
-
Fetch
|
|
1597
|
+
Fetch:
|
|
1593
1598
|
ductape_execute("databases.action.fetch", ["action_tag"])
|
|
1594
|
-
|
|
1595
|
-
ductape_execute("databases.action.delete", ["action_tag"])
|
|
1599
|
+
Update/delete are administrative and must be performed in Workbench.
|
|
1596
1600
|
|
|
1597
1601
|
Actions are the preferred way to encapsulate complex or reused queries — they can be
|
|
1598
1602
|
scheduled, dispatched with retries, and audited via logs.
|
|
@@ -1856,16 +1860,8 @@ DUCTAPE SECRETS
|
|
|
1856
1860
|
Secrets are workspace-level encrypted key-value pairs. They are referenced in resource configs,
|
|
1857
1861
|
connection URLs, and any string field using the $Secret{KEY_NAME} syntax.
|
|
1858
1862
|
|
|
1859
|
-
Create
|
|
1860
|
-
|
|
1861
|
-
key: "STRIPE_API_KEY",
|
|
1862
|
-
value: "sk_live_...", // plaintext — encrypted AES-256-GCM client-side before send
|
|
1863
|
-
description: "Stripe live key",
|
|
1864
|
-
token_type: "api", // "api" | "password" | "certificate"
|
|
1865
|
-
scope: ["my-product"], // which products can read this secret
|
|
1866
|
-
envs: ["prd"], // which env slugs can read this secret
|
|
1867
|
-
expires_at: 1800000000, // optional epoch ms
|
|
1868
|
-
}])
|
|
1863
|
+
Create/update secrets with ductape_cli or Workbench (administrative access), never ductape_execute.
|
|
1864
|
+
Definition fields include key, value, description, token_type, scope, envs, and expires_at.
|
|
1869
1865
|
The server never receives the plaintext value. Encryption uses the workspace private key.
|
|
1870
1866
|
|
|
1871
1867
|
Fetch / resolve:
|
|
@@ -1880,10 +1876,7 @@ $Secret{} reference syntax:
|
|
|
1880
1876
|
- The in-memory cache stores the encrypted form only; decryption happens on each cache hit.
|
|
1881
1877
|
- Cache TTL: 5 minutes. Clear with: secrets.clearCache()
|
|
1882
1878
|
|
|
1883
|
-
Lifecycle:
|
|
1884
|
-
ductape_execute("secrets.revoke", ["KEY"]) → disables without deleting (recoverable)
|
|
1885
|
-
ductape_execute("secrets.delete", ["KEY"]) → permanent deletion (irreversible)
|
|
1886
|
-
ductape_execute("secrets.update", ["KEY", { value: "new_value", expires_at: ... }])
|
|
1879
|
+
Lifecycle mutations (revoke/delete/update) are administrative: use ductape_cli or Workbench.
|
|
1887
1880
|
|
|
1888
1881
|
List all secrets (keys only — values are not returned in list):
|
|
1889
1882
|
ductape_execute("secrets.list", [])
|
|
@@ -1934,9 +1927,7 @@ Import from a file:
|
|
|
1934
1927
|
ductape_cli("apps import <file.json> -t postman|openapi")
|
|
1935
1928
|
Supports Postman v2.1 collection and OpenAPI 3.0 spec.
|
|
1936
1929
|
|
|
1937
|
-
Manage environments (base URLs per stage)
|
|
1938
|
-
ductape_execute("app.environments.create", [app_tag, { slug, env_name, base_url }])
|
|
1939
|
-
ductape_execute("app.environments.list", [app_tag])
|
|
1930
|
+
Manage environments (base URLs per stage) in Workbench; this currently has no CLI command.
|
|
1940
1931
|
|
|
1941
1932
|
Discover apps in a product and their actions (ALWAYS do this before writing any ctx.api.run call):
|
|
1942
1933
|
Step 1 — list apps connected to the product:
|
|
@@ -1953,16 +1944,8 @@ Discover apps in a product and their actions (ALWAYS do this before writing any
|
|
|
1953
1944
|
app resources (not for runtime input — use actions.fetch or ductape_generate_payload for that)
|
|
1954
1945
|
NEVER assume action input field names. Always fetch the action definition first.
|
|
1955
1946
|
|
|
1956
|
-
Manage actions (individual API endpoints)
|
|
1957
|
-
|
|
1958
|
-
tag, name, resource: "/users/{id}", method: "GET"|"POST"|"PUT"|"PATCH"|"DELETE",
|
|
1959
|
-
body?: { fieldName: { type, required? } },
|
|
1960
|
-
params?: { id: { type: "string" } },
|
|
1961
|
-
query?: { filter: { type: "string" } },
|
|
1962
|
-
headers?: { Authorization: { type: "string" } },
|
|
1963
|
-
response?: { status_code: 200, success: true, body: { ... }, response_format: "json" },
|
|
1964
|
-
}])
|
|
1965
|
-
ductape_execute("actions.update", [app_tag, action_tag, data])
|
|
1947
|
+
Manage actions (individual API endpoints) in Workbench or import an OpenAPI/Postman file.
|
|
1948
|
+
Creation/update are administrative and must never use ductape_execute.
|
|
1966
1949
|
ductape_execute("actions.list", [app_tag])
|
|
1967
1950
|
ductape_execute("actions.fetch", [app_tag, action_tag])
|
|
1968
1951
|
|
|
@@ -1983,16 +1966,14 @@ Run an action at runtime:
|
|
|
1983
1966
|
|
|
1984
1967
|
Auth schemes (how the app authenticates outbound requests):
|
|
1985
1968
|
Setup types: header | bearer | basic | oauth2 | apikey
|
|
1986
|
-
|
|
1969
|
+
Configure auth in Workbench (administrative).
|
|
1987
1970
|
ductape_execute("auths.list", [app_tag])
|
|
1988
1971
|
|
|
1989
1972
|
Webhooks (inbound events from the external service):
|
|
1990
|
-
|
|
1991
|
-
ductape_execute("webhooks.events.create", [app_tag, { tag, name, selector, description, sample }])
|
|
1973
|
+
Configure webhooks and webhook events in Workbench (administrative).
|
|
1992
1974
|
|
|
1993
1975
|
Variables (per-env mutable values) and Constants (fixed values):
|
|
1994
|
-
|
|
1995
|
-
ductape_execute("app.constants.create", [app_tag, { key, value }])
|
|
1976
|
+
Configure variables and constants in Workbench (administrative).
|
|
1996
1977
|
|
|
1997
1978
|
Connecting an app to a product (after creation):
|
|
1998
1979
|
NOTE: All product.* module methods require the access key and CANNOT use ductape_execute.
|
|
@@ -2000,14 +1981,13 @@ Connecting an app to a product (after creation):
|
|
|
2000
1981
|
|
|
2001
1982
|
FULL FLOW to make an app callable from a product:
|
|
2002
1983
|
1. Create the app: ductape_cli("apps create --name \\"Service Name\\" --description \\"...\\"")
|
|
2003
|
-
2. Add environments:
|
|
2004
|
-
3. Configure auth:
|
|
2005
|
-
4. Define actions:
|
|
1984
|
+
2. Add environments: Workbench
|
|
1985
|
+
3. Configure auth: Workbench
|
|
1986
|
+
4. Define actions: Workbench
|
|
2006
1987
|
OR import: ductape_cli("apps import <file.json> -t postman|openapi")
|
|
2007
1988
|
5. Connect to product: Requires the product_id (from ductape_cli("products get --tag <tag> --json")).
|
|
2008
1989
|
There is no CLI command for this step — the SDK product.apps.add method requires
|
|
2009
|
-
an access key which only the backend can provide.
|
|
2010
|
-
admin-authenticated context, or connect via the Workbench UI.
|
|
1990
|
+
an access key which only the backend can provide. Connect via Workbench.
|
|
2011
1991
|
6. Verify: ductape_cli("products get --tag <product_tag> --json") → check apps[] contains the app
|
|
2012
1992
|
ductape_execute("actions.list", [app_tag]) → verify actions are registered
|
|
2013
1993
|
`.trim(),
|
|
@@ -2084,8 +2064,8 @@ IMPORTANT — selector must be "$Session{fieldName}" format:
|
|
|
2084
2064
|
CORRECT: selector: "$Session{playerId}"
|
|
2085
2065
|
INCORRECT: selector: "playerId" ← WILL FAIL with "Selector should be in the format $Session{...}{key}"
|
|
2086
2066
|
|
|
2087
|
-
Example (
|
|
2088
|
-
|
|
2067
|
+
Example definition (configure via declarative apply or Workbench, not ductape_execute):
|
|
2068
|
+
{
|
|
2089
2069
|
tag: "player-session",
|
|
2090
2070
|
name: "Player Session",
|
|
2091
2071
|
expiry: 24,
|
|
@@ -2097,7 +2077,7 @@ Example (game product — player identity in JWT):
|
|
|
2097
2077
|
role: "player",
|
|
2098
2078
|
accountId: "acct_xyz",
|
|
2099
2079
|
},
|
|
2100
|
-
}
|
|
2080
|
+
}
|
|
2101
2081
|
|
|
2102
2082
|
Runtime — create a session (sign a JWT):
|
|
2103
2083
|
→ CALL ductape_generate_payload FIRST (operation_family="session", method="start", targets={tag})
|
|
@@ -2137,10 +2117,33 @@ Token format: "session_tag:jwt_token" — pass this format verbatim wherever ses
|
|
|
2137
2117
|
JWT is signed with the product private key (not a symmetric shared secret).
|
|
2138
2118
|
|
|
2139
2119
|
SESSION PROPAGATION
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2120
|
+
Extract the original full "session_tag:jwt" token once at the HTTP/transport boundary and retain
|
|
2121
|
+
it separately from verified/decoded claims. Claims are authorization data; they are not a token.
|
|
2122
|
+
Store a request ActorContext in a NestJS request-scoped provider or AsyncLocalStorage:
|
|
2123
|
+
type ActorContext =
|
|
2124
|
+
| { kind: "user"; session: string; actorId: string }
|
|
2125
|
+
| { kind: "delegated"; delegatedIdentity: string; actorId: string; initiatedAt: string }
|
|
2126
|
+
| { kind: "system"; reason: string };
|
|
2127
|
+
Pass ActorContext explicitly through service boundaries. At each immediate Ductape runtime call,
|
|
2128
|
+
pass actor.kind === "user" ? actor.session : the SDK-approved delegated identity. Put only
|
|
2129
|
+
non-secret actor metadata (actorId, kind, initiatedAt, correlationId) in Event envelopes.
|
|
2130
|
+
Feature execution should receive the same explicit actor classification. Never log, serialize
|
|
2131
|
+
into business payloads, or attach the raw session/refresh token to traces or error messages.
|
|
2132
|
+
|
|
2133
|
+
SECURITY BOUNDARY FOR DURABLE WORK
|
|
2134
|
+
The current SDK accepts a session string on dispatch, but it does not expose an immutable
|
|
2135
|
+
attribution snapshot API, an expired-token attribution contract, or a general delegated-session
|
|
2136
|
+
issuer. Therefore do NOT persist reusable JWTs indefinitely or assume an expired initiating token
|
|
2137
|
+
will remain valid when delayed work executes. Pass the full token only for immediate work within
|
|
2138
|
+
its validity window. For delayed/recurring work, classify execution as system-context, or use an
|
|
2139
|
+
application-owned immutable actor-context envelope plus a short-lived delegated identity issued
|
|
2140
|
+
by an explicitly approved auth design. Until the SDK provides that design, actor metadata is for
|
|
2141
|
+
audit/correlation and must not be treated as authorization.
|
|
2142
|
+
|
|
2143
|
+
Distinguish contexts deliberately:
|
|
2144
|
+
user — active request; original valid full token is required when the operation accepts it
|
|
2145
|
+
delegated — delayed/on-behalf-of work; approved short-lived credential + immutable actor metadata
|
|
2146
|
+
system — scheduler/maintenance with no user authority; omit session and record a reason
|
|
2144
2147
|
|
|
2145
2148
|
Backend propagation attributes component activity to an actor, but it does not record frontend
|
|
2146
2149
|
pageviews, navigation, UI intent, funnels, or client failures. Continue immediately with
|
|
@@ -2152,14 +2155,12 @@ DUCTAPE CACHES
|
|
|
2152
2155
|
|
|
2153
2156
|
Caches are product-level Redis (or in-memory) stores for temporary key-value data with optional TTL.
|
|
2154
2157
|
|
|
2155
|
-
Registration (admin — ductape_cli
|
|
2158
|
+
Registration (admin — ductape_cli):
|
|
2156
2159
|
ductape_cli("resources caches create -f cache.json")
|
|
2157
2160
|
File: { name, tag, description?, expiry: <milliseconds> }
|
|
2158
2161
|
No type or envs — Ductape manages the store infrastructure.
|
|
2159
2162
|
expiry is in MILLISECONDS: 3600000 = 1 hour, 86400000 = 1 day, 604800000 = 1 week.
|
|
2160
2163
|
|
|
2161
|
-
SDK: ductape_execute("caches.create", [product_tag, { name, tag, description?, expiry: 3600000 }])
|
|
2162
|
-
|
|
2163
2164
|
Operations:
|
|
2164
2165
|
caches.set [{ product, cache, key, value: string, expiry?: string (ISO 8601), env }]
|
|
2165
2166
|
expiry is an ABSOLUTE TIMESTAMP (not a duration).
|
|
@@ -2208,20 +2209,20 @@ DUCTAPE NOTIFICATIONS
|
|
|
2208
2209
|
|
|
2209
2210
|
Notifications send messages across multiple channels: email, SMS, push, or HTTP callback.
|
|
2210
2211
|
|
|
2211
|
-
Create a notification
|
|
2212
|
-
|
|
2212
|
+
Create a notification through declarative apply or Workbench (administrative), using:
|
|
2213
|
+
{
|
|
2213
2214
|
tag: "welcome-email",
|
|
2214
2215
|
name: "Welcome Email",
|
|
2215
2216
|
type: "email", // optional hint; actual channels configured per env
|
|
2216
|
-
}
|
|
2217
|
+
}
|
|
2217
2218
|
|
|
2218
|
-
Create a message template:
|
|
2219
|
-
|
|
2219
|
+
Create a message template through declarative apply or Workbench:
|
|
2220
|
+
{
|
|
2220
2221
|
tag: "welcome-email:default", // format: "notification_tag:message_tag"
|
|
2221
2222
|
notification: "welcome-email",
|
|
2222
2223
|
subject: { template: "Welcome, {{name}}!", data: { name: "" } },
|
|
2223
2224
|
body: { template: "Hi {{name}}, thanks for signing up.", data: { name: "" } },
|
|
2224
|
-
}
|
|
2225
|
+
}
|
|
2225
2226
|
|
|
2226
2227
|
Send at runtime (one channel at a time):
|
|
2227
2228
|
→ CALL ductape_generate_payload FIRST (operation_family="notification", method="email.send")
|
|
@@ -2260,8 +2261,15 @@ DUCTAPE RESILIENCE
|
|
|
2260
2261
|
Resilience covers three mechanisms: quotas (rate-limited provider pools), fallbacks (automatic
|
|
2261
2262
|
provider switching), and healthchecks (continuous probe monitoring with failure actions).
|
|
2262
2263
|
|
|
2264
|
+
CONFIGURATION BOUNDARY
|
|
2265
|
+
Quotas, fallbacks, and health checks are administrative product configuration. Configure them in
|
|
2266
|
+
Workbench (or a future access-key administrative tool explicitly documented for the asset).
|
|
2267
|
+
Never route administrative create/update methods through ductape_execute: its publishable-key
|
|
2268
|
+
runtime proxy will fail.
|
|
2269
|
+
|
|
2263
2270
|
QUOTAS — rate-limited multi-provider pools:
|
|
2264
|
-
|
|
2271
|
+
Workbench definition shape:
|
|
2272
|
+
{
|
|
2265
2273
|
tag: "sms-quota",
|
|
2266
2274
|
name: "SMS Provider Pool",
|
|
2267
2275
|
input: { to: { type: "string", required: true }, message: { type: "string" } },
|
|
@@ -2275,7 +2283,7 @@ QUOTAS — rate-limited multi-provider pools:
|
|
|
2275
2283
|
input: { "body:to": "$Input{to}", "body:body": "$Input{message}" },
|
|
2276
2284
|
output: {} },
|
|
2277
2285
|
],
|
|
2278
|
-
}
|
|
2286
|
+
}
|
|
2279
2287
|
Providers are tried in order until quota is not exhausted.
|
|
2280
2288
|
quotas.run [{ product, env, tag, input }] ← CALL ductape_generate_payload FIRST
|
|
2281
2289
|
quotas.dispatch [{ product, env, tag, input, schedule? }]
|
|
@@ -2283,12 +2291,13 @@ QUOTAS — rate-limited multi-provider pools:
|
|
|
2283
2291
|
FALLBACKS — automatic provider switching on failure:
|
|
2284
2292
|
Same schema as quotas but options are ordered: primary first, then fallback(s).
|
|
2285
2293
|
Primary is used first; on failure, the next provider is tried automatically.
|
|
2286
|
-
|
|
2294
|
+
Configure in Workbench: { tag, name, input: { ... }, options: [...] }
|
|
2287
2295
|
fallback.run [{ product, env, tag, input }] ← CALL ductape_generate_payload FIRST
|
|
2288
2296
|
fallback.dispatch [{ product, env, tag, input, schedule? }]
|
|
2289
2297
|
|
|
2290
2298
|
HEALTHCHECKS — continuous probe with failure notifications:
|
|
2291
|
-
|
|
2299
|
+
Workbench definition shape:
|
|
2300
|
+
{
|
|
2292
2301
|
tag: "payment-health",
|
|
2293
2302
|
name: "Payment Service Health",
|
|
2294
2303
|
probe: { type: "app", app: "stripe-app", event: "ping" },
|
|
@@ -2300,7 +2309,7 @@ HEALTHCHECKS — continuous probe with failure notifications:
|
|
|
2300
2309
|
channels: { email: { recipients: ["ops@example.com"] } } }],
|
|
2301
2310
|
webhooks: [{ url: "https://hooks.example.com/alert", method: "POST" }],
|
|
2302
2311
|
},
|
|
2303
|
-
}
|
|
2312
|
+
}
|
|
2304
2313
|
health.run [{ product, env, tag }] → triggers an immediate probe
|
|
2305
2314
|
health.check [{ product, env, tag }] → same as run
|
|
2306
2315
|
health.status [{ product, env, tag }] → current health status
|
|
@@ -2310,6 +2319,23 @@ Failure actions: notification channels, HTTP webhooks, and/or message broker emi
|
|
|
2310
2319
|
be configured simultaneously on the same healthcheck.
|
|
2311
2320
|
Input template references: $Input{field} → maps declared input to the probe's action input.
|
|
2312
2321
|
Provider status: available | unavailable
|
|
2322
|
+
|
|
2323
|
+
DECISION MATRIX
|
|
2324
|
+
Rate/capacity allocation across providers → quota
|
|
2325
|
+
Equivalent provider after operational failure → fallback
|
|
2326
|
+
Detect failure before routing provider traffic → health check
|
|
2327
|
+
Transient failure of one operation → bounded retry + idempotency policy
|
|
2328
|
+
Multi-step business recovery/compensation → Feature
|
|
2329
|
+
Database atomicity → database transaction, not Feature rollback
|
|
2330
|
+
Scheduled single operation → that component's dispatch
|
|
2331
|
+
Scheduled multi-step process → Feature dispatch
|
|
2332
|
+
|
|
2333
|
+
COMBINING MECHANISMS
|
|
2334
|
+
A health check may keep an unhealthy provider out of a fallback/quota pool; the pool controls
|
|
2335
|
+
provider selection; each operation may use bounded retries and an idempotency key; a Feature
|
|
2336
|
+
coordinates business steps and compensation around those resilient operations. Do not stack them
|
|
2337
|
+
reflexively: each layer needs a distinct failure it owns, bounded retry budgets, and observable
|
|
2338
|
+
terminal behavior. Database transactions remain the atomicity boundary for related DB writes.
|
|
2313
2339
|
`.trim(),
|
|
2314
2340
|
|
|
2315
2341
|
'frontend-analytics': `
|
|
@@ -2318,10 +2344,12 @@ DUCTAPE FRONTEND PRODUCT ANALYTICS AND SESSION ACTIVITY
|
|
|
2318
2344
|
Frontend analytics and backend session attribution are complementary; neither replaces the other.
|
|
2319
2345
|
|
|
2320
2346
|
BACKEND OPERATION ATTRIBUTION
|
|
2321
|
-
|
|
2322
|
-
notification, storage, graph, vector, action, and other runtime operation
|
|
2323
|
-
This attributes server work to the authenticated actor.
|
|
2324
|
-
|
|
2347
|
+
Immediate user-initiated backend work should pass the original full Ductape session token to every
|
|
2348
|
+
database, Event, Feature, notification, storage, graph, vector, action, and other runtime operation
|
|
2349
|
+
that accepts session. This attributes immediate server work to the authenticated actor.
|
|
2350
|
+
For delayed/durable work, do not persist raw JWTs indefinitely. Use system context, or an approved
|
|
2351
|
+
delegated actor design with immutable non-secret actor metadata. Actor metadata supports audit and
|
|
2352
|
+
correlation; it is not authorization. Intentional system/background work should remain sessionless.
|
|
2325
2353
|
|
|
2326
2354
|
FRONTEND PRODUCT ANALYTICS
|
|
2327
2355
|
Use the browser Analytics service for anonymous visits, authenticated page views, navigation,
|
|
@@ -2591,9 +2619,11 @@ STEP 4 — PRESENT the plan and get approval BEFORE writing any code or creating
|
|
|
2591
2619
|
Wait for confirmation before writing code or calling any create tool.
|
|
2592
2620
|
|
|
2593
2621
|
STEP 5 — CREATE missing components (only with user approval)
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2622
|
+
Administrative assets must never be created through ductape_execute. Use ductape_cli for products,
|
|
2623
|
+
apps, supported resources, broker topics, cloud connections, secrets, and declarative apply flows.
|
|
2624
|
+
App actions, auths, Features, quotas, fallbacks, health checks, and other assets for which the CLI
|
|
2625
|
+
has no command must be configured in Workbench. Do not generate an impossible publishable-key
|
|
2626
|
+
create/update call. For a missing database action, configure it in Workbench, then verify it exists.
|
|
2597
2627
|
For a missing child feature, recursively apply this same workflow.
|
|
2598
2628
|
Tell the user what you are about to create before each tool call.
|
|
2599
2629
|
|
|
@@ -2717,7 +2747,11 @@ When you call features.define({ handler }), the handler runs TWICE:
|
|
|
2717
2747
|
|
|
2718
2748
|
Invoke internal application business logic (your own NestJS/backend service code):
|
|
2719
2749
|
→ produce a broker event (ctx.messaging.produce or ductape.events.produce)
|
|
2720
|
-
→
|
|
2750
|
+
→ follow ductape_docs({ topic: "events" }) and use the canonical NestJS decorator:
|
|
2751
|
+
@Events.Consumer({ event: "broker-tag:topic-tag" })
|
|
2752
|
+
async handle(message: MessageShape) { /* injected-service business logic; throw to nack */ }
|
|
2753
|
+
→ DuctapeModule auto-registers the decorated consumer; no manual onModuleInit is needed
|
|
2754
|
+
→ events.consume() remains a supported lower-level alternative for plain TypeScript/Node.js
|
|
2721
2755
|
→ your service method runs with full access to DI, DB transactions, etc.
|
|
2722
2756
|
Do NOT create an App Action just to call your own service over HTTP.
|
|
2723
2757
|
|
|
@@ -3789,6 +3823,25 @@ async function main() {
|
|
|
3789
3823
|
|
|
3790
3824
|
const executeHandler = async (args: { publishable_key?: string; module: SDKModule; method: string; params: unknown[] }) => {
|
|
3791
3825
|
try {
|
|
3826
|
+
const runtimeMutationMethods: Partial<Record<SDKModule, Set<string>>> = {
|
|
3827
|
+
databases: new Set(['insert', 'update', 'delete', 'upsert']),
|
|
3828
|
+
graph: new Set(['insert', 'update', 'delete']),
|
|
3829
|
+
vector: new Set(['insert', 'upsert', 'upsertOne', 'delete']),
|
|
3830
|
+
sessions: new Set(['revoke']),
|
|
3831
|
+
};
|
|
3832
|
+
const isRuntimeDataMutation = runtimeMutationMethods[args.module]?.has(args.method) === true;
|
|
3833
|
+
const isAdministrativeMutation =
|
|
3834
|
+
(/^migration\./.test(args.method) ||
|
|
3835
|
+
/^schema\.(create|drop|add|remove|update)/.test(args.method) ||
|
|
3836
|
+
/(^|\.)(create|update|delete|configure|add|remove|revoke)$/.test(args.method)) &&
|
|
3837
|
+
!isRuntimeDataMutation;
|
|
3838
|
+
if (isAdministrativeMutation) {
|
|
3839
|
+
throw new Error(
|
|
3840
|
+
`Administrative operation "${args.module}.${args.method}" is blocked in ductape_execute. ` +
|
|
3841
|
+
'Use ductape_cli when that resource is supported by the CLI; otherwise configure it in Workbench. ' +
|
|
3842
|
+
'The runtime proxy uses a publishable key and cannot administer platform assets.',
|
|
3843
|
+
);
|
|
3844
|
+
}
|
|
3792
3845
|
const key = args.publishable_key || process.env.DUCTAPE_PUBLISHABLE_KEY;
|
|
3793
3846
|
if (!key) {
|
|
3794
3847
|
throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
|
|
@@ -3839,7 +3892,10 @@ async function main() {
|
|
|
3839
3892
|
if (!key) {
|
|
3840
3893
|
throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
|
|
3841
3894
|
}
|
|
3842
|
-
const result =
|
|
3895
|
+
const result = addSessionAwarenessMetadata(
|
|
3896
|
+
await generateExecutablePayload({ ...args, publishable_key: key }),
|
|
3897
|
+
args,
|
|
3898
|
+
);
|
|
3843
3899
|
|
|
3844
3900
|
let text = JSON.stringify(result ?? null, null, 2);
|
|
3845
3901
|
|
|
@@ -3871,7 +3927,10 @@ async function main() {
|
|
|
3871
3927
|
throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
|
|
3872
3928
|
}
|
|
3873
3929
|
ensureSupportedSnippetOperation(args.operation_family, args.method);
|
|
3874
|
-
const generated =
|
|
3930
|
+
const generated = addSessionAwarenessMetadata(
|
|
3931
|
+
await generateExecutablePayload({ ...args, publishable_key: key }),
|
|
3932
|
+
args,
|
|
3933
|
+
);
|
|
3875
3934
|
const payload = (generated as any)?.payload ?? {};
|
|
3876
3935
|
const snippet = buildSnippet(args.language, payload, args.operation_family, args.method);
|
|
3877
3936
|
return {
|
|
@@ -4055,7 +4114,7 @@ async function main() {
|
|
|
4055
4114
|
' Step 2 — check if the database component already exists:\n' +
|
|
4056
4115
|
' ductape_cli("resources databases list <product_tag> --json")\n' +
|
|
4057
4116
|
' If a component already uses the same Atlas cluster, do NOT re-import — instead update it:\n' +
|
|
4058
|
-
'
|
|
4117
|
+
' use ductape_cli resource update when supported; otherwise update it in Workbench\n' +
|
|
4059
4118
|
' Add or change the dbName in the env\'s connection_url to switch databases on the same cluster.\n' +
|
|
4060
4119
|
' Step 3 — import (only if no existing component uses this cluster):\n' +
|
|
4061
4120
|
' Use import-persist-all with one entry per product env. Required fields per entry:\n' +
|
package/src/proxy-client.ts
CHANGED
|
@@ -76,6 +76,7 @@ export interface IGenerateExecutablePayloadRequest {
|
|
|
76
76
|
method: string;
|
|
77
77
|
targets?: Record<string, unknown>;
|
|
78
78
|
include_session?: boolean;
|
|
79
|
+
execution_context?: 'user' | 'delegated' | 'system';
|
|
79
80
|
include_cache?: boolean;
|
|
80
81
|
schema_mode?: 'strict' | 'best_effort';
|
|
81
82
|
input_hint?: Record<string, unknown>;
|