* Hydrate a stored doc back into a SessionState. * * Security invariant (per #2283 security review): `structuredDeserialize` * walks untrusted JSONB via `Object.entries` and can reconstitute a Map * whose entries include a "constructor" or "__proto__" key. Those are safe * on Map lookups (`.get
(data: Record<string, unknown>)
| 258 | * matching line here or it will hydrate as a raw envelope object. |
| 259 | */ |
| 260 | function deserializeSession(data: Record<string, unknown>): SessionState { |
| 261 | const hydratedWithMeta = structuredDeserialize(data) as Partial<SessionState> & { lastGetProductsContext?: unknown; __sessionKey?: unknown }; |
| 262 | const { __sessionKey: _sessionKey, ...hydrated } = hydratedWithMeta; |
| 263 | const fresh = createSession(); |
| 264 | const asMap = <V>(v: unknown, fallback: Map<string, V>): Map<string, V> => |
| 265 | v instanceof Map ? (v as Map<string, V>) : fallback; |
| 266 | const hydratedComply = (hydrated.complyExtensions ?? {}) as Partial<SessionState['complyExtensions']>; |
| 267 | return { |
| 268 | ...fresh, |
| 269 | ...hydrated, |
| 270 | mediaBuys: asMap(hydrated.mediaBuys, fresh.mediaBuys), |
| 271 | creatives: asMap(hydrated.creatives, fresh.creatives), |
| 272 | signalActivations: asMap(hydrated.signalActivations, fresh.signalActivations), |
| 273 | governancePlans: asMap(hydrated.governancePlans, fresh.governancePlans), |
| 274 | governanceChecks: asMap(hydrated.governanceChecks, fresh.governanceChecks), |
| 275 | governanceOutcomes: asMap(hydrated.governanceOutcomes, fresh.governanceOutcomes), |
| 276 | propertyLists: asMap(hydrated.propertyLists, fresh.propertyLists), |
| 277 | collectionLists: asMap(hydrated.collectionLists, fresh.collectionLists), |
| 278 | contentStandards: asMap(hydrated.contentStandards, fresh.contentStandards), |
| 279 | rightsGrants: asMap(hydrated.rightsGrants, fresh.rightsGrants), |
| 280 | usageRecords: Array.isArray(hydrated.usageRecords) ? hydrated.usageRecords : [], |
| 281 | complyExtensions: { |
| 282 | accountStatuses: asMap(hydratedComply.accountStatuses, fresh.complyExtensions.accountStatuses), |
| 283 | siSessions: asMap(hydratedComply.siSessions, fresh.complyExtensions.siSessions), |
| 284 | deliverySimulations: asMap(hydratedComply.deliverySimulations, fresh.complyExtensions.deliverySimulations), |
| 285 | budgetSimulations: asMap(hydratedComply.budgetSimulations, fresh.complyExtensions.budgetSimulations), |
| 286 | seededProducts: asMap(hydratedComply.seededProducts, fresh.complyExtensions.seededProducts), |
| 287 | seededPricingOptions: asMap(hydratedComply.seededPricingOptions, fresh.complyExtensions.seededPricingOptions), |
| 288 | seededCreativeFormats: asMap(hydratedComply.seededCreativeFormats, fresh.complyExtensions.seededCreativeFormats), |
| 289 | forcedCreateMediaBuyArm: hydratedComply.forcedCreateMediaBuyArm, |
| 290 | }, |
| 291 | lastGetProductsContext: (hydrated.lastGetProductsContext as SessionState['lastGetProductsContext']) ?? undefined, |
| 292 | createdAt: hydrated.createdAt instanceof Date ? hydrated.createdAt : fresh.createdAt, |
| 293 | lastAccessedAt: hydrated.lastAccessedAt instanceof Date ? hydrated.lastAccessedAt : fresh.lastAccessedAt, |
| 294 | }; |
| 295 | } |
| 296 | |
| 297 | // ── Public session API (async) ─────────────────────────────────── |
| 298 |
no test coverage detected