@djangocfg/monitor 2.1.427 → 2.1.428

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.
@@ -62,7 +62,7 @@ type KeyMap = Map<
62
62
  }
63
63
  >;
64
64
 
65
- const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
65
+ function buildKeyMap(fields: FieldsConfig, map?: KeyMap): KeyMap {
66
66
  if (!map) {
67
67
  map = new Map();
68
68
  }
@@ -85,7 +85,7 @@ const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => {
85
85
  }
86
86
 
87
87
  return map;
88
- };
88
+ }
89
89
 
90
90
  interface Params {
91
91
  body: unknown;
@@ -94,16 +94,18 @@ interface Params {
94
94
  query: Record<string, unknown>;
95
95
  }
96
96
 
97
- const stripEmptySlots = (params: Params) => {
97
+ type ParamsSlotMap = Record<Slot, unknown>;
98
+
99
+ function stripEmptySlots(params: ParamsSlotMap): void {
98
100
  for (const [slot, value] of Object.entries(params)) {
99
101
  if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) {
100
102
  delete params[slot as Slot];
101
103
  }
102
104
  }
103
- };
105
+ }
104
106
 
105
- export const buildClientParams = (args: ReadonlyArray<unknown>, fields: FieldsConfig) => {
106
- const params: Params = {
107
+ export function buildClientParams(args: ReadonlyArray<unknown>, fields: FieldsConfig): Params {
108
+ const params: ParamsSlotMap = {
107
109
  body: Object.create(null),
108
110
  headers: Object.create(null),
109
111
  path: Object.create(null),
@@ -165,5 +167,5 @@ export const buildClientParams = (args: ReadonlyArray<unknown>, fields: FieldsCo
165
167
 
166
168
  stripEmptySlots(params);
167
169
 
168
- return params;
169
- };
170
+ return params as Params;
171
+ }
@@ -25,7 +25,7 @@ interface SerializePrimitiveParam extends SerializePrimitiveOptions {
25
25
  value: string;
26
26
  }
27
27
 
28
- export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
28
+ export const separatorArrayExplode = (style: ArraySeparatorStyle): '.' | ';' | ',' | '&' => {
29
29
  switch (style) {
30
30
  case 'label':
31
31
  return '.';
@@ -38,7 +38,7 @@ export const separatorArrayExplode = (style: ArraySeparatorStyle) => {
38
38
  }
39
39
  };
40
40
 
41
- export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
41
+ export const separatorArrayNoExplode = (style: ArraySeparatorStyle): ',' | '|' | '%20' => {
42
42
  switch (style) {
43
43
  case 'form':
44
44
  return ',';
@@ -51,7 +51,7 @@ export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => {
51
51
  }
52
52
  };
53
53
 
54
- export const separatorObjectExplode = (style: ObjectSeparatorStyle) => {
54
+ export const separatorObjectExplode = (style: ObjectSeparatorStyle): '.' | ';' | ',' | '&' => {
55
55
  switch (style) {
56
56
  case 'label':
57
57
  return '.';
@@ -72,7 +72,7 @@ export const serializeArrayParam = ({
72
72
  value,
73
73
  }: SerializeOptions<ArraySeparatorStyle> & {
74
74
  value: unknown[];
75
- }) => {
75
+ }): string => {
76
76
  if (!explode) {
77
77
  const joinedValues = (
78
78
  allowReserved ? value : value.map((v) => encodeURIComponent(v as string))
@@ -110,7 +110,7 @@ export const serializePrimitiveParam = ({
110
110
  allowReserved,
111
111
  name,
112
112
  value,
113
- }: SerializePrimitiveParam) => {
113
+ }: SerializePrimitiveParam): string => {
114
114
  if (value === undefined || value === null) {
115
115
  return '';
116
116
  }
@@ -134,7 +134,7 @@ export const serializeObjectParam = ({
134
134
  }: SerializeOptions<ObjectSeparatorStyle> & {
135
135
  value: Record<string, unknown> | Date;
136
136
  valueOnly?: boolean;
137
- }) => {
137
+ }): string => {
138
138
  if (value instanceof Date) {
139
139
  return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
140
140
  }
@@ -14,7 +14,7 @@ export type JsonValue =
14
14
  /**
15
15
  * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes.
16
16
  */
17
- export const queryKeyJsonReplacer = (_key: string, value: unknown) => {
17
+ export const queryKeyJsonReplacer = (_key: string, value: unknown): unknown | undefined => {
18
18
  if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
19
19
  return undefined;
20
20
  }
@@ -13,9 +13,9 @@ export interface PathSerializer {
13
13
  url: string;
14
14
  }
15
15
 
16
- export const PATH_PARAM_RE = /\{[^{}]+\}/g;
16
+ export const PATH_PARAM_RE: RegExp = /\{[^{}]+\}/g;
17
17
 
18
- export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
18
+ export const defaultPathSerializer = ({ path, url: _url }: PathSerializer): string => {
19
19
  let url = _url;
20
20
  const matches = _url.match(PATH_PARAM_RE);
21
21
  if (matches) {
@@ -94,7 +94,7 @@ export const getUrl = ({
94
94
  query?: Record<string, unknown>;
95
95
  querySerializer: QuerySerializer;
96
96
  url: string;
97
- }) => {
97
+ }): string => {
98
98
  const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
99
99
  let url = (baseUrl ?? '') + pathUrl;
100
100
  if (path) {
@@ -114,7 +114,7 @@ export function getValidRequestBody(options: {
114
114
  body?: unknown;
115
115
  bodySerializer?: BodySerializer | null;
116
116
  serializedBody?: unknown;
117
- }) {
117
+ }): unknown {
118
118
  const hasBody = options.body !== undefined;
119
119
  const isSerializedBody = hasBody && options.bodySerializer;
120
120
 
@@ -334,6 +334,119 @@ async function tryRefresh(): Promise<string | null> {
334
334
  * headers: { 'X-API-Key': userKey },
335
335
  * })
336
336
  */
337
+ // ── DPoP (RFC 9449) — sender-constrained tokens ────────────────────────────
338
+ //
339
+ // When NEXT_PUBLIC_DPOP_ENABLED === 'true', the client holds a P-256 keypair
340
+ // whose PRIVATE key is non-extractable (Web Crypto `extractable:false`) and
341
+ // stored in IndexedDB. JS — including XSS — can sign with it but can NEVER read
342
+ // it. Each request carries a fresh `DPoP` proof signed by that key; the backend
343
+ // binds the token to the key (`cnf.jkt`) and rejects any request whose proof key
344
+ // doesn't match. A stolen token is therefore useless to an attacker.
345
+ //
346
+ // Dormant unless the env flag is on — bearer-mode apps are unaffected.
347
+
348
+ function dpopEnabled(): boolean {
349
+ try {
350
+ return typeof process !== 'undefined'
351
+ && process.env?.NEXT_PUBLIC_DPOP_ENABLED === 'true';
352
+ } catch { return false; }
353
+ }
354
+
355
+ const _DPOP_DB = 'cfg-auth';
356
+ const _DPOP_STORE = 'keys';
357
+ const _DPOP_KEY_ID = 'dpop-ec-p256';
358
+
359
+ function _idbOpen(): Promise<IDBDatabase> {
360
+ return new Promise((resolve, reject) => {
361
+ const req = indexedDB.open(_DPOP_DB, 1);
362
+ req.onupgradeneeded = () => req.result.createObjectStore(_DPOP_STORE);
363
+ req.onsuccess = () => resolve(req.result);
364
+ req.onerror = () => reject(req.error);
365
+ });
366
+ }
367
+
368
+ function _idbGet(key: string): Promise<CryptoKeyPair | undefined> {
369
+ return _idbOpen().then((db) => new Promise((resolve, reject) => {
370
+ const tx = db.transaction(_DPOP_STORE, 'readonly');
371
+ const req = tx.objectStore(_DPOP_STORE).get(key);
372
+ req.onsuccess = () => resolve(req.result);
373
+ req.onerror = () => reject(req.error);
374
+ }));
375
+ }
376
+
377
+ function _idbPut(key: string, value: CryptoKeyPair): Promise<void> {
378
+ return _idbOpen().then((db) => new Promise((resolve, reject) => {
379
+ const tx = db.transaction(_DPOP_STORE, 'readwrite');
380
+ tx.objectStore(_DPOP_STORE).put(value, key);
381
+ tx.oncomplete = () => resolve();
382
+ tx.onerror = () => reject(tx.error);
383
+ }));
384
+ }
385
+
386
+ /** Single-flight keypair init — the private key is created non-extractable. */
387
+ let _dpopKeyPromise: Promise<CryptoKeyPair> | null = null;
388
+
389
+ function _getDpopKeyPair(): Promise<CryptoKeyPair> {
390
+ if (_dpopKeyPromise) return _dpopKeyPromise;
391
+ _dpopKeyPromise = (async () => {
392
+ const existing = await _idbGet(_DPOP_KEY_ID).catch(() => undefined);
393
+ if (existing) return existing;
394
+ const pair = await crypto.subtle.generateKey(
395
+ { name: 'ECDSA', namedCurve: 'P-256' },
396
+ false, // extractable:false — JS can sign but never export the private key
397
+ ['sign'],
398
+ );
399
+ // CryptoKey is structured-cloneable; IndexedDB persists it across reloads
400
+ // WITHOUT ever exposing raw key bytes to JS.
401
+ await _idbPut(_DPOP_KEY_ID, pair).catch(() => {});
402
+ return pair;
403
+ })();
404
+ return _dpopKeyPromise;
405
+ }
406
+
407
+ function _b64urlFromBytes(bytes: ArrayBuffer | Uint8Array): string {
408
+ const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
409
+ let s = '';
410
+ for (let i = 0; i < arr.length; i++) s += String.fromCharCode(arr[i]);
411
+ return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
412
+ }
413
+
414
+ function _b64urlFromString(str: string): string {
415
+ return _b64urlFromBytes(new TextEncoder().encode(str));
416
+ }
417
+
418
+ /** ECDSA raw signature (r||s) → JOSE base64url (Web Crypto already returns raw). */
419
+ async function _publicJwk(pub: CryptoKey): Promise<Record<string, string>> {
420
+ const jwk = await crypto.subtle.exportKey('jwk', pub);
421
+ // Public components only — required RFC 7638 members for EC.
422
+ return { kty: 'EC', crv: 'P-256', x: jwk.x as string, y: jwk.y as string };
423
+ }
424
+
425
+ /** Build a DPoP proof JWT for this request (htm/htu/iat/jti), signed in-key. */
426
+ async function _makeDpopProof(method: string, url: string): Promise<string | null> {
427
+ try {
428
+ const pair = await _getDpopKeyPair();
429
+ const jwk = await _publicJwk(pair.publicKey);
430
+ const header = { typ: 'dpop+jwt', alg: 'ES256', jwk };
431
+ // htu = scheme://authority/path without query/fragment.
432
+ const htu = url.split('#')[0].split('?')[0];
433
+ const jti =
434
+ (crypto.randomUUID && crypto.randomUUID()) ||
435
+ _b64urlFromBytes(crypto.getRandomValues(new Uint8Array(16)));
436
+ const payload = { htm: method.toUpperCase(), htu, iat: Math.floor(Date.now() / 1000), jti };
437
+ const signingInput =
438
+ `${_b64urlFromString(JSON.stringify(header))}.${_b64urlFromString(JSON.stringify(payload))}`;
439
+ const sig = await crypto.subtle.sign(
440
+ { name: 'ECDSA', hash: 'SHA-256' },
441
+ pair.privateKey,
442
+ new TextEncoder().encode(signingInput),
443
+ );
444
+ return `${signingInput}.${_b64urlFromBytes(sig)}`;
445
+ } catch {
446
+ return null; // never break a request because proof generation failed
447
+ }
448
+ }
449
+
337
450
  export function installAuthOnClient(client: HeyClient): void {
338
451
  if (_client) return; // idempotent
339
452
  _client = client;
@@ -343,7 +456,7 @@ export function installAuthOnClient(client: HeyClient): void {
343
456
  credentials: _withCredentials ? 'include' : 'same-origin',
344
457
  });
345
458
 
346
- client.interceptors.request.use((request) => {
459
+ client.interceptors.request.use(async (request) => {
347
460
  const token = auth.getToken();
348
461
  if (token) request.headers.set('Authorization', `Bearer ${token}`);
349
462
 
@@ -361,6 +474,13 @@ export function installAuthOnClient(client: HeyClient): void {
361
474
  } catch {}
362
475
  request.headers.set('X-Client-Time', new Date().toISOString());
363
476
 
477
+ // DPoP proof — sign a fresh per-request proof with the non-extractable key.
478
+ // Only in a browser, only when enabled. Failure is non-fatal (proof null).
479
+ if (dpopEnabled() && typeof window !== 'undefined') {
480
+ const proof = await _makeDpopProof(request.method, request.url);
481
+ if (proof) request.headers.set('DPoP', proof);
482
+ }
483
+
364
484
  return request;
365
485
  });
366
486
 
@@ -400,6 +520,12 @@ export function installAuthOnClient(client: HeyClient): void {
400
520
  const retry = request.clone();
401
521
  retry.headers.set('Authorization', `Bearer ${newToken}`);
402
522
  retry.headers.set(RETRY_MARKER, '1');
523
+ // This retry uses raw fetch() and bypasses the request interceptor, so the
524
+ // DPoP proof must be re-attached here or a bound token would 401 on retry.
525
+ if (dpopEnabled() && typeof window !== 'undefined') {
526
+ const proof = await _makeDpopProof(retry.method, retry.url);
527
+ if (proof) retry.headers.set('DPoP', proof);
528
+ }
403
529
  try {
404
530
  const retried = await fetch(retry);
405
531
  if (retried.status === 401 && _onUnauthorized) {
@@ -1,6 +1,6 @@
1
1
  // This file is auto-generated by @hey-api/openapi-ts
2
2
 
3
- import type { Client, Options as Options2, TDataShape } from './client';
3
+ import type { Client, Options as Options2, RequestResult, TDataShape } from './client';
4
4
  import { client } from './client.gen';
5
5
  import type { CfgMonitorIngestCreateData, CfgMonitorIngestCreateResponses } from './types.gen';
6
6
 
@@ -24,7 +24,7 @@ export class CfgMonitor {
24
24
  *
25
25
  * Accepts a batch of up to 50 frontend events. No authentication required — anonymous visitors can send events.
26
26
  */
27
- public static cfgMonitorIngestCreate<ThrowOnError extends boolean = false>(options: Options<CfgMonitorIngestCreateData, ThrowOnError>) {
27
+ public static cfgMonitorIngestCreate<ThrowOnError extends boolean = false>(options: Options<CfgMonitorIngestCreateData, ThrowOnError>): RequestResult<CfgMonitorIngestCreateResponses, unknown, ThrowOnError> {
28
28
  return (options.client ?? client).post<CfgMonitorIngestCreateResponses, unknown, ThrowOnError>({
29
29
  url: '/cfg/monitor/ingest/',
30
30
  ...options,