@sealant/api-contracts-next 0.40.0-next.769 → 0.40.0-next.771
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.
|
@@ -15,9 +15,93 @@
|
|
|
15
15
|
* before a handler runs. A deployment served without authentication (the development exception)
|
|
16
16
|
* serves them as it serves every other route. A request body is decoded strictly: a field the contract does not declare (an
|
|
17
17
|
* owner, a credential) is refused with `400` instead of being dropped.
|
|
18
|
+
*
|
|
19
|
+
* Members are the people the application lets reach the domain. A grant or a revoke carries
|
|
20
|
+
* `version`, the application's own number for its decision about this person, rising with each
|
|
21
|
+
* decision it makes (its record's revision, say). Core writes the decision only over a lower
|
|
22
|
+
* version. The same version with the same state is a repeat, answered with the member unchanged.
|
|
23
|
+
* The same version with the other state, or a lower one, is stale: `409`, carrying the decision
|
|
24
|
+
* that stands. So a delayed grant never undoes a newer revoke, whatever order decisions arrive in
|
|
25
|
+
* and however often each is retried, and the application never reads before it writes. A revoke
|
|
26
|
+
* keeps the member's row, with its version. One member reads at `GET …/members/:userId`, and the
|
|
27
|
+
* list pages by user id: `limit` (100 unless named, at most 500) and the previous page's
|
|
28
|
+
* `nextCursor` as `cursor`.
|
|
29
|
+
*
|
|
30
|
+
* A budget caps CPU (millicores), memory and storage (bytes): ceilings, not measurements, and
|
|
31
|
+
* zero allows nothing. Storage is one allowance for volumes, archived ones included, and the
|
|
32
|
+
* backups and snapshots that protect them. `ensureResourceDomain` may name a new domain's budget
|
|
33
|
+
* and never changes one: naming another gets `409`, carrying the domain and its revision.
|
|
34
|
+
* `setResourceDomainBudget` takes the `revision` it read: a change made since gets `409`, so a
|
|
35
|
+
* delayed update never overwrites a newer one, and it answers the budget it wrote. Changing a
|
|
36
|
+
* budget changes nothing the domain holds. The read is one snapshot. It reports, per dimension,
|
|
37
|
+
* `used` (confirmed to exist), `reserved` (admitted, not yet confirmed), the `limit` and a `warning`
|
|
38
|
+
* once both reach 80%, 95% or all of it; storage also reports its `archived` and `protection` bytes.
|
|
18
39
|
*/
|
|
19
40
|
import { Schema } from "effect";
|
|
20
41
|
import { HttpApiEndpoint, HttpApiGroup } from "effect/http-api";
|
|
42
|
+
/**
|
|
43
|
+
* The dimensions a budget caps, defined once: the contract's schemas, the database's budget, the
|
|
44
|
+
* admission check and the SDK all take theirs from these keys. `freedBy` is what makes room in a
|
|
45
|
+
* dimension besides raising its limit.
|
|
46
|
+
*/
|
|
47
|
+
export declare const RESOURCE_DOMAIN_BUDGET_DIMENSIONS: {
|
|
48
|
+
readonly cpuMillicores: {
|
|
49
|
+
readonly name: "CPU";
|
|
50
|
+
readonly freedBy: "stop-objects";
|
|
51
|
+
};
|
|
52
|
+
readonly memoryBytes: {
|
|
53
|
+
readonly name: "memory";
|
|
54
|
+
readonly freedBy: "stop-objects";
|
|
55
|
+
};
|
|
56
|
+
readonly storageBytes: {
|
|
57
|
+
readonly name: "storage";
|
|
58
|
+
readonly freedBy: "purge-objects";
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
export type ResourceDomainBudgetDimension = keyof typeof RESOURCE_DOMAIN_BUDGET_DIMENSIONS;
|
|
62
|
+
export declare const resourceDomainBudgetLimitsSchema: Schema.Struct<Record<"cpuMillicores" | "memoryBytes" | "storageBytes", Schema.Int>>;
|
|
63
|
+
export type ResourceDomainBudgetLimits = typeof resourceDomainBudgetLimitsSchema.Type;
|
|
64
|
+
/** The share of a limit at which the budget read starts to warn. */
|
|
65
|
+
export declare const RESOURCE_DOMAIN_BUDGET_WARNINGS: {
|
|
66
|
+
readonly "80-percent": 80;
|
|
67
|
+
readonly "95-percent": 95;
|
|
68
|
+
};
|
|
69
|
+
declare const BudgetWarning: Schema.Literals<readonly ["none", "80-percent", "95-percent", "full"]>;
|
|
70
|
+
export type ResourceDomainBudgetWarning = typeof BudgetWarning.Type;
|
|
71
|
+
export declare const resourceDomainBudgetSchema: Schema.Struct<{
|
|
72
|
+
readonly revision: Schema.Int;
|
|
73
|
+
readonly storageBytes: Schema.Struct<{
|
|
74
|
+
readonly limit: Schema.Int;
|
|
75
|
+
readonly used: Schema.Int;
|
|
76
|
+
readonly reserved: Schema.Int;
|
|
77
|
+
readonly warning: Schema.Literals<readonly ["none", "80-percent", "95-percent", "full"]>;
|
|
78
|
+
/** Archived volumes' data bytes, part of `used`: kept until a person purges them. */
|
|
79
|
+
readonly archived: Schema.Int;
|
|
80
|
+
/** Bytes that backups and snapshots hold, not a volume's unused reserve for them. */
|
|
81
|
+
readonly protection: Schema.Int;
|
|
82
|
+
}>;
|
|
83
|
+
readonly cpuMillicores: Schema.Struct<{
|
|
84
|
+
readonly limit: Schema.Int;
|
|
85
|
+
readonly used: Schema.Int;
|
|
86
|
+
readonly reserved: Schema.Int;
|
|
87
|
+
readonly warning: Schema.Literals<readonly ["none", "80-percent", "95-percent", "full"]>;
|
|
88
|
+
}>;
|
|
89
|
+
readonly memoryBytes: Schema.Struct<{
|
|
90
|
+
readonly limit: Schema.Int;
|
|
91
|
+
readonly used: Schema.Int;
|
|
92
|
+
readonly reserved: Schema.Int;
|
|
93
|
+
readonly warning: Schema.Literals<readonly ["none", "80-percent", "95-percent", "full"]>;
|
|
94
|
+
}>;
|
|
95
|
+
}>;
|
|
96
|
+
export type ResourceDomainBudgetWire = typeof resourceDomainBudgetSchema.Type;
|
|
97
|
+
export declare const setResourceDomainBudgetRequestSchema: Schema.Struct<{
|
|
98
|
+
/** The revision this change replaces: the one the caller last read. */
|
|
99
|
+
readonly revision: Schema.Int;
|
|
100
|
+
readonly cpuMillicores: Schema.Int;
|
|
101
|
+
readonly memoryBytes: Schema.Int;
|
|
102
|
+
readonly storageBytes: Schema.Int;
|
|
103
|
+
}>;
|
|
104
|
+
export type SetResourceDomainBudgetRequest = typeof setResourceDomainBudgetRequestSchema.Type;
|
|
21
105
|
export declare const resourceDomainSchema: Schema.Struct<{
|
|
22
106
|
readonly resourceDomainId: Schema.String;
|
|
23
107
|
readonly key: Schema.String;
|
|
@@ -26,6 +110,8 @@ export declare const resourceDomainSchema: Schema.Struct<{
|
|
|
26
110
|
export type ResourceDomainWire = typeof resourceDomainSchema.Type;
|
|
27
111
|
export declare const ensureResourceDomainRequestSchema: Schema.Struct<{
|
|
28
112
|
readonly key: Schema.String;
|
|
113
|
+
/** The budget a new domain starts with; none starts it at zero. */
|
|
114
|
+
readonly budget: Schema.optional<Schema.Struct<Record<"cpuMillicores" | "memoryBytes" | "storageBytes", Schema.Int>>>;
|
|
29
115
|
}>;
|
|
30
116
|
export type EnsureResourceDomainRequest = typeof ensureResourceDomainRequestSchema.Type;
|
|
31
117
|
export declare const ensureResourceDomainResponseSchema: Schema.Struct<{
|
|
@@ -36,6 +122,40 @@ export declare const ensureResourceDomainResponseSchema: Schema.Struct<{
|
|
|
36
122
|
readonly created: Schema.Boolean;
|
|
37
123
|
}>;
|
|
38
124
|
export type EnsureResourceDomainResponse = typeof ensureResourceDomainResponseSchema.Type;
|
|
125
|
+
export declare const resourceDomainMemberSchema: Schema.Struct<{
|
|
126
|
+
readonly userId: Schema.String;
|
|
127
|
+
readonly state: Schema.Literals<readonly ["member", "revoked"]>;
|
|
128
|
+
readonly version: Schema.Int;
|
|
129
|
+
readonly updatedAt: Schema.String;
|
|
130
|
+
}>;
|
|
131
|
+
export type ResourceDomainMemberWire = typeof resourceDomainMemberSchema.Type;
|
|
132
|
+
export declare const resourceDomainMemberDecisionSchema: Schema.Struct<{
|
|
133
|
+
/** The application's own version of this decision, rising with each decision it makes. */
|
|
134
|
+
readonly version: Schema.Int;
|
|
135
|
+
}>;
|
|
136
|
+
export type ResourceDomainMemberDecisionRequest = typeof resourceDomainMemberDecisionSchema.Type;
|
|
137
|
+
/** At most this many members a page, and this many when the request names no `limit`. */
|
|
138
|
+
export declare const RESOURCE_DOMAIN_MEMBERS_PAGE: {
|
|
139
|
+
readonly default: 100;
|
|
140
|
+
readonly max: 500;
|
|
141
|
+
};
|
|
142
|
+
export declare const listResourceDomainMembersQuerySchema: Schema.Struct<{
|
|
143
|
+
readonly limit: Schema.optional<Schema.FiniteFromString>;
|
|
144
|
+
/** A previous page's `nextCursor`: the page starts after it. */
|
|
145
|
+
readonly cursor: Schema.optional<Schema.String>;
|
|
146
|
+
}>;
|
|
147
|
+
export type ListResourceDomainMembersQuery = typeof listResourceDomainMembersQuerySchema.Type;
|
|
148
|
+
export declare const listResourceDomainMembersResponseSchema: Schema.Struct<{
|
|
149
|
+
readonly items: Schema.$Array<Schema.Struct<{
|
|
150
|
+
readonly userId: Schema.String;
|
|
151
|
+
readonly state: Schema.Literals<readonly ["member", "revoked"]>;
|
|
152
|
+
readonly version: Schema.Int;
|
|
153
|
+
readonly updatedAt: Schema.String;
|
|
154
|
+
}>>;
|
|
155
|
+
/** Present when more members follow: pass it as `cursor` for the next page. */
|
|
156
|
+
readonly nextCursor: Schema.optional<Schema.String>;
|
|
157
|
+
}>;
|
|
158
|
+
export type ListResourceDomainMembersResponse = typeof listResourceDomainMembersResponseSchema.Type;
|
|
39
159
|
declare const ResourceDomainForbiddenError_base: Schema.Class<ResourceDomainForbiddenError, Schema.TaggedStruct<"ResourceDomainForbiddenError", {
|
|
40
160
|
readonly message: Schema.String;
|
|
41
161
|
}>, import("effect/Cause").YieldableError>;
|
|
@@ -46,6 +166,46 @@ declare const ResourceDomainNotFoundError_base: Schema.Class<ResourceDomainNotFo
|
|
|
46
166
|
}>, import("effect/Cause").YieldableError>;
|
|
47
167
|
export declare class ResourceDomainNotFoundError extends ResourceDomainNotFoundError_base {
|
|
48
168
|
}
|
|
169
|
+
declare const ResourceDomainPersonNotFoundError_base: Schema.Class<ResourceDomainPersonNotFoundError, Schema.TaggedStruct<"ResourceDomainPersonNotFoundError", {
|
|
170
|
+
readonly message: Schema.String;
|
|
171
|
+
}>, import("effect/Cause").YieldableError>;
|
|
172
|
+
/** The person a member route names does not exist (the domain does). */
|
|
173
|
+
export declare class ResourceDomainPersonNotFoundError extends ResourceDomainPersonNotFoundError_base {
|
|
174
|
+
}
|
|
175
|
+
declare const ResourceDomainMemberNotFoundError_base: Schema.Class<ResourceDomainMemberNotFoundError, Schema.TaggedStruct<"ResourceDomainMemberNotFoundError", {
|
|
176
|
+
readonly message: Schema.String;
|
|
177
|
+
}>, import("effect/Cause").YieldableError>;
|
|
178
|
+
/** The person exists, and the application has made no decision about them in the domain. */
|
|
179
|
+
export declare class ResourceDomainMemberNotFoundError extends ResourceDomainMemberNotFoundError_base {
|
|
180
|
+
}
|
|
181
|
+
declare const ResourceDomainMemberVersionConflictError_base: Schema.Class<ResourceDomainMemberVersionConflictError, Schema.TaggedStruct<"ResourceDomainMemberVersionConflictError", {
|
|
182
|
+
readonly message: Schema.String;
|
|
183
|
+
readonly current: Schema.Struct<{
|
|
184
|
+
readonly state: Schema.Literals<readonly ["member", "revoked"]>;
|
|
185
|
+
readonly version: Schema.Int;
|
|
186
|
+
}>;
|
|
187
|
+
}>, import("effect/Cause").YieldableError>;
|
|
188
|
+
/**
|
|
189
|
+
* A decision at least as new stands, so this one changed nothing. `current` is the decision that
|
|
190
|
+
* stands; the message quotes nothing the request sent.
|
|
191
|
+
*/
|
|
192
|
+
export declare class ResourceDomainMemberVersionConflictError extends ResourceDomainMemberVersionConflictError_base {
|
|
193
|
+
}
|
|
194
|
+
declare const ResourceDomainBudgetDiffersError_base: Schema.Class<ResourceDomainBudgetDiffersError, Schema.TaggedStruct<"ResourceDomainBudgetDiffersError", {
|
|
195
|
+
readonly message: Schema.String;
|
|
196
|
+
readonly resourceDomainId: Schema.String;
|
|
197
|
+
readonly revision: Schema.Int;
|
|
198
|
+
}>, import("effect/Cause").YieldableError>;
|
|
199
|
+
/** The domain exists with another budget, at `revision`; nothing changed. */
|
|
200
|
+
export declare class ResourceDomainBudgetDiffersError extends ResourceDomainBudgetDiffersError_base {
|
|
201
|
+
}
|
|
202
|
+
declare const ResourceDomainBudgetRevisionConflictError_base: Schema.Class<ResourceDomainBudgetRevisionConflictError, Schema.TaggedStruct<"ResourceDomainBudgetRevisionConflictError", {
|
|
203
|
+
readonly message: Schema.String;
|
|
204
|
+
readonly revision: Schema.Int;
|
|
205
|
+
}>, import("effect/Cause").YieldableError>;
|
|
206
|
+
/** `revision` is the current one. */
|
|
207
|
+
export declare class ResourceDomainBudgetRevisionConflictError extends ResourceDomainBudgetRevisionConflictError_base {
|
|
208
|
+
}
|
|
49
209
|
declare const ResourceDomainInternalServerError_base: Schema.Class<ResourceDomainInternalServerError, Schema.TaggedStruct<"ResourceDomainInternalServerError", {
|
|
50
210
|
readonly message: Schema.String;
|
|
51
211
|
}>, import("effect/Cause").YieldableError>;
|
|
@@ -53,17 +213,122 @@ export declare class ResourceDomainInternalServerError extends ResourceDomainInt
|
|
|
53
213
|
}
|
|
54
214
|
export declare const ResourceDomainsGroup: HttpApiGroup.HttpApiGroup<"resourceDomains", HttpApiEndpoint.HttpApiEndpoint<"ensureResourceDomain", "POST", "/", never, never, Schema.toCodecJson<Schema.Struct<{
|
|
55
215
|
readonly key: Schema.String;
|
|
216
|
+
/** The budget a new domain starts with; none starts it at zero. */
|
|
217
|
+
readonly budget: Schema.optional<Schema.Struct<Record<"cpuMillicores" | "memoryBytes" | "storageBytes", Schema.Int>>>;
|
|
56
218
|
}>>, never, Schema.toCodecJson<Schema.Struct<{
|
|
57
219
|
readonly resourceDomainId: Schema.String;
|
|
58
220
|
readonly key: Schema.String;
|
|
59
221
|
readonly createdAt: Schema.String;
|
|
60
222
|
/** True when this call created the domain. */
|
|
61
223
|
readonly created: Schema.Boolean;
|
|
62
|
-
}>>, Schema.toCodecJson<typeof ResourceDomainForbiddenError | typeof ResourceDomainInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"getResourceDomain", "GET", "/:resourceDomainId", Schema.toCodecStringTree<Schema.Struct<{
|
|
224
|
+
}>>, Schema.toCodecJson<typeof ResourceDomainForbiddenError | typeof ResourceDomainBudgetDiffersError | typeof ResourceDomainInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"getResourceDomain", "GET", "/:resourceDomainId", Schema.toCodecStringTree<Schema.Struct<{
|
|
63
225
|
readonly resourceDomainId: Schema.String;
|
|
64
226
|
}>>, never, never, never, Schema.toCodecJson<Schema.Struct<{
|
|
65
227
|
readonly resourceDomainId: Schema.String;
|
|
66
228
|
readonly key: Schema.String;
|
|
67
229
|
readonly createdAt: Schema.String;
|
|
68
|
-
}>>, Schema.toCodecJson<typeof ResourceDomainForbiddenError | typeof ResourceDomainNotFoundError | typeof ResourceDomainInternalServerError>, never, never
|
|
230
|
+
}>>, Schema.toCodecJson<typeof ResourceDomainForbiddenError | typeof ResourceDomainNotFoundError | typeof ResourceDomainInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"getResourceDomainBudget", "GET", "/:resourceDomainId/budget", Schema.toCodecStringTree<Schema.Struct<{
|
|
231
|
+
readonly resourceDomainId: Schema.String;
|
|
232
|
+
}>>, never, never, never, Schema.toCodecJson<Schema.Struct<{
|
|
233
|
+
readonly revision: Schema.Int;
|
|
234
|
+
readonly storageBytes: Schema.Struct<{
|
|
235
|
+
readonly limit: Schema.Int;
|
|
236
|
+
readonly used: Schema.Int;
|
|
237
|
+
readonly reserved: Schema.Int;
|
|
238
|
+
readonly warning: Schema.Literals<readonly ["none", "80-percent", "95-percent", "full"]>;
|
|
239
|
+
/** Archived volumes' data bytes, part of `used`: kept until a person purges them. */
|
|
240
|
+
readonly archived: Schema.Int;
|
|
241
|
+
/** Bytes that backups and snapshots hold, not a volume's unused reserve for them. */
|
|
242
|
+
readonly protection: Schema.Int;
|
|
243
|
+
}>;
|
|
244
|
+
readonly cpuMillicores: Schema.Struct<{
|
|
245
|
+
readonly limit: Schema.Int;
|
|
246
|
+
readonly used: Schema.Int;
|
|
247
|
+
readonly reserved: Schema.Int;
|
|
248
|
+
readonly warning: Schema.Literals<readonly ["none", "80-percent", "95-percent", "full"]>;
|
|
249
|
+
}>;
|
|
250
|
+
readonly memoryBytes: Schema.Struct<{
|
|
251
|
+
readonly limit: Schema.Int;
|
|
252
|
+
readonly used: Schema.Int;
|
|
253
|
+
readonly reserved: Schema.Int;
|
|
254
|
+
readonly warning: Schema.Literals<readonly ["none", "80-percent", "95-percent", "full"]>;
|
|
255
|
+
}>;
|
|
256
|
+
}>>, Schema.toCodecJson<typeof ResourceDomainForbiddenError | typeof ResourceDomainNotFoundError | typeof ResourceDomainInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"getResourceDomainMember", "GET", "/:resourceDomainId/members/:userId", Schema.toCodecStringTree<Schema.Struct<{
|
|
257
|
+
readonly resourceDomainId: Schema.String;
|
|
258
|
+
readonly userId: Schema.String;
|
|
259
|
+
}>>, never, never, never, Schema.toCodecJson<Schema.Struct<{
|
|
260
|
+
readonly userId: Schema.String;
|
|
261
|
+
readonly state: Schema.Literals<readonly ["member", "revoked"]>;
|
|
262
|
+
readonly version: Schema.Int;
|
|
263
|
+
readonly updatedAt: Schema.String;
|
|
264
|
+
}>>, Schema.toCodecJson<typeof ResourceDomainForbiddenError | typeof ResourceDomainNotFoundError | typeof ResourceDomainPersonNotFoundError | typeof ResourceDomainMemberNotFoundError | typeof ResourceDomainInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"grantResourceDomainMember", "POST", "/:resourceDomainId/members/:userId/grant", Schema.toCodecStringTree<Schema.Struct<{
|
|
265
|
+
readonly resourceDomainId: Schema.String;
|
|
266
|
+
readonly userId: Schema.String;
|
|
267
|
+
}>>, never, Schema.toCodecJson<Schema.Struct<{
|
|
268
|
+
/** The application's own version of this decision, rising with each decision it makes. */
|
|
269
|
+
readonly version: Schema.Int;
|
|
270
|
+
}>>, never, Schema.toCodecJson<Schema.Struct<{
|
|
271
|
+
readonly userId: Schema.String;
|
|
272
|
+
readonly state: Schema.Literals<readonly ["member", "revoked"]>;
|
|
273
|
+
readonly version: Schema.Int;
|
|
274
|
+
readonly updatedAt: Schema.String;
|
|
275
|
+
}>>, Schema.toCodecJson<typeof ResourceDomainForbiddenError | typeof ResourceDomainNotFoundError | typeof ResourceDomainPersonNotFoundError | typeof ResourceDomainMemberVersionConflictError | typeof ResourceDomainInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"listResourceDomainMembers", "GET", "/:resourceDomainId/members", Schema.toCodecStringTree<Schema.Struct<{
|
|
276
|
+
readonly resourceDomainId: Schema.String;
|
|
277
|
+
}>>, Schema.toCodecStringTree<Schema.Struct<{
|
|
278
|
+
readonly limit: Schema.optional<Schema.FiniteFromString>;
|
|
279
|
+
/** A previous page's `nextCursor`: the page starts after it. */
|
|
280
|
+
readonly cursor: Schema.optional<Schema.String>;
|
|
281
|
+
}>>, never, never, Schema.toCodecJson<Schema.Struct<{
|
|
282
|
+
readonly items: Schema.$Array<Schema.Struct<{
|
|
283
|
+
readonly userId: Schema.String;
|
|
284
|
+
readonly state: Schema.Literals<readonly ["member", "revoked"]>;
|
|
285
|
+
readonly version: Schema.Int;
|
|
286
|
+
readonly updatedAt: Schema.String;
|
|
287
|
+
}>>;
|
|
288
|
+
/** Present when more members follow: pass it as `cursor` for the next page. */
|
|
289
|
+
readonly nextCursor: Schema.optional<Schema.String>;
|
|
290
|
+
}>>, Schema.toCodecJson<typeof ResourceDomainForbiddenError | typeof ResourceDomainNotFoundError | typeof ResourceDomainInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"revokeResourceDomainMember", "POST", "/:resourceDomainId/members/:userId/revoke", Schema.toCodecStringTree<Schema.Struct<{
|
|
291
|
+
readonly resourceDomainId: Schema.String;
|
|
292
|
+
readonly userId: Schema.String;
|
|
293
|
+
}>>, never, Schema.toCodecJson<Schema.Struct<{
|
|
294
|
+
/** The application's own version of this decision, rising with each decision it makes. */
|
|
295
|
+
readonly version: Schema.Int;
|
|
296
|
+
}>>, never, Schema.toCodecJson<Schema.Struct<{
|
|
297
|
+
readonly userId: Schema.String;
|
|
298
|
+
readonly state: Schema.Literals<readonly ["member", "revoked"]>;
|
|
299
|
+
readonly version: Schema.Int;
|
|
300
|
+
readonly updatedAt: Schema.String;
|
|
301
|
+
}>>, Schema.toCodecJson<typeof ResourceDomainForbiddenError | typeof ResourceDomainNotFoundError | typeof ResourceDomainPersonNotFoundError | typeof ResourceDomainMemberVersionConflictError | typeof ResourceDomainInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"setResourceDomainBudget", "PUT", "/:resourceDomainId/budget", Schema.toCodecStringTree<Schema.Struct<{
|
|
302
|
+
readonly resourceDomainId: Schema.String;
|
|
303
|
+
}>>, never, Schema.toCodecJson<Schema.Struct<{
|
|
304
|
+
/** The revision this change replaces: the one the caller last read. */
|
|
305
|
+
readonly revision: Schema.Int;
|
|
306
|
+
readonly cpuMillicores: Schema.Int;
|
|
307
|
+
readonly memoryBytes: Schema.Int;
|
|
308
|
+
readonly storageBytes: Schema.Int;
|
|
309
|
+
}>>, never, Schema.toCodecJson<Schema.Struct<{
|
|
310
|
+
readonly revision: Schema.Int;
|
|
311
|
+
readonly storageBytes: Schema.Struct<{
|
|
312
|
+
readonly limit: Schema.Int;
|
|
313
|
+
readonly used: Schema.Int;
|
|
314
|
+
readonly reserved: Schema.Int;
|
|
315
|
+
readonly warning: Schema.Literals<readonly ["none", "80-percent", "95-percent", "full"]>;
|
|
316
|
+
/** Archived volumes' data bytes, part of `used`: kept until a person purges them. */
|
|
317
|
+
readonly archived: Schema.Int;
|
|
318
|
+
/** Bytes that backups and snapshots hold, not a volume's unused reserve for them. */
|
|
319
|
+
readonly protection: Schema.Int;
|
|
320
|
+
}>;
|
|
321
|
+
readonly cpuMillicores: Schema.Struct<{
|
|
322
|
+
readonly limit: Schema.Int;
|
|
323
|
+
readonly used: Schema.Int;
|
|
324
|
+
readonly reserved: Schema.Int;
|
|
325
|
+
readonly warning: Schema.Literals<readonly ["none", "80-percent", "95-percent", "full"]>;
|
|
326
|
+
}>;
|
|
327
|
+
readonly memoryBytes: Schema.Struct<{
|
|
328
|
+
readonly limit: Schema.Int;
|
|
329
|
+
readonly used: Schema.Int;
|
|
330
|
+
readonly reserved: Schema.Int;
|
|
331
|
+
readonly warning: Schema.Literals<readonly ["none", "80-percent", "95-percent", "full"]>;
|
|
332
|
+
}>;
|
|
333
|
+
}>>, Schema.toCodecJson<typeof ResourceDomainForbiddenError | typeof ResourceDomainNotFoundError | typeof ResourceDomainBudgetRevisionConflictError | typeof ResourceDomainInternalServerError>, never, never>, false>;
|
|
69
334
|
export {};
|
|
@@ -15,38 +15,159 @@
|
|
|
15
15
|
* before a handler runs. A deployment served without authentication (the development exception)
|
|
16
16
|
* serves them as it serves every other route. A request body is decoded strictly: a field the contract does not declare (an
|
|
17
17
|
* owner, a credential) is refused with `400` instead of being dropped.
|
|
18
|
+
*
|
|
19
|
+
* Members are the people the application lets reach the domain. A grant or a revoke carries
|
|
20
|
+
* `version`, the application's own number for its decision about this person, rising with each
|
|
21
|
+
* decision it makes (its record's revision, say). Core writes the decision only over a lower
|
|
22
|
+
* version. The same version with the same state is a repeat, answered with the member unchanged.
|
|
23
|
+
* The same version with the other state, or a lower one, is stale: `409`, carrying the decision
|
|
24
|
+
* that stands. So a delayed grant never undoes a newer revoke, whatever order decisions arrive in
|
|
25
|
+
* and however often each is retried, and the application never reads before it writes. A revoke
|
|
26
|
+
* keeps the member's row, with its version. One member reads at `GET …/members/:userId`, and the
|
|
27
|
+
* list pages by user id: `limit` (100 unless named, at most 500) and the previous page's
|
|
28
|
+
* `nextCursor` as `cursor`.
|
|
29
|
+
*
|
|
30
|
+
* A budget caps CPU (millicores), memory and storage (bytes): ceilings, not measurements, and
|
|
31
|
+
* zero allows nothing. Storage is one allowance for volumes, archived ones included, and the
|
|
32
|
+
* backups and snapshots that protect them. `ensureResourceDomain` may name a new domain's budget
|
|
33
|
+
* and never changes one: naming another gets `409`, carrying the domain and its revision.
|
|
34
|
+
* `setResourceDomainBudget` takes the `revision` it read: a change made since gets `409`, so a
|
|
35
|
+
* delayed update never overwrites a newer one, and it answers the budget it wrote. Changing a
|
|
36
|
+
* budget changes nothing the domain holds. The read is one snapshot. It reports, per dimension,
|
|
37
|
+
* `used` (confirmed to exist), `reserved` (admitted, not yet confirmed), the `limit` and a `warning`
|
|
38
|
+
* once both reach 80%, 95% or all of it; storage also reports its `archived` and `protection` bytes.
|
|
18
39
|
*/
|
|
19
40
|
import { Schema } from "effect";
|
|
20
41
|
import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api";
|
|
21
|
-
import
|
|
42
|
+
import * as Rec from "effect/Record";
|
|
43
|
+
import { PersonId, RESOURCE_DOMAIN_ID_PREFIX } from "./person-id.js";
|
|
22
44
|
const RESOURCE_DOMAIN_ID_PATTERN = new RegExp(`^${RESOURCE_DOMAIN_ID_PREFIX}[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`);
|
|
23
45
|
const RESOURCE_DOMAIN_KEY_PATTERN = /^[a-z][a-z0-9-]{0,31}:[A-Za-z0-9._:/@+=-]{1,223}$/;
|
|
24
46
|
const ResourceDomainId = Schema.String.check(Schema.makeFilter((id) => RESOURCE_DOMAIN_ID_PATTERN.test(id) ? undefined : "must be a resource domain id (rdm:<uuid>)"));
|
|
25
47
|
const ResourceDomainKey = Schema.String.check(Schema.makeFilter((key) => RESOURCE_DOMAIN_KEY_PATTERN.test(key)
|
|
26
48
|
? undefined
|
|
27
49
|
: "must be <application>:<name>: a lower-case application name (letters, digits, '-'), a colon, then at most 223 letters, digits, '.', '_', ':', '/', '@', '+', '=' or '-'"));
|
|
50
|
+
const Amount = Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }));
|
|
51
|
+
const BudgetRevision = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }));
|
|
52
|
+
/**
|
|
53
|
+
* The dimensions a budget caps, defined once: the contract's schemas, the database's budget, the
|
|
54
|
+
* admission check and the SDK all take theirs from these keys. `freedBy` is what makes room in a
|
|
55
|
+
* dimension besides raising its limit.
|
|
56
|
+
*/
|
|
57
|
+
export const RESOURCE_DOMAIN_BUDGET_DIMENSIONS = {
|
|
58
|
+
cpuMillicores: { name: "CPU", freedBy: "stop-objects" },
|
|
59
|
+
memoryBytes: { name: "memory", freedBy: "stop-objects" },
|
|
60
|
+
storageBytes: { name: "storage", freedBy: "purge-objects" },
|
|
61
|
+
};
|
|
62
|
+
export const resourceDomainBudgetLimitsSchema = Schema.Struct(Rec.map(RESOURCE_DOMAIN_BUDGET_DIMENSIONS, () => Amount));
|
|
63
|
+
/** The share of a limit at which the budget read starts to warn. */
|
|
64
|
+
export const RESOURCE_DOMAIN_BUDGET_WARNINGS = { "80-percent": 80, "95-percent": 95 };
|
|
65
|
+
const BudgetWarning = Schema.Literals(["none", "80-percent", "95-percent", "full"]);
|
|
66
|
+
const budgetDimensionSchema = Schema.Struct({
|
|
67
|
+
limit: Amount,
|
|
68
|
+
used: Amount,
|
|
69
|
+
reserved: Amount,
|
|
70
|
+
warning: BudgetWarning,
|
|
71
|
+
});
|
|
72
|
+
export const resourceDomainBudgetSchema = Schema.Struct({
|
|
73
|
+
revision: BudgetRevision,
|
|
74
|
+
...Rec.map(RESOURCE_DOMAIN_BUDGET_DIMENSIONS, () => budgetDimensionSchema),
|
|
75
|
+
storageBytes: Schema.Struct({
|
|
76
|
+
...budgetDimensionSchema.fields,
|
|
77
|
+
/** Archived volumes' data bytes, part of `used`: kept until a person purges them. */
|
|
78
|
+
archived: Amount,
|
|
79
|
+
/** Bytes that backups and snapshots hold, not a volume's unused reserve for them. */
|
|
80
|
+
protection: Amount,
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
export const setResourceDomainBudgetRequestSchema = Schema.Struct({
|
|
84
|
+
...resourceDomainBudgetLimitsSchema.fields,
|
|
85
|
+
/** The revision this change replaces: the one the caller last read. */
|
|
86
|
+
revision: BudgetRevision,
|
|
87
|
+
});
|
|
28
88
|
export const resourceDomainSchema = Schema.Struct({
|
|
29
89
|
resourceDomainId: ResourceDomainId,
|
|
30
90
|
key: ResourceDomainKey,
|
|
31
91
|
createdAt: Schema.String,
|
|
32
92
|
});
|
|
33
|
-
export const ensureResourceDomainRequestSchema = Schema.Struct({
|
|
93
|
+
export const ensureResourceDomainRequestSchema = Schema.Struct({
|
|
94
|
+
key: ResourceDomainKey,
|
|
95
|
+
/** The budget a new domain starts with; none starts it at zero. */
|
|
96
|
+
budget: Schema.optional(resourceDomainBudgetLimitsSchema),
|
|
97
|
+
});
|
|
34
98
|
export const ensureResourceDomainResponseSchema = Schema.Struct({
|
|
35
99
|
...resourceDomainSchema.fields,
|
|
36
100
|
/** True when this call created the domain. */
|
|
37
101
|
created: Schema.Boolean,
|
|
38
102
|
});
|
|
103
|
+
const MemberVersion = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }));
|
|
104
|
+
const MemberState = Schema.Literals(["member", "revoked"]);
|
|
105
|
+
export const resourceDomainMemberSchema = Schema.Struct({
|
|
106
|
+
userId: PersonId,
|
|
107
|
+
state: MemberState,
|
|
108
|
+
version: MemberVersion,
|
|
109
|
+
updatedAt: Schema.String,
|
|
110
|
+
});
|
|
111
|
+
export const resourceDomainMemberDecisionSchema = Schema.Struct({
|
|
112
|
+
/** The application's own version of this decision, rising with each decision it makes. */
|
|
113
|
+
version: MemberVersion,
|
|
114
|
+
});
|
|
115
|
+
/** At most this many members a page, and this many when the request names no `limit`. */
|
|
116
|
+
export const RESOURCE_DOMAIN_MEMBERS_PAGE = { default: 100, max: 500 };
|
|
117
|
+
export const listResourceDomainMembersQuerySchema = Schema.Struct({
|
|
118
|
+
limit: Schema.optional(Schema.FiniteFromString.check(Schema.isInt(), Schema.isBetween({ minimum: 1, maximum: RESOURCE_DOMAIN_MEMBERS_PAGE.max }))),
|
|
119
|
+
/** A previous page's `nextCursor`: the page starts after it. */
|
|
120
|
+
cursor: Schema.optional(Schema.String.check(Schema.isNonEmpty())),
|
|
121
|
+
});
|
|
122
|
+
export const listResourceDomainMembersResponseSchema = Schema.Struct({
|
|
123
|
+
items: Schema.Array(resourceDomainMemberSchema),
|
|
124
|
+
/** Present when more members follow: pass it as `cursor` for the next page. */
|
|
125
|
+
nextCursor: Schema.optional(Schema.String),
|
|
126
|
+
});
|
|
39
127
|
export class ResourceDomainForbiddenError extends Schema.TaggedError()("ResourceDomainForbiddenError", { message: Schema.String }, { httpApiStatus: 403 }) {
|
|
40
128
|
}
|
|
41
129
|
export class ResourceDomainNotFoundError extends Schema.TaggedError()("ResourceDomainNotFoundError", { message: Schema.String }, { httpApiStatus: 404 }) {
|
|
42
130
|
}
|
|
131
|
+
/** The person a member route names does not exist (the domain does). */
|
|
132
|
+
export class ResourceDomainPersonNotFoundError extends Schema.TaggedError()("ResourceDomainPersonNotFoundError", { message: Schema.String }, { httpApiStatus: 404 }) {
|
|
133
|
+
}
|
|
134
|
+
/** The person exists, and the application has made no decision about them in the domain. */
|
|
135
|
+
export class ResourceDomainMemberNotFoundError extends Schema.TaggedError()("ResourceDomainMemberNotFoundError", { message: Schema.String }, { httpApiStatus: 404 }) {
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* A decision at least as new stands, so this one changed nothing. `current` is the decision that
|
|
139
|
+
* stands; the message quotes nothing the request sent.
|
|
140
|
+
*/
|
|
141
|
+
export class ResourceDomainMemberVersionConflictError extends Schema.TaggedError()("ResourceDomainMemberVersionConflictError", {
|
|
142
|
+
message: Schema.String,
|
|
143
|
+
current: Schema.Struct({ state: MemberState, version: MemberVersion }),
|
|
144
|
+
}, { httpApiStatus: 409 }) {
|
|
145
|
+
}
|
|
146
|
+
/** The domain exists with another budget, at `revision`; nothing changed. */
|
|
147
|
+
export class ResourceDomainBudgetDiffersError extends Schema.TaggedError()("ResourceDomainBudgetDiffersError", { message: Schema.String, resourceDomainId: ResourceDomainId, revision: BudgetRevision }, { httpApiStatus: 409 }) {
|
|
148
|
+
}
|
|
149
|
+
/** `revision` is the current one. */
|
|
150
|
+
export class ResourceDomainBudgetRevisionConflictError extends Schema.TaggedError()("ResourceDomainBudgetRevisionConflictError", { message: Schema.String, revision: BudgetRevision }, { httpApiStatus: 409 }) {
|
|
151
|
+
}
|
|
43
152
|
export class ResourceDomainInternalServerError extends Schema.TaggedError()("ResourceDomainInternalServerError", { message: Schema.String }, { httpApiStatus: 500 }) {
|
|
44
153
|
}
|
|
154
|
+
const memberParams = Schema.Struct({ resourceDomainId: ResourceDomainId, userId: PersonId });
|
|
155
|
+
const memberErrors = [
|
|
156
|
+
ResourceDomainForbiddenError,
|
|
157
|
+
ResourceDomainNotFoundError,
|
|
158
|
+
ResourceDomainPersonNotFoundError,
|
|
159
|
+
ResourceDomainMemberVersionConflictError,
|
|
160
|
+
ResourceDomainInternalServerError,
|
|
161
|
+
];
|
|
45
162
|
export const ResourceDomainsGroup = HttpApiGroup.make("resourceDomains")
|
|
46
163
|
.add(HttpApiEndpoint.post("ensureResourceDomain", "/", {
|
|
47
164
|
payload: ensureResourceDomainRequestSchema,
|
|
48
165
|
success: ensureResourceDomainResponseSchema,
|
|
49
|
-
error: [
|
|
166
|
+
error: [
|
|
167
|
+
ResourceDomainForbiddenError,
|
|
168
|
+
ResourceDomainBudgetDiffersError,
|
|
169
|
+
ResourceDomainInternalServerError,
|
|
170
|
+
],
|
|
50
171
|
}))
|
|
51
172
|
.add(HttpApiEndpoint.get("getResourceDomain", "/:resourceDomainId", {
|
|
52
173
|
params: Schema.Struct({ resourceDomainId: ResourceDomainId }),
|
|
@@ -56,6 +177,59 @@ export const ResourceDomainsGroup = HttpApiGroup.make("resourceDomains")
|
|
|
56
177
|
ResourceDomainNotFoundError,
|
|
57
178
|
ResourceDomainInternalServerError,
|
|
58
179
|
],
|
|
180
|
+
}))
|
|
181
|
+
.add(HttpApiEndpoint.post("grantResourceDomainMember", "/:resourceDomainId/members/:userId/grant", {
|
|
182
|
+
params: memberParams,
|
|
183
|
+
payload: resourceDomainMemberDecisionSchema,
|
|
184
|
+
success: resourceDomainMemberSchema,
|
|
185
|
+
error: memberErrors,
|
|
186
|
+
}))
|
|
187
|
+
.add(HttpApiEndpoint.post("revokeResourceDomainMember", "/:resourceDomainId/members/:userId/revoke", {
|
|
188
|
+
params: memberParams,
|
|
189
|
+
payload: resourceDomainMemberDecisionSchema,
|
|
190
|
+
success: resourceDomainMemberSchema,
|
|
191
|
+
error: memberErrors,
|
|
192
|
+
}))
|
|
193
|
+
.add(HttpApiEndpoint.get("getResourceDomainMember", "/:resourceDomainId/members/:userId", {
|
|
194
|
+
params: memberParams,
|
|
195
|
+
success: resourceDomainMemberSchema,
|
|
196
|
+
error: [
|
|
197
|
+
ResourceDomainForbiddenError,
|
|
198
|
+
ResourceDomainNotFoundError,
|
|
199
|
+
ResourceDomainPersonNotFoundError,
|
|
200
|
+
ResourceDomainMemberNotFoundError,
|
|
201
|
+
ResourceDomainInternalServerError,
|
|
202
|
+
],
|
|
203
|
+
}))
|
|
204
|
+
.add(HttpApiEndpoint.get("listResourceDomainMembers", "/:resourceDomainId/members", {
|
|
205
|
+
params: Schema.Struct({ resourceDomainId: ResourceDomainId }),
|
|
206
|
+
query: listResourceDomainMembersQuerySchema,
|
|
207
|
+
success: listResourceDomainMembersResponseSchema,
|
|
208
|
+
error: [
|
|
209
|
+
ResourceDomainForbiddenError,
|
|
210
|
+
ResourceDomainNotFoundError,
|
|
211
|
+
ResourceDomainInternalServerError,
|
|
212
|
+
],
|
|
213
|
+
}))
|
|
214
|
+
.add(HttpApiEndpoint.get("getResourceDomainBudget", "/:resourceDomainId/budget", {
|
|
215
|
+
params: Schema.Struct({ resourceDomainId: ResourceDomainId }),
|
|
216
|
+
success: resourceDomainBudgetSchema,
|
|
217
|
+
error: [
|
|
218
|
+
ResourceDomainForbiddenError,
|
|
219
|
+
ResourceDomainNotFoundError,
|
|
220
|
+
ResourceDomainInternalServerError,
|
|
221
|
+
],
|
|
222
|
+
}))
|
|
223
|
+
.add(HttpApiEndpoint.put("setResourceDomainBudget", "/:resourceDomainId/budget", {
|
|
224
|
+
params: Schema.Struct({ resourceDomainId: ResourceDomainId }),
|
|
225
|
+
payload: setResourceDomainBudgetRequestSchema,
|
|
226
|
+
success: resourceDomainBudgetSchema,
|
|
227
|
+
error: [
|
|
228
|
+
ResourceDomainForbiddenError,
|
|
229
|
+
ResourceDomainNotFoundError,
|
|
230
|
+
ResourceDomainBudgetRevisionConflictError,
|
|
231
|
+
ResourceDomainInternalServerError,
|
|
232
|
+
],
|
|
59
233
|
}))
|
|
60
234
|
.annotate(HttpApi.PayloadParseOptions, { onExcessProperty: "error" })
|
|
61
235
|
.annotate(OpenApi.Description, "Owners that are projects, not people: they never hold a login or a provider credential.");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sealant/api-contracts-next",
|
|
3
|
-
"version": "0.40.0-next.
|
|
3
|
+
"version": "0.40.0-next.771",
|
|
4
4
|
"description": "Contract-first HTTP API definitions for Sealant services",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"effect": "^4.0.5"
|
|
38
38
|
},
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "697e8d55f312105e4f52964dd5a7fb956c0c6d63",
|
|
40
40
|
"publishConfig": {
|
|
41
41
|
"access": "public"
|
|
42
42
|
}
|