@greatwait/contracts 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/dist/authority.d.ts +217 -0
- package/dist/authority.js +261 -0
- package/dist/callback-ownership.d.ts +58 -0
- package/dist/callback-ownership.js +71 -0
- package/dist/callback-safety.d.ts +38 -0
- package/dist/callback-safety.js +403 -0
- package/dist/compatibility.d.ts +82 -0
- package/dist/compatibility.js +115 -0
- package/dist/delivery.d.ts +401 -0
- package/dist/delivery.js +311 -0
- package/dist/endpoints.d.ts +305 -0
- package/dist/endpoints.js +179 -0
- package/dist/events.d.ts +62 -0
- package/dist/events.js +38 -0
- package/dist/first-value-journey.d.ts +305 -0
- package/dist/first-value-journey.js +466 -0
- package/dist/forecast.d.ts +443 -0
- package/dist/forecast.js +729 -0
- package/dist/identifiers.d.ts +109 -0
- package/dist/identifiers.js +131 -0
- package/dist/identity.d.ts +196 -0
- package/dist/identity.js +182 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +27 -0
- package/dist/mcp.d.ts +51 -0
- package/dist/mcp.js +56 -0
- package/dist/oauth.d.ts +796 -0
- package/dist/oauth.js +708 -0
- package/dist/operations.d.ts +3985 -0
- package/dist/operations.js +1837 -0
- package/dist/partition.d.ts +771 -0
- package/dist/partition.js +206 -0
- package/dist/policy.d.ts +93 -0
- package/dist/policy.js +180 -0
- package/dist/problems.d.ts +158 -0
- package/dist/problems.js +104 -0
- package/dist/protocol.d.ts +308 -0
- package/dist/protocol.js +515 -0
- package/dist/scheduling-records.d.ts +395 -0
- package/dist/scheduling-records.js +284 -0
- package/dist/scheduling.d.ts +1098 -0
- package/dist/scheduling.js +1116 -0
- package/dist/secrets.d.ts +383 -0
- package/dist/secrets.js +272 -0
- package/dist/signatures.d.ts +704 -0
- package/dist/signatures.js +867 -0
- package/dist/tenancy.d.ts +178 -0
- package/dist/tenancy.js +145 -0
- package/dist/test-sink.d.ts +986 -0
- package/dist/test-sink.js +755 -0
- package/dist/time.d.ts +45 -0
- package/dist/time.js +128 -0
- package/dist/usage.d.ts +121 -0
- package/dist/usage.js +200 -0
- package/dist/webhooks.d.ts +116 -0
- package/dist/webhooks.js +161 -0
- package/dist/workspace.d.ts +92 -0
- package/dist/workspace.js +59 -0
- package/generated/v1/callback-delivery-signature.json +467 -0
- package/generated/v1/callback-endpoint-verification.json +59 -0
- package/generated/v1/callback-receiver.json +241 -0
- package/generated/v1/examples/attempts.list.response.json +24 -0
- package/generated/v1/examples/attempts.read.response.json +20 -0
- package/generated/v1/examples/credentials.create.request.json +5 -0
- package/generated/v1/examples/credentials.create.response.json +10 -0
- package/generated/v1/examples/credentials.list.response.json +17 -0
- package/generated/v1/examples/credentials.revoke.response.json +11 -0
- package/generated/v1/examples/credentials.rotate.request.json +3 -0
- package/generated/v1/examples/credentials.rotate.response.json +22 -0
- package/generated/v1/examples/endpoints.create.request.json +3 -0
- package/generated/v1/examples/endpoints.create.response.json +16 -0
- package/generated/v1/examples/endpoints.disable.response.json +15 -0
- package/generated/v1/examples/endpoints.list.response.json +21 -0
- package/generated/v1/examples/endpoints.read.response.json +14 -0
- package/generated/v1/examples/endpoints.rotateSigningKey.request.json +3 -0
- package/generated/v1/examples/endpoints.rotateSigningKey.response.json +23 -0
- package/generated/v1/examples/endpoints.update.request.json +3 -0
- package/generated/v1/examples/endpoints.update.response.json +13 -0
- package/generated/v1/examples/endpoints.verify.response.json +15 -0
- package/generated/v1/examples/forecast.read.response.json +28 -0
- package/generated/v1/examples/forecast.windows.response.json +21 -0
- package/generated/v1/examples/grants.list.response.json +17 -0
- package/generated/v1/examples/grants.revoke.response.json +16 -0
- package/generated/v1/examples/meta.scaffold.response.json +5 -0
- package/generated/v1/examples/meta.version.response.json +5 -0
- package/generated/v1/examples/occurrences.list.response.json +17 -0
- package/generated/v1/examples/occurrences.read.response.json +15 -0
- package/generated/v1/examples/occurrences.redeliver.response.json +16 -0
- package/generated/v1/examples/projects.create.request.json +3 -0
- package/generated/v1/examples/projects.create.response.json +5 -0
- package/generated/v1/examples/projects.list.response.json +9 -0
- package/generated/v1/examples/schedules.archive.response.json +25 -0
- package/generated/v1/examples/schedules.cancel.response.json +24 -0
- package/generated/v1/examples/schedules.create.request.json +9 -0
- package/generated/v1/examples/schedules.create.response.json +25 -0
- package/generated/v1/examples/schedules.list.response.json +23 -0
- package/generated/v1/examples/schedules.pause.response.json +23 -0
- package/generated/v1/examples/schedules.read.response.json +25 -0
- package/generated/v1/examples/schedules.replace.request.json +8 -0
- package/generated/v1/examples/schedules.replace.response.json +25 -0
- package/generated/v1/examples/schedules.resume.response.json +23 -0
- package/generated/v1/examples/sinkdeliveries.list.response.json +19 -0
- package/generated/v1/examples/sinkdeliveries.read.response.json +16 -0
- package/generated/v1/examples/testsinks.configure.request.json +3 -0
- package/generated/v1/examples/testsinks.configure.response.json +13 -0
- package/generated/v1/examples/testsinks.create.request.json +8 -0
- package/generated/v1/examples/testsinks.create.response.json +13 -0
- package/generated/v1/examples/testsinks.delete.response.json +13 -0
- package/generated/v1/examples/testsinks.list.response.json +20 -0
- package/generated/v1/examples/testsinks.read.response.json +14 -0
- package/generated/v1/examples/usage.read.response.json +20 -0
- package/generated/v1/first-value-journey.json +436 -0
- package/generated/v1/manifest.json +1037 -0
- package/generated/v1/openapi.json +23665 -0
- package/generated/v1/policy-matrix.json +305 -0
- package/generated/v1/schemas/attempts.list.query.json +16 -0
- package/generated/v1/schemas/attempts.list.response.json +175 -0
- package/generated/v1/schemas/attempts.read.response.json +168 -0
- package/generated/v1/schemas/credentials.create.request.json +39 -0
- package/generated/v1/schemas/credentials.create.response.json +88 -0
- package/generated/v1/schemas/credentials.list.query.json +16 -0
- package/generated/v1/schemas/credentials.list.response.json +124 -0
- package/generated/v1/schemas/credentials.revoke.response.json +84 -0
- package/generated/v1/schemas/credentials.rotate.request.json +11 -0
- package/generated/v1/schemas/credentials.rotate.response.json +171 -0
- package/generated/v1/schemas/endpoints.create.request.json +12 -0
- package/generated/v1/schemas/endpoints.create.response.json +115 -0
- package/generated/v1/schemas/endpoints.disable.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.disable.response.json +100 -0
- package/generated/v1/schemas/endpoints.list.query.json +16 -0
- package/generated/v1/schemas/endpoints.list.response.json +146 -0
- package/generated/v1/schemas/endpoints.read.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.read.response.json +100 -0
- package/generated/v1/schemas/endpoints.rotateSigningKey.request-headers.json +10 -0
- package/generated/v1/schemas/endpoints.rotateSigningKey.request.json +11 -0
- package/generated/v1/schemas/endpoints.rotateSigningKey.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.rotateSigningKey.response.json +118 -0
- package/generated/v1/schemas/endpoints.update.request-headers.json +10 -0
- package/generated/v1/schemas/endpoints.update.request.json +12 -0
- package/generated/v1/schemas/endpoints.update.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.update.response.json +100 -0
- package/generated/v1/schemas/endpoints.verify.request-headers.json +10 -0
- package/generated/v1/schemas/endpoints.verify.response-headers.json +11 -0
- package/generated/v1/schemas/endpoints.verify.response.json +100 -0
- package/generated/v1/schemas/forecast.read.query.json +26 -0
- package/generated/v1/schemas/forecast.read.response.json +100 -0
- package/generated/v1/schemas/forecast.windows.query.json +26 -0
- package/generated/v1/schemas/forecast.windows.response.json +94 -0
- package/generated/v1/schemas/grants.list.response.json +98 -0
- package/generated/v1/schemas/grants.revoke.response.json +95 -0
- package/generated/v1/schemas/meta.scaffold.response.json +36 -0
- package/generated/v1/schemas/meta.version.response.json +19 -0
- package/generated/v1/schemas/occurrences.list.query.json +16 -0
- package/generated/v1/schemas/occurrences.list.response.json +83 -0
- package/generated/v1/schemas/occurrences.read.response.json +76 -0
- package/generated/v1/schemas/occurrences.redeliver.request-headers.json +13 -0
- package/generated/v1/schemas/occurrences.redeliver.response-headers.json +10 -0
- package/generated/v1/schemas/occurrences.redeliver.response.json +168 -0
- package/generated/v1/schemas/projects.create.request.json +12 -0
- package/generated/v1/schemas/projects.create.response.json +25 -0
- package/generated/v1/schemas/projects.list.response.json +35 -0
- package/generated/v1/schemas/schedules.archive.request-headers.json +10 -0
- package/generated/v1/schemas/schedules.archive.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.archive.response.json +189 -0
- package/generated/v1/schemas/schedules.cancel.request-headers.json +10 -0
- package/generated/v1/schemas/schedules.cancel.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.cancel.response.json +189 -0
- package/generated/v1/schemas/schedules.create.request-headers.json +12 -0
- package/generated/v1/schemas/schedules.create.request.json +99 -0
- package/generated/v1/schemas/schedules.create.response-headers.json +15 -0
- package/generated/v1/schemas/schedules.create.response.json +189 -0
- package/generated/v1/schemas/schedules.list.query.json +16 -0
- package/generated/v1/schemas/schedules.list.response.json +170 -0
- package/generated/v1/schemas/schedules.pause.request-headers.json +10 -0
- package/generated/v1/schemas/schedules.pause.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.pause.response.json +189 -0
- package/generated/v1/schemas/schedules.read.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.read.response.json +189 -0
- package/generated/v1/schemas/schedules.replace.request-headers.json +10 -0
- package/generated/v1/schemas/schedules.replace.request.json +99 -0
- package/generated/v1/schemas/schedules.replace.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.replace.response.json +189 -0
- package/generated/v1/schemas/schedules.resume.request-headers.json +10 -0
- package/generated/v1/schemas/schedules.resume.response-headers.json +11 -0
- package/generated/v1/schemas/schedules.resume.response.json +189 -0
- package/generated/v1/schemas/sinkdeliveries.list.query.json +16 -0
- package/generated/v1/schemas/sinkdeliveries.list.response.json +126 -0
- package/generated/v1/schemas/sinkdeliveries.read.response.json +98 -0
- package/generated/v1/schemas/testsinks.configure.request.json +66 -0
- package/generated/v1/schemas/testsinks.configure.response.json +115 -0
- package/generated/v1/schemas/testsinks.create.request.json +76 -0
- package/generated/v1/schemas/testsinks.create.response.json +115 -0
- package/generated/v1/schemas/testsinks.delete.response.json +115 -0
- package/generated/v1/schemas/testsinks.list.query.json +16 -0
- package/generated/v1/schemas/testsinks.list.response.json +161 -0
- package/generated/v1/schemas/testsinks.read.response.json +115 -0
- package/generated/v1/schemas/usage.read.query.json +15 -0
- package/generated/v1/schemas/usage.read.response.json +71 -0
- package/generated/v1/snippets/attempts.list.json +5 -0
- package/generated/v1/snippets/attempts.read.json +5 -0
- package/generated/v1/snippets/credentials.create.json +5 -0
- package/generated/v1/snippets/credentials.list.json +5 -0
- package/generated/v1/snippets/credentials.revoke.json +5 -0
- package/generated/v1/snippets/credentials.rotate.json +5 -0
- package/generated/v1/snippets/endpoints.create.json +5 -0
- package/generated/v1/snippets/endpoints.disable.json +5 -0
- package/generated/v1/snippets/endpoints.list.json +5 -0
- package/generated/v1/snippets/endpoints.read.json +5 -0
- package/generated/v1/snippets/endpoints.rotateSigningKey.json +5 -0
- package/generated/v1/snippets/endpoints.update.json +5 -0
- package/generated/v1/snippets/endpoints.verify.json +5 -0
- package/generated/v1/snippets/forecast.read.json +5 -0
- package/generated/v1/snippets/forecast.windows.json +5 -0
- package/generated/v1/snippets/grants.list.json +5 -0
- package/generated/v1/snippets/grants.revoke.json +5 -0
- package/generated/v1/snippets/meta.scaffold.json +5 -0
- package/generated/v1/snippets/meta.version.json +5 -0
- package/generated/v1/snippets/occurrences.list.json +5 -0
- package/generated/v1/snippets/occurrences.read.json +5 -0
- package/generated/v1/snippets/occurrences.redeliver.json +5 -0
- package/generated/v1/snippets/projects.create.json +5 -0
- package/generated/v1/snippets/projects.list.json +5 -0
- package/generated/v1/snippets/schedules.archive.json +5 -0
- package/generated/v1/snippets/schedules.cancel.json +5 -0
- package/generated/v1/snippets/schedules.create.json +5 -0
- package/generated/v1/snippets/schedules.list.json +5 -0
- package/generated/v1/snippets/schedules.pause.json +5 -0
- package/generated/v1/snippets/schedules.read.json +5 -0
- package/generated/v1/snippets/schedules.replace.json +5 -0
- package/generated/v1/snippets/schedules.resume.json +5 -0
- package/generated/v1/snippets/sinkdeliveries.list.json +5 -0
- package/generated/v1/snippets/sinkdeliveries.read.json +5 -0
- package/generated/v1/snippets/testsinks.configure.json +5 -0
- package/generated/v1/snippets/testsinks.create.json +5 -0
- package/generated/v1/snippets/testsinks.delete.json +5 -0
- package/generated/v1/snippets/testsinks.list.json +5 -0
- package/generated/v1/snippets/testsinks.read.json +5 -0
- package/generated/v1/snippets/usage.read.json +5 -0
- package/package.json +153 -0
|
@@ -0,0 +1,1837 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { callbackEndpointCreatedSchema, callbackEndpointPageSchema, callbackEndpointRequestSchema, callbackEndpointRotatedSchema, callbackEndpointSchema, callbackSigningKeyRotationRequestSchema, } from "./endpoints.js";
|
|
3
|
+
import { FORECAST_WINDOW_VERSION, forecastRequestSchema, forecastSchema, forecastWindowsSchema, } from "./forecast.js";
|
|
4
|
+
import { usageRequestSchema, usageSchema } from "./usage.js";
|
|
5
|
+
import { conditionalRequestHeaderSchema, idempotencyReplayResponseHeaderSchema, idempotencyRequestHeaderSchema, pageRequestSchema, replayedResponseHeaderSchema, requiredIdempotencyRequestHeaderSchema, versionResponseHeaderSchema, } from "./protocol.js";
|
|
6
|
+
import { oauthGrantListSchema, oauthGrantRevokedSchema } from "./oauth.js";
|
|
7
|
+
import { credentialCreatedSchema, credentialPageSchema, credentialRequestSchema, credentialRevokedSchema, credentialRotatedSchema, credentialRotationRequestSchema, } from "./secrets.js";
|
|
8
|
+
import { attemptPageSchema, attemptViewSchema, occurrencePageSchema, occurrenceViewSchema, schedulePageSchema, scheduleRequestSchema, scheduleViewSchema, } from "./scheduling.js";
|
|
9
|
+
import { testSinkCreateRequestSchema, testSinkDeliveryBodySchema, testSinkDeliveryPageSchema, testSinkPageSchema, testSinkProgramSchema, testSinkSchema, } from "./test-sink.js";
|
|
10
|
+
import { projectCreateRequestSchema, projectListSchema, projectSummarySchema, } from "./tenancy.js";
|
|
11
|
+
import { scaffoldVersionSchema, workspaceScaffoldSchema } from "./workspace.js";
|
|
12
|
+
import { timestampSchema } from "./time.js";
|
|
13
|
+
/**
|
|
14
|
+
* Deterministic behaviors the contract mock can be asked to produce.
|
|
15
|
+
*
|
|
16
|
+
* These are the failure modes an integrator must be able to rehearse before
|
|
17
|
+
* going live, and they are part of the contract rather than mock trivia: a
|
|
18
|
+
* customer tunes their receiver against exactly these categories.
|
|
19
|
+
*/
|
|
20
|
+
export const MOCK_BEHAVIORS = [
|
|
21
|
+
"ambiguous",
|
|
22
|
+
"redelivery",
|
|
23
|
+
"refusal",
|
|
24
|
+
"retry",
|
|
25
|
+
"success",
|
|
26
|
+
"timeout",
|
|
27
|
+
];
|
|
28
|
+
export const mockBehaviorSchema = z.enum(MOCK_BEHAVIORS);
|
|
29
|
+
/**
|
|
30
|
+
* A mock scenario. `now` pins the clock so a test that depends on time is
|
|
31
|
+
* reproducible: without it, "what does a stale signature look like" is a test
|
|
32
|
+
* that passes until it doesn't.
|
|
33
|
+
*/
|
|
34
|
+
export const mockScenarioRequestSchema = z.object({
|
|
35
|
+
behavior: mockBehaviorSchema,
|
|
36
|
+
/** Fixed instant the mock reports as the current time. */
|
|
37
|
+
now: timestampSchema.optional(),
|
|
38
|
+
/** How many attempts fail before `retry` succeeds. */
|
|
39
|
+
failuresBeforeSuccess: z.number().int().min(0).max(10).optional(),
|
|
40
|
+
});
|
|
41
|
+
export const mockScenarioResponseSchema = z.object({
|
|
42
|
+
behavior: mockBehaviorSchema,
|
|
43
|
+
/** The instant the mock will report for the duration of this scenario. */
|
|
44
|
+
now: timestampSchema,
|
|
45
|
+
scenarioId: z.string().min(1),
|
|
46
|
+
});
|
|
47
|
+
export const fixtureListSchema = z.object({
|
|
48
|
+
fixtures: z.array(z.object({ id: z.string().min(1), description: z.string().min(1) })),
|
|
49
|
+
});
|
|
50
|
+
export const OPERATIONS = [
|
|
51
|
+
{
|
|
52
|
+
creates: false,
|
|
53
|
+
description: "Reports the service, environment, and build the request reached. Unauthenticated: it carries no customer data and is used by canaries and deployment smoke checks.",
|
|
54
|
+
example: {
|
|
55
|
+
buildSha: "9f2c1ab",
|
|
56
|
+
environment: "development",
|
|
57
|
+
service: "@greatwait/api",
|
|
58
|
+
},
|
|
59
|
+
id: "meta.version",
|
|
60
|
+
idempotent: true,
|
|
61
|
+
// A resource, not a tool. It takes no arguments and reports a fact about
|
|
62
|
+
// the deployment; publishing it as a tool would put a build sha in an
|
|
63
|
+
// agent's tool list beside the operations that actually administer
|
|
64
|
+
// schedules, and an agent picking tools reads that list.
|
|
65
|
+
mcp: { kind: "resource", uri: "greatwait://meta/version" },
|
|
66
|
+
method: "GET",
|
|
67
|
+
mutates: false,
|
|
68
|
+
path: "/__greatwait/version",
|
|
69
|
+
principals: [],
|
|
70
|
+
problems: [],
|
|
71
|
+
response: scaffoldVersionSchema,
|
|
72
|
+
scopes: [],
|
|
73
|
+
summary: "Read the deployed version",
|
|
74
|
+
surfaces: ["api", "mock"],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
creates: false,
|
|
78
|
+
description: "Reports the workspace layer this service occupies and the layers it may depend on. Exists so the architecture boundary is observable at runtime, not only at build time.",
|
|
79
|
+
example: {
|
|
80
|
+
dependsOn: ["application", "contract", "platform"],
|
|
81
|
+
layer: "adapter",
|
|
82
|
+
service: "@greatwait/api",
|
|
83
|
+
},
|
|
84
|
+
id: "meta.scaffold",
|
|
85
|
+
idempotent: true,
|
|
86
|
+
mcp: { kind: "resource", uri: "greatwait://meta/scaffold" },
|
|
87
|
+
method: "GET",
|
|
88
|
+
mutates: false,
|
|
89
|
+
path: "/_scaffold",
|
|
90
|
+
principals: [],
|
|
91
|
+
problems: [],
|
|
92
|
+
response: workspaceScaffoldSchema,
|
|
93
|
+
scopes: [],
|
|
94
|
+
summary: "Read the workspace scaffold",
|
|
95
|
+
surfaces: ["api", "mock"],
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
creates: false,
|
|
99
|
+
description: "Lists the committed contract fixtures the mock can serve. Mock-only: the API has no fixtures.",
|
|
100
|
+
example: {
|
|
101
|
+
fixtures: [
|
|
102
|
+
{
|
|
103
|
+
description: "The workspace scaffold every service reports.",
|
|
104
|
+
id: "workspace-scaffold",
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
id: "mock.listFixtures",
|
|
109
|
+
idempotent: true,
|
|
110
|
+
mcp: {
|
|
111
|
+
kind: "unavailable",
|
|
112
|
+
reason: "The contract mock's control plane. An agent handed this would call it against the real service.",
|
|
113
|
+
},
|
|
114
|
+
method: "GET",
|
|
115
|
+
mutates: false,
|
|
116
|
+
path: "/_contract/fixtures",
|
|
117
|
+
principals: [],
|
|
118
|
+
problems: [],
|
|
119
|
+
response: fixtureListSchema,
|
|
120
|
+
scopes: [],
|
|
121
|
+
summary: "List contract fixtures",
|
|
122
|
+
surfaces: ["mock"],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
creates: false,
|
|
126
|
+
description: "Serves one committed fixture, byte-identical on every call. Returns a not_found problem for an unknown identifier.",
|
|
127
|
+
example: {
|
|
128
|
+
dependsOn: ["application", "contract", "platform"],
|
|
129
|
+
layer: "adapter",
|
|
130
|
+
service: "@greatwait/contract-mock",
|
|
131
|
+
},
|
|
132
|
+
id: "mock.getFixture",
|
|
133
|
+
idempotent: true,
|
|
134
|
+
mcp: {
|
|
135
|
+
kind: "unavailable",
|
|
136
|
+
reason: "The contract mock's control plane. An agent handed this would call it against the real service.",
|
|
137
|
+
},
|
|
138
|
+
method: "GET",
|
|
139
|
+
mutates: false,
|
|
140
|
+
path: "/_contract/fixtures/{fixtureId}",
|
|
141
|
+
principals: [],
|
|
142
|
+
problems: ["not_found"],
|
|
143
|
+
response: workspaceScaffoldSchema,
|
|
144
|
+
scopes: [],
|
|
145
|
+
summary: "Read a contract fixture",
|
|
146
|
+
surfaces: ["mock"],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
creates: true,
|
|
150
|
+
description: "Creates a reusable callback endpoint in the caller's project and environment. The destination must be HTTPS on effective port 443, contain no URL credentials or fragment, and resolve only to public unicast addresses. The endpoint begins pending. Whether delivery waits for its secret-bound ownership challenge is a deployment policy: where the challenge is not required, which is the default, a pending endpoint is delivered to and answering the challenge only records the instant it proved possession; where it is required, attaching a pending endpoint to a schedule is refused endpoint_unverified until the challenge succeeds. A disabled endpoint is never delivered to under either policy, and a schedule write naming one answers not_found, because disabling is terminal rather than a state to recover from. The per-endpoint secret is returned in this response and nowhere else. The 50-record safety ceiling includes disabled endpoints because they remain readable for audit.",
|
|
151
|
+
example: {
|
|
152
|
+
createdAt: "2026-08-05T15:00:00.000Z",
|
|
153
|
+
environmentKind: "test",
|
|
154
|
+
id: "ep_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
155
|
+
keyId: "csk_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
156
|
+
project: "proj_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
157
|
+
secret: "ptsk_test_<the-one-time-endpoint-secret-here>",
|
|
158
|
+
shownOnce: true,
|
|
159
|
+
signingKeys: { current: "csk_01h8xyz9k2m4n6p8r0t2v4w6y8" },
|
|
160
|
+
status: "pending",
|
|
161
|
+
updatedAt: "2026-08-05T15:00:00.000Z",
|
|
162
|
+
url: "https://hooks.example.com/greatwait",
|
|
163
|
+
version: 1,
|
|
164
|
+
},
|
|
165
|
+
id: "endpoints.create",
|
|
166
|
+
idempotent: false,
|
|
167
|
+
mcp: {
|
|
168
|
+
kind: "unavailable",
|
|
169
|
+
reason: "Creation returns a one-time endpoint signing secret, and MCP has no negotiated secure-result channel that can keep it out of ordinary tool history.",
|
|
170
|
+
},
|
|
171
|
+
method: "POST",
|
|
172
|
+
mutates: true,
|
|
173
|
+
path: "/v1/endpoints",
|
|
174
|
+
principals: ["api-credential", "oauth", "session"],
|
|
175
|
+
problems: ["invalid_request", "plan_limit_reached", "rate_limited"],
|
|
176
|
+
request: callbackEndpointRequestSchema,
|
|
177
|
+
requestExample: { url: "https://hooks.example.com/greatwait" },
|
|
178
|
+
response: callbackEndpointCreatedSchema,
|
|
179
|
+
scopes: ["endpoints:write"],
|
|
180
|
+
sessionAuthority: "headers",
|
|
181
|
+
summary: "Create a callback endpoint",
|
|
182
|
+
surfaces: ["api", "mock"],
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
creates: false,
|
|
186
|
+
description: "Lists the caller's callback endpoints for exactly its own project and environment. The result is authoritative and metadata-only: no signing secret or sealed key material has a field in this shape.",
|
|
187
|
+
example: {
|
|
188
|
+
data: [
|
|
189
|
+
{
|
|
190
|
+
createdAt: "2026-08-05T15:00:00.000Z",
|
|
191
|
+
environmentKind: "test",
|
|
192
|
+
id: "ep_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
193
|
+
project: "proj_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
194
|
+
signingKeys: { current: "csk_01h8xyz9k2m4n6p8r0t2v4w6y8" },
|
|
195
|
+
status: "verified",
|
|
196
|
+
updatedAt: "2026-08-05T15:01:00.000Z",
|
|
197
|
+
url: "https://hooks.example.com/greatwait",
|
|
198
|
+
verifiedAt: "2026-08-05T15:01:00.000Z",
|
|
199
|
+
version: 2,
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
freshness: { kind: "authoritative" },
|
|
203
|
+
},
|
|
204
|
+
id: "endpoints.list",
|
|
205
|
+
idempotent: true,
|
|
206
|
+
mcp: { destructive: false, kind: "tool", openWorld: false },
|
|
207
|
+
method: "GET",
|
|
208
|
+
mutates: false,
|
|
209
|
+
path: "/v1/endpoints",
|
|
210
|
+
principals: ["api-credential", "oauth", "session"],
|
|
211
|
+
problems: ["invalid_request"],
|
|
212
|
+
query: pageRequestSchema,
|
|
213
|
+
response: callbackEndpointPageSchema,
|
|
214
|
+
scopes: ["endpoints:read"],
|
|
215
|
+
sessionAuthority: "headers",
|
|
216
|
+
summary: "List callback endpoints",
|
|
217
|
+
surfaces: ["api", "mock"],
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
creates: false,
|
|
221
|
+
description: "Reads one callback endpoint from the caller's own project and environment. A well-formed identifier belonging to another authority is indistinguishable from an absent endpoint, and the response never contains secret material.",
|
|
222
|
+
example: {
|
|
223
|
+
createdAt: "2026-08-05T15:00:00.000Z",
|
|
224
|
+
environmentKind: "test",
|
|
225
|
+
id: "ep_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
226
|
+
project: "proj_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
227
|
+
signingKeys: { current: "csk_01h8xyz9k2m4n6p8r0t2v4w6y8" },
|
|
228
|
+
status: "verified",
|
|
229
|
+
updatedAt: "2026-08-05T15:01:00.000Z",
|
|
230
|
+
url: "https://hooks.example.com/greatwait",
|
|
231
|
+
verifiedAt: "2026-08-05T15:01:00.000Z",
|
|
232
|
+
version: 2,
|
|
233
|
+
},
|
|
234
|
+
id: "endpoints.read",
|
|
235
|
+
idempotent: true,
|
|
236
|
+
mcp: { destructive: false, kind: "tool", openWorld: false },
|
|
237
|
+
method: "GET",
|
|
238
|
+
mutates: false,
|
|
239
|
+
path: "/v1/endpoints/{endpointId}",
|
|
240
|
+
principals: ["api-credential", "oauth", "session"],
|
|
241
|
+
problems: ["invalid_request", "not_found"],
|
|
242
|
+
response: callbackEndpointSchema,
|
|
243
|
+
// The version a conditional update or verification must send back. Published
|
|
244
|
+
// because a caller expected to put a value in `If-Match` needs the operation
|
|
245
|
+
// that hands it out to say so — the same reason `schedules.read` carries it.
|
|
246
|
+
responseHeaders: versionResponseHeaderSchema,
|
|
247
|
+
scopes: ["endpoints:read"],
|
|
248
|
+
sessionAuthority: "headers",
|
|
249
|
+
summary: "Read a callback endpoint",
|
|
250
|
+
surfaces: ["api", "mock"],
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
creates: false,
|
|
254
|
+
description: "Replaces an endpoint's destination after applying the complete URL and DNS policy. A changed destination resets verification to pending; the endpoint cannot be used again until ownership of the new URL is proved. Disabled endpoints cannot be updated. Send If-Match with the version the endpoint was read at to be refused rather than overwrite a destination somebody else changed in the meantime.",
|
|
255
|
+
example: {
|
|
256
|
+
createdAt: "2026-08-05T15:00:00.000Z",
|
|
257
|
+
environmentKind: "test",
|
|
258
|
+
id: "ep_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
259
|
+
project: "proj_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
260
|
+
signingKeys: { current: "csk_01h8xyz9k2m4n6p8r0t2v4w6y8" },
|
|
261
|
+
status: "pending",
|
|
262
|
+
updatedAt: "2026-08-05T16:00:00.000Z",
|
|
263
|
+
url: "https://hooks.example.com/replacement",
|
|
264
|
+
version: 3,
|
|
265
|
+
},
|
|
266
|
+
id: "endpoints.update",
|
|
267
|
+
idempotent: true,
|
|
268
|
+
mcp: { destructive: true, kind: "tool", openWorld: true },
|
|
269
|
+
method: "PUT",
|
|
270
|
+
mutates: true,
|
|
271
|
+
path: "/v1/endpoints/{endpointId}",
|
|
272
|
+
principals: ["api-credential", "oauth", "session"],
|
|
273
|
+
problems: [
|
|
274
|
+
"invalid_request",
|
|
275
|
+
"not_found",
|
|
276
|
+
"rate_limited",
|
|
277
|
+
"state_conflict",
|
|
278
|
+
"version_conflict",
|
|
279
|
+
],
|
|
280
|
+
request: callbackEndpointRequestSchema,
|
|
281
|
+
requestExample: { url: "https://hooks.example.com/replacement" },
|
|
282
|
+
requestHeaders: conditionalRequestHeaderSchema,
|
|
283
|
+
response: callbackEndpointSchema,
|
|
284
|
+
responseHeaders: versionResponseHeaderSchema,
|
|
285
|
+
scopes: ["endpoints:write"],
|
|
286
|
+
sessionAuthority: "headers",
|
|
287
|
+
summary: "Update a callback endpoint",
|
|
288
|
+
surfaces: ["api", "mock"],
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
creates: false,
|
|
292
|
+
description: "Re-resolves the endpoint immediately before sending one bounded ownership challenge with redirects disabled. The receiver proves possession of the one-time endpoint secret in its response. A successful challenge marks the endpoint verified; a failed challenge records only a safe failure category. Send If-Match with the version the endpoint was read at to be refused rather than challenge a destination somebody else has since changed.",
|
|
293
|
+
example: {
|
|
294
|
+
createdAt: "2026-08-05T15:00:00.000Z",
|
|
295
|
+
environmentKind: "test",
|
|
296
|
+
id: "ep_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
297
|
+
lastVerificationAttemptAt: "2026-08-05T15:01:00.000Z",
|
|
298
|
+
project: "proj_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
299
|
+
signingKeys: { current: "csk_01h8xyz9k2m4n6p8r0t2v4w6y8" },
|
|
300
|
+
status: "verified",
|
|
301
|
+
updatedAt: "2026-08-05T15:01:00.000Z",
|
|
302
|
+
url: "https://hooks.example.com/greatwait",
|
|
303
|
+
verifiedAt: "2026-08-05T15:01:00.000Z",
|
|
304
|
+
version: 2,
|
|
305
|
+
},
|
|
306
|
+
id: "endpoints.verify",
|
|
307
|
+
idempotent: false,
|
|
308
|
+
mcp: { destructive: false, kind: "tool", openWorld: true },
|
|
309
|
+
method: "POST",
|
|
310
|
+
mutates: true,
|
|
311
|
+
path: "/v1/endpoints/{endpointId}/verify",
|
|
312
|
+
principals: ["api-credential", "oauth", "session"],
|
|
313
|
+
problems: [
|
|
314
|
+
"invalid_request",
|
|
315
|
+
"not_found",
|
|
316
|
+
"rate_limited",
|
|
317
|
+
"state_conflict",
|
|
318
|
+
"version_conflict",
|
|
319
|
+
],
|
|
320
|
+
requestHeaders: conditionalRequestHeaderSchema,
|
|
321
|
+
response: callbackEndpointSchema,
|
|
322
|
+
responseHeaders: versionResponseHeaderSchema,
|
|
323
|
+
scopes: ["endpoints:write"],
|
|
324
|
+
sessionAuthority: "headers",
|
|
325
|
+
summary: "Verify callback endpoint ownership",
|
|
326
|
+
surfaces: ["api", "mock"],
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
creates: true,
|
|
330
|
+
description: "Creates the next endpoint signing key and selects it immediately for new deliveries while retaining the current key in the receiver acceptance set for a bounded overlap. The overlap defaults to and cannot exceed 24 hours. A second rotation is refused until the current overlap ends. The new secret is returned once and has no recovery operation, so a call whose answer was never read may have rotated a key nobody holds: read the endpoint and compare its signing keys before repeating one. Send If-Match with the version the endpoint was read at to be refused rather than rotate an endpoint that has changed since.",
|
|
331
|
+
example: {
|
|
332
|
+
endpoint: {
|
|
333
|
+
createdAt: "2026-08-05T15:00:00.000Z",
|
|
334
|
+
environmentKind: "test",
|
|
335
|
+
id: "ep_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
336
|
+
project: "proj_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
337
|
+
signingKeys: {
|
|
338
|
+
current: "csk_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
339
|
+
next: {
|
|
340
|
+
id: "csk_11h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
341
|
+
overlapEndsAt: "2026-08-06T15:05:00.000Z",
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
status: "verified",
|
|
345
|
+
updatedAt: "2026-08-05T15:05:00.000Z",
|
|
346
|
+
url: "https://hooks.example.com/greatwait",
|
|
347
|
+
verifiedAt: "2026-08-05T15:01:00.000Z",
|
|
348
|
+
version: 3,
|
|
349
|
+
},
|
|
350
|
+
keyId: "csk_11h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
351
|
+
secret: "ptsk_test_<the-next-one-time-endpoint-secret-here>",
|
|
352
|
+
shownOnce: true,
|
|
353
|
+
},
|
|
354
|
+
id: "endpoints.rotateSigningKey",
|
|
355
|
+
idempotent: false,
|
|
356
|
+
mcp: {
|
|
357
|
+
kind: "unavailable",
|
|
358
|
+
reason: "Rotation returns a one-time endpoint signing secret, and MCP has no negotiated secure-result channel that can keep it out of ordinary tool history.",
|
|
359
|
+
},
|
|
360
|
+
method: "POST",
|
|
361
|
+
mutates: true,
|
|
362
|
+
path: "/v1/endpoints/{endpointId}/signing-keys/rotate",
|
|
363
|
+
principals: ["api-credential", "oauth", "session"],
|
|
364
|
+
problems: [
|
|
365
|
+
"invalid_request",
|
|
366
|
+
"not_found",
|
|
367
|
+
"state_conflict",
|
|
368
|
+
"version_conflict",
|
|
369
|
+
],
|
|
370
|
+
request: callbackSigningKeyRotationRequestSchema,
|
|
371
|
+
requestExample: { overlapSeconds: 3600 },
|
|
372
|
+
requestHeaders: conditionalRequestHeaderSchema,
|
|
373
|
+
response: callbackEndpointRotatedSchema,
|
|
374
|
+
// The endpoint's version after the rotation, so the next conditional write
|
|
375
|
+
// from the same page has something to send.
|
|
376
|
+
responseHeaders: versionResponseHeaderSchema,
|
|
377
|
+
scopes: ["endpoints:write"],
|
|
378
|
+
sessionAuthority: "headers",
|
|
379
|
+
summary: "Rotate a callback endpoint signing key",
|
|
380
|
+
surfaces: ["api", "mock"],
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
creates: false,
|
|
384
|
+
description: "Disables an endpoint immediately and permanently: there is no operation that re-enables one. It stays readable for audit but is no longer eligible for update, verification or callback send admission, and schedules pointed at it deliver nothing. Repeating the disable is idempotent and does not write a second audit event, which is why it takes no precondition — the outcome does not depend on what else has changed.",
|
|
385
|
+
example: {
|
|
386
|
+
createdAt: "2026-08-05T15:00:00.000Z",
|
|
387
|
+
disabledAt: "2026-08-05T17:00:00.000Z",
|
|
388
|
+
environmentKind: "test",
|
|
389
|
+
id: "ep_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
390
|
+
project: "proj_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
391
|
+
signingKeys: { current: "csk_01h8xyz9k2m4n6p8r0t2v4w6y8" },
|
|
392
|
+
status: "disabled",
|
|
393
|
+
updatedAt: "2026-08-05T17:00:00.000Z",
|
|
394
|
+
url: "https://hooks.example.com/greatwait",
|
|
395
|
+
verifiedAt: "2026-08-05T15:01:00.000Z",
|
|
396
|
+
version: 3,
|
|
397
|
+
},
|
|
398
|
+
id: "endpoints.disable",
|
|
399
|
+
idempotent: true,
|
|
400
|
+
mcp: { destructive: true, kind: "tool", openWorld: false },
|
|
401
|
+
method: "DELETE",
|
|
402
|
+
mutates: true,
|
|
403
|
+
path: "/v1/endpoints/{endpointId}",
|
|
404
|
+
principals: ["api-credential", "oauth", "session"],
|
|
405
|
+
problems: ["invalid_request", "not_found"],
|
|
406
|
+
response: callbackEndpointSchema,
|
|
407
|
+
responseHeaders: versionResponseHeaderSchema,
|
|
408
|
+
scopes: ["endpoints:write"],
|
|
409
|
+
sessionAuthority: "headers",
|
|
410
|
+
summary: "Disable a callback endpoint",
|
|
411
|
+
surfaces: ["api", "mock"],
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
creates: true,
|
|
415
|
+
description: "Creates a deterministic receiver for one callback endpoint so failures can be rehearsed before they happen in production. Test environments only: a live principal has no such operation, and there is no request field that could widen the organization, project, or environment the sink belongs to. The sink verifies deliveries with the bound endpoint's own signing keys, so key rotation and a wrong key behave here exactly as they would at the customer's receiver. It receives and never sends: no field names a destination and no configuration makes it forward a delivery. Point the endpoint at the returned url and verify it, and every attempt lands here.",
|
|
416
|
+
example: {
|
|
417
|
+
createdAt: "2026-08-07T15:00:00.000Z",
|
|
418
|
+
deliveryCount: 0,
|
|
419
|
+
endpoint: "ep_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
420
|
+
environmentKind: "test",
|
|
421
|
+
id: "tsk_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
422
|
+
program: { behavior: "success" },
|
|
423
|
+
project: "proj_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
424
|
+
updatedAt: "2026-08-07T15:00:00.000Z",
|
|
425
|
+
url: "https://sink.greatwait.dev/__greatwait/test-sinks/tsk_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
426
|
+
},
|
|
427
|
+
id: "testsinks.create",
|
|
428
|
+
idempotent: false,
|
|
429
|
+
mcp: { destructive: false, kind: "tool", openWorld: false },
|
|
430
|
+
method: "POST",
|
|
431
|
+
mutates: true,
|
|
432
|
+
path: "/v1/test-sinks",
|
|
433
|
+
// **A session, from W4-02's third merge, and this is the last of the
|
|
434
|
+
// moratorium.** The story's first acceptance criterion says a member
|
|
435
|
+
// reaches an observed test callback "without holding an API credential",
|
|
436
|
+
// and a sink is the only receiver this product hosts — so leaving this
|
|
437
|
+
// refused would mean onboarding that ends where it always ended, at a
|
|
438
|
+
// terminal. The three sink writes move together because a surface that can
|
|
439
|
+
// make one and neither reprogram nor remove it is a surface that leaves a
|
|
440
|
+
// hosted receiver bound to somebody's destination with no way back.
|
|
441
|
+
principals: ["api-credential", "oauth", "session"],
|
|
442
|
+
problems: [
|
|
443
|
+
"invalid_request",
|
|
444
|
+
"not_found",
|
|
445
|
+
"plan_limit_reached",
|
|
446
|
+
"state_conflict",
|
|
447
|
+
],
|
|
448
|
+
request: testSinkCreateRequestSchema,
|
|
449
|
+
requestExample: {
|
|
450
|
+
endpoint: "ep_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
451
|
+
program: { behavior: "server_error", forDeliveries: 2, then: "success" },
|
|
452
|
+
},
|
|
453
|
+
response: testSinkSchema,
|
|
454
|
+
scopes: ["endpoints:write"],
|
|
455
|
+
sessionAuthority: "headers",
|
|
456
|
+
summary: "Create a test sink",
|
|
457
|
+
surfaces: ["api", "mock"],
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
creates: false,
|
|
461
|
+
description: "Lists the caller's test sinks for exactly its own project and test environment. A live principal receives an empty-handed refusal rather than another environment's sinks.",
|
|
462
|
+
example: {
|
|
463
|
+
data: [
|
|
464
|
+
{
|
|
465
|
+
createdAt: "2026-08-07T15:00:00.000Z",
|
|
466
|
+
deliveryCount: 3,
|
|
467
|
+
endpoint: "ep_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
468
|
+
environmentKind: "test",
|
|
469
|
+
id: "tsk_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
470
|
+
program: { behavior: "success" },
|
|
471
|
+
project: "proj_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
472
|
+
updatedAt: "2026-08-07T15:02:00.000Z",
|
|
473
|
+
url: "https://sink.greatwait.dev/__greatwait/test-sinks/tsk_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
freshness: { kind: "authoritative" },
|
|
477
|
+
},
|
|
478
|
+
id: "testsinks.list",
|
|
479
|
+
idempotent: true,
|
|
480
|
+
mcp: { destructive: false, kind: "tool", openWorld: false },
|
|
481
|
+
method: "GET",
|
|
482
|
+
mutates: false,
|
|
483
|
+
path: "/v1/test-sinks",
|
|
484
|
+
principals: ["api-credential", "oauth", "session"],
|
|
485
|
+
problems: ["invalid_request", "state_conflict"],
|
|
486
|
+
query: pageRequestSchema,
|
|
487
|
+
response: testSinkPageSchema,
|
|
488
|
+
scopes: ["endpoints:read"],
|
|
489
|
+
sessionAuthority: "headers",
|
|
490
|
+
summary: "List test sinks",
|
|
491
|
+
surfaces: ["api", "mock"],
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
creates: false,
|
|
495
|
+
description: "Reads one test sink from the caller's own project and test environment, including how many deliveries it has taken. A well-formed identifier belonging to another authority is indistinguishable from an absent sink.",
|
|
496
|
+
example: {
|
|
497
|
+
createdAt: "2026-08-07T15:00:00.000Z",
|
|
498
|
+
deliveryCount: 3,
|
|
499
|
+
endpoint: "ep_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
500
|
+
environmentKind: "test",
|
|
501
|
+
id: "tsk_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
502
|
+
program: { behavior: "rate_limited", retryAfterSeconds: 5 },
|
|
503
|
+
project: "proj_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
504
|
+
updatedAt: "2026-08-07T15:02:00.000Z",
|
|
505
|
+
url: "https://sink.greatwait.dev/__greatwait/test-sinks/tsk_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
506
|
+
},
|
|
507
|
+
id: "testsinks.read",
|
|
508
|
+
idempotent: true,
|
|
509
|
+
mcp: { destructive: false, kind: "tool", openWorld: false },
|
|
510
|
+
method: "GET",
|
|
511
|
+
mutates: false,
|
|
512
|
+
path: "/v1/test-sinks/{sinkId}",
|
|
513
|
+
principals: ["api-credential", "oauth", "session"],
|
|
514
|
+
problems: ["invalid_request", "not_found", "state_conflict"],
|
|
515
|
+
response: testSinkSchema,
|
|
516
|
+
scopes: ["endpoints:read"],
|
|
517
|
+
sessionAuthority: "headers",
|
|
518
|
+
summary: "Read a test sink",
|
|
519
|
+
surfaces: ["api", "mock"],
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
creates: false,
|
|
523
|
+
description: "Replaces a sink's behavior program. The program is resolved against the number of deliveries the sink has already taken, so `forDeliveries` counts from the next delivery rather than from the sink's creation. Repeating the identical program is idempotent and changes nothing an integrator can observe.",
|
|
524
|
+
example: {
|
|
525
|
+
createdAt: "2026-08-07T15:00:00.000Z",
|
|
526
|
+
deliveryCount: 3,
|
|
527
|
+
endpoint: "ep_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
528
|
+
environmentKind: "test",
|
|
529
|
+
id: "tsk_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
530
|
+
program: { behavior: "timeout" },
|
|
531
|
+
project: "proj_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
532
|
+
updatedAt: "2026-08-07T15:03:00.000Z",
|
|
533
|
+
url: "https://sink.greatwait.dev/__greatwait/test-sinks/tsk_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
534
|
+
},
|
|
535
|
+
id: "testsinks.configure",
|
|
536
|
+
idempotent: true,
|
|
537
|
+
mcp: { destructive: false, kind: "tool", openWorld: false },
|
|
538
|
+
method: "PUT",
|
|
539
|
+
mutates: true,
|
|
540
|
+
path: "/v1/test-sinks/{sinkId}/program",
|
|
541
|
+
principals: ["api-credential", "oauth", "session"],
|
|
542
|
+
problems: ["invalid_request", "not_found", "state_conflict"],
|
|
543
|
+
request: testSinkProgramSchema,
|
|
544
|
+
requestExample: { behavior: "timeout" },
|
|
545
|
+
response: testSinkSchema,
|
|
546
|
+
scopes: ["endpoints:write"],
|
|
547
|
+
sessionAuthority: "headers",
|
|
548
|
+
summary: "Set a test sink behavior program",
|
|
549
|
+
surfaces: ["api", "mock"],
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
creates: false,
|
|
553
|
+
description: "Lists what the sink received, most recent first with the record identifier breaking a tie, bounded by the retained-delivery ceiling. Metadata only: the identifiers, the signing key that authenticated the delivery, how the receiver disposed of it, and what it answered. The delivered body is not in this shape.",
|
|
554
|
+
example: {
|
|
555
|
+
data: [
|
|
556
|
+
{
|
|
557
|
+
behavior: "success",
|
|
558
|
+
delivery: "dlv_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
559
|
+
disposition: "processed",
|
|
560
|
+
id: "tsd_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
561
|
+
keyId: "csk_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
562
|
+
occurrenceId: "occ_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
563
|
+
receivedAt: "2026-08-07T15:02:00.000Z",
|
|
564
|
+
respondedWith: 200,
|
|
565
|
+
scheduleId: "sch_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
566
|
+
sink: "tsk_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
567
|
+
},
|
|
568
|
+
],
|
|
569
|
+
freshness: { kind: "authoritative" },
|
|
570
|
+
},
|
|
571
|
+
id: "sinkdeliveries.list",
|
|
572
|
+
idempotent: true,
|
|
573
|
+
mcp: { destructive: false, kind: "tool", openWorld: false },
|
|
574
|
+
method: "GET",
|
|
575
|
+
mutates: false,
|
|
576
|
+
path: "/v1/test-sinks/{sinkId}/deliveries",
|
|
577
|
+
principals: ["api-credential", "oauth", "session"],
|
|
578
|
+
problems: ["invalid_request", "not_found", "state_conflict"],
|
|
579
|
+
query: pageRequestSchema,
|
|
580
|
+
response: testSinkDeliveryPageSchema,
|
|
581
|
+
scopes: ["endpoints:read"],
|
|
582
|
+
sessionAuthority: "headers",
|
|
583
|
+
summary: "List test sink deliveries",
|
|
584
|
+
surfaces: ["api", "mock"],
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
creates: false,
|
|
588
|
+
description: "Reads one recorded delivery including the exact bytes that arrived, truncated at the published cap with the original length stated. This is the one place a callback body is readable, and it is narrow on purpose: a test environment only, the creating authority only, a single record rather than a listing, and never a log. The signature is included because a MAC is not secret and comparing it against a locally computed one is how a body-mutation bug is found; no signing key appears in this shape.",
|
|
589
|
+
example: {
|
|
590
|
+
behavior: "success",
|
|
591
|
+
body: '{"version":"2026-08-08","type":"schedule.occurrence","scheduleId":"sch_01h8xyz9k2m4n6p8r0t2v4w6y8","occurrenceId":"occ_01h8xyz9k2m4n6p8r0t2v4w6y8","plannedFor":"2026-08-07T15:00:00.000Z","attempt":1,"trigger":{"kind":"automatic"},"data":{}}',
|
|
592
|
+
bodyTruncated: false,
|
|
593
|
+
delivery: "dlv_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
594
|
+
disposition: "processed",
|
|
595
|
+
id: "tsd_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
596
|
+
keyId: "csk_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
597
|
+
occurrenceId: "occ_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
598
|
+
receivedAt: "2026-08-07T15:02:00.000Z",
|
|
599
|
+
receivedBytes: 240,
|
|
600
|
+
respondedWith: 200,
|
|
601
|
+
scheduleId: "sch_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
602
|
+
signature: "v1=11135fdfc0bc5d49ebcc9d3a0d8df1116179097cefe961c6eca281339e5cfab3",
|
|
603
|
+
sink: "tsk_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
604
|
+
},
|
|
605
|
+
id: "sinkdeliveries.read",
|
|
606
|
+
idempotent: true,
|
|
607
|
+
mcp: {
|
|
608
|
+
kind: "unavailable",
|
|
609
|
+
reason: "The only operation that returns a delivered callback body. An agent handed it would put customer payload into ordinary tool history, which is the disclosure the narrow exception was written to avoid.",
|
|
610
|
+
},
|
|
611
|
+
method: "GET",
|
|
612
|
+
mutates: false,
|
|
613
|
+
path: "/v1/test-sinks/{sinkId}/deliveries/{deliveryRecordId}",
|
|
614
|
+
principals: ["api-credential", "oauth", "session"],
|
|
615
|
+
problems: ["invalid_request", "not_found", "state_conflict"],
|
|
616
|
+
response: testSinkDeliveryBodySchema,
|
|
617
|
+
scopes: ["endpoints:read"],
|
|
618
|
+
sessionAuthority: "headers",
|
|
619
|
+
summary: "Read a test sink delivery",
|
|
620
|
+
surfaces: ["api", "mock"],
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
creates: false,
|
|
624
|
+
description: "Deletes a sink and every delivery it recorded. The bound callback endpoint is untouched and keeps its signing keys; deliveries sent afterwards find nothing at the url and fail the way an unreachable receiver does.",
|
|
625
|
+
example: {
|
|
626
|
+
createdAt: "2026-08-07T15:00:00.000Z",
|
|
627
|
+
deliveryCount: 3,
|
|
628
|
+
endpoint: "ep_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
629
|
+
environmentKind: "test",
|
|
630
|
+
id: "tsk_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
631
|
+
program: { behavior: "success" },
|
|
632
|
+
project: "proj_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
633
|
+
updatedAt: "2026-08-07T15:04:00.000Z",
|
|
634
|
+
url: "https://sink.greatwait.dev/__greatwait/test-sinks/tsk_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
635
|
+
},
|
|
636
|
+
id: "testsinks.delete",
|
|
637
|
+
idempotent: true,
|
|
638
|
+
mcp: { destructive: true, kind: "tool", openWorld: false },
|
|
639
|
+
method: "DELETE",
|
|
640
|
+
mutates: true,
|
|
641
|
+
path: "/v1/test-sinks/{sinkId}",
|
|
642
|
+
principals: ["api-credential", "oauth", "session"],
|
|
643
|
+
problems: ["invalid_request", "not_found", "state_conflict"],
|
|
644
|
+
response: testSinkSchema,
|
|
645
|
+
scopes: ["endpoints:write"],
|
|
646
|
+
sessionAuthority: "headers",
|
|
647
|
+
summary: "Delete a test sink",
|
|
648
|
+
surfaces: ["api", "mock"],
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
creates: true,
|
|
652
|
+
description: "Pins the mock's behavior and clock so an integrator can rehearse success, refusal, retry, timeout, ambiguity, and redelivery deterministically. Mock-only, and never evidence that the real runtime behaves this way.",
|
|
653
|
+
example: {
|
|
654
|
+
behavior: "retry",
|
|
655
|
+
now: "2026-01-01T00:00:00.000Z",
|
|
656
|
+
scenarioId: "scenario-1",
|
|
657
|
+
},
|
|
658
|
+
id: "mock.setScenario",
|
|
659
|
+
idempotent: false,
|
|
660
|
+
mcp: {
|
|
661
|
+
kind: "unavailable",
|
|
662
|
+
reason: "The contract mock's control plane. An agent handed this would call it against the real service.",
|
|
663
|
+
},
|
|
664
|
+
method: "POST",
|
|
665
|
+
mutates: true,
|
|
666
|
+
path: "/_contract/scenarios",
|
|
667
|
+
principals: [],
|
|
668
|
+
problems: ["invalid_request"],
|
|
669
|
+
request: mockScenarioRequestSchema,
|
|
670
|
+
requestExample: { behavior: "retry", failuresBeforeSuccess: 2 },
|
|
671
|
+
response: mockScenarioResponseSchema,
|
|
672
|
+
scopes: [],
|
|
673
|
+
summary: "Set the mock scenario",
|
|
674
|
+
surfaces: ["mock"],
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
creates: true,
|
|
678
|
+
description: "Issues an API credential bound to one project and environment, with scopes capped by the issuing member's current role. The full secret is in this response and in no other, is not recoverable afterwards, and there is deliberately no reveal endpoint. The value is the sixteen-character display prefix followed immediately by thirty-two more characters; the example below is a visible placeholder rather than that shape, because a realistic credential in a committed file is one the repository's own secret scanner refuses.",
|
|
679
|
+
example: {
|
|
680
|
+
createdAt: "2026-07-28T15:00:00.000Z",
|
|
681
|
+
displayPrefix: "pt_test_a1b2c3d4",
|
|
682
|
+
environmentKind: "test",
|
|
683
|
+
id: "ak_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
684
|
+
project: "proj_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
685
|
+
scopes: ["schedules:write"],
|
|
686
|
+
secret: "pt_test_a1b2c3d4<the-32-character-secret-half-here>",
|
|
687
|
+
shownOnce: true,
|
|
688
|
+
},
|
|
689
|
+
id: "credentials.create",
|
|
690
|
+
idempotent: false,
|
|
691
|
+
// The one API-surface operation an agent cannot reach, and the reason is
|
|
692
|
+
// the route's own: issuance is authenticated by a Clerk session and refuses
|
|
693
|
+
// an API credential outright, because a key that can mint keys turns one
|
|
694
|
+
// disclosure into permanent access. MCP carries API credentials and OAuth
|
|
695
|
+
// tokens and never a session, so a tool here would be a tool that answers
|
|
696
|
+
// `authentication_required` to every caller. Publishing the exclusion is
|
|
697
|
+
// the honest version of that, and it is not MCP-only behaviour: it is what
|
|
698
|
+
// `POST /v1/credentials` already does to the same principals.
|
|
699
|
+
mcp: {
|
|
700
|
+
kind: "unavailable",
|
|
701
|
+
reason: "Issuance is authenticated by a signed-in member. MCP carries API credentials and OAuth tokens, and `POST /v1/credentials` refuses both.",
|
|
702
|
+
},
|
|
703
|
+
method: "POST",
|
|
704
|
+
mutates: true,
|
|
705
|
+
path: "/v1/credentials",
|
|
706
|
+
principals: ["session"],
|
|
707
|
+
// No `test_to_live_escalation`. It was declared here and is structurally
|
|
708
|
+
// unreachable: the issuer is a human session, whose authority tuple is
|
|
709
|
+
// *derived from* the environment it asked for, so the held kind and the
|
|
710
|
+
// requested kind are always equal and `decideEnvironmentIntent` has nothing
|
|
711
|
+
// to compare. An admin issuing a live credential is ordinary work, not an
|
|
712
|
+
// escalation. Publishing a refusal the implementation cannot produce is a
|
|
713
|
+
// contract that overstates what it enforces, which is worse than one that
|
|
714
|
+
// says less: a client writes a branch that never runs and believes it is
|
|
715
|
+
// protected. Escalation stays a real refusal wherever a *request* names an
|
|
716
|
+
// environment. This said "which is W2-02's create-in-live" until W2-02 was
|
|
717
|
+
// built and found no such thing: a schedule create takes its environment
|
|
718
|
+
// from the principal too, so no operation on this surface can yet have a
|
|
719
|
+
// held kind and a requested kind that differ. Whichever story first accepts
|
|
720
|
+
// an environment as *input* — a migration import is the likely one — is
|
|
721
|
+
// where the refusal becomes reachable and declarable.
|
|
722
|
+
problems: ["invalid_request", "not_found"],
|
|
723
|
+
request: credentialRequestSchema,
|
|
724
|
+
requestExample: {
|
|
725
|
+
environmentKind: "test",
|
|
726
|
+
project: "proj_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
727
|
+
scopes: ["schedules:write"],
|
|
728
|
+
},
|
|
729
|
+
response: credentialCreatedSchema,
|
|
730
|
+
scopes: ["credentials:write"],
|
|
731
|
+
sessionAuthority: "request-body",
|
|
732
|
+
summary: "Issue an API credential",
|
|
733
|
+
surfaces: ["api", "mock"],
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
creates: false,
|
|
737
|
+
description: "Lists the credentials issued for the caller's own project and environment, oldest first, one page at a time. The authority tuple comes from the verified principal, so there is no parameter that could name another project — a listing is the caller's own book or nothing. Revoked and rotated credentials stay in the listing and carry the fields that say so; a credential nobody can see is a credential nobody can audit. `limit` is bounded by the contract and `cursor` is the opaque value the previous page returned. No page carries a secret: the item is the read shape, which has no field one could be put in.",
|
|
738
|
+
example: {
|
|
739
|
+
data: [
|
|
740
|
+
{
|
|
741
|
+
createdAt: "2026-07-28T15:00:00.000Z",
|
|
742
|
+
displayPrefix: "pt_test_a1b2c3d4",
|
|
743
|
+
environmentKind: "test",
|
|
744
|
+
id: "ak_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
745
|
+
lastUsedAt: "2026-07-28T15:04:00.000Z",
|
|
746
|
+
project: "proj_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
747
|
+
scopes: ["schedules:write"],
|
|
748
|
+
},
|
|
749
|
+
],
|
|
750
|
+
// Authoritative rather than projected: credentials are read from the row
|
|
751
|
+
// that owns them, not from a cross-partition projection. The
|
|
752
|
+
// discriminator is on every page so a client never has to know which
|
|
753
|
+
// collections lag.
|
|
754
|
+
freshness: { kind: "authoritative" },
|
|
755
|
+
// Opaque. Clients pass back exactly what they were given, which is why
|
|
756
|
+
// this is the whole value rather than an elided one — a truncated example
|
|
757
|
+
// teaches a reader that a cursor has readable parts.
|
|
758
|
+
nextCursor: "eyJhIjoxNzg1MjUwODAwMDAwLCJpIjoiYWtfMDFoOHh5ejlrMm00bjZwOHIwdDJ2NHc2eTgiLCJrIjoiOTUxMzU0NmYifQ",
|
|
759
|
+
},
|
|
760
|
+
id: "credentials.list",
|
|
761
|
+
idempotent: true,
|
|
762
|
+
mcp: { destructive: false, kind: "tool", openWorld: false },
|
|
763
|
+
method: "GET",
|
|
764
|
+
mutates: false,
|
|
765
|
+
path: "/v1/credentials",
|
|
766
|
+
principals: ["api-credential", "oauth", "session"],
|
|
767
|
+
// `invalid_request` covers a limit outside the contract's bounds and a
|
|
768
|
+
// cursor this listing did not issue. There is no `not_found`: the
|
|
769
|
+
// collection is addressed by the principal, so it always exists and is
|
|
770
|
+
// sometimes empty.
|
|
771
|
+
problems: ["invalid_request"],
|
|
772
|
+
query: pageRequestSchema,
|
|
773
|
+
response: credentialPageSchema,
|
|
774
|
+
scopes: ["credentials:read"],
|
|
775
|
+
sessionAuthority: "headers",
|
|
776
|
+
summary: "List API credentials",
|
|
777
|
+
surfaces: ["api", "mock"],
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
creates: false,
|
|
781
|
+
description: "Lists the projects in the signed-in member's organization that they may act in, with the environment kinds each one has. This is what a browser asks before it can address anything else: an API credential and an OAuth token each carry one project intrinsically, but a member belongs to an organization and chooses. Archived and deleted projects are omitted, because a member cannot act in one. The result is a display aid rather than an authorization control — naming a project on a later request re-resolves it against the member's own organization, and a project that is not theirs answers exactly as one that does not exist.",
|
|
782
|
+
example: {
|
|
783
|
+
data: [
|
|
784
|
+
{
|
|
785
|
+
environmentKinds: ["test", "live"],
|
|
786
|
+
id: "proj_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
787
|
+
name: "Billing reminders",
|
|
788
|
+
},
|
|
789
|
+
],
|
|
790
|
+
},
|
|
791
|
+
id: "projects.list",
|
|
792
|
+
idempotent: true,
|
|
793
|
+
mcp: {
|
|
794
|
+
kind: "unavailable",
|
|
795
|
+
reason: "Enumerating an organization's projects is a signed-in member's question. MCP carries API credentials and OAuth tokens, whose authority already is one project, and this operation accepts neither.",
|
|
796
|
+
},
|
|
797
|
+
method: "GET",
|
|
798
|
+
mutates: false,
|
|
799
|
+
path: "/v1/projects",
|
|
800
|
+
principals: ["session"],
|
|
801
|
+
// No `not_found`: the collection is the member's own organization, so it
|
|
802
|
+
// always exists and is sometimes empty. No paging, so no `invalid_request`.
|
|
803
|
+
problems: [],
|
|
804
|
+
response: projectListSchema,
|
|
805
|
+
scopes: ["projects:read"],
|
|
806
|
+
sessionAuthority: "organization",
|
|
807
|
+
summary: "List the member's projects",
|
|
808
|
+
surfaces: ["api", "mock"],
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
creates: true,
|
|
812
|
+
description: "Creates a project in the signed-in member's organization, with both of its environments. This is the first call a new customer makes: an organization has no project until somebody makes one, and every other operation on this surface acts inside one. The `test` and `live` environments are provisioned with it in a single transaction rather than asked for, so a project is never observable in a half-provisioned state. The organization comes from the verified session and is never read from the request — a member who belongs to one organization cannot create a project in another. `PROJECT_LIST_LIMIT` bounds how many an organization may hold, and it is the same ceiling the listing publishes: a create that would take an organization past a list it can no longer return is refused rather than silently truncating a later read.",
|
|
813
|
+
example: {
|
|
814
|
+
environmentKinds: ["test", "live"],
|
|
815
|
+
id: "proj_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
816
|
+
name: "Billing reminders",
|
|
817
|
+
},
|
|
818
|
+
id: "projects.create",
|
|
819
|
+
// Not idempotent, and it cannot be made so here: repeating this makes a
|
|
820
|
+
// second project with the same name, because a name is a label rather than
|
|
821
|
+
// an identifier — `projectNameSchema` says so, and non-uniqueness is what
|
|
822
|
+
// lets somebody rename one without breaking a caller's URLs. No route on
|
|
823
|
+
// this Worker reads an idempotency key for it, so declaring `true` would
|
|
824
|
+
// publish a promise nothing keeps.
|
|
825
|
+
idempotent: false,
|
|
826
|
+
// Withheld from MCP for `projects.list`'s reason and one of its own. The
|
|
827
|
+
// shared half: this operation accepts only a signed-in member, and MCP
|
|
828
|
+
// carries API credentials and OAuth tokens. The half that is this
|
|
829
|
+
// operation's alone: a credential's authority *is* one project, so an agent
|
|
830
|
+
// that could create a second could not then act in it — the tool would
|
|
831
|
+
// succeed and leave the caller holding an identifier nothing they carry can
|
|
832
|
+
// address.
|
|
833
|
+
mcp: {
|
|
834
|
+
kind: "unavailable",
|
|
835
|
+
reason: "Creating a project is a signed-in member's act. MCP carries API credentials and OAuth tokens, whose authority already is one project — so this operation accepts neither, and an agent that somehow created one could not act in it.",
|
|
836
|
+
},
|
|
837
|
+
method: "POST",
|
|
838
|
+
mutates: true,
|
|
839
|
+
path: "/v1/projects",
|
|
840
|
+
principals: ["session"],
|
|
841
|
+
// `invalid_request` covers a blank or over-long name. `plan_limit_reached`
|
|
842
|
+
// is the organization ceiling, the same code `endpoints.create` uses for
|
|
843
|
+
// the same shape of refusal. There is no `not_found`: the organization is
|
|
844
|
+
// the member's own and is created on first use, so it always exists by the
|
|
845
|
+
// time the project is written.
|
|
846
|
+
problems: ["invalid_request", "plan_limit_reached"],
|
|
847
|
+
request: projectCreateRequestSchema,
|
|
848
|
+
requestExample: { name: "Billing reminders" },
|
|
849
|
+
response: projectSummarySchema,
|
|
850
|
+
scopes: ["projects:write"],
|
|
851
|
+
sessionAuthority: "organization",
|
|
852
|
+
summary: "Create a project",
|
|
853
|
+
surfaces: ["api", "mock"],
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
creates: false,
|
|
857
|
+
description: "Lists the applications this member has connected to their own account, newest first, across every project in their organization — because a person connects an application, not a project. Revoked connections stay in the listing with the instant and the reason they ended: a connection nobody can see is one nobody can audit. This is a signed-in member's view of their own delegations. An application holding a grant cannot read it, which is why the operation accepts no API credential and no access token, and why it is on no agent's tool list.",
|
|
858
|
+
example: {
|
|
859
|
+
data: [
|
|
860
|
+
{
|
|
861
|
+
client: { id: "oc_01h8xyz9k2m4n6p8r0t2v4w6y8", name: "Flow XO" },
|
|
862
|
+
createdAt: "2026-08-14T11:00:00.000Z",
|
|
863
|
+
environmentKind: "test",
|
|
864
|
+
id: "og_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
865
|
+
lastUsedAt: "2026-08-15T09:12:00.000Z",
|
|
866
|
+
project: "proj_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
867
|
+
scopes: ["schedules:read", "schedules:write"],
|
|
868
|
+
status: "active",
|
|
869
|
+
},
|
|
870
|
+
],
|
|
871
|
+
},
|
|
872
|
+
id: "grants.list",
|
|
873
|
+
idempotent: true,
|
|
874
|
+
// **Withheld, and the reason is what the surface is for.** An application
|
|
875
|
+
// authenticates to MCP with the very grant this operation enumerates. A
|
|
876
|
+
// tool here would let one connected application read every other
|
|
877
|
+
// application the member has connected, and the revoke beside it would let
|
|
878
|
+
// it disconnect them. That is not a narrower scope's problem — it is why
|
|
879
|
+
// the operation accepts only a session.
|
|
880
|
+
mcp: {
|
|
881
|
+
kind: "unavailable",
|
|
882
|
+
reason: "A member's own connected applications. MCP carries API credentials and OAuth tokens, and an application holding a grant must not be able to enumerate its rivals — still less disconnect them — so this operation accepts neither.",
|
|
883
|
+
},
|
|
884
|
+
method: "GET",
|
|
885
|
+
mutates: false,
|
|
886
|
+
path: "/v1/grants",
|
|
887
|
+
principals: ["session"],
|
|
888
|
+
// No paging and no `not_found`: the collection is the member's own and is
|
|
889
|
+
// sometimes empty. Its size is how many applications one person has
|
|
890
|
+
// connected, which is not a number a cursor helps with.
|
|
891
|
+
problems: [],
|
|
892
|
+
response: oauthGrantListSchema,
|
|
893
|
+
scopes: ["grants:read"],
|
|
894
|
+
sessionAuthority: "member",
|
|
895
|
+
summary: "List the member's connected applications",
|
|
896
|
+
surfaces: ["api", "mock"],
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
creates: false,
|
|
900
|
+
description: "Disconnects one application, ending every token that grant ever issued — the access tokens and the refresh token together. The grant stays readable afterwards with the instant and the reason it ended. Only the member who granted it can disconnect it, and a grant belonging to somebody else answers exactly as one that does not exist.",
|
|
901
|
+
example: {
|
|
902
|
+
grant: {
|
|
903
|
+
client: { id: "oc_01h8xyz9k2m4n6p8r0t2v4w6y8", name: "Flow XO" },
|
|
904
|
+
createdAt: "2026-08-14T11:00:00.000Z",
|
|
905
|
+
environmentKind: "test",
|
|
906
|
+
id: "og_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
907
|
+
project: "proj_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
908
|
+
revocationReason: "member-revoked",
|
|
909
|
+
revokedAt: "2026-08-15T10:00:00.000Z",
|
|
910
|
+
scopes: ["schedules:read", "schedules:write"],
|
|
911
|
+
status: "revoked",
|
|
912
|
+
},
|
|
913
|
+
},
|
|
914
|
+
id: "grants.revoke",
|
|
915
|
+
// Repeating it is safe: the grant is already revoked, and the second call
|
|
916
|
+
// reports the first one's instant rather than moving it. That is
|
|
917
|
+
// `credentials.revoke`'s shape, for the same reason.
|
|
918
|
+
idempotent: true,
|
|
919
|
+
mcp: {
|
|
920
|
+
kind: "unavailable",
|
|
921
|
+
reason: "Disconnecting an application is the member's own act. MCP carries API credentials and OAuth tokens, and an application able to disconnect a grant could disconnect the ones belonging to every other application the member has connected.",
|
|
922
|
+
},
|
|
923
|
+
method: "DELETE",
|
|
924
|
+
mutates: true,
|
|
925
|
+
path: "/v1/grants/{grantId}",
|
|
926
|
+
principals: ["session"],
|
|
927
|
+
problems: ["not_found"],
|
|
928
|
+
response: oauthGrantRevokedSchema,
|
|
929
|
+
scopes: ["grants:write"],
|
|
930
|
+
sessionAuthority: "member",
|
|
931
|
+
summary: "Disconnect an application",
|
|
932
|
+
surfaces: ["api", "mock"],
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
creates: true,
|
|
936
|
+
description: "Replaces a credential with a new one and gives the original a deadline, so consumers can pick up the new secret before the old stops working. The replacement is a new credential with its own identifier and the same authority and scopes; the original keeps working until it expires, then refuses with `credential_expired` rather than `credential_revoked`. `overlapSeconds` may only shorten the window — the default is also the maximum — and zero retires the original immediately. The new secret is in this response and in no other.",
|
|
937
|
+
example: {
|
|
938
|
+
credential: {
|
|
939
|
+
createdAt: "2026-07-28T15:00:00.000Z",
|
|
940
|
+
displayPrefix: "pt_test_e5f6g7h8",
|
|
941
|
+
environmentKind: "test",
|
|
942
|
+
id: "ak_01h8xyz9k2m4n6p8r0t2v4w6y9",
|
|
943
|
+
project: "proj_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
944
|
+
scopes: ["schedules:write"],
|
|
945
|
+
secret: "pt_test_e5f6g7h8<the-32-character-secret-half-here>",
|
|
946
|
+
shownOnce: true,
|
|
947
|
+
},
|
|
948
|
+
retired: {
|
|
949
|
+
createdAt: "2026-07-28T15:00:00.000Z",
|
|
950
|
+
displayPrefix: "pt_test_a1b2c3d4",
|
|
951
|
+
environmentKind: "test",
|
|
952
|
+
expiresAt: "2026-07-29T15:00:00.000Z",
|
|
953
|
+
id: "ak_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
954
|
+
project: "proj_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
955
|
+
scopes: ["schedules:write"],
|
|
956
|
+
supersededBy: "ak_01h8xyz9k2m4n6p8r0t2v4w6y9",
|
|
957
|
+
},
|
|
958
|
+
},
|
|
959
|
+
id: "credentials.rotate",
|
|
960
|
+
// Destructive: the original credential is given a deadline it did not have,
|
|
961
|
+
// and whatever is still using it stops working when that deadline passes.
|
|
962
|
+
// The new secret is in the result, which is architecture section 9's "secrets
|
|
963
|
+
// are one-time results" — it is in this response and in no other, and no MCP
|
|
964
|
+
// read can recover it afterwards.
|
|
965
|
+
mcp: { destructive: true, kind: "tool", openWorld: false },
|
|
966
|
+
// Not idempotent, and it cannot be made so here: repeating this mints a
|
|
967
|
+
// second replacement and retires the first. `operations.ts` says a write
|
|
968
|
+
// qualifies only by carrying an idempotency key, and no route on this
|
|
969
|
+
// Worker reads one yet — declaring `true` would publish a promise nothing
|
|
970
|
+
// keeps.
|
|
971
|
+
idempotent: false,
|
|
972
|
+
method: "POST",
|
|
973
|
+
mutates: true,
|
|
974
|
+
path: "/v1/credentials/{credentialId}/rotate",
|
|
975
|
+
conditionalScopes: ["credentials:administer"],
|
|
976
|
+
principals: ["api-credential", "oauth", "session"],
|
|
977
|
+
problems: ["invalid_request", "not_found", "scope_missing"],
|
|
978
|
+
request: credentialRotationRequestSchema,
|
|
979
|
+
requestExample: { overlapSeconds: 3600 },
|
|
980
|
+
response: credentialRotatedSchema,
|
|
981
|
+
scopes: ["credentials:write"],
|
|
982
|
+
sessionAuthority: "headers",
|
|
983
|
+
summary: "Rotate an API credential",
|
|
984
|
+
surfaces: ["api", "mock"],
|
|
985
|
+
},
|
|
986
|
+
{
|
|
987
|
+
creates: false,
|
|
988
|
+
description: "Revokes a credential immediately, with no overlap. The credential stops verifying on the next request and refuses with `credential_revoked`. The row survives its revocation so the credential's history remains answerable, and revoking a credential that is already revoked reports the original revocation rather than moving it. A credential belonging to another project is indistinguishable from one that does not exist.",
|
|
989
|
+
example: {
|
|
990
|
+
credential: {
|
|
991
|
+
createdAt: "2026-07-28T15:00:00.000Z",
|
|
992
|
+
displayPrefix: "pt_test_a1b2c3d4",
|
|
993
|
+
environmentKind: "test",
|
|
994
|
+
id: "ak_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
995
|
+
project: "proj_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
996
|
+
revokedAt: "2026-07-28T16:00:00.000Z",
|
|
997
|
+
scopes: ["schedules:write"],
|
|
998
|
+
},
|
|
999
|
+
},
|
|
1000
|
+
id: "credentials.revoke",
|
|
1001
|
+
mcp: { destructive: true, kind: "tool", openWorld: false },
|
|
1002
|
+
// Repeating this leaves the same credential revoked at the same instant, so
|
|
1003
|
+
// it is idempotent in the sense the field means — no key required, because
|
|
1004
|
+
// there is no second effect to suppress.
|
|
1005
|
+
idempotent: true,
|
|
1006
|
+
method: "DELETE",
|
|
1007
|
+
mutates: true,
|
|
1008
|
+
path: "/v1/credentials/{credentialId}",
|
|
1009
|
+
conditionalScopes: ["credentials:administer"],
|
|
1010
|
+
principals: ["api-credential", "oauth", "session"],
|
|
1011
|
+
problems: ["invalid_request", "not_found", "scope_missing"],
|
|
1012
|
+
response: credentialRevokedSchema,
|
|
1013
|
+
scopes: ["credentials:write"],
|
|
1014
|
+
sessionAuthority: "headers",
|
|
1015
|
+
summary: "Revoke an API credential",
|
|
1016
|
+
surfaces: ["api", "mock"],
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
creates: true,
|
|
1020
|
+
description: "Creates a schedule and returns it together with its first planned occurrence. Give exactly one of `at`, `delaySeconds`, `everySeconds` and `cron`. An optional `partitionKey` selects a fairness lane inside this project and environment; it is fixed after create. Add a verified `endpoint` to deliver each occurrence there; optional JSON `data` is encrypted before PostgreSQL persistence and is placed in the signed callback, never returned by schedule reads. `at` and `delaySeconds` fire once; a delay is converted to an absolute instant when the request is accepted, so a schedule created either way reads back the same afterwards. `everySeconds` is a fixed duration and its first occurrence falls one interval after acceptance — it stays the same length across a daylight-saving change, and takes no timezone for that reason. `cron` is a wall clock: it is read against the IANA `timeZone` sent with it, `UTC` when none is, and the first occurrence is the first instant the expression names after acceptance. Where the clocks go forward and the wall time named does not exist that day, the occurrence falls at the first instant after the gap; where they go back and it happens twice, it falls once, at the earlier of the two. A recurring schedule is materialized one occurrence at a time: the next one is planned once the current one is due, so a read always shows one occurrence still to come. The organization, project and environment come from the verified principal; a body cannot name them, and the endpoint must belong to the same tuple, so a credential issued for one project can only ever create schedules in that project. Send an `Idempotency-Key` to make a retry safe: the identical request returns the schedule that key already created, marked `Idempotency-Replayed: true`, and creates nothing further; the same key with a different request is refused `idempotency_key_reused`. A replay reports the schedule **as it stands now**, not a frozen copy of the first response — if it was paused in between, the replay says so.",
|
|
1021
|
+
example: {
|
|
1022
|
+
occurrence: {
|
|
1023
|
+
attemptCount: 0,
|
|
1024
|
+
generation: 1,
|
|
1025
|
+
id: "occ_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1026
|
+
plannedFor: "2026-07-28T15:00:00.000Z",
|
|
1027
|
+
schedule: "sch_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1028
|
+
sequence: 1,
|
|
1029
|
+
state: "planned",
|
|
1030
|
+
},
|
|
1031
|
+
schedule: {
|
|
1032
|
+
createdAt: "2026-07-28T14:59:00.000Z",
|
|
1033
|
+
endpoint: "ep_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1034
|
+
generation: 1,
|
|
1035
|
+
id: "sch_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1036
|
+
latencyClass: "realtime",
|
|
1037
|
+
partitionKey: "campaign.spring",
|
|
1038
|
+
recurrence: { kind: "at" },
|
|
1039
|
+
status: "active",
|
|
1040
|
+
updatedAt: "2026-07-28T14:59:00.000Z",
|
|
1041
|
+
version: 1,
|
|
1042
|
+
},
|
|
1043
|
+
},
|
|
1044
|
+
id: "schedules.create",
|
|
1045
|
+
// Additive: it brings a schedule into existence and takes nothing away.
|
|
1046
|
+
// A callback schedule causes a future request to the verified external
|
|
1047
|
+
// endpoint. It does not destructively modify that system, but it does act
|
|
1048
|
+
// beyond Greatwait, so MCP clients must receive the open-world hint.
|
|
1049
|
+
mcp: { destructive: false, kind: "tool", openWorld: true },
|
|
1050
|
+
// False even though this operation reads `Idempotency-Key`, because the
|
|
1051
|
+
// header is optional and this flag is what a client reads before deciding
|
|
1052
|
+
// whether a blind retry is safe. It is not: a retry without the key creates
|
|
1053
|
+
// a second schedule. What says the protection is available is
|
|
1054
|
+
// `requestHeaders`, which publishes the key rather than promising it was
|
|
1055
|
+
// used.
|
|
1056
|
+
idempotent: false,
|
|
1057
|
+
method: "POST",
|
|
1058
|
+
mutates: true,
|
|
1059
|
+
path: "/v1/schedules",
|
|
1060
|
+
principals: ["api-credential", "oauth", "session"],
|
|
1061
|
+
problems: [
|
|
1062
|
+
// **Reachable in every deployment, and undeclared until W9-03.** The
|
|
1063
|
+
// route refuses here when the endpoint's status is not one this
|
|
1064
|
+
// deployment will send to — under the default `not-required` ownership
|
|
1065
|
+
// policy that still covers a `disabled` destination, and under
|
|
1066
|
+
// `required` it covers a `pending` one as well. An operation that can
|
|
1067
|
+
// refuse for a code it never mentions is a document that lies, and a
|
|
1068
|
+
// published quickstart could not tell a reader what to do about a
|
|
1069
|
+
// refusal the contract said was impossible.
|
|
1070
|
+
"endpoint_unverified",
|
|
1071
|
+
"idempotency_key_reused",
|
|
1072
|
+
"invalid_request",
|
|
1073
|
+
"not_found",
|
|
1074
|
+
// The organization ceiling, the same code and the same shape
|
|
1075
|
+
// `projects.create` and `endpoints.create` use. A plan may bound the
|
|
1076
|
+
// live schedules an organization holds, the distinct partition keys it
|
|
1077
|
+
// has live, and how large a `data` body it may carry; all three refuse
|
|
1078
|
+
// here rather than truncating, because a schedule created without the
|
|
1079
|
+
// body the caller sent is not the schedule they asked for.
|
|
1080
|
+
"plan_limit_reached",
|
|
1081
|
+
"service_unavailable",
|
|
1082
|
+
],
|
|
1083
|
+
request: scheduleRequestSchema,
|
|
1084
|
+
requestExample: {
|
|
1085
|
+
cron: "0 9 * * 1-5",
|
|
1086
|
+
data: { report: "daily" },
|
|
1087
|
+
endpoint: "ep_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1088
|
+
partitionKey: "campaign.spring",
|
|
1089
|
+
timeZone: "Europe/London",
|
|
1090
|
+
},
|
|
1091
|
+
requestHeaders: idempotencyRequestHeaderSchema,
|
|
1092
|
+
response: scheduleViewSchema,
|
|
1093
|
+
responseHeaders: replayedResponseHeaderSchema,
|
|
1094
|
+
scopes: ["schedules:write"],
|
|
1095
|
+
sessionAuthority: "headers",
|
|
1096
|
+
summary: "Create a schedule",
|
|
1097
|
+
surfaces: ["api", "mock"],
|
|
1098
|
+
},
|
|
1099
|
+
{
|
|
1100
|
+
creates: false,
|
|
1101
|
+
description: "Reads one schedule and its next occurrence from the partition that owns it. A schedule belonging to another organization, project or environment answers not_found rather than a refusal that would confirm it exists. The response carries the schedule's version as a strong ETag; send it back in `If-Match` to make a later write conditional on nothing having changed in between.",
|
|
1102
|
+
example: {
|
|
1103
|
+
occurrence: {
|
|
1104
|
+
attemptCount: 0,
|
|
1105
|
+
generation: 1,
|
|
1106
|
+
id: "occ_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1107
|
+
plannedFor: "2026-07-28T15:00:00.000Z",
|
|
1108
|
+
schedule: "sch_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1109
|
+
sequence: 1,
|
|
1110
|
+
state: "planned",
|
|
1111
|
+
},
|
|
1112
|
+
schedule: {
|
|
1113
|
+
createdAt: "2026-07-28T14:59:00.000Z",
|
|
1114
|
+
endpoint: "ep_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1115
|
+
generation: 1,
|
|
1116
|
+
id: "sch_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1117
|
+
latencyClass: "realtime",
|
|
1118
|
+
partitionKey: "campaign.spring",
|
|
1119
|
+
recurrence: { kind: "at" },
|
|
1120
|
+
status: "active",
|
|
1121
|
+
updatedAt: "2026-07-28T14:59:00.000Z",
|
|
1122
|
+
version: 1,
|
|
1123
|
+
},
|
|
1124
|
+
},
|
|
1125
|
+
id: "schedules.read",
|
|
1126
|
+
idempotent: true,
|
|
1127
|
+
mcp: { destructive: false, kind: "tool", openWorld: false },
|
|
1128
|
+
method: "GET",
|
|
1129
|
+
mutates: false,
|
|
1130
|
+
path: "/v1/schedules/{scheduleId}",
|
|
1131
|
+
principals: ["api-credential", "oauth", "session"],
|
|
1132
|
+
problems: ["invalid_request", "not_found"],
|
|
1133
|
+
response: scheduleViewSchema,
|
|
1134
|
+
responseHeaders: versionResponseHeaderSchema,
|
|
1135
|
+
scopes: ["schedules:read"],
|
|
1136
|
+
sessionAuthority: "headers",
|
|
1137
|
+
summary: "Read a schedule",
|
|
1138
|
+
surfaces: ["api", "mock"],
|
|
1139
|
+
},
|
|
1140
|
+
{
|
|
1141
|
+
creates: false,
|
|
1142
|
+
description: "Lists the caller's own schedules, oldest first, one page at a time. Archived schedules are left out: archiving is how a schedule leaves the working set, and a listing that still carried them would make the operation pointless. Items are schedules rather than schedules-with-occurrences, because a cross-partition listing is served from a projection that holds a summary; read a schedule to get its authoritative next occurrence. Every page states whether it is authoritative or projected.",
|
|
1143
|
+
example: {
|
|
1144
|
+
data: [
|
|
1145
|
+
{
|
|
1146
|
+
createdAt: "2026-07-28T14:59:00.000Z",
|
|
1147
|
+
endpoint: "ep_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1148
|
+
generation: 1,
|
|
1149
|
+
id: "sch_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1150
|
+
latencyClass: "realtime",
|
|
1151
|
+
recurrence: { kind: "at" },
|
|
1152
|
+
status: "active",
|
|
1153
|
+
updatedAt: "2026-07-28T14:59:00.000Z",
|
|
1154
|
+
version: 1,
|
|
1155
|
+
},
|
|
1156
|
+
],
|
|
1157
|
+
// `projected` since W2-06 served this listing from the control-plane
|
|
1158
|
+
// directory. `asOf` is the instant the projection was last known to hold
|
|
1159
|
+
// everything every partition had handed over, and `lagSeconds` is how long
|
|
1160
|
+
// ago that was — so a page states how far behind it may be rather than
|
|
1161
|
+
// implying it is current.
|
|
1162
|
+
freshness: {
|
|
1163
|
+
asOf: "2026-07-28T15:00:00.000Z",
|
|
1164
|
+
kind: "projected",
|
|
1165
|
+
lagSeconds: 0,
|
|
1166
|
+
},
|
|
1167
|
+
nextCursor: "eyJhIjoxNzg1MjUwODAwMDAwLCJpIjoic2NoXzAxaDh4eXo5azJtNG42cDhyMHQydjR3Nnk4IiwiayI6Ijk1MTM1NDZmIn0",
|
|
1168
|
+
},
|
|
1169
|
+
id: "schedules.list",
|
|
1170
|
+
idempotent: true,
|
|
1171
|
+
mcp: { destructive: false, kind: "tool", openWorld: false },
|
|
1172
|
+
method: "GET",
|
|
1173
|
+
mutates: false,
|
|
1174
|
+
path: "/v1/schedules",
|
|
1175
|
+
principals: ["api-credential", "oauth", "session"],
|
|
1176
|
+
problems: ["invalid_request"],
|
|
1177
|
+
query: pageRequestSchema,
|
|
1178
|
+
response: schedulePageSchema,
|
|
1179
|
+
scopes: ["schedules:read"],
|
|
1180
|
+
sessionAuthority: "headers",
|
|
1181
|
+
summary: "List schedules",
|
|
1182
|
+
surfaces: ["api", "mock"],
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
creates: false,
|
|
1186
|
+
description: "Counts the occurrences this project and environment expect to fire, in epoch-aligned time buckets, with a latency-class split on every bucket and an optional customer partition-key breakdown. A recurring series contributes every instant it fires inside the window rather than only its next one, and `nextFireCount` reports how many of a bucket's occurrences are a schedule's next firing. The answer states its own freshness and is refused rather than shortened when a partition cannot complete it. It carries counts only: no schedule identifier, callback destination, payload or endpoint detail has a field in the response.",
|
|
1187
|
+
example: {
|
|
1188
|
+
buckets: [
|
|
1189
|
+
{
|
|
1190
|
+
byLatencyClass: { bulk: 12, realtime: 3 },
|
|
1191
|
+
byPartitionKey: { "campaign.spring": 9, "campaign.winter": 4 },
|
|
1192
|
+
count: 15,
|
|
1193
|
+
nextFireCount: 4,
|
|
1194
|
+
otherPartitionKeyCount: 2,
|
|
1195
|
+
start: "2026-07-28T15:00:00.000Z",
|
|
1196
|
+
},
|
|
1197
|
+
],
|
|
1198
|
+
freshness: {
|
|
1199
|
+
asOf: "2026-07-28T14:58:30.000Z",
|
|
1200
|
+
kind: "projected",
|
|
1201
|
+
lagSeconds: 90,
|
|
1202
|
+
},
|
|
1203
|
+
window: {
|
|
1204
|
+
bucketSeconds: 1800,
|
|
1205
|
+
from: "2026-07-28T15:00:00.000Z",
|
|
1206
|
+
to: "2026-07-29T15:00:00.000Z",
|
|
1207
|
+
},
|
|
1208
|
+
},
|
|
1209
|
+
id: "forecast.read",
|
|
1210
|
+
idempotent: true,
|
|
1211
|
+
mcp: { destructive: false, kind: "tool", openWorld: false },
|
|
1212
|
+
method: "GET",
|
|
1213
|
+
mutates: false,
|
|
1214
|
+
path: "/v1/forecast",
|
|
1215
|
+
principals: ["api-credential", "oauth", "session"],
|
|
1216
|
+
problems: ["invalid_request"],
|
|
1217
|
+
query: forecastRequestSchema,
|
|
1218
|
+
response: forecastSchema,
|
|
1219
|
+
scopes: ["forecast:read"],
|
|
1220
|
+
sessionAuthority: "headers",
|
|
1221
|
+
summary: "Forecast upcoming occurrences",
|
|
1222
|
+
surfaces: ["api", "mock"],
|
|
1223
|
+
},
|
|
1224
|
+
{
|
|
1225
|
+
creates: false,
|
|
1226
|
+
description: "What this project was charged for, by UTC day, from the authoritative scheduling model. One terminal occurrence is one billable unit however many attempts it took, so a schedule that failed four times and succeeded on the fifth appears here once; the attempts it cost are on that occurrence's own attempt history. Days with no billable occurrence are absent rather than zero, and the window states which days the answer covers. `from` and `to` are snapped outward to UTC midnight, so an answer always covers whole days.",
|
|
1227
|
+
example: {
|
|
1228
|
+
days: [
|
|
1229
|
+
{
|
|
1230
|
+
billableOccurrences: 42,
|
|
1231
|
+
byTerminalState: { succeeded: 40, terminal_failed: 2 },
|
|
1232
|
+
start: "2026-07-28T00:00:00.000Z",
|
|
1233
|
+
},
|
|
1234
|
+
],
|
|
1235
|
+
freshness: { kind: "authoritative" },
|
|
1236
|
+
totalBillableOccurrences: 42,
|
|
1237
|
+
window: {
|
|
1238
|
+
from: "2026-07-28T00:00:00.000Z",
|
|
1239
|
+
to: "2026-07-29T00:00:00.000Z",
|
|
1240
|
+
},
|
|
1241
|
+
},
|
|
1242
|
+
id: "usage.read",
|
|
1243
|
+
idempotent: true,
|
|
1244
|
+
mcp: { destructive: false, kind: "tool", openWorld: false },
|
|
1245
|
+
method: "GET",
|
|
1246
|
+
mutates: false,
|
|
1247
|
+
path: "/v1/usage",
|
|
1248
|
+
principals: ["api-credential", "oauth", "session"],
|
|
1249
|
+
problems: ["invalid_request"],
|
|
1250
|
+
query: usageRequestSchema,
|
|
1251
|
+
response: usageSchema,
|
|
1252
|
+
scopes: ["usage:read"],
|
|
1253
|
+
sessionAuthority: "headers",
|
|
1254
|
+
summary: "Read billable usage by day",
|
|
1255
|
+
surfaces: ["api", "mock"],
|
|
1256
|
+
},
|
|
1257
|
+
{
|
|
1258
|
+
creates: false,
|
|
1259
|
+
description: "The same forecast in the window shape the legacy scheduler's autoscaler already parses, published under a dated version so a consumer can pin it. It exists for one migration and is expected to be withdrawn once that migration completes; `forecast.read` is the shape this product publishes. `byTenant` is the customer partition-key breakdown under its legacy name, and `count` is always the bucket total rather than the named subtotal.",
|
|
1260
|
+
example: {
|
|
1261
|
+
bucketSeconds: 600,
|
|
1262
|
+
buckets: [
|
|
1263
|
+
{
|
|
1264
|
+
byTenant: { "campaign.spring": 9, "campaign.winter": 4 },
|
|
1265
|
+
count: 15,
|
|
1266
|
+
nextFireCount: 4,
|
|
1267
|
+
otherCount: 2,
|
|
1268
|
+
start: "2026-07-28T15:00:00.000Z",
|
|
1269
|
+
},
|
|
1270
|
+
],
|
|
1271
|
+
freshness: {
|
|
1272
|
+
asOf: "2026-07-28T14:58:30.000Z",
|
|
1273
|
+
kind: "projected",
|
|
1274
|
+
lagSeconds: 90,
|
|
1275
|
+
},
|
|
1276
|
+
version: FORECAST_WINDOW_VERSION,
|
|
1277
|
+
},
|
|
1278
|
+
id: "forecast.windows",
|
|
1279
|
+
idempotent: true,
|
|
1280
|
+
// Withheld from MCP rather than published as a tool. It is a compatibility
|
|
1281
|
+
// projection for one migrating consumer, and `forecast.read` is the same
|
|
1282
|
+
// data in the shape this product intends to keep — so putting it in an
|
|
1283
|
+
// agent's tool list would invite an agent to build against a shape that is
|
|
1284
|
+
// scheduled for removal, which is the opposite of what a tool list is for.
|
|
1285
|
+
mcp: {
|
|
1286
|
+
kind: "unavailable",
|
|
1287
|
+
reason: "A transitional shape for the autoscaler migration, scheduled for withdrawal once W6-02 has cut over. forecast.read is the same data in the shape this product publishes, and it is the tool an agent should reach for.",
|
|
1288
|
+
},
|
|
1289
|
+
method: "GET",
|
|
1290
|
+
mutates: false,
|
|
1291
|
+
path: "/v1/forecast/windows",
|
|
1292
|
+
principals: ["api-credential", "oauth", "session"],
|
|
1293
|
+
problems: ["invalid_request"],
|
|
1294
|
+
query: forecastRequestSchema,
|
|
1295
|
+
response: forecastWindowsSchema,
|
|
1296
|
+
scopes: ["forecast:read"],
|
|
1297
|
+
sessionAuthority: "headers",
|
|
1298
|
+
summary: "Forecast in the transitional window shape",
|
|
1299
|
+
surfaces: ["api", "mock"],
|
|
1300
|
+
},
|
|
1301
|
+
{
|
|
1302
|
+
creates: false,
|
|
1303
|
+
description: "Lists the retained occurrences of one schedule from its authoritative Scheduler Partition, ordered by durable creation instant and occurrence identifier so tied timestamps never duplicate or omit a row. The cursor is opaque and scoped to the verified principal's organization, project and environment. Each item is metadata only: callback payloads, destinations, response bodies and signing material have no field in the response.",
|
|
1304
|
+
example: {
|
|
1305
|
+
data: [
|
|
1306
|
+
{
|
|
1307
|
+
attemptCount: 1,
|
|
1308
|
+
generation: 1,
|
|
1309
|
+
id: "occ_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1310
|
+
plannedFor: "2026-07-28T15:00:00.000Z",
|
|
1311
|
+
schedule: "sch_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1312
|
+
sequence: 1,
|
|
1313
|
+
state: "retry_wait",
|
|
1314
|
+
},
|
|
1315
|
+
],
|
|
1316
|
+
freshness: { kind: "authoritative" },
|
|
1317
|
+
nextCursor: "eyJhIjoxNzg1MjUwODAwMDAwLCJpIjoib2NjXzAxaDh4eXo5azJtNG42cDhyMHQydjR3Nnk4IiwiayI6Ijk1MTM1NDZmIn0",
|
|
1318
|
+
},
|
|
1319
|
+
id: "occurrences.list",
|
|
1320
|
+
idempotent: true,
|
|
1321
|
+
mcp: { destructive: false, kind: "tool", openWorld: false },
|
|
1322
|
+
method: "GET",
|
|
1323
|
+
mutates: false,
|
|
1324
|
+
path: "/v1/schedules/{scheduleId}/occurrences",
|
|
1325
|
+
principals: ["api-credential", "oauth", "session"],
|
|
1326
|
+
problems: ["invalid_request", "not_found"],
|
|
1327
|
+
query: pageRequestSchema,
|
|
1328
|
+
response: occurrencePageSchema,
|
|
1329
|
+
scopes: ["occurrences:read"],
|
|
1330
|
+
sessionAuthority: "headers",
|
|
1331
|
+
summary: "List retained occurrences",
|
|
1332
|
+
surfaces: ["api", "mock"],
|
|
1333
|
+
},
|
|
1334
|
+
{
|
|
1335
|
+
creates: false,
|
|
1336
|
+
description: "Reads one retained occurrence from the Scheduler Partition that owns its schedule. The response explicitly states that it is authoritative; a stale D1 directory or schedule projection is used only to find the partition and never as occurrence truth. An identifier outside the verified principal's organization, project or environment is indistinguishable from an absent occurrence.",
|
|
1337
|
+
example: {
|
|
1338
|
+
freshness: { kind: "authoritative" },
|
|
1339
|
+
occurrence: {
|
|
1340
|
+
attemptCount: 2,
|
|
1341
|
+
generation: 1,
|
|
1342
|
+
id: "occ_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1343
|
+
plannedFor: "2026-07-28T15:00:00.000Z",
|
|
1344
|
+
schedule: "sch_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1345
|
+
sequence: 1,
|
|
1346
|
+
settledAt: "2026-07-28T15:02:00.000Z",
|
|
1347
|
+
state: "terminal_failed",
|
|
1348
|
+
},
|
|
1349
|
+
},
|
|
1350
|
+
id: "occurrences.read",
|
|
1351
|
+
idempotent: true,
|
|
1352
|
+
mcp: { destructive: false, kind: "tool", openWorld: false },
|
|
1353
|
+
method: "GET",
|
|
1354
|
+
mutates: false,
|
|
1355
|
+
path: "/v1/schedules/{scheduleId}/occurrences/{occurrenceId}",
|
|
1356
|
+
principals: ["api-credential", "oauth", "session"],
|
|
1357
|
+
problems: ["invalid_request", "not_found"],
|
|
1358
|
+
response: occurrenceViewSchema,
|
|
1359
|
+
scopes: ["occurrences:read"],
|
|
1360
|
+
sessionAuthority: "headers",
|
|
1361
|
+
summary: "Read an occurrence",
|
|
1362
|
+
surfaces: ["api", "mock"],
|
|
1363
|
+
},
|
|
1364
|
+
{
|
|
1365
|
+
creates: false,
|
|
1366
|
+
description: "Lists the immutable attempts retained under one occurrence, ordered by start instant and attempt identifier so equal timestamps have a stable position. The owning Scheduler Partition is authoritative and every page says so. Results expose only bounded outcome, response-status, duration, diagnostic, remediation and redelivery-cause metadata; callback payloads, response bodies, destinations, signing material, secrets and transport traces are structurally absent.",
|
|
1367
|
+
example: {
|
|
1368
|
+
data: [
|
|
1369
|
+
{
|
|
1370
|
+
attemptNumber: 1,
|
|
1371
|
+
classifiedAt: "2026-07-28T15:00:00.071Z",
|
|
1372
|
+
delivery: "dlv_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1373
|
+
diagnostic: "http_retryable",
|
|
1374
|
+
durationMs: 71,
|
|
1375
|
+
id: "att_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1376
|
+
occurrence: "occ_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1377
|
+
remediation: "review_retry_policy",
|
|
1378
|
+
responseStatus: 503,
|
|
1379
|
+
startedAt: "2026-07-28T15:00:00.000Z",
|
|
1380
|
+
state: "retryable_failure",
|
|
1381
|
+
trigger: { kind: "automatic" },
|
|
1382
|
+
},
|
|
1383
|
+
],
|
|
1384
|
+
freshness: { kind: "authoritative" },
|
|
1385
|
+
nextCursor: "eyJhIjoxNzg1MjUwODAwMDAwLCJpIjoiYXR0XzAxaDh4eXo5azJtNG42cDhyMHQydjR3Nnk4IiwiayI6Ijk1MTM1NDZmIn0",
|
|
1386
|
+
},
|
|
1387
|
+
id: "attempts.list",
|
|
1388
|
+
idempotent: true,
|
|
1389
|
+
mcp: { destructive: false, kind: "tool", openWorld: false },
|
|
1390
|
+
method: "GET",
|
|
1391
|
+
mutates: false,
|
|
1392
|
+
path: "/v1/schedules/{scheduleId}/occurrences/{occurrenceId}/attempts",
|
|
1393
|
+
principals: ["api-credential", "oauth", "session"],
|
|
1394
|
+
problems: ["invalid_request", "not_found"],
|
|
1395
|
+
query: pageRequestSchema,
|
|
1396
|
+
response: attemptPageSchema,
|
|
1397
|
+
scopes: ["occurrences:read"],
|
|
1398
|
+
sessionAuthority: "headers",
|
|
1399
|
+
summary: "List delivery attempts",
|
|
1400
|
+
surfaces: ["api", "mock"],
|
|
1401
|
+
},
|
|
1402
|
+
{
|
|
1403
|
+
creates: false,
|
|
1404
|
+
description: "Reads one immutable delivery attempt from its occurrence's authoritative Scheduler Partition. Ambiguous means the receiver may have acted and is never rewritten into certainty without durable result evidence. The bounded response omits callback payloads, response bodies, destinations, signing material, secrets and raw transport failures by construction.",
|
|
1405
|
+
example: {
|
|
1406
|
+
attempt: {
|
|
1407
|
+
attemptNumber: 2,
|
|
1408
|
+
classifiedAt: "2026-07-28T15:02:00.000Z",
|
|
1409
|
+
delivery: "dlv_11h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1410
|
+
diagnostic: "network_ambiguous",
|
|
1411
|
+
durationMs: 10_000,
|
|
1412
|
+
id: "att_11h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1413
|
+
occurrence: "occ_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1414
|
+
remediation: "review_before_redelivery",
|
|
1415
|
+
startedAt: "2026-07-28T15:01:50.000Z",
|
|
1416
|
+
state: "ambiguous",
|
|
1417
|
+
trigger: { kind: "automatic" },
|
|
1418
|
+
},
|
|
1419
|
+
freshness: { kind: "authoritative" },
|
|
1420
|
+
},
|
|
1421
|
+
id: "attempts.read",
|
|
1422
|
+
idempotent: true,
|
|
1423
|
+
mcp: { destructive: false, kind: "tool", openWorld: false },
|
|
1424
|
+
method: "GET",
|
|
1425
|
+
mutates: false,
|
|
1426
|
+
path: "/v1/schedules/{scheduleId}/occurrences/{occurrenceId}/attempts/{attemptId}",
|
|
1427
|
+
principals: ["api-credential", "oauth", "session"],
|
|
1428
|
+
problems: ["invalid_request", "not_found"],
|
|
1429
|
+
response: attemptViewSchema,
|
|
1430
|
+
scopes: ["occurrences:read"],
|
|
1431
|
+
sessionAuthority: "headers",
|
|
1432
|
+
summary: "Read a delivery attempt",
|
|
1433
|
+
surfaces: ["api", "mock"],
|
|
1434
|
+
},
|
|
1435
|
+
{
|
|
1436
|
+
creates: true,
|
|
1437
|
+
description: "Creates one new immutable attempt under the same retained terminal occurrence and dispatches it through the ordinary delivery boundary. The required Idempotency-Key makes an identical replay converge on the same attempt and conflicting reuse is refused. The new attempt names the retained attempt that caused the redelivery; no earlier identity or outcome is changed. MCP marks this outbound mutation for explicit approval even though it is non-destructive.",
|
|
1438
|
+
example: {
|
|
1439
|
+
attempt: {
|
|
1440
|
+
attemptNumber: 3,
|
|
1441
|
+
delivery: "dlv_21h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1442
|
+
id: "att_21h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1443
|
+
occurrence: "occ_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1444
|
+
startedAt: "2026-07-28T15:05:00.000Z",
|
|
1445
|
+
trigger: {
|
|
1446
|
+
kind: "redelivery",
|
|
1447
|
+
sourceAttemptId: "att_11h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1448
|
+
},
|
|
1449
|
+
},
|
|
1450
|
+
freshness: { kind: "authoritative" },
|
|
1451
|
+
},
|
|
1452
|
+
id: "occurrences.redeliver",
|
|
1453
|
+
idempotent: true,
|
|
1454
|
+
mcp: { destructive: false, kind: "tool", openWorld: true },
|
|
1455
|
+
method: "POST",
|
|
1456
|
+
mutates: true,
|
|
1457
|
+
path: "/v1/schedules/{scheduleId}/occurrences/{occurrenceId}/redeliver",
|
|
1458
|
+
principals: ["api-credential", "oauth", "session"],
|
|
1459
|
+
problems: [
|
|
1460
|
+
"idempotency_key_reused",
|
|
1461
|
+
"invalid_request",
|
|
1462
|
+
"not_found",
|
|
1463
|
+
"service_unavailable",
|
|
1464
|
+
"state_conflict",
|
|
1465
|
+
],
|
|
1466
|
+
requestHeaders: requiredIdempotencyRequestHeaderSchema,
|
|
1467
|
+
response: attemptViewSchema,
|
|
1468
|
+
responseHeaders: idempotencyReplayResponseHeaderSchema,
|
|
1469
|
+
scopes: ["occurrences:redeliver"],
|
|
1470
|
+
sessionAuthority: "headers",
|
|
1471
|
+
summary: "Redeliver an occurrence",
|
|
1472
|
+
surfaces: ["api", "mock"],
|
|
1473
|
+
},
|
|
1474
|
+
{
|
|
1475
|
+
creates: false,
|
|
1476
|
+
description: "Replaces a schedule's definition, starting a new generation. Takes the same four ways of saying when as a create, so a one-shot can be replaced by a recurring definition and the other way round. An omitted callback endpoint/data pair preserves the stored callback; sending `data` alone replaces the encrypted data for the current endpoint, and sending `endpoint` selects a verified endpoint in the same authority tuple. The occurrence planned under the old definition is settled `skipped_stale` and can never fire unless it was already in flight, in which case its immutable attempt is allowed to settle honestly; a fresh occurrence is planned under the new definition, and the old definition's series stops advancing. Send `If-Match` with the version you read to make this conditional; without it the replace is unconditional and the last writer wins. A cancelled or completed schedule cannot be replaced. `partitionKey` is fixed when the schedule is created: omit it to preserve the stored value or echo that exact value in a GET → modify → PUT flow. Adding, changing, or clearing it is refused as `invalid_request` with `/partitionKey` identified. `latencyClass` remains derived from the replacement recurrence when the caller omits it.",
|
|
1477
|
+
example: {
|
|
1478
|
+
occurrence: {
|
|
1479
|
+
attemptCount: 0,
|
|
1480
|
+
generation: 2,
|
|
1481
|
+
id: "occ_01h8xyz9k2m4n6p8r0t2v4w6y9",
|
|
1482
|
+
plannedFor: "2026-07-28T16:00:00.000Z",
|
|
1483
|
+
schedule: "sch_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1484
|
+
sequence: 1,
|
|
1485
|
+
state: "planned",
|
|
1486
|
+
},
|
|
1487
|
+
schedule: {
|
|
1488
|
+
createdAt: "2026-07-28T14:59:00.000Z",
|
|
1489
|
+
endpoint: "ep_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1490
|
+
generation: 2,
|
|
1491
|
+
id: "sch_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1492
|
+
latencyClass: "realtime",
|
|
1493
|
+
partitionKey: "campaign.spring",
|
|
1494
|
+
recurrence: { kind: "at" },
|
|
1495
|
+
status: "active",
|
|
1496
|
+
updatedAt: "2026-07-28T15:10:00.000Z",
|
|
1497
|
+
version: 2,
|
|
1498
|
+
},
|
|
1499
|
+
},
|
|
1500
|
+
id: "schedules.replace",
|
|
1501
|
+
idempotent: true,
|
|
1502
|
+
// Destructive: the occurrence planned under the old definition is settled
|
|
1503
|
+
// `skipped_stale` and can never fire. A caller who meant to adjust a
|
|
1504
|
+
// schedule has discarded the work it was about to do.
|
|
1505
|
+
mcp: { destructive: true, kind: "tool", openWorld: true },
|
|
1506
|
+
method: "PUT",
|
|
1507
|
+
mutates: true,
|
|
1508
|
+
path: "/v1/schedules/{scheduleId}",
|
|
1509
|
+
principals: ["api-credential", "oauth", "session"],
|
|
1510
|
+
problems: [
|
|
1511
|
+
// The same undeclared refusal `schedules.create` carried, for the same
|
|
1512
|
+
// reason: a replacement may attach a different endpoint and resolves it
|
|
1513
|
+
// through the same status rule.
|
|
1514
|
+
"endpoint_unverified",
|
|
1515
|
+
"invalid_request",
|
|
1516
|
+
"not_found",
|
|
1517
|
+
// Only the payload ceiling can produce this here. A replacement adds no
|
|
1518
|
+
// schedule and cannot change `partitionKey`, so neither counted ceiling
|
|
1519
|
+
// can move; what it can do is carry a larger body than the plan admits.
|
|
1520
|
+
"plan_limit_reached",
|
|
1521
|
+
"service_unavailable",
|
|
1522
|
+
"state_conflict",
|
|
1523
|
+
"version_conflict",
|
|
1524
|
+
],
|
|
1525
|
+
request: scheduleRequestSchema,
|
|
1526
|
+
// The other arm of the same schema, deliberately: the create example shows
|
|
1527
|
+
// `at` and this one shows `delaySeconds`, so a reader of the published
|
|
1528
|
+
// document meets both ways of naming an instant.
|
|
1529
|
+
requestExample: {
|
|
1530
|
+
data: { report: "weekly" },
|
|
1531
|
+
everySeconds: 86_400,
|
|
1532
|
+
endpoint: "ep_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1533
|
+
partitionKey: "campaign.spring",
|
|
1534
|
+
},
|
|
1535
|
+
requestHeaders: conditionalRequestHeaderSchema,
|
|
1536
|
+
response: scheduleViewSchema,
|
|
1537
|
+
responseHeaders: versionResponseHeaderSchema,
|
|
1538
|
+
scopes: ["schedules:write"],
|
|
1539
|
+
sessionAuthority: "headers",
|
|
1540
|
+
summary: "Replace a schedule",
|
|
1541
|
+
surfaces: ["api", "mock"],
|
|
1542
|
+
},
|
|
1543
|
+
{
|
|
1544
|
+
creates: false,
|
|
1545
|
+
description: "Suspends a schedule without discarding it: its occurrence stays planned, its timer is released, and resume re-arms the original planned instant so elapsed work becomes due rather than being silently moved. A schedule with a dispatching or ambiguous callback is refused until that attempt settles, because pausing cannot strand its watchdog or turn a proven success into an unreachable completed state. Pausing an already paused schedule changes nothing and returns the same version, so a retry is free. A cancelled or completed schedule cannot be paused.",
|
|
1546
|
+
example: {
|
|
1547
|
+
occurrence: {
|
|
1548
|
+
attemptCount: 0,
|
|
1549
|
+
generation: 1,
|
|
1550
|
+
id: "occ_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1551
|
+
plannedFor: "2026-07-28T15:00:00.000Z",
|
|
1552
|
+
schedule: "sch_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1553
|
+
sequence: 1,
|
|
1554
|
+
state: "planned",
|
|
1555
|
+
},
|
|
1556
|
+
schedule: {
|
|
1557
|
+
createdAt: "2026-07-28T14:59:00.000Z",
|
|
1558
|
+
generation: 1,
|
|
1559
|
+
id: "sch_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1560
|
+
latencyClass: "realtime",
|
|
1561
|
+
recurrence: { kind: "at" },
|
|
1562
|
+
status: "paused",
|
|
1563
|
+
updatedAt: "2026-07-28T15:10:00.000Z",
|
|
1564
|
+
version: 2,
|
|
1565
|
+
},
|
|
1566
|
+
},
|
|
1567
|
+
id: "schedules.pause",
|
|
1568
|
+
idempotent: true,
|
|
1569
|
+
// Not destructive: the occurrence stays planned and the timer stays armed.
|
|
1570
|
+
// A pause defers work; resuming delivers it.
|
|
1571
|
+
mcp: { destructive: false, kind: "tool", openWorld: false },
|
|
1572
|
+
method: "POST",
|
|
1573
|
+
mutates: true,
|
|
1574
|
+
path: "/v1/schedules/{scheduleId}/pause",
|
|
1575
|
+
principals: ["api-credential", "oauth", "session"],
|
|
1576
|
+
problems: [
|
|
1577
|
+
"invalid_request",
|
|
1578
|
+
"not_found",
|
|
1579
|
+
"state_conflict",
|
|
1580
|
+
"version_conflict",
|
|
1581
|
+
],
|
|
1582
|
+
requestHeaders: conditionalRequestHeaderSchema,
|
|
1583
|
+
response: scheduleViewSchema,
|
|
1584
|
+
responseHeaders: versionResponseHeaderSchema,
|
|
1585
|
+
scopes: ["schedules:write"],
|
|
1586
|
+
sessionAuthority: "headers",
|
|
1587
|
+
summary: "Pause a schedule",
|
|
1588
|
+
surfaces: ["api", "mock"],
|
|
1589
|
+
},
|
|
1590
|
+
{
|
|
1591
|
+
creates: false,
|
|
1592
|
+
description: "Returns a paused schedule to active. An occurrence whose instant passed while the schedule was paused becomes due at once rather than being skipped — a pause defers work, it does not discard it. Resuming an already active schedule changes nothing and returns the same version.",
|
|
1593
|
+
example: {
|
|
1594
|
+
occurrence: {
|
|
1595
|
+
attemptCount: 0,
|
|
1596
|
+
generation: 1,
|
|
1597
|
+
id: "occ_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1598
|
+
plannedFor: "2026-07-28T15:00:00.000Z",
|
|
1599
|
+
schedule: "sch_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1600
|
+
sequence: 1,
|
|
1601
|
+
state: "planned",
|
|
1602
|
+
},
|
|
1603
|
+
schedule: {
|
|
1604
|
+
createdAt: "2026-07-28T14:59:00.000Z",
|
|
1605
|
+
generation: 1,
|
|
1606
|
+
id: "sch_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1607
|
+
latencyClass: "realtime",
|
|
1608
|
+
recurrence: { kind: "at" },
|
|
1609
|
+
status: "active",
|
|
1610
|
+
updatedAt: "2026-07-28T15:20:00.000Z",
|
|
1611
|
+
version: 3,
|
|
1612
|
+
},
|
|
1613
|
+
},
|
|
1614
|
+
id: "schedules.resume",
|
|
1615
|
+
idempotent: true,
|
|
1616
|
+
mcp: { destructive: false, kind: "tool", openWorld: false },
|
|
1617
|
+
method: "POST",
|
|
1618
|
+
mutates: true,
|
|
1619
|
+
path: "/v1/schedules/{scheduleId}/resume",
|
|
1620
|
+
principals: ["api-credential", "oauth", "session"],
|
|
1621
|
+
problems: [
|
|
1622
|
+
"invalid_request",
|
|
1623
|
+
"not_found",
|
|
1624
|
+
"state_conflict",
|
|
1625
|
+
"version_conflict",
|
|
1626
|
+
],
|
|
1627
|
+
requestHeaders: conditionalRequestHeaderSchema,
|
|
1628
|
+
response: scheduleViewSchema,
|
|
1629
|
+
responseHeaders: versionResponseHeaderSchema,
|
|
1630
|
+
scopes: ["schedules:write"],
|
|
1631
|
+
sessionAuthority: "headers",
|
|
1632
|
+
summary: "Resume a schedule",
|
|
1633
|
+
surfaces: ["api", "mock"],
|
|
1634
|
+
},
|
|
1635
|
+
{
|
|
1636
|
+
creates: false,
|
|
1637
|
+
description: "Cancels a schedule and settles its occurrence `cancelled`, releasing the timer. Terminal: a cancelled schedule cannot be resumed or replaced, and cancelling one that is already cancelled changes nothing and returns the same version. A cancel that races its own occurrence becoming due still wins — the occurrence settles cancelled and no work is delivered — which is why this is a refusal-free retry rather than a race a client has to reason about. The schedule stays readable; archive it to take it out of the listing.",
|
|
1638
|
+
example: {
|
|
1639
|
+
occurrence: {
|
|
1640
|
+
attemptCount: 0,
|
|
1641
|
+
generation: 1,
|
|
1642
|
+
id: "occ_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1643
|
+
plannedFor: "2026-07-28T15:00:00.000Z",
|
|
1644
|
+
schedule: "sch_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1645
|
+
sequence: 1,
|
|
1646
|
+
settledAt: "2026-07-28T15:10:00.000Z",
|
|
1647
|
+
state: "cancelled",
|
|
1648
|
+
},
|
|
1649
|
+
schedule: {
|
|
1650
|
+
createdAt: "2026-07-28T14:59:00.000Z",
|
|
1651
|
+
generation: 1,
|
|
1652
|
+
id: "sch_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1653
|
+
latencyClass: "realtime",
|
|
1654
|
+
recurrence: { kind: "at" },
|
|
1655
|
+
status: "cancelled",
|
|
1656
|
+
updatedAt: "2026-07-28T15:10:00.000Z",
|
|
1657
|
+
version: 2,
|
|
1658
|
+
},
|
|
1659
|
+
},
|
|
1660
|
+
id: "schedules.cancel",
|
|
1661
|
+
idempotent: true,
|
|
1662
|
+
// Destructive and terminal: the occurrence settles `cancelled`, no work is
|
|
1663
|
+
// delivered, and nothing resumes or replaces it afterwards.
|
|
1664
|
+
mcp: { destructive: true, kind: "tool", openWorld: false },
|
|
1665
|
+
method: "DELETE",
|
|
1666
|
+
mutates: true,
|
|
1667
|
+
path: "/v1/schedules/{scheduleId}",
|
|
1668
|
+
principals: ["api-credential", "oauth", "session"],
|
|
1669
|
+
problems: [
|
|
1670
|
+
"invalid_request",
|
|
1671
|
+
"not_found",
|
|
1672
|
+
"state_conflict",
|
|
1673
|
+
"version_conflict",
|
|
1674
|
+
],
|
|
1675
|
+
requestHeaders: conditionalRequestHeaderSchema,
|
|
1676
|
+
response: scheduleViewSchema,
|
|
1677
|
+
responseHeaders: versionResponseHeaderSchema,
|
|
1678
|
+
scopes: ["schedules:write"],
|
|
1679
|
+
sessionAuthority: "headers",
|
|
1680
|
+
summary: "Cancel a schedule",
|
|
1681
|
+
surfaces: ["api", "mock"],
|
|
1682
|
+
},
|
|
1683
|
+
{
|
|
1684
|
+
creates: false,
|
|
1685
|
+
description: "Files a finished schedule away: it leaves the default listing and stays readable by identifier, carrying the instant it was archived. Only a cancelled or completed schedule can be archived — archiving is filing, not stopping, so cancel it first. Archiving an already archived schedule changes nothing and returns the same version.",
|
|
1686
|
+
example: {
|
|
1687
|
+
occurrence: {
|
|
1688
|
+
attemptCount: 0,
|
|
1689
|
+
generation: 1,
|
|
1690
|
+
id: "occ_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1691
|
+
plannedFor: "2026-07-28T15:00:00.000Z",
|
|
1692
|
+
schedule: "sch_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1693
|
+
sequence: 1,
|
|
1694
|
+
settledAt: "2026-07-28T15:10:00.000Z",
|
|
1695
|
+
state: "cancelled",
|
|
1696
|
+
},
|
|
1697
|
+
schedule: {
|
|
1698
|
+
archivedAt: "2026-07-28T15:30:00.000Z",
|
|
1699
|
+
createdAt: "2026-07-28T14:59:00.000Z",
|
|
1700
|
+
generation: 1,
|
|
1701
|
+
id: "sch_01h8xyz9k2m4n6p8r0t2v4w6y8",
|
|
1702
|
+
latencyClass: "realtime",
|
|
1703
|
+
recurrence: { kind: "at" },
|
|
1704
|
+
status: "cancelled",
|
|
1705
|
+
updatedAt: "2026-07-28T15:30:00.000Z",
|
|
1706
|
+
version: 3,
|
|
1707
|
+
},
|
|
1708
|
+
},
|
|
1709
|
+
id: "schedules.archive",
|
|
1710
|
+
idempotent: true,
|
|
1711
|
+
// Not destructive: archiving is filing. The schedule stays readable by
|
|
1712
|
+
// identifier and only leaves the default listing, and it is refused unless
|
|
1713
|
+
// the schedule has already stopped.
|
|
1714
|
+
mcp: { destructive: false, kind: "tool", openWorld: false },
|
|
1715
|
+
method: "POST",
|
|
1716
|
+
mutates: true,
|
|
1717
|
+
path: "/v1/schedules/{scheduleId}/archive",
|
|
1718
|
+
principals: ["api-credential", "oauth", "session"],
|
|
1719
|
+
problems: [
|
|
1720
|
+
"invalid_request",
|
|
1721
|
+
"not_found",
|
|
1722
|
+
"state_conflict",
|
|
1723
|
+
"version_conflict",
|
|
1724
|
+
],
|
|
1725
|
+
requestHeaders: conditionalRequestHeaderSchema,
|
|
1726
|
+
response: scheduleViewSchema,
|
|
1727
|
+
responseHeaders: versionResponseHeaderSchema,
|
|
1728
|
+
scopes: ["schedules:write"],
|
|
1729
|
+
sessionAuthority: "headers",
|
|
1730
|
+
summary: "Archive a schedule",
|
|
1731
|
+
surfaces: ["api", "mock"],
|
|
1732
|
+
},
|
|
1733
|
+
];
|
|
1734
|
+
/**
|
|
1735
|
+
* Headers the contract mock uses to expose state a body cannot carry.
|
|
1736
|
+
*
|
|
1737
|
+
* Declared here rather than written into the adapter for the same reason every
|
|
1738
|
+
* other wire detail is: a header invented at the edge is a wire shape that
|
|
1739
|
+
* drifts.
|
|
1740
|
+
*/
|
|
1741
|
+
export const MOCK_HEADERS = {
|
|
1742
|
+
/** The instant the mock's pinned clock reports. */
|
|
1743
|
+
now: "X-Greatwait-Now",
|
|
1744
|
+
/** Present when this response is an at-least-once duplicate. */
|
|
1745
|
+
replayed: "X-Greatwait-Replayed",
|
|
1746
|
+
};
|
|
1747
|
+
/** The registry, widened. `as const` narrows `surfaces` to per-operation
|
|
1748
|
+
* literal tuples, which is what makes `OperationId` precise but makes the
|
|
1749
|
+
* registry awkward to iterate generically. */
|
|
1750
|
+
const ALL = OPERATIONS;
|
|
1751
|
+
export function operationsFor(surface) {
|
|
1752
|
+
return ALL.filter((operation) => operation.surfaces.includes(surface));
|
|
1753
|
+
}
|
|
1754
|
+
export function findOperation(id) {
|
|
1755
|
+
return ALL.find((operation) => operation.id === id);
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* The schema an operation's success body is parsed with, by operation id.
|
|
1759
|
+
*
|
|
1760
|
+
* **Exists so a consumer can parse without naming a schema export.** The typed
|
|
1761
|
+
* client returns `unknown` and tells callers to parse with "that operation's
|
|
1762
|
+
* contract schema", which until W9-03 meant every caller had to map an
|
|
1763
|
+
* operation id onto a symbol by hand — a mapping that is a second statement of
|
|
1764
|
+
* the registry, and one a renamed export breaks silently. The published
|
|
1765
|
+
* quickstarts and anything W10 generates resolve it here instead, so the
|
|
1766
|
+
* operation id is the only name a consumer has to know.
|
|
1767
|
+
*
|
|
1768
|
+
* `undefined` for an id the registry does not carry, rather than a throw: the
|
|
1769
|
+
* caller usually has a better message than this function could write.
|
|
1770
|
+
*/
|
|
1771
|
+
export function responseSchemaFor(id) {
|
|
1772
|
+
return findOperation(id)?.response;
|
|
1773
|
+
}
|
|
1774
|
+
/** The schema an operation's request body is validated with, by id. */
|
|
1775
|
+
export function requestSchemaFor(id) {
|
|
1776
|
+
return findOperation(id)?.request;
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* The status a successful call returns.
|
|
1780
|
+
*
|
|
1781
|
+
* Computed in one place, so the OpenAPI document, the contract mock and the API
|
|
1782
|
+
* adapter cannot disagree about it — which they did for exactly one commit,
|
|
1783
|
+
* when a route returned 201 and the published document said 200. An operation
|
|
1784
|
+
* that **creates** something, **on the product surface**, answers 201;
|
|
1785
|
+
* everything else answers 200.
|
|
1786
|
+
*
|
|
1787
|
+
* The first clause read `method === "POST" && mutates` until W2-02, which added
|
|
1788
|
+
* the first mutating POST that creates nothing — pausing a schedule — and would
|
|
1789
|
+
* have had "201 Created" published for it. `creates` is declared rather than
|
|
1790
|
+
* inferred because no combination of method, path and schema distinguishes
|
|
1791
|
+
* "changed something" from "brought something into being".
|
|
1792
|
+
*
|
|
1793
|
+
* The surface clause is not a per-operation exception, and it is not
|
|
1794
|
+
* cosmetic. Without it the rule reaches `mock.setScenario`, whose 200 is
|
|
1795
|
+
* already published in `openapi.mock.json` and already served from a deployed
|
|
1796
|
+
* host — and `classifyChange` in `compatibility.ts` classifies a changed status
|
|
1797
|
+
* as **breaking**, requiring a new contract version. Tidying the mock's control
|
|
1798
|
+
* plane is not worth a contract version, and shipping the change without one
|
|
1799
|
+
* would make the compatibility module a thing this repository consults and then
|
|
1800
|
+
* ignores. The mock's control plane is not a product operation; that is the
|
|
1801
|
+
* distinction, and it is structural rather than a judgement about any one
|
|
1802
|
+
* operation.
|
|
1803
|
+
*/
|
|
1804
|
+
export function successStatusFor(operation) {
|
|
1805
|
+
return operation.creates && operation.surfaces.includes("api") ? 201 : 200;
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Problems any operation may return regardless of what it does. Kept separate
|
|
1809
|
+
* so an operation's own `problems` list stays about that operation.
|
|
1810
|
+
*/
|
|
1811
|
+
export const UNIVERSAL_PROBLEMS = [
|
|
1812
|
+
"rate_limited",
|
|
1813
|
+
"service_unavailable",
|
|
1814
|
+
];
|
|
1815
|
+
/**
|
|
1816
|
+
* Problems an operation returns once authentication applies to it.
|
|
1817
|
+
*
|
|
1818
|
+
* `credential_expired` is here rather than on the operations that rotate,
|
|
1819
|
+
* because expiry is a property of the credential being *presented*, not of what
|
|
1820
|
+
* the caller asked to do. Any authenticated operation can meet a credential
|
|
1821
|
+
* whose rotation window has closed.
|
|
1822
|
+
*/
|
|
1823
|
+
export const AUTHENTICATED_PROBLEMS = [
|
|
1824
|
+
"authentication_required",
|
|
1825
|
+
"credential_expired",
|
|
1826
|
+
"credential_revoked",
|
|
1827
|
+
"scope_missing",
|
|
1828
|
+
];
|
|
1829
|
+
export function problemsFor(operation) {
|
|
1830
|
+
return [
|
|
1831
|
+
...new Set([
|
|
1832
|
+
...operation.problems,
|
|
1833
|
+
...UNIVERSAL_PROBLEMS,
|
|
1834
|
+
...(operation.scopes.length > 0 ? AUTHENTICATED_PROBLEMS : []),
|
|
1835
|
+
]),
|
|
1836
|
+
].sort();
|
|
1837
|
+
}
|