@yolk-sdk/emulators 0.1.0-canary.96
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 +21 -0
- package/README.md +2135 -0
- package/dist/anthropic.d.mts +269 -0
- package/dist/anthropic.d.mts.map +1 -0
- package/dist/anthropic.mjs +177 -0
- package/dist/anthropic.mjs.map +1 -0
- package/dist/chat-completions.d.mts +288 -0
- package/dist/chat-completions.d.mts.map +1 -0
- package/dist/chat-completions.mjs +451 -0
- package/dist/chat-completions.mjs.map +1 -0
- package/dist/codex.d.mts +265 -0
- package/dist/codex.d.mts.map +1 -0
- package/dist/codex.mjs +199 -0
- package/dist/codex.mjs.map +1 -0
- package/dist/dropbox/api.d.mts +63 -0
- package/dist/dropbox/api.d.mts.map +1 -0
- package/dist/dropbox/api.mjs +565 -0
- package/dist/dropbox/api.mjs.map +1 -0
- package/dist/dropbox/state.d.mts +136 -0
- package/dist/dropbox/state.d.mts.map +1 -0
- package/dist/dropbox/state.mjs +209 -0
- package/dist/dropbox/state.mjs.map +1 -0
- package/dist/dropbox.d.mts +79 -0
- package/dist/dropbox.d.mts.map +1 -0
- package/dist/dropbox.mjs +124 -0
- package/dist/dropbox.mjs.map +1 -0
- package/dist/email-fixtures.d.mts +36 -0
- package/dist/email-fixtures.d.mts.map +1 -0
- package/dist/email-fixtures.mjs +1080 -0
- package/dist/email-fixtures.mjs.map +1 -0
- package/dist/email.d.mts +160 -0
- package/dist/email.d.mts.map +1 -0
- package/dist/email.mjs +608 -0
- package/dist/email.mjs.map +1 -0
- package/dist/emulator-compose.d.mts +40 -0
- package/dist/emulator-compose.d.mts.map +1 -0
- package/dist/emulator-compose.mjs +91 -0
- package/dist/emulator-compose.mjs.map +1 -0
- package/dist/emulator-http.d.mts +53 -0
- package/dist/emulator-http.d.mts.map +1 -0
- package/dist/emulator-http.mjs +117 -0
- package/dist/emulator-http.mjs.map +1 -0
- package/dist/emulator-kernel.d.mts +176 -0
- package/dist/emulator-kernel.d.mts.map +1 -0
- package/dist/emulator-kernel.mjs +413 -0
- package/dist/emulator-kernel.mjs.map +1 -0
- package/dist/fixture-route.d.mts +129 -0
- package/dist/fixture-route.d.mts.map +1 -0
- package/dist/fixture-route.mjs +257 -0
- package/dist/fixture-route.mjs.map +1 -0
- package/dist/fortnox/api.d.mts +92 -0
- package/dist/fortnox/api.d.mts.map +1 -0
- package/dist/fortnox/api.mjs +750 -0
- package/dist/fortnox/api.mjs.map +1 -0
- package/dist/fortnox/state.d.mts +533 -0
- package/dist/fortnox/state.d.mts.map +1 -0
- package/dist/fortnox/state.mjs +612 -0
- package/dist/fortnox/state.mjs.map +1 -0
- package/dist/fortnox.d.mts +128 -0
- package/dist/fortnox.d.mts.map +1 -0
- package/dist/fortnox.mjs +403 -0
- package/dist/fortnox.mjs.map +1 -0
- package/dist/gateway-evaluate-recordings.d.mts +12 -0
- package/dist/gateway-evaluate-recordings.d.mts.map +1 -0
- package/dist/gateway-evaluate-recordings.mjs +128 -0
- package/dist/gateway-evaluate-recordings.mjs.map +1 -0
- package/dist/gateway.d.mts +272 -0
- package/dist/gateway.d.mts.map +1 -0
- package/dist/gateway.mjs +400 -0
- package/dist/gateway.mjs.map +1 -0
- package/dist/github/api.d.mts +63 -0
- package/dist/github/api.d.mts.map +1 -0
- package/dist/github/api.mjs +527 -0
- package/dist/github/api.mjs.map +1 -0
- package/dist/github/state.d.mts +272 -0
- package/dist/github/state.d.mts.map +1 -0
- package/dist/github/state.mjs +303 -0
- package/dist/github/state.mjs.map +1 -0
- package/dist/github.d.mts +77 -0
- package/dist/github.d.mts.map +1 -0
- package/dist/github.mjs +180 -0
- package/dist/github.mjs.map +1 -0
- package/dist/google/calendar.d.mts +10 -0
- package/dist/google/calendar.d.mts.map +1 -0
- package/dist/google/calendar.mjs +252 -0
- package/dist/google/calendar.mjs.map +1 -0
- package/dist/google/drive.d.mts +8 -0
- package/dist/google/drive.d.mts.map +1 -0
- package/dist/google/drive.mjs +252 -0
- package/dist/google/drive.mjs.map +1 -0
- package/dist/google/gmail.d.mts +10 -0
- package/dist/google/gmail.d.mts.map +1 -0
- package/dist/google/gmail.mjs +591 -0
- package/dist/google/gmail.mjs.map +1 -0
- package/dist/google/shared.d.mts +107 -0
- package/dist/google/shared.d.mts.map +1 -0
- package/dist/google/shared.mjs +146 -0
- package/dist/google/shared.mjs.map +1 -0
- package/dist/google/state.d.mts +385 -0
- package/dist/google/state.d.mts.map +1 -0
- package/dist/google/state.mjs +496 -0
- package/dist/google/state.mjs.map +1 -0
- package/dist/google.d.mts +75 -0
- package/dist/google.d.mts.map +1 -0
- package/dist/google.mjs +210 -0
- package/dist/google.mjs.map +1 -0
- package/dist/linkedin-search/api.d.mts +45 -0
- package/dist/linkedin-search/api.d.mts.map +1 -0
- package/dist/linkedin-search/api.mjs +181 -0
- package/dist/linkedin-search/api.mjs.map +1 -0
- package/dist/linkedin-search/state.d.mts +123 -0
- package/dist/linkedin-search/state.d.mts.map +1 -0
- package/dist/linkedin-search/state.mjs +261 -0
- package/dist/linkedin-search/state.mjs.map +1 -0
- package/dist/linkedin-search.d.mts +69 -0
- package/dist/linkedin-search.d.mts.map +1 -0
- package/dist/linkedin-search.mjs +158 -0
- package/dist/linkedin-search.mjs.map +1 -0
- package/dist/mcp/api.d.mts +44 -0
- package/dist/mcp/api.d.mts.map +1 -0
- package/dist/mcp/api.mjs +557 -0
- package/dist/mcp/api.mjs.map +1 -0
- package/dist/mcp/recordings.d.mts +40 -0
- package/dist/mcp/recordings.d.mts.map +1 -0
- package/dist/mcp/recordings.mjs +2390 -0
- package/dist/mcp/recordings.mjs.map +1 -0
- package/dist/mcp/state.d.mts +71 -0
- package/dist/mcp/state.d.mts.map +1 -0
- package/dist/mcp/state.mjs +83 -0
- package/dist/mcp/state.mjs.map +1 -0
- package/dist/mcp.d.mts +84 -0
- package/dist/mcp.d.mts.map +1 -0
- package/dist/mcp.mjs +241 -0
- package/dist/mcp.mjs.map +1 -0
- package/dist/messages.d.mts +183 -0
- package/dist/messages.d.mts.map +1 -0
- package/dist/messages.mjs +532 -0
- package/dist/messages.mjs.map +1 -0
- package/dist/microsoft/api.d.mts +47 -0
- package/dist/microsoft/api.d.mts.map +1 -0
- package/dist/microsoft/api.mjs +178 -0
- package/dist/microsoft/api.mjs.map +1 -0
- package/dist/microsoft/calendar.d.mts +30 -0
- package/dist/microsoft/calendar.d.mts.map +1 -0
- package/dist/microsoft/calendar.mjs +271 -0
- package/dist/microsoft/calendar.mjs.map +1 -0
- package/dist/microsoft/drive.d.mts +36 -0
- package/dist/microsoft/drive.d.mts.map +1 -0
- package/dist/microsoft/drive.mjs +298 -0
- package/dist/microsoft/drive.mjs.map +1 -0
- package/dist/microsoft/graph.d.mts +198 -0
- package/dist/microsoft/graph.d.mts.map +1 -0
- package/dist/microsoft/graph.mjs +264 -0
- package/dist/microsoft/graph.mjs.map +1 -0
- package/dist/microsoft/mail.d.mts +48 -0
- package/dist/microsoft/mail.d.mts.map +1 -0
- package/dist/microsoft/mail.mjs +488 -0
- package/dist/microsoft/mail.mjs.map +1 -0
- package/dist/microsoft/state.d.mts +480 -0
- package/dist/microsoft/state.d.mts.map +1 -0
- package/dist/microsoft/state.mjs +625 -0
- package/dist/microsoft/state.mjs.map +1 -0
- package/dist/microsoft.d.mts +168 -0
- package/dist/microsoft.d.mts.map +1 -0
- package/dist/microsoft.mjs +483 -0
- package/dist/microsoft.mjs.map +1 -0
- package/dist/node.d.mts +47 -0
- package/dist/node.d.mts.map +1 -0
- package/dist/node.mjs +178 -0
- package/dist/node.mjs.map +1 -0
- package/dist/notion/api.d.mts +51 -0
- package/dist/notion/api.d.mts.map +1 -0
- package/dist/notion/api.mjs +507 -0
- package/dist/notion/api.mjs.map +1 -0
- package/dist/notion/state.d.mts +329 -0
- package/dist/notion/state.d.mts.map +1 -0
- package/dist/notion/state.mjs +432 -0
- package/dist/notion/state.mjs.map +1 -0
- package/dist/notion.d.mts +72 -0
- package/dist/notion.d.mts.map +1 -0
- package/dist/notion.mjs +127 -0
- package/dist/notion.mjs.map +1 -0
- package/dist/openai.d.mts +165 -0
- package/dist/openai.d.mts.map +1 -0
- package/dist/openai.mjs +141 -0
- package/dist/openai.mjs.map +1 -0
- package/dist/opencode-recordings.d.mts +7 -0
- package/dist/opencode-recordings.d.mts.map +1 -0
- package/dist/opencode-recordings.mjs +222 -0
- package/dist/opencode-recordings.mjs.map +1 -0
- package/dist/opencode.d.mts +70 -0
- package/dist/opencode.d.mts.map +1 -0
- package/dist/opencode.mjs +217 -0
- package/dist/opencode.mjs.map +1 -0
- package/dist/r2-fixtures.d.mts +36 -0
- package/dist/r2-fixtures.d.mts.map +1 -0
- package/dist/r2-fixtures.mjs +245 -0
- package/dist/r2-fixtures.mjs.map +1 -0
- package/dist/r2-guard.d.mts +43 -0
- package/dist/r2-guard.d.mts.map +1 -0
- package/dist/r2-guard.mjs +215 -0
- package/dist/r2-guard.mjs.map +1 -0
- package/dist/r2.d.mts +166 -0
- package/dist/r2.d.mts.map +1 -0
- package/dist/r2.mjs +517 -0
- package/dist/r2.mjs.map +1 -0
- package/dist/responses.d.mts +231 -0
- package/dist/responses.d.mts.map +1 -0
- package/dist/responses.mjs +556 -0
- package/dist/responses.mjs.map +1 -0
- package/dist/route-evidence.d.mts +45 -0
- package/dist/route-evidence.d.mts.map +1 -0
- package/dist/route-evidence.mjs +56 -0
- package/dist/route-evidence.mjs.map +1 -0
- package/dist/router.d.mts +93 -0
- package/dist/router.d.mts.map +1 -0
- package/dist/router.mjs +259 -0
- package/dist/router.mjs.map +1 -0
- package/dist/stateful-core.d.mts +17 -0
- package/dist/stateful-core.d.mts.map +1 -0
- package/dist/stateful-core.mjs +41 -0
- package/dist/stateful-core.mjs.map +1 -0
- package/dist/stateful-emulator.d.mts +565 -0
- package/dist/stateful-emulator.d.mts.map +1 -0
- package/dist/stateful-emulator.mjs +1228 -0
- package/dist/stateful-emulator.mjs.map +1 -0
- package/dist/stateful-secrets.d.mts +84 -0
- package/dist/stateful-secrets.d.mts.map +1 -0
- package/dist/stateful-secrets.mjs +216 -0
- package/dist/stateful-secrets.mjs.map +1 -0
- package/dist/subscription-usage-recordings.d.mts +9 -0
- package/dist/subscription-usage-recordings.d.mts.map +1 -0
- package/dist/subscription-usage-recordings.mjs +53 -0
- package/dist/subscription-usage-recordings.mjs.map +1 -0
- package/dist/subscription-usage.d.mts +53 -0
- package/dist/subscription-usage.d.mts.map +1 -0
- package/dist/subscription-usage.mjs +21 -0
- package/dist/subscription-usage.mjs.map +1 -0
- package/dist/telegram/api.d.mts +34 -0
- package/dist/telegram/api.d.mts.map +1 -0
- package/dist/telegram/api.mjs +257 -0
- package/dist/telegram/api.mjs.map +1 -0
- package/dist/telegram/state.d.mts +128 -0
- package/dist/telegram/state.d.mts.map +1 -0
- package/dist/telegram/state.mjs +154 -0
- package/dist/telegram/state.mjs.map +1 -0
- package/dist/telegram.d.mts +62 -0
- package/dist/telegram.d.mts.map +1 -0
- package/dist/telegram.mjs +127 -0
- package/dist/telegram.mjs.map +1 -0
- package/dist/todoist/api.d.mts +55 -0
- package/dist/todoist/api.d.mts.map +1 -0
- package/dist/todoist/api.mjs +415 -0
- package/dist/todoist/api.mjs.map +1 -0
- package/dist/todoist/state.d.mts +275 -0
- package/dist/todoist/state.d.mts.map +1 -0
- package/dist/todoist/state.mjs +345 -0
- package/dist/todoist/state.mjs.map +1 -0
- package/dist/todoist.d.mts +68 -0
- package/dist/todoist.d.mts.map +1 -0
- package/dist/todoist.mjs +181 -0
- package/dist/todoist.mjs.map +1 -0
- package/dist/xai.d.mts +267 -0
- package/dist/xai.d.mts.map +1 -0
- package/dist/xai.mjs +252 -0
- package/dist/xai.mjs.map +1 -0
- package/package.json +157 -0
- package/src/anthropic.ts +289 -0
- package/src/chat-completions.ts +924 -0
- package/src/codex.ts +296 -0
- package/src/dropbox/api.ts +1084 -0
- package/src/dropbox/state.ts +364 -0
- package/src/dropbox.ts +203 -0
- package/src/email-fixtures.ts +1297 -0
- package/src/email.ts +1108 -0
- package/src/emulator-compose.ts +147 -0
- package/src/emulator-http.ts +184 -0
- package/src/emulator-kernel.ts +844 -0
- package/src/fixture-route.ts +561 -0
- package/src/fortnox/api.ts +1352 -0
- package/src/fortnox/state.ts +798 -0
- package/src/fortnox.ts +801 -0
- package/src/gateway-evaluate-recordings.ts +153 -0
- package/src/gateway.ts +530 -0
- package/src/github/api.ts +986 -0
- package/src/github/state.ts +439 -0
- package/src/github.ts +271 -0
- package/src/google/calendar.ts +486 -0
- package/src/google/drive.ts +471 -0
- package/src/google/gmail.ts +1011 -0
- package/src/google/shared.ts +321 -0
- package/src/google/state.ts +686 -0
- package/src/google.ts +298 -0
- package/src/linkedin-search/api.ts +363 -0
- package/src/linkedin-search/state.ts +373 -0
- package/src/linkedin-search.ts +241 -0
- package/src/mcp/api.ts +995 -0
- package/src/mcp/recordings.ts +2665 -0
- package/src/mcp/state.ts +125 -0
- package/src/mcp.ts +319 -0
- package/src/messages.ts +844 -0
- package/src/microsoft/api.ts +426 -0
- package/src/microsoft/calendar.ts +491 -0
- package/src/microsoft/drive.ts +541 -0
- package/src/microsoft/graph.ts +550 -0
- package/src/microsoft/mail.ts +833 -0
- package/src/microsoft/state.ts +822 -0
- package/src/microsoft.ts +982 -0
- package/src/node.ts +293 -0
- package/src/notion/api.ts +976 -0
- package/src/notion/state.ts +512 -0
- package/src/notion.ts +210 -0
- package/src/openai.ts +198 -0
- package/src/opencode-recordings.ts +257 -0
- package/src/opencode.ts +271 -0
- package/src/r2-fixtures.ts +295 -0
- package/src/r2-guard.ts +323 -0
- package/src/r2.ts +890 -0
- package/src/responses.ts +901 -0
- package/src/route-evidence.ts +90 -0
- package/src/router.ts +490 -0
- package/src/stateful-core.ts +70 -0
- package/src/stateful-emulator.ts +2571 -0
- package/src/stateful-secrets.ts +299 -0
- package/src/subscription-usage-recordings.ts +77 -0
- package/src/subscription-usage.ts +68 -0
- package/src/telegram/api.ts +437 -0
- package/src/telegram/state.ts +229 -0
- package/src/telegram.ts +227 -0
- package/src/todoist/api.ts +736 -0
- package/src/todoist/state.ts +456 -0
- package/src/todoist.ts +316 -0
- package/src/xai.ts +341 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { isNotEmulated, notEmulated, statefulRoute } from "../stateful-emulator.mjs";
|
|
2
|
+
import { isTodoistMintedId, makeTodoistProject, makeTodoistTask, todoistIdPattern, todoistMintedProjectPrefix, todoistMintedTaskPrefix } from "./state.mjs";
|
|
3
|
+
import { Predicate } from "effect";
|
|
4
|
+
//#region src/todoist/api.ts
|
|
5
|
+
/**
|
|
6
|
+
* Todoist emulator API: the route table (evidence, query allowlist, handler) and route matching
|
|
7
|
+
* (internal; re-exported by `src/todoist.ts`, which runs the table on the shared stateful wrapper,
|
|
8
|
+
* `src/stateful-emulator.ts`, in its resolved mode).
|
|
9
|
+
*
|
|
10
|
+
* Only the routes the seven Todoist conformance cases (and their cleanup) need are emulated, with
|
|
11
|
+
* wire shapes copied from the synthetic fixtures; everything else answers the wrapper's ledgered
|
|
12
|
+
* 400 not-emulated. Each route lists the cases whose claims it follows. The project listing that
|
|
13
|
+
* the read-only leftover lookup (`findTodoistConformanceLeftovers`) sends has no fixture, so it is
|
|
14
|
+
* not emulated: the lookup fails, and runners print their lookup-failed WARN.
|
|
15
|
+
*
|
|
16
|
+
* @experimental
|
|
17
|
+
*/
|
|
18
|
+
/** API v1 prefix of every Todoist route. */
|
|
19
|
+
const todoistEmulatorBasePath = "/api/v1";
|
|
20
|
+
const pagingCase = "todoist.tasks.list-cursor-paging";
|
|
21
|
+
const notFoundCase = "todoist.errors.not-found-envelope";
|
|
22
|
+
const labelsCase = "todoist.labels.task-labels-are-names";
|
|
23
|
+
const lifecycleCase = "todoist.tasks.lifecycle-close";
|
|
24
|
+
const dueCase = "todoist.tasks.due-dates";
|
|
25
|
+
const parentCase = "todoist.projects.parent-id";
|
|
26
|
+
const deleteCase = "todoist.projects.delete-then-not-found";
|
|
27
|
+
/** Every write case creates, reads, and deletes its own project. */
|
|
28
|
+
const projectCases = [
|
|
29
|
+
lifecycleCase,
|
|
30
|
+
dueCase,
|
|
31
|
+
parentCase,
|
|
32
|
+
deleteCase
|
|
33
|
+
];
|
|
34
|
+
/** The task listing page size the fixtures send (the only one emulated), and the largest
|
|
35
|
+
* unlimited listing they record. */
|
|
36
|
+
const taskLimit = 2;
|
|
37
|
+
/** The `limit` the label listing fixture sends (the largest one emulated). */
|
|
38
|
+
const maxListLimit = 200;
|
|
39
|
+
/** The only due values the due-dates fixture records, with the due objects it answers. */
|
|
40
|
+
const recordedDueDate = "2030-01-15";
|
|
41
|
+
const recordedDueDatetime = "2030-01-15T12:00:00Z";
|
|
42
|
+
const dueOfDate = {
|
|
43
|
+
date: recordedDueDate,
|
|
44
|
+
timezone: null,
|
|
45
|
+
string: "Jan 15 2030",
|
|
46
|
+
lang: "en",
|
|
47
|
+
is_recurring: false
|
|
48
|
+
};
|
|
49
|
+
const dueOfDatetime = {
|
|
50
|
+
date: recordedDueDatetime,
|
|
51
|
+
timezone: "UTC",
|
|
52
|
+
string: "Jan 15 2030 12:00",
|
|
53
|
+
lang: "en",
|
|
54
|
+
is_recurring: false
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Case project names: `yolk-conformance-<runId>-<suffix>` with a conformance run id (`run-` then
|
|
58
|
+
* lower-case letters, digits, and inner hyphens, at most 40 characters) and a recorded suffix.
|
|
59
|
+
*/
|
|
60
|
+
const caseProjectNamePattern = /^yolk-conformance-(run-[a-z0-9]+(?:-[a-z0-9]+)*)-(lifecycle|due|parent|delete)$/;
|
|
61
|
+
const json = (status, body) => new Response(JSON.stringify(body), {
|
|
62
|
+
status,
|
|
63
|
+
headers: { "content-type": "application/json" }
|
|
64
|
+
});
|
|
65
|
+
const noContent = () => new Response(null, { status: 204 });
|
|
66
|
+
const padded = (value, width) => String(value).padStart(width, "0");
|
|
67
|
+
/** An API v1 timestamp (`2026-09-30T12:00:00.000000Z`) from the clock; throws on a bad clock. */
|
|
68
|
+
const timestamp = (env) => {
|
|
69
|
+
const value = env.now();
|
|
70
|
+
if (!Number.isFinite(value)) throw new Error("the emulator clock is not a finite instant");
|
|
71
|
+
return `${new Date(value).toISOString().slice(0, 23)}000Z`;
|
|
72
|
+
};
|
|
73
|
+
/** The 404 error body of the fixtures, with the next synthetic `event_id`. */
|
|
74
|
+
const notFound = (state, env, error) => {
|
|
75
|
+
const number = state.counters.nextEventNumber;
|
|
76
|
+
state.counters = {
|
|
77
|
+
...state.counters,
|
|
78
|
+
nextEventNumber: number + 1
|
|
79
|
+
};
|
|
80
|
+
const rest = {
|
|
81
|
+
error_code: 478,
|
|
82
|
+
error_extra: { event_id: padded(number, 32) },
|
|
83
|
+
error_tag: "NOT_FOUND",
|
|
84
|
+
http_code: 404
|
|
85
|
+
};
|
|
86
|
+
return json(404, env.drills.notFoundWithoutError ? rest : {
|
|
87
|
+
error,
|
|
88
|
+
...rest
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
const projectWire = (project) => makeTodoistProject({
|
|
92
|
+
id: project.id,
|
|
93
|
+
name: project.name,
|
|
94
|
+
parentId: project.parent_id,
|
|
95
|
+
childOrder: project.child_order,
|
|
96
|
+
createdAt: project.created_at,
|
|
97
|
+
updatedAt: project.updated_at
|
|
98
|
+
});
|
|
99
|
+
const taskWire = (state, env, task) => ({
|
|
100
|
+
id: task.id,
|
|
101
|
+
user_id: task.user_id,
|
|
102
|
+
project_id: task.project_id,
|
|
103
|
+
section_id: task.section_id,
|
|
104
|
+
parent_id: task.parent_id,
|
|
105
|
+
added_by_uid: task.added_by_uid,
|
|
106
|
+
assigned_by_uid: task.assigned_by_uid,
|
|
107
|
+
responsible_uid: task.responsible_uid,
|
|
108
|
+
labels: env.drills.taskLabelsAsIds ? task.labels.map((name) => state.labels.find((item) => item.name === name)?.id ?? name) : task.labels,
|
|
109
|
+
deadline: task.deadline,
|
|
110
|
+
duration: task.duration,
|
|
111
|
+
checked: task.checked,
|
|
112
|
+
is_deleted: task.is_deleted,
|
|
113
|
+
added_at: task.added_at,
|
|
114
|
+
completed_at: task.completed_at,
|
|
115
|
+
updated_at: task.updated_at,
|
|
116
|
+
due: task.due,
|
|
117
|
+
priority: task.priority,
|
|
118
|
+
child_order: task.child_order,
|
|
119
|
+
content: task.content,
|
|
120
|
+
description: task.description,
|
|
121
|
+
note_count: task.note_count,
|
|
122
|
+
day_order: task.day_order,
|
|
123
|
+
is_collapsed: task.is_collapsed
|
|
124
|
+
});
|
|
125
|
+
const isJsonObject = (value) => value !== void 0 && value !== null && Predicate.isObject(value) && !Array.isArray(value);
|
|
126
|
+
const mediaType = (contentType) => contentType?.split(";", 1)[0]?.trim().toLowerCase();
|
|
127
|
+
/**
|
|
128
|
+
* A write body: `application/json`, a JSON object with only `allowed` keys and every `required`
|
|
129
|
+
* key, or a refusal.
|
|
130
|
+
*/
|
|
131
|
+
const bodyObject = (request, allowed, required) => {
|
|
132
|
+
if (mediaType(request.contentType) !== "application/json") return notEmulated("write bodies other than application/json are not emulated");
|
|
133
|
+
if (!isJsonObject(request.body)) return notEmulated("a request body that is not a JSON object is not emulated");
|
|
134
|
+
const keys = Object.keys(request.body);
|
|
135
|
+
const unknown = keys.find((key) => !allowed.includes(key));
|
|
136
|
+
if (unknown !== void 0) return notEmulated(`the body field ${unknown} is not emulated on this route`);
|
|
137
|
+
const missing = required.find((key) => !keys.includes(key));
|
|
138
|
+
return missing === void 0 ? request.body : notEmulated(`requests without the body field ${missing} are not emulated on this route`);
|
|
139
|
+
};
|
|
140
|
+
const noBody = (request) => request.hasBody ? notEmulated("a request body is not emulated on this route") : void 0;
|
|
141
|
+
/** A positive integer query value: `undefined` when absent, a refusal when not in `allowed`. */
|
|
142
|
+
const limitOf = (request, allowed, what) => {
|
|
143
|
+
const raw = request.query.get("limit");
|
|
144
|
+
if (raw === null) return void 0;
|
|
145
|
+
const value = Number(raw);
|
|
146
|
+
return /^[1-9][0-9]{0,3}$/.test(raw) && allowed(value) ? value : notEmulated(`limit must be ${what} (as the fixtures send it)`);
|
|
147
|
+
};
|
|
148
|
+
/** A task created through the recorded create flow (never a seeded one), still active. */
|
|
149
|
+
const createdActiveTask = (state, id) => isTodoistMintedId(id, todoistMintedTaskPrefix) ? state.tasks.find((task) => task.id === id && !task.checked) : void 0;
|
|
150
|
+
/** A case project created through the recorded create flow (never a seeded one). */
|
|
151
|
+
const createdProject = (state, id) => isTodoistMintedId(id, todoistMintedProjectPrefix) ? state.projects.find((project) => project.id === id) : void 0;
|
|
152
|
+
const byOrder = (left, right) => left.child_order - right.child_order || (left.id < right.id ? -1 : left.id > right.id ? 1 : 0);
|
|
153
|
+
const listTasks = (state, request, env) => {
|
|
154
|
+
const projectId = request.query.get("project_id");
|
|
155
|
+
if (projectId === null) return notEmulated("task listings without project_id are not emulated");
|
|
156
|
+
if (!state.projects.some((project) => project.id === projectId)) return notEmulated("a task listing of an unknown project is not emulated");
|
|
157
|
+
const limit = limitOf(request, (value) => value === taskLimit, String(taskLimit));
|
|
158
|
+
if (isNotEmulated(limit)) return limit;
|
|
159
|
+
if (!(limit === void 0 ? createdProject(state, projectId) !== void 0 : projectId === "6XSyntheticPage0")) return notEmulated("task listings other than the paging project with limit=2 or a case project created here without limit are not emulated");
|
|
160
|
+
const tasks = state.tasks.filter((task) => task.project_id === projectId && (env.drills.listIncludesClosed || !task.checked)).sort(byOrder);
|
|
161
|
+
const rawCursor = request.query.get("cursor");
|
|
162
|
+
const cursor = rawCursor === null ? void 0 : env.cursors.get(rawCursor);
|
|
163
|
+
if (rawCursor !== null && cursor === void 0) return notEmulated("a cursor this emulator did not issue is not emulated");
|
|
164
|
+
if (cursor !== void 0 && (cursor.projectId !== projectId || cursor.limit !== request.query.get("limit"))) return notEmulated("a cursor sent with another project_id or limit is not emulated");
|
|
165
|
+
if (limit === void 0) return () => json(200, {
|
|
166
|
+
results: tasks.map((task) => taskWire(state, env, task)),
|
|
167
|
+
next_cursor: null
|
|
168
|
+
});
|
|
169
|
+
const offset = cursor === void 0 || env.drills.cursorRestarts ? 0 : cursor.offset;
|
|
170
|
+
const end = offset + limit;
|
|
171
|
+
return () => {
|
|
172
|
+
let next = null;
|
|
173
|
+
if (end < tasks.length) {
|
|
174
|
+
next = `SyntheticTaskCursor${padded(env.cursorCounter.next++, 4)}`;
|
|
175
|
+
env.cursors.set(next, {
|
|
176
|
+
id: next,
|
|
177
|
+
projectId,
|
|
178
|
+
limit: String(limit),
|
|
179
|
+
offset: end
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return json(200, {
|
|
183
|
+
results: tasks.slice(offset, end).map((task) => taskWire(state, env, task)),
|
|
184
|
+
next_cursor: next
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
const getTask = (state, request, env) => {
|
|
189
|
+
const id = request.params.taskId ?? "";
|
|
190
|
+
const task = state.tasks.find((candidate) => candidate.id === id);
|
|
191
|
+
if (task === void 0) return () => notFound(state, env, "Task not found");
|
|
192
|
+
return task.checked ? notEmulated("reading a closed task is not emulated") : () => json(200, taskWire(state, env, task));
|
|
193
|
+
};
|
|
194
|
+
const createTask = (state, request, env) => {
|
|
195
|
+
const body = bodyObject(request, [
|
|
196
|
+
"content",
|
|
197
|
+
"project_id",
|
|
198
|
+
"due_date"
|
|
199
|
+
], ["content", "project_id"]);
|
|
200
|
+
if (isNotEmulated(body)) return body;
|
|
201
|
+
const { content, project_id: projectId, due_date: dueDate } = body;
|
|
202
|
+
if (!Predicate.isString(content) || content === "") return notEmulated("content must be a non-empty string");
|
|
203
|
+
if (!Predicate.isString(projectId) || createdProject(state, projectId) === void 0) return notEmulated("a task outside a case project created here is not emulated");
|
|
204
|
+
if (state.tasks.some((item) => item.project_id === projectId)) return notEmulated("a second task in one case project is not emulated");
|
|
205
|
+
if (dueDate !== void 0 && dueDate !== recordedDueDate) return notEmulated(`due_date other than ${recordedDueDate} (the recorded value) is not emulated`);
|
|
206
|
+
return () => {
|
|
207
|
+
const now = timestamp(env);
|
|
208
|
+
const number = state.counters.nextTaskNumber;
|
|
209
|
+
const task = makeTodoistTask({
|
|
210
|
+
id: `${todoistMintedTaskPrefix}${padded(number, 7)}`,
|
|
211
|
+
userId: state.userId,
|
|
212
|
+
projectId,
|
|
213
|
+
content,
|
|
214
|
+
labels: [],
|
|
215
|
+
addedAt: now,
|
|
216
|
+
updatedAt: now,
|
|
217
|
+
due: dueDate === void 0 || env.drills.ignoreDue ? null : dueOfDate,
|
|
218
|
+
childOrder: 1
|
|
219
|
+
});
|
|
220
|
+
state.counters = {
|
|
221
|
+
...state.counters,
|
|
222
|
+
nextTaskNumber: number + 1
|
|
223
|
+
};
|
|
224
|
+
state.tasks = [...state.tasks, task];
|
|
225
|
+
return json(200, taskWire(state, env, task));
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
const updateTask = (state, request, env) => {
|
|
229
|
+
const task = createdActiveTask(state, request.params.taskId ?? "");
|
|
230
|
+
if (task === void 0) return notEmulated("updating a seeded, unknown, or closed task is not emulated");
|
|
231
|
+
const body = bodyObject(request, ["content", "due_datetime"], []);
|
|
232
|
+
if (isNotEmulated(body)) return body;
|
|
233
|
+
const { content, due_datetime: dueDatetime } = body;
|
|
234
|
+
if (content === void 0 && dueDatetime === void 0) return notEmulated("an update without content or due_datetime is not emulated");
|
|
235
|
+
if (content !== void 0 && (!Predicate.isString(content) || content === "")) return notEmulated("content must be a non-empty string");
|
|
236
|
+
if (dueDatetime !== void 0 && dueDatetime !== recordedDueDatetime) return notEmulated(`due_datetime other than ${recordedDueDatetime} (the recorded value) is not emulated`);
|
|
237
|
+
const updated = {
|
|
238
|
+
...task,
|
|
239
|
+
content: Predicate.isString(content) ? content : task.content,
|
|
240
|
+
due: dueDatetime === void 0 || env.drills.ignoreDue ? task.due : dueOfDatetime
|
|
241
|
+
};
|
|
242
|
+
return () => {
|
|
243
|
+
state.tasks = state.tasks.map((item) => item.id === task.id ? updated : item);
|
|
244
|
+
return json(200, taskWire(state, env, updated));
|
|
245
|
+
};
|
|
246
|
+
};
|
|
247
|
+
const closeTask = (state, request, env) => {
|
|
248
|
+
const refused = noBody(request);
|
|
249
|
+
if (refused !== void 0) return refused;
|
|
250
|
+
const task = createdActiveTask(state, request.params.taskId ?? "");
|
|
251
|
+
if (task === void 0) return notEmulated("closing a seeded, unknown, or closed task is not emulated");
|
|
252
|
+
return () => {
|
|
253
|
+
const closed = {
|
|
254
|
+
...task,
|
|
255
|
+
checked: true,
|
|
256
|
+
completed_at: timestamp(env)
|
|
257
|
+
};
|
|
258
|
+
state.tasks = state.tasks.map((item) => item.id === task.id ? closed : item);
|
|
259
|
+
return noContent();
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
/** The label listing, one page (its fixture never pages): `limit` required; every label in it. */
|
|
263
|
+
const listLabels = (state, request) => {
|
|
264
|
+
const limit = limitOf(request, (value) => value <= maxListLimit, `from 1 to ${maxListLimit}`);
|
|
265
|
+
if (isNotEmulated(limit)) return limit;
|
|
266
|
+
if (limit === void 0) return notEmulated("label listings without limit are not emulated");
|
|
267
|
+
return state.labels.length > limit ? notEmulated(`more labels than limit (${limit}): paging labels is not emulated`) : () => json(200, {
|
|
268
|
+
results: state.labels,
|
|
269
|
+
next_cursor: null
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
const createProject = (state, request, env) => {
|
|
273
|
+
const body = bodyObject(request, ["name", "parent_id"], ["name", "parent_id"]);
|
|
274
|
+
if (isNotEmulated(body)) return body;
|
|
275
|
+
const { name, parent_id: parentId } = body;
|
|
276
|
+
const runId = Predicate.isString(name) ? caseProjectNamePattern.exec(name)?.[1] : void 0;
|
|
277
|
+
if (!Predicate.isString(name) || runId === void 0 || runId.length > 40) return notEmulated("project names other than yolk-conformance-<runId>-<lifecycle|due|parent|delete> are not emulated");
|
|
278
|
+
if (!Predicate.isString(parentId) || isTodoistMintedId(parentId, todoistMintedProjectPrefix) || !state.projects.some((item) => item.id === parentId)) return notEmulated("a project under anything but an existing seeded project is not emulated");
|
|
279
|
+
if (state.projects.some((item) => item.parent_id === parentId)) return notEmulated("a second sub-project under one parent is not emulated");
|
|
280
|
+
return () => {
|
|
281
|
+
const now = timestamp(env);
|
|
282
|
+
const number = state.counters.nextProjectNumber;
|
|
283
|
+
const project = makeTodoistProject({
|
|
284
|
+
id: `${todoistMintedProjectPrefix}${padded(number, 4)}`,
|
|
285
|
+
name,
|
|
286
|
+
parentId,
|
|
287
|
+
childOrder: 1,
|
|
288
|
+
createdAt: now,
|
|
289
|
+
updatedAt: now
|
|
290
|
+
});
|
|
291
|
+
state.counters = {
|
|
292
|
+
...state.counters,
|
|
293
|
+
nextProjectNumber: number + 1
|
|
294
|
+
};
|
|
295
|
+
state.projects = [...state.projects, project];
|
|
296
|
+
return json(200, env.drills.createOmitsParent ? {
|
|
297
|
+
...projectWire(project),
|
|
298
|
+
parent_id: null
|
|
299
|
+
} : projectWire(project));
|
|
300
|
+
};
|
|
301
|
+
};
|
|
302
|
+
const getProject = (state, request, env) => {
|
|
303
|
+
const id = request.params.projectId ?? "";
|
|
304
|
+
if (!state.projects.some((item) => item.id === id)) return () => notFound(state, env, "Project not found");
|
|
305
|
+
const project = createdProject(state, id);
|
|
306
|
+
return project === void 0 ? notEmulated("reading a seeded project is not emulated (no fixture records its answer)") : () => json(200, projectWire(project));
|
|
307
|
+
};
|
|
308
|
+
const deleteProject = (state, request, env) => {
|
|
309
|
+
const refused = noBody(request);
|
|
310
|
+
if (refused !== void 0) return refused;
|
|
311
|
+
const project = createdProject(state, request.params.projectId ?? "");
|
|
312
|
+
if (project === void 0) return notEmulated("deleting a seeded or unknown project is not emulated");
|
|
313
|
+
return () => {
|
|
314
|
+
state.projects = state.projects.filter((item) => item.id !== project.id);
|
|
315
|
+
if (!env.drills.deleteKeepsTasks) state.tasks = state.tasks.filter((task) => task.project_id !== project.id);
|
|
316
|
+
return noContent();
|
|
317
|
+
};
|
|
318
|
+
};
|
|
319
|
+
/** The route's request, as the handlers see it (the wrapper checked and parsed the body). */
|
|
320
|
+
const routeRequest = (request) => ({
|
|
321
|
+
params: request.params,
|
|
322
|
+
query: request.query,
|
|
323
|
+
body: request.json,
|
|
324
|
+
hasBody: request.json !== void 0,
|
|
325
|
+
contentType: request.header("content-type") ?? null
|
|
326
|
+
});
|
|
327
|
+
const route = (method, path, write, caseIds, handler, queryKeys = []) => ({
|
|
328
|
+
route: statefulRoute({
|
|
329
|
+
method,
|
|
330
|
+
path: `${todoistEmulatorBasePath}${path}`,
|
|
331
|
+
kind: "connector",
|
|
332
|
+
write,
|
|
333
|
+
caseIds,
|
|
334
|
+
evidence: "unverified"
|
|
335
|
+
}, "json-or-empty", routeRequest, (state, request, context) => handler(state, request, context.env)),
|
|
336
|
+
queryKeys
|
|
337
|
+
});
|
|
338
|
+
/** The route table: evidence plus handler. `todoistEmulatorRoutes` is its evidence part. */
|
|
339
|
+
const todoistApiRoutes = [
|
|
340
|
+
route("GET", "/tasks", false, [pagingCase, lifecycleCase], listTasks, [
|
|
341
|
+
"project_id",
|
|
342
|
+
"cursor",
|
|
343
|
+
"limit"
|
|
344
|
+
]),
|
|
345
|
+
route("POST", "/tasks", true, [
|
|
346
|
+
lifecycleCase,
|
|
347
|
+
dueCase,
|
|
348
|
+
deleteCase
|
|
349
|
+
], createTask),
|
|
350
|
+
route("GET", "/tasks/{taskId}", false, [
|
|
351
|
+
notFoundCase,
|
|
352
|
+
labelsCase,
|
|
353
|
+
lifecycleCase,
|
|
354
|
+
deleteCase
|
|
355
|
+
], getTask),
|
|
356
|
+
route("POST", "/tasks/{taskId}", true, [lifecycleCase, dueCase], updateTask),
|
|
357
|
+
route("POST", "/tasks/{taskId}/close", true, [lifecycleCase], closeTask),
|
|
358
|
+
route("GET", "/labels", false, [labelsCase], listLabels, ["limit"]),
|
|
359
|
+
route("POST", "/projects", true, projectCases, createProject),
|
|
360
|
+
route("GET", "/projects/{projectId}", false, projectCases, getProject),
|
|
361
|
+
route("DELETE", "/projects/{projectId}", true, projectCases, deleteProject)
|
|
362
|
+
];
|
|
363
|
+
const compiledRoutes = todoistApiRoutes.map((candidate) => ({
|
|
364
|
+
route: candidate,
|
|
365
|
+
pattern: new RegExp(`^${candidate.route.path.replace(/\{[A-Za-z]+\}/g, "([^/]+)")}$`),
|
|
366
|
+
names: [...candidate.route.path.matchAll(/\{([A-Za-z]+)\}/g)].map((match) => match[1] ?? "")
|
|
367
|
+
}));
|
|
368
|
+
/**
|
|
369
|
+
* Path words of other Todoist endpoints (`/tasks/filter`, `/tasks/completed`, `/projects/archived`,
|
|
370
|
+
* ...): never an id, so those endpoints stay unknown routes instead of reading as a missing item.
|
|
371
|
+
*/
|
|
372
|
+
const reservedSegments = new Set([
|
|
373
|
+
"filter",
|
|
374
|
+
"completed",
|
|
375
|
+
"quick",
|
|
376
|
+
"archived",
|
|
377
|
+
"search"
|
|
378
|
+
]);
|
|
379
|
+
/**
|
|
380
|
+
* The route answering `method` + raw `path` with its id parameters, or `undefined` (no route, a
|
|
381
|
+
* reserved endpoint word, or a raw segment that is not a Todoist id: percent-encoding is never
|
|
382
|
+
* recognised).
|
|
383
|
+
*/
|
|
384
|
+
const matchTodoistRoute = (method, path) => {
|
|
385
|
+
for (const candidate of compiledRoutes) {
|
|
386
|
+
if (candidate.route.route.method !== method.toUpperCase()) continue;
|
|
387
|
+
const match = candidate.pattern.exec(path);
|
|
388
|
+
if (match === null) continue;
|
|
389
|
+
const params = {};
|
|
390
|
+
for (const [index, name] of candidate.names.entries()) {
|
|
391
|
+
const value = match[index + 1] ?? "";
|
|
392
|
+
if (!todoistIdPattern.test(value) || reservedSegments.has(value)) return;
|
|
393
|
+
params[name] = value;
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
route: candidate.route,
|
|
397
|
+
params
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
/**
|
|
402
|
+
* Why the query is not emulated on `route` (a key it does not emulate, or a repeated key), or
|
|
403
|
+
* `undefined`.
|
|
404
|
+
*/
|
|
405
|
+
const todoistQueryProblem = (route, query) => {
|
|
406
|
+
const keys = [...query.keys()];
|
|
407
|
+
const unknown = keys.find((key) => !route.queryKeys.includes(key));
|
|
408
|
+
if (unknown !== void 0) return `the query parameter ${unknown} is not emulated on this route`;
|
|
409
|
+
const repeated = keys.find((key, index) => keys.indexOf(key) !== index);
|
|
410
|
+
return repeated === void 0 ? void 0 : `the repeated query parameter ${repeated} is not emulated`;
|
|
411
|
+
};
|
|
412
|
+
//#endregion
|
|
413
|
+
export { matchTodoistRoute, todoistApiRoutes, todoistEmulatorBasePath, todoistQueryProblem };
|
|
414
|
+
|
|
415
|
+
//# sourceMappingURL=api.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.mjs","names":[],"sources":["../../src/todoist/api.ts"],"sourcesContent":["/**\n * Todoist emulator API: the route table (evidence, query allowlist, handler) and route matching\n * (internal; re-exported by `src/todoist.ts`, which runs the table on the shared stateful wrapper,\n * `src/stateful-emulator.ts`, in its resolved mode).\n *\n * Only the routes the seven Todoist conformance cases (and their cleanup) need are emulated, with\n * wire shapes copied from the synthetic fixtures; everything else answers the wrapper's ledgered\n * 400 not-emulated. Each route lists the cases whose claims it follows. The project listing that\n * the read-only leftover lookup (`findTodoistConformanceLeftovers`) sends has no fixture, so it is\n * not emulated: the lookup fails, and runners print their lookup-failed WARN.\n *\n * @experimental\n */\nimport { Predicate } from 'effect'\nimport type * as Schema from 'effect/Schema'\nimport {\n isNotEmulated,\n notEmulated,\n statefulRoute,\n type Commit,\n type EmulatedRequest,\n type NotEmulated,\n type StatefulRoute\n} from '../stateful-emulator.ts'\nimport {\n isTodoistMintedId,\n makeTodoistProject,\n makeTodoistTask,\n todoistIdPattern,\n todoistMintedProjectPrefix,\n todoistMintedTaskPrefix,\n todoistPagingProjectId,\n type TodoistEmulatorDue,\n type TodoistEmulatorProject,\n type TodoistEmulatorState,\n type TodoistEmulatorTask\n} from './state.ts'\n\n/** API v1 prefix of every Todoist route. */\nexport const todoistEmulatorBasePath = '/api/v1'\n\n/** Drill knobs (tests only): each makes the emulator disagree with one conformance claim. */\nexport type TodoistEmulatorDrills = {\n /** `true`: a task-listing cursor leads back to the first page (tasks repeat across pages). */\n readonly cursorRestarts?: boolean\n /** `true`: 404 bodies omit the `error` message. */\n readonly notFoundWithoutError?: boolean\n /** `true`: tasks answer their labels as label ids instead of names. */\n readonly taskLabelsAsIds?: boolean\n /** `true`: task listings include closed tasks. */\n readonly listIncludesClosed?: boolean\n /** `true`: `due_date` / `due_datetime` are accepted but not applied (`due` stays `null`). */\n readonly ignoreDue?: boolean\n /** `true`: a project create answers `parent_id: null` (the project still has its parent). */\n readonly createOmitsParent?: boolean\n /** `true`: deleting a project keeps its tasks. */\n readonly deleteKeepsTasks?: boolean\n}\n\n/** Where a task-listing cursor leads (runtime data, like the ledger; not in the state). */\nexport type TodoistCursor = {\n readonly id: string\n readonly projectId: string\n readonly limit: string\n readonly offset: number\n}\n\nexport type TodoistApiEnv = {\n /** Clock in epoch milliseconds (timestamps of created and closed items). */\n readonly now: () => number\n readonly drills: Required<TodoistEmulatorDrills>\n /** Task-listing cursors by id (runtime-only; cleared by reset and seed). */\n readonly cursors: Map<string, TodoistCursor>\n readonly cursorCounter: { next: number }\n}\n\ntype RouteRequest = {\n /** Path parameters (Todoist ids, matched raw). */\n readonly params: Readonly<Record<string, string>>\n readonly query: URLSearchParams\n /** Parsed JSON body; `undefined` when absent. */\n readonly body: Schema.Json | undefined\n /** The request carried a non-empty body. */\n readonly hasBody: boolean\n readonly contentType: string | null\n}\n\n/**\n * A route handler (the wrapper's plan): it validates the request and the state without writing\n * (not emulated), then returns the commit that performs every write.\n */\ntype RouteHandler = (\n state: TodoistEmulatorState,\n request: RouteRequest,\n env: TodoistApiEnv\n) => Commit | NotEmulated\n\ntype TodoistApiRoute = {\n readonly route: StatefulRoute<TodoistEmulatorState, TodoistApiEnv>\n /** Query parameters the route emulates; any other (or a repeated) key is not emulated. */\n readonly queryKeys: ReadonlyArray<string>\n}\n\nconst pagingCase = 'todoist.tasks.list-cursor-paging'\n\nconst notFoundCase = 'todoist.errors.not-found-envelope'\n\nconst labelsCase = 'todoist.labels.task-labels-are-names'\n\nconst lifecycleCase = 'todoist.tasks.lifecycle-close'\n\nconst dueCase = 'todoist.tasks.due-dates'\n\nconst parentCase = 'todoist.projects.parent-id'\n\nconst deleteCase = 'todoist.projects.delete-then-not-found'\n\n/** Every write case creates, reads, and deletes its own project. */\nconst projectCases = [lifecycleCase, dueCase, parentCase, deleteCase]\n\n/** The task listing page size the fixtures send (the only one emulated), and the largest\n * unlimited listing they record. */\nconst taskLimit = 2\n\n/** The `limit` the label listing fixture sends (the largest one emulated). */\nconst maxListLimit = 200\n\n/** The only due values the due-dates fixture records, with the due objects it answers. */\nconst recordedDueDate = '2030-01-15'\n\nconst recordedDueDatetime = '2030-01-15T12:00:00Z'\n\nconst dueOfDate: TodoistEmulatorDue = {\n date: recordedDueDate,\n timezone: null,\n string: 'Jan 15 2030',\n lang: 'en',\n is_recurring: false\n}\n\nconst dueOfDatetime: TodoistEmulatorDue = {\n date: recordedDueDatetime,\n timezone: 'UTC',\n string: 'Jan 15 2030 12:00',\n lang: 'en',\n is_recurring: false\n}\n\n/**\n * Case project names: `yolk-conformance-<runId>-<suffix>` with a conformance run id (`run-` then\n * lower-case letters, digits, and inner hyphens, at most 40 characters) and a recorded suffix.\n */\nconst caseProjectNamePattern =\n /^yolk-conformance-(run-[a-z0-9]+(?:-[a-z0-9]+)*)-(lifecycle|due|parent|delete)$/\n\nconst json = (status: number, body: unknown): Response =>\n new Response(JSON.stringify(body), {\n status,\n headers: { 'content-type': 'application/json' }\n })\n\nconst noContent = (): Response => new Response(null, { status: 204 })\n\nconst padded = (value: number, width: number): string => String(value).padStart(width, '0')\n\n/** An API v1 timestamp (`2026-09-30T12:00:00.000000Z`) from the clock; throws on a bad clock. */\nconst timestamp = (env: TodoistApiEnv): string => {\n const value = env.now()\n\n if (!Number.isFinite(value)) throw new Error('the emulator clock is not a finite instant')\n\n return `${new Date(value).toISOString().slice(0, 23)}000Z`\n}\n\n/** The 404 error body of the fixtures, with the next synthetic `event_id`. */\nconst notFound = (state: TodoistEmulatorState, env: TodoistApiEnv, error: string): Response => {\n const number = state.counters.nextEventNumber\n\n state.counters = { ...state.counters, nextEventNumber: number + 1 }\n\n const rest = {\n error_code: 478,\n error_extra: { event_id: padded(number, 32) },\n error_tag: 'NOT_FOUND',\n http_code: 404\n }\n\n return json(404, env.drills.notFoundWithoutError ? rest : { error, ...rest })\n}\n\nconst projectWire = (project: TodoistEmulatorProject) =>\n makeTodoistProject({\n id: project.id,\n name: project.name,\n parentId: project.parent_id,\n childOrder: project.child_order,\n createdAt: project.created_at,\n updatedAt: project.updated_at\n })\n\nconst taskWire = (state: TodoistEmulatorState, env: TodoistApiEnv, task: TodoistEmulatorTask) => ({\n id: task.id,\n user_id: task.user_id,\n project_id: task.project_id,\n section_id: task.section_id,\n parent_id: task.parent_id,\n added_by_uid: task.added_by_uid,\n assigned_by_uid: task.assigned_by_uid,\n responsible_uid: task.responsible_uid,\n labels: env.drills.taskLabelsAsIds\n ? task.labels.map(name => state.labels.find(item => item.name === name)?.id ?? name)\n : task.labels,\n deadline: task.deadline,\n duration: task.duration,\n checked: task.checked,\n is_deleted: task.is_deleted,\n added_at: task.added_at,\n completed_at: task.completed_at,\n updated_at: task.updated_at,\n due: task.due,\n priority: task.priority,\n child_order: task.child_order,\n content: task.content,\n description: task.description,\n note_count: task.note_count,\n day_order: task.day_order,\n is_collapsed: task.is_collapsed\n})\n\nconst isJsonObject = (value: Schema.Json | undefined): value is Schema.JsonObject =>\n value !== undefined && value !== null && Predicate.isObject(value) && !Array.isArray(value)\n\nconst mediaType = (contentType: string | null): string | undefined =>\n contentType?.split(';', 1)[0]?.trim().toLowerCase()\n\n/**\n * A write body: `application/json`, a JSON object with only `allowed` keys and every `required`\n * key, or a refusal.\n */\nconst bodyObject = (\n request: RouteRequest,\n allowed: ReadonlyArray<string>,\n required: ReadonlyArray<string>\n): Schema.JsonObject | NotEmulated => {\n if (mediaType(request.contentType) !== 'application/json') {\n return notEmulated('write bodies other than application/json are not emulated')\n }\n\n if (!isJsonObject(request.body)) {\n return notEmulated('a request body that is not a JSON object is not emulated')\n }\n\n const keys = Object.keys(request.body)\n const unknown = keys.find(key => !allowed.includes(key))\n\n if (unknown !== undefined) {\n return notEmulated(`the body field ${unknown} is not emulated on this route`)\n }\n\n const missing = required.find(key => !keys.includes(key))\n\n return missing === undefined\n ? request.body\n : notEmulated(`requests without the body field ${missing} are not emulated on this route`)\n}\n\nconst noBody = (request: RouteRequest): NotEmulated | undefined =>\n request.hasBody ? notEmulated('a request body is not emulated on this route') : undefined\n\n/** A positive integer query value: `undefined` when absent, a refusal when not in `allowed`. */\nconst limitOf = (\n request: RouteRequest,\n allowed: (value: number) => boolean,\n what: string\n): number | undefined | NotEmulated => {\n const raw = request.query.get('limit')\n\n if (raw === null) return undefined\n\n const value = Number(raw)\n\n return /^[1-9][0-9]{0,3}$/.test(raw) && allowed(value)\n ? value\n : notEmulated(`limit must be ${what} (as the fixtures send it)`)\n}\n\n/** A task created through the recorded create flow (never a seeded one), still active. */\nconst createdActiveTask = (state: TodoistEmulatorState, id: string) =>\n isTodoistMintedId(id, todoistMintedTaskPrefix)\n ? state.tasks.find(task => task.id === id && !task.checked)\n : undefined\n\n/** A case project created through the recorded create flow (never a seeded one). */\nconst createdProject = (state: TodoistEmulatorState, id: string) =>\n isTodoistMintedId(id, todoistMintedProjectPrefix)\n ? state.projects.find(project => project.id === id)\n : undefined\n\nconst byOrder = (left: TodoistEmulatorTask, right: TodoistEmulatorTask): number =>\n left.child_order - right.child_order || (left.id < right.id ? -1 : left.id > right.id ? 1 : 0)\n\n// Handlers: validate (not emulated) first, then return the commit that writes.\n\nconst listTasks: RouteHandler = (state, request, env) => {\n const projectId = request.query.get('project_id')\n\n if (projectId === null) return notEmulated('task listings without project_id are not emulated')\n\n if (!state.projects.some(project => project.id === projectId)) {\n return notEmulated('a task listing of an unknown project is not emulated')\n }\n\n const limit = limitOf(request, value => value === taskLimit, String(taskLimit))\n\n if (isNotEmulated(limit)) return limit\n\n // The fixtures record two listings only: the paging project with `limit=2`, and a case project\n // created here without `limit`.\n const recorded =\n limit === undefined\n ? createdProject(state, projectId) !== undefined\n : projectId === todoistPagingProjectId\n\n if (!recorded) {\n return notEmulated(\n 'task listings other than the paging project with limit=2 or a case project created here ' +\n 'without limit are not emulated'\n )\n }\n\n const tasks = state.tasks\n .filter(\n task => task.project_id === projectId && (env.drills.listIncludesClosed || !task.checked)\n )\n .sort(byOrder)\n\n const rawCursor = request.query.get('cursor')\n const cursor = rawCursor === null ? undefined : env.cursors.get(rawCursor)\n\n // Cursors are valid only as issued by this emulator since its last reset or seed.\n if (rawCursor !== null && cursor === undefined) {\n return notEmulated('a cursor this emulator did not issue is not emulated')\n }\n\n if (\n cursor !== undefined &&\n (cursor.projectId !== projectId || cursor.limit !== request.query.get('limit'))\n ) {\n return notEmulated('a cursor sent with another project_id or limit is not emulated')\n }\n\n // A case project holds at most one task, so its unlimited listing is always one page.\n if (limit === undefined) {\n return () =>\n json(200, { results: tasks.map(task => taskWire(state, env, task)), next_cursor: null })\n }\n\n const offset = cursor === undefined || env.drills.cursorRestarts ? 0 : cursor.offset\n const end = offset + limit\n\n return () => {\n let next: string | null = null\n\n if (end < tasks.length) {\n next = `SyntheticTaskCursor${padded(env.cursorCounter.next++, 4)}`\n env.cursors.set(next, { id: next, projectId, limit: String(limit), offset: end })\n }\n\n return json(200, {\n results: tasks.slice(offset, end).map(task => taskWire(state, env, task)),\n next_cursor: next\n })\n }\n}\n\nconst getTask: RouteHandler = (state, request, env) => {\n const id = request.params.taskId ?? ''\n const task = state.tasks.find(candidate => candidate.id === id)\n\n if (task === undefined) return () => notFound(state, env, 'Task not found')\n\n return task.checked\n ? notEmulated('reading a closed task is not emulated')\n : () => json(200, taskWire(state, env, task))\n}\n\nconst createTask: RouteHandler = (state, request, env) => {\n const body = bodyObject(request, ['content', 'project_id', 'due_date'], ['content', 'project_id'])\n\n if (isNotEmulated(body)) return body\n\n const { content, project_id: projectId, due_date: dueDate } = body\n\n if (!Predicate.isString(content) || content === '') {\n return notEmulated('content must be a non-empty string')\n }\n\n if (!Predicate.isString(projectId) || createdProject(state, projectId) === undefined) {\n return notEmulated('a task outside a case project created here is not emulated')\n }\n\n // The fixtures record one task per case project (`child_order: 1`).\n if (state.tasks.some(item => item.project_id === projectId)) {\n return notEmulated('a second task in one case project is not emulated')\n }\n\n if (dueDate !== undefined && dueDate !== recordedDueDate) {\n return notEmulated(\n `due_date other than ${recordedDueDate} (the recorded value) is not emulated`\n )\n }\n\n return () => {\n const now = timestamp(env)\n const number = state.counters.nextTaskNumber\n\n const task = makeTodoistTask({\n id: `${todoistMintedTaskPrefix}${padded(number, 7)}`,\n userId: state.userId,\n projectId,\n content,\n labels: [],\n addedAt: now,\n updatedAt: now,\n due: dueDate === undefined || env.drills.ignoreDue ? null : dueOfDate,\n childOrder: 1\n })\n\n state.counters = { ...state.counters, nextTaskNumber: number + 1 }\n state.tasks = [...state.tasks, task]\n\n return json(200, taskWire(state, env, task))\n }\n}\n\nconst updateTask: RouteHandler = (state, request, env) => {\n const task = createdActiveTask(state, request.params.taskId ?? '')\n\n if (task === undefined) {\n return notEmulated('updating a seeded, unknown, or closed task is not emulated')\n }\n\n const body = bodyObject(request, ['content', 'due_datetime'], [])\n\n if (isNotEmulated(body)) return body\n\n const { content, due_datetime: dueDatetime } = body\n\n if (content === undefined && dueDatetime === undefined) {\n return notEmulated('an update without content or due_datetime is not emulated')\n }\n\n if (content !== undefined && (!Predicate.isString(content) || content === '')) {\n return notEmulated('content must be a non-empty string')\n }\n\n if (dueDatetime !== undefined && dueDatetime !== recordedDueDatetime) {\n return notEmulated(\n `due_datetime other than ${recordedDueDatetime} (the recorded value) is not emulated`\n )\n }\n\n // The fixtures answer an update with the task's unchanged `updated_at`.\n const updated: TodoistEmulatorTask = {\n ...task,\n content: Predicate.isString(content) ? content : task.content,\n due: dueDatetime === undefined || env.drills.ignoreDue ? task.due : dueOfDatetime\n }\n\n return () => {\n state.tasks = state.tasks.map(item => (item.id === task.id ? updated : item))\n\n return json(200, taskWire(state, env, updated))\n }\n}\n\nconst closeTask: RouteHandler = (state, request, env) => {\n const refused = noBody(request)\n\n if (refused !== undefined) return refused\n\n const task = createdActiveTask(state, request.params.taskId ?? '')\n\n if (task === undefined) {\n return notEmulated('closing a seeded, unknown, or closed task is not emulated')\n }\n\n return () => {\n const closed: TodoistEmulatorTask = { ...task, checked: true, completed_at: timestamp(env) }\n\n state.tasks = state.tasks.map(item => (item.id === task.id ? closed : item))\n\n return noContent()\n }\n}\n\n/** The label listing, one page (its fixture never pages): `limit` required; every label in it. */\nconst listLabels: RouteHandler = (state, request) => {\n const limit = limitOf(request, value => value <= maxListLimit, `from 1 to ${maxListLimit}`)\n\n if (isNotEmulated(limit)) return limit\n\n if (limit === undefined) return notEmulated('label listings without limit are not emulated')\n\n return state.labels.length > limit\n ? notEmulated(`more labels than limit (${limit}): paging labels is not emulated`)\n : () => json(200, { results: state.labels, next_cursor: null })\n}\n\nconst createProject: RouteHandler = (state, request, env) => {\n const body = bodyObject(request, ['name', 'parent_id'], ['name', 'parent_id'])\n\n if (isNotEmulated(body)) return body\n\n const { name, parent_id: parentId } = body\n const runId = Predicate.isString(name) ? caseProjectNamePattern.exec(name)?.[1] : undefined\n\n if (!Predicate.isString(name) || runId === undefined || runId.length > 40) {\n return notEmulated(\n 'project names other than yolk-conformance-<runId>-<lifecycle|due|parent|delete> ' +\n 'are not emulated'\n )\n }\n\n // The fixtures create case projects under the seeded work project, never under a case project.\n if (\n !Predicate.isString(parentId) ||\n isTodoistMintedId(parentId, todoistMintedProjectPrefix) ||\n !state.projects.some(item => item.id === parentId)\n ) {\n return notEmulated('a project under anything but an existing seeded project is not emulated')\n }\n\n // The fixtures record a first sub-project (`child_order: 1`) only.\n if (state.projects.some(item => item.parent_id === parentId)) {\n return notEmulated('a second sub-project under one parent is not emulated')\n }\n\n return () => {\n const now = timestamp(env)\n const number = state.counters.nextProjectNumber\n\n const project = makeTodoistProject({\n id: `${todoistMintedProjectPrefix}${padded(number, 4)}`,\n name,\n parentId,\n childOrder: 1,\n createdAt: now,\n updatedAt: now\n })\n\n state.counters = { ...state.counters, nextProjectNumber: number + 1 }\n state.projects = [...state.projects, project]\n\n return json(\n 200,\n env.drills.createOmitsParent\n ? { ...projectWire(project), parent_id: null }\n : projectWire(project)\n )\n }\n}\n\nconst getProject: RouteHandler = (state, request, env) => {\n const id = request.params.projectId ?? ''\n\n if (!state.projects.some(item => item.id === id)) {\n return () => notFound(state, env, 'Project not found')\n }\n\n // Only projects created here answer: no fixture records a seeded project's object.\n const project = createdProject(state, id)\n\n return project === undefined\n ? notEmulated('reading a seeded project is not emulated (no fixture records its answer)')\n : () => json(200, projectWire(project))\n}\n\nconst deleteProject: RouteHandler = (state, request, env) => {\n const refused = noBody(request)\n\n if (refused !== undefined) return refused\n\n const project = createdProject(state, request.params.projectId ?? '')\n\n // A created project never has sub-projects: projects are created under seeded parents only.\n if (project === undefined) {\n return notEmulated('deleting a seeded or unknown project is not emulated')\n }\n\n return () => {\n state.projects = state.projects.filter(item => item.id !== project.id)\n\n if (!env.drills.deleteKeepsTasks) {\n state.tasks = state.tasks.filter(task => task.project_id !== project.id)\n }\n\n return noContent()\n }\n}\n\n/** The route's request, as the handlers see it (the wrapper checked and parsed the body). */\nconst routeRequest = (request: EmulatedRequest): RouteRequest => ({\n params: request.params,\n query: request.query,\n body: request.json,\n // A non-empty body is valid JSON by now (`json-or-empty`), so it always parses.\n hasBody: request.json !== undefined,\n contentType: request.header('content-type') ?? null\n})\n\nconst route = (\n method: string,\n path: string,\n write: boolean,\n caseIds: ReadonlyArray<string>,\n handler: RouteHandler,\n queryKeys: ReadonlyArray<string> = []\n): TodoistApiRoute => ({\n route: statefulRoute(\n {\n method,\n path: `${todoistEmulatorBasePath}${path}`,\n kind: 'connector',\n write,\n caseIds,\n evidence: 'unverified'\n },\n 'json-or-empty',\n routeRequest,\n (state, request, context) => handler(state, request, context.env)\n ),\n queryKeys\n})\n\n/** The route table: evidence plus handler. `todoistEmulatorRoutes` is its evidence part. */\nexport const todoistApiRoutes: ReadonlyArray<TodoistApiRoute> = [\n route('GET', '/tasks', false, [pagingCase, lifecycleCase], listTasks, [\n 'project_id',\n 'cursor',\n 'limit'\n ]),\n route('POST', '/tasks', true, [lifecycleCase, dueCase, deleteCase], createTask),\n route(\n 'GET',\n '/tasks/{taskId}',\n false,\n [notFoundCase, labelsCase, lifecycleCase, deleteCase],\n getTask\n ),\n route('POST', '/tasks/{taskId}', true, [lifecycleCase, dueCase], updateTask),\n route('POST', '/tasks/{taskId}/close', true, [lifecycleCase], closeTask),\n route('GET', '/labels', false, [labelsCase], listLabels, ['limit']),\n route('POST', '/projects', true, projectCases, createProject),\n route('GET', '/projects/{projectId}', false, projectCases, getProject),\n route('DELETE', '/projects/{projectId}', true, projectCases, deleteProject)\n]\n\nconst compiledRoutes = todoistApiRoutes.map(candidate => ({\n route: candidate,\n pattern: new RegExp(`^${candidate.route.path.replace(/\\{[A-Za-z]+\\}/g, '([^/]+)')}$`),\n names: [...candidate.route.path.matchAll(/\\{([A-Za-z]+)\\}/g)].map(match => match[1] ?? '')\n}))\n\nexport type TodoistMatchedRoute = {\n readonly route: TodoistApiRoute\n /** Path parameters (Todoist ids, matched raw: never percent-encoded). */\n readonly params: Readonly<Record<string, string>>\n}\n\n/**\n * Path words of other Todoist endpoints (`/tasks/filter`, `/tasks/completed`, `/projects/archived`,\n * ...): never an id, so those endpoints stay unknown routes instead of reading as a missing item.\n */\nconst reservedSegments: ReadonlySet<string> = new Set([\n 'filter',\n 'completed',\n 'quick',\n 'archived',\n 'search'\n])\n\n/**\n * The route answering `method` + raw `path` with its id parameters, or `undefined` (no route, a\n * reserved endpoint word, or a raw segment that is not a Todoist id: percent-encoding is never\n * recognised).\n */\nexport const matchTodoistRoute = (\n method: string,\n path: string\n): TodoistMatchedRoute | undefined => {\n for (const candidate of compiledRoutes) {\n if (candidate.route.route.method !== method.toUpperCase()) continue\n\n const match = candidate.pattern.exec(path)\n\n if (match === null) continue\n\n const params: Record<string, string> = {}\n\n for (const [index, name] of candidate.names.entries()) {\n // The raw segment itself must be a Todoist id: a percent-encoded id is not recognised.\n const value = match[index + 1] ?? ''\n\n if (!todoistIdPattern.test(value) || reservedSegments.has(value)) {\n return undefined\n }\n\n params[name] = value\n }\n\n return { route: candidate.route, params }\n }\n\n return undefined\n}\n\n/**\n * Why the query is not emulated on `route` (a key it does not emulate, or a repeated key), or\n * `undefined`.\n */\nexport const todoistQueryProblem = (\n route: TodoistApiRoute,\n query: URLSearchParams\n): string | undefined => {\n const keys = [...query.keys()]\n const unknown = keys.find(key => !route.queryKeys.includes(key))\n\n if (unknown !== undefined) return `the query parameter ${unknown} is not emulated on this route`\n\n const repeated = keys.find((key, index) => keys.indexOf(key) !== index)\n\n return repeated === undefined\n ? undefined\n : `the repeated query parameter ${repeated} is not emulated`\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAuCA,MAAa,0BAA0B;AAgEvC,MAAM,aAAa;AAEnB,MAAM,eAAe;AAErB,MAAM,aAAa;AAEnB,MAAM,gBAAgB;AAEtB,MAAM,UAAU;AAEhB,MAAM,aAAa;AAEnB,MAAM,aAAa;;AAGnB,MAAM,eAAe;CAAC;CAAe;CAAS;CAAY;AAAU;;;AAIpE,MAAM,YAAY;;AAGlB,MAAM,eAAe;;AAGrB,MAAM,kBAAkB;AAExB,MAAM,sBAAsB;AAE5B,MAAM,YAAgC;CACpC,MAAM;CACN,UAAU;CACV,QAAQ;CACR,MAAM;CACN,cAAc;AAChB;AAEA,MAAM,gBAAoC;CACxC,MAAM;CACN,UAAU;CACV,QAAQ;CACR,MAAM;CACN,cAAc;AAChB;;;;;AAMA,MAAM,yBACJ;AAEF,MAAM,QAAQ,QAAgB,SAC5B,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG;CACjC;CACA,SAAS,EAAE,gBAAgB,mBAAmB;AAChD,CAAC;AAEH,MAAM,kBAA4B,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;AAEpE,MAAM,UAAU,OAAe,UAA0B,OAAO,KAAK,EAAE,SAAS,OAAO,GAAG;;AAG1F,MAAM,aAAa,QAA+B;CAChD,MAAM,QAAQ,IAAI,IAAI;CAEtB,IAAI,CAAC,OAAO,SAAS,KAAK,GAAG,MAAM,IAAI,MAAM,4CAA4C;CAEzF,OAAO,GAAG,IAAI,KAAK,KAAK,EAAE,YAAY,EAAE,MAAM,GAAG,EAAE,EAAE;AACvD;;AAGA,MAAM,YAAY,OAA6B,KAAoB,UAA4B;CAC7F,MAAM,SAAS,MAAM,SAAS;CAE9B,MAAM,WAAW;EAAE,GAAG,MAAM;EAAU,iBAAiB,SAAS;CAAE;CAElE,MAAM,OAAO;EACX,YAAY;EACZ,aAAa,EAAE,UAAU,OAAO,QAAQ,EAAE,EAAE;EAC5C,WAAW;EACX,WAAW;CACb;CAEA,OAAO,KAAK,KAAK,IAAI,OAAO,uBAAuB,OAAO;EAAE;EAAO,GAAG;CAAK,CAAC;AAC9E;AAEA,MAAM,eAAe,YACnB,mBAAmB;CACjB,IAAI,QAAQ;CACZ,MAAM,QAAQ;CACd,UAAU,QAAQ;CAClB,YAAY,QAAQ;CACpB,WAAW,QAAQ;CACnB,WAAW,QAAQ;AACrB,CAAC;AAEH,MAAM,YAAY,OAA6B,KAAoB,UAA+B;CAChG,IAAI,KAAK;CACT,SAAS,KAAK;CACd,YAAY,KAAK;CACjB,YAAY,KAAK;CACjB,WAAW,KAAK;CAChB,cAAc,KAAK;CACnB,iBAAiB,KAAK;CACtB,iBAAiB,KAAK;CACtB,QAAQ,IAAI,OAAO,kBACf,KAAK,OAAO,KAAI,SAAQ,MAAM,OAAO,MAAK,SAAQ,KAAK,SAAS,IAAI,GAAG,MAAM,IAAI,IACjF,KAAK;CACT,UAAU,KAAK;CACf,UAAU,KAAK;CACf,SAAS,KAAK;CACd,YAAY,KAAK;CACjB,UAAU,KAAK;CACf,cAAc,KAAK;CACnB,YAAY,KAAK;CACjB,KAAK,KAAK;CACV,UAAU,KAAK;CACf,aAAa,KAAK;CAClB,SAAS,KAAK;CACd,aAAa,KAAK;CAClB,YAAY,KAAK;CACjB,WAAW,KAAK;CAChB,cAAc,KAAK;AACrB;AAEA,MAAM,gBAAgB,UACpB,UAAU,KAAA,KAAa,UAAU,QAAQ,UAAU,SAAS,KAAK,KAAK,CAAC,MAAM,QAAQ,KAAK;AAE5F,MAAM,aAAa,gBACjB,aAAa,MAAM,KAAK,CAAC,EAAE,IAAI,KAAK,EAAE,YAAY;;;;;AAMpD,MAAM,cACJ,SACA,SACA,aACoC;CACpC,IAAI,UAAU,QAAQ,WAAW,MAAM,oBACrC,OAAO,YAAY,2DAA2D;CAGhF,IAAI,CAAC,aAAa,QAAQ,IAAI,GAC5B,OAAO,YAAY,0DAA0D;CAG/E,MAAM,OAAO,OAAO,KAAK,QAAQ,IAAI;CACrC,MAAM,UAAU,KAAK,MAAK,QAAO,CAAC,QAAQ,SAAS,GAAG,CAAC;CAEvD,IAAI,YAAY,KAAA,GACd,OAAO,YAAY,kBAAkB,QAAQ,+BAA+B;CAG9E,MAAM,UAAU,SAAS,MAAK,QAAO,CAAC,KAAK,SAAS,GAAG,CAAC;CAExD,OAAO,YAAY,KAAA,IACf,QAAQ,OACR,YAAY,mCAAmC,QAAQ,gCAAgC;AAC7F;AAEA,MAAM,UAAU,YACd,QAAQ,UAAU,YAAY,8CAA8C,IAAI,KAAA;;AAGlF,MAAM,WACJ,SACA,SACA,SACqC;CACrC,MAAM,MAAM,QAAQ,MAAM,IAAI,OAAO;CAErC,IAAI,QAAQ,MAAM,OAAO,KAAA;CAEzB,MAAM,QAAQ,OAAO,GAAG;CAExB,OAAO,oBAAoB,KAAK,GAAG,KAAK,QAAQ,KAAK,IACjD,QACA,YAAY,iBAAiB,KAAK,2BAA2B;AACnE;;AAGA,MAAM,qBAAqB,OAA6B,OACtD,kBAAkB,IAAI,uBAAuB,IACzC,MAAM,MAAM,MAAK,SAAQ,KAAK,OAAO,MAAM,CAAC,KAAK,OAAO,IACxD,KAAA;;AAGN,MAAM,kBAAkB,OAA6B,OACnD,kBAAkB,IAAI,0BAA0B,IAC5C,MAAM,SAAS,MAAK,YAAW,QAAQ,OAAO,EAAE,IAChD,KAAA;AAEN,MAAM,WAAW,MAA2B,UAC1C,KAAK,cAAc,MAAM,gBAAgB,KAAK,KAAK,MAAM,KAAK,KAAK,KAAK,KAAK,MAAM,KAAK,IAAI;AAI9F,MAAM,aAA2B,OAAO,SAAS,QAAQ;CACvD,MAAM,YAAY,QAAQ,MAAM,IAAI,YAAY;CAEhD,IAAI,cAAc,MAAM,OAAO,YAAY,mDAAmD;CAE9F,IAAI,CAAC,MAAM,SAAS,MAAK,YAAW,QAAQ,OAAO,SAAS,GAC1D,OAAO,YAAY,sDAAsD;CAG3E,MAAM,QAAQ,QAAQ,UAAS,UAAS,UAAU,WAAW,OAAO,SAAS,CAAC;CAE9E,IAAI,cAAc,KAAK,GAAG,OAAO;CASjC,IAAI,EAJF,UAAU,KAAA,IACN,eAAe,OAAO,SAAS,MAAM,KAAA,IACrC,cAAA,qBAGJ,OAAO,YACL,wHAEF;CAGF,MAAM,QAAQ,MAAM,MACjB,QACC,SAAQ,KAAK,eAAe,cAAc,IAAI,OAAO,sBAAsB,CAAC,KAAK,QACnF,EACC,KAAK,OAAO;CAEf,MAAM,YAAY,QAAQ,MAAM,IAAI,QAAQ;CAC5C,MAAM,SAAS,cAAc,OAAO,KAAA,IAAY,IAAI,QAAQ,IAAI,SAAS;CAGzE,IAAI,cAAc,QAAQ,WAAW,KAAA,GACnC,OAAO,YAAY,sDAAsD;CAG3E,IACE,WAAW,KAAA,MACV,OAAO,cAAc,aAAa,OAAO,UAAU,QAAQ,MAAM,IAAI,OAAO,IAE7E,OAAO,YAAY,gEAAgE;CAIrF,IAAI,UAAU,KAAA,GACZ,aACE,KAAK,KAAK;EAAE,SAAS,MAAM,KAAI,SAAQ,SAAS,OAAO,KAAK,IAAI,CAAC;EAAG,aAAa;CAAK,CAAC;CAG3F,MAAM,SAAS,WAAW,KAAA,KAAa,IAAI,OAAO,iBAAiB,IAAI,OAAO;CAC9E,MAAM,MAAM,SAAS;CAErB,aAAa;EACX,IAAI,OAAsB;EAE1B,IAAI,MAAM,MAAM,QAAQ;GACtB,OAAO,sBAAsB,OAAO,IAAI,cAAc,QAAQ,CAAC;GAC/D,IAAI,QAAQ,IAAI,MAAM;IAAE,IAAI;IAAM;IAAW,OAAO,OAAO,KAAK;IAAG,QAAQ;GAAI,CAAC;EAClF;EAEA,OAAO,KAAK,KAAK;GACf,SAAS,MAAM,MAAM,QAAQ,GAAG,EAAE,KAAI,SAAQ,SAAS,OAAO,KAAK,IAAI,CAAC;GACxE,aAAa;EACf,CAAC;CACH;AACF;AAEA,MAAM,WAAyB,OAAO,SAAS,QAAQ;CACrD,MAAM,KAAK,QAAQ,OAAO,UAAU;CACpC,MAAM,OAAO,MAAM,MAAM,MAAK,cAAa,UAAU,OAAO,EAAE;CAE9D,IAAI,SAAS,KAAA,GAAW,aAAa,SAAS,OAAO,KAAK,gBAAgB;CAE1E,OAAO,KAAK,UACR,YAAY,uCAAuC,UAC7C,KAAK,KAAK,SAAS,OAAO,KAAK,IAAI,CAAC;AAChD;AAEA,MAAM,cAA4B,OAAO,SAAS,QAAQ;CACxD,MAAM,OAAO,WAAW,SAAS;EAAC;EAAW;EAAc;CAAU,GAAG,CAAC,WAAW,YAAY,CAAC;CAEjG,IAAI,cAAc,IAAI,GAAG,OAAO;CAEhC,MAAM,EAAE,SAAS,YAAY,WAAW,UAAU,YAAY;CAE9D,IAAI,CAAC,UAAU,SAAS,OAAO,KAAK,YAAY,IAC9C,OAAO,YAAY,oCAAoC;CAGzD,IAAI,CAAC,UAAU,SAAS,SAAS,KAAK,eAAe,OAAO,SAAS,MAAM,KAAA,GACzE,OAAO,YAAY,4DAA4D;CAIjF,IAAI,MAAM,MAAM,MAAK,SAAQ,KAAK,eAAe,SAAS,GACxD,OAAO,YAAY,mDAAmD;CAGxE,IAAI,YAAY,KAAA,KAAa,YAAY,iBACvC,OAAO,YACL,uBAAuB,gBAAgB,sCACzC;CAGF,aAAa;EACX,MAAM,MAAM,UAAU,GAAG;EACzB,MAAM,SAAS,MAAM,SAAS;EAE9B,MAAM,OAAO,gBAAgB;GAC3B,IAAI,GAAG,0BAA0B,OAAO,QAAQ,CAAC;GACjD,QAAQ,MAAM;GACd;GACA;GACA,QAAQ,CAAC;GACT,SAAS;GACT,WAAW;GACX,KAAK,YAAY,KAAA,KAAa,IAAI,OAAO,YAAY,OAAO;GAC5D,YAAY;EACd,CAAC;EAED,MAAM,WAAW;GAAE,GAAG,MAAM;GAAU,gBAAgB,SAAS;EAAE;EACjE,MAAM,QAAQ,CAAC,GAAG,MAAM,OAAO,IAAI;EAEnC,OAAO,KAAK,KAAK,SAAS,OAAO,KAAK,IAAI,CAAC;CAC7C;AACF;AAEA,MAAM,cAA4B,OAAO,SAAS,QAAQ;CACxD,MAAM,OAAO,kBAAkB,OAAO,QAAQ,OAAO,UAAU,EAAE;CAEjE,IAAI,SAAS,KAAA,GACX,OAAO,YAAY,4DAA4D;CAGjF,MAAM,OAAO,WAAW,SAAS,CAAC,WAAW,cAAc,GAAG,CAAC,CAAC;CAEhE,IAAI,cAAc,IAAI,GAAG,OAAO;CAEhC,MAAM,EAAE,SAAS,cAAc,gBAAgB;CAE/C,IAAI,YAAY,KAAA,KAAa,gBAAgB,KAAA,GAC3C,OAAO,YAAY,2DAA2D;CAGhF,IAAI,YAAY,KAAA,MAAc,CAAC,UAAU,SAAS,OAAO,KAAK,YAAY,KACxE,OAAO,YAAY,oCAAoC;CAGzD,IAAI,gBAAgB,KAAA,KAAa,gBAAgB,qBAC/C,OAAO,YACL,2BAA2B,oBAAoB,sCACjD;CAIF,MAAM,UAA+B;EACnC,GAAG;EACH,SAAS,UAAU,SAAS,OAAO,IAAI,UAAU,KAAK;EACtD,KAAK,gBAAgB,KAAA,KAAa,IAAI,OAAO,YAAY,KAAK,MAAM;CACtE;CAEA,aAAa;EACX,MAAM,QAAQ,MAAM,MAAM,KAAI,SAAS,KAAK,OAAO,KAAK,KAAK,UAAU,IAAK;EAE5E,OAAO,KAAK,KAAK,SAAS,OAAO,KAAK,OAAO,CAAC;CAChD;AACF;AAEA,MAAM,aAA2B,OAAO,SAAS,QAAQ;CACvD,MAAM,UAAU,OAAO,OAAO;CAE9B,IAAI,YAAY,KAAA,GAAW,OAAO;CAElC,MAAM,OAAO,kBAAkB,OAAO,QAAQ,OAAO,UAAU,EAAE;CAEjE,IAAI,SAAS,KAAA,GACX,OAAO,YAAY,2DAA2D;CAGhF,aAAa;EACX,MAAM,SAA8B;GAAE,GAAG;GAAM,SAAS;GAAM,cAAc,UAAU,GAAG;EAAE;EAE3F,MAAM,QAAQ,MAAM,MAAM,KAAI,SAAS,KAAK,OAAO,KAAK,KAAK,SAAS,IAAK;EAE3E,OAAO,UAAU;CACnB;AACF;;AAGA,MAAM,cAA4B,OAAO,YAAY;CACnD,MAAM,QAAQ,QAAQ,UAAS,UAAS,SAAS,cAAc,aAAa,cAAc;CAE1F,IAAI,cAAc,KAAK,GAAG,OAAO;CAEjC,IAAI,UAAU,KAAA,GAAW,OAAO,YAAY,+CAA+C;CAE3F,OAAO,MAAM,OAAO,SAAS,QACzB,YAAY,2BAA2B,MAAM,iCAAiC,UACxE,KAAK,KAAK;EAAE,SAAS,MAAM;EAAQ,aAAa;CAAK,CAAC;AAClE;AAEA,MAAM,iBAA+B,OAAO,SAAS,QAAQ;CAC3D,MAAM,OAAO,WAAW,SAAS,CAAC,QAAQ,WAAW,GAAG,CAAC,QAAQ,WAAW,CAAC;CAE7E,IAAI,cAAc,IAAI,GAAG,OAAO;CAEhC,MAAM,EAAE,MAAM,WAAW,aAAa;CACtC,MAAM,QAAQ,UAAU,SAAS,IAAI,IAAI,uBAAuB,KAAK,IAAI,IAAI,KAAK,KAAA;CAElF,IAAI,CAAC,UAAU,SAAS,IAAI,KAAK,UAAU,KAAA,KAAa,MAAM,SAAS,IACrE,OAAO,YACL,kGAEF;CAIF,IACE,CAAC,UAAU,SAAS,QAAQ,KAC5B,kBAAkB,UAAU,0BAA0B,KACtD,CAAC,MAAM,SAAS,MAAK,SAAQ,KAAK,OAAO,QAAQ,GAEjD,OAAO,YAAY,yEAAyE;CAI9F,IAAI,MAAM,SAAS,MAAK,SAAQ,KAAK,cAAc,QAAQ,GACzD,OAAO,YAAY,uDAAuD;CAG5E,aAAa;EACX,MAAM,MAAM,UAAU,GAAG;EACzB,MAAM,SAAS,MAAM,SAAS;EAE9B,MAAM,UAAU,mBAAmB;GACjC,IAAI,GAAG,6BAA6B,OAAO,QAAQ,CAAC;GACpD;GACA;GACA,YAAY;GACZ,WAAW;GACX,WAAW;EACb,CAAC;EAED,MAAM,WAAW;GAAE,GAAG,MAAM;GAAU,mBAAmB,SAAS;EAAE;EACpE,MAAM,WAAW,CAAC,GAAG,MAAM,UAAU,OAAO;EAE5C,OAAO,KACL,KACA,IAAI,OAAO,oBACP;GAAE,GAAG,YAAY,OAAO;GAAG,WAAW;EAAK,IAC3C,YAAY,OAAO,CACzB;CACF;AACF;AAEA,MAAM,cAA4B,OAAO,SAAS,QAAQ;CACxD,MAAM,KAAK,QAAQ,OAAO,aAAa;CAEvC,IAAI,CAAC,MAAM,SAAS,MAAK,SAAQ,KAAK,OAAO,EAAE,GAC7C,aAAa,SAAS,OAAO,KAAK,mBAAmB;CAIvD,MAAM,UAAU,eAAe,OAAO,EAAE;CAExC,OAAO,YAAY,KAAA,IACf,YAAY,0EAA0E,UAChF,KAAK,KAAK,YAAY,OAAO,CAAC;AAC1C;AAEA,MAAM,iBAA+B,OAAO,SAAS,QAAQ;CAC3D,MAAM,UAAU,OAAO,OAAO;CAE9B,IAAI,YAAY,KAAA,GAAW,OAAO;CAElC,MAAM,UAAU,eAAe,OAAO,QAAQ,OAAO,aAAa,EAAE;CAGpE,IAAI,YAAY,KAAA,GACd,OAAO,YAAY,sDAAsD;CAG3E,aAAa;EACX,MAAM,WAAW,MAAM,SAAS,QAAO,SAAQ,KAAK,OAAO,QAAQ,EAAE;EAErE,IAAI,CAAC,IAAI,OAAO,kBACd,MAAM,QAAQ,MAAM,MAAM,QAAO,SAAQ,KAAK,eAAe,QAAQ,EAAE;EAGzE,OAAO,UAAU;CACnB;AACF;;AAGA,MAAM,gBAAgB,aAA4C;CAChE,QAAQ,QAAQ;CAChB,OAAO,QAAQ;CACf,MAAM,QAAQ;CAEd,SAAS,QAAQ,SAAS,KAAA;CAC1B,aAAa,QAAQ,OAAO,cAAc,KAAK;AACjD;AAEA,MAAM,SACJ,QACA,MACA,OACA,SACA,SACA,YAAmC,CAAC,OACf;CACrB,OAAO,cACL;EACE;EACA,MAAM,GAAG,0BAA0B;EACnC,MAAM;EACN;EACA;EACA,UAAU;CACZ,GACA,iBACA,eACC,OAAO,SAAS,YAAY,QAAQ,OAAO,SAAS,QAAQ,GAAG,CAClE;CACA;AACF;;AAGA,MAAa,mBAAmD;CAC9D,MAAM,OAAO,UAAU,OAAO,CAAC,YAAY,aAAa,GAAG,WAAW;EACpE;EACA;EACA;CACF,CAAC;CACD,MAAM,QAAQ,UAAU,MAAM;EAAC;EAAe;EAAS;CAAU,GAAG,UAAU;CAC9E,MACE,OACA,mBACA,OACA;EAAC;EAAc;EAAY;EAAe;CAAU,GACpD,OACF;CACA,MAAM,QAAQ,mBAAmB,MAAM,CAAC,eAAe,OAAO,GAAG,UAAU;CAC3E,MAAM,QAAQ,yBAAyB,MAAM,CAAC,aAAa,GAAG,SAAS;CACvE,MAAM,OAAO,WAAW,OAAO,CAAC,UAAU,GAAG,YAAY,CAAC,OAAO,CAAC;CAClE,MAAM,QAAQ,aAAa,MAAM,cAAc,aAAa;CAC5D,MAAM,OAAO,yBAAyB,OAAO,cAAc,UAAU;CACrE,MAAM,UAAU,yBAAyB,MAAM,cAAc,aAAa;AAC5E;AAEA,MAAM,iBAAiB,iBAAiB,KAAI,eAAc;CACxD,OAAO;CACP,SAAS,IAAI,OAAO,IAAI,UAAU,MAAM,KAAK,QAAQ,kBAAkB,SAAS,EAAE,EAAE;CACpF,OAAO,CAAC,GAAG,UAAU,MAAM,KAAK,SAAS,kBAAkB,CAAC,EAAE,KAAI,UAAS,MAAM,MAAM,EAAE;AAC3F,EAAE;;;;;AAYF,MAAM,mBAAwC,IAAI,IAAI;CACpD;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;AAOD,MAAa,qBACX,QACA,SACoC;CACpC,KAAK,MAAM,aAAa,gBAAgB;EACtC,IAAI,UAAU,MAAM,MAAM,WAAW,OAAO,YAAY,GAAG;EAE3D,MAAM,QAAQ,UAAU,QAAQ,KAAK,IAAI;EAEzC,IAAI,UAAU,MAAM;EAEpB,MAAM,SAAiC,CAAC;EAExC,KAAK,MAAM,CAAC,OAAO,SAAS,UAAU,MAAM,QAAQ,GAAG;GAErD,MAAM,QAAQ,MAAM,QAAQ,MAAM;GAElC,IAAI,CAAC,iBAAiB,KAAK,KAAK,KAAK,iBAAiB,IAAI,KAAK,GAC7D;GAGF,OAAO,QAAQ;EACjB;EAEA,OAAO;GAAE,OAAO,UAAU;GAAO;EAAO;CAC1C;AAGF;;;;;AAMA,MAAa,uBACX,OACA,UACuB;CACvB,MAAM,OAAO,CAAC,GAAG,MAAM,KAAK,CAAC;CAC7B,MAAM,UAAU,KAAK,MAAK,QAAO,CAAC,MAAM,UAAU,SAAS,GAAG,CAAC;CAE/D,IAAI,YAAY,KAAA,GAAW,OAAO,uBAAuB,QAAQ;CAEjE,MAAM,WAAW,KAAK,MAAM,KAAK,UAAU,KAAK,QAAQ,GAAG,MAAM,KAAK;CAEtE,OAAO,aAAa,KAAA,IAChB,KAAA,IACA,gCAAgC,SAAS;AAC/C"}
|