Skip to content

Commit de3f25e

Browse files
committed
@pryv/cmc@1.0.0: new sibling package — Level-0 + Level-1 CMC client surface
New npm package shipping the full CMC client-side surface, sibling to @pryv/monitor and @pryv/socket.io. The previously-shipped pryv@3.2.0 pryv.cmc.* namespace (Level-0 only) has been removed from pryv in the matching pryv@3.3.0 release; this package replaces and extends it. Level-0 (moved from pryv.cmc.*) - Namespace constants: NS, NS_INBOX, NS_APPS, NS_INTERNAL, NS_INTERNAL_RETRIES. - Event-type constants: ET_REQUEST, ET_ACCEPT, ET_REFUSE, ET_REVOKE, ET_CHAT, ET_SYSTEM_ALERT, ET_SYSTEM_ACK, ET_SCOPE_REQUEST, ET_SCOPE_UPDATE (canonical + legacy ET_SYSTEM_SCOPE_* aliases). NEW: ET_INVALIDATE_LINK ('consent/invalidate-link-cmc') for Phase-2 open-link invalidation (lands in open-pryv.io PR series). - Grouped collections EVENT_TYPES_LIFECYCLE / EVENT_TYPES_CHAT / EVENT_TYPES_SYSTEM. - Slug helpers: slugifyHost, counterpartySlug, parseCounterpartySlug, SEPARATOR. - Stream-id builders: appScope, chatsParentUnder, chatStreamUnder, collectorsParentUnder, collectorStreamUnder. - Classification + parsing: isCmcStreamId, isAppNestedPluginStream, getAppCode, parseChatStreamId, parseCollectorStreamId. - errorIds catalogue mirroring open-pryv.io's CmcErrorIds. Removed CHAT_RATE_LIMITED (rate-limiter removed server-side). Added CAPABILITY_ALREADY_ACCEPTED_BY_YOU for Phase-2 open-link same-patient re-click detection. Level-1 (NEW — protocol functions consumers no longer reimplement) Provider side: - createInvite(conn, params) -> { inviteEventId, capabilityUrl, mode, expiresAt } - listInvites(conn, params) -> { items: InviteRecord[], truncated } Capped single call, default limit 1000; document Connection.getEventsStreamed as fallback when truncated. - getInviteStatus(conn, inviteEventId) -> InviteRecord - revokeRelationship(conn, params) -> void (consent/revoke-cmc) - invalidateCapability(conn, params) -> void (consent/invalidate-link-cmc, open-link only) - requestScopeUpdate(conn, params) -> { scopeRequestEventId } Consumer side: - readOffer(capabilityUrl) -> { requester, consent, requestedPermissions, mode, features }. No auto-retry per lib-js convention; caller picks retry policy. - acceptInvite(conn, capabilityUrl, opts) -> Phase-2-completed record by default. waitForCompletion:false opt-out returns Phase-1 record immediately. Polls trigger event status for completion (default 200ms interval, 10s timeout) to avoid socket dep. - refuseInvite(conn, capabilityUrl, opts) -> { refuseEventId } - revokeAcceptance(conn, params) -> void - listAcceptedRelationships(conn, params) -> RelationshipRecord[] Cross-direction: - sendChat(conn, params) -> { chatEventId } (content: string, schema 1:1) - sendSystemAlert(conn, params) -> { alertEventId } - sendSystemAck(conn, params) -> { ackEventId } - acceptScopeUpdate(conn, scopeRequestEventId) -> { updateAcceptEventId, newDataGrantAccessId } - refuseScopeUpdate(conn, scopeRequestEventId, opts) -> { updateRefuseEventId } Observation: - scopes.inbox / scopes.chats / scopes.collectors return Monitor scope objects for use with 'new pryv.Monitor(conn, scope)'. Types - DecomposedAPIEndpoint shape consumer can reach via pryv.utils. - InviteRecord, RelationshipRecord, InviteStatus, CapabilityMode. - CmcError thrown on Phase-2 completion failure carrying tagged failure.reason as error.id. Tests: 43/43 ([CMCX] Level-0 18 + [CMCL1] Level-1 25). Stub Connection records { method, params, expectedKey } calls; tests assert wire shape. Deep integration left to _plans/68-.../tests/02-03 against deployed open-pryv.io (per release sequencing).
1 parent 06bd044 commit de3f25e

4 files changed

Lines changed: 1895 additions & 0 deletions

File tree

components/pryv-cmc/package.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "@pryv/cmc",
3+
"version": "1.0.0",
4+
"description": "Cross-account Messaging & Consent client helpers for Pryv.io",
5+
"keywords": [
6+
"Pryv",
7+
"Pryv.io",
8+
"CMC",
9+
"Consent",
10+
"Messaging"
11+
],
12+
"homepage": "https://github.com/pryv/lib-js/tree/master/components/pryv-cmc#readme",
13+
"bugs": {
14+
"url": "https://github.com/pryv/lib-js/issues"
15+
},
16+
"repository": {
17+
"type": "git",
18+
"url": "git://github.com/pryv/lib-js.git"
19+
},
20+
"license": "BSD-3-Clause",
21+
"author": "Pryv S.A. <info@pryv.com> (https://pryv.com)",
22+
"main": "src/index.js",
23+
"types": "src/index.d.ts",
24+
"engines": {
25+
"node": ">=20.0.0"
26+
},
27+
"peerDependencies": {
28+
"pryv": "^3.3.0"
29+
}
30+
}

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

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
declare module '@pryv/cmc' {
2+
// --- Namespace constants ---
3+
export const NS: ':_cmc:';
4+
export const NS_INBOX: ':_cmc:inbox';
5+
export const NS_APPS: ':_cmc:apps';
6+
export const NS_INTERNAL: ':_cmc:_internal';
7+
export const NS_INTERNAL_RETRIES: ':_cmc:_internal:retries';
8+
9+
// --- Event types ---
10+
export const ET_REQUEST: 'consent/request-cmc';
11+
export const ET_ACCEPT: 'consent/accept-cmc';
12+
export const ET_REFUSE: 'consent/refuse-cmc';
13+
export const ET_REVOKE: 'consent/revoke-cmc';
14+
export const ET_INVALIDATE_LINK: 'consent/invalidate-link-cmc';
15+
export const ET_SCOPE_REQUEST: 'consent/scope-request-cmc';
16+
export const ET_SCOPE_UPDATE: 'consent/scope-update-cmc';
17+
export const ET_CHAT: 'message/chat-cmc';
18+
export const ET_SYSTEM_ALERT: 'notification/alert-cmc';
19+
export const ET_SYSTEM_ACK: 'notification/ack-cmc';
20+
export const ET_SYSTEM_SCOPE_REQUEST: 'consent/scope-request-cmc';
21+
export const ET_SYSTEM_SCOPE_UPDATE: 'consent/scope-update-cmc';
22+
export const EVENT_TYPES_LIFECYCLE: readonly string[];
23+
export const EVENT_TYPES_CHAT: readonly string[];
24+
export const EVENT_TYPES_SYSTEM: readonly string[];
25+
26+
// --- Slug helpers ---
27+
export const SEPARATOR: '--';
28+
29+
export type CmcActor = {
30+
username: string;
31+
host: string;
32+
};
33+
34+
export type CmcCounterpartySlugInput = {
35+
username: string;
36+
host: string;
37+
};
38+
39+
export type CmcParsedCounterpartySlug = {
40+
username: string;
41+
/** Host with `.` replaced by `-`. Lossy — store the canonical host alongside if needed. */
42+
hostSlug: string;
43+
};
44+
45+
export type CmcParsedStreamId = {
46+
appCode: string | null;
47+
/** The :_cmc:apps:<app-code>[:...] prefix above the chats/collectors segment. */
48+
scopeStreamId: string;
49+
counterpartySlug: string;
50+
counterparty: CmcParsedCounterpartySlug;
51+
};
52+
53+
export function slugifyHost(host: string): string;
54+
export function counterpartySlug(params: CmcCounterpartySlugInput): string;
55+
export function parseCounterpartySlug(slug: string): CmcParsedCounterpartySlug;
56+
57+
// --- Stream-id builders ---
58+
export function appScope(appCode: string): string;
59+
export function chatsParentUnder(scopeStreamId: string): string;
60+
export function chatStreamUnder(scopeStreamId: string, slug: string): string;
61+
export function collectorsParentUnder(scopeStreamId: string): string;
62+
export function collectorStreamUnder(scopeStreamId: string, slug: string): string;
63+
64+
// --- Classification + parsing ---
65+
export function isCmcStreamId(streamId: string): boolean;
66+
export function isAppNestedPluginStream(streamId: string): boolean;
67+
export function getAppCode(streamId: string): string | null;
68+
export function parseChatStreamId(streamId: string): CmcParsedStreamId | null;
69+
export function parseCollectorStreamId(streamId: string): CmcParsedStreamId | null;
70+
71+
// --- Error catalogue ---
72+
export type CmcErrorId =
73+
| 'cmc-capability-invalid'
74+
| 'cmc-capability-consumed'
75+
| 'cmc-capability-invalidated'
76+
| 'cmc-capability-already-accepted-by-you'
77+
| 'cmc-capability-timeout'
78+
| 'cmc-capability-empty'
79+
| 'cmc-capability-multiple-offers'
80+
| 'cmc-handler-missing-capability-url'
81+
| 'cmc-handler-offer-missing-capability-id'
82+
| 'cmc-offer-empty-permissions'
83+
| 'cmc-handler-wrong-type'
84+
| 'cmc-handler-threw'
85+
| 'cmc-handler-offer-read-failed'
86+
| 'cmc-handler-counterparty-unknown'
87+
| 'cmc-handler-data-grant-create-failed'
88+
| 'cmc-handler-data-grant-no-apiendpoint'
89+
| 'cmc-handler-build-data-grant-failed'
90+
| 'cmc-back-channel-create-failed'
91+
| 'cmc-handler-delivery-threw'
92+
| 'cmc-handler-delivery-rejected'
93+
| 'cmc-handler-delivery-failed'
94+
| 'cmc-chat-stream-not-chat'
95+
| 'cmc-chat-counterparty-access-not-found'
96+
| 'cmc-chat-no-remote-apiendpoint'
97+
| 'cmc-chat-no-remote-chat-stream';
98+
99+
export const errorIds: {
100+
readonly CAPABILITY_INVALID: 'cmc-capability-invalid';
101+
readonly CAPABILITY_CONSUMED: 'cmc-capability-consumed';
102+
readonly CAPABILITY_INVALIDATED: 'cmc-capability-invalidated';
103+
readonly CAPABILITY_ALREADY_ACCEPTED_BY_YOU: 'cmc-capability-already-accepted-by-you';
104+
readonly CAPABILITY_TIMEOUT: 'cmc-capability-timeout';
105+
readonly CAPABILITY_EMPTY: 'cmc-capability-empty';
106+
readonly CAPABILITY_MULTIPLE_OFFERS: 'cmc-capability-multiple-offers';
107+
readonly HANDLER_MISSING_CAPABILITY_URL: 'cmc-handler-missing-capability-url';
108+
readonly HANDLER_OFFER_MISSING_CAPABILITY_ID: 'cmc-handler-offer-missing-capability-id';
109+
readonly OFFER_EMPTY_PERMISSIONS: 'cmc-offer-empty-permissions';
110+
readonly HANDLER_WRONG_TYPE: 'cmc-handler-wrong-type';
111+
readonly HANDLER_THREW: 'cmc-handler-threw';
112+
readonly HANDLER_OFFER_READ_FAILED: 'cmc-handler-offer-read-failed';
113+
readonly HANDLER_COUNTERPARTY_UNKNOWN: 'cmc-handler-counterparty-unknown';
114+
readonly HANDLER_DATA_GRANT_CREATE_FAILED: 'cmc-handler-data-grant-create-failed';
115+
readonly HANDLER_DATA_GRANT_NO_APIENDPOINT: 'cmc-handler-data-grant-no-apiendpoint';
116+
readonly HANDLER_BUILD_DATA_GRANT_FAILED: 'cmc-handler-build-data-grant-failed';
117+
readonly BACK_CHANNEL_CREATE_FAILED: 'cmc-back-channel-create-failed';
118+
readonly HANDLER_DELIVERY_THREW: 'cmc-handler-delivery-threw';
119+
readonly HANDLER_DELIVERY_REJECTED: 'cmc-handler-delivery-rejected';
120+
readonly HANDLER_DELIVERY_FAILED: 'cmc-handler-delivery-failed';
121+
readonly CHAT_STREAM_NOT_CHAT: 'cmc-chat-stream-not-chat';
122+
readonly CHAT_COUNTERPARTY_ACCESS_NOT_FOUND: 'cmc-chat-counterparty-access-not-found';
123+
readonly CHAT_NO_REMOTE_APIENDPOINT: 'cmc-chat-no-remote-apiendpoint';
124+
readonly CHAT_NO_REMOTE_CHAT_STREAM: 'cmc-chat-no-remote-chat-stream';
125+
};
126+
127+
/** Typed CMC failure surfaced by Level-1 functions. */
128+
export class CmcError extends Error {
129+
constructor(message: string, id: CmcErrorId | string, cause?: any);
130+
readonly name: 'CmcError';
131+
readonly id: CmcErrorId | string;
132+
readonly cause?: any;
133+
}
134+
135+
// --- Level-1: protocol records ---
136+
137+
export type InviteStatus = 'pending' | 'delivered' | 'accepted' | 'completed' | 'refused' | 'revoked' | 'invalidated' | 'expired' | 'failed';
138+
export type CapabilityMode = 'single-use' | 'open-link';
139+
export type PermissionLevel = 'read' | 'contribute' | 'manage' | 'create-only';
140+
export type Permission = { streamId: string; level: PermissionLevel };
141+
142+
export type InviteRecord = {
143+
inviteEventId: string;
144+
capabilityUrl: string | null;
145+
mode: CapabilityMode;
146+
status: InviteStatus;
147+
expiresAt: number | null;
148+
counterparty?: { username: string; host: string; displayName?: string } | null;
149+
acceptedAt?: number | null;
150+
scopeStreamId: string;
151+
};
152+
153+
export type RelationshipRecord = {
154+
acceptEventId: string;
155+
counterparty: { username: string; host: string; displayName?: string } | null;
156+
dataGrantAccessId: string | null;
157+
backChannelAccessId?: string | null;
158+
appCode: string | null;
159+
scopeStreamId: string;
160+
acceptedAt: number | null;
161+
features: { chat: boolean; system: boolean };
162+
};
163+
164+
// --- Level-1: provider side ---
165+
166+
export function createInvite(conn: any, params: {
167+
appCode: string;
168+
scopeStreamId: string;
169+
displayName: string;
170+
requestedPermissions: Permission[];
171+
mode?: CapabilityMode;
172+
title?: Record<string, string>;
173+
description?: Record<string, string>;
174+
consent?: Record<string, string>;
175+
features?: { chat?: boolean; systemMessaging?: boolean };
176+
expiresAt?: number;
177+
to?: string | null;
178+
requesterMeta?: { displayName?: string; appId?: string; appUrl?: string };
179+
}): Promise<{ inviteEventId: string; capabilityUrl: string; mode: CapabilityMode; expiresAt: number }>;
180+
181+
export function listInvites(conn: any, params?: {
182+
scopeStreamId?: string;
183+
limit?: number;
184+
}): Promise<{ items: InviteRecord[]; truncated: boolean }>;
185+
186+
export function getInviteStatus(conn: any, inviteEventId: string): Promise<InviteRecord>;
187+
188+
export function revokeRelationship(conn: any, params: {
189+
scopeStreamId: string;
190+
accessId: string;
191+
reason?: Record<string, string>;
192+
}): Promise<void>;
193+
194+
export function invalidateCapability(conn: any, params: {
195+
inviteEventId: string;
196+
scopeStreamId?: string;
197+
reason?: Record<string, string>;
198+
}): Promise<void>;
199+
200+
export function requestScopeUpdate(conn: any, params: {
201+
collectorStreamId: string;
202+
newPermissions: Permission[];
203+
message?: Record<string, string>;
204+
expires?: number;
205+
}): Promise<{ scopeRequestEventId: string }>;
206+
207+
// --- Level-1: consumer side ---
208+
209+
export function readOffer(capabilityUrl: string, opts?: { pryv?: any }): Promise<{
210+
requester: { username: string | null; host: string; displayName?: string };
211+
consent?: Record<string, string>;
212+
requestedPermissions: Permission[];
213+
mode: CapabilityMode;
214+
features: { chat?: boolean; systemMessaging?: boolean };
215+
}>;
216+
217+
export function acceptInvite(conn: any, capabilityUrl: string, opts?: {
218+
scopeStreamId?: string;
219+
extra?: { chat?: boolean; systemMessaging?: boolean };
220+
accessName?: string;
221+
waitForCompletion?: boolean;
222+
completionTimeoutMs?: number;
223+
completionPollIntervalMs?: number;
224+
}): Promise<
225+
| { acceptEventId: string; dataGrantAccessId: string | null; status: 'pending' }
226+
| { acceptEventId: string; dataGrantAccessId: string | null; dataGrantApiEndpoint: string | null; counterparty: any; features: any }
227+
>;
228+
229+
export function refuseInvite(conn: any, capabilityUrl: string, opts?: {
230+
scopeStreamId?: string;
231+
reason?: Record<string, string>;
232+
}): Promise<{ refuseEventId: string }>;
233+
234+
export function revokeAcceptance(conn: any, params: {
235+
scopeStreamId: string;
236+
accessId: string;
237+
reason?: Record<string, string>;
238+
}): Promise<void>;
239+
240+
export function listAcceptedRelationships(conn: any, params?: {
241+
scopeStreamId?: string;
242+
limit?: number;
243+
}): Promise<RelationshipRecord[]>;
244+
245+
// --- Level-1: cross-direction ---
246+
247+
export function sendChat(conn: any, params: {
248+
scopeStreamId: string;
249+
peerSlug: string;
250+
content: string;
251+
}): Promise<{ chatEventId: string }>;
252+
253+
export function sendSystemAlert(conn: any, params: {
254+
scopeStreamId: string;
255+
peerSlug: string;
256+
level?: 'info' | 'warning' | 'critical';
257+
title: Record<string, string>;
258+
body: Record<string, string>;
259+
ackRequired?: boolean;
260+
ackId?: string;
261+
}): Promise<{ alertEventId: string }>;
262+
263+
export function sendSystemAck(conn: any, params: {
264+
scopeStreamId: string;
265+
peerSlug: string;
266+
alertEventId: string;
267+
ackId: string;
268+
}): Promise<{ ackEventId: string }>;
269+
270+
export function acceptScopeUpdate(conn: any, scopeRequestEventId: string, opts?: {
271+
scopeStreamId?: string;
272+
}): Promise<{ updateAcceptEventId: string; newDataGrantAccessId: string | null }>;
273+
274+
export function refuseScopeUpdate(conn: any, scopeRequestEventId: string, opts?: {
275+
scopeStreamId?: string;
276+
reason?: Record<string, string>;
277+
}): Promise<{ updateRefuseEventId: string }>;
278+
279+
// --- Observation scopes (for use with `new pryv.Monitor(conn, scope)`) ---
280+
281+
export const scopes: {
282+
inbox(params?: { appCode?: string }): { streams: string[] };
283+
chats(params: { appCode?: string; peerSlug?: string; scopeStreamId?: string }): { streams: string[] };
284+
collectors(params: { appCode?: string; peerSlug?: string; scopeStreamId?: string }): { streams: string[] };
285+
};
286+
}

0 commit comments

Comments
 (0)