@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>, false>;
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 { RESOURCE_DOMAIN_ID_PREFIX } from "./person-id.js";
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({ key: ResourceDomainKey });
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: [ResourceDomainForbiddenError, ResourceDomainInternalServerError],
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.769",
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": "ab6f8d9ccf2f282c542e388b994099cae91c1354",
39
+ "gitHead": "697e8d55f312105e4f52964dd5a7fb956c0c6d63",
40
40
  "publishConfig": {
41
41
  "access": "public"
42
42
  }