@wtfalch/keys 0.1.0 → 0.3.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/README.md +25 -1
- package/dist/held/index.d.ts +9 -6
- package/dist/held/index.js +77 -18
- package/dist/issued/index.d.ts +29 -3
- package/dist/issued/index.js +160 -57
- package/dist/issued/ratelimit.d.ts +34 -0
- package/dist/issued/ratelimit.js +49 -0
- package/dist/issued/secret.js +19 -5
- package/dist/issued/tables.d.ts +61 -0
- package/dist/issued/tables.js +13 -0
- package/dist/migrations/0004_keys_security.sql +246 -0
- package/dist/migrations/0005_keys_environment.sql +52 -0
- package/dist/migrations/0006_keys_usage.sql +26 -0
- package/dist/migrations/0007_keys_rate_limit.sql +20 -0
- package/dist/webhooks.d.ts +55 -0
- package/dist/webhooks.js +51 -0
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ authentication or authorisation of its own — every check runs through
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
pnpm add @wtfalch/keys
|
|
12
|
+
pnpm add @wtfalch/keys drizzle-orm@^0.45.2
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
## Dev
|
|
@@ -19,3 +19,27 @@ pnpm build
|
|
|
19
19
|
pnpm typecheck
|
|
20
20
|
pnpm test
|
|
21
21
|
```
|
|
22
|
+
|
|
23
|
+
## Security upgrade
|
|
24
|
+
|
|
25
|
+
Apply the additive `0004_keys_security.sql` migration in every consumer database before relying on the updated guards. All issuer processes must upgrade together for issuance/cascade serialization. See [the remediation and rollout notes](../../security/REMEDIATION.md).
|
|
26
|
+
|
|
27
|
+
New bearer secrets have a 32-character base32 tail (160 random bits). Existing 20-character tails remain accepted. Rotation refuses invalid stored signatures and returns `conflict` if the row changes during signing.
|
|
28
|
+
|
|
29
|
+
Held keys use active cache expiry (60 seconds by default) and retain at most 256 versions per instance. Set `cacheSeconds: 0` or `maxCacheEntries: 0` to disable caching. Call `dispose()` to zero cached keys, clear timers and refuse further operations. A local forget/revocation invalidates pending opens; other processes remain bounded by their own cache TTL. Plaintext callbacks can copy or return values, so their lifetime cannot be enforced by the SDK.
|
|
30
|
+
|
|
31
|
+
## Issued-key environments
|
|
32
|
+
|
|
33
|
+
Every issued credential carries a built-in `environment`, `'live'` or `'test'` (default `'live'`), passed on `issue()` and reported by `check()` and `lineageOf()`. A credential can only mint a child in its own environment — `issue()` returns `{ ok: false, reason: 'environment_mismatch' }` otherwise. Apply `0005_keys_environment.sql` alongside `0004_keys_security.sql`.
|
|
34
|
+
|
|
35
|
+
## Issued-key usage tracking
|
|
36
|
+
|
|
37
|
+
Every successful `check()` stamps `lastUsedAt` and increments `useCount` on the credential's row — read them off `KeysIssuedCredentialRow` directly (an admin UI's own query, not a new `check()` return field). The write is best-effort: it never turns a genuine secret into a refusal, and a revoked, expired, tampered or wrong-secret check never bumps it. Apply `0006_keys_usage.sql` alongside `0004_keys_security.sql` and `0005_keys_environment.sql`.
|
|
38
|
+
|
|
39
|
+
## Per-issued-key rate limits
|
|
40
|
+
|
|
41
|
+
`CreateCredentialIssuerOptions.rateLimiter` (optional) is consulted on every `check()`, keyed by the credential's own `id` — spent on every attempt against a known credential, matched secret or not, so an unlimited number of wrong guesses can't get around it. Refused as `{ ok: false, reason: 'rate_limited', retryAfterMs }`. `createPostgresRateLimiter({ db, limitPerWindow, windowMs? })` is a ready-made fixed-window limiter backed by the host's own Postgres (`0007_keys_rate_limit.sql`, applied alongside `0004`–`0006`); bring your own `RateLimiter` for anything else (Redis, D1, …). Omit it and `check()` behaves exactly as before this existed — this was previously only enforced on the host's own service credential (`packages/worker/src/ratelimit.ts`), never on an individual end-user issued key.
|
|
42
|
+
|
|
43
|
+
## Outbound webhooks
|
|
44
|
+
|
|
45
|
+
`@wtfalch/keys/webhooks`'s `createWebhookDispatcher({ url, secret? })` returns a function that POSTs a lifecycle event as JSON — a drop-in for `./issued`'s `audit` option (`credential.minted` / `.rotated` / `.revoked`) or `./held`'s (`key.used`). With `secret`, the body is HMAC-SHA256-signed into an `X-Keys-Signature` header; the receiver checks it with `verifyWebhookSignature(body, signature, secret)`. Delivery is fire-and-forget and best-effort — a subscriber outage never fails the call that produced the event, it only calls `onDeliveryError`.
|
package/dist/held/index.d.ts
CHANGED
|
@@ -18,11 +18,11 @@ export type { ShredClock, ShredClockReason } from './shred.js';
|
|
|
18
18
|
export type HeldKeysDb = PgDatabase<PgQueryResultHKT, any, any>;
|
|
19
19
|
/**
|
|
20
20
|
* Every way `./held` refuses a call. `code` carries the Worker's own
|
|
21
|
-
* `WorkerErrorCode` verbatim when the Worker is what refused; the
|
|
21
|
+
* `WorkerErrorCode` verbatim when the Worker is what refused; the
|
|
22
22
|
* local codes are refusals this package makes itself, never sent by a
|
|
23
23
|
* Worker.
|
|
24
24
|
*/
|
|
25
|
-
export type HeldKeysErrorCode = WorkerErrorCode | 'not_found' | 'revoked' | 'unseal_failed';
|
|
25
|
+
export type HeldKeysErrorCode = WorkerErrorCode | 'not_found' | 'revoked' | 'unseal_failed' | 'disposed' | 'invalidated';
|
|
26
26
|
/**
|
|
27
27
|
* Thrown by `put`, `open` and `rewrap`. Never carries a value or a key --
|
|
28
28
|
* only what a caller can act on: which entry, which code, and (for
|
|
@@ -66,8 +66,10 @@ export interface CreateHeldKeysOptions {
|
|
|
66
66
|
readonly worker: Pick<WorkerClient, 'wrap' | 'unwrap' | 'rewrap'>;
|
|
67
67
|
readonly credential: SerializedCredential;
|
|
68
68
|
readonly audit: (event: KeyUsedEvent) => void | Promise<void>;
|
|
69
|
-
/** Seconds
|
|
69
|
+
/** Seconds before active cache eviction and key zeroing. Default 60 (#224). 0 disables caching. */
|
|
70
70
|
readonly cacheSeconds?: number;
|
|
71
|
+
/** Maximum cached versions. Default 256. Zero disables caching. */
|
|
72
|
+
readonly maxCacheEntries?: number;
|
|
71
73
|
/** Unix milliseconds. Defaults to `Date.now`; tests pass a fake clock to move it without waiting. */
|
|
72
74
|
readonly now?: () => number;
|
|
73
75
|
}
|
|
@@ -77,9 +79,8 @@ export interface HeldKeys {
|
|
|
77
79
|
readonly version: number;
|
|
78
80
|
}>;
|
|
79
81
|
/**
|
|
80
|
-
* Opens one version and hands the plaintext to `use
|
|
81
|
-
*
|
|
82
|
-
* variable that outlives the call -- but it cannot zero a JS string: a
|
|
82
|
+
* Opens one version and hands the plaintext to `use`. The callback can
|
|
83
|
+
* copy or return the value; this API cannot enforce its lifetime. A
|
|
83
84
|
* `.toString()`, a template literal, or a copy `use` itself makes can
|
|
84
85
|
* still outlive this call. That is a limit of the language, not a gap in
|
|
85
86
|
* this function.
|
|
@@ -87,6 +88,8 @@ export interface HeldKeys {
|
|
|
87
88
|
open<T>(binding: KeyBinding, use: (value: string) => Promise<T>): Promise<T>;
|
|
88
89
|
/** Drops and zeroes one cached data key, if one is cached for this exact version. */
|
|
89
90
|
forget(binding: KeyBinding): void;
|
|
91
|
+
/** Zero all cached keys, cancel expiry timers, and refuse subsequent operations. */
|
|
92
|
+
dispose(): void;
|
|
90
93
|
/**
|
|
91
94
|
* The rewrap sweep: moves every version still under `kekIdOld` onto
|
|
92
95
|
* `kekIdNew`. The raw data key never reaches this process.
|
package/dist/held/index.js
CHANGED
|
@@ -65,14 +65,25 @@ function gcmOpen(dataKey, iv, ciphertext, aad) {
|
|
|
65
65
|
export function createHeldKeys(options) {
|
|
66
66
|
const { db, worker, credential, audit } = options;
|
|
67
67
|
const cacheMs = (options.cacheSeconds ?? 60) * 1000;
|
|
68
|
+
const maxCacheEntries = options.maxCacheEntries ?? 256;
|
|
69
|
+
if (!Number.isFinite(cacheMs) || cacheMs < 0 || cacheMs > 2_147_483_647) {
|
|
70
|
+
throw new RangeError('cacheSeconds must be finite, nonnegative and at most 2147483');
|
|
71
|
+
}
|
|
72
|
+
if (!Number.isSafeInteger(maxCacheEntries) || maxCacheEntries < 0) {
|
|
73
|
+
throw new RangeError('maxCacheEntries must be a nonnegative safe integer');
|
|
74
|
+
}
|
|
68
75
|
const now = options.now ?? Date.now;
|
|
76
|
+
let disposed = false;
|
|
77
|
+
let epoch = 0;
|
|
69
78
|
const cache = new Map();
|
|
70
79
|
function evict(key, entry) {
|
|
71
80
|
// Not a guarantee -- the runtime may have copied it -- but it shortens
|
|
72
81
|
// the window in which a heap dump holds an evicted data key, same as
|
|
73
82
|
// app-template's envelope.ts.
|
|
83
|
+
clearTimeout(entry.timer);
|
|
74
84
|
entry.dataKey.fill(0);
|
|
75
|
-
cache.
|
|
85
|
+
if (cache.get(key) === entry)
|
|
86
|
+
cache.delete(key);
|
|
76
87
|
}
|
|
77
88
|
function cachedFor(binding) {
|
|
78
89
|
if (cacheMs <= 0)
|
|
@@ -89,12 +100,40 @@ export function createHeldKeys(options) {
|
|
|
89
100
|
}
|
|
90
101
|
return hit;
|
|
91
102
|
}
|
|
92
|
-
function remember(binding, dataKey, iv, ciphertext) {
|
|
93
|
-
if (cacheMs <= 0)
|
|
103
|
+
function remember(binding, dataKey, iv, ciphertext, until) {
|
|
104
|
+
if (cacheMs <= 0 || maxCacheEntries === 0 || until <= now())
|
|
94
105
|
return;
|
|
95
|
-
|
|
106
|
+
const key = cacheKeyOf(binding);
|
|
107
|
+
const previous = cache.get(key);
|
|
108
|
+
if (previous)
|
|
109
|
+
evict(key, previous);
|
|
110
|
+
while (cache.size >= maxCacheEntries) {
|
|
111
|
+
const oldest = cache.entries().next().value;
|
|
112
|
+
if (oldest)
|
|
113
|
+
evict(oldest[0], oldest[1]);
|
|
114
|
+
}
|
|
115
|
+
const entry = {
|
|
116
|
+
dataKey: new Uint8Array(dataKey),
|
|
117
|
+
iv,
|
|
118
|
+
ciphertext,
|
|
119
|
+
until,
|
|
120
|
+
timer: setTimeout(() => evict(key, entry), Math.max(0, until - now())),
|
|
121
|
+
};
|
|
122
|
+
entry.timer.unref?.();
|
|
123
|
+
cache.set(key, entry);
|
|
124
|
+
}
|
|
125
|
+
function requireActive() {
|
|
126
|
+
if (disposed)
|
|
127
|
+
throw new HeldKeysError('disposed');
|
|
128
|
+
}
|
|
129
|
+
function dispose() {
|
|
130
|
+
disposed = true;
|
|
131
|
+
epoch++;
|
|
132
|
+
for (const [key, hit] of cache)
|
|
133
|
+
evict(key, hit);
|
|
96
134
|
}
|
|
97
135
|
function forget(binding) {
|
|
136
|
+
epoch++;
|
|
98
137
|
const key = cacheKeyOf(binding);
|
|
99
138
|
const hit = cache.get(key);
|
|
100
139
|
if (hit)
|
|
@@ -102,6 +141,7 @@ export function createHeldKeys(options) {
|
|
|
102
141
|
}
|
|
103
142
|
/** Every cached version of one entry, evicted -- used on revoke, where the caller does not name a version. */
|
|
104
143
|
function forgetEntry(entry) {
|
|
144
|
+
epoch++;
|
|
105
145
|
const prefix = `${entry.tenantId} ${entry.entryId} `;
|
|
106
146
|
for (const [key, hit] of cache) {
|
|
107
147
|
if (key.startsWith(prefix))
|
|
@@ -109,6 +149,7 @@ export function createHeldKeys(options) {
|
|
|
109
149
|
}
|
|
110
150
|
}
|
|
111
151
|
async function put(entry, value) {
|
|
152
|
+
requireActive();
|
|
112
153
|
requireId(entry.tenantId, 'tenantId');
|
|
113
154
|
requireId(entry.entryId, 'entryId');
|
|
114
155
|
// Reserve the next version number under a row lock, and nothing else:
|
|
@@ -181,6 +222,8 @@ export function createHeldKeys(options) {
|
|
|
181
222
|
return { version };
|
|
182
223
|
}
|
|
183
224
|
async function open(binding, use) {
|
|
225
|
+
requireActive();
|
|
226
|
+
const openingEpoch = epoch;
|
|
184
227
|
requireId(binding.tenantId, 'tenantId');
|
|
185
228
|
requireId(binding.entryId, 'entryId');
|
|
186
229
|
requireVersion(binding.version);
|
|
@@ -224,20 +267,33 @@ export function createHeldKeys(options) {
|
|
|
224
267
|
if (!result.ok)
|
|
225
268
|
throw HeldKeysError.fromWorker(result.error);
|
|
226
269
|
const dataKey = result.dataKey;
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
270
|
+
const until = now() + cacheMs;
|
|
271
|
+
try {
|
|
272
|
+
requireActive();
|
|
273
|
+
if (epoch !== openingEpoch)
|
|
274
|
+
throw new HeldKeysError('invalidated');
|
|
275
|
+
const value = gcmOpen(dataKey, versionRow.iv, versionRow.ciphertext, encodeAad(binding));
|
|
276
|
+
await audit({
|
|
277
|
+
name: 'key.used',
|
|
278
|
+
tenantId: binding.tenantId,
|
|
279
|
+
entryId: binding.entryId,
|
|
280
|
+
version: binding.version,
|
|
281
|
+
requestId,
|
|
282
|
+
at: now(),
|
|
283
|
+
});
|
|
284
|
+
requireActive();
|
|
285
|
+
if (epoch !== openingEpoch)
|
|
286
|
+
throw new HeldKeysError('invalidated');
|
|
287
|
+
remember(binding, dataKey, versionRow.iv, versionRow.ciphertext, until);
|
|
288
|
+
dataKey.fill(0);
|
|
289
|
+
return use(value);
|
|
290
|
+
}
|
|
291
|
+
finally {
|
|
292
|
+
dataKey.fill(0);
|
|
293
|
+
}
|
|
239
294
|
}
|
|
240
295
|
async function rewrap(rewrapOptions) {
|
|
296
|
+
requireActive();
|
|
241
297
|
const { kekIdOld, kekIdNew } = rewrapOptions;
|
|
242
298
|
requireId(kekIdOld, 'kekIdOld');
|
|
243
299
|
requireId(kekIdNew, 'kekIdNew');
|
|
@@ -271,10 +327,11 @@ export function createHeldKeys(options) {
|
|
|
271
327
|
await db
|
|
272
328
|
.update(keysHeldVersions)
|
|
273
329
|
.set({ kekId: kekIdNew, wrappedKey: Buffer.from(result.wrappedKey) })
|
|
274
|
-
.where(and(eq(keysHeldVersions.tenantId, binding.tenantId), eq(keysHeldVersions.entryId, binding.entryId), eq(keysHeldVersions.version, binding.version)));
|
|
330
|
+
.where(and(eq(keysHeldVersions.tenantId, binding.tenantId), eq(keysHeldVersions.entryId, binding.entryId), eq(keysHeldVersions.version, binding.version), eq(keysHeldVersions.kekId, kekIdOld), eq(keysHeldVersions.wrappedKey, row.wrappedKey)));
|
|
275
331
|
}
|
|
276
332
|
}
|
|
277
333
|
async function revokeEntry(entry) {
|
|
334
|
+
requireActive();
|
|
278
335
|
requireId(entry.tenantId, 'tenantId');
|
|
279
336
|
requireId(entry.entryId, 'entryId');
|
|
280
337
|
// The `revoked_at is null` guard makes a second call a no-op rather than
|
|
@@ -288,6 +345,7 @@ export function createHeldKeys(options) {
|
|
|
288
345
|
forgetEntry(entry);
|
|
289
346
|
}
|
|
290
347
|
async function archiveTenant(tenantId) {
|
|
348
|
+
requireActive();
|
|
291
349
|
requireId(tenantId, 'tenantId');
|
|
292
350
|
// Every row for this tenant not already archived, in one statement --
|
|
293
351
|
// the `is null` guard is what makes a second call a no-op per row
|
|
@@ -298,11 +356,12 @@ export function createHeldKeys(options) {
|
|
|
298
356
|
.update(keysHeldEntries)
|
|
299
357
|
.set({ tenantArchivedAt: sql `now()` })
|
|
300
358
|
.where(and(eq(keysHeldEntries.tenantId, tenantId), sql `${keysHeldEntries.tenantArchivedAt} is null`));
|
|
359
|
+
epoch++;
|
|
301
360
|
const prefix = `${tenantId} `;
|
|
302
361
|
for (const [key, hit] of cache) {
|
|
303
362
|
if (key.startsWith(prefix))
|
|
304
363
|
evict(key, hit);
|
|
305
364
|
}
|
|
306
365
|
}
|
|
307
|
-
return { put, open, forget, rewrap, revokeEntry, archiveTenant };
|
|
366
|
+
return { put, open, forget, dispose, rewrap, revokeEntry, archiveTenant };
|
|
308
367
|
}
|
package/dist/issued/index.d.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import type { PgDatabase, PgQueryResultHKT } from 'drizzle-orm/pg-core';
|
|
2
2
|
import type { SerializedCredential, VerifyKeys, WorkerClient, WorkerErrorCode } from '../worker-contract.js';
|
|
3
|
+
import type { RateLimiter } from './ratelimit.js';
|
|
3
4
|
export { ID_PATTERN, SIGNED_PREFIX, type RequestMeta, type SerializedCredential, type SignableRow, type SignedKind, type SignedRow, type SignRowRequest, type SignRowResult, type SignRowSuccess, type VerifyKeys, type WorkerClient, type WorkerError, type WorkerErrorCode, type WorkerFailure, } from '../worker-contract.js';
|
|
5
|
+
export { createPostgresRateLimiter, type CreatePostgresRateLimiterOptions, type RateLimiter, type RateLimitResult, } from './ratelimit.js';
|
|
4
6
|
export type Db = PgDatabase<PgQueryResultHKT, any, any>;
|
|
7
|
+
/**
|
|
8
|
+
* #47: a built-in sandbox/live split, so a stamped host does not invent its
|
|
9
|
+
* own convention on top of `prefix`. Stored on the row (`environment`,
|
|
10
|
+
* immutable once set) but never signed -- `prefix` alone still decides what
|
|
11
|
+
* a presented secret is, same as before this existed.
|
|
12
|
+
*/
|
|
13
|
+
export type KeyEnvironment = 'live' | 'test';
|
|
5
14
|
export interface AuditEvent {
|
|
6
15
|
readonly name: 'credential.minted' | 'credential.rotated' | 'credential.revoked';
|
|
7
16
|
readonly credentialId: string;
|
|
@@ -22,6 +31,13 @@ export interface CreateCredentialIssuerOptions<TGrant> {
|
|
|
22
31
|
readonly audit?: AuditCallback;
|
|
23
32
|
/** This host's split-secret prefix, e.g. `ai_live_`. Fixed per issuer; never inferred from a row. */
|
|
24
33
|
readonly prefix: string;
|
|
34
|
+
/**
|
|
35
|
+
* #45: an abuse limit `check()` consults per credential `id`, spent on
|
|
36
|
+
* every attempt (matched secret or not) once a row is found. Omit it and
|
|
37
|
+
* `check()` behaves exactly as before this existed. `createPostgresRateLimiter`
|
|
38
|
+
* (this module) is a ready-made one backed by the host's own `db`.
|
|
39
|
+
*/
|
|
40
|
+
readonly rateLimiter?: RateLimiter;
|
|
25
41
|
/** Unix milliseconds. Defaults to `Date.now`; a test passes a fake clock. */
|
|
26
42
|
readonly now?: () => number;
|
|
27
43
|
}
|
|
@@ -32,8 +48,12 @@ export interface IssueRequest<TGrant> {
|
|
|
32
48
|
/** Unix milliseconds. Required: refused when missing or already past, never clamped to the minter's own. */
|
|
33
49
|
readonly expiresAt: number;
|
|
34
50
|
readonly idempotencyKey?: string;
|
|
51
|
+
/** Default `'live'`. A child must match its minter's environment -- see `IssueRefusalReason`. */
|
|
52
|
+
readonly environment?: KeyEnvironment;
|
|
35
53
|
}
|
|
36
|
-
export type IssueRefusalReason = 'expires_at_required' | 'expires_at_past' | 'minter_not_found' | 'minter_revoked' | 'minter_expired'
|
|
54
|
+
export type IssueRefusalReason = 'expires_at_required' | 'expires_at_past' | 'minter_not_found' | 'minter_revoked' | 'minter_expired'
|
|
55
|
+
/** #47: `request.environment` (or its `'live'` default) does not match the minter row's own. */
|
|
56
|
+
| 'environment_mismatch' | WorkerErrorCode;
|
|
37
57
|
export type IssueResult = {
|
|
38
58
|
readonly ok: true;
|
|
39
59
|
readonly id: string;
|
|
@@ -44,17 +64,22 @@ export type IssueResult = {
|
|
|
44
64
|
readonly ok: false;
|
|
45
65
|
readonly reason: IssueRefusalReason;
|
|
46
66
|
};
|
|
47
|
-
export type CheckRefusalReason = 'not_found' | 'signature_invalid' | 'revoked' | 'expired' | 'secret_mismatch'
|
|
67
|
+
export type CheckRefusalReason = 'not_found' | 'signature_invalid' | 'revoked' | 'expired' | 'secret_mismatch'
|
|
68
|
+
/** #45: `rateLimiter` refused this credential id for the current window. */
|
|
69
|
+
| 'rate_limited';
|
|
48
70
|
export type CheckResult<TGrant> = {
|
|
49
71
|
readonly ok: true;
|
|
50
72
|
readonly id: string;
|
|
51
73
|
readonly issuedById: string | null;
|
|
52
74
|
readonly grants: readonly TGrant[];
|
|
75
|
+
readonly environment: KeyEnvironment;
|
|
53
76
|
} | {
|
|
54
77
|
readonly ok: false;
|
|
55
78
|
readonly reason: CheckRefusalReason;
|
|
79
|
+
/** Only meaningful with `reason: 'rate_limited'`. */
|
|
80
|
+
readonly retryAfterMs?: number;
|
|
56
81
|
};
|
|
57
|
-
export type RotateRefusalReason = 'not_found' | 'revoked' | 'minter_not_found' | 'minter_revoked' | 'minter_expired' | WorkerErrorCode;
|
|
82
|
+
export type RotateRefusalReason = 'signature_invalid' | 'conflict' | 'not_found' | 'revoked' | 'minter_not_found' | 'minter_revoked' | 'minter_expired' | WorkerErrorCode;
|
|
58
83
|
export type RotateResult = {
|
|
59
84
|
readonly ok: true;
|
|
60
85
|
readonly secret: string;
|
|
@@ -68,6 +93,7 @@ export interface CredentialLink<TGrant> {
|
|
|
68
93
|
readonly grants: readonly TGrant[];
|
|
69
94
|
readonly expiresAt: number;
|
|
70
95
|
readonly revokedAt: number | null;
|
|
96
|
+
readonly environment: KeyEnvironment;
|
|
71
97
|
}
|
|
72
98
|
export interface CredentialIssuer<TGrant> {
|
|
73
99
|
/** Calls `worker.signRow`. */
|