@1shotapi/ows-signer 0.3.2 → 0.3.4

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 CHANGED
@@ -58,6 +58,7 @@ Ceremony UI params (optional on WebAuthn-triggering methods): `explanationHeader
58
58
  - `rpName` — relying party display name in passkey UI (default `"OWS"`); does not affect `rpId`
59
59
  - `userDisplayName` — friendly account label (defaults to `name`)
60
60
  - `userId` — stable user handle bytes (base64); random if omitted
61
+ - `deferKeyDerivation` — when true, skip the PRF follow-up get; `CredentialCreated` includes `credentialId` + `cosePublicKey` only (no `KeyDerived` / no `secp256k1PublicKey`)
61
62
 
62
63
  ## Signing schemes
63
64
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1shotapi/ows-signer",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "OWS Signing Layer — plain JavaScript WebAuthn PRF key custody with curve-based signing (on-chain via EIP-8244)",
5
5
  "type": "module",
6
6
  "files": [
package/src/handlers.js CHANGED
@@ -37,6 +37,7 @@ import {
37
37
  promptPassphrase,
38
38
  promptPrivateKey,
39
39
  showPrivateKey,
40
+ updateCeremonyCopy,
40
41
  } from "./ui.js";
41
42
  import {
42
43
  createPasskeyCredential,
@@ -202,9 +203,14 @@ function ceremonyFieldsFromParams(params) {
202
203
  *
203
204
  * @param {PublicKeyCredential} credential
204
205
  * @param {string} [credentialId]
206
+ * @param {(() => void) | undefined} [onBeforeFollowUpGet]
205
207
  * @returns {Promise<PublicKeyCredential>}
206
208
  */
207
- async function credentialForKeyDerivation(credential, credentialId) {
209
+ async function credentialForKeyDerivation(
210
+ credential,
211
+ credentialId,
212
+ onBeforeFollowUpGet,
213
+ ) {
208
214
  debugLog("ceremony extension results", describePrfExtensionResults(credential));
209
215
 
210
216
  const rawPrf = credential.getClientExtensionResults()?.prf?.results?.first;
@@ -217,7 +223,11 @@ async function credentialForKeyDerivation(credential, credentialId) {
217
223
  debugLog("no PRF results on registration; running assertion with prf.eval", {
218
224
  credentialId: id,
219
225
  });
220
- // Same RPC — no second Confirm (one screen per call).
226
+ if (typeof onBeforeFollowUpGet === "function") {
227
+ onBeforeFollowUpGet();
228
+ }
229
+ // Prefer keeping the parent Confirm panel open (call from runOnConfirm) so
230
+ // the user still sees why this second WebAuthn prompt appears.
221
231
  const assertion = await getPasskeyAssertion(undefined, id);
222
232
  debugLog(
223
233
  "assertion extension results",
@@ -239,20 +249,54 @@ async function handleCreateCredential(params, correlationId, targetOrigin) {
239
249
  }
240
250
  const options =
241
251
  params.options && typeof params.options === "object"
242
- ? /** @type {{ rpName?: string, userDisplayName?: string, userId?: string, explanationHeader?: string, explanationText?: string, confirmButtonText?: string, denyButtonText?: string }} */ (
252
+ ? /** @type {{ rpName?: string, userDisplayName?: string, userId?: string, deferKeyDerivation?: boolean, explanationHeader?: string, explanationText?: string, confirmButtonText?: string, denyButtonText?: string }} */ (
243
253
  params.options
244
254
  )
245
255
  : {};
256
+ const deferKeyDerivation = options.deferKeyDerivation === true;
246
257
 
247
258
  await withCeremony(async () => {
248
- const credential = await promptCeremonyConfirm(options, () =>
249
- createPasskeyCredential(name, options),
250
- );
251
- const credentialId = getCredentialId(credential);
252
- const prfCredential = await credentialForKeyDerivation(
253
- credential,
254
- credentialId,
255
- );
259
+ /** @type {PublicKeyCredential | undefined} */
260
+ let registrationCredential;
261
+
262
+ if (deferKeyDerivation) {
263
+ // Registration only — opener (or another signer session) will unlock later.
264
+ await promptCeremonyConfirm(options, async () => {
265
+ registrationCredential = await createPasskeyCredential(name, options);
266
+ });
267
+ if (!registrationCredential) {
268
+ throw new Error("createCredential: registration credential missing");
269
+ }
270
+ emitEvent(window.parent, targetOrigin, "CredentialCreated", correlationId, {
271
+ credentialId: getCredentialId(registrationCredential),
272
+ cosePublicKey: getCosePublicKeyBase64Url(registrationCredential),
273
+ });
274
+ return;
275
+ }
276
+
277
+ // Create + optional PRF follow-up get stay under one Confirm panel so the
278
+ // explanation remains visible for every WebAuthn prompt in this RPC.
279
+ const prfCredential = await promptCeremonyConfirm(options, async () => {
280
+ registrationCredential = await createPasskeyCredential(name, options);
281
+ const credentialId = getCredentialId(registrationCredential);
282
+ return credentialForKeyDerivation(
283
+ registrationCredential,
284
+ credentialId,
285
+ () => {
286
+ updateCeremonyCopy({
287
+ explanationHeader: "Unlock your new passkey",
288
+ explanationText:
289
+ "Confirm with your passkey again to finish setting up this wallet.",
290
+ confirmButtonText: options.confirmButtonText,
291
+ denyButtonText: options.denyButtonText,
292
+ });
293
+ },
294
+ );
295
+ });
296
+ if (!registrationCredential) {
297
+ throw new Error("createCredential: registration credential missing");
298
+ }
299
+ const credentialId = getCredentialId(registrationCredential);
256
300
  const keys = await deriveKeysFromCredential(prfCredential);
257
301
  emitKeyDerived(
258
302
  targetOrigin,
@@ -263,7 +307,7 @@ async function handleCreateCredential(params, correlationId, targetOrigin) {
263
307
 
264
308
  emitEvent(window.parent, targetOrigin, "CredentialCreated", correlationId, {
265
309
  credentialId,
266
- cosePublicKey: getCosePublicKeyBase64Url(credential),
310
+ cosePublicKey: getCosePublicKeyBase64Url(registrationCredential),
267
311
  secp256k1PublicKey: to0xHex(keys.secp256k1PublicKey),
268
312
  });
269
313
  zeroize(keys.secp256k1PrivateKey);
package/src/ui.js CHANGED
@@ -131,6 +131,22 @@ export function resolveCeremonyUi(fields) {
131
131
  };
132
132
  }
133
133
 
134
+ /**
135
+ * Update header/body on an open ceremony Confirm panel (e.g. before a follow-up
136
+ * WebAuthn get). No-op if the ceremony UI is not showing.
137
+ *
138
+ * @param {CeremonyUiFields | Record<string, unknown> | undefined} fields
139
+ */
140
+ export function updateCeremonyCopy(fields) {
141
+ if (!root) return;
142
+ const header = root.querySelector(".ows-ceremony-header");
143
+ const text = root.querySelector(".ows-ceremony-text");
144
+ if (!header && !text) return;
145
+ const ui = resolveCeremonyUi(fields);
146
+ if (header) header.textContent = ui.header;
147
+ if (text) text.textContent = ui.explanation;
148
+ }
149
+
134
150
  /**
135
151
  * Show Confirm/Cancel. On Confirm, runs `runOnConfirm` **synchronously from the
136
152
  * click handler** so WebAuthn keeps user activation / document focus on mobile.