@ductape/mcp 0.1.49 → 0.1.51

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 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 calling ductape_execute to create or update any asset — use this to discover required fields
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);
@@ -1192,7 +1232,7 @@ const docsInputSchema = z.object({
1192
1232
  topic: z.string().describe('Feature topic to look up. Supported: ' +
1193
1233
  'transactions, presave, triggers, aggregations, migrations, indexes, performance, actions, ' +
1194
1234
  'graphs, storage, cloud, vector, warehouse, secrets, apps, products, sessions, caches, ' +
1195
- 'notifications, resilience, features, events, logs, frontend, client, react, vue'),
1235
+ 'notifications, resilience, features, events, logs, frontend, frontend-analytics, client, react, vue'),
1196
1236
  });
1197
1237
  const DOCS = {
1198
1238
  frontend: `
@@ -1228,6 +1268,11 @@ SHARED APPLICATION LIFECYCLE
1228
1268
  connectionState to render disconnected/reconnecting UI; do not create duplicate subscriptions.
1229
1269
  7. Disconnect resource sessions and the root client when the owning application scope is torn down.
1230
1270
 
1271
+ PRODUCT ANALYTICS
1272
+ Frontend product analytics complements backend session propagation; neither replaces the other.
1273
+ Continue with ductape_docs({ topic: "frontend-analytics" }) for identity lifecycle, pageviews,
1274
+ custom events, privacy masking, hidden-state safety, trace correlation, and package-version checks.
1275
+
1231
1276
  AUTHENTICATION AND SECURITY
1232
1277
  - Use publishableKey in browser applications. Never ship workspace private keys or privileged
1233
1278
  access keys in frontend bundles.
@@ -1400,33 +1445,9 @@ DUCTAPE DATABASE MIGRATIONS
1400
1445
 
1401
1446
  Migrations are versioned SQL/NoSQL schema change scripts managed per database component.
1402
1447
 
1403
- Create a migration:
1404
- ductape_execute("databases.migration.create", [{
1405
- product: "my-product",
1406
- database: "core-db",
1407
- data: {
1408
- name: "add users table",
1409
- tag: "001-add-users",
1410
- value: {
1411
- up: ["CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT NOT NULL UNIQUE)"],
1412
- down: ["DROP TABLE users"],
1413
- },
1414
- },
1415
- }])
1416
-
1417
- Run migrations (applies all pending):
1418
- ductape_execute("databases.migration.run", [migrations, { env: "prd" }])
1448
+ Create migrations through the project migration files and ductape_cli, never ductape_execute.
1419
1449
 
1420
- Rollback:
1421
- ductape_execute("databases.migration.rollback", [migrations, 1]) // roll back 1
1422
-
1423
- Check status:
1424
- ductape_execute("databases.migration.status", [migrations])
1425
-
1426
- History:
1427
- ductape_execute("databases.migration.history", [])
1428
-
1429
- Via CLI:
1450
+ Use the access-key administrative CLI for running, rolling back, and inspecting migrations:
1430
1451
  ductape_cli("db migrate") // run pending
1431
1452
  ductape_cli("db migrate rollback") // roll back last
1432
1453
  ductape_cli("db migrate rollback -n 3")
@@ -1436,25 +1457,13 @@ MongoDB: migrations run as raw Mongo shell commands in the up/down arrays.
1436
1457
  indexes: `
1437
1458
  DUCTAPE DATABASE INDEXES
1438
1459
 
1439
- Create an index:
1440
- ductape_execute("databases.schema.createIndex", [
1441
- "collection_name",
1442
- ["field1", "field2"], // simple array of field names
1443
- { unique: true, name: "idx_email_unique" } // options (optional)
1444
- ])
1445
-
1446
- // Ordered index:
1447
- ductape_execute("databases.schema.createIndex", [
1448
- "orders",
1449
- [{ field: "createdAt", order: "DESC" }, { field: "userId", order: "ASC" }],
1450
- { name: "idx_orders_date_user" }
1451
- ])
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" }].
1452
1464
 
1453
- Drop an index:
1454
- ductape_execute("databases.schema.dropIndex", ["collection_name", "index_name"])
1455
-
1456
- List indexes on a collection:
1457
- 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.
1458
1467
 
1459
1468
  Performance guidance:
1460
1469
  - Index fields used in WHERE clauses, JOIN conditions, and ORDER BY.
@@ -1498,18 +1507,7 @@ DUCTAPE DATABASE ACTIONS
1498
1507
  A database action is a saved query or mutation (SQL string or NoSQL command) stored
1499
1508
  on the Ductape product and executed by tag at runtime.
1500
1509
 
1501
- Create an action (admin):
1502
- ductape_execute("databases.action.create", [{
1503
- product: "my-product",
1504
- database: "core-db",
1505
- data: {
1506
- tag: "get-active-users",
1507
- name: "Get active users",
1508
- description: "Returns all users with status=active",
1509
- type: "sql", // "sql" or "nosql"
1510
- query: "SELECT * FROM users WHERE status = :status",
1511
- },
1512
- }])
1510
+ Create/update actions in Workbench (administrative access), never through ductape_execute.
1513
1511
 
1514
1512
  Dispatch an action at runtime:
1515
1513
  → CALL ductape_generate_payload FIRST to get the canonical input shape.
@@ -1524,10 +1522,9 @@ Dispatch an action at runtime:
1524
1522
  List actions for a database:
1525
1523
  ductape_execute("databases.action.list", ["database_tag"])
1526
1524
 
1527
- Fetch / update / delete:
1525
+ Fetch:
1528
1526
  ductape_execute("databases.action.fetch", ["action_tag"])
1529
- ductape_execute("databases.action.update", ["my-product", "action_tag", { query: "..." }])
1530
- ductape_execute("databases.action.delete", ["action_tag"])
1527
+ Update/delete are administrative and must be performed in Workbench.
1531
1528
 
1532
1529
  Actions are the preferred way to encapsulate complex or reused queries — they can be
1533
1530
  scheduled, dispatched with retries, and audited via logs.
@@ -1785,16 +1782,8 @@ DUCTAPE SECRETS
1785
1782
  Secrets are workspace-level encrypted key-value pairs. They are referenced in resource configs,
1786
1783
  connection URLs, and any string field using the $Secret{KEY_NAME} syntax.
1787
1784
 
1788
- Create (admin ductape_cli or ductape_execute):
1789
- ductape_execute("secrets.create", [{
1790
- key: "STRIPE_API_KEY",
1791
- value: "sk_live_...", // plaintext — encrypted AES-256-GCM client-side before send
1792
- description: "Stripe live key",
1793
- token_type: "api", // "api" | "password" | "certificate"
1794
- scope: ["my-product"], // which products can read this secret
1795
- envs: ["prd"], // which env slugs can read this secret
1796
- expires_at: 1800000000, // optional epoch ms
1797
- }])
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.
1798
1787
  The server never receives the plaintext value. Encryption uses the workspace private key.
1799
1788
 
1800
1789
  Fetch / resolve:
@@ -1809,10 +1798,7 @@ $Secret{} reference syntax:
1809
1798
  - The in-memory cache stores the encrypted form only; decryption happens on each cache hit.
1810
1799
  - Cache TTL: 5 minutes. Clear with: secrets.clearCache()
1811
1800
 
1812
- Lifecycle:
1813
- ductape_execute("secrets.revoke", ["KEY"]) → disables without deleting (recoverable)
1814
- ductape_execute("secrets.delete", ["KEY"]) → permanent deletion (irreversible)
1815
- ductape_execute("secrets.update", ["KEY", { value: "new_value", expires_at: ... }])
1801
+ Lifecycle mutations (revoke/delete/update) are administrative: use ductape_cli or Workbench.
1816
1802
 
1817
1803
  List all secrets (keys only — values are not returned in list):
1818
1804
  ductape_execute("secrets.list", [])
@@ -1862,9 +1848,7 @@ Import from a file:
1862
1848
  ductape_cli("apps import <file.json> -t postman|openapi")
1863
1849
  Supports Postman v2.1 collection and OpenAPI 3.0 spec.
1864
1850
 
1865
- Manage environments (base URLs per stage):
1866
- ductape_execute("app.environments.create", [app_tag, { slug, env_name, base_url }])
1867
- ductape_execute("app.environments.list", [app_tag])
1851
+ Manage environments (base URLs per stage) in Workbench; this currently has no CLI command.
1868
1852
 
1869
1853
  Discover apps in a product and their actions (ALWAYS do this before writing any ctx.api.run call):
1870
1854
  Step 1 — list apps connected to the product:
@@ -1881,16 +1865,8 @@ Discover apps in a product and their actions (ALWAYS do this before writing any
1881
1865
  app resources (not for runtime input — use actions.fetch or ductape_generate_payload for that)
1882
1866
  NEVER assume action input field names. Always fetch the action definition first.
1883
1867
 
1884
- Manage actions (individual API endpoints):
1885
- ductape_execute("actions.create", [app_tag, {
1886
- tag, name, resource: "/users/{id}", method: "GET"|"POST"|"PUT"|"PATCH"|"DELETE",
1887
- body?: { fieldName: { type, required? } },
1888
- params?: { id: { type: "string" } },
1889
- query?: { filter: { type: "string" } },
1890
- headers?: { Authorization: { type: "string" } },
1891
- response?: { status_code: 200, success: true, body: { ... }, response_format: "json" },
1892
- }])
1893
- 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.
1894
1870
  ductape_execute("actions.list", [app_tag])
1895
1871
  ductape_execute("actions.fetch", [app_tag, action_tag])
1896
1872
 
@@ -1911,16 +1887,14 @@ Run an action at runtime:
1911
1887
 
1912
1888
  Auth schemes (how the app authenticates outbound requests):
1913
1889
  Setup types: header | bearer | basic | oauth2 | apikey
1914
- ductape_execute("auths.create", [app_tag, { tag, name, setup_type, expiry, period, action_tag? }])
1890
+ Configure auth in Workbench (administrative).
1915
1891
  ductape_execute("auths.list", [app_tag])
1916
1892
 
1917
1893
  Webhooks (inbound events from the external service):
1918
- ductape_execute("webhooks.create", [app_tag, { tag, name, description, envs: [{ slug, registration_url?, method? }] }])
1919
- ductape_execute("webhooks.events.create", [app_tag, { tag, name, selector, description, sample }])
1894
+ Configure webhooks and webhook events in Workbench (administrative).
1920
1895
 
1921
1896
  Variables (per-env mutable values) and Constants (fixed values):
1922
- ductape_execute("app.variables.create", [app_tag, { key, value, env_slug }])
1923
- ductape_execute("app.constants.create", [app_tag, { key, value }])
1897
+ Configure variables and constants in Workbench (administrative).
1924
1898
 
1925
1899
  Connecting an app to a product (after creation):
1926
1900
  NOTE: All product.* module methods require the access key and CANNOT use ductape_execute.
@@ -1928,14 +1902,13 @@ Connecting an app to a product (after creation):
1928
1902
 
1929
1903
  FULL FLOW to make an app callable from a product:
1930
1904
  1. Create the app: ductape_cli("apps create --name \\"Service Name\\" --description \\"...\\"")
1931
- 2. Add environments: ductape_execute("app.environments.create", [app_tag, { slug: "prd", env_name: "Production", base_url: "https://api.example.com" }])
1932
- 3. Configure auth: ductape_execute("auths.create", [app_tag, { tag, name, setup_type: "apikey"|"bearer"|"basic"|"oauth2", expiry, period }])
1933
- 4. Define actions: ductape_execute("actions.create", [app_tag, { tag, name, resource, method, body?, params?, query?, headers?, response? }])
1905
+ 2. Add environments: Workbench
1906
+ 3. Configure auth: Workbench
1907
+ 4. Define actions: Workbench
1934
1908
  OR import: ductape_cli("apps import <file.json> -t postman|openapi")
1935
1909
  5. Connect to product: Requires the product_id (from ductape_cli("products get --tag <tag> --json")).
1936
1910
  There is no CLI command for this step — the SDK product.apps.add method requires
1937
- an access key which only the backend can provide. Use ductape_execute via an
1938
- admin-authenticated context, or connect via the Workbench UI.
1911
+ an access key which only the backend can provide. Connect via Workbench.
1939
1912
  6. Verify: ductape_cli("products get --tag <product_tag> --json") → check apps[] contains the app
1940
1913
  ductape_execute("actions.list", [app_tag]) → verify actions are registered
1941
1914
  `.trim(),
@@ -1973,6 +1946,7 @@ Resource registration (all via ductape_cli or ductape_execute — see per-topic
1973
1946
  caches → ductape_docs({ topic: "caches" })
1974
1947
  notifications → ductape_docs({ topic: "notifications" })
1975
1948
  sessions → ductape_docs({ topic: "sessions" })
1949
+ frontend analytics → ductape_docs({ topic: "frontend-analytics" })
1976
1950
  resilience → ductape_docs({ topic: "resilience" })
1977
1951
  features → ductape_docs({ topic: "features" })
1978
1952
 
@@ -2009,8 +1983,8 @@ IMPORTANT — selector must be "$Session{fieldName}" format:
2009
1983
  CORRECT: selector: "$Session{playerId}"
2010
1984
  INCORRECT: selector: "playerId" ← WILL FAIL with "Selector should be in the format $Session{...}{key}"
2011
1985
 
2012
- Example (game product player identity in JWT):
2013
- ductape_execute("sessions.create", [product_tag, {
1986
+ Example definition (configure via declarative apply or Workbench, not ductape_execute):
1987
+ {
2014
1988
  tag: "player-session",
2015
1989
  name: "Player Session",
2016
1990
  expiry: 24,
@@ -2022,7 +1996,7 @@ Example (game product — player identity in JWT):
2022
1996
  role: "player",
2023
1997
  accountId: "acct_xyz",
2024
1998
  },
2025
- }])
1999
+ }
2026
2000
 
2027
2001
  Runtime — create a session (sign a JWT):
2028
2002
  → CALL ductape_generate_payload FIRST (operation_family="session", method="start", targets={tag})
@@ -2060,20 +2034,51 @@ Analytics:
2060
2034
 
2061
2035
  Token format: "session_tag:jwt_token" — pass this format verbatim wherever session is expected.
2062
2036
  JWT is signed with the product private key (not a symmetric shared secret).
2037
+
2038
+ SESSION PROPAGATION
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
2066
+
2067
+ Backend propagation attributes component activity to an actor, but it does not record frontend
2068
+ pageviews, navigation, UI intent, funnels, or client failures. Continue immediately with
2069
+ ductape_docs({ topic: "frontend-analytics" }); both layers are required for a complete picture.
2063
2070
  `.trim(),
2064
2071
  caches: `
2065
2072
  DUCTAPE CACHES
2066
2073
 
2067
2074
  Caches are product-level Redis (or in-memory) stores for temporary key-value data with optional TTL.
2068
2075
 
2069
- Registration (admin — ductape_cli or SDK):
2076
+ Registration (admin — ductape_cli):
2070
2077
  ductape_cli("resources caches create -f cache.json")
2071
2078
  File: { name, tag, description?, expiry: <milliseconds> }
2072
2079
  No type or envs — Ductape manages the store infrastructure.
2073
2080
  expiry is in MILLISECONDS: 3600000 = 1 hour, 86400000 = 1 day, 604800000 = 1 week.
2074
2081
 
2075
- SDK: ductape_execute("caches.create", [product_tag, { name, tag, description?, expiry: 3600000 }])
2076
-
2077
2082
  Operations:
2078
2083
  caches.set [{ product, cache, key, value: string, expiry?: string (ISO 8601), env }]
2079
2084
  expiry is an ABSOLUTE TIMESTAMP (not a duration).
@@ -2121,20 +2126,20 @@ DUCTAPE NOTIFICATIONS
2121
2126
 
2122
2127
  Notifications send messages across multiple channels: email, SMS, push, or HTTP callback.
2123
2128
 
2124
- Create a notification (admin ductape_execute):
2125
- ductape_execute("notifications.create", [product_tag, {
2129
+ Create a notification through declarative apply or Workbench (administrative), using:
2130
+ {
2126
2131
  tag: "welcome-email",
2127
2132
  name: "Welcome Email",
2128
2133
  type: "email", // optional hint; actual channels configured per env
2129
- }])
2134
+ }
2130
2135
 
2131
- Create a message template:
2132
- ductape_execute("notifications.messages.create", [product_tag, {
2136
+ Create a message template through declarative apply or Workbench:
2137
+ {
2133
2138
  tag: "welcome-email:default", // format: "notification_tag:message_tag"
2134
2139
  notification: "welcome-email",
2135
2140
  subject: { template: "Welcome, {{name}}!", data: { name: "" } },
2136
2141
  body: { template: "Hi {{name}}, thanks for signing up.", data: { name: "" } },
2137
- }])
2142
+ }
2138
2143
 
2139
2144
  Send at runtime (one channel at a time):
2140
2145
  → CALL ductape_generate_payload FIRST (operation_family="notification", method="email.send")
@@ -2172,8 +2177,15 @@ DUCTAPE RESILIENCE
2172
2177
  Resilience covers three mechanisms: quotas (rate-limited provider pools), fallbacks (automatic
2173
2178
  provider switching), and healthchecks (continuous probe monitoring with failure actions).
2174
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
+
2175
2186
  QUOTAS — rate-limited multi-provider pools:
2176
- ductape_execute("quotas.create", [product_tag, {
2187
+ Workbench definition shape:
2188
+ {
2177
2189
  tag: "sms-quota",
2178
2190
  name: "SMS Provider Pool",
2179
2191
  input: { to: { type: "string", required: true }, message: { type: "string" } },
@@ -2187,7 +2199,7 @@ QUOTAS — rate-limited multi-provider pools:
2187
2199
  input: { "body:to": "$Input{to}", "body:body": "$Input{message}" },
2188
2200
  output: {} },
2189
2201
  ],
2190
- }])
2202
+ }
2191
2203
  Providers are tried in order until quota is not exhausted.
2192
2204
  quotas.run [{ product, env, tag, input }] ← CALL ductape_generate_payload FIRST
2193
2205
  quotas.dispatch [{ product, env, tag, input, schedule? }]
@@ -2195,12 +2207,13 @@ QUOTAS — rate-limited multi-provider pools:
2195
2207
  FALLBACKS — automatic provider switching on failure:
2196
2208
  Same schema as quotas but options are ordered: primary first, then fallback(s).
2197
2209
  Primary is used first; on failure, the next provider is tried automatically.
2198
- ductape_execute("fallback.create", [product_tag, { tag, name, input: { ... }, options: [...] }])
2210
+ Configure in Workbench: { tag, name, input: { ... }, options: [...] }
2199
2211
  fallback.run [{ product, env, tag, input }] ← CALL ductape_generate_payload FIRST
2200
2212
  fallback.dispatch [{ product, env, tag, input, schedule? }]
2201
2213
 
2202
2214
  HEALTHCHECKS — continuous probe with failure notifications:
2203
- ductape_execute("health.create", [product_tag, {
2215
+ Workbench definition shape:
2216
+ {
2204
2217
  tag: "payment-health",
2205
2218
  name: "Payment Service Health",
2206
2219
  probe: { type: "app", app: "stripe-app", event: "ping" },
@@ -2212,7 +2225,7 @@ HEALTHCHECKS — continuous probe with failure notifications:
2212
2225
  channels: { email: { recipients: ["ops@example.com"] } } }],
2213
2226
  webhooks: [{ url: "https://hooks.example.com/alert", method: "POST" }],
2214
2227
  },
2215
- }])
2228
+ }
2216
2229
  health.run [{ product, env, tag }] → triggers an immediate probe
2217
2230
  health.check [{ product, env, tag }] → same as run
2218
2231
  health.status [{ product, env, tag }] → current health status
@@ -2222,6 +2235,239 @@ Failure actions: notification channels, HTTP webhooks, and/or message broker emi
2222
2235
  be configured simultaneously on the same healthcheck.
2223
2236
  Input template references: $Input{field} → maps declared input to the probe's action input.
2224
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.
2255
+ `.trim(),
2256
+ 'frontend-analytics': `
2257
+ DUCTAPE FRONTEND PRODUCT ANALYTICS AND SESSION ACTIVITY
2258
+
2259
+ Frontend analytics and backend session attribution are complementary; neither replaces the other.
2260
+
2261
+ BACKEND OPERATION ATTRIBUTION
2262
+ Pass the original full Ductape session token to every user-initiated database, Event, Feature,
2263
+ notification, storage, graph, vector, action, and other runtime operation that accepts session.
2264
+ This attributes server work to the authenticated actor. Durable work should retain the initiating
2265
+ session; intentional system/background work should remain sessionless.
2266
+
2267
+ FRONTEND PRODUCT ANALYTICS
2268
+ Use the browser Analytics service for anonymous visits, authenticated page views, navigation,
2269
+ UI interactions, funnels, client errors, realtime state, and product-surface engagement.
2270
+
2271
+ IMPORTANT:
2272
+ Passing session to backend component calls does not replace frontend analytics.
2273
+ Calling analytics.identify(sessionToken) and tracking frontend events does not replace backend
2274
+ session propagation. A complete activity picture requires both.
2275
+
2276
+ VERSION AND DOCUMENTATION PRECEDENCE
2277
+ 1. Installed package types and exports determine what can be called now.
2278
+ 2. Version-matched package documentation explains intended usage.
2279
+ 3. Current docs/docs/Frontend describes the latest supported design.
2280
+ 4. If they disagree, report the mismatch; never fabricate compatibility.
2281
+
2282
+ Inspect package.json plus package exports/type declarations before recommending framework hooks.
2283
+ The current repository exports useAnalytics from @ductape/react and @ductape/vue. Their installed
2284
+ hook/composable exposes track, pageview, identify, visitorId, enableAutoCapture, and flush.
2285
+ clearSession and disableAutoCapture are available on client.analytics but are not currently
2286
+ returned by those framework wrappers. Use useDuctape().client.analytics for those calls, or
2287
+ recommend a package version that exports them; do not generate a hook method that does not exist.
2288
+
2289
+ CLIENT API — IDENTIFY AFTER AUTHENTICATION
2290
+ ductape.analytics.identify(sessionToken);
2291
+
2292
+ sessionToken is the complete value returned by Ductape, in "player-session:jwt" format.
2293
+ It is NOT a player ID, session tag, session ID, decoded claims, or refresh token.
2294
+ identify links subsequent frontend analytics to the authenticated Ductape session and lets
2295
+ supported analytics correlate anonymous pre-login activity with authenticated activity.
2296
+
2297
+ LOGOUT / ACCOUNT SWITCHING
2298
+ await ductape.analytics.flush();
2299
+ ductape.analytics.clearSession();
2300
+
2301
+ Clear analytics identity when logout or revocation succeeds, refresh fails irrecoverably, local
2302
+ authentication is removed, or a different user is about to authenticate in the same browser.
2303
+ Removing only the application token can leave later anonymous/next-user events associated with
2304
+ the previous analytics identity. Flush is best-effort; browser shutdown does not guarantee it.
2305
+
2306
+ CUSTOM EVENTS
2307
+ await ductape.analytics.track({
2308
+ event: 'order_submitted',
2309
+ traceId,
2310
+ properties: { matchId, orderType },
2311
+ });
2312
+
2313
+ IAnalyticsTrackOptions:
2314
+ event: string
2315
+ properties?: Record<string, unknown>
2316
+ session?: string
2317
+ product?: string
2318
+ env?: string
2319
+ traceId?: string
2320
+ context?: { url?, path?, referrer?, locale?, screen?: { width, height } }
2321
+
2322
+ identify establishes the default analytics session. An individual event may explicitly provide
2323
+ session. Normally use product/env from client configuration. traceId correlates frontend intent
2324
+ with backend logs, Events, Features, and order processing.
2325
+
2326
+ PAGE VIEWS
2327
+ await ductape.analytics.pageview({
2328
+ path: location.pathname,
2329
+ title: document.title,
2330
+ properties: { matchId, screen: 'governance' },
2331
+ });
2332
+
2333
+ IAnalyticsPageviewOptions:
2334
+ path?: string
2335
+ title?: string
2336
+ session?: string
2337
+ product?: string
2338
+ env?: string
2339
+ properties?: Record<string, unknown>
2340
+
2341
+ AUTO-CAPTURE — OPT IN ONLY AFTER A PRIVACY AUDIT
2342
+ const stopAutoCapture = ductape.analytics.enableAutoCapture({
2343
+ session: () => authSession?.token,
2344
+ clicks: false,
2345
+ pageviews: true,
2346
+ maskTextSelectors: [
2347
+ '[data-private]',
2348
+ '[data-secret]',
2349
+ '[data-player-message]',
2350
+ '[data-intelligence-report]',
2351
+ ],
2352
+ });
2353
+ stopAutoCapture(); // or ductape.analytics.disableAutoCapture()
2354
+
2355
+ Mark sensitive UI with data-private/data-secret attributes. Text masking may not mask attributes,
2356
+ IDs, URLs, element names, custom properties, console errors, or network errors. Inspect actual
2357
+ payloads before production. For hidden-information products, begin with reviewed automatic
2358
+ pageviews, clicks disabled, and preferred custom named events.
2359
+
2360
+ VISITOR ID
2361
+ const visitorId = ductape.analytics.getVisitorId();
2362
+ A visitor ID is anonymous analytics identity, not authentication or authorization.
2363
+
2364
+ FRONTEND SESSION LIFECYCLE
2365
+ Before login:
2366
+ - Track anonymous pageviews/onboarding; do not invent a session.
2367
+ After login/registration:
2368
+ - Store the session securely, identify(fullSessionToken), track success, start authenticated
2369
+ pageviews, and pass the same token to user-context realtime/component operations.
2370
+ After refresh:
2371
+ - Replace the old token, identify(newSessionToken), update realtime connections/subscriptions,
2372
+ and use the refreshed token for future backend operations.
2373
+ Logout:
2374
+ - Optionally track logout_initiated, flush, revoke, disconnect realtime, clearSession, then
2375
+ remove local authentication.
2376
+ Refresh failure/revocation:
2377
+ - Disconnect user-context clients, clearSession, clear local auth, navigate to authentication,
2378
+ and track only anonymous events afterward.
2379
+
2380
+ REACT ROUTE TRACKING (verify installed exports first)
2381
+ import { useAnalytics, useDuctape } from '@ductape/react';
2382
+ import { useEffect } from 'react';
2383
+ import { useLocation } from 'react-router-dom';
2384
+
2385
+ function ProductAnalytics({ sessionToken }: { sessionToken?: string }) {
2386
+ const analytics = useAnalytics();
2387
+ const { client } = useDuctape();
2388
+ const location = useLocation();
2389
+ useEffect(() => {
2390
+ if (sessionToken) analytics.identify(sessionToken);
2391
+ else client.analytics.clearSession();
2392
+ }, [analytics, client, sessionToken]);
2393
+ useEffect(() => {
2394
+ void analytics.pageview({ path: location.pathname, title: document.title });
2395
+ }, [analytics, location.pathname]);
2396
+ return null;
2397
+ }
2398
+
2399
+ SAFE EVENT TAXONOMY
2400
+ Define stable names centrally; do not invent variants throughout components.
2401
+ Authentication:
2402
+ registration_started, registration_completed, registration_failed,
2403
+ login_started, login_completed, login_failed, session_refreshed,
2404
+ session_refresh_failed, logout_completed
2405
+ Match:
2406
+ match_list_viewed, match_creation_started, match_created, match_joined, lobby_viewed,
2407
+ player_marked_ready, match_preparation_started, world_loaded, match_reconnected,
2408
+ match_completed, endgame_viewed
2409
+ Orders:
2410
+ order_form_opened, order_previewed, order_submission_started, order_submitted,
2411
+ order_submission_failed, order_cancelled, boundary_result_viewed
2412
+ Realtime:
2413
+ realtime_connect_started, realtime_connected, realtime_disconnected,
2414
+ realtime_reconnect_attempted, realtime_subscription_failed, projection_refresh_failed,
2415
+ client_error
2416
+ Funnels:
2417
+ tutorial_started, tutorial_step_completed, tutorial_abandoned, first_match_created,
2418
+ first_order_submitted, first_boundary_viewed, first_match_completed
2419
+
2420
+ Safe properties include matchId, orderType, screen, tick, result, and errorCategory.
2421
+
2422
+ HIDDEN AND SENSITIVE DATA — NEVER SEND TO ANALYTICS
2423
+ Do not track exact hidden formations, operative/handler identities, secret operation payloads,
2424
+ false-report truth markers, undiscovered evasion details, invisible treaties, canonical hidden
2425
+ map state, private messages, passwords, authorization headers, session/refresh tokens in event
2426
+ properties, or full errors/records that may contain secrets. Analytics must observe usage, not
2427
+ become a hidden-state side channel.
2428
+
2429
+ CORRELATION
2430
+ Frontend: create traceId = crypto.randomUUID(), track intent with traceId, and send traceId with
2431
+ the actual request. Backend logs the same traceId and propagates session to Ductape operations.
2432
+ Track completion with the same traceId.
2433
+
2434
+ traceId = correlation
2435
+ session = actor attribution
2436
+ idempotencyKey = duplicate prevention
2437
+ matchId/orderId = domain identity
2438
+ These values are not interchangeable.
2439
+
2440
+ OWNERSHIP BOUNDARIES
2441
+ Analytics is never an authoritative order, authorization proof, or gameplay source of truth.
2442
+ Analytics failure must not block or alter gameplay. The server verifies sessions independently;
2443
+ authoritative database and Event streams remain the source of truth. Prefer non-blocking
2444
+ analytics except explicit best-effort flushes at safe lifecycle transitions.
2445
+
2446
+ FRONTEND PROJECT AUDIT
2447
+ Inspect installed @ductape/client/react/vue versions and actual exports; client/provider setup;
2448
+ identify after login and refresh; clearSession on logout/account switch; SPA pageviews; auto-
2449
+ capture and masking; taxonomy consistency; sensitive custom properties; and shared trace IDs.
2450
+ Report capability as Present/Missing/Partial/Not applicable with concrete findings.
2451
+
2452
+ WHEN “SESSION ACTIVITY IS MISSING FROM THE DASHBOARD”
2453
+ Investigate both tracks before blaming the SDK.
2454
+ Backend: start/verify format, correct env, session on database/Event/Feature/notification/etc.,
2455
+ and intentional background sessionlessness.
2456
+ Frontend: identify after login/refresh, clearSession on logout, pageviews/custom events, flush,
2457
+ correct publishable key/product/env, installed API compatibility, browser failures, and privacy
2458
+ controls that may suppress events.
2459
+
2460
+ PAYLOAD RECIPES
2461
+ Anonymous:
2462
+ await ductape.analytics.pageview({ path: window.location.pathname, title: document.title });
2463
+ Authenticated:
2464
+ ductape.analytics.identify(playerSessionToken);
2465
+ await ductape.analytics.track({
2466
+ event: 'match_created', session: playerSessionToken, traceId, properties: { matchId },
2467
+ });
2468
+ Logout:
2469
+ await ductape.analytics.flush();
2470
+ ductape.analytics.clearSession();
2225
2471
  `.trim(),
2226
2472
  features: `
2227
2473
  DUCTAPE FEATURES
@@ -2285,9 +2531,11 @@ STEP 4 — PRESENT the plan and get approval BEFORE writing any code or creating
2285
2531
  Wait for confirmation before writing code or calling any create tool.
2286
2532
 
2287
2533
  STEP 5 — CREATE missing components (only with user approval)
2288
- Use ductape_execute for any missing databases, apps, database actions, notification events, etc.
2289
- For a missing database action:
2290
- ductape_execute("databases.action.create", [product_tag, db_tag, { tag, name, type, query }])
2534
+ Administrative assets must never be created through ductape_execute. Use ductape_cli for products,
2535
+ apps, supported resources, broker topics, cloud connections, secrets, and declarative apply flows.
2536
+ App actions, auths, Features, quotas, fallbacks, health checks, and other assets for which the CLI
2537
+ has no command must be configured in Workbench. Do not generate an impossible publishable-key
2538
+ create/update call. For a missing database action, configure it in Workbench, then verify it exists.
2291
2539
  For a missing child feature, recursively apply this same workflow.
2292
2540
  Tell the user what you are about to create before each tool call.
2293
2541
 
@@ -2411,7 +2659,11 @@ When you call features.define({ handler }), the handler runs TWICE:
2411
2659
 
2412
2660
  Invoke internal application business logic (your own NestJS/backend service code):
2413
2661
  → produce a broker event (ctx.messaging.produce or ductape.events.produce)
2414
- consume it in your NestJS service with events.consume in onModuleInit
2662
+ follow ductape_docs({ topic: "events" }) and use the canonical NestJS decorator:
2663
+ @Events.Consumer({ event: "broker-tag:topic-tag" })
2664
+ async handle(message: MessageShape) { /* injected-service business logic; throw to nack */ }
2665
+ → DuctapeModule auto-registers the decorated consumer; no manual onModuleInit is needed
2666
+ → events.consume() remains a supported lower-level alternative for plain TypeScript/Node.js
2415
2667
  → your service method runs with full access to DI, DB transactions, etc.
2416
2668
  Do NOT create an App Action just to call your own service over HTTP.
2417
2669
 
@@ -2907,9 +3159,15 @@ SESSIONS SERVICE
2907
3159
  const { token } = await ductape.sessions.refresh({ refreshToken: '...' });
2908
3160
 
2909
3161
  ANALYTICS SERVICE
2910
- ductape.analytics.pageview({ page: '/dashboard' });
2911
- ductape.analytics.track('button_click', { button: 'sign-up' });
2912
- ductape.analytics.identify({ userId: 'u_123', traits: { plan: 'pro' } });
3162
+ ductape.analytics.identify('player-session:eyJ...'); // full Ductape session token
3163
+ await ductape.analytics.pageview({ path: '/dashboard', title: document.title });
3164
+ await ductape.analytics.track({
3165
+ event: 'button_clicked',
3166
+ properties: { button: 'sign-up' },
3167
+ });
3168
+ await ductape.analytics.flush();
3169
+ ductape.analytics.clearSession(); // logout/account switch
3170
+ See ductape_docs({ topic: "frontend-analytics" }) before enabling auto-capture.
2913
3171
 
2914
3172
  FRAMEWORK-SPECIFIC PACKAGES
2915
3173
  For React and Vue projects, use the dedicated packages instead of managing the client manually:
@@ -3010,13 +3268,15 @@ AGENT HOOKS
3010
3268
  useAgentSignal(hookOptions?) → { mutate, isLoading, error }
3011
3269
 
3012
3270
  BROKER HOOKS
3013
- useBroker(broker, options?)
3271
+ useBroker()
3014
3272
  → { isConnected, isConnecting, error, connect, disconnect }
3273
+ connect({ broker, session?, product?, env? }) forwards the complete options object to
3274
+ @ductape/client. Pass session for player-scoped authorization.
3015
3275
  useBrokerPublish(hookOptions?)
3016
3276
  → { mutate({ topic, message, headers?, key? }), isLoading, error, data }
3017
3277
  useBrokerSubscription(subscribeOptions, hookOptions?)
3018
3278
  → { data: BrokerMessage[], isSubscribed, error, unsubscribe, resubscribe }
3019
- subscribeOptions: { topic, group? }
3279
+ subscribeOptions: { broker?, topic, group?, session?, product?, env? }
3020
3280
 
3021
3281
  GRAPH HOOKS
3022
3282
  useGraph(graphTag) → { isConnected, isConnecting, error, connect, disconnect }
@@ -3086,7 +3346,10 @@ ACTIONS HOOKS
3086
3346
  useActionRun(hookOptions?) → { mutate, isLoading, error, data }
3087
3347
 
3088
3348
  ANALYTICS HOOK
3089
- useAnalytics() → { pageview, track, identify }
3349
+ useAnalytics() → { pageview, track, identify, visitorId, enableAutoCapture, flush }
3350
+ The currently installed hook does NOT return clearSession or disableAutoCapture.
3351
+ Use useDuctape().client.analytics.clearSession()/disableAutoCapture(), or verify a newer installed
3352
+ package export before generating those hook calls. See ductape_docs({ topic: "frontend-analytics" }).
3090
3353
 
3091
3354
  hookOptions pattern (applies to all hooks):
3092
3355
  enabled? boolean — false skips auto-fetch/subscribe
@@ -3255,7 +3518,10 @@ ACTIONS COMPOSABLES
3255
3518
  useActionRun(composableOptions?) → { mutate, isLoading, error, data }
3256
3519
 
3257
3520
  ANALYTICS COMPOSABLE
3258
- useAnalytics() → { pageview, track, identify }
3521
+ useAnalytics() → { pageview, track, identify, visitorId, enableAutoCapture, flush }
3522
+ The currently installed composable does NOT return clearSession or disableAutoCapture.
3523
+ Use useDuctape().client.analytics.clearSession()/disableAutoCapture(), or verify a newer installed
3524
+ package export before generating those composable calls. See ductape_docs({ topic: "frontend-analytics" }).
3259
3525
 
3260
3526
  composableOptions pattern (applies to all composables):
3261
3527
  enabled? boolean — false skips auto-fetch/subscribe
@@ -3443,6 +3709,22 @@ async function main() {
3443
3709
  };
3444
3710
  const executeHandler = async (args) => {
3445
3711
  try {
3712
+ const runtimeMutationMethods = {
3713
+ databases: new Set(['insert', 'update', 'delete', 'upsert']),
3714
+ graph: new Set(['insert', 'update', 'delete']),
3715
+ vector: new Set(['insert', 'upsert', 'upsertOne', 'delete']),
3716
+ sessions: new Set(['revoke']),
3717
+ };
3718
+ const isRuntimeDataMutation = runtimeMutationMethods[args.module]?.has(args.method) === true;
3719
+ const isAdministrativeMutation = (/^migration\./.test(args.method) ||
3720
+ /^schema\.(create|drop|add|remove|update)/.test(args.method) ||
3721
+ /(^|\.)(create|update|delete|configure|add|remove|revoke)$/.test(args.method)) &&
3722
+ !isRuntimeDataMutation;
3723
+ if (isAdministrativeMutation) {
3724
+ throw new Error(`Administrative operation "${args.module}.${args.method}" is blocked in ductape_execute. ` +
3725
+ 'Use ductape_cli when that resource is supported by the CLI; otherwise configure it in Workbench. ' +
3726
+ 'The runtime proxy uses a publishable key and cannot administer platform assets.');
3727
+ }
3446
3728
  const key = args.publishable_key || process.env.DUCTAPE_PUBLISHABLE_KEY;
3447
3729
  if (!key) {
3448
3730
  throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
@@ -3489,7 +3771,7 @@ async function main() {
3489
3771
  if (!key) {
3490
3772
  throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
3491
3773
  }
3492
- const result = await generateExecutablePayload({ ...args, publishable_key: key });
3774
+ const result = addSessionAwarenessMetadata(await generateExecutablePayload({ ...args, publishable_key: key }), args);
3493
3775
  let text = JSON.stringify(result ?? null, null, 2);
3494
3776
  if (args.operation_family === 'database') {
3495
3777
  const meta = result?.meta ?? {};
@@ -3516,7 +3798,7 @@ async function main() {
3516
3798
  throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
3517
3799
  }
3518
3800
  ensureSupportedSnippetOperation(args.operation_family, args.method);
3519
- const generated = await generateExecutablePayload({ ...args, publishable_key: key });
3801
+ const generated = addSessionAwarenessMetadata(await generateExecutablePayload({ ...args, publishable_key: key }), args);
3520
3802
  const payload = generated?.payload ?? {};
3521
3803
  const snippet = buildSnippet(args.language, payload, args.operation_family, args.method);
3522
3804
  return {
@@ -3598,7 +3880,7 @@ async function main() {
3598
3880
  'index strategy, operation types) that should be confirmed with the user first.\n\n' +
3599
3881
  'Available topics: transactions, presave, triggers, aggregations, migrations, indexes, performance, actions, ' +
3600
3882
  'graphs, storage, cloud, vector, warehouse, secrets, apps, products, sessions, caches, ' +
3601
- 'notifications, resilience, features, events, logs, frontend, client, react, vue',
3883
+ 'notifications, resilience, features, events, logs, frontend, frontend-analytics, client, react, vue',
3602
3884
  inputSchema: docsInputSchema,
3603
3885
  }, docsHandler);
3604
3886
  server.registerTool('ductape_cli', {
@@ -3669,7 +3951,7 @@ async function main() {
3669
3951
  ' Step 2 — check if the database component already exists:\n' +
3670
3952
  ' ductape_cli("resources databases list <product_tag> --json")\n' +
3671
3953
  ' If a component already uses the same Atlas cluster, do NOT re-import — instead update it:\n' +
3672
- ' ductape_execute("databases.updateDatabase", [product_tag, db_tag, { envs: [...updated envs...] }])\n' +
3954
+ ' use ductape_cli resource update when supported; otherwise update it in Workbench\n' +
3673
3955
  ' Add or change the dbName in the env\'s connection_url to switch databases on the same cluster.\n' +
3674
3956
  ' Step 3 — import (only if no existing component uses this cluster):\n' +
3675
3957
  ' Use import-persist-all with one entry per product env. Required fields per entry:\n' +