@qnsp/storage-sdk 0.2.1 → 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 +49 -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 +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +164 -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 +8 -8
- package/src/event-envelope.ts +1 -2
- package/src/events.ts +0 -2
- package/src/index.test.ts +42 -25
- package/src/index.ts +188 -27
- package/src/validation.ts +21 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;GAEG;AAEH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;AAEjE;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa,EAAE,SAAiB;IAC5D,IAAI,CAAC;QACJ,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACzB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,IAAI,KAAK,YAAY,CAAC,CAAC,QAAQ,EAAE,CAAC;YACjC,MAAM,IAAI,KAAK,CAAC,WAAW,SAAS,KAAK,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,IAAI,gBAAgB,EAAE,CAAC,CAAC;QAC1F,CAAC;QACD,MAAM,KAAK,CAAC;IACb,CAAC;AACF,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qnsp/storage-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -23,16 +23,16 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@opentelemetry/api": "^1.9.0",
|
|
26
|
-
"@opentelemetry/exporter-metrics-otlp-http": "^0.
|
|
27
|
-
"@opentelemetry/resources": "^2.
|
|
28
|
-
"@opentelemetry/sdk-metrics": "^2.
|
|
29
|
-
"undici": "^7.
|
|
30
|
-
"zod": "^4.
|
|
26
|
+
"@opentelemetry/exporter-metrics-otlp-http": "^0.210.0",
|
|
27
|
+
"@opentelemetry/resources": "^2.4.0",
|
|
28
|
+
"@opentelemetry/sdk-metrics": "^2.4.0",
|
|
29
|
+
"undici": "^7.18.2",
|
|
30
|
+
"zod": "^4.3.5"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@types/node": "^
|
|
33
|
+
"@types/node": "^25.0.9",
|
|
34
34
|
"tsx": "^4.21.0",
|
|
35
|
-
"vitest": "4.0.
|
|
35
|
+
"vitest": "4.0.17"
|
|
36
36
|
},
|
|
37
37
|
"engines": {
|
|
38
38
|
"node": "24.12.0"
|
package/src/event-envelope.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import { z } from "zod";
|
|
3
2
|
|
|
4
3
|
export const eventMetadataSchema = z.object({
|
|
@@ -20,7 +19,7 @@ export const eventEnvelopeSchema = z.object({
|
|
|
20
19
|
id: z
|
|
21
20
|
.string()
|
|
22
21
|
.uuid()
|
|
23
|
-
.default(() => randomUUID()),
|
|
22
|
+
.default(() => crypto.randomUUID()),
|
|
24
23
|
topic: z.string().min(1),
|
|
25
24
|
version: z.string().min(1).default("1"),
|
|
26
25
|
occuredAt: z
|
package/src/events.ts
CHANGED
package/src/index.test.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import { StorageClient, StorageEventsClient } from "./index.js";
|
|
3
3
|
|
|
4
|
-
const mockTenantId = "
|
|
4
|
+
const mockTenantId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
|
|
5
5
|
const baseUrl = "https://storage.qnsp.example/";
|
|
6
6
|
|
|
7
|
+
// Valid UUID test constants
|
|
8
|
+
const mockUploadId = "11111111-1111-4111-a111-111111111111";
|
|
9
|
+
const mockUploadId2 = "22222222-2222-4222-a222-222222222222";
|
|
10
|
+
const mockUploadIdErr = "33333333-3333-4333-a333-333333333333";
|
|
11
|
+
const mockDocumentId = "44444444-4444-4444-a444-444444444444";
|
|
12
|
+
|
|
7
13
|
function jsonResponse(payload: unknown, init?: ResponseInit): Response {
|
|
8
14
|
return new Response(JSON.stringify(payload), {
|
|
9
15
|
status: 200,
|
|
@@ -60,8 +66,8 @@ describe("StorageClient", () => {
|
|
|
60
66
|
});
|
|
61
67
|
|
|
62
68
|
const expectedResponse = {
|
|
63
|
-
uploadId:
|
|
64
|
-
documentId:
|
|
69
|
+
uploadId: mockUploadId,
|
|
70
|
+
documentId: mockDocumentId,
|
|
65
71
|
tenantId: mockTenantId,
|
|
66
72
|
chunkSizeBytes: 16_777_216,
|
|
67
73
|
totalSizeBytes: 42_000_000,
|
|
@@ -118,7 +124,7 @@ describe("StorageClient", () => {
|
|
|
118
124
|
});
|
|
119
125
|
|
|
120
126
|
const expectedPayload = {
|
|
121
|
-
uploadId:
|
|
127
|
+
uploadId: mockUploadId,
|
|
122
128
|
partId: 1,
|
|
123
129
|
status: "uploaded",
|
|
124
130
|
sizeBytes: 3,
|
|
@@ -137,7 +143,7 @@ describe("StorageClient", () => {
|
|
|
137
143
|
|
|
138
144
|
const source = Buffer.from([0xde, 0xad, 0xbe]);
|
|
139
145
|
|
|
140
|
-
const result = await client.uploadPart(
|
|
146
|
+
const result = await client.uploadPart(mockUploadId, 1, source);
|
|
141
147
|
expect(result).toEqual(expectedPayload);
|
|
142
148
|
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
143
149
|
const uploadCall = (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mock.calls.at(0);
|
|
@@ -158,6 +164,7 @@ describe("StorageClient", () => {
|
|
|
158
164
|
it("parses ranged download responses and surfaces stream metadata", async () => {
|
|
159
165
|
const client = new StorageClient({
|
|
160
166
|
baseUrl,
|
|
167
|
+
apiKey: "test-api-key",
|
|
161
168
|
tenantId: mockTenantId,
|
|
162
169
|
});
|
|
163
170
|
|
|
@@ -179,12 +186,12 @@ describe("StorageClient", () => {
|
|
|
179
186
|
|
|
180
187
|
(globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mockResolvedValue(response);
|
|
181
188
|
|
|
182
|
-
const result = await client.downloadStream(
|
|
189
|
+
const result = await client.downloadStream(mockDocumentId, 2, {
|
|
183
190
|
range: "bytes=0-2",
|
|
184
191
|
});
|
|
185
192
|
|
|
186
193
|
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
187
|
-
|
|
194
|
+
`https://storage.qnsp.example/storage/v1/documents/${mockDocumentId}/versions/2/content?tenantId=${mockTenantId}&range=bytes%3D0-2`,
|
|
188
195
|
expect.objectContaining({
|
|
189
196
|
method: "GET",
|
|
190
197
|
headers: expect.objectContaining({
|
|
@@ -207,13 +214,14 @@ describe("StorageClient", () => {
|
|
|
207
214
|
const telemetry = { record: vi.fn() };
|
|
208
215
|
const client = new StorageClient({
|
|
209
216
|
baseUrl,
|
|
217
|
+
apiKey: "test-api-key",
|
|
210
218
|
tenantId: mockTenantId,
|
|
211
219
|
telemetry,
|
|
212
220
|
});
|
|
213
221
|
|
|
214
222
|
const statusResponse = {
|
|
215
|
-
uploadId:
|
|
216
|
-
documentId:
|
|
223
|
+
uploadId: mockUploadId2,
|
|
224
|
+
documentId: mockDocumentId,
|
|
217
225
|
tenantId: mockTenantId,
|
|
218
226
|
status: "pending",
|
|
219
227
|
chunkSizeBytes: 4,
|
|
@@ -232,7 +240,7 @@ describe("StorageClient", () => {
|
|
|
232
240
|
jsonResponse(statusResponse),
|
|
233
241
|
);
|
|
234
242
|
|
|
235
|
-
await client.getUploadStatus(
|
|
243
|
+
await client.getUploadStatus(mockUploadId2);
|
|
236
244
|
|
|
237
245
|
expect(telemetry.record).toHaveBeenCalled();
|
|
238
246
|
const event = (telemetry.record as ReturnType<typeof vi.fn>).mock.calls.at(-1)?.[0];
|
|
@@ -248,6 +256,7 @@ describe("StorageClient", () => {
|
|
|
248
256
|
const telemetry = { record: vi.fn() };
|
|
249
257
|
const client = new StorageClient({
|
|
250
258
|
baseUrl,
|
|
259
|
+
apiKey: "test-api-key",
|
|
251
260
|
tenantId: mockTenantId,
|
|
252
261
|
telemetry,
|
|
253
262
|
});
|
|
@@ -256,7 +265,7 @@ describe("StorageClient", () => {
|
|
|
256
265
|
new Response("boom", { status: 500, statusText: "error" }),
|
|
257
266
|
);
|
|
258
267
|
|
|
259
|
-
await expect(client.completeUpload(
|
|
268
|
+
await expect(client.completeUpload(mockUploadIdErr)).rejects.toThrow(/Storage API error/);
|
|
260
269
|
|
|
261
270
|
const event = (telemetry.record as ReturnType<typeof vi.fn>).mock.calls.at(-1)?.[0];
|
|
262
271
|
expect(event).toBeDefined();
|
|
@@ -271,12 +280,13 @@ describe("StorageClient", () => {
|
|
|
271
280
|
const telemetry = { record: vi.fn() };
|
|
272
281
|
const client = new StorageClient({
|
|
273
282
|
baseUrl,
|
|
283
|
+
apiKey: "test-api-key",
|
|
274
284
|
tenantId: mockTenantId,
|
|
275
285
|
telemetry,
|
|
276
286
|
});
|
|
277
287
|
|
|
278
288
|
const expectedPayload = {
|
|
279
|
-
uploadId:
|
|
289
|
+
uploadId: mockUploadId,
|
|
280
290
|
partId: 1,
|
|
281
291
|
status: "uploaded",
|
|
282
292
|
sizeBytes: 3,
|
|
@@ -293,7 +303,7 @@ describe("StorageClient", () => {
|
|
|
293
303
|
jsonResponse(expectedPayload),
|
|
294
304
|
);
|
|
295
305
|
|
|
296
|
-
await client.uploadPart(
|
|
306
|
+
await client.uploadPart(mockUploadId, 1, Buffer.from([1, 2, 3]));
|
|
297
307
|
|
|
298
308
|
const event = (telemetry.record as ReturnType<typeof vi.fn>).mock.calls.at(-1)?.[0];
|
|
299
309
|
expect(event).toBeDefined();
|
|
@@ -315,7 +325,7 @@ describe("StorageClient", () => {
|
|
|
315
325
|
// PATCH response
|
|
316
326
|
.mockResolvedValueOnce(
|
|
317
327
|
jsonResponse({
|
|
318
|
-
documentId:
|
|
328
|
+
documentId: mockDocumentId,
|
|
319
329
|
tenantId: mockTenantId,
|
|
320
330
|
compliance: {
|
|
321
331
|
retentionMode: "compliance",
|
|
@@ -328,7 +338,7 @@ describe("StorageClient", () => {
|
|
|
328
338
|
// GET response
|
|
329
339
|
.mockResolvedValueOnce(
|
|
330
340
|
jsonResponse({
|
|
331
|
-
documentId:
|
|
341
|
+
documentId: mockDocumentId,
|
|
332
342
|
tenantId: mockTenantId,
|
|
333
343
|
compliance: {
|
|
334
344
|
retentionMode: "compliance",
|
|
@@ -344,26 +354,30 @@ describe("StorageClient", () => {
|
|
|
344
354
|
}),
|
|
345
355
|
);
|
|
346
356
|
|
|
347
|
-
const updated = await client.updateDocumentPolicies(
|
|
357
|
+
const updated = await client.updateDocumentPolicies(mockDocumentId, {
|
|
348
358
|
retentionMode: "compliance",
|
|
349
359
|
retainUntil: "2026-01-01T00:00:00Z",
|
|
350
360
|
legalHolds: ["hold-1"],
|
|
351
361
|
});
|
|
352
|
-
expect(updated.documentId).toBe(
|
|
362
|
+
expect(updated.documentId).toBe(mockDocumentId);
|
|
353
363
|
const patchCall = (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mock
|
|
354
364
|
.calls[0] as unknown as [string, RequestInit];
|
|
355
|
-
expect(patchCall[0]).toBe(
|
|
365
|
+
expect(patchCall[0]).toBe(
|
|
366
|
+
`https://storage.qnsp.example/storage/v1/documents/${mockDocumentId}/policies`,
|
|
367
|
+
);
|
|
356
368
|
expect(patchCall[1]?.headers).toMatchObject({
|
|
357
369
|
"Content-Type": "application/json",
|
|
358
370
|
Authorization: "Bearer key",
|
|
359
371
|
"x-tenant-id": mockTenantId,
|
|
360
372
|
});
|
|
361
373
|
|
|
362
|
-
const policies = await client.getDocumentPolicies(
|
|
374
|
+
const policies = await client.getDocumentPolicies(mockDocumentId);
|
|
363
375
|
expect(policies.tenantId).toBe(mockTenantId);
|
|
364
376
|
const getCall = (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mock
|
|
365
377
|
.calls[1] as unknown as [string, RequestInit];
|
|
366
|
-
expect(getCall[0]).toBe(
|
|
378
|
+
expect(getCall[0]).toBe(
|
|
379
|
+
`https://storage.qnsp.example/storage/v1/documents/${mockDocumentId}/policies`,
|
|
380
|
+
);
|
|
367
381
|
expect(getCall[1]?.headers).toMatchObject({
|
|
368
382
|
"Content-Type": "application/json",
|
|
369
383
|
Authorization: "Bearer key",
|
|
@@ -382,7 +396,7 @@ describe("StorageClient", () => {
|
|
|
382
396
|
// apply
|
|
383
397
|
.mockResolvedValueOnce(
|
|
384
398
|
jsonResponse({
|
|
385
|
-
documentId:
|
|
399
|
+
documentId: mockDocumentId,
|
|
386
400
|
tenantId: mockTenantId,
|
|
387
401
|
legalHolds: ["hold-9"],
|
|
388
402
|
}),
|
|
@@ -390,21 +404,23 @@ describe("StorageClient", () => {
|
|
|
390
404
|
// release (204)
|
|
391
405
|
.mockResolvedValueOnce(new Response(null, { status: 204 }));
|
|
392
406
|
|
|
393
|
-
const applied = await client.applyLegalHold(
|
|
407
|
+
const applied = await client.applyLegalHold(mockDocumentId, { holdId: "hold-9" });
|
|
394
408
|
expect(applied.legalHolds).toContain("hold-9");
|
|
395
409
|
const postCall = (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mock
|
|
396
410
|
.calls[0] as unknown as [string, RequestInit];
|
|
397
|
-
expect(postCall[0]).toBe(
|
|
411
|
+
expect(postCall[0]).toBe(
|
|
412
|
+
`https://storage.qnsp.example/storage/v1/documents/${mockDocumentId}/legal-holds`,
|
|
413
|
+
);
|
|
398
414
|
expect(postCall[1]?.method).toBe("POST");
|
|
399
415
|
expect(postCall[1]?.headers).toMatchObject({
|
|
400
416
|
"x-tenant-id": mockTenantId,
|
|
401
417
|
});
|
|
402
418
|
|
|
403
|
-
await client.releaseLegalHold(
|
|
419
|
+
await client.releaseLegalHold(mockDocumentId, "hold-9");
|
|
404
420
|
const delCall = (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mock
|
|
405
421
|
.calls[1] as unknown as [string, RequestInit];
|
|
406
422
|
expect(delCall[0]).toBe(
|
|
407
|
-
|
|
423
|
+
`https://storage.qnsp.example/storage/v1/documents/${mockDocumentId}/legal-holds/hold-9`,
|
|
408
424
|
);
|
|
409
425
|
expect(delCall[1]?.method).toBe("DELETE");
|
|
410
426
|
expect(delCall[1]?.headers).toMatchObject({
|
|
@@ -427,6 +443,7 @@ describe("StorageEventsClient", () => {
|
|
|
427
443
|
const telemetry = { record: vi.fn() };
|
|
428
444
|
const client = new StorageEventsClient({
|
|
429
445
|
baseUrl,
|
|
446
|
+
apiKey: "test-api-key",
|
|
430
447
|
telemetry,
|
|
431
448
|
});
|
|
432
449
|
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
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
|
|
@@ -26,15 +25,116 @@ export interface PqcMetadata {
|
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
/**
|
|
29
|
-
* Mapping from internal algorithm names to NIST
|
|
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
|
|
30
31
|
*/
|
|
31
32
|
export const ALGORITHM_TO_NIST: Record<string, string> = {
|
|
33
|
+
// FIPS 203 — ML-KEM
|
|
32
34
|
"kyber-512": "ML-KEM-512",
|
|
33
35
|
"kyber-768": "ML-KEM-768",
|
|
34
36
|
"kyber-1024": "ML-KEM-1024",
|
|
37
|
+
// FIPS 204 — ML-DSA
|
|
35
38
|
"dilithium-2": "ML-DSA-44",
|
|
36
39
|
"dilithium-3": "ML-DSA-65",
|
|
37
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",
|
|
38
138
|
};
|
|
39
139
|
|
|
40
140
|
/**
|
|
@@ -46,9 +146,11 @@ export function toNistAlgorithmName(algorithm: string): string {
|
|
|
46
146
|
|
|
47
147
|
export interface StorageClientConfig {
|
|
48
148
|
readonly baseUrl: string;
|
|
49
|
-
readonly apiKey
|
|
149
|
+
readonly apiKey: string;
|
|
50
150
|
readonly tenantId: string;
|
|
51
151
|
readonly timeoutMs?: number;
|
|
152
|
+
readonly maxRetries?: number;
|
|
153
|
+
readonly retryDelayMs?: number;
|
|
52
154
|
readonly telemetry?: StorageClientTelemetry | StorageClientTelemetryConfig;
|
|
53
155
|
}
|
|
54
156
|
|
|
@@ -57,6 +159,8 @@ type InternalStorageClientConfig = {
|
|
|
57
159
|
readonly apiKey: string;
|
|
58
160
|
readonly tenantId: string;
|
|
59
161
|
readonly timeoutMs: number;
|
|
162
|
+
readonly maxRetries: number;
|
|
163
|
+
readonly retryDelayMs: number;
|
|
60
164
|
};
|
|
61
165
|
|
|
62
166
|
export interface InitiateUploadOptions {
|
|
@@ -184,11 +288,37 @@ export class StorageClient {
|
|
|
184
288
|
private readonly targetService: string;
|
|
185
289
|
|
|
186
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
|
+
|
|
187
315
|
this.config = {
|
|
188
|
-
baseUrl
|
|
189
|
-
apiKey: config.apiKey
|
|
316
|
+
baseUrl,
|
|
317
|
+
apiKey: config.apiKey,
|
|
190
318
|
tenantId: config.tenantId,
|
|
191
319
|
timeoutMs: config.timeoutMs ?? 30_000,
|
|
320
|
+
maxRetries: config.maxRetries ?? 3,
|
|
321
|
+
retryDelayMs: config.retryDelayMs ?? 1_000,
|
|
192
322
|
};
|
|
193
323
|
|
|
194
324
|
this.telemetry = config.telemetry
|
|
@@ -205,15 +335,22 @@ export class StorageClient {
|
|
|
205
335
|
}
|
|
206
336
|
|
|
207
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> {
|
|
208
347
|
const url = `${this.config.baseUrl}${path}`;
|
|
209
348
|
const headers: Record<string, string> = {
|
|
210
349
|
"Content-Type": "application/json",
|
|
211
350
|
...options?.headers,
|
|
212
351
|
};
|
|
213
352
|
|
|
214
|
-
|
|
215
|
-
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
216
|
-
}
|
|
353
|
+
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
217
354
|
|
|
218
355
|
const controller = new AbortController();
|
|
219
356
|
const timeoutId = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
@@ -241,15 +378,39 @@ export class StorageClient {
|
|
|
241
378
|
clearTimeout(timeoutId);
|
|
242
379
|
httpStatus = response.status;
|
|
243
380
|
|
|
244
|
-
|
|
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
|
+
|
|
245
401
|
status = "error";
|
|
246
|
-
|
|
247
|
-
errorMessage = errorText;
|
|
402
|
+
errorMessage = `HTTP ${response.status}`;
|
|
248
403
|
throw new Error(
|
|
249
|
-
`Storage API error:
|
|
404
|
+
`Storage API error: Rate limit exceeded after ${this.config.maxRetries} retries`,
|
|
250
405
|
);
|
|
251
406
|
}
|
|
252
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
|
+
|
|
253
414
|
if (response.status === 204) {
|
|
254
415
|
return undefined as T;
|
|
255
416
|
}
|
|
@@ -340,9 +501,7 @@ export class StorageClient {
|
|
|
340
501
|
"Content-Type": "application/octet-stream",
|
|
341
502
|
};
|
|
342
503
|
|
|
343
|
-
|
|
344
|
-
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
345
|
-
}
|
|
504
|
+
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
346
505
|
|
|
347
506
|
const bytesSent =
|
|
348
507
|
data instanceof Buffer || data instanceof Uint8Array ? data.byteLength : undefined;
|
|
@@ -385,11 +544,8 @@ export class StorageClient {
|
|
|
385
544
|
|
|
386
545
|
if (!response.ok) {
|
|
387
546
|
status = "error";
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
throw new Error(
|
|
391
|
-
`Upload part error: ${response.status} ${response.statusText} - ${errorText}`,
|
|
392
|
-
);
|
|
547
|
+
errorMessage = `HTTP ${response.status}`;
|
|
548
|
+
throw new Error(`Upload part error: ${response.status} ${response.statusText}`);
|
|
393
549
|
}
|
|
394
550
|
|
|
395
551
|
return (await response.json()) as UploadPartResult;
|
|
@@ -422,6 +578,7 @@ export class StorageClient {
|
|
|
422
578
|
}
|
|
423
579
|
|
|
424
580
|
async getUploadStatus(uploadId: string): Promise<UploadStatus> {
|
|
581
|
+
validateUUID(uploadId, "uploadId");
|
|
425
582
|
// Use GET since we need the full status object
|
|
426
583
|
return this.request<UploadStatus>("GET", `/storage/v1/uploads/${uploadId}`, {
|
|
427
584
|
operation: "getUploadStatus",
|
|
@@ -430,6 +587,7 @@ export class StorageClient {
|
|
|
430
587
|
}
|
|
431
588
|
|
|
432
589
|
async completeUpload(uploadId: string): Promise<CompleteUploadResult> {
|
|
590
|
+
validateUUID(uploadId, "uploadId");
|
|
433
591
|
return this.request("POST", `/storage/v1/uploads/${uploadId}/complete`, {
|
|
434
592
|
operation: "completeUpload",
|
|
435
593
|
telemetryRoute: "/storage/v1/uploads/:uploadId/complete",
|
|
@@ -510,9 +668,7 @@ export class StorageClient {
|
|
|
510
668
|
headers["Range"] = options.range;
|
|
511
669
|
}
|
|
512
670
|
|
|
513
|
-
|
|
514
|
-
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
515
|
-
}
|
|
671
|
+
headers["Authorization"] = `Bearer ${this.config.apiKey}`;
|
|
516
672
|
|
|
517
673
|
const controller = new AbortController();
|
|
518
674
|
const timeoutId = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
@@ -535,9 +691,8 @@ export class StorageClient {
|
|
|
535
691
|
|
|
536
692
|
if (!response.ok) {
|
|
537
693
|
status = "error";
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
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}`);
|
|
541
696
|
}
|
|
542
697
|
|
|
543
698
|
const contentRange = response.headers.get("Content-Range");
|
|
@@ -612,6 +767,7 @@ export class StorageClient {
|
|
|
612
767
|
* Requires x-tenant-id header.
|
|
613
768
|
*/
|
|
614
769
|
async getDocumentPolicies(documentId: string): Promise<DocumentPolicies> {
|
|
770
|
+
validateUUID(documentId, "documentId");
|
|
615
771
|
return this.request("GET", `/storage/v1/documents/${documentId}/policies`, {
|
|
616
772
|
operation: "getDocumentPolicies",
|
|
617
773
|
telemetryRoute: "/storage/v1/documents/:documentId/policies",
|
|
@@ -629,6 +785,7 @@ export class StorageClient {
|
|
|
629
785
|
documentId: string,
|
|
630
786
|
input: UpdatePoliciesRequest,
|
|
631
787
|
): Promise<DocumentPolicies> {
|
|
788
|
+
validateUUID(documentId, "documentId");
|
|
632
789
|
return this.request("PATCH", `/storage/v1/documents/${documentId}/policies`, {
|
|
633
790
|
body: input,
|
|
634
791
|
operation: "updateDocumentPolicies",
|
|
@@ -651,6 +808,7 @@ export class StorageClient {
|
|
|
651
808
|
readonly tenantId: string;
|
|
652
809
|
readonly legalHolds: readonly string[];
|
|
653
810
|
}> {
|
|
811
|
+
validateUUID(documentId, "documentId");
|
|
654
812
|
return this.request("POST", `/storage/v1/documents/${documentId}/legal-holds`, {
|
|
655
813
|
body: request,
|
|
656
814
|
operation: "applyLegalHold",
|
|
@@ -666,6 +824,7 @@ export class StorageClient {
|
|
|
666
824
|
* Requires x-tenant-id header.
|
|
667
825
|
*/
|
|
668
826
|
async releaseLegalHold(documentId: string, holdId: string): Promise<void> {
|
|
827
|
+
validateUUID(documentId, "documentId");
|
|
669
828
|
return this.request("DELETE", `/storage/v1/documents/${documentId}/legal-holds/${holdId}`, {
|
|
670
829
|
operation: "releaseLegalHold",
|
|
671
830
|
telemetryRoute: "/storage/v1/documents/:documentId/legal-holds/:holdId",
|
|
@@ -691,6 +850,7 @@ export class StorageClient {
|
|
|
691
850
|
readonly transitionAfter?: string | null;
|
|
692
851
|
};
|
|
693
852
|
}> {
|
|
853
|
+
validateUUID(documentId, "documentId");
|
|
694
854
|
return this.request("POST", `/storage/v1/documents/${documentId}/lifecycle/transitions`, {
|
|
695
855
|
body: request,
|
|
696
856
|
operation: "scheduleLifecycleTransition",
|
|
@@ -711,3 +871,4 @@ export class StorageClient {
|
|
|
711
871
|
|
|
712
872
|
export * from "./events.js";
|
|
713
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
|
+
}
|