@volter/twin-upstash 0.1.0

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 (162) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +202 -0
  3. package/api/src/fetch.ts +54 -0
  4. package/api/src/generated/surface.gen.json +1 -0
  5. package/api/src/generated/ui.gen.json +1 -0
  6. package/api/src/index.ts +19 -0
  7. package/api/src/key-gate.ts +30 -0
  8. package/api/src/manifest.ts +103 -0
  9. package/api/src/screens/developer-api.tsx +106 -0
  10. package/api/src/screens/qstash.tsx +99 -0
  11. package/api/src/screens/session.tsx +125 -0
  12. package/api/src/screens/teams.tsx +114 -0
  13. package/api/src/semantics/backups.ts +90 -0
  14. package/api/src/semantics/index.ts +191 -0
  15. package/api/src/semantics/shared.ts +42 -0
  16. package/api/src/semantics/teams.ts +108 -0
  17. package/api/src/semantics/time.ts +40 -0
  18. package/dist/api/src/fetch.d.ts +15 -0
  19. package/dist/api/src/fetch.js +44 -0
  20. package/dist/api/src/fetch.ts +54 -0
  21. package/dist/api/src/generated/surface.gen.json +1 -0
  22. package/dist/api/src/generated/ui.gen.json +1 -0
  23. package/dist/api/src/index.ts +19 -0
  24. package/dist/api/src/key-gate.d.ts +3 -0
  25. package/dist/api/src/key-gate.js +30 -0
  26. package/dist/api/src/key-gate.ts +30 -0
  27. package/dist/api/src/manifest.d.ts +2 -0
  28. package/dist/api/src/manifest.js +81 -0
  29. package/dist/api/src/manifest.ts +103 -0
  30. package/dist/api/src/screens/developer-api.d.ts +3 -0
  31. package/dist/api/src/screens/developer-api.js +101 -0
  32. package/dist/api/src/screens/developer-api.tsx +106 -0
  33. package/dist/api/src/screens/qstash.d.ts +3 -0
  34. package/dist/api/src/screens/qstash.js +92 -0
  35. package/dist/api/src/screens/qstash.tsx +99 -0
  36. package/dist/api/src/screens/session.d.ts +9 -0
  37. package/dist/api/src/screens/session.js +118 -0
  38. package/dist/api/src/screens/session.tsx +125 -0
  39. package/dist/api/src/screens/teams.d.ts +3 -0
  40. package/dist/api/src/screens/teams.js +99 -0
  41. package/dist/api/src/screens/teams.tsx +114 -0
  42. package/dist/api/src/semantics/backups.d.ts +7 -0
  43. package/dist/api/src/semantics/backups.js +75 -0
  44. package/dist/api/src/semantics/backups.ts +90 -0
  45. package/dist/api/src/semantics/index.d.ts +10 -0
  46. package/dist/api/src/semantics/index.js +191 -0
  47. package/dist/api/src/semantics/index.ts +191 -0
  48. package/dist/api/src/semantics/shared.d.ts +21 -0
  49. package/dist/api/src/semantics/shared.js +34 -0
  50. package/dist/api/src/semantics/shared.ts +42 -0
  51. package/dist/api/src/semantics/teams.d.ts +13 -0
  52. package/dist/api/src/semantics/teams.js +100 -0
  53. package/dist/api/src/semantics/teams.ts +108 -0
  54. package/dist/api/src/semantics/time.d.ts +2 -0
  55. package/dist/api/src/semantics/time.js +34 -0
  56. package/dist/api/src/semantics/time.ts +40 -0
  57. package/dist/qstash/src/doors.d.ts +6 -0
  58. package/dist/qstash/src/doors.js +33 -0
  59. package/dist/qstash/src/doors.ts +51 -0
  60. package/dist/qstash/src/egress.d.ts +7 -0
  61. package/dist/qstash/src/egress.js +66 -0
  62. package/dist/qstash/src/egress.ts +58 -0
  63. package/dist/qstash/src/fetch.d.ts +7 -0
  64. package/dist/qstash/src/fetch.js +48 -0
  65. package/dist/qstash/src/fetch.ts +46 -0
  66. package/dist/qstash/src/generated/surface.gen.json +1 -0
  67. package/dist/qstash/src/generated/ui.gen.json +1 -0
  68. package/dist/qstash/src/index.ts +35 -0
  69. package/dist/qstash/src/manifest.d.ts +10 -0
  70. package/dist/qstash/src/manifest.js +105 -0
  71. package/dist/qstash/src/manifest.ts +134 -0
  72. package/dist/qstash/src/semantics/account.d.ts +29 -0
  73. package/dist/qstash/src/semantics/account.js +91 -0
  74. package/dist/qstash/src/semantics/account.ts +98 -0
  75. package/dist/qstash/src/semantics/delivery.d.ts +17 -0
  76. package/dist/qstash/src/semantics/delivery.js +274 -0
  77. package/dist/qstash/src/semantics/delivery.ts +264 -0
  78. package/dist/qstash/src/semantics/dlq.d.ts +4 -0
  79. package/dist/qstash/src/semantics/dlq.js +51 -0
  80. package/dist/qstash/src/semantics/dlq.ts +61 -0
  81. package/dist/qstash/src/semantics/index.d.ts +2 -0
  82. package/dist/qstash/src/semantics/index.js +10 -0
  83. package/dist/qstash/src/semantics/index.ts +13 -0
  84. package/dist/qstash/src/semantics/keys.d.ts +2 -0
  85. package/dist/qstash/src/semantics/keys.js +9 -0
  86. package/dist/qstash/src/semantics/keys.ts +14 -0
  87. package/dist/qstash/src/semantics/messages.d.ts +74 -0
  88. package/dist/qstash/src/semantics/messages.js +233 -0
  89. package/dist/qstash/src/semantics/messages.ts +249 -0
  90. package/dist/qstash/src/semantics/queues.d.ts +2 -0
  91. package/dist/qstash/src/semantics/queues.js +60 -0
  92. package/dist/qstash/src/semantics/queues.ts +66 -0
  93. package/dist/qstash/src/semantics/schedules.d.ts +19 -0
  94. package/dist/qstash/src/semantics/schedules.js +125 -0
  95. package/dist/qstash/src/semantics/schedules.ts +132 -0
  96. package/dist/qstash/src/semantics/shared.d.ts +45 -0
  97. package/dist/qstash/src/semantics/shared.js +115 -0
  98. package/dist/qstash/src/semantics/shared.ts +121 -0
  99. package/dist/qstash/src/semantics/urlgroups.d.ts +2 -0
  100. package/dist/qstash/src/semantics/urlgroups.js +58 -0
  101. package/dist/qstash/src/semantics/urlgroups.ts +69 -0
  102. package/dist/qstash/src/semantics/workflows.d.ts +44 -0
  103. package/dist/qstash/src/semantics/workflows.js +379 -0
  104. package/dist/qstash/src/semantics/workflows.ts +401 -0
  105. package/dist/qstash/src/signing.d.ts +4 -0
  106. package/dist/qstash/src/signing.js +16 -0
  107. package/dist/qstash/src/signing.ts +19 -0
  108. package/dist/src/cli.d.ts +2 -0
  109. package/dist/src/cli.js +35 -0
  110. package/dist/src/generated/surface.gen.json +1 -0
  111. package/dist/src/index.d.ts +18 -0
  112. package/dist/src/index.js +124 -0
  113. package/dist/src/manifest.d.ts +14 -0
  114. package/dist/src/manifest.js +8 -0
  115. package/dist/src/upstash-budget.d.ts +85 -0
  116. package/dist/src/upstash-budget.js +440 -0
  117. package/dist/src/upstash-capabilities.d.ts +4 -0
  118. package/dist/src/upstash-capabilities.js +1286 -0
  119. package/dist/src/upstash-conformance.d.ts +7 -0
  120. package/dist/src/upstash-conformance.js +119 -0
  121. package/dist/src/upstash-connector.d.ts +115 -0
  122. package/dist/src/upstash-connector.js +309 -0
  123. package/dist/src/upstash-lua.d.ts +140 -0
  124. package/dist/src/upstash-lua.js +1229 -0
  125. package/dist/src/upstash-server.d.ts +29 -0
  126. package/dist/src/upstash-server.js +81 -0
  127. package/dist/src/upstash-store.d.ts +114 -0
  128. package/dist/src/upstash-store.js +1663 -0
  129. package/dist/src/upstash-twin.d.ts +73 -0
  130. package/dist/src/upstash-twin.js +437 -0
  131. package/package.json +59 -0
  132. package/qstash/src/doors.ts +51 -0
  133. package/qstash/src/egress.ts +58 -0
  134. package/qstash/src/fetch.ts +46 -0
  135. package/qstash/src/generated/surface.gen.json +1 -0
  136. package/qstash/src/generated/ui.gen.json +1 -0
  137. package/qstash/src/index.ts +35 -0
  138. package/qstash/src/manifest.ts +134 -0
  139. package/qstash/src/semantics/account.ts +98 -0
  140. package/qstash/src/semantics/delivery.ts +264 -0
  141. package/qstash/src/semantics/dlq.ts +61 -0
  142. package/qstash/src/semantics/index.ts +13 -0
  143. package/qstash/src/semantics/keys.ts +14 -0
  144. package/qstash/src/semantics/messages.ts +249 -0
  145. package/qstash/src/semantics/queues.ts +66 -0
  146. package/qstash/src/semantics/schedules.ts +132 -0
  147. package/qstash/src/semantics/shared.ts +121 -0
  148. package/qstash/src/semantics/urlgroups.ts +69 -0
  149. package/qstash/src/semantics/workflows.ts +401 -0
  150. package/qstash/src/signing.ts +19 -0
  151. package/src/cli.ts +36 -0
  152. package/src/generated/surface.gen.json +1 -0
  153. package/src/index.ts +203 -0
  154. package/src/manifest.ts +26 -0
  155. package/src/upstash-budget.ts +486 -0
  156. package/src/upstash-capabilities.ts +1418 -0
  157. package/src/upstash-conformance.ts +131 -0
  158. package/src/upstash-connector.ts +340 -0
  159. package/src/upstash-lua.ts +1120 -0
  160. package/src/upstash-server.ts +103 -0
  161. package/src/upstash-store.ts +1437 -0
  162. package/src/upstash-twin.ts +465 -0
@@ -0,0 +1,58 @@
1
+ // The QStash lane's one outbound request: a delivery to the publisher's own destination (usually the application inside
2
+ // the World). The catch-up (semantics/delivery.ts) decides what is delivered and when; this performs one request and
3
+ // reports the destination's answer. The colocated twin host runs without the World's injector, so the kernel applies
4
+ // the World's rules before any socket opens: a destination on one of the application's own hostnames (its production
5
+ // URL, which an app in production mode publishes to) reaches the application where it listens (`appDestination`);
6
+ // otherwise `worldEgressRefusal` decides (internal addresses pass; a sealed World refuses the rest), and a refusal is a
7
+ // failed attempt, as an unreachable host is: QStash reports no answer as status -1.
8
+ import { appDestination, deliverToApp } from '@volter/world-core/app-route';
9
+ import { worldEgressRefusal } from '@volter/world-core/network-policy';
10
+
11
+ export type Outcome = { status: number; body: string; headers: Record<string, string>; error?: string };
12
+
13
+ export async function post(url: string, method: string, headers: Record<string, string>, body: string): Promise<Outcome> {
14
+ if (appDestination(url)) return toApp(url, method, headers, body);
15
+ const refusal = worldEgressRefusal(url);
16
+ return refusal !== null ? { status: -1, body: '', headers: {}, error: refusal } : send(url, method, headers, body);
17
+ }
18
+
19
+ /** The request itself, to a destination the World lets the request out to. */
20
+ async function send(url: string, method: string, headers: Record<string, string>, body: string): Promise<Outcome> {
21
+ try {
22
+ const res = await fetch(url, { method, headers, ...(method !== 'GET' && method !== 'HEAD' ? { body } : {}) });
23
+ const out: Record<string, string> = {};
24
+ res.headers.forEach((v, k) => { out[k.toLowerCase()] = v; });
25
+ return { status: res.status, body: await res.text(), headers: out };
26
+ } catch (error) {
27
+ return { status: -1, body: '', headers: {}, error: error instanceof Error ? error.message : String(error) };
28
+ }
29
+ }
30
+
31
+ /** A delivery to the application, answered as the lane's other deliveries are (fetch's defaults: a redirect followed,
32
+ * at most 20, a 303, or a 301/302 after a POST, becoming a GET without its body), each hop routed again. */
33
+ async function toApp(url: string, method: string, headers: Record<string, string>, body: string): Promise<Outcome> {
34
+ let at = url; let m = method; let b: string | undefined = body; const h = { ...headers };
35
+ for (let hops = 0; ; hops++) {
36
+ const app = appDestination(at);
37
+ // leaving the application drops what fetch drops crossing origins
38
+ if (!app) {
39
+ for (const k of Object.keys(h)) if (['authorization', 'cookie', 'proxy-authorization'].includes(k.toLowerCase())) delete h[k];
40
+ return post(at, m, h, b ?? '');
41
+ }
42
+ let answer;
43
+ try {
44
+ answer = await deliverToApp(app, at, { method: m, headers: h, body: b });
45
+ } catch (error) {
46
+ return { status: -1, body: '', headers: {}, error: error instanceof Error ? error.message : String(error) };
47
+ }
48
+ const location = answer.headers.location;
49
+ if (![301, 302, 303, 307, 308].includes(answer.status) || !location) return answer;
50
+ if (hops >= 20) return { status: -1, body: '', headers: {}, error: 'redirect count exceeded' };
51
+ at = new URL(location, at).toString();
52
+ if (answer.status === 303 || ((answer.status === 301 || answer.status === 302) && m === 'POST')) {
53
+ if (m !== 'HEAD') m = 'GET';
54
+ b = undefined;
55
+ for (const k of Object.keys(h)) if (/^content-/i.test(k)) delete h[k];
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,46 @@
1
+ // The QStash lane's fetch: QStash's and Workflow's API through the derived dispatch over the two documents' union, with the
2
+ // World's doors and the lane's gate in front, and the catch-up before all of them. The upstash pack's fetch
3
+ // (../../src/upstash-server.ts) sends it every request for a QStash host, every `/v2/` path (an application reaches QStash
4
+ // at its QSTASH_URL, the pack's own address) and the lane's doors.
5
+ import { bindSemantics, coreFor, createDerivedFetch, crossCutting, semanticsContext, type DerivedFetch, type DerivedSurface } from '@volter/world-core';
6
+ import surface from './generated/surface.gen.json' with { type: 'json' };
7
+ import { manifest, spanning } from './manifest.ts';
8
+ import { door, DOOR } from './doors.ts';
9
+ import { gate } from './semantics/account.ts';
10
+ import { catchUp, type CatchUpReport } from './semantics/delivery.ts';
11
+ import { qstashSemantics } from './semantics/index.ts';
12
+
13
+ export type QstashLaneOptions = { root?: string; readOnly?: boolean; clock?: () => string };
14
+
15
+ // A catch-up delivers to the application, which may call QStash back while it answers (a workflow's route sends its next
16
+ // step): that request is answered without a catch-up of its own, and the one already running takes up what it publishes.
17
+ const catchingUp = new Set<string>();
18
+
19
+ export function createQstashLaneFetch(options: QstashLaneOptions = {}): DerivedFetch {
20
+ const scope = { ...(options.root !== undefined ? { root: options.root } : {}), ...(options.clock ? { clock: options.clock } : {}) };
21
+ const derived = createDerivedFetch({
22
+ surface: { ...(surface as unknown as DerivedSurface), spanning },
23
+ handlers: bindSemantics(manifest, qstashSemantics, scope),
24
+ core: coreFor(manifest, scope),
25
+ around: crossCutting(manifest, { readOnly: options.readOnly ?? false, ...scope }),
26
+ // an operation the lane does not serve, and a path neither document has. Where the documents stop and the lane
27
+ // decides: QStash's answer to an unknown request is not documented; the lane answers 404 in the Error schema.
28
+ gap: () => Response.json({ error: 'Not Found' }, { status: 404 }),
29
+ });
30
+ return Object.assign(async (request: Request): Promise<Response> => {
31
+ // a body labelled JSON that does not parse is QStash's 400 (the derived dispatch answers it, `malformedBody`); the
32
+ // catch-up and the doors in front read no body of it
33
+ const ctx = await semanticsContext(manifest, request.clone(), DOOR, scope)
34
+ .catch(() => semanticsContext(manifest, new Request(request.url, { method: request.method, headers: request.headers }), DOOR, scope));
35
+ const key = options.root ?? '';
36
+ let report: CatchUpReport = { delivered: [], retried: [], failed: [] };
37
+ if (!options.readOnly && !catchingUp.has(key)) {
38
+ catchingUp.add(key);
39
+ try { report = await catchUp(ctx); } finally { catchingUp.delete(key); }
40
+ }
41
+ const own = await door(ctx, request, report);
42
+ if (own) return own;
43
+ const admitted = gate(ctx, request);
44
+ return 'refused' in admitted ? admitted.refused : derived(request);
45
+ }, { owners: () => derived.owners() });
46
+ }
@@ -0,0 +1 @@
1
+ {"generatedBy":"scripts/derive-pack.ts","spec":"openapi","format":"openapi","version":"2.0.0","resources":[{"name":"BulkAction","schema":"BulkAction","fields":[{"name":"actionId","type":"string","nullable":false},{"name":"completedAt","type":"integer","nullable":false},{"name":"createdAt","type":"integer","nullable":false},{"name":"filter","type":"object","nullable":false},{"name":"kind","type":"string","nullable":false,"enum":["cancel"]},{"name":"processed","type":"integer","nullable":false},{"name":"state","type":"string","nullable":false,"enum":["IN_PROGRESS","FINISHED"]},{"name":"total","type":"integer","nullable":false}],"stateCandidates":[{"field":"state","values":["FINISHED","IN_PROGRESS"]}],"expandable":[]},{"name":"Endpoint","schema":"Endpoint","fields":[{"name":"name","type":"string","nullable":false},{"name":"url","type":"string","nullable":false}],"stateCandidates":[],"expandable":[]},{"name":"FlowControlKey","schema":"FlowControlKey","fields":[{"name":"flowControlKey","type":"string","nullable":false},{"name":"isPaused","type":"boolean","nullable":false},{"name":"isPinnedParallelism","type":"boolean","nullable":false},{"name":"isPinnedRate","type":"boolean","nullable":false},{"name":"parallelismCount","type":"integer","nullable":false},{"name":"parallelismMax","type":"integer","nullable":false},{"name":"rateCount","type":"integer","nullable":false},{"name":"rateMax","type":"integer","nullable":false},{"name":"ratePeriod","type":"integer","nullable":false},{"name":"ratePeriodStart","type":"integer","nullable":false},{"name":"waitListSize","type":"integer","nullable":false}],"stateCandidates":[],"expandable":[]},{"name":"LogEntry","schema":"LogEntry","fields":[{"name":"body","type":"string","nullable":false},{"name":"callback","type":"string","nullable":false},{"name":"callbackHeaders","type":"object","nullable":false},{"name":"endpointName","type":"string","nullable":false},{"name":"error","type":"string","nullable":false},{"name":"failureCallback","type":"string","nullable":false},{"name":"failureCallbackHeaders","type":"object","nullable":false},{"name":"header","type":"object","nullable":false},{"name":"label","type":"string","nullable":false},{"name":"labels","type":"array","nullable":false},{"name":"maxRetries","type":"integer","nullable":false},{"name":"messageId","type":"string","nullable":false},{"name":"method","type":"string","nullable":false},{"name":"nextDeliveryTime","type":"integer","nullable":false},{"name":"queueName","type":"string","nullable":false},{"name":"responseBody","type":"string","nullable":false},{"name":"responseHeaders","type":"object","nullable":false},{"name":"responseStatus","type":"integer","nullable":false},{"name":"retryDelayExpression","type":"string","nullable":false},{"name":"scheduleId","type":"string","nullable":false},{"name":"state","type":"string","nullable":false,"enum":["CREATED","ACTIVE","RETRY","ERROR","DELIVERED","FAILED","CANCEL_REQUESTED","CANCELLED"]},{"name":"time","type":"integer","nullable":false},{"name":"timeout","type":"integer","nullable":false},{"name":"topicName","type":"string","nullable":false},{"name":"url","type":"string","nullable":false}],"stateCandidates":[{"field":"state","values":["ACTIVE","CANCELLED","CANCEL_REQUESTED","CREATED","DELIVERED","ERROR","FAILED","RETRY"]}],"expandable":[]},{"name":"Queue","schema":"Queue","fields":[{"name":"createdAt","type":"integer","nullable":false},{"name":"lag","type":"integer","nullable":false},{"name":"name","type":"string","nullable":false},{"name":"parallelism","type":"integer","nullable":false},{"name":"paused","type":"boolean","nullable":false},{"name":"updatedAt","type":"integer","nullable":false}],"stateCandidates":[],"expandable":[]},{"name":"Schedule","schema":"Schedule","fields":[{"name":"body","type":"string","nullable":false},{"name":"callback","type":"string","nullable":false},{"name":"callerIp","type":"string","nullable":false},{"name":"createdAt","type":"integer","nullable":false,"required":true},{"name":"cron","type":"string","nullable":false,"required":true},{"name":"delay","type":"integer","nullable":false},{"name":"destination","type":"string","nullable":false,"required":true},{"name":"failureCallback","type":"string","nullable":false},{"name":"flowControlKey","type":"string","nullable":false},{"name":"header","type":"object","nullable":false},{"name":"isPaused","type":"boolean","nullable":false,"required":true},{"name":"label","type":"string","nullable":false},{"name":"labels","type":"array","nullable":false},{"name":"lastScheduleStates","type":"object","nullable":false},{"name":"lastScheduleTime","type":"integer","nullable":false},{"name":"method","type":"string","nullable":false,"required":true},{"name":"nextScheduleTime","type":"integer","nullable":false},{"name":"parallelism","type":"integer","nullable":false},{"name":"period","type":"integer","nullable":false},{"name":"rate","type":"integer","nullable":false},{"name":"retries","type":"integer","nullable":false},{"name":"retryDelayExpression","type":"string","nullable":false},{"name":"scheduleId","type":"string","nullable":false,"required":true}],"stateCandidates":[],"expandable":[]},{"name":"URLGroup","schema":"URLGroup","fields":[{"name":"createdAt","type":"integer","nullable":false},{"name":"endpoints","type":"array","nullable":false},{"name":"name","type":"string","nullable":false},{"name":"updatedAt","type":"integer","nullable":false}],"stateCandidates":[],"expandable":[]},{"name":"Waiter","schema":"Waiter","fields":[{"name":"deadline","type":"integer","nullable":false},{"name":"headers","type":"object","nullable":false},{"name":"timeoutBody","type":"string","nullable":false},{"name":"timeoutHeaders","type":"object","nullable":false},{"name":"timeoutUrl","type":"string","nullable":false},{"name":"url","type":"string","nullable":false}],"stateCandidates":[],"expandable":[]},{"name":"WorkflowDLQMessage","schema":"WorkflowDLQMessage","fields":[{"name":"body","type":"string","nullable":false},{"name":"bodyBase64","type":"string","nullable":false},{"name":"callerIP","type":"string","nullable":false},{"name":"createdAt","type":"integer","nullable":false},{"name":"dlqId","type":"string","nullable":false},{"name":"failureCallback","type":"string","nullable":false},{"name":"failureFunctionState","type":"string","nullable":false},{"name":"flowControlKey","type":"string","nullable":false},{"name":"header","type":"object","nullable":false},{"name":"label","type":"string","nullable":false},{"name":"labels","type":"array","nullable":false},{"name":"maxRetries","type":"integer","nullable":false},{"name":"method","type":"string","nullable":false},{"name":"responseBody","type":"string","nullable":false},{"name":"responseBodyBase64","type":"string","nullable":false},{"name":"responseHeader","type":"object","nullable":false},{"name":"responseStatus","type":"integer","nullable":false},{"name":"url","type":"string","nullable":false},{"name":"workflowCreatedAt","type":"integer","nullable":false},{"name":"workflowRunId","type":"string","nullable":false},{"name":"workflowUrl","type":"string","nullable":false}],"stateCandidates":[],"expandable":[]},{"name":"WorkflowRun","schema":"WorkflowRun","fields":[{"name":"dlqId","type":"string","nullable":false},{"name":"failureFunction","type":"FailureFunction","nullable":false},{"name":"flowControlKey","type":"string","nullable":false},{"name":"invoker","type":"InvokerContext","nullable":false},{"name":"label","type":"string","nullable":false},{"name":"labels","type":"array","nullable":false},{"name":"steps","type":"array","nullable":false},{"name":"workflowRunCallerIp","type":"string","nullable":false},{"name":"workflowRunCompletedAt","type":"integer","nullable":false},{"name":"workflowRunCreatedAt","type":"integer","nullable":false},{"name":"workflowRunId","type":"string","nullable":false},{"name":"workflowRunResponse","type":"string","nullable":false},{"name":"workflowState","type":"string","nullable":false},{"name":"workflowUrl","type":"string","nullable":false}],"stateCandidates":[],"expandable":[]}],"operations":[{"id":"post_v2_batch","method":"post","path":"/v2/batch","pathParams":[],"query":[],"body":[],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"post_v2_batch_trigger","method":"post","path":"/v2/batch/trigger","pathParams":[],"query":[],"body":[],"bodyEncoding":"json","successStatus":201,"class":"non-resource"},{"id":"get_v2_bulkactions","method":"get","path":"/v2/bulkActions","pathParams":[],"query":[{"name":"count","type":"integer","required":false},{"name":"cursor","type":"string","required":false},{"name":"state","type":"string","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"BulkAction","list":true,"key":"bulkActions"},"class":"list","resource":"BulkAction"},{"id":"get_v2_bulkactions_actionid","method":"get","path":"/v2/bulkActions/{actionId}","pathParams":["actionId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"BulkAction","list":false},"class":"retrieve","resource":"BulkAction"},{"id":"get_v2_dlq","method":"get","path":"/v2/dlq","pathParams":[],"query":[{"name":"callerIp","type":"array","required":false},{"name":"count","type":"integer","required":false},{"name":"cursor","type":"string","required":false},{"name":"fromDate","type":"integer","required":false},{"name":"label","type":"array","required":false},{"name":"messageId","type":"string","required":false},{"name":"queueName","type":"array","required":false},{"name":"responseStatus","type":"array","required":false},{"name":"scheduleId","type":"array","required":false},{"name":"source","type":"string","required":false},{"name":"toDate","type":"integer","required":false},{"name":"topicName","type":"array","required":false},{"name":"url","type":"array","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"delete_v2_dlq","method":"delete","path":"/v2/dlq","pathParams":[],"query":[{"name":"callerIp","type":"array","required":false},{"name":"count","type":"integer","required":false},{"name":"cursor","type":"string","required":false},{"name":"dlqIds","type":"array","required":false},{"name":"flowControlKey","type":"array","required":false},{"name":"fromDate","type":"integer","required":false},{"name":"label","type":"array","required":false},{"name":"messageId","type":"string","required":false},{"name":"queueName","type":"array","required":false},{"name":"responseStatus","type":"array","required":false},{"name":"scheduleId","type":"array","required":false},{"name":"toDate","type":"integer","required":false},{"name":"topicName","type":"array","required":false},{"name":"url","type":"array","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action"},{"id":"get_v2_dlq_dlqid","method":"get","path":"/v2/dlq/{dlqId}","pathParams":["dlqId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"delete_v2_dlq_dlqid","method":"delete","path":"/v2/dlq/{dlqId}","pathParams":["dlqId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"delete"},{"id":"post_v2_dlq_retry","method":"post","path":"/v2/dlq/retry","pathParams":[],"query":[{"name":"callerIp","type":"array","required":false},{"name":"dlqIds","type":"array","required":false},{"name":"flowControlKey","type":"array","required":false},{"name":"fromDate","type":"integer","required":false},{"name":"label","type":"array","required":false},{"name":"messageId","type":"string","required":false},{"name":"queueName","type":"array","required":false},{"name":"responseStatus","type":"array","required":false},{"name":"scheduleId","type":"array","required":false},{"name":"toDate","type":"integer","required":false},{"name":"topicName","type":"array","required":false},{"name":"url","type":"array","required":false}],"body":[],"bodyEncoding":"none","successStatus":201,"class":"non-resource"},{"id":"post_v2_dlq_retry_dlqid","method":"post","path":"/v2/dlq/retry/{dlqId}","pathParams":["dlqId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":201,"class":"non-resource"},{"id":"post_v2_enqueue_queuename_destination","method":"post","path":"/v2/enqueue/{queueName}/{destination}","pathParams":["queueName","destination","queueName"],"query":[],"body":[],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"get_v2_flowcontrol","method":"get","path":"/v2/flowControl","pathParams":[],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"FlowControlKey","list":true},"class":"list","resource":"FlowControlKey"},{"id":"get_v2_flowcontrol_flowcontrolkey","method":"get","path":"/v2/flowControl/{flowControlKey}","pathParams":["flowControlKey"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"FlowControlKey","list":false},"class":"retrieve","resource":"FlowControlKey"},{"id":"post_v2_flowcontrol_flowcontrolkey_pause","method":"post","path":"/v2/flowControl/{flowControlKey}/pause","pathParams":["flowControlKey"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action","resource":"FlowControlKey"},{"id":"post_v2_flowcontrol_flowcontrolkey_pin","method":"post","path":"/v2/flowControl/{flowControlKey}/pin","pathParams":["flowControlKey"],"query":[{"name":"parallelism","type":"integer","required":false},{"name":"period","type":"integer","required":false},{"name":"rate","type":"integer","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action","resource":"FlowControlKey"},{"id":"post_v2_flowcontrol_flowcontrolkey_resetrate","method":"post","path":"/v2/flowControl/{flowControlKey}/resetRate","pathParams":["flowControlKey"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action","resource":"FlowControlKey"},{"id":"post_v2_flowcontrol_flowcontrolkey_resume","method":"post","path":"/v2/flowControl/{flowControlKey}/resume","pathParams":["flowControlKey"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action","resource":"FlowControlKey"},{"id":"post_v2_flowcontrol_flowcontrolkey_unpin","method":"post","path":"/v2/flowControl/{flowControlKey}/unpin","pathParams":["flowControlKey"],"query":[{"name":"parallelism","type":"boolean","required":false},{"name":"rate","type":"boolean","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action","resource":"FlowControlKey"},{"id":"get_v2_globalparallelism","method":"get","path":"/v2/globalParallelism","pathParams":[],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"get_v2_keys","method":"get","path":"/v2/keys","pathParams":[],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"get_v2_keys_rotate","method":"get","path":"/v2/keys/rotate","pathParams":["flowControlKey"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"FlowControlKey","list":false},"class":"retrieve","resource":"FlowControlKey"},{"id":"post_v2_keys_rotate","method":"post","path":"/v2/keys/rotate","pathParams":[],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"non-resource"},{"id":"get_v2_logs","method":"get","path":"/v2/logs","pathParams":[],"query":[{"name":"count","type":"integer","required":false},{"name":"cursor","type":"string","required":false},{"name":"fromDate","type":"integer","required":false},{"name":"label","type":"array","required":false},{"name":"messageId","type":"string","required":false},{"name":"queueName","type":"string","required":false},{"name":"scheduleId","type":"string","required":false},{"name":"state","type":"string","required":false},{"name":"toDate","type":"integer","required":false},{"name":"topicName","type":"string","required":false},{"name":"url","type":"string","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"LogEntry","list":true,"key":"logs"},"class":"list","resource":"LogEntry"},{"id":"delete_v2_messages","method":"delete","path":"/v2/messages","pathParams":[],"query":[{"name":"async","type":"boolean","required":false},{"name":"callerIp","type":"array","required":false},{"name":"callerIP","type":"array","required":false},{"name":"count","type":"integer","required":false},{"name":"flowControlKey","type":"array","required":false},{"name":"fromDate","type":"integer","required":false},{"name":"host","type":"array","required":false},{"name":"label","type":"array","required":false},{"name":"messageIds","type":"array","required":false},{"name":"path","type":"array","required":false},{"name":"queueName","type":"array","required":false},{"name":"scheduleId","type":"array","required":false},{"name":"toDate","type":"integer","required":false},{"name":"topicName","type":"array","required":false},{"name":"url","type":"array","required":false}],"body":[],"bodyEncoding":"none","successStatus":202,"class":"action"},{"id":"get_v2_messages_messageid","method":"get","path":"/v2/messages/{messageId}","pathParams":["messageId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"computed"},{"id":"delete_v2_messages_messageid","method":"delete","path":"/v2/messages/{messageId}","pathParams":["messageId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":202,"class":"delete"},{"id":"post_v2_messages_messageid_retry","method":"post","path":"/v2/messages/{messageId}/retry","pathParams":["messageId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"non-resource"},{"id":"post_v2_notify_eventid","method":"post","path":"/v2/notify/{eventId}","pathParams":["eventId"],"query":[],"body":[],"bodyEncoding":"json","successStatus":201,"class":"non-resource"},{"id":"post_v2_notify_workflowrunid_eventid","method":"post","path":"/v2/notify/{workflowRunId}/{eventId}","pathParams":["workflowRunId","eventId"],"query":[],"body":[],"bodyEncoding":"json","successStatus":201,"answers":{"resource":"Waiter","list":false,"key":"waiter"},"class":"action","resource":"Waiter"},{"id":"post_v2_publish_destination","method":"post","path":"/v2/publish/{destination}","pathParams":["destination"],"query":[],"body":[],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"get_v2_queues","method":"get","path":"/v2/queues","pathParams":[],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"Queue","list":true},"class":"list","resource":"Queue"},{"id":"post_v2_queues","method":"post","path":"/v2/queues","pathParams":[],"query":[],"body":[{"name":"parallelism","type":"integer","required":true},{"name":"queueName","type":"string","required":true}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"get_v2_queues_queuename","method":"get","path":"/v2/queues/{queueName}","pathParams":["queueName"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"Queue","list":false},"class":"retrieve","resource":"Queue"},{"id":"delete_v2_queues_queuename","method":"delete","path":"/v2/queues/{queueName}","pathParams":["queueName"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"delete","resource":"Queue"},{"id":"post_v2_queues_queuename_pause","method":"post","path":"/v2/queues/{queueName}/pause","pathParams":["queueName"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action","resource":"Queue"},{"id":"post_v2_queues_queuename_resume","method":"post","path":"/v2/queues/{queueName}/resume","pathParams":["queueName"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action","resource":"Queue"},{"id":"get_v2_schedules","method":"get","path":"/v2/schedules","pathParams":[],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"Schedule","list":true},"class":"list","resource":"Schedule"},{"id":"post_v2_schedules_destination","method":"post","path":"/v2/schedules/{destination}","pathParams":["destination"],"query":[],"body":[],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"get_v2_schedules_scheduleid","method":"get","path":"/v2/schedules/{scheduleId}","pathParams":["scheduleId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"Schedule","list":false},"class":"retrieve","resource":"Schedule"},{"id":"delete_v2_schedules_scheduleid","method":"delete","path":"/v2/schedules/{scheduleId}","pathParams":["scheduleId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"delete","resource":"Schedule"},{"id":"post_v2_schedules_scheduleid_pause","method":"post","path":"/v2/schedules/{scheduleId}/pause","pathParams":["scheduleId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action","resource":"Schedule"},{"id":"patch_v2_schedules_scheduleid_pause","method":"patch","path":"/v2/schedules/{scheduleId}/pause","pathParams":["scheduleId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action","resource":"Schedule"},{"id":"post_v2_schedules_scheduleid_resume","method":"post","path":"/v2/schedules/{scheduleId}/resume","pathParams":["scheduleId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action","resource":"Schedule"},{"id":"patch_v2_schedules_scheduleid_resume","method":"patch","path":"/v2/schedules/{scheduleId}/resume","pathParams":["scheduleId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action","resource":"Schedule"},{"id":"get_v2_topics","method":"get","path":"/v2/topics","pathParams":[],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"URLGroup","list":true},"class":"list","resource":"URLGroup"},{"id":"get_v2_topics_urlgroupname","method":"get","path":"/v2/topics/{urlGroupName}","pathParams":["urlGroupName"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"URLGroup","list":false},"class":"retrieve","resource":"URLGroup"},{"id":"delete_v2_topics_urlgroupname","method":"delete","path":"/v2/topics/{urlGroupName}","pathParams":["urlGroupName"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"delete","resource":"URLGroup"},{"id":"post_v2_topics_urlgroupname_endpoints","method":"post","path":"/v2/topics/{urlGroupName}/endpoints","pathParams":["urlGroupName"],"query":[],"body":[{"name":"endpoints","type":"array","required":true}],"bodyEncoding":"json","successStatus":200,"class":"action","resource":"URLGroup"},{"id":"delete_v2_topics_urlgroupname_endpoints","method":"delete","path":"/v2/topics/{urlGroupName}/endpoints","pathParams":["urlGroupName"],"query":[],"body":[{"name":"endpoints","type":"array","required":true}],"bodyEncoding":"json","successStatus":200,"class":"action","resource":"URLGroup"},{"id":"post_v2_trigger_workflowurl","method":"post","path":"/v2/trigger/{workflowUrl}","pathParams":["workflowUrl"],"query":[],"body":[],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"post_v2_wait_eventid","method":"post","path":"/v2/wait/{eventId}","pathParams":["eventId"],"query":[],"body":[{"name":"step","type":"object","required":false},{"name":"timeout","type":"string","required":false},{"name":"timeoutBody","type":"string","required":false},{"name":"timeoutHeaders","type":"object","required":false},{"name":"timeoutUrl","type":"string","required":false},{"name":"url","type":"string","required":false}],"bodyEncoding":"json","successStatus":200,"class":"non-resource"},{"id":"get_v2_waiters_eventid","method":"get","path":"/v2/waiters/{eventId}","pathParams":["eventId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"Waiter","list":true},"class":"list","resource":"Waiter"},{"id":"get_v2_workflows_bulkactions","method":"get","path":"/v2/workflows/bulkActions","pathParams":[],"query":[{"name":"count","type":"integer","required":false},{"name":"cursor","type":"string","required":false},{"name":"state","type":"string","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"BulkAction","list":true,"key":"bulkActions"},"class":"list","resource":"BulkAction"},{"id":"get_v2_workflows_bulkactions_actionid","method":"get","path":"/v2/workflows/bulkActions/{actionId}","pathParams":["actionId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"BulkAction","list":false},"class":"retrieve","resource":"BulkAction"},{"id":"get_v2_workflows_dlq","method":"get","path":"/v2/workflows/dlq","pathParams":[],"query":[{"name":"callerIp","type":"array","required":false},{"name":"count","type":"integer","required":false},{"name":"cursor","type":"string","required":false},{"name":"failureFunctionState","type":"array","required":false},{"name":"flowControlKey","type":"array","required":false},{"name":"fromDate","type":"integer","required":false},{"name":"label","type":"array","required":false},{"name":"toDate","type":"integer","required":false},{"name":"workflowCreatedAt","type":"integer","required":false},{"name":"workflowRunId","type":"string","required":false},{"name":"workflowUrl","type":"array","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"WorkflowDLQMessage","list":true,"key":"messages"},"class":"list","resource":"WorkflowDLQMessage"},{"id":"delete_v2_workflows_dlq","method":"delete","path":"/v2/workflows/dlq","pathParams":[],"query":[{"name":"callerIp","type":"array","required":false},{"name":"count","type":"integer","required":false},{"name":"cursor","type":"string","required":false},{"name":"dlqIds","type":"array","required":false},{"name":"failureFunctionState","type":"array","required":false},{"name":"flowControlKey","type":"array","required":false},{"name":"fromDate","type":"integer","required":false},{"name":"label","type":"array","required":false},{"name":"toDate","type":"integer","required":false},{"name":"workflowCreatedAt","type":"integer","required":false},{"name":"workflowRunId","type":"string","required":false},{"name":"workflowUrl","type":"array","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action"},{"id":"get_v2_workflows_dlq_dlqid","method":"get","path":"/v2/workflows/dlq/{dlqId}","pathParams":["dlqId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"WorkflowDLQMessage","list":false},"class":"retrieve","resource":"WorkflowDLQMessage"},{"id":"delete_v2_workflows_dlq_dlqid","method":"delete","path":"/v2/workflows/dlq/{dlqId}","pathParams":["dlqId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"delete","resource":"WorkflowDLQMessage"},{"id":"post_v2_workflows_dlq_callback_dlqid","method":"post","path":"/v2/workflows/dlq/callback/{dlqId}","pathParams":["dlqId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"non-resource"},{"id":"delete_v2_workflows_dlq_callback_dlqid","method":"delete","path":"/v2/workflows/dlq/callback/{dlqId}","pathParams":["dlqId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"delete"},{"id":"post_v2_workflows_dlq_restart","method":"post","path":"/v2/workflows/dlq/restart","pathParams":[],"query":[{"name":"callerIp","type":"array","required":false},{"name":"count","type":"integer","required":false},{"name":"cursor","type":"string","required":false},{"name":"dlqIds","type":"array","required":false},{"name":"failureFunctionState","type":"array","required":false},{"name":"flowControlKey","type":"array","required":false},{"name":"fromDate","type":"integer","required":false},{"name":"label","type":"array","required":false},{"name":"toDate","type":"integer","required":false},{"name":"workflowCreatedAt","type":"integer","required":false},{"name":"workflowRunId","type":"string","required":false},{"name":"workflowUrl","type":"array","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"non-resource"},{"id":"post_v2_workflows_dlq_restart_dlqid","method":"post","path":"/v2/workflows/dlq/restart/{dlqId}","pathParams":["dlqId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"non-resource"},{"id":"post_v2_workflows_dlq_resume","method":"post","path":"/v2/workflows/dlq/resume","pathParams":[],"query":[{"name":"callerIp","type":"array","required":false},{"name":"count","type":"integer","required":false},{"name":"cursor","type":"string","required":false},{"name":"dlqIds","type":"array","required":false},{"name":"failureFunctionState","type":"array","required":false},{"name":"flowControlKey","type":"array","required":false},{"name":"fromDate","type":"integer","required":false},{"name":"label","type":"array","required":false},{"name":"toDate","type":"integer","required":false},{"name":"workflowCreatedAt","type":"integer","required":false},{"name":"workflowRunId","type":"string","required":false},{"name":"workflowUrl","type":"array","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"non-resource"},{"id":"post_v2_workflows_dlq_resume_dlqid","method":"post","path":"/v2/workflows/dlq/resume/{dlqId}","pathParams":["dlqId"],"query":[],"body":[],"bodyEncoding":"none","successStatus":200,"class":"non-resource"},{"id":"get_v2_workflows_logs","method":"get","path":"/v2/workflows/logs","pathParams":[],"query":[{"name":"callerIp","type":"string","required":false},{"name":"count","type":"integer","required":false},{"name":"cursor","type":"string","required":false},{"name":"flowControlKey","type":"string","required":false},{"name":"fromDate","type":"integer","required":false},{"name":"label","type":"array","required":false},{"name":"state","type":"string","required":false},{"name":"toDate","type":"integer","required":false},{"name":"trimBody","type":"integer","required":false},{"name":"workflowCreatedAt","type":"integer","required":false},{"name":"workflowRunId","type":"string","required":false},{"name":"workflowRuns","type":"string","required":false},{"name":"workflowUrl","type":"string","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"answers":{"resource":"WorkflowRun","list":true,"key":"runs"},"class":"list","resource":"WorkflowRun"},{"id":"delete_v2_workflows_runs","method":"delete","path":"/v2/workflows/runs","pathParams":[],"query":[{"name":"async","type":"boolean","required":false},{"name":"callerIp","type":"array","required":false},{"name":"count","type":"integer","required":false},{"name":"flowControlKey","type":"array","required":false},{"name":"fromDate","type":"number","required":false},{"name":"host","type":"array","required":false},{"name":"label","type":"array","required":false},{"name":"path","type":"array","required":false},{"name":"toDate","type":"number","required":false},{"name":"workflowRunIds","type":"array","required":false},{"name":"workflowUrl","type":"array","required":false},{"name":"workflowUrlExactMatch","type":"boolean","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"action"},{"id":"delete_v2_workflows_runs_workflowrunid","method":"delete","path":"/v2/workflows/runs/{workflowRunId}","pathParams":["workflowRunId"],"query":[{"name":"cancel","type":"boolean","required":false}],"body":[],"bodyEncoding":"none","successStatus":200,"class":"delete"}]}
@@ -0,0 +1 @@
1
+ {"generatedBy":"scripts/derive-pack.ts","resources":{"Message":{"actions":[],"states":{"state":["ACTIVE","CANCELLED","CANCEL_REQUESTED","CREATED","DELIVERED","FAILED","RETRY"]},"embeds":{}},"Queue":{"retrieve":"/v2/queues/{queueName}","actions":[],"states":{},"embeds":{}},"Schedule":{"actions":[{"operation":"post_v2_schedules_scheduleid_pause","method":"POST","path":"/v2/schedules/{scheduleId}/pause","field":"isPaused","from":["true"]},{"operation":"post_v2_schedules_scheduleid_pause","method":"POST","path":"/v2/schedules/{scheduleId}/pause","field":"isPaused","from":["false"],"to":"true"},{"operation":"patch_v2_schedules_scheduleid_pause","method":"PATCH","path":"/v2/schedules/{scheduleId}/pause","field":"isPaused","from":["true"]},{"operation":"patch_v2_schedules_scheduleid_pause","method":"PATCH","path":"/v2/schedules/{scheduleId}/pause","field":"isPaused","from":["false"],"to":"true"}],"states":{"isPaused":["false","true"]},"embeds":{}},"URLGroup":{"list":"/v2/topics","retrieve":"/v2/topics/{urlGroupName}","actions":[],"states":{},"embeds":{}},"DLQMessage":{"actions":[],"states":{},"embeds":{}},"WorkflowRun":{"actions":[],"states":{"workflowState":["RUN_CANCELED","RUN_FAILED","RUN_STARTED","RUN_SUCCESS"]},"embeds":{}},"WorkflowDLQMessage":{"list":"/v2/workflows/dlq","actions":[],"states":{},"embeds":{}},"Waiter":{"actions":[],"states":{},"embeds":{}}}}
@@ -0,0 +1,35 @@
1
+ // The QStash lane of the upstash pack (QStash and Upstash Workflow, one API in two documents), as a plain fetch: the
2
+ // whole pack's fetch (the Redis REST wire, the Developer API and console at api.upstash.com and console.upstash.com, and
3
+ // QStash), whose owners are the operations this lane serves. Its life walks through them: a customer signs in to the
4
+ // console for its QStash token and keys, and publishes, schedules, queues and runs workflows through QStash's API
5
+ // (journeys/customer-life.outline.md).
6
+ import type { DerivedFetch } from '@volter/world-core';
7
+ import { createUpstashRedisTwinFetch } from '../../src/upstash-server.ts';
8
+ import { createQstashLaneFetch, type QstashLaneOptions } from './fetch.ts';
9
+
10
+ export { manifest } from './manifest.ts';
11
+ export { createQstashLaneFetch } from './fetch.ts';
12
+
13
+ export function createQstashTwinFetch(options: QstashLaneOptions = {}): DerivedFetch {
14
+ const lane = createQstashLaneFetch(options);
15
+ const pack = createUpstashRedisTwinFetch({ ...(options.root !== undefined ? { root: options.root } : {}), ...(options.readOnly !== undefined ? { readOnly: options.readOnly } : {}), qstash: lane });
16
+ return Object.assign((request: Request) => pack(request), { owners: () => lane.owners() });
17
+ }
18
+
19
+ /** The pack's views of the lane's answers, for a journey step's `view` (scripts/journey-kit.ts). `steps`: the deliveries
20
+ * door's answer with each workflow delivery's body read as `serve()` reads it: its steps, each step's published body
21
+ * decoded from base64 (and parsed, where it is JSON). */
22
+ export const answerViews: Record<string, (body: unknown) => unknown> = {
23
+ steps: (body) => {
24
+ const b = body as { deliveries?: Array<Record<string, unknown>> };
25
+ const decode = (s: string): unknown => { const t = Buffer.from(s, 'base64').toString('utf8'); try { return JSON.parse(t); } catch { return t; } };
26
+ return {
27
+ ...b,
28
+ deliveries: (b.deliveries ?? []).map((d) => {
29
+ let raw: unknown;
30
+ try { raw = JSON.parse(String(d.body)); } catch { return d; }
31
+ return Array.isArray(raw) ? { ...d, steps: raw.map((r: Record<string, unknown>) => ({ ...r, body: decode(String(r.body ?? '')) })) } : d;
32
+ }),
33
+ };
34
+ },
35
+ };
@@ -0,0 +1,134 @@
1
+ // Upstash QStash and Upstash Workflow, the `qstash` lane of the upstash pack: one API published as two OpenAPI documents
2
+ // (docs/contributing/architecture.md, "Other wires"). Upstash publishes qstash/openapi.yaml and workflow/openapi.yaml, both
3
+ // served at https://qstash-{region}.upstash.io with one key set and sharing endpoints; the lane vendors both as published
4
+ // (../spec/openapi/) and its surface is their union (./generated/surface.gen.json, by `bun scripts/derive-pack.ts
5
+ // upstash/qstash`). This manifest holds the vendor facts neither document carries, and the machines of the states the lane
6
+ // moves. The lane writes the pack's one store (`upstash`), beside the Redis keys and the console's records.
7
+ //
8
+ // A WORKFLOW IS QSTASH MESSAGES. "Upstash Workflow is built on top of Upstash QStash" and "Each step is executed in its
9
+ // own HTTP call to your application" (https://upstash.com/docs/workflow/basics/how): @upstash/workflow starts a run and
10
+ // sends each step through the QStash batch endpoint with its `Upstash-Workflow-*` headers, and QStash delivers each step
11
+ // to the app's workflow route as it delivers any message. So a run's steps are messages, moved by the message machine
12
+ // below, and the run has its own machine over them.
13
+ //
14
+ // NO `auth`: QStash reads its token as `Authorization: Bearer <QSTASH_TOKEN>` or as the `qstash_token` query parameter
15
+ // (both documents' `securitySchemes`: bearerAuth and bearerAuthQuery), and a token belongs to the account the console
16
+ // shows it to, which the kernel's gate cannot read; the lane's gate (semantics/account.ts) reads both.
17
+ //
18
+ // ERRORS: every refusal both documents give is the `Error` schema, `{"error": "<message>"}`; the documents name a
19
+ // refusal's status and cause ("Message not found") and never its message text. Where the documentation stops: each
20
+ // message below states the rule in the documents' words, not a text Upstash was seen to send.
21
+ import type { DerivedManifest, StateField } from '@volter/world-core';
22
+
23
+ /** The path parameters whose values hold slashes: a destination is a whole URL (`/v2/publish/https://example.com/x`). */
24
+ export const spanning = ['destination', 'workflowUrl'];
25
+
26
+ /** The hosts the lane serves: "https://qstash-{region}.upstash.io" with the regions us-east-1 and eu-central-1 (both
27
+ * documents' `servers`), and qstash.upstash.io, the host Upstash's pages send their examples to. An application pointed at
28
+ * the World through QSTASH_URL reaches the lane at the pack's own address instead. SHAPE judges the lane's life on these. */
29
+ export const hosts = [{ host: 'qstash-us-east-1.upstash.io' }, { host: 'qstash-eu-central-1.upstash.io' }, { host: 'qstash.upstash.io' }];
30
+
31
+ const LIFECYCLE = 'https://upstash.com/docs/qstash/howto/debug-logs';
32
+ const RETRY = 'https://upstash.com/docs/qstash/features/retry';
33
+ const CANCEL = 'spec:/documents/qstash/paths/~1v2~1messages~1{messageId}/delete "Cancel a pending message"';
34
+
35
+ /** A message's `state`: the state its log answers (the message's own view has no state field). "When a message is ready
36
+ * for execution, it will be become ACTIVE and a delivery to your API is attempted. If you API responds with a status code
37
+ * between 200 - 299, the task is considered successful and will be marked as DELIVERED. Otherwise the message is being
38
+ * retried if there are any retries left and moves to RETRY. If all retries are exhausted, the task has FAILED and the
39
+ * message will be moved to the DLQ." (debug-logs). A cancel logs CANCEL_REQUESTED, and "If retries are not exhausted
40
+ * yet, in the next deliver time, the message will be marked as CANCELLED" (the same page). A retry falls due on the World
41
+ * clock (`min(86400, e^(2.5n))` seconds, the retry page). ERROR is the log's record of a failed attempt, written beside
42
+ * the move to RETRY or FAILED: not a state the message rests in. Not made by the lane: a delivery in flight when a cancel
43
+ * arrives (ACTIVE → CANCEL_REQUESTED): the lane's attempt is over before any request is answered. */
44
+ const messageState: StateField = {
45
+ initial: 'CREATED',
46
+ transitions: [
47
+ { actor: 'time', from: ['CREATED'], to: 'ACTIVE', source: LIFECYCLE },
48
+ { actor: 'vendor', from: ['ACTIVE'], to: 'DELIVERED', source: LIFECYCLE },
49
+ { actor: 'vendor', from: ['ACTIVE'], to: 'RETRY', source: RETRY },
50
+ { actor: 'vendor', from: ['ACTIVE'], to: 'FAILED', source: LIFECYCLE },
51
+ { actor: 'time', from: ['RETRY'], to: 'ACTIVE', source: RETRY },
52
+ { operation: 'delete_v2_messages_messageid', from: ['CREATED', 'RETRY'], to: 'CANCEL_REQUESTED', refusal: { status: 404, message: 'Message not found.' }, source: CANCEL },
53
+ // a run canceled cancels the steps it still owes
54
+ { operation: 'delete_v2_workflows_runs', from: ['CREATED', 'RETRY'], to: 'CANCEL_REQUESTED', source: 'spec:/documents/workflow/paths/~1v2~1workflows~1runs/delete "Cancel all matching workflow runs."' },
55
+ { actor: 'time', from: ['CANCEL_REQUESTED'], to: 'CANCELLED', source: LIFECYCLE },
56
+ ],
57
+ };
58
+
59
+ const RUN_STATES = 'https://upstash.com/docs/workflow/basics/client/logs';
60
+ const RUN_CANCEL = 'spec:/documents/workflow/paths/~1v2~1workflows~1runs~1{workflowRunId}/delete "Cancel an ongoing workflow run."';
61
+ const RUNS_CANCEL = 'spec:/documents/workflow/paths/~1v2~1workflows~1runs/delete "Cancel all matching workflow runs."';
62
+
63
+ /** A workflow run's `workflowState`: "RUN_STARTED The workflow run is in progress. RUN_SUCCESS The workflow run completed
64
+ * successfully. RUN_FAILED The run failed after all retries. RUN_CANCELED The run was manually canceled." (the client's
65
+ * logs page). A run succeeds when its route function returns: `serve()` then reports it finished with
66
+ * `DELETE /v2/workflows/runs/{workflowRunId}?cancel=false` (the spec patch's `cancel`, from @upstash/workflow's source);
67
+ * `client.cancel` cancels through the bulk `DELETE /v2/workflows/runs?workflowRunIds=`. A run fails when a step's message
68
+ * is FAILED. A run in the DLQ is resumed as a new run with a new id ("A new workflow run ID is generated",
69
+ * https://upstash.com/docs/workflow/api-reference/dlq/resume-workflow-from-dlq), so the failed run never moves again. */
70
+ const runState: StateField = {
71
+ initial: 'RUN_STARTED',
72
+ transitions: [
73
+ { operation: 'delete_v2_workflows_runs_workflowrunid', from: ['RUN_STARTED'], to: 'RUN_SUCCESS', refusal: { status: 404, message: 'A workflow run is not found with the given id.' }, source: RUN_STATES },
74
+ { operation: 'delete_v2_workflows_runs_workflowrunid', from: ['RUN_STARTED'], to: 'RUN_CANCELED', refusal: { status: 404, message: 'A workflow run is not found with the given id.' }, source: RUN_CANCEL },
75
+ { operation: 'delete_v2_workflows_runs', from: ['RUN_STARTED'], to: 'RUN_CANCELED', source: RUNS_CANCEL },
76
+ { actor: 'vendor', from: ['RUN_STARTED'], to: 'RUN_FAILED', source: RUN_STATES },
77
+ ],
78
+ };
79
+
80
+ /** A schedule's `isPaused`: "When a schedule is paused, the cron trigger will simply be ignored. If the schedule is
81
+ * already paused, this action has no effect." (the spec's pause). The stay is declared first: an ask naming no target
82
+ * takes the first transition that allows it. Not made by the lane: resume (no customer of the life resumes a schedule;
83
+ * its operation is the gap). */
84
+ const SCHEDULE_PAUSE = 'spec:/documents/qstash/paths/~1v2~1schedules~1{scheduleId}~1pause/post "If the schedule is already paused, this action has no effect."';
85
+ const schedulePaused: StateField = {
86
+ initial: false,
87
+ transitions: [
88
+ { operation: 'post_v2_schedules_scheduleid_pause', from: ['true'], source: SCHEDULE_PAUSE },
89
+ { operation: 'post_v2_schedules_scheduleid_pause', from: ['false'], to: 'true', source: SCHEDULE_PAUSE },
90
+ // the clients' PATCH form of the same operation (the spec patch)
91
+ { operation: 'patch_v2_schedules_scheduleid_pause', from: ['true'], source: SCHEDULE_PAUSE },
92
+ { operation: 'patch_v2_schedules_scheduleid_pause', from: ['false'], to: 'true', source: SCHEDULE_PAUSE },
93
+ ],
94
+ };
95
+
96
+ export const manifest: DerivedManifest = {
97
+ vendor: 'upstash',
98
+ service: 'upstash',
99
+ body: {},
100
+ // QStash mints a message id `msg_…`, a schedule id `scd_…` and a workflow run id `wfr_…` (the pages' examples:
101
+ // msg_xxx, scd_xxx, wfr_abc123); the lane's handlers mint them, and a trigger may name its own run id
102
+ // (`workflowRunId`, https://upstash.com/docs/workflow/basics/client/trigger)
103
+ ids: { template: '{prefix}{n}', acceptProvided: true },
104
+ // createdAt, notBefore and a waiter's deadline are Unix times
105
+ time: 'unix',
106
+ error: { error: '{message}' },
107
+ readOnly: { status: 405, message: 'This twin is read-only: omit readOnly to accept writes.' },
108
+ malformedBody: { status: 400, message: 'The request body is not valid JSON.' },
109
+ notFound: { status: 404, message: 'Not found.' },
110
+ // every list is a handler's (the DLQ's `{messages, cursor}`, a bare array of queues); required by the type, nothing
111
+ // reads it
112
+ list: { style: 'envelope', envelope: { messages: '{data}', cursor: '{next_cursor}' }, limit: { param: 'count', default: 100, max: 100 }, cursor: { param: 'cursor', encoding: 'base64-offset' } },
113
+ deleted: {},
114
+ // the operations of a declared resource the lane does not serve: the gap
115
+ unmodeled: [
116
+ 'get_v2_dlq_dlqid', 'delete_v2_dlq_dlqid', 'get_v2_queues', 'get_v2_schedules', 'get_v2_schedules_scheduleid',
117
+ 'get_v2_waiters_eventid', 'get_v2_workflows_dlq_dlqid', 'delete_v2_workflows_dlq_dlqid', 'get_v2_workflows_logs',
118
+ ],
119
+ resources: {
120
+ // the message is not a resource of either document (GET /v2/messages/{messageId} answers `Message`, a view): its
121
+ // state is what its log answers
122
+ Message: { storedAs: 'qstash_message', idPrefix: 'msg_', state: { state: messageState } },
123
+ // a queue is made by its first enqueue or its upsert and holds its messages in order, paused or not
124
+ Queue: { storedAs: 'qstash_queue', idPrefix: '' },
125
+ Schedule: { storedAs: 'qstash_schedule', idPrefix: 'scd_', state: { isPaused: schedulePaused } },
126
+ // a URL group (a topic): its endpoints, keyed by its account and name
127
+ URLGroup: { storedAs: 'qstash_url_group', idPrefix: '' },
128
+ DLQMessage: { storedAs: 'qstash_dlq', idPrefix: '' },
129
+ WorkflowRun: { storedAs: 'workflow_run', idPrefix: 'wfr_', state: { workflowState: runState } },
130
+ WorkflowDLQMessage: { storedAs: 'workflow_dlq', idPrefix: '' },
131
+ // a waiter is present while a run waits on its event id, and gone when notified, timed out or canceled
132
+ Waiter: { storedAs: 'workflow_waiter', idPrefix: '' },
133
+ },
134
+ };
@@ -0,0 +1,98 @@
1
+ // A QStash account: its token and its two signing keys. Every Upstash account has QStash; the console's QStash page
2
+ // shows its `QSTASH_TOKEN` and its `QSTASH_CURRENT_SIGNING_KEY` and `QSTASH_NEXT_SIGNING_KEY` ("copy the `QSTASH_TOKEN`
3
+ // from the **Quickstart** section", https://upstash.com/docs/qstash/overall/getstarted) and resets the token ("Resetting
4
+ // your token will invalidate your current token and all future requests with the old token will be rejected",
5
+ // https://upstash.com/docs/qstash/howto/reset-token). Rolling the keys makes the next key the current one and a new next
6
+ // ("currentKey = nextKey / nextKey = generateNewKey()", https://upstash.com/docs/qstash/howto/roll-signing-keys).
7
+ //
8
+ // A person's account is their console email (the api lane's sign-in, ../../../api/src/screens/session.tsx). A token the
9
+ // console never showed is the World's own account's, as every token is for the pack root's Redis: an application pointed
10
+ // at the World (Dub) carries whatever token its configuration holds, and the World starts the twin with the signing keys
11
+ // it gave the application (QSTASH_CURRENT_SIGNING_KEY, QSTASH_NEXT_SIGNING_KEY).
12
+ //
13
+ // Where the documentation stops and the twin decides: a token has the form the local development page prints
14
+ // (base64 of `{"UserID":…,"Password":…}`) and a key the form it prints (`sig_` and 28 characters); both are derived from the
15
+ // account and how many times it was reset or rolled, so a World shows the same values every run. A request with no token
16
+ // answers 401 {"error": "Unauthorized"} (the documents' 401 is "Unauthorized"); a reset token answers the same.
17
+ import { createHash } from 'node:crypto';
18
+ import type { SemanticsContext } from '@volter/world-core';
19
+ import type { Row } from './shared.ts';
20
+
21
+ export const ACCOUNT = '_qstash_account';
22
+ /** The account a token no person was shown belongs to. */
23
+ export const WORLD_ACCOUNT = 'world';
24
+
25
+ const hex = (s: string): string => createHash('sha256').update(s).digest('hex');
26
+ const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
27
+
28
+ function tokenAt(owner: string, generation: number): string {
29
+ const h = hex(`qstash-token:${owner}:${generation}`);
30
+ const userId = `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-8${h.slice(17, 20)}-${h.slice(20, 32)}`;
31
+ return Buffer.from(JSON.stringify({ UserID: userId, Password: h.slice(32, 64) }), 'utf8').toString('base64');
32
+ }
33
+
34
+ function keyAt(owner: string, n: number): string {
35
+ if (owner === WORLD_ACCOUNT && n === 0 && process.env.QSTASH_CURRENT_SIGNING_KEY) return process.env.QSTASH_CURRENT_SIGNING_KEY;
36
+ if (owner === WORLD_ACCOUNT && n === 1 && process.env.QSTASH_NEXT_SIGNING_KEY) return process.env.QSTASH_NEXT_SIGNING_KEY;
37
+ const h = hex(`qstash-signing-key:${owner}:${n}`);
38
+ let out = '';
39
+ for (let i = 0; out.length < 28; i += 1) out += ALPHABET[parseInt(h.slice((i * 2) % 62, (i * 2) % 62 + 2), 16) % ALPHABET.length];
40
+ return `sig_${out}`;
41
+ }
42
+
43
+ /** How many times an account's token was reset and its keys rolled. */
44
+ function countsOf(ctx: SemanticsContext, owner: string): { resets: number; rolls: number } {
45
+ const row = ctx.rowsRaw(ACCOUNT).find((a) => a.id === owner) as Row | undefined;
46
+ return { resets: Number(row?.resets ?? 0), rolls: Number(row?.rolls ?? 0) };
47
+ }
48
+
49
+ /** An account's token and signing keys as they are now. */
50
+ export function accountOf(ctx: SemanticsContext, owner: string): { owner: string; token: string; current: string; next: string } {
51
+ const { resets, rolls } = countsOf(ctx, owner);
52
+ return { owner, token: tokenAt(owner, resets), current: keyAt(owner, rolls), next: keyAt(owner, rolls + 1) };
53
+ }
54
+
55
+ /** The console's Reset token: the old token is refused from now on. */
56
+ export async function resetToken(ctx: SemanticsContext, owner: string): Promise<string> {
57
+ const { resets, rolls } = countsOf(ctx, owner);
58
+ await ctx.record(ACCOUNT, { resets: resets + 1, rolls }, owner);
59
+ return tokenAt(owner, resets + 1);
60
+ }
61
+
62
+ /** Roll the signing keys: the next key becomes the current one, and a new next key is made. */
63
+ export async function rollKeys(ctx: SemanticsContext, owner: string): Promise<{ current: string; next: string }> {
64
+ const { resets, rolls } = countsOf(ctx, owner);
65
+ await ctx.record(ACCOUNT, { resets, rolls: rolls + 1 }, owner);
66
+ return { current: keyAt(owner, rolls + 1), next: keyAt(owner, rolls + 2) };
67
+ }
68
+
69
+ /** The token a request presents: `Authorization: Bearer <token>` or `?qstash_token=` (bearerAuth, bearerAuthQuery). */
70
+ export function tokenOf(request: Request): string | undefined {
71
+ const bearer = /^bearer\s+(\S+)\s*$/i.exec(request.headers.get('authorization') ?? '');
72
+ const query = new URL(request.url).searchParams.get('qstash_token');
73
+ return bearer?.[1] ?? (query && query.trim() !== '' ? query.trim() : undefined);
74
+ }
75
+
76
+ /** The account a token names: a person's current token names their account; a token a person was shown before a reset
77
+ * is refused; any other is the World's own account's. */
78
+ export function ownerOfToken(ctx: SemanticsContext, token: string): string | 'refused' {
79
+ const people = ctx.rowsRaw('_person').map((p) => ({ email: String(p.email), resets: countsOf(ctx, String(p.email)).resets }));
80
+ const current = people.find((p) => token === tokenAt(p.email, p.resets));
81
+ if (current) return current.email;
82
+ return people.some((p) => resetAway(p, token)) ? 'refused' : WORLD_ACCOUNT;
83
+ }
84
+
85
+ /** Whether a token is one a person held before they reset it: "Resetting your token will invalidate your current token and
86
+ * all future requests with the old token will be rejected." (https://upstash.com/docs/qstash/howto/reset-token) */
87
+ function resetAway(p: { email: string; resets: number }, token: string): boolean {
88
+ for (let g = 0; g < p.resets; g += 1) if (token === tokenAt(p.email, g)) return true;
89
+ return false;
90
+ }
91
+
92
+ /** The lane's gate: the account a request acts as, or the refusal it answers. */
93
+ export function gate(ctx: SemanticsContext, request: Request): { owner: string } | { refused: Response } {
94
+ const token = tokenOf(request);
95
+ const owner = token ? ownerOfToken(ctx, token) : 'refused';
96
+ if (owner === 'refused') return { refused: Response.json({ error: 'Unauthorized' }, { status: 401 }) };
97
+ return { owner };
98
+ }