@@ -47,6 +47,7 @@ import {
4747 getCard ,
4848 getNonce ,
4949 getPIN ,
50+ getProcessorDetails ,
5051 getSecrets ,
5152 getUser ,
5253 setPIN ,
@@ -92,6 +93,12 @@ const CardResponse = object({
9293 } ) ,
9394 productId : pipe ( string ( ) , metadata ( { examples : [ "402" ] } ) ) ,
9495 challenge : optional ( pipe ( string ( ) , metadata ( { examples : [ "1a2b3c" ] } ) ) ) ,
96+ provisioning : optional (
97+ object ( {
98+ id : pipe ( string ( ) , metadata ( { examples : [ "card_abc123" ] } ) ) ,
99+ secret : pipe ( string ( ) , metadata ( { examples : [ "otp_xyz" ] } ) ) ,
100+ } ) ,
101+ ) ,
95102} ) ;
96103
97104const CreatedCardResponse = object ( {
@@ -145,7 +152,7 @@ const UpdatedCardResponse = union([
145152 object ( { verification : literal ( "OK" ) } ) ,
146153] ) ;
147154
148- const Scopes = picklist ( [ "siwe" , "webauthn" ] ) ;
155+ const Scopes = picklist ( [ "provisioning" , " siwe", "webauthn" ] ) ;
149156
150157export default new Hono ( )
151158 . get (
@@ -183,6 +190,8 @@ The \`sessionid\` header and the \`scope\` query parameter are independent and m
183190- Provide \`sessionid\` to receive \`encryptedPan\`, \`encryptedCvc\`, and \`pin\`. Without it, only the card profile is returned.
184191- Provide \`scope=siwe\` or \`scope=webauthn\` to receive a \`challenge\` to be signed and submitted via \`PATCH /\`. \`siwe\` and \`webauthn\` are mutually exclusive within a single request.
185192
193+ Successful responses include push-provisioning credentials in the \`provisioning\` field only when the \`scope=provisioning\` query parameter is sent.
194+
186195**Retrieving encrypted card details**
1871961. **Generate a session ID**: Encrypt a 32‑character hexadecimal secret (no spaces/dashes) with the provided public RSA key using RSA‑OAEP.
1881972. **Send the request**: Include the encrypted secret in the header \`sessionid\` when calling this endpoint.
@@ -273,9 +282,9 @@ function decrypt(base64Secret: string, base64Iv: string, secretKey: string): str
273282 } ,
274283 } ) ,
275284 async ( c ) => {
276- const { scope } = c . req . valid ( "query" ) ;
285+ const query = c . req . valid ( "query" ) ;
277286 function include ( type : InferInput < typeof Scopes > ) {
278- return Array . isArray ( scope ) ? scope . includes ( type ) : scope === type ;
287+ return Array . isArray ( query . scope ) ? query . scope . includes ( type ) : query . scope === type ;
279288 }
280289 const { credentialId } = c . req . valid ( "cookie" ) ;
281290 const credential = await database . query . credentials . findFirst ( {
@@ -293,78 +302,84 @@ function decrypt(base64Secret: string, base64Iv: string, secretKey: string): str
293302 setUser ( { id : account } ) ;
294303 if ( ! credential . pandaId ) return c . json ( { code : "no panda" } , 403 ) ;
295304 const sessionid = c . req . valid ( "header" ) . sessionid ;
296- if ( credential . cards . length > 0 && credential . cards [ 0 ] ) {
297- const { id, lastFour, status, mode, productId } = credential . cards [ 0 ] ;
298- if ( status === "DELETED" ) throw new Error ( "card deleted" ) ;
299- const [ { expirationMonth, expirationYear, limit } , pan , user , pin , challenge ] = await Promise . all ( [
300- getCard ( id ) ,
301- sessionid && getSecrets ( id , sessionid ) ,
302- getUser ( credential . pandaId ) . catch ( ( error : unknown ) => {
303- const issue = noUser ( error ) ;
304- if ( ! issue ) throw error ;
305- const shouldCapture = issue . error . status === 404 || status === "ACTIVE" ;
306- if ( shouldCapture ) {
307- withScope ( ( s ) => {
308- s . addEventProcessor ( ( event ) => {
309- if ( event . exception ?. values ?. [ 0 ] ) event . exception . values [ 0 ] . type = issue . type ;
310- return event ;
311- } ) ;
312- captureException ( issue . error , {
313- level : "warning" ,
314- fingerprint : [ "{{ default }}" , issue . type ] ,
315- extra : {
316- cardId : id ,
317- credentialId,
318- pandaId : credential . pandaId ,
319- status,
320- shouldCapture,
321- userIssue : issue . type ,
322- } ,
323- } ) ;
305+ if ( credential . cards . length === 0 || ! credential . cards [ 0 ] ) return c . json ( { code : "no card" } , 404 ) ;
306+ const { id, lastFour, status, mode, productId } = credential . cards [ 0 ] ;
307+ if ( status === "DELETED" ) throw new Error ( "card deleted" ) ;
308+ const [ { expirationMonth, expirationYear, limit } , pan , user , pin , challenge , provisioning ] = await Promise . all ( [
309+ getCard ( id ) ,
310+ sessionid && getSecrets ( id , sessionid ) ,
311+ getUser ( credential . pandaId ) . catch ( ( error : unknown ) => {
312+ const issue = noUser ( error ) ;
313+ if ( ! issue ) throw error ;
314+ const shouldCapture = issue . error . status === 404 || status === "ACTIVE" ;
315+ if ( shouldCapture ) {
316+ withScope ( ( scope ) => {
317+ scope . addEventProcessor ( ( event ) => {
318+ if ( event . exception ?. values ?. [ 0 ] ) event . exception . values [ 0 ] . type = issue . type ;
319+ return event ;
324320 } ) ;
325- }
326- return null ;
327- } ) ,
328- sessionid && getPIN ( id , sessionid ) ,
329- ( async ( ) => {
330- if ( include ( "siwe" ) ) {
331- if ( ! credential . pandaId ) return ;
332- return getNonce ( credential . pandaId ) . then ( ( { nonce } ) =>
333- createSiweMessage ( {
334- domain,
335- address : parse ( Address , credentialId ) ,
336- statement : `I authorize the account ${ account } to be linked with the card ending in ${ lastFour } for my user (${ credential . pandaId } )` ,
337- uri : `https://${ domain } ` ,
338- version : "1" ,
339- chainId : chain . id ,
340- nonce,
341- } ) ,
342- ) ;
343- } else if ( include ( "webauthn" ) ) {
344- return `I authorize the account ${ account } to be linked with the card ending in ${ lastFour } for my user (${ credential . pandaId } )` ;
345- }
346- } ) ( ) ,
347- ] ) ;
348- if ( ! user ) return c . json ( { code : "no panda" } , 403 ) ;
349-
350- return c . json (
351- {
352- ...( pan && { ...pan } ) ,
353- ...( pin && { ...pin } ) ,
354- displayName : `${ user . firstName } ${ user . lastName } ` ,
355- expirationMonth,
356- expirationYear,
357- lastFour,
358- mode,
359- provider : "panda" as const ,
360- status,
361- limit,
362- productId,
363- ...( challenge && { challenge } ) ,
364- } satisfies InferOutput < typeof CardResponse > ,
365- 200 ,
366- ) ;
367- } else return c . json ( { code : "no card" } , 404 ) ;
321+ captureException ( issue . error , {
322+ level : "warning" ,
323+ fingerprint : [ "{{ default }}" , issue . type ] ,
324+ extra : {
325+ cardId : id ,
326+ credentialId,
327+ pandaId : credential . pandaId ,
328+ status,
329+ shouldCapture,
330+ userIssue : issue . type ,
331+ } ,
332+ } ) ;
333+ } ) ;
334+ }
335+ return null ;
336+ } ) ,
337+ sessionid && getPIN ( id , sessionid ) ,
338+ ( async ( ) => {
339+ if ( include ( "siwe" ) ) {
340+ if ( ! credential . pandaId ) return ;
341+ return getNonce ( credential . pandaId ) . then ( ( { nonce } ) =>
342+ createSiweMessage ( {
343+ domain,
344+ address : parse ( Address , credentialId ) ,
345+ statement : `I authorize the account ${ account } to be linked with the card ending in ${ lastFour } for my user (${ credential . pandaId } )` ,
346+ uri : `https://${ domain } ` ,
347+ version : "1" ,
348+ chainId : chain . id ,
349+ nonce,
350+ } ) ,
351+ ) ;
352+ } else if ( include ( "webauthn" ) ) {
353+ return `I authorize the account ${ account } to be linked with the card ending in ${ lastFour } for my user (${ credential . pandaId } )` ;
354+ }
355+ } ) ( ) ,
356+ include ( "provisioning" )
357+ ? getProcessorDetails ( id ) . then ( ( { processorCardId, timeBasedSecret } ) => ( {
358+ id : processorCardId ,
359+ secret : timeBasedSecret ,
360+ } ) )
361+ : undefined ,
362+ ] ) ;
363+ if ( ! user ) return c . json ( { code : "no panda" } , 403 ) ;
364+ if ( include ( "provisioning" ) ) c . header ( "Cache-Control" , "no-store" ) ;
365+ return c . json (
366+ {
367+ ...pan ,
368+ ...pin ,
369+ displayName : `${ user . firstName } ${ user . lastName } ` ,
370+ expirationMonth,
371+ expirationYear,
372+ lastFour,
373+ mode,
374+ provider : "panda" as const ,
375+ status,
376+ limit,
377+ productId,
378+ ...( challenge && { challenge } ) ,
379+ ...( provisioning && { provisioning } ) ,
380+ } satisfies InferOutput < typeof CardResponse > ,
381+ 200 ,
382+ ) ;
368383 } ,
369384 )
370385 . post (
0 commit comments