@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.
- package/dist/connect.d.ts +1 -1
- package/dist/{database-edkMkG02.d.ts → database-C63WjCrC.d.ts} +1 -1
- package/dist/dialect.d.ts +7 -1
- package/dist/dialect.js +12 -1
- package/dist/grants.d.ts +105 -10
- package/dist/grants.js +207 -7
- package/dist/hyperdrive.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/log.d.ts +392 -2
- package/dist/log.js +53 -1
- package/dist/migrations/postgres/0002_instance_roles.sql +4 -0
- package/dist/migrations/postgres/0003_credential_scopes.sql +4 -0
- package/dist/migrations/postgres/meta/0002_snapshot.json +2860 -0
- package/dist/migrations/postgres/meta/0003_snapshot.json +2872 -0
- package/dist/migrations/postgres/meta/_journal.json +14 -0
- package/dist/migrations/sqlite/0002_instance_roles.sql +2 -0
- package/dist/migrations/sqlite/0003_credential_scopes.sql +2 -0
- package/dist/migrations/sqlite/meta/0002_snapshot.json +2594 -0
- package/dist/migrations/sqlite/meta/0003_snapshot.json +2608 -0
- package/dist/migrations/sqlite/meta/_journal.json +14 -0
- package/dist/portable.d.ts +2 -2
- package/dist/{schema-BqCB3XFm.d.ts → schema-a7PkFgea.d.ts} +68 -0
- package/dist/schema-sqlite.d.ts +76 -0
- package/dist/schema-sqlite.js +4 -0
- package/dist/schema-version.d.ts +1 -1
- package/dist/schema-version.js +60 -26
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +4 -0
- package/package.json +2 -2
- package/src/dialect.ts +14 -0
- package/src/grants.ts +272 -11
- package/src/log.ts +66 -2
- package/src/migrations/postgres/0002_instance_roles.sql +4 -0
- package/src/migrations/postgres/0003_credential_scopes.sql +4 -0
- package/src/migrations/postgres/meta/0002_snapshot.json +2860 -0
- package/src/migrations/postgres/meta/0003_snapshot.json +2872 -0
- package/src/migrations/postgres/meta/_journal.json +14 -0
- package/src/migrations/sqlite/0002_instance_roles.sql +2 -0
- package/src/migrations/sqlite/0003_credential_scopes.sql +2 -0
- package/src/migrations/sqlite/meta/0002_snapshot.json +2594 -0
- package/src/migrations/sqlite/meta/0003_snapshot.json +2608 -0
- package/src/migrations/sqlite/meta/_journal.json +14 -0
- package/src/schema.sqlite.ts +6 -0
- package/src/schema.ts +11 -1
package/dist/connect.d.ts
CHANGED
package/dist/dialect.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as Queryable } from "./database-
|
|
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-
|
|
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)
|
|
7
|
-
*
|
|
8
|
-
* them for
|
|
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 {
|
|
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))
|
|
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 };
|
package/dist/hyperdrive.d.ts
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { m as schema_d_exports } from "./schema-
|
|
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-
|
|
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 };
|