@qnsp/storage-sdk 0.3.0 → 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/LICENSE +21 -7
- package/README.md +22 -72
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -3
- package/dist/index.js.map +1 -1
- package/package.json +59 -39
- package/src/index.test.ts +50 -24
- package/src/index.ts +45 -3
- package/tsconfig.tsbuildinfo +1 -1
package/src/index.test.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { clearActivationCache } from "@qnsp/sdk-activation";
|
|
1
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
3
|
import { StorageClient, StorageEventsClient } from "./index.js";
|
|
3
4
|
|
|
@@ -10,6 +11,24 @@ const mockUploadId2 = "22222222-2222-4222-a222-222222222222";
|
|
|
10
11
|
const mockUploadIdErr = "33333333-3333-4333-a333-333333333333";
|
|
11
12
|
const mockDocumentId = "44444444-4444-4444-a444-444444444444";
|
|
12
13
|
|
|
14
|
+
const MOCK_ACTIVATION_RESPONSE = {
|
|
15
|
+
activated: true,
|
|
16
|
+
tenantId: "a1b2c3d4-e5f6-4789-8abc-def012345678",
|
|
17
|
+
tier: "dev-pro",
|
|
18
|
+
activationToken: "tok_test",
|
|
19
|
+
expiresInSeconds: 3600,
|
|
20
|
+
activatedAt: new Date().toISOString(),
|
|
21
|
+
limits: {
|
|
22
|
+
storageGB: 50,
|
|
23
|
+
apiCalls: 100_000,
|
|
24
|
+
enclavesEnabled: false,
|
|
25
|
+
aiTrainingEnabled: false,
|
|
26
|
+
aiInferenceEnabled: true,
|
|
27
|
+
sseEnabled: true,
|
|
28
|
+
vaultEnabled: true,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
13
32
|
function jsonResponse(payload: unknown, init?: ResponseInit): Response {
|
|
14
33
|
return new Response(JSON.stringify(payload), {
|
|
15
34
|
status: 200,
|
|
@@ -49,6 +68,7 @@ async function collectStream(stream: ReadableStream<Uint8Array>): Promise<Uint8A
|
|
|
49
68
|
|
|
50
69
|
describe("StorageClient", () => {
|
|
51
70
|
beforeEach(() => {
|
|
71
|
+
clearActivationCache();
|
|
52
72
|
vi.stubGlobal("fetch", vi.fn());
|
|
53
73
|
});
|
|
54
74
|
|
|
@@ -82,9 +102,9 @@ describe("StorageClient", () => {
|
|
|
82
102
|
},
|
|
83
103
|
};
|
|
84
104
|
|
|
85
|
-
(globalThis.fetch as unknown as ReturnType<typeof vi.fn>)
|
|
86
|
-
jsonResponse(
|
|
87
|
-
|
|
105
|
+
(globalThis.fetch as unknown as ReturnType<typeof vi.fn>)
|
|
106
|
+
.mockResolvedValueOnce(jsonResponse(MOCK_ACTIVATION_RESPONSE))
|
|
107
|
+
.mockResolvedValueOnce(jsonResponse(expectedResponse));
|
|
88
108
|
|
|
89
109
|
const result = await client.initiateUpload({
|
|
90
110
|
name: "contract.pdf",
|
|
@@ -94,8 +114,8 @@ describe("StorageClient", () => {
|
|
|
94
114
|
});
|
|
95
115
|
|
|
96
116
|
expect(result).toEqual(expectedResponse);
|
|
97
|
-
expect(globalThis.fetch).toHaveBeenCalledTimes(
|
|
98
|
-
const call = (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mock.calls.at(
|
|
117
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(2);
|
|
118
|
+
const call = (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mock.calls.at(1);
|
|
99
119
|
expect(call).toBeDefined();
|
|
100
120
|
if (!call) throw new Error("fetch not invoked");
|
|
101
121
|
const [url, init] = call;
|
|
@@ -137,16 +157,16 @@ describe("StorageClient", () => {
|
|
|
137
157
|
resumeToken: null,
|
|
138
158
|
};
|
|
139
159
|
|
|
140
|
-
(globalThis.fetch as unknown as ReturnType<typeof vi.fn>)
|
|
141
|
-
jsonResponse(
|
|
142
|
-
|
|
160
|
+
(globalThis.fetch as unknown as ReturnType<typeof vi.fn>)
|
|
161
|
+
.mockResolvedValueOnce(jsonResponse(MOCK_ACTIVATION_RESPONSE))
|
|
162
|
+
.mockResolvedValueOnce(jsonResponse(expectedPayload));
|
|
143
163
|
|
|
144
164
|
const source = Buffer.from([0xde, 0xad, 0xbe]);
|
|
145
165
|
|
|
146
166
|
const result = await client.uploadPart(mockUploadId, 1, source);
|
|
147
167
|
expect(result).toEqual(expectedPayload);
|
|
148
|
-
expect(globalThis.fetch).toHaveBeenCalledTimes(
|
|
149
|
-
const uploadCall = (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mock.calls.at(
|
|
168
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(2);
|
|
169
|
+
const uploadCall = (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mock.calls.at(1);
|
|
150
170
|
expect(uploadCall).toBeDefined();
|
|
151
171
|
if (!uploadCall) throw new Error("fetch not invoked");
|
|
152
172
|
const [, init] = uploadCall;
|
|
@@ -184,7 +204,9 @@ describe("StorageClient", () => {
|
|
|
184
204
|
},
|
|
185
205
|
});
|
|
186
206
|
|
|
187
|
-
(globalThis.fetch as unknown as ReturnType<typeof vi.fn>)
|
|
207
|
+
(globalThis.fetch as unknown as ReturnType<typeof vi.fn>)
|
|
208
|
+
.mockResolvedValueOnce(jsonResponse(MOCK_ACTIVATION_RESPONSE))
|
|
209
|
+
.mockResolvedValueOnce(response);
|
|
188
210
|
|
|
189
211
|
const result = await client.downloadStream(mockDocumentId, 2, {
|
|
190
212
|
range: "bytes=0-2",
|
|
@@ -236,9 +258,9 @@ describe("StorageClient", () => {
|
|
|
236
258
|
lastPartNumber: 1,
|
|
237
259
|
};
|
|
238
260
|
|
|
239
|
-
(globalThis.fetch as unknown as ReturnType<typeof vi.fn>)
|
|
240
|
-
jsonResponse(
|
|
241
|
-
|
|
261
|
+
(globalThis.fetch as unknown as ReturnType<typeof vi.fn>)
|
|
262
|
+
.mockResolvedValueOnce(jsonResponse(MOCK_ACTIVATION_RESPONSE))
|
|
263
|
+
.mockResolvedValueOnce(jsonResponse(statusResponse));
|
|
242
264
|
|
|
243
265
|
await client.getUploadStatus(mockUploadId2);
|
|
244
266
|
|
|
@@ -261,9 +283,9 @@ describe("StorageClient", () => {
|
|
|
261
283
|
telemetry,
|
|
262
284
|
});
|
|
263
285
|
|
|
264
|
-
(globalThis.fetch as unknown as ReturnType<typeof vi.fn>)
|
|
265
|
-
|
|
266
|
-
|
|
286
|
+
(globalThis.fetch as unknown as ReturnType<typeof vi.fn>)
|
|
287
|
+
.mockResolvedValueOnce(jsonResponse(MOCK_ACTIVATION_RESPONSE))
|
|
288
|
+
.mockResolvedValueOnce(new Response("boom", { status: 500, statusText: "error" }));
|
|
267
289
|
|
|
268
290
|
await expect(client.completeUpload(mockUploadIdErr)).rejects.toThrow(/Storage API error/);
|
|
269
291
|
|
|
@@ -299,9 +321,9 @@ describe("StorageClient", () => {
|
|
|
299
321
|
resumeToken: null,
|
|
300
322
|
};
|
|
301
323
|
|
|
302
|
-
(globalThis.fetch as unknown as ReturnType<typeof vi.fn>)
|
|
303
|
-
jsonResponse(
|
|
304
|
-
|
|
324
|
+
(globalThis.fetch as unknown as ReturnType<typeof vi.fn>)
|
|
325
|
+
.mockResolvedValueOnce(jsonResponse(MOCK_ACTIVATION_RESPONSE))
|
|
326
|
+
.mockResolvedValueOnce(jsonResponse(expectedPayload));
|
|
305
327
|
|
|
306
328
|
await client.uploadPart(mockUploadId, 1, Buffer.from([1, 2, 3]));
|
|
307
329
|
|
|
@@ -322,6 +344,8 @@ describe("StorageClient", () => {
|
|
|
322
344
|
});
|
|
323
345
|
|
|
324
346
|
(globalThis.fetch as unknown as ReturnType<typeof vi.fn>)
|
|
347
|
+
// activation
|
|
348
|
+
.mockResolvedValueOnce(jsonResponse(MOCK_ACTIVATION_RESPONSE))
|
|
325
349
|
// PATCH response
|
|
326
350
|
.mockResolvedValueOnce(
|
|
327
351
|
jsonResponse({
|
|
@@ -361,7 +385,7 @@ describe("StorageClient", () => {
|
|
|
361
385
|
});
|
|
362
386
|
expect(updated.documentId).toBe(mockDocumentId);
|
|
363
387
|
const patchCall = (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mock
|
|
364
|
-
.calls[
|
|
388
|
+
.calls[1] as unknown as [string, RequestInit];
|
|
365
389
|
expect(patchCall[0]).toBe(
|
|
366
390
|
`https://storage.qnsp.example/storage/v1/documents/${mockDocumentId}/policies`,
|
|
367
391
|
);
|
|
@@ -374,7 +398,7 @@ describe("StorageClient", () => {
|
|
|
374
398
|
const policies = await client.getDocumentPolicies(mockDocumentId);
|
|
375
399
|
expect(policies.tenantId).toBe(mockTenantId);
|
|
376
400
|
const getCall = (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mock
|
|
377
|
-
.calls[
|
|
401
|
+
.calls[2] as unknown as [string, RequestInit];
|
|
378
402
|
expect(getCall[0]).toBe(
|
|
379
403
|
`https://storage.qnsp.example/storage/v1/documents/${mockDocumentId}/policies`,
|
|
380
404
|
);
|
|
@@ -393,6 +417,8 @@ describe("StorageClient", () => {
|
|
|
393
417
|
});
|
|
394
418
|
|
|
395
419
|
(globalThis.fetch as unknown as ReturnType<typeof vi.fn>)
|
|
420
|
+
// activation
|
|
421
|
+
.mockResolvedValueOnce(jsonResponse(MOCK_ACTIVATION_RESPONSE))
|
|
396
422
|
// apply
|
|
397
423
|
.mockResolvedValueOnce(
|
|
398
424
|
jsonResponse({
|
|
@@ -407,7 +433,7 @@ describe("StorageClient", () => {
|
|
|
407
433
|
const applied = await client.applyLegalHold(mockDocumentId, { holdId: "hold-9" });
|
|
408
434
|
expect(applied.legalHolds).toContain("hold-9");
|
|
409
435
|
const postCall = (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mock
|
|
410
|
-
.calls[
|
|
436
|
+
.calls[1] as unknown as [string, RequestInit];
|
|
411
437
|
expect(postCall[0]).toBe(
|
|
412
438
|
`https://storage.qnsp.example/storage/v1/documents/${mockDocumentId}/legal-holds`,
|
|
413
439
|
);
|
|
@@ -418,7 +444,7 @@ describe("StorageClient", () => {
|
|
|
418
444
|
|
|
419
445
|
await client.releaseLegalHold(mockDocumentId, "hold-9");
|
|
420
446
|
const delCall = (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mock
|
|
421
|
-
.calls[
|
|
447
|
+
.calls[2] as unknown as [string, RequestInit];
|
|
422
448
|
expect(delCall[0]).toBe(
|
|
423
449
|
`https://storage.qnsp.example/storage/v1/documents/${mockDocumentId}/legal-holds/hold-9`,
|
|
424
450
|
);
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { activateSdk, type SdkActivationConfig } from "@qnsp/sdk-activation";
|
|
2
|
+
|
|
1
3
|
import type {
|
|
2
4
|
StorageClientTelemetry,
|
|
3
5
|
StorageClientTelemetryConfig,
|
|
@@ -286,26 +288,48 @@ export class StorageClient {
|
|
|
286
288
|
private readonly config: InternalStorageClientConfig;
|
|
287
289
|
private readonly telemetry: StorageClientTelemetry | null;
|
|
288
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
|
+
}
|
|
289
300
|
|
|
290
301
|
constructor(config: StorageClientConfig) {
|
|
291
302
|
if (!config.apiKey || config.apiKey.trim().length === 0) {
|
|
292
303
|
throw new Error(
|
|
293
304
|
"QNSP Storage SDK: apiKey is required. " +
|
|
294
305
|
"Get your free API key at https://cloud.qnsp.cuilabs.io/signup — " +
|
|
295
|
-
"no credit card required (FREE tier:
|
|
306
|
+
"no credit card required (FREE tier: 10 GB storage, 50,000 API calls/month). " +
|
|
296
307
|
"Docs: https://docs.qnsp.cuilabs.io/sdk/storage-sdk",
|
|
297
308
|
);
|
|
298
309
|
}
|
|
299
310
|
|
|
300
311
|
const baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
301
312
|
|
|
302
|
-
// Enforce HTTPS in production (allow HTTP
|
|
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).
|
|
303
316
|
if (!baseUrl.startsWith("https://")) {
|
|
304
317
|
const isLocalhost =
|
|
305
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
|
+
}
|
|
306
330
|
const isDevelopment =
|
|
307
331
|
process.env["NODE_ENV"] === "development" || process.env["NODE_ENV"] === "test";
|
|
308
|
-
if (!isLocalhost || !isDevelopment) {
|
|
332
|
+
if ((!isLocalhost || !isDevelopment) && !isInternalService) {
|
|
309
333
|
throw new Error(
|
|
310
334
|
"baseUrl must use HTTPS in production. HTTP is only allowed for localhost in development.",
|
|
311
335
|
);
|
|
@@ -332,6 +356,13 @@ export class StorageClient {
|
|
|
332
356
|
} catch {
|
|
333
357
|
this.targetService = "storage-service";
|
|
334
358
|
}
|
|
359
|
+
|
|
360
|
+
this.activationConfig = {
|
|
361
|
+
apiKey: config.apiKey,
|
|
362
|
+
sdkId: "storage-sdk",
|
|
363
|
+
sdkVersion: "0.3.0",
|
|
364
|
+
platformUrl: config.baseUrl,
|
|
365
|
+
};
|
|
335
366
|
}
|
|
336
367
|
|
|
337
368
|
private async request<T>(method: string, path: string, options?: RequestOptions): Promise<T> {
|
|
@@ -454,6 +485,7 @@ export class StorageClient {
|
|
|
454
485
|
readonly resumeToken: string | null;
|
|
455
486
|
readonly pqc: PqcMetadata;
|
|
456
487
|
}> {
|
|
488
|
+
await this.ensureActivated();
|
|
457
489
|
const result = await this.request<{
|
|
458
490
|
uploadId: string;
|
|
459
491
|
documentId: string;
|
|
@@ -496,6 +528,7 @@ export class StorageClient {
|
|
|
496
528
|
partId: number,
|
|
497
529
|
data: ReadableStream<Uint8Array> | Buffer | Uint8Array,
|
|
498
530
|
): Promise<UploadPartResult> {
|
|
531
|
+
await this.ensureActivated();
|
|
499
532
|
const url = `${this.config.baseUrl}/storage/v1/uploads/${uploadId}/parts/${partId}`;
|
|
500
533
|
const headers: Record<string, string> = {
|
|
501
534
|
"Content-Type": "application/octet-stream",
|
|
@@ -579,6 +612,7 @@ export class StorageClient {
|
|
|
579
612
|
|
|
580
613
|
async getUploadStatus(uploadId: string): Promise<UploadStatus> {
|
|
581
614
|
validateUUID(uploadId, "uploadId");
|
|
615
|
+
await this.ensureActivated();
|
|
582
616
|
// Use GET since we need the full status object
|
|
583
617
|
return this.request<UploadStatus>("GET", `/storage/v1/uploads/${uploadId}`, {
|
|
584
618
|
operation: "getUploadStatus",
|
|
@@ -588,6 +622,7 @@ export class StorageClient {
|
|
|
588
622
|
|
|
589
623
|
async completeUpload(uploadId: string): Promise<CompleteUploadResult> {
|
|
590
624
|
validateUUID(uploadId, "uploadId");
|
|
625
|
+
await this.ensureActivated();
|
|
591
626
|
return this.request("POST", `/storage/v1/uploads/${uploadId}/complete`, {
|
|
592
627
|
operation: "completeUpload",
|
|
593
628
|
telemetryRoute: "/storage/v1/uploads/:uploadId/complete",
|
|
@@ -603,6 +638,7 @@ export class StorageClient {
|
|
|
603
638
|
readonly signature?: string | null;
|
|
604
639
|
},
|
|
605
640
|
): Promise<DownloadDescriptor> {
|
|
641
|
+
await this.ensureActivated();
|
|
606
642
|
const params = new URLSearchParams({
|
|
607
643
|
tenantId: this.config.tenantId,
|
|
608
644
|
});
|
|
@@ -644,6 +680,7 @@ export class StorageClient {
|
|
|
644
680
|
readonly range?: { readonly start: number; readonly end: number };
|
|
645
681
|
readonly checksumSha3: string;
|
|
646
682
|
}> {
|
|
683
|
+
await this.ensureActivated();
|
|
647
684
|
const params = new URLSearchParams({
|
|
648
685
|
tenantId: this.config.tenantId,
|
|
649
686
|
});
|
|
@@ -768,6 +805,7 @@ export class StorageClient {
|
|
|
768
805
|
*/
|
|
769
806
|
async getDocumentPolicies(documentId: string): Promise<DocumentPolicies> {
|
|
770
807
|
validateUUID(documentId, "documentId");
|
|
808
|
+
await this.ensureActivated();
|
|
771
809
|
return this.request("GET", `/storage/v1/documents/${documentId}/policies`, {
|
|
772
810
|
operation: "getDocumentPolicies",
|
|
773
811
|
telemetryRoute: "/storage/v1/documents/:documentId/policies",
|
|
@@ -786,6 +824,7 @@ export class StorageClient {
|
|
|
786
824
|
input: UpdatePoliciesRequest,
|
|
787
825
|
): Promise<DocumentPolicies> {
|
|
788
826
|
validateUUID(documentId, "documentId");
|
|
827
|
+
await this.ensureActivated();
|
|
789
828
|
return this.request("PATCH", `/storage/v1/documents/${documentId}/policies`, {
|
|
790
829
|
body: input,
|
|
791
830
|
operation: "updateDocumentPolicies",
|
|
@@ -809,6 +848,7 @@ export class StorageClient {
|
|
|
809
848
|
readonly legalHolds: readonly string[];
|
|
810
849
|
}> {
|
|
811
850
|
validateUUID(documentId, "documentId");
|
|
851
|
+
await this.ensureActivated();
|
|
812
852
|
return this.request("POST", `/storage/v1/documents/${documentId}/legal-holds`, {
|
|
813
853
|
body: request,
|
|
814
854
|
operation: "applyLegalHold",
|
|
@@ -825,6 +865,7 @@ export class StorageClient {
|
|
|
825
865
|
*/
|
|
826
866
|
async releaseLegalHold(documentId: string, holdId: string): Promise<void> {
|
|
827
867
|
validateUUID(documentId, "documentId");
|
|
868
|
+
await this.ensureActivated();
|
|
828
869
|
return this.request("DELETE", `/storage/v1/documents/${documentId}/legal-holds/${holdId}`, {
|
|
829
870
|
operation: "releaseLegalHold",
|
|
830
871
|
telemetryRoute: "/storage/v1/documents/:documentId/legal-holds/:holdId",
|
|
@@ -851,6 +892,7 @@ export class StorageClient {
|
|
|
851
892
|
};
|
|
852
893
|
}> {
|
|
853
894
|
validateUUID(documentId, "documentId");
|
|
895
|
+
await this.ensureActivated();
|
|
854
896
|
return this.request("POST", `/storage/v1/documents/${documentId}/lifecycle/transitions`, {
|
|
855
897
|
body: request,
|
|
856
898
|
operation: "scheduleLifecycleTransition",
|