@qnsp/storage-sdk 0.2.1 → 0.3.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/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +49 -0
- package/LICENSE +21 -7
- package/README.md +22 -72
- package/dist/event-envelope.d.ts.map +1 -1
- package/dist/event-envelope.js +1 -2
- package/dist/event-envelope.js.map +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +0 -1
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +204 -22
- package/dist/index.js.map +1 -1
- package/dist/validation.d.ts +10 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +20 -0
- package/dist/validation.js.map +1 -0
- package/package.json +59 -39
- package/src/event-envelope.ts +1 -2
- package/src/events.ts +0 -2
- package/src/index.test.ts +92 -49
- package/src/index.ts +229 -26
- package/src/validation.ts +21 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { activateSdk, type SdkActivationConfig } from "@qnsp/sdk-activation";
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
4
|
StorageClientTelemetry,
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
StorageClientTelemetryEvent,
|
|
7
7
|
} from "./observability.js";
|
|
8
8
|
import { createStorageClientTelemetry, isStorageClientTelemetry } from "./observability.js";
|
|
9
|
+
import { validateUUID } from "./validation.js";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* @qnsp/storage-sdk
|
|
@@ -26,15 +27,116 @@ export interface PqcMetadata {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
|
-
* Mapping from internal algorithm names to NIST
|
|
30
|
+
* Mapping from internal algorithm names to NIST/standards display names.
|
|
31
|
+
* Covers all 90 PQC algorithms supported by QNSP.
|
|
32
|
+
* Canonical source: @qnsp/cryptography pqc-standards.ts ALGORITHM_NIST_NAMES
|
|
30
33
|
*/
|
|
31
34
|
export const ALGORITHM_TO_NIST: Record<string, string> = {
|
|
35
|
+
// FIPS 203 — ML-KEM
|
|
32
36
|
"kyber-512": "ML-KEM-512",
|
|
33
37
|
"kyber-768": "ML-KEM-768",
|
|
34
38
|
"kyber-1024": "ML-KEM-1024",
|
|
39
|
+
// FIPS 204 — ML-DSA
|
|
35
40
|
"dilithium-2": "ML-DSA-44",
|
|
36
41
|
"dilithium-3": "ML-DSA-65",
|
|
37
42
|
"dilithium-5": "ML-DSA-87",
|
|
43
|
+
// FIPS 205 — SLH-DSA (SHA-2 variants)
|
|
44
|
+
"sphincs-sha2-128f-simple": "SLH-DSA-SHA2-128f",
|
|
45
|
+
"sphincs-sha2-128s-simple": "SLH-DSA-SHA2-128s",
|
|
46
|
+
"sphincs-sha2-192f-simple": "SLH-DSA-SHA2-192f",
|
|
47
|
+
"sphincs-sha2-192s-simple": "SLH-DSA-SHA2-192s",
|
|
48
|
+
"sphincs-sha2-256f-simple": "SLH-DSA-SHA2-256f",
|
|
49
|
+
"sphincs-sha2-256s-simple": "SLH-DSA-SHA2-256s",
|
|
50
|
+
// FIPS 205 — SLH-DSA (SHAKE variants)
|
|
51
|
+
"sphincs-shake-128f-simple": "SLH-DSA-SHAKE-128f",
|
|
52
|
+
"sphincs-shake-128s-simple": "SLH-DSA-SHAKE-128s",
|
|
53
|
+
"sphincs-shake-192f-simple": "SLH-DSA-SHAKE-192f",
|
|
54
|
+
"sphincs-shake-192s-simple": "SLH-DSA-SHAKE-192s",
|
|
55
|
+
"sphincs-shake-256f-simple": "SLH-DSA-SHAKE-256f",
|
|
56
|
+
"sphincs-shake-256s-simple": "SLH-DSA-SHAKE-256s",
|
|
57
|
+
// FN-DSA (FIPS 206 draft)
|
|
58
|
+
"falcon-512": "FN-DSA-512",
|
|
59
|
+
"falcon-1024": "FN-DSA-1024",
|
|
60
|
+
// HQC (NIST selected March 2025)
|
|
61
|
+
"hqc-128": "HQC-128",
|
|
62
|
+
"hqc-192": "HQC-192",
|
|
63
|
+
"hqc-256": "HQC-256",
|
|
64
|
+
// BIKE (NIST Round 4)
|
|
65
|
+
"bike-l1": "BIKE-L1",
|
|
66
|
+
"bike-l3": "BIKE-L3",
|
|
67
|
+
"bike-l5": "BIKE-L5",
|
|
68
|
+
// Classic McEliece (ISO standard)
|
|
69
|
+
"mceliece-348864": "Classic-McEliece-348864",
|
|
70
|
+
"mceliece-460896": "Classic-McEliece-460896",
|
|
71
|
+
"mceliece-6688128": "Classic-McEliece-6688128",
|
|
72
|
+
"mceliece-6960119": "Classic-McEliece-6960119",
|
|
73
|
+
"mceliece-8192128": "Classic-McEliece-8192128",
|
|
74
|
+
// FrodoKEM (ISO standard)
|
|
75
|
+
"frodokem-640-aes": "FrodoKEM-640-AES",
|
|
76
|
+
"frodokem-640-shake": "FrodoKEM-640-SHAKE",
|
|
77
|
+
"frodokem-976-aes": "FrodoKEM-976-AES",
|
|
78
|
+
"frodokem-976-shake": "FrodoKEM-976-SHAKE",
|
|
79
|
+
"frodokem-1344-aes": "FrodoKEM-1344-AES",
|
|
80
|
+
"frodokem-1344-shake": "FrodoKEM-1344-SHAKE",
|
|
81
|
+
// NTRU (lattice-based, re-added in liboqs 0.15)
|
|
82
|
+
"ntru-hps-2048-509": "NTRU-HPS-2048-509",
|
|
83
|
+
"ntru-hps-2048-677": "NTRU-HPS-2048-677",
|
|
84
|
+
"ntru-hps-4096-821": "NTRU-HPS-4096-821",
|
|
85
|
+
"ntru-hps-4096-1229": "NTRU-HPS-4096-1229",
|
|
86
|
+
"ntru-hrss-701": "NTRU-HRSS-701",
|
|
87
|
+
"ntru-hrss-1373": "NTRU-HRSS-1373",
|
|
88
|
+
// NTRU-Prime
|
|
89
|
+
sntrup761: "sntrup761",
|
|
90
|
+
// MAYO (NIST Additional Signatures Round 2)
|
|
91
|
+
"mayo-1": "MAYO-1",
|
|
92
|
+
"mayo-2": "MAYO-2",
|
|
93
|
+
"mayo-3": "MAYO-3",
|
|
94
|
+
"mayo-5": "MAYO-5",
|
|
95
|
+
// CROSS (NIST Additional Signatures Round 2)
|
|
96
|
+
"cross-rsdp-128-balanced": "CROSS-RSDP-128-balanced",
|
|
97
|
+
"cross-rsdp-128-fast": "CROSS-RSDP-128-fast",
|
|
98
|
+
"cross-rsdp-128-small": "CROSS-RSDP-128-small",
|
|
99
|
+
"cross-rsdp-192-balanced": "CROSS-RSDP-192-balanced",
|
|
100
|
+
"cross-rsdp-192-fast": "CROSS-RSDP-192-fast",
|
|
101
|
+
"cross-rsdp-192-small": "CROSS-RSDP-192-small",
|
|
102
|
+
"cross-rsdp-256-balanced": "CROSS-RSDP-256-balanced",
|
|
103
|
+
"cross-rsdp-256-fast": "CROSS-RSDP-256-fast",
|
|
104
|
+
"cross-rsdp-256-small": "CROSS-RSDP-256-small",
|
|
105
|
+
"cross-rsdpg-128-balanced": "CROSS-RSDPG-128-balanced",
|
|
106
|
+
"cross-rsdpg-128-fast": "CROSS-RSDPG-128-fast",
|
|
107
|
+
"cross-rsdpg-128-small": "CROSS-RSDPG-128-small",
|
|
108
|
+
"cross-rsdpg-192-balanced": "CROSS-RSDPG-192-balanced",
|
|
109
|
+
"cross-rsdpg-192-fast": "CROSS-RSDPG-192-fast",
|
|
110
|
+
"cross-rsdpg-192-small": "CROSS-RSDPG-192-small",
|
|
111
|
+
"cross-rsdpg-256-balanced": "CROSS-RSDPG-256-balanced",
|
|
112
|
+
"cross-rsdpg-256-fast": "CROSS-RSDPG-256-fast",
|
|
113
|
+
"cross-rsdpg-256-small": "CROSS-RSDPG-256-small",
|
|
114
|
+
// UOV (NIST Additional Signatures Round 2)
|
|
115
|
+
"ov-Is": "UOV-Is",
|
|
116
|
+
"ov-Ip": "UOV-Ip",
|
|
117
|
+
"ov-III": "UOV-III",
|
|
118
|
+
"ov-V": "UOV-V",
|
|
119
|
+
"ov-Is-pkc": "UOV-Is-pkc",
|
|
120
|
+
"ov-Ip-pkc": "UOV-Ip-pkc",
|
|
121
|
+
"ov-III-pkc": "UOV-III-pkc",
|
|
122
|
+
"ov-V-pkc": "UOV-V-pkc",
|
|
123
|
+
"ov-Is-pkc-skc": "UOV-Is-pkc-skc",
|
|
124
|
+
"ov-Ip-pkc-skc": "UOV-Ip-pkc-skc",
|
|
125
|
+
"ov-III-pkc-skc": "UOV-III-pkc-skc",
|
|
126
|
+
"ov-V-pkc-skc": "UOV-V-pkc-skc",
|
|
127
|
+
// SNOVA (NIST Additional Signatures Round 2, liboqs 0.14+)
|
|
128
|
+
"snova-24-5-4": "SNOVA-24-5-4",
|
|
129
|
+
"snova-24-5-4-shake": "SNOVA-24-5-4-SHAKE",
|
|
130
|
+
"snova-24-5-4-esk": "SNOVA-24-5-4-ESK",
|
|
131
|
+
"snova-24-5-4-shake-esk": "SNOVA-24-5-4-SHAKE-ESK",
|
|
132
|
+
"snova-25-8-3": "SNOVA-25-8-3",
|
|
133
|
+
"snova-37-17-2": "SNOVA-37-17-2",
|
|
134
|
+
"snova-37-8-4": "SNOVA-37-8-4",
|
|
135
|
+
"snova-24-5-5": "SNOVA-24-5-5",
|
|
136
|
+
"snova-56-25-2": "SNOVA-56-25-2",
|
|
137
|
+
"snova-49-11-3": "SNOVA-49-11-3",
|
|
138
|
+
"snova-60-10-4": "SNOVA-60-10-4",
|
|
139
|
+
"snova-29-6-5": "SNOVA-29-6-5",
|
|
38
140
|
};
|
|
39
141
|
|
|
40
142
|
/**
|
|
@@ -46,9 +148,11 @@ export function toNistAlgorithmName(algorithm: string): string {
|
|
|
46
148
|
|
|
47
149
|
export interface StorageClientConfig {
|
|
48
150
|
readonly baseUrl: string;
|
|
49
|
-
readonly apiKey
|
|
151
|
+
readonly apiKey: string;
|
|
50
152
|
readonly tenantId: string;
|
|
51
153
|
readonly timeoutMs?: number;
|
|
154
|
+
readonly maxRetries?: number;
|
|
155
|
+
readonly retryDelayMs?: number;
|
|
52
156
|
readonly telemetry?: StorageClientTelemetry | StorageClientTelemetryConfig;
|
|
53
157
|
}
|
|
54
158
|
|
|
@@ -57,6 +161,8 @@ type InternalStorageClientConfig = {
|
|
|
57
161
|
readonly apiKey: string;
|
|
58
162
|
readonly tenantId: string;
|
|
59
163
|
readonly timeoutMs: number;
|
|
164
|
+
readonly maxRetries: number;
|
|
165
|
+
readonly retryDelayMs: number;
|
|
60
166
|
};
|
|
61
167
|
|
|
62
168
|
export interface InitiateUploadOptions {
|
|
@@ -182,13 +288,61 @@ export class StorageClient {
|
|
|
182
288
|
private readonly config: InternalStorageClientConfig;
|
|
183
289
|
private readonly telemetry: StorageClientTelemetry | null;
|
|
184
290
|
private readonly targetService: string;
|
|
291
|
+
private activationPromise: Promise<void> | null = null;
|
|
292
|
+
private readonly activationConfig: SdkActivationConfig;
|
|
293
|
+
|
|
294
|
+
private async ensureActivated(): Promise<void> {
|
|
295
|
+
if (!this.activationPromise) {
|
|
296
|
+
this.activationPromise = activateSdk(this.activationConfig).then(() => undefined);
|
|
297
|
+
}
|
|
298
|
+
return this.activationPromise;
|
|
299
|
+
}
|
|
185
300
|
|
|
186
301
|
constructor(config: StorageClientConfig) {
|
|
302
|
+
if (!config.apiKey || config.apiKey.trim().length === 0) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
"QNSP Storage SDK: apiKey is required. " +
|
|
305
|
+
"Get your free API key at https://cloud.qnsp.cuilabs.io/signup — " +
|
|
306
|
+
"no credit card required (FREE tier: 10 GB storage, 50,000 API calls/month). " +
|
|
307
|
+
"Docs: https://docs.qnsp.cuilabs.io/sdk/storage-sdk",
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
312
|
+
|
|
313
|
+
// Enforce HTTPS in production (allow HTTP for localhost in development and
|
|
314
|
+
// for internal service-mesh hostnames — e.g. *.internal — which are on a
|
|
315
|
+
// private VPC network and do not require TLS termination at the transport layer).
|
|
316
|
+
if (!baseUrl.startsWith("https://")) {
|
|
317
|
+
const isLocalhost =
|
|
318
|
+
baseUrl.startsWith("http://localhost") || baseUrl.startsWith("http://127.0.0.1");
|
|
319
|
+
let isInternalService = false;
|
|
320
|
+
try {
|
|
321
|
+
const parsed = new URL(baseUrl);
|
|
322
|
+
isInternalService =
|
|
323
|
+
parsed.protocol === "http:" &&
|
|
324
|
+
(parsed.hostname.endsWith(".internal") ||
|
|
325
|
+
parsed.hostname === "localhost" ||
|
|
326
|
+
parsed.hostname === "127.0.0.1");
|
|
327
|
+
} catch {
|
|
328
|
+
// ignore; invalid URL will be caught later by fetch
|
|
329
|
+
}
|
|
330
|
+
const isDevelopment =
|
|
331
|
+
process.env["NODE_ENV"] === "development" || process.env["NODE_ENV"] === "test";
|
|
332
|
+
if ((!isLocalhost || !isDevelopment) && !isInternalService) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
"baseUrl must use HTTPS in production. HTTP is only allowed for localhost in development.",
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
187
339
|
this.config = {
|
|
188
|
-
baseUrl
|
|
189
|
-
apiKey: config.apiKey
|
|
340
|
+
baseUrl,
|
|
341
|
+
apiKey: config.apiKey,
|
|
190
342
|
tenantId: config.tenantId,
|
|
191
343
|
timeoutMs: config.timeoutMs ?? 30_000,
|
|
344
|
+
maxRetries: config.maxRetries ?? 3,
|
|
345
|
+
retryDelayMs: config.retryDelayMs ?? 1_000,
|
|
192
346
|
};
|
|
193
347
|
|
|
194
348
|
this.telemetry = config.telemetry
|
|
@@ -202,18 +356,32 @@ export class StorageClient {
|
|
|
202
356
|
} catch {
|
|
203
357
|
this.targetService = "storage-service";
|
|
204
358
|
}
|
|
359
|
+
|
|
360
|
+
this.activationConfig = {
|
|
361
|
+
apiKey: config.apiKey,
|
|
362
|
+
sdkId: "storage-sdk",
|
|
363
|
+
sdkVersion: "0.3.0",
|
|
364
|
+
platformUrl: config.baseUrl,
|
|
365
|
+
};
|
|
205
366
|
}
|
|
206
367
|
|
|
207
368
|
private async request<T>(method: string, path: string, options?: RequestOptions): Promise<T> {
|
|
369
|
+
return this.requestWithRetry<T>(method, path, options, 0);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private async requestWithRetry<T>(
|
|
373
|
+
method: string,
|
|
374
|
+
path: string,
|
|
375
|
+
options: RequestOptions | undefined,
|
|
376
|
+
attempt: number,
|
|
377
|
+
): Promise<T> {
|
|
208
378
|
const url = `${this.config.baseUrl}${path}`;
|
|
209
379
|
const headers: Record<string, string> = {
|
|
210
380
|
"Content-Type": "application/json",
|
|
211
381
|
...options?.headers,
|
|
212
382
|
};
|
|
213
383
|
|
|
214
|
-
|
|
215
|
-
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
216
|
-
}
|
|
384
|
+
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
217
385
|
|
|
218
386
|
const controller = new AbortController();
|
|
219
387
|
const timeoutId = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
@@ -241,15 +409,39 @@ export class StorageClient {
|
|
|
241
409
|
clearTimeout(timeoutId);
|
|
242
410
|
httpStatus = response.status;
|
|
243
411
|
|
|
244
|
-
|
|
412
|
+
// Handle rate limiting (429) with retry logic
|
|
413
|
+
if (response.status === 429) {
|
|
414
|
+
if (attempt < this.config.maxRetries) {
|
|
415
|
+
const retryAfterHeader = response.headers.get("Retry-After");
|
|
416
|
+
let delayMs = this.config.retryDelayMs;
|
|
417
|
+
|
|
418
|
+
if (retryAfterHeader) {
|
|
419
|
+
const retryAfterSeconds = Number.parseInt(retryAfterHeader, 10);
|
|
420
|
+
if (!Number.isNaN(retryAfterSeconds) && retryAfterSeconds > 0) {
|
|
421
|
+
delayMs = retryAfterSeconds * 1_000;
|
|
422
|
+
}
|
|
423
|
+
} else {
|
|
424
|
+
// Exponential backoff: 2^attempt * baseDelay, capped at 30 seconds
|
|
425
|
+
delayMs = Math.min(2 ** attempt * this.config.retryDelayMs, 30_000);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
429
|
+
return this.requestWithRetry<T>(method, path, options, attempt + 1);
|
|
430
|
+
}
|
|
431
|
+
|
|
245
432
|
status = "error";
|
|
246
|
-
|
|
247
|
-
errorMessage = errorText;
|
|
433
|
+
errorMessage = `HTTP ${response.status}`;
|
|
248
434
|
throw new Error(
|
|
249
|
-
`Storage API error:
|
|
435
|
+
`Storage API error: Rate limit exceeded after ${this.config.maxRetries} retries`,
|
|
250
436
|
);
|
|
251
437
|
}
|
|
252
438
|
|
|
439
|
+
if (!response.ok) {
|
|
440
|
+
status = "error";
|
|
441
|
+
errorMessage = `HTTP ${response.status}`;
|
|
442
|
+
throw new Error(`Storage API error: ${response.status} ${response.statusText}`);
|
|
443
|
+
}
|
|
444
|
+
|
|
253
445
|
if (response.status === 204) {
|
|
254
446
|
return undefined as T;
|
|
255
447
|
}
|
|
@@ -293,6 +485,7 @@ export class StorageClient {
|
|
|
293
485
|
readonly resumeToken: string | null;
|
|
294
486
|
readonly pqc: PqcMetadata;
|
|
295
487
|
}> {
|
|
488
|
+
await this.ensureActivated();
|
|
296
489
|
const result = await this.request<{
|
|
297
490
|
uploadId: string;
|
|
298
491
|
documentId: string;
|
|
@@ -335,14 +528,13 @@ export class StorageClient {
|
|
|
335
528
|
partId: number,
|
|
336
529
|
data: ReadableStream<Uint8Array> | Buffer | Uint8Array,
|
|
337
530
|
): Promise<UploadPartResult> {
|
|
531
|
+
await this.ensureActivated();
|
|
338
532
|
const url = `${this.config.baseUrl}/storage/v1/uploads/${uploadId}/parts/${partId}`;
|
|
339
533
|
const headers: Record<string, string> = {
|
|
340
534
|
"Content-Type": "application/octet-stream",
|
|
341
535
|
};
|
|
342
536
|
|
|
343
|
-
|
|
344
|
-
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
345
|
-
}
|
|
537
|
+
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
346
538
|
|
|
347
539
|
const bytesSent =
|
|
348
540
|
data instanceof Buffer || data instanceof Uint8Array ? data.byteLength : undefined;
|
|
@@ -385,11 +577,8 @@ export class StorageClient {
|
|
|
385
577
|
|
|
386
578
|
if (!response.ok) {
|
|
387
579
|
status = "error";
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
throw new Error(
|
|
391
|
-
`Upload part error: ${response.status} ${response.statusText} - ${errorText}`,
|
|
392
|
-
);
|
|
580
|
+
errorMessage = `HTTP ${response.status}`;
|
|
581
|
+
throw new Error(`Upload part error: ${response.status} ${response.statusText}`);
|
|
393
582
|
}
|
|
394
583
|
|
|
395
584
|
return (await response.json()) as UploadPartResult;
|
|
@@ -422,6 +611,8 @@ export class StorageClient {
|
|
|
422
611
|
}
|
|
423
612
|
|
|
424
613
|
async getUploadStatus(uploadId: string): Promise<UploadStatus> {
|
|
614
|
+
validateUUID(uploadId, "uploadId");
|
|
615
|
+
await this.ensureActivated();
|
|
425
616
|
// Use GET since we need the full status object
|
|
426
617
|
return this.request<UploadStatus>("GET", `/storage/v1/uploads/${uploadId}`, {
|
|
427
618
|
operation: "getUploadStatus",
|
|
@@ -430,6 +621,8 @@ export class StorageClient {
|
|
|
430
621
|
}
|
|
431
622
|
|
|
432
623
|
async completeUpload(uploadId: string): Promise<CompleteUploadResult> {
|
|
624
|
+
validateUUID(uploadId, "uploadId");
|
|
625
|
+
await this.ensureActivated();
|
|
433
626
|
return this.request("POST", `/storage/v1/uploads/${uploadId}/complete`, {
|
|
434
627
|
operation: "completeUpload",
|
|
435
628
|
telemetryRoute: "/storage/v1/uploads/:uploadId/complete",
|
|
@@ -445,6 +638,7 @@ export class StorageClient {
|
|
|
445
638
|
readonly signature?: string | null;
|
|
446
639
|
},
|
|
447
640
|
): Promise<DownloadDescriptor> {
|
|
641
|
+
await this.ensureActivated();
|
|
448
642
|
const params = new URLSearchParams({
|
|
449
643
|
tenantId: this.config.tenantId,
|
|
450
644
|
});
|
|
@@ -486,6 +680,7 @@ export class StorageClient {
|
|
|
486
680
|
readonly range?: { readonly start: number; readonly end: number };
|
|
487
681
|
readonly checksumSha3: string;
|
|
488
682
|
}> {
|
|
683
|
+
await this.ensureActivated();
|
|
489
684
|
const params = new URLSearchParams({
|
|
490
685
|
tenantId: this.config.tenantId,
|
|
491
686
|
});
|
|
@@ -510,9 +705,7 @@ export class StorageClient {
|
|
|
510
705
|
headers["Range"] = options.range;
|
|
511
706
|
}
|
|
512
707
|
|
|
513
|
-
|
|
514
|
-
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
515
|
-
}
|
|
708
|
+
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
516
709
|
|
|
517
710
|
const controller = new AbortController();
|
|
518
711
|
const timeoutId = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
@@ -535,9 +728,8 @@ export class StorageClient {
|
|
|
535
728
|
|
|
536
729
|
if (!response.ok) {
|
|
537
730
|
status = "error";
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
throw new Error(`Download error: ${response.status} ${response.statusText} - ${errorText}`);
|
|
731
|
+
errorMessage = `HTTP ${response.status}`;
|
|
732
|
+
throw new Error(`Download error: ${response.status} ${response.statusText}`);
|
|
541
733
|
}
|
|
542
734
|
|
|
543
735
|
const contentRange = response.headers.get("Content-Range");
|
|
@@ -612,6 +804,8 @@ export class StorageClient {
|
|
|
612
804
|
* Requires x-tenant-id header.
|
|
613
805
|
*/
|
|
614
806
|
async getDocumentPolicies(documentId: string): Promise<DocumentPolicies> {
|
|
807
|
+
validateUUID(documentId, "documentId");
|
|
808
|
+
await this.ensureActivated();
|
|
615
809
|
return this.request("GET", `/storage/v1/documents/${documentId}/policies`, {
|
|
616
810
|
operation: "getDocumentPolicies",
|
|
617
811
|
telemetryRoute: "/storage/v1/documents/:documentId/policies",
|
|
@@ -629,6 +823,8 @@ export class StorageClient {
|
|
|
629
823
|
documentId: string,
|
|
630
824
|
input: UpdatePoliciesRequest,
|
|
631
825
|
): Promise<DocumentPolicies> {
|
|
826
|
+
validateUUID(documentId, "documentId");
|
|
827
|
+
await this.ensureActivated();
|
|
632
828
|
return this.request("PATCH", `/storage/v1/documents/${documentId}/policies`, {
|
|
633
829
|
body: input,
|
|
634
830
|
operation: "updateDocumentPolicies",
|
|
@@ -651,6 +847,8 @@ export class StorageClient {
|
|
|
651
847
|
readonly tenantId: string;
|
|
652
848
|
readonly legalHolds: readonly string[];
|
|
653
849
|
}> {
|
|
850
|
+
validateUUID(documentId, "documentId");
|
|
851
|
+
await this.ensureActivated();
|
|
654
852
|
return this.request("POST", `/storage/v1/documents/${documentId}/legal-holds`, {
|
|
655
853
|
body: request,
|
|
656
854
|
operation: "applyLegalHold",
|
|
@@ -666,6 +864,8 @@ export class StorageClient {
|
|
|
666
864
|
* Requires x-tenant-id header.
|
|
667
865
|
*/
|
|
668
866
|
async releaseLegalHold(documentId: string, holdId: string): Promise<void> {
|
|
867
|
+
validateUUID(documentId, "documentId");
|
|
868
|
+
await this.ensureActivated();
|
|
669
869
|
return this.request("DELETE", `/storage/v1/documents/${documentId}/legal-holds/${holdId}`, {
|
|
670
870
|
operation: "releaseLegalHold",
|
|
671
871
|
telemetryRoute: "/storage/v1/documents/:documentId/legal-holds/:holdId",
|
|
@@ -691,6 +891,8 @@ export class StorageClient {
|
|
|
691
891
|
readonly transitionAfter?: string | null;
|
|
692
892
|
};
|
|
693
893
|
}> {
|
|
894
|
+
validateUUID(documentId, "documentId");
|
|
895
|
+
await this.ensureActivated();
|
|
694
896
|
return this.request("POST", `/storage/v1/documents/${documentId}/lifecycle/transitions`, {
|
|
695
897
|
body: request,
|
|
696
898
|
operation: "scheduleLifecycleTransition",
|
|
@@ -711,3 +913,4 @@ export class StorageClient {
|
|
|
711
913
|
|
|
712
914
|
export * from "./events.js";
|
|
713
915
|
export * from "./observability.js";
|
|
916
|
+
export * from "./validation.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validation schemas for storage-sdk inputs
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const uuidSchema = z.string().uuid("Invalid UUID format");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validates a UUID string
|
|
11
|
+
*/
|
|
12
|
+
export function validateUUID(value: string, fieldName: string): void {
|
|
13
|
+
try {
|
|
14
|
+
uuidSchema.parse(value);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (error instanceof z.ZodError) {
|
|
17
|
+
throw new Error(`Invalid ${fieldName}: ${error.issues[0]?.message ?? "Invalid format"}`);
|
|
18
|
+
}
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
}
|