@vouchington/utils 0.1.0 → 0.2.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 +2 -0
- package/dist/collections.d.mts +3 -0
- package/dist/collections.mjs +14 -0
- package/dist/feature-flags.d.mts +14 -0
- package/dist/feature-flags.mjs +48 -0
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -27,6 +27,8 @@ import { serializeCookie } from '@vouchington/utils/cookies'
|
|
|
27
27
|
caller to supply every policy attribute. `SameSite=None` requires `Secure`.
|
|
28
28
|
- `collections`, `async`, and `stable-json`: collection deduplication/merging, bounded async mapping,
|
|
29
29
|
and deterministic JSON serialization.
|
|
30
|
+
- `feature-flags`: base64 JSON feature-flag cookie parsing and serialization with caller-injected
|
|
31
|
+
codecs, cookie names, and size policy.
|
|
30
32
|
- `dates`, `format`, `strings`, and `text-metrics`: UTC-day, duration, formatting, text normalization,
|
|
31
33
|
title casing, and word/sentence helpers.
|
|
32
34
|
- `gtin`, `bigint-ids`, and `validation`: GTIN predicates, canonical positive PostgreSQL bigint IDs,
|
package/dist/collections.d.mts
CHANGED
|
@@ -3,6 +3,9 @@ export declare function dedupeByLast<T, K>(items: readonly T[], key: (item: T) =
|
|
|
3
3
|
export declare function dedupeById<T extends {
|
|
4
4
|
id: string;
|
|
5
5
|
}>(items: readonly T[]): T[];
|
|
6
|
+
export declare function indexById<T extends {
|
|
7
|
+
id: string;
|
|
8
|
+
}>(values: readonly (T | null | undefined)[]): Record<string, T>;
|
|
6
9
|
export declare function mergePageResultsById<T extends {
|
|
7
10
|
id: string;
|
|
8
11
|
}>(pages?: readonly ({
|
package/dist/collections.mjs
CHANGED
|
@@ -14,6 +14,20 @@ export function dedupeByLast(items, key) {
|
|
|
14
14
|
export function dedupeById(items) {
|
|
15
15
|
return dedupeBy(items, (item) => item.id);
|
|
16
16
|
}
|
|
17
|
+
export function indexById(values) {
|
|
18
|
+
const indexed = {};
|
|
19
|
+
for (const value of values) {
|
|
20
|
+
if (value === null || value === undefined)
|
|
21
|
+
continue;
|
|
22
|
+
Object.defineProperty(indexed, value.id, {
|
|
23
|
+
configurable: true,
|
|
24
|
+
enumerable: true,
|
|
25
|
+
value,
|
|
26
|
+
writable: true,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return indexed;
|
|
30
|
+
}
|
|
17
31
|
export function mergePageResultsById(pages) {
|
|
18
32
|
return dedupeById((pages ?? []).flatMap((page) => page?.results ?? []));
|
|
19
33
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type FeatureFlags = Record<string, boolean>;
|
|
2
|
+
export type FeatureFlagCookieCodec = {
|
|
3
|
+
encodeBase64(value: string): string;
|
|
4
|
+
decodeBase64(value: string): string;
|
|
5
|
+
};
|
|
6
|
+
export type FeatureFlagCookieOptions = {
|
|
7
|
+
maxCookieLength?: number;
|
|
8
|
+
};
|
|
9
|
+
export declare const DEFAULT_FEATURE_FLAG_COOKIE_MAX_LENGTH = 4096;
|
|
10
|
+
export declare function extractBooleanFeatureFlags(fields: Record<string, unknown>): FeatureFlags;
|
|
11
|
+
export declare function parseFeatureFlagCookie(cookieValue: string, codec: FeatureFlagCookieCodec, options?: FeatureFlagCookieOptions): FeatureFlags;
|
|
12
|
+
export declare function encodeFeatureFlagCookie(overrides: FeatureFlags, codec: FeatureFlagCookieCodec): string;
|
|
13
|
+
export declare function parseFeatureFlagCookieMaxLength(value: string | undefined, fallback?: number): number;
|
|
14
|
+
export declare function safeFeatureFlagCookiePart(cookieName: string, cookieValue: string | undefined, options?: FeatureFlagCookieOptions): string | null;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const DEFAULT_FEATURE_FLAG_COOKIE_MAX_LENGTH = 4096;
|
|
2
|
+
const BASE64 = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
3
|
+
const COOKIE_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
4
|
+
export function extractBooleanFeatureFlags(fields) {
|
|
5
|
+
const flags = {};
|
|
6
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
7
|
+
if (typeof value === 'boolean') {
|
|
8
|
+
Object.defineProperty(flags, key, {
|
|
9
|
+
configurable: true,
|
|
10
|
+
enumerable: true,
|
|
11
|
+
value,
|
|
12
|
+
writable: true,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return flags;
|
|
17
|
+
}
|
|
18
|
+
export function parseFeatureFlagCookie(cookieValue, codec, options = {}) {
|
|
19
|
+
try {
|
|
20
|
+
if (!isSafeCookieValue(cookieValue, options))
|
|
21
|
+
return {};
|
|
22
|
+
const parsed = JSON.parse(codec.decodeBase64(cookieValue));
|
|
23
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
24
|
+
return {};
|
|
25
|
+
return extractBooleanFeatureFlags(parsed);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function encodeFeatureFlagCookie(overrides, codec) {
|
|
32
|
+
return codec.encodeBase64(JSON.stringify(overrides));
|
|
33
|
+
}
|
|
34
|
+
export function parseFeatureFlagCookieMaxLength(value, fallback = DEFAULT_FEATURE_FLAG_COOKIE_MAX_LENGTH) {
|
|
35
|
+
const parsed = Number.parseInt(value ?? '', 10);
|
|
36
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
37
|
+
}
|
|
38
|
+
export function safeFeatureFlagCookiePart(cookieName, cookieValue, options = {}) {
|
|
39
|
+
return COOKIE_NAME.test(cookieName) && isSafeCookieValue(cookieValue, options)
|
|
40
|
+
? `${cookieName}=${cookieValue}`
|
|
41
|
+
: null;
|
|
42
|
+
}
|
|
43
|
+
function isSafeCookieValue(value, { maxCookieLength = DEFAULT_FEATURE_FLAG_COOKIE_MAX_LENGTH }) {
|
|
44
|
+
return (typeof value === 'string' &&
|
|
45
|
+
value.length > 0 &&
|
|
46
|
+
value.length <= maxCookieLength &&
|
|
47
|
+
BASE64.test(value));
|
|
48
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vouchington/utils",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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": {
|
|
@@ -60,6 +60,11 @@
|
|
|
60
60
|
"import": "./dist/collections.mjs",
|
|
61
61
|
"default": "./dist/collections.mjs"
|
|
62
62
|
},
|
|
63
|
+
"./feature-flags": {
|
|
64
|
+
"types": "./dist/feature-flags.d.mts",
|
|
65
|
+
"import": "./dist/feature-flags.mjs",
|
|
66
|
+
"default": "./dist/feature-flags.mjs"
|
|
67
|
+
},
|
|
63
68
|
"./dates": {
|
|
64
69
|
"types": "./dist/dates.d.mts",
|
|
65
70
|
"import": "./dist/dates.mjs",
|