diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 378972751..bd05bebe7 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -5842,6 +5842,8 @@ paths: 2. Use the session API keypair of a verified authentication credential on the same internal account to build an API-key stamp over `payloadToSign`, then retry with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The retry body must carry the **same** `clientPublicKey` submitted in step 1 — Grid rejects the retry with `401` if it disagrees with what was bound into `payloadToSign`. The signed retry returns `200` with `encryptedWalletCredentials`, which the client decrypts with the matching private key. + The export may not settle within that request: an approval- or consensus-gated wallet-provider activity answers `200` with `status: "PROCESSING"` instead. The credentials are never stored server-side, so collecting them is the client's job — re-send the byte-identical signed retry (same headers, same body) until it returns `encryptedWalletCredentials`. The `Request-Id` challenge stays usable until an attempt actually delivers them, so a `PROCESSING` response never burns it; a delivered export does, and a later re-send is then rejected with `401`. Subscribe to `wallet_operation.completed` to learn when re-sending will succeed rather than polling blind. + The `clientPublicKey` is ephemeral: generate a fresh P-256 keypair for this export and discard the private key after decrypting. Do not reuse the keypair from any prior verify call — that private key was already discarded after decrypting the session signing key it was issued against. operationId: exportInternalAccount tags: @@ -5882,11 +5884,11 @@ paths: clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 responses: '200': - description: Signed retry accepted. Returns the encrypted wallet credentials. + description: 'Signed retry accepted. Returns the encrypted wallet credentials (`InternalAccountExportResponse`); or, when the underlying wallet-provider activity is still in flight, a `WalletOperationProcessing` body with `status: "PROCESSING"` — the credentials are never stored server-side, so the client re-sends the byte-identical signed retry to collect them once the activity settles. The challenge stays usable until an attempt actually returns credentials.' content: application/json: schema: - $ref: '#/components/schemas/InternalAccountExportResponse' + $ref: '#/components/schemas/InternalAccountExportResult' '202': description: Challenge issued. The response contains `payloadToSign` (which binds the submitted `clientPublicKey`) plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair and echo `requestId` on the retry. content: @@ -23329,6 +23331,32 @@ components: The value is a JSON string of the form `{"version": "v1.0.0", "data": "", "dataSignature": "", "enclaveQuorumPublic": ""}`. `data` hex-decodes to JSON `{"encappedPublic": "", "ciphertext": "", "organizationId": ""}`, where `encappedPublic` is the uncompressed SEC1 ephemeral public key. `dataSignature` is an ECDSA-P256-SHA256 signature over the `data` bytes produced by the issuer key in `enclaveQuorumPublic`; verify before decrypting. In sandbox, `dataSignature` and `enclaveQuorumPublic` are empty strings. Clients should bypass attestation verification when calling against sandbox. example: '{"version":"v1.0.0","data":"7b22656e6361707065645075626c6963223a22303433...","dataSignature":"3045022100c9...","enclaveQuorumPublic":"04a1b2c3..."}' + WalletOperationProcessing: + title: Wallet Operation Processing + description: '`200` response returned by an Embedded Wallet operation that the wallet provider has accepted but not yet settled — a consensus- or approval-gated activity that is still in flight. It is not an error and needs no client action beyond patience: the backend reconciles the operation to its terminal state on its own. The client MAY re-send the byte-identical request to converge sooner; the request is idempotent and returns the settled success response once the operation completes.' + type: object + required: + - status + properties: + status: + type: string + enum: + - PROCESSING + description: Always `PROCESSING`. Marks a still-in-flight operation whose terminal result is not yet available. + example: PROCESSING + message: + type: string + description: Human-readable explanation that the operation is still being processed and the same request may be retried. + example: This login is still being processed. Retry the same request in a moment. + example: + status: PROCESSING + message: This login is still being processed. Retry the same request in a moment. + InternalAccountExportResult: + title: Internal Account Export Result + description: 'Response body for the signed retry of `POST /internal-accounts/{internalAccountId}/export`. Normally an `InternalAccountExportResponse` — the encrypted wallet credentials. When the underlying wallet-provider activity is still in flight, this is instead a `WalletOperationProcessing` body with `status: "PROCESSING"`. The credentials are never stored server-side, so re-sending the byte-identical signed retry is how they are collected: the re-send returns them as soon as the activity has settled, and the challenge is consumed only by the attempt that delivers them.' + anyOf: + - $ref: '#/components/schemas/InternalAccountExportResponse' + - $ref: '#/components/schemas/WalletOperationProcessing' AuthMethodType: type: string enum: @@ -23527,26 +23555,6 @@ components: SMS_OTP: '#/components/schemas/SmsOtpCredentialCreateRequest' OAUTH: '#/components/schemas/OauthCredentialCreateRequest' PASSKEY: '#/components/schemas/PasskeyCredentialCreateRequest' - WalletOperationProcessing: - title: Wallet Operation Processing - description: '`200` response returned by an Embedded Wallet operation that the wallet provider has accepted but not yet settled — a consensus- or approval-gated activity that is still in flight. It is not an error and needs no client action beyond patience: the backend reconciles the operation to its terminal state on its own. The client MAY re-send the byte-identical request to converge sooner; the request is idempotent and returns the settled success response once the operation completes.' - type: object - required: - - status - properties: - status: - type: string - enum: - - PROCESSING - description: Always `PROCESSING`. Marks a still-in-flight operation whose terminal result is not yet available. - example: PROCESSING - message: - type: string - description: Human-readable explanation that the operation is still being processed and the same request may be retried. - example: This login is still being processed. Retry the same request in a moment. - example: - status: PROCESSING - message: This login is still being processed. Retry the same request in a moment. AuthMethodResponse: title: Auth Method Response description: |- diff --git a/mintlify/snippets/global-accounts/authentication.mdx b/mintlify/snippets/global-accounts/authentication.mdx index f6813890e..c7d6bc54b 100644 --- a/mintlify/snippets/global-accounts/authentication.mdx +++ b/mintlify/snippets/global-accounts/authentication.mdx @@ -702,15 +702,16 @@ Every signed-retry endpoint can return it on the retry step: | `POST /auth/sessions/{id}/refresh` | Refresh a session | | `DELETE /auth/credentials/{id}` | Revoke a credential | | `DELETE /auth/sessions/{id}` | Revoke a session | +| `POST /internal-accounts/{id}/export` | Export wallet credentials | To handle it: 1. **Re-send the exact same signed request** — identical body, `Grid-Wallet-Signature`, and `Request-Id`. Don't regenerate the stamp or start a fresh request; Grid correlates the retry with the same underlying operation and returns its real outcome once the provider settles, rather than starting a second one. 2. **Show a pending state in your UI** while you retry — "removing…" for a credential revoke, "signing in…" for a login — instead of treating `PROCESSING` as an error or leaving the caller blocked with no feedback. -3. **Keep retrying with backoff** until you get a terminal response — the success code above, or an error. Grid also reconciles the operation to its terminal state on its own, so even a client that stops retrying and checks back later (for example via `GET /auth/credentials` or `GET /auth/sessions`) will see the settled result. +3. **Keep retrying with backoff** until you get a terminal response — the success code above, or an error. Grid also reconciles the operation to its terminal state on its own, so even a client that stops retrying and checks back later (for example via `GET /auth/credentials` or `GET /auth/sessions`) will see the settled result. **Wallet export is the exception**: Grid never stores the encrypted credentials, so re-sending the signed retry is the only way to receive them. Grid's own reconciliation settles the operation but has nothing to hand back, and the export challenge expires like any other (typically 5 minutes) — if it lapses, start a fresh export. - `verify` and `challenge` already use `200` for their terminal success body (`AuthSession` or the challenge response). For those two, `200` alone doesn't tell you which case you're in — check the response body's `status` field for `"PROCESSING"` to distinguish it from a settled success. The other four endpoints (`add credential`, `session refresh`, `revoke credential`, `revoke session`) use `201` or `204` on success, so a `200` by itself already means still-processing. + `verify`, `challenge`, and `export` already use `200` for their terminal success body (`AuthSession`, the challenge response, or the encrypted credentials). For those three, `200` alone doesn't tell you which case you're in — check the response body's `status` field for `"PROCESSING"` to distinguish it from a settled success. The other four endpoints (`add credential`, `session refresh`, `revoke credential`, `revoke session`) use `201` or `204` on success, so a `200` by itself already means still-processing. ### Add an additional credential diff --git a/mintlify/snippets/global-accounts/exporting-wallet.mdx b/mintlify/snippets/global-accounts/exporting-wallet.mdx index e96eff327..f4d9b5841 100644 --- a/mintlify/snippets/global-accounts/exporting-wallet.mdx +++ b/mintlify/snippets/global-accounts/exporting-wallet.mdx @@ -16,6 +16,10 @@ sequenceDiagram C->>C: stamp(payloadToSign, sessionPrivateKey) C->>IB: { stamp } IB->>G: POST /internal-accounts/{id}/export { same clientPublicKey }
Grid-Wallet-Signature
Request-Id + alt still settling at the wallet provider + G-->>IB: 200 { status: "PROCESSING" } + IB->>G: Re-send the identical signed request + end G-->>IB: 200 { id, encryptedWalletCredentials } IB-->>C: { encryptedWalletCredentials } C->>C: decrypt with client private key
→ mnemonic @@ -65,6 +69,8 @@ sequenceDiagram "encryptedWalletCredentials": "{\"version\":\"v1.0.0\",\"data\":\"7b22656e6361707065645075626c6963223a2230346634356632612e2e2e222c2263697068657274657874223a22316661313032333339302e2e2e222c226f7267616e697a6174696f6e4964223a226f72675f326d39462e2e2e227d\",\"dataSignature\":\"3045022100...\",\"enclaveQuorumPublic\":\"04a1b2c3...\"}" } ``` + + Export shares its terminal status code with the still-processing response: a `200` here is only the encrypted seed if the body isn't `{ "status": "PROCESSING" }`. See handling a still-processing response — if you get `PROCESSING`, re-send the identical signed retry (same `clientPublicKey`, `Grid-Wallet-Signature`, and `Request-Id`) until you get the credentials. Because Grid never stores them, the re-send is the only way to receive them; subscribe to `wallet_operation.completed` to know when it will succeed. The `Request-Id` challenge is consumed only by the attempt that actually returns credentials, so a `PROCESSING` response leaves it usable. `encryptedWalletCredentials` is a signed wallet export envelope, not the base58check session-bundle format used by `encryptedSessionSigningKey`. Parse the envelope, verify `dataSignature` against `enclaveQuorumPublic`, decode the hex `data` JSON to get `encappedPublic` and `ciphertext`, then decrypt with the export private key that matches the `clientPublicKey` you sent on both export requests. diff --git a/openapi.yaml b/openapi.yaml index 378972751..bd05bebe7 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -5842,6 +5842,8 @@ paths: 2. Use the session API keypair of a verified authentication credential on the same internal account to build an API-key stamp over `payloadToSign`, then retry with that full stamp as the `Grid-Wallet-Signature` header and the `requestId` echoed back as the `Request-Id` header. The retry body must carry the **same** `clientPublicKey` submitted in step 1 — Grid rejects the retry with `401` if it disagrees with what was bound into `payloadToSign`. The signed retry returns `200` with `encryptedWalletCredentials`, which the client decrypts with the matching private key. + The export may not settle within that request: an approval- or consensus-gated wallet-provider activity answers `200` with `status: "PROCESSING"` instead. The credentials are never stored server-side, so collecting them is the client's job — re-send the byte-identical signed retry (same headers, same body) until it returns `encryptedWalletCredentials`. The `Request-Id` challenge stays usable until an attempt actually delivers them, so a `PROCESSING` response never burns it; a delivered export does, and a later re-send is then rejected with `401`. Subscribe to `wallet_operation.completed` to learn when re-sending will succeed rather than polling blind. + The `clientPublicKey` is ephemeral: generate a fresh P-256 keypair for this export and discard the private key after decrypting. Do not reuse the keypair from any prior verify call — that private key was already discarded after decrypting the session signing key it was issued against. operationId: exportInternalAccount tags: @@ -5882,11 +5884,11 @@ paths: clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 responses: '200': - description: Signed retry accepted. Returns the encrypted wallet credentials. + description: 'Signed retry accepted. Returns the encrypted wallet credentials (`InternalAccountExportResponse`); or, when the underlying wallet-provider activity is still in flight, a `WalletOperationProcessing` body with `status: "PROCESSING"` — the credentials are never stored server-side, so the client re-sends the byte-identical signed retry to collect them once the activity settles. The challenge stays usable until an attempt actually returns credentials.' content: application/json: schema: - $ref: '#/components/schemas/InternalAccountExportResponse' + $ref: '#/components/schemas/InternalAccountExportResult' '202': description: Challenge issued. The response contains `payloadToSign` (which binds the submitted `clientPublicKey`) plus a `requestId`. Build an API-key stamp over `payloadToSign` with the session API keypair and echo `requestId` on the retry. content: @@ -23329,6 +23331,32 @@ components: The value is a JSON string of the form `{"version": "v1.0.0", "data": "", "dataSignature": "", "enclaveQuorumPublic": ""}`. `data` hex-decodes to JSON `{"encappedPublic": "", "ciphertext": "", "organizationId": ""}`, where `encappedPublic` is the uncompressed SEC1 ephemeral public key. `dataSignature` is an ECDSA-P256-SHA256 signature over the `data` bytes produced by the issuer key in `enclaveQuorumPublic`; verify before decrypting. In sandbox, `dataSignature` and `enclaveQuorumPublic` are empty strings. Clients should bypass attestation verification when calling against sandbox. example: '{"version":"v1.0.0","data":"7b22656e6361707065645075626c6963223a22303433...","dataSignature":"3045022100c9...","enclaveQuorumPublic":"04a1b2c3..."}' + WalletOperationProcessing: + title: Wallet Operation Processing + description: '`200` response returned by an Embedded Wallet operation that the wallet provider has accepted but not yet settled — a consensus- or approval-gated activity that is still in flight. It is not an error and needs no client action beyond patience: the backend reconciles the operation to its terminal state on its own. The client MAY re-send the byte-identical request to converge sooner; the request is idempotent and returns the settled success response once the operation completes.' + type: object + required: + - status + properties: + status: + type: string + enum: + - PROCESSING + description: Always `PROCESSING`. Marks a still-in-flight operation whose terminal result is not yet available. + example: PROCESSING + message: + type: string + description: Human-readable explanation that the operation is still being processed and the same request may be retried. + example: This login is still being processed. Retry the same request in a moment. + example: + status: PROCESSING + message: This login is still being processed. Retry the same request in a moment. + InternalAccountExportResult: + title: Internal Account Export Result + description: 'Response body for the signed retry of `POST /internal-accounts/{internalAccountId}/export`. Normally an `InternalAccountExportResponse` — the encrypted wallet credentials. When the underlying wallet-provider activity is still in flight, this is instead a `WalletOperationProcessing` body with `status: "PROCESSING"`. The credentials are never stored server-side, so re-sending the byte-identical signed retry is how they are collected: the re-send returns them as soon as the activity has settled, and the challenge is consumed only by the attempt that delivers them.' + anyOf: + - $ref: '#/components/schemas/InternalAccountExportResponse' + - $ref: '#/components/schemas/WalletOperationProcessing' AuthMethodType: type: string enum: @@ -23527,26 +23555,6 @@ components: SMS_OTP: '#/components/schemas/SmsOtpCredentialCreateRequest' OAUTH: '#/components/schemas/OauthCredentialCreateRequest' PASSKEY: '#/components/schemas/PasskeyCredentialCreateRequest' - WalletOperationProcessing: - title: Wallet Operation Processing - description: '`200` response returned by an Embedded Wallet operation that the wallet provider has accepted but not yet settled — a consensus- or approval-gated activity that is still in flight. It is not an error and needs no client action beyond patience: the backend reconciles the operation to its terminal state on its own. The client MAY re-send the byte-identical request to converge sooner; the request is idempotent and returns the settled success response once the operation completes.' - type: object - required: - - status - properties: - status: - type: string - enum: - - PROCESSING - description: Always `PROCESSING`. Marks a still-in-flight operation whose terminal result is not yet available. - example: PROCESSING - message: - type: string - description: Human-readable explanation that the operation is still being processed and the same request may be retried. - example: This login is still being processed. Retry the same request in a moment. - example: - status: PROCESSING - message: This login is still being processed. Retry the same request in a moment. AuthMethodResponse: title: Auth Method Response description: |- diff --git a/openapi/components/schemas/internal_accounts/InternalAccountExportResult.yaml b/openapi/components/schemas/internal_accounts/InternalAccountExportResult.yaml new file mode 100644 index 000000000..725b22d13 --- /dev/null +++ b/openapi/components/schemas/internal_accounts/InternalAccountExportResult.yaml @@ -0,0 +1,14 @@ +title: Internal Account Export Result +description: >- + Response body for the signed retry of `POST + /internal-accounts/{internalAccountId}/export`. Normally an + `InternalAccountExportResponse` — the encrypted wallet credentials. When + the underlying wallet-provider activity is still in flight, this is + instead a `WalletOperationProcessing` body with `status: "PROCESSING"`. + The credentials are never stored server-side, so re-sending the + byte-identical signed retry is how they are collected: the re-send + returns them as soon as the activity has settled, and the challenge is + consumed only by the attempt that delivers them. +anyOf: + - $ref: ./InternalAccountExportResponse.yaml + - $ref: ../common/WalletOperationProcessing.yaml diff --git a/openapi/paths/internal_accounts/internal_accounts_{id}_export.yaml b/openapi/paths/internal_accounts/internal_accounts_{id}_export.yaml index a9769cbd0..478e5bec2 100644 --- a/openapi/paths/internal_accounts/internal_accounts_{id}_export.yaml +++ b/openapi/paths/internal_accounts/internal_accounts_{id}_export.yaml @@ -30,6 +30,19 @@ post: with the matching private key. + The export may not settle within that request: an approval- or + consensus-gated wallet-provider activity answers `200` with + `status: "PROCESSING"` instead. The credentials are never stored + server-side, so collecting them is the client's job — re-send the + byte-identical signed retry (same headers, same body) until it + returns `encryptedWalletCredentials`. The `Request-Id` challenge + stays usable until an attempt actually delivers them, so a + `PROCESSING` response never burns it; a delivered export does, and a + later re-send is then rejected with `401`. Subscribe to + `wallet_operation.completed` to learn when re-sending will succeed + rather than polling blind. + + The `clientPublicKey` is ephemeral: generate a fresh P-256 keypair for this export and discard the private key after decrypting. Do not reuse the keypair from any prior verify call — that private @@ -82,11 +95,19 @@ post: clientPublicKey: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 responses: '200': - description: Signed retry accepted. Returns the encrypted wallet credentials. + description: >- + Signed retry accepted. Returns the encrypted wallet credentials + (`InternalAccountExportResponse`); or, when the underlying + wallet-provider activity is still in flight, a + `WalletOperationProcessing` body with `status: "PROCESSING"` — the + credentials are never stored server-side, so the client re-sends + the byte-identical signed retry to collect them once the activity + settles. The challenge stays usable until an attempt actually + returns credentials. content: application/json: schema: - $ref: ../../components/schemas/internal_accounts/InternalAccountExportResponse.yaml + $ref: ../../components/schemas/internal_accounts/InternalAccountExportResult.yaml '202': description: >- Challenge issued. The response contains `payloadToSign` (which