@greatwait/contracts 0.1.0-alpha.1
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 +77 -0
- package/dist/authority.d.ts +217 -0
- package/dist/authority.js +261 -0
- package/dist/callback-ownership.d.ts +58 -0
- package/dist/callback-ownership.js +71 -0
- package/dist/callback-safety.d.ts +38 -0
- package/dist/callback-safety.js +403 -0
- package/dist/compatibility.d.ts +82 -0
- package/dist/compatibility.js +115 -0
- package/dist/delivery.d.ts +401 -0
- package/dist/delivery.js +311 -0
- package/dist/endpoints.d.ts +305 -0
- package/dist/endpoints.js +179 -0
- package/dist/events.d.ts +62 -0
- package/dist/events.js +38 -0
- package/dist/first-value-journey.d.ts +305 -0
- package/dist/first-value-journey.js +466 -0
- package/dist/forecast.d.ts +443 -0
- package/dist/forecast.js +729 -0
- package/dist/identifiers.d.ts +109 -0
- package/dist/identifiers.js +131 -0
- package/dist/identity.d.ts +196 -0
- package/dist/identity.js +182 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +27 -0
- package/dist/mcp.d.ts +51 -0
- package/dist/mcp.js +56 -0
- package/dist/oauth.d.ts +796 -0
- package/dist/oauth.js +708 -0
- package/dist/operations.d.ts +3985 -0
- package/dist/operations.js +1837 -0
- package/dist/partition.d.ts +771 -0
- package/dist/partition.js +206 -0
- package/dist/policy.d.ts +93 -0
- package/dist/policy.js +180 -0
- package/dist/problems.d.ts +158 -0
- package/dist/problems.js +104 -0
- package/dist/protocol.d.ts +308 -0
- package/dist/protocol.js +515 -0
- package/dist/scheduling-records.d.ts +395 -0
- package/dist/scheduling-records.js +284 -0
- package/dist/scheduling.d.ts +1098 -0
- package/dist/scheduling.js +1116 -0
- package/dist/secrets.d.ts +383 -0
- package/dist/secrets.js +272 -0
- package/dist/signatures.d.ts +704 -0
- package/dist/signatures.js +867 -0
- package/dist/tenancy.d.ts +178 -0
- package/dist/tenancy.js +145 -0
- package/dist/test-sink.d.ts +986 -0
- package/dist/test-sink.js +755 -0
- package/dist/time.d.ts +45 -0
- package/dist/time.js +128 -0
- package/dist/usage.d.ts +121 -0
- package/dist/usage.js +200 -0
- package/dist/webhooks.d.ts +116 -0
- package/dist/webhooks.js +161 -0
- package/dist/workspace.d.ts +92 -0
- package/dist/workspace.js +59 -0
- package/generated/v1/callback-delivery-signature.json +467 -0
- package/generated/v1/callback-endpoint-verification.json +59 -0
- package/generated/v1/callback-receiver.json +241 -0
- package/generated/v1/examples/attempts.list.response.json +24 -0
- package/generated/v1/examples/attempts.read.response.json +20 -0
- package/generated/v1/examples/credentials.create.request.json +5 -0
- package/generated/v1/examples/credentials.create.response.json +10 -0
- package/generated/v1/examples/credentials.list.response.json +17 -0
- package/generated/v1/examples/credentials.revoke.response.json +11 -0
- package/generated/v1/examples/credentials.rotate.request.json +3 -0
- package/generated/v1/examples/credentials.rotate.response.json +22 -0
- package/generated/v1/examples/endpoints.create.request.json +3 -0
- package/generated/v1/examples/endpoints.create.response.json +16 -0
- package/generated/v1/examples/endpoints.disable.response.json +15 -0
- package/generated/v1/examples/endpoints.list.response.json +21 -0
- package/generated/v1/examples/endpoints.read.response.json +14 -0
- package/generated/v1/examples/endpoints.rotateSigningKey.request.json +3 -0
- package/generated/v1/examples/endpoints.rotateSigningKey.response.json +23 -0
- package/generated/v1/examples/endpoints.update.request.json +3 -0
- package/generated/v1/examples/endpoints.update.response.json +13 -0
- package/generated/v1/examples/endpoints.verify.response.json +15 -0
- package/generated/v1/examples/forecast.read.response.json +28 -0
- package/generated/v1/examples/forecast.windows.response.json +21 -0
- package/generated/v1/examples/grants.list.response.json +17 -0
- package/generated/v1/examples/grants.revoke.response.json +16 -0
- package/generated/v1/examples/meta.scaffold.response.json +5 -0
- package/generated/v1/examples/meta.version.response.json +5 -0
- package/generated/v1/examples/occurrences.list.response.json +17 -0
- package/generated/v1/examples/occurrences.read.response.json +15 -0
- package/generated/v1/examples/occurrences.redeliver.response.json +16 -0
- package/generated/v1/examples/projects.create.request.json +3 -0
- package/generated/v1/examples/projects.create.response.json +5 -0
- package/generated/v1/examples/projects.list.response.json +9 -0
- package/generated/v1/examples/schedules.archive.response.json +25 -0
- package/generated/v1/examples/schedules.cancel.response.json +24 -0
- package/generated/v1/examples/schedules.create.request.json +9 -0
- package/generated/v1/examples/schedules.create.response.json +25 -0
- package/generated/v1/examples/schedules.list.response.json +23 -0
- package/generated/v1/examples/schedules.pause.response.json +23 -0
- package/generated/v1/examples/schedules.read.response.json +25 -0
- package/generated/v1/examples/schedules.replace.request.json +8 -0
- package/generated/v1/examples/schedules.replace.response.json +25 -0
- package/generated/v1/examples/schedules.resume.response.json +23 -0
- package/generated/v1/examples/sinkdeliveries.list.response.json +19 -0
- package/generated/v1/examples/sinkdeliveries.read.response.json +16 -0
- package/generated/v1/examples/testsinks.configure.request.json +3 -0
- package/generated/v1/examples/testsinks.configure.response.json +13 -0
- package/generated/v1/examples/testsinks.create.request.json +8 -0
- package/generated/v1/examples/testsinks.create.response.json +13 -0
- package/generated/v1/examples/testsinks.delete.response.json +13 -0
- package/generated/v1/examples/testsinks.list.response.json +20 -0
- package/generated/v1/examples/testsinks.read.response.json +14 -0
- package/generated/v1/examples/usage.read.response.json +20 -0
- package/generated/v1/first-value-journey.json +436 -0
- package/generated/v1/manifest.json +1037 -0
- package/generated/v1/openapi.json +23665 -0
- package/generated/v1/policy-matrix.json +305 -0
- package/generated/v1/schemas/attempts.list.query.json +16 -0
- package/generated/v1/schemas/attempts.list.response.json +175 -0
- package/generated/v1/schemas/attempts.read.response.json +168 -0
- package/generated/v1/schemas/credentials.create.request.json +39 -0
- package/generated/v1/schemas/credentials.create.response.json +88 -0
- package/generated/v1/schemas/credentials.list.query.json +16 -0
- package/generated/v1/schemas/credentials.list.response.json +124 -0
- package/generated/v1/schemas/credentials.revoke.response.json +84 -0
- package/generated/v1/schemas/credentials.rotate.request.json +11 -0
- package/generated/v1/schemas/credentials.rotate.response.json +171 -0
- package/generated/v1/schemas/endpoints.create.request.json +12 -0
- package/generated/v1/schemas/endpoints.create.response.json +115 -0
- package/generated/v1/schemas/endpoints.disable.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.disable.response.json +100 -0
- package/generated/v1/schemas/endpoints.list.query.json +16 -0
- package/generated/v1/schemas/endpoints.list.response.json +146 -0
- package/generated/v1/schemas/endpoints.read.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.read.response.json +100 -0
- package/generated/v1/schemas/endpoints.rotateSigningKey.request-headers.json +10 -0
- package/generated/v1/schemas/endpoints.rotateSigningKey.request.json +11 -0
- package/generated/v1/schemas/endpoints.rotateSigningKey.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.rotateSigningKey.response.json +118 -0
- package/generated/v1/schemas/endpoints.update.request-headers.json +10 -0
- package/generated/v1/schemas/endpoints.update.request.json +12 -0
- package/generated/v1/schemas/endpoints.update.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.update.response.json +100 -0
- package/generated/v1/schemas/endpoints.verify.request-headers.json +10 -0
- package/generated/v1/schemas/endpoints.verify.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.verify.response.json +100 -0
- package/generated/v1/schemas/forecast.read.query.json +26 -0
- package/generated/v1/schemas/forecast.read.response.json +100 -0
- package/generated/v1/schemas/forecast.windows.query.json +26 -0
- package/generated/v1/schemas/forecast.windows.response.json +94 -0
- package/generated/v1/schemas/grants.list.response.json +98 -0
- package/generated/v1/schemas/grants.revoke.response.json +95 -0
- package/generated/v1/schemas/meta.scaffold.response.json +36 -0
- package/generated/v1/schemas/meta.version.response.json +19 -0
- package/generated/v1/schemas/occurrences.list.query.json +16 -0
- package/generated/v1/schemas/occurrences.list.response.json +83 -0
- package/generated/v1/schemas/occurrences.read.response.json +76 -0
- package/generated/v1/schemas/occurrences.redeliver.request-headers.json +13 -0
- package/generated/v1/schemas/occurrences.redeliver.response-headers.json +10 -0
- package/generated/v1/schemas/occurrences.redeliver.response.json +168 -0
- package/generated/v1/schemas/projects.create.request.json +12 -0
- package/generated/v1/schemas/projects.create.response.json +25 -0
- package/generated/v1/schemas/projects.list.response.json +35 -0
- package/generated/v1/schemas/schedules.archive.request-headers.json +10 -0
- package/generated/v1/schemas/schedules.archive.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.archive.response.json +189 -0
- package/generated/v1/schemas/schedules.cancel.request-headers.json +10 -0
- package/generated/v1/schemas/schedules.cancel.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.cancel.response.json +189 -0
- package/generated/v1/schemas/schedules.create.request-headers.json +12 -0
- package/generated/v1/schemas/schedules.create.request.json +99 -0
- package/generated/v1/schemas/schedules.create.response-headers.json +15 -0
- package/generated/v1/schemas/schedules.create.response.json +189 -0
- package/generated/v1/schemas/schedules.list.query.json +16 -0
- package/generated/v1/schemas/schedules.list.response.json +170 -0
- package/generated/v1/schemas/schedules.pause.request-headers.json +10 -0
- package/generated/v1/schemas/schedules.pause.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.pause.response.json +189 -0
- package/generated/v1/schemas/schedules.read.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.read.response.json +189 -0
- package/generated/v1/schemas/schedules.replace.request-headers.json +10 -0
- package/generated/v1/schemas/schedules.replace.request.json +99 -0
- package/generated/v1/schemas/schedules.replace.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.replace.response.json +189 -0
- package/generated/v1/schemas/schedules.resume.request-headers.json +10 -0
- package/generated/v1/schemas/schedules.resume.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.resume.response.json +189 -0
- package/generated/v1/schemas/sinkdeliveries.list.query.json +16 -0
- package/generated/v1/schemas/sinkdeliveries.list.response.json +126 -0
- package/generated/v1/schemas/sinkdeliveries.read.response.json +98 -0
- package/generated/v1/schemas/testsinks.configure.request.json +66 -0
- package/generated/v1/schemas/testsinks.configure.response.json +115 -0
- package/generated/v1/schemas/testsinks.create.request.json +76 -0
- package/generated/v1/schemas/testsinks.create.response.json +115 -0
- package/generated/v1/schemas/testsinks.delete.response.json +115 -0
- package/generated/v1/schemas/testsinks.list.query.json +16 -0
- package/generated/v1/schemas/testsinks.list.response.json +161 -0
- package/generated/v1/schemas/testsinks.read.response.json +115 -0
- package/generated/v1/schemas/usage.read.query.json +15 -0
- package/generated/v1/schemas/usage.read.response.json +71 -0
- package/generated/v1/snippets/attempts.list.json +5 -0
- package/generated/v1/snippets/attempts.read.json +5 -0
- package/generated/v1/snippets/credentials.create.json +5 -0
- package/generated/v1/snippets/credentials.list.json +5 -0
- package/generated/v1/snippets/credentials.revoke.json +5 -0
- package/generated/v1/snippets/credentials.rotate.json +5 -0
- package/generated/v1/snippets/endpoints.create.json +5 -0
- package/generated/v1/snippets/endpoints.disable.json +5 -0
- package/generated/v1/snippets/endpoints.list.json +5 -0
- package/generated/v1/snippets/endpoints.read.json +5 -0
- package/generated/v1/snippets/endpoints.rotateSigningKey.json +5 -0
- package/generated/v1/snippets/endpoints.update.json +5 -0
- package/generated/v1/snippets/endpoints.verify.json +5 -0
- package/generated/v1/snippets/forecast.read.json +5 -0
- package/generated/v1/snippets/forecast.windows.json +5 -0
- package/generated/v1/snippets/grants.list.json +5 -0
- package/generated/v1/snippets/grants.revoke.json +5 -0
- package/generated/v1/snippets/meta.scaffold.json +5 -0
- package/generated/v1/snippets/meta.version.json +5 -0
- package/generated/v1/snippets/occurrences.list.json +5 -0
- package/generated/v1/snippets/occurrences.read.json +5 -0
- package/generated/v1/snippets/occurrences.redeliver.json +5 -0
- package/generated/v1/snippets/projects.create.json +5 -0
- package/generated/v1/snippets/projects.list.json +5 -0
- package/generated/v1/snippets/schedules.archive.json +5 -0
- package/generated/v1/snippets/schedules.cancel.json +5 -0
- package/generated/v1/snippets/schedules.create.json +5 -0
- package/generated/v1/snippets/schedules.list.json +5 -0
- package/generated/v1/snippets/schedules.pause.json +5 -0
- package/generated/v1/snippets/schedules.read.json +5 -0
- package/generated/v1/snippets/schedules.replace.json +5 -0
- package/generated/v1/snippets/schedules.resume.json +5 -0
- package/generated/v1/snippets/sinkdeliveries.list.json +5 -0
- package/generated/v1/snippets/sinkdeliveries.read.json +5 -0
- package/generated/v1/snippets/testsinks.configure.json +5 -0
- package/generated/v1/snippets/testsinks.create.json +5 -0
- package/generated/v1/snippets/testsinks.delete.json +5 -0
- package/generated/v1/snippets/testsinks.list.json +5 -0
- package/generated/v1/snippets/testsinks.read.json +5 -0
- package/generated/v1/snippets/usage.read.json +5 -0
- package/package.json +153 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seam between the API Worker and a Scheduler Partition Durable Object.
|
|
3
|
+
*
|
|
4
|
+
* **This is not a public wire shape.** Nothing here is served to a customer,
|
|
5
|
+
* appears in OpenAPI, or is reachable from the internet — a Durable Object has
|
|
6
|
+
* no route, and only a Worker holding the namespace binding can address one.
|
|
7
|
+
* It lives in the contracts package for the single reason that two workspaces
|
|
8
|
+
* need to agree on it and this is the only package both `apps/api` and
|
|
9
|
+
* `apps/scheduler` may depend on. The alternative was each side writing the
|
|
10
|
+
* shape out separately, which is how `apps/scheduler/src/contract-runtime.ts`
|
|
11
|
+
* became a filed debt.
|
|
12
|
+
*
|
|
13
|
+
* It carries **no Zod and no runtime import of the contract barrel**. The
|
|
14
|
+
* addressing and routing functions below are a string join and a hash, and the
|
|
15
|
+
* rest is types, which are erased. That matters: `apps/scheduler` is the Worker
|
|
16
|
+
* every Durable Object cold start parses, and W2-08 measured the contract
|
|
17
|
+
* barrel's runtime exports at 3.4x its bundle (see
|
|
18
|
+
* `apps/scheduler/src/contract-runtime.ts`).
|
|
19
|
+
*
|
|
20
|
+
* **`@greatwait/contracts/partition` is a subpath export for that reason**,
|
|
21
|
+
* added by W2-05 when the scheduler needed the route function at runtime rather
|
|
22
|
+
* than only its types. Importing it through the barrel would have reached Zod
|
|
23
|
+
* and undone the whole of `contract-runtime.ts`. This is a narrow instance of
|
|
24
|
+
* what [FXO-1404](https://linear.app/flowxo/issue/FXO-1404) asks for in
|
|
25
|
+
* general — a Zod-free entry point — and it does not close that issue: the
|
|
26
|
+
* identifier, timestamp and problem helpers `contract-runtime.ts` copies still
|
|
27
|
+
* live in modules that do reach Zod.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* The shard every partition had until W2-05, and the only one an unsharded
|
|
31
|
+
* deployment ever uses.
|
|
32
|
+
*
|
|
33
|
+
* Named rather than written as `"0"` at two call sites, because the API Worker
|
|
34
|
+
* and the partition must derive the same Durable Object name and a literal
|
|
35
|
+
* repeated across a workspace boundary is a literal that will eventually
|
|
36
|
+
* disagree with itself. `partitionShardFor` returns exactly this when the shard
|
|
37
|
+
* count is one, so every Durable Object name a single-shard deployment produces
|
|
38
|
+
* is byte-identical to the ones it produced before sharding existed.
|
|
39
|
+
*/
|
|
40
|
+
export const SINGLE_SHARD = "0";
|
|
41
|
+
/**
|
|
42
|
+
* Projection intents handed over in one call.
|
|
43
|
+
*
|
|
44
|
+
* **Declared here because both sides of the seam enforce it, and a bound
|
|
45
|
+
* enforced twice from two workspaces is a bound that will eventually disagree
|
|
46
|
+
* with itself.** The API Worker asks for this many; the partition clamps what it
|
|
47
|
+
* hands over to it and *refuses* a settle carrying more. If the asking number
|
|
48
|
+
* ever exceeded the refusing one, every settle would be refused, no intent would
|
|
49
|
+
* ever be retired, the outbox would grow to `PARTITION_LIMITS.outboxRows` and
|
|
50
|
+
* creates would then start failing for bookkeeping — a customer-visible outage
|
|
51
|
+
* produced by two constants drifting apart. The test-gap audit named exactly
|
|
52
|
+
* that, and the fix is one declaration rather than a test comparing two.
|
|
53
|
+
*
|
|
54
|
+
* Thirty-two is one D1 batch's worth of work and a few kilobytes of response.
|
|
55
|
+
* The caller comes back for the rest, and `RetainedProjections.pending` is how
|
|
56
|
+
* it knows to.
|
|
57
|
+
*/
|
|
58
|
+
export const PROJECTION_DRAIN_LIMIT = 32;
|
|
59
|
+
/**
|
|
60
|
+
* The most shards one project/environment may be spread across.
|
|
61
|
+
*
|
|
62
|
+
* Eight, and the number is borrowed rather than invented:
|
|
63
|
+
* `apps/scheduler/src/routing.ts` clamps `HOST_EXECUTOR_SHARDS` at eight, and a
|
|
64
|
+
* repository carrying two different bounds on two knobs of the same shape is a
|
|
65
|
+
* repository where somebody eventually applies the wrong one. The costs differ
|
|
66
|
+
* but they point the same way — an executor shard divides a host budget, and a
|
|
67
|
+
* partition shard divides a project's schedules across Durable Objects.
|
|
68
|
+
*
|
|
69
|
+
* A listing had to fan out across all of them until W2-06 served listings from
|
|
70
|
+
* D1, which was the sharpest reason for a bound and is now gone. What is left is
|
|
71
|
+
* still a real fan-out: a listing whose projection is stale re-confirms every
|
|
72
|
+
* partition it covers, and a repair after an outage hands over from each in
|
|
73
|
+
* turn. Eight is where that stops being a request.
|
|
74
|
+
*
|
|
75
|
+
* A value above this is clamped rather than refused, which is upstream's
|
|
76
|
+
* judgement in `executorShardCount` and holds for the same reason: an operator
|
|
77
|
+
* asking for sixteen still wants sharding, just bounded.
|
|
78
|
+
*/
|
|
79
|
+
export const MAX_PARTITION_SHARDS = 8;
|
|
80
|
+
/**
|
|
81
|
+
* FNV-1a, 32-bit, with the standard constants.
|
|
82
|
+
*
|
|
83
|
+
* A second copy of `apps/scheduler/src/routing.ts`'s `fnv1a32`, and the
|
|
84
|
+
* duplication is deliberate rather than overlooked. That file is copied
|
|
85
|
+
* upstream source which W3-03 retenants, and this package may not depend on an
|
|
86
|
+
* application workspace in either direction. `partition.test.ts` holds the two
|
|
87
|
+
* equal over a vector set, which is the same arrangement
|
|
88
|
+
* `partition-contract-parity.test.ts` uses for the state vocabularies: the
|
|
89
|
+
* duplication is safe because a divergence fails a test rather than shipping.
|
|
90
|
+
*/
|
|
91
|
+
function fnv1a32(input) {
|
|
92
|
+
let hash = 0x811c9dc5;
|
|
93
|
+
for (let index = 0; index < input.length; index++) {
|
|
94
|
+
hash ^= input.charCodeAt(index);
|
|
95
|
+
hash = Math.imul(hash, 0x01000193);
|
|
96
|
+
}
|
|
97
|
+
return hash >>> 0;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* How many shards a deployment spreads one project/environment across.
|
|
101
|
+
*
|
|
102
|
+
* Reads the raw environment string rather than an `Env`, so both Workers parse
|
|
103
|
+
* it the same way without either of them naming the other's bindings. Missing,
|
|
104
|
+
* unparseable, non-integer and below-one all resolve to one — the unsharded
|
|
105
|
+
* default — because a partition count is the thing a *routing* decision is made
|
|
106
|
+
* from, and a routing decision made from a garbage value is worse than one made
|
|
107
|
+
* from the default.
|
|
108
|
+
*
|
|
109
|
+
* **Changing it moves where existing identifiers resolve, so it is retunable
|
|
110
|
+
* only under a precondition, and that is the most important thing to know about
|
|
111
|
+
* it.** A schedule's shard is derived from its identifier. Before W2-06 that was
|
|
112
|
+
* the only answer, so raising the count stranded every schedule already stored.
|
|
113
|
+
* Now the shard a schedule was actually stored in is *recorded* in the D1
|
|
114
|
+
* directory, and `resolveSchedule` falls back to it whenever the derived
|
|
115
|
+
* partition has nothing — so a schedule created under an older count keeps
|
|
116
|
+
* working, because its route was written down rather than recomputed.
|
|
117
|
+
*
|
|
118
|
+
* The precondition is that every schedule **has** such a route, which is exactly
|
|
119
|
+
* what `greatwait_projection_checkpoint` reporting `pending: 0` for every
|
|
120
|
+
* partition means. A schedule whose projection never landed has no recorded
|
|
121
|
+
* route and no derivable one, and it is unreachable until its partition is
|
|
122
|
+
* drained. `docs/operations/cloudflare-environments.md` carries the procedure
|
|
123
|
+
* and the two costs it does not remove — a keyed create's idempotency receipt is
|
|
124
|
+
* stranded for its 24-hour life, and detail reads of pre-change schedules cost
|
|
125
|
+
* an extra round trip for as long as they exist.
|
|
126
|
+
*
|
|
127
|
+
* It is still not `HOST_EXECUTOR_SHARDS`, which rolls `1 → 4 → 8` under load
|
|
128
|
+
* because an executor holds no durable state. This one holds all of it, and
|
|
129
|
+
* moving it is a planned operation rather than a dial.
|
|
130
|
+
*/
|
|
131
|
+
export function partitionShardCount(raw) {
|
|
132
|
+
if (raw === undefined) {
|
|
133
|
+
return 1;
|
|
134
|
+
}
|
|
135
|
+
const count = Number(raw);
|
|
136
|
+
if (!Number.isInteger(count) || count < 1) {
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
return Math.min(count, MAX_PARTITION_SHARDS);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The shard a schedule identifier belongs to.
|
|
143
|
+
*
|
|
144
|
+
* Architecture section 4's route function: the stable opaque schedule ID hashed
|
|
145
|
+
* into a project/environment shard, with the count an operational configuration
|
|
146
|
+
* rather than a public contract. Nothing a *client* sends reaches this — the
|
|
147
|
+
* identifier is minted by the API Worker — so a caller can neither choose their
|
|
148
|
+
* shard nor learn how many there are.
|
|
149
|
+
*/
|
|
150
|
+
export function partitionShardFor(scheduleId, shardCount) {
|
|
151
|
+
return shardOf(`sch:${scheduleId}`, shardCount);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The shard a create carrying an idempotency key routes to.
|
|
155
|
+
*
|
|
156
|
+
* **A create is the one operation that cannot route by schedule identifier,
|
|
157
|
+
* because it is the operation that mints one.** Every other operation is handed
|
|
158
|
+
* an identifier and derives the shard from it; a create has to pick a partition
|
|
159
|
+
* first and name the schedule afterwards. Picking randomly would break
|
|
160
|
+
* idempotency the moment there is more than one shard: the retry that carries
|
|
161
|
+
* the same key would mint a different identifier, land in a different Durable
|
|
162
|
+
* Object, find no receipt there, and create a second schedule — the exact
|
|
163
|
+
* failure the receipt exists to prevent.
|
|
164
|
+
*
|
|
165
|
+
* So a keyed create routes on the key, and the identifier is then minted *into*
|
|
166
|
+
* that shard (`mintScheduleIdIn`), which keeps `partitionShardFor` the single
|
|
167
|
+
* answer for every later read. A create with no key routes on the identifier it
|
|
168
|
+
* mints, because there is nothing to be idempotent about.
|
|
169
|
+
*
|
|
170
|
+
* The `idk:` prefix separates the two domains, so an identifier and a key that
|
|
171
|
+
* happen to be the same string cannot be conflated into the same hash input.
|
|
172
|
+
* The key is customer input and it selects a shard **inside the caller's own
|
|
173
|
+
* authority tuple**: it cannot reach another organization, project or
|
|
174
|
+
* environment, because those are the other components of the Durable Object
|
|
175
|
+
* name and none of them comes from the request.
|
|
176
|
+
*/
|
|
177
|
+
export function partitionShardForIdempotencyKey(key, shardCount) {
|
|
178
|
+
return shardOf(`idk:${key}`, shardCount);
|
|
179
|
+
}
|
|
180
|
+
function shardOf(input, shardCount) {
|
|
181
|
+
return shardCount <= 1 ? SINGLE_SHARD : String(fnv1a32(input) % shardCount);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The Durable Object instance name for one authority tuple and shard.
|
|
185
|
+
*
|
|
186
|
+
* Deriving the name from the authority is what makes a cross-tenant read reach
|
|
187
|
+
* a *different object*, so tenancy is enforced by addressing before any code
|
|
188
|
+
* runs. The partition's own bound-identity check is then defence against a
|
|
189
|
+
* defect in this function rather than the only thing standing between two
|
|
190
|
+
* tenants.
|
|
191
|
+
*
|
|
192
|
+
* `:` is safe as a separator because every component is either a prefixed
|
|
193
|
+
* identifier drawn from Crockford base32 or the fixed literal `test`/`live`,
|
|
194
|
+
* and none of those can contain it. Callers must validate the tuple before
|
|
195
|
+
* calling: this function makes a name, it does not police one.
|
|
196
|
+
*/
|
|
197
|
+
export function partitionInstanceName(authority, shard) {
|
|
198
|
+
return [
|
|
199
|
+
authority.organization,
|
|
200
|
+
authority.project,
|
|
201
|
+
authority.environment,
|
|
202
|
+
authority.environmentKind,
|
|
203
|
+
"s",
|
|
204
|
+
shard,
|
|
205
|
+
].join(":");
|
|
206
|
+
}
|
package/dist/policy.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type PrincipalKind, type Scope } from "./authority.js";
|
|
2
|
+
import { type MembershipRole } from "./identity.js";
|
|
3
|
+
import { type OperationDefinition } from "./operations.js";
|
|
4
|
+
export declare const ROLE_SCOPE_CEILINGS: Readonly<Record<MembershipRole, readonly Scope[]>>;
|
|
5
|
+
/** Everything a role may hold, expanded so a write implies its read. */
|
|
6
|
+
export declare function ceilingForRole(role: MembershipRole): Set<Scope>;
|
|
7
|
+
/** Whether a role may ever hold a scope. */
|
|
8
|
+
export declare function roleAllows(role: MembershipRole, scope: Scope): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Narrow requested scopes to what a role may actually hold.
|
|
11
|
+
*
|
|
12
|
+
* The rule the project specification states as "OAuth scopes cannot exceed the
|
|
13
|
+
* granting member's current role". Expressed as an intersection rather than a
|
|
14
|
+
* validation, deliberately: a grant request naming a scope above the ceiling is
|
|
15
|
+
* *reduced*, not refused. Refusing would tell the caller which scopes exist above
|
|
16
|
+
* their role, and a partial grant is what the OAuth consent screen has to show
|
|
17
|
+
* anyway.
|
|
18
|
+
*
|
|
19
|
+
* Order follows `SCOPES`, so the result is stable regardless of how the request
|
|
20
|
+
* was ordered — a granted-scope list that reorders between requests looks like a
|
|
21
|
+
* change to anything comparing them.
|
|
22
|
+
*/
|
|
23
|
+
export declare function capScopesToRole(requested: readonly Scope[], role: MembershipRole): readonly Scope[];
|
|
24
|
+
/**
|
|
25
|
+
* The scopes an OAuth client may ask a member to grant.
|
|
26
|
+
*
|
|
27
|
+
* **Derived from what the operations actually accept, not from `SCOPES`.** The
|
|
28
|
+
* authorization server advertises a `scopes_supported` list and the consent
|
|
29
|
+
* screen offers it; without this, a client could request — and a member could
|
|
30
|
+
* approve — a scope whose only operation refuses OAuth principals outright. The
|
|
31
|
+
* grant would look granted and buy nothing, which is the worst kind of consent:
|
|
32
|
+
* one that misrepresents what was agreed to.
|
|
33
|
+
*
|
|
34
|
+
* `projects:read` is the first scope this excludes. Listing an organization's
|
|
35
|
+
* projects is a signed-in member's question, and a token's authority already is
|
|
36
|
+
* one project.
|
|
37
|
+
*/
|
|
38
|
+
export declare function grantableScopes(): readonly Scope[];
|
|
39
|
+
/**
|
|
40
|
+
* The scopes an API credential can actually use.
|
|
41
|
+
*
|
|
42
|
+
* **`grantableScopes`'s sibling, and it exists for the same reason.** That one
|
|
43
|
+
* stops a consent screen offering a scope whose only operation refuses OAuth
|
|
44
|
+
* principals — "a grant that looks granted and buys nothing, which is the worst
|
|
45
|
+
* kind of consent". A credential-issuing surface has the identical exposure: a
|
|
46
|
+
* key issued with a scope no credential-callable operation declares is a key
|
|
47
|
+
* that will be refused `authentication_required` on the only route it names,
|
|
48
|
+
* for ever, and nothing about the key says so.
|
|
49
|
+
*
|
|
50
|
+
* `projects:read` was the first such scope and `projects:write` is the second —
|
|
51
|
+
* both belong to operations that accept only a signed-in member, because a
|
|
52
|
+
* key's authority already *is* one project. Deriving the list rather than
|
|
53
|
+
* naming them is what keeps a third from arriving unnoticed.
|
|
54
|
+
*/
|
|
55
|
+
export declare function credentialScopes(): readonly Scope[];
|
|
56
|
+
/** One row of the generated policy matrix. */
|
|
57
|
+
export interface PolicyMatrixRow {
|
|
58
|
+
/**
|
|
59
|
+
* Scopes only some calls to this operation need.
|
|
60
|
+
*
|
|
61
|
+
* Present on the row rather than folded into `scopes`, because folding them
|
|
62
|
+
* would say every caller needs them — and `roles` below would then report
|
|
63
|
+
* that a member cannot rotate a credential at all, which is false.
|
|
64
|
+
*/
|
|
65
|
+
readonly conditionalScopes?: readonly Scope[];
|
|
66
|
+
readonly mutates: boolean;
|
|
67
|
+
readonly operation: string;
|
|
68
|
+
/** Which kinds of principal may perform it. Empty means none is needed. */
|
|
69
|
+
readonly principals: readonly PrincipalKind[];
|
|
70
|
+
/** Roles that may perform it, in `MEMBERSHIP_ROLES` order. */
|
|
71
|
+
readonly roles: readonly MembershipRole[];
|
|
72
|
+
/** Scopes the operation requires. Empty means no principal is needed. */
|
|
73
|
+
readonly scopes: readonly Scope[];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Whether a role can perform an operation.
|
|
77
|
+
*
|
|
78
|
+
* An operation requiring no scopes needs no principal at all, so every role can
|
|
79
|
+
* reach it — and so can an unauthenticated caller. That is stated here rather
|
|
80
|
+
* than left implicit because "requires no scope" and "is public" are the same
|
|
81
|
+
* sentence, and a reader should not have to infer it.
|
|
82
|
+
*/
|
|
83
|
+
export declare function roleCanPerform(role: MembershipRole, operation: OperationDefinition): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* The policy matrix, derived from the operation registry.
|
|
86
|
+
*
|
|
87
|
+
* Generated rather than written down. A hand-maintained matrix is a second
|
|
88
|
+
* statement of the same policy, and the two drift — which is precisely what the
|
|
89
|
+
* artifact pipeline exists to prevent. Adding an operation to `OPERATIONS`
|
|
90
|
+
* changes this output, and `check:contracts` fails until the committed artifact
|
|
91
|
+
* matches.
|
|
92
|
+
*/
|
|
93
|
+
export declare function policyMatrix(): readonly PolicyMatrixRow[];
|
package/dist/policy.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { SCOPES, effectiveScopes, } from "./authority.js";
|
|
2
|
+
import { MEMBERSHIP_ROLES } from "./identity.js";
|
|
3
|
+
import { OPERATIONS } from "./operations.js";
|
|
4
|
+
/** The registry as its declared interface. See `grantableScopes` for why. */
|
|
5
|
+
function allOperations() {
|
|
6
|
+
return OPERATIONS;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* One policy for every way in.
|
|
10
|
+
*
|
|
11
|
+
* A Clerk session, an API credential, and an OAuth grant all resolve to the same
|
|
12
|
+
* `Principal`, and every one of them is authorized by the rules here. That is the
|
|
13
|
+
* point of W1-03: the legacy scheduler let each entry point decide for itself, so
|
|
14
|
+
* whichever one was least careful became the effective policy.
|
|
15
|
+
*
|
|
16
|
+
* **Scope names are public.** Renaming one is a contract change, not a
|
|
17
|
+
* refactor — an API credential issued yesterday carries the old name, and a
|
|
18
|
+
* customer's code names it too. `SCOPES` is the frozen list; this module says who
|
|
19
|
+
* may hold which.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The most a human role may ever hold.
|
|
23
|
+
*
|
|
24
|
+
* A ceiling, not a grant. An API credential or OAuth grant is issued *within* a
|
|
25
|
+
* member's ceiling and is usually narrower; nothing anywhere may exceed it. That
|
|
26
|
+
* asymmetry is what stops a member minting a credential more powerful than
|
|
27
|
+
* themselves, which is the standard privilege-escalation route in a system with
|
|
28
|
+
* delegated credentials.
|
|
29
|
+
*
|
|
30
|
+
* `admin` holds everything, and is written as `SCOPES` rather than a copied list
|
|
31
|
+
* so a new scope cannot be introduced that admins silently lack.
|
|
32
|
+
*
|
|
33
|
+
* `member` is everything except `credentials:administer` — acting on a
|
|
34
|
+
* credential somebody else issued.
|
|
35
|
+
*
|
|
36
|
+
* **It was everything except credential management entirely, and the operator
|
|
37
|
+
* changed that on 2026-08-15.** The reason it changed is that the argument for
|
|
38
|
+
* the old line did not support it: the comment above justifies the *ceiling
|
|
39
|
+
* mechanism*, which stops a credential exceeding its issuer, and that holds
|
|
40
|
+
* whatever the ceiling contains. What withholding `credentials:write` actually
|
|
41
|
+
* bought was a developer who could already create live schedules and live
|
|
42
|
+
* destinations from the browser, and had to ask somebody else for the key that
|
|
43
|
+
* does the same thing by another route. This is a developer tool and a `member`
|
|
44
|
+
* is a developer.
|
|
45
|
+
*
|
|
46
|
+
* **What is left is the distinction that survives being asked "who does this
|
|
47
|
+
* protect".** Your own access is yours; somebody else's is not, and taking a
|
|
48
|
+
* colleague's key away — or rotating it, which retires it on a delay — belongs
|
|
49
|
+
* with the role that manages the organization. Inviting and removing people is
|
|
50
|
+
* Clerk's and this product does not do it, so this is the whole of what `admin`
|
|
51
|
+
* means here.
|
|
52
|
+
*/
|
|
53
|
+
const MEMBER_SCOPES = SCOPES.filter((scope) => scope !== "credentials:administer");
|
|
54
|
+
export const ROLE_SCOPE_CEILINGS = {
|
|
55
|
+
admin: SCOPES,
|
|
56
|
+
member: MEMBER_SCOPES,
|
|
57
|
+
};
|
|
58
|
+
/** Everything a role may hold, expanded so a write implies its read. */
|
|
59
|
+
export function ceilingForRole(role) {
|
|
60
|
+
return effectiveScopes(ROLE_SCOPE_CEILINGS[role]);
|
|
61
|
+
}
|
|
62
|
+
/** Whether a role may ever hold a scope. */
|
|
63
|
+
export function roleAllows(role, scope) {
|
|
64
|
+
return ceilingForRole(role).has(scope);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Narrow requested scopes to what a role may actually hold.
|
|
68
|
+
*
|
|
69
|
+
* The rule the project specification states as "OAuth scopes cannot exceed the
|
|
70
|
+
* granting member's current role". Expressed as an intersection rather than a
|
|
71
|
+
* validation, deliberately: a grant request naming a scope above the ceiling is
|
|
72
|
+
* *reduced*, not refused. Refusing would tell the caller which scopes exist above
|
|
73
|
+
* their role, and a partial grant is what the OAuth consent screen has to show
|
|
74
|
+
* anyway.
|
|
75
|
+
*
|
|
76
|
+
* Order follows `SCOPES`, so the result is stable regardless of how the request
|
|
77
|
+
* was ordered — a granted-scope list that reorders between requests looks like a
|
|
78
|
+
* change to anything comparing them.
|
|
79
|
+
*/
|
|
80
|
+
export function capScopesToRole(requested, role) {
|
|
81
|
+
const ceiling = ceilingForRole(role);
|
|
82
|
+
const asked = new Set(requested);
|
|
83
|
+
return SCOPES.filter((scope) => asked.has(scope) && ceiling.has(scope));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The scopes an OAuth client may ask a member to grant.
|
|
87
|
+
*
|
|
88
|
+
* **Derived from what the operations actually accept, not from `SCOPES`.** The
|
|
89
|
+
* authorization server advertises a `scopes_supported` list and the consent
|
|
90
|
+
* screen offers it; without this, a client could request — and a member could
|
|
91
|
+
* approve — a scope whose only operation refuses OAuth principals outright. The
|
|
92
|
+
* grant would look granted and buy nothing, which is the worst kind of consent:
|
|
93
|
+
* one that misrepresents what was agreed to.
|
|
94
|
+
*
|
|
95
|
+
* `projects:read` is the first scope this excludes. Listing an organization's
|
|
96
|
+
* projects is a signed-in member's question, and a token's authority already is
|
|
97
|
+
* one project.
|
|
98
|
+
*/
|
|
99
|
+
export function grantableScopes() {
|
|
100
|
+
// `.some` rather than `.includes`: `OPERATIONS` is a literal array, so each
|
|
101
|
+
// entry's `principals` is its own tuple type and `includes` narrows its
|
|
102
|
+
// argument to that tuple's members — which is `never` across the union.
|
|
103
|
+
// **`operationsFor` rather than `OPERATIONS`**: the registry is a
|
|
104
|
+
// const-asserted tuple, so each entry is its own literal type and an optional
|
|
105
|
+
// field absent from one entry is absent from the union. Reading it as the
|
|
106
|
+
// declared interface is what lets an optional field be read at all.
|
|
107
|
+
const grantable = new Set(allOperations()
|
|
108
|
+
.filter((operation) => operation.principals.some((kind) => kind === "oauth"))
|
|
109
|
+
.flatMap((operation) => [
|
|
110
|
+
...operation.scopes,
|
|
111
|
+
// **And the ones only some calls need.** A scope an operation can refuse
|
|
112
|
+
// for but never declares is a scope no grant can carry, so the refusal
|
|
113
|
+
// would be unreachable through OAuth — and `credentials:administer` is
|
|
114
|
+
// the only scope a member does not hold, which would have left every
|
|
115
|
+
// role-narrowing rule on the consent path with nothing to narrow.
|
|
116
|
+
...(operation.conditionalScopes ?? []),
|
|
117
|
+
]));
|
|
118
|
+
// `SCOPES` order, so the advertised list is stable regardless of registry order.
|
|
119
|
+
return SCOPES.filter((scope) => grantable.has(scope));
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The scopes an API credential can actually use.
|
|
123
|
+
*
|
|
124
|
+
* **`grantableScopes`'s sibling, and it exists for the same reason.** That one
|
|
125
|
+
* stops a consent screen offering a scope whose only operation refuses OAuth
|
|
126
|
+
* principals — "a grant that looks granted and buys nothing, which is the worst
|
|
127
|
+
* kind of consent". A credential-issuing surface has the identical exposure: a
|
|
128
|
+
* key issued with a scope no credential-callable operation declares is a key
|
|
129
|
+
* that will be refused `authentication_required` on the only route it names,
|
|
130
|
+
* for ever, and nothing about the key says so.
|
|
131
|
+
*
|
|
132
|
+
* `projects:read` was the first such scope and `projects:write` is the second —
|
|
133
|
+
* both belong to operations that accept only a signed-in member, because a
|
|
134
|
+
* key's authority already *is* one project. Deriving the list rather than
|
|
135
|
+
* naming them is what keeps a third from arriving unnoticed.
|
|
136
|
+
*/
|
|
137
|
+
export function credentialScopes() {
|
|
138
|
+
const usable = new Set(allOperations()
|
|
139
|
+
.filter((operation) => operation.principals.some((kind) => kind === "api-credential"))
|
|
140
|
+
.flatMap((operation) => [
|
|
141
|
+
...operation.scopes,
|
|
142
|
+
...(operation.conditionalScopes ?? []),
|
|
143
|
+
]));
|
|
144
|
+
return SCOPES.filter((scope) => usable.has(scope));
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Whether a role can perform an operation.
|
|
148
|
+
*
|
|
149
|
+
* An operation requiring no scopes needs no principal at all, so every role can
|
|
150
|
+
* reach it — and so can an unauthenticated caller. That is stated here rather
|
|
151
|
+
* than left implicit because "requires no scope" and "is public" are the same
|
|
152
|
+
* sentence, and a reader should not have to infer it.
|
|
153
|
+
*/
|
|
154
|
+
export function roleCanPerform(role, operation) {
|
|
155
|
+
const ceiling = ceilingForRole(role);
|
|
156
|
+
return operation.scopes.every((scope) => ceiling.has(scope));
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The policy matrix, derived from the operation registry.
|
|
160
|
+
*
|
|
161
|
+
* Generated rather than written down. A hand-maintained matrix is a second
|
|
162
|
+
* statement of the same policy, and the two drift — which is precisely what the
|
|
163
|
+
* artifact pipeline exists to prevent. Adding an operation to `OPERATIONS`
|
|
164
|
+
* changes this output, and `check:contracts` fails until the committed artifact
|
|
165
|
+
* matches.
|
|
166
|
+
*/
|
|
167
|
+
export function policyMatrix() {
|
|
168
|
+
return [...allOperations()]
|
|
169
|
+
.sort((left, right) => left.id.localeCompare(right.id))
|
|
170
|
+
.map((operation) => ({
|
|
171
|
+
mutates: operation.mutates,
|
|
172
|
+
operation: operation.id,
|
|
173
|
+
...(operation.conditionalScopes === undefined
|
|
174
|
+
? {}
|
|
175
|
+
: { conditionalScopes: [...operation.conditionalScopes] }),
|
|
176
|
+
principals: [...operation.principals],
|
|
177
|
+
roles: MEMBERSHIP_ROLES.filter((role) => roleCanPerform(role, operation)),
|
|
178
|
+
scopes: [...operation.scopes],
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Refusals.
|
|
4
|
+
*
|
|
5
|
+
* Every non-2xx response is an RFC 9457 problem document. It carries a stable
|
|
6
|
+
* machine-readable `code`, a human `title`, and — where the caller can act on
|
|
7
|
+
* it — a `retry` hint and per-field `errors`. The legacy scheduler returned
|
|
8
|
+
* `{ "error": "invalid_cron" }` with no category, no correlation, and no way
|
|
9
|
+
* for a client to tell "retry this" from "never retry this"; every adapter and
|
|
10
|
+
* SDK invented its own interpretation.
|
|
11
|
+
*
|
|
12
|
+
* `code` is the contract. `title` and `detail` are prose and may change without
|
|
13
|
+
* being a breaking change; clients must never branch on them.
|
|
14
|
+
*/
|
|
15
|
+
export declare const PROBLEM_CATEGORIES: readonly ["authentication", "authorization", "conflict", "invalid-request", "not-found", "rate-limited", "unavailable"];
|
|
16
|
+
export declare const problemCategorySchema: z.ZodEnum<{
|
|
17
|
+
authentication: "authentication";
|
|
18
|
+
authorization: "authorization";
|
|
19
|
+
conflict: "conflict";
|
|
20
|
+
"invalid-request": "invalid-request";
|
|
21
|
+
"not-found": "not-found";
|
|
22
|
+
"rate-limited": "rate-limited";
|
|
23
|
+
unavailable: "unavailable";
|
|
24
|
+
}>;
|
|
25
|
+
export type ProblemCategory = z.infer<typeof problemCategorySchema>;
|
|
26
|
+
/**
|
|
27
|
+
* The stable refusal catalog. Adding a code is additive; removing one, or
|
|
28
|
+
* changing the category or status of an existing one, is breaking.
|
|
29
|
+
*/
|
|
30
|
+
export declare const PROBLEM_CODES: {
|
|
31
|
+
readonly authentication_required: {
|
|
32
|
+
readonly category: "authentication";
|
|
33
|
+
readonly status: 401;
|
|
34
|
+
};
|
|
35
|
+
readonly credential_expired: {
|
|
36
|
+
readonly category: "authentication";
|
|
37
|
+
readonly status: 401;
|
|
38
|
+
};
|
|
39
|
+
readonly credential_revoked: {
|
|
40
|
+
readonly category: "authentication";
|
|
41
|
+
readonly status: 401;
|
|
42
|
+
};
|
|
43
|
+
readonly endpoint_unverified: {
|
|
44
|
+
readonly category: "invalid-request";
|
|
45
|
+
readonly status: 422;
|
|
46
|
+
};
|
|
47
|
+
readonly idempotency_key_reused: {
|
|
48
|
+
readonly category: "conflict";
|
|
49
|
+
readonly status: 409;
|
|
50
|
+
};
|
|
51
|
+
readonly invalid_request: {
|
|
52
|
+
readonly category: "invalid-request";
|
|
53
|
+
readonly status: 400;
|
|
54
|
+
};
|
|
55
|
+
readonly not_found: {
|
|
56
|
+
readonly category: "not-found";
|
|
57
|
+
readonly status: 404;
|
|
58
|
+
};
|
|
59
|
+
readonly payload_too_large: {
|
|
60
|
+
readonly category: "invalid-request";
|
|
61
|
+
readonly status: 413;
|
|
62
|
+
};
|
|
63
|
+
readonly plan_limit_reached: {
|
|
64
|
+
readonly category: "conflict";
|
|
65
|
+
readonly status: 409;
|
|
66
|
+
};
|
|
67
|
+
readonly rate_limited: {
|
|
68
|
+
readonly category: "rate-limited";
|
|
69
|
+
readonly status: 429;
|
|
70
|
+
};
|
|
71
|
+
readonly scope_missing: {
|
|
72
|
+
readonly category: "authorization";
|
|
73
|
+
readonly status: 403;
|
|
74
|
+
};
|
|
75
|
+
readonly service_unavailable: {
|
|
76
|
+
readonly category: "unavailable";
|
|
77
|
+
readonly status: 503;
|
|
78
|
+
};
|
|
79
|
+
readonly state_conflict: {
|
|
80
|
+
readonly category: "conflict";
|
|
81
|
+
readonly status: 409;
|
|
82
|
+
};
|
|
83
|
+
readonly test_to_live_escalation: {
|
|
84
|
+
readonly category: "authorization";
|
|
85
|
+
readonly status: 403;
|
|
86
|
+
};
|
|
87
|
+
readonly version_conflict: {
|
|
88
|
+
readonly category: "conflict";
|
|
89
|
+
readonly status: 409;
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
export type ProblemCode = keyof typeof PROBLEM_CODES;
|
|
93
|
+
export declare const problemCodeSchema: z.ZodEnum<{
|
|
94
|
+
authentication_required: "authentication_required";
|
|
95
|
+
credential_expired: "credential_expired";
|
|
96
|
+
credential_revoked: "credential_revoked";
|
|
97
|
+
endpoint_unverified: "endpoint_unverified";
|
|
98
|
+
idempotency_key_reused: "idempotency_key_reused";
|
|
99
|
+
invalid_request: "invalid_request";
|
|
100
|
+
not_found: "not_found";
|
|
101
|
+
payload_too_large: "payload_too_large";
|
|
102
|
+
plan_limit_reached: "plan_limit_reached";
|
|
103
|
+
rate_limited: "rate_limited";
|
|
104
|
+
scope_missing: "scope_missing";
|
|
105
|
+
service_unavailable: "service_unavailable";
|
|
106
|
+
state_conflict: "state_conflict";
|
|
107
|
+
test_to_live_escalation: "test_to_live_escalation";
|
|
108
|
+
version_conflict: "version_conflict";
|
|
109
|
+
}>;
|
|
110
|
+
/** Whether repeating the identical request could succeed later. */
|
|
111
|
+
export declare function isRetryable(code: ProblemCode): boolean;
|
|
112
|
+
export declare const fieldErrorSchema: z.ZodObject<{
|
|
113
|
+
pointer: z.ZodString;
|
|
114
|
+
reason: z.ZodString;
|
|
115
|
+
}, z.core.$strip>;
|
|
116
|
+
export type FieldError = z.infer<typeof fieldErrorSchema>;
|
|
117
|
+
export declare const problemSchema: z.ZodObject<{
|
|
118
|
+
code: z.ZodEnum<{
|
|
119
|
+
authentication_required: "authentication_required";
|
|
120
|
+
credential_expired: "credential_expired";
|
|
121
|
+
credential_revoked: "credential_revoked";
|
|
122
|
+
endpoint_unverified: "endpoint_unverified";
|
|
123
|
+
idempotency_key_reused: "idempotency_key_reused";
|
|
124
|
+
invalid_request: "invalid_request";
|
|
125
|
+
not_found: "not_found";
|
|
126
|
+
payload_too_large: "payload_too_large";
|
|
127
|
+
plan_limit_reached: "plan_limit_reached";
|
|
128
|
+
rate_limited: "rate_limited";
|
|
129
|
+
scope_missing: "scope_missing";
|
|
130
|
+
service_unavailable: "service_unavailable";
|
|
131
|
+
state_conflict: "state_conflict";
|
|
132
|
+
test_to_live_escalation: "test_to_live_escalation";
|
|
133
|
+
version_conflict: "version_conflict";
|
|
134
|
+
}>;
|
|
135
|
+
detail: z.ZodOptional<z.ZodString>;
|
|
136
|
+
errors: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
137
|
+
pointer: z.ZodString;
|
|
138
|
+
reason: z.ZodString;
|
|
139
|
+
}, z.core.$strip>>>;
|
|
140
|
+
retryAfterSeconds: z.ZodOptional<z.ZodNumber>;
|
|
141
|
+
status: z.ZodNumber;
|
|
142
|
+
title: z.ZodString;
|
|
143
|
+
traceId: z.ZodOptional<z.ZodString>;
|
|
144
|
+
type: z.ZodLiteral<"about:blank">;
|
|
145
|
+
}, z.core.$strip>;
|
|
146
|
+
export type Problem = z.infer<typeof problemSchema>;
|
|
147
|
+
export interface ProblemOptions {
|
|
148
|
+
readonly detail?: string;
|
|
149
|
+
readonly errors?: readonly FieldError[];
|
|
150
|
+
readonly retryAfterSeconds?: number;
|
|
151
|
+
readonly title: string;
|
|
152
|
+
readonly traceId?: string;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Builds a problem whose status and category cannot contradict its code, and
|
|
156
|
+
* which cannot carry a retry hint on a code that is not retryable.
|
|
157
|
+
*/
|
|
158
|
+
export declare function problem(code: ProblemCode, options: ProblemOptions): Problem;
|