@coffre/vault 0.1.11 → 0.1.12
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/cloudflare.js +1 -1
- package/dist/node.js +1 -1
- package/dist/{vault-DfviNOts.js → vault-DTHHQw9L.js} +18 -28
- package/package.json +3 -3
- package/src/vault.ts +11 -35
package/dist/cloudflare.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as verifyCheckpoint, i as checkpointMessage, n as KekUnavailableError, o as derivedSigningKey, r as awsKms, s as resolveVaultConfig, t as KekBadClaimError } from "./src-DHuqw-IB.js";
|
|
2
|
-
import { n as prepareVault, t as openVault } from "./vault-
|
|
2
|
+
import { n as prepareVault, t as openVault } from "./vault-DTHHQw9L.js";
|
|
3
3
|
import { createDatabase } from "@coffre/db";
|
|
4
4
|
import { HyperdrivePool, postgres } from "@coffre/db/hyperdrive";
|
|
5
5
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
package/dist/node.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as verifyCheckpoint, i as checkpointMessage, n as KekUnavailableError, o as derivedSigningKey, r as awsKms, s as resolveVaultConfig, t as KekBadClaimError } from "./src-DHuqw-IB.js";
|
|
2
|
-
import { n as prepareVault, t as openVault } from "./vault-
|
|
2
|
+
import { n as prepareVault, t as openVault } from "./vault-DTHHQw9L.js";
|
|
3
3
|
import { chmodSync, existsSync, lstatSync, rmSync } from "node:fs";
|
|
4
4
|
import { Agent, createServer, request } from "node:http";
|
|
5
5
|
import { openDatabase } from "@coffre/db/connect";
|
|
@@ -2,7 +2,7 @@ import { ACCESS_ACTIONS, checkpointMessage, describeAccessFault, verifyCheckpoin
|
|
|
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 { allows, assignableToEnvironment, isRole,
|
|
5
|
+
import { allows, assignableToEnvironment, isRole, mayManageAccess } 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 { SNAPSHOT, clockMillis, engineOf, forUpdate, isUniqueViolation } from "@coffre/db/dialect";
|
|
@@ -819,6 +819,7 @@ const KEY_BUDGET_MS = 5e3;
|
|
|
819
819
|
/** How long a decision waits for a lock: above the key budget, so a removal outwaits a read in flight. */
|
|
820
820
|
const LOCK_TIMEOUT_MS = 15e3;
|
|
821
821
|
const PRINCIPAL = /^(user|token|sync):[^\s:][^\s]*$/;
|
|
822
|
+
const LIVE_PRINCIPAL = /^(user|token):[^\s:][^\s]*$/;
|
|
822
823
|
/** Who acts for the vault itself, as when it gives a root admin a member row. */
|
|
823
824
|
const VAULT_ACTOR = "system:vault";
|
|
824
825
|
/** How many stored keys a KEK with no check yet is tried on: one that opens proves it. */
|
|
@@ -1518,6 +1519,14 @@ var VaultService = class {
|
|
|
1518
1519
|
fault,
|
|
1519
1520
|
stored: grants$1
|
|
1520
1521
|
};
|
|
1522
|
+
if (!LIVE_PRINCIPAL.test(principal)) return {
|
|
1523
|
+
principal,
|
|
1524
|
+
status: "unknown",
|
|
1525
|
+
live: none,
|
|
1526
|
+
all: none,
|
|
1527
|
+
fault,
|
|
1528
|
+
stored: grants$1
|
|
1529
|
+
};
|
|
1521
1530
|
if (row?.status !== "active") return {
|
|
1522
1531
|
principal,
|
|
1523
1532
|
status: row?.status ?? "unknown",
|
|
@@ -1559,10 +1568,10 @@ var VaultService = class {
|
|
|
1559
1568
|
since: null,
|
|
1560
1569
|
by: null
|
|
1561
1570
|
};
|
|
1562
|
-
const active = !tampered && row?.status === "active";
|
|
1571
|
+
const active = LIVE_PRINCIPAL.test(principal) && !tampered && row?.status === "active";
|
|
1563
1572
|
return {
|
|
1564
1573
|
principal,
|
|
1565
|
-
status: tampered ? "tampered" : row?.status ?? "unknown",
|
|
1574
|
+
status: tampered ? "tampered" : !LIVE_PRINCIPAL.test(principal) ? "unknown" : row?.status ?? "unknown",
|
|
1566
1575
|
generation: row?.generation ?? 0,
|
|
1567
1576
|
isRootAdmin: false,
|
|
1568
1577
|
isOwner: active && row.owner && principal.startsWith("user:"),
|
|
@@ -1637,7 +1646,7 @@ var VaultService = class {
|
|
|
1637
1646
|
}]);
|
|
1638
1647
|
return this.#decide([actor, principal], async (d) => {
|
|
1639
1648
|
validateCorrelation(input);
|
|
1640
|
-
if (!
|
|
1649
|
+
if (!LIVE_PRINCIPAL.test(principal)) throw refused("invalid", `not a principal: ${principal}`);
|
|
1641
1650
|
if (this.#isRootAdmin(principal)) throw refused("root_admin");
|
|
1642
1651
|
const places$1 = /* @__PURE__ */ new Set();
|
|
1643
1652
|
for (const change of input.changes) {
|
|
@@ -1653,28 +1662,12 @@ var VaultService = class {
|
|
|
1653
1662
|
for (const { projectId, environmentId } of input.changes) if (!known.projects.has(projectId) || environmentId !== null && known.environments.get(environmentId) !== projectId) throw refused("invalid", `no such place: ${environmentId === null ? projectId : `${projectId}/${environmentId}`}`);
|
|
1654
1663
|
const acting = await this.#standing(d.tx, actor, d.members.get(actor), d.at, d.reports);
|
|
1655
1664
|
if (acting.status === "tampered") throw refused("tampered");
|
|
1656
|
-
if (!input.changes.every((change) => mayManageAccess(acting.live,
|
|
1665
|
+
if (!input.changes.every((change) => mayManageAccess(acting.live, change))) throw refused("not_allowed");
|
|
1657
1666
|
const row = d.members.get(principal);
|
|
1658
1667
|
const subject = await this.#standing(d.tx, principal, row, d.at, d.reports);
|
|
1659
1668
|
if (subject.status === "tampered") throw refused("tampered", TAMPERED_SUBJECT);
|
|
1660
1669
|
if (row?.status === "removed") throw refused("removed");
|
|
1661
|
-
if (row === void 0)
|
|
1662
|
-
if (!isSyncPrincipal(principal) || input.changes.every((change) => change.role === null)) throw refused("not_a_member");
|
|
1663
|
-
d.log.push(accessEntry(actor, "member.add", principal, "allow", input, { owner: false }));
|
|
1664
|
-
d.writes.push(async (at) => {
|
|
1665
|
-
await insertMember(d.tx, {
|
|
1666
|
-
principal,
|
|
1667
|
-
status: "active",
|
|
1668
|
-
owner: false,
|
|
1669
|
-
generation: 0,
|
|
1670
|
-
createdAt: at,
|
|
1671
|
-
createdBy: actor,
|
|
1672
|
-
statusChangedAt: at,
|
|
1673
|
-
statusChangedBy: actor,
|
|
1674
|
-
...UNSEALED
|
|
1675
|
-
});
|
|
1676
|
-
});
|
|
1677
|
-
}
|
|
1670
|
+
if (row === void 0) throw refused("not_a_member");
|
|
1678
1671
|
const held = subject.stored;
|
|
1679
1672
|
d.grants.set(principal, [...held]);
|
|
1680
1673
|
const changes = input.changes.map((change) => this.#apply(d, actor, principal, held, change, input));
|
|
@@ -1738,7 +1731,7 @@ var VaultService = class {
|
|
|
1738
1731
|
const acting = await this.#standing(d.tx, actor, d.members.get(actor), d.at, d.reports);
|
|
1739
1732
|
if (acting.status === "tampered") throw refused("tampered");
|
|
1740
1733
|
if (!acting.live.isOwner) throw refused("not_allowed", "only owners may add or restore members");
|
|
1741
|
-
if (!
|
|
1734
|
+
if (!LIVE_PRINCIPAL.test(principal)) throw refused("invalid", `not a member: ${principal}`);
|
|
1742
1735
|
if (this.#isRootAdmin(principal)) throw refused("root_admin");
|
|
1743
1736
|
if (input.owner === true && !principal.startsWith("user:")) throw refused("invalid", "service accounts cannot be owners");
|
|
1744
1737
|
const row = d.members.get(principal);
|
|
@@ -1792,6 +1785,7 @@ var VaultService = class {
|
|
|
1792
1785
|
const refused = (code, message = MESSAGES[code]) => new Refused(refusal(code, message), [accessEntry(actor, "member.remove", principal, "deny", input, {}, code)]);
|
|
1793
1786
|
return this.#decide([actor, principal], async (d) => {
|
|
1794
1787
|
validateCorrelation(input);
|
|
1788
|
+
if (!LIVE_PRINCIPAL.test(principal)) throw refused("invalid", `not a member: ${principal}`);
|
|
1795
1789
|
if (this.#isRootAdmin(principal)) throw refused("root_admin");
|
|
1796
1790
|
const row = d.members.get(principal);
|
|
1797
1791
|
const acting = await this.#standing(d.tx, actor, d.members.get(actor), d.at, d.reports);
|
|
@@ -1803,11 +1797,7 @@ var VaultService = class {
|
|
|
1803
1797
|
if (!holder.isOwner) throw refused("not_allowed", "only owners may remove a member whose record failed its check");
|
|
1804
1798
|
return this.#startOver(d, actor, principal, row, subject.fault, input, refused);
|
|
1805
1799
|
}
|
|
1806
|
-
|
|
1807
|
-
if (!(holder.isOwner || isSyncPrincipal(principal) && (held.length === 0 || places.some((place) => mayManageAccess(holder, principal, {
|
|
1808
|
-
...place,
|
|
1809
|
-
role: null
|
|
1810
|
-
}))))) throw refused("not_allowed", "only owners may remove members");
|
|
1800
|
+
if (!holder.isOwner) throw refused("not_allowed", "only owners may remove members");
|
|
1811
1801
|
if (row?.status !== "active") throw refused(row === void 0 ? "not_a_member" : "removed");
|
|
1812
1802
|
const revoked = held.filter((grant) => live(grant, d.at));
|
|
1813
1803
|
for (const grant of revoked) d.log.push({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffre/vault",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "coffre's vault: keys, grants and members, in the database the app uses, as a Cloudflare Worker or on Node.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"drizzle-orm": "0.45.2",
|
|
39
|
-
"@coffre/
|
|
40
|
-
"@coffre/
|
|
39
|
+
"@coffre/core": "0.1.12",
|
|
40
|
+
"@coffre/db": "0.1.12"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "26.1.1",
|
package/src/vault.ts
CHANGED
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
allows,
|
|
5
5
|
assignableToEnvironment,
|
|
6
6
|
isRole,
|
|
7
|
-
isSyncPrincipal,
|
|
8
7
|
mayManageAccess,
|
|
9
8
|
type Holdings,
|
|
10
9
|
type Permission,
|
|
@@ -149,7 +148,9 @@ const KEY_BUDGET_MS = 5_000;
|
|
|
149
148
|
/** How long a decision waits for a lock: above the key budget, so a removal outwaits a read in flight. */
|
|
150
149
|
const LOCK_TIMEOUT_MS = 15_000;
|
|
151
150
|
|
|
151
|
+
// Historical principals remain valid audit subjects and sealed rows.
|
|
152
152
|
const PRINCIPAL = /^(user|token|sync):[^\s:][^\s]*$/;
|
|
153
|
+
const LIVE_PRINCIPAL = /^(user|token):[^\s:][^\s]*$/;
|
|
153
154
|
|
|
154
155
|
/** Who acts for the vault itself, as when it gives a root admin a member row. */
|
|
155
156
|
const VAULT_ACTOR = 'system:vault';
|
|
@@ -937,6 +938,7 @@ class VaultService implements Vault {
|
|
|
937
938
|
const grants = row === undefined ? [] : await store.grants(db, principal);
|
|
938
939
|
const fault = await this.#integrity(db, principal, row, grants, reports);
|
|
939
940
|
if (fault !== null) return { principal, status: 'tampered', live: none, all: none, fault, stored: grants };
|
|
941
|
+
if (!LIVE_PRINCIPAL.test(principal)) return { principal, status: 'unknown', live: none, all: none, fault, stored: grants };
|
|
940
942
|
if (row?.status !== 'active') return { principal, status: row?.status ?? 'unknown', live: none, all: none, fault, stored: grants };
|
|
941
943
|
const held = grants.map((grant) => ({ ...grant, role: grant.role as Role }));
|
|
942
944
|
const isOwner = row.owner && principal.startsWith('user:');
|
|
@@ -954,10 +956,10 @@ class VaultService implements Vault {
|
|
|
954
956
|
if (this.#isRootAdmin(principal)) {
|
|
955
957
|
return { principal, status: 'active', generation: row?.generation ?? 0, isRootAdmin: true, isOwner: true, grants: [], since: null, by: null };
|
|
956
958
|
}
|
|
957
|
-
const active = !tampered && row?.status === 'active';
|
|
959
|
+
const active = LIVE_PRINCIPAL.test(principal) && !tampered && row?.status === 'active';
|
|
958
960
|
return {
|
|
959
961
|
principal,
|
|
960
|
-
status: tampered ? 'tampered' : (row?.status ?? 'unknown'),
|
|
962
|
+
status: tampered ? 'tampered' : !LIVE_PRINCIPAL.test(principal) ? 'unknown' : (row?.status ?? 'unknown'),
|
|
961
963
|
generation: row?.generation ?? 0,
|
|
962
964
|
isRootAdmin: false,
|
|
963
965
|
isOwner: active && row.owner && principal.startsWith('user:'),
|
|
@@ -1033,7 +1035,7 @@ class VaultService implements Vault {
|
|
|
1033
1035
|
}]);
|
|
1034
1036
|
return this.#decide([actor, principal], async (d) => {
|
|
1035
1037
|
validateCorrelation(input);
|
|
1036
|
-
if (!
|
|
1038
|
+
if (!LIVE_PRINCIPAL.test(principal)) throw refused('invalid', `not a principal: ${principal}`);
|
|
1037
1039
|
if (this.#isRootAdmin(principal)) throw refused('root_admin');
|
|
1038
1040
|
const places = new Set<string>();
|
|
1039
1041
|
for (const change of input.changes) {
|
|
@@ -1061,32 +1063,13 @@ class VaultService implements Vault {
|
|
|
1061
1063
|
}
|
|
1062
1064
|
const acting = await this.#standing(d.tx, actor, d.members.get(actor), d.at, d.reports);
|
|
1063
1065
|
if (acting.status === 'tampered') throw refused('tampered');
|
|
1064
|
-
if (!input.changes.every((change) => mayManageAccess(acting.live,
|
|
1066
|
+
if (!input.changes.every((change) => mayManageAccess(acting.live, change))) throw refused('not_allowed');
|
|
1065
1067
|
|
|
1066
1068
|
const row = d.members.get(principal);
|
|
1067
1069
|
const subject = await this.#standing(d.tx, principal, row, d.at, d.reports);
|
|
1068
1070
|
if (subject.status === 'tampered') throw refused('tampered', TAMPERED_SUBJECT);
|
|
1069
1071
|
if (row?.status === 'removed') throw refused('removed');
|
|
1070
|
-
if (row === undefined)
|
|
1071
|
-
// A sync is a member from its first grant; anyone else is admitted first.
|
|
1072
|
-
if (!isSyncPrincipal(principal) || input.changes.every((change) => change.role === null)) {
|
|
1073
|
-
throw refused('not_a_member');
|
|
1074
|
-
}
|
|
1075
|
-
d.log.push(accessEntry(actor, 'member.add', principal, 'allow', input, { owner: false }));
|
|
1076
|
-
d.writes.push(async (at) => {
|
|
1077
|
-
await store.insertMember(d.tx, {
|
|
1078
|
-
principal,
|
|
1079
|
-
status: 'active',
|
|
1080
|
-
owner: false,
|
|
1081
|
-
generation: 0,
|
|
1082
|
-
createdAt: at,
|
|
1083
|
-
createdBy: actor,
|
|
1084
|
-
statusChangedAt: at,
|
|
1085
|
-
statusChangedBy: actor,
|
|
1086
|
-
...UNSEALED,
|
|
1087
|
-
});
|
|
1088
|
-
});
|
|
1089
|
-
}
|
|
1072
|
+
if (row === undefined) throw refused('not_a_member');
|
|
1090
1073
|
// The grants the check verified, not a second read: what the decision changes, and seals.
|
|
1091
1074
|
const held = subject.stored;
|
|
1092
1075
|
d.grants.set(principal, [...held]);
|
|
@@ -1156,7 +1139,7 @@ class VaultService implements Vault {
|
|
|
1156
1139
|
const acting = await this.#standing(d.tx, actor, d.members.get(actor), d.at, d.reports);
|
|
1157
1140
|
if (acting.status === 'tampered') throw refused('tampered');
|
|
1158
1141
|
if (!acting.live.isOwner) throw refused('not_allowed', 'only owners may add or restore members');
|
|
1159
|
-
if (!
|
|
1142
|
+
if (!LIVE_PRINCIPAL.test(principal)) throw refused('invalid', `not a member: ${principal}`);
|
|
1160
1143
|
if (this.#isRootAdmin(principal)) throw refused('root_admin');
|
|
1161
1144
|
if (input.owner === true && !principal.startsWith('user:')) {
|
|
1162
1145
|
throw refused('invalid', 'service accounts cannot be owners');
|
|
@@ -1208,6 +1191,7 @@ class VaultService implements Vault {
|
|
|
1208
1191
|
new Refused(refusal(code, message), [accessEntry(actor, 'member.remove', principal, 'deny', input, {}, code)]);
|
|
1209
1192
|
return this.#decide([actor, principal], async (d) => {
|
|
1210
1193
|
validateCorrelation(input);
|
|
1194
|
+
if (!LIVE_PRINCIPAL.test(principal)) throw refused('invalid', `not a member: ${principal}`);
|
|
1211
1195
|
if (this.#isRootAdmin(principal)) throw refused('root_admin');
|
|
1212
1196
|
const row = d.members.get(principal);
|
|
1213
1197
|
const acting = await this.#standing(d.tx, actor, d.members.get(actor), d.at, d.reports);
|
|
@@ -1219,15 +1203,7 @@ class VaultService implements Vault {
|
|
|
1219
1203
|
if (!holder.isOwner) throw refused('not_allowed', 'only owners may remove a member whose record failed its check');
|
|
1220
1204
|
return this.#startOver(d, actor, principal, row, subject.fault!, input, refused);
|
|
1221
1205
|
}
|
|
1222
|
-
|
|
1223
|
-
// whoever may take away one of its grants, or manage it at its
|
|
1224
|
-
// source, may remove it, and anyone may remove one that holds nothing.
|
|
1225
|
-
const places = input.source === undefined ? held : [...held, input.source];
|
|
1226
|
-
const may =
|
|
1227
|
-
holder.isOwner ||
|
|
1228
|
-
(isSyncPrincipal(principal) &&
|
|
1229
|
-
(held.length === 0 || places.some((place) => mayManageAccess(holder, principal, { ...place, role: null }))));
|
|
1230
|
-
if (!may) throw refused('not_allowed', 'only owners may remove members');
|
|
1206
|
+
if (!holder.isOwner) throw refused('not_allowed', 'only owners may remove members');
|
|
1231
1207
|
if (row?.status !== 'active') throw refused(row === undefined ? 'not_a_member' : 'removed');
|
|
1232
1208
|
|
|
1233
1209
|
const revoked = held.filter((grant) => live(grant, d.at));
|