@diegoaltoworks/chatter 0.24.0 → 0.26.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/README.md +3 -1
- package/dist/core/buckets.d.ts +74 -0
- package/dist/core/buckets.d.ts.map +1 -0
- package/dist/core/buckets.test.d.ts +10 -0
- package/dist/core/buckets.test.d.ts.map +1 -0
- package/dist/core/pipeline.d.ts +12 -1
- package/dist/core/pipeline.d.ts.map +1 -1
- package/dist/core/retrieval.d.ts +12 -2
- package/dist/core/retrieval.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +80 -13
- package/dist/index.mjs +77 -13
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +47 -3
- package/dist/mcp-server.mjs +47 -3
- package/dist/middleware/jwt.d.ts +10 -0
- package/dist/middleware/jwt.d.ts.map +1 -1
- package/dist/routes/demo.d.ts.map +1 -1
- package/dist/routes/openai.d.ts.map +1 -1
- package/dist/routes/private.d.ts.map +1 -1
- package/dist/routes/public.d.ts.map +1 -1
- package/dist/types.d.ts +16 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/usage/index.d.ts +14 -0
- package/dist/usage/index.d.ts.map +1 -0
- package/dist/usage/index.js +116 -0
- package/dist/usage/index.mjs +85 -0
- package/dist/usage/limits.d.ts +87 -0
- package/dist/usage/limits.d.ts.map +1 -0
- package/dist/usage/limits.test.d.ts +10 -0
- package/dist/usage/limits.test.d.ts.map +1 -0
- package/dist/usage/tursoStore.d.ts +25 -0
- package/dist/usage/tursoStore.d.ts.map +1 -0
- package/dist/usage/tursoStore.test.d.ts +9 -0
- package/dist/usage/tursoStore.test.d.ts.map +1 -0
- package/package.json +8 -2
package/dist/mcp-server.js
CHANGED
|
@@ -106,6 +106,37 @@ async function answerOnce({
|
|
|
106
106
|
return completeOnce({ client, system, messages, temperature, model });
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
// src/core/buckets.ts
|
|
110
|
+
function defaultBuckets(mode) {
|
|
111
|
+
return ["base", mode];
|
|
112
|
+
}
|
|
113
|
+
function isIdentified(sender) {
|
|
114
|
+
return typeof sender === "string" && sender.trim().length > 0;
|
|
115
|
+
}
|
|
116
|
+
function normalize(buckets) {
|
|
117
|
+
const out = [];
|
|
118
|
+
for (const raw of buckets) {
|
|
119
|
+
if (typeof raw !== "string") continue;
|
|
120
|
+
const bucket = raw.trim();
|
|
121
|
+
if (bucket && !out.includes(bucket)) out.push(bucket);
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
async function resolveBuckets({
|
|
126
|
+
mode,
|
|
127
|
+
sender,
|
|
128
|
+
bucketsFor
|
|
129
|
+
}) {
|
|
130
|
+
if (!bucketsFor) return void 0;
|
|
131
|
+
const identified = isIdentified(sender);
|
|
132
|
+
const requested = await bucketsFor(identified ? { mode, sender } : { mode });
|
|
133
|
+
if (!Array.isArray(requested)) return void 0;
|
|
134
|
+
const cleaned = normalize(requested);
|
|
135
|
+
if (identified) return cleaned;
|
|
136
|
+
const ceiling = new Set(defaultBuckets(mode));
|
|
137
|
+
return cleaned.filter((bucket) => ceiling.has(bucket));
|
|
138
|
+
}
|
|
139
|
+
|
|
109
140
|
// src/core/prompts.ts
|
|
110
141
|
var import_node_fs = require("node:fs");
|
|
111
142
|
var import_node_path = require("node:path");
|
|
@@ -314,8 +345,19 @@ var VectorStore = class _VectorStore {
|
|
|
314
345
|
}
|
|
315
346
|
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
316
347
|
}
|
|
317
|
-
|
|
348
|
+
/**
|
|
349
|
+
* Retrieve top-k across allowed buckets; compute similarity in app (simple
|
|
350
|
+
* & portable).
|
|
351
|
+
*
|
|
352
|
+
* `allowed` is a bucket-name list rather than the {@link Bucket} union: the
|
|
353
|
+
* `chunks` table constrains nothing, so a deployment that ingests its own
|
|
354
|
+
* buckets can query them (`build` only writes and prunes the three the
|
|
355
|
+
* knowledge loader walks). Names are bound as query parameters, never
|
|
356
|
+
* interpolated. An empty list retrieves nothing, and short-circuits before
|
|
357
|
+
* the embedding call.
|
|
358
|
+
*/
|
|
318
359
|
async query(q, k = 6, allowed = ["base"]) {
|
|
360
|
+
if (allowed.length === 0) return [];
|
|
319
361
|
const qv = (await this.client.embeddings.create({ model: EMB_MODEL, input: [q] })).data[0].embedding;
|
|
320
362
|
const placeholders = allowed.map(() => "?").join(",");
|
|
321
363
|
const res = await this.db.execute({
|
|
@@ -560,7 +602,8 @@ async function createMCPServer(config) {
|
|
|
560
602
|
if (!lastUserMsg) {
|
|
561
603
|
throw new Error("No user message found in conversation");
|
|
562
604
|
}
|
|
563
|
-
const
|
|
605
|
+
const buckets = await resolveBuckets({ mode: "public", bucketsFor: config.bucketsFor }) ?? defaultBuckets("public");
|
|
606
|
+
const ctx = await store.query(lastUserMsg.content, 6, buckets);
|
|
564
607
|
const system = [
|
|
565
608
|
prompts.baseSystemRules,
|
|
566
609
|
prompts.publicPersona,
|
|
@@ -646,7 +689,8 @@ ${ctx.join("\n\n")}`
|
|
|
646
689
|
if (!lastUserMsg) {
|
|
647
690
|
throw new Error("No user message found in conversation");
|
|
648
691
|
}
|
|
649
|
-
const
|
|
692
|
+
const buckets = await resolveBuckets({ mode: "private", bucketsFor: config.bucketsFor }) ?? defaultBuckets("private");
|
|
693
|
+
const ctx = await store.query(lastUserMsg.content, 8, buckets);
|
|
650
694
|
const system = [
|
|
651
695
|
prompts.baseSystemRules,
|
|
652
696
|
prompts.privatePersona,
|
package/dist/mcp-server.mjs
CHANGED
|
@@ -72,6 +72,37 @@ async function answerOnce({
|
|
|
72
72
|
return completeOnce({ client, system, messages, temperature, model });
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// src/core/buckets.ts
|
|
76
|
+
function defaultBuckets(mode) {
|
|
77
|
+
return ["base", mode];
|
|
78
|
+
}
|
|
79
|
+
function isIdentified(sender) {
|
|
80
|
+
return typeof sender === "string" && sender.trim().length > 0;
|
|
81
|
+
}
|
|
82
|
+
function normalize(buckets) {
|
|
83
|
+
const out = [];
|
|
84
|
+
for (const raw of buckets) {
|
|
85
|
+
if (typeof raw !== "string") continue;
|
|
86
|
+
const bucket = raw.trim();
|
|
87
|
+
if (bucket && !out.includes(bucket)) out.push(bucket);
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
async function resolveBuckets({
|
|
92
|
+
mode,
|
|
93
|
+
sender,
|
|
94
|
+
bucketsFor
|
|
95
|
+
}) {
|
|
96
|
+
if (!bucketsFor) return void 0;
|
|
97
|
+
const identified = isIdentified(sender);
|
|
98
|
+
const requested = await bucketsFor(identified ? { mode, sender } : { mode });
|
|
99
|
+
if (!Array.isArray(requested)) return void 0;
|
|
100
|
+
const cleaned = normalize(requested);
|
|
101
|
+
if (identified) return cleaned;
|
|
102
|
+
const ceiling = new Set(defaultBuckets(mode));
|
|
103
|
+
return cleaned.filter((bucket) => ceiling.has(bucket));
|
|
104
|
+
}
|
|
105
|
+
|
|
75
106
|
// src/core/prompts.ts
|
|
76
107
|
import { readFileSync } from "node:fs";
|
|
77
108
|
import { join } from "node:path";
|
|
@@ -280,8 +311,19 @@ var VectorStore = class _VectorStore {
|
|
|
280
311
|
}
|
|
281
312
|
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
282
313
|
}
|
|
283
|
-
|
|
314
|
+
/**
|
|
315
|
+
* Retrieve top-k across allowed buckets; compute similarity in app (simple
|
|
316
|
+
* & portable).
|
|
317
|
+
*
|
|
318
|
+
* `allowed` is a bucket-name list rather than the {@link Bucket} union: the
|
|
319
|
+
* `chunks` table constrains nothing, so a deployment that ingests its own
|
|
320
|
+
* buckets can query them (`build` only writes and prunes the three the
|
|
321
|
+
* knowledge loader walks). Names are bound as query parameters, never
|
|
322
|
+
* interpolated. An empty list retrieves nothing, and short-circuits before
|
|
323
|
+
* the embedding call.
|
|
324
|
+
*/
|
|
284
325
|
async query(q, k = 6, allowed = ["base"]) {
|
|
326
|
+
if (allowed.length === 0) return [];
|
|
285
327
|
const qv = (await this.client.embeddings.create({ model: EMB_MODEL, input: [q] })).data[0].embedding;
|
|
286
328
|
const placeholders = allowed.map(() => "?").join(",");
|
|
287
329
|
const res = await this.db.execute({
|
|
@@ -526,7 +568,8 @@ async function createMCPServer(config) {
|
|
|
526
568
|
if (!lastUserMsg) {
|
|
527
569
|
throw new Error("No user message found in conversation");
|
|
528
570
|
}
|
|
529
|
-
const
|
|
571
|
+
const buckets = await resolveBuckets({ mode: "public", bucketsFor: config.bucketsFor }) ?? defaultBuckets("public");
|
|
572
|
+
const ctx = await store.query(lastUserMsg.content, 6, buckets);
|
|
530
573
|
const system = [
|
|
531
574
|
prompts.baseSystemRules,
|
|
532
575
|
prompts.publicPersona,
|
|
@@ -612,7 +655,8 @@ ${ctx.join("\n\n")}`
|
|
|
612
655
|
if (!lastUserMsg) {
|
|
613
656
|
throw new Error("No user message found in conversation");
|
|
614
657
|
}
|
|
615
|
-
const
|
|
658
|
+
const buckets = await resolveBuckets({ mode: "private", bucketsFor: config.bucketsFor }) ?? defaultBuckets("private");
|
|
659
|
+
const ctx = await store.query(lastUserMsg.content, 8, buckets);
|
|
616
660
|
const system = [
|
|
617
661
|
prompts.baseSystemRules,
|
|
618
662
|
prompts.privatePersona,
|
package/dist/middleware/jwt.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { Context, Next } from "hono";
|
|
2
2
|
import type { ChatterConfig } from "../types";
|
|
3
|
+
/**
|
|
4
|
+
* The verified JWT subject attached by {@link createJWTMiddleware}, or
|
|
5
|
+
* `undefined` when the request carried no identity (unauthenticated route, or
|
|
6
|
+
* a token whose payload had no `sub`).
|
|
7
|
+
*
|
|
8
|
+
* Surfaces use this as the sender identity for per-request policy such as
|
|
9
|
+
* retrieval bucket gating, where "no identity" must mean anonymous rather
|
|
10
|
+
* than an empty-string user.
|
|
11
|
+
*/
|
|
12
|
+
export declare function jwtSubject(c: Context): string | undefined;
|
|
3
13
|
/**
|
|
4
14
|
* Create JWT authentication middleware for private API endpoints
|
|
5
15
|
* Supports JWKS and public key PEM verification
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../src/middleware/jwt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAO9C;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,OAwBZ,OAAO,QAAQ,IAAI;;8BAa/D;AAGD,eAAO,MAAM,iBAAiB,MAhBe,OAAO,QAAQ,IAAI;;6BAgBS,CAAC"}
|
|
1
|
+
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../src/middleware/jwt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAO9C;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAGzD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,aAAa,OAwBZ,OAAO,QAAQ,IAAI;;8BAa/D;AAGD,eAAO,MAAM,iBAAiB,MAhBe,OAAO,QAAQ,IAAI;;6BAgBS,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"demo.d.ts","sourceRoot":"","sources":["../../src/routes/demo.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"demo.d.ts","sourceRoot":"","sources":["../../src/routes/demo.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAK5B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAsHnD,wBAAgB,UAAU,CAAC,IAAI,EAAE,kBAAkB,8EAqHlD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openai.d.ts","sourceRoot":"","sources":["../../src/routes/openai.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"openai.d.ts","sourceRoot":"","sources":["../../src/routes/openai.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAK5B,OAAO,EAAE,KAAK,eAAe,EAAkC,MAAM,kBAAkB,CAAC;AAGxF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAEnD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,eAAe,EAAE,GAAG,IAAI,CA8BxE;AASD,wBAAgB,YAAY,CAAC,IAAI,EAAE,kBAAkB,8EAwIpD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"private.d.ts","sourceRoot":"","sources":["../../src/routes/private.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"private.d.ts","sourceRoot":"","sources":["../../src/routes/private.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAO5B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AASnD,wBAAgB,aAAa,CAAC,IAAI,EAAE,kBAAkB,8EAoErD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/routes/public.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/routes/public.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAS5B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AASnD,wBAAgB,YAAY,CAAC,IAAI,EAAE,kBAAkB,8EA0EpD"}
|
package/dist/types.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { Client as LibsqlClient } from "@libsql/client";
|
|
|
8
8
|
import type { Hono } from "hono";
|
|
9
9
|
import type OpenAI from "openai";
|
|
10
10
|
import type { AnswerFn } from "./core/answer";
|
|
11
|
+
import type { BucketsFor } from "./core/buckets";
|
|
11
12
|
import type { PromptLoader } from "./core/prompts";
|
|
12
13
|
import type { VectorStore } from "./core/retrieval";
|
|
13
14
|
/**
|
|
@@ -156,6 +157,21 @@ export interface ChatterConfig {
|
|
|
156
157
|
* errors. Unset: the built-in OpenAI completion is used.
|
|
157
158
|
*/
|
|
158
159
|
answerFn?: AnswerFn;
|
|
160
|
+
/**
|
|
161
|
+
* Decides which knowledge buckets each chat turn may retrieve from, given
|
|
162
|
+
* the pipeline mode and — where the surface knows it — the sender's
|
|
163
|
+
* identity. Use it for role-gated knowledge: entitle a signed-in operator
|
|
164
|
+
* to buckets an anonymous visitor cannot see.
|
|
165
|
+
*
|
|
166
|
+
* Consulted by the chat routes, the OpenAI-compatible endpoints, the MCP
|
|
167
|
+
* chat tools and the demo route. Return `undefined` to keep the mode
|
|
168
|
+
* defaults (`base` plus the mode's own bucket). For a caller the surface
|
|
169
|
+
* could not identify, the answer is filtered down to those same defaults —
|
|
170
|
+
* the hook can narrow retrieval for an unnamed caller but never widen it.
|
|
171
|
+
*
|
|
172
|
+
* Unset: the mode defaults apply everywhere.
|
|
173
|
+
*/
|
|
174
|
+
bucketsFor?: BucketsFor;
|
|
159
175
|
/** Custom route handler for advanced use cases. May be async — see {@link CustomRoutes} */
|
|
160
176
|
customRoutes?: CustomRoutes;
|
|
161
177
|
}
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage metering — daily spend guards for anything that costs money per call.
|
|
3
|
+
*
|
|
4
|
+
* Published as the `@diegoaltoworks/chatter/usage` subpath so the core install
|
|
5
|
+
* stays untouched by it. The limiter itself is pure; the Turso store is the
|
|
6
|
+
* shipped persistence binding and needs `@libsql/client`, which chatter
|
|
7
|
+
* already expects for retrieval.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
export type { DailyLimitCheck, DailyLimiter, DailyLimiterDeps, DailyLimitReason, DailyLimitsConfig, DailyLimitsStore, } from "./limits";
|
|
12
|
+
export { createDailyLimiter, GLOBAL_LIMIT_KEY, pickDailyLimit, utcDayKey } from "./limits";
|
|
13
|
+
export { createTursoUsageStore } from "./tursoStore";
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/usage/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,YAAY,EACV,eAAe,EACf,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAC3F,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/usage/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
GLOBAL_LIMIT_KEY: () => GLOBAL_LIMIT_KEY,
|
|
24
|
+
createDailyLimiter: () => createDailyLimiter,
|
|
25
|
+
createTursoUsageStore: () => createTursoUsageStore,
|
|
26
|
+
pickDailyLimit: () => pickDailyLimit,
|
|
27
|
+
utcDayKey: () => utcDayKey
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/usage/limits.ts
|
|
32
|
+
var GLOBAL_LIMIT_KEY = "global";
|
|
33
|
+
function utcDayKey(now) {
|
|
34
|
+
return new Date(now).toISOString().slice(0, 10);
|
|
35
|
+
}
|
|
36
|
+
function createDailyLimiter(config, deps) {
|
|
37
|
+
const now = deps.now ?? Date.now;
|
|
38
|
+
return {
|
|
39
|
+
async checkAndReserve(key) {
|
|
40
|
+
const day = utcDayKey(now());
|
|
41
|
+
const perKeyCount = await deps.store.incrementAndGet("key", key, day);
|
|
42
|
+
if (perKeyCount > config.perKeyDailyLimit) {
|
|
43
|
+
return { allowed: false, reason: "per-key" };
|
|
44
|
+
}
|
|
45
|
+
const globalCount = await deps.store.incrementAndGet("global", GLOBAL_LIMIT_KEY, day);
|
|
46
|
+
if (globalCount > config.globalDailyLimit) {
|
|
47
|
+
return { allowed: false, reason: "global" };
|
|
48
|
+
}
|
|
49
|
+
return { allowed: true, reason: null };
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function pickDailyLimit(value, fallback) {
|
|
54
|
+
const trimmed = value?.trim();
|
|
55
|
+
if (trimmed === void 0 || trimmed === "") return fallback;
|
|
56
|
+
const parsed = Number(trimmed);
|
|
57
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/usage/tursoStore.ts
|
|
61
|
+
var schemaReady = /* @__PURE__ */ new WeakMap();
|
|
62
|
+
function ensureUsageSchema(client, tableName) {
|
|
63
|
+
let perTable = schemaReady.get(client);
|
|
64
|
+
if (!perTable) {
|
|
65
|
+
perTable = /* @__PURE__ */ new Map();
|
|
66
|
+
schemaReady.set(client, perTable);
|
|
67
|
+
}
|
|
68
|
+
const tables = perTable;
|
|
69
|
+
let ready = tables.get(tableName);
|
|
70
|
+
if (!ready) {
|
|
71
|
+
ready = client.execute(
|
|
72
|
+
`CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
73
|
+
scope TEXT NOT NULL,
|
|
74
|
+
key TEXT NOT NULL,
|
|
75
|
+
day TEXT NOT NULL,
|
|
76
|
+
count INTEGER NOT NULL DEFAULT 0,
|
|
77
|
+
PRIMARY KEY (scope, key, day)
|
|
78
|
+
)`
|
|
79
|
+
).catch((error) => {
|
|
80
|
+
tables.delete(tableName);
|
|
81
|
+
throw error;
|
|
82
|
+
});
|
|
83
|
+
tables.set(tableName, ready);
|
|
84
|
+
}
|
|
85
|
+
return ready;
|
|
86
|
+
}
|
|
87
|
+
var VALID_TABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
88
|
+
function createTursoUsageStore(client, tableName) {
|
|
89
|
+
if (!VALID_TABLE_NAME.test(tableName)) {
|
|
90
|
+
throw new Error(`Invalid usage store table name: ${tableName}`);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
async incrementAndGet(scope, key, day) {
|
|
94
|
+
await ensureUsageSchema(client, tableName);
|
|
95
|
+
const result = await client.execute({
|
|
96
|
+
sql: `INSERT INTO ${tableName} (scope, key, day, count) VALUES (?, ?, ?, 1)
|
|
97
|
+
ON CONFLICT(scope, key, day) DO UPDATE SET count = count + 1
|
|
98
|
+
RETURNING count`,
|
|
99
|
+
args: [scope, key, day]
|
|
100
|
+
});
|
|
101
|
+
const row = result.rows[0];
|
|
102
|
+
if (!row) {
|
|
103
|
+
throw new Error(`${tableName} returned no row from upsert`);
|
|
104
|
+
}
|
|
105
|
+
return Number(row.count);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
110
|
+
0 && (module.exports = {
|
|
111
|
+
GLOBAL_LIMIT_KEY,
|
|
112
|
+
createDailyLimiter,
|
|
113
|
+
createTursoUsageStore,
|
|
114
|
+
pickDailyLimit,
|
|
115
|
+
utcDayKey
|
|
116
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// src/usage/limits.ts
|
|
2
|
+
var GLOBAL_LIMIT_KEY = "global";
|
|
3
|
+
function utcDayKey(now) {
|
|
4
|
+
return new Date(now).toISOString().slice(0, 10);
|
|
5
|
+
}
|
|
6
|
+
function createDailyLimiter(config, deps) {
|
|
7
|
+
const now = deps.now ?? Date.now;
|
|
8
|
+
return {
|
|
9
|
+
async checkAndReserve(key) {
|
|
10
|
+
const day = utcDayKey(now());
|
|
11
|
+
const perKeyCount = await deps.store.incrementAndGet("key", key, day);
|
|
12
|
+
if (perKeyCount > config.perKeyDailyLimit) {
|
|
13
|
+
return { allowed: false, reason: "per-key" };
|
|
14
|
+
}
|
|
15
|
+
const globalCount = await deps.store.incrementAndGet("global", GLOBAL_LIMIT_KEY, day);
|
|
16
|
+
if (globalCount > config.globalDailyLimit) {
|
|
17
|
+
return { allowed: false, reason: "global" };
|
|
18
|
+
}
|
|
19
|
+
return { allowed: true, reason: null };
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function pickDailyLimit(value, fallback) {
|
|
24
|
+
const trimmed = value?.trim();
|
|
25
|
+
if (trimmed === void 0 || trimmed === "") return fallback;
|
|
26
|
+
const parsed = Number(trimmed);
|
|
27
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/usage/tursoStore.ts
|
|
31
|
+
var schemaReady = /* @__PURE__ */ new WeakMap();
|
|
32
|
+
function ensureUsageSchema(client, tableName) {
|
|
33
|
+
let perTable = schemaReady.get(client);
|
|
34
|
+
if (!perTable) {
|
|
35
|
+
perTable = /* @__PURE__ */ new Map();
|
|
36
|
+
schemaReady.set(client, perTable);
|
|
37
|
+
}
|
|
38
|
+
const tables = perTable;
|
|
39
|
+
let ready = tables.get(tableName);
|
|
40
|
+
if (!ready) {
|
|
41
|
+
ready = client.execute(
|
|
42
|
+
`CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
43
|
+
scope TEXT NOT NULL,
|
|
44
|
+
key TEXT NOT NULL,
|
|
45
|
+
day TEXT NOT NULL,
|
|
46
|
+
count INTEGER NOT NULL DEFAULT 0,
|
|
47
|
+
PRIMARY KEY (scope, key, day)
|
|
48
|
+
)`
|
|
49
|
+
).catch((error) => {
|
|
50
|
+
tables.delete(tableName);
|
|
51
|
+
throw error;
|
|
52
|
+
});
|
|
53
|
+
tables.set(tableName, ready);
|
|
54
|
+
}
|
|
55
|
+
return ready;
|
|
56
|
+
}
|
|
57
|
+
var VALID_TABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
58
|
+
function createTursoUsageStore(client, tableName) {
|
|
59
|
+
if (!VALID_TABLE_NAME.test(tableName)) {
|
|
60
|
+
throw new Error(`Invalid usage store table name: ${tableName}`);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
async incrementAndGet(scope, key, day) {
|
|
64
|
+
await ensureUsageSchema(client, tableName);
|
|
65
|
+
const result = await client.execute({
|
|
66
|
+
sql: `INSERT INTO ${tableName} (scope, key, day, count) VALUES (?, ?, ?, 1)
|
|
67
|
+
ON CONFLICT(scope, key, day) DO UPDATE SET count = count + 1
|
|
68
|
+
RETURNING count`,
|
|
69
|
+
args: [scope, key, day]
|
|
70
|
+
});
|
|
71
|
+
const row = result.rows[0];
|
|
72
|
+
if (!row) {
|
|
73
|
+
throw new Error(`${tableName} returned no row from upsert`);
|
|
74
|
+
}
|
|
75
|
+
return Number(row.count);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export {
|
|
80
|
+
GLOBAL_LIMIT_KEY,
|
|
81
|
+
createDailyLimiter,
|
|
82
|
+
createTursoUsageStore,
|
|
83
|
+
pickDailyLimit,
|
|
84
|
+
utcDayKey
|
|
85
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daily spend guards — the counting half of "this feature costs money, cap it".
|
|
3
|
+
*
|
|
4
|
+
* Framework-free and storage-free: this module knows how to decide, never how
|
|
5
|
+
* to persist. A host supplies a {@link DailyLimitsStore} (see ./tursoStore for
|
|
6
|
+
* the shipped one) and instantiates one limiter per metered resource — image
|
|
7
|
+
* generation, voice minutes, anything with a per-unit price — by giving each
|
|
8
|
+
* its own store table. Nothing here is chat-specific.
|
|
9
|
+
*/
|
|
10
|
+
/** The two caps every metered resource is subject to. */
|
|
11
|
+
export interface DailyLimitsConfig {
|
|
12
|
+
/** Units one key may reserve per UTC day. */
|
|
13
|
+
perKeyDailyLimit: number;
|
|
14
|
+
/** Units all keys together may reserve per UTC day. */
|
|
15
|
+
globalDailyLimit: number;
|
|
16
|
+
}
|
|
17
|
+
/** The key the global counter is stored under — that scope has no caller key. */
|
|
18
|
+
export declare const GLOBAL_LIMIT_KEY = "global";
|
|
19
|
+
/** Which cap blocked the request, or null when it was allowed. */
|
|
20
|
+
export type DailyLimitReason = "per-key" | "global" | null;
|
|
21
|
+
export interface DailyLimitCheck {
|
|
22
|
+
allowed: boolean;
|
|
23
|
+
reason: DailyLimitReason;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Structural dependency — a host implements this against its own storage.
|
|
27
|
+
*
|
|
28
|
+
* Must increment and read back atomically. Deployments run multiple instances,
|
|
29
|
+
* so a read-then-write from this process would race with another instance's
|
|
30
|
+
* and both would see room under a cap that only had space for one.
|
|
31
|
+
*/
|
|
32
|
+
export interface DailyLimitsStore {
|
|
33
|
+
/** Increments the counter for (scope, key, day) and returns the count after incrementing. */
|
|
34
|
+
incrementAndGet(scope: "key" | "global", key: string, day: string): Promise<number>;
|
|
35
|
+
}
|
|
36
|
+
/** The UTC calendar day counters roll over on, e.g. "2026-08-01". */
|
|
37
|
+
export declare function utcDayKey(now: number): string;
|
|
38
|
+
export interface DailyLimiterDeps {
|
|
39
|
+
store: DailyLimitsStore;
|
|
40
|
+
/** Clock, injectable so day-rollover tests don't depend on real time. */
|
|
41
|
+
now?: () => number;
|
|
42
|
+
}
|
|
43
|
+
export interface DailyLimiter {
|
|
44
|
+
/**
|
|
45
|
+
* Reserves one unit for `key`, subject to both daily caps.
|
|
46
|
+
*
|
|
47
|
+
* Call this exactly once per logical request: the increment is permanent and
|
|
48
|
+
* unconditional (there is no release path), so a duplicate call — or a retry
|
|
49
|
+
* after a transient failure — permanently burns a second unit of quota for
|
|
50
|
+
* one request never delivered. Blocked calls still count: a call that trips
|
|
51
|
+
* the per-key cap still consumes one of that key's daily units, and (by
|
|
52
|
+
* design — see below) a call that trips the global cap after passing the
|
|
53
|
+
* per-key check also consumes one of that key's units, for nothing.
|
|
54
|
+
*
|
|
55
|
+
* The per-key counter increments before the global one, so a key blocked at
|
|
56
|
+
* its own cap never touches the shared global budget. The mirror case is
|
|
57
|
+
* accepted, not fixed: once the global cap is exhausted for the day, every
|
|
58
|
+
* further caller still burns one of their own per-key units on the way to
|
|
59
|
+
* being told no. This favours guarding against the more common cost risk
|
|
60
|
+
* (one key hammering the endpoint) over the rarer one (the global cap
|
|
61
|
+
* saturating and clipping everyone's personal quota).
|
|
62
|
+
*
|
|
63
|
+
* Reserve before spending, not after: the ordering is what makes the cap a
|
|
64
|
+
* cap. Callers with a cache should check it first, so a cache hit costs
|
|
65
|
+
* neither money nor quota.
|
|
66
|
+
*/
|
|
67
|
+
checkAndReserve(key: string): Promise<DailyLimitCheck>;
|
|
68
|
+
}
|
|
69
|
+
export declare function createDailyLimiter(config: DailyLimitsConfig, deps: DailyLimiterDeps): DailyLimiter;
|
|
70
|
+
/**
|
|
71
|
+
* Reads a cap out of loosely-typed configuration (an env var, a JSON field).
|
|
72
|
+
*
|
|
73
|
+
* A configured non-negative integer wins over the default; anything else —
|
|
74
|
+
* unset, blank, negative, fractional, unparseable — falls back. A malformed
|
|
75
|
+
* cap must not silently become `NaN`, which compares false against every
|
|
76
|
+
* count and would disable the guard entirely.
|
|
77
|
+
*
|
|
78
|
+
* Blank is trimmed before parsing rather than compared raw: `Number(" ")` is
|
|
79
|
+
* `0`, so a config value that is whitespace would otherwise be read as a cap
|
|
80
|
+
* of zero and switch the feature off entirely instead of falling back.
|
|
81
|
+
*
|
|
82
|
+
* This is the supported way to build a {@link DailyLimitsConfig}. The limiter
|
|
83
|
+
* itself does not validate its caps, so a `NaN` reaching it from somewhere
|
|
84
|
+
* else fails open.
|
|
85
|
+
*/
|
|
86
|
+
export declare function pickDailyLimit(value: string | undefined, fallback: number): number;
|
|
87
|
+
//# sourceMappingURL=limits.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limits.d.ts","sourceRoot":"","sources":["../../src/usage/limits.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,yDAAyD;AACzD,MAAM,WAAW,iBAAiB;IAChC,6CAA6C;IAC7C,gBAAgB,EAAE,MAAM,CAAC;IACzB,uDAAuD;IACvD,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,WAAW,CAAC;AAEzC,kEAAkE;AAClE,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,QAAQ,GAAG,IAAI,CAAC;AAE3D,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,6FAA6F;IAC7F,eAAe,CAAC,KAAK,EAAE,KAAK,GAAG,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACrF;AAED,qEAAqE;AACrE,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,gBAAgB,CAAC;IACxB,yEAAyE;IACzE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;CACxD;AAED,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,iBAAiB,EACzB,IAAI,EAAE,gBAAgB,GACrB,YAAY,CAoBd;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKlF"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daily limiter tests.
|
|
3
|
+
*
|
|
4
|
+
* Two properties matter more than the arithmetic and are asserted directly:
|
|
5
|
+
* the *order* of the two increments (a key blocked at its own cap must not
|
|
6
|
+
* burn shared budget), and the reserve-once semantics (every call counts,
|
|
7
|
+
* including blocked ones — there is no release path to lean on).
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=limits.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limits.test.d.ts","sourceRoot":"","sources":["../../src/usage/limits.test.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turso/libsql-backed {@link DailyLimitsStore}.
|
|
3
|
+
*
|
|
4
|
+
* Schema and upsert only — error typing and wrapping stay with the caller,
|
|
5
|
+
* because each metered feature wants its own error contract and this layer
|
|
6
|
+
* should not invent one for them.
|
|
7
|
+
*
|
|
8
|
+
* `@libsql/client` is imported for its type alone, so this module adds no
|
|
9
|
+
* runtime dependency to anyone importing `./usage` for the pure limiter.
|
|
10
|
+
*/
|
|
11
|
+
import type { Client } from "@libsql/client";
|
|
12
|
+
import type { DailyLimitsStore } from "./limits";
|
|
13
|
+
/**
|
|
14
|
+
* Creates a store that counts into `tableName` on `client`.
|
|
15
|
+
*
|
|
16
|
+
* The upsert is a single atomic statement returning the post-increment count,
|
|
17
|
+
* so concurrent instances each get a distinct number and the cap holds across
|
|
18
|
+
* a multi-instance deployment. Table creation is idempotent and happens once
|
|
19
|
+
* per (client, table).
|
|
20
|
+
*
|
|
21
|
+
* Give each metered resource its own table — sharing one would make separate
|
|
22
|
+
* features compete for a single global counter.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createTursoUsageStore(client: Client, tableName: string): DailyLimitsStore;
|
|
25
|
+
//# sourceMappingURL=tursoStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tursoStore.d.ts","sourceRoot":"","sources":["../../src/usage/tursoStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAyCjD;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAuBzF"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turso usage store tests.
|
|
3
|
+
*
|
|
4
|
+
* Run against a real in-memory libsql client rather than a fake, because the
|
|
5
|
+
* properties worth proving here are SQL-level: the upsert is atomic, it hands
|
|
6
|
+
* every concurrent caller a distinct number, and table creation is idempotent.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=tursoStore.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tursoStore.test.d.ts","sourceRoot":"","sources":["../../src/usage/tursoStore.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
|