@opensoftware/contracts 1.0.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.
@@ -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";