feat(data): PdR registration via inbox, notifications, public discovery, protected store

Polyfill-enabled features (T02). All NextGraph I/O goes through
@ng-eventually/client (docs/inbox/storeRegistry); no direct @ng-org.

- Shapes: FpMeetingPoint + FpNotification are now real SHEX shapes with ORM
  bindings (previously app-TS-only, unpersisted).
- Registration (registration.ts, new): joinEvent persists a Participation +
  deposits to the host's inbox + creates a Notification (from = registrant if
  connected, anonymous otherwise). leaveEvent deletes the Participation
  authoritatively via SPARQL DELETE-WHERE (sweep by event+user AND by subject,
  then re-query to confirm) — the désinscription CRDT-resurrection bug is fixed:
  the reactive delete is applied only once the broker confirms 0 remaining.
- Public discovery: useNgData fans out over every account's public docs so a
  user sees others' public events without a connection (dedup union).
- Cap attribution: createEntityDoc declares the ReadCap (open + makePublic/
  grantRead per scope), activating the per-document read filter.
- Protected store (T02.h): the default path now reads/writes shareable domain
  entities in the native protected store (did🆖${protected_store_id}) instead
  of private — verified openable against the broker — matching the per-wallet
  target. Private still anchors the shim/inbox + settings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-07-03 15:51:23 +02:00
parent 555c670b22
commit aacc2ec3ee
9 changed files with 876 additions and 33 deletions
@@ -176,4 +176,129 @@ export const festipodShapesSchema: Schema = {
},
],
},
"http://festipod.org/MeetingPoint": {
iri: "http://festipod.org/MeetingPoint",
predicates: [
{
dataTypes: [
{
valType: "iri",
literals: ["http://festipod.org/MeetingPoint"],
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
readablePredicate: "@type",
},
{
dataTypes: [{ valType: "iri" }],
maxCardinality: 1,
minCardinality: 1,
iri: "http://festipod.org/event",
readablePredicate: "event",
},
{
dataTypes: [{ valType: "iri" }],
maxCardinality: 1,
minCardinality: 1,
iri: "http://festipod.org/host",
readablePredicate: "host",
},
{
dataTypes: [{ valType: "string" }],
maxCardinality: 1,
minCardinality: 1,
iri: "http://festipod.org/title",
readablePredicate: "title",
},
{
dataTypes: [{ valType: "string" }],
maxCardinality: 1,
minCardinality: 0,
iri: "http://festipod.org/description",
readablePredicate: "description",
},
{
dataTypes: [{ valType: "string" }],
maxCardinality: 1,
minCardinality: 0,
iri: "http://festipod.org/place",
readablePredicate: "place",
},
{
dataTypes: [{ valType: "string" }],
maxCardinality: 1,
minCardinality: 0,
iri: "http://festipod.org/time",
readablePredicate: "time",
},
{
dataTypes: [{ valType: "string" }],
maxCardinality: 1,
minCardinality: 0,
iri: "http://festipod.org/inbox",
readablePredicate: "inbox",
},
],
},
"http://festipod.org/Notification": {
iri: "http://festipod.org/Notification",
predicates: [
{
dataTypes: [
{
valType: "iri",
literals: ["http://festipod.org/Notification"],
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
readablePredicate: "@type",
},
{
dataTypes: [{ valType: "iri" }],
maxCardinality: 1,
minCardinality: 1,
iri: "http://festipod.org/recipient",
readablePredicate: "recipient",
},
{
dataTypes: [{ valType: "string" }],
maxCardinality: 1,
minCardinality: 1,
iri: "http://festipod.org/type",
readablePredicate: "type",
},
{
dataTypes: [{ valType: "iri" }],
maxCardinality: 1,
minCardinality: 0,
iri: "http://festipod.org/ref",
readablePredicate: "ref",
},
{
dataTypes: [{ valType: "string" }],
maxCardinality: 1,
minCardinality: 0,
iri: "http://festipod.org/payload",
readablePredicate: "payload",
},
{
dataTypes: [{ valType: "string" }],
maxCardinality: 1,
minCardinality: 1,
iri: "http://festipod.org/timestamp",
readablePredicate: "timestamp",
},
{
dataTypes: [{ valType: "boolean" }],
maxCardinality: 1,
minCardinality: 1,
iri: "http://festipod.org/isRead",
readablePredicate: "isRead",
},
],
},
};
@@ -1,6 +1,12 @@
import type { ShapeType } from "@ng-org/shex-orm";
import { festipodShapesSchema } from "./festipodShapes.schema";
import type { FpEvent, FpUserProfile, FpParticipation } from "./festipodShapes.typings";
import type {
FpEvent,
FpUserProfile,
FpParticipation,
FpMeetingPoint,
FpNotification,
} from "./festipodShapes.typings";
// ShapeTypes for festipodShapes
export const FpEventShapeType: ShapeType<FpEvent> = {
@@ -15,3 +21,11 @@ export const FpParticipationShapeType: ShapeType<FpParticipation> = {
schema: festipodShapesSchema,
shape: "http://festipod.org/Participation",
};
export const FpMeetingPointShapeType: ShapeType<FpMeetingPoint> = {
schema: festipodShapesSchema,
shape: "http://festipod.org/MeetingPoint",
};
export const FpNotificationShapeType: ShapeType<FpNotification> = {
schema: festipodShapesSchema,
shape: "http://festipod.org/Notification",
};
@@ -161,3 +161,117 @@ export interface FpParticipation {
*/
isConfirmed: boolean;
}
/**
* MeetingPoint Type
*/
export interface FpMeetingPoint {
/**
* The graph IRI.
*/
readonly "@graph": IRI;
/**
* The subject IRI.
*/
readonly "@id": IRI;
/**
* Original IRI: http://www.w3.org/1999/02/22-rdf-syntax-ns#type
*/
"@type": "http://festipod.org/MeetingPoint";
/**
* Reference to the public event this meeting point is anchored to
*
* Original IRI: http://festipod.org/event
*/
event: IRI;
/**
* Reference to the user hosting the meeting point
*
* Original IRI: http://festipod.org/host
*/
host: IRI;
/**
* The title of the meeting point
*
* Original IRI: http://festipod.org/title
*/
title: string;
/**
* A description of the meeting point
*
* Original IRI: http://festipod.org/description
*/
description?: string;
/**
* Where participants gather (e.g. 'Entrée principale, sous l'horloge')
*
* Original IRI: http://festipod.org/place
*/
place?: string;
/**
* When participants gather (display string)
*
* Original IRI: http://festipod.org/time
*/
time?: string;
/**
* NURI of the meeting point's inbox (wired in T02.b/c)
*
* Original IRI: http://festipod.org/inbox
*/
inbox?: string;
}
/**
* Notification Type
*/
export interface FpNotification {
/**
* The graph IRI.
*/
readonly "@graph": IRI;
/**
* The subject IRI.
*/
readonly "@id": IRI;
/**
* Original IRI: http://www.w3.org/1999/02/22-rdf-syntax-ns#type
*/
"@type": "http://festipod.org/Notification";
/**
* Reference to the user receiving the notification
*
* Original IRI: http://festipod.org/recipient
*/
recipient: IRI;
/**
* Notification kind (e.g. 'new-participant', 'meeting-point-join')
*
* Original IRI: http://festipod.org/type
*/
type: string;
/**
* Reference to the subject resource (meeting point, participation, user)
*
* Original IRI: http://festipod.org/ref
*/
ref?: IRI;
/**
* Opaque JSON payload for the notification
*
* Original IRI: http://festipod.org/payload
*/
payload?: string;
/**
* ISO8601 timestamp when the notification was created
*
* Original IRI: http://festipod.org/timestamp
*/
timestamp: string;
/**
* Whether the recipient has read the notification
*
* Original IRI: http://festipod.org/isRead
*/
isRead: boolean;
}
@@ -47,3 +47,37 @@ fp:Participation {
fp:isConfirmed xsd:boolean
// rdfs:comment "Whether the participation is confirmed" ;
}
fp:MeetingPoint {
a [fp:MeetingPoint] ;
fp:event IRI
// rdfs:comment "Reference to the public event this meeting point is anchored to" ;
fp:host IRI
// rdfs:comment "Reference to the user hosting the meeting point" ;
fp:title xsd:string
// rdfs:comment "The title of the meeting point" ;
fp:description xsd:string ?
// rdfs:comment "A description of the meeting point" ;
fp:place xsd:string ?
// rdfs:comment "Where participants gather (e.g. 'Entrée principale, sous l'horloge')" ;
fp:time xsd:string ?
// rdfs:comment "When participants gather (display string)" ;
fp:inbox xsd:string ?
// rdfs:comment "NURI of the meeting point's inbox (wired in T02.b/c)" ;
}
fp:Notification {
a [fp:Notification] ;
fp:recipient IRI
// rdfs:comment "Reference to the user receiving the notification" ;
fp:type xsd:string
// rdfs:comment "Notification kind (e.g. 'new-participant', 'meeting-point-join')" ;
fp:ref IRI ?
// rdfs:comment "Reference to the subject resource (meeting point, participation, user)" ;
fp:payload xsd:string ?
// rdfs:comment "Opaque JSON payload for the notification" ;
fp:timestamp xsd:string
// rdfs:comment "ISO8601 timestamp when the notification was created" ;
fp:isRead xsd:boolean
// rdfs:comment "Whether the recipient has read the notification" ;
}