@coffre/db 0.10.1 → 0.11.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-DBtn3jqp.d.ts → database-Dh4W2pAj.d.ts} +1 -1
- package/dist/dialect.d.ts +7 -1
- package/dist/dialect.js +10 -1
- package/dist/grants.d.ts +12 -5
- package/dist/grants.js +13 -8
- package/dist/hyperdrive.d.ts +9 -2
- package/dist/hyperdrive.js +16 -2
- package/dist/index.d.ts +2 -2
- package/dist/log.d.ts +65 -3
- package/dist/log.js +26 -14
- package/dist/migrations/postgres/0015_workload_access.sql +43 -0
- package/dist/migrations/postgres/0016_spent_tokens.sql +12 -0
- package/dist/migrations/postgres/meta/0015_snapshot.json +3256 -0
- package/dist/migrations/postgres/meta/0016_snapshot.json +3256 -0
- package/dist/migrations/postgres/meta/_journal.json +14 -0
- package/dist/migrations/sqlite/0015_workload_access.sql +70 -0
- package/dist/migrations/sqlite/meta/0015_snapshot.json +2959 -0
- package/dist/migrations/sqlite/meta/_journal.json +7 -0
- package/dist/portable.d.ts +2 -2
- package/dist/{schema-NBVA9q-s.d.ts → schema-DG2t57Kf.d.ts} +327 -2
- package/dist/schema-sqlite.d.ts +346 -1
- package/dist/schema-sqlite.js +53 -5
- package/dist/schema-version.d.ts +1 -1
- package/dist/schema-version.js +21 -0
- package/dist/schema.d.ts +2 -2
- package/dist/schema.js +54 -5
- package/package.json +2 -2
- package/src/dialect.ts +10 -0
- package/src/grants.ts +18 -9
- package/src/hyperdrive.ts +16 -2
- package/src/log.ts +35 -10
- package/src/migrations/postgres/0015_workload_access.sql +43 -0
- package/src/migrations/postgres/0016_spent_tokens.sql +12 -0
- package/src/migrations/postgres/meta/0015_snapshot.json +3256 -0
- package/src/migrations/postgres/meta/0016_snapshot.json +3256 -0
- package/src/migrations/postgres/meta/_journal.json +14 -0
- package/src/migrations/sqlite/0015_workload_access.sql +70 -0
- package/src/migrations/sqlite/meta/0015_snapshot.json +2959 -0
- package/src/migrations/sqlite/meta/_journal.json +7 -0
- package/src/schema.sqlite.ts +73 -2
- package/src/schema.ts +73 -2
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-Dh4W2pAj.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
|
|
@@ -32,6 +32,12 @@ export declare function engineOf(db: Queryable): Engine;
|
|
|
32
32
|
export declare function forUpdate<Query extends {
|
|
33
33
|
for: (strength: 'update' | 'no key update') => unknown;
|
|
34
34
|
}>(db: Queryable, query: Query, strength?: 'update' | 'no key update'): ReturnType<Query['for']>;
|
|
35
|
+
/**
|
|
36
|
+
* The rows a hand-written query answers, as the driver gives them: what
|
|
37
|
+
* Postgres's `execute` and SQLite's `all` each call a result. Column names
|
|
38
|
+
* are the query's own, unmapped.
|
|
39
|
+
*/
|
|
40
|
+
export declare function rowsOf<Row>(db: Queryable, query: SQL): Promise<Row[]>;
|
|
35
41
|
/** Insert the rows whose unique keys are free; returns how many that was. */
|
|
36
42
|
export declare function insertIfAbsent(db: Queryable, table: Table, rows: object[]): Promise<number>;
|
|
37
43
|
/**
|
package/dist/dialect.js
CHANGED
|
@@ -20,6 +20,15 @@ function forUpdate(db, query, strength = "update") {
|
|
|
20
20
|
if (engineOf(db) === "sqlite") return query;
|
|
21
21
|
return query.for(strength);
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* The rows a hand-written query answers, as the driver gives them: what
|
|
25
|
+
* Postgres's `execute` and SQLite's `all` each call a result. Column names
|
|
26
|
+
* are the query's own, unmapped.
|
|
27
|
+
*/
|
|
28
|
+
async function rowsOf(db, query) {
|
|
29
|
+
if (engineOf(db) === "postgres") return (await db.execute(query)).rows;
|
|
30
|
+
return db.all(query);
|
|
31
|
+
}
|
|
23
32
|
/** Insert the rows whose unique keys are free; returns how many that was. */
|
|
24
33
|
async function insertIfAbsent(db, table, rows) {
|
|
25
34
|
return changedRows(await db.insert(table).values(rows).onConflictDoNothing());
|
|
@@ -169,4 +178,4 @@ function canonicalTimestamp(raw) {
|
|
|
169
178
|
return `${(/* @__PURE__ */ new Date(Date.UTC(+year, +month - 1, +day, +hour, +minute, +second) - offsetMinutes * 6e4)).toISOString().slice(0, 19)}.${fraction.padEnd(6, "0")}Z`;
|
|
170
179
|
}
|
|
171
180
|
//#endregion
|
|
172
|
-
export { SNAPSHOT, ahead, canonicalTimestamp, changedRows, clock, clockMillis, engineOf, forUpdate, insertIfAbsent, isUniqueViolation, isUnreachable, migrationLedger, tombstone, truth, upsert };
|
|
181
|
+
export { SNAPSHOT, ahead, canonicalTimestamp, changedRows, clock, clockMillis, engineOf, forUpdate, insertIfAbsent, isUniqueViolation, isUnreachable, migrationLedger, rowsOf, tombstone, truth, upsert };
|
package/dist/grants.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as Queryable } from "./database-
|
|
1
|
+
import { i as Queryable } from "./database-Dh4W2pAj.js";
|
|
2
2
|
import { Authenticity, Newest } from "./log.js";
|
|
3
3
|
import { StoredEntry } from "@coffre/core/audit";
|
|
4
4
|
import { Conversion, ConversionInput, InstanceRole, Scope } from "@coffre/core/access";
|
|
@@ -92,17 +92,24 @@ export type EveryProjectPreview = {
|
|
|
92
92
|
* converts is listed.
|
|
93
93
|
*/
|
|
94
94
|
export declare function everyProjectPreview(db: Queryable, at: number): Promise<EveryProjectPreview[]>;
|
|
95
|
-
/**
|
|
95
|
+
/**
|
|
96
|
+
* A service token or trust binding whose scope this release has not sealed
|
|
97
|
+
* yet: none of its own, as 0.4 made it (`scope` null), or one that does not
|
|
98
|
+
* say where it reads sensitive values, as a release before 0.11 made it. By
|
|
99
|
+
* whom it was issued, `createdBy`, a person's email.
|
|
100
|
+
*/
|
|
96
101
|
export type Unsealed = {
|
|
97
102
|
kind: 'token' | 'binding';
|
|
98
103
|
id: string;
|
|
99
104
|
principal: string;
|
|
105
|
+
createdBy: string;
|
|
106
|
+
scope: string | null;
|
|
100
107
|
};
|
|
101
108
|
/**
|
|
102
109
|
* The live service tokens and trust bindings of active service accounts
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
110
|
+
* whose scope is not sealed yet (`Unsealed`): what the app seals once a
|
|
111
|
+
* process (`sealScopes`), and what `coffre migrate` says it will. A
|
|
112
|
+
* credential a binding issued carries its binding's.
|
|
106
113
|
*/
|
|
107
114
|
export declare function unsealedCredentials(db: Queryable, at: Date): Promise<Unsealed[]>;
|
|
108
115
|
/**
|
package/dist/grants.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { tombstone, truth } from "./dialect.js";
|
|
2
2
|
import { a as prepared, i as tablesOf } from "./database-BadNeeN-.js";
|
|
3
3
|
import { entryColumns, newestAuthentic } from "./log.js";
|
|
4
|
-
import { and, asc, desc, eq, gt, inArray, isNull, lt, or, sql } from "drizzle-orm";
|
|
4
|
+
import { and, asc, desc, eq, gt, inArray, isNull, like, 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
|
|
@@ -170,19 +170,24 @@ async function everyProjectPreview(db, at) {
|
|
|
170
170
|
}
|
|
171
171
|
/**
|
|
172
172
|
* The live service tokens and trust bindings of active service accounts
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
173
|
+
* whose scope is not sealed yet (`Unsealed`): what the app seals once a
|
|
174
|
+
* process (`sealScopes`), and what `coffre migrate` says it will. A
|
|
175
|
+
* credential a binding issued carries its binding's.
|
|
176
176
|
*/
|
|
177
177
|
async function unsealedCredentials(db, at) {
|
|
178
178
|
const { credentials, serviceBindings, vaultMembers } = tablesOf(db);
|
|
179
|
+
const unsealed = (scope) => or(isNull(scope), and(like(scope, "%\"projectId\"%"), sql`${scope} NOT LIKE '%"sensitive":%'`));
|
|
179
180
|
const [tokens, bindings] = await Promise.all([db.select({
|
|
180
181
|
id: credentials.id,
|
|
181
|
-
principal: credentials.principal
|
|
182
|
-
|
|
182
|
+
principal: credentials.principal,
|
|
183
|
+
createdBy: credentials.createdBy,
|
|
184
|
+
scope: credentials.scope
|
|
185
|
+
}).from(credentials).innerJoin(vaultMembers, and(eq(vaultMembers.principal, credentials.principal), eq(vaultMembers.generation, credentials.generation))).where(and(eq(credentials.kind, "service"), unsealed(credentials.scope), sql`${credentials.createdBy} NOT LIKE 'binding:%'`, isNull(credentials.revokedAt), gt(credentials.expiresAt, at), eq(vaultMembers.status, "active"))), db.select({
|
|
183
186
|
id: serviceBindings.id,
|
|
184
|
-
principal: serviceBindings.principal
|
|
185
|
-
|
|
187
|
+
principal: serviceBindings.principal,
|
|
188
|
+
createdBy: serviceBindings.createdBy,
|
|
189
|
+
scope: serviceBindings.scope
|
|
190
|
+
}).from(serviceBindings).innerJoin(vaultMembers, and(eq(vaultMembers.principal, serviceBindings.principal), eq(vaultMembers.generation, serviceBindings.generation))).where(and(unsealed(serviceBindings.scope), isNull(serviceBindings.revokedAt), eq(vaultMembers.status, "active")))]);
|
|
186
191
|
return [...tokens.map((row) => ({
|
|
187
192
|
kind: "token",
|
|
188
193
|
...row
|
package/dist/hyperdrive.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as PoolLike } from "./database-
|
|
1
|
+
import { n as PoolLike } from "./database-Dh4W2pAj.js";
|
|
2
2
|
import pg from "pg";
|
|
3
3
|
//#region src/hyperdrive.d.ts
|
|
4
4
|
/** Postgres through a Hyperdrive binding: `postgres(env.HYPERDRIVE)`. */
|
|
@@ -36,7 +36,14 @@ export declare function postgres(hyperdrive: {
|
|
|
36
36
|
*/
|
|
37
37
|
export declare class HyperdrivePool implements Exclude<PoolLike, pg.Pool> {
|
|
38
38
|
#private;
|
|
39
|
-
|
|
39
|
+
/**
|
|
40
|
+
* `closeIdleMs`: for what outlives a call, a Durable Object, which queries
|
|
41
|
+
* now and then: the client closes once it has been idle that long, and
|
|
42
|
+
* the next query connects again.
|
|
43
|
+
*/
|
|
44
|
+
constructor(connectionString: string, options?: {
|
|
45
|
+
closeIdleMs?: number;
|
|
46
|
+
});
|
|
40
47
|
readonly query: pg.Pool['query'];
|
|
41
48
|
/** The client, for a transaction: held from now until `release`. */
|
|
42
49
|
connect(): Promise<Pick<pg.PoolClient, 'query'> & {
|
package/dist/hyperdrive.js
CHANGED
|
@@ -43,12 +43,22 @@ var HyperdrivePool = class {
|
|
|
43
43
|
#busy = 0;
|
|
44
44
|
/** What waits for the client to be idle: `end`. */
|
|
45
45
|
#idle = [];
|
|
46
|
-
|
|
46
|
+
/** How long the client may sit idle before it closes, or null: closed by `end` alone. */
|
|
47
|
+
#closeIdleMs;
|
|
48
|
+
#idleTimer;
|
|
49
|
+
/**
|
|
50
|
+
* `closeIdleMs`: for what outlives a call, a Durable Object, which queries
|
|
51
|
+
* now and then: the client closes once it has been idle that long, and
|
|
52
|
+
* the next query connects again.
|
|
53
|
+
*/
|
|
54
|
+
constructor(connectionString, options = {}) {
|
|
47
55
|
if (connectionString.trim().length === 0) throw new Error("Hyperdrive returned an empty connection string");
|
|
48
56
|
this.#connectionString = connectionString;
|
|
57
|
+
this.#closeIdleMs = options.closeIdleMs ?? null;
|
|
49
58
|
}
|
|
50
59
|
query = (async (...args) => {
|
|
51
60
|
this.#busy += 1;
|
|
61
|
+
clearTimeout(this.#idleTimer);
|
|
52
62
|
try {
|
|
53
63
|
const client = await this.#connected();
|
|
54
64
|
return await new Promise((resolve, reject) => {
|
|
@@ -61,6 +71,7 @@ var HyperdrivePool = class {
|
|
|
61
71
|
/** The client, for a transaction: held from now until `release`. */
|
|
62
72
|
async connect() {
|
|
63
73
|
this.#busy += 1;
|
|
74
|
+
clearTimeout(this.#idleTimer);
|
|
64
75
|
let client;
|
|
65
76
|
try {
|
|
66
77
|
client = await this.#connected();
|
|
@@ -86,6 +97,7 @@ var HyperdrivePool = class {
|
|
|
86
97
|
}
|
|
87
98
|
/** Close the client once nothing waits on it: what a Worker hands `waitUntil` when the call is done. */
|
|
88
99
|
async end() {
|
|
100
|
+
clearTimeout(this.#idleTimer);
|
|
89
101
|
while (this.#busy > 0) await new Promise((resolve) => this.#idle.push(resolve));
|
|
90
102
|
const client = this.#client;
|
|
91
103
|
this.#client = null;
|
|
@@ -104,7 +116,9 @@ var HyperdrivePool = class {
|
|
|
104
116
|
/** One query or transaction done: once none is left, what waits for the client to be idle goes on. */
|
|
105
117
|
#done() {
|
|
106
118
|
this.#busy -= 1;
|
|
107
|
-
if (this.#busy
|
|
119
|
+
if (this.#busy > 0) return;
|
|
120
|
+
for (const resolve of this.#idle.splice(0)) resolve();
|
|
121
|
+
if (this.#closeIdleMs !== null && this.#client !== null) this.#idleTimer = setTimeout(() => void this.end(), this.#closeIdleMs);
|
|
108
122
|
}
|
|
109
123
|
/** The call's client, connecting it the first time. One that fails is dropped: the next query connects again. */
|
|
110
124
|
#connected() {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
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-
|
|
1
|
+
import { h as schema_d_exports } from "./schema-DG2t57Kf.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-Dh4W2pAj.js";
|
|
3
3
|
export { Database, PoolLike, QueryOutsideTransaction, Queryable, Transaction, createDatabase, own, prepared, schema_d_exports as schema, tablesOf };
|
package/dist/log.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as Transaction, i as Queryable } from "./database-
|
|
2
|
-
import { SQL } from "drizzle-orm";
|
|
1
|
+
import { a as Transaction, i as Queryable } from "./database-Dh4W2pAj.js";
|
|
2
|
+
import { Placeholder, SQL } from "drizzle-orm";
|
|
3
3
|
import { LogKey, StoredEntry } from "@coffre/core/audit";
|
|
4
4
|
//#region src/log.d.ts
|
|
5
5
|
/**
|
|
@@ -75,7 +75,25 @@ export declare function lockLogHead(tx: Transaction): Promise<{
|
|
|
75
75
|
* log as it stands, which no other append can change until this one
|
|
76
76
|
* commits. It throws to refuse.
|
|
77
77
|
*/
|
|
78
|
-
export declare function appendEntries(tx: Transaction, key: LogKey, entries: readonly NewEntry[], check?: (tx: Transaction) => Promise<void
|
|
78
|
+
export declare function appendEntries(tx: Transaction, key: LogKey, entries: readonly NewEntry[], check?: (tx: Transaction) => Promise<void>,
|
|
79
|
+
/**
|
|
80
|
+
* The head as a statement of the caller's locked it, with the log's last
|
|
81
|
+
* entry and the clock as that statement read them once it held the lock:
|
|
82
|
+
* nothing to read again.
|
|
83
|
+
*/
|
|
84
|
+
locked?: HeldHead): Promise<Appended>;
|
|
85
|
+
/** A head locked by a statement of the caller's, and what it read under the lock (`appendEntries`). */
|
|
86
|
+
export type HeldHead = {
|
|
87
|
+
head: {
|
|
88
|
+
nextSeq: bigint;
|
|
89
|
+
headHash: Buffer;
|
|
90
|
+
};
|
|
91
|
+
last: {
|
|
92
|
+
seq: bigint;
|
|
93
|
+
hash: Buffer;
|
|
94
|
+
} | null;
|
|
95
|
+
at: number;
|
|
96
|
+
};
|
|
79
97
|
/** An entry's columns, as the chain's codec reads them (`StoredEntry`). */
|
|
80
98
|
export declare function entryColumns(db: Queryable): {
|
|
81
99
|
seq: import("drizzle-orm/pg-core").PgColumn<{
|
|
@@ -492,6 +510,50 @@ export declare function newestVaultEntries(db: Queryable, action: string, limit:
|
|
|
492
510
|
* of what it says, so its MAC is not checked. One indexed read.
|
|
493
511
|
*/
|
|
494
512
|
export declare function newestVaultSeq(db: Queryable, action: string): Promise<bigint | null>;
|
|
513
|
+
/**
|
|
514
|
+
* `newestVaultSeq`'s query, to run or to read inside another, as a
|
|
515
|
+
* credential's read takes the newest `policy.change` with it: one rule for
|
|
516
|
+
* what the newest is, wherever it is read.
|
|
517
|
+
*/
|
|
518
|
+
export declare function newestSeqQuery(db: Queryable, action: string | Placeholder): Omit<import("drizzle-orm/pg-core").PgSelectBase<"audit_log", {
|
|
519
|
+
seq: import("drizzle-orm/pg-core").PgColumn<{
|
|
520
|
+
name: "seq";
|
|
521
|
+
tableName: "audit_log";
|
|
522
|
+
dataType: "bigint";
|
|
523
|
+
columnType: "PgBigInt64";
|
|
524
|
+
data: bigint;
|
|
525
|
+
driverParam: string;
|
|
526
|
+
notNull: true;
|
|
527
|
+
hasDefault: false;
|
|
528
|
+
isPrimaryKey: true;
|
|
529
|
+
isAutoincrement: false;
|
|
530
|
+
hasRuntimeDefault: false;
|
|
531
|
+
enumValues: undefined;
|
|
532
|
+
baseColumn: never;
|
|
533
|
+
identity: undefined;
|
|
534
|
+
generated: undefined;
|
|
535
|
+
}, {}, {}>;
|
|
536
|
+
}, "partial", Record<"audit_log", "not-null">, false, "where" | "orderBy" | "limit", {
|
|
537
|
+
seq: bigint;
|
|
538
|
+
}[], {
|
|
539
|
+
seq: import("drizzle-orm/pg-core").PgColumn<{
|
|
540
|
+
name: "seq";
|
|
541
|
+
tableName: "audit_log";
|
|
542
|
+
dataType: "bigint";
|
|
543
|
+
columnType: "PgBigInt64";
|
|
544
|
+
data: bigint;
|
|
545
|
+
driverParam: string;
|
|
546
|
+
notNull: true;
|
|
547
|
+
hasDefault: false;
|
|
548
|
+
isPrimaryKey: true;
|
|
549
|
+
isAutoincrement: false;
|
|
550
|
+
hasRuntimeDefault: false;
|
|
551
|
+
enumValues: undefined;
|
|
552
|
+
baseColumn: never;
|
|
553
|
+
identity: undefined;
|
|
554
|
+
generated: undefined;
|
|
555
|
+
}, {}, {}>;
|
|
556
|
+
}>, "where" | "orderBy" | "limit">;
|
|
495
557
|
/** How many entries in its name the vault reads back, a page at a time, for its own newest of a kind (`newestAuthentic`). */
|
|
496
558
|
export declare const LOOK_BACK: {
|
|
497
559
|
page: number;
|
package/dist/log.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as afterCommit } from "./commits-BuW-NL3n.js";
|
|
2
|
-
import { clockMillis, forUpdate } from "./dialect.js";
|
|
2
|
+
import { clockMillis, engineOf, forUpdate } from "./dialect.js";
|
|
3
3
|
import { a as prepared, i as tablesOf } from "./database-BadNeeN-.js";
|
|
4
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";
|
|
@@ -69,13 +69,13 @@ const lockedHead = prepared((db) => {
|
|
|
69
69
|
* log as it stands, which no other append can change until this one
|
|
70
70
|
* commits. It throws to refuse.
|
|
71
71
|
*/
|
|
72
|
-
async function appendEntries(tx, key, entries, check) {
|
|
72
|
+
async function appendEntries(tx, key, entries, check, locked) {
|
|
73
73
|
if (entries.length === 0) throw new Error("appendEntries called with no entries");
|
|
74
74
|
const { auditChainHead, auditLog } = tablesOf(tx);
|
|
75
|
-
const head = await lockLogHead(tx);
|
|
75
|
+
const head = locked?.head ?? await lockLogHead(tx);
|
|
76
76
|
await check?.(tx);
|
|
77
|
-
const
|
|
78
|
-
const
|
|
77
|
+
const now = locked?.at ?? (await clockNow(tx))[0].now;
|
|
78
|
+
const last = locked === void 0 ? (await lastEntry(tx))[0] : locked.last ?? void 0;
|
|
79
79
|
const expected = last === void 0 ? {
|
|
80
80
|
nextSeq: 0n,
|
|
81
81
|
headHash: GENESIS_HASH
|
|
@@ -120,11 +120,15 @@ async function appendEntries(tx, key, entries, check) {
|
|
|
120
120
|
prevHash = hash;
|
|
121
121
|
seq += 1n;
|
|
122
122
|
}
|
|
123
|
-
await tx.insert(auditLog).values(rows)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
123
|
+
if (engineOf(tx) === "postgres" && rows.length <= ROWS_AT_ONCE) await tx.execute(sql`WITH appended AS (${tx.insert(auditLog).values(rows).getSQL()})
|
|
124
|
+
UPDATE audit_chain_head SET next_seq = ${seq}, head_hash = ${prevHash} WHERE only_row`);
|
|
125
|
+
else {
|
|
126
|
+
await tx.insert(auditLog).values(rows);
|
|
127
|
+
await tx.update(auditChainHead).set({
|
|
128
|
+
nextSeq: seq,
|
|
129
|
+
headHash: prevHash
|
|
130
|
+
}).where(eq(auditChainHead.onlyRow, true));
|
|
131
|
+
}
|
|
128
132
|
const written = {
|
|
129
133
|
nextSeq: seq,
|
|
130
134
|
headHash: prevHash
|
|
@@ -136,6 +140,8 @@ async function appendEntries(tx, key, entries, check) {
|
|
|
136
140
|
occurredAt: now
|
|
137
141
|
};
|
|
138
142
|
}
|
|
143
|
+
/** Entries one statement inserts at most, at 21 parameters each: Postgres takes 65,535. */
|
|
144
|
+
const ROWS_AT_ONCE = 3e3;
|
|
139
145
|
const clockNow = prepared((db) => db.select({ now: clockMillis(db) }).from(tablesOf(db).auditChainHead));
|
|
140
146
|
const lastEntry = prepared((db) => {
|
|
141
147
|
const { auditLog } = tablesOf(db);
|
|
@@ -260,10 +266,16 @@ async function newestVaultSeq(db, action) {
|
|
|
260
266
|
const [row] = await newestSeq(db, { action });
|
|
261
267
|
return row?.seq ?? null;
|
|
262
268
|
}
|
|
263
|
-
const newestSeq = prepared((db) =>
|
|
269
|
+
const newestSeq = prepared((db) => newestSeqQuery(db, sql.placeholder("action")));
|
|
270
|
+
/**
|
|
271
|
+
* `newestVaultSeq`'s query, to run or to read inside another, as a
|
|
272
|
+
* credential's read takes the newest `policy.change` with it: one rule for
|
|
273
|
+
* what the newest is, wherever it is read.
|
|
274
|
+
*/
|
|
275
|
+
function newestSeqQuery(db, action) {
|
|
264
276
|
const { auditLog } = tablesOf(db);
|
|
265
|
-
return db.select({ seq: auditLog.seq }).from(auditLog).where(and(eq(auditLog.author, "vault"), eq(auditLog.action,
|
|
266
|
-
}
|
|
277
|
+
return db.select({ seq: auditLog.seq }).from(auditLog).where(and(eq(auditLog.author, "vault"), eq(auditLog.action, action), eq(auditLog.decision, "allow"))).orderBy(desc(auditLog.seq)).limit(1);
|
|
278
|
+
}
|
|
267
279
|
/** How many entries in its name the vault reads back, a page at a time, for its own newest of a kind (`newestAuthentic`). */
|
|
268
280
|
const LOOK_BACK = {
|
|
269
281
|
page: 32,
|
|
@@ -294,4 +306,4 @@ async function newestAuthentic(page, check) {
|
|
|
294
306
|
return "unreached";
|
|
295
307
|
}
|
|
296
308
|
//#endregion
|
|
297
|
-
export { BATCH_BYTES, LOOK_BACK, LogHeadMismatch, LogRewound, appendEntries, entryColumns, forgetLogHeads, lockLogHead, missingEntry, newestAuthentic, newestVaultEntries, newestVaultSeq, withinBytes };
|
|
309
|
+
export { BATCH_BYTES, LOOK_BACK, LogHeadMismatch, LogRewound, appendEntries, entryColumns, forgetLogHeads, lockLogHead, missingEntry, newestAuthentic, newestSeqQuery, newestVaultEntries, newestVaultSeq, withinBytes };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
-- Workload access (docs/design/workload-access.md, section 11): key-bound sessions, their enrollment codes, and bindings' policies. It only adds: the release before neither reads nor writes any of it, and every check is only looser for its rows, so it serves on this schema unchanged.
|
|
2
|
+
CREATE TABLE "enrollments" (
|
|
3
|
+
"id" uuid PRIMARY KEY NOT NULL,
|
|
4
|
+
"auth_mac" "bytea" NOT NULL,
|
|
5
|
+
"binding_id" uuid NOT NULL,
|
|
6
|
+
"code_hash" "bytea" NOT NULL,
|
|
7
|
+
"code_hint" text NOT NULL,
|
|
8
|
+
"scope" text NOT NULL,
|
|
9
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
10
|
+
"created_by" text NOT NULL,
|
|
11
|
+
"created_with" uuid,
|
|
12
|
+
"expires_at" timestamp with time zone NOT NULL,
|
|
13
|
+
CONSTRAINT "enrollments_code_hash_key" UNIQUE("code_hash"),
|
|
14
|
+
CONSTRAINT "enrollments_auth_mac_check" CHECK (octet_length("enrollments"."auth_mac") = 32),
|
|
15
|
+
CONSTRAINT "enrollments_code_hash_check" CHECK (octet_length("enrollments"."code_hash") = 32),
|
|
16
|
+
CONSTRAINT "enrollments_scope_check" CHECK ("enrollments"."scope"::jsonb IS NOT NULL)
|
|
17
|
+
);
|
|
18
|
+
--> statement-breakpoint
|
|
19
|
+
-- The checks widen to a workload's session, a service's credential like a service token; a session carries the key it is bound to.
|
|
20
|
+
ALTER TABLE "credentials" DROP CONSTRAINT "credentials_kind_check";--> statement-breakpoint
|
|
21
|
+
ALTER TABLE "credentials" DROP CONSTRAINT "credentials_kind_matches_principal";--> statement-breakpoint
|
|
22
|
+
ALTER TABLE "credentials" ADD COLUMN "jkt" text;--> statement-breakpoint
|
|
23
|
+
ALTER TABLE "credentials" ADD COLUMN "not_after" timestamp with time zone;--> statement-breakpoint
|
|
24
|
+
ALTER TABLE "credentials" ADD COLUMN "ancestors" text;--> statement-breakpoint
|
|
25
|
+
ALTER TABLE "credentials" ADD COLUMN "constraints" text;--> statement-breakpoint
|
|
26
|
+
ALTER TABLE "credentials" ADD COLUMN "enrollment_id" uuid;--> statement-breakpoint
|
|
27
|
+
ALTER TABLE "credentials" ADD COLUMN "approved_at" timestamp with time zone;--> statement-breakpoint
|
|
28
|
+
ALTER TABLE "credentials" ADD COLUMN "approved_by" text;--> statement-breakpoint
|
|
29
|
+
-- A binding's policy, inserted with it under the table-wide INSERT, and named by no UPDATE grant: as immutable as the rest of it.
|
|
30
|
+
ALTER TABLE "service_bindings" ADD COLUMN "policy" text;--> statement-breakpoint
|
|
31
|
+
ALTER TABLE "enrollments" ADD CONSTRAINT "enrollments_binding_id_fkey" FOREIGN KEY ("binding_id") REFERENCES "public"."service_bindings"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
|
|
32
|
+
CREATE INDEX "enrollments_binding_idx" ON "enrollments" USING btree ("binding_id","created_at" DESC NULLS LAST);--> statement-breakpoint
|
|
33
|
+
ALTER TABLE "credentials" ADD CONSTRAINT "credentials_enrollment_id_fkey" FOREIGN KEY ("enrollment_id") REFERENCES "public"."enrollments"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
|
|
34
|
+
-- Freezes are the log's: a target's newest allowed freeze or lift, and a code's supersession, its tombstone.
|
|
35
|
+
CREATE INDEX "audit_log_freeze_idx" ON "audit_log" USING btree ((("metadata")::jsonb ->> 'target'),"seq" DESC NULLS LAST) WHERE "audit_log"."author" = 'app' AND "audit_log"."action" IN ('workload.freeze', 'workload.unfreeze') AND "audit_log"."decision" = 'allow';--> statement-breakpoint
|
|
36
|
+
CREATE INDEX "audit_log_supersede_idx" ON "audit_log" USING btree ((("metadata")::jsonb ->> 'enrollmentId')) WHERE "audit_log"."author" = 'app' AND "audit_log"."action" = 'enrollment.supersede' AND "audit_log"."decision" = 'allow';--> statement-breakpoint
|
|
37
|
+
CREATE INDEX "credentials_enrollment_roots_idx" ON "credentials" USING btree ("enrollment_id","created_at") WHERE "credentials"."kind" = 'workload' AND "credentials"."ancestors" IS NULL;--> statement-breakpoint
|
|
38
|
+
ALTER TABLE "credentials" ADD CONSTRAINT "credentials_workload_jkt_check" CHECK ("credentials"."kind" <> 'workload' OR "credentials"."jkt" IS NOT NULL);--> statement-breakpoint
|
|
39
|
+
ALTER TABLE "credentials" ADD CONSTRAINT "credentials_kind_check" CHECK ("credentials"."kind" IN ('browser', 'cli', 'service', 'personal', 'handle', 'workload'));--> statement-breakpoint
|
|
40
|
+
ALTER TABLE "credentials" ADD CONSTRAINT "credentials_kind_matches_principal" CHECK (("credentials"."kind" IN ('service', 'workload')) = ("credentials"."principal" LIKE 'token:%'));--> statement-breakpoint
|
|
41
|
+
-- The app makes codes and never changes one; a session's key, hard end, ancestors, limits and code are set when it is made, and never after: renewal moves its expiry, and an approval is the one other change.
|
|
42
|
+
GRANT SELECT, INSERT ON enrollments TO coffre_app;--> statement-breakpoint
|
|
43
|
+
GRANT UPDATE (expires_at, approved_at, approved_by) ON credentials TO coffre_app;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- Spent tokens (W157): a token's record goes once no exchange could take the token again. An exchange refuses one issued over an hour ago (MAX_TOKEN_AGE_SECONDS, 30 s of clock tolerance), so two hours after it was spent its record guards nothing. The runtime login still deletes no row itself: it asks this function, which deletes only such records, at most at_most a call. The release before never calls it.
|
|
2
|
+
CREATE FUNCTION prune_consumed_tokens(at_most integer) RETURNS integer
|
|
3
|
+
LANGUAGE sql SECURITY DEFINER SET search_path = pg_catalog, pg_temp AS $$
|
|
4
|
+
WITH pruned AS (
|
|
5
|
+
DELETE FROM public.consumed_tokens
|
|
6
|
+
WHERE hash IN (SELECT hash FROM public.consumed_tokens WHERE consumed_at < now() - interval '2 hours' LIMIT at_most)
|
|
7
|
+
RETURNING 1
|
|
8
|
+
)
|
|
9
|
+
SELECT count(*)::integer FROM pruned;
|
|
10
|
+
$$;--> statement-breakpoint
|
|
11
|
+
REVOKE ALL ON FUNCTION prune_consumed_tokens(integer) FROM PUBLIC;--> statement-breakpoint
|
|
12
|
+
GRANT EXECUTE ON FUNCTION prune_consumed_tokens(integer) TO coffre_app;
|