@coffre/core 0.4.6 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,420 @@
1
+ //#region src/access-conversion.d.ts
2
+ /**
3
+ * How the vault replaces the grants on every project of 0.4 (`*`, and `*`
4
+ * on one environment slug) with what 0.5 has instead: a person's instance
5
+ * role and its scope, and project grants. Pure, so the vault converts with
6
+ * it and anyone can read what it does to each case.
7
+ *
8
+ * Nobody ends up holding more than before, anywhere, at any time. Where the
9
+ * new model cannot say exactly what a grant said, it says less:
10
+ *
11
+ * developer on * Developer, everywhere
12
+ * developer on dev in each Developer, environments only dev
13
+ * auditor on * Auditor, everywhere
14
+ * viewer on * viewer on each project there is now
15
+ * maintainer on * Developer, everywhere, and maintainer on each project there is now
16
+ * owner on *, as an owner Owner (an owner of 0.4 is an Admin)
17
+ * any, for a service the same role on each project, or each environment of the slug, there is now
18
+ *
19
+ * What a grant leaves to project grants reaches the projects there are
20
+ * now, not those made later (`narrowed`, `later-projects`). A place holds
21
+ * one grant: where they hold one already that neither it nor the new one
22
+ * holds all of, for as long, a project's goes on to each of its
23
+ * environments instead, the one that does not read the log first
24
+ * (`environments`: not environments made later, nor, for an auditor, the
25
+ * project's own entries). Only where neither can, or on an environment, is
26
+ * something lost, the one that reads kept (`lost`):
27
+ *
28
+ * viewer on *, auditor on billing auditor on billing, viewer on each of billing's environments
29
+ * viewer on *, developer on billing developer on billing
30
+ * maintainer on *, access-manager on billing, as a service
31
+ * maintainer on billing: access-manager there is lost
32
+ */
33
+ /** A grant on every project of 0.4: on all of each (`environmentSlug` null), or on the environment of one slug in each. */
34
+ type EveryProjectGrant = {
35
+ environmentSlug: string | null;
36
+ role: Role;
37
+ expiresAt: number | null;
38
+ };
39
+ type ConversionInput = {
40
+ /** A person may hold an instance role; a service account holds grants only. */
41
+ person: boolean;
42
+ /** Their instance role before: `admin` for an owner of 0.4, `member` for anyone else. */
43
+ role: InstanceRole;
44
+ /** Their live grants on every project. */
45
+ everyProject: readonly EveryProjectGrant[];
46
+ /** Their live grants on projects and environments. */
47
+ grants: readonly (GrantPlace & {
48
+ role: Role;
49
+ expiresAt: number | null;
50
+ })[];
51
+ /** The projects there are now, deleted ones left out, with their environments. */
52
+ projects: readonly {
53
+ id: string;
54
+ environments: readonly {
55
+ id: string;
56
+ slug: string;
57
+ }[];
58
+ }[];
59
+ };
60
+ /** A project grant the conversion gives: new, or in place of the one they held there (`replaces`). */
61
+ type ConvertedGrant = {
62
+ projectId: string;
63
+ environmentId: string | null;
64
+ role: Role;
65
+ expiresAt: number | null;
66
+ replaces: Role | null;
67
+ };
68
+ /** Where the conversion gives less than a grant on every project did, each naming the grant it comes of. */
69
+ type Narrowed =
70
+ /** It reaches the projects there are now as project grants, and none made later. */
71
+ {
72
+ kind: 'later-projects';
73
+ grant: EveryProjectGrant;
74
+ } |
75
+ /**
76
+ * `role` on this project, the grant's or the one they held there, is on
77
+ * each of its environments instead: not on environments made later, nor
78
+ * on the project's own log entries.
79
+ */
80
+ {
81
+ kind: 'environments';
82
+ grant: EveryProjectGrant;
83
+ projectId: string;
84
+ role: Role;
85
+ } |
86
+ /** No grant here holds both: `kept` stays (until `keptUntil`), and what `lost` held beyond it is gone. */
87
+ {
88
+ kind: 'lost';
89
+ grant: EveryProjectGrant;
90
+ projectId: string;
91
+ environmentId: string | null;
92
+ lost: Role;
93
+ kept: Role;
94
+ keptUntil: number | null;
95
+ };
96
+ type Conversion = {
97
+ role: InstanceRole;
98
+ scope: Scope;
99
+ grants: ConvertedGrant[];
100
+ narrowed: Narrowed[];
101
+ };
102
+ declare function convertEveryProjectGrants(input: ConversionInput): Conversion;
103
+ //#endregion
104
+ //#region src/access.d.ts
105
+ /**
106
+ * Who may do what: seven permissions, the six project roles a grant names,
107
+ * and the five instance roles a person holds across every project.
108
+ *
109
+ * Deliberately constants and not tables or a policy language. A grant names
110
+ * one role for one member on one place, a person's instance role names one
111
+ * role and its scope, and a check is a lookup in this file, so authorisation
112
+ * stays something you can read end to end.
113
+ */
114
+ declare const PERMISSIONS: readonly ["secret.read", "secret.write", "secret.archive", "audit.read", "environment.manage", "grant.manage", "project.manage"];
115
+ type Permission = (typeof PERMISSIONS)[number];
116
+ /**
117
+ * Permissions that only make sense on a project. Granting
118
+ * `environment.manage` on one environment would be incoherent: what it
119
+ * authorises is creating that environment's siblings.
120
+ */
121
+ declare const PROJECT_ONLY_PERMISSIONS: readonly Permission[];
122
+ declare const ROLES: {
123
+ readonly viewer: {
124
+ readonly name: "Viewer";
125
+ readonly description: "Read secret values.";
126
+ readonly permissions: readonly ["secret.read"];
127
+ };
128
+ readonly developer: {
129
+ readonly name: "Developer";
130
+ readonly description: "Read and write secrets.";
131
+ readonly permissions: readonly ["secret.read", "secret.write"];
132
+ };
133
+ readonly maintainer: {
134
+ readonly name: "Maintainer";
135
+ readonly description: "Read, write and retire secrets, and manage environments.";
136
+ readonly permissions: readonly ["secret.read", "secret.write", "secret.archive", "environment.manage"];
137
+ };
138
+ readonly 'access-manager': {
139
+ readonly name: "Access manager";
140
+ readonly description: "Manage who has access. Cannot read secret values.";
141
+ readonly permissions: readonly ["grant.manage"];
142
+ };
143
+ readonly auditor: {
144
+ readonly name: "Auditor";
145
+ readonly description: "Read the audit log. Cannot read secret values.";
146
+ readonly permissions: readonly ["audit.read"];
147
+ };
148
+ readonly owner: {
149
+ readonly name: "Owner";
150
+ readonly description: "Everything, including reading secret values.";
151
+ readonly permissions: readonly ["secret.read", "secret.write", "secret.archive", "audit.read", "environment.manage", "grant.manage", "project.manage"];
152
+ };
153
+ };
154
+ type Role = keyof typeof ROLES;
155
+ declare const ROLE_NAMES: Role[];
156
+ declare function isRole(value: string): value is Role;
157
+ declare function roleGrants(role: Role, permission: Permission): boolean;
158
+ /** Roles that hold a project-only permission can only be granted on a project. */
159
+ declare function assignableToEnvironment(role: Role): boolean;
160
+ /**
161
+ * A person's role on the whole instance: what they hold in every project
162
+ * their scope covers, before any grant. Service accounts are always
163
+ * members, and hold only what their grants give. Admins and owners also
164
+ * run the instance (`runsInstance`) when nothing narrows their scope.
165
+ */
166
+ declare const INSTANCE_ROLES: {
167
+ readonly member: {
168
+ readonly name: "Member";
169
+ readonly description: "Only what projects grant.";
170
+ readonly permissions: readonly [];
171
+ };
172
+ readonly auditor: {
173
+ readonly name: "Auditor";
174
+ readonly description: "Read the audit log. No secret values.";
175
+ readonly permissions: readonly ["audit.read"];
176
+ };
177
+ readonly developer: {
178
+ readonly name: "Developer";
179
+ readonly description: "Read and write secrets.";
180
+ readonly permissions: readonly ["secret.read", "secret.write"];
181
+ };
182
+ readonly admin: {
183
+ readonly name: "Admin";
184
+ readonly description: "Manage people, service accounts, projects and access. No secret values.";
185
+ readonly permissions: readonly ["audit.read", "environment.manage", "grant.manage", "project.manage"];
186
+ };
187
+ readonly owner: {
188
+ readonly name: "Owner";
189
+ readonly description: "Everything an admin does, and read every secret.";
190
+ readonly permissions: readonly ["secret.read", "secret.write", "secret.archive", "audit.read", "environment.manage", "grant.manage", "project.manage"];
191
+ };
192
+ };
193
+ type InstanceRole = keyof typeof INSTANCE_ROLES;
194
+ declare const INSTANCE_ROLE_NAMES: InstanceRole[];
195
+ declare function isInstanceRole(value: string): value is InstanceRole;
196
+ declare function instanceRoleGrants(role: InstanceRole, permission: Permission): boolean;
197
+ /** The roles that manage people and access: Admin, and Owner, which adds every secret. */
198
+ declare function administers(role: InstanceRole): boolean;
199
+ /**
200
+ * Which projects, or environments, a scope takes in: all of them, only
201
+ * those listed, or all but those listed. Projects are listed by id, so a
202
+ * rename keeps them; environments by slug, so `dev` is the `dev` of every
203
+ * project, the ones made later too.
204
+ */
205
+ type Filter = 'all' | {
206
+ only: string[];
207
+ } | {
208
+ except: string[];
209
+ };
210
+ /**
211
+ * Where a person's instance role applies: the projects it reaches, and the
212
+ * environments in them, by slug. A grant reaches past it; nothing else does.
213
+ *
214
+ * { projects: 'all', environments: { only: ['dev'] } } every project's dev
215
+ * { projects: { except: [billing] }, environments: 'all' } all but billing
216
+ */
217
+ type Scope = {
218
+ projects: Filter;
219
+ environments: Filter;
220
+ };
221
+ /** Every project and every environment. */
222
+ declare const EVERYWHERE: Scope;
223
+ /** Whether a scope narrows nothing. */
224
+ declare function unscoped(scope: Scope): boolean;
225
+ /** Whether a filter takes in a project's id or an environment's slug; one not read (null) is in none that lists. */
226
+ declare function admits(filter: Filter, value: string | null | undefined): boolean;
227
+ /**
228
+ * One way to write each scope, which the vault seals and the log replays:
229
+ * lists sorted, without repeats; an empty `except` is `all`. An empty
230
+ * `only` stays: it takes in nothing.
231
+ */
232
+ declare function normalScope(scope: Scope): Scope;
233
+ /** Whether `value` is a scope as `normalScope` writes it, and as the vault stores it. */
234
+ declare function isScope(value: unknown): value is Scope;
235
+ /**
236
+ * A scope in words, its projects named as the caller has them (slugs, at
237
+ * the API's edges): `All projects`, `All projects · dev only`,
238
+ * `web, api`, `All projects but billing · all but prod`.
239
+ */
240
+ declare function scopeInWords(scope: Scope): string;
241
+ /**
242
+ * A place a permission applies to: a project, or one of its environments,
243
+ * named by its slug too, which a scope matches environments by. A slug not
244
+ * read (null) is in no scope that names environments.
245
+ */
246
+ type Place = {
247
+ projectId: string;
248
+ environmentId?: null;
249
+ } | {
250
+ projectId: string;
251
+ environmentId: string;
252
+ environmentSlug: string | null;
253
+ };
254
+ /**
255
+ * Whether a scope takes in a place. A project as a whole is in it only when
256
+ * the scope keeps all of its environments: a role scoped to `dev` reaches
257
+ * every `dev`, never a project around one.
258
+ */
259
+ declare function inScope(scope: Scope, place: Place): boolean;
260
+ /**
261
+ * Where a grant applies: a project (`environmentId` null), or one of its
262
+ * environments, which names its project too.
263
+ */
264
+ type GrantPlace = {
265
+ projectId: string | null;
266
+ environmentId: string | null;
267
+ };
268
+ /**
269
+ * What a grant's fields say it is on, or null when they say nothing
270
+ * coherent: an environment without its project, as a grant whose
271
+ * environment row is gone reads, or neither, as the grants on every project
272
+ * of 0.4 read until the vault replaces them. Null covers nothing.
273
+ */
274
+ declare function grantKind(grant: GrantPlace): 'project' | 'environment' | null;
275
+ /**
276
+ * What a service account's token or trust binding carries, its scope:
277
+ * grants as the account's are, a role on a project or an environment, each
278
+ * one the account held and its issuer could give when it was issued.
279
+ * Nothing changes it after. A request made with it holds what both it and
280
+ * the account hold now (`allows`), so narrowing the account narrows it, and
281
+ * a grant the account gets later reaches no credential issued before:
282
+ *
283
+ * account developer on market/dev, then viewer on market/prod too
284
+ * token developer on market/dev, issued before market/prod
285
+ * request reads and writes market/dev, never market/prod
286
+ */
287
+ type CredentialScope = readonly (GrantPlace & {
288
+ role: Role;
289
+ })[];
290
+ /** The most places a scope asked for names: far more than any credential needs, and a bound on what a request makes the app check. */
291
+ declare const MAX_SCOPE_PLACES = 1000;
292
+ /**
293
+ * A credential's scope, by path as the API names it, in words, each role
294
+ * with its places: `developer on market/dev, api/dev; viewer on market/prod`.
295
+ */
296
+ declare function credentialScopeInWords(scope: Readonly<Record<string, string>>): string;
297
+ /** Whether `value` is a credential's scope as the app stores it and the vault reads it: one grant a place, each role one that place takes. */
298
+ declare function isCredentialScope(value: unknown): value is CredentialScope;
299
+ /**
300
+ * What someone holds: everything a permission check reads. The app builds
301
+ * it from what the vault says once per request; the vault builds it from
302
+ * its own store. Both then ask the same functions below, so the rules
303
+ * cannot drift apart.
304
+ */
305
+ type Holdings = {
306
+ /** Named in the vault's configuration: everything, everywhere. */
307
+ isRootAdmin: boolean;
308
+ /** `member` for service accounts, and for anyone not active. */
309
+ role: InstanceRole;
310
+ scope: Scope;
311
+ /** Live grants only. */
312
+ grants: readonly (GrantPlace & {
313
+ role: Role;
314
+ })[];
315
+ /** The scope of the credential a request came in on, past which nothing reaches; absent or null for a session's. */
316
+ credentialScope?: CredentialScope | null;
317
+ };
318
+ /** Someone who holds nothing: a stranger, a removed member, a tampered row. */
319
+ declare const NOTHING: Holdings;
320
+ /**
321
+ * Whether a grant at `grant` reaches `place`:
322
+ *
323
+ * market market and each of its environments
324
+ * market/dev market/dev
325
+ *
326
+ * An environment's grant never reaches up to its project.
327
+ */
328
+ declare function covers(grant: GrantPlace, place: Place): boolean;
329
+ /**
330
+ * Whether `holder` may do `permission` at `place`: their instance role
331
+ * gives its permissions inside its scope, a grant that covers the place
332
+ * gives its role's there, and they add up. A Developer scoped to `dev` with
333
+ * `viewer` on `billing` reads and writes every `dev`, and reads all of
334
+ * `billing`.
335
+ */
336
+ declare function allows(holder: Holdings, permission: Permission, place: Place): boolean;
337
+ /**
338
+ * Whether the actor may give or take grants at this place: with
339
+ * `grant.manage` there, from a project grant or an instance role whose
340
+ * scope takes it in. A grant on a project reaches all its environments, so
341
+ * an admin scoped to `dev` manages `market/dev`'s grants, not `market`'s.
342
+ */
343
+ declare function mayManageAccess(actor: Holdings, place: Place): boolean;
344
+ /**
345
+ * Whether the holder runs the instance: a root admin, or an Admin or Owner
346
+ * whose scope narrows nothing. They add and remove people, set instance
347
+ * roles, and change the instance's settings; they read its whole log too
348
+ * (`readsWholeLog`). A scoped Admin manages only inside its scope, so it
349
+ * does none of these.
350
+ */
351
+ declare function runsInstance(holder: Holdings): boolean;
352
+ /**
353
+ * Who may set up service accounts themselves, the instance's setting: a
354
+ * scope as a role's is, projects by id and environments by slug, which
355
+ * only those who run the instance change. Inside it, any person gives a
356
+ * service account what they hold, never more (`givesService`). Everywhere,
357
+ * unless it says otherwise:
358
+ *
359
+ * { projects: 'all', environments: { except: ['prod'] } } CI for dev and staging; prod's takes an Admin
360
+ */
361
+ type ServiceSetting = Scope;
362
+ /**
363
+ * Whether `holder` may give a service account `role` at `place`: where
364
+ * they manage access, as anyone may; or inside the setting, where they hold
365
+ * everything `role` does. A Developer on `market/dev` gives viewer or
366
+ * developer there, never maintainer; a grant on `market` takes holding it
367
+ * on all of `market`. `setting` is null for a service account, which sets
368
+ * up none.
369
+ */
370
+ declare function givesService(holder: Holdings, setting: ServiceSetting | null, role: Role, place: Place): boolean;
371
+ /** Whether `holder` gives a service account anything anywhere among `places`: whether they set any up. */
372
+ declare function setsUpServices(holder: Holdings, setting: ServiceSetting | null, places: readonly Place[]): boolean;
373
+ /** A service account as `managesService` weighs it: its live grants, where they are, and who admitted it. */
374
+ type ServiceAccount = {
375
+ grants: readonly {
376
+ role: Role;
377
+ place: Place;
378
+ }[];
379
+ /** The member who admitted it last (`user:…`), as its row says. */
380
+ admittedBy: string | null;
381
+ };
382
+ /**
383
+ * Whether `holder`, the member `actor`, manages a service account: issues
384
+ * and revokes its tokens, adds and removes its trust bindings, changes its
385
+ * grants and removes it. A credential for it reads everything it holds, so
386
+ * they must reach every grant it holds, as if giving it (`givesService`):
387
+ * a Developer on `dev` manages an account on `market/dev`, and not one that
388
+ * holds `market/prod` too. One that holds nothing, only the person who
389
+ * admitted it: anyone else's token for it would read whatever it is given
390
+ * next. Those who run the instance manage every one.
391
+ */
392
+ declare function managesService(holder: Holdings, setting: ServiceSetting | null, actor: string, account: ServiceAccount): boolean;
393
+ /**
394
+ * Whether `holder`, the member `actor`, may issue a token or a trust
395
+ * binding of `account` whose scope is `scope`: they manage the account
396
+ * (`managesService`), and each grant of the scope is one they could give it
397
+ * (`givesService`) and one it holds, every permission of its role there.
398
+ * For an account that holds `developer` on `market/dev`, a Developer of
399
+ * `dev` issues `developer` or `viewer` there; nobody issues `market/prod`
400
+ * for it, or `maintainer` on `market/dev`, until the account holds them.
401
+ */
402
+ declare function issuesCredential(holder: Holdings, setting: ServiceSetting | null, actor: string, account: ServiceAccount, scope: ServiceAccount['grants']): boolean;
403
+ /**
404
+ * Whether the holder reads the whole log, the instance's own entries
405
+ * (sign-ins, people, service accounts, settings) with every project's, and
406
+ * verifies it: whoever holds `audit.read` by an instance role whose scope
407
+ * narrows nothing, an Auditor's included, and a root admin. Reading, not
408
+ * running: an Auditor of the whole instance changes nothing it reads about.
409
+ * Anyone else reads the places they hold `audit.read` in.
410
+ */
411
+ declare function readsWholeLog(holder: Holdings): boolean;
412
+ /**
413
+ * Whether the holder may make a project: an Admin or Owner whose scope
414
+ * takes the new one in as a whole. A scope listing only some projects does
415
+ * not list the new one; one that keeps only some environments does not
416
+ * hold the project around them.
417
+ */
418
+ declare function makesProjects(holder: Holdings): boolean;
419
+ //#endregion
420
+ export { instanceRoleGrants as A, readsWholeLog as B, allows as C, givesService as D, credentialScopeInWords as E, issuesCredential as F, unscoped as G, runsInstance as H, makesProjects as I, ConvertedGrant as J, Conversion as K, managesService as L, isInstanceRole as M, isRole as N, grantKind as O, isScope as P, mayManageAccess as R, admits as S, covers as T, scopeInWords as U, roleGrants as V, setsUpServices as W, Narrowed as X, EveryProjectGrant as Y, convertEveryProjectGrants as Z, Role as _, Holdings as a, ServiceSetting as b, InstanceRole as c, PERMISSIONS as d, PROJECT_ONLY_PERMISSIONS as f, ROLE_NAMES as g, ROLES as h, GrantPlace as i, isCredentialScope as j, inScope as k, MAX_SCOPE_PLACES as l, Place as m, EVERYWHERE as n, INSTANCE_ROLES as o, Permission as p, ConversionInput as q, Filter as r, INSTANCE_ROLE_NAMES as s, CredentialScope as t, NOTHING as u, Scope as v, assignableToEnvironment as w, administers as x, ServiceAccount as y, normalScope as z };