@qnsp/tenant-sdk 0.2.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.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { performance } from "node:perf_hooks";
2
2
 
3
+ import { activateSdk, type SdkActivationConfig } from "@qnsp/sdk-activation";
4
+
3
5
  import type {
4
6
  TenantClientTelemetry,
5
7
  TenantClientTelemetryConfig,
@@ -17,7 +19,7 @@ import { validateUUID } from "./validation.js";
17
19
 
18
20
  export interface TenantClientConfig {
19
21
  readonly baseUrl: string;
20
- readonly apiKey?: string;
22
+ readonly apiKey: string;
21
23
  readonly timeoutMs?: number;
22
24
  readonly telemetry?: TenantClientTelemetry | TenantClientTelemetryConfig;
23
25
  readonly maxRetries?: number;
@@ -34,12 +36,102 @@ type InternalTenantClientConfig = {
34
36
 
35
37
  export type TenantStatus = "active" | "suspended" | "pending" | "deleted";
36
38
 
39
+ export type HsmMode = "none" | "supported" | "required";
40
+
37
41
  /**
38
42
  * Crypto policy tier determines which PQC algorithms are allowed.
39
43
  * Maps to tenant_crypto_policies.policy_tier in tenant-service.
40
44
  */
41
45
  export type CryptoPolicyTier = "default" | "strict" | "maximum" | "government";
42
46
 
47
+ /**
48
+ * Crypto policy v1 uses profiles + tiers (evidence-first model).
49
+ */
50
+ export type QnspTier =
51
+ | "TIER0_LEGACY"
52
+ | "TIER1_APPROVED"
53
+ | "TIER2_HIGH_ASSURANCE"
54
+ | "TIER3_DIVERSITY"
55
+ | "TIER4_EXPERIMENTAL";
56
+
57
+ export type TenantType =
58
+ | "FREE_FOREVER"
59
+ | "DEV_STARTER"
60
+ | "DEV_PRO"
61
+ | "DEV_ELITE"
62
+ | "BUSINESS_TEAM"
63
+ | "BUSINESS_ADVANCED"
64
+ | "BUSINESS_ELITE"
65
+ | "ENTERPRISE_STANDARD"
66
+ | "ENTERPRISE_PRO"
67
+ | "ENTERPRISE_ELITE"
68
+ | "PUBLIC_SECTOR";
69
+
70
+ export type CryptoProfileId =
71
+ | "gov-high-assurance"
72
+ | "defense-long-life-data"
73
+ | "financial-hybrid-pqc"
74
+ | "research-eval";
75
+
76
+ export type TenantCryptoPolicyV1Action = "CREATE" | "UPDATE" | "ROLLBACK";
77
+
78
+ export interface CryptoPolicyV1 {
79
+ readonly version: "v1";
80
+ readonly tenantType: TenantType;
81
+ readonly profile: CryptoProfileId;
82
+ readonly enabledTiers: readonly QnspTier[];
83
+ readonly tier0Expiry?: string;
84
+ readonly tier4Acknowledgement?: {
85
+ readonly nonCompliant: true;
86
+ readonly noProductionSecrets: true;
87
+ readonly approvedBy: string;
88
+ readonly approvedAt: string;
89
+ };
90
+ readonly overrides?: {
91
+ readonly allowFalcon?: boolean;
92
+ };
93
+ readonly requirements: {
94
+ readonly fipsAligned: boolean;
95
+ readonly evidenceRequired: boolean;
96
+ readonly cryptoAgilityMetadataRequired: boolean;
97
+ readonly statefulLifecycleGuards: boolean;
98
+ readonly downgradeDetection: boolean;
99
+ };
100
+ }
101
+
102
+ export interface TenantCryptoPolicyV1Record {
103
+ readonly id: string;
104
+ readonly tenantId: string;
105
+ readonly version: "v1";
106
+ readonly policy: CryptoPolicyV1;
107
+ readonly policyHash: string;
108
+ readonly etag: string;
109
+ readonly createdAt: string;
110
+ readonly createdByPrincipal: string;
111
+ readonly createdByIp: string | null;
112
+ readonly updatedAt: string;
113
+ readonly updatedByPrincipal: string;
114
+ readonly updatedByIp: string | null;
115
+ }
116
+
117
+ export interface TenantCryptoPolicyV1HistoryRecord {
118
+ readonly id: string;
119
+ readonly tenantId: string;
120
+ readonly version: "v1";
121
+ readonly policy: CryptoPolicyV1;
122
+ readonly policyHash: string;
123
+ readonly action: TenantCryptoPolicyV1Action;
124
+ readonly reason: string | null;
125
+ readonly changedAt: string;
126
+ readonly changedByPrincipal: string;
127
+ readonly changedByIp: string | null;
128
+ }
129
+
130
+ export interface TenantCryptoPolicyV1HistoryResponse {
131
+ readonly tenantId: string;
132
+ readonly items: readonly TenantCryptoPolicyV1HistoryRecord[];
133
+ }
134
+
43
135
  /**
44
136
  * Tenant crypto policy configuration.
45
137
  */
@@ -80,48 +172,272 @@ export interface TierAlgorithmConfig {
80
172
 
81
173
  /**
82
174
  * Default algorithms per crypto policy tier.
175
+ * These match the definitions in packages/security/src/crypto-policy.ts
176
+ * and determine which algorithms appear in the portal's Generate Key dropdown.
177
+ *
178
+ * default: All supported PQC algorithms (NIST-finalized + candidates via liboqs)
179
+ * strict: NIST-finalized/selected at higher security levels
180
+ * maximum: Highest-security NIST-finalized only
181
+ * government: FIPS-finalized only (no draft standards)
83
182
  */
84
183
  export const CRYPTO_POLICY_ALGORITHMS: Record<CryptoPolicyTier, TierAlgorithmConfig> = {
85
184
  default: {
86
- kemAlgorithms: ["kyber-512", "kyber-768", "kyber-1024"],
87
- signatureAlgorithms: ["dilithium-2", "dilithium-3", "dilithium-5"],
185
+ kemAlgorithms: [
186
+ // FIPS 203 — ML-KEM (NIST finalized)
187
+ "kyber-512",
188
+ "kyber-768",
189
+ "kyber-1024",
190
+ // HQC (NIST selected March 2025)
191
+ "hqc-128",
192
+ "hqc-192",
193
+ "hqc-256",
194
+ // BIKE (NIST Round 4 candidate)
195
+ "bike-l1",
196
+ "bike-l3",
197
+ "bike-l5",
198
+ // Classic McEliece (ISO standard)
199
+ "mceliece-348864",
200
+ "mceliece-460896",
201
+ "mceliece-6688128",
202
+ "mceliece-6960119",
203
+ "mceliece-8192128",
204
+ // FrodoKEM (ISO standard)
205
+ "frodokem-640-aes",
206
+ "frodokem-640-shake",
207
+ "frodokem-976-aes",
208
+ "frodokem-976-shake",
209
+ "frodokem-1344-aes",
210
+ "frodokem-1344-shake",
211
+ // NTRU (lattice-based)
212
+ "ntru-hps-2048-509",
213
+ "ntru-hps-2048-677",
214
+ "ntru-hps-4096-821",
215
+ "ntru-hps-4096-1229",
216
+ "ntru-hrss-701",
217
+ "ntru-hrss-1373",
218
+ // NTRU-Prime
219
+ "sntrup761",
220
+ ],
221
+ signatureAlgorithms: [
222
+ // FIPS 204 — ML-DSA (NIST finalized)
223
+ "dilithium-2",
224
+ "dilithium-3",
225
+ "dilithium-5",
226
+ // FIPS 205 — SLH-DSA (NIST finalized, SHA-2 variants)
227
+ "sphincs-sha2-128f-simple",
228
+ "sphincs-sha2-128s-simple",
229
+ "sphincs-sha2-192f-simple",
230
+ "sphincs-sha2-192s-simple",
231
+ "sphincs-sha2-256f-simple",
232
+ "sphincs-sha2-256s-simple",
233
+ // FIPS 205 — SLH-DSA (NIST finalized, SHAKE variants)
234
+ "sphincs-shake-128f-simple",
235
+ "sphincs-shake-128s-simple",
236
+ "sphincs-shake-192f-simple",
237
+ "sphincs-shake-192s-simple",
238
+ "sphincs-shake-256f-simple",
239
+ "sphincs-shake-256s-simple",
240
+ // FN-DSA / Falcon (FIPS 206 draft)
241
+ "falcon-512",
242
+ "falcon-1024",
243
+ // MAYO (NIST Additional Signatures Round 2)
244
+ "mayo-1",
245
+ "mayo-2",
246
+ "mayo-3",
247
+ "mayo-5",
248
+ // CROSS (NIST Additional Signatures Round 2)
249
+ "cross-rsdp-128-balanced",
250
+ "cross-rsdp-128-fast",
251
+ "cross-rsdp-128-small",
252
+ "cross-rsdp-192-balanced",
253
+ "cross-rsdp-192-fast",
254
+ "cross-rsdp-192-small",
255
+ "cross-rsdp-256-balanced",
256
+ "cross-rsdp-256-fast",
257
+ "cross-rsdp-256-small",
258
+ "cross-rsdpg-128-balanced",
259
+ "cross-rsdpg-128-fast",
260
+ "cross-rsdpg-128-small",
261
+ "cross-rsdpg-192-balanced",
262
+ "cross-rsdpg-192-fast",
263
+ "cross-rsdpg-192-small",
264
+ "cross-rsdpg-256-balanced",
265
+ "cross-rsdpg-256-fast",
266
+ "cross-rsdpg-256-small",
267
+ // UOV (NIST Additional Signatures Round 2)
268
+ "ov-Is",
269
+ "ov-Ip",
270
+ "ov-III",
271
+ "ov-V",
272
+ "ov-Is-pkc",
273
+ "ov-Ip-pkc",
274
+ "ov-III-pkc",
275
+ "ov-V-pkc",
276
+ "ov-Is-pkc-skc",
277
+ "ov-Ip-pkc-skc",
278
+ "ov-III-pkc-skc",
279
+ "ov-V-pkc-skc",
280
+ // SNOVA (NIST Additional Signatures Round 2)
281
+ "snova-24-5-4",
282
+ "snova-24-5-4-shake",
283
+ "snova-24-5-4-esk",
284
+ "snova-24-5-4-shake-esk",
285
+ "snova-25-8-3",
286
+ "snova-37-17-2",
287
+ "snova-37-8-4",
288
+ "snova-24-5-5",
289
+ "snova-56-25-2",
290
+ "snova-49-11-3",
291
+ "snova-60-10-4",
292
+ "snova-29-6-5",
293
+ ],
88
294
  defaultKemAlgorithm: "kyber-768",
89
295
  defaultSignatureAlgorithm: "dilithium-3",
90
296
  },
91
297
  strict: {
92
- kemAlgorithms: ["kyber-768", "kyber-1024"],
93
- signatureAlgorithms: ["dilithium-3", "dilithium-5", "falcon-1024"],
298
+ kemAlgorithms: ["kyber-768", "kyber-1024", "hqc-192", "hqc-256"],
299
+ signatureAlgorithms: [
300
+ "dilithium-3",
301
+ "dilithium-5",
302
+ "falcon-1024",
303
+ "sphincs-sha2-256f-simple",
304
+ "sphincs-sha2-256s-simple",
305
+ "sphincs-shake-256f-simple",
306
+ "sphincs-shake-256s-simple",
307
+ ],
94
308
  defaultKemAlgorithm: "kyber-768",
95
309
  defaultSignatureAlgorithm: "dilithium-3",
96
310
  },
97
311
  maximum: {
98
- kemAlgorithms: ["kyber-1024"],
99
- signatureAlgorithms: ["dilithium-5", "falcon-1024", "sphincs-shake-256f-simple"],
312
+ kemAlgorithms: ["kyber-1024", "hqc-256"],
313
+ signatureAlgorithms: [
314
+ "dilithium-5",
315
+ "falcon-1024",
316
+ "sphincs-sha2-256f-simple",
317
+ "sphincs-shake-256f-simple",
318
+ ],
100
319
  defaultKemAlgorithm: "kyber-1024",
101
320
  defaultSignatureAlgorithm: "dilithium-5",
102
321
  },
103
322
  government: {
104
323
  kemAlgorithms: ["kyber-1024"],
105
- signatureAlgorithms: ["dilithium-5", "sphincs-shake-256f-simple"],
324
+ signatureAlgorithms: ["dilithium-5", "sphincs-sha2-256f-simple", "sphincs-shake-256f-simple"],
106
325
  defaultKemAlgorithm: "kyber-1024",
107
326
  defaultSignatureAlgorithm: "dilithium-5",
108
327
  },
109
328
  };
110
329
 
111
330
  /**
112
- * Mapping from internal algorithm names to NIST standardized names.
331
+ * Mapping from internal algorithm names to NIST/standards display names.
332
+ * Covers all 90 PQC algorithms supported by QNSP.
333
+ * Canonical source: @qnsp/cryptography pqc-standards.ts ALGORITHM_NIST_NAMES
113
334
  */
114
335
  export const ALGORITHM_TO_NIST: Record<string, string> = {
336
+ // FIPS 203 — ML-KEM
115
337
  "kyber-512": "ML-KEM-512",
116
338
  "kyber-768": "ML-KEM-768",
117
339
  "kyber-1024": "ML-KEM-1024",
340
+ // FIPS 204 — ML-DSA
118
341
  "dilithium-2": "ML-DSA-44",
119
342
  "dilithium-3": "ML-DSA-65",
120
343
  "dilithium-5": "ML-DSA-87",
121
- "falcon-512": "FN-DSA-512",
122
- "falcon-1024": "FN-DSA-1024",
344
+ // FIPS 205 — SLH-DSA (SHA-2 variants)
345
+ "sphincs-sha2-128f-simple": "SLH-DSA-SHA2-128f",
346
+ "sphincs-sha2-128s-simple": "SLH-DSA-SHA2-128s",
347
+ "sphincs-sha2-192f-simple": "SLH-DSA-SHA2-192f",
348
+ "sphincs-sha2-192s-simple": "SLH-DSA-SHA2-192s",
349
+ "sphincs-sha2-256f-simple": "SLH-DSA-SHA2-256f",
350
+ "sphincs-sha2-256s-simple": "SLH-DSA-SHA2-256s",
351
+ // FIPS 205 — SLH-DSA (SHAKE variants)
123
352
  "sphincs-shake-128f-simple": "SLH-DSA-SHAKE-128f",
353
+ "sphincs-shake-128s-simple": "SLH-DSA-SHAKE-128s",
354
+ "sphincs-shake-192f-simple": "SLH-DSA-SHAKE-192f",
355
+ "sphincs-shake-192s-simple": "SLH-DSA-SHAKE-192s",
124
356
  "sphincs-shake-256f-simple": "SLH-DSA-SHAKE-256f",
357
+ "sphincs-shake-256s-simple": "SLH-DSA-SHAKE-256s",
358
+ // FN-DSA (FIPS 206 draft)
359
+ "falcon-512": "FN-DSA-512",
360
+ "falcon-1024": "FN-DSA-1024",
361
+ // HQC (NIST selected March 2025)
362
+ "hqc-128": "HQC-128",
363
+ "hqc-192": "HQC-192",
364
+ "hqc-256": "HQC-256",
365
+ // BIKE (NIST Round 4)
366
+ "bike-l1": "BIKE-L1",
367
+ "bike-l3": "BIKE-L3",
368
+ "bike-l5": "BIKE-L5",
369
+ // Classic McEliece (ISO standard)
370
+ "mceliece-348864": "Classic-McEliece-348864",
371
+ "mceliece-460896": "Classic-McEliece-460896",
372
+ "mceliece-6688128": "Classic-McEliece-6688128",
373
+ "mceliece-6960119": "Classic-McEliece-6960119",
374
+ "mceliece-8192128": "Classic-McEliece-8192128",
375
+ // FrodoKEM (ISO standard)
376
+ "frodokem-640-aes": "FrodoKEM-640-AES",
377
+ "frodokem-640-shake": "FrodoKEM-640-SHAKE",
378
+ "frodokem-976-aes": "FrodoKEM-976-AES",
379
+ "frodokem-976-shake": "FrodoKEM-976-SHAKE",
380
+ "frodokem-1344-aes": "FrodoKEM-1344-AES",
381
+ "frodokem-1344-shake": "FrodoKEM-1344-SHAKE",
382
+ // NTRU (lattice-based, re-added in liboqs 0.15)
383
+ "ntru-hps-2048-509": "NTRU-HPS-2048-509",
384
+ "ntru-hps-2048-677": "NTRU-HPS-2048-677",
385
+ "ntru-hps-4096-821": "NTRU-HPS-4096-821",
386
+ "ntru-hps-4096-1229": "NTRU-HPS-4096-1229",
387
+ "ntru-hrss-701": "NTRU-HRSS-701",
388
+ "ntru-hrss-1373": "NTRU-HRSS-1373",
389
+ // NTRU-Prime
390
+ sntrup761: "sntrup761",
391
+ // MAYO (NIST Additional Signatures Round 2)
392
+ "mayo-1": "MAYO-1",
393
+ "mayo-2": "MAYO-2",
394
+ "mayo-3": "MAYO-3",
395
+ "mayo-5": "MAYO-5",
396
+ // CROSS (NIST Additional Signatures Round 2)
397
+ "cross-rsdp-128-balanced": "CROSS-RSDP-128-balanced",
398
+ "cross-rsdp-128-fast": "CROSS-RSDP-128-fast",
399
+ "cross-rsdp-128-small": "CROSS-RSDP-128-small",
400
+ "cross-rsdp-192-balanced": "CROSS-RSDP-192-balanced",
401
+ "cross-rsdp-192-fast": "CROSS-RSDP-192-fast",
402
+ "cross-rsdp-192-small": "CROSS-RSDP-192-small",
403
+ "cross-rsdp-256-balanced": "CROSS-RSDP-256-balanced",
404
+ "cross-rsdp-256-fast": "CROSS-RSDP-256-fast",
405
+ "cross-rsdp-256-small": "CROSS-RSDP-256-small",
406
+ "cross-rsdpg-128-balanced": "CROSS-RSDPG-128-balanced",
407
+ "cross-rsdpg-128-fast": "CROSS-RSDPG-128-fast",
408
+ "cross-rsdpg-128-small": "CROSS-RSDPG-128-small",
409
+ "cross-rsdpg-192-balanced": "CROSS-RSDPG-192-balanced",
410
+ "cross-rsdpg-192-fast": "CROSS-RSDPG-192-fast",
411
+ "cross-rsdpg-192-small": "CROSS-RSDPG-192-small",
412
+ "cross-rsdpg-256-balanced": "CROSS-RSDPG-256-balanced",
413
+ "cross-rsdpg-256-fast": "CROSS-RSDPG-256-fast",
414
+ "cross-rsdpg-256-small": "CROSS-RSDPG-256-small",
415
+ // UOV (NIST Additional Signatures Round 2)
416
+ "ov-Is": "UOV-Is",
417
+ "ov-Ip": "UOV-Ip",
418
+ "ov-III": "UOV-III",
419
+ "ov-V": "UOV-V",
420
+ "ov-Is-pkc": "UOV-Is-pkc",
421
+ "ov-Ip-pkc": "UOV-Ip-pkc",
422
+ "ov-III-pkc": "UOV-III-pkc",
423
+ "ov-V-pkc": "UOV-V-pkc",
424
+ "ov-Is-pkc-skc": "UOV-Is-pkc-skc",
425
+ "ov-Ip-pkc-skc": "UOV-Ip-pkc-skc",
426
+ "ov-III-pkc-skc": "UOV-III-pkc-skc",
427
+ "ov-V-pkc-skc": "UOV-V-pkc-skc",
428
+ // SNOVA (NIST Additional Signatures Round 2, liboqs 0.14+)
429
+ "snova-24-5-4": "SNOVA-24-5-4",
430
+ "snova-24-5-4-shake": "SNOVA-24-5-4-SHAKE",
431
+ "snova-24-5-4-esk": "SNOVA-24-5-4-ESK",
432
+ "snova-24-5-4-shake-esk": "SNOVA-24-5-4-SHAKE-ESK",
433
+ "snova-25-8-3": "SNOVA-25-8-3",
434
+ "snova-37-17-2": "SNOVA-37-17-2",
435
+ "snova-37-8-4": "SNOVA-37-8-4",
436
+ "snova-24-5-5": "SNOVA-24-5-5",
437
+ "snova-56-25-2": "SNOVA-56-25-2",
438
+ "snova-49-11-3": "SNOVA-49-11-3",
439
+ "snova-60-10-4": "SNOVA-60-10-4",
440
+ "snova-29-6-5": "SNOVA-29-6-5",
125
441
  };
126
442
 
127
443
  /**
@@ -174,6 +490,7 @@ export interface Tenant {
174
490
  readonly plan: string;
175
491
  readonly region: string;
176
492
  readonly complianceTags: readonly string[];
493
+ readonly hsmMode: HsmMode;
177
494
  readonly metadata: Record<string, unknown>;
178
495
  readonly security: TenantSecurityEnvelope;
179
496
  readonly domains: readonly TenantDomain[];
@@ -187,6 +504,7 @@ export interface CreateTenantRequest {
187
504
  readonly plan?: string;
188
505
  readonly region?: string;
189
506
  readonly complianceTags?: readonly string[];
507
+ readonly hsmMode?: HsmMode;
190
508
  readonly metadata?: Record<string, unknown>;
191
509
  readonly domains?: readonly {
192
510
  readonly domain: string;
@@ -200,6 +518,7 @@ export interface UpdateTenantRequest {
200
518
  readonly plan?: string;
201
519
  readonly status?: TenantStatus;
202
520
  readonly complianceTags?: readonly string[];
521
+ readonly hsmMode?: HsmMode;
203
522
  readonly metadata?: Record<string, unknown>;
204
523
  readonly security: TenantSecurityEnvelope;
205
524
  readonly signature?: TenantSignature;
@@ -223,17 +542,48 @@ export class TenantClient {
223
542
  private readonly config: InternalTenantClientConfig;
224
543
  private readonly telemetry: TenantClientTelemetry | null;
225
544
  private readonly targetService: string;
545
+ private activationPromise: Promise<void> | null = null;
546
+ private readonly activationConfig: SdkActivationConfig;
547
+
548
+ private async ensureActivated(): Promise<void> {
549
+ if (!this.activationPromise) {
550
+ this.activationPromise = activateSdk(this.activationConfig).then(() => undefined);
551
+ }
552
+ await this.activationPromise;
553
+ }
226
554
 
227
555
  constructor(config: TenantClientConfig) {
556
+ if (!config.apiKey || config.apiKey.trim().length === 0) {
557
+ throw new Error(
558
+ "QNSP Tenant SDK: apiKey is required. " +
559
+ "Get your free API key at https://cloud.qnsp.cuilabs.io/signup — " +
560
+ "no credit card required (FREE tier: 10 GB storage, 50,000 API calls/month). " +
561
+ "Docs: https://docs.qnsp.cuilabs.io/sdk/tenant-sdk",
562
+ );
563
+ }
564
+
228
565
  const baseUrl = config.baseUrl.replace(/\/$/, "");
229
566
 
230
- // Enforce HTTPS in production (allow HTTP only for localhost in development)
567
+ // Enforce HTTPS in production (allow HTTP for localhost in development and
568
+ // for internal service-mesh hostnames — e.g. *.internal — which are on a
569
+ // private VPC network and do not require TLS termination at the transport layer).
231
570
  if (!baseUrl.startsWith("https://")) {
232
571
  const isLocalhost =
233
572
  baseUrl.startsWith("http://localhost") || baseUrl.startsWith("http://127.0.0.1");
573
+ let isInternalService = false;
574
+ try {
575
+ const parsed = new URL(baseUrl);
576
+ isInternalService =
577
+ parsed.protocol === "http:" &&
578
+ (parsed.hostname.endsWith(".internal") ||
579
+ parsed.hostname === "localhost" ||
580
+ parsed.hostname === "127.0.0.1");
581
+ } catch {
582
+ // ignore; invalid URL will be caught later by fetch
583
+ }
234
584
  const isDevelopment =
235
585
  process.env["NODE_ENV"] === "development" || process.env["NODE_ENV"] === "test";
236
- if (!isLocalhost || !isDevelopment) {
586
+ if ((!isLocalhost || !isDevelopment) && !isInternalService) {
237
587
  throw new Error(
238
588
  "baseUrl must use HTTPS in production. HTTP is only allowed for localhost in development.",
239
589
  );
@@ -242,7 +592,7 @@ export class TenantClient {
242
592
 
243
593
  this.config = {
244
594
  baseUrl,
245
- apiKey: config.apiKey ?? "",
595
+ apiKey: config.apiKey,
246
596
  timeoutMs: config.timeoutMs ?? 30_000,
247
597
  maxRetries: config.maxRetries ?? 3,
248
598
  retryDelayMs: config.retryDelayMs ?? 1_000,
@@ -259,6 +609,13 @@ export class TenantClient {
259
609
  } catch {
260
610
  this.targetService = "tenant-service";
261
611
  }
612
+
613
+ this.activationConfig = {
614
+ apiKey: config.apiKey,
615
+ sdkId: "tenant-sdk",
616
+ sdkVersion: "0.3.0",
617
+ platformUrl: config.baseUrl,
618
+ };
262
619
  }
263
620
 
264
621
  private async request<T>(method: string, path: string, options?: RequestOptions): Promise<T> {
@@ -277,9 +634,7 @@ export class TenantClient {
277
634
  ...options?.headers,
278
635
  };
279
636
 
280
- if (this.config.apiKey) {
281
- headers["Authorization"] = `Bearer ${this.config.apiKey}`;
282
- }
637
+ headers["Authorization"] = `Bearer ${this.config.apiKey}`;
283
638
 
284
639
  const controller = new AbortController();
285
640
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeoutMs);
@@ -386,6 +741,7 @@ export class TenantClient {
386
741
  * Requires PQC-signed security envelope and optional signature.
387
742
  */
388
743
  async createTenant(request: CreateTenantRequest): Promise<Tenant> {
744
+ await this.ensureActivated();
389
745
  // Validation is handled by the service, but we validate format here for early feedback
390
746
  return this.request<Tenant>("POST", "/tenant/v1/tenants", {
391
747
  body: {
@@ -394,6 +750,7 @@ export class TenantClient {
394
750
  ...(request.plan !== undefined ? { plan: request.plan } : {}),
395
751
  ...(request.region !== undefined ? { region: request.region } : {}),
396
752
  ...(request.complianceTags !== undefined ? { complianceTags: request.complianceTags } : {}),
753
+ ...(request.hsmMode !== undefined ? { hsmMode: request.hsmMode } : {}),
397
754
  ...(request.metadata !== undefined ? { metadata: request.metadata } : {}),
398
755
  ...(request.domains !== undefined ? { domains: request.domains } : {}),
399
756
  security: request.security,
@@ -409,12 +766,14 @@ export class TenantClient {
409
766
  */
410
767
  async updateTenant(id: string, request: UpdateTenantRequest): Promise<Tenant> {
411
768
  validateUUID(id, "id");
769
+ await this.ensureActivated();
412
770
 
413
771
  return this.request<Tenant>("PATCH", `/tenant/v1/tenants/${id}`, {
414
772
  body: {
415
773
  ...(request.plan !== undefined ? { plan: request.plan } : {}),
416
774
  ...(request.status !== undefined ? { status: request.status } : {}),
417
775
  ...(request.complianceTags !== undefined ? { complianceTags: request.complianceTags } : {}),
776
+ ...(request.hsmMode !== undefined ? { hsmMode: request.hsmMode } : {}),
418
777
  ...(request.metadata !== undefined ? { metadata: request.metadata } : {}),
419
778
  security: request.security,
420
779
  ...(request.signature !== undefined ? { signature: request.signature } : {}),
@@ -429,6 +788,7 @@ export class TenantClient {
429
788
  */
430
789
  async getTenant(id: string): Promise<Tenant> {
431
790
  validateUUID(id, "id");
791
+ await this.ensureActivated();
432
792
 
433
793
  return this.request<Tenant>("GET", `/tenant/v1/tenants/${id}`, {
434
794
  operation: "getTenant",
@@ -443,6 +803,7 @@ export class TenantClient {
443
803
  readonly limit?: number;
444
804
  readonly cursor?: string;
445
805
  }): Promise<ListTenantsResponse> {
806
+ await this.ensureActivated();
446
807
  const params = new URLSearchParams();
447
808
  if (options?.limit !== undefined) {
448
809
  params.set("limit", String(options.limit));
@@ -465,12 +826,53 @@ export class TenantClient {
465
826
  */
466
827
  async getTenantCryptoPolicy(tenantId: string): Promise<TenantCryptoPolicy> {
467
828
  validateUUID(tenantId, "tenantId");
829
+ await this.ensureActivated();
468
830
 
469
831
  return this.request<TenantCryptoPolicy>("GET", `/tenant/v1/tenants/${tenantId}/crypto-policy`, {
470
832
  operation: "getTenantCryptoPolicy",
471
833
  });
472
834
  }
473
835
 
836
+ /**
837
+ * Get the v1 crypto policy for a tenant (profiles + tiers model).
838
+ * If no policy exists, a default policy is created and returned.
839
+ */
840
+ async getTenantCryptoPolicyV1(tenantId: string): Promise<TenantCryptoPolicyV1Record> {
841
+ validateUUID(tenantId, "tenantId");
842
+ await this.ensureActivated();
843
+
844
+ return this.request<TenantCryptoPolicyV1Record>(
845
+ "GET",
846
+ `/tenant/v1/tenants/${tenantId}/crypto-policy-v1`,
847
+ {
848
+ operation: "getTenantCryptoPolicyV1",
849
+ },
850
+ );
851
+ }
852
+
853
+ /**
854
+ * List v1 crypto policy history entries.
855
+ */
856
+ async listTenantCryptoPolicyV1History(
857
+ tenantId: string,
858
+ options?: { readonly limit?: number },
859
+ ): Promise<TenantCryptoPolicyV1HistoryResponse> {
860
+ validateUUID(tenantId, "tenantId");
861
+ await this.ensureActivated();
862
+ const params = new URLSearchParams();
863
+ if (options?.limit !== undefined) {
864
+ params.set("limit", String(options.limit));
865
+ }
866
+ const query = params.toString();
867
+ const path = query
868
+ ? `/tenant/v1/tenants/${tenantId}/crypto-policy-v1/history?${query}`
869
+ : `/tenant/v1/tenants/${tenantId}/crypto-policy-v1/history`;
870
+
871
+ return this.request<TenantCryptoPolicyV1HistoryResponse>("GET", path, {
872
+ operation: "listTenantCryptoPolicyV1History",
873
+ });
874
+ }
875
+
474
876
  /**
475
877
  * Create or update the crypto policy for a tenant.
476
878
  * Sets the policy tier and optional custom algorithm restrictions.
@@ -480,6 +882,7 @@ export class TenantClient {
480
882
  policy: TenantCryptoPolicyInput,
481
883
  ): Promise<TenantCryptoPolicy> {
482
884
  validateUUID(tenantId, "tenantId");
885
+ await this.ensureActivated();
483
886
 
484
887
  return this.request<TenantCryptoPolicy>("PUT", `/tenant/v1/tenants/${tenantId}/crypto-policy`, {
485
888
  body: {
@@ -499,11 +902,150 @@ export class TenantClient {
499
902
  });
500
903
  }
501
904
 
905
+ /**
906
+ * Update the v1 crypto policy for a tenant (requires If-Match with current ETag).
907
+ */
908
+ async updateTenantCryptoPolicyV1(
909
+ tenantId: string,
910
+ policy: CryptoPolicyV1,
911
+ etag: string,
912
+ ): Promise<TenantCryptoPolicyV1Record> {
913
+ validateUUID(tenantId, "tenantId");
914
+ await this.ensureActivated();
915
+ if (!etag) {
916
+ throw new Error("etag is required for updateTenantCryptoPolicyV1");
917
+ }
918
+
919
+ return this.request<TenantCryptoPolicyV1Record>(
920
+ "PUT",
921
+ `/tenant/v1/tenants/${tenantId}/crypto-policy-v1`,
922
+ {
923
+ body: { policy },
924
+ headers: {
925
+ "If-Match": etag,
926
+ },
927
+ operation: "updateTenantCryptoPolicyV1",
928
+ },
929
+ );
930
+ }
931
+
932
+ /**
933
+ * Enable Tier0 legacy algorithms (time-bounded) for a tenant.
934
+ */
935
+ async enableTier0Legacy(
936
+ tenantId: string,
937
+ input: { readonly expiry: string },
938
+ etag: string,
939
+ ): Promise<TenantCryptoPolicyV1Record> {
940
+ validateUUID(tenantId, "tenantId");
941
+ await this.ensureActivated();
942
+ if (!etag) {
943
+ throw new Error("etag is required for enableTier0Legacy");
944
+ }
945
+
946
+ return this.request<TenantCryptoPolicyV1Record>(
947
+ "POST",
948
+ `/tenant/v1/tenants/${tenantId}/crypto-policy-v1/tier0/enable`,
949
+ {
950
+ body: { expiry: input.expiry },
951
+ headers: {
952
+ "If-Match": etag,
953
+ },
954
+ operation: "enableTier0Legacy",
955
+ },
956
+ );
957
+ }
958
+
959
+ /**
960
+ * Disable Tier0 legacy algorithms for a tenant.
961
+ */
962
+ async disableTier0Legacy(tenantId: string, etag: string): Promise<TenantCryptoPolicyV1Record> {
963
+ validateUUID(tenantId, "tenantId");
964
+ await this.ensureActivated();
965
+ if (!etag) {
966
+ throw new Error("etag is required for disableTier0Legacy");
967
+ }
968
+
969
+ return this.request<TenantCryptoPolicyV1Record>(
970
+ "POST",
971
+ `/tenant/v1/tenants/${tenantId}/crypto-policy-v1/tier0/disable`,
972
+ {
973
+ body: {},
974
+ headers: {
975
+ "If-Match": etag,
976
+ },
977
+ operation: "disableTier0Legacy",
978
+ },
979
+ );
980
+ }
981
+
982
+ /**
983
+ * Enable Tier4 experimental algorithms with acknowledgement.
984
+ */
985
+ async enableTier4Experimental(
986
+ tenantId: string,
987
+ input: { readonly approvedBy: string },
988
+ etag: string,
989
+ ): Promise<TenantCryptoPolicyV1Record> {
990
+ validateUUID(tenantId, "tenantId");
991
+ await this.ensureActivated();
992
+ if (!etag) {
993
+ throw new Error("etag is required for enableTier4Experimental");
994
+ }
995
+
996
+ return this.request<TenantCryptoPolicyV1Record>(
997
+ "POST",
998
+ `/tenant/v1/tenants/${tenantId}/crypto-policy-v1/tier4/enable`,
999
+ {
1000
+ body: { approvedBy: input.approvedBy },
1001
+ headers: {
1002
+ "If-Match": etag,
1003
+ },
1004
+ operation: "enableTier4Experimental",
1005
+ },
1006
+ );
1007
+ }
1008
+
1009
+ /**
1010
+ * Roll back the v1 crypto policy to a previous history record or policy hash.
1011
+ */
1012
+ async rollbackTenantCryptoPolicyV1(
1013
+ tenantId: string,
1014
+ input: { readonly historyId?: string; readonly policyHash?: string },
1015
+ etag: string,
1016
+ ): Promise<TenantCryptoPolicyV1Record> {
1017
+ validateUUID(tenantId, "tenantId");
1018
+ await this.ensureActivated();
1019
+ if (!etag) {
1020
+ throw new Error("etag is required for rollbackTenantCryptoPolicyV1");
1021
+ }
1022
+ if (!input.historyId && !input.policyHash) {
1023
+ throw new Error("historyId or policyHash is required for rollbackTenantCryptoPolicyV1");
1024
+ }
1025
+
1026
+ return this.request<TenantCryptoPolicyV1Record>(
1027
+ "POST",
1028
+ `/tenant/v1/tenants/${tenantId}/crypto-policy-v1/rollback`,
1029
+ {
1030
+ body: {
1031
+ ...(input.historyId ? { historyId: input.historyId } : {}),
1032
+ ...(input.policyHash ? { policyHash: input.policyHash } : {}),
1033
+ },
1034
+ headers: {
1035
+ "If-Match": etag,
1036
+ },
1037
+ operation: "rollbackTenantCryptoPolicyV1",
1038
+ },
1039
+ );
1040
+ }
1041
+
502
1042
  /**
503
1043
  * Get the allowed KEM algorithms for a tenant based on their crypto policy.
504
1044
  * Convenience method that fetches the policy and returns the allowed algorithms.
505
1045
  */
506
1046
  async getAllowedKemAlgorithms(tenantId: string): Promise<readonly string[]> {
1047
+ validateUUID(tenantId, "tenantId");
1048
+ await this.ensureActivated();
507
1049
  const policy = await this.getTenantCryptoPolicy(tenantId);
508
1050
  if (policy.customAllowedKemAlgorithms && policy.customAllowedKemAlgorithms.length > 0) {
509
1051
  return policy.customAllowedKemAlgorithms;
@@ -516,6 +1058,8 @@ export class TenantClient {
516
1058
  * Convenience method that fetches the policy and returns the allowed algorithms.
517
1059
  */
518
1060
  async getAllowedSignatureAlgorithms(tenantId: string): Promise<readonly string[]> {
1061
+ validateUUID(tenantId, "tenantId");
1062
+ await this.ensureActivated();
519
1063
  const policy = await this.getTenantCryptoPolicy(tenantId);
520
1064
  if (
521
1065
  policy.customAllowedSignatureAlgorithms &&
@@ -530,6 +1074,8 @@ export class TenantClient {
530
1074
  * Get the default KEM algorithm for a tenant based on their crypto policy tier.
531
1075
  */
532
1076
  async getDefaultKemAlgorithm(tenantId: string): Promise<string> {
1077
+ validateUUID(tenantId, "tenantId");
1078
+ await this.ensureActivated();
533
1079
  const policy = await this.getTenantCryptoPolicy(tenantId);
534
1080
  return CRYPTO_POLICY_ALGORITHMS[policy.policyTier].defaultKemAlgorithm;
535
1081
  }
@@ -538,6 +1084,8 @@ export class TenantClient {
538
1084
  * Get the default signature algorithm for a tenant based on their crypto policy tier.
539
1085
  */
540
1086
  async getDefaultSignatureAlgorithm(tenantId: string): Promise<string> {
1087
+ validateUUID(tenantId, "tenantId");
1088
+ await this.ensureActivated();
541
1089
  const policy = await this.getTenantCryptoPolicy(tenantId);
542
1090
  return CRYPTO_POLICY_ALGORITHMS[policy.policyTier].defaultSignatureAlgorithm;
543
1091
  }