API change log
2.0.0-rc.81
Section titled “2.0.0-rc.81”Only personal use keeps an account active for auto-deletion. No schema migration; one new configuration key.
- Account auto-deletion: an account’s last activity (
lastActivityinaccount.getAutoDeletion, the clock of aninactivityschedule) is now the latest use of its personal accesses: the holder’s sign-ins and personal tokens, and a delegate’s own token. App accesses (the holder’s apps, OAuth sessions, apps a delegate granted) count only with the new per-core settingaccount.autoDeletion.inactivity.countAppUse(defaultfalse); shared accesses still never count. In the breakdown,delegateis a delegate’s own token,appevery app access, reported even when it does not count. The recorded maximum never moves backwards, so an account kept active by app use until now keeps that date. Operators who rely on app use: setcountAppUse: true. See What counts as use.
2.0.0-rc.80
Section titled “2.0.0-rc.80”Operator-registered OAuth clients are named on their grant and sessions. No breaking change, no new configuration key, no schema migration. As announced with 2.0.0-rc.73, a later release is planned to default oauth.audienceBoundSessions to true.
- OAuth: the consent grant and every session of a registry client the operator registered carry, server-owned,
clientData.oauth.client = { name?, origin: "operator" }: the name the operator registered, never one taken from a request. A metadata-document client’s stamp also gainsorigin: "metadata-document"(a stamp written before has none; itshosttells it apart). An account app names a connected app by this stamp. A grant made before carries it from its next consent, its sessions from their next refresh; a derived session (token exchange) carries none. See client metadata documents.
2.0.0-rc.79
Section titled “2.0.0-rc.79”Member access budgets for bulk reads by group accounts, a member access that can be asked again after it expired, and Retry-After readable by browser clients. No breaking change, no schema migration. Three new configuration keys. As announced with 2.0.0-rc.73, a later release is planned to default oauth.audienceBoundSessions to true.
- CMC member access budgets. Asking again for a member access that is still live no longer counts as a new one. Each membership has two budgets, counted for the whole core: new member accesses (
cmc:memberAccess:mintBurst, default 5000 at once, thencmc:memberAccess:ratePerMinute, default 1000 a minute, was 30 per worker process) and requests of any kind (cmc:memberAccess:askBurst, 10000, thencmc:memberAccess:askRatePerMinute, 3000 a minute). Past either, the ask answers429too-many-attemptswithRetry-Afteranderror.data.retryAfterSeconds, instead of400;error.data.idstayscmc-member-access-rate-limited. Operators who had tunedratePerMinuteto be strict: setmintBursttoo. See group accounts. - CMC member access after expiry. Asking again for a patient once the previous member access expired no longer fails
cmc-member-access-peer-rejected(409item-already-exists): the expired access is deleted through the grant and a new one is minted. - CMC: an ask whose event could not be stored no longer leaves that patient answering
cmc-member-access-in-flightfor a minute. - CORS:
Access-Control-Expose-Headersnow listsRetry-After, so a browser client can read the wait of a429. - OAuth: the account owner consenting at one of their own address aliases that another app’s grant holds is refused
403 access_deniedwithreason: "address-alias-held", instead ofaudience-account-mismatch. See per-user resource addresses.
2.0.0-rc.78
Section titled “2.0.0-rc.78”Public email lookups within a per-address budget, and a single-use CMC link that accepts one accepter only. No new configuration key, no schema migration. As announced with 2.0.0-rc.73, a later release is planned to default oauth.audienceBoundSessions to true.
- Public email lookups.
GET /reg/{email}/check_email,/reg/{email}/username,/reg/{email}/uid,/reg/cores?email=and, with the registration email gate on,POST /reg/email-challengeshare one budget of 30 requests a minute per client address (IPv4, or the /64 of IPv6); past it they answer429too-many-attemptswithRetry-After. A client that tests many addresses in a burst should throttle or handle the429. auth.emailCheckandauth.coresare no longer accepted inside a batch call or over socket.io.- CMC: a single-use link accepts one accepter only, also when two accepts arrive at the same moment.
2.0.0-rc.77
Section titled “2.0.0-rc.77”An opt-in per-user OAuth issuer. No breaking change, no schema migration. As announced with 2.0.0-rc.73, a later release is planned to default oauth.audienceBoundSessions to true.
- Per-user issuer (
oauth.perUserIssuer, off by default). On, an account’s own host (https://<username>.<domain>, or an address alias’s) is its OAuth issuer: discovery there names that issuer and its endpoints,issis that host on every redirect and in the id_token, a code or refresh token is redeemed at its issuer only, and only the account the host names consents there. Off, nothing changes. See per-user issuer. - OAuth
421 wrong_corebehind a proxy: on an account’s own host,/oauth2/tokenand/oauth2/authorizeread the client-facing host (X-Forwarded-Hostfrom a trusted proxy, elseHost), so a proxy that rewritesHostno longer hides a wrong-core answer. - Audit: a streamed call (
events.getand other calls answered as a stream) that the client cuts short is audited as an error (client-closed-request), no longer as a success. - HF series: points written through a former username right after a username change are gathered under the new one.
2.0.0-rc.76
Section titled “2.0.0-rc.76”Address aliases: an account owner can mint addresses of the account other than its username, to give an app instead of the username. Breaking (registration): a new username of the exact shape a- + 8 letters or digits is refused. No new configuration key, no schema migration. As announced with 2.0.0-rc.73, a later release is planned to default oauth.audienceBoundSessions to true.
- Address aliases. New owner-only methods create, list and delete an address alias (personal token only). The server mints the name (
a-+ 8 lower-case letters or digits), at most 10 per account. An address alias is an address of the account like its username:https://<alias>.<domain>/reaches the account’s core. - OAuth consent at an address alias. Under a
{username}audience entry, a consent athttps://<alias>.<host>/...adopts the alias for that client’s grant: introspectionsub,api_base, the token’s address and its audience are the alias, never the username, and the tokens are used at that address only. One grant per alias: an alias another grant holds, another account’s alias or a former username is refused (403,reason: "audience-account-mismatch"). A consent at the username over a grant that adopted an alias is a change of subject (subjectChange, to acknowledge). Deleting the alias, or withdrawing the grant, ends the app’s sessions and refresh tokens. See per-user resource addresses. - Usernames. A new username of the shape
a-+ 8 letters or digits is refused at registration and at a username change (/reg/{name}/check_usernamereads it as reserved). Existing accounts keep their name. Operators: right after the upgrade, runnode bin/reconcile-user-cores.js --address-alias-shapeon each core (read only; 0 and 0 expected). - Adapter routes: an adapter that answers before reading a large upload (a
401ahead of the body, for example) is relayed to the client instead of502.
2.0.0-rc.75
Section titled “2.0.0-rc.75”Per-user resource addresses for OAuth clients and adapters: a resource server or an adapter is addressed at each account’s own host, https://<username>.<domain>/..., and the core names the addressed account to the adapter. No breaking change, no new configuration key, no schema migration. As announced with 2.0.0-rc.73, a later release is planned to default oauth.audienceBoundSessions to true.
- Per-user audiences. An OAuth client’s
allowedAudiences(a registry client’s--audience, or the metadata-document template’sclient.allowedAudiences) may holdhttps://{username}.<host>/...entries. Such an entry matches the address of any account and is bound at consent to the signed-in account: another account’s address, or a name the platform answers itself, is refused (403 access_denied,reason: "audience-account-mismatch"). Add such an entry only once every core of the platform runs this release. See per-user resource addresses. - Token exchange:
subject_token_resource. A resource server may send the resource URL at which it received the subject token; it must be the token’s audience (400 invalid_targetotherwise), so a token issued for one account’s address is not exchanged when presented at another’s. - Adapter routes on per-user hosts. On
https://<username>.<dns.domain>/adapter/<name>/...the core setsX-Adapter-NameandX-Adapter-Baseon the forwarded request and drops client-sent copies; an account served by another core answers421 wrong-corewithRetry-After(retry the same address). Upgrade each core before the sidecar next to it reads the two headers. An nginx that routes adapter paths around the core serves platform addresses only. See per-user addresses. - OAuth on the host of an account another core serves:
/oauth2/tokenanswers421 wrong_corewithRetry-Afterbefore any code or refresh token is read;/oauth2/authorizeanswers421with a static “this address has just moved” page. /service/info.adapterslists adapter bases as the operator sets them: use the{username}template form.- Usernames: a name another core of the platform answers itself (its static DNS entries, its OAuth issuer label) is refused as a username on every core.
accesses.checkAppreads:local:<id>as<id>, asaccesses.createstores it: an app requesting it is no longer asked for consent again at every sign-in.
2.0.0-rc.74
Section titled “2.0.0-rc.74”OAuth client metadata documents (an https URL as client_id), RS256 client assertions, a clear answer at the consent for an account served by another core, the OAuth surface’s own CORS, and server-created streams that match the stream schema. No breaking change. One new configuration block, off by default: oauth.clientMetadataDocuments. No schema migration. As announced with 2.0.0-rc.73, a later release is planned to default oauth.audienceBoundSessions to true: set it explicitly where it is not set yet.
- Client metadata documents. With
oauth.clientMetadataDocuments.modeset toallowList(the hosts ofallowedHostsonly) orany, a client may use the https URL of a JSON document it publishes as itsclient_id: a public client (PKCE) or a confidential one (private_key_jwtagainst the document’sjwks_urior inlinejwks). The core fetches the document (https only, no redirect followed, 16 KiB, 5 s, private addresses refused, rate-bounded), takes the client’s name and redirect URIs from it and what the client may be granted from the operator’s template (oauth.clientMetadataDocuments.client), never from the document. The consent offer carriesclient.origin,client.metadataUrlandclient.metadataHost: a consent UI must show the host next to the client’s name. The grant and its sessions carryclientData.oauth.client({ name?, host, url }). Discovery advertisesclient_id_metadata_document_supported: truewhile the mode is notoff. Operator tools:bin/oauth-client.js check-metadata,refresh-metadata,template-from,list --origin metadata-document. See client metadata documents. RS256client assertions.private_key_jwtacceptsRS256(RSA keys of 2048 bits or more) next toES256,ES384andRS384, for every client.- Revoke. A revoked client’s authorization codes and refresh tokens issued before the revoke stay refused when its id is registered again (or its metadata document row is re-created), and its users are asked for a new consent. The revoke time carries a five-second margin: keep the host that runs
bin/oauth-client.jsand the cores NTP-synced. A client id may no longer start withurl~. - Consent for an account served by another core. On a multi-core platform,
/oauth2/authorize/offerand/oauth2/authorize/acceptanswer421 { "error": "wrong_core", "error_description", "coreUrl" }when the signed-in account is served by another core than the one that issued the authorization (before: a uniform401/authenticated: false, read as an ended session, so the user was sent back to the sign-in). A consent UI ends the flow with a message and a way back to the app. See multi-core deployments. - OAuth CORS.
/oauth2/*and/.well-known/*answer CORS with their own policy: any origin, no credentials,POST, GET, OPTIONS,Content-Type, Authorization, DPoP; a preflight answers204.Access-Control-Expose-Headers: API-Versionis no longer sent there. The other routes keep the API’s CORS. - Server-created streams match the stream schema. The streams the server creates itself (the cross-account messaging tree
:_cmc:*,:_emails:, the delegation and shared-secret namespaces) carrycreated,createdBy,modifiedandmodifiedBy; streams stored earlier without them read with the data store defaults (10000000.00000001,'unknown'). Nothing to migrate. - Email lookups in any case (
platform.piiMode: cleartext). The username recovery (GET /reg/{email}/usernameand/uid), the core lookup (GET /reg/cores?email=) and the third-party sign-in’s account matching find the account in any case, as hashed mode does. - Socket.IO: a client that connected while another client of the same account was leaving no longer stops receiving change notifications.
- Security: a revoked OAuth client’s codes and refresh tokens worked again once its id was registered again. See the open-pryv.io security audit report.
2.0.0-rc.73
Section titled “2.0.0-rc.73”OAuth token exchange for resource servers, adapter routes (a sidecar served under the core’s own URL), the OAuth endpoints on a vanity issuer host, email uniqueness in any case on cleartext platforms, and backup restore follow-ups. No breaking change. Two new configuration keys, both off by default: adapterRoutes and oauth.audienceBoundSessions (a later release is planned to default it to true). No schema migration. Operators of platforms run with platform.piiMode: cleartext should run bin/reconcile-user-cores.js --emails --dry-run on each core after upgrading (below).
- OAuth token exchange (RFC 8693). A resource server in front of a core (an API facade, an agent gateway), registered as a registry client with
--can-exchange, trades the access token a user’s client sent it for a session of its own on the user’s core:POST /oauth2/tokenwithgrant_type=urn:ietf:params:oauth:grant-type:token-exchange,subject_tokenandsubject_token_type=urn:ietf:params:oauth:token-type:access_token. The answer carriesaccess_token,issued_token_type,expires_in,scopeandapi_base, no refresh token. A repeated exchange of the same subject token answers the same token while it lives. Advertise the grant by adding its URN tooauth.grantTypesSupported. See token exchange for resource servers. - Sessions bound to their resource server. A session minted by a registry client’s authorization naming a resource server (
aud/resource) records it (clientData.oauth.audience). Withoauth.audienceBoundSessions: truethe data API refuses such a token (403 invalid-access-token): it works only through that resource server’s exchange. Defaultfalsein this release; a later release is planned to default it totrue: set it explicitly tofalseon a core whose resource servers do not exchange yet. Audit eventsoauth.token.exchangedandoauth.token.exchange_refused. - Adapter routes. A sidecar on the same host as a core is served under
/adapter/<name>/(and optional/.well-known/<label>/adapter/<name>/prefixes) on every host the core serves, without an nginx in front. The core forwards every method, header and body and authenticates nothing on those paths: the sidecar must.adapteris now a reserved username; a core with a route refuses to boot while an account namedadapterexists. See adapter routes. - OAuth on a vanity issuer host. On a platform with the embedded DNS, the OAuth endpoints,
/.well-known/*, the third-party sign-in routes (/auth/sso/*) andPOST /users/DELETE /users/:usernameanswered 404 on a host such asauth-eu.<domain>(its first label was read as a username). They are now served on every host the core answers. The issuer’s label is refused as a username, a runtime DNS record name is no longer read as a username, andbin/check-config.js(and the boot log) check theoauth.issuerhost (open-pryv.io#167). - Email uniqueness in any case (
platform.piiMode: cleartext). Registration, the email availability check and challenge, an email change, adding an email and the/systemreservations refuse an address another account holds under another spelling (User@xnext touser@x), as hashed mode already did; no password-reset, MFA-change or auto-deletion mail goes to an address another account holds in any spelling. After upgrading, runnode bin/reconcile-user-cores.js --emails --dry-runon each core: it lists the pairs of accounts sharing an address up to case and names the other holder; change the email of one account of each pair before the next auto-deletion sweep. Hashed platforms (the default) need no step. - Backup restore. A
--verify-integrityrollback removes the accounts the restore created (an account overwritten with--overwriteis left emptied, with a warning to restore it from its own backup); a single-account backup carries the account’s SSO bindings, and a--userrestore links them again; a unique value holding/(an email such asa/b@x, an identity provider subject URL) is read whole by both platform engines (deletion and backups cut it before). A backup that names an accountadapteris refused at restore. - CMC: a single-use link is consumed before its invite reads
accepted, so an answer sent on seeingacceptedfails withcmc-capability-consumed. - Security: on cleartext platforms an email address was unique only as written, so a password-reset mail could reach the inbox of another account holding the address in another case. See the open-pryv.io security audit report.
2.0.0-rc.72
Section titled “2.0.0-rc.72”mfa/activate answers 200, backup restore that respects multi-core routing and the unique values other accounts hold, no password-reset or notice mail to an address another account holds, a vetted change for registry client launch types, faster SQLite audit export pages, and security fixes. One breaking change for API clients (mfa/activate, below). No configuration key and no schema migration in this release. Operators who restored accounts with bin/backup.js --restore --user on an earlier release should run bin/reconcile-user-cores.js --emails on each core (below).
- BREAKING (API clients):
mfa.activateanswers200 OKwith the same body (mfaToken, and fortotpalsomethod,otpauthUriandsecret). It answered302 Foundwith noLocationheader, inherited from the v1 MFA service, so a client that follows redirects or treats a 3xx as an error could not read the answer. A client that checks for302must accept200; a client that only reads the body is not affected. The MFA login (auth.loginon an account with MFA) already answered200with anmfaToken; the reference said302and is corrected. - Backup restore and multi-core routing. A restore reads the routing of each account before writing: with
--user, an account the platform routes to another core is refused (also with--overwrite); a whole-backup restore treats it as a conflict (--skip-conflictsskips it). A restored account claims the row routing its name to this core. Only the restored accounts’ platform rows are written, and a unique value (email, SSO binding) that another account holds now stays with that account (counted in the restore report). A--userrestore now writes the account’s platform rows (email, indexed fields; from a whole backup also its external-id namespaces and its v1 server mapping). - Restore and stopped erasures. A restore withdraws the erasure record of each account it restores (before, the next auto-deletion sweep erased the restored account again), puts it back when the restore fails, and refuses to run while an erasure of that account is under way. The release notes give the queries that find the records left on accounts restored earlier.
- Deletion on a multi-core platform leaves the platform rows of a username the platform routes to another core.
- No password-reset or notice mail to an address another account holds. The password reset, the MFA-change notice and the account auto-deletion notices are no longer sent to the account’s stored email when the platform reserves that address for another account (the case of an account restored from a backup older than a change of hands of its address). The reset answers as if the mail were sent; for auto-deletion such an account counts as one without an address (
mail: "no-email").node bin/reconcile-user-cores.js --emails [--dry-run]claims the missing email rows of a core’s accounts and lists the accounts whose email another account holds. - Registry OAuth clients: launch types are vetted.
bin/oauth-client.js update --launch-typeon an active registry client now moves it toreviewunless the command carries--approve.updatesays when it narrows what a client may be granted and prints thepurge-grantsstep that ends the grants given before. A registry client whose grant types no longer listrefresh_tokencan no longer refresh the chains issued before (unauthorized_client). - CMC: the chat and collector streams of an ended relationship can be deleted (guide, open-pryv.io#166); the membership access keeps the
displayNamea member gave when asking to join (clientData.cmc.counterparty.displayName); nothing is written for an account erased while one of its consent operations runs. - SQLite: export pages naming the audit streams no longer pay a full-text listing (a SQLite audit file of an earlier release lists its stream ids once, at its first open), and an
events.getwith a recentmodifiedSincereads the modified index. - Store streams carry
created,createdBy,modifiedandmodifiedBy, as the stream schema requires. - Security: a whole-backup restore could give a unique value that had changed hands back to the restored account (SSO first sign-in, email lookups); password-reset mails of a restored account could reach the new holder of its old address; an account erased during a consent operation kept a record holding a peer’s capability token. See the open-pryv.io security audit report.
2.0.0-rc.71
Section titled “2.0.0-rc.71”A vetting status for registry OAuth2 clients, client profile fields and regions, grouped consent entries, http://localhost as a loopback redirect host, and one security fix. Operators whose registry clients have logos must set oauth.logoHosts on every core before upgrading, and a change of a vetted client field now needs update --approve: read the breaking changes below. No schema migration in this release.
- BREAKING (operators): registry client vetting status.
bin/oauth-client.js updateon a vetted field (redirect URIs, scope, key set, auth method, assertion algorithms, grant types, audiences, allowed features, introspection role, lifetimes, offline access) moves an active registry client to the new statusreviewunless the same command carries--approve. A client under review, like a suspended one, gets an HTML 400 at/oauth2/authorize(no redirect),403 access_deniedat/offerand/accept,400 unauthorized_clientat/oauth2/token(a refresh token is not used up),active: falseat introspection, and its live access tokens are refused by the data API (403 invalid-access-token) withinoauth.clientRevokeCheckSeconds(default 30 s). Sessions are kept and work again once the client is active. Every status change is kept in the row’sstatusLog, printed byshow. - BREAKING (operators with client logos):
oauth.logoHosts. A new list of the hosts client logos may come from (default empty).--logo-urimust be https on one of them, and the consent offer serves a logo only when it is. Set it on every core, identical everywhere, before upgrading, or no logo is shown. client_credentialsrefuses a suspended client (400 unauthorized_client), as the other grants already did.- Registry client profile:
--localized-name,--description/--localized-description,--software-id,--max-entries(the ceiling on the entries one authorization may request and on the grant a consent leaves, 1 to 1000)./oauth2/authorize/offeranswers carryclient.nameandclient.description(a text or a map by language code)./oauth2/authorize/acceptrefuses a consent whose resulting grant exceeds the ceiling (400 invalid_scope,reason: 'grant-too-large'), with or withoutshownDigest, writing nothing. - Regions:
--region <key>binds a client to regions of the platform’shostingshierarchy; a core of another region refuses it (unauthorized_client). purge-grants:node bin/oauth-client.js purge-grants <clientId> --yesdeletes, on one core, every grant of a client (CMC data-grants its sessions are bound to included) with its sessions, the accesses they created and itsclient_credentialstokens.revokeandsuspendprint it as the next step; run it on each core.- Removed:
oauth.requireAppAccountMfa. It was never enforced; a core where it is still set logs a warning at boot. - Grouped consent entries. A registry client’s offer may carry
entryGroups({ key, label, entries }, display only, part of the offer digests), and entry keys may be level-less (s|<streamId>,f|<feature>). A whole offer is bounded to 256 KiB of JSON; a registered scope breaking the bounds refuses every request of that client and is reported at boot. http://localhostis a loopback redirect host: any port, like127.0.0.1and[::1]; the three forms are not interchangeable, register each one your client sends. A redirect URI containing a control character is refused.- Security: a suspended client could still mint
client_credentialstokens on its own app account; suspension now also reaches the data API; client names and descriptions refuse control and direction-override characters. See the open-pryv.io security audit report. - Platform integrity check: reports orphan rows on SQLite too.
2.0.0-rc.70
Section titled “2.0.0-rc.70”OAuth2 client authentication with RS384 / ES384 and key sets by reference, per-client token lifetimes, what an OAuth2 session may create, RFC 8707 resource indicators, and one security fix. The two state keys are now required where their feature is on, and registry OAuth2 clients whose sessions create accesses need --allow-feature accessCreation: read the breaking changes below before upgrading. Schema migration: PostgreSQL adds the column accesses.last_login (migration v4). It runs at boot when migrations.autoRunOnStart is true (the default); with it false, run node bin/migrate.js up BEFORE switching to 2.0.0-rc.70, or password sign-in fails until it has run. SQLite needs none.
- BREAKING (configuration): the state keys are required. A core refuses to boot when
oauth.consentUrlis set withoutoauth.stateKey, orsso.enabledis true withoutsso.stateKey(an empty, placeholder or unexpanded value; in production also one shorter than 16 characters);bin/check-config.jsreports the same. The fallback on the admin key is removed. Generate each withopenssl rand -base64 32, one per key and per core. A core whose 2.0.0-rc.69 boot log showed no state-key warning already has them. - BREAKING (registry OAuth2 clients):
accessCreation. A new feature permission,{ feature: 'accessCreation', setting: 'forbidden' }, forbids creating accesses, webhooks and CMC capability requests (403 forbidden); the access still manages the accesses it created. Registry grants forbid it by default, applied at the user’s next consent. Before upgrading, runnode bin/oauth-client.js update <clientId> --allow-feature accessCreationfor every registry client whose sessions create accesses, webhooks or invitations (keep the features already allowed in the same command). - Accesses an OAuth2 session creates belong to its grant. They carry
clientData.oauth.dataGrantId; when the user narrows the grant they are narrowed with it (deleted when nothing is left), and the grant’s deletion deletes them.accesses.updatecannot remove the binding (403). private_key_jwt: RS384 and ES384, pinned algorithms, key sets by reference. A client’s key set may hold EC P-256, EC P-384 and RSA (2048 bits or more) keys;--signing-algpins the accepted algorithms;--jwks-uriregisters a key set by reference (https only, fetched with the core’s counterparty-call guard, cached 5 minutes). BREAKING: a key set of more than one key needs a distinctkidon every key, and the assertion must name its key; a client registered with several kid-less keys getsinvalid_clientuntil re-registered (every core reports such rows at boot).client_idmay be left out next to aclient_assertion.- Per-client token lifetimes and refresh tokens.
--access-token-ttl,--refresh-token-ttl,--refresh-token-absolute-ttl(capped by the platform’soauthvalues) and--offline-access false(no refresh token for that client). - Client secret rotation window.
bin/oauth-client.js rotate-secret <clientId> --previous-valid-for <seconds>(at most 30 days) keeps the replaced secret valid for that long. - RFC 8707 resource indicators.
/oauth2/authorizeacceptsresourceas a synonym ofaud, and/oauth2/tokenchecks it on the code and refresh grants. BREAKING: an audience refusal answersinvalid_targetinstead ofinvalid_request; a malformedaudfrom a client without an audience list is now refused. - OAuth extension: grant state. A new optional hook stores per-grant state (up to 16 KiB of JSON) at consent and hands it back to the echo and the response hooks; the response hooks also receive the authorized resource.
- Security:
/oauth2/authorizeno longer spends super-linear time on a longaud(one unauthenticated request could hold a worker for seconds, for clients registered with a list of audiences);aud/resourcevalues are size-capped./oauth2/authorizerefuses ascopelonger than 8192 bytes. Deleting the session a refresh just replaced no longer hides a later replay of the replaced refresh token. See the open-pryv.io security audit report. - OpenID Connect: the id_token
auth_timeis the user’s last completed sign-in (new read-only personal-access fieldlastLogin). accesses.createrefuseslastUsed(invalid-parameters-format); account activity ignores alastUsedmore than an hour in the future.
2.0.0-rc.69
Section titled “2.0.0-rc.69”OAuth2 token introspection and revocation, OpenID Connect for registry clients, pseudonymous OAuth2 subjects with alias-bound addressing, and two new per-core state keys: set oauth.stateKey (and sso.stateKey where third-party sign-in is on), then upgrade every core. No migration.
- Token introspection (new, opt-in).
POST /oauth2/introspect(RFC 7662) answers resource servers: a confidential client the operator registered with--can-introspect(bin/oauth-client.js create-registryorupdate). An active answer carriesscope,client_id,exp,iat,sub,iss,token_type,aud,cnf(DPoP-bound sessions), plus two Pryv members:api_base(the account’s API base URL, no credential in it) and, for a registry client’s session,permissions: [{ streamId, level }](what the session holds now, as the core enforces it). A deleted or expired session, a withdrawn consent or a revoked or suspended client answers{ "active": false }; a token whose session lives on another core answers421 { "error": "wrong_core", "coreUrl": "<home core>" }: ask that core. Any other caller gets401 invalid_client. - Token revocation (new).
POST /oauth2/revoke(RFC 7009): a client revokes its own access or refresh token, which ends that refresh chain only; the user’s consent and the client’s other chains (other devices) stay. An unknown token, another client’s, or one already ended answers200; a chain on another core answers421with itscoreUrl. Both endpoints are in the discovery document. - The life of a refresh chain. A refresh now retires the access token it replaces
oauth.refreshRetireGraceSecondsafter it (default60) instead of at its own expiry: switch to the new token at once. Deleting an OAuth2 session (from the account’s access list, by a grant change, or by a revocation) ends its refresh chain. A refresh with a wrong client secret or assertion, or presenting another client’s refresh token, no longer uses the token up (before, the legitimate app’s next refresh was taken for theft and the user’s consent withdrawn). A refresh refused for good no longer makes a later replay look like theft. Sessions minted by earlier releases are not linked to their chain and expire on their own. - OpenID Connect for registry clients (new, opt-in). On when
oauth.signingKeyslists at least one RSA key (RS256): theopenidscope, anid_tokenat the code exchange (never on refresh;nonceaccepted once per client),GET /.well-known/openid-configurationandGET /oauth2/jwks. Key rotation by ordering: the first listed key signs, every listed key is published. - Pseudonymous subjects and alias-bound addressing. A
pairwiseregistry client (--subject-type pairwise, refused in 2.0.0-rc.68), or an offer asking for a pseudonym, gets an aliased grant: one alias per user and client, kept across refreshes and re-consents, given assuband as the subject of every answer; the token responses carry noapiEndpoint, and the username reaches the app in no answer. BREAKING: an access carrying an alias (randomAlias, a pseudonymous OAuth2 session, or an access such an access created, which now carries its creator’s alias) is accepted at its alias only, and an access without an alias at the username and former usernames only; any other use answers as an unknown token (403 invalid-access-token). The owner’s personal token works at every name. A client that called the username with an aliased token must use the access’s ownapiEndpoint. - Security. Sign-in and password reset addressed by an access alias are refused with one answer (
400 invalid-operation), so an alias no longer confirms a guessed username; the consent-flow state and the third-party sign-in state cookie are signed with their own per-core keys instead of keys derived from the admin key; refresh audit rows identify a token by a digest, not its first characters. See the open-pryv.io security audit report. - Registry clients: on a re-authorization with per-entry choice, an entry the user’s current grant does not hold opens unticked unless the request marks it mandatory. The composed consent screen shows a feature the client’s grants forbid by default as the locked restriction it becomes.
- Operator CLI fixes.
bin/oauth-client.js updateapplies every registry flag (--policy-uri,--tos-uri,--contact,--extra,--launch-type,--auth-methodwith its credentials); a key set is accepted withprivate_key_jwtonly; unknown flags are refused;rotate-secretrefuses a client that uses no secret;showprints"clientSecretHash": "(set)"instead of the hash. - Dependency:
follow-redirects1.16.2 (GHSA-mrwp-f3q8-3g9p). - Operators: set
oauth.stateKeyon every core andsso.stateKeyon every core wheresso.enabledis true (random values of at least 32 characters, distinct from each other and from the admin key, e.g.openssl rand -base64 32). Until set, both are derived fromauth.adminAccessKeyand every boot logs a warning; 2.0.0-rc.70 requiresoauth.stateKeywhenoauth.consentUrlis set andsso.stateKeywhensso.enabledis true. Setting a key only cancels the consent screens or sign-ins open at that moment.oauth.audAllowListis removed (it was never enforced; audiences are registered per client withcreate-registry --audience); a configuration that still sets it gets a boot warning. New optional keys:oauth.refreshRetireGraceSeconds,oauth.signingKeys,oauth.idTokenTTL(default300). See OAuth2, third-party sign-in and the open-pryv.ioCHANGELOG-v2.md.
2.0.0-rc.68
Section titled “2.0.0-rc.68”OAuth2 clients registered by the operator (new, opt-in, through an OAuth extension), consent screens composed with what the app already holds, and OAuth2 consent hardening: upgrade every core. No migration and no configuration change required.
- OAuth2 registry clients (new, opt-in). An operator can register an OAuth2 client that has no app account:
node bin/oauth-client.js create-registry <clientId> --redirect-uri <uri> --scope <scope> --subject-type public --audience <url>(--subject-typeis required; at least one--audience, or--any-audience; plain-http redirect URIs only on a loopback host unless--allow-insecure-redirect).suspend/resumerefuse or restore a client everywhere;update,showandlistknow the new fields. The client’s scope string is mapped to Pryv permissions by an OAuth extension module (customExtensions.oauthExtension, options inoauth.extensionOptions) loaded at boot; without one, every registry client is refused withinvalid_scope. Registry grants forbidwebhooksandsecretSharingunless the client allows them (--allow-feature); pseudonymous subjects (--subject-type pairwise) are refused for now. Details in the open-pryv.ioCHANGELOG-v2.md. - Consent UIs: offer by reference and composed re-consent.
POST /oauth2/authorize/offerwith{ state }answers{ client, offer, scope }for a registry client (the state carries the offer’s digest only; a state whose client or grammar changed since is refused: restart the flow). Sent with the owner’susernameanduserToken(the personal token), it answers the consent screen composed with the user’s current grant: stream entries already granted carrycurrentlyGrantedandcurrentLevel(the level held now), plusauthenticated: true, an opaqueshownDigest,currentGrantandsubjectChange; another token gets the request alone withauthenticated: false.POST /oauth2/authorize/accepttakesshownDigest(the grant then becomes exactly what the user ticked, so unticking narrows it) andacknowledgeSubjectChange: truewhen the identity mode changes. Both fields are ignored for app-account clients. reasonon/oauth2/authorize/*errors. An error body may carry a machine-readablereasonbesideerror:consent-out-of-date(restart the flow),subject-change-unacknowledged,permission-level-conflict,keep-one-stream,consent-widens-offer. Match onreason, never onerror_description.- Security: OAuth2 consents hardened. The session minted at consent starts under the restrictions the owner set on the grant (exclusions, forbidden features), as refreshed sessions do; a change to a grant (the owner’s
accesses.update, a re-consent, a consent scope update) reaches the app’s live access tokens at once instead of at their next refresh; a forbidden feature in a consent offer is always mandatory and one markedoptInis refused (OAuth2 and consent offers, and theconsentpart of an app access request); only the owner’s personal token can consent (403 access_deniedotherwise). See OAuth2. - BREAKING (for a client that wrote it; none known):
clientData.oauthis server-owned.accesses.createrefuses an access carrying it andaccesses.updaterefuses any change to it (403 forbidden); sending it back unchanged passes. The session minted at consent hascreatedBy: systemand no longer adds anaccesses.createrow to the user’s audit log (the consent is recorded byoauth.consent.granted). - OAuth2 refresh: a refresh that fails on the server side no longer uses up the refresh token: retry it. A
consent/request-cmcoffer naming one stream twice is refused at write. - Operators: nothing required. New optional key
oauth.extensionHookTimeoutMs(default5000) bounds each extension hook. Each OAuth2 session mint writes one more replicated platform row (a session index row: no token, no username), and sessions minted by a refresh or byclient_credentialsare now in the breach-scope access index.
2.0.0-rc.66
Section titled “2.0.0-rc.66”One breaking configuration change (dns.domain needs an explicit dnsLess.isActive), security hardening of access aliases and username changes, OAuth2 re-consents and refreshed sessions that combine permissions per stream, and account auto-deletion: run check-config before restarting, then upgrade every core. No migration; optional one-time repair tools.
- BREAKING (configuration):
dns.domainwithdnsLess.isActiveunset refuses to boot. Such a core was silently broken (multi-core identity and per-user subdomain service URLs, path-style routing). SetdnsLess.isActiveexplicitly, or dropdns.domainon a DNSless multi-core member, and runnode bin/check-config.json your configuration before restarting. See DNS configuration. - Security: aliases and username changes hardened. Backup and restore keep each account’s aliases (access aliases and former usernames), so they keep routing and a former username cannot be registered by someone else after a restore; take a new backup after upgrading. A username change refuses a name taken while it runs (
409 item-already-exists), undoes itself when a step fails, and applies once when sent twice at once; its result carrieswarnings. A refusedaccesses.createkeeps no alias, and an account holds at mostaccesses.maxAliasesPerAccountaliases (default1000). Custom account streams may not be namedusernameorusernameChangeCount. Sign-in works with a former username, HF series follow the username, sockets opened before a change are disconnected, and on a single core serving its own DNS aliases and changed usernames resolve. See aliases and username changes, including the one-time repairs after upgrading (bin/reconcile-user-cores.js,bin/gather-former-series.js). - Security: OAuth2 refreshed sessions stay within the grant. A refresh now keeps, stream by stream, what both the session’s consent and the current grant allow: an exclusion the user added to the grant below a stream the session holds, or a feature restriction added to the grant, now reaches the refreshed token, and a child stream the session held at a narrower level keeps it. A re-authorization merges into the grant per stream; one combining
readandcreate-onlyon a stream is refused with400 invalid_scope, as is agrantedPermissionsnaming a stream twice. See OAuth2. accesses.createandaccesses.updaterefuse two permissions on the same stream (in any spelling) with400 invalid-request-structure: only the last one applied. A client that writes back a stored access’s permissions unchanged must keep one entry per stream.- Account auto-deletion (new):
account.getAutoDeletion/account.setAutoDeletionand a per-core sweep with reminder mails, configured inaccount.autoDeletion. See account auto-deletion. - Operators:
bin/check-config.jsreads the shipped defaults and evaluates the configuration as boot does; the platform integrity check now reports user ids unknown to the users index; an account erasure interrupted half-way is completed later;auth.loginsetslastUsedon the personal access it issues.
2.0.0-rc.65
Section titled “2.0.0-rc.65”One breaking security change (account fields are written by the personal access only), a consent-integrity fix, and fixes for deletion records, OAuth2 token timing and the SQLite engine: upgrade every core. No migration.
- BREAKING (security): account fields are written by the personal access only. App and shared accesses, including those granted through an app access request, a consent offer or an OAuth2 authorization, are limited to
read(ornone) on account streams.accesses.create/accesses.updaterefuse a higher level with400 invalid-operation; a consent offer holding one, or a hidden or unknown account stream, fails at accept withcmc-offer-invalid-permissions. An access already stored with a higher level keeps reading, but its writes on account streams are refused with403 forbidden(a write on the email field by an app or shared access now also answers403, instead of400). Apps that set account fields for the user must ask forreadand let the user change them in a personal session (the account app oraccount.update). - Security: a consent refused for its permissions is final. A consent accept or a scope update refused for its permissions was retried without the checks that refused it, and could complete shortly after being reported as failed. These refusals are no longer retried.
- Deletion records of internal streams are no longer returned to non-personal accesses, in
events.getwithincludeDeletionsand instreams.getwithincludeDeletionsSince. The filter applies afterlimitandskip: a page can hold fewer records thanlimit, so a sync client must not treat a short page as the end of the listing. - OAuth2: a token minted right after an operator revoke of its client is no longer refused as revoked, and the refresh grant refuses a consent that expired less than a second ago.
- SQLite engine:
events.getOneno longer serves a history version by its own storage id. - Operators:
oauth.consentUrlis declared in the default configuration and asked by the install wizard; an empty value is reported at boot and bycheck-config.
2.0.0-rc.64
Section titled “2.0.0-rc.64”Two security fixes (OAuth2 consent withdrawal, event history reads) and fixes for OAuth2 confidential clients, the SQLite engine, deletion records, notification scopes and streamed errors: upgrade every core. No migration.
- Security: withdrawing an OAuth2 consent ends the app’s live tokens at once. Deleting the consent data grant of an OAuth2 client (from the account app,
accesses.delete, or a revoke from the client) ended the refresh chain but left the client’s current session accesses valid until their own expiry (one hour by default). They are now deleted with the grant, together with the accesses they created, and listed in the answer’srelatedDeletions. A session access names its grant inclientData.oauth.dataGrantId, whichaccesses.updatecannot change or remove (403). Sessions minted before the upgrade expire on their own. - Security:
events.getOnewithincludeHistoryno longer returns versions the access cannot read. Each earlier version now goes through the read check of the event itself; a version that fails it is left out ofhistory. /oauth2/tokenaccepts a confidential client identified by HTTP Basic alone, without repeatingclient_idin the body.events.getwithincludeDeletionsno longer returns the deletion record of an event that also sits in a stream the access cannot read.- Notification scopes match
class/*event types asevents.getdoes. - A streamed answer whose storage fails before the first byte returns a JSON error instead of a dropped connection.
- SQLite engine:
events.getno longer fails when an excluded subtree holds more than about 250 streams, and stream ids are escaped in the full-text query.
2.0.0-rc.63
Section titled “2.0.0-rc.63”Cross-account messaging: the revoke notices of a deleted account are no longer lost, and a failed accept no longer leaves its data grant behind. Also stream and access fixes, a 400 for a refused mail address, and dependency security updates: upgrade every core. No migration; the new outbox table is created at boot.
- CMC: the revoke notices of an account being deleted are queued and re-sent. A
consent/revoke-cmcnotice whose counterparty core could not be reached during the deletion (network error, timeout, 5xx, 408 / 429) was logged and lost, so the counterparty kept the relationship as active. It is now held in a core-level outbox that outlives the account and re-sent on the schedule of any other revoke notice (8 re-sends over about 27 hours, thenfailed-permanentfor the operator). Nothing is queued when the retry loop is off. See Revoking. - CMC: a revoke notice answered
403 invalid-access-tokencounts as delivered. The counterparty already deleted the access the notice is sent through (it revoked first): a revoke trigger now reportscontent.peerNotified: truewith nodeliveryFailure, and a queued notice is markedsucceeded. Any other 4xx, including403 forbidden, is still a failure. - CMC: an accept that fails for good no longer leaves its data grant behind. When an accept’s retries end
failed-permanent, the retry loop deletes the data grant it minted, unless the grant already carries the requester’s back-channel (the relationship did form). A retried accept refused ascmc-capability-consumedorcmc-capability-already-accepted-by-youon such a grant now completes instead of rolling the live grant back. streams.createkeeps a valid predefined id as given. An id of lowercase letters, digits and dashes (1 to 100 characters) is stored exactly as sent; a double dash was collapsed (a--bbecamea-b) and leading or trailing dashes dropped. Other ids are still slugified. Re-sending such an id now creates that stream instead of answering409on the normalised one.accesses.updatecreates a missing stream from a permission’sdefaultName, asaccesses.createdoes, and on both methods adefaultNameno longer creates a stream with a reserved id (400 invalid-item-id, asstreams.create).- A mail address the SMTP server rejects answers
400, not500, where the address is the caller’s own input: the registration email code and the verification resend answer400 invalid-parameters-formatwithdata: { email, reason: 'recipient-rejected' }. The password reset keeps the generic500. See Email verification. - Security: dependencies.
node-forge1.4.2 (GHSA-86w9-cpqp-85rv, RSA PKCS#1 v1.5 signature verification),qs6.16.1 (GHSA-6pq8-jhm2-2vcw, denial of service inqs.stringify, not called by the server at runtime).
2.0.0-rc.62
Section titled “2.0.0-rc.62”Multi-core registration answers: a registration forwarded to another core now carries that core’s refusal to the client, and a forward that cannot complete answers 503. No migration, no configuration change.
- Multi-core registration: a refusal from the core the registration is forwarded to reaches the client with its own status. A
POST /usersthat the landing core forwards to another core (the account’s hosting is on that core) answered500 unexpected-errorwhenever that core refused it, for example409 item-already-existsfor a taken username or409 external-id-already-claimed. Such a refusal now reaches the client with that core’s status, error id, message anddata. A forward the other core does not accept, an answer that is not an API error (a proxy’s page), a 5xx from that core, or a core that cannot be reached answers503 cross-core-registration-failed: nothing is created, retry later. Seeusers.createerrors. - A restore no longer leaves an attachment it does not restore open behind it (an encrypted backup kept one file descriptor per such attachment).
2.0.0-rc.61
Section titled “2.0.0-rc.61”Cross-account messaging fixes for rate-limited counterparties, and the register host now serves three documented methods it answered 404 for. No migration, no configuration change.
- CMC: a peer answering 408 or 429 is retried, no earlier than its
Retry-After. Every 4xx used to be permanent, so a counterparty behind a rate-limiting or slow proxy lost an accept, refusal, chat, system message, revoke notice or member-access deletion for good. A 408 or 429 is now retryable for every delivery kind:failure.reason: 'cmc-handler-delivery-failed'with'http-retry-later'infailure.detail.reason(accept, refusal) orfailure.detail.peerReason(chat, system), plusretryAfterMswhen the peer sent aRetry-After. The retry queue waits at least that long (capped at 10 minutes for a trigger, 30 minutes for a queued revoke notice). A member-access mint answered 408 or 429 fails withcmc-member-access-peer-unreachable(wascmc-member-access-peer-rejected), withfailure.detail.retryAfterMswhen given. A 503 (already retried) now honours itsRetry-Afterthe same way. Every other 4xx stays permanent. - CMC: a refusal answered by the capability itself fails with its typed id and is not retried. A
consent/refuse-cmcthe requester’s core refuses with a 4xx now fails as an accept does (cmc-capability-consumed,cmc-capability-invalidated,cmc-capability-already-accepted-by-you, orcmc-handler-delivery-rejectedfor any other 4xx, withfailure.detail: { status, body }), and is sent once. It used to fail ascmc-handler-delivery-failedand be re-sent up to six more times. - Register host:
GET /apps,GET /apps/{appid}andPOST /access/invitationtoken/checkanswered 404 onreg.<domain>(the request fell through to the user route). They are now served there as documented, and still at the root of a core. - A mail that fails to send (for example a recipient the mail server refuses) is logged once, at its level, instead of twice.
2.0.0-rc.60
Section titled “2.0.0-rc.60”External ids: the administrator can release an id claimed by the wrong account, the per-user backup carries the linked ids, and a core rename moves them. No migration. Deployments whose mail templates were seeded earlier add the new external-id-released template with bin/mail.js templates set.
- External ids: releasing an id claimed by the wrong account.
POST /system/admin/ext-id/release { namespace, blindedValue, reason }(admin key), sent to the core that holds the id, releases it from the account holding it and answers{ released: true, notified: 'mail' | 'audit-only' }, never naming the account (404when no account holds it). The account gets anaccount.externalIds.adminReleaseentry in its own audit trail (namespace and reason, never the value) and the newexternal-id-releasedmail when mail is configured (services.email.enabled.externalIdReleased, templateservices.email.externalIdReleasedTemplate; variablesUSERNAME,NAMESPACE_LABEL,REASON). A failed mail never undoes the release. A core that does not hold the id answers307to the one that does ({ server }). See Releasing an id claimed by the wrong account. - External ids: per-user backup and core rename. The per-user backup of
bin/backup.jscarries each account’s linked ids (namespace, blinded value, claim date); a restore links them again on the restoring core, and an id another account holds by then stays with it (counted as lost in the restore report). Renaming a core (/reg/admin/servers/:src/rename/:dst) moves its external-id index rows; the answer gainsexternalIds(the number moved). See Operators.
2.0.0-rc.59
Section titled “2.0.0-rc.59”External ids in forced mode, a fix of the identity host of dnsLess cores configured with core.url only, and a dependency security update: upgrade every core. No migration.
- External ids: forced mode.
externalId.mode: 'forced'(or a namespace whosedefaultModeisforced) is now accepted onPOST /reg/accessandPOST /users; it answeredforced-mode-not-availableuntil now. On an auth request, only the account that holds the id receives the credential: atACCEPTEDthe core checks that the posted token is an app access of the postedusernameand that this account holds the id, otherwise400 external-id-mismatch(data: { namespace, reason: 'token-invalid' | 'not-holder' }) and the request staysNEED_SIGNIN. A check that cannot run answers503 external-id-check-unavailable, never a pass. A forced acceptance refuses the page-madehandoffandexternalIdDeclined(400 invalid-parameters);not-holderrefusals count in the external-id lookup budget. A forced registration with an id any account holds is refused with409 external-id-already-claimed(reason: 'value-taken') before anything is reserved; an unclaimed id is linked. See Forced mode. - CMC and delegation identity host on a dnsLess core configured with
core.urlonly. The host a core presents to counterparties (CMC relationships, delegations, OAuth revoke notices) now falls back to the host ofcore:urlbeforelocalhostwhen neitherdns.domainnor aserviceblock is configured. Such a core presented itself aslocalhost, which broke cross-core relationships. Every valid configuration resolves to the same host as before. - Security: dependencies.
nodemailer10.2.1 (GHSA-8c8m-p3m8-cjg8, GHSA-m943-m63m-w62w, GHSA-85x2-q946-cp5h: address-parser flaws that could turn display text into a recipient). unknown-resourcemessages without an id no longer end with a space (Unknown external id). The error id and status are unchanged.
2.0.0-rc.58
Section titled “2.0.0-rc.58”External ids: a service links a third-party record id (an EHR patient id, for example) to exactly one account on the platform, without the platform ever seeing the id. Also a cross-account messaging fix and dependency security updates (denial of service in request parsing and uploads): upgrade every core. No migration; the new tables are created at boot. Runs on Node.js 24.22.1 or later, as 2.0.0-rc.57.
- External ids: one account per record id, platform-wide. The service sends a blinded value
HMAC(K_namespace, id)computed with its own key. Operators create namespaces withPUT /system/admin/ext-id-ns/<namespace>; an account links an id withaccount.externalIds.claim(personal token), lists, replaces and releases them;POST /reg/ext-id/coresfinds the core that holds an id. One id per account per namespace, and one account per id across all cores: a second account gets409 external-id-already-claimed, never told which account holds it. Lookups and refused claims share a budget of 10 a minute per client address. See External ids. - External ids in sign-in and registration (hint mode).
POST /reg/accessandPOST /usersacceptexternalId: { namespace, blindedValue, mode? }. On an auth request, the auth page asks the user whether to link the record to the account that signs in; “Not now” postsexternalIdDeclined: true, which the app then reads in the accepted state. A registration links the id to the new account when no other account holds it and says so inexternalIdLinked. Theforcedmode is refused for now (forced-mode-not-available). See Sign-in and registration, hint mode. - Cross-account messaging: revoke notices. On a
consent/revoke-cmcarriving in:_cmc:inbox, the fields the receiving server owns (superseded,backChannelAccessId,dataGrantAccessId,revokedAccessIds) are always written by that server, never taken from the sender. Before, a sender could mark its own revokesuperseded, and apps then showed a torn-down relationship as still live. - Native installs: replacing the bundled rqlite.
storages/engines/rqlite/scripts/setupno longer moves an olderrqlitedleft atvar-pryv/rqlite-bin/(installs that predate 2.0.0-rc.1) intobin-ext/: only a binary of the pinned version is moved, any other is renamed aside and the pinned version downloaded. Before, following the replacement steps on such a host installed that older binary, which cannot open a 10.x data directory. See INSTALL, Upgrading the bundled rqlite, which now also describes checking the new binary before stopping the core. - Security: dependencies.
body-parser1.20.9 (GHSA-27r3-jxmh-p828, denial of service through trailing bytes after a compressed request body),multer2.5.0 (GHSA-q7qp-rjg7-q26m, denial of service through a pending upload filter),undici6.29.1 (GHSA-4qv8-2p9j-2685, GHSA-xjj6-f33q-929c),fast-uri3.1.9 (moderate URI parsing advisories).
2.0.0-rc.57
Section titled “2.0.0-rc.57”Security release, runtime only: Node.js 24.22.1. No API change since 2.0.0-rc.56, no migration. Docker: pull the new image. Native installs: upgrade Node to 24.22.1 or later and run npm rebuild (a host held on 24.18.1 per the earlier advice must be released first: sudo apt-mark unhold nodejs); a native host can take the Node upgrade before this release, 2.0.0-rc.56 runs on 24.22.1 too.
- Security: Node.js 24.22.1. The Docker image moves from Node 24.18.1 to 24.22.1, which carries the Node.js security release of 2026-10-21: four high-severity fixes, among them an out-of-bounds write in the Brotli decoder that can lead to remote code execution, two TLS certificate hostname check bypasses and
dns.resolvetruncating names at a NUL character. The server decodes the responses of its outbound HTTP calls (consent counterparties and webhook targets among them), so the Brotli flaw is reachable.engines.nodeis now>=24.22.1 <25.0.0; the server warns at boot andbin/check-config.jsreports a problem on an older Node. The SQLite abort on Node 24.19 to 24.21 (nodejs/node#65446), the reason for the earlier 24.18 pin, does not occur on 24.22.1 (#162). See INSTALL, Node major bumps. - Dependencies.
follow-redirects1.16.1 (GHSA-8r9p-f939-6h3c, GHSA-q3jh-wrc6-jgq8), reached through the ACME client.
2.0.0-rc.56
Section titled “2.0.0-rc.56”Security release: upgrade every core. It closes authorization gaps in the consent (CMC) and account-delegation namespaces and in the account-field checks, refuses calls to a counterparty that would reach a private address, and tightens stream moves. Operators still on 2.0.0-rc.53 to rc.55 who cannot upgrade at once can set cmc.retryLoop.enabled: false and restart until they do: while it is off, a consent delivery that fails is not retried later and expired group memberships are not swept (member accesses end at their own expiry); set it back to true after the upgrade. This setting covers only the retry-queue part: the other fixes in this release (the reserved-region and account-field checks, the counterparty destination guard, stream moves) have no configuration workaround and need the upgrade. No migration.
- Security: store-prefixed stream ids. A stream id has store-prefixed spellings that address the same stream (
:local:<id>for<id>,:account::system:<field>for:system:<field>). Every guard now judges the canonical spelling: the plugin-managed regions of:_cmc:and:_delegation:(astreams.createorstreams.updatewith aparentIdinside them is refused,400 invalid-operation,cmc-reserved-stream/delegation-reserved-stream, for every token; a plugin-managed stream cannot be moved or trashed), and the account fields onevents.create,events.update,events.delete,accesses.createandaccesses.update(an id routing to the account store that names no known account field is an unknown stream; a permission given through an alias is stored under the canonical id). Requests that use the canonical ids are unchanged. - Security: the CMC retry worker runs only records the server wrote. Any other event in the internal retry stream is retired without being processed.
- Security: calls to a counterparty. Consent deliveries, chat, revocations, scope updates, the capability read of an accept, delegation notices and OAuth revocation notices are refused before any connection when the URL’s host is or resolves to a loopback, private, link-local or otherwise reserved address, uses
http:(unlesscluster.allowInsecurePeerUrl) or carries credentials, and when the answer is a redirect or larger than 1 MiB. The platform’s own host names are always allowed; peers on a private network go in the newcmc.allowedPrivateHosts(host names, IPs or CIDR ranges, aswebhooks.allowedPrivateHosts). A refused delivery is permanent:cmc-handler-destination-refused, orcmc-capability-destination-refusedwhen an accept’s offer read is refused. - Security: moving a stream.
streams.updatewith a newparentIdby a non-personal access needs manage rights on the whole moved subtree as well as on the target parent (403 forbiddenotherwise); aparentIdnaming one of the stream’s descendants is refused (400 invalid-operation). - Withdrawing a single-use invite.
consent/invalidate-link-cmcnow withdraws a single-use invite nobody accepted yet (#160): the trigger’scontent.statusbecomesinvalidatedand a later accept fails withcmc-capability-invalidated; on an accepted invite it stays a no-op. The offer text stays readable through the link until it expires, so set a shortexpiresAton invites you may withdraw. Clients that enumerate invite statuses:invalidatedis no longer open-link only. - Usernames and platform names. Registration, the username check and change, admin pre-registration and delegated account creation refuse a name equal to a core id, a
dns.staticEntrieskey or a runtime DNS record; a new DNS record or core id under an existing account’s name is refused (POST /reg/recordsanswers409 item-already-exists). - Access deletion records (operators). New per-core setting
accesses.deletionRecords.retentionDays(default 0, kept until the account is deleted): above 0, deletion records older than that are purged once a day, after which audit pages show such an access by id only.
2.0.0-rc.55
Section titled “2.0.0-rc.55”Docker image on Debian 13 with rqlite 10.6.0, log masking hardened, and a core without an SMTP host now treats mail as not configured instead of logging an error for every registration. Operators of native installs: replace the bundled rqlited binary yourself (below). No migration.
- Docker image: Debian 13 (trixie). The image is based on Debian 13 instead of Debian 12 (bookworm), with the same Node 24.18.1: Debian 12 left its remaining critical and high OS-package advisories unfixed. Images built
FROM pryvio/open-pryv.iothatapt-get installpackages now resolve them against trixie. - rqlite 10.6.0. The image bundles rqlite 10.6.0 (was 10.5.1). Within 10.x no data-directory step is needed; in a cluster, upgrade the followers first and the leader last. Native installs:
storages/engines/rqlite/scripts/setupnow pins 10.6.0 and checks the download’s sha256, but never replaces an existing binary, so replacebin-ext/rqlitedyourself while the core is stopped. See Upgrading the platform DB’s rqlite and INSTALL, Upgrading the bundled rqlite. - Security: log masking hardened. Credential keys (
token,password, …) andauth=/ URL credentials are now masked in contexts that were written as is: a context holding a BigInt (the JSON console also threw on it), and a context that cannot be turned into JSON (a circular object such as an HTTP request, or one whosetoJSONor a getter throws). In such a context a cycle shows as[Circular], an unreadable branch as[Unserialisable], nesting beyond 32 levels as[Too deep]and what lies beyond 20,000 values as[Truncated]. A message that is not a string (such as a URL holding a token) is masked like a text message, a function logged as context prints as[Function: name], and credential keys inside a text (a logged JSON body) are hidden whatever their case, for every key hidden as an object property (addingsecret,key,authorization,cookie,set-cookieand others). Contexts that convert to JSON print as before. - Logs: Error contexts in the log file. An error logged with its Error object carries the error’s name, masked message and stack in the
contextfield oflogs.file(it was{}), as the JSON console does since 2.0.0-rc.54. - Logs: no boot-time
[winston]line. A process start no longer prints[winston] Attempt to write logs with no transports ...when nologs.customis configured. Nothing was lost. - Mail without an SMTP host: not configured. With in-process mail (
services.email.method: in-process, the default) and no SMTP host (services.email.smtp.hostunset, or a placeholder such asREPLACE MEas on a fresh install), the core logs one warning at boot naming the mail classes that are switched on but not sent, and skips those mails quietly; registrations and password resets no longer log anunexpected-error (500)each.POST /:username/account/request-password-resetanswers 200 with no mail sent (it failed with500 unexpected-error), andPOST /system/admin/mail/send-testanswers503 api-unavailable(“Mail is not configured: …”) instead of500 unexpected-error. A configured SMTP host keeps the previous behaviour: a delivery failure is still logged as an error. See Without an SMTP host.
2.0.0-rc.54
Section titled “2.0.0-rc.54”Logging for operators: a core with no log target configured, such as one installed with an earlier install wizard, now writes its warnings and errors to stderr instead of dropping them; production console logs carry no ANSI colour codes; the master’s messages appear once, with a new prefix (adjust any grep on [master] WARNING: or [master] ERROR:). No migration.
- Logs: warnings and errors are never silent. When no log target is configured (
logs.console,logs.file,logs.fileSyncandlogs.customall inactive), the master and the workers write their warnings and errors to stderr, so the journal ordocker logskeeps them. Until now such a core logged nothing but the master’s own lines: a configuration without alogs:section, as written by the install wizard, turned every worker error into silence. Any configured target keeps the previous behaviour. See Where do the core’s logs go? - Logs: no colour codes in production. Console logs carry no ANSI colour codes in the journal or
docker logswith the production defaults (NODE_ENV=production, the Docker image default), with a wizard-generated configuration and with the stderr fallback above. The setting islogs.console.format.color(defaulttrue, kept for interactive development); thelogs.console.colorizekey the production defaults used was never read: a configuration that still sets it should switch toformat.color: false. - Logs: master messages once, new prefix. The master process’s messages appear once, through the logger, instead of twice (a logger line plus a raw console copy). Their text is unchanged, the prefix changes:
[master] WARNING: <msg>is now[warn]: [master:master] <msg>,[master] ERROR: <msg>is[error]: [master:master] <msg>and[master] <msg>is[info]: [master:master] <msg>, with the console’s timestamp. A grep on the message text (such asMaster process ready) still matches. The master’s info lines followlogs.consolelike every other line; a boot failure before the logger is ready still prints[master] FATAL: ...on stderr. - Logs: Error contexts in JSON logs. With
LOG_FORMAT=json(orlogs.console.format.json), an error logged with its Error object carries the error’s name, message and stack incontext(it was{}). - Install wizard: a
logs:section. Thepryv-config.ymlwritten byinitnow has alogs:section, as in the production defaults: the console atinfowithout colours, and errors in a daily file<data folder>/logs/api-server.errors.log(14 days kept) on the data volume. Configurations written by earlier versions can add the same section. See Where do the core’s logs go?
2.0.0-rc.53
Section titled “2.0.0-rc.53”Group accounts in consent (CMC), with @pryv/cmc 3.21.0, and behaviour changes for every CMC deployment: revoke notices the other core could not take are re-sent, the CMC retry loop now runs by default (cmc.retryLoop.enabled: true), and every consent and delegation teardown is now the accesses.delete deletion. Operators: before upgrading a core that ran with the retry loop off, size its retry backlog; records older than their schedule are retired, not replayed. No migration.
- Group accounts. A practice, a research team or a bridge shares one Pryv account. Its members join it from their own accounts (
consent/request-cmcwithrequest.membership: true), and each member reads a patient’s data under a short-lived access that names them, created by the group’s core on the patient’s core through the patient’s delegable consent grant (new event typeconsent/member-access-cmc). When a membership ends (revoked, left, expired, narrowed, detached), those accesses are deleted on the patients’ cores and the offboarding is recorded (new event typeconsent/member-revoke-cmc). The group’s and the members’ cores need this release; a patient core of an earlier release gets attribution by name only (cmc.memberAccess.allowNameOnly). Newcmc.memberAccess.*settings on the group’s core. See Group accounts, and the reference member app app-web-group-member. - Revoke notices are re-sent until they land. A
consent/revoke-cmcwhose counterparty could not be reached (network error, timeout, 5xx) is queued and re-sent after 1, 5 and 25 minutes, about 2 hours, then every 6 hours (8 re-sends over about 27 hours), then markedfailed-permanentfor the operator; the event reportscontent.deliveryQueued: true. A rawaccesses.deleteof a relationship access and a delegation detach queue their notices the same way. Not queued: the notices sent while an account is being deleted (#157, a known limitation). A late notice never ends a relationship accepted again meanwhile: it names the accept it ends (content.acceptEventId,content.revokedAt), and such an arrival is markedcontent.superseded: 'relationship-re-established'. See Revoking. - Behaviour change: the retry loop is on by default.
cmc.retryLoop.enablednow defaults totrueon every core; set tofalse, nothing is queued and a warning is logged at boot. When the loop starts, a pending record older than its whole schedule (2 hours for a trigger, 30 hours for a revoke notice) is retired asfailed-permanentwithlastFailureReason: 'cmc-retry-stale'and never sent. Settled records are pruned, an account with nothing pending is read every 10 minutes instead of every minute, and in a cluster the loop moves to the replacement of a crashed worker (it ran on worker 1 only and stopped for good when that worker restarted). Newcmc.retryLoop.startScheduler. See Retries and the retry loop. - Behaviour change: consent and delegation teardowns are
accesses.delete. Accesses the server removes (a revoke written or received, a delegation detach, the teardown at account deletion, a collected capability, a member offboarding) were removed outright. They are now deleted asaccesses.deletedoes: listed byaccesses.getwithincludeDeletions: true(name kept, token never), their webhooks deleted, their aliases released. Apps that list deletions will see these records. A new accept after a revoke creates a new access. - Offers refuse the streams the plugin manages. A
consent/request-cmcasking for:_cmc:inbox, the:_cmc:_internalsubtree, a chats or collectors stream or the:_cmc:appsroot is refused at write (cmc-invalid-event-content) and again at accept (cmc-offer-stream-not-offerable).consent/invalidate-link-cmcmust be written on the invite’s stream or above it (cmc-invalidate-link-scope-mismatch). See Streams reserved by the plugin. - Accesses. An app token may delete only the
sharedaccesses it created (403 forbiddenotherwise), andaccesses.updatecan no longer removeclientData.cmcfrom an access that carries it (cmc-clientdata-cmc-forbidden). - Delegation and invites. A membership approved by a delegate administrator ends with that delegation. A directed invite (
content.toonconsent/request-cmc) is recorded, never delivered: hand over the capability URL. - Mail templates without a restart. With
services.email.method: in-process, a template written or deleted through the admin API is used by every worker of the serving core from the next mail on; when the serving worker cannot reload, the request answers503 api-unavailable(the template is stored: repeat the request). The other cores use it from their next start, andbin/mail.jswrites still need a restart (#159). See Managing templates: admin HTTP API. - Operator tools log to stderr.
bin/mail.js,dns-records.js,breach-scope.js,integrity-check.js,observability.jsandoauth-client.jswrite their logs to stderr, so their stdout holds only their output. Scripts that read these logs from stdout must read stderr. - Event types 1.3.0. The catalogue bundled with the server adds the event types and fields of group accounts and of the revoke notices. See Event types.
- Erased accounts stay erased on disk. A request still holding the id of an account being erased (a sign-in in flight, for example) no longer re-creates an empty per-user directory and database for it; such a request now fails instead.
2.0.0-rc.52
Section titled “2.0.0-rc.52”Two security fixes in consent (CMC) and delegation: upgrade promptly. Operators: after deploying, run node bin/access-scope-audit.js (read only) to list the sub-accesses that earlier withdrawals left behind, and review them with the account owners. No migration.
- Security fix: withdrawing a consent ends its sub-accesses. A delegable consent grant (offer
request.accessType: 'app') lets the requester createsharedsub-accesses. Until this release only a plainaccesses.deleteof the grant took them with it: aconsent/revoke-cmcwritten by either side, a revocation received from the counterparty, the end of a delegation and the deletion of the counterparty’s account removed the grant alone, and the sub-accesses kept opening the data until their own expiry. Every path now removes them first. Sub-accesses left behind earlier are not removed automatically:bin/access-scope-audit.jslists the live accesses whose creator is gone under “no longer managed”. An access created under a caller id (createdBy: '<id> <callerId>') is now matched on its creator’s id, soaccesses.deleteof the creator removes it and the creator can list and delete it. - Security fix: a delegate no longer reads the owner’s credentials. Wherever an access is returned without its token (a delegate’s reads of the owner’s accesses), every URL inside its
clientDatacomes without its user part, such as the counterparty endpoints of a consent relationship; host and path are kept. A webhook returned to a caller that neither owns its access nor created it carries itsurlreduced to scheme, host and path (user part, query and fragment removed). The owner’s own reads are unchanged. Deletion records are redacted the same way for every caller, and the URL credentials leave the stored row and its history once the access is deleted. - Logs: size cap.
logs.file.rotation.maxSize(for example"10m") caps the size of each daily log file; unset by default. Removelogs.file.maxFileBytesandmaxNbFilesfrom your config: they never applied. SettingmaxSizestarts a new list of files to prune, so delete the earlier daily files once by hand. - Pre-registration.
POST /system/users/validate(admin) refuses a username that account creation would refuse (400 invalid-parameters-format) before reserving anything or consuming the invitation token.
2.0.0-rc.51
Section titled “2.0.0-rc.51”Backup operators: check every directory that received bin/backup.js --incremental from an earlier release. It is damaged (it holds only the last delta, nothing in it can be repaired) and its restore is now refused: take a new full backup into a new directory and continue with --incremental from there. Full backups written by earlier releases are not affected. A configuration declaring custom.systemStreams.other entries now refuses the boot.
- Backups: incremental runs form a chain. An incremental run writes its own
incrementals/<runId>/directory (own manifest, ownencryption.json) next to the full backup, listed inchain.json; it no longer overwrites the full backup, whose restore reported success with only the last delta.--restoreapplies the full backup then every run in order (--through-run <runId>stops at a given run),--inspect <dir>says whether a directory is sound (DAMAGEDfor one written by an earlier--incremental;--allow-incompleterestores that delta anyway). A run’s window starts at the previous run’s snapshot (nothing changed during a run is missed), deletions of streams, accesses and webhooks are carried, an account deleted between runs is not re-created, an emptied series stays empty, and an encrypted chain is continued with the same key model (checked before anything is written). Two overlapping runs into one directory: the second to finish is refused and left aside. On a chain, use the newest run’sdecrypt-backup.mjs.node bin/backup.js --helpprints the tool’s usage. See Incremental backup. - Configuration, BREAKING: no custom other system streams. A config declaring any entry under
custom.systemStreams.otheris refused at boot and bybin/check-config.js. Such streams had no storage:streams.getdid not return them and no event could be written in them. Remove the section (an empty list is accepted); account fields belong undercustom.systemStreams.account. See System streams. - Audit and event reads on SQLite.
events.geton the audit streams (such as an account export over:_audit:accessesand:_audit:actionswith an open time range) walks the time index and stops atlimit, instead of scanning and sorting the whole audit file and stalling the API worker for minutes on a large one (#156). The same applies toevents.getwithstreams*(or no stream filter) on the SQLite event store. Results are unchanged. - SQLite: replaced account files. A running core now reads an account’s
account-1.0.0.sqlitethat a restore (or another process) deleted or replaced; it kept reading the deleted file, and changes made in that window were lost. - Login and MFA. A login refused after its MFA session was opened ends that session, so it no longer holds one of the account’s pending places until it expires. Two concurrent failed
mfa.activatecalls no longer discard the enrolment pending before them. - Docker images:
latest.pryvio/open-pryv.io:latestandpryvio/open-pryv.io-encrypted:latestfollow every release tag, release candidates included;pryvio/open-pryv.io:lateststill pointed at the v1 image (1.7.1). Production deployments should keep pinning a version tag.
2.0.0-rc.50
Section titled “2.0.0-rc.50”Hardening of API inputs and query semantics, notifications, registration, access requests and account deletion; several refusals are visible to clients (listed below). Before deploying, operators can count, read only, what the new rules touch: local events naming an account stream next to another stream, accesses granting :_system:account above read, and usernames of the shapes now reserved. A core configured with integrity.algorithm: md5 or sha1 refuses to boot. The Docker image no longer ships npm / npx.
- Stream queries. An
allentry that resolves to no stream (unknown, or also excluded bynot) now makes the query match nothing; it was ignored. The same holds for webhook and Socket.IO scopes. A store root (:local:,:<store>:) stands for*and may only appear alone inany; a malformed store prefix (:abc,::x) is refused withinvalid-request-structure. events.get.fromTime,toTimeandmodifiedSincemust be finite numbers (invalid-parameters-format). SQLite cores now behave as PostgreSQL cores:skipwithoutlimitpages the result, and deletions withmodifiedSinceare paged, ordered bysortAscending, with a strict bound.- Deletion records only for whole-store readers.
events.get?includeDeletionsandstreams.get?includeDeletionsSincereturn deletions only to a personal access or a*read access with nonone/create-onlyexception; others get an empty list (withversioning.deletionMode: keep-everything, the event deletions whose kept streams they can read). A lib-jsMonitoron a stream-scoped app token therefore no longer receives final deletions; trashing still arrives as an update. - Account fields written through events.
events.create/events.updateon an account field (:_system:language, editable custom fields) follow the field’s own rules (language1 to 5 characters, the field’sregexValidation, strings of at most 300 characters, finite numbers;invalid-parameters-format) and take the server time, ignoring a clienttime. An event naming an account stream next to another stream is refused withinvalid-operation. A unique custom field releases its previous value once the new one is written. - Accesses and streams.
:_system:accountcan be granted atreadornoneonly (invalid-operation): grant the fields themselves to write them.accesses.get:includeDeletionsandincludeExpiredmust be booleans, andincludeDeletions=falseleaves deletions out.streams.createrefuses an id naming a built-in object property (constructor,__proto__,toString, …) withinvalid-item-id. - HTTP. Urlencoded requests:
_methodmust beGET,POST,PUTorDELETE,_jsonmust hold an object or array, and_auth/_methodmay not repeat (invalid-request-structure). Anauthquery parameter that is not a single string answers401 invalid-access-token. Query-string keys naming built-in object properties are ignored; a__proto__key in a profile, an eventclientDataor a webhook scope name is kept as an ordinary entry. - Notifications. Scoped Socket.IO subscriptions and webhooks cover streams created or moved after they were set. A socket connected through an alias of the account receives its notifications.
accessUpdatedgoes only to the sockets of the updated access (the others still getaccessesChanged). A/<username>/<username>namespace must name the same account twice. - Webhooks. Each webhook is called once per change (it was called once per API worker). Scopes follow their access: narrowed by
accesses.update, a scope the access no longer reaches is removed, and a webhook left without scopes turnsinactive.webhooks.updatewithscopesreplaces the map and applies at once. Deleting an access or an account stops its webhooks at once, and a renamed account’s webhooks keep firing.webhooks.testis allowed once perminIntervalMsper webhook (429 too-many-attemptswithRetry-After). - Access requests and OAuth2. On
POST /reg/access/:key, the first decision wins: the same decision again gets the stored outcome, a different one409 invalid-operation. Polling keys are 32 characters.POST /access/invitationtoken/checkallows 10 checks a minute per address (429). OAuth2 PKCE: the S256code_challengemust be 43 base64url characters andcode_verifiermust follow RFC 7636 (invalid_request). - Registration and accounts. New usernames whose 3rd and 4th characters are
--(xn--...) or of the shape of a minted alias (r-and 8 letters or digits) are reserved; existing accounts are not affected.delegations.createAccountapplies the registration rules (400 delegation-invalid-username,409 delegation-username-taken).emails.resendrefuses an address another account has proved. An account created without an email and then given one stores it as its primary email (accounts already in that state: set the email again). An account that is the delegate of an account without email cannot be deleted with its personal token (invalid-operation,data.controlledAccountsWithoutEmail) until that account has an email, as it would be left with no way in; new routePOST /{controlled}/delegations/controlled-side/statuslets the delegate’s core check it across cores. - Sessions, previews and hosted sites. A personal session is extended only by a call that passed every check, including the custom auth step. A preview of an image declaring more than
eventFiles.previewsMaxInputPixels(default 100 000 000) answers413 payload-too-large. A proxy hosted site refuses a path with an encoded/or\, a./..segment or a malformed percent-encoding (400). - Operators.
integrity.algorithmacceptssha256andsha512only. The boot log andbin/observability.js showgive the admin key asauth.adminAccessKey.fingerprint(an HMAC keyed byauth.filesReadTokenSecret), so the snapshot hash changes once. New log files are created0640in0750directories, and more values are masked (errors, more credential names, URL user info,DEBUG=pryv:*output).bin/check-config.jsreads keys set as environment variables (#155). Joining a cluster with--bootstrapwritesbootstrap-config.ymland no longer replacesoverride-config.yml. MFA SMS: newsendLimits.perCorePerHour(default 100), andperDestinationPerDayis counted platform-wide. Admin routes check their input (invitationcount, the target core of a rename) and never take over another core’s username mapping or another account’s unique value; a restore refuses usernames that are unsafe or that the platform routes itself; boot warns when an access may stay cached above 300 s. Previews render at mosteventFiles.previewsRenderConcurrency(2) at once, the image setsUV_THREADPOOL_SIZE=16, crashing workers are replaced with a growing delay, andbin/backup.jswarns about plaintext backups and takes--passphrase-file.
2.0.0-rc.49
Section titled “2.0.0-rc.49”Hardening release. Client-visible: JSON answers carry Cache-Control: no-store and Vary: Origin, a path id always wins over an id query parameter, and HF series points and bounds are checked strictly. Operators: backups, bin/breach-scope.js --output reports and the config-to-env env file are now written owner-only (0600), so adjust any process that reads them as another user; a restore refuses a backup whose user ids or names are not one safe file name, or that keeps data behind symbolic links.
- HTTP: cache directives. Every JSON answer carries
Cache-Control: no-store(unless the route sets its own, as the OAuth2 discovery document does) andVary: Origin; preflight answers also vary onAccess-Control-Request-MethodandAccess-Control-Request-Headers. Attachments, previews and the HTML root page are unchanged. - HTTP: path ids. Routes that take an id in the path (
/accesses/{id},/events/{id},DELETE /streams/{id},/webhooks/{id},POST /webhooks/{id}/test) always act on it; anidquery parameter used to replace it. - Accesses. Deletion records in
accessDeletionsno longer carry the deleted access’stokenorapiEndpoint.accesses.checkAppmatches a feature permission only against the samefeatureandsetting(a changed setting now answersmismatchingAccess), and a request with a feature permission that matches no access getscheckedPermissions. - HF series. A non-finite or out-of-range
deltaTime/timestamp, adeltaTimestring that is not a plain decimal, a non-finite number field and an emptypointslist are refused withinvalid-request-structure. Two points with the samedeltaTimein one request keep the last, on every engine.fromDeltaTime/toDeltaTimegiven as a list or an object are refused. - Events. Time bounds on account events now match other events (
toTimeinclusive,modifiedSincestrict). On SQLite cores, a type wildcard (note/*) no longer matches classes with the same prefix (notebook/...). - Fresh installs. An unset
service.assets.definitionsnow defaults to the public Pryv assets instead of a path the core does not serve, which broke lib-jsServiceAssetsand the login button. The install wizard asks for it and, in dns-active mode, addshttps://reg.<domain>andhttps://access.<domain>toauth.trustedApps. - Operators. The embedded DNS answers
reg.,access.,mfa.andlsc.with available cores only (a pre-registered core waits for its join to be acknowledged). Logs maskcode/statequery values and theReferer, and parameter validation errors no longer log the rejected value. The per-address password-reset budget counts an IPv6 address toward its /64. A custom system stream named like a built-in account field refuses the boot. The S3 attachment engine refuses ids that are not one key segment.POST /reg/recordsanswers400to a body that is not an object. - Fixes.
callBatchcounts only existing methods in the access usage statistics; a custom indexed account field’sdefaultis indexed at registration; a failedaccesses.update(and consent or delegation access changes) drops the access from every worker’s cache; account deletion checks the previews directory before deleting and reports a failed removal once all removals end; a registration that lost its username to a concurrent one no longer releases the holder’s verified secondary address.
2.0.0-rc.48
Section titled “2.0.0-rc.48”Operator step, once per core after the upgrade: node bin/events-stamp-backfill.js --dry-run (counts the events written without change tracking), then the same command without --dry-run.
- Server-written events carry change tracking. Events the platform writes on the user’s behalf (consent inbox copies and scope-update records, account email records, delegation records) now carry
created,createdBy,modifiedandmodifiedBy, withsystemas the access id, and the status changes the platform makes to existing events (consent request and trigger status, delegation records, consent withdrawals) setmodifiedandmodifiedBy: system.modifiedSincesyncs now return them.createdBy/modifiedBymay therefore readsystemon events, as they already could on accesses: see Event. Not a breaking change. - Events kept by
streams.delete. Events moved to the parent stream (mergeEventsWithParent), or left in their other streams, get a freshmodifiedand the deleting access asmodifiedBy, somodifiedSincesyncs see the change. - Shared secrets. An item consumed or discarded at retrieval records
systemasmodifiedBy; it kept the creating access. - Operator step.
bin/events-stamp-backfill.jsstamps the existing rows (--config <host config>on each core of a multi-core platform; safe to re-run, no restart needed). Each repaired event is returned once by the nextmodifiedSincesync of each client and gets a newintegrityvalue. - Fix. The
consent/scope-update-cmcrecord written when a relationship access is changed withaccesses.updatelands on that relationship’s own collectors stream; under a nested scope it went to a stream that does not exist and could not be read.
2.0.0-rc.47
Section titled “2.0.0-rc.47”Security release: upgrade promptly, and read the client-visible refusals first. Operators: the new per-address limits read the client address through http.trustedProxies only, so behind a reverse proxy list it there; once after the upgrade, remove the 32-hex upload temp files earlier versions left in the OS temp directory; bin/emails-unproved-report.js (read only) lists the pending email claims that still hold an address.
- Security, BREAKING for some clients: credential methods on their own routes only.
auth.login,auth.register,auth.emailChallenge,auth.emailChallengeVerify,account.requestPasswordReset,account.resetPassword,account.verifyEmail,mfa.recoverandsharedSecrets.retrieveanswer400 invalid-operationinside a batch call or over socket.io.account.changePassword,mfa.activate,mfa.confirmand the other methods stay batchable. - Batch calls. A batch holds at most 1000 calls (
400 invalid-parameters-format), a nestedcallBatchis refused in its slot, and the results of one batch are bounded bylimits.batch.maxTotalItems(default 100000,413 too-many-results). - Security: failed passwords are delayed, never locked out. Sign-in,
account.changePassword, the MFA step-up by password andmfa.recovershare one tally per account: afterauth.passwordAttempts.freeFailures(5) failures withinwindowSeconds(900), the next attempt answers429 too-many-attemptswithRetry-Afteranddata.retryAfterSeconds, for a delay doubling from 2 s up to 300 s; a success clears it. The same failures also count per client address (an IPv6 address toward its /64) across cores: pastperIp.maxFailures(30), password checks from that address answer429until the window ends. See Limits at a glance. - Account deletion ends the account’s relationships. As a delegate: on each account it controlled, its tokens, control access and grants are removed and the name can be invited again. As a controlled account: its delegates’ lists drop it. Cross-account consent counterparties receive
consent/revoke-cmc. A delegation records the delegate’s account id: an account that later takes the name is refused (delegation-delegate-mismatch),delegations.issueTokenanswers410 delegation-not-activeonce the delegate is gone, and the relationship turnsstaleuntil the owner removes it. New controlled-side routePOST /<username>/delegations/controlled-side/release. - Email addresses. An address added with
emails.addno longer reserves it on the platform until it is verified; the first verification takes it, and a link for an address another account now holds answers401 invalid-access-token. Withaccount.emailVerification.requireAtRegistration, the legacyemailfield ofaccount.updateaccepts only an address already verified on the account. Two accounts setting the same unique value at once: one gets409 item-already-exists. - Errors. An
unexpected-error(500) no longer carries the fault’s text: itsmessageisUnexpected error (ref <8 hex characters>), the reference of the server’s log line. Do not parse it. HF series routes answer the same error. - Parameters and limits.
events.getrefuses alimitorskipthat is not an integer from 0 to 100000.streams.getrefuses undeclared parameters (id,storeId,includeTrashed: useparentIdandstate: 'all').accesses.createrefuses a token named like a built-in object property (invalid-item-id). An upload carries one event part and at mostuploads.maxFiles(10) files. A socket.io namespace is/<username>or/<username>/<username>only, and messages are bounded bysocketIO.maxMessageBytes. An account holds at mostservices.mfa.sessions.maxPendingPerUser(5) pending MFA logins.POST /reg/accessallowsaccess.maxLiveRequestsPerIp(50) live requests per address on a core (429), and an unread request livesaccess.unopenedRequestTtl(600 s). - Security: more fixes. An access’s excluded streams stay excluded when trashed. Concurrent requests of one token never see its restrictions missing. A malformed OAuth2 signed
stateanswers400. Each password hash gets its own salt. Account deletion erases the HF series stored under every name of the account, and a username change or a deletion drops the pending password-reset requests of the former names. Access-request keys stay out of the logs. A restore given a decryption secret refuses a plaintext backup (--allow-plaintext,--require-encrypted). Previews follow a replaced or removed attachment. - Fixes. Account deletion no longer takes minutes on a core that is not the platform database leader. A dns-active core with the default
core.id: singleserves its root routes onhttps://single.<domain>/.
2.0.0-rc.46
Section titled “2.0.0-rc.46”Security release: upgrade promptly. Before restarting a dns-active core, check its stored DNS records with node bin/dns-records.js list.
- Security: embedded DNS server. Stricter request validation, size-limited answers (truncated over UDP), at most
dns.tcpMaxConnectionsTCP connections (default 64), and validated stored records: they are normalised (lowercase, no trailing dot) when loaded, and a record that still does not validate is not served and is named once in the master log. A record holds at most 32 values per key (a,aaaa,txt) and 64 per subdomain (/reg/records:400). On the first boot of a dns-active core, adns.publicIpthat is not an IPv4 address stops the boot. - Security: events.
events.getnever returns events of internal plugin streams, whatever stream ids are queried.events.createandevents.updatevalidate their attachment parameters more strictly (events.updatetakesidandupdateonly). - Security: socket.io. A call whose reply argument is not a function is answered without affecting the server.
- Audit:
audit.onUserDelete: keepon SQLite. The per-user audit now stays after an account deletion with the SQLite audit engine too (the file stays in the user directory, readable by user id); it was erased with the directory.erase(default) is unchanged (#154).
2.0.0-rc.45
Section titled “2.0.0-rc.45”Security release: upgrade promptly. Before restarting each core, run node bin/check-config.js with your override: a node with core.ip set and no Raft TLS now refuses to start.
- Multi-core, BREAKING: Raft TLS required. A node with
core.ipset that runs its own rqlited and has nostorages.engines.rqlite.tlsrefuses to start (it was a boot warning), single-core included. Multi-core: runnode bin/bootstrap.js init-ca-holderon the CA-holder core (joiners gettlsfrom their bundle). Single-core dns-active: removecore.ipand setdns.publicIp.check-configreports it as a problem. - MFA, BREAKING: the step-up is always required. The one-release opt-out
services.mfa.stepUp.requiredis removed: a configuration that still setsservices.mfa.stepUpboots with a warning saying it is ignored.mfa.confirmis refused when the enrolment changed after itsmfa.activate. - Accesses:
webhooksfeature permission. An app or shared access carrying{ feature: 'webhooks', setting: 'forbidden' }cannot create, update or test webhooks (403 forbidden), its webhooks no longer fire, and every access it creates carries the same entry. Meant for tokens handed out publicly. - Security: webhooks.
webhooks.getwith a shared access lists only the webhooks that access created. - Security: admin pre-registration.
POST /system/users/validateconsumes the invitation token (given back if the reservation fails), refuses reserved names, requiresusernameand releases the rows it wrote when a reservation fails. - Login. The legacy
ssocookie is no longer set (auth.ssoCookie*settings are ignored). A login that fails after its session was written, or that loses the race to create its personal access, leaves no stray session. - Fixes. An app managing a stream can rename it and its children without manage rights on the parent (a move still needs them). The
system.deactivateMfanotice names the account, not the alias used in the path.bin/access-scope-audit.jsno longer creates empty per-user audit files on SQLite.
2.0.0-rc.44
Section titled “2.0.0-rc.44”Security release: upgrade promptly, and read the MFA and API contract notes first: some change what clients send. Before restarting each core: run node bin/check-config.js with your override and node bin/mail.js templates validate; with in-process mail on an already seeded platform, add the new mfa-change template on one core (node bin/mail.js templates set mfa-change <lang> <part> --file components/mail/templates/mfa-change/<lang>/<part>.pug, en and fr, subject and html); update the account web app at the same time.
- MFA, BREAKING: step-up.
mfa.deactivate, andmfa.activateover an active enrolment, need{ password }or{ code }(a current TOTP code, single use):400withdata.id: 'step-up-required'when missing,403 invalid-step-upwhen wrong (counted in the MFA backoff).services.mfa.stepUp.required: falseis a one-release opt-out. The account’s address gets a notice on every MFA change (templatemfa-change). - MFA, BREAKING: inactive methods. A login of an account enrolled in a method that is not active on the server is refused (
403 mfa-method-inactive);services.mfa.allowLoginWhenMethodInactiverestores the former password-only login. Legacyservices.mfa.modedeployments: setservices.mfa.methods.sms.active: truebefore removingmode. - MFA, BREAKING: SMS.
phonein E.164 form, other enrolment keys only fromservices.mfa.methods.sms.contentKeys;coderequired inmfa.confirmandmfa.verify; provider templates substituted once and encoded per context. Server-generated codes have 6 digits (codeLength), live 300 s (codeTtlSeconds) and belong to their MFA session; send limits (sendLimits: 30 s per session, 5 per user per hour, 10 per number per day,429 too-many-attempts); one pending enrolment per user. Challenge-verify providers answering200with a body needendpoints.verify.success: { jsonPath, equals }. - MFA: sessions. At most
services.mfa.sessions.maxPendingpending MFA sessions per core (429 too-many-requests), a fixed lifetime, anmfaTokenvalid only on its own account’s path, and a login session refused once its enrolment changed. A refused login writes no session and no access. - Security:
streams.delete. Deleting a trashed stream with an app or shared access needs manage rights on its whole subtree, and rights to create events on the parent withmergeEventsWithParent=true. - Security, BREAKING: webhooks.
webhooks.createtakesurlandscopesonly; URLs are http(s) without credentials; calls to loopback, private, link-local and other reserved addresses are refused unless listed inwebhooks.allowedPrivateHosts; no redirects, awebhooks.requestTimeoutMstimeout (10 s), andwebhooks.testanswers one error for every failure. - Storage, BREAKING for SQLite deployments. A PostgreSQL series engine with a SQLite base engine is refused at boot and by
check-config;storages.series.enginedefaults topostgresql, so a SQLite deployment sets it tosqliteorinfluxdb. - Email verification visible to apps. An access that can read
:system:emailcan ask for a read-onlyverification/emailevent (verified,method,verifiedAt) withtypes: ['verification/email']; a read withouttypesis unchanged. The welcome mail carries a “verify my email” link (VERIFY_LINK) when the founding address is not proved; an existingin-processplatform keeps its storedwelcome-emailuntil the template is updated. - Events. Account events carry the time their value was set (stable
integrity,modifiedSinceworks on them);skip/limitand class wildcards intypesnow apply to account streams. - Operators. A core with hashed PII refuses to start without
platform.piiHmacKey(and, in production, with the public development value). The template validator accepts a plain string literal (= 'text'). The encryption-at-rest image is also published forlinux/arm64. - Fixes. SQLite engine: workers no longer serve a user’s files after another process deleted or replaced them, concurrent logins no longer create duplicate accesses, account deletion leaves no empty directory. HF series queries with out-of-range time bounds no longer fail.
bin/hfs-duration-repair.js(re-run it if it failed),bin/access-scope-audit.jsandbin/hfs-author-scrub.js(paged, covers deleted rows; re-run--dry-runonce) are fixed.
2.0.0-rc.43
Section titled “2.0.0-rc.43”Security release: upgrade promptly. Some fixes need operator attention before the restart: on every core, run node bin/check-config.js (with your --config) and node bin/mail.js templates validate; fix any template it lists with bin/mail.js templates set (legacy templates using the backtick-literal form must be rewritten as plain text); on multi-core platforms, point storages.engines.rqlite.url at the local loopback. Read the configuration and multi-core notes below.
- Configuration, BREAKING: the production layer carries deployment-neutral settings only.
config/production-config.yml(added under your override withNODE_ENV=productionand no--config, as in the Docker image) no longer supplies platform values. If you relied on it, set in your own override:auth.adminAccessKey,auth.filesReadTokenSecret,auth.trustedApps,auth.passwordResetPageURL(unless password-reset mail is off),auth.emailVerificationPageURL(otherwise email verification stays off, with a boot warning) andservices.email(the default is nowin-process, which needsservices.email.smtp.host).bin/check-config.jsreports which base layer the server adds. - Security:
auth.trustedAppsis required and matched as origins. Boot refuses a missing, empty or unparsable value. Scheme, host and port are compared, paths are ignored, and*is only accepted as a whole leading label (https://*.example.commatches its subdomains). A trailing*from older configs still means “any port”. - Security: password reset. Requests are throttled (
429 too-many-attemptswithRetry-After); reset links are single use, one per account, and links mailed before the upgrade must be requested again. A completed reset revokes the account’s personal sessions and personal accesses (apps sign in again); a password change revokes the account’s other personal sessions and pending reset links. - Security: logs. Credential values are no longer written to logs: error logs keep the request body’s keys only, credential keys are masked and URL redaction covers access tokens and one-time link tokens. Check the retention of logs written by earlier releases.
- Security: access cache. Cached accesses expire after
caching.accessMaxAgeSeconds(new, default 60 s), and workers apply invalidations more strictly. A personal session with no account is refused.backloopis now a reserved username, and an invitation token admits a single registration. - Security: child accesses carry the creator’s narrower rules. A shared access created or widened by an app or shared access never reaches more than its creator; the creator’s restrictions on the sub-streams it covers appear in the created access’s
permissions. - Security: single-event reads apply the
events.getexclusions.events.getOne, attachments, previews and HF series answer403to an app or shared token for an event it cannot list. - Security: CMC write protections. App and shared tokens can no longer update events of the CMC inbox or internal streams, move events into or out of them, change a CMC event type, or create internal events outside their own responses stream (
403,data.id: 'cmc-protected-event-write'). CMC content is validated on update as on create. - Security: MFA attempts.
mfa.verifyandmfa.confirmreserve their attempt before the code is checked, so parallel attempts cannot exceed the limits; an attempt that keeps losing to concurrent ones gets429. - Security: admin key. Compared in constant time and read only from the
Authorizationheader onDELETE /users/:username(no?auth=, Bearer or Basic form); failed attempts are logged./reg/recordsanswers404to a missing or wrong key. - Security, BREAKING for some multi-core setups: multi-core hardening. rqlite’s HTTP API binds to loopback on every core (
storages.engines.rqlite.httpBindAddrexposes it, with a boot warning);storages.engines.rqlite.urlmust behttp://127.0.0.1:<port>unlesshttpBindAddris set to the same address.bootstrap.js promote-coreneeds--target-applied-index <n>(frombootstrap.js applied-indexon the target) or--force. A multi-core core withoutstorages.engines.rqlite.tlslogs a boot warning (a later release will refuse it). Core ids must match[a-z0-9][a-z0-9-]*and core URLs must be https origins; stored mail templates are validated before use (invalid ones are skipped and listed at boot). - Security: joining a core. The joiner stages the bundle’s files and installs them only after an accepted acknowledgement bound to its certificate fingerprint (refusals are
401 ack-refused);new-corerefuses an existing id unless--replace. Treat a delivered bundle and its passphrase as admin credentials: see SINGLE-TO-MULTIPLE.md, “If a bundle may have been exposed”. - Operator tools.
bin/platform-backloop-cleanup.js(report, then--apply, once per platform) removes rows a former test-only registration path left;bin/access-scope-audit.js [--user u]reports existing accesses that reach further than their creator (read only);bin/hfs-duration-repair.js(--dry-run, then without the flag, once per core, safe to re-run) repairs HF series events written before 2.0.0-rc.40 whosedurationheld nanoseconds. - Fix: integrity with a fractional
duration. Events created or updated with a fractionaldurationnow carry anintegrityhash that verifies. Events written by earlier releases keep the failing hash until rewritten:bin/integrity-check.jsreports them, and anyevents.updaterecomputes it. - Let’s Encrypt: the contact email is optional.
letsEncrypt.emaildefaults tonulland is no longer required whenletsEncrypt.enabledis true.
2.0.0-rc.42
Section titled “2.0.0-rc.42”Docker operators: before upgrading, check that storages.engines.sqlite.path points at a mounted volume; the image now refuses to start otherwise.
- Docker image: refuses a user data root on the container’s own filesystem. Inside the image, the server does not start when
storages.engines.sqlite.path(per-user databases, attachments, SQLite audit and series) is on the container’s own filesystem or on a tmpfs while an engine writes there: that data would be lost whenever the container is recreated. Set it to a mounted path (e.g./app/data/userswith/app/datamounted);PRYV_EPHEMERAL_DATA_OK=truelifts the check for a throwaway container. Native installs are not checked. - Configuration:
${PRYV_DATADIR}/${PRYV_LOGSDIR}are gone fromproduction-config.yml. Nothing ever expanded them, so the documented plain-Docker and Dokku setups failed at boot unless the override restated both keys. INSTALL.md now shows the override the/app/datamount needs and the real order of the configuration layers. - Docker image: Node 24.18.1 and current Debian security updates. The image stays below Node 24.19 (nodejs/node#65446) and no longer ships
curl(usenode -e "fetch(…)"for checks inside the container). Native installs should run Node 24.18.1 and hold the package so routine upgrades do not move past 24.18. - Install wizard. The generated
run-pryv.shrestarts the platform after a host reboot (--restart unless-stopped,--stop-timeout 30), andpryv-config.yml, which holds the generated secrets, is written readable by its owner only.
2.0.0-rc.41
Section titled “2.0.0-rc.41”Security release: upgrade promptly. No configuration change is needed for the security fixes; Docker operators, read the non-root note below.
- Security: SMS MFA in
singlemode. The pending code is shared by the API workers, andmfa.verify/mfa.confirmrefuse unless that exact code is sent. TOTP and SMSchallenge-verifysetups were not concerned. A correct code is also no longer refused when the request reaches another worker. - Security: MFA changes need the account’s own login.
mfa.activateandmfa.deactivatecalled with a delegated personal access (account delegation) get403 delegation-genuine-login-required. - Security: access aliases are set by the server only. Use
randomAlias: true; analiasinaccesses.createis refused with403. Deleting an access releases only an alias of the same account. - Security: a delegated personal access does not see the owner’s tokens. In
accesses.get,accesses.getOne,accesses.updateandaccesses.checkApp, it gets thetokenandapiEndpointof itself and of the accesses it created only. An app authorised through a delegated session therefore gets its own access, removed with the delegation. - Security: login sessions belong to one account. A session is reused and accepted only for the account it was opened for; a session left under a username by a former owner of that name is never handed over. Existing sessions keep working; the next login of each app opens a new one.
- Security (deployments allowing
personalTokeninuser-account.delete). A personal token only deletes its own account, whatever the transport, and a logged-out or expired token cannot delete it. - Docker image: the server runs as a non-root user. The container still starts as root, hands its data directories to the
nodeuser (uid 1000) and drops to it, keeping only the right to bind ports below 1024. No action for the documented layouts; data placed elsewhere needsPRYV_OWNED_DIRS, and files the server only reads must be readable by uid 1000.PRYV_RUN_AS_ROOT=truerestores the previous behaviour. See INSTALL.md, “Container user”. - Docker image for
linux/arm64.pryvio/open-pryv.iois published forlinux/amd64andlinux/arm64under the same tag.
2.0.0-rc.40
Section titled “2.0.0-rc.40”Security release: upgrade promptly. Before upgrading, check that auth.adminAccessKey and auth.filesReadTokenSecret are at least 16 characters; after upgrading, run bin/hfs-author-scrub.js once per core (see below).
- Security:
accesses.updateonly changes the updatable fields. Onlyname,deviceName,permissions,expireAfter,expiresandclientDatacan be updated, as the method always documented; any other field (type,token,id,createdBy,alias, …) is refused with403 forbiddenand the access is left unchanged. A personal access is now valid only with a session opened for the same account (sessions survive a username change). - Security: registration ignores server-owned fields.
POST /users(and/reg/user,/reg/users) no longer takesid,passwordHashoreventsfrom its parameters. Custom account fields are unchanged. - Security: HF series
modifiedByis the writing access id. Writing to a high-frequency series recorded the request’s authorization value as the series event’smodifiedBy; it now records the access id (with the caller id when given), like every other write. Operators: after upgrading the code and before restarting, runnode bin/hfs-author-scrub.js --dry-run, then--revoke(rewrites the stored values and deletes the accesses concerned asaccesses.deletedoes), then restart. With the Docker image, run it withdocker execin the running container, then restart it. The tool prints no credential. - Security: event ids. A client-supplied event id must be exactly one of the accepted shapes (the store-prefixed form was not anchored at its end); attachment and preview paths are also checked to stay inside the account’s directory.
- Security, BREAKING for weak configurations: placeholder secrets refused at boot. A core does not start when
auth.adminAccessKeyorauth.filesReadTokenSecretis a placeholder (such asOVERRIDE ME), or shorter than 16 characters withNODE_ENV=production.bin/check-config.jsreports the same. - Security (hardening): previews no longer decode SVG. A
picture/attachedevent whose attachment is an SVG file gets422 corrupted-datafrom the previews route, as any format it cannot preview. - HF series
durationin seconds. Writing points set the series event’sdurationin nanoseconds; new writes now set it in seconds. Series events written by earlier releases keep the oversized duration until a repair tool in a later release.
2.0.0-rc.39
Section titled “2.0.0-rc.39”- Security: image previews (CVE-2026-96889). The image library behind event previews is updated: it bundled a
librsvgwith a memory flaw in SVG decoding that can lead to code execution, and previews detect an attachment’s format from its content, so an SVG file attached to apicture/attachedevent was decoded when its preview was requested. Every earlier Docker image is affected; update promptly. No API or configuration change. - Security: cross-account consent, an accept is only taken through the capability. A
consent/accept-cmcorconsent/refuse-cmcwritten to:_cmc:inboxwith a relationship token is refused with400,data.id: 'cmc-event-type-not-allowed'. Before, a peer could open a relationship under any app scope of the requester’s account, or mark one of its invites refused. Accepts and refusals reach the requester through the capability’s responses stream, as every released version delivers them, so no client or peer change is needed. - Security: previews cache clean-up requires the admin key. The previews worker’s
clean-up-cacheroutes now requireAuthorization: <auth.adminAccessKey>. The worker’s port is internal and the public port never routed them. - Server-written event fields are applied onto the stored event. Following 2.0.0-rc.38, the fields the server itself writes onto an existing event (CMC trigger status, consent withdrawal, invite state, delegation and email records, a series event’s
duration, an attachment added or removed) no longer overwrite a clientevents.updatethat lands in between. - Event types 1.1.3.
consent/accept-cmcdeclarescontent.features, the field the platform reads;content.extrais deprecated. No behaviour change.
2.0.0-rc.38
Section titled “2.0.0-rc.38”- Cross-account consent: the offer’s
featuresdecide the chat channel. A relationship’sfeatures(chat,systemMessaging) are now resolved by the server from the offer; the accept may only narrow them (atrueagainst an offer that turned the feature off is ignored). The resolved pair is recorded on the accept event (kept on update), on both relationship accesses and on the requester’s inbox mirror. A relationship without chat gets nochats:<counterparty-slug>stream and no chat permission on either side, and a counterparty writing or editing amessage/chat-cmcdirectly is refused with403,data.id: 'cmc-chat-disabled'(likewise alerts and acks without system messaging). Relationships accepted earlier keep their chat stream: readfeatures, not the stream, to decide whether to offer chat.consent/accept-cmccontent.featuresis validated. See Sending chat messages. - Cross-account consent: an account cannot accept its own invite. An accept of an offer the accepting account made itself (an open link opened by its requester) fails with
cmc-self-accept-forbiddenbefore anything is provisioned. A self-relationship created earlier records its withdrawal when its access is deleted. - Server-written event fields survive a concurrent update. A field the server writes on an event (a CMC dispatch status, a consent withdrawal, the owner’s confirmation) is no longer lost when a client updates or trashes the same event at that moment: the client’s change is applied onto the event as stored at write time.
content.statusandcontent.failureon CMC events are server-written only. Nothing changes in requests or responses.
2.0.0-rc.37
Section titled “2.0.0-rc.37”- Security: trusted-proxy matching (CVE-2026-90711). The library that matches
http.trustedProxiesis updated: before, an IPv4-mapped IPv6 entry with a short prefix (for example::ffff:10.0.0.0/8instead of::ffff:10.0.0.0/104) matched every IPv4 client, so any client could choose the address recorded for it. Lists in plain IPv4 notation, the namesloopback/linklocal/uniquelocal, and the default were not affected. Boot check: a list that would trust every client (for example::/1) or a subnet that matches no client now refuses the boot, naming the entry; write IPv4 subnets in IPv4 notation (10.0.0.0/8). - SQLite storage engine. A busy account file no longer blocks the server while it waits, and a data store’s per-account key-value storage can remove an entry (
set(key, null)).
2.0.0-rc.36
Section titled “2.0.0-rc.36”- Breaking (security): no more
REDIRECTEDoutcome for an auth request.POST /reg/access/{key}refusesstatus: 'REDIRECTED'with400 invalid-parameterslike any unknown status, no outcome post stores aredirectUrl, and the poll never answersREDIRECTED. The status let anyone holding a request key store an unvalidated URL served to the authentication page. No shipped authentication page sent it: a request stays on the core that created it, and an account on another core is served through the shared-secret hand-off. A custom page that posted it must stop. - Breaking (security): client addresses only from trusted proxies. The address recorded in the audit log (
source.ip) and theX-Forwarded-Host/X-Forwarded-Protoa DPoP proof is checked against are read from the forwarding headers only when the request comes from a proxy listed inhttp.trustedProxies(IPs, CIDRs, orloopback/linklocal/uniquelocal; defaultloopback). Operator action: a proxy on another host, or one reaching the core over a Docker bridge (Dokku’s nginx arrives from172.17.0.1), must be added to the list, or every request is recorded with the proxy’s address and DPoP proofs naming the public host are refused. nginx on the same host and a directly exposed core need nothing. The list in use is in the boot log; an entry that is not an IP, a CIDR or a known name refuses the boot. See INSTALL, client addresses behind a proxy. - Image previews are reachable on the public port again.
{apiEndpoint}previews/events/{id}(optional.jpg/.jpeg,w/h,auth) is now routed to the previews worker in-process (Get event preview); a core terminating TLS itself answered 404 before. Deployments behind nginx need the matching location, see INSTALL, running behind nginx. Usernamesusers,oauth2,previews,seriesandsystemare reserved (existing accounts are not affected),GET /reg/{username}/check_usernameanswersreserved: truefor every name registration refuses, and request logs writeauth=***for tokens passed in the query. - A cross-account revocation that did not take effect no longer reads completed. A
consent/revoke-cmcwhose local delete fails endsfailed(cmc-revoke-delete-failed) and is retried without telling the peer twice. - Cross-account consent: the withdrawal is recorded on the person’s own record. However a consent ends (its data grant deleted with
accesses.delete, aconsent/revoke-cmcwritten by the person, a revocation by the requester), the person’sconsent/accept-cmcevent now carriescontent.withdrawal = { at, by, accessId, revokeEventId? }, as a delegation detach already did. The field is server-owned and never overwritten once set. Apps listing a person’s consents should treat an accept event withwithdrawalas ended;@pryv/cmc3.18.0 does so by default. See Revoking. - A consent invite can name its data grant. A
cmcInvitesentry of the auth request may carryaccessName(1 to 256 characters), echoed with the invite; the authentication page names the grant with it. An older core refuses an entry carrying it with400 invalid-parameters. - An auth request can require an account the user manages.
actAsManagedOnly: true, withactAs: 'allow'or a username, asks that the access be granted for a managed account only, never for the signed-in account; the authentication page enforces it and refuses withreasonId: 'MANAGED_ACCOUNT_UNAVAILABLE'when no managed account can be used. Echoed only when understood; an older core drops it. See account delegation.
2.0.0-rc.28
Section titled “2.0.0-rc.28”- Hosted sites. A platform can serve a small web app itself under a reserved name, for example the account app on
account.${DOMAIN}: either from a static folder on each core (a root-based build, which each app-web-user-account release ships as a tarball) or as a fixed proxy to an upstream URL. In DNS mode the site gets its own subdomain and origin, covered by the platform certificate; in dnsLess mode it is served under/<name>/on the core’s public URL (the API’s own origin). The name is reserved, so no account can take it. See FAQ infrastructure. The Pryv Lab now serves its account app at https://account.pryv.me/, and itsservice/infopublishesaccountaccordingly. - The master stops its rqlited when it exits early, and refuses to start when another rqlited already answers on its port, instead of silently using it.
2.0.0-rc.27
Section titled “2.0.0-rc.27”- Security: MFA per-account limit is a backoff, not a lockout. After
services.mfa.attempts.backoff.freeFailures(default 3) failed second factors, each further failure delays the next attempt, doubling up tomaxSeconds(default 300); during a delay verify, confirm and trigger answer429 too-many-attemptswithRetry-Afteranderror.data.retryAfterSeconds. Someone who knows the password can no longer lock the real user out, and a success clears the tally.attempts.perAccountandattempts.lockoutSecondsare no longer read (boot warning). Theattemptsblock is platform-wide: set the same values on every core. See MFA. - Security: a TOTP code is consumed atomically. The same code verified at the same moment on two sessions releases exactly one token.
- MFA settings are checked at boot: settings that cannot work stop the core with the key named instead of failing each request.
- Security: the private profile keeps MFA state to the MFA methods. Get private profile shows
mfawithout its secrets (method,content, TOTP parameters) and never the failed-attempt tally; update private profile refusesmfaandmfaThrottle(400 invalid-operation). An app access namedprivateorpublichas no app profile (400 invalid-operation): such an access could previously reach the user’s own profiles. - New accounts get the CMC reserved streams at creation, so a fresh account’s stream list already shows
:_cmc:,:_cmc:inboxand:_cmc:apps. - Embedded DNS answers over TCP too, and conforms to the DNS RFCs (authoritative flag, SOA on negative answers, REFUSED out of zone). Operator action: open TCP/53 inbound alongside UDP/53. See DNS configuration.
2.0.0-rc.23
Section titled “2.0.0-rc.23”- Grant an app access for an account you control. On platforms with account delegation, the authentication page asks, after sign-in, whom the access is for; an access granted for a controlled account is marked on the server (
accessInfo().delegation.grantedVia: 'app') and the auth request takes anactAsfield ('allow','deny'or a username). Breaking: ending a delegation now also revokes the accesses granted through it. A delegate’s token can no longer accept an OAuth2 consent (403 access_denied) or a cross-account consent (400,delegation-grant-requires-owner) on the controlled account.GET /service/infogainsaccount(the account app’s URL) andfeatures.delegation. - Credential hand-off by one-time shared secret. An auth request may ask for
credentialHandoff: 'shared-secret': theACCEPTEDpoll then carries a one-timehandoffkey instead of the token, redeemed once on the user’s core. Requests that do not ask behave as before; a core that does not support it ignores the field. - Security: a managed shared access no longer outlives its managing app access. A shared access created by an expiring app access without
expireAfternow takes the app’s expiry, and one without expiry is refused once its app access has expired. Breaking (update access): clearing such a shared access’s expiry, or giving its app an expiry while it has none, is refused (invalid-operation, withoffendingChildrenin the second case). - Security: a webhook stops firing once its access has expired (or, for a shared access without expiry, once its managing app access has); it becomes
inactive, andwebhooks.updatewithstate: 'active'reactivates it once the access is valid again.
2.0.0-rc.18 to 2.0.0-rc.22
Section titled “2.0.0-rc.18 to 2.0.0-rc.22”- An auth request can say how each permission should be presented. The new optional
consentobject of the auth request marks permissions as required (mandatory), or as ones the user has to choose deliberately (optIn); a permission in neither list is optional and shown pre-selected, which is what every permission does today.allowUserChoiceis what enables per-permission choice at all. The authentication page then mints only what the user kept, and the server verifies the result against the offer: a grant that does not match is refused withinvalid-consent-grant, and a core that could not perform that check answersconsent-check-unavailablerather than accepting it. Requests without aconsentobject behave exactly as before, and a core that does not support it ignores the object and falls back to all-or-nothing, so the echo ofconsentin the response is how an app detects support. Where consent is your lawful basis, preferoptIn: see the consent guide. - The same
optInannotation is available on OAuth2 and cross-account consent offers, beside the existingmandatory, with identical meaning: it decides whether the entry opens ticked, never what may be granted. See OAuth2 authorization.
Email verification is now general availability, and on by default. services.email.enabled.verifyEmail defaults to true. Upgrading does not stop a working configuration from booting: a deployment that never set the key keeps booting, logs one warning per start, and leaves the feature off until auth.emailVerificationPageURL and a mail setup are in place. Setting the key to true explicitly makes the page URL required at boot, as before; setting it to false turns the feature off silently. “Mail is configured” is decided from configuration alone, with no SMTP probe, and the sender address is not part of that test, so a deployment that was sending mail without one keeps sending mail.
- Email verification at sign-up (optional, off by default). Operators can require a proved address before an account is created, with
account.emailVerification.requireAtRegistration. The client requests a one-time code withPOST {register}email-challenge, the holder pastes it back toPOST {register}email-challenge/verify, and the resultingemailProofis sent with create user. Addresses proved this way recordverificationMethod: 'email-code', a new proved value alongside'email-link'and'operator'. Admin-created accounts are never gated. The core refuses to boot when the gate is on and mail is incomplete, and a delivery failure blocks that registration rather than letting an unverified account through. The rate limits are keyed on the target address, because the endpoints are public: read the caveat in the operator guide before enabling it. - The multiple-emails surface leaves beta, including verify email address and the
emailsoperations of update account information. See the new email verification guide. - The mailed verification link now carries
usernameas well as the token, so a landing page can address/:username/account/verify-emailwithout an email lookup. Existing links keep working, and the parameters are appended with the right separator, so a page URL that already carries its own query keeps it intact. - Mail templates ship with the server.
welcome-email,reset-password,verify-emailandemail-challenge, in English and French, are seeded on first boot when no template directory is configured, so a fresh deployment can send mail without authoring a template first. Deployments that already hold templates are untouched. GET /service/infonow always carriesfeatures.emailVerification: { atRegistration, onAccount }.
2.0.0-rc.17
Section titled “2.0.0-rc.17”- Third-party sign-in (beta, off by default). Pryv.io can act as an OpenID Connect client, letting an account holder sign in through an external identity provider the operator configures. New routes
GET /auth/sso/:provider/startand/callback, plus a publicGET /auth/sso/providersdescriptor for the sign-in buttons. A first sign-in links the provider identity to an account only when that account has proved ownership of the provider’s verified email address, so an address that was never inbox-proved cannot be taken over. The session is handed to the auth app through a one-time shared secret, so the token never appears in a redirect URL. See third-party sign-in. - Fixed: an OAuth2 authorization accept no longer mints an access through a consent that was refused or through an invalidated link.
- Fixed: when several data-grants serve one relationship, a consent revoke now reliably notifies the peer instead of reporting
peerNotified: false(#129). - Fixed:
accesses.createnow rejects a creation stream-id with junk after a valid prefix, or longer than 100 characters, as its error message already promised (#130).
2.0.0-rc.16
Section titled “2.0.0-rc.16”(supersedes the 2.0.0-rc.15 tag, which was cut from a commit that failed CI and was never published.)
- MFA: per-account failed-attempt limit. The failed-second-factor budget now also accrues per account, not only per pending MFA session, so repeated wrong codes across repeated logins no longer reset it. Once an account reaches
services.mfa.attempts.perAccountfailures within the window, the second-factor step is locked andmfa.verify,mfa.confirmandmfa.challengeanswer429 too-many-attemptswith aRetry-Afterheader. Password login is not locked and existing tokens keep working, so a password-holder cannot use this to lock a user out. A lock clears on its own, or immediately throughmfa.recoveror the admin deactivation. New configservices.mfa.attempts:perSession(5),perAccount(20,0disables),perAccountWindowSeconds(900),lockoutSeconds(900). This supersedes the known limitation noted in 2.0.0-rc.14.
2.0.0-rc.14
Section titled “2.0.0-rc.14”MFA: server-side TOTP (authenticator apps), enabled by default, alongside SMS. MFA is no longer SMS-only and is now active by default. A built-in TOTP factor (RFC 6238) works with no configuration and no external service, and is the default method; SMS keeps working unchanged and stays off until configured. Nothing is forced: a user with no enrolled factor logs in exactly as before. mfa.activate takes an optional method and returns an otpauthUri and Base32 secret for TOTP; auth.login returns mfaMethod next to mfaToken; service.info().features.mfa.methods lists the active methods so a client offers only what the operator enabled. Legacy deployments upgrade unchanged: a configuration carrying the old services.mfa.mode keeps precedence. To keep MFA off after upgrading, set services.mfa.active: false. See MFA setup.
- Multiple emails per account (beta). An account can hold more than one address, each with its own verification state, while the singular
emailstays authoritative as the primary.GET /:username/accountreturns anemailsarray;PUT /:username/accountaccepts anemailsoperations object (add,setPrimary,remove,resend);POST /:username/account/verify-emailconsumes the mailed one-time token.verificationMethoddistinguishes proved ownership ('email-link','operator') from asserted ('registration','legacy'), so averifiedstatus alone does not imply proof. Existing accounts need no migration. - Email verification ships OFF by default in this release (beta). Enabling it makes
auth.emailVerificationPageURLa required key, and a required key that is unset refuses the boot, so a default-on sub-feature could stop a previously valid deployment from starting. Opt in with both keys together. (In a later release this becomes on by default, with a soft landing instead of a boot refusal: see the entry at the top of this page.) - Shared secrets: hand a secret to a third party by one-time key. Instead of putting an apiEndpoint in a URL, where it survives in history, referrer headers and access logs, store the payload on the account and hand over a random key redeemable exactly once. Create returns the key once and keeps only its SHA-256; redeem takes no token, since the key is the credential, and travels in the request body; status inspects without consuming. An optional signature binds redemption to a passphrase or an HMAC proof. An access can be barred from minting them with the
secretSharingfeature permission. Config:sharedSecrets.enabled(defaulttrue),maxSizeBytes,maxTtl. - OAuth2: DPoP, sender-constrained tokens (RFC 9449, beta). A client can bind its tokens to a key pair it holds, so a stolen bearer token alone is useless: every call must carry a short-lived
DPoPproof. Opt-in per session and backward compatible. Proofs are single-use cluster-wide, enforcement sits in the shared access-validation path (REST, batch, socket.io and HF series alike, fail-closed), and a refresh must prove the same key. Requires a reverse proxy that overwritesX-Forwarded-Host/X-Forwarded-Proto, since proofs bind to the client-facing URI. - OAuth2:
private_key_jwtclient authentication (RFC 7521/7523, beta). A confidential client can authenticate with a signed JWT instead of a shared secret, by registering its public JWK Set (ES256 only). Verification failures answer uniformly, so the endpoint leaks nothing about why. - OAuth2: revocation reaches live tokens cluster-wide (beta). Revoking a client now also kills already-issued access tokens within
oauth.clientRevokeCheckSeconds(default 30), including open socket.io connections and the HF series token cache, instead of letting them live out their TTL. Operators can also revoke by key thumbprint, which kills everything bound to that key including later refresh attempts, and inspect what a revocation would hit. - OAuth2: an abandoned authorization code no longer leaves its access alive. The access minted at consent is revoked when the token exchange never succeeds. Session accesses now carry an explicit
{ feature: 'selfRevoke', setting: 'allowed' }permission, andaccesses.createacceptssetting: 'allowed'as the explicit form of the default. - Observability rebuilt: no third-party agent. ⚠ If you enabled the optional APM integration in an earlier version, read the note below. Telemetry is now constructed by the platform from a closed vocabulary rather than auto-instrumented by a vendor agent, and shipped over OTLP to any backend, including a collector inside your own infrastructure. What can be emitted is per-method call counts, duration histograms and error counts, labelled with an API method id, a status class and a documented error id, plus service name, version, machine hostname and worker index. Request URLs, parameters, bodies, headers, usernames, identifiers, log records and error messages have no representation in the schema. Error reports are aggregated and time-coarsened, because a precise failure timestamp is a re-identification handle. Earlier versions: the vendor agent’s scrubbing configuration was placed in a file the agent does not read, so it never loaded, and affected deployments sent request URLs, the
Hostheader, route parameters including the username, obfuscated SQL and forwarded log records including their message text. Assume that applied for as long as the integration was enabled and check what your provider account holds. See observability. - Fixed: concurrent
streams.createof the same id returns409 item-already-existsinstead of a500leaking the storage engine’s unique-constraint message (#126). - Fixed: attachment uploads are now bounded by
uploads.maxSizeMb, which previously bounded only JSON bodies, so a deployment without a size-limiting proxy accepted attachments of any size. Oversized parts now answer413 payload-too-largecarryingdata.limitMb. Operator note: a deployment relying on unbounded uploads will receive413until the limit is raised (#125). - Fixed: a peer-rejected OAuth2 consent accept answers
400 invalid_grantwith the peer’s machine-readable reason instead of a bare500. - Fixed: withdrawing consent no longer blocks re-consent through the same multi-use shareable link.
- Fixed: the telemetry endpoint guard refused plain
http://to a collector on a private address, which is the layout it recommends; loopback, private and link-local destinations are now accepted and anything routable still requireshttps:. The rule moved into the emitter, so it holds however the endpoint was configured (#119). - Fixed:
451 unavailable-methodno longer blames the commercial licence when a method is unavailable for another reason, such as an operator turning an optional feature off. - Fixed: a cross-account back-channel handshake could be dropped silently and permanently, leaving every later consent revocation on that relationship undeliverable. Affects 2.0.0-rc.10 and rc.11. Relationships already affected do not heal on upgrade: each needs a fresh request and accept.
- Fixed: two concurrent relationships with the same counterparty under the same app are now told apart, keyed on their per-request scope stream. Previously the newer relationship’s back-channel overwrote the older one’s pointers and deliveries were misrouted.
2.0.0-rc.11
Section titled “2.0.0-rc.11”- OAuth2: refresh-token reuse detection. Replaying an already-rotated refresh token, the signature of a stolen token chain, now revokes the whole chain rather than merely rejecting the call: the durable consent record and all live session accesses for that user and client, plus their descendants, are soft-deleted and dependent webhooks cascaded. A benign double-submit inside
oauth.refreshReuseGraceSeconds(default 10s) is tolerated. The error response is identical whether reuse was detected or not, so the endpoint cannot be used as an oracle. oauth.*events reach the audit trail. Five user-scoped events, including the newoauth.token.reuse_detected, persist to the user’s audit storage and are readable through the usual:_audit:*streams; four user-less events go to syslog only. Audit emission never fails a token grant.- Fixed: audit input validation could never reject anything, because the validators return a diagnostic string on failure and the guard treated it as success.
- Correction to the documentation: cross-account consent revocation was described as a server-orchestrated dual delete in which the peer’s side tears itself down. That was never implemented. A revoke tears down only the accesses on the account where it was written, and the forwarded notification is delivered without running a teardown on the receiving side, so revocation is advisory in the trigger-writer to peer direction. Integrators should delete their own half when they observe a revoke arriving. The enforcing direction is the local one: an accepter revoking destroys the data-grant on their own account, which is what cuts the requester’s read.
2.0.0-rc.10
Section titled “2.0.0-rc.10”- Cross-account consent: revocation reaches the counterparty whatever path performs it. Deleting a relationship access with a plain
accesses.delete, for example from a generic “connected apps” screen, now delivers the same revoke notification as the dedicated helpers, so consent withdrawal is observable on the other side regardless of how it was performed. The forwarded event always carriescontent.accessId, which is also the authoritative selector of the relationship to revoke: with several relationships to one counterparty, the previous matching could tear down the wrong one (#109). - The
:_cmc:*namespace now materialises on reads too. An account’s reserved streams are created on first use, but the trigger covered writes only, so a consumer whose first action was a read, typically an inbox watcher, gotunknown-referenced-resourceon every poll and could never bootstrap (#111). - Scope edits made with a plain
accesses.updatenow reach the counterparty, like the helper flow does. The notification previously targeted a stream that only admits lifecycle events and was silently rejected there.
2.0.0-rc.9
Section titled “2.0.0-rc.9”- Cross-account consent: request a delegable data-grant. A consent request may carry
request.accessType: "app"(default"shared"), so the accepted data-grant is minted as anappaccess and the approved requester can create scoped, individually-named sub-accesses within it: least-privilege re-delegation with per-actor audit attribution. Existing offers and the OAuth2 flow are unchanged.
2.0.0-rc.8
Section titled “2.0.0-rc.8”- Fixed: 2.0.0-rc.7 is dead on arrival, use rc.8. A production image built with
--omit=devpruned a package the OAuth2 route required at module load, so every API worker crash-looped and the core never served (#106).
2.0.0-rc.7
Section titled “2.0.0-rc.7”- OAuth2 authorization-code flow (server-side). Pryv.io can act as an OAuth2 authorization server (RFC 6749 with PKCE), so third-party applications obtain tokens through the standard redirect flow instead of the Pryv-native access-request polling flow. Both flows remain supported. New endpoints:
GET /.well-known/oauth-authorization-server(RFC 8414 discovery),GET /oauth2/authorizeandPOST /oauth2/token(authorization code, refresh, client credentials). The token response carries a PryvapiEndpointextension so multi-core clients build a working connection, and a vanilla client that calls the wrong core receives421with the correctcoreUrl. Clients are registered out of band by the operator. Consent scopes are granular, with no coarse wildcards: thescopeparameter carries exactly one consent-offer reference, the consent screen lets the user untick individual permissions, and the minted access carries exactly the kept subset. Revoking the durable consent record invalidates the refresh chain; narrowing it propagates on the next refresh, while widening always requires a fresh authorization. See OAuth2.
2.0.0-rc.6
Section titled “2.0.0-rc.6”- Access aliases: de-identifying endpoints.
accesses.createacceptsrandomAlias: true, which issues a platform-unique routable alias that replaces the username everywhere the access is addressed, including the returnedapiEndpointandaccess-info. Accesses handed to different parties therefore cannot be cross-matched back to one account. The alias routes exactly like the username, across cores, and is released when the access is deleted. - Changeable username.
POST /account/change-usernamelets a user choose a new one. Accesses issued under the previous username keep working, since the old name is kept as a routable alias, and theiraccess-inforeports the new username. The number of changes is capped by the operator (default 2);GET /account/username-changesreports used, limit and remaining. - Fixed: accepting a cross-account invite with an access name already in use failed permanently with a raw database duplicate-key message, and an internal retry loop kept re-attempting an accept that could never succeed (#105).
- Fixed: a failed transactional email no longer returns a
500carrying the mail-service URL and the upstream error to unauthenticated callers of password reset. The diagnostic is logged server-side instead (#104).
2.0.0-rc.5
Section titled “2.0.0-rc.5”- BREAKING: cross-account consent writes that mint or widen accesses now require a personal token. Writing an accept or a scope-update trigger mints or widens a data-grant on the user’s account, so it now requires user presence at the moment the action is recorded, closing a path where an app token with narrow write permission could trigger creation of a much broader access. Non-personal tokens receive
400 invalid-operationwitherror.data.id = "cmc-accept-requires-personal-token". Revoke is gated differently, on the standard access-permission check rather than the token class, because a revoke is a contraction: a personal token always may, the relationship’s own holder may self-revoke by default, the app that created the access may, and everything else is refused withcmc-revoke-forbidden. Cross-platform protocol deliveries pass through unaffected. Other trigger types, including request, refuse, invalidate-link and the chat and notification families, are unchanged. - Optional encryption-at-rest image variant. A published variant adds optional encryption at rest for the data directories (events, attachments, series, audit, platform database), with pluggable backends and key providers. The base image is unchanged and the variant is off until opted in.
/service/infocan advertise adapters: an optionaladaptersarray of base URLs, each serving an adapter UI and amanifest.jsondescribing its name, type, version and capabilities. Fully additive.
2.0.0-rc.4
Section titled “2.0.0-rc.4”- Multi-core: cores join as non-voters by default, so adding a core can no longer take an existing core’s control plane offline.
- On-demand encrypted backups. The backup tool can encrypt its output, so plaintext personal data never touches the destination disk. Two key models: a recipient public key, where the backup host holds no secret that can decrypt its own output, or a passphrase. Each file is encrypted independently, so chunking, incremental runs and single-user restore keep working, and restore auto-detects an encrypted backup. Opt-in: without the flags, backups behave exactly as before. A lost key makes the backup unrecoverable, which is the point of the feature. See encrypting the backup.
2.0.0-rc.1
Section titled “2.0.0-rc.1”First release candidate of open-pryv.io v2.
- Single-binary topology. One process manages API, HF series and preview workers in one Docker image. No MongoDB, and no separate registration, MFA or mail containers: all merged into the core.
- Two production-grade user-data engines: PostgreSQL (default, cross-user queries through shared tables) or SQLite (per-user files, cleaner erasure semantics). Both pass the same test matrix at parity.
- Multi-core cluster bootstrap over mTLS-protected Raft, with DNS discovery and wildcard certificate renewal across the cluster.
- Cross-account messaging and consent, federated consent, chat and system notifications between two accounts.
- Versioned accesses:
accesses.updateis back, with composite-id versioning and audit history,accesses.getOneaccepts?includeHistory=true, and socket.io emitsaccessUpdated. - Engine-agnostic schema migrations and a v1 to v2 migration path.
- An interactive install wizard (
docker run … init) that writes a commented configuration, a launcher and a configuration checker into the operator’s chosen directory. - BREAKING since
2.0.0-pre: the/reg/accesspolling endpoint response shapes were trimmed (SDK callers onpryv@>=3.5.0are unaffected), and MongoDB was removed as a user-data storage engine, so MongoDB-backed deployments must export and re-import into PostgreSQL or SQLite. Both are documented in the dedicated entries further down this page.
2.0.0-rc.3
Section titled “2.0.0-rc.3”- BREAKING — the deprecated
GET /audit/logsroute has been removed. Query audit logs through the Events API as described in the Audit logs guide: callevents.getwith the audit streams in thestreamsparameter (:_audit:for all,:_audit:access-{access-id}for one access,:_audit:action-{method-id}for one action). Returned items are standard audit events (audit-log/pryv-api/audit-log/pryv-api-error). No official SDK used the route.
Scoped real-time notifications for both notification transports — be notified only of the changes you care about, paired with lib-js pryv@3.7.0 + @pryv/socket.io@3.7.0 + @pryv/monitor@3.7.0 (lockstep).
- Webhooks gained an optional
scopesfield: a map of named scopes, each{ kind, query }wherekindisevents(default),streamsoraccessesandqueryis shaped like the matching read method’s parameters (an events.get query forevents). A scoped webhook fires only on matching changes and its data changes payload carries the matched scope keys instead of the coarseeventsChanged/streamsChangedmessages.scopesis alterable via webhooks.update. Unfiltered webhooks are unchanged. - Websockets gained
subscribe/unsubscribe/getSubscriptionsmessages and a unifiednotificationsChangedmessage. A connection holding at least one scope opts out of the coarse broadcasts and receivesnotificationsChanged({ keys })with the matched scope keys. Back-compatible: servers without the feature rejectsubscribe, and the SDK falls back to the coarse messages. @pryv/monitortransparently registers a scope derived from itseventsGetScopeand usesnotificationsChangedwhen the server supports it, falling back to coarse signals otherwise — no API change for monitor consumers.
2.0.0-rc.2
Section titled “2.0.0-rc.2”Diskless deployment shape for single-core dnsLess installs in full PostgreSQL mode — no persistent filesystem needed on the app host:
storages.platform.engine: postgresqlstores platform data (registrations index, DNS records, TLS certificates, invitation tokens, …) in PostgreSQL; no rqlited process runs. Single-core only — multi-core keeps rqlite; boot-time validation enforces the topology.- New
s3fileStorage engine: event attachments on any S3-compatible object store (storages.file.engine: s3+storages.engines.s3.*). - New
bin/migrate-platform.jsmoves platform data between rqlite and PostgreSQL in either direction (adopt diskless, or go multi-core later). - New
bin/config-to-env.js(+config-to-envdocker subcommand) converts a config file into an env file for pure-ENVdocker run --env-filedeployments. - The install wizard offers the diskless options for dnsLess + postgresql runs and documents them as commented blocks otherwise; it now also generates a
config-to-env.shlauncher. - Account deletion erases attachments through the fileStorage engine, so S3-stored attachments are removed too.
2.0.0-pre.4
Section titled “2.0.0-pre.4”Cross-account Messaging & Consent (CMC) plugin: completeness and security hardening on the open-pryv.io server, paired with lib-js pryv@3.4.0 + @pryv/cmc@1.1.0 + @pryv/monitor@3.4.0 + @pryv/socket.io@3.4.0 (lockstep).
Plugin completeness
- Capability TTL configurable per-invite.
cmc.createInvite({ ..., expiresAt })(orcontent.request.expiresAton a rawconsent/request-cmc) now controls the capability’s lifetime. Plugin bounds the value to[60s, 30d]at mint time; out-of-range rejects withcmc-capability-ttl-out-of-range. Default unchanged at 7 days when omitted. - Features gating binding at send time.
content.request.features.{chat, systemMessaging}is enforced on BOTH sides —cmc.sendChat/cmc.sendSystemAlertagainst a relationship whose negotiated feature flag isfalserejects withcmc-chat-disabled/cmc-system-messaging-disabled. Default-permit on omission (matches the offer-side default).consent/scope-request-cmcandconsent/scope-update-cmcremain protocol-level and permitted regardless. - Doctor-side
revokeRelationship({ inviteEventId })is now reliable. Previously the inbox-mirror droppedinviteEventId; the plugin now stamps it from the capability access’sclientData.cmc.requestEventIdso the SDK’s convenience lookup matches. The{ accessId, scopeStreamId }power-user form keeps working. requestEventIdpopulated on real-deploy capability accesses. Pre-fix the mint hook ran pre-persist whenevent.idwas null; new post-create hook stamps the real id after persistence. This makes theinviteEventIdmirror work end-to-end.
Security hardening (route-level guards)
clientData.cmc.*forge-prevention.accesses.create/accesses.updatereject any user-suppliedclientData.cmc.*withcmc-clientdata-cmc-forbidden. That namespace is plugin-owned end-to-end (role, appCode, counterparty, capability, requestEventId, features); allowing user-set values would let an app forge a counterparty role and bypass the handshake. The CMC plugin’s own internal calls reach storage viamall.accessesdirectly so the handshake is unaffected.- Reserved-root immutability.
streams.deleterejects deletion of the five reserved CMC parents (:_cmc:,:_cmc:inbox,:_cmc:apps,:_cmc:_internal,:_cmc:_internal:retries) +:_cmc:_internal:*+ plugin-managedchats/collectorssegments withcmc-reserved-stream-undeletable— even from a personal token. Without this guard aDELETE :_cmc:would silently break every active relationship on the account. User-creatable:_cmc:apps:<app>:<sub>streams remain deletable. content.fromstamping extended.inboxWriteHookalready stamped from-field on:_cmc:inboxwrites; the new counterparty-from stamping hook extends the same protection to per-appchats:*/collectors:*writes by counterparty-marked accesses. A peer cannot forgecontent.fromonmessage/chat-cmc,notification/alert-cmc,notification/ack-cmc,consent/scope-request-cmc, orconsent/scope-update-cmc.:_cmc:_internal:*defense-in-depth filter. New read-hooks onevents.get(strips internal-stream ids fromparams.streams),events.getOne(returns 404 if event has internal streamId), andstreams.get(prunes the internal subtree from the response tree). Today the internal subtree has no app-visible permissions so explicit queries return empty anyway; this guards against future regressions in the permission system.
Tests / contract
- 7 new error ids on
cmc.errorIdsmirror the server-side additions:CAPABILITY_TTL_OUT_OF_RANGE,HANDLER_MISSING_CAPABILITY_ID,CHAT_DISABLED,SYSTEM_MESSAGING_DISABLED,CLIENTDATA_CMC_FORBIDDEN,RESERVED_STREAM_UNDELETABLE,COUNTERPARTY_IDENTITY_MISSING. New[CMCXEC]J9 catalogue-match test pins all 7 against the server strings. - J3–J10 wire-shape contract tests added to
@pryv/cmc/test/cmc.test.js(listInvitesusesstreamsnotstreamIds,listAcceptedRelationshipscounterparty mapping precedence,waitForAcceptsinceTimefilter,acceptInvitescopeStreamIdrequirement +dataGrantAccessIdresolution).
Upgrade
npm install pryv@3.4.0 @pryv/cmc@1.1.0—@pryv/monitor+@pryv/socket.iofollow via transitive resolution. No source-level breaking change; existing apps onpryv@3.3.x+@pryv/cmc@1.0.xkeep working.- Server deployments:
docker pull pryvio/open-pryv.io:2.0.0-pre.4(Dokku/Docker), orgit pull origin master && npm install --ignore-scripts && npm rebuild && sudo systemctl restart pryv-master.service(raw deploys).
2.0.0-pre
Section titled “2.0.0-pre”First v2 preview — major consolidation and new features.
Open source
- Open Pryv.io v2 is now fully open source under BSD-3-Clause. The former “Enterprise version” and Open Pryv.io are now the same codebase.
- Docker image published as
pryvio/open-pryv.io(waspryvio/corefor v1). Pullpryvio/open-pryv.io:2.0.0-pre.
Consolidated runtime
- Registration, MFA, high-frequency series and previews are now served by a single binary (
bin/master.js), replacing the previous per-service Docker images (pryvio/core,pryvio/hfs,pryvio/preview,service-register,service-mfa). - Cluster mode: configurable N API workers + M HFS workers + optional previews worker under one master process.
- High-frequency series endpoints (
/{user}/events/{id}/series,/{user}/series/batch) are reachable on the same public port as the rest of the API — an in-process dispatcher routes them to the HFS worker on:4000. Setcluster.hfsWorkers: 1(or more) to enable HFS; no external reverse proxy required. For high-throughput installs, front the cluster with nginx (reference vhost:docs/nginx-ingress-sample.conf). SDKs readfeatures.noHFon/service/infoto know whether HFS is served — auto-derived fromcluster.hfsWorkers.
Multi-factor authentication (ported from the former Enterprise version)
- New API:
mfa.activate,mfa.confirm,mfa.challenge,mfa.verify,mfa.deactivate,mfa.recover. - When a user has MFA active,
auth.loginreturns{ mfaToken }instead of a regular access token. Clients must follow up withmfa.verify(SMS code) to obtain the Pryv access token. - Back-office
system.deactivateMfaremains available alongside the user-facingmfa.deactivate. - Disabled by default (
services.mfa.mode: disabled) — existing deployments see no change.
Registration merged into core (was service-register)
- All
/reg/*endpoints are now served by the main binary. No separate register service to deploy. - Backing store is PlatformDB (rqlite) — replaces the previous redis/leveldb storage.
- Legacy routes, invitations, DNS-less registration and multi-core indirection are all preserved.
Multi-core deployments
- Per-user routes (
/:username/*) enforce core affinity. Cross-core requests receive HTTP 421 Misdirected Request with body{ error: { id: 'wrong-core', message, coreUrl } }— clients must retry directly againstcoreUrl. No HTTP redirect is issued (cross-origin redirects stripAuthorizationheaders, WebSockets cannot follow). /reg/*and/system/*remain load-balanceable.- New
core.urlper-core config override for DNSless multi-core deployments where FQDNs cannot be derived from{core.id}.{dns.domain}. Other cores discover the URL viaPlatform.coreIdToUrl()(PlatformDB-backed).
Storage
- PostgreSQL backend (optional): full MongoDB parity for events, streams, accounts. Select via
storages.engines.*config. Note: v1→v2 data migration currently only operates from a MongoDB v1 source; migration into a PostgreSQL v2 backend is not yet supported. See INSTALL.md. - rqlite is now the only platform-storage engine — the legacy sqlite platform store was removed.
master.jsalways spawnsrqlited.
Upgrading from v1
- Toolkit at
dev-migrate-v1-v2: exports v1 user data (MongoDB) and produces a v2-compatible backup directory thatbin/backup.js --restorecan import.
Access versioning
accesses.updateis back (was removed in early v2).PUT /accesses/{id}mutates the head, snapshots the prior state into history, and bumps the access’sserial. Mutable fields:name,deviceName,permissions,expireAfter/expires,clientData. See Update access.- New
GET /accesses/{id}(Get one access) accepting either bare<base>or composite<base>:<serial>ids. Composite with an older serial returns the historical snapshot + acurrenthint. Pass?includeHistory=truefor the full version history. - Composite-id wire format:
access.id/access.createdBy/access.modifiedBynow serialise as<base>:<serial>once an access has been updated at least once. Never-updated accesses still serialise as bare cuid — fully backwards-compatible. Usepryv.utils.parseAccessRef(lib-js ≥ 3.1.0) to extract{ base, serial }. - Composite-id conflict:
accesses.updateandaccesses.deleterequire the caller’s{id}to match the current head’s serial. A stale composite returns409 stale-resourcewithdata: { provided, currentSerial }; refetch and retry. - New socket.io event
accessUpdatedfired after every successfulaccesses.update, alongside the existing coarse-grainedaccessesChanged. Payload:{ type: 'access-updated', accessId: '<base>:<serial>', serial }. - Chain rules: shared-access permissions must remain a subset of their managing app; narrowing a parent rejects with
offendingChildrenif any managed child would be orphaned. Expiry chain enforced on bothaccesses.updateAND retrofitted onaccesses.create(breaking if you previously created shared accesses with longer expiry than the parent app).
Cross-account Messaging & Consent (CMC plugin)
- New
:_cmc:reserved stream-id namespace + ninecmc/*event types power direct cross-platform / cross-core consent + chat + system-channel flows between two Pryv.io accounts — without shared CA, federation auth, or a central registry. The peer’sapiEndpoint(with its existing access token) is the entire auth surface. - Per-app scoping: data lives under
:_cmc:apps:<app-code>:[<user-path>:]chats:<counterparty-slug>and:_cmc:apps:<app-code>:[<user-path>:]collectors:<counterparty-slug>so an access can be scoped at app-level (:_cmc:apps:<app>:*) or per-request (:_cmc:apps:<app>:<request-slug>:*) via natural prefix matching. - One-shot capability-URL handoff for cross-platform acceptance; bidirectional shared accesses post-acceptance.
- Server-stamped
content.fromon inbox writes (unforgeable counterparty identity). - Auto
accesses.updatepost-hook deliverscmc/system-scope-update-v1to the peer whenever a CMC-tagged access has its permissions changed. - Outbound retry queue persists pending retries as events in
:_cmc:_internal:retrieswith exponential backoff; no new storage primitive. - Backwards-compat: nothing legacy changes; deployments that don’t use CMC see the namespace as inert. No migration required.
- Client SDK:
@pryv/cmc(lib-js sibling package,npm install @pryv/cmc) ships slug + stream-id builders, lifecycle wrappers (createInvite,acceptInvite,sendChat,sendSystemAlert,revokeRelationship,waitForAccept,listAcceptedRelationships,readOffer), and a frozenerrorIdscatalogue mirroring the server-sideCmcErrorIds. Source:lib-js/components/pryv-cmc/. Design docs:components/cmc/README.md,IMPLEMENTERS-GUIDE.md,INTERNALS.md.
Fixes
- Password-reset email (2.0.0-pre.3): the
account.requestPasswordResetmail now reliably embeds the platform’s password-reset URL even when that config value is populated by an override or extra-config plugin. Previously, in some boot orderings the captured config slice missed the value and the Pug template rendered a relative?resetToken=…href that Outlook/Apple Mail QuickLook silently dropped. New template substitution#{RESET_LINK}provides the pre-composed full URL — existing templates using#{RESET_URL}?resetToken=#{RESET_TOKEN}keep working unchanged.
Known gaps in 2.0.0-pre
- OAuth2 authorization-code flow (RFC 6749
/oauth2/authorize,/oauth2/token, client registration, refresh tokens, PKCE) is not in this preview. Clients that need OAuth2-style authorization must continue using the existing/reg/accesspolling flow (now core-affinity aware in multi-core deployments).
- Added Audit from Entreprise version to Open-Pryv.io.
- Refactored Attachments (Event Files) Logic to be modular for future cloud storage of files such as S3.
- Implemented ferretDB compatibility allowing full-open source modules
- Replaced rec.la by backloop.dev
Many under-the-hood changes and a couple fixes, including:
- Stream response to
streams.deletemethod to avoid potential timeout - Deleted stream ids are now already reusable when following the auth process
- Username is not available anymore form
usernamesystem stream. It should be retreived fromaccess-info.
Fixes migration issue when upgrading from 1.6.x to 1.8.0
New features:
- Password policy support: rules for password complexity (length, character categories), age (minimum, maximum i.e. expiration) and reuse (i.e. history) can be enabled in the platform settings under ‘Advanced API settings’
- External data stores support (a.k.a. dynamic mapping, or personal data mapping); enterprise users please contact us for the details.
1.7.14
Section titled “1.7.14”- Fixes two issues with
selfRevokepermissions, one of which related to the system streams backward compatibility flag; the issues caused a crash and prevented creation of accesses withselfRevokepermissions.
1.7.13
Section titled “1.7.13”- Fixes for miscellaneous issues, including issues with the system streams backward compatibility flag (
BACKWARD_COMPATIBILITY_SYSTEM_STREAMS_PREFIX), occasionally sluggish performance when querying events by type, and an occasional failure to restart services after a configuration change.
1.7.10
Section titled “1.7.10”- API change: Don’t coerce the event content values according to type
- Fixes: Allow event types validation for array
- Security fix: make password reset token single-use
Changes:
- Audit has been re-implemented, offering improved performance:
- See the Audit logs guide for API usage
- Audit logs are now available through the Events API, deprecating the previous
GET /audit/logsroute and its data structure
- System streams have been modified. Their prefix changes from
.(dot) to:_system:&:system:. See the System streams page for details. - Tags have been removed from Events. In Pryv.io platforms that contained them, they are migrated to streams, See
BACKWARD_COMPATIBILITY_TAGSplatform parameter in your platform configuration. The tags functionality is ensured by Streams queries for the events.get API method. - Permission levels are computed differently: If a child stream has a different permission than a parent, its level is indeed applied on the child (instead of the higher permission taking precendence as was done before).
- Integrity hash is computed for Events, Attachments and Accesses. This functionality can be disabled.
- Automated platform migration using the migrations.get and migrations.apply API methods.
1.6.20
Section titled “1.6.20”New routes:
- Deactivate MFA for admin API, for when the user has lost his 2nd factor.
1.6.19
Section titled “1.6.19”New routes:
- Get core API method that returns the hostname of the core on which a certain user data is stored.
New Features:
Removals:
- Deprecated “GET /who-am-i” API method removed
- Remove pryvuser-cli, as it is now available through the admin API
Changes:
- Custom auth function has now access to all request headers. See custom authentication guide.
Changes:
- increase JSON input payload to 10MB for HF server. See Data format.
v1.6.0
Section titled “v1.6.0”New features:
- System streams:
- Customizable unique and indexed properties for registration
- Account data accessible through Events API
- More details on System streams
- Admin API:
- Edit platform parameters
- Manage platform users
- More details on Admin reference
- Admin Panel:
- Web application for entreprise Pryv.io platform administration
Changes:
- New registration flow, more details on Account creation
Deprecated:
- Old registration flow
v1.5.22
Section titled “v1.5.22”Changes:
- Deleting an app token deletes the shared accesses that were generated from it (if any).
v1.5.18
Section titled “v1.5.18”New Features:
- Call ‘GET /access-info’ now returns the username to avoid having to extract it manually from
pryvApiEndpoint.
Changes:
- Call ‘POST /user’ (create user) on register. The property
serveris now deprecated in favor ofapiEnpoint.
v1.5.8
Section titled “v1.5.8”New Features:
- Socket.io v2
Removals:
- Socket.io v0.9
v1.5.6
Section titled “v1.5.6”Changes:
- Webhooks API routes now available for
sharedaccesses. - Socket.io interface availablel for
sharedaccesses. - Socket.io interface availablel for accesses with
create-onlypermissions.
v1.5.5
Section titled “v1.5.5”New feature:
- Access permission
{ "feature": "selfRevoke", "setting": "forbidden"}, more details on Access data structure.
New Features:
- Events can now be part of multiple streamIds
authUrlreplacesurlin Auth request in-progress responsepryvApiEndpointreplacesusernameandtokenin Auth request accepted responseaccesses.deletehas been extended for self revocation tosharedandappaccesses
Deprecated:
event.streamId: replaced byevent.streamIdsevent.tags: their functionality will soon be totally replaced by streamIdsurlin Auth request in-progress responseusernameandtokenin Auth request accepted response
Removals:
- Timetracking functionalities have been removed
- singleActivity streams are now standard streams
events.startevents.stop
accesses.update
New features:
- Auth request now accepts a custom
serviceInfoobject, which is returned by the polling url. In case of success, apryvApiEndpointfield is returned. See Auth request for more details. - Add
create-onlypermission level. See the Access data structure for more details. - Add multi-factor authentication for login using the optional MFA service. See the MFA API methods for more details.
- Add auditing capabilities through the Audit API. See the Audit API methods for more details.
- Pryv.io API now supports the Basic HTTP Authorization scheme.
- Release of webhooks to notify of data changes. See Webhook data structure and methods for more details.
- Add route
/service/infothat provides a unified way for third party services to access the necessary information related to a Pryv.io platform. See description for more details. - Most API calls now present a
Pryv-Access-Idresponse header that contains the id of the access used for the call. This is the case only when a valid authorization token has been provided during the request (even if the token is expired). See metadata for more details.
Changes:
- Enrich access-info result with exhaustive access properties.
- Improve the update account API call, in particular when it applies a change of email address. It now correctly checks if the email address is not already in use before updating the account and throws consistent errors.
Deprecated:
- Timetracking functionalities
- singleActivity streams
- events.start
- events.stop
New features:
- High Frequency events allow storing data at high frequency and high data density. Create them by using types that start with
series:X, where X is a normal Pryv type. The API also supports inputting data into multiple series at once, this is called a ‘seriesBatch’ (POST/series/batch). - Add
clientDatafield to Accesses. - Add
httpOnlyflag to server-side cookie sent in response to successful/auth/loginrequest. - Deleted accesses can now be retrieved. See accesses.get method for more details.
- Accesses can now be made to expire. See the access data structure documentation for more details.
Changes:
- Some invalid requests that used to return a HTTP status code of 401 (Unauthorized) now return a 403 (Forbidden). Only the requests that are missing some form of authentication will return a 401 code.
updates.ignoreProtectedFieldsis now off by default. This means that updates that address protected fields will result in an error being returned.
Changes:
- Fix login with Firefox (and other browsers using Referer but no Origin).
- Security fix 2018020801: ‘accesses.update’ was missing an authorisation check.
- Update of the API version in API responses.
- Fix events.get JSON formatting.
- Add configuration options to disable resetPassword and welcome emails.
- Add configuration option to ignore updates of read-only fields.
- Tags have a maximum length of 500 characters. An error is returned from the API when this limit is exceeded.
v1.1.8
Section titled “v1.1.8”Changes:
- Fix edge-case behaviour on very large
streams.deleteoperations. - Direct
events.getAPI call now supports really large results. Changes made have improved this call’s performance by around 30%. As a by-product of this change, we now do not send the ‘Content-Size’ HTTP header anymore. - Allow custom cuid-like ids when creating events.
New feature:
- Versioning:
- a new endpoint on
/events/{id}allows to retrieve a specific event by hisid. Setting theincludeHistoryparameter totrue, the response will contain an array of the previous versions of the event in thehistoryfield.
- a new endpoint on
Validated initial set of API features.
Changes:
- Deletion methods now:
- Reply to permanent deletions with a
{item}Deletionfield confirming the deleted item’s identifier. - Always return code 200 on HTTP (that’s a rollback of the v0.7.x change which was a bit too zealous to be practical).
- Reply to permanent deletions with a
New features:
- Event and stream deletions are now kept for sync purposes; they’re accessible via parameter
includeDeletions(events.get) orincludeDeletionsSince(streams.get). Deletions are cleaned up after some time (currently a year).
Major changes here towards more standardization and flexibility:
- All JSON responses (both in HTTP and Socket.IO) are now structured as follows:
{ "{resource}": {...} }if a single resource item is expected; for example:{ "event": {...} },{ "error": {...} }{ "{resources}": [ {...}, ... ] }if an indeterminate number of items is expected; for example:{ "events": [ {...}, ... ] }
- All responses to resource creation and update calls now include the full object instead of respectively its id and nothing; for example:
{ "stream": {...} } - All JSON responses now include
meta.apiVersionandmeta.serverTimeproperties mirroring the originalAPI-VersionandServer-TimeHTTP headers; HTTP headerAPI-Versionremains - Deleting a resource now returns code 204 if the item was permanently deleted; it still returns a 200 when trashed (now including the trashed item in the response)
- Method ids for deletion/trashing are now
{resource}.deleteinstead of{resource}.del - The
attachmentsproperty of events is now an array (instead of an object), with each attachment now identified by a newidproperty (instead offileName) - As a security measure, reading attached files now either requires auth via the
AuthorizationHTTP header or a newreadTokenquery string parameter (authisn’t allowed anymore in this case); the token to use is specific to each file and access, and is defined in thereadTokenproperty of each event attachment - Event batch creation method has been replaced with generic batch method (
callBatch, HTTP:POST /) - Bookmarks have been renamed to “followed slices”, corresponding method ids to
followedSlices.*and HTTP routes to/followed-slices - Getting events: setting the
tagsparameter now returns events with any of the specified tags, instead of all of them - Error ids:
unknown-*errors replaced with eitherunknown-resourceorunknown-referenced-resourceitem-*-already-existsreplaced withitem-already-existsmissing-parameterreplaced withinvalid-parameters-format
- Other improvements and fixes (data validation performance, minor bugs on auth for trusted apps)
New features:
- Getting events: filter for specific event types with the
typesparameter - Accesses can now define tag permissions in
permissions(in addition to the existing stream permissions)- If only tag permissions are set, all streams are considered readable, and vice-versa
- When stream and tag permissions conflict, the highest permission level is considered
- Full support for managing account information, including password change and reset
Changes to HTTP paths and auth for trusted apps:
- Get streams: removed
trashedoption forstateas it was more trouble than anything useful - Accesses now includes property
id(exposed for referencing)- Create access response now includes both
idandtokenproperties - For existing accesses,
idandtokenare equal
- Create access response now includes both
- Events, streams and accesses now includes change tracking properties:
createdandmodified(timestamp)createdByandmodifiedBy(access id or"system")
- Socket.IO method calls now directly use method ids (e.g.
events.createand pass method params, instead of usingcommandand passing an object with method id and params - For trusted apps only: removed the distinction between “admin” methods and others; breaking changes
/admin/login,/admin/logoutand/admin/who-am-imoved to/auth/login,/auth/logoutand/auth/who-am-irespectivelysessionIDrenamed totokenin login response and SSO cookie data- Personal accesses are now automatically created on login; they can’t be created explicitly anymore
/admin/user-infomoved to/user-info/admin/accessesmerged into/accesses/admin/bookmarksmoved to/bookmarks/admin/profilemerged into/profile
This is a major update that will break most libs and clients, which should be updated ASAP.
- Simplified the API by removing channels and renamed folders into “streams”; adjusted the structure of accesses, streams and events accordingly; more details:
- As a consequence, every event now belongs to a stream
- Data migration: former channels will be converted into root-level streams, and former folders into sub-streams of those
- Events structure:
event.typeis now a string of format{class}/{format}(e.g.picture/attached) instead of an object withclassandformatpropertiesevent.valuehas been renamed toevent.content
- Get events:
- Renamed parameter
onlyFoldersto juststreams - Added
runningboolean parameter, replacing “get running periods” method
- Renamed parameter
- Removed “get running periods” (i.e.
GET /events/running, see above) - Removed
hiddenproperty of streams (ex-folders), which was mostly unused and out of place
- New feature: Allow HTTP method overriding by POSTing _method, _json, and _auth parameters in an URL-encoded request
- Improvement: Retrieving events for a specific timeframe now includes all events that overlap that timeframe, including period events that started earlier
- Added event type validation: the API will now check if an event being created or updated has a known type (as listed on our event types directory), and if yes perform data validation on its value (returning a 400 error if invalid)
- All error ids have been changed to use
slug-styleinstead ofC_CONSTANT_STYLE(so that e.g.INVALID_PARAMETERS_FORMATis nowinvalid-parameters-format); this is consistent with the other ids we’re using in the system
Earlier
Section titled “Earlier”Versions earlier than v0.4 are not covered here.

