@buildproven/license-core 1.0.1 → 1.0.2

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.
Files changed (2) hide show
  1. package/README.md +99 -0
  2. package/package.json +2 -1
package/README.md CHANGED
@@ -153,6 +153,105 @@ openssl rsa -in private.pem -pubout -out public.pem
153
153
 
154
154
  The package never handles key generation, storage, or rotation — that's your call. Use whatever secret manager you already have.
155
155
 
156
+ ### Rotating a signing key
157
+
158
+ Every entry in the registry has a `keyId` field, and `_metadata.keyId` records which key signed the registry as a whole. To rotate without breaking already-shipped clients:
159
+
160
+ 1. Generate a new keypair. Add the new private key to your fulfillment env vars (e.g. `LICENSE_PRIVATE_KEY_V2`).
161
+ 2. Update fulfillment to sign new entries with the new key, passing `keyId: 'v2'`:
162
+
163
+ ```ts
164
+ buildSignedRegistry(entries, NEW_PRIVATE_KEY, 'v2');
165
+ ```
166
+
167
+ 3. **Bundle BOTH public keys with your client.** When verifying, look up the right key based on `entry.keyId`:
168
+
169
+ ```ts
170
+ const publicKeys = { default: OLD_PUBLIC_KEY, v2: NEW_PUBLIC_KEY };
171
+ const result = validateRegistryEntry({
172
+ licenseKey,
173
+ entry,
174
+ publicKeyPem: publicKeys[entry.keyId] ?? publicKeys.default,
175
+ });
176
+ ```
177
+
178
+ 4. Existing entries (signed with `keyId: 'default'`) continue to verify under the old public key. New entries verify under the new key.
179
+ 5. Once all old entries have expired or been re-issued, you can ship a client release that drops the old public key.
180
+
181
+ The package leaves `keyId` as a free-form string, so use whatever convention you like (`v1`/`v2`, dates, fingerprints).
182
+
183
+ ## Recurring billing & subscriptions
184
+
185
+ The frozen-contract v1.x API doesn't include `expiresAt` on payloads, so subscriptions need a layer above the package. Two patterns work:
186
+
187
+ ### Pattern A — short-lived registries (re-sign on a schedule)
188
+
189
+ Your fulfillment service holds the source-of-truth subscription state (Stripe, paddle, whatever). On a cron/interval (e.g. once an hour), it walks active customers, builds a fresh `Registry`, calls `buildSignedRegistry`, and serves the result. Cancelled customers simply stop appearing in the next signed registry.
190
+
191
+ The client refreshes the registry once per launch + once per N hours/days, and falls back to the locally-cached signed JSON if offline. A 7-day grace period (registry valid 7 days offline) is typical — long enough that flaky internet doesn't lock paying customers out, short enough that cancellations propagate within a week.
192
+
193
+ ```ts
194
+ // Client side
195
+ async function getRegistry() {
196
+ try {
197
+ const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(5000) });
198
+ const signed = await res.json();
199
+ const entries = verifyRegistryMetadata(signed, PUBLIC_KEY);
200
+ await fs.writeFile(CACHE_PATH, JSON.stringify(signed)); // cache for offline
201
+ return entries;
202
+ } catch {
203
+ const cached = JSON.parse(await fs.readFile(CACHE_PATH, 'utf8'));
204
+ return verifyRegistryMetadata(cached, PUBLIC_KEY); // offline path
205
+ }
206
+ }
207
+ ```
208
+
209
+ ### Pattern B — long-lived entries + revocation list
210
+
211
+ Entries are signed once and stay valid forever. A separate signed `revoked.json` lists keys that should no longer verify. The client fetches both and rejects any license that appears in the revocations file.
212
+
213
+ This is faster on the fulfillment side (you don't re-sign the whole registry every hour) but requires more client logic. Tracked in the v2.x roadmap because v1.x doesn't ship a revocation helper yet.
214
+
215
+ ### Which pattern when?
216
+
217
+ - **Few customers, frequent cancellations:** Pattern A — re-signing daily is cheap.
218
+ - **Many customers, rare cancellations:** Pattern B — appending to a small revocations list is cheaper than re-signing thousands of entries.
219
+ - **Lifetime licenses only:** neither — sign once on purchase, never re-sign.
220
+
221
+ ## Publishing your own fork (Trusted Publishing setup)
222
+
223
+ If you fork this and want to publish under your own scope via npm Trusted Publishing (no `NPM_TOKEN`), there's one gotcha worth documenting because it cost an hour during the initial release here:
224
+
225
+ **Don't pass `registry-url` to `actions/setup-node`.** It auto-generates an `.npmrc` with a placeholder `${NODE_AUTH_TOKEN}` value, which makes `npm publish` authenticate via that fake token instead of falling through to OIDC. Result: a `404 Not Found` from the registry that looks like a misconfigured trusted publisher.
226
+
227
+ The minimal working workflow:
228
+
229
+ ```yaml
230
+ name: Publish to npm
231
+ on:
232
+ push:
233
+ tags: ['v*.*.*']
234
+
235
+ jobs:
236
+ publish:
237
+ runs-on: ubuntu-latest
238
+ permissions:
239
+ contents: read
240
+ id-token: write # required for OIDC
241
+ steps:
242
+ - uses: actions/checkout@v4
243
+ - uses: actions/setup-node@v4
244
+ with:
245
+ node-version: '22'
246
+ # NO registry-url here — it would inject a fake NODE_AUTH_TOKEN
247
+ - run: npm install -g npm@latest # need >=11.5.1 for Trusted Publishing
248
+ - run: npm ci
249
+ - run: npm test
250
+ - run: npm publish --access public --provenance
251
+ ```
252
+
253
+ On the npm side, configure the trusted publisher under your package's settings page after the package exists (chicken-and-egg: do the *first* publish via a granular access token, then switch to OIDC for everything after).
254
+
156
255
  ## License
157
256
 
158
257
  [MIT](./LICENSE) © Vibe Build Lab LLC
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buildproven/license-core",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Shared license signing & verification primitives for BuildProven products (RSA-SHA256, signed registry).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -29,6 +29,7 @@
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^22.0.0",
32
+ "fast-check": "^4.7.0",
32
33
  "prettier": "^3.0.0",
33
34
  "tsup": "^8.0.0",
34
35
  "typescript": "^5.4.0",