@vouchington/utils 0.5.3 → 0.6.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/README.md +20 -6
- package/dist/api-errors.d.mts +29 -0
- package/dist/api-errors.mjs +80 -0
- package/dist/api-response-contract.d.mts +62 -0
- package/dist/api-response-contract.mjs +51 -0
- package/dist/content-identity-cache.d.mts +11 -0
- package/dist/content-identity-cache.mjs +26 -0
- package/dist/env-contract.d.mts +8 -0
- package/dist/env-contract.mjs +9 -0
- package/dist/observability-body.d.mts +5 -0
- package/dist/observability-body.mjs +10 -0
- package/dist/observability-credentials.d.mts +6 -0
- package/dist/observability-credentials.mjs +30 -0
- package/dist/observability-event.d.mts +31 -0
- package/dist/observability-event.mjs +59 -0
- package/dist/observability-records.d.mts +9 -0
- package/dist/observability-records.mjs +26 -0
- package/dist/observability-url.d.mts +19 -0
- package/dist/observability-url.mjs +69 -0
- package/dist/observability.d.mts +5 -16
- package/dist/observability.mjs +5 -114
- package/dist/url-signing-hosts.d.mts +21 -0
- package/dist/url-signing-hosts.mjs +57 -0
- package/dist/url-signing-keys.d.mts +23 -0
- package/dist/url-signing-keys.mjs +65 -0
- package/dist/url-signing-proxy.d.mts +19 -0
- package/dist/url-signing-proxy.mjs +36 -0
- package/dist/url-signing.d.mts +3 -2
- package/dist/url-signing.mjs +3 -12
- package/package.json +16 -1
package/README.md
CHANGED
|
@@ -29,19 +29,25 @@ import { createScopeCatalog } from '@vouchington/utils/scopes'
|
|
|
29
29
|
`createTokenSecrets({ hashSecret, encryptionKeys })` throws for invalid keys or ciphertext.
|
|
30
30
|
- `deploy-environment`: `getDeployEnvironment(source)` classifies explicit environment inputs without
|
|
31
31
|
throwing.
|
|
32
|
-
- `url-signing`: Node-only HMAC SHA-256 `signPathWithKey
|
|
33
|
-
|
|
32
|
+
- `url-signing`: Node-only HMAC SHA-256 path signing (`signPathWithKey`, `signPath`,
|
|
33
|
+
`verifyPathSignature` with rotation, `signExpiringPath`/`verifyExpiringPath` with an injectable
|
|
34
|
+
clock), `parseSigningKeys`, `buildSignedProxyUrl` (caller-owned origin, route prefix, query, and
|
|
35
|
+
skip predicate), `createProtectedHostPolicy`, `matchRouteSegment`, and `authorizeDependencyStates`.
|
|
36
|
+
Prefixes, hosts, and keys are injected; verification fails closed with no keys unless
|
|
37
|
+
`allowUnsignedWhenNoKeys` is set.
|
|
34
38
|
- `request-client-info`: `createClientInfoParser(config)` validates caller-selected headers,
|
|
35
39
|
families, platforms, compatibility, and versions.
|
|
36
40
|
- `money`: `createMoneyCatalog(currencies, scale)` supplies exact integer parsing and validation for
|
|
37
41
|
the caller's catalog. `allocateProportionalAmount(amount, numerator, denominator, rounding)`
|
|
38
42
|
allocates an inclusive fraction with `down` or `up` integer rounding chosen explicitly by the
|
|
39
43
|
caller; it uses exact integer arithmetic rather than floating-point division.
|
|
40
|
-
- `env-contract`: `groupEnvContracts()` and `
|
|
44
|
+
- `env-contract`: `groupEnvContracts()`, `normalizeEnvContractGroups()`, and `envContractManifest()` produce delimiter-safe
|
|
41
45
|
metadata keys and surface/sensitivity lookups.
|
|
42
46
|
- `cookies`: `serializeCookie(name, value, attributes)` validates cookie grammar and requires the
|
|
43
47
|
caller to supply every policy attribute. `SameSite=None` requires `Secure`.
|
|
44
48
|
`parseCookies(header)` splits a `Cookie` request header into a name/value `Map`.
|
|
49
|
+
- `content-identity-cache`: `ContentAndIdentityCache` memoizes by object identity (WeakMap fast
|
|
50
|
+
path) with a content-key fallback; the content key is a thunk and entries are never evicted.
|
|
45
51
|
- `http-signatures`: Node-only (`node:crypto`) Cavage-12 HTTP Signatures. Callers pass signed
|
|
46
52
|
headers, allowed algorithms, and `maxAgeSeconds` on both sign and verify. The package has no
|
|
47
53
|
default header set or product keyId policy.
|
|
@@ -68,6 +74,11 @@ import { createScopeCatalog } from '@vouchington/utils/scopes'
|
|
|
68
74
|
- `utm`: `createUtmParser({ sourceAliases, fallbackSourceParam? })` lowercases and trims UTM fields
|
|
69
75
|
and resolves `utm_source` through caller-owned aliases. The package has no default shortcode map.
|
|
70
76
|
- `fetch-ports`: the Fetch standard forbidden-port list and membership predicates.
|
|
77
|
+
- `api-errors`: the house JSON error envelope (`message`, optional `code`, `request_id`, `stack`),
|
|
78
|
+
status/message/code extraction helpers, and `buildApiErrorEnvelope`, which redacts 5xx details
|
|
79
|
+
unless the caller sets `exposeDetails`. Callers own serialization, logging, and environment policy.
|
|
80
|
+
- `api-response-contract`: identity markers (`apiResponse`, `apiRequest`, `apiQuery`, ...) and
|
|
81
|
+
response-variant types read by a static contract extractor; no runtime effect.
|
|
71
82
|
- `http-body`: bounded Fetch response-body reads with abort propagation and typed missing/oversize
|
|
72
83
|
errors.
|
|
73
84
|
- `http-link-pagination`: fail-closed RFC 8288 next-link parsing with same-origin URL and
|
|
@@ -85,9 +96,12 @@ import { createScopeCatalog } from '@vouchington/utils/scopes'
|
|
|
85
96
|
descriptors. Catalog segments cannot contain dots because dots delimit nested keys. Plural values
|
|
86
97
|
and configured number parameters must be finite numbers; callers own catalog content, locale
|
|
87
98
|
choice, and number formatting.
|
|
88
|
-
- `observability`: SDK-free URL, request, event, and span
|
|
89
|
-
|
|
90
|
-
|
|
99
|
+
- `observability`: SDK-free (structural types only) URL, request, breadcrumb, event, and span
|
|
100
|
+
scrubbing plus a bounded spike-window tracker. All scrubbers are copy-on-write: unchanged input is
|
|
101
|
+
returned by identity. Callers provide `credentialHeaders`, an optional `redactUrl` hook (applied
|
|
102
|
+
after query and fragment stripping, for path-borne credentials), and an opt-in `dropRequestBody`.
|
|
103
|
+
URL-only and credential-only passes are exported separately, and `composeBeforeSend` accepts
|
|
104
|
+
either `ScrubOptions` or a final scrubber function. This package has no product defaults.
|
|
91
105
|
|
|
92
106
|
```ts
|
|
93
107
|
const money = createMoneyCatalog([{ code: 'credit', minorUnitExponent: 2 }] as const, 6)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** House JSON error envelope. `code`, `request_id`, and `stack` are omitted when absent. */
|
|
2
|
+
export interface ApiErrorEnvelope {
|
|
3
|
+
readonly message: string;
|
|
4
|
+
readonly code?: string;
|
|
5
|
+
readonly request_id?: string;
|
|
6
|
+
readonly stack?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface BuildApiErrorEnvelopeOptions {
|
|
9
|
+
/** Request correlation id echoed as `request_id`. */
|
|
10
|
+
readonly requestId?: string | null;
|
|
11
|
+
/** When false, 5xx errors are redacted to the status text and stacks are omitted. */
|
|
12
|
+
readonly exposeDetails: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function getErrorStatus(error: unknown): number;
|
|
15
|
+
/** Stamps `status` on 5xx errors so loggers and reporters classify them consistently. */
|
|
16
|
+
export declare function normalizeErrorForLogging(error: Error): Error;
|
|
17
|
+
export declare function getErrorResponseMessage(error: unknown, status: number): string;
|
|
18
|
+
export declare function getErrorResponseCode(error: unknown, status: number): string | undefined;
|
|
19
|
+
export declare function getErrorResponseStack(error: unknown): string | undefined;
|
|
20
|
+
export declare function getStatusMessage(status: number): string;
|
|
21
|
+
/**
|
|
22
|
+
* Builds the house error envelope. With `exposeDetails: false`, 5xx errors collapse to the HTTP
|
|
23
|
+
* status text with code `INTERNAL_ERROR` and never leak messages or stacks; 4xx messages and
|
|
24
|
+
* codes are still returned. Callers own serialization, logging, and environment policy.
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildApiErrorEnvelope(error: unknown, options: BuildApiErrorEnvelopeOptions): {
|
|
27
|
+
readonly status: number;
|
|
28
|
+
readonly body: ApiErrorEnvelope;
|
|
29
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { STATUS_CODES } from 'node:http';
|
|
2
|
+
export function getErrorStatus(error) {
|
|
3
|
+
if (!isObjectLike(error))
|
|
4
|
+
return 500;
|
|
5
|
+
return getValidHttpStatus(error.status) ?? getValidHttpStatus(error.statusCode) ?? 500;
|
|
6
|
+
}
|
|
7
|
+
/** Stamps `status` on 5xx errors so loggers and reporters classify them consistently. */
|
|
8
|
+
export function normalizeErrorForLogging(error) {
|
|
9
|
+
const status = getErrorStatus(error);
|
|
10
|
+
if (status >= 500)
|
|
11
|
+
Object.assign(error, { status });
|
|
12
|
+
return error;
|
|
13
|
+
}
|
|
14
|
+
export function getErrorResponseMessage(error, status) {
|
|
15
|
+
if (error instanceof Error)
|
|
16
|
+
return error.message;
|
|
17
|
+
if (isObjectLike(error) && error.message !== undefined && error.message !== null) {
|
|
18
|
+
return safeString(error.message) ?? getStatusMessage(status);
|
|
19
|
+
}
|
|
20
|
+
if (isObjectLike(error) || error === null || error === undefined)
|
|
21
|
+
return getStatusMessage(status);
|
|
22
|
+
// Only primitives reach here, and String() cannot throw for them.
|
|
23
|
+
return String(error);
|
|
24
|
+
}
|
|
25
|
+
export function getErrorResponseCode(error, status) {
|
|
26
|
+
if (isObjectLike(error) && typeof error.code === 'string')
|
|
27
|
+
return error.code;
|
|
28
|
+
return status >= 500 ? 'INTERNAL_ERROR' : undefined;
|
|
29
|
+
}
|
|
30
|
+
export function getErrorResponseStack(error) {
|
|
31
|
+
if (isObjectLike(error) && typeof error.stack === 'string')
|
|
32
|
+
return error.stack;
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
export function getStatusMessage(status) {
|
|
36
|
+
return STATUS_CODES[status] ?? 'Internal Server Error';
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Builds the house error envelope. With `exposeDetails: false`, 5xx errors collapse to the HTTP
|
|
40
|
+
* status text with code `INTERNAL_ERROR` and never leak messages or stacks; 4xx messages and
|
|
41
|
+
* codes are still returned. Callers own serialization, logging, and environment policy.
|
|
42
|
+
*/
|
|
43
|
+
export function buildApiErrorEnvelope(error, options) {
|
|
44
|
+
const status = getErrorStatus(error);
|
|
45
|
+
const requestId = options.requestId ? { request_id: options.requestId } : {};
|
|
46
|
+
if (!options.exposeDetails && status >= 500) {
|
|
47
|
+
return {
|
|
48
|
+
status,
|
|
49
|
+
body: { message: getStatusMessage(status), code: 'INTERNAL_ERROR', ...requestId },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const code = getErrorResponseCode(error, status);
|
|
53
|
+
const stack = options.exposeDetails ? getErrorResponseStack(error) : undefined;
|
|
54
|
+
return {
|
|
55
|
+
status,
|
|
56
|
+
body: {
|
|
57
|
+
message: getErrorResponseMessage(error, status),
|
|
58
|
+
...(code ? { code } : {}),
|
|
59
|
+
...requestId,
|
|
60
|
+
...(stack ? { stack } : {}),
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function getValidHttpStatus(status) {
|
|
65
|
+
if (typeof status === 'number' && Number.isInteger(status) && status >= 100 && status <= 599) {
|
|
66
|
+
return status;
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
function isObjectLike(value) {
|
|
71
|
+
return typeof value === 'object' && value !== null;
|
|
72
|
+
}
|
|
73
|
+
function safeString(value) {
|
|
74
|
+
try {
|
|
75
|
+
return String(value);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** Compiler-discovered variants carried by an opaque response without reading its body. */
|
|
2
|
+
export type ApiHttpResponseVariant = {
|
|
3
|
+
status: number;
|
|
4
|
+
bodyKind: 'content';
|
|
5
|
+
mediaType: string;
|
|
6
|
+
body: unknown;
|
|
7
|
+
} | {
|
|
8
|
+
status: number;
|
|
9
|
+
bodyKind: 'none';
|
|
10
|
+
};
|
|
11
|
+
export type ApiHttpResponse<TVariants extends ApiHttpResponseVariant> = Response & {
|
|
12
|
+
readonly apiHttpResponseVariants?: TVariants;
|
|
13
|
+
};
|
|
14
|
+
/** Adds JSON Schema array constraints to a statically extracted API request contract. */
|
|
15
|
+
export type ApiArrayContract<TItem, _TMinItems extends number, _TMaxItems extends number, _TUniqueItems extends boolean> = TItem[];
|
|
16
|
+
export interface ApiHeader {
|
|
17
|
+
description?: string;
|
|
18
|
+
format?: 'uuid';
|
|
19
|
+
required?: boolean;
|
|
20
|
+
type: 'integer' | 'string';
|
|
21
|
+
}
|
|
22
|
+
export interface ApiHeaderContract {
|
|
23
|
+
request?: Readonly<Record<string, ApiHeader>>;
|
|
24
|
+
responses?: Readonly<Record<number, {
|
|
25
|
+
description?: string;
|
|
26
|
+
errors?: readonly {
|
|
27
|
+
code: string;
|
|
28
|
+
message: string;
|
|
29
|
+
}[];
|
|
30
|
+
headers?: Readonly<Record<string, ApiHeader>>;
|
|
31
|
+
}>>;
|
|
32
|
+
}
|
|
33
|
+
/** A named event and its canonical payload, before SSE wire framing. */
|
|
34
|
+
export interface ApiSseEvent<TName extends string = string, TData = unknown> {
|
|
35
|
+
event: TName;
|
|
36
|
+
data: TData;
|
|
37
|
+
}
|
|
38
|
+
/** Marks an HTTP response body as a shared API fixture contract. */
|
|
39
|
+
export declare function apiResponse<const TKey extends string, TBody>(_key: TKey, body: TBody): TBody;
|
|
40
|
+
/** Marks a request body as a shared API fixture contract. */
|
|
41
|
+
export declare function apiRequest<const TKey extends string, TBody>(_key: TKey, body: TBody): TBody;
|
|
42
|
+
/** Marks a fixture-backed response that intentionally has no response body. */
|
|
43
|
+
export declare function apiNoContent<const TKey extends string>(_key: TKey): void;
|
|
44
|
+
/** Documents a helper-emitted fixed-status bodyless response for OpenAPI discovery only. */
|
|
45
|
+
export declare function apiOpenApiNoContent<const TKey extends string>(_key: TKey, _status: number): void;
|
|
46
|
+
/** Documents a fixed-media raw response for OpenAPI discovery without changing the payload. */
|
|
47
|
+
export declare function apiOpenApiRawResponse<const TKey extends string, TBody>(_key: TKey, _mediaType: string, body: TBody): TBody;
|
|
48
|
+
/** Marks a request schema without constructing a runtime body. */
|
|
49
|
+
export declare function apiRequestContract<const TKey extends string, TBody>(_key: TKey): TBody | undefined;
|
|
50
|
+
/** Marks a route that intentionally reads no meaningful request body. */
|
|
51
|
+
export declare function apiNoRequestBody<const TKey extends string>(_key: TKey): void;
|
|
52
|
+
/**
|
|
53
|
+
* Marks the query contract for one operation. Sources are opaque here; applications that need
|
|
54
|
+
* compile-time carrier validation re-declare this with their own constrained signature.
|
|
55
|
+
*/
|
|
56
|
+
export declare function apiQuery<const TKey extends string, const TSources extends readonly unknown[]>(_key: TKey, ..._sources: TSources): void;
|
|
57
|
+
/** Marks request and response headers for OpenAPI discovery (documentation only). */
|
|
58
|
+
export declare function apiHeaders<const TKey extends string>(_key: TKey, _contract: ApiHeaderContract): void;
|
|
59
|
+
/** Formats an SSE frame and exposes its payload to the compiler. */
|
|
60
|
+
export declare function apiSseFrame<const TKey extends string, const TEvent extends ApiSseEvent>(_key: TKey, event: TEvent): string;
|
|
61
|
+
/** Annotates SDK response variants while preserving status, headers, bytes and body ownership. */
|
|
62
|
+
export declare function apiOpenApiHttpResponse<const TKey extends string, TVariants extends ApiHttpResponseVariant>(_key: TKey, response: ApiHttpResponse<TVariants>): ApiHttpResponse<TVariants>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Identity markers read by a static (TypeScript compiler) contract extractor. None of them change
|
|
3
|
+
* the runtime payload; the string key names the operation in the extracted contract.
|
|
4
|
+
*/
|
|
5
|
+
/** Marks an HTTP response body as a shared API fixture contract. */
|
|
6
|
+
export function apiResponse(_key, body) {
|
|
7
|
+
return body;
|
|
8
|
+
}
|
|
9
|
+
/** Marks a request body as a shared API fixture contract. */
|
|
10
|
+
export function apiRequest(_key, body) {
|
|
11
|
+
return body;
|
|
12
|
+
}
|
|
13
|
+
/** Marks a fixture-backed response that intentionally has no response body. */
|
|
14
|
+
export function apiNoContent(_key) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
/** Documents a helper-emitted fixed-status bodyless response for OpenAPI discovery only. */
|
|
18
|
+
export function apiOpenApiNoContent(_key, _status) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
/** Documents a fixed-media raw response for OpenAPI discovery without changing the payload. */
|
|
22
|
+
export function apiOpenApiRawResponse(_key, _mediaType, body) {
|
|
23
|
+
return body;
|
|
24
|
+
}
|
|
25
|
+
/** Marks a request schema without constructing a runtime body. */
|
|
26
|
+
export function apiRequestContract(_key) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
/** Marks a route that intentionally reads no meaningful request body. */
|
|
30
|
+
export function apiNoRequestBody(_key) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Marks the query contract for one operation. Sources are opaque here; applications that need
|
|
35
|
+
* compile-time carrier validation re-declare this with their own constrained signature.
|
|
36
|
+
*/
|
|
37
|
+
export function apiQuery(_key, ..._sources) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
/** Marks request and response headers for OpenAPI discovery (documentation only). */
|
|
41
|
+
export function apiHeaders(_key, _contract) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
/** Formats an SSE frame and exposes its payload to the compiler. */
|
|
45
|
+
export function apiSseFrame(_key, event) {
|
|
46
|
+
return `event: ${event.event}\ndata: ${JSON.stringify(event.data)}\n\n`;
|
|
47
|
+
}
|
|
48
|
+
/** Annotates SDK response variants while preserving status, headers, bytes and body ownership. */
|
|
49
|
+
export function apiOpenApiHttpResponse(_key, response) {
|
|
50
|
+
return response;
|
|
51
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two-level memoizing cache: a WeakMap fast path keyed on an object's identity (for example a
|
|
3
|
+
* binding stable for a process or isolate lifetime) over a Map keyed on a secondary value, falling
|
|
4
|
+
* through to a content-keyed Map. `getContentKey` is a thunk so an identity hit skips deriving it.
|
|
5
|
+
* Entries are never evicted, so callers must pass a small, bounded set of key-material combinations.
|
|
6
|
+
*/
|
|
7
|
+
export declare class ContentAndIdentityCache<TIdentityKey extends object, TSubKey, TValue> {
|
|
8
|
+
private readonly byContent;
|
|
9
|
+
private readonly byIdentity;
|
|
10
|
+
getOrCompute(identityKey: TIdentityKey | undefined, subKey: TSubKey, getContentKey: () => string, compute: () => TValue): TValue;
|
|
11
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two-level memoizing cache: a WeakMap fast path keyed on an object's identity (for example a
|
|
3
|
+
* binding stable for a process or isolate lifetime) over a Map keyed on a secondary value, falling
|
|
4
|
+
* through to a content-keyed Map. `getContentKey` is a thunk so an identity hit skips deriving it.
|
|
5
|
+
* Entries are never evicted, so callers must pass a small, bounded set of key-material combinations.
|
|
6
|
+
*/
|
|
7
|
+
export class ContentAndIdentityCache {
|
|
8
|
+
byContent = new Map();
|
|
9
|
+
byIdentity = new WeakMap();
|
|
10
|
+
getOrCompute(identityKey, subKey, getContentKey, compute) {
|
|
11
|
+
const identityMap = identityKey && this.byIdentity.get(identityKey);
|
|
12
|
+
if (identityMap?.has(subKey))
|
|
13
|
+
return identityMap.get(subKey);
|
|
14
|
+
const contentKey = getContentKey();
|
|
15
|
+
const value = this.byContent.has(contentKey)
|
|
16
|
+
? this.byContent.get(contentKey)
|
|
17
|
+
: compute();
|
|
18
|
+
this.byContent.set(contentKey, value);
|
|
19
|
+
if (identityKey) {
|
|
20
|
+
const map = identityMap || new Map();
|
|
21
|
+
map.set(subKey, value);
|
|
22
|
+
this.byIdentity.set(identityKey, map);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
}
|
package/dist/env-contract.d.mts
CHANGED
|
@@ -17,3 +17,11 @@ export declare function normalizeEnvContractGroups<Source extends string, Surfac
|
|
|
17
17
|
export declare function envNamesForSurface<Source extends string, Surface extends string>(contract: readonly EnvVarContract<Source, Surface>[], surface: Surface): readonly string[];
|
|
18
18
|
export declare function envContractsByName<Source extends string, Surface extends string>(contract: readonly EnvVarContract<Source, Surface>[]): ReadonlyMap<string, readonly EnvVarContract<Source, Surface>[]>;
|
|
19
19
|
export declare function envNamesBySensitivity<Source extends string, Surface extends string>(contract: readonly EnvVarContract<Source, Surface>[], sensitivity: EnvVarSensitivity): readonly string[];
|
|
20
|
+
export type EnvContractManifestEntry<Source extends string = string, Surface extends string = string> = {
|
|
21
|
+
name: string;
|
|
22
|
+
contractKey: string;
|
|
23
|
+
sourceOfTruth: Source;
|
|
24
|
+
sensitivity: EnvVarSensitivity;
|
|
25
|
+
runtimeSurfaces: Surface[];
|
|
26
|
+
};
|
|
27
|
+
export declare function envContractManifest<Source extends string, Surface extends string>(contract: readonly EnvVarContract<Source, Surface>[]): EnvContractManifestEntry<Source, Surface>[];
|
package/dist/env-contract.mjs
CHANGED
|
@@ -43,3 +43,12 @@ export function envNamesBySensitivity(contract, sensitivity) {
|
|
|
43
43
|
...new Set(contract.filter((entry) => entry.sensitivity === sensitivity).map((entry) => entry.name)),
|
|
44
44
|
].toSorted();
|
|
45
45
|
}
|
|
46
|
+
export function envContractManifest(contract) {
|
|
47
|
+
return contract.map((entry) => ({
|
|
48
|
+
name: entry.name,
|
|
49
|
+
contractKey: entry.key,
|
|
50
|
+
sourceOfTruth: entry.sourceOfTruth,
|
|
51
|
+
sensitivity: entry.sensitivity,
|
|
52
|
+
runtimeSurfaces: [...entry.surfaces],
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const REQUEST_BODY_SPAN_ATTRIBUTE = "http.request.body.data";
|
|
2
|
+
/** Drop the request body attribute from span or breadcrumb data. */
|
|
3
|
+
export declare function dropSpanRequestBody<T extends Record<string, unknown>>(data: T): T;
|
|
4
|
+
/** Drop the body (`data`) from an event request. */
|
|
5
|
+
export declare function dropRequestBody<T extends object>(request: T | undefined): T | undefined;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { DROP, transformRecord } from './observability-records.mjs';
|
|
2
|
+
export const REQUEST_BODY_SPAN_ATTRIBUTE = 'http.request.body.data';
|
|
3
|
+
/** Drop the request body attribute from span or breadcrumb data. */
|
|
4
|
+
export function dropSpanRequestBody(data) {
|
|
5
|
+
return transformRecord(data, (key, value) => (key === REQUEST_BODY_SPAN_ATTRIBUTE ? DROP : value));
|
|
6
|
+
}
|
|
7
|
+
/** Drop the body (`data`) from an event request. */
|
|
8
|
+
export function dropRequestBody(request) {
|
|
9
|
+
return request && transformRecord(request, (key, value) => (key === 'data' ? DROP : value));
|
|
10
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Replace the values of credential headers (case-insensitive exact names). */
|
|
2
|
+
export declare function scrubCredentialHeaders(headers: Record<string, unknown>, credentialHeaders: readonly string[]): Record<string, unknown>;
|
|
3
|
+
/** Replace credential header span attributes (hyphen or underscore spelling) and cookie attributes. */
|
|
4
|
+
export declare function scrubSpanCredentialAttributes<T extends Record<string, unknown>>(data: T, credentialHeaders: readonly string[]): T;
|
|
5
|
+
/** Replace credential headers and every cookie value on an event request. */
|
|
6
|
+
export declare function scrubRequestCredentialFields<T extends object>(request: T | undefined, credentialHeaders: readonly string[]): T | undefined;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { SENSITIVE_VALUE, isRecord, transformRecord } from './observability-records.mjs';
|
|
2
|
+
const COOKIE_ATTRIBUTE_PREFIX = 'http.request.header.cookie.';
|
|
3
|
+
function lowerSet(names) {
|
|
4
|
+
return new Set(names.map((name) => name.toLowerCase()));
|
|
5
|
+
}
|
|
6
|
+
/** Replace the values of credential headers (case-insensitive exact names). */
|
|
7
|
+
export function scrubCredentialHeaders(headers, credentialHeaders) {
|
|
8
|
+
const credentials = lowerSet(credentialHeaders);
|
|
9
|
+
return transformRecord(headers, (name, value) => credentials.has(name.toLowerCase()) ? SENSITIVE_VALUE : value);
|
|
10
|
+
}
|
|
11
|
+
/** Replace credential header span attributes (hyphen or underscore spelling) and cookie attributes. */
|
|
12
|
+
export function scrubSpanCredentialAttributes(data, credentialHeaders) {
|
|
13
|
+
const keys = new Set([...lowerSet(credentialHeaders)].flatMap((name) => [
|
|
14
|
+
`http.request.header.${name}`,
|
|
15
|
+
`http.request.header.${name.replaceAll('-', '_')}`,
|
|
16
|
+
]));
|
|
17
|
+
return transformRecord(data, (key, value) => keys.has(key) || key.startsWith(COOKIE_ATTRIBUTE_PREFIX) ? SENSITIVE_VALUE : value);
|
|
18
|
+
}
|
|
19
|
+
/** Replace credential headers and every cookie value on an event request. */
|
|
20
|
+
export function scrubRequestCredentialFields(request, credentialHeaders) {
|
|
21
|
+
if (!request)
|
|
22
|
+
return request;
|
|
23
|
+
return transformRecord(request, (key, value) => {
|
|
24
|
+
if (key === 'headers' && isRecord(value))
|
|
25
|
+
return scrubCredentialHeaders(value, credentialHeaders);
|
|
26
|
+
return key === 'cookies' && isRecord(value)
|
|
27
|
+
? transformRecord(value, () => SENSITIVE_VALUE)
|
|
28
|
+
: value;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type UrlScrubOptions } from './observability-url.mts';
|
|
2
|
+
export interface ScrubOptions extends UrlScrubOptions {
|
|
3
|
+
/** Header names (case-insensitive) whose values are replaced with the filtered marker. */
|
|
4
|
+
credentialHeaders: readonly string[];
|
|
5
|
+
/** Also drop `request.data` and the `http.request.body.data` attribute. Defaults to false. */
|
|
6
|
+
dropRequestBody?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Structural subset of an SDK event; no SDK type is imported. */
|
|
9
|
+
export interface ObservableEvent {
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
breadcrumbs?: Array<{
|
|
12
|
+
data?: Record<string, unknown>;
|
|
13
|
+
}>;
|
|
14
|
+
request?: object;
|
|
15
|
+
}
|
|
16
|
+
export declare function scrubHeaders(headers: Record<string, unknown>, credentialHeaders: readonly string[], redactUrl?: (url: string) => string): Record<string, unknown>;
|
|
17
|
+
export declare function scrubSpanAttributes<T extends Record<string, unknown>>(data: T, options: ScrubOptions): T;
|
|
18
|
+
export declare function scrubRequest<T extends object>(request: T | undefined, options: ScrubOptions): T | undefined;
|
|
19
|
+
/** Scrub each breadcrumb's `data`; the input array is returned when nothing changed. */
|
|
20
|
+
export declare function scrubBreadcrumbs<T extends {
|
|
21
|
+
data?: Record<string, unknown>;
|
|
22
|
+
}>(breadcrumbs: T[] | undefined, options: ScrubOptions): T[] | undefined;
|
|
23
|
+
/** Scrub an event's request and breadcrumbs; the input event is returned when nothing changed. */
|
|
24
|
+
export declare function scrubEvent<T extends ObservableEvent>(event: T, options: ScrubOptions): T;
|
|
25
|
+
type BeforeSendResult<T> = T | null | PromiseLike<T | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Wrap an SDK `beforeSend` so its result (sync or async, including foreign-realm thenables) always
|
|
28
|
+
* passes through a final scrubber. Pass `ScrubOptions` to use {@link scrubEvent}, or a function.
|
|
29
|
+
*/
|
|
30
|
+
export declare function composeBeforeSend<T extends ObservableEvent, THint>(beforeSend: ((event: T, hint: THint) => BeforeSendResult<T>) | undefined, scrubber: ScrubOptions | ((event: T) => T)): (event: T, hint: THint) => BeforeSendResult<T>;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { dropRequestBody, dropSpanRequestBody } from './observability-body.mjs';
|
|
2
|
+
import { scrubCredentialHeaders, scrubRequestCredentialFields, scrubSpanCredentialAttributes, } from './observability-credentials.mjs';
|
|
3
|
+
import { scrubHeaderUrls, scrubRequestUrlFields, scrubSpanUrlAttributes, } from './observability-url.mjs';
|
|
4
|
+
export function scrubHeaders(headers, credentialHeaders, redactUrl) {
|
|
5
|
+
const urlScrubbed = scrubHeaderUrls(headers, redactUrl ? { redactUrl } : {});
|
|
6
|
+
return scrubCredentialHeaders(urlScrubbed, credentialHeaders);
|
|
7
|
+
}
|
|
8
|
+
export function scrubSpanAttributes(data, options) {
|
|
9
|
+
const scrubbed = scrubSpanCredentialAttributes(scrubSpanUrlAttributes(data, options), options.credentialHeaders);
|
|
10
|
+
return options.dropRequestBody ? dropSpanRequestBody(scrubbed) : scrubbed;
|
|
11
|
+
}
|
|
12
|
+
export function scrubRequest(request, options) {
|
|
13
|
+
const scrubbed = scrubRequestCredentialFields(scrubRequestUrlFields(request, options), options.credentialHeaders);
|
|
14
|
+
return options.dropRequestBody ? dropRequestBody(scrubbed) : scrubbed;
|
|
15
|
+
}
|
|
16
|
+
/** Scrub each breadcrumb's `data`; the input array is returned when nothing changed. */
|
|
17
|
+
export function scrubBreadcrumbs(breadcrumbs, options) {
|
|
18
|
+
if (!breadcrumbs?.length)
|
|
19
|
+
return breadcrumbs;
|
|
20
|
+
const result = breadcrumbs.map((breadcrumb) => {
|
|
21
|
+
if (typeof breadcrumb.data !== 'object' || breadcrumb.data === null)
|
|
22
|
+
return breadcrumb;
|
|
23
|
+
const data = scrubSpanAttributes(breadcrumb.data, options);
|
|
24
|
+
return data === breadcrumb.data ? breadcrumb : { ...breadcrumb, data };
|
|
25
|
+
});
|
|
26
|
+
return result.some((breadcrumb, index) => breadcrumb !== breadcrumbs[index])
|
|
27
|
+
? result
|
|
28
|
+
: breadcrumbs;
|
|
29
|
+
}
|
|
30
|
+
/** Scrub an event's request and breadcrumbs; the input event is returned when nothing changed. */
|
|
31
|
+
export function scrubEvent(event, options) {
|
|
32
|
+
const request = scrubRequest(event.request, options);
|
|
33
|
+
const breadcrumbs = scrubBreadcrumbs(event.breadcrumbs, options);
|
|
34
|
+
if (request === event.request && breadcrumbs === event.breadcrumbs)
|
|
35
|
+
return event;
|
|
36
|
+
return {
|
|
37
|
+
...event,
|
|
38
|
+
...(request === event.request ? {} : { request }),
|
|
39
|
+
...(breadcrumbs === event.breadcrumbs ? {} : { breadcrumbs }),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function isPromiseLike(value) {
|
|
43
|
+
if (value === null || (typeof value !== 'object' && typeof value !== 'function'))
|
|
44
|
+
return false;
|
|
45
|
+
return typeof value.then === 'function';
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Wrap an SDK `beforeSend` so its result (sync or async, including foreign-realm thenables) always
|
|
49
|
+
* passes through a final scrubber. Pass `ScrubOptions` to use {@link scrubEvent}, or a function.
|
|
50
|
+
*/
|
|
51
|
+
export function composeBeforeSend(beforeSend, scrubber) {
|
|
52
|
+
const finalScrub = typeof scrubber === 'function' ? scrubber : (e) => scrubEvent(e, scrubber);
|
|
53
|
+
return (event, hint) => {
|
|
54
|
+
const result = beforeSend ? beforeSend(event, hint) : event;
|
|
55
|
+
if (isPromiseLike(result))
|
|
56
|
+
return Promise.resolve(result).then((next) => (next === null ? null : finalScrub(next)));
|
|
57
|
+
return result === null ? null : finalScrub(result);
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const SENSITIVE_VALUE = "[Filtered]";
|
|
2
|
+
/** Return from a {@link transformRecord} callback to omit the entry. */
|
|
3
|
+
export declare const DROP: unique symbol;
|
|
4
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
5
|
+
/**
|
|
6
|
+
* Copy-on-write entry mapping: returns the input when no entry changed. Rebuilds with
|
|
7
|
+
* `Object.fromEntries` so keys such as `__proto__` stay own data properties.
|
|
8
|
+
*/
|
|
9
|
+
export declare function transformRecord<T extends object>(record: T, transform: (key: string, value: unknown) => unknown): T;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const SENSITIVE_VALUE = '[Filtered]';
|
|
2
|
+
/** Return from a {@link transformRecord} callback to omit the entry. */
|
|
3
|
+
export const DROP = Symbol('drop');
|
|
4
|
+
export function isRecord(value) {
|
|
5
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Copy-on-write entry mapping: returns the input when no entry changed. Rebuilds with
|
|
9
|
+
* `Object.fromEntries` so keys such as `__proto__` stay own data properties.
|
|
10
|
+
*/
|
|
11
|
+
export function transformRecord(record, transform) {
|
|
12
|
+
let changed = false;
|
|
13
|
+
const entries = [];
|
|
14
|
+
for (const [key, value] of Object.entries(record)) {
|
|
15
|
+
const next = transform(key, value);
|
|
16
|
+
if (next === DROP) {
|
|
17
|
+
changed = true;
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
if (!Object.is(next, value))
|
|
21
|
+
changed = true;
|
|
22
|
+
entries.push([key, next]);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return changed ? Object.fromEntries(entries) : record;
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface UrlScrubOptions {
|
|
2
|
+
/** Extra caller-owned redaction applied after the query string and fragment are stripped. */
|
|
3
|
+
redactUrl?: (url: string) => string;
|
|
4
|
+
}
|
|
5
|
+
/** Span, breadcrumb, and request attribute keys whose string value is a URL. */
|
|
6
|
+
export declare const URL_ATTRIBUTE_KEYS: readonly string[];
|
|
7
|
+
/** Attribute keys whose value is only a query string or fragment; they are dropped. */
|
|
8
|
+
export declare const URL_COMPONENT_ATTRIBUTE_KEYS: readonly string[];
|
|
9
|
+
/** Every attribute key the URL scrubber rewrites or drops. */
|
|
10
|
+
export declare const SCRUBBED_URL_ATTRIBUTE_KEYS: readonly string[];
|
|
11
|
+
export declare function stripUrlQueryAndFragment(value: string): string;
|
|
12
|
+
/** Scrub a string or each string item of an array; other values and clean arrays pass through. */
|
|
13
|
+
export declare function scrubUrlValue(value: unknown, options?: UrlScrubOptions): unknown;
|
|
14
|
+
/** Strip query strings and fragments from URL attributes and drop query/fragment-only keys. */
|
|
15
|
+
export declare function scrubSpanUrlAttributes<T extends Record<string, unknown>>(data: T, options?: UrlScrubOptions): T;
|
|
16
|
+
/** Scrub the `referer`/`referrer` header values of a headers record. */
|
|
17
|
+
export declare function scrubHeaderUrls(headers: Record<string, unknown>, options?: UrlScrubOptions): Record<string, unknown>;
|
|
18
|
+
/** Drop `query_string`, strip `url`, and scrub URL-bearing headers on an event request. */
|
|
19
|
+
export declare function scrubRequestUrlFields<T extends object>(request: T | undefined, options?: UrlScrubOptions): T | undefined;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { DROP, isRecord, transformRecord } from './observability-records.mjs';
|
|
2
|
+
/** Span, breadcrumb, and request attribute keys whose string value is a URL. */
|
|
3
|
+
export const URL_ATTRIBUTE_KEYS = Object.freeze([
|
|
4
|
+
'url',
|
|
5
|
+
'url.full',
|
|
6
|
+
'http.url',
|
|
7
|
+
'http.target',
|
|
8
|
+
'http.request.header.referer',
|
|
9
|
+
'http.request.header.referrer',
|
|
10
|
+
]);
|
|
11
|
+
/** Attribute keys whose value is only a query string or fragment; they are dropped. */
|
|
12
|
+
export const URL_COMPONENT_ATTRIBUTE_KEYS = Object.freeze([
|
|
13
|
+
'url.query',
|
|
14
|
+
'http.query',
|
|
15
|
+
'url.fragment',
|
|
16
|
+
'http.fragment',
|
|
17
|
+
]);
|
|
18
|
+
/** Every attribute key the URL scrubber rewrites or drops. */
|
|
19
|
+
export const SCRUBBED_URL_ATTRIBUTE_KEYS = Object.freeze([
|
|
20
|
+
...URL_ATTRIBUTE_KEYS,
|
|
21
|
+
...URL_COMPONENT_ATTRIBUTE_KEYS,
|
|
22
|
+
]);
|
|
23
|
+
export function stripUrlQueryAndFragment(value) {
|
|
24
|
+
const match = /[?#]/.exec(value);
|
|
25
|
+
return match ? value.slice(0, match.index) : value;
|
|
26
|
+
}
|
|
27
|
+
function scrubUrl(value, options) {
|
|
28
|
+
const stripped = stripUrlQueryAndFragment(value);
|
|
29
|
+
return options.redactUrl ? options.redactUrl(stripped) : stripped;
|
|
30
|
+
}
|
|
31
|
+
/** Scrub a string or each string item of an array; other values and clean arrays pass through. */
|
|
32
|
+
export function scrubUrlValue(value, options = {}) {
|
|
33
|
+
if (typeof value === 'string')
|
|
34
|
+
return scrubUrl(value, options);
|
|
35
|
+
if (!Array.isArray(value))
|
|
36
|
+
return value;
|
|
37
|
+
const items = value.map((item) => typeof item === 'string' ? scrubUrl(item, options) : item);
|
|
38
|
+
return items.some((item, index) => !Object.is(item, value[index])) ? items : value;
|
|
39
|
+
}
|
|
40
|
+
function isUrlHeader(name) {
|
|
41
|
+
const lower = name.toLowerCase();
|
|
42
|
+
return lower === 'referer' || lower === 'referrer';
|
|
43
|
+
}
|
|
44
|
+
/** Strip query strings and fragments from URL attributes and drop query/fragment-only keys. */
|
|
45
|
+
export function scrubSpanUrlAttributes(data, options = {}) {
|
|
46
|
+
return transformRecord(data, (key, value) => {
|
|
47
|
+
if (URL_COMPONENT_ATTRIBUTE_KEYS.includes(key))
|
|
48
|
+
return DROP;
|
|
49
|
+
return URL_ATTRIBUTE_KEYS.includes(key) && typeof value === 'string'
|
|
50
|
+
? scrubUrl(value, options)
|
|
51
|
+
: value;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** Scrub the `referer`/`referrer` header values of a headers record. */
|
|
55
|
+
export function scrubHeaderUrls(headers, options = {}) {
|
|
56
|
+
return transformRecord(headers, (name, value) => isUrlHeader(name) ? scrubUrlValue(value, options) : value);
|
|
57
|
+
}
|
|
58
|
+
/** Drop `query_string`, strip `url`, and scrub URL-bearing headers on an event request. */
|
|
59
|
+
export function scrubRequestUrlFields(request, options = {}) {
|
|
60
|
+
if (!request)
|
|
61
|
+
return request;
|
|
62
|
+
return transformRecord(request, (key, value) => {
|
|
63
|
+
if (key === 'query_string')
|
|
64
|
+
return DROP;
|
|
65
|
+
if (key === 'url' && typeof value === 'string')
|
|
66
|
+
return scrubUrl(value, options);
|
|
67
|
+
return key === 'headers' && isRecord(value) ? scrubHeaderUrls(value, options) : value;
|
|
68
|
+
});
|
|
69
|
+
}
|
package/dist/observability.d.mts
CHANGED
|
@@ -1,19 +1,8 @@
|
|
|
1
|
-
export
|
|
1
|
+
export { SENSITIVE_VALUE } from './observability-records.mts';
|
|
2
2
|
export { createSpikeWindowTracker, type SpikeWindowTracker } from './spike-window.mts';
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
}
|
|
6
|
-
export
|
|
7
|
-
[key: string]: unknown;
|
|
8
|
-
breadcrumbs?: Array<{
|
|
9
|
-
data?: Record<string, unknown>;
|
|
10
|
-
}>;
|
|
11
|
-
request?: object;
|
|
12
|
-
}
|
|
13
|
-
export declare function stripUrlQueryAndFragment(value: string): string;
|
|
14
|
-
export declare function scrubHeaders(headers: Record<string, unknown>, credentialHeaders: readonly string[]): Record<string, unknown>;
|
|
15
|
-
export declare function scrubSpanAttributes<T extends Record<string, unknown>>(data: T, options: ScrubOptions): T;
|
|
16
|
-
export declare function scrubEvent<T extends ObservableEvent>(event: T, options: ScrubOptions): T;
|
|
17
|
-
export declare function composeBeforeSend<T extends ObservableEvent, THint>(beforeSend: ((event: T, hint: THint) => T | null | Promise<T | null>) | undefined, options: ScrubOptions): (event: T, hint: THint) => T | null | Promise<T | null>;
|
|
3
|
+
export { composeBeforeSend, scrubBreadcrumbs, scrubEvent, scrubHeaders, scrubRequest, scrubSpanAttributes, type ObservableEvent, type ScrubOptions, } from './observability-event.mts';
|
|
4
|
+
export { scrubCredentialHeaders, scrubRequestCredentialFields, scrubSpanCredentialAttributes, } from './observability-credentials.mts';
|
|
5
|
+
export { SCRUBBED_URL_ATTRIBUTE_KEYS, URL_ATTRIBUTE_KEYS, URL_COMPONENT_ATTRIBUTE_KEYS, scrubHeaderUrls, scrubRequestUrlFields, scrubSpanUrlAttributes, scrubUrlValue, stripUrlQueryAndFragment, type UrlScrubOptions, } from './observability-url.mts';
|
|
6
|
+
export { REQUEST_BODY_SPAN_ATTRIBUTE, dropRequestBody, dropSpanRequestBody, } from './observability-body.mts';
|
|
18
7
|
export declare function isAllowedEnvironment(value: string | undefined, allowed: readonly string[]): boolean;
|
|
19
8
|
export declare function shouldReportSpike(count: number, threshold: number): boolean;
|
package/dist/observability.mjs
CHANGED
|
@@ -1,121 +1,12 @@
|
|
|
1
|
-
export
|
|
1
|
+
export { SENSITIVE_VALUE } from './observability-records.mjs';
|
|
2
2
|
export { createSpikeWindowTracker } from './spike-window.mjs';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
'http.target',
|
|
8
|
-
'http.request.header.referer',
|
|
9
|
-
'http.request.header.referrer',
|
|
10
|
-
];
|
|
11
|
-
const URL_COMPONENT_ATTRIBUTES = ['url.query', 'http.query', 'url.fragment', 'http.fragment'];
|
|
12
|
-
export function stripUrlQueryAndFragment(value) {
|
|
13
|
-
const match = /[?#]/.exec(value);
|
|
14
|
-
return match ? value.slice(0, match.index) : value;
|
|
15
|
-
}
|
|
16
|
-
export function scrubHeaders(headers, credentialHeaders) {
|
|
17
|
-
const credentials = new Set(credentialHeaders.map((name) => name.toLowerCase()));
|
|
18
|
-
return Object.fromEntries(Object.entries(headers).map(([name, value]) => [
|
|
19
|
-
name,
|
|
20
|
-
credentials.has(name.toLowerCase())
|
|
21
|
-
? SENSITIVE_VALUE
|
|
22
|
-
: isUrlHeader(name)
|
|
23
|
-
? scrubUrlHeaderValue(value)
|
|
24
|
-
: value,
|
|
25
|
-
]));
|
|
26
|
-
}
|
|
27
|
-
export function scrubSpanAttributes(data, options) {
|
|
28
|
-
const credentialKeys = new Set(options.credentialHeaders.flatMap((name) => {
|
|
29
|
-
const lower = name.toLowerCase();
|
|
30
|
-
return [`http.request.header.${lower}`, `http.request.header.${lower.replaceAll('-', '_')}`];
|
|
31
|
-
}));
|
|
32
|
-
const result = Object.entries(data).flatMap(([key, value]) => {
|
|
33
|
-
if (URL_COMPONENT_ATTRIBUTES.includes(key))
|
|
34
|
-
return [];
|
|
35
|
-
if (credentialKeys.has(key) || key.startsWith('http.request.header.cookie.'))
|
|
36
|
-
return [[key, SENSITIVE_VALUE]];
|
|
37
|
-
return [
|
|
38
|
-
[
|
|
39
|
-
key,
|
|
40
|
-
URL_ATTRIBUTES.includes(key) && typeof value === 'string'
|
|
41
|
-
? stripUrlQueryAndFragment(value)
|
|
42
|
-
: value,
|
|
43
|
-
],
|
|
44
|
-
];
|
|
45
|
-
});
|
|
46
|
-
const changed = result.length !== Object.keys(data).length || result.some(([key, value]) => data[key] !== value);
|
|
47
|
-
return (changed ? Object.fromEntries(result) : data);
|
|
48
|
-
}
|
|
49
|
-
export function scrubEvent(event, options) {
|
|
50
|
-
const request = scrubRequest(event.request, options);
|
|
51
|
-
const breadcrumbs = event.breadcrumbs?.map((breadcrumb) => breadcrumb.data
|
|
52
|
-
? { ...breadcrumb, data: scrubSpanAttributes(breadcrumb.data, options) }
|
|
53
|
-
: breadcrumb);
|
|
54
|
-
const changed = request !== event.request ||
|
|
55
|
-
breadcrumbs?.some((breadcrumb, index) => breadcrumb !== event.breadcrumbs?.[index]);
|
|
56
|
-
return changed
|
|
57
|
-
? {
|
|
58
|
-
...event,
|
|
59
|
-
...(request === event.request ? {} : { request }),
|
|
60
|
-
...(breadcrumbs === event.breadcrumbs ? {} : { breadcrumbs }),
|
|
61
|
-
}
|
|
62
|
-
: event;
|
|
63
|
-
}
|
|
64
|
-
export function composeBeforeSend(beforeSend, options) {
|
|
65
|
-
return (event, hint) => {
|
|
66
|
-
const result = beforeSend ? beforeSend(event, hint) : event;
|
|
67
|
-
if (isPromiseLike(result))
|
|
68
|
-
return Promise.resolve(result).then((next) => next === null ? null : scrubEvent(next, options));
|
|
69
|
-
return result === null ? null : scrubEvent(result, options);
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
function isPromiseLike(value) {
|
|
73
|
-
return value !== null && typeof value.then === 'function';
|
|
74
|
-
}
|
|
3
|
+
export { composeBeforeSend, scrubBreadcrumbs, scrubEvent, scrubHeaders, scrubRequest, scrubSpanAttributes, } from './observability-event.mjs';
|
|
4
|
+
export { scrubCredentialHeaders, scrubRequestCredentialFields, scrubSpanCredentialAttributes, } from './observability-credentials.mjs';
|
|
5
|
+
export { SCRUBBED_URL_ATTRIBUTE_KEYS, URL_ATTRIBUTE_KEYS, URL_COMPONENT_ATTRIBUTE_KEYS, scrubHeaderUrls, scrubRequestUrlFields, scrubSpanUrlAttributes, scrubUrlValue, stripUrlQueryAndFragment, } from './observability-url.mjs';
|
|
6
|
+
export { REQUEST_BODY_SPAN_ATTRIBUTE, dropRequestBody, dropSpanRequestBody, } from './observability-body.mjs';
|
|
75
7
|
export function isAllowedEnvironment(value, allowed) {
|
|
76
8
|
return value !== undefined && allowed.includes(value);
|
|
77
9
|
}
|
|
78
10
|
export function shouldReportSpike(count, threshold) {
|
|
79
11
|
return Number.isFinite(count) && Number.isFinite(threshold) && count >= threshold;
|
|
80
12
|
}
|
|
81
|
-
function scrubRequest(request, options) {
|
|
82
|
-
if (!request)
|
|
83
|
-
return request;
|
|
84
|
-
const fields = request;
|
|
85
|
-
const headers = isRecord(fields.headers)
|
|
86
|
-
? scrubHeaders(fields.headers, options.credentialHeaders)
|
|
87
|
-
: fields.headers;
|
|
88
|
-
const cookies = isRecord(fields.cookies)
|
|
89
|
-
? Object.fromEntries(Object.keys(fields.cookies).map((name) => [name, SENSITIVE_VALUE]))
|
|
90
|
-
: fields.cookies;
|
|
91
|
-
const result = Object.entries(fields).flatMap(([key, value]) => key === 'query_string'
|
|
92
|
-
? []
|
|
93
|
-
: [
|
|
94
|
-
[
|
|
95
|
-
key,
|
|
96
|
-
key === 'url' && typeof value === 'string'
|
|
97
|
-
? stripUrlQueryAndFragment(value)
|
|
98
|
-
: key === 'headers'
|
|
99
|
-
? headers
|
|
100
|
-
: key === 'cookies'
|
|
101
|
-
? cookies
|
|
102
|
-
: value,
|
|
103
|
-
],
|
|
104
|
-
]);
|
|
105
|
-
const changed = result.length !== Object.keys(fields).length ||
|
|
106
|
-
result.some(([key, value]) => fields[key] !== value);
|
|
107
|
-
return (changed ? Object.fromEntries(result) : request);
|
|
108
|
-
}
|
|
109
|
-
function isUrlHeader(name) {
|
|
110
|
-
return ['referer', 'referrer'].includes(name.toLowerCase());
|
|
111
|
-
}
|
|
112
|
-
function scrubUrlHeaderValue(value) {
|
|
113
|
-
if (typeof value === 'string')
|
|
114
|
-
return stripUrlQueryAndFragment(value);
|
|
115
|
-
return Array.isArray(value)
|
|
116
|
-
? value.map((item) => (typeof item === 'string' ? stripUrlQueryAndFragment(item) : item))
|
|
117
|
-
: value;
|
|
118
|
-
}
|
|
119
|
-
function isRecord(value) {
|
|
120
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
121
|
-
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type DependencyAuthorization = 'allow' | 'withheld' | 'unknown';
|
|
2
|
+
/** Empty manifests have no dependency bytes. Any withheld or unknown dependency denies delivery. */
|
|
3
|
+
export declare function authorizeDependencyStates(states: readonly DependencyAuthorization[]): 'allow' | 'deny';
|
|
4
|
+
export declare function normalizeHostname(hostname: string): string;
|
|
5
|
+
/** Hostname of a URL string or a bare host. Returns undefined when empty or unparsable. */
|
|
6
|
+
export declare function hostnameOf(value: string): string | undefined;
|
|
7
|
+
/** Route matcher: `<prefix><one non-empty segment>`. Returns the segment, or undefined. */
|
|
8
|
+
export declare function matchRouteSegment(pathname: string, prefix: string): string | undefined;
|
|
9
|
+
export type ProtectedHostPolicy = {
|
|
10
|
+
hosts: () => string[];
|
|
11
|
+
isProtected: (url: URL, extraHosts?: readonly string[]) => boolean;
|
|
12
|
+
blockedHosts: () => {
|
|
13
|
+
exact: string[];
|
|
14
|
+
suffixes: string[];
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Protected-host policy. Each entry (URL or bare host) protects the host and all subdomains.
|
|
19
|
+
* The caller supplies the host list; nothing is read from the environment.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createProtectedHostPolicy(entries: readonly string[]): ProtectedHostPolicy;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Empty manifests have no dependency bytes. Any withheld or unknown dependency denies delivery. */
|
|
2
|
+
export function authorizeDependencyStates(states) {
|
|
3
|
+
return states.every((state) => state === 'allow') ? 'allow' : 'deny';
|
|
4
|
+
}
|
|
5
|
+
export function normalizeHostname(hostname) {
|
|
6
|
+
return hostname.replace(/\.$/, '').toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
/** Hostname of a URL string or a bare host. Returns undefined when empty or unparsable. */
|
|
9
|
+
export function hostnameOf(value) {
|
|
10
|
+
if (value.includes('://')) {
|
|
11
|
+
try {
|
|
12
|
+
return normalizeHostname(new URL(value).hostname);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const host = value.trim();
|
|
19
|
+
return host ? normalizeHostname(host) : undefined;
|
|
20
|
+
}
|
|
21
|
+
/** Route matcher: `<prefix><one non-empty segment>`. Returns the segment, or undefined. */
|
|
22
|
+
export function matchRouteSegment(pathname, prefix) {
|
|
23
|
+
if (!pathname.startsWith(prefix))
|
|
24
|
+
return undefined;
|
|
25
|
+
const rest = pathname.slice(prefix.length);
|
|
26
|
+
return rest && !rest.includes('/') ? rest : undefined;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Protected-host policy. Each entry (URL or bare host) protects the host and all subdomains.
|
|
30
|
+
* The caller supplies the host list; nothing is read from the environment.
|
|
31
|
+
*/
|
|
32
|
+
export function createProtectedHostPolicy(entries) {
|
|
33
|
+
const collect = (extra = []) => {
|
|
34
|
+
const hosts = new Set();
|
|
35
|
+
for (const entry of [...entries, ...extra]) {
|
|
36
|
+
const host = hostnameOf(entry);
|
|
37
|
+
if (host)
|
|
38
|
+
hosts.add(host);
|
|
39
|
+
}
|
|
40
|
+
return hosts;
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
hosts: () => [...collect()],
|
|
44
|
+
isProtected(url, extraHosts) {
|
|
45
|
+
const host = normalizeHostname(url.hostname);
|
|
46
|
+
for (const protectedHost of collect(extraHosts)) {
|
|
47
|
+
if (host === protectedHost || host.endsWith(`.${protectedHost}`))
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
},
|
|
52
|
+
blockedHosts() {
|
|
53
|
+
const hosts = [...collect()];
|
|
54
|
+
return { exact: hosts, suffixes: hosts.map((host) => `.${host}`) };
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare function signPathWithKey(path: string, keyHex: string): string;
|
|
2
|
+
export declare function verifyPathWithKey(path: string, signature: string, keyHex: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Parse a comma-separated list of hex keys. Entries are trimmed; empty entries and
|
|
5
|
+
* case-insensitive placeholder entries are skipped.
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseSigningKeys(value: string | undefined, options?: {
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
}): string[];
|
|
10
|
+
/** Sign with the first usable key. Returns '' when no key is usable. */
|
|
11
|
+
export declare function signPath(path: string, keys: readonly string[]): string;
|
|
12
|
+
export type VerifyPathOptions = {
|
|
13
|
+
/** Accept any signature when no keys are configured (dev mode). Defaults to false. */
|
|
14
|
+
allowUnsignedWhenNoKeys?: boolean;
|
|
15
|
+
};
|
|
16
|
+
/** Verify against every key (for rotation). Invalid keys never match. */
|
|
17
|
+
export declare function verifyPathSignature(path: string, signature: string, keys: readonly string[], options?: VerifyPathOptions): boolean;
|
|
18
|
+
/** Sign a path bound to an expiry (epoch milliseconds). Returns '' when no key is usable. */
|
|
19
|
+
export declare function signExpiringPath(path: string, expiresAtMs: number, keys: readonly string[]): string;
|
|
20
|
+
/** Verify an expiring signature. Fails closed for unsafe expiry values and expired links. */
|
|
21
|
+
export declare function verifyExpiringPath(path: string, expiresAtMs: number, signature: string, keys: readonly string[], options?: {
|
|
22
|
+
now?: number;
|
|
23
|
+
}): boolean;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
export function signPathWithKey(path, keyHex) {
|
|
3
|
+
if (!/^[\da-f]{64}$/i.test(keyHex))
|
|
4
|
+
throw new Error('Signing keys must be 64 hexadecimal characters');
|
|
5
|
+
return createHmac('sha256', Buffer.from(keyHex, 'hex')).update(path).digest('hex');
|
|
6
|
+
}
|
|
7
|
+
export function verifyPathWithKey(path, signature, keyHex) {
|
|
8
|
+
const expected = signPathWithKey(path, keyHex);
|
|
9
|
+
if (expected.length !== signature.length || !/^[\da-f]+$/i.test(signature))
|
|
10
|
+
return false;
|
|
11
|
+
return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Parse a comma-separated list of hex keys. Entries are trimmed; empty entries and
|
|
15
|
+
* case-insensitive placeholder entries are skipped.
|
|
16
|
+
*/
|
|
17
|
+
export function parseSigningKeys(value, options = {}) {
|
|
18
|
+
if (!value)
|
|
19
|
+
return [];
|
|
20
|
+
const placeholder = (options.placeholder ?? 'PLACEHOLDER').toUpperCase();
|
|
21
|
+
return value.split(',').flatMap((entry) => {
|
|
22
|
+
const key = entry.trim();
|
|
23
|
+
return key && key.toUpperCase() !== placeholder ? [key] : [];
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** Sign with the first usable key. Returns '' when no key is usable. */
|
|
27
|
+
export function signPath(path, keys) {
|
|
28
|
+
for (const key of keys) {
|
|
29
|
+
try {
|
|
30
|
+
return signPathWithKey(path, key);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// Invalid key: try the next one.
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
/** Verify against every key (for rotation). Invalid keys never match. */
|
|
39
|
+
export function verifyPathSignature(path, signature, keys, options = {}) {
|
|
40
|
+
if (keys.length === 0)
|
|
41
|
+
return options.allowUnsignedWhenNoKeys === true;
|
|
42
|
+
return keys.some((key) => {
|
|
43
|
+
try {
|
|
44
|
+
return verifyPathWithKey(path, signature, key);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function expiringPayload(path, expiresAtMs) {
|
|
52
|
+
return `${expiresAtMs}\n${path}`;
|
|
53
|
+
}
|
|
54
|
+
/** Sign a path bound to an expiry (epoch milliseconds). Returns '' when no key is usable. */
|
|
55
|
+
export function signExpiringPath(path, expiresAtMs, keys) {
|
|
56
|
+
return signPath(expiringPayload(path, expiresAtMs), keys);
|
|
57
|
+
}
|
|
58
|
+
/** Verify an expiring signature. Fails closed for unsafe expiry values and expired links. */
|
|
59
|
+
export function verifyExpiringPath(path, expiresAtMs, signature, keys, options = {}) {
|
|
60
|
+
if (!Number.isSafeInteger(expiresAtMs))
|
|
61
|
+
return false;
|
|
62
|
+
if ((options.now ?? Date.now()) > expiresAtMs)
|
|
63
|
+
return false;
|
|
64
|
+
return verifyPathSignature(expiringPayload(path, expiresAtMs), signature, keys);
|
|
65
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type BuildSignedProxyUrlOptions = {
|
|
2
|
+
/** Origin that serves the proxy route. */
|
|
3
|
+
origin: string;
|
|
4
|
+
/** Route prefix; the base64url target is appended, e.g. `/proxy/v1/`. */
|
|
5
|
+
pathPrefix: string;
|
|
6
|
+
/** Ordered query parameters; undefined values are skipped. The signature is appended last. */
|
|
7
|
+
query?: Record<string, string | number | undefined>;
|
|
8
|
+
signingKeys: readonly string[];
|
|
9
|
+
/** Return true to leave a URL unproxied (first-party hosts, legacy routes, ...). */
|
|
10
|
+
skip?: (url: URL) => boolean;
|
|
11
|
+
/** Name of the signature query parameter. Defaults to `sig`. */
|
|
12
|
+
signatureParam?: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Build a signed proxy URL for an external http(s) URL. Returns null for empty, relative,
|
|
16
|
+
* protocol-relative, non-http(s), already-proxied, or caller-skipped URLs. When no key is usable
|
|
17
|
+
* the URL is returned without a signature.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildSignedProxyUrl(externalUrl: string, options: BuildSignedProxyUrlOptions): string | null;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { signPath } from './url-signing-keys.mjs';
|
|
2
|
+
/**
|
|
3
|
+
* Build a signed proxy URL for an external http(s) URL. Returns null for empty, relative,
|
|
4
|
+
* protocol-relative, non-http(s), already-proxied, or caller-skipped URLs. When no key is usable
|
|
5
|
+
* the URL is returned without a signature.
|
|
6
|
+
*/
|
|
7
|
+
export function buildSignedProxyUrl(externalUrl, options) {
|
|
8
|
+
const value = externalUrl.trim();
|
|
9
|
+
if (!value || value.startsWith('//'))
|
|
10
|
+
return null;
|
|
11
|
+
let target;
|
|
12
|
+
try {
|
|
13
|
+
target = new URL(value);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
if (target.protocol !== 'http:' && target.protocol !== 'https:')
|
|
19
|
+
return null;
|
|
20
|
+
const origin = new URL(options.origin).origin;
|
|
21
|
+
if (target.origin === origin && target.pathname.startsWith(options.pathPrefix))
|
|
22
|
+
return null;
|
|
23
|
+
if (options.skip?.(target))
|
|
24
|
+
return null;
|
|
25
|
+
const path = `${options.pathPrefix}${Buffer.from(value, 'utf8').toString('base64url')}`;
|
|
26
|
+
const params = new URLSearchParams();
|
|
27
|
+
for (const [name, entry] of Object.entries(options.query ?? {})) {
|
|
28
|
+
if (entry !== undefined)
|
|
29
|
+
params.append(name, String(entry));
|
|
30
|
+
}
|
|
31
|
+
const signature = signPath(path, options.signingKeys);
|
|
32
|
+
if (signature)
|
|
33
|
+
params.append(options.signatureParam ?? 'sig', signature);
|
|
34
|
+
const query = params.toString();
|
|
35
|
+
return `${origin}${path}${query ? `?${query}` : ''}`;
|
|
36
|
+
}
|
package/dist/url-signing.d.mts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
1
|
+
export { parseSigningKeys, signExpiringPath, signPath, signPathWithKey, verifyExpiringPath, verifyPathSignature, verifyPathWithKey, type VerifyPathOptions, } from './url-signing-keys.mts';
|
|
2
|
+
export { authorizeDependencyStates, createProtectedHostPolicy, hostnameOf, matchRouteSegment, normalizeHostname, type DependencyAuthorization, type ProtectedHostPolicy, } from './url-signing-hosts.mts';
|
|
3
|
+
export { buildSignedProxyUrl, type BuildSignedProxyUrlOptions } from './url-signing-proxy.mts';
|
package/dist/url-signing.mjs
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
throw new Error('Signing keys must be 64 hexadecimal characters');
|
|
5
|
-
return createHmac('sha256', Buffer.from(keyHex, 'hex')).update(path).digest('hex');
|
|
6
|
-
}
|
|
7
|
-
export function verifyPathWithKey(path, signature, keyHex) {
|
|
8
|
-
const expected = signPathWithKey(path, keyHex);
|
|
9
|
-
if (expected.length !== signature.length || !/^[\da-f]+$/i.test(signature))
|
|
10
|
-
return false;
|
|
11
|
-
return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(signature, 'hex'));
|
|
12
|
-
}
|
|
1
|
+
export { parseSigningKeys, signExpiringPath, signPath, signPathWithKey, verifyExpiringPath, verifyPathSignature, verifyPathWithKey, } from './url-signing-keys.mjs';
|
|
2
|
+
export { authorizeDependencyStates, createProtectedHostPolicy, hostnameOf, matchRouteSegment, normalizeHostname, } from './url-signing-hosts.mjs';
|
|
3
|
+
export { buildSignedProxyUrl } from './url-signing-proxy.mjs';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vouchington/utils",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Dependency-free runtime utilities for Vouchington-style applications.",
|
|
5
5
|
"homepage": "https://github.com/vouchington/vouchington-platform/tree/main/packages/utils#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -20,6 +20,16 @@
|
|
|
20
20
|
],
|
|
21
21
|
"type": "module",
|
|
22
22
|
"exports": {
|
|
23
|
+
"./api-errors": {
|
|
24
|
+
"types": "./dist/api-errors.d.mts",
|
|
25
|
+
"import": "./dist/api-errors.mjs",
|
|
26
|
+
"default": "./dist/api-errors.mjs"
|
|
27
|
+
},
|
|
28
|
+
"./api-response-contract": {
|
|
29
|
+
"types": "./dist/api-response-contract.d.mts",
|
|
30
|
+
"import": "./dist/api-response-contract.mjs",
|
|
31
|
+
"default": "./dist/api-response-contract.mjs"
|
|
32
|
+
},
|
|
23
33
|
"./cache-tags": {
|
|
24
34
|
"types": "./dist/cache-tags.d.mts",
|
|
25
35
|
"import": "./dist/cache-tags.mjs",
|
|
@@ -70,6 +80,11 @@
|
|
|
70
80
|
"import": "./dist/cookies.mjs",
|
|
71
81
|
"default": "./dist/cookies.mjs"
|
|
72
82
|
},
|
|
83
|
+
"./content-identity-cache": {
|
|
84
|
+
"types": "./dist/content-identity-cache.d.mts",
|
|
85
|
+
"import": "./dist/content-identity-cache.mjs",
|
|
86
|
+
"default": "./dist/content-identity-cache.mjs"
|
|
87
|
+
},
|
|
73
88
|
"./collections": {
|
|
74
89
|
"types": "./dist/collections.d.mts",
|
|
75
90
|
"import": "./dist/collections.mjs",
|