openapi: 3.1.0 info: title: Passbox API version: 0.2.0 license: name: MIT identifier: MIT description: | HTTP API of the Passbox server. All vault data is encrypted on the client; the server only stores and relays ciphertext. Binary values are unpadded Base64url strings. Timestamps are Unix milliseconds. Errors have the shape `{ "error": "", "message": "...", "details"?: {...} }` with the codes `bad_request`, `unauthorized`, `invalid_credentials`, `not_found`, `conflict`, `rate_limited` and `internal`, and with status 403 `forbidden` (not an instance admin), `account_disabled`, `registration_closed`, `domain_not_allowed` and `invitation_invalid`. Two-factor errors: `reauthentication_required` (403), `invalid_two_factor_code` and `two_factor_expired` (401). Recovery errors: `recovery_needs_security_key` (401), `emergency_access_waiting` and `device_link_expired` (403). Account recovery, emergency access and device codes are described in docs/architecture/recovery.md. Single sign-on errors (403) start with `sso_`, for example `sso_required`, `sso_proof_invalid`, `sso_expired` or `sso_device_not_trusted`; SSO, trusted devices and SCIM are described in docs/architecture/sso.md (tags `sso` and `scim`). Two-factor authentication: when an account has a second factor (TOTP authenticator app or WebAuthn security key), `login/finish` answers with a `TwoFactorChallenge` instead of a session, unless the device token sent to `login/start` belongs to a device remembered within the last 30 days. `login/two-factor` completes the login. The challenge only appears after the password was verified, so unknown accounts stay indistinguishable. servers: - url: http://localhost:8080 security: - session: [] paths: /health: get: summary: Liveness check operationId: health security: [] responses: "200": description: The server is running. content: application/json: schema: type: object required: [status] properties: status: { type: string, const: ok } /api/v1/meta: get: summary: Public server information operationId: meta security: [] responses: "200": description: Server information. content: application/json: schema: { $ref: "#/components/schemas/ServerMeta" } /api/v1/auth/prelogin: post: summary: Key derivation parameters for an email description: Unknown emails receive stable fake values, so the response does not reveal whether an account exists. operationId: prelogin security: [] requestBody: required: true content: application/json: schema: type: object required: [email] properties: email: { type: string } responses: "200": description: Parameters. content: application/json: schema: type: object required: [accountId, kdfSalt, kdfParams] properties: accountId: { $ref: "#/components/schemas/Id" } kdfSalt: { $ref: "#/components/schemas/Base64Url" } kdfParams: { $ref: "#/components/schemas/KdfParams" } /api/v1/auth/register/start: post: summary: Start an OPAQUE registration operationId: registerStart security: [] requestBody: required: true content: application/json: schema: type: object required: [accountId, email, registrationRequest] properties: accountId: { $ref: "#/components/schemas/Id" } email: { type: string } registrationRequest: { $ref: "#/components/schemas/Base64Url" } inviteToken: type: string description: Token from an invitation link. Needed when the registration mode is not `open` (in `domains` mode for other domains). responses: "200": description: OPAQUE registration response. content: application/json: schema: type: object required: [registrationResponse] properties: registrationResponse: { $ref: "#/components/schemas/Base64Url" } "403": { $ref: "#/components/responses/RegistrationRefused" } "409": description: The email or account ID is taken. /api/v1/auth/register/finish: post: summary: Finish the registration and create the account with its first vault operationId: registerFinish security: [] requestBody: required: true content: application/json: schema: type: object required: [accountId, email, registrationUpload, kdfSalt, kdfParams, encryptedAccountKey, vault] properties: accountId: { $ref: "#/components/schemas/Id" } email: { type: string } registrationUpload: { $ref: "#/components/schemas/Base64Url" } kdfSalt: { $ref: "#/components/schemas/Base64Url" } kdfParams: { $ref: "#/components/schemas/KdfParams" } encryptedAccountKey: { $ref: "#/components/schemas/Base64Url" } vault: { $ref: "#/components/schemas/NewVault" } inviteToken: { type: string } locale: type: string enum: [en, de] description: Language of mails to the account; defaults to `Accept-Language`. responses: "201": description: Account created. content: application/json: schema: type: object properties: accountId: { $ref: "#/components/schemas/Id" } "403": { $ref: "#/components/responses/RegistrationRefused" } "409": description: The email or account ID is taken. /api/v1/auth/login/start: post: summary: Start an OPAQUE login operationId: loginStart security: [] requestBody: required: true content: application/json: schema: type: object required: [email, credentialRequest] properties: email: { type: string } credentialRequest: { $ref: "#/components/schemas/Base64Url" } deviceToken: type: string description: | The `deviceToken` from this device's last successful login. A valid token for this account exempts the login from the per-email lockout (it still counts against the per-IP limit and the token's own limit of 10 failures per 15 minutes). Invalid or foreign tokens are ignored. responses: "200": description: OPAQUE credential response. Finish within 5 minutes. content: application/json: schema: type: object required: [loginId, credentialResponse] properties: loginId: { $ref: "#/components/schemas/Id" } credentialResponse: { $ref: "#/components/schemas/Base64Url" } "429": { $ref: "#/components/responses/RateLimited" } /api/v1/auth/login/finish: post: summary: Finish the login and receive a session token operationId: loginFinish security: [] requestBody: required: true content: application/json: schema: type: object required: [loginId, credentialFinalization] properties: loginId: { $ref: "#/components/schemas/Id" } credentialFinalization: { $ref: "#/components/schemas/Base64Url" } deviceName: { type: string, description: 'Shown in the device list, for example "Chrome on macOS".' } clientKind: { type: string, enum: [web, extension, desktop, ios, android, cli] } origin: type: string description: | The web client's `location.origin`. Selects the security keys offered in `TwoFactorChallenge.webauthn`. ssoProof: type: string description: | Proof of an SSO sign-in (`/sso/redeem`). Required (403 `sso_required`) for members of organizations that require SSO, unless the device signed in with SSO recently; a valid proof also replaces the second factor. responses: "200": description: | A session, or a second-factor challenge when two-factor authentication is on and the device is not remembered. content: application/json: schema: oneOf: - $ref: "#/components/schemas/Session" - $ref: "#/components/schemas/TwoFactorChallenge" "401": description: Wrong credentials, unknown or expired login. "403": description: "`account_disabled`: the password was right, but an instance admin disabled the account." /api/v1/auth/login/two-factor: post: summary: Complete a login with its second factor description: | Wrong factors count against the account (10) and the client IP (100) per 15 minutes; a challenge allows 5 attempts. A TOTP code is accepted once (±1 time step of 30 seconds); a recovery code is deleted when used. operationId: loginTwoFactor security: [] requestBody: required: true content: application/json: schema: allOf: - $ref: "#/components/schemas/SecondFactor" - type: object required: [challengeToken] properties: challengeToken: { type: string } rememberDevice: type: boolean description: | Skip the second step on this device for 30 days. Bound to the device token issued with this session (it carries over when the token is rotated). responses: "200": description: Session created. content: application/json: schema: { $ref: "#/components/schemas/Session" } "400": { description: Unknown method. } "401": description: | `invalid_two_factor_code` (try again) or `two_factor_expired` (unknown, used or expired challenge; sign in again). "403": { description: "`account_disabled`" } "429": { $ref: "#/components/responses/RateLimited" } /api/v1/auth/logout: post: summary: End the current session operationId: logout responses: "204": { description: Session ended. } "401": { $ref: "#/components/responses/Unauthorized" } /api/v1/account: get: summary: The signed-in account operationId: account responses: "200": description: Account. content: application/json: schema: type: object required: [accountId, email, kdfSalt, kdfParams, encryptedAccountKey] properties: accountId: { $ref: "#/components/schemas/Id" } email: { type: string } kdfSalt: { $ref: "#/components/schemas/Base64Url" } kdfParams: { $ref: "#/components/schemas/KdfParams" } encryptedAccountKey: { $ref: "#/components/schemas/Base64Url" } instanceAdmin: { type: boolean } "401": { $ref: "#/components/responses/Unauthorized" } /api/v1/account/preferences: get: summary: Mail language and notification settings operationId: preferences responses: "200": description: Preferences. content: application/json: schema: { $ref: "#/components/schemas/AccountPreferences" } "401": { $ref: "#/components/responses/Unauthorized" } put: summary: Change preferences (omitted fields stay) operationId: updatePreferences requestBody: required: true content: application/json: schema: type: object properties: locale: { type: string, enum: [en, de, ""], description: '`""` resets to the instance default.' } notifyNewDevice: { type: boolean } responses: "200": description: The stored preferences. content: application/json: schema: { $ref: "#/components/schemas/AccountPreferences" } "400": { description: Unknown locale. } "401": { $ref: "#/components/responses/Unauthorized" } /api/v1/admin/settings: get: summary: Instance settings and mail status operationId: adminSettings tags: [admin] responses: "200": description: Settings. content: application/json: schema: { $ref: "#/components/schemas/AdminSettings" } "403": { $ref: "#/components/responses/Forbidden" } put: summary: Change the instance settings operationId: updateAdminSettings tags: [admin] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/InstanceSettings" } responses: "200": description: The stored settings. Changes are recorded in the admin log. content: application/json: schema: { $ref: "#/components/schemas/AdminSettings" } "400": { description: Invalid settings. } "403": { $ref: "#/components/responses/Forbidden" } /api/v1/admin/users: get: summary: Accounts on the server operationId: adminUsers tags: [admin] parameters: - { name: q, in: query, schema: { type: string }, description: Part of the email. } - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 50 } } - { name: offset, in: query, schema: { type: integer, minimum: 0, default: 0 } } responses: "200": description: Newest accounts first. content: application/json: schema: type: object required: [users, total] properties: users: { type: array, items: { $ref: "#/components/schemas/AdminUser" } } total: { type: integer } "403": { $ref: "#/components/responses/Forbidden" } /api/v1/admin/users/{id}/disable: post: summary: Disable an account description: Deletes all its sessions at once; logins then fail with `account_disabled`. Data stays. Admins cannot disable themselves. operationId: disableUser tags: [admin] parameters: - { name: id, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } responses: "204": { description: Disabled (or already disabled). } "400": { description: The admin's own account. } "403": { $ref: "#/components/responses/Forbidden" } "404": { description: Unknown account. } /api/v1/admin/users/{id}/enable: post: summary: Enable a disabled account operationId: enableUser tags: [admin] parameters: - { name: id, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } responses: "204": { description: Enabled. } "403": { $ref: "#/components/responses/Forbidden" } "404": { description: Unknown account. } /api/v1/admin/mail/test: post: summary: Queue a test mail operationId: sendTestMail tags: [admin] requestBody: required: true content: application/json: schema: type: object properties: to: { type: string, description: Defaults to the admin's own email. } responses: "200": description: Queued. Without SMTP it is written to the server log. content: application/json: schema: type: object required: [id, to, mailConfigured] properties: id: { type: string } to: { type: string } mailConfigured: { type: boolean } "403": { $ref: "#/components/responses/Forbidden" } /api/v1/admin/invites: get: summary: Invitations, newest first operationId: adminInvites tags: [admin] responses: "200": description: Invitations. content: application/json: schema: { type: array, items: { $ref: "#/components/schemas/Invite" } } "403": { $ref: "#/components/responses/Forbidden" } post: summary: Invite an email to create an account description: | Mails a link `/#/?invite=&email=` that is valid for 7 days and replaces earlier unused invitations for the email. The base is `PASSBOX_PUBLIC_URL`, else `linkBase`. operationId: createInvite tags: [admin] requestBody: required: true content: application/json: schema: type: object required: [email] properties: email: { type: string } locale: { type: string, enum: [en, de] } linkBase: { type: string, description: "Origin of the web app, for example `https://vault.example.com`." } responses: "201": description: Created. content: application/json: schema: allOf: - { $ref: "#/components/schemas/Invite" } - type: object required: [link, mailConfigured] properties: link: { type: [string, "null"] } mailConfigured: { type: boolean } "403": { $ref: "#/components/responses/Forbidden" } "409": { description: An account with this email exists. } /api/v1/admin/invites/{id}: delete: summary: Withdraw an unused invitation operationId: revokeInvite tags: [admin] parameters: - { name: id, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } responses: "204": { description: Withdrawn. } "403": { $ref: "#/components/responses/Forbidden" } "404": { description: Unknown or already used. } /api/v1/admin/events: get: summary: Admin audit log, newest first description: | Rows are hash-chained: `hash` is the SHA-256 (hex) of the JSON array `[prevHash, seq, id, actorId, actorEmail, action, target, details, ip, createdAt]` (with `details` as the stored JSON text), and `prevHash` is the previous row's hash (`""` for the first). `chain` reports the check of the whole log. operationId: adminEvents tags: [admin] parameters: - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 500, default: 50 } } - { name: before, in: query, schema: { type: integer }, description: Only events with a lower `seq`. } responses: "200": description: Events. content: application/json: schema: type: object required: [events, chain] properties: events: { type: array, items: { $ref: "#/components/schemas/AdminEvent" } } chain: type: object required: [valid, length, brokenAt] properties: valid: { type: boolean } length: { type: integer } brokenAt: { type: [integer, "null"] } "403": { $ref: "#/components/responses/Forbidden" } /api/v1/account/two-factor: get: summary: Two-factor authentication status operationId: twoFactorStatus responses: "200": description: Status. content: application/json: schema: type: object required: [enabled, totp, securityKeys, recoveryCodesRemaining] properties: enabled: { type: boolean } totp: { type: boolean } securityKeys: type: array items: type: object required: [id, name, createdAt, lastUsedAt] properties: id: { $ref: "#/components/schemas/Id" } name: { type: string } createdAt: { type: integer } lastUsedAt: { type: integer, description: 0 if never used. } recoveryCodesRemaining: { type: integer } "401": { $ref: "#/components/responses/Unauthorized" } /api/v1/account/two-factor/totp/start: post: summary: Create a TOTP secret for an authenticator app description: | Needs a session from a login in the last 10 minutes (`reauthentication_required` otherwise). The secret stays pending until `totp/confirm`; it is stored encrypted with the server's two-factor key. operationId: twoFactorTotpStart responses: "200": description: The secret (Base32) and an `otpauth://` URI for a QR code. content: application/json: schema: type: object required: [secret, uri] properties: secret: { type: string } uri: { type: string } "403": { description: "`reauthentication_required`" } "409": { description: An authenticator app is already set up. } /api/v1/account/two-factor/totp/confirm: post: summary: Turn the pending TOTP on with a first code operationId: twoFactorTotpConfirm requestBody: required: true content: application/json: schema: type: object required: [code] properties: code: { type: string } responses: "200": description: Enabled. `recoveryCodes` (shown once) when the account had none. content: application/json: schema: { $ref: "#/components/schemas/RecoveryCodes" } "400": { description: No pending secret. } "401": { description: "`invalid_two_factor_code`" } "403": { description: "`reauthentication_required`" } /api/v1/account/two-factor/totp: delete: summary: Remove the authenticator app description: Refused with 409 when it is the last second factor; use `disable`. operationId: twoFactorTotpRemove responses: "204": { description: Removed. } "403": { description: "`reauthentication_required`" } "404": { description: No authenticator app. } "409": { description: Last second factor. } /api/v1/account/two-factor/webauthn/register/start: post: summary: Start registering a security key or passkey description: | The RP ID is the host of `origin`, which must be https (or http on localhost). Needs a recent login. operationId: twoFactorWebauthnRegisterStart requestBody: required: true content: application/json: schema: type: object required: [origin] properties: origin: { type: string, description: The page's `location.origin`. } responses: "200": description: Options for `navigator.credentials.create` (binary values Base64url). content: application/json: schema: type: object required: [registrationId, publicKey] properties: registrationId: { type: string } publicKey: { type: object, description: PublicKeyCredentialCreationOptions. } "400": { description: Origin not usable for WebAuthn. } "403": { description: "`reauthentication_required`" } /api/v1/account/two-factor/webauthn/register/finish: post: summary: Store a verified security key description: | Checks type, challenge, origin, RP ID hash, user presence and the ES256 or Ed25519 public key. Attestation statements are not verified. operationId: twoFactorWebauthnRegisterFinish requestBody: required: true content: application/json: schema: type: object required: [registrationId, credential] properties: registrationId: { type: string } name: { type: string } credential: type: object required: [clientDataJson, attestationObject] properties: clientDataJson: { $ref: "#/components/schemas/Base64Url" } attestationObject: { $ref: "#/components/schemas/Base64Url" } responses: "201": description: Stored. `recoveryCodes` (shown once) when the account had none. content: application/json: schema: allOf: - $ref: "#/components/schemas/RecoveryCodes" - type: object required: [id] properties: id: { $ref: "#/components/schemas/Id" } "400": { description: Verification failed. } "401": { description: "`two_factor_expired`: unknown or used registration." } "409": { description: Key already registered. } /api/v1/account/two-factor/webauthn/{id}: delete: summary: Remove a security key description: Refused with 409 when it is the last second factor; use `disable`. operationId: twoFactorWebauthnRemove parameters: - { name: id, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } responses: "204": { description: Removed. } "403": { description: "`reauthentication_required`" } "404": { description: Unknown key. } "409": { description: Last second factor. } /api/v1/account/two-factor/recovery-codes: post: summary: Replace the recovery codes description: Needs a recent login and two-factor authentication on. operationId: twoFactorRecoveryCodes responses: "200": description: Ten new codes, shown once. The old ones stop working. content: application/json: schema: { $ref: "#/components/schemas/RecoveryCodes" } "403": { description: "`reauthentication_required`" } /api/v1/account/two-factor/challenge: post: summary: Start checking a second factor while signed in description: Needed to turn two-factor authentication off with a security key. operationId: twoFactorChallenge requestBody: content: application/json: schema: type: object properties: origin: { type: string } responses: "200": description: A challenge (same shape as at login, without `twoFactorRequired`). content: application/json: schema: { $ref: "#/components/schemas/TwoFactorChallenge" } /api/v1/account/two-factor/disable: post: summary: Turn two-factor authentication off description: | Needs a valid second factor or recovery code. Removes the authenticator app, all security keys and recovery codes, and forgets remembered devices. operationId: twoFactorDisable requestBody: required: true content: application/json: schema: allOf: - $ref: "#/components/schemas/SecondFactor" - type: object properties: challengeToken: { type: string, description: From `two-factor/challenge`; required for `webauthn`. } responses: "204": { description: Turned off. } "401": { description: "`invalid_two_factor_code`" } "429": { $ref: "#/components/responses/RateLimited" } /api/v1/sync: get: summary: Changes since a cursor operationId: sync parameters: - name: since in: query schema: { type: integer, minimum: 0, default: 0 } - name: sharing in: query schema: { type: integer, enum: [0, 1], default: 0 } description: | `1` includes shared vaults (rows with `orgId` and `keyEpoch`), their items and the `sharing` object. A shared vault the account gained access to after `since` is sent in full. Without it the response covers personal vaults only, as before. responses: "200": description: Vaults and items changed after `since`, and the new cursor. content: application/json: schema: type: object required: [cursor, vaults, items] properties: cursor: { type: integer } vaults: type: array items: { $ref: "#/components/schemas/SyncVault" } items: type: array items: { $ref: "#/components/schemas/SyncItem" } sharing: { $ref: "#/components/schemas/SharingDelta" } "401": { $ref: "#/components/responses/Unauthorized" } /api/v1/vaults: post: summary: Create a vault operationId: createVault requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/NewVault" } responses: "201": description: Created. content: application/json: schema: { $ref: "#/components/schemas/WriteResponse" } "409": { description: The vault ID exists. } /api/v1/vaults/{id}: put: summary: Update vault metadata operationId: updateVault parameters: - { name: id, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } requestBody: required: true content: application/json: schema: type: object required: [revision, encryptedMeta] properties: revision: { type: integer, description: Stored revision + 1 } encryptedMeta: { $ref: "#/components/schemas/Base64Url" } keyEpoch: { type: integer, default: 0, description: "Shared vaults (managers): the current key epoch" } responses: "200": description: Updated. content: application/json: schema: { $ref: "#/components/schemas/WriteResponse" } "404": { description: Unknown vault. } "409": { $ref: "#/components/responses/Conflict" } /api/v1/items/{id}: put: summary: Create (revision 1) or update an item operationId: putItem parameters: - { name: id, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } requestBody: required: true content: application/json: schema: type: object required: [vaultId, revision, encryptedData] properties: vaultId: { $ref: "#/components/schemas/Id" } revision: { type: integer, description: 1 for new items, otherwise stored revision + 1 } encryptedData: { $ref: "#/components/schemas/Base64Url" } keyEpoch: { type: integer, default: 0, description: "Vault key epoch of the data; must be the vault's current epoch", } responses: "200": description: Saved. content: application/json: schema: { $ref: "#/components/schemas/WriteResponse" } "403": { description: "`forbidden`: view-only member of a shared vault." } "404": { description: Unknown vault or item. } "409": { description: "Revision conflict, or `details.currentKeyEpoch` when the vault key was rotated." } delete: summary: Delete an item (keeps a tombstone) operationId: deleteItem parameters: - { name: id, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } - { name: revision, in: query, required: true, schema: { type: integer }, description: Stored revision + 1 } responses: "200": description: Deleted. content: application/json: schema: { $ref: "#/components/schemas/WriteResponse" } "404": { description: Unknown item. } "409": { $ref: "#/components/responses/Conflict" } /api/v1/items/{id}/versions: get: summary: Earlier versions of an item description: | Every update keeps the replaced, still encrypted version (newest 20 per item, including re-encryptions after a key rotation). Each version is bound to its own revision and key epoch, so clients decrypt it with the item AAD of that revision. Versions whose key epoch the client does not have are shown as undecryptable. A permanent deletion removes the history. Readable by everyone who can read the item, including view-only members of a shared vault. Restoring a version is a normal `PUT /api/v1/items/{id}` with its data. operationId: itemVersions parameters: - { name: id, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } responses: "200": description: Versions, newest first. content: application/json: schema: type: object required: [versions] properties: versions: { type: array, items: { $ref: "#/components/schemas/ItemVersion" } } "404": { description: Unknown or deleted item, or no access. } /api/v1/items/move: post: summary: Move items into another vault description: | The client re-encrypts each item and the versions of its history it can read for the target vault (bound to the target vault ID, the revision and the target's current key epoch). The item keeps its ID, creation date and attachments; its history is replaced by `versions`. All items or none; at most 100. Needs write access to the source and the target vault. Devices that can see the old vault receive a tombstone (`deleted: true` with the old `vaultId`) in sync. See docs/architecture/sharing.md. operationId: moveItems requestBody: required: true content: application/json: schema: type: object required: [targetVaultId, items] properties: targetVaultId: { $ref: "#/components/schemas/Id" } keyEpoch: { type: integer, default: 0 } items: type: array maxItems: 100 items: type: object required: [id, revision, encryptedData] properties: id: { $ref: "#/components/schemas/Id" } revision: { type: integer, description: Stored revision + 1 } encryptedData: { $ref: "#/components/schemas/Base64Url" } versions: type: array items: type: object required: [revision, encryptedData] properties: revision: { type: integer } encryptedData: { $ref: "#/components/schemas/Base64Url" } responses: "200": description: Moved. content: application/json: schema: type: object properties: moved: type: array items: type: object properties: { id: { type: string }, revision: { type: integer }, seq: { type: integer } } "400": { description: Invalid request, or an item is already in the target vault. } "403": { description: "`forbidden` (view-only), `policy_violation` (organization keeps its items) or `quota_exceeded` (attachments).", } "404": { description: Unknown vault or item. } "409": { description: "`details.itemId` and `details.currentRevision`, or `details.currentKeyEpoch`." } /api/v1/attachments/{id}: put: summary: Upload an encrypted attachment blob description: | The raw body is the blob from `passbox_core::file_crypto` (the file key, name and type are in the encrypted item). IDs come from the client; a blob cannot be replaced. See docs/architecture/attachments.md. operationId: uploadAttachment parameters: - { name: id, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } - { name: itemId, in: query, required: true, schema: { $ref: "#/components/schemas/Id" } } requestBody: required: true content: application/octet-stream: schema: { type: string, format: binary } responses: "201": { description: "Stored: `{ id, size }`." } "400": { description: Not an encrypted file, or 50 attachments already. } "403": { description: "`forbidden`, `file_too_large` or `quota_exceeded`." } "404": { description: Unknown item. } "409": { description: An attachment with this ID exists. } get: summary: Download an encrypted attachment blob operationId: downloadAttachment parameters: - { name: id, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } responses: "200": description: The blob. content: application/octet-stream: schema: { type: string, format: binary } "404": { description: Unknown attachment or no access. } delete: summary: Delete an attachment operationId: deleteAttachment parameters: - { name: id, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } responses: "204": { description: Deleted. } "403": { description: View-only. } "404": { description: Unknown attachment. } /api/v1/attachments/usage: get: summary: Attachment limits and storage used operationId: attachmentUsage responses: "200": description: '`{ maxFileSize, usage: [{ owner: "account" | orgId, used, quota (0 = unlimited) }] }`' /api/v1/share-links: get: summary: The account's share links that have not expired operationId: shareLinks responses: "200": { description: "`{ links: [{ id, encryptedLabel, vaultId, expiresAt, maxViews, views, hasPassphrase, createdAt, lastViewedAt }] }`", } post: summary: Create a share link description: | The snapshot is encrypted on the device; the key is only in the link's fragment. `authHash` is the hash of the access token (`passbox_core::share_link`). Expires within 365 days; `maxViews` 0 = unlimited (at most 1000). With `vaultId`, the policy of the vault's organization applies (403 `policy_violation`). operationId: createShareLink requestBody: required: true content: application/json: schema: type: object required: [id, encryptedLabel, blob, authHash, expiresAt] properties: id: { $ref: "#/components/schemas/Id" } encryptedLabel: { $ref: "#/components/schemas/Base64Url" } blob: { $ref: "#/components/schemas/Base64Url" } authHash: { $ref: "#/components/schemas/Base64Url" } salt: { $ref: "#/components/schemas/Base64Url" } expiresAt: { type: integer } maxViews: { type: integer, default: 0 } vaultId: { $ref: "#/components/schemas/Id" } responses: "201": { description: Created. } "403": { description: "`policy_violation`." } /api/v1/share-links/{id}: delete: summary: End a share link operationId: deleteShareLink parameters: - { name: id, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } responses: "204": { description: Ended. } "404": { description: Unknown link. } /api/v1/public/share-links/{id}: get: summary: What the viewer needs before opening (no session) operationId: publicShareLink security: [] parameters: - { name: id, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } responses: "200": { description: "`{ salt (null without passphrase), expiresAt, viewsLeft (null = unlimited) }`" } "404": { description: Unknown, expired or used up. } /api/v1/public/share-links/{id}/open: post: summary: The encrypted snapshot, for the access token (no session; counts a view) operationId: openShareLink security: [] parameters: - { name: id, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } requestBody: required: true content: application/json: schema: { type: object, required: [auth], properties: { auth: { $ref: "#/components/schemas/Base64Url" } } } responses: "200": { description: "`{ blob, expiresAt, viewsLeft }`; the last allowed view deletes the link." } "403": { description: "`wrong_passphrase`; after 10 the link is deleted." } "404": { description: Unknown, expired or used up. } /api/v1/vaults/{id}/access-request: get: summary: Whether this account can open a vault, and its own request operationId: vaultAccessRequest parameters: - { name: id, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } responses: "200": { description: "`{ orgId, hasAccess, request }`" } "404": { description: Unknown vault or not a confirmed member of its organization. } /api/v1/vaults/{id}/access-requests: post: summary: Ask the vault's managers for access operationId: requestVaultAccess parameters: - { name: id, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } requestBody: required: true content: application/json: schema: { type: object, required: [role], properties: { role: { type: string, enum: [view, edit, manage] } } } responses: "201": { description: The request. } "400": { description: Already has this access. } "404": { description: Unknown vault or not a confirmed member of its organization. } "409": { description: A request is pending. } "429": { description: More than 20 requests a day. } /api/v1/access-requests: get: summary: Pending requests for vaults this account manages, and its own operationId: accessRequests responses: "200": { description: "`{ incoming: [...], outgoing: [...] }` with `{ id, vaultId, orgId, accountId, email, role, status, createdAt, decidedAt }`", } /api/v1/access-requests/{id}: delete: summary: Withdraw an own pending request operationId: cancelAccessRequest parameters: - { name: id, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } responses: "204": { description: Withdrawn. } "404": { description: Unknown or not pending. } /api/v1/access-requests/{id}/approve: post: summary: Mark a request approved (after sharing the vault with the requester) operationId: approveAccessRequest parameters: - { name: id, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } responses: "204": { description: Approved. } "404": { description: Unknown request or not a manager. } "409": { description: Already decided, or the requester has no access yet. } /api/v1/access-requests/{id}/deny: post: summary: Decline a request operationId: denyAccessRequest parameters: - { name: id, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } responses: "204": { description: Declined. } "404": { description: Unknown request or not a manager. } "409": { description: Already decided. } /api/v1/icons/{domain}: get: summary: A website's icon, fetched by the server description: | Lets clients show favicons without contacting the website themselves. The server tries `https://{domain}/apple-touch-icon.png`, `/favicon.ico` and the `` of the homepage, over HTTPS on port 443 only, refusing IP literals and every non-public address (also after DNS resolution and on redirects; at most 3 redirects, 3 s per request, 200 KB). Only PNG, ICO, JPEG, GIF and WebP images whose bytes match are returned. Results are stored once for all users of the instance (refreshed after 30 days, misses retried after 7 days) and never linked to the account that asked. 600 requests per session and minute. Turned off with `PASSBOX_ICONS=off` (then always 404; see `ServerMeta.icons`). The server learns which domains users look at; clients bundle the icons of popular sites (`@passbox/site-icons`) and do not ask for those. operationId: icon parameters: - name: domain in: path required: true description: Host name, for example `github.com` (Unicode names are converted to punycode). schema: { type: string } responses: "200": description: "The icon, with `Cache-Control: private, max-age=604800`." content: image/png: {} image/x-icon: {} image/jpeg: {} image/gif: {} image/webp: {} "400": { description: "Not a valid public domain name: an IP literal, a port, a single label, ..." } "401": { $ref: "#/components/responses/Unauthorized" } "404": { description: "No icon, or icons are turned off; `Cache-Control: private, max-age=86400`." } "429": { description: "`rate_limited`." } /api/v1/breach/range/{prefix}: get: summary: Pwned Passwords range through the server (k-anonymity) description: | Returns Have I Been Pwned's padded range response for the first 5 hex characters of a password's SHA-1 hash, fetched by the server, so Have I Been Pwned sees the server's IP address instead of the user's. The server learns only hash prefixes (each matches hundreds of passwords); matching happens on the device. Cached in memory for 24 hours; 3000 requests per account and hour. Turned off with `PASSBOX_BREACH_PROXY=off` (then 404; see `ServerMeta.breachProxy`). operationId: breachRange parameters: - name: prefix in: path required: true description: 5 hex characters (case-insensitive). schema: { type: string, pattern: "^[0-9A-Fa-f]{5}$" } responses: "200": description: "`SUFFIX:COUNT` lines, padded with count-0 entries; `Cache-Control: private, no-store`." content: text/plain: {} "400": { description: Not a 5-character hex prefix. } "401": { $ref: "#/components/responses/Unauthorized" } "403": { description: "`breach_unavailable`: Have I Been Pwned could not be reached from the server." } "404": { description: The proxy is turned off. } "429": { description: "`rate_limited`." } /api/v1/breach/emails: get: summary: Email breach monitoring of the account description: | Addresses the account monitors and the breaches Have I Been Pwned reported for them. `available` is false when the operator has not set `PASSBOX_HIBP_API_KEY`. operationId: breachEmails responses: "200": description: The monitored addresses. content: application/json: schema: { $ref: "#/components/schemas/EmailMonitoring" } "401": { $ref: "#/components/responses/Unauthorized" } post: summary: Monitor an email address (sends a confirmation code) description: | Adds an address (lower-cased) or, for an unconfirmed one, sends a new code. The code (6 digits, valid 30 minutes, 5 attempts) is mailed to the address itself. Nothing is sent to Have I Been Pwned before the address is confirmed. Up to 10 addresses per account; one code per address and minute, 10 per account and hour. operationId: addBreachEmail requestBody: required: true content: application/json: schema: type: object required: [email] properties: email: { type: string } locale: { type: string, enum: [en, de], description: Language of the confirmation mail. } responses: "200": { description: Already confirmed; the entry. } "201": description: Code sent. content: application/json: schema: { $ref: "#/components/schemas/MonitoredEmail" } "400": { description: Not a valid address, or 10 addresses already. } "403": { description: "`monitoring_unavailable`: no API key configured." } "429": { description: "`rate_limited`." } /api/v1/breach/emails/{id}: delete: summary: Stop monitoring an address description: Deletes the address and its results. operationId: deleteBreachEmail parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: "204": { description: Deleted. } "404": { description: Unknown address or another account's. } /api/v1/breach/emails/{id}/verify: post: summary: Confirm an address with the mailed code description: Confirms the address and runs the first check right away (its results are shown, not mailed). operationId: verifyBreachEmail parameters: - { name: id, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: type: object required: [code] properties: code: { type: string } responses: "200": description: The confirmed address with its breaches. content: application/json: schema: { $ref: "#/components/schemas/MonitoredEmail" } "401": { description: "`invalid_two_factor_code`: wrong code." } "403": { description: "`code_expired`: expired or too many attempts; request a new code." } /api/v1/breach/emails/{id}/check: post: summary: Check a confirmed address now description: At most every ten minutes per address. The server also checks every `PASSBOX_HIBP_CHECK_HOURS` and mails new breaches to the account. operationId: checkBreachEmail parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: "200": description: The address with its breaches. content: application/json: schema: { $ref: "#/components/schemas/MonitoredEmail" } "429": { description: "`rate_limited`." } /api/v1/breaches/catalog: get: summary: The breach catalogue (breached websites) description: >- Have I Been Pwned's public list of breached websites, refreshed by the server every `PASSBOX_BREACH_CATALOG_HOURS`, with only the fields clients need. Entries without a domain, fabricated entries, spam lists and retired breaches are left out; sensitive breaches are flagged. The same document for everyone and no session needed: clients match it against their own logins on the device, so the server never learns which sites a user has. Before the first successful fetch the server serves the snapshot bundled with it. Send `If-None-Match` with the `ETag` for a `304`; `Accept-Encoding: gzip` gets a compressed body. Data: Have I Been Pwned, CC BY 4.0 (name the source wherever it is shown). operationId: breachCatalog security: [] parameters: - { name: If-None-Match, in: header, required: false, schema: { type: string } } responses: "200": description: "The catalogue. Headers `ETag` (the quoted version) and `Cache-Control: public, no-cache`." content: application/json: schema: { $ref: "#/components/schemas/BreachCatalog" } "304": { description: Unchanged since the given `ETag`. } "404": { description: "`PASSBOX_BREACH_CATALOG=off`: clients use their bundled snapshot." } /api/v1/breaches/catalog/version: get: summary: Version of the breach catalogue description: For clients that check often (at unlock, every few hours); download the catalogue only when the version changed. operationId: breachCatalogVersion security: [] responses: "200": description: Version and date. content: application/json: schema: { $ref: "#/components/schemas/BreachCatalogVersion" } "404": { description: "`PASSBOX_BREACH_CATALOG=off`." } /api/v1/account/keys: get: summary: The account's own key pairs description: Public bundle and private keys encrypted with the account key. 404 until published. operationId: ownKeys responses: "200": description: The key pairs. content: application/json: schema: { $ref: "#/components/schemas/AccountKeys" } "404": { description: No key pairs published yet; the client creates them. } put: summary: Publish the account's key pairs description: | Appends the bundle to the key log. The bundle must be self-signed and belong to the session's account. Publishing the same bundle again is accepted; a different one is a conflict (key rotation is not supported yet). Records `keys_published`. operationId: publishKeys requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/AccountKeys" } responses: "204": { description: Published. } "400": { description: Invalid bundle or signature. } "409": { description: Other key pairs are already published. } /api/v1/keys/head: get: summary: Signed head of the key log operationId: keyLogHead responses: "200": description: The current head, signed with the server's log key. content: application/json: schema: { $ref: "#/components/schemas/SignedHead" } /api/v1/keys/log: get: summary: Entries of the key log operationId: keyLog parameters: - { name: from, in: query, schema: { type: integer, minimum: 1, default: 1 } } - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 1000, default: 1000 } } responses: "200": description: Entries in order, with the head they lead to. content: application/json: schema: type: object required: [entries, head] properties: entries: { type: array, items: { $ref: "#/components/schemas/KeyLogEntry" } } head: { $ref: "#/components/schemas/SignedHead" } /api/v1/keys/{accountId}: get: summary: Look up an account's current key with a proof description: | Returns the bundle and the log entries from its entry (or from `since + 1`, whichever is lower; from the entry itself when `since` is 0) to the signed head, so the client can verify inclusion, that no newer key exists and that the log extends the head it verified before. operationId: lookupKey parameters: - { name: accountId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } - { name: since, in: query, schema: { type: integer, minimum: 0, default: 0 }, description: Size of the last head the client verified, } responses: "200": description: Bundle and proof. content: application/json: schema: { $ref: "#/components/schemas/KeyLookup" } "404": { description: The account has no published keys. } /api/v1/orgs: get: summary: The account's organizations operationId: listOrgs responses: "200": description: Organizations with the account's own role and status. content: application/json: schema: { type: array, items: { $ref: "#/components/schemas/OrgRecord" } } post: summary: Create an organization description: | The caller becomes owner. Body built by `Keyring.createOrg`: genesis signed with the caller's key, encrypted metadata, the organization key sealed for the caller and, for `family` (required) or optionally `team`, the first shared vault. Records `org_created`. operationId: createOrg requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/NewOrg" } responses: "201": description: Created. content: application/json: schema: type: object properties: id: { $ref: "#/components/schemas/Id" } seq: { type: integer } defaultVaultId: { type: [string, "null"] } "400": { description: Invalid or forged objects, or a family without its vault. } "429": { $ref: "#/components/responses/RateLimited" } /api/v1/orgs/{orgId}: parameters: - { name: orgId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } get: summary: An organization with its members description: Members only (404 otherwise). Guests and unconfirmed members see themselves and the admins. operationId: getOrg responses: "200": description: Organization and roster. content: application/json: schema: allOf: - { $ref: "#/components/schemas/OrgRecord" } - type: object properties: members: { type: array, items: { $ref: "#/components/schemas/RosterMember" } } "404": { description: Not a member. } put: summary: Update encrypted metadata (admins) operationId: updateOrg requestBody: required: true content: application/json: schema: type: object required: [revision, metaEpoch, encryptedMeta] properties: revision: { type: integer } metaEpoch: { type: integer } encryptedMeta: { $ref: "#/components/schemas/Base64Url" } responses: "204": { description: Updated. } "403": { $ref: "#/components/responses/SharingForbidden" } "409": { $ref: "#/components/responses/Conflict" } delete: summary: Delete the organization and its vaults (owner) operationId: deleteOrg responses: "204": { description: Deleted. } "403": { $ref: "#/components/responses/SharingForbidden" } /api/v1/orgs/{orgId}/rotate-key: post: summary: Rotate the organization key (admins) description: New epoch sealed for every confirmed member; organization and group metadata re-encrypted. operationId: rotateOrgKey parameters: - { name: orgId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/SharingRequest" } responses: "204": { description: Rotated. } "400": { description: Not sealed for every confirmed member, or metadata missing. } "409": { description: Rotated meanwhile. } /api/v1/orgs/{orgId}/invitations: parameters: - { name: orgId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } get: summary: Pending invitations (admins) operationId: listInvitations responses: "200": description: Invitations without tokens (only hashes are stored). content: application/json: schema: { type: array, items: { $ref: "#/components/schemas/InvitationInfo" } } post: summary: Invite by email (admins; admins only by the owner) description: | Stores the SHA-256 hash of a new 256-bit token (valid 7 days), queues an `invitation` mail through the mailer (link `{publicUrl}{path}#{linkFragment}`) and returns the token once. Rate limited per IP and account (50 per hour). operationId: invite requestBody: required: true content: application/json: schema: type: object required: [email, role] properties: email: { type: string } role: { type: string, enum: [admin, member, guest] } linkFragment: { type: string, description: "`org=` for the mailed link" } responses: "201": description: Created. content: application/json: schema: type: object required: [id, token, path, expiresAt] properties: id: { $ref: "#/components/schemas/Id" } token: { type: string } path: { type: string, description: "`/invite/{token}`" } expiresAt: { type: integer } "403": { $ref: "#/components/responses/SharingForbidden" } "409": { description: Already a member. } "429": { $ref: "#/components/responses/RateLimited" } /api/v1/orgs/{orgId}/invitations/{invitationId}: delete: summary: Revoke a pending invitation (admins) operationId: revokeInvitation parameters: - { name: orgId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } - { name: invitationId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } responses: "204": { description: Revoked. } "404": { description: Unknown or not pending. } /api/v1/invitations/{token}: get: summary: What an invitation is for (no session needed) description: Unknown, used, revoked and expired tokens answer 404. Rate limited per IP. operationId: previewInvitation security: [] parameters: - { name: token, in: path, required: true, schema: { type: string } } responses: "200": description: Preview. content: application/json: schema: type: object properties: orgId: { $ref: "#/components/schemas/Id" } orgKind: { type: string, enum: [family, team] } email: { type: string } role: { type: string } expiresAt: { type: integer } inviterEmail: { type: string, description: Email of the inviting admin. } recovery: type: boolean description: The organization's admins will be able to restore the invitee's access (recovery set up and required by the policy). Never true for guests. Apps show this before accepting. "404": { description: Invalid invitation. } "429": { $ref: "#/components/responses/RateLimited" } /api/v1/invitations/accept: post: summary: Accept an invitation description: | The account must have the invited email and published keys. The membership is `accepted` until an admin confirms it. A pending invitation also allows registering on invite-only servers (`inviteToken` at registration). operationId: acceptInvitation requestBody: required: true content: application/json: schema: { type: object, required: [token], properties: { token: { type: string } } } responses: "200": description: Accepted. content: application/json: schema: { type: object, properties: { orgId: { type: string }, role: { type: string } } } "403": { description: The invitation is for another email address. } "404": { description: Invalid invitation. } "409": { description: Already a member. } "429": { $ref: "#/components/responses/RateLimited" } /api/v1/orgs/{orgId}/members/{accountId}: parameters: - { name: orgId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } - { name: accountId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } put: summary: Change a member's role or re-attest (admins; admin rights by the owner) operationId: updateMember requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/SharingRequest" } responses: "204": { description: Updated. } "403": { $ref: "#/components/responses/SharingForbidden" } delete: summary: Remove a member (admins) or leave description: | Access ends immediately: memberships of the organization's vaults and groups and sealed keys are deleted; affected vaults get `rotationPending`. Returns the vaults and groups whose keys a manager must rotate. operationId: removeMember responses: "200": description: Removed. content: application/json: schema: type: object properties: vaults: { type: array, items: { type: string } } groups: { type: array, items: { type: string } } "403": { $ref: "#/components/responses/SharingForbidden" } /api/v1/orgs/{orgId}/members/{accountId}/confirm: post: summary: Confirm an accepted member (admins) description: Stores the admin's signed attestation of the member's key and the organization key sealed for them. operationId: confirmMember parameters: - { name: orgId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } - { name: accountId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/SharingRequest" } responses: "204": { description: Confirmed. } "400": { description: Forged or mismatching attestation or key. } "403": { $ref: "#/components/responses/SharingForbidden" } "409": { description: Not waiting for confirmation. } /api/v1/orgs/{orgId}/members/{accountId}/suspend: post: summary: Suspend a member (admins; not cryptographic) operationId: suspendMember parameters: - { name: orgId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } - { name: accountId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } responses: "204": { description: Suspended. } /api/v1/orgs/{orgId}/members/{accountId}/reactivate: post: summary: Reactivate a suspended member (admins) operationId: reactivateMember parameters: - { name: orgId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } - { name: accountId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } responses: "204": { description: Reactivated. } /api/v1/orgs/{orgId}/groups: parameters: - { name: orgId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } get: summary: Groups of an organization (confirmed members) operationId: listGroups responses: "200": description: Groups; member grants only for groups the caller belongs to. content: application/json: schema: { type: array, items: { $ref: "#/components/schemas/SharingRequest" } } post: summary: Create a group (admins; the creator is a member) operationId: createGroup requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/SharingRequest" } responses: "201": { description: Created. } "403": { $ref: "#/components/responses/SharingForbidden" } /api/v1/orgs/{orgId}/groups/{groupId}: parameters: - { name: orgId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } - { name: groupId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } put: summary: Rename a group (admins) operationId: updateGroup requestBody: required: true content: application/json: schema: type: object required: [metaEpoch, encryptedMeta] properties: metaEpoch: { type: integer } encryptedMeta: { $ref: "#/components/schemas/Base64Url" } responses: "204": { description: Renamed. } delete: summary: Delete a group (admins) operationId: deleteGroup responses: "204": { description: Deleted. } "409": { description: Vaults are still shared with the group. } /api/v1/orgs/{orgId}/groups/{groupId}/members: post: summary: Add a group member (one sealing of the group key) operationId: addGroupMember parameters: - { name: orgId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } - { name: groupId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/SharingRequest" } responses: "200": { description: Added. } /api/v1/orgs/{orgId}/groups/{groupId}/rotate: post: summary: Rotate a group's key, removing members (admins) description: Returns the vaults shared with the group, now marked for rotation. operationId: rotateGroup parameters: - { name: orgId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } - { name: groupId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/SharingRequest" } responses: "200": { description: Rotated. } /api/v1/orgs/{orgId}/vaults: post: summary: Create a shared vault (owners and admins) operationId: createSharedVault parameters: - { name: orgId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/SharingRequest" } responses: "201": { description: Created. } "403": { $ref: "#/components/responses/SharingForbidden" } /api/v1/vaults/{id}/members: parameters: - { name: id, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } get: summary: Members and manifest chain of a shared vault (members) operationId: vaultMembers responses: "200": { description: Members, accounts with access and the manifest chain. } "404": { description: No access. } post: summary: Share with an account or group, or change a role (managers) description: | Body built by `Keyring.shareVault`: the next manifest version, signed by the caller and equal to the current members plus the change, and the vault key of the current epoch sealed for the principal with a signed grant. operationId: putVaultMember requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/SharingRequest" } responses: "200": { description: Shared. } "400": { description: Manifest or grant invalid or not matching. } "403": { $ref: "#/components/responses/SharingForbidden" } "409": { description: The members changed meanwhile. } /api/v1/vaults/{id}/rotate: post: summary: Rotate the vault key, removing members (managers) description: | New key epoch sealed for every remaining member, a new signed manifest and the metadata re-encrypted. The only way to remove a member. Writes with older epochs are refused afterwards; re-encrypt items with `items/batch`. operationId: rotateVault parameters: - { name: id, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/SharingRequest" } responses: "200": { description: Rotated; `keyEpoch` is the new epoch. } "403": { $ref: "#/components/responses/SharingForbidden" } "409": { description: Rotated meanwhile (`details.currentKeyEpoch`). } /api/v1/vaults/{id}/keys/{epoch}/wrapped: put: summary: Store the caller's own wrapped copy of a vault key operationId: putWrappedKey parameters: - { name: id, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } - { name: epoch, in: path, required: true, schema: { type: integer } } requestBody: required: true content: application/json: schema: { type: object, required: [wrappedKey], properties: { wrappedKey: { $ref: "#/components/schemas/Base64Url" } }, } responses: "204": { description: Stored. } "404": { description: No key for the caller in that epoch. } /api/v1/vaults/{id}/items/batch: post: summary: Write up to 100 items of a vault (re-encryption after rotation) operationId: reencryptItems parameters: - { name: id, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } requestBody: required: true content: application/json: schema: type: object required: [items] properties: items: type: array maxItems: 100 items: type: object required: [id, revision, keyEpoch, encryptedData] properties: id: { $ref: "#/components/schemas/Id" } revision: { type: integer } keyEpoch: { type: integer } encryptedData: { $ref: "#/components/schemas/Base64Url" } responses: "200": { description: "Per item: `ok` or `conflict` (changed meanwhile)." } "403": { $ref: "#/components/responses/SharingForbidden" } "409": { description: Not the current key epoch. } /api/v1/account/recovery: get: summary: Recovery status of the account (recovery code, organization escrows) operationId: recoveryStatus tags: [recovery] responses: "200": description: Status. content: application/json: schema: type: object required: [recoveryCode, orgEscrows] properties: recoveryCode: type: [object, "null"] properties: { createdAt: { type: integer } } orgEscrows: type: array items: type: object properties: orgId: { $ref: "#/components/schemas/Id" } keyVersion: { type: integer } createdAt: { type: integer } /api/v1/account/recovery-code/start: post: summary: Start registering the recovery code's OPAQUE record (login in the last 10 minutes) operationId: recoveryCodeStart tags: [recovery] requestBody: required: true content: application/json: schema: type: object required: [registrationRequest] properties: { registrationRequest: { $ref: "#/components/schemas/Base64Url" } } responses: "200": description: OPAQUE registration response. content: application/json: schema: type: object properties: { registrationResponse: { $ref: "#/components/schemas/Base64Url" } } "403": { description: "`reauthentication_required`." } /api/v1/account/recovery-code: post: summary: Store the recovery code record and the account key wrapped with it (replaces an older code) operationId: recoveryCodeFinish tags: [recovery] requestBody: required: true content: application/json: schema: type: object required: [registrationUpload, wrappedAccountKey] properties: registrationUpload: { $ref: "#/components/schemas/Base64Url" } wrappedAccountKey: { type: string, description: "Envelope under HKDF(R, passbox/v1/recovery-code)." } responses: "204": { description: Stored; the account gets a `recovery_code_created` mail. } "403": { description: "`reauthentication_required`." } delete: summary: Remove the recovery code (login in the last 10 minutes) operationId: recoveryCodeDelete tags: [recovery] responses: "204": { description: Removed. } /api/v1/recovery/code/start: post: summary: Start an OPAQUE login with the recovery code description: Unknown emails and accounts without a code run against a fake record. operationId: recoveryLoginStart tags: [recovery] security: [] requestBody: required: true content: application/json: schema: type: object required: [email, credentialRequest] properties: email: { type: string } credentialRequest: { $ref: "#/components/schemas/Base64Url" } responses: "200": description: OPAQUE credential response. content: application/json: schema: type: object properties: loginId: { type: string } credentialResponse: { $ref: "#/components/schemas/Base64Url" } "429": { $ref: "#/components/responses/RateLimited" } /api/v1/recovery/code/finish: post: summary: Finish the recovery-code login operationId: recoveryLoginFinish tags: [recovery] security: [] requestBody: required: true content: application/json: schema: type: object required: [loginId, credentialFinalization] properties: loginId: { type: string } credentialFinalization: { $ref: "#/components/schemas/Base64Url" } deviceName: { type: string } clientKind: { type: string } responses: "200": description: A recovery grant (with `twoFactorMethods` instead of the wrapped key when the account uses two-factor sign-in). content: application/json: schema: { $ref: "#/components/schemas/RecoveryGrant" } "401": { description: "`invalid_credentials`, or `recovery_needs_security_key`." } "429": { $ref: "#/components/responses/RateLimited" } /api/v1/recovery/two-factor: post: summary: Second factor of a recovery (TOTP or two-factor recovery code) operationId: recoveryTwoFactor tags: [recovery] security: [] requestBody: required: true content: application/json: schema: type: object required: [recoveryToken, method, code] properties: recoveryToken: { type: string } method: { type: string, enum: [totp, recovery] } code: { type: string } responses: "200": description: The recovery grant with the wrapped account key. content: application/json: schema: { $ref: "#/components/schemas/RecoveryGrant" } "401": { description: "`invalid_two_factor_code` or `two_factor_expired`." } "429": { $ref: "#/components/responses/RateLimited" } /api/v1/recovery/complete/start: post: summary: Start registering new credentials after a recovery operationId: recoveryCompleteStart tags: [recovery] security: [] requestBody: required: true content: application/json: schema: type: object required: [recoveryToken, registrationRequest] properties: recoveryToken: { type: string } registrationRequest: { $ref: "#/components/schemas/Base64Url" } recoveryRegistrationRequest: { $ref: "#/components/schemas/Base64Url" } responses: "200": description: OPAQUE registration responses (login and, if requested, a new recovery code). content: application/json: schema: type: object properties: registrationResponse: { $ref: "#/components/schemas/Base64Url" } recoveryRegistrationResponse: { $ref: "#/components/schemas/Base64Url" } /api/v1/recovery/complete/finish: post: summary: Store new credentials, revoke every session and sign in the recovering device description: | Sets the new OPAQUE record, KDF salt and wrapped account key (new master password and Secret Key) and optionally a new recovery code. Deletes all sessions, device tokens, device links and recovery sessions of the account; the recovery token is single use. operationId: recoveryCompleteFinish tags: [recovery] security: [] requestBody: required: true content: application/json: schema: type: object required: [recoveryToken, registrationUpload, kdfSalt, kdfParams, encryptedAccountKey] properties: recoveryToken: { type: string } registrationUpload: { $ref: "#/components/schemas/Base64Url" } kdfSalt: { $ref: "#/components/schemas/Base64Url" } kdfParams: { $ref: "#/components/schemas/KdfParams" } encryptedAccountKey: { type: string } recoveryCode: type: object required: [registrationUpload, wrappedAccountKey] properties: registrationUpload: { $ref: "#/components/schemas/Base64Url" } wrappedAccountKey: { type: string } responses: "200": description: A session for the recovering device. content: application/json: schema: { $ref: "#/components/schemas/Session" } "401": { description: Unknown, used or expired recovery token. } /api/v1/recovery/org/request: post: summary: Ask an organization's recovery admins for recovery (signed out) description: Unknown emails and members who did not opt in get a fake request that never gets approved. operationId: orgRecoveryRequest tags: [recovery] security: [] requestBody: required: true content: application/json: schema: type: object required: [email, ephemeralKey] properties: email: { type: string } orgId: { $ref: "#/components/schemas/Id" } ephemeralKey: { $ref: "#/components/schemas/Base64Url", description: X-Wing public key of the requesting device. } deviceName: { type: string } responses: "201": description: Request created; valid for 48 hours. content: application/json: schema: type: object properties: requestId: { type: string } requestToken: { type: string } expiresAt: { type: integer } "429": { $ref: "#/components/responses/RateLimited" } /api/v1/recovery/org/status: post: summary: Poll an organization recovery request operationId: orgRecoveryStatus tags: [recovery] security: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/OrgRecoveryRequestRef" } responses: "200": description: "`pending`, `approved` (with the signed approval, the re-sealed key and a recovery grant), `denied` or `expired`." content: application/json: schema: type: object properties: status: { type: string, enum: [pending, approved, denied, expired] } expiresAt: { type: integer } approved: type: object properties: requestId: { type: string } accountId: { $ref: "#/components/schemas/Id" } approval: { type: string } approvalSignature: { type: string } sealedKey: { type: string } approverBundle: { type: string } approverBundleSignature: { type: string } recovery: { $ref: "#/components/schemas/RecoveryGrant" } /api/v1/recovery/org/cancel: post: summary: Cancel an organization recovery request operationId: orgRecoveryCancel tags: [recovery] security: [] requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/OrgRecoveryRequestRef" } responses: "204": { description: Cancelled. } /api/v1/orgs/{orgId}/recovery: delete: summary: Turn organization recovery off for everyone description: | Owners and admins. Deletes the recovery key, every admin's copy and every member's escrow and cancels open requests. Refused with 403 `recovery_required` while the organization's policy requires recovery. operationId: orgRecoveryTurnOff tags: [recovery] parameters: - { name: orgId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } responses: "204": { description: Recovery is off. } "403": { description: "`forbidden` (not an admin) or `recovery_required`." } "404": { description: Recovery is not set up, or not a member. } get: summary: Organization recovery key, grants and the caller's escrow description: Admins also see the enrolled members and the number of pending requests. operationId: orgRecovery tags: [recovery] parameters: - { name: orgId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } responses: "200": description: State. content: application/json: schema: type: object properties: key: type: [object, "null"] properties: keyVersion: { type: integer } bundle: { type: string } bundleSignature: { type: string } grants: type: array items: type: object properties: accountId: { $ref: "#/components/schemas/Id" } keyVersion: { type: integer } grant: { type: string } grantSignature: { type: string } sealedKey: { type: string, description: Only the caller's own grant. } escrow: type: [object, "null"] properties: keyVersion: { type: integer } createdAt: { type: integer } enrolled: { type: array, items: { $ref: "#/components/schemas/Id" } } pendingRequests: { type: integer } "403": { $ref: "#/components/responses/SharingForbidden" } /api/v1/orgs/{orgId}/recovery/keys: post: summary: Create the organization recovery key (owners and admins) operationId: createOrgRecoveryKey tags: [recovery] parameters: - { name: orgId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } requestBody: required: true content: application/json: schema: type: object required: [keyVersion, bundle, bundleSignature, grant] properties: keyVersion: { type: integer } bundle: { type: string, description: Signed `RecoveryKeyBundle`. } bundleSignature: { type: string } grant: { $ref: "#/components/schemas/RecoveryGrantUpload" } responses: "204": { description: Created. } "403": { $ref: "#/components/responses/SharingForbidden" } "409": { description: A key with that version exists. } /api/v1/orgs/{orgId}/recovery/grants: post: summary: Seal the recovery key to another admin operationId: addOrgRecoveryGrant tags: [recovery] parameters: - { name: orgId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } requestBody: required: true content: application/json: schema: { $ref: "#/components/schemas/RecoveryGrantUpload" } responses: "204": { description: Stored. } "403": { $ref: "#/components/responses/SharingForbidden" } /api/v1/orgs/{orgId}/recovery/escrow: put: summary: Opt in to organization recovery (signed escrow of the account key) operationId: putOrgRecoveryEscrow tags: [recovery] parameters: - { name: orgId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } requestBody: required: true content: application/json: schema: type: object required: [keyVersion, escrow, escrowSignature, sealedKey] properties: keyVersion: { type: integer } escrow: { type: string } escrowSignature: { type: string } sealedKey: { type: string } responses: "204": { description: Stored. } delete: summary: Withdraw from organization recovery operationId: deleteOrgRecoveryEscrow tags: [recovery] parameters: - { name: orgId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } responses: "204": { description: Removed. } /api/v1/orgs/{orgId}/recovery/requests: get: summary: Pending recovery requests (recovery admins) operationId: orgRecoveryRequests tags: [recovery] parameters: - { name: orgId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } responses: "200": description: Requests with the member's escrow and the requesting device's ephemeral key. content: application/json: schema: type: array items: type: object properties: id: { type: string } orgId: { $ref: "#/components/schemas/Id" } accountId: { $ref: "#/components/schemas/Id" } email: { type: string } deviceName: { type: string } ip: { type: string } createdAt: { type: integer } expiresAt: { type: integer } ephemeralKey: { type: string } keyVersion: { type: integer } escrow: { type: string } escrowSignature: { type: string } sealedKey: { type: string } "403": { $ref: "#/components/responses/SharingForbidden" } /api/v1/orgs/{orgId}/recovery/requests/{requestId}/approve: post: summary: Approve a recovery request (after an out-of-band identity check) operationId: approveOrgRecovery tags: [recovery] parameters: - { name: orgId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } - { name: requestId, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: type: object required: [approval, approvalSignature, sealedKey, requestCode] properties: approval: { type: string, description: Signed `RecoveryApproval`, bound to the ephemeral key hash. } approvalSignature: { type: string } sealedKey: { type: string, description: The account key sealed to the ephemeral key. } requestCode: { type: string } responses: "204": { description: Approved; the member and the other recovery admins get a mail. } "403": { $ref: "#/components/responses/SharingForbidden" } "404": { description: No pending request. } /api/v1/orgs/{orgId}/recovery/requests/{requestId}/deny: post: summary: Deny a recovery request operationId: denyOrgRecovery tags: [recovery] parameters: - { name: orgId, in: path, required: true, schema: { $ref: "#/components/schemas/Id" } } - { name: requestId, in: path, required: true, schema: { type: string } } responses: "204": { description: Denied. } /api/v1/emergency: get: summary: Emergency contacts granted and received operationId: emergencyList tags: [emergency] responses: "200": description: Lists. content: application/json: schema: type: object properties: granted: { type: array, items: { $ref: "#/components/schemas/EmergencyGrant" } } received: { type: array, items: { $ref: "#/components/schemas/EmergencyGrant" } } /api/v1/emergency/grants: post: summary: Invite an emergency contact operationId: emergencyCreate tags: [emergency] requestBody: required: true content: application/json: schema: type: object required: [granteeEmail, waitDays] properties: granteeEmail: { type: string } waitDays: { type: integer, minimum: 1, maximum: 30 } scope: { type: string, enum: [read], default: read } responses: "201": description: Invited. content: application/json: schema: type: object properties: { id: { type: string } } "429": { $ref: "#/components/responses/RateLimited" } /api/v1/emergency/grants/{id}: delete: summary: Remove an emergency contact (owner) or decline it (contact) operationId: emergencyDelete tags: [emergency] parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: "204": { description: Removed. } /api/v1/emergency/grants/{id}/accept: post: summary: Accept an invitation (contact) operationId: emergencyAccept tags: [emergency] parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: "204": { description: Accepted. } /api/v1/emergency/grants/{id}/confirm: post: summary: Store the signed grant and the vault keys sealed to the contact (owner); also used to re-seal operationId: emergencyConfirm tags: [emergency] parameters: - { name: id, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: type: object required: [grant, grantSignature, sealedKeys, vaultIds, version, fingerprint] properties: grant: { type: string, description: Signed `EmergencyGrant` (grantee, waiting period, vaults, version). } grantSignature: { type: string } sealedKeys: { type: string } vaultIds: { type: array, items: { $ref: "#/components/schemas/Id" } } version: { type: integer, description: Must increase. } fingerprint: { type: string } responses: "204": { description: Stored. } /api/v1/emergency/grants/{id}/request: post: summary: Request access (contact); starts the waiting period operationId: emergencyRequest tags: [emergency] parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: "204": { description: Requested; the owner gets a mail. } /api/v1/emergency/grants/{id}/approve: post: summary: Release access before the waiting period ends (owner) operationId: emergencyApprove tags: [emergency] parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: "204": { description: Approved. } /api/v1/emergency/grants/{id}/deny: post: summary: Deny a pending or released request (owner) operationId: emergencyDeny tags: [emergency] parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: "204": { description: Denied. } /api/v1/emergency/grants/{id}/access: get: summary: Sealed vault keys and the vaults' ciphertext (contact, after release) operationId: emergencyAccess tags: [emergency] parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: "200": description: Access data, decrypted on the contact's device. content: application/json: schema: type: object properties: grantorId: { $ref: "#/components/schemas/Id" } grant: { type: string } grantSignature: { type: string } sealedKeys: { type: string } vaults: { type: array, items: { type: object, additionalProperties: true } } items: { type: array, items: { type: object, additionalProperties: true } } "403": { description: "`emergency_access_waiting`: no request, the waiting period has not ended, or the owner denied it.", } /api/v1/device-links: post: summary: Start adding a device by code (signed-in device) operationId: deviceLinkCreate tags: [devices] responses: "201": description: Link; valid for 10 minutes. The secret part of the code never reaches the server. content: application/json: schema: type: object properties: id: { type: string } shortId: { type: string } expiresAt: { type: integer } "429": { $ref: "#/components/responses/RateLimited" } /api/v1/device-links/{id}: get: summary: Link status, with the new device's ephemeral key and MAC once claimed operationId: deviceLinkGet tags: [devices] parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: "200": description: Status. content: application/json: schema: type: object properties: status: { type: string, enum: [open, claimed, approved] } expiresAt: { type: integer } ephemeralKey: { type: string } mac: { type: string } deviceName: { type: string } ip: { type: string } claimedAt: { type: integer } delete: summary: Cancel a link operationId: deviceLinkDelete tags: [devices] parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: "204": { description: Cancelled. } /api/v1/device-links/{id}/approve: post: summary: Approve a claimed link with the sealed email and Secret Key operationId: deviceLinkApprove tags: [devices] parameters: - { name: id, in: path, required: true, schema: { type: string } } requestBody: required: true content: application/json: schema: type: object required: [sealedPayload] properties: { sealedPayload: { type: string } } responses: "204": { description: Approved. } /api/v1/device-links/claim: post: summary: Claim a link from the new device operationId: deviceLinkClaim tags: [devices] security: [] requestBody: required: true content: application/json: schema: type: object required: [shortId, ephemeralKey, mac] properties: shortId: { type: string } ephemeralKey: { $ref: "#/components/schemas/Base64Url" } mac: { type: string, description: HMAC under a key derived from the code's secret part. } deviceName: { type: string } responses: "200": description: Claimed. content: application/json: schema: type: object properties: claimToken: { type: string } expiresAt: { type: integer } "403": { description: "`device_link_expired`: unknown, used or expired code." } "429": { $ref: "#/components/responses/RateLimited" } /api/v1/device-links/poll: post: summary: Poll a claimed link from the new device operationId: deviceLinkPoll tags: [devices] security: [] requestBody: required: true content: application/json: schema: type: object required: [claimToken] properties: { claimToken: { type: string } } responses: "200": description: "`claimed` until approved, then `approved` with the sealed payload (returned once)." content: application/json: schema: type: object properties: status: { type: string } sealedPayload: { type: string } # --- Single sign-on, trusted devices and SCIM (passboxnet/core: docs/architecture/sso.md) --- /api/v1/orgs/{orgId}/sso: parameters: - { name: orgId, in: path, required: true, schema: { type: string } } get: summary: The organization's OIDC configuration, or null (owners and admins) operationId: ssoConfig tags: [sso] responses: "200": description: The configuration without the client secret (`hasClientSecret`), the redirect URI and the cached discovery endpoints. content: { application/json: { schema: { oneOf: [{ $ref: "#/components/schemas/SsoConfig" }, { type: "null" }] } } } put: summary: Create or replace the configuration (team organizations) description: | Enabling fetches the discovery document and the JWKS (`sso_unreachable`, `sso_issuer_mismatch`, `sso_insecure_url`, … as 403) and claims the email domains (409 with `details.code = sso_domain_taken`). Omit `clientSecret` to keep the stored one, send `""` to remove it. operationId: ssoSaveConfig tags: [sso] requestBody: required: true content: { application/json: { schema: { $ref: "#/components/schemas/SsoSettings" } } } responses: "200": { description: Saved., content: { application/json: { schema: { $ref: "#/components/schemas/SsoConfig" } } } } delete: summary: Remove the configuration, its domains, links and pending approvals operationId: ssoRemoveConfig tags: [sso] responses: "204": { description: Removed. } /api/v1/orgs/{orgId}/sso/check: post: summary: Fetch discovery and JWKS and report what was found (no response bodies) operationId: ssoCheck tags: [sso] parameters: - { name: orgId, in: path, required: true, schema: { type: string } } requestBody: content: { application/json: { schema: { type: object, properties: { issuer: { type: string, description: Check this issuer instead of the saved one. } }, }, }, } responses: "200": description: "`ok`, endpoints, number of keys, supported algorithms, or `problem: {code, message}`." content: { application/json: { schema: { type: object } } } /api/v1/orgs/{orgId}/sso/test/start: post: summary: Start a test sign-in (prompt=login); nothing is linked or signed in operationId: ssoTestStart tags: [sso] parameters: - { name: orgId, in: path, required: true, schema: { type: string } } responses: "200": { description: The authorization URL and flow secret., content: { application/json: { schema: { $ref: "#/components/schemas/SsoStart" } } }, } /api/v1/orgs/{orgId}/sso/test/result: post: summary: The claims of a test sign-in (only for the admin who started it) operationId: ssoTestResult tags: [sso] parameters: - { name: orgId, in: path, required: true, schema: { type: string } } requestBody: required: true content: { application/json: { schema: { $ref: "#/components/schemas/SsoTicket" } } } responses: "200": { description: "`outcome`, `subject`, `email`, `emailVerified`, `name`, `groups`, `accountFound`, `claims`.", content: { application/json: { schema: { type: object } } }, } /api/v1/sso/discover: post: summary: Which organization's identity provider signs in an email domain operationId: ssoDiscover tags: [sso] security: [] requestBody: required: true content: { application/json: { schema: { type: object, required: [email], properties: { email: { type: string } } } } } responses: "200": description: "`{ sso: { orgId, displayName, provider, required, deviceUnlock } | null }`; per domain, it reveals nothing about accounts." content: { application/json: { schema: { type: object } } } /api/v1/sso/start: post: summary: Start a sign-in (authorization code with PKCE) operationId: ssoStart tags: [sso] security: [] requestBody: required: true content: { application/json: { schema: { type: object, required: [orgId], properties: { orgId: { type: string }, email: { type: string, description: Sent as login_hint. } }, }, }, } responses: "200": { description: Send the browser to `authorizationUrl`; keep `flowSecret` in the tab., content: { application/json: { schema: { $ref: "#/components/schemas/SsoStart" } } }, } /api/v1/sso/callback: get: summary: The identity provider's redirect description: Answers with 303 to `{PASSBOX_PUBLIC_URL}/#/sso?ticket=…` or `#/sso?error=` (`&test=1` for connection tests). operationId: ssoCallback tags: [sso] security: [] parameters: - { name: state, in: query, schema: { type: string } } - { name: code, in: query, schema: { type: string } } - { name: error, in: query, schema: { type: string } } responses: "303": { description: Back to the web app. } /api/v1/sso/redeem: post: summary: Turn a ticket into a single-use proof (10 minutes) operationId: ssoRedeem tags: [sso] security: [] requestBody: required: true content: application/json: schema: allOf: - $ref: "#/components/schemas/SsoTicket" - { type: object, properties: { deviceId: { type: string, description: This device's trusted device ID. } }, } responses: "200": description: "`status` `ok` (with `accountId`, `proof`, `proofExpiresAt`) or `no_account`; `email`, `orgId`, `deviceUnlock`, `deviceTrusted`." content: { application/json: { schema: { type: object } } } /api/v1/sso/unlock: post: summary: A trusted device trades a proof for a session and its sealed account key operationId: ssoUnlock tags: [sso] security: [] requestBody: required: true content: { application/json: { schema: { type: object, required: [proof, deviceId], properties: { proof: { type: string }, deviceId: { type: string }, deviceName: { type: string }, clientKind: { type: string }, }, }, }, } responses: "200": { description: "`{ session, unlock: TrustedDeviceUnlock, email }`.", content: { application/json: { schema: { type: object } } }, } /api/v1/sso/device-requests: post: summary: A new device asks a trusted device for approval (30 minutes) operationId: ssoRequestDevice tags: [sso] security: [] requestBody: required: true content: { application/json: { schema: { type: object, required: [proof, publicKey], properties: { proof: { type: string }, publicKey: { type: string }, deviceName: { type: string }, clientKind: { type: string }, }, }, }, } responses: "201": { description: "`requestId`, `requestToken`, `expiresAt`, `approvalCode`.", content: { application/json: { schema: { type: object } } }, } /api/v1/sso/device-requests/status: post: summary: Poll an approval; returns the session and sealed key once operationId: ssoDeviceRequestStatus tags: [sso] security: [] requestBody: required: true content: { application/json: { schema: { type: object, required: [requestToken], properties: { requestToken: { type: string } } } }, } responses: "200": { description: "`status` `pending`, `approved` (with `result`) or `denied`.", content: { application/json: { schema: { type: object } } }, } /api/v1/account/trusted-devices: get: summary: Trusted devices and pending approval requests operationId: trustedDevices tags: [sso] responses: "200": { description: "`allowed`, `orgId`, `devices`, `requests`.", content: { application/json: { schema: { type: object } } }, } post: summary: Trust this device (login in the last 10 minutes) operationId: trustDevice tags: [sso] requestBody: required: true content: { application/json: { schema: { $ref: "#/components/schemas/TrustedDeviceUpload" } } } responses: "201": { description: Trusted. } /api/v1/account/trusted-devices/{id}: delete: summary: Remove a trusted device operationId: removeTrustedDevice tags: [sso] parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: "204": { description: Removed. } /api/v1/account/trusted-devices/requests/{id}/approve: post: summary: Approve a new device after comparing its code (login in the last 10 minutes) operationId: approveDevice tags: [sso] parameters: - { name: id, in: path, required: true, schema: { type: string } } requestBody: required: true content: { application/json: { schema: { $ref: "#/components/schemas/TrustedDeviceUpload" } } } responses: "204": { description: Approved. } /api/v1/account/trusted-devices/requests/{id}/deny: post: summary: Decline a new device operationId: denyDevice tags: [sso] parameters: - { name: id, in: path, required: true, schema: { type: string } } responses: "204": { description: Declined. } /api/v1/orgs/{orgId}/scim: get: summary: SCIM base URL, tokens, provisioned people (with status) and groups (owners and admins) operationId: scimOverview tags: [scim] parameters: - { name: orgId, in: path, required: true, schema: { type: string } } responses: "200": { description: Overview., content: { application/json: { schema: { type: object } } } } /api/v1/orgs/{orgId}/scim/tokens: post: summary: Create a SCIM bearer token (shown once; login in the last 10 minutes) operationId: scimCreateToken tags: [scim] parameters: - { name: orgId, in: path, required: true, schema: { type: string } } requestBody: required: true content: { application/json: { schema: { type: object, required: [name], properties: { name: { type: string }, expiresInDays: { type: integer, minimum: 1, maximum: 730 } }, }, }, } responses: "201": { description: "`id`, `token`, `expiresAt`, `baseUrl`.", content: { application/json: { schema: { type: object } } }, } /api/v1/orgs/{orgId}/scim/tokens/{id}: delete: summary: Revoke a SCIM token operationId: scimRevokeToken tags: [scim] parameters: - { name: orgId, in: path, required: true, schema: { type: string } } - { name: id, in: path, required: true, schema: { type: string } } responses: "204": { description: Revoked. } /api/v1/scim/v2/{resource}: get: summary: SCIM 2.0 (RFC 7644) — ServiceProviderConfig, ResourceTypes, Schemas, Users, Groups description: | Bearer token of the organization (`pbx_scim_…`). Bodies are `application/scim+json`. `Users` and `Groups` support list with `filter`, `startIndex` and `count`, `POST`, and on `/{id}` `GET`, `PUT`, `PATCH` and `DELETE`. Errors use the SCIM error schema. Deactivating or deleting a user suspends the membership and ends all sessions of the account. See docs/architecture/sso.md. operationId: scim tags: [scim] security: [{ scim: [] }] parameters: - { name: resource, in: path, required: true, schema: { type: string, enum: [ServiceProviderConfig, ResourceTypes, Schemas, Users, Groups] }, } - { name: filter, in: query, schema: { type: string } } - { name: startIndex, in: query, schema: { type: integer } } - { name: count, in: query, schema: { type: integer } } responses: "200": { description: A SCIM resource or ListResponse., content: { application/scim+json: { schema: { type: object } } }, } "401": { description: Missing or invalid SCIM token., content: { application/scim+json: { schema: { type: object } } }, } # --- Service accounts, see passboxnet/core: docs/architecture/service-accounts.md ---- /api/v1/agent-limit: get: summary: The fair-use limit for AI agents and service accounts description: | Agents and service accounts are free and never count as members. Active service accounts are limited by `PASSBOX_AGENT_LIMIT_PER_MEMBER` (or the instance setting `agentLimitPerMember`); members of an organization share a pool of the limit times its members who are not guests. Creating one more answers 403 `agent_limit_reached`. operationId: agentLimit tags: [service-accounts] responses: "200": description: The pool that applies to the account. content: application/json: schema: type: object properties: limitPerMember: { type: integer, description: 0 = unlimited. } used: { type: integer } allowed: { type: integer, description: 0 = unlimited. } pool: { type: string, enum: [personal, organization] } orgId: { $ref: "#/components/schemas/Id" } members: { type: integer } /api/v1/service-accounts: get: summary: The account's service accounts, newest first operationId: serviceAccountsList tags: [service-accounts] responses: "200": description: Service accounts with their vault grants (`keyEpoch` below `currentKeyEpoch` needs renewing). content: application/json: schema: { type: array, items: { $ref: "#/components/schemas/ServiceAccount" } } post: summary: Register a service account created by the client description: | The client generates the key pair and token (`passbox_core::service_account`). The public key and every grant must be signed by the account's published signing key; grants need the vault's current key epoch, and `write` needs the account's write access. The token itself is never sent. operationId: serviceAccountsCreate tags: [service-accounts] requestBody: required: true content: application/json: schema: type: object required: [id, name, publicKey, publicKeySignature, encryptedPrivate, authHash] properties: id: { type: string } name: { type: string, maxLength: 100 } publicKey: { $ref: "#/components/schemas/Base64Url" } publicKeySignature: { $ref: "#/components/schemas/Base64Url" } encryptedPrivate: { $ref: "#/components/schemas/Base64Url" } authHash: { type: string, description: Lowercase hex SHA-256 of the Base64url authentication key. } expiresAt: { type: integer, description: Unix ms, 0 for none. } grants: { type: array, items: { $ref: "#/components/schemas/ServiceAccountGrantUpload" } } responses: "201": { description: Created., content: { application/json: { schema: { $ref: "#/components/schemas/ServiceAccount" } } }, } "400": { description: Invalid or unsigned key or grant. } "409": { description: The vault's key was rotated (`currentKeyEpoch` in the details). } /api/v1/service-accounts/{id}: put: summary: Rename or change the expiry operationId: serviceAccountsUpdate tags: [service-accounts] parameters: [{ name: id, in: path, required: true, schema: { type: string } }] requestBody: content: application/json: schema: { type: object, properties: { name: { type: string }, expiresAt: { type: integer } } } responses: "200": { description: Updated., content: { application/json: { schema: { $ref: "#/components/schemas/ServiceAccount" } } }, } delete: summary: Delete the service account and its activity operationId: serviceAccountsDelete tags: [service-accounts] parameters: [{ name: id, in: path, required: true, schema: { type: string } }] responses: "204": { description: Deleted. } /api/v1/service-accounts/{id}/grants: put: summary: Add or renew vault grants operationId: serviceAccountsPutGrants tags: [service-accounts] parameters: [{ name: id, in: path, required: true, schema: { type: string } }] requestBody: required: true content: application/json: schema: type: object required: [grants] properties: { grants: { type: array, items: { $ref: "#/components/schemas/ServiceAccountGrantUpload" } } } responses: "200": { description: Updated., content: { application/json: { schema: { $ref: "#/components/schemas/ServiceAccount" } } }, } /api/v1/service-accounts/{id}/grants/{vaultId}: delete: summary: Remove access to a vault (all key epochs) operationId: serviceAccountsDeleteGrant tags: [service-accounts] parameters: - { name: id, in: path, required: true, schema: { type: string } } - { name: vaultId, in: path, required: true, schema: { type: string } } responses: "204": { description: Removed. } /api/v1/service-accounts/{id}/revoke: post: summary: Revoke the token at once (sealed keys are deleted, agent grants revoked) operationId: serviceAccountsRevoke tags: [service-accounts] parameters: [{ name: id, in: path, required: true, schema: { type: string } }] responses: "204": { description: Revoked. } /api/v1/service-accounts/{id}/events: get: summary: What the service account did operationId: serviceAccountsEvents tags: [service-accounts] parameters: - { name: id, in: path, required: true, schema: { type: string } } - { name: limit, in: query, schema: { type: integer, default: 50, maximum: 200 } } responses: "200": description: Events, newest first (`created`, `read`, `item_written`, `item_deleted`, `access_granted`, `access_removed`, `updated`, `revoked`). /api/v1/service-account: get: summary: Everything the service account can read (service account authentication) description: | Authenticated with `Authorization: Bearer sa.{id}.{authKey}`. Returns the encrypted private key, the grants, vaults with the effective permission (the lower of the grant and the owner's live access) and the items of those vaults. 403 `service_account_revoked` / `service_account_expired`. operationId: serviceAccountState tags: [service-accounts] security: [{ serviceAccount: [] }] responses: "200": { description: The state. } "401": { $ref: "#/components/responses/Unauthorized" } /api/v1/service-account/items/{id}: put: summary: Create or update an item in a vault the service account may write to operationId: serviceAccountPutItem tags: [service-accounts] security: [{ serviceAccount: [] }] parameters: [{ name: id, in: path, required: true, schema: { type: string } }] responses: "200": { description: Written. } "403": { description: Read-only access. } "409": { description: Revision or key epoch conflict. } delete: summary: Delete an item (tombstone) operationId: serviceAccountDeleteItem tags: [service-accounts] security: [{ serviceAccount: [] }] parameters: - { name: id, in: path, required: true, schema: { type: string } } - { name: revision, in: query, required: true, schema: { type: integer } } responses: "200": { description: Deleted. } # --- AI agents, see passboxnet/core: docs/architecture/ai-agents.md ---------------------- /api/v1/agent-grants: get: summary: Agent grant requests for this account (pending by default, `status=all` for 30 days) operationId: agentGrantsList tags: [agents] parameters: [{ name: status, in: query, schema: { type: string, enum: [pending, all] } }] responses: "200": { description: "Requests with the Base64url canonical `request` to parse and display before signing." } post: summary: Ask for a grant (pbx mcp signed in as the account) operationId: agentGrantsCreate tags: [agents] requestBody: required: true content: application/json: schema: { type: object, required: [request], properties: { request: { $ref: "#/components/schemas/Base64Url" } } } responses: "201": { description: "Created; pending for 10 minutes. Response `{ id, expiresAt }`." } "429": { $ref: "#/components/responses/RateLimited" } /api/v1/agent-grants/{id}: get: summary: The decision on a request (polled by the requesting broker) operationId: agentGrantsGet tags: [agents] parameters: [{ name: id, in: path, required: true, schema: { type: string } }] responses: "200": { description: "`{ id, status, decision?, decisionSignature?, notAfter, expiresAt }`; status `pending`, `approved`, `denied`, `expired` or `revoked`.", } /api/v1/agent-grants/{id}/decision: post: summary: Approve or deny, signed with the account key on the approving device operationId: agentGrantsDecide tags: [agents] parameters: [{ name: id, in: path, required: true, schema: { type: string } }] requestBody: required: true content: application/json: schema: type: object required: [decision, decisionSignature] properties: decision: { $ref: "#/components/schemas/Base64Url" } decisionSignature: { $ref: "#/components/schemas/Base64Url" } responses: "200": { description: Recorded. } "400": { description: The signature or the decision does not match the request. } "409": { description: Already answered or expired. } /api/v1/agent-grants/{id}/revoke: post: summary: End a pending or approved grant operationId: agentGrantsRevoke tags: [agents] parameters: [{ name: id, in: path, required: true, schema: { type: string } }] responses: "204": { description: Revoked. } /api/v1/agent-events: get: summary: What agents did on behalf of this account operationId: agentEventsList tags: [agents] parameters: [{ name: limit, in: query, schema: { type: integer, default: 100, maximum: 500 } }] responses: "200": { description: "Events: principal, client name, grant, tool, item IDs, target (command word and hash), result.", } post: summary: Report an agent action (the broker); never values operationId: agentEventsCreate tags: [agents] requestBody: required: true content: application/json: schema: type: object required: [tool, result] properties: grantId: { type: string } tool: { type: string, enum: [search, request_access, run, inject, create, generate, reveal] } itemIds: { type: array, items: { type: string } } target: { type: string, maxLength: 200 } result: { type: string, enum: [ok, failed, denied, expired, blocked] } clientName: { type: string, maxLength: 100 } responses: "204": { description: Recorded. } "403": description: | `agent_reveal_not_allowed`: a successful `reveal` needs an approved, unexpired grant whose signed decision allows revealing those items, and organization policies that allow it. The broker hands out the value only after this endpoint accepted the event. /api/v1/service-account/agent-grants: post: summary: Ask the owner for a grant (pbx mcp with a service account token) operationId: serviceAccountAgentGrantsCreate tags: [agents] security: [{ serviceAccount: [] }] responses: "201": { description: Created. } /api/v1/service-account/agent-grants/{id}: get: summary: The owner's decision operationId: serviceAccountAgentGrantsGet tags: [agents] security: [{ serviceAccount: [] }] parameters: [{ name: id, in: path, required: true, schema: { type: string } }] responses: "200": { description: "As for `/agent-grants/{id}`." } /api/v1/service-account/agent-events: post: summary: Report an agent action of a service account operationId: serviceAccountAgentEventsCreate tags: [agents] security: [{ serviceAccount: [] }] responses: "204": { description: Recorded. } components: securitySchemes: session: type: http scheme: bearer serviceAccount: type: http scheme: bearer description: "`sa.{id}.{authKey}`, where `authKey` is derived from the service account token's secret." scim: type: http scheme: bearer description: A SCIM token of an organization (`pbx_scim_…`). responses: Unauthorized: description: Missing, invalid or expired session token. RateLimited: description: | Too many requests or failed logins. `details.retryAfterSeconds` and the `Retry-After` header give the remaining time of the current window. Conflict: description: The revision does not follow the stored one. `details.currentRevision` holds the stored revision. Forbidden: description: "`forbidden`: the account is not an instance admin." SharingForbidden: description: "`forbidden`: the caller's role in the organization or vault does not allow this." RegistrationRefused: description: | `registration_closed` (invite-only server), `domain_not_allowed` (`domains` mode, other domain) or `invitation_invalid`. schemas: ServiceAccountGrantUpload: type: object required: [vaultId, keyEpoch, permission, sealedKey, grant, grantSignature] properties: vaultId: { type: string } keyEpoch: { type: integer } permission: { type: string, enum: [read, write] } sealedKey: { $ref: "#/components/schemas/Base64Url" } grant: { $ref: "#/components/schemas/Base64Url" } grantSignature: { $ref: "#/components/schemas/Base64Url" } ServiceAccount: type: object properties: id: { type: string } name: { type: string } publicKey: { $ref: "#/components/schemas/Base64Url" } publicKeySignature: { $ref: "#/components/schemas/Base64Url" } expiresAt: { type: integer } revokedAt: { type: integer } lastUsedAt: { type: integer } lastUsedIp: { type: string } createdAt: { type: integer } grants: type: array items: type: object properties: vaultId: { type: string } keyEpoch: { type: integer } permission: { type: string, enum: [read, write] } currentKeyEpoch: { type: integer } createdAt: { type: integer } SsoSettings: type: object required: [enabled, displayName, issuer, clientId] properties: enabled: { type: boolean } required: { type: boolean, description: Members (not owners and admins) must sign in with SSO. } allowDeviceUnlock: { type: boolean, description: Members may unlock with SSO on trusted devices. } provider: { type: string, enum: [generic, entra, google, okta, keycloak, authentik, zitadel] } displayName: { type: string, maxLength: 80 } issuer: { type: string, format: uri } clientId: { type: string } clientSecret: { type: string, writeOnly: true } tokenAuthMethod: { type: string, enum: [client_secret_basic, client_secret_post, none] } scopes: { type: string, default: openid email profile } emailClaim: { type: string, default: email } requireVerifiedEmail: { type: boolean, default: true } nameClaim: { type: string, default: name } groupsClaim: { type: string, default: groups } allowedDomains: { type: array, items: { type: string } } reauthHours: { type: integer, minimum: 1, maximum: 720, default: 12 } SsoConfig: allOf: - $ref: "#/components/schemas/SsoSettings" - type: object properties: hasClientSecret: { type: boolean } redirectUri: { type: [string, "null"] } discovery: { type: [object, "null"] } updatedAt: { type: integer } SsoStart: type: object required: [authorizationUrl, flowSecret, expiresAt] properties: authorizationUrl: { type: string } flowSecret: { type: string, description: Keep it in the tab that started the flow. } expiresAt: { type: integer } SsoTicket: type: object required: [ticket, flowSecret] properties: ticket: { type: string } flowSecret: { type: string } TrustedDeviceUpload: type: object required: [deviceId, publicKey, sealedKey, grant, grantSignature] properties: deviceId: { type: string } publicKey: { type: string, description: X-Wing public key of the device. } sealedKey: { type: string } grant: { type: string, description: "Canonical `passbox/v1/trusted-device-grant`, signed by the account." } grantSignature: { type: string } name: { type: string } Session: type: object required: [token, accountId, sessionId, expiresAt, deviceToken] properties: token: { type: string } accountId: { $ref: "#/components/schemas/Id" } sessionId: { $ref: "#/components/schemas/Id" } expiresAt: { type: integer } deviceToken: type: string description: | Store it on the device and send it with the next `login/start`. Every successful login issues a new token and invalidates the one it was started with. Tokens grant no access by themselves, survive logout and session revocation, expire after a year, and are invalidated for all other devices by a master password change. recoveryCodesRemaining: type: integer description: Present after signing in with a recovery code. TwoFactorChallenge: type: object required: [twoFactorRequired, challengeToken, expiresAt, methods] properties: twoFactorRequired: { type: boolean, const: true } challengeToken: { type: string, description: Valid for 5 minutes. } expiresAt: { type: integer } methods: type: array items: { type: string, enum: [totp, webauthn, recovery] } webauthn: type: object description: | PublicKeyCredentialRequestOptions for `navigator.credentials.get` (binary values Base64url), present when the account has security keys. required: [challenge, rpId, allowCredentials, timeout, userVerification] properties: challenge: { $ref: "#/components/schemas/Base64Url" } rpId: { type: string } allowCredentials: type: array items: type: object properties: type: { type: string, const: public-key } id: { $ref: "#/components/schemas/Base64Url" } timeout: { type: integer } userVerification: { type: string } SecondFactor: type: object required: [method] properties: method: { type: string, enum: [totp, recovery, webauthn] } code: { type: string, description: 6-digit code or recovery code (case, spaces and dashes ignored). } webauthn: type: object required: [credentialId, clientDataJson, authenticatorData, signature] properties: credentialId: { $ref: "#/components/schemas/Base64Url" } clientDataJson: { $ref: "#/components/schemas/Base64Url" } authenticatorData: { $ref: "#/components/schemas/Base64Url" } signature: { $ref: "#/components/schemas/Base64Url" } RecoveryCodes: type: object properties: recoveryCodes: type: array items: { type: string, example: 7K2M-9QXD-4HPT-WZ3C } Id: type: string pattern: "^[0-7][0-9a-hjkmnp-tv-z]{25}$" description: 128 random bits as 26 lowercase Crockford Base32 characters. Base64Url: type: string pattern: "^[A-Za-z0-9_-]+$" KdfParams: type: object required: [memoryKib, iterations, parallelism] properties: memoryKib: { type: integer, minimum: 65536 } iterations: { type: integer, minimum: 3 } parallelism: { type: integer, minimum: 1 } ServerMeta: type: object required: [name, version, apiVersion, minKdfParams] properties: name: { type: string } version: { type: string } apiVersion: { type: integer } minKdfParams: { $ref: "#/components/schemas/KdfParams" } instanceName: { type: string } registration: { type: string, enum: [open, invite_only, domains] } allowedDomains: { type: array, items: { type: string }, description: Only in `domains` mode. } supportUrl: { type: [string, "null"] } defaultLocale: { type: string, enum: [en, de] } passwordPolicy: { $ref: "#/components/schemas/PasswordPolicy" } icons: { type: boolean, description: "Whether `/api/v1/icons/{domain}` serves website icons (`PASSBOX_ICONS`)." } breachProxy: { type: boolean, description: "Whether `/api/v1/breach/range/{prefix}` proxies Have I Been Pwned (`PASSBOX_BREACH_PROXY`).", } emailMonitoring: { type: boolean, description: "Whether email breach monitoring is available (`PASSBOX_HIBP_API_KEY` set)." } breachCatalog: { type: boolean, description: "Whether `/api/v1/breaches/catalog` serves the breach catalogue (`PASSBOX_BREACH_CATALOG`).", } SiteBreach: type: object description: One breached website of the catalogue (Have I Been Pwned's breach model, compacted). required: [name, title, domain, breachDate, addedDate, modifiedDate, pwnCount, dataClasses, verified, sensitive] properties: name: { type: string, description: Have I Been Pwned's stable name. } title: { type: string } domain: { type: string, description: The breached site, lower case. } breachDate: { type: string, description: "`YYYY-MM-DD` (often approximate)." } addedDate: { type: string, description: "When it was published (ISO 8601). Passwords set before it count as affected." } modifiedDate: { type: string } pwnCount: { type: integer } dataClasses: { type: array, items: { type: string }, description: "`Passwords`, `Email addresses`, …" } verified: { type: boolean } sensitive: { type: boolean, description: Keep the name off lock screens and notifications. } BreachCatalog: type: object required: [version, updatedAt, source, sourceUrl, license, breaches] properties: version: { type: string, description: Changes with the content; the `ETag` is this value quoted. } updatedAt: { type: integer, description: Newest added or modified date of the entries (Unix ms). } source: { type: string, example: Have I Been Pwned } sourceUrl: { type: string, example: "https://haveibeenpwned.com/" } license: { type: string, example: CC BY 4.0 } breaches: { type: array, items: { $ref: "#/components/schemas/SiteBreach" } } BreachCatalogVersion: type: object required: [version, updatedAt, entries, bundled, source, sourceUrl, license] properties: version: { type: string } updatedAt: { type: integer } entries: { type: integer } bundled: { type: boolean, description: No fetch succeeded yet; the server serves its bundled snapshot. } source: { type: string } sourceUrl: { type: string } license: { type: string } EmailBreach: type: object description: A breach from Have I Been Pwned's breach model. required: [name, title, domain, breachDate, addedDate, pwnCount, dataClasses, firstSeenAt] properties: name: { type: string } title: { type: string } domain: { type: string } breachDate: { type: string, description: "`YYYY-MM-DD`." } addedDate: { type: string } pwnCount: { type: integer } dataClasses: { type: array, items: { type: string } } firstSeenAt: { type: integer, description: When this server first saw the address in the breach (Unix ms). } MonitoredEmail: type: object required: [id, email, verified, checkedAt, checkError, createdAt, breaches] properties: id: { type: string } email: { type: string } verified: { type: boolean } checkedAt: { type: integer, description: Last check (Unix ms, 0 = never). } checkError: { type: [string, "null"] } createdAt: { type: integer } breaches: { type: array, items: { $ref: "#/components/schemas/EmailBreach" } } EmailMonitoring: type: object required: [available, checkIntervalHours, emails] properties: available: { type: boolean } checkIntervalHours: { type: integer } emails: { type: array, items: { $ref: "#/components/schemas/MonitoredEmail" } } PasswordPolicy: type: object description: Minimum for new master passwords; clients enforce it. required: [minScore, minLength] properties: minScore: { type: integer, enum: [3, 4] } minLength: { type: integer, minimum: 12, maximum: 128 } AccountPreferences: type: object required: [locale, notifyNewDevice] properties: locale: { type: [string, "null"], enum: [en, de, null] } notifyNewDevice: { type: boolean } InstanceSettings: type: object required: [instanceName, registration, allowedDomains, supportUrl, defaultLocale, passwordPolicy] properties: instanceName: { type: string, minLength: 1, maxLength: 60 } registration: { type: string, enum: [open, invite_only, domains] } allowedDomains: { type: array, items: { type: string } } supportUrl: { type: string, description: "https://, http:// or mailto: address, or empty." } defaultLocale: { type: string, enum: [en, de] } passwordPolicy: { $ref: "#/components/schemas/PasswordPolicy" } AdminSettings: type: object required: [settings, mail, envAdminEmails] properties: settings: { $ref: "#/components/schemas/InstanceSettings" } envAdminEmails: { type: array, items: { type: string } } mail: type: object required: [configured, transport, host, from, publicUrl, pending, failed, lastSentAt, lastError] properties: configured: { type: boolean } transport: { type: string, enum: [smtps, starttls, smtp, log] } host: { type: [string, "null"] } from: { type: string } publicUrl: { type: [string, "null"] } pending: { type: integer } failed: { type: integer } lastSentAt: { type: [integer, "null"] } lastError: { type: [string, "null"] } AdminUser: type: object required: [id, email, createdAt, lastActivityAt, status, disabledAt, instanceAdmin, activeSessions, twoFactorEnabled] properties: id: { $ref: "#/components/schemas/Id" } email: { type: string } createdAt: { type: integer } lastActivityAt: { type: [integer, "null"] } status: { type: string, enum: [active, disabled] } disabledAt: { type: [integer, "null"] } instanceAdmin: { type: boolean } activeSessions: { type: integer } twoFactorEnabled: { type: [boolean, "null"], description: "`null` until the server supports login 2FA." } Invite: type: object required: [id, email, invitedBy, createdAt, expiresAt, usedAt, status] properties: id: { $ref: "#/components/schemas/Id" } email: { type: string } invitedBy: { type: string } createdAt: { type: integer } expiresAt: { type: integer } usedAt: { type: [integer, "null"] } status: { type: string, enum: [pending, used, expired] } AdminEvent: type: object required: [seq, id, actorId, actorEmail, action, target, details, ip, createdAt, prevHash, hash] properties: seq: { type: integer } id: { $ref: "#/components/schemas/Id" } actorId: { type: string } actorEmail: { type: string } action: type: string description: instance_admin_bootstrap, settings_updated, user_disabled, user_enabled, test_mail_sent, invite_created, invite_used, invite_revoked target: { type: string } details: { type: [object, "null"] } ip: { type: string } createdAt: { type: integer } prevHash: { type: string } hash: { type: string } NewVault: type: object required: [id, encryptedKey, encryptedMeta] properties: id: { $ref: "#/components/schemas/Id" } encryptedKey: { $ref: "#/components/schemas/Base64Url" } encryptedMeta: { $ref: "#/components/schemas/Base64Url" } SyncVault: type: object required: [id, encryptedKey, encryptedMeta, revision, updatedAt] properties: id: { $ref: "#/components/schemas/Id" } encryptedKey: { type: string, description: Empty for shared vaults (keys come in `sharing.vaultKeys`) } encryptedMeta: { $ref: "#/components/schemas/Base64Url" } revision: { type: integer } updatedAt: { type: integer } orgId: { type: string, description: Shared vaults only } keyEpoch: { type: integer, description: Key epoch of `encryptedMeta`; absent for personal vaults } SyncItem: type: object required: [id, vaultId, revision, encryptedData, deleted, updatedAt] properties: id: { $ref: "#/components/schemas/Id" } vaultId: { $ref: "#/components/schemas/Id" } revision: { type: integer } encryptedData: oneOf: - { $ref: "#/components/schemas/Base64Url" } - { type: "null" } deleted: { type: boolean } updatedAt: { type: integer } keyEpoch: { type: integer, description: Vault key epoch of `encryptedData` (0 for personal vaults) } createdAt: { type: integer, description: When the item was created (Unix ms). } ItemVersion: type: object required: [revision, keyEpoch, encryptedData, updatedAt, updatedBy] properties: revision: { type: integer } keyEpoch: { type: integer } encryptedData: { $ref: "#/components/schemas/Base64Url" } updatedAt: { type: integer } updatedBy: { type: [string, "null"], description: Public ID of the session that saved this version. } WriteResponse: type: object required: [revision, seq] properties: revision: { type: integer } seq: { type: integer } AccountKeys: type: object required: [keyVersion, bundle, bundleSignature, encryptedPrivate] properties: keyVersion: { type: integer } bundle: { $ref: "#/components/schemas/Base64Url", description: Canonical public bundle (account ID, key version, X-Wing and Ed25519 public keys, creation time), } bundleSignature: { $ref: "#/components/schemas/Base64Url" } encryptedPrivate: { $ref: "#/components/schemas/Base64Url", description: Private keys encrypted with the account key } SignedHead: type: object required: [head, signature, logKey] properties: head: { $ref: "#/components/schemas/Base64Url", description: Canonical head (size, root hash, timestamp) } signature: { $ref: "#/components/schemas/Base64Url" } logKey: { $ref: "#/components/schemas/Base64Url", description: Ed25519 log public key, pinned by clients } KeyLogEntry: type: object required: [seq, accountId, keyVersion, bundleHash, prevHash] properties: seq: { type: integer } accountId: { type: string } keyVersion: { type: integer } bundleHash: { $ref: "#/components/schemas/Base64Url" } prevHash: { $ref: "#/components/schemas/Base64Url" } KeyLookup: type: object required: [bundle, bundleSignature, seq, entries, head] properties: bundle: { $ref: "#/components/schemas/Base64Url" } bundleSignature: { $ref: "#/components/schemas/Base64Url" } seq: { type: integer } entries: { type: array, items: { $ref: "#/components/schemas/KeyLogEntry" } } head: { $ref: "#/components/schemas/SignedHead" } OrgRecord: type: object required: [id, kind, role, status, genesis, genesisSignature, encryptedMeta, metaEpoch, revision] properties: id: { $ref: "#/components/schemas/Id" } kind: { type: string, enum: [family, team] } role: { type: string, enum: [owner, admin, member, guest], description: The caller's role } status: { type: string, enum: [accepted, confirmed, suspended] } genesis: { $ref: "#/components/schemas/Base64Url" } genesisSignature: { $ref: "#/components/schemas/Base64Url" } encryptedMeta: { $ref: "#/components/schemas/Base64Url" } metaEpoch: { type: integer } revision: { type: integer } defaultVaultId: { type: [string, "null"] } RosterMember: type: object required: [accountId, role, status] properties: accountId: { type: string } role: { type: string } status: { type: string } email: { type: [string, "null"] } bundle: { type: [string, "null"] } bundleSignature: { type: [string, "null"] } attestation: { type: [string, "null"] } attestationSignature: { type: [string, "null"] } InvitationInfo: type: object properties: id: { type: string } email: { type: string } role: { type: string } status: { type: string } createdBy: { type: string } createdAt: { type: integer } expiresAt: { type: integer } NewOrg: type: object description: Built by `Keyring.createOrg` (`passbox_core::sharing::build::NewOrg`). required: [id, kind, genesis, genesisSignature, encryptedMeta, metaEpoch, orgKey] properties: id: { $ref: "#/components/schemas/Id" } kind: { type: string, enum: [family, team] } genesis: { $ref: "#/components/schemas/Base64Url" } genesisSignature: { $ref: "#/components/schemas/Base64Url" } encryptedMeta: { $ref: "#/components/schemas/Base64Url" } metaEpoch: { type: integer, enum: [1] } orgKey: { type: object } vault: { type: object } SharingRequest: type: object description: | A body built by the core's sharing builders (`passbox_core::sharing::build`, exposed as `Keyring.*` in `@passbox/core-wasm`): canonical signed objects (Base64url) with their Ed25519 signatures and sealed keys. The server checks that objects parse, name the right IDs, match its member lists and are signed by the caller's published key; clients verify them independently. additionalProperties: true SharingDelta: type: object description: | `passbox_core::sharing::data::SharingDelta`. Full lists: `orgs`, `groups`, `vaultAccess`. Changed rows: `rosters` (full member list per changed organization), `orgKeys`, `groupKeys`, `vaultKeys` (keys sealed for the caller or its groups), `manifests` (full chain of vaults whose members changed), `signers` (bundles of manifest signers). additionalProperties: true RecoveryGrant: type: object required: [recoveryToken, accountId, email, expiresAt, method] properties: recoveryToken: { type: string, description: Single use, valid for 15 minutes. } accountId: { $ref: "#/components/schemas/Id" } email: { type: string } expiresAt: { type: integer } method: { type: string, enum: [code, org] } twoFactorMethods: { type: array, items: { type: string } } wrappedAccountKey: { type: string } accountKeys: { $ref: "#/components/schemas/AccountKeys" } RecoveryGrantUpload: type: object required: [accountId, keyVersion, sealedKey, grant, grantSignature] properties: accountId: { $ref: "#/components/schemas/Id" } keyVersion: { type: integer } sealedKey: { type: string } grant: { type: string, description: Signed `RecoveryKeyGrant`. } grantSignature: { type: string } OrgRecoveryRequestRef: type: object required: [requestId, requestToken] properties: requestId: { type: string } requestToken: { type: string } clientKind: { type: string } EmergencyGrant: type: object properties: id: { type: string } grantorId: { $ref: "#/components/schemas/Id" } grantorEmail: { type: string } granteeId: { type: [string, "null"] } granteeEmail: { type: string } scope: { type: string, enum: [read] } waitDays: { type: integer } status: { type: string, enum: [invited, accepted, confirmed], description: "A request shows in `requestedAt`; `released` says whether access is open now.", } vaultIds: { type: array, items: { $ref: "#/components/schemas/Id" } } version: { type: integer } grant: { type: string } grantSignature: { type: string } requestedAt: { type: integer } approvedAt: { type: integer } deniedAt: { type: integer } accessedAt: { type: integer } releaseAt: { type: integer, description: When the server releases access without the owner's approval. } released: { type: boolean } createdAt: { type: integer }