@ductape/mcp 0.1.14 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +190 -56
  2. package/package.json +1 -1
  3. package/src/index.ts +190 -56
package/dist/index.js CHANGED
@@ -155,7 +155,9 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
155
155
  webhooks.events.list [app_tag, webhook_tag]
156
156
 
157
157
  ━━━ MODULE: sessions ━━━
158
- sessions.create [product_tag, data: { tag: string, name: string, description?: string, expiry: number, period: "seconds"|"minutes"|"hours"|"days", selector: string, schema: { fieldName: "string"|"number"|"boolean" } }]
158
+ sessions.create [product_tag, data: { tag: string, name: string, description?: string, expiry: number, period: "seconds"|"minutes"|"hours"|"days", selector: string, schema: { fieldName: { type: "string"|"number"|"boolean"|"object"|"array", required?: boolean, description?: string } } }]
159
+ ← selector is the dot-path of the primary identifier field (e.g. "playerId"); it MUST exist in schema.
160
+ ← schema declares the shape of data encrypted in the JWT; actual values are passed at runtime via sessions.start.
159
161
  sessions.update [product_tag, session_tag, data: { name?: string, description?: string, expiry?: number, period?: "seconds"|"minutes"|"hours"|"days", selector?: string, schema?: object }]
160
162
  sessions.fetch [product_tag, session_tag]
161
163
  sessions.list [product_tag]
@@ -291,8 +293,15 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
291
293
  messageBrokers.fetch [product_tag, broker_tag]
292
294
  messageBrokers.list [product_tag]
293
295
  messageBrokers.delete [product_tag, broker_tag]
294
- messageBrokers.topics.create [product_tag, data: { tag: string, name: string, broker: string, type: "producer"|"consumer"|"both" }]
295
- messageBrokers.topics.update [product_tag, topic_tag, data: { name?: string, type?: "producer"|"consumer"|"both" }]
296
+ messageBrokers.topics.create [product_tag, data: { tag: string, name: string, broker: string,
297
+ description?: string, sample?: object, idempotent?: boolean,
298
+ queueUrls?: [{ env_slug: string, url: string }] // SQS only: per-env queue URL per topic
299
+ }]
300
+ ← A broker can have unlimited topics. Add one per logical event type.
301
+ ← For SQS: each topic needs queueUrls mapping env slug → the SQS queue URL for that topic+env.
302
+ ← For Pub/Sub and Service Bus: topic name resolves from the tag or broker env config; no queueUrls needed.
303
+ messageBrokers.topics.update [product_tag, topic_tag, data: { name?: string, description?: string,
304
+ sample?: object, idempotent?: boolean, queueUrls?: [{ env_slug: string, url: string }] }]
296
305
  messageBrokers.topics.fetch [product_tag, topic_tag]
297
306
  messageBrokers.topics.list [product_tag, broker_tag]
298
307
  messageBrokers.produce [{ product, env, event: "broker_tag:topic_tag", message: { key: value }, session?, cache? }] ← CALL ductape_generate_payload FIRST (operation_family="messaging", method="produce", targets={broker, event})
@@ -584,7 +593,7 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
584
593
  caches.list [product_tag]
585
594
  caches.delete [product_tag, cache_tag]
586
595
  caches.get [{ key: string }]
587
- caches.set [{ product, cache, key, value: string, componentTag?, componentType?, expiry?: Date }]
596
+ caches.set [{ product, cache, key, value: string, componentTag?, componentType?, expiry?: string (ISO 8601 absolute timestamp — NOT a duration; e.g. new Date(Date.now()+3600000).toISOString() for 1h TTL), env }]
588
597
  caches.clear [{ key: string }]
589
598
  caches.clearAll [{ product, cache, env? }]
590
599
  caches.fetchValues [{ product, cache, env?, page?, limit?, expiryFilter?: "all"|"expiring"|"permanent"|"expired" }]
@@ -1618,24 +1627,48 @@ Bootstrap (single API call returning product context + component config + privat
1618
1627
  sessions: `
1619
1628
  DUCTAPE SESSIONS
1620
1629
 
1621
- A session is a named JWT schema on a product. It defines: a tag, an expiry duration, a selector
1622
- (which field in the data object is the user identifier), and a schema (the shape of the JWT payload).
1623
-
1624
- Define a session (admin ductape_execute):
1630
+ A session is a named JWT schema on a product. It defines:
1631
+ - tag / name — unique identifier and display name
1632
+ - expiry + period — how long each issued JWT is valid (duration, not an absolute date)
1633
+ - selector — the dot-path of the field inside the data object that is the PRIMARY
1634
+ user identifier (e.g. "playerId"). This field becomes the lookup key
1635
+ for revoke, list, and analytics. It MUST exist in the schema.
1636
+ - schema — the complete shape of data that will be embedded in the JWT payload.
1637
+ Each field is declared as { type, required?, description? }.
1638
+ This is a TYPE DECLARATION, not sample data. The actual values
1639
+ are passed at runtime when calling sessions.start.
1640
+
1641
+ IMPORTANT — schema field format: { fieldName: { type, required?, description? } }
1642
+ type values: "string" | "number" | "boolean" | "object" | "array"
1643
+ Do NOT use the shorthand { fieldName: "string" } — pass the full object form.
1644
+
1645
+ Example (game product — player identity in JWT):
1625
1646
  ductape_execute("sessions.create", [product_tag, {
1626
- tag: "user-session",
1627
- name: "User Session",
1647
+ tag: "player-session",
1648
+ name: "Player Session",
1628
1649
  expiry: 24,
1629
- period: "hours", // "seconds" | "minutes" | "hours" | "days"
1630
- selector: "userId", // field in data that identifies the user
1631
- schema: { userId: "string", role: "string", email: "string" },
1650
+ period: "hours",
1651
+ selector: "playerId", // primary identifier must be in schema below
1652
+ schema: {
1653
+ playerId: { type: "string", required: true, description: "Unique player ID" },
1654
+ username: { type: "string", required: true, description: "Player display name" },
1655
+ role: { type: "string", required: false, description: "admin | player | guest" },
1656
+ accountId: { type: "string", required: false, description: "Linked account ID" },
1657
+ },
1632
1658
  }])
1633
1659
 
1634
1660
  Runtime — create a session (sign a JWT):
1635
1661
  → CALL ductape_generate_payload FIRST (operation_family="session", method="start", targets={tag})
1636
- ductape_execute("sessions.start", [{ product, env, tag: "user-session",
1637
- data: { userId: "u_123", role: "admin", email: "user@example.com" } }])
1638
- → returns token: "user-session:eyJ..." ← format is always "session_tag:jwt"
1662
+ to discover the exact data field names accepted for this session tag.
1663
+ ductape_execute("sessions.start", [{ product, env, tag: "player-session",
1664
+ data: {
1665
+ playerId: "player_abc123", // must match selector — used as the revocation/lookup key
1666
+ username: "ShadowBlade",
1667
+ role: "player",
1668
+ accountId: "acct_xyz",
1669
+ } }])
1670
+ → returns token: "player-session:eyJ..." ← format is always "session_tag:jwt"
1671
+ The token embeds all schema fields in the JWT payload, signed with the product private key.
1639
1672
 
1640
1673
  Verify a token:
1641
1674
  ductape_execute("sessions.verify", [{ product, env, tag: "user-session", token: "user-session:eyJ..." }])
@@ -1672,11 +1705,16 @@ Registration (admin — ductape_cli):
1672
1705
  envs: [{ slug, connection_url }] }
1673
1706
 
1674
1707
  Operations:
1675
- caches.set [{ product, cache, key, value: string, expiry?: Date, env }]
1708
+ caches.set [{ product, cache, key, value: string, expiry?: string (ISO 8601), env }]
1709
+ expiry is an ABSOLUTE TIMESTAMP (not a duration).
1710
+ To expire in 1 hour: expiry = new Date(Date.now() + 3600_000).toISOString()
1711
+ To expire in 24 hours: expiry = new Date(Date.now() + 86400_000).toISOString()
1712
+ To never expire: omit expiry entirely.
1713
+ Via MCP: pass an ISO 8601 string e.g. "2026-07-17T12:00:00.000Z"
1676
1714
  → Writes to Redis synchronously, then fires remote API write in background (non-blocking).
1677
1715
  caches.get [{ key: string }]
1678
1716
  → Checks Redis first; on miss falls through to remote API; on hit from API, populates Redis.
1679
- → Enforces TTL by comparing stored expiry field against current time.
1717
+ → Enforces TTL by comparing the stored expiry timestamp against current time (client-side check).
1680
1718
  caches.clear [{ key: string }]
1681
1719
  → Deletes from Redis and from remote API.
1682
1720
  caches.clearAll [{ product, cache, env? }]
@@ -1691,13 +1729,22 @@ Tier architecture (three tiers applied automatically):
1691
1729
  Tier 2: Redis hash (hSet/hGetAll) with optional EXPIRE
1692
1730
  Tier 3: Remote Ductape API
1693
1731
 
1732
+ Practical examples:
1733
+ // Cache a player leaderboard for 5 minutes:
1734
+ { product, cache: "leaderboard-cache", key: "top-100", value: JSON.stringify(rows),
1735
+ expiry: new Date(Date.now() + 300_000).toISOString(), env: "prd" }
1736
+
1737
+ // Cache a session token lookup for 1 hour:
1738
+ { product, cache: "session-cache", key: "player:u_123", value: token,
1739
+ expiry: new Date(Date.now() + 3_600_000).toISOString(), env: "prd" }
1740
+
1694
1741
  Important:
1695
1742
  - Redis is optional; without it all reads/writes go through the remote API.
1696
- - expiry is a stored Date field, not a Redis TTL — expiry check happens client-side.
1697
- - clearAll only clears via remote API; Redis retains stale entries until accessed and found expired.
1743
+ - expiry is stored as a Date field, not a Redis TTL — the expiry check happens client-side on read.
1744
+ - clearAll only clears via the remote API bulk endpoint; Redis may retain stale entries until evicted.
1698
1745
  - Cache entries are stored as Redis hashes (not plain strings).
1699
- - Other services (storage, graph, notifications, sessions) also use the CacheManager for
1700
- their own result caching — configure a shared Redis URL at SDK init to share the pool.
1746
+ - Other services (storage, graph, notifications, sessions) also use CacheManager internally —
1747
+ configure a shared Redis URL at SDK init to share the pool.
1701
1748
  `.trim(),
1702
1749
  notifications: `
1703
1750
  DUCTAPE NOTIFICATIONS
@@ -1885,55 +1932,142 @@ Code-first (define API — compiles async handler to JSON step schema):
1885
1932
  events: `
1886
1933
  DUCTAPE EVENTS (MESSAGE BROKERS)
1887
1934
 
1888
- Supported broker types: kafka | rabbitmq | redis | aws_sqs | azure_servicebus | google_pubsub | nats
1935
+ ARCHITECTURE always two separate steps:
1936
+ Step 1: Register the BROKER COMPONENT (establishes the connection to the broker service).
1937
+ The broker's envs[] holds connection credentials and host/project info, NOT topics.
1938
+ Step 2: Create TOPIC DEFINITIONS on that broker (unlimited; each is a named subject or queue).
1939
+ Topics are separate from the broker registration and added after.
1940
+ A single broker component can have as many topics as needed.
1941
+
1942
+ ━━━ SUPPORTED BROKER TYPES ━━━
1943
+
1944
+ Cloud-managed (provision OR import via cloud connection):
1945
+ GCP Pub/Sub → service: "pubsub", type in envs: "google_pubsub"
1946
+ AWS SQS → service: "sqs", type in envs: "aws_sqs"
1947
+ Azure SvcBus → service: "servicebus", type in envs: "azure_servicebus"
1948
+
1949
+ Self-hosted (import-only — supply connection URL manually):
1950
+ Kafka → type in envs: "kafka"
1951
+ RabbitMQ → type in envs: "rabbitmq"
1952
+ Redis → type in envs: "redis"
1953
+ NATS → type in envs: "nats"
1954
+
1955
+ ━━━ STEP 1A: REGISTER VIA CLOUD CONNECTION (cloud-managed brokers) ━━━
1956
+
1957
+ Provision (create a NEW resource in the cloud):
1958
+ ductape_cli("cloud resources provision-persist-all -f brokers.json --json")
1959
+ File: JSON ARRAY — one entry per env, same product + component tag across all entries.
1960
+ type field: "messageBrokers" (exact — not "messagebrokers" or "events")
1961
+
1962
+ GCP Pub/Sub — creates a new Pub/Sub topic in GCP, stores credentials in secrets:
1963
+ [{"cloud":"gcp-snd","service":"pubsub","type":"messageBrokers",
1964
+ "product":"my-product","component":"notifications-broker","env":"snd",
1965
+ "topicName":"my-product-notifications-snd"},
1966
+ {"cloud":"gcp-prd","service":"pubsub","type":"messageBrokers",
1967
+ "product":"my-product","component":"notifications-broker","env":"prd",
1968
+ "topicName":"my-product-notifications-prd"}]
1969
+ If topicName is omitted a timestamped name is generated — always supply it explicitly.
1970
+
1971
+ AWS SQS — creates a new SQS queue per env:
1972
+ [{"cloud":"aws-snd","service":"sqs","type":"messageBrokers",
1973
+ "product":"my-product","component":"notifications-broker","env":"snd",
1974
+ "queueName":"my-product-notifications-snd"},
1975
+ {"cloud":"aws-prd","service":"sqs","type":"messageBrokers",
1976
+ "product":"my-product","component":"notifications-broker","env":"prd",
1977
+ "queueName":"my-product-notifications-prd"}]
1978
+
1979
+ Azure Service Bus — creates a namespace + queue per env:
1980
+ [{"cloud":"azure-snd","service":"servicebus","type":"messageBrokers",
1981
+ "product":"my-product","component":"notifications-broker","env":"snd",
1982
+ "namespaceName":"myproduct-snd","queueName":"notifications"},
1983
+ {"cloud":"azure-prd","service":"servicebus","type":"messageBrokers",
1984
+ "product":"my-product","component":"notifications-broker","env":"prd",
1985
+ "namespaceName":"myproduct-prd","queueName":"notifications"}]
1986
+
1987
+ Import (register an EXISTING cloud resource):
1988
+ Same as above but use import-persist-all and supply "resource" (the existing resource name/ID):
1989
+ ductape_cli("cloud resources import-persist-all -f brokers.json --json")
1990
+ Each entry: { cloud, service, type: "messageBrokers", product, component, env, resource: "<id>" }
1889
1991
 
1890
- CLI aliases for the messageBrokers module: events, event, broker, brokers, message-brokers
1891
- ductape_cli("resources events list <product_tag> --json")
1992
+ IMPORTANT: Never share one cloud resource (topic/queue) across snd and prd envs — use
1993
+ separate resources per env to avoid mixing sandbox and production events.
1892
1994
 
1893
- Registration (admin ductape_cli):
1894
- Message brokers are IMPORT-ONLY (no provision-persist). Use import-persist-all:
1895
- ductape_cli("cloud resources import-persist-all -f brokers.json --json")
1896
- File is a JSON ARRAY — one entry per env.
1897
- service identifiers: pubsub (GCP Pub/Sub) | sqs (AWS SQS) | servicebus (Azure Service Bus)
1898
- type field: "messageBrokers" (not "messagebrokers" or "events")
1995
+ ━━━ STEP 1B: REGISTER SELF-HOSTED BROKER (no cloud connection needed) ━━━
1996
+
1997
+ ductape_cli("resources events create -f broker.json")
1998
+ File: {
1999
+ name: string, tag: string, description?: string,
2000
+ envs: [
2001
+ {
2002
+ slug: "snd",
2003
+ type: "kafka"|"rabbitmq"|"redis"|"nats",
2004
+ config: <see config shapes below>
2005
+ },
2006
+ { slug: "prd", type: "kafka", config: { ... } }
2007
+ ]
2008
+ }
2009
+
2010
+ Config shapes per type:
2011
+ kafka: { brokers: ["host:9092"], clientId: "my-app", groupId?: "...",
2012
+ ssl?: true, sasl?: { mechanism: "plain", username, password } }
2013
+ rabbitmq: { url: "amqp://user:pass@host:5672/vhost" }
2014
+ redis: { host: "...", port: 6379, password?: "..." }
2015
+ nats: { servers: ["nats://host:4222"], token?: "...", user?: "...", pass?: "...", tls?: true }
2016
+
2017
+ ━━━ STEP 2: ADD TOPIC DEFINITIONS (all broker types — add as many as needed) ━━━
1899
2018
 
1900
- After importing, create topics:
1901
2019
  ductape_execute("messageBrokers.topics.create", [product_tag, {
1902
- tag: "user-events", name: "User Events", broker: "broker-tag",
1903
- type: "producer"|"consumer"|"both",
2020
+ tag: "player-joined",
2021
+ name: "Player Joined",
2022
+ broker: "notifications-broker", // broker component tag
2023
+ description?: string,
2024
+ sample: { playerId: "string", username: "string" }, // example message shape
2025
+ idempotent?: boolean,
2026
+ // SQS only — map per-env queue URLs (each topic can be a different queue):
2027
+ queueUrls?: [
2028
+ { env_slug: "snd", url: "https://sqs.us-east-1.amazonaws.com/123456/my-product-player-joined-snd" },
2029
+ { env_slug: "prd", url: "https://sqs.us-east-1.amazonaws.com/123456/my-product-player-joined-prd" }
2030
+ ]
1904
2031
  }])
1905
2032
 
1906
- Produce a message (runtime):
1907
- CALL ductape_generate_payload FIRST (operation_family="messaging", method="produce")
1908
- messageBrokers.produce [{ product, env, event: "broker_tag:topic_tag", message: { key: value } }]
1909
- Idempotent publish:
1910
- messageBrokers.publishIdempotent [{ product, env, event, message, idempotency_key, ttl? }]
1911
- → checks if key was already processed; returns cached result if so.
2033
+ For GCP Pub/Sub and Azure Service Bus, the SDK resolves the topic name from the topic tag (or
2034
+ config.topicName on the env config). No extra per-topic URL mapping is needed beyond the tag.
2035
+ For SQS, every topic definition needs queueUrls to point to the specific per-env queue.
2036
+ Repeat this call for each logical event type — there is no limit on number of topics.
1912
2037
 
1913
- Consume a message (subscribe):
1914
- messageBrokers.consume [{ product, env, event: "broker_tag:topic_tag",
1915
- callback: async (message) => { ... } }]
1916
- Callback tracking is deferred (setImmediate) so user callback latency is unaffected.
1917
- Callback errors are re-thrown so the broker can nack/retry.
2038
+ List topics on a broker:
2039
+ ductape_execute("messageBrokers.topics.list", [product_tag, "broker-tag"])
1918
2040
 
1919
- Background dispatch with scheduling:
1920
- messageBrokers.dispatch [{ product, env, broker, event, input: { message },
1921
- schedule?: { start_at?, cron?, every?, limit?, endDate?, tz? } }]
1922
- → CALL ductape_generate_payload FIRST (operation_family="messaging", method="dispatch")
2041
+ ━━━ RUNTIME: PRODUCE AND CONSUME ━━━
2042
+
2043
+ Produce (publish a message):
2044
+ → CALL ductape_generate_payload FIRST (operation_family="messaging", method="produce")
2045
+ messageBrokers.produce [{ product, env, event: "broker_tag:topic_tag", message: { key: value } }]
2046
+ Idempotent publish (deduplicates by key, default TTL 86400 s):
2047
+ messageBrokers.publishIdempotent [{ product, env, event, message, idempotency_key, ttl? }]
2048
+
2049
+ Consume (subscribe):
2050
+ messageBrokers.consume [{ product, env, event: "broker_tag:topic_tag",
2051
+ callback: async (message) => { ... } }]
2052
+ Callback errors are re-thrown so the broker can nack/retry.
2053
+
2054
+ Background dispatch with scheduling:
2055
+ messageBrokers.dispatch [{ product, env, broker, event, input: { message },
2056
+ schedule?: { start_at?, cron?, every?, limit?, endDate?, tz? } }]
2057
+ → CALL ductape_generate_payload FIRST (operation_family="messaging", method="dispatch")
2058
+
2059
+ Event format string: "broker_tag:topic_tag" — always colon-separated.
2060
+ Message payload is AES-encrypted before the tracking API call — tracking never sees plaintext.
2061
+
2062
+ ━━━ OBSERVABILITY ━━━
1923
2063
 
1924
- Event tracking and observability:
1925
2064
  messageBrokers.messages.query [{ product, env, brokerTag, topicTag?, status?, page?, limit? }]
1926
2065
  messageBrokers.messages.getStats [{ product, env, brokerTag }]
1927
- → { total_events, success_count, failed_count, dead_letter_count, events_by_topic }
1928
2066
  messageBrokers.messages.getDashboard [{ product, env, brokerTag }]
1929
2067
  messageBrokers.messages.getDeadLetters [{ product, env, brokerTag, topicTag?, limit? }]
1930
2068
  messageBrokers.replayEvent [{ product, env, eventId, force? }]
1931
2069
  messageBrokers.reprocessDLQ [{ product, env, brokerTag, topicTag?, messageIds?, limit? }]
1932
- messageBrokers.checkIdempotency [{ product, env, brokerTag, idempotency_key }] → { exists, event_id? }
1933
-
1934
- Event format string: "broker_tag:topic_tag" — always colon-separated; parsed by the SDK.
1935
- Message payload is AES-encrypted before the tracking API call — tracking endpoint never sees plaintext.
1936
- Connection pool: deduplicates live connections across BrokersService instances by workspace+product+config.
2070
+ messageBrokers.checkIdempotency [{ product, env, brokerTag, idempotency_key }]
1937
2071
  `.trim(),
1938
2072
  logs: `
1939
2073
  DUCTAPE LOGS
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ductape/mcp",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "MCP server that exposes Ductape SDK operations via the backend proxy",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/index.ts CHANGED
@@ -166,7 +166,9 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
166
166
  webhooks.events.list [app_tag, webhook_tag]
167
167
 
168
168
  ━━━ MODULE: sessions ━━━
169
- sessions.create [product_tag, data: { tag: string, name: string, description?: string, expiry: number, period: "seconds"|"minutes"|"hours"|"days", selector: string, schema: { fieldName: "string"|"number"|"boolean" } }]
169
+ sessions.create [product_tag, data: { tag: string, name: string, description?: string, expiry: number, period: "seconds"|"minutes"|"hours"|"days", selector: string, schema: { fieldName: { type: "string"|"number"|"boolean"|"object"|"array", required?: boolean, description?: string } } }]
170
+ ← selector is the dot-path of the primary identifier field (e.g. "playerId"); it MUST exist in schema.
171
+ ← schema declares the shape of data encrypted in the JWT; actual values are passed at runtime via sessions.start.
170
172
  sessions.update [product_tag, session_tag, data: { name?: string, description?: string, expiry?: number, period?: "seconds"|"minutes"|"hours"|"days", selector?: string, schema?: object }]
171
173
  sessions.fetch [product_tag, session_tag]
172
174
  sessions.list [product_tag]
@@ -302,8 +304,15 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
302
304
  messageBrokers.fetch [product_tag, broker_tag]
303
305
  messageBrokers.list [product_tag]
304
306
  messageBrokers.delete [product_tag, broker_tag]
305
- messageBrokers.topics.create [product_tag, data: { tag: string, name: string, broker: string, type: "producer"|"consumer"|"both" }]
306
- messageBrokers.topics.update [product_tag, topic_tag, data: { name?: string, type?: "producer"|"consumer"|"both" }]
307
+ messageBrokers.topics.create [product_tag, data: { tag: string, name: string, broker: string,
308
+ description?: string, sample?: object, idempotent?: boolean,
309
+ queueUrls?: [{ env_slug: string, url: string }] // SQS only: per-env queue URL per topic
310
+ }]
311
+ ← A broker can have unlimited topics. Add one per logical event type.
312
+ ← For SQS: each topic needs queueUrls mapping env slug → the SQS queue URL for that topic+env.
313
+ ← For Pub/Sub and Service Bus: topic name resolves from the tag or broker env config; no queueUrls needed.
314
+ messageBrokers.topics.update [product_tag, topic_tag, data: { name?: string, description?: string,
315
+ sample?: object, idempotent?: boolean, queueUrls?: [{ env_slug: string, url: string }] }]
307
316
  messageBrokers.topics.fetch [product_tag, topic_tag]
308
317
  messageBrokers.topics.list [product_tag, broker_tag]
309
318
  messageBrokers.produce [{ product, env, event: "broker_tag:topic_tag", message: { key: value }, session?, cache? }] ← CALL ductape_generate_payload FIRST (operation_family="messaging", method="produce", targets={broker, event})
@@ -595,7 +604,7 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
595
604
  caches.list [product_tag]
596
605
  caches.delete [product_tag, cache_tag]
597
606
  caches.get [{ key: string }]
598
- caches.set [{ product, cache, key, value: string, componentTag?, componentType?, expiry?: Date }]
607
+ caches.set [{ product, cache, key, value: string, componentTag?, componentType?, expiry?: string (ISO 8601 absolute timestamp — NOT a duration; e.g. new Date(Date.now()+3600000).toISOString() for 1h TTL), env }]
599
608
  caches.clear [{ key: string }]
600
609
  caches.clearAll [{ product, cache, env? }]
601
610
  caches.fetchValues [{ product, cache, env?, page?, limit?, expiryFilter?: "all"|"expiring"|"permanent"|"expired" }]
@@ -1682,24 +1691,48 @@ Bootstrap (single API call returning product context + component config + privat
1682
1691
  sessions: `
1683
1692
  DUCTAPE SESSIONS
1684
1693
 
1685
- A session is a named JWT schema on a product. It defines: a tag, an expiry duration, a selector
1686
- (which field in the data object is the user identifier), and a schema (the shape of the JWT payload).
1687
-
1688
- Define a session (admin ductape_execute):
1694
+ A session is a named JWT schema on a product. It defines:
1695
+ - tag / name — unique identifier and display name
1696
+ - expiry + period — how long each issued JWT is valid (duration, not an absolute date)
1697
+ - selector — the dot-path of the field inside the data object that is the PRIMARY
1698
+ user identifier (e.g. "playerId"). This field becomes the lookup key
1699
+ for revoke, list, and analytics. It MUST exist in the schema.
1700
+ - schema — the complete shape of data that will be embedded in the JWT payload.
1701
+ Each field is declared as { type, required?, description? }.
1702
+ This is a TYPE DECLARATION, not sample data. The actual values
1703
+ are passed at runtime when calling sessions.start.
1704
+
1705
+ IMPORTANT — schema field format: { fieldName: { type, required?, description? } }
1706
+ type values: "string" | "number" | "boolean" | "object" | "array"
1707
+ Do NOT use the shorthand { fieldName: "string" } — pass the full object form.
1708
+
1709
+ Example (game product — player identity in JWT):
1689
1710
  ductape_execute("sessions.create", [product_tag, {
1690
- tag: "user-session",
1691
- name: "User Session",
1711
+ tag: "player-session",
1712
+ name: "Player Session",
1692
1713
  expiry: 24,
1693
- period: "hours", // "seconds" | "minutes" | "hours" | "days"
1694
- selector: "userId", // field in data that identifies the user
1695
- schema: { userId: "string", role: "string", email: "string" },
1714
+ period: "hours",
1715
+ selector: "playerId", // primary identifier must be in schema below
1716
+ schema: {
1717
+ playerId: { type: "string", required: true, description: "Unique player ID" },
1718
+ username: { type: "string", required: true, description: "Player display name" },
1719
+ role: { type: "string", required: false, description: "admin | player | guest" },
1720
+ accountId: { type: "string", required: false, description: "Linked account ID" },
1721
+ },
1696
1722
  }])
1697
1723
 
1698
1724
  Runtime — create a session (sign a JWT):
1699
1725
  → CALL ductape_generate_payload FIRST (operation_family="session", method="start", targets={tag})
1700
- ductape_execute("sessions.start", [{ product, env, tag: "user-session",
1701
- data: { userId: "u_123", role: "admin", email: "user@example.com" } }])
1702
- → returns token: "user-session:eyJ..." ← format is always "session_tag:jwt"
1726
+ to discover the exact data field names accepted for this session tag.
1727
+ ductape_execute("sessions.start", [{ product, env, tag: "player-session",
1728
+ data: {
1729
+ playerId: "player_abc123", // must match selector — used as the revocation/lookup key
1730
+ username: "ShadowBlade",
1731
+ role: "player",
1732
+ accountId: "acct_xyz",
1733
+ } }])
1734
+ → returns token: "player-session:eyJ..." ← format is always "session_tag:jwt"
1735
+ The token embeds all schema fields in the JWT payload, signed with the product private key.
1703
1736
 
1704
1737
  Verify a token:
1705
1738
  ductape_execute("sessions.verify", [{ product, env, tag: "user-session", token: "user-session:eyJ..." }])
@@ -1737,11 +1770,16 @@ Registration (admin — ductape_cli):
1737
1770
  envs: [{ slug, connection_url }] }
1738
1771
 
1739
1772
  Operations:
1740
- caches.set [{ product, cache, key, value: string, expiry?: Date, env }]
1773
+ caches.set [{ product, cache, key, value: string, expiry?: string (ISO 8601), env }]
1774
+ expiry is an ABSOLUTE TIMESTAMP (not a duration).
1775
+ To expire in 1 hour: expiry = new Date(Date.now() + 3600_000).toISOString()
1776
+ To expire in 24 hours: expiry = new Date(Date.now() + 86400_000).toISOString()
1777
+ To never expire: omit expiry entirely.
1778
+ Via MCP: pass an ISO 8601 string e.g. "2026-07-17T12:00:00.000Z"
1741
1779
  → Writes to Redis synchronously, then fires remote API write in background (non-blocking).
1742
1780
  caches.get [{ key: string }]
1743
1781
  → Checks Redis first; on miss falls through to remote API; on hit from API, populates Redis.
1744
- → Enforces TTL by comparing stored expiry field against current time.
1782
+ → Enforces TTL by comparing the stored expiry timestamp against current time (client-side check).
1745
1783
  caches.clear [{ key: string }]
1746
1784
  → Deletes from Redis and from remote API.
1747
1785
  caches.clearAll [{ product, cache, env? }]
@@ -1756,13 +1794,22 @@ Tier architecture (three tiers applied automatically):
1756
1794
  Tier 2: Redis hash (hSet/hGetAll) with optional EXPIRE
1757
1795
  Tier 3: Remote Ductape API
1758
1796
 
1797
+ Practical examples:
1798
+ // Cache a player leaderboard for 5 minutes:
1799
+ { product, cache: "leaderboard-cache", key: "top-100", value: JSON.stringify(rows),
1800
+ expiry: new Date(Date.now() + 300_000).toISOString(), env: "prd" }
1801
+
1802
+ // Cache a session token lookup for 1 hour:
1803
+ { product, cache: "session-cache", key: "player:u_123", value: token,
1804
+ expiry: new Date(Date.now() + 3_600_000).toISOString(), env: "prd" }
1805
+
1759
1806
  Important:
1760
1807
  - Redis is optional; without it all reads/writes go through the remote API.
1761
- - expiry is a stored Date field, not a Redis TTL — expiry check happens client-side.
1762
- - clearAll only clears via remote API; Redis retains stale entries until accessed and found expired.
1808
+ - expiry is stored as a Date field, not a Redis TTL — the expiry check happens client-side on read.
1809
+ - clearAll only clears via the remote API bulk endpoint; Redis may retain stale entries until evicted.
1763
1810
  - Cache entries are stored as Redis hashes (not plain strings).
1764
- - Other services (storage, graph, notifications, sessions) also use the CacheManager for
1765
- their own result caching — configure a shared Redis URL at SDK init to share the pool.
1811
+ - Other services (storage, graph, notifications, sessions) also use CacheManager internally —
1812
+ configure a shared Redis URL at SDK init to share the pool.
1766
1813
  `.trim(),
1767
1814
 
1768
1815
  notifications: `
@@ -1954,55 +2001,142 @@ Code-first (define API — compiles async handler to JSON step schema):
1954
2001
  events: `
1955
2002
  DUCTAPE EVENTS (MESSAGE BROKERS)
1956
2003
 
1957
- Supported broker types: kafka | rabbitmq | redis | aws_sqs | azure_servicebus | google_pubsub | nats
2004
+ ARCHITECTURE always two separate steps:
2005
+ Step 1: Register the BROKER COMPONENT (establishes the connection to the broker service).
2006
+ The broker's envs[] holds connection credentials and host/project info, NOT topics.
2007
+ Step 2: Create TOPIC DEFINITIONS on that broker (unlimited; each is a named subject or queue).
2008
+ Topics are separate from the broker registration and added after.
2009
+ A single broker component can have as many topics as needed.
2010
+
2011
+ ━━━ SUPPORTED BROKER TYPES ━━━
2012
+
2013
+ Cloud-managed (provision OR import via cloud connection):
2014
+ GCP Pub/Sub → service: "pubsub", type in envs: "google_pubsub"
2015
+ AWS SQS → service: "sqs", type in envs: "aws_sqs"
2016
+ Azure SvcBus → service: "servicebus", type in envs: "azure_servicebus"
2017
+
2018
+ Self-hosted (import-only — supply connection URL manually):
2019
+ Kafka → type in envs: "kafka"
2020
+ RabbitMQ → type in envs: "rabbitmq"
2021
+ Redis → type in envs: "redis"
2022
+ NATS → type in envs: "nats"
2023
+
2024
+ ━━━ STEP 1A: REGISTER VIA CLOUD CONNECTION (cloud-managed brokers) ━━━
2025
+
2026
+ Provision (create a NEW resource in the cloud):
2027
+ ductape_cli("cloud resources provision-persist-all -f brokers.json --json")
2028
+ File: JSON ARRAY — one entry per env, same product + component tag across all entries.
2029
+ type field: "messageBrokers" (exact — not "messagebrokers" or "events")
2030
+
2031
+ GCP Pub/Sub — creates a new Pub/Sub topic in GCP, stores credentials in secrets:
2032
+ [{"cloud":"gcp-snd","service":"pubsub","type":"messageBrokers",
2033
+ "product":"my-product","component":"notifications-broker","env":"snd",
2034
+ "topicName":"my-product-notifications-snd"},
2035
+ {"cloud":"gcp-prd","service":"pubsub","type":"messageBrokers",
2036
+ "product":"my-product","component":"notifications-broker","env":"prd",
2037
+ "topicName":"my-product-notifications-prd"}]
2038
+ If topicName is omitted a timestamped name is generated — always supply it explicitly.
2039
+
2040
+ AWS SQS — creates a new SQS queue per env:
2041
+ [{"cloud":"aws-snd","service":"sqs","type":"messageBrokers",
2042
+ "product":"my-product","component":"notifications-broker","env":"snd",
2043
+ "queueName":"my-product-notifications-snd"},
2044
+ {"cloud":"aws-prd","service":"sqs","type":"messageBrokers",
2045
+ "product":"my-product","component":"notifications-broker","env":"prd",
2046
+ "queueName":"my-product-notifications-prd"}]
2047
+
2048
+ Azure Service Bus — creates a namespace + queue per env:
2049
+ [{"cloud":"azure-snd","service":"servicebus","type":"messageBrokers",
2050
+ "product":"my-product","component":"notifications-broker","env":"snd",
2051
+ "namespaceName":"myproduct-snd","queueName":"notifications"},
2052
+ {"cloud":"azure-prd","service":"servicebus","type":"messageBrokers",
2053
+ "product":"my-product","component":"notifications-broker","env":"prd",
2054
+ "namespaceName":"myproduct-prd","queueName":"notifications"}]
2055
+
2056
+ Import (register an EXISTING cloud resource):
2057
+ Same as above but use import-persist-all and supply "resource" (the existing resource name/ID):
2058
+ ductape_cli("cloud resources import-persist-all -f brokers.json --json")
2059
+ Each entry: { cloud, service, type: "messageBrokers", product, component, env, resource: "<id>" }
1958
2060
 
1959
- CLI aliases for the messageBrokers module: events, event, broker, brokers, message-brokers
1960
- ductape_cli("resources events list <product_tag> --json")
2061
+ IMPORTANT: Never share one cloud resource (topic/queue) across snd and prd envs — use
2062
+ separate resources per env to avoid mixing sandbox and production events.
1961
2063
 
1962
- Registration (admin ductape_cli):
1963
- Message brokers are IMPORT-ONLY (no provision-persist). Use import-persist-all:
1964
- ductape_cli("cloud resources import-persist-all -f brokers.json --json")
1965
- File is a JSON ARRAY — one entry per env.
1966
- service identifiers: pubsub (GCP Pub/Sub) | sqs (AWS SQS) | servicebus (Azure Service Bus)
1967
- type field: "messageBrokers" (not "messagebrokers" or "events")
2064
+ ━━━ STEP 1B: REGISTER SELF-HOSTED BROKER (no cloud connection needed) ━━━
2065
+
2066
+ ductape_cli("resources events create -f broker.json")
2067
+ File: {
2068
+ name: string, tag: string, description?: string,
2069
+ envs: [
2070
+ {
2071
+ slug: "snd",
2072
+ type: "kafka"|"rabbitmq"|"redis"|"nats",
2073
+ config: <see config shapes below>
2074
+ },
2075
+ { slug: "prd", type: "kafka", config: { ... } }
2076
+ ]
2077
+ }
2078
+
2079
+ Config shapes per type:
2080
+ kafka: { brokers: ["host:9092"], clientId: "my-app", groupId?: "...",
2081
+ ssl?: true, sasl?: { mechanism: "plain", username, password } }
2082
+ rabbitmq: { url: "amqp://user:pass@host:5672/vhost" }
2083
+ redis: { host: "...", port: 6379, password?: "..." }
2084
+ nats: { servers: ["nats://host:4222"], token?: "...", user?: "...", pass?: "...", tls?: true }
2085
+
2086
+ ━━━ STEP 2: ADD TOPIC DEFINITIONS (all broker types — add as many as needed) ━━━
1968
2087
 
1969
- After importing, create topics:
1970
2088
  ductape_execute("messageBrokers.topics.create", [product_tag, {
1971
- tag: "user-events", name: "User Events", broker: "broker-tag",
1972
- type: "producer"|"consumer"|"both",
2089
+ tag: "player-joined",
2090
+ name: "Player Joined",
2091
+ broker: "notifications-broker", // broker component tag
2092
+ description?: string,
2093
+ sample: { playerId: "string", username: "string" }, // example message shape
2094
+ idempotent?: boolean,
2095
+ // SQS only — map per-env queue URLs (each topic can be a different queue):
2096
+ queueUrls?: [
2097
+ { env_slug: "snd", url: "https://sqs.us-east-1.amazonaws.com/123456/my-product-player-joined-snd" },
2098
+ { env_slug: "prd", url: "https://sqs.us-east-1.amazonaws.com/123456/my-product-player-joined-prd" }
2099
+ ]
1973
2100
  }])
1974
2101
 
1975
- Produce a message (runtime):
1976
- CALL ductape_generate_payload FIRST (operation_family="messaging", method="produce")
1977
- messageBrokers.produce [{ product, env, event: "broker_tag:topic_tag", message: { key: value } }]
1978
- Idempotent publish:
1979
- messageBrokers.publishIdempotent [{ product, env, event, message, idempotency_key, ttl? }]
1980
- → checks if key was already processed; returns cached result if so.
2102
+ For GCP Pub/Sub and Azure Service Bus, the SDK resolves the topic name from the topic tag (or
2103
+ config.topicName on the env config). No extra per-topic URL mapping is needed beyond the tag.
2104
+ For SQS, every topic definition needs queueUrls to point to the specific per-env queue.
2105
+ Repeat this call for each logical event type — there is no limit on number of topics.
1981
2106
 
1982
- Consume a message (subscribe):
1983
- messageBrokers.consume [{ product, env, event: "broker_tag:topic_tag",
1984
- callback: async (message) => { ... } }]
1985
- Callback tracking is deferred (setImmediate) so user callback latency is unaffected.
1986
- Callback errors are re-thrown so the broker can nack/retry.
2107
+ List topics on a broker:
2108
+ ductape_execute("messageBrokers.topics.list", [product_tag, "broker-tag"])
1987
2109
 
1988
- Background dispatch with scheduling:
1989
- messageBrokers.dispatch [{ product, env, broker, event, input: { message },
1990
- schedule?: { start_at?, cron?, every?, limit?, endDate?, tz? } }]
1991
- → CALL ductape_generate_payload FIRST (operation_family="messaging", method="dispatch")
2110
+ ━━━ RUNTIME: PRODUCE AND CONSUME ━━━
2111
+
2112
+ Produce (publish a message):
2113
+ → CALL ductape_generate_payload FIRST (operation_family="messaging", method="produce")
2114
+ messageBrokers.produce [{ product, env, event: "broker_tag:topic_tag", message: { key: value } }]
2115
+ Idempotent publish (deduplicates by key, default TTL 86400 s):
2116
+ messageBrokers.publishIdempotent [{ product, env, event, message, idempotency_key, ttl? }]
2117
+
2118
+ Consume (subscribe):
2119
+ messageBrokers.consume [{ product, env, event: "broker_tag:topic_tag",
2120
+ callback: async (message) => { ... } }]
2121
+ Callback errors are re-thrown so the broker can nack/retry.
2122
+
2123
+ Background dispatch with scheduling:
2124
+ messageBrokers.dispatch [{ product, env, broker, event, input: { message },
2125
+ schedule?: { start_at?, cron?, every?, limit?, endDate?, tz? } }]
2126
+ → CALL ductape_generate_payload FIRST (operation_family="messaging", method="dispatch")
2127
+
2128
+ Event format string: "broker_tag:topic_tag" — always colon-separated.
2129
+ Message payload is AES-encrypted before the tracking API call — tracking never sees plaintext.
2130
+
2131
+ ━━━ OBSERVABILITY ━━━
1992
2132
 
1993
- Event tracking and observability:
1994
2133
  messageBrokers.messages.query [{ product, env, brokerTag, topicTag?, status?, page?, limit? }]
1995
2134
  messageBrokers.messages.getStats [{ product, env, brokerTag }]
1996
- → { total_events, success_count, failed_count, dead_letter_count, events_by_topic }
1997
2135
  messageBrokers.messages.getDashboard [{ product, env, brokerTag }]
1998
2136
  messageBrokers.messages.getDeadLetters [{ product, env, brokerTag, topicTag?, limit? }]
1999
2137
  messageBrokers.replayEvent [{ product, env, eventId, force? }]
2000
2138
  messageBrokers.reprocessDLQ [{ product, env, brokerTag, topicTag?, messageIds?, limit? }]
2001
- messageBrokers.checkIdempotency [{ product, env, brokerTag, idempotency_key }] → { exists, event_id? }
2002
-
2003
- Event format string: "broker_tag:topic_tag" — always colon-separated; parsed by the SDK.
2004
- Message payload is AES-encrypted before the tracking API call — tracking endpoint never sees plaintext.
2005
- Connection pool: deduplicates live connections across BrokersService instances by workspace+product+config.
2139
+ messageBrokers.checkIdempotency [{ product, env, brokerTag, idempotency_key }]
2006
2140
  `.trim(),
2007
2141
 
2008
2142
  logs: `