@ductape/mcp 0.1.14 → 0.1.16
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 +214 -60
- package/package.json +1 -1
- package/src/index.ts +214 -60
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:
|
|
158
|
+
sessions.create [product_tag, data: { tag: string, name: string, description?: string, expiry: number, period: "seconds"|"minutes"|"hours"|"days", selector: string, schema: Record<string, unknown> }]
|
|
159
|
+
← selector MUST be in the format "$Session{fieldName}" (e.g. "$Session{playerId}"). Plain dot-paths are rejected.
|
|
160
|
+
← schema is SAMPLE DATA — actual example values, not type declarations. The value at the selector path must be a primitive (string|number|boolean), not an object.
|
|
159
161
|
sessions.update [product_tag, session_tag, data: { name?: string, description?: string, expiry?: number, period?: "seconds"|"minutes"|"hours"|"days", selector?: string, schema?: object }]
|
|
160
162
|
sessions.fetch [product_tag, session_tag]
|
|
161
163
|
sessions.list [product_tag]
|
|
@@ -291,8 +293,15 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
291
293
|
messageBrokers.fetch [product_tag, broker_tag]
|
|
292
294
|
messageBrokers.list [product_tag]
|
|
293
295
|
messageBrokers.delete [product_tag, broker_tag]
|
|
294
|
-
messageBrokers.topics.create [product_tag, data: { tag: string, name: string, broker: string,
|
|
295
|
-
|
|
296
|
+
messageBrokers.topics.create [product_tag, data: { tag: string, name: string, broker: string,
|
|
297
|
+
description?: string, sample?: object, idempotent?: boolean,
|
|
298
|
+
queueUrls?: [{ env_slug: string, url: string }] // SQS only: per-env queue URL per topic
|
|
299
|
+
}]
|
|
300
|
+
← A broker can have unlimited topics. Add one per logical event type.
|
|
301
|
+
← For SQS: each topic needs queueUrls mapping env slug → the SQS queue URL for that topic+env.
|
|
302
|
+
← For Pub/Sub and Service Bus: topic name resolves from the tag or broker env config; no queueUrls needed.
|
|
303
|
+
messageBrokers.topics.update [product_tag, topic_tag, data: { name?: string, description?: string,
|
|
304
|
+
sample?: object, idempotent?: boolean, queueUrls?: [{ env_slug: string, url: string }] }]
|
|
296
305
|
messageBrokers.topics.fetch [product_tag, topic_tag]
|
|
297
306
|
messageBrokers.topics.list [product_tag, broker_tag]
|
|
298
307
|
messageBrokers.produce [{ product, env, event: "broker_tag:topic_tag", message: { key: value }, session?, cache? }] ← CALL ductape_generate_payload FIRST (operation_family="messaging", method="produce", targets={broker, event})
|
|
@@ -578,13 +587,14 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
578
587
|
features.compare [executionId1: string, executionId2: string]
|
|
579
588
|
|
|
580
589
|
━━━ MODULE: caches ━━━
|
|
581
|
-
caches.create [{
|
|
590
|
+
caches.create [product_tag, data: { name: string, tag: string, description?: string, expiry: number }]
|
|
591
|
+
← expiry is in SECONDS (e.g. 3600 = 1 hour, 86400 = 1 day). No type or envs — Ductape manages the store.
|
|
582
592
|
caches.update [product_tag, cache_tag, data: { name?: string, description?: string, expiry?: number }]
|
|
583
593
|
caches.fetch [product_tag, cache_tag]
|
|
584
594
|
caches.list [product_tag]
|
|
585
595
|
caches.delete [product_tag, cache_tag]
|
|
586
596
|
caches.get [{ key: string }]
|
|
587
|
-
caches.set [{ product, cache, key, value: string, componentTag?, componentType?, expiry?: Date }]
|
|
597
|
+
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
598
|
caches.clear [{ key: string }]
|
|
589
599
|
caches.clearAll [{ product, cache, env? }]
|
|
590
600
|
caches.fetchValues [{ product, cache, env?, page?, limit?, expiryFilter?: "all"|"expiring"|"permanent"|"expired" }]
|
|
@@ -1618,24 +1628,53 @@ Bootstrap (single API call returning product context + component config + privat
|
|
|
1618
1628
|
sessions: `
|
|
1619
1629
|
DUCTAPE SESSIONS
|
|
1620
1630
|
|
|
1621
|
-
A session is a named JWT schema on a product. It defines:
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1631
|
+
A session is a named JWT schema on a product. It defines:
|
|
1632
|
+
- tag / name — unique identifier and display name
|
|
1633
|
+
- expiry + period — how long each issued JWT is valid (duration, not an absolute date)
|
|
1634
|
+
- selector — MUST be in the format "$Session{fieldName}" where fieldName is the key
|
|
1635
|
+
in the schema that is the PRIMARY user identifier (e.g. "$Session{playerId}").
|
|
1636
|
+
Plain dot-paths like "playerId" are REJECTED by the validator.
|
|
1637
|
+
This field becomes the lookup key for revoke, list, and analytics.
|
|
1638
|
+
- schema — SAMPLE DATA showing example values for each field embedded in the JWT.
|
|
1639
|
+
This is NOT a type declaration. Use actual example values.
|
|
1640
|
+
The value at the selector path must be a primitive (string/number/boolean),
|
|
1641
|
+
not an object or array.
|
|
1642
|
+
|
|
1643
|
+
IMPORTANT — schema is sample data, not type declarations:
|
|
1644
|
+
CORRECT: schema: { playerId: "player_abc123", username: "Alice", role: "player" }
|
|
1645
|
+
INCORRECT: schema: { playerId: { type: "string", required: true } } ← WILL FAIL
|
|
1646
|
+
|
|
1647
|
+
IMPORTANT — selector must be "$Session{fieldName}" format:
|
|
1648
|
+
CORRECT: selector: "$Session{playerId}"
|
|
1649
|
+
INCORRECT: selector: "playerId" ← WILL FAIL with "Selector should be in the format $Session{...}{key}"
|
|
1650
|
+
|
|
1651
|
+
Example (game product — player identity in JWT):
|
|
1625
1652
|
ductape_execute("sessions.create", [product_tag, {
|
|
1626
|
-
tag: "
|
|
1627
|
-
name: "
|
|
1653
|
+
tag: "player-session",
|
|
1654
|
+
name: "Player Session",
|
|
1628
1655
|
expiry: 24,
|
|
1629
|
-
period: "hours",
|
|
1630
|
-
selector: "
|
|
1631
|
-
schema: {
|
|
1656
|
+
period: "hours",
|
|
1657
|
+
selector: "$Session{playerId}", // $Session{} wrapper required
|
|
1658
|
+
schema: {
|
|
1659
|
+
playerId: "player_abc123", // sample value — primitive required at selector path
|
|
1660
|
+
username: "ShadowBlade",
|
|
1661
|
+
role: "player",
|
|
1662
|
+
accountId: "acct_xyz",
|
|
1663
|
+
},
|
|
1632
1664
|
}])
|
|
1633
1665
|
|
|
1634
1666
|
Runtime — create a session (sign a JWT):
|
|
1635
1667
|
→ CALL ductape_generate_payload FIRST (operation_family="session", method="start", targets={tag})
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1668
|
+
to discover the exact data field names accepted for this session tag.
|
|
1669
|
+
ductape_execute("sessions.start", [{ product, env, tag: "player-session",
|
|
1670
|
+
data: {
|
|
1671
|
+
playerId: "player_abc123", // must match selector path — used as the revocation/lookup key
|
|
1672
|
+
username: "ShadowBlade",
|
|
1673
|
+
role: "player",
|
|
1674
|
+
accountId: "acct_xyz",
|
|
1675
|
+
} }])
|
|
1676
|
+
→ returns token: "player-session:eyJ..." ← format is always "session_tag:jwt"
|
|
1677
|
+
The token embeds all schema fields in the JWT payload, signed with the product private key.
|
|
1639
1678
|
|
|
1640
1679
|
Verify a token:
|
|
1641
1680
|
ductape_execute("sessions.verify", [{ product, env, tag: "user-session", token: "user-session:eyJ..." }])
|
|
@@ -1666,17 +1705,25 @@ DUCTAPE CACHES
|
|
|
1666
1705
|
|
|
1667
1706
|
Caches are product-level Redis (or in-memory) stores for temporary key-value data with optional TTL.
|
|
1668
1707
|
|
|
1669
|
-
Registration (admin — ductape_cli):
|
|
1708
|
+
Registration (admin — ductape_cli or SDK):
|
|
1670
1709
|
ductape_cli("resources caches create -f cache.json")
|
|
1671
|
-
File: { name, tag,
|
|
1672
|
-
|
|
1710
|
+
File: { name, tag, description?, expiry: <seconds> }
|
|
1711
|
+
No type or envs — Ductape manages the store infrastructure.
|
|
1712
|
+
expiry is in SECONDS: 3600 = 1 hour, 86400 = 1 day, 604800 = 1 week.
|
|
1713
|
+
|
|
1714
|
+
SDK: ductape_execute("caches.create", [product_tag, { name, tag, description?, expiry: 3600 }])
|
|
1673
1715
|
|
|
1674
1716
|
Operations:
|
|
1675
|
-
caches.set [{ product, cache, key, value: string, expiry?:
|
|
1717
|
+
caches.set [{ product, cache, key, value: string, expiry?: string (ISO 8601), env }]
|
|
1718
|
+
expiry is an ABSOLUTE TIMESTAMP (not a duration).
|
|
1719
|
+
To expire in 1 hour: expiry = new Date(Date.now() + 3600_000).toISOString()
|
|
1720
|
+
To expire in 24 hours: expiry = new Date(Date.now() + 86400_000).toISOString()
|
|
1721
|
+
To never expire: omit expiry entirely.
|
|
1722
|
+
Via MCP: pass an ISO 8601 string e.g. "2026-07-17T12:00:00.000Z"
|
|
1676
1723
|
→ Writes to Redis synchronously, then fires remote API write in background (non-blocking).
|
|
1677
1724
|
caches.get [{ key: string }]
|
|
1678
1725
|
→ Checks Redis first; on miss falls through to remote API; on hit from API, populates Redis.
|
|
1679
|
-
→ Enforces TTL by comparing stored expiry
|
|
1726
|
+
→ Enforces TTL by comparing the stored expiry timestamp against current time (client-side check).
|
|
1680
1727
|
caches.clear [{ key: string }]
|
|
1681
1728
|
→ Deletes from Redis and from remote API.
|
|
1682
1729
|
caches.clearAll [{ product, cache, env? }]
|
|
@@ -1691,13 +1738,22 @@ Tier architecture (three tiers applied automatically):
|
|
|
1691
1738
|
Tier 2: Redis hash (hSet/hGetAll) with optional EXPIRE
|
|
1692
1739
|
Tier 3: Remote Ductape API
|
|
1693
1740
|
|
|
1741
|
+
Practical examples:
|
|
1742
|
+
// Cache a player leaderboard for 5 minutes:
|
|
1743
|
+
{ product, cache: "leaderboard-cache", key: "top-100", value: JSON.stringify(rows),
|
|
1744
|
+
expiry: new Date(Date.now() + 300_000).toISOString(), env: "prd" }
|
|
1745
|
+
|
|
1746
|
+
// Cache a session token lookup for 1 hour:
|
|
1747
|
+
{ product, cache: "session-cache", key: "player:u_123", value: token,
|
|
1748
|
+
expiry: new Date(Date.now() + 3_600_000).toISOString(), env: "prd" }
|
|
1749
|
+
|
|
1694
1750
|
Important:
|
|
1695
1751
|
- Redis is optional; without it all reads/writes go through the remote API.
|
|
1696
|
-
- expiry is a
|
|
1697
|
-
- clearAll only clears via remote API; Redis
|
|
1752
|
+
- expiry is stored as a Date field, not a Redis TTL — the expiry check happens client-side on read.
|
|
1753
|
+
- clearAll only clears via the remote API bulk endpoint; Redis may retain stale entries until evicted.
|
|
1698
1754
|
- Cache entries are stored as Redis hashes (not plain strings).
|
|
1699
|
-
- Other services (storage, graph, notifications, sessions) also use
|
|
1700
|
-
|
|
1755
|
+
- Other services (storage, graph, notifications, sessions) also use CacheManager internally —
|
|
1756
|
+
configure a shared Redis URL at SDK init to share the pool.
|
|
1701
1757
|
`.trim(),
|
|
1702
1758
|
notifications: `
|
|
1703
1759
|
DUCTAPE NOTIFICATIONS
|
|
@@ -1885,55 +1941,153 @@ Code-first (define API — compiles async handler to JSON step schema):
|
|
|
1885
1941
|
events: `
|
|
1886
1942
|
DUCTAPE EVENTS (MESSAGE BROKERS)
|
|
1887
1943
|
|
|
1888
|
-
|
|
1944
|
+
ARCHITECTURE — always two separate steps:
|
|
1945
|
+
Step 1: Register the BROKER COMPONENT (establishes the connection to the broker service).
|
|
1946
|
+
The broker's envs[] holds connection credentials and host/project info, NOT topics.
|
|
1947
|
+
Step 2: Create TOPIC DEFINITIONS on that broker (unlimited; each is a named subject or queue).
|
|
1948
|
+
Topics are separate from the broker registration and added after.
|
|
1949
|
+
A single broker component can have as many topics as needed.
|
|
1950
|
+
|
|
1951
|
+
━━━ SUPPORTED BROKER TYPES ━━━
|
|
1952
|
+
|
|
1953
|
+
Cloud-managed (provision OR import via cloud connection):
|
|
1954
|
+
GCP Pub/Sub → service: "pubsub", type in envs: "google_pubsub"
|
|
1955
|
+
AWS SQS → service: "sqs", type in envs: "aws_sqs"
|
|
1956
|
+
Azure SvcBus → service: "servicebus", type in envs: "azure_servicebus"
|
|
1957
|
+
|
|
1958
|
+
Self-hosted (import-only — supply connection URL manually):
|
|
1959
|
+
Kafka → type in envs: "kafka"
|
|
1960
|
+
RabbitMQ → type in envs: "rabbitmq"
|
|
1961
|
+
Redis → type in envs: "redis"
|
|
1962
|
+
NATS → type in envs: "nats"
|
|
1963
|
+
|
|
1964
|
+
━━━ STEP 1A: REGISTER VIA CLOUD CONNECTION (cloud-managed brokers) ━━━
|
|
1965
|
+
|
|
1966
|
+
Provision (create a NEW resource in the cloud):
|
|
1967
|
+
ductape_cli("cloud resources provision-persist-all -f brokers.json --json")
|
|
1968
|
+
File: JSON ARRAY — one entry per env, same product + component tag across all entries.
|
|
1969
|
+
type field: "messageBrokers" (exact — not "messagebrokers" or "events")
|
|
1970
|
+
|
|
1971
|
+
GCP Pub/Sub — creates a new Pub/Sub topic in GCP, stores credentials in secrets:
|
|
1972
|
+
Cost: GCP Pub/Sub is usage-based — no upfront cost, no tier selection required.
|
|
1973
|
+
You pay per GB of data published/subscribed (first 10 GB/month free).
|
|
1974
|
+
It is safe to provision without user approval of a fixed monthly cost.
|
|
1975
|
+
[{"cloud":"gcp-snd","service":"pubsub","type":"messageBrokers",
|
|
1976
|
+
"product":"my-product","component":"notifications-broker","env":"snd",
|
|
1977
|
+
"topicName":"my-product-notifications-snd"},
|
|
1978
|
+
{"cloud":"gcp-prd","service":"pubsub","type":"messageBrokers",
|
|
1979
|
+
"product":"my-product","component":"notifications-broker","env":"prd",
|
|
1980
|
+
"topicName":"my-product-notifications-prd"}]
|
|
1981
|
+
If topicName is omitted a timestamped name is generated — always supply it explicitly.
|
|
1982
|
+
|
|
1983
|
+
AWS SQS — creates a new SQS queue per env:
|
|
1984
|
+
Cost: SQS is usage-based — no upfront cost, no tier selection required.
|
|
1985
|
+
First 1 million requests/month free; $0.40 per million after that.
|
|
1986
|
+
It is safe to provision without user approval of a fixed monthly cost.
|
|
1987
|
+
[{"cloud":"aws-snd","service":"sqs","type":"messageBrokers",
|
|
1988
|
+
"product":"my-product","component":"notifications-broker","env":"snd",
|
|
1989
|
+
"queueName":"my-product-notifications-snd"},
|
|
1990
|
+
{"cloud":"aws-prd","service":"sqs","type":"messageBrokers",
|
|
1991
|
+
"product":"my-product","component":"notifications-broker","env":"prd",
|
|
1992
|
+
"queueName":"my-product-notifications-prd"}]
|
|
1993
|
+
|
|
1994
|
+
Azure Service Bus — creates a namespace + queue per env:
|
|
1995
|
+
Cost: Azure Service Bus has TIERED pricing — confirm the tier with the user before provisioning.
|
|
1996
|
+
Basic: queues only, ~$0.05/million operations. No topics/subscriptions.
|
|
1997
|
+
Standard: queues + topics, ~$10/month base + $0.10/million operations.
|
|
1998
|
+
Premium: dedicated capacity, starts ~$677/month. Not needed for standard workloads.
|
|
1999
|
+
DO NOT provision Azure Service Bus without confirming the tier with the user.
|
|
2000
|
+
[{"cloud":"azure-snd","service":"servicebus","type":"messageBrokers",
|
|
2001
|
+
"product":"my-product","component":"notifications-broker","env":"snd",
|
|
2002
|
+
"namespaceName":"myproduct-snd","queueName":"notifications"},
|
|
2003
|
+
{"cloud":"azure-prd","service":"servicebus","type":"messageBrokers",
|
|
2004
|
+
"product":"my-product","component":"notifications-broker","env":"prd",
|
|
2005
|
+
"namespaceName":"myproduct-prd","queueName":"notifications"}]
|
|
2006
|
+
|
|
2007
|
+
Import (register an EXISTING cloud resource):
|
|
2008
|
+
Same as above but use import-persist-all and supply "resource" (the existing resource name/ID):
|
|
2009
|
+
ductape_cli("cloud resources import-persist-all -f brokers.json --json")
|
|
2010
|
+
Each entry: { cloud, service, type: "messageBrokers", product, component, env, resource: "<id>" }
|
|
1889
2011
|
|
|
1890
|
-
|
|
1891
|
-
|
|
2012
|
+
IMPORTANT: Never share one cloud resource (topic/queue) across snd and prd envs — use
|
|
2013
|
+
separate resources per env to avoid mixing sandbox and production events.
|
|
1892
2014
|
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
File
|
|
1897
|
-
|
|
1898
|
-
|
|
2015
|
+
━━━ STEP 1B: REGISTER SELF-HOSTED BROKER (no cloud connection needed) ━━━
|
|
2016
|
+
|
|
2017
|
+
ductape_cli("resources events create -f broker.json")
|
|
2018
|
+
File: {
|
|
2019
|
+
name: string, tag: string, description?: string,
|
|
2020
|
+
envs: [
|
|
2021
|
+
{
|
|
2022
|
+
slug: "snd",
|
|
2023
|
+
type: "kafka"|"rabbitmq"|"redis"|"nats",
|
|
2024
|
+
config: <see config shapes below>
|
|
2025
|
+
},
|
|
2026
|
+
{ slug: "prd", type: "kafka", config: { ... } }
|
|
2027
|
+
]
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
Config shapes per type:
|
|
2031
|
+
kafka: { brokers: ["host:9092"], clientId: "my-app", groupId?: "...",
|
|
2032
|
+
ssl?: true, sasl?: { mechanism: "plain", username, password } }
|
|
2033
|
+
rabbitmq: { url: "amqp://user:pass@host:5672/vhost" }
|
|
2034
|
+
redis: { host: "...", port: 6379, password?: "..." }
|
|
2035
|
+
nats: { servers: ["nats://host:4222"], token?: "...", user?: "...", pass?: "...", tls?: true }
|
|
2036
|
+
|
|
2037
|
+
━━━ STEP 2: ADD TOPIC DEFINITIONS (all broker types — add as many as needed) ━━━
|
|
1899
2038
|
|
|
1900
|
-
After importing, create topics:
|
|
1901
2039
|
ductape_execute("messageBrokers.topics.create", [product_tag, {
|
|
1902
|
-
tag: "
|
|
1903
|
-
|
|
2040
|
+
tag: "player-joined",
|
|
2041
|
+
name: "Player Joined",
|
|
2042
|
+
broker: "notifications-broker", // broker component tag
|
|
2043
|
+
description?: string,
|
|
2044
|
+
sample: { playerId: "string", username: "string" }, // example message shape
|
|
2045
|
+
idempotent?: boolean,
|
|
2046
|
+
// SQS only — map per-env queue URLs (each topic can be a different queue):
|
|
2047
|
+
queueUrls?: [
|
|
2048
|
+
{ env_slug: "snd", url: "https://sqs.us-east-1.amazonaws.com/123456/my-product-player-joined-snd" },
|
|
2049
|
+
{ env_slug: "prd", url: "https://sqs.us-east-1.amazonaws.com/123456/my-product-player-joined-prd" }
|
|
2050
|
+
]
|
|
1904
2051
|
}])
|
|
1905
2052
|
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
messageBrokers.publishIdempotent [{ product, env, event, message, idempotency_key, ttl? }]
|
|
1911
|
-
→ checks if key was already processed; returns cached result if so.
|
|
2053
|
+
For GCP Pub/Sub and Azure Service Bus, the SDK resolves the topic name from the topic tag (or
|
|
2054
|
+
config.topicName on the env config). No extra per-topic URL mapping is needed beyond the tag.
|
|
2055
|
+
For SQS, every topic definition needs queueUrls to point to the specific per-env queue.
|
|
2056
|
+
Repeat this call for each logical event type — there is no limit on number of topics.
|
|
1912
2057
|
|
|
1913
|
-
|
|
1914
|
-
|
|
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.
|
|
2058
|
+
List topics on a broker:
|
|
2059
|
+
ductape_execute("messageBrokers.topics.list", [product_tag, "broker-tag"])
|
|
1918
2060
|
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
2061
|
+
━━━ RUNTIME: PRODUCE AND CONSUME ━━━
|
|
2062
|
+
|
|
2063
|
+
Produce (publish a message):
|
|
2064
|
+
→ CALL ductape_generate_payload FIRST (operation_family="messaging", method="produce")
|
|
2065
|
+
messageBrokers.produce [{ product, env, event: "broker_tag:topic_tag", message: { key: value } }]
|
|
2066
|
+
Idempotent publish (deduplicates by key, default TTL 86400 s):
|
|
2067
|
+
messageBrokers.publishIdempotent [{ product, env, event, message, idempotency_key, ttl? }]
|
|
2068
|
+
|
|
2069
|
+
Consume (subscribe):
|
|
2070
|
+
messageBrokers.consume [{ product, env, event: "broker_tag:topic_tag",
|
|
2071
|
+
callback: async (message) => { ... } }]
|
|
2072
|
+
Callback errors are re-thrown so the broker can nack/retry.
|
|
2073
|
+
|
|
2074
|
+
Background dispatch with scheduling:
|
|
2075
|
+
messageBrokers.dispatch [{ product, env, broker, event, input: { message },
|
|
2076
|
+
schedule?: { start_at?, cron?, every?, limit?, endDate?, tz? } }]
|
|
2077
|
+
→ CALL ductape_generate_payload FIRST (operation_family="messaging", method="dispatch")
|
|
2078
|
+
|
|
2079
|
+
Event format string: "broker_tag:topic_tag" — always colon-separated.
|
|
2080
|
+
Message payload is AES-encrypted before the tracking API call — tracking never sees plaintext.
|
|
2081
|
+
|
|
2082
|
+
━━━ OBSERVABILITY ━━━
|
|
1923
2083
|
|
|
1924
|
-
Event tracking and observability:
|
|
1925
2084
|
messageBrokers.messages.query [{ product, env, brokerTag, topicTag?, status?, page?, limit? }]
|
|
1926
2085
|
messageBrokers.messages.getStats [{ product, env, brokerTag }]
|
|
1927
|
-
→ { total_events, success_count, failed_count, dead_letter_count, events_by_topic }
|
|
1928
2086
|
messageBrokers.messages.getDashboard [{ product, env, brokerTag }]
|
|
1929
2087
|
messageBrokers.messages.getDeadLetters [{ product, env, brokerTag, topicTag?, limit? }]
|
|
1930
2088
|
messageBrokers.replayEvent [{ product, env, eventId, force? }]
|
|
1931
2089
|
messageBrokers.reprocessDLQ [{ product, env, brokerTag, topicTag?, messageIds?, limit? }]
|
|
1932
|
-
messageBrokers.checkIdempotency [{ product, env, brokerTag, idempotency_key }]
|
|
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.
|
|
2090
|
+
messageBrokers.checkIdempotency [{ product, env, brokerTag, idempotency_key }]
|
|
1937
2091
|
`.trim(),
|
|
1938
2092
|
logs: `
|
|
1939
2093
|
DUCTAPE LOGS
|
package/package.json
CHANGED
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:
|
|
169
|
+
sessions.create [product_tag, data: { tag: string, name: string, description?: string, expiry: number, period: "seconds"|"minutes"|"hours"|"days", selector: string, schema: Record<string, unknown> }]
|
|
170
|
+
← selector MUST be in the format "$Session{fieldName}" (e.g. "$Session{playerId}"). Plain dot-paths are rejected.
|
|
171
|
+
← schema is SAMPLE DATA — actual example values, not type declarations. The value at the selector path must be a primitive (string|number|boolean), not an object.
|
|
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,
|
|
306
|
-
|
|
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})
|
|
@@ -589,13 +598,14 @@ ALL params are passed as a JSON array in positional order matching the SDK signa
|
|
|
589
598
|
features.compare [executionId1: string, executionId2: string]
|
|
590
599
|
|
|
591
600
|
━━━ MODULE: caches ━━━
|
|
592
|
-
caches.create [{
|
|
601
|
+
caches.create [product_tag, data: { name: string, tag: string, description?: string, expiry: number }]
|
|
602
|
+
← expiry is in SECONDS (e.g. 3600 = 1 hour, 86400 = 1 day). No type or envs — Ductape manages the store.
|
|
593
603
|
caches.update [product_tag, cache_tag, data: { name?: string, description?: string, expiry?: number }]
|
|
594
604
|
caches.fetch [product_tag, cache_tag]
|
|
595
605
|
caches.list [product_tag]
|
|
596
606
|
caches.delete [product_tag, cache_tag]
|
|
597
607
|
caches.get [{ key: string }]
|
|
598
|
-
caches.set [{ product, cache, key, value: string, componentTag?, componentType?, expiry?: Date }]
|
|
608
|
+
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
609
|
caches.clear [{ key: string }]
|
|
600
610
|
caches.clearAll [{ product, cache, env? }]
|
|
601
611
|
caches.fetchValues [{ product, cache, env?, page?, limit?, expiryFilter?: "all"|"expiring"|"permanent"|"expired" }]
|
|
@@ -1682,24 +1692,53 @@ Bootstrap (single API call returning product context + component config + privat
|
|
|
1682
1692
|
sessions: `
|
|
1683
1693
|
DUCTAPE SESSIONS
|
|
1684
1694
|
|
|
1685
|
-
A session is a named JWT schema on a product. It defines:
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1695
|
+
A session is a named JWT schema on a product. It defines:
|
|
1696
|
+
- tag / name — unique identifier and display name
|
|
1697
|
+
- expiry + period — how long each issued JWT is valid (duration, not an absolute date)
|
|
1698
|
+
- selector — MUST be in the format "$Session{fieldName}" where fieldName is the key
|
|
1699
|
+
in the schema that is the PRIMARY user identifier (e.g. "$Session{playerId}").
|
|
1700
|
+
Plain dot-paths like "playerId" are REJECTED by the validator.
|
|
1701
|
+
This field becomes the lookup key for revoke, list, and analytics.
|
|
1702
|
+
- schema — SAMPLE DATA showing example values for each field embedded in the JWT.
|
|
1703
|
+
This is NOT a type declaration. Use actual example values.
|
|
1704
|
+
The value at the selector path must be a primitive (string/number/boolean),
|
|
1705
|
+
not an object or array.
|
|
1706
|
+
|
|
1707
|
+
IMPORTANT — schema is sample data, not type declarations:
|
|
1708
|
+
CORRECT: schema: { playerId: "player_abc123", username: "Alice", role: "player" }
|
|
1709
|
+
INCORRECT: schema: { playerId: { type: "string", required: true } } ← WILL FAIL
|
|
1710
|
+
|
|
1711
|
+
IMPORTANT — selector must be "$Session{fieldName}" format:
|
|
1712
|
+
CORRECT: selector: "$Session{playerId}"
|
|
1713
|
+
INCORRECT: selector: "playerId" ← WILL FAIL with "Selector should be in the format $Session{...}{key}"
|
|
1714
|
+
|
|
1715
|
+
Example (game product — player identity in JWT):
|
|
1689
1716
|
ductape_execute("sessions.create", [product_tag, {
|
|
1690
|
-
tag: "
|
|
1691
|
-
name: "
|
|
1717
|
+
tag: "player-session",
|
|
1718
|
+
name: "Player Session",
|
|
1692
1719
|
expiry: 24,
|
|
1693
|
-
period: "hours",
|
|
1694
|
-
selector: "
|
|
1695
|
-
schema: {
|
|
1720
|
+
period: "hours",
|
|
1721
|
+
selector: "$Session{playerId}", // $Session{} wrapper required
|
|
1722
|
+
schema: {
|
|
1723
|
+
playerId: "player_abc123", // sample value — primitive required at selector path
|
|
1724
|
+
username: "ShadowBlade",
|
|
1725
|
+
role: "player",
|
|
1726
|
+
accountId: "acct_xyz",
|
|
1727
|
+
},
|
|
1696
1728
|
}])
|
|
1697
1729
|
|
|
1698
1730
|
Runtime — create a session (sign a JWT):
|
|
1699
1731
|
→ CALL ductape_generate_payload FIRST (operation_family="session", method="start", targets={tag})
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1732
|
+
to discover the exact data field names accepted for this session tag.
|
|
1733
|
+
ductape_execute("sessions.start", [{ product, env, tag: "player-session",
|
|
1734
|
+
data: {
|
|
1735
|
+
playerId: "player_abc123", // must match selector path — used as the revocation/lookup key
|
|
1736
|
+
username: "ShadowBlade",
|
|
1737
|
+
role: "player",
|
|
1738
|
+
accountId: "acct_xyz",
|
|
1739
|
+
} }])
|
|
1740
|
+
→ returns token: "player-session:eyJ..." ← format is always "session_tag:jwt"
|
|
1741
|
+
The token embeds all schema fields in the JWT payload, signed with the product private key.
|
|
1703
1742
|
|
|
1704
1743
|
Verify a token:
|
|
1705
1744
|
ductape_execute("sessions.verify", [{ product, env, tag: "user-session", token: "user-session:eyJ..." }])
|
|
@@ -1731,17 +1770,25 @@ DUCTAPE CACHES
|
|
|
1731
1770
|
|
|
1732
1771
|
Caches are product-level Redis (or in-memory) stores for temporary key-value data with optional TTL.
|
|
1733
1772
|
|
|
1734
|
-
Registration (admin — ductape_cli):
|
|
1773
|
+
Registration (admin — ductape_cli or SDK):
|
|
1735
1774
|
ductape_cli("resources caches create -f cache.json")
|
|
1736
|
-
File: { name, tag,
|
|
1737
|
-
|
|
1775
|
+
File: { name, tag, description?, expiry: <seconds> }
|
|
1776
|
+
No type or envs — Ductape manages the store infrastructure.
|
|
1777
|
+
expiry is in SECONDS: 3600 = 1 hour, 86400 = 1 day, 604800 = 1 week.
|
|
1778
|
+
|
|
1779
|
+
SDK: ductape_execute("caches.create", [product_tag, { name, tag, description?, expiry: 3600 }])
|
|
1738
1780
|
|
|
1739
1781
|
Operations:
|
|
1740
|
-
caches.set [{ product, cache, key, value: string, expiry?:
|
|
1782
|
+
caches.set [{ product, cache, key, value: string, expiry?: string (ISO 8601), env }]
|
|
1783
|
+
expiry is an ABSOLUTE TIMESTAMP (not a duration).
|
|
1784
|
+
To expire in 1 hour: expiry = new Date(Date.now() + 3600_000).toISOString()
|
|
1785
|
+
To expire in 24 hours: expiry = new Date(Date.now() + 86400_000).toISOString()
|
|
1786
|
+
To never expire: omit expiry entirely.
|
|
1787
|
+
Via MCP: pass an ISO 8601 string e.g. "2026-07-17T12:00:00.000Z"
|
|
1741
1788
|
→ Writes to Redis synchronously, then fires remote API write in background (non-blocking).
|
|
1742
1789
|
caches.get [{ key: string }]
|
|
1743
1790
|
→ Checks Redis first; on miss falls through to remote API; on hit from API, populates Redis.
|
|
1744
|
-
→ Enforces TTL by comparing stored expiry
|
|
1791
|
+
→ Enforces TTL by comparing the stored expiry timestamp against current time (client-side check).
|
|
1745
1792
|
caches.clear [{ key: string }]
|
|
1746
1793
|
→ Deletes from Redis and from remote API.
|
|
1747
1794
|
caches.clearAll [{ product, cache, env? }]
|
|
@@ -1756,13 +1803,22 @@ Tier architecture (three tiers applied automatically):
|
|
|
1756
1803
|
Tier 2: Redis hash (hSet/hGetAll) with optional EXPIRE
|
|
1757
1804
|
Tier 3: Remote Ductape API
|
|
1758
1805
|
|
|
1806
|
+
Practical examples:
|
|
1807
|
+
// Cache a player leaderboard for 5 minutes:
|
|
1808
|
+
{ product, cache: "leaderboard-cache", key: "top-100", value: JSON.stringify(rows),
|
|
1809
|
+
expiry: new Date(Date.now() + 300_000).toISOString(), env: "prd" }
|
|
1810
|
+
|
|
1811
|
+
// Cache a session token lookup for 1 hour:
|
|
1812
|
+
{ product, cache: "session-cache", key: "player:u_123", value: token,
|
|
1813
|
+
expiry: new Date(Date.now() + 3_600_000).toISOString(), env: "prd" }
|
|
1814
|
+
|
|
1759
1815
|
Important:
|
|
1760
1816
|
- Redis is optional; without it all reads/writes go through the remote API.
|
|
1761
|
-
- expiry is a
|
|
1762
|
-
- clearAll only clears via remote API; Redis
|
|
1817
|
+
- expiry is stored as a Date field, not a Redis TTL — the expiry check happens client-side on read.
|
|
1818
|
+
- clearAll only clears via the remote API bulk endpoint; Redis may retain stale entries until evicted.
|
|
1763
1819
|
- Cache entries are stored as Redis hashes (not plain strings).
|
|
1764
|
-
- Other services (storage, graph, notifications, sessions) also use
|
|
1765
|
-
|
|
1820
|
+
- Other services (storage, graph, notifications, sessions) also use CacheManager internally —
|
|
1821
|
+
configure a shared Redis URL at SDK init to share the pool.
|
|
1766
1822
|
`.trim(),
|
|
1767
1823
|
|
|
1768
1824
|
notifications: `
|
|
@@ -1954,55 +2010,153 @@ Code-first (define API — compiles async handler to JSON step schema):
|
|
|
1954
2010
|
events: `
|
|
1955
2011
|
DUCTAPE EVENTS (MESSAGE BROKERS)
|
|
1956
2012
|
|
|
1957
|
-
|
|
2013
|
+
ARCHITECTURE — always two separate steps:
|
|
2014
|
+
Step 1: Register the BROKER COMPONENT (establishes the connection to the broker service).
|
|
2015
|
+
The broker's envs[] holds connection credentials and host/project info, NOT topics.
|
|
2016
|
+
Step 2: Create TOPIC DEFINITIONS on that broker (unlimited; each is a named subject or queue).
|
|
2017
|
+
Topics are separate from the broker registration and added after.
|
|
2018
|
+
A single broker component can have as many topics as needed.
|
|
2019
|
+
|
|
2020
|
+
━━━ SUPPORTED BROKER TYPES ━━━
|
|
2021
|
+
|
|
2022
|
+
Cloud-managed (provision OR import via cloud connection):
|
|
2023
|
+
GCP Pub/Sub → service: "pubsub", type in envs: "google_pubsub"
|
|
2024
|
+
AWS SQS → service: "sqs", type in envs: "aws_sqs"
|
|
2025
|
+
Azure SvcBus → service: "servicebus", type in envs: "azure_servicebus"
|
|
2026
|
+
|
|
2027
|
+
Self-hosted (import-only — supply connection URL manually):
|
|
2028
|
+
Kafka → type in envs: "kafka"
|
|
2029
|
+
RabbitMQ → type in envs: "rabbitmq"
|
|
2030
|
+
Redis → type in envs: "redis"
|
|
2031
|
+
NATS → type in envs: "nats"
|
|
2032
|
+
|
|
2033
|
+
━━━ STEP 1A: REGISTER VIA CLOUD CONNECTION (cloud-managed brokers) ━━━
|
|
2034
|
+
|
|
2035
|
+
Provision (create a NEW resource in the cloud):
|
|
2036
|
+
ductape_cli("cloud resources provision-persist-all -f brokers.json --json")
|
|
2037
|
+
File: JSON ARRAY — one entry per env, same product + component tag across all entries.
|
|
2038
|
+
type field: "messageBrokers" (exact — not "messagebrokers" or "events")
|
|
2039
|
+
|
|
2040
|
+
GCP Pub/Sub — creates a new Pub/Sub topic in GCP, stores credentials in secrets:
|
|
2041
|
+
Cost: GCP Pub/Sub is usage-based — no upfront cost, no tier selection required.
|
|
2042
|
+
You pay per GB of data published/subscribed (first 10 GB/month free).
|
|
2043
|
+
It is safe to provision without user approval of a fixed monthly cost.
|
|
2044
|
+
[{"cloud":"gcp-snd","service":"pubsub","type":"messageBrokers",
|
|
2045
|
+
"product":"my-product","component":"notifications-broker","env":"snd",
|
|
2046
|
+
"topicName":"my-product-notifications-snd"},
|
|
2047
|
+
{"cloud":"gcp-prd","service":"pubsub","type":"messageBrokers",
|
|
2048
|
+
"product":"my-product","component":"notifications-broker","env":"prd",
|
|
2049
|
+
"topicName":"my-product-notifications-prd"}]
|
|
2050
|
+
If topicName is omitted a timestamped name is generated — always supply it explicitly.
|
|
2051
|
+
|
|
2052
|
+
AWS SQS — creates a new SQS queue per env:
|
|
2053
|
+
Cost: SQS is usage-based — no upfront cost, no tier selection required.
|
|
2054
|
+
First 1 million requests/month free; $0.40 per million after that.
|
|
2055
|
+
It is safe to provision without user approval of a fixed monthly cost.
|
|
2056
|
+
[{"cloud":"aws-snd","service":"sqs","type":"messageBrokers",
|
|
2057
|
+
"product":"my-product","component":"notifications-broker","env":"snd",
|
|
2058
|
+
"queueName":"my-product-notifications-snd"},
|
|
2059
|
+
{"cloud":"aws-prd","service":"sqs","type":"messageBrokers",
|
|
2060
|
+
"product":"my-product","component":"notifications-broker","env":"prd",
|
|
2061
|
+
"queueName":"my-product-notifications-prd"}]
|
|
2062
|
+
|
|
2063
|
+
Azure Service Bus — creates a namespace + queue per env:
|
|
2064
|
+
Cost: Azure Service Bus has TIERED pricing — confirm the tier with the user before provisioning.
|
|
2065
|
+
Basic: queues only, ~$0.05/million operations. No topics/subscriptions.
|
|
2066
|
+
Standard: queues + topics, ~$10/month base + $0.10/million operations.
|
|
2067
|
+
Premium: dedicated capacity, starts ~$677/month. Not needed for standard workloads.
|
|
2068
|
+
DO NOT provision Azure Service Bus without confirming the tier with the user.
|
|
2069
|
+
[{"cloud":"azure-snd","service":"servicebus","type":"messageBrokers",
|
|
2070
|
+
"product":"my-product","component":"notifications-broker","env":"snd",
|
|
2071
|
+
"namespaceName":"myproduct-snd","queueName":"notifications"},
|
|
2072
|
+
{"cloud":"azure-prd","service":"servicebus","type":"messageBrokers",
|
|
2073
|
+
"product":"my-product","component":"notifications-broker","env":"prd",
|
|
2074
|
+
"namespaceName":"myproduct-prd","queueName":"notifications"}]
|
|
2075
|
+
|
|
2076
|
+
Import (register an EXISTING cloud resource):
|
|
2077
|
+
Same as above but use import-persist-all and supply "resource" (the existing resource name/ID):
|
|
2078
|
+
ductape_cli("cloud resources import-persist-all -f brokers.json --json")
|
|
2079
|
+
Each entry: { cloud, service, type: "messageBrokers", product, component, env, resource: "<id>" }
|
|
1958
2080
|
|
|
1959
|
-
|
|
1960
|
-
|
|
2081
|
+
IMPORTANT: Never share one cloud resource (topic/queue) across snd and prd envs — use
|
|
2082
|
+
separate resources per env to avoid mixing sandbox and production events.
|
|
1961
2083
|
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
File
|
|
1966
|
-
|
|
1967
|
-
|
|
2084
|
+
━━━ STEP 1B: REGISTER SELF-HOSTED BROKER (no cloud connection needed) ━━━
|
|
2085
|
+
|
|
2086
|
+
ductape_cli("resources events create -f broker.json")
|
|
2087
|
+
File: {
|
|
2088
|
+
name: string, tag: string, description?: string,
|
|
2089
|
+
envs: [
|
|
2090
|
+
{
|
|
2091
|
+
slug: "snd",
|
|
2092
|
+
type: "kafka"|"rabbitmq"|"redis"|"nats",
|
|
2093
|
+
config: <see config shapes below>
|
|
2094
|
+
},
|
|
2095
|
+
{ slug: "prd", type: "kafka", config: { ... } }
|
|
2096
|
+
]
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
Config shapes per type:
|
|
2100
|
+
kafka: { brokers: ["host:9092"], clientId: "my-app", groupId?: "...",
|
|
2101
|
+
ssl?: true, sasl?: { mechanism: "plain", username, password } }
|
|
2102
|
+
rabbitmq: { url: "amqp://user:pass@host:5672/vhost" }
|
|
2103
|
+
redis: { host: "...", port: 6379, password?: "..." }
|
|
2104
|
+
nats: { servers: ["nats://host:4222"], token?: "...", user?: "...", pass?: "...", tls?: true }
|
|
2105
|
+
|
|
2106
|
+
━━━ STEP 2: ADD TOPIC DEFINITIONS (all broker types — add as many as needed) ━━━
|
|
1968
2107
|
|
|
1969
|
-
After importing, create topics:
|
|
1970
2108
|
ductape_execute("messageBrokers.topics.create", [product_tag, {
|
|
1971
|
-
tag: "
|
|
1972
|
-
|
|
2109
|
+
tag: "player-joined",
|
|
2110
|
+
name: "Player Joined",
|
|
2111
|
+
broker: "notifications-broker", // broker component tag
|
|
2112
|
+
description?: string,
|
|
2113
|
+
sample: { playerId: "string", username: "string" }, // example message shape
|
|
2114
|
+
idempotent?: boolean,
|
|
2115
|
+
// SQS only — map per-env queue URLs (each topic can be a different queue):
|
|
2116
|
+
queueUrls?: [
|
|
2117
|
+
{ env_slug: "snd", url: "https://sqs.us-east-1.amazonaws.com/123456/my-product-player-joined-snd" },
|
|
2118
|
+
{ env_slug: "prd", url: "https://sqs.us-east-1.amazonaws.com/123456/my-product-player-joined-prd" }
|
|
2119
|
+
]
|
|
1973
2120
|
}])
|
|
1974
2121
|
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
messageBrokers.publishIdempotent [{ product, env, event, message, idempotency_key, ttl? }]
|
|
1980
|
-
→ checks if key was already processed; returns cached result if so.
|
|
2122
|
+
For GCP Pub/Sub and Azure Service Bus, the SDK resolves the topic name from the topic tag (or
|
|
2123
|
+
config.topicName on the env config). No extra per-topic URL mapping is needed beyond the tag.
|
|
2124
|
+
For SQS, every topic definition needs queueUrls to point to the specific per-env queue.
|
|
2125
|
+
Repeat this call for each logical event type — there is no limit on number of topics.
|
|
1981
2126
|
|
|
1982
|
-
|
|
1983
|
-
|
|
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.
|
|
2127
|
+
List topics on a broker:
|
|
2128
|
+
ductape_execute("messageBrokers.topics.list", [product_tag, "broker-tag"])
|
|
1987
2129
|
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
2130
|
+
━━━ RUNTIME: PRODUCE AND CONSUME ━━━
|
|
2131
|
+
|
|
2132
|
+
Produce (publish a message):
|
|
2133
|
+
→ CALL ductape_generate_payload FIRST (operation_family="messaging", method="produce")
|
|
2134
|
+
messageBrokers.produce [{ product, env, event: "broker_tag:topic_tag", message: { key: value } }]
|
|
2135
|
+
Idempotent publish (deduplicates by key, default TTL 86400 s):
|
|
2136
|
+
messageBrokers.publishIdempotent [{ product, env, event, message, idempotency_key, ttl? }]
|
|
2137
|
+
|
|
2138
|
+
Consume (subscribe):
|
|
2139
|
+
messageBrokers.consume [{ product, env, event: "broker_tag:topic_tag",
|
|
2140
|
+
callback: async (message) => { ... } }]
|
|
2141
|
+
Callback errors are re-thrown so the broker can nack/retry.
|
|
2142
|
+
|
|
2143
|
+
Background dispatch with scheduling:
|
|
2144
|
+
messageBrokers.dispatch [{ product, env, broker, event, input: { message },
|
|
2145
|
+
schedule?: { start_at?, cron?, every?, limit?, endDate?, tz? } }]
|
|
2146
|
+
→ CALL ductape_generate_payload FIRST (operation_family="messaging", method="dispatch")
|
|
2147
|
+
|
|
2148
|
+
Event format string: "broker_tag:topic_tag" — always colon-separated.
|
|
2149
|
+
Message payload is AES-encrypted before the tracking API call — tracking never sees plaintext.
|
|
2150
|
+
|
|
2151
|
+
━━━ OBSERVABILITY ━━━
|
|
1992
2152
|
|
|
1993
|
-
Event tracking and observability:
|
|
1994
2153
|
messageBrokers.messages.query [{ product, env, brokerTag, topicTag?, status?, page?, limit? }]
|
|
1995
2154
|
messageBrokers.messages.getStats [{ product, env, brokerTag }]
|
|
1996
|
-
→ { total_events, success_count, failed_count, dead_letter_count, events_by_topic }
|
|
1997
2155
|
messageBrokers.messages.getDashboard [{ product, env, brokerTag }]
|
|
1998
2156
|
messageBrokers.messages.getDeadLetters [{ product, env, brokerTag, topicTag?, limit? }]
|
|
1999
2157
|
messageBrokers.replayEvent [{ product, env, eventId, force? }]
|
|
2000
2158
|
messageBrokers.reprocessDLQ [{ product, env, brokerTag, topicTag?, messageIds?, limit? }]
|
|
2001
|
-
messageBrokers.checkIdempotency [{ product, env, brokerTag, idempotency_key }]
|
|
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.
|
|
2159
|
+
messageBrokers.checkIdempotency [{ product, env, brokerTag, idempotency_key }]
|
|
2006
2160
|
`.trim(),
|
|
2007
2161
|
|
|
2008
2162
|
logs: `
|