@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/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>).mockResolvedValue(
86
- jsonResponse(expectedResponse),
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(1);
98
- const call = (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mock.calls.at(0);
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>).mockResolvedValue(
141
- jsonResponse(expectedPayload),
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(1);
149
- const uploadCall = (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mock.calls.at(0);
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>).mockResolvedValue(response);
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>).mockResolvedValue(
240
- jsonResponse(statusResponse),
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>).mockResolvedValue(
265
- new Response("boom", { status: 500, statusText: "error" }),
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>).mockResolvedValue(
303
- jsonResponse(expectedPayload),
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[0] as unknown as [string, RequestInit];
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[1] as unknown as [string, RequestInit];
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[0] as unknown as [string, RequestInit];
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[1] as unknown as [string, RequestInit];
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: 5 GB storage, 2,000 API calls/month). " +
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 only for localhost in development)
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",