@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.
- package/LICENSE +202 -0
- package/README.md +202 -0
- package/api/src/fetch.ts +54 -0
- package/api/src/generated/surface.gen.json +1 -0
- package/api/src/generated/ui.gen.json +1 -0
- package/api/src/index.ts +19 -0
- package/api/src/key-gate.ts +30 -0
- package/api/src/manifest.ts +103 -0
- package/api/src/screens/developer-api.tsx +106 -0
- package/api/src/screens/qstash.tsx +99 -0
- package/api/src/screens/session.tsx +125 -0
- package/api/src/screens/teams.tsx +114 -0
- package/api/src/semantics/backups.ts +90 -0
- package/api/src/semantics/index.ts +191 -0
- package/api/src/semantics/shared.ts +42 -0
- package/api/src/semantics/teams.ts +108 -0
- package/api/src/semantics/time.ts +40 -0
- package/dist/api/src/fetch.d.ts +15 -0
- package/dist/api/src/fetch.js +44 -0
- package/dist/api/src/fetch.ts +54 -0
- package/dist/api/src/generated/surface.gen.json +1 -0
- package/dist/api/src/generated/ui.gen.json +1 -0
- package/dist/api/src/index.ts +19 -0
- package/dist/api/src/key-gate.d.ts +3 -0
- package/dist/api/src/key-gate.js +30 -0
- package/dist/api/src/key-gate.ts +30 -0
- package/dist/api/src/manifest.d.ts +2 -0
- package/dist/api/src/manifest.js +81 -0
- package/dist/api/src/manifest.ts +103 -0
- package/dist/api/src/screens/developer-api.d.ts +3 -0
- package/dist/api/src/screens/developer-api.js +101 -0
- package/dist/api/src/screens/developer-api.tsx +106 -0
- package/dist/api/src/screens/qstash.d.ts +3 -0
- package/dist/api/src/screens/qstash.js +92 -0
- package/dist/api/src/screens/qstash.tsx +99 -0
- package/dist/api/src/screens/session.d.ts +9 -0
- package/dist/api/src/screens/session.js +118 -0
- package/dist/api/src/screens/session.tsx +125 -0
- package/dist/api/src/screens/teams.d.ts +3 -0
- package/dist/api/src/screens/teams.js +99 -0
- package/dist/api/src/screens/teams.tsx +114 -0
- package/dist/api/src/semantics/backups.d.ts +7 -0
- package/dist/api/src/semantics/backups.js +75 -0
- package/dist/api/src/semantics/backups.ts +90 -0
- package/dist/api/src/semantics/index.d.ts +10 -0
- package/dist/api/src/semantics/index.js +191 -0
- package/dist/api/src/semantics/index.ts +191 -0
- package/dist/api/src/semantics/shared.d.ts +21 -0
- package/dist/api/src/semantics/shared.js +34 -0
- package/dist/api/src/semantics/shared.ts +42 -0
- package/dist/api/src/semantics/teams.d.ts +13 -0
- package/dist/api/src/semantics/teams.js +100 -0
- package/dist/api/src/semantics/teams.ts +108 -0
- package/dist/api/src/semantics/time.d.ts +2 -0
- package/dist/api/src/semantics/time.js +34 -0
- package/dist/api/src/semantics/time.ts +40 -0
- package/dist/qstash/src/doors.d.ts +6 -0
- package/dist/qstash/src/doors.js +33 -0
- package/dist/qstash/src/doors.ts +51 -0
- package/dist/qstash/src/egress.d.ts +7 -0
- package/dist/qstash/src/egress.js +66 -0
- package/dist/qstash/src/egress.ts +58 -0
- package/dist/qstash/src/fetch.d.ts +7 -0
- package/dist/qstash/src/fetch.js +48 -0
- package/dist/qstash/src/fetch.ts +46 -0
- package/dist/qstash/src/generated/surface.gen.json +1 -0
- package/dist/qstash/src/generated/ui.gen.json +1 -0
- package/dist/qstash/src/index.ts +35 -0
- package/dist/qstash/src/manifest.d.ts +10 -0
- package/dist/qstash/src/manifest.js +105 -0
- package/dist/qstash/src/manifest.ts +134 -0
- package/dist/qstash/src/semantics/account.d.ts +29 -0
- package/dist/qstash/src/semantics/account.js +91 -0
- package/dist/qstash/src/semantics/account.ts +98 -0
- package/dist/qstash/src/semantics/delivery.d.ts +17 -0
- package/dist/qstash/src/semantics/delivery.js +274 -0
- package/dist/qstash/src/semantics/delivery.ts +264 -0
- package/dist/qstash/src/semantics/dlq.d.ts +4 -0
- package/dist/qstash/src/semantics/dlq.js +51 -0
- package/dist/qstash/src/semantics/dlq.ts +61 -0
- package/dist/qstash/src/semantics/index.d.ts +2 -0
- package/dist/qstash/src/semantics/index.js +10 -0
- package/dist/qstash/src/semantics/index.ts +13 -0
- package/dist/qstash/src/semantics/keys.d.ts +2 -0
- package/dist/qstash/src/semantics/keys.js +9 -0
- package/dist/qstash/src/semantics/keys.ts +14 -0
- package/dist/qstash/src/semantics/messages.d.ts +74 -0
- package/dist/qstash/src/semantics/messages.js +233 -0
- package/dist/qstash/src/semantics/messages.ts +249 -0
- package/dist/qstash/src/semantics/queues.d.ts +2 -0
- package/dist/qstash/src/semantics/queues.js +60 -0
- package/dist/qstash/src/semantics/queues.ts +66 -0
- package/dist/qstash/src/semantics/schedules.d.ts +19 -0
- package/dist/qstash/src/semantics/schedules.js +125 -0
- package/dist/qstash/src/semantics/schedules.ts +132 -0
- package/dist/qstash/src/semantics/shared.d.ts +45 -0
- package/dist/qstash/src/semantics/shared.js +115 -0
- package/dist/qstash/src/semantics/shared.ts +121 -0
- package/dist/qstash/src/semantics/urlgroups.d.ts +2 -0
- package/dist/qstash/src/semantics/urlgroups.js +58 -0
- package/dist/qstash/src/semantics/urlgroups.ts +69 -0
- package/dist/qstash/src/semantics/workflows.d.ts +44 -0
- package/dist/qstash/src/semantics/workflows.js +379 -0
- package/dist/qstash/src/semantics/workflows.ts +401 -0
- package/dist/qstash/src/signing.d.ts +4 -0
- package/dist/qstash/src/signing.js +16 -0
- package/dist/qstash/src/signing.ts +19 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +35 -0
- package/dist/src/generated/surface.gen.json +1 -0
- package/dist/src/index.d.ts +18 -0
- package/dist/src/index.js +124 -0
- package/dist/src/manifest.d.ts +14 -0
- package/dist/src/manifest.js +8 -0
- package/dist/src/upstash-budget.d.ts +85 -0
- package/dist/src/upstash-budget.js +440 -0
- package/dist/src/upstash-capabilities.d.ts +4 -0
- package/dist/src/upstash-capabilities.js +1286 -0
- package/dist/src/upstash-conformance.d.ts +7 -0
- package/dist/src/upstash-conformance.js +119 -0
- package/dist/src/upstash-connector.d.ts +115 -0
- package/dist/src/upstash-connector.js +309 -0
- package/dist/src/upstash-lua.d.ts +140 -0
- package/dist/src/upstash-lua.js +1229 -0
- package/dist/src/upstash-server.d.ts +29 -0
- package/dist/src/upstash-server.js +81 -0
- package/dist/src/upstash-store.d.ts +114 -0
- package/dist/src/upstash-store.js +1663 -0
- package/dist/src/upstash-twin.d.ts +73 -0
- package/dist/src/upstash-twin.js +437 -0
- package/package.json +59 -0
- package/qstash/src/doors.ts +51 -0
- package/qstash/src/egress.ts +58 -0
- package/qstash/src/fetch.ts +46 -0
- package/qstash/src/generated/surface.gen.json +1 -0
- package/qstash/src/generated/ui.gen.json +1 -0
- package/qstash/src/index.ts +35 -0
- package/qstash/src/manifest.ts +134 -0
- package/qstash/src/semantics/account.ts +98 -0
- package/qstash/src/semantics/delivery.ts +264 -0
- package/qstash/src/semantics/dlq.ts +61 -0
- package/qstash/src/semantics/index.ts +13 -0
- package/qstash/src/semantics/keys.ts +14 -0
- package/qstash/src/semantics/messages.ts +249 -0
- package/qstash/src/semantics/queues.ts +66 -0
- package/qstash/src/semantics/schedules.ts +132 -0
- package/qstash/src/semantics/shared.ts +121 -0
- package/qstash/src/semantics/urlgroups.ts +69 -0
- package/qstash/src/semantics/workflows.ts +401 -0
- package/qstash/src/signing.ts +19 -0
- package/src/cli.ts +36 -0
- package/src/generated/surface.gen.json +1 -0
- package/src/index.ts +203 -0
- package/src/manifest.ts +26 -0
- package/src/upstash-budget.ts +486 -0
- package/src/upstash-capabilities.ts +1418 -0
- package/src/upstash-conformance.ts +131 -0
- package/src/upstash-connector.ts +340 -0
- package/src/upstash-lua.ts +1120 -0
- package/src/upstash-server.ts +103 -0
- package/src/upstash-store.ts +1437 -0
- 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
|
+
}
|