@axiom-lattice/protocols 4.1.4 → 4.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiom-lattice/protocols",
3
- "version": "4.1.4",
3
+ "version": "4.2.0",
4
4
  "description": "Unified protocol type definitions for Axiom Lattice framework",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -29,8 +29,14 @@ export interface McpServerConfig {
29
29
  args?: string[];
30
30
  /** URL for HTTP/SSE transport */
31
31
  url?: string;
32
- /** Environment variables */
32
+ /** Environment variables (stdio transport; credentials passed to the server process) */
33
33
  env?: Record<string, string>;
34
+ /**
35
+ * Custom HTTP headers sent with every request (streamable_http / sse only).
36
+ * Commonly used for authentication, e.g. `{ Authorization: "Bearer <token>" }`.
37
+ * Ignored for stdio transport. Values are encrypted at rest by config stores.
38
+ */
39
+ headers?: Record<string, string>;
34
40
  /** Connection timeout in milliseconds */
35
41
  timeout?: number;
36
42
  /** Retry attempts on connection failure */
@@ -38,7 +38,8 @@ export interface McpServerConfigEntry {
38
38
  selectedTools: string[];
39
39
 
40
40
  /**
41
- * Whether the env field is encrypted in storage
41
+ * Whether secret values (env vars and headers) are encrypted at rest.
42
+ * Field name kept for backward compatibility; it covers config.headers too.
42
43
  */
43
44
  isEnvEncrypted: boolean;
44
45
 
@@ -23,4 +23,12 @@ export interface ProjectRoomMessageStore {
23
23
 
24
24
  /** Finds a room message by identifier within a tenant. */
25
25
  findById(tenantId: string, id: string): Promise<ProjectRoomMessage | null>;
26
+
27
+ /** Counts messages created strictly after a horizon, optionally excluding one human author. */
28
+ countAfter(input: {
29
+ tenantId: string;
30
+ roomId: string;
31
+ after: Date;
32
+ excludeAuthorUserId?: string;
33
+ }): Promise<number>;
26
34
  }
@@ -0,0 +1,20 @@
1
+ /** A user's per-room read marker used to compute unread counts. */
2
+ export interface ProjectRoomReadState {
3
+ tenantId: string;
4
+ roomId: string;
5
+ userId: string;
6
+ /** Read horizon: messages created strictly after this instant are unread. */
7
+ lastReadAt: Date;
8
+ updatedAt: Date;
9
+ }
10
+
11
+ /** Persistence operations for per-user, per-room read markers. */
12
+ export interface ProjectRoomReadStateStore {
13
+ /** Returns the user's read marker for a room, or null when never reported. */
14
+ get(tenantId: string, roomId: string, userId: string): Promise<ProjectRoomReadState | null>;
15
+ /**
16
+ * Upserts the read marker monotonically: an earlier lastReadAt never moves
17
+ * the marker backwards. Returns the stored state after the write.
18
+ */
19
+ markRead(input: { tenantId: string; roomId: string; userId: string; lastReadAt: Date }): Promise<ProjectRoomReadState>;
20
+ }
@@ -83,25 +83,41 @@ export type ProjectRoomTaskChangedEvent = ProjectRoomEventOf<
83
83
  }
84
84
  >;
85
85
 
86
+ /** A read-marker update broadcast on the acting user's channel. */
87
+ export type ProjectRoomReadChangedEvent = ProjectRoomEventOf<
88
+ "read.changed",
89
+ { projectId: string; roomId: string; lastReadAt: string }
90
+ >;
91
+
92
+ /** A membership mutation that may change a user's room subscription set. */
93
+ export type ProjectRoomMembershipAffectedEvent = ProjectRoomEventOf<
94
+ "membership.affected",
95
+ { change: "added" | "removed" | "role_changed"; projectId: string; roomId: string }
96
+ >;
97
+
86
98
  /** All identified business events retained by the realtime broker. */
87
99
  export type ProjectRoomBusinessEvent =
88
100
  | ProjectRoomMessageCreatedEvent
89
101
  | ProjectRoomRosterChangedEvent
90
102
  | ProjectRoomMembershipChangedEvent
91
- | ProjectRoomTaskChangedEvent;
103
+ | ProjectRoomTaskChangedEvent
104
+ | ProjectRoomReadChangedEvent
105
+ | ProjectRoomMembershipAffectedEvent;
92
106
 
93
107
  /** A business event before the broker assigns its process-local ID. */
94
108
  export type ProjectRoomBusinessEventDraft =
95
109
  | Omit<ProjectRoomMessageCreatedEvent, "id">
96
110
  | Omit<ProjectRoomRosterChangedEvent, "id">
97
111
  | Omit<ProjectRoomMembershipChangedEvent, "id">
98
- | Omit<ProjectRoomTaskChangedEvent, "id">;
112
+ | Omit<ProjectRoomTaskChangedEvent, "id">
113
+ | Omit<ProjectRoomReadChangedEvent, "id">
114
+ | Omit<ProjectRoomMembershipAffectedEvent, "id">;
99
115
 
100
116
  /** A connection control event; control events are never replayed. */
101
117
  export type ProjectRoomControlEvent =
102
118
  | { type: "ready"; data: { epoch: string; headEventId: string | null } }
103
119
  | { type: "resync"; data: { reason: "SERVER_RESTART" | "CURSOR_EXPIRED" | "SLOW_CONSUMER" } }
104
- | { type: "access.revoked"; data: { reason: "PROJECT_ACCESS_REVOKED" | "TOKEN_EXPIRED" } };
120
+ | { type: "access.revoked"; data: { reason: "PROJECT_ACCESS_REVOKED" | "TOKEN_EXPIRED" | "CONNECTION_SUPERSEDED" } };
105
121
 
106
122
  /** The authenticated identity used by Project Room realtime access checks. */
107
123
  export interface ProjectRoomRealtimeActor {
@@ -194,6 +210,15 @@ export function isProjectRoomEventId(value: unknown): value is string {
194
210
  return parseProjectRoomEventId(value) !== undefined;
195
211
  }
196
212
 
213
+ /** projectId sentinel marking a per-user event channel inside the room broker. */
214
+ export const PROJECT_ROOM_USER_CHANNEL_PROJECT = "__user_channel__";
215
+
216
+ /** Builds the per-user broker scope used for membership and read broadcasts. */
217
+ export function projectRoomUserEventScope(tenantId: string, userId: string): ProjectRoomEventScope {
218
+ if (!tenantId || !userId) throw new TypeError("Project Room user scope requires non-empty identifiers");
219
+ return { tenantId, roomId: `__user__:${userId}`, projectId: PROJECT_ROOM_USER_CHANNEL_PROJECT };
220
+ }
221
+
197
222
  function isoDate(value: unknown): string | undefined {
198
223
  if (typeof value !== "object" || value === null) return undefined;
199
224
  try {
@@ -0,0 +1,16 @@
1
+ import { projectRoomUserEventScope, parseProjectRoomEventId } from "../ProjectRoomRealtimeProtocol";
2
+
3
+ describe("projectRoomUserEventScope", () => {
4
+ it("builds a stable per-user scope that cannot collide with room scopes", () => {
5
+ expect(projectRoomUserEventScope("t1", "u1")).toEqual({
6
+ tenantId: "t1", roomId: "__user__:u1", projectId: "__user_channel__",
7
+ });
8
+ expect(projectRoomUserEventScope("t1", "u1")).toEqual(projectRoomUserEventScope("t1", "u1"));
9
+ expect(projectRoomUserEventScope("t1", "u2")).not.toEqual(projectRoomUserEventScope("t1", "u1"));
10
+ });
11
+ it("user scope ids are valid broker event id candidates", () => {
12
+ const scope = projectRoomUserEventScope("t1", "u1");
13
+ expect(typeof scope.roomId).toBe("string");
14
+ expect(parseProjectRoomEventId(`${"0a1b2c3d-4e5f-4a6b-8c9d-0e1f2a3b4c5d"}:1`)?.sequence).toBe(1);
15
+ });
16
+ });
@@ -264,6 +264,10 @@ class FakeProjectRoomMessageStore implements ProjectRoomMessageStore {
264
264
  const [tenantId, id] = args;
265
265
  return tenantId === "tenant-1" && id === "message-1" ? message : null;
266
266
  }
267
+
268
+ async countAfter(input: Parameters<ProjectRoomMessageStore["countAfter"]>[0]): Promise<number> {
269
+ return input.tenantId === "tenant-1" && input.roomId === "room-1" ? 1 : 0;
270
+ }
267
271
  }
268
272
 
269
273
  describe("ProjectRoomStore", () => {
package/src/index.ts CHANGED
@@ -56,6 +56,7 @@ export * from "./ProjectRoomStoreProtocol";
56
56
  export * from "./ProjectMembershipStoreProtocol";
57
57
  export * from "./ProjectBotMembershipStoreProtocol";
58
58
  export * from "./ProjectRoomMessageStoreProtocol";
59
+ export * from "./ProjectRoomReadStateProtocol";
59
60
  export * from "./ExactDataSnapshot";
60
61
  export * from "./ProjectRoomRealtimeProtocol";
61
62