@greatwait/contracts 0.1.0-alpha.1 → 0.1.0-alpha.3
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/dist/authority.d.ts +57 -0
- package/dist/authority.js +35 -0
- package/dist/callback-receiver.d.ts +50 -0
- package/dist/callback-receiver.js +51 -0
- package/dist/first-value-journey.d.ts +156 -0
- package/dist/first-value-journey.js +213 -0
- package/dist/identity.d.ts +4 -4
- package/dist/index.d.ts +4 -5
- package/dist/index.js +4 -5
- package/dist/mcp.d.ts +148 -0
- package/dist/mcp.js +158 -0
- package/dist/oauth.js +83 -7
- package/dist/operations.d.ts +157 -3
- package/dist/operations.js +104 -2
- package/dist/scheduling.d.ts +2 -2
- package/dist/test-sink.d.ts +24 -4
- package/dist/test-sink.js +13 -2
- package/generated/v1/callback-receiver.json +1 -1
- package/generated/v1/examples/context.read.response.json +6 -0
- package/generated/v1/examples/credentials.connection.response.json +16 -0
- package/generated/v1/manifest.json +53 -0
- package/generated/v1/openapi.json +690 -1
- package/generated/v1/policy-matrix.json +14 -0
- package/generated/v1/schemas/context.read.response.json +43 -0
- package/generated/v1/schemas/credentials.connection.response.json +64 -0
- package/generated/v1/snippets/attempts.list.json +1 -0
- package/generated/v1/snippets/attempts.read.json +1 -0
- package/generated/v1/snippets/context.read.json +6 -0
- package/generated/v1/snippets/credentials.connection.json +6 -0
- package/generated/v1/snippets/credentials.create.json +1 -0
- package/generated/v1/snippets/credentials.list.json +1 -0
- package/generated/v1/snippets/credentials.revoke.json +1 -0
- package/generated/v1/snippets/credentials.rotate.json +1 -0
- package/generated/v1/snippets/endpoints.create.json +1 -0
- package/generated/v1/snippets/endpoints.disable.json +1 -0
- package/generated/v1/snippets/endpoints.list.json +1 -0
- package/generated/v1/snippets/endpoints.read.json +1 -0
- package/generated/v1/snippets/endpoints.rotateSigningKey.json +1 -0
- package/generated/v1/snippets/endpoints.update.json +1 -0
- package/generated/v1/snippets/endpoints.verify.json +1 -0
- package/generated/v1/snippets/forecast.read.json +1 -0
- package/generated/v1/snippets/forecast.windows.json +1 -0
- package/generated/v1/snippets/grants.list.json +1 -0
- package/generated/v1/snippets/grants.revoke.json +1 -0
- package/generated/v1/snippets/journey/await-occurrence.json +6 -0
- package/generated/v1/snippets/journey/check-credential.json +6 -0
- package/generated/v1/snippets/journey/check-service.json +6 -0
- package/generated/v1/snippets/journey/clean-up.json +6 -0
- package/generated/v1/snippets/journey/create-destination.json +6 -0
- package/generated/v1/snippets/journey/create-one-shot.json +6 -0
- package/generated/v1/snippets/journey/read-attempt.json +6 -0
- package/generated/v1/snippets/journey/read-schedule.json +6 -0
- package/generated/v1/snippets/journey/replay-one-shot.json +6 -0
- package/generated/v1/snippets/meta.scaffold.json +1 -0
- package/generated/v1/snippets/meta.version.json +1 -0
- package/generated/v1/snippets/occurrences.list.json +1 -0
- package/generated/v1/snippets/occurrences.read.json +1 -0
- package/generated/v1/snippets/occurrences.redeliver.json +1 -0
- package/generated/v1/snippets/projects.create.json +1 -0
- package/generated/v1/snippets/projects.list.json +1 -0
- package/generated/v1/snippets/schedules.archive.json +1 -0
- package/generated/v1/snippets/schedules.cancel.json +1 -0
- package/generated/v1/snippets/schedules.create.json +1 -0
- package/generated/v1/snippets/schedules.list.json +1 -0
- package/generated/v1/snippets/schedules.pause.json +1 -0
- package/generated/v1/snippets/schedules.read.json +1 -0
- package/generated/v1/snippets/schedules.replace.json +1 -0
- package/generated/v1/snippets/schedules.resume.json +1 -0
- package/generated/v1/snippets/sinkdeliveries.list.json +1 -0
- package/generated/v1/snippets/sinkdeliveries.read.json +1 -0
- package/generated/v1/snippets/testsinks.configure.json +1 -0
- package/generated/v1/snippets/testsinks.create.json +1 -0
- package/generated/v1/snippets/testsinks.delete.json +1 -0
- package/generated/v1/snippets/testsinks.list.json +1 -0
- package/generated/v1/snippets/testsinks.read.json +1 -0
- package/generated/v1/snippets/usage.read.json +1 -0
- package/package.json +7 -2
- package/dist/webhooks.d.ts +0 -116
- package/dist/webhooks.js +0 -161
package/dist/authority.d.ts
CHANGED
|
@@ -215,3 +215,60 @@ export declare const SESSION_AUTHORITY_HEADERS: {
|
|
|
215
215
|
readonly environment: "Greatwait-Environment";
|
|
216
216
|
readonly project: "Greatwait-Project";
|
|
217
217
|
};
|
|
218
|
+
/**
|
|
219
|
+
* The authority a presented credential carries, answered back to its holder.
|
|
220
|
+
*
|
|
221
|
+
* **Why this exists at all.** The authority tuple is fixed at issue for a key
|
|
222
|
+
* and at consent for a grant — and until W10-01 nothing published it. A holder
|
|
223
|
+
* could act, and could not ask what it was acting *on*. The gap is invisible
|
|
224
|
+
* to a browser, which resolves its own project from a session it can see, and
|
|
225
|
+
* invisible to a documentation page, which tells a reader to substitute values
|
|
226
|
+
* they already know. It is not invisible to a command-line client: after
|
|
227
|
+
* `login` it holds a working token and cannot tell the person in front of it
|
|
228
|
+
* which project it is in, or whether the schedule they are about to create is
|
|
229
|
+
* a `test` one or a `live` one. Both were observable only by listing something
|
|
230
|
+
* the project already owned, which a new project does not have.
|
|
231
|
+
*
|
|
232
|
+
* **No organization, following every other `/v1` response.** Tenancy above the
|
|
233
|
+
* project is not a name this surface publishes, and a caller that cannot name
|
|
234
|
+
* an organization anywhere has no use for one here.
|
|
235
|
+
*
|
|
236
|
+
* **`scopes` is what was granted, not what was asked for and not what it
|
|
237
|
+
* implies.** A grant is capped by the consenting member's role and a key by
|
|
238
|
+
* what the issuer chose, so this is narrower than any request; it is also the
|
|
239
|
+
* *raw* list rather than `effectiveScopes`, which expands a write into its
|
|
240
|
+
* read. Publishing the expansion would make this answer disagree with the
|
|
241
|
+
* credential's own row in `credentials.list` and with what the consent screen
|
|
242
|
+
* showed the member — three places, two answers — and a caller that needs to
|
|
243
|
+
* know whether a scope is implied can apply the same published expansion the
|
|
244
|
+
* service does.
|
|
245
|
+
*/
|
|
246
|
+
export declare const credentialContextSchema: z.ZodObject<{
|
|
247
|
+
environmentKind: z.ZodEnum<{
|
|
248
|
+
test: "test";
|
|
249
|
+
live: "live";
|
|
250
|
+
}>;
|
|
251
|
+
principal: z.ZodEnum<{
|
|
252
|
+
"api-credential": "api-credential";
|
|
253
|
+
oauth: "oauth";
|
|
254
|
+
}>;
|
|
255
|
+
project: z.ZodString;
|
|
256
|
+
scopes: z.ZodArray<z.ZodEnum<{
|
|
257
|
+
"credentials:administer": "credentials:administer";
|
|
258
|
+
"credentials:read": "credentials:read";
|
|
259
|
+
"credentials:write": "credentials:write";
|
|
260
|
+
"endpoints:read": "endpoints:read";
|
|
261
|
+
"endpoints:write": "endpoints:write";
|
|
262
|
+
"forecast:read": "forecast:read";
|
|
263
|
+
"grants:read": "grants:read";
|
|
264
|
+
"grants:write": "grants:write";
|
|
265
|
+
"occurrences:read": "occurrences:read";
|
|
266
|
+
"occurrences:redeliver": "occurrences:redeliver";
|
|
267
|
+
"projects:read": "projects:read";
|
|
268
|
+
"projects:write": "projects:write";
|
|
269
|
+
"schedules:read": "schedules:read";
|
|
270
|
+
"schedules:write": "schedules:write";
|
|
271
|
+
"usage:read": "usage:read";
|
|
272
|
+
}>>;
|
|
273
|
+
}, z.core.$strip>;
|
|
274
|
+
export type CredentialContext = z.infer<typeof credentialContextSchema>;
|
package/dist/authority.js
CHANGED
|
@@ -259,3 +259,38 @@ export const SESSION_AUTHORITY_HEADERS = {
|
|
|
259
259
|
environment: "Greatwait-Environment",
|
|
260
260
|
project: "Greatwait-Project",
|
|
261
261
|
};
|
|
262
|
+
/**
|
|
263
|
+
* The authority a presented credential carries, answered back to its holder.
|
|
264
|
+
*
|
|
265
|
+
* **Why this exists at all.** The authority tuple is fixed at issue for a key
|
|
266
|
+
* and at consent for a grant — and until W10-01 nothing published it. A holder
|
|
267
|
+
* could act, and could not ask what it was acting *on*. The gap is invisible
|
|
268
|
+
* to a browser, which resolves its own project from a session it can see, and
|
|
269
|
+
* invisible to a documentation page, which tells a reader to substitute values
|
|
270
|
+
* they already know. It is not invisible to a command-line client: after
|
|
271
|
+
* `login` it holds a working token and cannot tell the person in front of it
|
|
272
|
+
* which project it is in, or whether the schedule they are about to create is
|
|
273
|
+
* a `test` one or a `live` one. Both were observable only by listing something
|
|
274
|
+
* the project already owned, which a new project does not have.
|
|
275
|
+
*
|
|
276
|
+
* **No organization, following every other `/v1` response.** Tenancy above the
|
|
277
|
+
* project is not a name this surface publishes, and a caller that cannot name
|
|
278
|
+
* an organization anywhere has no use for one here.
|
|
279
|
+
*
|
|
280
|
+
* **`scopes` is what was granted, not what was asked for and not what it
|
|
281
|
+
* implies.** A grant is capped by the consenting member's role and a key by
|
|
282
|
+
* what the issuer chose, so this is narrower than any request; it is also the
|
|
283
|
+
* *raw* list rather than `effectiveScopes`, which expands a write into its
|
|
284
|
+
* read. Publishing the expansion would make this answer disagree with the
|
|
285
|
+
* credential's own row in `credentials.list` and with what the consent screen
|
|
286
|
+
* showed the member — three places, two answers — and a caller that needs to
|
|
287
|
+
* know whether a scope is implied can apply the same published expansion the
|
|
288
|
+
* service does.
|
|
289
|
+
*/
|
|
290
|
+
export const credentialContextSchema = z.object({
|
|
291
|
+
environmentKind: environmentKindSchema,
|
|
292
|
+
/** How this credential authenticated. Never `session`: see the operation. */
|
|
293
|
+
principal: z.enum(["api-credential", "oauth"]),
|
|
294
|
+
project: projectIdSchema,
|
|
295
|
+
scopes: z.array(scopeSchema).min(1),
|
|
296
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounds a receiver applies to the deliveries this service sends it.
|
|
3
|
+
*
|
|
4
|
+
* **A receiver's bounds are not the sender's, and the shipped SDK used the
|
|
5
|
+
* sender's.** `createNodeCallbackMiddleware` and the receiver conformance suite
|
|
6
|
+
* both defaulted their body limit to `TEST_SINK_BOUNDS.maxRequestBodyBytes` —
|
|
7
|
+
* a number whose every neighbour is justified by the test sink running *inside
|
|
8
|
+
* the API Worker's request budget, on the product's own account*. None of that
|
|
9
|
+
* reasoning is about a customer's own server, and the value it produced was
|
|
10
|
+
* wrong for one.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Headroom for the envelope wrapped around a payload.
|
|
14
|
+
*
|
|
15
|
+
* A receiver is sent an envelope, not a payload: version, type, schedule and
|
|
16
|
+
* occurrence identifiers, the planned time, the attempt ordinal, the trigger
|
|
17
|
+
* that says why the delivery exists, and in `callback` mode a completion URL,
|
|
18
|
+
* bearer token and deadline. All of that is additive to `payloadBytes`.
|
|
19
|
+
*
|
|
20
|
+
* **Measured, not chosen.** Serializing every published delivery test vector
|
|
21
|
+
* around a payload at exactly `SCHEDULING_BOUNDS.payloadBytes`, with a 180-
|
|
22
|
+
* character completion host and a 512-character token — both far past anything
|
|
23
|
+
* the service mints — the largest envelope exceeded the payload maximum by
|
|
24
|
+
* 1,097 bytes. `callback-receiver.test.ts` re-derives that rather than trusting
|
|
25
|
+
* this sentence, so an envelope that grows past the headroom fails a test
|
|
26
|
+
* instead of silently costing a customer their largest deliveries.
|
|
27
|
+
*
|
|
28
|
+
* Four kibibytes is roughly four times the measured worst case, which is slack
|
|
29
|
+
* enough to absorb a new envelope field and tight enough that this is still a
|
|
30
|
+
* bound rather than a shrug.
|
|
31
|
+
*/
|
|
32
|
+
export declare const CALLBACK_ENVELOPE_HEADROOM_BYTES = 4096;
|
|
33
|
+
/**
|
|
34
|
+
* The largest callback body a conforming receiver reads before refusing.
|
|
35
|
+
*
|
|
36
|
+
* Derived rather than written down, because the thing that must stay true is
|
|
37
|
+
* the *relationship*: a receiver has to accept the largest payload this service
|
|
38
|
+
* will accept, plus whatever the envelope adds. A literal here would be a
|
|
39
|
+
* second statement of `payloadBytes` and would go stale the first time that
|
|
40
|
+
* moved.
|
|
41
|
+
*
|
|
42
|
+
* **The defect this replaces was live in `0.1.0-alpha.1`.** The old default was
|
|
43
|
+
* 65,536 — exactly `payloadBytes`, and therefore below every envelope carrying
|
|
44
|
+
* a maximum payload. A customer scheduling their largest legal payload against
|
|
45
|
+
* a receiver built with the SDK's defaults got `413 body_too_large`, which the
|
|
46
|
+
* sender classifies as terminal: the occurrence fails and is not retried. The
|
|
47
|
+
* bound was reachable by anyone using both the documented maximum and the
|
|
48
|
+
* documented defaults.
|
|
49
|
+
*/
|
|
50
|
+
export declare const CALLBACK_RECEIVER_MAX_BODY_BYTES: number;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { SCHEDULING_BOUNDS } from "./scheduling.js";
|
|
2
|
+
/**
|
|
3
|
+
* Bounds a receiver applies to the deliveries this service sends it.
|
|
4
|
+
*
|
|
5
|
+
* **A receiver's bounds are not the sender's, and the shipped SDK used the
|
|
6
|
+
* sender's.** `createNodeCallbackMiddleware` and the receiver conformance suite
|
|
7
|
+
* both defaulted their body limit to `TEST_SINK_BOUNDS.maxRequestBodyBytes` —
|
|
8
|
+
* a number whose every neighbour is justified by the test sink running *inside
|
|
9
|
+
* the API Worker's request budget, on the product's own account*. None of that
|
|
10
|
+
* reasoning is about a customer's own server, and the value it produced was
|
|
11
|
+
* wrong for one.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Headroom for the envelope wrapped around a payload.
|
|
15
|
+
*
|
|
16
|
+
* A receiver is sent an envelope, not a payload: version, type, schedule and
|
|
17
|
+
* occurrence identifiers, the planned time, the attempt ordinal, the trigger
|
|
18
|
+
* that says why the delivery exists, and in `callback` mode a completion URL,
|
|
19
|
+
* bearer token and deadline. All of that is additive to `payloadBytes`.
|
|
20
|
+
*
|
|
21
|
+
* **Measured, not chosen.** Serializing every published delivery test vector
|
|
22
|
+
* around a payload at exactly `SCHEDULING_BOUNDS.payloadBytes`, with a 180-
|
|
23
|
+
* character completion host and a 512-character token — both far past anything
|
|
24
|
+
* the service mints — the largest envelope exceeded the payload maximum by
|
|
25
|
+
* 1,097 bytes. `callback-receiver.test.ts` re-derives that rather than trusting
|
|
26
|
+
* this sentence, so an envelope that grows past the headroom fails a test
|
|
27
|
+
* instead of silently costing a customer their largest deliveries.
|
|
28
|
+
*
|
|
29
|
+
* Four kibibytes is roughly four times the measured worst case, which is slack
|
|
30
|
+
* enough to absorb a new envelope field and tight enough that this is still a
|
|
31
|
+
* bound rather than a shrug.
|
|
32
|
+
*/
|
|
33
|
+
export const CALLBACK_ENVELOPE_HEADROOM_BYTES = 4_096;
|
|
34
|
+
/**
|
|
35
|
+
* The largest callback body a conforming receiver reads before refusing.
|
|
36
|
+
*
|
|
37
|
+
* Derived rather than written down, because the thing that must stay true is
|
|
38
|
+
* the *relationship*: a receiver has to accept the largest payload this service
|
|
39
|
+
* will accept, plus whatever the envelope adds. A literal here would be a
|
|
40
|
+
* second statement of `payloadBytes` and would go stale the first time that
|
|
41
|
+
* moved.
|
|
42
|
+
*
|
|
43
|
+
* **The defect this replaces was live in `0.1.0-alpha.1`.** The old default was
|
|
44
|
+
* 65,536 — exactly `payloadBytes`, and therefore below every envelope carrying
|
|
45
|
+
* a maximum payload. A customer scheduling their largest legal payload against
|
|
46
|
+
* a receiver built with the SDK's defaults got `413 body_too_large`, which the
|
|
47
|
+
* sender classifies as terminal: the occurrence fails and is not retried. The
|
|
48
|
+
* bound was reachable by anyone using both the documented maximum and the
|
|
49
|
+
* documented defaults.
|
|
50
|
+
*/
|
|
51
|
+
export const CALLBACK_RECEIVER_MAX_BODY_BYTES = SCHEDULING_BOUNDS.payloadBytes + CALLBACK_ENVELOPE_HEADROOM_BYTES;
|
|
@@ -303,3 +303,159 @@ export declare function resolveJourneyReferences<T>(value: T, resolve: (name: st
|
|
|
303
303
|
* value the reader supplies from one an earlier call handed them.
|
|
304
304
|
*/
|
|
305
305
|
export declare function journeyCapturedVariables(): readonly string[];
|
|
306
|
+
/**
|
|
307
|
+
* The website's rendering of the same journey.
|
|
308
|
+
*
|
|
309
|
+
* **Declared here rather than in the dashboard, because it is the *binding*
|
|
310
|
+
* between two things that already exist and neither of them may own it.** The
|
|
311
|
+
* dashboard has a four-step onboarding flow derived from what a member's
|
|
312
|
+
* project contains; this file has a nine-step sequence derived from the
|
|
313
|
+
* operation registry. Left unrelated they answered the same question with
|
|
314
|
+
* different words and different examples, and the website's answer was the one
|
|
315
|
+
* that went stale: its "Schedule a one-shot" step rendered the registry's
|
|
316
|
+
* `schedules.create` example, which is a **recurring cron** — a live dashboard
|
|
317
|
+
* action showing a reader the one thing the step promises it is not.
|
|
318
|
+
*
|
|
319
|
+
* So a website step names journey steps, and never a body, a title or a
|
|
320
|
+
* sentence of its own. What the browser prints is what the curl page prints,
|
|
321
|
+
* because it is the same declaration rendered twice.
|
|
322
|
+
*
|
|
323
|
+
* **`verified` maps to nothing, deliberately.** The ownership challenge is
|
|
324
|
+
* optional and off by default since W4-09, so the journey does not perform it;
|
|
325
|
+
* a deployment that turns it on gets the step from
|
|
326
|
+
* `endpoints.verify`'s own snippet. Mapping it to a journey step that does not
|
|
327
|
+
* exist would be worse than mapping it to none.
|
|
328
|
+
*/
|
|
329
|
+
export declare const WEBSITE_JOURNEY_STEP_KEYS: Readonly<Record<string, readonly string[]>>;
|
|
330
|
+
/** The journey steps a website step renders, resolved against the sequence. */
|
|
331
|
+
export declare function websiteJourneySteps(step: string): readonly JourneyStep[];
|
|
332
|
+
/**
|
|
333
|
+
* The bounded wait the website inherits, rather than one it chose.
|
|
334
|
+
*
|
|
335
|
+
* W9-01's fourth criterion asks the browser to bound outcome polling and to
|
|
336
|
+
* leave a recovery action on timeout. The journey already declares both numbers
|
|
337
|
+
* on `await-occurrence`, and the curl and JavaScript quickstarts already wait
|
|
338
|
+
* exactly that long — so a browser that picked its own interval would be a
|
|
339
|
+
* third answer to a question with one published answer, and the reader who
|
|
340
|
+
* followed both would be told two different things about how long to wait.
|
|
341
|
+
*/
|
|
342
|
+
export declare function journeyPoll(key: string): JourneyPoll;
|
|
343
|
+
/**
|
|
344
|
+
* What the reader has to bring, in the words the quickstarts already use.
|
|
345
|
+
*
|
|
346
|
+
* The website used to write its own: "anything that can take an HTTP POST. A
|
|
347
|
+
* webhook playground is fine for a first look." That sentence steered a new
|
|
348
|
+
* customer to paste their first callback into a third-party inspection service,
|
|
349
|
+
* which is the thing this project's receiver decision exists to refuse, and it
|
|
350
|
+
* disagreed with the curl page on the same screenful of product.
|
|
351
|
+
*
|
|
352
|
+
* The operator's decision on W9-01 was that the *invitation* says none of this
|
|
353
|
+
* — a prerequisite stated before somebody has any context is noise they cannot
|
|
354
|
+
* act on. It is said here instead, at the step that needs it.
|
|
355
|
+
*/
|
|
356
|
+
export declare function journeyPrerequisite(id: string): JourneyPrerequisite;
|
|
357
|
+
/**
|
|
358
|
+
* How long a website step took, as a bucket and never as a number.
|
|
359
|
+
*
|
|
360
|
+
* Coarser than `MCP_ACTIVATION_DURATIONS`, because these measure different
|
|
361
|
+
* things: a connection test is a handshake and finishes in seconds, and a
|
|
362
|
+
* person adding a destination and scheduling a callback is doing minutes of
|
|
363
|
+
* work. Reusing the millisecond buckets would put every website step in
|
|
364
|
+
* `over_30s` and record nothing.
|
|
365
|
+
*/
|
|
366
|
+
export declare const JOURNEY_ACTIVATION_DURATIONS: readonly ["under_1m", "under_5m", "under_15m", "over_15m"];
|
|
367
|
+
export type JourneyActivationDuration = (typeof JOURNEY_ACTIVATION_DURATIONS)[number];
|
|
368
|
+
export declare function journeyActivationDuration(elapsedMs: number): JourneyActivationDuration;
|
|
369
|
+
/** Every way a website step can end, drawn from a closed list. */
|
|
370
|
+
export declare const JOURNEY_ACTIVATION_OUTCOMES: readonly ["reached", "refused", "timed_out", "failed"];
|
|
371
|
+
export type JourneyActivationOutcome = (typeof JOURNEY_ACTIVATION_OUTCOMES)[number];
|
|
372
|
+
/** The website steps an activation fact may name. */
|
|
373
|
+
export declare const WEBSITE_JOURNEY_STEPS: readonly ["destination", "verified", "scheduled", "delivered"];
|
|
374
|
+
/**
|
|
375
|
+
* What is recorded when somebody moves through the website journey, and the
|
|
376
|
+
* only thing that is.
|
|
377
|
+
*
|
|
378
|
+
* **Modelled on `mcpActivationEventSchema` down to the `strictObject`, and for
|
|
379
|
+
* the same reason.** Every field is drawn from a closed set: there is no
|
|
380
|
+
* free-text field, no identifier, no address, no URL and no measurement finer
|
|
381
|
+
* than a bucket, so an event of this shape cannot carry an account, an
|
|
382
|
+
* organization, a project, an invitation link, a token, a destination or a
|
|
383
|
+
* payload however carelessly it is constructed.
|
|
384
|
+
*
|
|
385
|
+
* Strict rather than merely typed. An ordinary object schema *strips* what it
|
|
386
|
+
* does not know, so an emitter that added `project` would look like it was
|
|
387
|
+
* recording one while the field silently vanished — safe and dishonest at once.
|
|
388
|
+
* The MCP schema learned that from a test and this one inherits the lesson
|
|
389
|
+
* rather than rediscovering it.
|
|
390
|
+
*/
|
|
391
|
+
export declare const websiteActivationEventSchema: z.ZodObject<{
|
|
392
|
+
duration: z.ZodEnum<{
|
|
393
|
+
under_1m: "under_1m";
|
|
394
|
+
under_5m: "under_5m";
|
|
395
|
+
under_15m: "under_15m";
|
|
396
|
+
over_15m: "over_15m";
|
|
397
|
+
}>;
|
|
398
|
+
outcome: z.ZodEnum<{
|
|
399
|
+
failed: "failed";
|
|
400
|
+
timed_out: "timed_out";
|
|
401
|
+
refused: "refused";
|
|
402
|
+
reached: "reached";
|
|
403
|
+
}>;
|
|
404
|
+
step: z.ZodEnum<{
|
|
405
|
+
verified: "verified";
|
|
406
|
+
delivered: "delivered";
|
|
407
|
+
destination: "destination";
|
|
408
|
+
scheduled: "scheduled";
|
|
409
|
+
}>;
|
|
410
|
+
type: z.ZodLiteral<"website.journey_step">;
|
|
411
|
+
}, z.core.$strict>;
|
|
412
|
+
export type WebsiteActivationEvent = z.infer<typeof websiteActivationEventSchema>;
|
|
413
|
+
/**
|
|
414
|
+
* Parses an activation fact before it leaves the browser.
|
|
415
|
+
*
|
|
416
|
+
* The parse is the control, exactly as it is on the MCP side: a caller hands
|
|
417
|
+
* this an object and the schema refuses anything the closed sets do not admit,
|
|
418
|
+
* so the mistake surfaces where it was made rather than as a field that quietly
|
|
419
|
+
* disappeared.
|
|
420
|
+
*/
|
|
421
|
+
export declare function parseWebsiteActivationEvent(value: unknown): WebsiteActivationEvent;
|
|
422
|
+
/**
|
|
423
|
+
* A path parameter's name, as the journey's variables spell it.
|
|
424
|
+
*
|
|
425
|
+
* **Published because a machine consumer cannot run the journey without it.**
|
|
426
|
+
* A step names its operation and the variables it needs — `read-schedule` says
|
|
427
|
+
* `/v1/schedules/{scheduleId}` and, separately, `GREATWAIT_SCHEDULEID` — and
|
|
428
|
+
* the rule joining the two lived in `scripts/first-value-projection.ts`, which
|
|
429
|
+
* is not published. A documentation page never notices: it renders both halves
|
|
430
|
+
* as text and a human joins them by eye. A command-line client has to *fill*
|
|
431
|
+
* the parameter, and had to re-derive the convention by guessing at it.
|
|
432
|
+
*
|
|
433
|
+
* The transformation is deliberately total and lossy in the safe direction:
|
|
434
|
+
* every non-alphanumeric character becomes `_`, so a parameter that differs
|
|
435
|
+
* from another only in punctuation would collide. `journeyPathVariables` is
|
|
436
|
+
* where that would surface, and `first-value-journey.test.ts` holds the
|
|
437
|
+
* journey's own parameters to distinct names.
|
|
438
|
+
*/
|
|
439
|
+
export declare function journeyVariableName(parameter: string): string;
|
|
440
|
+
/** Every `{parameter}` in a path, in the order the path names them. */
|
|
441
|
+
export declare function pathParameterNames(path: string): readonly string[];
|
|
442
|
+
/**
|
|
443
|
+
* The variables a step's path parameters are filled from, in path order.
|
|
444
|
+
*
|
|
445
|
+
* Order is the contract: a client filling positional arguments — which is what
|
|
446
|
+
* the generated client takes — needs them in the order the path names them,
|
|
447
|
+
* and `attempts.list` has two.
|
|
448
|
+
*/
|
|
449
|
+
export declare function journeyPathVariables(step: JourneyStep): readonly string[];
|
|
450
|
+
/**
|
|
451
|
+
* Everything a step must be given before it can be performed.
|
|
452
|
+
*
|
|
453
|
+
* **Published because it is a join, and a join is where a second consumer
|
|
454
|
+
* invents its own copy.** A step's inputs come from three places — the
|
|
455
|
+
* operation's path parameters, the `{{…}}` references in its body, query and
|
|
456
|
+
* headers — and stitching them is the kind of two-line rule every consumer gets
|
|
457
|
+
* *nearly* right. The machine-readable artifact's own `variables` field is this
|
|
458
|
+
* function, so a client reading the artifact and a client reading this module
|
|
459
|
+
* cannot disagree about what a step needs.
|
|
460
|
+
*/
|
|
461
|
+
export declare function journeyStepVariables(step: JourneyStep): readonly string[];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { findOperation } from "./operations.js";
|
|
2
3
|
/**
|
|
3
4
|
* One thing that can go wrong at a step, and the next move.
|
|
4
5
|
*
|
|
@@ -464,3 +465,215 @@ export function resolveJourneyReferences(value, resolve) {
|
|
|
464
465
|
export function journeyCapturedVariables() {
|
|
465
466
|
return FIRST_VALUE_STEPS.flatMap((step) => (step.capture ?? []).map((capture) => capture.variable));
|
|
466
467
|
}
|
|
468
|
+
/**
|
|
469
|
+
* The website's rendering of the same journey.
|
|
470
|
+
*
|
|
471
|
+
* **Declared here rather than in the dashboard, because it is the *binding*
|
|
472
|
+
* between two things that already exist and neither of them may own it.** The
|
|
473
|
+
* dashboard has a four-step onboarding flow derived from what a member's
|
|
474
|
+
* project contains; this file has a nine-step sequence derived from the
|
|
475
|
+
* operation registry. Left unrelated they answered the same question with
|
|
476
|
+
* different words and different examples, and the website's answer was the one
|
|
477
|
+
* that went stale: its "Schedule a one-shot" step rendered the registry's
|
|
478
|
+
* `schedules.create` example, which is a **recurring cron** — a live dashboard
|
|
479
|
+
* action showing a reader the one thing the step promises it is not.
|
|
480
|
+
*
|
|
481
|
+
* So a website step names journey steps, and never a body, a title or a
|
|
482
|
+
* sentence of its own. What the browser prints is what the curl page prints,
|
|
483
|
+
* because it is the same declaration rendered twice.
|
|
484
|
+
*
|
|
485
|
+
* **`verified` maps to nothing, deliberately.** The ownership challenge is
|
|
486
|
+
* optional and off by default since W4-09, so the journey does not perform it;
|
|
487
|
+
* a deployment that turns it on gets the step from
|
|
488
|
+
* `endpoints.verify`'s own snippet. Mapping it to a journey step that does not
|
|
489
|
+
* exist would be worse than mapping it to none.
|
|
490
|
+
*/
|
|
491
|
+
export const WEBSITE_JOURNEY_STEP_KEYS = {
|
|
492
|
+
delivered: ["await-occurrence", "read-attempt"],
|
|
493
|
+
destination: ["create-destination"],
|
|
494
|
+
scheduled: ["create-one-shot"],
|
|
495
|
+
verified: [],
|
|
496
|
+
};
|
|
497
|
+
/** The journey steps a website step renders, resolved against the sequence. */
|
|
498
|
+
export function websiteJourneySteps(step) {
|
|
499
|
+
const keys = WEBSITE_JOURNEY_STEP_KEYS[step] ?? [];
|
|
500
|
+
return keys.map((key) => {
|
|
501
|
+
const found = FIRST_VALUE_STEPS.find((entry) => entry.key === key);
|
|
502
|
+
if (found === undefined) {
|
|
503
|
+
// Not reachable from the constant above, and that is exactly why it
|
|
504
|
+
// throws: the map is hand-written and the sequence is not, so a step
|
|
505
|
+
// renamed in `FIRST_VALUE_STEPS` must fail loudly here rather than
|
|
506
|
+
// silently render one fewer call than the page promises.
|
|
507
|
+
throw new Error(`website journey names unknown step ${key}`);
|
|
508
|
+
}
|
|
509
|
+
return found;
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* The bounded wait the website inherits, rather than one it chose.
|
|
514
|
+
*
|
|
515
|
+
* W9-01's fourth criterion asks the browser to bound outcome polling and to
|
|
516
|
+
* leave a recovery action on timeout. The journey already declares both numbers
|
|
517
|
+
* on `await-occurrence`, and the curl and JavaScript quickstarts already wait
|
|
518
|
+
* exactly that long — so a browser that picked its own interval would be a
|
|
519
|
+
* third answer to a question with one published answer, and the reader who
|
|
520
|
+
* followed both would be told two different things about how long to wait.
|
|
521
|
+
*/
|
|
522
|
+
export function journeyPoll(key) {
|
|
523
|
+
const step = FIRST_VALUE_STEPS.find((entry) => entry.key === key);
|
|
524
|
+
if (step?.poll === undefined) {
|
|
525
|
+
throw new Error(`journey step ${key} declares no bounded wait`);
|
|
526
|
+
}
|
|
527
|
+
return step.poll;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* What the reader has to bring, in the words the quickstarts already use.
|
|
531
|
+
*
|
|
532
|
+
* The website used to write its own: "anything that can take an HTTP POST. A
|
|
533
|
+
* webhook playground is fine for a first look." That sentence steered a new
|
|
534
|
+
* customer to paste their first callback into a third-party inspection service,
|
|
535
|
+
* which is the thing this project's receiver decision exists to refuse, and it
|
|
536
|
+
* disagreed with the curl page on the same screenful of product.
|
|
537
|
+
*
|
|
538
|
+
* The operator's decision on W9-01 was that the *invitation* says none of this
|
|
539
|
+
* — a prerequisite stated before somebody has any context is noise they cannot
|
|
540
|
+
* act on. It is said here instead, at the step that needs it.
|
|
541
|
+
*/
|
|
542
|
+
export function journeyPrerequisite(id) {
|
|
543
|
+
const found = FIRST_VALUE_PREREQUISITES.find((entry) => entry.id === id);
|
|
544
|
+
if (found === undefined) {
|
|
545
|
+
throw new Error(`journey declares no prerequisite ${id}`);
|
|
546
|
+
}
|
|
547
|
+
return found;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* How long a website step took, as a bucket and never as a number.
|
|
551
|
+
*
|
|
552
|
+
* Coarser than `MCP_ACTIVATION_DURATIONS`, because these measure different
|
|
553
|
+
* things: a connection test is a handshake and finishes in seconds, and a
|
|
554
|
+
* person adding a destination and scheduling a callback is doing minutes of
|
|
555
|
+
* work. Reusing the millisecond buckets would put every website step in
|
|
556
|
+
* `over_30s` and record nothing.
|
|
557
|
+
*/
|
|
558
|
+
export const JOURNEY_ACTIVATION_DURATIONS = [
|
|
559
|
+
"under_1m",
|
|
560
|
+
"under_5m",
|
|
561
|
+
"under_15m",
|
|
562
|
+
"over_15m",
|
|
563
|
+
];
|
|
564
|
+
export function journeyActivationDuration(elapsedMs) {
|
|
565
|
+
if (elapsedMs < 60_000)
|
|
566
|
+
return "under_1m";
|
|
567
|
+
if (elapsedMs < 300_000)
|
|
568
|
+
return "under_5m";
|
|
569
|
+
if (elapsedMs < 900_000)
|
|
570
|
+
return "under_15m";
|
|
571
|
+
return "over_15m";
|
|
572
|
+
}
|
|
573
|
+
/** Every way a website step can end, drawn from a closed list. */
|
|
574
|
+
export const JOURNEY_ACTIVATION_OUTCOMES = [
|
|
575
|
+
"reached",
|
|
576
|
+
"refused",
|
|
577
|
+
"timed_out",
|
|
578
|
+
"failed",
|
|
579
|
+
];
|
|
580
|
+
/** The website steps an activation fact may name. */
|
|
581
|
+
export const WEBSITE_JOURNEY_STEPS = [
|
|
582
|
+
"destination",
|
|
583
|
+
"verified",
|
|
584
|
+
"scheduled",
|
|
585
|
+
"delivered",
|
|
586
|
+
];
|
|
587
|
+
/**
|
|
588
|
+
* What is recorded when somebody moves through the website journey, and the
|
|
589
|
+
* only thing that is.
|
|
590
|
+
*
|
|
591
|
+
* **Modelled on `mcpActivationEventSchema` down to the `strictObject`, and for
|
|
592
|
+
* the same reason.** Every field is drawn from a closed set: there is no
|
|
593
|
+
* free-text field, no identifier, no address, no URL and no measurement finer
|
|
594
|
+
* than a bucket, so an event of this shape cannot carry an account, an
|
|
595
|
+
* organization, a project, an invitation link, a token, a destination or a
|
|
596
|
+
* payload however carelessly it is constructed.
|
|
597
|
+
*
|
|
598
|
+
* Strict rather than merely typed. An ordinary object schema *strips* what it
|
|
599
|
+
* does not know, so an emitter that added `project` would look like it was
|
|
600
|
+
* recording one while the field silently vanished — safe and dishonest at once.
|
|
601
|
+
* The MCP schema learned that from a test and this one inherits the lesson
|
|
602
|
+
* rather than rediscovering it.
|
|
603
|
+
*/
|
|
604
|
+
export const websiteActivationEventSchema = z.strictObject({
|
|
605
|
+
duration: z.enum(JOURNEY_ACTIVATION_DURATIONS),
|
|
606
|
+
outcome: z.enum(JOURNEY_ACTIVATION_OUTCOMES),
|
|
607
|
+
step: z.enum(WEBSITE_JOURNEY_STEPS),
|
|
608
|
+
type: z.literal("website.journey_step"),
|
|
609
|
+
});
|
|
610
|
+
/**
|
|
611
|
+
* Parses an activation fact before it leaves the browser.
|
|
612
|
+
*
|
|
613
|
+
* The parse is the control, exactly as it is on the MCP side: a caller hands
|
|
614
|
+
* this an object and the schema refuses anything the closed sets do not admit,
|
|
615
|
+
* so the mistake surfaces where it was made rather than as a field that quietly
|
|
616
|
+
* disappeared.
|
|
617
|
+
*/
|
|
618
|
+
export function parseWebsiteActivationEvent(value) {
|
|
619
|
+
return websiteActivationEventSchema.parse(value);
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* A path parameter's name, as the journey's variables spell it.
|
|
623
|
+
*
|
|
624
|
+
* **Published because a machine consumer cannot run the journey without it.**
|
|
625
|
+
* A step names its operation and the variables it needs — `read-schedule` says
|
|
626
|
+
* `/v1/schedules/{scheduleId}` and, separately, `GREATWAIT_SCHEDULEID` — and
|
|
627
|
+
* the rule joining the two lived in `scripts/first-value-projection.ts`, which
|
|
628
|
+
* is not published. A documentation page never notices: it renders both halves
|
|
629
|
+
* as text and a human joins them by eye. A command-line client has to *fill*
|
|
630
|
+
* the parameter, and had to re-derive the convention by guessing at it.
|
|
631
|
+
*
|
|
632
|
+
* The transformation is deliberately total and lossy in the safe direction:
|
|
633
|
+
* every non-alphanumeric character becomes `_`, so a parameter that differs
|
|
634
|
+
* from another only in punctuation would collide. `journeyPathVariables` is
|
|
635
|
+
* where that would surface, and `first-value-journey.test.ts` holds the
|
|
636
|
+
* journey's own parameters to distinct names.
|
|
637
|
+
*/
|
|
638
|
+
export function journeyVariableName(parameter) {
|
|
639
|
+
return `GREATWAIT_${parameter.toUpperCase().replace(/[^A-Z0-9]/gu, "_")}`;
|
|
640
|
+
}
|
|
641
|
+
/** Every `{parameter}` in a path, in the order the path names them. */
|
|
642
|
+
export function pathParameterNames(path) {
|
|
643
|
+
return [...path.matchAll(/\{([^}]+)\}/gu)].map((match) => match[1]);
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* The variables a step's path parameters are filled from, in path order.
|
|
647
|
+
*
|
|
648
|
+
* Order is the contract: a client filling positional arguments — which is what
|
|
649
|
+
* the generated client takes — needs them in the order the path names them,
|
|
650
|
+
* and `attempts.list` has two.
|
|
651
|
+
*/
|
|
652
|
+
export function journeyPathVariables(step) {
|
|
653
|
+
const operation = findOperation(step.operationId);
|
|
654
|
+
if (operation === undefined) {
|
|
655
|
+
throw new Error(`the first-value journey names ${step.operationId}, which is not an operation`);
|
|
656
|
+
}
|
|
657
|
+
return pathParameterNames(operation.path).map(journeyVariableName);
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Everything a step must be given before it can be performed.
|
|
661
|
+
*
|
|
662
|
+
* **Published because it is a join, and a join is where a second consumer
|
|
663
|
+
* invents its own copy.** A step's inputs come from three places — the
|
|
664
|
+
* operation's path parameters, the `{{…}}` references in its body, query and
|
|
665
|
+
* headers — and stitching them is the kind of two-line rule every consumer gets
|
|
666
|
+
* *nearly* right. The machine-readable artifact's own `variables` field is this
|
|
667
|
+
* function, so a client reading the artifact and a client reading this module
|
|
668
|
+
* cannot disagree about what a step needs.
|
|
669
|
+
*/
|
|
670
|
+
export function journeyStepVariables(step) {
|
|
671
|
+
return [
|
|
672
|
+
...new Set([
|
|
673
|
+
...journeyPathVariables(step),
|
|
674
|
+
...journeyReferences(step.request),
|
|
675
|
+
...journeyReferences(step.query),
|
|
676
|
+
...journeyReferences(step.requestHeaders),
|
|
677
|
+
]),
|
|
678
|
+
];
|
|
679
|
+
}
|
package/dist/identity.d.ts
CHANGED
|
@@ -57,8 +57,8 @@ export type ExternalMembershipId = z.infer<typeof externalMembershipIdSchema>;
|
|
|
57
57
|
*/
|
|
58
58
|
export declare const MEMBERSHIP_ROLES: readonly ["admin", "member"];
|
|
59
59
|
export declare const membershipRoleSchema: z.ZodEnum<{
|
|
60
|
-
admin: "admin";
|
|
61
60
|
member: "member";
|
|
61
|
+
admin: "admin";
|
|
62
62
|
}>;
|
|
63
63
|
export type MembershipRole = z.infer<typeof membershipRoleSchema>;
|
|
64
64
|
/**
|
|
@@ -99,8 +99,8 @@ export declare const membershipSchema: z.ZodObject<{
|
|
|
99
99
|
id: z.ZodString;
|
|
100
100
|
organization: z.ZodString;
|
|
101
101
|
role: z.ZodEnum<{
|
|
102
|
-
admin: "admin";
|
|
103
102
|
member: "member";
|
|
103
|
+
admin: "admin";
|
|
104
104
|
}>;
|
|
105
105
|
status: z.ZodEnum<{
|
|
106
106
|
removed: "removed";
|
|
@@ -140,8 +140,8 @@ export declare const membershipChangeSchema: z.ZodObject<{
|
|
|
140
140
|
id: z.ZodString;
|
|
141
141
|
organization: z.ZodString;
|
|
142
142
|
role: z.ZodEnum<{
|
|
143
|
-
admin: "admin";
|
|
144
143
|
member: "member";
|
|
144
|
+
admin: "admin";
|
|
145
145
|
}>;
|
|
146
146
|
status: z.ZodEnum<{
|
|
147
147
|
removed: "removed";
|
|
@@ -164,8 +164,8 @@ export declare const sessionMembershipSchema: z.ZodObject<{
|
|
|
164
164
|
assertedAt: z.ZodNumber;
|
|
165
165
|
organization: z.ZodString;
|
|
166
166
|
role: z.ZodEnum<{
|
|
167
|
-
admin: "admin";
|
|
168
167
|
member: "member";
|
|
168
|
+
admin: "admin";
|
|
169
169
|
}>;
|
|
170
170
|
user: z.ZodString;
|
|
171
171
|
}, z.core.$strip>;
|