@real-life/trust-protocol 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +99 -0
  2. package/dist/core.d.ts +32 -0
  3. package/dist/core.d.ts.map +1 -0
  4. package/dist/core.js +312 -0
  5. package/dist/core.js.map +1 -0
  6. package/dist/crypto.d.ts +32 -0
  7. package/dist/crypto.d.ts.map +1 -0
  8. package/dist/crypto.js +114 -0
  9. package/dist/crypto.js.map +1 -0
  10. package/dist/delivery.d.ts +51 -0
  11. package/dist/delivery.d.ts.map +1 -0
  12. package/dist/delivery.js +170 -0
  13. package/dist/delivery.js.map +1 -0
  14. package/dist/encounter.d.ts +63 -0
  15. package/dist/encounter.d.ts.map +1 -0
  16. package/dist/encounter.js +56 -0
  17. package/dist/encounter.js.map +1 -0
  18. package/dist/identity.d.ts +25 -0
  19. package/dist/identity.d.ts.map +1 -0
  20. package/dist/identity.js +87 -0
  21. package/dist/identity.js.map +1 -0
  22. package/dist/index.d.ts +6 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +29 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/probe/continuity.d.ts +99 -0
  27. package/dist/probe/continuity.d.ts.map +1 -0
  28. package/dist/probe/continuity.js +303 -0
  29. package/dist/probe/continuity.js.map +1 -0
  30. package/dist/probe/deps.d.ts +61 -0
  31. package/dist/probe/deps.d.ts.map +1 -0
  32. package/dist/probe/deps.js +54 -0
  33. package/dist/probe/deps.js.map +1 -0
  34. package/dist/probe/introduce.d.ts +70 -0
  35. package/dist/probe/introduce.d.ts.map +1 -0
  36. package/dist/probe/introduce.js +295 -0
  37. package/dist/probe/introduce.js.map +1 -0
  38. package/dist/probe/membership.d.ts +90 -0
  39. package/dist/probe/membership.d.ts.map +1 -0
  40. package/dist/probe/membership.js +450 -0
  41. package/dist/probe/membership.js.map +1 -0
  42. package/dist/probe/trust.d.ts +116 -0
  43. package/dist/probe/trust.d.ts.map +1 -0
  44. package/dist/probe/trust.js +345 -0
  45. package/dist/probe/trust.js.map +1 -0
  46. package/dist/probe.d.ts +8 -0
  47. package/dist/probe.d.ts.map +1 -0
  48. package/dist/probe.js +41 -0
  49. package/dist/probe.js.map +1 -0
  50. package/dist/schemas.d.ts +2 -0
  51. package/dist/schemas.d.ts.map +1 -0
  52. package/dist/schemas.js +51 -0
  53. package/dist/schemas.js.map +1 -0
  54. package/dist/unicode15.d.ts +8 -0
  55. package/dist/unicode15.d.ts.map +1 -0
  56. package/dist/unicode15.js +55 -0
  57. package/dist/unicode15.js.map +1 -0
  58. package/package.json +55 -0
@@ -0,0 +1,90 @@
1
+ import * as C from './deps.js';
2
+ import type { Person } from './deps.js';
3
+ export declare function memberContext(p: Person, genesisDigest: string): Promise<C.Context>;
4
+ export declare function foundGroup(p: Person, label: string, when: number, ent?: any): Promise<any>;
5
+ export declare function preludeRequest(p: Person, contactAnchor: string, genesisDigest: string, when: number, ent?: any): Promise<{
6
+ to: any;
7
+ env: C.Envelope;
8
+ }>;
9
+ export declare function receiveDoc(p: Person, env: any, when: number, ent?: any): Promise<{
10
+ handled: boolean;
11
+ prompt: {
12
+ kind: string;
13
+ doc: any;
14
+ invite: any;
15
+ from: any;
16
+ fromAnchor: any;
17
+ fromContact: any;
18
+ myMemberCtx: C.Context;
19
+ decided: boolean;
20
+ };
21
+ error?: undefined;
22
+ } | {
23
+ handled: boolean;
24
+ idempotent: boolean;
25
+ error?: undefined;
26
+ admitted?: undefined;
27
+ outbound?: undefined;
28
+ } | {
29
+ handled: boolean;
30
+ admitted: any;
31
+ outbound: {
32
+ to: any;
33
+ env: C.Envelope;
34
+ }[];
35
+ error?: undefined;
36
+ idempotent?: undefined;
37
+ } | {
38
+ handled: boolean;
39
+ joined: any;
40
+ error?: undefined;
41
+ } | {
42
+ handled: boolean;
43
+ idempotent: boolean;
44
+ vouched?: undefined;
45
+ fulfilled?: undefined;
46
+ } | {
47
+ handled: boolean;
48
+ vouched: any;
49
+ fulfilled: boolean;
50
+ idempotent?: undefined;
51
+ } | {
52
+ handled: boolean;
53
+ doc: any;
54
+ error?: undefined;
55
+ outbound?: undefined;
56
+ } | {
57
+ handled: boolean;
58
+ error: string;
59
+ doc?: undefined;
60
+ outbound?: undefined;
61
+ } | {
62
+ handled: boolean;
63
+ outbound: {
64
+ to: any;
65
+ env: C.Envelope;
66
+ }[];
67
+ doc?: undefined;
68
+ error?: undefined;
69
+ } | {
70
+ handled: boolean;
71
+ duplicate: boolean;
72
+ } | {
73
+ handled: boolean;
74
+ duplicate?: undefined;
75
+ }>;
76
+ export declare function buildInvite(p: Person, g: any, contact: any, inviteeMemberAnchor: string, when: number, threadId: string, ent?: any): Promise<{
77
+ to: any;
78
+ env: C.Envelope;
79
+ }>;
80
+ export declare function acceptInvite(p: Person, entry: any, when: number, ent?: any): Promise<{
81
+ to: any;
82
+ env: C.Envelope;
83
+ }>;
84
+ export declare function vouchFor(p: Person, genesisDigest: string, candidateAnchor: string, provenance: any, when: number, ent?: any): Promise<{
85
+ to: any;
86
+ env: C.Envelope;
87
+ } | {
88
+ error: string;
89
+ }>;
90
+ //# sourceMappingURL=membership.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"membership.d.ts","sourceRoot":"","sources":["../../src/probe/membership.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,CAAC,MAAM,WAAW,CAAA;AAC9B,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAA;AAYvC,wBAAsB,aAAa,CAAE,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,sBAKpE;AAKD,wBAAsB,UAAU,CAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,GAAE,GAAQ,gBAoBtF;AAWD,wBAAsB,cAAc,CAAE,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,GAAE,GAAQ;;;GAYzH;AAID,wBAAsB,UAAU,CAAE,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,GAAE,GAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAOjF;AAoDD,wBAAsB,WAAW,CAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,mBAAmB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,GAAE,GAAQ;;;GAqB7I;AAyBD,wBAAsB,YAAY,CAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,GAAE,GAAQ;;;GAoBrF;AAoFD,wBAAsB,QAAQ,CAAE,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,GAAE,GAAQ;;;;;GA2BtI"}
@@ -0,0 +1,450 @@
1
+ // groups.mjs — Stage C: groups over the introduced channel. PROBE.
2
+ // DOM-free, on rltp-core + rltp-crypto (+ the shipped schemas: the invite
3
+ // IS the DTG InvitationCredential of membership-invite/0.2, the accept IS
4
+ // membership-accept/0.2 — both schema-validated at issuance AND receipt).
5
+ //
6
+ // WHAT THIS PROBE EXERCISES (the converged Membership 0.16 / Access 0.30
7
+ // semantics, instantiated live):
8
+ // founding — the founder signs the genesis under a FOUNDING pair
9
+ // context (Access 3.4.1: group/<digest> cannot exist
10
+ // before the digest does); genesisDigest = multihash
11
+ // over the proof-free genesis; the founder then derives
12
+ // the member anchor group/<digest> and self-enrolls.
13
+ // prelude — the inviter cannot derive the invitee's member anchor;
14
+ // the invitee's app derives it from the genesisDigest
15
+ // (canonical-u before the label, Access 5.1) and answers
16
+ // over the EXISTING relationship channel (Membership 1.2).
17
+ // invite — a conformant DTG InvitationCredential in a delivery
18
+ // document WITHOUT a document proof (one carrier,
19
+ // Membership 2); invitee re-derives and compares
20
+ // (the prelude check, Membership 3.1).
21
+ // accept — consent, task-proved under the subject; ref = the
22
+ // CREDENTIAL digest (decoded-bytes equality).
23
+ // admission — the inviter runs the Access 5.3 pair-internal checks
24
+ // (path convention, time window incl. membership-skew,
25
+ // size budget, card enclosure profile) and updates the
26
+ // roster; a WELCOME (probe form) carries the roster to
27
+ // members over their channels.
28
+ // OUT OF PROBE SCOPE (named, not faked): the full access-operation
29
+ // envelope (oid/prev/epochs), welcome seal + keydist/epoch keys, vouch
30
+ // paths and candidacy surfacing, removal. That is the library's next
31
+ // growth ring — the checks here are the real ones, the transport forms
32
+ // marked @probe are not wire-normative.
33
+ import { jcs, makeValidator, toU, sameDigest, tsec, calOK } from '../core.js';
34
+ import * as C from './deps.js';
35
+ import { SCHEMAS } from '../schemas.js';
36
+ const V = makeValidator(SCHEMAS);
37
+ const validOr = (data, file) => { const s = SCHEMAS[file]; const errs = V.validate(data, s, s); if (errs.length)
38
+ throw new Error(file + ': ' + errs[0]); return data; };
39
+ const MEMBERSHIP_SKEW = 300; // PT5M (Membership §5)
40
+ const TT = 'https://real-life.org/trust-tasks/';
41
+ const CTX3 = ['https://www.w3.org/ns/credentials/v2', 'https://firstperson.network/credentials/dtg/v1', 'https://real-life.org/rltp/v1'];
42
+ const say = (p, m) => p.log.push(m);
43
+ const uuid = (ent) => ent ?? globalThis.crypto.randomUUID();
44
+ // member anchor: canonical-u re-encoding BEFORE the label (Access 5.1)
45
+ export async function memberContext(p, genesisDigest) {
46
+ const label = 'group/' + toU(genesisDigest);
47
+ const ctx = await C.labeledContext(p.rootIkm, label);
48
+ p.contexts.set(ctx.anchor, ctx);
49
+ p.contexts.set(ctx.keyAgreement, ctx);
50
+ return ctx;
51
+ }
52
+ const memberCard = async (ctx, name, whenIso) => validOr(await C.signCard(ctx, C.cardBody(ctx, { name }), whenIso), 'contact-card.schema.json');
53
+ // ── founding ────────────────────────────────────────────────────────────
54
+ export async function foundGroup(p, label, when, ent = {}) {
55
+ const iso = C.iso(when);
56
+ const founding = await C.pairContext(p.rootIkm, ent.foundingNonce ?? C.rand(32)); // pair class: the founding occasion
57
+ const groupDid = C.anchorOfEd((await C.edFromSeed(ent.groupSeed ?? C.rand(32))).pubRaw); // group address (probe mint; Identity work)
58
+ const genesisBody = { type: 'group-genesis@probe', group: groupDid, label, founder: founding.anchor, issuedAt: iso };
59
+ const genesis = await C.diSign(founding, genesisBody, iso);
60
+ const genesisDigest = await C.digestDoc(genesisBody); // proof-free signature input (Access 3.2)
61
+ const my = await memberContext(p, genesisDigest);
62
+ const card = await memberCard(my, p.name, iso);
63
+ const g = {
64
+ label, groupDid, genesisDigest, genesis, myMemberCtx: my, role: 'founder',
65
+ vouchThreshold: ent.vouchThreshold ?? 1, // Bürgschaftsschwelle (Gruppenregel; UI zeigt sie nur > 1)
66
+ vouchesReceived: new Map(), // Bürgschaften FÜR MICH (issuer -> vouch@2)
67
+ myVouches: new Map(), // Bürgschaften VON MIR (candidate -> vouch@2)
68
+ roster: new Map([[my.anchor, { name: p.name, card, addedAt: iso, founder: true }]]),
69
+ threads: new Map(), // invitee member anchor -> { threadId, invite } (open invitations)
70
+ };
71
+ p.groups.set(genesisDigest, g);
72
+ say(p, `Gruppe „${label}" gegründet — Genesis unter Gründungs-pair-Anker, Member-Anker ${my.anchor.slice(0, 20)}… abgeleitet`);
73
+ return g;
74
+ }
75
+ // ── transport helper: sealed doc over an existing relationship channel ──
76
+ async function sendDoc(p, contact, doc, ent = {}) {
77
+ return { to: contact, env: await C.seal(doc, contact.channel.counterpartKa, ent) };
78
+ }
79
+ const channelCtxOf = (p, doc) => p.contexts.get(doc.recipient) ?? null;
80
+ const contactByAnchor = (p, anchor) => p.contacts.get(anchor) ?? null;
81
+ const contactOfIssuer = (p, issuer) => p.contacts.get(issuer) ?? null;
82
+ // ── prelude (Membership 1.2/3.1: the invitee supplies the member anchor) ─
83
+ export async function preludeRequest(p, contactAnchor, genesisDigest, when, ent = {}) {
84
+ const contact = contactByAnchor(p, contactAnchor);
85
+ const g = p.groups.get(genesisDigest);
86
+ const body = {
87
+ id: uuid(ent.id), type: TT + 'membership-prelude-request@probe',
88
+ issuer: contact.channel.own.anchor, recipient: contactAnchor,
89
+ threadId: uuid(ent.threadId), issuedAt: C.iso(when),
90
+ payload: { genesisDigest, group: g.groupDid, label: g.label },
91
+ };
92
+ const doc = await C.diSign(contact.channel.own, body, C.iso(when));
93
+ say(p, `Prelude an ${contact.name}: genesisDigest der Gruppe „${g.label}" über den Beziehungs-Kanal`);
94
+ return sendDoc(p, contact, doc, ent);
95
+ }
96
+ // ── receive dispatch: every membership doc travels the channel sealed ───
97
+ // Returns { handled, outbound?: [{to, env}], prompt?: entry }.
98
+ export async function receiveDoc(p, env, when, ent = {}) {
99
+ const opened = await C.openEnvelope(p, env); // Stufen 1–4, Cache-Lesung
100
+ if (opened.duplicate)
101
+ return { handled: true, duplicate: true };
102
+ if (opened.error)
103
+ return { handled: false };
104
+ const r = await receiveDocInner(p, env, opened.doc, when, ent);
105
+ if (r?.handled && !r.error)
106
+ C.effectDone(p, opened.digest);
107
+ return r;
108
+ }
109
+ async function receiveDocInner(p, env, doc, when, ent = {}) {
110
+ const ctx = channelCtxOf(p, { recipient: env.rkid }) ?? p.contexts.get(env.rkid);
111
+ if (!ctx)
112
+ return { handled: false };
113
+ if (typeof doc?.type !== 'string' || !doc.type.startsWith(TT + 'membership-'))
114
+ return { handled: false, doc };
115
+ // the sender is identified by the CHANNEL the envelope arrived on (rkid
116
+ // names our own pair context of exactly one relationship) — membership
117
+ // documents carry MEMBER anchors as issuer, which no contact key holds
118
+ const from = [...p.contacts.values()].find((c) => c.channel?.own?.anchor === ctx.anchor) ?? contactOfIssuer(p, doc.issuer);
119
+ switch (doc.type.slice((TT + 'membership-').length)) {
120
+ case 'prelude-request@probe': {
121
+ // form BEFORE fields (review 3, M-3): a valid signature binds
122
+ // whatever was signed — payload.genesisDigest earns its type here
123
+ if (!C.shaped(doc, { issuer: 'string', threadId: 'string', payload: 'object', proof: 'object' })
124
+ || !C.shaped(doc.payload, { genesisDigest: 'string', group: 'string', label: 'string' }))
125
+ return { handled: true, error: 'malformed prelude' };
126
+ if (!from || !(await C.diVerify(doc, doc.issuer)))
127
+ return { handled: true, error: 'prelude proof' };
128
+ let my;
129
+ try {
130
+ my = await memberContext(p, doc.payload.genesisDigest);
131
+ }
132
+ catch {
133
+ return { handled: true, error: 'genesisDigest not canonical' };
134
+ }
135
+ const body = {
136
+ id: uuid(), type: TT + 'membership-prelude-reply@probe',
137
+ issuer: from.channel.own.anchor, recipient: doc.issuer,
138
+ threadId: doc.threadId, issuedAt: C.iso(when),
139
+ payload: { genesisDigest: doc.payload.genesisDigest, memberAnchor: my.anchor },
140
+ };
141
+ const reply = await C.diSign(from.channel.own, body, C.iso(when));
142
+ say(p, `Prelude von ${from.name}: Member-Anker für „${doc.payload.label}" abgeleitet und geantwortet`);
143
+ return { handled: true, outbound: [await sendDoc(p, from, reply, ent)] };
144
+ }
145
+ case 'prelude-reply@probe': {
146
+ if (!C.shaped(doc, { issuer: 'string', threadId: 'string', payload: 'object', proof: 'object' })
147
+ || !C.shaped(doc.payload, { genesisDigest: 'string', memberAnchor: 'string' })
148
+ || C.edRawOfAnchor(doc.payload.memberAnchor) === null)
149
+ return { handled: true, error: 'malformed prelude-reply' };
150
+ if (!from || !(await C.diVerify(doc, doc.issuer)))
151
+ return { handled: true, error: 'prelude-reply proof' };
152
+ const g = [...p.groups.values()].find((g) => g.genesisDigest === doc.payload.genesisDigest);
153
+ if (!g)
154
+ return { handled: true, error: 'unknown group' };
155
+ // Log erst NACH dem gelungenen Bau; ein Schema-Throw aus
156
+ // buildInvite verlässt die Empfangs-Promise nie (Review 9, P-B2)
157
+ let inv;
158
+ try {
159
+ inv = await buildInvite(p, g, from, doc.payload.memberAnchor, when, doc.threadId, ent);
160
+ }
161
+ catch (e) {
162
+ return { handled: true, error: 'invite build: ' + String(e?.message ?? e) };
163
+ }
164
+ say(p, `${from.name}s Member-Anker erhalten — Einladung wird ausgestellt`);
165
+ return { handled: true, outbound: [inv] };
166
+ }
167
+ case 'invite/0.2': return handleInvite(p, doc, from, when);
168
+ case 'accept/0.2': return handleAccept(p, doc, from, when, ent);
169
+ case 'welcome@probe': return handleWelcome(p, doc, from);
170
+ case 'vouch/0.1': return handleVouch(p, doc, from);
171
+ }
172
+ return { handled: false, doc };
173
+ }
174
+ // ── the invite: a conformant DTG InvitationCredential (one carrier) ─────
175
+ export async function buildInvite(p, g, contact, inviteeMemberAnchor, when, threadId, ent = {}) {
176
+ const iso = C.iso(when), until = C.iso(when + 24 * 3600 * 1000);
177
+ const inviterCard = await memberCard(g.myMemberCtx, p.name, iso);
178
+ const credBody = {
179
+ '@context': CTX3,
180
+ type: ['VerifiableCredential', 'DTGCredential', 'InvitationCredential', 'MembershipInvite'],
181
+ issuer: g.myMemberCtx.anchor,
182
+ credentialSubject: { id: inviteeMemberAnchor, group: g.groupDid, genesisDigest: g.genesisDigest, card: inviterCard, name: g.label },
183
+ validFrom: iso, validUntil: until, taskContext: threadId,
184
+ };
185
+ const invite = await C.diSign(g.myMemberCtx, credBody, iso);
186
+ validOr({ invite }, 'payload-membership-invite.schema.json');
187
+ const doc = {
188
+ id: uuid(ent.docId), type: TT + 'membership-invite/0.2',
189
+ issuer: g.myMemberCtx.anchor, recipient: inviteeMemberAnchor,
190
+ threadId, issuedAt: iso, payload: { invite },
191
+ };
192
+ validOr(doc, 'rltp-delivery-document.schema.json');
193
+ g.threads.set(inviteeMemberAnchor, { threadId, invite, contactAnchor: [...p.contacts.entries()].find(([, c]) => c === contact)[0] });
194
+ say(p, `Einladung (InvitationCredential) an ${contact.name} — Thread ${threadId.slice(0, 8)}…`);
195
+ return sendDoc(p, contact, doc, ent);
196
+ }
197
+ async function handleInvite(p, doc, from, when) {
198
+ if (!from)
199
+ return { handled: true, error: 'invite not from a held contact' };
200
+ const inv = doc.payload?.invite;
201
+ try {
202
+ validOr(doc.payload, 'payload-membership-invite.schema.json');
203
+ validOr(doc, 'rltp-delivery-document.schema.json');
204
+ }
205
+ catch (e) {
206
+ say(p, 'Einladung schema-ungültig: ' + e.message);
207
+ return { handled: true, error: 'schema' };
208
+ }
209
+ if ('proof' in doc) {
210
+ say(p, 'Einladung trägt Dokument-Proof — one carrier verletzt');
211
+ return { handled: true, error: 'one-carrier' };
212
+ }
213
+ if (!(await C.diVerify(inv, inv.issuer)))
214
+ return { handled: true, error: 'credential proof' };
215
+ if (inv.issuer !== doc.issuer || inv.credentialSubject.id !== doc.recipient || inv.taskContext !== doc.threadId)
216
+ return { handled: true, error: 'document bindings' };
217
+ if (inv.credentialSubject.card.anchor !== inv.issuer || !(await C.diVerify(inv.credentialSubject.card, inv.issuer)))
218
+ return { handled: true, error: 'card ownership' };
219
+ const cardOK = !('sentTo' in inv.credentialSubject.card) && !('boundTo' in inv.credentialSubject.card) && !('deliveryHints' in inv.credentialSubject.card);
220
+ if (!cardOK)
221
+ return { handled: true, error: 'card enclosure profile' };
222
+ if (![inv.validFrom, inv.validUntil, inv.proof.created].every(calOK) || tsec(inv.validUntil) < tsec(inv.validFrom))
223
+ return { handled: true, error: 'validity' };
224
+ // THE PRELUDE CHECK (Membership 3.1): my own derivation from the
225
+ // genesisDigest must equal credentialSubject.id — a substituted anchor
226
+ // dies here, whatever the prelude's path was
227
+ const my = await memberContext(p, inv.credentialSubject.genesisDigest);
228
+ if (my.anchor !== inv.credentialSubject.id) {
229
+ say(p, 'Prelude-Check FEHLGESCHLAGEN: Einladung nennt nicht meine Ableitung');
230
+ return { handled: true, error: 'prelude mismatch' };
231
+ }
232
+ const entry = { kind: 'invite', doc, invite: inv, from: from.name, fromAnchor: doc.issuer, fromContact: from, myMemberCtx: my, decided: false };
233
+ p.inbox.push(entry);
234
+ say(p, `${from.name} lädt dich in „${inv.credentialSubject.name ?? 'eine Gruppe'}" ein (Einladung geprüft, Prelude-Check bestanden)`);
235
+ return { handled: true, prompt: entry };
236
+ }
237
+ // ── the accept: consent, task-proved, credential-digest ref ─────────────
238
+ export async function acceptInvite(p, entry, when, ent = {}) {
239
+ const inv = entry.invite;
240
+ const iso = C.iso(when);
241
+ const card = await memberCard(entry.myMemberCtx, p.name, iso);
242
+ const payload = { accept: {
243
+ group: inv.credentialSubject.group, subject: entry.myMemberCtx.anchor,
244
+ ref: await C.digestDoc(inv), // the invitation identity: JCS over the COMPLETE credential incl. proof
245
+ card, candidacy: ent.candidacy ?? false,
246
+ } };
247
+ validOr(payload, 'payload-membership-accept.schema.json');
248
+ const body = {
249
+ id: uuid(ent.docId), type: TT + 'membership-accept/0.2',
250
+ issuer: entry.myMemberCtx.anchor, recipient: inv.issuer,
251
+ threadId: inv.taskContext, issuedAt: iso, payload,
252
+ };
253
+ const doc = await C.diSign(entry.myMemberCtx, body, iso); // task proof under the subject (Membership 2)
254
+ validOr(doc, 'rltp-delivery-document.schema.json');
255
+ entry.decided = true;
256
+ entry.accepted = true;
257
+ say(p, `Beitritt zu „${inv.credentialSubject.name}" erklärt (Accept, ref = Credential-Digest)`);
258
+ return sendDoc(p, entry.fromContact, doc, ent);
259
+ }
260
+ // ── admission: the Access 5.3 pair-internal checks, then roster + welcome ─
261
+ async function handleAccept(p, doc, from, when, ent = {}) {
262
+ const g = [...p.groups.values()].find((g) => g.threads.has(doc.issuer));
263
+ if (!g)
264
+ return { handled: true, error: 'no open invitation for this subject' };
265
+ const th = g.threads.get(doc.issuer);
266
+ const inv = th.invite, acc = doc.payload?.accept;
267
+ const fail = async (why) => { say(p, `Admission abgelehnt: ${why}`); return { handled: true, error: why }; };
268
+ try {
269
+ validOr(doc.payload, 'payload-membership-accept.schema.json');
270
+ validOr(doc, 'rltp-delivery-document.schema.json');
271
+ }
272
+ catch {
273
+ return fail('schema');
274
+ }
275
+ if (!(await C.diVerify(doc, doc.issuer)))
276
+ return fail('accept task proof');
277
+ if (!sameDigest(acc.ref, await C.digestDoc(inv)))
278
+ return fail('ref ≠ credential digest'); // decoded-bytes equality
279
+ if (acc.subject !== inv.credentialSubject.id || acc.subject !== doc.issuer)
280
+ return fail('subject binding');
281
+ if (acc.group !== inv.credentialSubject.group || !sameDigest(inv.credentialSubject.genesisDigest, g.genesisDigest))
282
+ return fail('group binding');
283
+ if (doc.threadId !== inv.taskContext)
284
+ return fail('thread binding');
285
+ if (acc.card.anchor !== acc.subject || !(await C.diVerify(acc.card, acc.subject)))
286
+ return fail('card ownership');
287
+ if (('sentTo' in acc.card) || ('boundTo' in acc.card) || ('deliveryHints' in acc.card))
288
+ return fail('card enclosure profile');
289
+ if (![doc.issuedAt, doc.proof.created].every(calOK))
290
+ return fail('calendar validity');
291
+ if (tsec(doc.issuedAt) > tsec(inv.validUntil) + MEMBERSHIP_SKEW * 1000 || tsec(doc.proof.created) > tsec(inv.validUntil) + MEMBERSHIP_SKEW * 1000)
292
+ return fail('accept after validUntil + membership-skew');
293
+ if (Buffer_(jcs(doc)) > 16384 || Buffer_(jcs({ invite: inv })) > 16384)
294
+ return fail('size budget');
295
+ if (!g.roster.has(inv.issuer))
296
+ return fail('inviter not a member');
297
+ if (g.roster.has(acc.subject)) {
298
+ say(p, 'Same-Subject-Admission: idempotent');
299
+ return { handled: true, idempotent: true };
300
+ }
301
+ // ATOMICITY (review 3, B-2): every await that can fail — digest,
302
+ // signing, sealing — happens BEFORE the first state mutation. If the
303
+ // welcome cannot be built, the roster has not grown, the thread still
304
+ // stands, and the SAME delivery can be retried cleanly.
305
+ const newEntry = { name: acc.card.name, card: acc.card, addedAt: C.iso(when), candidacy: acc.candidacy === true,
306
+ acceptDigest: await C.digestDoc(doc), contactAnchor: th.contactAnchor };
307
+ const contact = contactByAnchor(p, th.contactAnchor);
308
+ const rosterAfter = [...g.roster.entries(), [acc.subject, newEntry]];
309
+ const wBody = {
310
+ id: uuid(), type: TT + 'membership-welcome@probe',
311
+ issuer: g.myMemberCtx.anchor, recipient: acc.subject,
312
+ threadId: doc.threadId, issuedAt: C.iso(when),
313
+ payload: { genesisDigest: g.genesisDigest, group: g.groupDid, label: g.label, genesis: g.genesis, vouchThreshold: g.vouchThreshold ?? 1,
314
+ roster: rosterAfter.map(([anchor, m]) => ({ anchor, name: m.name, addedAt: m.addedAt, founder: !!m.founder, candidacy: !!m.candidacy, acceptDigest: m.acceptDigest ?? null })) },
315
+ };
316
+ const welcome = await sendDoc(p, contact, await C.diSign(g.myMemberCtx, wBody, C.iso(when)), ent);
317
+ // — from here on, synchronous only —
318
+ g.roster.set(acc.subject, newEntry);
319
+ {
320
+ const c = contactByAnchor(p, th.contactAnchor);
321
+ if (c)
322
+ (c.sharedGroups ??= []).push(g.genesisDigest);
323
+ } // der Inviter WEISS, wer beigetreten ist
324
+ g.threads.delete(doc.issuer);
325
+ say(p, `${acc.card.name} ist Mitglied von „${g.label}" — Admission kanonisch, Consent konsumiert`);
326
+ return { handled: true, admitted: acc.subject, outbound: [welcome] };
327
+ }
328
+ const Buffer_ = (s) => new TextEncoder().encode(s).length;
329
+ async function handleWelcome(p, doc, from) {
330
+ // form BEFORE fields (M-3): the roster travels as data and becomes
331
+ // this person's map — every entry earns its shape before adoption
332
+ if (!C.shaped(doc, { issuer: 'string', recipient: 'string', payload: 'object', proof: 'object' })
333
+ || !C.shaped(doc.payload, { genesisDigest: 'string', group: 'string', label: 'string', roster: 'array' })
334
+ || !doc.payload.roster.every((m) => C.shaped(m, { anchor: 'string', name: 'string', addedAt: 'string' })))
335
+ return { handled: true, error: 'malformed welcome' };
336
+ if (toU(doc.payload.genesisDigest) !== doc.payload.genesisDigest)
337
+ return { handled: true, error: 'malformed welcome (genesisDigest)' };
338
+ if (!(await C.diVerify(doc, doc.issuer)))
339
+ return { handled: true, error: 'welcome proof' };
340
+ const gd = doc.payload.genesisDigest;
341
+ let my;
342
+ try {
343
+ my = await memberContext(p, gd);
344
+ }
345
+ catch {
346
+ return { handled: true, error: 'genesisDigest not canonical' };
347
+ }
348
+ if (doc.recipient !== my.anchor)
349
+ return { handled: true, error: 'welcome not for my member anchor' };
350
+ const g = {
351
+ label: doc.payload.label, groupDid: doc.payload.group, genesisDigest: gd,
352
+ genesis: doc.payload.genesis, myMemberCtx: my, role: 'member',
353
+ vouchThreshold: doc.payload.vouchThreshold ?? 1,
354
+ vouchesReceived: new Map(), myVouches: new Map(),
355
+ roster: new Map(doc.payload.roster.map((m) => [m.anchor, m])),
356
+ threads: new Map(),
357
+ };
358
+ g.myAcceptDigest = g.roster.get(my.anchor)?.acceptDigest ?? null;
359
+ p.groups.set(gd, g);
360
+ if (from)
361
+ (from.sharedGroups ??= []).push(gd); // der Invitee WEISS, wer ihn eingeladen hat
362
+ say(p, `Willkommen in „${g.label}" — Roster mit ${g.roster.size} Mitgliedern übernommen`);
363
+ return { handled: true, joined: gd };
364
+ }
365
+ // ── die Bürgschaft (Task membership-vouch/0.1, Artefakt vouch@2) ────────
366
+ // ein konformes DTG EndorsementCredential ──────────────────────────────
367
+ // (AdmissionVouch, Access 4.2/4.3): genesisDigest bindet die Gruppe,
368
+ // accept bindet die KONSENTIERTE Kandidatur (keine stehende Bürgschaft),
369
+ // provenance ist selbst-attestiert (met | introduced). Signiert unter dem
370
+ // MEMBER-Anker des Bürgen; schema-validiert bei Ausstellung UND Empfang.
371
+ // Transport (probe): über den Beziehungs-Kanal zum KANDIDATEN — nur wer
372
+ // einen Kanal zum Kandidaten hält, kann liefern (per-device-Wissen,
373
+ // nichts wird erfunden); der Kandidat sammelt und zählt gegen die
374
+ // Schwelle. Gruppenweite Sichtbarkeit der Zählung = Access-Envelope-Ring.
375
+ export async function vouchFor(p, genesisDigest, candidateAnchor, provenance, when, ent = {}) {
376
+ const g = p.groups.get(genesisDigest);
377
+ const m = g?.roster.get(candidateAnchor);
378
+ if (!g || !m)
379
+ return { error: 'unbekannter Kandidat' };
380
+ if (!m.candidacy)
381
+ return { error: 'keine sichtbare Kandidatur (Opt-in fehlt)' };
382
+ if (!m.acceptDigest)
383
+ return { error: 'kein accept-Bezug — Bürgschaft wäre stehend' };
384
+ if (!m.contactAnchor || !p.contacts.get(m.contactAnchor))
385
+ return { error: 'kein Kanal zum Kandidaten' };
386
+ if (g.myVouches.has(candidateAnchor))
387
+ return { error: 'bereits gebürgt' };
388
+ const iso = C.iso(when);
389
+ const cred = await C.diSign(g.myMemberCtx, {
390
+ '@context': CTX3,
391
+ type: ['VerifiableCredential', 'DTGCredential', 'EndorsementCredential', 'AdmissionVouch'],
392
+ issuer: g.myMemberCtx.anchor,
393
+ validFrom: iso,
394
+ credentialSubject: { id: candidateAnchor, endorsement: {
395
+ type: 'AdmissionVouch', genesisDigest: toU(g.genesisDigest), accept: toU(m.acceptDigest), provenance
396
+ } },
397
+ }, iso);
398
+ validOr(cred, 'access-vouch.schema.json');
399
+ g.myVouches.set(candidateAnchor, cred);
400
+ const contact = p.contacts.get(m.contactAnchor);
401
+ const doc = {
402
+ id: uuid(ent.docId), type: TT + 'membership-vouch/0.1',
403
+ issuer: g.myMemberCtx.anchor, recipient: candidateAnchor,
404
+ threadId: uuid(ent.threadId), issuedAt: iso, payload: { vouch: cred },
405
+ };
406
+ say(p, `Bürgschaft für ${m.name} in „${g.label}" ausgestellt (${provenance}, accept-gebunden)`);
407
+ return sendDoc(p, contact, await C.diSign(g.myMemberCtx, doc, iso), ent);
408
+ }
409
+ // Kandidatenseite: jede Prüfung an ihrem Punkt, dann zählen
410
+ async function handleVouch(p, doc, from) {
411
+ const cred = doc.payload?.vouch;
412
+ const g = [...p.groups.values()].find((g) => cred && sameDigest(cred.credentialSubject?.endorsement?.genesisDigest ?? '', g.genesisDigest));
413
+ const fail = (why) => { say(p, `Bürgschaft verworfen: ${why}`); return { handled: true, error: why }; };
414
+ if (!g)
415
+ return fail('unbekannte Gruppe');
416
+ try {
417
+ validOr(cred, 'access-vouch.schema.json');
418
+ }
419
+ catch {
420
+ return fail('schema');
421
+ }
422
+ if (!(await C.diVerify(cred, cred.issuer)) || !(await C.diVerify(doc, doc.issuer)))
423
+ return fail('proof');
424
+ if (cred.issuer !== doc.issuer)
425
+ return fail('issuer binding');
426
+ if (!g.roster.has(cred.issuer))
427
+ return fail('Bürge ist kein Mitglied');
428
+ if (cred.credentialSubject.id !== g.myMemberCtx.anchor)
429
+ return fail('nicht für mich');
430
+ if (!g.myAcceptDigest || !sameDigest(cred.credentialSubject.endorsement.accept, g.myAcceptDigest))
431
+ return fail('accept-Bindung');
432
+ if (g.vouchesReceived.has(cred.issuer)) {
433
+ say(p, 'Bürgschaft doppelt — idempotent');
434
+ return { handled: true, idempotent: true };
435
+ }
436
+ g.vouchesReceived.set(cred.issuer, cred);
437
+ const n = g.vouchesReceived.size, need = g.vouchThreshold ?? 1;
438
+ if (n >= need) {
439
+ const me = g.roster.get(g.myMemberCtx.anchor);
440
+ if (me)
441
+ me.candidacy = false;
442
+ g.candidacyFulfilled = true;
443
+ say(p, `Bürgschaft von ${g.roster.get(cred.issuer)?.name ?? '?'} — Schwelle erreicht (${n}/${need}), Kandidatur erfüllt`);
444
+ }
445
+ else {
446
+ say(p, `Bürgschaft von ${g.roster.get(cred.issuer)?.name ?? '?'} erhalten (${n}/${need})`);
447
+ }
448
+ return { handled: true, vouched: cred.issuer, fulfilled: n >= need };
449
+ }
450
+ //# sourceMappingURL=membership.js.map