Commit Graph

15 Commits

Author SHA1 Message Date
Sylvain Duchesne 88f396a7ac fix(caps): créer un document en donne le cap, + corriger 9 faits NextGraph
Le trou trouvé par l'e2e contre le broker en ligne : `docs.docCreate` ne
déposait aucun cap pour le créateur, donc un consommateur pouvait créer un
document par la primitive publique puis se voir refuser sa lecture et son
écriture. En amont c'est impossible — `doc_create` commite
`AddRepo { read_cap }` sur la branche Store du store, et le créateur le détient
dès le premier instant. Délibérément non répliqué dans `physical.ts` : les
documents du shim n'appartiennent à aucun utilisateur virtuel, et
`store-registry` classe leurs caps là où il sait à qui ils sont.

e2e : 22 passés / 8 échoués → 39 / 0. Les autres échecs venaient du harnais,
qui agissait comme une seconde identité sans l'établir, ou lisait un document
quelconque comme une inbox. Un run e2e contre un wallet persistant exige une
identité FRAÎCHE par run : `walletInbox(id)` rend l'inbox stable pour son
propriétaire — c'est son intérêt — donc un id fixe accumule les dépôts des runs
précédents (vert au 2e run, rouge au 3e, à code inchangé).

Revue adverse de la documentation, 9 défauts, tous vérifiés à la source avant
correction :

- « chaque document a une inbox native » est FAUX. Seuls les repos de store
  public et protected en ont une (`site.rs:128,149`) ; `new_store_default` n'en
  pose que `if !private` et `doc_create` laisse `inbox: None`. Le store privé
  n'en a pas non plus. Ce que le code fait est donc une ANTICIPATION — assumée
  et notée comme telle dans `documentInbox`, le brief et l'ADR discovery. Ce qui
  est vérifié, c'est la FORME : `AddInboxCapV0` est clé par `repo_id`.
- `InboxMsgContent::Link` est une variante unit sans charge utile : l'inbox ne
  transporte aucun ReadCap. `shareCap` était juste et le reste ; ses citations
  sont complétées aux deux bouts (émetteur `unimplemented!()`, récepteur qui
  ignore `details.read_cap`).
- les 3 stores appartiennent au user (`SiteV0`), pas au wallet ;
- le TODO `OpenRepo` ne concerne pas la lecture cross-wallet — il est dans
  `open_branch_`, après `RepoNotFound` ; charger par cap, c'est
  `load_repo_from_read_cap` ;
- la liste des méthodes JS était un sous-ensemble présenté comme la surface
  (77 exportées) ;
- `outbox-log.ts` n'enregistre rien : il inspecte l'outbox du SDK ;
- l'ADR private-store-nuri-scope citait `orm_start_graph` au présent, remplacé
  par `ensureRepoOpen` ;
- l'incident write-loss plaçait `disconnections_sender.send` dans `broker.rs` ;
- la section « Apps & services » n'a aucune citation et rien ne lui correspond
  dans le moteur : marquée à re-confirmer, pas à citer comme vérifiée.

Aussi : `fileOwnCaps` n'existe plus (`holdOwnCap` / `readStoreCaps` /
`fileOwnStructure`) — pointeur mort corrigé dans `caps.ts`.
2026-08-03 12:17:40 +02:00
Sylvain Duchesne 9d3e2d2bfe Fix documentation defects found by an adversarial review
Fifteen findings, all verified before acting. The ones that mattered:

- Corrections added without updating what they corrected. §5's table still
  said a cap-less NURI is one "without :k:", two hundred lines after §4
  established the discriminant is `r:`. Same shape of defect in the P1a
  report, which kept the sentence "it is the owner's keyring, upstream the
  keyring is the wallet" — the exact sentence §4quater declares wrong, and the
  one that produced a global in-memory keyring.

- A wrong source citation: RootCapRefresh/BranchCapRefresh live in
  verifier/src/commits/mod.rs, not repo/src/commit.rs, and are no-op stubs.

- Documentation describing deleted code: isolation.ts, discovery.readIndex,
  the global index, and an acceptance test that was dropped with discovery.

- The P1a implementation report had aged into being wrong in four places
  (caps not persisted, inbox processing not started, plain string types, the
  :k: segment). It is dated, so it now carries a header saying what later lots
  overtook, rather than being rewritten.

- vision.md stated "a document's data is stored encrypted" in the present
  tense. That is the target; here the cap value is the constant OK and nothing
  is encrypted. Said plainly now.

- Prose left mangled by an earlier mechanical find-and-replace, in four places
  I had claimed were repaired.

Also: reach.ts and connect.ts had no home in the permanent docs — the boundary
and the connection sequence are now described in simulation.md, not only in a
brief.
2026-08-03 11:34:24 +02:00
Sylvain Duchesne ae9c32e271 Align the cap emulation on NextGraph's model, and confine it to a virtual user
Two batches, verified against nextgraph-rs throughout.

P1a — the capability surface. Reading was an ACL (Map<doc, Set<principal>>),
the exact inversion of key possession. It is now possession: `capFor(nuri)` is
the only question, there is no principal parameter anywhere, and nothing turns
a bare reference into a cap. Sharing is `shareCap(cap, toInbox)`, a Link
deposit; receiving needs no operation. `Nuri` and `ReadCap` are template
literal types, so passing a bare reference where a cap belongs is a compile
error, with runtime guards behind it for JavaScript callers.

The virtual user boundary. Every access function is now confined to the
connected user, through two rules on one criterion (possession), implemented in
two places so a lapse in either is caught by the other: authorization at the
passage points, and "do not even attempt" at the callers. The polyfill's own
machinery moved to physical.ts — unguarded, never exported — which replaced an
exemption list: the machinery no longer gets waved through the guard, it calls
something the guard never saw.

Removed, as emulating capabilities the target does not have:
- discovery.ts and its global index. There is no discovery in NextGraph; you
  follow links. It also pooled user data across wallets.
- the cross-account fan-out (listEntityDocs, resolveReadGraphs, allAccounts,
  loadShim), which was cross-user enumeration by construction.
- resolveInboxAnchor, a single inbox common to every user.

Caps are now stored where NextGraph stores them, and read back rather than
recomputed: AddRepo on the store's Store branch for documents a user creates,
AddLink on its User branch for caps received. Inboxes belong to someone — the
user's own, plus one per document — and connecting a user drains them all;
that is the library's job, not the app's.

Corrections worth recording: a ReadCap is `r:`, not `:k:` (reported by
NextGraph's developer, verified in BlockRef::readcap_nuri); received caps DO
have a register (AddLink), contrary to what this repo's notes claimed; and
"wallet" upstream means keyring — what owns three stores is a user, so the
vocabulary follows.

The cap value is the constant OK: the only question the emulation answers is
whether a cap is held. P1b replaces that one constant with a real key.

After this the shape is right and the isolation is still fake. Nothing here may
be described as anonymous or private.
2026-08-03 11:22:01 +02:00
Sylvain Duchesne cf9500f0cf feat(inbox): logs du cycle de l'inbox virtuelle (dépôt/lecture/watch)
Réutilise le format identité-first [<id>][polyfill] d'access-log : deposit/read/materialize/readSynced + watch (materializing vs unchanged-skip). Gated par debugAccessLog, aucun changement de comportement. tsc 0, bun test 126.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
2026-07-14 18:58:38 +02:00
Sylvain Duchesne 70de7afa3c feat(logs): logs données restructurés — identité en préfixe, trace résolution/barrière, inspection outbox
Chemin données bas-niveau du polyfill rendu lisible pour diagnostiquer en session live :

- Format identité-en-premier : `[<identity>][polyfill] OP shortNuri (label)` ;
  console.error épars (store-registry, inbox) unifiés au même préfixe.
- Trace (derrière le flag debug) : issue de la barrière ensureRepoOpen
  (synced|timed-out + durée) et résultat sémantique de chaque étage de résolution
  (resolvePointer/canonicalDoc/resolveAccount/resolveShimDoc/readScopeIndex).
- outbox-log.ts (nouveau) : inspection read-only de l'outbox hors-ligne au
  démarrage de session ; console.warn si non vide (anomalie, toujours visible),
  sous flag si vide. Le comptage seul est fiable (payloads BARE opaques côté JS).

Pas de changement fonctionnel. bun test 126 pass ; tsc 0 erreur.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
2026-07-14 10:37:46 +02:00
Sylvain Duchesne 5cdc6ce77f feat(client): inbox.readSynced — lecture inbox gated sur la barrière first-State
ensureRepoOpen(target) (attend le 1er State, barrière de sync déterministe) puis
read(target) — même pattern que discovery.readIndex. Permet au propriétaire d'un
événement de voir un dépôt DÉJÀ synchronisé au lieu de lire trop tôt un inbox vide.
Consommé par Festipod (materializeAttendance). Pas de polling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:40:37 +02:00
Sylvain Duchesne 9103996dbe fix(client): dé-dupliquer les ensureAccount concurrents au cold-start (fork résolu)
Dernière couche du bug de reconnexion : au cold-start, `watchShape` public +
protected + l'effet owned-events appellent `ensureAccount(A)` quasi-simultanément
AVANT la sync du shim → chacun lit 0 → chacun provisionne un nouveau jeu de docs
(fork par-appelant) → la résolution déterministe canonique fait alors diverger
lecteur et écrivain sur le docProtected → `readScopeIndex` vide.

Fix : `ensureInFlight` (map de promesses) dé-duplique les provisions concurrentes
en UNE seule ; `discovery.readIndex` ouvre son repo au cold-start (`ensureRepoOpen`).
Avec la résolution canonique déjà committée, écrivain et lecteur convergent.
Mesuré (levier isSuccess) : la participation protected converge `isSuccess=true,
data=1` sur la page fraîche (plus « vide à 30s »).

gate : tsc 0 ; bun test 123 ; test:e2e 42/42.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 11:29:59 +02:00
Sylvain Duchesne d8c36bac3b feat(client): OFF-by-default document access log, prefixed by active identity
Observability probe for the shared-wallet isolation footgun: on one physical
wallet several virtual identities coexist, and a read must never surface a doc
scoped to another identity. When it does (B reading A's doc), the leak is
invisible in the data — it looks like a normal read. This makes it VISIBLE.

Every real read/write is logged, prefixed by the ACTIVE virtual identity
(getCurrentUser → the account the op is scoped under, NOT the constant shared
physical wallet id). Reads append the row count — a strong leak signal:

  [urn:festipod:user:bob] READ did:ng:o:docA (readDoc) → 3 rows

Instrumented at the LOW common point in docs.ts: every read routes through
sparqlQuery, every write through sparqlUpdate, container creation through
docCreate. Callers pass a semantic label (readDoc|readUnion|listMyEntityDocs|
writeEntity|deposit|…) that is a lib-internal probe param, NOT forwarded to the
real `ng` (preserves docs.test.ts exact-forwarding assertions).

OFF by default → one boolean read on the hot path, zero output. On via
configure({ debugAccessLog: true }) or env NG_EVENTUALLY_ACCESS_LOG=1 (no code
change). Polyfill-era; removed at the real multi-store migration.

tsc --noEmit: 0 errors. bun test: 91 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 21:20:47 +02:00
Sylvain Duchesne 4a2569e243 docs+test(client): correct the phantom-graph claim, verify graph behavior in e2e
The lib e2e harness now characterizes all three SPARQL graph shapes against the real
broker (24/24): (a) no-GRAPH anchored write round-trips; (b) an explicit anchored
GRAPH <plainNuri> ALSO resolves to the same repo (no phantom graph) — the earlier
'targets a phantom graph, does NOT round-trip' claim is FALSE on @ng-org/web
0.1.2-alpha.13; (c) an anchorless GRAPH ?g scan spans every named graph (O(wallet)
union, 32 graphs) — TRUE, re-verified.

Reconcile the comments accordingly: inbox.ts / store-registry.ts drop the false
phantom-graph justification (no-GRAPH stays as the canonical, always-safe shape — a
simplicity choice, not a round-trip necessity; re-verify via e2e if the broker
version changes). read-model.md keeps the anchorless-O(wallet) rationale (the real
reason reads are per-doc anchored) and appends a note distinguishing the variable
GRAPH ?g SCAN from a constant GRAPH <D> WRITE. No new absolute introduced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 09:38:59 +02:00
Sylvain Duchesne c0498a6ebc feat(client): per-document reactive subscription (doc_subscribe), drop polling
Expose subscribeDoc(nuri, onChange) / subscribeDocs(nuris, onChange) wrapping the
real ng.doc_subscribe — per-document, event-driven (initial State + a Patch per
commit, local or broker-synced from a remote peer), returning a sync unsubscribe.
subscribeDocs isolates per doc (a failing/unsynced doc never aborts the others),
so it sidesteps the ORM fan-out hang (never orm_start_graph(graphs:[…])).

Replace the setInterval polling in inbox.watch() and discovery.watchIndex() with
doc_subscribe — same public contract, now push not poll. NextGraph is subscription-
first; no polling remains.

Verified against the real broker (@data harness spike): the doc_subscribe callback
marshals across the iframe RPC (@ng-org/web strips the callback and drives it via a
MessagePort — no DataCloneError) and fires on the initial push and on a real write.
Lib bun test 91 pass, tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 23:04:32 +02:00
Sylvain Duchesne 63ecfeeff8 docs+refactor(client): fidelity pass — id identity, drop connections, no faux-login, accurate NextGraph framing
Align the polyfill's surface and docs with the verified NextGraph reality and
remove application-level concepts:

- Identity is an ID, not a username: AccountRecord.id, shim predicate shim:id,
  normalizeId; accounts core becomes IdentityStore (set/clear/get) — the faux
  login/logout framing is gone (identity is set at wallet-import time).
- Relationship/connection is an application concept, not a platform primitive
  (NextGraph has no bilateral-connection primitive: grantee is unpersisted
  scaffolding, cap-send is unimplemented). Remove connections.ts; caps exposes
  only a directed grantRead(doc, granteeId) + a read-only protectedDocsOf(owner).
  Delete the now-dead isolation.ts social-visibility axis.
- Inbox docs: NextGraph has no separate curator — the recipient's own verifier
  unseals and applies each queued sealed message inline (process_inbox);
  inbox_post_link is a proposed/future API. Stop attributing the emulated
  curator to the platform.
- Read isolation reframed around the outcome: no cap -> empty union read;
  targeted read of an unheld repo -> RepoNotFound; cap introspection
  (canRead/governsRead) is emulation-only with no NextGraph API behind it.
- read-model.md corrected: the listing path is per-doc ANCHORED default-graph
  queries, never the anchorless GRAPH ?g union (that is O(wallet)); the probe
  section no longer claims the opposite.
- README recap table restructured (target | current NextGraph status | current
  emulation); INDEX_ACCOUNT documented as reservedAccount("index") in the
  sentinel namespace; de-domained generic-layer comments; softened tone.

Consumer application (Festipod) rewired separately to own the relationship
concept and feed the lib an id. Lib gates: bun test 83 pass / 0 fail, tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 14:02:16 +02:00
Sylvain Duchesne d39b12885a fix(client): anchored default-graph writes for inbox + scope index
Inbox deposits and the per-(account,scope) index append were written into an
explicit GRAPH <plainNuri> named graph, which the real broker stores separately
from the repo's default graph (repo_graph_name with overlay suffix) — so an
anchored default-graph read (read-model.readDoc) never saw them and deposits
did not round-trip. Drop the GRAPH wrapper: the anchor scopes the write to the
repo's default graph, matching the read. Mocks updated accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 12:45:11 +02:00
Sylvain Duchesne bf753770b8 feat(client): real per-document isolation + bilateral connections + deposit guards
The ReadCap filter now enforces on per-entity documents (consumers create one doc
per entity, so each has a declared policy — private→owner, protected→owner+
connections, public→all). Isolation is genuinely active, not dormant.

- connections.ts (new): a BILATERAL connection registry — a link grants protected
  read only when BOTH sides have asserted it (each assertion bound to its author).
  A unilateral/self-declared connection grants nothing (closes the confused-deputy
  hole). declareConnections is authenticated to the current identity.
- inbox.post: `from` is bound to the current identity — a spoofed `from` throws.
- discovery.submitToIndex: PUBLIC-ONLY — a governed non-public doc is refused
  (no protected/private leak into the world-readable index).
- docs/simulation.md: documents this as application-level emulated isolation on a
  shared wallet (not crypto); at NextGraph maturity → real caps, consumer unchanged.

89 tests pass (+10 covering: active protected isolation via bilateral connect,
unilateral grants nothing, from-spoof rejected, non-public submit refused). tsc rc=0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 10:40:44 +02:00
Sylvain Duchesne d804a436d7 feat(client): inbox mechanism, write-guard, SPARQL injection hardening
Polyfill capabilities landed for Festipod's T02 features (all generic,
zero-domain — the consumer injects the domain).

- inbox: implement the previously-stubbed namespace. post(target,{from?,
  payload,ts?}) deposits {from,payload,ts} as RDF via docs.sparqlUpdate (the
  real injected ng, never makeNg); read/materialize + watch emulate the curator
  in-lib (deposits read via docs.sparqlQuery). `from` optional = anonymity.
- write-guard: caps.hasWritePolicy() + ng-proxy.sparql_update rejects when the
  target doc is under a write policy and the current user lacks its write cap;
  passthrough otherwise (no regression). Read-cap registry unchanged.
- sparql.ts (new): escapeLiteral / escapeIri / assertNuri, exported from index.
  store-registry now escapes every literal and validates/encodes every IRI-
  position value — closes a SPARQL-injection hole where an untrusted username
  could inject triples into the shim (the account→doc-NURI trust root).

Tests: inbox, sparql (incl. injection), ng-proxy write-guard, isolation-active.
68 tests pass; tsc --noEmit rc=0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 15:51:00 +02:00
Sylvain Duchesne bb2d9c3e59 Initial scaffold: @ng-eventually/client — SDK-shaped polyfill over NextGraph
Generic polyfill layer that makes a single NextGraph broker behave like the
not-yet-shipped multi-user NextGraph (emulated capabilities + inbox). Zero app domain.

@ng-eventually/client exposes an SDK-identical surface (ng, useShape, inbox); the
polyfill bootstrap (configure + capability helpers) is isolated under /polyfill, so
the main entry stays a drop-in for @ng-org/web|orm. The real SDK is injected at
configure() (no hard import → build-alias safe + testable).

Scaffold: NextGraph wiring stubbed with TODO; capability helpers implemented and
unit-tested (4 tests, typecheck clean). The global-index curator is deferred — in
NextGraph apps/services are mono-user with no global data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 16:35:40 +02:00