From 67d8a951c8307207ac1dcad23f59ec33605a33ce Mon Sep 17 00:00:00 2001 From: Dhruv Pareek Date: Tue, 21 Apr 2026 21:42:45 -0700 Subject: [PATCH] feat: add PASSKEY branch to additional-credential challenge flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the PASSKEY branch to `AuthCredentialAdditionalChallengeOneOf`, letting platforms register a second (or third, etc.) passkey credential on an internal account that already has one. Completes the "add another credential" challenge/retry pattern for passkeys, matching the EMAIL_OTP and OAUTH flows already in the stack. **Flow** 1. `POST /auth/credentials` with `{ type: "PASSKEY", accountId, nickname, challenge, attestation }` on an account that already has a credential. 2. Response is 202 with `{ type: "PASSKEY", payloadToSign, requestId, expiresAt }`. 3. Client signs `payloadToSign` with the session private key of an existing verified credential on the same internal account and retries the request with `Grid-Wallet-Signature` + `Request-Id` headers. 4. Signed retry returns 201 with the created `AuthMethod`. **Schemas added** - `PasskeyCredentialAdditionalChallengeFields` — `{ type: "PASSKEY" }` (variant single-value enum on `type`; no per-type extra fields, same shape as the OAUTH variant). - `PasskeyCredentialAdditionalChallenge` — `allOf(AuthCredentialAdditionalChallenge, PasskeyCredentialAdditionalChallengeFields)`; wire shape is `{ type, payloadToSign, requestId, expiresAt }` (signing fields inherited from the base). **Wire-up** - `AuthCredentialAdditionalChallengeOneOf.yaml` discriminator map extended with `PASSKEY → PasskeyCredentialAdditionalChallenge`. - PASSKEY example added to the 202 response on `POST /auth/credentials`. - `.stainless/stainless.yml` registers the two new schemas under `auth.credentials`. **Notes** - Multiple passkey credentials per internal account are allowed (no `PASSKEY_CREDENTIAL_ALREADY_EXISTS`); this PR documents the concrete wire shape Grid returns when the client hits that branch. - Final PR in the PASSKEY sub-stack on top of the OAUTH stack; together with the two prior PASSKEY PRs it covers create, verify, and additional-credential registration. - Bundled `openapi.yaml` and `mintlify/openapi.yaml` regenerated via `make build`. --- .stainless/stainless.yml | 2 ++ mintlify/openapi.yaml | 24 +++++++++++++++++++ openapi.yaml | 24 +++++++++++++++++++ ...uthCredentialAdditionalChallengeOneOf.yaml | 2 ++ .../PasskeyCredentialAdditionalChallenge.yaml | 4 ++++ ...eyCredentialAdditionalChallengeFields.yaml | 11 +++++++++ openapi/paths/auth/auth_credentials.yaml | 7 ++++++ 7 files changed, 74 insertions(+) create mode 100644 openapi/components/schemas/auth/PasskeyCredentialAdditionalChallenge.yaml create mode 100644 openapi/components/schemas/auth/PasskeyCredentialAdditionalChallengeFields.yaml diff --git a/.stainless/stainless.yml b/.stainless/stainless.yml index d71dcf22..04c76c69 100644 --- a/.stainless/stainless.yml +++ b/.stainless/stainless.yml @@ -356,6 +356,8 @@ resources: passkey_credential_create_request_fields: '#/components/schemas/PasskeyCredentialCreateRequestFields' passkey_credential_verify_request: '#/components/schemas/PasskeyCredentialVerifyRequest' passkey_credential_verify_request_fields: '#/components/schemas/PasskeyCredentialVerifyRequestFields' + passkey_credential_additional_challenge: '#/components/schemas/PasskeyCredentialAdditionalChallenge' + passkey_credential_additional_challenge_fields: '#/components/schemas/PasskeyCredentialAdditionalChallengeFields' exchange_rates: methods: list: diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index e513165f..de9fab3f 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -3831,6 +3831,13 @@ paths: payloadToSign: Y2hhbGxlbmdlLXBheWxvYWQtdG8tc2lnbg== requestId: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 expiresAt: '2026-04-08T15:35:00Z' + passkey: + summary: Additional passkey credential challenge + value: + type: PASSKEY + payloadToSign: Y2hhbGxlbmdlLXBheWxvYWQtdG8tc2lnbg== + requestId: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + expiresAt: '2026-04-08T15:35:00Z' '400': description: Bad request. Returned with `EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS` when registering an `EMAIL_OTP` credential on an internal account that already has one — only one email OTP credential is supported per internal account at this time. content: @@ -13508,15 +13515,32 @@ components: allOf: - $ref: '#/components/schemas/AuthCredentialAdditionalChallenge' - $ref: '#/components/schemas/OauthCredentialAdditionalChallengeFields' + PasskeyCredentialAdditionalChallengeFields: + type: object + required: + - type + properties: + type: + type: string + enum: + - PASSKEY + description: Discriminator value identifying this as an additional-credential challenge for a passkey credential. + PasskeyCredentialAdditionalChallenge: + title: Passkey Credential Additional Challenge + allOf: + - $ref: '#/components/schemas/AuthCredentialAdditionalChallenge' + - $ref: '#/components/schemas/PasskeyCredentialAdditionalChallengeFields' AuthCredentialAdditionalChallengeOneOf: oneOf: - $ref: '#/components/schemas/EmailOtpCredentialAdditionalChallenge' - $ref: '#/components/schemas/OauthCredentialAdditionalChallenge' + - $ref: '#/components/schemas/PasskeyCredentialAdditionalChallenge' discriminator: propertyName: type mapping: EMAIL_OTP: '#/components/schemas/EmailOtpCredentialAdditionalChallenge' OAUTH: '#/components/schemas/OauthCredentialAdditionalChallenge' + PASSKEY: '#/components/schemas/PasskeyCredentialAdditionalChallenge' AuthCredentialVerifyRequest: type: object required: diff --git a/openapi.yaml b/openapi.yaml index e513165f..de9fab3f 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3831,6 +3831,13 @@ paths: payloadToSign: Y2hhbGxlbmdlLXBheWxvYWQtdG8tc2lnbg== requestId: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 expiresAt: '2026-04-08T15:35:00Z' + passkey: + summary: Additional passkey credential challenge + value: + type: PASSKEY + payloadToSign: Y2hhbGxlbmdlLXBheWxvYWQtdG8tc2lnbg== + requestId: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + expiresAt: '2026-04-08T15:35:00Z' '400': description: Bad request. Returned with `EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS` when registering an `EMAIL_OTP` credential on an internal account that already has one — only one email OTP credential is supported per internal account at this time. content: @@ -13508,15 +13515,32 @@ components: allOf: - $ref: '#/components/schemas/AuthCredentialAdditionalChallenge' - $ref: '#/components/schemas/OauthCredentialAdditionalChallengeFields' + PasskeyCredentialAdditionalChallengeFields: + type: object + required: + - type + properties: + type: + type: string + enum: + - PASSKEY + description: Discriminator value identifying this as an additional-credential challenge for a passkey credential. + PasskeyCredentialAdditionalChallenge: + title: Passkey Credential Additional Challenge + allOf: + - $ref: '#/components/schemas/AuthCredentialAdditionalChallenge' + - $ref: '#/components/schemas/PasskeyCredentialAdditionalChallengeFields' AuthCredentialAdditionalChallengeOneOf: oneOf: - $ref: '#/components/schemas/EmailOtpCredentialAdditionalChallenge' - $ref: '#/components/schemas/OauthCredentialAdditionalChallenge' + - $ref: '#/components/schemas/PasskeyCredentialAdditionalChallenge' discriminator: propertyName: type mapping: EMAIL_OTP: '#/components/schemas/EmailOtpCredentialAdditionalChallenge' OAUTH: '#/components/schemas/OauthCredentialAdditionalChallenge' + PASSKEY: '#/components/schemas/PasskeyCredentialAdditionalChallenge' AuthCredentialVerifyRequest: type: object required: diff --git a/openapi/components/schemas/auth/AuthCredentialAdditionalChallengeOneOf.yaml b/openapi/components/schemas/auth/AuthCredentialAdditionalChallengeOneOf.yaml index 9448c792..0adcabcd 100644 --- a/openapi/components/schemas/auth/AuthCredentialAdditionalChallengeOneOf.yaml +++ b/openapi/components/schemas/auth/AuthCredentialAdditionalChallengeOneOf.yaml @@ -1,8 +1,10 @@ oneOf: - $ref: ./EmailOtpCredentialAdditionalChallenge.yaml - $ref: ./OauthCredentialAdditionalChallenge.yaml + - $ref: ./PasskeyCredentialAdditionalChallenge.yaml discriminator: propertyName: type mapping: EMAIL_OTP: ./EmailOtpCredentialAdditionalChallenge.yaml OAUTH: ./OauthCredentialAdditionalChallenge.yaml + PASSKEY: ./PasskeyCredentialAdditionalChallenge.yaml diff --git a/openapi/components/schemas/auth/PasskeyCredentialAdditionalChallenge.yaml b/openapi/components/schemas/auth/PasskeyCredentialAdditionalChallenge.yaml new file mode 100644 index 00000000..049cfaab --- /dev/null +++ b/openapi/components/schemas/auth/PasskeyCredentialAdditionalChallenge.yaml @@ -0,0 +1,4 @@ +title: Passkey Credential Additional Challenge +allOf: + - $ref: ./AuthCredentialAdditionalChallenge.yaml + - $ref: ./PasskeyCredentialAdditionalChallengeFields.yaml diff --git a/openapi/components/schemas/auth/PasskeyCredentialAdditionalChallengeFields.yaml b/openapi/components/schemas/auth/PasskeyCredentialAdditionalChallengeFields.yaml new file mode 100644 index 00000000..5a9f147d --- /dev/null +++ b/openapi/components/schemas/auth/PasskeyCredentialAdditionalChallengeFields.yaml @@ -0,0 +1,11 @@ +type: object +required: + - type +properties: + type: + type: string + enum: + - PASSKEY + description: >- + Discriminator value identifying this as an additional-credential + challenge for a passkey credential. diff --git a/openapi/paths/auth/auth_credentials.yaml b/openapi/paths/auth/auth_credentials.yaml index 0371d3fe..5a65c86f 100644 --- a/openapi/paths/auth/auth_credentials.yaml +++ b/openapi/paths/auth/auth_credentials.yaml @@ -140,6 +140,13 @@ post: payloadToSign: Y2hhbGxlbmdlLXBheWxvYWQtdG8tc2lnbg== requestId: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 expiresAt: '2026-04-08T15:35:00Z' + passkey: + summary: Additional passkey credential challenge + value: + type: PASSKEY + payloadToSign: Y2hhbGxlbmdlLXBheWxvYWQtdG8tc2lnbg== + requestId: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 + expiresAt: '2026-04-08T15:35:00Z' '400': description: >- Bad request. Returned with `EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS` when