@sealant/api-contracts-next 0.40.0-next.769 → 0.40.0-next.770
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,6 +15,17 @@
|
|
|
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`.
|
|
18
29
|
*/
|
|
19
30
|
import { Schema } from "effect";
|
|
20
31
|
import { HttpApiEndpoint, HttpApiGroup } from "effect/http-api";
|
|
@@ -36,6 +47,40 @@ export declare const ensureResourceDomainResponseSchema: Schema.Struct<{
|
|
|
36
47
|
readonly created: Schema.Boolean;
|
|
37
48
|
}>;
|
|
38
49
|
export type EnsureResourceDomainResponse = typeof ensureResourceDomainResponseSchema.Type;
|
|
50
|
+
export declare const resourceDomainMemberSchema: Schema.Struct<{
|
|
51
|
+
readonly userId: Schema.String;
|
|
52
|
+
readonly state: Schema.Literals<readonly ["member", "revoked"]>;
|
|
53
|
+
readonly version: Schema.Int;
|
|
54
|
+
readonly updatedAt: Schema.String;
|
|
55
|
+
}>;
|
|
56
|
+
export type ResourceDomainMemberWire = typeof resourceDomainMemberSchema.Type;
|
|
57
|
+
export declare const resourceDomainMemberDecisionSchema: Schema.Struct<{
|
|
58
|
+
/** The application's own version of this decision, rising with each decision it makes. */
|
|
59
|
+
readonly version: Schema.Int;
|
|
60
|
+
}>;
|
|
61
|
+
export type ResourceDomainMemberDecisionRequest = typeof resourceDomainMemberDecisionSchema.Type;
|
|
62
|
+
/** At most this many members a page, and this many when the request names no `limit`. */
|
|
63
|
+
export declare const RESOURCE_DOMAIN_MEMBERS_PAGE: {
|
|
64
|
+
readonly default: 100;
|
|
65
|
+
readonly max: 500;
|
|
66
|
+
};
|
|
67
|
+
export declare const listResourceDomainMembersQuerySchema: Schema.Struct<{
|
|
68
|
+
readonly limit: Schema.optional<Schema.FiniteFromString>;
|
|
69
|
+
/** A previous page's `nextCursor`: the page starts after it. */
|
|
70
|
+
readonly cursor: Schema.optional<Schema.String>;
|
|
71
|
+
}>;
|
|
72
|
+
export type ListResourceDomainMembersQuery = typeof listResourceDomainMembersQuerySchema.Type;
|
|
73
|
+
export declare const listResourceDomainMembersResponseSchema: Schema.Struct<{
|
|
74
|
+
readonly items: Schema.$Array<Schema.Struct<{
|
|
75
|
+
readonly userId: Schema.String;
|
|
76
|
+
readonly state: Schema.Literals<readonly ["member", "revoked"]>;
|
|
77
|
+
readonly version: Schema.Int;
|
|
78
|
+
readonly updatedAt: Schema.String;
|
|
79
|
+
}>>;
|
|
80
|
+
/** Present when more members follow: pass it as `cursor` for the next page. */
|
|
81
|
+
readonly nextCursor: Schema.optional<Schema.String>;
|
|
82
|
+
}>;
|
|
83
|
+
export type ListResourceDomainMembersResponse = typeof listResourceDomainMembersResponseSchema.Type;
|
|
39
84
|
declare const ResourceDomainForbiddenError_base: Schema.Class<ResourceDomainForbiddenError, Schema.TaggedStruct<"ResourceDomainForbiddenError", {
|
|
40
85
|
readonly message: Schema.String;
|
|
41
86
|
}>, import("effect/Cause").YieldableError>;
|
|
@@ -46,6 +91,31 @@ declare const ResourceDomainNotFoundError_base: Schema.Class<ResourceDomainNotFo
|
|
|
46
91
|
}>, import("effect/Cause").YieldableError>;
|
|
47
92
|
export declare class ResourceDomainNotFoundError extends ResourceDomainNotFoundError_base {
|
|
48
93
|
}
|
|
94
|
+
declare const ResourceDomainPersonNotFoundError_base: Schema.Class<ResourceDomainPersonNotFoundError, Schema.TaggedStruct<"ResourceDomainPersonNotFoundError", {
|
|
95
|
+
readonly message: Schema.String;
|
|
96
|
+
}>, import("effect/Cause").YieldableError>;
|
|
97
|
+
/** The person a member route names does not exist (the domain does). */
|
|
98
|
+
export declare class ResourceDomainPersonNotFoundError extends ResourceDomainPersonNotFoundError_base {
|
|
99
|
+
}
|
|
100
|
+
declare const ResourceDomainMemberNotFoundError_base: Schema.Class<ResourceDomainMemberNotFoundError, Schema.TaggedStruct<"ResourceDomainMemberNotFoundError", {
|
|
101
|
+
readonly message: Schema.String;
|
|
102
|
+
}>, import("effect/Cause").YieldableError>;
|
|
103
|
+
/** The person exists, and the application has made no decision about them in the domain. */
|
|
104
|
+
export declare class ResourceDomainMemberNotFoundError extends ResourceDomainMemberNotFoundError_base {
|
|
105
|
+
}
|
|
106
|
+
declare const ResourceDomainMemberVersionConflictError_base: Schema.Class<ResourceDomainMemberVersionConflictError, Schema.TaggedStruct<"ResourceDomainMemberVersionConflictError", {
|
|
107
|
+
readonly message: Schema.String;
|
|
108
|
+
readonly current: Schema.Struct<{
|
|
109
|
+
readonly state: Schema.Literals<readonly ["member", "revoked"]>;
|
|
110
|
+
readonly version: Schema.Int;
|
|
111
|
+
}>;
|
|
112
|
+
}>, import("effect/Cause").YieldableError>;
|
|
113
|
+
/**
|
|
114
|
+
* A decision at least as new stands, so this one changed nothing. `current` is the decision that
|
|
115
|
+
* stands; the message quotes nothing the request sent.
|
|
116
|
+
*/
|
|
117
|
+
export declare class ResourceDomainMemberVersionConflictError extends ResourceDomainMemberVersionConflictError_base {
|
|
118
|
+
}
|
|
49
119
|
declare const ResourceDomainInternalServerError_base: Schema.Class<ResourceDomainInternalServerError, Schema.TaggedStruct<"ResourceDomainInternalServerError", {
|
|
50
120
|
readonly message: Schema.String;
|
|
51
121
|
}>, import("effect/Cause").YieldableError>;
|
|
@@ -65,5 +135,50 @@ export declare const ResourceDomainsGroup: HttpApiGroup.HttpApiGroup<"resourceDo
|
|
|
65
135
|
readonly resourceDomainId: Schema.String;
|
|
66
136
|
readonly key: Schema.String;
|
|
67
137
|
readonly createdAt: Schema.String;
|
|
68
|
-
}>>, Schema.toCodecJson<typeof ResourceDomainForbiddenError | typeof ResourceDomainNotFoundError | typeof ResourceDomainInternalServerError>, never, never
|
|
138
|
+
}>>, Schema.toCodecJson<typeof ResourceDomainForbiddenError | typeof ResourceDomainNotFoundError | typeof ResourceDomainInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"getResourceDomainMember", "GET", "/:resourceDomainId/members/:userId", Schema.toCodecStringTree<Schema.Struct<{
|
|
139
|
+
readonly resourceDomainId: Schema.String;
|
|
140
|
+
readonly userId: Schema.String;
|
|
141
|
+
}>>, never, never, never, Schema.toCodecJson<Schema.Struct<{
|
|
142
|
+
readonly userId: Schema.String;
|
|
143
|
+
readonly state: Schema.Literals<readonly ["member", "revoked"]>;
|
|
144
|
+
readonly version: Schema.Int;
|
|
145
|
+
readonly updatedAt: Schema.String;
|
|
146
|
+
}>>, 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<{
|
|
147
|
+
readonly resourceDomainId: Schema.String;
|
|
148
|
+
readonly userId: Schema.String;
|
|
149
|
+
}>>, never, Schema.toCodecJson<Schema.Struct<{
|
|
150
|
+
/** The application's own version of this decision, rising with each decision it makes. */
|
|
151
|
+
readonly version: Schema.Int;
|
|
152
|
+
}>>, never, Schema.toCodecJson<Schema.Struct<{
|
|
153
|
+
readonly userId: Schema.String;
|
|
154
|
+
readonly state: Schema.Literals<readonly ["member", "revoked"]>;
|
|
155
|
+
readonly version: Schema.Int;
|
|
156
|
+
readonly updatedAt: Schema.String;
|
|
157
|
+
}>>, Schema.toCodecJson<typeof ResourceDomainForbiddenError | typeof ResourceDomainNotFoundError | typeof ResourceDomainPersonNotFoundError | typeof ResourceDomainMemberVersionConflictError | typeof ResourceDomainInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"listResourceDomainMembers", "GET", "/:resourceDomainId/members", Schema.toCodecStringTree<Schema.Struct<{
|
|
158
|
+
readonly resourceDomainId: Schema.String;
|
|
159
|
+
}>>, Schema.toCodecStringTree<Schema.Struct<{
|
|
160
|
+
readonly limit: Schema.optional<Schema.FiniteFromString>;
|
|
161
|
+
/** A previous page's `nextCursor`: the page starts after it. */
|
|
162
|
+
readonly cursor: Schema.optional<Schema.String>;
|
|
163
|
+
}>>, never, never, Schema.toCodecJson<Schema.Struct<{
|
|
164
|
+
readonly items: Schema.$Array<Schema.Struct<{
|
|
165
|
+
readonly userId: Schema.String;
|
|
166
|
+
readonly state: Schema.Literals<readonly ["member", "revoked"]>;
|
|
167
|
+
readonly version: Schema.Int;
|
|
168
|
+
readonly updatedAt: Schema.String;
|
|
169
|
+
}>>;
|
|
170
|
+
/** Present when more members follow: pass it as `cursor` for the next page. */
|
|
171
|
+
readonly nextCursor: Schema.optional<Schema.String>;
|
|
172
|
+
}>>, Schema.toCodecJson<typeof ResourceDomainForbiddenError | typeof ResourceDomainNotFoundError | typeof ResourceDomainInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"revokeResourceDomainMember", "POST", "/:resourceDomainId/members/:userId/revoke", Schema.toCodecStringTree<Schema.Struct<{
|
|
173
|
+
readonly resourceDomainId: Schema.String;
|
|
174
|
+
readonly userId: Schema.String;
|
|
175
|
+
}>>, never, Schema.toCodecJson<Schema.Struct<{
|
|
176
|
+
/** The application's own version of this decision, rising with each decision it makes. */
|
|
177
|
+
readonly version: Schema.Int;
|
|
178
|
+
}>>, never, Schema.toCodecJson<Schema.Struct<{
|
|
179
|
+
readonly userId: Schema.String;
|
|
180
|
+
readonly state: Schema.Literals<readonly ["member", "revoked"]>;
|
|
181
|
+
readonly version: Schema.Int;
|
|
182
|
+
readonly updatedAt: Schema.String;
|
|
183
|
+
}>>, Schema.toCodecJson<typeof ResourceDomainForbiddenError | typeof ResourceDomainNotFoundError | typeof ResourceDomainPersonNotFoundError | typeof ResourceDomainMemberVersionConflictError | typeof ResourceDomainInternalServerError>, never, never>, false>;
|
|
69
184
|
export {};
|
|
@@ -15,10 +15,21 @@
|
|
|
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`.
|
|
18
29
|
*/
|
|
19
30
|
import { Schema } from "effect";
|
|
20
31
|
import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/http-api";
|
|
21
|
-
import { RESOURCE_DOMAIN_ID_PREFIX } from "./person-id.js";
|
|
32
|
+
import { PersonId, RESOURCE_DOMAIN_ID_PREFIX } from "./person-id.js";
|
|
22
33
|
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
34
|
const RESOURCE_DOMAIN_KEY_PATTERN = /^[a-z][a-z0-9-]{0,31}:[A-Za-z0-9._:/@+=-]{1,223}$/;
|
|
24
35
|
const ResourceDomainId = Schema.String.check(Schema.makeFilter((id) => RESOURCE_DOMAIN_ID_PATTERN.test(id) ? undefined : "must be a resource domain id (rdm:<uuid>)"));
|
|
@@ -36,12 +47,59 @@ export const ensureResourceDomainResponseSchema = Schema.Struct({
|
|
|
36
47
|
/** True when this call created the domain. */
|
|
37
48
|
created: Schema.Boolean,
|
|
38
49
|
});
|
|
50
|
+
const MemberVersion = Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }));
|
|
51
|
+
const MemberState = Schema.Literals(["member", "revoked"]);
|
|
52
|
+
export const resourceDomainMemberSchema = Schema.Struct({
|
|
53
|
+
userId: PersonId,
|
|
54
|
+
state: MemberState,
|
|
55
|
+
version: MemberVersion,
|
|
56
|
+
updatedAt: Schema.String,
|
|
57
|
+
});
|
|
58
|
+
export const resourceDomainMemberDecisionSchema = Schema.Struct({
|
|
59
|
+
/** The application's own version of this decision, rising with each decision it makes. */
|
|
60
|
+
version: MemberVersion,
|
|
61
|
+
});
|
|
62
|
+
/** At most this many members a page, and this many when the request names no `limit`. */
|
|
63
|
+
export const RESOURCE_DOMAIN_MEMBERS_PAGE = { default: 100, max: 500 };
|
|
64
|
+
export const listResourceDomainMembersQuerySchema = Schema.Struct({
|
|
65
|
+
limit: Schema.optional(Schema.FiniteFromString.check(Schema.isInt(), Schema.isBetween({ minimum: 1, maximum: RESOURCE_DOMAIN_MEMBERS_PAGE.max }))),
|
|
66
|
+
/** A previous page's `nextCursor`: the page starts after it. */
|
|
67
|
+
cursor: Schema.optional(Schema.String.check(Schema.isNonEmpty())),
|
|
68
|
+
});
|
|
69
|
+
export const listResourceDomainMembersResponseSchema = Schema.Struct({
|
|
70
|
+
items: Schema.Array(resourceDomainMemberSchema),
|
|
71
|
+
/** Present when more members follow: pass it as `cursor` for the next page. */
|
|
72
|
+
nextCursor: Schema.optional(Schema.String),
|
|
73
|
+
});
|
|
39
74
|
export class ResourceDomainForbiddenError extends Schema.TaggedError()("ResourceDomainForbiddenError", { message: Schema.String }, { httpApiStatus: 403 }) {
|
|
40
75
|
}
|
|
41
76
|
export class ResourceDomainNotFoundError extends Schema.TaggedError()("ResourceDomainNotFoundError", { message: Schema.String }, { httpApiStatus: 404 }) {
|
|
42
77
|
}
|
|
78
|
+
/** The person a member route names does not exist (the domain does). */
|
|
79
|
+
export class ResourceDomainPersonNotFoundError extends Schema.TaggedError()("ResourceDomainPersonNotFoundError", { message: Schema.String }, { httpApiStatus: 404 }) {
|
|
80
|
+
}
|
|
81
|
+
/** The person exists, and the application has made no decision about them in the domain. */
|
|
82
|
+
export class ResourceDomainMemberNotFoundError extends Schema.TaggedError()("ResourceDomainMemberNotFoundError", { message: Schema.String }, { httpApiStatus: 404 }) {
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A decision at least as new stands, so this one changed nothing. `current` is the decision that
|
|
86
|
+
* stands; the message quotes nothing the request sent.
|
|
87
|
+
*/
|
|
88
|
+
export class ResourceDomainMemberVersionConflictError extends Schema.TaggedError()("ResourceDomainMemberVersionConflictError", {
|
|
89
|
+
message: Schema.String,
|
|
90
|
+
current: Schema.Struct({ state: MemberState, version: MemberVersion }),
|
|
91
|
+
}, { httpApiStatus: 409 }) {
|
|
92
|
+
}
|
|
43
93
|
export class ResourceDomainInternalServerError extends Schema.TaggedError()("ResourceDomainInternalServerError", { message: Schema.String }, { httpApiStatus: 500 }) {
|
|
44
94
|
}
|
|
95
|
+
const memberParams = Schema.Struct({ resourceDomainId: ResourceDomainId, userId: PersonId });
|
|
96
|
+
const memberErrors = [
|
|
97
|
+
ResourceDomainForbiddenError,
|
|
98
|
+
ResourceDomainNotFoundError,
|
|
99
|
+
ResourceDomainPersonNotFoundError,
|
|
100
|
+
ResourceDomainMemberVersionConflictError,
|
|
101
|
+
ResourceDomainInternalServerError,
|
|
102
|
+
];
|
|
45
103
|
export const ResourceDomainsGroup = HttpApiGroup.make("resourceDomains")
|
|
46
104
|
.add(HttpApiEndpoint.post("ensureResourceDomain", "/", {
|
|
47
105
|
payload: ensureResourceDomainRequestSchema,
|
|
@@ -56,6 +114,39 @@ export const ResourceDomainsGroup = HttpApiGroup.make("resourceDomains")
|
|
|
56
114
|
ResourceDomainNotFoundError,
|
|
57
115
|
ResourceDomainInternalServerError,
|
|
58
116
|
],
|
|
117
|
+
}))
|
|
118
|
+
.add(HttpApiEndpoint.post("grantResourceDomainMember", "/:resourceDomainId/members/:userId/grant", {
|
|
119
|
+
params: memberParams,
|
|
120
|
+
payload: resourceDomainMemberDecisionSchema,
|
|
121
|
+
success: resourceDomainMemberSchema,
|
|
122
|
+
error: memberErrors,
|
|
123
|
+
}))
|
|
124
|
+
.add(HttpApiEndpoint.post("revokeResourceDomainMember", "/:resourceDomainId/members/:userId/revoke", {
|
|
125
|
+
params: memberParams,
|
|
126
|
+
payload: resourceDomainMemberDecisionSchema,
|
|
127
|
+
success: resourceDomainMemberSchema,
|
|
128
|
+
error: memberErrors,
|
|
129
|
+
}))
|
|
130
|
+
.add(HttpApiEndpoint.get("getResourceDomainMember", "/:resourceDomainId/members/:userId", {
|
|
131
|
+
params: memberParams,
|
|
132
|
+
success: resourceDomainMemberSchema,
|
|
133
|
+
error: [
|
|
134
|
+
ResourceDomainForbiddenError,
|
|
135
|
+
ResourceDomainNotFoundError,
|
|
136
|
+
ResourceDomainPersonNotFoundError,
|
|
137
|
+
ResourceDomainMemberNotFoundError,
|
|
138
|
+
ResourceDomainInternalServerError,
|
|
139
|
+
],
|
|
140
|
+
}))
|
|
141
|
+
.add(HttpApiEndpoint.get("listResourceDomainMembers", "/:resourceDomainId/members", {
|
|
142
|
+
params: Schema.Struct({ resourceDomainId: ResourceDomainId }),
|
|
143
|
+
query: listResourceDomainMembersQuerySchema,
|
|
144
|
+
success: listResourceDomainMembersResponseSchema,
|
|
145
|
+
error: [
|
|
146
|
+
ResourceDomainForbiddenError,
|
|
147
|
+
ResourceDomainNotFoundError,
|
|
148
|
+
ResourceDomainInternalServerError,
|
|
149
|
+
],
|
|
59
150
|
}))
|
|
60
151
|
.annotate(HttpApi.PayloadParseOptions, { onExcessProperty: "error" })
|
|
61
152
|
.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.770",
|
|
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": "1e8b4f1153f5107504649c863c305cd31f424d58",
|
|
40
40
|
"publishConfig": {
|
|
41
41
|
"access": "public"
|
|
42
42
|
}
|