@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 +15 -0
- package/README.md +58 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +204 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
- package/src/index.ts +279 -0
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)
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|