sos-sdk-node-ts 0.1.2 → 0.1.3

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
@@ -9,7 +9,7 @@ deterministic failures and never commit a local package reference. Staging
9
9
  begins only after exact registry packages are installed and verified.
10
10
 
11
11
 
12
- SOSChain-specific Node adapters built above the shared runtime-neutral SDK.
12
+ Product-neutral Node adapters shared by SOSChain, Vet and UHC trusted runtimes.
13
13
 
14
14
  ```text
15
15
  gdc-* frozen
@@ -21,10 +21,10 @@ sos-sdk-core-ts
21
21
  sos-sdk-node-ts
22
22
  ```
23
23
 
24
- This package owns server-side SOSChain transport, credentials, persistence and
25
- jobs. It does not own deterministic data contracts or common authorization.
26
- Vet and UHC consume `sos-sdk-core-ts` independently; they do not consume this
27
- SOSChain runtime package.
24
+ This package owns reusable server-side Node transport, credentials,
25
+ persistence, jobs and prompt-audio runtime behavior. It does not own
26
+ deterministic data contracts, common authorization or sector policy. Vet, UHC
27
+ and SOSChain may consume it without depending on one another.
28
28
 
29
29
  The first adapter, `SosEmergencyDispatchClient`, sends an already-authorized
30
30
  emergency request over HTTPS with a stable idempotency key and validates the
@@ -43,6 +43,16 @@ API. Applications select the protected `contentType`, apply their own upload or
43
43
  audio-duration limits, and authorize every recipient change before calling the
44
44
  shared core. See [docs/SECURE_MESSAGES.md](docs/SECURE_MESSAGES.md).
45
45
 
46
+ ## Immutable prompt audio
47
+
48
+ `sos-sdk-node-ts/prompt-audio-runtime` hashes the canonical descriptor produced
49
+ by `sos-data-utils-ts/prompt-audio`, coalesces concurrent misses, persists the
50
+ audio and prewarms prompts sequentially. It includes a filesystem adapter for
51
+ local execution and an ElevenLabs multilingual-v2 synthesizer. A production
52
+ runtime supplies its approved durable-object-store adapter. Complete live OTPs
53
+ are never cache entries; ICA composes static prefix and digit assets. See
54
+ [docs/PROMPT_AUDIO_RUNTIME.md](docs/PROMPT_AUDIO_RUNTIME.md).
55
+
46
56
  ```bash
47
57
  npm ci
48
58
  npm run check
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
- /** SOSChain-specific Node adapters over the shared runtime-neutral SDK. */
1
+ /** Product-neutral Node adapters shared by SOSChain, Vet and UHC runtimes. */
2
2
  export * from './emergency-dispatch.js';
3
+ export * from './prompt-audio-runtime.js';
3
4
  export * from './secure-message-crypto.js';
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
- /** SOSChain-specific Node adapters over the shared runtime-neutral SDK. */
1
+ /** Product-neutral Node adapters shared by SOSChain, Vet and UHC runtimes. */
2
2
  export * from './emergency-dispatch.js';
3
+ export * from './prompt-audio-runtime.js';
3
4
  export * from './secure-message-crypto.js';
@@ -0,0 +1,85 @@
1
+ export interface PromptAudioObjectStore {
2
+ get(objectKey: string): Promise<Uint8Array | undefined>;
3
+ putIfAbsent(objectKey: string, audio: Uint8Array): Promise<void>;
4
+ }
5
+ /** Structural subset of a GCS Bucket; keeps the shared SDK cloud-client-version agnostic. */
6
+ export interface PromptAudioGcsBucket {
7
+ file(name: string): {
8
+ download(): Promise<readonly [Buffer]>;
9
+ save(data: Uint8Array, options: Readonly<{
10
+ resumable: false;
11
+ contentType: string;
12
+ metadata: Readonly<{
13
+ cacheControl: string;
14
+ }>;
15
+ preconditionOpts: Readonly<{
16
+ ifGenerationMatch: number;
17
+ }>;
18
+ }>): Promise<unknown>;
19
+ };
20
+ }
21
+ export interface CachedPromptAudio {
22
+ objectKey: string;
23
+ audio: Uint8Array;
24
+ created: boolean;
25
+ }
26
+ export interface ElevenLabsPromptAudioRequest {
27
+ voiceId: string;
28
+ text: string;
29
+ modelId?: string;
30
+ outputFormat?: string;
31
+ }
32
+ /** Hash a canonical descriptor produced by `sos-data-utils-ts/prompt-audio`. */
33
+ export declare function buildPromptAudioObjectKey(canonicalDescriptor: string, extension: string): string;
34
+ /** Durable local adapter. Production callers may implement the same interface over object storage. */
35
+ export declare class FileSystemPromptAudioStore implements PromptAudioObjectStore {
36
+ private readonly rootDirectory;
37
+ constructor(rootDirectory: string);
38
+ private resolveObjectPath;
39
+ get(objectKey: string): Promise<Uint8Array | undefined>;
40
+ putIfAbsent(objectKey: string, audio: Uint8Array): Promise<void>;
41
+ }
42
+ /**
43
+ * Shared durable GCS adapter for public immutable prompts.
44
+ *
45
+ * The default prefix is intentionally product-neutral: Vet, UHC and Connect
46
+ * ICA resolve the same object whenever their complete canonical descriptors
47
+ * match. `ifGenerationMatch: 0` makes concurrent cross-process writes
48
+ * create-only; an existing object wins and is read back by the cache.
49
+ */
50
+ export declare class GcsPromptAudioStore implements PromptAudioObjectStore {
51
+ private readonly bucket;
52
+ private readonly prefix;
53
+ constructor(bucket: PromptAudioGcsBucket, options?: Readonly<{
54
+ prefix?: string;
55
+ cacheControl?: string;
56
+ }>);
57
+ private readonly cacheControl;
58
+ private resolveObjectName;
59
+ get(objectKey: string): Promise<Uint8Array | undefined>;
60
+ putIfAbsent(objectKey: string, audio: Uint8Array): Promise<void>;
61
+ }
62
+ /** Runtime cache with one synthesis operation for every in-process immutable miss. */
63
+ export declare class ImmutablePromptAudioCache {
64
+ private readonly store;
65
+ private readonly inFlight;
66
+ constructor(store: PromptAudioObjectStore);
67
+ getOrCreate(input: Readonly<{
68
+ canonicalDescriptor: string;
69
+ extension: string;
70
+ synthesize: () => Promise<Uint8Array>;
71
+ }>): Promise<CachedPromptAudio>;
72
+ private create;
73
+ }
74
+ /** Prewarm deliberately serializes paid synthesis and stops at the first failure. */
75
+ export declare function prewarmPromptAudioSequentially<Input, Output>(inputs: readonly Input[], prepare: (input: Input) => Promise<Output>): Promise<Output[]>;
76
+ /** Minimal ElevenLabs adapter; credentials stay only in the trusted Node runtime. */
77
+ export declare class ElevenLabsPromptAudioSynthesizer {
78
+ private readonly apiKey;
79
+ private readonly fetchImplementation;
80
+ constructor(options: Readonly<{
81
+ apiKey: string;
82
+ fetch?: typeof fetch;
83
+ }>);
84
+ synthesize(request: ElevenLabsPromptAudioRequest): Promise<Uint8Array>;
85
+ }
@@ -0,0 +1,193 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ const REQUIRED_TOKEN = /^[A-Za-z0-9._-]+$/u;
5
+ function requireText(value, errorCode) {
6
+ const normalized = String(value ?? '').trim();
7
+ if (!normalized)
8
+ throw new Error(errorCode);
9
+ return normalized;
10
+ }
11
+ /** Hash a canonical descriptor produced by `sos-data-utils-ts/prompt-audio`. */
12
+ export function buildPromptAudioObjectKey(canonicalDescriptor, extension) {
13
+ requireText(canonicalDescriptor, 'prompt_audio_canonical_descriptor_required');
14
+ const normalizedExtension = requireText(extension, 'prompt_audio_extension_required').replace(/^\./u, '');
15
+ if (!REQUIRED_TOKEN.test(normalizedExtension))
16
+ throw new Error('prompt_audio_extension_invalid');
17
+ const digest = createHash('sha256').update(canonicalDescriptor, 'utf8').digest('hex');
18
+ return `prompt-audio/sha256/${digest}.${normalizedExtension}`;
19
+ }
20
+ /** Durable local adapter. Production callers may implement the same interface over object storage. */
21
+ export class FileSystemPromptAudioStore {
22
+ rootDirectory;
23
+ constructor(rootDirectory) {
24
+ this.rootDirectory = rootDirectory;
25
+ if (!path.isAbsolute(rootDirectory))
26
+ throw new Error('prompt_audio_store_root_must_be_absolute');
27
+ }
28
+ resolveObjectPath(objectKey) {
29
+ if (!/^prompt-audio\/sha256\/[a-f0-9]{64}\.[A-Za-z0-9._-]+$/u.test(objectKey)) {
30
+ throw new Error('prompt_audio_object_key_invalid');
31
+ }
32
+ const resolved = path.resolve(this.rootDirectory, objectKey);
33
+ const relative = path.relative(this.rootDirectory, resolved);
34
+ if (relative.startsWith('..') || path.isAbsolute(relative))
35
+ throw new Error('prompt_audio_object_key_invalid');
36
+ return resolved;
37
+ }
38
+ async get(objectKey) {
39
+ try {
40
+ return new Uint8Array(await readFile(this.resolveObjectPath(objectKey)));
41
+ }
42
+ catch (error) {
43
+ if (error.code === 'ENOENT')
44
+ return undefined;
45
+ throw error;
46
+ }
47
+ }
48
+ async putIfAbsent(objectKey, audio) {
49
+ if (!(audio instanceof Uint8Array) || audio.byteLength === 0)
50
+ throw new Error('prompt_audio_bytes_required');
51
+ const objectPath = this.resolveObjectPath(objectKey);
52
+ await mkdir(path.dirname(objectPath), { recursive: true });
53
+ try {
54
+ await writeFile(objectPath, audio, { flag: 'wx' });
55
+ }
56
+ catch (error) {
57
+ if (error.code !== 'EEXIST')
58
+ throw error;
59
+ }
60
+ }
61
+ }
62
+ /**
63
+ * Shared durable GCS adapter for public immutable prompts.
64
+ *
65
+ * The default prefix is intentionally product-neutral: Vet, UHC and Connect
66
+ * ICA resolve the same object whenever their complete canonical descriptors
67
+ * match. `ifGenerationMatch: 0` makes concurrent cross-process writes
68
+ * create-only; an existing object wins and is read back by the cache.
69
+ */
70
+ export class GcsPromptAudioStore {
71
+ bucket;
72
+ prefix;
73
+ constructor(bucket, options = {}) {
74
+ this.bucket = bucket;
75
+ this.prefix = String(options.prefix ?? '').trim().replace(/^\/+|\/+$/gu, '');
76
+ this.cacheControl = String(options.cacheControl ?? 'public, max-age=31536000, immutable').trim();
77
+ if (!this.cacheControl)
78
+ throw new Error('prompt_audio_gcs_cache_control_required');
79
+ }
80
+ cacheControl;
81
+ resolveObjectName(objectKey) {
82
+ if (!/^prompt-audio\/sha256\/[a-f0-9]{64}\.[A-Za-z0-9._-]+$/u.test(objectKey)) {
83
+ throw new Error('prompt_audio_object_key_invalid');
84
+ }
85
+ return this.prefix ? `${this.prefix}/${objectKey}` : objectKey;
86
+ }
87
+ async get(objectKey) {
88
+ try {
89
+ const [bytes] = await this.bucket.file(this.resolveObjectName(objectKey)).download();
90
+ return new Uint8Array(bytes);
91
+ }
92
+ catch (error) {
93
+ const status = error.code;
94
+ if (status === 404 || status === '404')
95
+ return undefined;
96
+ throw error;
97
+ }
98
+ }
99
+ async putIfAbsent(objectKey, audio) {
100
+ if (!(audio instanceof Uint8Array) || audio.byteLength === 0)
101
+ throw new Error('prompt_audio_bytes_required');
102
+ try {
103
+ await this.bucket.file(this.resolveObjectName(objectKey)).save(audio, {
104
+ resumable: false,
105
+ contentType: objectKey.endsWith('.mp3') ? 'audio/mpeg' : 'application/octet-stream',
106
+ metadata: { cacheControl: this.cacheControl },
107
+ preconditionOpts: { ifGenerationMatch: 0 },
108
+ });
109
+ }
110
+ catch (error) {
111
+ const status = error.code;
112
+ if (status !== 409 && status !== 412 && status !== '409' && status !== '412')
113
+ throw error;
114
+ }
115
+ }
116
+ }
117
+ /** Runtime cache with one synthesis operation for every in-process immutable miss. */
118
+ export class ImmutablePromptAudioCache {
119
+ store;
120
+ inFlight = new Map();
121
+ constructor(store) {
122
+ this.store = store;
123
+ }
124
+ async getOrCreate(input) {
125
+ const objectKey = buildPromptAudioObjectKey(input.canonicalDescriptor, input.extension);
126
+ const stored = await this.store.get(objectKey);
127
+ if (stored)
128
+ return { objectKey, audio: stored, created: false };
129
+ const current = this.inFlight.get(objectKey);
130
+ if (current)
131
+ return current;
132
+ const creation = this.create(objectKey, input.synthesize);
133
+ this.inFlight.set(objectKey, creation);
134
+ try {
135
+ return await creation;
136
+ }
137
+ finally {
138
+ this.inFlight.delete(objectKey);
139
+ }
140
+ }
141
+ async create(objectKey, synthesize) {
142
+ const audio = await synthesize();
143
+ if (!(audio instanceof Uint8Array) || audio.byteLength === 0)
144
+ throw new Error('prompt_audio_synthesis_empty');
145
+ await this.store.putIfAbsent(objectKey, audio);
146
+ const durableAudio = await this.store.get(objectKey);
147
+ if (!durableAudio)
148
+ throw new Error('prompt_audio_persistence_failed');
149
+ return { objectKey, audio: durableAudio, created: true };
150
+ }
151
+ }
152
+ /** Prewarm deliberately serializes paid synthesis and stops at the first failure. */
153
+ export async function prewarmPromptAudioSequentially(inputs, prepare) {
154
+ const outputs = [];
155
+ for (const input of inputs)
156
+ outputs.push(await prepare(input));
157
+ return outputs;
158
+ }
159
+ /** Minimal ElevenLabs adapter; credentials stay only in the trusted Node runtime. */
160
+ export class ElevenLabsPromptAudioSynthesizer {
161
+ apiKey;
162
+ fetchImplementation;
163
+ constructor(options) {
164
+ this.apiKey = requireText(options.apiKey, 'elevenlabs_api_key_required');
165
+ this.fetchImplementation = options.fetch ?? fetch;
166
+ }
167
+ async synthesize(request) {
168
+ const voiceId = requireText(request.voiceId, 'elevenlabs_voice_id_required');
169
+ const outputFormat = request.outputFormat ?? 'mp3_44100_128';
170
+ if (!REQUIRED_TOKEN.test(voiceId))
171
+ throw new Error('elevenlabs_voice_id_invalid');
172
+ if (!REQUIRED_TOKEN.test(outputFormat))
173
+ throw new Error('elevenlabs_output_format_invalid');
174
+ const response = await this.fetchImplementation(`https://api.elevenlabs.io/v1/text-to-speech/${encodeURIComponent(voiceId)}?output_format=${encodeURIComponent(outputFormat)}`, {
175
+ method: 'POST',
176
+ headers: {
177
+ accept: 'audio/mpeg',
178
+ 'content-type': 'application/json',
179
+ 'xi-api-key': this.apiKey,
180
+ },
181
+ body: JSON.stringify({
182
+ text: requireText(request.text, 'elevenlabs_text_required'),
183
+ model_id: request.modelId ?? 'eleven_multilingual_v2',
184
+ }),
185
+ });
186
+ if (!response.ok)
187
+ throw new Error(`prompt_audio_synthesis_failed:${response.status}`);
188
+ const audio = new Uint8Array(await response.arrayBuffer());
189
+ if (audio.byteLength === 0)
190
+ throw new Error('prompt_audio_synthesis_empty');
191
+ return audio;
192
+ }
193
+ }
@@ -0,0 +1,36 @@
1
+ # Immutable prompt audio in Node runtimes
2
+
3
+ Telephone, portal, chatbot and messaging channels use the same reviewed prompt
4
+ identity from `sos-data-utils-ts/prompt-audio`. The application canonicalizes
5
+ the public-static descriptor, then passes that opaque canonical value to
6
+ `ImmutablePromptAudioCache`.
7
+
8
+ ```text
9
+ reviewed prompt descriptor
10
+ ↓ canonicalize in sos-data-utils-ts
11
+ canonical JSON
12
+ ↓ SHA-256 in sos-sdk-node-ts
13
+ immutable object key
14
+ ↓
15
+ durable store ── miss only ──> configured synthesizer
16
+ ```
17
+
18
+ The durable store, rather than process memory, is authoritative across
19
+ restarts. Concurrent in-process misses are coalesced. Startup prewarming is
20
+ sequential so it cannot create a burst of paid requests or exhaust a shared
21
+ developer machine.
22
+
23
+ `FileSystemPromptAudioStore` supports local and demo execution. Production
24
+ applications use `GcsPromptAudioStore` or implement `PromptAudioObjectStore`
25
+ over another approved object store without changing the cache lifecycle.
26
+ One GCS bucket may be shared by Vet, UHC and Connect ICA: the default object
27
+ name has no product prefix and reuse occurs only when the complete canonical
28
+ descriptor matches. Deployments may configure an isolation prefix when policy
29
+ requires separate storage. `ElevenLabsPromptAudioSynthesizer`
30
+ defaults to `eleven_multilingual_v2`; API keys stay in the trusted runtime and
31
+ are neither logged nor included in an error.
32
+
33
+ Activation and verification services must never cache a complete live code.
34
+ They retain its value, expiry and attempt counter, and compose the call from a
35
+ static prefix plus localized digit fragments. Personalized names and clinical
36
+ data are not public-static prompts.
@@ -1,17 +1,25 @@
1
- # SOSChain Node SDK boundary
1
+ # Shared Node SDK boundary
2
2
 
3
3
  ```mermaid
4
4
  flowchart TB
5
5
  GDC["gdc-* frozen"] --> DATA["sos-data-utils-ts<br/>deterministic contracts"]
6
6
  DATA --> CORE["sos-sdk-core-ts<br/>runtime-neutral authorization"]
7
- CORE --> NODE["sos-sdk-node-ts<br/>SOSChain Node adapters"]
8
- NODE --> RUNTIME["SOSChain BFF · jobs · portal server"]
7
+ DATA --> NODE["sos-sdk-node-ts<br/>product-neutral Node adapters"]
8
+ CORE --> NODE
9
+ NODE --> RUNTIME["SOSChain · Vet · UHC trusted Node runtimes"]
9
10
  CORE --> VET["vet-sdk-core-ts"]
10
11
  CORE --> UHC["uhc-sdk-core-ts"]
11
12
  DATA --> VET
12
13
  DATA --> UHC
13
14
  ```
14
15
 
15
- `sos-sdk-node-ts` is a product runtime consumer, not another common layer. It
16
- may integrate public Vet or UHC contracts for an SOSChain operation, but that
17
- does not permit a dependency from either shared package back to a product SDK.
16
+ `sos-sdk-node-ts` is the common Node-runtime layer; it is not the SOSChain
17
+ product SDK. Product-neutral storage, transport, credential, job and audio
18
+ adapters belong here. Sector policy remains in the corresponding Vet, UHC or
19
+ SOSChain package, and no shared package may depend back on a product SDK.
20
+
21
+ For immutable prompt audio, `sos-data-utils-ts/prompt-audio` produces the
22
+ canonical descriptor and localized numeric-code fragments. This package
23
+ hashes that exact descriptor, stores the resulting bytes durably and invokes
24
+ the configured provider only on a miss. Complete activation or verification
25
+ codes remain ICA state and are played by composing reusable digit assets.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sos-sdk-node-ts",
3
- "version": "0.1.2",
4
- "description": "SOSChain-specific Node adapters over the shared runtime-neutral SOS SDK",
3
+ "version": "0.1.3",
4
+ "description": "Product-neutral Node adapters shared by SOSChain, Vet and UHC runtimes",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Connecting Solution & Applications Ltd",
7
7
  "repository": {
@@ -20,6 +20,10 @@
20
20
  "types": "./dist/emergency-dispatch.d.ts",
21
21
  "default": "./dist/emergency-dispatch.js"
22
22
  },
23
+ "./prompt-audio-runtime": {
24
+ "types": "./dist/prompt-audio-runtime.d.ts",
25
+ "default": "./dist/prompt-audio-runtime.js"
26
+ },
23
27
  "./secure-message-crypto": {
24
28
  "types": "./dist/secure-message-crypto.d.ts",
25
29
  "default": "./dist/secure-message-crypto.js"
@@ -34,7 +38,7 @@
34
38
  "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
35
39
  "build": "npm run clean && tsc -p tsconfig.json",
36
40
  "typecheck": "tsc -p tsconfig.json --noEmit",
37
- "test": "npm run build && node --test tests/*.test.mjs",
41
+ "test": "npm run build && node --test --test-concurrency=1 tests/*.test.mjs",
38
42
  "check": "npm run typecheck && npm test",
39
43
  "prepack": "npm run check"
40
44
  },