@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.
Files changed (79) hide show
  1. package/dist/authority.d.ts +57 -0
  2. package/dist/authority.js +35 -0
  3. package/dist/callback-receiver.d.ts +50 -0
  4. package/dist/callback-receiver.js +51 -0
  5. package/dist/first-value-journey.d.ts +156 -0
  6. package/dist/first-value-journey.js +213 -0
  7. package/dist/identity.d.ts +4 -4
  8. package/dist/index.d.ts +4 -5
  9. package/dist/index.js +4 -5
  10. package/dist/mcp.d.ts +148 -0
  11. package/dist/mcp.js +158 -0
  12. package/dist/oauth.js +83 -7
  13. package/dist/operations.d.ts +157 -3
  14. package/dist/operations.js +104 -2
  15. package/dist/scheduling.d.ts +2 -2
  16. package/dist/test-sink.d.ts +24 -4
  17. package/dist/test-sink.js +13 -2
  18. package/generated/v1/callback-receiver.json +1 -1
  19. package/generated/v1/examples/context.read.response.json +6 -0
  20. package/generated/v1/examples/credentials.connection.response.json +16 -0
  21. package/generated/v1/manifest.json +53 -0
  22. package/generated/v1/openapi.json +690 -1
  23. package/generated/v1/policy-matrix.json +14 -0
  24. package/generated/v1/schemas/context.read.response.json +43 -0
  25. package/generated/v1/schemas/credentials.connection.response.json +64 -0
  26. package/generated/v1/snippets/attempts.list.json +1 -0
  27. package/generated/v1/snippets/attempts.read.json +1 -0
  28. package/generated/v1/snippets/context.read.json +6 -0
  29. package/generated/v1/snippets/credentials.connection.json +6 -0
  30. package/generated/v1/snippets/credentials.create.json +1 -0
  31. package/generated/v1/snippets/credentials.list.json +1 -0
  32. package/generated/v1/snippets/credentials.revoke.json +1 -0
  33. package/generated/v1/snippets/credentials.rotate.json +1 -0
  34. package/generated/v1/snippets/endpoints.create.json +1 -0
  35. package/generated/v1/snippets/endpoints.disable.json +1 -0
  36. package/generated/v1/snippets/endpoints.list.json +1 -0
  37. package/generated/v1/snippets/endpoints.read.json +1 -0
  38. package/generated/v1/snippets/endpoints.rotateSigningKey.json +1 -0
  39. package/generated/v1/snippets/endpoints.update.json +1 -0
  40. package/generated/v1/snippets/endpoints.verify.json +1 -0
  41. package/generated/v1/snippets/forecast.read.json +1 -0
  42. package/generated/v1/snippets/forecast.windows.json +1 -0
  43. package/generated/v1/snippets/grants.list.json +1 -0
  44. package/generated/v1/snippets/grants.revoke.json +1 -0
  45. package/generated/v1/snippets/journey/await-occurrence.json +6 -0
  46. package/generated/v1/snippets/journey/check-credential.json +6 -0
  47. package/generated/v1/snippets/journey/check-service.json +6 -0
  48. package/generated/v1/snippets/journey/clean-up.json +6 -0
  49. package/generated/v1/snippets/journey/create-destination.json +6 -0
  50. package/generated/v1/snippets/journey/create-one-shot.json +6 -0
  51. package/generated/v1/snippets/journey/read-attempt.json +6 -0
  52. package/generated/v1/snippets/journey/read-schedule.json +6 -0
  53. package/generated/v1/snippets/journey/replay-one-shot.json +6 -0
  54. package/generated/v1/snippets/meta.scaffold.json +1 -0
  55. package/generated/v1/snippets/meta.version.json +1 -0
  56. package/generated/v1/snippets/occurrences.list.json +1 -0
  57. package/generated/v1/snippets/occurrences.read.json +1 -0
  58. package/generated/v1/snippets/occurrences.redeliver.json +1 -0
  59. package/generated/v1/snippets/projects.create.json +1 -0
  60. package/generated/v1/snippets/projects.list.json +1 -0
  61. package/generated/v1/snippets/schedules.archive.json +1 -0
  62. package/generated/v1/snippets/schedules.cancel.json +1 -0
  63. package/generated/v1/snippets/schedules.create.json +1 -0
  64. package/generated/v1/snippets/schedules.list.json +1 -0
  65. package/generated/v1/snippets/schedules.pause.json +1 -0
  66. package/generated/v1/snippets/schedules.read.json +1 -0
  67. package/generated/v1/snippets/schedules.replace.json +1 -0
  68. package/generated/v1/snippets/schedules.resume.json +1 -0
  69. package/generated/v1/snippets/sinkdeliveries.list.json +1 -0
  70. package/generated/v1/snippets/sinkdeliveries.read.json +1 -0
  71. package/generated/v1/snippets/testsinks.configure.json +1 -0
  72. package/generated/v1/snippets/testsinks.create.json +1 -0
  73. package/generated/v1/snippets/testsinks.delete.json +1 -0
  74. package/generated/v1/snippets/testsinks.list.json +1 -0
  75. package/generated/v1/snippets/testsinks.read.json +1 -0
  76. package/generated/v1/snippets/usage.read.json +1 -0
  77. package/package.json +7 -2
  78. package/dist/webhooks.d.ts +0 -116
  79. package/dist/webhooks.js +0 -161
@@ -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
+ }
@@ -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>;