@coffre/vault 0.5.0 → 0.6.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 +1 -1
- package/dist/cloudflare.d.ts +5 -8
- package/dist/cloudflare.js +2 -2
- package/dist/{index-CGVAd1mz.d.ts → index-CwsWOH6j.d.ts} +27 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.js +2 -2
- package/dist/{src-1yVV2L0_.js → src-CQJS3OZZ.js} +10 -10
- package/dist/{vault-CP8yU8xq.js → vault-ihE3Yc4a.js} +247 -189
- package/package.json +3 -3
- package/src/cloudflare.ts +1 -1
- package/src/config.ts +45 -19
- package/src/store.ts +20 -35
- package/src/vault.ts +244 -138
|
@@ -2,13 +2,13 @@ import { ACCESS_ACTIONS, checkpointMessage, checkpointVerifier, describeAccessFa
|
|
|
2
2
|
import { DEK_BYTES, KEY_CHECK, KEY_CHECK_CONTEXT, KEY_CHECK_VALUE, KekBadClaimError, KekCancelledError, KekUnavailableError, LocalKekProvider } from "@coffre/core/kek";
|
|
3
3
|
import { createHmac, hkdfSync, randomUUID, timingSafeEqual } from "node:crypto";
|
|
4
4
|
import { tablesOf } from "@coffre/db";
|
|
5
|
-
import { EVERYWHERE, NOTHING, allows, assignableToEnvironment,
|
|
5
|
+
import { EVERYWHERE, NOTHING, allows, assignableToEnvironment, givesService, grantKind, isCredentialScope, isInstanceRole, isRole, isScope, managesService, mayManageAccess, normalScope, ownsInstance, runsInstance, setsUpServices, unscoped } from "@coffre/core/access";
|
|
6
6
|
import { GENESIS_HASH, deriveLogKey, verifyEntries } from "@coffre/core/audit";
|
|
7
7
|
import { checkContext } from "@coffre/core/envelope";
|
|
8
8
|
import { shownMember } from "@coffre/core/schemas";
|
|
9
9
|
import { SNAPSHOT, clockMillis, engineOf, forUpdate, isUniqueViolation, tombstone, truth } from "@coffre/db/dialect";
|
|
10
|
-
import { accessEntriesAbout, insertGrant, newestAccess, readGrants, reportStands, roleColumns, storedRole } from "@coffre/db/grants";
|
|
11
|
-
import { LOOK_BACK, appendEntries, entryColumns, lockLogHead, newestAuthentic } from "@coffre/db/log";
|
|
10
|
+
import { accessEntriesAbout, everyProjectConversion, insertGrant, liveProjects, newestAccess, readGrants, reportStands, roleColumns, storedRole } from "@coffre/db/grants";
|
|
11
|
+
import { LOOK_BACK, appendEntries, entryColumns, lockLogHead, newestAuthentic, newestVaultEntries } from "@coffre/db/log";
|
|
12
12
|
import { and, asc, count, desc, eq, gt, gte, inArray, isNull, lt, lte, or, sql } from "drizzle-orm";
|
|
13
13
|
//#region src/accounting.ts
|
|
14
14
|
const KEY_ACTIONS = /* @__PURE__ */ new Set([
|
|
@@ -228,6 +228,16 @@ async function allMembers(db) {
|
|
|
228
228
|
return (await db.select(memberColumns(db)).from(vaultMembers).orderBy(asc(vaultMembers.principal))).map(memberOf);
|
|
229
229
|
}
|
|
230
230
|
/**
|
|
231
|
+
* The rows that say their member is an active Owner of the whole instance
|
|
232
|
+
* (`ownsInstance` in @coffre/core/access), as stored: a row changed around
|
|
233
|
+
* the vault says so too, which the caller checks. The scope is read as the
|
|
234
|
+
* row's MAC reads it, parsed: any way of writing everywhere is everywhere.
|
|
235
|
+
*/
|
|
236
|
+
async function instanceOwners(db) {
|
|
237
|
+
const { vaultMembers } = tablesOf(db);
|
|
238
|
+
return (await db.select(memberColumns(db)).from(vaultMembers).where(and(eq(vaultMembers.status, "active"), eq(vaultMembers.role, "owner"))).orderBy(asc(vaultMembers.principal))).map(memberOf).filter(ownsInstance);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
231
241
|
* Lock these members' rows for the rest of the transaction, in one order
|
|
232
242
|
* everywhere, and read them: those that have none are not in the map. Every
|
|
233
243
|
* decision about a member takes its row first, so two of them, from any
|
|
@@ -331,26 +341,6 @@ async function places(db, projectIds, environmentIds) {
|
|
|
331
341
|
deleted: new Set([...foundProjects, ...foundEnvironments].filter((row) => row.deleted).map((row) => row.id))
|
|
332
342
|
};
|
|
333
343
|
}
|
|
334
|
-
/** Every project not deleted, with its environments not deleted: what a grant on every project of 0.4 reached. */
|
|
335
|
-
async function liveProjects(db) {
|
|
336
|
-
const { projects, environments } = tablesOf(db);
|
|
337
|
-
const [found, inside] = await Promise.all([db.select({
|
|
338
|
-
id: projects.id,
|
|
339
|
-
deleted: truth(tombstone(projects.slug))
|
|
340
|
-
}).from(projects).orderBy(asc(projects.id)), db.select({
|
|
341
|
-
id: environments.id,
|
|
342
|
-
projectId: environments.projectId,
|
|
343
|
-
slug: environments.slug,
|
|
344
|
-
deleted: truth(tombstone(environments.slug))
|
|
345
|
-
}).from(environments).orderBy(asc(environments.id))]);
|
|
346
|
-
return found.filter((project) => !project.deleted).map((project) => ({
|
|
347
|
-
id: project.id,
|
|
348
|
-
environments: inside.filter((environment) => environment.projectId === project.id && !environment.deleted).map(({ id, slug }) => ({
|
|
349
|
-
id,
|
|
350
|
-
slug
|
|
351
|
-
}))
|
|
352
|
-
}));
|
|
353
|
-
}
|
|
354
344
|
/** Who holds a grant on every project of 0.4, lapsed or not, which the vault replaces (`#convert`). */
|
|
355
345
|
async function everyProjectHolders(db) {
|
|
356
346
|
const { vaultGrants } = tablesOf(db);
|
|
@@ -476,11 +466,6 @@ async function latestVaultEntry(db, actions) {
|
|
|
476
466
|
const [row] = await db.select(entryColumns(db)).from(auditLog).where(and(eq(auditLog.author, "vault"), inArray(auditLog.action, [...actions]), eq(auditLog.decision, "allow"))).orderBy(desc(auditLog.seq)).limit(1);
|
|
477
467
|
return row === void 0 ? void 0 : stored([row])[0];
|
|
478
468
|
}
|
|
479
|
-
/** Up to `limit` of the vault's allowed entries of `action` before `before` (the newest when undefined), newest first. */
|
|
480
|
-
async function newestVaultEntries(db, action, limit, before) {
|
|
481
|
-
const { auditLog } = tablesOf(db);
|
|
482
|
-
return stored(await db.select(entryColumns(db)).from(auditLog).where(and(eq(auditLog.author, "vault"), eq(auditLog.action, action), eq(auditLog.decision, "allow"), before === void 0 ? void 0 : lt(auditLog.seq, before))).orderBy(desc(auditLog.seq)).limit(limit));
|
|
483
|
-
}
|
|
484
469
|
/** The vault's newest allowed access entry about each member who has one. */
|
|
485
470
|
async function newestAccessEntries(db) {
|
|
486
471
|
const { auditLog } = tablesOf(db);
|
|
@@ -964,7 +949,6 @@ async function prepareVault(config, options = {}) {
|
|
|
964
949
|
clockOffset: options.clockOffset ?? (() => 0)
|
|
965
950
|
},
|
|
966
951
|
verified: UNVERIFIED,
|
|
967
|
-
rooted: /* @__PURE__ */ new Set(),
|
|
968
952
|
reported: /* @__PURE__ */ new Set(),
|
|
969
953
|
since: null,
|
|
970
954
|
superseded: null,
|
|
@@ -1000,7 +984,7 @@ const PRINCIPAL = /^(user|token):[^\s:][^\s]*$/;
|
|
|
1000
984
|
/** A person, and a service account: only a person sets one up (`givesService`). */
|
|
1001
985
|
const PERSON = /^user:/;
|
|
1002
986
|
const SERVICE = /^token:/;
|
|
1003
|
-
/** Who acts for the vault itself, as when it
|
|
987
|
+
/** Who acts for the vault itself, as when it makes an Owner its deployment names. */
|
|
1004
988
|
const VAULT_ACTOR = "system:vault";
|
|
1005
989
|
/** How many stored keys a KEK with no check yet is tried on: one that opens proves it. */
|
|
1006
990
|
const KEY_CHECK_SAMPLE = 3;
|
|
@@ -1024,6 +1008,12 @@ const CLOSED_SETTINGS = { serviceAccounts: {
|
|
|
1024
1008
|
projects: { only: [] },
|
|
1025
1009
|
environments: "all"
|
|
1026
1010
|
} };
|
|
1011
|
+
/** Why a change to who is an Owner, scoped or not, is refused to anyone else: Owners make and unmake Owners. */
|
|
1012
|
+
const OWNERS_ONLY = "only an Owner of the whole instance makes someone an Owner, or changes or removes one";
|
|
1013
|
+
/** Why nobody demotes or removes a person the deployment names (`owners`), themselves included. */
|
|
1014
|
+
function declaredOwner(principal) {
|
|
1015
|
+
return `the deployment names ${principal.slice(5)} an Owner (owners, in the vault's configuration): take them off that list and deploy first`;
|
|
1016
|
+
}
|
|
1027
1017
|
/** Why a person's change to a service account is refused. */
|
|
1028
1018
|
const NOT_THEIRS = "a person gives a service account at most what they hold, where the instance lets them set one up, and only to one whose every grant they hold";
|
|
1029
1019
|
/** Who asks for checkpoints: the app's scheduled job. */
|
|
@@ -1072,7 +1062,7 @@ const MESSAGES = {
|
|
|
1072
1062
|
deleted: "this project or environment was deleted",
|
|
1073
1063
|
ended: "the reference read through was ended",
|
|
1074
1064
|
not_allowed: "not allowed to change this",
|
|
1075
|
-
|
|
1065
|
+
last_owner: "the last Owner of the whole instance stays one: make someone else an Owner first",
|
|
1076
1066
|
invalid: "not something the rules allow",
|
|
1077
1067
|
log_broken: "the vault log does not hold from the last checkpoint",
|
|
1078
1068
|
wrong_kek: "this vault's key does not open the data it holds",
|
|
@@ -1319,7 +1309,7 @@ var VaultService = class {
|
|
|
1319
1309
|
async #settled() {
|
|
1320
1310
|
const prepared = this.#prepared;
|
|
1321
1311
|
if (prepared.since === null && prepared.superseded === null) await this.#settle();
|
|
1322
|
-
if (prepared.superseded === null && !prepared.converted) await this.#convert();
|
|
1312
|
+
if (prepared.superseded === null && prepared.since !== null && !prepared.converted) await this.#convert();
|
|
1323
1313
|
return prepared.superseded;
|
|
1324
1314
|
}
|
|
1325
1315
|
/**
|
|
@@ -1336,14 +1326,16 @@ var VaultService = class {
|
|
|
1336
1326
|
* genuine row put back). `reportStands` is the rule, which `coffre
|
|
1337
1327
|
* migrate` applies too, holding no key, to every entry in the vault's
|
|
1338
1328
|
* name: it lists and marks a member so reported, forged or not.
|
|
1339
|
-
* Removing them clears both.
|
|
1329
|
+
* Removing them clears both. Then everyone the deployment names becomes
|
|
1330
|
+
* an Owner (`#declare`): after, so that what their grants become is what
|
|
1331
|
+
* `coffre migrate` says, by the same rule over the same rows
|
|
1332
|
+
* (`everyProjectConversion`), which cannot tell who the configuration
|
|
1333
|
+
* names. Until both finish, each call does again what is left, a few
|
|
1334
|
+
* reads once nothing is.
|
|
1340
1335
|
*/
|
|
1341
1336
|
async #convert() {
|
|
1342
|
-
for (const principal of await everyProjectHolders(this.#db))
|
|
1343
|
-
|
|
1344
|
-
await this.#decideNow([principal], (d) => this.#replaceEveryProject(d, principal));
|
|
1345
|
-
}
|
|
1346
|
-
this.#prepared.converted = true;
|
|
1337
|
+
for (const principal of await everyProjectHolders(this.#db)) await this.#decideNow([principal], (d) => this.#replaceEveryProject(d, principal));
|
|
1338
|
+
this.#prepared.converted = await this.#declare(this.#config.owners.map((email) => `user:${email}`));
|
|
1347
1339
|
}
|
|
1348
1340
|
async #replaceEveryProject(d, principal) {
|
|
1349
1341
|
const row = d.members.get(principal);
|
|
@@ -1354,22 +1346,7 @@ var VaultService = class {
|
|
|
1354
1346
|
if (await reportStands(d.tx, principal, this.#check(d.reports, principal))) return;
|
|
1355
1347
|
await lockLogHead(d.tx);
|
|
1356
1348
|
const before = roleOf(principal, row);
|
|
1357
|
-
const conversion =
|
|
1358
|
-
person: principal.startsWith("user:"),
|
|
1359
|
-
role: before.role,
|
|
1360
|
-
everyProject: everyProject.filter((grant) => live(grant, d.at)).map((grant) => ({
|
|
1361
|
-
environmentSlug: grant.environmentSlug,
|
|
1362
|
-
role: grant.role,
|
|
1363
|
-
expiresAt: grant.expiresAt
|
|
1364
|
-
})),
|
|
1365
|
-
grants: subject.stored.filter((grant) => grantKind(grant) !== null && live(grant, d.at)).map((grant) => ({
|
|
1366
|
-
projectId: grant.projectId,
|
|
1367
|
-
environmentId: grant.environmentId,
|
|
1368
|
-
role: grant.role,
|
|
1369
|
-
expiresAt: grant.expiresAt
|
|
1370
|
-
})),
|
|
1371
|
-
projects: await liveProjects(d.tx)
|
|
1372
|
-
});
|
|
1349
|
+
const conversion = everyProjectConversion(principal, before.role, subject.stored, d.at, await liveProjects(d.tx));
|
|
1373
1350
|
const after = {
|
|
1374
1351
|
role: conversion.role,
|
|
1375
1352
|
scope: conversion.scope
|
|
@@ -1693,7 +1670,6 @@ var VaultService = class {
|
|
|
1693
1670
|
};
|
|
1694
1671
|
}
|
|
1695
1672
|
async #refuseVersions(principal, code, entries) {
|
|
1696
|
-
if (this.#isRootAdmin(principal)) await this.#rootRow(principal);
|
|
1697
1673
|
return this.#decide([principal], async () => {
|
|
1698
1674
|
throw new Refused(refusal(code, MESSAGES[code]), entries);
|
|
1699
1675
|
});
|
|
@@ -1729,7 +1705,6 @@ var VaultService = class {
|
|
|
1729
1705
|
if (wrongKek !== null) return this.#decide([], async () => {
|
|
1730
1706
|
throw new Refused(refusal("wrong_kek", wrongKek), secrets.map((secret) => entry(secret, "deny", "wrong_kek")));
|
|
1731
1707
|
});
|
|
1732
|
-
if (this.#isRootAdmin(principal)) await this.#rootRow(principal);
|
|
1733
1708
|
const check = async (d) => {
|
|
1734
1709
|
const reader = through(await this.#standing(d.tx, principal, d.members.get(principal), d.at, d.reports), call.input);
|
|
1735
1710
|
const at = secrets.map((secret, i) => call.places?.[i] ?? secret);
|
|
@@ -1875,7 +1850,6 @@ var VaultService = class {
|
|
|
1875
1850
|
})
|
|
1876
1851
|
};
|
|
1877
1852
|
};
|
|
1878
|
-
if (this.#isRootAdmin(principal)) await this.#rootRow(principal);
|
|
1879
1853
|
return this.#decide([principal], async (d) => {
|
|
1880
1854
|
const reader = through(await this.#standing(d.tx, principal, d.members.get(principal), d.at, d.reports), input);
|
|
1881
1855
|
const environments = await environmentsById(d.tx, items.flatMap((item) => {
|
|
@@ -1948,7 +1922,6 @@ var VaultService = class {
|
|
|
1948
1922
|
...traced(input)
|
|
1949
1923
|
})
|
|
1950
1924
|
});
|
|
1951
|
-
if (this.#isRootAdmin(principal)) await this.#rootRow(principal);
|
|
1952
1925
|
if (sealed.some((seal) => seal === null)) return this.#decide([principal], async () => {
|
|
1953
1926
|
throw new Refused(refusal("bad_claim", MESSAGES.bad_claim), vias.map((via, i) => entry(via, sealed[i], "deny", "bad_claim")));
|
|
1954
1927
|
});
|
|
@@ -1996,80 +1969,129 @@ var VaultService = class {
|
|
|
1996
1969
|
return sealedReference(entry, via.reference);
|
|
1997
1970
|
});
|
|
1998
1971
|
}
|
|
1999
|
-
|
|
2000
|
-
|
|
1972
|
+
/** Whether the deployment names `principal` an Owner of the whole instance (`owners`). */
|
|
1973
|
+
#listed(principal) {
|
|
1974
|
+
return principal.startsWith("user:") && this.#config.owners.includes(principal.slice(5));
|
|
2001
1975
|
}
|
|
2002
1976
|
/**
|
|
2003
|
-
*
|
|
2004
|
-
*
|
|
2005
|
-
*
|
|
2006
|
-
*
|
|
2007
|
-
*
|
|
2008
|
-
* anyone a root admin; the configuration does. Rows are never deleted, so
|
|
2009
|
-
* one read finds it made already, as on every fresh isolate but the first,
|
|
2010
|
-
* without queueing on the log's lock.
|
|
1977
|
+
* An Owner of the whole instance other than `except`, as their row says
|
|
1978
|
+
* and its check holds (`#integrity`), or null when there is none. A row
|
|
1979
|
+
* changed around the vault owns nothing, and is reported. Read under the
|
|
1980
|
+
* log's head, which every change to a member takes before it writes, the
|
|
1981
|
+
* answer stands until the reader commits.
|
|
2011
1982
|
*/
|
|
2012
|
-
async #
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
this.#
|
|
2016
|
-
return;
|
|
1983
|
+
async #anOwner(db, reports, except = null) {
|
|
1984
|
+
for (const row of await instanceOwners(db)) {
|
|
1985
|
+
if (row.principal === except) continue;
|
|
1986
|
+
if (await this.#integrity(db, row.principal, row, await grants(db, row.principal), reports) === null) return row.principal;
|
|
2017
1987
|
}
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
1988
|
+
return null;
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* Make each of `principals`, people the deployment names (`owners`), an
|
|
1992
|
+
* Owner of the whole instance, whoever else is one: asked about all of
|
|
1993
|
+
* them at the start of each process (`#convert`), and at `access` about
|
|
1994
|
+
* one who owns nothing, which is a new instance's first sign-in, before
|
|
1995
|
+
* the vault has written to its log, and the way back for someone whose
|
|
1996
|
+
* row was changed around the vault since. An unlocked read of each finds
|
|
1997
|
+
* who is one already, the quick way out: a hint, never a report, and
|
|
1998
|
+
* never `#record`, which would settle the vault, and so convert, again;
|
|
1999
|
+
* one it cannot tell, a row changing under it as another process makes
|
|
2000
|
+
* them an Owner, is due. Then one decision, under the others' rows, reads
|
|
2001
|
+
* again (`#makeOwner`). One whose row the log never admitted stays as it is,
|
|
2002
|
+
* and is asked about again at each of their requests. Whether it ran.
|
|
2003
|
+
*/
|
|
2004
|
+
async #declare(principals) {
|
|
2005
|
+
const at = await this.#now(this.#db);
|
|
2006
|
+
const due = [];
|
|
2007
|
+
for (const principal of principals) {
|
|
2008
|
+
const subject = await this.#standing(this.#db, principal, await member(this.#db, principal), at, []).catch((error) => {
|
|
2009
|
+
if (error instanceof Retry) return null;
|
|
2010
|
+
throw error;
|
|
2011
|
+
});
|
|
2012
|
+
if (!(subject?.status === "active" && ownsInstance(subject.live))) due.push(principal);
|
|
2013
|
+
}
|
|
2014
|
+
if (due.length === 0) return true;
|
|
2015
|
+
return (await this.#decideNow(due, async (d) => {
|
|
2016
|
+
for (const principal of due) await this.#makeOwner(d, principal, "owners");
|
|
2017
|
+
})).ok;
|
|
2018
|
+
}
|
|
2019
|
+
/**
|
|
2020
|
+
* Refuse a change that would leave `principal`, an Owner of the whole
|
|
2021
|
+
* instance, no longer one, unless another is. Counted under the log's
|
|
2022
|
+
* head: two changes at once, each demoting or removing a different
|
|
2023
|
+
* Owner, lock different rows, and only the head puts one after the other.
|
|
2024
|
+
*/
|
|
2025
|
+
async #keepAnOwner(d, actor, principal, refused) {
|
|
2026
|
+
await lockLogHead(d.tx);
|
|
2027
|
+
if (await this.#anOwner(d.tx, d.reports, principal) !== null) return;
|
|
2028
|
+
throw refused("last_owner", actor === principal ? "you're the last Owner of the whole instance: make someone else an Owner first" : `${principal.slice(5)} is the last Owner of the whole instance: make someone else an Owner first`);
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* Make `principal`, a person, an Owner of the whole instance, as the
|
|
2032
|
+
* vault, for `reason`: admitted, brought back, or given the role, the
|
|
2033
|
+
* grants they hold kept. One whose row fails its check is started over
|
|
2034
|
+
* first, as removing them would; one the log never admitted, whose row was
|
|
2035
|
+
* written around the vault, is left as it is. Whether it made them one.
|
|
2036
|
+
*/
|
|
2037
|
+
async #makeOwner(d, principal, reason) {
|
|
2038
|
+
const row = d.members.get(principal);
|
|
2039
|
+
const subject = await this.#standing(d.tx, principal, row, d.at, d.reports);
|
|
2040
|
+
if (subject.status === "active" && ownsInstance(subject.live)) return false;
|
|
2041
|
+
const tampered = subject.status === "tampered";
|
|
2042
|
+
if (tampered && await this.#logged(d.tx, principal) === void 0) return false;
|
|
2043
|
+
const correlation = { operationId: randomUUID() };
|
|
2044
|
+
const owner = {
|
|
2045
|
+
role: "owner",
|
|
2046
|
+
scope: EVERYWHERE
|
|
2047
|
+
};
|
|
2048
|
+
d.grants.set(principal, [...subject.stored]);
|
|
2049
|
+
d.touched.add(principal);
|
|
2050
|
+
if (tampered) await this.#startOver(d, VAULT_ACTOR, principal, row, subject.fault, correlation, (code, message) => new Refused(refusal(code, message ?? MESSAGES[code]), []));
|
|
2051
|
+
if (row === void 0 && !tampered) {
|
|
2052
|
+
d.log.push(accessEntry(VAULT_ACTOR, "member.add", principal, "allow", correlation, {
|
|
2053
|
+
...owner,
|
|
2054
|
+
reason
|
|
2055
|
+
}));
|
|
2056
|
+
d.writes.push((at) => insertMember(d.tx, {
|
|
2032
2057
|
principal,
|
|
2033
2058
|
status: "active",
|
|
2034
|
-
|
|
2035
|
-
scope: EVERYWHERE,
|
|
2059
|
+
...owner,
|
|
2036
2060
|
generation: 0,
|
|
2037
2061
|
createdAt: at,
|
|
2038
2062
|
createdBy: VAULT_ACTOR,
|
|
2039
2063
|
statusChangedAt: at,
|
|
2040
2064
|
statusChangedBy: VAULT_ACTOR,
|
|
2041
|
-
|
|
2042
|
-
};
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2065
|
+
...UNSEALED
|
|
2066
|
+
}));
|
|
2067
|
+
} else if (tampered || row?.status === "removed") {
|
|
2068
|
+
d.log.push(accessEntry(VAULT_ACTOR, "member.restore", principal, "allow", correlation, {
|
|
2069
|
+
...owner,
|
|
2070
|
+
reason
|
|
2071
|
+
}));
|
|
2072
|
+
d.writes.push((at) => updateMember(d.tx, principal, {
|
|
2073
|
+
status: "active",
|
|
2074
|
+
...owner,
|
|
2075
|
+
statusChangedAt: at,
|
|
2076
|
+
statusChangedBy: VAULT_ACTOR
|
|
2077
|
+
}));
|
|
2078
|
+
} else {
|
|
2079
|
+
d.log.push(accessEntry(VAULT_ACTOR, "member.role", principal, "allow", correlation, {
|
|
2080
|
+
...owner,
|
|
2081
|
+
previousRole: row.role,
|
|
2082
|
+
previousScope: row.scope,
|
|
2083
|
+
reason
|
|
2084
|
+
}));
|
|
2085
|
+
d.writes.push(() => updateMember(d.tx, principal, owner));
|
|
2086
|
+
}
|
|
2087
|
+
return true;
|
|
2049
2088
|
}
|
|
2050
2089
|
/**
|
|
2051
2090
|
* What `principal` holds, their row checked first (`#integrity`): a row
|
|
2052
|
-
* that fails holds nothing, and is `tampered`.
|
|
2053
|
-
* configuration, which no row can change.
|
|
2091
|
+
* that fails holds nothing, and is `tampered`.
|
|
2054
2092
|
*/
|
|
2055
2093
|
async #standing(db, principal, row, at, reports) {
|
|
2056
2094
|
const none = NOTHING;
|
|
2057
|
-
if (this.#isRootAdmin(principal)) {
|
|
2058
|
-
const root = {
|
|
2059
|
-
isRootAdmin: true,
|
|
2060
|
-
role: "owner",
|
|
2061
|
-
scope: EVERYWHERE,
|
|
2062
|
-
grants: []
|
|
2063
|
-
};
|
|
2064
|
-
return {
|
|
2065
|
-
principal,
|
|
2066
|
-
status: "active",
|
|
2067
|
-
live: root,
|
|
2068
|
-
all: root,
|
|
2069
|
-
fault: null,
|
|
2070
|
-
stored: []
|
|
2071
|
-
};
|
|
2072
|
-
}
|
|
2073
2095
|
const grants$1 = row === void 0 ? [] : await grants(db, principal);
|
|
2074
2096
|
const fault = await this.#integrity(db, principal, row, grants$1, reports);
|
|
2075
2097
|
if (fault !== null) return {
|
|
@@ -2105,13 +2127,11 @@ var VaultService = class {
|
|
|
2105
2127
|
principal,
|
|
2106
2128
|
status: "active",
|
|
2107
2129
|
live: {
|
|
2108
|
-
isRootAdmin: false,
|
|
2109
2130
|
role,
|
|
2110
2131
|
scope,
|
|
2111
2132
|
grants: held.filter((grant) => live(grant, at))
|
|
2112
2133
|
},
|
|
2113
2134
|
all: {
|
|
2114
|
-
isRootAdmin: false,
|
|
2115
2135
|
role,
|
|
2116
2136
|
scope,
|
|
2117
2137
|
grants: held
|
|
@@ -2121,23 +2141,11 @@ var VaultService = class {
|
|
|
2121
2141
|
};
|
|
2122
2142
|
}
|
|
2123
2143
|
#access(principal, row, held, at, tampered) {
|
|
2124
|
-
if (this.#isRootAdmin(principal)) return {
|
|
2125
|
-
principal,
|
|
2126
|
-
status: "active",
|
|
2127
|
-
generation: row?.generation ?? 0,
|
|
2128
|
-
isRootAdmin: true,
|
|
2129
|
-
role: "owner",
|
|
2130
|
-
scope: EVERYWHERE,
|
|
2131
|
-
grants: [],
|
|
2132
|
-
since: null,
|
|
2133
|
-
by: null
|
|
2134
|
-
};
|
|
2135
2144
|
const active = PRINCIPAL.test(principal) && !tampered && row?.status === "active";
|
|
2136
2145
|
return {
|
|
2137
2146
|
principal,
|
|
2138
2147
|
status: tampered ? "tampered" : !PRINCIPAL.test(principal) ? "unknown" : row?.status ?? "unknown",
|
|
2139
2148
|
generation: row?.generation ?? 0,
|
|
2140
|
-
isRootAdmin: false,
|
|
2141
2149
|
...active ? roleOf(principal, row) : {
|
|
2142
2150
|
role: "member",
|
|
2143
2151
|
scope: EVERYWHERE
|
|
@@ -2151,18 +2159,29 @@ var VaultService = class {
|
|
|
2151
2159
|
* What `principal` holds now. Read without locks, the fast way; a row that
|
|
2152
2160
|
* seems to fail its check is read again in one snapshot before anyone is
|
|
2153
2161
|
* called tampered, since a change committed between two of the reads
|
|
2154
|
-
* looks like one.
|
|
2162
|
+
* looks like one. A person the deployment names (`owners`) who owns
|
|
2163
|
+
* nothing becomes an Owner of the whole instance first (`#declare`). A
|
|
2164
|
+
* vault that holds no key the log's newest entries are under, given the
|
|
2165
|
+
* wrong vault key, cannot tell a row it cannot check from one changed
|
|
2166
|
+
* around it: it says so, an outage, rather than call the member tampered.
|
|
2167
|
+
* A row it can check still answers, so a forged entry that stops it
|
|
2168
|
+
* writing leaves the Owners able to sign in and find it.
|
|
2155
2169
|
*/
|
|
2156
2170
|
async access(principal) {
|
|
2157
|
-
await this.#settled();
|
|
2158
|
-
|
|
2171
|
+
const superseded = await this.#settled();
|
|
2172
|
+
const access = await this.#read(principal, superseded);
|
|
2173
|
+
if (superseded !== null || !this.#listed(principal) || access.status === "active" && ownsInstance(access)) return access;
|
|
2174
|
+
return await this.#declare([principal]) ? this.#read(principal, superseded) : access;
|
|
2175
|
+
}
|
|
2176
|
+
/** `access`, as the rows are now. */
|
|
2177
|
+
async #read(principal, superseded) {
|
|
2159
2178
|
const read = async (db, reports) => {
|
|
2160
2179
|
const [row, held, at] = await Promise.all([
|
|
2161
2180
|
member(db, principal),
|
|
2162
2181
|
grants(db, principal),
|
|
2163
2182
|
this.#now(db)
|
|
2164
2183
|
]);
|
|
2165
|
-
const fault =
|
|
2184
|
+
const fault = await this.#integrity(db, principal, row, held, reports);
|
|
2166
2185
|
return this.#access(principal, row, held, at, fault !== null);
|
|
2167
2186
|
};
|
|
2168
2187
|
const found = [];
|
|
@@ -2176,6 +2195,7 @@ var VaultService = class {
|
|
|
2176
2195
|
}
|
|
2177
2196
|
const reports = [];
|
|
2178
2197
|
const access = await this.#db.transaction((tx) => read(tx, reports), SNAPSHOT);
|
|
2198
|
+
if (access.status === "tampered" && superseded !== null) throw new Error(superseded);
|
|
2179
2199
|
await this.#record(reports);
|
|
2180
2200
|
return access;
|
|
2181
2201
|
}
|
|
@@ -2194,7 +2214,6 @@ var VaultService = class {
|
|
|
2194
2214
|
const held = await grants(db);
|
|
2195
2215
|
const newest = await newestAccessEntries(db);
|
|
2196
2216
|
for (const row of rows) {
|
|
2197
|
-
if (this.#isRootAdmin(row.principal)) continue;
|
|
2198
2217
|
const grants = held.filter((grant) => grant.principal === row.principal);
|
|
2199
2218
|
const entry = newest.get(row.principal);
|
|
2200
2219
|
if (!(entry !== void 0 && this.#authentic(entry) && this.#sealed(row, grants) && entry.seq === row.accessSeq)) await this.#integrity(db, row.principal, row, grants, reports);
|
|
@@ -2227,7 +2246,6 @@ var VaultService = class {
|
|
|
2227
2246
|
return this.#decide([actor, principal], async (d) => {
|
|
2228
2247
|
validateCorrelation(input);
|
|
2229
2248
|
if (!PRINCIPAL.test(principal)) throw refused("invalid", `not a principal: ${principal}`);
|
|
2230
|
-
if (this.#isRootAdmin(principal)) throw refused("root_admin");
|
|
2231
2249
|
const places$1 = /* @__PURE__ */ new Set();
|
|
2232
2250
|
for (const change of changes) {
|
|
2233
2251
|
if (grantKind(change) === null) throw refused("invalid", change.projectId === null && change.environmentId === null ? "grants are on a project or an environment: a person reaches every project by their instance role" : "an environment is named in its project");
|
|
@@ -2255,15 +2273,19 @@ var VaultService = class {
|
|
|
2255
2273
|
if (acting.status === "tampered") throw refused("tampered");
|
|
2256
2274
|
const manages = where.every((place) => mayManageAccess(acting.live, place));
|
|
2257
2275
|
if (!manages && !(SERVICE.test(principal) && PERSON.test(actor))) throw refused("not_allowed");
|
|
2258
|
-
if (actor === principal &&
|
|
2276
|
+
if (actor === principal && changes.some((change) => change.role !== null)) throw refused("not_allowed", "nobody grants themselves a role: ask another admin");
|
|
2259
2277
|
const row = d.members.get(principal);
|
|
2260
2278
|
const subject = await this.#standing(d.tx, principal, row, d.at, d.reports);
|
|
2261
2279
|
if (subject.status === "tampered") throw refused("tampered", TAMPERED_SUBJECT);
|
|
2262
|
-
|
|
2263
|
-
if (
|
|
2280
|
+
const admitted = input.admit === true && row?.status !== "active" ? await this.#admitWith(d, acting, principal, row, input) : void 0;
|
|
2281
|
+
if (admitted === void 0 && row?.status === "removed") throw refused("removed");
|
|
2282
|
+
if (admitted === void 0 && row === void 0) throw refused("not_a_member");
|
|
2264
2283
|
if (!manages) {
|
|
2265
2284
|
const setting = (await this.#settingsAt(d.tx, d.reports)).serviceAccounts;
|
|
2266
|
-
const account =
|
|
2285
|
+
const account = admitted !== void 0 || row === void 0 ? {
|
|
2286
|
+
grants: [],
|
|
2287
|
+
admittedBy: actor
|
|
2288
|
+
} : await this.#account(d.tx, subject, row);
|
|
2267
2289
|
if (!changes.every((change, i) => {
|
|
2268
2290
|
const before = subject.live.grants.find((grant) => placeKey({
|
|
2269
2291
|
...grant,
|
|
@@ -2276,9 +2298,23 @@ var VaultService = class {
|
|
|
2276
2298
|
d.grants.set(principal, [...held]);
|
|
2277
2299
|
const outcomes = changes.map((change) => this.#apply(d, actor, principal, held, change, input));
|
|
2278
2300
|
if (d.writes.length > 0) d.touched.add(principal);
|
|
2279
|
-
return {
|
|
2301
|
+
return {
|
|
2302
|
+
changes: outcomes,
|
|
2303
|
+
...admitted === void 0 ? {} : { admitted }
|
|
2304
|
+
};
|
|
2280
2305
|
});
|
|
2281
2306
|
}
|
|
2307
|
+
/**
|
|
2308
|
+
* Admit a service account that `setAccess` gives its first grants, as
|
|
2309
|
+
* `admit` would, its refusals logged as `member.add`'s: someone who does
|
|
2310
|
+
* not run the instance sets one up as they would through `admit`.
|
|
2311
|
+
*/
|
|
2312
|
+
async #admitWith(d, acting, principal, row, input) {
|
|
2313
|
+
const refused = (code, message = MESSAGES[code]) => new Refused(refusal(code, message), [accessEntry(acting.principal, "member.add", principal, "deny", input, { changes: input.changes }, code)]);
|
|
2314
|
+
if (!SERVICE.test(principal)) throw refused("invalid", "only a service account is admitted with its grants: a person, by an instance role");
|
|
2315
|
+
if (!runsInstance(acting.live)) await this.#setsUp(d, acting, true, refused);
|
|
2316
|
+
return this.#enlist(d, acting, principal, row, void 0, input, refused);
|
|
2317
|
+
}
|
|
2282
2318
|
/** One place's change, logged when it changes what is live. */
|
|
2283
2319
|
#apply(d, actor, principal, held, change, correlation) {
|
|
2284
2320
|
const existing = held.find((grant) => placeKey(grant) === placeKey(change));
|
|
@@ -2341,15 +2377,9 @@ var VaultService = class {
|
|
|
2341
2377
|
validateCorrelation(input);
|
|
2342
2378
|
const acting = through(await this.#standing(d.tx, actor, d.members.get(actor), d.at, d.reports), input);
|
|
2343
2379
|
if (acting.status === "tampered") throw refused("tampered");
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
const setting = (await this.#settingsAt(d.tx, d.reports)).serviceAccounts;
|
|
2347
|
-
if (!service || !PERSON.test(actor) || !setsUpServices(acting.live, setting, placesOf(await liveProjects(d.tx)))) throw refused("not_allowed", "only admins and owners of the whole instance may add people or set their roles, and a service account takes access somewhere the instance lets people set one up");
|
|
2348
|
-
const removed = d.members.get(principal);
|
|
2349
|
-
if (removed?.status === "removed" && removed.statusChangedBy !== actor) throw refused("not_allowed", `${shownMember(removed.statusChangedBy)} removed this service account: only they, or someone who runs the whole instance, may bring it back`);
|
|
2350
|
-
}
|
|
2380
|
+
const service = SERVICE.test(principal) && (input.role ?? "member") === "member" && input.scope === void 0;
|
|
2381
|
+
if (!runsInstance(acting.live)) await this.#setsUp(d, acting, service, refused);
|
|
2351
2382
|
if (!PRINCIPAL.test(principal)) throw refused("invalid", `not a member: ${shownMember(principal)}`);
|
|
2352
|
-
if (this.#isRootAdmin(principal)) throw refused("root_admin");
|
|
2353
2383
|
if (input.role !== void 0 && !isInstanceRole(input.role)) throw refused("invalid", `no such instance role: ${input.role}`);
|
|
2354
2384
|
if (input.scope !== void 0 && (input.role === void 0 || !isScope(input.scope))) throw refused("invalid", "a scope is a role's: { projects, environments }, each all, only or except");
|
|
2355
2385
|
const wanted = input.role === void 0 ? void 0 : {
|
|
@@ -2362,53 +2392,22 @@ var VaultService = class {
|
|
|
2362
2392
|
if (subject.status === "tampered") throw refused("tampered", TAMPERED_SUBJECT);
|
|
2363
2393
|
d.touched.add(principal);
|
|
2364
2394
|
d.grants.set(principal, [...subject.stored]);
|
|
2365
|
-
const entry = (action, detail) => d.log.push(accessEntry(actor, action, principal, "allow", input, detail));
|
|
2366
2395
|
if (row === void 0 || row.status === "removed") {
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
scope: EVERYWHERE
|
|
2370
|
-
};
|
|
2371
|
-
entry(row === void 0 ? "member.add" : "member.restore", {
|
|
2372
|
-
role,
|
|
2373
|
-
scope
|
|
2374
|
-
});
|
|
2375
|
-
d.writes.push(async (at) => {
|
|
2376
|
-
if (row === void 0) await insertMember(d.tx, {
|
|
2377
|
-
principal,
|
|
2378
|
-
status: "active",
|
|
2379
|
-
role,
|
|
2380
|
-
scope,
|
|
2381
|
-
generation: 0,
|
|
2382
|
-
createdAt: at,
|
|
2383
|
-
createdBy: actor,
|
|
2384
|
-
statusChangedAt: at,
|
|
2385
|
-
statusChangedBy: actor,
|
|
2386
|
-
...UNSEALED
|
|
2387
|
-
});
|
|
2388
|
-
else await updateMember(d.tx, principal, {
|
|
2389
|
-
status: "active",
|
|
2390
|
-
role,
|
|
2391
|
-
scope,
|
|
2392
|
-
statusChangedAt: at,
|
|
2393
|
-
statusChangedBy: actor
|
|
2394
|
-
});
|
|
2395
|
-
});
|
|
2396
|
-
return {
|
|
2397
|
-
created: true,
|
|
2398
|
-
role,
|
|
2399
|
-
scope,
|
|
2400
|
-
generation: row?.generation ?? 0
|
|
2401
|
-
};
|
|
2396
|
+
if (this.#listed(principal) && !(wanted !== void 0 && ownsInstance(wanted))) throw refused("not_allowed", declaredOwner(principal));
|
|
2397
|
+
return this.#enlist(d, acting, principal, row, wanted, input, refused);
|
|
2402
2398
|
}
|
|
2403
2399
|
const changed = wanted !== void 0 && JSON.stringify(standing(wanted)) !== JSON.stringify(standing(row));
|
|
2404
|
-
if (changed &&
|
|
2400
|
+
if (changed && this.#listed(principal) && !ownsInstance(wanted)) throw refused("not_allowed", declaredOwner(principal));
|
|
2401
|
+
if (changed && (wanted.role === "owner" || subject.live.role === "owner") && !ownsInstance(acting.live)) throw refused("not_allowed", OWNERS_ONLY);
|
|
2402
|
+
if (changed && actor === principal && !ownsInstance(acting.live)) throw refused("not_allowed", "nobody changes their own instance role: ask another admin");
|
|
2403
|
+
if (changed && ownsInstance(subject.live) && !ownsInstance(wanted)) await this.#keepAnOwner(d, actor, principal, refused);
|
|
2405
2404
|
if (changed) {
|
|
2406
|
-
|
|
2405
|
+
d.log.push(accessEntry(actor, "member.role", principal, "allow", input, {
|
|
2407
2406
|
role: wanted.role,
|
|
2408
2407
|
scope: wanted.scope,
|
|
2409
2408
|
previousRole: row.role,
|
|
2410
2409
|
previousScope: row.scope
|
|
2411
|
-
});
|
|
2410
|
+
}));
|
|
2412
2411
|
d.writes.push(() => updateMember(d.tx, principal, {
|
|
2413
2412
|
role: wanted.role,
|
|
2414
2413
|
scope: wanted.scope
|
|
@@ -2427,19 +2426,75 @@ var VaultService = class {
|
|
|
2427
2426
|
};
|
|
2428
2427
|
});
|
|
2429
2428
|
}
|
|
2429
|
+
/**
|
|
2430
|
+
* Whether `acting`, who does not run the instance, may admit `principal`:
|
|
2431
|
+
* only a service account (`service`: one asked for with no role), as a
|
|
2432
|
+
* person who could give it a grant somewhere (`setsUpServices`), and one
|
|
2433
|
+
* removed only if they removed it themselves, never one someone else
|
|
2434
|
+
* took away.
|
|
2435
|
+
*/
|
|
2436
|
+
async #setsUp(d, acting, service, refused) {
|
|
2437
|
+
const setting = (await this.#settingsAt(d.tx, d.reports)).serviceAccounts;
|
|
2438
|
+
if (!service || !PERSON.test(acting.principal) || !setsUpServices(acting.live, setting, placesOf(await liveProjects(d.tx)))) throw refused("not_allowed", "only admins and owners of the whole instance may add people or set their roles, and a service account takes access somewhere the instance lets people set one up");
|
|
2439
|
+
}
|
|
2440
|
+
/**
|
|
2441
|
+
* Admit `principal`, who has no row or a removed one: a fresh start, as a
|
|
2442
|
+
* member unless given a role (`wanted`), at the generation a removal
|
|
2443
|
+
* moved on. The caller has checked who may, and locked the row.
|
|
2444
|
+
*/
|
|
2445
|
+
#enlist(d, acting, principal, row, wanted, correlation, refused) {
|
|
2446
|
+
const actor = acting.principal;
|
|
2447
|
+
if (row?.status === "removed" && row.statusChangedBy !== actor && !runsInstance(acting.live)) throw refused("not_allowed", `${shownMember(row.statusChangedBy)} removed this service account: only they, or someone who runs the whole instance, may bring it back`);
|
|
2448
|
+
if (wanted?.role === "owner" && !ownsInstance(acting.live)) throw refused("not_allowed", OWNERS_ONLY);
|
|
2449
|
+
const { role, scope } = wanted ?? {
|
|
2450
|
+
role: "member",
|
|
2451
|
+
scope: EVERYWHERE
|
|
2452
|
+
};
|
|
2453
|
+
d.log.push(accessEntry(actor, row === void 0 ? "member.add" : "member.restore", principal, "allow", correlation, {
|
|
2454
|
+
role,
|
|
2455
|
+
scope
|
|
2456
|
+
}));
|
|
2457
|
+
d.writes.push(async (at) => {
|
|
2458
|
+
if (row === void 0) await insertMember(d.tx, {
|
|
2459
|
+
principal,
|
|
2460
|
+
status: "active",
|
|
2461
|
+
role,
|
|
2462
|
+
scope,
|
|
2463
|
+
generation: 0,
|
|
2464
|
+
createdAt: at,
|
|
2465
|
+
createdBy: actor,
|
|
2466
|
+
statusChangedAt: at,
|
|
2467
|
+
statusChangedBy: actor,
|
|
2468
|
+
...UNSEALED
|
|
2469
|
+
});
|
|
2470
|
+
else await updateMember(d.tx, principal, {
|
|
2471
|
+
status: "active",
|
|
2472
|
+
role,
|
|
2473
|
+
scope,
|
|
2474
|
+
statusChangedAt: at,
|
|
2475
|
+
statusChangedBy: actor
|
|
2476
|
+
});
|
|
2477
|
+
});
|
|
2478
|
+
return {
|
|
2479
|
+
created: true,
|
|
2480
|
+
role,
|
|
2481
|
+
scope,
|
|
2482
|
+
generation: row?.generation ?? 0
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2430
2485
|
remove(input) {
|
|
2431
2486
|
const { actor, principal } = input;
|
|
2432
2487
|
const refused = (code, message = MESSAGES[code]) => new Refused(refusal(code, message), [accessEntry(actor, "member.remove", principal, "deny", input, {}, code)]);
|
|
2433
2488
|
return this.#decide([actor, principal], async (d) => {
|
|
2434
2489
|
validateCorrelation(input);
|
|
2435
2490
|
if (!PRINCIPAL.test(principal)) throw refused("invalid", `not a member: ${shownMember(principal)}`);
|
|
2436
|
-
if (this.#isRootAdmin(principal)) throw refused("root_admin");
|
|
2437
2491
|
const row = d.members.get(principal);
|
|
2438
2492
|
const acting = through(await this.#standing(d.tx, actor, d.members.get(actor), d.at, d.reports), input);
|
|
2439
2493
|
if (acting.status === "tampered") throw refused("tampered");
|
|
2440
2494
|
const holder = acting.live;
|
|
2441
2495
|
const subject = await this.#standing(d.tx, principal, row, d.at, d.reports);
|
|
2442
2496
|
const held = subject.stored;
|
|
2497
|
+
if (this.#listed(principal)) throw refused("not_allowed", declaredOwner(principal));
|
|
2443
2498
|
if (subject.status === "tampered") {
|
|
2444
2499
|
if (!runsInstance(holder)) throw refused("not_allowed", "only admins and owners of the whole instance may remove a member whose record failed its check");
|
|
2445
2500
|
return this.#startOver(d, actor, principal, row, subject.fault, input, refused);
|
|
@@ -2448,6 +2503,8 @@ var VaultService = class {
|
|
|
2448
2503
|
if (!(SERVICE.test(principal) && PERSON.test(actor) && row?.status === "active" && managesService(holder, (await this.#settingsAt(d.tx, d.reports)).serviceAccounts, actor, await this.#account(d.tx, subject, row)))) throw refused("not_allowed", "only admins and owners of the whole instance may remove members, and a service account whoever manages it");
|
|
2449
2504
|
}
|
|
2450
2505
|
if (row?.status !== "active") throw refused(row === void 0 ? "not_a_member" : "removed");
|
|
2506
|
+
if (subject.live.role === "owner" && !ownsInstance(holder)) throw refused("not_allowed", OWNERS_ONLY);
|
|
2507
|
+
if (ownsInstance(subject.live)) await this.#keepAnOwner(d, actor, principal, refused);
|
|
2451
2508
|
const revoked = held.filter((grant) => live(grant, d.at));
|
|
2452
2509
|
for (const grant of revoked) {
|
|
2453
2510
|
const where = located(grant);
|
|
@@ -2814,7 +2871,8 @@ var VaultService = class {
|
|
|
2814
2871
|
publicKey,
|
|
2815
2872
|
until: i === 0 ? null : until
|
|
2816
2873
|
}])),
|
|
2817
|
-
|
|
2874
|
+
owners: [...this.#config.owners],
|
|
2875
|
+
rootAdmins: []
|
|
2818
2876
|
};
|
|
2819
2877
|
}
|
|
2820
2878
|
async keyChecks() {
|