@pcreative/license-client 1.0.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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pcreativedev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # @pcreative/license-client
2
+
3
+ License verification client for **pcreative.dev** templates.
4
+
5
+ - 🔐 **RS256 JWT** signatures verified locally with embedded public key
6
+ - 🌐 **Offline-capable** — once activated, no network needed
7
+ - 💔 **Soft-kill state machine** — active → grace (7d) → degraded (30d) → invalid
8
+ - 🔍 **Watermark tracking** — each license has a unique ID embedded in the JWT
9
+ - 📦 **Domain binding** — JWT is bound to a specific domain at activation time
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @pcreative/license-client
15
+ # or
16
+ bun add @pcreative/license-client
17
+ ```
18
+
19
+ ## Usage (server-side, Next.js / Express / Node)
20
+
21
+ ```typescript
22
+ import { activateLicense, getLicenseState, heartbeat } from "@pcreative/license-client";
23
+
24
+ // 1. First activation (e.g., in your /setup wizard)
25
+ const result = await activateLicense({
26
+ licenseKey: "AURORA-XXXX-XXXX",
27
+ product: "aurora",
28
+ domain: "yoursite.com",
29
+ });
30
+
31
+ if (result.valid) {
32
+ // JWT now stored in .pcreative-license.json at project root
33
+ }
34
+
35
+ // 2. On every request / startup, check state
36
+ const state = await getLicenseState("aurora");
37
+ switch (state.status) {
38
+ case "missing": /* redirect to /setup */ break;
39
+ case "active": /* normal */ break;
40
+ case "grace": /* show banner: state.daysLeft */ break;
41
+ case "degraded": /* random failures (soft-kill phase) */ break;
42
+ case "invalid": /* block */ break;
43
+ }
44
+
45
+ // 3. Daily heartbeat (cron / setInterval)
46
+ await heartbeat();
47
+ ```
48
+
49
+ ## Environment variables
50
+
51
+ | Variable | Default | Purpose |
52
+ |---|---|---|
53
+ | `PCREATIVE_LICENSE_API` | `https://api.pcreative.dev` | License API base URL |
54
+ | `PCREATIVE_LICENSE_DOMAIN` | `request.host` | Override domain detection |
55
+
56
+ ## License
57
+
58
+ MIT — see [LICENSE](./LICENSE)
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @pcreative/license-client — universal license verification helper
3
+ *
4
+ * Designed to be dropped into any template (Next.js, Express, Vite+Express).
5
+ * Verifies JWT licenses offline with the embedded public key, with optional
6
+ * heartbeat-based remote re-verification (24h cadence).
7
+ *
8
+ * Storage convention:
9
+ * - `.pcreative-license.json` at the project root contains the active JWT + metadata.
10
+ * - The user fills it manually OR via the Setup Wizard UI.
11
+ *
12
+ * Environment variable overrides (.env):
13
+ * - PCREATIVE_LICENSE_KEY (raw license key)
14
+ * - PCREATIVE_LICENSE_DOMAIN (force a specific domain, otherwise host header)
15
+ * - PCREATIVE_LICENSE_API (default https://api.pcreative.dev)
16
+ */
17
+ export declare const LICENSE_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy7OtwWHOMmhJiUERABDt\nILypQ2fTNWtyoF5TNjqq6fMbSF6r9JXxkiiWZhqew0PcHg3SvU/q9HJ0o+RCoKRu\nIRnkIRys3yghwOsDXq2IHUjnxH/XycB8w3NsevKl09rHltdSAGUL4YkA2bWdIknd\nAe2GPIt4nbewK3sO6ZnsC2jaLqUvB7I4vl4zxVVoj8yIOmy+AA15r81fERquUCTH\n-----END PUBLIC KEY-----";
18
+ export interface LicensePayload {
19
+ sub: string;
20
+ product: string;
21
+ domain: string;
22
+ type: "regular" | "extended";
23
+ extended: boolean;
24
+ watermark: string;
25
+ email: string;
26
+ iat: number;
27
+ exp: number;
28
+ iss: string;
29
+ aud: string;
30
+ jti: string;
31
+ }
32
+ export interface StoredLicense {
33
+ jwt: string;
34
+ payload: LicensePayload;
35
+ invalid_since: string | null;
36
+ last_heartbeat: string | null;
37
+ }
38
+ export declare function loadStored(): StoredLicense | null;
39
+ export declare function saveStored(data: StoredLicense): void;
40
+ export declare function clearStored(): void;
41
+ export declare function verifyJwt(jwt: string, product: string): Promise<LicensePayload | null>;
42
+ export interface ActivateInput {
43
+ licenseKey: string;
44
+ product: string;
45
+ domain: string;
46
+ }
47
+ export interface ActivateResult {
48
+ valid: boolean;
49
+ jwt?: string;
50
+ error?: string;
51
+ }
52
+ export declare function activateLicense(input: ActivateInput): Promise<ActivateResult>;
53
+ export declare function heartbeat(): Promise<boolean>;
54
+ export type LicenseState = {
55
+ status: "missing";
56
+ } | {
57
+ status: "active";
58
+ payload: LicensePayload;
59
+ } | {
60
+ status: "grace";
61
+ payload: LicensePayload;
62
+ daysLeft: number;
63
+ reason: string;
64
+ } | {
65
+ status: "degraded";
66
+ payload: LicensePayload;
67
+ reason: string;
68
+ } | {
69
+ status: "invalid";
70
+ reason: string;
71
+ };
72
+ export declare function getLicenseState(product: string): Promise<LicenseState>;
73
+ export declare function isLicensed(product: string): Promise<boolean>;
74
+ export interface SoftKillEffects {
75
+ /** Show a small warning banner */
76
+ showWarning: boolean;
77
+ /** Inject a watermark visible to end users */
78
+ showWatermark: boolean;
79
+ /** Randomly slow down some responses (50/50) */
80
+ randomLatency: boolean;
81
+ /** Randomly fail 30% of API calls */
82
+ failRandomly: boolean;
83
+ /** Block entirely */
84
+ blockAll: boolean;
85
+ }
86
+ export declare function softKillEffects(state: LicenseState): SoftKillEffects;
87
+ export declare function getProductFromEnv(fallback: string): string;
88
+ export declare function getDomainFromHost(hostHeader: string | null | undefined): string;
89
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,204 @@
1
+ /**
2
+ * @pcreative/license-client — universal license verification helper
3
+ *
4
+ * Designed to be dropped into any template (Next.js, Express, Vite+Express).
5
+ * Verifies JWT licenses offline with the embedded public key, with optional
6
+ * heartbeat-based remote re-verification (24h cadence).
7
+ *
8
+ * Storage convention:
9
+ * - `.pcreative-license.json` at the project root contains the active JWT + metadata.
10
+ * - The user fills it manually OR via the Setup Wizard UI.
11
+ *
12
+ * Environment variable overrides (.env):
13
+ * - PCREATIVE_LICENSE_KEY (raw license key)
14
+ * - PCREATIVE_LICENSE_DOMAIN (force a specific domain, otherwise host header)
15
+ * - PCREATIVE_LICENSE_API (default https://api.pcreative.dev)
16
+ */
17
+ import fs from "node:fs";
18
+ import path from "node:path";
19
+ import { jwtVerify, importSPKI } from "jose";
20
+ // =============================================================================
21
+ // EMBEDDED PUBLIC KEY — generated from /api/license/pubkey on 2026-05-22.
22
+ // This is the ONLY thing the template needs to verify JWTs offline.
23
+ // =============================================================================
24
+ export const LICENSE_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
25
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy7OtwWHOMmhJiUERABDt
26
+ ILypQ2fTNWtyoF5TNjqq6fMbSF6r9JXxkiiWZhqew0PcHg3SvU/q9HJ0o+RCoKRu
27
+ IRnkIRys3yghwOsDXq2IHUjnxH/XycB8w3NsevKl09rHltdSAGUL4YkA2bWdIknd
28
+ Ae2GPIt4nbewK3sO6ZnsC2jaLqUvB7I4vl4zxVVoj8yIOmy+AA15r81fERquUCTH
29
+ -----END PUBLIC KEY-----`;
30
+ // NOTE: replace this constant with the actual pubkey from your account
31
+ // (curl https://api.pcreative.dev/api/license/pubkey). The template ships
32
+ // with the pcreative.dev pubkey embedded so no network call is needed to verify.
33
+ const API_BASE = process.env.PCREATIVE_LICENSE_API || "https://api.pcreative.dev";
34
+ const STORAGE_FILE = ".pcreative-license.json";
35
+ const STORAGE_PATH = path.resolve(process.cwd(), STORAGE_FILE);
36
+ // =============================================================================
37
+ // Storage
38
+ // =============================================================================
39
+ export function loadStored() {
40
+ try {
41
+ return JSON.parse(fs.readFileSync(STORAGE_PATH, "utf-8"));
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ }
47
+ export function saveStored(data) {
48
+ fs.writeFileSync(STORAGE_PATH, JSON.stringify(data, null, 2), { mode: 0o600 });
49
+ }
50
+ export function clearStored() {
51
+ try {
52
+ fs.unlinkSync(STORAGE_PATH);
53
+ }
54
+ catch { }
55
+ }
56
+ // =============================================================================
57
+ // JWT verification (offline, with embedded public key)
58
+ // =============================================================================
59
+ let cachedPublicKey = null;
60
+ async function getPublicKey() {
61
+ if (!cachedPublicKey) {
62
+ cachedPublicKey = await importSPKI(LICENSE_PUBLIC_KEY, "RS256");
63
+ }
64
+ return cachedPublicKey;
65
+ }
66
+ export async function verifyJwt(jwt, product) {
67
+ try {
68
+ const pubKey = await getPublicKey();
69
+ const { payload } = await jwtVerify(jwt, pubKey, {
70
+ issuer: "pcreative.dev",
71
+ audience: product,
72
+ });
73
+ return payload;
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
79
+ export async function activateLicense(input) {
80
+ try {
81
+ const res = await fetch(`${API_BASE}/api/license/activate`, {
82
+ method: "POST",
83
+ headers: { "Content-Type": "application/json" },
84
+ body: JSON.stringify({
85
+ license_key: input.licenseKey,
86
+ product: input.product,
87
+ domain: input.domain,
88
+ }),
89
+ });
90
+ const data = await res.json().catch(() => ({}));
91
+ if (data?.valid && data?.jwt) {
92
+ const payload = await verifyJwt(data.jwt, input.product);
93
+ if (!payload) {
94
+ return { valid: false, error: "Received JWT failed signature verification" };
95
+ }
96
+ saveStored({ jwt: data.jwt, payload, invalid_since: null, last_heartbeat: new Date().toISOString() });
97
+ return { valid: true, jwt: data.jwt };
98
+ }
99
+ return { valid: false, error: data?.error || "Activation failed" };
100
+ }
101
+ catch (err) {
102
+ return { valid: false, error: err instanceof Error ? err.message : "Network error" };
103
+ }
104
+ }
105
+ export async function heartbeat() {
106
+ const stored = loadStored();
107
+ if (!stored)
108
+ return false;
109
+ try {
110
+ const res = await fetch(`${API_BASE}/api/license/heartbeat`, {
111
+ method: "POST",
112
+ headers: { "Content-Type": "application/json" },
113
+ body: JSON.stringify({ jwt: stored.jwt }),
114
+ });
115
+ const data = await res.json().catch(() => ({}));
116
+ if (data?.valid && data?.jwt) {
117
+ const payload = await verifyJwt(data.jwt, stored.payload.product);
118
+ if (payload) {
119
+ saveStored({ jwt: data.jwt, payload, invalid_since: null, last_heartbeat: new Date().toISOString() });
120
+ return true;
121
+ }
122
+ }
123
+ // Invalid response — mark as failing
124
+ if (!stored.invalid_since) {
125
+ saveStored({ ...stored, invalid_since: new Date().toISOString() });
126
+ }
127
+ return false;
128
+ }
129
+ catch {
130
+ // Network error — don't penalize immediately, just update heartbeat
131
+ return false;
132
+ }
133
+ }
134
+ const GRACE_PERIOD_DAYS = 7; // After invalid_since, this many days of full features
135
+ const SOFT_KILL_DAYS = 30; // After this many days from invalid_since → fully blocked
136
+ export async function getLicenseState(product) {
137
+ const stored = loadStored();
138
+ if (!stored)
139
+ return { status: "missing" };
140
+ // Verify JWT signature
141
+ const payload = await verifyJwt(stored.jwt, product);
142
+ if (!payload) {
143
+ return { status: "invalid", reason: "JWT signature invalid or expired" };
144
+ }
145
+ // Check exp
146
+ if (payload.exp * 1000 < Date.now()) {
147
+ return { status: "invalid", reason: "JWT expired" };
148
+ }
149
+ // Check if marked as failing on previous heartbeats
150
+ if (stored.invalid_since) {
151
+ const since = new Date(stored.invalid_since).getTime();
152
+ const elapsed = (Date.now() - since) / (1000 * 60 * 60 * 24);
153
+ if (elapsed < GRACE_PERIOD_DAYS) {
154
+ return {
155
+ status: "grace",
156
+ payload,
157
+ daysLeft: Math.ceil(GRACE_PERIOD_DAYS - elapsed),
158
+ reason: "License verification has been failing — re-check your subscription"
159
+ };
160
+ }
161
+ if (elapsed < SOFT_KILL_DAYS) {
162
+ return {
163
+ status: "degraded",
164
+ payload,
165
+ reason: `License has been invalid for ${Math.ceil(elapsed)} days — features will progressively degrade`
166
+ };
167
+ }
168
+ return { status: "invalid", reason: "Soft-kill period exhausted" };
169
+ }
170
+ return { status: "active", payload };
171
+ }
172
+ export async function isLicensed(product) {
173
+ const state = await getLicenseState(product);
174
+ return state.status === "active" || state.status === "grace";
175
+ }
176
+ export function softKillEffects(state) {
177
+ switch (state.status) {
178
+ case "missing":
179
+ return { showWarning: false, showWatermark: false, randomLatency: false, failRandomly: false, blockAll: true };
180
+ case "active":
181
+ return { showWarning: false, showWatermark: false, randomLatency: false, failRandomly: false, blockAll: false };
182
+ case "grace":
183
+ return { showWarning: true, showWatermark: false, randomLatency: false, failRandomly: false, blockAll: false };
184
+ case "degraded":
185
+ // Effects scale with elapsed time — caller can be smarter
186
+ return { showWarning: true, showWatermark: true, randomLatency: true, failRandomly: true, blockAll: false };
187
+ case "invalid":
188
+ return { showWarning: true, showWatermark: true, randomLatency: false, failRandomly: false, blockAll: true };
189
+ }
190
+ }
191
+ // =============================================================================
192
+ // Convenience wrappers
193
+ // =============================================================================
194
+ export function getProductFromEnv(fallback) {
195
+ return process.env.PCREATIVE_LICENSE_PRODUCT || fallback;
196
+ }
197
+ export function getDomainFromHost(hostHeader) {
198
+ if (process.env.PCREATIVE_LICENSE_DOMAIN)
199
+ return process.env.PCREATIVE_LICENSE_DOMAIN;
200
+ if (!hostHeader)
201
+ return "localhost";
202
+ return String(hostHeader).toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").split(":")[0];
203
+ }
204
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@pcreative/license-client",
3
+ "version": "1.0.0",
4
+ "description": "License verification client for pcreative.dev templates \u2014 RS256 JWT, offline verification, soft-kill state machine",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc"
22
+ },
23
+ "keywords": [
24
+ "license",
25
+ "drm",
26
+ "jwt",
27
+ "rs256",
28
+ "pcreative",
29
+ "template",
30
+ "anti-piracy"
31
+ ],
32
+ "author": "pcreativedev <contact@differentxperience.com>",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/pcreativedev/license-client.git"
37
+ },
38
+ "homepage": "https://github.com/pcreativedev/license-client",
39
+ "bugs": {
40
+ "url": "https://github.com/pcreativedev/license-client/issues"
41
+ },
42
+ "dependencies": {
43
+ "jose": "^6.2.3"
44
+ },
45
+ "devDependencies": {
46
+ "typescript": "^5.0.0",
47
+ "@types/node": "^20.0.0"
48
+ },
49
+ "engines": {
50
+ "node": ">=18"
51
+ }
52
+ }
package/src/index.ts ADDED
@@ -0,0 +1,279 @@
1
+ /**
2
+ * @pcreative/license-client — universal license verification helper
3
+ *
4
+ * Designed to be dropped into any template (Next.js, Express, Vite+Express).
5
+ * Verifies JWT licenses offline with the embedded public key, with optional
6
+ * heartbeat-based remote re-verification (24h cadence).
7
+ *
8
+ * Storage convention:
9
+ * - `.pcreative-license.json` at the project root contains the active JWT + metadata.
10
+ * - The user fills it manually OR via the Setup Wizard UI.
11
+ *
12
+ * Environment variable overrides (.env):
13
+ * - PCREATIVE_LICENSE_KEY (raw license key)
14
+ * - PCREATIVE_LICENSE_DOMAIN (force a specific domain, otherwise host header)
15
+ * - PCREATIVE_LICENSE_API (default https://api.pcreative.dev)
16
+ */
17
+
18
+ import fs from "node:fs";
19
+ import path from "node:path";
20
+ import { jwtVerify, importSPKI } from "jose";
21
+
22
+ // =============================================================================
23
+ // EMBEDDED PUBLIC KEY — generated from /api/license/pubkey on 2026-05-22.
24
+ // This is the ONLY thing the template needs to verify JWTs offline.
25
+ // =============================================================================
26
+ export const LICENSE_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
27
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy7OtwWHOMmhJiUERABDt
28
+ ILypQ2fTNWtyoF5TNjqq6fMbSF6r9JXxkiiWZhqew0PcHg3SvU/q9HJ0o+RCoKRu
29
+ IRnkIRys3yghwOsDXq2IHUjnxH/XycB8w3NsevKl09rHltdSAGUL4YkA2bWdIknd
30
+ Ae2GPIt4nbewK3sO6ZnsC2jaLqUvB7I4vl4zxVVoj8yIOmy+AA15r81fERquUCTH
31
+ -----END PUBLIC KEY-----`;
32
+
33
+ // NOTE: replace this constant with the actual pubkey from your account
34
+ // (curl https://api.pcreative.dev/api/license/pubkey). The template ships
35
+ // with the pcreative.dev pubkey embedded so no network call is needed to verify.
36
+
37
+ const API_BASE = process.env.PCREATIVE_LICENSE_API || "https://api.pcreative.dev";
38
+ const STORAGE_FILE = ".pcreative-license.json";
39
+ const STORAGE_PATH = path.resolve(process.cwd(), STORAGE_FILE);
40
+
41
+ export interface LicensePayload {
42
+ sub: string; // license key
43
+ product: string; // product slug (e.g., "aurora")
44
+ domain: string; // bound domain
45
+ type: "regular" | "extended";
46
+ extended: boolean;
47
+ watermark: string; // unique tracking id
48
+ email: string;
49
+ iat: number;
50
+ exp: number;
51
+ iss: string; // "pcreative.dev"
52
+ aud: string;
53
+ jti: string;
54
+ }
55
+
56
+ export interface StoredLicense {
57
+ jwt: string;
58
+ payload: LicensePayload;
59
+ invalid_since: string | null; // ISO timestamp set when verification starts failing
60
+ last_heartbeat: string | null;
61
+ }
62
+
63
+ // =============================================================================
64
+ // Storage
65
+ // =============================================================================
66
+ export function loadStored(): StoredLicense | null {
67
+ try {
68
+ return JSON.parse(fs.readFileSync(STORAGE_PATH, "utf-8"));
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+
74
+ export function saveStored(data: StoredLicense): void {
75
+ fs.writeFileSync(STORAGE_PATH, JSON.stringify(data, null, 2), { mode: 0o600 });
76
+ }
77
+
78
+ export function clearStored(): void {
79
+ try { fs.unlinkSync(STORAGE_PATH); } catch {}
80
+ }
81
+
82
+ // =============================================================================
83
+ // JWT verification (offline, with embedded public key)
84
+ // =============================================================================
85
+ let cachedPublicKey: Awaited<ReturnType<typeof importSPKI>> | null = null;
86
+
87
+ async function getPublicKey() {
88
+ if (!cachedPublicKey) {
89
+ cachedPublicKey = await importSPKI(LICENSE_PUBLIC_KEY, "RS256");
90
+ }
91
+ return cachedPublicKey;
92
+ }
93
+
94
+ export async function verifyJwt(jwt: string, product: string): Promise<LicensePayload | null> {
95
+ try {
96
+ const pubKey = await getPublicKey();
97
+ const { payload } = await jwtVerify(jwt, pubKey, {
98
+ issuer: "pcreative.dev",
99
+ audience: product,
100
+ });
101
+ return payload as unknown as LicensePayload;
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ // =============================================================================
108
+ // Remote activation + heartbeat
109
+ // =============================================================================
110
+ export interface ActivateInput {
111
+ licenseKey: string;
112
+ product: string;
113
+ domain: string;
114
+ }
115
+
116
+ export interface ActivateResult {
117
+ valid: boolean;
118
+ jwt?: string;
119
+ error?: string;
120
+ }
121
+
122
+ export async function activateLicense(input: ActivateInput): Promise<ActivateResult> {
123
+ try {
124
+ const res = await fetch(`${API_BASE}/api/license/activate`, {
125
+ method: "POST",
126
+ headers: { "Content-Type": "application/json" },
127
+ body: JSON.stringify({
128
+ license_key: input.licenseKey,
129
+ product: input.product,
130
+ domain: input.domain,
131
+ }),
132
+ });
133
+
134
+ const data = await res.json().catch(() => ({})) as { valid?: boolean; jwt?: string; error?: string };
135
+ if (data?.valid && data?.jwt) {
136
+ const payload = await verifyJwt(data.jwt, input.product);
137
+ if (!payload) {
138
+ return { valid: false, error: "Received JWT failed signature verification" };
139
+ }
140
+ saveStored({ jwt: data.jwt, payload, invalid_since: null, last_heartbeat: new Date().toISOString() });
141
+ return { valid: true, jwt: data.jwt };
142
+ }
143
+ return { valid: false, error: data?.error || "Activation failed" };
144
+ } catch (err) {
145
+ return { valid: false, error: err instanceof Error ? err.message : "Network error" };
146
+ }
147
+ }
148
+
149
+ export async function heartbeat(): Promise<boolean> {
150
+ const stored = loadStored();
151
+ if (!stored) return false;
152
+
153
+ try {
154
+ const res = await fetch(`${API_BASE}/api/license/heartbeat`, {
155
+ method: "POST",
156
+ headers: { "Content-Type": "application/json" },
157
+ body: JSON.stringify({ jwt: stored.jwt }),
158
+ });
159
+ const data = await res.json().catch(() => ({})) as { valid?: boolean; jwt?: string; error?: string };
160
+ if (data?.valid && data?.jwt) {
161
+ const payload = await verifyJwt(data.jwt, stored.payload.product);
162
+ if (payload) {
163
+ saveStored({ jwt: data.jwt, payload, invalid_since: null, last_heartbeat: new Date().toISOString() });
164
+ return true;
165
+ }
166
+ }
167
+ // Invalid response — mark as failing
168
+ if (!stored.invalid_since) {
169
+ saveStored({ ...stored, invalid_since: new Date().toISOString() });
170
+ }
171
+ return false;
172
+ } catch {
173
+ // Network error — don't penalize immediately, just update heartbeat
174
+ return false;
175
+ }
176
+ }
177
+
178
+ // =============================================================================
179
+ // High-level state machine
180
+ // =============================================================================
181
+ export type LicenseState =
182
+ | { status: "missing" } // no license yet — show setup wizard
183
+ | { status: "active"; payload: LicensePayload } // all good
184
+ | { status: "grace"; payload: LicensePayload; daysLeft: number; reason: string } // failing but in grace period
185
+ | { status: "degraded"; payload: LicensePayload; reason: string } // grace expired, soft-kill in progress
186
+ | { status: "invalid"; reason: string }; // fully invalid, hard kill
187
+
188
+ const GRACE_PERIOD_DAYS = 7; // After invalid_since, this many days of full features
189
+ const SOFT_KILL_DAYS = 30; // After this many days from invalid_since → fully blocked
190
+
191
+ export async function getLicenseState(product: string): Promise<LicenseState> {
192
+ const stored = loadStored();
193
+ if (!stored) return { status: "missing" };
194
+
195
+ // Verify JWT signature
196
+ const payload = await verifyJwt(stored.jwt, product);
197
+ if (!payload) {
198
+ return { status: "invalid", reason: "JWT signature invalid or expired" };
199
+ }
200
+
201
+ // Check exp
202
+ if (payload.exp * 1000 < Date.now()) {
203
+ return { status: "invalid", reason: "JWT expired" };
204
+ }
205
+
206
+ // Check if marked as failing on previous heartbeats
207
+ if (stored.invalid_since) {
208
+ const since = new Date(stored.invalid_since).getTime();
209
+ const elapsed = (Date.now() - since) / (1000 * 60 * 60 * 24);
210
+ if (elapsed < GRACE_PERIOD_DAYS) {
211
+ return {
212
+ status: "grace",
213
+ payload,
214
+ daysLeft: Math.ceil(GRACE_PERIOD_DAYS - elapsed),
215
+ reason: "License verification has been failing — re-check your subscription"
216
+ };
217
+ }
218
+ if (elapsed < SOFT_KILL_DAYS) {
219
+ return {
220
+ status: "degraded",
221
+ payload,
222
+ reason: `License has been invalid for ${Math.ceil(elapsed)} days — features will progressively degrade`
223
+ };
224
+ }
225
+ return { status: "invalid", reason: "Soft-kill period exhausted" };
226
+ }
227
+
228
+ return { status: "active", payload };
229
+ }
230
+
231
+ export async function isLicensed(product: string): Promise<boolean> {
232
+ const state = await getLicenseState(product);
233
+ return state.status === "active" || state.status === "grace";
234
+ }
235
+
236
+ // =============================================================================
237
+ // Soft-kill effects (templates can wire these into UI as desired)
238
+ // =============================================================================
239
+ export interface SoftKillEffects {
240
+ /** Show a small warning banner */
241
+ showWarning: boolean;
242
+ /** Inject a watermark visible to end users */
243
+ showWatermark: boolean;
244
+ /** Randomly slow down some responses (50/50) */
245
+ randomLatency: boolean;
246
+ /** Randomly fail 30% of API calls */
247
+ failRandomly: boolean;
248
+ /** Block entirely */
249
+ blockAll: boolean;
250
+ }
251
+
252
+ export function softKillEffects(state: LicenseState): SoftKillEffects {
253
+ switch (state.status) {
254
+ case "missing":
255
+ return { showWarning: false, showWatermark: false, randomLatency: false, failRandomly: false, blockAll: true };
256
+ case "active":
257
+ return { showWarning: false, showWatermark: false, randomLatency: false, failRandomly: false, blockAll: false };
258
+ case "grace":
259
+ return { showWarning: true, showWatermark: false, randomLatency: false, failRandomly: false, blockAll: false };
260
+ case "degraded":
261
+ // Effects scale with elapsed time — caller can be smarter
262
+ return { showWarning: true, showWatermark: true, randomLatency: true, failRandomly: true, blockAll: false };
263
+ case "invalid":
264
+ return { showWarning: true, showWatermark: true, randomLatency: false, failRandomly: false, blockAll: true };
265
+ }
266
+ }
267
+
268
+ // =============================================================================
269
+ // Convenience wrappers
270
+ // =============================================================================
271
+ export function getProductFromEnv(fallback: string): string {
272
+ return process.env.PCREATIVE_LICENSE_PRODUCT || fallback;
273
+ }
274
+
275
+ export function getDomainFromHost(hostHeader: string | null | undefined): string {
276
+ if (process.env.PCREATIVE_LICENSE_DOMAIN) return process.env.PCREATIVE_LICENSE_DOMAIN;
277
+ if (!hostHeader) return "localhost";
278
+ return String(hostHeader).toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "").split(":")[0];
279
+ }