@blotout/qinto-cli 0.1.30 → 0.1.31

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.
@@ -21071,6 +21071,7 @@ var endpointSchema = external_exports.object({
21071
21071
  var eventsSchema = external_exports.object({
21072
21072
  publishes: external_exports.array(external_exports.string().min(1).max(MAX_EVENT_TOPIC_LENGTH).regex(EVENT_TOPIC)).max(MAX_PUBLISHED_TOPICS).default([])
21073
21073
  }).strict();
21074
+ var dependenciesSchema = external_exports.array(external_exports.object({ app: external_exports.string().regex(APP_SLUG_PATTERN), description: renderableText(1, 200).optional() }).strict()).max(10);
21074
21075
  var rpcSchema = external_exports.object({
21075
21076
  exposes: external_exports.array(external_exports.object({ name: external_exports.string().regex(FUNCTION_NAME).max(64), visibility: external_exports.enum(["public", "private"]) }).strict()).max(32).optional(),
21076
21077
  calls: external_exports.array(external_exports.object({ app: external_exports.string().regex(APP_SLUG_PATTERN), fn: external_exports.string().regex(FUNCTION_NAME).max(64) }).strict()).max(64).optional()
@@ -21114,6 +21115,9 @@ var baseSchema = external_exports.object({
21114
21115
  description: "The app's browser surface: an optional entry for the bundle that ships inside the site's q.js, and the query params and cookies it declares for capture."
21115
21116
  }),
21116
21117
  resources: external_exports.array(resourceSchema).max(32).default([]).meta({ description: "The Cloudflare bindings the app worker is given." }),
21118
+ dependencies: dependenciesSchema.optional().meta({
21119
+ description: "The apps this app needs on the same site. Every app it calls over RPC must be listed."
21120
+ }),
21117
21121
  rpc: rpcSchema.optional().meta({ description: "The functions this app exposes to other apps, and the ones it calls on them." }),
21118
21122
  events: eventsSchema.optional().meta({
21119
21123
  description: "The push topics this app broadcasts to every app on the site. Requires the `sharedContext` data capability; a topic left out reaches only this app\u2019s own browser code."
@@ -21430,12 +21434,33 @@ var validateRpc = /* @__PURE__ */ __name((manifest, ctx) => {
21430
21434
  ["rpc", "exposes"],
21431
21435
  "function"
21432
21436
  );
21437
+ const dependencies = new Set((manifest.dependencies ?? []).map((dependency) => dependency.app));
21433
21438
  for (const [index, call] of (manifest.rpc?.calls ?? []).entries()) {
21434
21439
  if (call.app === manifest.slug) {
21435
21440
  ctx.addIssue({ code: "custom", path: ["rpc", "calls", index, "app"], message: "An app cannot declare an RPC call to itself" });
21441
+ } else if (!dependencies.has(call.app)) {
21442
+ ctx.addIssue({
21443
+ code: "custom",
21444
+ path: ["rpc", "calls", index, "app"],
21445
+ message: `${call.app} is called over RPC, so it must be in dependencies. Add { "app": "${call.app}" } to dependencies; if your Qinto CLI rejects that field, upgrade it.`
21446
+ });
21436
21447
  }
21437
21448
  }
21438
21449
  }, "validateRpc");
21450
+ var validateDependencies = /* @__PURE__ */ __name((manifest, ctx) => {
21451
+ const dependencies = manifest.dependencies ?? [];
21452
+ uniqueOrIssue(
21453
+ ctx,
21454
+ dependencies.map((dependency) => dependency.app),
21455
+ ["dependencies"],
21456
+ "dependency"
21457
+ );
21458
+ for (const [index, dependency] of dependencies.entries()) {
21459
+ if (dependency.app === manifest.slug) {
21460
+ ctx.addIssue({ code: "custom", path: ["dependencies", index, "app"], message: "An app cannot depend on itself" });
21461
+ }
21462
+ }
21463
+ }, "validateDependencies");
21439
21464
  var validateServer = /* @__PURE__ */ __name((manifest, ctx) => {
21440
21465
  uniqueOrIssue(ctx, manifest.server.hooks.transform, ["server", "hooks", "transform"], "transform hook");
21441
21466
  uniqueOrIssue(ctx, manifest.server.hooks.onEvent, ["server", "hooks", "onEvent"], "event hook");
@@ -21483,6 +21508,7 @@ var appManifestSchema = baseSchema.superRefine((manifest, ctx) => {
21483
21508
  validateEndpoints(manifest, ctx);
21484
21509
  validateCapture(manifest, ctx);
21485
21510
  validateBrowserHooks(manifest, ctx);
21511
+ validateDependencies(manifest, ctx);
21486
21512
  validateRpc(manifest, ctx);
21487
21513
  validateEvents(manifest, ctx);
21488
21514
  validateOauth(manifest, ctx);
@@ -21511,6 +21537,44 @@ var storageLinkBindingName = /* @__PURE__ */ __name((domainId) => {
21511
21537
  }, "storageLinkBindingName");
21512
21538
  var MAX_APPS_Q_BYTES = 4 * 1024 * 1024;
21513
21539
 
21540
+ // ../../libs/app-envelope/src/geo.ts
21541
+ var cfString = external_exports.string().optional().catch(void 0);
21542
+ var requestGeoSchema = external_exports.object({
21543
+ country: cfString,
21544
+ city: cfString,
21545
+ region: cfString,
21546
+ regionCode: cfString,
21547
+ timezone: cfString,
21548
+ postalCode: cfString,
21549
+ continent: cfString,
21550
+ latitude: cfString,
21551
+ longitude: cfString,
21552
+ isEUCountry: cfString,
21553
+ botManagement: external_exports.object({
21554
+ ja4: cfString,
21555
+ ja3Hash: cfString,
21556
+ score: external_exports.number().optional().catch(void 0),
21557
+ verifiedBot: external_exports.boolean().optional().catch(void 0)
21558
+ }).optional().catch(void 0)
21559
+ });
21560
+ var userGeoSchema = external_exports.object({
21561
+ ip: external_exports.string().optional(),
21562
+ userAgent: external_exports.string().optional(),
21563
+ timezone: external_exports.string().optional(),
21564
+ city: external_exports.string().optional(),
21565
+ postalCode: external_exports.string().optional(),
21566
+ region: external_exports.string().optional(),
21567
+ regionCode: external_exports.string().optional(),
21568
+ country: external_exports.string().optional(),
21569
+ continent: external_exports.string().optional(),
21570
+ isEUCountry: external_exports.string().optional(),
21571
+ latitude: external_exports.string().optional(),
21572
+ longitude: external_exports.string().optional(),
21573
+ ja4: external_exports.string().optional(),
21574
+ ja3: external_exports.string().optional(),
21575
+ botScore: external_exports.string().optional()
21576
+ });
21577
+
21514
21578
  // ../../libs/app-envelope/src/envelope.ts
21515
21579
  var ENVELOPE_VERSION = 3;
21516
21580
  var ENVELOPE_ERROR_CODES = ["invalid_arguments"];
@@ -21588,11 +21652,21 @@ var envelopeRequestSchema = external_exports.object({
21588
21652
  anonId: external_exports.string().min(1).max(64),
21589
21653
  globalId: external_exports.string().min(1).max(64).nullable(),
21590
21654
  createdAt: external_exports.number().int().optional(),
21591
- properties: external_exports.record(external_exports.string(), external_exports.unknown()),
21592
- geo: external_exports.record(external_exports.string(), external_exports.string()).optional()
21655
+ properties: external_exports.record(external_exports.string(), external_exports.string()),
21656
+ geo: userGeoSchema.optional()
21657
+ }).strict().optional(),
21658
+ consent: external_exports.object({
21659
+ enabled: external_exports.boolean(),
21660
+ categories: external_exports.object({
21661
+ necessary: external_exports.literal(true),
21662
+ functional: external_exports.boolean(),
21663
+ analytics: external_exports.boolean(),
21664
+ marketing: external_exports.boolean(),
21665
+ salesOfData: external_exports.boolean()
21666
+ }),
21667
+ apps: external_exports.record(external_exports.string(), external_exports.boolean())
21593
21668
  }).strict().optional(),
21594
- 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(),
21595
- geo: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
21669
+ geo: requestGeoSchema.optional(),
21596
21670
  // Connect envelopes only: the page the visitor's socket opened from, and the visit session it joined.
21597
21671
  page: pageSchema.optional(),
21598
21672
  session: sessionSchema.optional(),
@@ -21900,7 +21974,7 @@ var endpointContextSchema = external_exports.object({
21900
21974
  method: external_exports.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
21901
21975
  mode: external_exports.enum(["webhook", "api"]),
21902
21976
  version: external_exports.string().min(1).max(32),
21903
- geo: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
21977
+ geo: requestGeoSchema.optional(),
21904
21978
  installs: external_exports.array(installSchema3).min(1).max(15),
21905
21979
  user: external_exports.object({ anonId: external_exports.string().min(1).max(64) }).strict().optional()
21906
21980
  }).strict().refine((context) => {
@@ -26665,6 +26739,26 @@ var siteRefOf = /* @__PURE__ */ __name(({ domainId, fingerprint }) => {
26665
26739
 
26666
26740
  // ../../libs/cf-data/src/cf-data.ts
26667
26741
  var CF_DATA_HEADER = "x-cf-data";
26742
+ var cfDataFrom = /* @__PURE__ */ __name((cf) => {
26743
+ return {
26744
+ country: cf?.country,
26745
+ city: cf?.city,
26746
+ region: cf?.region,
26747
+ regionCode: cf?.regionCode,
26748
+ timezone: cf?.timezone,
26749
+ postalCode: cf?.postalCode,
26750
+ continent: cf?.continent,
26751
+ longitude: cf?.longitude,
26752
+ latitude: cf?.latitude,
26753
+ isEUCountry: cf?.isEUCountry,
26754
+ botManagement: {
26755
+ ja4: cf?.botManagement?.ja4,
26756
+ ja3Hash: cf?.botManagement?.ja3Hash,
26757
+ score: cf?.botManagement?.score,
26758
+ verifiedBot: cf?.botManagement?.verifiedBot
26759
+ }
26760
+ };
26761
+ }, "cfDataFrom");
26668
26762
  var parseCfData = /* @__PURE__ */ __name((raw2) => {
26669
26763
  if (raw2 === null) {
26670
26764
  return void 0;
@@ -28125,7 +28219,7 @@ var BOT_SCORE_MIN = 1;
28125
28219
  var BOT_SCORE_MAX = 99;
28126
28220
 
28127
28221
  // ../../libs/bot-protection/src/config.ts
28128
- var NO_PROTECTION = { minBotScore: null, countries: null };
28222
+ var NO_PROTECTION = { minBotScore: null, countries: null, blockVerifiedBots: false };
28129
28223
  var normalizeScore = /* @__PURE__ */ __name((value, problems) => {
28130
28224
  if (value === void 0 || value === null) {
28131
28225
  return null;
@@ -28159,6 +28253,13 @@ var normalizeCountries = /* @__PURE__ */ __name((value, problems) => {
28159
28253
  }
28160
28254
  return list.length === 0 ? null : { mode: record2.mode, list };
28161
28255
  }, "normalizeCountries");
28256
+ var normalizeBlockVerifiedBots = /* @__PURE__ */ __name((value, problems) => {
28257
+ if (value === void 0 || typeof value === "boolean") {
28258
+ return value ?? false;
28259
+ }
28260
+ problems.push("has a non-boolean blockVerifiedBots");
28261
+ return false;
28262
+ }, "normalizeBlockVerifiedBots");
28162
28263
  var parseBotProtection = /* @__PURE__ */ __name((raw2) => {
28163
28264
  if (raw2.trim() === "") {
28164
28265
  return { config: NO_PROTECTION };
@@ -28176,15 +28277,19 @@ var parseBotProtection = /* @__PURE__ */ __name((raw2) => {
28176
28277
  const problems = [];
28177
28278
  const config2 = {
28178
28279
  minBotScore: normalizeScore(record2.minBotScore, problems),
28179
- countries: normalizeCountries(record2.countries, problems)
28280
+ countries: normalizeCountries(record2.countries, problems),
28281
+ blockVerifiedBots: normalizeBlockVerifiedBots(record2.blockVerifiedBots, problems)
28180
28282
  };
28181
28283
  return problems.length > 0 ? { config: config2, error: `BOT_PROTECTION ${problems.join("; ")}` } : { config: config2 };
28182
28284
  }, "parseBotProtection");
28183
28285
  var isRequestBlocked = /* @__PURE__ */ __name((config2, signal) => {
28184
- if (config2.minBotScore !== null && signal.botScore !== void 0 && signal.botScore < config2.minBotScore) {
28286
+ if (isCountryBlocked(signal.country, signal.isEU ?? false, config2.countries)) {
28185
28287
  return true;
28186
28288
  }
28187
- return isCountryBlocked(signal.country, signal.isEU ?? false, config2.countries);
28289
+ if (signal.verifiedBot === true) {
28290
+ return config2.blockVerifiedBots;
28291
+ }
28292
+ return config2.minBotScore !== null && signal.botScore !== void 0 && signal.botScore < config2.minBotScore;
28188
28293
  }, "isRequestBlocked");
28189
28294
 
28190
28295
  // ../../libs/geo-filter/src/us-states.ts
@@ -28583,7 +28688,7 @@ var buildEnvelope = /* @__PURE__ */ __name((target, trigger, snapshot, context)
28583
28688
  ...geoAllowed ? { geo: snapshot.userGeo } : {}
28584
28689
  },
28585
28690
  ...buckets.includes("consentState") ? { consent: { enabled: context.consentEnforced, categories: snapshot.consent.categories, apps: snapshot.consent.apps } } : {},
28586
- ...geoAllowed && currentGeo !== void 0 ? { geo: currentGeo } : {},
28691
+ ...geoAllowed && currentGeo !== void 0 ? { geo: cfDataFrom(currentGeo) } : {},
28587
28692
  ...trigger.kind === "connect" && buckets.includes("eventContext") ? connectContext(trigger) : {},
28588
28693
  // Not gated on a capabilities.data bucket: an app only ever sees what it captured or wrote.
28589
28694
  ...captures === void 0 ? {} : { captures },
@@ -29765,7 +29870,7 @@ var streamWithDeadlines = /* @__PURE__ */ __name((source, { idleMs, totalMs }) =
29765
29870
  }, "streamWithDeadlines");
29766
29871
 
29767
29872
  // src/helpers/bot-check.ts
29768
- var botRejected = /* @__PURE__ */ __name((env, geo) => {
29873
+ var botRejected = /* @__PURE__ */ __name((env, request, geo) => {
29769
29874
  return traced("bot.check", {}, () => {
29770
29875
  const { config: protection, error: error61 } = parseBotProtection(env.BOT_PROTECTION);
29771
29876
  if (error61 !== void 0) {
@@ -29774,8 +29879,19 @@ var botRejected = /* @__PURE__ */ __name((env, geo) => {
29774
29879
  attrs: { error: String(error61) }
29775
29880
  });
29776
29881
  }
29777
- const rejected = isRequestBlocked(protection, { botScore: geo.botScore, country: geo.country, isEU: geo.isEUCountry === "yes" });
29778
- setSpanAttributes({ "bot.blocked": rejected, ...geo.botScore === void 0 ? {} : { "bot.score": geo.botScore } });
29882
+ const cf = request.cf;
29883
+ const verifiedBot = cf?.botManagement?.verifiedBot;
29884
+ const rejected = isRequestBlocked(protection, {
29885
+ botScore: geo.botScore,
29886
+ verifiedBot,
29887
+ country: geo.country,
29888
+ isEU: geo.isEUCountry === "yes"
29889
+ });
29890
+ setSpanAttributes({
29891
+ "bot.blocked": rejected,
29892
+ ...geo.botScore === void 0 ? {} : { "bot.score": geo.botScore },
29893
+ ...verifiedBot === void 0 ? {} : { "bot.verified": verifiedBot }
29894
+ });
29779
29895
  return rejected;
29780
29896
  });
29781
29897
  }, "botRejected");
@@ -29865,7 +29981,7 @@ var withCors = /* @__PURE__ */ __name((response, request, method) => {
29865
29981
  }, "withCors");
29866
29982
  var authorize = /* @__PURE__ */ __name(async (c, { slug, app: app2 }) => {
29867
29983
  const geo = geoFromRequest(c.req.raw);
29868
- if (await botRejected(c.env, geo)) {
29984
+ if (await botRejected(c.env, c.req.raw, geo)) {
29869
29985
  return refuse(403, "traffic_rejected");
29870
29986
  }
29871
29987
  const anonId = getCookie(c, COOKIE_NAME2);
@@ -30064,7 +30180,7 @@ var runForwardToApp = /* @__PURE__ */ __name(async (c, input2) => {
30064
30180
  method: endpoint.method,
30065
30181
  mode: endpoint.mode,
30066
30182
  version: app2.version,
30067
- ...app2.data.includes("geo") && requestGeo !== void 0 ? { geo: requestGeo } : {},
30183
+ ...app2.data.includes("geo") && requestGeo !== void 0 ? { geo: cfDataFrom(requestGeo) } : {},
30068
30184
  installs: installs.map((install) => ({ id: install.id, ord: install.ord, label: install.label })),
30069
30185
  ...anonId === void 0 ? {} : { user: { anonId } }
30070
30186
  })
@@ -30660,6 +30776,7 @@ var createApiRouter = /* @__PURE__ */ __name(() => new OpenAPIHono({ defaultHook
30660
30776
  var CONNECTION_CF_HEADER = "x-connection-cf";
30661
30777
  var SESSION_HEADER = "x-session";
30662
30778
  var GPC_HEADER = "x-gpc";
30779
+ var RECONNECT_HEADER = "x-reconnect";
30663
30780
  var OPEN_PAGE_PARAMS = ["url", "referrer"];
30664
30781
  var MAX_HOST_USER_AGENT_LENGTH = 512;
30665
30782
  var MAX_HOST_LOCALE_LENGTH = 64;
@@ -30671,6 +30788,7 @@ var INTERNAL_HEADERS = /* @__PURE__ */ new Set([
30671
30788
  CONNECTION_CF_HEADER,
30672
30789
  SESSION_HEADER,
30673
30790
  GPC_HEADER,
30791
+ RECONNECT_HEADER,
30674
30792
  TRACE_HEADER,
30675
30793
  CF_DATA_HEADER
30676
30794
  ]);
@@ -30759,7 +30877,7 @@ var boundedHost = /* @__PURE__ */ __name((request) => {
30759
30877
  };
30760
30878
  return bounded.ip === void 0 && bounded.userAgent === void 0 && bounded.locale === void 0 ? void 0 : bounded;
30761
30879
  }, "boundedHost");
30762
- var connectionAttachment = /* @__PURE__ */ __name((request, geo, siteId) => {
30880
+ var connectionAttachment = /* @__PURE__ */ __name((request, geo, { siteId, anonId }) => {
30763
30881
  const userAgent = request.headers.get("user-agent");
30764
30882
  const attrs = {
30765
30883
  "url.full": withoutQuery(request.url),
@@ -30775,7 +30893,7 @@ var connectionAttachment = /* @__PURE__ */ __name((request, geo, siteId) => {
30775
30893
  const sessionBytes = attachmentFieldBytes("session", session2);
30776
30894
  const gpc = request.headers.get(GPC_HEADER) === "1";
30777
30895
  const gpcBytes = attachmentFieldBytes("gpc", gpc ? true : void 0);
30778
- const siteBytes = attachmentFieldBytes("siteId", siteId);
30896
+ const siteBytes = attachmentFieldBytes("siteId", siteId) + attachmentFieldBytes("anonId", anonId);
30779
30897
  const cf = forwardedCf(request);
30780
30898
  const cfSuffix = cf === void 0 ? "" : `,"cf":${JSON.stringify(cf)}`;
30781
30899
  const cfBytes = new TextEncoder().encode(cfSuffix).length;
@@ -30786,6 +30904,7 @@ var connectionAttachment = /* @__PURE__ */ __name((request, geo, siteId) => {
30786
30904
  return {
30787
30905
  ...budgeted,
30788
30906
  siteId,
30907
+ anonId,
30789
30908
  ...session2 === void 0 ? {} : { session: session2 },
30790
30909
  ...browserGeo === void 0 ? {} : { browserGeo },
30791
30910
  ...includedCf === void 0 ? {} : { cf: includedCf },
@@ -30793,6 +30912,14 @@ var connectionAttachment = /* @__PURE__ */ __name((request, geo, siteId) => {
30793
30912
  ...gpc ? { gpc: true } : {}
30794
30913
  };
30795
30914
  }, "connectionAttachment");
30915
+ var attachmentGeo = /* @__PURE__ */ __name((attachment) => {
30916
+ return {
30917
+ // `cf` is the forwarded `request.cf`, kept as plain JSON on the socket.
30918
+ ...geoFromCf(attachment.cf),
30919
+ ip: attachment.host?.ip,
30920
+ userAgent: attachment.host?.userAgent
30921
+ };
30922
+ }, "attachmentGeo");
30796
30923
 
30797
30924
  // src/helpers/request-span.ts
30798
30925
  var OMITTED_HEADERS = /* @__PURE__ */ new Set(["authorization", DISPATCH_HEADER, CF_DATA_HEADER]);
@@ -31192,6 +31319,29 @@ var withSetCookie = /* @__PURE__ */ __name((res, cookies) => {
31192
31319
  }
31193
31320
  return out;
31194
31321
  }, "withSetCookie");
31322
+ var connectCookies = /* @__PURE__ */ __name(async (c, userId, session2) => {
31323
+ const root = c.env.ROOT_DOMAIN;
31324
+ const hostname3 = new URL(c.req.url).hostname;
31325
+ const domain2 = root !== void 0 && root !== "" && (hostname3 === root || hostname3.endsWith(`.${root}`)) ? root : void 0;
31326
+ const attributes = {
31327
+ httpOnly: true,
31328
+ // `wrangler types` narrows COOKIE_SAME_SITE to its dev default; deploys also set none.
31329
+ sameSite: c.env.COOKIE_SAME_SITE,
31330
+ secure: true,
31331
+ path: "/",
31332
+ maxAge: COOKIE_MAX_AGE
31333
+ };
31334
+ const cookies = [
31335
+ generateCookie(COOKIE_NAME2, userId, { ...attributes, ...domain2 === void 0 ? {} : { domain: domain2 } }),
31336
+ // Re-issued on every connect, which is what makes the session expire after 30 idle minutes.
31337
+ generateCookie(SESSION_COOKIE_NAME, `${session2.id}.${session2.startedAt}`, { ...attributes, maxAge: SESSION_MAX_AGE })
31338
+ ];
31339
+ const signature = await signVisitor(c.env, userId);
31340
+ if (signature !== void 0) {
31341
+ cookies.push(generateCookie(VISITOR_SIGNATURE_COOKIE, signature, attributes));
31342
+ }
31343
+ return cookies;
31344
+ }, "connectCookies");
31195
31345
  var open2 = /* @__PURE__ */ __name(async (c) => {
31196
31346
  if (c.req.header("upgrade")?.toLowerCase() !== "websocket") {
31197
31347
  return c.text("expected websocket", 426);
@@ -31204,13 +31354,14 @@ var open2 = /* @__PURE__ */ __name(async (c) => {
31204
31354
  });
31205
31355
  return enriched;
31206
31356
  });
31207
- if (await botRejected(c.env, geo)) {
31357
+ if (await botRejected(c.env, c.req.raw, geo)) {
31208
31358
  return c.text("traffic rejected", 403);
31209
31359
  }
31210
31360
  const existing = getCookie(c, COOKIE_NAME2);
31361
+ const canonical = existing !== void 0 && isCanonicalId(existing);
31211
31362
  let isNewUser = false;
31212
31363
  let userId;
31213
- if (existing !== void 0 && isCanonicalId(existing)) {
31364
+ if (canonical) {
31214
31365
  userId = existing;
31215
31366
  } else {
31216
31367
  const adopted = edgeTagUserId(c);
@@ -31236,6 +31387,10 @@ var open2 = /* @__PURE__ */ __name(async (c) => {
31236
31387
  headers.set(GPC_HEADER, "1");
31237
31388
  setSpanAttributes({ "qinto.gpc": true });
31238
31389
  }
31390
+ headers.delete(RECONNECT_HEADER);
31391
+ if (canonical && c.req.query("reconnect") === "1") {
31392
+ headers.set(RECONNECT_HEADER, "1");
31393
+ }
31239
31394
  const res = await traced("rpc anon.connect", {}, () => {
31240
31395
  headers.delete(TRACE_HEADER);
31241
31396
  const trace = currentTraceContext();
@@ -31244,27 +31399,7 @@ var open2 = /* @__PURE__ */ __name(async (c) => {
31244
31399
  }
31245
31400
  return anonStub(c.env, userId).fetch(new Request(c.req.url, { method: "GET", headers }));
31246
31401
  });
31247
- const root = c.env.ROOT_DOMAIN;
31248
- const hostname3 = new URL(c.req.url).hostname;
31249
- const domain2 = root !== void 0 && root !== "" && (hostname3 === root || hostname3.endsWith(`.${root}`)) ? root : void 0;
31250
- const attributes = {
31251
- httpOnly: true,
31252
- // `wrangler types` narrows COOKIE_SAME_SITE to its dev default; deploys also set none.
31253
- sameSite: c.env.COOKIE_SAME_SITE,
31254
- secure: true,
31255
- path: "/",
31256
- maxAge: COOKIE_MAX_AGE
31257
- };
31258
- const cookies = [
31259
- generateCookie(COOKIE_NAME2, userId, { ...attributes, ...domain2 === void 0 ? {} : { domain: domain2 } }),
31260
- // Re-issued on every connect, which is what makes the session expire after 30 idle minutes.
31261
- generateCookie(SESSION_COOKIE_NAME, `${session2.id}.${session2.startedAt}`, { ...attributes, maxAge: SESSION_MAX_AGE })
31262
- ];
31263
- const signature = await signVisitor(c.env, userId);
31264
- if (signature !== void 0) {
31265
- cookies.push(generateCookie(VISITOR_SIGNATURE_COOKIE, signature, attributes));
31266
- }
31267
- return withSetCookie(res, cookies);
31402
+ return withSetCookie(res, await connectCookies(c, userId, session2));
31268
31403
  }, "open");
31269
31404
 
31270
31405
  // browser-artifact/meta.json
@@ -32476,13 +32611,15 @@ var cachedKv = /* @__PURE__ */ __name((kv) => {
32476
32611
  var VERSION_KEY = "schemaVersion";
32477
32612
  var applyMigrations = /* @__PURE__ */ __name((storage, migrations) => {
32478
32613
  const applied = storage.kv.get(VERSION_KEY) ?? 0;
32479
- for (const [index, migration] of migrations.entries()) {
32480
- if (index < applied) {
32481
- continue;
32482
- }
32483
- storage.sql.exec(migration);
32484
- storage.kv.put(VERSION_KEY, index + 1);
32614
+ if (applied >= migrations.length) {
32615
+ return;
32485
32616
  }
32617
+ storage.transactionSync(() => {
32618
+ for (const migration of migrations.slice(applied)) {
32619
+ storage.sql.exec(migration);
32620
+ }
32621
+ storage.kv.put(VERSION_KEY, migrations.length);
32622
+ });
32486
32623
  }, "applyMigrations");
32487
32624
 
32488
32625
  // src/id-graph/retention.ts
@@ -32768,15 +32905,15 @@ var seedMeta = /* @__PURE__ */ __name((kv, anonId, createdAt) => {
32768
32905
  kv.put(META_KEY, { anonId, linkedGlobalId: null, linkedAt: null, createdAt });
32769
32906
  return true;
32770
32907
  }, "seedMeta");
32908
+ var lastActivityAt = /* @__PURE__ */ __name((kv) => {
32909
+ return kv.get(LAST_ACTIVITY_KEY) ?? kv.get(META_KEY)?.createdAt ?? null;
32910
+ }, "lastActivityAt");
32771
32911
  var touchActivity = /* @__PURE__ */ __name((kv, receivedAt) => {
32772
- const last = kv.get(LAST_ACTIVITY_KEY);
32773
- if (last === void 0 || receivedAt - last >= CLEANUP_INTERVAL_MS) {
32912
+ const last = lastActivityAt(kv);
32913
+ if (last === null || receivedAt - last >= CLEANUP_INTERVAL_MS) {
32774
32914
  kv.put(LAST_ACTIVITY_KEY, receivedAt);
32775
32915
  }
32776
32916
  }, "touchActivity");
32777
- var lastActivityAt = /* @__PURE__ */ __name((kv) => {
32778
- return kv.get(LAST_ACTIVITY_KEY) ?? null;
32779
- }, "lastActivityAt");
32780
32917
  var linkedGlobalId = /* @__PURE__ */ __name((kv) => {
32781
32918
  return kv.get(META_KEY)?.linkedGlobalId ?? null;
32782
32919
  }, "linkedGlobalId");
@@ -33010,7 +33147,7 @@ var standsDown = /* @__PURE__ */ __name(async (storage, staleActivity) => {
33010
33147
  if (lastActivity === staleActivity) {
33011
33148
  return false;
33012
33149
  }
33013
- await storage.setAlarm(cleanupTarget(storage, Date.now(), lastActivity ?? Date.now()));
33150
+ await storage.setAlarm(cleanupTarget(storage, Date.now(), lastActivity));
33014
33151
  return true;
33015
33152
  }, "standsDown");
33016
33153
  var relink = /* @__PURE__ */ __name(async (storage, env, anonId, globalId) => {
@@ -33029,7 +33166,7 @@ var relink = /* @__PURE__ */ __name(async (storage, env, anonId, globalId) => {
33029
33166
  addSpanEvent("expiry relink failed", { level: "warn", attrs: { "anon.id": anonId, "global.id": globalId, error: String(error61) } });
33030
33167
  }
33031
33168
  if (mutated) {
33032
- await storage.setAlarm(cleanupTarget(storage, Date.now(), lastActivityAt(storage.kv) ?? Date.now()));
33169
+ await storage.setAlarm(cleanupTarget(storage, Date.now(), lastActivityAt(storage.kv)));
33033
33170
  }
33034
33171
  }, "relink");
33035
33172
  var unlinkFromPerson = /* @__PURE__ */ __name((env, anonId, globalId) => {
@@ -33588,10 +33725,9 @@ var AnonDO = class extends DurableObject2 {
33588
33725
  // In memory on purpose: a hibernated socket sent nothing, so a fresh bucket on wake is the right one.
33589
33726
  frameBuckets = /* @__PURE__ */ new WeakMap();
33590
33727
  emitBudget = createTokenBucket(EMIT_BURST, EMITS_PER_SECOND);
33591
- // deleteAll removes the live schema; only a later write recreates it.
33728
+ stored;
33729
+ // This instance deleted the visitor's storage; only a later write recreates it.
33592
33730
  wiped = false;
33593
- // True while processAction holds ingestQueue, including the transform await.
33594
- ingestTurn = false;
33595
33731
  // Warm write-through mirrors reset together after an expiry or an erase.
33596
33732
  kvCache;
33597
33733
  cachedStorage;
@@ -33607,7 +33743,6 @@ var AnonDO = class extends DurableObject2 {
33607
33743
  sites;
33608
33744
  constructor(ctx, env) {
33609
33745
  super(ctx, env);
33610
- applyMigrations(ctx.storage, ANON_MIGRATIONS);
33611
33746
  this.kvCache = cachedKv(ctx.storage.kv);
33612
33747
  this.cachedStorage = { kv: this.kvCache, sql: ctx.storage.sql };
33613
33748
  this.graph = ownGraphEnv(ctx, env);
@@ -33630,32 +33765,29 @@ var AnonDO = class extends DurableObject2 {
33630
33765
  const trace = parseTraceContext(request.headers.get(TRACE_HEADER));
33631
33766
  return runInvocation({ name: "do.anon.connect", trace, attrs: connectAttrs(request, geo) }, async () => {
33632
33767
  const context = await this.sites.resolve({ domainId: siteId }, Date.now());
33633
- this.revive();
33634
- const now = Date.now();
33635
- this.cleanupAlarm = await seedCleanupAlarm(this.ctx.storage, now, lastActivityAt(this.kv) ?? now, this.cleanupAlarm);
33636
- seedMeta(this.kv, userId, now);
33637
- touchActivity(this.kv, now);
33638
- if (geo !== void 0 && !hasGeoRecord(this.kv)) {
33639
- this.seedGeoOnConnect(userId, geo, now);
33768
+ if (this.isStored()) {
33769
+ touchActivity(this.kv, Date.now());
33640
33770
  }
33641
33771
  const { 0: client, 1: server } = new WebSocketPair();
33642
33772
  this.ctx.acceptWebSocket(server, [siteId]);
33643
- const attachment = connectionAttachment(request, geo, siteId);
33773
+ const attachment = connectionAttachment(request, geo, { siteId, anonId: userId });
33644
33774
  server.serializeAttachment(attachment);
33645
33775
  this.sendSharedContext([server], context);
33646
- const page = connectPage(request);
33647
- this.appFanOut(
33648
- {
33649
- kind: "connect",
33650
- ...attachment.cf === void 0 ? {} : { geo: attachment.cf },
33651
- host: attachment.host,
33652
- ...page === void 0 ? {} : { page },
33653
- ...attachment.session === void 0 ? {} : { session: attachment.session },
33654
- gpc: attachment.gpc
33655
- },
33656
- userId,
33657
- context
33658
- );
33776
+ if (request.headers.get(RECONNECT_HEADER) !== "1") {
33777
+ const page = connectPage(request);
33778
+ this.appFanOut(
33779
+ {
33780
+ kind: "connect",
33781
+ ...attachment.cf === void 0 ? {} : { geo: attachment.cf },
33782
+ host: attachment.host,
33783
+ ...page === void 0 ? {} : { page },
33784
+ ...attachment.session === void 0 ? {} : { session: attachment.session },
33785
+ gpc: attachment.gpc
33786
+ },
33787
+ userId,
33788
+ context
33789
+ );
33790
+ }
33659
33791
  return new Response(null, { status: 101, webSocket: client });
33660
33792
  });
33661
33793
  }
@@ -33693,7 +33825,7 @@ var AnonDO = class extends DurableObject2 {
33693
33825
  }
33694
33826
  if (frame.t === "capture") {
33695
33827
  const { accepted } = await this.capture(
33696
- { apps: frame.apps, location: socketLocation(attachment.cf), gpc: attachment.gpc === true },
33828
+ { anonId: userId, apps: frame.apps, location: socketLocation(attachment.cf), gpc: attachment.gpc === true },
33697
33829
  site
33698
33830
  );
33699
33831
  this.respond(ws, { t: "captureAck", ref, anonId: userId, accepted });
@@ -33738,7 +33870,8 @@ var AnonDO = class extends DurableObject2 {
33738
33870
  syncFromGlobal(snapshot, trace) {
33739
33871
  return runInvocation({ name: "do.anon.sync_from_global", trace, attrs: { "global.id": snapshot.globalId } }, () => {
33740
33872
  return this.enqueue(async () => {
33741
- if (await this.discardIfUnused()) {
33873
+ const anonId = storedAnonId(this.kv);
33874
+ if (anonId === null) {
33742
33875
  addSpanEvent("snapshot dropped: no visitor state", { level: "warn", attrs: { "global.id": snapshot.globalId } });
33743
33876
  return;
33744
33877
  }
@@ -33746,7 +33879,7 @@ var AnonDO = class extends DurableObject2 {
33746
33879
  addSpanEvent("foreign snapshot dropped", { level: "warn", attrs: { "global.id": snapshot.globalId } });
33747
33880
  return;
33748
33881
  }
33749
- this.cleanupAlarm = await seedCleanupAlarm(this.ctx.storage, Date.now(), lastActivityAt(this.kv) ?? Date.now(), this.cleanupAlarm);
33882
+ await this.storeVisitor(anonId, Date.now());
33750
33883
  await this.applyOutcome(applySnapshot(this.cachedStorage, snapshot, this.snapshotCaches()));
33751
33884
  });
33752
33885
  });
@@ -33763,7 +33896,7 @@ var AnonDO = class extends DurableObject2 {
33763
33896
  this.respond(ws, { t: "error", ref: parsed.ref, message: "invalid message" });
33764
33897
  return;
33765
33898
  }
33766
- const userId = storedAnonId(this.kv);
33899
+ const userId = attachment?.anonId ?? null;
33767
33900
  setSpanAttributes(frameSpanAttrs(parsed.frame, userId));
33768
33901
  if (parsed.frame.t === "getConsent") {
33769
33902
  this.respond(ws, this.consentReply(parsed.ref, userId));
@@ -33813,10 +33946,6 @@ var AnonDO = class extends DurableObject2 {
33813
33946
  };
33814
33947
  return runInvocation({ name: "do.anon.app_push", trace, attrs }, () => {
33815
33948
  return this.enqueue(async () => {
33816
- if (await this.discardIfUnused()) {
33817
- addSpanEvent("app push dropped: no visitor state", { level: "warn", attrs: { app: push.slug } });
33818
- return;
33819
- }
33820
33949
  this.deliverAppPush(push, await this.sites.resolve(site, Date.now()));
33821
33950
  });
33822
33951
  });
@@ -33836,26 +33965,19 @@ var AnonDO = class extends DurableObject2 {
33836
33965
  // so a value the caps refused must never be reported as stored.
33837
33966
  capture(input2, site, trace) {
33838
33967
  return runInvocation({ name: "do.anon.capture", trace }, () => {
33839
- return this.enqueue(async () => {
33840
- if (await this.discardIfUnused()) {
33841
- addSpanEvent("capture dropped: no visitor state", { level: "warn" });
33842
- return { accepted: {} };
33843
- }
33844
- return this.runCaptures(input2.apps, site, { location: input2.location, gpc: input2.gpc });
33968
+ return this.enqueue(() => {
33969
+ return this.runCaptures(input2.anonId, input2.apps, site, { location: input2.location, gpc: input2.gpc });
33845
33970
  });
33846
33971
  });
33847
33972
  }
33848
- // The server door. There is no connect to have seeded the visitor, and a migrated visitor can
33849
- // carry click ids before any identify, so unlike the socket this one creates the profile.
33973
+ // A migrated visitor can carry click ids before any identify, so this door creates the profile.
33850
33974
  serverCapture(input2, site, trace) {
33851
33975
  return runInvocation({ name: "do.anon.server_capture", trace }, () => {
33852
33976
  return this.enqueue(async () => {
33853
- this.revive();
33854
33977
  const now = Date.now();
33855
- this.cleanupAlarm = await seedCleanupAlarm(this.ctx.storage, now, lastActivityAt(this.kv) ?? now, this.cleanupAlarm);
33856
- seedMeta(this.kv, input2.anonId, now);
33978
+ await this.storeVisitor(input2.anonId, now);
33857
33979
  touchActivity(this.kv, now);
33858
- return this.runCaptures(input2.apps, site, { location: null, gpc: false });
33980
+ return this.runCaptures(input2.anonId, input2.apps, site, { location: null, gpc: false });
33859
33981
  });
33860
33982
  });
33861
33983
  }
@@ -33864,7 +33986,8 @@ var AnonDO = class extends DurableObject2 {
33864
33986
  appCaptures(input2, site, trace) {
33865
33987
  return runInvocation({ name: "do.anon.app_captures", trace, attrs: { "app.slug": input2.slug } }, () => {
33866
33988
  return this.enqueue(async () => {
33867
- if (await this.discardIfUnused()) {
33989
+ const anonId = this.visitorId();
33990
+ if (anonId === null) {
33868
33991
  addSpanEvent("capture dropped: no visitor state", { level: "warn", attrs: { app: input2.slug } });
33869
33992
  return;
33870
33993
  }
@@ -33877,7 +34000,8 @@ var AnonDO = class extends DurableObject2 {
33877
34000
  })) {
33878
34001
  return;
33879
34002
  }
33880
- this.storeCaptures(
34003
+ await this.storeCaptures(
34004
+ anonId,
33881
34005
  input2.writes.map((write) => {
33882
34006
  return { ...write, appId: input2.slug, source: "app", ttlMs: declaredTtlMs(context.registry, input2.slug, write.key) };
33883
34007
  })
@@ -33885,7 +34009,7 @@ var AnonDO = class extends DurableObject2 {
33885
34009
  });
33886
34010
  });
33887
34011
  }
33888
- async runCaptures(apps, site, { location, gpc }) {
34012
+ async runCaptures(anonId, apps, site, { location, gpc }) {
33889
34013
  const context = await this.sites.resolve(site, Date.now());
33890
34014
  const writes = acceptedCaptures(context.registry, apps, {
33891
34015
  consent: consentState2(this.kv),
@@ -33893,13 +34017,13 @@ var AnonDO = class extends DurableObject2 {
33893
34017
  gpc,
33894
34018
  location
33895
34019
  });
33896
- return { accepted: this.storeCaptures(writes) };
34020
+ return { accepted: await this.storeCaptures(anonId, writes) };
33897
34021
  }
33898
- storeCaptures(writes) {
34022
+ async storeCaptures(anonId, writes) {
33899
34023
  if (writes.length === 0) {
33900
34024
  return {};
33901
34025
  }
33902
- this.revive();
34026
+ await this.storeVisitor(anonId, Date.now());
33903
34027
  const accepted = {};
33904
34028
  const stored = writeCaptures(this.sql, writes, Date.now(), this.captureMirror);
33905
34029
  for (const write of stored) {
@@ -33915,7 +34039,8 @@ var AnonDO = class extends DurableObject2 {
33915
34039
  appContext(input2, site, trace) {
33916
34040
  return runInvocation({ name: "do.anon.app_context", trace, attrs: { "app.slug": input2.slug } }, () => {
33917
34041
  return this.enqueue(async () => {
33918
- if (await this.discardIfUnused()) {
34042
+ const anonId = this.visitorId();
34043
+ if (anonId === null) {
33919
34044
  addSpanEvent("context dropped: no visitor state", { level: "warn", attrs: { app: input2.slug } });
33920
34045
  return [];
33921
34046
  }
@@ -33940,15 +34065,18 @@ var AnonDO = class extends DurableObject2 {
33940
34065
  }
33941
34066
  return true;
33942
34067
  });
33943
- return this.storeContext(writes.map((write) => ({ ...write, appId: input2.slug })));
34068
+ return this.storeContext(
34069
+ anonId,
34070
+ writes.map((write) => ({ ...write, appId: input2.slug }))
34071
+ );
33944
34072
  });
33945
34073
  });
33946
34074
  }
33947
- async storeContext(writes) {
34075
+ async storeContext(anonId, writes) {
33948
34076
  if (writes.length === 0) {
33949
34077
  return [];
33950
34078
  }
33951
- this.revive();
34079
+ await this.storeVisitor(anonId, Date.now());
33952
34080
  const stored = [];
33953
34081
  for (const write of writes) {
33954
34082
  if (writeContext(this.sql, [write], Date.now(), this.contextMirror).length === 0) {
@@ -33966,7 +34094,7 @@ var AnonDO = class extends DurableObject2 {
33966
34094
  return stored;
33967
34095
  }
33968
34096
  hasSharedContext() {
33969
- return !this.wiped && Object.keys(allContext(this.sql, "shared", this.contextMirror)).length > 0;
34097
+ return this.isStored() && Object.keys(allContext(this.sql, "shared", this.contextMirror)).length > 0;
33970
34098
  }
33971
34099
  // Shared context is the level every app may read, so the browser gets it too — the connecting
33972
34100
  // socket on open, every socket on a change. Private never leaves the server. The connect frame is
@@ -33974,7 +34102,7 @@ var AnonDO = class extends DurableObject2 {
33974
34102
  // context from one that has not arrived yet.
33975
34103
  sendSharedContext(sockets, context) {
33976
34104
  const consent2 = consentState2(this.kv);
33977
- const stored = allContext(this.sql, "shared", this.contextMirror);
34105
+ const stored = this.isStored() ? allContext(this.sql, "shared", this.contextMirror) : {};
33978
34106
  for (const socket of sockets) {
33979
34107
  const { gpc } = socket.deserializeAttachment();
33980
34108
  sendFrame(socket, sharedContextFrame(context, consent2, stored, gpc === true));
@@ -34002,12 +34130,10 @@ var AnonDO = class extends DurableObject2 {
34002
34130
  }
34003
34131
  }
34004
34132
  }
34005
- // A read must not resurrect a visitor: an rpc or workflow asking about an expired or unknown anon
34006
- // would otherwise leave an empty object behind that no cleanup alarm collects.
34007
34133
  getAppContext(slug, site, trace) {
34008
34134
  return runInvocation({ name: "do.anon.get_app_context", trace, attrs: { "app.slug": slug } }, () => {
34009
34135
  return this.enqueue(async () => {
34010
- if (await this.discardIfUnused()) {
34136
+ if (!this.isStored()) {
34011
34137
  return readableContext(void 0, {});
34012
34138
  }
34013
34139
  const context = await this.sites.resolve(site, Date.now());
@@ -34026,20 +34152,18 @@ var AnonDO = class extends DurableObject2 {
34026
34152
  });
34027
34153
  });
34028
34154
  }
34029
- // Committed projection: must not join ingestQueue (a transform that lists facts holds that slot)
34030
- // and must not discardIfUnused during an ingest turn (meta is seeded after the transform).
34155
+ // Must not join ingestQueue: a transform that lists facts holds that slot.
34031
34156
  getAppFacts(slug, query, site, trace) {
34032
34157
  return runInvocation({ name: "do.anon.get_app_facts", trace, attrs: { "app.slug": slug } }, async () => {
34033
34158
  const context = await this.sites.resolve(site, Date.now());
34034
- const unused = this.ingestTurn ? false : await this.discardIfUnused();
34035
34159
  const app2 = context.registry.apps[slug];
34036
- if (unused || app2 === void 0 || !app2.data.includes("facts")) {
34160
+ if (app2 === void 0 || !app2.data.includes("facts")) {
34037
34161
  return null;
34038
34162
  }
34039
34163
  if (!consentAllows(consentState2(this.kv), slug, app2.consent.category, { enforced: context.consentEnforced, gpc: false })) {
34040
34164
  return null;
34041
34165
  }
34042
- if (this.wiped) {
34166
+ if (!this.isStored()) {
34043
34167
  return { items: [], next: null };
34044
34168
  }
34045
34169
  return listAppFacts(this.sql, query, Date.now());
@@ -34074,8 +34198,7 @@ var AnonDO = class extends DurableObject2 {
34074
34198
  }
34075
34199
  }
34076
34200
  }
34077
- // A getConsent can arrive before the session is seeded, so the visitor is named only when there
34078
- // is one — the tag treats its absence as "cannot vouch for the memo" rather than as a match.
34201
+ // No anonId tells the tag "cannot vouch for the memo", not a match.
34079
34202
  consentReply(ref, userId) {
34080
34203
  return { t: "consentState", ref, ...userId === null ? {} : { anonId: userId }, ...consentState2(this.kv) };
34081
34204
  }
@@ -34108,7 +34231,6 @@ var AnonDO = class extends DurableObject2 {
34108
34231
  return run;
34109
34232
  }
34110
34233
  async processAction(action, context) {
34111
- this.ingestTurn = true;
34112
34234
  let recorded = action;
34113
34235
  try {
34114
34236
  if (isNoopAction(action)) {
@@ -34122,12 +34244,15 @@ var AnonDO = class extends DurableObject2 {
34122
34244
  await this.runAction(transformed.action, transformed.releaseEmissions, context);
34123
34245
  } finally {
34124
34246
  this.recordActionBlocks(recorded, context);
34125
- this.ingestTurn = false;
34126
34247
  }
34127
34248
  }
34128
34249
  recordActionBlocks(action, context) {
34129
34250
  setSpanAttributes(
34130
- actionBlocks(action, this.appsSnapshot(action.userId, context), this.wiped ? [] : allIdentifiers(this.sql, this.identifiersMirror))
34251
+ actionBlocks(
34252
+ action,
34253
+ this.appsSnapshot(action.userId, context),
34254
+ this.isStored() ? allIdentifiers(this.sql, this.identifiersMirror) : []
34255
+ )
34131
34256
  );
34132
34257
  }
34133
34258
  async runAction(current, releaseEmissions, context) {
@@ -34136,14 +34261,7 @@ var AnonDO = class extends DurableObject2 {
34136
34261
  this.releaseTransformEmissions(releaseEmissions);
34137
34262
  return;
34138
34263
  }
34139
- this.revive();
34140
- this.cleanupAlarm = await seedCleanupAlarm(
34141
- this.ctx.storage,
34142
- Date.now(),
34143
- lastActivityAt(this.kv) ?? current.receivedAt,
34144
- this.cleanupAlarm
34145
- );
34146
- seedMeta(this.kv, current.userId, current.receivedAt);
34264
+ await this.storeVisitor(current.userId, current.receivedAt);
34147
34265
  touchActivity(this.kv, current.receivedAt);
34148
34266
  const novelIdentifier = this.recordIdentifiers(current);
34149
34267
  if (novelIdentifier && linkedGlobalId(this.kv) !== null) {
@@ -34200,16 +34318,15 @@ var AnonDO = class extends DurableObject2 {
34200
34318
  }
34201
34319
  appsSnapshot(anonId, context) {
34202
34320
  const meta3 = storedMeta(this.kv);
34321
+ const stored = this.isStored();
34203
34322
  return {
34204
34323
  anonId,
34205
34324
  globalId: meta3?.linkedGlobalId ?? null,
34206
- createdAt: meta3?.createdAt ?? null,
34325
+ createdAt: meta3?.createdAt ?? Date.now(),
34207
34326
  pii: userProperties(this.kv),
34208
- // A wipe dropped the sql tables, so reading them would throw — and reviving for a transform
34209
- // snapshot would strand recreated storage when the chain fails or drops.
34210
- custom: this.wiped ? {} : customProperties(this.sql, this.customPropsMirror),
34211
- captures: this.wiped ? {} : allCaptures(this.sql, Date.now(), this.captureMirror),
34212
- context: this.wiped ? { private: {}, shared: {} } : {
34327
+ custom: stored ? customProperties(this.sql, this.customPropsMirror) : {},
34328
+ captures: stored ? allCaptures(this.sql, Date.now(), this.captureMirror) : {},
34329
+ context: !stored ? { private: {}, shared: {} } : {
34213
34330
  private: allContext(this.sql, "private", this.contextMirror),
34214
34331
  shared: readableSharedContext(
34215
34332
  context.registry,
@@ -34244,7 +34361,7 @@ var AnonDO = class extends DurableObject2 {
34244
34361
  getProperties(trace) {
34245
34362
  return runInvocation({ name: "do.anon.get_properties", trace }, () => {
34246
34363
  return this.enqueue(async () => {
34247
- if (await this.discardIfUnused()) {
34364
+ if (!this.isStored()) {
34248
34365
  return {};
34249
34366
  }
34250
34367
  return { ...userProperties(this.kv), ...customProperties(this.sql, this.customPropsMirror) };
@@ -34254,16 +34371,15 @@ var AnonDO = class extends DurableObject2 {
34254
34371
  getConsent(trace) {
34255
34372
  return runInvocation({ name: "do.anon.get_consent", trace }, () => {
34256
34373
  return this.enqueue(async () => {
34257
- await this.discardIfUnused();
34258
34374
  return consentState2(this.kv);
34259
34375
  });
34260
34376
  });
34261
34377
  }
34262
- // Null means no session: the id was never seeded or its profile expired, and the object it addressed is discarded.
34378
+ // Null means no session: the id stored nothing yet or its profile expired.
34263
34379
  getPersonLink(trace) {
34264
34380
  return runInvocation({ name: "do.anon.get_person_link", trace }, () => {
34265
34381
  return this.enqueue(async () => {
34266
- if (await this.discardIfUnused()) {
34382
+ if (!this.isStored()) {
34267
34383
  return null;
34268
34384
  }
34269
34385
  return { globalId: linkedGlobalId(this.kv) };
@@ -34273,7 +34389,7 @@ var AnonDO = class extends DurableObject2 {
34273
34389
  getGlobalId(trace) {
34274
34390
  return runInvocation({ name: "do.anon.get_global_id", trace }, () => {
34275
34391
  return this.enqueue(async () => {
34276
- if (await this.discardIfUnused()) {
34392
+ if (!this.isStored()) {
34277
34393
  return null;
34278
34394
  }
34279
34395
  return linkedGlobalId(this.kv);
@@ -34283,8 +34399,8 @@ var AnonDO = class extends DurableObject2 {
34283
34399
  erasable(trace) {
34284
34400
  return runInvocation({ name: "do.anon.erasable", trace }, () => {
34285
34401
  return this.enqueue(async () => {
34286
- if (await this.discardIfUnused()) {
34287
- return null;
34402
+ if (!this.isStored()) {
34403
+ return this.ctx.getWebSockets().length === 0 ? null : { globalId: null, identifiers: [] };
34288
34404
  }
34289
34405
  const identifiers = allIdentifiers(this.sql, this.identifiersMirror).map(({ type, value }) => ({ type, value }));
34290
34406
  return { globalId: linkedGlobalId(this.kv), identifiers };
@@ -34294,7 +34410,7 @@ var AnonDO = class extends DurableObject2 {
34294
34410
  inspect(trace) {
34295
34411
  return runInvocation({ name: "do.anon.inspect", trace }, () => {
34296
34412
  return this.enqueue(async () => {
34297
- if (await this.discardIfUnused()) {
34413
+ if (!this.isStored()) {
34298
34414
  return { exists: false };
34299
34415
  }
34300
34416
  return { exists: true, objectId: this.ctx.id.toString(), ...dumpStorage(this.ctx.storage) };
@@ -34304,7 +34420,7 @@ var AnonDO = class extends DurableObject2 {
34304
34420
  inspectTable(table, offset, limit, trace) {
34305
34421
  return runInvocation({ name: "do.anon.inspect_table", trace, attrs: { "inspect.table": table } }, () => {
34306
34422
  return this.enqueue(async () => {
34307
- if (await this.discardIfUnused()) {
34423
+ if (!this.isStored()) {
34308
34424
  return null;
34309
34425
  }
34310
34426
  return readTable(this.ctx.storage, table, offset, limit);
@@ -34314,13 +34430,16 @@ var AnonDO = class extends DurableObject2 {
34314
34430
  erase(trace) {
34315
34431
  return runInvocation({ name: "do.anon.erase", trace }, () => {
34316
34432
  return this.enqueue(async () => {
34317
- if (await this.discardIfUnused()) {
34433
+ const stored = this.isStored();
34434
+ if (!stored && this.ctx.getWebSockets().length === 0) {
34318
34435
  return false;
34319
34436
  }
34320
- try {
34321
- await eraseAnon(this.ctx.storage, this.graph);
34322
- } finally {
34323
- this.resetMirrors();
34437
+ if (stored) {
34438
+ try {
34439
+ await eraseAnon(this.ctx.storage, this.graph);
34440
+ } finally {
34441
+ this.resetMirrors();
34442
+ }
34324
34443
  }
34325
34444
  this.wiped = true;
34326
34445
  for (const socket of this.ctx.getWebSockets()) {
@@ -34330,19 +34449,24 @@ var AnonDO = class extends DurableObject2 {
34330
34449
  });
34331
34450
  });
34332
34451
  }
34333
- // Addressing an object runs the constructor's schema exec, so a call that never writes — a read, an
34334
- // inspect, a push or a snapshot for an id that was never used — would otherwise leave an empty object
34335
- // behind that no cleanup alarm ever collects. Every write path seeds meta before it stores anything,
34336
- // so a missing meta record means there is nothing to lose.
34337
- async discardIfUnused() {
34338
- if (storedAnonId(this.kv) !== null) {
34339
- return false;
34452
+ // Every write seeds meta through storeVisitor first, so an object without it has no schema to read.
34453
+ isStored() {
34454
+ if (this.stored === void 0) {
34455
+ this.stored = storedMeta(this.kv) !== void 0;
34456
+ if (this.stored) {
34457
+ applyMigrations(this.ctx.storage, ANON_MIGRATIONS);
34458
+ }
34340
34459
  }
34341
- if (!this.wiped) {
34342
- await this.ctx.storage.deleteAll();
34343
- this.wiped = true;
34460
+ return this.stored;
34461
+ }
34462
+ // Stubs address this object by anon id, but an app may name any user, so only a canonical id counts.
34463
+ visitorId() {
34464
+ const anonId = storedAnonId(this.kv);
34465
+ if (anonId !== null || this.wiped) {
34466
+ return anonId;
34344
34467
  }
34345
- return true;
34468
+ const name = this.ctx.id.name;
34469
+ return name !== void 0 && isCanonicalId(name) ? name : null;
34346
34470
  }
34347
34471
  appendProjectionHistory(outcomes, source) {
34348
34472
  for (const { entry } of outcomes) {
@@ -34551,6 +34675,7 @@ var AnonDO = class extends DurableObject2 {
34551
34675
  }
34552
34676
  async alarm() {
34553
34677
  return runInvocation({ name: "do.anon.cleanup" }, async () => {
34678
+ applyMigrations(this.ctx.storage, ANON_MIGRATIONS);
34554
34679
  this.cleanupAlarm = void 0;
34555
34680
  const now = Date.now();
34556
34681
  const lastActivity = lastActivityAt(this.kv) ?? now;
@@ -34575,6 +34700,7 @@ var AnonDO = class extends DurableObject2 {
34575
34700
  }
34576
34701
  resetMirrors() {
34577
34702
  this.kvCache.clear();
34703
+ this.stored = void 0;
34578
34704
  this.cleanupAlarm = void 0;
34579
34705
  this.identifiersMirror.reset();
34580
34706
  this.conversionsMirror.reset();
@@ -34582,14 +34708,22 @@ var AnonDO = class extends DurableObject2 {
34582
34708
  this.captureMirror.reset();
34583
34709
  this.contextMirror.reset();
34584
34710
  }
34585
- revive() {
34586
- if (this.wiped) {
34711
+ // Callers run in ingestQueue and nothing awaits between the stored check and the seeds.
34712
+ async storeVisitor(anonId, at) {
34713
+ this.cleanupAlarm = await seedCleanupAlarm(this.ctx.storage, Date.now(), lastActivityAt(this.kv) ?? at, this.cleanupAlarm);
34714
+ if (!this.isStored()) {
34587
34715
  applyMigrations(this.ctx.storage, ANON_MIGRATIONS);
34716
+ seedMeta(this.kv, anonId, at);
34717
+ this.stored = true;
34588
34718
  this.wiped = false;
34589
34719
  }
34720
+ const socket = hasGeoRecord(this.kv) ? void 0 : this.ctx.getWebSockets()[0]?.deserializeAttachment();
34721
+ if (socket !== void 0) {
34722
+ updateGeo(this.cachedStorage, attachmentGeo(socket), at);
34723
+ }
34590
34724
  }
34591
34725
  async retargetCleanupAlarm(now) {
34592
- const target = cleanupTarget(this.cachedStorage, now, lastActivityAt(this.kv) ?? now);
34726
+ const target = cleanupTarget(this.cachedStorage, now, lastActivityAt(this.kv));
34593
34727
  if (this.cleanupAlarm === target) {
34594
34728
  return;
34595
34729
  }
@@ -34599,14 +34733,6 @@ var AnonDO = class extends DurableObject2 {
34599
34733
  await this.ctx.storage.setAlarm(target);
34600
34734
  this.cleanupAlarm = target;
34601
34735
  }
34602
- seedGeoOnConnect(anonId, geo, observedAt) {
34603
- const run = this.enqueue(async () => {
34604
- this.revive();
34605
- updateGeo(this.cachedStorage, geo, observedAt);
34606
- await this.retargetCleanupAlarm(Date.now());
34607
- });
34608
- this.ctx.waitUntil(run.catch((error61) => console.warn("geo seed failed", { anonId, error: error61 })));
34609
- }
34610
34736
  };
34611
34737
 
34612
34738
  // src/id-graph/global/do.ts