@pithy-sh/cloudflare 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/ai/aiManager.d.ts +99 -0
  2. package/dist/ai/aiManager.d.ts.map +1 -0
  3. package/dist/ai/aiManager.js +148 -0
  4. package/dist/ai/vectorizeManager.d.ts +67 -0
  5. package/dist/ai/vectorizeManager.d.ts.map +1 -0
  6. package/dist/ai/vectorizeManager.js +101 -0
  7. package/dist/ai/vectorizeProvisioner.d.ts +123 -0
  8. package/dist/ai/vectorizeProvisioner.d.ts.map +1 -0
  9. package/dist/ai/vectorizeProvisioner.js +195 -0
  10. package/dist/client/accounts.d.ts +48 -0
  11. package/dist/client/accounts.d.ts.map +1 -0
  12. package/dist/client/accounts.js +55 -0
  13. package/dist/client/clients.d.ts +122 -0
  14. package/dist/client/clients.d.ts.map +1 -0
  15. package/dist/client/clients.js +216 -0
  16. package/dist/client/errors.d.ts +73 -0
  17. package/dist/client/errors.d.ts.map +1 -0
  18. package/dist/client/errors.js +98 -0
  19. package/dist/client/manager.d.ts +45 -0
  20. package/dist/client/manager.d.ts.map +1 -0
  21. package/dist/client/manager.js +51 -0
  22. package/dist/d1/d1Manager.d.ts +74 -0
  23. package/dist/d1/d1Manager.d.ts.map +1 -0
  24. package/dist/d1/d1Manager.js +128 -0
  25. package/dist/d1/d1PreparedStatement.d.ts +24 -0
  26. package/dist/d1/d1PreparedStatement.d.ts.map +1 -0
  27. package/dist/d1/d1PreparedStatement.js +91 -0
  28. package/dist/d1/d1Provisioner.d.ts +28 -0
  29. package/dist/d1/d1Provisioner.d.ts.map +1 -0
  30. package/dist/d1/d1Provisioner.js +67 -0
  31. package/dist/email/emailRoutingManager.d.ts +36 -0
  32. package/dist/email/emailRoutingManager.d.ts.map +1 -0
  33. package/dist/email/emailRoutingManager.js +124 -0
  34. package/dist/email/emailSendManager.d.ts +45 -0
  35. package/dist/email/emailSendManager.d.ts.map +1 -0
  36. package/dist/email/emailSendManager.js +52 -0
  37. package/dist/env/devVars.d.ts +65 -0
  38. package/dist/env/devVars.d.ts.map +1 -0
  39. package/dist/env/devVars.js +84 -0
  40. package/dist/hostnames/customHostnamesManager.d.ts +51 -0
  41. package/dist/hostnames/customHostnamesManager.d.ts.map +1 -0
  42. package/dist/hostnames/customHostnamesManager.js +84 -0
  43. package/dist/kv/kvManager.d.ts +76 -0
  44. package/dist/kv/kvManager.d.ts.map +1 -0
  45. package/dist/kv/kvManager.js +135 -0
  46. package/dist/kv/kvProvisioner.d.ts +28 -0
  47. package/dist/kv/kvProvisioner.d.ts.map +1 -0
  48. package/dist/kv/kvProvisioner.js +71 -0
  49. package/dist/media/assetSeeder.d.ts +31 -0
  50. package/dist/media/assetSeeder.d.ts.map +1 -0
  51. package/dist/media/assetSeeder.js +70 -0
  52. package/dist/media/imageManager.d.ts +34 -0
  53. package/dist/media/imageManager.d.ts.map +1 -0
  54. package/dist/media/imageManager.js +103 -0
  55. package/dist/media/ownership.d.ts +40 -0
  56. package/dist/media/ownership.d.ts.map +1 -0
  57. package/dist/media/ownership.js +46 -0
  58. package/dist/media/streamManager.d.ts +83 -0
  59. package/dist/media/streamManager.d.ts.map +1 -0
  60. package/dist/media/streamManager.js +145 -0
  61. package/dist/queue/queueManager.d.ts +84 -0
  62. package/dist/queue/queueManager.d.ts.map +1 -0
  63. package/dist/queue/queueManager.js +114 -0
  64. package/dist/r2/r2Credentials.d.ts +12 -0
  65. package/dist/r2/r2Credentials.d.ts.map +1 -0
  66. package/dist/r2/r2Credentials.js +13 -0
  67. package/dist/r2/r2Manager.d.ts +226 -0
  68. package/dist/r2/r2Manager.d.ts.map +1 -0
  69. package/dist/r2/r2Manager.js +405 -0
  70. package/dist/r2/r2Provisioner.d.ts +39 -0
  71. package/dist/r2/r2Provisioner.d.ts.map +1 -0
  72. package/dist/r2/r2Provisioner.js +86 -0
  73. package/dist/secrets/secretsStoreManager.d.ts +82 -0
  74. package/dist/secrets/secretsStoreManager.d.ts.map +1 -0
  75. package/dist/secrets/secretsStoreManager.js +147 -0
  76. package/dist/secrets/secretsStores.d.ts +47 -0
  77. package/dist/secrets/secretsStores.d.ts.map +1 -0
  78. package/dist/secrets/secretsStores.js +65 -0
  79. package/dist/test-utils/emailRoutingRules.d.ts +39 -0
  80. package/dist/test-utils/emailRoutingRules.d.ts.map +1 -0
  81. package/dist/test-utils/emailRoutingRules.js +89 -0
  82. package/dist/test-utils/fixtureReportSetup.d.ts +25 -0
  83. package/dist/test-utils/fixtureReportSetup.d.ts.map +1 -0
  84. package/dist/test-utils/fixtureReportSetup.js +30 -0
  85. package/dist/test-utils/fixtures.d.ts +224 -0
  86. package/dist/test-utils/fixtures.d.ts.map +1 -0
  87. package/dist/test-utils/fixtures.js +277 -0
  88. package/dist/test-utils/harness.d.ts +192 -0
  89. package/dist/test-utils/harness.d.ts.map +1 -0
  90. package/dist/test-utils/harness.js +351 -0
  91. package/dist/test-utils/inboundRecorder.d.ts +91 -0
  92. package/dist/test-utils/inboundRecorder.d.ts.map +1 -0
  93. package/dist/test-utils/inboundRecorder.js +141 -0
  94. package/dist/test-utils/integrationSetup.d.ts +22 -0
  95. package/dist/test-utils/integrationSetup.d.ts.map +1 -0
  96. package/dist/test-utils/integrationSetup.js +37 -0
  97. package/dist/test-utils/reap.d.ts +112 -0
  98. package/dist/test-utils/reap.d.ts.map +1 -0
  99. package/dist/test-utils/reap.js +185 -0
  100. package/dist/tokens/accountTokensManager.d.ts +157 -0
  101. package/dist/tokens/accountTokensManager.d.ts.map +1 -0
  102. package/dist/tokens/accountTokensManager.js +280 -0
  103. package/dist/tokens/permissions.d.ts +36 -0
  104. package/dist/tokens/permissions.d.ts.map +1 -0
  105. package/dist/tokens/permissions.js +50 -0
  106. package/dist/tokens/profiles.d.ts +102 -0
  107. package/dist/tokens/profiles.d.ts.map +1 -0
  108. package/dist/tokens/profiles.js +137 -0
  109. package/dist/turnstile/turnstileManager.d.ts +75 -0
  110. package/dist/turnstile/turnstileManager.d.ts.map +1 -0
  111. package/dist/turnstile/turnstileManager.js +138 -0
  112. package/dist/user/userManager.d.ts +36 -0
  113. package/dist/user/userManager.d.ts.map +1 -0
  114. package/dist/user/userManager.js +58 -0
  115. package/dist/workers/buildsManager.d.ts +116 -0
  116. package/dist/workers/buildsManager.d.ts.map +1 -0
  117. package/dist/workers/buildsManager.js +237 -0
  118. package/dist/workers/buildsTypes.d.ts +102 -0
  119. package/dist/workers/buildsTypes.d.ts.map +1 -0
  120. package/dist/workers/buildsTypes.js +87 -0
  121. package/dist/workers/workersBuildEvent.d.ts +64 -0
  122. package/dist/workers/workersBuildEvent.d.ts.map +1 -0
  123. package/dist/workers/workersBuildEvent.js +47 -0
  124. package/dist/workers/workersManager.d.ts +136 -0
  125. package/dist/workers/workersManager.d.ts.map +1 -0
  126. package/dist/workers/workersManager.js +312 -0
  127. package/dist/workers/workersProvisioner.d.ts +80 -0
  128. package/dist/workers/workersProvisioner.d.ts.map +1 -0
  129. package/dist/workers/workersProvisioner.js +106 -0
  130. package/dist/workflows/stepFailure.d.ts +180 -0
  131. package/dist/workflows/stepFailure.d.ts.map +1 -0
  132. package/dist/workflows/stepFailure.js +227 -0
  133. package/dist/workflows/workflowsClient.d.ts +87 -0
  134. package/dist/workflows/workflowsClient.d.ts.map +1 -0
  135. package/dist/workflows/workflowsClient.js +156 -0
  136. package/dist/zones/zonesManager.d.ts +50 -0
  137. package/dist/zones/zonesManager.d.ts.map +1 -0
  138. package/dist/zones/zonesManager.js +74 -0
  139. package/package.json +18 -9
@@ -0,0 +1,192 @@
1
+ import { R2Credentials } from "../r2/r2Credentials";
2
+ /**
3
+ * The live-suite credentials: this package's own `.dev.vars`, then `process.env` per key for anything it
4
+ * did not set. The `.dev.vars` half is why this is here rather than in `../env/devVars` — nothing outside
5
+ * a live test reads a `.dev.vars` for credentials now, and a shared function that still did would be an
6
+ * invitation to point a command back at the checkout.
7
+ *
8
+ * Exported for `fixtures.ts`, which resolves the whole fixture estate against the same pair. Two readers
9
+ * of two files is how a suite and its own report come to disagree about what this run has.
10
+ */
11
+ export declare function loadIntegrationEnv(): Record<string, string>;
12
+ /** Cloudflare credentials for a live run, plus whether enough of them are present to run at all. */
13
+ export interface IntegrationCreds {
14
+ /** The target account id (`CLOUDFLARE_ACCOUNT_ID`). Empty string when unset. */
15
+ accountId: string;
16
+ /** The scoped API token (`CLOUDFLARE_API_TOKEN`). Empty string when unset. */
17
+ apiToken: string;
18
+ /** The Secrets Store id (`SECRETS_STORE_ID`), for the secrets-store live test. Empty string when unset. */
19
+ secretsStoreId: string;
20
+ /** R2 S3 keys (from `R2_CREDENTIALS` JSON), for the R2 presigned-URL test. Null when unset. */
21
+ r2: R2Credentials | null;
22
+ /** True only when both an account id and a token are present — the gate for `describe.skipIf`. */
23
+ hasCreds: boolean;
24
+ }
25
+ /**
26
+ * Parse the `R2_CREDENTIALS` JSON blob through the canonical {@link R2Credentials} schema — the same
27
+ * object the R2 storage feature manages as a JSON payload, so the harness and the feature validate it
28
+ * identically. Returns null when unset (the R2 suite then skips); a set-but-malformed/invalid blob
29
+ * fails `.parse()` and throws, surfacing the misconfiguration loudly rather than silently skipping.
30
+ */
31
+ export declare function parseR2Creds(raw: string | undefined): R2Credentials | null;
32
+ /**
33
+ * Load the live-CF credentials a `*.integration.test.ts` needs, reading the package `.dev.vars` and
34
+ * overlaying `process.env` (the loader's own fallback, so CI passes them as plain env vars). Gate the
35
+ * suite with `describe.skipIf(!loadIntegrationCreds().hasCreds)` so it skips cleanly with no creds.
36
+ */
37
+ export declare function loadIntegrationCreds(): IntegrationCreds;
38
+ /**
39
+ * The project name a live test provisions under, so that names composed by the *product's* provisioners —
40
+ * `<project>-<env>-<thing>`, where the project segment is verbatim and first — land inside the reservation
41
+ * too. A live test of `pithy provision --feature` cannot route through {@link uniqueName}: the names are the
42
+ * thing under test. Giving it a reserved project puts them in the namespace anyway.
43
+ *
44
+ * The reservation itself is {@link RESERVED_TEST_PREFIX}, in `@pithy-sh/core`. It lives beside the name
45
+ * composer because both sides of it are naming facts: this harness mints inside it, and `scaffoldProject`
46
+ * refuses to mint a project that would land in it. One literal, or the two drift apart.
47
+ */
48
+ export declare const RESERVED_TEST_PROJECT = "pithy-int-test";
49
+ /**
50
+ * The compatibility date every throwaway Worker a live suite uploads runs on.
51
+ *
52
+ * `createWorker` has no default any more (#396): a compatibility date is a behavior contract in
53
+ * somebody's account, so the caller names it. That makes every live suite a caller, and one place for
54
+ * them to read it from is the difference between one date and six.
55
+ *
56
+ * **It is stated rather than imported, and the reason is a wall rather than a preference.** The floor
57
+ * lives in `compatibility.ts` at the repository root, and this package's `rootDir` is `src` — a static
58
+ * import of a file above it is TS6059, the same wall `compatibilityDates.test.ts` records for
59
+ * `packages/cli`. So this is a fixture stating a date, which is the one shape that gate excludes on
60
+ * purpose. Keep it level with the floor when the floor moves.
61
+ */
62
+ export declare const INTEGRATION_COMPATIBILITY_DATE = "2026-06-01";
63
+ /**
64
+ * Mint a collision-proof name for a throwaway live resource:
65
+ * `pithy-int-<label>-<timestamp>-<counter>-<rand>`.
66
+ *
67
+ * The caller supplies only the distinguishing part — {@link RESERVED_TEST_PREFIX} is composed on, never
68
+ * passed in. That is the point: when the prefix was a *default* a caller could override, a suite could
69
+ * silently create resources under a name the reaper does not recognize, which is exactly how orphans
70
+ * came to sit on a real account. A label that already carries the prefix is refused rather than doubled.
71
+ *
72
+ * The 6-char random suffix guarantees uniqueness across separate test files (Vitest isolates each file, so
73
+ * the counter only deduplicates within one); the timestamp is what {@link testResourceAge} reads, so every
74
+ * name this mints is reapable by construction. The label is kebabbed and fitted, so the result always
75
+ * stays inside R2's {@link MAX_RESOURCE_NAME} cap and the lowercase `a-z0-9-` charset every CF namespace
76
+ * accepts, whatever the caller passed.
77
+ */
78
+ export declare function uniqueName(label: string): string;
79
+ /**
80
+ * Create a live resource, exercise it, and guarantee teardown — even if `exercise` throws. This is the
81
+ * spine of every live integration test: a failed assertion must never orphan a real Cloudflare resource.
82
+ * `create` runs outside the `try`, so a creation failure does not call `teardown` on something that was
83
+ * never created; once `create` resolves, `teardown` always runs in the `finally`. The exercise result is
84
+ * returned for the rare caller that wants it.
85
+ */
86
+ export declare function withThrowawayResource<R, T>(create: () => Promise<R>, exercise: (resource: R) => Promise<T>, teardown: (resource: R) => Promise<void>): Promise<T>;
87
+ /**
88
+ * Create a live resource **at a name the caller already chose**, exercise it, and guarantee teardown —
89
+ * including when `create` itself rejects.
90
+ *
91
+ * {@link withThrowawayResource} runs `create` outside the `try` on purpose: for a kind whose id only
92
+ * exists once creation succeeds, tearing down a failed create would be tearing down nothing. But a whole
93
+ * family of Cloudflare calls are **named writes** — `putSecret(name, …)`, `createBucket(name)`,
94
+ * `createDatabase(name)`, `createWorker(name)` — where the address is the caller's own argument. There a
95
+ * rejection is ambiguous: the server may have accepted the write and failed on the way back, and the
96
+ * resource now exists under a name the test is still holding. `withThrowawayResource` walks away from it.
97
+ *
98
+ * This variant tears down on the **name**, and arms the teardown before `create` is ever called, so the
99
+ * ambiguous case cleans up. `teardown` must therefore tolerate a resource that was never created — every
100
+ * caller here passes an idempotent delete, and the reaper is the backstop if one is not.
101
+ */
102
+ export declare function withNamedResource<T>(name: string, create: (name: string) => Promise<unknown>, exercise: (name: string) => Promise<T>, teardown: (name: string) => Promise<void>): Promise<T>;
103
+ /**
104
+ * How old a resource must be before a reaper will remove it.
105
+ *
106
+ * **Twelve hours, and the size is the safety argument.** Reaping is a race with any suite that is still
107
+ * running: a resource deleted mid-flight surfaces as an unexplained 404 in a test that was passing, which
108
+ * is a far worse failure than debris. Cloudflare offers no way to mark a D1, KV namespace, bucket, or
109
+ * index as owned by a live process — no lease, no tag our client can set on every kind — so the only
110
+ * defense available is a window nothing plausible can cross.
111
+ *
112
+ * A single live test is capped at 120s and a hook at 120s (`vitest.integration.config.ts`); a whole
113
+ * package's suite is minutes, and every package's suites together are well under an hour even when
114
+ * Vectorize is settling. Twelve hours is two orders of magnitude past that, and still short enough that
115
+ * debris never survives a day. The cost of the wider window is a handful of orphans lingering longer;
116
+ * the cost of the narrower one is a green suite turned red by its own housekeeping.
117
+ */
118
+ export declare const DEFAULT_STALE_AFTER_MS: number;
119
+ /**
120
+ * The ms-epoch {@link uniqueName} embedded, or `null` if this is not one of our names.
121
+ *
122
+ * `uniqueName` composes `pithy-int-<label>-<epoch>-<counter>-<rand>`, so the timestamp is the third
123
+ * segment from the end. Anything that does not match that shape returns `null` and is therefore never
124
+ * reaped — including a name inside the reservation that some *other* generator produced, such as a
125
+ * feature resource provisioned under {@link RESERVED_TEST_PROJECT}. Conservative on purpose: failing to
126
+ * clean up costs pennies, deleting someone's data does not.
127
+ */
128
+ export declare function testResourceAge(name: string, now: number): number | null;
129
+ /**
130
+ * Which of these names are stale test debris — ours, and old enough that no running suite owns them.
131
+ *
132
+ * Pure, so the age arithmetic is unit-tested without touching Cloudflare.
133
+ */
134
+ export declare function staleTestResourceNames(names: readonly string[], options?: {
135
+ now?: number;
136
+ staleAfterMs?: number;
137
+ }): string[];
138
+ /** One reapable resource kind: what it is called, how to list it, and how to remove one. */
139
+ export interface ReapableKind {
140
+ /** Human label for the reap log, e.g. "Vectorize index". */
141
+ label: string;
142
+ /** Every resource name of this kind currently in the account. */
143
+ list: () => Promise<string[]>;
144
+ /** Remove one by name. Must be idempotent — another runner may have reaped it first. */
145
+ remove: (name: string) => Promise<void>;
146
+ }
147
+ /**
148
+ * Remove stale throwaway resources of one kind, and report what went.
149
+ *
150
+ * `withThrowawayResource` guarantees teardown only while the process lives. A run killed by a test
151
+ * timeout, a Ctrl-C, or a crash orphans whatever it had created — which is exactly how two Vectorize
152
+ * indexes came to sit on a real account for a month. Calling this in a suite's `beforeAll` makes each
153
+ * run clean up after the last one, so the failure mode self-heals instead of accumulating.
154
+ *
155
+ * Never throws: a reaper that fails must not fail the suite it is trying to help. Failures are reported
156
+ * in the returned list of what could not be removed.
157
+ */
158
+ export declare function reapStaleTestResources(kind: ReapableKind, options?: {
159
+ now?: number;
160
+ staleAfterMs?: number;
161
+ }): Promise<{
162
+ reaped: string[];
163
+ failed: string[];
164
+ }>;
165
+ /**
166
+ * Empty a throwaway bucket so R2 will delete it.
167
+ *
168
+ * Two things hold a bucket against deletion: stored objects, and the parts of a multipart upload that was
169
+ * never completed or aborted — precisely what a test that failed mid-upload leaves behind. Both are
170
+ * cleared, dangling uploads first. Every page is drained, so a bucket with more than a thousand keys is
171
+ * emptied rather than half-emptied.
172
+ *
173
+ * Throws when the S3 key pair is absent: a bucket that cannot be emptied cannot be deleted, and silence
174
+ * there is how debris becomes permanent.
175
+ */
176
+ export declare function emptyTestBucket(creds: IntegrationCreds, bucketName: string): Promise<void>;
177
+ /**
178
+ * Reclaim stale throwaway R2 buckets — the one kind that needs more than an API token, so it lives here
179
+ * once instead of being re-derived by every suite that makes a bucket.
180
+ *
181
+ * Without `R2_CREDENTIALS` this reaps nothing and says so loudly rather than failing quietly: the buckets
182
+ * are still there, still cost money, and the only fix is the key pair. Never throws — a reaper must not
183
+ * fail the suite it is trying to help.
184
+ */
185
+ export declare function reapStaleTestBuckets(creds: IntegrationCreds, options?: {
186
+ now?: number;
187
+ staleAfterMs?: number;
188
+ }): Promise<{
189
+ reaped: string[];
190
+ failed: string[];
191
+ }>;
192
+ //# sourceMappingURL=harness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harness.d.ts","sourceRoot":"","sources":["../../src/test-utils/harness.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,351 @@
1
+ import { R2Credentials } from "../r2/r2Credentials.js";
2
+ import { CloudflareClients } from "../client/clients.js";
3
+ import { CLOUDFLARE_ENV_KEYS, parseDevVars } from "../env/devVars.js";
4
+ import { ValidationError } from "@pithy-sh/core/src/error/pithyError";
5
+ import { AbortMultipartUploadCommand, DeleteObjectCommand, ListMultipartUploadsCommand, ListObjectsV2Command, S3Client } from "@aws-sdk/client-s3";
6
+ import { readFileSync } from "node:fs";
7
+ import path from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+ import { MAX_RESOURCE_NAME, RESERVED_TEST_PREFIX, fitSegment, kebab } from "@pithy-sh/core/src/naming/resource";
10
+ //#region src/test-utils/harness.ts
11
+ /**
12
+ * Shared scaffolding for the package's `*.integration.test.ts` live suites. Every live test does the
13
+ * same three things — find credentials, create a throwaway resource, and guarantee it is torn down —
14
+ * so that logic lives here once instead of being re-derived per manager. Generalized from the D1
15
+ * provisioner's first live test; see `README.md` § "Live integration tests" for the template.
16
+ */
17
+ /**
18
+ * **This** package's root, whichever package's live suite is running — `@pithy-sh/storage` imports this
19
+ * harness, so its credentials come from `packages/cloudflare/.dev.vars` too, and a `.dev.vars` beside the
20
+ * importing package is read by nothing. See `README.md` § "Live integration tests".
21
+ *
22
+ * Nothing creates that file, and nothing in the CLI reads one any more — adopter credentials moved to
23
+ * the account-scoped `<config>/cloudflare.json` (#182), and this package cannot resolve that directory
24
+ * without a second implementation of where Pithy's config lives. So this stays a **maintainer-authored
25
+ * file for the live suites and nothing else**: no command writes it, no adopter has one, and it holds
26
+ * whatever account the person running `test:integration` wants the throwaway resources created in.
27
+ * Write it by hand, or export the keys — {@link loadIntegrationEnv} overlays `process.env` per key for
28
+ * anything the file does not set, which is how CI runs with no file at all.
29
+ */
30
+ const PACKAGE_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "../..");
31
+ /**
32
+ * The live-suite credentials: this package's own `.dev.vars`, then `process.env` per key for anything it
33
+ * did not set. The `.dev.vars` half is why this is here rather than in `../env/devVars` — nothing outside
34
+ * a live test reads a `.dev.vars` for credentials now, and a shared function that still did would be an
35
+ * invitation to point a command back at the checkout.
36
+ *
37
+ * Exported for `fixtures.ts`, which resolves the whole fixture estate against the same pair. Two readers
38
+ * of two files is how a suite and its own report come to disagree about what this run has.
39
+ */
40
+ function loadIntegrationEnv() {
41
+ let vars = {};
42
+ try {
43
+ vars = parseDevVars(readFileSync(path.join(PACKAGE_ROOT, ".dev.vars"), "utf8"));
44
+ } catch {}
45
+ for (const key of CLOUDFLARE_ENV_KEYS) {
46
+ const fromEnv = process.env[key];
47
+ if (!vars[key] && fromEnv) vars[key] = fromEnv;
48
+ }
49
+ return vars;
50
+ }
51
+ /**
52
+ * Parse the `R2_CREDENTIALS` JSON blob through the canonical {@link R2Credentials} schema — the same
53
+ * object the R2 storage feature manages as a JSON payload, so the harness and the feature validate it
54
+ * identically. Returns null when unset (the R2 suite then skips); a set-but-malformed/invalid blob
55
+ * fails `.parse()` and throws, surfacing the misconfiguration loudly rather than silently skipping.
56
+ */
57
+ function parseR2Creds(raw) {
58
+ if (!raw) return null;
59
+ return R2Credentials.parse(JSON.parse(raw));
60
+ }
61
+ /**
62
+ * Load the live-CF credentials a `*.integration.test.ts` needs, reading the package `.dev.vars` and
63
+ * overlaying `process.env` (the loader's own fallback, so CI passes them as plain env vars). Gate the
64
+ * suite with `describe.skipIf(!loadIntegrationCreds().hasCreds)` so it skips cleanly with no creds.
65
+ */
66
+ function loadIntegrationCreds() {
67
+ const vars = loadIntegrationEnv();
68
+ const accountId = vars.CLOUDFLARE_ACCOUNT_ID ?? "";
69
+ const apiToken = vars.CLOUDFLARE_API_TOKEN ?? "";
70
+ return {
71
+ accountId,
72
+ apiToken,
73
+ secretsStoreId: vars.SECRETS_STORE_ID ?? "",
74
+ r2: parseR2Creds(vars.R2_CREDENTIALS),
75
+ hasCreds: Boolean(accountId && apiToken)
76
+ };
77
+ }
78
+ /**
79
+ * The project name a live test provisions under, so that names composed by the *product's* provisioners —
80
+ * `<project>-<env>-<thing>`, where the project segment is verbatim and first — land inside the reservation
81
+ * too. A live test of `pithy provision --feature` cannot route through {@link uniqueName}: the names are the
82
+ * thing under test. Giving it a reserved project puts them in the namespace anyway.
83
+ *
84
+ * The reservation itself is {@link RESERVED_TEST_PREFIX}, in `@pithy-sh/core`. It lives beside the name
85
+ * composer because both sides of it are naming facts: this harness mints inside it, and `scaffoldProject`
86
+ * refuses to mint a project that would land in it. One literal, or the two drift apart.
87
+ */
88
+ const RESERVED_TEST_PROJECT = `${RESERVED_TEST_PREFIX}test`;
89
+ /**
90
+ * The compatibility date every throwaway Worker a live suite uploads runs on.
91
+ *
92
+ * `createWorker` has no default any more (#396): a compatibility date is a behavior contract in
93
+ * somebody's account, so the caller names it. That makes every live suite a caller, and one place for
94
+ * them to read it from is the difference between one date and six.
95
+ *
96
+ * **It is stated rather than imported, and the reason is a wall rather than a preference.** The floor
97
+ * lives in `compatibility.ts` at the repository root, and this package's `rootDir` is `src` — a static
98
+ * import of a file above it is TS6059, the same wall `compatibilityDates.test.ts` records for
99
+ * `packages/cli`. So this is a fixture stating a date, which is the one shape that gate excludes on
100
+ * purpose. Keep it level with the floor when the floor moves.
101
+ */
102
+ const INTEGRATION_COMPATIBILITY_DATE = "2026-06-01";
103
+ /** Deduplicates names minted within one test file; the random suffix covers the cross-file case. */
104
+ let nameCounter = 0;
105
+ /**
106
+ * Mint a collision-proof name for a throwaway live resource:
107
+ * `pithy-int-<label>-<timestamp>-<counter>-<rand>`.
108
+ *
109
+ * The caller supplies only the distinguishing part — {@link RESERVED_TEST_PREFIX} is composed on, never
110
+ * passed in. That is the point: when the prefix was a *default* a caller could override, a suite could
111
+ * silently create resources under a name the reaper does not recognize, which is exactly how orphans
112
+ * came to sit on a real account. A label that already carries the prefix is refused rather than doubled.
113
+ *
114
+ * The 6-char random suffix guarantees uniqueness across separate test files (Vitest isolates each file, so
115
+ * the counter only deduplicates within one); the timestamp is what {@link testResourceAge} reads, so every
116
+ * name this mints is reapable by construction. The label is kebabbed and fitted, so the result always
117
+ * stays inside R2's {@link MAX_RESOURCE_NAME} cap and the lowercase `a-z0-9-` charset every CF namespace
118
+ * accepts, whatever the caller passed.
119
+ */
120
+ function uniqueName(label) {
121
+ const segment = kebab(label);
122
+ if (!segment) throw new ValidationError({
123
+ message: "A throwaway resource needs a label.",
124
+ action: "Pass a short label, e.g. uniqueName('kv').",
125
+ detail: `uniqueName received ${JSON.stringify(label)}, which kebabs to nothing.`
126
+ });
127
+ if (segment.startsWith(RESERVED_TEST_PREFIX)) throw new ValidationError({
128
+ message: `A throwaway label must not repeat the reserved "${RESERVED_TEST_PREFIX}" prefix.`,
129
+ action: `Pass only the distinguishing part, e.g. uniqueName('${segment.slice(RESERVED_TEST_PREFIX.length) || "kv"}').`,
130
+ detail: `uniqueName composes ${RESERVED_TEST_PREFIX} itself; ${JSON.stringify(label)} would double it.`
131
+ });
132
+ nameCounter += 1;
133
+ const rand = Math.random().toString(36).slice(2, 8).padEnd(6, "0");
134
+ const suffix = `-${Date.now()}-${nameCounter}-${rand}`;
135
+ const budget = MAX_RESOURCE_NAME - RESERVED_TEST_PREFIX.length - suffix.length;
136
+ return `${RESERVED_TEST_PREFIX}${fitSegment(segment, budget)}${suffix}`;
137
+ }
138
+ /**
139
+ * Create a live resource, exercise it, and guarantee teardown — even if `exercise` throws. This is the
140
+ * spine of every live integration test: a failed assertion must never orphan a real Cloudflare resource.
141
+ * `create` runs outside the `try`, so a creation failure does not call `teardown` on something that was
142
+ * never created; once `create` resolves, `teardown` always runs in the `finally`. The exercise result is
143
+ * returned for the rare caller that wants it.
144
+ */
145
+ async function withThrowawayResource(create, exercise, teardown) {
146
+ const resource = await create();
147
+ try {
148
+ return await exercise(resource);
149
+ } finally {
150
+ await teardown(resource);
151
+ }
152
+ }
153
+ /**
154
+ * Create a live resource **at a name the caller already chose**, exercise it, and guarantee teardown —
155
+ * including when `create` itself rejects.
156
+ *
157
+ * {@link withThrowawayResource} runs `create` outside the `try` on purpose: for a kind whose id only
158
+ * exists once creation succeeds, tearing down a failed create would be tearing down nothing. But a whole
159
+ * family of Cloudflare calls are **named writes** — `putSecret(name, …)`, `createBucket(name)`,
160
+ * `createDatabase(name)`, `createWorker(name)` — where the address is the caller's own argument. There a
161
+ * rejection is ambiguous: the server may have accepted the write and failed on the way back, and the
162
+ * resource now exists under a name the test is still holding. `withThrowawayResource` walks away from it.
163
+ *
164
+ * This variant tears down on the **name**, and arms the teardown before `create` is ever called, so the
165
+ * ambiguous case cleans up. `teardown` must therefore tolerate a resource that was never created — every
166
+ * caller here passes an idempotent delete, and the reaper is the backstop if one is not.
167
+ */
168
+ async function withNamedResource(name, create, exercise, teardown) {
169
+ try {
170
+ await create(name);
171
+ return await exercise(name);
172
+ } finally {
173
+ await teardown(name);
174
+ }
175
+ }
176
+ /**
177
+ * How old a resource must be before a reaper will remove it.
178
+ *
179
+ * **Twelve hours, and the size is the safety argument.** Reaping is a race with any suite that is still
180
+ * running: a resource deleted mid-flight surfaces as an unexplained 404 in a test that was passing, which
181
+ * is a far worse failure than debris. Cloudflare offers no way to mark a D1, KV namespace, bucket, or
182
+ * index as owned by a live process — no lease, no tag our client can set on every kind — so the only
183
+ * defense available is a window nothing plausible can cross.
184
+ *
185
+ * A single live test is capped at 120s and a hook at 120s (`vitest.integration.config.ts`); a whole
186
+ * package's suite is minutes, and every package's suites together are well under an hour even when
187
+ * Vectorize is settling. Twelve hours is two orders of magnitude past that, and still short enough that
188
+ * debris never survives a day. The cost of the wider window is a handful of orphans lingering longer;
189
+ * the cost of the narrower one is a green suite turned red by its own housekeeping.
190
+ */
191
+ const DEFAULT_STALE_AFTER_MS = 432e5;
192
+ /**
193
+ * The ms-epoch {@link uniqueName} embedded, or `null` if this is not one of our names.
194
+ *
195
+ * `uniqueName` composes `pithy-int-<label>-<epoch>-<counter>-<rand>`, so the timestamp is the third
196
+ * segment from the end. Anything that does not match that shape returns `null` and is therefore never
197
+ * reaped — including a name inside the reservation that some *other* generator produced, such as a
198
+ * feature resource provisioned under {@link RESERVED_TEST_PROJECT}. Conservative on purpose: failing to
199
+ * clean up costs pennies, deleting someone's data does not.
200
+ */
201
+ function testResourceAge(name, now) {
202
+ if (!name.startsWith(RESERVED_TEST_PREFIX)) return null;
203
+ const segments = name.split("-");
204
+ if (segments.length < 3) return null;
205
+ const stamp = Number(segments[segments.length - 3]);
206
+ if (!Number.isInteger(stamp) || stamp <= 0 || stamp > now) return null;
207
+ return now - stamp;
208
+ }
209
+ /**
210
+ * Which of these names are stale test debris — ours, and old enough that no running suite owns them.
211
+ *
212
+ * Pure, so the age arithmetic is unit-tested without touching Cloudflare.
213
+ */
214
+ function staleTestResourceNames(names, options = {}) {
215
+ const now = options.now ?? Date.now();
216
+ const staleAfterMs = options.staleAfterMs ?? 432e5;
217
+ return names.filter((name) => {
218
+ const age = testResourceAge(name, now);
219
+ return age !== null && age >= staleAfterMs;
220
+ });
221
+ }
222
+ /**
223
+ * Remove stale throwaway resources of one kind, and report what went.
224
+ *
225
+ * `withThrowawayResource` guarantees teardown only while the process lives. A run killed by a test
226
+ * timeout, a Ctrl-C, or a crash orphans whatever it had created — which is exactly how two Vectorize
227
+ * indexes came to sit on a real account for a month. Calling this in a suite's `beforeAll` makes each
228
+ * run clean up after the last one, so the failure mode self-heals instead of accumulating.
229
+ *
230
+ * Never throws: a reaper that fails must not fail the suite it is trying to help. Failures are reported
231
+ * in the returned list of what could not be removed.
232
+ */
233
+ async function reapStaleTestResources(kind, options = {}) {
234
+ const reaped = [];
235
+ const failed = [];
236
+ let names;
237
+ try {
238
+ names = await kind.list();
239
+ } catch {
240
+ return {
241
+ reaped,
242
+ failed
243
+ };
244
+ }
245
+ for (const name of staleTestResourceNames(names, options)) try {
246
+ await kind.remove(name);
247
+ reaped.push(name);
248
+ } catch {
249
+ failed.push(name);
250
+ }
251
+ if (reaped.length > 0) console.warn(`reaped ${reaped.length} stale ${kind.label}(s): ${reaped.join(", ")}`);
252
+ if (failed.length > 0) console.warn(`could not reap ${failed.length} stale ${kind.label}(s): ${failed.join(", ")}`);
253
+ return {
254
+ reaped,
255
+ failed
256
+ };
257
+ }
258
+ /**
259
+ * An S3 client for the account's R2, built from the harness's own key pair.
260
+ *
261
+ * **R2 is the one kind the API token cannot reap.** Cloudflare refuses to delete a non-empty bucket, and
262
+ * emptying one is an S3-protocol operation — `ListObjectsV2`, `DeleteObject`, `AbortMultipartUpload` —
263
+ * that the REST API does not offer and a scoped API token cannot authenticate. So the harness holds the
264
+ * S3 credentials itself: {@link IntegrationCreds.r2}, parsed from `R2_CREDENTIALS`, is what makes a
265
+ * throwaway bucket reclaimable at all.
266
+ *
267
+ * Raw S3 rather than `CloudflareR2Manager` on purpose. This is teardown, and teardown must not depend on
268
+ * the code under test: if `emptyBucket` is the thing that broke, the bucket still has to come back empty
269
+ * or a failing test leaks a real resource instead of reporting a bug.
270
+ */
271
+ function testS3(creds) {
272
+ if (!creds.r2) throw new ValidationError({
273
+ message: "Emptying an R2 bucket needs the S3 key pair.",
274
+ action: "Set R2_CREDENTIALS in .dev.vars, or gate the suite on `creds.r2`.",
275
+ detail: "R2 refuses to delete a non-empty bucket, and draining one is an S3 operation, not a REST one."
276
+ });
277
+ return new S3Client({
278
+ region: "auto",
279
+ endpoint: `https://${creds.accountId}.r2.cloudflarestorage.com`,
280
+ credentials: {
281
+ accessKeyId: creds.r2.accessKeyId,
282
+ secretAccessKey: creds.r2.secretAccessKey
283
+ }
284
+ });
285
+ }
286
+ /**
287
+ * Empty a throwaway bucket so R2 will delete it.
288
+ *
289
+ * Two things hold a bucket against deletion: stored objects, and the parts of a multipart upload that was
290
+ * never completed or aborted — precisely what a test that failed mid-upload leaves behind. Both are
291
+ * cleared, dangling uploads first. Every page is drained, so a bucket with more than a thousand keys is
292
+ * emptied rather than half-emptied.
293
+ *
294
+ * Throws when the S3 key pair is absent: a bucket that cannot be emptied cannot be deleted, and silence
295
+ * there is how debris becomes permanent.
296
+ */
297
+ async function emptyTestBucket(creds, bucketName) {
298
+ const s3 = testS3(creds);
299
+ const uploads = await s3.send(new ListMultipartUploadsCommand({ Bucket: bucketName }));
300
+ for (const upload of uploads.Uploads ?? []) {
301
+ if (!upload.Key || !upload.UploadId) continue;
302
+ await s3.send(new AbortMultipartUploadCommand({
303
+ Bucket: bucketName,
304
+ Key: upload.Key,
305
+ UploadId: upload.UploadId
306
+ }));
307
+ }
308
+ let token;
309
+ do {
310
+ const page = await s3.send(new ListObjectsV2Command({
311
+ Bucket: bucketName,
312
+ ContinuationToken: token
313
+ }));
314
+ for (const entry of page.Contents ?? []) if (entry.Key) await s3.send(new DeleteObjectCommand({
315
+ Bucket: bucketName,
316
+ Key: entry.Key
317
+ }));
318
+ token = page.IsTruncated ? page.NextContinuationToken : void 0;
319
+ } while (token);
320
+ }
321
+ /**
322
+ * Reclaim stale throwaway R2 buckets — the one kind that needs more than an API token, so it lives here
323
+ * once instead of being re-derived by every suite that makes a bucket.
324
+ *
325
+ * Without `R2_CREDENTIALS` this reaps nothing and says so loudly rather than failing quietly: the buckets
326
+ * are still there, still cost money, and the only fix is the key pair. Never throws — a reaper must not
327
+ * fail the suite it is trying to help.
328
+ */
329
+ async function reapStaleTestBuckets(creds, options = {}) {
330
+ if (!creds.r2) {
331
+ console.warn("no R2_CREDENTIALS: stale pithy-int- buckets cannot be emptied, so none were reaped.");
332
+ return {
333
+ reaped: [],
334
+ failed: []
335
+ };
336
+ }
337
+ const provisioner = new CloudflareClients({
338
+ accountId: creds.accountId,
339
+ apiToken: creds.apiToken
340
+ }).r2Provisioner();
341
+ return reapStaleTestResources({
342
+ label: "R2 bucket",
343
+ list: async () => (await provisioner.listBuckets()).map((bucket) => bucket.name),
344
+ remove: async (name) => {
345
+ await emptyTestBucket(creds, name);
346
+ await provisioner.deleteBucket(name);
347
+ }
348
+ }, options);
349
+ }
350
+ //#endregion
351
+ export { DEFAULT_STALE_AFTER_MS, INTEGRATION_COMPATIBILITY_DATE, RESERVED_TEST_PROJECT, emptyTestBucket, loadIntegrationCreds, loadIntegrationEnv, parseR2Creds, reapStaleTestBuckets, reapStaleTestResources, staleTestResourceNames, testResourceAge, uniqueName, withNamedResource, withThrowawayResource };
@@ -0,0 +1,91 @@
1
+ import { z } from "zod";
2
+ import type { CloudflareWorkersManager } from "../workers/workersManager";
3
+ import type { IntegrationCreds } from "./harness";
4
+ /**
5
+ * A throwaway Worker that records what its `email()` handler was handed, and the reader that fetches
6
+ * the record back out.
7
+ *
8
+ * ## Why a Worker has to exist at all
9
+ *
10
+ * An Email Routing rule cannot point at a Worker that is not there. Cloudflare rejects the rule with
11
+ * `2016 Workers Script Info not found` at creation time, so a live test of `ensureWorkerRoute` has to
12
+ * deploy a real script before it can create a real rule. That fact alone forces this file; everything
13
+ * else here is what the script may as well do once it is running.
14
+ *
15
+ * ## Why the record goes through KV
16
+ *
17
+ * There is no inbox. The fixture zone's catch-all drops and it has zero verified destination
18
+ * addresses, so nothing this suite sends can be read from a mailbox — and a test that waits for one
19
+ * waits forever. The Worker is the destination, so the Worker has to be the witness: it writes what it
20
+ * received to a KV namespace the test created, and the test reads it back over the REST API.
21
+ *
22
+ * ## What it deliberately does not do
23
+ *
24
+ * It does not classify, suppress, or parse MIME. Everything `@pithy-sh/email` actually does with an
25
+ * inbound message is unit-tested against real D1 in the Workers pool, where a hostile message can be
26
+ * constructed exactly rather than mailed and hoped for. What only a live delivery can answer is what
27
+ * Cloudflare *hands* the handler — which headers survive, which Cloudflare adds, and whether a
28
+ * sender's own headers arrive intact. So the recorder records, and asserts nothing.
29
+ */
30
+ /** The KV binding the recorder writes through. */
31
+ export declare const INBOUND_RECORDER_BINDING = "OBSERVED";
32
+ /** The header a sender stamps to address its record. One send, one key, so parallel runs cannot collide. */
33
+ export declare const INBOUND_NONCE_HEADER = "X-Pithy-Probe";
34
+ /**
35
+ * The recorder's source, as a module string.
36
+ *
37
+ * Kept verbatim here rather than built from a template because it is uploaded as-is and read by whoever
38
+ * is debugging a delivery that did not arrive. It writes **once**, under the nonce it finds in the raw
39
+ * message, and it must never throw: a throw inside `email()` is a delivery failure Cloudflare retries,
40
+ * which turns one test message into several.
41
+ */
42
+ export declare const INBOUND_RECORDER_MODULE = "\nconst NONCE = /^X-Pithy-Probe:[ \\t]*(\\S+)[ \\t]*$/im;\nconst AUTH_RESULTS = /^Authentication-Results:[ \\t]*([\\s\\S]*?)(?=\\r?\\n[^ \\t])/gim;\n\n/** Header names in the raw block, in order, lowercased. Duplicates kept \u2014 two DKIM signatures are two facts. */\nfunction rawHeaderNames(raw) {\n const block = raw.split(/\\r?\\n\\r?\\n/)[0] ?? \"\";\n return block\n .split(/\\r?\\n/)\n .filter((line) => /^[A-Za-z0-9-]+:/.test(line))\n .map((line) => line.slice(0, line.indexOf(\":\")).toLowerCase());\n}\n\nexport default {\n async email(message, env) {\n let record;\n let nonce = \"unkeyed\";\n try {\n const raw = await new Response(message.raw).text();\n nonce = NONCE.exec(raw)?.[1] ?? \"unkeyed\";\n record = {\n envelopeFrom: message.from,\n envelopeTo: message.to,\n rawSize: message.rawSize,\n seen: [...message.headers.keys()].map((name) => name.toLowerCase()).sort(),\n rawHeaderNames: rawHeaderNames(raw),\n authenticationResults: [...raw.matchAll(AUTH_RESULTS)].map((match) => match[1].trim()),\n raw: raw.slice(0, 16384),\n };\n } catch (error) {\n record = { failure: String(error) };\n }\n await env.OBSERVED.put(nonce, JSON.stringify(record), { expirationTtl: 3600 });\n },\n};\n";
43
+ /**
44
+ * What the recorder saw — validated on the way back in, because a KV read is a boundary like any other.
45
+ *
46
+ * The Worker that wrote it is ours, which is exactly the argument that has let every unvalidated
47
+ * internal read through. A truncated write or a stale record from an older recorder deserves a Zod
48
+ * failure naming the field, not a `TypeError` three assertions later.
49
+ */
50
+ export declare const ObservedInbound: z.ZodObject<{
51
+ envelopeFrom: z.ZodString;
52
+ envelopeTo: z.ZodString;
53
+ rawSize: z.ZodNumber;
54
+ seen: z.ZodArray<z.ZodString>;
55
+ rawHeaderNames: z.ZodArray<z.ZodString>;
56
+ authenticationResults: z.ZodArray<z.ZodString>;
57
+ raw: z.ZodString;
58
+ }, z.core.$strip>;
59
+ /** What the recorder saw. */
60
+ export type ObservedInbound = z.output<typeof ObservedInbound>;
61
+ /** A trivial module Worker: enough for an Email Routing rule to be allowed to point at it. */
62
+ export declare const PLACEHOLDER_INBOUND_MODULE = "export default { async email() {} };\n";
63
+ /** The module filename every throwaway Worker here is uploaded under. */
64
+ export declare const INBOUND_MODULE_NAME = "worker.mjs";
65
+ /**
66
+ * Deploy the recorder at `scriptName`, bound to the KV namespace records are written to.
67
+ *
68
+ * Uploaded through `CloudflareWorkersManager.createWorker` like anything else. It briefly was not:
69
+ * #373 was a manager that could not upload a module to the live API at all, and this file carried a
70
+ * hand-rolled `fetch` around it. The manager is the path again — a helper that exists because the
71
+ * product is broken is the thing people copy.
72
+ */
73
+ export declare function deployInboundRecorder(options: {
74
+ workers: CloudflareWorkersManager;
75
+ scriptName: string;
76
+ namespaceId: string;
77
+ /** The date this throwaway Worker runs on. Named by the suite, because #396 left no default to inherit. */
78
+ compatibilityDate: string;
79
+ }): Promise<void>;
80
+ /**
81
+ * The record for one nonce, or null while nothing has arrived.
82
+ *
83
+ * Null rather than a throw for the missing case, because "not yet" is the normal state of a mail
84
+ * delivery: the caller polls. A 404 from KV is that state; anything else is a real failure and throws.
85
+ */
86
+ export declare function readObservedInbound(options: {
87
+ creds: IntegrationCreds;
88
+ namespaceId: string;
89
+ nonce: string;
90
+ }): Promise<ObservedInbound | null>;
91
+ //# sourceMappingURL=inboundRecorder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inboundRecorder.d.ts","sourceRoot":"","sources":["../../src/test-utils/inboundRecorder.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAC1E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,kDAAkD;AAClD,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAEnD,4GAA4G;AAC5G,eAAO,MAAM,oBAAoB,kBAAkB,CAAC;AAapD;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,myCAmCnC,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,eAAe;;;;;;;;iBAgBqD,CAAC;AAElF,6BAA6B;AAC7B,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,eAAe,CAAC,CAAC;AAE/D,8FAA8F;AAC9F,eAAO,MAAM,0BAA0B,2CAA2C,CAAC;AAEnF,yEAAyE;AACzE,eAAO,MAAM,mBAAmB,eAAe,CAAC;AAEhD;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CAAC,OAAO,EAAE;IACnD,OAAO,EAAE,wBAAwB,CAAC;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,2GAA2G;IAC3G,iBAAiB,EAAE,MAAM,CAAC;CAC3B,GAAG,OAAO,CAAC,IAAI,CAAC,CAQhB;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE;IACjD,KAAK,EAAE,gBAAgB,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAuBlC"}