···1313The current baseline for saying "the audited suite is green" is:
14141515- `prove -lr t`
1616- - latest full green result in the realigned Meridian worktree: `Files=48, Tests=2898`
1616+ - latest full green result in the realigned Meridian worktree: `Files=48, Tests=2918`
1717- `prove -lv t/server-auth.t`
1818- `perl -c script/differential-validate`
1919- `PERLSKY_RUN_REFERENCE_DIFF=1 prove -lv t/reference-differential.t`
···5959- `com.atproto.repo.createRecord` follows the reference runtime by ignoring a stray `swapRecord` field, and direct reference coverage now pins `putRecord` / `deleteRecord` `swapCommit` and `swapRecord` mismatch semantics explicitly.
6060- App-password sessions follow the official runtime more closely than the older local assumptions did: access-token scopes use the `com.atproto.appPass` / `com.atproto.appPassPrivileged` names, standard app-password sessions may list app passwords, privileged-only `getServiceAuth` failures report `InvalidRequest`, and revoked refresh tokens on `refreshSession` fail with `400 ExpiredToken`.
6161- `com.atproto.server.requestPasswordReset` and `com.atproto.server.deleteAccount` now follow the reference form-token flow, with focused regression coverage for missing-account and bearerless deletion semantics.
6262+- Password-bearing account endpoints need the same bounded-length behavior as the official runtime: `createAccount` rejects passwords longer than 256 characters, `createSession` rejects passwords longer than 512 characters with the reset hint, and `resetPassword` / `deleteAccount` reject overlong password inputs with `Invalid password length.`
6263- `com.atproto.server.createAccount` with an explicit `did` must behave like an authenticated migration flow: require auth from that same DID, keep the existing DID document, and start the new account deactivated until activation catches the DID document up to the new PDS.
6364- `com.atproto.server.checkAccountStatus` must validate the stored DID document against the PDS service endpoint and signing key, and `com.atproto.repo.describeRepo` must derive `didDoc` / `handleIsCorrect` from that document instead of hardcoding success.
6465- `com.atproto.sync.getBlob` should ship the same download-hardening headers as the reference PDS (`X-Content-Type-Options`, `Content-Disposition`, `Content-Security-Policy`).
···116117| `t/oauth-permissions.t` | audited local regression | granular OAuth permission enforcement across account/email, identity, repo, blob, and rpc scope families |
117118| `t/oauth-scopes.t` | audited local regression | OAuth scope parsing, normalization, and token-grant shaping |
118119| `t/oauth.t` | audited local regression | OAuth provider metadata, PAR, PKCE, DPoP, and token lifecycle coverage |
119119-| `t/password-reset.t` | audited local regression | password reset token issuance and missing-email rejection semantics |
120120+| `t/password-reset.t` | audited local regression | password reset token issuance, case-insensitive email lookup, and overlong-password rejection semantics |
120121| `t/pds_smoke.t` | local correctness/infrastructure | broad local PDS smoke; still intentionally optimistic and should only carry a small number of negative assertions |
121122| `t/plc-identity.t` | direct reference differential | PLC mock driven by official library semantics |
122123| `t/reference-differential-plc.t` | direct reference differential | official runtime comparison in PLC mode |
···125126| `t/repo-api.t` | audited local regression | record mutation and read semantics, but still lighter than ideal on some negative/reference edge cases |
126127| `t/repo-firehose-car.t` | audited local regression | repo commit CAR shape and firehose interactions |
127128| `t/repo_formats.t` | audited local regression | direct repo wire-format and CAR expectations |
128128-| `t/server-auth.t` | direct reference differential | auth/session/service-auth behavior repeatedly compared to official runtime |
129129+| `t/server-auth.t` | direct reference differential | auth/session/service-auth behavior repeatedly compared to official runtime, including bounded create-session password semantics |
129130| `t/service-proxy-local.t` | audited local regression | local appview fallback behavior |
130131| `t/service-proxy.t` | audited local regression | upstream proxy behavior plus conservative local appview fallback and preference semantics |
131132| `t/sqlite-binary.t` | local correctness/infrastructure | SQLite binary round-trip correctness |
+12-1
lib/ATProto/PDS/API/Server.pm
···40404141our @EXPORT_OK = qw(register_server_handlers require_auth require_access_or_service_auth session_view);
42424343+my $OLD_PASSWORD_MAX_LENGTH = 512;
4444+my $NEW_PASSWORD_MAX_LENGTH = 256;
4545+4346my %PROTECTED_SERVICE_AUTH_METHOD = map { lc($_) => 1 } qw(
4447 com.atproto.identity.requestPlcOperationSignature
4548 com.atproto.identity.signPlcOperation
···7275 my $password = $body->{password} // q();
7376 xrpc_error(400, 'InvalidPassword', 'Passwords must be at least 8 characters long')
7477 if length($password) < 8;
7878+ xrpc_error(400, 'InvalidRequest', "Password too long. Maximum length is $NEW_PASSWORD_MAX_LENGTH characters.")
7979+ if length($password) > $NEW_PASSWORD_MAX_LENGTH;
75807681 my $invite;
7782 if (defined($body->{inviteCode}) && length($body->{inviteCode})) {
···205210206211 $registry->register('com.atproto.server.createSession', sub ($c, $endpoint) {
207212 my $body = $c->req->json || {};
213213+ xrpc_error(401, 'AuthRequired', 'Password too long. Consider resetting your password.')
214214+ if length($body->{password} // q()) > $OLD_PASSWORD_MAX_LENGTH;
208215 my $account = find_account($c, $body->{identifier} // q());
209216 xrpc_error(401, 'AuthRequired', 'Invalid identifier or password') unless $account;
210217 xrpc_error(401, 'AuthRequired', 'Invalid identifier or password')
···442449 my $body = $c->req->json || {};
443450 xrpc_error(400, 'InvalidPassword', 'Passwords must be at least 8 characters long')
444451 if length($body->{password} // q()) < 8;
452452+ xrpc_error(400, 'InvalidRequest', 'Invalid password length.')
453453+ if length($body->{password} // q()) > $NEW_PASSWORD_MAX_LENGTH;
445454 my $token = _require_action_token($c,
446455 token => $body->{token},
447456 purpose => ACTION_TOKEN_PASSWORD_RESET,
···596605 my $account = $c->store->get_account_by_did($did);
597606 xrpc_error(400, 'InvalidRequest', 'account not found')
598607 unless $account && !defined($account->{deleted_at});
599599- xrpc_error(401, 'AuthRequired', 'Invalid identifier or password')
608608+ xrpc_error(400, 'InvalidRequest', 'Invalid password length.')
609609+ if length($body->{password} // q()) > $OLD_PASSWORD_MAX_LENGTH;
610610+ xrpc_error(401, 'AuthRequired', 'Invalid did or password')
600611 unless verify_account_password($c, $account, $body->{password} // q());
601612 my $token = _require_action_token($c,
602613 token => $body->{token},