@blotout/qinto-cli 0.1.6 → 0.1.8

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).
@@ -15913,7 +15917,14 @@ var scheduleSchema = external_exports.object({
15913
15917
  id: external_exports.string().min(1).max(32),
15914
15918
  cron: external_exports.string().min(1).max(MAX_CRON_LENGTH),
15915
15919
  scheduledTime: external_exports.number().int().nonnegative().safe(),
15916
- installs: external_exports.array(external_exports.object({ id: external_exports.string().min(1).max(64), ord: external_exports.number().int().min(1), label: external_exports.string().max(100) }).strict()).min(1).max(MAX_INSTALLS_PER_APP)
15920
+ installs: external_exports.array(
15921
+ external_exports.object({
15922
+ id: external_exports.string().min(1).max(64),
15923
+ ord: external_exports.number().int().min(1),
15924
+ label: external_exports.string().max(100),
15925
+ catalogQueryTicket: external_exports.string().min(1).max(2048)
15926
+ }).strict()
15927
+ ).min(1).max(MAX_INSTALLS_PER_APP)
15917
15928
  }).strict();
15918
15929
  var controlEnvelopeSchema = external_exports.object({
15919
15930
  v: external_exports.literal(CONTROL_ENVELOPE_VERSION),
@@ -18227,7 +18238,7 @@ var APP_FACT_KINDS = [
18227
18238
  ];
18228
18239
  var MAX_APP_FACTS_PER_PAGE = 100;
18229
18240
  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";
18241
+ var COLUMNS = "kind, scope, key, value, source, amount, currency, event_id, data_json, first_at, observed_at, synced_at, expires_at";
18231
18242
  var rowOf = /* @__PURE__ */ __name((row) => {
18232
18243
  return {
18233
18244
  kind: row.kind,
@@ -18241,20 +18252,21 @@ var rowOf = /* @__PURE__ */ __name((row) => {
18241
18252
  dataJson: row.data_json,
18242
18253
  firstAt: row.first_at,
18243
18254
  observedAt: row.observed_at,
18244
- syncedAt: row.synced_at
18255
+ syncedAt: row.synced_at,
18256
+ expiresAt: row.expires_at
18245
18257
  };
18246
18258
  }, "rowOf");
18247
18259
  var appFactOf = /* @__PURE__ */ __name((row) => {
18248
- const { syncedAt: _syncedAt, ...fact } = rowOf(row);
18260
+ const { syncedAt: _syncedAt, expiresAt: _expiresAt, ...fact } = rowOf(row);
18249
18261
  return { ...fact, kind: fact.kind };
18250
18262
  }, "appFactOf");
18251
- var listAppFacts = /* @__PURE__ */ __name((sql, query) => {
18263
+ var listAppFacts = /* @__PURE__ */ __name((sql, query, now) => {
18252
18264
  const maxRowId = query.maxRowId ?? sql.exec("SELECT MAX(rowid) AS maxRowId FROM facts").one().maxRowId;
18253
18265
  if (maxRowId === null) {
18254
18266
  return { items: [], next: null };
18255
18267
  }
18256
- const where = ["rowid > ?", "rowid <= ?"];
18257
- const params = [query.afterRowId, maxRowId];
18268
+ const where = ["rowid > ?", "rowid <= ?", "(expires_at IS NULL OR expires_at > ?)"];
18269
+ const params = [query.afterRowId, maxRowId, now];
18258
18270
  if (query.kinds.length > 0) {
18259
18271
  where.push(`kind IN (${query.kinds.map(() => "?").join(", ")})`);
18260
18272
  params.push(...query.kinds);
@@ -18287,7 +18299,7 @@ var listAppFacts = /* @__PURE__ */ __name((sql, query) => {
18287
18299
  }, "listAppFacts");
18288
18300
  var insertFact = /* @__PURE__ */ __name((sql, fact) => {
18289
18301
  sql.exec(
18290
- `INSERT INTO facts (${COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)`,
18302
+ `INSERT INTO facts (${COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?)`,
18291
18303
  fact.kind,
18292
18304
  fact.scope,
18293
18305
  fact.key,
@@ -18298,7 +18310,8 @@ var insertFact = /* @__PURE__ */ __name((sql, fact) => {
18298
18310
  fact.eventId ?? null,
18299
18311
  fact.dataJson ?? null,
18300
18312
  fact.firstAt ?? null,
18301
- fact.observedAt
18313
+ fact.observedAt,
18314
+ fact.expiresAt ?? null
18302
18315
  );
18303
18316
  }, "insertFact");
18304
18317
  var latestFact = /* @__PURE__ */ __name((sql, coords) => {
@@ -19051,7 +19064,7 @@ var stillDeclared = /* @__PURE__ */ __name((target, key, source) => {
19051
19064
  return true;
19052
19065
  }
19053
19066
  const { queryParams, cookies } = target.app.capture;
19054
- return source === "query" ? queryParams.includes(key) : cookies.includes(key);
19067
+ return source === "query" ? queryParams.some((entry) => entry.name === key) : cookies.some((entry) => entry.name === key);
19055
19068
  }, "stillDeclared");
19056
19069
  var deliverableAttribution = /* @__PURE__ */ __name((target, rows) => {
19057
19070
  if (rows === void 0) {
@@ -19339,9 +19352,12 @@ var appSchema = external_exports.object({
19339
19352
  // Method, auth, and mode stay plain strings for the same tolerance: an unknown value from a newer
19340
19353
  // registry refuses that one endpoint instead of turning the app — and every other app — off.
19341
19354
  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: [] })
19355
+ // The emitter omits the block entirely for an app that declares no capture, so the default is
19356
+ // what a manifest without a `browser.capture` serializes to.
19357
+ capture: external_exports.object({
19358
+ queryParams: external_exports.array(external_exports.object({ name: external_exports.string(), expiresInSeconds: external_exports.number().optional() }).loose()).default([]),
19359
+ cookies: external_exports.array(external_exports.object({ name: external_exports.string(), expiresInSeconds: external_exports.number().optional() }).loose()).default([])
19360
+ }).default({ queryParams: [], cookies: [] })
19345
19361
  });
19346
19362
  var registrySchema = external_exports.object({
19347
19363
  v: external_exports.literal(1),
@@ -19397,7 +19413,8 @@ var pushBody = external_exports.object({
19397
19413
  var rpcBody = external_exports.object({
19398
19414
  ...identityFields,
19399
19415
  kind: external_exports.literal("rpc"),
19400
- userId: external_exports.string().min(1).max(64),
19416
+ // Absent when the caller is an endpoint, a schedule or a ui handler; the callee then sees no user.
19417
+ userId: external_exports.string().min(1).max(64).optional(),
19401
19418
  callee: external_exports.string().min(1).max(30),
19402
19419
  fn: external_exports.string().min(1).max(64),
19403
19420
  args: external_exports.unknown().optional(),
@@ -19553,7 +19570,7 @@ var calleeFailed = /* @__PURE__ */ __name((callee, reason) => {
19553
19570
  addSpanEvent("rpc callee failed", { level: "warn", attrs: { app: callee, reason } });
19554
19571
  return Response.json({ error: "callee_failed" }, { status: 502 });
19555
19572
  }, "calleeFailed");
19556
- var deliverRpcEffects = /* @__PURE__ */ __name(async (env, body, effects, install) => {
19573
+ var deliverRpcEffects = /* @__PURE__ */ __name(async (env, { body, userId, effects, install }) => {
19557
19574
  for (const ord of effects.rejectedOrds) {
19558
19575
  addSpanEvent("app effects rejected over the per-install caps", { level: "warn", attrs: { app: body.callee, ord } });
19559
19576
  }
@@ -19562,21 +19579,21 @@ var deliverRpcEffects = /* @__PURE__ */ __name(async (env, body, effects, instal
19562
19579
  slug: body.callee,
19563
19580
  groups: effects.accepted,
19564
19581
  depth: body.depth,
19565
- anonId: body.userId,
19582
+ anonId: userId,
19566
19583
  installs: [install],
19567
19584
  deliverPush: /* @__PURE__ */ __name((push) => {
19568
19585
  pushes.push(push);
19569
19586
  }, "deliverPush"),
19570
19587
  // The broker runs outside the user's AnonDO, so this reaches it the same way a push does.
19571
19588
  writeAttribution: /* @__PURE__ */ __name(async (slug, writes) => {
19572
- await anonStub(env, body.userId).appAttribution({ slug, writes }, currentTraceContext());
19589
+ await anonStub(env, userId).appAttribution({ slug, writes }, currentTraceContext());
19573
19590
  }, "writeAttribution"),
19574
19591
  writeContext: /* @__PURE__ */ __name(async (slug, writes) => {
19575
- await anonStub(env, body.userId).appContext({ slug, writes }, currentTraceContext());
19592
+ await anonStub(env, userId).appContext({ slug, writes }, currentTraceContext());
19576
19593
  }, "writeContext")
19577
19594
  });
19578
19595
  for (const push of pushes) {
19579
- await anonStub(env, body.userId).appPush(push, currentTraceContext());
19596
+ await anonStub(env, userId).appPush(push, currentTraceContext());
19580
19597
  }
19581
19598
  }, "deliverRpcEffects");
19582
19599
  var runRelayRpc = /* @__PURE__ */ __name(async (env, { context, body, callee, install }) => {
@@ -19593,7 +19610,8 @@ var runRelayRpc = /* @__PURE__ */ __name(async (env, { context, body, callee, in
19593
19610
  app: { slug: body.callee, version: callee.version },
19594
19611
  installs: [install],
19595
19612
  // No bucket projection for rpc: the callee sees only the subject's id, never its profile.
19596
- user: { anonId: body.userId, globalId: null, properties: {} },
19613
+ // Omitted entirely when the caller had no visitor, so the callee's ctx.user is null.
19614
+ ...body.userId === void 0 ? {} : { user: { anonId: body.userId, globalId: null, properties: {} } },
19597
19615
  depth: body.depth,
19598
19616
  // JSON.stringify drops an undefined key, and the envelope requires `args` to be present.
19599
19617
  rpc: { caller: body.slug, fn: body.fn, args: body.args ?? null }
@@ -19625,7 +19643,13 @@ var runRelayRpc = /* @__PURE__ */ __name(async (env, { context, body, callee, in
19625
19643
  if (verdict.rpc === null) {
19626
19644
  return calleeFailed(body.callee, "The response carries no rpc result.");
19627
19645
  }
19628
- await deliverRpcEffects(env, body, verdict.effects, install);
19646
+ if (body.userId === void 0) {
19647
+ if (verdict.effects.accepted.length > 0) {
19648
+ addSpanEvent("rpc effects dropped: the call carries no subject", { level: "warn", attrs: { app: body.callee } });
19649
+ }
19650
+ } else {
19651
+ await deliverRpcEffects(env, { body, userId: body.userId, effects: verdict.effects, install });
19652
+ }
19629
19653
  return Response.json({ result: verdict.rpc.result });
19630
19654
  }, "runRelayRpc");
19631
19655
  var relayRpc = /* @__PURE__ */ __name(async (env, input) => {
@@ -20523,10 +20547,24 @@ var verifyAppUiToken = /* @__PURE__ */ __name(async (input) => {
20523
20547
  return { ok: true, claims: claims.data, kid: signature.kid, key: signature.key };
20524
20548
  }, "verifyAppUiToken");
20525
20549
 
20550
+ // ../../libs/app-ui-token/src/schedule-catalog-ticket.ts
20551
+ var APP_SCHEDULE_CATALOG_TICKET_ISSUER = "qinto-portal";
20552
+ var claimsSchema2 = external_exports.object({
20553
+ iss: external_exports.literal(APP_SCHEDULE_CATALOG_TICKET_ISSUER),
20554
+ aud: external_exports.string().min(1),
20555
+ sub: external_exports.string().min(1),
20556
+ iat: external_exports.number().int(),
20557
+ exp: external_exports.number().int(),
20558
+ domainId: external_exports.string().min(1),
20559
+ appId: external_exports.string().min(1),
20560
+ appInstallId: external_exports.string().min(1),
20561
+ appVersionId: external_exports.string().min(1)
20562
+ }).strict();
20563
+
20526
20564
  // ../../libs/app-ui-token/src/write-ticket.ts
20527
20565
  var APP_UI_WRITE_TICKET_TTL_SECONDS = 60;
20528
20566
  var APP_UI_WRITE_TICKET_ISSUER = "qinto-site";
20529
- var claimsSchema2 = external_exports.object({
20567
+ var claimsSchema3 = external_exports.object({
20530
20568
  iss: external_exports.literal(APP_UI_WRITE_TICKET_ISSUER),
20531
20569
  aud: external_exports.string().min(1),
20532
20570
  sub: external_exports.string().min(1),
@@ -23822,7 +23860,7 @@ var declaredCapture = /* @__PURE__ */ __name((app2) => {
23822
23860
  if (queryParams.length === 0 && cookies.length === 0 || !app2.installs.some((install) => install.active)) {
23823
23861
  return void 0;
23824
23862
  }
23825
- return { queryParams, cookies };
23863
+ return { queryParams: queryParams.map((entry) => entry.name), cookies: cookies.map((entry) => entry.name) };
23826
23864
  }, "declaredCapture");
23827
23865
  var appsMetadata = /* @__PURE__ */ __name(() => {
23828
23866
  const { registry: registry3 } = siteRegistry();
@@ -24299,9 +24337,22 @@ var rowOf2 = /* @__PURE__ */ __name((row) => {
24299
24337
  value: row.value,
24300
24338
  createdAt: row.firstAt,
24301
24339
  updatedAt: row.observedAt,
24302
- source: row.source
24340
+ source: row.source,
24341
+ expiresAt: row.expiresAt
24303
24342
  };
24304
24343
  }, "rowOf");
24344
+ var deadline = /* @__PURE__ */ __name((sql, write, now) => {
24345
+ if (write.ttlMs === null) {
24346
+ return null;
24347
+ }
24348
+ const { firstSeen } = sql.exec(
24349
+ "SELECT MIN(observed_at) AS firstSeen FROM facts WHERE kind = 'attribution' AND scope = ? AND key = ? AND value = ?",
24350
+ write.appId,
24351
+ write.key,
24352
+ write.value
24353
+ ).one();
24354
+ return (firstSeen ?? now) + write.ttlMs;
24355
+ }, "deadline");
24305
24356
  var hydrate2 = /* @__PURE__ */ __name((sql, cache) => {
24306
24357
  let map2 = cache?.map();
24307
24358
  if (map2 === void 0) {
@@ -24321,16 +24372,18 @@ var withinCaps = /* @__PURE__ */ __name((write, stored) => {
24321
24372
  }
24322
24373
  return stored.has(write.key) || stored.size < MAX_ATTRIBUTION_KEYS_PER_APP;
24323
24374
  }, "withinCaps");
24324
- var persist2 = /* @__PURE__ */ __name((sql, write, now, previous) => {
24375
+ var persist2 = /* @__PURE__ */ __name((sql, write, state) => {
24376
+ const { now, previous, expiresAt: expiresAt2 } = state;
24325
24377
  const createdAt = previous?.createdAt ?? now;
24326
24378
  if (previous !== void 0 && previous.value === write.value) {
24327
24379
  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)",
24380
+ "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
24381
  write.source,
24382
+ expiresAt2,
24330
24383
  write.appId,
24331
24384
  write.key
24332
24385
  );
24333
- return { value: write.value, createdAt, updatedAt: previous.updatedAt, source: write.source };
24386
+ return { value: write.value, createdAt, updatedAt: previous.updatedAt, source: write.source, expiresAt: expiresAt2 };
24334
24387
  }
24335
24388
  insertFact(sql, {
24336
24389
  kind: "attribution",
@@ -24339,9 +24392,10 @@ var persist2 = /* @__PURE__ */ __name((sql, write, now, previous) => {
24339
24392
  value: write.value,
24340
24393
  source: write.source,
24341
24394
  firstAt: createdAt,
24342
- observedAt: now
24395
+ observedAt: now,
24396
+ expiresAt: expiresAt2
24343
24397
  });
24344
- return { value: write.value, createdAt, updatedAt: now, source: write.source };
24398
+ return { value: write.value, createdAt, updatedAt: now, source: write.source, expiresAt: expiresAt2 };
24345
24399
  }, "persist");
24346
24400
  var writeAttribution = /* @__PURE__ */ __name((sql, writes, now, cache) => {
24347
24401
  const map2 = hydrate2(sql, cache);
@@ -24352,18 +24406,19 @@ var writeAttribution = /* @__PURE__ */ __name((sql, writes, now, cache) => {
24352
24406
  if (!withinCaps(write, forApp)) {
24353
24407
  continue;
24354
24408
  }
24355
- if (previous?.value !== write.value || previous.source !== write.source) {
24356
- forApp.set(write.key, persist2(sql, write, now, previous));
24409
+ const expiresAt2 = deadline(sql, write, now);
24410
+ if (previous?.value !== write.value || previous.source !== write.source || previous.expiresAt !== expiresAt2) {
24411
+ forApp.set(write.key, persist2(sql, write, { now, previous, expiresAt: expiresAt2 }));
24357
24412
  map2.set(write.appId, forApp);
24358
24413
  }
24359
24414
  accepted.push(write);
24360
24415
  }
24361
24416
  return accepted;
24362
24417
  }, "writeAttribution");
24363
- var allAttribution = /* @__PURE__ */ __name((sql, cache) => {
24418
+ var allAttribution = /* @__PURE__ */ __name((sql, now, cache) => {
24364
24419
  const all = {};
24365
24420
  for (const [appId, rows] of hydrate2(sql, cache)) {
24366
- all[appId] = Object.fromEntries(rows);
24421
+ all[appId] = Object.fromEntries([...rows].filter(([, row]) => row.expiresAt === null || row.expiresAt > now));
24367
24422
  }
24368
24423
  return all;
24369
24424
  }, "allAttribution");
@@ -24385,12 +24440,17 @@ var appWriteAllowed = /* @__PURE__ */ __name((registry3, consent2, slug, enforce
24385
24440
  }
24386
24441
  return true;
24387
24442
  }, "appWriteAllowed");
24388
- var declaredSource = /* @__PURE__ */ __name((app2, key) => {
24389
- if (app2.capture.queryParams.includes(key)) {
24390
- return "query";
24443
+ var declaration = /* @__PURE__ */ __name((app2, key) => {
24444
+ const param = app2.capture.queryParams.find((entry) => entry.name === key);
24445
+ const declared = param ?? app2.capture.cookies.find((entry) => entry.name === key);
24446
+ if (declared === void 0) {
24447
+ return void 0;
24391
24448
  }
24392
- return app2.capture.cookies.includes(key) ? "cookie" : void 0;
24393
- }, "declaredSource");
24449
+ return {
24450
+ source: param === void 0 ? "cookie" : "query",
24451
+ ttlMs: declared.expiresInSeconds === void 0 ? null : declared.expiresInSeconds * 1e3
24452
+ };
24453
+ }, "declaration");
24394
24454
  var acceptedCaptures = /* @__PURE__ */ __name((registry3, consent2, apps, enforced) => {
24395
24455
  const accepted = [];
24396
24456
  for (const [slug, values] of Object.entries(apps)) {
@@ -24399,8 +24459,8 @@ var acceptedCaptures = /* @__PURE__ */ __name((registry3, consent2, apps, enforc
24399
24459
  continue;
24400
24460
  }
24401
24461
  for (const [key, value] of Object.entries(values)) {
24402
- const source = declaredSource(app2, key);
24403
- if (source === void 0) {
24462
+ const declared = declaration(app2, key);
24463
+ if (declared === void 0) {
24404
24464
  addSpanEvent("capture dropped: the app declares no such name", { level: "warn", attrs: { app: slug, key } });
24405
24465
  continue;
24406
24466
  }
@@ -24408,7 +24468,7 @@ var acceptedCaptures = /* @__PURE__ */ __name((registry3, consent2, apps, enforc
24408
24468
  addSpanEvent("capture dropped: over the size cap", { level: "warn", attrs: { app: slug, key } });
24409
24469
  continue;
24410
24470
  }
24411
- accepted.push({ appId: slug, key, value, source });
24471
+ accepted.push({ appId: slug, key, value, source: declared.source, ttlMs: declared.ttlMs });
24412
24472
  }
24413
24473
  }
24414
24474
  return accepted;
@@ -24712,6 +24772,19 @@ var cachedKv = /* @__PURE__ */ __name((kv) => {
24712
24772
  };
24713
24773
  }, "cachedKv");
24714
24774
 
24775
+ // src/id-graph/migrate.ts
24776
+ var VERSION_KEY = "schemaVersion";
24777
+ var applyMigrations = /* @__PURE__ */ __name((storage, migrations) => {
24778
+ const applied = storage.kv.get(VERSION_KEY) ?? 0;
24779
+ for (const [index, migration] of migrations.entries()) {
24780
+ if (index < applied) {
24781
+ continue;
24782
+ }
24783
+ storage.sql.exec(migration);
24784
+ storage.kv.put(VERSION_KEY, index + 1);
24785
+ }
24786
+ }, "applyMigrations");
24787
+
24715
24788
  // src/id-graph/retention.ts
24716
24789
  var RETENTION_MS = 90 * 24 * 60 * 60 * 1e3;
24717
24790
  var CLEANUP_INTERVAL_MS = 7 * 24 * 60 * 60 * 1e3;
@@ -25223,8 +25296,8 @@ var expiryHorizon = /* @__PURE__ */ __name((storage, lastActivity) => {
25223
25296
  }, "expiryHorizon");
25224
25297
  var cleanupTarget = /* @__PURE__ */ __name((storage, now, lastActivity) => {
25225
25298
  const horizon = expiryHorizon(storage, lastActivity);
25226
- const deadline = nextRetentionDeadline(storage.sql);
25227
- const target = deadline === null ? horizon : Math.min(deadline, horizon);
25299
+ const deadline2 = nextRetentionDeadline(storage.sql);
25300
+ const target = deadline2 === null ? horizon : Math.min(deadline2, horizon);
25228
25301
  return Math.max(target, now + IMMEDIATE_CLEANUP_DELAY_MS);
25229
25302
  }, "cleanupTarget");
25230
25303
  var standsDown = /* @__PURE__ */ __name(async (storage, staleActivity) => {
@@ -25389,7 +25462,8 @@ var assignAll = /* @__PURE__ */ __name(async (env, sorted, claim) => {
25389
25462
  }, "assignAll");
25390
25463
 
25391
25464
  // src/id-graph/anon/schema.ts
25392
- var ANON_SCHEMA = `
25465
+ var ANON_MIGRATIONS = [
25466
+ `
25393
25467
  CREATE TABLE IF NOT EXISTS facts (
25394
25468
  kind TEXT NOT NULL,
25395
25469
  scope TEXT NOT NULL,
@@ -25406,7 +25480,9 @@ CREATE TABLE IF NOT EXISTS facts (
25406
25480
  );
25407
25481
  CREATE INDEX IF NOT EXISTS idx_facts_key ON facts (kind, scope, key, observed_at);
25408
25482
  CREATE UNIQUE INDEX IF NOT EXISTS idx_facts_conversion ON facts (key) WHERE kind = 'conversion';
25409
- `;
25483
+ `,
25484
+ "ALTER TABLE facts ADD COLUMN expires_at INTEGER;"
25485
+ ];
25410
25486
 
25411
25487
  // src/id-graph/anon/websocket.ts
25412
25488
  var MAX_BROWSER_RESULT_BYTES = 1024;
@@ -25600,7 +25676,7 @@ var AnonDO = class extends DurableObject2 {
25600
25676
  cleanupAlarm;
25601
25677
  constructor(ctx, env) {
25602
25678
  super(ctx, env);
25603
- ctx.storage.sql.exec(ANON_SCHEMA);
25679
+ applyMigrations(ctx.storage, ANON_MIGRATIONS);
25604
25680
  this.kvCache = cachedKv(ctx.storage.kv);
25605
25681
  this.cachedStorage = { kv: this.kvCache, sql: ctx.storage.sql };
25606
25682
  }
@@ -25755,7 +25831,7 @@ var AnonDO = class extends DurableObject2 {
25755
25831
  if (!appWriteAllowed(siteRegistry().registry, consentState2(this.kv), input.slug, consentEnforced(this.env))) {
25756
25832
  return;
25757
25833
  }
25758
- this.storeAttribution(input.writes.map((write) => ({ ...write, appId: input.slug, source: "app" })));
25834
+ this.storeAttribution(input.writes.map((write) => ({ ...write, appId: input.slug, source: "app", ttlMs: null })));
25759
25835
  });
25760
25836
  });
25761
25837
  }
@@ -25877,7 +25953,7 @@ var AnonDO = class extends DurableObject2 {
25877
25953
  if (!consentAllows(consentState2(this.kv), slug, app2.consent.category, consentEnforced(this.env))) {
25878
25954
  return null;
25879
25955
  }
25880
- return listAppFacts(this.sql, query);
25956
+ return listAppFacts(this.sql, query, Date.now());
25881
25957
  });
25882
25958
  });
25883
25959
  }
@@ -26057,7 +26133,7 @@ var AnonDO = class extends DurableObject2 {
26057
26133
  // A wipe dropped the sql tables, so reading them would throw — and reviving for a transform
26058
26134
  // snapshot would strand recreated storage when the chain fails or drops.
26059
26135
  custom: this.wiped ? {} : customProperties(this.sql, this.customPropsMirror),
26060
- attribution: this.wiped ? {} : allAttribution(this.sql, this.attributionMirror),
26136
+ attribution: this.wiped ? {} : allAttribution(this.sql, Date.now(), this.attributionMirror),
26061
26137
  context: this.wiped ? { private: {}, shared: {} } : {
26062
26138
  private: allContext(this.sql, "private", this.contextMirror),
26063
26139
  shared: readableSharedContext(
@@ -26393,7 +26469,7 @@ var AnonDO = class extends DurableObject2 {
26393
26469
  }
26394
26470
  revive() {
26395
26471
  if (this.wiped) {
26396
- this.ctx.storage.sql.exec(ANON_SCHEMA);
26472
+ applyMigrations(this.ctx.storage, ANON_MIGRATIONS);
26397
26473
  this.wiped = false;
26398
26474
  }
26399
26475
  }
@@ -26595,7 +26671,8 @@ var storedGlobalId = /* @__PURE__ */ __name((kv) => {
26595
26671
  }, "storedGlobalId");
26596
26672
 
26597
26673
  // src/id-graph/global/schema.ts
26598
- var GLOBAL_SCHEMA = `
26674
+ var GLOBAL_MIGRATIONS = [
26675
+ `
26599
26676
  CREATE TABLE IF NOT EXISTS facts (
26600
26677
  kind TEXT NOT NULL,
26601
26678
  scope TEXT NOT NULL,
@@ -26610,7 +26687,8 @@ CREATE TABLE IF NOT EXISTS facts (
26610
26687
  observed_at INTEGER NOT NULL
26611
26688
  );
26612
26689
  CREATE UNIQUE INDEX IF NOT EXISTS idx_facts_key ON facts (kind, scope, key);
26613
- `;
26690
+ `
26691
+ ];
26614
26692
 
26615
26693
  // src/id-graph/global/do.ts
26616
26694
  var GlobalDO = class extends DurableObject3 {
@@ -26623,7 +26701,7 @@ var GlobalDO = class extends DurableObject3 {
26623
26701
  wiped = false;
26624
26702
  constructor(ctx, env) {
26625
26703
  super(ctx, env);
26626
- ctx.storage.sql.exec(GLOBAL_SCHEMA);
26704
+ applyMigrations(ctx.storage, GLOBAL_MIGRATIONS);
26627
26705
  this.kvCache = cachedKv(ctx.storage.kv);
26628
26706
  this.identifiersMirror = identifiersCache2();
26629
26707
  }
@@ -26690,7 +26768,7 @@ var GlobalDO = class extends DurableObject3 {
26690
26768
  }
26691
26769
  revive() {
26692
26770
  if (this.wiped) {
26693
- this.ctx.storage.sql.exec(GLOBAL_SCHEMA);
26771
+ applyMigrations(this.ctx.storage, GLOBAL_MIGRATIONS);
26694
26772
  this.wiped = false;
26695
26773
  }
26696
26774
  }
@@ -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": "38683fc9a0a7f29125a35ad77e2c0c701992c82b",
5
+ "builtAt": "2026-08-27T07:08:20.610Z",
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": "db5a262cc87eefb7191739982830998dd648ff8b",
44
+ "builtAt": "2026-08-27T05:45:00.519Z",
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.6",
3
+ "version": "0.1.8",
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"] },