@tempoxyz/id 0.0.1 → 0.0.2
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/app/server/.vite/manifest.json +65 -65
- package/dist/app/server/assets/Grant-D_tun0mF.js.map +1 -1
- package/dist/app/server/assets/authorize-DXYo-K2n-B-6vqhvc.js +56 -0
- package/dist/app/server/assets/authorize-DXYo-K2n-B-6vqhvc.js.map +1 -0
- package/dist/app/server/assets/core-FSS9-r35.js +56 -0
- package/dist/app/server/assets/core-FSS9-r35.js.map +1 -0
- package/dist/app/server/assets/default-u_dwuiYb-Bby14ucv.js +2 -0
- package/dist/app/server/assets/{default-u_dwuiYb-DOrMMcg9.js.map → default-u_dwuiYb-Bby14ucv.js.map} +1 -1
- package/dist/app/server/assets/{dist-CoH0hD1s.js → dist-B9KiBJYw.js} +2 -2
- package/dist/app/server/assets/{dist-CoH0hD1s.js.map → dist-B9KiBJYw.js.map} +1 -1
- package/dist/app/server/assets/{dist-DcICI3fd.js → dist-CU1NXSiz.js} +2 -2
- package/dist/app/server/assets/{dist-DcICI3fd.js.map → dist-CU1NXSiz.js.map} +1 -1
- package/dist/app/server/assets/dist-CetH4JSY.js +1 -0
- package/dist/app/server/assets/{effect-QlVUlMFu-mXPkuQTb.js → effect-QlVUlMFu-Cm-eTtaO.js} +2 -2
- package/dist/app/server/assets/{effect-QlVUlMFu-mXPkuQTb.js.map → effect-QlVUlMFu-Cm-eTtaO.js.map} +1 -1
- package/dist/app/server/assets/factory-g3i_Ffi5.js +5 -0
- package/dist/app/server/assets/{factory-dvoOlKer.js.map → factory-g3i_Ffi5.js.map} +1 -1
- package/dist/app/server/assets/get-id-field-BpOAoqqI.js +6 -0
- package/dist/app/server/assets/get-id-field-BpOAoqqI.js.map +1 -0
- package/dist/app/server/assets/{index-CLddUTqr-CC33CMvS.js → index-CLddUTqr-CmgkbWi9.js} +2 -2
- package/dist/app/server/assets/{index-CLddUTqr-CC33CMvS.js.map → index-CLddUTqr-CmgkbWi9.js.map} +1 -1
- package/dist/app/server/assets/{index-DZEfthgZ-Ec2MMQ_e.js → index-DZEfthgZ-BnPmps9O.js} +2 -2
- package/dist/app/server/assets/{index-DZEfthgZ-Ec2MMQ_e.js.map → index-DZEfthgZ-BnPmps9O.js.map} +1 -1
- package/dist/app/server/assets/kysely-adapter-B8VoPArC.js +1 -0
- package/dist/app/server/assets/{schema-check-oLqvHR8y.js → schema-check-Bgwl1v0_.js} +2 -2
- package/dist/app/server/assets/{schema-check-oLqvHR8y.js.map → schema-check-Bgwl1v0_.js.map} +1 -1
- package/dist/app/server/assets/{sury-CWZTCd75-CDaOS2oM.js → sury-CWZTCd75-Bd_PzqCV.js} +2 -2
- package/dist/app/server/assets/{sury-CWZTCd75-CDaOS2oM.js.map → sury-CWZTCd75-Bd_PzqCV.js.map} +1 -1
- package/dist/app/server/assets/{valibot--1zFm7rT-CIKoH3MS.js → valibot--1zFm7rT-Dd2aEhWG.js} +2 -2
- package/dist/app/server/assets/{valibot--1zFm7rT-CIKoH3MS.js.map → valibot--1zFm7rT-Dd2aEhWG.js.map} +1 -1
- package/dist/app/server/assets/{valibot-D_HTw1Gn-BdPyLkXt.js → valibot-D_HTw1Gn-C1l3va6F.js} +2 -2
- package/dist/app/server/assets/{valibot-D_HTw1Gn-BdPyLkXt.js.map → valibot-D_HTw1Gn-C1l3va6F.js.map} +1 -1
- package/dist/app/server/assets/{zod-Bwrt9trS-BezI5Gi1.js → zod-Bwrt9trS-Dry2w0in.js} +2 -2
- package/dist/app/server/assets/{zod-Bwrt9trS-BezI5Gi1.js.map → zod-Bwrt9trS-Dry2w0in.js.map} +1 -1
- package/dist/app/server/assets/{zod-DSgpEGAE-DZ7M_Q33.js → zod-DSgpEGAE-BZMP86UA.js} +2 -2
- package/dist/app/server/assets/{zod-DSgpEGAE-DZ7M_Q33.js.map → zod-DSgpEGAE-BZMP86UA.js.map} +1 -1
- package/dist/app/server/index.js +35 -35
- package/dist/app/server/index.js.map +1 -1
- package/dist/client/Auth.d.ts +1 -1
- package/dist/client/Auth.d.ts.map +1 -1
- package/dist/client/Auth.js.map +1 -1
- package/dist/client/Decorator.d.ts +3 -1
- package/dist/client/Decorator.d.ts.map +1 -1
- package/dist/client/Decorator.js +1 -0
- package/dist/client/Decorator.js.map +1 -1
- package/dist/client/actions/passkey.d.ts +63 -2
- package/dist/client/actions/passkey.d.ts.map +1 -1
- package/dist/client/actions/passkey.js +121 -3
- package/dist/client/actions/passkey.js.map +1 -1
- package/dist/internal/Apps.d.ts +14 -0
- package/dist/internal/Apps.d.ts.map +1 -1
- package/dist/internal/Apps.js +14 -2
- package/dist/internal/Apps.js.map +1 -1
- package/dist/internal/Auth.d.ts +7 -0
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +2 -1
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Authorize.d.ts.map +1 -1
- package/dist/internal/Authorize.js +13 -13
- package/dist/internal/Authorize.js.map +1 -1
- package/dist/internal/ErrorResponse.d.ts +5 -0
- package/dist/internal/ErrorResponse.d.ts.map +1 -1
- package/dist/internal/ErrorResponse.js +8 -0
- package/dist/internal/ErrorResponse.js.map +1 -1
- package/dist/internal/KeyAuthorization.d.ts.map +1 -1
- package/dist/internal/KeyAuthorization.js +28 -28
- package/dist/internal/KeyAuthorization.js.map +1 -1
- package/dist/internal/Owner.d.ts +16 -0
- package/dist/internal/Owner.d.ts.map +1 -1
- package/dist/internal/Owner.js.map +1 -1
- package/dist/internal/Passkey.d.ts +66 -4
- package/dist/internal/Passkey.d.ts.map +1 -1
- package/dist/internal/Passkey.js +66 -30
- package/dist/internal/Passkey.js.map +1 -1
- package/dist/internal/Privy.d.ts +27 -0
- package/dist/internal/Privy.d.ts.map +1 -1
- package/dist/internal/Privy.js +82 -0
- package/dist/internal/Privy.js.map +1 -1
- package/dist/internal/TempoAccount.d.ts +57 -0
- package/dist/internal/TempoAccount.d.ts.map +1 -1
- package/dist/internal/TempoAccount.js +118 -5
- package/dist/internal/TempoAccount.js.map +1 -1
- package/dist/internal/owners/Privy.d.ts.map +1 -1
- package/dist/internal/owners/Privy.js +11 -2
- package/dist/internal/owners/Privy.js.map +1 -1
- package/package.json +1 -1
- package/src/api/account.ts +140 -103
- package/src/api/authenticate.ts +48 -51
- package/src/api/authorize.ts +35 -36
- package/src/api/device.ts +20 -23
- package/src/api/health.ts +1 -2
- package/src/api/lib/AccountStep.ts +75 -0
- package/src/api/lib/Auth.ts +6 -0
- package/src/api/lib/Cors.ts +3 -0
- package/src/api/lib/KeySigner.ts +1 -1
- package/src/api/oauth.ts +23 -22
- package/src/client/Auth.ts +1 -0
- package/src/client/Decorator.ts +6 -1
- package/src/client/actions/passkey.ts +197 -3
- package/src/internal/Apps.ts +26 -2
- package/src/internal/Auth.ts +4 -0
- package/src/internal/Authorize.ts +13 -15
- package/src/internal/ErrorResponse.ts +10 -0
- package/src/internal/KeyAuthorization.ts +28 -31
- package/src/internal/OpenApi.ts +35 -0
- package/src/internal/Owner.ts +16 -0
- package/src/internal/Passkey.ts +99 -34
- package/src/internal/Privy.ts +128 -0
- package/src/internal/TempoAccount.ts +221 -7
- package/src/internal/owners/Privy.ts +19 -6
- package/dist/app/server/assets/authorize-DXYo-K2n-rmQQ42x1.js +0 -56
- package/dist/app/server/assets/authorize-DXYo-K2n-rmQQ42x1.js.map +0 -1
- package/dist/app/server/assets/core-BBrZXfrB.js +0 -56
- package/dist/app/server/assets/core-BBrZXfrB.js.map +0 -1
- package/dist/app/server/assets/default-u_dwuiYb-DOrMMcg9.js +0 -2
- package/dist/app/server/assets/dist-CR6FW-yS.js +0 -1
- package/dist/app/server/assets/factory-dvoOlKer.js +0 -5
- package/dist/app/server/assets/get-id-field-OwLpfjj2.js +0 -6
- package/dist/app/server/assets/get-id-field-OwLpfjj2.js.map +0 -1
- package/dist/app/server/assets/kysely-adapter-B8tQwTcu.js +0 -1
package/src/internal/Passkey.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { Address, Base64, Hash, Hex, P256, PublicKey } from 'ox'
|
|
|
17
17
|
import { Authentication, Registration } from 'webauthx/server'
|
|
18
18
|
import { z } from 'zod'
|
|
19
19
|
import * as Schema from '../app/db/schema.js'
|
|
20
|
-
import
|
|
20
|
+
import * as Apps from './Apps.js'
|
|
21
21
|
import * as Authorize from './Authorize.js'
|
|
22
22
|
import * as ErrorResponse from './ErrorResponse.js'
|
|
23
23
|
import * as KeyAuthorization from './KeyAuthorization.js'
|
|
@@ -57,8 +57,8 @@ export class LinkExpiredError extends ErrorResponse.define(
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
* A passkey sign-in names an app, or reaches a realm, that holds no passkeys for
|
|
61
|
-
* passkeys whose relying party covers the app's page.
|
|
60
|
+
* A passkey sign-in or registration names an app, or reaches a realm, that holds no passkeys for
|
|
61
|
+
* Tempo ID, or no passkeys whose relying party covers the app's page.
|
|
62
62
|
*/
|
|
63
63
|
export class NotPermittedError extends ErrorResponse.define(
|
|
64
64
|
'passkey_not_permitted',
|
|
@@ -79,6 +79,12 @@ export class NotPermittedError extends ErrorResponse.define(
|
|
|
79
79
|
error_description: `Passkey not permitted: the app ${clientId} holds no passkeys. Sign in with an emailed code.`,
|
|
80
80
|
meta: { client_id: clientId },
|
|
81
81
|
}
|
|
82
|
+
// A registration names no app, only the page that runs it.
|
|
83
|
+
if (origin)
|
|
84
|
+
return {
|
|
85
|
+
error_description: `Passkey not permitted: no realm holds passkeys for ${origin}. Serve the page on the domain of the relying party, or one of its subdomains.`,
|
|
86
|
+
meta: { origin },
|
|
87
|
+
}
|
|
82
88
|
return {
|
|
83
89
|
error_description:
|
|
84
90
|
'Passkey not permitted: no realm holds passkeys for this sign-in. Sign in with an emailed code.',
|
|
@@ -113,6 +119,32 @@ export class ReauthenticationRequiredError extends ErrorResponse.define(
|
|
|
113
119
|
}
|
|
114
120
|
}
|
|
115
121
|
|
|
122
|
+
/**
|
|
123
|
+
* A passkey registration comes from an app's page, and no app at the page's origin holds the
|
|
124
|
+
* `tempo-id:sign` scope. A registered passkey signs in as the user, so only an app that the
|
|
125
|
+
* deployment trusts with its own challenges registers one.
|
|
126
|
+
*/
|
|
127
|
+
export class RegisterNotPermittedError extends ErrorResponse.define(
|
|
128
|
+
'passkey_register_not_permitted',
|
|
129
|
+
) {
|
|
130
|
+
override readonly name = 'Passkey.RegisterNotPermittedError'
|
|
131
|
+
|
|
132
|
+
constructor(options: RegisterNotPermittedError.Options) {
|
|
133
|
+
const { origin } = options
|
|
134
|
+
super({
|
|
135
|
+
error_description: `Passkey registration not permitted: no app at ${origin} holds the \`${Apps.signScope}\` scope. Register the passkey on Tempo ID, or grant the app the scope.`,
|
|
136
|
+
meta: { origin, scope: Apps.signScope },
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export declare namespace RegisterNotPermittedError {
|
|
142
|
+
type Options = {
|
|
143
|
+
/** The origin of the app's page that sent the request. */
|
|
144
|
+
origin: string
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
116
148
|
/**
|
|
117
149
|
* A passkey ceremony failed: its challenge is unknown, spent, or expired, or the response does not
|
|
118
150
|
* verify. One refusal covers every case, so it reveals neither which check failed nor whether a
|
|
@@ -186,6 +218,10 @@ export type ProofChallenge = {
|
|
|
186
218
|
challenge: Hex.Hex
|
|
187
219
|
/** The credential ID of the new passkey, base64url. */
|
|
188
220
|
id: string
|
|
221
|
+
/**
|
|
222
|
+
* The origin of the app's page that the ceremony runs at, or `undefined` for Tempo ID's own page.
|
|
223
|
+
*/
|
|
224
|
+
origin: string | undefined
|
|
189
225
|
/** The public key that the attestation holds, which the proof must verify against. */
|
|
190
226
|
publicKey: Hex.Hex
|
|
191
227
|
/** The relying party of the `default` realm, which the new passkey belongs to. */
|
|
@@ -196,6 +232,10 @@ export type ProofChallenge = {
|
|
|
196
232
|
export type RegistrationChallenge = {
|
|
197
233
|
/** The challenge, as hex. */
|
|
198
234
|
challenge: Hex.Hex
|
|
235
|
+
/**
|
|
236
|
+
* The origin of the app's page that the ceremony runs at, or `undefined` for Tempo ID's own page.
|
|
237
|
+
*/
|
|
238
|
+
origin: string | undefined
|
|
199
239
|
/** The relying party of the `default` realm, which the new passkey must belong to. */
|
|
200
240
|
rpId: string
|
|
201
241
|
}
|
|
@@ -311,7 +351,7 @@ export namespace schema {
|
|
|
311
351
|
.optional()
|
|
312
352
|
.meta({
|
|
313
353
|
description:
|
|
314
|
-
'The character offset of `"challenge"` in `clientDataJSON`. Absent
|
|
354
|
+
'The character offset of `"challenge"` in `clientDataJSON`. Absent to search for it.',
|
|
315
355
|
examples: [23],
|
|
316
356
|
}),
|
|
317
357
|
clientDataJSON: z
|
|
@@ -331,7 +371,7 @@ export namespace schema {
|
|
|
331
371
|
.optional()
|
|
332
372
|
.meta({
|
|
333
373
|
description:
|
|
334
|
-
'The character offset of `"type"` in `clientDataJSON`. Absent
|
|
374
|
+
'The character offset of `"type"` in `clientDataJSON`. Absent to search for it.',
|
|
335
375
|
examples: [1],
|
|
336
376
|
}),
|
|
337
377
|
userVerificationRequired: z
|
|
@@ -339,13 +379,12 @@ export namespace schema {
|
|
|
339
379
|
.optional()
|
|
340
380
|
.meta({
|
|
341
381
|
description:
|
|
342
|
-
'Whether the client required user verification.
|
|
382
|
+
'Whether the client required user verification. Ignored, since Tempo ID always requires it.',
|
|
343
383
|
examples: [true],
|
|
344
384
|
}),
|
|
345
385
|
})
|
|
346
386
|
.meta({
|
|
347
|
-
description:
|
|
348
|
-
'The signed data that verification reads with `signature`, decoded from the browser response.',
|
|
387
|
+
description: "The signed data, decoded from the browser's response.",
|
|
349
388
|
}),
|
|
350
389
|
raw: z
|
|
351
390
|
.object({
|
|
@@ -355,7 +394,7 @@ export namespace schema {
|
|
|
355
394
|
.default(null)
|
|
356
395
|
.meta({
|
|
357
396
|
description:
|
|
358
|
-
'How the authenticator
|
|
397
|
+
'How the authenticator connects, such as `platform` or `cross-platform`, or `null`.',
|
|
359
398
|
examples: ['platform'],
|
|
360
399
|
}),
|
|
361
400
|
id: base64url.meta({
|
|
@@ -379,7 +418,7 @@ export namespace schema {
|
|
|
379
418
|
})
|
|
380
419
|
.meta({
|
|
381
420
|
description:
|
|
382
|
-
"The authenticator's response
|
|
421
|
+
"The authenticator's response, of which Tempo ID reads only `clientDataJSON`.",
|
|
383
422
|
}),
|
|
384
423
|
type: z.string().meta({
|
|
385
424
|
description: 'The credential type.',
|
|
@@ -399,7 +438,8 @@ export namespace schema {
|
|
|
399
438
|
}),
|
|
400
439
|
})
|
|
401
440
|
.meta({
|
|
402
|
-
description:
|
|
441
|
+
description:
|
|
442
|
+
'The result of `navigator.credentials.get()`, as `webauthx/client` serializes it.',
|
|
403
443
|
id: 'PasskeyAssertion',
|
|
404
444
|
})
|
|
405
445
|
|
|
@@ -433,7 +473,7 @@ export namespace schema {
|
|
|
433
473
|
.templateLiteral(['0x', z.string().regex(/^04[0-9a-fA-F]{128}$/)])
|
|
434
474
|
.meta({
|
|
435
475
|
description:
|
|
436
|
-
"The new passkey's uncompressed P256 public key, as hex.
|
|
476
|
+
"The new passkey's uncompressed P256 public key, as hex. Ignored, since Tempo ID reads the key from `attestationObject`.",
|
|
437
477
|
examples: [
|
|
438
478
|
'0x04f6310589abb21116acc6e123967ed0a125b5041c43f8e157237093e07eaa354c6c3f1e0a9b7d24f58e1c3a6b9d0f2e4a7c5b8d1e3f6a9c2b4d7e0f1a3c5b8d',
|
|
439
479
|
],
|
|
@@ -446,7 +486,7 @@ export namespace schema {
|
|
|
446
486
|
.default(null)
|
|
447
487
|
.meta({
|
|
448
488
|
description:
|
|
449
|
-
'How the authenticator
|
|
489
|
+
'How the authenticator connects, such as `platform` or `cross-platform`, or `null`.',
|
|
450
490
|
examples: ['platform'],
|
|
451
491
|
}),
|
|
452
492
|
id: base64url.max(1364).meta({
|
|
@@ -471,7 +511,7 @@ export namespace schema {
|
|
|
471
511
|
})
|
|
472
512
|
.meta({
|
|
473
513
|
description:
|
|
474
|
-
"The authenticator's response
|
|
514
|
+
"The authenticator's response, of which Tempo ID reads only `clientDataJSON`.",
|
|
475
515
|
}),
|
|
476
516
|
type: z
|
|
477
517
|
.string()
|
|
@@ -488,7 +528,7 @@ export namespace schema {
|
|
|
488
528
|
})
|
|
489
529
|
.meta({
|
|
490
530
|
description:
|
|
491
|
-
'The
|
|
531
|
+
'The result of `navigator.credentials.create()`, as `webauthx/client` serializes it.',
|
|
492
532
|
id: 'PasskeyCredential',
|
|
493
533
|
})
|
|
494
534
|
|
|
@@ -510,12 +550,16 @@ export namespace schema {
|
|
|
510
550
|
/** A proof challenge's `verification` value. */
|
|
511
551
|
export const proof = z.object({
|
|
512
552
|
id: z.string(),
|
|
553
|
+
// A challenge without an origin runs at Tempo ID's own page.
|
|
554
|
+
origin: z.string().optional(),
|
|
513
555
|
publicKey: hex,
|
|
514
556
|
rpId: z.string(),
|
|
515
557
|
})
|
|
516
558
|
|
|
517
559
|
/** A registration challenge's `verification` value. */
|
|
518
560
|
export const registration = z.object({
|
|
561
|
+
// A challenge without an origin runs at Tempo ID's own page.
|
|
562
|
+
origin: z.string().optional(),
|
|
519
563
|
rpId: z.string(),
|
|
520
564
|
})
|
|
521
565
|
|
|
@@ -558,11 +602,11 @@ export namespace schema {
|
|
|
558
602
|
)
|
|
559
603
|
.meta({
|
|
560
604
|
description:
|
|
561
|
-
"The account's passkey, the only
|
|
605
|
+
"The account's passkey, which is the only passkey that the browser offers.",
|
|
562
606
|
}),
|
|
563
607
|
challenge: z.templateLiteral(['0x', z.string()]).meta({
|
|
564
608
|
description:
|
|
565
|
-
"The key authorization's digest, as hex
|
|
609
|
+
"The key authorization's digest, as hex, which expires after 5 minutes.",
|
|
566
610
|
examples: [
|
|
567
611
|
'0xf6310589abb21116acc6e123967ed0a125b5041c43f8e157237093e07eaa354c',
|
|
568
612
|
],
|
|
@@ -574,30 +618,29 @@ export namespace schema {
|
|
|
574
618
|
}),
|
|
575
619
|
userVerification: z.literal('required').meta({
|
|
576
620
|
description:
|
|
577
|
-
'
|
|
621
|
+
'Requires user verification, such as a biometric or PIN.',
|
|
578
622
|
}),
|
|
579
623
|
})
|
|
580
624
|
.meta({
|
|
581
625
|
description:
|
|
582
|
-
'The `PublicKeyCredentialRequestOptions`,
|
|
626
|
+
'The `PublicKeyCredentialRequestOptions`, with the challenge as hex.',
|
|
583
627
|
}),
|
|
584
628
|
})
|
|
585
629
|
.meta({
|
|
586
|
-
description: 'The
|
|
630
|
+
description: 'The argument for `navigator.credentials.get()`.',
|
|
587
631
|
}),
|
|
588
632
|
type: z.literal('passkey').meta({
|
|
589
633
|
description:
|
|
590
|
-
'Sign the
|
|
634
|
+
'Sign the challenge with the passkey, then repeat the request with `passkey: { assertion }`.',
|
|
591
635
|
}),
|
|
592
636
|
})
|
|
593
637
|
.meta({
|
|
594
638
|
description:
|
|
595
|
-
"The next step: the passkey's signature
|
|
639
|
+
"The next step: the passkey's signature of the key authorization.",
|
|
596
640
|
}),
|
|
597
641
|
})
|
|
598
642
|
.meta({
|
|
599
|
-
description:
|
|
600
|
-
'An approval that continues when the passkey signs the key authorization.',
|
|
643
|
+
description: 'An approval that waits for the passkey to sign the key.',
|
|
601
644
|
id: 'PasskeyStep',
|
|
602
645
|
})
|
|
603
646
|
|
|
@@ -609,24 +652,24 @@ export namespace schema {
|
|
|
609
652
|
.int()
|
|
610
653
|
.meta({
|
|
611
654
|
description:
|
|
612
|
-
"The key's expiry in Unix seconds.
|
|
655
|
+
"The key's expiry, in Unix seconds. Send it as `key_authorization.expiry` in the repeat, so the digest stays the same.",
|
|
613
656
|
examples: [1791869364],
|
|
614
657
|
}),
|
|
615
658
|
step: step.shape.step
|
|
616
659
|
.extend({
|
|
617
660
|
type: z.literal('passkey').meta({
|
|
618
661
|
description:
|
|
619
|
-
'Sign the
|
|
662
|
+
'Sign the challenge with the passkey, then repeat `POST /v1/authorize` with `passkey: { assertion }`.',
|
|
620
663
|
}),
|
|
621
664
|
})
|
|
622
665
|
.meta({
|
|
623
666
|
description:
|
|
624
|
-
"The next step: the passkey's signature
|
|
667
|
+
"The next step: the passkey's signature of the key authorization.",
|
|
625
668
|
}),
|
|
626
669
|
})
|
|
627
670
|
.meta({
|
|
628
671
|
description:
|
|
629
|
-
"A headless authorization that
|
|
672
|
+
"A headless authorization that waits for the account's passkey to sign the key.",
|
|
630
673
|
id: 'HeadlessPasskeyStep',
|
|
631
674
|
})
|
|
632
675
|
|
|
@@ -1167,7 +1210,7 @@ export async function requestProof(
|
|
|
1167
1210
|
db: Realm.Database,
|
|
1168
1211
|
options: requestProof.Options,
|
|
1169
1212
|
) {
|
|
1170
|
-
const { credential, rpId, userId } = options
|
|
1213
|
+
const { credential, origin, rpId, userId } = options
|
|
1171
1214
|
const challenge = Hex.fromBytes(crypto.getRandomValues(new Uint8Array(32)))
|
|
1172
1215
|
await db.insert(Schema.verification).values({
|
|
1173
1216
|
expiresAt: new Date(Date.now() + lifetime * 1000),
|
|
@@ -1177,7 +1220,7 @@ export async function requestProof(
|
|
|
1177
1220
|
prefix: proofPrefix,
|
|
1178
1221
|
userId,
|
|
1179
1222
|
}),
|
|
1180
|
-
value: JSON.stringify(schema.proof.parse({ ...credential, rpId })),
|
|
1223
|
+
value: JSON.stringify(schema.proof.parse({ ...credential, origin, rpId })),
|
|
1181
1224
|
})
|
|
1182
1225
|
return challenge
|
|
1183
1226
|
}
|
|
@@ -1191,6 +1234,11 @@ export declare namespace requestProof {
|
|
|
1191
1234
|
/** The uncompressed P256 public key that the attestation holds, hex. */
|
|
1192
1235
|
publicKey: Hex.Hex
|
|
1193
1236
|
}
|
|
1237
|
+
/**
|
|
1238
|
+
* The origin of the app's page that the ceremony runs at. Absent for Tempo ID's own page. Only
|
|
1239
|
+
* an assertion that reports it, from a request that page sends, spends the challenge.
|
|
1240
|
+
*/
|
|
1241
|
+
origin?: string | undefined
|
|
1194
1242
|
/** The relying party of the `default` realm, which the new passkey belongs to. */
|
|
1195
1243
|
rpId: string
|
|
1196
1244
|
/** The signed-in user's ID. */
|
|
@@ -1215,13 +1263,23 @@ export async function requestRegistration(
|
|
|
1215
1263
|
prefix: registrationPrefix,
|
|
1216
1264
|
userId: options.userId,
|
|
1217
1265
|
}),
|
|
1218
|
-
value: JSON.stringify(
|
|
1266
|
+
value: JSON.stringify(
|
|
1267
|
+
schema.registration.parse({
|
|
1268
|
+
origin: options.origin,
|
|
1269
|
+
rpId: options.rpId,
|
|
1270
|
+
}),
|
|
1271
|
+
),
|
|
1219
1272
|
})
|
|
1220
1273
|
return challenge
|
|
1221
1274
|
}
|
|
1222
1275
|
|
|
1223
1276
|
export declare namespace requestRegistration {
|
|
1224
1277
|
type Options = {
|
|
1278
|
+
/**
|
|
1279
|
+
* The origin of the app's page that the ceremony runs at. Absent for Tempo ID's own page. Only
|
|
1280
|
+
* a credential that reports it, from a request that page sends, spends the challenge.
|
|
1281
|
+
*/
|
|
1282
|
+
origin?: string | undefined
|
|
1225
1283
|
/** The relying party of the `default` realm, which the new passkey must belong to. */
|
|
1226
1284
|
rpId: string
|
|
1227
1285
|
/** The signed-in user's ID. */
|
|
@@ -1487,7 +1545,14 @@ export async function takeProof(
|
|
|
1487
1545
|
.returning({ value: Schema.verification.value })
|
|
1488
1546
|
if (!row) return undefined
|
|
1489
1547
|
|
|
1490
|
-
|
|
1548
|
+
const value = schema.proof.parse(JSON.parse(row.value))
|
|
1549
|
+
return {
|
|
1550
|
+
challenge,
|
|
1551
|
+
id: value.id,
|
|
1552
|
+
origin: value.origin,
|
|
1553
|
+
publicKey: value.publicKey,
|
|
1554
|
+
rpId: value.rpId,
|
|
1555
|
+
}
|
|
1491
1556
|
}
|
|
1492
1557
|
|
|
1493
1558
|
export declare namespace takeProof {
|
|
@@ -1540,7 +1605,7 @@ export async function takeRegistration(
|
|
|
1540
1605
|
if (!row) return undefined
|
|
1541
1606
|
|
|
1542
1607
|
const value = schema.registration.parse(JSON.parse(row.value))
|
|
1543
|
-
return { challenge, rpId: value.rpId }
|
|
1608
|
+
return { challenge, origin: value.origin, rpId: value.rpId }
|
|
1544
1609
|
}
|
|
1545
1610
|
|
|
1546
1611
|
export declare namespace takeRegistration {
|
|
@@ -1896,7 +1961,7 @@ export declare namespace verifyRegistration {
|
|
|
1896
1961
|
type Options = {
|
|
1897
1962
|
/** The registration challenge that Tempo ID issued, as hex. */
|
|
1898
1963
|
challenge: Hex.Hex
|
|
1899
|
-
/** The origin the ceremony must report: Tempo ID's own. */
|
|
1964
|
+
/** The origin the ceremony must report: Tempo ID's own, or the app's page that the challenge names. */
|
|
1900
1965
|
origin: string
|
|
1901
1966
|
/** The relying party ID the challenge was issued for. */
|
|
1902
1967
|
rpId: string
|
package/src/internal/Privy.ts
CHANGED
|
@@ -10,8 +10,10 @@ import { createViemAccount } from '@privy-io/node/viem'
|
|
|
10
10
|
import { createMiddleware } from 'hono/factory'
|
|
11
11
|
import { Base64 } from 'ox'
|
|
12
12
|
import { Account } from 'viem/tempo'
|
|
13
|
+
import { z } from 'zod'
|
|
13
14
|
import type { Env } from './Env.js'
|
|
14
15
|
import * as ErrorResponse from './ErrorResponse.js'
|
|
16
|
+
import * as Log from './Log.js'
|
|
15
17
|
import * as Timing from './Timing.js'
|
|
16
18
|
|
|
17
19
|
/** No `PRIVY_JWT_PRIVATE_KEY` is set, so Tempo ID cannot sign as a Privy wallet. */
|
|
@@ -28,6 +30,20 @@ export class SigningKeyMissingError extends Error {
|
|
|
28
30
|
/** How long a token for Privy lasts, in seconds: long enough for one signing request. */
|
|
29
31
|
const tokenLifetime = 60
|
|
30
32
|
|
|
33
|
+
/** The name of the Cache API cache that holds users' Privy wallets, as {@link cachedWallets} reads them. */
|
|
34
|
+
const walletCache = 'privy-wallets'
|
|
35
|
+
|
|
36
|
+
/** Zod schemas for the wallets that {@link cachedWallets} stores. */
|
|
37
|
+
namespace schema {
|
|
38
|
+
export const wallets = z.array(
|
|
39
|
+
z.object({
|
|
40
|
+
address: z.templateLiteral(['0x', z.string().regex(/^[0-9a-fA-F]{40}$/)]),
|
|
41
|
+
createdAt: z.coerce.date(),
|
|
42
|
+
id: z.string().min(1),
|
|
43
|
+
}),
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
31
47
|
/** A realm's Privy app, which holds the owner wallets of the realm's Privy-owned accounts. */
|
|
32
48
|
export type App = {
|
|
33
49
|
/** The Privy app ID. */
|
|
@@ -68,6 +84,101 @@ export type Wallet = {
|
|
|
68
84
|
id: string
|
|
69
85
|
}
|
|
70
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Returns the user's Privy wallets in `app`, as {@link listWallets} does, from the data center's
|
|
89
|
+
* cache when a read in the last `maxAge` seconds stored them. A read from Privy that finds a wallet
|
|
90
|
+
* stores its result, so a later wallet that Privy creates elsewhere appears within `maxAge`. A user
|
|
91
|
+
* without a wallet is never stored, so a first wallet appears in the next read. The cache holds each wallet's address, ID, and creation time, never the
|
|
92
|
+
* app secret. Without the Cache API, as in the CLI, every call reads Privy.
|
|
93
|
+
*/
|
|
94
|
+
export async function cachedWallets(
|
|
95
|
+
c: Context,
|
|
96
|
+
options: cachedWallets.Options,
|
|
97
|
+
): Promise<readonly Wallet[]> {
|
|
98
|
+
if (typeof caches === 'undefined') return await listWallets(c, options)
|
|
99
|
+
const key = cacheKey(options)
|
|
100
|
+
const { warn = Log.warn } = options
|
|
101
|
+
|
|
102
|
+
// The cache only saves reads of Privy, so a failure of it falls back to Privy.
|
|
103
|
+
const cached = await (async () => {
|
|
104
|
+
try {
|
|
105
|
+
const cache = await Timing.measure(
|
|
106
|
+
c,
|
|
107
|
+
caches.open(walletCache),
|
|
108
|
+
'cache.privyWallets.open',
|
|
109
|
+
)
|
|
110
|
+
const hit = await Timing.measure(
|
|
111
|
+
c,
|
|
112
|
+
cache.match(key),
|
|
113
|
+
'cache.privyWallets.match',
|
|
114
|
+
)
|
|
115
|
+
if (!hit) return { cache, wallets: undefined }
|
|
116
|
+
const parsed = schema.wallets.safeParse(
|
|
117
|
+
await Timing.measure(c, hit.json(), 'cache.privyWallets.read'),
|
|
118
|
+
)
|
|
119
|
+
return { cache, wallets: parsed.success ? parsed.data : undefined }
|
|
120
|
+
} catch (error) {
|
|
121
|
+
warn({
|
|
122
|
+
error,
|
|
123
|
+
message:
|
|
124
|
+
'A read of the cached Privy wallets failed, so Privy was read.',
|
|
125
|
+
})
|
|
126
|
+
return undefined
|
|
127
|
+
}
|
|
128
|
+
})()
|
|
129
|
+
if (cached?.wallets) return cached.wallets
|
|
130
|
+
|
|
131
|
+
const wallets = await listWallets(c, options)
|
|
132
|
+
// A user without a wallet stays uncached, so the read after Tempo ID creates their first wallet,
|
|
133
|
+
// the only wallet that Tempo ID creates, asks Privy again. A read in flight during the creation
|
|
134
|
+
// thus stores no stale empty list.
|
|
135
|
+
if (wallets.length === 0 || !cached) return wallets
|
|
136
|
+
const stored = Timing.measure(
|
|
137
|
+
c,
|
|
138
|
+
cached.cache
|
|
139
|
+
.put(
|
|
140
|
+
key,
|
|
141
|
+
new Response(JSON.stringify(wallets), {
|
|
142
|
+
headers: {
|
|
143
|
+
'cache-control': `max-age=${options.maxAge}`,
|
|
144
|
+
'content-type': 'application/json',
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
)
|
|
148
|
+
.catch((error: unknown) => {
|
|
149
|
+
warn({
|
|
150
|
+
error,
|
|
151
|
+
message: 'A store of the Privy wallets in the cache failed.',
|
|
152
|
+
})
|
|
153
|
+
}),
|
|
154
|
+
'cache.privyWallets.put',
|
|
155
|
+
)
|
|
156
|
+
// The store only serves later reads, so with `waitUntil` the wallets return before it settles.
|
|
157
|
+
if (options.waitUntil) options.waitUntil(stored)
|
|
158
|
+
else await stored
|
|
159
|
+
return wallets
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export namespace cachedWallets {
|
|
163
|
+
export type Options = listWallets.Options & {
|
|
164
|
+
/** How old, in seconds, a cached read may be. */
|
|
165
|
+
maxAge: number
|
|
166
|
+
/**
|
|
167
|
+
* Keeps the store in the cache alive after the response, such as `c.executionCtx.waitUntil`,
|
|
168
|
+
* so the wallets return before it settles. Absent, the call waits for the store.
|
|
169
|
+
*/
|
|
170
|
+
waitUntil?: ((promise: Promise<unknown>) => void) | undefined
|
|
171
|
+
/**
|
|
172
|
+
* Logs a failure of the cache with the request's details, such as `Log.warn` with the request's
|
|
173
|
+
* context. Defaults to `Log.warn` without them.
|
|
174
|
+
*/
|
|
175
|
+
warn?: ((options: Log.Options) => void) | undefined
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The refusals `cachedWallets` throws, which `ErrorResponse.handle` responds with: Privy's failures. */
|
|
179
|
+
export const errors = listWallets.errors
|
|
180
|
+
}
|
|
181
|
+
|
|
71
182
|
/**
|
|
72
183
|
* Records that the request created the user with `userId`, so the request reads no Privy user for
|
|
73
184
|
* them from Privy. Privy keys each user by the user's ID, and no Privy app holds a new ID before
|
|
@@ -463,6 +574,23 @@ function inProgress(error: unknown) {
|
|
|
463
574
|
)
|
|
464
575
|
}
|
|
465
576
|
|
|
577
|
+
/**
|
|
578
|
+
* Returns the Cache API key of the wallets of the user with `userId` in `app`. The key names the app
|
|
579
|
+
* by its ID alone, never its secret, on a host that serves nothing.
|
|
580
|
+
*/
|
|
581
|
+
function cacheKey(options: cacheKey.Options) {
|
|
582
|
+
return `https://privy-wallets.tempo-id.invalid/${encodeURIComponent(options.app.appId)}/${encodeURIComponent(options.userId)}`
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
declare namespace cacheKey {
|
|
586
|
+
type Options = {
|
|
587
|
+
/** The Privy app that holds the user. */
|
|
588
|
+
app: App
|
|
589
|
+
/** The user's ID. */
|
|
590
|
+
userId: string
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
466
594
|
/**
|
|
467
595
|
* Returns the key of the user with `userId` in `app`, in the request's {@link Users}. Without `app`,
|
|
468
596
|
* the key marks a user that the request created.
|