@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/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 calling ductape_execute to create or update any asset — use this to discover required fields
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,
@@ -1242,7 +1294,7 @@ const docsInputSchema = z.object({
1242
1294
  'Feature topic to look up. Supported: ' +
1243
1295
  'transactions, presave, triggers, aggregations, migrations, indexes, performance, actions, ' +
1244
1296
  'graphs, storage, cloud, vector, warehouse, secrets, apps, products, sessions, caches, ' +
1245
- 'notifications, resilience, features, events, logs, frontend, client, react, vue',
1297
+ 'notifications, resilience, features, events, logs, frontend, frontend-analytics, client, react, vue',
1246
1298
  ),
1247
1299
  });
1248
1300
 
@@ -1280,6 +1332,11 @@ SHARED APPLICATION LIFECYCLE
1280
1332
  connectionState to render disconnected/reconnecting UI; do not create duplicate subscriptions.
1281
1333
  7. Disconnect resource sessions and the root client when the owning application scope is torn down.
1282
1334
 
1335
+ PRODUCT ANALYTICS
1336
+ Frontend product analytics complements backend session propagation; neither replaces the other.
1337
+ Continue with ductape_docs({ topic: "frontend-analytics" }) for identity lifecycle, pageviews,
1338
+ custom events, privacy masking, hidden-state safety, trace correlation, and package-version checks.
1339
+
1283
1340
  AUTHENTICATION AND SECURITY
1284
1341
  - Use publishableKey in browser applications. Never ship workspace private keys or privileged
1285
1342
  access keys in frontend bundles.
@@ -1457,33 +1514,9 @@ DUCTAPE DATABASE MIGRATIONS
1457
1514
 
1458
1515
  Migrations are versioned SQL/NoSQL schema change scripts managed per database component.
1459
1516
 
1460
- Create a migration:
1461
- ductape_execute("databases.migration.create", [{
1462
- product: "my-product",
1463
- database: "core-db",
1464
- data: {
1465
- name: "add users table",
1466
- tag: "001-add-users",
1467
- value: {
1468
- up: ["CREATE TABLE users (id SERIAL PRIMARY KEY, email TEXT NOT NULL UNIQUE)"],
1469
- down: ["DROP TABLE users"],
1470
- },
1471
- },
1472
- }])
1473
-
1474
- Run migrations (applies all pending):
1475
- ductape_execute("databases.migration.run", [migrations, { env: "prd" }])
1476
-
1477
- Rollback:
1478
- ductape_execute("databases.migration.rollback", [migrations, 1]) // roll back 1
1517
+ Create migrations through the project migration files and ductape_cli, never ductape_execute.
1479
1518
 
1480
- Check status:
1481
- ductape_execute("databases.migration.status", [migrations])
1482
-
1483
- History:
1484
- ductape_execute("databases.migration.history", [])
1485
-
1486
- Via CLI:
1519
+ Use the access-key administrative CLI for running, rolling back, and inspecting migrations:
1487
1520
  ductape_cli("db migrate") // run pending
1488
1521
  ductape_cli("db migrate rollback") // roll back last
1489
1522
  ductape_cli("db migrate rollback -n 3")
@@ -1494,25 +1527,13 @@ MongoDB: migrations run as raw Mongo shell commands in the up/down arrays.
1494
1527
  indexes: `
1495
1528
  DUCTAPE DATABASE INDEXES
1496
1529
 
1497
- Create an index:
1498
- ductape_execute("databases.schema.createIndex", [
1499
- "collection_name",
1500
- ["field1", "field2"], // simple array of field names
1501
- { unique: true, name: "idx_email_unique" } // options (optional)
1502
- ])
1503
-
1504
- // Ordered index:
1505
- ductape_execute("databases.schema.createIndex", [
1506
- "orders",
1507
- [{ field: "createdAt", order: "DESC" }, { field: "userId", order: "ASC" }],
1508
- { name: "idx_orders_date_user" }
1509
- ])
1510
-
1511
- Drop an index:
1512
- ductape_execute("databases.schema.dropIndex", ["collection_name", "index_name"])
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" }].
1513
1534
 
1514
- List indexes on a collection:
1515
- 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.
1516
1537
 
1517
1538
  Performance guidance:
1518
1539
  - Index fields used in WHERE clauses, JOIN conditions, and ORDER BY.
@@ -1558,18 +1579,7 @@ DUCTAPE DATABASE ACTIONS
1558
1579
  A database action is a saved query or mutation (SQL string or NoSQL command) stored
1559
1580
  on the Ductape product and executed by tag at runtime.
1560
1581
 
1561
- Create an action (admin):
1562
- ductape_execute("databases.action.create", [{
1563
- product: "my-product",
1564
- database: "core-db",
1565
- data: {
1566
- tag: "get-active-users",
1567
- name: "Get active users",
1568
- description: "Returns all users with status=active",
1569
- type: "sql", // "sql" or "nosql"
1570
- query: "SELECT * FROM users WHERE status = :status",
1571
- },
1572
- }])
1582
+ Create/update actions in Workbench (administrative access), never through ductape_execute.
1573
1583
 
1574
1584
  Dispatch an action at runtime:
1575
1585
  → CALL ductape_generate_payload FIRST to get the canonical input shape.
@@ -1584,10 +1594,9 @@ Dispatch an action at runtime:
1584
1594
  List actions for a database:
1585
1595
  ductape_execute("databases.action.list", ["database_tag"])
1586
1596
 
1587
- Fetch / update / delete:
1597
+ Fetch:
1588
1598
  ductape_execute("databases.action.fetch", ["action_tag"])
1589
- ductape_execute("databases.action.update", ["my-product", "action_tag", { query: "..." }])
1590
- ductape_execute("databases.action.delete", ["action_tag"])
1599
+ Update/delete are administrative and must be performed in Workbench.
1591
1600
 
1592
1601
  Actions are the preferred way to encapsulate complex or reused queries — they can be
1593
1602
  scheduled, dispatched with retries, and audited via logs.
@@ -1851,16 +1860,8 @@ DUCTAPE SECRETS
1851
1860
  Secrets are workspace-level encrypted key-value pairs. They are referenced in resource configs,
1852
1861
  connection URLs, and any string field using the $Secret{KEY_NAME} syntax.
1853
1862
 
1854
- Create (admin ductape_cli or ductape_execute):
1855
- ductape_execute("secrets.create", [{
1856
- key: "STRIPE_API_KEY",
1857
- value: "sk_live_...", // plaintext — encrypted AES-256-GCM client-side before send
1858
- description: "Stripe live key",
1859
- token_type: "api", // "api" | "password" | "certificate"
1860
- scope: ["my-product"], // which products can read this secret
1861
- envs: ["prd"], // which env slugs can read this secret
1862
- expires_at: 1800000000, // optional epoch ms
1863
- }])
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.
1864
1865
  The server never receives the plaintext value. Encryption uses the workspace private key.
1865
1866
 
1866
1867
  Fetch / resolve:
@@ -1875,10 +1876,7 @@ $Secret{} reference syntax:
1875
1876
  - The in-memory cache stores the encrypted form only; decryption happens on each cache hit.
1876
1877
  - Cache TTL: 5 minutes. Clear with: secrets.clearCache()
1877
1878
 
1878
- Lifecycle:
1879
- ductape_execute("secrets.revoke", ["KEY"]) → disables without deleting (recoverable)
1880
- ductape_execute("secrets.delete", ["KEY"]) → permanent deletion (irreversible)
1881
- ductape_execute("secrets.update", ["KEY", { value: "new_value", expires_at: ... }])
1879
+ Lifecycle mutations (revoke/delete/update) are administrative: use ductape_cli or Workbench.
1882
1880
 
1883
1881
  List all secrets (keys only — values are not returned in list):
1884
1882
  ductape_execute("secrets.list", [])
@@ -1929,9 +1927,7 @@ Import from a file:
1929
1927
  ductape_cli("apps import <file.json> -t postman|openapi")
1930
1928
  Supports Postman v2.1 collection and OpenAPI 3.0 spec.
1931
1929
 
1932
- Manage environments (base URLs per stage):
1933
- ductape_execute("app.environments.create", [app_tag, { slug, env_name, base_url }])
1934
- ductape_execute("app.environments.list", [app_tag])
1930
+ Manage environments (base URLs per stage) in Workbench; this currently has no CLI command.
1935
1931
 
1936
1932
  Discover apps in a product and their actions (ALWAYS do this before writing any ctx.api.run call):
1937
1933
  Step 1 — list apps connected to the product:
@@ -1948,16 +1944,8 @@ Discover apps in a product and their actions (ALWAYS do this before writing any
1948
1944
  app resources (not for runtime input — use actions.fetch or ductape_generate_payload for that)
1949
1945
  NEVER assume action input field names. Always fetch the action definition first.
1950
1946
 
1951
- Manage actions (individual API endpoints):
1952
- ductape_execute("actions.create", [app_tag, {
1953
- tag, name, resource: "/users/{id}", method: "GET"|"POST"|"PUT"|"PATCH"|"DELETE",
1954
- body?: { fieldName: { type, required? } },
1955
- params?: { id: { type: "string" } },
1956
- query?: { filter: { type: "string" } },
1957
- headers?: { Authorization: { type: "string" } },
1958
- response?: { status_code: 200, success: true, body: { ... }, response_format: "json" },
1959
- }])
1960
- 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.
1961
1949
  ductape_execute("actions.list", [app_tag])
1962
1950
  ductape_execute("actions.fetch", [app_tag, action_tag])
1963
1951
 
@@ -1978,16 +1966,14 @@ Run an action at runtime:
1978
1966
 
1979
1967
  Auth schemes (how the app authenticates outbound requests):
1980
1968
  Setup types: header | bearer | basic | oauth2 | apikey
1981
- ductape_execute("auths.create", [app_tag, { tag, name, setup_type, expiry, period, action_tag? }])
1969
+ Configure auth in Workbench (administrative).
1982
1970
  ductape_execute("auths.list", [app_tag])
1983
1971
 
1984
1972
  Webhooks (inbound events from the external service):
1985
- ductape_execute("webhooks.create", [app_tag, { tag, name, description, envs: [{ slug, registration_url?, method? }] }])
1986
- ductape_execute("webhooks.events.create", [app_tag, { tag, name, selector, description, sample }])
1973
+ Configure webhooks and webhook events in Workbench (administrative).
1987
1974
 
1988
1975
  Variables (per-env mutable values) and Constants (fixed values):
1989
- ductape_execute("app.variables.create", [app_tag, { key, value, env_slug }])
1990
- ductape_execute("app.constants.create", [app_tag, { key, value }])
1976
+ Configure variables and constants in Workbench (administrative).
1991
1977
 
1992
1978
  Connecting an app to a product (after creation):
1993
1979
  NOTE: All product.* module methods require the access key and CANNOT use ductape_execute.
@@ -1995,14 +1981,13 @@ Connecting an app to a product (after creation):
1995
1981
 
1996
1982
  FULL FLOW to make an app callable from a product:
1997
1983
  1. Create the app: ductape_cli("apps create --name \\"Service Name\\" --description \\"...\\"")
1998
- 2. Add environments: ductape_execute("app.environments.create", [app_tag, { slug: "prd", env_name: "Production", base_url: "https://api.example.com" }])
1999
- 3. Configure auth: ductape_execute("auths.create", [app_tag, { tag, name, setup_type: "apikey"|"bearer"|"basic"|"oauth2", expiry, period }])
2000
- 4. Define actions: ductape_execute("actions.create", [app_tag, { tag, name, resource, method, body?, params?, query?, headers?, response? }])
1984
+ 2. Add environments: Workbench
1985
+ 3. Configure auth: Workbench
1986
+ 4. Define actions: Workbench
2001
1987
  OR import: ductape_cli("apps import <file.json> -t postman|openapi")
2002
1988
  5. Connect to product: Requires the product_id (from ductape_cli("products get --tag <tag> --json")).
2003
1989
  There is no CLI command for this step — the SDK product.apps.add method requires
2004
- an access key which only the backend can provide. Use ductape_execute via an
2005
- admin-authenticated context, or connect via the Workbench UI.
1990
+ an access key which only the backend can provide. Connect via Workbench.
2006
1991
  6. Verify: ductape_cli("products get --tag <product_tag> --json") → check apps[] contains the app
2007
1992
  ductape_execute("actions.list", [app_tag]) → verify actions are registered
2008
1993
  `.trim(),
@@ -2041,6 +2026,7 @@ Resource registration (all via ductape_cli or ductape_execute — see per-topic
2041
2026
  caches → ductape_docs({ topic: "caches" })
2042
2027
  notifications → ductape_docs({ topic: "notifications" })
2043
2028
  sessions → ductape_docs({ topic: "sessions" })
2029
+ frontend analytics → ductape_docs({ topic: "frontend-analytics" })
2044
2030
  resilience → ductape_docs({ topic: "resilience" })
2045
2031
  features → ductape_docs({ topic: "features" })
2046
2032
 
@@ -2078,8 +2064,8 @@ IMPORTANT — selector must be "$Session{fieldName}" format:
2078
2064
  CORRECT: selector: "$Session{playerId}"
2079
2065
  INCORRECT: selector: "playerId" ← WILL FAIL with "Selector should be in the format $Session{...}{key}"
2080
2066
 
2081
- Example (game product player identity in JWT):
2082
- ductape_execute("sessions.create", [product_tag, {
2067
+ Example definition (configure via declarative apply or Workbench, not ductape_execute):
2068
+ {
2083
2069
  tag: "player-session",
2084
2070
  name: "Player Session",
2085
2071
  expiry: 24,
@@ -2091,7 +2077,7 @@ Example (game product — player identity in JWT):
2091
2077
  role: "player",
2092
2078
  accountId: "acct_xyz",
2093
2079
  },
2094
- }])
2080
+ }
2095
2081
 
2096
2082
  Runtime — create a session (sign a JWT):
2097
2083
  → CALL ductape_generate_payload FIRST (operation_family="session", method="start", targets={tag})
@@ -2129,6 +2115,39 @@ Analytics:
2129
2115
 
2130
2116
  Token format: "session_tag:jwt_token" — pass this format verbatim wherever session is expected.
2131
2117
  JWT is signed with the product private key (not a symmetric shared secret).
2118
+
2119
+ SESSION PROPAGATION
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
2147
+
2148
+ Backend propagation attributes component activity to an actor, but it does not record frontend
2149
+ pageviews, navigation, UI intent, funnels, or client failures. Continue immediately with
2150
+ ductape_docs({ topic: "frontend-analytics" }); both layers are required for a complete picture.
2132
2151
  `.trim(),
2133
2152
 
2134
2153
  caches: `
@@ -2136,14 +2155,12 @@ DUCTAPE CACHES
2136
2155
 
2137
2156
  Caches are product-level Redis (or in-memory) stores for temporary key-value data with optional TTL.
2138
2157
 
2139
- Registration (admin — ductape_cli or SDK):
2158
+ Registration (admin — ductape_cli):
2140
2159
  ductape_cli("resources caches create -f cache.json")
2141
2160
  File: { name, tag, description?, expiry: <milliseconds> }
2142
2161
  No type or envs — Ductape manages the store infrastructure.
2143
2162
  expiry is in MILLISECONDS: 3600000 = 1 hour, 86400000 = 1 day, 604800000 = 1 week.
2144
2163
 
2145
- SDK: ductape_execute("caches.create", [product_tag, { name, tag, description?, expiry: 3600000 }])
2146
-
2147
2164
  Operations:
2148
2165
  caches.set [{ product, cache, key, value: string, expiry?: string (ISO 8601), env }]
2149
2166
  expiry is an ABSOLUTE TIMESTAMP (not a duration).
@@ -2192,20 +2209,20 @@ DUCTAPE NOTIFICATIONS
2192
2209
 
2193
2210
  Notifications send messages across multiple channels: email, SMS, push, or HTTP callback.
2194
2211
 
2195
- Create a notification (admin ductape_execute):
2196
- ductape_execute("notifications.create", [product_tag, {
2212
+ Create a notification through declarative apply or Workbench (administrative), using:
2213
+ {
2197
2214
  tag: "welcome-email",
2198
2215
  name: "Welcome Email",
2199
2216
  type: "email", // optional hint; actual channels configured per env
2200
- }])
2217
+ }
2201
2218
 
2202
- Create a message template:
2203
- ductape_execute("notifications.messages.create", [product_tag, {
2219
+ Create a message template through declarative apply or Workbench:
2220
+ {
2204
2221
  tag: "welcome-email:default", // format: "notification_tag:message_tag"
2205
2222
  notification: "welcome-email",
2206
2223
  subject: { template: "Welcome, {{name}}!", data: { name: "" } },
2207
2224
  body: { template: "Hi {{name}}, thanks for signing up.", data: { name: "" } },
2208
- }])
2225
+ }
2209
2226
 
2210
2227
  Send at runtime (one channel at a time):
2211
2228
  → CALL ductape_generate_payload FIRST (operation_family="notification", method="email.send")
@@ -2244,8 +2261,15 @@ DUCTAPE RESILIENCE
2244
2261
  Resilience covers three mechanisms: quotas (rate-limited provider pools), fallbacks (automatic
2245
2262
  provider switching), and healthchecks (continuous probe monitoring with failure actions).
2246
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
+
2247
2270
  QUOTAS — rate-limited multi-provider pools:
2248
- ductape_execute("quotas.create", [product_tag, {
2271
+ Workbench definition shape:
2272
+ {
2249
2273
  tag: "sms-quota",
2250
2274
  name: "SMS Provider Pool",
2251
2275
  input: { to: { type: "string", required: true }, message: { type: "string" } },
@@ -2259,7 +2283,7 @@ QUOTAS — rate-limited multi-provider pools:
2259
2283
  input: { "body:to": "$Input{to}", "body:body": "$Input{message}" },
2260
2284
  output: {} },
2261
2285
  ],
2262
- }])
2286
+ }
2263
2287
  Providers are tried in order until quota is not exhausted.
2264
2288
  quotas.run [{ product, env, tag, input }] ← CALL ductape_generate_payload FIRST
2265
2289
  quotas.dispatch [{ product, env, tag, input, schedule? }]
@@ -2267,12 +2291,13 @@ QUOTAS — rate-limited multi-provider pools:
2267
2291
  FALLBACKS — automatic provider switching on failure:
2268
2292
  Same schema as quotas but options are ordered: primary first, then fallback(s).
2269
2293
  Primary is used first; on failure, the next provider is tried automatically.
2270
- ductape_execute("fallback.create", [product_tag, { tag, name, input: { ... }, options: [...] }])
2294
+ Configure in Workbench: { tag, name, input: { ... }, options: [...] }
2271
2295
  fallback.run [{ product, env, tag, input }] ← CALL ductape_generate_payload FIRST
2272
2296
  fallback.dispatch [{ product, env, tag, input, schedule? }]
2273
2297
 
2274
2298
  HEALTHCHECKS — continuous probe with failure notifications:
2275
- ductape_execute("health.create", [product_tag, {
2299
+ Workbench definition shape:
2300
+ {
2276
2301
  tag: "payment-health",
2277
2302
  name: "Payment Service Health",
2278
2303
  probe: { type: "app", app: "stripe-app", event: "ping" },
@@ -2284,7 +2309,7 @@ HEALTHCHECKS — continuous probe with failure notifications:
2284
2309
  channels: { email: { recipients: ["ops@example.com"] } } }],
2285
2310
  webhooks: [{ url: "https://hooks.example.com/alert", method: "POST" }],
2286
2311
  },
2287
- }])
2312
+ }
2288
2313
  health.run [{ product, env, tag }] → triggers an immediate probe
2289
2314
  health.check [{ product, env, tag }] → same as run
2290
2315
  health.status [{ product, env, tag }] → current health status
@@ -2294,6 +2319,240 @@ Failure actions: notification channels, HTTP webhooks, and/or message broker emi
2294
2319
  be configured simultaneously on the same healthcheck.
2295
2320
  Input template references: $Input{field} → maps declared input to the probe's action input.
2296
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.
2339
+ `.trim(),
2340
+
2341
+ 'frontend-analytics': `
2342
+ DUCTAPE FRONTEND PRODUCT ANALYTICS AND SESSION ACTIVITY
2343
+
2344
+ Frontend analytics and backend session attribution are complementary; neither replaces the other.
2345
+
2346
+ BACKEND OPERATION ATTRIBUTION
2347
+ Pass the original full Ductape session token to every user-initiated database, Event, Feature,
2348
+ notification, storage, graph, vector, action, and other runtime operation that accepts session.
2349
+ This attributes server work to the authenticated actor. Durable work should retain the initiating
2350
+ session; intentional system/background work should remain sessionless.
2351
+
2352
+ FRONTEND PRODUCT ANALYTICS
2353
+ Use the browser Analytics service for anonymous visits, authenticated page views, navigation,
2354
+ UI interactions, funnels, client errors, realtime state, and product-surface engagement.
2355
+
2356
+ IMPORTANT:
2357
+ Passing session to backend component calls does not replace frontend analytics.
2358
+ Calling analytics.identify(sessionToken) and tracking frontend events does not replace backend
2359
+ session propagation. A complete activity picture requires both.
2360
+
2361
+ VERSION AND DOCUMENTATION PRECEDENCE
2362
+ 1. Installed package types and exports determine what can be called now.
2363
+ 2. Version-matched package documentation explains intended usage.
2364
+ 3. Current docs/docs/Frontend describes the latest supported design.
2365
+ 4. If they disagree, report the mismatch; never fabricate compatibility.
2366
+
2367
+ Inspect package.json plus package exports/type declarations before recommending framework hooks.
2368
+ The current repository exports useAnalytics from @ductape/react and @ductape/vue. Their installed
2369
+ hook/composable exposes track, pageview, identify, visitorId, enableAutoCapture, and flush.
2370
+ clearSession and disableAutoCapture are available on client.analytics but are not currently
2371
+ returned by those framework wrappers. Use useDuctape().client.analytics for those calls, or
2372
+ recommend a package version that exports them; do not generate a hook method that does not exist.
2373
+
2374
+ CLIENT API — IDENTIFY AFTER AUTHENTICATION
2375
+ ductape.analytics.identify(sessionToken);
2376
+
2377
+ sessionToken is the complete value returned by Ductape, in "player-session:jwt" format.
2378
+ It is NOT a player ID, session tag, session ID, decoded claims, or refresh token.
2379
+ identify links subsequent frontend analytics to the authenticated Ductape session and lets
2380
+ supported analytics correlate anonymous pre-login activity with authenticated activity.
2381
+
2382
+ LOGOUT / ACCOUNT SWITCHING
2383
+ await ductape.analytics.flush();
2384
+ ductape.analytics.clearSession();
2385
+
2386
+ Clear analytics identity when logout or revocation succeeds, refresh fails irrecoverably, local
2387
+ authentication is removed, or a different user is about to authenticate in the same browser.
2388
+ Removing only the application token can leave later anonymous/next-user events associated with
2389
+ the previous analytics identity. Flush is best-effort; browser shutdown does not guarantee it.
2390
+
2391
+ CUSTOM EVENTS
2392
+ await ductape.analytics.track({
2393
+ event: 'order_submitted',
2394
+ traceId,
2395
+ properties: { matchId, orderType },
2396
+ });
2397
+
2398
+ IAnalyticsTrackOptions:
2399
+ event: string
2400
+ properties?: Record<string, unknown>
2401
+ session?: string
2402
+ product?: string
2403
+ env?: string
2404
+ traceId?: string
2405
+ context?: { url?, path?, referrer?, locale?, screen?: { width, height } }
2406
+
2407
+ identify establishes the default analytics session. An individual event may explicitly provide
2408
+ session. Normally use product/env from client configuration. traceId correlates frontend intent
2409
+ with backend logs, Events, Features, and order processing.
2410
+
2411
+ PAGE VIEWS
2412
+ await ductape.analytics.pageview({
2413
+ path: location.pathname,
2414
+ title: document.title,
2415
+ properties: { matchId, screen: 'governance' },
2416
+ });
2417
+
2418
+ IAnalyticsPageviewOptions:
2419
+ path?: string
2420
+ title?: string
2421
+ session?: string
2422
+ product?: string
2423
+ env?: string
2424
+ properties?: Record<string, unknown>
2425
+
2426
+ AUTO-CAPTURE — OPT IN ONLY AFTER A PRIVACY AUDIT
2427
+ const stopAutoCapture = ductape.analytics.enableAutoCapture({
2428
+ session: () => authSession?.token,
2429
+ clicks: false,
2430
+ pageviews: true,
2431
+ maskTextSelectors: [
2432
+ '[data-private]',
2433
+ '[data-secret]',
2434
+ '[data-player-message]',
2435
+ '[data-intelligence-report]',
2436
+ ],
2437
+ });
2438
+ stopAutoCapture(); // or ductape.analytics.disableAutoCapture()
2439
+
2440
+ Mark sensitive UI with data-private/data-secret attributes. Text masking may not mask attributes,
2441
+ IDs, URLs, element names, custom properties, console errors, or network errors. Inspect actual
2442
+ payloads before production. For hidden-information products, begin with reviewed automatic
2443
+ pageviews, clicks disabled, and preferred custom named events.
2444
+
2445
+ VISITOR ID
2446
+ const visitorId = ductape.analytics.getVisitorId();
2447
+ A visitor ID is anonymous analytics identity, not authentication or authorization.
2448
+
2449
+ FRONTEND SESSION LIFECYCLE
2450
+ Before login:
2451
+ - Track anonymous pageviews/onboarding; do not invent a session.
2452
+ After login/registration:
2453
+ - Store the session securely, identify(fullSessionToken), track success, start authenticated
2454
+ pageviews, and pass the same token to user-context realtime/component operations.
2455
+ After refresh:
2456
+ - Replace the old token, identify(newSessionToken), update realtime connections/subscriptions,
2457
+ and use the refreshed token for future backend operations.
2458
+ Logout:
2459
+ - Optionally track logout_initiated, flush, revoke, disconnect realtime, clearSession, then
2460
+ remove local authentication.
2461
+ Refresh failure/revocation:
2462
+ - Disconnect user-context clients, clearSession, clear local auth, navigate to authentication,
2463
+ and track only anonymous events afterward.
2464
+
2465
+ REACT ROUTE TRACKING (verify installed exports first)
2466
+ import { useAnalytics, useDuctape } from '@ductape/react';
2467
+ import { useEffect } from 'react';
2468
+ import { useLocation } from 'react-router-dom';
2469
+
2470
+ function ProductAnalytics({ sessionToken }: { sessionToken?: string }) {
2471
+ const analytics = useAnalytics();
2472
+ const { client } = useDuctape();
2473
+ const location = useLocation();
2474
+ useEffect(() => {
2475
+ if (sessionToken) analytics.identify(sessionToken);
2476
+ else client.analytics.clearSession();
2477
+ }, [analytics, client, sessionToken]);
2478
+ useEffect(() => {
2479
+ void analytics.pageview({ path: location.pathname, title: document.title });
2480
+ }, [analytics, location.pathname]);
2481
+ return null;
2482
+ }
2483
+
2484
+ SAFE EVENT TAXONOMY
2485
+ Define stable names centrally; do not invent variants throughout components.
2486
+ Authentication:
2487
+ registration_started, registration_completed, registration_failed,
2488
+ login_started, login_completed, login_failed, session_refreshed,
2489
+ session_refresh_failed, logout_completed
2490
+ Match:
2491
+ match_list_viewed, match_creation_started, match_created, match_joined, lobby_viewed,
2492
+ player_marked_ready, match_preparation_started, world_loaded, match_reconnected,
2493
+ match_completed, endgame_viewed
2494
+ Orders:
2495
+ order_form_opened, order_previewed, order_submission_started, order_submitted,
2496
+ order_submission_failed, order_cancelled, boundary_result_viewed
2497
+ Realtime:
2498
+ realtime_connect_started, realtime_connected, realtime_disconnected,
2499
+ realtime_reconnect_attempted, realtime_subscription_failed, projection_refresh_failed,
2500
+ client_error
2501
+ Funnels:
2502
+ tutorial_started, tutorial_step_completed, tutorial_abandoned, first_match_created,
2503
+ first_order_submitted, first_boundary_viewed, first_match_completed
2504
+
2505
+ Safe properties include matchId, orderType, screen, tick, result, and errorCategory.
2506
+
2507
+ HIDDEN AND SENSITIVE DATA — NEVER SEND TO ANALYTICS
2508
+ Do not track exact hidden formations, operative/handler identities, secret operation payloads,
2509
+ false-report truth markers, undiscovered evasion details, invisible treaties, canonical hidden
2510
+ map state, private messages, passwords, authorization headers, session/refresh tokens in event
2511
+ properties, or full errors/records that may contain secrets. Analytics must observe usage, not
2512
+ become a hidden-state side channel.
2513
+
2514
+ CORRELATION
2515
+ Frontend: create traceId = crypto.randomUUID(), track intent with traceId, and send traceId with
2516
+ the actual request. Backend logs the same traceId and propagates session to Ductape operations.
2517
+ Track completion with the same traceId.
2518
+
2519
+ traceId = correlation
2520
+ session = actor attribution
2521
+ idempotencyKey = duplicate prevention
2522
+ matchId/orderId = domain identity
2523
+ These values are not interchangeable.
2524
+
2525
+ OWNERSHIP BOUNDARIES
2526
+ Analytics is never an authoritative order, authorization proof, or gameplay source of truth.
2527
+ Analytics failure must not block or alter gameplay. The server verifies sessions independently;
2528
+ authoritative database and Event streams remain the source of truth. Prefer non-blocking
2529
+ analytics except explicit best-effort flushes at safe lifecycle transitions.
2530
+
2531
+ FRONTEND PROJECT AUDIT
2532
+ Inspect installed @ductape/client/react/vue versions and actual exports; client/provider setup;
2533
+ identify after login and refresh; clearSession on logout/account switch; SPA pageviews; auto-
2534
+ capture and masking; taxonomy consistency; sensitive custom properties; and shared trace IDs.
2535
+ Report capability as Present/Missing/Partial/Not applicable with concrete findings.
2536
+
2537
+ WHEN “SESSION ACTIVITY IS MISSING FROM THE DASHBOARD”
2538
+ Investigate both tracks before blaming the SDK.
2539
+ Backend: start/verify format, correct env, session on database/Event/Feature/notification/etc.,
2540
+ and intentional background sessionlessness.
2541
+ Frontend: identify after login/refresh, clearSession on logout, pageviews/custom events, flush,
2542
+ correct publishable key/product/env, installed API compatibility, browser failures, and privacy
2543
+ controls that may suppress events.
2544
+
2545
+ PAYLOAD RECIPES
2546
+ Anonymous:
2547
+ await ductape.analytics.pageview({ path: window.location.pathname, title: document.title });
2548
+ Authenticated:
2549
+ ductape.analytics.identify(playerSessionToken);
2550
+ await ductape.analytics.track({
2551
+ event: 'match_created', session: playerSessionToken, traceId, properties: { matchId },
2552
+ });
2553
+ Logout:
2554
+ await ductape.analytics.flush();
2555
+ ductape.analytics.clearSession();
2297
2556
  `.trim(),
2298
2557
 
2299
2558
  features: `
@@ -2358,9 +2617,11 @@ STEP 4 — PRESENT the plan and get approval BEFORE writing any code or creating
2358
2617
  Wait for confirmation before writing code or calling any create tool.
2359
2618
 
2360
2619
  STEP 5 — CREATE missing components (only with user approval)
2361
- Use ductape_execute for any missing databases, apps, database actions, notification events, etc.
2362
- For a missing database action:
2363
- ductape_execute("databases.action.create", [product_tag, db_tag, { tag, name, type, query }])
2620
+ Administrative assets must never be created through ductape_execute. Use ductape_cli for products,
2621
+ apps, supported resources, broker topics, cloud connections, secrets, and declarative apply flows.
2622
+ App actions, auths, Features, quotas, fallbacks, health checks, and other assets for which the CLI
2623
+ has no command must be configured in Workbench. Do not generate an impossible publishable-key
2624
+ create/update call. For a missing database action, configure it in Workbench, then verify it exists.
2364
2625
  For a missing child feature, recursively apply this same workflow.
2365
2626
  Tell the user what you are about to create before each tool call.
2366
2627
 
@@ -2484,7 +2745,11 @@ When you call features.define({ handler }), the handler runs TWICE:
2484
2745
 
2485
2746
  Invoke internal application business logic (your own NestJS/backend service code):
2486
2747
  → produce a broker event (ctx.messaging.produce or ductape.events.produce)
2487
- consume it in your NestJS service with events.consume in onModuleInit
2748
+ follow ductape_docs({ topic: "events" }) and use the canonical NestJS decorator:
2749
+ @Events.Consumer({ event: "broker-tag:topic-tag" })
2750
+ async handle(message: MessageShape) { /* injected-service business logic; throw to nack */ }
2751
+ → DuctapeModule auto-registers the decorated consumer; no manual onModuleInit is needed
2752
+ → events.consume() remains a supported lower-level alternative for plain TypeScript/Node.js
2488
2753
  → your service method runs with full access to DI, DB transactions, etc.
2489
2754
  Do NOT create an App Action just to call your own service over HTTP.
2490
2755
 
@@ -2983,9 +3248,15 @@ SESSIONS SERVICE
2983
3248
  const { token } = await ductape.sessions.refresh({ refreshToken: '...' });
2984
3249
 
2985
3250
  ANALYTICS SERVICE
2986
- ductape.analytics.pageview({ page: '/dashboard' });
2987
- ductape.analytics.track('button_click', { button: 'sign-up' });
2988
- ductape.analytics.identify({ userId: 'u_123', traits: { plan: 'pro' } });
3251
+ ductape.analytics.identify('player-session:eyJ...'); // full Ductape session token
3252
+ await ductape.analytics.pageview({ path: '/dashboard', title: document.title });
3253
+ await ductape.analytics.track({
3254
+ event: 'button_clicked',
3255
+ properties: { button: 'sign-up' },
3256
+ });
3257
+ await ductape.analytics.flush();
3258
+ ductape.analytics.clearSession(); // logout/account switch
3259
+ See ductape_docs({ topic: "frontend-analytics" }) before enabling auto-capture.
2989
3260
 
2990
3261
  FRAMEWORK-SPECIFIC PACKAGES
2991
3262
  For React and Vue projects, use the dedicated packages instead of managing the client manually:
@@ -3087,13 +3358,15 @@ AGENT HOOKS
3087
3358
  useAgentSignal(hookOptions?) → { mutate, isLoading, error }
3088
3359
 
3089
3360
  BROKER HOOKS
3090
- useBroker(broker, options?)
3361
+ useBroker()
3091
3362
  → { isConnected, isConnecting, error, connect, disconnect }
3363
+ connect({ broker, session?, product?, env? }) forwards the complete options object to
3364
+ @ductape/client. Pass session for player-scoped authorization.
3092
3365
  useBrokerPublish(hookOptions?)
3093
3366
  → { mutate({ topic, message, headers?, key? }), isLoading, error, data }
3094
3367
  useBrokerSubscription(subscribeOptions, hookOptions?)
3095
3368
  → { data: BrokerMessage[], isSubscribed, error, unsubscribe, resubscribe }
3096
- subscribeOptions: { topic, group? }
3369
+ subscribeOptions: { broker?, topic, group?, session?, product?, env? }
3097
3370
 
3098
3371
  GRAPH HOOKS
3099
3372
  useGraph(graphTag) → { isConnected, isConnecting, error, connect, disconnect }
@@ -3163,7 +3436,10 @@ ACTIONS HOOKS
3163
3436
  useActionRun(hookOptions?) → { mutate, isLoading, error, data }
3164
3437
 
3165
3438
  ANALYTICS HOOK
3166
- useAnalytics() → { pageview, track, identify }
3439
+ useAnalytics() → { pageview, track, identify, visitorId, enableAutoCapture, flush }
3440
+ The currently installed hook does NOT return clearSession or disableAutoCapture.
3441
+ Use useDuctape().client.analytics.clearSession()/disableAutoCapture(), or verify a newer installed
3442
+ package export before generating those hook calls. See ductape_docs({ topic: "frontend-analytics" }).
3167
3443
 
3168
3444
  hookOptions pattern (applies to all hooks):
3169
3445
  enabled? boolean — false skips auto-fetch/subscribe
@@ -3333,7 +3609,10 @@ ACTIONS COMPOSABLES
3333
3609
  useActionRun(composableOptions?) → { mutate, isLoading, error, data }
3334
3610
 
3335
3611
  ANALYTICS COMPOSABLE
3336
- useAnalytics() → { pageview, track, identify }
3612
+ useAnalytics() → { pageview, track, identify, visitorId, enableAutoCapture, flush }
3613
+ The currently installed composable does NOT return clearSession or disableAutoCapture.
3614
+ Use useDuctape().client.analytics.clearSession()/disableAutoCapture(), or verify a newer installed
3615
+ package export before generating those composable calls. See ductape_docs({ topic: "frontend-analytics" }).
3337
3616
 
3338
3617
  composableOptions pattern (applies to all composables):
3339
3618
  enabled? boolean — false skips auto-fetch/subscribe
@@ -3542,6 +3821,25 @@ async function main() {
3542
3821
 
3543
3822
  const executeHandler = async (args: { publishable_key?: string; module: SDKModule; method: string; params: unknown[] }) => {
3544
3823
  try {
3824
+ const runtimeMutationMethods: Partial<Record<SDKModule, Set<string>>> = {
3825
+ databases: new Set(['insert', 'update', 'delete', 'upsert']),
3826
+ graph: new Set(['insert', 'update', 'delete']),
3827
+ vector: new Set(['insert', 'upsert', 'upsertOne', 'delete']),
3828
+ sessions: new Set(['revoke']),
3829
+ };
3830
+ const isRuntimeDataMutation = runtimeMutationMethods[args.module]?.has(args.method) === true;
3831
+ const isAdministrativeMutation =
3832
+ (/^migration\./.test(args.method) ||
3833
+ /^schema\.(create|drop|add|remove|update)/.test(args.method) ||
3834
+ /(^|\.)(create|update|delete|configure|add|remove|revoke)$/.test(args.method)) &&
3835
+ !isRuntimeDataMutation;
3836
+ if (isAdministrativeMutation) {
3837
+ throw new Error(
3838
+ `Administrative operation "${args.module}.${args.method}" is blocked in ductape_execute. ` +
3839
+ 'Use ductape_cli when that resource is supported by the CLI; otherwise configure it in Workbench. ' +
3840
+ 'The runtime proxy uses a publishable key and cannot administer platform assets.',
3841
+ );
3842
+ }
3545
3843
  const key = args.publishable_key || process.env.DUCTAPE_PUBLISHABLE_KEY;
3546
3844
  if (!key) {
3547
3845
  throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
@@ -3592,7 +3890,10 @@ async function main() {
3592
3890
  if (!key) {
3593
3891
  throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
3594
3892
  }
3595
- const result = await generateExecutablePayload({ ...args, publishable_key: key });
3893
+ const result = addSessionAwarenessMetadata(
3894
+ await generateExecutablePayload({ ...args, publishable_key: key }),
3895
+ args,
3896
+ );
3596
3897
 
3597
3898
  let text = JSON.stringify(result ?? null, null, 2);
3598
3899
 
@@ -3624,7 +3925,10 @@ async function main() {
3624
3925
  throw new Error('Not authenticated. Set DUCTAPE_PUBLISHABLE_KEY in your MCP server env config, or pass publishable_key on every tool call.');
3625
3926
  }
3626
3927
  ensureSupportedSnippetOperation(args.operation_family, args.method);
3627
- const generated = await generateExecutablePayload({ ...args, publishable_key: key });
3928
+ const generated = addSessionAwarenessMetadata(
3929
+ await generateExecutablePayload({ ...args, publishable_key: key }),
3930
+ args,
3931
+ );
3628
3932
  const payload = (generated as any)?.payload ?? {};
3629
3933
  const snippet = buildSnippet(args.language, payload, args.operation_family, args.method);
3630
3934
  return {
@@ -3732,7 +4036,7 @@ async function main() {
3732
4036
  'index strategy, operation types) that should be confirmed with the user first.\n\n' +
3733
4037
  'Available topics: transactions, presave, triggers, aggregations, migrations, indexes, performance, actions, ' +
3734
4038
  'graphs, storage, cloud, vector, warehouse, secrets, apps, products, sessions, caches, ' +
3735
- 'notifications, resilience, features, events, logs, frontend, client, react, vue',
4039
+ 'notifications, resilience, features, events, logs, frontend, frontend-analytics, client, react, vue',
3736
4040
  inputSchema: docsInputSchema,
3737
4041
  },
3738
4042
  docsHandler,
@@ -3808,7 +4112,7 @@ async function main() {
3808
4112
  ' Step 2 — check if the database component already exists:\n' +
3809
4113
  ' ductape_cli("resources databases list <product_tag> --json")\n' +
3810
4114
  ' If a component already uses the same Atlas cluster, do NOT re-import — instead update it:\n' +
3811
- ' ductape_execute("databases.updateDatabase", [product_tag, db_tag, { envs: [...updated envs...] }])\n' +
4115
+ ' use ductape_cli resource update when supported; otherwise update it in Workbench\n' +
3812
4116
  ' Add or change the dbName in the env\'s connection_url to switch databases on the same cluster.\n' +
3813
4117
  ' Step 3 — import (only if no existing component uses this cluster):\n' +
3814
4118
  ' Use import-persist-all with one entry per product env. Required fields per entry:\n' +