@memberjunction/server 5.43.0 → 5.44.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 (82) hide show
  1. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  2. package/dist/agentSessions/SessionManager.js +3 -0
  3. package/dist/agentSessions/SessionManager.js.map +1 -1
  4. package/dist/agentSessions/index.d.ts.map +1 -1
  5. package/dist/agentSessions/index.js +3 -1
  6. package/dist/agentSessions/index.js.map +1 -1
  7. package/dist/generated/generated.d.ts +1159 -51
  8. package/dist/generated/generated.d.ts.map +1 -1
  9. package/dist/generated/generated.js +35284 -29068
  10. package/dist/generated/generated.js.map +1 -1
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +9 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/resolvers/FileResolver.d.ts +74 -0
  16. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  17. package/dist/resolvers/FileResolver.js +211 -2
  18. package/dist/resolvers/FileResolver.js.map +1 -1
  19. package/dist/resolvers/QueryResolver.d.ts +6 -4
  20. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  21. package/dist/resolvers/QueryResolver.js +29 -14
  22. package/dist/resolvers/QueryResolver.js.map +1 -1
  23. package/dist/resolvers/RealtimeBridgeResolver.d.ts +8 -1
  24. package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
  25. package/dist/resolvers/RealtimeBridgeResolver.js +28 -4
  26. package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
  27. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +81 -2
  28. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  29. package/dist/resolvers/RealtimeClientSessionResolver.js +293 -17
  30. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  31. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  32. package/dist/resolvers/RemoteBrowserActionResolver.js +17 -2
  33. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  34. package/dist/resolvers/RunAIAgentResolver.d.ts +12 -3
  35. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  36. package/dist/resolvers/RunAIAgentResolver.js +37 -15
  37. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  38. package/dist/resolvers/meetingRecordingRegistration.d.ts +124 -0
  39. package/dist/resolvers/meetingRecordingRegistration.d.ts.map +1 -0
  40. package/dist/resolvers/meetingRecordingRegistration.js +311 -0
  41. package/dist/resolvers/meetingRecordingRegistration.js.map +1 -0
  42. package/dist/resolvers/peaksSidecar.d.ts +30 -0
  43. package/dist/resolvers/peaksSidecar.d.ts.map +1 -0
  44. package/dist/resolvers/peaksSidecar.js +51 -0
  45. package/dist/resolvers/peaksSidecar.js.map +1 -0
  46. package/dist/rest/MediaAccessKeys.d.ts +68 -0
  47. package/dist/rest/MediaAccessKeys.d.ts.map +1 -0
  48. package/dist/rest/MediaAccessKeys.js +96 -0
  49. package/dist/rest/MediaAccessKeys.js.map +1 -0
  50. package/dist/rest/MediaStreamHandler.d.ts +26 -0
  51. package/dist/rest/MediaStreamHandler.d.ts.map +1 -0
  52. package/dist/rest/MediaStreamHandler.js +192 -0
  53. package/dist/rest/MediaStreamHandler.js.map +1 -0
  54. package/dist/rest/mediaRange.d.ts +41 -0
  55. package/dist/rest/mediaRange.d.ts.map +1 -0
  56. package/dist/rest/mediaRange.js +60 -0
  57. package/dist/rest/mediaRange.js.map +1 -0
  58. package/package.json +83 -82
  59. package/src/__tests__/FileResolverPeaks.test.ts +64 -0
  60. package/src/__tests__/MediaAccessKeys.test.ts +68 -0
  61. package/src/__tests__/MediaStreamHandler.test.ts +91 -0
  62. package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -1
  63. package/src/__tests__/RealtimeClientSessionResolver.test.ts +69 -0
  64. package/src/__tests__/RealtimeCoAgentAppAwareness.integration.test.ts +191 -0
  65. package/src/__tests__/RemoteBrowserSnapshot.test.ts +123 -0
  66. package/src/__tests__/SessionManager.test.ts +2 -0
  67. package/src/__tests__/meetingRecordingRegistration.test.ts +248 -0
  68. package/src/agentSessions/SessionManager.ts +3 -0
  69. package/src/agentSessions/index.ts +3 -1
  70. package/src/generated/generated.ts +17839 -13567
  71. package/src/index.ts +10 -0
  72. package/src/resolvers/FileResolver.ts +199 -1
  73. package/src/resolvers/QueryResolver.ts +33 -19
  74. package/src/resolvers/RealtimeBridgeResolver.ts +36 -4
  75. package/src/resolvers/RealtimeClientSessionResolver.ts +326 -9
  76. package/src/resolvers/RemoteBrowserActionResolver.ts +18 -2
  77. package/src/resolvers/RunAIAgentResolver.ts +44 -11
  78. package/src/resolvers/meetingRecordingRegistration.ts +432 -0
  79. package/src/resolvers/peaksSidecar.ts +52 -0
  80. package/src/rest/MediaAccessKeys.ts +121 -0
  81. package/src/rest/MediaStreamHandler.ts +220 -0
  82. package/src/rest/mediaRange.ts +76 -0
@@ -0,0 +1,311 @@
1
+ /**
2
+ * @fileoverview Meeting-Room recording registration — the server-side path that closes the LiveKit
3
+ * meeting-room recording loop. After a room's composite egress completes, this module:
4
+ *
5
+ * 1. Resolves the **Meeting-Room Conversation** for the room (prefer by `EgressID`, fall back to the
6
+ * room name → `ExternalID` + `Type='Meeting Room'`, else create one via the sink's pattern).
7
+ * 2. Resolves the **meeting-recording storage account** — a configured `MJStorage` account that points
8
+ * at the same sink LiveKit egress wrote the MP4 to, so MJ can read/stream it.
9
+ * 3. Creates the `MJ: Files` row for the recording (v1 points DIRECTLY at the egress output — no byte
10
+ * copy — so playback streams straight from the sink). An OPTIONAL copy-to-canonical path (OFF by
11
+ * default, behind config) reads the bytes and re-uploads into a separate canonical provider.
12
+ * 4. Stamps `Conversation.RecordingFileID` (+ `EgressID`) and saves.
13
+ *
14
+ * Lives in `@memberjunction/server` (the one package that depends on the egress service, MJStorage, and
15
+ * core-entities). It is invoked from `RealtimeBridgeResolver.StopLiveKitRecording`. Everything is
16
+ * best-effort: any failure returns a non-throwing failure result and logs — a recording that fails to
17
+ * register never crashes the stop-recording mutation.
18
+ *
19
+ * ## Required configuration
20
+ *
21
+ * The egress sink and the MJStorage account must point at the **same** bucket/container so MJ can read the
22
+ * file LiveKit wrote. Configure the **provider** whose accounts target that sink via either:
23
+ *
24
+ * - env: `MJ_MEETING_RECORDING_STORAGE_PROVIDER=<MJ: File Storage Providers ID>`
25
+ * - config (`mj.config.cjs`): `meetingRecordingStorageProviderID: '<provider id>'`
26
+ *
27
+ * When unset, registration returns a clear, non-throwing failure explaining the config is required (the
28
+ * recording still stopped; it just isn't registered as a `MJ: Files` row).
29
+ *
30
+ * ### Optional copy-to-canonical ("copy into Box")
31
+ *
32
+ * To copy the MP4 OUT of the egress sink into a separate canonical provider (e.g. Box) instead of pointing
33
+ * the Files row at the sink, set a DIFFERENT provider via:
34
+ *
35
+ * - env: `MJ_MEETING_RECORDING_CANONICAL_STORAGE_PROVIDER=<provider id>`
36
+ * - config: `meetingRecordingCanonicalStorageProviderID: '<provider id>'`
37
+ *
38
+ * When set and different from the sink provider, the bytes are read from the sink and uploaded into the
39
+ * canonical provider, and the Files row points there. OFF by default.
40
+ *
41
+ * @module @memberjunction/server
42
+ * @author MemberJunction.com
43
+ */
44
+ import { RunView, LogError, LogStatus } from '@memberjunction/core';
45
+ import { FileStorageEngine } from '@memberjunction/storage';
46
+ const CONVERSATION_ENTITY = 'MJ: Conversations';
47
+ const FILES_ENTITY = 'MJ: Files';
48
+ const MEETING_ROOM_CONVERSATION_TYPE = 'Meeting Room';
49
+ /**
50
+ * Registers a completed meeting recording: resolves the Meeting-Room Conversation, creates the `MJ: Files`
51
+ * row for the egress MP4, and stamps `Conversation.RecordingFileID` (+ `EgressID`). Best-effort — never
52
+ * throws; returns a failure result with a clear message instead.
53
+ *
54
+ * @param egress The completed egress result (must carry an `OutputLocation`).
55
+ * @param contextUser The authenticated user (for DB writes + credential access).
56
+ * @param provider The metadata provider servicing this request.
57
+ * @param config Optional config overrides (otherwise read from env).
58
+ */
59
+ export async function registerMeetingRecordingFile(egress, contextUser, provider, config) {
60
+ try {
61
+ if (!egress.OutputLocation) {
62
+ return {
63
+ Success: false,
64
+ ErrorMessage: 'The recording has no output yet (egress still in progress) — nothing to register. ' + 'Register only after the egress has stopped/completed.',
65
+ };
66
+ }
67
+ const sinkProviderID = config?.sinkStorageProviderID ?? process.env.MJ_MEETING_RECORDING_STORAGE_PROVIDER;
68
+ if (!sinkProviderID) {
69
+ return {
70
+ Success: false,
71
+ ErrorMessage: 'No meeting-recording storage provider is configured. Set MJ_MEETING_RECORDING_STORAGE_PROVIDER ' +
72
+ '(or meetingRecordingStorageProviderID in mj.config.cjs) to the MJ: File Storage Providers id ' +
73
+ 'whose accounts target the same sink LiveKit egress writes to.',
74
+ };
75
+ }
76
+ await FileStorageEngine.Instance.Config(false, contextUser, provider);
77
+ const sinkAccountID = resolveStorageAccountForProvider(sinkProviderID);
78
+ if (!sinkAccountID) {
79
+ return {
80
+ Success: false,
81
+ ErrorMessage: `No active MJStorage account is linked to the configured meeting-recording provider '${sinkProviderID}'. ` +
82
+ 'Create a File Storage Account for that provider pointing at the egress sink bucket/container.',
83
+ };
84
+ }
85
+ const conversationID = await resolveMeetingConversation(egress, contextUser, provider);
86
+ if (!conversationID) {
87
+ return { Success: false, ErrorMessage: 'Could not resolve or create the Meeting-Room Conversation for the recording.' };
88
+ }
89
+ // v1: point the Files row directly at the egress output in the sink account (no byte copy). The
90
+ // OPTIONAL copy-to-canonical path overrides both the provider+key when a differing canonical
91
+ // provider is configured.
92
+ const fileLocation = await resolveFileLocation(egress, sinkProviderID, sinkAccountID, contextUser, config);
93
+ if (!fileLocation.Success) {
94
+ return { Success: false, ErrorMessage: fileLocation.ErrorMessage, ConversationID: conversationID };
95
+ }
96
+ const fileID = await createRecordingFileRow(egress, fileLocation.ProviderID, fileLocation.ProviderKey, contextUser, provider);
97
+ if (!fileID) {
98
+ return { Success: false, ErrorMessage: 'Failed to create the MJ: Files row for the recording.', ConversationID: conversationID };
99
+ }
100
+ const stamped = await stampConversationRecording(conversationID, fileID, egress.EgressID, contextUser, provider);
101
+ if (!stamped) {
102
+ return {
103
+ Success: false,
104
+ ErrorMessage: 'Created the recording file but failed to stamp it onto the Conversation.',
105
+ ConversationID: conversationID,
106
+ RecordingFileID: fileID,
107
+ };
108
+ }
109
+ LogStatus(`registerMeetingRecordingFile: registered recording ${fileID} for room '${egress.RoomName}' on conversation ${conversationID}.`);
110
+ return { Success: true, RecordingFileID: fileID, ConversationID: conversationID };
111
+ }
112
+ catch (error) {
113
+ const msg = error instanceof Error ? error.message : String(error);
114
+ LogError(`registerMeetingRecordingFile failed: ${msg}`);
115
+ return { Success: false, ErrorMessage: msg };
116
+ }
117
+ }
118
+ /**
119
+ * Best-effort correlation when a recording STARTS: stamps `Conversation.EgressID` onto the room's
120
+ * Meeting-Room Conversation (if it already exists) so a live recording is tracked against the room. If the
121
+ * conversation doesn't exist yet, this resolves to `false` silently — the stop-flow will resolve/create it.
122
+ * Never throws.
123
+ *
124
+ * @param roomName The LiveKit room name (= the Conversation's `ExternalID`).
125
+ * @param egressID The egress id that just started.
126
+ * @param contextUser The authenticated user.
127
+ * @param provider The metadata provider.
128
+ * @returns `true` when an existing conversation was stamped, else `false`.
129
+ */
130
+ export async function correlateRecordingStart(roomName, egressID, contextUser, provider) {
131
+ try {
132
+ const conversationID = await findConversationByRoomName(roomName, contextUser, provider);
133
+ if (!conversationID) {
134
+ return false; // not created yet — stop-flow resolves/creates it
135
+ }
136
+ const conversation = await provider.GetEntityObject(CONVERSATION_ENTITY, contextUser);
137
+ if (!(await conversation.Load(conversationID))) {
138
+ return false;
139
+ }
140
+ conversation.EgressID = egressID;
141
+ if (await conversation.Save()) {
142
+ return true;
143
+ }
144
+ LogError(`correlateRecordingStart: failed to stamp EgressID: ${conversation.LatestResult?.CompleteMessage ?? 'unknown error'}`);
145
+ return false;
146
+ }
147
+ catch (error) {
148
+ // Never fail the recording start on correlation.
149
+ LogError(`correlateRecordingStart (best-effort) failed: ${error instanceof Error ? error.message : String(error)}`);
150
+ return false;
151
+ }
152
+ }
153
+ /**
154
+ * Resolves the Meeting-Room Conversation for a completed recording: prefer matching by `EgressID` (set on
155
+ * start), fall back to the room name (`ExternalID` + `Type`), else create one following the transcript
156
+ * sink's pattern (scoped `Application` so it stays out of the normal chat list).
157
+ */
158
+ export async function resolveMeetingConversation(egress, contextUser, provider) {
159
+ const byEgress = await findConversationByEgressID(egress.EgressID, contextUser, provider);
160
+ if (byEgress) {
161
+ return byEgress;
162
+ }
163
+ const byRoom = await findConversationByRoomName(egress.RoomName, contextUser, provider);
164
+ if (byRoom) {
165
+ return byRoom;
166
+ }
167
+ return createMeetingConversation(egress.RoomName, contextUser, provider);
168
+ }
169
+ /** Finds a non-archived Meeting-Room Conversation by its `EgressID`. */
170
+ async function findConversationByEgressID(egressID, contextUser, provider) {
171
+ if (!egressID) {
172
+ return null;
173
+ }
174
+ const rv = RunView.FromMetadataProvider(provider);
175
+ const found = await rv.RunView({
176
+ EntityName: CONVERSATION_ENTITY,
177
+ ExtraFilter: `EgressID='${escapeSql(egressID)}' AND Type='${escapeSql(MEETING_ROOM_CONVERSATION_TYPE)}' AND (IsArchived IS NULL OR IsArchived=0)`,
178
+ Fields: ['ID'],
179
+ OrderBy: '__mj_CreatedAt DESC',
180
+ MaxRows: 1,
181
+ ResultType: 'simple',
182
+ }, contextUser);
183
+ return found.Success && found.Results.length > 0 ? found.Results[0].ID : null;
184
+ }
185
+ /** Finds a non-archived Meeting-Room Conversation by room name (`ExternalID` + `Type`). */
186
+ async function findConversationByRoomName(roomName, contextUser, provider) {
187
+ if (!roomName) {
188
+ return null;
189
+ }
190
+ const rv = RunView.FromMetadataProvider(provider);
191
+ const found = await rv.RunView({
192
+ EntityName: CONVERSATION_ENTITY,
193
+ ExtraFilter: `ExternalID='${escapeSql(roomName)}' AND Type='${escapeSql(MEETING_ROOM_CONVERSATION_TYPE)}' AND (IsArchived IS NULL OR IsArchived=0)`,
194
+ Fields: ['ID'],
195
+ OrderBy: '__mj_CreatedAt DESC',
196
+ MaxRows: 1,
197
+ ResultType: 'simple',
198
+ }, contextUser);
199
+ return found.Success && found.Results.length > 0 ? found.Results[0].ID : null;
200
+ }
201
+ /** Creates a Meeting-Room Conversation for a room (scoped `Application`, keyed by room name) — mirrors the transcript sink. */
202
+ async function createMeetingConversation(roomName, contextUser, provider) {
203
+ const conversation = await provider.GetEntityObject(CONVERSATION_ENTITY, contextUser);
204
+ conversation.NewRecord();
205
+ conversation.UserID = contextUser.ID;
206
+ conversation.Name = `Meeting Room ${roomName}`;
207
+ conversation.Type = MEETING_ROOM_CONVERSATION_TYPE;
208
+ conversation.ExternalID = roomName;
209
+ conversation.ApplicationScope = 'Application';
210
+ if (await conversation.Save()) {
211
+ return conversation.ID;
212
+ }
213
+ LogError(`registerMeetingRecordingFile: failed to create Meeting-Room conversation for room '${roomName}': ${conversation.LatestResult?.CompleteMessage ?? 'unknown error'}`);
214
+ return null;
215
+ }
216
+ /**
217
+ * Resolves the `(ProviderID, ProviderKey)` the Files row should point at. v1 default: the egress output in
218
+ * the sink provider (no copy). When a DIFFERING canonical provider is configured, copies the bytes into it
219
+ * and points there instead.
220
+ */
221
+ async function resolveFileLocation(egress, sinkProviderID, sinkAccountID, contextUser, config) {
222
+ const outputLocation = egress.OutputLocation;
223
+ const canonicalProviderID = config?.canonicalStorageProviderID ?? process.env.MJ_MEETING_RECORDING_CANONICAL_STORAGE_PROVIDER;
224
+ // Default (v1): point directly at the sink output — playback streams straight from it, no byte copy.
225
+ if (!canonicalProviderID || canonicalProviderID === sinkProviderID) {
226
+ return { Success: true, ProviderID: sinkProviderID, ProviderKey: outputLocation };
227
+ }
228
+ // OPTIONAL "copy into Box": a separate canonical provider IS configured and differs from the sink —
229
+ // read the bytes from the sink and upload them into the canonical provider, then point there.
230
+ const canonicalKey = await copyEgressOutputToCanonical(outputLocation, sinkAccountID, canonicalProviderID, contextUser);
231
+ if (!canonicalKey.Success) {
232
+ return { Success: false, ErrorMessage: canonicalKey.ErrorMessage, ProviderID: '', ProviderKey: '' };
233
+ }
234
+ return { Success: true, ProviderID: canonicalProviderID, ProviderKey: canonicalKey.ProviderKey };
235
+ }
236
+ /**
237
+ * OPTIONAL — copies the egress MP4 OUT of the sink account into a configured canonical provider (the "copy
238
+ * into Box" option). Reads the bytes via the sink driver's `GetObject({ fullPath })` and re-uploads them
239
+ * into the canonical provider via `FileStorageEngine.UploadFile`. Returns the canonical `ProviderKey`.
240
+ */
241
+ export async function copyEgressOutputToCanonical(outputLocation, sinkAccountID, canonicalProviderID, contextUser) {
242
+ const engine = FileStorageEngine.Instance;
243
+ const canonicalAccountID = resolveStorageAccountForProvider(canonicalProviderID);
244
+ if (!canonicalAccountID) {
245
+ return {
246
+ Success: false,
247
+ ErrorMessage: `No active MJStorage account is linked to the configured canonical provider '${canonicalProviderID}'.`,
248
+ ProviderKey: '',
249
+ };
250
+ }
251
+ const sinkDriver = await engine.GetDriver(sinkAccountID, contextUser);
252
+ const bytes = await sinkDriver.GetObject({ fullPath: outputLocation });
253
+ const fileName = outputLocation.split('/').pop() || `recording-${Date.now()}.mp4`;
254
+ const uploaded = await engine.UploadFile({
255
+ content: bytes,
256
+ fileName,
257
+ mimeType: 'video/mp4',
258
+ contextUser,
259
+ storageAccountId: canonicalAccountID,
260
+ pathPrefix: `meeting-recordings/${new Date().toISOString().slice(0, 10)}`,
261
+ });
262
+ return { Success: true, ProviderKey: uploaded.StoragePath };
263
+ }
264
+ /** Creates the `MJ: Files` row pointing at the recording (no byte copy in the default v1 path). */
265
+ async function createRecordingFileRow(egress, providerID, providerKey, contextUser, provider) {
266
+ const file = await provider.GetEntityObject(FILES_ENTITY, contextUser);
267
+ file.NewRecord();
268
+ file.Name = `Meeting Recording — ${egress.RoomName} — ${new Date().toISOString().slice(0, 10)}`;
269
+ file.ContentType = 'video/mp4';
270
+ file.ProviderID = providerID;
271
+ file.ProviderKey = providerKey;
272
+ file.Status = 'Uploaded';
273
+ // NOTE: `MJ: Files` has no size column, so `egress.OutputSizeBytes` is not persisted here (it is logged
274
+ // and used by the player's range-streaming on demand). If a size column is added, set it here.
275
+ if (await file.Save()) {
276
+ return file.ID;
277
+ }
278
+ LogError(`registerMeetingRecordingFile: failed to save MJ: Files row: ${file.LatestResult?.CompleteMessage ?? 'unknown error'}`);
279
+ return null;
280
+ }
281
+ /** Loads the Conversation and stamps `RecordingFileID` (+ `EgressID` if unset), then saves. */
282
+ async function stampConversationRecording(conversationID, fileID, egressID, contextUser, provider) {
283
+ const conversation = await provider.GetEntityObject(CONVERSATION_ENTITY, contextUser);
284
+ if (!(await conversation.Load(conversationID))) {
285
+ LogError(`registerMeetingRecordingFile: could not load conversation ${conversationID} to stamp the recording.`);
286
+ return false;
287
+ }
288
+ conversation.RecordingFileID = fileID;
289
+ if (!conversation.EgressID) {
290
+ conversation.EgressID = egressID;
291
+ }
292
+ if (await conversation.Save()) {
293
+ return true;
294
+ }
295
+ LogError(`registerMeetingRecordingFile: failed to stamp the Conversation: ${conversation.LatestResult?.CompleteMessage ?? 'unknown error'}`);
296
+ return false;
297
+ }
298
+ /**
299
+ * Resolves an MJStorage account linked to a provider id (the first one). `IsActive` lives on the
300
+ * *provider*, not the account, so provider-level inactivity is handled upstream by the engine's driver
301
+ * cache; here we just pick the provider's account.
302
+ */
303
+ function resolveStorageAccountForProvider(providerID) {
304
+ const accounts = FileStorageEngine.Instance.GetAccountsByProviderID(providerID);
305
+ return accounts.length > 0 ? accounts[0].ID : null;
306
+ }
307
+ /** Escapes single quotes for safe embedding in an `ExtraFilter` literal. */
308
+ function escapeSql(value) {
309
+ return value.replace(/'/g, "''");
310
+ }
311
+ //# sourceMappingURL=meetingRecordingRegistration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meetingRecordingRegistration.js","sourceRoot":"","sources":["../../src/resolvers/meetingRecordingRegistration.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @fileoverview Pure helpers for the `peaks.json` waveform sidecar that rides alongside a stored
3
+ * recording. Kept free of type-graphql / resolver / DB dependencies so they can be unit-tested in
4
+ * isolation (the resolver class itself can't be imported under vitest without standing up the full
5
+ * decorated schema). {@link FileResolver.tryReadPeaksSidecar} composes these with a storage driver.
6
+ *
7
+ * @module @memberjunction/server/resolvers/peaksSidecar
8
+ */
9
+ /**
10
+ * Derives the `peaks.json` sidecar path that sits in the SAME folder as a file's `ProviderKey`:
11
+ * the final path segment of `providerKey` is replaced with `peaks.json`. Returns `null` when
12
+ * `providerKey` is empty (no folder to derive from).
13
+ *
14
+ * @example deriveSidecarPath('realtime-recordings/sess-1/recording.wav') === 'realtime-recordings/sess-1/peaks.json'
15
+ * @example deriveSidecarPath('recording.wav') === 'peaks.json'
16
+ */
17
+ export declare function deriveSidecarPath(providerKey: string | null | undefined): string | null;
18
+ /** Generous ceiling on the number of peaks returned — matches the capture-side cap. */
19
+ export declare const MAX_PEAKS = 4096;
20
+ /**
21
+ * Parses + sanitizes a `peaks.json` payload (UTF-8 bytes). The content must be a JSON array of
22
+ * finite numbers; each value is clamped to `[0, 1]` and the array is capped at {@link MAX_PEAKS}
23
+ * entries so a malformed/hostile sidecar can't bloat the response. Returns `undefined` for anything
24
+ * that isn't a non-empty array of finite numbers, and NEVER throws (a parse error → `undefined`).
25
+ *
26
+ * @param bytes The raw sidecar bytes (e.g. from a storage `GetObject`).
27
+ * @returns Sanitized `0..1` peaks, or `undefined`.
28
+ */
29
+ export declare function parsePeaksSidecar(bytes: Buffer): number[] | undefined;
30
+ //# sourceMappingURL=peaksSidecar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"peaksSidecar.d.ts","sourceRoot":"","sources":["../../src/resolvers/peaksSidecar.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAMvF;AAED,uFAAuF;AACvF,eAAO,MAAM,SAAS,OAAO,CAAC;AAE9B;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAcrE"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @fileoverview Pure helpers for the `peaks.json` waveform sidecar that rides alongside a stored
3
+ * recording. Kept free of type-graphql / resolver / DB dependencies so they can be unit-tested in
4
+ * isolation (the resolver class itself can't be imported under vitest without standing up the full
5
+ * decorated schema). {@link FileResolver.tryReadPeaksSidecar} composes these with a storage driver.
6
+ *
7
+ * @module @memberjunction/server/resolvers/peaksSidecar
8
+ */
9
+ /**
10
+ * Derives the `peaks.json` sidecar path that sits in the SAME folder as a file's `ProviderKey`:
11
+ * the final path segment of `providerKey` is replaced with `peaks.json`. Returns `null` when
12
+ * `providerKey` is empty (no folder to derive from).
13
+ *
14
+ * @example deriveSidecarPath('realtime-recordings/sess-1/recording.wav') === 'realtime-recordings/sess-1/peaks.json'
15
+ * @example deriveSidecarPath('recording.wav') === 'peaks.json'
16
+ */
17
+ export function deriveSidecarPath(providerKey) {
18
+ if (!providerKey) {
19
+ return null;
20
+ }
21
+ const lastSlash = providerKey.lastIndexOf('/');
22
+ return lastSlash >= 0 ? `${providerKey.slice(0, lastSlash)}/peaks.json` : 'peaks.json';
23
+ }
24
+ /** Generous ceiling on the number of peaks returned — matches the capture-side cap. */
25
+ export const MAX_PEAKS = 4096;
26
+ /**
27
+ * Parses + sanitizes a `peaks.json` payload (UTF-8 bytes). The content must be a JSON array of
28
+ * finite numbers; each value is clamped to `[0, 1]` and the array is capped at {@link MAX_PEAKS}
29
+ * entries so a malformed/hostile sidecar can't bloat the response. Returns `undefined` for anything
30
+ * that isn't a non-empty array of finite numbers, and NEVER throws (a parse error → `undefined`).
31
+ *
32
+ * @param bytes The raw sidecar bytes (e.g. from a storage `GetObject`).
33
+ * @returns Sanitized `0..1` peaks, or `undefined`.
34
+ */
35
+ export function parsePeaksSidecar(bytes) {
36
+ try {
37
+ const parsed = JSON.parse(bytes.toString('utf8'));
38
+ if (!Array.isArray(parsed) || parsed.length === 0) {
39
+ return undefined;
40
+ }
41
+ const cleaned = parsed
42
+ .slice(0, MAX_PEAKS)
43
+ .filter((v) => typeof v === 'number' && Number.isFinite(v))
44
+ .map((v) => (v < 0 ? 0 : v > 1 ? 1 : v));
45
+ return cleaned.length > 0 ? cleaned : undefined;
46
+ }
47
+ catch {
48
+ return undefined;
49
+ }
50
+ }
51
+ //# sourceMappingURL=peaksSidecar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"peaksSidecar.js","sourceRoot":"","sources":["../../src/resolvers/peaksSidecar.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,WAAsC;IACpE,IAAI,CAAC,WAAW,EAAE,CAAC;QACf,OAAO,IAAI,CAAC;IAChB,CAAC;IACD,MAAM,SAAS,GAAG,WAAW,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAC/C,OAAO,SAAS,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC,YAAY,CAAC;AAC3F,CAAC;AAED,uFAAuF;AACvF,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,CAAC;AAE9B;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC3C,IAAI,CAAC;QACD,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;QAC3D,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAChD,OAAO,SAAS,CAAC;QACrB,CAAC;QACD,MAAM,OAAO,GAAG,MAAM;aACjB,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC;aACnB,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;aACvE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7C,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,SAAS,CAAC;IACrB,CAAC;AACL,CAAC"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @fileoverview Short-lived HS256 token manager for authenticated media streaming.
3
+ *
4
+ * Media-access tokens are minted by the `CreateMediaAccessToken` GraphQL mutation
5
+ * (after a per-user permission check on the `MJ: Files` row) and verified by the
6
+ * `/media/:fileId` streaming route. The token IS the capability: access is checked
7
+ * once at mint time, and the route trusts a valid, unexpired, file-matching token.
8
+ *
9
+ * This is intentionally a SEPARATE, self-contained mechanism from the magic-link
10
+ * RS256 manager: media streaming must work on EVERY deployment, whereas the
11
+ * magic-link keypair is only initialized when `magicLink.enabled` is configured.
12
+ * We sign HS256 with the deployment's symmetric secret (`MJ_BASE_ENCRYPTION_KEY`),
13
+ * falling back to an ephemeral per-process secret (with a warning) when unset —
14
+ * the same degradation MagicLinkKeyManager uses for its ephemeral keypair.
15
+ *
16
+ * @module @memberjunction/server/rest/MediaAccessKeys
17
+ */
18
+ import { BaseSingleton } from '@memberjunction/global';
19
+ /** The token's `typ` claim — the route rejects any token whose `typ` differs. */
20
+ export declare const MEDIA_ACCESS_TOKEN_TYPE = "media-access";
21
+ /** Default media-access token lifetime (hours). Long enough to cover a full playback session. */
22
+ export declare const MEDIA_ACCESS_DEFAULT_TTL_HOURS = 4;
23
+ /** The claims carried by a media-access token. */
24
+ export interface MediaAccessTokenClaims {
25
+ /** The `MJ: Files` id this token grants streaming access to. */
26
+ fileId: string;
27
+ /** The id of the user the token was minted for (audit/forensics only — access was already checked). */
28
+ userId: string;
29
+ /** Discriminator — always {@link MEDIA_ACCESS_TOKEN_TYPE}. */
30
+ typ: typeof MEDIA_ACCESS_TOKEN_TYPE;
31
+ }
32
+ /** Outcome of verifying a media-access token. */
33
+ export type MediaTokenVerifyResult = {
34
+ Valid: true;
35
+ Claims: MediaAccessTokenClaims;
36
+ } | {
37
+ Valid: false;
38
+ };
39
+ /**
40
+ * Signs and verifies short-lived HS256 media-access tokens. Singleton so the
41
+ * signing secret is stable for the process lifetime. The secret is resolved
42
+ * lazily on first use from `MJ_BASE_ENCRYPTION_KEY`; if unset, an ephemeral
43
+ * 32-byte secret is generated (outstanding tokens die on restart — acceptable
44
+ * for a 4h-TTL streaming credential).
45
+ */
46
+ export declare class MediaAccessKeyManager extends BaseSingleton<MediaAccessKeyManager> {
47
+ private secret;
48
+ private _initialized;
49
+ constructor();
50
+ static get Instance(): MediaAccessKeyManager;
51
+ /** Resolves the signing secret once, preferring the deployment's symmetric key. */
52
+ private ensureSecret;
53
+ /**
54
+ * Mints a signed media-access token for a file + user, expiring after `ttlHours`.
55
+ *
56
+ * @returns `{ Token, ExpiresAt }` — the compact JWT and its absolute expiry.
57
+ */
58
+ Sign(fileId: string, userId: string, ttlHours?: number): {
59
+ Token: string;
60
+ ExpiresAt: Date;
61
+ };
62
+ /**
63
+ * Verifies a token's signature, expiry, and `typ`. Returns the typed claims on
64
+ * success. Never throws — an invalid/expired/malformed token returns `{ Valid: false }`.
65
+ */
66
+ Verify(token: string): MediaTokenVerifyResult;
67
+ }
68
+ //# sourceMappingURL=MediaAccessKeys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MediaAccessKeys.d.ts","sourceRoot":"","sources":["../../src/rest/MediaAccessKeys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAKvD,iFAAiF;AACjF,eAAO,MAAM,uBAAuB,iBAAiB,CAAC;AAEtD,iGAAiG;AACjG,eAAO,MAAM,8BAA8B,IAAI,CAAC;AAEhD,kDAAkD;AAClD,MAAM,WAAW,sBAAsB;IACrC,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;IACf,uGAAuG;IACvG,MAAM,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,GAAG,EAAE,OAAO,uBAAuB,CAAC;CACrC;AAED,iDAAiD;AACjD,MAAM,MAAM,sBAAsB,GAC9B;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,sBAAsB,CAAA;CAAE,GAC/C;IAAE,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AAErB;;;;;;GAMG;AACH,qBAAa,qBAAsB,SAAQ,aAAa,CAAC,qBAAqB,CAAC;IAC7E,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,YAAY,CAAS;;IAO7B,WAAkB,QAAQ,IAAI,qBAAqB,CAElD;IAED,mFAAmF;IACnF,OAAO,CAAC,YAAY;IAkBpB;;;;OAIG;IACI,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAuC,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,IAAI,CAAA;KAAE;IASlI;;;OAGG;IACI,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,sBAAsB;CAmBrD"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @fileoverview Short-lived HS256 token manager for authenticated media streaming.
3
+ *
4
+ * Media-access tokens are minted by the `CreateMediaAccessToken` GraphQL mutation
5
+ * (after a per-user permission check on the `MJ: Files` row) and verified by the
6
+ * `/media/:fileId` streaming route. The token IS the capability: access is checked
7
+ * once at mint time, and the route trusts a valid, unexpired, file-matching token.
8
+ *
9
+ * This is intentionally a SEPARATE, self-contained mechanism from the magic-link
10
+ * RS256 manager: media streaming must work on EVERY deployment, whereas the
11
+ * magic-link keypair is only initialized when `magicLink.enabled` is configured.
12
+ * We sign HS256 with the deployment's symmetric secret (`MJ_BASE_ENCRYPTION_KEY`),
13
+ * falling back to an ephemeral per-process secret (with a warning) when unset —
14
+ * the same degradation MagicLinkKeyManager uses for its ephemeral keypair.
15
+ *
16
+ * @module @memberjunction/server/rest/MediaAccessKeys
17
+ */
18
+ import { BaseSingleton } from '@memberjunction/global';
19
+ import { LogStatus } from '@memberjunction/core';
20
+ import { randomBytes } from 'node:crypto';
21
+ import jwt from 'jsonwebtoken';
22
+ /** The token's `typ` claim — the route rejects any token whose `typ` differs. */
23
+ export const MEDIA_ACCESS_TOKEN_TYPE = 'media-access';
24
+ /** Default media-access token lifetime (hours). Long enough to cover a full playback session. */
25
+ export const MEDIA_ACCESS_DEFAULT_TTL_HOURS = 4;
26
+ /**
27
+ * Signs and verifies short-lived HS256 media-access tokens. Singleton so the
28
+ * signing secret is stable for the process lifetime. The secret is resolved
29
+ * lazily on first use from `MJ_BASE_ENCRYPTION_KEY`; if unset, an ephemeral
30
+ * 32-byte secret is generated (outstanding tokens die on restart — acceptable
31
+ * for a 4h-TTL streaming credential).
32
+ */
33
+ export class MediaAccessKeyManager extends BaseSingleton {
34
+ // Public to satisfy BaseSingleton.getInstance's `this: new () => T` constraint.
35
+ constructor() {
36
+ super();
37
+ this.secret = '';
38
+ this._initialized = false;
39
+ }
40
+ static get Instance() {
41
+ return MediaAccessKeyManager.getInstance();
42
+ }
43
+ /** Resolves the signing secret once, preferring the deployment's symmetric key. */
44
+ ensureSecret() {
45
+ if (this._initialized) {
46
+ return;
47
+ }
48
+ const configured = process.env.MJ_BASE_ENCRYPTION_KEY?.trim();
49
+ if (configured) {
50
+ this.secret = configured;
51
+ }
52
+ else {
53
+ this.secret = randomBytes(32).toString('base64');
54
+ LogStatus('[MediaAccess] No MJ_BASE_ENCRYPTION_KEY configured — generated an EPHEMERAL HS256 secret for ' +
55
+ 'media-access tokens. Outstanding media URLs will be invalidated on restart. Set ' +
56
+ 'MJ_BASE_ENCRYPTION_KEY for stable signing.');
57
+ }
58
+ this._initialized = true;
59
+ }
60
+ /**
61
+ * Mints a signed media-access token for a file + user, expiring after `ttlHours`.
62
+ *
63
+ * @returns `{ Token, ExpiresAt }` — the compact JWT and its absolute expiry.
64
+ */
65
+ Sign(fileId, userId, ttlHours = MEDIA_ACCESS_DEFAULT_TTL_HOURS) {
66
+ this.ensureSecret();
67
+ const claims = { fileId, userId, typ: MEDIA_ACCESS_TOKEN_TYPE };
68
+ const expiresInSeconds = Math.floor(ttlHours * 3600);
69
+ const token = jwt.sign(claims, this.secret, { algorithm: 'HS256', expiresIn: expiresInSeconds });
70
+ const expiresAt = new Date(Date.now() + expiresInSeconds * 1000);
71
+ return { Token: token, ExpiresAt: expiresAt };
72
+ }
73
+ /**
74
+ * Verifies a token's signature, expiry, and `typ`. Returns the typed claims on
75
+ * success. Never throws — an invalid/expired/malformed token returns `{ Valid: false }`.
76
+ */
77
+ Verify(token) {
78
+ this.ensureSecret();
79
+ try {
80
+ const decoded = jwt.verify(token, this.secret, { algorithms: ['HS256'] });
81
+ if (typeof decoded === 'object' && decoded !== null &&
82
+ decoded.typ === MEDIA_ACCESS_TOKEN_TYPE &&
83
+ typeof decoded.fileId === 'string' &&
84
+ typeof decoded.userId === 'string') {
85
+ const d = decoded;
86
+ return { Valid: true, Claims: { fileId: d.fileId, userId: d.userId, typ: MEDIA_ACCESS_TOKEN_TYPE } };
87
+ }
88
+ return { Valid: false };
89
+ }
90
+ catch {
91
+ // Signature mismatch, expiry, or malformed token — all map to "not valid".
92
+ return { Valid: false };
93
+ }
94
+ }
95
+ }
96
+ //# sourceMappingURL=MediaAccessKeys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MediaAccessKeys.js","sourceRoot":"","sources":["../../src/rest/MediaAccessKeys.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @fileoverview Authenticated HTTP-Range media streaming route (`GET /media/:fileId`).
3
+ *
4
+ * Streams an `MJ: Files` object's bytes (optionally a byte range) to the browser so
5
+ * `<audio>`/`<video>` elements get native progressive playback + seek-before-download
6
+ * for large media — instead of the whole file being base64'd over GraphQL.
7
+ *
8
+ * Auth model: the request carries a short-lived signed token (`?token=`) minted by
9
+ * `CreateMediaAccessToken` AFTER a per-user permission check. The token IS the
10
+ * capability: this route re-verifies its signature/expiry/`typ` and that it matches
11
+ * the `:fileId`, then loads the file under a SYSTEM context (it only needs
12
+ * `ProviderKey`/`ProviderID` to locate the bytes — no row-level re-check, since
13
+ * access was already authorized at mint).
14
+ *
15
+ * Mount unauthenticated, BEFORE the unified auth middleware:
16
+ * `app.use('/media', cors<cors.CorsRequest>(), createMediaStreamRouter())`
17
+ *
18
+ * @module @memberjunction/server/rest/MediaStreamHandler
19
+ */
20
+ import { type Router } from 'express';
21
+ /**
22
+ * Builds the Express router exposing `GET /media/:fileId`. Stateless — verification
23
+ * and byte-source resolution happen per request.
24
+ */
25
+ export declare function createMediaStreamRouter(): Router;
26
+ //# sourceMappingURL=MediaStreamHandler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MediaStreamHandler.d.ts","sourceRoot":"","sources":["../../src/rest/MediaStreamHandler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAgB,EAAE,KAAK,MAAM,EAA+B,MAAM,SAAS,CAAC;AAiB5E;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAMhD"}