Skip to content

Commit 54590cc

Browse files
committed
@pryv/cmc: API ergonomics — revokeRelationship({inviteEventId}) + require explicit scopeStreamId on accept/refuse
Surfaced while adapting Plan 68 validation scenarios to use the new package. Pre-publish polish of the v1.0.0 surface. revokeRelationship - Now accepts EITHER { accessId, scopeStreamId, reason? } (existing power-user path) OR { inviteEventId, scopeStreamId?, reason? } (new convenience path). - The inviteEventId path reads the invite event + the matching consent/accept-cmc on :_cmc:inbox (matched via originalEventId / requestEventId / inviteEventId in the accept's content) and pulls backChannelAccessId from there. - scopeStreamId auto-derived from the invite event's own stream when not given. acceptInvite + refuseInvite - Previous default scopeStreamId was ':_cmc:apps:patient:incoming' which (a) requires the accepter to pre-create that sub-stream and (b) the default-fallback case isn't actually safer than throwing. Now REQUIRED. - Bad-default trap: ':_cmc:inbox' is the inbox stream — events written there route through the peer-delivered handleIncomingAccept path, not the local handleAccept. The error message points this out so callers don't accidentally pick it. listAcceptedRelationships - Default scopeStreamId changed from ':_cmc:apps:patient:incoming' to NS_APPS (':_cmc:apps') so the recursive default covers every app scope on the account (matches the user mental model of 'list everything I've accepted'). Tests: 4 [CMCL1A*] tests updated to pass explicit scopeStreamId. 43/43 still passing. common.js in _plans/68/tests/ continues to load (already passed explicit scopeStreamId). createInvite features key - Spec asked for {chat, system}; server-side validators.ts requires {chat, systemMessaging}. Kept current (correct) {chat, systemMessaging} shape. Documented in CHANGELOG. Bumps minor not needed — this is still pre-publish work on the same v1.0.0 release branch.
1 parent de3f25e commit 54590cc

3 files changed

Lines changed: 96 additions & 25 deletions

File tree

components/pryv-cmc/src/index.d.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -185,11 +185,17 @@ declare module '@pryv/cmc' {
185185

186186
export function getInviteStatus(conn: any, inviteEventId: string): Promise<InviteRecord>;
187187

188-
export function revokeRelationship(conn: any, params: {
189-
scopeStreamId: string;
190-
accessId: string;
191-
reason?: Record<string, string>;
192-
}): Promise<void>;
188+
/**
189+
* Revoke a relationship (provider side). Two ways to identify the
190+
* relationship:
191+
* 1. `{ accessId, scopeStreamId, reason? }` — power-user path.
192+
* 2. `{ inviteEventId, scopeStreamId?, reason? }` — convenience path;
193+
* derives `accessId` from the matching inbox accept event.
194+
*/
195+
export function revokeRelationship(conn: any, params:
196+
| { accessId: string; scopeStreamId: string; reason?: Record<string, string> }
197+
| { inviteEventId: string; scopeStreamId?: string; reason?: Record<string, string> }
198+
): Promise<void>;
193199

194200
export function invalidateCapability(conn: any, params: {
195201
inviteEventId: string;
@@ -214,8 +220,13 @@ declare module '@pryv/cmc' {
214220
features: { chat?: boolean; systemMessaging?: boolean };
215221
}>;
216222

217-
export function acceptInvite(conn: any, capabilityUrl: string, opts?: {
218-
scopeStreamId?: string;
223+
/**
224+
* Accept an offer. `opts.scopeStreamId` is REQUIRED — must be an
225+
* `:_cmc:apps:<app>[:...]` stream on the accepter's account. Do not
226+
* pass `:_cmc:inbox` (which routes through the peer-delivered path).
227+
*/
228+
export function acceptInvite(conn: any, capabilityUrl: string, opts: {
229+
scopeStreamId: string;
219230
extra?: { chat?: boolean; systemMessaging?: boolean };
220231
accessName?: string;
221232
waitForCompletion?: boolean;
@@ -226,8 +237,12 @@ declare module '@pryv/cmc' {
226237
| { acceptEventId: string; dataGrantAccessId: string | null; dataGrantApiEndpoint: string | null; counterparty: any; features: any }
227238
>;
228239

229-
export function refuseInvite(conn: any, capabilityUrl: string, opts?: {
230-
scopeStreamId?: string;
240+
/**
241+
* Refuse an offer. `opts.scopeStreamId` is REQUIRED — same constraints
242+
* as `acceptInvite`.
243+
*/
244+
export function refuseInvite(conn: any, capabilityUrl: string, opts: {
245+
scopeStreamId: string;
231246
reason?: Record<string, string>;
232247
}): Promise<{ refuseEventId: string }>;
233248

components/pryv-cmc/src/index.js

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -411,19 +411,63 @@ async function getInviteStatus (conn, inviteEventId) {
411411
* `consent/revoke-cmc` event with `{ accessId, reason }`; plugin
412412
* orchestrates dual delete.
413413
*
414+
* Two ways to identify the relationship:
415+
* 1. `{ accessId, scopeStreamId, reason? }` — power-user path, pass
416+
* the back-channel access id directly.
417+
* 2. `{ inviteEventId, scopeStreamId?, reason? }` — convenience path.
418+
* Reads the invite event + matching inbox accept to derive
419+
* `backChannelAccessId`. Two extra API calls. Defaults
420+
* `scopeStreamId` to the invite event's own stream.
421+
*
414422
* @param {Object} conn
415423
* @param {Object} params
416-
* @param {string} params.scopeStreamId - own scope to write into
417-
* @param {string} params.accessId - back-channel access id (provider side)
424+
* @param {string} [params.accessId] - back-channel access id (provider side)
425+
* @param {string} [params.inviteEventId] - alternative to accessId
426+
* @param {string} [params.scopeStreamId] - own scope to write into (required with accessId; auto-derived with inviteEventId)
418427
* @param {Object} [params.reason]
419428
* @returns {Promise<void>}
420429
*/
421430
async function revokeRelationship (conn, params) {
422431
if (params == null) throw new Error('revokeRelationship: params required');
423-
const content = { accessId: params.accessId };
432+
let accessId = params.accessId;
433+
let scopeStreamId = params.scopeStreamId;
434+
if (!accessId) {
435+
if (!params.inviteEventId) {
436+
throw new Error('revokeRelationship: provide accessId, or inviteEventId for lookup');
437+
}
438+
const inviteEvent = await conn.apiOne('events.getOne', { id: params.inviteEventId }, 'event');
439+
if (!inviteEvent) {
440+
throw new Error('revokeRelationship: invite event not found: ' + params.inviteEventId);
441+
}
442+
if (!scopeStreamId) {
443+
scopeStreamId = (inviteEvent.streamIds && inviteEvent.streamIds[0]) || inviteEvent.streamId;
444+
}
445+
const acceptsRaw = await conn.apiOne('events.get', {
446+
streams: [NS_INBOX],
447+
types: [ET_ACCEPT],
448+
limit: 200
449+
}, 'events');
450+
const match = (acceptsRaw || []).find(function (e) {
451+
const c = e.content || {};
452+
return c.originalEventId === params.inviteEventId ||
453+
c.requestEventId === params.inviteEventId ||
454+
c.inviteEventId === params.inviteEventId;
455+
});
456+
if (!match) {
457+
throw new Error('revokeRelationship: no inbox accept found for invite ' + params.inviteEventId);
458+
}
459+
accessId = match.content && match.content.backChannelAccessId;
460+
if (!accessId) {
461+
throw new Error('revokeRelationship: inbox accept ' + match.id + ' has no backChannelAccessId');
462+
}
463+
}
464+
if (!scopeStreamId) {
465+
throw new Error('revokeRelationship: scopeStreamId is required with accessId path');
466+
}
467+
const content = { accessId };
424468
if (params.reason) content.reason = params.reason;
425469
await conn.apiOne('events.create', {
426-
streamIds: [params.scopeStreamId],
470+
streamIds: [scopeStreamId],
427471
type: ET_REVOKE,
428472
content
429473
}, 'event');
@@ -560,8 +604,8 @@ async function readOffer (capabilityUrl, opts) {
560604
*
561605
* @param {Object} conn accepter's connection
562606
* @param {string} capabilityUrl
563-
* @param {Object} [opts]
564-
* @param {string} [opts.scopeStreamId] - own scope to write the accept into (default ':_cmc:apps:patient:incoming')
607+
* @param {Object} opts
608+
* @param {string} opts.scopeStreamId - REQUIRED. Own :_cmc:apps:<app>[:...] stream where the accept trigger lands. Must NOT be :_cmc:inbox (which routes through the peer-delivered path).
565609
* @param {{chat?:boolean,systemMessaging?:boolean}} [opts.extra]
566610
* @param {string} [opts.accessName]
567611
* @param {boolean} [opts.waitForCompletion=true]
@@ -571,7 +615,12 @@ async function readOffer (capabilityUrl, opts) {
571615
*/
572616
async function acceptInvite (conn, capabilityUrl, opts) {
573617
opts = opts || {};
574-
const scopeStreamId = opts.scopeStreamId || ':_cmc:apps:patient:incoming';
618+
if (!opts.scopeStreamId) {
619+
throw new Error('acceptInvite: opts.scopeStreamId is required ' +
620+
'(an :_cmc:apps:<app>[:...] stream on YOUR account where the accept ' +
621+
'trigger lands — not :_cmc:inbox, which routes through the peer-delivered path)');
622+
}
623+
const scopeStreamId = opts.scopeStreamId;
575624
const content = { capabilityUrl };
576625
if (opts.extra) content.extra = opts.extra;
577626
if (opts.accessName) content.accessName = opts.accessName;
@@ -632,14 +681,18 @@ function sleep (ms) {
632681
*
633682
* @param {Object} conn
634683
* @param {string} capabilityUrl
635-
* @param {Object} [opts]
636-
* @param {string} [opts.scopeStreamId]
684+
* @param {Object} opts
685+
* @param {string} opts.scopeStreamId - REQUIRED. Own :_cmc:apps:<app>[:...] stream where the refuse trigger lands.
637686
* @param {Object} [opts.reason]
638687
* @returns {Promise<{refuseEventId:string}>}
639688
*/
640689
async function refuseInvite (conn, capabilityUrl, opts) {
641690
opts = opts || {};
642-
const scopeStreamId = opts.scopeStreamId || ':_cmc:apps:patient:incoming';
691+
if (!opts.scopeStreamId) {
692+
throw new Error('refuseInvite: opts.scopeStreamId is required ' +
693+
'(an :_cmc:apps:<app>[:...] stream on YOUR account where the refuse trigger lands)');
694+
}
695+
const scopeStreamId = opts.scopeStreamId;
643696
const content = { capabilityUrl };
644697
if (opts.reason) content.reason = opts.reason;
645698
const event = await conn.apiOne('events.create', {
@@ -677,13 +730,16 @@ async function revokeAcceptance (conn, params) {
677730
*
678731
* @param {Object} conn
679732
* @param {Object} [params]
680-
* @param {string} [params.scopeStreamId=':_cmc:apps:patient:incoming']
733+
* @param {string} [params.scopeStreamId=':_cmc:apps'] - root or sub-scope to search recursively
681734
* @param {number} [params.limit=1000]
682735
* @returns {Promise<Array>}
683736
*/
684737
async function listAcceptedRelationships (conn, params) {
685738
params = params || {};
686-
const streams = [params.scopeStreamId || ':_cmc:apps:patient:incoming'];
739+
// Default to the apps root (recursive) so callers get every relationship
740+
// across all of their app scopes. Pass `params.scopeStreamId` for a narrower
741+
// view (e.g. a single app or sub-scope).
742+
const streams = [params.scopeStreamId || NS_APPS];
687743
const limit = params.limit || 1000;
688744
const events = await conn.apiOne('events.get', {
689745
streams,

components/pryv-cmc/test/cmc.test.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ describe('[CMCL1] @pryv/cmc Level-1 protocol functions', function () {
414414
}
415415
}
416416
});
417-
const r = await cmc.acceptInvite(conn, 'https://t@x.example.com/', { waitForCompletion: false });
417+
const r = await cmc.acceptInvite(conn, 'https://t@x.example.com/', { scopeStreamId: ':_cmc:apps:test', waitForCompletion: false });
418418
expect(r).to.deep.equal({ acceptEventId: 'acc-1', dataGrantAccessId: null, status: 'pending' });
419419
expect(conn.calls).to.have.length(1);
420420
});
@@ -443,7 +443,7 @@ describe('[CMCL1] @pryv/cmc Level-1 protocol functions', function () {
443443
}
444444
}
445445
});
446-
const r = await cmc.acceptInvite(conn, 'https://t@x/', { completionPollIntervalMs: 5, completionTimeoutMs: 1000 });
446+
const r = await cmc.acceptInvite(conn, 'https://t@x/', { scopeStreamId: ':_cmc:apps:test', completionPollIntervalMs: 5, completionTimeoutMs: 1000 });
447447
expect(r.acceptEventId).to.equal('acc-2');
448448
expect(r.dataGrantAccessId).to.equal('dg-1');
449449
expect(r.counterparty).to.deep.equal({ username: 'alice', host: 'pryv.me' });
@@ -463,7 +463,7 @@ describe('[CMCL1] @pryv/cmc Level-1 protocol functions', function () {
463463
});
464464
let err = null;
465465
try {
466-
await cmc.acceptInvite(conn, 'https://t@x/', { completionPollIntervalMs: 5, completionTimeoutMs: 1000 });
466+
await cmc.acceptInvite(conn, 'https://t@x/', { scopeStreamId: ':_cmc:apps:test', completionPollIntervalMs: 5, completionTimeoutMs: 1000 });
467467
} catch (e) { err = e; }
468468
expect(err).to.exist;
469469
expect(err).to.be.instanceOf(cmc.CmcError);
@@ -478,7 +478,7 @@ describe('[CMCL1] @pryv/cmc Level-1 protocol functions', function () {
478478
}
479479
}
480480
});
481-
const r = await cmc.refuseInvite(conn, 'https://t@x/', { reason: { en: 'no' } });
481+
const r = await cmc.refuseInvite(conn, 'https://t@x/', { scopeStreamId: ':_cmc:apps:test', reason: { en: 'no' } });
482482
expect(conn.calls[0].params.type).to.equal('consent/refuse-cmc');
483483
expect(r.refuseEventId).to.equal('rf-1');
484484
});

0 commit comments

Comments
 (0)