@faable/auth-sdk 2.7.28 → 2.7.30

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/spec/openapi.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "info": {
4
4
  "title": "@faablecloud/auth",
5
5
  "description": "Auth Platform made by Faable. Manage Users and Roles",
6
- "version": "2.34.0",
6
+ "version": "2.35.1",
7
7
  "license": {
8
8
  "name": "private",
9
9
  "url": "https://faable.com/docs/platform/privacy-policy"
@@ -2267,11 +2267,17 @@
2267
2267
  "password_reset"
2268
2268
  ]
2269
2269
  },
2270
+ {
2271
+ "type": "string",
2272
+ "enum": [
2273
+ "import"
2274
+ ]
2275
+ },
2270
2276
  {
2271
2277
  "type": "null"
2272
2278
  }
2273
2279
  ],
2274
- "description": "How `email_verified` was last set. `manual` — admin flipped the flag via `POST /user/:id`. `verification_flow` — user clicked the verification link. `passwordless_otp` — user completed passwordless OTP login. `team_invite` — user clicked a team invitation link. `email_change` — user confirmed a self-service email change. `federated` — verified by the external IdP on OAuth callback. `password_reset` — user completed the recovery link that was emailed to them (the `email` channel only). `null` — the field was cleared (admin un-verified the email)."
2280
+ "description": "How `email_verified` was last set. `manual` — admin flipped the flag via `POST /user/:id`. `verification_flow` — user clicked the verification link. `passwordless_otp` — user completed passwordless OTP login. `team_invite` — user clicked a team invitation link. `email_change` — user confirmed a self-service email change. `federated` — verified by the external IdP on OAuth callback. `password_reset` — user completed the recovery link that was emailed to them (the `email` channel only). `import` — the user arrived verified from another provider through `POST /user/import` (asserted by whoever ran the migration). `null` — the field was cleared (admin un-verified the email)."
2275
2281
  },
2276
2282
  "email_verified_at": {
2277
2283
  "type": "string",
@@ -2293,6 +2299,11 @@
2293
2299
  "description": "ISO 8601 timestamp of the built-in welcome email send. Set once by the platform; guarantees at most one welcome per user.",
2294
2300
  "nullable": true
2295
2301
  },
2302
+ "imported_at": {
2303
+ "type": "string",
2304
+ "description": "ISO 8601 timestamp of when the user was created by `POST /user/import`. Imported users get none of the built-in new-user emails (verification, welcome, `user.created` notifications); `user.created` webhooks still fire and carry this field.",
2305
+ "nullable": true
2306
+ },
2296
2307
  "passkey_prompted_at": {
2297
2308
  "type": "string",
2298
2309
  "description": "ISO 8601 timestamp of the last time the hosted \"create a passkey\" offer was shown to this user after a login. Drives the snooze in `login_methods.passkey_promotion_snooze_days`.",
@@ -2370,6 +2381,12 @@
2370
2381
  "password_reset"
2371
2382
  ]
2372
2383
  },
2384
+ {
2385
+ "type": "string",
2386
+ "enum": [
2387
+ "import"
2388
+ ]
2389
+ },
2373
2390
  {
2374
2391
  "type": "null"
2375
2392
  }
@@ -2963,7 +2980,8 @@
2963
2980
  "username": {
2964
2981
  "type": "string"
2965
2982
  }
2966
- }
2983
+ },
2984
+ "additionalProperties": false
2967
2985
  },
2968
2986
  "CredentialsUpdate": {
2969
2987
  "type": "object",
@@ -6947,7 +6965,7 @@
6947
6965
  },
6948
6966
  "ErrorCode": {
6949
6967
  "type": "string",
6950
- "description": "Machine-readable error code. One HTTP status per code.\n\n- `bad_request` (400): The request is malformed. `message` says what is wrong.\n- `validation_error` (400): The body, query or path failed schema validation. `details.issues` lists each failing field.\n- `unauthorized` (401): The request carries no valid credentials.\n- `payment_required` (402): The feature is not included in the current plan.\n- `forbidden` (403): The credentials are valid but do not allow this operation.\n- `not_found` (404): The resource does not exist in this account.\n- `method_not_allowed` (405): The HTTP method is not supported on this path.\n- `conflict` (409): The request conflicts with the current state of the resource.\n- `payload_too_large` (413): The request body exceeds the size limit.\n- `unsupported_media_type` (415): The `Content-Type` is not accepted by this endpoint.\n- `unprocessable_entity` (422): The request is well-formed but cannot be processed.\n- `too_many_requests` (429): Rate limit exceeded. Honour the `Retry-After` header.\n- `internal_error` (500): Unexpected server error. Retry later; the request id is logged.\n- `already_exists` (400): A resource with the same unique field already exists (for a user, usually `email`). `details.fields` names the field.\n- `invalid_id` (400): The id in the path is not a valid id for this resource.\n- `account_not_found` (404): No Auth Account matches the request (domain, header or token).\n- `invalid_query` (400): The `?query=` FaableQL expression could not be parsed.\n- `search_not_supported` (400): This resource does not support `?search=`.\n- `invalid_expand` (400): An `?expand=` path is not allowed on this resource.\n- `access_denied` (401): The caller may not access this row (ownership or machine-to-machine restriction).\n- `insufficient_scope` (403): The token lacks a scope this operation requires. `message` names it.\n- `team_required` (400): An Auth Account must belong to a project (team).\n- `account_limit_reached` (409): The project already has the maximum number of Auth Accounts.\n- `invalid_project_id` (400): The project id is not of the form `project_<hex>` or `team_<hex>`.\n- `account_transfer_refused` (409): The Auth Account cannot be moved to that project. `details.blockers` lists why.\n- `plan_required` (402): The setting requires a higher plan. `message` says which.\n- `not_logged_in` (401): There is no signed-in session for this request.\n- `invalid_token` (401): The bearer token is missing, malformed, expired or has no subject.\n- `not_owner` (403): The caller may only perform this operation on their own user.\n- `user_suspended` (403): The user is suspended and cannot sign in or be modified.\n- `invalid_credentials` (400): The email/username or password is incorrect.\n- `login_denied` (401): A post-login Action denied the sign-in. `message` carries the reason it gave. On token grants it is a 403 with `error: invalid_grant`.\n- `signup_denied` (403): A pre-signup Action or an active block denied the sign-up. `message` carries the reason.\n- `client_credentials_denied` (401): An Action denied the client_credentials grant. `message` carries the reason.\n- `signup_disabled` (403): Self-service sign-up is disabled on this connection.\n- `email_taken` (409): Another user in this account already uses that email.\n- `invalid_email` (400): The email address is not valid.\n- `same_email` (400): The new email is the same as the current one.\n- `user_has_no_email` (400): The operation needs an email but the user has none.\n- `password_too_weak` (400): The password does not meet the connection policy. `message` lists each unmet rule.\n- `invalid_code` (400): The verification code is wrong, expired or already used.\n- `invalid_phone` (400): The phone number is not E.164 and could not be resolved with the account default country. Include the country code.\n- `phone_changed` (400): The phone on the user changed after the code was sent. Start again.\n- `phone_already_verified` (409): The user already has a verified phone; the number cannot be swapped from the screen.\n- `sms_unavailable` (409): This account cannot send SMS right now. The suffix says why: `sms_unavailable:plan`, `:quota`, `:provider`.\n- `sms_failed` (409): The SMS provider rejected the message.\n- `invalid_state` (400): The `state` is missing, expired, or does not describe a resumable step.\n- `state_mismatch` (401): The `state` belongs to another account, session, client or ceremony.\n- `invalid_ticket` (400): The ticket does not exist or is not the kind this endpoint accepts.\n- `ticket_used` (400): The ticket was already consumed.\n- `ticket_expired` (400): The ticket is past its expiry.\n- `ticket_not_found` (404): No ticket with that id belongs to this user.\n- `credential_not_found` (404): The user has no password credential on this connection.\n- `invalid_link` (400): The magic link is malformed.\n- `expired_link` (400): The magic link token was not found or has expired.\n- `invalid_otp` (401): The one-time password is wrong or expired.\n- `invalid_client` (400): The `client_id` in the request does not name a client of this account.\n- `client_not_found` (404): No client with that `client_id`.\n- `client_mismatch` (401): The client belongs to a different account.\n- `invalid_client_metadata` (400): Dynamic client registration rejected a field (RFC 7591). `message` names it.\n- `invalid_connection` (400): The connection does not exist, is disabled, or is not of the type this flow needs.\n- `connection_required` (400): No connection was given and the client has no `default_connection`.\n- `connection_misconfigured` (400): The connection is missing settings needed to talk to its provider.\n- `passwordless_unavailable` (400): No passwordless connection is configured or enabled for this client.\n- `ambiguous_connection` (400): Several connections match; pass `connection_id`.\n- `invalid_redirect_uri` (400): The redirect URI is not registered for the client.\n- `origin_not_allowed` (403): The request `Origin` is not in the client's Allowed Web Origins.\n- `audience_not_found` (403): The `audience` does not name an API registered in this account.\n- `system_resource` (403): The resource is managed by Faable and cannot be modified or deleted.\n- `invalid_request` (400): A required OAuth parameter is missing or two of them are incompatible. `message` says which.\n- `invalid_device_code` (400): The device or user code is invalid or expired.\n- `provider_error` (400): The upstream identity provider returned an error. The suffix is the provider’s own code, when it sent one: `provider_error:bad_refresh_token` (the user must re-authorize), `provider_error:incorrect_client_credentials` (our configuration, the user can do nothing). `details.provider_error_description` carries the provider’s sentence.\n- `identity_already_linked` (409): That external identity is already linked to another user.\n- `identity_link_denied` (409): That external identity cannot be linked to this user.\n- `identity_conflict` (409): The user already has a linked identity for this connection.\n- `identity_orphaned` (400): The identity pointed at a user that no longer exists; it was removed. Sign in again.\n- `no_provider_token` (400): The identity holds no provider access token.\n- `no_refresh_token` (400): The identity holds no provider refresh token, so it cannot be refreshed.\n- `action_unavailable` (401): The Action that paused this login is disabled or belongs elsewhere.\n- `login_flow_misconfigured` (500): The login flow of this account cannot run. `message` carries the node error.\n- `mfa_required` (403): The login needs a second factor that this flow cannot collect, or one is still pending on the session.\n- `step_up_required` (403): Confirm an existing factor before changing your factors.\n- `interaction_required` (403): An Action asked for a redirect on a non-interactive flow.\n- `passkey_login_disabled` (400): Sign in with a passkey is not enabled for this client.\n- `mfa_pending` (401): A second-factor challenge is pending; answer it before continuing.\n- `mfa_invalid_code` (401): The authenticator code did not verify.\n- `mfa_invalid_recovery_code` (401): The recovery code did not verify.\n- `too_many_attempts` (429): Too many wrong codes. Wait a few minutes and try again.\n- `totp_not_allowed` (401): This account does not accept authenticator apps.\n- `no_usable_factor` (400): The user has no confirmed second factor to challenge.\n- `factor_not_found` (400): No factor with that id belongs to this user.\n- `factor_already_confirmed` (400): The factor was already confirmed.\n- `invalid_factor` (400): The factor cannot be verified (no secret material).\n- `session_missing` (401): The session that started this login no longer exists.\n- `passkey_verification_failed` (400): The WebAuthn response could not be verified.\n- `no_passkey_enrolled` (400): The user has no passkey to sign in with.\n- `malformed_credential` (400): The WebAuthn credential is malformed.\n- `unknown_passkey` (401): The passkey is not enrolled for this user.\n- `invalid_passkey` (401): The passkey assertion did not verify.\n- `invite_not_found` (404): No invite with that id in this account.\n- `invite_used` (400): The invite was already accepted.\n- `already_member` (400): The user is already a member of the team.\n- `not_a_member` (404): The user is not a member of the team.\n- `role_not_found` (404): No role with that id in this account.\n- `invalid_role` (400): The role belongs to another account.\n- `flow_not_found` (400): The login flow does not exist in this account.\n- `revision_not_found` (404): The revision is not in the flow history.",
6968
+ "description": "Machine-readable error code. One HTTP status per code.\n\n- `bad_request` (400): The request is malformed. `message` says what is wrong.\n- `validation_error` (400): The body, query or path failed schema validation. `details.issues` lists each failing field.\n- `unauthorized` (401): The request carries no valid credentials.\n- `payment_required` (402): The feature is not included in the current plan.\n- `forbidden` (403): The credentials are valid but do not allow this operation.\n- `not_found` (404): The resource does not exist in this account.\n- `method_not_allowed` (405): The HTTP method is not supported on this path.\n- `conflict` (409): The request conflicts with the current state of the resource.\n- `payload_too_large` (413): The request body exceeds the size limit.\n- `unsupported_media_type` (415): The `Content-Type` is not accepted by this endpoint.\n- `unprocessable_entity` (422): The request is well-formed but cannot be processed.\n- `too_many_requests` (429): Rate limit exceeded. Honour the `Retry-After` header.\n- `internal_error` (500): Unexpected server error. Retry later; the request id is logged.\n- `already_exists` (400): A resource with the same unique field already exists (for a user, usually `email`). `details.fields` names the field.\n- `invalid_id` (400): The id in the path is not a valid id for this resource.\n- `account_not_found` (404): No Auth Account matches the request (domain, header or token).\n- `invalid_query` (400): The `?query=` FaableQL expression could not be parsed.\n- `search_not_supported` (400): This resource does not support `?search=`.\n- `invalid_expand` (400): An `?expand=` path is not allowed on this resource.\n- `access_denied` (401): The caller may not access this row (ownership or machine-to-machine restriction).\n- `insufficient_scope` (403): The token lacks a scope this operation requires. `message` names it.\n- `team_required` (400): An Auth Account must belong to a project (team).\n- `account_limit_reached` (409): The project already has the maximum number of Auth Accounts.\n- `invalid_project_id` (400): The project id is not of the form `project_<hex>` or `team_<hex>`.\n- `account_transfer_refused` (409): The Auth Account cannot be moved to that project. `details.blockers` lists why.\n- `plan_required` (402): The setting requires a higher plan. `message` says which.\n- `not_logged_in` (401): There is no signed-in session for this request.\n- `invalid_token` (401): The bearer token is missing, malformed, expired or has no subject.\n- `not_owner` (403): The caller may only perform this operation on their own user.\n- `user_suspended` (403): The user is suspended and cannot sign in or be modified.\n- `invalid_credentials` (400): The email/username or password is incorrect.\n- `login_denied` (401): A post-login Action denied the sign-in. `message` carries the reason it gave. On token grants it is a 403 with `error: invalid_grant`.\n- `signup_denied` (403): A pre-signup Action or an active block denied the sign-up. `message` carries the reason.\n- `client_credentials_denied` (401): An Action denied the client_credentials grant. `message` carries the reason.\n- `signup_disabled` (403): Self-service sign-up is disabled on this connection.\n- `email_taken` (409): Another user in this account already uses that email.\n- `invalid_email` (400): The email address is not valid.\n- `same_email` (400): The new email is the same as the current one.\n- `user_has_no_email` (400): The operation needs an email but the user has none.\n- `password_too_weak` (400): The password does not meet the connection policy. `message` lists each unmet rule.\n- `invalid_password_hash` (400): The imported password hash cannot be read, or its cost is above what a login can afford. `message` names the field.\n- `weak_password_hash` (400): The imported password hash uses a fast digest (MD5, SHA-1, SHA-2) or a cost below the minimum. `message` says which.\n- `invalid_code` (400): The verification code is wrong, expired or already used.\n- `invalid_phone` (400): The phone number is not E.164 and could not be resolved with the account default country. Include the country code.\n- `phone_changed` (400): The phone on the user changed after the code was sent. Start again.\n- `phone_already_verified` (409): The user already has a verified phone; the number cannot be swapped from the screen.\n- `sms_unavailable` (409): This account cannot send SMS right now. The suffix says why: `sms_unavailable:plan`, `:quota`, `:provider`.\n- `sms_failed` (409): The SMS provider rejected the message.\n- `invalid_state` (400): The `state` is missing, expired, or does not describe a resumable step.\n- `state_mismatch` (401): The `state` belongs to another account, session, client or ceremony.\n- `invalid_ticket` (400): The ticket does not exist or is not the kind this endpoint accepts.\n- `ticket_used` (400): The ticket was already consumed.\n- `ticket_expired` (400): The ticket is past its expiry.\n- `ticket_not_found` (404): No ticket with that id belongs to this user.\n- `credential_not_found` (404): The user has no password credential on this connection.\n- `invalid_link` (400): The magic link is malformed.\n- `expired_link` (400): The magic link token was not found or has expired.\n- `invalid_otp` (401): The one-time password is wrong or expired.\n- `invalid_client` (400): The `client_id` in the request does not name a client of this account.\n- `client_not_found` (404): No client with that `client_id`.\n- `client_mismatch` (401): The client belongs to a different account.\n- `invalid_client_metadata` (400): Dynamic client registration rejected a field (RFC 7591). `message` names it.\n- `invalid_connection` (400): The connection does not exist, is disabled, or is not of the type this flow needs.\n- `connection_required` (400): No connection was given and the client has no `default_connection`.\n- `connection_misconfigured` (400): The connection is missing settings needed to talk to its provider.\n- `passwordless_unavailable` (400): No passwordless connection is configured or enabled for this client.\n- `ambiguous_connection` (400): Several connections match; pass `connection_id`.\n- `invalid_redirect_uri` (400): The redirect URI is not registered for the client.\n- `origin_not_allowed` (403): The request `Origin` is not in the client's Allowed Web Origins.\n- `audience_not_found` (403): The `audience` does not name an API registered in this account.\n- `system_resource` (403): The resource is managed by Faable and cannot be modified or deleted.\n- `invalid_request` (400): A required OAuth parameter is missing or two of them are incompatible. `message` says which.\n- `invalid_device_code` (400): The device or user code is invalid or expired.\n- `provider_error` (400): The upstream identity provider returned an error. The suffix is the provider’s own code, when it sent one: `provider_error:bad_refresh_token` (the user must re-authorize), `provider_error:incorrect_client_credentials` (our configuration, the user can do nothing). `details.provider_error_description` carries the provider’s sentence.\n- `identity_already_linked` (409): That external identity is already linked to another user.\n- `identity_link_denied` (409): That external identity cannot be linked to this user.\n- `identity_conflict` (409): The user already has a linked identity for this connection.\n- `identity_orphaned` (400): The identity pointed at a user that no longer exists; it was removed. Sign in again.\n- `no_provider_token` (400): The identity holds no provider access token.\n- `no_refresh_token` (400): The identity holds no provider refresh token, so it cannot be refreshed.\n- `action_unavailable` (401): The Action that paused this login is disabled or belongs elsewhere.\n- `login_flow_misconfigured` (500): The login flow of this account cannot run. `message` carries the node error.\n- `mfa_required` (403): The login needs a second factor that this flow cannot collect, or one is still pending on the session.\n- `step_up_required` (403): Confirm an existing factor before changing your factors.\n- `interaction_required` (403): An Action asked for a redirect on a non-interactive flow.\n- `passkey_login_disabled` (400): Sign in with a passkey is not enabled for this client.\n- `mfa_pending` (401): A second-factor challenge is pending; answer it before continuing.\n- `mfa_invalid_code` (401): The authenticator code did not verify.\n- `mfa_invalid_recovery_code` (401): The recovery code did not verify.\n- `too_many_attempts` (429): Too many wrong codes. Wait a few minutes and try again.\n- `totp_not_allowed` (401): This account does not accept authenticator apps.\n- `no_usable_factor` (400): The user has no confirmed second factor to challenge.\n- `factor_not_found` (400): No factor with that id belongs to this user.\n- `factor_already_confirmed` (400): The factor was already confirmed.\n- `invalid_factor` (400): The factor cannot be verified (no secret material).\n- `session_missing` (401): The session that started this login no longer exists.\n- `passkey_verification_failed` (400): The WebAuthn response could not be verified.\n- `no_passkey_enrolled` (400): The user has no passkey to sign in with.\n- `malformed_credential` (400): The WebAuthn credential is malformed.\n- `unknown_passkey` (401): The passkey is not enrolled for this user.\n- `invalid_passkey` (401): The passkey assertion did not verify.\n- `invite_not_found` (404): No invite with that id in this account.\n- `invite_used` (400): The invite was already accepted.\n- `already_member` (400): The user is already a member of the team.\n- `not_a_member` (404): The user is not a member of the team.\n- `role_not_found` (404): No role with that id in this account.\n- `invalid_role` (400): The role belongs to another account.\n- `flow_not_found` (400): The login flow does not exist in this account.\n- `revision_not_found` (404): The revision is not in the flow history.",
6951
6969
  "enum": [
6952
6970
  "bad_request",
6953
6971
  "validation_error",
@@ -6989,6 +7007,8 @@
6989
7007
  "same_email",
6990
7008
  "user_has_no_email",
6991
7009
  "password_too_weak",
7010
+ "invalid_password_hash",
7011
+ "weak_password_hash",
6992
7012
  "invalid_code",
6993
7013
  "invalid_phone",
6994
7014
  "phone_changed",
@@ -19114,11 +19134,17 @@
19114
19134
  "password_reset"
19115
19135
  ]
19116
19136
  },
19137
+ {
19138
+ "type": "string",
19139
+ "enum": [
19140
+ "import"
19141
+ ]
19142
+ },
19117
19143
  {
19118
19144
  "type": "null"
19119
19145
  }
19120
19146
  ],
19121
- "description": "How `email_verified` was last set. `manual` — admin flipped the flag via `POST /user/:id`. `verification_flow` — user clicked the verification link. `passwordless_otp` — user completed passwordless OTP login. `team_invite` — user clicked a team invitation link. `email_change` — user confirmed a self-service email change. `federated` — verified by the external IdP on OAuth callback. `password_reset` — user completed the recovery link that was emailed to them (the `email` channel only). `null` — the field was cleared (admin un-verified the email)."
19147
+ "description": "How `email_verified` was last set. `manual` — admin flipped the flag via `POST /user/:id`. `verification_flow` — user clicked the verification link. `passwordless_otp` — user completed passwordless OTP login. `team_invite` — user clicked a team invitation link. `email_change` — user confirmed a self-service email change. `federated` — verified by the external IdP on OAuth callback. `password_reset` — user completed the recovery link that was emailed to them (the `email` channel only). `import` — the user arrived verified from another provider through `POST /user/import` (asserted by whoever ran the migration). `null` — the field was cleared (admin un-verified the email)."
19122
19148
  },
19123
19149
  "email_verified_at": {
19124
19150
  "type": "string",
@@ -19140,6 +19166,11 @@
19140
19166
  "description": "ISO 8601 timestamp of the built-in welcome email send. Set once by the platform; guarantees at most one welcome per user.",
19141
19167
  "nullable": true
19142
19168
  },
19169
+ "imported_at": {
19170
+ "type": "string",
19171
+ "description": "ISO 8601 timestamp of when the user was created by `POST /user/import`. Imported users get none of the built-in new-user emails (verification, welcome, `user.created` notifications); `user.created` webhooks still fire and carry this field.",
19172
+ "nullable": true
19173
+ },
19143
19174
  "passkey_prompted_at": {
19144
19175
  "type": "string",
19145
19176
  "description": "ISO 8601 timestamp of the last time the hosted \"create a passkey\" offer was shown to this user after a login. Drives the snooze in `login_methods.passkey_promotion_snooze_days`.",
@@ -19217,6 +19248,12 @@
19217
19248
  "password_reset"
19218
19249
  ]
19219
19250
  },
19251
+ {
19252
+ "type": "string",
19253
+ "enum": [
19254
+ "import"
19255
+ ]
19256
+ },
19220
19257
  {
19221
19258
  "type": "null"
19222
19259
  }
@@ -19668,11 +19705,17 @@
19668
19705
  "password_reset"
19669
19706
  ]
19670
19707
  },
19708
+ {
19709
+ "type": "string",
19710
+ "enum": [
19711
+ "import"
19712
+ ]
19713
+ },
19671
19714
  {
19672
19715
  "type": "null"
19673
19716
  }
19674
19717
  ],
19675
- "description": "How `email_verified` was last set. `manual` — admin flipped the flag via `POST /user/:id`. `verification_flow` — user clicked the verification link. `passwordless_otp` — user completed passwordless OTP login. `team_invite` — user clicked a team invitation link. `email_change` — user confirmed a self-service email change. `federated` — verified by the external IdP on OAuth callback. `password_reset` — user completed the recovery link that was emailed to them (the `email` channel only). `null` — the field was cleared (admin un-verified the email)."
19718
+ "description": "How `email_verified` was last set. `manual` — admin flipped the flag via `POST /user/:id`. `verification_flow` — user clicked the verification link. `passwordless_otp` — user completed passwordless OTP login. `team_invite` — user clicked a team invitation link. `email_change` — user confirmed a self-service email change. `federated` — verified by the external IdP on OAuth callback. `password_reset` — user completed the recovery link that was emailed to them (the `email` channel only). `import` — the user arrived verified from another provider through `POST /user/import` (asserted by whoever ran the migration). `null` — the field was cleared (admin un-verified the email)."
19676
19719
  },
19677
19720
  "email_verified_at": {
19678
19721
  "type": "string",
@@ -19694,6 +19737,11 @@
19694
19737
  "description": "ISO 8601 timestamp of the built-in welcome email send. Set once by the platform; guarantees at most one welcome per user.",
19695
19738
  "nullable": true
19696
19739
  },
19740
+ "imported_at": {
19741
+ "type": "string",
19742
+ "description": "ISO 8601 timestamp of when the user was created by `POST /user/import`. Imported users get none of the built-in new-user emails (verification, welcome, `user.created` notifications); `user.created` webhooks still fire and carry this field.",
19743
+ "nullable": true
19744
+ },
19697
19745
  "passkey_prompted_at": {
19698
19746
  "type": "string",
19699
19747
  "description": "ISO 8601 timestamp of the last time the hosted \"create a passkey\" offer was shown to this user after a login. Drives the snooze in `login_methods.passkey_promotion_snooze_days`.",
@@ -19771,6 +19819,12 @@
19771
19819
  "password_reset"
19772
19820
  ]
19773
19821
  },
19822
+ {
19823
+ "type": "string",
19824
+ "enum": [
19825
+ "import"
19826
+ ]
19827
+ },
19774
19828
  {
19775
19829
  "type": "null"
19776
19830
  }
@@ -20302,11 +20356,17 @@
20302
20356
  "password_reset"
20303
20357
  ]
20304
20358
  },
20359
+ {
20360
+ "type": "string",
20361
+ "enum": [
20362
+ "import"
20363
+ ]
20364
+ },
20305
20365
  {
20306
20366
  "type": "null"
20307
20367
  }
20308
20368
  ],
20309
- "description": "How `email_verified` was last set. `manual` — admin flipped the flag via `POST /user/:id`. `verification_flow` — user clicked the verification link. `passwordless_otp` — user completed passwordless OTP login. `team_invite` — user clicked a team invitation link. `email_change` — user confirmed a self-service email change. `federated` — verified by the external IdP on OAuth callback. `password_reset` — user completed the recovery link that was emailed to them (the `email` channel only). `null` — the field was cleared (admin un-verified the email)."
20369
+ "description": "How `email_verified` was last set. `manual` — admin flipped the flag via `POST /user/:id`. `verification_flow` — user clicked the verification link. `passwordless_otp` — user completed passwordless OTP login. `team_invite` — user clicked a team invitation link. `email_change` — user confirmed a self-service email change. `federated` — verified by the external IdP on OAuth callback. `password_reset` — user completed the recovery link that was emailed to them (the `email` channel only). `import` — the user arrived verified from another provider through `POST /user/import` (asserted by whoever ran the migration). `null` — the field was cleared (admin un-verified the email)."
20310
20370
  },
20311
20371
  "email_verified_at": {
20312
20372
  "type": "string",
@@ -20328,6 +20388,11 @@
20328
20388
  "description": "ISO 8601 timestamp of the built-in welcome email send. Set once by the platform; guarantees at most one welcome per user.",
20329
20389
  "nullable": true
20330
20390
  },
20391
+ "imported_at": {
20392
+ "type": "string",
20393
+ "description": "ISO 8601 timestamp of when the user was created by `POST /user/import`. Imported users get none of the built-in new-user emails (verification, welcome, `user.created` notifications); `user.created` webhooks still fire and carry this field.",
20394
+ "nullable": true
20395
+ },
20331
20396
  "passkey_prompted_at": {
20332
20397
  "type": "string",
20333
20398
  "description": "ISO 8601 timestamp of the last time the hosted \"create a passkey\" offer was shown to this user after a login. Drives the snooze in `login_methods.passkey_promotion_snooze_days`.",
@@ -20405,6 +20470,12 @@
20405
20470
  "password_reset"
20406
20471
  ]
20407
20472
  },
20473
+ {
20474
+ "type": "string",
20475
+ "enum": [
20476
+ "import"
20477
+ ]
20478
+ },
20408
20479
  {
20409
20480
  "type": "null"
20410
20481
  }
@@ -20856,11 +20927,17 @@
20856
20927
  "password_reset"
20857
20928
  ]
20858
20929
  },
20930
+ {
20931
+ "type": "string",
20932
+ "enum": [
20933
+ "import"
20934
+ ]
20935
+ },
20859
20936
  {
20860
20937
  "type": "null"
20861
20938
  }
20862
20939
  ],
20863
- "description": "How `email_verified` was last set. `manual` — admin flipped the flag via `POST /user/:id`. `verification_flow` — user clicked the verification link. `passwordless_otp` — user completed passwordless OTP login. `team_invite` — user clicked a team invitation link. `email_change` — user confirmed a self-service email change. `federated` — verified by the external IdP on OAuth callback. `password_reset` — user completed the recovery link that was emailed to them (the `email` channel only). `null` — the field was cleared (admin un-verified the email)."
20940
+ "description": "How `email_verified` was last set. `manual` — admin flipped the flag via `POST /user/:id`. `verification_flow` — user clicked the verification link. `passwordless_otp` — user completed passwordless OTP login. `team_invite` — user clicked a team invitation link. `email_change` — user confirmed a self-service email change. `federated` — verified by the external IdP on OAuth callback. `password_reset` — user completed the recovery link that was emailed to them (the `email` channel only). `import` — the user arrived verified from another provider through `POST /user/import` (asserted by whoever ran the migration). `null` — the field was cleared (admin un-verified the email)."
20864
20941
  },
20865
20942
  "email_verified_at": {
20866
20943
  "type": "string",
@@ -20882,6 +20959,11 @@
20882
20959
  "description": "ISO 8601 timestamp of the built-in welcome email send. Set once by the platform; guarantees at most one welcome per user.",
20883
20960
  "nullable": true
20884
20961
  },
20962
+ "imported_at": {
20963
+ "type": "string",
20964
+ "description": "ISO 8601 timestamp of when the user was created by `POST /user/import`. Imported users get none of the built-in new-user emails (verification, welcome, `user.created` notifications); `user.created` webhooks still fire and carry this field.",
20965
+ "nullable": true
20966
+ },
20885
20967
  "passkey_prompted_at": {
20886
20968
  "type": "string",
20887
20969
  "description": "ISO 8601 timestamp of the last time the hosted \"create a passkey\" offer was shown to this user after a login. Drives the snooze in `login_methods.passkey_promotion_snooze_days`.",
@@ -20959,6 +21041,12 @@
20959
21041
  "password_reset"
20960
21042
  ]
20961
21043
  },
21044
+ {
21045
+ "type": "string",
21046
+ "enum": [
21047
+ "import"
21048
+ ]
21049
+ },
20962
21050
  {
20963
21051
  "type": "null"
20964
21052
  }
@@ -24129,117 +24217,178 @@
24129
24217
  }
24130
24218
  }
24131
24219
  },
24132
- "/identity": {
24133
- "get": {
24134
- "operationId": "identity/list",
24135
- "summary": "List Identitys",
24220
+ "/user/import": {
24221
+ "post": {
24222
+ "operationId": "user/import",
24223
+ "summary": "Import users, with their password hashes",
24136
24224
  "tags": [
24137
- "identity"
24225
+ "user"
24138
24226
  ],
24139
- "description": "List Identitys",
24140
- "parameters": [
24141
- {
24142
- "schema": {
24143
- "type": "number",
24144
- "minimum": 1,
24145
- "maximum": 200
24146
- },
24147
- "in": "query",
24148
- "name": "pageSize",
24149
- "required": false,
24150
- "description": "Number of items per page (max 200)"
24151
- },
24152
- {
24153
- "schema": {
24154
- "type": "string"
24155
- },
24156
- "in": "query",
24157
- "name": "cursor",
24158
- "required": false,
24159
- "description": "Cursor for next page"
24160
- },
24161
- {
24162
- "schema": {
24163
- "type": "string"
24164
- },
24165
- "in": "query",
24166
- "name": "next",
24167
- "required": false,
24168
- "description": "Cursor returned by the previous page"
24169
- },
24170
- {
24171
- "schema": {
24172
- "type": "string"
24173
- },
24174
- "in": "query",
24175
- "name": "user",
24176
- "required": false,
24177
- "description": "Exact match on `user`. Equivalent to `?query=user:<value>`."
24178
- },
24179
- {
24180
- "schema": {
24181
- "type": "string"
24182
- },
24183
- "in": "query",
24184
- "name": "connection",
24185
- "required": false,
24186
- "description": "Exact match on `connection`. Equivalent to `?query=connection:<value>`."
24187
- },
24188
- {
24189
- "schema": {
24190
- "type": "string"
24191
- },
24192
- "in": "query",
24193
- "name": "identity_id",
24194
- "required": false,
24195
- "description": "Exact match on `identity_id`. Equivalent to `?query=identity_id:<value>`."
24196
- },
24197
- {
24198
- "schema": {
24199
- "type": "string"
24200
- },
24201
- "in": "query",
24202
- "name": "provider",
24203
- "required": false,
24204
- "description": "Exact match on `provider`. Equivalent to `?query=provider:<value>`."
24205
- },
24206
- {
24207
- "schema": {
24208
- "type": "string"
24209
- },
24210
- "in": "query",
24211
- "name": "created_at",
24212
- "required": false,
24213
- "description": "Exact match on `created_at`. Equivalent to `?query=created_at:<value>`."
24214
- },
24215
- {
24216
- "schema": {
24217
- "type": "string"
24218
- },
24219
- "in": "query",
24220
- "name": "query",
24221
- "required": false,
24222
- "description": "Filter using a FaableQL query"
24223
- },
24224
- {
24225
- "schema": {
24226
- "type": "array",
24227
- "items": {
24228
- "type": "string",
24229
- "enum": [
24230
- "user",
24231
- "connection"
24227
+ "description": "Creates up to 500 users per request, for migrations from another provider. Each row can carry a `password_hash` exported from that provider (bcrypt, scrypt, Firebase scrypt, PBKDF2, argon2): the user signs in with their existing password and the hash is upgraded to argon2id on that first login. Fast digests (MD5, SHA-1, SHA-2) and low costs are refused per row with `weak_password_hash`. Rows are matched by email, so the same file can be sent twice: existing users are `skipped` unless `update_existing`. A bad row never fails the batch: each row gets its own `status` and `error`. Imported users get none of the built-in new-user emails. The response never contains a hash, and neither does the audit log.",
24228
+ "requestBody": {
24229
+ "required": true,
24230
+ "content": {
24231
+ "application/json": {
24232
+ "schema": {
24233
+ "type": "object",
24234
+ "required": [
24235
+ "users"
24232
24236
  ],
24233
- "description": "Allowed expand paths on `identity`: `user`, `connection`."
24234
- },
24235
- "maxItems": 2
24236
- },
24237
- "in": "query",
24238
- "name": "expand",
24239
- "required": false,
24240
- "description": "Expand id-only fields in the response. Pass one path per entry (e.g. `?expand=user`)."
24237
+ "properties": {
24238
+ "users": {
24239
+ "type": "array",
24240
+ "items": {
24241
+ "type": "object",
24242
+ "required": [
24243
+ "email"
24244
+ ],
24245
+ "properties": {
24246
+ "email": {
24247
+ "type": "string",
24248
+ "minLength": 3,
24249
+ "description": "The user's email."
24250
+ },
24251
+ "email_verified": {
24252
+ "type": "boolean",
24253
+ "description": "The email was verified at the previous provider. Recorded with `email_verified_method: import`."
24254
+ },
24255
+ "name": {
24256
+ "type": "string"
24257
+ },
24258
+ "given_name": {
24259
+ "type": "string"
24260
+ },
24261
+ "family_name": {
24262
+ "type": "string"
24263
+ },
24264
+ "phone": {
24265
+ "type": "string"
24266
+ },
24267
+ "birth_date": {
24268
+ "type": "string"
24269
+ },
24270
+ "locale": {
24271
+ "type": "string"
24272
+ },
24273
+ "picture": {
24274
+ "type": "string"
24275
+ },
24276
+ "user_metadata": {
24277
+ "type": "object",
24278
+ "properties": {},
24279
+ "additionalProperties": true,
24280
+ "description": "User Metadata"
24281
+ },
24282
+ "app_metadata": {
24283
+ "type": "object",
24284
+ "properties": {},
24285
+ "additionalProperties": true,
24286
+ "description": "App Metadata"
24287
+ },
24288
+ "password_hash": {
24289
+ "anyOf": [
24290
+ {
24291
+ "type": "string",
24292
+ "minLength": 1,
24293
+ "maxLength": 1024,
24294
+ "description": "A self-describing hash: bcrypt (`$2a$`/`$2b$`/`$2y$`), argon2 PHC (`$argon2id$…`), scrypt PHC (`$scrypt$ln=…`), PBKDF2 (`$pbkdf2-sha256$i=…` or Django `pbkdf2_sha256$…`)."
24295
+ },
24296
+ {
24297
+ "type": "object",
24298
+ "required": [
24299
+ "algorithm",
24300
+ "hash"
24301
+ ],
24302
+ "properties": {
24303
+ "algorithm": {
24304
+ "type": "string",
24305
+ "description": "`bcrypt`, `argon2id`, `argon2i`, `argon2d`, `scrypt`, `pbkdf2-sha256`, `pbkdf2-sha512` or `firebase-scrypt`."
24306
+ },
24307
+ "hash": {
24308
+ "type": "string",
24309
+ "minLength": 1,
24310
+ "maxLength": 1024,
24311
+ "description": "The hash, or its base64 digest when `salt` is given."
24312
+ },
24313
+ "salt": {
24314
+ "type": "string",
24315
+ "maxLength": 512,
24316
+ "description": "Base64 salt."
24317
+ },
24318
+ "params": {
24319
+ "type": "object",
24320
+ "description": "Algorithm parameters: `iterations` (pbkdf2); `N`, `r`, `p` (scrypt); `signer_key`, `salt_separator`, `rounds`, `mem_cost` (firebase-scrypt).",
24321
+ "additionalProperties": {
24322
+ "anyOf": [
24323
+ {
24324
+ "type": "number"
24325
+ },
24326
+ {
24327
+ "type": "string",
24328
+ "maxLength": 512
24329
+ }
24330
+ ]
24331
+ }
24332
+ }
24333
+ },
24334
+ "additionalProperties": false
24335
+ }
24336
+ ],
24337
+ "description": "A password hash exported from another provider. Verified on the first login and upgraded to argon2id. Fast digests (MD5, SHA-1, SHA-2) and low costs are refused with `weak_password_hash`."
24338
+ },
24339
+ "identities": {
24340
+ "type": "array",
24341
+ "items": {
24342
+ "type": "object",
24343
+ "required": [
24344
+ "connection",
24345
+ "identity_id"
24346
+ ],
24347
+ "properties": {
24348
+ "connection": {
24349
+ "type": "string",
24350
+ "description": "connection_name of the social/enterprise connection."
24351
+ },
24352
+ "identity_id": {
24353
+ "type": "string",
24354
+ "description": "The user's id at the provider (GitHub id, Google `sub`…), so their first social login finds this user instead of creating a new one."
24355
+ },
24356
+ "profile_data": {
24357
+ "type": "object",
24358
+ "description": "Provider profile, stored as-is.",
24359
+ "additionalProperties": {}
24360
+ }
24361
+ },
24362
+ "additionalProperties": false
24363
+ },
24364
+ "maxItems": 20
24365
+ }
24366
+ },
24367
+ "additionalProperties": false
24368
+ },
24369
+ "minItems": 1,
24370
+ "maxItems": 500
24371
+ },
24372
+ "connection": {
24373
+ "type": "string",
24374
+ "description": "connection_name of the database connection the password hashes go to. Defaults to the tenant database connection."
24375
+ },
24376
+ "update_existing": {
24377
+ "type": "boolean",
24378
+ "description": "Set the password hash and identities of users that already exist (matched by email) instead of skipping them. Their profile is never overwritten.",
24379
+ "default": false
24380
+ },
24381
+ "dry_run": {
24382
+ "type": "boolean",
24383
+ "description": "Validate every row and resolve every match without writing anything.",
24384
+ "default": false
24385
+ }
24386
+ },
24387
+ "additionalProperties": false
24388
+ }
24389
+ }
24241
24390
  }
24242
- ],
24391
+ },
24243
24392
  "security": [
24244
24393
  {
24245
24394
  "bearerAuth": []
@@ -24253,34 +24402,110 @@
24253
24402
  "schema": {
24254
24403
  "type": "object",
24255
24404
  "required": [
24256
- "next",
24405
+ "summary",
24257
24406
  "results"
24258
24407
  ],
24259
24408
  "properties": {
24260
- "next": {
24261
- "anyOf": [
24262
- {
24263
- "type": "string"
24409
+ "summary": {
24410
+ "type": "object",
24411
+ "required": [
24412
+ "total",
24413
+ "created",
24414
+ "updated",
24415
+ "skipped",
24416
+ "error"
24417
+ ],
24418
+ "properties": {
24419
+ "total": {
24420
+ "type": "number"
24264
24421
  },
24265
- {
24266
- "type": "null"
24422
+ "created": {
24423
+ "type": "number"
24424
+ },
24425
+ "updated": {
24426
+ "type": "number"
24427
+ },
24428
+ "skipped": {
24429
+ "type": "number"
24430
+ },
24431
+ "error": {
24432
+ "type": "number"
24267
24433
  }
24268
- ]
24434
+ }
24269
24435
  },
24270
24436
  "results": {
24271
24437
  "type": "array",
24272
24438
  "items": {
24273
- "$ref": "#/components/schemas/Identity"
24439
+ "type": "object",
24440
+ "required": [
24441
+ "index",
24442
+ "email",
24443
+ "status"
24444
+ ],
24445
+ "properties": {
24446
+ "index": {
24447
+ "type": "number",
24448
+ "description": "Position of the row in `users`."
24449
+ },
24450
+ "email": {
24451
+ "type": "string"
24452
+ },
24453
+ "status": {
24454
+ "anyOf": [
24455
+ {
24456
+ "type": "string",
24457
+ "enum": [
24458
+ "created"
24459
+ ]
24460
+ },
24461
+ {
24462
+ "type": "string",
24463
+ "enum": [
24464
+ "updated"
24465
+ ]
24466
+ },
24467
+ {
24468
+ "type": "string",
24469
+ "enum": [
24470
+ "skipped"
24471
+ ]
24472
+ },
24473
+ {
24474
+ "type": "string",
24475
+ "enum": [
24476
+ "error"
24477
+ ]
24478
+ }
24479
+ ]
24480
+ },
24481
+ "user_id": {
24482
+ "type": "string"
24483
+ },
24484
+ "error": {
24485
+ "type": "object",
24486
+ "required": [
24487
+ "code",
24488
+ "message"
24489
+ ],
24490
+ "properties": {
24491
+ "code": {
24492
+ "type": "string"
24493
+ },
24494
+ "message": {
24495
+ "type": "string"
24496
+ }
24497
+ }
24498
+ }
24499
+ }
24274
24500
  }
24275
24501
  }
24276
- },
24277
- "additionalProperties": false
24502
+ }
24278
24503
  }
24279
24504
  }
24280
24505
  }
24281
24506
  },
24282
24507
  "400": {
24283
- "description": "`invalid_query` — The `?query=` FaableQL expression could not be parsed.\n\n`invalid_expand` — An `?expand=` path is not allowed on this resource.\n\n`search_not_supported` — This resource does not support `?search=`.\n\n`validation_error` — The body, query or path failed schema validation. `details.issues` lists each failing field.",
24508
+ "description": "`validation_error` — The body, query or path failed schema validation. `details.issues` lists each failing field.",
24284
24509
  "content": {
24285
24510
  "application/json": {
24286
24511
  "schema": {
@@ -24294,9 +24519,676 @@
24294
24519
  "error_code": {
24295
24520
  "type": "string",
24296
24521
  "enum": [
24297
- "invalid_query",
24298
- "invalid_expand",
24299
- "search_not_supported",
24522
+ "validation_error"
24523
+ ]
24524
+ }
24525
+ }
24526
+ }
24527
+ ]
24528
+ }
24529
+ }
24530
+ }
24531
+ },
24532
+ "401": {
24533
+ "description": "`unauthorized` — The request carries no valid credentials.",
24534
+ "content": {
24535
+ "application/json": {
24536
+ "schema": {
24537
+ "allOf": [
24538
+ {
24539
+ "$ref": "#/components/schemas/ErrorResponse"
24540
+ },
24541
+ {
24542
+ "type": "object",
24543
+ "properties": {
24544
+ "error_code": {
24545
+ "type": "string",
24546
+ "enum": [
24547
+ "unauthorized"
24548
+ ]
24549
+ }
24550
+ }
24551
+ }
24552
+ ]
24553
+ }
24554
+ }
24555
+ }
24556
+ },
24557
+ "403": {
24558
+ "description": "`forbidden` — The credentials are valid but do not allow this operation.\n\n`insufficient_scope` — The token lacks a scope this operation requires. `message` names it.\n\n`user_suspended` — The user is suspended and cannot sign in or be modified.",
24559
+ "content": {
24560
+ "application/json": {
24561
+ "schema": {
24562
+ "allOf": [
24563
+ {
24564
+ "$ref": "#/components/schemas/ErrorResponse"
24565
+ },
24566
+ {
24567
+ "type": "object",
24568
+ "properties": {
24569
+ "error_code": {
24570
+ "type": "string",
24571
+ "enum": [
24572
+ "forbidden",
24573
+ "insufficient_scope",
24574
+ "user_suspended"
24575
+ ]
24576
+ }
24577
+ }
24578
+ }
24579
+ ]
24580
+ }
24581
+ }
24582
+ }
24583
+ },
24584
+ "404": {
24585
+ "description": "`account_not_found` — No Auth Account matches the request (domain, header or token).",
24586
+ "content": {
24587
+ "application/json": {
24588
+ "schema": {
24589
+ "allOf": [
24590
+ {
24591
+ "$ref": "#/components/schemas/ErrorResponse"
24592
+ },
24593
+ {
24594
+ "type": "object",
24595
+ "properties": {
24596
+ "error_code": {
24597
+ "type": "string",
24598
+ "enum": [
24599
+ "account_not_found"
24600
+ ]
24601
+ }
24602
+ }
24603
+ }
24604
+ ]
24605
+ }
24606
+ }
24607
+ }
24608
+ },
24609
+ "429": {
24610
+ "description": "`too_many_requests` — Rate limit exceeded. Honour the `Retry-After` header.",
24611
+ "content": {
24612
+ "application/json": {
24613
+ "schema": {
24614
+ "allOf": [
24615
+ {
24616
+ "$ref": "#/components/schemas/ErrorResponse"
24617
+ },
24618
+ {
24619
+ "type": "object",
24620
+ "properties": {
24621
+ "error_code": {
24622
+ "type": "string",
24623
+ "enum": [
24624
+ "too_many_requests"
24625
+ ]
24626
+ }
24627
+ }
24628
+ }
24629
+ ]
24630
+ }
24631
+ }
24632
+ }
24633
+ },
24634
+ "500": {
24635
+ "description": "`internal_error` — Unexpected server error. Retry later; the request id is logged.",
24636
+ "content": {
24637
+ "application/json": {
24638
+ "schema": {
24639
+ "allOf": [
24640
+ {
24641
+ "$ref": "#/components/schemas/ErrorResponse"
24642
+ },
24643
+ {
24644
+ "type": "object",
24645
+ "properties": {
24646
+ "error_code": {
24647
+ "type": "string",
24648
+ "enum": [
24649
+ "internal_error"
24650
+ ]
24651
+ }
24652
+ }
24653
+ }
24654
+ ]
24655
+ }
24656
+ }
24657
+ }
24658
+ }
24659
+ }
24660
+ }
24661
+ },
24662
+ "/user/export": {
24663
+ "get": {
24664
+ "operationId": "user/export",
24665
+ "summary": "Export users, optionally with their password hashes",
24666
+ "tags": [
24667
+ "user"
24668
+ ],
24669
+ "description": "Pages through every user of the tenant, 500 at a time, in the row shape `POST /user/import` takes. With `include_hashes` each row carries its `password_hash` (needs the `read:password_hashes` scope, is audited as `credentials.exported` and notifies its subscribers). Provider access and refresh tokens of linked identities are never exported. Available on every plan.",
24670
+ "parameters": [
24671
+ {
24672
+ "schema": {
24673
+ "type": "string"
24674
+ },
24675
+ "in": "query",
24676
+ "name": "cursor",
24677
+ "required": false,
24678
+ "description": "`next_cursor` of the previous page."
24679
+ },
24680
+ {
24681
+ "schema": {
24682
+ "type": "integer",
24683
+ "minimum": 1,
24684
+ "maximum": 500,
24685
+ "default": 500
24686
+ },
24687
+ "in": "query",
24688
+ "name": "limit",
24689
+ "required": false
24690
+ },
24691
+ {
24692
+ "schema": {
24693
+ "type": "string"
24694
+ },
24695
+ "in": "query",
24696
+ "name": "connection",
24697
+ "required": false,
24698
+ "description": "connection_name of the database connection whose password hashes are exported. Defaults to the tenant database connection."
24699
+ },
24700
+ {
24701
+ "schema": {
24702
+ "type": "boolean",
24703
+ "default": false
24704
+ },
24705
+ "in": "query",
24706
+ "name": "include_hashes",
24707
+ "required": false,
24708
+ "description": "Include `password_hash`. Needs the `read:password_hashes` scope, is written to the audit log and notifies the `credentials.exported` subscribers."
24709
+ }
24710
+ ],
24711
+ "security": [
24712
+ {
24713
+ "bearerAuth": []
24714
+ }
24715
+ ],
24716
+ "responses": {
24717
+ "200": {
24718
+ "description": "Default Response",
24719
+ "content": {
24720
+ "application/json": {
24721
+ "schema": {
24722
+ "type": "object",
24723
+ "required": [
24724
+ "users",
24725
+ "next_cursor",
24726
+ "connection"
24727
+ ],
24728
+ "properties": {
24729
+ "users": {
24730
+ "type": "array",
24731
+ "items": {
24732
+ "type": "object",
24733
+ "required": [
24734
+ "user_id",
24735
+ "email",
24736
+ "email_verified",
24737
+ "password_hash_status"
24738
+ ],
24739
+ "properties": {
24740
+ "user_id": {
24741
+ "type": "string"
24742
+ },
24743
+ "email": {
24744
+ "anyOf": [
24745
+ {
24746
+ "type": "string"
24747
+ },
24748
+ {
24749
+ "type": "null"
24750
+ }
24751
+ ]
24752
+ },
24753
+ "email_verified": {
24754
+ "type": "boolean"
24755
+ },
24756
+ "name": {
24757
+ "type": "string"
24758
+ },
24759
+ "given_name": {
24760
+ "type": "string"
24761
+ },
24762
+ "family_name": {
24763
+ "type": "string"
24764
+ },
24765
+ "phone": {
24766
+ "type": "string"
24767
+ },
24768
+ "birth_date": {
24769
+ "type": "string"
24770
+ },
24771
+ "locale": {
24772
+ "type": "string"
24773
+ },
24774
+ "picture": {
24775
+ "type": "string"
24776
+ },
24777
+ "user_metadata": {
24778
+ "type": "object",
24779
+ "additionalProperties": {}
24780
+ },
24781
+ "app_metadata": {
24782
+ "type": "object",
24783
+ "additionalProperties": {}
24784
+ },
24785
+ "password_hash_status": {
24786
+ "anyOf": [
24787
+ {
24788
+ "type": "string",
24789
+ "enum": [
24790
+ "set"
24791
+ ]
24792
+ },
24793
+ {
24794
+ "type": "string",
24795
+ "enum": [
24796
+ "not_set"
24797
+ ]
24798
+ },
24799
+ {
24800
+ "type": "string",
24801
+ "enum": [
24802
+ "no_credential"
24803
+ ]
24804
+ },
24805
+ {
24806
+ "type": "string",
24807
+ "enum": [
24808
+ "legacy_unexportable"
24809
+ ]
24810
+ }
24811
+ ],
24812
+ "description": "`legacy_unexportable`: the password is still stored in a pre-2026 format that is not exported; it upgrades on the next login."
24813
+ },
24814
+ "password_hash": {
24815
+ "type": "string",
24816
+ "description": "Self-describing hash (argon2id PHC for passwords set on Faable; the original format for imported ones not yet upgraded). Only with `include_hashes`."
24817
+ },
24818
+ "identities": {
24819
+ "type": "array",
24820
+ "items": {
24821
+ "type": "object",
24822
+ "required": [
24823
+ "connection",
24824
+ "identity_id"
24825
+ ],
24826
+ "properties": {
24827
+ "connection": {
24828
+ "type": "string"
24829
+ },
24830
+ "identity_id": {
24831
+ "type": "string"
24832
+ },
24833
+ "profile_data": {
24834
+ "type": "object",
24835
+ "additionalProperties": {}
24836
+ }
24837
+ }
24838
+ }
24839
+ }
24840
+ }
24841
+ }
24842
+ },
24843
+ "next_cursor": {
24844
+ "anyOf": [
24845
+ {
24846
+ "type": "string"
24847
+ },
24848
+ {
24849
+ "type": "null"
24850
+ }
24851
+ ]
24852
+ },
24853
+ "connection": {
24854
+ "anyOf": [
24855
+ {
24856
+ "type": "string"
24857
+ },
24858
+ {
24859
+ "type": "null"
24860
+ }
24861
+ ]
24862
+ }
24863
+ }
24864
+ }
24865
+ }
24866
+ }
24867
+ },
24868
+ "400": {
24869
+ "description": "`invalid_connection` — The connection does not exist, is disabled, or is not of the type this flow needs.\n\n`validation_error` — The body, query or path failed schema validation. `details.issues` lists each failing field.",
24870
+ "content": {
24871
+ "application/json": {
24872
+ "schema": {
24873
+ "allOf": [
24874
+ {
24875
+ "$ref": "#/components/schemas/ErrorResponse"
24876
+ },
24877
+ {
24878
+ "type": "object",
24879
+ "properties": {
24880
+ "error_code": {
24881
+ "type": "string",
24882
+ "enum": [
24883
+ "invalid_connection",
24884
+ "validation_error"
24885
+ ]
24886
+ }
24887
+ }
24888
+ }
24889
+ ]
24890
+ }
24891
+ }
24892
+ }
24893
+ },
24894
+ "401": {
24895
+ "description": "`unauthorized` — The request carries no valid credentials.",
24896
+ "content": {
24897
+ "application/json": {
24898
+ "schema": {
24899
+ "allOf": [
24900
+ {
24901
+ "$ref": "#/components/schemas/ErrorResponse"
24902
+ },
24903
+ {
24904
+ "type": "object",
24905
+ "properties": {
24906
+ "error_code": {
24907
+ "type": "string",
24908
+ "enum": [
24909
+ "unauthorized"
24910
+ ]
24911
+ }
24912
+ }
24913
+ }
24914
+ ]
24915
+ }
24916
+ }
24917
+ }
24918
+ },
24919
+ "403": {
24920
+ "description": "`forbidden` — The credentials are valid but do not allow this operation.\n\n`insufficient_scope` — The token lacks a scope this operation requires. `message` names it.\n\n`user_suspended` — The user is suspended and cannot sign in or be modified.",
24921
+ "content": {
24922
+ "application/json": {
24923
+ "schema": {
24924
+ "allOf": [
24925
+ {
24926
+ "$ref": "#/components/schemas/ErrorResponse"
24927
+ },
24928
+ {
24929
+ "type": "object",
24930
+ "properties": {
24931
+ "error_code": {
24932
+ "type": "string",
24933
+ "enum": [
24934
+ "forbidden",
24935
+ "insufficient_scope",
24936
+ "user_suspended"
24937
+ ]
24938
+ }
24939
+ }
24940
+ }
24941
+ ]
24942
+ }
24943
+ }
24944
+ }
24945
+ },
24946
+ "404": {
24947
+ "description": "`account_not_found` — No Auth Account matches the request (domain, header or token).",
24948
+ "content": {
24949
+ "application/json": {
24950
+ "schema": {
24951
+ "allOf": [
24952
+ {
24953
+ "$ref": "#/components/schemas/ErrorResponse"
24954
+ },
24955
+ {
24956
+ "type": "object",
24957
+ "properties": {
24958
+ "error_code": {
24959
+ "type": "string",
24960
+ "enum": [
24961
+ "account_not_found"
24962
+ ]
24963
+ }
24964
+ }
24965
+ }
24966
+ ]
24967
+ }
24968
+ }
24969
+ }
24970
+ },
24971
+ "429": {
24972
+ "description": "`too_many_requests` — Rate limit exceeded. Honour the `Retry-After` header.",
24973
+ "content": {
24974
+ "application/json": {
24975
+ "schema": {
24976
+ "allOf": [
24977
+ {
24978
+ "$ref": "#/components/schemas/ErrorResponse"
24979
+ },
24980
+ {
24981
+ "type": "object",
24982
+ "properties": {
24983
+ "error_code": {
24984
+ "type": "string",
24985
+ "enum": [
24986
+ "too_many_requests"
24987
+ ]
24988
+ }
24989
+ }
24990
+ }
24991
+ ]
24992
+ }
24993
+ }
24994
+ }
24995
+ },
24996
+ "500": {
24997
+ "description": "`internal_error` — Unexpected server error. Retry later; the request id is logged.",
24998
+ "content": {
24999
+ "application/json": {
25000
+ "schema": {
25001
+ "allOf": [
25002
+ {
25003
+ "$ref": "#/components/schemas/ErrorResponse"
25004
+ },
25005
+ {
25006
+ "type": "object",
25007
+ "properties": {
25008
+ "error_code": {
25009
+ "type": "string",
25010
+ "enum": [
25011
+ "internal_error"
25012
+ ]
25013
+ }
25014
+ }
25015
+ }
25016
+ ]
25017
+ }
25018
+ }
25019
+ }
25020
+ }
25021
+ }
25022
+ }
25023
+ },
25024
+ "/identity": {
25025
+ "get": {
25026
+ "operationId": "identity/list",
25027
+ "summary": "List Identitys",
25028
+ "tags": [
25029
+ "identity"
25030
+ ],
25031
+ "description": "List Identitys",
25032
+ "parameters": [
25033
+ {
25034
+ "schema": {
25035
+ "type": "number",
25036
+ "minimum": 1,
25037
+ "maximum": 200
25038
+ },
25039
+ "in": "query",
25040
+ "name": "pageSize",
25041
+ "required": false,
25042
+ "description": "Number of items per page (max 200)"
25043
+ },
25044
+ {
25045
+ "schema": {
25046
+ "type": "string"
25047
+ },
25048
+ "in": "query",
25049
+ "name": "cursor",
25050
+ "required": false,
25051
+ "description": "Cursor for next page"
25052
+ },
25053
+ {
25054
+ "schema": {
25055
+ "type": "string"
25056
+ },
25057
+ "in": "query",
25058
+ "name": "next",
25059
+ "required": false,
25060
+ "description": "Cursor returned by the previous page"
25061
+ },
25062
+ {
25063
+ "schema": {
25064
+ "type": "string"
25065
+ },
25066
+ "in": "query",
25067
+ "name": "user",
25068
+ "required": false,
25069
+ "description": "Exact match on `user`. Equivalent to `?query=user:<value>`."
25070
+ },
25071
+ {
25072
+ "schema": {
25073
+ "type": "string"
25074
+ },
25075
+ "in": "query",
25076
+ "name": "connection",
25077
+ "required": false,
25078
+ "description": "Exact match on `connection`. Equivalent to `?query=connection:<value>`."
25079
+ },
25080
+ {
25081
+ "schema": {
25082
+ "type": "string"
25083
+ },
25084
+ "in": "query",
25085
+ "name": "identity_id",
25086
+ "required": false,
25087
+ "description": "Exact match on `identity_id`. Equivalent to `?query=identity_id:<value>`."
25088
+ },
25089
+ {
25090
+ "schema": {
25091
+ "type": "string"
25092
+ },
25093
+ "in": "query",
25094
+ "name": "provider",
25095
+ "required": false,
25096
+ "description": "Exact match on `provider`. Equivalent to `?query=provider:<value>`."
25097
+ },
25098
+ {
25099
+ "schema": {
25100
+ "type": "string"
25101
+ },
25102
+ "in": "query",
25103
+ "name": "created_at",
25104
+ "required": false,
25105
+ "description": "Exact match on `created_at`. Equivalent to `?query=created_at:<value>`."
25106
+ },
25107
+ {
25108
+ "schema": {
25109
+ "type": "string"
25110
+ },
25111
+ "in": "query",
25112
+ "name": "query",
25113
+ "required": false,
25114
+ "description": "Filter using a FaableQL query"
25115
+ },
25116
+ {
25117
+ "schema": {
25118
+ "type": "array",
25119
+ "items": {
25120
+ "type": "string",
25121
+ "enum": [
25122
+ "user",
25123
+ "connection"
25124
+ ],
25125
+ "description": "Allowed expand paths on `identity`: `user`, `connection`."
25126
+ },
25127
+ "maxItems": 2
25128
+ },
25129
+ "in": "query",
25130
+ "name": "expand",
25131
+ "required": false,
25132
+ "description": "Expand id-only fields in the response. Pass one path per entry (e.g. `?expand=user`)."
25133
+ }
25134
+ ],
25135
+ "security": [
25136
+ {
25137
+ "bearerAuth": []
25138
+ }
25139
+ ],
25140
+ "responses": {
25141
+ "200": {
25142
+ "description": "Default Response",
25143
+ "content": {
25144
+ "application/json": {
25145
+ "schema": {
25146
+ "type": "object",
25147
+ "required": [
25148
+ "next",
25149
+ "results"
25150
+ ],
25151
+ "properties": {
25152
+ "next": {
25153
+ "anyOf": [
25154
+ {
25155
+ "type": "string"
25156
+ },
25157
+ {
25158
+ "type": "null"
25159
+ }
25160
+ ]
25161
+ },
25162
+ "results": {
25163
+ "type": "array",
25164
+ "items": {
25165
+ "$ref": "#/components/schemas/Identity"
25166
+ }
25167
+ }
25168
+ },
25169
+ "additionalProperties": false
25170
+ }
25171
+ }
25172
+ }
25173
+ },
25174
+ "400": {
25175
+ "description": "`invalid_query` — The `?query=` FaableQL expression could not be parsed.\n\n`invalid_expand` — An `?expand=` path is not allowed on this resource.\n\n`search_not_supported` — This resource does not support `?search=`.\n\n`validation_error` — The body, query or path failed schema validation. `details.issues` lists each failing field.",
25176
+ "content": {
25177
+ "application/json": {
25178
+ "schema": {
25179
+ "allOf": [
25180
+ {
25181
+ "$ref": "#/components/schemas/ErrorResponse"
25182
+ },
25183
+ {
25184
+ "type": "object",
25185
+ "properties": {
25186
+ "error_code": {
25187
+ "type": "string",
25188
+ "enum": [
25189
+ "invalid_query",
25190
+ "invalid_expand",
25191
+ "search_not_supported",
24300
25192
  "validation_error"
24301
25193
  ]
24302
25194
  }
@@ -25838,7 +26730,8 @@
25838
26730
  "username": {
25839
26731
  "type": "string"
25840
26732
  }
25841
- }
26733
+ },
26734
+ "additionalProperties": false
25842
26735
  }
25843
26736
  }
25844
26737
  }
@@ -26882,20 +27775,68 @@
26882
27775
  "tags": [
26883
27776
  "credentials"
26884
27777
  ],
26885
- "description": "Sets or rotates the password for a database credential. The previous password is invalidated immediately. Records a `credential.password_changed` audit entry (initiated_by=admin) for traceability. The confirmation email is only sent when an EXISTING password is rotated: setting the first password on a credential that had none is a creation, not a change. The password is never returned.",
27778
+ "description": "Sets or rotates the password for a database credential. Send exactly one of `password` (plaintext, hashed with argon2id) or `password_hash` (a hash exported from another provider: bcrypt, scrypt, Firebase scrypt, PBKDF2 or argon2 — verified on the first login and upgraded to argon2id). The previous password is invalidated immediately. A plaintext password records a `credential.password_changed` audit entry (initiated_by=admin); the confirmation email is only sent when an EXISTING password is rotated. An imported hash records `credential.password_imported` with its algorithm and sends no email. Neither the password nor the hash is ever returned or logged.",
26886
27779
  "requestBody": {
26887
27780
  "required": true,
26888
27781
  "content": {
26889
27782
  "application/json": {
26890
27783
  "schema": {
26891
27784
  "type": "object",
26892
- "required": [
26893
- "password"
26894
- ],
26895
27785
  "properties": {
26896
27786
  "password": {
26897
27787
  "type": "string",
26898
27788
  "description": "New plaintext password (hashed on write)."
27789
+ },
27790
+ "password_hash": {
27791
+ "anyOf": [
27792
+ {
27793
+ "type": "string",
27794
+ "minLength": 1,
27795
+ "maxLength": 1024,
27796
+ "description": "A self-describing hash: bcrypt (`$2a$`/`$2b$`/`$2y$`), argon2 PHC (`$argon2id$…`), scrypt PHC (`$scrypt$ln=…`), PBKDF2 (`$pbkdf2-sha256$i=…` or Django `pbkdf2_sha256$…`)."
27797
+ },
27798
+ {
27799
+ "type": "object",
27800
+ "required": [
27801
+ "algorithm",
27802
+ "hash"
27803
+ ],
27804
+ "properties": {
27805
+ "algorithm": {
27806
+ "type": "string",
27807
+ "description": "`bcrypt`, `argon2id`, `argon2i`, `argon2d`, `scrypt`, `pbkdf2-sha256`, `pbkdf2-sha512` or `firebase-scrypt`."
27808
+ },
27809
+ "hash": {
27810
+ "type": "string",
27811
+ "minLength": 1,
27812
+ "maxLength": 1024,
27813
+ "description": "The hash, or its base64 digest when `salt` is given."
27814
+ },
27815
+ "salt": {
27816
+ "type": "string",
27817
+ "maxLength": 512,
27818
+ "description": "Base64 salt."
27819
+ },
27820
+ "params": {
27821
+ "type": "object",
27822
+ "description": "Algorithm parameters: `iterations` (pbkdf2); `N`, `r`, `p` (scrypt); `signer_key`, `salt_separator`, `rounds`, `mem_cost` (firebase-scrypt).",
27823
+ "additionalProperties": {
27824
+ "anyOf": [
27825
+ {
27826
+ "type": "number"
27827
+ },
27828
+ {
27829
+ "type": "string",
27830
+ "maxLength": 512
27831
+ }
27832
+ ]
27833
+ }
27834
+ }
27835
+ },
27836
+ "additionalProperties": false
27837
+ }
27838
+ ],
27839
+ "description": "A password hash exported from another provider. Verified on the first login and upgraded to argon2id. Fast digests (MD5, SHA-1, SHA-2) and low costs are refused with `weak_password_hash`."
26899
27840
  }
26900
27841
  },
26901
27842
  "additionalProperties": false
@@ -26993,7 +27934,7 @@
26993
27934
  }
26994
27935
  },
26995
27936
  "400": {
26996
- "description": "`validation_error` — The body, query or path failed schema validation. `details.issues` lists each failing field.",
27937
+ "description": "`bad_request` — The request is malformed. `message` says what is wrong.\n\n`invalid_password_hash` — The imported password hash cannot be read, or its cost is above what a login can afford. `message` names the field.\n\n`weak_password_hash` — The imported password hash uses a fast digest (MD5, SHA-1, SHA-2) or a cost below the minimum. `message` says which.\n\n`validation_error` — The body, query or path failed schema validation. `details.issues` lists each failing field.",
26997
27938
  "content": {
26998
27939
  "application/json": {
26999
27940
  "schema": {
@@ -27007,6 +27948,9 @@
27007
27948
  "error_code": {
27008
27949
  "type": "string",
27009
27950
  "enum": [
27951
+ "bad_request",
27952
+ "invalid_password_hash",
27953
+ "weak_password_hash",
27010
27954
  "validation_error"
27011
27955
  ]
27012
27956
  }
@@ -52677,11 +53621,17 @@
52677
53621
  "password_reset"
52678
53622
  ]
52679
53623
  },
53624
+ {
53625
+ "type": "string",
53626
+ "enum": [
53627
+ "import"
53628
+ ]
53629
+ },
52680
53630
  {
52681
53631
  "type": "null"
52682
53632
  }
52683
53633
  ],
52684
- "description": "How `email_verified` was last set. `manual` — admin flipped the flag via `POST /user/:id`. `verification_flow` — user clicked the verification link. `passwordless_otp` — user completed passwordless OTP login. `team_invite` — user clicked a team invitation link. `email_change` — user confirmed a self-service email change. `federated` — verified by the external IdP on OAuth callback. `password_reset` — user completed the recovery link that was emailed to them (the `email` channel only). `null` — the field was cleared (admin un-verified the email)."
53634
+ "description": "How `email_verified` was last set. `manual` — admin flipped the flag via `POST /user/:id`. `verification_flow` — user clicked the verification link. `passwordless_otp` — user completed passwordless OTP login. `team_invite` — user clicked a team invitation link. `email_change` — user confirmed a self-service email change. `federated` — verified by the external IdP on OAuth callback. `password_reset` — user completed the recovery link that was emailed to them (the `email` channel only). `import` — the user arrived verified from another provider through `POST /user/import` (asserted by whoever ran the migration). `null` — the field was cleared (admin un-verified the email)."
52685
53635
  },
52686
53636
  "email_verified_at": {
52687
53637
  "type": "string",
@@ -52703,6 +53653,11 @@
52703
53653
  "description": "ISO 8601 timestamp of the built-in welcome email send. Set once by the platform; guarantees at most one welcome per user.",
52704
53654
  "nullable": true
52705
53655
  },
53656
+ "imported_at": {
53657
+ "type": "string",
53658
+ "description": "ISO 8601 timestamp of when the user was created by `POST /user/import`. Imported users get none of the built-in new-user emails (verification, welcome, `user.created` notifications); `user.created` webhooks still fire and carry this field.",
53659
+ "nullable": true
53660
+ },
52706
53661
  "passkey_prompted_at": {
52707
53662
  "type": "string",
52708
53663
  "description": "ISO 8601 timestamp of the last time the hosted \"create a passkey\" offer was shown to this user after a login. Drives the snooze in `login_methods.passkey_promotion_snooze_days`.",
@@ -52780,6 +53735,12 @@
52780
53735
  "password_reset"
52781
53736
  ]
52782
53737
  },
53738
+ {
53739
+ "type": "string",
53740
+ "enum": [
53741
+ "import"
53742
+ ]
53743
+ },
52783
53744
  {
52784
53745
  "type": "null"
52785
53746
  }
@@ -53190,11 +54151,17 @@
53190
54151
  "password_reset"
53191
54152
  ]
53192
54153
  },
54154
+ {
54155
+ "type": "string",
54156
+ "enum": [
54157
+ "import"
54158
+ ]
54159
+ },
53193
54160
  {
53194
54161
  "type": "null"
53195
54162
  }
53196
54163
  ],
53197
- "description": "How `email_verified` was last set. `manual` — admin flipped the flag via `POST /user/:id`. `verification_flow` — user clicked the verification link. `passwordless_otp` — user completed passwordless OTP login. `team_invite` — user clicked a team invitation link. `email_change` — user confirmed a self-service email change. `federated` — verified by the external IdP on OAuth callback. `password_reset` — user completed the recovery link that was emailed to them (the `email` channel only). `null` — the field was cleared (admin un-verified the email)."
54164
+ "description": "How `email_verified` was last set. `manual` — admin flipped the flag via `POST /user/:id`. `verification_flow` — user clicked the verification link. `passwordless_otp` — user completed passwordless OTP login. `team_invite` — user clicked a team invitation link. `email_change` — user confirmed a self-service email change. `federated` — verified by the external IdP on OAuth callback. `password_reset` — user completed the recovery link that was emailed to them (the `email` channel only). `import` — the user arrived verified from another provider through `POST /user/import` (asserted by whoever ran the migration). `null` — the field was cleared (admin un-verified the email)."
53198
54165
  },
53199
54166
  "email_verified_at": {
53200
54167
  "type": "string",
@@ -53216,6 +54183,11 @@
53216
54183
  "description": "ISO 8601 timestamp of the built-in welcome email send. Set once by the platform; guarantees at most one welcome per user.",
53217
54184
  "nullable": true
53218
54185
  },
54186
+ "imported_at": {
54187
+ "type": "string",
54188
+ "description": "ISO 8601 timestamp of when the user was created by `POST /user/import`. Imported users get none of the built-in new-user emails (verification, welcome, `user.created` notifications); `user.created` webhooks still fire and carry this field.",
54189
+ "nullable": true
54190
+ },
53219
54191
  "passkey_prompted_at": {
53220
54192
  "type": "string",
53221
54193
  "description": "ISO 8601 timestamp of the last time the hosted \"create a passkey\" offer was shown to this user after a login. Drives the snooze in `login_methods.passkey_promotion_snooze_days`.",
@@ -53293,6 +54265,12 @@
53293
54265
  "password_reset"
53294
54266
  ]
53295
54267
  },
54268
+ {
54269
+ "type": "string",
54270
+ "enum": [
54271
+ "import"
54272
+ ]
54273
+ },
53296
54274
  {
53297
54275
  "type": "null"
53298
54276
  }