@fungi.computer/ear-tag 0.0.0-stage → 0.0.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/LICENSE +21 -0
- package/README.md +149 -2
- package/dist/grants.d.ts +74 -0
- package/dist/grants.js +113 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +17 -0
- package/dist/keys-node.d.ts +8 -0
- package/dist/keys-node.js +22 -0
- package/dist/keys.d.ts +242 -0
- package/dist/keys.js +567 -0
- package/docs/api/README.md +16 -0
- package/docs/api/grants.md +191 -0
- package/docs/api/index.md +100 -0
- package/docs/api/keys-node.md +31 -0
- package/docs/api/keys.md +406 -0
- package/package.json +75 -3
package/dist/keys.d.ts
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import type { Grant, GrantVocabulary, Scope } from "./grants.js";
|
|
2
|
+
/** One bound SQL value a key store statement may carry. */
|
|
3
|
+
export type KeySqlValue = string | number | null;
|
|
4
|
+
/** One materialized SQL row, keyed by column name. */
|
|
5
|
+
export type KeySqlRow = Readonly<Record<string, KeySqlValue>>;
|
|
6
|
+
/** Materialized result of one executed statement. */
|
|
7
|
+
export type KeySqlCursor = Readonly<{
|
|
8
|
+
/** Every row the statement produced. */
|
|
9
|
+
toArray: () => readonly KeySqlRow[];
|
|
10
|
+
}>;
|
|
11
|
+
/**
|
|
12
|
+
* Synchronous SQL execution the key store runs its statements against. This
|
|
13
|
+
* shape fits Cloudflare Durable Object SQLite (`state.storage.sql`) as is.
|
|
14
|
+
* A `node:sqlite` adapter for hosts outside a Durable Object lives at
|
|
15
|
+
* `@fungi.computer/ear-tag/keys/node`, kept out of this module so a Worker
|
|
16
|
+
* consumer of this type never needs Node's own types to typecheck.
|
|
17
|
+
*/
|
|
18
|
+
export type KeySql = Readonly<{
|
|
19
|
+
/** Execute one statement with positional bindings and return its rows. */
|
|
20
|
+
exec: (statement: string, ...bindings: readonly KeySqlValue[]) => KeySqlCursor;
|
|
21
|
+
}>;
|
|
22
|
+
/** A per-key fixed-window request policy. */
|
|
23
|
+
export type RateLimitPolicy = Readonly<{
|
|
24
|
+
/** Requests admitted per window. */
|
|
25
|
+
limit: number;
|
|
26
|
+
/** Window length, in seconds. */
|
|
27
|
+
windowSeconds: number;
|
|
28
|
+
}>;
|
|
29
|
+
/** A stored key's durable state, without its bearer secret. */
|
|
30
|
+
export type KeyRecord<Capability extends string> = Readonly<{
|
|
31
|
+
/** Opaque, canonical key identifier. */
|
|
32
|
+
keyId: string;
|
|
33
|
+
/** Opaque identifier of the accountable owner this key was issued under. */
|
|
34
|
+
owner: string;
|
|
35
|
+
/** Caller-chosen label; not secret, not unique. */
|
|
36
|
+
name: string;
|
|
37
|
+
/** The key's authority, or `null` when its stored grants could not be read back. An unreadable scope carries no authority but remains revocable. */
|
|
38
|
+
scope: Scope<Capability> | null;
|
|
39
|
+
/** Monotonic version, incremented by every accepted grant edit. */
|
|
40
|
+
grantVersion: number;
|
|
41
|
+
/** Unix milliseconds after which the key stops verifying, or `null` when the owner chose no expiry. */
|
|
42
|
+
expiresAt: number | null;
|
|
43
|
+
/** Unix milliseconds the key was revoked, or `null` while still live. */
|
|
44
|
+
revokedAt: number | null;
|
|
45
|
+
/** The key's own rate policy, or `null` when unlimited. */
|
|
46
|
+
rateLimit: RateLimitPolicy | null;
|
|
47
|
+
/** Unix milliseconds of the key's last `touch`, or `null` before one. `verify` does not stamp this on its own; a host calls `touch` when it wants usage tracking. */
|
|
48
|
+
lastUsedAt: number | null;
|
|
49
|
+
/** Unix milliseconds the key was issued. */
|
|
50
|
+
createdAt: number;
|
|
51
|
+
/** Unix milliseconds of the key's last state change. */
|
|
52
|
+
updatedAt: number;
|
|
53
|
+
}>;
|
|
54
|
+
/** A verified bearer credential: a live key's identity and current authority. */
|
|
55
|
+
export type KeyPrincipal<Capability extends string> = Readonly<{
|
|
56
|
+
/** Opaque, canonical key identifier. */
|
|
57
|
+
keyId: string;
|
|
58
|
+
/** Opaque identifier of the accountable owner this key was issued under. */
|
|
59
|
+
owner: string;
|
|
60
|
+
/** Caller-chosen label; not secret, not unique. */
|
|
61
|
+
name: string;
|
|
62
|
+
/** The key's authority at the moment it was read. */
|
|
63
|
+
scope: Scope<Capability>;
|
|
64
|
+
/** The grant version this authority was read at; used to detect a stale credential. */
|
|
65
|
+
grantVersion: number;
|
|
66
|
+
/** Unix milliseconds after which the key stops verifying, or `null` when unbounded. */
|
|
67
|
+
expiresAt: number | null;
|
|
68
|
+
}>;
|
|
69
|
+
/** Stable, closed reasons a key-store operation refuses to proceed. */
|
|
70
|
+
export type KeyRefusalCode = "invalid" | "denied" | "conflict" | "expired" | "rate_limited" | "unavailable";
|
|
71
|
+
/**
|
|
72
|
+
* One closed refusal, carried as data rather than a thrown error type.
|
|
73
|
+
*
|
|
74
|
+
* `invalid` means the caller's own input failed the operation's schema; it
|
|
75
|
+
* is the caller's bug. `denied` means well-formed input was refused on its
|
|
76
|
+
* merits (no authority, revoked, suspended). `unavailable` means the store
|
|
77
|
+
* could not read back its own state (a corrupt row, a corrupt rate limit,
|
|
78
|
+
* or any other failure that is not one of the other closed reasons); that
|
|
79
|
+
* is the store's own fault, never the caller's, and every such failure is
|
|
80
|
+
* also logged before this outcome is returned.
|
|
81
|
+
*/
|
|
82
|
+
export type KeyRefusal = Readonly<{
|
|
83
|
+
/** The stable, closed refusal reason. */
|
|
84
|
+
code: KeyRefusalCode;
|
|
85
|
+
/** Seconds to wait before retrying; present only for `rate_limited`. */
|
|
86
|
+
retryAfterSeconds?: number;
|
|
87
|
+
}>;
|
|
88
|
+
/** The outcome of one key-store operation: its value, or a closed refusal. */
|
|
89
|
+
export type KeyOutcome<Value> = Readonly<Readonly<{
|
|
90
|
+
/** The operation succeeded. */
|
|
91
|
+
kind: "ok";
|
|
92
|
+
/** The operation's result. */
|
|
93
|
+
value: Value;
|
|
94
|
+
}> | Readonly<{
|
|
95
|
+
/** The operation refused to proceed. */
|
|
96
|
+
kind: "error";
|
|
97
|
+
/** The closed refusal. */
|
|
98
|
+
error: KeyRefusal;
|
|
99
|
+
}>>;
|
|
100
|
+
/**
|
|
101
|
+
* What an issuing actor may grant. `"unrestricted"` may grant any capability
|
|
102
|
+
* in the host's vocabulary; `"bounded"` may grant only a subset of its own
|
|
103
|
+
* live grants, checked with the vocabulary's `isSubset`.
|
|
104
|
+
*/
|
|
105
|
+
export type IssuerAuthority<Capability extends string> = Readonly<{
|
|
106
|
+
/** The actor may grant any capability in the host's vocabulary. */
|
|
107
|
+
kind: "unrestricted";
|
|
108
|
+
}> | Readonly<{
|
|
109
|
+
/** The actor may grant only a subset of `grants`. */
|
|
110
|
+
kind: "bounded";
|
|
111
|
+
/** The actor's own live grants; every new grant must be their subset. */
|
|
112
|
+
grants: readonly Grant<Capability>[];
|
|
113
|
+
}>;
|
|
114
|
+
/** One durable notice that a key's authority changed, for a host's own downstream fencing. */
|
|
115
|
+
export type AuthorityFence = Readonly<{
|
|
116
|
+
/** The request that caused the change; stable across a safe retry. */
|
|
117
|
+
requestId: string;
|
|
118
|
+
/** The affected key. */
|
|
119
|
+
keyId: string;
|
|
120
|
+
/** The key's grant version after the change. */
|
|
121
|
+
grantVersion: number;
|
|
122
|
+
/** The key's expiry after the change, or `null` when unbounded. */
|
|
123
|
+
expiresAt: number | null;
|
|
124
|
+
}>;
|
|
125
|
+
/** Construction options for {@link createKeyStore}. */
|
|
126
|
+
export type CreateKeyStoreOptions<Capability extends string> = Readonly<{
|
|
127
|
+
/** Synchronous SQL execution this store's tables are kept in. */
|
|
128
|
+
sql: KeySql;
|
|
129
|
+
/** Commit a durable write with the host's own transaction and recovery coverage. */
|
|
130
|
+
commit: <Value>(body: () => Value) => Promise<Value>;
|
|
131
|
+
/** The host's capability vocabulary and grant operations. */
|
|
132
|
+
vocabulary: GrantVocabulary<Capability>;
|
|
133
|
+
/** Lowercase, underscore-separated namespace for this store's bearer keys, identifiers and tables. */
|
|
134
|
+
prefix: string;
|
|
135
|
+
/**
|
|
136
|
+
* Resolve one actor's authority to issue or edit keys, or `null` to deny
|
|
137
|
+
* the actor outright. The host owns actor identity, role checks, and
|
|
138
|
+
* re-reading a parent key's live authority.
|
|
139
|
+
*
|
|
140
|
+
* `"bounded"` only narrows which grants the actor may assign (`issue`,
|
|
141
|
+
* `updateGrants`): it does not narrow which keys they may `list`, `revoke`
|
|
142
|
+
* or edit. Any actor this resolves to a non-null authority for may manage
|
|
143
|
+
* every key in the tenant, including keys another actor issued. This
|
|
144
|
+
* matches Hub's own `authorizeManage`.
|
|
145
|
+
*/
|
|
146
|
+
authorizeIssuer: (actorId: string) => Promise<IssuerAuthority<Capability> | null>;
|
|
147
|
+
/** Notified, inside the host's commit, whenever a key's authority changes. */
|
|
148
|
+
onAuthorityChange?: (fence: AuthorityFence) => void;
|
|
149
|
+
/** Clock override for tests; defaults to `Date.now`. */
|
|
150
|
+
now?: () => number;
|
|
151
|
+
}>;
|
|
152
|
+
/** Input to {@link KeyStore}'s `issue`. */
|
|
153
|
+
export type IssueKeyInput<Capability extends string> = Readonly<{
|
|
154
|
+
/** Caller-chosen label; not secret, not unique. */
|
|
155
|
+
name: string;
|
|
156
|
+
/** The grants the new key carries; 1 to 64. */
|
|
157
|
+
grants: readonly Grant<Capability>[];
|
|
158
|
+
/**
|
|
159
|
+
* Host-chosen id for the new key; omit to let the store generate one.
|
|
160
|
+
* Once an id has been used, including by a now-revoked key, it is never
|
|
161
|
+
* reused: issuing with a reused id refuses `conflict`.
|
|
162
|
+
*/
|
|
163
|
+
keyId?: string;
|
|
164
|
+
/** Unix milliseconds after which the key stops verifying; omit for no expiry. */
|
|
165
|
+
expiresAt?: number;
|
|
166
|
+
/** The key's own rate policy; omit for unlimited. */
|
|
167
|
+
rateLimit?: RateLimitPolicy;
|
|
168
|
+
}>;
|
|
169
|
+
/** Result of {@link KeyStore}'s `issue`: the bearer secret and its durable record. */
|
|
170
|
+
export type IssuedKey<Capability extends string> = Readonly<{
|
|
171
|
+
/** Bearer secret to disclose once to its authorized recipient; never stored. */
|
|
172
|
+
key: string;
|
|
173
|
+
/** The issued key's durable record. */
|
|
174
|
+
record: KeyRecord<Capability>;
|
|
175
|
+
}>;
|
|
176
|
+
/** Input to {@link KeyStore}'s `updateGrants`: a compare-and-swap grant replacement. */
|
|
177
|
+
export type UpdateGrantsInput<Capability extends string> = Readonly<{
|
|
178
|
+
/** The request this edit belongs to; a safe retry replays the same outcome. */
|
|
179
|
+
requestId: string;
|
|
180
|
+
/** The key being edited. */
|
|
181
|
+
keyId: string;
|
|
182
|
+
/** The grant version this edit expects to replace. */
|
|
183
|
+
expectedGrantVersion: number;
|
|
184
|
+
/** The key's replacement grants; 1 to 64. */
|
|
185
|
+
grants: readonly Grant<Capability>[];
|
|
186
|
+
}>;
|
|
187
|
+
/** Input to {@link KeyStore}'s `revoke`: an exact, replayable revocation. */
|
|
188
|
+
export type RevokeInput = Readonly<{
|
|
189
|
+
/** The request this revocation belongs to; a safe retry replays the same outcome. */
|
|
190
|
+
requestId: string;
|
|
191
|
+
/** The key being revoked. */
|
|
192
|
+
keyId: string;
|
|
193
|
+
}>;
|
|
194
|
+
/** Input to {@link KeyStore}'s `admitUse`: one exact, replayable spend admission. */
|
|
195
|
+
export type AdmitUseInput<Capability extends string> = Readonly<{
|
|
196
|
+
/** The capability being spent. */
|
|
197
|
+
capability: Capability;
|
|
198
|
+
/** The resource it is spent against, when the capability is resource-scoped. */
|
|
199
|
+
target?: string;
|
|
200
|
+
/** The request this admission belongs to; a safe retry replays the same outcome. */
|
|
201
|
+
requestId: string;
|
|
202
|
+
}>;
|
|
203
|
+
/** The operations one key store exposes over its tenant's keys. */
|
|
204
|
+
export type KeyStore<Capability extends string> = Readonly<{
|
|
205
|
+
/** Issue a new key under `actorId`'s authority. */
|
|
206
|
+
issue: (actorId: string, input: IssueKeyInput<Capability>) => Promise<KeyOutcome<IssuedKey<Capability>>>;
|
|
207
|
+
/** Verify a bearer secret and return its current principal. */
|
|
208
|
+
verify: (secret: string) => Promise<KeyOutcome<KeyPrincipal<Capability>>>;
|
|
209
|
+
/** Re-read a captured principal; refuses once its grant version goes stale. */
|
|
210
|
+
current: (principal: KeyPrincipal<Capability>) => Promise<KeyOutcome<KeyPrincipal<Capability>>>;
|
|
211
|
+
/** List every key, or every key owned by `owner`, once `actorId` is authorized to manage keys. */
|
|
212
|
+
list: (actorId: string, owner?: string) => Promise<KeyOutcome<readonly KeyRecord<Capability>[]>>;
|
|
213
|
+
/**
|
|
214
|
+
* Read one key's live principal by id, once `actorId` is authorized to
|
|
215
|
+
* manage keys covering at least this key's own grants. Refuses `denied`
|
|
216
|
+
* for an unknown, revoked or suspended key and `expired` for one past
|
|
217
|
+
* its expiry, the same as `verify`.
|
|
218
|
+
*/
|
|
219
|
+
read: (actorId: string, keyId: string) => Promise<KeyOutcome<KeyPrincipal<Capability>>>;
|
|
220
|
+
/** Compare-and-swap a key's grants. */
|
|
221
|
+
updateGrants: (actorId: string, input: UpdateGrantsInput<Capability>) => Promise<KeyOutcome<KeyRecord<Capability>>>;
|
|
222
|
+
/** Tombstone a key; idempotent on repeat. */
|
|
223
|
+
revoke: (actorId: string, input: RevokeInput) => Promise<KeyOutcome<KeyRecord<Capability>>>;
|
|
224
|
+
/** Permanently suspend every key owned by `owner`, for example on its departure. Commits its own write. */
|
|
225
|
+
suspendOwner: (owner: string, requestId: string) => Promise<KeyOutcome<void>>;
|
|
226
|
+
/** Revoke every still-live key as of `closedAt`. Runs inside the caller's own transaction; it does not commit. */
|
|
227
|
+
revokeAll: (closedAt: number) => Promise<KeyOutcome<void>>;
|
|
228
|
+
/** Re-verify a capability against a principal's live authority and record one idempotent use receipt. */
|
|
229
|
+
admitUse: (principal: KeyPrincipal<Capability>, input: AdmitUseInput<Capability>) => Promise<KeyOutcome<KeyPrincipal<Capability>>>;
|
|
230
|
+
/**
|
|
231
|
+
* Stamp a key's `lastUsedAt`. The only place that writes it; `verify`
|
|
232
|
+
* never stamps on its own. Refuses `denied` for an unknown key.
|
|
233
|
+
*/
|
|
234
|
+
touch: (keyId: string) => Promise<KeyOutcome<void>>;
|
|
235
|
+
}>;
|
|
236
|
+
/**
|
|
237
|
+
* Create one tenant-scoped key store: bearer-key issuance, verification and
|
|
238
|
+
* lifecycle over one host's capability vocabulary. One store instance is one
|
|
239
|
+
* tenant; namespacing between tenants is the host's own table or storage
|
|
240
|
+
* boundary, not a column inside this store.
|
|
241
|
+
*/
|
|
242
|
+
export declare function createKeyStore<Capability extends string>(options: CreateKeyStoreOptions<Capability>): KeyStore<Capability>;
|