@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FlowXO LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @greatwait/contracts
|
|
2
|
+
|
|
3
|
+
The public contracts for [Greatwait](https://greatwait.dev): the schemas every
|
|
4
|
+
response is parsed with, the stable refusal catalog, the operation registry, and
|
|
5
|
+
the generated OpenAPI documents.
|
|
6
|
+
|
|
7
|
+
> **Invited alpha.** This package publishes on the `alpha` dist-tag and has no
|
|
8
|
+
> `latest`. Install it explicitly, and pin the exact version in anything you
|
|
9
|
+
> care about.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @greatwait/contracts@alpha
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Most people arrive here through [`@greatwait/api-client`](https://www.npmjs.com/package/@greatwait/api-client),
|
|
16
|
+
which depends on this package. Install it directly when you want the schemas
|
|
17
|
+
without the client — to validate a callback body, to generate code, or to hold
|
|
18
|
+
your own client against the published shapes.
|
|
19
|
+
|
|
20
|
+
## Parsing a response
|
|
21
|
+
|
|
22
|
+
```javascript
|
|
23
|
+
import { responseSchemaFor } from "@greatwait/contracts";
|
|
24
|
+
|
|
25
|
+
const schedule = responseSchemaFor("schedules.create").parse(body);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Schemas are [Zod](https://zod.dev) and are the same objects the service
|
|
29
|
+
validates with, so a shape is stated once rather than mirrored. The JSON Schema
|
|
30
|
+
form of each is shipped too, under `generated/v1/schemas/`, for consumers that
|
|
31
|
+
are not JavaScript.
|
|
32
|
+
|
|
33
|
+
## What ships
|
|
34
|
+
|
|
35
|
+
| Path | What it is |
|
|
36
|
+
| --------------------------------------- | ------------------------------------------------------------------------------------------- |
|
|
37
|
+
| `generated/v1/openapi.json` | The public API, as OpenAPI |
|
|
38
|
+
| `generated/v1/openapi.mock.json` | The contract mock's surface, labelled separately so it can never be read as the product API |
|
|
39
|
+
| `generated/v1/schemas/` | JSON Schema per operation, request and response |
|
|
40
|
+
| `generated/v1/snippets/` | curl, JavaScript and MCP examples per operation |
|
|
41
|
+
| `generated/v1/first-value-journey.json` | The canonical first-value sequence, as data |
|
|
42
|
+
| `generated/v1/policy-matrix.json` | Who may call what, derived from the registry |
|
|
43
|
+
| `generated/v1/manifest.json` | The index of everything above, with the contract version |
|
|
44
|
+
|
|
45
|
+
All of it is generated from one registry, so a change to an operation changes
|
|
46
|
+
every artifact at once. Nothing here is hand-maintained beside the code it
|
|
47
|
+
describes.
|
|
48
|
+
|
|
49
|
+
## The conventions the schemas encode
|
|
50
|
+
|
|
51
|
+
- **Identifiers** are opaque, prefixed and server-assigned. Compare them for
|
|
52
|
+
equality and nothing else.
|
|
53
|
+
- **Time** is RFC 3339, UTC, milliseconds, literal `Z`. Durations are whole
|
|
54
|
+
seconds. An IANA timezone appears only beside a `cron` recurrence.
|
|
55
|
+
- **Collections** are cursor-paged, never offset-paged, and every page carries
|
|
56
|
+
`freshness` so a client can tell an authoritative answer from a projected one.
|
|
57
|
+
- **Concurrency** is a monotonic `version` surfaced as a strong `ETag`; a
|
|
58
|
+
conditional write sends it back in `If-Match` and is refused
|
|
59
|
+
`version_conflict` rather than resolving by luck.
|
|
60
|
+
- **Refusals** are RFC 9457 problem documents with a stable `code`. Adding a
|
|
61
|
+
code is additive; removing one, or changing its category or status, is
|
|
62
|
+
breaking.
|
|
63
|
+
|
|
64
|
+
`CONTRACT_VERSION` is the dated version of all of this. It moves independently
|
|
65
|
+
of any deployment, because a service that ships twice a day must not imply the
|
|
66
|
+
contract changed twice a day.
|
|
67
|
+
|
|
68
|
+
## The first-value journey
|
|
69
|
+
|
|
70
|
+
`generated/v1/first-value-journey.json` is the one sequence that takes a
|
|
71
|
+
developer from a credential to an observed callback — the same steps the curl
|
|
72
|
+
quickstart, the JavaScript quickstart and the website walkthrough render. Read
|
|
73
|
+
it rather than restating the steps: each entry carries the operation, the
|
|
74
|
+
method, the path, the scopes it needs, the refusals it can produce and what to
|
|
75
|
+
do about each of them.
|
|
76
|
+
|
|
77
|
+
MIT licensed. Issues: <https://github.com/flowxo/greatwait/issues>.
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ProblemCode } from "./problems.js";
|
|
3
|
+
/**
|
|
4
|
+
* Who is asking, and what they may do.
|
|
5
|
+
*
|
|
6
|
+
* The authority tuple `(organization, project, environment)` comes from a
|
|
7
|
+
* verified principal — a Clerk session, an API credential, or an OAuth access
|
|
8
|
+
* token. It is never read from a request body, a query parameter, or a path
|
|
9
|
+
* segment. A client can no more select its own organization than it can select
|
|
10
|
+
* its own password.
|
|
11
|
+
*
|
|
12
|
+
* The legacy scheduler put `:owner` and `:partition` in the path and trusted
|
|
13
|
+
* them. Those segments are migration inputs for W6 and are never authorization
|
|
14
|
+
* here. See `docs/architecture/legacy-provenance.md`.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Environments are immutable and hard-separated. A test principal cannot name,
|
|
18
|
+
* infer, mutate, or fire a live resource, and no operation escalates from test
|
|
19
|
+
* to live.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Both kinds, in a fixed order.
|
|
23
|
+
*
|
|
24
|
+
* Exported so a caller that must handle every kind — provisioning a project,
|
|
25
|
+
* say — iterates the list rather than writing the two out and quietly missing a
|
|
26
|
+
* third if one is ever added.
|
|
27
|
+
*/
|
|
28
|
+
export declare const ENVIRONMENT_KINDS_IN_ORDER: readonly ["test", "live"];
|
|
29
|
+
export declare const environmentKindSchema: z.ZodEnum<{
|
|
30
|
+
test: "test";
|
|
31
|
+
live: "live";
|
|
32
|
+
}>;
|
|
33
|
+
export type EnvironmentKind = z.infer<typeof environmentKindSchema>;
|
|
34
|
+
export declare const authoritySchema: z.ZodObject<{
|
|
35
|
+
environment: z.ZodString;
|
|
36
|
+
environmentKind: z.ZodEnum<{
|
|
37
|
+
test: "test";
|
|
38
|
+
live: "live";
|
|
39
|
+
}>;
|
|
40
|
+
organization: z.ZodString;
|
|
41
|
+
project: z.ZodString;
|
|
42
|
+
}, z.core.$strip>;
|
|
43
|
+
export type Authority = z.infer<typeof authoritySchema>;
|
|
44
|
+
/**
|
|
45
|
+
* The initial scope vocabulary. Scopes are `resource:action`, lower case, and
|
|
46
|
+
* additive — a new scope is a compatible change, removing or narrowing one is
|
|
47
|
+
* not.
|
|
48
|
+
*
|
|
49
|
+
* Granted scopes are always the intersection of what was requested, what the
|
|
50
|
+
* client is permitted, and what the granting member's current role allows.
|
|
51
|
+
* Nothing here can exceed a human's own access.
|
|
52
|
+
*/
|
|
53
|
+
export declare const SCOPES: readonly ["credentials:administer", "credentials:read", "credentials:write", "endpoints:read", "endpoints:write", "forecast:read", "grants:read", "grants:write", "occurrences:read", "occurrences:redeliver", "projects:read", "projects:write", "schedules:read", "schedules:write", "usage:read"];
|
|
54
|
+
export declare const scopeSchema: z.ZodEnum<{
|
|
55
|
+
"credentials:administer": "credentials:administer";
|
|
56
|
+
"credentials:read": "credentials:read";
|
|
57
|
+
"credentials:write": "credentials:write";
|
|
58
|
+
"endpoints:read": "endpoints:read";
|
|
59
|
+
"endpoints:write": "endpoints:write";
|
|
60
|
+
"forecast:read": "forecast:read";
|
|
61
|
+
"grants:read": "grants:read";
|
|
62
|
+
"grants:write": "grants:write";
|
|
63
|
+
"occurrences:read": "occurrences:read";
|
|
64
|
+
"occurrences:redeliver": "occurrences:redeliver";
|
|
65
|
+
"projects:read": "projects:read";
|
|
66
|
+
"projects:write": "projects:write";
|
|
67
|
+
"schedules:read": "schedules:read";
|
|
68
|
+
"schedules:write": "schedules:write";
|
|
69
|
+
"usage:read": "usage:read";
|
|
70
|
+
}>;
|
|
71
|
+
export type Scope = z.infer<typeof scopeSchema>;
|
|
72
|
+
/**
|
|
73
|
+
* Expands granted scopes with the reads their writes imply, so a caller that
|
|
74
|
+
* holds `schedules:write` need not also be granted `schedules:read`.
|
|
75
|
+
*/
|
|
76
|
+
export declare function effectiveScopes(granted: readonly Scope[]): Set<Scope>;
|
|
77
|
+
/**
|
|
78
|
+
* The three ways a principal can be established.
|
|
79
|
+
*
|
|
80
|
+
* Hoisted out of `principalSchema` so the operation registry can say which of
|
|
81
|
+
* them an operation accepts. Before that, "who may call this" was carried
|
|
82
|
+
* nowhere: the published document derived its security requirement from
|
|
83
|
+
* `scopes` alone, so `credentials.create` advertised API-credential
|
|
84
|
+
* authentication for a route that refuses an API credential outright.
|
|
85
|
+
*
|
|
86
|
+
* - `api-credential` — a scoped key. Its authority tuple is fixed at issue.
|
|
87
|
+
* - `oauth` — a delegated grant, capped by the granting member's role. Its
|
|
88
|
+
* authority tuple is fixed at consent.
|
|
89
|
+
* - `session` — a signed-in human, in a browser. **This is the only kind whose
|
|
90
|
+
* authority tuple is not fixed in advance**: a member belongs to an
|
|
91
|
+
* organization and may act in any project inside it, so the request names the
|
|
92
|
+
* project and environment and the service resolves the tuple against that
|
|
93
|
+
* member's own organization. Naming is not widening — `resolveAuthority`
|
|
94
|
+
* refuses a project the organization does not own, with the same answer it
|
|
95
|
+
* gives for one that does not exist.
|
|
96
|
+
*/
|
|
97
|
+
export declare const PRINCIPAL_KINDS: readonly ["api-credential", "oauth", "session"];
|
|
98
|
+
export declare const principalKindSchema: z.ZodEnum<{
|
|
99
|
+
"api-credential": "api-credential";
|
|
100
|
+
oauth: "oauth";
|
|
101
|
+
session: "session";
|
|
102
|
+
}>;
|
|
103
|
+
export type PrincipalKind = z.infer<typeof principalKindSchema>;
|
|
104
|
+
export declare const principalSchema: z.ZodObject<{
|
|
105
|
+
authority: z.ZodObject<{
|
|
106
|
+
environment: z.ZodString;
|
|
107
|
+
environmentKind: z.ZodEnum<{
|
|
108
|
+
test: "test";
|
|
109
|
+
live: "live";
|
|
110
|
+
}>;
|
|
111
|
+
organization: z.ZodString;
|
|
112
|
+
project: z.ZodString;
|
|
113
|
+
}, z.core.$strip>;
|
|
114
|
+
scopes: z.ZodArray<z.ZodEnum<{
|
|
115
|
+
"credentials:administer": "credentials:administer";
|
|
116
|
+
"credentials:read": "credentials:read";
|
|
117
|
+
"credentials:write": "credentials:write";
|
|
118
|
+
"endpoints:read": "endpoints:read";
|
|
119
|
+
"endpoints:write": "endpoints:write";
|
|
120
|
+
"forecast:read": "forecast:read";
|
|
121
|
+
"grants:read": "grants:read";
|
|
122
|
+
"grants:write": "grants:write";
|
|
123
|
+
"occurrences:read": "occurrences:read";
|
|
124
|
+
"occurrences:redeliver": "occurrences:redeliver";
|
|
125
|
+
"projects:read": "projects:read";
|
|
126
|
+
"projects:write": "projects:write";
|
|
127
|
+
"schedules:read": "schedules:read";
|
|
128
|
+
"schedules:write": "schedules:write";
|
|
129
|
+
"usage:read": "usage:read";
|
|
130
|
+
}>>;
|
|
131
|
+
via: z.ZodEnum<{
|
|
132
|
+
"api-credential": "api-credential";
|
|
133
|
+
oauth: "oauth";
|
|
134
|
+
session: "session";
|
|
135
|
+
}>;
|
|
136
|
+
}, z.core.$strip>;
|
|
137
|
+
export type Principal = z.infer<typeof principalSchema>;
|
|
138
|
+
export interface AccessRequest {
|
|
139
|
+
readonly principal: Principal;
|
|
140
|
+
/** The authority the resource actually belongs to. */
|
|
141
|
+
readonly resource: Authority;
|
|
142
|
+
readonly scope: Scope;
|
|
143
|
+
}
|
|
144
|
+
export type AccessDecision = {
|
|
145
|
+
readonly allowed: true;
|
|
146
|
+
} | {
|
|
147
|
+
readonly allowed: false;
|
|
148
|
+
readonly reason: AccessRefusal;
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* Why access was refused. `not-found` exists so a cross-tenant read is
|
|
152
|
+
* indistinguishable from a genuine miss: telling a caller that a resource
|
|
153
|
+
* exists but belongs to someone else is itself a disclosure.
|
|
154
|
+
*/
|
|
155
|
+
export type AccessRefusal = "not-found" | "scope-missing";
|
|
156
|
+
/**
|
|
157
|
+
* Whether a principal may act on an already-resolved resource.
|
|
158
|
+
*
|
|
159
|
+
* Order matters. Any mismatch in the authority tuple — organization, project,
|
|
160
|
+
* or environment — is `not-found`, and that is deliberate: distinguishing
|
|
161
|
+
* "exists but is not yours" from "does not exist" is an existence oracle. A
|
|
162
|
+
* test-scoped credential holding a live identifier must not be able to learn
|
|
163
|
+
* that the identifier is real, and environment is part of the authority tuple
|
|
164
|
+
* exactly so it gets the same treatment as organization.
|
|
165
|
+
*
|
|
166
|
+
* Escalation is refused before this point, on *intent* rather than on a
|
|
167
|
+
* resolved resource — see `decideEnvironmentIntent`. Tenancy is checked before
|
|
168
|
+
* scope so a refusal can never be used to probe another tenant.
|
|
169
|
+
*/
|
|
170
|
+
export declare function decideAccess(request: AccessRequest): AccessDecision;
|
|
171
|
+
export type EnvironmentIntentDecision = {
|
|
172
|
+
readonly allowed: true;
|
|
173
|
+
} | {
|
|
174
|
+
readonly allowed: false;
|
|
175
|
+
readonly reason: "test-to-live-escalation";
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Whether a principal may act in an environment it has *asked* for — creating
|
|
179
|
+
* a live schedule, say.
|
|
180
|
+
*
|
|
181
|
+
* This is the one place escalation is named, because here the caller stated
|
|
182
|
+
* the target rather than being told a resource exists. Keeping it separate
|
|
183
|
+
* from `decideAccess` means a distinct, actionable refusal without leaking
|
|
184
|
+
* whether any particular resource is real.
|
|
185
|
+
*/
|
|
186
|
+
export declare function decideEnvironmentIntent(principal: Principal, requested: EnvironmentKind): EnvironmentIntentDecision;
|
|
187
|
+
/**
|
|
188
|
+
* The wire refusal for every possible decision, so no adapter invents one.
|
|
189
|
+
*
|
|
190
|
+
* Typed against `ProblemCode` rather than a hand-written union, so renaming a
|
|
191
|
+
* code in the catalog fails here instead of silently returning one that is no
|
|
192
|
+
* longer catalogued.
|
|
193
|
+
*/
|
|
194
|
+
export declare function problemCodeForRefusal(reason: AccessRefusal | "principal-kind" | "test-to-live-escalation"): ProblemCode;
|
|
195
|
+
/**
|
|
196
|
+
* Where a session names the project and environment it is acting in.
|
|
197
|
+
*
|
|
198
|
+
* **Headers rather than query parameters, and that is an authentication
|
|
199
|
+
* decision rather than a stylistic one.** These two values are part of
|
|
200
|
+
* establishing *who is asking* — together with the Clerk session they produce
|
|
201
|
+
* the authority tuple that a key or a token carries intrinsically. Declaring
|
|
202
|
+
* them as request parameters would put them in the published parameter list of
|
|
203
|
+
* every operation a signed-in member can reach, which means the generated MCP
|
|
204
|
+
* tools would gain a `project` argument. A count stood here — "fourteen
|
|
205
|
+
* operations" — and was wrong by one before anybody read it twice; the number is
|
|
206
|
+
* derivable from the registry and nothing checks a number written in prose.
|
|
207
|
+
* `packages/mcp-tools/src/invoke.ts` already refuses that
|
|
208
|
+
* argument by name, on the grounds that an agent which sent it "believes it
|
|
209
|
+
* asked for another project", and offering it would read as consent.
|
|
210
|
+
*
|
|
211
|
+
* A cache keyed on the URL alone would serve one project's page to another, so
|
|
212
|
+
* every response that varies on these must say so in `Vary`.
|
|
213
|
+
*/
|
|
214
|
+
export declare const SESSION_AUTHORITY_HEADERS: {
|
|
215
|
+
readonly environment: "Greatwait-Environment";
|
|
216
|
+
readonly project: "Greatwait-Project";
|
|
217
|
+
};
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { environmentIdSchema, organizationIdSchema, projectIdSchema, } from "./identifiers.js";
|
|
3
|
+
/**
|
|
4
|
+
* Who is asking, and what they may do.
|
|
5
|
+
*
|
|
6
|
+
* The authority tuple `(organization, project, environment)` comes from a
|
|
7
|
+
* verified principal — a Clerk session, an API credential, or an OAuth access
|
|
8
|
+
* token. It is never read from a request body, a query parameter, or a path
|
|
9
|
+
* segment. A client can no more select its own organization than it can select
|
|
10
|
+
* its own password.
|
|
11
|
+
*
|
|
12
|
+
* The legacy scheduler put `:owner` and `:partition` in the path and trusted
|
|
13
|
+
* them. Those segments are migration inputs for W6 and are never authorization
|
|
14
|
+
* here. See `docs/architecture/legacy-provenance.md`.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Environments are immutable and hard-separated. A test principal cannot name,
|
|
18
|
+
* infer, mutate, or fire a live resource, and no operation escalates from test
|
|
19
|
+
* to live.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Both kinds, in a fixed order.
|
|
23
|
+
*
|
|
24
|
+
* Exported so a caller that must handle every kind — provisioning a project,
|
|
25
|
+
* say — iterates the list rather than writing the two out and quietly missing a
|
|
26
|
+
* third if one is ever added.
|
|
27
|
+
*/
|
|
28
|
+
export const ENVIRONMENT_KINDS_IN_ORDER = ["test", "live"];
|
|
29
|
+
export const environmentKindSchema = z.enum(ENVIRONMENT_KINDS_IN_ORDER);
|
|
30
|
+
export const authoritySchema = z.object({
|
|
31
|
+
environment: environmentIdSchema,
|
|
32
|
+
environmentKind: environmentKindSchema,
|
|
33
|
+
organization: organizationIdSchema,
|
|
34
|
+
project: projectIdSchema,
|
|
35
|
+
});
|
|
36
|
+
/**
|
|
37
|
+
* The initial scope vocabulary. Scopes are `resource:action`, lower case, and
|
|
38
|
+
* additive — a new scope is a compatible change, removing or narrowing one is
|
|
39
|
+
* not.
|
|
40
|
+
*
|
|
41
|
+
* Granted scopes are always the intersection of what was requested, what the
|
|
42
|
+
* client is permitted, and what the granting member's current role allows.
|
|
43
|
+
* Nothing here can exceed a human's own access.
|
|
44
|
+
*/
|
|
45
|
+
export const SCOPES = [
|
|
46
|
+
/**
|
|
47
|
+
* Act on a credential somebody else issued.
|
|
48
|
+
*
|
|
49
|
+
* **The one scope `member` does not hold, and the reason the role ceiling
|
|
50
|
+
* still distinguishes anything.** Every other scope is ordinary developer
|
|
51
|
+
* work; this one is the difference between managing your own access and
|
|
52
|
+
* managing everybody's. It gates rotation as well as revocation, because a
|
|
53
|
+
* rotation with a zero overlap retires the current key immediately —
|
|
54
|
+
* `credentialRotationRequestSchema` says so on the field — so gating one and
|
|
55
|
+
* leaving the other open would be a restriction with a door beside it.
|
|
56
|
+
*
|
|
57
|
+
* `credentials:write` still covers acting on your *own*, which is why this is
|
|
58
|
+
* a third scope rather than a narrowing of that one: an operation declares
|
|
59
|
+
* the scopes it always needs, and "whose credential is this" is a property of
|
|
60
|
+
* the row rather than of the route.
|
|
61
|
+
*/
|
|
62
|
+
"credentials:administer",
|
|
63
|
+
"credentials:read",
|
|
64
|
+
"credentials:write",
|
|
65
|
+
"endpoints:read",
|
|
66
|
+
"endpoints:write",
|
|
67
|
+
"forecast:read",
|
|
68
|
+
/**
|
|
69
|
+
* See which applications a member has connected, and disconnect one.
|
|
70
|
+
*
|
|
71
|
+
* **Two scopes no delegated grant can ever carry, and that is the point.**
|
|
72
|
+
* `grantableScopes` derives what an OAuth client may ask for from the
|
|
73
|
+
* operations that accept an OAuth principal, and both grant operations accept
|
|
74
|
+
* only a signed-in member — so an application cannot request them. That is
|
|
75
|
+
* `oauth.ts`'s rule made structural: an application holding a grant must not
|
|
76
|
+
* be able to enumerate its rivals, still less disconnect them.
|
|
77
|
+
*
|
|
78
|
+
* They exist because W4-08 moved the grant surface onto `/v1`, where every
|
|
79
|
+
* operation declares what it needs. A published route that enforces nothing
|
|
80
|
+
* is a surface whose authorization story nobody can read.
|
|
81
|
+
*/
|
|
82
|
+
"grants:read",
|
|
83
|
+
"grants:write",
|
|
84
|
+
"occurrences:read",
|
|
85
|
+
"occurrences:redeliver",
|
|
86
|
+
"projects:read",
|
|
87
|
+
/**
|
|
88
|
+
* Create a project, with its `test` and `live` environments.
|
|
89
|
+
*
|
|
90
|
+
* **Added by W4-08, and it is the scope that makes the product usable by
|
|
91
|
+
* somebody nobody has set up by hand.** Until then `provisionProject` was
|
|
92
|
+
* reachable from no deployed surface at all: an invited member with a Clerk
|
|
93
|
+
* organization and no project saw "No projects" and had no move, and the only
|
|
94
|
+
* way to give them one was SQL against the control plane — which is the
|
|
95
|
+
* hidden operator path that story's criterion 3 forbids.
|
|
96
|
+
*
|
|
97
|
+
* `member` holds it, like every scope but `credentials:administer`. A project
|
|
98
|
+
* is a container: creating one grants its creator nothing they did not
|
|
99
|
+
* already have, and the ceiling still bounds everything inside it. Making it
|
|
100
|
+
* admin-only would also have broken the sentence `policy.ts` states about
|
|
101
|
+
* what `admin` means — acting on somebody else's access, and nothing else.
|
|
102
|
+
*/
|
|
103
|
+
"projects:write",
|
|
104
|
+
"schedules:read",
|
|
105
|
+
"schedules:write",
|
|
106
|
+
/**
|
|
107
|
+
* See what this project was charged for.
|
|
108
|
+
*
|
|
109
|
+
* A scope of its own rather than a corner of `schedules:read`, because the
|
|
110
|
+
* two answer to different people: a service account that lists schedules to
|
|
111
|
+
* keep them in step has no business reading a bill, and somebody
|
|
112
|
+
* reconciling one has no need to enumerate the book. Read-only by
|
|
113
|
+
* construction — there is no `usage:write`, because a usage fact is written
|
|
114
|
+
* by a settlement and by nothing a customer can ask for.
|
|
115
|
+
*/
|
|
116
|
+
"usage:read",
|
|
117
|
+
];
|
|
118
|
+
export const scopeSchema = z.enum(SCOPES);
|
|
119
|
+
/** Read scopes implied by the matching write scope. */
|
|
120
|
+
const IMPLIED_BY_WRITE = {
|
|
121
|
+
"credentials:write": "credentials:read",
|
|
122
|
+
"endpoints:write": "endpoints:read",
|
|
123
|
+
"grants:write": "grants:read",
|
|
124
|
+
"projects:write": "projects:read",
|
|
125
|
+
"schedules:write": "schedules:read",
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Expands granted scopes with the reads their writes imply, so a caller that
|
|
129
|
+
* holds `schedules:write` need not also be granted `schedules:read`.
|
|
130
|
+
*/
|
|
131
|
+
export function effectiveScopes(granted) {
|
|
132
|
+
const effective = new Set(granted);
|
|
133
|
+
for (const scope of granted) {
|
|
134
|
+
const implied = IMPLIED_BY_WRITE[scope];
|
|
135
|
+
if (implied) {
|
|
136
|
+
effective.add(implied);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return effective;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The three ways a principal can be established.
|
|
143
|
+
*
|
|
144
|
+
* Hoisted out of `principalSchema` so the operation registry can say which of
|
|
145
|
+
* them an operation accepts. Before that, "who may call this" was carried
|
|
146
|
+
* nowhere: the published document derived its security requirement from
|
|
147
|
+
* `scopes` alone, so `credentials.create` advertised API-credential
|
|
148
|
+
* authentication for a route that refuses an API credential outright.
|
|
149
|
+
*
|
|
150
|
+
* - `api-credential` — a scoped key. Its authority tuple is fixed at issue.
|
|
151
|
+
* - `oauth` — a delegated grant, capped by the granting member's role. Its
|
|
152
|
+
* authority tuple is fixed at consent.
|
|
153
|
+
* - `session` — a signed-in human, in a browser. **This is the only kind whose
|
|
154
|
+
* authority tuple is not fixed in advance**: a member belongs to an
|
|
155
|
+
* organization and may act in any project inside it, so the request names the
|
|
156
|
+
* project and environment and the service resolves the tuple against that
|
|
157
|
+
* member's own organization. Naming is not widening — `resolveAuthority`
|
|
158
|
+
* refuses a project the organization does not own, with the same answer it
|
|
159
|
+
* gives for one that does not exist.
|
|
160
|
+
*/
|
|
161
|
+
export const PRINCIPAL_KINDS = ["api-credential", "oauth", "session"];
|
|
162
|
+
export const principalKindSchema = z.enum(PRINCIPAL_KINDS);
|
|
163
|
+
export const principalSchema = z.object({
|
|
164
|
+
authority: authoritySchema,
|
|
165
|
+
scopes: z.array(scopeSchema).min(1),
|
|
166
|
+
/** How the principal was established, for audit and refusal messages. */
|
|
167
|
+
via: principalKindSchema,
|
|
168
|
+
});
|
|
169
|
+
/**
|
|
170
|
+
* Whether a principal may act on an already-resolved resource.
|
|
171
|
+
*
|
|
172
|
+
* Order matters. Any mismatch in the authority tuple — organization, project,
|
|
173
|
+
* or environment — is `not-found`, and that is deliberate: distinguishing
|
|
174
|
+
* "exists but is not yours" from "does not exist" is an existence oracle. A
|
|
175
|
+
* test-scoped credential holding a live identifier must not be able to learn
|
|
176
|
+
* that the identifier is real, and environment is part of the authority tuple
|
|
177
|
+
* exactly so it gets the same treatment as organization.
|
|
178
|
+
*
|
|
179
|
+
* Escalation is refused before this point, on *intent* rather than on a
|
|
180
|
+
* resolved resource — see `decideEnvironmentIntent`. Tenancy is checked before
|
|
181
|
+
* scope so a refusal can never be used to probe another tenant.
|
|
182
|
+
*/
|
|
183
|
+
export function decideAccess(request) {
|
|
184
|
+
const { principal, resource, scope } = request;
|
|
185
|
+
const held = principal.authority;
|
|
186
|
+
if (held.organization !== resource.organization ||
|
|
187
|
+
held.project !== resource.project ||
|
|
188
|
+
held.environment !== resource.environment ||
|
|
189
|
+
// Kind is a property of the environment id, so this is belt-and-braces —
|
|
190
|
+
// but the rule says "any mismatch in the authority tuple", and the whole
|
|
191
|
+
// tuple is cheap to compare.
|
|
192
|
+
held.environmentKind !== resource.environmentKind) {
|
|
193
|
+
return { allowed: false, reason: "not-found" };
|
|
194
|
+
}
|
|
195
|
+
if (!effectiveScopes(principal.scopes).has(scope)) {
|
|
196
|
+
return { allowed: false, reason: "scope-missing" };
|
|
197
|
+
}
|
|
198
|
+
return { allowed: true };
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Whether a principal may act in an environment it has *asked* for — creating
|
|
202
|
+
* a live schedule, say.
|
|
203
|
+
*
|
|
204
|
+
* This is the one place escalation is named, because here the caller stated
|
|
205
|
+
* the target rather than being told a resource exists. Keeping it separate
|
|
206
|
+
* from `decideAccess` means a distinct, actionable refusal without leaking
|
|
207
|
+
* whether any particular resource is real.
|
|
208
|
+
*/
|
|
209
|
+
export function decideEnvironmentIntent(principal, requested) {
|
|
210
|
+
return principal.authority.environmentKind === "test" && requested === "live"
|
|
211
|
+
? { allowed: false, reason: "test-to-live-escalation" }
|
|
212
|
+
: { allowed: true };
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The wire refusal for every possible decision, so no adapter invents one.
|
|
216
|
+
*
|
|
217
|
+
* Typed against `ProblemCode` rather than a hand-written union, so renaming a
|
|
218
|
+
* code in the catalog fails here instead of silently returning one that is no
|
|
219
|
+
* longer catalogued.
|
|
220
|
+
*/
|
|
221
|
+
export function problemCodeForRefusal(reason) {
|
|
222
|
+
switch (reason) {
|
|
223
|
+
case "not-found":
|
|
224
|
+
return "not_found";
|
|
225
|
+
// Not `scope_missing`, and the difference matters to whoever is holding the
|
|
226
|
+
// credential. A missing scope says "ask for more"; this says "that kind of
|
|
227
|
+
// credential is not accepted here at all" — an API key cannot acquire the
|
|
228
|
+
// scope that would let it list an organization's projects, because the
|
|
229
|
+
// refusal is about what it *is*. 401 is what sends a browser to sign in and
|
|
230
|
+
// stops a key holder retrying with a wider grant.
|
|
231
|
+
case "principal-kind":
|
|
232
|
+
return "authentication_required";
|
|
233
|
+
case "scope-missing":
|
|
234
|
+
return "scope_missing";
|
|
235
|
+
case "test-to-live-escalation":
|
|
236
|
+
return "test_to_live_escalation";
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Where a session names the project and environment it is acting in.
|
|
241
|
+
*
|
|
242
|
+
* **Headers rather than query parameters, and that is an authentication
|
|
243
|
+
* decision rather than a stylistic one.** These two values are part of
|
|
244
|
+
* establishing *who is asking* — together with the Clerk session they produce
|
|
245
|
+
* the authority tuple that a key or a token carries intrinsically. Declaring
|
|
246
|
+
* them as request parameters would put them in the published parameter list of
|
|
247
|
+
* every operation a signed-in member can reach, which means the generated MCP
|
|
248
|
+
* tools would gain a `project` argument. A count stood here — "fourteen
|
|
249
|
+
* operations" — and was wrong by one before anybody read it twice; the number is
|
|
250
|
+
* derivable from the registry and nothing checks a number written in prose.
|
|
251
|
+
* `packages/mcp-tools/src/invoke.ts` already refuses that
|
|
252
|
+
* argument by name, on the grounds that an agent which sent it "believes it
|
|
253
|
+
* asked for another project", and offering it would read as consent.
|
|
254
|
+
*
|
|
255
|
+
* A cache keyed on the URL alone would serve one project's page to another, so
|
|
256
|
+
* every response that varies on these must say so in `Vary`.
|
|
257
|
+
*/
|
|
258
|
+
export const SESSION_AUTHORITY_HEADERS = {
|
|
259
|
+
environment: "Greatwait-Environment",
|
|
260
|
+
project: "Greatwait-Project",
|
|
261
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a receiver has to prove it owns a destination before Greatwait
|
|
3
|
+
* delivers anything there.
|
|
4
|
+
*
|
|
5
|
+
* **Zod-free and dependency-free on purpose.** Both the API Worker and the
|
|
6
|
+
* Scheduler Worker enforce this, and `apps/scheduler/src/contract-runtime.ts`
|
|
7
|
+
* records what reaching the contract barrel costs a Durable Object cold start.
|
|
8
|
+
* This module is an enumeration and two pure functions, so the subpath export
|
|
9
|
+
* `@greatwait/contracts/callback-ownership` costs the scheduler nothing.
|
|
10
|
+
*
|
|
11
|
+
* **Why the rule is stated once rather than twice.** Two sites resolve a
|
|
12
|
+
* destination for sending — `apps/api` when a schedule is created, and
|
|
13
|
+
* `apps/scheduler` at the moment of delivery — and they live in different
|
|
14
|
+
* workspaces. If each wrote its own status predicate they would eventually
|
|
15
|
+
* disagree, and the shape of that disagreement is a schedule the product
|
|
16
|
+
* accepts and then silently never delivers. `sendableStatuses` is the single
|
|
17
|
+
* statement both build their query from.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* The answers this product understands, in the order a person would read them
|
|
21
|
+
* as increasing strictness.
|
|
22
|
+
*
|
|
23
|
+
* `not-required` does **not** mean verification is gone. A destination may
|
|
24
|
+
* still answer its ownership challenge, and doing so still records the instant
|
|
25
|
+
* it proved possession of the secret. It means nothing waits for that.
|
|
26
|
+
*/
|
|
27
|
+
export declare const CALLBACK_OWNERSHIP_POLICIES: readonly ["not-required", "required"];
|
|
28
|
+
export type CallbackOwnershipPolicy = (typeof CALLBACK_OWNERSHIP_POLICIES)[number];
|
|
29
|
+
/**
|
|
30
|
+
* What an unset variable means.
|
|
31
|
+
*
|
|
32
|
+
* Not required, decided on 2026-08-18 after the first invited customers walked
|
|
33
|
+
* the product: nobody writes an HMAC responder to try a scheduler, so requiring
|
|
34
|
+
* one before the first delivery cost the whole top of the funnel to buy a
|
|
35
|
+
* property that `evaluateCallbackUrl`, the delivery rate limits and the request
|
|
36
|
+
* signature already defend from other directions. See W4-09.
|
|
37
|
+
*/
|
|
38
|
+
export declare const DEFAULT_CALLBACK_OWNERSHIP_POLICY: CallbackOwnershipPolicy;
|
|
39
|
+
/**
|
|
40
|
+
* Read the policy a deployment declares, or refuse to run.
|
|
41
|
+
*
|
|
42
|
+
* **An unrecognised value throws rather than falling back.** The entire reason
|
|
43
|
+
* this is configuration is that turning the requirement on or off should be a
|
|
44
|
+
* visible decision; a typo, a rename, or a half-finished edit silently read as
|
|
45
|
+
* `not-required` would hand back exactly the off-switch the setting exists to
|
|
46
|
+
* make visible. Absent is a decision — it is the documented default — and
|
|
47
|
+
* `"requred"` is not.
|
|
48
|
+
*/
|
|
49
|
+
export declare function readCallbackOwnershipPolicy(value: string | undefined): CallbackOwnershipPolicy;
|
|
50
|
+
/**
|
|
51
|
+
* The destination statuses a send may resolve under a policy, as a query
|
|
52
|
+
* builds its `IN` list from.
|
|
53
|
+
*
|
|
54
|
+
* `disabled` is absent from both and always will be: disabling is terminal and
|
|
55
|
+
* is the one refusal this setting does not reach. A caller that widened this to
|
|
56
|
+
* include it would be delivering to a destination somebody turned off.
|
|
57
|
+
*/
|
|
58
|
+
export declare function sendableStatuses(policy: CallbackOwnershipPolicy): readonly string[];
|