@qnsp/sdk-activation 0.1.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/dist/types.js ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * SDK Activation Types
3
+ *
4
+ * Shared contract between SDK clients and the billing-service activation endpoint.
5
+ * Browser-compatible — no node: imports.
6
+ *
7
+ * @module
8
+ */
9
+ import { z } from "zod";
10
+ /**
11
+ * SDK identifier sent during activation to identify which package is being used.
12
+ */
13
+ export const SdkIdentifierSchema = z.enum([
14
+ "browser-sdk",
15
+ "vault-sdk",
16
+ "storage-sdk",
17
+ "search-sdk",
18
+ "kms-client",
19
+ "ai-sdk",
20
+ "tenant-sdk",
21
+ "billing-sdk",
22
+ "auth-sdk",
23
+ "audit-sdk",
24
+ "access-control-sdk",
25
+ "crypto-inventory-sdk",
26
+ ]);
27
+ /**
28
+ * Request body for POST /billing/v1/sdk/activate
29
+ */
30
+ export const SdkActivationRequestSchema = z.object({
31
+ /** The SDK package requesting activation */
32
+ sdkId: SdkIdentifierSchema,
33
+ /** SDK version string (e.g., "0.1.0") */
34
+ sdkVersion: z.string().min(1).max(32),
35
+ /** Runtime environment: "browser", "node", "edge" */
36
+ runtime: z.enum(["browser", "node", "edge"]),
37
+ });
38
+ /**
39
+ * Tier limits returned in the activation response.
40
+ * Subset of full TierLimits — only what SDKs need for client-side enforcement.
41
+ */
42
+ export const SdkActivationLimitsSchema = z.object({
43
+ storageGB: z.number(),
44
+ apiCalls: z.number(),
45
+ enclavesEnabled: z.boolean(),
46
+ aiTrainingEnabled: z.boolean(),
47
+ aiInferenceEnabled: z.boolean(),
48
+ sseEnabled: z.boolean(),
49
+ vaultEnabled: z.boolean(),
50
+ });
51
+ /**
52
+ * Successful activation response from billing-service.
53
+ */
54
+ export const SdkActivationResponseSchema = z.object({
55
+ /** Whether activation succeeded */
56
+ activated: z.literal(true),
57
+ /** Tenant ID associated with the API key */
58
+ tenantId: z.string().uuid(),
59
+ /** Current pricing tier */
60
+ tier: z.string().min(1),
61
+ /** Tier limits for client-side enforcement */
62
+ limits: SdkActivationLimitsSchema,
63
+ /** Activation token — opaque string, valid for `expiresInSeconds` */
64
+ activationToken: z.string().min(1),
65
+ /** Token validity in seconds (default: 3600 = 1 hour) */
66
+ expiresInSeconds: z.number().int().positive(),
67
+ /** ISO 8601 timestamp of activation */
68
+ activatedAt: z.string().datetime(),
69
+ });
70
+ /**
71
+ * Error response from the activation endpoint.
72
+ */
73
+ export const SdkActivationErrorSchema = z.object({
74
+ activated: z.literal(false),
75
+ error: z.string(),
76
+ code: z.enum([
77
+ "INVALID_API_KEY",
78
+ "ACCOUNT_SUSPENDED",
79
+ "TIER_INSUFFICIENT",
80
+ "RATE_LIMITED",
81
+ "SERVICE_UNAVAILABLE",
82
+ ]),
83
+ });
84
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;GAEG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,IAAI,CAAC;IACzC,aAAa;IACb,WAAW;IACX,aAAa;IACb,YAAY;IACZ,YAAY;IACZ,QAAQ;IACR,YAAY;IACZ,aAAa;IACb,UAAU;IACV,WAAW;IACX,oBAAoB;IACpB,sBAAsB;CACtB,CAAC,CAAC;AAIH;;GAEG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC;IAClD,4CAA4C;IAC5C,KAAK,EAAE,mBAAmB;IAC1B,yCAAyC;IACzC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;IACrC,qDAAqD;IACrD,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;CAC5C,CAAC,CAAC;AAIH;;;GAGG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,MAAM,CAAC;IACjD,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE;IACrB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB,eAAe,EAAE,CAAC,CAAC,OAAO,EAAE;IAC5B,iBAAiB,EAAE,CAAC,CAAC,OAAO,EAAE;IAC9B,kBAAkB,EAAE,CAAC,CAAC,OAAO,EAAE;IAC/B,UAAU,EAAE,CAAC,CAAC,OAAO,EAAE;IACvB,YAAY,EAAE,CAAC,CAAC,OAAO,EAAE;CACzB,CAAC,CAAC;AAIH;;GAEG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAAC;IACnD,mCAAmC;IACnC,SAAS,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;IAC1B,4CAA4C;IAC5C,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;IAC3B,2BAA2B;IAC3B,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvB,8CAA8C;IAC9C,MAAM,EAAE,yBAAyB;IACjC,qEAAqE;IACrE,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAClC,yDAAyD;IACzD,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IAC7C,uCAAuC;IACvC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAClC,CAAC,CAAC;AAIH;;GAEG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC;IAChD,SAAS,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;IAC3B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;QACZ,iBAAiB;QACjB,mBAAmB;QACnB,mBAAmB;QACnB,cAAc;QACd,qBAAqB;KACrB,CAAC;CACF,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@qnsp/sdk-activation",
3
+ "version": "0.1.1",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "type": "module",
8
+ "license": "Apache-2.0",
9
+ "description": "SDK activation and usage metering for QNSP platform SDKs. Ensures all SDK usage is tied to a registered QNSP account.",
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
19
+ "dependencies": {
20
+ "zod": "^4.3.6"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^25.5.0",
24
+ "typescript": "^6.0.2",
25
+ "vitest": "4.1.2"
26
+ },
27
+ "engines": {
28
+ "node": ">=24.12.0"
29
+ },
30
+ "keywords": [
31
+ "qnsp",
32
+ "sdk",
33
+ "activation",
34
+ "metering",
35
+ "pqc",
36
+ "post-quantum",
37
+ "post-quantum-cryptography",
38
+ "cryptography",
39
+ "api-key",
40
+ "free-tier",
41
+ "quantum-safe",
42
+ "typescript",
43
+ "nodejs"
44
+ ],
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/cuilabs/qnsp",
48
+ "directory": "packages/sdk-activation"
49
+ },
50
+ "homepage": "https://cloud.qnsp.cuilabs.io",
51
+ "bugs": {
52
+ "url": "https://github.com/cuilabs/qnsp/issues"
53
+ },
54
+ "scripts": {
55
+ "build": "tsc --project tsconfig.build.json",
56
+ "lint": "biome check .",
57
+ "test": "vitest run",
58
+ "typecheck": "tsc --project tsconfig.json --noEmit"
59
+ }
60
+ }
@@ -0,0 +1,169 @@
1
+ import { beforeEach, describe, expect, it } from "vitest";
2
+ import {
3
+ activateSdk,
4
+ clearActivationCache,
5
+ getActivationLimits,
6
+ getCachedActivation,
7
+ type SdkActivationConfig,
8
+ SdkActivationError_,
9
+ } from "./activation-client.js";
10
+
11
+ const BASE_CONFIG: SdkActivationConfig = {
12
+ apiKey: "qnsp_test_key_00000000",
13
+ sdkId: "vault-sdk",
14
+ sdkVersion: "0.3.0",
15
+ platformUrl: "https://api.qnsp.cuilabs.io",
16
+ };
17
+
18
+ describe("SdkActivationError_", () => {
19
+ it("creates error with correct properties", () => {
20
+ const err = new SdkActivationError_("INVALID_API_KEY", "bad key", 401);
21
+ expect(err.name).toBe("SdkActivationError");
22
+ expect(err.code).toBe("INVALID_API_KEY");
23
+ expect(err.message).toBe("bad key");
24
+ expect(err.statusCode).toBe(401);
25
+ expect(err).toBeInstanceOf(Error);
26
+ });
27
+ });
28
+
29
+ describe("activateSdk", () => {
30
+ beforeEach(() => {
31
+ clearActivationCache();
32
+ });
33
+
34
+ it("rejects empty API key", async () => {
35
+ await expect(activateSdk({ ...BASE_CONFIG, apiKey: "" })).rejects.toThrow(SdkActivationError_);
36
+
37
+ try {
38
+ await activateSdk({ ...BASE_CONFIG, apiKey: " " });
39
+ } catch (err) {
40
+ expect(err).toBeInstanceOf(SdkActivationError_);
41
+ expect((err as SdkActivationError_).code).toBe("INVALID_API_KEY");
42
+ expect((err as SdkActivationError_).statusCode).toBe(401);
43
+ }
44
+ });
45
+
46
+ it("throws SERVICE_UNAVAILABLE on network failure", async () => {
47
+ const config: SdkActivationConfig = {
48
+ ...BASE_CONFIG,
49
+ platformUrl: "https://localhost:1",
50
+ timeoutMs: 500,
51
+ fetchImpl: () => {
52
+ throw new Error("Connection refused");
53
+ },
54
+ };
55
+
56
+ await expect(activateSdk(config)).rejects.toThrow(SdkActivationError_);
57
+
58
+ try {
59
+ await activateSdk(config);
60
+ } catch (err) {
61
+ expect((err as SdkActivationError_).code).toBe("SERVICE_UNAVAILABLE");
62
+ expect((err as SdkActivationError_).statusCode).toBe(503);
63
+ }
64
+ });
65
+
66
+ it("throws on 401 response", async () => {
67
+ const config: SdkActivationConfig = {
68
+ ...BASE_CONFIG,
69
+ fetchImpl: () => Promise.resolve(new Response(null, { status: 401 })),
70
+ };
71
+
72
+ try {
73
+ await activateSdk(config);
74
+ } catch (err) {
75
+ expect(err).toBeInstanceOf(SdkActivationError_);
76
+ expect((err as SdkActivationError_).code).toBe("INVALID_API_KEY");
77
+ expect((err as SdkActivationError_).statusCode).toBe(401);
78
+ }
79
+ });
80
+
81
+ it("parses 401 JSON error body and preserves api-keys guidance when present", async () => {
82
+ const config: SdkActivationConfig = {
83
+ ...BASE_CONFIG,
84
+ fetchImpl: () =>
85
+ Promise.resolve(
86
+ new Response(
87
+ JSON.stringify({
88
+ activated: false,
89
+ code: "INVALID_API_KEY",
90
+ error: "Invalid API key. Get your key at https://cloud.qnsp.cuilabs.io/api-keys",
91
+ }),
92
+ { status: 401, headers: { "content-type": "application/json" } },
93
+ ),
94
+ ),
95
+ };
96
+
97
+ try {
98
+ await activateSdk(config);
99
+ } catch (err) {
100
+ expect(err).toBeInstanceOf(SdkActivationError_);
101
+ expect((err as SdkActivationError_).message).toContain("cloud.qnsp.cuilabs.io/api-keys");
102
+ }
103
+ });
104
+
105
+ it("throws on 429 response", async () => {
106
+ const config: SdkActivationConfig = {
107
+ ...BASE_CONFIG,
108
+ fetchImpl: () => Promise.resolve(new Response(null, { status: 429 })),
109
+ };
110
+
111
+ try {
112
+ await activateSdk(config);
113
+ } catch (err) {
114
+ expect(err).toBeInstanceOf(SdkActivationError_);
115
+ expect((err as SdkActivationError_).code).toBe("RATE_LIMITED");
116
+ expect((err as SdkActivationError_).statusCode).toBe(429);
117
+ }
118
+ });
119
+
120
+ it("caches successful activation and returns from cache", async () => {
121
+ const activationResponse = {
122
+ activated: true,
123
+ tenantId: "a1b2c3d4-e5f6-4789-8abc-def012345678",
124
+ tier: "dev-starter",
125
+ activationToken: "tok_test",
126
+ expiresInSeconds: 3600,
127
+ activatedAt: "2026-03-13T10:00:00Z",
128
+ limits: {
129
+ storageGB: 5,
130
+ apiCalls: 10_000,
131
+ enclavesEnabled: false,
132
+ aiTrainingEnabled: false,
133
+ aiInferenceEnabled: false,
134
+ sseEnabled: false,
135
+ vaultEnabled: true,
136
+ },
137
+ };
138
+
139
+ const config: SdkActivationConfig = {
140
+ ...BASE_CONFIG,
141
+ fetchImpl: () =>
142
+ Promise.resolve(
143
+ new Response(JSON.stringify(activationResponse), {
144
+ status: 200,
145
+ headers: { "content-type": "application/json" },
146
+ }),
147
+ ),
148
+ };
149
+
150
+ const result = await activateSdk(config);
151
+ expect(result.activated).toBe(true);
152
+ expect(result.tier).toBe("dev-starter");
153
+ expect(result.limits.apiCalls).toBe(10_000);
154
+
155
+ // Should return from cache
156
+ const cached = getCachedActivation(config);
157
+ expect(cached).not.toBeNull();
158
+ expect(cached?.tenantId).toBe("a1b2c3d4-e5f6-4789-8abc-def012345678");
159
+
160
+ const limits = getActivationLimits(config);
161
+ expect(limits).not.toBeNull();
162
+ expect(limits?.storageGB).toBe(5);
163
+ });
164
+
165
+ it("returns null for uncached activation", () => {
166
+ expect(getCachedActivation(BASE_CONFIG)).toBeNull();
167
+ expect(getActivationLimits(BASE_CONFIG)).toBeNull();
168
+ });
169
+ });
@@ -0,0 +1,243 @@
1
+ /**
2
+ * SDK Activation Client
3
+ *
4
+ * Handles activation handshake with the QNSP billing-service.
5
+ * Browser-compatible — uses global fetch, no node: imports.
6
+ *
7
+ * Every SDK must call `activateSdk()` before performing operations.
8
+ * The activation validates the API key against the QNSP backend,
9
+ * returns tier limits, and issues a short-lived activation token.
10
+ *
11
+ * Activation tokens are cached in-memory and auto-refreshed before expiry.
12
+ *
13
+ * @module
14
+ */
15
+
16
+ import {
17
+ type SdkActivationError,
18
+ SdkActivationErrorSchema,
19
+ type SdkActivationLimits,
20
+ type SdkActivationRequest,
21
+ type SdkActivationResponse,
22
+ SdkActivationResponseSchema,
23
+ type SdkIdentifier,
24
+ } from "./types.js";
25
+
26
+ const ACTIVATION_PATH = "/billing/v1/sdk/activate";
27
+ const DEFAULT_PLATFORM_URL = "https://api.qnsp.cuilabs.io";
28
+ const ACTIVATION_TIMEOUT_MS = 15_000;
29
+ const REFRESH_BUFFER_SECONDS = 300; // Refresh 5 minutes before expiry
30
+
31
+ /**
32
+ * Typed error for SDK activation failures.
33
+ */
34
+ export class SdkActivationError_ extends Error {
35
+ readonly code: SdkActivationError["code"];
36
+ readonly statusCode: number;
37
+
38
+ constructor(code: SdkActivationError["code"], message: string, statusCode: number) {
39
+ super(message);
40
+ this.name = "SdkActivationError";
41
+ this.code = code;
42
+ this.statusCode = statusCode;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Cached activation state.
48
+ */
49
+ interface ActivationState {
50
+ readonly response: SdkActivationResponse;
51
+ readonly expiresAt: number; // Unix timestamp in ms
52
+ }
53
+
54
+ /**
55
+ * Configuration for SDK activation.
56
+ */
57
+ export interface SdkActivationConfig {
58
+ /** QNSP API key (required — get one at https://cloud.qnsp.cuilabs.io/signup) */
59
+ readonly apiKey: string;
60
+ /** SDK package identifier */
61
+ readonly sdkId: SdkIdentifier;
62
+ /** SDK version string */
63
+ readonly sdkVersion: string;
64
+ /** Override platform URL (defaults to https://api.qnsp.cuilabs.io) */
65
+ readonly platformUrl?: string | undefined;
66
+ /** Override activation timeout in ms (defaults to 15000) */
67
+ readonly timeoutMs?: number | undefined;
68
+ /** Custom fetch implementation (for testing or non-standard runtimes) */
69
+ readonly fetchImpl?: (typeof globalThis)["fetch"] | undefined;
70
+ }
71
+
72
+ /** In-memory activation cache keyed by `${platformUrl}:${apiKeyPrefix}:${sdkId}` */
73
+ const activationCache = new Map<string, ActivationState>();
74
+
75
+ function cacheKey(config: SdkActivationConfig): string {
76
+ const url = config.platformUrl ?? DEFAULT_PLATFORM_URL;
77
+ // Use first 8 chars of API key for cache key (avoid storing full key in memory map keys)
78
+ const keyPrefix = config.apiKey.slice(0, 8);
79
+ return `${url}:${keyPrefix}:${config.sdkId}`;
80
+ }
81
+
82
+ function detectRuntime(): "browser" | "node" | "edge" {
83
+ if (typeof globalThis.process !== "undefined" && globalThis.process.versions?.node) {
84
+ return "node";
85
+ }
86
+ if ("window" in globalThis && "document" in globalThis) {
87
+ return "browser";
88
+ }
89
+ return "edge";
90
+ }
91
+
92
+ /**
93
+ * Activate an SDK against the QNSP platform.
94
+ *
95
+ * This must be called before any SDK operations. It validates the API key,
96
+ * returns the tenant's tier and limits, and caches the activation token.
97
+ *
98
+ * Cached activations are returned immediately if still valid.
99
+ * Tokens are auto-refreshed 5 minutes before expiry.
100
+ *
101
+ * @throws {SdkActivationError_} if the API key is invalid, account is suspended, or service is unavailable
102
+ */
103
+ export async function activateSdk(config: SdkActivationConfig): Promise<SdkActivationResponse> {
104
+ if (!config.apiKey || config.apiKey.trim().length === 0) {
105
+ throw new SdkActivationError_(
106
+ "INVALID_API_KEY",
107
+ `QNSP ${config.sdkId}: apiKey is required. ` +
108
+ "Get your free API key at https://cloud.qnsp.cuilabs.io/signup — " +
109
+ "no credit card required (FREE tier: 10 GB storage, 50,000 API calls/month). " +
110
+ `Docs: https://docs.qnsp.cuilabs.io/sdk/${config.sdkId}`,
111
+ 401,
112
+ );
113
+ }
114
+
115
+ const key = cacheKey(config);
116
+ const cached = activationCache.get(key);
117
+ const now = Date.now();
118
+
119
+ // Return cached activation if still valid (with refresh buffer)
120
+ if (cached && cached.expiresAt - REFRESH_BUFFER_SECONDS * 1000 > now) {
121
+ return cached.response;
122
+ }
123
+
124
+ const platformUrl = (config.platformUrl ?? DEFAULT_PLATFORM_URL).replace(/\/$/, "");
125
+ const timeoutMs = config.timeoutMs ?? ACTIVATION_TIMEOUT_MS;
126
+ const fetchFn = config.fetchImpl ?? globalThis.fetch;
127
+
128
+ const body: SdkActivationRequest = {
129
+ sdkId: config.sdkId,
130
+ sdkVersion: config.sdkVersion,
131
+ runtime: detectRuntime(),
132
+ };
133
+
134
+ let response: Response;
135
+ try {
136
+ response = await fetchFn(`${platformUrl}${ACTIVATION_PATH}`, {
137
+ method: "POST",
138
+ headers: {
139
+ "content-type": "application/json",
140
+ authorization: `Bearer ${config.apiKey}`,
141
+ },
142
+ body: JSON.stringify(body),
143
+ signal: AbortSignal.timeout(timeoutMs),
144
+ });
145
+ } catch (cause) {
146
+ throw new SdkActivationError_(
147
+ "SERVICE_UNAVAILABLE",
148
+ `QNSP ${config.sdkId}: Failed to reach QNSP platform for activation. ` +
149
+ `Ensure network connectivity to ${platformUrl}. ` +
150
+ `Error: ${cause instanceof Error ? cause.message : String(cause)}`,
151
+ 503,
152
+ );
153
+ }
154
+
155
+ if (!response.ok) {
156
+ let errorBody: SdkActivationError | undefined;
157
+ try {
158
+ const json: unknown = await response.json();
159
+ const parsed = SdkActivationErrorSchema.safeParse(json);
160
+ if (parsed.success) {
161
+ errorBody = parsed.data;
162
+ }
163
+ } catch {
164
+ // Ignore JSON parse errors — use status code
165
+ }
166
+
167
+ if (errorBody) {
168
+ throw new SdkActivationError_(errorBody.code, errorBody.error, response.status);
169
+ }
170
+
171
+ if (response.status === 401) {
172
+ throw new SdkActivationError_(
173
+ "INVALID_API_KEY",
174
+ `QNSP ${config.sdkId}: Invalid API key. ` +
175
+ "Get your API key at https://cloud.qnsp.cuilabs.io/api-keys",
176
+ 401,
177
+ );
178
+ }
179
+
180
+ if (response.status === 429) {
181
+ throw new SdkActivationError_(
182
+ "RATE_LIMITED",
183
+ `QNSP ${config.sdkId}: Activation rate limited. Please retry after a short delay.`,
184
+ 429,
185
+ );
186
+ }
187
+
188
+ throw new SdkActivationError_(
189
+ "SERVICE_UNAVAILABLE",
190
+ `QNSP ${config.sdkId}: Activation failed with HTTP ${response.status}`,
191
+ response.status,
192
+ );
193
+ }
194
+
195
+ const json: unknown = await response.json();
196
+ const parsed = SdkActivationResponseSchema.safeParse(json);
197
+ if (!parsed.success) {
198
+ throw new SdkActivationError_(
199
+ "SERVICE_UNAVAILABLE",
200
+ `QNSP ${config.sdkId}: Invalid activation response from platform`,
201
+ 502,
202
+ );
203
+ }
204
+
205
+ const activation = parsed.data;
206
+
207
+ // Cache the activation
208
+ activationCache.set(key, {
209
+ response: activation,
210
+ expiresAt: now + activation.expiresInSeconds * 1000,
211
+ });
212
+
213
+ return activation;
214
+ }
215
+
216
+ /**
217
+ * Get the cached activation for an SDK, or null if not activated.
218
+ */
219
+ export function getCachedActivation(config: SdkActivationConfig): SdkActivationResponse | null {
220
+ const key = cacheKey(config);
221
+ const cached = activationCache.get(key);
222
+ if (!cached) return null;
223
+ if (cached.expiresAt < Date.now()) {
224
+ activationCache.delete(key);
225
+ return null;
226
+ }
227
+ return cached.response;
228
+ }
229
+
230
+ /**
231
+ * Get the tier limits from a cached activation.
232
+ */
233
+ export function getActivationLimits(config: SdkActivationConfig): SdkActivationLimits | null {
234
+ const activation = getCachedActivation(config);
235
+ return activation?.limits ?? null;
236
+ }
237
+
238
+ /**
239
+ * Clear all cached activations. Primarily for testing.
240
+ */
241
+ export function clearActivationCache(): void {
242
+ activationCache.clear();
243
+ }
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @qnsp/sdk-activation
3
+ *
4
+ * SDK activation and usage metering for QNSP platform SDKs.
5
+ * Ensures all SDK usage is tied to a registered QNSP account.
6
+ *
7
+ * Browser-compatible — no node: imports.
8
+ *
9
+ * @module
10
+ */
11
+
12
+ export {
13
+ activateSdk,
14
+ clearActivationCache,
15
+ getActivationLimits,
16
+ getCachedActivation,
17
+ type SdkActivationConfig,
18
+ SdkActivationError_,
19
+ } from "./activation-client.js";
20
+
21
+ export {
22
+ type SdkActivationError,
23
+ SdkActivationErrorSchema,
24
+ type SdkActivationLimits,
25
+ SdkActivationLimitsSchema,
26
+ type SdkActivationRequest,
27
+ SdkActivationRequestSchema,
28
+ type SdkActivationResponse,
29
+ SdkActivationResponseSchema,
30
+ type SdkIdentifier,
31
+ SdkIdentifierSchema,
32
+ } from "./types.js";
package/src/types.ts ADDED
@@ -0,0 +1,99 @@
1
+ /**
2
+ * SDK Activation Types
3
+ *
4
+ * Shared contract between SDK clients and the billing-service activation endpoint.
5
+ * Browser-compatible — no node: imports.
6
+ *
7
+ * @module
8
+ */
9
+
10
+ import { z } from "zod";
11
+
12
+ /**
13
+ * SDK identifier sent during activation to identify which package is being used.
14
+ */
15
+ export const SdkIdentifierSchema = z.enum([
16
+ "browser-sdk",
17
+ "vault-sdk",
18
+ "storage-sdk",
19
+ "search-sdk",
20
+ "kms-client",
21
+ "ai-sdk",
22
+ "tenant-sdk",
23
+ "billing-sdk",
24
+ "auth-sdk",
25
+ "audit-sdk",
26
+ "access-control-sdk",
27
+ "crypto-inventory-sdk",
28
+ ]);
29
+
30
+ export type SdkIdentifier = z.infer<typeof SdkIdentifierSchema>;
31
+
32
+ /**
33
+ * Request body for POST /billing/v1/sdk/activate
34
+ */
35
+ export const SdkActivationRequestSchema = z.object({
36
+ /** The SDK package requesting activation */
37
+ sdkId: SdkIdentifierSchema,
38
+ /** SDK version string (e.g., "0.1.0") */
39
+ sdkVersion: z.string().min(1).max(32),
40
+ /** Runtime environment: "browser", "node", "edge" */
41
+ runtime: z.enum(["browser", "node", "edge"]),
42
+ });
43
+
44
+ export type SdkActivationRequest = z.infer<typeof SdkActivationRequestSchema>;
45
+
46
+ /**
47
+ * Tier limits returned in the activation response.
48
+ * Subset of full TierLimits — only what SDKs need for client-side enforcement.
49
+ */
50
+ export const SdkActivationLimitsSchema = z.object({
51
+ storageGB: z.number(),
52
+ apiCalls: z.number(),
53
+ enclavesEnabled: z.boolean(),
54
+ aiTrainingEnabled: z.boolean(),
55
+ aiInferenceEnabled: z.boolean(),
56
+ sseEnabled: z.boolean(),
57
+ vaultEnabled: z.boolean(),
58
+ });
59
+
60
+ export type SdkActivationLimits = z.infer<typeof SdkActivationLimitsSchema>;
61
+
62
+ /**
63
+ * Successful activation response from billing-service.
64
+ */
65
+ export const SdkActivationResponseSchema = z.object({
66
+ /** Whether activation succeeded */
67
+ activated: z.literal(true),
68
+ /** Tenant ID associated with the API key */
69
+ tenantId: z.string().uuid(),
70
+ /** Current pricing tier */
71
+ tier: z.string().min(1),
72
+ /** Tier limits for client-side enforcement */
73
+ limits: SdkActivationLimitsSchema,
74
+ /** Activation token — opaque string, valid for `expiresInSeconds` */
75
+ activationToken: z.string().min(1),
76
+ /** Token validity in seconds (default: 3600 = 1 hour) */
77
+ expiresInSeconds: z.number().int().positive(),
78
+ /** ISO 8601 timestamp of activation */
79
+ activatedAt: z.string().datetime(),
80
+ });
81
+
82
+ export type SdkActivationResponse = z.infer<typeof SdkActivationResponseSchema>;
83
+
84
+ /**
85
+ * Error response from the activation endpoint.
86
+ */
87
+ export const SdkActivationErrorSchema = z.object({
88
+ activated: z.literal(false),
89
+ error: z.string(),
90
+ code: z.enum([
91
+ "INVALID_API_KEY",
92
+ "ACCOUNT_SUSPENDED",
93
+ "TIER_INSUFFICIENT",
94
+ "RATE_LIMITED",
95
+ "SERVICE_UNAVAILABLE",
96
+ ]),
97
+ });
98
+
99
+ export type SdkActivationError = z.infer<typeof SdkActivationErrorSchema>;
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "ignoreDeprecations": "6.0",
5
+ "declaration": true,
6
+ "declarationMap": true,
7
+ "sourceMap": true
8
+ },
9
+ "include": ["src/**/*"],
10
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
11
+ }