@diegoaltoworks/chatter 0.25.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/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/README.md
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
- 📱 **Mobile Optimized**: Responsive design with iOS/Android-specific fixes
|
|
26
26
|
- ⚡ **High Performance**: Built on Hono with streaming support
|
|
27
27
|
- 🛡️ **Security First**: Rate limiting, CORS, referrer checking, and input guardrails
|
|
28
|
+
- 💸 **Usage Metering**: Per-caller and global daily caps for paid features, multi-instance safe, via `@diegoaltoworks/chatter/usage` ([guide](./docs/usage.md))
|
|
28
29
|
- 📦 **Framework Agnostic**: Works with any JavaScript framework or vanilla JS
|
|
29
30
|
- 🎯 **TypeScript**: Fully typed for excellent developer experience
|
|
30
31
|
|
|
@@ -71,8 +72,9 @@ Complete guides for setup, deployment, and integration:
|
|
|
71
72
|
- **[Requirements](./docs/requirements.md)** - OpenAI, Turso, Clerk setup and pricing
|
|
72
73
|
- **[Server Setup](./docs/server.md)** - Configuration, knowledge base, prompts, API keys
|
|
73
74
|
- **[Client Integration](./docs/client.md)** - Widgets, React components, theming
|
|
75
|
+
- **[Usage Metering](./docs/usage.md)** - Daily spend caps for paid features
|
|
74
76
|
- **[Deployment](./docs/deployment.md)** - Google Cloud Run, Fly.io, Railway, VPS
|
|
75
|
-
- **[Testing](./docs/testing.md)** - Comprehensive testing guide
|
|
77
|
+
- **[Testing](./docs/testing.md)** - Comprehensive testing guide
|
|
76
78
|
- **[Code Quality](./docs/CODE_QUALITY.md)** - Security assessment and architecture review
|
|
77
79
|
- **[FAQs](./docs/faqs.md)** - Troubleshooting and common questions
|
|
78
80
|
|
|
@@ -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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegoaltoworks/chatter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Embeddable AI chatbot framework with RAG, authentication, and customizable widgets",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
"types": "./dist/mcp-server.d.ts",
|
|
29
29
|
"import": "./dist/mcp-server.mjs",
|
|
30
30
|
"require": "./dist/mcp-server.js"
|
|
31
|
+
},
|
|
32
|
+
"./usage": {
|
|
33
|
+
"types": "./dist/usage/index.d.ts",
|
|
34
|
+
"import": "./dist/usage/index.mjs",
|
|
35
|
+
"require": "./dist/usage/index.js"
|
|
31
36
|
}
|
|
32
37
|
},
|
|
33
38
|
"files": [
|
|
@@ -37,12 +42,13 @@
|
|
|
37
42
|
],
|
|
38
43
|
"scripts": {
|
|
39
44
|
"dev": "bun --watch src/index.ts",
|
|
40
|
-
"build": "bun run build:clean && bun run build:types && bun run build:esm && bun run build:cjs && bun run build:mcp && bun run build:bin && bun run build:client && bun run build:copy-client",
|
|
45
|
+
"build": "bun run build:clean && bun run build:types && bun run build:esm && bun run build:cjs && bun run build:mcp && bun run build:usage && bun run build:bin && bun run build:client && bun run build:copy-client",
|
|
41
46
|
"build:clean": "rm -rf dist",
|
|
42
47
|
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
|
43
48
|
"build:esm": "esbuild src/index.ts --bundle --format=esm --outfile=dist/index.mjs --external:hono --external:openai --external:@libsql/client --external:jose --platform=node --target=node24",
|
|
44
49
|
"build:cjs": "esbuild src/index.ts --bundle --format=cjs --outfile=dist/index.js --external:hono --external:openai --external:@libsql/client --external:jose --platform=node --target=node24",
|
|
45
50
|
"build:mcp": "esbuild src/mcp-server.ts --bundle --format=esm --outfile=dist/mcp-server.mjs --external:@modelcontextprotocol/sdk --external:openai --external:@libsql/client --external:zod --platform=node --target=node24 && esbuild src/mcp-server.ts --bundle --format=cjs --outfile=dist/mcp-server.js --external:@modelcontextprotocol/sdk --external:openai --external:@libsql/client --external:zod --platform=node --target=node24",
|
|
51
|
+
"build:usage": "esbuild src/usage/index.ts --bundle --format=esm --outfile=dist/usage/index.mjs --external:@libsql/client --platform=node --target=node24 && esbuild src/usage/index.ts --bundle --format=cjs --outfile=dist/usage/index.js --external:@libsql/client --platform=node --target=node24",
|
|
46
52
|
"build:bin": "esbuild bin/create-apikey.ts --bundle --format=cjs --outfile=dist/bin/create-apikey.js --external:jose --platform=node --target=node18 --banner:js='#!/usr/bin/env node'",
|
|
47
53
|
"build:client": "cd src/client && bun run build",
|
|
48
54
|
"build:copy-client": "mkdir -p dist/static && cp src/client/dist/chatter.js src/client/dist/chatter.css dist/static/",
|