@vouchington/utils 0.4.1 → 0.5.1
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 +21 -7
- package/dist/cookies.d.mts +1 -0
- package/dist/cookies.mjs +16 -0
- package/dist/hashtags.d.mts +5 -0
- package/dist/hashtags.mjs +43 -0
- package/dist/http-signatures-digest.d.mts +3 -0
- package/dist/http-signatures-digest.mjs +20 -0
- package/dist/http-signatures-keys.d.mts +7 -0
- package/dist/http-signatures-keys.mjs +27 -0
- package/dist/http-signatures-sign.d.mts +17 -0
- package/dist/http-signatures-sign.mjs +35 -0
- package/dist/http-signatures-string.d.mts +12 -0
- package/dist/http-signatures-string.mjs +40 -0
- package/dist/http-signatures-test-helpers.d.mts +30 -0
- package/dist/http-signatures-test-helpers.mjs +62 -0
- package/dist/http-signatures-verify.d.mts +21 -0
- package/dist/http-signatures-verify.mjs +88 -0
- package/dist/http-signatures.d.mts +4 -0
- package/dist/http-signatures.mjs +4 -0
- package/dist/moderation.d.mts +34 -0
- package/dist/moderation.mjs +49 -0
- package/dist/money.d.mts +9 -0
- package/dist/money.mjs +20 -0
- package/dist/slugs.d.mts +2 -0
- package/dist/slugs.mjs +4 -0
- package/dist/utm.d.mts +16 -0
- package/dist/utm.mjs +28 -0
- package/package.json +20 -6
package/README.md
CHANGED
|
@@ -6,9 +6,11 @@ export: import the capability you use from an explicit subpath.
|
|
|
6
6
|
```ts
|
|
7
7
|
import { createTokenSecrets } from '@vouchington/utils/token-secrets'
|
|
8
8
|
import { createMoneyCatalog } from '@vouchington/utils/money'
|
|
9
|
-
import { serializeCookie } from '@vouchington/utils/cookies'
|
|
9
|
+
import { parseCookies, serializeCookie } from '@vouchington/utils/cookies'
|
|
10
|
+
import { buildSignatureHeaders, verifySignature } from '@vouchington/utils/http-signatures'
|
|
10
11
|
import { bestAcceptLanguageMatch } from '@vouchington/utils/language-tags'
|
|
11
12
|
import { createMessageTranslator } from '@vouchington/utils/message-catalog'
|
|
13
|
+
import { createReportInputParser } from '@vouchington/utils/moderation'
|
|
12
14
|
```
|
|
13
15
|
|
|
14
16
|
## Subpaths
|
|
@@ -22,11 +24,17 @@ import { createMessageTranslator } from '@vouchington/utils/message-catalog'
|
|
|
22
24
|
- `request-client-info`: `createClientInfoParser(config)` validates caller-selected headers,
|
|
23
25
|
families, platforms, compatibility, and versions.
|
|
24
26
|
- `money`: `createMoneyCatalog(currencies, scale)` supplies exact integer parsing and validation for
|
|
25
|
-
the caller's catalog.
|
|
27
|
+
the caller's catalog. `allocateProportionalAmount(amount, numerator, denominator, rounding)`
|
|
28
|
+
allocates an inclusive fraction with `down` or `up` integer rounding chosen explicitly by the
|
|
29
|
+
caller; it uses exact integer arithmetic rather than floating-point division.
|
|
26
30
|
- `env-contract`: `groupEnvContracts()` and `normalizeEnvContractGroups()` produce delimiter-safe
|
|
27
31
|
metadata keys and surface/sensitivity lookups.
|
|
28
32
|
- `cookies`: `serializeCookie(name, value, attributes)` validates cookie grammar and requires the
|
|
29
33
|
caller to supply every policy attribute. `SameSite=None` requires `Secure`.
|
|
34
|
+
`parseCookies(header)` splits a `Cookie` request header into a name/value `Map`.
|
|
35
|
+
- `http-signatures`: Node-only (`node:crypto`) Cavage-12 HTTP Signatures. Callers pass signed
|
|
36
|
+
headers, allowed algorithms, and `maxAgeSeconds` on both sign and verify. The package has no
|
|
37
|
+
default header set or product keyId policy.
|
|
30
38
|
- `collections`, `async`, and `stable-json`: collection deduplication/merging, bounded async mapping,
|
|
31
39
|
and deterministic JSON serialization.
|
|
32
40
|
- `feature-flags`: base64 JSON feature-flag cookie parsing and serialization with caller-injected
|
|
@@ -35,14 +43,20 @@ import { createMessageTranslator } from '@vouchington/utils/message-catalog'
|
|
|
35
43
|
formatting, text normalization, title casing, and word/sentence helpers.
|
|
36
44
|
- `gtin`, `bigint-ids`, and `validation`: GTIN predicates, canonical positive PostgreSQL bigint IDs,
|
|
37
45
|
and basic email/UUID predicates.
|
|
46
|
+
- `moderation`: caller-configured report target, reason, identifier, note, and cross-field input
|
|
47
|
+
validation. Applications own catalogs, policy, error mapping, persistence, and HTTP behavior.
|
|
38
48
|
- `query` and `query-string`: generic array/boolean/number parsing, bounded integer parsing, and
|
|
39
49
|
query-string serialization. Cursor parsing intentionally belongs to `@vouchington/pagination`.
|
|
40
|
-
- `slugs` and `urls`: ASCII-only slugification
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
- `slugs` and `urls`: ASCII-only slugification, the `isSlug` lowercase `[a-z0-9-]+` predicate
|
|
51
|
+
(leading, trailing, and consecutive hyphens are allowed), and URL/hostname primitives. URL hostname
|
|
52
|
+
helpers return `null` for invalid input; `normalizeAsciiHostname` names its ASCII-only contract
|
|
53
|
+
directly, while `normalizeHostname` also supports WHATWG IDN conversion.
|
|
54
|
+
`matchesHostnamePattern` supports exact and inclusive `*.` hostname patterns.
|
|
44
55
|
- `hashtags`: caller-configured authored/key length and separator policy for canonical ASCII hashtag
|
|
45
|
-
keys
|
|
56
|
+
keys, plus `maskHashtagBearingUrls` to blank absolute URLs and fragment-bearing relative paths.
|
|
57
|
+
The package does not choose application limits or accepted input separators.
|
|
58
|
+
- `utm`: `createUtmParser({ sourceAliases, fallbackSourceParam? })` lowercases and trims UTM fields
|
|
59
|
+
and resolves `utm_source` through caller-owned aliases. The package has no default shortcode map.
|
|
46
60
|
- `fetch-ports`: the Fetch standard forbidden-port list and membership predicates.
|
|
47
61
|
- `http-body`: bounded Fetch response-body reads with abort propagation and typed missing/oversize
|
|
48
62
|
errors.
|
package/dist/cookies.d.mts
CHANGED
package/dist/cookies.mjs
CHANGED
|
@@ -30,3 +30,19 @@ export function serializeCookie(name, value, attributes) {
|
|
|
30
30
|
parts.push('Secure');
|
|
31
31
|
return parts.join('; ');
|
|
32
32
|
}
|
|
33
|
+
const COOKIE_SPLIT_REGEX = /;\s*/;
|
|
34
|
+
export function parseCookies(cookieHeader) {
|
|
35
|
+
const cookies = new Map();
|
|
36
|
+
if (!cookieHeader)
|
|
37
|
+
return cookies;
|
|
38
|
+
for (const part of cookieHeader.split(COOKIE_SPLIT_REGEX)) {
|
|
39
|
+
const separatorIndex = part.indexOf('=');
|
|
40
|
+
if (separatorIndex <= 0)
|
|
41
|
+
continue;
|
|
42
|
+
const name = part.slice(0, separatorIndex).trim();
|
|
43
|
+
const value = part.slice(separatorIndex + 1).trim();
|
|
44
|
+
if (name.length > 0)
|
|
45
|
+
cookies.set(name, value);
|
|
46
|
+
}
|
|
47
|
+
return cookies;
|
|
48
|
+
}
|
package/dist/hashtags.d.mts
CHANGED
|
@@ -13,3 +13,8 @@ export interface HashtagNormalizer {
|
|
|
13
13
|
}
|
|
14
14
|
/** Creates an ASCII hashtag normalizer whose length and separator policy is caller-owned. */
|
|
15
15
|
export declare function createHashtagNormalizer(options: HashtagNormalizerOptions): HashtagNormalizer;
|
|
16
|
+
/**
|
|
17
|
+
* Blanks absolute URLs and relative paths that contain `#` so fragment text is
|
|
18
|
+
* not treated as a hashtag. Parentheses stay balanced for Wikipedia-style paths.
|
|
19
|
+
*/
|
|
20
|
+
export declare function maskHashtagBearingUrls(input: string): string;
|
package/dist/hashtags.mjs
CHANGED
|
@@ -42,3 +42,46 @@ function createSeparatorPattern(separators) {
|
|
|
42
42
|
function escapeRegularExpression(value) {
|
|
43
43
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
44
44
|
}
|
|
45
|
+
const absoluteUrlPrefix = /(?:https?:\/\/|www\.)/gi;
|
|
46
|
+
const urlBoundary = /[\s<>]/;
|
|
47
|
+
/**
|
|
48
|
+
* Blanks absolute URLs and relative paths that contain `#` so fragment text is
|
|
49
|
+
* not treated as a hashtag. Parentheses stay balanced for Wikipedia-style paths.
|
|
50
|
+
*/
|
|
51
|
+
export function maskHashtagBearingUrls(input) {
|
|
52
|
+
const masked = input.split('');
|
|
53
|
+
for (const match of input.matchAll(absoluteUrlPrefix)) {
|
|
54
|
+
const start = match.index;
|
|
55
|
+
maskRange(masked, start, findUrlEnd(input, start));
|
|
56
|
+
}
|
|
57
|
+
for (let start = 0; start < input.length; start += 1) {
|
|
58
|
+
if (input[start] !== '/')
|
|
59
|
+
continue;
|
|
60
|
+
const end = findUrlEnd(input, start);
|
|
61
|
+
if (input.slice(start, end).includes('#'))
|
|
62
|
+
maskRange(masked, start, end);
|
|
63
|
+
start = end - 1;
|
|
64
|
+
}
|
|
65
|
+
return masked.join('');
|
|
66
|
+
}
|
|
67
|
+
function findUrlEnd(input, start) {
|
|
68
|
+
let parenthesisDepth = 0;
|
|
69
|
+
for (let index = start; index < input.length; index += 1) {
|
|
70
|
+
const character = input[index];
|
|
71
|
+
if (urlBoundary.test(character))
|
|
72
|
+
return index;
|
|
73
|
+
if (character === '(') {
|
|
74
|
+
parenthesisDepth += 1;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (character === ')') {
|
|
78
|
+
if (parenthesisDepth === 0)
|
|
79
|
+
return index;
|
|
80
|
+
parenthesisDepth -= 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return input.length;
|
|
84
|
+
}
|
|
85
|
+
function maskRange(masked, start, end) {
|
|
86
|
+
masked.fill(' ', start, end);
|
|
87
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
const DIGEST_HEADER_PATTERN = /^SHA-256=(.+)$/;
|
|
3
|
+
export function computeDigest(body) {
|
|
4
|
+
const bodyBuffer = typeof body === 'string' ? Buffer.from(body, 'utf-8') : body;
|
|
5
|
+
const hash = createHash('sha256').update(bodyBuffer).digest('base64');
|
|
6
|
+
return `SHA-256=${hash}`;
|
|
7
|
+
}
|
|
8
|
+
export function extractDigestHash(digestHeader) {
|
|
9
|
+
const match = DIGEST_HEADER_PATTERN.exec(digestHeader);
|
|
10
|
+
if (!match?.[1])
|
|
11
|
+
throw new Error('Invalid Digest header format');
|
|
12
|
+
return match[1];
|
|
13
|
+
}
|
|
14
|
+
export function verifyDigest(body, expectedDigestHeader) {
|
|
15
|
+
const computed = Buffer.from(computeDigest(body));
|
|
16
|
+
const expected = Buffer.from(expectedDigestHeader);
|
|
17
|
+
if (computed.length !== expected.length)
|
|
18
|
+
return false;
|
|
19
|
+
return timingSafeEqual(computed, expected);
|
|
20
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type RsaSha256KeyPair = {
|
|
2
|
+
publicKeyPem: string;
|
|
3
|
+
privateKeyPem: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function generateRsaSha256KeyPair(): RsaSha256KeyPair;
|
|
6
|
+
export declare function assertPublicKeyPem(pem: string): void;
|
|
7
|
+
export declare function assertPrivateKeyPem(pem: string): void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createPrivateKey, createPublicKey, generateKeyPairSync } from 'node:crypto';
|
|
2
|
+
export function generateRsaSha256KeyPair() {
|
|
3
|
+
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
|
|
4
|
+
modulusLength: 2048,
|
|
5
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
6
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
7
|
+
});
|
|
8
|
+
return { publicKeyPem: publicKey, privateKeyPem: privateKey };
|
|
9
|
+
}
|
|
10
|
+
export function assertPublicKeyPem(pem) {
|
|
11
|
+
if (pem.includes('PRIVATE'))
|
|
12
|
+
throw new Error('Invalid public key PEM format');
|
|
13
|
+
try {
|
|
14
|
+
createPublicKey(pem);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new Error('Invalid public key PEM format');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function assertPrivateKeyPem(pem) {
|
|
21
|
+
try {
|
|
22
|
+
createPrivateKey(pem);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new Error('Invalid private key PEM format');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type SignatureHeaders = {
|
|
2
|
+
digest: string;
|
|
3
|
+
signature: string;
|
|
4
|
+
date: string;
|
|
5
|
+
};
|
|
6
|
+
export type BuildSignatureHeadersOptions = {
|
|
7
|
+
method: string;
|
|
8
|
+
url: string;
|
|
9
|
+
body: string | Buffer;
|
|
10
|
+
keyId: string;
|
|
11
|
+
privateKeyPem: string;
|
|
12
|
+
signedHeaders: readonly string[];
|
|
13
|
+
algorithm: string;
|
|
14
|
+
now?: Date;
|
|
15
|
+
additionalHeaders?: Record<string, string>;
|
|
16
|
+
};
|
|
17
|
+
export declare function buildSignatureHeaders(options: BuildSignatureHeadersOptions): SignatureHeaders;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createPrivateKey, sign } from 'node:crypto';
|
|
2
|
+
import { computeDigest } from './http-signatures-digest.mjs';
|
|
3
|
+
import { assertPrivateKeyPem } from './http-signatures-keys.mjs';
|
|
4
|
+
import { buildSigningString, isSha256SignatureAlgorithm } from './http-signatures-string.mjs';
|
|
5
|
+
export function buildSignatureHeaders(options) {
|
|
6
|
+
if (options.signedHeaders.length === 0)
|
|
7
|
+
throw new Error('signedHeaders must not be empty');
|
|
8
|
+
if (!isSha256SignatureAlgorithm(options.algorithm)) {
|
|
9
|
+
throw new Error(`Unsupported signature algorithm: ${options.algorithm}`);
|
|
10
|
+
}
|
|
11
|
+
assertPrivateKeyPem(options.privateKeyPem);
|
|
12
|
+
const digest = computeDigest(options.body);
|
|
13
|
+
const date = (options.now ?? new Date()).toUTCString();
|
|
14
|
+
const url = new URL(options.url);
|
|
15
|
+
const host = url.host;
|
|
16
|
+
const path = url.pathname + url.search;
|
|
17
|
+
const signingString = buildSigningString(options.signedHeaders, {
|
|
18
|
+
method: options.method,
|
|
19
|
+
path,
|
|
20
|
+
host,
|
|
21
|
+
date,
|
|
22
|
+
digest,
|
|
23
|
+
additionalHeaders: options.additionalHeaders,
|
|
24
|
+
});
|
|
25
|
+
if (typeof signingString !== 'string')
|
|
26
|
+
throw new Error(signingString.error);
|
|
27
|
+
const signature = sign('sha256', Buffer.from(signingString), createPrivateKey(options.privateKeyPem)).toString('base64');
|
|
28
|
+
const signatureHeaderValue = [
|
|
29
|
+
`keyId="${options.keyId}"`,
|
|
30
|
+
`algorithm="${options.algorithm}"`,
|
|
31
|
+
`headers="${options.signedHeaders.map((name) => name.toLowerCase()).join(' ')}"`,
|
|
32
|
+
`signature="${signature}"`,
|
|
33
|
+
].join(',');
|
|
34
|
+
return { digest, signature: signatureHeaderValue, date };
|
|
35
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type SigningHeaderValues = {
|
|
2
|
+
method: string;
|
|
3
|
+
path: string;
|
|
4
|
+
host: string;
|
|
5
|
+
date: string;
|
|
6
|
+
digest: string;
|
|
7
|
+
additionalHeaders?: Record<string, string> | undefined;
|
|
8
|
+
};
|
|
9
|
+
export declare function isSha256SignatureAlgorithm(algorithm: string): boolean;
|
|
10
|
+
export declare function buildSigningString(headerNames: readonly string[], values: SigningHeaderValues): string | {
|
|
11
|
+
error: string;
|
|
12
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const SHA256_ALGORITHMS = new Set(['rsa-sha256', 'hs2019']);
|
|
2
|
+
export function isSha256SignatureAlgorithm(algorithm) {
|
|
3
|
+
return SHA256_ALGORITHMS.has(algorithm);
|
|
4
|
+
}
|
|
5
|
+
function additionalHeaderValue(headers, name) {
|
|
6
|
+
if (!headers)
|
|
7
|
+
return undefined;
|
|
8
|
+
if (Object.hasOwn(headers, name))
|
|
9
|
+
return headers[name];
|
|
10
|
+
const lower = name.toLowerCase();
|
|
11
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
12
|
+
if (key.toLowerCase() === lower)
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
function signingLineForHeader(name, values) {
|
|
18
|
+
const lower = name.toLowerCase();
|
|
19
|
+
if (lower === '(request-target)') {
|
|
20
|
+
return `(request-target): ${values.method.toLowerCase()} ${values.path}`;
|
|
21
|
+
}
|
|
22
|
+
if (lower === 'host')
|
|
23
|
+
return `host: ${values.host}`;
|
|
24
|
+
if (lower === 'date')
|
|
25
|
+
return `date: ${values.date}`;
|
|
26
|
+
if (lower === 'digest')
|
|
27
|
+
return `digest: ${values.digest}`;
|
|
28
|
+
const extra = additionalHeaderValue(values.additionalHeaders, name);
|
|
29
|
+
return extra === undefined ? undefined : `${lower}: ${extra}`;
|
|
30
|
+
}
|
|
31
|
+
export function buildSigningString(headerNames, values) {
|
|
32
|
+
const lines = [];
|
|
33
|
+
for (const name of headerNames) {
|
|
34
|
+
const line = signingLineForHeader(name, values);
|
|
35
|
+
if (line === undefined)
|
|
36
|
+
return { error: `Unknown signed header: ${name}` };
|
|
37
|
+
lines.push(line);
|
|
38
|
+
}
|
|
39
|
+
return lines.join('\n');
|
|
40
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { VerifySignatureOptions } from './http-signatures-verify.mts';
|
|
2
|
+
export declare const KEY_ID = "https://example.test/users/alice#main-key";
|
|
3
|
+
export declare const BODY: string;
|
|
4
|
+
export declare const FULL_HEADERS: readonly ['(request-target)', 'host', 'date', 'digest'];
|
|
5
|
+
export declare const defaultVerifyPolicy: {
|
|
6
|
+
readonly requiredHeaders: readonly ["(request-target)", "host", "date", "digest"];
|
|
7
|
+
readonly allowedAlgorithms: readonly ["rsa-sha256", "hs2019"];
|
|
8
|
+
readonly maxAgeSeconds: 3600;
|
|
9
|
+
};
|
|
10
|
+
export declare const defaultSignPolicy: {
|
|
11
|
+
readonly signedHeaders: readonly ["(request-target)", "host", "date", "digest"];
|
|
12
|
+
readonly algorithm: 'rsa-sha256';
|
|
13
|
+
};
|
|
14
|
+
export declare function digestAndDate(body?: string | Buffer): {
|
|
15
|
+
digestHeader: string;
|
|
16
|
+
dateHeader: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function verifyArgs(publicKeyPem: string, signatureHeader: string, extras?: Partial<VerifySignatureOptions>): VerifySignatureOptions;
|
|
19
|
+
export declare function buildManualSignature(options: {
|
|
20
|
+
privateKeyPem: string;
|
|
21
|
+
dateHeader: string;
|
|
22
|
+
digestHeader: string;
|
|
23
|
+
headerNames: readonly string[];
|
|
24
|
+
additionalHeaders?: Record<string, string>;
|
|
25
|
+
keyId?: string;
|
|
26
|
+
algorithm?: string | null;
|
|
27
|
+
method?: string;
|
|
28
|
+
path?: string;
|
|
29
|
+
host?: string;
|
|
30
|
+
}): string;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createPrivateKey, sign as cryptoSign } from 'node:crypto';
|
|
2
|
+
import { computeDigest } from './http-signatures-digest.mjs';
|
|
3
|
+
export const KEY_ID = 'https://example.test/users/alice#main-key';
|
|
4
|
+
const METHOD = 'POST';
|
|
5
|
+
const PATH = '/inbox';
|
|
6
|
+
const HOST = 'mastodon.example';
|
|
7
|
+
export const BODY = JSON.stringify({ type: 'Follow', object: 'https://example.test' });
|
|
8
|
+
export const FULL_HEADERS = ['(request-target)', 'host', 'date', 'digest'];
|
|
9
|
+
const ALLOWED_ALGORITHMS = ['rsa-sha256', 'hs2019'];
|
|
10
|
+
const MAX_AGE_SECONDS = 3600;
|
|
11
|
+
export const defaultVerifyPolicy = {
|
|
12
|
+
requiredHeaders: FULL_HEADERS,
|
|
13
|
+
allowedAlgorithms: ALLOWED_ALGORITHMS,
|
|
14
|
+
maxAgeSeconds: MAX_AGE_SECONDS,
|
|
15
|
+
};
|
|
16
|
+
export const defaultSignPolicy = {
|
|
17
|
+
signedHeaders: FULL_HEADERS,
|
|
18
|
+
algorithm: 'rsa-sha256',
|
|
19
|
+
};
|
|
20
|
+
export function digestAndDate(body = BODY) {
|
|
21
|
+
return { digestHeader: computeDigest(body), dateHeader: new Date().toUTCString() };
|
|
22
|
+
}
|
|
23
|
+
export function verifyArgs(publicKeyPem, signatureHeader, extras = {}) {
|
|
24
|
+
const { digestHeader, dateHeader } = digestAndDate(extras.body ?? BODY);
|
|
25
|
+
return {
|
|
26
|
+
method: METHOD,
|
|
27
|
+
path: PATH,
|
|
28
|
+
host: HOST,
|
|
29
|
+
body: BODY,
|
|
30
|
+
signatureHeader,
|
|
31
|
+
digestHeader,
|
|
32
|
+
dateHeader,
|
|
33
|
+
publicKeyPem,
|
|
34
|
+
...defaultVerifyPolicy,
|
|
35
|
+
...extras,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function buildManualSignature(options) {
|
|
39
|
+
const headerNames = options.headerNames;
|
|
40
|
+
const signingString = headerNames
|
|
41
|
+
.map((name) => {
|
|
42
|
+
if (name === '(request-target)') {
|
|
43
|
+
return `(request-target): ${(options.method ?? METHOD).toLowerCase()} ${options.path ?? PATH}`;
|
|
44
|
+
}
|
|
45
|
+
if (name === 'host')
|
|
46
|
+
return `host: ${options.host ?? HOST}`;
|
|
47
|
+
if (name === 'date')
|
|
48
|
+
return `date: ${options.dateHeader}`;
|
|
49
|
+
if (name === 'digest')
|
|
50
|
+
return `digest: ${options.digestHeader}`;
|
|
51
|
+
return `${name}: ${options.additionalHeaders?.[name]}`;
|
|
52
|
+
})
|
|
53
|
+
.join('\n');
|
|
54
|
+
const signature = cryptoSign('sha256', Buffer.from(signingString), createPrivateKey(options.privateKeyPem)).toString('base64');
|
|
55
|
+
const headerParts = [`keyId="${options.keyId ?? KEY_ID}"`];
|
|
56
|
+
if (options.algorithm !== null) {
|
|
57
|
+
headerParts.push(`algorithm="${options.algorithm ?? 'rsa-sha256'}"`);
|
|
58
|
+
}
|
|
59
|
+
headerParts.push(`headers="${headerNames.join(' ')}"`);
|
|
60
|
+
headerParts.push(`signature="${signature}"`);
|
|
61
|
+
return headerParts.join(',');
|
|
62
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type VerifySignatureOptions = {
|
|
2
|
+
method: string;
|
|
3
|
+
path: string;
|
|
4
|
+
host: string;
|
|
5
|
+
body: string | Buffer;
|
|
6
|
+
signatureHeader: string;
|
|
7
|
+
digestHeader: string;
|
|
8
|
+
dateHeader: string;
|
|
9
|
+
publicKeyPem: string;
|
|
10
|
+
requiredHeaders: readonly string[];
|
|
11
|
+
allowedAlgorithms: readonly string[];
|
|
12
|
+
maxAgeSeconds: number;
|
|
13
|
+
additionalHeaders?: Record<string, string>;
|
|
14
|
+
referenceTime?: Date;
|
|
15
|
+
};
|
|
16
|
+
export type SignatureVerificationResult = {
|
|
17
|
+
valid: boolean;
|
|
18
|
+
error?: string;
|
|
19
|
+
};
|
|
20
|
+
export declare function extractSignatureKeyId(signatureHeader: string): string | undefined;
|
|
21
|
+
export declare function verifySignature(options: VerifySignatureOptions): SignatureVerificationResult;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { verify } from 'node:crypto';
|
|
2
|
+
import { verifyDigest } from './http-signatures-digest.mjs';
|
|
3
|
+
import { assertPublicKeyPem } from './http-signatures-keys.mjs';
|
|
4
|
+
import { buildSigningString, isSha256SignatureAlgorithm } from './http-signatures-string.mjs';
|
|
5
|
+
const SIGNATURE_VALUE_PATTERN = /signature="([^"]+)"/;
|
|
6
|
+
const HEADERS_VALUE_PATTERN = /headers="([^"]+)"/;
|
|
7
|
+
const ALGORITHM_VALUE_PATTERN = /algorithm="([^"]+)"/;
|
|
8
|
+
const KEY_ID_VALUE_PATTERN = /keyId="([^"]+)"/;
|
|
9
|
+
export function extractSignatureKeyId(signatureHeader) {
|
|
10
|
+
return KEY_ID_VALUE_PATTERN.exec(signatureHeader)?.[1];
|
|
11
|
+
}
|
|
12
|
+
function parseSignatureHeader(header) {
|
|
13
|
+
const signatureMatch = SIGNATURE_VALUE_PATTERN.exec(header);
|
|
14
|
+
const headersMatch = HEADERS_VALUE_PATTERN.exec(header);
|
|
15
|
+
if (!signatureMatch?.[1] || !headersMatch?.[1])
|
|
16
|
+
return null;
|
|
17
|
+
return {
|
|
18
|
+
signature: signatureMatch[1],
|
|
19
|
+
headers: headersMatch[1].split(' ').map((name) => name.toLowerCase()),
|
|
20
|
+
algorithm: ALGORITHM_VALUE_PATTERN.exec(header)?.[1] ?? null,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function verifySignature(options) {
|
|
24
|
+
try {
|
|
25
|
+
return verifySignatureBody(options);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
return {
|
|
29
|
+
valid: false,
|
|
30
|
+
error: `Signature verification exception: ${err.message}`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function verifySignatureBody(options) {
|
|
35
|
+
assertPublicKeyPem(options.publicKeyPem);
|
|
36
|
+
const requestDate = new Date(options.dateHeader);
|
|
37
|
+
if (Number.isNaN(requestDate.getTime()))
|
|
38
|
+
return { valid: false, error: 'Invalid date header' };
|
|
39
|
+
const ageSeconds = ((options.referenceTime?.getTime() ?? Date.now()) - requestDate.getTime()) / 1000;
|
|
40
|
+
if (!Number.isFinite(options.maxAgeSeconds) || options.maxAgeSeconds < 0) {
|
|
41
|
+
return { valid: false, error: 'maxAgeSeconds must be a finite non-negative number' };
|
|
42
|
+
}
|
|
43
|
+
if (Math.abs(ageSeconds) > options.maxAgeSeconds) {
|
|
44
|
+
return {
|
|
45
|
+
valid: false,
|
|
46
|
+
error: `Date header too old: ${ageSeconds}s > ${options.maxAgeSeconds}s`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (!verifyDigest(options.body, options.digestHeader)) {
|
|
50
|
+
return { valid: false, error: 'Digest verification failed' };
|
|
51
|
+
}
|
|
52
|
+
const sigParts = parseSignatureHeader(options.signatureHeader);
|
|
53
|
+
if (!sigParts)
|
|
54
|
+
return { valid: false, error: 'Invalid signature header format' };
|
|
55
|
+
const { signature, headers, algorithm } = sigParts;
|
|
56
|
+
if (algorithm === null ||
|
|
57
|
+
!options.allowedAlgorithms.includes(algorithm) ||
|
|
58
|
+
!isSha256SignatureAlgorithm(algorithm)) {
|
|
59
|
+
return {
|
|
60
|
+
valid: false,
|
|
61
|
+
error: algorithm === null
|
|
62
|
+
? 'Signature algorithm is required'
|
|
63
|
+
: `Unsupported signature algorithm: ${algorithm}`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const covered = new Set(headers);
|
|
67
|
+
const missingRequired = options.requiredHeaders
|
|
68
|
+
.map((name) => name.toLowerCase())
|
|
69
|
+
.filter((name) => !covered.has(name));
|
|
70
|
+
if (missingRequired.length > 0) {
|
|
71
|
+
return {
|
|
72
|
+
valid: false,
|
|
73
|
+
error: `Signature must cover ${options.requiredHeaders.join(', ')} (missing: ${missingRequired.join(', ')})`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const signingString = buildSigningString(headers, {
|
|
77
|
+
method: options.method,
|
|
78
|
+
path: options.path,
|
|
79
|
+
host: options.host,
|
|
80
|
+
date: options.dateHeader,
|
|
81
|
+
digest: options.digestHeader,
|
|
82
|
+
additionalHeaders: options.additionalHeaders,
|
|
83
|
+
});
|
|
84
|
+
if (typeof signingString !== 'string')
|
|
85
|
+
return { valid: false, error: signingString.error };
|
|
86
|
+
const isValid = verify('sha256', Buffer.from(signingString), options.publicKeyPem, Buffer.from(signature, 'base64'));
|
|
87
|
+
return isValid ? { valid: true } : { valid: false, error: 'Signature verification failed' };
|
|
88
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { computeDigest, extractDigestHash, verifyDigest } from './http-signatures-digest.mts';
|
|
2
|
+
export { assertPrivateKeyPem, assertPublicKeyPem, generateRsaSha256KeyPair, type RsaSha256KeyPair, } from './http-signatures-keys.mts';
|
|
3
|
+
export { buildSignatureHeaders, type BuildSignatureHeadersOptions, type SignatureHeaders, } from './http-signatures-sign.mts';
|
|
4
|
+
export { extractSignatureKeyId, verifySignature, type SignatureVerificationResult, type VerifySignatureOptions, } from './http-signatures-verify.mts';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { computeDigest, extractDigestHash, verifyDigest } from './http-signatures-digest.mjs';
|
|
2
|
+
export { assertPrivateKeyPem, assertPublicKeyPem, generateRsaSha256KeyPair, } from './http-signatures-keys.mjs';
|
|
3
|
+
export { buildSignatureHeaders, } from './http-signatures-sign.mjs';
|
|
4
|
+
export { extractSignatureKeyId, verifySignature, } from './http-signatures-verify.mjs';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
interface ReportTarget<TType extends string, TId> {
|
|
2
|
+
type: TType;
|
|
3
|
+
id: TId;
|
|
4
|
+
}
|
|
5
|
+
interface ReportDraft<TType extends string, TId, TReason extends string> {
|
|
6
|
+
target: ReportTarget<TType, TId>;
|
|
7
|
+
reason: TReason;
|
|
8
|
+
note: string | null;
|
|
9
|
+
}
|
|
10
|
+
export interface ReportInputConfig<TType extends string, TId, TReason extends string> {
|
|
11
|
+
targetTypes: readonly TType[];
|
|
12
|
+
reasons: readonly TReason[];
|
|
13
|
+
parseTargetId(value: string): TId | null;
|
|
14
|
+
maxNoteLength: number;
|
|
15
|
+
validate?(draft: ReportDraft<TType, TId, TReason>): void;
|
|
16
|
+
}
|
|
17
|
+
interface RawReportInput {
|
|
18
|
+
targetType?: unknown;
|
|
19
|
+
targetId?: unknown;
|
|
20
|
+
reason?: unknown;
|
|
21
|
+
note?: unknown;
|
|
22
|
+
}
|
|
23
|
+
export interface ReportInputParser<TType extends string, TId, TReason extends string> {
|
|
24
|
+
parse(raw: RawReportInput): ReportDraft<TType, TId, TReason>;
|
|
25
|
+
readonly targetTypes: readonly TType[];
|
|
26
|
+
readonly reasons: readonly TReason[];
|
|
27
|
+
}
|
|
28
|
+
export type ReportValidationCode = 'invalid_target_type' | 'invalid_target_identifier' | 'invalid_reason' | 'invalid_note' | 'note_too_long';
|
|
29
|
+
export declare class ReportValidationError extends Error {
|
|
30
|
+
readonly code: ReportValidationCode;
|
|
31
|
+
constructor(code: ReportValidationCode, message: string);
|
|
32
|
+
}
|
|
33
|
+
export declare function createReportInputParser<TType extends string, TId, TReason extends string>(config: ReportInputConfig<TType, TId, TReason>): ReportInputParser<TType, TId, TReason>;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export class ReportValidationError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(code, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = 'ReportValidationError';
|
|
6
|
+
this.code = code;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export function createReportInputParser(config) {
|
|
10
|
+
const targetTypes = readonlyCatalog(config.targetTypes, 'targetTypes');
|
|
11
|
+
const reasons = readonlyCatalog(config.reasons, 'reasons');
|
|
12
|
+
if (!Number.isSafeInteger(config.maxNoteLength) || config.maxNoteLength < 0)
|
|
13
|
+
throw new TypeError('maxNoteLength must be a non-negative safe integer');
|
|
14
|
+
return {
|
|
15
|
+
targetTypes,
|
|
16
|
+
reasons,
|
|
17
|
+
parse(raw) {
|
|
18
|
+
const targetType = enumValue(raw.targetType, targetTypes, 'invalid_target_type', 'target type');
|
|
19
|
+
if (typeof raw.targetId !== 'string')
|
|
20
|
+
invalid('invalid_target_identifier', 'target identifier');
|
|
21
|
+
const targetId = config.parseTargetId(raw.targetId);
|
|
22
|
+
if (targetId === null)
|
|
23
|
+
invalid('invalid_target_identifier', 'target identifier');
|
|
24
|
+
const reason = enumValue(raw.reason, reasons, 'invalid_reason', 'reason');
|
|
25
|
+
if (raw.note !== undefined && raw.note !== null && typeof raw.note !== 'string')
|
|
26
|
+
invalid('invalid_note', 'note');
|
|
27
|
+
if (typeof raw.note === 'string' && raw.note.length > config.maxNoteLength)
|
|
28
|
+
throw new ReportValidationError('note_too_long', 'Note is too long');
|
|
29
|
+
const note = typeof raw.note === 'string' ? raw.note.trim() || null : null;
|
|
30
|
+
const draft = { target: { type: targetType, id: targetId }, reason, note };
|
|
31
|
+
config.validate?.(draft);
|
|
32
|
+
return draft;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function readonlyCatalog(values, name) {
|
|
37
|
+
if (values.length === 0 ||
|
|
38
|
+
values.some((value) => typeof value !== 'string' || value.length === 0))
|
|
39
|
+
throw new TypeError(`${name} must contain non-empty strings`);
|
|
40
|
+
return Object.freeze([...new Set(values)]);
|
|
41
|
+
}
|
|
42
|
+
function enumValue(value, catalog, code, name) {
|
|
43
|
+
if (typeof value !== 'string' || !catalog.includes(value))
|
|
44
|
+
invalid(code, name);
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
function invalid(code, name) {
|
|
48
|
+
throw new ReportValidationError(code, `Invalid ${name}`);
|
|
49
|
+
}
|
package/dist/money.d.mts
CHANGED
|
@@ -19,6 +19,15 @@ export type MoneyRange<Code extends string = string> = {
|
|
|
19
19
|
minimum: Money<Code>;
|
|
20
20
|
maximum: Money<Code> | null;
|
|
21
21
|
};
|
|
22
|
+
export type ProportionalRounding = 'down' | 'up';
|
|
23
|
+
/**
|
|
24
|
+
* Allocates a non-negative integer amount by an inclusive fraction.
|
|
25
|
+
*
|
|
26
|
+
* The result is exact when `amount * numerator` divides by `denominator`; otherwise callers
|
|
27
|
+
* explicitly select integer rounding down or up. All arithmetic is performed with `bigint` so
|
|
28
|
+
* safe integer inputs never pass through floating-point division.
|
|
29
|
+
*/
|
|
30
|
+
export declare function allocateProportionalAmount(amount: number, numerator: number, denominator: number, rounding: ProportionalRounding): number;
|
|
22
31
|
export declare function createMoneyCatalog<const Currencies extends readonly Currency[]>(entries: Currencies, scale: number): {
|
|
23
32
|
currencies: readonly Readonly<{
|
|
24
33
|
code: string;
|
package/dist/money.mjs
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
export const MAX_MONEY_AMOUNT = Number.MAX_SAFE_INTEGER;
|
|
2
|
+
/**
|
|
3
|
+
* Allocates a non-negative integer amount by an inclusive fraction.
|
|
4
|
+
*
|
|
5
|
+
* The result is exact when `amount * numerator` divides by `denominator`; otherwise callers
|
|
6
|
+
* explicitly select integer rounding down or up. All arithmetic is performed with `bigint` so
|
|
7
|
+
* safe integer inputs never pass through floating-point division.
|
|
8
|
+
*/
|
|
9
|
+
export function allocateProportionalAmount(amount, numerator, denominator, rounding) {
|
|
10
|
+
if (!isAmount(amount) || !isAmount(numerator) || !isAmount(denominator))
|
|
11
|
+
throw new TypeError('Proportional allocation requires non-negative safe integer inputs');
|
|
12
|
+
if (denominator === 0 || numerator > denominator)
|
|
13
|
+
throw new RangeError('Proportional allocation requires a fraction from zero through one');
|
|
14
|
+
if (rounding !== 'down' && rounding !== 'up')
|
|
15
|
+
throw new TypeError('Proportional allocation rounding must be down or up');
|
|
16
|
+
const product = BigInt(amount) * BigInt(numerator);
|
|
17
|
+
const divisor = BigInt(denominator);
|
|
18
|
+
const quotient = product / divisor;
|
|
19
|
+
const result = rounding === 'up' && product % divisor !== 0n ? quotient + 1n : quotient;
|
|
20
|
+
return Number(result);
|
|
21
|
+
}
|
|
2
22
|
export function createMoneyCatalog(entries, scale) {
|
|
3
23
|
validateScale(scale);
|
|
4
24
|
for (const currency of entries)
|
package/dist/slugs.d.mts
CHANGED
package/dist/slugs.mjs
CHANGED
|
@@ -7,3 +7,7 @@ export function slugifyAscii(value) {
|
|
|
7
7
|
.replace(/-+/g, '-')
|
|
8
8
|
.replace(/^-|-$/g, '');
|
|
9
9
|
}
|
|
10
|
+
/** True for a non-empty string of lowercase ASCII letters, digits, and hyphens. */
|
|
11
|
+
export function isSlug(value) {
|
|
12
|
+
return value.length > 0 && /^[a-z0-9-]+$/.test(value);
|
|
13
|
+
}
|
package/dist/utm.d.mts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface UtmParserOptions {
|
|
2
|
+
sourceAliases: Readonly<Record<string, string>>;
|
|
3
|
+
fallbackSourceParam?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ExtractedUtmParams {
|
|
6
|
+
utmSource: string | null;
|
|
7
|
+
utmMedium: string | null;
|
|
8
|
+
utmCampaign: string | null;
|
|
9
|
+
utmContent: string | null;
|
|
10
|
+
}
|
|
11
|
+
export interface UtmParser {
|
|
12
|
+
resolveSource(value: string): string;
|
|
13
|
+
extractFromUrl(url: URL): ExtractedUtmParams;
|
|
14
|
+
}
|
|
15
|
+
/** Creates a UTM parser whose source aliases and fallback param are caller-owned. */
|
|
16
|
+
export declare function createUtmParser(options: UtmParserOptions): UtmParser;
|
package/dist/utm.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Creates a UTM parser whose source aliases and fallback param are caller-owned. */
|
|
2
|
+
export function createUtmParser(options) {
|
|
3
|
+
const sourceAliases = {};
|
|
4
|
+
for (const [key, value] of Object.entries(options.sourceAliases)) {
|
|
5
|
+
sourceAliases[key.toLowerCase().trim()] = value.toLowerCase().trim();
|
|
6
|
+
}
|
|
7
|
+
const fallbackSourceParam = options.fallbackSourceParam;
|
|
8
|
+
function resolveSource(value) {
|
|
9
|
+
const lower = value.toLowerCase().trim();
|
|
10
|
+
return sourceAliases[lower] ?? lower;
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
resolveSource,
|
|
14
|
+
extractFromUrl(url) {
|
|
15
|
+
const rawSource = url.searchParams.get('utm_source');
|
|
16
|
+
const fallback = fallbackSourceParam === undefined ? null : url.searchParams.get(fallbackSourceParam);
|
|
17
|
+
const normalizedSource = rawSource ? resolveSource(rawSource) || null : null;
|
|
18
|
+
const normalizedFallback = fallback ? resolveSource(fallback) || null : null;
|
|
19
|
+
const utmSource = normalizedSource ?? normalizedFallback;
|
|
20
|
+
return {
|
|
21
|
+
utmSource,
|
|
22
|
+
utmMedium: url.searchParams.get('utm_medium')?.toLowerCase().trim() || null,
|
|
23
|
+
utmCampaign: url.searchParams.get('utm_campaign')?.toLowerCase().trim() || null,
|
|
24
|
+
utmContent: url.searchParams.get('utm_content')?.toLowerCase().trim() || null,
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vouchington/utils",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
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": {
|
|
@@ -130,6 +130,11 @@
|
|
|
130
130
|
"import": "./dist/validation.mjs",
|
|
131
131
|
"default": "./dist/validation.mjs"
|
|
132
132
|
},
|
|
133
|
+
"./moderation": {
|
|
134
|
+
"types": "./dist/moderation.d.mts",
|
|
135
|
+
"import": "./dist/moderation.mjs",
|
|
136
|
+
"default": "./dist/moderation.mjs"
|
|
137
|
+
},
|
|
133
138
|
"./async": {
|
|
134
139
|
"types": "./dist/async.d.mts",
|
|
135
140
|
"import": "./dist/async.mjs",
|
|
@@ -150,6 +155,11 @@
|
|
|
150
155
|
"import": "./dist/hashtags.mjs",
|
|
151
156
|
"default": "./dist/hashtags.mjs"
|
|
152
157
|
},
|
|
158
|
+
"./utm": {
|
|
159
|
+
"types": "./dist/utm.d.mts",
|
|
160
|
+
"import": "./dist/utm.mjs",
|
|
161
|
+
"default": "./dist/utm.mjs"
|
|
162
|
+
},
|
|
153
163
|
"./fetch-ports": {
|
|
154
164
|
"types": "./dist/fetch-ports.d.mts",
|
|
155
165
|
"import": "./dist/fetch-ports.mjs",
|
|
@@ -164,16 +174,20 @@
|
|
|
164
174
|
"types": "./dist/http-link-pagination.d.mts",
|
|
165
175
|
"import": "./dist/http-link-pagination.mjs",
|
|
166
176
|
"default": "./dist/http-link-pagination.mjs"
|
|
177
|
+
},
|
|
178
|
+
"./http-signatures": {
|
|
179
|
+
"types": "./dist/http-signatures.d.mts",
|
|
180
|
+
"import": "./dist/http-signatures.mjs",
|
|
181
|
+
"default": "./dist/http-signatures.mjs"
|
|
167
182
|
}
|
|
168
183
|
},
|
|
169
184
|
"publishConfig": {
|
|
170
185
|
"access": "public"
|
|
171
186
|
},
|
|
172
|
-
"scripts": {
|
|
173
|
-
"build": "tsc --project tsconfig.build.json",
|
|
174
|
-
"prepack": "pnpm run build"
|
|
175
|
-
},
|
|
176
187
|
"engines": {
|
|
177
188
|
"node": ">=24.0.0"
|
|
189
|
+
},
|
|
190
|
+
"scripts": {
|
|
191
|
+
"build": "tsc --project tsconfig.build.json"
|
|
178
192
|
}
|
|
179
|
-
}
|
|
193
|
+
}
|