@pithy-sh/core 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access/access.d.ts +172 -0
- package/dist/access/access.d.ts.map +1 -0
- package/dist/access/access.js +147 -0
- package/dist/access/acting.d.ts +49 -0
- package/dist/access/acting.d.ts.map +1 -0
- package/dist/access/acting.js +21 -0
- package/dist/access/policyCheck.d.ts +33 -0
- package/dist/access/policyCheck.d.ts.map +1 -0
- package/dist/access/policyCheck.js +48 -0
- package/dist/access/usage.d.ts +83 -0
- package/dist/access/usage.d.ts.map +1 -0
- package/dist/access/usage.js +104 -0
- package/dist/access/visibility.d.ts +66 -0
- package/dist/access/visibility.d.ts.map +1 -0
- package/dist/access/visibility.js +86 -0
- package/dist/audit/auditEvent.d.ts +5 -0
- package/dist/audit/auditEvent.d.ts.map +1 -1
- package/dist/audit/auditEvent.js +2 -0
- package/dist/capability/bindings.d.ts +47 -2
- package/dist/capability/bindings.d.ts.map +1 -1
- package/dist/capability/bindings.js +117 -7
- package/dist/capability/capability.d.ts +98 -28
- package/dist/capability/capability.d.ts.map +1 -1
- package/dist/capability/manifest.d.ts +2 -1
- package/dist/capability/manifest.d.ts.map +1 -1
- package/dist/capability/manifest.js +1 -1
- package/dist/capability/settings.d.ts +86 -2
- package/dist/capability/settings.d.ts.map +1 -1
- package/dist/capability/settings.js +80 -1
- package/dist/cli/commandCatalog.generated.d.ts +184 -22
- package/dist/cli/commandCatalog.generated.d.ts.map +1 -1
- package/dist/cli/commandCatalog.generated.js +196 -22
- package/dist/cli/commandFacts.d.ts +1 -1
- package/dist/cli/commandFacts.d.ts.map +1 -1
- package/dist/cli/commandFacts.js +1 -0
- package/dist/controlPlane/capability.d.ts.map +1 -1
- package/dist/controlPlane/capability.js +12 -2
- package/dist/controlPlane/context.d.ts +5 -0
- package/dist/controlPlane/context.d.ts.map +1 -1
- package/dist/controlPlane/context.js +3 -1
- package/dist/controlPlane/data/connection.d.ts +47 -0
- package/dist/controlPlane/data/connection.d.ts.map +1 -1
- package/dist/controlPlane/data/connection.js +55 -1
- package/dist/controlPlane/http/cors.d.ts +4 -4
- package/dist/controlPlane/http/cors.js +4 -4
- package/dist/controlPlane/http/guard.d.ts +11 -0
- package/dist/controlPlane/http/guard.d.ts.map +1 -1
- package/dist/controlPlane/http/guard.js +17 -1
- package/dist/controlPlane/http/handlers.d.ts +60 -0
- package/dist/controlPlane/http/handlers.d.ts.map +1 -1
- package/dist/controlPlane/http/handlers.js +31 -2
- package/dist/controlPlane/http/readiness.d.ts +33 -0
- package/dist/controlPlane/http/readiness.d.ts.map +1 -0
- package/dist/controlPlane/http/readiness.js +39 -0
- package/dist/controlPlane/http/responses.d.ts +147 -0
- package/dist/controlPlane/http/responses.d.ts.map +1 -1
- package/dist/controlPlane/http/responses.js +61 -4
- package/dist/controlPlane/http/routes.d.ts +1 -0
- package/dist/controlPlane/http/routes.d.ts.map +1 -1
- package/dist/controlPlane/http/routes.js +17 -3
- package/dist/controlPlane/http/verify.d.ts.map +1 -1
- package/dist/controlPlane/http/verify.js +2 -1
- package/dist/controlPlane/migrationState/migrationState.d.ts +102 -0
- package/dist/controlPlane/migrationState/migrationState.d.ts.map +1 -0
- package/dist/controlPlane/migrationState/migrationState.js +142 -0
- package/dist/controlPlane/migrations/0003_connection_principal.d.ts +21 -0
- package/dist/controlPlane/migrations/0003_connection_principal.d.ts.map +1 -0
- package/dist/controlPlane/migrations/0003_connection_principal.js +33 -0
- package/dist/controlPlane/scope/scope.d.ts +13 -2
- package/dist/controlPlane/scope/scope.d.ts.map +1 -1
- package/dist/controlPlane/scope/scope.js +14 -3
- package/dist/createBackend.d.ts.map +1 -1
- package/dist/createBackend.js +7 -2
- package/dist/createEntrypoint.d.ts.map +1 -1
- package/dist/createEntrypoint.js +5 -3
- package/dist/entitlement/allowance.d.ts +25 -0
- package/dist/entitlement/allowance.d.ts.map +1 -0
- package/dist/entitlement/allowance.js +80 -0
- package/dist/entitlement/entitlement.d.ts +41 -0
- package/dist/entitlement/entitlement.d.ts.map +1 -1
- package/dist/entitlement/meter.d.ts +57 -0
- package/dist/entitlement/meter.d.ts.map +1 -0
- package/dist/entitlement/meter.js +13 -0
- package/dist/error/http.d.ts +144 -0
- package/dist/error/http.d.ts.map +1 -1
- package/dist/error/payload.d.ts +288 -0
- package/dist/error/payload.d.ts.map +1 -1
- package/dist/error/payload.js +100 -4
- package/dist/http/authContext.d.ts +7 -0
- package/dist/http/authContext.d.ts.map +1 -1
- package/dist/http/authContext.js +8 -1
- package/dist/http/scopeGates.d.ts +20 -0
- package/dist/http/scopeGates.d.ts.map +1 -0
- package/dist/http/scopeGates.js +36 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/logger/record.d.ts +84 -0
- package/dist/logger/record.d.ts.map +1 -1
- package/dist/migrations/capabilitySets.d.ts +23 -0
- package/dist/migrations/capabilitySets.d.ts.map +1 -0
- package/dist/migrations/capabilitySets.js +74 -0
- package/dist/migrations/registry.d.ts +26 -0
- package/dist/migrations/registry.d.ts.map +1 -1
- package/dist/migrations/registry.js +38 -7
- package/dist/migrations/state.d.ts +60 -0
- package/dist/migrations/state.d.ts.map +1 -0
- package/dist/migrations/state.js +46 -0
- package/dist/naming/feature.d.ts +27 -5
- package/dist/naming/feature.d.ts.map +1 -1
- package/dist/naming/feature.js +36 -6
- package/dist/naming/limits.d.ts +47 -3
- package/dist/naming/limits.d.ts.map +1 -1
- package/dist/naming/limits.js +47 -3
- package/dist/naming/provisionScope.d.ts +6 -0
- package/dist/naming/provisionScope.d.ts.map +1 -1
- package/dist/naming/provisionScope.js +8 -3
- package/dist/naming/resourceNames.d.ts +21 -2
- package/dist/naming/resourceNames.d.ts.map +1 -1
- package/dist/naming/resourceNames.js +21 -3
- package/dist/seed/writeD1.d.ts +7 -1
- package/dist/seed/writeD1.d.ts.map +1 -1
- package/dist/seed/writeD1.js +9 -3
- package/dist/seed/writeKv.d.ts +2 -0
- package/dist/seed/writeKv.d.ts.map +1 -1
- package/dist/seed/writeKv.js +6 -2
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/dist/worker/serviceLoopback.d.ts +36 -0
- package/dist/worker/serviceLoopback.d.ts.map +1 -0
- package/dist/worker/serviceLoopback.js +74 -0
- package/dist/workflow/faults.d.ts +12 -1
- package/dist/workflow/faults.d.ts.map +1 -1
- package/dist/workflow/faults.js +22 -15
- package/package.json +2 -2
- package/src/access/access.ts +298 -0
- package/src/access/acting.ts +59 -0
- package/src/access/policyCheck.ts +77 -0
- package/src/access/usage.ts +141 -0
- package/src/access/visibility.ts +109 -0
- package/src/audit/auditEvent.ts +4 -0
- package/src/capability/bindings.ts +159 -13
- package/src/capability/capability.ts +99 -29
- package/src/capability/settings.ts +146 -2
- package/src/cli/commandCatalog.generated.ts +212 -22
- package/src/cli/commandFacts.ts +1 -0
- package/src/controlPlane/capability.ts +25 -1
- package/src/controlPlane/context.ts +4 -0
- package/src/controlPlane/data/connection.ts +90 -0
- package/src/controlPlane/http/cors.ts +4 -4
- package/src/controlPlane/http/guard.ts +21 -0
- package/src/controlPlane/http/handlers.ts +41 -0
- package/src/controlPlane/http/readiness.ts +50 -0
- package/src/controlPlane/http/responses.ts +140 -2
- package/src/controlPlane/http/routes.ts +25 -1
- package/src/controlPlane/http/verify.ts +2 -0
- package/src/controlPlane/migrationState/migrationState.ts +200 -0
- package/src/controlPlane/migrations/0003_connection_principal.ts +37 -0
- package/src/controlPlane/scope/scope.ts +14 -2
- package/src/createBackend.ts +24 -6
- package/src/createEntrypoint.ts +9 -3
- package/src/entitlement/allowance.ts +85 -0
- package/src/entitlement/entitlement.ts +38 -0
- package/src/entitlement/meter.ts +69 -0
- package/src/error/payload.ts +211 -4
- package/src/http/authContext.ts +13 -1
- package/src/http/scopeGates.ts +34 -0
- package/src/index.ts +1 -0
- package/src/migrations/capabilitySets.ts +87 -0
- package/src/migrations/registry.ts +46 -2
- package/src/migrations/state.ts +94 -0
- package/src/naming/feature.ts +46 -7
- package/src/naming/limits.ts +35 -3
- package/src/naming/provisionScope.ts +19 -1
- package/src/naming/resourceNames.ts +52 -5
- package/src/seed/writeD1.ts +13 -4
- package/src/seed/writeKv.ts +6 -2
- package/src/version.generated.ts +1 -1
- package/src/worker/serviceLoopback.ts +105 -0
- package/src/workflow/faults.ts +51 -24
package/src/naming/feature.ts
CHANGED
|
@@ -34,11 +34,26 @@ import { assertValidProjectName, kebab } from "./resource";
|
|
|
34
34
|
* implementation rather than two that drift.
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
|
-
/** A Cloudflare resource kind
|
|
38
|
-
export type FeatureResourceKind = "d1" | "kv" | "r2";
|
|
37
|
+
/** A Cloudflare resource kind Pithy names and creates — in a feature, and in every declared environment. */
|
|
38
|
+
export type FeatureResourceKind = "d1" | "kv" | "r2" | "queue";
|
|
39
39
|
|
|
40
40
|
/** Every feature resource kind, for tests and for callers that enumerate. */
|
|
41
|
-
export const FEATURE_RESOURCE_KINDS: readonly FeatureResourceKind[] = ["d1", "kv", "r2"];
|
|
41
|
+
export const FEATURE_RESOURCE_KINDS: readonly FeatureResourceKind[] = ["d1", "kv", "r2", "queue"];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* **The segment each kind ends a feature resource's name with — never more than `MAX_FEATURE_KIND` (#662).**
|
|
45
|
+
*
|
|
46
|
+
* `d1`, `kv` and `r2` spell themselves. A queue is `q`, and that is load-bearing: `MAX_FEATURE_KIND` is a term in
|
|
47
|
+
* `FEATURE_DERIVED_PROJECT_NAME`, so spelling it `queue` would take the project budget from 26 to 23 — and a
|
|
48
|
+
* project name is stable forever once anything is provisioned, so a smaller budget orphans every resource of a
|
|
49
|
+
* project already named against the old one. `feature.test.ts` fails on a segment longer than the budget.
|
|
50
|
+
*/
|
|
51
|
+
export const FEATURE_KIND_SEGMENT: Readonly<Record<FeatureResourceKind, string>> = {
|
|
52
|
+
d1: "d1",
|
|
53
|
+
kv: "kv",
|
|
54
|
+
r2: "r2",
|
|
55
|
+
queue: "q",
|
|
56
|
+
};
|
|
42
57
|
|
|
43
58
|
/** The identity of a feature — everything a feature name is derived from. */
|
|
44
59
|
export interface FeatureIdentity {
|
|
@@ -292,10 +307,11 @@ export function assertFeatureSlugFits(identity: FeatureIdentity, shapes: readonl
|
|
|
292
307
|
}
|
|
293
308
|
|
|
294
309
|
/**
|
|
295
|
-
* The full Cloudflare resource name for a feature's binding — `<project>-f<issue>-<slug>--<binding>-<kind
|
|
310
|
+
* The full Cloudflare resource name for a feature's binding — `<project>-f<issue>-<slug>--<binding>-<kind>`, the
|
|
311
|
+
* kind spelled by {@link FEATURE_KIND_SEGMENT}.
|
|
296
312
|
*
|
|
297
|
-
* Held to **R2's 63**, the strictest of the
|
|
298
|
-
* all of them: lowercase, hyphenated, alphanumeric at both ends. A D1 or KV name could be longer, but a
|
|
313
|
+
* Held to **R2's 63**, the strictest of the kinds a feature provisions and a queue's number too, so one shape is
|
|
314
|
+
* legal for all of them: lowercase, hyphenated, alphanumeric at both ends. A D1 or KV name could be longer, but a
|
|
299
315
|
* feature that provisions a bucket and a database wants them recognizably the same name.
|
|
300
316
|
*/
|
|
301
317
|
export function featureResourceName(identity: FeatureIdentity, binding: string, kind: FeatureResourceKind): string {
|
|
@@ -303,7 +319,7 @@ export function featureResourceName(identity: FeatureIdentity, binding: string,
|
|
|
303
319
|
identity,
|
|
304
320
|
kebab(identity.slug),
|
|
305
321
|
kebab(binding),
|
|
306
|
-
`-${kind}`,
|
|
322
|
+
`-${FEATURE_KIND_SEGMENT[kind]}`,
|
|
307
323
|
NAMESPACE_LIMITS.r2.maxLength,
|
|
308
324
|
NAMESPACE_LIMITS.r2.label,
|
|
309
325
|
);
|
|
@@ -364,6 +380,29 @@ export function featureWorkerName(identity: FeatureIdentity, app: string): strin
|
|
|
364
380
|
);
|
|
365
381
|
}
|
|
366
382
|
|
|
383
|
+
/** The `<thing>` a feature's Artifacts namespace ends with, after {@link FEATURE_TAIL_SEPARATOR} (#673). */
|
|
384
|
+
export const FEATURE_ARTIFACTS_THING = "artifacts";
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* A feature's **Artifacts namespace** — `<project>-f<issue>-<slug>--artifacts` (#673).
|
|
388
|
+
*
|
|
389
|
+
* A declared environment's namespace is `<project>-<env>` and has no `<thing>`. A feature's needs one, because the
|
|
390
|
+
* separator and what follows it are what make a name a feature name: {@link isFeatureOwnedName} reads this one back
|
|
391
|
+
* as this feature's, so `pithy feature destroy` reaps the repos of exactly this branch and of no sibling. Held to the
|
|
392
|
+
* namespace's 63, the same number the rest of a feature's names are derived against, and `artifacts` is shorter than
|
|
393
|
+
* the shortest binding tail, so it moves no slug budget.
|
|
394
|
+
*/
|
|
395
|
+
export function featureArtifactsNamespace(identity: FeatureIdentity): string {
|
|
396
|
+
return composeFeatureName(
|
|
397
|
+
identity,
|
|
398
|
+
kebab(identity.slug),
|
|
399
|
+
FEATURE_ARTIFACTS_THING,
|
|
400
|
+
"",
|
|
401
|
+
NAMESPACE_LIMITS.artifactsNamespace.maxLength,
|
|
402
|
+
NAMESPACE_LIMITS.artifactsNamespace.label,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
367
406
|
/**
|
|
368
407
|
* The deployed name of one of a feature's **Workflows** — `<project>-f<issue>-<slug>--<capability>-<job>` (#643).
|
|
369
408
|
*
|
package/src/naming/limits.ts
CHANGED
|
@@ -107,6 +107,35 @@ export const NAMESPACE_LIMITS = {
|
|
|
107
107
|
* no longer hashes one at 63.
|
|
108
108
|
*/
|
|
109
109
|
kv: { label: "a KV namespace title", maxLength: 512, minLength: 1, policy: "truncate", source: "cloudflare" },
|
|
110
|
+
/**
|
|
111
|
+
* A Queue name: **1 to 63 characters**, lowercase letters, digits and dashes, starting and ending with a letter
|
|
112
|
+
* or number ([Queues · Get started](https://developers.cloudflare.com/queues/get-started/), verified
|
|
113
|
+
* 2026-10-18). Truncated rather than refused: Pithy recomputes a queue's name from its binding on every run,
|
|
114
|
+
* as it does a bucket's, so a deterministic hash is safe (#662). The same 63 as R2, so a feature's queue fits
|
|
115
|
+
* the shape {@link FEATURE_DERIVED_PROJECT_NAME} is derived against and moves no budget.
|
|
116
|
+
*/
|
|
117
|
+
queue: { label: "a Queue name", maxLength: 63, minLength: 1, policy: "truncate", source: "cloudflare" },
|
|
118
|
+
/**
|
|
119
|
+
* An Artifacts namespace: **2 to 63 characters**, starting with a letter or digit, the rest letters, digits, `.`,
|
|
120
|
+
* `_` and `-` ([Artifacts · Limits](https://developers.cloudflare.com/artifacts/platform/limits/), verified
|
|
121
|
+
* 2026-10-18). Truncated rather than refused: the kit derives it from the project and the environment on every
|
|
122
|
+
* run, like an R2 bucket name, and the longest project and environment compose 34 characters, so it never is (#673).
|
|
123
|
+
*/
|
|
124
|
+
artifactsNamespace: {
|
|
125
|
+
label: "an Artifacts namespace name",
|
|
126
|
+
maxLength: 63,
|
|
127
|
+
minLength: 2,
|
|
128
|
+
policy: "truncate",
|
|
129
|
+
source: "cloudflare",
|
|
130
|
+
},
|
|
131
|
+
/**
|
|
132
|
+
* An Artifacts repo name. **The charset is Cloudflare's** — a letter or digit, then letters, digits, `.`, `_` and
|
|
133
|
+
* `-` ([Artifacts · Limits](https://developers.cloudflare.com/artifacts/platform/limits/), verified 2026-10-18) —
|
|
134
|
+
* and **the length is ours**: that page states one for a namespace and none for a repo, so 2 to 63 mirrors the
|
|
135
|
+
* namespace rather than inventing a second number. Refused, never truncated: a repo name is either a UUID or a
|
|
136
|
+
* name somebody chose, and a name somebody chose is never silently shortened (#673).
|
|
137
|
+
*/
|
|
138
|
+
artifactsRepo: { label: "an Artifacts repo name", maxLength: 63, minLength: 2, policy: "refuse", source: "pithy" },
|
|
110
139
|
/**
|
|
111
140
|
* A D1 database name: `^[a-zA-Z0-9][a-zA-Z0-9_-]*$` in the OpenAPI schema, and **no documented
|
|
112
141
|
* length cap** — the limits page has no row for it. So the ceiling here is {@link MAX_PITHY_NAME}.
|
|
@@ -196,7 +225,10 @@ export const FEATURE_TAIL_SEPARATOR = "--";
|
|
|
196
225
|
*/
|
|
197
226
|
export const MIN_VERBATIM_BINDING = 12;
|
|
198
227
|
|
|
199
|
-
/**
|
|
228
|
+
/**
|
|
229
|
+
* The longest feature resource kind segment — `d1`, `kv`, `r2`, and a queue's `q` (#662). `feature.test.ts` pins
|
|
230
|
+
* every kind's `FEATURE_KIND_SEGMENT` to it, so a longer segment fails rather than shrinking every project's budget.
|
|
231
|
+
*/
|
|
200
232
|
export const MAX_FEATURE_KIND = 2;
|
|
201
233
|
|
|
202
234
|
/**
|
|
@@ -217,7 +249,7 @@ export const WORKFLOW_DERIVED_PROJECT_NAME =
|
|
|
217
249
|
|
|
218
250
|
/**
|
|
219
251
|
* What the longest feature resource name leaves a project — `<project>-f<issue>-<slug>--<binding>-<kind>`,
|
|
220
|
-
* against R2's 63 (the strictest of the
|
|
252
|
+
* against R2's 63 (the strictest of the kinds a feature provisions, a queue's number too, and the same number a
|
|
221
253
|
* feature Worker script gets).
|
|
222
254
|
*
|
|
223
255
|
* ```
|
|
@@ -226,7 +258,7 @@ export const WORKFLOW_DERIVED_PROJECT_NAME =
|
|
|
226
258
|
* - 6 the issue number, six digits — `f999999` (MAX_ISSUE_DIGITS)
|
|
227
259
|
* - 12 `-` + a legible slug (MIN_LEGIBLE_SLUG)
|
|
228
260
|
* - 14 `--` + a binding kept whole (FEATURE_TAIL_SEPARATOR, MIN_VERBATIM_BINDING)
|
|
229
|
-
* - 3 `-` + the kind, `d1` | `kv` | `r2`
|
|
261
|
+
* - 3 `-` + the kind, `d1` | `kv` | `r2` | `q`
|
|
230
262
|
* ----
|
|
231
263
|
* 26
|
|
232
264
|
* ```
|
|
@@ -8,6 +8,7 @@ import { assertValidEnvironment, FEATURE_ENVIRONMENT, GLOBAL_SCOPE, isFeatureMar
|
|
|
8
8
|
import {
|
|
9
9
|
type FeatureIdentity,
|
|
10
10
|
type FeatureResourceKind,
|
|
11
|
+
featureArtifactsNamespace,
|
|
11
12
|
featureResourceName,
|
|
12
13
|
featureSecretEntryName,
|
|
13
14
|
featureWorkerName,
|
|
@@ -189,10 +190,21 @@ export interface ProvisionScope {
|
|
|
189
190
|
* `<project>-feature-email-send` would be one Workflow every open branch deployed over every other's.
|
|
190
191
|
*/
|
|
191
192
|
readonly workflowHost: Omit<WorkflowHostNameParts, "capability">;
|
|
193
|
+
/**
|
|
194
|
+
* The Artifacts namespace an `artifacts` binding names in this scope (#673): `<project>-<env>` in a declared
|
|
195
|
+
* environment, `<project>-f<issue>-<slug>--artifacts` in a feature. On the scope for the reason every other name
|
|
196
|
+
* is: the stanza a run writes and the namespace it writes into it are one decision.
|
|
197
|
+
*/
|
|
198
|
+
readonly artifactsNamespace: string;
|
|
192
199
|
}
|
|
193
200
|
|
|
194
201
|
/** The wrangler binding array a resource kind's name is composed for. */
|
|
195
|
-
const KIND_NAMER: Record<FeatureResourceKind, "d1" | "kv" | "r2"
|
|
202
|
+
const KIND_NAMER: Record<FeatureResourceKind, "d1" | "kv" | "r2" | "queue"> = {
|
|
203
|
+
d1: "d1",
|
|
204
|
+
kv: "kv",
|
|
205
|
+
r2: "r2",
|
|
206
|
+
queue: "queue",
|
|
207
|
+
};
|
|
196
208
|
|
|
197
209
|
/**
|
|
198
210
|
* The one branch every scope shares: a `global` secret resolves to the project's single
|
|
@@ -345,6 +357,7 @@ export function environmentScope(project: string, environment: string): Provisio
|
|
|
345
357
|
secretEntry: (secret, secretScope) =>
|
|
346
358
|
secretEntryName(project, secret, secretScope, () => names.secretEntry(secret)),
|
|
347
359
|
workflowHost: { project, env: environment },
|
|
360
|
+
artifactsNamespace: names.artifactsNamespace(),
|
|
348
361
|
};
|
|
349
362
|
}
|
|
350
363
|
|
|
@@ -392,6 +405,11 @@ export function featureScope(identity: FeatureIdentity): ProvisionScope {
|
|
|
392
405
|
secretEntry: (secret) => featureSecretEntryName(identity, secret),
|
|
393
406
|
// `env` is the stanza — what the host's `ENVIRONMENT` var says — and `feature` is what its names take.
|
|
394
407
|
workflowHost: { project: identity.project, env: FEATURE_ENVIRONMENT, feature: identity },
|
|
408
|
+
// The feature's own, read back as its own by `isFeatureOwnedName`, so teardown reaps no sibling's repos. A
|
|
409
|
+
// getter, so a slug too long for it refuses where the namespace is asked for, never where the scope is made.
|
|
410
|
+
get artifactsNamespace() {
|
|
411
|
+
return featureArtifactsNamespace(identity);
|
|
412
|
+
},
|
|
395
413
|
};
|
|
396
414
|
}
|
|
397
415
|
|
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
import { InternalError, ValidationError } from "../error/pithyError";
|
|
5
5
|
import { workflowHostName, workflowScriptName } from "../workflow/naming";
|
|
6
6
|
import { assertValidEnvironment, GLOBAL_SCOPE } from "./environment";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
type FeatureIdentity,
|
|
9
|
+
type FeatureResourceKind,
|
|
10
|
+
featureArtifactsNamespace,
|
|
11
|
+
featureResourceName,
|
|
12
|
+
featureWorkerName,
|
|
13
|
+
} from "./feature";
|
|
8
14
|
import { NAMESPACE_LIMITS, type NamespaceLimit } from "./limits";
|
|
9
15
|
import { assertValidProjectName, kebab, resourceName } from "./resource";
|
|
10
16
|
|
|
@@ -57,6 +63,8 @@ export interface ScopedNames {
|
|
|
57
63
|
kv(binding: string): string;
|
|
58
64
|
/** An R2 bucket. Cloudflare's 63 — the one namespace where that number was always right. */
|
|
59
65
|
r2(binding: string): string;
|
|
66
|
+
/** A Queue. Cloudflare's 63, truncated with a stable hash like a bucket (#662). */
|
|
67
|
+
queue(binding: string): string;
|
|
60
68
|
/** A Vectorize index. 64 bytes, refused rather than truncated: a renamed index has no vectors. */
|
|
61
69
|
vectorizeIndex(index: string): string;
|
|
62
70
|
/** A Secrets Store entry. No Cloudflare cap; Pithy's ceiling. */
|
|
@@ -65,15 +73,33 @@ export interface ScopedNames {
|
|
|
65
73
|
apiToken(profile: string): string;
|
|
66
74
|
}
|
|
67
75
|
|
|
76
|
+
/**
|
|
77
|
+
* The names in one declared environment: everything a scope has, and the one name that exists per environment
|
|
78
|
+
* and never at `global`.
|
|
79
|
+
*/
|
|
80
|
+
export interface EnvironmentNames extends ScopedNames {
|
|
81
|
+
/**
|
|
82
|
+
* The Artifacts namespace — `<project>-<env>`, with no `<thing>` (#673). One per environment, so a feature's
|
|
83
|
+
* repos never sit beside production's and the per-namespace rate limit splits per environment. There is no
|
|
84
|
+
* global one, because a namespace shared across environments is exactly that mix.
|
|
85
|
+
*/
|
|
86
|
+
artifactsNamespace(): string;
|
|
87
|
+
}
|
|
88
|
+
|
|
68
89
|
/** The names a feature environment provisions under, with the project already bound. */
|
|
69
90
|
export interface FeatureNames {
|
|
70
|
-
/** A feature's D1, KV, or
|
|
91
|
+
/** A feature's D1, KV, R2 or Queue resource — `<project>-f<issue>-<slug>--<binding>-<kind>`. */
|
|
71
92
|
resource(binding: string, kind: FeatureResourceKind): string;
|
|
72
93
|
/**
|
|
73
94
|
* A feature's Worker script — `<project>-f<issue>-<slug>-<app>`, from the `apps/<app>` directory and
|
|
74
95
|
* never the deploy name, which already leads with the project (#587).
|
|
75
96
|
*/
|
|
76
97
|
worker(app: string): string;
|
|
98
|
+
/**
|
|
99
|
+
* A feature's Artifacts namespace — `<project>-f<issue>-<slug>--artifacts` (#673). The tail is what makes it a
|
|
100
|
+
* feature name `isFeatureOwnedName` reads back, so teardown reaps this feature's repos and no sibling's.
|
|
101
|
+
*/
|
|
102
|
+
artifactsNamespace(): string;
|
|
77
103
|
}
|
|
78
104
|
|
|
79
105
|
/** Everything a project can be asked to name. Created once, from the project, by {@link resourceNames}. */
|
|
@@ -81,7 +107,7 @@ export interface ProjectNames {
|
|
|
81
107
|
/** The kebabbed project name every name below leads with. */
|
|
82
108
|
readonly project: string;
|
|
83
109
|
/** The names in one environment. Throws if the environment is not one this scheme accepts. */
|
|
84
|
-
env(environment: string):
|
|
110
|
+
env(environment: string): EnvironmentNames;
|
|
85
111
|
/** The names shared by every environment — the `global` scope, without typing the word. */
|
|
86
112
|
readonly global: ScopedNames;
|
|
87
113
|
/** The names a feature environment provisions under. */
|
|
@@ -134,6 +160,22 @@ function scopedName(project: string, scope: string, thing: string, limit: Namesp
|
|
|
134
160
|
return name;
|
|
135
161
|
}
|
|
136
162
|
|
|
163
|
+
/**
|
|
164
|
+
* `<project>-<env>`, under the Artifacts namespace rule. The project and environment are both validated by now, and
|
|
165
|
+
* the longest of each compose 34 characters, so the cap is a check rather than a truncation that ever happens.
|
|
166
|
+
*/
|
|
167
|
+
function artifactsNamespaceName(project: string, environment: string): string {
|
|
168
|
+
const limit = NAMESPACE_LIMITS.artifactsNamespace;
|
|
169
|
+
const name = `${project}-${environment}`;
|
|
170
|
+
if (name.length > limit.maxLength || name.length < limit.minLength) {
|
|
171
|
+
throw new InternalError({
|
|
172
|
+
message: `"${name}" does not fit ${limit.label}, which takes ${limit.minLength} to ${limit.maxLength} characters.`,
|
|
173
|
+
action: "Shorten the project or environment name.",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return name;
|
|
177
|
+
}
|
|
178
|
+
|
|
137
179
|
/** The getters for one scope. `global` and every environment share this, so nothing special-cases either. */
|
|
138
180
|
function scopedNames(project: string, scope: string): ScopedNames {
|
|
139
181
|
return {
|
|
@@ -142,6 +184,7 @@ function scopedNames(project: string, scope: string): ScopedNames {
|
|
|
142
184
|
d1: (binding) => scopedName(project, scope, binding, NAMESPACE_LIMITS.d1),
|
|
143
185
|
kv: (binding) => scopedName(project, scope, binding, NAMESPACE_LIMITS.kv),
|
|
144
186
|
r2: (binding) => scopedName(project, scope, binding, NAMESPACE_LIMITS.r2),
|
|
187
|
+
queue: (binding) => scopedName(project, scope, binding, NAMESPACE_LIMITS.queue),
|
|
145
188
|
vectorizeIndex: (index) => scopedName(project, scope, index, NAMESPACE_LIMITS.vectorizeIndex),
|
|
146
189
|
secretEntry: (secret) => scopedName(project, scope, secret, NAMESPACE_LIMITS.secretEntry),
|
|
147
190
|
apiToken: (profile) => scopedName(project, scope, profile, NAMESPACE_LIMITS.apiToken),
|
|
@@ -161,9 +204,12 @@ export function resourceNames(project: string): ProjectNames {
|
|
|
161
204
|
|
|
162
205
|
return {
|
|
163
206
|
project: normalized,
|
|
164
|
-
env(environment: string):
|
|
207
|
+
env(environment: string): EnvironmentNames {
|
|
165
208
|
assertValidEnvironment(environment);
|
|
166
|
-
return
|
|
209
|
+
return {
|
|
210
|
+
...scopedNames(normalized, environment),
|
|
211
|
+
artifactsNamespace: () => artifactsNamespaceName(normalized, environment),
|
|
212
|
+
};
|
|
167
213
|
},
|
|
168
214
|
global: scopedNames(normalized, GLOBAL_SCOPE),
|
|
169
215
|
feature(identity): FeatureNames {
|
|
@@ -171,6 +217,7 @@ export function resourceNames(project: string): ProjectNames {
|
|
|
171
217
|
return {
|
|
172
218
|
resource: (binding, kind) => featureResourceName(full, binding, kind),
|
|
173
219
|
worker: (app) => featureWorkerName(full, app),
|
|
220
|
+
artifactsNamespace: () => featureArtifactsNamespace(full),
|
|
174
221
|
};
|
|
175
222
|
},
|
|
176
223
|
};
|
package/src/seed/writeD1.ts
CHANGED
|
@@ -17,8 +17,14 @@ export interface SeedD1Options {
|
|
|
17
17
|
export interface SeedD1Result {
|
|
18
18
|
/** The table seeded. */
|
|
19
19
|
table: string;
|
|
20
|
-
/** The number of rows
|
|
20
|
+
/** The number of rows the fixture holds, every one validated. */
|
|
21
21
|
rows: number;
|
|
22
|
+
/**
|
|
23
|
+
* The number of rows this call inserted. Under `INSERT OR IGNORE` a row whose key is already there is
|
|
24
|
+
* not, so a re-run writes none, and says so here rather than repeating `rows` (#764). Always `0` under
|
|
25
|
+
* `dryRun`.
|
|
26
|
+
*/
|
|
27
|
+
written: number;
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
/**
|
|
@@ -89,15 +95,18 @@ export async function seedD1Group(
|
|
|
89
95
|
});
|
|
90
96
|
|
|
91
97
|
if (options.dryRun || encoded.length === 0) {
|
|
92
|
-
return { table: group.table, rows: encoded.length };
|
|
98
|
+
return { table: group.table, rows: encoded.length, written: 0 };
|
|
93
99
|
}
|
|
94
100
|
|
|
101
|
+
let written = 0n;
|
|
95
102
|
for (const chunk of chunkRowsByBoundParameters(encoded, columnsPerRow(encoded))) {
|
|
96
|
-
await db
|
|
103
|
+
const results = await db
|
|
97
104
|
.insertInto(group.table)
|
|
98
105
|
.orIgnore()
|
|
99
106
|
.values(chunk as never)
|
|
100
107
|
.execute();
|
|
108
|
+
// D1's `meta.changes`: the rows the statement inserted, which an ignored conflict is not.
|
|
109
|
+
for (const result of results) written += result.numInsertedOrUpdatedRows ?? 0n;
|
|
101
110
|
}
|
|
102
|
-
return { table: group.table, rows: encoded.length };
|
|
111
|
+
return { table: group.table, rows: encoded.length, written: Number(written) };
|
|
103
112
|
}
|
package/src/seed/writeKv.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface SeedKvResult {
|
|
|
19
19
|
store: string;
|
|
20
20
|
/** The number of entries validated (and, unless `dryRun`, considered for writing). */
|
|
21
21
|
entries: number;
|
|
22
|
+
/** The number of entries this call put. An existing key is left alone and not counted (#764). Always `0` under `dryRun`. */
|
|
23
|
+
written: number;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
/**
|
|
@@ -87,13 +89,15 @@ export async function seedKvGroup<V extends z.ZodType, K extends z.ZodObject, M
|
|
|
87
89
|
});
|
|
88
90
|
|
|
89
91
|
if (options.dryRun) {
|
|
90
|
-
return { store: group.store, entries: group.entries.length };
|
|
92
|
+
return { store: group.store, entries: group.entries.length, written: 0 };
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
let written = 0;
|
|
93
96
|
for (const entry of group.entries) {
|
|
94
97
|
// Never overwrite an existing key — the KV counterpart of D1's INSERT OR IGNORE.
|
|
95
98
|
if (await keyExists(store, entry.key)) continue;
|
|
96
99
|
await store.put(entry.key, entry.value, entry.metadata !== undefined ? { metadata: entry.metadata } : {});
|
|
100
|
+
written += 1;
|
|
97
101
|
}
|
|
98
|
-
return { store: group.store, entries: group.entries.length };
|
|
102
|
+
return { store: group.store, entries: group.entries.length, written };
|
|
99
103
|
}
|
package/src/version.generated.ts
CHANGED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { commandName } from "../cli/command";
|
|
6
|
+
import { UpstreamError } from "../error/pithyError";
|
|
7
|
+
import { ENVIRONMENT_VAR } from "./identity";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* **A service binding under `pithy dev`: the call reaches the local target on its pinned port (#617).**
|
|
11
|
+
*
|
|
12
|
+
* Deployed, `env.APP_B.fetch(request)` is a service binding: Cloudflare runs the call inside the runtime, into
|
|
13
|
+
* the script the binding names. Locally there is no such script. `wrangler dev` would look for one in its own
|
|
14
|
+
* cross-process registry — the one `pithy dev` never relies on — and a Worker started on its own with
|
|
15
|
+
* `pithy dev --app app-a` would find nothing there at all. What does exist is the target itself, on the port its
|
|
16
|
+
* feature pinned for it, and `pithy dev` knows that address before anything starts.
|
|
17
|
+
*
|
|
18
|
+
* So `pithy dev` publishes one var, {@link SERVICE_ORIGINS_VAR}: a JSON map from each service binding this
|
|
19
|
+
* Worker declares to its target's local origin. Under `dev`, and only there, each of those bindings is replaced
|
|
20
|
+
* with a stand-in whose `fetch` sends the same request to that origin. The call site is byte-identical either
|
|
21
|
+
* way. A target that is not running refuses the connection, so the call fails at once as
|
|
22
|
+
* `core/upstream_failed`, naming the binding and the address, rather than hanging on a registry lookup.
|
|
23
|
+
*
|
|
24
|
+
* **Only `dev`, and only what was published.** The var is written by `pithy dev` and by nothing else, and the
|
|
25
|
+
* gate reads `ENVIRONMENT` off the request env as the Workflow loopback does (`workflow/dispatch.ts`): a
|
|
26
|
+
* deployment that lost its `ENVIRONMENT` var never opens it.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** The var `pithy dev` publishes: each service binding's local target, as JSON. */
|
|
30
|
+
export const SERVICE_ORIGINS_VAR = "PITHY_SERVICE_ORIGINS";
|
|
31
|
+
|
|
32
|
+
/** The one environment the stand-in is made in. */
|
|
33
|
+
const LOCAL = "dev";
|
|
34
|
+
|
|
35
|
+
/** What the var carries: binding name to an `http(s)` origin. */
|
|
36
|
+
const ServiceOrigins = z
|
|
37
|
+
.record(
|
|
38
|
+
z.string().describe("A service binding's name in the Worker env."),
|
|
39
|
+
z.url({ protocol: /^https?$/ }).describe("The local origin of the Worker it calls, as `pithy dev` pinned it."),
|
|
40
|
+
)
|
|
41
|
+
.describe("Each service binding's local target, published by `pithy dev`.");
|
|
42
|
+
|
|
43
|
+
/** The transport. The runtime's `fetch`, or a test's. */
|
|
44
|
+
export type ServiceLoopbackFetch = (request: Request) => Promise<Response>;
|
|
45
|
+
|
|
46
|
+
/** A Fetcher's one method that a plain `fetch` target uses. */
|
|
47
|
+
interface LocalService {
|
|
48
|
+
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A stand-in for one service binding: every request sent to `origin`, its path, method, headers and body kept. */
|
|
52
|
+
function localService(binding: string, origin: string, send: ServiceLoopbackFetch): LocalService {
|
|
53
|
+
const target = new URL(origin);
|
|
54
|
+
return {
|
|
55
|
+
async fetch(input, init) {
|
|
56
|
+
const request = new Request(input, init);
|
|
57
|
+
const url = new URL(request.url);
|
|
58
|
+
url.protocol = target.protocol;
|
|
59
|
+
url.host = target.host;
|
|
60
|
+
try {
|
|
61
|
+
return await send(new Request(url, request));
|
|
62
|
+
} catch (cause) {
|
|
63
|
+
throw new UpstreamError(
|
|
64
|
+
{
|
|
65
|
+
message: `The Worker ${binding} calls is not answering.`,
|
|
66
|
+
action: `Check that it is running under ${commandName("dev")} at ${target.origin}.`,
|
|
67
|
+
detail: `${request.method} ${url.href} failed: ${String(cause)}`,
|
|
68
|
+
},
|
|
69
|
+
{ cause },
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Options for {@link withLocalServices}. */
|
|
77
|
+
export interface LocalServicesOptions {
|
|
78
|
+
/** The transport. Defaults to the runtime's `fetch`. */
|
|
79
|
+
fetch?: ServiceLoopbackFetch;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The env a request runs with: under `pithy dev`, each published service binding replaced with its local
|
|
84
|
+
* stand-in; anywhere else, the env unchanged — the same object, so nothing outside `dev` pays for this.
|
|
85
|
+
*/
|
|
86
|
+
export function withLocalServices(
|
|
87
|
+
env: Record<string, unknown>,
|
|
88
|
+
options: LocalServicesOptions = {},
|
|
89
|
+
): Record<string, unknown> {
|
|
90
|
+
if (env[ENVIRONMENT_VAR] !== LOCAL) return env;
|
|
91
|
+
const published = env[SERVICE_ORIGINS_VAR];
|
|
92
|
+
if (typeof published !== "string") return env;
|
|
93
|
+
let parsed: unknown;
|
|
94
|
+
try {
|
|
95
|
+
parsed = JSON.parse(published);
|
|
96
|
+
} catch {
|
|
97
|
+
return env;
|
|
98
|
+
}
|
|
99
|
+
const origins = ServiceOrigins.safeParse(parsed);
|
|
100
|
+
if (!origins.success || Object.keys(origins.data).length === 0) return env;
|
|
101
|
+
const send = options.fetch ?? ((request: Request) => fetch(request));
|
|
102
|
+
const local: Record<string, unknown> = { ...env };
|
|
103
|
+
for (const [binding, origin] of Object.entries(origins.data)) local[binding] = localService(binding, origin, send);
|
|
104
|
+
return local;
|
|
105
|
+
}
|
package/src/workflow/faults.ts
CHANGED
|
@@ -192,6 +192,27 @@ export interface DurableStepLike extends WorkflowStepLike {
|
|
|
192
192
|
sleep(name: string, durationMs: number): Promise<void>;
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
/**
|
|
196
|
+
* The platform step's configured `do`: a retry policy ahead of the body. Every `WorkflowStep` has it; a
|
|
197
|
+
* test double that only takes two arguments does not, which is why {@link ClassifiedSteps.once} is the one
|
|
198
|
+
* place it is reached for.
|
|
199
|
+
*/
|
|
200
|
+
interface ConfiguredStepLike {
|
|
201
|
+
do<T>(name: string, config: { retries: { limit: number; delay: number } }, fn: () => Promise<T>): Promise<T>;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** What {@link classifiedSteps} hands back: a pausing runner, and a step that runs exactly once. */
|
|
205
|
+
export interface ClassifiedSteps extends DurableStepLike {
|
|
206
|
+
/**
|
|
207
|
+
* Run a named step with **no retries**, still classified (pithy-sh/pithy#602).
|
|
208
|
+
*
|
|
209
|
+
* For a body that mutates somebody else's system — a provider roll — where a platform re-running the
|
|
210
|
+
* step after an eviction would do it twice. The classification still decides what the instance's
|
|
211
|
+
* failure says; it can no longer decide on a retry, because there is none to give.
|
|
212
|
+
*/
|
|
213
|
+
once<T>(name: string, fn: () => Promise<T>): Promise<T>;
|
|
214
|
+
}
|
|
215
|
+
|
|
195
216
|
/**
|
|
196
217
|
* The platform's terminal-error class — `NonRetryableError` from `cloudflare:workflows`, taken as a
|
|
197
218
|
* parameter.
|
|
@@ -222,31 +243,37 @@ export function classifiedSteps(
|
|
|
222
243
|
step: WorkflowStepLike,
|
|
223
244
|
policy: WorkflowRetryPolicy,
|
|
224
245
|
terminalError: TerminalErrorConstructor,
|
|
225
|
-
):
|
|
226
|
-
const
|
|
246
|
+
): ClassifiedSteps {
|
|
247
|
+
const classify =
|
|
248
|
+
<T>(fn: () => Promise<T>) =>
|
|
249
|
+
async (): Promise<T> => {
|
|
250
|
+
try {
|
|
251
|
+
return await fn();
|
|
252
|
+
} catch (error) {
|
|
253
|
+
const fault = classifyWorkflowFault(error, policy);
|
|
254
|
+
if (fault.disposition === "retry") throw error;
|
|
255
|
+
// The one sanctioned non-`PithyError` throw in the kit: a platform contract, at the durable
|
|
256
|
+
// boundary, carrying the `PithyError` it replaced as its `cause` so nothing is lost in this
|
|
257
|
+
// process. Across the boundary the text is the whole channel — the engine records it and
|
|
258
|
+
// discards the throw — so what an operator is owed is encoded into it, at one stated site,
|
|
259
|
+
// and read back at one (./stepMessage). The `cause` does not travel; `detail` never did.
|
|
260
|
+
const terminal = new terminalError(
|
|
261
|
+
encodeWorkflowStepMessage({
|
|
262
|
+
code: fault.code,
|
|
263
|
+
message: messageOf(error),
|
|
264
|
+
action: pithyActionOf(error),
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
267
|
+
terminal.cause = error;
|
|
268
|
+
throw terminal;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
const runner: ClassifiedSteps = {
|
|
227
272
|
do<T>(name: string, fn: () => Promise<T>): Promise<T> {
|
|
228
|
-
return step.do(name,
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const fault = classifyWorkflowFault(error, policy);
|
|
233
|
-
if (fault.disposition === "retry") throw error;
|
|
234
|
-
// The one sanctioned non-`PithyError` throw in the kit: a platform contract, at the durable
|
|
235
|
-
// boundary, carrying the `PithyError` it replaced as its `cause` so nothing is lost in this
|
|
236
|
-
// process. Across the boundary the text is the whole channel — the engine records it and
|
|
237
|
-
// discards the throw — so what an operator is owed is encoded into it, at one stated site,
|
|
238
|
-
// and read back at one (./stepMessage). The `cause` does not travel; `detail` never did.
|
|
239
|
-
const terminal = new terminalError(
|
|
240
|
-
encodeWorkflowStepMessage({
|
|
241
|
-
code: fault.code,
|
|
242
|
-
message: messageOf(error),
|
|
243
|
-
action: pithyActionOf(error),
|
|
244
|
-
}),
|
|
245
|
-
);
|
|
246
|
-
terminal.cause = error;
|
|
247
|
-
throw terminal;
|
|
248
|
-
}
|
|
249
|
-
});
|
|
273
|
+
return step.do(name, classify(fn));
|
|
274
|
+
},
|
|
275
|
+
once<T>(name: string, fn: () => Promise<T>): Promise<T> {
|
|
276
|
+
return (step as unknown as ConfiguredStepLike).do(name, { retries: { limit: 0, delay: 0 } }, classify(fn));
|
|
250
277
|
},
|
|
251
278
|
/**
|
|
252
279
|
* Forwarded, and deliberately unclassified. A pause raises no fault of the capability's — the
|