11import {
22 assertExactSqliteSchema ,
3- assertSqliteRowCountPreserved ,
43 type ExpectedSqliteObject ,
5- reconcileExactSqliteStorage ,
64 setCurrentSqliteStorageVersion ,
75} from "@cheatcode/durable-storage" ;
8- import { QUOTA_FEATURES } from "@cheatcode/types/quota" ;
96
107const MAX_FINITE_REAL = "1.7976931348623157e308" ;
118const FEATURE_CHECK = "feature IN ('composio_calls', 'sandbox_hours')" ;
@@ -46,8 +43,6 @@ const USAGE_EVENT_INDEX_SQL =
4643 "CREATE INDEX usage_event_feature_time_idx ON usage_event(feature, recorded_at)" ;
4744const QUOTA_OPERATION_INDEX_SQL =
4845 "CREATE INDEX quota_operation_feature_time_idx ON quota_operation(feature, recorded_at)" ;
49- const QUOTA_TABLES = [ "counter" , "limit_override" , "usage_event" , "quota_operation" ] as const ;
50- const CURRENT_QUOTA_FEATURES = [ QUOTA_FEATURES . composioCalls , QUOTA_FEATURES . sandboxHours ] as const ;
5146
5247const QUOTA_STORAGE_SCHEMA : readonly ExpectedSqliteObject [ ] = [
5348 { name : "counter" , sql : COUNTER_SQL , tableName : "counter" , type : "table" } ,
@@ -78,63 +73,33 @@ const QUOTA_STORAGE_SCHEMA: readonly ExpectedSqliteObject[] = [
7873 } ,
7974] ;
8075
81- /**
82- * Opens quota storage and transactionally upgrades an older supported schema.
83- *
84- * Durable Object input gates serialize this synchronous admission path. The
85- * guarded maintenance route still calls the same reconciler for planned bulk
86- * verification, but ordinary forward-compatible releases cannot strand a
87- * dormant user object on its previous schema.
88- */
76+ /** Opens quota storage only when it matches the exact current contract. */
8977export function ensureQuotaTrackerStorage ( ctx : DurableObjectState ) : void {
9078 if ( ! hasQuotaTrackerStorage ( ctx ) ) {
9179 initializeQuotaTrackerStorage ( ctx ) ;
9280 return ;
9381 }
94- reconcileExactSqliteStorage (
95- "reconcile" ,
96- ( ) => assertQuotaTrackerStorage ( ctx ) ,
97- ( ) => reconcileQuotaTrackerStorage ( ctx ) ,
98- ) ;
99- }
100-
101- /**
102- * Force-normalizes quota storage while preserving every current product row.
103- *
104- * Quotas removed from the product are intentionally discarded before the
105- * exact-schema rebuild. Feature values must still be valid text so corrupt
106- * rows cannot be mistaken for safely retired data.
107- */
108- export function reconcileQuotaTrackerStorage ( ctx : DurableObjectState ) : void {
109- ctx . storage . transactionSync ( ( ) => {
110- ensureSourceTables ( ctx ) ;
111- const limitColumns = ctx . storage . sql . exec ( "PRAGMA table_info(limit_override)" ) . toArray ( ) ;
112- if ( ! hasColumn ( limitColumns , "feature" ) || ! hasColumn ( limitColumns , "limit_val" ) ) {
113- throw new Error ( "Unsupported quota limit schema; refusing lossy evolution." ) ;
114- }
115- const hasEntitlementVersion = hasColumn ( limitColumns , "entitlement_version" ) ;
116- assertQuotaFeatureTypes ( ctx ) ;
117- deleteRetiredQuotaRows ( ctx ) ;
118- assertQuotaSourceRows ( ctx , hasEntitlementVersion ) ;
119- const entitlementVersion = hasEntitlementVersion ? "entitlement_version" : "0" ;
120- rebuildQuotaTables ( ctx , entitlementVersion ) ;
121- setCurrentSqliteStorageVersion ( ctx ) ;
122- assertQuotaTrackerStorage ( ctx ) ;
123- } ) ;
82+ assertQuotaTrackerStorage ( ctx ) ;
12483}
12584
12685export function assertQuotaTrackerStorage ( ctx : DurableObjectState ) : void {
12786 assertExactSqliteSchema ( ctx , QUOTA_STORAGE_SCHEMA ) ;
12887}
12988
13089function initializeQuotaTrackerStorage ( ctx : DurableObjectState ) : void {
131- ensureSourceTables ( ctx ) ;
132- ctx . storage . sql . exec ( USAGE_EVENT_INDEX_SQL . replace ( "CREATE INDEX" , "CREATE INDEX IF NOT EXISTS" ) ) ;
133- ctx . storage . sql . exec (
134- QUOTA_OPERATION_INDEX_SQL . replace ( "CREATE INDEX" , "CREATE INDEX IF NOT EXISTS" ) ,
135- ) ;
136- setCurrentSqliteStorageVersion ( ctx ) ;
137- assertQuotaTrackerStorage ( ctx ) ;
90+ ctx . storage . transactionSync ( ( ) => {
91+ for ( const sql of [ COUNTER_SQL , LIMIT_OVERRIDE_SQL , USAGE_EVENT_SQL , QUOTA_OPERATION_SQL ] ) {
92+ ctx . storage . sql . exec ( sql . replace ( "CREATE TABLE" , "CREATE TABLE IF NOT EXISTS" ) ) ;
93+ }
94+ ctx . storage . sql . exec (
95+ USAGE_EVENT_INDEX_SQL . replace ( "CREATE INDEX" , "CREATE INDEX IF NOT EXISTS" ) ,
96+ ) ;
97+ ctx . storage . sql . exec (
98+ QUOTA_OPERATION_INDEX_SQL . replace ( "CREATE INDEX" , "CREATE INDEX IF NOT EXISTS" ) ,
99+ ) ;
100+ setCurrentSqliteStorageVersion ( ctx ) ;
101+ assertQuotaTrackerStorage ( ctx ) ;
102+ } ) ;
138103}
139104
140105export function hasQuotaTrackerStorage ( ctx : DurableObjectState ) : boolean {
@@ -146,140 +111,3 @@ export function hasQuotaTrackerStorage(ctx: DurableObjectState): boolean {
146111 . toArray ( ) . length > 0
147112 ) ;
148113}
149-
150- function ensureSourceTables ( ctx : DurableObjectState ) : void {
151- for ( const sql of [ COUNTER_SQL , LIMIT_OVERRIDE_SQL , USAGE_EVENT_SQL , QUOTA_OPERATION_SQL ] ) {
152- ctx . storage . sql . exec ( sql . replace ( "CREATE TABLE" , "CREATE TABLE IF NOT EXISTS" ) ) ;
153- }
154- }
155-
156- function rebuildQuotaTables ( ctx : DurableObjectState , entitlementVersion : string ) : void {
157- ctx . storage . sql . exec ( "DROP INDEX IF EXISTS usage_event_feature_time_idx" ) ;
158- ctx . storage . sql . exec ( "DROP INDEX IF EXISTS quota_operation_feature_time_idx" ) ;
159- for ( const table of QUOTA_TABLES ) {
160- ctx . storage . sql . exec ( `ALTER TABLE ${ table } RENAME TO ${ table } _reconcile_source` ) ;
161- }
162- for ( const sql of [ COUNTER_SQL , LIMIT_OVERRIDE_SQL , USAGE_EVENT_SQL , QUOTA_OPERATION_SQL ] ) {
163- ctx . storage . sql . exec ( sql ) ;
164- }
165- copyQuotaRows ( ctx , entitlementVersion ) ;
166- for ( const table of QUOTA_TABLES ) {
167- assertSqliteRowCountPreserved ( ctx , `${ table } _reconcile_source` , table ) ;
168- ctx . storage . sql . exec ( `DROP TABLE ${ table } _reconcile_source` ) ;
169- }
170- ctx . storage . sql . exec ( USAGE_EVENT_INDEX_SQL ) ;
171- ctx . storage . sql . exec ( QUOTA_OPERATION_INDEX_SQL ) ;
172- }
173-
174- function copyQuotaRows ( ctx : DurableObjectState , entitlementVersion : string ) : void {
175- ctx . storage . sql . exec (
176- `INSERT INTO counter (feature, period_key, used, updated_at)
177- SELECT feature, period_key, used, updated_at FROM counter_reconcile_source` ,
178- ) ;
179- ctx . storage . sql . exec (
180- `INSERT INTO limit_override (feature, limit_val, entitlement_version)
181- SELECT feature, limit_val, ${ entitlementVersion } FROM limit_override_reconcile_source` ,
182- ) ;
183- ctx . storage . sql . exec (
184- `INSERT INTO usage_event (id, feature, amount, recorded_at)
185- SELECT id, feature, amount, recorded_at FROM usage_event_reconcile_source` ,
186- ) ;
187- ctx . storage . sql . exec (
188- `INSERT INTO quota_operation
189- (event_id, operation, feature, period_key, amount, allowed,
190- limit_val, remaining, used, recorded_at)
191- SELECT event_id, operation, feature, period_key, amount, allowed,
192- limit_val, remaining, used, recorded_at
193- FROM quota_operation_reconcile_source` ,
194- ) ;
195- }
196-
197- function assertQuotaFeatureTypes ( ctx : DurableObjectState ) : void {
198- for ( const table of QUOTA_TABLES ) {
199- if (
200- ctx . storage . sql
201- . exec ( `SELECT 1 FROM ${ table } WHERE typeof(feature) <> 'text' LIMIT 1` )
202- . toArray ( ) . length > 0
203- ) {
204- throw new Error ( `Quota ${ table } contains a malformed feature; refusing lossy evolution.` ) ;
205- }
206- }
207- }
208-
209- function deleteRetiredQuotaRows ( ctx : DurableObjectState ) : void {
210- for ( const table of QUOTA_TABLES ) {
211- ctx . storage . sql . exec (
212- `DELETE FROM ${ table } WHERE feature NOT IN (?, ?)` ,
213- ...CURRENT_QUOTA_FEATURES ,
214- ) ;
215- }
216- }
217-
218- function assertQuotaSourceRows ( ctx : DurableObjectState , hasEntitlementVersion : boolean ) : void {
219- assertNoInvalidRows (
220- ctx ,
221- "counter" ,
222- `SELECT 1 FROM counter WHERE
223- typeof(feature) <> 'text' OR feature NOT IN (?, ?) OR
224- typeof(period_key) <> 'text' OR length(period_key) <> 7 OR
225- period_key NOT GLOB '[0-9][0-9][0-9][0-9]-[0-9][0-9]' OR
226- typeof(used) NOT IN ('integer', 'real') OR used < 0 OR abs(used) > ${ MAX_FINITE_REAL } OR
227- typeof(updated_at) <> 'integer' OR updated_at < 0 LIMIT 1` ,
228- ) ;
229- const entitlementPredicate = hasEntitlementVersion
230- ? " OR typeof(entitlement_version) <> 'integer' OR entitlement_version < 0"
231- : "" ;
232- assertNoInvalidRows (
233- ctx ,
234- "limit_override" ,
235- `SELECT 1 FROM limit_override WHERE
236- typeof(feature) <> 'text' OR feature NOT IN (?, ?) OR
237- typeof(limit_val) NOT IN ('integer', 'real') OR limit_val < 0 OR
238- abs(limit_val) > ${ MAX_FINITE_REAL } ${ entitlementPredicate } LIMIT 1` ,
239- ) ;
240- assertNoInvalidRows (
241- ctx ,
242- "usage_event" ,
243- `SELECT 1 FROM usage_event WHERE
244- typeof(id) <> 'integer' OR typeof(feature) <> 'text' OR feature NOT IN (?, ?) OR
245- typeof(amount) NOT IN ('integer', 'real') OR amount <= 0 OR
246- abs(amount) > ${ MAX_FINITE_REAL } OR typeof(recorded_at) <> 'integer' OR
247- recorded_at < 0 LIMIT 1` ,
248- ) ;
249- assertNoInvalidRows (
250- ctx ,
251- "quota_operation" ,
252- `SELECT 1 FROM quota_operation WHERE
253- typeof(event_id) <> 'text' OR length(event_id) NOT BETWEEN 1 AND 200 OR
254- typeof(operation) <> 'text' OR operation NOT IN ('record', 'try-consume') OR
255- typeof(feature) <> 'text' OR feature NOT IN (?, ?) OR
256- typeof(period_key) <> 'text' OR length(period_key) <> 7 OR
257- period_key NOT GLOB '[0-9][0-9][0-9][0-9]-[0-9][0-9]' OR
258- typeof(amount) NOT IN ('integer', 'real') OR amount <= 0 OR
259- abs(amount) > ${ MAX_FINITE_REAL } OR typeof(allowed) <> 'integer' OR
260- allowed NOT IN (0, 1) OR typeof(limit_val) NOT IN ('integer', 'real') OR
261- limit_val < 0 OR abs(limit_val) > ${ MAX_FINITE_REAL } OR
262- typeof(remaining) NOT IN ('integer', 'real') OR remaining < 0 OR
263- abs(remaining) > ${ MAX_FINITE_REAL } OR typeof(used) NOT IN ('integer', 'real') OR
264- used < 0 OR abs(used) > ${ MAX_FINITE_REAL } OR
265- typeof(recorded_at) <> 'integer' OR recorded_at < 0 LIMIT 1` ,
266- ) ;
267- }
268-
269- function assertNoInvalidRows (
270- ctx : DurableObjectState ,
271- table : ( typeof QUOTA_TABLES ) [ number ] ,
272- sql : string ,
273- ) : void {
274- if ( ctx . storage . sql . exec ( sql , ...CURRENT_QUOTA_FEATURES ) . toArray ( ) . length > 0 ) {
275- throw new Error ( `Quota ${ table } contains invalid current data; refusing lossy evolution.` ) ;
276- }
277- }
278-
279- function hasColumn ( rows : unknown [ ] , name : string ) : boolean {
280- return rows . some ( ( row ) => isRecord ( row ) && row [ "name" ] === name ) ;
281- }
282-
283- function isRecord ( value : unknown ) : value is Record < string , unknown > {
284- return typeof value === "object" && value !== null ;
285- }
0 commit comments