@compilr-dev/sdk 0.9.20 → 0.9.21
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/entitlements/cache.js +34 -1
- package/package.json +1 -1
|
@@ -11,7 +11,36 @@
|
|
|
11
11
|
* - fetchFn: how to call the server endpoint
|
|
12
12
|
* - store (optional): encrypted persistence for offline grace (IEntitlementStore)
|
|
13
13
|
*/
|
|
14
|
+
import { createPublicKey, verify } from 'node:crypto';
|
|
14
15
|
import { UNLIMITED, OFFLINE_FALLBACK_LIMITS } from './types.js';
|
|
16
|
+
/**
|
|
17
|
+
* Ed25519 public key for verifying entitlement response signatures.
|
|
18
|
+
* The corresponding private key is stored in Netlify env var ENTITLEMENT_SIGNING_KEY.
|
|
19
|
+
* This public key cannot forge signatures — it can only verify them.
|
|
20
|
+
*/
|
|
21
|
+
const ENTITLEMENT_PUBLIC_KEY = 'MCowBQYDK2VwAyEAMgDyiGWQVSnZKV6NF1UrDIkWtfH9BAyv93PyEg7AH40=';
|
|
22
|
+
/**
|
|
23
|
+
* Verify the Ed25519 signature on an entitlement response.
|
|
24
|
+
* Returns true if valid or if signature verification is not available.
|
|
25
|
+
*/
|
|
26
|
+
function verifySignature(response) {
|
|
27
|
+
if (!response.signature)
|
|
28
|
+
return true; // No signature — allow (beta/dev)
|
|
29
|
+
try {
|
|
30
|
+
const { signature, ...payload } = response;
|
|
31
|
+
const publicKey = createPublicKey({
|
|
32
|
+
key: Buffer.from(ENTITLEMENT_PUBLIC_KEY, 'base64'),
|
|
33
|
+
format: 'der',
|
|
34
|
+
type: 'spki',
|
|
35
|
+
});
|
|
36
|
+
const payloadBuffer = Buffer.from(JSON.stringify(payload));
|
|
37
|
+
const signatureBuffer = Buffer.from(signature, 'base64');
|
|
38
|
+
return verify(null, payloadBuffer, publicKey, signatureBuffer);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return true; // Verification unavailable — fail open (don't break during migration)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
15
44
|
// =============================================================================
|
|
16
45
|
// Cache Implementation
|
|
17
46
|
// =============================================================================
|
|
@@ -146,6 +175,10 @@ export class EntitlementCache {
|
|
|
146
175
|
async fetchAndCache() {
|
|
147
176
|
try {
|
|
148
177
|
const response = await this.fetchFn();
|
|
178
|
+
// Verify Ed25519 signature (prevents tampered responses)
|
|
179
|
+
if (!verifySignature(response)) {
|
|
180
|
+
throw new Error('Entitlement signature verification failed');
|
|
181
|
+
}
|
|
149
182
|
this.entitlements = response;
|
|
150
183
|
this.fetchedAtMonotonic = process.hrtime.bigint();
|
|
151
184
|
this.fetchedAtWall = Date.now();
|
|
@@ -174,7 +207,7 @@ export class EntitlementCache {
|
|
|
174
207
|
if (stored) {
|
|
175
208
|
const parsed = JSON.parse(stored);
|
|
176
209
|
const age = Date.now() - parsed.fetchedAtWall;
|
|
177
|
-
if (age < this.offlineGraceMs) {
|
|
210
|
+
if (age < this.offlineGraceMs && verifySignature(parsed.response)) {
|
|
178
211
|
this.entitlements = parsed.response;
|
|
179
212
|
// Compute monotonic offset from wall-clock age so isWithinOfflineGrace()
|
|
180
213
|
// returns the correct remaining time (not a fresh 24h window on every restart)
|