@chatpanel/events 0.47.0 → 0.49.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/entitlement.js +50 -16
- package/index.js +3 -0
- package/library.js +13 -4
- package/markdown-render.js +0 -0
- package/package.json +3 -1
package/entitlement.js
CHANGED
|
@@ -229,13 +229,26 @@ async function verifyKey(subtle) {
|
|
|
229
229
|
return _keyPromise;
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
/**
|
|
233
|
+
* The payload field names the WORKER actually signs. Not negotiable, and not guessable:
|
|
234
|
+
* `server/src/worker.js` grant() signs `{ typ:'ent', install_id, plan, sub, iat, exp }`,
|
|
235
|
+
* and the extension checks `p.typ !== 'ent' || p.install_id !== installId`.
|
|
236
|
+
*
|
|
237
|
+
* Written down here because getting them wrong fails in the worst possible way: the
|
|
238
|
+
* signature verifies, the token is genuine, and the binding check reads `undefined` — so
|
|
239
|
+
* every real token is rejected and the user is told their licence is invalid. That is
|
|
240
|
+
* exactly what happened when this module first read `payload.install`.
|
|
241
|
+
*/
|
|
242
|
+
export const TOKEN_TYPE = 'ent';
|
|
243
|
+
|
|
232
244
|
/**
|
|
233
245
|
* Verify a server entitlement token and return its payload, or `null`.
|
|
234
246
|
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
247
|
+
* Four checks, and all four matter: the SIGNATURE (it came from the worker), the TYPE (a
|
|
248
|
+
* `claim` token is also signed by the same key and must not be accepted as an entitlement),
|
|
249
|
+
* the INSTALL BINDING (issued to this device, so a token copied from a forum grants
|
|
250
|
+
* nothing), and EXPIRY. Returning null rather than throwing is deliberate — a caller
|
|
251
|
+
* resolving a licence at startup must degrade to free, not crash the app.
|
|
239
252
|
*/
|
|
240
253
|
export async function verifyEntitlement(token, installId, { subtle = null, now = Date.now() } = {}) {
|
|
241
254
|
if (!token || typeof token !== 'string' || token.indexOf('.') < 0) return null;
|
|
@@ -258,24 +271,42 @@ export async function verifyEntitlement(token, installId, { subtle = null, now =
|
|
|
258
271
|
|
|
259
272
|
const payload = decodePayload(head);
|
|
260
273
|
if (!payload) return null;
|
|
274
|
+
// The same key signs `claim` and `restore` tokens. Accepting one of those as an
|
|
275
|
+
// entitlement would turn a portable, install-independent token into a licence.
|
|
276
|
+
if (payload.typ !== TOKEN_TYPE) return null;
|
|
261
277
|
if (payload.exp && now > Number(payload.exp)) return null;
|
|
262
|
-
// `
|
|
263
|
-
// treated as universal — "unbound" must never be the permissive case.
|
|
264
|
-
if (!payload.
|
|
278
|
+
// `install_id` binds the token to one device. A token with no binding is refused rather
|
|
279
|
+
// than treated as universal — "unbound" must never be the permissive case.
|
|
280
|
+
if (!payload.install_id || (installId && payload.install_id !== installId)) return null;
|
|
265
281
|
if (!PLANS.includes(payload.plan)) return null;
|
|
266
282
|
return payload;
|
|
267
283
|
}
|
|
268
284
|
|
|
269
|
-
/**
|
|
270
|
-
|
|
285
|
+
/**
|
|
286
|
+
* A verified payload → the licence record a client stores.
|
|
287
|
+
*
|
|
288
|
+
* TWO EXPIRIES, AND CONFLATING THEM COSTS SOMEONE THEIR PRO.
|
|
289
|
+
*
|
|
290
|
+
* · `tokenExp` is the signed token's TTL — about a week. It exists so a revoked device
|
|
291
|
+
* stops working without the server having to reach it.
|
|
292
|
+
* · `expiresAt` is when the SUBSCRIPTION ends, which the server returns beside the token.
|
|
293
|
+
*
|
|
294
|
+
* `planOf` lapses a licence at `expiresAt`. Setting that from the token's TTL means anyone
|
|
295
|
+
* offline for longer than the TTL silently drops to Free while still paying — which is
|
|
296
|
+
* exactly the bug this signature now prevents by taking the subscription's date separately.
|
|
297
|
+
* With no server date the licence does not expire locally; the next successful check is
|
|
298
|
+
* what corrects it, and that is the safer direction to be wrong in.
|
|
299
|
+
*/
|
|
300
|
+
export function licenseFromPayload(payload, { at = Date.now(), expiresAt = 0 } = {}) {
|
|
271
301
|
if (!payload) return { plan: 'free' };
|
|
272
302
|
return {
|
|
273
303
|
plan: payload.plan,
|
|
274
|
-
expiresAt: Number(
|
|
275
|
-
|
|
276
|
-
|
|
304
|
+
expiresAt: Number(expiresAt) || 0,
|
|
305
|
+
tokenExp: Number(payload.exp) || 0,
|
|
306
|
+
installId: payload.install_id || '',
|
|
307
|
+
sub: payload.sub || null,
|
|
277
308
|
checkedAt: at,
|
|
278
|
-
source:
|
|
309
|
+
source: 'entitlement',
|
|
279
310
|
};
|
|
280
311
|
}
|
|
281
312
|
|
|
@@ -291,8 +322,11 @@ export const RECHECK_INTERVAL_MS = 12 * 60 * 60 * 1000;
|
|
|
291
322
|
export function needsRecheck(license, { now = Date.now(), interval = RECHECK_INTERVAL_MS } = {}) {
|
|
292
323
|
if (!license || !license.checkedAt) return true;
|
|
293
324
|
if (now - license.checkedAt > interval) return true;
|
|
294
|
-
// Inside the last day of
|
|
295
|
-
//
|
|
296
|
-
|
|
325
|
+
// Inside the last day of either deadline, check eagerly: a renewal lands near the
|
|
326
|
+
// subscription date, and the token has to be replaced before ITS ttl runs out or the
|
|
327
|
+
// client is left holding something it can no longer prove.
|
|
328
|
+
const soon = 24 * 60 * 60 * 1000;
|
|
329
|
+
if (license.expiresAt && license.expiresAt - now < soon) return true;
|
|
330
|
+
if (license.tokenExp && license.tokenExp - now < soon) return true;
|
|
297
331
|
return false;
|
|
298
332
|
}
|
package/index.js
CHANGED
|
@@ -208,3 +208,6 @@ export {
|
|
|
208
208
|
checkoutUrl, planOf, planLabel, isPro, isTeam, can, tierFor, withinFreeLimit,
|
|
209
209
|
verifyEntitlement, licenseFromPayload, needsRecheck,
|
|
210
210
|
} from './entitlement.js';
|
|
211
|
+
|
|
212
|
+
// One markdown renderer for every client — escaped first, link policy injected.
|
|
213
|
+
export { renderMarkdown, defaultLinkPolicy } from './markdown-render.js';
|
package/library.js
CHANGED
|
@@ -190,11 +190,20 @@ function bodyText(rec) {
|
|
|
190
190
|
if (b == null) return '';
|
|
191
191
|
if (typeof b === 'string') return b;
|
|
192
192
|
|
|
193
|
+
// A record may arrive FLATTENED rather than structured — the gateway's warm index stores
|
|
194
|
+
// `{ text }` for every kind, with no message list and no markdown. Falling through the
|
|
195
|
+
// kind-specific branches would project it to the empty string, which is worse than it
|
|
196
|
+
// sounds: the record is stored, listed and counted, but is invisible to search and shows
|
|
197
|
+
// an empty preview. So a flat `text` is honoured for any kind, and the structured
|
|
198
|
+
// extraction below wins whenever it actually finds something.
|
|
199
|
+
const flat = typeof b.text === 'string' ? b.text : '';
|
|
200
|
+
|
|
193
201
|
if (rec.kind === 'chat') {
|
|
194
|
-
|
|
202
|
+
const turns = (b.messages || [])
|
|
195
203
|
.filter((m) => m && m.content)
|
|
196
204
|
.map((m) => `${ROLE_LABEL[m.role] || 'You'}: ${textOfContent(m.content)}`)
|
|
197
205
|
.join('\n\n');
|
|
206
|
+
return turns || flat;
|
|
198
207
|
}
|
|
199
208
|
if (rec.kind === 'note') return str(b.markdown ?? b.text);
|
|
200
209
|
if (rec.kind === 'meeting') {
|
|
@@ -202,13 +211,13 @@ function bodyText(rec) {
|
|
|
202
211
|
const segs = (b.segments || [])
|
|
203
212
|
.map((s) => `${str(s.speaker) || '?'}: ${str(s.text)}`)
|
|
204
213
|
.join('\n');
|
|
205
|
-
return [notes, segs].filter(Boolean).join('\n\n');
|
|
214
|
+
return [notes, segs].filter(Boolean).join('\n\n') || flat;
|
|
206
215
|
}
|
|
207
216
|
if (rec.kind === 'brief') {
|
|
208
217
|
const claims = (b.claims || []).map((c) => str(c.text)).filter(Boolean).join('\n');
|
|
209
|
-
return [str(b.summary), claims].filter(Boolean).join('\n\n');
|
|
218
|
+
return [str(b.summary), claims].filter(Boolean).join('\n\n') || flat;
|
|
210
219
|
}
|
|
211
|
-
return
|
|
220
|
+
return flat;
|
|
212
221
|
}
|
|
213
222
|
|
|
214
223
|
/**
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/events",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"description": "The canonical ChatPanel event-log and capability contracts — typed durable facts, clock-free deterministic linearization, schema upcasting, and the invariants the replay harness asserts. Pure, dependency-free ESM shared by the ChatPanel extension, gateway and bridge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"./loop.js": "./loop.js",
|
|
27
27
|
"./manifest.js": "./manifest.js",
|
|
28
28
|
"./markdown-authoring.js": "./markdown-authoring.js",
|
|
29
|
+
"./markdown-render.js": "./markdown-render.js",
|
|
29
30
|
"./mcp-errors.js": "./mcp-errors.js",
|
|
30
31
|
"./media-transcript.js": "./media-transcript.js",
|
|
31
32
|
"./meeting-analyzers.js": "./meeting-analyzers.js",
|
|
@@ -97,6 +98,7 @@
|
|
|
97
98
|
"loop.js",
|
|
98
99
|
"manifest.js",
|
|
99
100
|
"markdown-authoring.js",
|
|
101
|
+
"markdown-render.js",
|
|
100
102
|
"mcp-errors.js",
|
|
101
103
|
"media-transcript.js",
|
|
102
104
|
"meeting-analyzers.js",
|