@blotout/qinto-cli 0.1.5 → 0.1.7

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.
@@ -15769,7 +15769,7 @@ var envelopeRequestSchema = external_exports.object({
15769
15769
  globalId: external_exports.string().min(1).max(64).nullable(),
15770
15770
  properties: external_exports.record(external_exports.string(), external_exports.unknown()),
15771
15771
  geo: external_exports.record(external_exports.string(), external_exports.string()).optional()
15772
- }).strict(),
15772
+ }).strict().optional(),
15773
15773
  consent: external_exports.object({ enabled: external_exports.boolean(), categories: external_exports.record(external_exports.string(), external_exports.boolean()), apps: external_exports.record(external_exports.string(), external_exports.boolean()) }).strict().optional(),
15774
15774
  geo: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
15775
15775
  // Only what this app captured or wrote for this visitor; `updatedAt` is when the current value
@@ -15793,7 +15793,11 @@ var envelopeRequestSchema = external_exports.object({
15793
15793
  browser: external_exports.record(external_exports.string().min(1).max(64), external_exports.unknown()).optional(),
15794
15794
  origin: external_exports.object({ app: external_exports.string().min(1).max(30), install: external_exports.number().int().min(1), depth: external_exports.number().int().min(0) }).strict().optional()
15795
15795
  }).strict().optional()
15796
- }).strict();
15796
+ }).strict().superRefine((envelope, ctx) => {
15797
+ if (envelope.kind !== "rpc" && envelope.user === void 0) {
15798
+ ctx.addIssue({ code: "custom", path: ["user"], message: `A ${envelope.kind} envelope needs a user` });
15799
+ }
15800
+ });
15797
15801
  var envelopeResponseSchema = external_exports.object({
15798
15802
  v: external_exports.literal(ENVELOPE_VERSION),
15799
15803
  // Transform invocations only: pass the action through, drop it, or replace its payload (§5.4).
@@ -18227,7 +18231,7 @@ var APP_FACT_KINDS = [
18227
18231
  ];
18228
18232
  var MAX_APP_FACTS_PER_PAGE = 100;
18229
18233
  var MAX_APP_FACT_PAGE_BYTES = 1024 * 1024;
18230
- var COLUMNS = "kind, scope, key, value, source, amount, currency, event_id, data_json, first_at, observed_at, synced_at";
18234
+ var COLUMNS = "kind, scope, key, value, source, amount, currency, event_id, data_json, first_at, observed_at, synced_at, expires_at";
18231
18235
  var rowOf = /* @__PURE__ */ __name((row) => {
18232
18236
  return {
18233
18237
  kind: row.kind,
@@ -18241,20 +18245,21 @@ var rowOf = /* @__PURE__ */ __name((row) => {
18241
18245
  dataJson: row.data_json,
18242
18246
  firstAt: row.first_at,
18243
18247
  observedAt: row.observed_at,
18244
- syncedAt: row.synced_at
18248
+ syncedAt: row.synced_at,
18249
+ expiresAt: row.expires_at
18245
18250
  };
18246
18251
  }, "rowOf");
18247
18252
  var appFactOf = /* @__PURE__ */ __name((row) => {
18248
- const { syncedAt: _syncedAt, ...fact } = rowOf(row);
18253
+ const { syncedAt: _syncedAt, expiresAt: _expiresAt, ...fact } = rowOf(row);
18249
18254
  return { ...fact, kind: fact.kind };
18250
18255
  }, "appFactOf");
18251
- var listAppFacts = /* @__PURE__ */ __name((sql, query) => {
18256
+ var listAppFacts = /* @__PURE__ */ __name((sql, query, now) => {
18252
18257
  const maxRowId = query.maxRowId ?? sql.exec("SELECT MAX(rowid) AS maxRowId FROM facts").one().maxRowId;
18253
18258
  if (maxRowId === null) {
18254
18259
  return { items: [], next: null };
18255
18260
  }
18256
- const where = ["rowid > ?", "rowid <= ?"];
18257
- const params = [query.afterRowId, maxRowId];
18261
+ const where = ["rowid > ?", "rowid <= ?", "(expires_at IS NULL OR expires_at > ?)"];
18262
+ const params = [query.afterRowId, maxRowId, now];
18258
18263
  if (query.kinds.length > 0) {
18259
18264
  where.push(`kind IN (${query.kinds.map(() => "?").join(", ")})`);
18260
18265
  params.push(...query.kinds);
@@ -18287,7 +18292,7 @@ var listAppFacts = /* @__PURE__ */ __name((sql, query) => {
18287
18292
  }, "listAppFacts");
18288
18293
  var insertFact = /* @__PURE__ */ __name((sql, fact) => {
18289
18294
  sql.exec(
18290
- `INSERT INTO facts (${COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)`,
18295
+ `INSERT INTO facts (${COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?)`,
18291
18296
  fact.kind,
18292
18297
  fact.scope,
18293
18298
  fact.key,
@@ -18298,7 +18303,8 @@ var insertFact = /* @__PURE__ */ __name((sql, fact) => {
18298
18303
  fact.eventId ?? null,
18299
18304
  fact.dataJson ?? null,
18300
18305
  fact.firstAt ?? null,
18301
- fact.observedAt
18306
+ fact.observedAt,
18307
+ fact.expiresAt ?? null
18302
18308
  );
18303
18309
  }, "insertFact");
18304
18310
  var latestFact = /* @__PURE__ */ __name((sql, coords) => {
@@ -19051,7 +19057,7 @@ var stillDeclared = /* @__PURE__ */ __name((target, key, source) => {
19051
19057
  return true;
19052
19058
  }
19053
19059
  const { queryParams, cookies } = target.app.capture;
19054
- return source === "query" ? queryParams.includes(key) : cookies.includes(key);
19060
+ return source === "query" ? queryParams.some((entry) => entry.name === key) : cookies.some((entry) => entry.name === key);
19055
19061
  }, "stillDeclared");
19056
19062
  var deliverableAttribution = /* @__PURE__ */ __name((target, rows) => {
19057
19063
  if (rows === void 0) {
@@ -19339,9 +19345,12 @@ var appSchema = external_exports.object({
19339
19345
  // Method, auth, and mode stay plain strings for the same tolerance: an unknown value from a newer
19340
19346
  // registry refuses that one endpoint instead of turning the app — and every other app — off.
19341
19347
  endpoints: external_exports.array(external_exports.object({ path: external_exports.string(), methods: external_exports.array(external_exports.string()), auth: external_exports.string(), mode: external_exports.string() }).loose()).default([]),
19342
- // Absent from a registry that predates capture; the empty default declares nothing, so an app
19343
- // deployed before this field simply captures nothing until its site redeploys.
19344
- capture: external_exports.object({ queryParams: external_exports.array(external_exports.string()).default([]), cookies: external_exports.array(external_exports.string()).default([]) }).default({ queryParams: [], cookies: [] })
19348
+ // The emitter omits the block entirely for an app that declares no capture, so the default is
19349
+ // what a manifest without a `browser.capture` serializes to.
19350
+ capture: external_exports.object({
19351
+ queryParams: external_exports.array(external_exports.object({ name: external_exports.string(), expiresInSeconds: external_exports.number().optional() }).loose()).default([]),
19352
+ cookies: external_exports.array(external_exports.object({ name: external_exports.string(), expiresInSeconds: external_exports.number().optional() }).loose()).default([])
19353
+ }).default({ queryParams: [], cookies: [] })
19345
19354
  });
19346
19355
  var registrySchema = external_exports.object({
19347
19356
  v: external_exports.literal(1),
@@ -19397,7 +19406,8 @@ var pushBody = external_exports.object({
19397
19406
  var rpcBody = external_exports.object({
19398
19407
  ...identityFields,
19399
19408
  kind: external_exports.literal("rpc"),
19400
- userId: external_exports.string().min(1).max(64),
19409
+ // Absent when the caller is an endpoint, a schedule or a ui handler; the callee then sees no user.
19410
+ userId: external_exports.string().min(1).max(64).optional(),
19401
19411
  callee: external_exports.string().min(1).max(30),
19402
19412
  fn: external_exports.string().min(1).max(64),
19403
19413
  args: external_exports.unknown().optional(),
@@ -19553,7 +19563,7 @@ var calleeFailed = /* @__PURE__ */ __name((callee, reason) => {
19553
19563
  addSpanEvent("rpc callee failed", { level: "warn", attrs: { app: callee, reason } });
19554
19564
  return Response.json({ error: "callee_failed" }, { status: 502 });
19555
19565
  }, "calleeFailed");
19556
- var deliverRpcEffects = /* @__PURE__ */ __name(async (env, body, effects, install) => {
19566
+ var deliverRpcEffects = /* @__PURE__ */ __name(async (env, { body, userId, effects, install }) => {
19557
19567
  for (const ord of effects.rejectedOrds) {
19558
19568
  addSpanEvent("app effects rejected over the per-install caps", { level: "warn", attrs: { app: body.callee, ord } });
19559
19569
  }
@@ -19562,21 +19572,21 @@ var deliverRpcEffects = /* @__PURE__ */ __name(async (env, body, effects, instal
19562
19572
  slug: body.callee,
19563
19573
  groups: effects.accepted,
19564
19574
  depth: body.depth,
19565
- anonId: body.userId,
19575
+ anonId: userId,
19566
19576
  installs: [install],
19567
19577
  deliverPush: /* @__PURE__ */ __name((push) => {
19568
19578
  pushes.push(push);
19569
19579
  }, "deliverPush"),
19570
19580
  // The broker runs outside the user's AnonDO, so this reaches it the same way a push does.
19571
19581
  writeAttribution: /* @__PURE__ */ __name(async (slug, writes) => {
19572
- await anonStub(env, body.userId).appAttribution({ slug, writes }, currentTraceContext());
19582
+ await anonStub(env, userId).appAttribution({ slug, writes }, currentTraceContext());
19573
19583
  }, "writeAttribution"),
19574
19584
  writeContext: /* @__PURE__ */ __name(async (slug, writes) => {
19575
- await anonStub(env, body.userId).appContext({ slug, writes }, currentTraceContext());
19585
+ await anonStub(env, userId).appContext({ slug, writes }, currentTraceContext());
19576
19586
  }, "writeContext")
19577
19587
  });
19578
19588
  for (const push of pushes) {
19579
- await anonStub(env, body.userId).appPush(push, currentTraceContext());
19589
+ await anonStub(env, userId).appPush(push, currentTraceContext());
19580
19590
  }
19581
19591
  }, "deliverRpcEffects");
19582
19592
  var runRelayRpc = /* @__PURE__ */ __name(async (env, { context, body, callee, install }) => {
@@ -19593,7 +19603,8 @@ var runRelayRpc = /* @__PURE__ */ __name(async (env, { context, body, callee, in
19593
19603
  app: { slug: body.callee, version: callee.version },
19594
19604
  installs: [install],
19595
19605
  // No bucket projection for rpc: the callee sees only the subject's id, never its profile.
19596
- user: { anonId: body.userId, globalId: null, properties: {} },
19606
+ // Omitted entirely when the caller had no visitor, so the callee's ctx.user is null.
19607
+ ...body.userId === void 0 ? {} : { user: { anonId: body.userId, globalId: null, properties: {} } },
19597
19608
  depth: body.depth,
19598
19609
  // JSON.stringify drops an undefined key, and the envelope requires `args` to be present.
19599
19610
  rpc: { caller: body.slug, fn: body.fn, args: body.args ?? null }
@@ -19625,7 +19636,13 @@ var runRelayRpc = /* @__PURE__ */ __name(async (env, { context, body, callee, in
19625
19636
  if (verdict.rpc === null) {
19626
19637
  return calleeFailed(body.callee, "The response carries no rpc result.");
19627
19638
  }
19628
- await deliverRpcEffects(env, body, verdict.effects, install);
19639
+ if (body.userId === void 0) {
19640
+ if (verdict.effects.accepted.length > 0) {
19641
+ addSpanEvent("rpc effects dropped: the call carries no subject", { level: "warn", attrs: { app: body.callee } });
19642
+ }
19643
+ } else {
19644
+ await deliverRpcEffects(env, { body, userId: body.userId, effects: verdict.effects, install });
19645
+ }
19629
19646
  return Response.json({ result: verdict.rpc.result });
19630
19647
  }, "runRelayRpc");
19631
19648
  var relayRpc = /* @__PURE__ */ __name(async (env, input) => {
@@ -23822,7 +23839,7 @@ var declaredCapture = /* @__PURE__ */ __name((app2) => {
23822
23839
  if (queryParams.length === 0 && cookies.length === 0 || !app2.installs.some((install) => install.active)) {
23823
23840
  return void 0;
23824
23841
  }
23825
- return { queryParams, cookies };
23842
+ return { queryParams: queryParams.map((entry) => entry.name), cookies: cookies.map((entry) => entry.name) };
23826
23843
  }, "declaredCapture");
23827
23844
  var appsMetadata = /* @__PURE__ */ __name(() => {
23828
23845
  const { registry: registry3 } = siteRegistry();
@@ -24299,9 +24316,22 @@ var rowOf2 = /* @__PURE__ */ __name((row) => {
24299
24316
  value: row.value,
24300
24317
  createdAt: row.firstAt,
24301
24318
  updatedAt: row.observedAt,
24302
- source: row.source
24319
+ source: row.source,
24320
+ expiresAt: row.expiresAt
24303
24321
  };
24304
24322
  }, "rowOf");
24323
+ var deadline = /* @__PURE__ */ __name((sql, write, now) => {
24324
+ if (write.ttlMs === null) {
24325
+ return null;
24326
+ }
24327
+ const { firstSeen } = sql.exec(
24328
+ "SELECT MIN(observed_at) AS firstSeen FROM facts WHERE kind = 'attribution' AND scope = ? AND key = ? AND value = ?",
24329
+ write.appId,
24330
+ write.key,
24331
+ write.value
24332
+ ).one();
24333
+ return (firstSeen ?? now) + write.ttlMs;
24334
+ }, "deadline");
24305
24335
  var hydrate2 = /* @__PURE__ */ __name((sql, cache) => {
24306
24336
  let map2 = cache?.map();
24307
24337
  if (map2 === void 0) {
@@ -24321,16 +24351,18 @@ var withinCaps = /* @__PURE__ */ __name((write, stored) => {
24321
24351
  }
24322
24352
  return stored.has(write.key) || stored.size < MAX_ATTRIBUTION_KEYS_PER_APP;
24323
24353
  }, "withinCaps");
24324
- var persist2 = /* @__PURE__ */ __name((sql, write, now, previous) => {
24354
+ var persist2 = /* @__PURE__ */ __name((sql, write, state) => {
24355
+ const { now, previous, expiresAt: expiresAt2 } = state;
24325
24356
  const createdAt = previous?.createdAt ?? now;
24326
24357
  if (previous !== void 0 && previous.value === write.value) {
24327
24358
  sql.exec(
24328
- "UPDATE facts SET source = ? WHERE rowid = (SELECT rowid FROM facts WHERE kind = 'attribution' AND scope = ? AND key = ? ORDER BY observed_at DESC, rowid DESC LIMIT 1)",
24359
+ "UPDATE facts SET source = ?, expires_at = ? WHERE rowid = (SELECT rowid FROM facts WHERE kind = 'attribution' AND scope = ? AND key = ? ORDER BY observed_at DESC, rowid DESC LIMIT 1)",
24329
24360
  write.source,
24361
+ expiresAt2,
24330
24362
  write.appId,
24331
24363
  write.key
24332
24364
  );
24333
- return { value: write.value, createdAt, updatedAt: previous.updatedAt, source: write.source };
24365
+ return { value: write.value, createdAt, updatedAt: previous.updatedAt, source: write.source, expiresAt: expiresAt2 };
24334
24366
  }
24335
24367
  insertFact(sql, {
24336
24368
  kind: "attribution",
@@ -24339,9 +24371,10 @@ var persist2 = /* @__PURE__ */ __name((sql, write, now, previous) => {
24339
24371
  value: write.value,
24340
24372
  source: write.source,
24341
24373
  firstAt: createdAt,
24342
- observedAt: now
24374
+ observedAt: now,
24375
+ expiresAt: expiresAt2
24343
24376
  });
24344
- return { value: write.value, createdAt, updatedAt: now, source: write.source };
24377
+ return { value: write.value, createdAt, updatedAt: now, source: write.source, expiresAt: expiresAt2 };
24345
24378
  }, "persist");
24346
24379
  var writeAttribution = /* @__PURE__ */ __name((sql, writes, now, cache) => {
24347
24380
  const map2 = hydrate2(sql, cache);
@@ -24352,18 +24385,19 @@ var writeAttribution = /* @__PURE__ */ __name((sql, writes, now, cache) => {
24352
24385
  if (!withinCaps(write, forApp)) {
24353
24386
  continue;
24354
24387
  }
24355
- if (previous?.value !== write.value || previous.source !== write.source) {
24356
- forApp.set(write.key, persist2(sql, write, now, previous));
24388
+ const expiresAt2 = deadline(sql, write, now);
24389
+ if (previous?.value !== write.value || previous.source !== write.source || previous.expiresAt !== expiresAt2) {
24390
+ forApp.set(write.key, persist2(sql, write, { now, previous, expiresAt: expiresAt2 }));
24357
24391
  map2.set(write.appId, forApp);
24358
24392
  }
24359
24393
  accepted.push(write);
24360
24394
  }
24361
24395
  return accepted;
24362
24396
  }, "writeAttribution");
24363
- var allAttribution = /* @__PURE__ */ __name((sql, cache) => {
24397
+ var allAttribution = /* @__PURE__ */ __name((sql, now, cache) => {
24364
24398
  const all = {};
24365
24399
  for (const [appId, rows] of hydrate2(sql, cache)) {
24366
- all[appId] = Object.fromEntries(rows);
24400
+ all[appId] = Object.fromEntries([...rows].filter(([, row]) => row.expiresAt === null || row.expiresAt > now));
24367
24401
  }
24368
24402
  return all;
24369
24403
  }, "allAttribution");
@@ -24385,12 +24419,17 @@ var appWriteAllowed = /* @__PURE__ */ __name((registry3, consent2, slug, enforce
24385
24419
  }
24386
24420
  return true;
24387
24421
  }, "appWriteAllowed");
24388
- var declaredSource = /* @__PURE__ */ __name((app2, key) => {
24389
- if (app2.capture.queryParams.includes(key)) {
24390
- return "query";
24422
+ var declaration = /* @__PURE__ */ __name((app2, key) => {
24423
+ const param = app2.capture.queryParams.find((entry) => entry.name === key);
24424
+ const declared = param ?? app2.capture.cookies.find((entry) => entry.name === key);
24425
+ if (declared === void 0) {
24426
+ return void 0;
24391
24427
  }
24392
- return app2.capture.cookies.includes(key) ? "cookie" : void 0;
24393
- }, "declaredSource");
24428
+ return {
24429
+ source: param === void 0 ? "cookie" : "query",
24430
+ ttlMs: declared.expiresInSeconds === void 0 ? null : declared.expiresInSeconds * 1e3
24431
+ };
24432
+ }, "declaration");
24394
24433
  var acceptedCaptures = /* @__PURE__ */ __name((registry3, consent2, apps, enforced) => {
24395
24434
  const accepted = [];
24396
24435
  for (const [slug, values] of Object.entries(apps)) {
@@ -24399,8 +24438,8 @@ var acceptedCaptures = /* @__PURE__ */ __name((registry3, consent2, apps, enforc
24399
24438
  continue;
24400
24439
  }
24401
24440
  for (const [key, value] of Object.entries(values)) {
24402
- const source = declaredSource(app2, key);
24403
- if (source === void 0) {
24441
+ const declared = declaration(app2, key);
24442
+ if (declared === void 0) {
24404
24443
  addSpanEvent("capture dropped: the app declares no such name", { level: "warn", attrs: { app: slug, key } });
24405
24444
  continue;
24406
24445
  }
@@ -24408,7 +24447,7 @@ var acceptedCaptures = /* @__PURE__ */ __name((registry3, consent2, apps, enforc
24408
24447
  addSpanEvent("capture dropped: over the size cap", { level: "warn", attrs: { app: slug, key } });
24409
24448
  continue;
24410
24449
  }
24411
- accepted.push({ appId: slug, key, value, source });
24450
+ accepted.push({ appId: slug, key, value, source: declared.source, ttlMs: declared.ttlMs });
24412
24451
  }
24413
24452
  }
24414
24453
  return accepted;
@@ -24712,6 +24751,19 @@ var cachedKv = /* @__PURE__ */ __name((kv) => {
24712
24751
  };
24713
24752
  }, "cachedKv");
24714
24753
 
24754
+ // src/id-graph/migrate.ts
24755
+ var VERSION_KEY = "schemaVersion";
24756
+ var applyMigrations = /* @__PURE__ */ __name((storage, migrations) => {
24757
+ const applied = storage.kv.get(VERSION_KEY) ?? 0;
24758
+ for (const [index, migration] of migrations.entries()) {
24759
+ if (index < applied) {
24760
+ continue;
24761
+ }
24762
+ storage.sql.exec(migration);
24763
+ storage.kv.put(VERSION_KEY, index + 1);
24764
+ }
24765
+ }, "applyMigrations");
24766
+
24715
24767
  // src/id-graph/retention.ts
24716
24768
  var RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
24717
24769
  var CLEANUP_INTERVAL_MS = 7 * 24 * 60 * 60 * 1e3;
@@ -25223,8 +25275,8 @@ var expiryHorizon = /* @__PURE__ */ __name((storage, lastActivity) => {
25223
25275
  }, "expiryHorizon");
25224
25276
  var cleanupTarget = /* @__PURE__ */ __name((storage, now, lastActivity) => {
25225
25277
  const horizon = expiryHorizon(storage, lastActivity);
25226
- const deadline = nextRetentionDeadline(storage.sql);
25227
- const target = deadline === null ? horizon : Math.min(deadline, horizon);
25278
+ const deadline2 = nextRetentionDeadline(storage.sql);
25279
+ const target = deadline2 === null ? horizon : Math.min(deadline2, horizon);
25228
25280
  return Math.max(target, now + IMMEDIATE_CLEANUP_DELAY_MS);
25229
25281
  }, "cleanupTarget");
25230
25282
  var standsDown = /* @__PURE__ */ __name(async (storage, staleActivity) => {
@@ -25389,7 +25441,8 @@ var assignAll = /* @__PURE__ */ __name(async (env, sorted, claim) => {
25389
25441
  }, "assignAll");
25390
25442
 
25391
25443
  // src/id-graph/anon/schema.ts
25392
- var ANON_SCHEMA = `
25444
+ var ANON_MIGRATIONS = [
25445
+ `
25393
25446
  CREATE TABLE IF NOT EXISTS facts (
25394
25447
  kind TEXT NOT NULL,
25395
25448
  scope TEXT NOT NULL,
@@ -25406,7 +25459,9 @@ CREATE TABLE IF NOT EXISTS facts (
25406
25459
  );
25407
25460
  CREATE INDEX IF NOT EXISTS idx_facts_key ON facts (kind, scope, key, observed_at);
25408
25461
  CREATE UNIQUE INDEX IF NOT EXISTS idx_facts_conversion ON facts (key) WHERE kind = 'conversion';
25409
- `;
25462
+ `,
25463
+ "ALTER TABLE facts ADD COLUMN expires_at INTEGER;"
25464
+ ];
25410
25465
 
25411
25466
  // src/id-graph/anon/websocket.ts
25412
25467
  var MAX_BROWSER_RESULT_BYTES = 1024;
@@ -25600,7 +25655,7 @@ var AnonDO = class extends DurableObject2 {
25600
25655
  cleanupAlarm;
25601
25656
  constructor(ctx, env) {
25602
25657
  super(ctx, env);
25603
- ctx.storage.sql.exec(ANON_SCHEMA);
25658
+ applyMigrations(ctx.storage, ANON_MIGRATIONS);
25604
25659
  this.kvCache = cachedKv(ctx.storage.kv);
25605
25660
  this.cachedStorage = { kv: this.kvCache, sql: ctx.storage.sql };
25606
25661
  }
@@ -25755,7 +25810,7 @@ var AnonDO = class extends DurableObject2 {
25755
25810
  if (!appWriteAllowed(siteRegistry().registry, consentState2(this.kv), input.slug, consentEnforced(this.env))) {
25756
25811
  return;
25757
25812
  }
25758
- this.storeAttribution(input.writes.map((write) => ({ ...write, appId: input.slug, source: "app" })));
25813
+ this.storeAttribution(input.writes.map((write) => ({ ...write, appId: input.slug, source: "app", ttlMs: null })));
25759
25814
  });
25760
25815
  });
25761
25816
  }
@@ -25877,7 +25932,7 @@ var AnonDO = class extends DurableObject2 {
25877
25932
  if (!consentAllows(consentState2(this.kv), slug, app2.consent.category, consentEnforced(this.env))) {
25878
25933
  return null;
25879
25934
  }
25880
- return listAppFacts(this.sql, query);
25935
+ return listAppFacts(this.sql, query, Date.now());
25881
25936
  });
25882
25937
  });
25883
25938
  }
@@ -26057,7 +26112,7 @@ var AnonDO = class extends DurableObject2 {
26057
26112
  // A wipe dropped the sql tables, so reading them would throw — and reviving for a transform
26058
26113
  // snapshot would strand recreated storage when the chain fails or drops.
26059
26114
  custom: this.wiped ? {} : customProperties(this.sql, this.customPropsMirror),
26060
- attribution: this.wiped ? {} : allAttribution(this.sql, this.attributionMirror),
26115
+ attribution: this.wiped ? {} : allAttribution(this.sql, Date.now(), this.attributionMirror),
26061
26116
  context: this.wiped ? { private: {}, shared: {} } : {
26062
26117
  private: allContext(this.sql, "private", this.contextMirror),
26063
26118
  shared: readableSharedContext(
@@ -26393,7 +26448,7 @@ var AnonDO = class extends DurableObject2 {
26393
26448
  }
26394
26449
  revive() {
26395
26450
  if (this.wiped) {
26396
- this.ctx.storage.sql.exec(ANON_SCHEMA);
26451
+ applyMigrations(this.ctx.storage, ANON_MIGRATIONS);
26397
26452
  this.wiped = false;
26398
26453
  }
26399
26454
  }
@@ -26595,7 +26650,8 @@ var storedGlobalId = /* @__PURE__ */ __name((kv) => {
26595
26650
  }, "storedGlobalId");
26596
26651
 
26597
26652
  // src/id-graph/global/schema.ts
26598
- var GLOBAL_SCHEMA = `
26653
+ var GLOBAL_MIGRATIONS = [
26654
+ `
26599
26655
  CREATE TABLE IF NOT EXISTS facts (
26600
26656
  kind TEXT NOT NULL,
26601
26657
  scope TEXT NOT NULL,
@@ -26610,7 +26666,8 @@ CREATE TABLE IF NOT EXISTS facts (
26610
26666
  observed_at INTEGER NOT NULL
26611
26667
  );
26612
26668
  CREATE UNIQUE INDEX IF NOT EXISTS idx_facts_key ON facts (kind, scope, key);
26613
- `;
26669
+ `
26670
+ ];
26614
26671
 
26615
26672
  // src/id-graph/global/do.ts
26616
26673
  var GlobalDO = class extends DurableObject3 {
@@ -26623,7 +26680,7 @@ var GlobalDO = class extends DurableObject3 {
26623
26680
  wiped = false;
26624
26681
  constructor(ctx, env) {
26625
26682
  super(ctx, env);
26626
- ctx.storage.sql.exec(GLOBAL_SCHEMA);
26683
+ applyMigrations(ctx.storage, GLOBAL_MIGRATIONS);
26627
26684
  this.kvCache = cachedKv(ctx.storage.kv);
26628
26685
  this.identifiersMirror = identifiersCache2();
26629
26686
  }
@@ -26690,7 +26747,7 @@ var GlobalDO = class extends DurableObject3 {
26690
26747
  }
26691
26748
  revive() {
26692
26749
  if (this.wiped) {
26693
- this.ctx.storage.sql.exec(GLOBAL_SCHEMA);
26750
+ applyMigrations(this.ctx.storage, GLOBAL_MIGRATIONS);
26694
26751
  this.wiped = false;
26695
26752
  }
26696
26753
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "main": {
3
3
  "version": "0.1.0",
4
- "gitSha": "45acc2f65889b33f7cf2828b967a26fd879455f0",
5
- "builtAt": "2026-08-25T22:20:08.908Z",
4
+ "gitSha": "d2dbd3fccee9436ca4a4e3a6429925e6f9f4d0ce",
5
+ "builtAt": "2026-08-26T15:36:18.499Z",
6
6
  "compatibilityDate": "2026-06-23",
7
7
  "compatibilityFlags": [
8
8
  "nodejs_compat"
@@ -40,8 +40,8 @@
40
40
  },
41
41
  "app-runtime": {
42
42
  "version": "0.1.0",
43
- "gitSha": "45acc2f65889b33f7cf2828b967a26fd879455f0",
44
- "builtAt": "2026-08-25T22:08:45.522Z",
43
+ "gitSha": "d2dbd3fccee9436ca4a4e3a6429925e6f9f4d0ce",
44
+ "builtAt": "2026-08-26T15:36:22.261Z",
45
45
  "compatibilityDate": "2026-06-23",
46
46
  "compatibilityFlags": [
47
47
  "nodejs_compat"
@@ -57,8 +57,8 @@
57
57
  },
58
58
  "apps-ui": {
59
59
  "version": "0.1.0",
60
- "gitSha": "23b843bfa99a83ee577ee32b8e6ca9334b6f213c",
61
- "builtAt": "2026-08-18T05:43:57.464Z",
60
+ "gitSha": "d2dbd3fccee9436ca4a4e3a6429925e6f9f4d0ce",
61
+ "builtAt": "2026-08-26T15:50:13.405Z",
62
62
  "compatibilityDate": "2026-06-23",
63
63
  "compatibilityFlags": [
64
64
  "nodejs_compat"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blotout/qinto-cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "description": "Qinto CLI: authenticate against the Qinto platform and operate it from the terminal or from an agent.",
6
6
  "repository": {
@@ -36,7 +36,10 @@
36
36
  // "capture" names the query params and cookies the platform records against each visitor for
37
37
  // this app, which only this app reads back. Each name belongs to one list. Uncomment to use it —
38
38
  // a site manager sees the list and must accept any name a later version adds.
39
- // "capture": { "queryParams": ["fbclid"], "cookies": ["_fbp"] },
39
+ //
40
+ // "expiresInSeconds" is optional and runs from the first sighting of that value; seeing the same
41
+ // value again never extends it. Omit it to keep the value for the life of the visitor profile.
42
+ // "capture": { "queryParams": [{ "name": "fbclid", "expiresInSeconds": 604800 }], "cookies": [{ "name": "_fbp" }] },
40
43
  // eventResults lets the matching server install receive this browser handler's return value on
41
44
  // the same action as action.browser. It is untrusted page input and delivery stays fail-open.
42
45
  "browser": { "entry": "src/browser.ts", "eventResults": ["track"] },