@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.
- package/dist/index.js +190 -56
- package/package.json +1 -1
- 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,
|
|
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})
|
|
@@ -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:
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
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: "
|
|
1627
|
-
name: "
|
|
1647
|
+
tag: "player-session",
|
|
1648
|
+
name: "Player Session",
|
|
1628
1649
|
expiry: 24,
|
|
1629
|
-
period: "hours",
|
|
1630
|
-
selector: "
|
|
1631
|
-
schema: {
|
|
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
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
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?:
|
|
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
|
|
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
|
|
1697
|
-
- clearAll only clears via remote API; Redis
|
|
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
|
|
1700
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1891
|
-
|
|
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
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
File
|
|
1897
|
-
|
|
1898
|
-
|
|
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: "
|
|
1903
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
2038
|
+
List topics on a broker:
|
|
2039
|
+
ductape_execute("messageBrokers.topics.list", [product_tag, "broker-tag"])
|
|
1918
2040
|
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
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 }]
|
|
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
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,
|
|
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})
|
|
@@ -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:
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
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: "
|
|
1691
|
-
name: "
|
|
1711
|
+
tag: "player-session",
|
|
1712
|
+
name: "Player Session",
|
|
1692
1713
|
expiry: 24,
|
|
1693
|
-
period: "hours",
|
|
1694
|
-
selector: "
|
|
1695
|
-
schema: {
|
|
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
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
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?:
|
|
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
|
|
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
|
|
1762
|
-
- clearAll only clears via remote API; Redis
|
|
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
|
|
1765
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1960
|
-
|
|
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
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
File
|
|
1966
|
-
|
|
1967
|
-
|
|
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: "
|
|
1972
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
2107
|
+
List topics on a broker:
|
|
2108
|
+
ductape_execute("messageBrokers.topics.list", [product_tag, "broker-tag"])
|
|
1987
2109
|
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
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 }]
|
|
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: `
|