@opensoftware/contracts 1.4.0 → 1.5.0

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/README.md CHANGED
@@ -42,8 +42,17 @@ Each format has one schema, generated from `typespec/`, and fixtures that CONTRA
42
42
  | run record | `run-record/run-record.schema.json` | `RunRecord` | OTel resource and root-span attributes; pins in `typespec/run-record.tsp`. |
43
43
  | evidence bundle | `evidence-bundle/evidence-bundle.schema.json` | `EvidenceManifest` | `evidence.json` at the bundle root. |
44
44
  | score | `score/score.schema.json` | `Score` | Subset of Langfuse's create-score request; factory fields in `metadata`. |
45
+ | gate record | `gate-record/gate-record.schema.json` | `GateRecord` | gatekeeper's signed approval record (ADR 0047). |
45
46
  | events | `events/<event>/<event>.schema.json`, `events/asyncapi.yaml` | `WorkReceived` … `PrClosed` | CloudEvents 1.0, structured JSON. |
46
47
 
48
+ ## HTTP APIs
49
+
50
+ `openapi/openapi.json` also holds the paths of the module APIs, from the operations in `typespec/` (ADR 0048). The
51
+ first is gatekeeper's: `POST /v1/gates` and `GET /v1/gates/{gateId}` with the models `GateRequest`, `GateView` and
52
+ `ApiProblem` (RFC 9457). `pnpm build` compiles the operations with the TypeSpec OpenAPI 3 emitter and copies their
53
+ paths next to the named models; an operation that uses a type that is no model fails the build. The package exports
54
+ the type `paths`, which types a client such as openapi-fetch.
55
+
47
56
  Wire formats (everything but the factory definition) are open: readers ignore fields they do not know, so a minor
48
57
  version may add optional fields. Read `factory.yaml` with a YAML 1.2 parser: YAML 1.1 parsers turn the key `on`
49
58
  into `true`.
@@ -71,8 +80,9 @@ release-please opens a release pull request from the Conventional Commits under
71
80
  the tag `contracts-v<version>`. The release pull request also sets the version in `openapi/openapi.json`, which
72
81
  `pnpm build` copies from `package.json`, so the generated file stays current. The workflow `.github/workflows/publish-contracts.yml` then builds the package and
73
82
  publishes it to npm as `@opensoftware/contracts`. npm accepts a provenance statement only from a public
74
- repository, so the workflow adds one only when this repository is public. To publish a tag again, run the
75
- workflow by hand with the tag. The CONTRACT check and `osf-contracts breaking` compare with the newest `contracts-v*` tag.
83
+ repository, so the workflow adds one only when this repository is public. When the newest tag is missing on npm,
84
+ for example after a failed upload, the next run of the workflow on `main` publishes it. To publish a tag again, run
85
+ the workflow by hand with the tag. The CONTRACT check and `osf-contracts breaking` compare with the newest `contracts-v*` tag.
76
86
 
77
87
  Only the TypeScript package exists (ADR 0012). A client for another language can be generated from
78
88
  `openapi/openapi.json` with any OpenAPI generator when someone needs one.
package/dist/formats.js CHANGED
@@ -6,6 +6,7 @@ export const FORMATS = [
6
6
  format("run-record", "RunRecord"),
7
7
  format("evidence-bundle", "EvidenceManifest"),
8
8
  format("score", "Score"),
9
+ format("gate-record", "GateRecord"),
9
10
  format("events/work-received", "WorkReceived"),
10
11
  format("events/run-completed", "RunCompleted"),
11
12
  format("events/gate-requested", "GateRequested"),
@@ -1 +1 @@
1
- {"version":3,"file":"formats.js","sourceRoot":"","sources":["../src/formats.ts"],"names":[],"mappings":"AAAA,2GAA2G;AAW3G,MAAM,MAAM,GAAG,CAAC,IAAY,EAAE,KAAa,EAAU,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAY,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AAEjH,MAAM,CAAC,MAAM,OAAO,GAAsB;IACxC,MAAM,CAAC,oBAAoB,EAAE,mBAAmB,CAAC;IACjD,MAAM,CAAC,WAAW,EAAE,UAAU,CAAC;IAC/B,MAAM,CAAC,YAAY,EAAE,WAAW,CAAC;IACjC,MAAM,CAAC,iBAAiB,EAAE,kBAAkB,CAAC;IAC7C,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC;IACxB,MAAM,CAAC,sBAAsB,EAAE,cAAc,CAAC;IAC9C,MAAM,CAAC,sBAAsB,EAAE,cAAc,CAAC;IAC9C,MAAM,CAAC,uBAAuB,EAAE,eAAe,CAAC;IAChD,MAAM,CAAC,qBAAqB,EAAE,aAAa,CAAC;IAC5C,MAAM,CAAC,uBAAuB,EAAE,eAAe,CAAC;IAChD,MAAM,CAAC,kBAAkB,EAAE,UAAU,CAAC;IACtC,MAAM,CAAC,oBAAoB,EAAE,YAAY,CAAC;IAC1C,MAAM,CAAC,kBAAkB,EAAE,UAAU,CAAC;CACvC,CAAC;AAEF,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;AAC9D,CAAC"}
1
+ {"version":3,"file":"formats.js","sourceRoot":"","sources":["../src/formats.ts"],"names":[],"mappings":"AAAA,2GAA2G;AAW3G,MAAM,MAAM,GAAG,CAAC,IAAY,EAAE,KAAa,EAAU,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAY,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AAEjH,MAAM,CAAC,MAAM,OAAO,GAAsB;IACxC,MAAM,CAAC,oBAAoB,EAAE,mBAAmB,CAAC;IACjD,MAAM,CAAC,WAAW,EAAE,UAAU,CAAC;IAC/B,MAAM,CAAC,YAAY,EAAE,WAAW,CAAC;IACjC,MAAM,CAAC,iBAAiB,EAAE,kBAAkB,CAAC;IAC7C,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC;IACxB,MAAM,CAAC,aAAa,EAAE,YAAY,CAAC;IACnC,MAAM,CAAC,sBAAsB,EAAE,cAAc,CAAC;IAC9C,MAAM,CAAC,sBAAsB,EAAE,cAAc,CAAC;IAC9C,MAAM,CAAC,uBAAuB,EAAE,eAAe,CAAC;IAChD,MAAM,CAAC,qBAAqB,EAAE,aAAa,CAAC;IAC5C,MAAM,CAAC,uBAAuB,EAAE,eAAe,CAAC;IAChD,MAAM,CAAC,kBAAkB,EAAE,UAAU,CAAC;IACtC,MAAM,CAAC,oBAAoB,EAAE,YAAY,CAAC;IAC1C,MAAM,CAAC,kBAAkB,EAAE,UAAU,CAAC;CACvC,CAAC;AAEF,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;AAC9D,CAAC"}
@@ -1,4 +1,39 @@
1
- export type paths = Record<string, never>;
1
+ export type paths = {
2
+ "/v1/gates": {
3
+ parameters: {
4
+ query?: never;
5
+ header?: never;
6
+ path?: never;
7
+ cookie?: never;
8
+ };
9
+ get?: never;
10
+ put?: never;
11
+ /** @description Ask the policy's approver groups for a decision. The same request again answers the gate it made before. */
12
+ post: operations["GatekeeperApi_requestGate"];
13
+ delete?: never;
14
+ options?: never;
15
+ head?: never;
16
+ patch?: never;
17
+ trace?: never;
18
+ };
19
+ "/v1/gates/{gateId}": {
20
+ parameters: {
21
+ query?: never;
22
+ header?: never;
23
+ path?: never;
24
+ cookie?: never;
25
+ };
26
+ /** @description A gate: its state, and its record once it is decided. */
27
+ get: operations["GatekeeperApi_getGate"];
28
+ put?: never;
29
+ post?: never;
30
+ delete?: never;
31
+ options?: never;
32
+ head?: never;
33
+ patch?: never;
34
+ trace?: never;
35
+ };
36
+ };
2
37
  export type webhooks = Record<string, never>;
3
38
  export type components = {
4
39
  schemas: {
@@ -33,6 +68,15 @@ export type components = {
33
68
  /** @description Runtime of the agent. */
34
69
  sandbox?: components["schemas"]["Sandbox"];
35
70
  };
71
+ /** @description An error of an HTTP API (RFC 9457). */
72
+ ApiProblem: {
73
+ /** @description A short summary, the same for every occurrence of the problem. */
74
+ title: string;
75
+ /** @description The HTTP status code. */
76
+ status: number;
77
+ /** @description What went wrong this time, for example the field that is missing. */
78
+ detail?: string;
79
+ };
36
80
  Artifact: {
37
81
  path: components["schemas"]["BundlePath"];
38
82
  kind: components["schemas"]["ArtifactKind"];
@@ -172,6 +216,47 @@ export type components = {
172
216
  };
173
217
  /** @enum {string} */
174
218
  GateDecision: "approved" | "rejected" | "expired";
219
+ /** @description gatekeeper: who decided what and when, signed with Ed25519 (ADR 0047). */
220
+ GateRecord: {
221
+ gate: string;
222
+ workItem: string;
223
+ stage: components["schemas"]["Name"];
224
+ summary: string;
225
+ /** @description SHA-256 of the canonical request: what was approved. */
226
+ request: string;
227
+ /** @description The policy file. */
228
+ policy: string;
229
+ /** @description SHA-256 of the policy's text: the rules that picked the approvers. */
230
+ policyDigest: string;
231
+ risk?: string;
232
+ approvers: components["schemas"]["Name"][];
233
+ channel: string;
234
+ /** Format: date-time */
235
+ requestedAt: string;
236
+ decision: components["schemas"]["GateDecision"];
237
+ /** @description Who decided. Absent only when the gate expired. A policy that names no group decides as a trigger. */
238
+ decidedBy?: components["schemas"]["Actor"];
239
+ /** Format: date-time */
240
+ decidedAt: string;
241
+ /** @description The reason of a rejection. */
242
+ reason?: string;
243
+ /** @description Ed25519 over the canonical JSON of the rest of the record. Absent when the record is unsigned. */
244
+ signature?: components["schemas"]["RecordSignature"];
245
+ };
246
+ /** @description gatekeeper: what a person is asked to approve, and the facts the policy decides on (ADR 0047). */
247
+ GateRequest: {
248
+ /** @description What waits: a work item ID or a URL. */
249
+ workItem: string;
250
+ /** @description The stage that waits, for example "merge". */
251
+ stage: components["schemas"]["Name"];
252
+ /** @description What the approvers decide, shown in the channel. */
253
+ summary: string;
254
+ links?: components["schemas"]["Link"][];
255
+ /** @description Any facts the policy needs, such as changed paths, labels or the author. */
256
+ change?: {
257
+ [key: string]: unknown;
258
+ };
259
+ };
175
260
  /** @description gatekeeper: a decision is needed before a stage starts. */
176
261
  GateRequested: {
177
262
  /** @constant */
@@ -210,6 +295,39 @@ export type components = {
210
295
  /** Format: date-time */
211
296
  expiresAt?: string;
212
297
  };
298
+ /**
299
+ * @description A gate's state: waiting, or how it was decided.
300
+ * @enum {string}
301
+ */
302
+ GateState: "pending" | "approved" | "rejected" | "expired";
303
+ /** @description gatekeeper: a gate as the HTTP API shows it (ADR 0048). */
304
+ GateView: {
305
+ /** @description "g" and 16 hex digits of the SHA-256 of the work item, the stage and the change. */
306
+ id: string;
307
+ workItem: string;
308
+ stage: components["schemas"]["Name"];
309
+ summary: string;
310
+ /** @description The risk the policy named, for example "high". */
311
+ risk?: string;
312
+ /** @description The groups whose members may decide. Empty when the policy approved without a person. */
313
+ approvers: components["schemas"]["Name"][];
314
+ /** @description The groups asked after the escalation time. */
315
+ escalateTo?: components["schemas"]["Name"][];
316
+ /** @description Where people decide, for example "slack:C0GATES". */
317
+ channel: string;
318
+ state: components["schemas"]["GateState"];
319
+ /** @description The groups that may decide now. Only while the gate is pending. */
320
+ waitingFor?: components["schemas"]["Name"][];
321
+ /** Format: date-time */
322
+ requestedAt: string;
323
+ /**
324
+ * Format: date-time
325
+ * @description When the gate expires without a decision.
326
+ */
327
+ expiresAt?: string;
328
+ /** @description The record, once the gate is decided. */
329
+ record?: components["schemas"]["GateRecord"];
330
+ };
213
331
  /** @description An identifier chosen by the producer. ULIDs are recommended; consumers treat it as opaque. */
214
332
  Identifier: string;
215
333
  Link: {
@@ -367,6 +485,14 @@ export type components = {
367
485
  [key: string]: components["schemas"]["VersionRange"];
368
486
  };
369
487
  };
488
+ RecordSignature: {
489
+ /** @constant */
490
+ algorithm: "Ed25519";
491
+ /** @description SHA-256 of the public key in DER, like "sha256:…". */
492
+ key: string;
493
+ /** @description The signature in base64. */
494
+ value: string;
495
+ };
370
496
  /** @enum {string} */
371
497
  ReviewState: "approved" | "changes_requested" | "commented";
372
498
  /** @description foreman: a run ended. */
@@ -633,4 +759,96 @@ export type components = {
633
759
  pathItems: never;
634
760
  };
635
761
  export type $defs = Record<string, never>;
636
- export type operations = Record<string, never>;
762
+ export interface operations {
763
+ GatekeeperApi_requestGate: {
764
+ parameters: {
765
+ query?: never;
766
+ header?: never;
767
+ path?: never;
768
+ cookie?: never;
769
+ };
770
+ requestBody: {
771
+ content: {
772
+ "application/json": components["schemas"]["GateRequest"];
773
+ };
774
+ };
775
+ responses: {
776
+ /** @description The request has succeeded. */
777
+ 200: {
778
+ headers: {
779
+ [name: string]: unknown;
780
+ };
781
+ content: {
782
+ "application/json": components["schemas"]["GateView"];
783
+ };
784
+ };
785
+ /** @description The request has succeeded and a new resource has been created as a result. */
786
+ 201: {
787
+ headers: {
788
+ [name: string]: unknown;
789
+ };
790
+ content: {
791
+ "application/json": components["schemas"]["GateView"];
792
+ };
793
+ };
794
+ /** @description The server could not understand the request due to invalid syntax. */
795
+ 400: {
796
+ headers: {
797
+ [name: string]: unknown;
798
+ };
799
+ content: {
800
+ "application/problem+json": components["schemas"]["ApiProblem"];
801
+ };
802
+ };
803
+ /** @description Access is unauthorized. */
804
+ 401: {
805
+ headers: {
806
+ [name: string]: unknown;
807
+ };
808
+ content: {
809
+ "application/problem+json": components["schemas"]["ApiProblem"];
810
+ };
811
+ };
812
+ };
813
+ };
814
+ GatekeeperApi_getGate: {
815
+ parameters: {
816
+ query?: never;
817
+ header?: never;
818
+ path: {
819
+ gateId: string;
820
+ };
821
+ cookie?: never;
822
+ };
823
+ requestBody?: never;
824
+ responses: {
825
+ /** @description The request has succeeded. */
826
+ 200: {
827
+ headers: {
828
+ [name: string]: unknown;
829
+ };
830
+ content: {
831
+ "application/json": components["schemas"]["GateView"];
832
+ };
833
+ };
834
+ /** @description Access is unauthorized. */
835
+ 401: {
836
+ headers: {
837
+ [name: string]: unknown;
838
+ };
839
+ content: {
840
+ "application/problem+json": components["schemas"]["ApiProblem"];
841
+ };
842
+ };
843
+ /** @description The server cannot find the requested resource. */
844
+ 404: {
845
+ headers: {
846
+ [name: string]: unknown;
847
+ };
848
+ content: {
849
+ "application/problem+json": components["schemas"]["ApiProblem"];
850
+ };
851
+ };
852
+ };
853
+ };
854
+ }
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import { type Format } from "./formats.ts";
2
2
  import type { components } from "./generated/types.ts";
3
3
  export { diff as breakingChanges, type Schema } from "./breaking.ts";
4
4
  export { FORMATS, type Format, formatNamed } from "./formats.ts";
5
- export type { components } from "./generated/types.ts";
5
+ export type { components, paths } from "./generated/types.ts";
6
6
  export { type AttributeValue, type RunSpan, runSpans, type SpanAttributes } from "./run-record.ts";
7
7
  type Schemas = components["schemas"];
8
8
  export type FactoryDefinition = Schemas["FactoryDefinition"];
@@ -18,6 +18,10 @@ export type ScoreRecorded = Schemas["ScoreRecorded"];
18
18
  export type PrOpened = Schemas["PrOpened"];
19
19
  export type PrReviewed = Schemas["PrReviewed"];
20
20
  export type PrClosed = Schemas["PrClosed"];
21
+ export type GateRecord = Schemas["GateRecord"];
22
+ export type GateRequest = Schemas["GateRequest"];
23
+ export type GateView = Schemas["GateView"];
24
+ export type ApiProblem = Schemas["ApiProblem"];
21
25
  /** One reason a document does not match its format. `path` is a JSON Pointer into the document. */
22
26
  export interface Problem {
23
27
  path: string;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,140 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/sesigl/open-software-factory/main/contracts/gate-record/gate-record.schema.json",
4
+ "title": "GateRecord",
5
+ "$comment": "Generated by `pnpm build` in contracts/ from typespec/. Do not edit.",
6
+ "type": "object",
7
+ "properties": {
8
+ "gate": {
9
+ "type": "string",
10
+ "pattern": "^g[0-9a-f]{16}$"
11
+ },
12
+ "workItem": {
13
+ "type": "string"
14
+ },
15
+ "stage": {
16
+ "type": "string",
17
+ "pattern": "^[a-z][a-z0-9-]{0,62}$",
18
+ "description": "A lowercase name: letters, digits and hyphens, starting with a letter. Agents, stages, adapters, sources."
19
+ },
20
+ "summary": {
21
+ "type": "string"
22
+ },
23
+ "request": {
24
+ "type": "string",
25
+ "pattern": "^sha256:[0-9a-f]{64}$",
26
+ "description": "SHA-256 of the canonical request: what was approved."
27
+ },
28
+ "policy": {
29
+ "type": "string",
30
+ "minLength": 1,
31
+ "description": "The policy file."
32
+ },
33
+ "policyDigest": {
34
+ "type": "string",
35
+ "pattern": "^sha256:[0-9a-f]{64}$",
36
+ "description": "SHA-256 of the policy's text: the rules that picked the approvers."
37
+ },
38
+ "risk": {
39
+ "type": "string"
40
+ },
41
+ "approvers": {
42
+ "type": "array",
43
+ "items": {
44
+ "type": "string",
45
+ "pattern": "^[a-z][a-z0-9-]{0,62}$",
46
+ "description": "A lowercase name: letters, digits and hyphens, starting with a letter. Agents, stages, adapters, sources."
47
+ }
48
+ },
49
+ "channel": {
50
+ "type": "string"
51
+ },
52
+ "requestedAt": {
53
+ "type": "string",
54
+ "format": "date-time"
55
+ },
56
+ "decision": {
57
+ "type": "string",
58
+ "enum": [
59
+ "approved",
60
+ "rejected",
61
+ "expired"
62
+ ]
63
+ },
64
+ "decidedBy": {
65
+ "type": "object",
66
+ "properties": {
67
+ "kind": {
68
+ "type": "string",
69
+ "enum": [
70
+ "person",
71
+ "trigger"
72
+ ],
73
+ "description": "Who caused something: a person, or an automated trigger such as a cron rule or an alert."
74
+ },
75
+ "id": {
76
+ "type": "string",
77
+ "minLength": 1,
78
+ "description": "Stable ID in the source system, prefixed with the source, for example \"slack:U024BE7LH\"."
79
+ },
80
+ "displayName": {
81
+ "type": "string",
82
+ "description": "Shown to people, for example \"Varun\"."
83
+ }
84
+ },
85
+ "required": [
86
+ "kind",
87
+ "id"
88
+ ],
89
+ "description": "Who decided. Absent only when the gate expired. A policy that names no group decides as a trigger."
90
+ },
91
+ "decidedAt": {
92
+ "type": "string",
93
+ "format": "date-time"
94
+ },
95
+ "reason": {
96
+ "type": "string",
97
+ "description": "The reason of a rejection."
98
+ },
99
+ "signature": {
100
+ "type": "object",
101
+ "properties": {
102
+ "algorithm": {
103
+ "type": "string",
104
+ "const": "Ed25519"
105
+ },
106
+ "key": {
107
+ "type": "string",
108
+ "pattern": "^sha256:",
109
+ "description": "SHA-256 of the public key in DER, like \"sha256:…\"."
110
+ },
111
+ "value": {
112
+ "type": "string",
113
+ "minLength": 1,
114
+ "description": "The signature in base64."
115
+ }
116
+ },
117
+ "required": [
118
+ "algorithm",
119
+ "key",
120
+ "value"
121
+ ],
122
+ "description": "Ed25519 over the canonical JSON of the rest of the record. Absent when the record is unsigned."
123
+ }
124
+ },
125
+ "required": [
126
+ "gate",
127
+ "workItem",
128
+ "stage",
129
+ "summary",
130
+ "request",
131
+ "policy",
132
+ "policyDigest",
133
+ "approvers",
134
+ "channel",
135
+ "requestedAt",
136
+ "decision",
137
+ "decidedAt"
138
+ ],
139
+ "description": "gatekeeper: who decided what and when, signed with Ed25519 (ADR 0047)."
140
+ }
@@ -2,14 +2,127 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Open Software Factory contracts",
5
- "version": "1.4.0",
6
- "description": "The shared formats as named models. No operations: module APIs are added per module. Generated by `pnpm build` in contracts/ from typespec/. Do not edit.",
5
+ "version": "1.5.0",
6
+ "description": "The shared formats as named models, and the HTTP APIs of the modules. Generated by `pnpm build` in contracts/ from typespec/. Do not edit.",
7
7
  "license": {
8
8
  "name": "Apache-2.0",
9
9
  "identifier": "Apache-2.0"
10
10
  }
11
11
  },
12
- "paths": {},
12
+ "paths": {
13
+ "/v1/gates": {
14
+ "post": {
15
+ "operationId": "GatekeeperApi_requestGate",
16
+ "description": "Ask the policy's approver groups for a decision. The same request again answers the gate it made before.",
17
+ "parameters": [],
18
+ "responses": {
19
+ "200": {
20
+ "description": "The request has succeeded.",
21
+ "content": {
22
+ "application/json": {
23
+ "schema": {
24
+ "$ref": "#/components/schemas/GateView"
25
+ }
26
+ }
27
+ }
28
+ },
29
+ "201": {
30
+ "description": "The request has succeeded and a new resource has been created as a result.",
31
+ "content": {
32
+ "application/json": {
33
+ "schema": {
34
+ "$ref": "#/components/schemas/GateView"
35
+ }
36
+ }
37
+ }
38
+ },
39
+ "400": {
40
+ "description": "The server could not understand the request due to invalid syntax.",
41
+ "content": {
42
+ "application/problem+json": {
43
+ "schema": {
44
+ "$ref": "#/components/schemas/ApiProblem"
45
+ }
46
+ }
47
+ }
48
+ },
49
+ "401": {
50
+ "description": "Access is unauthorized.",
51
+ "content": {
52
+ "application/problem+json": {
53
+ "schema": {
54
+ "$ref": "#/components/schemas/ApiProblem"
55
+ }
56
+ }
57
+ }
58
+ }
59
+ },
60
+ "requestBody": {
61
+ "required": true,
62
+ "content": {
63
+ "application/json": {
64
+ "schema": {
65
+ "$ref": "#/components/schemas/GateRequest"
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ },
72
+ "/v1/gates/{gateId}": {
73
+ "get": {
74
+ "operationId": "GatekeeperApi_getGate",
75
+ "description": "A gate: its state, and its record once it is decided.",
76
+ "parameters": [
77
+ {
78
+ "name": "gateId",
79
+ "in": "path",
80
+ "required": true,
81
+ "schema": {
82
+ "type": "string"
83
+ }
84
+ }
85
+ ],
86
+ "responses": {
87
+ "200": {
88
+ "description": "The request has succeeded.",
89
+ "content": {
90
+ "application/json": {
91
+ "schema": {
92
+ "$ref": "#/components/schemas/GateView"
93
+ }
94
+ }
95
+ }
96
+ },
97
+ "401": {
98
+ "description": "Access is unauthorized.",
99
+ "content": {
100
+ "application/problem+json": {
101
+ "schema": {
102
+ "$ref": "#/components/schemas/ApiProblem"
103
+ }
104
+ }
105
+ }
106
+ },
107
+ "404": {
108
+ "description": "The server cannot find the requested resource.",
109
+ "content": {
110
+ "application/problem+json": {
111
+ "schema": {
112
+ "$ref": "#/components/schemas/ApiProblem"
113
+ }
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+ }
120
+ },
121
+ "security": [
122
+ {
123
+ "BearerAuth": []
124
+ }
125
+ ],
13
126
  "components": {
14
127
  "schemas": {
15
128
  "Actor": {
@@ -101,6 +214,31 @@
101
214
  ],
102
215
  "additionalProperties": false
103
216
  },
217
+ "ApiProblem": {
218
+ "type": "object",
219
+ "properties": {
220
+ "title": {
221
+ "type": "string",
222
+ "minLength": 1,
223
+ "description": "A short summary, the same for every occurrence of the problem."
224
+ },
225
+ "status": {
226
+ "type": "integer",
227
+ "minimum": -2147483648,
228
+ "maximum": 2147483647,
229
+ "description": "The HTTP status code."
230
+ },
231
+ "detail": {
232
+ "type": "string",
233
+ "description": "What went wrong this time, for example the field that is missing."
234
+ }
235
+ },
236
+ "required": [
237
+ "title",
238
+ "status"
239
+ ],
240
+ "description": "An error of an HTTP API (RFC 9457)."
241
+ },
104
242
  "Artifact": {
105
243
  "type": "object",
106
244
  "properties": {
@@ -489,6 +627,127 @@
489
627
  "expired"
490
628
  ]
491
629
  },
630
+ "GateRecord": {
631
+ "type": "object",
632
+ "properties": {
633
+ "gate": {
634
+ "type": "string",
635
+ "pattern": "^g[0-9a-f]{16}$"
636
+ },
637
+ "workItem": {
638
+ "type": "string"
639
+ },
640
+ "stage": {
641
+ "$ref": "#/components/schemas/Name"
642
+ },
643
+ "summary": {
644
+ "type": "string"
645
+ },
646
+ "request": {
647
+ "type": "string",
648
+ "pattern": "^sha256:[0-9a-f]{64}$",
649
+ "description": "SHA-256 of the canonical request: what was approved."
650
+ },
651
+ "policy": {
652
+ "type": "string",
653
+ "minLength": 1,
654
+ "description": "The policy file."
655
+ },
656
+ "policyDigest": {
657
+ "type": "string",
658
+ "pattern": "^sha256:[0-9a-f]{64}$",
659
+ "description": "SHA-256 of the policy's text: the rules that picked the approvers."
660
+ },
661
+ "risk": {
662
+ "type": "string"
663
+ },
664
+ "approvers": {
665
+ "type": "array",
666
+ "items": {
667
+ "$ref": "#/components/schemas/Name"
668
+ }
669
+ },
670
+ "channel": {
671
+ "type": "string"
672
+ },
673
+ "requestedAt": {
674
+ "type": "string",
675
+ "format": "date-time"
676
+ },
677
+ "decision": {
678
+ "$ref": "#/components/schemas/GateDecision"
679
+ },
680
+ "decidedBy": {
681
+ "$ref": "#/components/schemas/Actor",
682
+ "description": "Who decided. Absent only when the gate expired. A policy that names no group decides as a trigger."
683
+ },
684
+ "decidedAt": {
685
+ "type": "string",
686
+ "format": "date-time"
687
+ },
688
+ "reason": {
689
+ "type": "string",
690
+ "description": "The reason of a rejection."
691
+ },
692
+ "signature": {
693
+ "$ref": "#/components/schemas/RecordSignature",
694
+ "description": "Ed25519 over the canonical JSON of the rest of the record. Absent when the record is unsigned."
695
+ }
696
+ },
697
+ "required": [
698
+ "gate",
699
+ "workItem",
700
+ "stage",
701
+ "summary",
702
+ "request",
703
+ "policy",
704
+ "policyDigest",
705
+ "approvers",
706
+ "channel",
707
+ "requestedAt",
708
+ "decision",
709
+ "decidedAt"
710
+ ],
711
+ "description": "gatekeeper: who decided what and when, signed with Ed25519 (ADR 0047)."
712
+ },
713
+ "GateRequest": {
714
+ "type": "object",
715
+ "properties": {
716
+ "workItem": {
717
+ "type": "string",
718
+ "minLength": 1,
719
+ "maxLength": 128,
720
+ "description": "What waits: a work item ID or a URL."
721
+ },
722
+ "stage": {
723
+ "$ref": "#/components/schemas/Name",
724
+ "description": "The stage that waits, for example \"merge\"."
725
+ },
726
+ "summary": {
727
+ "type": "string",
728
+ "minLength": 1,
729
+ "description": "What the approvers decide, shown in the channel."
730
+ },
731
+ "links": {
732
+ "type": "array",
733
+ "items": {
734
+ "$ref": "#/components/schemas/Link"
735
+ }
736
+ },
737
+ "change": {
738
+ "type": "object",
739
+ "properties": {},
740
+ "unevaluatedProperties": {},
741
+ "description": "Any facts the policy needs, such as changed paths, labels or the author."
742
+ }
743
+ },
744
+ "required": [
745
+ "workItem",
746
+ "stage",
747
+ "summary"
748
+ ],
749
+ "description": "gatekeeper: what a person is asked to approve, and the facts the policy decides on (ADR 0047)."
750
+ },
492
751
  "GateRequested": {
493
752
  "type": "object",
494
753
  "properties": {
@@ -597,6 +856,91 @@
597
856
  "summary"
598
857
  ]
599
858
  },
859
+ "GateState": {
860
+ "type": "string",
861
+ "enum": [
862
+ "pending",
863
+ "approved",
864
+ "rejected",
865
+ "expired"
866
+ ],
867
+ "description": "A gate's state: waiting, or how it was decided."
868
+ },
869
+ "GateView": {
870
+ "type": "object",
871
+ "properties": {
872
+ "id": {
873
+ "type": "string",
874
+ "pattern": "^g[0-9a-f]{16}$",
875
+ "description": "\"g\" and 16 hex digits of the SHA-256 of the work item, the stage and the change."
876
+ },
877
+ "workItem": {
878
+ "type": "string"
879
+ },
880
+ "stage": {
881
+ "$ref": "#/components/schemas/Name"
882
+ },
883
+ "summary": {
884
+ "type": "string"
885
+ },
886
+ "risk": {
887
+ "type": "string",
888
+ "description": "The risk the policy named, for example \"high\"."
889
+ },
890
+ "approvers": {
891
+ "type": "array",
892
+ "items": {
893
+ "$ref": "#/components/schemas/Name"
894
+ },
895
+ "description": "The groups whose members may decide. Empty when the policy approved without a person."
896
+ },
897
+ "escalateTo": {
898
+ "type": "array",
899
+ "items": {
900
+ "$ref": "#/components/schemas/Name"
901
+ },
902
+ "description": "The groups asked after the escalation time."
903
+ },
904
+ "channel": {
905
+ "type": "string",
906
+ "description": "Where people decide, for example \"slack:C0GATES\"."
907
+ },
908
+ "state": {
909
+ "$ref": "#/components/schemas/GateState"
910
+ },
911
+ "waitingFor": {
912
+ "type": "array",
913
+ "items": {
914
+ "$ref": "#/components/schemas/Name"
915
+ },
916
+ "description": "The groups that may decide now. Only while the gate is pending."
917
+ },
918
+ "requestedAt": {
919
+ "type": "string",
920
+ "format": "date-time"
921
+ },
922
+ "expiresAt": {
923
+ "type": "string",
924
+ "format": "date-time",
925
+ "description": "When the gate expires without a decision."
926
+ },
927
+ "record": {
928
+ "$ref": "#/components/schemas/GateRecord",
929
+ "description": "The record, once the gate is decided."
930
+ }
931
+ },
932
+ "required": [
933
+ "id",
934
+ "workItem",
935
+ "stage",
936
+ "summary",
937
+ "approvers",
938
+ "channel",
939
+ "state",
940
+ "requestedAt"
941
+ ],
942
+ "description": "gatekeeper: a gate as the HTTP API shows it (ADR 0048)."
943
+ },
600
944
  "Identifier": {
601
945
  "type": "string",
602
946
  "minLength": 1,
@@ -997,6 +1341,30 @@
997
1341
  "description": "The adapters a factory uses, per port (docs/architecture/internals.md). Keys are adapter names.",
998
1342
  "additionalProperties": false
999
1343
  },
1344
+ "RecordSignature": {
1345
+ "type": "object",
1346
+ "properties": {
1347
+ "algorithm": {
1348
+ "type": "string",
1349
+ "const": "Ed25519"
1350
+ },
1351
+ "key": {
1352
+ "type": "string",
1353
+ "pattern": "^sha256:",
1354
+ "description": "SHA-256 of the public key in DER, like \"sha256:…\"."
1355
+ },
1356
+ "value": {
1357
+ "type": "string",
1358
+ "minLength": 1,
1359
+ "description": "The signature in base64."
1360
+ }
1361
+ },
1362
+ "required": [
1363
+ "algorithm",
1364
+ "key",
1365
+ "value"
1366
+ ]
1367
+ },
1000
1368
  "ReviewState": {
1001
1369
  "type": "string",
1002
1370
  "enum": [
@@ -1710,6 +2078,12 @@
1710
2078
  ],
1711
2079
  "description": "intake: a work item was accepted."
1712
2080
  }
2081
+ },
2082
+ "securitySchemes": {
2083
+ "BearerAuth": {
2084
+ "type": "http",
2085
+ "scheme": "Bearer"
2086
+ }
1713
2087
  }
1714
2088
  }
1715
2089
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opensoftware/contracts",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "The shared formats of the open software factory: JSON Schemas, TypeScript types and the osf-contracts CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -39,9 +39,12 @@
39
39
  "devDependencies": {
40
40
  "@asyncapi/parser": "3.6.6",
41
41
  "@typespec/compiler": "1.17.0",
42
+ "@typespec/http": "1.17.0",
42
43
  "@typespec/json-schema": "1.17.0",
44
+ "@typespec/openapi": "1.17.0",
45
+ "@typespec/openapi3": "1.17.0",
43
46
  "openapi-typescript": "7.13.0",
44
- "@opensoftware/testkit": "0.8.0"
47
+ "@opensoftware/testkit": "0.12.0"
45
48
  },
46
49
  "scripts": {
47
50
  "build": "node scripts/build.ts",
@@ -0,0 +1,39 @@
1
+ // The HTTP API of `gatekeeper serve` (ADR 0048). `pnpm build` compiles it with the OpenAPI 3 emitter and copies its
2
+ // paths into openapi/openapi.json; the models are the named models of the JSON Schema bundle.
3
+ import "@typespec/http";
4
+ import "./gatekeeper.tsp";
5
+
6
+ using TypeSpec.Http;
7
+
8
+ @service(#{ title: "Open Software Factory contracts" })
9
+ @useAuth(BearerAuth)
10
+ namespace Osf.Contracts;
11
+
12
+ namespace GatekeeperApi {
13
+ @error
14
+ model Failure<Code extends int32> {
15
+ @statusCode statusCode: Code;
16
+ @header contentType: "application/problem+json";
17
+ @body problem: ApiProblem;
18
+ }
19
+
20
+ /** Ask the policy's approver groups for a decision. The same request again answers the gate it made before. */
21
+ @route("/v1/gates")
22
+ @post
23
+ op requestGate(@body request: GateRequest):
24
+ | {
25
+ @statusCode statusCode: 201;
26
+ @body gate: GateView;
27
+ }
28
+ | {
29
+ @statusCode statusCode: 200;
30
+ @body gate: GateView;
31
+ }
32
+ | Failure<400>
33
+ | Failure<401>;
34
+
35
+ /** A gate: its state, and its record once it is decided. */
36
+ @route("/v1/gates/{gateId}")
37
+ @get
38
+ op getGate(@path gateId: string): GateView | Failure<401> | Failure<404>;
39
+ }
@@ -0,0 +1,134 @@
1
+ import "@typespec/json-schema";
2
+ import "./common.tsp";
3
+
4
+ using TypeSpec.JsonSchema;
5
+
6
+ namespace Osf.Contracts;
7
+
8
+ /** gatekeeper: what a person is asked to approve, and the facts the policy decides on (ADR 0047). */
9
+ model GateRequest {
10
+ /** What waits: a work item ID or a URL. */
11
+ @minLength(1)
12
+ @maxLength(128)
13
+ workItem: string;
14
+
15
+ /** The stage that waits, for example "merge". */
16
+ stage: Name;
17
+
18
+ /** What the approvers decide, shown in the channel. */
19
+ @minLength(1)
20
+ summary: string;
21
+
22
+ links?: Link[];
23
+
24
+ /** Any facts the policy needs, such as changed paths, labels or the author. */
25
+ change?: Record<unknown>;
26
+ }
27
+
28
+ /** A gate's state: waiting, or how it was decided. */
29
+ enum GateState {
30
+ pending,
31
+ approved,
32
+ rejected,
33
+ expired,
34
+ }
35
+
36
+ /** gatekeeper: a gate as the HTTP API shows it (ADR 0048). */
37
+ model GateView {
38
+ /** "g" and 16 hex digits of the SHA-256 of the work item, the stage and the change. */
39
+ @pattern("^g[0-9a-f]{16}$")
40
+ id: string;
41
+
42
+ workItem: string;
43
+ stage: Name;
44
+ summary: string;
45
+
46
+ /** The risk the policy named, for example "high". */
47
+ risk?: string;
48
+
49
+ /** The groups whose members may decide. Empty when the policy approved without a person. */
50
+ approvers: Name[];
51
+
52
+ /** The groups asked after the escalation time. */
53
+ escalateTo?: Name[];
54
+
55
+ /** Where people decide, for example "slack:C0GATES". */
56
+ channel: string;
57
+
58
+ state: GateState;
59
+
60
+ /** The groups that may decide now. Only while the gate is pending. */
61
+ waitingFor?: Name[];
62
+
63
+ requestedAt: utcDateTime;
64
+
65
+ /** When the gate expires without a decision. */
66
+ expiresAt?: utcDateTime;
67
+
68
+ /** The record, once the gate is decided. */
69
+ record?: GateRecord;
70
+ }
71
+
72
+ /** gatekeeper: who decided what and when, signed with Ed25519 (ADR 0047). */
73
+ model GateRecord {
74
+ @pattern("^g[0-9a-f]{16}$")
75
+ gate: string;
76
+
77
+ workItem: string;
78
+ stage: Name;
79
+ summary: string;
80
+
81
+ /** SHA-256 of the canonical request: what was approved. */
82
+ @pattern("^sha256:[0-9a-f]{64}$")
83
+ request: string;
84
+
85
+ /** The policy file. */
86
+ @minLength(1)
87
+ policy: string;
88
+
89
+ /** SHA-256 of the policy's text: the rules that picked the approvers. */
90
+ @pattern("^sha256:[0-9a-f]{64}$")
91
+ policyDigest: string;
92
+
93
+ risk?: string;
94
+ approvers: Name[];
95
+ channel: string;
96
+ requestedAt: utcDateTime;
97
+ decision: GateDecision;
98
+
99
+ /** Who decided. Absent only when the gate expired. A policy that names no group decides as a trigger. */
100
+ decidedBy?: Actor;
101
+
102
+ decidedAt: utcDateTime;
103
+
104
+ /** The reason of a rejection. */
105
+ reason?: string;
106
+
107
+ /** Ed25519 over the canonical JSON of the rest of the record. Absent when the record is unsigned. */
108
+ signature?: RecordSignature;
109
+ }
110
+
111
+ model RecordSignature {
112
+ algorithm: "Ed25519";
113
+
114
+ /** SHA-256 of the public key in DER, like "sha256:…". */
115
+ @pattern("^sha256:")
116
+ key: string;
117
+
118
+ /** The signature in base64. */
119
+ @minLength(1)
120
+ value: string;
121
+ }
122
+
123
+ /** An error of an HTTP API (RFC 9457). */
124
+ model ApiProblem {
125
+ /** A short summary, the same for every occurrence of the problem. */
126
+ @minLength(1)
127
+ title: string;
128
+
129
+ /** The HTTP status code. */
130
+ status: int32;
131
+
132
+ /** What went wrong this time, for example the field that is missing. */
133
+ detail?: string;
134
+ }
package/typespec/main.tsp CHANGED
@@ -8,3 +8,5 @@ import "./run-record.tsp";
8
8
  import "./evidence-bundle.tsp";
9
9
  import "./score.tsp";
10
10
  import "./events.tsp";
11
+ import "./gatekeeper.tsp";
12
+ import "./gatekeeper-api.tsp";