@openchambery/relay-server 1.19.7-beta.6 → 1.19.7-beta.8

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": "@openchambery/relay-server",
3
- "version": "1.19.7-beta.6",
3
+ "version": "1.19.7-beta.8",
4
4
  "description": "Self-hosted private relay server for OpenChamber",
5
5
  "private": false,
6
6
  "type": "module",
@@ -8,6 +8,14 @@ import { createTokenStore } from './store.js';
8
8
  const WINDOW_MS = 60_000;
9
9
  const STOP_DEADLINE_MS = 5_500;
10
10
 
11
+ const liveActivitySignMessage = (ts, tokens, event, contentState, dismissalDate, staleDate) => {
12
+ let message = `${ts}.${tokens.join(',')}.${event}.${contentState.status}.${contentState.eventVersion}.${contentState.updatedAt}.${contentState.endedAt ?? ''}.${dismissalDate ?? ''}.${staleDate ?? ''}`;
13
+ if (contentState.title !== undefined || contentState.workingCount !== undefined || contentState.items !== undefined) {
14
+ message += `.${contentState.title ?? ''}.${contentState.workingCount ?? ''}.${JSON.stringify(contentState.items ?? [])}`;
15
+ }
16
+ return message;
17
+ };
18
+
11
19
  const sendJson = (response, status, payload, method = 'GET') => {
12
20
  if (response.writableEnded) return;
13
21
  response.setHeader('cache-control', 'no-store');
@@ -161,7 +169,7 @@ export const createPushRelayHandler = (options = {}) => {
161
169
  const handleLiveActivity = async (parsed) => {
162
170
  const sorted = [...parsed.tokens].sort();
163
171
  const contentState = parsed.contentState;
164
- const message = `${parsed.ts}.${sorted.join(',')}.${parsed.event}.${contentState.status}.${contentState.eventVersion}.${contentState.updatedAt}.${contentState.endedAt ?? ''}.${parsed.dismissalDate ?? ''}.${parsed.staleDate ?? ''}`;
172
+ const message = liveActivitySignMessage(parsed.ts, sorted, parsed.event, contentState, parsed.dismissalDate, parsed.staleDate);
165
173
  const authError = authenticate(parsed.publicKeyJwk, message, parsed.sig, parsed.ts);
166
174
  if (authError) return { status: 401, body: { error: authError } };
167
175
  const serverId = deriveServerId(parsed.publicKeyJwk);
@@ -14,7 +14,18 @@ export const MAX_DATA_TOTAL_BYTES = 2048;
14
14
  export const LIVE_ACTIVITY_KIND = 'liveactivity';
15
15
  export const LIVE_ACTIVITY_EVENTS = new Set(['update', 'end']);
16
16
  export const LIVE_ACTIVITY_STATUSES = new Set(['working', 'tool', 'retry', 'input', 'permission', 'stale', 'complete', 'error']);
17
- export const LIVE_ACTIVITY_CONTENT_KEYS = new Set(['status', 'eventVersion', 'updatedAt', 'endedAt']);
17
+ export const LIVE_ACTIVITY_CONTENT_KEYS = new Set([
18
+ 'status',
19
+ 'eventVersion',
20
+ 'updatedAt',
21
+ 'endedAt',
22
+ 'title',
23
+ 'workingCount',
24
+ 'items',
25
+ ]);
26
+ const MAX_LIVE_ACTIVITY_ITEMS = 4;
27
+ const MAX_LIVE_ACTIVITY_ITEM_TITLE = 80;
28
+ const MAX_LIVE_ACTIVITY_ITEM_SESSION = 128;
18
29
 
19
30
  const bytes = (value) => Buffer.byteLength(value, 'utf8');
20
31
  const isSafeInt = (value) => typeof value === 'number' && Number.isSafeInteger(value);
@@ -71,6 +82,29 @@ const uniqueTokens = (tokens) => {
71
82
  return unique;
72
83
  };
73
84
 
85
+ const parseLiveActivityItems = (value) => {
86
+ if (value === undefined) return undefined;
87
+ if (!Array.isArray(value) || value.length > MAX_LIVE_ACTIVITY_ITEMS) return null;
88
+ const items = [];
89
+ for (const raw of value) {
90
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
91
+ const sessionID = typeof raw.sessionID === 'string'
92
+ ? raw.sessionID
93
+ : (typeof raw.sessionId === 'string' ? raw.sessionId : '');
94
+ if (!sessionID || sessionID.length > MAX_LIVE_ACTIVITY_ITEM_SESSION) return null;
95
+ if (typeof raw.status !== 'string' || !LIVE_ACTIVITY_STATUSES.has(raw.status)) return null;
96
+ if (!isFiniteNumber(raw.startedAt)) return null;
97
+ const title = typeof raw.title === 'string' ? raw.title.slice(0, MAX_LIVE_ACTIVITY_ITEM_TITLE) : '';
98
+ const item = { sessionID, title, status: raw.status, startedAt: raw.startedAt };
99
+ if (raw.endedAt !== undefined) {
100
+ if (!isFiniteNumber(raw.endedAt)) return null;
101
+ item.endedAt = raw.endedAt;
102
+ }
103
+ items.push(item);
104
+ }
105
+ return items;
106
+ };
107
+
74
108
  const parseContentState = (value, event) => {
75
109
  if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
76
110
  const keys = Object.keys(value);
@@ -79,8 +113,19 @@ const parseContentState = (value, event) => {
79
113
  if (!isSafeInt(value.eventVersion) || !isFiniteNumber(value.updatedAt)) return null;
80
114
  if (event === 'end' && !isFiniteNumber(value.endedAt)) return null;
81
115
  if (value.endedAt !== undefined && !isFiniteNumber(value.endedAt)) return null;
116
+ if (value.title !== undefined && (typeof value.title !== 'string' || value.title.length > MAX_LIVE_ACTIVITY_ITEM_TITLE)) {
117
+ return null;
118
+ }
119
+ if (value.workingCount !== undefined && (!isSafeInt(value.workingCount) || value.workingCount < 0)) {
120
+ return null;
121
+ }
122
+ const items = parseLiveActivityItems(value.items);
123
+ if (items === null) return null;
82
124
  const contentState = { status: value.status, eventVersion: value.eventVersion, updatedAt: value.updatedAt };
83
125
  if (value.endedAt !== undefined) contentState.endedAt = value.endedAt;
126
+ if (value.title !== undefined) contentState.title = value.title;
127
+ if (value.workingCount !== undefined) contentState.workingCount = value.workingCount;
128
+ if (items !== undefined) contentState.items = items;
84
129
  return contentState;
85
130
  };
86
131