@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.
package/dist/access.js CHANGED
@@ -1,135 +1,2 @@
1
- //#region src/access.ts
2
- /**
3
- * Who may do what: seven permissions and the six roles that bundle them.
4
- *
5
- * Deliberately constants and not tables or a policy language. A grant names
6
- * one role for one member on one place, and a check is a lookup in this file,
7
- * so authorisation stays something you can read end to end.
8
- */
9
- const PERMISSIONS = [
10
- "secret.read",
11
- "secret.write",
12
- "secret.archive",
13
- "audit.read",
14
- "environment.manage",
15
- "grant.manage",
16
- "project.manage"
17
- ];
18
- /**
19
- * Permissions that only make sense on a project. Granting
20
- * `environment.manage` on one environment would be incoherent: what it
21
- * authorises is creating that environment's siblings.
22
- */
23
- const PROJECT_ONLY_PERMISSIONS = [
24
- "environment.manage",
25
- "grant.manage",
26
- "project.manage"
27
- ];
28
- const ROLES = {
29
- viewer: {
30
- name: "Viewer",
31
- description: "Read secret values.",
32
- permissions: ["secret.read"]
33
- },
34
- developer: {
35
- name: "Developer",
36
- description: "Read and write secrets.",
37
- permissions: ["secret.read", "secret.write"]
38
- },
39
- maintainer: {
40
- name: "Maintainer",
41
- description: "Read, write and retire secrets, and manage environments.",
42
- permissions: [
43
- "secret.read",
44
- "secret.write",
45
- "secret.archive",
46
- "environment.manage"
47
- ]
48
- },
49
- "access-manager": {
50
- name: "Access manager",
51
- description: "Manage who has access. Cannot read secret values.",
52
- permissions: ["grant.manage"]
53
- },
54
- auditor: {
55
- name: "Auditor",
56
- description: "Read the audit log. Cannot read secret values.",
57
- permissions: ["audit.read"]
58
- },
59
- owner: {
60
- name: "Owner",
61
- description: "Everything, including reading secret values.",
62
- permissions: [...PERMISSIONS]
63
- }
64
- };
65
- const ROLE_NAMES = Object.keys(ROLES);
66
- function isRole(value) {
67
- return Object.hasOwn(ROLES, value);
68
- }
69
- function roleGrants(role, permission) {
70
- return ROLES[role].permissions.includes(permission);
71
- }
72
- /** Roles that hold a project-only permission can only be granted on a project. */
73
- function assignableToEnvironment(role) {
74
- return !ROLES[role].permissions.some((permission) => PROJECT_ONLY_PERMISSIONS.includes(permission));
75
- }
76
- /**
77
- * What a grant's fields say it is on, or null when they say nothing
78
- * coherent: an environment without its project, as a grant whose
79
- * environment row is gone reads, or a slug beside an id. A grant is on
80
- * every project only when it names no place at all; anything else that
81
- * lacks a field covers nothing.
82
- */
83
- function grantKind(grant) {
84
- if (grant.environmentId !== null) return grant.projectId !== null && grant.environmentSlug === null ? "environment" : null;
85
- if (grant.projectId !== null) return grant.environmentSlug === null ? "project" : null;
86
- return "every-project";
87
- }
88
- /** Every project, as a path names it. */
89
- const EVERY_PROJECT = "*";
90
- /** A grant on every project as a path: `*`, or `*` and the environment slug it covers in each. */
91
- function everyProjectPath(environmentSlug) {
92
- return environmentSlug === null ? "*" : `*/${environmentSlug}`;
93
- }
94
- /**
95
- * Whether a grant at `grant` reaches `place`:
96
- *
97
- * market market and each of its environments
98
- * market/dev market/dev
99
- * * every project and every environment
100
- * *, on dev each environment whose slug is dev, in every project
101
- *
102
- * An environment's grant never reaches up to its project, whether it names
103
- * the environment or its slug.
104
- */
105
- function covers(grant, place) {
106
- switch (grantKind(grant)) {
107
- case "environment": return grant.projectId === place.projectId && grant.environmentId === (place.environmentId ?? null);
108
- case "project": return grant.projectId === place.projectId;
109
- case "every-project": return grant.environmentSlug === null || place.environmentId != null && place.environmentSlug !== null && place.environmentSlug === grant.environmentSlug;
110
- case null: return false;
111
- }
112
- }
113
- /**
114
- * Whether `holder` may do `permission` at `place`: a grant that covers it
115
- * gives the role's permissions there, and grants add up. Owners also manage
116
- * every project. `developer` on `market/prod` does not let anyone rename
117
- * `market`.
118
- */
119
- function allows(holder, permission, place) {
120
- if (holder.isRootAdmin) return true;
121
- if (place.environmentId == null && holder.isOwner && PROJECT_ONLY_PERMISSIONS.includes(permission)) return true;
122
- return holder.grants.some((grant) => covers(grant, place) && roleGrants(grant.role, permission));
123
- }
124
- /**
125
- * Whether the actor may manage grants at this place: on a project or its
126
- * environments, with `grant.manage` on the project; on every project, as an
127
- * instance owner or a root admin only.
128
- */
129
- function mayManageAccess(actor, place) {
130
- const kind = grantKind(place);
131
- if (kind === "every-project") return actor.isOwner || actor.isRootAdmin;
132
- return kind !== null && place.projectId !== null && allows(actor, "grant.manage", { projectId: place.projectId });
133
- }
134
- //#endregion
135
- export { EVERY_PROJECT, PERMISSIONS, PROJECT_ONLY_PERMISSIONS, ROLES, ROLE_NAMES, allows, assignableToEnvironment, covers, everyProjectPath, grantKind, isRole, mayManageAccess, roleGrants };
1
+ import { A as roleGrants, C as isScope, D as mayManageAccess, E as managesService, F as convertEveryProjectGrants, M as scopeInWords, N as setsUpServices, O as normalScope, P as unscoped, S as isRole, T as makesProjects, _ as grantKind, a as NOTHING, b as isCredentialScope, c as ROLES, d as admits, f as allows, g as givesService, h as credentialScopeInWords, i as MAX_SCOPE_PLACES, j as runsInstance, k as readsWholeLog, l as ROLE_NAMES, m as covers, n as INSTANCE_ROLES, o as PERMISSIONS, p as assignableToEnvironment, r as INSTANCE_ROLE_NAMES, s as PROJECT_ONLY_PERMISSIONS, t as EVERYWHERE, u as administers, v as inScope, w as issuesCredential, x as isInstanceRole, y as instanceRoleGrants } from "./access-pnJ-u4x2.js";
2
+ export { EVERYWHERE, INSTANCE_ROLES, INSTANCE_ROLE_NAMES, MAX_SCOPE_PLACES, NOTHING, PERMISSIONS, PROJECT_ONLY_PERMISSIONS, ROLES, ROLE_NAMES, administers, admits, allows, assignableToEnvironment, convertEveryProjectGrants, covers, credentialScopeInWords, givesService, grantKind, inScope, instanceRoleGrants, isCredentialScope, isInstanceRole, isRole, isScope, issuesCredential, makesProjects, managesService, mayManageAccess, normalScope, readsWholeLog, roleGrants, runsInstance, scopeInWords, setsUpServices, unscoped };
@@ -1,4 +1,4 @@
1
- import { BindingClaims, BindingInvalid, BindingPolicy, GITHUB_EVENTS, GITHUB_ISSUER, GITLAB_ISSUER, GITLAB_PIPELINE_SOURCES, MAX_BINDINGS, MAX_CLAIMS, MAX_CLAIM_BYTES, REQUIRED_CLAIMS, WORKLOAD_PROFILES, WorkloadProfile, canonicalClaims, checkBinding, checkFetchUrl, defaultIssuer, fullRef, githubReusable, githubWorkflow, gitlabProject, workflowRef } from "./workloads.js";
1
+ import { BindingClaims, BindingInvalid, BindingPolicy, ClaimValue, EVENT_EXPOSURE, GITHUB_EVENTS, GITHUB_ISSUER, GITLAB_ISSUER, GITLAB_PIPELINE_SOURCES, MAX_BINDINGS, MAX_CLAIMS, MAX_CLAIM_BYTES, REQUIRED_CLAIMS, Starts, WORKLOAD_PROFILES, WorkloadProfile, canonicalClaims, checkBinding, checkFetchUrl, claimValues, defaultIssuer, differingClaims, fullRef, githubEventsAt, githubReusable, githubWorkflow, gitlabProject, workflowRef } from "./workloads.js";
2
2
  import { JSONWebKeySet, JWTPayload } from "jose";
3
3
  //#region src/identity/signin/types.d.ts
4
4
  /**
@@ -587,4 +587,4 @@ export declare function verifyWorkloadToken(token: string, keys: JSONWebKeySet,
587
587
  */
588
588
  export declare function issuedRefusal(iat: number, now: number): WorkloadTokenRefused | null;
589
589
  //#endregion
590
- export { BindingClaims, BindingInvalid, BindingPolicy, GITHUB_EVENTS, GITHUB_ISSUER, GITLAB_ISSUER, GITLAB_PIPELINE_SOURCES, type JSONWebKeySet, MAX_BINDINGS, MAX_CLAIMS, MAX_CLAIM_BYTES, REQUIRED_CLAIMS, WORKLOAD_PROFILES, WorkloadProfile, canonicalClaims, checkBinding, checkFetchUrl, defaultIssuer, fullRef, githubReusable, githubWorkflow, gitlabProject, workflowRef };
590
+ export { BindingClaims, BindingInvalid, BindingPolicy, ClaimValue, EVENT_EXPOSURE, GITHUB_EVENTS, GITHUB_ISSUER, GITLAB_ISSUER, GITLAB_PIPELINE_SOURCES, type JSONWebKeySet, MAX_BINDINGS, MAX_CLAIMS, MAX_CLAIM_BYTES, REQUIRED_CLAIMS, Starts, WORKLOAD_PROFILES, WorkloadProfile, canonicalClaims, checkBinding, checkFetchUrl, claimValues, defaultIssuer, differingClaims, fullRef, githubEventsAt, githubReusable, githubWorkflow, gitlabProject, workflowRef };
package/dist/identity.js CHANGED
@@ -1,4 +1,4 @@
1
- import { BindingInvalid, GITHUB_EVENTS, GITHUB_ISSUER, GITLAB_ISSUER, GITLAB_PIPELINE_SOURCES, MAX_BINDINGS, MAX_CLAIMS, MAX_CLAIM_BYTES, REQUIRED_CLAIMS, WORKLOAD_PROFILES, canonicalClaims, checkBinding, checkFetchUrl, defaultIssuer, fullRef, githubReusable, githubWorkflow, gitlabProject, workflowRef } from "./workloads.js";
1
+ import { BindingInvalid, EVENT_EXPOSURE, GITHUB_EVENTS, GITHUB_ISSUER, GITLAB_ISSUER, GITLAB_PIPELINE_SOURCES, MAX_BINDINGS, MAX_CLAIMS, MAX_CLAIM_BYTES, REQUIRED_CLAIMS, WORKLOAD_PROFILES, canonicalClaims, checkBinding, checkFetchUrl, claimValues, defaultIssuer, differingClaims, fullRef, githubEventsAt, githubReusable, githubWorkflow, gitlabProject, workflowRef } from "./workloads.js";
2
2
  import { createCipheriv, createDecipheriv, createHash, hkdfSync, randomBytes } from "node:crypto";
3
3
  import * as oauth from "oauth4webapi";
4
4
  import { createLocalJWKSet, createRemoteJWKSet, errors, jwtVerify } from "jose";
@@ -830,4 +830,4 @@ function refusalOf(error) {
830
830
  return new WorkloadTokenRefused("signature", "the token does not verify");
831
831
  }
832
832
  //#endregion
833
- export { ACCESS_JWT_HEADER, AccessIdentityVerifier, BindingInvalid, CLOCK_TOLERANCE_SECONDS, GITHUB_EVENTS, GITHUB_ISSUER, GITLAB_ISSUER, GITLAB_PIPELINE_SOURCES, GitHubSigninProvider, MAX_BINDINGS, MAX_CLAIMS, MAX_CLAIM_BYTES, MAX_TOKEN_AGE_SECONDS, MAX_TOKEN_BYTES, OidcSigninProvider, REQUIRED_CLAIMS, SigninError, WORKLOAD_ALGORITHMS, WORKLOAD_PROFILES, WorkloadTokenRefused, canonicalClaims, checkBinding, checkFetchUrl, cloudflareAccess, decodeWorkloadToken, defaultIssuer, defineSignin, deriveKey, fullRef, generateToken, github, githubReusable, githubWorkflow, gitlabProject, google, hashToken, isCoffreToken, issuedRefusal, microsoft, oidc, publicOrigin, seal, signin, toPrincipal, tokenHint, tokenKind, unseal, verifyWorkloadToken, workflowRef };
833
+ export { ACCESS_JWT_HEADER, AccessIdentityVerifier, BindingInvalid, CLOCK_TOLERANCE_SECONDS, EVENT_EXPOSURE, GITHUB_EVENTS, GITHUB_ISSUER, GITLAB_ISSUER, GITLAB_PIPELINE_SOURCES, GitHubSigninProvider, MAX_BINDINGS, MAX_CLAIMS, MAX_CLAIM_BYTES, MAX_TOKEN_AGE_SECONDS, MAX_TOKEN_BYTES, OidcSigninProvider, REQUIRED_CLAIMS, SigninError, WORKLOAD_ALGORITHMS, WORKLOAD_PROFILES, WorkloadTokenRefused, canonicalClaims, checkBinding, checkFetchUrl, claimValues, cloudflareAccess, decodeWorkloadToken, defaultIssuer, defineSignin, deriveKey, differingClaims, fullRef, generateToken, github, githubEventsAt, githubReusable, githubWorkflow, gitlabProject, google, hashToken, isCoffreToken, issuedRefusal, microsoft, oidc, publicOrigin, seal, signin, toPrincipal, tokenHint, tokenKind, unseal, verifyWorkloadToken, workflowRef };
package/dist/schemas.d.ts CHANGED
@@ -29,9 +29,31 @@ export declare const principalType: z.ZodEnum<{
29
29
  }>;
30
30
  export declare const principalId: z.ZodString;
31
31
  export declare const instanceRole: z.ZodEnum<{
32
+ member: "member";
33
+ auditor: "auditor";
34
+ developer: "developer";
35
+ admin: "admin";
32
36
  owner: "owner";
33
- user: "user";
34
37
  }>;
38
+ /** Which projects or environments a scope takes in, by slug: `all`, `{ only: [...] }` or `{ except: [...] }`. */
39
+ export declare const scopeFilter: z.ZodUnion<readonly [z.ZodLiteral<"all">, z.ZodObject<{
40
+ only: z.ZodArray<z.ZodString>;
41
+ }, z.core.$strict>, z.ZodObject<{
42
+ except: z.ZodArray<z.ZodString>;
43
+ }, z.core.$strict>]>;
44
+ /** Where an instance role applies, as the API takes it: either filter left out is `all`. */
45
+ export declare const scopeInput: z.ZodObject<{
46
+ projects: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"all">, z.ZodObject<{
47
+ only: z.ZodArray<z.ZodString>;
48
+ }, z.core.$strict>, z.ZodObject<{
49
+ except: z.ZodArray<z.ZodString>;
50
+ }, z.core.$strict>]>>;
51
+ environments: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<"all">, z.ZodObject<{
52
+ only: z.ZodArray<z.ZodString>;
53
+ }, z.core.$strict>, z.ZodObject<{
54
+ except: z.ZodArray<z.ZodString>;
55
+ }, z.core.$strict>]>>;
56
+ }, z.core.$strict>;
35
57
  export declare const grantId: z.ZodString;
36
58
  export declare const emailAddress: z.ZodString;
37
59
  //#endregion
package/dist/schemas.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { r as INSTANCE_ROLE_NAMES } from "./access-pnJ-u4x2.js";
1
2
  import { PROJECT_PAGES } from "./pages.js";
2
3
  import { z } from "zod";
3
4
  //#region src/schemas.ts
@@ -33,8 +34,19 @@ function shownMember(member) {
33
34
  }
34
35
  const principalType = z.enum(["user", "service"]);
35
36
  const principalId = z.string().trim().min(1).max(320);
36
- const instanceRole = z.enum(["user", "owner"]);
37
+ const instanceRole = z.enum(INSTANCE_ROLE_NAMES);
38
+ /** Which projects or environments a scope takes in, by slug: `all`, `{ only: [...] }` or `{ except: [...] }`. */
39
+ const scopeFilter = z.union([
40
+ z.literal("all"),
41
+ z.object({ only: z.array(slug).max(200) }).strict(),
42
+ z.object({ except: z.array(slug).min(1).max(200) }).strict()
43
+ ]);
44
+ /** Where an instance role applies, as the API takes it: either filter left out is `all`. */
45
+ const scopeInput = z.object({
46
+ projects: scopeFilter.optional(),
47
+ environments: scopeFilter.optional()
48
+ }).strict();
37
49
  const grantId = z.string().uuid();
38
50
  const emailAddress = z.string().email().max(320);
39
51
  //#endregion
40
- export { TOMBSTONE, displayName, emailAddress, environmentSlug, folderName, grantId, instanceRole, isTombstone, principalId, principalType, secretKey, shownMember, slug, tombstoneOf };
52
+ export { TOMBSTONE, displayName, emailAddress, environmentSlug, folderName, grantId, instanceRole, isTombstone, principalId, principalType, scopeFilter, scopeInput, secretKey, shownMember, slug, tombstoneOf };
package/dist/vault.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { GrantPlace, Role } from "./access.js";
1
+ import { _ as Role, b as ServiceSetting, c as InstanceRole, i as GrantPlace, t as CredentialScope, v as Scope } from "./access-CwsFLfTz.js";
2
2
  //#region src/access-fault.d.ts
3
3
  /**
4
4
  * Why the vault's members and grants do not follow from its log, as facts
@@ -48,14 +48,16 @@ type AccessFault =
48
48
  principal: string;
49
49
  why: 'mac' | 'stale';
50
50
  };
51
+ /** A grant a fault names; `environmentSlug` only on a grant on every project of 0.4, which names no project. */
51
52
  type FaultGrant = GrantPlace & {
53
+ environmentSlug?: string | null;
52
54
  principal: string;
53
55
  role: string;
54
56
  };
55
57
  /** How to name a principal (`user:ada@…`) and a place; ids by default. */
56
58
  type FaultNames = {
57
59
  principal?: (principal: string) => string;
58
- /** Asked only of a project's or an environment's place: a grant on every project is named by its path. */
60
+ /** Asked only of a project's or an environment's place: a grant on every project of 0.4 is named by its path. */
59
61
  place?: (projectId: string, environmentId: string | null) => string;
60
62
  };
61
63
  /** The fault in a sentence, with whatever names the caller can give. */
@@ -146,13 +148,14 @@ export interface Vault {
146
148
  changes: AccessChange[];
147
149
  }>>;
148
150
  /**
149
- * Add a member, bring back a removed one, or change whether they are an
150
- * owner. `generation` is theirs now: sessions and tokens of an earlier
151
- * one are dead.
151
+ * Add a member, bring back a removed one, or change a person's instance
152
+ * role and its scope. `generation` is theirs now: sessions and tokens of
153
+ * an earlier one are dead.
152
154
  */
153
155
  admit(input: AdmitInput): Promise<Outcome<{
154
156
  created: boolean;
155
- owner: boolean;
157
+ role: InstanceRole;
158
+ scope: Scope;
156
159
  generation: number;
157
160
  }>>;
158
161
  /** Remove a member: revoke every grant and refuse them until admitted again, from `generation` on. */
@@ -160,6 +163,16 @@ export interface Vault {
160
163
  revoked: Grant[];
161
164
  generation: number;
162
165
  }>>;
166
+ /**
167
+ * The instance's settings, as the vault's newest `settings.change` entry
168
+ * says, by its MAC: an entry is where they live, so each change is logged
169
+ * by being made. Everywhere, until someone narrows it.
170
+ */
171
+ settings(): Promise<Settings>;
172
+ /** Change the instance's settings: those who run it, never anyone else. */
173
+ setSettings(input: SetSettingsInput): Promise<Outcome<{
174
+ settings: Settings;
175
+ }>>;
163
176
  /**
164
177
  * Sign the log up to its last entry, in an `audit.checkpoint` entry of
165
178
  * the vault's, if the chain recomputes, the vault's entries by their MACs,
@@ -198,7 +211,7 @@ export interface Vault {
198
211
  * from it. The links and hashes of the chain are the caller's to
199
212
  * recompute through `upTo`, as the app does first; with none, the vault
200
213
  * recomputes them all itself. A verdict and nothing else, so anyone may
201
- * ask; the app asks for owners.
214
+ * ask; the app asks for those who read the whole log (`readsWholeLog`).
202
215
  */
203
216
  verifyLog(input: VerifyLogInput): Promise<LogVerification>;
204
217
  }
@@ -207,7 +220,7 @@ export interface Vault {
207
220
  * `access_seq` names, and what a finding about them is newer than while it
208
221
  * stands.
209
222
  */
210
- export declare const ACCESS_ACTIONS: readonly ["member.add", "member.restore", "member.owner", "member.remove", "access.grant", "access.revoke"];
223
+ export declare const ACCESS_ACTIONS: readonly ["member.add", "member.restore", "member.owner", "member.role", "member.remove", "access.grant", "access.revoke"];
211
224
  /** A refusal, already logged by the time the app sees it. */
212
225
  export type Refusal = {
213
226
  code: RefusalCode;
@@ -237,7 +250,7 @@ export type RefusalCode =
237
250
  'not_allowed' |
238
251
  /** Root admins come from the vault's configuration and cannot be changed. */
239
252
  'root_admin' |
240
- /** Well-formed, but not something the rules allow: a project role on an environment, an owner token. */
253
+ /** Well-formed, but not something the rules allow: a project role on an environment, a service account with an instance role. */
241
254
  'invalid' |
242
255
  /** The vault's own log no longer carries the head the last checkpoint signed, or does not rehash since. */
243
256
  'log_broken' |
@@ -311,7 +324,16 @@ type Correlation = {
311
324
  */
312
325
  credentialId?: string | null;
313
326
  };
314
- export type UnwrapInput = Correlation & {
327
+ /**
328
+ * The scope of the credential the request came in on, a service token's or
329
+ * a trust binding's: unlike the correlation, the vault decides on it, and
330
+ * its principal holds nothing past it (`allows`). Null, or left out, for a
331
+ * session's, which carries all its member holds.
332
+ */
333
+ export type Through = {
334
+ credentialScope?: CredentialScope | null;
335
+ };
336
+ export type UnwrapInput = Correlation & Through & {
315
337
  principal: string;
316
338
  purpose: Purpose;
317
339
  /**
@@ -336,7 +358,7 @@ export type HolderRef = {
336
358
  secretId: string;
337
359
  path: string;
338
360
  };
339
- export type ReferenceInput = Correlation & {
361
+ export type ReferenceInput = Correlation & Through & {
340
362
  principal: string;
341
363
  /** `id` is the reference's, a fresh UUID; the vault reads the source by its id. */
342
364
  items: {
@@ -347,13 +369,13 @@ export type ReferenceInput = Correlation & {
347
369
  };
348
370
  }[];
349
371
  };
350
- export type EndReferencesInput = Correlation & {
372
+ export type EndReferencesInput = Correlation & Through & {
351
373
  principal: string;
352
374
  /** `abandoned`: sealed for a write that stored nothing, so that no row written later revives it. */
353
375
  reason: 'replaced' | 'broken' | 'abandoned';
354
376
  items: Via[];
355
377
  };
356
- export type WrapInput = Correlation & {
378
+ export type WrapInput = Correlation & Through & {
357
379
  principal: string;
358
380
  /** `key` is a fresh 32-byte data key, base64. */
359
381
  items: {
@@ -361,7 +383,7 @@ export type WrapInput = Correlation & {
361
383
  key: string;
362
384
  }[];
363
385
  };
364
- export type RewrapInput = Correlation & {
386
+ export type RewrapInput = Correlation & Through & {
365
387
  principal: string;
366
388
  /** `secret` is the proposed new version; `secretVersionId` is the stored source. */
367
389
  items: {
@@ -370,9 +392,8 @@ export type RewrapInput = Correlation & {
370
392
  }[];
371
393
  };
372
394
  /**
373
- * A grant: on a project, one of its environments (`projectId` is the
374
- * environment's project), every project, or one environment slug in every
375
- * project (`GrantPlace` in @coffre/core/access).
395
+ * A grant: on a project, or one of its environments (`projectId` is the
396
+ * environment's project; `GrantPlace` in @coffre/core/access).
376
397
  */
377
398
  export type Grant = GrantPlace & {
378
399
  role: Role;
@@ -392,8 +413,13 @@ export type Access = {
392
413
  /** Advanced by removal, even if the app cannot commit its credential revocations. */
393
414
  generation: number;
394
415
  isRootAdmin: boolean;
395
- /** Root admins and users admitted as owners, while active. */
396
- isOwner: boolean;
416
+ /**
417
+ * Their instance role and its scope, while active: `member` everywhere
418
+ * for service accounts and anyone not active, `owner` everywhere for
419
+ * root admins. Scopes name projects by id.
420
+ */
421
+ role: InstanceRole;
422
+ scope: Scope;
397
423
  /** Live grants only, and none unless active. */
398
424
  grants: Grant[];
399
425
  /** When the status last changed, and who changed it; null for root admins and strangers. */
@@ -402,30 +428,42 @@ export type Access = {
402
428
  };
403
429
  /**
404
430
  * One place: a role to hold there, with an optional end, or null for none.
405
- * An environment's place names its project too; `environmentSlug` left out
406
- * is null.
431
+ * An environment's place names its project too.
407
432
  */
408
- export type GrantChange = Omit<GrantPlace, 'environmentSlug'> & {
409
- environmentSlug?: string | null;
433
+ export type GrantChange = GrantPlace & {
410
434
  role: Role | null;
411
435
  expiresAt: string | null;
412
436
  };
413
437
  export type AccessChange = 'created' | 'updated' | 'revoked' | 'unchanged';
414
- export type SetAccessInput = Correlation & {
438
+ export type SetAccessInput = Correlation & Through & {
415
439
  actor: string;
416
440
  principal: string;
417
441
  changes: GrantChange[];
418
442
  };
419
- export type AdmitInput = Correlation & {
443
+ export type AdmitInput = Correlation & Through & {
420
444
  actor: string;
421
445
  principal: string;
422
- /** Left out, an existing member keeps their role and a new one is not an owner. */
423
- owner?: boolean;
446
+ /**
447
+ * A person's instance role, and where it applies (everywhere, left out).
448
+ * Left out, an existing member keeps theirs and a new one is a `member`.
449
+ * Changing it takes someone who runs the instance, about someone else.
450
+ */
451
+ role?: InstanceRole;
452
+ scope?: Scope;
424
453
  };
425
- export type RemoveInput = Correlation & {
454
+ export type RemoveInput = Correlation & Through & {
426
455
  actor: string;
427
456
  principal: string;
428
457
  };
458
+ /** What the instance's settings hold. */
459
+ export type Settings = {
460
+ /** Where people set up service accounts themselves (`ServiceSetting` in @coffre/core/access), projects by id. */
461
+ serviceAccounts: ServiceSetting;
462
+ };
463
+ export type SetSettingsInput = Correlation & Through & {
464
+ actor: string;
465
+ settings: Settings;
466
+ };
429
467
  /**
430
468
  * A prefix of the log the vault signed: every entry up to `seq`, the last
431
469
  * of which has `hash`, which pins every byte before it. Kept as the vault's
package/dist/vault.js CHANGED
@@ -1,4 +1,3 @@
1
- import { everyProjectPath, grantKind } from "./access.js";
2
1
  //#region src/checkpoint.ts
3
2
  /** What a checkpoint's signature covers: the prefix of the log up to entry `seq`, and when. */
4
3
  function checkpointMessage(checkpoint) {
@@ -26,7 +25,7 @@ function describeAccessFault(fault, names = {}) {
26
25
  const where = names.place ?? ((projectId, environmentId) => environmentId === null ? projectId : `${projectId}/${environmentId}`);
27
26
  const grant = (held) => {
28
27
  const { principal, projectId, environmentId, environmentSlug, role } = held;
29
- const place = grantKind(held) === "every-project" ? everyProjectPath(environmentSlug) : where(projectId ?? "no project", environmentId);
28
+ const place = projectId === null && environmentId === null ? environmentSlug == null ? "*" : `*/${environmentSlug}` : where(projectId ?? "no project", environmentId);
30
29
  return `${who(principal)} as ${role} on ${place}`;
31
30
  };
32
31
  switch (fault.kind) {
@@ -50,6 +49,7 @@ const ACCESS_ACTIONS = [
50
49
  "member.add",
51
50
  "member.restore",
52
51
  "member.owner",
52
+ "member.role",
53
53
  "member.remove",
54
54
  "access.grant",
55
55
  "access.revoke"
@@ -4,15 +4,18 @@
4
4
  * their platform signs for them (docs/design/oidc.md).
5
5
  *
6
6
  * A binding names a profile, an issuer, and claims a token must carry, each
7
- * a top-level string matched exactly. The profile sets the minimum: the
8
- * server refuses a binding that lacks a claim its profile requires, from the
9
- * UI, the CLI or the API alike. Say a service is bound to `deploy.yml`,
10
- * pushed to `main` of `acme/api`:
7
+ * a top-level string matched exactly. The event that started the run may
8
+ * be any of several: a list. The profile sets the minimum: the server
9
+ * refuses a binding that lacks a claim its profile requires, from the UI,
10
+ * the CLI or the API alike. Say a service is bound to `ci.yml` of
11
+ * `acme/api`, pushed or dispatched on `main`, or run for a pull request
12
+ * into `main`:
11
13
  *
12
14
  * { profile: 'github', issuer: 'https://token.actions.githubusercontent.com',
13
15
  * claims: { repository_owner_id: '9919', repository_id: '41532',
14
- * workflow_ref: 'acme/api/.github/workflows/deploy.yml@refs/heads/main',
15
- * ref: 'refs/heads/main', event_name: 'push' } }
16
+ * workflow_ref: 'acme/api/.github/workflows/ci.yml@refs/heads/main',
17
+ * ref: 'refs/heads/main',
18
+ * event_name: ['pull_request', 'push', 'workflow_dispatch'] } }
16
19
  */
17
20
  export declare const GITHUB_ISSUER = "https://token.actions.githubusercontent.com";
18
21
  export declare const GITLAB_ISSUER = "https://gitlab.com";
@@ -30,12 +33,29 @@ export declare const WORKLOAD_PROFILES: readonly WorkloadProfile[];
30
33
  /** The claims each profile requires; a binding may add more, to narrow it. */
31
34
  export declare const REQUIRED_CLAIMS: Record<WorkloadProfile, readonly string[]>;
32
35
  /**
33
- * Events triggered by someone with write access, at a ref the binding names
34
- * exactly. `pull_request`, `pull_request_target`, `workflow_run` and the
35
- * rest are refused, even when their ref happens to match: their runs can
36
- * carry a stranger's code.
36
+ * The events a GitHub binding may trust. The first four are started by
37
+ * someone with write access, at the ref the binding names. The last two
38
+ * can run code nobody reviewed, so the owner opts in to each:
39
+ *
40
+ * - `pull_request` runs the pull request's own code and workflow file, at
41
+ * `refs/pull/<n>/merge`; a binding matches it by the branch it merges
42
+ * into. Forks get no ID token, unless the repository sends write tokens
43
+ * to their pull requests (a setting of private repositories), so this
44
+ * trusts whoever can push a branch to the repository.
45
+ * - `workflow_run` runs the default branch's workflow, but a fork's pull
46
+ * request can start it: the run is as safe as what it does with the
47
+ * code and artifacts of the run that started it.
48
+ *
49
+ * `pull_request_target` and the rest are refused, even when their ref
50
+ * happens to match: they exist to act on anyone's pull request, with the
51
+ * base repository's tokens.
52
+ */
53
+ export declare const GITHUB_EVENTS: readonly ["push", "workflow_dispatch", "schedule", "release", "pull_request", "workflow_run"];
54
+ /**
55
+ * What trusting each of the last two events exposes, as the UI, the CLI and
56
+ * the approval page say it when one is chosen.
37
57
  */
38
- export declare const GITHUB_EVENTS: readonly ["push", "workflow_dispatch", "schedule", "release"];
58
+ export declare const EVENT_EXPOSURE: Readonly<Record<string, string>>;
39
59
  /** Pipelines that run the project's own code, at its own ref: never a merge request's. */
40
60
  export declare const GITLAB_PIPELINE_SOURCES: readonly ["push", "web", "schedule"];
41
61
  /** At most this many live bindings per service, and claims per binding. */
@@ -43,7 +63,9 @@ export declare const MAX_BINDINGS = 16;
43
63
  export declare const MAX_CLAIMS = 16;
44
64
  /** The longest claim value, in bytes. */
45
65
  export declare const MAX_CLAIM_BYTES = 256;
46
- export type BindingClaims = Record<string, string>;
66
+ /** A claim a binding names: one value, or, for what started the run, any of several. */
67
+ export type ClaimValue = string | string[];
68
+ export type BindingClaims = Record<string, ClaimValue>;
47
69
  /** What makes a binding, checked: its profile, its issuer as a URL, and its claims, sorted. */
48
70
  export type BindingPolicy = {
49
71
  profile: WorkloadProfile;
@@ -67,6 +89,17 @@ export declare function checkBinding(input: {
67
89
  }, options?: {
68
90
  allowLoopback?: boolean;
69
91
  }): BindingPolicy;
92
+ /** A claim's values, one or several. */
93
+ export declare function claimValues(value: ClaimValue | undefined): string[];
94
+ /**
95
+ * The claims of a verified token that differ from what a binding expects,
96
+ * by name: none when it matches. A claim matches when the token carries its
97
+ * value, or one of its values. A GitHub `pull_request` run, at
98
+ * `refs/pull/<n>/merge`, counts as a run at the branch it merges into: its
99
+ * `ref`, and the ref its `workflow_ref` ends in, are read as
100
+ * `refs/heads/<base_ref>`.
101
+ */
102
+ export declare function differingClaims(profile: WorkloadProfile, expected: BindingClaims, token: Record<string, unknown>): string[];
70
103
  /** The claims as the MAC and the database hold them: JSON, sorted by name. */
71
104
  export declare function canonicalClaims(claims: BindingClaims): string;
72
105
  /**
@@ -84,17 +117,26 @@ export declare function checkFetchUrl(value: string, what: string, options: {
84
117
  }): URL;
85
118
  /** `refs/heads/main`, or `refs/tags/v1`, as GitHub names a ref. */
86
119
  export declare function fullRef(kind: 'branch' | 'tag', name: string): string;
120
+ /** The GitHub events a binding at a branch, or at a tag, may name. */
121
+ export declare function githubEventsAt(kind: 'branch' | 'tag'): string[];
87
122
  /** A workflow file at a ref, as GitHub's tokens name it: `acme/api/.github/workflows/deploy.yml@refs/heads/main`. */
88
123
  export declare function workflowRef(repository: string, file: string, ref: string): string;
89
- /** A workflow of the repository, run by `event` at `ref`. */
124
+ /**
125
+ * What started the run, as the presets take it: `event` (`source`, for
126
+ * GitLab), the one value 0.4.6's presets took, or `events` (`sources`), any
127
+ * of several. Naming both, or neither, is a type error and, for a caller the
128
+ * types do not reach, a `BindingInvalid` at run time: a preset never guesses
129
+ * which one was meant.
130
+ */
131
+ export type Starts<One extends string, Many extends string> = ({ [K in One]: string; } & { [K in Many]?: never; }) | ({ [K in Many]: string[]; } & { [K in One]?: never; });
132
+ /** A workflow of the repository, run at `ref` by any of `events`; a pull request, by the branch it merges into. */
90
133
  export declare function githubWorkflow(input: {
91
134
  repository: string;
92
135
  repositoryId: string;
93
136
  ownerId: string;
94
137
  workflow: string;
95
138
  ref: string;
96
- event: string;
97
- }): Omit<BindingPolicy, 'issuer'>;
139
+ } & Starts<'event', 'events'>): Omit<BindingPolicy, 'issuer'>;
98
140
  /**
99
141
  * A reusable workflow at one commit, called at `ref`: from one repository,
100
142
  * or, with `repositoryId` null, from any repository of the organization. A
@@ -104,20 +146,18 @@ export declare function githubReusable(input: {
104
146
  repositoryId: string | null;
105
147
  ownerId: string;
106
148
  ref: string;
107
- event: string;
108
149
  called: string;
109
150
  sha: string;
110
151
  caller?: {
111
152
  repository: string;
112
153
  workflow: string;
113
154
  };
114
- }): Omit<BindingPolicy, 'issuer'>;
115
- /** A GitLab project's pipelines from `source`, at a branch or tag. */
155
+ } & Starts<'event', 'events'>): Omit<BindingPolicy, 'issuer'>;
156
+ /** A GitLab project's pipelines from any of `sources`, at a branch or tag. */
116
157
  export declare function gitlabProject(input: {
117
158
  namespaceId: string;
118
159
  projectId: string;
119
160
  refType: 'branch' | 'tag';
120
161
  ref: string;
121
- source: string;
122
- }): Omit<BindingPolicy, 'issuer'>;
162
+ } & Starts<'source', 'sources'>): Omit<BindingPolicy, 'issuer'>;
123
163
  //#endregion