@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,466 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* One thing that can go wrong at a step, and the next move.
|
|
4
|
+
*
|
|
5
|
+
* `when` is a `ProblemCode` when the service refuses, and one of the named
|
|
6
|
+
* conditions below when the failure has no problem document — a timeout, a
|
|
7
|
+
* receiver that answered badly, an outcome that never settled. Both kinds
|
|
8
|
+
* exist because an integrator hits both, and a recovery table that only covers
|
|
9
|
+
* refusals leaves the reader alone at exactly the point they are stuck.
|
|
10
|
+
*/
|
|
11
|
+
export const JOURNEY_CONDITIONS = [
|
|
12
|
+
"attempt-failed",
|
|
13
|
+
"no-response",
|
|
14
|
+
"not-settled",
|
|
15
|
+
"receiver-unreachable",
|
|
16
|
+
"unexpected-answer",
|
|
17
|
+
];
|
|
18
|
+
const identifier = z
|
|
19
|
+
.string()
|
|
20
|
+
.min(1)
|
|
21
|
+
.regex(/^[a-z][a-z0-9-]*$/u, "must be lower-case kebab-case");
|
|
22
|
+
const sentence = z.string().min(12);
|
|
23
|
+
/**
|
|
24
|
+
* A variable name, and a placeholder that is provably not a credential.
|
|
25
|
+
*
|
|
26
|
+
* The three patterns are the product's own credential shapes, taken from
|
|
27
|
+
* `scripts/secret-patterns.ts` in spirit rather than by import — this package
|
|
28
|
+
* is the contract and may not depend on a repository script. A generated page
|
|
29
|
+
* carrying a real one would be a leak that no reviewer reads carefully enough
|
|
30
|
+
* to catch, so the refusal is here rather than in a convention.
|
|
31
|
+
*/
|
|
32
|
+
export const journeyVariableSchema = z.object({
|
|
33
|
+
defaultExpression: z
|
|
34
|
+
.object({ javascript: z.string().min(1), shell: z.string().min(1) })
|
|
35
|
+
.optional(),
|
|
36
|
+
detail: sentence,
|
|
37
|
+
name: z
|
|
38
|
+
.string()
|
|
39
|
+
.regex(/^GREATWAIT_[A-Z0-9_]+$/u, "must be a GREATWAIT_ environment name"),
|
|
40
|
+
placeholder: z
|
|
41
|
+
.string()
|
|
42
|
+
.min(1)
|
|
43
|
+
.refine((value) => !/(?:^|[^A-Za-z0-9_-])(?:pt|ptsk|oat|ort|oac)_(?:live|test)_[A-Za-z0-9_-]{8,}/u.test(value), "must not look like a credential"),
|
|
44
|
+
secret: z.boolean(),
|
|
45
|
+
title: z.string().min(3),
|
|
46
|
+
// Not annotated `z.ZodType<JourneyVariable>`: under
|
|
47
|
+
// `exactOptionalPropertyTypes` an optional Zod property produces
|
|
48
|
+
// `T | undefined`, which is not assignable to an optional interface field,
|
|
49
|
+
// and the annotation would have to be widened until it stopped describing
|
|
50
|
+
// the interface. `journeyVariable` below is what holds the two together —
|
|
51
|
+
// it returns the declared type, so a schema that drifted from the interface
|
|
52
|
+
// fails to compile there instead.
|
|
53
|
+
});
|
|
54
|
+
export const journeyStepSchema = z.object({
|
|
55
|
+
capture: z
|
|
56
|
+
.array(z.object({
|
|
57
|
+
from: z.string().min(1),
|
|
58
|
+
header: z.boolean().optional(),
|
|
59
|
+
secret: z.boolean().optional(),
|
|
60
|
+
variable: z.string().regex(/^GREATWAIT_[A-Z0-9_]+$/u),
|
|
61
|
+
}))
|
|
62
|
+
.optional(),
|
|
63
|
+
expect: z
|
|
64
|
+
.array(z.object({
|
|
65
|
+
equals: z.string().min(1),
|
|
66
|
+
message: sentence,
|
|
67
|
+
subject: z.string().regex(/^GREATWAIT_[A-Z0-9_]+$/u),
|
|
68
|
+
}))
|
|
69
|
+
.optional(),
|
|
70
|
+
key: identifier,
|
|
71
|
+
observe: sentence,
|
|
72
|
+
operationId: z.string().min(1),
|
|
73
|
+
poll: z
|
|
74
|
+
.object({
|
|
75
|
+
describe: sentence,
|
|
76
|
+
intervalSeconds: z.number().int().positive(),
|
|
77
|
+
timeoutSeconds: z.number().int().positive(),
|
|
78
|
+
})
|
|
79
|
+
.optional(),
|
|
80
|
+
purpose: sentence,
|
|
81
|
+
query: z.record(z.string(), z.string()).optional(),
|
|
82
|
+
recovery: z
|
|
83
|
+
.array(z.object({ nextMove: sentence, when: z.string().min(1) }))
|
|
84
|
+
.min(1, "every step names at least one next move"),
|
|
85
|
+
request: z.unknown().optional(),
|
|
86
|
+
requestHeaders: z.record(z.string(), z.string()).optional(),
|
|
87
|
+
title: z.string().min(3),
|
|
88
|
+
});
|
|
89
|
+
/**
|
|
90
|
+
* The lowest curl that runs the generated commands.
|
|
91
|
+
*
|
|
92
|
+
* `--variable` and `--expand-header` arrived in curl 8.3.0, and they are what
|
|
93
|
+
* keeps the credential out of the process arguments: the command line names
|
|
94
|
+
* `%GREATWAIT_API_KEY`, and curl reads the value out of its own environment.
|
|
95
|
+
* The ordinary `-H "Authorization: Bearer $GREATWAIT_API_KEY"` spelling puts
|
|
96
|
+
* the secret in `argv`, where any other user on the machine can read it out of
|
|
97
|
+
* `ps`.
|
|
98
|
+
*
|
|
99
|
+
* Stated as a constant because the requirement is published in three places —
|
|
100
|
+
* both quickstarts and the journey artifact — and a version number written
|
|
101
|
+
* three times is a version number that will disagree with itself.
|
|
102
|
+
*/
|
|
103
|
+
export const MINIMUM_CURL_VERSION = "8.3.0";
|
|
104
|
+
/** The tools the shell rendering of the journey needs on the reader's PATH. */
|
|
105
|
+
export const CURL_JOURNEY_TOOLS = [
|
|
106
|
+
{ name: "curl", version: MINIMUM_CURL_VERSION },
|
|
107
|
+
{ name: "jq", version: "1.6" },
|
|
108
|
+
];
|
|
109
|
+
export const FIRST_VALUE_PREREQUISITES = [
|
|
110
|
+
{
|
|
111
|
+
action: "Accept your Greatwait invitation, sign in, and create a project in the dashboard.",
|
|
112
|
+
because: "projects.create accepts only a signed-in member, so a project is created by a person rather than by an API credential.",
|
|
113
|
+
id: "project",
|
|
114
|
+
title: "A project",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
action: "In that project, create an API credential and copy the secret it shows once.",
|
|
118
|
+
because: "credentials.create accepts only a signed-in member: a credential that could mint another credential would make revoking one meaningless.",
|
|
119
|
+
id: "credential",
|
|
120
|
+
title: "An API credential",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
action: "Have an HTTPS URL of your own that answers POST with a 2xx status, and can be reached from the public internet.",
|
|
124
|
+
because: "Greatwait delivers your callback to an address you control. It does not offer a shared receiver to try, and it will not send your payload to a third-party inspection service on your behalf.",
|
|
125
|
+
id: "receiver",
|
|
126
|
+
title: "An HTTPS receiver you control",
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
export const FIRST_VALUE_VARIABLES = [
|
|
130
|
+
{
|
|
131
|
+
detail: "The API origin for the environment you are working against, with no trailing slash.",
|
|
132
|
+
name: "GREATWAIT_API_URL",
|
|
133
|
+
placeholder: "https://api.greatwait.dev",
|
|
134
|
+
secret: false,
|
|
135
|
+
title: "API origin",
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
detail: "The API credential secret, shown once when you created it. Export it in your shell; do not paste it into a command, a URL, or a file you commit.",
|
|
139
|
+
name: "GREATWAIT_API_KEY",
|
|
140
|
+
placeholder: "<the credential secret, shown once at creation>",
|
|
141
|
+
secret: true,
|
|
142
|
+
title: "API credential",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
detail: "Your own HTTPS receiver. Every callback in this journey is delivered here and nowhere else.",
|
|
146
|
+
name: "GREATWAIT_RECEIVER_URL",
|
|
147
|
+
placeholder: "https://receiver.example.com/greatwait",
|
|
148
|
+
secret: false,
|
|
149
|
+
title: "Receiver URL",
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
// A fresh key per run, because the same key with the same body is a replay
|
|
153
|
+
// rather than a new schedule — which is the property step five proves, and
|
|
154
|
+
// would make a second run of the whole journey return the first run's
|
|
155
|
+
// schedule and never fire anything.
|
|
156
|
+
defaultExpression: {
|
|
157
|
+
javascript: "`first-value-${crypto.randomUUID()}`",
|
|
158
|
+
// `date` and `$$` rather than `uuidgen`, which is not on every minimal
|
|
159
|
+
// Linux image and would make the quickstart fail at line four for a
|
|
160
|
+
// reason that has nothing to do with Greatwait.
|
|
161
|
+
shell: "first-value-$(date +%s)-$$",
|
|
162
|
+
},
|
|
163
|
+
detail: "A value you choose, at least 16 printable ASCII characters, that makes the create call safe to retry. Use a fresh one for each schedule you mean to create; these quickstarts generate one if you do not set it.",
|
|
164
|
+
name: "GREATWAIT_IDEMPOTENCY_KEY",
|
|
165
|
+
placeholder: "first-value-0000000000000000",
|
|
166
|
+
secret: false,
|
|
167
|
+
title: "Idempotency key",
|
|
168
|
+
},
|
|
169
|
+
];
|
|
170
|
+
/**
|
|
171
|
+
* The sequence.
|
|
172
|
+
*
|
|
173
|
+
* Nine steps, and the shape of them is the acceptance criterion: verify the
|
|
174
|
+
* context and the credential, create a destination, create an idempotent
|
|
175
|
+
* one-shot, prove the idempotency, read the schedule back, wait for the
|
|
176
|
+
* occurrence to settle, read the delivery attempt, and clean up. Creating a
|
|
177
|
+
* resource is not first value; **step `read-attempt` is**, because it is the
|
|
178
|
+
* first point at which the customer can read what happened at their own
|
|
179
|
+
* receiver.
|
|
180
|
+
*
|
|
181
|
+
* `delaySeconds` rather than `at`, deliberately. An absolute instant published
|
|
182
|
+
* in an example is stale the day after it is written, and a reader who pastes
|
|
183
|
+
* it is refused for a reason that has nothing to do with what they were
|
|
184
|
+
* learning. A delay is converted to an instant when the request is accepted,
|
|
185
|
+
* so the schedule reads back afterwards exactly as an `at` schedule would.
|
|
186
|
+
*/
|
|
187
|
+
export const FIRST_VALUE_STEPS = [
|
|
188
|
+
{
|
|
189
|
+
key: "check-service",
|
|
190
|
+
observe: "The service name and the environment you meant to work in. If the environment is not the one you expect, stop and fix GREATWAIT_API_URL before creating anything.",
|
|
191
|
+
operationId: "meta.version",
|
|
192
|
+
purpose: "Confirms the origin you are pointed at is a Greatwait API and says which environment it serves. It needs no credential, so it separates 'wrong URL' from 'wrong key'.",
|
|
193
|
+
recovery: [
|
|
194
|
+
{
|
|
195
|
+
nextMove: "Something answered, but not this service. Check GREATWAIT_API_URL — a 404 here comes from a proxy in front of the API rather than from the API, so it carries no problem document to read.",
|
|
196
|
+
when: "unexpected-answer",
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
nextMove: "The origin did not answer. Check the hostname and that you can reach it from this network before going further.",
|
|
200
|
+
when: "no-response",
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
title: "Check which service and environment you reached",
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
key: "check-credential",
|
|
207
|
+
observe: "An empty page on a new project, or your existing schedules. Either way the credential is accepted and carries the scopes this journey needs.",
|
|
208
|
+
operationId: "schedules.list",
|
|
209
|
+
purpose: "Proves the credential works and is bound to the project you think it is, before you create anything with it. A credential names exactly one project, and this is the cheapest call that says so.",
|
|
210
|
+
query: { limit: "1" },
|
|
211
|
+
recovery: [
|
|
212
|
+
{
|
|
213
|
+
nextMove: "No credential reached the service. Export GREATWAIT_API_KEY in this shell and try again.",
|
|
214
|
+
when: "authentication_required",
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
nextMove: "The credential was revoked. Create a new one in the dashboard; a revoked credential never comes back.",
|
|
218
|
+
when: "credential_revoked",
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
nextMove: "The credential expired. Rotate it in the dashboard and export the new secret.",
|
|
222
|
+
when: "credential_expired",
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
nextMove: "The credential is missing schedules:read. Create one with the scopes this journey needs rather than widening this one.",
|
|
226
|
+
when: "scope_missing",
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
title: "Check the credential",
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
key: "create-destination",
|
|
233
|
+
capture: [
|
|
234
|
+
{ from: "id", variable: "GREATWAIT_ENDPOINTID" },
|
|
235
|
+
{ from: "secret", secret: true, variable: "GREATWAIT_ENDPOINT_SECRET" },
|
|
236
|
+
],
|
|
237
|
+
observe: "An endpoint identifier, and a signing secret returned by this call and by no other. Store the secret now: reading the endpoint back later will not show it again.",
|
|
238
|
+
operationId: "endpoints.create",
|
|
239
|
+
purpose: "Registers the address every occurrence of this schedule is delivered to, and mints the per-endpoint signing key your receiver verifies callbacks with.",
|
|
240
|
+
recovery: [
|
|
241
|
+
{
|
|
242
|
+
nextMove: "The URL was refused. It must be HTTPS on port 443, carry no credentials or fragment, and resolve to a public address — a localhost or private-range host cannot be reached from the service.",
|
|
243
|
+
when: "invalid_request",
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
nextMove: "This project is at its destination ceiling. Disable a destination you no longer deliver to; disabled ones still count, because they stay readable for audit.",
|
|
247
|
+
when: "plan_limit_reached",
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
request: { url: "{{GREATWAIT_RECEIVER_URL}}" },
|
|
251
|
+
title: "Create the destination",
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
key: "create-one-shot",
|
|
255
|
+
capture: [
|
|
256
|
+
{ from: "schedule.id", variable: "GREATWAIT_SCHEDULEID" },
|
|
257
|
+
{ from: "occurrence.id", variable: "GREATWAIT_OCCURRENCEID" },
|
|
258
|
+
],
|
|
259
|
+
observe: "A schedule whose recurrence kind is `at`, and its first occurrence in state `planned` with the instant your delay resolved to.",
|
|
260
|
+
operationId: "schedules.create",
|
|
261
|
+
purpose: "Creates the one-shot. The delay is converted to an absolute instant when the request is accepted, so the schedule reads back the same afterwards however it was expressed.",
|
|
262
|
+
recovery: [
|
|
263
|
+
{
|
|
264
|
+
nextMove: "This deployment requires a destination to answer its ownership challenge before anything is delivered there, and yours has not. Answer the challenge with the secret the create call returned, then create the schedule.",
|
|
265
|
+
when: "endpoint_unverified",
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
nextMove: "The endpoint identifier is not one this credential's project can deliver to — it belongs to another project, or it has been disabled. Disabling is terminal, so create a new destination rather than trying to re-enable that one.",
|
|
269
|
+
when: "not_found",
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
nextMove: "The same idempotency key was used for a different request. Use a new key, or send the byte-identical body the key was first used with.",
|
|
273
|
+
when: "idempotency_key_reused",
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
nextMove: "Give exactly one of at, delaySeconds, everySeconds and cron, and make sure the endpoint identifier is one this credential's project owns.",
|
|
277
|
+
when: "invalid_request",
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
nextMove: "This organization is at a plan ceiling — live schedules, distinct partition keys, or payload size. Nothing was created; reduce the payload or archive a schedule and retry.",
|
|
281
|
+
when: "plan_limit_reached",
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
nextMove: "The service could not accept the schedule right now. This is safe to retry with the same idempotency key, which is what the key is for.",
|
|
285
|
+
when: "service_unavailable",
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
request: {
|
|
289
|
+
data: { greeting: "first value" },
|
|
290
|
+
delaySeconds: 30,
|
|
291
|
+
endpoint: "{{GREATWAIT_ENDPOINTID}}",
|
|
292
|
+
},
|
|
293
|
+
requestHeaders: { "Idempotency-Key": "{{GREATWAIT_IDEMPOTENCY_KEY}}" },
|
|
294
|
+
title: "Create the one-shot",
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
key: "replay-one-shot",
|
|
298
|
+
capture: [
|
|
299
|
+
{
|
|
300
|
+
from: "Idempotency-Replayed",
|
|
301
|
+
header: true,
|
|
302
|
+
variable: "GREATWAIT_REPLAYED",
|
|
303
|
+
},
|
|
304
|
+
{ from: "schedule.id", variable: "GREATWAIT_REPLAYED_SCHEDULEID" },
|
|
305
|
+
],
|
|
306
|
+
expect: [
|
|
307
|
+
{
|
|
308
|
+
equals: "true",
|
|
309
|
+
message: "the second create was not reported as a replay, which means it may have created a second schedule — read your schedule listing before creating anything else",
|
|
310
|
+
subject: "GREATWAIT_REPLAYED",
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
equals: "{{GREATWAIT_SCHEDULEID}}",
|
|
314
|
+
message: "the replay returned a different schedule from the one the first call created",
|
|
315
|
+
subject: "GREATWAIT_REPLAYED_SCHEDULEID",
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
observe: "The identical schedule identifier and an `Idempotency-Replayed: true` response header. Nothing was created the second time, and no second callback will be delivered.",
|
|
319
|
+
operationId: "schedules.create",
|
|
320
|
+
purpose: "Repeats the previous call byte for byte, with the same key. This is the retry a network failure would have made for you, and it is the reason a create is safe to retry at all.",
|
|
321
|
+
recovery: [
|
|
322
|
+
{
|
|
323
|
+
nextMove: "The body differed from the first call. A replay must be byte-identical; a changed body with a used key is a conflict rather than a second write.",
|
|
324
|
+
when: "idempotency_key_reused",
|
|
325
|
+
},
|
|
326
|
+
],
|
|
327
|
+
request: {
|
|
328
|
+
data: { greeting: "first value" },
|
|
329
|
+
delaySeconds: 30,
|
|
330
|
+
endpoint: "{{GREATWAIT_ENDPOINTID}}",
|
|
331
|
+
},
|
|
332
|
+
requestHeaders: { "Idempotency-Key": "{{GREATWAIT_IDEMPOTENCY_KEY}}" },
|
|
333
|
+
title: "Retry it safely",
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
key: "read-schedule",
|
|
337
|
+
observe: "The same schedule, with a strong ETag in the response. Send that value back in `If-Match` to make a later pause, replace or cancel conditional on nothing having changed in between.",
|
|
338
|
+
operationId: "schedules.read",
|
|
339
|
+
purpose: "Reads the schedule back from the partition that owns it, which is also where the version you need for a conditional write comes from.",
|
|
340
|
+
recovery: [
|
|
341
|
+
{
|
|
342
|
+
nextMove: "A schedule in another organization, project or environment answers not_found rather than a refusal. Check that this credential belongs to the project that created it.",
|
|
343
|
+
when: "not_found",
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
title: "Read the schedule back",
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
key: "await-occurrence",
|
|
350
|
+
observe: "The occurrence reaching `succeeded`. `terminal_failed` is also a settled answer, and the attempt in the next step says why.",
|
|
351
|
+
operationId: "occurrences.read",
|
|
352
|
+
poll: {
|
|
353
|
+
describe: "Poll until the occurrence reaches a terminal state, or until the deadline passes.",
|
|
354
|
+
intervalSeconds: 5,
|
|
355
|
+
timeoutSeconds: 180,
|
|
356
|
+
},
|
|
357
|
+
purpose: "Waits for the occurrence to settle. Delivery is at-least-once and a failing attempt is retried, so the state moves through `due` and `dispatching` before it settles.",
|
|
358
|
+
recovery: [
|
|
359
|
+
{
|
|
360
|
+
nextMove: "The occurrence never settled inside the deadline. It is not lost: read it again later, and read its attempts to see how far delivery got.",
|
|
361
|
+
when: "not-settled",
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
title: "Wait for the occurrence to settle",
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
key: "read-attempt",
|
|
368
|
+
observe: "At least one attempt. `state: succeeded` with the status your receiver returned is first value: the callback was made, your receiver answered, and the outcome is recorded.",
|
|
369
|
+
operationId: "attempts.list",
|
|
370
|
+
purpose: "Reads the immutable delivery history. This is the point the journey exists for — the customer-readable outcome of a real call to their own address.",
|
|
371
|
+
recovery: [
|
|
372
|
+
{
|
|
373
|
+
nextMove: "The attempt failed. Read `diagnostic` and `remediation`: they name the classified cause and the next action, and `failureSnippet` carries what your receiver said.",
|
|
374
|
+
when: "attempt-failed",
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
nextMove: "Nothing reached your receiver. Confirm the URL answers a POST from the public internet, and that it is not behind an allowlist Greatwait is not on.",
|
|
378
|
+
when: "receiver-unreachable",
|
|
379
|
+
},
|
|
380
|
+
],
|
|
381
|
+
title: "Read the delivery attempt",
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
key: "clean-up",
|
|
385
|
+
observe: "The endpoint in status `disabled`. Nothing further is delivered to it, and it stays readable so the attempts above keep their context.",
|
|
386
|
+
operationId: "endpoints.disable",
|
|
387
|
+
purpose: "Stops delivery to the destination you registered for this walkthrough. Disabling is terminal and deliberate: it leaves the history intact rather than deleting evidence of what happened.",
|
|
388
|
+
recovery: [
|
|
389
|
+
{
|
|
390
|
+
nextMove: "Already gone, or never in this project. Nothing to clean up.",
|
|
391
|
+
when: "not_found",
|
|
392
|
+
},
|
|
393
|
+
],
|
|
394
|
+
title: "Clean up",
|
|
395
|
+
},
|
|
396
|
+
];
|
|
397
|
+
export const FIRST_VALUE_JOURNEY = {
|
|
398
|
+
prerequisites: FIRST_VALUE_PREREQUISITES,
|
|
399
|
+
steps: FIRST_VALUE_STEPS,
|
|
400
|
+
variables: FIRST_VALUE_VARIABLES,
|
|
401
|
+
};
|
|
402
|
+
/** The declared variable of that name, or `undefined`. */
|
|
403
|
+
export function journeyVariable(name) {
|
|
404
|
+
return FIRST_VALUE_VARIABLES.find((variable) => variable.name === name);
|
|
405
|
+
}
|
|
406
|
+
/** Every `{{VARIABLE}}` reference inside a value, in first-appearance order. */
|
|
407
|
+
export function journeyReferences(value) {
|
|
408
|
+
const found = [];
|
|
409
|
+
const walk = (node) => {
|
|
410
|
+
if (typeof node === "string") {
|
|
411
|
+
for (const match of node.matchAll(/\{\{([A-Z0-9_]+)\}\}/gu)) {
|
|
412
|
+
const name = match[1];
|
|
413
|
+
if (!found.includes(name)) {
|
|
414
|
+
found.push(name);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (Array.isArray(node)) {
|
|
420
|
+
for (const entry of node)
|
|
421
|
+
walk(entry);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (node !== null && typeof node === "object") {
|
|
425
|
+
for (const entry of Object.values(node))
|
|
426
|
+
walk(entry);
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
walk(value);
|
|
430
|
+
return found;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Replaces every `{{VARIABLE}}` reference using `resolve`.
|
|
434
|
+
*
|
|
435
|
+
* A whole-string reference becomes the resolved value; an embedded one is
|
|
436
|
+
* substituted in place. An unresolved name throws rather than leaving the
|
|
437
|
+
* braces behind, because a `{{…}}` that reached a published page is a page
|
|
438
|
+
* that teaches a reader to send a literal brace.
|
|
439
|
+
*/
|
|
440
|
+
export function resolveJourneyReferences(value, resolve) {
|
|
441
|
+
const substitute = (text) => text.replace(/\{\{([A-Z0-9_]+)\}\}/gu, (_match, name) => {
|
|
442
|
+
const resolved = resolve(name);
|
|
443
|
+
if (resolved === undefined) {
|
|
444
|
+
throw new Error(`journey references undeclared variable ${name}`);
|
|
445
|
+
}
|
|
446
|
+
return resolved;
|
|
447
|
+
});
|
|
448
|
+
const walk = (node) => {
|
|
449
|
+
if (typeof node === "string")
|
|
450
|
+
return substitute(node);
|
|
451
|
+
if (Array.isArray(node))
|
|
452
|
+
return node.map(walk);
|
|
453
|
+
if (node !== null && typeof node === "object") {
|
|
454
|
+
return Object.fromEntries(Object.entries(node).map(([key, entry]) => [key, walk(entry)]));
|
|
455
|
+
}
|
|
456
|
+
return node;
|
|
457
|
+
};
|
|
458
|
+
return walk(value);
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* The variables a step's own capture list produces, so a consumer can tell a
|
|
462
|
+
* value the reader supplies from one an earlier call handed them.
|
|
463
|
+
*/
|
|
464
|
+
export function journeyCapturedVariables() {
|
|
465
|
+
return FIRST_VALUE_STEPS.flatMap((step) => (step.capture ?? []).map((capture) => capture.variable));
|
|
466
|
+
}
|