@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.
@@ -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-DfviNOts.js";
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-DfviNOts.js";
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, isSyncPrincipal, mayManageAccess } from "@coffre/core/access";
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 (!PRINCIPAL.test(principal)) throw refused("invalid", `not a principal: ${principal}`);
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, principal, change))) throw refused("not_allowed");
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 (!PRINCIPAL.test(principal) || isSyncPrincipal(principal)) throw refused("invalid", `not a member: ${principal}`);
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
- const places = input.source === void 0 ? held : [...held, input.source];
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.11",
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/db": "0.1.11",
40
- "@coffre/core": "0.1.11"
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 (!PRINCIPAL.test(principal)) throw refused('invalid', `not a principal: ${principal}`);
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, principal, change))) throw refused('not_allowed');
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 (!PRINCIPAL.test(principal) || isSyncPrincipal(principal)) throw refused('invalid', `not a member: ${principal}`);
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
- // Owners remove anyone. Removing a sync only takes access away, so
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));