@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.
Files changed (241) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/dist/authority.d.ts +217 -0
  4. package/dist/authority.js +261 -0
  5. package/dist/callback-ownership.d.ts +58 -0
  6. package/dist/callback-ownership.js +71 -0
  7. package/dist/callback-safety.d.ts +38 -0
  8. package/dist/callback-safety.js +403 -0
  9. package/dist/compatibility.d.ts +82 -0
  10. package/dist/compatibility.js +115 -0
  11. package/dist/delivery.d.ts +401 -0
  12. package/dist/delivery.js +311 -0
  13. package/dist/endpoints.d.ts +305 -0
  14. package/dist/endpoints.js +179 -0
  15. package/dist/events.d.ts +62 -0
  16. package/dist/events.js +38 -0
  17. package/dist/first-value-journey.d.ts +305 -0
  18. package/dist/first-value-journey.js +466 -0
  19. package/dist/forecast.d.ts +443 -0
  20. package/dist/forecast.js +729 -0
  21. package/dist/identifiers.d.ts +109 -0
  22. package/dist/identifiers.js +131 -0
  23. package/dist/identity.d.ts +196 -0
  24. package/dist/identity.js +182 -0
  25. package/dist/index.d.ts +27 -0
  26. package/dist/index.js +27 -0
  27. package/dist/mcp.d.ts +51 -0
  28. package/dist/mcp.js +56 -0
  29. package/dist/oauth.d.ts +796 -0
  30. package/dist/oauth.js +708 -0
  31. package/dist/operations.d.ts +3985 -0
  32. package/dist/operations.js +1837 -0
  33. package/dist/partition.d.ts +771 -0
  34. package/dist/partition.js +206 -0
  35. package/dist/policy.d.ts +93 -0
  36. package/dist/policy.js +180 -0
  37. package/dist/problems.d.ts +158 -0
  38. package/dist/problems.js +104 -0
  39. package/dist/protocol.d.ts +308 -0
  40. package/dist/protocol.js +515 -0
  41. package/dist/scheduling-records.d.ts +395 -0
  42. package/dist/scheduling-records.js +284 -0
  43. package/dist/scheduling.d.ts +1098 -0
  44. package/dist/scheduling.js +1116 -0
  45. package/dist/secrets.d.ts +383 -0
  46. package/dist/secrets.js +272 -0
  47. package/dist/signatures.d.ts +704 -0
  48. package/dist/signatures.js +867 -0
  49. package/dist/tenancy.d.ts +178 -0
  50. package/dist/tenancy.js +145 -0
  51. package/dist/test-sink.d.ts +986 -0
  52. package/dist/test-sink.js +755 -0
  53. package/dist/time.d.ts +45 -0
  54. package/dist/time.js +128 -0
  55. package/dist/usage.d.ts +121 -0
  56. package/dist/usage.js +200 -0
  57. package/dist/webhooks.d.ts +116 -0
  58. package/dist/webhooks.js +161 -0
  59. package/dist/workspace.d.ts +92 -0
  60. package/dist/workspace.js +59 -0
  61. package/generated/v1/callback-delivery-signature.json +467 -0
  62. package/generated/v1/callback-endpoint-verification.json +59 -0
  63. package/generated/v1/callback-receiver.json +241 -0
  64. package/generated/v1/examples/attempts.list.response.json +24 -0
  65. package/generated/v1/examples/attempts.read.response.json +20 -0
  66. package/generated/v1/examples/credentials.create.request.json +5 -0
  67. package/generated/v1/examples/credentials.create.response.json +10 -0
  68. package/generated/v1/examples/credentials.list.response.json +17 -0
  69. package/generated/v1/examples/credentials.revoke.response.json +11 -0
  70. package/generated/v1/examples/credentials.rotate.request.json +3 -0
  71. package/generated/v1/examples/credentials.rotate.response.json +22 -0
  72. package/generated/v1/examples/endpoints.create.request.json +3 -0
  73. package/generated/v1/examples/endpoints.create.response.json +16 -0
  74. package/generated/v1/examples/endpoints.disable.response.json +15 -0
  75. package/generated/v1/examples/endpoints.list.response.json +21 -0
  76. package/generated/v1/examples/endpoints.read.response.json +14 -0
  77. package/generated/v1/examples/endpoints.rotateSigningKey.request.json +3 -0
  78. package/generated/v1/examples/endpoints.rotateSigningKey.response.json +23 -0
  79. package/generated/v1/examples/endpoints.update.request.json +3 -0
  80. package/generated/v1/examples/endpoints.update.response.json +13 -0
  81. package/generated/v1/examples/endpoints.verify.response.json +15 -0
  82. package/generated/v1/examples/forecast.read.response.json +28 -0
  83. package/generated/v1/examples/forecast.windows.response.json +21 -0
  84. package/generated/v1/examples/grants.list.response.json +17 -0
  85. package/generated/v1/examples/grants.revoke.response.json +16 -0
  86. package/generated/v1/examples/meta.scaffold.response.json +5 -0
  87. package/generated/v1/examples/meta.version.response.json +5 -0
  88. package/generated/v1/examples/occurrences.list.response.json +17 -0
  89. package/generated/v1/examples/occurrences.read.response.json +15 -0
  90. package/generated/v1/examples/occurrences.redeliver.response.json +16 -0
  91. package/generated/v1/examples/projects.create.request.json +3 -0
  92. package/generated/v1/examples/projects.create.response.json +5 -0
  93. package/generated/v1/examples/projects.list.response.json +9 -0
  94. package/generated/v1/examples/schedules.archive.response.json +25 -0
  95. package/generated/v1/examples/schedules.cancel.response.json +24 -0
  96. package/generated/v1/examples/schedules.create.request.json +9 -0
  97. package/generated/v1/examples/schedules.create.response.json +25 -0
  98. package/generated/v1/examples/schedules.list.response.json +23 -0
  99. package/generated/v1/examples/schedules.pause.response.json +23 -0
  100. package/generated/v1/examples/schedules.read.response.json +25 -0
  101. package/generated/v1/examples/schedules.replace.request.json +8 -0
  102. package/generated/v1/examples/schedules.replace.response.json +25 -0
  103. package/generated/v1/examples/schedules.resume.response.json +23 -0
  104. package/generated/v1/examples/sinkdeliveries.list.response.json +19 -0
  105. package/generated/v1/examples/sinkdeliveries.read.response.json +16 -0
  106. package/generated/v1/examples/testsinks.configure.request.json +3 -0
  107. package/generated/v1/examples/testsinks.configure.response.json +13 -0
  108. package/generated/v1/examples/testsinks.create.request.json +8 -0
  109. package/generated/v1/examples/testsinks.create.response.json +13 -0
  110. package/generated/v1/examples/testsinks.delete.response.json +13 -0
  111. package/generated/v1/examples/testsinks.list.response.json +20 -0
  112. package/generated/v1/examples/testsinks.read.response.json +14 -0
  113. package/generated/v1/examples/usage.read.response.json +20 -0
  114. package/generated/v1/first-value-journey.json +436 -0
  115. package/generated/v1/manifest.json +1037 -0
  116. package/generated/v1/openapi.json +23665 -0
  117. package/generated/v1/policy-matrix.json +305 -0
  118. package/generated/v1/schemas/attempts.list.query.json +16 -0
  119. package/generated/v1/schemas/attempts.list.response.json +175 -0
  120. package/generated/v1/schemas/attempts.read.response.json +168 -0
  121. package/generated/v1/schemas/credentials.create.request.json +39 -0
  122. package/generated/v1/schemas/credentials.create.response.json +88 -0
  123. package/generated/v1/schemas/credentials.list.query.json +16 -0
  124. package/generated/v1/schemas/credentials.list.response.json +124 -0
  125. package/generated/v1/schemas/credentials.revoke.response.json +84 -0
  126. package/generated/v1/schemas/credentials.rotate.request.json +11 -0
  127. package/generated/v1/schemas/credentials.rotate.response.json +171 -0
  128. package/generated/v1/schemas/endpoints.create.request.json +12 -0
  129. package/generated/v1/schemas/endpoints.create.response.json +115 -0
  130. package/generated/v1/schemas/endpoints.disable.response-headers.json +11 -0
  131. package/generated/v1/schemas/endpoints.disable.response.json +100 -0
  132. package/generated/v1/schemas/endpoints.list.query.json +16 -0
  133. package/generated/v1/schemas/endpoints.list.response.json +146 -0
  134. package/generated/v1/schemas/endpoints.read.response-headers.json +11 -0
  135. package/generated/v1/schemas/endpoints.read.response.json +100 -0
  136. package/generated/v1/schemas/endpoints.rotateSigningKey.request-headers.json +10 -0
  137. package/generated/v1/schemas/endpoints.rotateSigningKey.request.json +11 -0
  138. package/generated/v1/schemas/endpoints.rotateSigningKey.response-headers.json +11 -0
  139. package/generated/v1/schemas/endpoints.rotateSigningKey.response.json +118 -0
  140. package/generated/v1/schemas/endpoints.update.request-headers.json +10 -0
  141. package/generated/v1/schemas/endpoints.update.request.json +12 -0
  142. package/generated/v1/schemas/endpoints.update.response-headers.json +11 -0
  143. package/generated/v1/schemas/endpoints.update.response.json +100 -0
  144. package/generated/v1/schemas/endpoints.verify.request-headers.json +10 -0
  145. package/generated/v1/schemas/endpoints.verify.response-headers.json +11 -0
  146. package/generated/v1/schemas/endpoints.verify.response.json +100 -0
  147. package/generated/v1/schemas/forecast.read.query.json +26 -0
  148. package/generated/v1/schemas/forecast.read.response.json +100 -0
  149. package/generated/v1/schemas/forecast.windows.query.json +26 -0
  150. package/generated/v1/schemas/forecast.windows.response.json +94 -0
  151. package/generated/v1/schemas/grants.list.response.json +98 -0
  152. package/generated/v1/schemas/grants.revoke.response.json +95 -0
  153. package/generated/v1/schemas/meta.scaffold.response.json +36 -0
  154. package/generated/v1/schemas/meta.version.response.json +19 -0
  155. package/generated/v1/schemas/occurrences.list.query.json +16 -0
  156. package/generated/v1/schemas/occurrences.list.response.json +83 -0
  157. package/generated/v1/schemas/occurrences.read.response.json +76 -0
  158. package/generated/v1/schemas/occurrences.redeliver.request-headers.json +13 -0
  159. package/generated/v1/schemas/occurrences.redeliver.response-headers.json +10 -0
  160. package/generated/v1/schemas/occurrences.redeliver.response.json +168 -0
  161. package/generated/v1/schemas/projects.create.request.json +12 -0
  162. package/generated/v1/schemas/projects.create.response.json +25 -0
  163. package/generated/v1/schemas/projects.list.response.json +35 -0
  164. package/generated/v1/schemas/schedules.archive.request-headers.json +10 -0
  165. package/generated/v1/schemas/schedules.archive.response-headers.json +11 -0
  166. package/generated/v1/schemas/schedules.archive.response.json +189 -0
  167. package/generated/v1/schemas/schedules.cancel.request-headers.json +10 -0
  168. package/generated/v1/schemas/schedules.cancel.response-headers.json +11 -0
  169. package/generated/v1/schemas/schedules.cancel.response.json +189 -0
  170. package/generated/v1/schemas/schedules.create.request-headers.json +12 -0
  171. package/generated/v1/schemas/schedules.create.request.json +99 -0
  172. package/generated/v1/schemas/schedules.create.response-headers.json +15 -0
  173. package/generated/v1/schemas/schedules.create.response.json +189 -0
  174. package/generated/v1/schemas/schedules.list.query.json +16 -0
  175. package/generated/v1/schemas/schedules.list.response.json +170 -0
  176. package/generated/v1/schemas/schedules.pause.request-headers.json +10 -0
  177. package/generated/v1/schemas/schedules.pause.response-headers.json +11 -0
  178. package/generated/v1/schemas/schedules.pause.response.json +189 -0
  179. package/generated/v1/schemas/schedules.read.response-headers.json +11 -0
  180. package/generated/v1/schemas/schedules.read.response.json +189 -0
  181. package/generated/v1/schemas/schedules.replace.request-headers.json +10 -0
  182. package/generated/v1/schemas/schedules.replace.request.json +99 -0
  183. package/generated/v1/schemas/schedules.replace.response-headers.json +11 -0
  184. package/generated/v1/schemas/schedules.replace.response.json +189 -0
  185. package/generated/v1/schemas/schedules.resume.request-headers.json +10 -0
  186. package/generated/v1/schemas/schedules.resume.response-headers.json +11 -0
  187. package/generated/v1/schemas/schedules.resume.response.json +189 -0
  188. package/generated/v1/schemas/sinkdeliveries.list.query.json +16 -0
  189. package/generated/v1/schemas/sinkdeliveries.list.response.json +126 -0
  190. package/generated/v1/schemas/sinkdeliveries.read.response.json +98 -0
  191. package/generated/v1/schemas/testsinks.configure.request.json +66 -0
  192. package/generated/v1/schemas/testsinks.configure.response.json +115 -0
  193. package/generated/v1/schemas/testsinks.create.request.json +76 -0
  194. package/generated/v1/schemas/testsinks.create.response.json +115 -0
  195. package/generated/v1/schemas/testsinks.delete.response.json +115 -0
  196. package/generated/v1/schemas/testsinks.list.query.json +16 -0
  197. package/generated/v1/schemas/testsinks.list.response.json +161 -0
  198. package/generated/v1/schemas/testsinks.read.response.json +115 -0
  199. package/generated/v1/schemas/usage.read.query.json +15 -0
  200. package/generated/v1/schemas/usage.read.response.json +71 -0
  201. package/generated/v1/snippets/attempts.list.json +5 -0
  202. package/generated/v1/snippets/attempts.read.json +5 -0
  203. package/generated/v1/snippets/credentials.create.json +5 -0
  204. package/generated/v1/snippets/credentials.list.json +5 -0
  205. package/generated/v1/snippets/credentials.revoke.json +5 -0
  206. package/generated/v1/snippets/credentials.rotate.json +5 -0
  207. package/generated/v1/snippets/endpoints.create.json +5 -0
  208. package/generated/v1/snippets/endpoints.disable.json +5 -0
  209. package/generated/v1/snippets/endpoints.list.json +5 -0
  210. package/generated/v1/snippets/endpoints.read.json +5 -0
  211. package/generated/v1/snippets/endpoints.rotateSigningKey.json +5 -0
  212. package/generated/v1/snippets/endpoints.update.json +5 -0
  213. package/generated/v1/snippets/endpoints.verify.json +5 -0
  214. package/generated/v1/snippets/forecast.read.json +5 -0
  215. package/generated/v1/snippets/forecast.windows.json +5 -0
  216. package/generated/v1/snippets/grants.list.json +5 -0
  217. package/generated/v1/snippets/grants.revoke.json +5 -0
  218. package/generated/v1/snippets/meta.scaffold.json +5 -0
  219. package/generated/v1/snippets/meta.version.json +5 -0
  220. package/generated/v1/snippets/occurrences.list.json +5 -0
  221. package/generated/v1/snippets/occurrences.read.json +5 -0
  222. package/generated/v1/snippets/occurrences.redeliver.json +5 -0
  223. package/generated/v1/snippets/projects.create.json +5 -0
  224. package/generated/v1/snippets/projects.list.json +5 -0
  225. package/generated/v1/snippets/schedules.archive.json +5 -0
  226. package/generated/v1/snippets/schedules.cancel.json +5 -0
  227. package/generated/v1/snippets/schedules.create.json +5 -0
  228. package/generated/v1/snippets/schedules.list.json +5 -0
  229. package/generated/v1/snippets/schedules.pause.json +5 -0
  230. package/generated/v1/snippets/schedules.read.json +5 -0
  231. package/generated/v1/snippets/schedules.replace.json +5 -0
  232. package/generated/v1/snippets/schedules.resume.json +5 -0
  233. package/generated/v1/snippets/sinkdeliveries.list.json +5 -0
  234. package/generated/v1/snippets/sinkdeliveries.read.json +5 -0
  235. package/generated/v1/snippets/testsinks.configure.json +5 -0
  236. package/generated/v1/snippets/testsinks.create.json +5 -0
  237. package/generated/v1/snippets/testsinks.delete.json +5 -0
  238. package/generated/v1/snippets/testsinks.list.json +5 -0
  239. package/generated/v1/snippets/testsinks.read.json +5 -0
  240. package/generated/v1/snippets/usage.read.json +5 -0
  241. 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[];