@openkey/sdk 0.9.0 → 0.10.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/index.d.ts CHANGED
@@ -1,3 +1,54 @@
1
+ /** NIP-01 unsigned event template. */
2
+ interface UnsignedNostrEvent {
3
+ pubkey: string;
4
+ created_at: number;
5
+ kind: number;
6
+ tags: string[][];
7
+ content: string;
8
+ }
9
+ /** A fully signed NIP-01 event, as returned by OpenKey custody signing. */
10
+ interface SignedNostrEvent extends UnsignedNostrEvent {
11
+ id: string;
12
+ sig: string;
13
+ }
14
+ /** Public descriptor for the user's OpenKey-custodied Nostr identity. Never includes the secret key. */
15
+ interface NostrIdentity {
16
+ keyId: string;
17
+ pubkey: string;
18
+ npub: string;
19
+ }
20
+ interface NostrError {
21
+ code: 'USER_CANCELLED' | 'TIMEOUT' | 'INTERACTION_REQUIRED' | 'UNAUTHORIZED' | 'UNKNOWN';
22
+ message: string;
23
+ }
24
+ /**
25
+ * Browser client for the OpenKey Nostr custody + signing flow. Get an
26
+ * instance from `OpenKey#nostr` rather than constructing this directly.
27
+ */
28
+ declare class OpenKeyNostr {
29
+ private host;
30
+ constructor(host: string);
31
+ /**
32
+ * Get-or-create the user's OpenKey-custodied Nostr identity. Always shows
33
+ * a visible consent card (identity creation, or - when `relayUrl` is
34
+ * given - a relay-scoped kind:22242/NIP-42 grant for that exact relay).
35
+ * Never resolves with the secret key; only `keyId`/`pubkey`/`npub`.
36
+ */
37
+ connect(opts?: {
38
+ relayUrl?: string;
39
+ }): Promise<NostrIdentity>;
40
+ /**
41
+ * Sign a NIP-01 event template with the user's OpenKey Nostr key. Silent
42
+ * (no visible UI) when an active grant already covers this client origin
43
+ * + key + kind (+ exact relay, for kind 22242). Otherwise shows a visible
44
+ * consent card before signing - required for every first kind:9 message to
45
+ * a new origin, per NIP-29 channel-message policy.
46
+ */
47
+ signEvent(keyId: string, event: UnsignedNostrEvent): Promise<SignedNostrEvent>;
48
+ private widgetUrl;
49
+ private run;
50
+ }
51
+
1
52
  interface EIP1193Provider {
2
53
  request(args: {
3
54
  method: string;
@@ -45,6 +96,20 @@ interface AuthResult {
45
96
  keyId: string;
46
97
  keyType: 'MANAGED' | 'EXTERNAL';
47
98
  }
99
+ /**
100
+ * Confirmation returned by {@link OpenKey.signOut}. `requestId` is generated
101
+ * by the SDK and echoed by the OpenKey-owned widget, so callers can associate
102
+ * the acknowledgement with the sign-out they initiated.
103
+ */
104
+ interface SignOutAcknowledgement {
105
+ requestId: string;
106
+ /**
107
+ * Whether the widget confirmed that the previous bearer no longer
108
+ * authenticates. `false` means the SDK is locally signed out, but remote
109
+ * revocation could not be verified.
110
+ */
111
+ revoked: boolean;
112
+ }
48
113
  interface OpenKeyError {
49
114
  code: 'USER_CANCELLED' | 'POPUP_BLOCKED' | 'TIMEOUT' | 'NO_KEY' | 'UNAUTHORIZED' | 'UNKNOWN' | 'STATE_MISMATCH';
50
115
  message: string;
@@ -74,6 +139,176 @@ interface OAuthTokenResponse {
74
139
  scope: string;
75
140
  refresh_token?: string;
76
141
  }
142
+ interface TinyCloudAuthorizationRequestV1 {
143
+ protocolVersion: 1;
144
+ /** Suggested SIWE (may be edited server-side before signing). */
145
+ siwe: string;
146
+ /** The keyId the user picked, if the caller has one in mind. */
147
+ keyId?: string;
148
+ /**
149
+ * The session-key JWK bound to `siwe`. Required so the server can
150
+ * regenerate a narrowed SIWE tied to the SAME session key when the
151
+ * user removes capabilities. Passing a mismatched jwk (or omitting it
152
+ * when the widget attempts to narrow) causes the /authorize-sign call
153
+ * to fail — never a silent fallback to the caller's exact bytes.
154
+ */
155
+ jwk?: Record<string, unknown>;
156
+ /**
157
+ * The TinyCloud host the resulting session will activate against.
158
+ * Bound into the /authorize-sign-prepare context so /authorize-sign
159
+ * cannot swap hosts server-side. Optional for callers that do not
160
+ * plan to activate a delegation (they should still forward it when
161
+ * known; the empty string is treated as "unknown").
162
+ */
163
+ host?: string;
164
+ /** Optional presentation envelope (name, reason, manifest). */
165
+ presentation?: CapabilityPresentationEnvelopeV1;
166
+ }
167
+ interface TinyCloudAuthorizationResultV1 {
168
+ protocolVersion: 1;
169
+ /** EIP-55 signer address. */
170
+ address: string;
171
+ /** Signature over `signedMessage`. */
172
+ signature: string;
173
+ /** The EXACT bytes the signature verifies against. Never the caller's original. */
174
+ signedMessage: string;
175
+ /** The action IDs the user (or default consent) selected. */
176
+ selectedActionKeys: string[];
177
+ /** Effective grant set after any narrowing. */
178
+ permissions: Array<{
179
+ service: string;
180
+ space: string;
181
+ path: string;
182
+ actions: string[];
183
+ }>;
184
+ }
185
+ interface CapabilityPresentationEnvelopeV1 {
186
+ protocolVersion: 1;
187
+ /** Human-readable requester name shown in the review. */
188
+ displayName?: string;
189
+ /**
190
+ * Reason string the requester supplied for the delegation. Rendered as
191
+ * caller-supplied (never as verified) unless a signed manifest carries
192
+ * the same reason string.
193
+ */
194
+ reason?: string;
195
+ /** Manifest ID + digest, when the app publishes a signed manifest. */
196
+ manifestId?: string;
197
+ manifestDigest?: string;
198
+ /**
199
+ * Full manifest payload(s) the caller wants surfaced in the review. The
200
+ * receiving route validates and size-bounds this before it is used. This
201
+ * is display-only: no metadata may expand authority beyond what the
202
+ * ReCap already grants. Bounded to a reasonable size at the transport
203
+ * boundary — oversized envelopes are dropped.
204
+ */
205
+ manifests?: Array<{
206
+ name?: string;
207
+ appId?: string;
208
+ payload?: Record<string, unknown>;
209
+ }>;
210
+ }
211
+ /**
212
+ * Sol final continuation contract requirement 5: pure validation of an
213
+ * incoming iframe resize message envelope. Exported so unit tests can
214
+ * exercise every rejection branch — wrong requestId, wrong version, no
215
+ * active request, malformed height — without booting the browser
216
+ * runtime that `IframeModal` requires.
217
+ *
218
+ * Returns the sanitized height when acceptable, or `null` when the
219
+ * message must be dropped.
220
+ */
221
+ declare function validateIframeResize(incoming: unknown, expected: {
222
+ requestId: string | null;
223
+ protocolVersion: number | null;
224
+ viewportHeight: number;
225
+ }): number | null;
226
+ /**
227
+ * Sol MAJOR-4 (final): pure routing helper — decides whether an
228
+ * `authorizeTinyCloud` call should route through the external-wallet
229
+ * preview→sign→finalize flow or the managed-key widget-signs-server-side
230
+ * flow. Exported so unit tests can exercise every branch
231
+ * (explicit-external + external-session, explicit-external + external-
232
+ * session with different keyId, no-explicit + external-session,
233
+ * explicit-managed + managed-session, no-session) without booting the
234
+ * browser runtime.
235
+ *
236
+ * The decision is authoritative from the ACTIVE session's keyType when
237
+ * no explicit `requestKeyId` was supplied.
238
+ *
239
+ * Kept as a thin wrapper for backwards compatibility. New callers should
240
+ * prefer `resolveAuthorizeTinyCloudRouting` which also considers the
241
+ * request's explicit `keyId`.
242
+ */
243
+ declare function shouldRouteAuthorizeTinyCloudExternal(state: {
244
+ lastAuth: {
245
+ keyType?: 'MANAGED' | 'EXTERNAL' | null;
246
+ } | null;
247
+ }): boolean;
248
+ /**
249
+ * Sol MAJOR-4 (final continuation): resolve the target key's identity and
250
+ * route by that key's type — NOT by `lastAuth.keyType` alone.
251
+ *
252
+ * Sol's rejection: when the caller supplies an explicit `requestKeyId`
253
+ * that identifies a DIFFERENT key than `lastAuth.keyId` (address vs
254
+ * internal id, or a genuinely different key), the SDK must
255
+ * - resolve THAT key's type,
256
+ * - route by that resolved type (external ⇒ wallet path, managed ⇒
257
+ * server-signing path), and
258
+ * - reject an inconsistent explicit pin whose resolved type contradicts
259
+ * the caller's other implicit assumptions (e.g. `lastAuth` is
260
+ * external but the caller pinned a managed key).
261
+ *
262
+ * Inputs:
263
+ * - `lastAuth`: the connected-session key metadata (may be null on
264
+ * boot; the widget renders a connect flow first in that case).
265
+ * - `requestKeyId`: the caller's explicit pin (may be absent).
266
+ * - `knownKeys`: any keys we have metadata for; the SDK's higher-level
267
+ * `authorizeTinyCloud` populates this from the server before calling
268
+ * the resolver so the pure helper is fully synchronous and testable.
269
+ *
270
+ * Return shape:
271
+ * { route: 'external' | 'managed', resolvedKeyId: string | null,
272
+ * resolvedKeyType: 'EXTERNAL' | 'MANAGED' | null }
273
+ *
274
+ * `resolvedKeyId` is null only when neither `requestKeyId` nor
275
+ * `lastAuth.keyId` exists. When both exist and identify the same key
276
+ * (matching id OR matching known-key type), the resolver returns that
277
+ * shared identity. When both exist and identify different keys AND we
278
+ * know both types AND those types disagree, the resolver throws
279
+ * `KEY_ID_TYPE_MISMATCH` so no silent route coercion occurs.
280
+ */
281
+ interface AuthorizeTinyCloudRouting {
282
+ route: 'external' | 'managed';
283
+ resolvedKeyId: string | null;
284
+ resolvedKeyType: 'EXTERNAL' | 'MANAGED' | null;
285
+ }
286
+ declare class KeyIdTypeMismatchError extends Error {
287
+ code: 'KEY_ID_TYPE_MISMATCH';
288
+ requestKeyId: string;
289
+ lastAuthKeyId: string | null;
290
+ requestKeyType: 'EXTERNAL' | 'MANAGED' | null;
291
+ lastAuthKeyType: 'EXTERNAL' | 'MANAGED' | null;
292
+ constructor(fields: {
293
+ message: string;
294
+ requestKeyId: string;
295
+ lastAuthKeyId: string | null;
296
+ requestKeyType: 'EXTERNAL' | 'MANAGED' | null;
297
+ lastAuthKeyType: 'EXTERNAL' | 'MANAGED' | null;
298
+ });
299
+ }
300
+ declare function resolveAuthorizeTinyCloudRouting(state: {
301
+ lastAuth: {
302
+ keyId?: string | null;
303
+ keyType?: 'MANAGED' | 'EXTERNAL' | null;
304
+ } | null;
305
+ requestKeyId?: string | null;
306
+ knownKeys?: ReadonlyArray<{
307
+ id: string;
308
+ address?: string;
309
+ keyType: 'MANAGED' | 'EXTERNAL';
310
+ }>;
311
+ }): AuthorizeTinyCloudRouting;
77
312
  declare class OpenKey {
78
313
  private host;
79
314
  private oauthHost;
@@ -84,6 +319,16 @@ declare class OpenKey {
84
319
  private lastAuth;
85
320
  private discoveredProviders;
86
321
  private sessionToken;
322
+ /** Cancels widget flows that could outlive a sign-out on this instance. */
323
+ private activeFlowCancellations;
324
+ /**
325
+ * Nostr identity custody + signing (secp256k1 Schnorr / BIP-340). Kept
326
+ * fully separate from the Ethereum flows above: it never reuses
327
+ * `lastAuth`/`sessionToken`, and its widget messages never carry a
328
+ * sessionToken - the OpenKey session and any signing grants stay inside
329
+ * the OpenKey-origin iframe for the whole lifetime of this client.
330
+ */
331
+ readonly nostr: OpenKeyNostr;
87
332
  constructor(config?: OpenKeyConfig);
88
333
  /**
89
334
  * Derive the OAuth API host from the main host by prefixing 'api.'
@@ -91,6 +336,20 @@ declare class OpenKey {
91
336
  * @returns The derived OAuth API host URL
92
337
  */
93
338
  private deriveOAuthHost;
339
+ /**
340
+ * Sol MAJOR-4 (final continuation): fetch the caller's registered keys
341
+ * so `resolveAuthorizeTinyCloudRouting` can resolve an explicit pin
342
+ * whose keyId does not match the active session key. Called only when
343
+ * the caller passes a `requestKeyId` we do not already recognise; the
344
+ * common no-pin path skips the network round-trip.
345
+ *
346
+ * Failures do NOT block routing — we fall through with `knownKeys=[]`
347
+ * so the resolver's lastAuth-based fallback still runs. If the caller
348
+ * pinned a genuinely unknown key that turns out to be external, the
349
+ * widget rejects the server-signing path and the SDK surfaces that
350
+ * error from the finalize call rather than silently routing wrong.
351
+ */
352
+ private fetchKnownKeysIfNeeded;
94
353
  /**
95
354
  * Connect to OpenKey and get user's wallet address
96
355
  * Opens auth flow for user to select/create a key
@@ -98,6 +357,17 @@ declare class OpenKey {
98
357
  connect(opts?: {
99
358
  mode?: OpenKeyMode;
100
359
  }): Promise<AuthResult>;
360
+ /**
361
+ * Signs the active OpenKey account out through the OpenKey-owned widget.
362
+ *
363
+ * The SDK passes its Better Auth bearer through the OpenKey-owned widget
364
+ * protocol so applications do not have to implement revocation themselves.
365
+ * Local SDK session state is cleared before opening the widget, including
366
+ * when remote revocation is unavailable.
367
+ */
368
+ signOut(opts?: {
369
+ mode?: OpenKeyMode;
370
+ }): Promise<SignOutAcknowledgement>;
101
371
  /**
102
372
  * Better-auth session token relayed during connect(), or null if the
103
373
  * flow did not provide one. Use as a bearer token for API calls made
@@ -130,6 +400,47 @@ declare class OpenKey {
130
400
  signMessage(request: SignMessageRequest, opts?: {
131
401
  mode?: OpenKeyMode;
132
402
  }): Promise<SignResult>;
403
+ /**
404
+ * Versioned TinyCloud authorization protocol (v1).
405
+ *
406
+ * Unlike `signMessage()` (which is contractually byte-exact — the returned
407
+ * signature always verifies against the caller's exact bytes), this method
408
+ * permits the user to narrow the requested capabilities before signing.
409
+ * OpenKey may therefore regenerate the SIWE. The response carries
410
+ * `signedMessage` — the exact bytes the signature actually verifies against.
411
+ *
412
+ * TinyCloud completes the session with `signedMessage`, not the original
413
+ * `siwe` passed in. Consumers MUST use the returned bytes and MUST NOT
414
+ * assume the returned grants are a superset of what they requested — the
415
+ * user is allowed to remove any grant that is not required.
416
+ *
417
+ * Legacy `signMessage()` callers are unaffected: their exact-byte contract
418
+ * remains preserved and OpenKey never rewrites their bytes.
419
+ */
420
+ authorizeTinyCloud(request: TinyCloudAuthorizationRequestV1, opts?: {
421
+ mode?: OpenKeyMode;
422
+ }): Promise<TinyCloudAuthorizationResultV1>;
423
+ /**
424
+ * Sol MAJOR-3 (continuation): authorizeTinyCloud path for external keys.
425
+ * The wallet lives outside OpenKey, so the SDK opens the shared widget
426
+ * for review + preview, then invokes the user's wallet to sign the
427
+ * preview bytes, then finalizes server-side via /authorize-sign.
428
+ *
429
+ * Flow:
430
+ * 1. Open the widget with `externalSign: true` — the widget renders
431
+ * the shared SigningApproval UI, calls /authorize-sign-prepare,
432
+ * calls /authorize-sign-preview after the user approves a
433
+ * selection, and hands back `{ previewApprovalToken,
434
+ * signedMessage, selectedActionIds, address, authorizationContextToken }`.
435
+ * 2. SDK invokes the user's wallet on the returned signedMessage.
436
+ * 3. SDK POSTs /authorize-sign with `externalSignature`.
437
+ *
438
+ * Legacy exact-byte signMessage callers are unaffected — this path is
439
+ * only taken via authorizeTinyCloud (protocolVersion:1) when the target
440
+ * key is EXTERNAL. Sol's rejection required this path to go through
441
+ * the same review UI as managed keys.
442
+ */
443
+ private authorizeTinyCloudExternal;
133
444
  /**
134
445
  * Sign typed data (EIP-712) with the user's OpenKey wallet
135
446
  * For external keys, routes directly to the user's wallet provider
@@ -199,9 +510,11 @@ declare class OpenKey {
199
510
  private hasDetectedEoa;
200
511
  private openFlow;
201
512
  private openIframeModal;
513
+ private isResponseForAction;
202
514
  private handleWalletLinkDelegation;
203
515
  private handleExternalWalletConnect;
204
516
  private openPopup;
517
+ private cancelActiveFlows;
205
518
  /**
206
519
  * Disconnect and close any open flows
207
520
  */
@@ -227,4 +540,4 @@ declare class OpenKeyProvider implements EIP1193Provider {
227
540
 
228
541
  declare function getOpenKey(config?: OpenKeyConfig): OpenKey;
229
542
 
230
- export { type AuthResult, type EIP1193Provider, type OAuthConfig, type OAuthResult, type OAuthTokenResponse, OpenKey, type OpenKeyConfig, type OpenKeyError, type OpenKeyMode, OpenKeyProvider, type SignMessageRequest, type SignResult, type SignTypedDataRequest, OpenKey as default, getOpenKey };
543
+ export { type AuthResult, type AuthorizeTinyCloudRouting, type CapabilityPresentationEnvelopeV1, type EIP1193Provider, KeyIdTypeMismatchError, type NostrError, type NostrIdentity, type OAuthConfig, type OAuthResult, type OAuthTokenResponse, OpenKey, type OpenKeyConfig, type OpenKeyError, type OpenKeyMode, OpenKeyNostr, OpenKeyProvider, type SignMessageRequest, type SignOutAcknowledgement, type SignResult, type SignTypedDataRequest, type SignedNostrEvent, type TinyCloudAuthorizationRequestV1, type TinyCloudAuthorizationResultV1, type UnsignedNostrEvent, OpenKey as default, getOpenKey, resolveAuthorizeTinyCloudRouting, shouldRouteAuthorizeTinyCloudExternal, validateIframeResize };