@blotout/qinto-cli 0.1.17 → 0.1.19

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.
@@ -743,7 +743,6 @@ var cors = /* @__PURE__ */ __name((options) => {
743
743
  }, "cors");
744
744
 
745
745
  // src/apps/broker.ts
746
- import { timingSafeEqual } from "node:crypto";
747
746
  import { WorkerEntrypoint } from "cloudflare:workers";
748
747
 
749
748
  // ../../libs/app-deploy/src/control.ts
@@ -778,12 +777,18 @@ var verifyControlRequestSignature = /* @__PURE__ */ __name(async (key, timestamp
778
777
  }, "verifyControlRequestSignature");
779
778
 
780
779
  // ../../libs/app-deploy/src/keys.ts
781
- var deriveAppInternalKey = /* @__PURE__ */ __name(async (masterBase64, domainId, slug) => {
780
+ var hmacHex = /* @__PURE__ */ __name(async (masterBase64, message) => {
782
781
  const master = Uint8Array.from(atob(masterBase64), (char) => char.charCodeAt(0));
783
782
  const key = await crypto.subtle.importKey("raw", master, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
784
- const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${domainId}|${slug}`));
783
+ const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message));
785
784
  return [...new Uint8Array(signature)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
785
+ }, "hmacHex");
786
+ var deriveAppInternalKey = /* @__PURE__ */ __name(async (masterBase64, domainId, slug) => {
787
+ return hmacHex(masterBase64, `${domainId}|${slug}`);
786
788
  }, "deriveAppInternalKey");
789
+ var deriveVisitorSignature = /* @__PURE__ */ __name(async (masterBase64, domainId, anonId) => {
790
+ return hmacHex(masterBase64, `${domainId}#visitor#${anonId}`);
791
+ }, "deriveVisitorSignature");
787
792
 
788
793
  // ../../libs/app-manifest/src/cron.ts
789
794
  var MAX_CRON_LENGTH = 128;
@@ -20355,6 +20360,10 @@ var uniqueOrIssue = /* @__PURE__ */ __name((ctx, values, path, label) => {
20355
20360
  seen.add(value);
20356
20361
  }
20357
20362
  }, "uniqueOrIssue");
20363
+ var PLATFORM_ZONES = ["platform.blotout.io", "qinto-api.blotout.io", "appui.blotout.io", "mcp.blotout.io", "qinto.tech"];
20364
+ var isPlatformHost = /* @__PURE__ */ __name((host) => {
20365
+ return PLATFORM_ZONES.some((zone) => host === zone || host.endsWith(`.${zone}`));
20366
+ }, "isPlatformHost");
20358
20367
 
20359
20368
  // ../../libs/app-manifest/src/oauth.ts
20360
20369
  var OAUTH_TOKEN_METHODS = ["POST", "GET"];
@@ -20384,8 +20393,7 @@ var RESERVED_AUTHORIZE_PARAMS = /* @__PURE__ */ new Set([
20384
20393
  var PRIVATE_HOST = /^(localhost|\[?::1\]?|.+\.local|.+\.internal|10\..+|127\..+|169\.254\..+|192\.168\..+|172\.(1[6-9]|2\d|3[01])\..+|\d+\.\d+\.\d+\.\d+)$/;
20385
20394
  var vendorUrl = /* @__PURE__ */ __name((field) => {
20386
20395
  return httpsUrl(field).refine((value) => {
20387
- const host = new URL(value).hostname;
20388
- return host !== "qinto.io" && !host.endsWith(".qinto.io") && host !== "qinto.tech" && !host.endsWith(".qinto.tech");
20396
+ return !isPlatformHost(new URL(value).hostname);
20389
20397
  }, `${field} must be the vendor's own endpoint, not a Qinto platform URL`).refine((value) => {
20390
20398
  return !PRIVATE_HOST.test(new URL(value).hostname);
20391
20399
  }, `${field} must be a public host`).refine((value) => {
@@ -20845,8 +20853,7 @@ var PLATFORM_APP_SCOPE_VARIABLE_IDENTIFIER_PREFIX = "appvar-";
20845
20853
  var STORAGE_LINK_BINDING_PREFIX = "QINTO_LINK_";
20846
20854
  var legalUrl = /* @__PURE__ */ __name((field) => {
20847
20855
  return httpsUrl(field).refine((value) => {
20848
- const host = new URL(value).hostname;
20849
- return host !== "qinto.io" && !host.endsWith(".qinto.io");
20856
+ return !isPlatformHost(new URL(value).hostname);
20850
20857
  }, `${field} must be the developer's own document, not a Qinto platform URL`);
20851
20858
  }, "legalUrl");
20852
20859
  var bindingField = external_exports.string().min(1).max(255).regex(BINDING_NAME);
@@ -21019,7 +21026,7 @@ var captureSchema = external_exports.object({
21019
21026
  var endpointSchema = external_exports.object({
21020
21027
  path: external_exports.string().min(1).max(255).regex(ENDPOINT_PATH_PATTERN, "Endpoint paths are absolute; each segment is a literal or a :name parameter"),
21021
21028
  methods: external_exports.array(external_exports.enum(["GET", "POST", "PUT", "PATCH", "DELETE"])).min(1).max(5),
21022
- auth: external_exports.enum(["public", "token"]),
21029
+ auth: external_exports.enum(["public", "token", "visitor"]),
21023
21030
  mode: external_exports.enum(["webhook", "api"])
21024
21031
  }).strict();
21025
21032
  var eventsSchema = external_exports.object({
@@ -21245,6 +21252,22 @@ var validatePipelines = /* @__PURE__ */ __name((manifest, ctx) => {
21245
21252
  }
21246
21253
  }
21247
21254
  }, "validatePipelines");
21255
+ var validateVisitorEndpoint = /* @__PURE__ */ __name((manifest, ctx, endpoint, index) => {
21256
+ if (endpoint.mode !== "api") {
21257
+ ctx.addIssue({
21258
+ code: "custom",
21259
+ path: ["endpoints", index, "mode"],
21260
+ message: "A visitor endpoint answers one visitor, so its mode must be api"
21261
+ });
21262
+ }
21263
+ if (manifest.browser?.entry === void 0) {
21264
+ ctx.addIssue({
21265
+ code: "custom",
21266
+ path: ["endpoints", index, "auth"],
21267
+ message: "A visitor endpoint is called by the app's own browser code, so it needs a browser entry"
21268
+ });
21269
+ }
21270
+ }, "validateVisitorEndpoint");
21248
21271
  var validateEndpoints = /* @__PURE__ */ __name((manifest, ctx) => {
21249
21272
  const shapes = /* @__PURE__ */ new Map();
21250
21273
  for (const [index, endpoint] of manifest.endpoints.entries()) {
@@ -21270,7 +21293,9 @@ var validateEndpoints = /* @__PURE__ */ __name((manifest, ctx) => {
21270
21293
  message: `Endpoint path ${endpoint.path} names the parameter ${repeated} twice`
21271
21294
  });
21272
21295
  }
21273
- if (endpoint.mode === "api" && manifest.install.multiple) {
21296
+ if (endpoint.auth === "visitor") {
21297
+ validateVisitorEndpoint(manifest, ctx, endpoint, index);
21298
+ } else if (endpoint.mode === "api" && manifest.install.multiple) {
21274
21299
  ctx.addIssue({
21275
21300
  code: "custom",
21276
21301
  path: ["endpoints", index, "mode"],
@@ -21313,29 +21338,29 @@ var validateCapture = /* @__PURE__ */ __name((manifest, ctx) => {
21313
21338
  if (eventResults.length > 0 && browser.entry === void 0) {
21314
21339
  ctx.addIssue({ code: "custom", path: ["browser", "eventResults"], message: "eventResults needs a browser entry" });
21315
21340
  }
21316
- const capture = browser.capture;
21317
- if (capture === void 0) {
21341
+ const capture2 = browser.capture;
21342
+ if (capture2 === void 0) {
21318
21343
  return;
21319
21344
  }
21320
- if (capture.queryParams.length === 0 && capture.cookies.length === 0) {
21345
+ if (capture2.queryParams.length === 0 && capture2.cookies.length === 0) {
21321
21346
  ctx.addIssue({ code: "custom", path: ["browser", "capture"], message: "capture must name at least one query param or cookie" });
21322
21347
  }
21323
21348
  uniqueOrIssue(
21324
21349
  ctx,
21325
- capture.queryParams.map((entry) => entry.name),
21350
+ capture2.queryParams.map((entry) => entry.name),
21326
21351
  ["browser", "capture", "queryParams"],
21327
21352
  "query param"
21328
21353
  );
21329
21354
  uniqueOrIssue(
21330
21355
  ctx,
21331
- capture.cookies.map((entry) => entry.name),
21356
+ capture2.cookies.map((entry) => entry.name),
21332
21357
  ["browser", "capture", "cookies"],
21333
21358
  "cookie"
21334
21359
  );
21335
- validateCaptureNames(ctx, capture.queryParams, ["browser", "capture", "queryParams"], false);
21336
- validateCaptureNames(ctx, capture.cookies, ["browser", "capture", "cookies"], true);
21337
- const params = new Set(capture.queryParams.map((entry) => entry.name));
21338
- for (const [index, entry] of capture.cookies.entries()) {
21360
+ validateCaptureNames(ctx, capture2.queryParams, ["browser", "capture", "queryParams"], false);
21361
+ validateCaptureNames(ctx, capture2.cookies, ["browser", "capture", "cookies"], true);
21362
+ const params = new Set(capture2.queryParams.map((entry) => entry.name));
21363
+ for (const [index, entry] of capture2.cookies.entries()) {
21339
21364
  if (params.has(entry.name)) {
21340
21365
  ctx.addIssue({
21341
21366
  code: "custom",
@@ -21433,7 +21458,7 @@ var storageLinkBindingName = /* @__PURE__ */ __name((domainId) => {
21433
21458
  var MAX_APPS_Q_BYTES = 4 * 1024 * 1024;
21434
21459
 
21435
21460
  // ../../libs/app-envelope/src/envelope.ts
21436
- var ENVELOPE_VERSION = 2;
21461
+ var ENVELOPE_VERSION = 3;
21437
21462
  var ENVELOPE_ERROR_CODES = ["invalid_arguments"];
21438
21463
  var MAX_INSTALLS_PER_APP = 15;
21439
21464
  var MAX_EMITS_PER_INSTALL = 10;
@@ -21489,12 +21514,12 @@ var actionContextSchema = external_exports.discriminatedUnion("source", [
21489
21514
  var envelopeRequestSchema = external_exports.object({
21490
21515
  v: external_exports.literal(ENVELOPE_VERSION),
21491
21516
  kind: external_exports.enum(["event", "connect", "transform", "rpc"]),
21492
- site: external_exports.object({ origin: external_exports.string().min(1).max(255) }).strict(),
21493
21517
  app: external_exports.object({ slug: external_exports.string().min(1).max(30), version: external_exports.string().min(1).max(32) }).strict(),
21494
21518
  installs: external_exports.array(installSchema).min(1).max(MAX_INSTALLS_PER_APP),
21495
21519
  user: external_exports.object({
21496
21520
  anonId: external_exports.string().min(1).max(64),
21497
21521
  globalId: external_exports.string().min(1).max(64).nullable(),
21522
+ createdAt: external_exports.number().int().optional(),
21498
21523
  properties: external_exports.record(external_exports.string(), external_exports.unknown()),
21499
21524
  geo: external_exports.record(external_exports.string(), external_exports.string()).optional()
21500
21525
  }).strict().optional(),
@@ -21579,7 +21604,7 @@ var validateEnvelopeResponse = /* @__PURE__ */ __name((raw2, installCount) => {
21579
21604
  }, "validateEnvelopeResponse");
21580
21605
 
21581
21606
  // ../../libs/app-envelope/src/control.ts
21582
- var CONTROL_ENVELOPE_VERSION = 1;
21607
+ var CONTROL_ENVELOPE_VERSION = 2;
21583
21608
  var MAX_CONTROL_BYTES = 256 * 1024;
21584
21609
  var MAX_MIGRATION_CHECKPOINT_BYTES = 16 * 1024;
21585
21610
  var MAX_TOOL_ARGUMENTS_BYTES = 32 * 1024;
@@ -21688,7 +21713,6 @@ var toolSchema2 = external_exports.object({
21688
21713
  var controlEnvelopeSchema = external_exports.object({
21689
21714
  v: external_exports.literal(CONTROL_ENVELOPE_VERSION),
21690
21715
  kind: external_exports.enum(["noop", "lifecycle", "migration", "schedule", "tool"]),
21691
- site: external_exports.object({ origin: external_exports.string().min(1).max(255) }).strict(),
21692
21716
  app: external_exports.object({ slug: external_exports.string().min(1).max(30), version: external_exports.string().min(1).max(32) }).strict(),
21693
21717
  install: 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().optional(),
21694
21718
  lifecycle: lifecycleEventSchema.optional(),
@@ -21762,6 +21786,7 @@ var controlResponseSchema = external_exports.object({
21762
21786
  var ENDPOINT_CONTEXT_HEADER = "x-qinto-endpoint-context";
21763
21787
  var ENDPOINT_OUTCOMES_HEADER = "x-qinto-endpoint-outcomes";
21764
21788
  var MAX_ENDPOINT_BODY_BYTES = 1024 * 1024;
21789
+ var MAX_VISITOR_ENDPOINT_BODY_BYTES = 2 * 1024 * 1024;
21765
21790
  var MAX_ENDPOINT_OUTCOMES_BYTES = 64 * 1024;
21766
21791
  var MAX_ENDPOINT_PATH_LENGTH = 16 * 1024;
21767
21792
  var installSchema3 = 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();
@@ -21777,12 +21802,14 @@ var endpointContextSchema = external_exports.object({
21777
21802
  method: external_exports.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
21778
21803
  mode: external_exports.enum(["webhook", "api"]),
21779
21804
  version: external_exports.string().min(1).max(32),
21780
- site: external_exports.object({ origin: external_exports.string().min(1).max(255) }).strict(),
21781
21805
  geo: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
21782
- installs: external_exports.array(installSchema3).min(1).max(15)
21806
+ installs: external_exports.array(installSchema3).min(1).max(15),
21807
+ user: external_exports.object({ anonId: external_exports.string().min(1).max(64) }).strict().optional()
21783
21808
  }).strict().refine((context) => {
21784
21809
  return context.mode === "webhook" || context.installs.length === 1;
21785
- }, "An api endpoint carries exactly one install");
21810
+ }, "An api endpoint carries exactly one install").refine((context) => {
21811
+ return context.user === void 0 || context.mode === "api";
21812
+ }, "Only an api endpoint carries a visitor");
21786
21813
  var endpointOutcomesSchema = external_exports.object({
21787
21814
  outcomes: external_exports.array(
21788
21815
  external_exports.object({
@@ -25098,14 +25125,14 @@ var OpenAPIGenerator = class {
25098
25125
  this.sortDefinitions();
25099
25126
  }
25100
25127
  generateDocumentData() {
25101
- this.definitions.forEach((definition10) => this.generateSingle(definition10));
25128
+ this.definitions.forEach((definition11) => this.generateSingle(definition11));
25102
25129
  return {
25103
25130
  components: this.buildComponents(),
25104
25131
  paths: this.pathRefs
25105
25132
  };
25106
25133
  }
25107
25134
  generateComponents() {
25108
- this.definitions.forEach((definition10) => this.generateSingle(definition10));
25135
+ this.definitions.forEach((definition11) => this.generateSingle(definition11));
25109
25136
  return {
25110
25137
  components: this.buildComponents()
25111
25138
  };
@@ -25156,23 +25183,23 @@ var OpenAPIGenerator = class {
25156
25183
  return leftIndex - rightIndex;
25157
25184
  });
25158
25185
  }
25159
- generateSingle(definition10) {
25160
- if (!("type" in definition10)) {
25161
- this.generateSchemaWithRef(definition10);
25186
+ generateSingle(definition11) {
25187
+ if (!("type" in definition11)) {
25188
+ this.generateSchemaWithRef(definition11);
25162
25189
  return;
25163
25190
  }
25164
- switch (definition10.type) {
25191
+ switch (definition11.type) {
25165
25192
  case "parameter":
25166
- this.generateParameterDefinition(definition10.schema);
25193
+ this.generateParameterDefinition(definition11.schema);
25167
25194
  return;
25168
25195
  case "schema":
25169
- this.generateSchemaWithRef(definition10.schema);
25196
+ this.generateSchemaWithRef(definition11.schema);
25170
25197
  return;
25171
25198
  case "route":
25172
- this.generateSingleRoute(definition10.route);
25199
+ this.generateSingleRoute(definition11.route);
25173
25200
  return;
25174
25201
  case "component":
25175
- this.rawComponents.push(definition10);
25202
+ this.rawComponents.push(definition11);
25176
25203
  return;
25177
25204
  }
25178
25205
  }
@@ -25617,8 +25644,8 @@ var OpenApiGeneratorV31Specifics = class {
25617
25644
  }));
25618
25645
  }
25619
25646
  };
25620
- function isWebhookDefinition$1(definition10) {
25621
- return "type" in definition10 && definition10.type === "webhook";
25647
+ function isWebhookDefinition$1(definition11) {
25648
+ return "type" in definition11 && definition11.type === "webhook";
25622
25649
  }
25623
25650
  __name(isWebhookDefinition$1, "isWebhookDefinition$1");
25624
25651
  var OpenApiGeneratorV31 = class {
@@ -25641,7 +25668,7 @@ var OpenApiGeneratorV31 = class {
25641
25668
  }
25642
25669
  generateWebhooks() {
25643
25670
  this.webhookRefs = {};
25644
- this.definitions.filter(isWebhookDefinition$1).forEach((definition10) => this.generateSingleWebhook(definition10.webhook));
25671
+ this.definitions.filter(isWebhookDefinition$1).forEach((definition11) => this.generateSingleWebhook(definition11.webhook));
25645
25672
  }
25646
25673
  generateSingleWebhook(route) {
25647
25674
  const routeDoc = this.generator.generatePath(route);
@@ -26234,7 +26261,7 @@ var isGlobalId = /* @__PURE__ */ __name((id) => {
26234
26261
 
26235
26262
  // src/common/validation.ts
26236
26263
  var identifierField2 = external_exports.string().trim().min(1).refine((value) => !isGlobalId(value), { message: "gid_\u2026 ids are internal; pass an anon_\u2026 id or your own identifier" }).openapi({
26237
- description: "Who the request applies to: a canonical Qinto id (`anon_\u2026`), or any identifier from your own systems (email, phone, custom id). Internal `gid_\u2026` ids are rejected.",
26264
+ description: "Who the request applies to: a canonical Qinto id (`anon_\u2026`, or a `tag_user_id` adopted from EdgeTag), or any identifier from your own systems (email, phone, custom id). Internal `gid_\u2026` ids are rejected.",
26238
26265
  example: "crm-user-8412"
26239
26266
  });
26240
26267
  var consentCategoriesField = external_exports.object({
@@ -26264,6 +26291,12 @@ var consentAppsField = external_exports.record(
26264
26291
  external_exports.string().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9_-]*$/),
26265
26292
  external_exports.boolean()
26266
26293
  );
26294
+ var captureAppsField = external_exports.record(external_exports.string().max(30), external_exports.record(external_exports.string().max(64), external_exports.string().max(512))).refine((apps) => {
26295
+ return Object.keys(apps).length <= 50 && Object.values(apps).every((values) => Object.keys(values).length <= 32);
26296
+ }, "capture carries more apps or keys than a site can have").openapi({
26297
+ description: "Values to record, keyed by app slug and then by capture name. A name the app does not declare is dropped.",
26298
+ example: { facebook: { fbclid: "IwAR0abc" }, "google-ads": { gclid: "Cj0KCQ" } }
26299
+ });
26267
26300
  var serverActionContextField = external_exports.object({
26268
26301
  occurredAt: external_exports.number().int().nonnegative().safe().optional().openapi({
26269
26302
  description: "When the visitor action occurred, as Unix milliseconds; defaults to Qinto receipt time."
@@ -26293,6 +26326,14 @@ var acceptedResponse = external_exports.object({
26293
26326
  }).openapi("AcceptedAction", { description: "Confirmation that the action was accepted." });
26294
26327
  var respond = /* @__PURE__ */ __name(((c, schema, data, status) => c.json(schema.parse(data), status ?? 200)), "respond");
26295
26328
 
26329
+ // src/helpers/constant-time.ts
26330
+ import { timingSafeEqual } from "node:crypto";
26331
+ var constantTimeEqual = /* @__PURE__ */ __name((expected, presented) => {
26332
+ const expectedBytes = new TextEncoder().encode(expected);
26333
+ const presentedBytes = new TextEncoder().encode(presented);
26334
+ return expectedBytes.length === presentedBytes.length && timingSafeEqual(expectedBytes, presentedBytes);
26335
+ }, "constantTimeEqual");
26336
+
26296
26337
  // src/apps/consent-gate.ts
26297
26338
  var parseEnforced = /* @__PURE__ */ __name((raw2) => {
26298
26339
  let parsed;
@@ -26692,19 +26733,22 @@ var claimPair = /* @__PURE__ */ __name(async (env, identifier, globalId, anonId)
26692
26733
  }, "claimPair");
26693
26734
 
26694
26735
  // src/id-graph/user.ts
26736
+ var EDGETAG_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-[0-9]{13}$/;
26737
+ var isEdgeTagId = /* @__PURE__ */ __name((id) => {
26738
+ return EDGETAG_ID.test(id);
26739
+ }, "isEdgeTagId");
26695
26740
  var isCanonicalId = /* @__PURE__ */ __name((id) => {
26696
- return id.startsWith("anon_");
26741
+ return id.startsWith("anon_") || isEdgeTagId(id);
26697
26742
  }, "isCanonicalId");
26698
26743
  var generateUserId = /* @__PURE__ */ __name(() => {
26699
26744
  return `anon_${crypto.randomUUID()}`;
26700
26745
  }, "generateUserId");
26701
- var restoreUser = /* @__PURE__ */ __name(async (env, cookie) => {
26702
- const minted = generateUserId();
26746
+ var restoreUser = /* @__PURE__ */ __name(async (env, cookie, candidate) => {
26703
26747
  if (cookie === void 0) {
26704
- return minted;
26748
+ return candidate;
26705
26749
  }
26706
- const assignment = await claimFallbackAnon(env, cookie, minted);
26707
- setSpanAttributes({ "qinto.fallback_restored": assignment.anonId !== minted });
26750
+ const assignment = await claimFallbackAnon(env, cookie, candidate);
26751
+ setSpanAttributes({ "qinto.fallback_restored": assignment.anonId !== candidate });
26708
26752
  return assignment.anonId;
26709
26753
  }, "restoreUser");
26710
26754
  var resolveUser = /* @__PURE__ */ __name(async (env, id) => {
@@ -27497,11 +27541,11 @@ var APP_HOOK_TIMEOUT_MS = 25e3;
27497
27541
  var MAX_EMIT_DEPTH = 2;
27498
27542
  var HOOK_URL = "https://app/__qinto/hook";
27499
27543
  var invocationContext = /* @__PURE__ */ __name((env) => {
27500
- const { APPS_KEY_MASTER: master, QINTO_DOMAIN_ID: domainId, ROOT_DOMAIN: rootDomain } = env;
27544
+ const { APPS_KEY_MASTER: master, QINTO_DOMAIN_ID: domainId } = env;
27501
27545
  if (master === void 0 || domainId === void 0) {
27502
27546
  return void 0;
27503
27547
  }
27504
- return { master, domainId, origin: `https://${rootDomain ?? ""}`, consentEnforced: consentEnforced(env) };
27548
+ return { master, domainId, consentEnforced: consentEnforced(env) };
27505
27549
  }, "invocationContext");
27506
27550
  var postHookEnvelope = /* @__PURE__ */ __name(async (env, { binding, key, envelope, timeoutMs }) => {
27507
27551
  const service = env[binding];
@@ -27567,12 +27611,12 @@ var buildEnvelope = /* @__PURE__ */ __name((target, trigger, snapshot, context)
27567
27611
  return {
27568
27612
  v: ENVELOPE_VERSION,
27569
27613
  kind: trigger.kind,
27570
- site: { origin: context.origin },
27571
27614
  app: { slug: target.slug, version: target.app.version },
27572
27615
  installs: target.installs,
27573
27616
  user: {
27574
27617
  anonId: snapshot.anonId,
27575
27618
  globalId: snapshot.globalId,
27619
+ ...snapshot.createdAt === null ? {} : { createdAt: snapshot.createdAt },
27576
27620
  // Custom properties intentionally win on key collisions with PII, matching the profile read path.
27577
27621
  properties: {
27578
27622
  ...buckets.includes("pii") ? snapshot.pii : {},
@@ -27784,12 +27828,12 @@ var invokeApp = /* @__PURE__ */ __name(async (env, input2) => {
27784
27828
 
27785
27829
  // src/apps/rate-limit.ts
27786
27830
  var limiterAbsenceLogged = false;
27787
- var rateLimited = /* @__PURE__ */ __name(async (env, key) => {
27788
- const limiter = env.BROKER_RATE_LIMITER;
27831
+ var rateLimited = /* @__PURE__ */ __name(async (env, key, binding = "BROKER_RATE_LIMITER") => {
27832
+ const limiter = env[binding];
27789
27833
  if (limiter === void 0) {
27790
27834
  if (!limiterAbsenceLogged) {
27791
27835
  limiterAbsenceLogged = true;
27792
- addSpanEvent("broker rate limiter binding is absent; limits are off", { level: "warn" });
27836
+ addSpanEvent("rate limiter binding is absent; limits are off", { level: "warn", attrs: { binding } });
27793
27837
  }
27794
27838
  return false;
27795
27839
  }
@@ -27887,11 +27931,6 @@ var parseJson = /* @__PURE__ */ __name((raw2) => {
27887
27931
  return null;
27888
27932
  }
27889
27933
  }, "parseJson");
27890
- var constantTimeEqual = /* @__PURE__ */ __name((expected, presented) => {
27891
- const expectedBytes = new TextEncoder().encode(expected);
27892
- const presentedBytes = new TextEncoder().encode(presented);
27893
- return expectedBytes.length === presentedBytes.length && timingSafeEqual(expectedBytes, presentedBytes);
27894
- }, "constantTimeEqual");
27895
27934
  var factsCursor = external_exports.object({
27896
27935
  userId: external_exports.string().min(1).max(64),
27897
27936
  kinds: external_exports.array(external_exports.enum(APP_FACT_KINDS)).min(1).max(APP_FACT_KINDS.length),
@@ -28022,7 +28061,6 @@ var runRelayRpc = /* @__PURE__ */ __name(async (env, { context, body, callee, in
28022
28061
  envelope: {
28023
28062
  v: ENVELOPE_VERSION,
28024
28063
  kind: "rpc",
28025
- site: { origin: context.origin },
28026
28064
  app: { slug: body.callee, version: callee.version },
28027
28065
  installs: [install],
28028
28066
  // No bucket projection for rpc: the callee sees only the subject's id, never its profile.
@@ -28526,7 +28564,8 @@ var SCOPE = {
28526
28564
  propertiesRead: 16,
28527
28565
  consentRead: 32,
28528
28566
  sessionRead: 64,
28529
- idGraphInspect: 128
28567
+ idGraphInspect: 128,
28568
+ capture: 256
28530
28569
  };
28531
28570
  var ALL_SCOPE_MASK = Object.values(SCOPE).reduce((mask, bit) => mask | bit, 0);
28532
28571
  var WORKER_ACCESS_TOKEN_SCOPES = [
@@ -28556,6 +28595,12 @@ var WORKER_ACCESS_TOKEN_SCOPES = [
28556
28595
  bit: SCOPE.sessionRead,
28557
28596
  label: "Read session ids",
28558
28597
  description: "GET /v1/session, GET /v1/person \u2014 read the anonymous and person ids for a user, and the sessions linked to a person."
28598
+ },
28599
+ {
28600
+ key: "capture",
28601
+ bit: SCOPE.capture,
28602
+ label: "Write captures",
28603
+ description: "POST /v1/capture \u2014 record click ids and vendor ids an installed app declares."
28559
28604
  }
28560
28605
  ];
28561
28606
 
@@ -28781,18 +28826,498 @@ var streamWithDeadlines = /* @__PURE__ */ __name((source, { idleMs, totalMs }) =
28781
28826
  });
28782
28827
  }, "streamWithDeadlines");
28783
28828
 
28829
+ // ../../libs/bot-protection/src/countries.ts
28830
+ var EU_TOKEN = "EU";
28831
+ var COUNTRY_CODES = [
28832
+ "AD",
28833
+ "AE",
28834
+ "AF",
28835
+ "AG",
28836
+ "AI",
28837
+ "AL",
28838
+ "AM",
28839
+ "AO",
28840
+ "AQ",
28841
+ "AR",
28842
+ "AS",
28843
+ "AT",
28844
+ "AU",
28845
+ "AW",
28846
+ "AX",
28847
+ "AZ",
28848
+ "BA",
28849
+ "BB",
28850
+ "BD",
28851
+ "BE",
28852
+ "BF",
28853
+ "BG",
28854
+ "BH",
28855
+ "BI",
28856
+ "BJ",
28857
+ "BL",
28858
+ "BM",
28859
+ "BN",
28860
+ "BO",
28861
+ "BQ",
28862
+ "BR",
28863
+ "BS",
28864
+ "BT",
28865
+ "BV",
28866
+ "BW",
28867
+ "BY",
28868
+ "BZ",
28869
+ "CA",
28870
+ "CC",
28871
+ "CD",
28872
+ "CF",
28873
+ "CG",
28874
+ "CH",
28875
+ "CI",
28876
+ "CK",
28877
+ "CL",
28878
+ "CM",
28879
+ "CN",
28880
+ "CO",
28881
+ "CR",
28882
+ "CU",
28883
+ "CV",
28884
+ "CW",
28885
+ "CX",
28886
+ "CY",
28887
+ "CZ",
28888
+ "DE",
28889
+ "DJ",
28890
+ "DK",
28891
+ "DM",
28892
+ "DO",
28893
+ "DZ",
28894
+ "EC",
28895
+ "EE",
28896
+ "EG",
28897
+ "EH",
28898
+ "ER",
28899
+ "ES",
28900
+ "ET",
28901
+ "FI",
28902
+ "FJ",
28903
+ "FK",
28904
+ "FM",
28905
+ "FO",
28906
+ "FR",
28907
+ "GA",
28908
+ "GB",
28909
+ "GD",
28910
+ "GE",
28911
+ "GF",
28912
+ "GG",
28913
+ "GH",
28914
+ "GI",
28915
+ "GL",
28916
+ "GM",
28917
+ "GN",
28918
+ "GP",
28919
+ "GQ",
28920
+ "GR",
28921
+ "GS",
28922
+ "GT",
28923
+ "GU",
28924
+ "GW",
28925
+ "GY",
28926
+ "HK",
28927
+ "HM",
28928
+ "HN",
28929
+ "HR",
28930
+ "HT",
28931
+ "HU",
28932
+ "ID",
28933
+ "IE",
28934
+ "IL",
28935
+ "IM",
28936
+ "IN",
28937
+ "IO",
28938
+ "IQ",
28939
+ "IR",
28940
+ "IS",
28941
+ "IT",
28942
+ "JE",
28943
+ "JM",
28944
+ "JO",
28945
+ "JP",
28946
+ "KE",
28947
+ "KG",
28948
+ "KH",
28949
+ "KI",
28950
+ "KM",
28951
+ "KN",
28952
+ "KP",
28953
+ "KR",
28954
+ "KW",
28955
+ "KY",
28956
+ "KZ",
28957
+ "LA",
28958
+ "LB",
28959
+ "LC",
28960
+ "LI",
28961
+ "LK",
28962
+ "LR",
28963
+ "LS",
28964
+ "LT",
28965
+ "LU",
28966
+ "LV",
28967
+ "LY",
28968
+ "MA",
28969
+ "MC",
28970
+ "MD",
28971
+ "ME",
28972
+ "MF",
28973
+ "MG",
28974
+ "MH",
28975
+ "MK",
28976
+ "ML",
28977
+ "MM",
28978
+ "MN",
28979
+ "MO",
28980
+ "MP",
28981
+ "MQ",
28982
+ "MR",
28983
+ "MS",
28984
+ "MT",
28985
+ "MU",
28986
+ "MV",
28987
+ "MW",
28988
+ "MX",
28989
+ "MY",
28990
+ "MZ",
28991
+ "NA",
28992
+ "NC",
28993
+ "NE",
28994
+ "NF",
28995
+ "NG",
28996
+ "NI",
28997
+ "NL",
28998
+ "NO",
28999
+ "NP",
29000
+ "NR",
29001
+ "NU",
29002
+ "NZ",
29003
+ "OM",
29004
+ "PA",
29005
+ "PE",
29006
+ "PF",
29007
+ "PG",
29008
+ "PH",
29009
+ "PK",
29010
+ "PL",
29011
+ "PM",
29012
+ "PN",
29013
+ "PR",
29014
+ "PS",
29015
+ "PT",
29016
+ "PW",
29017
+ "PY",
29018
+ "QA",
29019
+ "RE",
29020
+ "RO",
29021
+ "RS",
29022
+ "RU",
29023
+ "RW",
29024
+ "SA",
29025
+ "SB",
29026
+ "SC",
29027
+ "SD",
29028
+ "SE",
29029
+ "SG",
29030
+ "SH",
29031
+ "SI",
29032
+ "SJ",
29033
+ "SK",
29034
+ "SL",
29035
+ "SM",
29036
+ "SN",
29037
+ "SO",
29038
+ "SR",
29039
+ "SS",
29040
+ "ST",
29041
+ "SV",
29042
+ "SX",
29043
+ "SY",
29044
+ "SZ",
29045
+ "TC",
29046
+ "TD",
29047
+ "TF",
29048
+ "TG",
29049
+ "TH",
29050
+ "TJ",
29051
+ "TK",
29052
+ "TL",
29053
+ "TM",
29054
+ "TN",
29055
+ "TO",
29056
+ "TR",
29057
+ "TT",
29058
+ "TV",
29059
+ "TW",
29060
+ "TZ",
29061
+ "UA",
29062
+ "UG",
29063
+ "UM",
29064
+ "US",
29065
+ "UY",
29066
+ "UZ",
29067
+ "VA",
29068
+ "VC",
29069
+ "VE",
29070
+ "VG",
29071
+ "VI",
29072
+ "VN",
29073
+ "VU",
29074
+ "WF",
29075
+ "WS",
29076
+ "YE",
29077
+ "YT",
29078
+ "ZA",
29079
+ "ZM",
29080
+ "ZW"
29081
+ ];
29082
+ var SELECTABLE_TOKENS = /* @__PURE__ */ new Set([EU_TOKEN, ...COUNTRY_CODES]);
29083
+ var isSelectableCountryToken = /* @__PURE__ */ __name((token) => {
29084
+ return SELECTABLE_TOKENS.has(token);
29085
+ }, "isSelectableCountryToken");
29086
+ var matchesRule = /* @__PURE__ */ __name((country, isEU, list) => {
29087
+ if (country !== void 0 && list.includes(country)) {
29088
+ return true;
29089
+ }
29090
+ return isEU && list.includes(EU_TOKEN);
29091
+ }, "matchesRule");
29092
+ var isCountryBlocked = /* @__PURE__ */ __name((country, isEU, rule) => {
29093
+ if (rule === null || rule.list.length === 0) {
29094
+ return false;
29095
+ }
29096
+ const matched = matchesRule(country, isEU, rule.list);
29097
+ return rule.mode === "block" ? matched : !matched;
29098
+ }, "isCountryBlocked");
29099
+
29100
+ // ../../libs/bot-protection/src/score.ts
29101
+ var BOT_SCORE_MIN = 1;
29102
+ var BOT_SCORE_MAX = 99;
29103
+
29104
+ // ../../libs/bot-protection/src/config.ts
29105
+ var NO_PROTECTION = { minBotScore: null, countries: null };
29106
+ var normalizeScore = /* @__PURE__ */ __name((value, problems) => {
29107
+ if (value === void 0 || value === null) {
29108
+ return null;
29109
+ }
29110
+ if (typeof value === "number" && Number.isInteger(value) && value >= BOT_SCORE_MIN && value <= BOT_SCORE_MAX) {
29111
+ return value;
29112
+ }
29113
+ problems.push("has an out-of-range minBotScore");
29114
+ return null;
29115
+ }, "normalizeScore");
29116
+ var normalizeCountries = /* @__PURE__ */ __name((value, problems) => {
29117
+ if (value === void 0 || value === null) {
29118
+ return null;
29119
+ }
29120
+ const record2 = value;
29121
+ if (record2.mode !== "allow" && record2.mode !== "block" || !Array.isArray(record2.list)) {
29122
+ problems.push("has an invalid countries rule");
29123
+ return null;
29124
+ }
29125
+ const list = [];
29126
+ let dropped = 0;
29127
+ for (const token of record2.list) {
29128
+ if (typeof token === "string" && isSelectableCountryToken(token)) {
29129
+ list.push(token);
29130
+ } else {
29131
+ dropped += 1;
29132
+ }
29133
+ }
29134
+ if (dropped > 0) {
29135
+ problems.push(`dropped ${dropped} invalid country token(s)`);
29136
+ }
29137
+ return list.length === 0 ? null : { mode: record2.mode, list };
29138
+ }, "normalizeCountries");
29139
+ var parseBotProtection = /* @__PURE__ */ __name((raw2) => {
29140
+ if (raw2.trim() === "") {
29141
+ return { config: NO_PROTECTION };
29142
+ }
29143
+ let value;
29144
+ try {
29145
+ value = JSON.parse(raw2);
29146
+ } catch {
29147
+ return { config: NO_PROTECTION, error: "BOT_PROTECTION is not valid JSON" };
29148
+ }
29149
+ if (typeof value !== "object" || value === null) {
29150
+ return { config: NO_PROTECTION, error: "BOT_PROTECTION is not an object" };
29151
+ }
29152
+ const record2 = value;
29153
+ const problems = [];
29154
+ const config2 = {
29155
+ minBotScore: normalizeScore(record2.minBotScore, problems),
29156
+ countries: normalizeCountries(record2.countries, problems)
29157
+ };
29158
+ return problems.length > 0 ? { config: config2, error: `BOT_PROTECTION ${problems.join("; ")}` } : { config: config2 };
29159
+ }, "parseBotProtection");
29160
+ var isRequestBlocked = /* @__PURE__ */ __name((config2, signal) => {
29161
+ if (config2.minBotScore !== null && signal.botScore !== void 0 && signal.botScore < config2.minBotScore) {
29162
+ return true;
29163
+ }
29164
+ return isCountryBlocked(signal.country, signal.isEU ?? false, config2.countries);
29165
+ }, "isRequestBlocked");
29166
+
29167
+ // src/helpers/bot-check.ts
29168
+ var botRejected = /* @__PURE__ */ __name((env, geo) => {
29169
+ return traced("bot.check", {}, () => {
29170
+ const { config: protection, error: error61 } = parseBotProtection(env.BOT_PROTECTION);
29171
+ if (error61 !== void 0) {
29172
+ addSpanEvent("BOT_PROTECTION is misconfigured; enforcing it with the invalid parts dropped", {
29173
+ level: "error",
29174
+ attrs: { error: String(error61) }
29175
+ });
29176
+ }
29177
+ const rejected = isRequestBlocked(protection, { botScore: geo.botScore, country: geo.country, isEU: geo.isEUCountry === "yes" });
29178
+ setSpanAttributes({ "bot.blocked": rejected, ...geo.botScore === void 0 ? {} : { "bot.score": geo.botScore } });
29179
+ return rejected;
29180
+ });
29181
+ }, "botRejected");
29182
+
29183
+ // src/helpers/cookie.ts
29184
+ var COOKIE_NAME2 = "q_user_id";
29185
+ var EDGETAG_COOKIE_NAME = "tag_user_id";
29186
+ var COOKIE_MAX_AGE = 400 * 24 * 60 * 60;
29187
+ var SESSION_COOKIE_NAME = "q_session";
29188
+ var SESSION_MAX_AGE = 30 * 60;
29189
+ var SESSION_VALUE = /^(vis_[0-9a-f-]{36})\.(\d{1,15})$/;
29190
+ var FALLBACK_MIN_LENGTH = 4;
29191
+ var fallbackCookie = /* @__PURE__ */ __name((c) => {
29192
+ const name = c.env.FALLBACK_COOKIE;
29193
+ if (name === "") {
29194
+ return void 0;
29195
+ }
29196
+ const value = getCookie(c, name);
29197
+ if (value === void 0 || value.length < FALLBACK_MIN_LENGTH) {
29198
+ return void 0;
29199
+ }
29200
+ return { name, value };
29201
+ }, "fallbackCookie");
29202
+ var edgeTagUserId = /* @__PURE__ */ __name((c) => {
29203
+ const value = getCookie(c, EDGETAG_COOKIE_NAME);
29204
+ return value !== void 0 && isEdgeTagId(value) ? value : void 0;
29205
+ }, "edgeTagUserId");
29206
+ var visitSession = /* @__PURE__ */ __name((c, now) => {
29207
+ const match2 = SESSION_VALUE.exec(getCookie(c, SESSION_COOKIE_NAME) ?? "");
29208
+ if (match2 === null) {
29209
+ return { id: `vis_${crypto.randomUUID()}`, isNew: true, startedAt: now };
29210
+ }
29211
+ return { id: match2[1], isNew: false, startedAt: Number(match2[2]) };
29212
+ }, "visitSession");
29213
+
29214
+ // src/helpers/visitor-signature.ts
29215
+ var VISITOR_SIGNATURE_COOKIE = "q_user_sig";
29216
+ var signVisitor = /* @__PURE__ */ __name(async (env, anonId) => {
29217
+ const context = invocationContext(env);
29218
+ if (context === void 0) {
29219
+ return void 0;
29220
+ }
29221
+ return deriveVisitorSignature(context.master, context.domainId, anonId);
29222
+ }, "signVisitor");
29223
+ var verifyVisitor = /* @__PURE__ */ __name(async (env, anonId, signature) => {
29224
+ const expected = await signVisitor(env, anonId);
29225
+ return expected !== void 0 && constantTimeEqual(expected, signature);
29226
+ }, "verifyVisitor");
29227
+
29228
+ // src/apps/visitor-endpoint.ts
29229
+ var VISITOR_INSTALL_HEADER = "x-qinto-install";
29230
+ var refuse = /* @__PURE__ */ __name((status, error61) => {
29231
+ return Response.json({ error: error61 }, { status });
29232
+ }, "refuse");
29233
+ var originAllowed = /* @__PURE__ */ __name((request, env) => {
29234
+ const origin = request.headers.get("origin");
29235
+ if (origin === null) {
29236
+ return true;
29237
+ }
29238
+ let host;
29239
+ try {
29240
+ host = new URL(origin).host;
29241
+ } catch {
29242
+ return false;
29243
+ }
29244
+ if (host === new URL(request.url).host) {
29245
+ return true;
29246
+ }
29247
+ const root = env.ROOT_DOMAIN;
29248
+ return root !== void 0 && root !== "" && (host === root || host.endsWith(`.${root}`));
29249
+ }, "originAllowed");
29250
+ var withCors = /* @__PURE__ */ __name((response, request, method) => {
29251
+ const origin = request.headers.get("origin");
29252
+ if (origin === null) {
29253
+ return response;
29254
+ }
29255
+ const headers = new Headers(response.headers);
29256
+ headers.set("access-control-allow-origin", origin);
29257
+ headers.set("access-control-allow-credentials", "true");
29258
+ headers.set("access-control-allow-methods", method);
29259
+ headers.set("access-control-allow-headers", `content-type, ${VISITOR_INSTALL_HEADER}`);
29260
+ headers.append("vary", "Origin");
29261
+ return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
29262
+ }, "withCors");
29263
+ var authorize = /* @__PURE__ */ __name(async (c, { slug, app: app2 }) => {
29264
+ if (await botRejected(c.env, geoFromRequest(c.req.raw))) {
29265
+ return refuse(403, "traffic_rejected");
29266
+ }
29267
+ const anonId = getCookie(c, COOKIE_NAME2);
29268
+ const signature = getCookie(c, VISITOR_SIGNATURE_COOKIE);
29269
+ if (anonId === void 0 || signature === void 0 || !await verifyVisitor(c.env, anonId, signature)) {
29270
+ return refuse(401, "visitor_required");
29271
+ }
29272
+ const installId = c.req.header(VISITOR_INSTALL_HEADER);
29273
+ const install = app2.installs.find((candidate) => {
29274
+ return candidate.active && candidate.id === installId;
29275
+ });
29276
+ if (install === void 0) {
29277
+ return refuse(403, "forbidden");
29278
+ }
29279
+ if (await rateLimited(c.env, `${slug}:${anonId}:visitor`, "VISITOR_RATE_LIMITER")) {
29280
+ return refuse(429, "rate_limited");
29281
+ }
29282
+ if (!await anonStub(c.env, anonId).appAllowed(slug, ownSite(c.env), currentTraceContext())) {
29283
+ return refuse(403, "forbidden");
29284
+ }
29285
+ return { anonId, install };
29286
+ }, "authorize");
29287
+ var serve = /* @__PURE__ */ __name(async (c, request, forward) => {
29288
+ const visitor = await authorize(c, request);
29289
+ if (visitor instanceof Response) {
29290
+ return visitor;
29291
+ }
29292
+ if (request.method === "GET") {
29293
+ return forward(visitor, void 0);
29294
+ }
29295
+ const body = await readBoundedBody(c.req.raw, MAX_VISITOR_ENDPOINT_BODY_BYTES);
29296
+ if (body === null) {
29297
+ return refuse(413, "too_large");
29298
+ }
29299
+ return forward(visitor, body);
29300
+ }, "serve");
29301
+ var handleVisitorEndpoint = /* @__PURE__ */ __name(async (c, request, forward) => {
29302
+ if (!originAllowed(c.req.raw, c.env)) {
29303
+ return refuse(403, "forbidden");
29304
+ }
29305
+ const response = request.preflight ? new Response(null, { status: 204 }) : await serve(c, request, forward);
29306
+ return withCors(response, c.req.raw, request.method);
29307
+ }, "handleVisitorEndpoint");
29308
+
28784
29309
  // src/apps/endpoint-proxy.ts
28785
29310
  var API_ENDPOINT_START_TIMEOUT_MS = 6e4;
28786
29311
  var WEBHOOK_ENDPOINT_TIMEOUT_MS = 6e4;
28787
29312
  var API_ENDPOINT_STREAM_IDLE_TIMEOUT_MS = 15e4;
28788
29313
  var API_ENDPOINT_STREAM_MAX_DURATION_MS = 3e5;
28789
29314
  var DENIED_REQUEST_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "x-cf-data"]);
28790
- var refuse = /* @__PURE__ */ __name((status, error61) => {
29315
+ var refuse2 = /* @__PURE__ */ __name((status, error61) => {
28791
29316
  return Response.json({ error: error61 }, { status });
28792
29317
  }, "refuse");
28793
29318
  var upstreamFailed = /* @__PURE__ */ __name((slug, reason) => {
28794
29319
  addSpanEvent("app endpoint upstream failed", { level: "warn", attrs: { app: slug, reason } });
28795
- return refuse(502, "upstream");
29320
+ return refuse2(502, "upstream");
28796
29321
  }, "upstreamFailed");
28797
29322
  var isHttpMethod = /* @__PURE__ */ __name((value) => {
28798
29323
  return HTTP_METHODS.includes(value);
@@ -28814,10 +29339,17 @@ var matchEndpoint = /* @__PURE__ */ __name((app2, path, method) => {
28814
29339
  const declared = serving.find((endpoint) => {
28815
29340
  return endpoint.path === matched.route;
28816
29341
  });
28817
- if (declared.auth !== "public" && declared.auth !== "token" || declared.mode !== "webhook" && declared.mode !== "api") {
29342
+ if (!["public", "token", "visitor"].includes(declared.auth) || declared.mode !== "webhook" && declared.mode !== "api") {
28818
29343
  return void 0;
28819
29344
  }
28820
- return { route: declared.path, path, params: matched.params, method, auth: declared.auth, mode: declared.mode };
29345
+ return {
29346
+ route: declared.path,
29347
+ path,
29348
+ params: matched.params,
29349
+ method,
29350
+ auth: declared.auth,
29351
+ mode: declared.mode
29352
+ };
28821
29353
  }, "matchEndpoint");
28822
29354
  var authorizeEndpoint = /* @__PURE__ */ __name(async (c, slug, endpoint) => {
28823
29355
  if (endpoint.auth === "public") {
@@ -28825,14 +29357,14 @@ var authorizeEndpoint = /* @__PURE__ */ __name(async (c, slug, endpoint) => {
28825
29357
  }
28826
29358
  const token = readBearerToken(c.req.raw);
28827
29359
  if (token === null) {
28828
- return refuse(401, "unauthorized");
29360
+ return refuse2(401, "unauthorized");
28829
29361
  }
28830
29362
  const entry = await workerAccessTokenEntry(c.env, token);
28831
29363
  if (entry === void 0) {
28832
- return refuse(401, "unauthorized");
29364
+ return refuse2(401, "unauthorized");
28833
29365
  }
28834
29366
  if (!matchesAppEndpoint(entry.endpoints ?? [], `/app/${slug}${endpoint.path}`, endpoint.method)) {
28835
- return refuse(403, "forbidden");
29367
+ return refuse2(403, "forbidden");
28836
29368
  }
28837
29369
  return void 0;
28838
29370
  }, "authorizeEndpoint");
@@ -28885,9 +29417,9 @@ var webhookAck = /* @__PURE__ */ __name(async (slug, upstream) => {
28885
29417
  return Response.json({ ok: true });
28886
29418
  }
28887
29419
  if (status === 404) {
28888
- return refuse(404, "not_handled");
29420
+ return refuse2(404, "not_handled");
28889
29421
  }
28890
- return refuse(502, "upstream");
29422
+ return refuse2(502, "upstream");
28891
29423
  }, "webhookAck");
28892
29424
  var fetchApiResponse = /* @__PURE__ */ __name(async (service, url2, init) => {
28893
29425
  const controller = new AbortController();
@@ -28901,7 +29433,7 @@ var fetchApiResponse = /* @__PURE__ */ __name(async (service, url2, init) => {
28901
29433
  }
28902
29434
  }, "fetchApiResponse");
28903
29435
  var runForwardToApp = /* @__PURE__ */ __name(async (c, input2) => {
28904
- const { slug, app: app2, endpoint, installs, body, search } = input2;
29436
+ const { slug, app: app2, endpoint, installs, body, search, anonId } = input2;
28905
29437
  const context = invocationContext(c.env);
28906
29438
  if (context === void 0) {
28907
29439
  return upstreamFailed(slug, "The deploy carries no apps key master.");
@@ -28926,9 +29458,9 @@ var runForwardToApp = /* @__PURE__ */ __name(async (c, input2) => {
28926
29458
  method: endpoint.method,
28927
29459
  mode: endpoint.mode,
28928
29460
  version: app2.version,
28929
- site: { origin: context.origin },
28930
29461
  ...app2.data.includes("geo") && requestGeo !== void 0 ? { geo: requestGeo } : {},
28931
- installs: installs.map((install) => ({ id: install.id, ord: install.ord, label: install.label }))
29462
+ installs: installs.map((install) => ({ id: install.id, ord: install.ord, label: install.label })),
29463
+ ...anonId === void 0 ? {} : { user: { anonId } }
28932
29464
  })
28933
29465
  );
28934
29466
  let upstream;
@@ -28967,20 +29499,8 @@ var forwardToApp = /* @__PURE__ */ __name(async (c, input2) => {
28967
29499
  }
28968
29500
  );
28969
29501
  }, "forwardToApp");
28970
- var handleAppEndpoint = /* @__PURE__ */ __name(async (c, slug) => {
28971
- const app2 = siteRegistry().registry.apps[slug];
28972
- if (app2 === void 0) {
28973
- return refuse(404, "not_found");
28974
- }
28975
- const url2 = new URL(c.req.url);
28976
- const prefix = `/app/${slug}`;
28977
- if (url2.pathname !== prefix && !url2.pathname.startsWith(`${prefix}/`)) {
28978
- return refuse(404, "not_found");
28979
- }
28980
- const endpoint = matchEndpoint(app2, url2.pathname.slice(prefix.length) || "/", c.req.method);
28981
- if (endpoint === void 0) {
28982
- return refuse(404, "not_found");
28983
- }
29502
+ var serveEndpoint = /* @__PURE__ */ __name(async (c, input2) => {
29503
+ const { slug, app: app2, endpoint, search } = input2;
28984
29504
  const refusal = await authorizeEndpoint(c, slug, endpoint);
28985
29505
  if (refusal !== void 0) {
28986
29506
  return refusal;
@@ -28988,21 +29508,44 @@ var handleAppEndpoint = /* @__PURE__ */ __name(async (c, slug) => {
28988
29508
  const installs = app2.installs.filter((install) => install.active);
28989
29509
  const [first] = installs;
28990
29510
  if (first === void 0) {
28991
- return refuse(503, "inactive");
29511
+ return refuse2(503, "inactive");
28992
29512
  }
28993
29513
  const key = endpoint.mode === "api" ? `${slug}:${first.id}:endpoint` : `${slug}:${endpoint.route}:endpoint`;
28994
29514
  if (await rateLimited(c.env, key)) {
28995
- return refuse(429, "rate_limited");
29515
+ return refuse2(429, "rate_limited");
28996
29516
  }
28997
29517
  let body;
28998
29518
  if (endpoint.method !== "GET") {
28999
29519
  const bytes = await readBoundedBody(c.req.raw, MAX_ENDPOINT_BODY_BYTES);
29000
29520
  if (bytes === null) {
29001
- return refuse(413, "too_large");
29521
+ return refuse2(413, "too_large");
29002
29522
  }
29003
29523
  body = bytes;
29004
29524
  }
29005
- return forwardToApp(c, { slug, app: app2, endpoint, installs, body, search: url2.search });
29525
+ return forwardToApp(c, { slug, app: app2, endpoint, installs, body, search });
29526
+ }, "serveEndpoint");
29527
+ var handleAppEndpoint = /* @__PURE__ */ __name(async (c, slug) => {
29528
+ const app2 = siteRegistry().registry.apps[slug];
29529
+ if (app2 === void 0) {
29530
+ return refuse2(404, "not_found");
29531
+ }
29532
+ const url2 = new URL(c.req.url);
29533
+ const prefix = `/app/${slug}`;
29534
+ if (url2.pathname !== prefix && !url2.pathname.startsWith(`${prefix}/`)) {
29535
+ return refuse2(404, "not_found");
29536
+ }
29537
+ const preflight2 = c.req.method === "OPTIONS";
29538
+ const method = preflight2 ? c.req.header("access-control-request-method") : c.req.method;
29539
+ const endpoint = matchEndpoint(app2, url2.pathname.slice(prefix.length) || "/", method ?? "");
29540
+ if (endpoint === void 0 || preflight2 && endpoint.auth !== "visitor") {
29541
+ return refuse2(404, "not_found");
29542
+ }
29543
+ if (endpoint.auth === "visitor") {
29544
+ return handleVisitorEndpoint(c, { slug, app: app2, method: endpoint.method, preflight: preflight2 }, (visitor, body) => {
29545
+ return forwardToApp(c, { slug, app: app2, endpoint, installs: [visitor.install], body, search: url2.search, anonId: visitor.anonId });
29546
+ });
29547
+ }
29548
+ return serveEndpoint(c, { slug, app: app2, endpoint, search: url2.search });
29006
29549
  }, "handleAppEndpoint");
29007
29550
  var appEndpointRoutes = new Hono2();
29008
29551
  appEndpointRoutes.all("/app/:slug/*", (c) => {
@@ -29204,12 +29747,12 @@ var mintAppUiWriteTicket = /* @__PURE__ */ __name(async (input2) => {
29204
29747
 
29205
29748
  // src/apps/ui-proxy.ts
29206
29749
  var APP_UI_TIMEOUT_MS = 6e4;
29207
- var refuse2 = /* @__PURE__ */ __name((origin, status, error61) => {
29750
+ var refuse3 = /* @__PURE__ */ __name((origin, status, error61) => {
29208
29751
  return Response.json({ error: error61 }, { status, headers: { "access-control-allow-origin": origin } });
29209
29752
  }, "refuse");
29210
29753
  var upstreamFailed2 = /* @__PURE__ */ __name((origin, slug, reason) => {
29211
29754
  addSpanEvent("app ui upstream failed", { level: "warn", attrs: { app: slug, reason } });
29212
- return refuse2(origin, 502, "upstream");
29755
+ return refuse3(origin, 502, "upstream");
29213
29756
  }, "upstreamFailed");
29214
29757
  var preflight = /* @__PURE__ */ __name((origin) => {
29215
29758
  return new Response(null, {
@@ -29310,30 +29853,30 @@ var forwardToApp2 = /* @__PURE__ */ __name(async (c, input2) => {
29310
29853
  var authorizeUi = /* @__PURE__ */ __name(async (c, slug, origin) => {
29311
29854
  const app2 = siteRegistry().registry.apps[slug];
29312
29855
  if (app2 === void 0) {
29313
- return refuse2(origin, 404, "not_found");
29856
+ return refuse3(origin, 404, "not_found");
29314
29857
  }
29315
29858
  const authorization = c.req.header("authorization");
29316
29859
  const token = authorization?.startsWith("Bearer ") === true ? authorization.slice("Bearer ".length) : void 0;
29317
29860
  const keyEnv = c.env.APP_UI_KEY;
29318
29861
  const keys = keyEnv === void 0 ? null : parseAppUiKeyEnv(keyEnv);
29319
29862
  if (token === void 0 || keys === null) {
29320
- return refuse2(origin, 401, "unauthorized");
29863
+ return refuse3(origin, 401, "unauthorized");
29321
29864
  }
29322
29865
  const verdict = await verifyAppUiToken({ token, keys, slug, now: /* @__PURE__ */ new Date() });
29323
29866
  if (!verdict.ok) {
29324
- return verdict.reason === "wrong_app" ? refuse2(origin, 403, "forbidden") : refuse2(origin, 401, "unauthorized");
29867
+ return verdict.reason === "wrong_app" ? refuse3(origin, 403, "forbidden") : refuse3(origin, 401, "unauthorized");
29325
29868
  }
29326
29869
  if (verdict.claims.appVersionId !== app2.versionId) {
29327
- return refuse2(origin, 401, "token_stale");
29870
+ return refuse3(origin, 401, "token_stale");
29328
29871
  }
29329
29872
  const install = app2.installs.find((entry) => (entry.enabled ?? entry.active) && entry.id === verdict.claims.appInstallId);
29330
29873
  if (install === void 0) {
29331
- return refuse2(origin, 401, "token_stale");
29874
+ return refuse3(origin, 401, "token_stale");
29332
29875
  }
29333
29876
  const method = c.req.method;
29334
29877
  const writing = method !== "GET" && method !== "HEAD";
29335
29878
  if (writing && verdict.claims.access !== "write") {
29336
- return refuse2(origin, 403, "forbidden");
29879
+ return refuse3(origin, 403, "forbidden");
29337
29880
  }
29338
29881
  const writeTicket = writing ? await mintAppUiWriteTicket({
29339
29882
  kid: verdict.kid,
@@ -29361,13 +29904,13 @@ var handleAppUi = /* @__PURE__ */ __name(async (c, slug) => {
29361
29904
  return authorized;
29362
29905
  }
29363
29906
  if (await rateLimited(c.env, `${slug}:${authorized.install.id}:ui`)) {
29364
- return refuse2(origin, 429, "rate_limited");
29907
+ return refuse3(origin, 429, "rate_limited");
29365
29908
  }
29366
29909
  let body;
29367
29910
  if (c.req.method !== "GET" && c.req.method !== "HEAD") {
29368
29911
  const bytes = await readBoundedBody(c.req.raw, MAX_UI_BODY_BYTES);
29369
29912
  if (bytes === null) {
29370
- return refuse2(origin, 413, "too_large");
29913
+ return refuse3(origin, 413, "too_large");
29371
29914
  }
29372
29915
  body = bytes;
29373
29916
  }
@@ -29433,7 +29976,7 @@ var ERRORS = {
29433
29976
  },
29434
29977
  unknown_user: {
29435
29978
  status: 404,
29436
- description: "The `identifier` is one of your own that this site has never seen, and tracking never creates a user from one. Send a canonical `anon_\u2026` id, or bind the identifier first with an identify or alias call."
29979
+ description: "The `identifier` is one of your own that this site has never seen, and this operation never creates a user from one. Send a canonical `anon_\u2026` id, or bind the identifier first with an identify or alias call."
29437
29980
  },
29438
29981
  forbidden: {
29439
29982
  status: 403,
@@ -29718,365 +30261,21 @@ idGraphInspect.post("/internal/id-graph/rows", async (c) => {
29718
30261
  return c.json({ page: await inspectObjectTable(c.env, body) });
29719
30262
  });
29720
30263
 
29721
- // ../../libs/bot-protection/src/countries.ts
29722
- var EU_TOKEN = "EU";
29723
- var COUNTRY_CODES = [
29724
- "AD",
29725
- "AE",
29726
- "AF",
29727
- "AG",
29728
- "AI",
29729
- "AL",
29730
- "AM",
29731
- "AO",
29732
- "AQ",
29733
- "AR",
29734
- "AS",
29735
- "AT",
29736
- "AU",
29737
- "AW",
29738
- "AX",
29739
- "AZ",
29740
- "BA",
29741
- "BB",
29742
- "BD",
29743
- "BE",
29744
- "BF",
29745
- "BG",
29746
- "BH",
29747
- "BI",
29748
- "BJ",
29749
- "BL",
29750
- "BM",
29751
- "BN",
29752
- "BO",
29753
- "BQ",
29754
- "BR",
29755
- "BS",
29756
- "BT",
29757
- "BV",
29758
- "BW",
29759
- "BY",
29760
- "BZ",
29761
- "CA",
29762
- "CC",
29763
- "CD",
29764
- "CF",
29765
- "CG",
29766
- "CH",
29767
- "CI",
29768
- "CK",
29769
- "CL",
29770
- "CM",
29771
- "CN",
29772
- "CO",
29773
- "CR",
29774
- "CU",
29775
- "CV",
29776
- "CW",
29777
- "CX",
29778
- "CY",
29779
- "CZ",
29780
- "DE",
29781
- "DJ",
29782
- "DK",
29783
- "DM",
29784
- "DO",
29785
- "DZ",
29786
- "EC",
29787
- "EE",
29788
- "EG",
29789
- "EH",
29790
- "ER",
29791
- "ES",
29792
- "ET",
29793
- "FI",
29794
- "FJ",
29795
- "FK",
29796
- "FM",
29797
- "FO",
29798
- "FR",
29799
- "GA",
29800
- "GB",
29801
- "GD",
29802
- "GE",
29803
- "GF",
29804
- "GG",
29805
- "GH",
29806
- "GI",
29807
- "GL",
29808
- "GM",
29809
- "GN",
29810
- "GP",
29811
- "GQ",
29812
- "GR",
29813
- "GS",
29814
- "GT",
29815
- "GU",
29816
- "GW",
29817
- "GY",
29818
- "HK",
29819
- "HM",
29820
- "HN",
29821
- "HR",
29822
- "HT",
29823
- "HU",
29824
- "ID",
29825
- "IE",
29826
- "IL",
29827
- "IM",
29828
- "IN",
29829
- "IO",
29830
- "IQ",
29831
- "IR",
29832
- "IS",
29833
- "IT",
29834
- "JE",
29835
- "JM",
29836
- "JO",
29837
- "JP",
29838
- "KE",
29839
- "KG",
29840
- "KH",
29841
- "KI",
29842
- "KM",
29843
- "KN",
29844
- "KP",
29845
- "KR",
29846
- "KW",
29847
- "KY",
29848
- "KZ",
29849
- "LA",
29850
- "LB",
29851
- "LC",
29852
- "LI",
29853
- "LK",
29854
- "LR",
29855
- "LS",
29856
- "LT",
29857
- "LU",
29858
- "LV",
29859
- "LY",
29860
- "MA",
29861
- "MC",
29862
- "MD",
29863
- "ME",
29864
- "MF",
29865
- "MG",
29866
- "MH",
29867
- "MK",
29868
- "ML",
29869
- "MM",
29870
- "MN",
29871
- "MO",
29872
- "MP",
29873
- "MQ",
29874
- "MR",
29875
- "MS",
29876
- "MT",
29877
- "MU",
29878
- "MV",
29879
- "MW",
29880
- "MX",
29881
- "MY",
29882
- "MZ",
29883
- "NA",
29884
- "NC",
29885
- "NE",
29886
- "NF",
29887
- "NG",
29888
- "NI",
29889
- "NL",
29890
- "NO",
29891
- "NP",
29892
- "NR",
29893
- "NU",
29894
- "NZ",
29895
- "OM",
29896
- "PA",
29897
- "PE",
29898
- "PF",
29899
- "PG",
29900
- "PH",
29901
- "PK",
29902
- "PL",
29903
- "PM",
29904
- "PN",
29905
- "PR",
29906
- "PS",
29907
- "PT",
29908
- "PW",
29909
- "PY",
29910
- "QA",
29911
- "RE",
29912
- "RO",
29913
- "RS",
29914
- "RU",
29915
- "RW",
29916
- "SA",
29917
- "SB",
29918
- "SC",
29919
- "SD",
29920
- "SE",
29921
- "SG",
29922
- "SH",
29923
- "SI",
29924
- "SJ",
29925
- "SK",
29926
- "SL",
29927
- "SM",
29928
- "SN",
29929
- "SO",
29930
- "SR",
29931
- "SS",
29932
- "ST",
29933
- "SV",
29934
- "SX",
29935
- "SY",
29936
- "SZ",
29937
- "TC",
29938
- "TD",
29939
- "TF",
29940
- "TG",
29941
- "TH",
29942
- "TJ",
29943
- "TK",
29944
- "TL",
29945
- "TM",
29946
- "TN",
29947
- "TO",
29948
- "TR",
29949
- "TT",
29950
- "TV",
29951
- "TW",
29952
- "TZ",
29953
- "UA",
29954
- "UG",
29955
- "UM",
29956
- "US",
29957
- "UY",
29958
- "UZ",
29959
- "VA",
29960
- "VC",
29961
- "VE",
29962
- "VG",
29963
- "VI",
29964
- "VN",
29965
- "VU",
29966
- "WF",
29967
- "WS",
29968
- "YE",
29969
- "YT",
29970
- "ZA",
29971
- "ZM",
29972
- "ZW"
29973
- ];
29974
- var SELECTABLE_TOKENS = /* @__PURE__ */ new Set([EU_TOKEN, ...COUNTRY_CODES]);
29975
- var isSelectableCountryToken = /* @__PURE__ */ __name((token) => {
29976
- return SELECTABLE_TOKENS.has(token);
29977
- }, "isSelectableCountryToken");
29978
- var matchesRule = /* @__PURE__ */ __name((country, isEU, list) => {
29979
- if (country !== void 0 && list.includes(country)) {
29980
- return true;
29981
- }
29982
- return isEU && list.includes(EU_TOKEN);
29983
- }, "matchesRule");
29984
- var isCountryBlocked = /* @__PURE__ */ __name((country, isEU, rule) => {
29985
- if (rule === null || rule.list.length === 0) {
29986
- return false;
29987
- }
29988
- const matched = matchesRule(country, isEU, rule.list);
29989
- return rule.mode === "block" ? matched : !matched;
29990
- }, "isCountryBlocked");
29991
-
29992
- // ../../libs/bot-protection/src/score.ts
29993
- var BOT_SCORE_MIN = 1;
29994
- var BOT_SCORE_MAX = 99;
29995
-
29996
- // ../../libs/bot-protection/src/config.ts
29997
- var NO_PROTECTION = { minBotScore: null, countries: null };
29998
- var normalizeScore = /* @__PURE__ */ __name((value, problems) => {
29999
- if (value === void 0 || value === null) {
30000
- return null;
30001
- }
30002
- if (typeof value === "number" && Number.isInteger(value) && value >= BOT_SCORE_MIN && value <= BOT_SCORE_MAX) {
30003
- return value;
30004
- }
30005
- problems.push("has an out-of-range minBotScore");
30006
- return null;
30007
- }, "normalizeScore");
30008
- var normalizeCountries = /* @__PURE__ */ __name((value, problems) => {
30009
- if (value === void 0 || value === null) {
30010
- return null;
30011
- }
30012
- const record2 = value;
30013
- if (record2.mode !== "allow" && record2.mode !== "block" || !Array.isArray(record2.list)) {
30014
- problems.push("has an invalid countries rule");
30015
- return null;
30016
- }
30017
- const list = [];
30018
- let dropped = 0;
30019
- for (const token of record2.list) {
30020
- if (typeof token === "string" && isSelectableCountryToken(token)) {
30021
- list.push(token);
30022
- } else {
30023
- dropped += 1;
30024
- }
30025
- }
30026
- if (dropped > 0) {
30027
- problems.push(`dropped ${dropped} invalid country token(s)`);
30028
- }
30029
- return list.length === 0 ? null : { mode: record2.mode, list };
30030
- }, "normalizeCountries");
30031
- var parseBotProtection = /* @__PURE__ */ __name((raw2) => {
30032
- if (raw2.trim() === "") {
30033
- return { config: NO_PROTECTION };
30034
- }
30035
- let value;
30036
- try {
30037
- value = JSON.parse(raw2);
30038
- } catch {
30039
- return { config: NO_PROTECTION, error: "BOT_PROTECTION is not valid JSON" };
30040
- }
30041
- if (typeof value !== "object" || value === null) {
30042
- return { config: NO_PROTECTION, error: "BOT_PROTECTION is not an object" };
30043
- }
30044
- const record2 = value;
30045
- const problems = [];
30046
- const config2 = {
30047
- minBotScore: normalizeScore(record2.minBotScore, problems),
30048
- countries: normalizeCountries(record2.countries, problems)
30049
- };
30050
- return problems.length > 0 ? { config: config2, error: `BOT_PROTECTION ${problems.join("; ")}` } : { config: config2 };
30051
- }, "parseBotProtection");
30052
- var isRequestBlocked = /* @__PURE__ */ __name((config2, signal) => {
30053
- if (config2.minBotScore !== null && signal.botScore !== void 0 && signal.botScore < config2.minBotScore) {
30054
- return true;
30055
- }
30056
- return isCountryBlocked(signal.country, signal.isEU ?? false, config2.countries);
30057
- }, "isRequestBlocked");
30058
-
30059
- // src/helpers/cookie.ts
30060
- var COOKIE_NAME2 = "q_user_id";
30061
- var COOKIE_MAX_AGE = 400 * 24 * 60 * 60;
30062
- var FALLBACK_MIN_LENGTH = 4;
30063
- var fallbackCookie = /* @__PURE__ */ __name((c) => {
30064
- const name = c.env.FALLBACK_COOKIE;
30065
- if (name === "") {
30066
- return void 0;
30067
- }
30068
- const value = getCookie(c, name);
30069
- if (value === void 0 || value.length < FALLBACK_MIN_LENGTH) {
30070
- return void 0;
30071
- }
30072
- return { name, value };
30073
- }, "fallbackCookie");
30074
-
30075
30264
  // src/id-graph/anon/connection.ts
30076
30265
  var CONNECTION_CF_HEADER = "x-connection-cf";
30266
+ var SESSION_HEADER = "x-session";
30077
30267
  var MAX_HOST_USER_AGENT_LENGTH = 512;
30078
30268
  var MAX_HOST_LOCALE_LENGTH = 64;
30079
- var INTERNAL_HEADERS = /* @__PURE__ */ new Set(["x-user-id", "x-new-user", "x-geo", SITE_ID_HEADER, CONNECTION_CF_HEADER, TRACE_HEADER, CF_DATA_HEADER]);
30269
+ var INTERNAL_HEADERS = /* @__PURE__ */ new Set([
30270
+ "x-user-id",
30271
+ "x-new-user",
30272
+ "x-geo",
30273
+ SITE_ID_HEADER,
30274
+ CONNECTION_CF_HEADER,
30275
+ SESSION_HEADER,
30276
+ TRACE_HEADER,
30277
+ CF_DATA_HEADER
30278
+ ]);
30080
30279
  var REDACTED_HEADERS = /* @__PURE__ */ new Set(["authorization"]);
30081
30280
  var encodeConnectionCf = /* @__PURE__ */ __name((cf) => {
30082
30281
  if (cf === void 0) {
@@ -30092,6 +30291,13 @@ var forwardedCf = /* @__PURE__ */ __name((request) => {
30092
30291
  }
30093
30292
  return JSON.parse(raw2);
30094
30293
  }, "forwardedCf");
30294
+ var forwardedSession = /* @__PURE__ */ __name((request) => {
30295
+ const raw2 = request.headers.get(SESSION_HEADER);
30296
+ if (raw2 === null) {
30297
+ return void 0;
30298
+ }
30299
+ return JSON.parse(raw2);
30300
+ }, "forwardedSession");
30095
30301
  var connectAttrs = /* @__PURE__ */ __name((request, geo) => {
30096
30302
  const headers = {};
30097
30303
  for (const [name, value] of request.headers) {
@@ -30128,6 +30334,15 @@ var browserGeoFrom = /* @__PURE__ */ __name((geo) => {
30128
30334
  var attachmentFieldBytes = /* @__PURE__ */ __name((name, value) => {
30129
30335
  return value === void 0 ? 0 : new TextEncoder().encode(`,"${name}":${JSON.stringify(value)}`).length;
30130
30336
  }, "attachmentFieldBytes");
30337
+ var boundedHost = /* @__PURE__ */ __name((request) => {
30338
+ const raw2 = hostFromRequest(request);
30339
+ const bounded = {
30340
+ ip: raw2.ip,
30341
+ userAgent: raw2.userAgent !== void 0 && raw2.userAgent.length <= MAX_HOST_USER_AGENT_LENGTH ? raw2.userAgent : void 0,
30342
+ locale: raw2.locale !== void 0 && raw2.locale.length <= MAX_HOST_LOCALE_LENGTH ? raw2.locale : void 0
30343
+ };
30344
+ return bounded.ip === void 0 && bounded.userAgent === void 0 && bounded.locale === void 0 ? void 0 : bounded;
30345
+ }, "boundedHost");
30131
30346
  var connectionAttachment = /* @__PURE__ */ __name((request, geo, siteId) => {
30132
30347
  const userAgent = request.headers.get("user-agent");
30133
30348
  const attrs = {
@@ -30136,27 +30351,24 @@ var connectionAttachment = /* @__PURE__ */ __name((request, geo, siteId) => {
30136
30351
  ...geo?.city === void 0 ? {} : { "qinto.city": geo.city },
30137
30352
  ...userAgent === null ? {} : { user_agent: userAgent }
30138
30353
  };
30139
- const raw2 = hostFromRequest(request);
30140
- const bounded = {
30141
- ip: raw2.ip,
30142
- userAgent: raw2.userAgent !== void 0 && raw2.userAgent.length <= MAX_HOST_USER_AGENT_LENGTH ? raw2.userAgent : void 0,
30143
- locale: raw2.locale !== void 0 && raw2.locale.length <= MAX_HOST_LOCALE_LENGTH ? raw2.locale : void 0
30144
- };
30145
- const host = bounded.ip === void 0 && bounded.userAgent === void 0 && bounded.locale === void 0 ? void 0 : bounded;
30354
+ const host = boundedHost(request);
30146
30355
  const hostBytes = attachmentFieldBytes("host", host);
30147
30356
  const browserGeo = browserGeoFrom(geo);
30148
30357
  const browserGeoBytes = attachmentFieldBytes("browserGeo", browserGeo);
30358
+ const session2 = forwardedSession(request);
30359
+ const sessionBytes = attachmentFieldBytes("session", session2);
30149
30360
  const siteBytes = attachmentFieldBytes("siteId", siteId);
30150
30361
  const cf = forwardedCf(request);
30151
30362
  const cfSuffix = cf === void 0 ? "" : `,"cf":${JSON.stringify(cf)}`;
30152
30363
  const cfBytes = new TextEncoder().encode(cfSuffix).length;
30153
30364
  const emptyAttrsBytes = new TextEncoder().encode(JSON.stringify({ attrs: {} })).length;
30154
- const fixedBytes = siteBytes + hostBytes + browserGeoBytes;
30365
+ const fixedBytes = siteBytes + sessionBytes + hostBytes + browserGeoBytes;
30155
30366
  const includedCf = cfBytes <= MAX_ATTACHMENT_BYTES - emptyAttrsBytes - fixedBytes ? cf : void 0;
30156
30367
  const budgeted = budgetAttrs(attrs, MAX_ATTACHMENT_BYTES - fixedBytes - (includedCf === void 0 ? 0 : cfBytes));
30157
30368
  return {
30158
30369
  ...budgeted,
30159
30370
  siteId,
30371
+ ...session2 === void 0 ? {} : { session: session2 },
30160
30372
  ...browserGeo === void 0 ? {} : { browserGeo },
30161
30373
  ...includedCf === void 0 ? {} : { cf: includedCf },
30162
30374
  ...host === void 0 ? {} : { host }
@@ -30164,14 +30376,18 @@ var connectionAttachment = /* @__PURE__ */ __name((request, geo, siteId) => {
30164
30376
  }, "connectionAttachment");
30165
30377
 
30166
30378
  // src/paths/open.ts
30167
- var withSetCookie = /* @__PURE__ */ __name((res, cookie) => {
30379
+ var withSetCookie = /* @__PURE__ */ __name((res, cookies) => {
30168
30380
  if (res.webSocket) {
30169
30381
  const headers = new Headers(res.headers);
30170
- headers.append("set-cookie", cookie);
30382
+ for (const cookie of cookies) {
30383
+ headers.append("set-cookie", cookie);
30384
+ }
30171
30385
  return new Response(null, { status: res.status, webSocket: res.webSocket, headers });
30172
30386
  }
30173
30387
  const out = new Response(res.body, res);
30174
- out.headers.append("set-cookie", cookie);
30388
+ for (const cookie of cookies) {
30389
+ out.headers.append("set-cookie", cookie);
30390
+ }
30175
30391
  return out;
30176
30392
  }, "withSetCookie");
30177
30393
  var open2 = /* @__PURE__ */ __name(async (c) => {
@@ -30186,35 +30402,27 @@ var open2 = /* @__PURE__ */ __name(async (c) => {
30186
30402
  });
30187
30403
  return enriched;
30188
30404
  });
30189
- const blocked = await traced("bot.check", {}, () => {
30190
- const { config: protection, error: error61 } = parseBotProtection(c.env.BOT_PROTECTION);
30191
- if (error61 !== void 0) {
30192
- addSpanEvent("BOT_PROTECTION is misconfigured; enforcing it with the invalid parts dropped", {
30193
- level: "error",
30194
- attrs: { error: String(error61) }
30195
- });
30196
- }
30197
- const rejected = isRequestBlocked(protection, { botScore: geo.botScore, country: geo.country, isEU: geo.isEUCountry === "yes" });
30198
- setSpanAttributes({ "bot.blocked": rejected, ...geo.botScore === void 0 ? {} : { "bot.score": geo.botScore } });
30199
- return rejected;
30200
- });
30201
- if (blocked) {
30405
+ if (await botRejected(c.env, geo)) {
30202
30406
  return c.text("traffic rejected", 403);
30203
30407
  }
30204
30408
  const existing = getCookie(c, COOKIE_NAME2);
30205
30409
  let isNewUser = false;
30206
30410
  let userId;
30207
- if (!existing || !isCanonicalId(existing)) {
30208
- userId = await restoreUser(c.env, fallbackCookie(c));
30209
- isNewUser = true;
30210
- } else {
30411
+ if (existing !== void 0 && isCanonicalId(existing)) {
30211
30412
  userId = existing;
30413
+ } else {
30414
+ const adopted = edgeTagUserId(c);
30415
+ setSpanAttributes({ "qinto.edgetag_adopted": adopted !== void 0 });
30416
+ userId = await restoreUser(c.env, fallbackCookie(c), adopted ?? generateUserId());
30417
+ isNewUser = adopted === void 0;
30212
30418
  }
30213
30419
  const headers = new Headers(c.req.raw.headers);
30214
30420
  headers.set("x-user-id", userId);
30215
30421
  headers.set("x-new-user", isNewUser.toString());
30216
30422
  headers.set(SITE_ID_HEADER, ownSiteId(c.env));
30217
30423
  headers.set("x-geo", headerJson(geo));
30424
+ const session2 = visitSession(c, Date.now());
30425
+ headers.set(SESSION_HEADER, JSON.stringify(session2));
30218
30426
  headers.delete(CONNECTION_CF_HEADER);
30219
30427
  const cf = encodeConnectionCf(c.req.raw.cf);
30220
30428
  if (cf !== void 0) {
@@ -30231,28 +30439,36 @@ var open2 = /* @__PURE__ */ __name(async (c) => {
30231
30439
  const root = c.env.ROOT_DOMAIN;
30232
30440
  const hostname3 = new URL(c.req.url).hostname;
30233
30441
  const domain2 = root !== void 0 && root !== "" && (hostname3 === root || hostname3.endsWith(`.${root}`)) ? root : void 0;
30234
- const cookie = generateCookie(COOKIE_NAME2, userId, {
30442
+ const attributes = {
30235
30443
  httpOnly: true,
30236
30444
  // `wrangler types` narrows COOKIE_SAME_SITE to its dev default; deploys also set none.
30237
30445
  sameSite: c.env.COOKIE_SAME_SITE,
30238
30446
  secure: true,
30239
30447
  path: "/",
30240
- maxAge: COOKIE_MAX_AGE,
30241
- ...domain2 === void 0 ? {} : { domain: domain2 }
30242
- });
30243
- return withSetCookie(res, cookie);
30448
+ maxAge: COOKIE_MAX_AGE
30449
+ };
30450
+ const cookies = [
30451
+ generateCookie(COOKIE_NAME2, userId, { ...attributes, ...domain2 === void 0 ? {} : { domain: domain2 } }),
30452
+ // Re-issued on every connect, which is what makes the session expire after 30 idle minutes.
30453
+ generateCookie(SESSION_COOKIE_NAME, `${session2.id}.${session2.startedAt}`, { ...attributes, maxAge: SESSION_MAX_AGE })
30454
+ ];
30455
+ const signature = await signVisitor(c.env, userId);
30456
+ if (signature !== void 0) {
30457
+ cookies.push(generateCookie(VISITOR_SIGNATURE_COOKIE, signature, attributes));
30458
+ }
30459
+ return withSetCookie(res, cookies);
30244
30460
  }, "open");
30245
30461
 
30246
30462
  // browser-artifact/meta.json
30247
30463
  var meta_default = {
30248
- version: "0.1.6",
30249
- gitSha: "c1c5efdcdd94c4d8871c27924e1941be3444effe",
30250
- builtAt: "2026-09-08T14:30:58.153Z",
30251
- hash: "2a40c5a8fc3a17ea4eaeb819842f609694ccaddd1fc563c95463cc92bd6690df"
30464
+ version: "0.1.10",
30465
+ gitSha: "f21182929c88fa0748f575164bde781b31f4fbdd",
30466
+ builtAt: "2026-09-23T15:34:39.048Z",
30467
+ hash: "d1804e7914363626fed0280696cad5e06728b899e23c8a5ee6dcced3b67cf831"
30252
30468
  };
30253
30469
 
30254
30470
  // src/paths/q-js.ts
30255
- import qScript from "./b6b625dbf1615da405354d9b8813ffcc4e753b93-q.js.txt";
30471
+ import qScript from "./7bc13565a6fa9d520d0ebed788d11591e8db5b44-q.js.txt";
30256
30472
 
30257
30473
  // src/apps-q.ts
30258
30474
  import appsQScript from "./apps-q.json";
@@ -30300,7 +30516,7 @@ var appsMetadata = /* @__PURE__ */ __name(() => {
30300
30516
  const { registry: registry3 } = siteRegistry();
30301
30517
  return Object.fromEntries(
30302
30518
  Object.entries(registry3.apps).map(([slug, app2]) => {
30303
- const capture = declaredCapture(app2);
30519
+ const capture2 = declaredCapture(app2);
30304
30520
  const geo = app2.data.includes("geo") ? { geo: true } : {};
30305
30521
  const sharedContext = app2.data.includes(SHARED_CONTEXT_CAPABILITY) ? { sharedContext: true } : {};
30306
30522
  return [
@@ -30308,7 +30524,7 @@ var appsMetadata = /* @__PURE__ */ __name(() => {
30308
30524
  {
30309
30525
  category: app2.consent.category,
30310
30526
  eventResults: app2.eventResults,
30311
- ...capture === void 0 ? {} : { capture },
30527
+ ...capture2 === void 0 ? {} : { capture: capture2 },
30312
30528
  ...geo,
30313
30529
  ...sharedContext
30314
30530
  }
@@ -30387,6 +30603,60 @@ var alias = createApiRouter().openapi(definition2, async (c) => {
30387
30603
  return respond(c, acceptedResponse, { ok: true, id });
30388
30604
  });
30389
30605
 
30606
+ // src/paths/v1/capture.ts
30607
+ var captureBody = external_exports.object({
30608
+ identifier: identifierField2,
30609
+ apps: captureAppsField
30610
+ }).strict().openapi("CaptureRequest", { description: "Click ids and vendor ids to record for the user, keyed by app." });
30611
+ var captureResponse = external_exports.object({
30612
+ ok: external_exports.literal(true).openapi({
30613
+ description: "Always true: the values were accepted, not that every one of them was stored.",
30614
+ example: true
30615
+ }),
30616
+ accepted: external_exports.record(external_exports.string(), external_exports.array(external_exports.string())).openapi({
30617
+ description: "The capture names now stored, per app slug. A name missing here was dropped.",
30618
+ example: { facebook: ["fbclid"] }
30619
+ })
30620
+ }).openapi("CaptureAccepted", { description: "The capture names that are now stored." });
30621
+ var definition3 = createRoute({
30622
+ method: "post",
30623
+ path: "/v1/capture",
30624
+ operationId: "captureValues",
30625
+ tags: ["Identity"],
30626
+ security: [{ QintoWorkerAccessToken: [] }],
30627
+ middleware: [requireScope(SCOPE.capture)],
30628
+ summary: "Record click ids",
30629
+ description: operationDescription({
30630
+ intro: "Records click ids and vendor ids for the user, such as `fbclid` or `gclid`, keyed by the app that reads them.",
30631
+ howItWorks: "A value is stored only when the named app is installed on this site, declares that name, and holds the user\u2019s consent for its category \u2014 `accepted` reports exactly what landed, so compare it against what you sent. Re-sending an unchanged value is a no-op and keeps the original click time, which is what any expiry window runs from. Capturing never creates a user: an identifier of your own that this site has not seen fails with `unknown_user`."
30632
+ }),
30633
+ request: {
30634
+ body: {
30635
+ required: true,
30636
+ description: "The values to record.",
30637
+ content: { "application/json": { schema: captureBody } }
30638
+ }
30639
+ },
30640
+ responses: {
30641
+ 200: {
30642
+ description: "The values were accepted.",
30643
+ content: { "application/json": { schema: captureResponse } }
30644
+ },
30645
+ ...errorResponses("invalid_body", "unauthenticated", "forbidden", "unknown_user", "internal")
30646
+ }
30647
+ });
30648
+ var capture = createApiRouter().openapi(definition3, async (c) => {
30649
+ const { identifier, apps } = c.req.valid("json");
30650
+ const resolved = await resolveExistingUser(c.env, identifier);
30651
+ if (resolved === null) {
30652
+ return c.json({ error: "unknown_user" }, 404);
30653
+ }
30654
+ const { accepted } = await traced("rpc anon.server_capture", {}, () => {
30655
+ return anonStub(c.env, resolved.id).serverCapture({ anonId: resolved.id, apps }, ownSite(c.env), currentTraceContext());
30656
+ });
30657
+ return respond(c, captureResponse, { ok: true, accepted });
30658
+ });
30659
+
30390
30660
  // src/paths/v1/consent.ts
30391
30661
  var consentBody = external_exports.object({
30392
30662
  identifier: identifierField2,
@@ -30399,7 +30669,7 @@ var consentBody = external_exports.object({
30399
30669
  }),
30400
30670
  context: serverActionContextField
30401
30671
  }).strict().openapi("ConsentRequest", { description: "Consent choices to record for the user." });
30402
- var definition3 = createRoute({
30672
+ var definition4 = createRoute({
30403
30673
  method: "post",
30404
30674
  path: "/v1/consent",
30405
30675
  operationId: "saveUserConsent",
@@ -30426,7 +30696,7 @@ var definition3 = createRoute({
30426
30696
  ...errorResponses("invalid_body", "unauthenticated", "forbidden", "internal")
30427
30697
  }
30428
30698
  });
30429
- var consent = createApiRouter().openapi(definition3, async (c) => {
30699
+ var consent = createApiRouter().openapi(definition4, async (c) => {
30430
30700
  const { identifier, categories, apps, context } = c.req.valid("json");
30431
30701
  const id = await submitAction(
30432
30702
  c.env,
@@ -30460,7 +30730,7 @@ var consentStateResponse = external_exports.object({
30460
30730
  }).openapi("ConsentState", {
30461
30731
  description: "The consent state currently stored for the user; categories never saved read as not granted."
30462
30732
  });
30463
- var definition4 = createRoute({
30733
+ var definition5 = createRoute({
30464
30734
  method: "get",
30465
30735
  path: "/v1/consent",
30466
30736
  operationId: "getUserConsent",
@@ -30481,7 +30751,7 @@ var definition4 = createRoute({
30481
30751
  ...errorResponses("invalid_params", "unauthenticated", "forbidden", "internal")
30482
30752
  }
30483
30753
  });
30484
- var consentState = createApiRouter().openapi(definition4, async (c) => {
30754
+ var consentState = createApiRouter().openapi(definition5, async (c) => {
30485
30755
  const identifier = c.req.valid("query").identifier ?? getCookie(c, COOKIE_NAME2);
30486
30756
  if (identifier === void 0 || !isCanonicalId(identifier)) {
30487
30757
  return c.json({ error: "invalid_params" }, 400);
@@ -30501,7 +30771,7 @@ var identifyBody = external_exports.object({
30501
30771
  }),
30502
30772
  context: serverActionContextField
30503
30773
  }).openapi("IdentifyRequest", { description: "Identity traits to record for the user." });
30504
- var definition5 = createRoute({
30774
+ var definition6 = createRoute({
30505
30775
  method: "post",
30506
30776
  path: "/v1/identify",
30507
30777
  operationId: "identifyUser",
@@ -30528,7 +30798,7 @@ var definition5 = createRoute({
30528
30798
  ...errorResponses("invalid_body", "unauthenticated", "forbidden", "internal")
30529
30799
  }
30530
30800
  });
30531
- var identify = createApiRouter().openapi(definition5, async (c) => {
30801
+ var identify = createApiRouter().openapi(definition6, async (c) => {
30532
30802
  const { identifier, traits, context } = c.req.valid("json");
30533
30803
  const id = await submitAction(c.env, identifier, { type: "identify", payload: { traits: traits ?? {} }, context }, c.req.raw);
30534
30804
  return respond(c, acceptedResponse, { ok: true, id });
@@ -30538,7 +30808,7 @@ var identify = createApiRouter().openapi(definition5, async (c) => {
30538
30808
  var personQuery = external_exports.object({
30539
30809
  identifier: identifierField2.optional().openapi({
30540
30810
  param: { name: "identifier", in: "query" },
30541
- description: "User to read: any identifier from your own systems (email, phone, custom id), or a canonical Qinto id (`anon_\u2026`). Omit it to read the user from the first-party cookie instead. Internal `gid_\u2026` ids are rejected.",
30811
+ description: "User to read: any identifier from your own systems (email, phone, custom id), or a canonical Qinto id (`anon_\u2026`, or a `tag_user_id` adopted from EdgeTag). Omit it to read the user from the first-party cookie instead. Internal `gid_\u2026` ids are rejected.",
30542
30812
  example: "ada@example.com"
30543
30813
  }),
30544
30814
  cursor: external_exports.string().regex(/^\d+$/).optional().openapi({
@@ -30561,7 +30831,7 @@ var personResponse = external_exports.object({
30561
30831
  example: null
30562
30832
  })
30563
30833
  }).openapi("Person", { description: "The person a user belongs to, and the sessions linked to that person." });
30564
- var definition6 = createRoute({
30834
+ var definition7 = createRoute({
30565
30835
  method: "get",
30566
30836
  path: "/v1/person",
30567
30837
  operationId: "getPerson",
@@ -30582,7 +30852,7 @@ var definition6 = createRoute({
30582
30852
  ...errorResponses("invalid_params", "unauthenticated", "forbidden", "internal")
30583
30853
  }
30584
30854
  });
30585
- var person = createApiRouter().openapi(definition6, async (c) => {
30855
+ var person = createApiRouter().openapi(definition7, async (c) => {
30586
30856
  const { identifier, cursor } = c.req.valid("query");
30587
30857
  const id = identifier ?? getCookie(c, COOKIE_NAME2);
30588
30858
  if (id === void 0) {
@@ -30600,7 +30870,7 @@ var propertiesQuery = external_exports.object({
30600
30870
  })
30601
30871
  });
30602
30872
  var propertiesResponse = external_exports.record(external_exports.string(), external_exports.unknown()).openapi("UserProperties", { description: "Every property currently stored for the user, keyed by property name." });
30603
- var definition7 = createRoute({
30873
+ var definition8 = createRoute({
30604
30874
  method: "get",
30605
30875
  path: "/v1/properties",
30606
30876
  operationId: "getUserProperties",
@@ -30621,7 +30891,7 @@ var definition7 = createRoute({
30621
30891
  ...errorResponses("invalid_params", "unauthenticated", "forbidden", "internal")
30622
30892
  }
30623
30893
  });
30624
- var properties = createApiRouter().openapi(definition7, async (c) => {
30894
+ var properties = createApiRouter().openapi(definition8, async (c) => {
30625
30895
  const identifier = c.req.valid("query").identifier ?? getCookie(c, COOKIE_NAME2);
30626
30896
  if (identifier === void 0 || !isCanonicalId(identifier)) {
30627
30897
  return c.json({ error: "invalid_params" }, 400);
@@ -30650,7 +30920,7 @@ var sessionResponse = external_exports.object({
30650
30920
  example: "gid_01JZWG7Q4S1T8M6Y2E9RBD3KXN"
30651
30921
  })
30652
30922
  }).openapi("Session", { description: "The anonymous session id and the person id it is linked to." });
30653
- var definition8 = createRoute({
30923
+ var definition9 = createRoute({
30654
30924
  method: "get",
30655
30925
  path: "/v1/session",
30656
30926
  operationId: "getSession",
@@ -30671,7 +30941,7 @@ var definition8 = createRoute({
30671
30941
  ...errorResponses("invalid_params", "unauthenticated", "forbidden", "internal")
30672
30942
  }
30673
30943
  });
30674
- var session = createApiRouter().openapi(definition8, async (c) => {
30944
+ var session = createApiRouter().openapi(definition9, async (c) => {
30675
30945
  const identifier = c.req.valid("query").identifier ?? getCookie(c, COOKIE_NAME2);
30676
30946
  if (identifier === void 0) {
30677
30947
  return c.json({ error: "invalid_params" }, 400);
@@ -30696,7 +30966,7 @@ var trackBody = external_exports.object({
30696
30966
  }),
30697
30967
  context: serverActionContextField
30698
30968
  }).strict().openapi("TrackRequest", { description: "An event the user performed, with optional free-form properties." });
30699
- var definition9 = createRoute({
30969
+ var definition10 = createRoute({
30700
30970
  method: "post",
30701
30971
  path: "/v1/track",
30702
30972
  operationId: "trackEvent",
@@ -30723,7 +30993,7 @@ var definition9 = createRoute({
30723
30993
  ...errorResponses("invalid_body", "unauthenticated", "forbidden", "unknown_user", "internal")
30724
30994
  }
30725
30995
  });
30726
- var track = createApiRouter().openapi(definition9, async (c) => {
30996
+ var track = createApiRouter().openapi(definition10, async (c) => {
30727
30997
  const { identifier, event, eventId, properties: properties2, context } = c.req.valid("json");
30728
30998
  try {
30729
30999
  const id = await submitAction(
@@ -30741,6 +31011,12 @@ var track = createApiRouter().openapi(definition9, async (c) => {
30741
31011
  }
30742
31012
  });
30743
31013
 
31014
+ // src/paths/welcome.ts
31015
+ var WELCOME = "Welcome to Qinto by Blotout. This site is set up.\nFor more information, go to https://docs.blotout.io\n";
31016
+ var welcome = /* @__PURE__ */ __name((c) => {
31017
+ return c.text(WELCOME);
31018
+ }, "welcome");
31019
+
30744
31020
  // src/apps/fan-out-entrypoint.ts
30745
31021
  import { WorkerEntrypoint as WorkerEntrypoint2 } from "cloudflare:workers";
30746
31022
 
@@ -31603,9 +31879,11 @@ var nextRetentionDeadline = /* @__PURE__ */ __name((sql) => {
31603
31879
  var META_KEY = "meta";
31604
31880
  var LAST_ACTIVITY_KEY = "lastActivityAt";
31605
31881
  var seedMeta = /* @__PURE__ */ __name((kv, anonId, createdAt) => {
31606
- if (kv.get(META_KEY) === void 0) {
31607
- kv.put(META_KEY, { anonId, linkedGlobalId: null, linkedAt: null, createdAt });
31882
+ if (kv.get(META_KEY) !== void 0) {
31883
+ return false;
31608
31884
  }
31885
+ kv.put(META_KEY, { anonId, linkedGlobalId: null, linkedAt: null, createdAt });
31886
+ return true;
31609
31887
  }, "seedMeta");
31610
31888
  var touchActivity = /* @__PURE__ */ __name((kv, receivedAt) => {
31611
31889
  const last = kv.get(LAST_ACTIVITY_KEY);
@@ -31622,6 +31900,9 @@ var linkedGlobalId = /* @__PURE__ */ __name((kv) => {
31622
31900
  var storedAnonId = /* @__PURE__ */ __name((kv) => {
31623
31901
  return kv.get(META_KEY)?.anonId ?? null;
31624
31902
  }, "storedAnonId");
31903
+ var storedMeta = /* @__PURE__ */ __name((kv) => {
31904
+ return kv.get(META_KEY);
31905
+ }, "storedMeta");
31625
31906
  var SYNC_DEBT_KEY = "syncDebt";
31626
31907
  var syncDebt = /* @__PURE__ */ __name((kv) => {
31627
31908
  const stored = kv.get(SYNC_DEBT_KEY);
@@ -31927,6 +32208,9 @@ var updateGeo = /* @__PURE__ */ __name((storage, geo, observedAt) => {
31927
32208
  }
31928
32209
  return appended;
31929
32210
  }, "updateGeo");
32211
+ var hasGeoRecord = /* @__PURE__ */ __name((kv) => {
32212
+ return kv.get(GEO_KEY) !== void 0;
32213
+ }, "hasGeoRecord");
31930
32214
  var geoSnapshot = /* @__PURE__ */ __name((kv) => {
31931
32215
  const record2 = kv.get(GEO_KEY);
31932
32216
  if (record2 === void 0) {
@@ -32200,8 +32484,7 @@ var MAX_BROWSER_RESULTS_BYTES = 32 * 1024;
32200
32484
  var MAX_FRAME_BYTES = 64 * 1024;
32201
32485
  var browserContextFrame = external_exports.object({
32202
32486
  occurredAt: external_exports.number().int().nonnegative().safe(),
32203
- page: external_exports.object({ url: external_exports.string().min(1).max(4096), referrer: external_exports.string().min(1).max(4096).optional() }).strict(),
32204
- session: external_exports.object({ id: external_exports.string().min(1).max(64), isNew: external_exports.boolean(), startedAt: external_exports.number().int().nonnegative().safe() }).strict()
32487
+ page: external_exports.object({ url: external_exports.string().min(1).max(4096), referrer: external_exports.string().min(1).max(4096).optional() }).strict()
32205
32488
  }).strict();
32206
32489
  var browserResult = external_exports.unknown().refine((value) => {
32207
32490
  return new TextEncoder().encode(JSON.stringify(value)).length <= MAX_BROWSER_RESULT_BYTES;
@@ -32241,9 +32524,7 @@ var consentFrame = external_exports.object({
32241
32524
  var captureFrame = external_exports.object({
32242
32525
  t: external_exports.literal("capture"),
32243
32526
  ref: external_exports.string().min(1).optional(),
32244
- apps: external_exports.record(external_exports.string().max(30), external_exports.record(external_exports.string().max(64), external_exports.string().max(512))).refine((apps) => {
32245
- return Object.keys(apps).length <= 50 && Object.values(apps).every((values) => Object.keys(values).length <= 32);
32246
- }, "capture carries more apps or keys than a site can have")
32527
+ apps: captureAppsField
32247
32528
  }).strict();
32248
32529
  var getConsentFrame = external_exports.object({
32249
32530
  t: external_exports.literal("getConsent"),
@@ -32303,7 +32584,7 @@ var frameSpanAttrs = /* @__PURE__ */ __name((frame, userId) => {
32303
32584
  ...userId === null ? {} : { "action.user_id": userId }
32304
32585
  };
32305
32586
  }, "frameSpanAttrs");
32306
- var browserActionContext = /* @__PURE__ */ __name((frame, host) => {
32587
+ var browserActionContext = /* @__PURE__ */ __name((frame, host, session2) => {
32307
32588
  const context = {
32308
32589
  source: "browser",
32309
32590
  occurredAt: frame.occurredAt,
@@ -32316,11 +32597,12 @@ var browserActionContext = /* @__PURE__ */ __name((frame, host) => {
32316
32597
  provenance: "edge"
32317
32598
  }
32318
32599
  },
32319
- session: frame.session
32600
+ ...session2 === void 0 ? {} : { session: session2 }
32320
32601
  };
32321
32602
  return context;
32322
32603
  }, "browserActionContext");
32323
- var actionFromFrame = /* @__PURE__ */ __name((frame, userId, geo, host) => {
32604
+ var actionFromFrame = /* @__PURE__ */ __name((frame, userId, connection = {}) => {
32605
+ const { cf: geo, host, session: session2 } = connection;
32324
32606
  switch (frame.t) {
32325
32607
  case "identify": {
32326
32608
  const identifier = frame.id === void 0 ? void 0 : classifyIdentifier(frame.id);
@@ -32330,7 +32612,7 @@ var actionFromFrame = /* @__PURE__ */ __name((frame, userId, geo, host) => {
32330
32612
  identifier,
32331
32613
  source: "ws",
32332
32614
  payload: { traits: frame.traits ?? {} },
32333
- context: browserActionContext(frame.context, host),
32615
+ context: browserActionContext(frame.context, host, session2),
32334
32616
  browserResults: frame.appResults,
32335
32617
  geo,
32336
32618
  host
@@ -32342,7 +32624,7 @@ var actionFromFrame = /* @__PURE__ */ __name((frame, userId, geo, host) => {
32342
32624
  userId,
32343
32625
  source: "ws",
32344
32626
  payload: { event: frame.event, eventId: frame.eventId, properties: frame.properties ?? {} },
32345
- context: browserActionContext(frame.context, host),
32627
+ context: browserActionContext(frame.context, host, session2),
32346
32628
  browserResults: frame.appResults,
32347
32629
  geo,
32348
32630
  host
@@ -32357,7 +32639,7 @@ var actionFromFrame = /* @__PURE__ */ __name((frame, userId, geo, host) => {
32357
32639
  userId,
32358
32640
  source: "ws",
32359
32641
  payload: { categories: frame.categories ?? {}, apps: frame.apps ?? {} },
32360
- context: browserActionContext(frame.context, host),
32642
+ context: browserActionContext(frame.context, host, session2),
32361
32643
  geo,
32362
32644
  host
32363
32645
  });
@@ -32426,8 +32708,8 @@ var AnonDO = class extends DurableObject2 {
32426
32708
  this.cleanupAlarm = await seedCleanupAlarm(this.ctx.storage, now, lastActivityAt(this.kv) ?? now, this.cleanupAlarm);
32427
32709
  seedMeta(this.kv, userId, now);
32428
32710
  touchActivity(this.kv, now);
32429
- if (geo !== void 0) {
32430
- this.pushGeoOnConnect(userId, geo, now);
32711
+ if (geo !== void 0 && !hasGeoRecord(this.kv)) {
32712
+ this.seedGeoOnConnect(userId, geo, now);
32431
32713
  }
32432
32714
  const { 0: client, 1: server } = new WebSocketPair();
32433
32715
  this.ctx.acceptWebSocket(server, [siteId]);
@@ -32543,7 +32825,7 @@ var AnonDO = class extends DurableObject2 {
32543
32825
  if (frame === null) {
32544
32826
  return;
32545
32827
  }
32546
- const action = actionFromFrame(frame, userId, attachment.cf, attachment.host);
32828
+ const action = actionFromFrame(frame, userId, this.spendSession(ws, frame));
32547
32829
  const ref = parsed.ref ?? action.actionId;
32548
32830
  setSpanAttributes({ "action.id": action.actionId });
32549
32831
  try {
@@ -32556,6 +32838,15 @@ var AnonDO = class extends DurableObject2 {
32556
32838
  this.respond(ws, { t: "ack", ref, anonId: userId });
32557
32839
  });
32558
32840
  }
32841
+ // Re-read, not taken from the message's start: across its awaits a pipelined frame may already have
32842
+ // spent the session's first action.
32843
+ spendSession(ws, frame) {
32844
+ const current = ws.deserializeAttachment();
32845
+ if (current.session?.isNew === true && frame.t !== "alias") {
32846
+ ws.serializeAttachment({ ...current, session: { ...current.session, isNew: false } });
32847
+ }
32848
+ return current;
32849
+ }
32559
32850
  respond(ws, frame) {
32560
32851
  sendFrame(ws, JSON.stringify(frame));
32561
32852
  }
@@ -32596,9 +32887,21 @@ var AnonDO = class extends DurableObject2 {
32596
32887
  addSpanEvent("capture dropped: no visitor state", { level: "warn" });
32597
32888
  return { accepted: {} };
32598
32889
  }
32599
- const context = await this.sites.resolve(site, Date.now());
32600
- const writes = acceptedCaptures(context.registry, consentState2(this.kv), input2.apps, context.consentEnforced);
32601
- return { accepted: this.storeCaptures(writes) };
32890
+ return this.runCaptures(input2.apps, site);
32891
+ });
32892
+ });
32893
+ }
32894
+ // The server door. There is no connect to have seeded the visitor, and a migrated visitor can
32895
+ // carry click ids before any identify, so unlike the socket this one creates the profile.
32896
+ serverCapture(input2, site, trace) {
32897
+ return runInvocation({ name: "do.anon.server_capture", trace }, () => {
32898
+ return this.enqueue(async () => {
32899
+ this.revive();
32900
+ const now = Date.now();
32901
+ this.cleanupAlarm = await seedCleanupAlarm(this.ctx.storage, now, lastActivityAt(this.kv) ?? now, this.cleanupAlarm);
32902
+ seedMeta(this.kv, input2.anonId, now);
32903
+ touchActivity(this.kv, now);
32904
+ return this.runCaptures(input2.apps, site);
32602
32905
  });
32603
32906
  });
32604
32907
  }
@@ -32619,6 +32922,11 @@ var AnonDO = class extends DurableObject2 {
32619
32922
  });
32620
32923
  });
32621
32924
  }
32925
+ async runCaptures(apps, site) {
32926
+ const context = await this.sites.resolve(site, Date.now());
32927
+ const writes = acceptedCaptures(context.registry, consentState2(this.kv), apps, context.consentEnforced);
32928
+ return { accepted: this.storeCaptures(writes) };
32929
+ }
32622
32930
  storeCaptures(writes) {
32623
32931
  if (writes.length === 0) {
32624
32932
  return {};
@@ -32906,9 +33214,11 @@ var AnonDO = class extends DurableObject2 {
32906
33214
  return { action: outcome.action, releaseEmissions };
32907
33215
  }
32908
33216
  appsSnapshot(anonId, context) {
33217
+ const meta3 = storedMeta(this.kv);
32909
33218
  return {
32910
33219
  anonId,
32911
- globalId: linkedGlobalId(this.kv),
33220
+ globalId: meta3?.linkedGlobalId ?? null,
33221
+ createdAt: meta3?.createdAt ?? null,
32912
33222
  pii: userProperties(this.kv),
32913
33223
  // A wipe dropped the sql tables, so reading them would throw — and reviving for a transform
32914
33224
  // snapshot would strand recreated storage when the chain fails or drops.
@@ -33270,24 +33580,13 @@ var AnonDO = class extends DurableObject2 {
33270
33580
  await this.ctx.storage.setAlarm(target);
33271
33581
  this.cleanupAlarm = target;
33272
33582
  }
33273
- pushGeoOnConnect(anonId, geo, observedAt) {
33583
+ seedGeoOnConnect(anonId, geo, observedAt) {
33274
33584
  const run = this.enqueue(async () => {
33275
33585
  this.revive();
33276
- const latest = updateGeo(this.cachedStorage, geo, observedAt);
33277
- if (latest.length === 0) {
33278
- return;
33279
- }
33280
- const globalId = linkedGlobalId(this.kv);
33281
- if (globalId === null) {
33282
- await this.retargetCleanupAlarm(Date.now());
33283
- return;
33284
- }
33285
- recordSyncDebt(this.kv, "ride");
33586
+ updateGeo(this.cachedStorage, geo, observedAt);
33286
33587
  await this.retargetCleanupAlarm(Date.now());
33287
- const snapshot = await this.sync({ globalId, anonId, syncedAt: observedAt, identifiers: [], latest });
33288
- await this.applyReturned(snapshot, globalId);
33289
33588
  });
33290
- this.ctx.waitUntil(run.catch((error61) => console.warn("geo push failed", { anonId, error: error61 })));
33589
+ this.ctx.waitUntil(run.catch((error61) => console.warn("geo seed failed", { anonId, error: error61 })));
33291
33590
  }
33292
33591
  };
33293
33592
 
@@ -33605,6 +33904,7 @@ var GlobalDO = class extends DurableObject3 {
33605
33904
  var app = createApiRouter();
33606
33905
  app.use(requestSpan);
33607
33906
  app.use(siteStatusGuard);
33907
+ app.get("/", welcome);
33608
33908
  app.get("/open", open2);
33609
33909
  app.use("/q.js", cors());
33610
33910
  app.get("/q.js", qJs);
@@ -33616,6 +33916,7 @@ app.use("/v1/*", allowCors);
33616
33916
  app.use("/v1/*", bodyLimit({ maxSize: MAX_V1_BODY_BYTES, onError: /* @__PURE__ */ __name((c) => c.json({ error: "too_large" }, 413), "onError") }));
33617
33917
  app.route("/", alias);
33618
33918
  app.route("/", identify);
33919
+ app.route("/", capture);
33619
33920
  app.route("/", track);
33620
33921
  app.route("/", properties);
33621
33922
  app.route("/", consent);