@memberjunction/server 5.42.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.
- package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
- package/dist/agentSessions/SessionJanitor.js +5 -1
- package/dist/agentSessions/SessionJanitor.js.map +1 -1
- package/dist/agentSessions/SessionManager.d.ts +15 -2
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +53 -11
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/agentSessions/index.d.ts.map +1 -1
- package/dist/agentSessions/index.js +3 -1
- package/dist/agentSessions/index.js.map +1 -1
- package/dist/generated/generated.d.ts +1180 -51
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +35564 -29262
- package/dist/generated/generated.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts +20 -1
- package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts.map +1 -1
- package/dist/resolvers/ExecuteRemoteOperationResolver.js +61 -5
- package/dist/resolvers/ExecuteRemoteOperationResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +74 -0
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +211 -2
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +98 -29
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +6 -4
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +29 -14
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts +17 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +78 -7
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +81 -2
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +293 -17
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +17 -2
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +12 -3
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +37 -15
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunTemplateResolver.d.ts +8 -0
- package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTemplateResolver.js +8 -0
- package/dist/resolvers/RunTemplateResolver.js.map +1 -1
- package/dist/resolvers/meetingRecordingRegistration.d.ts +124 -0
- package/dist/resolvers/meetingRecordingRegistration.d.ts.map +1 -0
- package/dist/resolvers/meetingRecordingRegistration.js +311 -0
- package/dist/resolvers/meetingRecordingRegistration.js.map +1 -0
- package/dist/resolvers/peaksSidecar.d.ts +30 -0
- package/dist/resolvers/peaksSidecar.d.ts.map +1 -0
- package/dist/resolvers/peaksSidecar.js +51 -0
- package/dist/resolvers/peaksSidecar.js.map +1 -0
- package/dist/rest/MediaAccessKeys.d.ts +68 -0
- package/dist/rest/MediaAccessKeys.d.ts.map +1 -0
- package/dist/rest/MediaAccessKeys.js +96 -0
- package/dist/rest/MediaAccessKeys.js.map +1 -0
- package/dist/rest/MediaStreamHandler.d.ts +26 -0
- package/dist/rest/MediaStreamHandler.d.ts.map +1 -0
- package/dist/rest/MediaStreamHandler.js +192 -0
- package/dist/rest/MediaStreamHandler.js.map +1 -0
- package/dist/rest/mediaRange.d.ts +41 -0
- package/dist/rest/mediaRange.d.ts.map +1 -0
- package/dist/rest/mediaRange.js +60 -0
- package/dist/rest/mediaRange.js.map +1 -0
- package/package.json +83 -82
- package/src/__tests__/FileResolverPeaks.test.ts +64 -0
- package/src/__tests__/MediaAccessKeys.test.ts +68 -0
- package/src/__tests__/MediaStreamHandler.test.ts +91 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -1
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +69 -0
- package/src/__tests__/RealtimeCoAgentAppAwareness.integration.test.ts +191 -0
- package/src/__tests__/RemoteBrowserSnapshot.test.ts +123 -0
- package/src/__tests__/SessionManager.test.ts +2 -0
- package/src/__tests__/meetingRecordingRegistration.test.ts +248 -0
- package/src/agentSessions/SessionJanitor.ts +15 -1
- package/src/agentSessions/SessionManager.ts +62 -10
- package/src/agentSessions/index.ts +3 -1
- package/src/generated/generated.ts +17659 -13322
- package/src/index.ts +15 -1
- package/src/resolvers/ExecuteRemoteOperationResolver.ts +60 -4
- package/src/resolvers/FileResolver.ts +199 -1
- package/src/resolvers/IntegrationDiscoveryResolver.ts +108 -30
- package/src/resolvers/QueryResolver.ts +33 -19
- package/src/resolvers/RealtimeBridgeResolver.ts +82 -7
- package/src/resolvers/RealtimeClientSessionResolver.ts +326 -9
- package/src/resolvers/RemoteBrowserActionResolver.ts +18 -2
- package/src/resolvers/RunAIAgentResolver.ts +44 -11
- package/src/resolvers/RunTemplateResolver.ts +8 -0
- package/src/resolvers/meetingRecordingRegistration.ts +432 -0
- package/src/resolvers/peaksSidecar.ts +52 -0
- package/src/rest/MediaAccessKeys.ts +121 -0
- package/src/rest/MediaStreamHandler.ts +220 -0
- package/src/rest/mediaRange.ts +76 -0
|
@@ -0,0 +1,192 @@
|
|
|
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 express from 'express';
|
|
21
|
+
import { LogError, Metadata } from '@memberjunction/core';
|
|
22
|
+
import { FileStorageEngine } from '@memberjunction/storage';
|
|
23
|
+
import { getSystemUser } from '../auth/index.js';
|
|
24
|
+
import { MediaAccessKeyManager } from './MediaAccessKeys.js';
|
|
25
|
+
import { parseRange, parseRangeHeaderLoose } from './mediaRange.js';
|
|
26
|
+
/**
|
|
27
|
+
* Builds the Express router exposing `GET /media/:fileId`. Stateless — verification
|
|
28
|
+
* and byte-source resolution happen per request.
|
|
29
|
+
*/
|
|
30
|
+
export function createMediaStreamRouter() {
|
|
31
|
+
const router = express.Router();
|
|
32
|
+
router.get('/:fileId', async (req, res) => {
|
|
33
|
+
await handleMediaRequest(req, res);
|
|
34
|
+
});
|
|
35
|
+
return router;
|
|
36
|
+
}
|
|
37
|
+
/** Top-level request handler: verify token → resolve bytes → stream/buffer with Range support. */
|
|
38
|
+
async function handleMediaRequest(req, res) {
|
|
39
|
+
const fileId = req.params.fileId;
|
|
40
|
+
const token = typeof req.query.token === 'string' ? req.query.token : '';
|
|
41
|
+
const claims = verifyMediaToken(token, fileId);
|
|
42
|
+
if (!claims) {
|
|
43
|
+
// No body leak — a bad/expired/mismatched token is indistinguishable from "forbidden".
|
|
44
|
+
res.status(403).end();
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// Never let CDNs or shared caches retain authorized media bytes.
|
|
48
|
+
res.setHeader('Cache-Control', 'private, no-store');
|
|
49
|
+
try {
|
|
50
|
+
const source = await resolveFileBytesSource(fileId);
|
|
51
|
+
if (!source) {
|
|
52
|
+
res.status(404).end();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
await streamOrBuffer(req, res, source);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
LogError(`[MediaStream] Failed to serve file ${fileId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
59
|
+
if (!res.headersSent) {
|
|
60
|
+
res.status(500).end();
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
res.destroy();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Verifies the token and that it grants access to THIS file. Returns null on any failure
|
|
69
|
+
* (signature, expiry, `typ`, or fileId mismatch) so the caller can 403 uniformly.
|
|
70
|
+
*/
|
|
71
|
+
function verifyMediaToken(token, fileId) {
|
|
72
|
+
if (!token) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const result = MediaAccessKeyManager.Instance.Verify(token);
|
|
76
|
+
if (!result.Valid) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
// The token's fileId must match the path — a token minted for file A cannot stream file B.
|
|
80
|
+
if (result.Claims.fileId !== fileId) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return { fileId: result.Claims.fileId, userId: result.Claims.userId };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Loads the file under a SYSTEM context and resolves its storage driver + provider key.
|
|
87
|
+
* Returns null when the file row, its account, or its provider key can't be resolved
|
|
88
|
+
* (→ 404). Mirrors `readRealtimeRecordingFile`'s account-resolution (engine config with
|
|
89
|
+
* force-refresh fallback for newly-provisioned accounts).
|
|
90
|
+
*/
|
|
91
|
+
async function resolveFileBytesSource(fileId) {
|
|
92
|
+
const systemUser = await getSystemUser();
|
|
93
|
+
// The /media route runs pre-auth on the server's own provider; access was already authorized
|
|
94
|
+
// at token-mint time, so this load only locates the bytes.
|
|
95
|
+
const md = new Metadata(); // global-provider-ok: pre-auth system-context route, no per-request provider
|
|
96
|
+
const file = await md.GetEntityObject('MJ: Files', systemUser);
|
|
97
|
+
if (!await file.Load(fileId) || !file.ProviderKey) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
// Resolve the storage account for the file's provider; force-refresh once if a
|
|
101
|
+
// newly-provisioned account isn't in the cache yet.
|
|
102
|
+
await FileStorageEngine.Instance.Config(false, systemUser);
|
|
103
|
+
let accounts = FileStorageEngine.Instance.GetAccountsByProviderID(file.ProviderID);
|
|
104
|
+
if (accounts.length === 0) {
|
|
105
|
+
await FileStorageEngine.Instance.Config(true, systemUser);
|
|
106
|
+
accounts = FileStorageEngine.Instance.GetAccountsByProviderID(file.ProviderID);
|
|
107
|
+
}
|
|
108
|
+
const account = accounts[0];
|
|
109
|
+
if (!account) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const driver = await FileStorageEngine.Instance.GetDriver(account.ID, systemUser);
|
|
113
|
+
return {
|
|
114
|
+
driver,
|
|
115
|
+
providerKey: file.ProviderKey,
|
|
116
|
+
contentType: file.ContentType ?? 'application/octet-stream',
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Streams the object honoring an HTTP `Range` request. Prefers true streaming
|
|
121
|
+
* (`GetObjectStream`) for streaming-capable drivers; gracefully falls back to a
|
|
122
|
+
* full buffered `GetObject` (slicing for Range) for drivers that don't.
|
|
123
|
+
*/
|
|
124
|
+
async function streamOrBuffer(req, res, source) {
|
|
125
|
+
const rangeHeader = typeof req.headers.range === 'string' ? req.headers.range : undefined;
|
|
126
|
+
if (source.driver.SupportsStreaming) {
|
|
127
|
+
await serveViaStream(res, source, rangeHeader);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
await serveViaBuffer(res, source, rangeHeader);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** True-streaming path: `GetObjectStream` + pipe, with 206 when a Range was honored. */
|
|
134
|
+
async function serveViaStream(res, source, rangeHeader) {
|
|
135
|
+
const range = rangeHeader ? parseRangeHeaderLoose(rangeHeader) : undefined;
|
|
136
|
+
// Omit End for an open-ended range so the driver streams to EOF (per ByteRange semantics).
|
|
137
|
+
const streamRange = range
|
|
138
|
+
? (range.end == null ? { Start: range.start } : { Start: range.start, End: range.end })
|
|
139
|
+
: undefined;
|
|
140
|
+
const result = await source.driver.GetObjectStream({ fullPath: source.providerKey, Range: streamRange });
|
|
141
|
+
res.setHeader('Accept-Ranges', 'bytes');
|
|
142
|
+
res.setHeader('Content-Type', result.ContentType ?? source.contentType);
|
|
143
|
+
if (result.ContentLength != null) {
|
|
144
|
+
res.setHeader('Content-Length', String(result.ContentLength));
|
|
145
|
+
}
|
|
146
|
+
if (result.ContentRange) {
|
|
147
|
+
const { Start, End, Total } = result.ContentRange;
|
|
148
|
+
res.setHeader('Content-Range', `bytes ${Start}-${End}/${Total}`);
|
|
149
|
+
res.status(206);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
res.status(200);
|
|
153
|
+
}
|
|
154
|
+
// Tear down the source stream if the client aborts (closed tab, seek, etc.).
|
|
155
|
+
res.on('close', () => result.Stream.destroy());
|
|
156
|
+
result.Stream.on('error', (err) => {
|
|
157
|
+
LogError(`[MediaStream] Source stream error: ${err instanceof Error ? err.message : String(err)}`);
|
|
158
|
+
if (!res.headersSent) {
|
|
159
|
+
res.status(500).end();
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
res.destroy();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
result.Stream.pipe(res);
|
|
166
|
+
}
|
|
167
|
+
/** Fallback path: full buffered `GetObject`, slicing the buffer for Range (206) or sending whole (200). */
|
|
168
|
+
async function serveViaBuffer(res, source, rangeHeader) {
|
|
169
|
+
const buffer = await source.driver.GetObject({ fullPath: source.providerKey });
|
|
170
|
+
const total = buffer.length;
|
|
171
|
+
res.setHeader('Accept-Ranges', 'bytes');
|
|
172
|
+
res.setHeader('Content-Type', source.contentType);
|
|
173
|
+
if (!rangeHeader) {
|
|
174
|
+
res.status(200);
|
|
175
|
+
res.setHeader('Content-Length', String(total));
|
|
176
|
+
res.end(buffer);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const range = parseRange(rangeHeader, total);
|
|
180
|
+
if (!range) {
|
|
181
|
+
// Unsatisfiable range — per RFC 7233, 416 + Content-Range with the total size.
|
|
182
|
+
res.setHeader('Content-Range', `bytes */${total}`);
|
|
183
|
+
res.status(416).end();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const slice = buffer.subarray(range.start, range.end + 1);
|
|
187
|
+
res.status(206);
|
|
188
|
+
res.setHeader('Content-Range', `bytes ${range.start}-${range.end}/${total}`);
|
|
189
|
+
res.setHeader('Content-Length', String(slice.length));
|
|
190
|
+
res.end(slice);
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=MediaStreamHandler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MediaStreamHandler.js","sourceRoot":"","sources":["../../src/rest/MediaStreamHandler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Pure HTTP `Range` header parsing for the `/media/:fileId` stream route.
|
|
3
|
+
*
|
|
4
|
+
* Split out of {@link module:@memberjunction/server/rest/MediaStreamHandler} (which pulls in
|
|
5
|
+
* auth/config/storage at module load) so this load-bearing parsing logic is unit-testable in
|
|
6
|
+
* isolation — same pattern as `peaksSidecar.ts`. These functions decide whether a request gets
|
|
7
|
+
* a 206 partial body (and over which byte window) vs. a full 200 / a 416.
|
|
8
|
+
*
|
|
9
|
+
* Both helpers are deliberately strict single-range parsers: anything malformed, multi-range,
|
|
10
|
+
* or suffix-only (`bytes=-N`) yields `undefined`, letting the caller fall back to a full read
|
|
11
|
+
* (streaming path) or a 416 (buffer path).
|
|
12
|
+
*
|
|
13
|
+
* @module @memberjunction/server/rest/mediaRange
|
|
14
|
+
*/
|
|
15
|
+
/** A parsed, validated `Range` request against a known total size. */
|
|
16
|
+
export interface ParsedRange {
|
|
17
|
+
start: number;
|
|
18
|
+
end: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A parsed `Range` request WITHOUT a known total (streaming path). `end` is undefined
|
|
22
|
+
* for an open-ended `bytes=start-` request, matching `ByteRange`'s "omit End = EOF".
|
|
23
|
+
*/
|
|
24
|
+
export interface ParsedOpenRange {
|
|
25
|
+
start: number;
|
|
26
|
+
end?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Parses a single-range `bytes=start-end` header WITHOUT a known total (streaming path).
|
|
30
|
+
* Returns the inclusive offsets, or undefined when the header is malformed/multi-range/
|
|
31
|
+
* suffix-only (those we leave to the driver as a full read). `end` is left undefined when
|
|
32
|
+
* open-ended (`bytes=start-`) so the driver streams to EOF.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseRangeHeaderLoose(rangeHeader: string): ParsedOpenRange | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Parses a single-range `bytes=start-end` header against a known total (buffer path),
|
|
37
|
+
* clamping `end` to the last byte. Returns undefined when unsatisfiable (→ 416) or
|
|
38
|
+
* malformed/multi-range.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseRange(rangeHeader: string, total: number): ParsedRange | undefined;
|
|
41
|
+
//# sourceMappingURL=mediaRange.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mediaRange.d.ts","sourceRoot":"","sources":["../../src/rest/mediaRange.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,sEAAsE;AACtE,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAiBtF;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAetF"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Pure HTTP `Range` header parsing for the `/media/:fileId` stream route.
|
|
3
|
+
*
|
|
4
|
+
* Split out of {@link module:@memberjunction/server/rest/MediaStreamHandler} (which pulls in
|
|
5
|
+
* auth/config/storage at module load) so this load-bearing parsing logic is unit-testable in
|
|
6
|
+
* isolation — same pattern as `peaksSidecar.ts`. These functions decide whether a request gets
|
|
7
|
+
* a 206 partial body (and over which byte window) vs. a full 200 / a 416.
|
|
8
|
+
*
|
|
9
|
+
* Both helpers are deliberately strict single-range parsers: anything malformed, multi-range,
|
|
10
|
+
* or suffix-only (`bytes=-N`) yields `undefined`, letting the caller fall back to a full read
|
|
11
|
+
* (streaming path) or a 416 (buffer path).
|
|
12
|
+
*
|
|
13
|
+
* @module @memberjunction/server/rest/mediaRange
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Parses a single-range `bytes=start-end` header WITHOUT a known total (streaming path).
|
|
17
|
+
* Returns the inclusive offsets, or undefined when the header is malformed/multi-range/
|
|
18
|
+
* suffix-only (those we leave to the driver as a full read). `end` is left undefined when
|
|
19
|
+
* open-ended (`bytes=start-`) so the driver streams to EOF.
|
|
20
|
+
*/
|
|
21
|
+
export function parseRangeHeaderLoose(rangeHeader) {
|
|
22
|
+
const match = /^bytes=(\d+)-(\d*)$/.exec(rangeHeader.trim());
|
|
23
|
+
if (!match) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const start = Number(match[1]);
|
|
27
|
+
if (!Number.isFinite(start)) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
if (match[2] === '') {
|
|
31
|
+
return { start };
|
|
32
|
+
}
|
|
33
|
+
const end = Number(match[2]);
|
|
34
|
+
if (!Number.isFinite(end) || end < start) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return { start, end };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Parses a single-range `bytes=start-end` header against a known total (buffer path),
|
|
41
|
+
* clamping `end` to the last byte. Returns undefined when unsatisfiable (→ 416) or
|
|
42
|
+
* malformed/multi-range.
|
|
43
|
+
*/
|
|
44
|
+
export function parseRange(rangeHeader, total) {
|
|
45
|
+
const match = /^bytes=(\d+)-(\d*)$/.exec(rangeHeader.trim());
|
|
46
|
+
if (!match) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
const start = Number(match[1]);
|
|
50
|
+
if (!Number.isFinite(start) || start >= total) {
|
|
51
|
+
return undefined; // start past EOF → unsatisfiable
|
|
52
|
+
}
|
|
53
|
+
const requestedEnd = match[2] === '' ? total - 1 : Number(match[2]);
|
|
54
|
+
const end = Math.min(requestedEnd, total - 1);
|
|
55
|
+
if (end < start) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
return { start, end };
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=mediaRange.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mediaRange.js","sourceRoot":"","sources":["../../src/rest/mediaRange.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAiBH;;;;;GAKG;AACH,MAAM,UAAU,qBAAqB,CAAC,WAAmB;IACvD,MAAM,KAAK,GAAG,qBAAqB,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC;IAC7D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5B,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC;QACpB,OAAO,EAAE,KAAK,EAAE,CAAC;IACnB,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,KAAK,EAAE,CAAC;QACzC,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;AACxB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,WAAmB,EAAE,KAAa;IAC3D,MAAM,KAAK,GAAG,qBAAqB,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC;IAC7D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,KAAK,EAAE,CAAC;QAC9C,OAAO,SAAS,CAAC,CAAC,iCAAiC;IACrD,CAAC;IACD,MAAM,YAAY,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACpE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;IAC9C,IAAI,GAAG,GAAG,KAAK,EAAE,CAAC;QAChB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;AACxB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/server",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.44.0",
|
|
4
4
|
"description": "MemberJunction: This project provides API access via GraphQL to the common data store.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -27,87 +27,88 @@
|
|
|
27
27
|
"@as-integrations/express5": "^1.0.0",
|
|
28
28
|
"@graphql-tools/schema": "latest",
|
|
29
29
|
"@graphql-tools/utils": "^11.0.0",
|
|
30
|
-
"@memberjunction/actions": "5.
|
|
31
|
-
"@memberjunction/actions-apollo": "5.
|
|
32
|
-
"@memberjunction/actions-base": "5.
|
|
33
|
-
"@memberjunction/actions-bizapps-accounting": "5.
|
|
34
|
-
"@memberjunction/actions-bizapps-crm": "5.
|
|
35
|
-
"@memberjunction/actions-bizapps-formbuilders": "5.
|
|
36
|
-
"@memberjunction/actions-bizapps-lms": "5.
|
|
37
|
-
"@memberjunction/actions-bizapps-social": "5.
|
|
38
|
-
"@memberjunction/ai": "5.
|
|
39
|
-
"@memberjunction/ai-agent-manager": "5.
|
|
40
|
-
"@memberjunction/ai-agent-manager-actions": "5.
|
|
41
|
-
"@memberjunction/ai-agents": "5.
|
|
42
|
-
"@memberjunction/ai-bridge-server": "5.
|
|
43
|
-
"@memberjunction/ai-core-plus": "5.
|
|
44
|
-
"@memberjunction/ai-engine-base": "5.
|
|
45
|
-
"@memberjunction/ai-mcp-client": "5.
|
|
46
|
-
"@memberjunction/ai-prompts": "5.
|
|
47
|
-
"@memberjunction/ai-provider-bundle": "5.
|
|
48
|
-
"@memberjunction/ai-vector-sync": "5.
|
|
49
|
-
"@memberjunction/ai-vectordb": "5.
|
|
50
|
-
"@memberjunction/ai-vectors-pinecone": "5.
|
|
51
|
-
"@memberjunction/aiengine": "5.
|
|
52
|
-
"@memberjunction/api-keys": "5.
|
|
53
|
-
"@memberjunction/auth-providers": "5.
|
|
54
|
-
"@memberjunction/clustering-engine": "5.
|
|
55
|
-
"@memberjunction/codegen-lib": "5.
|
|
56
|
-
"@memberjunction/communication-engine": "5.
|
|
57
|
-
"@memberjunction/communication-ms-graph": "5.
|
|
58
|
-
"@memberjunction/notifications": "5.
|
|
59
|
-
"@memberjunction/communication-sendgrid": "5.
|
|
60
|
-
"@memberjunction/communication-types": "5.
|
|
61
|
-
"@memberjunction/component-registry-client-sdk": "5.
|
|
62
|
-
"@memberjunction/computer-use": "5.
|
|
63
|
-
"@memberjunction/computer-use-engine": "5.
|
|
64
|
-
"@memberjunction/config": "5.
|
|
65
|
-
"@memberjunction/core": "5.
|
|
66
|
-
"@memberjunction/core-actions": "5.
|
|
67
|
-
"@memberjunction/core-entities": "5.
|
|
68
|
-
"@memberjunction/core-entities-server": "5.
|
|
69
|
-
"@memberjunction/data-context": "5.
|
|
70
|
-
"@memberjunction/data-context-server": "5.
|
|
71
|
-
"@memberjunction/doc-utils": "5.
|
|
72
|
-
"@memberjunction/encryption": "5.
|
|
73
|
-
"@memberjunction/entity-communications-base": "5.
|
|
74
|
-
"@memberjunction/entity-communications-server": "5.
|
|
75
|
-
"@memberjunction/esignature": "5.
|
|
76
|
-
"@memberjunction/external-change-detection": "5.
|
|
77
|
-
"@memberjunction/generic-database-provider": "5.
|
|
78
|
-
"@memberjunction/global": "5.
|
|
79
|
-
"@memberjunction/graphql-dataprovider": "5.
|
|
80
|
-
"@memberjunction/integration-engine": "5.
|
|
81
|
-
"@memberjunction/integration-progress-artifacts": "5.
|
|
82
|
-
"@memberjunction/integration-schema-builder": "5.
|
|
83
|
-
"@memberjunction/interactive-component-types": "5.
|
|
84
|
-
"@memberjunction/lists": "5.
|
|
85
|
-
"@memberjunction/lists-base": "5.
|
|
86
|
-
"@memberjunction/livekit-room-server": "5.
|
|
87
|
-
"@memberjunction/postgresql-dataprovider": "5.
|
|
88
|
-
"@memberjunction/queue": "5.
|
|
89
|
-
"@memberjunction/
|
|
90
|
-
"@memberjunction/
|
|
91
|
-
"@memberjunction/remote-browser-
|
|
92
|
-
"@memberjunction/remote-browser-
|
|
93
|
-
"@memberjunction/remote-browser-
|
|
94
|
-
"@memberjunction/
|
|
95
|
-
"@memberjunction/scheduling-
|
|
96
|
-
"@memberjunction/scheduling-
|
|
97
|
-
"@memberjunction/scheduling-engine
|
|
98
|
-
"@memberjunction/
|
|
99
|
-
"@memberjunction/
|
|
100
|
-
"@memberjunction/
|
|
101
|
-
"@memberjunction/
|
|
102
|
-
"@memberjunction/
|
|
103
|
-
"@memberjunction/
|
|
104
|
-
"@memberjunction/
|
|
105
|
-
"@memberjunction/
|
|
106
|
-
"@memberjunction/tag-engine
|
|
107
|
-
"@memberjunction/
|
|
108
|
-
"@memberjunction/
|
|
109
|
-
"@memberjunction/testing-engine
|
|
110
|
-
"@memberjunction/
|
|
30
|
+
"@memberjunction/actions": "5.44.0",
|
|
31
|
+
"@memberjunction/actions-apollo": "5.44.0",
|
|
32
|
+
"@memberjunction/actions-base": "5.44.0",
|
|
33
|
+
"@memberjunction/actions-bizapps-accounting": "5.44.0",
|
|
34
|
+
"@memberjunction/actions-bizapps-crm": "5.44.0",
|
|
35
|
+
"@memberjunction/actions-bizapps-formbuilders": "5.44.0",
|
|
36
|
+
"@memberjunction/actions-bizapps-lms": "5.44.0",
|
|
37
|
+
"@memberjunction/actions-bizapps-social": "5.44.0",
|
|
38
|
+
"@memberjunction/ai": "5.44.0",
|
|
39
|
+
"@memberjunction/ai-agent-manager": "5.44.0",
|
|
40
|
+
"@memberjunction/ai-agent-manager-actions": "5.44.0",
|
|
41
|
+
"@memberjunction/ai-agents": "5.44.0",
|
|
42
|
+
"@memberjunction/ai-bridge-server": "5.44.0",
|
|
43
|
+
"@memberjunction/ai-core-plus": "5.44.0",
|
|
44
|
+
"@memberjunction/ai-engine-base": "5.44.0",
|
|
45
|
+
"@memberjunction/ai-mcp-client": "5.44.0",
|
|
46
|
+
"@memberjunction/ai-prompts": "5.44.0",
|
|
47
|
+
"@memberjunction/ai-provider-bundle": "5.44.0",
|
|
48
|
+
"@memberjunction/ai-vector-sync": "5.44.0",
|
|
49
|
+
"@memberjunction/ai-vectordb": "5.44.0",
|
|
50
|
+
"@memberjunction/ai-vectors-pinecone": "5.44.0",
|
|
51
|
+
"@memberjunction/aiengine": "5.44.0",
|
|
52
|
+
"@memberjunction/api-keys": "5.44.0",
|
|
53
|
+
"@memberjunction/auth-providers": "5.44.0",
|
|
54
|
+
"@memberjunction/clustering-engine": "5.44.0",
|
|
55
|
+
"@memberjunction/codegen-lib": "5.44.0",
|
|
56
|
+
"@memberjunction/communication-engine": "5.44.0",
|
|
57
|
+
"@memberjunction/communication-ms-graph": "5.44.0",
|
|
58
|
+
"@memberjunction/notifications": "5.44.0",
|
|
59
|
+
"@memberjunction/communication-sendgrid": "5.44.0",
|
|
60
|
+
"@memberjunction/communication-types": "5.44.0",
|
|
61
|
+
"@memberjunction/component-registry-client-sdk": "5.44.0",
|
|
62
|
+
"@memberjunction/computer-use": "5.44.0",
|
|
63
|
+
"@memberjunction/computer-use-engine": "5.44.0",
|
|
64
|
+
"@memberjunction/config": "5.44.0",
|
|
65
|
+
"@memberjunction/core": "5.44.0",
|
|
66
|
+
"@memberjunction/core-actions": "5.44.0",
|
|
67
|
+
"@memberjunction/core-entities": "5.44.0",
|
|
68
|
+
"@memberjunction/core-entities-server": "5.44.0",
|
|
69
|
+
"@memberjunction/data-context": "5.44.0",
|
|
70
|
+
"@memberjunction/data-context-server": "5.44.0",
|
|
71
|
+
"@memberjunction/doc-utils": "5.44.0",
|
|
72
|
+
"@memberjunction/encryption": "5.44.0",
|
|
73
|
+
"@memberjunction/entity-communications-base": "5.44.0",
|
|
74
|
+
"@memberjunction/entity-communications-server": "5.44.0",
|
|
75
|
+
"@memberjunction/esignature": "5.44.0",
|
|
76
|
+
"@memberjunction/external-change-detection": "5.44.0",
|
|
77
|
+
"@memberjunction/generic-database-provider": "5.44.0",
|
|
78
|
+
"@memberjunction/global": "5.44.0",
|
|
79
|
+
"@memberjunction/graphql-dataprovider": "5.44.0",
|
|
80
|
+
"@memberjunction/integration-engine": "5.44.0",
|
|
81
|
+
"@memberjunction/integration-progress-artifacts": "5.44.0",
|
|
82
|
+
"@memberjunction/integration-schema-builder": "5.44.0",
|
|
83
|
+
"@memberjunction/interactive-component-types": "5.44.0",
|
|
84
|
+
"@memberjunction/lists": "5.44.0",
|
|
85
|
+
"@memberjunction/lists-base": "5.44.0",
|
|
86
|
+
"@memberjunction/livekit-room-server": "5.44.0",
|
|
87
|
+
"@memberjunction/postgresql-dataprovider": "5.44.0",
|
|
88
|
+
"@memberjunction/queue": "5.44.0",
|
|
89
|
+
"@memberjunction/record-comparison": "5.44.0",
|
|
90
|
+
"@memberjunction/redis-provider": "5.44.0",
|
|
91
|
+
"@memberjunction/remote-browser-base": "5.44.0",
|
|
92
|
+
"@memberjunction/remote-browser-cdp": "5.44.0",
|
|
93
|
+
"@memberjunction/remote-browser-selfhost": "5.44.0",
|
|
94
|
+
"@memberjunction/remote-browser-server": "5.44.0",
|
|
95
|
+
"@memberjunction/scheduling-actions": "5.44.0",
|
|
96
|
+
"@memberjunction/scheduling-base-types": "5.44.0",
|
|
97
|
+
"@memberjunction/scheduling-engine": "5.44.0",
|
|
98
|
+
"@memberjunction/scheduling-engine-base": "5.44.0",
|
|
99
|
+
"@memberjunction/schema-engine": "5.44.0",
|
|
100
|
+
"@memberjunction/search-engine": "5.44.0",
|
|
101
|
+
"@memberjunction/server-extensions-core": "5.44.0",
|
|
102
|
+
"@memberjunction/skip-types": "5.44.0",
|
|
103
|
+
"@memberjunction/sql-dialect": "5.44.0",
|
|
104
|
+
"@memberjunction/sqlserver-dataprovider": "5.44.0",
|
|
105
|
+
"@memberjunction/storage": "5.44.0",
|
|
106
|
+
"@memberjunction/tag-engine": "5.44.0",
|
|
107
|
+
"@memberjunction/tag-engine-base": "5.44.0",
|
|
108
|
+
"@memberjunction/templates": "5.44.0",
|
|
109
|
+
"@memberjunction/testing-engine": "5.44.0",
|
|
110
|
+
"@memberjunction/testing-engine-base": "5.44.0",
|
|
111
|
+
"@memberjunction/version-history": "5.44.0",
|
|
111
112
|
"@octokit/auth-app": "^7.1.5",
|
|
112
113
|
"@octokit/rest": "^21.1.1",
|
|
113
114
|
"@types/compression": "^1.8.1",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { deriveSidecarPath, parsePeaksSidecar, MAX_PEAKS } from '../resolvers/peaksSidecar.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tests for the pure `peaks.json` sidecar helpers that back FileResolver.CreateMediaAccessToken's
|
|
6
|
+
* peaks pass-through. These cover the load-bearing behavior the mutation depends on:
|
|
7
|
+
* - deriving the sidecar path that sits in the SAME folder as the recording, and
|
|
8
|
+
* - parsing + sanitizing the sidecar so valid peaks come back and garbage / wrong-shape / missing
|
|
9
|
+
* payloads gracefully yield `undefined` (the resolver treats that as "omit Peaks, never fail").
|
|
10
|
+
*
|
|
11
|
+
* The resolver wires these to a storage driver behind a try/catch; the helpers themselves never
|
|
12
|
+
* throw, which is what guarantees a sidecar problem can't block token minting.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
describe('deriveSidecarPath', () => {
|
|
16
|
+
it('replaces the final path segment with peaks.json (recording in a session folder)', () => {
|
|
17
|
+
expect(deriveSidecarPath('realtime-recordings/sess-1/recording.wav')).toBe('realtime-recordings/sess-1/peaks.json');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('uses a bare peaks.json when the key has no folder', () => {
|
|
21
|
+
expect(deriveSidecarPath('recording.wav')).toBe('peaks.json');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('returns null when there is no ProviderKey (cannot derive a folder)', () => {
|
|
25
|
+
expect(deriveSidecarPath(null)).toBeNull();
|
|
26
|
+
expect(deriveSidecarPath(undefined)).toBeNull();
|
|
27
|
+
expect(deriveSidecarPath('')).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('parsePeaksSidecar', () => {
|
|
32
|
+
it('returns sanitized peaks from a valid JSON array (clamped to 0..1)', () => {
|
|
33
|
+
const bytes = Buffer.from(JSON.stringify([0, 0.5, 1, 1.7, -0.3]), 'utf8');
|
|
34
|
+
expect(parsePeaksSidecar(bytes)).toEqual([0, 0.5, 1, 1, 0]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('returns undefined for non-JSON garbage (never throws)', () => {
|
|
38
|
+
expect(parsePeaksSidecar(Buffer.from('this is not json', 'utf8'))).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('returns undefined when the JSON is not an array', () => {
|
|
42
|
+
expect(parsePeaksSidecar(Buffer.from(JSON.stringify({ not: 'an array' }), 'utf8'))).toBeUndefined();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('returns undefined for an empty array', () => {
|
|
46
|
+
expect(parsePeaksSidecar(Buffer.from('[]', 'utf8'))).toBeUndefined();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('drops non-finite / non-number entries, keeping the finite numbers', () => {
|
|
50
|
+
const bytes = Buffer.from(JSON.stringify([0.2, 'x', null, NaN, 0.8]), 'utf8');
|
|
51
|
+
// NaN serializes to null in JSON; both 'x' and the nulls are dropped, leaving the two numbers.
|
|
52
|
+
expect(parsePeaksSidecar(bytes)).toEqual([0.2, 0.8]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('returns undefined when nothing finite survives sanitization', () => {
|
|
56
|
+
expect(parsePeaksSidecar(Buffer.from(JSON.stringify(['a', 'b', null]), 'utf8'))).toBeUndefined();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('caps the array length at MAX_PEAKS', () => {
|
|
60
|
+
const huge = new Array(MAX_PEAKS + 500).fill(0.5);
|
|
61
|
+
const result = parsePeaksSidecar(Buffer.from(JSON.stringify(huge), 'utf8'));
|
|
62
|
+
expect(result).toHaveLength(MAX_PEAKS);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import jwt from 'jsonwebtoken';
|
|
3
|
+
import { MediaAccessKeyManager, MEDIA_ACCESS_TOKEN_TYPE } from '../rest/MediaAccessKeys.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The manager is a process-wide BaseSingleton whose secret is resolved lazily on first
|
|
7
|
+
* Sign/Verify. Tests share that single ephemeral secret (no MJ_BASE_ENCRYPTION_KEY in the
|
|
8
|
+
* test env), which is exactly what production does within one process.
|
|
9
|
+
*/
|
|
10
|
+
describe('MediaAccessKeyManager', () => {
|
|
11
|
+
it('signs a token that round-trips through Verify with the correct claims', () => {
|
|
12
|
+
const km = MediaAccessKeyManager.Instance;
|
|
13
|
+
const { Token, ExpiresAt } = km.Sign('FILE-1', 'USER-1');
|
|
14
|
+
|
|
15
|
+
expect(typeof Token).toBe('string');
|
|
16
|
+
expect(ExpiresAt.getTime()).toBeGreaterThan(Date.now());
|
|
17
|
+
|
|
18
|
+
const result = km.Verify(Token);
|
|
19
|
+
expect(result.Valid).toBe(true);
|
|
20
|
+
if (result.Valid) {
|
|
21
|
+
expect(result.Claims.fileId).toBe('FILE-1');
|
|
22
|
+
expect(result.Claims.userId).toBe('USER-1');
|
|
23
|
+
expect(result.Claims.typ).toBe(MEDIA_ACCESS_TOKEN_TYPE);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('honors the requested TTL on ExpiresAt', () => {
|
|
28
|
+
const km = MediaAccessKeyManager.Instance;
|
|
29
|
+
const { ExpiresAt } = km.Sign('FILE-2', 'USER-2', 2); // 2 hours
|
|
30
|
+
const deltaMs = ExpiresAt.getTime() - Date.now();
|
|
31
|
+
// ~2h, allow a generous window for execution time.
|
|
32
|
+
expect(deltaMs).toBeGreaterThan(1.9 * 3600 * 1000);
|
|
33
|
+
expect(deltaMs).toBeLessThan(2.1 * 3600 * 1000);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('rejects a tampered token', () => {
|
|
37
|
+
const km = MediaAccessKeyManager.Instance;
|
|
38
|
+
const { Token } = km.Sign('FILE-3', 'USER-3');
|
|
39
|
+
const tampered = Token.slice(0, -2) + (Token.slice(-2) === 'AA' ? 'BB' : 'AA');
|
|
40
|
+
expect(km.Verify(tampered).Valid).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('rejects a malformed / empty token', () => {
|
|
44
|
+
const km = MediaAccessKeyManager.Instance;
|
|
45
|
+
expect(km.Verify('').Valid).toBe(false);
|
|
46
|
+
expect(km.Verify('not.a.jwt').Valid).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('rejects an expired token', () => {
|
|
50
|
+
const km = MediaAccessKeyManager.Instance;
|
|
51
|
+
// Negative TTL → already expired when minted.
|
|
52
|
+
const { Token } = km.Sign('FILE-4', 'USER-4', -1);
|
|
53
|
+
expect(km.Verify(Token).Valid).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('rejects a token with the wrong typ even if validly signed', () => {
|
|
57
|
+
const km = MediaAccessKeyManager.Instance;
|
|
58
|
+
// Mint a valid token, then read the real secret out of a known-good token by
|
|
59
|
+
// re-signing through the manager is not possible; instead assert that a token
|
|
60
|
+
// whose typ differs is rejected by signing a structurally-valid JWT with a
|
|
61
|
+
// foreign secret (signature failure also yields false, which is the safe outcome).
|
|
62
|
+
const foreign = jwt.sign({ fileId: 'FILE-5', userId: 'USER-5', typ: 'something-else' }, 'foreign-secret', {
|
|
63
|
+
algorithm: 'HS256',
|
|
64
|
+
expiresIn: 3600,
|
|
65
|
+
});
|
|
66
|
+
expect(km.Verify(foreign).Valid).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
});
|