@camstack/server 1.2.72 → 1.2.74

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.
@@ -98,15 +98,38 @@ function parseShareToken(data) {
98
98
  class ShareTokenService {
99
99
  getStore;
100
100
  logger;
101
+ /**
102
+ * Declaration is one-time per process; reset only if the backend itself
103
+ * is swapped (it never is at runtime — the getter is lazy solely because
104
+ * the sqlite builtin registers after this service is constructed).
105
+ */
106
+ collectionDeclared = false;
101
107
  constructor(getStore, logger = null) {
102
108
  this.getStore = getStore;
103
109
  this.logger = logger;
104
110
  }
105
- store() {
111
+ /**
112
+ * Resolve the backend AND ensure `share_view_tokens` is declared before
113
+ * the first operation. The structured settings backend fail-closes on
114
+ * undeclared collections; this KV shape (`id` PK + `data` TEXT) is
115
+ * byte-identical to what the legacy on-demand path created, so existing
116
+ * rows keep working with no migration.
117
+ */
118
+ async store() {
106
119
  const store = this.getStore();
107
120
  if (!store) {
108
121
  throw new Error('Share tokens unavailable — settings backend not ready');
109
122
  }
123
+ if (!this.collectionDeclared) {
124
+ await store.declareCollection({
125
+ collection: SHARE_TOKENS_COLLECTION,
126
+ columns: [
127
+ { name: 'id', type: 'TEXT', primaryKey: true, notNull: true },
128
+ { name: 'data', type: 'TEXT', notNull: true },
129
+ ],
130
+ });
131
+ this.collectionDeclared = true;
132
+ }
110
133
  return store;
111
134
  }
112
135
  /**
@@ -137,7 +160,7 @@ class ShareTokenService {
137
160
  createdAt: now,
138
161
  expiresAt: ttlSec === 'never' ? null : now + ttlSec * 1000,
139
162
  };
140
- await this.store().insert({
163
+ await (await this.store()).insert({
141
164
  collection: SHARE_TOKENS_COLLECTION,
142
165
  record: { id: record.id, data: { ...record } },
143
166
  });
@@ -161,7 +184,7 @@ class ShareTokenService {
161
184
  if (!rawToken.startsWith(exports.SHARE_TOKEN_PREFIX))
162
185
  return null;
163
186
  const tokenHash = crypto.createHash('sha256').update(rawToken).digest('hex');
164
- const results = await this.store().query({
187
+ const results = await (await this.store()).query({
165
188
  collection: SHARE_TOKENS_COLLECTION,
166
189
  filter: { where: { tokenHash } },
167
190
  });
@@ -186,7 +209,7 @@ class ShareTokenService {
186
209
  * permission mismatch so the router surfaces a FORBIDDEN.
187
210
  */
188
211
  async revoke(input) {
189
- const results = await this.store().query({
212
+ const results = await (await this.store()).query({
190
213
  collection: SHARE_TOKENS_COLLECTION,
191
214
  filter: { where: { id: input.id } },
192
215
  });
@@ -196,13 +219,13 @@ class ShareTokenService {
196
219
  const record = parseShareToken(first.data);
197
220
  if (!record) {
198
221
  // Corrupt record — delete it regardless (it can never validate).
199
- await this.store().delete({ collection: SHARE_TOKENS_COLLECTION, key: input.id });
222
+ await (await this.store()).delete({ collection: SHARE_TOKENS_COLLECTION, key: input.id });
200
223
  return true;
201
224
  }
202
225
  if (!input.callerIsAdmin && record.userId !== input.callerUserId) {
203
226
  throw new Error('Only the token owner or an admin can revoke a share token');
204
227
  }
205
- await this.store().delete({ collection: SHARE_TOKENS_COLLECTION, key: input.id });
228
+ await (await this.store()).delete({ collection: SHARE_TOKENS_COLLECTION, key: input.id });
206
229
  this.logger?.info('Share token revoked', {
207
230
  meta: { id: record.id, byUserId: input.callerUserId },
208
231
  });
@@ -210,7 +233,7 @@ class ShareTokenService {
210
233
  }
211
234
  /** Every share token minted by `userId`, expired ones included. */
212
235
  async listForUser(userId) {
213
- const results = await this.store().query({
236
+ const results = await (await this.store()).query({
214
237
  collection: SHARE_TOKENS_COLLECTION,
215
238
  filter: { where: { userId } },
216
239
  });
@@ -218,14 +241,14 @@ class ShareTokenService {
218
241
  }
219
242
  /** All share tokens (admin listing). */
220
243
  async listAll() {
221
- const results = await this.store().query({
244
+ const results = await (await this.store()).query({
222
245
  collection: SHARE_TOKENS_COLLECTION,
223
246
  filter: {},
224
247
  });
225
248
  return results.map((r) => parseShareToken(r.data)).filter((r) => r !== null);
226
249
  }
227
250
  async touchLastUsed(record) {
228
- await this.store().update({
251
+ await (await this.store()).update({
229
252
  collection: SHARE_TOKENS_COLLECTION,
230
253
  id: record.id,
231
254
  data: { ...record, lastUsedAt: Date.now() },
package/dist/main.js CHANGED
@@ -42,6 +42,7 @@ const ws_1 = require("@trpc/server/adapters/ws");
42
42
  const static_1 = __importDefault(require("@fastify/static"));
43
43
  const compress_1 = __importDefault(require("@fastify/compress"));
44
44
  const cookie_1 = __importDefault(require("@fastify/cookie"));
45
+ const sse_no_compression_js_1 = require("./api/sse-no-compression.js");
45
46
  const ws_2 = require("ws");
46
47
  const fs = __importStar(require("node:fs"));
47
48
  const path = __importStar(require("node:path"));
@@ -223,7 +224,16 @@ async function bootstrap() {
223
224
  // replaces — so the telemetry reaches a running hub the same day it is
224
225
  // written. See packages/system/src/kernel/heap-watch.ts for why it exists
225
226
  // (four silent OOMs in ~15h, one nine seconds after a viewer connected).
226
- (0, system_2.startHeapWatch)();
227
+ //
228
+ // The reclaimer is what turns the heartbeat from a report into a fix. hub-main's
229
+ // RSS is a HIGH-WATER MARK — measured over 1019 samples and seven boots, it never
230
+ // once fell below the running maximum of heapTotal+external — because V8 keeps the
231
+ // 256KB pages it committed at a sawtooth peak, and an ordinary major GC does not
232
+ // return them. `createV8Reclaimer()` returns undefined if V8 declines, and the
233
+ // heartbeat then behaves exactly as it did before.
234
+ const heapReclaimer = (0, system_2.createV8Reclaimer)();
235
+ const heapReclaim = heapReclaimer === undefined ? undefined : { reclaim: heapReclaimer };
236
+ (0, system_2.startHeapWatch)('hub-main', undefined, undefined, heapReclaim);
227
237
  // Clean up orphaned processes from previous crashes before starting
228
238
  cleanupOrphanProcesses();
229
239
  // SPA fallback — set later when admin UI is resolved, used by addon route catch-all
@@ -252,6 +262,13 @@ async function bootstrap() {
252
262
  // Registered before @fastify/static so the compress plugin wraps the
253
263
  // static send path — hashed admin-ui chunks go from ~2MB to ~600KB on
254
264
  // the wire. threshold:1024 skips compression for tiny payloads.
265
+ //
266
+ // SSE opt-out FIRST: without it a gzipped low-rate event stream starves
267
+ // silently in the compressor buffer — see `api/sse-no-compression.ts` for
268
+ // the mechanism and the live measurement.
269
+ fastify.addHook('onRequest', async (request) => {
270
+ (0, sse_no_compression_js_1.markSseRequestNoCompression)(request);
271
+ });
255
272
  await fastify.register(compress_1.default, { global: true, threshold: 1024 });
256
273
  // Data-plane POST bodies: the addon reverse-proxy (`proxyToUpstream`) pipes
257
274
  // `request.raw` upstream, but Fastify's default application/json parser would
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.72",
3
+ "version": "1.2.74",
4
4
  "private": false,
5
5
  "files": [
6
6
  "dist",
@@ -33,19 +33,19 @@
33
33
  ]
34
34
  },
35
35
  "dependencies": {
36
- "@camstack/addon-admin-ui": "1.2.33",
36
+ "@camstack/addon-admin-ui": "1.2.35",
37
37
  "@camstack/addon-agent-ui": "1.2.10",
38
38
  "@camstack/addon-auth": "1.2.11",
39
39
  "@camstack/addon-decoder-nodeav": "1.2.9",
40
40
  "@camstack/addon-notifiers": "1.2.13",
41
- "@camstack/addon-pipeline": "1.2.44",
42
- "@camstack/addon-pipeline-orchestrator": "1.2.27",
43
- "@camstack/addon-post-analysis": "1.2.49",
41
+ "@camstack/addon-pipeline": "1.2.46",
42
+ "@camstack/addon-pipeline-orchestrator": "1.2.29",
43
+ "@camstack/addon-post-analysis": "1.2.51",
44
44
  "@camstack/sdk": "1.2.10",
45
45
  "@camstack/shm-ring": "1.1.9",
46
- "@camstack/system": "1.2.59",
47
- "@camstack/types": "1.2.43",
48
- "@camstack/ui-library": "1.2.31",
46
+ "@camstack/system": "1.2.61",
47
+ "@camstack/types": "1.2.45",
48
+ "@camstack/ui-library": "1.2.33",
49
49
  "@fastify/compress": "^9.0.0",
50
50
  "@fastify/cookie": "^11.0.2",
51
51
  "@fastify/cors": "^11.2.0",