@greatwait/contracts 0.1.0-alpha.2 → 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 (71) hide show
  1. package/dist/authority.d.ts +57 -0
  2. package/dist/authority.js +35 -0
  3. package/dist/first-value-journey.d.ts +156 -0
  4. package/dist/first-value-journey.js +213 -0
  5. package/dist/identity.d.ts +4 -4
  6. package/dist/index.d.ts +3 -3
  7. package/dist/index.js +3 -3
  8. package/dist/mcp.d.ts +199 -0
  9. package/dist/mcp.js +214 -0
  10. package/dist/oauth.js +83 -7
  11. package/dist/operations.d.ts +155 -1
  12. package/dist/operations.js +104 -2
  13. package/generated/v1/examples/context.read.response.json +6 -0
  14. package/generated/v1/examples/credentials.connection.response.json +16 -0
  15. package/generated/v1/manifest.json +53 -0
  16. package/generated/v1/openapi.json +689 -0
  17. package/generated/v1/policy-matrix.json +14 -0
  18. package/generated/v1/schemas/context.read.response.json +43 -0
  19. package/generated/v1/schemas/credentials.connection.response.json +64 -0
  20. package/generated/v1/snippets/attempts.list.json +1 -0
  21. package/generated/v1/snippets/attempts.read.json +1 -0
  22. package/generated/v1/snippets/context.read.json +6 -0
  23. package/generated/v1/snippets/credentials.connection.json +6 -0
  24. package/generated/v1/snippets/credentials.create.json +1 -0
  25. package/generated/v1/snippets/credentials.list.json +1 -0
  26. package/generated/v1/snippets/credentials.revoke.json +1 -0
  27. package/generated/v1/snippets/credentials.rotate.json +1 -0
  28. package/generated/v1/snippets/endpoints.create.json +1 -0
  29. package/generated/v1/snippets/endpoints.disable.json +1 -0
  30. package/generated/v1/snippets/endpoints.list.json +1 -0
  31. package/generated/v1/snippets/endpoints.read.json +1 -0
  32. package/generated/v1/snippets/endpoints.rotateSigningKey.json +1 -0
  33. package/generated/v1/snippets/endpoints.update.json +1 -0
  34. package/generated/v1/snippets/endpoints.verify.json +1 -0
  35. package/generated/v1/snippets/forecast.read.json +1 -0
  36. package/generated/v1/snippets/forecast.windows.json +1 -0
  37. package/generated/v1/snippets/grants.list.json +1 -0
  38. package/generated/v1/snippets/grants.revoke.json +1 -0
  39. package/generated/v1/snippets/journey/await-occurrence.json +6 -0
  40. package/generated/v1/snippets/journey/check-credential.json +6 -0
  41. package/generated/v1/snippets/journey/check-service.json +6 -0
  42. package/generated/v1/snippets/journey/clean-up.json +6 -0
  43. package/generated/v1/snippets/journey/create-destination.json +6 -0
  44. package/generated/v1/snippets/journey/create-one-shot.json +6 -0
  45. package/generated/v1/snippets/journey/read-attempt.json +6 -0
  46. package/generated/v1/snippets/journey/read-schedule.json +6 -0
  47. package/generated/v1/snippets/journey/replay-one-shot.json +6 -0
  48. package/generated/v1/snippets/meta.scaffold.json +1 -0
  49. package/generated/v1/snippets/meta.version.json +1 -0
  50. package/generated/v1/snippets/occurrences.list.json +1 -0
  51. package/generated/v1/snippets/occurrences.read.json +1 -0
  52. package/generated/v1/snippets/occurrences.redeliver.json +1 -0
  53. package/generated/v1/snippets/projects.create.json +1 -0
  54. package/generated/v1/snippets/projects.list.json +1 -0
  55. package/generated/v1/snippets/schedules.archive.json +1 -0
  56. package/generated/v1/snippets/schedules.cancel.json +1 -0
  57. package/generated/v1/snippets/schedules.create.json +1 -0
  58. package/generated/v1/snippets/schedules.list.json +1 -0
  59. package/generated/v1/snippets/schedules.pause.json +1 -0
  60. package/generated/v1/snippets/schedules.read.json +1 -0
  61. package/generated/v1/snippets/schedules.replace.json +1 -0
  62. package/generated/v1/snippets/schedules.resume.json +1 -0
  63. package/generated/v1/snippets/sinkdeliveries.list.json +1 -0
  64. package/generated/v1/snippets/sinkdeliveries.read.json +1 -0
  65. package/generated/v1/snippets/testsinks.configure.json +1 -0
  66. package/generated/v1/snippets/testsinks.create.json +1 -0
  67. package/generated/v1/snippets/testsinks.delete.json +1 -0
  68. package/generated/v1/snippets/testsinks.list.json +1 -0
  69. package/generated/v1/snippets/testsinks.read.json +1 -0
  70. package/generated/v1/snippets/usage.read.json +1 -0
  71. package/package.json +2 -2
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import type { PrincipalKind, Scope } from "./authority.js";
2
+ import type { EnvironmentKind, PrincipalKind, Scope } from "./authority.js";
3
3
  import type { ProblemCode } from "./problems.js";
4
4
  /**
5
5
  * The operation registry: one declaration per public operation, and the single
@@ -100,6 +100,29 @@ export interface OperationDefinition {
100
100
  * teaches a shape the service will reject.
101
101
  */
102
102
  readonly example: unknown;
103
+ /**
104
+ * The environment kinds this operation is available in. Absent means both.
105
+ *
106
+ * **Declared because the tool listing has to know, and could not.**
107
+ * `tools/list` is filtered by `toolsVisibleTo`, which asks `authorizeOperation`
108
+ * — a decision about *scopes*. The environment restriction lived only in the
109
+ * routes and in prose, so a `live` credential holding `endpoints:read` was
110
+ * offered `testsinks_list`, `testsinks_read` and `sinkdeliveries_list`: three
111
+ * tools whose refusal tells it to "act in that environment instead", which a
112
+ * credential cannot do, because its authority is fixed when it is issued.
113
+ *
114
+ * Absent rather than required on all forty-one, because a restriction is the
115
+ * exception and a field repeated identically on thirty-four entries is one
116
+ * nobody reads. What stops the next restricted operation being forgotten is
117
+ * not this declaration but the case that drives every operation with both
118
+ * principals and compares the refusal against it — see
119
+ * `v1-environment-restriction.worker.test.ts`.
120
+ *
121
+ * **The filter is still a courtesy and the route is still the enforcement.**
122
+ * Nothing here is a permission check: a tool omitted from a listing is
123
+ * dispatched and refused exactly as before if a client calls it anyway.
124
+ */
125
+ readonly environments?: readonly EnvironmentKind[];
103
126
  /** Stable machine name, also the MCP tool name and the SDK method name. */
104
127
  readonly id: string;
105
128
  /**
@@ -1061,6 +1084,7 @@ export declare const OPERATIONS: readonly [{
1061
1084
  readonly updatedAt: "2026-08-07T15:00:00.000Z";
1062
1085
  readonly url: "https://sink.greatwait.dev/__greatwait/test-sinks/tsk_01h8xyz9k2m4n6p8r0t2v4w6y8";
1063
1086
  };
1087
+ readonly environments: readonly ["test"];
1064
1088
  readonly id: "testsinks.create";
1065
1089
  readonly idempotent: false;
1066
1090
  readonly mcp: {
@@ -1189,6 +1213,7 @@ export declare const OPERATIONS: readonly [{
1189
1213
  readonly kind: "authoritative";
1190
1214
  };
1191
1215
  };
1216
+ readonly environments: readonly ["test"];
1192
1217
  readonly id: "testsinks.list";
1193
1218
  readonly idempotent: true;
1194
1219
  readonly mcp: {
@@ -1281,6 +1306,7 @@ export declare const OPERATIONS: readonly [{
1281
1306
  readonly updatedAt: "2026-08-07T15:02:00.000Z";
1282
1307
  readonly url: "https://sink.greatwait.dev/__greatwait/test-sinks/tsk_01h8xyz9k2m4n6p8r0t2v4w6y8";
1283
1308
  };
1309
+ readonly environments: readonly ["test"];
1284
1310
  readonly id: "testsinks.read";
1285
1311
  readonly idempotent: true;
1286
1312
  readonly mcp: {
@@ -1358,6 +1384,7 @@ export declare const OPERATIONS: readonly [{
1358
1384
  readonly updatedAt: "2026-08-07T15:03:00.000Z";
1359
1385
  readonly url: "https://sink.greatwait.dev/__greatwait/test-sinks/tsk_01h8xyz9k2m4n6p8r0t2v4w6y8";
1360
1386
  };
1387
+ readonly environments: readonly ["test"];
1361
1388
  readonly id: "testsinks.configure";
1362
1389
  readonly idempotent: true;
1363
1390
  readonly mcp: {
@@ -1477,6 +1504,7 @@ export declare const OPERATIONS: readonly [{
1477
1504
  readonly kind: "authoritative";
1478
1505
  };
1479
1506
  };
1507
+ readonly environments: readonly ["test"];
1480
1508
  readonly id: "sinkdeliveries.list";
1481
1509
  readonly idempotent: true;
1482
1510
  readonly mcp: {
@@ -1563,6 +1591,7 @@ export declare const OPERATIONS: readonly [{
1563
1591
  readonly signature: "v1=11135fdfc0bc5d49ebcc9d3a0d8df1116179097cefe961c6eca281339e5cfab3";
1564
1592
  readonly sink: "tsk_01h8xyz9k2m4n6p8r0t2v4w6y8";
1565
1593
  };
1594
+ readonly environments: readonly ["test"];
1566
1595
  readonly id: "sinkdeliveries.read";
1567
1596
  readonly idempotent: true;
1568
1597
  readonly mcp: {
@@ -1635,6 +1664,7 @@ export declare const OPERATIONS: readonly [{
1635
1664
  readonly updatedAt: "2026-08-07T15:04:00.000Z";
1636
1665
  readonly url: "https://sink.greatwait.dev/__greatwait/test-sinks/tsk_01h8xyz9k2m4n6p8r0t2v4w6y8";
1637
1666
  };
1667
+ readonly environments: readonly ["test"];
1638
1668
  readonly id: "testsinks.delete";
1639
1669
  readonly idempotent: true;
1640
1670
  readonly mcp: {
@@ -1746,6 +1776,57 @@ export declare const OPERATIONS: readonly [{
1746
1776
  readonly scopes: readonly [];
1747
1777
  readonly summary: "Set the mock scenario";
1748
1778
  readonly surfaces: readonly ["mock"];
1779
+ }, {
1780
+ readonly creates: false;
1781
+ readonly description: "Reports the authority the presented credential carries: the project it acts in, which of that project's two environments, and the scopes the service will enforce for it. The answer comes entirely from the verified principal, so there is no parameter that could name another project and no way for the reply to describe anything but the caller itself. It requires a principal and no scope, because every value in it is a property of the credential being presented rather than of anything the credential may reach: a caller that could not read this could still act, and would simply be acting without being able to say where. A signed-in member is not accepted — a session's authority is an organization and it names its project per request, so there is no single answer to give one.";
1782
+ readonly example: {
1783
+ readonly environmentKind: "test";
1784
+ readonly principal: "api-credential";
1785
+ readonly project: "proj_01h8xyz9k2m4n6p8r0t2v4w6y8";
1786
+ readonly scopes: readonly ["schedules:read", "schedules:write"];
1787
+ };
1788
+ readonly id: "context.read";
1789
+ readonly idempotent: true;
1790
+ readonly mcp: {
1791
+ readonly kind: "resource";
1792
+ readonly uri: "greatwait://context";
1793
+ };
1794
+ readonly method: "GET";
1795
+ readonly mutates: false;
1796
+ readonly path: "/v1/context";
1797
+ readonly principals: readonly ["api-credential", "oauth"];
1798
+ readonly problems: readonly [];
1799
+ readonly response: z.ZodObject<{
1800
+ environmentKind: z.ZodEnum<{
1801
+ test: "test";
1802
+ live: "live";
1803
+ }>;
1804
+ principal: z.ZodEnum<{
1805
+ "api-credential": "api-credential";
1806
+ oauth: "oauth";
1807
+ }>;
1808
+ project: z.ZodString;
1809
+ scopes: z.ZodArray<z.ZodEnum<{
1810
+ "credentials:administer": "credentials:administer";
1811
+ "credentials:read": "credentials:read";
1812
+ "credentials:write": "credentials:write";
1813
+ "endpoints:read": "endpoints:read";
1814
+ "endpoints:write": "endpoints:write";
1815
+ "forecast:read": "forecast:read";
1816
+ "grants:read": "grants:read";
1817
+ "grants:write": "grants:write";
1818
+ "occurrences:read": "occurrences:read";
1819
+ "occurrences:redeliver": "occurrences:redeliver";
1820
+ "projects:read": "projects:read";
1821
+ "projects:write": "projects:write";
1822
+ "schedules:read": "schedules:read";
1823
+ "schedules:write": "schedules:write";
1824
+ "usage:read": "usage:read";
1825
+ }>>;
1826
+ }, z.core.$strip>;
1827
+ readonly scopes: readonly [];
1828
+ readonly summary: "Read this credential's authority";
1829
+ readonly surfaces: readonly ["api", "mock"];
1749
1830
  }, {
1750
1831
  readonly creates: true;
1751
1832
  readonly description: "Issues an API credential bound to one project and environment, with scopes capped by the issuing member's current role. The full secret is in this response and in no other, is not recoverable afterwards, and there is deliberately no reveal endpoint. The value is the sixteen-character display prefix followed immediately by thirty-two more characters; the example below is a visible placeholder rather than that shape, because a realistic credential in a committed file is one the repository's own secret scanner refuses.";
@@ -1917,6 +1998,62 @@ export declare const OPERATIONS: readonly [{
1917
1998
  readonly sessionAuthority: "headers";
1918
1999
  readonly summary: "List API credentials";
1919
2000
  readonly surfaces: readonly ["api", "mock"];
2001
+ }, {
2002
+ readonly creates: false;
2003
+ readonly description: "Reads what a key this project issued should be able to do over MCP: the endpoint to point a client at, the discovery document beside it, the scopes fixed into the key when it was made, and the MCP tool names those scopes authorize. It returns no secret and cannot: the transient value is shown once at creation and is never sent here. The credential is addressed by identifier and re-resolved against the signed-in member's own project and environment, so a key belonging to another project answers exactly as one that does not exist. `state` distinguishes a key that still verifies from one that was revoked and one whose rotation overlap has run out, because a client configured with either fails identically and the remedies differ.";
2004
+ readonly example: {
2005
+ readonly discovery: "https://api.greatwait.dev/.well-known/oauth-protected-resource/mcp";
2006
+ readonly displayPrefix: "pt_test_a1b2c3d4";
2007
+ readonly endpoint: "https://api.greatwait.dev/mcp";
2008
+ readonly id: "ak_01h8xyz9k2m4n6p8r0t2v4w6y8";
2009
+ readonly scopes: readonly ["occurrences:read", "schedules:read", "schedules:write"];
2010
+ readonly state: "usable";
2011
+ readonly tools: readonly ["occurrences_list", "occurrences_read", "schedules_cancel", "schedules_create", "schedules_list", "schedules_read"];
2012
+ };
2013
+ readonly id: "credentials.connection";
2014
+ readonly idempotent: true;
2015
+ readonly mcp: {
2016
+ readonly kind: "unavailable";
2017
+ readonly reason: "A signed-in member checking a key whose secret is still on screen. MCP carries API credentials and OAuth tokens, neither of which holds a transient secret or a session to bind one to.";
2018
+ };
2019
+ readonly method: "GET";
2020
+ readonly mutates: false;
2021
+ readonly path: "/v1/credentials/{credentialId}/connection";
2022
+ readonly principals: readonly ["session"];
2023
+ readonly problems: readonly ["not_found"];
2024
+ readonly response: z.ZodObject<{
2025
+ discovery: z.ZodString;
2026
+ displayPrefix: z.ZodString;
2027
+ endpoint: z.ZodString;
2028
+ id: z.ZodString;
2029
+ scopes: z.ZodArray<z.ZodEnum<{
2030
+ "credentials:administer": "credentials:administer";
2031
+ "credentials:read": "credentials:read";
2032
+ "credentials:write": "credentials:write";
2033
+ "endpoints:read": "endpoints:read";
2034
+ "endpoints:write": "endpoints:write";
2035
+ "forecast:read": "forecast:read";
2036
+ "grants:read": "grants:read";
2037
+ "grants:write": "grants:write";
2038
+ "occurrences:read": "occurrences:read";
2039
+ "occurrences:redeliver": "occurrences:redeliver";
2040
+ "projects:read": "projects:read";
2041
+ "projects:write": "projects:write";
2042
+ "schedules:read": "schedules:read";
2043
+ "schedules:write": "schedules:write";
2044
+ "usage:read": "usage:read";
2045
+ }>>;
2046
+ state: z.ZodEnum<{
2047
+ revoked: "revoked";
2048
+ usable: "usable";
2049
+ expired: "expired";
2050
+ }>;
2051
+ tools: z.ZodArray<z.ZodString>;
2052
+ }, z.core.$strip>;
2053
+ readonly scopes: readonly ["credentials:read"];
2054
+ readonly sessionAuthority: "headers";
2055
+ readonly summary: "Read a credential's MCP connection";
2056
+ readonly surfaces: readonly ["api", "mock"];
1920
2057
  }, {
1921
2058
  readonly creates: false;
1922
2059
  readonly description: "Lists the projects in the signed-in member's organization that they may act in, with the environment kinds each one has. This is what a browser asks before it can address anything else: an API credential and an OAuth token each carry one project intrinsically, but a member belongs to an organization and chooses. Archived and deleted projects are omitted, because a member cannot act in one. The result is a display aid rather than an authorization control — naming a project on a later request re-resolves it against the member's own organization, and a project that is not theirs answers exactly as one that does not exist.";
@@ -3982,4 +4119,21 @@ export declare const UNIVERSAL_PROBLEMS: readonly ProblemCode[];
3982
4119
  * whose rotation window has closed.
3983
4120
  */
3984
4121
  export declare const AUTHENTICATED_PROBLEMS: readonly ProblemCode[];
4122
+ /**
4123
+ * The problem an operation returns when the principal is verified and its scope
4124
+ * list falls short.
4125
+ *
4126
+ * **Separated from the list above at W10-01, because the two were keyed on the
4127
+ * wrong field.** `problemsFor` read `scopes.length > 0` and published all four
4128
+ * problems from it, which was exact only while "requires a scope" and "requires
4129
+ * a credential" were the same sentence. `context.read` is the operation that
4130
+ * needs a credential and no scope, and under the old rule it would have been
4131
+ * published with no authentication problems and — through
4132
+ * `securityRequirements` — no security requirement either: a route that refuses
4133
+ * an anonymous caller, documented as open to one. Keying authentication on
4134
+ * `principals` and this one on `scopes` says what each actually depends on, and
4135
+ * it also stops publishing a `scope_missing` that no request to such an
4136
+ * operation could ever produce.
4137
+ */
4138
+ export declare const SCOPED_PROBLEMS: readonly ProblemCode[];
3985
4139
  export declare function problemsFor(operation: OperationDefinition): readonly ProblemCode[];
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { credentialContextSchema } from "./authority.js";
2
3
  import { callbackEndpointCreatedSchema, callbackEndpointPageSchema, callbackEndpointRequestSchema, callbackEndpointRotatedSchema, callbackEndpointSchema, callbackSigningKeyRotationRequestSchema, } from "./endpoints.js";
3
4
  import { FORECAST_WINDOW_VERSION, forecastRequestSchema, forecastSchema, forecastWindowsSchema, } from "./forecast.js";
4
5
  import { usageRequestSchema, usageSchema } from "./usage.js";
@@ -8,6 +9,7 @@ import { credentialCreatedSchema, credentialPageSchema, credentialRequestSchema,
8
9
  import { attemptPageSchema, attemptViewSchema, occurrencePageSchema, occurrenceViewSchema, schedulePageSchema, scheduleRequestSchema, scheduleViewSchema, } from "./scheduling.js";
9
10
  import { testSinkCreateRequestSchema, testSinkDeliveryBodySchema, testSinkDeliveryPageSchema, testSinkPageSchema, testSinkProgramSchema, testSinkSchema, } from "./test-sink.js";
10
11
  import { projectCreateRequestSchema, projectListSchema, projectSummarySchema, } from "./tenancy.js";
12
+ import { mcpConnectionSchema } from "./mcp.js";
11
13
  import { scaffoldVersionSchema, workspaceScaffoldSchema } from "./workspace.js";
12
14
  import { timestampSchema } from "./time.js";
13
15
  /**
@@ -424,6 +426,7 @@ export const OPERATIONS = [
424
426
  updatedAt: "2026-08-07T15:00:00.000Z",
425
427
  url: "https://sink.greatwait.dev/__greatwait/test-sinks/tsk_01h8xyz9k2m4n6p8r0t2v4w6y8",
426
428
  },
429
+ environments: ["test"],
427
430
  id: "testsinks.create",
428
431
  idempotent: false,
429
432
  mcp: { destructive: false, kind: "tool", openWorld: false },
@@ -475,6 +478,7 @@ export const OPERATIONS = [
475
478
  ],
476
479
  freshness: { kind: "authoritative" },
477
480
  },
481
+ environments: ["test"],
478
482
  id: "testsinks.list",
479
483
  idempotent: true,
480
484
  mcp: { destructive: false, kind: "tool", openWorld: false },
@@ -504,6 +508,7 @@ export const OPERATIONS = [
504
508
  updatedAt: "2026-08-07T15:02:00.000Z",
505
509
  url: "https://sink.greatwait.dev/__greatwait/test-sinks/tsk_01h8xyz9k2m4n6p8r0t2v4w6y8",
506
510
  },
511
+ environments: ["test"],
507
512
  id: "testsinks.read",
508
513
  idempotent: true,
509
514
  mcp: { destructive: false, kind: "tool", openWorld: false },
@@ -532,6 +537,7 @@ export const OPERATIONS = [
532
537
  updatedAt: "2026-08-07T15:03:00.000Z",
533
538
  url: "https://sink.greatwait.dev/__greatwait/test-sinks/tsk_01h8xyz9k2m4n6p8r0t2v4w6y8",
534
539
  },
540
+ environments: ["test"],
535
541
  id: "testsinks.configure",
536
542
  idempotent: true,
537
543
  mcp: { destructive: false, kind: "tool", openWorld: false },
@@ -568,6 +574,7 @@ export const OPERATIONS = [
568
574
  ],
569
575
  freshness: { kind: "authoritative" },
570
576
  },
577
+ environments: ["test"],
571
578
  id: "sinkdeliveries.list",
572
579
  idempotent: true,
573
580
  mcp: { destructive: false, kind: "tool", openWorld: false },
@@ -602,6 +609,7 @@ export const OPERATIONS = [
602
609
  signature: "v1=11135fdfc0bc5d49ebcc9d3a0d8df1116179097cefe961c6eca281339e5cfab3",
603
610
  sink: "tsk_01h8xyz9k2m4n6p8r0t2v4w6y8",
604
611
  },
612
+ environments: ["test"],
605
613
  id: "sinkdeliveries.read",
606
614
  idempotent: true,
607
615
  mcp: {
@@ -633,6 +641,7 @@ export const OPERATIONS = [
633
641
  updatedAt: "2026-08-07T15:04:00.000Z",
634
642
  url: "https://sink.greatwait.dev/__greatwait/test-sinks/tsk_01h8xyz9k2m4n6p8r0t2v4w6y8",
635
643
  },
644
+ environments: ["test"],
636
645
  id: "testsinks.delete",
637
646
  idempotent: true,
638
647
  mcp: { destructive: true, kind: "tool", openWorld: false },
@@ -673,6 +682,40 @@ export const OPERATIONS = [
673
682
  summary: "Set the mock scenario",
674
683
  surfaces: ["mock"],
675
684
  },
685
+ {
686
+ creates: false,
687
+ description: "Reports the authority the presented credential carries: the project it acts in, which of that project's two environments, and the scopes the service will enforce for it. The answer comes entirely from the verified principal, so there is no parameter that could name another project and no way for the reply to describe anything but the caller itself. It requires a principal and no scope, because every value in it is a property of the credential being presented rather than of anything the credential may reach: a caller that could not read this could still act, and would simply be acting without being able to say where. A signed-in member is not accepted — a session's authority is an organization and it names its project per request, so there is no single answer to give one.",
688
+ example: {
689
+ environmentKind: "test",
690
+ principal: "api-credential",
691
+ project: "proj_01h8xyz9k2m4n6p8r0t2v4w6y8",
692
+ scopes: ["schedules:read", "schedules:write"],
693
+ },
694
+ id: "context.read",
695
+ idempotent: true,
696
+ // A resource rather than a tool: it takes no arguments, it reports a fact
697
+ // about the caller, and an agent choosing between tools should not have to
698
+ // step past "what am I" to find the operations that schedule anything.
699
+ // Being scopeless is what makes a resource legal here — `check:mcp-parity`
700
+ // refuses a resource that requires one, because a resource carries no
701
+ // scope declaration for a host to gate on.
702
+ mcp: { kind: "resource", uri: "greatwait://context" },
703
+ method: "GET",
704
+ mutates: false,
705
+ path: "/v1/context",
706
+ principals: ["api-credential", "oauth"],
707
+ // No `not_found`: the authority tuple exists by the time a principal is
708
+ // verified, so there is nothing here that can be absent.
709
+ problems: [],
710
+ response: credentialContextSchema,
711
+ // **Empty, and that is the point of the operation.** Every other
712
+ // authenticated operation names the scope it consumes; this one consumes
713
+ // none, so `scope_missing` is a refusal it can never produce and
714
+ // `problemsFor` does not publish one for it.
715
+ scopes: [],
716
+ summary: "Read this credential's authority",
717
+ surfaces: ["api", "mock"],
718
+ },
676
719
  {
677
720
  creates: true,
678
721
  description: "Issues an API credential bound to one project and environment, with scopes capped by the issuing member's current role. The full secret is in this response and in no other, is not recoverable afterwards, and there is deliberately no reveal endpoint. The value is the sixteen-character display prefix followed immediately by thirty-two more characters; the example below is a visible placeholder rather than that shape, because a realistic credential in a committed file is one the repository's own secret scanner refuses.",
@@ -776,6 +819,48 @@ export const OPERATIONS = [
776
819
  summary: "List API credentials",
777
820
  surfaces: ["api", "mock"],
778
821
  },
822
+ {
823
+ creates: false,
824
+ description: "Reads what a key this project issued should be able to do over MCP: the endpoint to point a client at, the discovery document beside it, the scopes fixed into the key when it was made, and the MCP tool names those scopes authorize. It returns no secret and cannot: the transient value is shown once at creation and is never sent here. The credential is addressed by identifier and re-resolved against the signed-in member's own project and environment, so a key belonging to another project answers exactly as one that does not exist. `state` distinguishes a key that still verifies from one that was revoked and one whose rotation overlap has run out, because a client configured with either fails identically and the remedies differ.",
825
+ example: {
826
+ discovery: "https://api.greatwait.dev/.well-known/oauth-protected-resource/mcp",
827
+ displayPrefix: "pt_test_a1b2c3d4",
828
+ endpoint: "https://api.greatwait.dev/mcp",
829
+ id: "ak_01h8xyz9k2m4n6p8r0t2v4w6y8",
830
+ scopes: ["occurrences:read", "schedules:read", "schedules:write"],
831
+ state: "usable",
832
+ tools: [
833
+ "occurrences_list",
834
+ "occurrences_read",
835
+ "schedules_cancel",
836
+ "schedules_create",
837
+ "schedules_list",
838
+ "schedules_read",
839
+ ],
840
+ },
841
+ id: "credentials.connection",
842
+ idempotent: true,
843
+ // **Withheld, and the reason is the same one that withholds issuance.** A
844
+ // member asks this about a key they are holding in a browser, in the window
845
+ // where its secret is still on screen. An agent is already connected: it has
846
+ // no transient secret to check and no session to bind one to, so a tool here
847
+ // would answer `authentication_required` to every caller that could reach
848
+ // it. Publishing the exclusion is the honest version of that.
849
+ mcp: {
850
+ kind: "unavailable",
851
+ reason: "A signed-in member checking a key whose secret is still on screen. MCP carries API credentials and OAuth tokens, neither of which holds a transient secret or a session to bind one to.",
852
+ },
853
+ method: "GET",
854
+ mutates: false,
855
+ path: "/v1/credentials/{credentialId}/connection",
856
+ principals: ["session"],
857
+ problems: ["not_found"],
858
+ response: mcpConnectionSchema,
859
+ scopes: ["credentials:read"],
860
+ sessionAuthority: "headers",
861
+ summary: "Read a credential's MCP connection",
862
+ surfaces: ["api", "mock"],
863
+ },
779
864
  {
780
865
  creates: false,
781
866
  description: "Lists the projects in the signed-in member's organization that they may act in, with the environment kinds each one has. This is what a browser asks before it can address anything else: an API credential and an OAuth token each carry one project intrinsically, but a member belongs to an organization and chooses. Archived and deleted projects are omitted, because a member cannot act in one. The result is a display aid rather than an authorization control — naming a project on a later request re-resolves it against the member's own organization, and a project that is not theirs answers exactly as one that does not exist.",
@@ -1824,14 +1909,31 @@ export const AUTHENTICATED_PROBLEMS = [
1824
1909
  "authentication_required",
1825
1910
  "credential_expired",
1826
1911
  "credential_revoked",
1827
- "scope_missing",
1828
1912
  ];
1913
+ /**
1914
+ * The problem an operation returns when the principal is verified and its scope
1915
+ * list falls short.
1916
+ *
1917
+ * **Separated from the list above at W10-01, because the two were keyed on the
1918
+ * wrong field.** `problemsFor` read `scopes.length > 0` and published all four
1919
+ * problems from it, which was exact only while "requires a scope" and "requires
1920
+ * a credential" were the same sentence. `context.read` is the operation that
1921
+ * needs a credential and no scope, and under the old rule it would have been
1922
+ * published with no authentication problems and — through
1923
+ * `securityRequirements` — no security requirement either: a route that refuses
1924
+ * an anonymous caller, documented as open to one. Keying authentication on
1925
+ * `principals` and this one on `scopes` says what each actually depends on, and
1926
+ * it also stops publishing a `scope_missing` that no request to such an
1927
+ * operation could ever produce.
1928
+ */
1929
+ export const SCOPED_PROBLEMS = ["scope_missing"];
1829
1930
  export function problemsFor(operation) {
1830
1931
  return [
1831
1932
  ...new Set([
1832
1933
  ...operation.problems,
1833
1934
  ...UNIVERSAL_PROBLEMS,
1834
- ...(operation.scopes.length > 0 ? AUTHENTICATED_PROBLEMS : []),
1935
+ ...(operation.principals.length > 0 ? AUTHENTICATED_PROBLEMS : []),
1936
+ ...(operation.scopes.length > 0 ? SCOPED_PROBLEMS : []),
1835
1937
  ]),
1836
1938
  ].sort();
1837
1939
  }
@@ -0,0 +1,6 @@
1
+ {
2
+ "environmentKind": "test",
3
+ "principal": "api-credential",
4
+ "project": "proj_01h8xyz9k2m4n6p8r0t2v4w6y8",
5
+ "scopes": ["schedules:read", "schedules:write"]
6
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "discovery": "https://api.greatwait.dev/.well-known/oauth-protected-resource/mcp",
3
+ "displayPrefix": "pt_test_a1b2c3d4",
4
+ "endpoint": "https://api.greatwait.dev/mcp",
5
+ "id": "ak_01h8xyz9k2m4n6p8r0t2v4w6y8",
6
+ "scopes": ["occurrences:read", "schedules:read", "schedules:write"],
7
+ "state": "usable",
8
+ "tools": [
9
+ "occurrences_list",
10
+ "occurrences_read",
11
+ "schedules_cancel",
12
+ "schedules_create",
13
+ "schedules_list",
14
+ "schedules_read"
15
+ ]
16
+ }
@@ -2,10 +2,13 @@
2
2
  "contractVersion": "2026-08-22",
3
3
  "externalFiles": [
4
4
  "docs/quickstarts/README.md",
5
+ "docs/quickstarts/cli.md",
5
6
  "docs/quickstarts/curl.md",
6
7
  "docs/quickstarts/first-value.mjs",
7
8
  "docs/quickstarts/first-value.sh",
8
9
  "docs/quickstarts/javascript.md",
10
+ "docs/quickstarts/mcp-reference.md",
11
+ "docs/quickstarts/mcp.md",
9
12
  "packages/api-client/src/operations.generated.ts",
10
13
  "packages/mcp-tools/src/schemas.generated.ts"
11
14
  ],
@@ -15,6 +18,8 @@
15
18
  "callback-receiver.json",
16
19
  "examples/attempts.list.response.json",
17
20
  "examples/attempts.read.response.json",
21
+ "examples/context.read.response.json",
22
+ "examples/credentials.connection.response.json",
18
23
  "examples/credentials.create.request.json",
19
24
  "examples/credentials.create.response.json",
20
25
  "examples/credentials.list.response.json",
@@ -75,6 +80,8 @@
75
80
  "schemas/attempts.list.query.json",
76
81
  "schemas/attempts.list.response.json",
77
82
  "schemas/attempts.read.response.json",
83
+ "schemas/context.read.response.json",
84
+ "schemas/credentials.connection.response.json",
78
85
  "schemas/credentials.create.request.json",
79
86
  "schemas/credentials.create.response.json",
80
87
  "schemas/credentials.list.query.json",
@@ -161,6 +168,8 @@
161
168
  "schemas/usage.read.response.json",
162
169
  "snippets/attempts.list.json",
163
170
  "snippets/attempts.read.json",
171
+ "snippets/context.read.json",
172
+ "snippets/credentials.connection.json",
164
173
  "snippets/credentials.create.json",
165
174
  "snippets/credentials.list.json",
166
175
  "snippets/credentials.revoke.json",
@@ -176,6 +185,15 @@
176
185
  "snippets/forecast.windows.json",
177
186
  "snippets/grants.list.json",
178
187
  "snippets/grants.revoke.json",
188
+ "snippets/journey/await-occurrence.json",
189
+ "snippets/journey/check-credential.json",
190
+ "snippets/journey/check-service.json",
191
+ "snippets/journey/clean-up.json",
192
+ "snippets/journey/create-destination.json",
193
+ "snippets/journey/create-one-shot.json",
194
+ "snippets/journey/read-attempt.json",
195
+ "snippets/journey/read-schedule.json",
196
+ "snippets/journey/replay-one-shot.json",
179
197
  "snippets/meta.scaffold.json",
180
198
  "snippets/meta.version.json",
181
199
  "snippets/mock/mock.getFixture.json",
@@ -546,6 +564,22 @@
546
564
  "scopes": [],
547
565
  "surfaces": ["mock"]
548
566
  },
567
+ {
568
+ "id": "context.read",
569
+ "method": "GET",
570
+ "mutates": false,
571
+ "path": "/v1/context",
572
+ "principals": ["api-credential", "oauth"],
573
+ "problems": [
574
+ "authentication_required",
575
+ "credential_expired",
576
+ "credential_revoked",
577
+ "rate_limited",
578
+ "service_unavailable"
579
+ ],
580
+ "scopes": [],
581
+ "surfaces": ["api", "mock"]
582
+ },
549
583
  {
550
584
  "id": "credentials.create",
551
585
  "method": "POST",
@@ -585,6 +619,25 @@
585
619
  "sessionAuthority": "headers",
586
620
  "surfaces": ["api", "mock"]
587
621
  },
622
+ {
623
+ "id": "credentials.connection",
624
+ "method": "GET",
625
+ "mutates": false,
626
+ "path": "/v1/credentials/{credentialId}/connection",
627
+ "principals": ["session"],
628
+ "problems": [
629
+ "authentication_required",
630
+ "credential_expired",
631
+ "credential_revoked",
632
+ "not_found",
633
+ "rate_limited",
634
+ "scope_missing",
635
+ "service_unavailable"
636
+ ],
637
+ "scopes": ["credentials:read"],
638
+ "sessionAuthority": "headers",
639
+ "surfaces": ["api", "mock"]
640
+ },
588
641
  {
589
642
  "id": "projects.list",
590
643
  "method": "GET",