feat(auth)+refactor(app): identifier at the access barrier; adopt the lib fidelity refactor

Consumer-side of the @ng-eventually/client fidelity pass, plus the identifier UX:

- Identity: the user types an IDENTIFIER at the access barrier (AccessGateScreen),
  in the same act that opens the shared wallet — the separate 'pick a username'
  screen (ConnexionScreen) is removed. The identifier is a technical id (a pseudo
  in practice, not a Festipod username), normalized (trim, @-stripped, lowercased)
  and persisted before the broker redirect, then handed to the SDK as the identity.
  AccountContext keeps its API but its stored value is now this normalized id.
- Relationship/connections are app-owned: new src/shared/utils/connections.ts holds
  the bilateral registry and maps each link to the SDK's directed grantRead(doc,
  grantee); the lib no longer carries a connection concept. Rewired FestipodData
  and the @data harness to it.
- Login removed: accounts use the SDK's IdentityStore (set/clear/get); no faux
  login/logout framing in the SDK boundary.

Doctrine reconciled: app-security (knowledge_authentication flow, knowledge_trust-model
directed grants, decision_2026-07-06_identifier-at-access-barrier), data-layer
(knowledge_context-internals: stable id principal + single-seed), app-architecture
(knowledge_screens auth inventory), bdd-testing (caveat_wallet-bloat-hang).

App gates: tsc no new errors, build OK. @data path unaffected (harness bypasses the
gate and sets identity directly; login() is not on that path).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-07-06 14:52:40 +02:00
parent 0911b1f9de
commit e951eaaf96
20 changed files with 311 additions and 230 deletions
+10 -7
View File
@@ -14,7 +14,9 @@ import { AccountProvider, useAccount } from '../context/AccountContext';
import { FestipodDataProvider, useFestipodData } from '../context/FestipodDataContext';
// useShape routed through the lib (SDK-identical surface); caps from /polyfill.
import { useShape, docs, inbox as docsInbox } from '@ng-eventually/client';
import { getCaps, getCurrentUser, setCurrentUser, resetCaps, declareConnections } from '@ng-eventually/client/polyfill';
import { getCaps, getCurrentUser, setCurrentUser, resetCaps } from '@ng-eventually/client/polyfill';
// Relationship is an app concept: directed grants come from the app's own module.
import { declareConnections, resetConnections } from '../utils/connections';
import { hostInboxNuri as regInboxNuri } from '../data/registration';
import type { DeepSignalSet } from '@ng-eventually/client';
// doc_create goes through the lib's `docs` primitive (T01.a): it calls the REAL
@@ -476,10 +478,11 @@ function ConnectedHarness() {
// (storeRegistry.createEntityDoc) does; the protected participations
// document is governed, and a separate makePublic'd doc models a public
// entity. <FilterProbe> exposes the read-filtered VIEW over the protected
// participations doc. `connect` calls the SDK's declareConnections — the
// app's domain sharing act — never touches a doc NURI or the registry.
// participations doc. `connect` calls the app's declareConnections — the
// domain sharing act — which issues the SDK's directed read grants.
governProtected(owner: string, reader: string) {
resetCaps();
resetConnections(); // clear the app's relationship registry too
// The protected participations document (owner-only read at first).
getCaps().open(protectedNuri!, 'protected', owner);
// A public entity document — readable by anyone regardless of caps.
@@ -487,9 +490,9 @@ function ConnectedHarness() {
setCurrentUser(reader);
setFilterActive(true);
},
/** Declare a BILATERAL owner↔reader connection to the SDK (domain sharing
* act). Each side asserts the other (bound to that identity); only then
* does the SDK issue the protected doc's read cap to the connection. */
/** Declare a bilateral owner↔reader connection (domain sharing act). Each
* side asserts the other; only a two-sided link makes the app issue the
* protected doc's directed read grant to the reader. */
connect(a: string, b: string) {
declareConnections([b], a); // a asserts b
declareConnections([a], b); // b asserts a → bilateral link materializes
@@ -524,7 +527,7 @@ function ConnectedHarness() {
const created = await reg.ensureAccount(username);
reg.resetRegistryCache();
const reloaded = (await reg.allAccounts()).find(
a => a.username === username,
a => a.id === username,
) ?? null;
return { created, reloaded };
},