@djangocfg/monitor 2.1.427 → 2.1.429
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/README.md +9 -0
- package/dist/client.cjs +103 -2
- package/dist/client.cjs.map +1 -1
- package/dist/client.mjs +103 -2
- package/dist/client.mjs.map +1 -1
- package/dist/index.cjs +102 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +102 -1
- package/dist/index.mjs.map +1 -1
- package/dist/server.cjs +102 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.mjs +102 -1
- package/dist/server.mjs.map +1 -1
- package/package.json +2 -2
- package/src/_api/generated/client/index.ts +1 -0
- package/src/_api/generated/client/utils.gen.ts +2 -2
- package/src/_api/generated/client.gen.ts +2 -2
- package/src/_api/generated/core/auth.gen.ts +7 -0
- package/src/_api/generated/core/params.gen.ts +10 -8
- package/src/_api/generated/core/pathSerializer.gen.ts +6 -6
- package/src/_api/generated/core/queryKeySerializer.gen.ts +1 -1
- package/src/_api/generated/core/utils.gen.ts +4 -4
- package/src/_api/generated/helpers/auth.ts +127 -1
- package/src/_api/generated/sdk.gen.ts +2 -2
|
@@ -62,7 +62,7 @@ type KeyMap = Map<
|
|
|
62
62
|
}
|
|
63
63
|
>;
|
|
64
64
|
|
|
65
|
-
|
|
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
|
-
|
|
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
|
|
106
|
-
const 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,
|