ccpool-server 0.0.1 → 0.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.
- package/dist/index.js +204 -108
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ function isValidName(name) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
// ../../packages/core/dist/storage/storage.js
|
|
23
|
-
var SCHEMA_VERSION =
|
|
23
|
+
var SCHEMA_VERSION = 2;
|
|
24
24
|
var DEFAULT_GROUP_ID = "default";
|
|
25
25
|
function isEmptyBatch(b) {
|
|
26
26
|
return b.samples.length === 0 && b.resets.length === 0 && b.messages.length === 0 && b.markers.length === 0;
|
|
@@ -57,19 +57,42 @@ function attributeShares(samples, messages, now = Date.now(), resets = [], marke
|
|
|
57
57
|
const capSamples = samples.filter((s) => s.cap === cap).map((s) => ({ t: Date.parse(s.capturedAt), pct: s.pct })).filter((s) => Number.isFinite(s.t)).sort((a, b) => a.t - b.t);
|
|
58
58
|
if (capSamples.length === 0)
|
|
59
59
|
continue;
|
|
60
|
-
const
|
|
61
|
-
out.push(...attributeCap(cap, capSamples, msgs, mks, now,
|
|
60
|
+
const capResets = resets.filter((r) => r.cap === cap).map((r) => ({ t: Date.parse(r.at), previousPct: r.previousPct })).filter((r) => Number.isFinite(r.t) && r.t <= now);
|
|
61
|
+
out.push(...attributeCap(cap, capSamples, msgs, mks, now, capResets));
|
|
62
62
|
}
|
|
63
63
|
return out;
|
|
64
64
|
}
|
|
65
|
-
|
|
65
|
+
var RESET_DROP_MIN_PCT = 2;
|
|
66
|
+
function resolveResetBoundary(capSamples, resetEvents) {
|
|
67
|
+
let boundary = -Infinity;
|
|
68
|
+
for (const e of resetEvents) {
|
|
69
|
+
if (e.t <= boundary)
|
|
70
|
+
continue;
|
|
71
|
+
let before = null;
|
|
72
|
+
let after = null;
|
|
73
|
+
for (const s of capSamples) {
|
|
74
|
+
if (s.t < e.t)
|
|
75
|
+
before = s.pct;
|
|
76
|
+
else {
|
|
77
|
+
after = s.pct;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const beforeLevel = before ?? e.previousPct;
|
|
82
|
+
const corroborated = after === null || beforeLevel - after > RESET_DROP_MIN_PCT;
|
|
83
|
+
if (corroborated)
|
|
84
|
+
boundary = e.t;
|
|
85
|
+
}
|
|
86
|
+
return boundary;
|
|
87
|
+
}
|
|
88
|
+
function attributeCap(cap, capSamples, msgs, markers, now, resetEvents) {
|
|
66
89
|
const cutoff = now - CAP_WINDOW_MS[cap];
|
|
67
90
|
let start = 0;
|
|
68
91
|
for (let i = 1; i < capSamples.length; i++) {
|
|
69
92
|
if (capSamples[i].t < cutoff)
|
|
70
93
|
start = i;
|
|
71
94
|
}
|
|
72
|
-
const lastReset =
|
|
95
|
+
const lastReset = resolveResetBoundary(capSamples, resetEvents);
|
|
73
96
|
if (lastReset > -Infinity) {
|
|
74
97
|
let firstAfter = capSamples.length - 1;
|
|
75
98
|
for (let i = 0; i < capSamples.length; i++) {
|
|
@@ -226,10 +249,104 @@ var AccountConflictError = class extends Error {
|
|
|
226
249
|
// ../../packages/core/dist/remote/api.js
|
|
227
250
|
var MIN_PASSWORD_LENGTH = 8;
|
|
228
251
|
|
|
229
|
-
// ../../packages/core/dist/backend/
|
|
230
|
-
var DEFAULT_PRUNE_INTERVAL_MS = 6 * 36e5;
|
|
252
|
+
// ../../packages/core/dist/backend/finalizer.js
|
|
231
253
|
var DEFAULT_GRACE_MS = 30 * 6e4;
|
|
232
254
|
var DEFAULT_FINALIZE_INTERVAL_MS = 6e4;
|
|
255
|
+
var HistoryFinalizer = class {
|
|
256
|
+
storage;
|
|
257
|
+
graceMs;
|
|
258
|
+
finalizeIntervalMs;
|
|
259
|
+
retentionMs;
|
|
260
|
+
now;
|
|
261
|
+
lastFinalizeMs = 0;
|
|
262
|
+
/** `${cap} ${windowStart}` of windows already frozen — skip recompute. */
|
|
263
|
+
frozen = /* @__PURE__ */ new Set();
|
|
264
|
+
constructor(storage, opts = {}) {
|
|
265
|
+
this.storage = storage;
|
|
266
|
+
this.graceMs = opts.graceMs ?? DEFAULT_GRACE_MS;
|
|
267
|
+
this.finalizeIntervalMs = opts.finalizeIntervalMs ?? DEFAULT_FINALIZE_INTERVAL_MS;
|
|
268
|
+
this.retentionMs = opts.retentionMs ?? RETENTION_MS;
|
|
269
|
+
this.now = opts.now ?? Date.now;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Freeze any window whose closing reset is now past the grace period. Throttled.
|
|
273
|
+
* A window spans two consecutive resets of the cap (the first opens at the
|
|
274
|
+
* earliest retained sample); shares come from the same `attributeShares` the live
|
|
275
|
+
* view uses.
|
|
276
|
+
*/
|
|
277
|
+
async maybeFinalize(now = this.now()) {
|
|
278
|
+
if (now - this.lastFinalizeMs < this.finalizeIntervalMs)
|
|
279
|
+
return;
|
|
280
|
+
this.lastFinalizeMs = now;
|
|
281
|
+
const since = new Date(now - this.retentionMs).toISOString();
|
|
282
|
+
const resets = await this.storage.getResetsSince(since);
|
|
283
|
+
if (resets.length === 0)
|
|
284
|
+
return;
|
|
285
|
+
const capResets = /* @__PURE__ */ new Map();
|
|
286
|
+
for (const r of resets) {
|
|
287
|
+
const t = Date.parse(r.at);
|
|
288
|
+
if (!Number.isFinite(t))
|
|
289
|
+
continue;
|
|
290
|
+
const arr = capResets.get(r.cap);
|
|
291
|
+
if (arr)
|
|
292
|
+
arr.push(t);
|
|
293
|
+
else
|
|
294
|
+
capResets.set(r.cap, [t]);
|
|
295
|
+
}
|
|
296
|
+
let samples = null;
|
|
297
|
+
let messages = null;
|
|
298
|
+
let markers = null;
|
|
299
|
+
for (const [cap, times] of capResets) {
|
|
300
|
+
times.sort((a, b) => a - b);
|
|
301
|
+
for (let i = 0; i < times.length; i++) {
|
|
302
|
+
const endMs = times[i];
|
|
303
|
+
if (endMs + this.graceMs > now)
|
|
304
|
+
continue;
|
|
305
|
+
if (samples === null) {
|
|
306
|
+
[samples, messages, markers] = await Promise.all([
|
|
307
|
+
this.storage.getUsageSamplesSince(since),
|
|
308
|
+
this.storage.getMessageUsageSince(since),
|
|
309
|
+
this.storage.getUsageMarkersSince(since).catch(() => [])
|
|
310
|
+
]);
|
|
311
|
+
}
|
|
312
|
+
const capSamples = samples.filter((s) => s.cap === cap);
|
|
313
|
+
const startMs = i > 0 ? times[i - 1] : Math.min(...capSamples.map((s) => Date.parse(s.capturedAt)).filter(Number.isFinite));
|
|
314
|
+
if (!Number.isFinite(startMs) || startMs >= endMs)
|
|
315
|
+
continue;
|
|
316
|
+
const windowStart = new Date(startMs).toISOString();
|
|
317
|
+
const key = `${cap} ${windowStart}`;
|
|
318
|
+
if (this.frozen.has(key))
|
|
319
|
+
continue;
|
|
320
|
+
const windowEnd = new Date(endMs).toISOString();
|
|
321
|
+
const inWin = (iso) => {
|
|
322
|
+
const t = Date.parse(iso);
|
|
323
|
+
return t >= startMs && t < endMs;
|
|
324
|
+
};
|
|
325
|
+
const winSamples = capSamples.filter((s) => inWin(s.capturedAt));
|
|
326
|
+
if (winSamples.length === 0) {
|
|
327
|
+
this.frozen.add(key);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const winMsgs = messages.filter((m) => inWin(m.timestamp));
|
|
331
|
+
const winMarkers = markers.filter((m) => inWin(m.at));
|
|
332
|
+
const openingReset = [{ cap, at: windowStart, previousPct: 0 }];
|
|
333
|
+
const shares = attributeShares(winSamples, winMsgs, endMs, openingReset, winMarkers).filter((sh) => sh.cap === cap);
|
|
334
|
+
const overall = Math.max(...winSamples.map((s) => s.pct));
|
|
335
|
+
const historyShares = shares.map((sh) => ({
|
|
336
|
+
cap,
|
|
337
|
+
windowStart,
|
|
338
|
+
user: sh.user,
|
|
339
|
+
pct: sh.pct
|
|
340
|
+
}));
|
|
341
|
+
await this.storage.recordHistoryWindow({ cap, windowStart, windowEnd, overall, closedAt: new Date(now).toISOString() }, historyShares);
|
|
342
|
+
this.frozen.add(key);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// ../../packages/core/dist/backend/storage.js
|
|
349
|
+
var DEFAULT_PRUNE_INTERVAL_MS = 6 * 36e5;
|
|
233
350
|
var EnvelopeFilter = class {
|
|
234
351
|
envMax = /* @__PURE__ */ new Map();
|
|
235
352
|
winStart = /* @__PURE__ */ new Map();
|
|
@@ -269,22 +386,22 @@ var StorageIngestSink = class {
|
|
|
269
386
|
lastPruneMs;
|
|
270
387
|
retentionMs;
|
|
271
388
|
pruneIntervalMs;
|
|
272
|
-
graceMs;
|
|
273
|
-
finalizeIntervalMs;
|
|
274
|
-
lastFinalizeMs = 0;
|
|
275
|
-
/** `${cap} ${windowStart}` of windows already frozen — skip recompute. */
|
|
276
|
-
frozen = /* @__PURE__ */ new Set();
|
|
277
389
|
now;
|
|
278
390
|
window;
|
|
391
|
+
finalizer;
|
|
279
392
|
envelope = new EnvelopeFilter();
|
|
280
393
|
constructor(storage, opts = {}) {
|
|
281
394
|
this.storage = storage;
|
|
282
395
|
this.retentionMs = opts.retentionMs ?? RETENTION_MS;
|
|
283
396
|
this.pruneIntervalMs = opts.pruneIntervalMs ?? DEFAULT_PRUNE_INTERVAL_MS;
|
|
284
|
-
this.graceMs = opts.graceMs ?? DEFAULT_GRACE_MS;
|
|
285
|
-
this.finalizeIntervalMs = opts.finalizeIntervalMs ?? DEFAULT_FINALIZE_INTERVAL_MS;
|
|
286
397
|
this.now = opts.now ?? Date.now;
|
|
287
398
|
this.window = opts.window;
|
|
399
|
+
this.finalizer = opts.finalizer ?? new HistoryFinalizer(storage, {
|
|
400
|
+
graceMs: opts.graceMs,
|
|
401
|
+
finalizeIntervalMs: opts.finalizeIntervalMs,
|
|
402
|
+
retentionMs: opts.retentionMs,
|
|
403
|
+
now: opts.now
|
|
404
|
+
});
|
|
288
405
|
this.lastPruneMs = this.now();
|
|
289
406
|
}
|
|
290
407
|
/**
|
|
@@ -316,7 +433,7 @@ var StorageIngestSink = class {
|
|
|
316
433
|
this.window?.append(reduced);
|
|
317
434
|
}
|
|
318
435
|
const now = this.now();
|
|
319
|
-
await this.maybeFinalize(now);
|
|
436
|
+
await this.finalizer.maybeFinalize(now);
|
|
320
437
|
if (now - this.lastPruneMs >= this.pruneIntervalMs) {
|
|
321
438
|
this.lastPruneMs = now;
|
|
322
439
|
const before = new Date(now - this.retentionMs).toISOString();
|
|
@@ -324,82 +441,6 @@ var StorageIngestSink = class {
|
|
|
324
441
|
this.window?.applyPrune(before);
|
|
325
442
|
}
|
|
326
443
|
}
|
|
327
|
-
/**
|
|
328
|
-
* Freeze any window whose closing reset is now past the grace period into an
|
|
329
|
-
* immutable {@link HistoryWindow} + shares (ADR-0002/0008). Throttled (grace is 30
|
|
330
|
-
* min). A window spans two consecutive resets of the cap (the first opens at the
|
|
331
|
-
* earliest retained sample); shares come from the same `attributeShares` the live
|
|
332
|
-
* view uses. `recordHistoryWindow` is idempotent and `frozen` skips recompute.
|
|
333
|
-
*/
|
|
334
|
-
async maybeFinalize(now) {
|
|
335
|
-
if (now - this.lastFinalizeMs < this.finalizeIntervalMs)
|
|
336
|
-
return;
|
|
337
|
-
this.lastFinalizeMs = now;
|
|
338
|
-
const since = new Date(now - this.retentionMs).toISOString();
|
|
339
|
-
const resets = await this.storage.getResetsSince(since);
|
|
340
|
-
if (resets.length === 0)
|
|
341
|
-
return;
|
|
342
|
-
const capResets = /* @__PURE__ */ new Map();
|
|
343
|
-
for (const r of resets) {
|
|
344
|
-
const t = Date.parse(r.at);
|
|
345
|
-
if (!Number.isFinite(t))
|
|
346
|
-
continue;
|
|
347
|
-
const arr = capResets.get(r.cap);
|
|
348
|
-
if (arr)
|
|
349
|
-
arr.push(t);
|
|
350
|
-
else
|
|
351
|
-
capResets.set(r.cap, [t]);
|
|
352
|
-
}
|
|
353
|
-
let samples = null;
|
|
354
|
-
let messages = null;
|
|
355
|
-
let markers = null;
|
|
356
|
-
for (const [cap, times] of capResets) {
|
|
357
|
-
times.sort((a, b) => a - b);
|
|
358
|
-
for (let i = 0; i < times.length; i++) {
|
|
359
|
-
const endMs = times[i];
|
|
360
|
-
if (endMs + this.graceMs > now)
|
|
361
|
-
continue;
|
|
362
|
-
if (samples === null) {
|
|
363
|
-
[samples, messages, markers] = await Promise.all([
|
|
364
|
-
this.storage.getUsageSamplesSince(since),
|
|
365
|
-
this.storage.getMessageUsageSince(since),
|
|
366
|
-
this.storage.getUsageMarkersSince(since).catch(() => [])
|
|
367
|
-
]);
|
|
368
|
-
}
|
|
369
|
-
const capSamples = samples.filter((s) => s.cap === cap);
|
|
370
|
-
const startMs = i > 0 ? times[i - 1] : Math.min(...capSamples.map((s) => Date.parse(s.capturedAt)).filter(Number.isFinite));
|
|
371
|
-
if (!Number.isFinite(startMs) || startMs >= endMs)
|
|
372
|
-
continue;
|
|
373
|
-
const windowStart = new Date(startMs).toISOString();
|
|
374
|
-
const key = `${cap} ${windowStart}`;
|
|
375
|
-
if (this.frozen.has(key))
|
|
376
|
-
continue;
|
|
377
|
-
const windowEnd = new Date(endMs).toISOString();
|
|
378
|
-
const inWin = (iso) => {
|
|
379
|
-
const t = Date.parse(iso);
|
|
380
|
-
return t >= startMs && t < endMs;
|
|
381
|
-
};
|
|
382
|
-
const winSamples = capSamples.filter((s) => inWin(s.capturedAt));
|
|
383
|
-
if (winSamples.length === 0) {
|
|
384
|
-
this.frozen.add(key);
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
const winMsgs = messages.filter((m) => inWin(m.timestamp));
|
|
388
|
-
const winMarkers = markers.filter((m) => inWin(m.at));
|
|
389
|
-
const openingReset = [{ cap, at: windowStart, previousPct: 0 }];
|
|
390
|
-
const shares = attributeShares(winSamples, winMsgs, endMs, openingReset, winMarkers).filter((sh) => sh.cap === cap);
|
|
391
|
-
const overall = Math.max(...winSamples.map((s) => s.pct));
|
|
392
|
-
const historyShares = shares.map((sh) => ({
|
|
393
|
-
cap,
|
|
394
|
-
windowStart,
|
|
395
|
-
user: sh.user,
|
|
396
|
-
pct: sh.pct
|
|
397
|
-
}));
|
|
398
|
-
await this.storage.recordHistoryWindow({ cap, windowStart, windowEnd, overall, closedAt: new Date(now).toISOString() }, historyShares);
|
|
399
|
-
this.frozen.add(key);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
444
|
async close() {
|
|
404
445
|
await this.storage.close();
|
|
405
446
|
}
|
|
@@ -422,16 +463,22 @@ var StorageViewSource = class {
|
|
|
422
463
|
before: query.before,
|
|
423
464
|
limit
|
|
424
465
|
});
|
|
425
|
-
const
|
|
466
|
+
const shares = await this.storage.getHistorySharesForWindows(query.cap, windows.map((w) => w.windowStart));
|
|
467
|
+
const byWindow = /* @__PURE__ */ new Map();
|
|
468
|
+
for (const s of shares) {
|
|
469
|
+
const arr = byWindow.get(s.windowStart);
|
|
470
|
+
if (arr)
|
|
471
|
+
arr.push({ user: s.user, pct: s.pct });
|
|
472
|
+
else
|
|
473
|
+
byWindow.set(s.windowStart, [{ user: s.user, pct: s.pct }]);
|
|
474
|
+
}
|
|
475
|
+
const view = windows.map((w) => ({
|
|
426
476
|
cap: w.cap,
|
|
427
477
|
windowStart: w.windowStart,
|
|
428
478
|
windowEnd: w.windowEnd,
|
|
429
479
|
overall: w.overall,
|
|
430
|
-
shares:
|
|
431
|
-
|
|
432
|
-
pct: s.pct
|
|
433
|
-
}))
|
|
434
|
-
})));
|
|
480
|
+
shares: byWindow.get(w.windowStart) ?? []
|
|
481
|
+
}));
|
|
435
482
|
const nextBefore = windows.length === limit && windows.length > 0 ? windows[windows.length - 1].windowStart : null;
|
|
436
483
|
return { windows: view, nextBefore };
|
|
437
484
|
}
|
|
@@ -868,6 +915,8 @@ var MAX_INGEST_BYTES = 1024 * 1024;
|
|
|
868
915
|
var TOKEN_TOUCH_INTERVAL_MS = 6e4;
|
|
869
916
|
var TOKEN_CACHE_TTL_MS = 6e4;
|
|
870
917
|
var TOKEN_CACHE_MAX = 2e4;
|
|
918
|
+
var TOKEN_STALE_MS = 90 * 24 * 36e5;
|
|
919
|
+
var TOKEN_SWEEP_INTERVAL_MS = 6 * 36e5;
|
|
871
920
|
function err(c, status, code, error) {
|
|
872
921
|
return c.json({ error, code }, status);
|
|
873
922
|
}
|
|
@@ -897,6 +946,7 @@ function makeApp(deps2) {
|
|
|
897
946
|
const damper = new FailureDamper();
|
|
898
947
|
const lastTouch = /* @__PURE__ */ new Map();
|
|
899
948
|
const tokenCache = /* @__PURE__ */ new Map();
|
|
949
|
+
let lastTokenSweep = 0;
|
|
900
950
|
const damperKey = (accountId) => accountId;
|
|
901
951
|
app2.get("/healthz", (c) => c.json({ ok: true }));
|
|
902
952
|
app2.use("/v1/ingest", bearer);
|
|
@@ -925,10 +975,21 @@ function makeApp(deps2) {
|
|
|
925
975
|
tokenCache.set(tokenHash, hit);
|
|
926
976
|
}
|
|
927
977
|
if (now - (lastTouch.get(tokenHash) ?? 0) >= TOKEN_TOUCH_INTERVAL_MS) {
|
|
978
|
+
lastTouch.delete(tokenHash);
|
|
928
979
|
lastTouch.set(tokenHash, now);
|
|
980
|
+
if (lastTouch.size > TOKEN_CACHE_MAX) {
|
|
981
|
+
const oldest = lastTouch.keys().next().value;
|
|
982
|
+
if (oldest !== void 0) lastTouch.delete(oldest);
|
|
983
|
+
}
|
|
929
984
|
await registry.touchToken(tokenHash).catch(() => {
|
|
930
985
|
});
|
|
931
986
|
}
|
|
987
|
+
if (now - lastTokenSweep >= TOKEN_SWEEP_INTERVAL_MS) {
|
|
988
|
+
lastTokenSweep = now;
|
|
989
|
+
const before = new Date(now - TOKEN_STALE_MS).toISOString();
|
|
990
|
+
void registry.deleteStaleTokens(before).catch(() => {
|
|
991
|
+
});
|
|
992
|
+
}
|
|
932
993
|
c.set("member", hit.member);
|
|
933
994
|
c.set("group", hit.group);
|
|
934
995
|
await next();
|
|
@@ -969,7 +1030,7 @@ function makeApp(deps2) {
|
|
|
969
1030
|
if (e instanceof RegistryConflictError) {
|
|
970
1031
|
return err(c, 409, "conflict", "a group for this Claude account already exists \u2014 join it");
|
|
971
1032
|
}
|
|
972
|
-
return err(c, 500, "
|
|
1033
|
+
return err(c, 500, "server", `could not provision the group: ${e.message}`);
|
|
973
1034
|
}
|
|
974
1035
|
});
|
|
975
1036
|
app2.post("/v1/groups/join", async (c) => {
|
|
@@ -1064,7 +1125,7 @@ function makeApp(deps2) {
|
|
|
1064
1125
|
if (e instanceof AccountConflictError) {
|
|
1065
1126
|
return err(c, 409, "account-conflict", e.message);
|
|
1066
1127
|
}
|
|
1067
|
-
return err(c, 500, "
|
|
1128
|
+
return err(c, 500, "server", e.message);
|
|
1068
1129
|
}
|
|
1069
1130
|
return c.body(null, 204);
|
|
1070
1131
|
});
|
|
@@ -1078,6 +1139,8 @@ function makeApp(deps2) {
|
|
|
1078
1139
|
const group = c.get("group");
|
|
1079
1140
|
const tenant = await tenants.get(group);
|
|
1080
1141
|
const now = Date.now();
|
|
1142
|
+
await tenant.finalizer.maybeFinalize(now).catch(() => {
|
|
1143
|
+
});
|
|
1081
1144
|
const etag = `"${await tenant.view.currentKey(now)}"`;
|
|
1082
1145
|
if (c.req.header("if-none-match") === etag) {
|
|
1083
1146
|
return c.body(null, 304, { ETag: etag });
|
|
@@ -1092,6 +1155,8 @@ function makeApp(deps2) {
|
|
|
1092
1155
|
const limitRaw = Number(c.req.query("limit"));
|
|
1093
1156
|
const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? Math.min(200, Math.floor(limitRaw)) : 50;
|
|
1094
1157
|
const tenant = await tenants.get(c.get("group"));
|
|
1158
|
+
await tenant.finalizer.maybeFinalize(Date.now()).catch(() => {
|
|
1159
|
+
});
|
|
1095
1160
|
const page = await tenant.view.history({ cap: capQ, before, limit });
|
|
1096
1161
|
return c.json(page);
|
|
1097
1162
|
});
|
|
@@ -1157,7 +1222,7 @@ var LEDGER_DDL = [
|
|
|
1157
1222
|
)`,
|
|
1158
1223
|
`CREATE INDEX IF NOT EXISTS idx_reset_events_at ON reset_events (group_id, at)`,
|
|
1159
1224
|
`CREATE UNIQUE INDEX IF NOT EXISTS idx_reset_events_uniq ON reset_events (group_id, cap, at)`,
|
|
1160
|
-
// Immutable history of completed cap cycles
|
|
1225
|
+
// Immutable history of completed cap cycles. Retained unbounded.
|
|
1161
1226
|
`CREATE TABLE IF NOT EXISTS history_windows (
|
|
1162
1227
|
group_id TEXT NOT NULL,
|
|
1163
1228
|
cap TEXT NOT NULL,
|
|
@@ -1269,6 +1334,21 @@ var LibsqlRegistry = class {
|
|
|
1269
1334
|
args: [(/* @__PURE__ */ new Date()).toISOString(), tokenHash]
|
|
1270
1335
|
});
|
|
1271
1336
|
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Bearer retention: delete every token last used (or, if never used, created)
|
|
1339
|
+
* before `before` (ISO 8601), returning how many were removed. A live daemon
|
|
1340
|
+
* touches its token ~once a minute, so a token untouched for the whole window is
|
|
1341
|
+
* genuinely abandoned — this bounds the `tokens` table without logging anyone
|
|
1342
|
+
* active out. `COALESCE(lastUsedAt, createdAt)` covers a token minted but never
|
|
1343
|
+
* used (login/rejoin that was thrown away).
|
|
1344
|
+
*/
|
|
1345
|
+
async deleteStaleTokens(before) {
|
|
1346
|
+
const { rowsAffected } = await this.client.execute({
|
|
1347
|
+
sql: `DELETE FROM tokens WHERE COALESCE(lastUsedAt, createdAt) < ?`,
|
|
1348
|
+
args: [before]
|
|
1349
|
+
});
|
|
1350
|
+
return rowsAffected;
|
|
1351
|
+
}
|
|
1272
1352
|
async createGroupWithMember(input) {
|
|
1273
1353
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1274
1354
|
const group = {
|
|
@@ -1663,12 +1743,18 @@ var LibsqlStorage = class {
|
|
|
1663
1743
|
WHERE group_id = ? AND cap = ? AND windowStart = ?`,
|
|
1664
1744
|
args: [this.groupId, cap, windowStart]
|
|
1665
1745
|
});
|
|
1666
|
-
return rows.map(
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1746
|
+
return rows.map(toHistoryShare);
|
|
1747
|
+
}
|
|
1748
|
+
async getHistorySharesForWindows(cap, windowStarts) {
|
|
1749
|
+
if (windowStarts.length === 0)
|
|
1750
|
+
return [];
|
|
1751
|
+
const placeholders = windowStarts.map(() => "?").join(", ");
|
|
1752
|
+
const { rows } = await this.client.execute({
|
|
1753
|
+
sql: `SELECT cap, windowStart, user, pct FROM history_shares
|
|
1754
|
+
WHERE group_id = ? AND cap = ? AND windowStart IN (${placeholders})`,
|
|
1755
|
+
args: [this.groupId, cap, ...windowStarts]
|
|
1756
|
+
});
|
|
1757
|
+
return rows.map(toHistoryShare);
|
|
1672
1758
|
}
|
|
1673
1759
|
/** Every write batch ends with this so one tick bumps this group's token once. */
|
|
1674
1760
|
bumpWriteSeq() {
|
|
@@ -1678,6 +1764,14 @@ var LibsqlStorage = class {
|
|
|
1678
1764
|
};
|
|
1679
1765
|
}
|
|
1680
1766
|
};
|
|
1767
|
+
function toHistoryShare(r) {
|
|
1768
|
+
return {
|
|
1769
|
+
cap: r.cap,
|
|
1770
|
+
windowStart: String(r.windowStart),
|
|
1771
|
+
user: String(r.user),
|
|
1772
|
+
pct: Number(r.pct)
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1681
1775
|
|
|
1682
1776
|
// ../../packages/storage-libsql/dist/url.js
|
|
1683
1777
|
import { mkdirSync } from "fs";
|
|
@@ -1769,10 +1863,12 @@ var TenantCache = class {
|
|
|
1769
1863
|
}
|
|
1770
1864
|
const storage = this.db.forGroup(group.id);
|
|
1771
1865
|
const window = new LedgerWindow(storage);
|
|
1866
|
+
const finalizer = new HistoryFinalizer(storage);
|
|
1772
1867
|
const entry = {
|
|
1773
1868
|
tenant: {
|
|
1774
|
-
sink: new StorageIngestSink(storage, { window }),
|
|
1775
|
-
view: new StorageViewSource(storage, { window })
|
|
1869
|
+
sink: new StorageIngestSink(storage, { window, finalizer }),
|
|
1870
|
+
view: new StorageViewSource(storage, { window }),
|
|
1871
|
+
finalizer
|
|
1776
1872
|
}
|
|
1777
1873
|
};
|
|
1778
1874
|
this.tenants.set(group.id, entry);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccpool-server",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.2",
|
|
5
5
|
"description": "Multi-tenant HTTP server for ccpool (Hono + libSQL) — the one path to the shared ledger. Self-host it to run your own group.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"claude",
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
"tsup": "^8.3.5",
|
|
49
49
|
"tsx": "^4.19.2",
|
|
50
50
|
"typescript": "^5.7.2",
|
|
51
|
-
"@ccpool/core": "0.0.
|
|
52
|
-
"@ccpool/
|
|
53
|
-
"@ccpool/
|
|
51
|
+
"@ccpool/core": "0.0.2",
|
|
52
|
+
"@ccpool/daemon": "0.0.1",
|
|
53
|
+
"@ccpool/storage-libsql": "0.0.1"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"build": "tsup",
|