* Deterministic session id for (projectId, deviceId) within a time window, * with a grace period at the *start* of each window to avoid boundary splits. * * - windowMs: 30 minutes by default * - graceMs: 1 minute by default (events in first minute of a bucket map to previous bucket) * - Output:
(params: {
projectId: string;
deviceId: string;
eventMs?: number; // use event timestamp; defaults to Date.now()
windowMs?: number; // default 5 min
graceMs?: number; // default 1 min
bytes?: number; // default 16 (128-bit). You can set 24 or 32 for longer ids.
})
| 144 | * - Output: base64url, 128-bit (16 bytes) truncated from SHA-256 |
| 145 | */ |
| 146 | function getSessionId(params: { |
| 147 | projectId: string; |
| 148 | deviceId: string; |
| 149 | eventMs?: number; // use event timestamp; defaults to Date.now() |
| 150 | windowMs?: number; // default 5 min |
| 151 | graceMs?: number; // default 1 min |
| 152 | bytes?: number; // default 16 (128-bit). You can set 24 or 32 for longer ids. |
| 153 | }): string { |
| 154 | const { |
| 155 | projectId, |
| 156 | deviceId, |
| 157 | eventMs = Date.now(), |
| 158 | windowMs = 5 * 60 * 1000, |
| 159 | graceMs = 60 * 1000, |
| 160 | bytes = 16, |
| 161 | } = params; |
| 162 | |
| 163 | if (!projectId) { |
| 164 | throw new Error('projectId is required'); |
| 165 | } |
| 166 | if (!deviceId) { |
| 167 | throw new Error('deviceId is required'); |
| 168 | } |
| 169 | if (windowMs <= 0) { |
| 170 | throw new Error('windowMs must be > 0'); |
| 171 | } |
| 172 | if (graceMs < 0 || graceMs >= windowMs) { |
| 173 | throw new Error('graceMs must be >= 0 and < windowMs'); |
| 174 | } |
| 175 | if (bytes < 8 || bytes > 32) { |
| 176 | throw new Error('bytes must be between 8 and 32'); |
| 177 | } |
| 178 | |
| 179 | const bucket = Math.floor(eventMs / windowMs); |
| 180 | const offset = eventMs - bucket * windowMs; |
| 181 | |
| 182 | // Grace at the start of the bucket: stick to the previous bucket. |
| 183 | const chosenBucket = offset < graceMs ? bucket - 1 : bucket; |
| 184 | |
| 185 | const input = `sess:v1:${projectId}:${deviceId}:${chosenBucket}`; |
| 186 | |
| 187 | const digest = crypto.createHash('sha256').update(input).digest(); |
| 188 | const truncated = digest.subarray(0, bytes); |
| 189 | |
| 190 | // base64url |
| 191 | return truncated |
| 192 | .toString('base64') |
| 193 | .replace(/\+/g, '-') |
| 194 | .replace(/\//g, '_') |
| 195 | .replace(/=+$/g, ''); |
| 196 | } |
no test coverage detected