@coffre/db 0.4.5 → 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.
Files changed (44) hide show
  1. package/dist/connect.d.ts +1 -1
  2. package/dist/{database-edkMkG02.d.ts → database-C63WjCrC.d.ts} +1 -1
  3. package/dist/dialect.d.ts +7 -1
  4. package/dist/dialect.js +12 -1
  5. package/dist/grants.d.ts +105 -10
  6. package/dist/grants.js +207 -7
  7. package/dist/hyperdrive.d.ts +1 -1
  8. package/dist/index.d.ts +2 -2
  9. package/dist/log.d.ts +392 -2
  10. package/dist/log.js +53 -1
  11. package/dist/migrations/postgres/0002_instance_roles.sql +4 -0
  12. package/dist/migrations/postgres/0003_credential_scopes.sql +4 -0
  13. package/dist/migrations/postgres/meta/0002_snapshot.json +2860 -0
  14. package/dist/migrations/postgres/meta/0003_snapshot.json +2872 -0
  15. package/dist/migrations/postgres/meta/_journal.json +14 -0
  16. package/dist/migrations/sqlite/0002_instance_roles.sql +2 -0
  17. package/dist/migrations/sqlite/0003_credential_scopes.sql +2 -0
  18. package/dist/migrations/sqlite/meta/0002_snapshot.json +2594 -0
  19. package/dist/migrations/sqlite/meta/0003_snapshot.json +2608 -0
  20. package/dist/migrations/sqlite/meta/_journal.json +14 -0
  21. package/dist/portable.d.ts +2 -2
  22. package/dist/{schema-BqCB3XFm.d.ts → schema-a7PkFgea.d.ts} +68 -0
  23. package/dist/schema-sqlite.d.ts +76 -0
  24. package/dist/schema-sqlite.js +4 -0
  25. package/dist/schema-version.d.ts +1 -1
  26. package/dist/schema-version.js +60 -26
  27. package/dist/schema.d.ts +1 -1
  28. package/dist/schema.js +4 -0
  29. package/package.json +2 -2
  30. package/src/dialect.ts +14 -0
  31. package/src/grants.ts +272 -11
  32. package/src/log.ts +66 -2
  33. package/src/migrations/postgres/0002_instance_roles.sql +4 -0
  34. package/src/migrations/postgres/0003_credential_scopes.sql +4 -0
  35. package/src/migrations/postgres/meta/0002_snapshot.json +2860 -0
  36. package/src/migrations/postgres/meta/0003_snapshot.json +2872 -0
  37. package/src/migrations/postgres/meta/_journal.json +14 -0
  38. package/src/migrations/sqlite/0002_instance_roles.sql +2 -0
  39. package/src/migrations/sqlite/0003_credential_scopes.sql +2 -0
  40. package/src/migrations/sqlite/meta/0002_snapshot.json +2594 -0
  41. package/src/migrations/sqlite/meta/0003_snapshot.json +2608 -0
  42. package/src/migrations/sqlite/meta/_journal.json +14 -0
  43. package/src/schema.sqlite.ts +6 -0
  44. package/src/schema.ts +11 -1
package/dist/connect.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as Database } from "./database-edkMkG02.js";
1
+ import { t as Database } from "./database-C63WjCrC.js";
2
2
  import { Engine } from "./dialect.js";
3
3
  import { ClientConfig } from "pg";
4
4
  //#region src/postgres.d.ts
@@ -1,4 +1,4 @@
1
- import { m as schema_d_exports } from "./schema-BqCB3XFm.js";
1
+ import { m as schema_d_exports } from "./schema-a7PkFgea.js";
2
2
  import { Table } from "drizzle-orm";
3
3
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
4
4
  import pg from "pg";
package/dist/dialect.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { i as Queryable } from "./database-edkMkG02.js";
1
+ import { i as Queryable } from "./database-C63WjCrC.js";
2
2
  import { SQL, SQLWrapper } from "drizzle-orm";
3
3
  import { PgTable } from "drizzle-orm/pg-core";
4
4
  //#region src/dialect.d.ts
@@ -74,6 +74,12 @@ export declare function clock(db: Queryable): SQL<string>;
74
74
  * before one written while it waited. Read it in a statement after the lock.
75
75
  */
76
76
  export declare function clockMillis(db: Queryable): SQL<number>;
77
+ /**
78
+ * Whether a timestamp column is still ahead of the database clock as the
79
+ * statement runs: a condition, for a write that must not land once the
80
+ * time it names has passed.
81
+ */
82
+ export declare function ahead(db: Queryable, column: SQLWrapper): SQL;
77
83
  /**
78
84
  * The migrator's own ledger, read by readiness to tell a database that is
79
85
  * up but not yet migrated from one that is ready. Drizzle keeps it in a
package/dist/dialect.js CHANGED
@@ -124,6 +124,17 @@ function clockMillis(db) {
124
124
  }
125
125
  }
126
126
  /**
127
+ * Whether a timestamp column is still ahead of the database clock as the
128
+ * statement runs: a condition, for a write that must not land once the
129
+ * time it names has passed.
130
+ */
131
+ function ahead(db, column) {
132
+ switch (engineOf(db)) {
133
+ case "postgres": return sql`${column} > clock_timestamp()`;
134
+ case "sqlite": return sql`${column} > CAST(unixepoch('subsec') * 1000 AS INTEGER)`;
135
+ }
136
+ }
137
+ /**
127
138
  * The migrator's own ledger, read by readiness to tell a database that is
128
139
  * up but not yet migrated from one that is ready. Drizzle keeps it in a
129
140
  * `drizzle` schema on Postgres, and in a plain table elsewhere.
@@ -158,4 +169,4 @@ function canonicalTimestamp(raw) {
158
169
  return `${(/* @__PURE__ */ new Date(Date.UTC(+year, +month - 1, +day, +hour, +minute, +second) - offsetMinutes * 6e4)).toISOString().slice(0, 19)}.${fraction.padEnd(6, "0")}Z`;
159
170
  }
160
171
  //#endregion
161
- export { SNAPSHOT, canonicalTimestamp, changedRows, clock, clockMillis, engineOf, forUpdate, insertIfAbsent, isUniqueViolation, isUnreachable, migrationLedger, tombstone, truth, upsert };
172
+ export { SNAPSHOT, ahead, canonicalTimestamp, changedRows, clock, clockMillis, engineOf, forUpdate, insertIfAbsent, isUniqueViolation, isUnreachable, migrationLedger, tombstone, truth, upsert };
package/dist/grants.d.ts CHANGED
@@ -1,11 +1,15 @@
1
- import { i as Queryable } from "./database-edkMkG02.js";
1
+ import { i as Queryable } from "./database-C63WjCrC.js";
2
+ import { Authenticity, Newest } from "./log.js";
3
+ import { Conversion, InstanceRole, Scope } from "@coffre/core/access";
4
+ import { StoredEntry } from "@coffre/core/audit";
2
5
  //#region src/grants.d.ts
3
6
  /**
4
7
  * One grant, as `vault_grants` holds it: on a project (`environmentId`
5
- * null), one of its environments (`projectId` is the environment's
6
- * project), every project (both ids null), or one environment slug in every
7
- * project (`environmentSlug`). Only the vault writes grants; the app reads
8
- * them for its lists.
8
+ * null), or one of its environments (`projectId` is the environment's
9
+ * project). A grant on every project of 0.4 names neither, and
10
+ * `environmentSlug` the one environment slug it covers in each, until the
11
+ * vault replaces it. Only the vault writes grants; the app reads them for
12
+ * its lists.
9
13
  */
10
14
  export type GrantRow = {
11
15
  principal: string;
@@ -18,15 +22,106 @@ export type GrantRow = {
18
22
  grantedAt: number;
19
23
  grantedBy: string;
20
24
  };
21
- /**
22
- * Grants, lapsed ones too unless `liveAt` says when to judge them: one
23
- * member's, or everyone's; `everyProject`, only those on every project.
24
- */
25
+ /** Grants, lapsed ones too unless `liveAt` says when to judge them: one member's, or everyone's. */
25
26
  export declare function readGrants(db: Queryable, filter?: {
26
27
  principal?: string;
27
28
  liveAt?: number;
28
- everyProject?: boolean;
29
29
  }): Promise<GrantRow[]>;
30
30
  /** Add a grant; an environment's row names only the environment. */
31
31
  export declare function insertGrant(db: Queryable, grant: GrantRow): Promise<void>;
32
+ /** A member's instance role and its scope, as `vault_members` stores them. */
33
+ export type StoredRole = {
34
+ owner: boolean;
35
+ role: string | null;
36
+ scope: string | null;
37
+ };
38
+ /**
39
+ * A member's instance role and its scope, as their row says: a row no vault
40
+ * of 0.5 wrote has no role, and its `owner` says whether they are an Admin.
41
+ * A role or a scope the vault would never have written reads as holding
42
+ * nothing (the vault then refuses the row by its MAC); a member's scope is
43
+ * everywhere, since a member's role reaches nothing.
44
+ */
45
+ export declare function storedRole({ owner, role, scope }: StoredRole): {
46
+ role: InstanceRole;
47
+ scope: Scope;
48
+ };
49
+ /** The columns that store a member's instance role and scope: `owner` too, for a vault of 0.4 to read. */
50
+ export declare function roleColumns(role: InstanceRole, scope: Scope): StoredRole;
51
+ /** What the vault does, when this version first runs, to one member's grants on every project of 0.4. */
52
+ export type EveryProjectPreview = {
53
+ principal: string;
54
+ /** Each, as a path (`*`, or `*` and the slug it covers), its role and its end. */
55
+ before: {
56
+ place: string;
57
+ role: string;
58
+ expiresAt: number | null;
59
+ }[];
60
+ conversion: Conversion;
61
+ /** The places the conversion names, by id: `market`, `market/dev`. */
62
+ paths: Record<string, string>;
63
+ /**
64
+ * Whether the vault has reported their row changed around it since the
65
+ * last change to what they hold (`tamperedMembers`), by the entries in its
66
+ * name: then it leaves them as they are, unless the report is forged.
67
+ */
68
+ reported: boolean;
69
+ };
70
+ /**
71
+ * What the vault will make of the grants on every project of 0.4 that live
72
+ * members hold, by the rule it converts them with
73
+ * (`convertEveryProjectGrants`), from the rows as they are: what
74
+ * `coffre migrate` says before the vault does it. A display: the vault
75
+ * reads again, under its locks, when it converts, and checks what this
76
+ * cannot, holding no key: each row's MAC, and each entry's in its name. So
77
+ * a member it has reported changed around it is listed, and marked
78
+ * `reported`, by the vault's own rule (`tamperedMembers`) over the entries as
79
+ * they read: whether that report is the vault's own, or forged so as to
80
+ * hide what it will do, only the vault can tell. Any member the vault
81
+ * converts is listed. A root admin's it leaves while they are one, until
82
+ * they are not; which they are, the vault's configuration says and the
83
+ * database does not, so they are listed too.
84
+ */
85
+ export declare function everyProjectPreview(db: Queryable, at: number): Promise<EveryProjectPreview[]>;
86
+ /** A service token or trust binding of 0.4, which carries no scope of its own yet. */
87
+ export type Unsealed = {
88
+ kind: 'token' | 'binding';
89
+ id: string;
90
+ principal: string;
91
+ };
92
+ /**
93
+ * The live service tokens and trust bindings of active service accounts
94
+ * that carry no scope yet, as 0.4 made them: what the app seals with each
95
+ * account's grants once a process (`sealScopes`), and what `coffre migrate`
96
+ * says it will. A credential a binding issued carries its binding's.
97
+ */
98
+ export declare function unsealedCredentials(db: Queryable, at: Date): Promise<Unsealed[]>;
99
+ /**
100
+ * The members the vault has found changed around it, and not started over
101
+ * since, as a reader without its key can tell: `reportStands`' rule, taking
102
+ * every entry in the vault's name as its own, for everyone at once, where
103
+ * the app lists members and `coffre migrate` marks them. Their newest
104
+ * `vault.tampered`, for their row (`mac`) or an older one put back
105
+ * (`stale`), is newer than the newest entry changing what they hold. Both
106
+ * reads go by the log's (author, action, seq) and (author, subject, seq)
107
+ * indexes, and findings are few.
108
+ */
109
+ export declare function tamperedMembers(db: Queryable, principal?: string): Promise<Set<string>>;
110
+ /**
111
+ * Whether the vault's report that `principal`'s row was changed around it
112
+ * stands: one of its own is newer than its newest entry changing what they
113
+ * hold, and only starting them over, or another change, settles it. While
114
+ * one stands the vault leaves their grants on every project of 0.4 as they
115
+ * are. Both entries as `check`, the vault's MACs, tells them apart,
116
+ * passing over those forged in its name; a reader without its key takes
117
+ * them all as the vault's (`tamperedMembers`). Only reports after that
118
+ * entry are read, so older ones, forged or not, never hide one. From
119
+ * LOOK_BACK forged entries on, of either kind, one may stand, so it is
120
+ * taken to.
121
+ */
122
+ export declare function reportStands(db: Queryable, principal: string, check: Authenticity): Promise<boolean>;
123
+ /** The newest access entry about `principal` that `check` finds the vault's own. */
124
+ export declare function newestAccess(db: Queryable, principal: string, check: Authenticity): Promise<Newest>;
125
+ /** Up to `limit` of the vault's allowed access entries about `principal` before `before` (the newest when undefined), newest first. */
126
+ export declare function accessEntriesAbout(db: Queryable, principal: string, limit: number, before?: bigint): Promise<StoredEntry[]>;
32
127
  //#endregion
package/dist/grants.js CHANGED
@@ -1,16 +1,17 @@
1
1
  import { tablesOf } from "./index.js";
2
- import { and, eq, gt, isNull, or } from "drizzle-orm";
2
+ import { LOOK_BACK, entryColumns, newestAuthentic } from "./log.js";
3
+ import { and, desc, eq, gt, inArray, isNull, lt, or, sql } from "drizzle-orm";
4
+ import { EVERYWHERE, administers, convertEveryProjectGrants, isInstanceRole, isScope, normalScope, unscoped } from "@coffre/core/access";
5
+ import { isTombstone } from "@coffre/core/schemas";
6
+ import { ACCESS_ACTIONS } from "@coffre/core/vault";
3
7
  //#region src/grants.ts
4
- /**
5
- * Grants, lapsed ones too unless `liveAt` says when to judge them: one
6
- * member's, or everyone's; `everyProject`, only those on every project.
7
- */
8
+ /** Grants, lapsed ones too unless `liveAt` says when to judge them: one member's, or everyone's. */
8
9
  async function readGrants(db, filter = {}) {
9
10
  const { vaultGrants, environments } = tablesOf(db);
10
11
  return (await db.select({
11
12
  grant: vaultGrants,
12
13
  environmentProjectId: environments.projectId
13
- }).from(vaultGrants).leftJoin(environments, eq(environments.id, vaultGrants.environmentId)).where(and(filter.principal === void 0 ? void 0 : eq(vaultGrants.principal, filter.principal), filter.liveAt === void 0 ? void 0 : or(isNull(vaultGrants.expiresAt), gt(vaultGrants.expiresAt, filter.liveAt)), filter.everyProject === true ? and(isNull(vaultGrants.projectId), isNull(vaultGrants.environmentId)) : void 0))).map(({ grant, environmentProjectId }) => ({
14
+ }).from(vaultGrants).leftJoin(environments, eq(environments.id, vaultGrants.environmentId)).where(and(filter.principal === void 0 ? void 0 : eq(vaultGrants.principal, filter.principal), filter.liveAt === void 0 ? void 0 : or(isNull(vaultGrants.expiresAt), gt(vaultGrants.expiresAt, filter.liveAt))))).map(({ grant, environmentProjectId }) => ({
14
15
  ...grant,
15
16
  projectId: grant.projectId ?? environmentProjectId
16
17
  }));
@@ -23,5 +24,204 @@ async function insertGrant(db, grant) {
23
24
  projectId: grant.environmentId === null ? grant.projectId : null
24
25
  });
25
26
  }
27
+ /**
28
+ * A member's instance role and its scope, as their row says: a row no vault
29
+ * of 0.5 wrote has no role, and its `owner` says whether they are an Admin.
30
+ * A role or a scope the vault would never have written reads as holding
31
+ * nothing (the vault then refuses the row by its MAC); a member's scope is
32
+ * everywhere, since a member's role reaches nothing.
33
+ */
34
+ function storedRole({ owner, role, scope }) {
35
+ const named = role === null ? owner ? "admin" : "member" : isInstanceRole(role) ? role : "member";
36
+ if (named === "member" || scope === null) return {
37
+ role: named,
38
+ scope: EVERYWHERE
39
+ };
40
+ let parsed = null;
41
+ try {
42
+ parsed = JSON.parse(scope);
43
+ } catch {}
44
+ return {
45
+ role: named,
46
+ scope: isScope(parsed) ? normalScope(parsed) : NOWHERE
47
+ };
48
+ }
49
+ /** A scope that takes in nothing: what a stored scope nobody can read means. */
50
+ const NOWHERE = {
51
+ projects: { only: [] },
52
+ environments: { only: [] }
53
+ };
54
+ /** The columns that store a member's instance role and scope: `owner` too, for a vault of 0.4 to read. */
55
+ function roleColumns(role, scope) {
56
+ const normal = normalScope(scope);
57
+ return {
58
+ owner: administers(role),
59
+ role,
60
+ scope: role === "member" || unscoped(normal) ? null : JSON.stringify(normal)
61
+ };
62
+ }
63
+ /**
64
+ * What the vault will make of the grants on every project of 0.4 that live
65
+ * members hold, by the rule it converts them with
66
+ * (`convertEveryProjectGrants`), from the rows as they are: what
67
+ * `coffre migrate` says before the vault does it. A display: the vault
68
+ * reads again, under its locks, when it converts, and checks what this
69
+ * cannot, holding no key: each row's MAC, and each entry's in its name. So
70
+ * a member it has reported changed around it is listed, and marked
71
+ * `reported`, by the vault's own rule (`tamperedMembers`) over the entries as
72
+ * they read: whether that report is the vault's own, or forged so as to
73
+ * hide what it will do, only the vault can tell. Any member the vault
74
+ * converts is listed. A root admin's it leaves while they are one, until
75
+ * they are not; which they are, the vault's configuration says and the
76
+ * database does not, so they are listed too.
77
+ */
78
+ async function everyProjectPreview(db, at) {
79
+ const { vaultMembers, projects, environments } = tablesOf(db);
80
+ const held = (await readGrants(db, { liveAt: at })).filter((grant) => grant.projectId === null && grant.environmentId === null);
81
+ if (held.length === 0) return [];
82
+ const holders = [...new Set(held.map((grant) => grant.principal))].sort();
83
+ const [members, grants, places, inside, tampered] = await Promise.all([
84
+ db.select({
85
+ principal: vaultMembers.principal,
86
+ status: vaultMembers.status,
87
+ owner: vaultMembers.owner,
88
+ role: vaultMembers.role,
89
+ scope: vaultMembers.scope
90
+ }).from(vaultMembers),
91
+ readGrants(db, { liveAt: at }),
92
+ db.select({
93
+ id: projects.id,
94
+ slug: projects.slug
95
+ }).from(projects),
96
+ db.select({
97
+ id: environments.id,
98
+ projectId: environments.projectId,
99
+ slug: environments.slug
100
+ }).from(environments),
101
+ tamperedMembers(db)
102
+ ]);
103
+ const live = places.filter((project) => !isTombstone(project.slug)).map((project) => ({
104
+ id: project.id,
105
+ environments: inside.filter((environment) => environment.projectId === project.id && !isTombstone(environment.slug)).map(({ id, slug }) => ({
106
+ id,
107
+ slug
108
+ }))
109
+ }));
110
+ const paths = Object.fromEntries(places.flatMap((project) => [[project.id, project.slug], ...inside.filter((environment) => environment.projectId === project.id).map((environment) => [environment.id, `${project.slug}/${environment.slug}`])]));
111
+ return holders.flatMap((principal) => {
112
+ const member = members.find((row) => row.principal === principal);
113
+ if (member?.status !== "active") return [];
114
+ const theirs = held.filter((grant) => grant.principal === principal);
115
+ return [{
116
+ principal,
117
+ before: theirs.map((grant) => ({
118
+ place: grant.environmentSlug === null ? "*" : `*/${grant.environmentSlug}`,
119
+ role: grant.role,
120
+ expiresAt: grant.expiresAt
121
+ })),
122
+ conversion: convertEveryProjectGrants({
123
+ person: principal.startsWith("user:"),
124
+ role: principal.startsWith("user:") ? storedRole(member).role : "member",
125
+ everyProject: theirs.map((grant) => ({
126
+ environmentSlug: grant.environmentSlug,
127
+ role: grant.role,
128
+ expiresAt: grant.expiresAt
129
+ })),
130
+ grants: grants.filter((grant) => grant.principal === principal && grant.projectId !== null).map((grant) => ({
131
+ projectId: grant.projectId,
132
+ environmentId: grant.environmentId,
133
+ role: grant.role,
134
+ expiresAt: grant.expiresAt
135
+ })),
136
+ projects: live
137
+ }),
138
+ paths,
139
+ reported: tampered.has(principal)
140
+ }];
141
+ });
142
+ }
143
+ /**
144
+ * The live service tokens and trust bindings of active service accounts
145
+ * that carry no scope yet, as 0.4 made them: what the app seals with each
146
+ * account's grants once a process (`sealScopes`), and what `coffre migrate`
147
+ * says it will. A credential a binding issued carries its binding's.
148
+ */
149
+ async function unsealedCredentials(db, at) {
150
+ const { credentials, serviceBindings, vaultMembers } = tablesOf(db);
151
+ const [tokens, bindings] = await Promise.all([db.select({
152
+ id: credentials.id,
153
+ principal: credentials.principal
154
+ }).from(credentials).innerJoin(vaultMembers, and(eq(vaultMembers.principal, credentials.principal), eq(vaultMembers.generation, credentials.generation))).where(and(eq(credentials.kind, "service"), isNull(credentials.scope), sql`${credentials.createdBy} NOT LIKE 'binding:%'`, isNull(credentials.revokedAt), gt(credentials.expiresAt, at), eq(vaultMembers.status, "active"))), db.select({
155
+ id: serviceBindings.id,
156
+ principal: serviceBindings.principal
157
+ }).from(serviceBindings).innerJoin(vaultMembers, and(eq(vaultMembers.principal, serviceBindings.principal), eq(vaultMembers.generation, serviceBindings.generation))).where(and(isNull(serviceBindings.scope), isNull(serviceBindings.revokedAt), eq(vaultMembers.status, "active")))]);
158
+ return [...tokens.map((row) => ({
159
+ kind: "token",
160
+ ...row
161
+ })), ...bindings.map((row) => ({
162
+ kind: "binding",
163
+ ...row
164
+ }))].sort((a, b) => a.principal.localeCompare(b.principal) || a.kind.localeCompare(b.kind) || a.id.localeCompare(b.id));
165
+ }
166
+ /**
167
+ * The members the vault has found changed around it, and not started over
168
+ * since, as a reader without its key can tell: `reportStands`' rule, taking
169
+ * every entry in the vault's name as its own, for everyone at once, where
170
+ * the app lists members and `coffre migrate` marks them. Their newest
171
+ * `vault.tampered`, for their row (`mac`) or an older one put back
172
+ * (`stale`), is newer than the newest entry changing what they hold. Both
173
+ * reads go by the log's (author, action, seq) and (author, subject, seq)
174
+ * indexes, and findings are few.
175
+ */
176
+ async function tamperedMembers(db, principal) {
177
+ const { auditLog } = tablesOf(db);
178
+ const newest = (actions, extra) => db.select({
179
+ principal: auditLog.subjectPrincipal,
180
+ seq: sql`max(${auditLog.seq})`.mapWith(BigInt)
181
+ }).from(auditLog).where(and(eq(auditLog.author, "vault"), inArray(auditLog.action, [...actions]), principal === void 0 ? void 0 : eq(auditLog.subjectPrincipal, principal), extra)).groupBy(auditLog.subjectPrincipal);
182
+ const found = await newest(["vault.tampered"], inArray(auditLog.code, [...TAMPER_CODES]));
183
+ if (found.length === 0) return /* @__PURE__ */ new Set();
184
+ const changed = new Map((await newest(ACCESS_ACTIONS, and(eq(auditLog.decision, "allow"), inArray(auditLog.subjectPrincipal, found.map((row) => row.principal))))).map((row) => [row.principal, row.seq]));
185
+ return new Set(found.filter((row) => row.seq > (changed.get(row.principal) ?? -1n)).map((row) => row.principal));
186
+ }
187
+ /** The codes of the vault's reports that a member's row was changed around it: their row's MAC fails (`mac`), or an older one was put back (`stale`). */
188
+ const TAMPER_CODES = ["mac", "stale"];
189
+ /**
190
+ * Whether the vault's report that `principal`'s row was changed around it
191
+ * stands: one of its own is newer than its newest entry changing what they
192
+ * hold, and only starting them over, or another change, settles it. While
193
+ * one stands the vault leaves their grants on every project of 0.4 as they
194
+ * are. Both entries as `check`, the vault's MACs, tells them apart,
195
+ * passing over those forged in its name; a reader without its key takes
196
+ * them all as the vault's (`tamperedMembers`). Only reports after that
197
+ * entry are read, so older ones, forged or not, never hide one. From
198
+ * LOOK_BACK forged entries on, of either kind, one may stand, so it is
199
+ * taken to.
200
+ */
201
+ async function reportStands(db, principal, check) {
202
+ const access = await newestAccess(db, principal, check);
203
+ if (access === "unreached") return true;
204
+ const after = typeof access === "string" ? -1n : access.seq;
205
+ const report = await newestAuthentic((before) => tamperReportsAbout(db, principal, LOOK_BACK.page, after, before), check);
206
+ return report === "unreached" || typeof report !== "string";
207
+ }
208
+ /** The newest access entry about `principal` that `check` finds the vault's own. */
209
+ function newestAccess(db, principal, check) {
210
+ return newestAuthentic((before) => accessEntriesAbout(db, principal, LOOK_BACK.page, before), check);
211
+ }
212
+ /** Up to `limit` of the vault's allowed access entries about `principal` before `before` (the newest when undefined), newest first. */
213
+ async function accessEntriesAbout(db, principal, limit, before) {
214
+ const { auditLog } = tablesOf(db);
215
+ return await db.select(entryColumns(db)).from(auditLog).where(and(eq(auditLog.author, "vault"), eq(auditLog.subjectPrincipal, principal), inArray(auditLog.action, [...ACCESS_ACTIONS]), eq(auditLog.decision, "allow"), before === void 0 ? void 0 : lt(auditLog.seq, before))).orderBy(desc(auditLog.seq)).limit(limit);
216
+ }
217
+ /**
218
+ * Up to `limit` of the vault's reports that `principal`'s row was changed
219
+ * around it (`TAMPER_CODES`), after `after` and before `before` (the newest
220
+ * when undefined), newest first.
221
+ */
222
+ async function tamperReportsAbout(db, principal, limit, after, before) {
223
+ const { auditLog } = tablesOf(db);
224
+ return await db.select(entryColumns(db)).from(auditLog).where(and(eq(auditLog.author, "vault"), eq(auditLog.subjectPrincipal, principal), eq(auditLog.action, "vault.tampered"), inArray(auditLog.code, [...TAMPER_CODES]), gt(auditLog.seq, after), before === void 0 ? void 0 : lt(auditLog.seq, before))).orderBy(desc(auditLog.seq)).limit(limit);
225
+ }
26
226
  //#endregion
27
- export { insertGrant, readGrants };
227
+ export { accessEntriesAbout, everyProjectPreview, insertGrant, newestAccess, readGrants, reportStands, roleColumns, storedRole, tamperedMembers, unsealedCredentials };
@@ -1,4 +1,4 @@
1
- import { n as PoolLike } from "./database-edkMkG02.js";
1
+ import { n as PoolLike } from "./database-C63WjCrC.js";
2
2
  import pg from "pg";
3
3
  //#region src/hyperdrive.d.ts
4
4
  /** Postgres through a Hyperdrive binding: `postgres(env.HYPERDRIVE)`. */
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- import { m as schema_d_exports } from "./schema-BqCB3XFm.js";
2
- import { a as Transaction, c as tablesOf, i as Queryable, n as PoolLike, o as createDatabase, r as QueryOutsideTransaction, s as own, t as Database } from "./database-edkMkG02.js";
1
+ import { m as schema_d_exports } from "./schema-a7PkFgea.js";
2
+ import { a as Transaction, c as tablesOf, i as Queryable, n as PoolLike, o as createDatabase, r as QueryOutsideTransaction, s as own, t as Database } from "./database-C63WjCrC.js";
3
3
  export { Database, PoolLike, QueryOutsideTransaction, Queryable, Transaction, createDatabase, own, schema_d_exports as schema, tablesOf };