@hediet/linkrpc-hub 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/README.md +101 -0
- package/dist/chunks/config-BCvkg7jv.d.ts +566 -0
- package/dist/chunks/configFile-y6Phntqu.d.ts +9 -0
- package/dist/chunks/connectionTokenBinder.interfaces-B-beCg06.js +40 -0
- package/dist/chunks/connectionTokenBinder.interfaces-B-beCg06.js.map +1 -0
- package/dist/chunks/connectionTokenBinder.interfaces-BhzQ1DTS.d.ts +36 -0
- package/dist/chunks/hubConnectionAcceptor-B5-8JsFY.js +2768 -0
- package/dist/chunks/hubConnectionAcceptor-B5-8JsFY.js.map +1 -0
- package/dist/chunks/hubConnectionAcceptor-BwydvFa4.d.ts +1560 -0
- package/dist/chunks/index-CLIUrV88.d.ts +481 -0
- package/dist/chunks/node-CTXsQ6oa.js +460 -0
- package/dist/chunks/node-CTXsQ6oa.js.map +1 -0
- package/dist/chunks/nodeTransit-CWeFnbwt.js +444 -0
- package/dist/chunks/nodeTransit-CWeFnbwt.js.map +1 -0
- package/dist/chunks/nodeTransit-cmtZgdpW.d.ts +226 -0
- package/dist/chunks/runHub-P3YUwwdv.js +1797 -0
- package/dist/chunks/runHub-P3YUwwdv.js.map +1 -0
- package/dist/chunks/server-BAxchQhy.js +1368 -0
- package/dist/chunks/server-BAxchQhy.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +38 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +305 -0
- package/dist/config.js.map +1 -0
- package/dist/configFile.d.ts +2 -0
- package/dist/configFile.js +27 -0
- package/dist/configFile.js.map +1 -0
- package/dist/engine/runHub.d.ts +42 -0
- package/dist/engine/runHub.js +2 -0
- package/dist/hub/server/client.d.ts +2 -0
- package/dist/hub/server/client.js +2 -0
- package/dist/hub/server/connectionTokenBinder.d.ts +2 -0
- package/dist/hub/server/connectionTokenBinder.js +2 -0
- package/dist/hub/server/index.d.ts +5 -0
- package/dist/hub/server/index.js +5 -0
- package/dist/hub/server/node/index.d.ts +218 -0
- package/dist/hub/server/node/index.js +2 -0
- package/dist/hub/server/transit.d.ts +2 -0
- package/dist/hub/server/transit.js +2 -0
- package/dist/index.d.ts +512 -0
- package/dist/index.js +309 -0
- package/dist/index.js.map +1 -0
- package/dist/serve.d.ts +14 -0
- package/dist/serve.js +31 -0
- package/dist/serve.js.map +1 -0
- package/dist/spawn.d.ts +12 -0
- package/dist/spawn.js +25 -0
- package/dist/spawn.js.map +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import "./nodeTransit-cmtZgdpW.js";
|
|
2
|
+
import { Et as Hub, Ot as IDisposable, Tt as AttachedLink } from "./hubConnectionAcceptor-BwydvFa4.js";
|
|
3
|
+
import { AcceptedRootIssuer, Base64Sha256, Capability, IMessageTransport, Identity, Keypair, LinkRpcConnection, ManagedIdentityStorageBackend, Permission, PrincipalId, PublicSigningIdentity, SignedCapability, SigningIdentity, X25519Keypair } from "@hediet/linkrpc";
|
|
4
|
+
import { ServiceId } from "@hediet/linkrpc/hub/common";
|
|
5
|
+
//#region src/hub/server/routing/forwardingTable.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* A longest-prefix routing table keyed by {@link ServiceId} prefixes — the
|
|
8
|
+
* hub's equivalent of an IP forwarding table.
|
|
9
|
+
*
|
|
10
|
+
* A prefix `"a/b"` owns every serviceId equal to it or beneath it
|
|
11
|
+
* (`"a/b"`, `"a/b/c"`, …) *unless* a longer claimed prefix matches first.
|
|
12
|
+
* Matching is segment-aware: `"a/bc"` is **not** under `"a/b"`.
|
|
13
|
+
*
|
|
14
|
+
* The table is intentionally a plain stateful container with no opinion on
|
|
15
|
+
* *who* may claim what — that policy lives in {@link Hub.claimPrefix}. This
|
|
16
|
+
* keeps the data structure trivially testable in isolation.
|
|
17
|
+
*/
|
|
18
|
+
declare class ForwardingTable<T> {
|
|
19
|
+
private readonly _entries;
|
|
20
|
+
/**
|
|
21
|
+
* Reverse index: value → the set of prefixes claimed for it. Kept in
|
|
22
|
+
* lock-step with {@link _entries} so {@link deleteByValue} (used when a
|
|
23
|
+
* link detaches) is O(claims-for-that-value) instead of an O(size) scan.
|
|
24
|
+
*/
|
|
25
|
+
private readonly _prefixesByValue;
|
|
26
|
+
/** Number of claimed prefixes. */
|
|
27
|
+
get size(): number;
|
|
28
|
+
has(prefix: ServiceId): boolean;
|
|
29
|
+
get(prefix: ServiceId): T | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Claim `prefix` for `value`. Overwrites any existing claim — callers
|
|
32
|
+
* that want claim-once semantics must check {@link has} first (the hub
|
|
33
|
+
* does).
|
|
34
|
+
*/
|
|
35
|
+
set(prefix: ServiceId, value: T): void;
|
|
36
|
+
delete(prefix: ServiceId): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Remove every claim bound to `value` (matched by reference/equality).
|
|
39
|
+
* O(number of prefixes that value owns). Returns the removed prefixes.
|
|
40
|
+
*/
|
|
41
|
+
deleteByValue(value: T): ServiceId[];
|
|
42
|
+
private _dropFromIndex;
|
|
43
|
+
entries(): IterableIterator<[ServiceId, T]>;
|
|
44
|
+
prefixes(): ServiceId[];
|
|
45
|
+
/**
|
|
46
|
+
* Find the value owning the longest claimed prefix of `serviceId`.
|
|
47
|
+
* Walks up the `'/'` segments — `"a/b/c"` tries `"a/b/c"`, `"a/b"`,
|
|
48
|
+
* `"a"` in that order — and returns the first hit, or `undefined` if
|
|
49
|
+
* none of its ancestors are claimed.
|
|
50
|
+
*/
|
|
51
|
+
longestPrefixMatch(serviceId: ServiceId): {
|
|
52
|
+
prefix: ServiceId;
|
|
53
|
+
value: T;
|
|
54
|
+
} | undefined;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Validate a forwarding-table **prefix**: a non-root {@link ServiceId}. The
|
|
58
|
+
* root (`""`) is a valid service id but cannot be claimed as a prefix (it
|
|
59
|
+
* would capture every address); the uplink is the route of last resort
|
|
60
|
+
* instead. Returns an error string, or `undefined` when well-formed.
|
|
61
|
+
*/
|
|
62
|
+
declare function validatePrefix(prefix: unknown): string | undefined;
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/hub/server/hubRegisterServiceId.d.ts
|
|
65
|
+
/**
|
|
66
|
+
* Register an **in-process** service on `hub` under `serviceId`, returning a
|
|
67
|
+
* {@link RegisteredServiceId} whose `connection` serves its interfaces.
|
|
68
|
+
*
|
|
69
|
+
* This is the hubv2 replacement for the v1 `hub.attachParticipant(pair.a)` +
|
|
70
|
+
* `LinkRpcConnection.fromTransport(pair.b)` + `enableReflection({ serviceId })`
|
|
71
|
+
* dance that every built-in extension service performed. It:
|
|
72
|
+
*
|
|
73
|
+
* 1. attaches an in-memory link to the hub and claims `serviceId` on it, so
|
|
74
|
+
* the hub routes every fully-qualified `serviceId::…` call to this service;
|
|
75
|
+
* 2. exposes a {@link LinkRpcConnection} whose registered interfaces answer
|
|
76
|
+
* those calls (register them with `{ serviceId }`); and
|
|
77
|
+
* 3. enables reflection under `serviceId` so the hub's aggregating
|
|
78
|
+
* `directory::list` surfaces this service.
|
|
79
|
+
*
|
|
80
|
+
* Disposing the returned handle (`svc.dispose()`) closes the connection and
|
|
81
|
+
* detaches the link, releasing the claimed prefix.
|
|
82
|
+
*
|
|
83
|
+
* Unlike an accepted *participant* (which gets a {@link RootOverlay} and only
|
|
84
|
+
* reaches the hub through its uplink), an in-process service is attached
|
|
85
|
+
* directly to the hub as a prefix owner. It is fully trusted — no provenance,
|
|
86
|
+
* identity, or forwarded-call gating applies to traffic it receives.
|
|
87
|
+
*/
|
|
88
|
+
declare function hubRegisterServiceId(hub: Hub, serviceId: ServiceId): RegisteredServiceId;
|
|
89
|
+
/**
|
|
90
|
+
* Handle to an in-process service registered on a {@link Hub} via
|
|
91
|
+
* {@link hubRegisterServiceId}. Owns both the {@link LinkRpcConnection} the
|
|
92
|
+
* service serves its interfaces on and the hub {@link AttachedLink} that routes
|
|
93
|
+
* traffic to it. {@link dispose} closes the connection and detaches the link
|
|
94
|
+
* (releasing the claimed prefix) — wire it into the owning feature's disposal.
|
|
95
|
+
*/
|
|
96
|
+
declare class RegisteredServiceId implements IDisposable {
|
|
97
|
+
/** Serve the service's interfaces here (register them with `{ serviceId }`). */
|
|
98
|
+
readonly connection: LinkRpcConnection;
|
|
99
|
+
private readonly _link;
|
|
100
|
+
constructor(
|
|
101
|
+
/** Serve the service's interfaces here (register them with `{ serviceId }`). */
|
|
102
|
+
connection: LinkRpcConnection, _link: AttachedLink);
|
|
103
|
+
dispose(): void;
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/hub/server/mintCapability.d.ts
|
|
107
|
+
interface MintCapabilityOptions {
|
|
108
|
+
/** The admin identity that issues (signs) the capability. */
|
|
109
|
+
readonly issuer: SigningIdentity;
|
|
110
|
+
/** The holder allowed to wield the capability. */
|
|
111
|
+
readonly audience: PrincipalId;
|
|
112
|
+
/** What the holder may do. A call matches iff it matches any permission. */
|
|
113
|
+
readonly permissions: readonly Permission[];
|
|
114
|
+
/** Unix milliseconds. Omit for a capability that never expires. */
|
|
115
|
+
readonly expiresAtMs?: number;
|
|
116
|
+
/**
|
|
117
|
+
* Per-cap distinguisher (base64url). Defaults to a fresh random 128-bit
|
|
118
|
+
* value.
|
|
119
|
+
*/
|
|
120
|
+
readonly nonce?: string;
|
|
121
|
+
/** Delegation parent: the content hash (`signedHash("capability", parent)`) of the parent capability. */
|
|
122
|
+
readonly parentHash?: Base64Sha256<Capability>;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Assemble and sign a {@link Capability} with the admin identity.
|
|
126
|
+
*
|
|
127
|
+
* This replaces the v1 `hub.proposeCapability` / `signProposedCapability`
|
|
128
|
+
* pair: the consent engine decides the permissions (e.g. from
|
|
129
|
+
* {@link resolveAccessCandidates} + the user's consent selection) and mints
|
|
130
|
+
* the capability bound to the consumer's PrincipalId as `audience`.
|
|
131
|
+
*/
|
|
132
|
+
declare function mintCapability(options: MintCapabilityOptions): Promise<SignedCapability>;
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/hub/server/verifiedSignature.d.ts
|
|
135
|
+
/**
|
|
136
|
+
* Wrap an inbound transport so every request's `$hubrpc` signature is verified
|
|
137
|
+
* **eagerly, at the door** — a pure authenticity gate with no capability check:
|
|
138
|
+
*
|
|
139
|
+
* - a **valid** signature → the request is delivered unchanged;
|
|
140
|
+
* - an **invalid** signature → the request is **rejected** here (an error
|
|
141
|
+
* response is sent and the message is never delivered);
|
|
142
|
+
* - an **unsigned** call (no envelope) → delivered unchanged, so keyless
|
|
143
|
+
* connections still work.
|
|
144
|
+
*
|
|
145
|
+
* This is the signature half of the hub's trust model. *Authorization*
|
|
146
|
+
* (capability chains) is **not** this wrapper's job — that is enforced
|
|
147
|
+
* separately by {@link import('./forwardedCallGate').withForwardedCallGate} on
|
|
148
|
+
* the hub-facing (forwarded) path. The root overlay uses this wrapper on its own
|
|
149
|
+
* root-form calls (consent / identity front doors), which never pass through the
|
|
150
|
+
* forwarded-call gate and so need their authenticity established here.
|
|
151
|
+
*
|
|
152
|
+
* When `verifySignatures` is not `true`, the transport is returned unchanged (no
|
|
153
|
+
* verification, every message passes verbatim).
|
|
154
|
+
*
|
|
155
|
+
* Verification is async but in-order delivery is preserved via a per-link
|
|
156
|
+
* promise chain.
|
|
157
|
+
*/
|
|
158
|
+
declare function withVerifiedSignature(link: IMessageTransport, options?: {
|
|
159
|
+
readonly verifySignatures?: boolean;
|
|
160
|
+
}): IMessageTransport;
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region src/hub/server/forwardedCallGate.d.ts
|
|
163
|
+
/** Options common to both gate modes (authenticity-only and capability). */
|
|
164
|
+
interface ForwardedCallGateBaseOptions {
|
|
165
|
+
/**
|
|
166
|
+
* ServiceId prefixes whose calls pass **verbatim**, unverified — an
|
|
167
|
+
* opt-in escape hatch. **Empty by default**: reflection and forwarded
|
|
168
|
+
* service calls are gated like any other. The consent front door
|
|
169
|
+
* (`hubAccess::*`) is served at the connection root (never forwarded), so it
|
|
170
|
+
* is reached directly and never passes through this gate.
|
|
171
|
+
*/
|
|
172
|
+
readonly exemptPrefixes?: Iterable<string>;
|
|
173
|
+
/** Override the clock (Unix milliseconds) used for skew / expiry checks. Testing seam. */
|
|
174
|
+
readonly nowMs?: () => number;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Per-service trust anchors, consulted with the call's `serviceId`. A
|
|
178
|
+
* capability authorises only if its chain roots at one of the anchors returned
|
|
179
|
+
* for that service, so a participant cannot self-issue authority. An empty
|
|
180
|
+
* result rejects every capability (fail closed) — there is deliberately no
|
|
181
|
+
* "accept any root" affordance. Anchors flagged `isPublic` are named in
|
|
182
|
+
* rejection messages.
|
|
183
|
+
*/
|
|
184
|
+
type AcceptedRootIssuerResolver = (serviceId: string) => readonly AcceptedRootIssuer[];
|
|
185
|
+
/**
|
|
186
|
+
* Gate configuration. A **discriminated union on `requireCapability`** so the
|
|
187
|
+
* type system enforces the one combination that is actually safe:
|
|
188
|
+
*
|
|
189
|
+
* - **authenticity only** (`requireCapability` omitted / `false`): a valid
|
|
190
|
+
* `$hubrpc` signature is enough; *authorization* is left to the target
|
|
191
|
+
* service / capability layer. `acceptedRootIssuers` is optional here.
|
|
192
|
+
* - **capability mode** (`requireCapability: true`): a bare signature is not
|
|
193
|
+
* enough — the request must also present a capability that {@link permits}
|
|
194
|
+
* the concrete call, rooted at an accepted issuer. Because there is no safe
|
|
195
|
+
* default trust anchor, either fixed `trustedRoots` or a service-specific
|
|
196
|
+
* `acceptedRootIssuers` resolver is **required at compile time**. This makes
|
|
197
|
+
* it impossible to request enforcement while silently forgetting whose
|
|
198
|
+
* capabilities to trust.
|
|
199
|
+
*/
|
|
200
|
+
type ForwardedCallGateOptions = (ForwardedCallGateBaseOptions & {
|
|
201
|
+
readonly requireCapability?: false;
|
|
202
|
+
readonly acceptedRootIssuers?: AcceptedRootIssuerResolver;
|
|
203
|
+
readonly trustedRoots?: never;
|
|
204
|
+
}) | (ForwardedCallGateBaseOptions & {
|
|
205
|
+
readonly requireCapability: true;
|
|
206
|
+
} & ({
|
|
207
|
+
readonly acceptedRootIssuers: AcceptedRootIssuerResolver;
|
|
208
|
+
readonly trustedRoots?: never;
|
|
209
|
+
} | {
|
|
210
|
+
/** Fixed public trust roots accepted for every forwarded service. */
|
|
211
|
+
readonly trustedRoots: readonly PublicSigningIdentity[];
|
|
212
|
+
readonly acceptedRootIssuers?: never;
|
|
213
|
+
}));
|
|
214
|
+
/**
|
|
215
|
+
* A **signature front door** for fully-qualified calls. Wrap an incoming
|
|
216
|
+
* transport with this before attaching it to a hub or constructing the serving
|
|
217
|
+
* connection, so every `serviceId::interfaceId::member` call must carry a
|
|
218
|
+
* valid `$hubrpc` signature.
|
|
219
|
+
* Unsigned or tampered calls are rejected with an error response and never
|
|
220
|
+
* reach the downstream hub or connection.
|
|
221
|
+
*
|
|
222
|
+
* What passes **verbatim** (ungated):
|
|
223
|
+
* - responses;
|
|
224
|
+
* - root-addressed requests (`interfaceId::member`) and bare requests
|
|
225
|
+
* (`member`) — these always terminate at the connection root and are never
|
|
226
|
+
* forwarded; and
|
|
227
|
+
* - requests targeting an {@link ForwardedCallGateOptions.exemptPrefixes exempt
|
|
228
|
+
* prefix} (the hub's own services, which gate themselves).
|
|
229
|
+
*
|
|
230
|
+
* The gate **does not strip** the envelope: signed wire params keep the real
|
|
231
|
+
* call params at the top level alongside `$hubrpc`/`$hubrpcUnsigned`, so the
|
|
232
|
+
* target's typed handler recovers them via schema-stripping while a target that
|
|
233
|
+
* cares may re-verify as defense-in-depth. Authorization is *not* this gate's
|
|
234
|
+
* job (unless {@link ForwardedCallGateOptions.requireCapability} is set): it
|
|
235
|
+
* proves *who* is calling, leaving *whether they may* to the capability layer.
|
|
236
|
+
*
|
|
237
|
+
* In-order delivery toward the hub is preserved across the async verification:
|
|
238
|
+
* inbound messages are processed through a single serial queue.
|
|
239
|
+
*/
|
|
240
|
+
declare function withForwardedCallGate(inner: IMessageTransport, options: ForwardedCallGateOptions): IMessageTransport;
|
|
241
|
+
/** Explicitly named alias for {@link withForwardedCallGate}. */
|
|
242
|
+
declare function withFullyQualifiedCallGate(inner: IMessageTransport, options: ForwardedCallGateOptions): IMessageTransport;
|
|
243
|
+
//#endregion
|
|
244
|
+
//#region src/hub/server/identityKeystore.d.ts
|
|
245
|
+
/**
|
|
246
|
+
* On-disk encrypted keystore for managed identities. State is keyed by
|
|
247
|
+
* {@link SlotId} (the absolute path to the entry HTML / Node entry point).
|
|
248
|
+
* Each slot owns up to four files under `storageDir`, mode `0o600`,
|
|
249
|
+
* AES-256-GCM-sealed under `keystoreSecret`:
|
|
250
|
+
*
|
|
251
|
+
* - `<hash>.bin` — the managed identity keypair
|
|
252
|
+
* - `<hash>.storage.bin` — the per-slot persistent KV store
|
|
253
|
+
* - `<hash>.keys.bin` — the access-key allow-list (gate)
|
|
254
|
+
* - `<hash>.snooze.bin` — the consent re-prompt suppression (optional)
|
|
255
|
+
* - `<hash>.filesdir.bin`— the recorded path of the plaintext app-files
|
|
256
|
+
* folder (optional; the folder itself lives
|
|
257
|
+
* wherever the owner placed it, NOT here)
|
|
258
|
+
*
|
|
259
|
+
* `<hash>` is `sha256(id).slice(0,16)`. The slot id is bound into each
|
|
260
|
+
* file's AEAD AAD so swapping files between slots fails the auth tag.
|
|
261
|
+
*
|
|
262
|
+
* The `keystoreSecret` itself is opaque bytes — the keystore does not
|
|
263
|
+
* know or care where it came from. In the extension, it's loaded from
|
|
264
|
+
* `vscode.SecretStorage`.
|
|
265
|
+
*/
|
|
266
|
+
interface IdentityKeystoreOptions {
|
|
267
|
+
/** Absolute directory path. Created if missing. */
|
|
268
|
+
readonly storageDir: string;
|
|
269
|
+
/** 32+ bytes of secret material used to seal identity files. */
|
|
270
|
+
readonly keystoreSecret: Uint8Array;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Stable slot identity: the absolute path to the entry HTML / Node entry
|
|
274
|
+
* point. One slot ⇒ one managed identity + one storage backend, shared
|
|
275
|
+
* across every approved {@link SlotAccessKey}.
|
|
276
|
+
*/
|
|
277
|
+
type SlotId = string;
|
|
278
|
+
/**
|
|
279
|
+
* An access key: a small, exact-match qualifier tuple a caller must
|
|
280
|
+
* present to reach a slot's identity/storage. Conventionally
|
|
281
|
+
* `{ appId, bundleHash }`, but the keystore treats it opaquely. Compared
|
|
282
|
+
* by canonical (sorted-field) JSON equality.
|
|
283
|
+
*/
|
|
284
|
+
type SlotAccessKey = Readonly<Record<string, string>>;
|
|
285
|
+
/**
|
|
286
|
+
* Result of `slotByIdAndKey`. Either the resolved
|
|
287
|
+
* slot, or a discriminated error:
|
|
288
|
+
* - `slotDoesNotExist` — no slot has ever been created for this id.
|
|
289
|
+
* - `unknownKey` — the slot exists but `key` is not in its
|
|
290
|
+
* allow-list (a different/unapproved bundle).
|
|
291
|
+
*/
|
|
292
|
+
type SlotByIdAndKeyResult = IdentitySlot | {
|
|
293
|
+
readonly error: "slotDoesNotExist";
|
|
294
|
+
} | {
|
|
295
|
+
readonly error: "unknownKey";
|
|
296
|
+
};
|
|
297
|
+
/**
|
|
298
|
+
* Result of `slotByIdAndTime`. Either the resolved slot (access is currently
|
|
299
|
+
* time-authorized) or a single discriminated error:
|
|
300
|
+
* - `consentRequired` — the slot holds inheritable state and no active
|
|
301
|
+
* snooze covers the supplied time, so the caller
|
|
302
|
+
* must obtain fresh consent before granting identity.
|
|
303
|
+
*/
|
|
304
|
+
type SlotByIdAndTimeResult = IdentitySlot | {
|
|
305
|
+
readonly error: "consentRequired";
|
|
306
|
+
};
|
|
307
|
+
/**
|
|
308
|
+
* A time-boxed, scoped suppression of identity-access consent re-prompts for
|
|
309
|
+
* a slot. Set from the consent dialog's "don't ask for file changes for 24h"
|
|
310
|
+
* checkbox; read on each load to decide whether a content change can load
|
|
311
|
+
* silently. Cleared by {@link IdentitySlot.delete}.
|
|
312
|
+
*
|
|
313
|
+
* - `scope: "entry"` — covers entry-HTML content changes only.
|
|
314
|
+
* - `scope: "all"` — covers entry-HTML AND external-file content changes.
|
|
315
|
+
* - `expiresAt` — absolute epoch ms; expired snoozes are ignored.
|
|
316
|
+
*
|
|
317
|
+
* A snooze never suppresses a *new external path* — that is always a fresh
|
|
318
|
+
* blast-radius decision the user must see.
|
|
319
|
+
*/
|
|
320
|
+
interface IdentitySnooze {
|
|
321
|
+
readonly scope: "entry" | "all";
|
|
322
|
+
readonly expiresAt: number;
|
|
323
|
+
}
|
|
324
|
+
interface IdentitySlot {
|
|
325
|
+
readonly id: SlotId;
|
|
326
|
+
/** The slot's managed identity; generated + persisted on first call. */
|
|
327
|
+
getOrCreateIdentity(): Promise<Identity>;
|
|
328
|
+
/** Existing identity or `undefined` — never creates. */
|
|
329
|
+
peekIdentity(): Promise<Identity | undefined>;
|
|
330
|
+
/** Per-slot persistent KV (caps + app data). File created on first write. */
|
|
331
|
+
readonly storage: ManagedIdentityStorageBackend;
|
|
332
|
+
/**
|
|
333
|
+
* Absolute path of the slot's *plaintext* app-files folder, or
|
|
334
|
+
* `undefined` if none has been recorded yet. The keystore only stores
|
|
335
|
+
* and wipes this opaque string — the owner (the extension) chooses the
|
|
336
|
+
* path (typically `<globalStorage>/app-files/<appId>` with a collision
|
|
337
|
+
* counter) and performs the actual file IO.
|
|
338
|
+
*
|
|
339
|
+
* Recording the path in the slot is deliberate: the folder name is not
|
|
340
|
+
* derivable from the app id alone (the counter breaks that), so an app
|
|
341
|
+
* can only reach its own folder by looking it up through its slot. This
|
|
342
|
+
* both isolates apps from each other and prevents accidental access to a
|
|
343
|
+
* sibling's folder.
|
|
344
|
+
*/
|
|
345
|
+
getFilesDir(): Promise<string | undefined>;
|
|
346
|
+
/** Record the chosen absolute path of this slot's app-files folder. */
|
|
347
|
+
setFilesDir(absPath: string): Promise<void>;
|
|
348
|
+
/**
|
|
349
|
+
* `true` iff a files folder is recorded AND it currently holds ≥1 entry.
|
|
350
|
+
* Counts toward {@link hasState} so a content change to a privileged app
|
|
351
|
+
* still trips the consent gate even when the app stored only files (and
|
|
352
|
+
* never created an identity or KV state).
|
|
353
|
+
*/
|
|
354
|
+
hasFiles(): Promise<boolean>;
|
|
355
|
+
/**
|
|
356
|
+
* Delete every entry inside the recorded files folder but keep the
|
|
357
|
+
* folder itself and its slot record (so the path stays stable for the
|
|
358
|
+
* user). No-op when no folder is recorded. Used by the consent dialog's
|
|
359
|
+
* "wipe app files" option on a *keep-identity* load.
|
|
360
|
+
*/
|
|
361
|
+
wipeFiles(): Promise<void>;
|
|
362
|
+
/**
|
|
363
|
+
* `true` iff there is anything a consumer could inherit: an identity
|
|
364
|
+
* has been created, storage holds ≥1 entry, OR the app-files folder
|
|
365
|
+
* holds ≥1 entry. When `false`, granting access exposes nothing — the
|
|
366
|
+
* consent prompt can be skipped safely.
|
|
367
|
+
*/
|
|
368
|
+
hasState(): Promise<boolean>;
|
|
369
|
+
/** All keys currently allowed to access this slot. */
|
|
370
|
+
listKeys(): Promise<SlotAccessKey[]>;
|
|
371
|
+
/** `true` iff `key` (exact canonical match) is in the allow-list. */
|
|
372
|
+
hasKey(key: SlotAccessKey): Promise<boolean>;
|
|
373
|
+
/** Add keys to the allow-list (creating the slot if needed). Idempotent. */
|
|
374
|
+
addKeys(...keys: SlotAccessKey[]): Promise<void>;
|
|
375
|
+
/** Remove one key. Returns `true` iff it was present. */
|
|
376
|
+
deleteKey(key: SlotAccessKey): Promise<boolean>;
|
|
377
|
+
/**
|
|
378
|
+
* The active, unexpired snooze for this slot, or `undefined`. Lazily
|
|
379
|
+
* treats an expired snooze as absent (does not rewrite the file).
|
|
380
|
+
*/
|
|
381
|
+
getSnooze(): Promise<IdentitySnooze | undefined>;
|
|
382
|
+
/** Persist a snooze for this slot (overwrites any previous one). */
|
|
383
|
+
setSnooze(snooze: IdentitySnooze): Promise<void>;
|
|
384
|
+
/** Remove any snooze for this slot. Idempotent. */
|
|
385
|
+
clearSnooze(): Promise<void>;
|
|
386
|
+
/** Wipe identity, storage, AND all access keys for this slot. */
|
|
387
|
+
delete(): Promise<void>;
|
|
388
|
+
}
|
|
389
|
+
interface IdentityKeystore {
|
|
390
|
+
/**
|
|
391
|
+
* Resolve the slot for `id`, enforcing the access gate. Returns the
|
|
392
|
+
* slot on success, or a discriminated error if the slot does not exist
|
|
393
|
+
* or `key` is not in its allow-list. Use this on the privileged path
|
|
394
|
+
* (registering `identity::*` on an iframe overlay) so an unapproved
|
|
395
|
+
* bundle can never reach prior state.
|
|
396
|
+
*/
|
|
397
|
+
slotByIdAndKey(id: SlotId, key: SlotAccessKey): Promise<SlotByIdAndKeyResult>;
|
|
398
|
+
/**
|
|
399
|
+
* Resolve the slot for `id`, gated on *time* rather than a content key.
|
|
400
|
+
* Returns the slot when access is currently authorized — either the slot
|
|
401
|
+
* holds nothing inheritable ({@link IdentitySlot.hasState} is `false`, so
|
|
402
|
+
* granting exposes nothing) or an active, unexpired snooze covers `nowMs`.
|
|
403
|
+
* Otherwise returns `consentRequired`, meaning the slot holds state the
|
|
404
|
+
* caller must obtain fresh consent for. Used on the privileged path for
|
|
405
|
+
* URL apps, which have no content key to present to {@link slotByIdAndKey}
|
|
406
|
+
* but may inherit the slot's identity under a time-boxed grant.
|
|
407
|
+
*/
|
|
408
|
+
slotByIdAndTime(id: SlotId, nowMs: number): Promise<SlotByIdAndTimeResult>;
|
|
409
|
+
/**
|
|
410
|
+
* Create the slot for `id` (if missing) and add `key` to its
|
|
411
|
+
* allow-list. Idempotent: re-creating an existing slot just ensures the
|
|
412
|
+
* key is present. Returns the resolved slot. This is the only way to
|
|
413
|
+
* bootstrap a brand-new slot before {@link slotByIdAndKey} can succeed.
|
|
414
|
+
*/
|
|
415
|
+
createSlotWithKey(id: SlotId, key: SlotAccessKey): Promise<IdentitySlot>;
|
|
416
|
+
/**
|
|
417
|
+
* Unsafe: return the slot handle for `id` with NO key check. Used to
|
|
418
|
+
* inspect `hasState`/`listKeys` and to run the consent decision before
|
|
419
|
+
* a key has been added. Never register the identity returned from here
|
|
420
|
+
* without having checked/added a key.
|
|
421
|
+
*/
|
|
422
|
+
slotById(id: SlotId): IdentitySlot;
|
|
423
|
+
}
|
|
424
|
+
declare function createIdentityKeystore(opts: IdentityKeystoreOptions): IdentityKeystore;
|
|
425
|
+
//#endregion
|
|
426
|
+
//#region src/hub/server/sqliteIdentityKeystore.d.ts
|
|
427
|
+
type DatabaseSync = import("node:sqlite").DatabaseSync;
|
|
428
|
+
/**
|
|
429
|
+
* SQLite-backed {@link IdentityKeystore} — a drop-in alternative to the
|
|
430
|
+
* file-per-slot {@link import('./identityKeystore').createIdentityKeystore}.
|
|
431
|
+
* All slot state (identity keypair, per-slot KV storage, the access-key gate,
|
|
432
|
+
* the consent snooze, and the recorded app-files folder) lives in **one**
|
|
433
|
+
* database file across three tables, so the whole keystore is a single
|
|
434
|
+
* `DatabaseSync` connection instead of four files per slot.
|
|
435
|
+
*
|
|
436
|
+
* The model is identical to the file keystore — `id` (the slot), `keys` (the
|
|
437
|
+
* gate), and `time` (the snooze) — even though config/CLI hubs only exercise
|
|
438
|
+
* the `id` + storage axes today. Keeping the full surface means the extension
|
|
439
|
+
* and node-runner can adopt this backend unchanged later.
|
|
440
|
+
*
|
|
441
|
+
* ## Encryption seam
|
|
442
|
+
*
|
|
443
|
+
* This backend currently runs in **unencrypted mode**: secret-bearing columns
|
|
444
|
+
* (identity private keys, storage values) are stored plaintext via the
|
|
445
|
+
* identity {@link _codec}. When at-rest encryption is added, swap {@link _codec}
|
|
446
|
+
* for an AEAD codec keyed by a `keystoreSecret` with `slotId` bound into the
|
|
447
|
+
* AAD (mirroring the file keystore's per-file AAD). The columns stay `TEXT`
|
|
448
|
+
* (ciphertext is base64url), so enabling encryption is a codec swap plus a
|
|
449
|
+
* one-time re-encode — not a schema migration.
|
|
450
|
+
*
|
|
451
|
+
* Uses the built-in `node:sqlite` `DatabaseSync`, matching the monorepo's
|
|
452
|
+
* other SQLite stores. The module is loaded only when a keystore opens a
|
|
453
|
+
* database, so consumers that only import the Hub package do not receive
|
|
454
|
+
* Node's SQLite experimental warning.
|
|
455
|
+
*/
|
|
456
|
+
interface SqliteIdentitySlot extends IdentitySlot {
|
|
457
|
+
/**
|
|
458
|
+
* Write a **specific** keypair into this slot iff it has no identity yet,
|
|
459
|
+
* and return the resulting identity. Used to import a legacy plaintext
|
|
460
|
+
* identity while preserving its principal. If an identity already exists
|
|
461
|
+
* it is returned unchanged (the import is a no-op).
|
|
462
|
+
*/
|
|
463
|
+
importIdentity(ed: Keypair, wrap: X25519Keypair): Promise<Identity>;
|
|
464
|
+
}
|
|
465
|
+
interface SqliteIdentityKeystore extends IdentityKeystore {
|
|
466
|
+
slotById(id: SlotId): SqliteIdentitySlot;
|
|
467
|
+
}
|
|
468
|
+
interface SqliteIdentityKeystoreOptions {
|
|
469
|
+
/**
|
|
470
|
+
* Path to the SQLite database file (created if missing) or `":memory:"`.
|
|
471
|
+
* Ignored when {@link db} is provided.
|
|
472
|
+
*/
|
|
473
|
+
readonly dbPath?: string;
|
|
474
|
+
/** An existing connection to reuse (share one file across stores). */
|
|
475
|
+
readonly db?: DatabaseSync;
|
|
476
|
+
}
|
|
477
|
+
/** Open (creating if needed) a SQLite-backed {@link IdentityKeystore}. */
|
|
478
|
+
declare function createSqliteIdentityKeystore(opts: SqliteIdentityKeystoreOptions): SqliteIdentityKeystore;
|
|
479
|
+
//#endregion
|
|
480
|
+
export { validatePrefix as C, ForwardingTable as S, withVerifiedSignature as _, IdentityKeystore as a, RegisteredServiceId as b, IdentitySnooze as c, SlotByIdAndTimeResult as d, SlotId as f, withFullyQualifiedCallGate as g, withForwardedCallGate as h, createSqliteIdentityKeystore as i, SlotAccessKey as l, ForwardedCallGateOptions as m, SqliteIdentityKeystoreOptions as n, IdentityKeystoreOptions as o, createIdentityKeystore as p, SqliteIdentitySlot as r, IdentitySlot as s, SqliteIdentityKeystore as t, SlotByIdAndKeyResult as u, MintCapabilityOptions as v, hubRegisterServiceId as x, mintCapability as y };
|
|
481
|
+
//# sourceMappingURL=index-CLIUrV88.d.ts.map
|