@coffre/db 0.9.0 → 0.10.1
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/connect.js +1 -1
- package/dist/database-BadNeeN-.js +146 -0
- package/dist/{database-hd1hAll-.d.ts → database-DBtn3jqp.d.ts} +27 -3
- package/dist/dialect.d.ts +1 -1
- package/dist/grants.d.ts +2 -2
- package/dist/grants.js +26 -10
- package/dist/hyperdrive.d.ts +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -89
- package/dist/log.d.ts +26 -6
- package/dist/log.js +86 -21
- package/dist/migrations/postgres/0012_sensitive_values.sql +18 -0
- package/dist/migrations/postgres/0013_contributor_role.sql +5 -0
- package/dist/migrations/postgres/0014_custom_roles.sql +4 -0
- package/dist/migrations/postgres/meta/0012_snapshot.json +3004 -0
- package/dist/migrations/postgres/meta/0013_snapshot.json +3004 -0
- package/dist/migrations/postgres/meta/0014_snapshot.json +3000 -0
- package/dist/migrations/postgres/meta/_journal.json +21 -0
- package/dist/migrations/sqlite/0012_sensitive_values.sql +15 -0
- package/dist/migrations/sqlite/0013_contributor_role.sql +28 -0
- package/dist/migrations/sqlite/0014_custom_roles.sql +27 -0
- package/dist/migrations/sqlite/meta/0012_snapshot.json +2726 -0
- package/dist/migrations/sqlite/meta/0013_snapshot.json +2726 -0
- package/dist/migrations/sqlite/meta/0014_snapshot.json +2722 -0
- package/dist/migrations/sqlite/meta/_journal.json +21 -0
- package/dist/portable.d.ts +2 -2
- package/dist/{schema-CCYKTG-E.d.ts → schema-NBVA9q-s.d.ts} +17 -0
- package/dist/schema-sqlite.d.ts +17 -0
- package/dist/schema-sqlite.js +3 -6
- package/dist/schema-version.d.ts +1 -1
- package/dist/schema-version.js +42 -0
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +3 -7
- package/package.json +2 -2
- package/src/database.ts +35 -3
- package/src/grants.ts +32 -18
- package/src/log.ts +90 -34
- package/src/migrations/postgres/0012_sensitive_values.sql +18 -0
- package/src/migrations/postgres/0013_contributor_role.sql +5 -0
- package/src/migrations/postgres/0014_custom_roles.sql +4 -0
- package/src/migrations/postgres/meta/0012_snapshot.json +3004 -0
- package/src/migrations/postgres/meta/0013_snapshot.json +3004 -0
- package/src/migrations/postgres/meta/0014_snapshot.json +3000 -0
- package/src/migrations/postgres/meta/_journal.json +21 -0
- package/src/migrations/sqlite/0012_sensitive_values.sql +15 -0
- package/src/migrations/sqlite/0013_contributor_role.sql +28 -0
- package/src/migrations/sqlite/0014_custom_roles.sql +27 -0
- package/src/migrations/sqlite/meta/0012_snapshot.json +2726 -0
- package/src/migrations/sqlite/meta/0013_snapshot.json +2726 -0
- package/src/migrations/sqlite/meta/0014_snapshot.json +2722 -0
- package/src/migrations/sqlite/meta/_journal.json +21 -0
- package/src/prepared.ts +60 -0
- package/src/schema.sqlite.ts +8 -9
- package/src/schema.ts +8 -10
package/dist/connect.d.ts
CHANGED
package/dist/connect.js
CHANGED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { n as trackCommits } from "./commits-BuW-NL3n.js";
|
|
2
|
+
import { t as schema_exports } from "./schema.js";
|
|
3
|
+
import { engineOf } from "./dialect.js";
|
|
4
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
|
+
import { Table, createTableRelationsHelpers, extractTablesRelationalConfig, getTableName, is } from "drizzle-orm";
|
|
6
|
+
import { NodePgDatabase, NodePgDriver } from "drizzle-orm/node-postgres";
|
|
7
|
+
import { PgDialect } from "drizzle-orm/pg-core";
|
|
8
|
+
//#region src/prepared.ts
|
|
9
|
+
/**
|
|
10
|
+
* A select built once per process, or once per isolate on Workers, and run
|
|
11
|
+
* with new values each time. Drizzle rebuilds a query's SQL from its
|
|
12
|
+
* builder at every run, and that was most of a short query's CPU; this
|
|
13
|
+
* builds it from `build` the first time a database of its dialect asks,
|
|
14
|
+
* then hands the SQL and its columns to that call's own session
|
|
15
|
+
* (`session.prepareQuery`), which binds `values` to the placeholders
|
|
16
|
+
* (`sql.placeholder`) and maps the rows as the builder would.
|
|
17
|
+
*
|
|
18
|
+
* `build` must not depend on anything but the database's dialect, and the
|
|
19
|
+
* values go to the driver as they are, not through a column's encoding:
|
|
20
|
+
* strings, numbers, bigints and bytes.
|
|
21
|
+
*
|
|
22
|
+
* const member = prepared((db) => db.select().from(tablesOf(db).vaultMembers).where(eq(…, sql.placeholder('principal'))));
|
|
23
|
+
* const [row] = await member(db, { principal });
|
|
24
|
+
*/
|
|
25
|
+
function prepared(build) {
|
|
26
|
+
const builds = /* @__PURE__ */ new WeakMap();
|
|
27
|
+
return (db, values = {}) => {
|
|
28
|
+
const tables = tablesOf(db);
|
|
29
|
+
let built = builds.get(tables);
|
|
30
|
+
if (built === void 0) {
|
|
31
|
+
const builder = build(db);
|
|
32
|
+
const { fields, joinsNotNullableMap } = builder.prepare();
|
|
33
|
+
built = {
|
|
34
|
+
query: builder.toSQL(),
|
|
35
|
+
fields,
|
|
36
|
+
joinsNotNullableMap,
|
|
37
|
+
engine: engineOf(db)
|
|
38
|
+
};
|
|
39
|
+
builds.set(tables, built);
|
|
40
|
+
}
|
|
41
|
+
const session = db.session;
|
|
42
|
+
const query = built.engine === "sqlite" ? session.prepareQuery(built.query, built.fields, "all", true) : session.prepareQuery(built.query, built.fields, void 0, true);
|
|
43
|
+
query.joinsNotNullableMap = built.joinsNotNullableMap;
|
|
44
|
+
return built.engine === "sqlite" ? query.all(values) : query.execute(values);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/database.ts
|
|
49
|
+
/**
|
|
50
|
+
* A query made on the database from inside one of its own transactions,
|
|
51
|
+
* rather than through the transaction. With one connection per call, as on
|
|
52
|
+
* Workers, it would wait for the transaction, which waits for it: a call
|
|
53
|
+
* Cloudflare cancels as hung, and nothing says why. So it is refused at
|
|
54
|
+
* once, with this, wherever the database runs.
|
|
55
|
+
*/
|
|
56
|
+
var QueryOutsideTransaction = class extends Error {
|
|
57
|
+
constructor() {
|
|
58
|
+
super("query outside its transaction: use tx. A query on the database inside one of its own transactions waits for that transaction, which waits for it");
|
|
59
|
+
this.name = "QueryOutsideTransaction";
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
/** The transaction a flow of work runs inside, with the pool it holds, while it is open. */
|
|
63
|
+
const inside = new AsyncLocalStorage();
|
|
64
|
+
/**
|
|
65
|
+
* `pool`, refusing a query or a transaction asked from inside a transaction
|
|
66
|
+
* it is already running. Work from any other flow, a concurrent request's,
|
|
67
|
+
* or what the transaction left running once its work was done, goes on as before.
|
|
68
|
+
*/
|
|
69
|
+
function guarded(pool) {
|
|
70
|
+
return new Proxy(pool, { get(target, key) {
|
|
71
|
+
const value = Reflect.get(target, key, target);
|
|
72
|
+
if (typeof value !== "function") return value;
|
|
73
|
+
if (key !== "query" && key !== "connect") return value.bind(target);
|
|
74
|
+
return (...args) => {
|
|
75
|
+
const open = inside.getStore();
|
|
76
|
+
if (open?.open === true && open.pool === target) throw new QueryOutsideTransaction();
|
|
77
|
+
return value.apply(target, args);
|
|
78
|
+
};
|
|
79
|
+
} });
|
|
80
|
+
}
|
|
81
|
+
/** The refusal `error` is, or was caused by. */
|
|
82
|
+
function brokenRule(error) {
|
|
83
|
+
for (let cause = error; cause instanceof Error; cause = cause.cause) if (cause instanceof QueryOutsideTransaction) return cause;
|
|
84
|
+
}
|
|
85
|
+
let relational;
|
|
86
|
+
function relationalSchema() {
|
|
87
|
+
if (relational === void 0) {
|
|
88
|
+
const { tables, tableNamesMap } = extractTablesRelationalConfig(schema_exports, createTableRelationsHelpers);
|
|
89
|
+
relational = {
|
|
90
|
+
fullSchema: schema_exports,
|
|
91
|
+
schema: tables,
|
|
92
|
+
tableNamesMap
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return relational;
|
|
96
|
+
}
|
|
97
|
+
function createDatabase(pool) {
|
|
98
|
+
const dialect = new PgDialect();
|
|
99
|
+
const config = relationalSchema();
|
|
100
|
+
const db = new NodePgDatabase(dialect, new NodePgDriver(guarded(pool), dialect).createSession(config), config);
|
|
101
|
+
const run = db.transaction.bind(db);
|
|
102
|
+
db.transaction = (async (work, config) => {
|
|
103
|
+
const open = {
|
|
104
|
+
pool,
|
|
105
|
+
open: true
|
|
106
|
+
};
|
|
107
|
+
try {
|
|
108
|
+
return await run((tx) => inside.run(open, async () => {
|
|
109
|
+
try {
|
|
110
|
+
return await work(tx);
|
|
111
|
+
} finally {
|
|
112
|
+
open.open = false;
|
|
113
|
+
}
|
|
114
|
+
}), config);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
throw brokenRule(error) ?? error;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
return trackCommits(db);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The tables of the database's own dialect: the Postgres ones on Postgres,
|
|
123
|
+
* the SQLite ones on SQLite. Every query builds on these.
|
|
124
|
+
*/
|
|
125
|
+
function tablesOf(db) {
|
|
126
|
+
return db._.fullSchema;
|
|
127
|
+
}
|
|
128
|
+
const byName = /* @__PURE__ */ new WeakMap();
|
|
129
|
+
/**
|
|
130
|
+
* The database's own twin of `table`, which may be another dialect's: the
|
|
131
|
+
* server names tables by importing schema.ts, and the database decides
|
|
132
|
+
* which one that means.
|
|
133
|
+
*/
|
|
134
|
+
function own(db, table) {
|
|
135
|
+
const tables = tablesOf(db);
|
|
136
|
+
let named = byName.get(tables);
|
|
137
|
+
if (named === void 0) {
|
|
138
|
+
named = new Map(Object.values(tables).filter((value) => is(value, Table)).map((value) => [getTableName(value), value]));
|
|
139
|
+
byName.set(tables, named);
|
|
140
|
+
}
|
|
141
|
+
const found = named.get(getTableName(table));
|
|
142
|
+
if (found === void 0) throw new Error(`no table ${getTableName(table)} in this database`);
|
|
143
|
+
return found;
|
|
144
|
+
}
|
|
145
|
+
//#endregion
|
|
146
|
+
export { prepared as a, tablesOf as i, createDatabase as n, own as r, QueryOutsideTransaction as t };
|
|
@@ -1,7 +1,31 @@
|
|
|
1
|
-
import { m as schema_d_exports } from "./schema-
|
|
2
|
-
import { Table } from "drizzle-orm";
|
|
1
|
+
import { m as schema_d_exports } from "./schema-NBVA9q-s.js";
|
|
2
|
+
import { Query, Table } from "drizzle-orm";
|
|
3
3
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
4
4
|
import pg from "pg";
|
|
5
|
+
//#region src/prepared.d.ts
|
|
6
|
+
/** The part of a select builder `prepared` reads, once. */
|
|
7
|
+
type Select = PromiseLike<unknown> & {
|
|
8
|
+
toSQL(): Query;
|
|
9
|
+
prepare(...name: never[]): unknown;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* A select built once per process, or once per isolate on Workers, and run
|
|
13
|
+
* with new values each time. Drizzle rebuilds a query's SQL from its
|
|
14
|
+
* builder at every run, and that was most of a short query's CPU; this
|
|
15
|
+
* builds it from `build` the first time a database of its dialect asks,
|
|
16
|
+
* then hands the SQL and its columns to that call's own session
|
|
17
|
+
* (`session.prepareQuery`), which binds `values` to the placeholders
|
|
18
|
+
* (`sql.placeholder`) and maps the rows as the builder would.
|
|
19
|
+
*
|
|
20
|
+
* `build` must not depend on anything but the database's dialect, and the
|
|
21
|
+
* values go to the driver as they are, not through a column's encoding:
|
|
22
|
+
* strings, numbers, bigints and bytes.
|
|
23
|
+
*
|
|
24
|
+
* const member = prepared((db) => db.select().from(tablesOf(db).vaultMembers).where(eq(…, sql.placeholder('principal'))));
|
|
25
|
+
* const [row] = await member(db, { principal });
|
|
26
|
+
*/
|
|
27
|
+
declare function prepared<Q extends Select>(build: (db: Queryable) => Q): (db: Queryable, values?: Record<string, unknown>) => Promise<Awaited<Q>>;
|
|
28
|
+
//#endregion
|
|
5
29
|
//#region src/database.d.ts
|
|
6
30
|
/**
|
|
7
31
|
* A database, typed as Postgres whichever it is; see portable.ts for why
|
|
@@ -46,4 +70,4 @@ declare function tablesOf(db: Queryable): typeof schema_d_exports;
|
|
|
46
70
|
*/
|
|
47
71
|
declare function own<T extends Table>(db: Queryable, table: T): T;
|
|
48
72
|
//#endregion
|
|
49
|
-
export { Transaction as a, tablesOf as c, Queryable as i, PoolLike as n, createDatabase as o, QueryOutsideTransaction as r, own as s, Database as t };
|
|
73
|
+
export { Transaction as a, tablesOf as c, Queryable as i, prepared as l, PoolLike as n, createDatabase as o, QueryOutsideTransaction as r, own as s, Database as t };
|
package/dist/dialect.d.ts
CHANGED
package/dist/grants.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { i as Queryable } from "./database-
|
|
1
|
+
import { i as Queryable } from "./database-DBtn3jqp.js";
|
|
2
2
|
import { Authenticity, Newest } from "./log.js";
|
|
3
|
-
import { Conversion, ConversionInput, InstanceRole, Scope } from "@coffre/core/access";
|
|
4
3
|
import { StoredEntry } from "@coffre/core/audit";
|
|
4
|
+
import { Conversion, ConversionInput, InstanceRole, Scope } from "@coffre/core/access";
|
|
5
5
|
//#region src/grants.d.ts
|
|
6
6
|
/**
|
|
7
7
|
* One grant, as `vault_grants` holds it: on a project (`environmentId`
|
package/dist/grants.js
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
|
-
import { tablesOf } from "./index.js";
|
|
2
1
|
import { tombstone, truth } from "./dialect.js";
|
|
3
|
-
import {
|
|
2
|
+
import { a as prepared, i as tablesOf } from "./database-BadNeeN-.js";
|
|
3
|
+
import { entryColumns, newestAuthentic } from "./log.js";
|
|
4
4
|
import { and, asc, desc, eq, gt, inArray, isNull, lt, or, sql } from "drizzle-orm";
|
|
5
5
|
import { EVERYWHERE, administers, convertEveryProjectGrants, isInstanceRole, isScope, normalScope, unscoped } from "@coffre/core/access";
|
|
6
6
|
import { ACCESS_ACTIONS } from "@coffre/core/vault";
|
|
7
7
|
//#region src/grants.ts
|
|
8
8
|
/** Grants, lapsed ones too unless `liveAt` says when to judge them: one member's, or everyone's. */
|
|
9
9
|
async function readGrants(db, filter = {}) {
|
|
10
|
-
|
|
11
|
-
return (await db.select({
|
|
12
|
-
grant: vaultGrants,
|
|
13
|
-
environmentProjectId: environments.projectId
|
|
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 }) => ({
|
|
10
|
+
return (filter.principal !== void 0 && filter.liveAt === void 0 ? await grantsOf(db, { principal: filter.principal }) : await grantRows(db, filter.principal === void 0 ? void 0 : eq(tablesOf(db).vaultGrants.principal, filter.principal), filter.liveAt)).map(({ grant, environmentProjectId }) => ({
|
|
15
11
|
...grant,
|
|
16
12
|
projectId: grant.projectId ?? environmentProjectId
|
|
17
13
|
}));
|
|
18
14
|
}
|
|
15
|
+
function grantRows(db, principal, liveAt) {
|
|
16
|
+
const { vaultGrants, environments } = tablesOf(db);
|
|
17
|
+
return db.select({
|
|
18
|
+
grant: vaultGrants,
|
|
19
|
+
environmentProjectId: environments.projectId
|
|
20
|
+
}).from(vaultGrants).leftJoin(environments, eq(environments.id, vaultGrants.environmentId)).where(and(principal, liveAt === void 0 ? void 0 : or(isNull(vaultGrants.expiresAt), gt(vaultGrants.expiresAt, liveAt))));
|
|
21
|
+
}
|
|
22
|
+
const grantsOf = prepared((db) => grantRows(db, eq(tablesOf(db).vaultGrants.principal, sql.placeholder("principal"))));
|
|
19
23
|
/** Add a grant; an environment's row names only the environment. */
|
|
20
24
|
async function insertGrant(db, grant) {
|
|
21
25
|
const { vaultGrants } = tablesOf(db);
|
|
@@ -226,18 +230,30 @@ async function reportStands(db, principal, check) {
|
|
|
226
230
|
const access = await newestAccess(db, principal, check);
|
|
227
231
|
if (access === "unreached") return true;
|
|
228
232
|
const after = typeof access === "string" ? -1n : access.seq;
|
|
229
|
-
const report = await newestAuthentic((before) => tamperReportsAbout(db, principal,
|
|
233
|
+
const report = await newestAuthentic((limit, before) => tamperReportsAbout(db, principal, limit, after, before), check);
|
|
230
234
|
return report === "unreached" || typeof report !== "string";
|
|
231
235
|
}
|
|
232
236
|
/** The newest access entry about `principal` that `check` finds the vault's own. */
|
|
233
237
|
function newestAccess(db, principal, check) {
|
|
234
|
-
return newestAuthentic((before) => accessEntriesAbout(db, principal,
|
|
238
|
+
return newestAuthentic((limit, before) => accessEntriesAbout(db, principal, limit, before), check);
|
|
235
239
|
}
|
|
236
240
|
/** Up to `limit` of the vault's allowed access entries about `principal` before `before` (the newest when undefined), newest first. */
|
|
237
241
|
async function accessEntriesAbout(db, principal, limit, before) {
|
|
242
|
+
return before === void 0 ? await newestAccessEntries(db, {
|
|
243
|
+
principal,
|
|
244
|
+
limit
|
|
245
|
+
}) : await accessEntriesBefore(db, {
|
|
246
|
+
principal,
|
|
247
|
+
limit,
|
|
248
|
+
before
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
function accessEntries(db, before) {
|
|
238
252
|
const { auditLog } = tablesOf(db);
|
|
239
|
-
return
|
|
253
|
+
return db.select(entryColumns(db)).from(auditLog).where(and(eq(auditLog.author, "vault"), eq(auditLog.subjectPrincipal, sql.placeholder("principal")), inArray(auditLog.action, [...ACCESS_ACTIONS]), eq(auditLog.decision, "allow"), before)).orderBy(desc(auditLog.seq)).limit(sql.placeholder("limit"));
|
|
240
254
|
}
|
|
255
|
+
const newestAccessEntries = prepared((db) => accessEntries(db));
|
|
256
|
+
const accessEntriesBefore = prepared((db) => accessEntries(db, lt(tablesOf(db).auditLog.seq, sql.placeholder("before"))));
|
|
241
257
|
/**
|
|
242
258
|
* Up to `limit` of the vault's reports that `principal`'s row was changed
|
|
243
259
|
* around it (`TAMPER_CODES`), after `after` and before `before` (the newest
|
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-
|
|
3
|
-
export { Database, PoolLike, QueryOutsideTransaction, Queryable, Transaction, createDatabase, own, schema_d_exports as schema, tablesOf };
|
|
1
|
+
import { m as schema_d_exports } from "./schema-NBVA9q-s.js";
|
|
2
|
+
import { a as Transaction, c as tablesOf, i as Queryable, l as prepared, n as PoolLike, o as createDatabase, r as QueryOutsideTransaction, s as own, t as Database } from "./database-DBtn3jqp.js";
|
|
3
|
+
export { Database, PoolLike, QueryOutsideTransaction, Queryable, Transaction, createDatabase, own, prepared, schema_d_exports as schema, tablesOf };
|
package/dist/index.js
CHANGED
|
@@ -1,90 +1,3 @@
|
|
|
1
|
-
import { n as trackCommits } from "./commits-BuW-NL3n.js";
|
|
2
1
|
import { t as schema_exports } from "./schema.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import { drizzle } from "drizzle-orm/node-postgres";
|
|
6
|
-
//#region src/database.ts
|
|
7
|
-
/**
|
|
8
|
-
* A query made on the database from inside one of its own transactions,
|
|
9
|
-
* rather than through the transaction. With one connection per call, as on
|
|
10
|
-
* Workers, it would wait for the transaction, which waits for it: a call
|
|
11
|
-
* Cloudflare cancels as hung, and nothing says why. So it is refused at
|
|
12
|
-
* once, with this, wherever the database runs.
|
|
13
|
-
*/
|
|
14
|
-
var QueryOutsideTransaction = class extends Error {
|
|
15
|
-
constructor() {
|
|
16
|
-
super("query outside its transaction: use tx. A query on the database inside one of its own transactions waits for that transaction, which waits for it");
|
|
17
|
-
this.name = "QueryOutsideTransaction";
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
/** The transaction a flow of work runs inside, with the pool it holds, while it is open. */
|
|
21
|
-
const inside = new AsyncLocalStorage();
|
|
22
|
-
/**
|
|
23
|
-
* `pool`, refusing a query or a transaction asked from inside a transaction
|
|
24
|
-
* it is already running. Work from any other flow, a concurrent request's,
|
|
25
|
-
* or what the transaction left running once its work was done, goes on as before.
|
|
26
|
-
*/
|
|
27
|
-
function guarded(pool) {
|
|
28
|
-
return new Proxy(pool, { get(target, key) {
|
|
29
|
-
const value = Reflect.get(target, key, target);
|
|
30
|
-
if (typeof value !== "function") return value;
|
|
31
|
-
if (key !== "query" && key !== "connect") return value.bind(target);
|
|
32
|
-
return (...args) => {
|
|
33
|
-
const open = inside.getStore();
|
|
34
|
-
if (open?.open === true && open.pool === target) throw new QueryOutsideTransaction();
|
|
35
|
-
return value.apply(target, args);
|
|
36
|
-
};
|
|
37
|
-
} });
|
|
38
|
-
}
|
|
39
|
-
/** The refusal `error` is, or was caused by. */
|
|
40
|
-
function brokenRule(error) {
|
|
41
|
-
for (let cause = error; cause instanceof Error; cause = cause.cause) if (cause instanceof QueryOutsideTransaction) return cause;
|
|
42
|
-
}
|
|
43
|
-
function createDatabase(pool) {
|
|
44
|
-
const db = drizzle(guarded(pool), { schema: schema_exports });
|
|
45
|
-
const run = db.transaction.bind(db);
|
|
46
|
-
db.transaction = (async (work, config) => {
|
|
47
|
-
const open = {
|
|
48
|
-
pool,
|
|
49
|
-
open: true
|
|
50
|
-
};
|
|
51
|
-
try {
|
|
52
|
-
return await run((tx) => inside.run(open, async () => {
|
|
53
|
-
try {
|
|
54
|
-
return await work(tx);
|
|
55
|
-
} finally {
|
|
56
|
-
open.open = false;
|
|
57
|
-
}
|
|
58
|
-
}), config);
|
|
59
|
-
} catch (error) {
|
|
60
|
-
throw brokenRule(error) ?? error;
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
return trackCommits(db);
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* The tables of the database's own dialect: the Postgres ones on Postgres,
|
|
67
|
-
* the SQLite ones on SQLite. Every query builds on these.
|
|
68
|
-
*/
|
|
69
|
-
function tablesOf(db) {
|
|
70
|
-
return db._.fullSchema;
|
|
71
|
-
}
|
|
72
|
-
const byName = /* @__PURE__ */ new WeakMap();
|
|
73
|
-
/**
|
|
74
|
-
* The database's own twin of `table`, which may be another dialect's: the
|
|
75
|
-
* server names tables by importing schema.ts, and the database decides
|
|
76
|
-
* which one that means.
|
|
77
|
-
*/
|
|
78
|
-
function own(db, table) {
|
|
79
|
-
const tables = tablesOf(db);
|
|
80
|
-
let named = byName.get(tables);
|
|
81
|
-
if (named === void 0) {
|
|
82
|
-
named = new Map(Object.values(tables).filter((value) => is(value, Table)).map((value) => [getTableName(value), value]));
|
|
83
|
-
byName.set(tables, named);
|
|
84
|
-
}
|
|
85
|
-
const found = named.get(getTableName(table));
|
|
86
|
-
if (found === void 0) throw new Error(`no table ${getTableName(table)} in this database`);
|
|
87
|
-
return found;
|
|
88
|
-
}
|
|
89
|
-
//#endregion
|
|
90
|
-
export { QueryOutsideTransaction, createDatabase, own, schema_exports as schema, tablesOf };
|
|
2
|
+
import { a as prepared, i as tablesOf, n as createDatabase, r as own, t as QueryOutsideTransaction } from "./database-BadNeeN-.js";
|
|
3
|
+
export { QueryOutsideTransaction, createDatabase, own, prepared, schema_exports as schema, tablesOf };
|
package/dist/log.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as Transaction, i as Queryable } from "./database-
|
|
1
|
+
import { a as Transaction, i as Queryable } from "./database-DBtn3jqp.js";
|
|
2
2
|
import { SQL } from "drizzle-orm";
|
|
3
3
|
import { LogKey, StoredEntry } from "@coffre/core/audit";
|
|
4
4
|
//#region src/log.d.ts
|
|
@@ -462,6 +462,19 @@ export declare function withinBytes(db: Queryable, where: SQL | undefined, limit
|
|
|
462
462
|
cut: boolean;
|
|
463
463
|
count: number;
|
|
464
464
|
} | null>;
|
|
465
|
+
/**
|
|
466
|
+
* The first entry missing from the log up to `seq`, and the one found in
|
|
467
|
+
* its place, or null when every one is there. Seqs are the primary key, so
|
|
468
|
+
* a stretch is whole exactly when its count is its length: one pass over
|
|
469
|
+
* the key's index, never the entries. Only a log with a gap pays for more,
|
|
470
|
+
* a count per halving to find where.
|
|
471
|
+
*
|
|
472
|
+
* entries 0..9, 4 cut: { expected: 4n, found: 5n }
|
|
473
|
+
*/
|
|
474
|
+
export declare function missingEntry(db: Queryable, seq: bigint): Promise<{
|
|
475
|
+
expected: bigint;
|
|
476
|
+
found: bigint | null;
|
|
477
|
+
} | null>;
|
|
465
478
|
/**
|
|
466
479
|
* How a reader tells the vault's own entries from those forged in its name:
|
|
467
480
|
* by their MAC, which only the vault holds the key to check. Each it finds
|
|
@@ -473,6 +486,12 @@ export type Authenticity = {
|
|
|
473
486
|
};
|
|
474
487
|
/** Up to `limit` of the entries in the vault's name, allowed, of one `action`, before `before` (the newest when undefined), newest first. */
|
|
475
488
|
export declare function newestVaultEntries(db: Queryable, action: string, limit: number, before?: bigint): Promise<StoredEntry[]>;
|
|
489
|
+
/**
|
|
490
|
+
* The number of the newest entry in the vault's name, allowed, of one
|
|
491
|
+
* `action`, or null for none: whether one was appended since, and nothing
|
|
492
|
+
* of what it says, so its MAC is not checked. One indexed read.
|
|
493
|
+
*/
|
|
494
|
+
export declare function newestVaultSeq(db: Queryable, action: string): Promise<bigint | null>;
|
|
476
495
|
/** How many entries in its name the vault reads back, a page at a time, for its own newest of a kind (`newestAuthentic`). */
|
|
477
496
|
export declare const LOOK_BACK: {
|
|
478
497
|
page: number;
|
|
@@ -482,10 +501,11 @@ export declare const LOOK_BACK: {
|
|
|
482
501
|
export type Newest = StoredEntry | 'none' | 'forged' | 'unreached';
|
|
483
502
|
/**
|
|
484
503
|
* The newest of the entries `page` reads, newest first and before a seq,
|
|
485
|
-
* that `check` finds authentic
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
504
|
+
* that `check` finds authentic: the newest alone first, which it is unless
|
|
505
|
+
* entries were forged in the vault's name since, then back a page at a
|
|
506
|
+
* time. 'none' when there are none at all, 'forged' when every one is in
|
|
507
|
+
* the vault's name but not its own, and 'unreached' once it has read
|
|
508
|
+
* LOOK_BACK such entries (1024), whether or not more are left.
|
|
489
509
|
*/
|
|
490
|
-
export declare function newestAuthentic(page: (before: bigint | undefined) => Promise<StoredEntry[]>, check: Authenticity): Promise<Newest>;
|
|
510
|
+
export declare function newestAuthentic(page: (limit: number, before: bigint | undefined) => Promise<StoredEntry[]>, check: Authenticity): Promise<Newest>;
|
|
491
511
|
//#endregion
|
package/dist/log.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as afterCommit } from "./commits-BuW-NL3n.js";
|
|
2
|
-
import { tablesOf } from "./index.js";
|
|
3
2
|
import { clockMillis, forUpdate } from "./dialect.js";
|
|
4
|
-
import {
|
|
3
|
+
import { a as prepared, i as tablesOf } from "./database-BadNeeN-.js";
|
|
4
|
+
import { and, asc, count, desc, eq, gt, gte, lt, lte, sql } from "drizzle-orm";
|
|
5
5
|
import { GENESIS_HASH, sealEntry } from "@coffre/core/audit";
|
|
6
6
|
//#region src/log.ts
|
|
7
7
|
/** The log's head does not name its last entry: something moved it, or cut the log behind it. */
|
|
@@ -52,14 +52,17 @@ function forgetLogHeads() {
|
|
|
52
52
|
* again in the same transaction is free.
|
|
53
53
|
*/
|
|
54
54
|
async function lockLogHead(tx) {
|
|
55
|
-
const
|
|
56
|
-
const [head] = await forUpdate(tx, tx.select({
|
|
57
|
-
nextSeq: auditChainHead.nextSeq,
|
|
58
|
-
headHash: auditChainHead.headHash
|
|
59
|
-
}).from(auditChainHead).where(eq(auditChainHead.onlyRow, true)));
|
|
55
|
+
const [head] = await lockedHead(tx);
|
|
60
56
|
if (head === void 0) throw new Error("audit_chain_head is missing; refusing to write an unchained entry");
|
|
61
57
|
return head;
|
|
62
58
|
}
|
|
59
|
+
const lockedHead = prepared((db) => {
|
|
60
|
+
const { auditChainHead } = tablesOf(db);
|
|
61
|
+
return forUpdate(db, db.select({
|
|
62
|
+
nextSeq: auditChainHead.nextSeq,
|
|
63
|
+
headHash: auditChainHead.headHash
|
|
64
|
+
}).from(auditChainHead).where(eq(auditChainHead.onlyRow, true)));
|
|
65
|
+
});
|
|
63
66
|
/**
|
|
64
67
|
* Append `entries` under `key`. `check`, when given, runs once the head is
|
|
65
68
|
* locked and before anything is written: a writer's own condition on the
|
|
@@ -71,11 +74,8 @@ async function appendEntries(tx, key, entries, check) {
|
|
|
71
74
|
const { auditChainHead, auditLog } = tablesOf(tx);
|
|
72
75
|
const head = await lockLogHead(tx);
|
|
73
76
|
await check?.(tx);
|
|
74
|
-
const [{ now }] = await
|
|
75
|
-
const [last] = await tx
|
|
76
|
-
seq: auditLog.seq,
|
|
77
|
-
hash: auditLog.hash
|
|
78
|
-
}).from(auditLog).orderBy(desc(auditLog.seq)).limit(1);
|
|
77
|
+
const [{ now }] = await clockNow(tx);
|
|
78
|
+
const [last] = await lastEntry(tx);
|
|
79
79
|
const expected = last === void 0 ? {
|
|
80
80
|
nextSeq: 0n,
|
|
81
81
|
headHash: GENESIS_HASH
|
|
@@ -136,6 +136,14 @@ async function appendEntries(tx, key, entries, check) {
|
|
|
136
136
|
occurredAt: now
|
|
137
137
|
};
|
|
138
138
|
}
|
|
139
|
+
const clockNow = prepared((db) => db.select({ now: clockMillis(db) }).from(tablesOf(db).auditChainHead));
|
|
140
|
+
const lastEntry = prepared((db) => {
|
|
141
|
+
const { auditLog } = tablesOf(db);
|
|
142
|
+
return db.select({
|
|
143
|
+
seq: auditLog.seq,
|
|
144
|
+
hash: auditLog.hash
|
|
145
|
+
}).from(auditLog).orderBy(desc(auditLog.seq)).limit(1);
|
|
146
|
+
});
|
|
139
147
|
/** An entry's columns, as the chain's codec reads them (`StoredEntry`). */
|
|
140
148
|
function entryColumns(db) {
|
|
141
149
|
const { auditLog } = tablesOf(db);
|
|
@@ -198,11 +206,64 @@ async function withinBytes(db, where, limit, order = "asc") {
|
|
|
198
206
|
count
|
|
199
207
|
};
|
|
200
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* The first entry missing from the log up to `seq`, and the one found in
|
|
211
|
+
* its place, or null when every one is there. Seqs are the primary key, so
|
|
212
|
+
* a stretch is whole exactly when its count is its length: one pass over
|
|
213
|
+
* the key's index, never the entries. Only a log with a gap pays for more,
|
|
214
|
+
* a count per halving to find where.
|
|
215
|
+
*
|
|
216
|
+
* entries 0..9, 4 cut: { expected: 4n, found: 5n }
|
|
217
|
+
*/
|
|
218
|
+
async function missingEntry(db, seq) {
|
|
219
|
+
const { auditLog } = tablesOf(db);
|
|
220
|
+
const whole = async (to) => {
|
|
221
|
+
const [{ n }] = await db.select({ n: count() }).from(auditLog).where(and(gte(auditLog.seq, 0n), lte(auditLog.seq, to)));
|
|
222
|
+
return BigInt(n) === to + 1n;
|
|
223
|
+
};
|
|
224
|
+
if (seq < 0n || await whole(seq)) return null;
|
|
225
|
+
let [low, high] = [0n, seq];
|
|
226
|
+
while (low < high) {
|
|
227
|
+
const middle = (low + high) / 2n;
|
|
228
|
+
if (await whole(middle)) low = middle + 1n;
|
|
229
|
+
else high = middle;
|
|
230
|
+
}
|
|
231
|
+
const [next] = await db.select({ seq: auditLog.seq }).from(auditLog).where(gt(auditLog.seq, low)).orderBy(asc(auditLog.seq)).limit(1);
|
|
232
|
+
return {
|
|
233
|
+
expected: low,
|
|
234
|
+
found: next?.seq ?? null
|
|
235
|
+
};
|
|
236
|
+
}
|
|
201
237
|
/** Up to `limit` of the entries in the vault's name, allowed, of one `action`, before `before` (the newest when undefined), newest first. */
|
|
202
238
|
async function newestVaultEntries(db, action, limit, before) {
|
|
239
|
+
return before === void 0 ? await newestEntries(db, {
|
|
240
|
+
action,
|
|
241
|
+
limit
|
|
242
|
+
}) : await entriesBefore(db, {
|
|
243
|
+
action,
|
|
244
|
+
limit,
|
|
245
|
+
before
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
function vaultEntries(db, before) {
|
|
203
249
|
const { auditLog } = tablesOf(db);
|
|
204
|
-
return
|
|
250
|
+
return db.select(entryColumns(db)).from(auditLog).where(and(eq(auditLog.author, "vault"), eq(auditLog.action, sql.placeholder("action")), eq(auditLog.decision, "allow"), before)).orderBy(desc(auditLog.seq)).limit(sql.placeholder("limit"));
|
|
251
|
+
}
|
|
252
|
+
const newestEntries = prepared((db) => vaultEntries(db));
|
|
253
|
+
const entriesBefore = prepared((db) => vaultEntries(db, lt(tablesOf(db).auditLog.seq, sql.placeholder("before"))));
|
|
254
|
+
/**
|
|
255
|
+
* The number of the newest entry in the vault's name, allowed, of one
|
|
256
|
+
* `action`, or null for none: whether one was appended since, and nothing
|
|
257
|
+
* of what it says, so its MAC is not checked. One indexed read.
|
|
258
|
+
*/
|
|
259
|
+
async function newestVaultSeq(db, action) {
|
|
260
|
+
const [row] = await newestSeq(db, { action });
|
|
261
|
+
return row?.seq ?? null;
|
|
205
262
|
}
|
|
263
|
+
const newestSeq = prepared((db) => {
|
|
264
|
+
const { auditLog } = tablesOf(db);
|
|
265
|
+
return db.select({ seq: auditLog.seq }).from(auditLog).where(and(eq(auditLog.author, "vault"), eq(auditLog.action, sql.placeholder("action")), eq(auditLog.decision, "allow"))).orderBy(desc(auditLog.seq)).limit(1);
|
|
266
|
+
});
|
|
206
267
|
/** How many entries in its name the vault reads back, a page at a time, for its own newest of a kind (`newestAuthentic`). */
|
|
207
268
|
const LOOK_BACK = {
|
|
208
269
|
page: 32,
|
|
@@ -210,23 +271,27 @@ const LOOK_BACK = {
|
|
|
210
271
|
};
|
|
211
272
|
/**
|
|
212
273
|
* The newest of the entries `page` reads, newest first and before a seq,
|
|
213
|
-
* that `check` finds authentic
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
274
|
+
* that `check` finds authentic: the newest alone first, which it is unless
|
|
275
|
+
* entries were forged in the vault's name since, then back a page at a
|
|
276
|
+
* time. 'none' when there are none at all, 'forged' when every one is in
|
|
277
|
+
* the vault's name but not its own, and 'unreached' once it has read
|
|
278
|
+
* LOOK_BACK such entries (1024), whether or not more are left.
|
|
217
279
|
*/
|
|
218
280
|
async function newestAuthentic(page, check) {
|
|
281
|
+
const most = LOOK_BACK.page * LOOK_BACK.pages;
|
|
282
|
+
let read = 0;
|
|
219
283
|
let before;
|
|
220
|
-
for (let
|
|
221
|
-
const entries = await page(before);
|
|
284
|
+
for (let limit = 1; read < most; limit = Math.min(LOOK_BACK.page, most - read)) {
|
|
285
|
+
const entries = await page(limit, before);
|
|
222
286
|
for (const entry of entries) {
|
|
223
287
|
if (check.authentic(entry)) return entry;
|
|
224
288
|
check.forged(entry);
|
|
225
289
|
}
|
|
226
|
-
|
|
290
|
+
read += entries.length;
|
|
291
|
+
if (entries.length < limit) return read === 0 ? "none" : "forged";
|
|
227
292
|
before = entries.at(-1).seq;
|
|
228
293
|
}
|
|
229
294
|
return "unreached";
|
|
230
295
|
}
|
|
231
296
|
//#endregion
|
|
232
|
-
export { BATCH_BYTES, LOOK_BACK, LogHeadMismatch, LogRewound, appendEntries, entryColumns, forgetLogHeads, lockLogHead, newestAuthentic, newestVaultEntries, withinBytes };
|
|
297
|
+
export { BATCH_BYTES, LOOK_BACK, LogHeadMismatch, LogRewound, appendEntries, entryColumns, forgetLogHeads, lockLogHead, missingEntry, newestAuthentic, newestVaultEntries, newestVaultSeq, withinBytes };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- Sensitive values (0.10): a version never handed back to a person, which the vault's wrap of its data key binds. Defaults to false, so the release before writes on as it did.
|
|
2
|
+
ALTER TABLE "secret_versions" ADD COLUMN "sensitive" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
|
3
|
+
-- A version is still only ever erased: its sensitivity with the rest of it.
|
|
4
|
+
CREATE OR REPLACE FUNCTION secret_versions_erase_only() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
5
|
+
BEGIN
|
|
6
|
+
IF octet_length(NEW.ciphertext) <> 0 OR octet_length(NEW.wrapped_dek) <> 0 OR NEW.plaintext IS NOT NULL
|
|
7
|
+
OR (NEW.id, NEW.secret_id, NEW.version, NEW.envelope_version, NEW.iv, NEW.auth_tag,
|
|
8
|
+
NEW.kek_provider, NEW.kek_id, NEW.kek_version, NEW.created_at, NEW.created_by,
|
|
9
|
+
NEW.recipe, NEW.pair_id, NEW.pair_half, NEW.sensitive)
|
|
10
|
+
IS DISTINCT FROM
|
|
11
|
+
(OLD.id, OLD.secret_id, OLD.version, OLD.envelope_version, OLD.iv, OLD.auth_tag,
|
|
12
|
+
OLD.kek_provider, OLD.kek_id, OLD.kek_version, OLD.created_at, OLD.created_by,
|
|
13
|
+
OLD.recipe, OLD.pair_id, OLD.pair_half, OLD.sensitive) THEN
|
|
14
|
+
RAISE EXCEPTION 'a secret version is only ever erased' USING ERRCODE = 'insufficient_privilege';
|
|
15
|
+
END IF;
|
|
16
|
+
RETURN NEW;
|
|
17
|
+
END
|
|
18
|
+
$$;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
-- The Contributor role (0.10): use and write, never read. The role checks widen to it, which the release before never grants.
|
|
2
|
+
ALTER TABLE "vault_grants" DROP CONSTRAINT "vault_grants_role_check";--> statement-breakpoint
|
|
3
|
+
ALTER TABLE "vault_grants" DROP CONSTRAINT "vault_grants_environment_role_check";--> statement-breakpoint
|
|
4
|
+
ALTER TABLE "vault_grants" ADD CONSTRAINT "vault_grants_role_check" CHECK ("vault_grants"."role" IN ('viewer', 'contributor', 'developer', 'maintainer', 'access-manager', 'auditor', 'owner'));--> statement-breakpoint
|
|
5
|
+
ALTER TABLE "vault_grants" ADD CONSTRAINT "vault_grants_environment_role_check" CHECK (("vault_grants"."environment_id" IS NULL AND "vault_grants"."environment_slug" IS NULL) OR "vault_grants"."role" IN ('viewer', 'contributor', 'developer', 'auditor'));
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
-- Roles an Owner makes (0.10): a grant names a role by its id, which the vault's policy defines; which of them an environment may be granted, the vault decides. The checks widen: the release before grants only the roles it ships.
|
|
2
|
+
ALTER TABLE "vault_grants" DROP CONSTRAINT "vault_grants_environment_role_check";--> statement-breakpoint
|
|
3
|
+
ALTER TABLE "vault_grants" DROP CONSTRAINT "vault_grants_role_check";--> statement-breakpoint
|
|
4
|
+
ALTER TABLE "vault_grants" ADD CONSTRAINT "vault_grants_role_check" CHECK ("vault_grants"."role" ~ '^[a-z][a-z0-9-]{1,31}$');
|