@vouchington/utils 0.7.3 → 0.7.5

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 CHANGED
@@ -18,6 +18,11 @@ import { createScopeCatalog } from '@vouchington/utils/scopes'
18
18
 
19
19
  ## Subpaths
20
20
 
21
+ - `ai-moderation`: moderation input building, flagged-category and prompt-injection checks that
22
+ return violations; the caller makes the provider call.
23
+ - `bedrock-embeddings` and `bedrock-embedding-batches`: Nova `InvokeModel` input builders, response
24
+ parsing and dense-vector validation, and batch JSONL record and result codecs with opaque record
25
+ ids; the caller owns the AWS client.
21
26
  - `cache-tags`: canonical printable-ASCII wire encoding, one-segment URL decoding, validation,
22
27
  and escape-boundary truncation. Applications own family names, route recognition, and purge policy.
23
28
  Single-tag validation rejects spaces and comma separators; raw commas are percent-encoded.
@@ -31,6 +36,10 @@ import { createScopeCatalog } from '@vouchington/utils/scopes'
31
36
  request/acknowledge round trip with an injected message type and acceptance check, and a named
32
37
  Web Lock wrapper. Request actions, expected state transitions, and the bind/clear/reconcile
33
38
  sequence stay in the application.
39
+ - `service-worker-activation`: `waitForActiveServiceWorker(registration, options)` resolves when
40
+ the registration has an active worker. An installing or waiting worker is preferred over
41
+ `serviceWorker.ready`. `timeoutMs` bounds the wait; omit it to wait without a deadline. The
42
+ caller supplies the timeout and may inject `serviceWorker` (default `navigator.serviceWorker`).
34
43
  - `json-ld`: schema.org JSON-LD builders (organization, website, navigation, collection, thing,
35
44
  comparison, item list, breadcrumb, profile) and script-safe serialization. Callers inject the
36
45
  absolute-URL resolver and site name; product schemas such as posts stay in the application.
@@ -0,0 +1,34 @@
1
+ /** The text or image parts a moderation endpoint accepts. */
2
+ export type ModerationInputPart = {
3
+ type: 'text';
4
+ text: string;
5
+ } | {
6
+ type: 'image_url';
7
+ image_url: {
8
+ url: string;
9
+ };
10
+ };
11
+ /** The fields of a moderation result this module reads. */
12
+ export type ModerationResultLike = {
13
+ flagged: boolean;
14
+ categories: Record<string, boolean>;
15
+ };
16
+ export type MessageSafetyViolation = {
17
+ code: 'PROMPT_INJECTION';
18
+ } | {
19
+ code: 'MODERATION_VIOLATION';
20
+ categories: string[];
21
+ };
22
+ /** Text first, then images, in the order given. */
23
+ export declare function buildModerationInput(texts: readonly string[], imageUrls?: readonly string[]): ModerationInputPart[];
24
+ /** Names of the categories a result flagged. */
25
+ export declare function getFlaggedCategories(result: ModerationResultLike): string[];
26
+ /**
27
+ * A violation naming every category flagged by any result, else null. Results line up with the
28
+ * request's inputs, so a later flagged input is never masked by a clean earlier one.
29
+ */
30
+ export declare function checkModerationResults(results: readonly ModerationResultLike[]): MessageSafetyViolation | null;
31
+ /** Common instruction-override and chat-template markers. Callers may pass their own list. */
32
+ export declare const DEFAULT_PROMPT_INJECTION_PATTERNS: readonly RegExp[];
33
+ /** A `PROMPT_INJECTION` violation when any pattern matches the message, else null. */
34
+ export declare function checkPromptInjection(message: string, patterns?: readonly RegExp[]): MessageSafetyViolation | null;
@@ -0,0 +1,54 @@
1
+ /** Text first, then images, in the order given. */
2
+ export function buildModerationInput(texts, imageUrls = []) {
3
+ return [
4
+ ...texts.map((text) => ({ type: 'text', text })),
5
+ ...imageUrls.map((url) => ({ type: 'image_url', image_url: { url } })),
6
+ ];
7
+ }
8
+ /** Names of the categories a result flagged. */
9
+ export function getFlaggedCategories(result) {
10
+ return Object.entries(result.categories).flatMap(([category, flagged]) => flagged ? [category] : []);
11
+ }
12
+ /**
13
+ * A violation naming every category flagged by any result, else null. Results line up with the
14
+ * request's inputs, so a later flagged input is never masked by a clean earlier one.
15
+ */
16
+ export function checkModerationResults(results) {
17
+ const flagged = results.filter((result) => result.flagged);
18
+ if (flagged.length === 0)
19
+ return null;
20
+ const categories = [...new Set(flagged.flatMap(getFlaggedCategories))];
21
+ return { code: 'MODERATION_VIOLATION', categories };
22
+ }
23
+ /** Common instruction-override and chat-template markers. Callers may pass their own list. */
24
+ export const DEFAULT_PROMPT_INJECTION_PATTERNS = [
25
+ /ignore\s+(all\s+)?(previous|prior|earlier)\s+(instructions?|prompts?|rules?)/i,
26
+ /disregard\s+(all\s+)?(previous|prior|earlier)\s+(instructions?|prompts?|rules?)/i,
27
+ /forget\s+(all\s+)?(previous|prior|earlier)\s+(instructions?|prompts?|rules?)/i,
28
+ // A chat-template style role prefix at the start of a line, not "operating system: Linux".
29
+ /(?:^|\n)\s*system\s*:/i,
30
+ /<\|im_start\|>/i,
31
+ /<\|im_end\|>/i,
32
+ /\[INST\]/i,
33
+ /\[\/INST\]/i,
34
+ /<system>/i,
35
+ /<\/system>/i,
36
+ // Require "now" to avoid benign phrases like "you are a great help"
37
+ /you\s+are\s+now\s+(a|an)\s+/i,
38
+ // A heading-style "new instructions:" at the start of a line, not "new instructions for the cake".
39
+ /(?:^|\n)\s*new\s+instructions?\s*:/i,
40
+ /override\s+(previous|prior|all)\s+(instructions?|rules?)/i,
41
+ ];
42
+ /** A `PROMPT_INJECTION` violation when any pattern matches the message, else null. */
43
+ export function checkPromptInjection(message, patterns = DEFAULT_PROMPT_INJECTION_PATTERNS) {
44
+ const matched = patterns.some((pattern) => {
45
+ // Global and sticky patterns are stateful; test from the start, then leave the caller's regex
46
+ // as it was found.
47
+ const previous = pattern.lastIndex;
48
+ pattern.lastIndex = 0;
49
+ const result = pattern.test(message);
50
+ pattern.lastIndex = previous;
51
+ return result;
52
+ });
53
+ return matched ? { code: 'PROMPT_INJECTION' } : null;
54
+ }
@@ -0,0 +1,53 @@
1
+ import { type EmbeddingOptions } from './bedrock-embeddings.mts';
2
+ /**
3
+ * Nova accepts inline media up to 25 MB after Base64 encoding; 18,000,000 raw bytes encode to
4
+ * 24,000,000, leaving room for framing; callers that enforce a smaller
5
+ * decoded bound (e.g. after converting images) pass it as `maxImageBytes`.
6
+ */
7
+ export declare const DEFAULT_MAX_IMAGE_EMBEDDING_BYTES = 18000000;
8
+ export type BatchSizeOptions = EmbeddingOptions & {
9
+ /** Decoded image bytes bound used to budget the worst-case record. */
10
+ maxImageBytes?: number;
11
+ /**
12
+ * Longest `recordId` in UTF-16 code units (default 128). Budgeted at JSON's worst case of six
13
+ * bytes per unit, since escapes can expand the encoded line.
14
+ */
15
+ maxRecordIdLength?: number;
16
+ };
17
+ /** One JSONL line of a batch input file. `recordId` is opaque and comes back on the result line. */
18
+ export declare function createEmbeddingRecordLine(recordId: string, modelInput: Record<string, unknown>): string;
19
+ export declare function createEmbeddingTextRecordLine(record: {
20
+ recordId: string;
21
+ content: string;
22
+ }, options?: EmbeddingOptions): string;
23
+ export declare function createEmbeddingImageRecordLine(record: {
24
+ recordId: string;
25
+ format: 'jpeg' | 'png' | 'webp' | 'gif';
26
+ bytes: string;
27
+ }, options?: EmbeddingOptions): string;
28
+ /** Lower bound on a batch file for `records` images: worst-case payload plus actual framing. */
29
+ export declare function minimumImageBatchSizeMB(records: number, options?: BatchSizeOptions): number;
30
+ /** Same for text; JSON control-character escapes cost up to six bytes per UTF-16 code unit. */
31
+ export declare function minimumTextBatchSizeMB(records: number, options?: BatchSizeOptions): number;
32
+ /** One line of a batch output file. */
33
+ export type BatchResultLine = {
34
+ recordId: string;
35
+ error?: unknown;
36
+ modelOutput?: {
37
+ embedding?: number[];
38
+ embeddings?: Array<{
39
+ embedding: number[];
40
+ } | number[]>;
41
+ inputTokenCount?: number;
42
+ };
43
+ };
44
+ export type BatchEmbeddingResult = {
45
+ recordId: string;
46
+ embedding: number[];
47
+ inputTokenCount: number | null;
48
+ };
49
+ /**
50
+ * The vector of a result line, or null for errored lines and lines without one. Decoding the
51
+ * opaque `recordId` and validating the vector's dimension stay with the application.
52
+ */
53
+ export declare function parseBatchResult(line: BatchResultLine): BatchEmbeddingResult | null;
@@ -0,0 +1,51 @@
1
+ import { buildEmbeddingModelInput, extractEmbedding, NOVA_EMBEDDING_DEFAULTS, } from './bedrock-embeddings.mjs';
2
+ /**
3
+ * Nova accepts inline media up to 25 MB after Base64 encoding; 18,000,000 raw bytes encode to
4
+ * 24,000,000, leaving room for framing; callers that enforce a smaller
5
+ * decoded bound (e.g. after converting images) pass it as `maxImageBytes`.
6
+ */
7
+ export const DEFAULT_MAX_IMAGE_EMBEDDING_BYTES = 18_000_000;
8
+ const DEFAULT_MAX_RECORD_ID_LENGTH = 128;
9
+ /** A control character JSON-escapes to six bytes (`\u0001`), the worst case per code unit. */
10
+ const WORST_CASE_ID_UNIT = '\u0001';
11
+ /** One JSONL line of a batch input file. `recordId` is opaque and comes back on the result line. */
12
+ export function createEmbeddingRecordLine(recordId, modelInput) {
13
+ return JSON.stringify({ recordId, modelInput });
14
+ }
15
+ export function createEmbeddingTextRecordLine(record, options = {}) {
16
+ return createEmbeddingRecordLine(record.recordId, buildEmbeddingModelInput({ text: record.content }, options));
17
+ }
18
+ export function createEmbeddingImageRecordLine(record, options = {}) {
19
+ return createEmbeddingRecordLine(record.recordId, buildEmbeddingModelInput({ image: { format: record.format, bytes: record.bytes } }, options));
20
+ }
21
+ function framingBytes(line) {
22
+ return Buffer.byteLength(line) + 1;
23
+ }
24
+ /** Lower bound on a batch file for `records` images: worst-case payload plus actual framing. */
25
+ export function minimumImageBatchSizeMB(records, options = {}) {
26
+ const recordId = WORST_CASE_ID_UNIT.repeat(options.maxRecordIdLength ?? DEFAULT_MAX_RECORD_ID_LENGTH);
27
+ const framing = framingBytes(createEmbeddingImageRecordLine({ recordId, format: 'jpeg', bytes: '' }, options));
28
+ const maxBytes = options.maxImageBytes ?? DEFAULT_MAX_IMAGE_EMBEDDING_BYTES;
29
+ return (records * (Math.ceil(maxBytes / 3) * 4 + framing)) / 1024 / 1024;
30
+ }
31
+ /** Same for text; JSON control-character escapes cost up to six bytes per UTF-16 code unit. */
32
+ export function minimumTextBatchSizeMB(records, options = {}) {
33
+ const recordId = WORST_CASE_ID_UNIT.repeat(options.maxRecordIdLength ?? DEFAULT_MAX_RECORD_ID_LENGTH);
34
+ const maxTextLength = options.maxTextLength ?? NOVA_EMBEDDING_DEFAULTS.maxTextLength;
35
+ const framing = framingBytes(createEmbeddingTextRecordLine({ recordId, content: '' }, options));
36
+ return (records * (maxTextLength * 6 + framing)) / 1024 / 1024;
37
+ }
38
+ /**
39
+ * The vector of a result line, or null for errored lines and lines without one. Decoding the
40
+ * opaque `recordId` and validating the vector's dimension stay with the application.
41
+ */
42
+ export function parseBatchResult(line) {
43
+ const embedding = line.error ? undefined : extractEmbedding(line.modelOutput);
44
+ if (!embedding)
45
+ return null;
46
+ return {
47
+ recordId: line.recordId,
48
+ embedding,
49
+ inputTokenCount: line.modelOutput?.inputTokenCount ?? null,
50
+ };
51
+ }
@@ -0,0 +1,62 @@
1
+ /** Defaults for Amazon Nova multimodal embeddings; every field can be overridden. */
2
+ export declare const NOVA_EMBEDDING_DEFAULTS: {
3
+ readonly modelId: 'amazon.nova-2-multimodal-embeddings-v1:0';
4
+ readonly dimension: 1024;
5
+ /** Nova's inline `text.value` cap (UTF-16 code units). */
6
+ readonly maxTextLength: 8192;
7
+ /** Nova documents no input token count; this approximate estimate is for analytics only. */
8
+ readonly charactersPerToken: 4;
9
+ };
10
+ export type EmbeddingOptions = {
11
+ modelId?: string;
12
+ dimension?: number;
13
+ maxTextLength?: number;
14
+ /** `embeddingPurpose`: `GENERIC_INDEX` (default) for stored content, a retrieval purpose for queries. */
15
+ purpose?: string;
16
+ /** Nova image `detailLevel` (e.g. `STANDARD_IMAGE` or `DOCUMENT_IMAGE`); omitted by default. */
17
+ imageDetailLevel?: string;
18
+ };
19
+ /** The `InvokeModel` input; the app wraps it in its own command and client. */
20
+ export type InvokeEmbeddingInput = {
21
+ modelId: string;
22
+ contentType: 'application/json';
23
+ accept: 'application/json';
24
+ body: string;
25
+ };
26
+ type EmbeddingEntry = {
27
+ embedding: number[];
28
+ } | number[];
29
+ type EmbeddingResponseBody = {
30
+ embedding?: number[];
31
+ embeddings?: EmbeddingEntry[];
32
+ };
33
+ export declare class InvalidEmbeddingError extends Error {
34
+ constructor(message: string, options?: ErrorOptions);
35
+ }
36
+ /**
37
+ * Slices to the limit. When the cut falls inside a word, drops a trailing high surrogate and backs
38
+ * up to the last whitespace so the word is not cut; a cut that already ends a word is kept. Falls
39
+ * back to the hard slice when the head has no whitespace.
40
+ */
41
+ export declare function truncateEmbeddingText(text: string, maxLength?: number): string;
42
+ export declare function estimateEmbeddingTokens(text: string, charactersPerToken?: number): number;
43
+ /** The model input for one text or image, shared by the single and batch record formats. */
44
+ export declare function buildEmbeddingModelInput(content: {
45
+ text: string;
46
+ } | {
47
+ image: {
48
+ format: 'jpeg' | 'png' | 'webp' | 'gif';
49
+ bytes: string;
50
+ };
51
+ }, options?: EmbeddingOptions): Record<string, unknown>;
52
+ export declare function buildInvokeEmbeddingInput(text: string, options?: EmbeddingOptions): InvokeEmbeddingInput;
53
+ /** The first embedding of a response or batch `modelOutput`, in either documented shape. */
54
+ export declare function extractEmbedding(body: EmbeddingResponseBody | undefined): number[] | undefined;
55
+ /**
56
+ * Parses an `InvokeModel` response body into a dense vector of `dimension` finite numbers; throws
57
+ * `InvalidEmbeddingError` when the body is empty or holds no valid vector.
58
+ */
59
+ export declare function parseEmbeddingResponseBody(body: Uint8Array | undefined, dimension?: number): number[];
60
+ /** Narrows to a finite-number vector of exactly `dimension` entries or throws. */
61
+ export declare function requireDenseEmbedding(value: unknown, dimension?: number): number[];
62
+ export {};
@@ -0,0 +1,106 @@
1
+ /** Defaults for Amazon Nova multimodal embeddings; every field can be overridden. */
2
+ export const NOVA_EMBEDDING_DEFAULTS = {
3
+ modelId: 'amazon.nova-2-multimodal-embeddings-v1:0',
4
+ dimension: 1024,
5
+ /** Nova's inline `text.value` cap (UTF-16 code units). */
6
+ maxTextLength: 8_192,
7
+ /** Nova documents no input token count; this approximate estimate is for analytics only. */
8
+ charactersPerToken: 4,
9
+ };
10
+ export class InvalidEmbeddingError extends Error {
11
+ constructor(message, options) {
12
+ super(message, options);
13
+ this.name = 'InvalidEmbeddingError';
14
+ }
15
+ }
16
+ /**
17
+ * Slices to the limit. When the cut falls inside a word, drops a trailing high surrogate and backs
18
+ * up to the last whitespace so the word is not cut; a cut that already ends a word is kept. Falls
19
+ * back to the hard slice when the head has no whitespace.
20
+ */
21
+ export function truncateEmbeddingText(text, maxLength = NOVA_EMBEDDING_DEFAULTS.maxTextLength) {
22
+ if (text.length <= maxLength)
23
+ return text;
24
+ let sliced = text.slice(0, maxLength);
25
+ if (/\s/.test(text.charAt(maxLength)))
26
+ return sliced;
27
+ const last = sliced.charCodeAt(sliced.length - 1);
28
+ if (last >= 0xd800 && last <= 0xdbff)
29
+ sliced = sliced.slice(0, -1);
30
+ const lastWhitespace = sliced.search(/\s\S*$/);
31
+ return lastWhitespace > 0 ? sliced.slice(0, lastWhitespace) : sliced;
32
+ }
33
+ export function estimateEmbeddingTokens(text, charactersPerToken = NOVA_EMBEDDING_DEFAULTS.charactersPerToken) {
34
+ return Math.ceil(text.length / charactersPerToken);
35
+ }
36
+ /** The model input for one text or image, shared by the single and batch record formats. */
37
+ export function buildEmbeddingModelInput(content, options = {}) {
38
+ const dimension = options.dimension ?? NOVA_EMBEDDING_DEFAULTS.dimension;
39
+ const common = {
40
+ embeddingPurpose: options.purpose ?? 'GENERIC_INDEX',
41
+ embeddingDimension: dimension,
42
+ };
43
+ return {
44
+ taskType: 'SINGLE_EMBEDDING',
45
+ singleEmbeddingParams: 'text' in content
46
+ ? {
47
+ ...common,
48
+ text: {
49
+ truncationMode: 'END',
50
+ value: truncateEmbeddingText(content.text, options.maxTextLength),
51
+ },
52
+ }
53
+ : {
54
+ ...common,
55
+ image: {
56
+ format: content.image.format,
57
+ source: { bytes: content.image.bytes },
58
+ ...(options.imageDetailLevel ? { detailLevel: options.imageDetailLevel } : {}),
59
+ },
60
+ },
61
+ };
62
+ }
63
+ export function buildInvokeEmbeddingInput(text, options = {}) {
64
+ return {
65
+ modelId: options.modelId ?? NOVA_EMBEDDING_DEFAULTS.modelId,
66
+ contentType: 'application/json',
67
+ accept: 'application/json',
68
+ body: JSON.stringify(buildEmbeddingModelInput({ text }, options)),
69
+ };
70
+ }
71
+ /** The first embedding of a response or batch `modelOutput`, in either documented shape. */
72
+ export function extractEmbedding(body) {
73
+ if (body?.embedding)
74
+ return body.embedding;
75
+ const first = body?.embeddings?.[0];
76
+ return Array.isArray(first) ? first : first?.embedding;
77
+ }
78
+ /**
79
+ * Parses an `InvokeModel` response body into a dense vector of `dimension` finite numbers; throws
80
+ * `InvalidEmbeddingError` when the body is empty or holds no valid vector.
81
+ */
82
+ export function parseEmbeddingResponseBody(body, dimension = NOVA_EMBEDDING_DEFAULTS.dimension) {
83
+ if (!body)
84
+ throw new InvalidEmbeddingError('Bedrock embedding response body is empty');
85
+ let parsed;
86
+ try {
87
+ parsed = JSON.parse(new TextDecoder().decode(body));
88
+ }
89
+ catch (err) {
90
+ throw new InvalidEmbeddingError('Bedrock embedding response is not valid JSON', { cause: err });
91
+ }
92
+ const embedding = extractEmbedding(parsed);
93
+ if (!embedding) {
94
+ throw new InvalidEmbeddingError('Bedrock embedding response did not include an embedding');
95
+ }
96
+ return requireDenseEmbedding(embedding, dimension);
97
+ }
98
+ /** Narrows to a finite-number vector of exactly `dimension` entries or throws. */
99
+ export function requireDenseEmbedding(value, dimension = NOVA_EMBEDDING_DEFAULTS.dimension) {
100
+ if (!Array.isArray(value) ||
101
+ value.length !== dimension ||
102
+ value.some((element) => typeof element !== 'number' || !Number.isFinite(element))) {
103
+ throw new InvalidEmbeddingError('Bedrock embedding is not a dense vector');
104
+ }
105
+ return value;
106
+ }
@@ -0,0 +1,26 @@
1
+ export declare class ServiceWorkerActivationTimeoutError extends Error {
2
+ constructor();
3
+ }
4
+ export interface WaitForActiveServiceWorkerOptions {
5
+ timeoutMs?: number;
6
+ /** Defaults to navigator.serviceWorker; injectable for tests. */
7
+ serviceWorker?: ServiceWorkerContainer;
8
+ }
9
+ declare global {
10
+ interface ServiceWorker extends EventTarget {
11
+ readonly state: 'parsed' | 'installing' | 'installed' | 'activating' | 'activated' | 'redundant';
12
+ }
13
+ interface ServiceWorkerRegistration {
14
+ readonly active: ServiceWorker | null;
15
+ readonly installing: ServiceWorker | null;
16
+ readonly waiting: ServiceWorker | null;
17
+ }
18
+ interface ServiceWorkerContainer {
19
+ readonly ready: Promise<ServiceWorkerRegistration>;
20
+ }
21
+ interface Navigator {
22
+ readonly serviceWorker: ServiceWorkerContainer;
23
+ }
24
+ }
25
+ /** Resolves when `registration` has an active worker. Pass `timeoutMs` to bound the wait. */
26
+ export declare function waitForActiveServiceWorker(registration: ServiceWorkerRegistration, options?: WaitForActiveServiceWorkerOptions): Promise<ServiceWorkerRegistration>;
@@ -0,0 +1,50 @@
1
+ export class ServiceWorkerActivationTimeoutError extends Error {
2
+ constructor() {
3
+ super('Timed out waiting for the service worker to activate.');
4
+ this.name = 'ServiceWorkerActivationTimeoutError';
5
+ }
6
+ }
7
+ /** Resolves when `registration` has an active worker. Pass `timeoutMs` to bound the wait. */
8
+ export function waitForActiveServiceWorker(registration, options = {}) {
9
+ const replacement = registration.installing ?? registration.waiting;
10
+ if (!replacement) {
11
+ if (registration.active)
12
+ return Promise.resolve(registration);
13
+ const container = options.serviceWorker ?? navigator.serviceWorker;
14
+ return waitWithOptionalTimeout(container.ready, options.timeoutMs);
15
+ }
16
+ if (replacement.state === 'activated')
17
+ return Promise.resolve(registration);
18
+ return waitForReplacement(registration, replacement, options.timeoutMs);
19
+ }
20
+ function waitForReplacement(registration, worker, timeoutMs) {
21
+ return new Promise((resolve, reject) => {
22
+ const timeout = timeoutMs === undefined
23
+ ? undefined
24
+ : setTimeout(() => finish(() => reject(new ServiceWorkerActivationTimeoutError())), timeoutMs);
25
+ function finish(settle) {
26
+ worker.removeEventListener('statechange', onStateChange);
27
+ if (timeout !== undefined)
28
+ clearTimeout(timeout);
29
+ settle();
30
+ }
31
+ function onStateChange() {
32
+ if (worker.state === 'activated')
33
+ finish(() => resolve(registration));
34
+ else if (worker.state === 'redundant') {
35
+ finish(() => reject(new Error('Service worker replacement became redundant.')));
36
+ }
37
+ }
38
+ worker.addEventListener('statechange', onStateChange);
39
+ onStateChange();
40
+ });
41
+ }
42
+ function waitWithOptionalTimeout(ready, timeoutMs) {
43
+ if (timeoutMs === undefined)
44
+ return ready;
45
+ let timeout;
46
+ const timedOut = new Promise((_, reject) => {
47
+ timeout = setTimeout(() => reject(new ServiceWorkerActivationTimeoutError()), timeoutMs);
48
+ });
49
+ return Promise.race([ready, timedOut]).finally(() => clearTimeout(timeout));
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vouchington/utils",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
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,21 @@
20
20
  ],
21
21
  "type": "module",
22
22
  "exports": {
23
+ "./ai-moderation": {
24
+ "types": "./dist/ai-moderation.d.mts",
25
+ "import": "./dist/ai-moderation.mjs",
26
+ "default": "./dist/ai-moderation.mjs"
27
+ },
28
+ "./bedrock-embedding-batches": {
29
+ "types": "./dist/bedrock-embedding-batches.d.mts",
30
+ "import": "./dist/bedrock-embedding-batches.mjs",
31
+ "default": "./dist/bedrock-embedding-batches.mjs"
32
+ },
33
+ "./bedrock-embeddings": {
34
+ "types": "./dist/bedrock-embeddings.d.mts",
35
+ "import": "./dist/bedrock-embeddings.mjs",
36
+ "default": "./dist/bedrock-embeddings.mjs"
37
+ },
23
38
  "./api-errors": {
24
39
  "types": "./dist/api-errors.d.mts",
25
40
  "import": "./dist/api-errors.mjs",
@@ -50,6 +65,11 @@
50
65
  "import": "./dist/web-push-binding.mjs",
51
66
  "default": "./dist/web-push-binding.mjs"
52
67
  },
68
+ "./service-worker-activation": {
69
+ "types": "./dist/service-worker-activation.d.mts",
70
+ "import": "./dist/service-worker-activation.mjs",
71
+ "default": "./dist/service-worker-activation.mjs"
72
+ },
53
73
  "./json-ld": {
54
74
  "types": "./dist/json-ld.d.mts",
55
75
  "import": "./dist/json-ld.mjs",