From 619b94ac0e312fd331b6d407f7bb79996b45d52e Mon Sep 17 00:00:00 2001 From: Sylvain Duchesne Date: Fri, 3 Jul 2026 23:42:03 +0200 Subject: [PATCH] =?UTF-8?q?refactor(data):=20route=20entities=20by=20scope?= =?UTF-8?q?=20via=20the=20SDK=20=E2=80=94=20no=20store=20ids=20in=20the=20?= =?UTF-8?q?app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Festipod now treats @ng-eventually/client as a finished NextGraph SDK: the app decides only each entity's logical scope (events/PdR public, profiles/ participations protected, settings private) and calls the lib by scope. The old mono-store default and the FESTIPOD_MULTISTORE path collapse into ONE scope path. Removed every physical-store leak from the app data-plane (ngGraph, registration, FestipodDataContext, NextGraphContext, useShapeWithDefaults): no more did:ng:${store_id} construction. The session is handed to the lib only at the sanctioned injection point (ngSession/configureStoreRegistry). Product behavior unchanged. @data 20/20; build + tsc clean. (_debt.md included; the T03.e doctrine pass settles accumulated doc-debt.) Co-Authored-By: Claude Opus 4.8 (1M context) --- .project/concepts/app-architecture/_debt.md | 9 ++ .project/concepts/app-security/_debt.md | 9 ++ .project/concepts/data-layer/_debt.md | 9 ++ src/shared/context/FestipodDataContext.tsx | 117 +++++++------------- src/shared/context/NextGraphContext.tsx | 10 +- src/shared/data/registration.ts | 19 ++-- src/shared/hooks/useShapeWithDefaults.ts | 19 ++-- src/shared/utils/ngGraph.ts | 25 ++--- src/shared/utils/ngSession.ts | 2 +- src/shared/utils/storeRegistry.ts | 15 ++- 10 files changed, 117 insertions(+), 117 deletions(-) create mode 100644 .project/concepts/app-architecture/_debt.md create mode 100644 .project/concepts/app-security/_debt.md create mode 100644 .project/concepts/data-layer/_debt.md diff --git a/.project/concepts/app-architecture/_debt.md b/.project/concepts/app-architecture/_debt.md new file mode 100644 index 0000000..1ee887e --- /dev/null +++ b/.project/concepts/app-architecture/_debt.md @@ -0,0 +1,9 @@ +# Doc-debt — app-architecture + +> Presence of a block = doc to update. Processed → delete the block; no blocks left → delete this file. +> One block = one "big change": `why` + `files` + `verify` (leaves to review). + +## Block: T03.g — routage par scope (pas de changement d'archi) +- **why**: FestipodDataContext route les entités par scope via le SDK et NextGraphContext ne surface plus les store-ids. La structure (provider stack, invariant d'imports module→shared, app shell) est inchangée — touches incidentes, pas d'évolution architecturale. +- **files**: src/shared/context/NextGraphContext.tsx, src/shared/context/FestipodDataContext.tsx +- **verify (leaves à relire)**: aucune — knowledge_app-shell.md / knowledge_module-structure.md inchangés. Bloc à supprimer après relecture confirmatoire. diff --git a/.project/concepts/app-security/_debt.md b/.project/concepts/app-security/_debt.md new file mode 100644 index 0000000..be6e470 --- /dev/null +++ b/.project/concepts/app-security/_debt.md @@ -0,0 +1,9 @@ +# Doc-debt — app-security + +> Presence of a block = doc to update. Processed → delete the block; no blocks left → delete this file. +> One block = one "big change": `why` + `files` + `verify` (leaves to review). + +## Block: T03.g — NextGraphContext ne surface plus les store-ids +- **why**: `NextGraphContext` ne journalise/expose plus les trois store-ids dans le contexte app (routés uniquement au point d'injection SDK). Renforce la doctrine "isolation déléguée au SDK, l'app ne manipule pas de store physique" — ne l'invalide pas. +- **files**: src/shared/context/NextGraphContext.tsx +- **verify (leaves à relire)**: knowledge_trust-model.md (confirmer : confiance dans le SDK, aucune manipulation de store côté app). Aucun changement de contenu attendu — relecture confirmatoire. diff --git a/.project/concepts/data-layer/_debt.md b/.project/concepts/data-layer/_debt.md new file mode 100644 index 0000000..7331827 --- /dev/null +++ b/.project/concepts/data-layer/_debt.md @@ -0,0 +1,9 @@ +# Doc-debt — data-layer + +> Presence of a block = doc to update. Processed → delete the block; no blocks left → delete this file. +> One block = one "big change": `why` + `files` + `verify` (leaves to review). + +## Block: T03.g — l'app route par SCOPE via la lib, zéro store-id +- **why**: Les fuites de store physique retirées de l'app applicative. Toute lecture/écriture d'entité passe par le SDK par scope (public/protected/private) ; l'app ne construit plus de `did:ng:${store_id}`. La lib expose `resolveScopeGraph(scope)` / `resolveInboxAnchor()` (placement interne). Le flag `FESTIPOD_MULTISTORE` et le chemin mono-store/multi-doc sont fusionnés en UN chemin par scope. Le point d'injection unique (`ngSession`/`storeRegistry.configureStoreRegistry`) passe la session (dont les store-ids) à la lib — wiring sanctionné. +- **files**: src/shared/utils/ngGraph.ts, src/shared/hooks/useShapeWithDefaults.ts, src/shared/context/FestipodDataContext.tsx, src/shared/utils/ngSession.ts, src/shared/utils/storeRegistry.ts, src/shared/data/registration.ts +- **verify (leaves à relire)**: _overview.md (le modèle "entité = document par scope" est déjà énoncé — vérifier qu'il ne reste aucune trace de mono-store/store physique dans l'app), knowledge_nextgraph-stack.md (frontière SDK : confirmer "l'app parle uniquement en scopes, jamais de store-id"), knowledge_context-internals.md (les scopes sont désormais résolus async via le SDK dans un effet — `scopeGraphs` state, gate `ready`). NB : le contenu doctrinal actuel décrit déjà la cible ; ces changements font *converger le code vers la doctrine*, ils ne l'invalident pas. Relecture confirmatoire (T03.e possède la passe doctrine). diff --git a/src/shared/context/FestipodDataContext.tsx b/src/shared/context/FestipodDataContext.tsx index 7337bd8..7faa6ae 100644 --- a/src/shared/context/FestipodDataContext.tsx +++ b/src/shared/context/FestipodDataContext.tsx @@ -26,17 +26,8 @@ import { import { useNextGraph } from './NextGraphContext'; import { useAccount, normalizeUsername } from './AccountContext'; import { applyIsolation } from '../utils/isolation'; -import { ensureAccount, resolveReadGraphs, resolveWriteGraph, createEntityDoc, listEntityDocs } from '../utils/storeRegistry'; +import { resolveScopeGraph, listEntityDocs } from '../utils/storeRegistry'; import { useShapeWithDefaults, type ShapeScope } from '../hooks/useShapeWithDefaults'; - -// Multi-document mode (storeRegistry): one document per (account × scope), -// mirroring the target per-user stores. Default OFF — the validated mono-store -// path stays the default until the multi-document path is broker-validated. -// Flip via FESTIPOD_MULTISTORE=1 at build time. See brief_2026-06-15_shared-wallet-shim. -// Browser-safe env read: the bundler inlines process.env.NODE_ENV but NOT -// custom vars, so a bare `process.env.FESTIPOD_MULTISTORE` throws -// "process is not defined" in the browser harness. Guard it. -const MULTISTORE = typeof process !== 'undefined' && process?.env?.FESTIPOD_MULTISTORE === '1'; import { FpEventShapeType, FpUserProfileShapeType, @@ -257,45 +248,32 @@ function useLocalData(empty?: boolean): FestipodDataContextValue { function useNgData(): FestipodDataContextValue { const { session } = useNextGraph(); const { username } = useAccount(); - // Mono-store scopes (MULTISTORE off). Two native stores of the shared wallet: - // - privateNuri: kept as the anchor for the inbox shim (host inbox deposits) - // and for private settings. Opens the repo in the verifier. - // - protectedNuri: T02.h (axe A) — the SHAREABLE domain entities (events, - // profiles, participations) now READ from and WRITE to the real protected - // native store, representative of the target per-user wallet. Verified to - // open for ORM reads+writes exactly like private (round-trip, no - // RepoNotFound — see protected-store.feature). Subscribing useShape with - // this NURI opens its repo in the verifier (required for writes). - const privateNuri = session ? `did:ng:${session.private_store_id}` : undefined; - const protectedNuri = session ? `did:ng:${session.protected_store_id}` : undefined; - - // Multi-document state (storeRegistry): read fan-out (all accounts' docs per - // scope) and the current account's write docs. Populated by the effect below. - const [readGraphs, setReadGraphs] = useState<{ public: string[]; protected: string[] }>({ public: [], protected: [] }); - const [writeGraphs, setWriteGraphs] = useState<{ protected?: string }>({}); + // The app speaks ONLY in logical scopes — it holds no store id and builds no + // `did:ng:${…}` NURI. It asks the SDK (`resolveScopeGraph(scope)`) for the + // opaque graph NURI of each scope; the SDK owns the physical placement (today + // it resolves the shareable domain scopes to the shared wallet's native + // stores — its internal detail). `ready` gates the effects on the session. + const ready = !!session; + // Scope-resolved graphs (from the SDK). Domain entities: events → public, + // profiles + participations → protected. Populated by the effect below. + const [scopeGraphs, setScopeGraphs] = useState<{ public?: string; protected?: string }>({}); useEffect(() => { - if (!MULTISTORE || !privateNuri || !username) return; + if (!ready) return; let cancelled = false; (async () => { try { - await ensureAccount(username); // create this account's index docs on first sight - // Public = per-entity documents (events/PdR) listed by the public index. - // Protected = grouped in each account's protected index document. - const [pub, prot, wProt] = await Promise.all([ - listEntityDocs('public'), - resolveReadGraphs('protected'), - resolveWriteGraph(username, 'protected'), + const [pub, prot] = await Promise.all([ + resolveScopeGraph('public'), + resolveScopeGraph('protected'), ]); - if (cancelled) return; - setReadGraphs({ public: pub, protected: prot }); - setWriteGraphs({ protected: wProt }); + if (!cancelled) setScopeGraphs({ public: pub, protected: prot }); } catch (err) { - console.error('[FestipodData] storeRegistry init failed:', err); + console.error('[FestipodData] scope resolution failed:', err); } })(); return () => { cancelled = true; }; - }, [privateNuri, username]); + }, [ready]); // --- Public discovery (T02.e): cross-account fan-out, ALWAYS on ------------ // Materialize the cross-account source of PUBLIC entities so a user discovers @@ -313,7 +291,7 @@ function useNgData(): FestipodDataContextValue { // shim IS populated (multi-account staging), discovery unions those events in. const [discoveryGraphs, setDiscoveryGraphs] = useState([]); useEffect(() => { - if (!privateNuri) return; + if (!ready) return; let cancelled = false; (async () => { try { @@ -324,18 +302,14 @@ function useNgData(): FestipodDataContextValue { } })(); return () => { cancelled = true; }; - }, [privateNuri, username]); + }, [ready, username]); const discoveryScope: ShapeScope = discoveryGraphs.length ? { graphs: discoveryGraphs } : undefined; - // Scope per entity: events live in the PUBLIC docs, profiles + participations - // in the PROTECTED docs. Mono-store mode collapses all to the PROTECTED native - // store (T02.h, axe A) — the shareable domain entities read from there. - const publicScope: ShapeScope = MULTISTORE - ? (readGraphs.public.length ? { graphs: readGraphs.public } : undefined) - : protectedNuri; - const protectedScope: ShapeScope = MULTISTORE - ? (readGraphs.protected.length ? { graphs: readGraphs.protected } : undefined) - : protectedNuri; + // Scope per entity: events read/write the PUBLIC scope, profiles + + // participations the PROTECTED scope. Both are opaque SDK-resolved graph NURIs + // (the SDK owns placement) — the app never sees a store id. + const publicScope: ShapeScope = scopeGraphs.public; + const protectedScope: ShapeScope = scopeGraphs.protected; // useShapeWithDefaults: show EMPTY data until NG populates (no seed defaults) const emptyEvents: FpEventData[] = []; @@ -388,9 +362,8 @@ function useNgData(): FestipodDataContextValue { const hasTriedAutoSeed = useRef(false); useEffect(() => { if (process.env.NODE_ENV === 'production') return; - if (MULTISTORE) return; // seed targets the private store; multi-doc seeding is a separate concern if (hasTriedAutoSeed.current) return; - if (!privateNuri) return; + if (!ready) return; const t = setTimeout(() => { hasTriedAutoSeed.current = true; if (eventsShape.ngSet.size === 0 && usersShape.ngSet.size === 0) { @@ -405,7 +378,7 @@ function useNgData(): FestipodDataContextValue { } }, 3000); return () => clearTimeout(t); - }, [privateNuri, eventsShape.ngSet, usersShape.ngSet, participationsShape.ngSet]); + }, [ready, eventsShape.ngSet, usersShape.ngSet, participationsShape.ngSet]); // --- Derived --- // Resolve current user from the chosen account username (the perceived login); @@ -428,12 +401,12 @@ function useNgData(): FestipodDataContextValue { [events, currentUserId], ); useEffect(() => { - if (!privateNuri || hostedEventIds.length === 0) return; + if (!ready || hostedEventIds.length === 0) return; let cancelled = false; (async () => { try { - // In the polyfill every host inbox resolves to the shared private store, - // so read it ONCE and let the curator filter deposits per hosted event. + // The SDK resolves the inbox anchor for the current session; read it ONCE + // and let the curator filter deposits per hosted event. const targetInbox = await hostInboxNuri(''); const all: FpNotificationData[] = []; for (const evId of hostedEventIds) { @@ -454,7 +427,7 @@ function useNgData(): FestipodDataContextValue { })(); return () => { cancelled = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [privateNuri, hostedEventIds.join('|')]); + }, [ready, hostedEventIds.join('|')]); // Isolation (staging realism): the app honors the matrix in connected mode — // participations/connections narrowed to self + connections. See isolation.ts. @@ -472,24 +445,16 @@ function useNgData(): FestipodDataContextValue { '| selectedEvent:', selectedEvent?.title ?? '(none)'); // --- Mutations (NG) --- - // Participations stay GROUPED in the account's protected index document. - // Mono-store mode writes to the PROTECTED native store (T02.h, axe A) — the - // same store the domain read scopes subscribe, so writes round-trip. - const protectedGraph = (MULTISTORE ? writeGraphs.protected : undefined) || protectedNuri || ''; + // Writes target the SCOPE-resolved graphs (opaque SDK NURIs — no store id). + // Participations + profiles → protected scope; events → public scope. The + // read scopes subscribe the same graphs, so writes round-trip. + const protectedGraph = protectedScope || ''; + const publicGraph = publicScope || ''; const createEvent = useCallback(async (event: Omit): Promise => { console.log('[FestipodData] createEvent (NG):', event.title); - // Per-entity: in multistore each event is its OWN document. Mono-store: the - // private store. (Multistore create reactivity is best-effort — the new doc - // is appended to the read fan-out so it shows after re-subscribe.) - const eventGraph = MULTISTORE - ? await createEntityDoc(username || '', 'public') - : (protectedNuri || ''); - if (MULTISTORE && eventGraph) { - setReadGraphs(prev => - prev.public.includes(eventGraph) ? prev : { ...prev, public: [...prev.public, eventGraph] }, - ); - } + // Events live in the PUBLIC scope (SDK-resolved graph — no store id). + const eventGraph = publicGraph; eventsShape.ngSet.add({ "@graph": eventGraph, "@type": "http://festipod.org/Event", "@id": "", title: event.title, description: event.description, date: event.date, @@ -506,7 +471,7 @@ function useNgData(): FestipodDataContextValue { setSelectedEventId(addedEvent["@id"]); } return { ...event, id: addedEvent?.["@id"] || `ng-pending-${Date.now()}` }; - }, [protectedGraph, eventsShape.ngSet, participationsShape.ngSet, currentUserId, protectedNuri, username]); + }, [protectedGraph, publicGraph, eventsShape.ngSet, participationsShape.ngSet, currentUserId, username]); const updateEvent = useCallback((id: string, updates: Partial) => { console.log('[FestipodData] updateEvent (NG):', id, updates); @@ -529,7 +494,7 @@ function useNgData(): FestipodDataContextValue { console.log('[FestipodData] Already participating, skipping'); return; } - // 1) Persist the Participation (reactive ORM set — mono-store default path). + // 1) Persist the Participation (reactive ORM set — protected scope graph). participationsShape.ngSet.add({ "@graph": protectedGraph, "@type": "http://festipod.org/Participation", "@id": "", event: eventId, user: uid, isConfirmed: true, @@ -571,8 +536,8 @@ function useNgData(): FestipodDataContextValue { // deletion is a SPARQL DELETE via the real injected `ng` (docs.sparqlUpdate), // which removes the Participation server-side so it does NOT come back after // re-sync. The delete targets the participation's own @graph (the doc it lives - // in) — mono-store: the private store; multistore: the protected write doc — - // and is identified by the participation's OWN subject IRI (ngPart["@id"]), + // in) — falling back to the protected SCOPE graph (SDK-resolved, no store id) + // — and is identified by the participation's OWN subject IRI (ngPart["@id"]), // not a string-match on the object IRIs (the F2 bug: object string-match could // hit 0 rows on IRI-form drift → silent no-op → resurrection). const graphNuri = ngPart["@graph"] || protectedGraph; diff --git a/src/shared/context/NextGraphContext.tsx b/src/shared/context/NextGraphContext.tsx index 28e1f54..c2280f1 100644 --- a/src/shared/context/NextGraphContext.tsx +++ b/src/shared/context/NextGraphContext.tsx @@ -40,11 +40,11 @@ export function NextGraphProvider({ children }: { children: ReactNode }) { sessionPromise .then((s) => { - console.log('[NG] Session obtained, stores:', { - private: s.private_store_id, - protected: s.protected_store_id, - public: s.public_store_id, - }); + // The session (incl. native store ids) is handed to the SDK at the + // sanctioned injection point (ngSession/storeRegistry). The app context + // itself does not surface or manipulate store ids — it only tracks the + // connection status and the opaque session handle. + console.log('[NG] Session obtained — connected'); setNgSession(s); setStatus('connected'); }) diff --git a/src/shared/data/registration.ts b/src/shared/data/registration.ts index 9d17f74..e1da30d 100644 --- a/src/shared/data/registration.ts +++ b/src/shared/data/registration.ts @@ -19,6 +19,7 @@ import { inbox, docs, escapeLiteral, escapeIri, assertNuri } from '@ng-eventually/client'; import { sessionPromise } from '../utils/ngSession'; +import { resolveInboxAnchor } from '../utils/storeRegistry'; import type { FpNotificationData } from './types'; /** Notification IRI/type constants (mirror the SHEX Notification shape). */ @@ -64,20 +65,18 @@ function mintDepositUid(): string { * Resolve the inbox document NURI for a meeting point / host. * * Preference order: the explicit MeetingPoint `inbox` NURI (SHEX field, T02.a) - * when known → else the shared-wallet PRIVATE STORE NURI. The lib's `inbox` - * uses `targetInbox` as BOTH the RDF graph AND the SPARQL anchor, and the anchor - * must be a REAL repo NURI (a `urn:` is rejected as `InvalidNuri` by the broker) - * — so in the polyfill every host inbox physically resolves to the shared wallet - * private store; deposits are DISCRIMINATED by their `eventId` payload (the - * curator filters per event). At migration this becomes the host's native inbox - * NURI and the deposits move to per-host docs. Async because the session (hence - * the private_store_id) is resolved lazily. + * when known → else the SDK-resolved inbox anchor for the current session + * (`resolveInboxAnchor()`). The app asks the SDK for the anchor by intent and + * holds NO store id: the SDK owns where deposits physically land (today: the + * shared wallet's private store — a real repo NURI, required because the broker + * rejects a `urn:` anchor; deposits are discriminated by their `eventId` + * payload, the curator filters per event). At migration the SDK returns the + * host's native inbox NURI and this call is unchanged. */ export async function hostInboxNuri(eventId: string, explicitInbox?: string): Promise { void eventId; // reserved: per-event inbox docs at migration if (explicitInbox) return explicitInbox; - const { private_store_id } = await sessionPromise; - return `did:ng:${private_store_id}`; + return resolveInboxAnchor(); } /** diff --git a/src/shared/hooks/useShapeWithDefaults.ts b/src/shared/hooks/useShapeWithDefaults.ts index 67f49cb..71ccd7a 100644 --- a/src/shared/hooks/useShapeWithDefaults.ts +++ b/src/shared/hooks/useShapeWithDefaults.ts @@ -1,9 +1,10 @@ /** - * useShapeWithDefaults — wrapper around NextGraph ORM's useShape. + * useShapeWithDefaults — wrapper around the SDK ORM's useShape. * - * Subscribes to the private store via did:ng: scope, - * which opens the store repo in the verifier (required for writes). - * Maps results to app types. If the NG set is empty, returns defaults. + * Subscribes to a SCOPE-resolved graph NURI (obtained from the SDK by logical + * scope — the app holds no store id), which opens the repo in the verifier + * (required for writes). Maps results to app types. If the NG set is empty, + * returns defaults. * * Must only be called when NG is connected (inside NgDataProvider). */ @@ -18,9 +19,9 @@ export interface ShapeWithDefaults { } /** - * `scope` is either a single store/document NURI (mono-store mode) or a - * `{ graphs }` set of document NURIs (multi-document mode — storeRegistry). - * `useShape` accepts both natively. + * `scope` is either a single scope-resolved graph NURI (from the SDK) or a + * `{ graphs }` set of document NURIs (a read fan-out). `useShape` accepts both + * natively. Either way the value is opaque to the app — it never builds it. */ export type ShapeScope = string | { graphs: string[] } | undefined; @@ -31,8 +32,8 @@ export function useShapeWithDefaults( mapFromNg: (item: NgT) => AppT, shapesReady: boolean, ): ShapeWithDefaults { - // Mono-store: a single store NURI opens the repo in the verifier (enables - // writes). Multi-document: a { graphs } scope subscribes to several docs. + // A single scope-resolved graph NURI opens the repo in the verifier (enables + // writes); a { graphs } scope subscribes to several docs (read fan-out). const ngSet = useShape(shapeType, storeNuri as any) as DeepSignalSet; const usingDefaults = !shapesReady; const items = usingDefaults ? defaults : [...ngSet].map(item => mapFromNg(item as unknown as NgT)); diff --git a/src/shared/utils/ngGraph.ts b/src/shared/utils/ngGraph.ts index b87b5e5..5558c2e 100644 --- a/src/shared/utils/ngGraph.ts +++ b/src/shared/utils/ngGraph.ts @@ -1,24 +1,20 @@ /** * NextGraph graph NURI management. * - * Returns the PROTECTED store NURI as @graph for ORM entity creation of the - * SHAREABLE domain entities (events, profiles, participations). T02.h (axe A) - * switched the default domain scope from the private store to the real - * protected native store (`did:ng:${protected_store_id}`) — the store - * representative of the target per-user wallet. Verified empirically: the - * protected store opens for ORM reads AND writes the same way private does - * (round-trip probe, no RepoNotFound). Like private, subscribing useShape with - * the protected store NURI as scope opens its repo in the verifier, and writes - * target the same NURI. (Private stays the anchor for the inbox shim + settings.) + * Returns the graph NURI where the SHAREABLE domain entities (events, profiles, + * participations) are created via the ORM. These entities live in the PROTECTED + * scope, so the NURI is resolved by SCOPE through the SDK + * (`resolveScopeGraph('protected')`) — the app holds NO physical store id and + * builds NO `did:ng:${store_id}` NURI. The SDK owns the placement/resolution. */ -import { sessionPromise } from './ngSession'; +import { resolveScopeGraph } from './storeRegistry'; let cachedGraphNuri: string | undefined; /** * Get the graph NURI for adding shareable ORM entities. - * Uses the protected store NURI (T02.h; was the private store before). + * Resolves the PROTECTED scope via the SDK (was a raw store NURI before T03.g). */ export async function ensureGraphNuri( ...sets: Iterable<{ readonly "@graph": string }>[] @@ -36,9 +32,8 @@ export async function ensureGraphNuri( } } - // Use PROTECTED store NURI (the repo is opened by useShape with this scope). - const session = await sessionPromise; - cachedGraphNuri = `did:ng:${session.protected_store_id}`; - console.log('[ngGraph] Using protected store as graph:', cachedGraphNuri); + // Resolve the PROTECTED scope's graph via the SDK (opaque NURI — no store id). + cachedGraphNuri = await resolveScopeGraph('protected'); + console.log('[ngGraph] Using protected scope as graph:', cachedGraphNuri); return cachedGraphNuri; } diff --git a/src/shared/utils/ngSession.ts b/src/shared/utils/ngSession.ts index 84ba291..82f606f 100644 --- a/src/shared/utils/ngSession.ts +++ b/src/shared/utils/ngSession.ts @@ -39,7 +39,7 @@ export function init(): Promise { async (event: any) => { session = event.session; session!.ng ??= realNg; - console.log('[NG session] Connected — private_store:', session!.private_store_id); + console.log('[NG session] Connected'); resolveSessionPromise(session!); initNgSignals(realNg, session!); }, diff --git a/src/shared/utils/storeRegistry.ts b/src/shared/utils/storeRegistry.ts index f465292..232357a 100644 --- a/src/shared/utils/storeRegistry.ts +++ b/src/shared/utils/storeRegistry.ts @@ -48,7 +48,16 @@ export function entityScope(kind: EntityKind): Scope { configureStoreRegistry({ getSession: async () => { const session = await sessionPromise; - return { sessionId: session.session_id, privateStoreId: session.private_store_id }; + // Sanctioned injection point: the session (incl. the three native store ids) + // is handed to the lib HERE and nowhere else. The lib owns physical placement + // and resolves scope → store internally; the rest of the app speaks only in + // logical scopes and never touches a store id / builds a `did:ng:${…}` NURI. + return { + sessionId: session.session_id, + privateStoreId: session.private_store_id, + protectedStoreId: session.protected_store_id, + publicStoreId: session.public_store_id, + }; }, normalizeUser: normalizeUsername, }); @@ -64,6 +73,10 @@ export const { allAccounts, resolveReadGraphs, resetRegistryCache, + // SDK-shaped scope resolvers — the app asks by scope, the lib resolves + // placement (no store-id ever crosses the boundary). + resolveScopeGraph, + resolveInboxAnchor, } = libStoreRegistry; /**