Skip to content

Develop - #1129

Merged
rcbjBlueMars merged 76 commits into
mainfrom
develop
Oct 28, 2026
Merged

Develop#1129
rcbjBlueMars merged 76 commits into
mainfrom
develop

Conversation

@rcbjBlueMars

Copy link
Copy Markdown
Contributor

No description provided.

rcbjBlueMars and others added 30 commits October 27, 2026 19:13
…wn: behind the balancer the second poll can land on a node that has not yet replicated the first poll's lastPolledAt (cluster mode, run #5).
…the first GET /admin-api/scheduler after a realm's removal never answered and the job was killed silent at 300 s; the next hang names its request.
).

Past risk.populationUsersCacheS the held count is answered and counted
again in the background; only a realm's first count is waited for. On 8081
the sign-in that found it due waited 9.6 s for a cold count of a million
rows. A failed recount keeps the held number.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ot its lane (#1076).

It was drawn from the lane alone (#643), which is `pqc` only while
webauthn.pqcAlongsideClassical is on (off by default), so an ML-DSA passkey
said "classical" beside an Algorithm row saying post-quantum. A post-quantum
key in the post-quantum lane keeps its second-account note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…st by default (#1078).

webauthn.algorithms' default puts the strongest parameter set first; every
other algorithm keeps its place. POST_QUANTUM_ALGORITHMS, the list a
post-quantum-only or pq-first request offers when its list names no ML-DSA,
follows the same order. env/defaults.js and the docs follow the table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ortal were the only receivers.

`node env/generate_defaults.js` over develop. The internalReceivers and
actOnSignalsInDevelopment comments are the table's labels since #1042 made
reports a third receiver; the workers group's comment column is realigned.
No value changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…).

An AAGUID, AAID or attestation certificate key identifier, its kind told
by its shape, answered by RiskDatasets.describeAuthenticator(): the model,
certification, latest status, compromised, status reports and statement,
or why nothing is listed (not-a-key, all-zero, no-blob, not-listed,
unreadable). A stale BLOB is still asked and marked stale. The page's
second lookup form and GET /admin-api/risk?authenticator= (rule 7), as
the view's authenticatorLookup. Nothing that decides reads it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…vice Root: #1064 completed every reported chain through pki.chainThroughRoot() and this check still said "three certificates, no Root" (run #6, memory mode). The issue-pep-certificate description and docs/remote-pep.md say the fourth certificate too.
The tab's label is its section heading, which still said "Look up an
address" after #1080 put the authenticator lookup beside it. The heading
is now Lookups, and each form has a subheading: lookup.address.heading
(the old text and its translations) and authenticator.heading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both lookups are GET forms whose action was bare /admin/risk. The console
runtime navigates a GET form to its action's fragment, and the tab drawn
is the :target, so with none the answer opened on Metadata. Both forms
now go to /admin/risk#risk-tab-lookup, protocolTabs()'s id for the
section; tests/risk_realm_admin.js C3 holds both in that tab and
returning to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…portal and reports (#1086).

Server configuration → Broadcast Announcement (/admin/broadcast) and its
rule 7 twin POST /admin-api/broadcast/send: a message, the applications
(all three ticked), and for a service administrator in the default realm
this realm or every realm. It travels as this service's own sts-family
SET, urn:iya:sts:secevent:event-type:announcement, on the three internal
receiver streams CAEP and RISC already use (they now ask for it; a stream
seeded earlier is made to ask at the next seeding), and only on those.

Each surface's receiver collects it over /ssf/poll as before and keeps a
verified one naming it for ssf.announcementKeepS. Every open tab asks its
own API every ssf.announcementPollMs (/admin-api, /account-api,
/report-api /announcements): the first ask fixes the tab's start, so only
what arrives later is shown. One browser module, common/web_announcements.ts,
draws the banner for all three: the floating flash shape, text only, an ×,
closed after ten seconds; it asks through sendOnce so a background poll
never begins a sign-in.

Settings ssf.announcementPollMs, ssf.announcementKeepS,
ssf.announcementMaxChars; STS-SSF-0153..0155; catalogs and prose catalogs
in English and six translations; docs/shared-signals.md; ssf/CLAUDE.md.
tests/ssf_announcements.js in process, and
tests/protocol/sts_broadcast_announcement.js (local) over HTTP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1088).

On 8081 a broadcast answered "Nothing was sent": broadcastAnnouncement()
selected streams by record.internalSurface, the half of a stream record a
persisted round trip can lose (ssf/CLAUDE.md, One stream per surface),
which is why streamFor() asks for the derived id first. It now asks
streamFor() for each chosen surface, and when nothing is sent the answer
names each surface's reason: no stream, disabled, or does not take the
event. tests/ssf_announcements.js C2 (marker removed, still sent) and C3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uncement event (#1091).

After #1088 a broadcast on 8081 answered "does not take the announcement
event" for all three surfaces. seedStreams() asked for the type as the
module loaded, before the store's copy of the streams was read, so the
stored record without it was the one kept - moveToPoll()'s trap.
askForAnnouncements() is now asked by pollAll() with the store open and by
broadcastAnnouncement() just before it selects, and checks the requested
and delivered lists separately. tests/ssf_announcements.js C1b, C1c.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ust realm (#1090).

rcbj: the Signals tab served no purpose. The tab, drawSignals() and its
sixteen report.signals* catalog keys go; Refresh moves from the page bar
to the top bar, after the Trust realm field. The receiver stays (CAEP/RISC
reactions, #1086 announcements), and GET /report-api/signals stays as an
API; its inboxPath, the ssf.internalReceivers description (and its prose
catalog translations), docs/signals-received.md, docs/shared-signals.md,
report/CLAUDE.md and ssf/CLAUDE.md now name the API, not a page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ore asserting the shared secret: behind the balancer the draw can land on a node that has not yet pulled the started enrolment's pendingTotp row (cluster mode, run #6).
…inct jti, not by arrival: a lease-claimed delivery may POST the same token twice (section 2.6 deduplicates), which run #6 met in cluster mode.
A browser receiver (rcbj/oidc-caep-client) creates its stream with the
person's token and deletes it at sign-out; a tab closed for good left it
for ever (ssf.inactivityTimeoutS is 0 by default, for push receivers), and
a person who kept losing the stored id reached ssf.maxStreams. A stream a
person owns and collects by poll now takes ssf.personStreamInactivityTimeoutS
(86400) and ssf.personStreamInactivityAction (delete) when neither its
application nor ssf.inactivityTimeoutS sets a timeout; push streams are
excluded. inactivityTimeout()/inactivityAction()/isPersonPollStream() in
ssf_streams.ts; the maintenance job and its off() use them. Settings,
prose catalogs in six languages, docs/shared-signals.md, ssf/CLAUDE.md,
tests/ssf_person_streams.js section 7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ments store (#1086) is classified in persistence/tiers.js, GET /admin-api/announcements names what it mirrors (in words, as /me does), and report_query expects Refresh in the top bar where #1090 moved it.
rcbjBlueMars and others added 29 commits October 28, 2026 04:52
…mon/ and tests/CLAUDE.md (the deferred allow-list sentence removed, the captured-registration fixture format written down), and the two settings' prose in every full settings catalog.
…s capabilities ride on client after a '|', so audit.js's cap no longer drops the attestation field, which carries the enterprise downgrade.
…instead of being skipped after one stale read (#1118).

Run #7's cluster mode skipped a new realm's first minted change (audit.nums)
on node-b under STS-STORE-0089 for a key node-a had made 0.8 s earlier. The
one read of the data-key rows could be a refresh already in flight, sent
before the key's row committed, or prefetch()'s page read, which marked the
key unheld.

keystore.refreshDekRows({ fresh: true }) now waits out a read in flight and
reads again. persistence_minted.js's waitForDek() reads up to five times,
200 ms apart, one wait per key shared by its changes (the directory
applier's keyedRetries shape), and only then notes the key for the two-second
skip. prefetch() no longer notes a key unheld. STS-STORE-0089 now means a
key still not held after the reads. tests/minted_unheld_dek.js U5 and U6;
UNHELD_READS = 1 fails U3, U5 and U6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he actions are: start, pause, resume, end."), the sentence shape the two parity checks read; run #8 found sts_admin_api_operations red on it (#1111).
…nd keeps webauthn.passkeyHistoryDays of them; the passkeyHistory source in /report reads them, with three predefined queries (#1109).

The passkey report's recount, right after it writes the baseline, writes
passkey_report.history keyed by the UTC day: people by kind, passkeys by
algorithm, by lane and by attestation algorithm, and each named passkey
profile's population, bounded as the profile filter is and at most
twenty-five profiles. The same run removes the rows older than the new
setting webauthn.passkeyHistoryDays (400 by default). A row that cannot
be written or pruned is logged (STS-AUTHN-0520, 0521) and fails nothing;
a profile whose population cannot be read is recorded as not counted
(STS-AUTHN-0522). The store is the cell tier's.

report/passkey_history.ts projects each stored day into rows told apart
by scope (realm, profile, algorithm, lane, attestation); the catalog's
passkeyHistory source carries three presets: post-quantum adoption by
day, passkeys by algorithm by day, adoption by passkey profile by day.
tests/passkey_history.js holds the row, its retention, the source and
the presets, in process and over /report-api.
docs/passkeys.md: the move over time, read in /report, and the setting.
docs/reporting.md: the passkeyHistory source, its scopes, fields and
predefined queries. common/CLAUDE.md 3cs argues writing the row in the
recount rather than a sibling job; report/CLAUDE.md the one-row-per-scope
projection; cluster/CLAUDE.md's job table and tests/CLAUDE.md's row.
persistence/CLAUDE.md records that the tier scan reads literals only, so
the passkey report's two earlier stores are unlisted and sealed for the
cell tier by the unclassified fallback.
…tings catalog, and the adoption preset's limit at the synchronous cap so it answers at once (#1109).
…duration, and a device link writes one (#1108).

Each door hands webauthn_ceremonies.record() the application in play and
the instant its pending ceremony's options were issued; durationMs is
measured from that on the server (the step's optionsAt, the screen's
passkeyChallengeAt, a pending enrolment's or upgrade's issuedAt, an
upgrade's provenAt, a device challenge's issuedAt). The row was already
thirteen fields, so audit.js's cap had been dropping the attestation: the
attachment, transports and attestation now fold into `authenticator`, and
postQuantum, a function of the algorithm, is no longer stored.
unfold() reads both the new and the older shape back. The device link
(device_enrolment.finishLink()) wraps its check and writes the row, door
device-link. tests/webauthn_ceremony_log.js covers all three.
…rows, with three predefined queries (#1107).

One row per webauthn.ceremony and webauthn.algorithm-mismatch audit row
still in the ring, read as authActivity reads it (the fresh() wait, the
audit.maxEvents cap) and projected by report/passkey_ceremonies.ts: the
door, outcome, client, error name, offered order and the client rule
after its first bar (#1112, offerRule), the algorithm used and whether
either is ML-DSA, the application and server-measured duration, and the
authenticator facts unfolded from either row shape. Nothing is indexed:
the ring is a list in memory and the read is bounded by it.

Its three predefined queries are #1110's presets: ML-DSA registrations
that failed by client, registrations that asked for ML-DSA and made a
classical passkey by client, and classical sign-ins by people who hold an
ML-DSA passkey. The query form now draws a saved or preset boolean
filter. The catalog has thirteen sources.
… and how to find out for a realm (#1113).

A section of docs/passkeys.md for operators planning the move: the five
kinds of failure (a whole registration failing instead of falling back,
the offered order not honoured, a security key with no PIN, payload size
on tight transports, slower large parameter sets), what each looks like
on the ceremony row, what the service does about each (the classical
retry, #1112's client rules, the registration modes and the offer's
order, list-and-refuse), and the passkeyCeremonies report source as the
way to measure a realm. The dated compatibility table it replaces named
products, versions and outside reports, which go stale; no such list
remains.
…nt apart from capabilities a #1114-shaped row carried on it, and the docs describe the attestation inside authenticator (#1107, #1108).

#1114 met the same twelve-field cap and moved the capabilities onto
client after a bar; the two folds are reconciled into #1108's shape
(client plain, capabilities its own field, the enterprise downgrade
leading the attestation part of authenticator). unfold() and the
passkeyCeremonies projection read a row written either way.
…ys, with a section on post-login activities (#1122).

The short pointer to post-login activities becomes a walk-through: who is
asked, how the policy is targeted and turned on, what a person sees at
sign-in (the methods, Skip and the allotted time, the classical fallback, a
device that cannot make ML-DSA), the mail reminders, and the three report
sources. Requiring a post-quantum attestation moves beside requiring a
post-quantum passkey. Stale text goes: the default algorithm order since
#1078, the upgrade paragraph that predates the post-login method, the
refusal page's link to an upgrade, and the duplicated door list. The
lanes row, the post-login policy and its operations, and the missing
passkey error codes are added to the page's tables.
…-list pages, and the passkey reference brought up to #1100-#1114 (#1122).

The authentication page gains a short section on a sign-in held for a
post-login activity, the passkey policy's lanes row, the post-login step
as a place a passkey is registered, and the two WebAuthn settings its table
lacked. The console's Policies section names every policy kind and
describes the post-login activity policy; the page list has
/authn/post-login. The post-login page says what an administrator can do
for a device that cannot make ML-DSA, that the attestation rule always adds
the passkey alongside, and where the reminder's link goes.
… every section ran and passed in run #8's memory mode and the job then failed on its own count (#1111).
…sfCluster.contend() and the scheduler tests' w.contend(), so the word #1123 removes appears nowhere in this repository (#1123).

Only the word changes; the role's lease, hold-off and heartbeat are as they were.
…is a post-login activity, and its groups are chosen by search, several at once, their union mailed (#1123).

ONE CONCEPT. common/post_login_activities.ts (was post_login_campaigns.ts,
class PostLoginActivities) keeps a realm's started post-login activities in
post_login.activities and what each run did in post_login.activityRuns; the
scheduler job is post-login.activity-mail; the audit actions are
admin.post-login-activity.change and authn.post-login.activity-run; the
operations are /admin-api/post-login-activities and its start, pause,
resume and end (mgmt-api/post_login_activities_api.ts); the console page is
Directory → Post-login activities (/admin/post-login-activities,
admin-ui/web_post_login_activities.ts, consolePostLoginActivities) with the
tabs Activities and Start an activity. A record is a StartedActivity (id
pla-…); the person's progress row and the postLoginProgress report column
that name it are mailedBy. The post-login activity policy kind on
Directory → Policies is unchanged: it is the rules a started activity
follows. No migration: no released data held a row under the old store
names.

SEVERAL GROUPS, THEIR UNION. A start takes `groups`, a list of cns or DNs
(at most fifty), each resolved through the group index; one the realm does
not have is refused by name with the new STS-AUTHN-0530. ldap_server.js's
groupMembersAfterAsync() now takes a list and walks the UNION of the
groups' member keys with one cursor, so a person in two groups is read and
mailed once. The duplicate check compares the set of DNs.

CHOSEN BY SEARCH. The start tab draws kit.chooserPane() from a slice the
view cuts (groupChooser: twenty from the clamped groupfrom, the chosen
left out), asked of the group index through the new groupChoices() and
groupsNamed(), never the directory's people. A result is a link adding its
group to the page's own `groups` query parameter (one DN a line), each
chosen group has a Remove link, and the form carries one hidden `groups`
per group. The GET operation takes groupq, groupfrom and groups and
answers groupChooser (rule 7).

Every catalog in the seven languages, the error-code text, the
sts_metadata descriptions, the tests (renamed, with the union, the
de-duplication, the chooser's slice and paging, the API's list and its
refusal) and each directory's CLAUDE.md follow.
…e group search and its list, the union of the groups, STS-AUTHN-0530, the API's groups, the report's mailedBy (#1123).
…hod does to the classical passkey, STS-AUTHN-0475 and 0309 as the code checks them, who can register a passkey for a person, and a dead heading link (#1123).

* post_login_policy.ts: the activity field said the classical passkey is
  kept; under `method: upgrade` it is replaced in place. It now says the
  method decides (the policies prose catalog in seven languages).
* STS-AUTHN-0475 blamed webauthn.pqcAlongsideClassical/pqcOnly; the code
  asks canRegister(): passkeys on, the role allowed, an ML-DSA algorithm
  offered (the default list has them), the post-quantum lane not refused,
  and for an upgrade the upgrade not refused.
* STS-AUTHN-0309 also names an unreadable passkey-policy clientRules rule.
* authn.postLogin.failedHelp no longer promises that an administrator can
  register a passkey: only where the person's passkey policy allows
  administrator enrolment, or by an upgrade from the console (seven
  languages).
* docs/risk-scoring.md and docs/xacml.md point at signals-received.md's
  "What the three applications do with a signal".
…with ';', since a query value may hold no control character and the line break was refused at the gate (#1123).

RFC 4514 escapes a ';' inside a DN value, so groupNamesOf() splits on an unescaped one only.
…sign-in, not only by mail (#1126).

A started activity turns the sign-in invitation on for its population, the
union of its groups by DN or the whole realm, under the rules of the profile
the post-login policy selects for each person; only `enabled` is overridden.
A named profile switched off still excludes its members; `default` off
excludes nobody. Paused or ended, the activity asks nobody, and a person the
policy itself enables is still asked. The realm's running activities are read
from the started store each process already holds (at most fifty rows), and
membership from the person's own groups through the directory's
personGroups() hook, asked once per question.

The progress row records who asked (`askedBy`: policy, or the started
activity's id) at the ask, Skip and done, never at a mail, and the
postLoginProgress report source shows it. The console page says what a
started activity does at sign-in and by mail in all seven languages and drops
the policy-off warning; the API descriptions, the docs and the CLAUDE.md files
follow. tests/post_login_started.js holds the selection in process.
…e as a refusal-close race: OpenSSL's unexpected_message alert and node's close reached tlsfuzzer the wrong way round once on the debugger listener (run #10 of 2026-10-28, memory mode), the race the kit already names for two other probes.
@rcbjBlueMars
rcbjBlueMars merged commit f125557 into main Oct 28, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant