@qnsp/storage-sdk 0.1.0 → 0.3.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/CHANGELOG.md +103 -0
- package/README.md +2 -2
- 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 +26 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +189 -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 +9 -9
- package/src/event-envelope.ts +1 -2
- package/src/events.ts +0 -2
- package/src/index.test.ts +43 -25
- package/src/index.ts +242 -32
- package/src/validation.ts +21 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/.turbo/turbo-build.log +0 -5
- package/.turbo/turbo-lint.log +0 -6
- package/.turbo/turbo-test.log +0 -41
- package/.turbo/turbo-typecheck.log +0 -5
package/src/index.ts
CHANGED
|
@@ -1,24 +1,156 @@
|
|
|
1
|
-
import { performance } from "node:perf_hooks";
|
|
2
|
-
|
|
3
1
|
import type {
|
|
4
2
|
StorageClientTelemetry,
|
|
5
3
|
StorageClientTelemetryConfig,
|
|
6
4
|
StorageClientTelemetryEvent,
|
|
7
5
|
} from "./observability.js";
|
|
8
6
|
import { createStorageClientTelemetry, isStorageClientTelemetry } from "./observability.js";
|
|
7
|
+
import { validateUUID } from "./validation.js";
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
10
|
* @qnsp/storage-sdk
|
|
12
11
|
*
|
|
13
12
|
* TypeScript SDK client for the QNSP storage-service API.
|
|
14
13
|
* Provides a high-level interface for document upload, download, and management operations.
|
|
14
|
+
* All cryptographic operations use tenant-specific PQC algorithms based on crypto policy.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* PQC metadata for cryptographic operations.
|
|
19
|
+
*/
|
|
20
|
+
export interface PqcMetadata {
|
|
21
|
+
readonly provider: string;
|
|
22
|
+
readonly algorithm: string;
|
|
23
|
+
readonly algorithmNist?: string;
|
|
24
|
+
readonly keyId: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Mapping from internal algorithm names to NIST/standards display names.
|
|
29
|
+
* Covers all 90 PQC algorithms supported by QNSP.
|
|
30
|
+
* Canonical source: @qnsp/cryptography pqc-standards.ts ALGORITHM_NIST_NAMES
|
|
31
|
+
*/
|
|
32
|
+
export const ALGORITHM_TO_NIST: Record<string, string> = {
|
|
33
|
+
// FIPS 203 — ML-KEM
|
|
34
|
+
"kyber-512": "ML-KEM-512",
|
|
35
|
+
"kyber-768": "ML-KEM-768",
|
|
36
|
+
"kyber-1024": "ML-KEM-1024",
|
|
37
|
+
// FIPS 204 — ML-DSA
|
|
38
|
+
"dilithium-2": "ML-DSA-44",
|
|
39
|
+
"dilithium-3": "ML-DSA-65",
|
|
40
|
+
"dilithium-5": "ML-DSA-87",
|
|
41
|
+
// FIPS 205 — SLH-DSA (SHA-2 variants)
|
|
42
|
+
"sphincs-sha2-128f-simple": "SLH-DSA-SHA2-128f",
|
|
43
|
+
"sphincs-sha2-128s-simple": "SLH-DSA-SHA2-128s",
|
|
44
|
+
"sphincs-sha2-192f-simple": "SLH-DSA-SHA2-192f",
|
|
45
|
+
"sphincs-sha2-192s-simple": "SLH-DSA-SHA2-192s",
|
|
46
|
+
"sphincs-sha2-256f-simple": "SLH-DSA-SHA2-256f",
|
|
47
|
+
"sphincs-sha2-256s-simple": "SLH-DSA-SHA2-256s",
|
|
48
|
+
// FIPS 205 — SLH-DSA (SHAKE variants)
|
|
49
|
+
"sphincs-shake-128f-simple": "SLH-DSA-SHAKE-128f",
|
|
50
|
+
"sphincs-shake-128s-simple": "SLH-DSA-SHAKE-128s",
|
|
51
|
+
"sphincs-shake-192f-simple": "SLH-DSA-SHAKE-192f",
|
|
52
|
+
"sphincs-shake-192s-simple": "SLH-DSA-SHAKE-192s",
|
|
53
|
+
"sphincs-shake-256f-simple": "SLH-DSA-SHAKE-256f",
|
|
54
|
+
"sphincs-shake-256s-simple": "SLH-DSA-SHAKE-256s",
|
|
55
|
+
// FN-DSA (FIPS 206 draft)
|
|
56
|
+
"falcon-512": "FN-DSA-512",
|
|
57
|
+
"falcon-1024": "FN-DSA-1024",
|
|
58
|
+
// HQC (NIST selected March 2025)
|
|
59
|
+
"hqc-128": "HQC-128",
|
|
60
|
+
"hqc-192": "HQC-192",
|
|
61
|
+
"hqc-256": "HQC-256",
|
|
62
|
+
// BIKE (NIST Round 4)
|
|
63
|
+
"bike-l1": "BIKE-L1",
|
|
64
|
+
"bike-l3": "BIKE-L3",
|
|
65
|
+
"bike-l5": "BIKE-L5",
|
|
66
|
+
// Classic McEliece (ISO standard)
|
|
67
|
+
"mceliece-348864": "Classic-McEliece-348864",
|
|
68
|
+
"mceliece-460896": "Classic-McEliece-460896",
|
|
69
|
+
"mceliece-6688128": "Classic-McEliece-6688128",
|
|
70
|
+
"mceliece-6960119": "Classic-McEliece-6960119",
|
|
71
|
+
"mceliece-8192128": "Classic-McEliece-8192128",
|
|
72
|
+
// FrodoKEM (ISO standard)
|
|
73
|
+
"frodokem-640-aes": "FrodoKEM-640-AES",
|
|
74
|
+
"frodokem-640-shake": "FrodoKEM-640-SHAKE",
|
|
75
|
+
"frodokem-976-aes": "FrodoKEM-976-AES",
|
|
76
|
+
"frodokem-976-shake": "FrodoKEM-976-SHAKE",
|
|
77
|
+
"frodokem-1344-aes": "FrodoKEM-1344-AES",
|
|
78
|
+
"frodokem-1344-shake": "FrodoKEM-1344-SHAKE",
|
|
79
|
+
// NTRU (lattice-based, re-added in liboqs 0.15)
|
|
80
|
+
"ntru-hps-2048-509": "NTRU-HPS-2048-509",
|
|
81
|
+
"ntru-hps-2048-677": "NTRU-HPS-2048-677",
|
|
82
|
+
"ntru-hps-4096-821": "NTRU-HPS-4096-821",
|
|
83
|
+
"ntru-hps-4096-1229": "NTRU-HPS-4096-1229",
|
|
84
|
+
"ntru-hrss-701": "NTRU-HRSS-701",
|
|
85
|
+
"ntru-hrss-1373": "NTRU-HRSS-1373",
|
|
86
|
+
// NTRU-Prime
|
|
87
|
+
sntrup761: "sntrup761",
|
|
88
|
+
// MAYO (NIST Additional Signatures Round 2)
|
|
89
|
+
"mayo-1": "MAYO-1",
|
|
90
|
+
"mayo-2": "MAYO-2",
|
|
91
|
+
"mayo-3": "MAYO-3",
|
|
92
|
+
"mayo-5": "MAYO-5",
|
|
93
|
+
// CROSS (NIST Additional Signatures Round 2)
|
|
94
|
+
"cross-rsdp-128-balanced": "CROSS-RSDP-128-balanced",
|
|
95
|
+
"cross-rsdp-128-fast": "CROSS-RSDP-128-fast",
|
|
96
|
+
"cross-rsdp-128-small": "CROSS-RSDP-128-small",
|
|
97
|
+
"cross-rsdp-192-balanced": "CROSS-RSDP-192-balanced",
|
|
98
|
+
"cross-rsdp-192-fast": "CROSS-RSDP-192-fast",
|
|
99
|
+
"cross-rsdp-192-small": "CROSS-RSDP-192-small",
|
|
100
|
+
"cross-rsdp-256-balanced": "CROSS-RSDP-256-balanced",
|
|
101
|
+
"cross-rsdp-256-fast": "CROSS-RSDP-256-fast",
|
|
102
|
+
"cross-rsdp-256-small": "CROSS-RSDP-256-small",
|
|
103
|
+
"cross-rsdpg-128-balanced": "CROSS-RSDPG-128-balanced",
|
|
104
|
+
"cross-rsdpg-128-fast": "CROSS-RSDPG-128-fast",
|
|
105
|
+
"cross-rsdpg-128-small": "CROSS-RSDPG-128-small",
|
|
106
|
+
"cross-rsdpg-192-balanced": "CROSS-RSDPG-192-balanced",
|
|
107
|
+
"cross-rsdpg-192-fast": "CROSS-RSDPG-192-fast",
|
|
108
|
+
"cross-rsdpg-192-small": "CROSS-RSDPG-192-small",
|
|
109
|
+
"cross-rsdpg-256-balanced": "CROSS-RSDPG-256-balanced",
|
|
110
|
+
"cross-rsdpg-256-fast": "CROSS-RSDPG-256-fast",
|
|
111
|
+
"cross-rsdpg-256-small": "CROSS-RSDPG-256-small",
|
|
112
|
+
// UOV (NIST Additional Signatures Round 2)
|
|
113
|
+
"ov-Is": "UOV-Is",
|
|
114
|
+
"ov-Ip": "UOV-Ip",
|
|
115
|
+
"ov-III": "UOV-III",
|
|
116
|
+
"ov-V": "UOV-V",
|
|
117
|
+
"ov-Is-pkc": "UOV-Is-pkc",
|
|
118
|
+
"ov-Ip-pkc": "UOV-Ip-pkc",
|
|
119
|
+
"ov-III-pkc": "UOV-III-pkc",
|
|
120
|
+
"ov-V-pkc": "UOV-V-pkc",
|
|
121
|
+
"ov-Is-pkc-skc": "UOV-Is-pkc-skc",
|
|
122
|
+
"ov-Ip-pkc-skc": "UOV-Ip-pkc-skc",
|
|
123
|
+
"ov-III-pkc-skc": "UOV-III-pkc-skc",
|
|
124
|
+
"ov-V-pkc-skc": "UOV-V-pkc-skc",
|
|
125
|
+
// SNOVA (NIST Additional Signatures Round 2, liboqs 0.14+)
|
|
126
|
+
"snova-24-5-4": "SNOVA-24-5-4",
|
|
127
|
+
"snova-24-5-4-shake": "SNOVA-24-5-4-SHAKE",
|
|
128
|
+
"snova-24-5-4-esk": "SNOVA-24-5-4-ESK",
|
|
129
|
+
"snova-24-5-4-shake-esk": "SNOVA-24-5-4-SHAKE-ESK",
|
|
130
|
+
"snova-25-8-3": "SNOVA-25-8-3",
|
|
131
|
+
"snova-37-17-2": "SNOVA-37-17-2",
|
|
132
|
+
"snova-37-8-4": "SNOVA-37-8-4",
|
|
133
|
+
"snova-24-5-5": "SNOVA-24-5-5",
|
|
134
|
+
"snova-56-25-2": "SNOVA-56-25-2",
|
|
135
|
+
"snova-49-11-3": "SNOVA-49-11-3",
|
|
136
|
+
"snova-60-10-4": "SNOVA-60-10-4",
|
|
137
|
+
"snova-29-6-5": "SNOVA-29-6-5",
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Convert internal algorithm name to NIST standardized name.
|
|
15
142
|
*/
|
|
143
|
+
export function toNistAlgorithmName(algorithm: string): string {
|
|
144
|
+
return ALGORITHM_TO_NIST[algorithm] ?? algorithm;
|
|
145
|
+
}
|
|
16
146
|
|
|
17
147
|
export interface StorageClientConfig {
|
|
18
148
|
readonly baseUrl: string;
|
|
19
|
-
readonly apiKey
|
|
149
|
+
readonly apiKey: string;
|
|
20
150
|
readonly tenantId: string;
|
|
21
151
|
readonly timeoutMs?: number;
|
|
152
|
+
readonly maxRetries?: number;
|
|
153
|
+
readonly retryDelayMs?: number;
|
|
22
154
|
readonly telemetry?: StorageClientTelemetry | StorageClientTelemetryConfig;
|
|
23
155
|
}
|
|
24
156
|
|
|
@@ -27,6 +159,8 @@ type InternalStorageClientConfig = {
|
|
|
27
159
|
readonly apiKey: string;
|
|
28
160
|
readonly tenantId: string;
|
|
29
161
|
readonly timeoutMs: number;
|
|
162
|
+
readonly maxRetries: number;
|
|
163
|
+
readonly retryDelayMs: number;
|
|
30
164
|
};
|
|
31
165
|
|
|
32
166
|
export interface InitiateUploadOptions {
|
|
@@ -154,11 +288,37 @@ export class StorageClient {
|
|
|
154
288
|
private readonly targetService: string;
|
|
155
289
|
|
|
156
290
|
constructor(config: StorageClientConfig) {
|
|
291
|
+
if (!config.apiKey || config.apiKey.trim().length === 0) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
"QNSP Storage SDK: apiKey is required. " +
|
|
294
|
+
"Get your free API key at https://cloud.qnsp.cuilabs.io/signup — " +
|
|
295
|
+
"no credit card required (FREE tier: 5 GB storage, 2,000 API calls/month). " +
|
|
296
|
+
"Docs: https://docs.qnsp.cuilabs.io/sdk/storage-sdk",
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
301
|
+
|
|
302
|
+
// Enforce HTTPS in production (allow HTTP only for localhost in development)
|
|
303
|
+
if (!baseUrl.startsWith("https://")) {
|
|
304
|
+
const isLocalhost =
|
|
305
|
+
baseUrl.startsWith("http://localhost") || baseUrl.startsWith("http://127.0.0.1");
|
|
306
|
+
const isDevelopment =
|
|
307
|
+
process.env["NODE_ENV"] === "development" || process.env["NODE_ENV"] === "test";
|
|
308
|
+
if (!isLocalhost || !isDevelopment) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
"baseUrl must use HTTPS in production. HTTP is only allowed for localhost in development.",
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
157
315
|
this.config = {
|
|
158
|
-
baseUrl
|
|
159
|
-
apiKey: config.apiKey
|
|
316
|
+
baseUrl,
|
|
317
|
+
apiKey: config.apiKey,
|
|
160
318
|
tenantId: config.tenantId,
|
|
161
319
|
timeoutMs: config.timeoutMs ?? 30_000,
|
|
320
|
+
maxRetries: config.maxRetries ?? 3,
|
|
321
|
+
retryDelayMs: config.retryDelayMs ?? 1_000,
|
|
162
322
|
};
|
|
163
323
|
|
|
164
324
|
this.telemetry = config.telemetry
|
|
@@ -175,15 +335,22 @@ export class StorageClient {
|
|
|
175
335
|
}
|
|
176
336
|
|
|
177
337
|
private async request<T>(method: string, path: string, options?: RequestOptions): Promise<T> {
|
|
338
|
+
return this.requestWithRetry<T>(method, path, options, 0);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private async requestWithRetry<T>(
|
|
342
|
+
method: string,
|
|
343
|
+
path: string,
|
|
344
|
+
options: RequestOptions | undefined,
|
|
345
|
+
attempt: number,
|
|
346
|
+
): Promise<T> {
|
|
178
347
|
const url = `${this.config.baseUrl}${path}`;
|
|
179
348
|
const headers: Record<string, string> = {
|
|
180
349
|
"Content-Type": "application/json",
|
|
181
350
|
...options?.headers,
|
|
182
351
|
};
|
|
183
352
|
|
|
184
|
-
|
|
185
|
-
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
186
|
-
}
|
|
353
|
+
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
187
354
|
|
|
188
355
|
const controller = new AbortController();
|
|
189
356
|
const timeoutId = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
@@ -211,15 +378,39 @@ export class StorageClient {
|
|
|
211
378
|
clearTimeout(timeoutId);
|
|
212
379
|
httpStatus = response.status;
|
|
213
380
|
|
|
214
|
-
|
|
381
|
+
// Handle rate limiting (429) with retry logic
|
|
382
|
+
if (response.status === 429) {
|
|
383
|
+
if (attempt < this.config.maxRetries) {
|
|
384
|
+
const retryAfterHeader = response.headers.get("Retry-After");
|
|
385
|
+
let delayMs = this.config.retryDelayMs;
|
|
386
|
+
|
|
387
|
+
if (retryAfterHeader) {
|
|
388
|
+
const retryAfterSeconds = Number.parseInt(retryAfterHeader, 10);
|
|
389
|
+
if (!Number.isNaN(retryAfterSeconds) && retryAfterSeconds > 0) {
|
|
390
|
+
delayMs = retryAfterSeconds * 1_000;
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
// Exponential backoff: 2^attempt * baseDelay, capped at 30 seconds
|
|
394
|
+
delayMs = Math.min(2 ** attempt * this.config.retryDelayMs, 30_000);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
398
|
+
return this.requestWithRetry<T>(method, path, options, attempt + 1);
|
|
399
|
+
}
|
|
400
|
+
|
|
215
401
|
status = "error";
|
|
216
|
-
|
|
217
|
-
errorMessage = errorText;
|
|
402
|
+
errorMessage = `HTTP ${response.status}`;
|
|
218
403
|
throw new Error(
|
|
219
|
-
`Storage API error:
|
|
404
|
+
`Storage API error: Rate limit exceeded after ${this.config.maxRetries} retries`,
|
|
220
405
|
);
|
|
221
406
|
}
|
|
222
407
|
|
|
408
|
+
if (!response.ok) {
|
|
409
|
+
status = "error";
|
|
410
|
+
errorMessage = `HTTP ${response.status}`;
|
|
411
|
+
throw new Error(`Storage API error: ${response.status} ${response.statusText}`);
|
|
412
|
+
}
|
|
413
|
+
|
|
223
414
|
if (response.status === 204) {
|
|
224
415
|
return undefined as T;
|
|
225
416
|
}
|
|
@@ -261,13 +452,23 @@ export class StorageClient {
|
|
|
261
452
|
readonly totalParts: number;
|
|
262
453
|
readonly expiresAt: string;
|
|
263
454
|
readonly resumeToken: string | null;
|
|
264
|
-
readonly pqc:
|
|
265
|
-
readonly provider: string;
|
|
266
|
-
readonly algorithm: string;
|
|
267
|
-
readonly keyId: string;
|
|
268
|
-
};
|
|
455
|
+
readonly pqc: PqcMetadata;
|
|
269
456
|
}> {
|
|
270
|
-
|
|
457
|
+
const result = await this.request<{
|
|
458
|
+
uploadId: string;
|
|
459
|
+
documentId: string;
|
|
460
|
+
tenantId: string;
|
|
461
|
+
chunkSizeBytes: number;
|
|
462
|
+
totalSizeBytes: number;
|
|
463
|
+
totalParts: number;
|
|
464
|
+
expiresAt: string;
|
|
465
|
+
resumeToken: string | null;
|
|
466
|
+
pqc: {
|
|
467
|
+
provider: string;
|
|
468
|
+
algorithm: string;
|
|
469
|
+
keyId: string;
|
|
470
|
+
};
|
|
471
|
+
}>("POST", "/storage/v1/documents", {
|
|
271
472
|
body: {
|
|
272
473
|
name: options.name,
|
|
273
474
|
mimeType: options.mimeType,
|
|
@@ -279,6 +480,15 @@ export class StorageClient {
|
|
|
279
480
|
},
|
|
280
481
|
operation: "initiateUpload",
|
|
281
482
|
});
|
|
483
|
+
|
|
484
|
+
// Enrich PQC metadata with NIST algorithm name
|
|
485
|
+
return {
|
|
486
|
+
...result,
|
|
487
|
+
pqc: {
|
|
488
|
+
...result.pqc,
|
|
489
|
+
algorithmNist: toNistAlgorithmName(result.pqc.algorithm),
|
|
490
|
+
},
|
|
491
|
+
};
|
|
282
492
|
}
|
|
283
493
|
|
|
284
494
|
async uploadPart(
|
|
@@ -291,9 +501,7 @@ export class StorageClient {
|
|
|
291
501
|
"Content-Type": "application/octet-stream",
|
|
292
502
|
};
|
|
293
503
|
|
|
294
|
-
|
|
295
|
-
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
296
|
-
}
|
|
504
|
+
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
297
505
|
|
|
298
506
|
const bytesSent =
|
|
299
507
|
data instanceof Buffer || data instanceof Uint8Array ? data.byteLength : undefined;
|
|
@@ -336,11 +544,8 @@ export class StorageClient {
|
|
|
336
544
|
|
|
337
545
|
if (!response.ok) {
|
|
338
546
|
status = "error";
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
throw new Error(
|
|
342
|
-
`Upload part error: ${response.status} ${response.statusText} - ${errorText}`,
|
|
343
|
-
);
|
|
547
|
+
errorMessage = `HTTP ${response.status}`;
|
|
548
|
+
throw new Error(`Upload part error: ${response.status} ${response.statusText}`);
|
|
344
549
|
}
|
|
345
550
|
|
|
346
551
|
return (await response.json()) as UploadPartResult;
|
|
@@ -373,6 +578,7 @@ export class StorageClient {
|
|
|
373
578
|
}
|
|
374
579
|
|
|
375
580
|
async getUploadStatus(uploadId: string): Promise<UploadStatus> {
|
|
581
|
+
validateUUID(uploadId, "uploadId");
|
|
376
582
|
// Use GET since we need the full status object
|
|
377
583
|
return this.request<UploadStatus>("GET", `/storage/v1/uploads/${uploadId}`, {
|
|
378
584
|
operation: "getUploadStatus",
|
|
@@ -381,6 +587,7 @@ export class StorageClient {
|
|
|
381
587
|
}
|
|
382
588
|
|
|
383
589
|
async completeUpload(uploadId: string): Promise<CompleteUploadResult> {
|
|
590
|
+
validateUUID(uploadId, "uploadId");
|
|
384
591
|
return this.request("POST", `/storage/v1/uploads/${uploadId}/complete`, {
|
|
385
592
|
operation: "completeUpload",
|
|
386
593
|
telemetryRoute: "/storage/v1/uploads/:uploadId/complete",
|
|
@@ -461,9 +668,7 @@ export class StorageClient {
|
|
|
461
668
|
headers["Range"] = options.range;
|
|
462
669
|
}
|
|
463
670
|
|
|
464
|
-
|
|
465
|
-
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
466
|
-
}
|
|
671
|
+
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
467
672
|
|
|
468
673
|
const controller = new AbortController();
|
|
469
674
|
const timeoutId = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
@@ -486,9 +691,8 @@ export class StorageClient {
|
|
|
486
691
|
|
|
487
692
|
if (!response.ok) {
|
|
488
693
|
status = "error";
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
throw new Error(`Download error: ${response.status} ${response.statusText} - ${errorText}`);
|
|
694
|
+
errorMessage = `HTTP ${response.status}`;
|
|
695
|
+
throw new Error(`Download error: ${response.status} ${response.statusText}`);
|
|
492
696
|
}
|
|
493
697
|
|
|
494
698
|
const contentRange = response.headers.get("Content-Range");
|
|
@@ -563,6 +767,7 @@ export class StorageClient {
|
|
|
563
767
|
* Requires x-tenant-id header.
|
|
564
768
|
*/
|
|
565
769
|
async getDocumentPolicies(documentId: string): Promise<DocumentPolicies> {
|
|
770
|
+
validateUUID(documentId, "documentId");
|
|
566
771
|
return this.request("GET", `/storage/v1/documents/${documentId}/policies`, {
|
|
567
772
|
operation: "getDocumentPolicies",
|
|
568
773
|
telemetryRoute: "/storage/v1/documents/:documentId/policies",
|
|
@@ -580,6 +785,7 @@ export class StorageClient {
|
|
|
580
785
|
documentId: string,
|
|
581
786
|
input: UpdatePoliciesRequest,
|
|
582
787
|
): Promise<DocumentPolicies> {
|
|
788
|
+
validateUUID(documentId, "documentId");
|
|
583
789
|
return this.request("PATCH", `/storage/v1/documents/${documentId}/policies`, {
|
|
584
790
|
body: input,
|
|
585
791
|
operation: "updateDocumentPolicies",
|
|
@@ -602,6 +808,7 @@ export class StorageClient {
|
|
|
602
808
|
readonly tenantId: string;
|
|
603
809
|
readonly legalHolds: readonly string[];
|
|
604
810
|
}> {
|
|
811
|
+
validateUUID(documentId, "documentId");
|
|
605
812
|
return this.request("POST", `/storage/v1/documents/${documentId}/legal-holds`, {
|
|
606
813
|
body: request,
|
|
607
814
|
operation: "applyLegalHold",
|
|
@@ -617,6 +824,7 @@ export class StorageClient {
|
|
|
617
824
|
* Requires x-tenant-id header.
|
|
618
825
|
*/
|
|
619
826
|
async releaseLegalHold(documentId: string, holdId: string): Promise<void> {
|
|
827
|
+
validateUUID(documentId, "documentId");
|
|
620
828
|
return this.request("DELETE", `/storage/v1/documents/${documentId}/legal-holds/${holdId}`, {
|
|
621
829
|
operation: "releaseLegalHold",
|
|
622
830
|
telemetryRoute: "/storage/v1/documents/:documentId/legal-holds/:holdId",
|
|
@@ -642,6 +850,7 @@ export class StorageClient {
|
|
|
642
850
|
readonly transitionAfter?: string | null;
|
|
643
851
|
};
|
|
644
852
|
}> {
|
|
853
|
+
validateUUID(documentId, "documentId");
|
|
645
854
|
return this.request("POST", `/storage/v1/documents/${documentId}/lifecycle/transitions`, {
|
|
646
855
|
body: request,
|
|
647
856
|
operation: "scheduleLifecycleTransition",
|
|
@@ -662,3 +871,4 @@ export class StorageClient {
|
|
|
662
871
|
|
|
663
872
|
export * from "./events.js";
|
|
664
873
|
export * from "./observability.js";
|
|
874
|
+
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
|
+
}
|