@coffre/vault 0.6.2 → 0.8.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/cloudflare.d.ts +2 -2
- package/dist/cloudflare.js +2 -2
- package/dist/{index-CwsWOH6j.d.ts → index-BkHJR9Gt.d.ts} +12 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/node.d.ts +2 -2
- package/dist/node.js +2 -2
- package/dist/{src-CQJS3OZZ.js → src-BcttALFQ.js} +6 -1
- package/dist/{vault-ihE3Yc4a.js → vault-lBbyZ3kQ.js} +235 -54
- package/package.json +3 -3
- package/src/accounting.ts +30 -18
- package/src/config.ts +20 -0
- package/src/index.ts +1 -1
- package/src/log.ts +1 -2
- package/src/store.ts +37 -12
- package/src/vault.ts +193 -25
package/dist/cloudflare.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as KekUnavailableError, c as awsKms, d as Kek, f as
|
|
1
|
+
import { a as KekUnavailableError, c as awsKms, d as Kek, f as People, i as KekProvider, l as checkpointMessage, m as derivedSigningKey, n as AwsKmsOptions, o as SecretContext, p as VaultConfig, r as KekBadClaimError, s as WrappedDek, t as AwsCredentials, u as verifyCheckpoint } from "./index-BkHJR9Gt.js";
|
|
2
2
|
import { AdmitInput, EndReferencesInput, ReferenceInput, RemoveInput, RewrapInput, SetAccessInput, SetSettingsInput, UnwrapInput, Vault, VerifyLogInput, WrapInput } from "@coffre/core/vault";
|
|
3
3
|
import { PostgresDatabase, PostgresDatabase as PostgresDatabase$1, postgres } from "@coffre/db/hyperdrive";
|
|
4
4
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
@@ -63,4 +63,4 @@ export declare class VaultEntrypoint extends WorkerEntrypoint implements Vault {
|
|
|
63
63
|
/** The vault Worker's default export, `VaultEntrypoint`, configured from its `env`. */
|
|
64
64
|
export declare function vault<Env>(configure_: (env: Env) => WorkersVaultConfig): typeof VaultEntrypoint;
|
|
65
65
|
//#endregion
|
|
66
|
-
export { type AwsCredentials, type AwsKmsOptions, type Kek, KekBadClaimError, type KekProvider, KekUnavailableError, type PostgresDatabase, type SecretContext, type Vault, type VaultConfig, type WrappedDek, awsKms, checkpointMessage, derivedSigningKey, postgres, verifyCheckpoint };
|
|
66
|
+
export { type AwsCredentials, type AwsKmsOptions, type Kek, KekBadClaimError, type KekProvider, KekUnavailableError, type People, type PostgresDatabase, type SecretContext, type Vault, type VaultConfig, type WrappedDek, awsKms, checkpointMessage, derivedSigningKey, postgres, verifyCheckpoint };
|
package/dist/cloudflare.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as verifyCheckpoint, i as checkpointMessage, n as KekUnavailableError, o as derivedSigningKey, r as awsKms, s as resolveVaultConfig, t as KekBadClaimError } from "./src-
|
|
2
|
-
import { n as prepareVault, t as openVault } from "./vault-
|
|
1
|
+
import { a as verifyCheckpoint, i as checkpointMessage, n as KekUnavailableError, o as derivedSigningKey, r as awsKms, s as resolveVaultConfig, t as KekBadClaimError } from "./src-BcttALFQ.js";
|
|
2
|
+
import { n as prepareVault, t as openVault } from "./vault-lBbyZ3kQ.js";
|
|
3
3
|
import { createDatabase } from "@coffre/db";
|
|
4
4
|
import { HyperdrivePool, postgres } from "@coffre/db/hyperdrive";
|
|
5
5
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
@@ -2,6 +2,15 @@ import { checkpointMessage as checkpointMessage$1, verifyCheckpoint } from "@cof
|
|
|
2
2
|
import { AwsCredentials, AwsKmsOptions, KekBadClaimError as KekBadClaimError$1, KekProvider, KekProvider as KekProvider$1, KekRegistry, KekUnavailableError as KekUnavailableError$1, WrappedDek, awsKms } from "@coffre/core/kek";
|
|
3
3
|
import { SecretContext } from "@coffre/core/envelope";
|
|
4
4
|
//#region src/config.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* What a person's reads open (docs/design/credential-proxy.md): `values`,
|
|
7
|
+
* as any member's; or `stand-ins`, where a person, on any credential of
|
|
8
|
+
* theirs (a session, a personal token, an MCP connection), opens values
|
|
9
|
+
* only for coffre to use for them, never to be handed back: the credential
|
|
10
|
+
* proxy injects them, and the deployment's `expose` list names the few it
|
|
11
|
+
* may pass on. Service accounts read values either way.
|
|
12
|
+
*/
|
|
13
|
+
type People = 'values' | 'stand-ins';
|
|
5
14
|
/**
|
|
6
15
|
* A key-encryption key: 32 random bytes, base64, and the id envelopes record
|
|
7
16
|
* it under; or one a key service holds, such as `awsKms({ keyArn, … })`.
|
|
@@ -41,6 +50,8 @@ type VaultConfig = {
|
|
|
41
50
|
count: number;
|
|
42
51
|
windowMinutes: number;
|
|
43
52
|
};
|
|
53
|
+
/** What a person's reads open (`People`): `values` unless set. */
|
|
54
|
+
people?: People;
|
|
44
55
|
} & Owners;
|
|
45
56
|
/**
|
|
46
57
|
* At least one email, each an Owner of the whole instance: made one when
|
|
@@ -69,4 +80,4 @@ type Owners = {
|
|
|
69
80
|
/** The signing key a local KEK stands for, when the vault is given none: HKDF-SHA-256 of the KEK. */
|
|
70
81
|
declare function derivedSigningKey(kek: Uint8Array): Buffer;
|
|
71
82
|
//#endregion
|
|
72
|
-
export { KekUnavailableError$1 as a, awsKms as c, Kek as d,
|
|
83
|
+
export { KekUnavailableError$1 as a, awsKms as c, Kek as d, People as f, KekProvider$1 as i, checkpointMessage$1 as l, derivedSigningKey as m, AwsKmsOptions as n, SecretContext as o, VaultConfig as p, KekBadClaimError$1 as r, WrappedDek as s, AwsCredentials as t, verifyCheckpoint as u };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as KekUnavailableError, c as awsKms, d as Kek, f as
|
|
1
|
+
import { a as KekUnavailableError, c as awsKms, d as Kek, f as People, i as KekProvider, l as checkpointMessage, m as derivedSigningKey, n as AwsKmsOptions, o as SecretContext, p as VaultConfig, r as KekBadClaimError, s as WrappedDek, t as AwsCredentials, u as verifyCheckpoint } from "./index-BkHJR9Gt.js";
|
|
2
2
|
export type * from "@coffre/core/vault";
|
|
3
|
-
export { type AwsCredentials, type AwsKmsOptions, type Kek, KekBadClaimError, type KekProvider, KekUnavailableError, type SecretContext, type VaultConfig, type WrappedDek, awsKms, checkpointMessage, derivedSigningKey, verifyCheckpoint };
|
|
3
|
+
export { type AwsCredentials, type AwsKmsOptions, type Kek, KekBadClaimError, type KekProvider, KekUnavailableError, type People, type SecretContext, type VaultConfig, type WrappedDek, awsKms, checkpointMessage, derivedSigningKey, verifyCheckpoint };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as verifyCheckpoint, i as checkpointMessage, n as KekUnavailableError, o as derivedSigningKey, r as awsKms, t as KekBadClaimError } from "./src-
|
|
1
|
+
import { a as verifyCheckpoint, i as checkpointMessage, n as KekUnavailableError, o as derivedSigningKey, r as awsKms, t as KekBadClaimError } from "./src-BcttALFQ.js";
|
|
2
2
|
export { KekBadClaimError, KekUnavailableError, awsKms, checkpointMessage, derivedSigningKey, verifyCheckpoint };
|
package/dist/node.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as KekUnavailableError, c as awsKms, d as Kek, f as
|
|
1
|
+
import { a as KekUnavailableError, c as awsKms, d as Kek, f as People, i as KekProvider, l as checkpointMessage, m as derivedSigningKey, n as AwsKmsOptions, o as SecretContext, p as VaultConfig, r as KekBadClaimError, s as WrappedDek, t as AwsCredentials, u as verifyCheckpoint } from "./index-BkHJR9Gt.js";
|
|
2
2
|
import { Vault, Vault as Vault$1 } from "@coffre/core/vault";
|
|
3
3
|
import { Database } from "@coffre/db";
|
|
4
4
|
import "@coffre/core/audit";
|
|
@@ -45,4 +45,4 @@ export declare function serveVault(config: NodeVaultConfig & {
|
|
|
45
45
|
/** The vault that `serveVault` runs, from the server's side of its socket. */
|
|
46
46
|
export declare function connectVault(socket: string): Vault;
|
|
47
47
|
//#endregion
|
|
48
|
-
export { type AwsCredentials, type AwsKmsOptions, type Kek, KekBadClaimError, type KekProvider, KekUnavailableError, type LocalVault, type SecretContext, type Vault, type VaultConfig, type VaultOptions, type WrappedDek, awsKms, checkpointMessage, derivedSigningKey, verifyCheckpoint };
|
|
48
|
+
export { type AwsCredentials, type AwsKmsOptions, type Kek, KekBadClaimError, type KekProvider, KekUnavailableError, type LocalVault, type People, type SecretContext, type Vault, type VaultConfig, type VaultOptions, type WrappedDek, awsKms, checkpointMessage, derivedSigningKey, verifyCheckpoint };
|
package/dist/node.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as verifyCheckpoint, i as checkpointMessage, n as KekUnavailableError, o as derivedSigningKey, r as awsKms, s as resolveVaultConfig, t as KekBadClaimError } from "./src-
|
|
2
|
-
import { n as prepareVault, t as openVault } from "./vault-
|
|
1
|
+
import { a as verifyCheckpoint, i as checkpointMessage, n as KekUnavailableError, o as derivedSigningKey, r as awsKms, s as resolveVaultConfig, t as KekBadClaimError } from "./src-BcttALFQ.js";
|
|
2
|
+
import { n as prepareVault, t as openVault } from "./vault-lBbyZ3kQ.js";
|
|
3
3
|
import { chmodSync, existsSync, lstatSync, rmSync } from "node:fs";
|
|
4
4
|
import { Agent, createServer, request } from "node:http";
|
|
5
5
|
import { openDatabase } from "@coffre/db/connect";
|
|
@@ -57,7 +57,8 @@ function resolveVaultConfig(config) {
|
|
|
57
57
|
keks: new KekRegistry(current, previous),
|
|
58
58
|
owners: checkOwners(config.owners ?? config.bootstrapOwners ?? config.rootAdmins ?? []),
|
|
59
59
|
signingKeys: signingKeys(config.signingKey, keks),
|
|
60
|
-
bulkLimit: config.bulkLimit === void 0 ? DEFAULT_BULK_LIMIT : checkBulkLimit(config.bulkLimit)
|
|
60
|
+
bulkLimit: config.bulkLimit === void 0 ? DEFAULT_BULK_LIMIT : checkBulkLimit(config.bulkLimit),
|
|
61
|
+
people: checkPeople(config.people ?? "values")
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
64
|
/**
|
|
@@ -80,6 +81,10 @@ function signingKeys(given, keks) {
|
|
|
80
81
|
}
|
|
81
82
|
return [signing, ...derived].filter((key, i, all) => all.findIndex((other) => other.equals(key)) === i);
|
|
82
83
|
}
|
|
84
|
+
function checkPeople(people) {
|
|
85
|
+
if (people !== "values" && people !== "stand-ins") throw new Error(`people is 'values' or 'stand-ins'; got ${JSON.stringify(people)}`);
|
|
86
|
+
return people;
|
|
87
|
+
}
|
|
83
88
|
function checkBulkLimit({ count, windowMinutes }) {
|
|
84
89
|
if (!Number.isInteger(count) || count < 1 || !(windowMinutes > 0)) throw new Error("bulkLimit needs a whole count of at least 1 and a window above 0 minutes");
|
|
85
90
|
return {
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { ACCESS_ACTIONS, checkpointMessage, checkpointVerifier, describeAccessFault } from "@coffre/core/vault";
|
|
1
|
+
import { ACCESS_ACTIONS, HANDED_BACK, checkpointMessage, checkpointVerifier, describeAccessFault } from "@coffre/core/vault";
|
|
2
2
|
import { DEK_BYTES, KEY_CHECK, KEY_CHECK_CONTEXT, KEY_CHECK_VALUE, KekBadClaimError, KekCancelledError, KekUnavailableError, LocalKekProvider } from "@coffre/core/kek";
|
|
3
3
|
import { createHmac, hkdfSync, randomUUID, timingSafeEqual } from "node:crypto";
|
|
4
4
|
import { tablesOf } from "@coffre/db";
|
|
5
|
-
import { EVERYWHERE, NOTHING, allows, assignableToEnvironment, givesService, grantKind, isCredentialScope, isInstanceRole, isRole, isScope, managesService, mayManageAccess, normalScope, ownsInstance, runsInstance, setsUpServices, unscoped } from "@coffre/core/access";
|
|
5
|
+
import { EVERYWHERE, NOTHING, allows, assignableToEnvironment, givesService, grantKind, isCredentialScope, isInstanceRole, isRole, isScope, managesService, mayManageAccess, normalScope, ownsInstance, runsInstance, setsUpServices, unscoped, withinCredential } from "@coffre/core/access";
|
|
6
6
|
import { GENESIS_HASH, deriveLogKey, verifyEntries } from "@coffre/core/audit";
|
|
7
|
-
import { checkContext } from "@coffre/core/envelope";
|
|
7
|
+
import { checkContext, ownerKey } from "@coffre/core/envelope";
|
|
8
8
|
import { shownMember } from "@coffre/core/schemas";
|
|
9
9
|
import { SNAPSHOT, clockMillis, engineOf, forUpdate, isUniqueViolation, tombstone, truth } from "@coffre/db/dialect";
|
|
10
10
|
import { accessEntriesAbout, everyProjectConversion, insertGrant, liveProjects, newestAccess, readGrants, reportStands, roleColumns, storedRole } from "@coffre/db/grants";
|
|
11
|
-
import { LOOK_BACK, appendEntries, entryColumns, lockLogHead, newestAuthentic, newestVaultEntries } from "@coffre/db/log";
|
|
12
|
-
import { and, asc, count, desc, eq, gt, gte, inArray, isNull, lt, lte, or, sql } from "drizzle-orm";
|
|
11
|
+
import { LOOK_BACK, appendEntries, entryColumns, lockLogHead, newestAuthentic, newestVaultEntries, withinBytes } from "@coffre/db/log";
|
|
12
|
+
import { and, asc, count, desc, eq, gt, gte, inArray, isNotNull, isNull, lt, lte, or, sql } from "drizzle-orm";
|
|
13
13
|
//#region src/accounting.ts
|
|
14
14
|
const KEY_ACTIONS = /* @__PURE__ */ new Set([
|
|
15
15
|
"secret.read",
|
|
@@ -49,12 +49,14 @@ var KeyAccounting = class {
|
|
|
49
49
|
const parsed = parseOutcome(entry);
|
|
50
50
|
if (typeof parsed === "string") return broken(parsed);
|
|
51
51
|
const intent = this.#intents.get(parsed.relatedSeq);
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
for (const outcome of parsed.items) {
|
|
53
|
+
const key = intent?.keys[outcome.item];
|
|
54
|
+
if (intent === void 0 || key === void 0 || intent.seen.has(outcome.item)) return broken("key outcome does not identify one item of its intent");
|
|
55
|
+
if (parsed.intentId !== intent.intentId || entry.actor !== intent.entry.actor || entry.action !== intent.operation) return broken("key outcome belongs to another operation");
|
|
56
|
+
if (outcome.secretId !== key.secretId || outcome.version !== key.version || outcome.subject !== key.subject) return broken("key outcome does not match its intended secret");
|
|
57
|
+
intent.seen.add(outcome.item);
|
|
58
|
+
}
|
|
59
|
+
if (intent !== void 0 && intent.seen.size === intent.keys.length) this.#intents.delete(intent.entry.seq);
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
/** The first fault, or the batches still under way at `at`: live calls can finish after the snapshot. */
|
|
@@ -109,19 +111,32 @@ function parseIntent(entry) {
|
|
|
109
111
|
function parseOutcome(entry) {
|
|
110
112
|
const detail = payload(entry.metadata);
|
|
111
113
|
if (detail === null) return "key outcome has no valid accounting payload";
|
|
112
|
-
const { intent
|
|
114
|
+
const { intent } = detail;
|
|
113
115
|
if (typeof intent !== "string" || entry.relatedSeq === null) return "key outcome has no intent identity";
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
const many = detail.secrets;
|
|
117
|
+
if (many !== void 0 && (!Array.isArray(many) || many.length === 0)) return "key outcome has no valid item list";
|
|
118
|
+
const listed = many === void 0 ? [{
|
|
119
|
+
...detail,
|
|
120
|
+
secretId: entry.secretId ?? detail.secretId
|
|
121
|
+
}] : many.map(record);
|
|
122
|
+
const items = [];
|
|
123
|
+
for (const one of listed) {
|
|
124
|
+
if (one === null) return "key outcome has no valid item list";
|
|
125
|
+
const { item, secretId, version, subject } = one;
|
|
126
|
+
if (typeof item !== "number" || !Number.isSafeInteger(item) || item < 0) return "key outcome has no valid item number";
|
|
127
|
+
if (typeof secretId !== "string" || typeof subject !== "string") return "key outcome has no secret identity";
|
|
128
|
+
if (typeof version !== "number" || !Number.isSafeInteger(version) || version < 1) return "key outcome has no valid version";
|
|
129
|
+
items.push({
|
|
130
|
+
item,
|
|
131
|
+
secretId,
|
|
132
|
+
version,
|
|
133
|
+
subject
|
|
134
|
+
});
|
|
135
|
+
}
|
|
118
136
|
return {
|
|
119
137
|
intentId: intent,
|
|
120
138
|
relatedSeq: entry.relatedSeq,
|
|
121
|
-
|
|
122
|
-
secretId,
|
|
123
|
-
version,
|
|
124
|
-
subject
|
|
139
|
+
items
|
|
125
140
|
};
|
|
126
141
|
}
|
|
127
142
|
function payload(text) {
|
|
@@ -274,12 +289,6 @@ function at(db, place) {
|
|
|
274
289
|
if (place.projectId !== null) return and(eq(vaultGrants.projectId, place.projectId), isNull(vaultGrants.environmentId));
|
|
275
290
|
return and(isNull(vaultGrants.projectId), isNull(vaultGrants.environmentId), place.environmentSlug === null ? isNull(vaultGrants.environmentSlug) : eq(vaultGrants.environmentSlug, place.environmentSlug));
|
|
276
291
|
}
|
|
277
|
-
/**
|
|
278
|
-
* Each of these environments that exists, by id, with its project, its
|
|
279
|
-
* slug, what grants on one slug in every project match, and whether it was
|
|
280
|
-
* deleted, alone or with its project: then no key under it opens, or is
|
|
281
|
-
* wrapped, whatever grant would cover it.
|
|
282
|
-
*/
|
|
283
292
|
async function environmentsById(db, ids) {
|
|
284
293
|
if (ids.length === 0) return /* @__PURE__ */ new Map();
|
|
285
294
|
const { environments, projects } = tablesOf(db);
|
|
@@ -287,7 +296,10 @@ async function environmentsById(db, ids) {
|
|
|
287
296
|
id: environments.id,
|
|
288
297
|
projectId: environments.projectId,
|
|
289
298
|
slug: environments.slug,
|
|
290
|
-
|
|
299
|
+
owner: environments.owner,
|
|
300
|
+
parentId: environments.parentId,
|
|
301
|
+
deleted: truth(or(tombstone(environments.slug), tombstone(projects.slug))),
|
|
302
|
+
archived: truth(or(isNotNull(environments.archivedAt), isNotNull(projects.archivedAt)))
|
|
291
303
|
}).from(environments).innerJoin(projects, eq(projects.id, environments.projectId)).where(inArray(environments.id, [...new Set(ids)]));
|
|
292
304
|
return new Map(rows.map(({ id, ...environment }) => [id, environment]));
|
|
293
305
|
}
|
|
@@ -348,11 +360,16 @@ async function everyProjectHolders(db) {
|
|
|
348
360
|
}
|
|
349
361
|
/** The action of an entry that releases a key, which the bulk limit counts. */
|
|
350
362
|
const RELEASE = "secret.read";
|
|
351
|
-
/**
|
|
363
|
+
/**
|
|
364
|
+
* How many keys the vault has released to `principal` since `after`: one
|
|
365
|
+
* for an entry about one secret, and as many as it lists for one about
|
|
366
|
+
* several (`secrets`), a read logged once per place.
|
|
367
|
+
*/
|
|
352
368
|
async function releasesSince(db, principal, after) {
|
|
353
369
|
const { auditLog } = tablesOf(db);
|
|
354
|
-
const
|
|
355
|
-
|
|
370
|
+
const listed = engineOf(db) === "postgres" ? sql`jsonb_array_length((${auditLog.metadata})::jsonb -> 'secrets')` : sql`json_array_length(${auditLog.metadata}, '$.secrets')`;
|
|
371
|
+
const [{ n }] = await db.select({ n: sql`SUM(COALESCE(${listed}, 1))` }).from(auditLog).where(and(eq(auditLog.author, "vault"), eq(auditLog.actor, principal), eq(auditLog.action, RELEASE), eq(auditLog.decision, "allow"), gt(auditLog.occurredAt, after)));
|
|
372
|
+
return Number(n ?? 0);
|
|
356
373
|
}
|
|
357
374
|
/** Rows of the log as the chain's codec reads them: `author` and `decision` are checked by the table. */
|
|
358
375
|
function stored(rows) {
|
|
@@ -361,12 +378,17 @@ function stored(rows) {
|
|
|
361
378
|
/** Up to `limit` entries of either author from `fromSeq`, oldest first: the chain as it runs. */
|
|
362
379
|
async function entriesFrom(db, fromSeq, limit) {
|
|
363
380
|
const { auditLog } = tablesOf(db);
|
|
364
|
-
|
|
381
|
+
const batch = await withinBytes(db, gte(auditLog.seq, fromSeq), limit);
|
|
382
|
+
if (batch === null) return [];
|
|
383
|
+
return stored(await db.select(entryColumns(db)).from(auditLog).where(and(gte(auditLog.seq, batch.from), lte(auditLog.seq, batch.to))).orderBy(asc(auditLog.seq)));
|
|
365
384
|
}
|
|
366
385
|
/** Up to `limit` of the vault's entries after `afterSeq`, oldest first: every one, whatever its action or decision. */
|
|
367
386
|
async function vaultEntriesAfter(db, afterSeq, limit) {
|
|
368
387
|
const { auditLog } = tablesOf(db);
|
|
369
|
-
|
|
388
|
+
const vault = eq(auditLog.author, "vault");
|
|
389
|
+
const batch = await withinBytes(db, and(vault, gt(auditLog.seq, afterSeq)), limit);
|
|
390
|
+
if (batch === null) return [];
|
|
391
|
+
return stored(await db.select(entryColumns(db)).from(auditLog).where(and(vault, gte(auditLog.seq, batch.from), lte(auditLog.seq, batch.to))).orderBy(asc(auditLog.seq)));
|
|
370
392
|
}
|
|
371
393
|
/** The hash of the entry at each of `seqs` the log holds, by seq. */
|
|
372
394
|
async function hashesAt(db, seqs) {
|
|
@@ -552,7 +574,7 @@ const UNVERIFIED = {
|
|
|
552
574
|
function further(a, b) {
|
|
553
575
|
return b.nextSeq > a.nextSeq ? b : a;
|
|
554
576
|
}
|
|
555
|
-
/** Entries read at once
|
|
577
|
+
/** Entries read at once, at most, and no more than `BATCH_BYTES` of what they say: well within a Worker's memory, and a fifth of the round trips of 1000. */
|
|
556
578
|
const VERIFY_BATCH = 5e3;
|
|
557
579
|
/**
|
|
558
580
|
* Check the chain, a batch in memory at a time, and return where it is now
|
|
@@ -611,7 +633,6 @@ async function verifyChain(db, logKeys, shown, anchor, onBatch = () => {}) {
|
|
|
611
633
|
vaultEntries: verified.vaultEntries + result.authenticated,
|
|
612
634
|
vaultKeys: moved.keys
|
|
613
635
|
};
|
|
614
|
-
if (batch.length < 5e3) break;
|
|
615
636
|
}
|
|
616
637
|
return {
|
|
617
638
|
verification: {
|
|
@@ -1061,6 +1082,8 @@ const MESSAGES = {
|
|
|
1061
1082
|
bad_claim: "the key does not belong to this secret",
|
|
1062
1083
|
deleted: "this project or environment was deleted",
|
|
1063
1084
|
ended: "the reference read through was ended",
|
|
1085
|
+
personal: "a personal config's values are its owner's alone",
|
|
1086
|
+
stand_ins: "people use this instance's values through stand-ins, never as values: `coffre run` gives your app them; a service account reads values",
|
|
1064
1087
|
not_allowed: "not allowed to change this",
|
|
1065
1088
|
last_owner: "the last Owner of the whole instance stays one: make someone else an Owner first",
|
|
1066
1089
|
invalid: "not something the rules allow",
|
|
@@ -1161,7 +1184,7 @@ var VaultService = class {
|
|
|
1161
1184
|
underLock: []
|
|
1162
1185
|
};
|
|
1163
1186
|
const result = await decide(d);
|
|
1164
|
-
const entries = [...reports, ...d.log];
|
|
1187
|
+
const entries = [...reports, ...readsByPlace(d.log)];
|
|
1165
1188
|
let at = d.at;
|
|
1166
1189
|
const accessSeq = /* @__PURE__ */ new Map();
|
|
1167
1190
|
if (entries.length > 0) {
|
|
@@ -1184,7 +1207,7 @@ var VaultService = class {
|
|
|
1184
1207
|
};
|
|
1185
1208
|
} catch (error) {
|
|
1186
1209
|
if (!(error instanceof Refused || error instanceof Outage)) throw error;
|
|
1187
|
-
const entries = [...reports, ...error.entries];
|
|
1210
|
+
const entries = [...reports, ...readsByPlace(error.entries)];
|
|
1188
1211
|
if (entries.length > 0) await this.#db.transaction(async (tx) => {
|
|
1189
1212
|
await boundLockWaits(tx, LOCK_TIMEOUT_MS);
|
|
1190
1213
|
await this.#append(tx, entries);
|
|
@@ -1507,6 +1530,7 @@ var VaultService = class {
|
|
|
1507
1530
|
const { principal } = input;
|
|
1508
1531
|
validateText(input.purpose);
|
|
1509
1532
|
const vias = input.items.map((item) => validateVia(item.via));
|
|
1533
|
+
const throughs = input.items.map((item) => validateThrough(item.through));
|
|
1510
1534
|
const loaded = await this.#versions(input, "secret.read", { purpose: input.purpose });
|
|
1511
1535
|
if (!loaded.ok) return loaded;
|
|
1512
1536
|
const items = loaded.versions.map((version) => ({
|
|
@@ -1515,22 +1539,39 @@ var VaultService = class {
|
|
|
1515
1539
|
}));
|
|
1516
1540
|
validateItems(items, "wrapped");
|
|
1517
1541
|
const sealed = await this.#referencesAt(vias);
|
|
1542
|
+
const forks = await environmentsById(this.#db, throughs.flatMap((through) => through === null ? [] : [through]));
|
|
1543
|
+
const viaOk = items.map((item, i) => vias[i] === null || sealed[i] !== null && sealed[i].source.secretId === item.secret.secretId && item.current);
|
|
1544
|
+
const throughOk = items.map((item, i) => {
|
|
1545
|
+
const through = throughs[i];
|
|
1546
|
+
if (through === null) return true;
|
|
1547
|
+
if (!viaOk[i]) return false;
|
|
1548
|
+
const fork = forks.get(through);
|
|
1549
|
+
const decidedAt = sealed[i]?.holder ?? item.secret;
|
|
1550
|
+
return fork !== void 0 && !fork.deleted && !fork.archived && fork.projectId === decidedAt.projectId && through !== decidedAt.environmentId;
|
|
1551
|
+
});
|
|
1518
1552
|
const versionIds = new Map(items.map((item) => [item.secret, item.id]));
|
|
1519
1553
|
const viaOf = new Map(items.map((item, i) => [item.secret, {
|
|
1520
1554
|
via: vias[i],
|
|
1521
|
-
sealed: sealed[i]
|
|
1555
|
+
sealed: sealed[i],
|
|
1556
|
+
through: throughs[i],
|
|
1557
|
+
throughOk: throughOk[i]
|
|
1522
1558
|
}]));
|
|
1523
1559
|
const entry = (secret, decision, code) => {
|
|
1524
|
-
const { via, sealed: seal } = viaOf.get(secret);
|
|
1560
|
+
const { via, sealed: seal, through, throughOk: ok } = viaOf.get(secret);
|
|
1525
1561
|
return {
|
|
1526
1562
|
...keyEntry("secret.read", principal, secret, decision, code, input, {
|
|
1527
1563
|
purpose: input.purpose,
|
|
1528
|
-
...via === null ? {} : viaDetail(via, seal)
|
|
1564
|
+
...via === null ? {} : viaDetail(via, seal),
|
|
1565
|
+
...through === null ? {} : ok ? throughDetail(through, forks.get(through), via !== null) : { through: {
|
|
1566
|
+
environmentId: through,
|
|
1567
|
+
refused: true
|
|
1568
|
+
} }
|
|
1529
1569
|
}),
|
|
1530
1570
|
secretVersionId: versionIds.get(secret)
|
|
1531
1571
|
};
|
|
1532
1572
|
};
|
|
1533
|
-
if (
|
|
1573
|
+
if (viaOk.includes(false)) return this.#refuseVersions(principal, "bad_claim", items.map((item) => entry(item.secret, "deny", "bad_claim")));
|
|
1574
|
+
if (throughOk.includes(false)) return this.#refuseVersions(principal, "bad_claim", items.map((item) => entry(item.secret, "deny", "bad_claim")));
|
|
1534
1575
|
const ends = sealed.flatMap((seal) => seal === null ? [] : [seal.seq]);
|
|
1535
1576
|
const remote = items.some(({ wrapped }) => this.#remote(this.#config.keks.providerOf(wrapped)));
|
|
1536
1577
|
return this.#keys({
|
|
@@ -1546,8 +1587,8 @@ var VaultService = class {
|
|
|
1546
1587
|
remote,
|
|
1547
1588
|
entry,
|
|
1548
1589
|
input
|
|
1549
|
-
}, () => items.map(({ secret, wrapped }) => async (operation) => {
|
|
1550
|
-
const key = await this.#open(wrapped, secret, operation);
|
|
1590
|
+
}, (contextOf) => items.map(({ secret, wrapped }) => async (operation) => {
|
|
1591
|
+
const key = await this.#open(wrapped, contextOf(secret), operation);
|
|
1551
1592
|
return key && {
|
|
1552
1593
|
key,
|
|
1553
1594
|
wipe: () => key.fill(0)
|
|
@@ -1574,11 +1615,11 @@ var VaultService = class {
|
|
|
1574
1615
|
remote: this.#remote(this.#config.keks.primary),
|
|
1575
1616
|
entry,
|
|
1576
1617
|
input
|
|
1577
|
-
}, () => items.map(({ secret, key }) => async (operation) => {
|
|
1618
|
+
}, (contextOf) => items.map(({ secret, key }) => async (operation) => {
|
|
1578
1619
|
const dek = Buffer.from(key, "base64");
|
|
1579
1620
|
try {
|
|
1580
1621
|
return {
|
|
1581
|
-
wrapped: serialisable(await this.#config.keks.wrap(dek,
|
|
1622
|
+
wrapped: serialisable(await this.#config.keks.wrap(dek, contextOf(secret), operation)),
|
|
1582
1623
|
wipe: () => {}
|
|
1583
1624
|
};
|
|
1584
1625
|
} finally {
|
|
@@ -1617,12 +1658,12 @@ var VaultService = class {
|
|
|
1617
1658
|
remote,
|
|
1618
1659
|
entry,
|
|
1619
1660
|
input
|
|
1620
|
-
}, () => items.map(({ secret, wrapped }) => async (operation) => {
|
|
1621
|
-
const key = await this.#open(wrapped, secret, operation);
|
|
1661
|
+
}, (contextOf) => items.map(({ secret, wrapped }) => async (operation) => {
|
|
1662
|
+
const key = await this.#open(wrapped, contextOf(secret), operation);
|
|
1622
1663
|
if (key === null) return null;
|
|
1623
1664
|
try {
|
|
1624
1665
|
return {
|
|
1625
|
-
wrapped: serialisable(await this.#config.keks.wrap(key,
|
|
1666
|
+
wrapped: serialisable(await this.#config.keks.wrap(key, contextOf(secret), operation)),
|
|
1626
1667
|
wipe: () => {}
|
|
1627
1668
|
};
|
|
1628
1669
|
} finally {
|
|
@@ -1679,9 +1720,9 @@ var VaultService = class {
|
|
|
1679
1720
|
* that is not what it says. A key service that cannot answer is an outage,
|
|
1680
1721
|
* not a verdict on the claim, and throws.
|
|
1681
1722
|
*/
|
|
1682
|
-
async #open(wrapped,
|
|
1723
|
+
async #open(wrapped, context, operation) {
|
|
1683
1724
|
try {
|
|
1684
|
-
return await this.#config.keks.unwrap(unwrappable(wrapped), context
|
|
1725
|
+
return await this.#config.keks.unwrap(unwrappable(wrapped), context, operation);
|
|
1685
1726
|
} catch (error) {
|
|
1686
1727
|
if (error instanceof KekBadClaimError) return null;
|
|
1687
1728
|
throw error;
|
|
@@ -1705,14 +1746,24 @@ var VaultService = class {
|
|
|
1705
1746
|
if (wrongKek !== null) return this.#decide([], async () => {
|
|
1706
1747
|
throw new Refused(refusal("wrong_kek", wrongKek), secrets.map((secret) => entry(secret, "deny", "wrong_kek")));
|
|
1707
1748
|
});
|
|
1749
|
+
const owners = /* @__PURE__ */ new Map();
|
|
1708
1750
|
const check = async (d) => {
|
|
1709
1751
|
const reader = through(await this.#standing(d.tx, principal, d.members.get(principal), d.at, d.reports), call.input);
|
|
1710
1752
|
const at = secrets.map((secret, i) => call.places?.[i] ?? secret);
|
|
1711
1753
|
const environments = await environmentsById(d.tx, [...secrets, ...at].map((place) => place.environmentId));
|
|
1754
|
+
const parents = [...environments.values()].flatMap((environment) => environment.owner === null || environment.parentId === null ? [] : [environment.parentId]);
|
|
1755
|
+
for (const [id, parent] of await environmentsById(d.tx, parents)) environments.set(id, parent);
|
|
1712
1756
|
const gone = (place) => environments.get(place.environmentId)?.deleted ?? false;
|
|
1713
|
-
|
|
1757
|
+
owners.clear();
|
|
1758
|
+
for (const [id, environment] of environments) if (environment.owner !== null) owners.set(id, ownerKey(environment.owner));
|
|
1759
|
+
const codes = secrets.map((secret, i) => {
|
|
1760
|
+
const personal = personalRefusal(reader, principal, call.permission, secret, at[i], environments);
|
|
1761
|
+
if (personal !== void 0) return personal;
|
|
1762
|
+
return refuses(reader, call.permission, placeIn(at[i], environments), gone(secret) || gone(at[i]));
|
|
1763
|
+
});
|
|
1714
1764
|
if (call.vet !== void 0 && !codes.some((code) => code !== null)) (await call.vet(d.tx)).forEach((code, i) => codes[i] = code);
|
|
1715
1765
|
let first = codes.find((code) => code !== null) ?? null;
|
|
1766
|
+
if (first === null && action === "secret.read" && this.#config.people === "stand-ins" && principal.startsWith("user:") && HANDED_BACK.includes(call.input.purpose)) first = "stand_ins";
|
|
1716
1767
|
if (first === null && action === "secret.read" && await this.#overBulkLimit(d, principal, secrets.length)) first = "bulk_limit";
|
|
1717
1768
|
if (first !== null) throw new Refused(refusal(first, MESSAGES[first]), secrets.map((secret, i) => entry(secret, "deny", codes[i] ?? first)));
|
|
1718
1769
|
};
|
|
@@ -1774,7 +1825,7 @@ var VaultService = class {
|
|
|
1774
1825
|
if (first === void 0 || first === null) return;
|
|
1775
1826
|
throw new Refused(refusal(first, MESSAGES[first]), secrets.map((secret, item) => outcomeEntry(secret, item, "deny", codes[item] ?? first)));
|
|
1776
1827
|
});
|
|
1777
|
-
const { outcomes, expired } = await settle(operations(), this.#prepared.options.keyBudgetMs);
|
|
1828
|
+
const { outcomes, expired } = await settle(operations((secret) => context(secret, owners.get(secret.environmentId))), this.#prepared.options.keyBudgetMs);
|
|
1778
1829
|
const done = outcomes.flatMap((outcome) => outcome.ok ? [outcome.value] : []);
|
|
1779
1830
|
try {
|
|
1780
1831
|
const entries = () => secrets.map((secret, i) => outcomeEntry(secret, i, "deny", outcomes[i].ok ? "withheld" : outcomes[i].code));
|
|
@@ -1861,6 +1912,7 @@ var VaultService = class {
|
|
|
1861
1912
|
const source = sources.get(item.source.secretId);
|
|
1862
1913
|
if (source === void 0 || projectOf.get(item.holder.environmentId) !== item.holder.projectId) return "bad_claim";
|
|
1863
1914
|
if (source.secretId === item.holder.secretId) return "invalid";
|
|
1915
|
+
if (environments.get(item.holder.environmentId)?.owner != null || environments.get(source.environmentId)?.owner != null) return "personal";
|
|
1864
1916
|
return refuses(reader, "secret.write", placeIn(item.holder, environments), gone(item.holder)) ?? refuses(reader, "secret.read", placeIn(source, environments), gone(source));
|
|
1865
1917
|
});
|
|
1866
1918
|
const first = codes.find((code) => code !== null) ?? null;
|
|
@@ -2965,6 +3017,7 @@ var VaultService = class {
|
|
|
2965
3017
|
let keys = [];
|
|
2966
3018
|
for (let after = -1n;;) {
|
|
2967
3019
|
const batch = await vaultEntriesAfter(tx, after, VERIFY_BATCH);
|
|
3020
|
+
if (batch.length === 0) return { ok: true };
|
|
2968
3021
|
for (const entry of batch) {
|
|
2969
3022
|
const result = verifyEntries([entry], {
|
|
2970
3023
|
startSeq: entry.seq,
|
|
@@ -2985,7 +3038,6 @@ var VaultService = class {
|
|
|
2985
3038
|
};
|
|
2986
3039
|
keys = moved.keys;
|
|
2987
3040
|
for (const entry of batch) findings.add(entry);
|
|
2988
|
-
if (batch.length < 5e3) return { ok: true };
|
|
2989
3041
|
after = batch[batch.length - 1].seq;
|
|
2990
3042
|
}
|
|
2991
3043
|
}
|
|
@@ -3211,6 +3263,30 @@ function validateVia(via) {
|
|
|
3211
3263
|
seq: via.seq
|
|
3212
3264
|
};
|
|
3213
3265
|
}
|
|
3266
|
+
/** A read's fork, checked for shape: null for none. */
|
|
3267
|
+
function validateThrough(through) {
|
|
3268
|
+
if (through === void 0 || through === null) return null;
|
|
3269
|
+
if (typeof through !== "string" || !UUID.test(through)) throw new Error("a fork's id must be a lowercase UUID");
|
|
3270
|
+
return through;
|
|
3271
|
+
}
|
|
3272
|
+
/**
|
|
3273
|
+
* How a read of an inherited value is logged: the fork it was read in,
|
|
3274
|
+
* `through`, and as `also`, which puts it in the fork's log too, unless it
|
|
3275
|
+
* was read through the parent's reference. The fork decides nothing: the
|
|
3276
|
+
* reader's grants where the value is held do, as for any read.
|
|
3277
|
+
*/
|
|
3278
|
+
function throughDetail(through, fork, viaReference) {
|
|
3279
|
+
return {
|
|
3280
|
+
through: {
|
|
3281
|
+
environmentId: through,
|
|
3282
|
+
...fork === void 0 ? {} : { slug: fork.slug }
|
|
3283
|
+
},
|
|
3284
|
+
...viaReference ? {} : { also: {
|
|
3285
|
+
projectId: fork?.projectId ?? null,
|
|
3286
|
+
environmentId: through
|
|
3287
|
+
} }
|
|
3288
|
+
};
|
|
3289
|
+
}
|
|
3214
3290
|
/**
|
|
3215
3291
|
* How a read through a reference is logged: the way in, and `also`, the
|
|
3216
3292
|
* holder's place, which puts the read in the holder's project's log too.
|
|
@@ -3254,6 +3330,26 @@ function placeIn(place, environments) {
|
|
|
3254
3330
|
* deleted place is refused before any grant is asked, so no kind of grant,
|
|
3255
3331
|
* on the project, the environment or every project, can reach into one.
|
|
3256
3332
|
*/
|
|
3333
|
+
/**
|
|
3334
|
+
* A key of a personal config is its owner's alone (docs/design/forking.md,
|
|
3335
|
+
* section 4): no grant or role opens or wraps it, an Owner's included,
|
|
3336
|
+
* only its owner, while they read the environment it is a config of,
|
|
3337
|
+
* whatever the call is. Nor does any reference reach into or out of one.
|
|
3338
|
+
* Undefined for a key of any other environment, decided as ever.
|
|
3339
|
+
*/
|
|
3340
|
+
function personalRefusal(reader, principal, permission, secret, at, environments) {
|
|
3341
|
+
const own = environments.get(secret.environmentId);
|
|
3342
|
+
const held = at.environmentId === secret.environmentId ? own : environments.get(at.environmentId);
|
|
3343
|
+
if (own?.owner == null && held?.owner == null) return void 0;
|
|
3344
|
+
if (own !== held || principal !== own?.owner || own.parentId === null) return "personal";
|
|
3345
|
+
const parent = placeIn({
|
|
3346
|
+
projectId: secret.projectId,
|
|
3347
|
+
environmentId: own.parentId
|
|
3348
|
+
}, environments);
|
|
3349
|
+
const refused = refuses(reader, "secret.read", parent, own.deleted || (environments.get(own.parentId)?.deleted ?? true));
|
|
3350
|
+
if (refused !== null) return refused;
|
|
3351
|
+
return withinCredential(reader.live, permission, parent) ? null : "no_grant";
|
|
3352
|
+
}
|
|
3257
3353
|
function refuses(reader, permission, where, deleted) {
|
|
3258
3354
|
if (reader.status === "tampered") return "tampered";
|
|
3259
3355
|
if (reader.status === "removed") return "removed";
|
|
@@ -3286,6 +3382,89 @@ function keyEntry(action, principal, secret, decision, code, correlation, detail
|
|
|
3286
3382
|
})
|
|
3287
3383
|
};
|
|
3288
3384
|
}
|
|
3385
|
+
/** What every secret of one read at a place shares, said once in its entry rather than in each of its `secrets`. */
|
|
3386
|
+
const READ_SHARED = [
|
|
3387
|
+
"purpose",
|
|
3388
|
+
"intent",
|
|
3389
|
+
"credentialId",
|
|
3390
|
+
"through"
|
|
3391
|
+
];
|
|
3392
|
+
/**
|
|
3393
|
+
* A decision's `secret.read` entries, made one per secret, as the log keeps
|
|
3394
|
+
* them: one per place it was decided at, the environment read and, through
|
|
3395
|
+
* a reference, the holder's, or the fork that inherits it (`also`,
|
|
3396
|
+
* `through`). An entry about several secrets lists
|
|
3397
|
+
* them in `secrets`, each with what its own entry would have said: its
|
|
3398
|
+
* `secretId`, `secretVersionId`, `subject`, `version`, and its `via`, holder
|
|
3399
|
+
* (`also.secretId`) and intent `item` when it has them. An entry about one
|
|
3400
|
+
* secret is as it always was. A `coffre run` of a hundred keys is one entry,
|
|
3401
|
+
* not a hundred; what it decided, and for which keys, is the same. Other
|
|
3402
|
+
* entries pass through, in order.
|
|
3403
|
+
*/
|
|
3404
|
+
function readsByPlace(entries) {
|
|
3405
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3406
|
+
const out = [];
|
|
3407
|
+
for (const entry of entries) {
|
|
3408
|
+
if (entry.action !== "secret.read") {
|
|
3409
|
+
out.push(entry);
|
|
3410
|
+
continue;
|
|
3411
|
+
}
|
|
3412
|
+
const { also, through } = JSON.parse(entry.metadata ?? "{}");
|
|
3413
|
+
const place = JSON.stringify([
|
|
3414
|
+
entry.actor,
|
|
3415
|
+
entry.decision,
|
|
3416
|
+
entry.code ?? null,
|
|
3417
|
+
entry.projectId ?? null,
|
|
3418
|
+
entry.environmentId ?? null,
|
|
3419
|
+
entry.operationId ?? null,
|
|
3420
|
+
entry.requestId ?? null,
|
|
3421
|
+
String(entry.relatedSeq ?? null),
|
|
3422
|
+
also?.projectId ?? null,
|
|
3423
|
+
also?.environmentId ?? null,
|
|
3424
|
+
through ?? null
|
|
3425
|
+
]);
|
|
3426
|
+
const group = groups.get(place);
|
|
3427
|
+
if (group !== void 0) {
|
|
3428
|
+
group.push(entry);
|
|
3429
|
+
continue;
|
|
3430
|
+
}
|
|
3431
|
+
const fresh = [entry];
|
|
3432
|
+
groups.set(place, fresh);
|
|
3433
|
+
out.push(fresh);
|
|
3434
|
+
}
|
|
3435
|
+
return out.map((item) => Array.isArray(item) ? oneRead(item) : item);
|
|
3436
|
+
}
|
|
3437
|
+
/** Entries of one read at one place as one entry: `readsByPlace`. */
|
|
3438
|
+
function oneRead(group) {
|
|
3439
|
+
const [first] = group;
|
|
3440
|
+
if (group.length === 1) return first;
|
|
3441
|
+
const details = group.map((entry) => JSON.parse(entry.metadata ?? "{}"));
|
|
3442
|
+
const shared = Object.fromEntries(READ_SHARED.flatMap((field) => details[0][field] === void 0 ? [] : [[field, details[0][field]]]));
|
|
3443
|
+
const beside = details[0].also;
|
|
3444
|
+
const secrets = group.map((entry, i) => {
|
|
3445
|
+
const own = Object.fromEntries(Object.entries(details[i]).filter(([field]) => !READ_SHARED.includes(field) && field !== "also"));
|
|
3446
|
+
const holder = details[i].also?.secretId;
|
|
3447
|
+
return {
|
|
3448
|
+
...entry.secretId == null ? {} : { secretId: entry.secretId },
|
|
3449
|
+
...entry.secretVersionId == null ? {} : { secretVersionId: entry.secretVersionId },
|
|
3450
|
+
...own,
|
|
3451
|
+
...holder === void 0 ? {} : { also: { secretId: holder } }
|
|
3452
|
+
};
|
|
3453
|
+
});
|
|
3454
|
+
return {
|
|
3455
|
+
...first,
|
|
3456
|
+
secretId: null,
|
|
3457
|
+
secretVersionId: null,
|
|
3458
|
+
metadata: JSON.stringify({
|
|
3459
|
+
...shared,
|
|
3460
|
+
...beside === void 0 ? {} : { also: {
|
|
3461
|
+
projectId: beside.projectId,
|
|
3462
|
+
environmentId: beside.environmentId
|
|
3463
|
+
} },
|
|
3464
|
+
secrets
|
|
3465
|
+
})
|
|
3466
|
+
};
|
|
3467
|
+
}
|
|
3289
3468
|
/** Whether `entry` changes a member's access: what their row's `access_seq` names. */
|
|
3290
3469
|
function isAccessEntry(entry) {
|
|
3291
3470
|
return entry.decision === "allow" && entry.subjectPrincipal !== void 0 && entry.subjectPrincipal !== null && ACCESS_ACTIONS.includes(entry.action);
|
|
@@ -3366,11 +3545,13 @@ function view(grant) {
|
|
|
3366
3545
|
function iso(ms) {
|
|
3367
3546
|
return new Date(ms).toISOString();
|
|
3368
3547
|
}
|
|
3369
|
-
|
|
3548
|
+
/** A secret's binding context: its ids, and a personal config's owner, hashed, as the decision read it. */
|
|
3549
|
+
function context(secret, owner) {
|
|
3370
3550
|
return {
|
|
3371
3551
|
projectId: secret.projectId,
|
|
3372
3552
|
environmentId: secret.environmentId,
|
|
3373
|
-
secretId: secret.secretId
|
|
3553
|
+
secretId: secret.secretId,
|
|
3554
|
+
...owner === void 0 ? {} : { owner }
|
|
3374
3555
|
};
|
|
3375
3556
|
}
|
|
3376
3557
|
function unwrappable(wrapped) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffre/vault",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "coffre's vault: keys, grants and members, in the database the app uses, as a Cloudflare Worker or on Node.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"drizzle-orm": "0.45.2",
|
|
39
|
-
"@coffre/core": "0.
|
|
40
|
-
"@coffre/db": "0.
|
|
39
|
+
"@coffre/core": "0.8.0",
|
|
40
|
+
"@coffre/db": "0.8.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "26.1.1",
|