@plantops/contracts 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/audit.d.ts +149 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +87 -0
- package/dist/bindings.d.ts +125 -0
- package/dist/bindings.d.ts.map +1 -0
- package/dist/bindings.js +40 -0
- package/dist/clients.d.ts +240 -0
- package/dist/clients.d.ts.map +1 -0
- package/dist/clients.js +49 -0
- package/dist/constants.d.ts +91 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +92 -0
- package/dist/entitlements.d.ts +135 -0
- package/dist/entitlements.d.ts.map +1 -0
- package/dist/entitlements.js +43 -0
- package/dist/errors.d.ts +68 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +83 -0
- package/dist/grants.d.ts +70 -0
- package/dist/grants.d.ts.map +1 -0
- package/dist/grants.js +13 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/jwt.d.ts +155 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js +49 -0
- package/dist/lib/contracts.d.ts +2 -0
- package/dist/lib/contracts.d.ts.map +1 -0
- package/dist/lib/contracts.js +3 -0
- package/dist/manifest.d.ts +136 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +22 -0
- package/dist/nav.d.ts +53 -0
- package/dist/nav.d.ts.map +1 -0
- package/dist/nav.js +25 -0
- package/dist/pagination.d.ts +28 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +27 -0
- package/dist/registry.d.ts +183 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +24 -0
- package/dist/roles.d.ts +215 -0
- package/dist/roles.d.ts.map +1 -0
- package/dist/roles.js +45 -0
- package/dist/scopes.d.ts +220 -0
- package/dist/scopes.d.ts.map +1 -0
- package/dist/scopes.js +108 -0
- package/dist/service-accounts.d.ts +81 -0
- package/dist/service-accounts.d.ts.map +1 -0
- package/dist/service-accounts.js +35 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/dist/type-assertions.d.ts +18 -0
- package/dist/type-assertions.d.ts.map +1 -0
- package/dist/type-assertions.js +8 -0
- package/dist/users.d.ts +364 -0
- package/dist/users.d.ts.map +1 -0
- package/dist/users.js +134 -0
- package/package.json +39 -0
package/dist/users.d.ts
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User contract — the WHO of the access equation (Doc 01 §3.6, Doc 06 §8).
|
|
3
|
+
*
|
|
4
|
+
* A user is a human identity belonging to **exactly one** client. There is no
|
|
5
|
+
* global user and there is not going to be one: `email` is unique per client and
|
|
6
|
+
* login is by `(client_slug, email)` (Doc 03 §3), so the same address is a
|
|
7
|
+
* different person in two tenants and neither of them can see the other.
|
|
8
|
+
*
|
|
9
|
+
* ## No credential is ever on this shape
|
|
10
|
+
*
|
|
11
|
+
* Not the hash — that lives on `user_identity` (Doc 01 §4.6), so an admin list
|
|
12
|
+
* cannot carry one by accident — and not a plaintext password either, because
|
|
13
|
+
* {@link CreateUserRequest} has no password field. A user created here has no
|
|
14
|
+
* credential at all until they set one through the tokenized reset flow of
|
|
15
|
+
* Doc 03 §7, which is the same flow Doc 09 §3.3 puts behind the detail screen's
|
|
16
|
+
* *reset password* button. That makes creation and invitation one act instead of
|
|
17
|
+
* two, and means no operator ever handles somebody else's password.
|
|
18
|
+
*
|
|
19
|
+
* The single exception on the whole surface is `POST /iam/clients/:id/admins`
|
|
20
|
+
* ({@link ClientAdminDTO}), where a platform operator provisions a tenant's very
|
|
21
|
+
* first administrator and there is nobody yet who *could* invite them.
|
|
22
|
+
*
|
|
23
|
+
* ## Status is a state machine, not a field
|
|
24
|
+
*
|
|
25
|
+
* {@link UserStatus} looks like an ordinary enum column and is not: `locked` and
|
|
26
|
+
* `disabled` both force-log-out every session the user has (Doc 03 §6, Doc 04
|
|
27
|
+
* §7), and `disabled` is one-way — an offboarded account is not quietly walked
|
|
28
|
+
* back to `active` through the same control that unlocks a mistyped password.
|
|
29
|
+
* {@link UpdateUserRequest} therefore accepts a status, but the transitions it
|
|
30
|
+
* may express are the ones Doc 03 §8's table allows.
|
|
31
|
+
*
|
|
32
|
+
* Field naming is snake_case, matching every other published shape here.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* The three account states (Doc 03 §8).
|
|
36
|
+
*
|
|
37
|
+
* | State | Login | Meaning |
|
|
38
|
+
* |---|---|---|
|
|
39
|
+
* | `active` | allowed | the ordinary case |
|
|
40
|
+
* | `locked` | refused `423` | an administrator, or the failed-attempt policy — the "Account Locked Users" list of Doc 09 §3.3 |
|
|
41
|
+
* | `disabled` | refused | offboarded, sessions revoked |
|
|
42
|
+
*
|
|
43
|
+
* Spelled here rather than imported from `@plantops/db`, which contracts must
|
|
44
|
+
* not depend on — it has zero dependencies by design (Doc 08 §3). The Postgres
|
|
45
|
+
* enum is the same three values in the same order, and `libs/db`'s
|
|
46
|
+
* `entities.spec.ts` asserts the two spellings against each other so they cannot
|
|
47
|
+
* drift into a status the API accepts and the column rejects. Same arrangement
|
|
48
|
+
* as {@link ClientStatus} and {@link ServiceAccountStatus}.
|
|
49
|
+
*/
|
|
50
|
+
export declare const UserStatus: {
|
|
51
|
+
readonly ACTIVE: "active";
|
|
52
|
+
readonly LOCKED: "locked";
|
|
53
|
+
readonly DISABLED: "disabled";
|
|
54
|
+
};
|
|
55
|
+
export type UserStatus = (typeof UserStatus)[keyof typeof UserStatus];
|
|
56
|
+
export declare const USER_STATUS_VALUES: readonly ["active", "locked", "disabled"];
|
|
57
|
+
/**
|
|
58
|
+
* One user of one tenant (Doc 01 §3.6).
|
|
59
|
+
*
|
|
60
|
+
* `is_client_admin` is published but not settable: it is Doc 01 §3.6's "shortcut
|
|
61
|
+
* flag; still enforced via permissions", the interim stand-in the client-tier
|
|
62
|
+
* services read until Session 23 turns tenant administration into an ordinary
|
|
63
|
+
* `iam.client.*` binding. A screen still has to be able to *show* who holds it —
|
|
64
|
+
* an admin list in which the administrators are indistinguishable is not much of
|
|
65
|
+
* a list — so it is on the read shape and absent from every write one.
|
|
66
|
+
*/
|
|
67
|
+
export interface UserDTO {
|
|
68
|
+
id: string;
|
|
69
|
+
client_id: string;
|
|
70
|
+
/** Lowercased on write; half of the login credential (Doc 03 §3). */
|
|
71
|
+
email: string;
|
|
72
|
+
full_name: string;
|
|
73
|
+
/** Optional, and reserved for the WhatsApp channel of Doc 03 §10. */
|
|
74
|
+
phone: string | null;
|
|
75
|
+
status: UserStatus;
|
|
76
|
+
/** Interim tenant-administration flag — read-only on this surface. */
|
|
77
|
+
is_client_admin: boolean;
|
|
78
|
+
/** ISO-8601. */
|
|
79
|
+
created_at: string;
|
|
80
|
+
updated_at: string;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* One grant this user holds, as the detail screen's *bindings* sub-panel renders
|
|
84
|
+
* it (Doc 09 §3.3, Doc 01 §4.5).
|
|
85
|
+
*
|
|
86
|
+
* A projection of `role_binding` **from the user's side**, which is why it names
|
|
87
|
+
* no subject: the subject is the user this arrived with. Session 20's
|
|
88
|
+
* `/iam/role-bindings` surface publishes the symmetric shape — one that names
|
|
89
|
+
* the subject and is filterable by role and scope — and the two are deliberately
|
|
90
|
+
* separate: this one exists to answer "what can this person do", and a panel
|
|
91
|
+
* that repeated the person's own id in every row would be answering a question
|
|
92
|
+
* nobody asked.
|
|
93
|
+
*
|
|
94
|
+
* `role_name` and `scope_node_path` are joined in rather than left to the caller
|
|
95
|
+
* to resolve, because a binding rendered as two uuids is not a rendering of
|
|
96
|
+
* anything.
|
|
97
|
+
*/
|
|
98
|
+
export interface UserBindingDTO {
|
|
99
|
+
/** The `role_binding` row — what `DELETE /iam/role-bindings/:id` takes. */
|
|
100
|
+
id: string;
|
|
101
|
+
role_id: string;
|
|
102
|
+
role_name: string;
|
|
103
|
+
scope_node_id: string;
|
|
104
|
+
scope_node_name: string;
|
|
105
|
+
/** The materialized `ltree` path — `n_<hex>` labels, never display names (Doc 01 §3.5). */
|
|
106
|
+
scope_node_path: string;
|
|
107
|
+
/** ISO-8601, or `null` for a grant that does not expire (Doc 01 §4.5). */
|
|
108
|
+
expires_at: string | null;
|
|
109
|
+
/**
|
|
110
|
+
* Whether {@link expires_at} has passed.
|
|
111
|
+
*
|
|
112
|
+
* Expired bindings are **listed, not hidden**: a grant that lapsed last Friday
|
|
113
|
+
* is the answer to "why did this stop working", and a panel that silently
|
|
114
|
+
* dropped the row would leave that question unanswerable. Resolution ignores
|
|
115
|
+
* them (Doc 04 §4), so the flag is what keeps the screen honest about the
|
|
116
|
+
* difference between held and effective.
|
|
117
|
+
*/
|
|
118
|
+
expired: boolean;
|
|
119
|
+
created_at: string;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* `GET /iam/users/:id` — the profile with its grants (Doc 06 §8, Doc 09 §3.3).
|
|
123
|
+
*
|
|
124
|
+
* Bindings are inlined here and deliberately absent from {@link UserDTO}: the
|
|
125
|
+
* list is a page of up to a hundred people and a grant set per row is a payload
|
|
126
|
+
* nobody reads, while the detail screen cannot render without them. The same
|
|
127
|
+
* split {@link RoleDTO} makes for its permission mapping, for the same reason.
|
|
128
|
+
*/
|
|
129
|
+
export interface UserDetailDTO extends UserDTO {
|
|
130
|
+
bindings: UserBindingDTO[];
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* `POST /iam/users` body (Doc 06 §8, Doc 09 §3.3).
|
|
134
|
+
*
|
|
135
|
+
* The client is the caller's own, taken from the token's `cid` and from nowhere
|
|
136
|
+
* else. `is_client_admin` is absent for the reason `is_system` is absent from
|
|
137
|
+
* {@link CreateRoleRequest} — a flag that grants administration is not a field
|
|
138
|
+
* of a create form — and there is no password, for the reason in the header.
|
|
139
|
+
*
|
|
140
|
+
* `status` is Doc 09 §3.3's "initial status", and it is a genuine field rather
|
|
141
|
+
* than a fixed `active`: a tenant onboarding a plant in advance of its opening
|
|
142
|
+
* creates the accounts now and turns them on later, and the alternative — create
|
|
143
|
+
* active, then immediately disable — is two audit records for one intention.
|
|
144
|
+
*/
|
|
145
|
+
export interface CreateUserRequest {
|
|
146
|
+
email: string;
|
|
147
|
+
full_name: string;
|
|
148
|
+
phone?: string;
|
|
149
|
+
/** Defaults to `active`. */
|
|
150
|
+
status?: UserStatus;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* `PATCH /iam/users/:id` body — Doc 06 §8's "update, lock, unlock, disable".
|
|
154
|
+
*
|
|
155
|
+
* One route for the profile and the state machine, because Doc 06 §8 gives them
|
|
156
|
+
* one, and because they are the same screen's save button (Doc 09 §3.3). What
|
|
157
|
+
* separates them is what happens underneath: a changed `full_name` is an
|
|
158
|
+
* `update`, while a changed `status` runs a transition that revokes sessions and
|
|
159
|
+
* invalidates grants (Doc 03 §8, Doc 04 §7).
|
|
160
|
+
*
|
|
161
|
+
* `email` is editable — it is the only way to correct a typo in an address
|
|
162
|
+
* nobody can log in with, and there is no `DELETE /iam/users/:id` to fall back
|
|
163
|
+
* on (offboarding is `disabled`, so that a person's access history survives
|
|
164
|
+
* them). Changing it changes half the login credential, which is why it is
|
|
165
|
+
* audited with its before and after like any other field.
|
|
166
|
+
*
|
|
167
|
+
* `is_client_admin` is not here. Promoting somebody is an access grant, and
|
|
168
|
+
* access grants are role bindings (Session 20) — routing one through a boolean
|
|
169
|
+
* on a profile form would put the tenant's most consequential change on the
|
|
170
|
+
* screen least likely to be reviewed.
|
|
171
|
+
*/
|
|
172
|
+
export interface UpdateUserRequest {
|
|
173
|
+
email?: string;
|
|
174
|
+
full_name?: string;
|
|
175
|
+
phone?: string;
|
|
176
|
+
status?: UserStatus;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* How many data rows one `POST /iam/users/bulk` may carry.
|
|
180
|
+
*
|
|
181
|
+
* The same argument {@link MAX_PERMISSIONS_PER_ROLE} makes: the whole upload is
|
|
182
|
+
* validated and written inside one request transaction, so an unbounded body is
|
|
183
|
+
* an unbounded transaction — and this one holds it open while it writes user
|
|
184
|
+
* rows an administrator is about to start binding roles to.
|
|
185
|
+
*
|
|
186
|
+
* Five hundred rather than two hundred because the thing being uploaded is a
|
|
187
|
+
* staff list rather than a hand-curated selection: a plant's shift roster is a
|
|
188
|
+
* few hundred people, and splitting one onboarding into three files to satisfy a
|
|
189
|
+
* limit is friction with nothing behind it. It is still well inside the 64 kB
|
|
190
|
+
* body ceiling of Doc 06 §1 at any realistic row width, so the request is
|
|
191
|
+
* refused for its row count — which names the problem — rather than for its
|
|
192
|
+
* byte count, which does not.
|
|
193
|
+
*/
|
|
194
|
+
export declare const MAX_BULK_USER_ROWS = 500;
|
|
195
|
+
/**
|
|
196
|
+
* The two shapes a bulk upload may take (Doc 06 §8's "CSV/JSON").
|
|
197
|
+
*
|
|
198
|
+
* Both arrive as `application/json`, and that is deliberate rather than a
|
|
199
|
+
* compromise. A `multipart/form-data` upload would need a second body parser in
|
|
200
|
+
* front of a surface that has exactly one (`body-parser.middleware.ts`), a
|
|
201
|
+
* second failure taxonomy to translate into the Doc 06 §2 envelope, and a second
|
|
202
|
+
* ceiling to publish — all so that a browser could send bytes it has already
|
|
203
|
+
* read into memory to render the operator's preview. Carrying the CSV as a
|
|
204
|
+
* string inside the JSON envelope keeps one parser, one ceiling and one error
|
|
205
|
+
* shape.
|
|
206
|
+
*/
|
|
207
|
+
export declare const BULK_USER_UPLOAD_FORMATS: readonly ["csv", "json"];
|
|
208
|
+
export type BulkUserUploadFormat = (typeof BULK_USER_UPLOAD_FORMATS)[number];
|
|
209
|
+
/**
|
|
210
|
+
* The CSV columns a bulk upload is read from, in the order the template writes
|
|
211
|
+
* them.
|
|
212
|
+
*
|
|
213
|
+
* Matching is by header **name**, case- and whitespace-insensitive, never by
|
|
214
|
+
* position: a spreadsheet round-trip reorders columns, and a file whose `phone`
|
|
215
|
+
* column silently became its `status` column is the failure mode a positional
|
|
216
|
+
* reader cannot detect. Columns not named here are ignored, which is the same
|
|
217
|
+
* treatment `z.object` gives an unknown key on the JSON side (`users.dto.ts`).
|
|
218
|
+
*
|
|
219
|
+
* `email` and `full_name` are required *of the header*; `phone` and `status` may
|
|
220
|
+
* be absent entirely, and an empty cell in either is the field being omitted
|
|
221
|
+
* rather than being set to the empty string.
|
|
222
|
+
*/
|
|
223
|
+
export declare const BULK_USER_CSV_COLUMNS: readonly ["email", "full_name", "phone", "status"];
|
|
224
|
+
/**
|
|
225
|
+
* `POST /iam/users/bulk` body (Doc 06 §8).
|
|
226
|
+
*
|
|
227
|
+
* Discriminated on `format` rather than inferred from which field is present,
|
|
228
|
+
* so an upload that carries neither — or both — is refused by the schema with a
|
|
229
|
+
* message that names the field, instead of being guessed at.
|
|
230
|
+
*
|
|
231
|
+
* The `json` arm's rows are typed as {@link CreateUserRequest} here because that
|
|
232
|
+
* is what a well-formed row *is*. The server-side schema deliberately types the
|
|
233
|
+
* array as unknown and validates each element separately: a body validated
|
|
234
|
+
* element-wise by the pipe would fail the whole request on its first bad row,
|
|
235
|
+
* and the per-row report this endpoint exists to produce would never be reached
|
|
236
|
+
* (see `users.dto.ts`).
|
|
237
|
+
*/
|
|
238
|
+
export type BulkUserUploadRequest = {
|
|
239
|
+
format: 'csv';
|
|
240
|
+
content: string;
|
|
241
|
+
} | {
|
|
242
|
+
format: 'json';
|
|
243
|
+
users: CreateUserRequest[];
|
|
244
|
+
};
|
|
245
|
+
/**
|
|
246
|
+
* What happened to one row (Doc 09 §3.3's "created / skipped / errored").
|
|
247
|
+
*
|
|
248
|
+
* The distinction between the two failures is *whose* problem it is:
|
|
249
|
+
*
|
|
250
|
+
* - `skipped` — the row was well-formed and describes a user who already
|
|
251
|
+
* exists, either earlier in this same file or already in the tenant. Nothing
|
|
252
|
+
* is wrong with the file; re-uploading it after adding people is the ordinary
|
|
253
|
+
* way this endpoint gets used, and every previously-uploaded row skipping is
|
|
254
|
+
* what makes that safe.
|
|
255
|
+
* - `errored` — the row could not be read as a user at all. It is a defect in
|
|
256
|
+
* the file, and it is the only status whose rows an operator has to go and
|
|
257
|
+
* fix.
|
|
258
|
+
*/
|
|
259
|
+
export declare const BulkUserRowStatus: {
|
|
260
|
+
readonly CREATED: "created";
|
|
261
|
+
readonly SKIPPED: "skipped";
|
|
262
|
+
readonly ERRORED: "errored";
|
|
263
|
+
};
|
|
264
|
+
export type BulkUserRowStatus = (typeof BulkUserRowStatus)[keyof typeof BulkUserRowStatus];
|
|
265
|
+
export declare const BULK_USER_ROW_STATUS_VALUES: readonly ["created", "skipped", "errored"];
|
|
266
|
+
/**
|
|
267
|
+
* One line of the per-row result report (Doc 06 §8, Doc 09 §3.3).
|
|
268
|
+
*
|
|
269
|
+
* `row` is 1-based and counts **data** rows, so row 1 is the first person in the
|
|
270
|
+
* file whichever format it arrived in — a CSV header is not row 1, and the
|
|
271
|
+
* report an operator reads beside their spreadsheet should not be off by one
|
|
272
|
+
* against it.
|
|
273
|
+
*
|
|
274
|
+
* `email` is echoed as submitted, normalized where it could be parsed and `null`
|
|
275
|
+
* where it could not, because a report keyed only by row number is one an
|
|
276
|
+
* operator has to hold two documents open to read.
|
|
277
|
+
*/
|
|
278
|
+
export interface BulkUserRowResult {
|
|
279
|
+
row: number;
|
|
280
|
+
email: string | null;
|
|
281
|
+
status: BulkUserRowStatus;
|
|
282
|
+
/**
|
|
283
|
+
* Why, for everything that is not `created`.
|
|
284
|
+
*
|
|
285
|
+
* Always present on `skipped` and `errored` and always absent on `created`:
|
|
286
|
+
* Doc 09 §3.3's report table has a reason column, and a blank cell beside a
|
|
287
|
+
* refusal is the one thing it must never show.
|
|
288
|
+
*/
|
|
289
|
+
reason?: string;
|
|
290
|
+
/** The created user's id, or `null` — the handle the UI links the row to. */
|
|
291
|
+
user_id: string | null;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* `POST /iam/users/bulk` → the per-row report and its counts (Doc 06 §8).
|
|
295
|
+
*
|
|
296
|
+
* A `200`, not a `201`, and not a `207`. The request as a whole succeeded — it
|
|
297
|
+
* was read, every row was adjudicated, and the answer is this report — so the
|
|
298
|
+
* status describes the request and the report describes the rows. A `201` would
|
|
299
|
+
* claim a location that does not exist (there are up to
|
|
300
|
+
* {@link MAX_BULK_USER_ROWS} of them), and a `207` is a WebDAV multi-status
|
|
301
|
+
* whose body shape is not this one and which the Doc 06 §2 code table does not
|
|
302
|
+
* contain.
|
|
303
|
+
*
|
|
304
|
+
* ## Partial success, stated precisely
|
|
305
|
+
*
|
|
306
|
+
* Valid rows commit even when others do not. What that means mechanically is
|
|
307
|
+
* that an invalid row is *never attempted*: every row is validated, and every
|
|
308
|
+
* duplicate resolved, before a single insert runs, so the write itself cannot
|
|
309
|
+
* fail on a row's account. The transaction is still all-or-nothing — an
|
|
310
|
+
* unexpected database failure rolls the whole upload back and answers `500`,
|
|
311
|
+
* leaving no half-loaded tenant — and `created` in this report therefore always
|
|
312
|
+
* means committed.
|
|
313
|
+
*
|
|
314
|
+
* `results` covers every row in file order, including the created ones, so the
|
|
315
|
+
* report can be rendered as the file with a verdict column rather than as three
|
|
316
|
+
* separate lists.
|
|
317
|
+
*/
|
|
318
|
+
export interface BulkUserUploadResponse {
|
|
319
|
+
/** Rows read from the file — the length of {@link results}. */
|
|
320
|
+
total: number;
|
|
321
|
+
created: number;
|
|
322
|
+
skipped: number;
|
|
323
|
+
errored: number;
|
|
324
|
+
results: BulkUserRowResult[];
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* One place a user holds the role being asked about — Session 34's "scope
|
|
328
|
+
* context".
|
|
329
|
+
*
|
|
330
|
+
* The mirror image of {@link UserBindingDTO}: that one names the role and omits
|
|
331
|
+
* the subject, because it answers "what can this person do"; this one names the
|
|
332
|
+
* scope and omits the role, because it answers "who can do this, and where". A
|
|
333
|
+
* shape that carried both would be `role_binding` itself, which is Session 20's
|
|
334
|
+
* surface and a different question again.
|
|
335
|
+
*/
|
|
336
|
+
export interface RoleScopeGrantDTO {
|
|
337
|
+
/** The `role_binding` row — what `DELETE /iam/role-bindings/:id` takes. */
|
|
338
|
+
binding_id: string;
|
|
339
|
+
scope_node_id: string;
|
|
340
|
+
scope_node_name: string;
|
|
341
|
+
/** The materialized `ltree` path — `n_<hex>` labels (Doc 01 §3.5). */
|
|
342
|
+
scope_node_path: string;
|
|
343
|
+
expires_at: string | null;
|
|
344
|
+
/** Whether {@link expires_at} has passed — see {@link UserBindingDTO.expired}. */
|
|
345
|
+
expired: boolean;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* `GET /iam/users/by-role/:roleId` — one holder of the role (Doc 06 §8).
|
|
349
|
+
*
|
|
350
|
+
* A user appears **once**, with every scope they hold the role at gathered into
|
|
351
|
+
* {@link scopes}, rather than once per binding. "Who has this role" is a
|
|
352
|
+
* question about people, so somebody granted it at four plants is one row of the
|
|
353
|
+
* answer and not four — and the pagination envelope counts people, which is what
|
|
354
|
+
* a total under a role picker has to mean.
|
|
355
|
+
*
|
|
356
|
+
* Expired bindings are listed and flagged rather than dropped, for the reason
|
|
357
|
+
* {@link UserBindingDTO.expired} gives: a lapsed grant is the answer to "why did
|
|
358
|
+
* this stop working". A holder *all* of whose grants have expired still appears,
|
|
359
|
+
* with every entry flagged.
|
|
360
|
+
*/
|
|
361
|
+
export interface UserByRoleDTO extends UserDTO {
|
|
362
|
+
scopes: RoleScopeGrantDTO[];
|
|
363
|
+
}
|
|
364
|
+
//# sourceMappingURL=users.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../src/users.ts"],"names":[],"mappings":"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"}
|
package/dist/users.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User contract — the WHO of the access equation (Doc 01 §3.6, Doc 06 §8).
|
|
3
|
+
*
|
|
4
|
+
* A user is a human identity belonging to **exactly one** client. There is no
|
|
5
|
+
* global user and there is not going to be one: `email` is unique per client and
|
|
6
|
+
* login is by `(client_slug, email)` (Doc 03 §3), so the same address is a
|
|
7
|
+
* different person in two tenants and neither of them can see the other.
|
|
8
|
+
*
|
|
9
|
+
* ## No credential is ever on this shape
|
|
10
|
+
*
|
|
11
|
+
* Not the hash — that lives on `user_identity` (Doc 01 §4.6), so an admin list
|
|
12
|
+
* cannot carry one by accident — and not a plaintext password either, because
|
|
13
|
+
* {@link CreateUserRequest} has no password field. A user created here has no
|
|
14
|
+
* credential at all until they set one through the tokenized reset flow of
|
|
15
|
+
* Doc 03 §7, which is the same flow Doc 09 §3.3 puts behind the detail screen's
|
|
16
|
+
* *reset password* button. That makes creation and invitation one act instead of
|
|
17
|
+
* two, and means no operator ever handles somebody else's password.
|
|
18
|
+
*
|
|
19
|
+
* The single exception on the whole surface is `POST /iam/clients/:id/admins`
|
|
20
|
+
* ({@link ClientAdminDTO}), where a platform operator provisions a tenant's very
|
|
21
|
+
* first administrator and there is nobody yet who *could* invite them.
|
|
22
|
+
*
|
|
23
|
+
* ## Status is a state machine, not a field
|
|
24
|
+
*
|
|
25
|
+
* {@link UserStatus} looks like an ordinary enum column and is not: `locked` and
|
|
26
|
+
* `disabled` both force-log-out every session the user has (Doc 03 §6, Doc 04
|
|
27
|
+
* §7), and `disabled` is one-way — an offboarded account is not quietly walked
|
|
28
|
+
* back to `active` through the same control that unlocks a mistyped password.
|
|
29
|
+
* {@link UpdateUserRequest} therefore accepts a status, but the transitions it
|
|
30
|
+
* may express are the ones Doc 03 §8's table allows.
|
|
31
|
+
*
|
|
32
|
+
* Field naming is snake_case, matching every other published shape here.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* The three account states (Doc 03 §8).
|
|
36
|
+
*
|
|
37
|
+
* | State | Login | Meaning |
|
|
38
|
+
* |---|---|---|
|
|
39
|
+
* | `active` | allowed | the ordinary case |
|
|
40
|
+
* | `locked` | refused `423` | an administrator, or the failed-attempt policy — the "Account Locked Users" list of Doc 09 §3.3 |
|
|
41
|
+
* | `disabled` | refused | offboarded, sessions revoked |
|
|
42
|
+
*
|
|
43
|
+
* Spelled here rather than imported from `@plantops/db`, which contracts must
|
|
44
|
+
* not depend on — it has zero dependencies by design (Doc 08 §3). The Postgres
|
|
45
|
+
* enum is the same three values in the same order, and `libs/db`'s
|
|
46
|
+
* `entities.spec.ts` asserts the two spellings against each other so they cannot
|
|
47
|
+
* drift into a status the API accepts and the column rejects. Same arrangement
|
|
48
|
+
* as {@link ClientStatus} and {@link ServiceAccountStatus}.
|
|
49
|
+
*/
|
|
50
|
+
export const UserStatus = {
|
|
51
|
+
ACTIVE: 'active',
|
|
52
|
+
LOCKED: 'locked',
|
|
53
|
+
DISABLED: 'disabled',
|
|
54
|
+
};
|
|
55
|
+
export const USER_STATUS_VALUES = [
|
|
56
|
+
UserStatus.ACTIVE,
|
|
57
|
+
UserStatus.LOCKED,
|
|
58
|
+
UserStatus.DISABLED,
|
|
59
|
+
];
|
|
60
|
+
// ── bulk upload (Doc 06 §8, Doc 09 §3.3) ────────────────────────────────────
|
|
61
|
+
/**
|
|
62
|
+
* How many data rows one `POST /iam/users/bulk` may carry.
|
|
63
|
+
*
|
|
64
|
+
* The same argument {@link MAX_PERMISSIONS_PER_ROLE} makes: the whole upload is
|
|
65
|
+
* validated and written inside one request transaction, so an unbounded body is
|
|
66
|
+
* an unbounded transaction — and this one holds it open while it writes user
|
|
67
|
+
* rows an administrator is about to start binding roles to.
|
|
68
|
+
*
|
|
69
|
+
* Five hundred rather than two hundred because the thing being uploaded is a
|
|
70
|
+
* staff list rather than a hand-curated selection: a plant's shift roster is a
|
|
71
|
+
* few hundred people, and splitting one onboarding into three files to satisfy a
|
|
72
|
+
* limit is friction with nothing behind it. It is still well inside the 64 kB
|
|
73
|
+
* body ceiling of Doc 06 §1 at any realistic row width, so the request is
|
|
74
|
+
* refused for its row count — which names the problem — rather than for its
|
|
75
|
+
* byte count, which does not.
|
|
76
|
+
*/
|
|
77
|
+
export const MAX_BULK_USER_ROWS = 500;
|
|
78
|
+
/**
|
|
79
|
+
* The two shapes a bulk upload may take (Doc 06 §8's "CSV/JSON").
|
|
80
|
+
*
|
|
81
|
+
* Both arrive as `application/json`, and that is deliberate rather than a
|
|
82
|
+
* compromise. A `multipart/form-data` upload would need a second body parser in
|
|
83
|
+
* front of a surface that has exactly one (`body-parser.middleware.ts`), a
|
|
84
|
+
* second failure taxonomy to translate into the Doc 06 §2 envelope, and a second
|
|
85
|
+
* ceiling to publish — all so that a browser could send bytes it has already
|
|
86
|
+
* read into memory to render the operator's preview. Carrying the CSV as a
|
|
87
|
+
* string inside the JSON envelope keeps one parser, one ceiling and one error
|
|
88
|
+
* shape.
|
|
89
|
+
*/
|
|
90
|
+
export const BULK_USER_UPLOAD_FORMATS = ['csv', 'json'];
|
|
91
|
+
/**
|
|
92
|
+
* The CSV columns a bulk upload is read from, in the order the template writes
|
|
93
|
+
* them.
|
|
94
|
+
*
|
|
95
|
+
* Matching is by header **name**, case- and whitespace-insensitive, never by
|
|
96
|
+
* position: a spreadsheet round-trip reorders columns, and a file whose `phone`
|
|
97
|
+
* column silently became its `status` column is the failure mode a positional
|
|
98
|
+
* reader cannot detect. Columns not named here are ignored, which is the same
|
|
99
|
+
* treatment `z.object` gives an unknown key on the JSON side (`users.dto.ts`).
|
|
100
|
+
*
|
|
101
|
+
* `email` and `full_name` are required *of the header*; `phone` and `status` may
|
|
102
|
+
* be absent entirely, and an empty cell in either is the field being omitted
|
|
103
|
+
* rather than being set to the empty string.
|
|
104
|
+
*/
|
|
105
|
+
export const BULK_USER_CSV_COLUMNS = [
|
|
106
|
+
'email',
|
|
107
|
+
'full_name',
|
|
108
|
+
'phone',
|
|
109
|
+
'status',
|
|
110
|
+
];
|
|
111
|
+
/**
|
|
112
|
+
* What happened to one row (Doc 09 §3.3's "created / skipped / errored").
|
|
113
|
+
*
|
|
114
|
+
* The distinction between the two failures is *whose* problem it is:
|
|
115
|
+
*
|
|
116
|
+
* - `skipped` — the row was well-formed and describes a user who already
|
|
117
|
+
* exists, either earlier in this same file or already in the tenant. Nothing
|
|
118
|
+
* is wrong with the file; re-uploading it after adding people is the ordinary
|
|
119
|
+
* way this endpoint gets used, and every previously-uploaded row skipping is
|
|
120
|
+
* what makes that safe.
|
|
121
|
+
* - `errored` — the row could not be read as a user at all. It is a defect in
|
|
122
|
+
* the file, and it is the only status whose rows an operator has to go and
|
|
123
|
+
* fix.
|
|
124
|
+
*/
|
|
125
|
+
export const BulkUserRowStatus = {
|
|
126
|
+
CREATED: 'created',
|
|
127
|
+
SKIPPED: 'skipped',
|
|
128
|
+
ERRORED: 'errored',
|
|
129
|
+
};
|
|
130
|
+
export const BULK_USER_ROW_STATUS_VALUES = [
|
|
131
|
+
BulkUserRowStatus.CREATED,
|
|
132
|
+
BulkUserRowStatus.SKIPPED,
|
|
133
|
+
BulkUserRowStatus.ERRORED,
|
|
134
|
+
];
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@plantops/contracts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
"./package.json": "./package.json",
|
|
11
|
+
".": {
|
|
12
|
+
"@plantops/source": "./src/index.ts",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"nx": {
|
|
19
|
+
"tags": [
|
|
20
|
+
"type:lib",
|
|
21
|
+
"scope:contracts"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"tslib": "^2.3.0"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/dileepraghumajji/plant-ops.git",
|
|
31
|
+
"directory": "libs/contracts"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
]
|
|
39
|
+
}
|