@columenlabs/lumen-person 0.1.162 → 0.1.164

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 CHANGED
@@ -53,6 +53,19 @@ await mine.call('add', { title: 'Buy saw blades' });
53
53
 
54
54
  The first call names the person and claims the store; a later call for anyone else is refused (403).
55
55
 
56
+ ## Row budgets: no loop can spend the account's day
57
+
58
+ Cloudflare's free plan allows an account 5M Durable Objects rows read and 100k rows written a day, across every object. Past either, every SQL call in every object throws until 00:00 UTC. Every store is bounded (`budget` in its deps, `RowBudget` in `./budget` for any other object):
59
+
60
+ | Bound | Default | Past it |
61
+ |---|---|---|
62
+ | One call | 100k rows read, 20k written | its next statement throws; the call answers 503 |
63
+ | One wake | 200k read, 20k written | the same, for that wake |
64
+ | The store's UTC day | 600k read, 12k written | calls answer 503 until 00:00 UTC |
65
+ | `background` share of the day | 0.6 | wakes wait for the next day (spread over its first half hour); calls keep working |
66
+
67
+ Counts are the cursors' own `rowsRead` and `rowsWritten`, what Cloudflare bills. `onLimit` hears each level once a day (alert someone there). `_object.budget` reads today's totals and `_object.brake {level: 'background' | 'stop' | 'ok', until?}` sets a brake from outside, for when the whole account nears its cap; both answer on a spent day. A request that needs more than one call's budget (an import, a rebuild) is a background job in small wakes, never one request.
68
+
56
69
  ## Rules it keeps
57
70
 
58
71
  - **Migrations fail per module.** Each module migrates on its first call or wake, each step in one transaction. A step that throws leaves only that module answering 503 for a minute before it tries again; every other module keeps working. Never edit or reorder a released step: add one.
@@ -0,0 +1,88 @@
1
+ export interface RowLimits {
2
+ /** Rows one call may read (default 100k). */
3
+ callRead?: number;
4
+ /** Rows one call may write (default 20k). */
5
+ callWrite?: number;
6
+ /** Rows one wake may read (default 200k). */
7
+ wakeRead?: number;
8
+ /** Rows one wake may write (default 20k). */
9
+ wakeWrite?: number;
10
+ /** Rows this object may read in one UTC day (default 600k). */
11
+ dayRead?: number;
12
+ /** Rows this object may write in one UTC day (default 12k). */
13
+ dayWrite?: number;
14
+ /** Share of the day after which wakes wait for the next day (default 0.6). */
15
+ background?: number;
16
+ }
17
+ export type BudgetLevel = 'ok' | 'background' | 'stop';
18
+ /** What `onLimit` hears, once per level per day, and what `_object.budget` returns. */
19
+ export interface BudgetState {
20
+ day: string;
21
+ read: number;
22
+ written: number;
23
+ level: BudgetLevel;
24
+ brake: BudgetLevel;
25
+ limits: Required<RowLimits>;
26
+ }
27
+ export interface BudgetHooks {
28
+ /** A level crossed today (`background`, `stop`) or one call or wake stopped (`call`, `wake`). */
29
+ onLimit?(e: {
30
+ kind: 'background' | 'stop' | 'call' | 'wake';
31
+ state: BudgetState;
32
+ }): void;
33
+ }
34
+ export declare const DEFAULT_LIMITS: Required<RowLimits>;
35
+ /** A spent budget: the person sees a 503 that says when it comes back. */
36
+ export declare class BudgetError extends Error {
37
+ readonly status: 503;
38
+ }
39
+ /** The UTC day of `now` (`2026-10-29`). */
40
+ export declare const utcDay: (now?: number) => string;
41
+ /** The next 00:00 UTC after `now`. */
42
+ export declare function nextUtcDay(now?: number): number;
43
+ interface BudgetStorage {
44
+ get<T>(key: string): Promise<T | undefined>;
45
+ put(key: string, value: unknown): Promise<void>;
46
+ }
47
+ /** One object's row budget. Wrap its SQL with `sql()`, run each call or wake in `scope()`. */
48
+ export declare class RowBudget {
49
+ private readonly storage;
50
+ private readonly hooks;
51
+ readonly limits: Required<RowLimits>;
52
+ private day;
53
+ private read;
54
+ private written;
55
+ private brake;
56
+ private told;
57
+ private loaded?;
58
+ private savedAt;
59
+ private loose;
60
+ constructor(storage: BudgetStorage, limits?: RowLimits, hooks?: BudgetHooks);
61
+ /** Reads the day's saved totals once (one row). */
62
+ load(): Promise<void>;
63
+ /** The SQLite with every statement counted against the current call or wake, and the object. */
64
+ sql<S extends {
65
+ exec(query: string, ...b: unknown[]): unknown;
66
+ }>(raw: S): S;
67
+ private roll;
68
+ /** Moves finished counts onto the day's totals. */
69
+ private settle;
70
+ private settleLoose;
71
+ /** The level today's totals and the brake give. */
72
+ level(): BudgetLevel;
73
+ state(): BudgetState;
74
+ private tell;
75
+ /** Throws before a statement that would run past the call's, wake's or day's budget. */
76
+ private check;
77
+ /** Whether a call may start now (false: the day is spent or the brake is on). */
78
+ callAllowed(): boolean;
79
+ /** Whether background work may start now. */
80
+ wakeAllowed(): boolean;
81
+ /** Runs `fn` as one call or wake, counted on its own and then on the day. */
82
+ scope<T>(kind: 'call' | 'wake', fn: () => Promise<T>): Promise<T>;
83
+ /** A brake from outside (`ok` lifts it); it ends by itself at `until` (default next 00:00 UTC). */
84
+ setBrake(level: BudgetLevel, until?: number): Promise<BudgetState>;
85
+ /** Saves the day's totals (at most once a minute unless forced). */
86
+ save(force?: boolean): Promise<void>;
87
+ }
88
+ export {};
package/dist/budget.js ADDED
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Row budgets for a Durable Object's SQLite. Cloudflare's free plan allows an account 5M rows
3
+ * read and 100k rows written a day across every object; past either, every SQL call in every
4
+ * object throws until 00:00 UTC. One looping page or a full-table scan per request can spend that
5
+ * in minutes, so every call, wake and object here is bounded:
6
+ *
7
+ * per call one request may read and write so many rows; past it, its next statement throws
8
+ * per wake the same for one alarm wake (background work)
9
+ * per day the object's share of the account for one UTC day; past `background` of it, wakes
10
+ * wait for the next day; past all of it, calls are refused until 00:00 UTC
11
+ * brake a level set from outside (the whole account is near its cap): `background` stops
12
+ * wakes, `stop` refuses calls too
13
+ *
14
+ * Counts come from the cursors themselves (`rowsRead`, `rowsWritten`), so they are what Cloudflare
15
+ * bills. The day's totals live in memory and are saved to storage at most once a minute.
16
+ */
17
+ import { AsyncLocalStorage } from 'node:async_hooks';
18
+ export const DEFAULT_LIMITS = {
19
+ callRead: 100_000,
20
+ callWrite: 20_000,
21
+ wakeRead: 200_000,
22
+ wakeWrite: 20_000,
23
+ dayRead: 600_000,
24
+ dayWrite: 12_000,
25
+ background: 0.6,
26
+ };
27
+ /** A spent budget: the person sees a 503 that says when it comes back. */
28
+ export class BudgetError extends Error {
29
+ status = 503;
30
+ }
31
+ const scopes = new AsyncLocalStorage();
32
+ /** The UTC day of `now` (`2026-10-29`). */
33
+ export const utcDay = (now = Date.now()) => new Date(now).toISOString().slice(0, 10);
34
+ /** The next 00:00 UTC after `now`. */
35
+ export function nextUtcDay(now = Date.now()) {
36
+ const d = new Date(now);
37
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + 1);
38
+ }
39
+ const sum = (cs, k) => cs.reduce((n, c) => n + (Number(c[k]) || 0), 0);
40
+ const KEY = '_row_budget';
41
+ const FLUSH_MS = 60_000;
42
+ /** One object's row budget. Wrap its SQL with `sql()`, run each call or wake in `scope()`. */
43
+ export class RowBudget {
44
+ storage;
45
+ hooks;
46
+ limits;
47
+ day = utcDay();
48
+ read = 0;
49
+ written = 0;
50
+ brake = { level: 'ok', until: 0 };
51
+ told = new Set();
52
+ loaded;
53
+ savedAt = 0;
54
+ loose = [];
55
+ constructor(storage, limits = {}, hooks = {}) {
56
+ this.storage = storage;
57
+ this.hooks = hooks;
58
+ this.limits = { ...DEFAULT_LIMITS, ...limits };
59
+ }
60
+ /** Reads the day's saved totals once (one row). */
61
+ load() {
62
+ this.loaded ??= (async () => {
63
+ const s = await this.storage
64
+ .get(KEY)
65
+ .catch(() => undefined);
66
+ if (s?.day === this.day) {
67
+ this.read = Math.max(this.read, s.read);
68
+ this.written = Math.max(this.written, s.written);
69
+ }
70
+ if (s?.brake && s.brake.until > Date.now())
71
+ this.brake = s.brake;
72
+ })();
73
+ return this.loaded;
74
+ }
75
+ /** The SQLite with every statement counted against the current call or wake, and the object. */
76
+ sql(raw) {
77
+ const self = this;
78
+ return new Proxy(raw, {
79
+ get(t, p) {
80
+ if (p !== 'exec')
81
+ return Reflect.get(t, p, t);
82
+ return (query, ...b) => {
83
+ self.check();
84
+ const cur = t.exec(query, ...b);
85
+ const s = scopes.getStore();
86
+ if (s)
87
+ s.cursors.push(cur);
88
+ else
89
+ self.loose.push(cur);
90
+ return cur;
91
+ };
92
+ },
93
+ });
94
+ }
95
+ roll() {
96
+ const today = utcDay();
97
+ if (today === this.day)
98
+ return;
99
+ this.day = today;
100
+ this.read = 0;
101
+ this.written = 0;
102
+ this.told.clear();
103
+ }
104
+ /** Moves finished counts onto the day's totals. */
105
+ settle(cursors) {
106
+ this.roll();
107
+ this.read += sum(cursors, 'rowsRead');
108
+ this.written += sum(cursors, 'rowsWritten');
109
+ }
110
+ settleLoose() {
111
+ if (!this.loose.length)
112
+ return;
113
+ const l = this.loose;
114
+ this.loose = [];
115
+ this.settle(l);
116
+ }
117
+ /** The level today's totals and the brake give. */
118
+ level() {
119
+ this.settleLoose();
120
+ this.roll();
121
+ const l = this.limits;
122
+ const brake = this.brake.until > Date.now() ? this.brake.level : 'ok';
123
+ if (brake === 'stop' || this.read >= l.dayRead || this.written >= l.dayWrite)
124
+ return 'stop';
125
+ if (brake === 'background' ||
126
+ this.read >= l.dayRead * l.background ||
127
+ this.written >= l.dayWrite * l.background)
128
+ return 'background';
129
+ return 'ok';
130
+ }
131
+ state() {
132
+ const level = this.level();
133
+ return {
134
+ day: this.day,
135
+ read: this.read,
136
+ written: this.written,
137
+ level,
138
+ brake: this.brake.until > Date.now() ? this.brake.level : 'ok',
139
+ limits: this.limits,
140
+ };
141
+ }
142
+ tell(kind) {
143
+ const key = kind === 'call' || kind === 'wake' ? `${kind}:${Math.floor(Date.now() / 3_600_000)}` : kind;
144
+ if (this.told.has(key))
145
+ return;
146
+ this.told.add(key);
147
+ try {
148
+ this.hooks.onLimit?.({ kind, state: this.state() });
149
+ }
150
+ catch { }
151
+ console.error(JSON.stringify({ event: 'row_budget', kind, ...this.state(), limits: undefined }));
152
+ }
153
+ /** Throws before a statement that would run past the call's, wake's or day's budget. */
154
+ check() {
155
+ const s = scopes.getStore();
156
+ if (s) {
157
+ const read = s.read + sum(s.cursors, 'rowsRead');
158
+ const written = s.written + sum(s.cursors, 'rowsWritten');
159
+ const l = this.limits;
160
+ const [r, w] = s.kind === 'call' ? [l.callRead, l.callWrite] : [l.wakeRead, l.wakeWrite];
161
+ if (read >= r || written >= w) {
162
+ this.tell(s.kind);
163
+ throw new BudgetError(`this ${s.kind === 'call' ? 'request' : 'background step'} reads or writes too much at once (${read} rows read, ${written} written)`);
164
+ }
165
+ // Only a step past the whole day's budget is stopped mid-way; background waits at its start.
166
+ if (this.read + read >= l.dayRead || this.written + written >= l.dayWrite) {
167
+ this.tell('stop');
168
+ throw new BudgetError(stopMessage());
169
+ }
170
+ }
171
+ else if (this.level() === 'stop') {
172
+ this.tell('stop');
173
+ throw new BudgetError(stopMessage());
174
+ }
175
+ }
176
+ /** Whether a call may start now (false: the day is spent or the brake is on). */
177
+ callAllowed() {
178
+ const ok = this.level() !== 'stop';
179
+ if (!ok)
180
+ this.tell('stop');
181
+ return ok;
182
+ }
183
+ /** Whether background work may start now. */
184
+ wakeAllowed() {
185
+ const level = this.level();
186
+ if (level !== 'ok')
187
+ this.tell(level);
188
+ return level === 'ok';
189
+ }
190
+ /** Runs `fn` as one call or wake, counted on its own and then on the day. */
191
+ async scope(kind, fn) {
192
+ const s = { kind, cursors: [], read: 0, written: 0 };
193
+ try {
194
+ return await scopes.run(s, fn);
195
+ }
196
+ finally {
197
+ this.settle(s.cursors);
198
+ const level = this.level();
199
+ if (level !== 'ok')
200
+ this.tell(level);
201
+ void this.save();
202
+ }
203
+ }
204
+ /** A brake from outside (`ok` lifts it); it ends by itself at `until` (default next 00:00 UTC). */
205
+ async setBrake(level, until = nextUtcDay()) {
206
+ this.brake = { level, until: level === 'ok' ? 0 : until };
207
+ await this.save(true);
208
+ return this.state();
209
+ }
210
+ /** Saves the day's totals (at most once a minute unless forced). */
211
+ async save(force = false) {
212
+ if (!force && Date.now() - this.savedAt < FLUSH_MS)
213
+ return;
214
+ this.savedAt = Date.now();
215
+ this.settleLoose();
216
+ try {
217
+ await this.storage.put(KEY, {
218
+ day: this.day,
219
+ read: this.read,
220
+ written: this.written,
221
+ brake: this.brake,
222
+ });
223
+ }
224
+ catch {
225
+ // The storage itself may be refusing (the account's limit): the totals stay in memory.
226
+ }
227
+ }
228
+ }
229
+ function stopMessage(now = Date.now()) {
230
+ return `today's storage budget is used; it comes back at ${new Date(nextUtcDay(now)).toISOString().slice(0, 16).replace('T', ' ')} UTC`;
231
+ }
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * already running in that person's store).
5
5
  */
6
6
  import { type PersonReply } from './object.js';
7
+ export * from './budget.js';
7
8
  export * from './move.js';
8
9
  export * from './object.js';
9
10
  /** What the Worker holds of the Durable Object namespace: the class's RPC `call` passes to `PersonObject.call`. */
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@
4
4
  * already running in that person's store).
5
5
  */
6
6
  import { OBJECT_MODULE, PersonError, insideStore } from './object.js';
7
+ export * from './budget.js';
7
8
  export * from './move.js';
8
9
  export * from './object.js';
9
10
  /** Calls `module.method(args)` in the person's store and returns its value, or throws its `PersonError`. */
package/dist/object.d.ts CHANGED
@@ -1,7 +1,10 @@
1
+ import { type BudgetHooks, type RowLimits } from './budget.js';
1
2
  /** The SQLite of a Durable Object (`ctx.storage.sql`): typed loosely, so `SqlStorage` passes as it is. */
2
3
  export interface SqlLike {
3
4
  exec<T extends Record<string, any> = Record<string, unknown>>(query: string, ...bindings: any[]): {
4
5
  toArray(): T[];
6
+ rowsRead?: number;
7
+ rowsWritten?: number;
5
8
  };
6
9
  }
7
10
  /** What the store needs of `DurableObjectState`. */
@@ -55,6 +58,11 @@ export interface PersonDeps<X = unknown> {
55
58
  onClaim?(owner: string): Promise<void>;
56
59
  /** How long one module's wake may run (default 30 s). */
57
60
  wakeMs?: number;
61
+ /**
62
+ * Row budgets for this store (`./budget`): per call, per wake and per UTC day, so no loop can
63
+ * spend the account's daily Durable Objects limit. Left out, the defaults apply.
64
+ */
65
+ budget?: RowLimits & BudgetHooks;
58
66
  }
59
67
  /** A refusal with a status and a message the person can act on. */
60
68
  export declare class PersonError extends Error {
@@ -102,8 +110,13 @@ export declare class PersonObject<X = unknown> {
102
110
  private ownerRead;
103
111
  private readonly ready;
104
112
  private readonly broken;
113
+ private rows?;
114
+ private metered?;
105
115
  constructor(state: PersonState, load: PersonDeps<X> | (() => Promise<PersonDeps<X>>));
116
+ /** The SQLite, every statement counted against the row budget once it is set up. */
106
117
  private get sql();
118
+ /** The store's row budget (set up with the first call or wake). */
119
+ private budget;
107
120
  private setup;
108
121
  /** The module's pending migrations, each in its own transaction. False: it is broken for now. */
109
122
  private migrate;
package/dist/object.js CHANGED
@@ -16,6 +16,7 @@
16
16
  * so a read-modify-write needs no retry loop. Network waits still let other calls in (input gates).
17
17
  */
18
18
  import { AsyncLocalStorage } from 'node:async_hooks';
19
+ import { BudgetError, RowBudget, nextUtcDay, } from './budget.js';
19
20
  import { fromJson, toJson } from './json.js';
20
21
  /** A refusal with a status and a message the person can act on. */
21
22
  export class PersonError extends Error {
@@ -44,21 +45,35 @@ export class PersonObject {
44
45
  ownerRead = false;
45
46
  ready = new Set();
46
47
  broken = new Map();
48
+ rows;
49
+ metered;
47
50
  constructor(state, load) {
48
51
  this.state = state;
49
52
  this.load = load;
50
53
  }
54
+ /** The SQLite, every statement counted against the row budget once it is set up. */
51
55
  get sql() {
52
- return this.state.storage.sql;
56
+ if (!this.rows)
57
+ return this.state.storage.sql;
58
+ this.metered ??= this.rows.sql(this.state.storage.sql);
59
+ return this.metered;
60
+ }
61
+ /** The store's row budget (set up with the first call or wake). */
62
+ budget(deps) {
63
+ this.rows ??= new RowBudget(this.state.storage, deps.budget, deps.budget);
64
+ return this.rows;
53
65
  }
54
66
  async setup() {
55
67
  this.deps ??= (async () => {
56
68
  const deps = typeof this.load === 'function' ? await this.load() : this.load;
69
+ await this.budget(deps).load();
57
70
  for (const m of deps.modules)
58
71
  if (!NAME.test(m.name))
59
72
  throw new Error(`person module name ${m.name}`);
60
- this.sql.exec('CREATE TABLE IF NOT EXISTS _schema (module TEXT PRIMARY KEY, version INTEGER NOT NULL)');
61
- this.sql.exec('CREATE TABLE IF NOT EXISTS _wakes (module TEXT PRIMARY KEY, at INTEGER NOT NULL, fails INTEGER NOT NULL DEFAULT 0)');
73
+ // The store's own two tables, unmetered so the budget can answer even on a spent day.
74
+ const raw = this.state.storage.sql;
75
+ raw.exec('CREATE TABLE IF NOT EXISTS _schema (module TEXT PRIMARY KEY, version INTEGER NOT NULL)');
76
+ raw.exec('CREATE TABLE IF NOT EXISTS _wakes (module TEXT PRIMARY KEY, at INTEGER NOT NULL, fails INTEGER NOT NULL DEFAULT 0)');
62
77
  return deps;
63
78
  })();
64
79
  this.deps.catch(() => {
@@ -157,13 +172,23 @@ export class PersonObject {
157
172
  async call(owner, module, method, args) {
158
173
  try {
159
174
  const deps = await this.setup();
160
- await this.claim(deps, owner);
161
- const value = await this.within(deps, owner, () => this.run(deps, owner, module, method, args ?? null));
175
+ const rows = this.budget(deps);
176
+ // The budget itself answers even when the day is spent: it reads no rows.
177
+ if (module === OBJECT_MODULE && (method === 'budget' || method === 'brake'))
178
+ return { ok: true, value: await this.own(deps, method, args ?? null) };
179
+ if (!rows.callAllowed())
180
+ throw new BudgetError(spentMessage());
181
+ const value = await rows.scope('call', async () => {
182
+ await this.claim(deps, owner);
183
+ return this.within(deps, owner, () => this.run(deps, owner, module, method, args ?? null));
184
+ });
162
185
  return { ok: true, value: value ?? null };
163
186
  }
164
187
  catch (err) {
165
188
  if (err instanceof PersonError)
166
189
  return { ok: false, status: err.status, error: err.message };
190
+ if (err instanceof BudgetError)
191
+ return { ok: false, status: 503, error: err.message };
167
192
  console.error(JSON.stringify({ event: 'person_call_failed', module, method, error: String(err) }));
168
193
  return { ok: false, status: 502, error: `${module} failed` };
169
194
  }
@@ -270,6 +295,15 @@ export class PersonObject {
270
295
  return this.module(deps, a.module);
271
296
  };
272
297
  switch (method) {
298
+ case 'budget':
299
+ return this.budget(deps).state();
300
+ case 'brake': {
301
+ // From the Worker when the whole account nears its cap: `background` stops wakes, `stop` calls too.
302
+ const b = (args ?? {});
303
+ if (b.level !== 'ok' && b.level !== 'background' && b.level !== 'stop')
304
+ throw new PersonError(400, 'brake takes level ok, background or stop');
305
+ return this.budget(deps).setBrake(b.level, b.until);
306
+ }
273
307
  case 'dump':
274
308
  for (const m of deps.modules)
275
309
  this.migrate(m);
@@ -349,6 +383,21 @@ export class PersonObject {
349
383
  const owner = await this.currentOwner();
350
384
  if (!owner)
351
385
  return;
386
+ const rows = this.budget(deps);
387
+ if (!rows.wakeAllowed()) {
388
+ // Background work waits for the next UTC day (spread over its first half hour), so the
389
+ // budget left today serves the person's own requests. Unmetered: two small statements.
390
+ const raw = this.state.storage.sql;
391
+ const at = nextUtcDay() + Math.floor(Math.random() * 30 * 60_000);
392
+ try {
393
+ raw.exec('UPDATE _wakes SET at = ? WHERE at < ?', at, at);
394
+ await this.state.storage.setAlarm(at);
395
+ }
396
+ catch {
397
+ // Storage refusing too: the alarm is not set again, the next call or wake sets it.
398
+ }
399
+ return;
400
+ }
352
401
  const start = Date.now();
353
402
  const due = this.sql
354
403
  .exec('SELECT module, fails FROM _wakes WHERE at <= ? ORDER BY at', start + 1000)
@@ -369,7 +418,7 @@ export class PersonObject {
369
418
  this.sql.exec('DELETE FROM _wakes WHERE module = ?', module);
370
419
  const wake = m.wake.bind(m);
371
420
  try {
372
- await this.within(deps, owner, () => wake(this.ctx(deps, owner, module, Date.now() + (deps.wakeMs ?? 30_000))));
421
+ await rows.scope('wake', () => this.within(deps, owner, () => wake(this.ctx(deps, owner, module, Date.now() + (deps.wakeMs ?? 30_000)))));
373
422
  }
374
423
  catch (err) {
375
424
  const wait = Math.min(RETRY_MS * 2 ** fails, BACKOFF_MAX_MS);
@@ -385,3 +434,6 @@ export class PersonObject {
385
434
  await this.arm();
386
435
  }
387
436
  }
437
+ function spentMessage() {
438
+ return `today's storage budget is used; it comes back at ${new Date(nextUtcDay()).toISOString().slice(0, 16).replace('T', ' ')} UTC`;
439
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@columenlabs/lumen-person",
3
- "version": "0.1.162",
3
+ "version": "0.1.164",
4
4
  "description": "Lumen's one store per person: a Durable Object whose SQLite holds every app's tables for that person, with migrations that fail per app, one alarm shared by every app, and backup, restore and delete per app or whole.",
5
5
  "license": "MIT",
6
6
  "type": "module",