@blotout/qinto-cli 0.1.30 → 0.1.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21071,6 +21071,7 @@ var endpointSchema = external_exports.object({
21071
21071
  var eventsSchema = external_exports.object({
21072
21072
  publishes: external_exports.array(external_exports.string().min(1).max(MAX_EVENT_TOPIC_LENGTH).regex(EVENT_TOPIC)).max(MAX_PUBLISHED_TOPICS).default([])
21073
21073
  }).strict();
21074
+ var dependenciesSchema = external_exports.array(external_exports.object({ app: external_exports.string().regex(APP_SLUG_PATTERN), description: renderableText(1, 200).optional() }).strict()).max(10);
21074
21075
  var rpcSchema = external_exports.object({
21075
21076
  exposes: external_exports.array(external_exports.object({ name: external_exports.string().regex(FUNCTION_NAME).max(64), visibility: external_exports.enum(["public", "private"]) }).strict()).max(32).optional(),
21076
21077
  calls: external_exports.array(external_exports.object({ app: external_exports.string().regex(APP_SLUG_PATTERN), fn: external_exports.string().regex(FUNCTION_NAME).max(64) }).strict()).max(64).optional()
@@ -21114,6 +21115,9 @@ var baseSchema = external_exports.object({
21114
21115
  description: "The app's browser surface: an optional entry for the bundle that ships inside the site's q.js, and the query params and cookies it declares for capture."
21115
21116
  }),
21116
21117
  resources: external_exports.array(resourceSchema).max(32).default([]).meta({ description: "The Cloudflare bindings the app worker is given." }),
21118
+ dependencies: dependenciesSchema.optional().meta({
21119
+ description: "The apps this app needs on the same site. Every app it calls over RPC must be listed."
21120
+ }),
21117
21121
  rpc: rpcSchema.optional().meta({ description: "The functions this app exposes to other apps, and the ones it calls on them." }),
21118
21122
  events: eventsSchema.optional().meta({
21119
21123
  description: "The push topics this app broadcasts to every app on the site. Requires the `sharedContext` data capability; a topic left out reaches only this app\u2019s own browser code."
@@ -21430,12 +21434,33 @@ var validateRpc = /* @__PURE__ */ __name((manifest, ctx) => {
21430
21434
  ["rpc", "exposes"],
21431
21435
  "function"
21432
21436
  );
21437
+ const dependencies = new Set((manifest.dependencies ?? []).map((dependency) => dependency.app));
21433
21438
  for (const [index, call] of (manifest.rpc?.calls ?? []).entries()) {
21434
21439
  if (call.app === manifest.slug) {
21435
21440
  ctx.addIssue({ code: "custom", path: ["rpc", "calls", index, "app"], message: "An app cannot declare an RPC call to itself" });
21441
+ } else if (!dependencies.has(call.app)) {
21442
+ ctx.addIssue({
21443
+ code: "custom",
21444
+ path: ["rpc", "calls", index, "app"],
21445
+ message: `${call.app} is called over RPC, so it must be in dependencies. Add { "app": "${call.app}" } to dependencies; if your Qinto CLI rejects that field, upgrade it.`
21446
+ });
21436
21447
  }
21437
21448
  }
21438
21449
  }, "validateRpc");
21450
+ var validateDependencies = /* @__PURE__ */ __name((manifest, ctx) => {
21451
+ const dependencies = manifest.dependencies ?? [];
21452
+ uniqueOrIssue(
21453
+ ctx,
21454
+ dependencies.map((dependency) => dependency.app),
21455
+ ["dependencies"],
21456
+ "dependency"
21457
+ );
21458
+ for (const [index, dependency] of dependencies.entries()) {
21459
+ if (dependency.app === manifest.slug) {
21460
+ ctx.addIssue({ code: "custom", path: ["dependencies", index, "app"], message: "An app cannot depend on itself" });
21461
+ }
21462
+ }
21463
+ }, "validateDependencies");
21439
21464
  var validateServer = /* @__PURE__ */ __name((manifest, ctx) => {
21440
21465
  uniqueOrIssue(ctx, manifest.server.hooks.transform, ["server", "hooks", "transform"], "transform hook");
21441
21466
  uniqueOrIssue(ctx, manifest.server.hooks.onEvent, ["server", "hooks", "onEvent"], "event hook");
@@ -21483,6 +21508,7 @@ var appManifestSchema = baseSchema.superRefine((manifest, ctx) => {
21483
21508
  validateEndpoints(manifest, ctx);
21484
21509
  validateCapture(manifest, ctx);
21485
21510
  validateBrowserHooks(manifest, ctx);
21511
+ validateDependencies(manifest, ctx);
21486
21512
  validateRpc(manifest, ctx);
21487
21513
  validateEvents(manifest, ctx);
21488
21514
  validateOauth(manifest, ctx);
@@ -21511,9 +21537,47 @@ var storageLinkBindingName = /* @__PURE__ */ __name((domainId) => {
21511
21537
  }, "storageLinkBindingName");
21512
21538
  var MAX_APPS_Q_BYTES = 4 * 1024 * 1024;
21513
21539
 
21540
+ // ../../libs/app-envelope/src/geo.ts
21541
+ var cfString = external_exports.string().optional().catch(void 0);
21542
+ var requestGeoSchema = external_exports.object({
21543
+ country: cfString,
21544
+ city: cfString,
21545
+ region: cfString,
21546
+ regionCode: cfString,
21547
+ timezone: cfString,
21548
+ postalCode: cfString,
21549
+ continent: cfString,
21550
+ latitude: cfString,
21551
+ longitude: cfString,
21552
+ isEUCountry: cfString,
21553
+ botManagement: external_exports.object({
21554
+ ja4: cfString,
21555
+ ja3Hash: cfString,
21556
+ score: external_exports.number().optional().catch(void 0),
21557
+ verifiedBot: external_exports.boolean().optional().catch(void 0)
21558
+ }).optional().catch(void 0)
21559
+ });
21560
+ var userGeoSchema = external_exports.object({
21561
+ ip: external_exports.string().optional(),
21562
+ userAgent: external_exports.string().optional(),
21563
+ timezone: external_exports.string().optional(),
21564
+ city: external_exports.string().optional(),
21565
+ postalCode: external_exports.string().optional(),
21566
+ region: external_exports.string().optional(),
21567
+ regionCode: external_exports.string().optional(),
21568
+ country: external_exports.string().optional(),
21569
+ continent: external_exports.string().optional(),
21570
+ isEUCountry: external_exports.string().optional(),
21571
+ latitude: external_exports.string().optional(),
21572
+ longitude: external_exports.string().optional(),
21573
+ ja4: external_exports.string().optional(),
21574
+ ja3: external_exports.string().optional(),
21575
+ botScore: external_exports.string().optional()
21576
+ });
21577
+
21514
21578
  // ../../libs/app-envelope/src/envelope.ts
21515
21579
  var ENVELOPE_VERSION = 3;
21516
- var ENVELOPE_ERROR_CODES = ["invalid_arguments"];
21580
+ var ENVELOPE_ERROR_CODES = ["invalid_arguments", "result_too_large", "callee_timeout", "rpc_error"];
21517
21581
  var MAX_INSTALLS_PER_APP = 15;
21518
21582
  var MAX_APPS_PER_DOMAIN = 50;
21519
21583
  var MAX_EMITS_PER_INSTALL = 10;
@@ -21529,6 +21593,7 @@ var MAX_SHARED_CONTEXT_ENTRY_BYTES = 4 * 1024;
21529
21593
  var MAX_PUSH_BYTES = 16 * 1024;
21530
21594
  var MAX_RESPONSE_BYTES = 1024 * 1024;
21531
21595
  var MAX_RPC_BYTES = 128 * 1024;
21596
+ var TRAFFIC_CLASSES = ["hot", "cold"];
21532
21597
  var installSchema = 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();
21533
21598
  var installRuleSchema = external_exports.object({
21534
21599
  mode: external_exports.enum(["except", "only"]),
@@ -21588,11 +21653,21 @@ var envelopeRequestSchema = external_exports.object({
21588
21653
  anonId: external_exports.string().min(1).max(64),
21589
21654
  globalId: external_exports.string().min(1).max(64).nullable(),
21590
21655
  createdAt: external_exports.number().int().optional(),
21591
- properties: external_exports.record(external_exports.string(), external_exports.unknown()),
21592
- geo: external_exports.record(external_exports.string(), external_exports.string()).optional()
21656
+ properties: external_exports.record(external_exports.string(), external_exports.string()),
21657
+ geo: userGeoSchema.optional()
21593
21658
  }).strict().optional(),
21594
- consent: external_exports.object({ enabled: external_exports.boolean(), categories: external_exports.record(external_exports.string(), external_exports.boolean()), apps: external_exports.record(external_exports.string(), external_exports.boolean()) }).strict().optional(),
21595
- geo: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
21659
+ consent: external_exports.object({
21660
+ enabled: external_exports.boolean(),
21661
+ categories: external_exports.object({
21662
+ necessary: external_exports.literal(true),
21663
+ functional: external_exports.boolean(),
21664
+ analytics: external_exports.boolean(),
21665
+ marketing: external_exports.boolean(),
21666
+ salesOfData: external_exports.boolean()
21667
+ }),
21668
+ apps: external_exports.record(external_exports.string(), external_exports.boolean())
21669
+ }).strict().optional(),
21670
+ geo: requestGeoSchema.optional(),
21596
21671
  // Connect envelopes only: the page the visitor's socket opened from, and the visit session it joined.
21597
21672
  page: pageSchema.optional(),
21598
21673
  session: sessionSchema.optional(),
@@ -21603,7 +21678,15 @@ var envelopeRequestSchema = external_exports.object({
21603
21678
  context: external_exports.object({ private: contextEntrySchema.optional(), shared: external_exports.record(external_exports.string().min(1).max(30), contextEntrySchema).optional() }).strict().optional(),
21604
21679
  // RPC depth is capped at 3 by the broker; hook depth at 2 by the emit path.
21605
21680
  depth: external_exports.number().int().min(0).max(3),
21606
- rpc: external_exports.object({ caller: external_exports.string().min(1).max(30).nullable(), fn: external_exports.string().min(1).max(64), args: external_exports.unknown() }).strict().refine((rpc) => {
21681
+ rpc: external_exports.object({
21682
+ caller: external_exports.string().min(1).max(30).nullable(),
21683
+ fn: external_exports.string().min(1).max(64),
21684
+ args: external_exports.unknown(),
21685
+ // Optional on the wire: a site worker predating traffic classes sends none, which means hot.
21686
+ traffic: external_exports.enum(TRAFFIC_CLASSES).optional(),
21687
+ // Cold calls only: the broker mints it so the callee can query its own catalog.
21688
+ queryTicket: external_exports.string().min(1).max(2048).optional()
21689
+ }).strict().refine((rpc) => {
21607
21690
  return new TextEncoder().encode(JSON.stringify(rpc.args ?? null)).length <= MAX_RPC_BYTES;
21608
21691
  }, "RPC args exceed the size cap").optional(),
21609
21692
  action: external_exports.object({
@@ -21900,7 +21983,7 @@ var endpointContextSchema = external_exports.object({
21900
21983
  method: external_exports.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
21901
21984
  mode: external_exports.enum(["webhook", "api"]),
21902
21985
  version: external_exports.string().min(1).max(32),
21903
- geo: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
21986
+ geo: requestGeoSchema.optional(),
21904
21987
  installs: external_exports.array(installSchema3).min(1).max(15),
21905
21988
  user: external_exports.object({ anonId: external_exports.string().min(1).max(64) }).strict().optional()
21906
21989
  }).strict().refine((context) => {
@@ -26495,6 +26578,207 @@ var constantTimeEqual = /* @__PURE__ */ __name((expected, presented) => {
26495
26578
  return expectedBytes.length === presentedBytes.length && timingSafeEqual(expectedBytes, presentedBytes);
26496
26579
  }, "constantTimeEqual");
26497
26580
 
26581
+ // src/helpers/durable.ts
26582
+ var ownGraphEnv = /* @__PURE__ */ __name((ctx, env) => {
26583
+ const exports = ctx.exports;
26584
+ return {
26585
+ ...env,
26586
+ get ANON_DO() {
26587
+ return exports.AnonDO;
26588
+ },
26589
+ get GLOBAL_DO() {
26590
+ return exports.GlobalDO;
26591
+ },
26592
+ get ALIAS_DO() {
26593
+ return exports.AliasDO;
26594
+ }
26595
+ };
26596
+ }, "ownGraphEnv");
26597
+ var JURISDICTIONS = /* @__PURE__ */ new Set(["eu", "fedramp", "fedramp-high", "us"]);
26598
+ var LOCATION_HINTS = /* @__PURE__ */ new Set([
26599
+ "wnam",
26600
+ "enam",
26601
+ "sam",
26602
+ "weur",
26603
+ "eeur",
26604
+ "apac",
26605
+ "apac-ne",
26606
+ "apac-se",
26607
+ "oc",
26608
+ "afr",
26609
+ "me"
26610
+ ]);
26611
+ var residencyStub = /* @__PURE__ */ __name((namespace, env, name) => {
26612
+ const residency = env;
26613
+ const jurisdiction = residency.DO_JURISDICTION;
26614
+ const scoped = jurisdiction !== void 0 && JURISDICTIONS.has(jurisdiction) ? namespace.jurisdiction(jurisdiction) : namespace;
26615
+ const locationHint = residency.LOCATION_HINT;
26616
+ const options = locationHint !== void 0 && LOCATION_HINTS.has(locationHint) ? { locationHint } : void 0;
26617
+ return scoped.get(scoped.idFromName(name), options);
26618
+ }, "residencyStub");
26619
+
26620
+ // src/id-graph/anon/client.ts
26621
+ var anonStub = /* @__PURE__ */ __name((env, anonId) => {
26622
+ return residencyStub(env.ANON_DO, env, anonId);
26623
+ }, "anonStub");
26624
+
26625
+ // src/id-graph/consent.ts
26626
+ var CONSENT_CATEGORIES2 = ["necessary", "functional", "analytics", "marketing", "salesOfData"];
26627
+ var CONSENT_CATEGORY_PREFIX = "consent.category.";
26628
+ var CONSENT_APP_PREFIX = "consent.app.";
26629
+ var consentEntries = /* @__PURE__ */ __name((payload, observedAt) => {
26630
+ const entries = [];
26631
+ for (const category of CONSENT_CATEGORIES2) {
26632
+ const granted = payload.categories[category];
26633
+ if (granted === void 0) {
26634
+ continue;
26635
+ }
26636
+ entries.push({ key: `${CONSENT_CATEGORY_PREFIX}${category}`, value: String(granted), observedAt });
26637
+ }
26638
+ for (const [appId, granted] of Object.entries(payload.apps)) {
26639
+ entries.push({ key: `${CONSENT_APP_PREFIX}${appId}`, value: String(granted), observedAt });
26640
+ }
26641
+ return entries;
26642
+ }, "consentEntries");
26643
+
26644
+ // src/id-graph/projections.ts
26645
+ var PII_PREFIX = "pii.";
26646
+ var CUSTOM_PREFIX = "custom.";
26647
+ var GEO_PREFIX = "geo.";
26648
+ var PROJECTION_KINDS = ["pii", "custom", "consent_category", "consent_app", "geo"];
26649
+ var PROJECTION_PREFIX = {
26650
+ pii: PII_PREFIX,
26651
+ custom: CUSTOM_PREFIX,
26652
+ consent_category: CONSENT_CATEGORY_PREFIX,
26653
+ consent_app: CONSENT_APP_PREFIX,
26654
+ geo: GEO_PREFIX
26655
+ };
26656
+ var FANOUT_KINDS = ["pii", "custom", "consent_category", "consent_app"];
26657
+ var projectionFact = /* @__PURE__ */ __name((projectionKey) => {
26658
+ for (const kind2 of PROJECTION_KINDS) {
26659
+ const prefix = PROJECTION_PREFIX[kind2];
26660
+ if (projectionKey.startsWith(prefix)) {
26661
+ return { kind: kind2, scope: "", key: projectionKey.slice(prefix.length) };
26662
+ }
26663
+ }
26664
+ throw new Error(`Unknown projection key: ${projectionKey}`);
26665
+ }, "projectionFact");
26666
+
26667
+ // src/id-graph/anon/facts.ts
26668
+ var FACT_KINDS = [
26669
+ ...PROJECTION_KINDS,
26670
+ "capture",
26671
+ "identifier",
26672
+ "conversion",
26673
+ "context_private_kv",
26674
+ "context_private_text",
26675
+ "context_shared_kv",
26676
+ "context_shared_text"
26677
+ ];
26678
+ var APP_FACT_KINDS = ["custom", "capture", "identifier", "conversion", "pii", "consent_category", "consent_app", "geo"];
26679
+ var MAX_APP_FACTS_PER_PAGE = 100;
26680
+ var MAX_APP_FACT_PAGE_BYTES = 1024 * 1024;
26681
+ var COLUMNS = "kind, scope, key, value, source, type, amount, currency, event_id, data_json, first_at, observed_at, synced_at, expires_at";
26682
+ var rowOf = /* @__PURE__ */ __name((row) => {
26683
+ return {
26684
+ kind: row.kind,
26685
+ scope: row.scope,
26686
+ key: row.key,
26687
+ value: row.value,
26688
+ source: row.source,
26689
+ type: row.type,
26690
+ amount: row.amount,
26691
+ currency: row.currency,
26692
+ eventId: row.event_id,
26693
+ dataJson: row.data_json,
26694
+ firstAt: row.first_at,
26695
+ observedAt: row.observed_at,
26696
+ syncedAt: row.synced_at,
26697
+ expiresAt: row.expires_at
26698
+ };
26699
+ }, "rowOf");
26700
+ var appFactOf = /* @__PURE__ */ __name((row) => {
26701
+ const { syncedAt: _syncedAt, expiresAt: _expiresAt, ...fact } = rowOf(row);
26702
+ return { ...fact, kind: fact.kind };
26703
+ }, "appFactOf");
26704
+ var listAppFacts = /* @__PURE__ */ __name((sql, query, now) => {
26705
+ const maxRowId = query.maxRowId ?? sql.exec("SELECT MAX(rowid) AS maxRowId FROM facts").one().maxRowId;
26706
+ if (maxRowId === null) {
26707
+ return { items: [], next: null };
26708
+ }
26709
+ const where = ["rowid > ?", "rowid <= ?", "(expires_at IS NULL OR expires_at > ?)"];
26710
+ const params = [query.afterRowId, maxRowId, now];
26711
+ if (query.kinds.length > 0) {
26712
+ where.push(`kind IN (${query.kinds.map(() => "?").join(", ")})`);
26713
+ params.push(...query.kinds);
26714
+ }
26715
+ if (query.observedAtGte !== null) {
26716
+ where.push("observed_at >= ?");
26717
+ params.push(query.observedAtGte);
26718
+ }
26719
+ if (query.observedAtLt !== null) {
26720
+ where.push("observed_at < ?");
26721
+ params.push(query.observedAtLt);
26722
+ }
26723
+ const rows = sql.exec(`SELECT rowid, ${COLUMNS} FROM facts WHERE ${where.join(" AND ")} ORDER BY rowid LIMIT ?`, ...params, query.limit + 1).toArray();
26724
+ const items = [];
26725
+ let lastRowId = query.afterRowId;
26726
+ for (const row of rows.slice(0, query.limit)) {
26727
+ const fact = appFactOf(row);
26728
+ const candidate = [...items, fact];
26729
+ if (new TextEncoder().encode(JSON.stringify(candidate)).byteLength > MAX_APP_FACT_PAGE_BYTES) {
26730
+ if (items.length === 0) {
26731
+ throw new Error("fact_too_large");
26732
+ }
26733
+ break;
26734
+ }
26735
+ items.push(fact);
26736
+ lastRowId = row.rowid;
26737
+ }
26738
+ const hasMore = rows.length > items.length;
26739
+ return { items, next: hasMore ? { afterRowId: lastRowId, maxRowId } : null };
26740
+ }, "listAppFacts");
26741
+ var insertFact = /* @__PURE__ */ __name((sql, fact) => {
26742
+ sql.exec(
26743
+ `INSERT INTO facts (${COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?)`,
26744
+ fact.kind,
26745
+ fact.scope,
26746
+ fact.key,
26747
+ fact.value ?? null,
26748
+ fact.source ?? null,
26749
+ fact.type ?? null,
26750
+ fact.amount ?? null,
26751
+ fact.currency ?? null,
26752
+ fact.eventId ?? null,
26753
+ fact.dataJson ?? null,
26754
+ fact.firstAt ?? null,
26755
+ fact.observedAt,
26756
+ fact.expiresAt ?? null
26757
+ );
26758
+ }, "insertFact");
26759
+ var latestFact = /* @__PURE__ */ __name((sql, coords) => {
26760
+ const row = sql.exec(
26761
+ `SELECT ${COLUMNS} FROM facts WHERE kind = ? AND scope = ? AND key = ? ORDER BY observed_at DESC, rowid DESC LIMIT 1`,
26762
+ coords.kind,
26763
+ coords.scope,
26764
+ coords.key
26765
+ ).toArray().at(0);
26766
+ return row === void 0 ? void 0 : rowOf(row);
26767
+ }, "latestFact");
26768
+ var latestFacts = /* @__PURE__ */ __name((sql, kind2) => {
26769
+ const rows = sql.exec(`SELECT ${COLUMNS} FROM facts WHERE kind = ? ORDER BY scope, key, observed_at, rowid`, kind2).toArray();
26770
+ const latest = [];
26771
+ for (const row of rows) {
26772
+ const previous = latest.at(-1);
26773
+ if (previous !== void 0 && previous.scope === row.scope && previous.key === row.key) {
26774
+ latest[latest.length - 1] = rowOf(row);
26775
+ continue;
26776
+ }
26777
+ latest.push(rowOf(row));
26778
+ }
26779
+ return latest;
26780
+ }, "latestFacts");
26781
+
26498
26782
  // src/apps/consent-gate.ts
26499
26783
  var parseEnforced = /* @__PURE__ */ __name((raw2) => {
26500
26784
  let parsed;
@@ -26577,6 +26861,9 @@ var appSchema = external_exports.object({
26577
26861
  // Absent from an old embedded registry; the default never equals a real claim, so the ui proxy
26578
26862
  // refuses stale registries with token_stale instead of turning the app off.
26579
26863
  versionId: external_exports.string().min(1).default(""),
26864
+ // Absent from a registry deployed before cold rpc calls carried a query ticket; without it the
26865
+ // broker sends the callee none.
26866
+ appId: external_exports.string().min(1).optional(),
26580
26867
  consent: external_exports.object({ category: external_exports.string() }),
26581
26868
  // Plain strings, not the bucket enum: an unknown bucket from a newer registry must not turn apps off.
26582
26869
  data: external_exports.array(external_exports.string()).default([]),
@@ -26663,210 +26950,6 @@ var siteRefOf = /* @__PURE__ */ __name(({ domainId, fingerprint }) => {
26663
26950
  return { domainId, fingerprint };
26664
26951
  }, "siteRefOf");
26665
26952
 
26666
- // ../../libs/cf-data/src/cf-data.ts
26667
- var CF_DATA_HEADER = "x-cf-data";
26668
- var parseCfData = /* @__PURE__ */ __name((raw2) => {
26669
- if (raw2 === null) {
26670
- return void 0;
26671
- }
26672
- let parsed;
26673
- try {
26674
- parsed = JSON.parse(raw2);
26675
- } catch {
26676
- return void 0;
26677
- }
26678
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
26679
- return void 0;
26680
- }
26681
- return parsed;
26682
- }, "parseCfData");
26683
-
26684
- // src/helpers/geo.ts
26685
- var UNDETERMINED_COUNTRIES = /* @__PURE__ */ new Set(["XX", "T1"]);
26686
- var detectedCountry = /* @__PURE__ */ __name((cf) => {
26687
- const country = cf?.country;
26688
- return country && !UNDETERMINED_COUNTRIES.has(country) ? country : void 0;
26689
- }, "detectedCountry");
26690
- var euCountry = /* @__PURE__ */ __name((cf, country) => {
26691
- if (cf?.isEUCountry === "1") {
26692
- return "yes";
26693
- }
26694
- return country ? "no" : "unknown";
26695
- }, "euCountry");
26696
- var withForwardedCf = /* @__PURE__ */ __name((request) => {
26697
- if (request.cf !== void 0) {
26698
- return request;
26699
- }
26700
- const cf = parseCfData(request.headers.get(CF_DATA_HEADER));
26701
- return cf === void 0 ? request : new Request(request, { cf });
26702
- }, "withForwardedCf");
26703
- var loggableGeo = /* @__PURE__ */ __name((geo) => {
26704
- const fields = {};
26705
- for (const [key, value] of Object.entries(geo)) {
26706
- if (value !== void 0) {
26707
- fields[key] = String(value);
26708
- }
26709
- }
26710
- return fields;
26711
- }, "loggableGeo");
26712
- var hostFromRequest = /* @__PURE__ */ __name((request) => {
26713
- return {
26714
- ip: request.headers.get("cf-connecting-ip") ?? void 0,
26715
- userAgent: request.headers.get("user-agent") ?? void 0,
26716
- locale: request.headers.get("accept-language")?.split(",")[0]?.trim() || void 0
26717
- };
26718
- }, "hostFromRequest");
26719
- var geoFromCf = /* @__PURE__ */ __name((cf) => {
26720
- const bot = cf?.botManagement;
26721
- const country = detectedCountry(cf);
26722
- return {
26723
- timezone: cf?.timezone ?? void 0,
26724
- city: cf?.city ?? void 0,
26725
- postalCode: cf?.postalCode ?? void 0,
26726
- region: cf?.region ?? void 0,
26727
- regionCode: cf?.regionCode ?? void 0,
26728
- country,
26729
- continent: cf?.continent ?? void 0,
26730
- isEUCountry: euCountry(cf, country),
26731
- latitude: cf?.latitude ?? void 0,
26732
- longitude: cf?.longitude ?? void 0,
26733
- ja4: bot?.ja4,
26734
- ja3: bot?.ja3Hash,
26735
- botScore: bot?.score
26736
- };
26737
- }, "geoFromCf");
26738
- var geoFromRequest = /* @__PURE__ */ __name((request) => {
26739
- return {
26740
- ...hostFromRequest(request),
26741
- ...geoFromCf(request.cf)
26742
- };
26743
- }, "geoFromRequest");
26744
-
26745
- // src/helpers/durable.ts
26746
- var ownGraphEnv = /* @__PURE__ */ __name((ctx, env) => {
26747
- const exports = ctx.exports;
26748
- return {
26749
- ...env,
26750
- get ANON_DO() {
26751
- return exports.AnonDO;
26752
- },
26753
- get GLOBAL_DO() {
26754
- return exports.GlobalDO;
26755
- },
26756
- get ALIAS_DO() {
26757
- return exports.AliasDO;
26758
- }
26759
- };
26760
- }, "ownGraphEnv");
26761
- var JURISDICTIONS = /* @__PURE__ */ new Set(["eu", "fedramp", "fedramp-high", "us"]);
26762
- var LOCATION_HINTS = /* @__PURE__ */ new Set([
26763
- "wnam",
26764
- "enam",
26765
- "sam",
26766
- "weur",
26767
- "eeur",
26768
- "apac",
26769
- "apac-ne",
26770
- "apac-se",
26771
- "oc",
26772
- "afr",
26773
- "me"
26774
- ]);
26775
- var residencyStub = /* @__PURE__ */ __name((namespace, env, name) => {
26776
- const residency = env;
26777
- const jurisdiction = residency.DO_JURISDICTION;
26778
- const scoped = jurisdiction !== void 0 && JURISDICTIONS.has(jurisdiction) ? namespace.jurisdiction(jurisdiction) : namespace;
26779
- const locationHint = residency.LOCATION_HINT;
26780
- const options = locationHint !== void 0 && LOCATION_HINTS.has(locationHint) ? { locationHint } : void 0;
26781
- return scoped.get(scoped.idFromName(name), options);
26782
- }, "residencyStub");
26783
-
26784
- // src/id-graph/anon/client.ts
26785
- var anonStub = /* @__PURE__ */ __name((env, anonId) => {
26786
- return residencyStub(env.ANON_DO, env, anonId);
26787
- }, "anonStub");
26788
-
26789
- // src/id-graph/consent.ts
26790
- var CONSENT_CATEGORIES2 = ["necessary", "functional", "analytics", "marketing", "salesOfData"];
26791
- var CONSENT_CATEGORY_PREFIX = "consent.category.";
26792
- var CONSENT_APP_PREFIX = "consent.app.";
26793
- var consentEntries = /* @__PURE__ */ __name((payload, observedAt) => {
26794
- const entries = [];
26795
- for (const category of CONSENT_CATEGORIES2) {
26796
- const granted = payload.categories[category];
26797
- if (granted === void 0) {
26798
- continue;
26799
- }
26800
- entries.push({ key: `${CONSENT_CATEGORY_PREFIX}${category}`, value: String(granted), observedAt });
26801
- }
26802
- for (const [appId, granted] of Object.entries(payload.apps)) {
26803
- entries.push({ key: `${CONSENT_APP_PREFIX}${appId}`, value: String(granted), observedAt });
26804
- }
26805
- return entries;
26806
- }, "consentEntries");
26807
-
26808
- // src/id-graph/traits.ts
26809
- var PII_KEYS = ["first_name", "last_name", "email", "phone", "country", "state", "city", "postal_code"];
26810
- var snakeCase = /* @__PURE__ */ __name((key) => {
26811
- return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
26812
- }, "snakeCase");
26813
- var stringifyTrait = /* @__PURE__ */ __name((value) => {
26814
- return typeof value === "string" ? value : JSON.stringify(value);
26815
- }, "stringifyTrait");
26816
- var piiTrait = /* @__PURE__ */ __name((key, rawValue) => {
26817
- if (key !== "email" && key !== "phone") {
26818
- return stringifyTrait(rawValue);
26819
- }
26820
- if (typeof rawValue !== "string") {
26821
- return void 0;
26822
- }
26823
- return (key === "email" ? emailIdentifier(rawValue) : phoneIdentifier(rawValue))?.value;
26824
- }, "piiTrait");
26825
- var resolvePiiTrait = /* @__PURE__ */ __name((key, rawValue, unset) => {
26826
- return unset ? null : piiTrait(key, rawValue);
26827
- }, "resolvePiiTrait");
26828
- var classifyTraits = /* @__PURE__ */ __name((traits, observedAt) => {
26829
- const classified = { pii: {}, custom: {}, latest: [], dropped: [] };
26830
- for (const [rawKey, rawValue] of Object.entries(traits)) {
26831
- const key = snakeCase(rawKey);
26832
- const unset = rawValue === null || rawValue === void 0;
26833
- if (PII_KEYS.includes(key)) {
26834
- const value2 = resolvePiiTrait(key, rawValue, unset);
26835
- if (value2 === void 0) {
26836
- classified.dropped.push(key);
26837
- continue;
26838
- }
26839
- classified.pii[key] = value2;
26840
- classified.latest.push({ key: `pii.${key}`, value: value2, observedAt });
26841
- continue;
26842
- }
26843
- const value = unset ? null : stringifyTrait(rawValue);
26844
- classified.custom[key] = value;
26845
- classified.latest.push({ key: `custom.${key}`, value, observedAt });
26846
- }
26847
- return classified;
26848
- }, "classifyTraits");
26849
- var conversionFromTrack = /* @__PURE__ */ __name((payload) => {
26850
- const amount = payload.properties["value"];
26851
- if (typeof amount !== "number" || !Number.isFinite(amount)) {
26852
- return void 0;
26853
- }
26854
- const orderId = payload.properties["orderId"];
26855
- if (typeof orderId !== "string" || orderId === "") {
26856
- return void 0;
26857
- }
26858
- const currency = payload.properties["currency"];
26859
- const contents = payload.properties["contents"];
26860
- return {
26861
- eventName: payload.event,
26862
- eventId: payload.eventId,
26863
- orderId,
26864
- amount,
26865
- currency: typeof currency === "string" ? currency : null,
26866
- contents: contents === void 0 ? null : JSON.stringify(contents)
26867
- };
26868
- }, "conversionFromTrack");
26869
-
26870
26953
  // src/id-graph/alias/client.ts
26871
26954
  var sha256Hex = /* @__PURE__ */ __name(async (value) => {
26872
26955
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
@@ -26902,291 +26985,6 @@ var claimPair = /* @__PURE__ */ __name(async (env, identifier, globalId, anonId)
26902
26985
  });
26903
26986
  }, "claimPair");
26904
26987
 
26905
- // src/id-graph/user.ts
26906
- 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}$/;
26907
- var isEdgeTagId = /* @__PURE__ */ __name((id) => {
26908
- return EDGETAG_ID.test(id);
26909
- }, "isEdgeTagId");
26910
- var isCanonicalId = /* @__PURE__ */ __name((id) => {
26911
- return id.startsWith("anon_") || isEdgeTagId(id);
26912
- }, "isCanonicalId");
26913
- var generateUserId = /* @__PURE__ */ __name(() => {
26914
- return `anon_${crypto.randomUUID()}`;
26915
- }, "generateUserId");
26916
- var restoreUser = /* @__PURE__ */ __name(async (env, cookie, candidate) => {
26917
- if (cookie === void 0) {
26918
- return candidate;
26919
- }
26920
- const assignment = await claimFallbackAnon(env, cookie, candidate);
26921
- setSpanAttributes({ "qinto.fallback_restored": assignment.anonId !== candidate });
26922
- return assignment.anonId;
26923
- }, "restoreUser");
26924
- var resolveUser = /* @__PURE__ */ __name(async (env, id) => {
26925
- if (isCanonicalId(id)) {
26926
- return { id };
26927
- }
26928
- const identifier = classifyIdentifier(id);
26929
- const assignment = await claimAnon(env, identifier, generateUserId());
26930
- return { id: assignment.anonId, identifier };
26931
- }, "resolveUser");
26932
- var resolveExistingUser = /* @__PURE__ */ __name(async (env, id) => {
26933
- if (isCanonicalId(id)) {
26934
- return { id };
26935
- }
26936
- const identifier = classifyIdentifier(id);
26937
- const anonId = (await getAssignment(env, identifier))?.anonId ?? null;
26938
- return anonId === null ? null : { id: anonId, identifier };
26939
- }, "resolveExistingUser");
26940
- var lookupUser = /* @__PURE__ */ __name(async (env, id) => {
26941
- if (isCanonicalId(id)) {
26942
- const globalId = await traced("rpc anon.get_global_id", {}, () => {
26943
- return anonStub(env, id).getGlobalId(currentTraceContext());
26944
- });
26945
- return { anonId: id, globalId };
26946
- }
26947
- const assignment = await getAssignment(env, classifyIdentifier(id));
26948
- return { anonId: assignment?.anonId ?? null, globalId: assignment?.globalId ?? null };
26949
- }, "lookupUser");
26950
-
26951
- // src/id-graph/noop.ts
26952
- var isNoopConsent = /* @__PURE__ */ __name((payload) => {
26953
- return consentEntries(payload, 0).length === 0;
26954
- }, "isNoopConsent");
26955
- var isNoopIdentify = /* @__PURE__ */ __name((traits) => {
26956
- return Object.keys(traits).length === 0;
26957
- }, "isNoopIdentify");
26958
- var isNoopSubmission = /* @__PURE__ */ __name((userId, args) => {
26959
- if (args.type === "consent") {
26960
- return isNoopConsent(args.payload);
26961
- }
26962
- if (args.type === "identify") {
26963
- return isCanonicalId(userId) && isNoopIdentify(args.payload.traits);
26964
- }
26965
- return false;
26966
- }, "isNoopSubmission");
26967
- var isNoopAction = /* @__PURE__ */ __name((action) => {
26968
- if (action.identifier !== void 0) {
26969
- return false;
26970
- }
26971
- return action.type === "consent" && isNoopConsent(action.payload);
26972
- }, "isNoopAction");
26973
- var isDeliveryOnlyAction = /* @__PURE__ */ __name((action) => {
26974
- if (action.identifier !== void 0 || action.type === "consent") {
26975
- return false;
26976
- }
26977
- return action.type === "track" ? conversionFromTrack(action.payload) === void 0 : isNoopIdentify(action.payload.traits);
26978
- }, "isDeliveryOnlyAction");
26979
-
26980
- // src/id-graph/action.ts
26981
- var UnknownUserError = class extends Error {
26982
- static {
26983
- __name(this, "UnknownUserError");
26984
- }
26985
- constructor() {
26986
- super("the identifier resolves to no user");
26987
- this.name = "UnknownUserError";
26988
- }
26989
- };
26990
- var mintActionId = /* @__PURE__ */ __name(() => {
26991
- return `act_${ulid3()}`;
26992
- }, "mintActionId");
26993
- var buildAction = /* @__PURE__ */ __name((args) => {
26994
- const receivedAt = args.receivedAt ?? Date.now();
26995
- return {
26996
- ...args,
26997
- actionId: mintActionId(),
26998
- receivedAt
26999
- };
27000
- }, "buildAction");
27001
- var ingest = /* @__PURE__ */ __name(async (env, action, site = ownSite(env)) => {
27002
- await traced("rpc anon.ingest", {}, () => {
27003
- return anonStub(env, action.userId).ingest(action, site, currentTraceContext());
27004
- });
27005
- }, "ingest");
27006
- var serverActionContext = /* @__PURE__ */ __name((input2, receivedAt) => {
27007
- const network = input2?.network;
27008
- return {
27009
- source: "server",
27010
- occurredAt: input2?.occurredAt ?? receivedAt,
27011
- ...input2?.page === void 0 ? {} : { page: input2.page },
27012
- ...network === void 0 || network.ip === void 0 && network.userAgent === void 0 && network.locale === void 0 ? {} : { network: { ...network, provenance: "caller" } },
27013
- ...input2?.session === void 0 ? {} : { session: input2.session },
27014
- offline: input2?.offline ?? false,
27015
- replay: input2?.replay ?? false
27016
- };
27017
- }, "serverActionContext");
27018
- var submitAction = /* @__PURE__ */ __name(async (env, userId, args, request) => {
27019
- const attrs = {
27020
- "action.type": args.type,
27021
- "action.payload": JSON.stringify(args.payload),
27022
- "action.identifier": userId,
27023
- ...args.type === "track" ? { "action.event": args.payload.event } : {}
27024
- };
27025
- return traced(`action ${args.type}`, { attrs }, async () => {
27026
- if (isNoopSubmission(userId, args)) {
27027
- setSpanAttributes({ "action.noop": true, ...isCanonicalId(userId) ? { "action.user_id": userId } : {} });
27028
- return mintActionId();
27029
- }
27030
- const resolved = await traced("resolve_user", {}, () => {
27031
- return args.type === "track" ? resolveExistingUser(env, userId) : resolveUser(env, userId);
27032
- });
27033
- if (resolved === null) {
27034
- throw new UnknownUserError();
27035
- }
27036
- const receivedAt = Date.now();
27037
- const action = buildAction({
27038
- ...args,
27039
- userId: resolved.id,
27040
- identifier: resolved.identifier,
27041
- receivedAt,
27042
- context: serverActionContext(args.context, receivedAt),
27043
- geo: request?.cf,
27044
- host: request === void 0 ? void 0 : hostFromRequest(request)
27045
- });
27046
- setSpanAttributes({ "action.user_id": resolved.id, "action.id": action.actionId });
27047
- await ingest(env, action);
27048
- return action.actionId;
27049
- });
27050
- }, "submitAction");
27051
-
27052
- // src/id-graph/projections.ts
27053
- var PII_PREFIX = "pii.";
27054
- var CUSTOM_PREFIX = "custom.";
27055
- var GEO_PREFIX = "geo.";
27056
- var PROJECTION_KINDS = ["pii", "custom", "consent_category", "consent_app", "geo"];
27057
- var PROJECTION_PREFIX = {
27058
- pii: PII_PREFIX,
27059
- custom: CUSTOM_PREFIX,
27060
- consent_category: CONSENT_CATEGORY_PREFIX,
27061
- consent_app: CONSENT_APP_PREFIX,
27062
- geo: GEO_PREFIX
27063
- };
27064
- var FANOUT_KINDS = ["pii", "custom", "consent_category", "consent_app"];
27065
- var projectionFact = /* @__PURE__ */ __name((projectionKey) => {
27066
- for (const kind2 of PROJECTION_KINDS) {
27067
- const prefix = PROJECTION_PREFIX[kind2];
27068
- if (projectionKey.startsWith(prefix)) {
27069
- return { kind: kind2, scope: "", key: projectionKey.slice(prefix.length) };
27070
- }
27071
- }
27072
- throw new Error(`Unknown projection key: ${projectionKey}`);
27073
- }, "projectionFact");
27074
-
27075
- // src/id-graph/anon/facts.ts
27076
- var FACT_KINDS = [
27077
- ...PROJECTION_KINDS,
27078
- "capture",
27079
- "identifier",
27080
- "conversion",
27081
- "context_private_kv",
27082
- "context_private_text",
27083
- "context_shared_kv",
27084
- "context_shared_text"
27085
- ];
27086
- var APP_FACT_KINDS = ["custom", "capture", "identifier", "conversion", "pii", "consent_category", "consent_app", "geo"];
27087
- var MAX_APP_FACTS_PER_PAGE = 100;
27088
- var MAX_APP_FACT_PAGE_BYTES = 1024 * 1024;
27089
- var COLUMNS = "kind, scope, key, value, source, type, amount, currency, event_id, data_json, first_at, observed_at, synced_at, expires_at";
27090
- var rowOf = /* @__PURE__ */ __name((row) => {
27091
- return {
27092
- kind: row.kind,
27093
- scope: row.scope,
27094
- key: row.key,
27095
- value: row.value,
27096
- source: row.source,
27097
- type: row.type,
27098
- amount: row.amount,
27099
- currency: row.currency,
27100
- eventId: row.event_id,
27101
- dataJson: row.data_json,
27102
- firstAt: row.first_at,
27103
- observedAt: row.observed_at,
27104
- syncedAt: row.synced_at,
27105
- expiresAt: row.expires_at
27106
- };
27107
- }, "rowOf");
27108
- var appFactOf = /* @__PURE__ */ __name((row) => {
27109
- const { syncedAt: _syncedAt, expiresAt: _expiresAt, ...fact } = rowOf(row);
27110
- return { ...fact, kind: fact.kind };
27111
- }, "appFactOf");
27112
- var listAppFacts = /* @__PURE__ */ __name((sql, query, now) => {
27113
- const maxRowId = query.maxRowId ?? sql.exec("SELECT MAX(rowid) AS maxRowId FROM facts").one().maxRowId;
27114
- if (maxRowId === null) {
27115
- return { items: [], next: null };
27116
- }
27117
- const where = ["rowid > ?", "rowid <= ?", "(expires_at IS NULL OR expires_at > ?)"];
27118
- const params = [query.afterRowId, maxRowId, now];
27119
- if (query.kinds.length > 0) {
27120
- where.push(`kind IN (${query.kinds.map(() => "?").join(", ")})`);
27121
- params.push(...query.kinds);
27122
- }
27123
- if (query.observedAtGte !== null) {
27124
- where.push("observed_at >= ?");
27125
- params.push(query.observedAtGte);
27126
- }
27127
- if (query.observedAtLt !== null) {
27128
- where.push("observed_at < ?");
27129
- params.push(query.observedAtLt);
27130
- }
27131
- const rows = sql.exec(`SELECT rowid, ${COLUMNS} FROM facts WHERE ${where.join(" AND ")} ORDER BY rowid LIMIT ?`, ...params, query.limit + 1).toArray();
27132
- const items = [];
27133
- let lastRowId = query.afterRowId;
27134
- for (const row of rows.slice(0, query.limit)) {
27135
- const fact = appFactOf(row);
27136
- const candidate = [...items, fact];
27137
- if (new TextEncoder().encode(JSON.stringify(candidate)).byteLength > MAX_APP_FACT_PAGE_BYTES) {
27138
- if (items.length === 0) {
27139
- throw new Error("fact_too_large");
27140
- }
27141
- break;
27142
- }
27143
- items.push(fact);
27144
- lastRowId = row.rowid;
27145
- }
27146
- const hasMore = rows.length > items.length;
27147
- return { items, next: hasMore ? { afterRowId: lastRowId, maxRowId } : null };
27148
- }, "listAppFacts");
27149
- var insertFact = /* @__PURE__ */ __name((sql, fact) => {
27150
- sql.exec(
27151
- `INSERT INTO facts (${COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?)`,
27152
- fact.kind,
27153
- fact.scope,
27154
- fact.key,
27155
- fact.value ?? null,
27156
- fact.source ?? null,
27157
- fact.type ?? null,
27158
- fact.amount ?? null,
27159
- fact.currency ?? null,
27160
- fact.eventId ?? null,
27161
- fact.dataJson ?? null,
27162
- fact.firstAt ?? null,
27163
- fact.observedAt,
27164
- fact.expiresAt ?? null
27165
- );
27166
- }, "insertFact");
27167
- var latestFact = /* @__PURE__ */ __name((sql, coords) => {
27168
- const row = sql.exec(
27169
- `SELECT ${COLUMNS} FROM facts WHERE kind = ? AND scope = ? AND key = ? ORDER BY observed_at DESC, rowid DESC LIMIT 1`,
27170
- coords.kind,
27171
- coords.scope,
27172
- coords.key
27173
- ).toArray().at(0);
27174
- return row === void 0 ? void 0 : rowOf(row);
27175
- }, "latestFact");
27176
- var latestFacts = /* @__PURE__ */ __name((sql, kind2) => {
27177
- const rows = sql.exec(`SELECT ${COLUMNS} FROM facts WHERE kind = ? ORDER BY scope, key, observed_at, rowid`, kind2).toArray();
27178
- const latest = [];
27179
- for (const row of rows) {
27180
- const previous = latest.at(-1);
27181
- if (previous !== void 0 && previous.scope === row.scope && previous.key === row.key) {
27182
- latest[latest.length - 1] = rowOf(row);
27183
- continue;
27184
- }
27185
- latest.push(rowOf(row));
27186
- }
27187
- return latest;
27188
- }, "latestFacts");
27189
-
27190
26988
  // src/id-graph/global/client.ts
27191
26989
  var globalStub = /* @__PURE__ */ __name((env, globalId) => {
27192
26990
  return residencyStub(env.GLOBAL_DO, env, globalId);
@@ -27255,14 +27053,60 @@ var linkedAnonsPage = /* @__PURE__ */ __name((sql, afterRowId, limit) => {
27255
27053
  anonIds.push(row.key);
27256
27054
  lastRowId = row.rowid;
27257
27055
  }
27258
- return { anonIds, next: hasMore ? lastRowId : null };
27259
- }, "linkedAnonsPage");
27260
- var linkedAnonsExcept = /* @__PURE__ */ __name((sql, originAnonId) => {
27261
- return sql.exec("SELECT key FROM facts WHERE kind = 'anon' AND key != ?", originAnonId).toArray().map((row) => row.key);
27262
- }, "linkedAnonsExcept");
27263
- var allLinkedAnons = /* @__PURE__ */ __name((sql) => {
27264
- return sql.exec("SELECT key FROM facts WHERE kind = 'anon' ORDER BY rowid").toArray().map((row) => row.key);
27265
- }, "allLinkedAnons");
27056
+ return { anonIds, next: hasMore ? lastRowId : null };
27057
+ }, "linkedAnonsPage");
27058
+ var linkedAnonsExcept = /* @__PURE__ */ __name((sql, originAnonId) => {
27059
+ return sql.exec("SELECT key FROM facts WHERE kind = 'anon' AND key != ?", originAnonId).toArray().map((row) => row.key);
27060
+ }, "linkedAnonsExcept");
27061
+ var allLinkedAnons = /* @__PURE__ */ __name((sql) => {
27062
+ return sql.exec("SELECT key FROM facts WHERE kind = 'anon' ORDER BY rowid").toArray().map((row) => row.key);
27063
+ }, "allLinkedAnons");
27064
+
27065
+ // src/id-graph/user.ts
27066
+ 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}$/;
27067
+ var isEdgeTagId = /* @__PURE__ */ __name((id) => {
27068
+ return EDGETAG_ID.test(id);
27069
+ }, "isEdgeTagId");
27070
+ var isCanonicalId = /* @__PURE__ */ __name((id) => {
27071
+ return id.startsWith("anon_") || isEdgeTagId(id);
27072
+ }, "isCanonicalId");
27073
+ var generateUserId = /* @__PURE__ */ __name(() => {
27074
+ return `anon_${crypto.randomUUID()}`;
27075
+ }, "generateUserId");
27076
+ var restoreUser = /* @__PURE__ */ __name(async (env, cookie, candidate) => {
27077
+ if (cookie === void 0) {
27078
+ return candidate;
27079
+ }
27080
+ const assignment = await claimFallbackAnon(env, cookie, candidate);
27081
+ setSpanAttributes({ "qinto.fallback_restored": assignment.anonId !== candidate });
27082
+ return assignment.anonId;
27083
+ }, "restoreUser");
27084
+ var resolveUser = /* @__PURE__ */ __name(async (env, id) => {
27085
+ if (isCanonicalId(id)) {
27086
+ return { id };
27087
+ }
27088
+ const identifier = classifyIdentifier(id);
27089
+ const assignment = await claimAnon(env, identifier, generateUserId());
27090
+ return { id: assignment.anonId, identifier };
27091
+ }, "resolveUser");
27092
+ var resolveExistingUser = /* @__PURE__ */ __name(async (env, id) => {
27093
+ if (isCanonicalId(id)) {
27094
+ return { id };
27095
+ }
27096
+ const identifier = classifyIdentifier(id);
27097
+ const anonId = (await getAssignment(env, identifier))?.anonId ?? null;
27098
+ return anonId === null ? null : { id: anonId, identifier };
27099
+ }, "resolveExistingUser");
27100
+ var lookupUser = /* @__PURE__ */ __name(async (env, id) => {
27101
+ if (isCanonicalId(id)) {
27102
+ const globalId = await traced("rpc anon.get_global_id", {}, () => {
27103
+ return anonStub(env, id).getGlobalId(currentTraceContext());
27104
+ });
27105
+ return { anonId: id, globalId };
27106
+ }
27107
+ const assignment = await getAssignment(env, classifyIdentifier(id));
27108
+ return { anonId: assignment?.anonId ?? null, globalId: assignment?.globalId ?? null };
27109
+ }, "lookupUser");
27266
27110
 
27267
27111
  // src/id-graph/person.ts
27268
27112
  var IDENTITY_CAPABILITY = "identity";
@@ -27377,86 +27221,348 @@ var parseCurrencyFeed = /* @__PURE__ */ __name((feed) => {
27377
27221
  if (rates === null) {
27378
27222
  return { ok: false, reason: "the rates literal does not parse" };
27379
27223
  }
27380
- if (rates["USD"] !== 1) {
27381
- return { ok: false, reason: "USD is not 1" };
27224
+ if (rates["USD"] !== 1) {
27225
+ return { ok: false, reason: "USD is not 1" };
27226
+ }
27227
+ const invalid = Object.entries(rates).find(([, rate]) => !Number.isFinite(rate) || rate <= 0);
27228
+ if (invalid !== void 0) {
27229
+ return { ok: false, reason: `${invalid[0]} is not a positive finite rate` };
27230
+ }
27231
+ const count = Object.keys(rates).length;
27232
+ if (count < MIN_CURRENCY_CODES) {
27233
+ return { ok: false, reason: `the feed has ${count} codes` };
27234
+ }
27235
+ return { ok: true, rates };
27236
+ }, "parseCurrencyFeed");
27237
+ var fetchCurrencyRates = /* @__PURE__ */ __name(async () => {
27238
+ return traced("currency.fetch", { attrs: { "http.url": CURRENCY_FEED_URL } }, async () => {
27239
+ let feed;
27240
+ try {
27241
+ const response = await fetch(CURRENCY_FEED_URL, {
27242
+ signal: AbortSignal.timeout(FEED_TIMEOUT_MS),
27243
+ cf: { cacheTtlByStatus: { "200-299": FEED_EDGE_CACHE_TTL_S, "300-599": 0 } }
27244
+ });
27245
+ if (!response.ok) {
27246
+ addSpanEvent("currency fetch failed", { level: "error", attrs: { status: response.status } });
27247
+ return null;
27248
+ }
27249
+ feed = await response.text();
27250
+ } catch (error61) {
27251
+ addSpanEvent("currency fetch failed", { level: "error", attrs: { error: String(error61) } });
27252
+ return null;
27253
+ }
27254
+ const verdict = parseCurrencyFeed(feed);
27255
+ if (!verdict.ok) {
27256
+ addSpanEvent("currency feed rejected", { level: "error", attrs: { reason: verdict.reason } });
27257
+ return null;
27258
+ }
27259
+ return verdict.rates;
27260
+ });
27261
+ }, "fetchCurrencyRates");
27262
+ var refreshCurrencyRates = /* @__PURE__ */ __name(async () => {
27263
+ const rates = await fetchCurrencyRates();
27264
+ const now = Date.now();
27265
+ if (rates === null) {
27266
+ const current2 = currencyMemo?.current ?? null;
27267
+ currencyMemo = { current: current2, nextAttemptAt: now + RETRY_AFTER_FAILURE_MS };
27268
+ return current2;
27269
+ }
27270
+ const current = { rates, fetchedAt: now };
27271
+ currencyMemo = { current, nextAttemptAt: now + RATES_TTL_MS };
27272
+ return current;
27273
+ }, "refreshCurrencyRates");
27274
+ var currentCurrencyRates = /* @__PURE__ */ __name(async (ctx) => {
27275
+ const now = Date.now();
27276
+ if (currencyMemo !== void 0 && now < currencyMemo.nextAttemptAt) {
27277
+ return currencyMemo.current;
27278
+ }
27279
+ const stale = currencyMemo?.current ?? null;
27280
+ if (stale === null) {
27281
+ return refreshCurrencyRates();
27282
+ }
27283
+ currencyMemo = { current: stale, nextAttemptAt: now + RETRY_AFTER_FAILURE_MS };
27284
+ ctx.waitUntil(
27285
+ runInvocation({ name: "currency.refresh", trace: currentTraceContext() }, () => {
27286
+ return refreshCurrencyRates();
27287
+ })
27288
+ );
27289
+ return stale;
27290
+ }, "currentCurrencyRates");
27291
+
27292
+ // src/apps/broker-currency.ts
27293
+ var currencyRatesBody = external_exports.object({
27294
+ ...identityFields,
27295
+ kind: external_exports.literal("currencyRates")
27296
+ }).strict();
27297
+ var handleCurrencyRates = /* @__PURE__ */ __name(async (ctx) => {
27298
+ const current = await currentCurrencyRates(ctx);
27299
+ if (current === null) {
27300
+ return Response.json({ error: "currency_rates_unavailable" }, { status: 503 });
27301
+ }
27302
+ return Response.json(current);
27303
+ }, "handleCurrencyRates");
27304
+
27305
+ // ../../libs/cf-data/src/cf-data.ts
27306
+ var CF_DATA_HEADER = "x-cf-data";
27307
+ var cfDataFrom = /* @__PURE__ */ __name((cf) => {
27308
+ return {
27309
+ country: cf?.country,
27310
+ city: cf?.city,
27311
+ region: cf?.region,
27312
+ regionCode: cf?.regionCode,
27313
+ timezone: cf?.timezone,
27314
+ postalCode: cf?.postalCode,
27315
+ continent: cf?.continent,
27316
+ longitude: cf?.longitude,
27317
+ latitude: cf?.latitude,
27318
+ isEUCountry: cf?.isEUCountry,
27319
+ botManagement: {
27320
+ ja4: cf?.botManagement?.ja4,
27321
+ ja3Hash: cf?.botManagement?.ja3Hash,
27322
+ score: cf?.botManagement?.score,
27323
+ verifiedBot: cf?.botManagement?.verifiedBot
27324
+ }
27325
+ };
27326
+ }, "cfDataFrom");
27327
+ var parseCfData = /* @__PURE__ */ __name((raw2) => {
27328
+ if (raw2 === null) {
27329
+ return void 0;
27330
+ }
27331
+ let parsed;
27332
+ try {
27333
+ parsed = JSON.parse(raw2);
27334
+ } catch {
27335
+ return void 0;
27336
+ }
27337
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
27338
+ return void 0;
27339
+ }
27340
+ return parsed;
27341
+ }, "parseCfData");
27342
+
27343
+ // src/helpers/geo.ts
27344
+ var UNDETERMINED_COUNTRIES = /* @__PURE__ */ new Set(["XX", "T1"]);
27345
+ var detectedCountry = /* @__PURE__ */ __name((cf) => {
27346
+ const country = cf?.country;
27347
+ return country && !UNDETERMINED_COUNTRIES.has(country) ? country : void 0;
27348
+ }, "detectedCountry");
27349
+ var euCountry = /* @__PURE__ */ __name((cf, country) => {
27350
+ if (cf?.isEUCountry === "1") {
27351
+ return "yes";
27352
+ }
27353
+ return country ? "no" : "unknown";
27354
+ }, "euCountry");
27355
+ var withForwardedCf = /* @__PURE__ */ __name((request) => {
27356
+ if (request.cf !== void 0) {
27357
+ return request;
27358
+ }
27359
+ const cf = parseCfData(request.headers.get(CF_DATA_HEADER));
27360
+ return cf === void 0 ? request : new Request(request, { cf });
27361
+ }, "withForwardedCf");
27362
+ var loggableGeo = /* @__PURE__ */ __name((geo) => {
27363
+ const fields = {};
27364
+ for (const [key, value] of Object.entries(geo)) {
27365
+ if (value !== void 0) {
27366
+ fields[key] = String(value);
27367
+ }
27368
+ }
27369
+ return fields;
27370
+ }, "loggableGeo");
27371
+ var hostFromRequest = /* @__PURE__ */ __name((request) => {
27372
+ return {
27373
+ ip: request.headers.get("cf-connecting-ip") ?? void 0,
27374
+ userAgent: request.headers.get("user-agent") ?? void 0,
27375
+ locale: request.headers.get("accept-language")?.split(",")[0]?.trim() || void 0
27376
+ };
27377
+ }, "hostFromRequest");
27378
+ var geoFromCf = /* @__PURE__ */ __name((cf) => {
27379
+ const bot = cf?.botManagement;
27380
+ const country = detectedCountry(cf);
27381
+ return {
27382
+ timezone: cf?.timezone ?? void 0,
27383
+ city: cf?.city ?? void 0,
27384
+ postalCode: cf?.postalCode ?? void 0,
27385
+ region: cf?.region ?? void 0,
27386
+ regionCode: cf?.regionCode ?? void 0,
27387
+ country,
27388
+ continent: cf?.continent ?? void 0,
27389
+ isEUCountry: euCountry(cf, country),
27390
+ latitude: cf?.latitude ?? void 0,
27391
+ longitude: cf?.longitude ?? void 0,
27392
+ ja4: bot?.ja4,
27393
+ ja3: bot?.ja3Hash,
27394
+ botScore: bot?.score
27395
+ };
27396
+ }, "geoFromCf");
27397
+ var geoFromRequest = /* @__PURE__ */ __name((request) => {
27398
+ return {
27399
+ ...hostFromRequest(request),
27400
+ ...geoFromCf(request.cf)
27401
+ };
27402
+ }, "geoFromRequest");
27403
+
27404
+ // src/id-graph/traits.ts
27405
+ var PII_KEYS = ["first_name", "last_name", "email", "phone", "country", "state", "city", "postal_code"];
27406
+ var snakeCase = /* @__PURE__ */ __name((key) => {
27407
+ return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
27408
+ }, "snakeCase");
27409
+ var stringifyTrait = /* @__PURE__ */ __name((value) => {
27410
+ return typeof value === "string" ? value : JSON.stringify(value);
27411
+ }, "stringifyTrait");
27412
+ var piiTrait = /* @__PURE__ */ __name((key, rawValue) => {
27413
+ if (key !== "email" && key !== "phone") {
27414
+ return stringifyTrait(rawValue);
27415
+ }
27416
+ if (typeof rawValue !== "string") {
27417
+ return void 0;
27418
+ }
27419
+ return (key === "email" ? emailIdentifier(rawValue) : phoneIdentifier(rawValue))?.value;
27420
+ }, "piiTrait");
27421
+ var resolvePiiTrait = /* @__PURE__ */ __name((key, rawValue, unset) => {
27422
+ return unset ? null : piiTrait(key, rawValue);
27423
+ }, "resolvePiiTrait");
27424
+ var classifyTraits = /* @__PURE__ */ __name((traits, observedAt) => {
27425
+ const classified = { pii: {}, custom: {}, latest: [], dropped: [] };
27426
+ for (const [rawKey, rawValue] of Object.entries(traits)) {
27427
+ const key = snakeCase(rawKey);
27428
+ const unset = rawValue === null || rawValue === void 0;
27429
+ if (PII_KEYS.includes(key)) {
27430
+ const value2 = resolvePiiTrait(key, rawValue, unset);
27431
+ if (value2 === void 0) {
27432
+ classified.dropped.push(key);
27433
+ continue;
27434
+ }
27435
+ classified.pii[key] = value2;
27436
+ classified.latest.push({ key: `pii.${key}`, value: value2, observedAt });
27437
+ continue;
27438
+ }
27439
+ const value = unset ? null : stringifyTrait(rawValue);
27440
+ classified.custom[key] = value;
27441
+ classified.latest.push({ key: `custom.${key}`, value, observedAt });
27442
+ }
27443
+ return classified;
27444
+ }, "classifyTraits");
27445
+ var conversionFromTrack = /* @__PURE__ */ __name((payload) => {
27446
+ const amount = payload.properties["value"];
27447
+ if (typeof amount !== "number" || !Number.isFinite(amount)) {
27448
+ return void 0;
27449
+ }
27450
+ const orderId = payload.properties["orderId"];
27451
+ if (typeof orderId !== "string" || orderId === "") {
27452
+ return void 0;
27453
+ }
27454
+ const currency = payload.properties["currency"];
27455
+ const contents = payload.properties["contents"];
27456
+ return {
27457
+ eventName: payload.event,
27458
+ eventId: payload.eventId,
27459
+ orderId,
27460
+ amount,
27461
+ currency: typeof currency === "string" ? currency : null,
27462
+ contents: contents === void 0 ? null : JSON.stringify(contents)
27463
+ };
27464
+ }, "conversionFromTrack");
27465
+
27466
+ // src/id-graph/noop.ts
27467
+ var isNoopConsent = /* @__PURE__ */ __name((payload) => {
27468
+ return consentEntries(payload, 0).length === 0;
27469
+ }, "isNoopConsent");
27470
+ var isNoopIdentify = /* @__PURE__ */ __name((traits) => {
27471
+ return Object.keys(traits).length === 0;
27472
+ }, "isNoopIdentify");
27473
+ var isNoopSubmission = /* @__PURE__ */ __name((userId, args) => {
27474
+ if (args.type === "consent") {
27475
+ return isNoopConsent(args.payload);
27476
+ }
27477
+ if (args.type === "identify") {
27478
+ return isCanonicalId(userId) && isNoopIdentify(args.payload.traits);
27479
+ }
27480
+ return false;
27481
+ }, "isNoopSubmission");
27482
+ var isNoopAction = /* @__PURE__ */ __name((action) => {
27483
+ if (action.identifier !== void 0) {
27484
+ return false;
27382
27485
  }
27383
- const invalid = Object.entries(rates).find(([, rate]) => !Number.isFinite(rate) || rate <= 0);
27384
- if (invalid !== void 0) {
27385
- return { ok: false, reason: `${invalid[0]} is not a positive finite rate` };
27486
+ return action.type === "consent" && isNoopConsent(action.payload);
27487
+ }, "isNoopAction");
27488
+ var isDeliveryOnlyAction = /* @__PURE__ */ __name((action) => {
27489
+ if (action.identifier !== void 0 || action.type === "consent") {
27490
+ return false;
27386
27491
  }
27387
- const count = Object.keys(rates).length;
27388
- if (count < MIN_CURRENCY_CODES) {
27389
- return { ok: false, reason: `the feed has ${count} codes` };
27492
+ return action.type === "track" ? conversionFromTrack(action.payload) === void 0 : isNoopIdentify(action.payload.traits);
27493
+ }, "isDeliveryOnlyAction");
27494
+
27495
+ // src/id-graph/action.ts
27496
+ var UnknownUserError = class extends Error {
27497
+ static {
27498
+ __name(this, "UnknownUserError");
27390
27499
  }
27391
- return { ok: true, rates };
27392
- }, "parseCurrencyFeed");
27393
- var fetchCurrencyRates = /* @__PURE__ */ __name(async () => {
27394
- return traced("currency.fetch", { attrs: { "http.url": CURRENCY_FEED_URL } }, async () => {
27395
- let feed;
27396
- try {
27397
- const response = await fetch(CURRENCY_FEED_URL, {
27398
- signal: AbortSignal.timeout(FEED_TIMEOUT_MS),
27399
- cf: { cacheTtlByStatus: { "200-299": FEED_EDGE_CACHE_TTL_S, "300-599": 0 } }
27400
- });
27401
- if (!response.ok) {
27402
- addSpanEvent("currency fetch failed", { level: "error", attrs: { status: response.status } });
27403
- return null;
27404
- }
27405
- feed = await response.text();
27406
- } catch (error61) {
27407
- addSpanEvent("currency fetch failed", { level: "error", attrs: { error: String(error61) } });
27408
- return null;
27500
+ constructor() {
27501
+ super("the identifier resolves to no user");
27502
+ this.name = "UnknownUserError";
27503
+ }
27504
+ };
27505
+ var mintActionId = /* @__PURE__ */ __name(() => {
27506
+ return `act_${ulid3()}`;
27507
+ }, "mintActionId");
27508
+ var buildAction = /* @__PURE__ */ __name((args) => {
27509
+ const receivedAt = args.receivedAt ?? Date.now();
27510
+ return {
27511
+ ...args,
27512
+ actionId: mintActionId(),
27513
+ receivedAt
27514
+ };
27515
+ }, "buildAction");
27516
+ var ingest = /* @__PURE__ */ __name(async (env, action, site = ownSite(env)) => {
27517
+ await traced("rpc anon.ingest", {}, () => {
27518
+ return anonStub(env, action.userId).ingest(action, site, currentTraceContext());
27519
+ });
27520
+ }, "ingest");
27521
+ var serverActionContext = /* @__PURE__ */ __name((input2, receivedAt) => {
27522
+ const network = input2?.network;
27523
+ return {
27524
+ source: "server",
27525
+ occurredAt: input2?.occurredAt ?? receivedAt,
27526
+ ...input2?.page === void 0 ? {} : { page: input2.page },
27527
+ ...network === void 0 || network.ip === void 0 && network.userAgent === void 0 && network.locale === void 0 ? {} : { network: { ...network, provenance: "caller" } },
27528
+ ...input2?.session === void 0 ? {} : { session: input2.session },
27529
+ offline: input2?.offline ?? false,
27530
+ replay: input2?.replay ?? false
27531
+ };
27532
+ }, "serverActionContext");
27533
+ var submitAction = /* @__PURE__ */ __name(async (env, userId, args, request) => {
27534
+ const attrs = {
27535
+ "action.type": args.type,
27536
+ "action.payload": JSON.stringify(args.payload),
27537
+ "action.identifier": userId,
27538
+ ...args.type === "track" ? { "action.event": args.payload.event } : {}
27539
+ };
27540
+ return traced(`action ${args.type}`, { attrs }, async () => {
27541
+ if (isNoopSubmission(userId, args)) {
27542
+ setSpanAttributes({ "action.noop": true, ...isCanonicalId(userId) ? { "action.user_id": userId } : {} });
27543
+ return mintActionId();
27409
27544
  }
27410
- const verdict = parseCurrencyFeed(feed);
27411
- if (!verdict.ok) {
27412
- addSpanEvent("currency feed rejected", { level: "error", attrs: { reason: verdict.reason } });
27413
- return null;
27545
+ const resolved = await traced("resolve_user", {}, () => {
27546
+ return args.type === "track" ? resolveExistingUser(env, userId) : resolveUser(env, userId);
27547
+ });
27548
+ if (resolved === null) {
27549
+ throw new UnknownUserError();
27414
27550
  }
27415
- return verdict.rates;
27551
+ const receivedAt = Date.now();
27552
+ const action = buildAction({
27553
+ ...args,
27554
+ userId: resolved.id,
27555
+ identifier: resolved.identifier,
27556
+ receivedAt,
27557
+ context: serverActionContext(args.context, receivedAt),
27558
+ geo: request?.cf,
27559
+ host: request === void 0 ? void 0 : hostFromRequest(request)
27560
+ });
27561
+ setSpanAttributes({ "action.user_id": resolved.id, "action.id": action.actionId });
27562
+ await ingest(env, action);
27563
+ return action.actionId;
27416
27564
  });
27417
- }, "fetchCurrencyRates");
27418
- var refreshCurrencyRates = /* @__PURE__ */ __name(async () => {
27419
- const rates = await fetchCurrencyRates();
27420
- const now = Date.now();
27421
- if (rates === null) {
27422
- const current2 = currencyMemo?.current ?? null;
27423
- currencyMemo = { current: current2, nextAttemptAt: now + RETRY_AFTER_FAILURE_MS };
27424
- return current2;
27425
- }
27426
- const current = { rates, fetchedAt: now };
27427
- currencyMemo = { current, nextAttemptAt: now + RATES_TTL_MS };
27428
- return current;
27429
- }, "refreshCurrencyRates");
27430
- var currentCurrencyRates = /* @__PURE__ */ __name(async (ctx) => {
27431
- const now = Date.now();
27432
- if (currencyMemo !== void 0 && now < currencyMemo.nextAttemptAt) {
27433
- return currencyMemo.current;
27434
- }
27435
- const stale = currencyMemo?.current ?? null;
27436
- if (stale === null) {
27437
- return refreshCurrencyRates();
27438
- }
27439
- currencyMemo = { current: stale, nextAttemptAt: now + RETRY_AFTER_FAILURE_MS };
27440
- ctx.waitUntil(
27441
- runInvocation({ name: "currency.refresh", trace: currentTraceContext() }, () => {
27442
- return refreshCurrencyRates();
27443
- })
27444
- );
27445
- return stale;
27446
- }, "currentCurrencyRates");
27447
-
27448
- // src/apps/broker-currency.ts
27449
- var currencyRatesBody = external_exports.object({
27450
- ...identityFields,
27451
- kind: external_exports.literal("currencyRates")
27452
- }).strict();
27453
- var handleCurrencyRates = /* @__PURE__ */ __name(async (ctx) => {
27454
- const current = await currentCurrencyRates(ctx);
27455
- if (current === null) {
27456
- return Response.json({ error: "currency_rates_unavailable" }, { status: 503 });
27457
- }
27458
- return Response.json(current);
27459
- }, "handleCurrencyRates");
27565
+ }, "submitAction");
27460
27566
 
27461
27567
  // src/id-graph/upsert.ts
27462
27568
  var identifierValue = external_exports.string().min(1).max(256).optional();
@@ -27544,35 +27650,249 @@ var rateLimited = /* @__PURE__ */ __name(async (env, key, binding = "BROKER_RATE
27544
27650
  }
27545
27651
  return false;
27546
27652
  }
27547
- const outcome = await limiter.limit({ key });
27548
- return !outcome.success;
27549
- }, "rateLimited");
27653
+ const outcome = await limiter.limit({ key });
27654
+ return !outcome.success;
27655
+ }, "rateLimited");
27656
+
27657
+ // src/apps/broker-identity.ts
27658
+ var identityUpsertBody = external_exports.object({
27659
+ ...identityFields,
27660
+ kind: external_exports.literal("identityUpsert"),
27661
+ person: personUpsertSchema
27662
+ }).strict();
27663
+ var identityGranted = /* @__PURE__ */ __name((slug) => {
27664
+ return siteRegistry().registry.apps[slug]?.data.includes(IDENTITY_CAPABILITY) === true;
27665
+ }, "identityGranted");
27666
+ var identityPermissionDenied = /* @__PURE__ */ __name(() => {
27667
+ return Response.json({ error: "identity_permission_denied" }, { status: 403 });
27668
+ }, "identityPermissionDenied");
27669
+ var handleIdentityUpsert = /* @__PURE__ */ __name(async (env, body) => {
27670
+ if (!identityGranted(body.slug)) {
27671
+ return identityPermissionDenied();
27672
+ }
27673
+ if (await rateLimited(env, `${body.slug}::identity-upsert`)) {
27674
+ return Response.json({ error: "rate_limited" }, { status: 429 });
27675
+ }
27676
+ const upserted = await upsertPerson(env, body.person);
27677
+ if (upserted === null) {
27678
+ return Response.json({ error: "invalid_identifier" }, { status: 400 });
27679
+ }
27680
+ return Response.json(upserted);
27681
+ }, "handleIdentityUpsert");
27682
+
27683
+ // ../../libs/app-ui-token/src/base64url.ts
27684
+ var base64UrlEncode = /* @__PURE__ */ __name((bytes) => {
27685
+ let binary = "";
27686
+ for (const byte of bytes) {
27687
+ binary += String.fromCharCode(byte);
27688
+ }
27689
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
27690
+ }, "base64UrlEncode");
27691
+ var base64UrlDecode = /* @__PURE__ */ __name((text) => {
27692
+ const padded = text.replaceAll("-", "+").replaceAll("_", "/").padEnd(text.length + (4 - text.length % 4) % 4, "=");
27693
+ let binary;
27694
+ try {
27695
+ binary = atob(padded);
27696
+ } catch {
27697
+ return null;
27698
+ }
27699
+ const bytes = new Uint8Array(binary.length);
27700
+ for (let index = 0; index < binary.length; index += 1) {
27701
+ bytes[index] = binary.charCodeAt(index);
27702
+ }
27703
+ return bytes;
27704
+ }, "base64UrlDecode");
27705
+
27706
+ // ../../libs/app-ui-token/src/env.ts
27707
+ var envSchema = external_exports.object({
27708
+ v: external_exports.literal(1),
27709
+ keys: external_exports.record(external_exports.string().min(1).max(16), external_exports.string().min(1))
27710
+ }).strict();
27711
+ var parseAppUiKeyEnv = /* @__PURE__ */ __name((raw2) => {
27712
+ let json2;
27713
+ try {
27714
+ json2 = JSON.parse(raw2);
27715
+ } catch {
27716
+ return null;
27717
+ }
27718
+ const parsed = envSchema.safeParse(json2);
27719
+ if (!parsed.success) {
27720
+ return null;
27721
+ }
27722
+ const keys = {};
27723
+ for (const [kid, encoded] of Object.entries(parsed.data.keys)) {
27724
+ const bytes = base64UrlDecode(encoded);
27725
+ if (bytes === null) {
27726
+ return null;
27727
+ }
27728
+ keys[kid] = bytes;
27729
+ }
27730
+ return keys;
27731
+ }, "parseAppUiKeyEnv");
27732
+
27733
+ // ../../libs/app-ui-token/src/jwt.ts
27734
+ var headerSchema = external_exports.object({
27735
+ alg: external_exports.literal("HS256"),
27736
+ typ: external_exports.literal("JWT"),
27737
+ kid: external_exports.string().min(1).max(16)
27738
+ }).strict();
27739
+ var encodeJsonPart = /* @__PURE__ */ __name((value) => {
27740
+ return base64UrlEncode(new TextEncoder().encode(JSON.stringify(value)));
27741
+ }, "encodeJsonPart");
27742
+ var parseJsonPart = /* @__PURE__ */ __name((part) => {
27743
+ const bytes = base64UrlDecode(part);
27744
+ if (bytes === null) {
27745
+ return null;
27746
+ }
27747
+ try {
27748
+ return JSON.parse(new TextDecoder().decode(bytes));
27749
+ } catch {
27750
+ return null;
27751
+ }
27752
+ }, "parseJsonPart");
27753
+ var hmacKey = /* @__PURE__ */ __name((key, usage) => {
27754
+ return crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" }, false, [usage]);
27755
+ }, "hmacKey");
27756
+ var signCompact = /* @__PURE__ */ __name(async (kid, key, claims) => {
27757
+ const signingInput = `${encodeJsonPart({ alg: "HS256", typ: "JWT", kid })}.${encodeJsonPart(claims)}`;
27758
+ const signature = await crypto.subtle.sign("HMAC", await hmacKey(key, "sign"), new TextEncoder().encode(signingInput));
27759
+ return `${signingInput}.${base64UrlEncode(new Uint8Array(signature))}`;
27760
+ }, "signCompact");
27761
+ var verifyCompactSignature = /* @__PURE__ */ __name(async (token, keys) => {
27762
+ const [headerPart, claimsPart, signaturePart, extra] = token.split(".");
27763
+ if (headerPart === void 0 || claimsPart === void 0 || signaturePart === void 0 || extra !== void 0) {
27764
+ return { ok: false, reason: "malformed" };
27765
+ }
27766
+ const header = headerSchema.safeParse(parseJsonPart(headerPart));
27767
+ if (!header.success) {
27768
+ return { ok: false, reason: "malformed" };
27769
+ }
27770
+ const key = keys[header.data.kid];
27771
+ if (key === void 0) {
27772
+ return { ok: false, reason: "unknown_kid" };
27773
+ }
27774
+ const signature = base64UrlDecode(signaturePart);
27775
+ if (signature === null) {
27776
+ return { ok: false, reason: "malformed" };
27777
+ }
27778
+ const signed = await crypto.subtle.verify(
27779
+ "HMAC",
27780
+ await hmacKey(key, "verify"),
27781
+ signature,
27782
+ new TextEncoder().encode(`${headerPart}.${claimsPart}`)
27783
+ );
27784
+ if (!signed) {
27785
+ return { ok: false, reason: "bad_signature" };
27786
+ }
27787
+ return { ok: true, claims: parseJsonPart(claimsPart), kid: header.data.kid, key };
27788
+ }, "verifyCompactSignature");
27550
27789
 
27551
- // src/apps/broker-identity.ts
27552
- var identityUpsertBody = external_exports.object({
27553
- ...identityFields,
27554
- kind: external_exports.literal("identityUpsert"),
27555
- person: personUpsertSchema
27790
+ // ../../libs/app-ui-token/src/token.ts
27791
+ var APP_UI_TOKEN_SKEW_SECONDS = 60;
27792
+ var APP_UI_TOKEN_ISSUER = "qinto-portal";
27793
+ var claimsSchema = external_exports.object({
27794
+ iss: external_exports.literal(APP_UI_TOKEN_ISSUER),
27795
+ aud: external_exports.string().min(1),
27796
+ sub: external_exports.string().min(1),
27797
+ userId: external_exports.string().min(1),
27798
+ iat: external_exports.number().int(),
27799
+ exp: external_exports.number().int(),
27800
+ domainId: external_exports.string().min(1),
27801
+ appId: external_exports.string().min(1),
27802
+ appInstallId: external_exports.string().min(1),
27803
+ appVersionId: external_exports.string().min(1),
27804
+ access: external_exports.enum(["read", "write"]),
27805
+ // Absent means `full`; see the mint below.
27806
+ appRole: external_exports.enum(APP_ROLES).optional()
27556
27807
  }).strict();
27557
- var identityGranted = /* @__PURE__ */ __name((slug) => {
27558
- return siteRegistry().registry.apps[slug]?.data.includes(IDENTITY_CAPABILITY) === true;
27559
- }, "identityGranted");
27560
- var identityPermissionDenied = /* @__PURE__ */ __name(() => {
27561
- return Response.json({ error: "identity_permission_denied" }, { status: 403 });
27562
- }, "identityPermissionDenied");
27563
- var handleIdentityUpsert = /* @__PURE__ */ __name(async (env, body) => {
27564
- if (!identityGranted(body.slug)) {
27565
- return identityPermissionDenied();
27808
+ var appUiAudience = /* @__PURE__ */ __name((slug) => {
27809
+ return `app-ui:${slug}`;
27810
+ }, "appUiAudience");
27811
+ var verifyAppUiToken = /* @__PURE__ */ __name(async (input2) => {
27812
+ const signature = await verifyCompactSignature(input2.token, input2.keys);
27813
+ if (!signature.ok) {
27814
+ return signature;
27566
27815
  }
27567
- if (await rateLimited(env, `${body.slug}::identity-upsert`)) {
27568
- return Response.json({ error: "rate_limited" }, { status: 429 });
27816
+ const claims = claimsSchema.safeParse(signature.claims);
27817
+ if (!claims.success) {
27818
+ return { ok: false, reason: "bad_claims" };
27569
27819
  }
27570
- const upserted = await upsertPerson(env, body.person);
27571
- if (upserted === null) {
27572
- return Response.json({ error: "invalid_identifier" }, { status: 400 });
27820
+ if (claims.data.aud !== appUiAudience(input2.slug)) {
27821
+ return { ok: false, reason: "wrong_app" };
27573
27822
  }
27574
- return Response.json(upserted);
27575
- }, "handleIdentityUpsert");
27823
+ const now = Math.floor(input2.now.getTime() / 1e3);
27824
+ if (claims.data.iat > now + (input2.skewSeconds ?? APP_UI_TOKEN_SKEW_SECONDS)) {
27825
+ return { ok: false, reason: "future_iat" };
27826
+ }
27827
+ if (claims.data.exp <= now) {
27828
+ return { ok: false, reason: "expired" };
27829
+ }
27830
+ return { ok: true, claims: claims.data, kid: signature.kid, key: signature.key };
27831
+ }, "verifyAppUiToken");
27832
+
27833
+ // ../../libs/app-ui-token/src/runtime-query-ticket.ts
27834
+ var APP_RUNTIME_QUERY_TICKET_TTL_SECONDS = 600;
27835
+ var APP_RUNTIME_QUERY_TICKET_ISSUER = "qinto-portal";
27836
+ var claimsSchema2 = external_exports.object({
27837
+ iss: external_exports.literal(APP_RUNTIME_QUERY_TICKET_ISSUER),
27838
+ aud: external_exports.string().min(1),
27839
+ sub: external_exports.string().min(1),
27840
+ iat: external_exports.number().int(),
27841
+ exp: external_exports.number().int(),
27842
+ domainId: external_exports.string().min(1),
27843
+ appId: external_exports.string().min(1),
27844
+ appInstallId: external_exports.string().min(1),
27845
+ appVersionId: external_exports.string().min(1)
27846
+ }).strict();
27847
+ var appRuntimeQueryAudience = /* @__PURE__ */ __name((slug) => {
27848
+ return `app-runtime-query:${slug}`;
27849
+ }, "appRuntimeQueryAudience");
27850
+ var mintAppRuntimeQueryTicket = /* @__PURE__ */ __name(async (input2) => {
27851
+ const iat = Math.floor(input2.now.getTime() / 1e3);
27852
+ const claims = {
27853
+ iss: APP_RUNTIME_QUERY_TICKET_ISSUER,
27854
+ aud: appRuntimeQueryAudience(input2.slug),
27855
+ sub: input2.appInstallId,
27856
+ iat,
27857
+ exp: iat + (input2.ttlSeconds ?? APP_RUNTIME_QUERY_TICKET_TTL_SECONDS),
27858
+ domainId: input2.domainId,
27859
+ appId: input2.appId,
27860
+ appInstallId: input2.appInstallId,
27861
+ appVersionId: input2.appVersionId
27862
+ };
27863
+ return signCompact(input2.kid, input2.key, claims);
27864
+ }, "mintAppRuntimeQueryTicket");
27865
+
27866
+ // ../../libs/app-ui-token/src/write-ticket.ts
27867
+ var APP_UI_WRITE_TICKET_TTL_SECONDS = 60;
27868
+ var APP_UI_WRITE_TICKET_ISSUER = "qinto-site";
27869
+ var claimsSchema3 = external_exports.object({
27870
+ iss: external_exports.literal(APP_UI_WRITE_TICKET_ISSUER),
27871
+ aud: external_exports.string().min(1),
27872
+ sub: external_exports.string().min(1),
27873
+ iat: external_exports.number().int(),
27874
+ exp: external_exports.number().int(),
27875
+ domainId: external_exports.string().min(1),
27876
+ appId: external_exports.string().min(1),
27877
+ appVersionId: external_exports.string().min(1)
27878
+ }).strict();
27879
+ var appUiWriteAudience = /* @__PURE__ */ __name((slug) => {
27880
+ return `app-ui-write:${slug}`;
27881
+ }, "appUiWriteAudience");
27882
+ var mintAppUiWriteTicket = /* @__PURE__ */ __name(async (input2) => {
27883
+ const iat = Math.floor(input2.now.getTime() / 1e3);
27884
+ const claims = {
27885
+ iss: APP_UI_WRITE_TICKET_ISSUER,
27886
+ aud: appUiWriteAudience(input2.slug),
27887
+ sub: input2.userId,
27888
+ iat,
27889
+ exp: iat + (input2.ttlSeconds ?? APP_UI_WRITE_TICKET_TTL_SECONDS),
27890
+ domainId: input2.domainId,
27891
+ appId: input2.appId,
27892
+ appVersionId: input2.appVersionId
27893
+ };
27894
+ return signCompact(input2.kid, input2.key, claims);
27895
+ }, "mintAppUiWriteTicket");
27576
27896
 
27577
27897
  // src/apps/emit-payload.ts
27578
27898
  var emitPayloadSchema = external_exports.object({
@@ -27591,51 +27911,6 @@ var emittedEventId = /* @__PURE__ */ __name((eventId, properties2) => {
27591
27911
  return `evt_${ulid3()}`;
27592
27912
  }, "emittedEventId");
27593
27913
 
27594
- // src/apps/broker-track.ts
27595
- var trackBody = external_exports.object({
27596
- ...identityFields,
27597
- kind: external_exports.literal("track"),
27598
- source: external_exports.enum(APP_HANDLER_SOURCES),
27599
- userId: external_exports.string().min(1).max(64),
27600
- installId: external_exports.string().min(1).max(64),
27601
- installs: installRuleSchema.optional(),
27602
- event: emitSchema.omit({ installs: true })
27603
- }).strict();
27604
- var handleTrack = /* @__PURE__ */ __name(async (env, body) => {
27605
- const app2 = siteRegistry().registry.apps[body.slug];
27606
- if (app2 === void 0) {
27607
- return Response.json({ error: "forbidden" }, { status: 403 });
27608
- }
27609
- if (!app2.installs.some((install) => install.active && install.id === body.installId)) {
27610
- return Response.json({ error: "invalid_install" }, { status: 400 });
27611
- }
27612
- const parsed = emitPayloadSchema.safeParse(body.event.payload);
27613
- if (!parsed.success) {
27614
- return Response.json({ error: "invalid_event" }, { status: 400 });
27615
- }
27616
- if (!isCanonicalId(body.userId)) {
27617
- return Response.json({ error: "invalid_user" }, { status: 400 });
27618
- }
27619
- if (await rateLimited(env, `${body.slug}:${body.installId}:track`)) {
27620
- return Response.json({ error: "rate_limited" }, { status: 429 });
27621
- }
27622
- const site = ownSite(env);
27623
- if (!await anonStub(env, body.userId).appAllowed(body.slug, site, currentTraceContext())) {
27624
- return Response.json({ error: "forbidden" }, { status: 403 });
27625
- }
27626
- const { event, eventId, properties: properties2 } = parsed.data;
27627
- const action = buildAction({
27628
- type: "track",
27629
- userId: body.userId,
27630
- source: body.source,
27631
- payload: { event, eventId: emittedEventId(eventId, properties2), properties: properties2 ?? {} },
27632
- context: { source: "app", occurredAt: body.event.occurredAt, offline: false, replay: false },
27633
- ...body.installs === void 0 ? {} : { installs: body.installs }
27634
- });
27635
- await ingest(env, action, site);
27636
- return Response.json({ accepted: true }, { status: 202 });
27637
- }, "handleTrack");
27638
-
27639
27914
  // src/id-graph/anon/context.ts
27640
27915
  var LEVEL_KINDS = {
27641
27916
  private: { kv: "context_private_kv", text: "context_private_text" },
@@ -28125,7 +28400,7 @@ var BOT_SCORE_MIN = 1;
28125
28400
  var BOT_SCORE_MAX = 99;
28126
28401
 
28127
28402
  // ../../libs/bot-protection/src/config.ts
28128
- var NO_PROTECTION = { minBotScore: null, countries: null };
28403
+ var NO_PROTECTION = { minBotScore: null, countries: null, blockVerifiedBots: false };
28129
28404
  var normalizeScore = /* @__PURE__ */ __name((value, problems) => {
28130
28405
  if (value === void 0 || value === null) {
28131
28406
  return null;
@@ -28159,6 +28434,13 @@ var normalizeCountries = /* @__PURE__ */ __name((value, problems) => {
28159
28434
  }
28160
28435
  return list.length === 0 ? null : { mode: record2.mode, list };
28161
28436
  }, "normalizeCountries");
28437
+ var normalizeBlockVerifiedBots = /* @__PURE__ */ __name((value, problems) => {
28438
+ if (value === void 0 || typeof value === "boolean") {
28439
+ return value ?? false;
28440
+ }
28441
+ problems.push("has a non-boolean blockVerifiedBots");
28442
+ return false;
28443
+ }, "normalizeBlockVerifiedBots");
28162
28444
  var parseBotProtection = /* @__PURE__ */ __name((raw2) => {
28163
28445
  if (raw2.trim() === "") {
28164
28446
  return { config: NO_PROTECTION };
@@ -28176,15 +28458,19 @@ var parseBotProtection = /* @__PURE__ */ __name((raw2) => {
28176
28458
  const problems = [];
28177
28459
  const config2 = {
28178
28460
  minBotScore: normalizeScore(record2.minBotScore, problems),
28179
- countries: normalizeCountries(record2.countries, problems)
28461
+ countries: normalizeCountries(record2.countries, problems),
28462
+ blockVerifiedBots: normalizeBlockVerifiedBots(record2.blockVerifiedBots, problems)
28180
28463
  };
28181
28464
  return problems.length > 0 ? { config: config2, error: `BOT_PROTECTION ${problems.join("; ")}` } : { config: config2 };
28182
28465
  }, "parseBotProtection");
28183
28466
  var isRequestBlocked = /* @__PURE__ */ __name((config2, signal) => {
28184
- if (config2.minBotScore !== null && signal.botScore !== void 0 && signal.botScore < config2.minBotScore) {
28467
+ if (isCountryBlocked(signal.country, signal.isEU ?? false, config2.countries)) {
28185
28468
  return true;
28186
28469
  }
28187
- return isCountryBlocked(signal.country, signal.isEU ?? false, config2.countries);
28470
+ if (signal.verifiedBot === true) {
28471
+ return config2.blockVerifiedBots;
28472
+ }
28473
+ return config2.minBotScore !== null && signal.botScore !== void 0 && signal.botScore < config2.minBotScore;
28188
28474
  }, "isRequestBlocked");
28189
28475
 
28190
28476
  // ../../libs/geo-filter/src/us-states.ts
@@ -28486,7 +28772,8 @@ var adoptAppSpans = /* @__PURE__ */ __name((response) => {
28486
28772
  }, "adoptAppSpans");
28487
28773
 
28488
28774
  // src/apps/invoke.ts
28489
- var APP_INVOCATION_TIMEOUT_MS = 1e4;
28775
+ var RPC_HOT_TIMEOUT_MS = 1e4;
28776
+ var RPC_COLD_TIMEOUT_MS = 4e4;
28490
28777
  var APP_HOOK_TIMEOUT_MS = 25e3;
28491
28778
  var MAX_EMIT_DEPTH = 2;
28492
28779
  var HOOK_URL = "https://app/__qinto/hook";
@@ -28583,7 +28870,7 @@ var buildEnvelope = /* @__PURE__ */ __name((target, trigger, snapshot, context)
28583
28870
  ...geoAllowed ? { geo: snapshot.userGeo } : {}
28584
28871
  },
28585
28872
  ...buckets.includes("consentState") ? { consent: { enabled: context.consentEnforced, categories: snapshot.consent.categories, apps: snapshot.consent.apps } } : {},
28586
- ...geoAllowed && currentGeo !== void 0 ? { geo: currentGeo } : {},
28873
+ ...geoAllowed && currentGeo !== void 0 ? { geo: cfDataFrom(currentGeo) } : {},
28587
28874
  ...trigger.kind === "connect" && buckets.includes("eventContext") ? connectContext(trigger) : {},
28588
28875
  // Not gated on a capabilities.data bucket: an app only ever sees what it captured or wrote.
28589
28876
  ...captures === void 0 ? {} : { captures },
@@ -28777,85 +29064,21 @@ var invokeApp = /* @__PURE__ */ __name(async (env, input2) => {
28777
29064
  );
28778
29065
  }, "invokeApp");
28779
29066
 
28780
- // src/apps/broker.ts
29067
+ // src/apps/broker-rpc.ts
28781
29068
  var MAX_RPC_DEPTH = 3;
28782
- var BROKER_PATHS = ["/__qinto/push", "/__qinto/rpc"];
28783
- var pushBody = external_exports.object({
28784
- ...identityFields,
28785
- kind: external_exports.literal("push"),
28786
- userId: external_exports.string().min(1).max(64),
28787
- installId: external_exports.string().min(1).max(64).optional(),
28788
- broadcast: external_exports.enum(PUSH_BROADCASTS).optional(),
28789
- topic: external_exports.string().min(1).max(64),
28790
- data: external_exports.unknown().optional()
28791
- }).strict();
28792
29069
  var rpcBody = external_exports.object({
28793
29070
  ...identityFields,
28794
29071
  kind: external_exports.literal("rpc"),
28795
29072
  // Absent when the caller is an endpoint, a schedule or a ui handler; the callee then sees no user.
28796
- userId: external_exports.string().min(1).max(64).optional(),
28797
- callee: external_exports.string().min(1).max(30),
28798
- fn: external_exports.string().min(1).max(64),
28799
- args: external_exports.unknown().optional(),
28800
- installId: external_exports.string().min(1).max(64).optional(),
28801
- depth: external_exports.number().int().min(0)
28802
- }).strict();
28803
- var installsBody = external_exports.object({
28804
- ...identityFields,
28805
- kind: external_exports.literal("installs"),
28806
- callee: external_exports.string().min(1).max(30)
28807
- }).strict();
28808
- var activeBody = external_exports.object({
28809
- ...identityFields,
28810
- kind: external_exports.literal("active"),
28811
- ord: external_exports.number().int().min(1),
28812
- userId: external_exports.string().min(1).max(64).optional()
28813
- }).strict();
28814
- var contextReadBody = external_exports.object({
28815
- ...identityFields,
28816
- kind: external_exports.literal("contextRead"),
28817
- userId: external_exports.string().min(1).max(64)
28818
- }).strict();
28819
- var contextWriteBody = external_exports.object({
28820
- ...identityFields,
28821
- kind: external_exports.literal("contextWrite"),
28822
- userId: external_exports.string().min(1).max(64),
28823
- writes: external_exports.array(contextWriteSchema).min(1).max(MAX_CONTEXT_WRITES_PER_INSTALL)
28824
- }).strict();
28825
- var factsQuery = external_exports.object({
28826
- kinds: external_exports.array(external_exports.enum(APP_FACT_KINDS)).min(1).max(APP_FACT_KINDS.length).optional(),
28827
- observedAtGte: external_exports.number().int().nonnegative().safe().optional(),
28828
- observedAtLt: external_exports.number().int().nonnegative().safe().optional(),
28829
- limit: external_exports.number().int().min(1).max(MAX_APP_FACTS_PER_PAGE).optional()
28830
- }).strict().refine((query) => query.observedAtGte === void 0 || query.observedAtLt === void 0 || query.observedAtGte < query.observedAtLt);
28831
- var factsReadBody = external_exports.object({
28832
- ...identityFields,
28833
- kind: external_exports.literal("factsRead"),
28834
- userId: external_exports.string().min(1).max(64),
28835
- query: factsQuery.optional(),
28836
- cursor: external_exports.string().min(1).max(4096).optional()
28837
- }).strict().refine((body) => body.query === void 0 !== (body.cursor === void 0));
28838
- var identityReadBody = external_exports.object({
28839
- ...identityFields,
28840
- kind: external_exports.literal("identityRead"),
28841
- identifier: identifierField2,
28842
- cursor: external_exports.string().regex(/^\d+$/).optional()
29073
+ userId: external_exports.string().min(1).max(64).optional(),
29074
+ callee: external_exports.string().min(1).max(30),
29075
+ fn: external_exports.string().min(1).max(64),
29076
+ args: external_exports.unknown().optional(),
29077
+ installId: external_exports.string().min(1).max(64).optional(),
29078
+ depth: external_exports.number().int().min(0),
29079
+ // Optional on the wire: a runtime predating traffic classes sends none, which means hot.
29080
+ traffic: external_exports.enum(TRAFFIC_CLASSES).optional()
28843
29081
  }).strict();
28844
- var brokerBody = external_exports.discriminatedUnion("kind", [
28845
- pushBody,
28846
- rpcBody,
28847
- installsBody,
28848
- activeBody,
28849
- contextReadBody,
28850
- contextWriteBody,
28851
- factsReadBody,
28852
- identityReadBody,
28853
- identityUpsertBody,
28854
- currencyRatesBody,
28855
- trackBody
28856
- ]);
28857
- var BODY_ENVELOPE_ALLOWANCE_BYTES = 2 * 1024;
28858
- var MAX_BODY_BYTES = MAX_RPC_BYTES + BODY_ENVELOPE_ALLOWANCE_BYTES;
28859
29082
  var parseJson = /* @__PURE__ */ __name((raw2) => {
28860
29083
  try {
28861
29084
  return JSON.parse(raw2);
@@ -28863,85 +29086,6 @@ var parseJson = /* @__PURE__ */ __name((raw2) => {
28863
29086
  return null;
28864
29087
  }
28865
29088
  }, "parseJson");
28866
- var factsCursor = external_exports.object({
28867
- userId: external_exports.string().min(1).max(64),
28868
- kinds: external_exports.array(external_exports.enum(APP_FACT_KINDS)).min(1).max(APP_FACT_KINDS.length),
28869
- observedAtGte: external_exports.number().int().nonnegative().safe().nullable(),
28870
- observedAtLt: external_exports.number().int().nonnegative().safe().nullable(),
28871
- limit: external_exports.number().int().min(1).max(MAX_APP_FACTS_PER_PAGE),
28872
- afterRowId: external_exports.number().int().nonnegative().safe(),
28873
- maxRowId: external_exports.number().int().positive().safe().nullable()
28874
- }).strict();
28875
- var cursorKey = /* @__PURE__ */ __name(async (hex3) => {
28876
- const bytes = Uint8Array.from(hex3.matchAll(/.{2}/g), ([pair]) => Number.parseInt(pair, 16));
28877
- return crypto.subtle.importKey("raw", bytes, "AES-GCM", false, ["encrypt", "decrypt"]);
28878
- }, "cursorKey");
28879
- var encodeBase64Url = /* @__PURE__ */ __name((bytes) => {
28880
- let binary = "";
28881
- for (const byte of bytes) {
28882
- binary += String.fromCharCode(byte);
28883
- }
28884
- return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
28885
- }, "encodeBase64Url");
28886
- var decodeBase64Url = /* @__PURE__ */ __name((value) => {
28887
- const base643 = value.replaceAll("-", "+").replaceAll("_", "/");
28888
- const padded = base643.padEnd(Math.ceil(base643.length / 4) * 4, "=");
28889
- const binary = atob(padded);
28890
- return Uint8Array.from(binary, (character) => character.charCodeAt(0));
28891
- }, "decodeBase64Url");
28892
- var encryptFactsCursor = /* @__PURE__ */ __name(async (key, cursor) => {
28893
- const iv = crypto.getRandomValues(new Uint8Array(12));
28894
- const plaintext = new TextEncoder().encode(JSON.stringify(cursor));
28895
- const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv }, await cursorKey(key), plaintext));
28896
- const packed = new Uint8Array(iv.byteLength + ciphertext.byteLength);
28897
- packed.set(iv);
28898
- packed.set(ciphertext, iv.byteLength);
28899
- return encodeBase64Url(packed);
28900
- }, "encryptFactsCursor");
28901
- var decryptFactsCursor = /* @__PURE__ */ __name(async (key, encoded) => {
28902
- try {
28903
- const packed = decodeBase64Url(encoded);
28904
- if (packed.byteLength <= 12) {
28905
- return null;
28906
- }
28907
- const plaintext = await crypto.subtle.decrypt({ name: "AES-GCM", iv: packed.slice(0, 12) }, await cursorKey(key), packed.slice(12));
28908
- return JSON.parse(new TextDecoder().decode(plaintext));
28909
- } catch {
28910
- return null;
28911
- }
28912
- }, "decryptFactsCursor");
28913
- var invalidBody = /* @__PURE__ */ __name(() => {
28914
- return Response.json({ error: "invalid_body" }, { status: 400 });
28915
- }, "invalidBody");
28916
- var forbidden = /* @__PURE__ */ __name(() => {
28917
- return Response.json({ error: "forbidden" }, { status: 403 });
28918
- }, "forbidden");
28919
- var factsPermissionDenied = /* @__PURE__ */ __name(() => {
28920
- return Response.json({ error: "facts_permission_denied" }, { status: 403 });
28921
- }, "factsPermissionDenied");
28922
- var handlePush = /* @__PURE__ */ __name(async (env, body) => {
28923
- if (new TextEncoder().encode(JSON.stringify(body.data ?? null)).length > MAX_PUSH_BYTES) {
28924
- return invalidBody();
28925
- }
28926
- const broadcast = body.broadcast ?? "install";
28927
- if (broadcast === "install" && body.installId === void 0) {
28928
- return invalidBody();
28929
- }
28930
- const installId = broadcast === "install" ? body.installId : null;
28931
- if (await rateLimited(env, `${body.slug}:${installId ?? ""}:push`)) {
28932
- return Response.json({ error: "rate_limited" }, { status: 429 });
28933
- }
28934
- const app2 = siteRegistry().registry.apps[body.slug];
28935
- if (broadcast === "site" && (app2 === void 0 || !app2.events.publishes.includes(body.topic) || !app2.data.includes(SHARED_CONTEXT_CAPABILITY))) {
28936
- return Response.json({ error: "topic_not_public" }, { status: 400 });
28937
- }
28938
- await anonStub(env, body.userId).appPush(
28939
- { slug: body.slug, installId, topic: body.topic, data: body.data, scope: broadcast === "site" ? "site" : "app" },
28940
- ownSite(env),
28941
- currentTraceContext()
28942
- );
28943
- return Response.json({ accepted: true }, { status: 202 });
28944
- }, "handlePush");
28945
29089
  var resolveInstall = /* @__PURE__ */ __name((callee, installId) => {
28946
29090
  const active = callee.installs.filter((install) => install.active);
28947
29091
  if (installId !== void 0) {
@@ -28955,6 +29099,14 @@ var calleeFailed = /* @__PURE__ */ __name((callee, reason) => {
28955
29099
  addSpanEvent("rpc callee failed", { level: "warn", attrs: { app: callee, reason } });
28956
29100
  return Response.json({ error: "callee_failed" }, { status: 502 });
28957
29101
  }, "calleeFailed");
29102
+ var calleeError = /* @__PURE__ */ __name((callee, error61, message) => {
29103
+ addSpanEvent("rpc callee failed", { level: "warn", attrs: { app: callee, reason: message, code: error61 } });
29104
+ return Response.json({ error: error61, message }, { status: 502 });
29105
+ }, "calleeError");
29106
+ var resultTooLarge = /* @__PURE__ */ __name((candidate) => {
29107
+ const result = candidate?.rpc?.result;
29108
+ return new TextEncoder().encode(JSON.stringify(result ?? null)).length > MAX_RPC_BYTES;
29109
+ }, "resultTooLarge");
28958
29110
  var deliverRpcEffects = /* @__PURE__ */ __name(async (env, { body, userId, effects, install }) => {
28959
29111
  for (const ord of effects.rejectedOrds) {
28960
29112
  addSpanEvent("app effects rejected over the per-install caps", { level: "warn", attrs: { app: body.callee, ord } });
@@ -28980,10 +29132,32 @@ var deliverRpcEffects = /* @__PURE__ */ __name(async (env, { body, userId, effec
28980
29132
  }, "ingest")
28981
29133
  });
28982
29134
  }, "deliverRpcEffects");
28983
- var runRelayRpc = /* @__PURE__ */ __name(async (env, { context, body, callee, install }) => {
28984
- let raw2;
29135
+ var calleeQueryTicket = /* @__PURE__ */ __name(async (env, { context, body, callee, install }) => {
29136
+ const keyEnv = env.APP_UI_KEY;
29137
+ const [signing] = Object.entries((keyEnv === void 0 ? null : parseAppUiKeyEnv(keyEnv)) ?? {});
29138
+ if (callee.appId === void 0 || signing === void 0) {
29139
+ return void 0;
29140
+ }
29141
+ const [kid, key] = signing;
29142
+ return mintAppRuntimeQueryTicket({
29143
+ kid,
29144
+ key,
29145
+ now: /* @__PURE__ */ new Date(),
29146
+ slug: body.callee,
29147
+ domainId: context.domainId,
29148
+ appId: callee.appId,
29149
+ appInstallId: install.id,
29150
+ // The portal authorizes a ticket only for the version the site serves, which is the registry's.
29151
+ appVersionId: callee.versionId
29152
+ });
29153
+ }, "calleeQueryTicket");
29154
+ var postToCallee = /* @__PURE__ */ __name(async (env, input2) => {
29155
+ const { context, body, callee, install } = input2;
29156
+ const traffic = body.traffic ?? "hot";
29157
+ const timeoutMs = traffic === "cold" ? RPC_COLD_TIMEOUT_MS : RPC_HOT_TIMEOUT_MS;
28985
29158
  try {
28986
29159
  const key = await deriveAppInternalKey(context.master, context.domainId, body.callee);
29160
+ const queryTicket = traffic === "cold" ? await calleeQueryTicket(env, input2) : void 0;
28987
29161
  const response = await postHookEnvelope(env, {
28988
29162
  binding: callee.binding,
28989
29163
  key,
@@ -28997,9 +29171,9 @@ var runRelayRpc = /* @__PURE__ */ __name(async (env, { context, body, callee, in
28997
29171
  ...body.userId === void 0 ? {} : { user: { anonId: body.userId, globalId: null, properties: {} } },
28998
29172
  depth: body.depth,
28999
29173
  // JSON.stringify drops an undefined key, and the envelope requires `args` to be present.
29000
- rpc: { caller: body.slug, fn: body.fn, args: body.args ?? null }
29174
+ rpc: { caller: body.slug, fn: body.fn, args: body.args ?? null, traffic, ...queryTicket === void 0 ? {} : { queryTicket } }
29001
29175
  },
29002
- timeoutMs: APP_INVOCATION_TIMEOUT_MS
29176
+ timeoutMs
29003
29177
  });
29004
29178
  if (response === void 0) {
29005
29179
  return calleeFailed(body.callee, "The callee service binding is missing.");
@@ -29015,13 +29189,27 @@ var runRelayRpc = /* @__PURE__ */ __name(async (env, { context, body, callee, in
29015
29189
  }
29016
29190
  const bytes = await readBoundedBody(response, MAX_RESPONSE_BYTES);
29017
29191
  if (bytes === null) {
29018
- return calleeFailed(body.callee, "The response exceeds the size cap.");
29192
+ return calleeError(body.callee, "result_too_large", `The callee response exceeds ${MAX_RESPONSE_BYTES / 1024 / 1024} MB.`);
29019
29193
  }
29020
- raw2 = new TextDecoder().decode(bytes);
29194
+ return new TextDecoder().decode(bytes);
29021
29195
  } catch (error61) {
29196
+ if (error61 instanceof Error && error61.name === "TimeoutError") {
29197
+ return calleeError(body.callee, "callee_timeout", `The callee did not answer within ${timeoutMs / 1e3} s.`);
29198
+ }
29022
29199
  return calleeFailed(body.callee, String(error61));
29023
29200
  }
29024
- const verdict = validateEnvelopeResponse(parseJson(raw2), 1);
29201
+ }, "postToCallee");
29202
+ var runRelayRpc = /* @__PURE__ */ __name(async (env, input2) => {
29203
+ const { body, install } = input2;
29204
+ const raw2 = await postToCallee(env, input2);
29205
+ if (raw2 instanceof Response) {
29206
+ return raw2;
29207
+ }
29208
+ const candidate = parseJson(raw2);
29209
+ if (resultTooLarge(candidate)) {
29210
+ return calleeError(body.callee, "result_too_large", `The rpc result exceeds ${MAX_RPC_BYTES / 1024} KB.`);
29211
+ }
29212
+ const verdict = validateEnvelopeResponse(candidate, 1);
29025
29213
  if (!verdict.ok) {
29026
29214
  return calleeFailed(body.callee, verdict.reason);
29027
29215
  }
@@ -29030,6 +29218,9 @@ var runRelayRpc = /* @__PURE__ */ __name(async (env, { context, body, callee, in
29030
29218
  if (verdict.errorCode === "invalid_arguments") {
29031
29219
  return Response.json({ error: "invalid_arguments", message: reason }, { status: 400 });
29032
29220
  }
29221
+ if (verdict.errorCode === "rpc_error") {
29222
+ return calleeError(body.callee, "rpc_error", reason);
29223
+ }
29033
29224
  return calleeFailed(body.callee, reason);
29034
29225
  }
29035
29226
  if (verdict.rpc === null) {
@@ -29058,7 +29249,8 @@ var relayRpc = /* @__PURE__ */ __name(async (env, input2) => {
29058
29249
  "app.install_ord": install.ord,
29059
29250
  "app.install_label": install.label,
29060
29251
  "app.rpc_caller": body.slug,
29061
- "app.rpc_depth": body.depth
29252
+ "app.rpc_depth": body.depth,
29253
+ "app.rpc_traffic": body.traffic ?? "hot"
29062
29254
  }
29063
29255
  },
29064
29256
  () => {
@@ -29072,24 +29264,223 @@ var handleRpc = /* @__PURE__ */ __name(async (env, context, body) => {
29072
29264
  }
29073
29265
  const callee = siteRegistry().registry.apps[body.callee];
29074
29266
  if (callee === void 0) {
29075
- return forbidden();
29267
+ return Response.json({ error: "forbidden" }, { status: 403 });
29268
+ }
29269
+ const visibility = callee.rpc.exposes[body.fn];
29270
+ if (visibility !== "public" && !(visibility === "private" && (callee.rpc.grants[body.slug] ?? []).includes(body.fn))) {
29271
+ return Response.json({ error: "forbidden" }, { status: 403 });
29272
+ }
29273
+ const install = resolveInstall(callee, body.installId);
29274
+ if (install === "invalid_install" || install === "install_required") {
29275
+ return Response.json({ error: install }, { status: 400 });
29276
+ }
29277
+ if (new TextEncoder().encode(JSON.stringify(body.args ?? null)).length > MAX_RPC_BYTES) {
29278
+ return Response.json({ error: "too_large" }, { status: 413 });
29279
+ }
29280
+ if (await rateLimited(env, `${body.slug}:${install.id}:rpc`)) {
29281
+ return Response.json({ error: "rate_limited" }, { status: 429 });
29282
+ }
29283
+ return relayRpc(env, { context, body, callee, install });
29284
+ }, "handleRpc");
29285
+
29286
+ // src/apps/broker-track.ts
29287
+ var trackBody = external_exports.object({
29288
+ ...identityFields,
29289
+ kind: external_exports.literal("track"),
29290
+ source: external_exports.enum(APP_HANDLER_SOURCES),
29291
+ userId: external_exports.string().min(1).max(64),
29292
+ installId: external_exports.string().min(1).max(64),
29293
+ installs: installRuleSchema.optional(),
29294
+ event: emitSchema.omit({ installs: true })
29295
+ }).strict();
29296
+ var handleTrack = /* @__PURE__ */ __name(async (env, body) => {
29297
+ const app2 = siteRegistry().registry.apps[body.slug];
29298
+ if (app2 === void 0) {
29299
+ return Response.json({ error: "forbidden" }, { status: 403 });
29300
+ }
29301
+ if (!app2.installs.some((install) => install.active && install.id === body.installId)) {
29302
+ return Response.json({ error: "invalid_install" }, { status: 400 });
29303
+ }
29304
+ const parsed = emitPayloadSchema.safeParse(body.event.payload);
29305
+ if (!parsed.success) {
29306
+ return Response.json({ error: "invalid_event" }, { status: 400 });
29307
+ }
29308
+ if (!isCanonicalId(body.userId)) {
29309
+ return Response.json({ error: "invalid_user" }, { status: 400 });
29310
+ }
29311
+ if (await rateLimited(env, `${body.slug}:${body.installId}:track`)) {
29312
+ return Response.json({ error: "rate_limited" }, { status: 429 });
29313
+ }
29314
+ const site = ownSite(env);
29315
+ if (!await anonStub(env, body.userId).appAllowed(body.slug, site, currentTraceContext())) {
29316
+ return Response.json({ error: "forbidden" }, { status: 403 });
29317
+ }
29318
+ const { event, eventId, properties: properties2 } = parsed.data;
29319
+ const action = buildAction({
29320
+ type: "track",
29321
+ userId: body.userId,
29322
+ source: body.source,
29323
+ payload: { event, eventId: emittedEventId(eventId, properties2), properties: properties2 ?? {} },
29324
+ context: { source: "app", occurredAt: body.event.occurredAt, offline: false, replay: false },
29325
+ ...body.installs === void 0 ? {} : { installs: body.installs }
29326
+ });
29327
+ await ingest(env, action, site);
29328
+ return Response.json({ accepted: true }, { status: 202 });
29329
+ }, "handleTrack");
29330
+
29331
+ // src/apps/broker.ts
29332
+ var BROKER_PATHS = ["/__qinto/push", "/__qinto/rpc"];
29333
+ var pushBody = external_exports.object({
29334
+ ...identityFields,
29335
+ kind: external_exports.literal("push"),
29336
+ userId: external_exports.string().min(1).max(64),
29337
+ installId: external_exports.string().min(1).max(64).optional(),
29338
+ broadcast: external_exports.enum(PUSH_BROADCASTS).optional(),
29339
+ topic: external_exports.string().min(1).max(64),
29340
+ data: external_exports.unknown().optional()
29341
+ }).strict();
29342
+ var installsBody = external_exports.object({
29343
+ ...identityFields,
29344
+ kind: external_exports.literal("installs"),
29345
+ callee: external_exports.string().min(1).max(30)
29346
+ }).strict();
29347
+ var activeBody = external_exports.object({
29348
+ ...identityFields,
29349
+ kind: external_exports.literal("active"),
29350
+ ord: external_exports.number().int().min(1),
29351
+ userId: external_exports.string().min(1).max(64).optional()
29352
+ }).strict();
29353
+ var contextReadBody = external_exports.object({
29354
+ ...identityFields,
29355
+ kind: external_exports.literal("contextRead"),
29356
+ userId: external_exports.string().min(1).max(64)
29357
+ }).strict();
29358
+ var contextWriteBody = external_exports.object({
29359
+ ...identityFields,
29360
+ kind: external_exports.literal("contextWrite"),
29361
+ userId: external_exports.string().min(1).max(64),
29362
+ writes: external_exports.array(contextWriteSchema).min(1).max(MAX_CONTEXT_WRITES_PER_INSTALL)
29363
+ }).strict();
29364
+ var factsQuery = external_exports.object({
29365
+ kinds: external_exports.array(external_exports.enum(APP_FACT_KINDS)).min(1).max(APP_FACT_KINDS.length).optional(),
29366
+ observedAtGte: external_exports.number().int().nonnegative().safe().optional(),
29367
+ observedAtLt: external_exports.number().int().nonnegative().safe().optional(),
29368
+ limit: external_exports.number().int().min(1).max(MAX_APP_FACTS_PER_PAGE).optional()
29369
+ }).strict().refine((query) => query.observedAtGte === void 0 || query.observedAtLt === void 0 || query.observedAtGte < query.observedAtLt);
29370
+ var factsReadBody = external_exports.object({
29371
+ ...identityFields,
29372
+ kind: external_exports.literal("factsRead"),
29373
+ userId: external_exports.string().min(1).max(64),
29374
+ query: factsQuery.optional(),
29375
+ cursor: external_exports.string().min(1).max(4096).optional()
29376
+ }).strict().refine((body) => body.query === void 0 !== (body.cursor === void 0));
29377
+ var identityReadBody = external_exports.object({
29378
+ ...identityFields,
29379
+ kind: external_exports.literal("identityRead"),
29380
+ identifier: identifierField2,
29381
+ cursor: external_exports.string().regex(/^\d+$/).optional()
29382
+ }).strict();
29383
+ var brokerBody = external_exports.discriminatedUnion("kind", [
29384
+ pushBody,
29385
+ rpcBody,
29386
+ installsBody,
29387
+ activeBody,
29388
+ contextReadBody,
29389
+ contextWriteBody,
29390
+ factsReadBody,
29391
+ identityReadBody,
29392
+ identityUpsertBody,
29393
+ currencyRatesBody,
29394
+ trackBody
29395
+ ]);
29396
+ var BODY_ENVELOPE_ALLOWANCE_BYTES = 2 * 1024;
29397
+ var MAX_BODY_BYTES = MAX_RPC_BYTES + BODY_ENVELOPE_ALLOWANCE_BYTES;
29398
+ var parseJson2 = /* @__PURE__ */ __name((raw2) => {
29399
+ try {
29400
+ return JSON.parse(raw2);
29401
+ } catch {
29402
+ return null;
29403
+ }
29404
+ }, "parseJson");
29405
+ var factsCursor = external_exports.object({
29406
+ userId: external_exports.string().min(1).max(64),
29407
+ kinds: external_exports.array(external_exports.enum(APP_FACT_KINDS)).min(1).max(APP_FACT_KINDS.length),
29408
+ observedAtGte: external_exports.number().int().nonnegative().safe().nullable(),
29409
+ observedAtLt: external_exports.number().int().nonnegative().safe().nullable(),
29410
+ limit: external_exports.number().int().min(1).max(MAX_APP_FACTS_PER_PAGE),
29411
+ afterRowId: external_exports.number().int().nonnegative().safe(),
29412
+ maxRowId: external_exports.number().int().positive().safe().nullable()
29413
+ }).strict();
29414
+ var cursorKey = /* @__PURE__ */ __name(async (hex3) => {
29415
+ const bytes = Uint8Array.from(hex3.matchAll(/.{2}/g), ([pair]) => Number.parseInt(pair, 16));
29416
+ return crypto.subtle.importKey("raw", bytes, "AES-GCM", false, ["encrypt", "decrypt"]);
29417
+ }, "cursorKey");
29418
+ var encodeBase64Url = /* @__PURE__ */ __name((bytes) => {
29419
+ let binary = "";
29420
+ for (const byte of bytes) {
29421
+ binary += String.fromCharCode(byte);
29076
29422
  }
29077
- const visibility = callee.rpc.exposes[body.fn];
29078
- if (visibility !== "public" && !(visibility === "private" && (callee.rpc.grants[body.slug] ?? []).includes(body.fn))) {
29079
- return forbidden();
29423
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
29424
+ }, "encodeBase64Url");
29425
+ var decodeBase64Url = /* @__PURE__ */ __name((value) => {
29426
+ const base643 = value.replaceAll("-", "+").replaceAll("_", "/");
29427
+ const padded = base643.padEnd(Math.ceil(base643.length / 4) * 4, "=");
29428
+ const binary = atob(padded);
29429
+ return Uint8Array.from(binary, (character) => character.charCodeAt(0));
29430
+ }, "decodeBase64Url");
29431
+ var encryptFactsCursor = /* @__PURE__ */ __name(async (key, cursor) => {
29432
+ const iv = crypto.getRandomValues(new Uint8Array(12));
29433
+ const plaintext = new TextEncoder().encode(JSON.stringify(cursor));
29434
+ const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv }, await cursorKey(key), plaintext));
29435
+ const packed = new Uint8Array(iv.byteLength + ciphertext.byteLength);
29436
+ packed.set(iv);
29437
+ packed.set(ciphertext, iv.byteLength);
29438
+ return encodeBase64Url(packed);
29439
+ }, "encryptFactsCursor");
29440
+ var decryptFactsCursor = /* @__PURE__ */ __name(async (key, encoded) => {
29441
+ try {
29442
+ const packed = decodeBase64Url(encoded);
29443
+ if (packed.byteLength <= 12) {
29444
+ return null;
29445
+ }
29446
+ const plaintext = await crypto.subtle.decrypt({ name: "AES-GCM", iv: packed.slice(0, 12) }, await cursorKey(key), packed.slice(12));
29447
+ return JSON.parse(new TextDecoder().decode(plaintext));
29448
+ } catch {
29449
+ return null;
29080
29450
  }
29081
- const install = resolveInstall(callee, body.installId);
29082
- if (install === "invalid_install" || install === "install_required") {
29083
- return Response.json({ error: install }, { status: 400 });
29451
+ }, "decryptFactsCursor");
29452
+ var invalidBody = /* @__PURE__ */ __name(() => {
29453
+ return Response.json({ error: "invalid_body" }, { status: 400 });
29454
+ }, "invalidBody");
29455
+ var forbidden = /* @__PURE__ */ __name(() => {
29456
+ return Response.json({ error: "forbidden" }, { status: 403 });
29457
+ }, "forbidden");
29458
+ var factsPermissionDenied = /* @__PURE__ */ __name(() => {
29459
+ return Response.json({ error: "facts_permission_denied" }, { status: 403 });
29460
+ }, "factsPermissionDenied");
29461
+ var handlePush = /* @__PURE__ */ __name(async (env, body) => {
29462
+ if (new TextEncoder().encode(JSON.stringify(body.data ?? null)).length > MAX_PUSH_BYTES) {
29463
+ return invalidBody();
29084
29464
  }
29085
- if (new TextEncoder().encode(JSON.stringify(body.args ?? null)).length > MAX_RPC_BYTES) {
29086
- return Response.json({ error: "too_large" }, { status: 413 });
29465
+ const broadcast = body.broadcast ?? "install";
29466
+ if (broadcast === "install" && body.installId === void 0) {
29467
+ return invalidBody();
29087
29468
  }
29088
- if (await rateLimited(env, `${body.slug}:${install.id}:rpc`)) {
29469
+ const installId = broadcast === "install" ? body.installId : null;
29470
+ if (await rateLimited(env, `${body.slug}:${installId ?? ""}:push`)) {
29089
29471
  return Response.json({ error: "rate_limited" }, { status: 429 });
29090
29472
  }
29091
- return relayRpc(env, { context, body, callee, install });
29092
- }, "handleRpc");
29473
+ const app2 = siteRegistry().registry.apps[body.slug];
29474
+ if (broadcast === "site" && (app2 === void 0 || !app2.events.publishes.includes(body.topic) || !app2.data.includes(SHARED_CONTEXT_CAPABILITY))) {
29475
+ return Response.json({ error: "topic_not_public" }, { status: 400 });
29476
+ }
29477
+ await anonStub(env, body.userId).appPush(
29478
+ { slug: body.slug, installId, topic: body.topic, data: body.data, scope: broadcast === "site" ? "site" : "app" },
29479
+ ownSite(env),
29480
+ currentTraceContext()
29481
+ );
29482
+ return Response.json({ accepted: true }, { status: 202 });
29483
+ }, "handlePush");
29093
29484
  var handleInstalls = /* @__PURE__ */ __name(async (env, body) => {
29094
29485
  const callee = siteRegistry().registry.apps[body.callee];
29095
29486
  if (callee === void 0 || !(Object.values(callee.rpc.exposes).includes("public") || (callee.rpc.grants[body.slug] ?? []).length > 0)) {
@@ -29188,7 +29579,7 @@ var handleBroker = /* @__PURE__ */ __name(async (request, env, ctx) => {
29188
29579
  if (bytes === null) {
29189
29580
  return Response.json({ error: "too_large" }, { status: 413 });
29190
29581
  }
29191
- const candidate = parseJson(new TextDecoder().decode(bytes));
29582
+ const candidate = parseJson2(new TextDecoder().decode(bytes));
29192
29583
  const parsed = brokerBody.safeParse(candidate);
29193
29584
  if (!parsed.success) {
29194
29585
  if (typeof candidate === "object" && candidate !== null && "kind" in candidate && candidate.kind === "factsRead") {
@@ -29367,7 +29758,7 @@ var textBody = /* @__PURE__ */ __name(async (request) => {
29367
29758
  const bytes = await readBoundedBody(request, MAX_CONTROL_BYTES);
29368
29759
  return bytes === null ? null : new TextDecoder().decode(bytes);
29369
29760
  }, "textBody");
29370
- var parseJson2 = /* @__PURE__ */ __name((value) => {
29761
+ var parseJson3 = /* @__PURE__ */ __name((value) => {
29371
29762
  try {
29372
29763
  return JSON.parse(value);
29373
29764
  } catch {
@@ -29396,7 +29787,7 @@ var handleControl = /* @__PURE__ */ __name(async (request, env) => {
29396
29787
  if (authentication === null) {
29397
29788
  return Response.json({ error: "unauthorized" }, { status: 401 });
29398
29789
  }
29399
- const raw2 = parseJson2(body);
29790
+ const raw2 = parseJson3(body);
29400
29791
  if (raw2 === void 0) {
29401
29792
  return Response.json({ error: "invalid_body" }, { status: 400 });
29402
29793
  }
@@ -29434,7 +29825,7 @@ var handleControl = /* @__PURE__ */ __name(async (request, env) => {
29434
29825
  if (responseBytes === null) {
29435
29826
  return Response.json({ error: "invalid_response" }, { status: 502 });
29436
29827
  }
29437
- const responseBody = parseJson2(new TextDecoder().decode(responseBytes));
29828
+ const responseBody = parseJson3(new TextDecoder().decode(responseBytes));
29438
29829
  if (responseBody === void 0) {
29439
29830
  return Response.json({ error: "invalid_response" }, { status: 502 });
29440
29831
  }
@@ -29765,7 +30156,7 @@ var streamWithDeadlines = /* @__PURE__ */ __name((source, { idleMs, totalMs }) =
29765
30156
  }, "streamWithDeadlines");
29766
30157
 
29767
30158
  // src/helpers/bot-check.ts
29768
- var botRejected = /* @__PURE__ */ __name((env, geo) => {
30159
+ var botRejected = /* @__PURE__ */ __name((env, request, geo) => {
29769
30160
  return traced("bot.check", {}, () => {
29770
30161
  const { config: protection, error: error61 } = parseBotProtection(env.BOT_PROTECTION);
29771
30162
  if (error61 !== void 0) {
@@ -29774,8 +30165,19 @@ var botRejected = /* @__PURE__ */ __name((env, geo) => {
29774
30165
  attrs: { error: String(error61) }
29775
30166
  });
29776
30167
  }
29777
- const rejected = isRequestBlocked(protection, { botScore: geo.botScore, country: geo.country, isEU: geo.isEUCountry === "yes" });
29778
- setSpanAttributes({ "bot.blocked": rejected, ...geo.botScore === void 0 ? {} : { "bot.score": geo.botScore } });
30168
+ const cf = request.cf;
30169
+ const verifiedBot = cf?.botManagement?.verifiedBot;
30170
+ const rejected = isRequestBlocked(protection, {
30171
+ botScore: geo.botScore,
30172
+ verifiedBot,
30173
+ country: geo.country,
30174
+ isEU: geo.isEUCountry === "yes"
30175
+ });
30176
+ setSpanAttributes({
30177
+ "bot.blocked": rejected,
30178
+ ...geo.botScore === void 0 ? {} : { "bot.score": geo.botScore },
30179
+ ...verifiedBot === void 0 ? {} : { "bot.verified": verifiedBot }
30180
+ });
29779
30181
  return rejected;
29780
30182
  });
29781
30183
  }, "botRejected");
@@ -29865,7 +30267,7 @@ var withCors = /* @__PURE__ */ __name((response, request, method) => {
29865
30267
  }, "withCors");
29866
30268
  var authorize = /* @__PURE__ */ __name(async (c, { slug, app: app2 }) => {
29867
30269
  const geo = geoFromRequest(c.req.raw);
29868
- if (await botRejected(c.env, geo)) {
30270
+ if (await botRejected(c.env, c.req.raw, geo)) {
29869
30271
  return refuse(403, "traffic_rejected");
29870
30272
  }
29871
30273
  const anonId = getCookie(c, COOKIE_NAME2);
@@ -29999,7 +30401,7 @@ var passThroughResponse = /* @__PURE__ */ __name((upstream) => {
29999
30401
  });
30000
30402
  return new Response(body, { status: upstream.status, headers });
30001
30403
  }, "passThroughResponse");
30002
- var parseJson3 = /* @__PURE__ */ __name((text) => {
30404
+ var parseJson4 = /* @__PURE__ */ __name((text) => {
30003
30405
  try {
30004
30406
  return JSON.parse(text);
30005
30407
  } catch {
@@ -30014,7 +30416,7 @@ var webhookAck = /* @__PURE__ */ __name(async (slug, upstream) => {
30014
30416
  if (bytes === null) {
30015
30417
  return upstreamFailed(slug, "The endpoint outcomes exceed the size cap.");
30016
30418
  }
30017
- const parsed = endpointOutcomesSchema.safeParse(parseJson3(new TextDecoder().decode(bytes)));
30419
+ const parsed = endpointOutcomesSchema.safeParse(parseJson4(new TextDecoder().decode(bytes)));
30018
30420
  if (!parsed.success) {
30019
30421
  return upstreamFailed(slug, "The endpoint outcomes are malformed.");
30020
30422
  }
@@ -30064,7 +30466,7 @@ var runForwardToApp = /* @__PURE__ */ __name(async (c, input2) => {
30064
30466
  method: endpoint.method,
30065
30467
  mode: endpoint.mode,
30066
30468
  version: app2.version,
30067
- ...app2.data.includes("geo") && requestGeo !== void 0 ? { geo: requestGeo } : {},
30469
+ ...app2.data.includes("geo") && requestGeo !== void 0 ? { geo: cfDataFrom(requestGeo) } : {},
30068
30470
  installs: installs.map((install) => ({ id: install.id, ord: install.ord, label: install.label })),
30069
30471
  ...anonId === void 0 ? {} : { user: { anonId } }
30070
30472
  })
@@ -30072,286 +30474,91 @@ var runForwardToApp = /* @__PURE__ */ __name(async (c, input2) => {
30072
30474
  let upstream;
30073
30475
  try {
30074
30476
  const url2 = `https://app/__qinto/endpoint${endpoint.route}${search}`;
30075
- const init = { method: endpoint.method, headers, body };
30076
- upstream = endpoint.mode === "api" ? await fetchApiResponse(service, url2, init) : await service.fetch(url2, { ...init, signal: AbortSignal.timeout(WEBHOOK_ENDPOINT_TIMEOUT_MS) });
30077
- } catch (error61) {
30078
- return upstreamFailed(slug, String(error61));
30079
- }
30080
- adoptAppSpans(upstream);
30081
- writeAppObservability(c.env, upstream, {
30082
- app: observabilityApp(slug, app2),
30083
- installs,
30084
- budget: createDataPointBudget()
30085
- });
30086
- setSpanAttributes({ "app.status": upstream.status });
30087
- return endpoint.mode === "api" ? passThroughResponse(upstream) : webhookAck(slug, upstream);
30088
- }, "runForwardToApp");
30089
- var forwardToApp = /* @__PURE__ */ __name(async (c, input2) => {
30090
- const { slug, app: app2, endpoint, installs } = input2;
30091
- return traced(
30092
- `app.endpoint ${slug}`,
30093
- {
30094
- attrs: {
30095
- "app.slug": slug,
30096
- "app.version": app2.version,
30097
- "app.binding": app2.binding,
30098
- "app.hook": `endpoint.${endpoint.method} ${endpoint.route}`,
30099
- "app.endpoint_mode": endpoint.mode,
30100
- "app.installs": installs.length
30101
- }
30102
- },
30103
- () => {
30104
- return runForwardToApp(c, input2);
30105
- }
30106
- );
30107
- }, "forwardToApp");
30108
- var serveEndpoint = /* @__PURE__ */ __name(async (c, input2) => {
30109
- const { slug, app: app2, endpoint, search } = input2;
30110
- const refusal = await authorizeEndpoint(c, slug, endpoint);
30111
- if (refusal !== void 0) {
30112
- return refusal;
30113
- }
30114
- const installs = app2.installs.filter((install) => install.active);
30115
- const [first] = installs;
30116
- if (first === void 0) {
30117
- return refuse2(503, "inactive");
30118
- }
30119
- const key = endpoint.mode === "api" ? `${slug}:${first.id}:endpoint` : `${slug}:${endpoint.route}:endpoint`;
30120
- if (await rateLimited(c.env, key)) {
30121
- return refuse2(429, "rate_limited");
30122
- }
30123
- let body;
30124
- if (endpoint.method !== "GET") {
30125
- const bytes = await readBoundedBody(c.req.raw, MAX_ENDPOINT_BODY_BYTES);
30126
- if (bytes === null) {
30127
- return refuse2(413, "too_large");
30128
- }
30129
- body = bytes;
30130
- }
30131
- return forwardToApp(c, { slug, app: app2, endpoint, installs, body, search });
30132
- }, "serveEndpoint");
30133
- var handleAppEndpoint = /* @__PURE__ */ __name(async (c, slug) => {
30134
- const app2 = siteRegistry().registry.apps[slug];
30135
- if (app2 === void 0) {
30136
- return refuse2(404, "not_found");
30137
- }
30138
- const url2 = new URL(c.req.url);
30139
- const prefix = `/app/${slug}`;
30140
- if (url2.pathname !== prefix && !url2.pathname.startsWith(`${prefix}/`)) {
30141
- return refuse2(404, "not_found");
30142
- }
30143
- const preflight2 = c.req.method === "OPTIONS";
30144
- const method = preflight2 ? c.req.header("access-control-request-method") : c.req.method;
30145
- const endpoint = matchEndpoint(app2, url2.pathname.slice(prefix.length) || "/", method ?? "");
30146
- if (endpoint === void 0 || preflight2 && endpoint.auth !== "visitor") {
30147
- return refuse2(404, "not_found");
30148
- }
30149
- if (endpoint.auth === "visitor") {
30150
- return handleVisitorEndpoint(c, { slug, app: app2, method: endpoint.method, preflight: preflight2 }, (visitor, body) => {
30151
- return forwardToApp(c, { slug, app: app2, endpoint, installs: [visitor.install], body, search: url2.search, anonId: visitor.anonId });
30152
- });
30153
- }
30154
- return serveEndpoint(c, { slug, app: app2, endpoint, search: url2.search });
30155
- }, "handleAppEndpoint");
30156
- var appEndpointRoutes = new Hono2();
30157
- appEndpointRoutes.all("/app/:slug/*", (c) => {
30158
- return handleAppEndpoint(c, c.req.param("slug"));
30159
- });
30160
-
30161
- // ../../libs/app-ui-token/src/base64url.ts
30162
- var base64UrlEncode = /* @__PURE__ */ __name((bytes) => {
30163
- let binary = "";
30164
- for (const byte of bytes) {
30165
- binary += String.fromCharCode(byte);
30166
- }
30167
- return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
30168
- }, "base64UrlEncode");
30169
- var base64UrlDecode = /* @__PURE__ */ __name((text) => {
30170
- const padded = text.replaceAll("-", "+").replaceAll("_", "/").padEnd(text.length + (4 - text.length % 4) % 4, "=");
30171
- let binary;
30172
- try {
30173
- binary = atob(padded);
30174
- } catch {
30175
- return null;
30176
- }
30177
- const bytes = new Uint8Array(binary.length);
30178
- for (let index = 0; index < binary.length; index += 1) {
30179
- bytes[index] = binary.charCodeAt(index);
30180
- }
30181
- return bytes;
30182
- }, "base64UrlDecode");
30183
-
30184
- // ../../libs/app-ui-token/src/env.ts
30185
- var envSchema = external_exports.object({
30186
- v: external_exports.literal(1),
30187
- keys: external_exports.record(external_exports.string().min(1).max(16), external_exports.string().min(1))
30188
- }).strict();
30189
- var parseAppUiKeyEnv = /* @__PURE__ */ __name((raw2) => {
30190
- let json2;
30191
- try {
30192
- json2 = JSON.parse(raw2);
30193
- } catch {
30194
- return null;
30195
- }
30196
- const parsed = envSchema.safeParse(json2);
30197
- if (!parsed.success) {
30198
- return null;
30199
- }
30200
- const keys = {};
30201
- for (const [kid, encoded] of Object.entries(parsed.data.keys)) {
30202
- const bytes = base64UrlDecode(encoded);
30203
- if (bytes === null) {
30204
- return null;
30205
- }
30206
- keys[kid] = bytes;
30207
- }
30208
- return keys;
30209
- }, "parseAppUiKeyEnv");
30210
-
30211
- // ../../libs/app-ui-token/src/jwt.ts
30212
- var headerSchema = external_exports.object({
30213
- alg: external_exports.literal("HS256"),
30214
- typ: external_exports.literal("JWT"),
30215
- kid: external_exports.string().min(1).max(16)
30216
- }).strict();
30217
- var encodeJsonPart = /* @__PURE__ */ __name((value) => {
30218
- return base64UrlEncode(new TextEncoder().encode(JSON.stringify(value)));
30219
- }, "encodeJsonPart");
30220
- var parseJsonPart = /* @__PURE__ */ __name((part) => {
30221
- const bytes = base64UrlDecode(part);
30222
- if (bytes === null) {
30223
- return null;
30224
- }
30225
- try {
30226
- return JSON.parse(new TextDecoder().decode(bytes));
30227
- } catch {
30228
- return null;
30229
- }
30230
- }, "parseJsonPart");
30231
- var hmacKey = /* @__PURE__ */ __name((key, usage) => {
30232
- return crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-256" }, false, [usage]);
30233
- }, "hmacKey");
30234
- var signCompact = /* @__PURE__ */ __name(async (kid, key, claims) => {
30235
- const signingInput = `${encodeJsonPart({ alg: "HS256", typ: "JWT", kid })}.${encodeJsonPart(claims)}`;
30236
- const signature = await crypto.subtle.sign("HMAC", await hmacKey(key, "sign"), new TextEncoder().encode(signingInput));
30237
- return `${signingInput}.${base64UrlEncode(new Uint8Array(signature))}`;
30238
- }, "signCompact");
30239
- var verifyCompactSignature = /* @__PURE__ */ __name(async (token, keys) => {
30240
- const [headerPart, claimsPart, signaturePart, extra] = token.split(".");
30241
- if (headerPart === void 0 || claimsPart === void 0 || signaturePart === void 0 || extra !== void 0) {
30242
- return { ok: false, reason: "malformed" };
30243
- }
30244
- const header = headerSchema.safeParse(parseJsonPart(headerPart));
30245
- if (!header.success) {
30246
- return { ok: false, reason: "malformed" };
30247
- }
30248
- const key = keys[header.data.kid];
30249
- if (key === void 0) {
30250
- return { ok: false, reason: "unknown_kid" };
30251
- }
30252
- const signature = base64UrlDecode(signaturePart);
30253
- if (signature === null) {
30254
- return { ok: false, reason: "malformed" };
30477
+ const init = { method: endpoint.method, headers, body };
30478
+ upstream = endpoint.mode === "api" ? await fetchApiResponse(service, url2, init) : await service.fetch(url2, { ...init, signal: AbortSignal.timeout(WEBHOOK_ENDPOINT_TIMEOUT_MS) });
30479
+ } catch (error61) {
30480
+ return upstreamFailed(slug, String(error61));
30255
30481
  }
30256
- const signed = await crypto.subtle.verify(
30257
- "HMAC",
30258
- await hmacKey(key, "verify"),
30259
- signature,
30260
- new TextEncoder().encode(`${headerPart}.${claimsPart}`)
30482
+ adoptAppSpans(upstream);
30483
+ writeAppObservability(c.env, upstream, {
30484
+ app: observabilityApp(slug, app2),
30485
+ installs,
30486
+ budget: createDataPointBudget()
30487
+ });
30488
+ setSpanAttributes({ "app.status": upstream.status });
30489
+ return endpoint.mode === "api" ? passThroughResponse(upstream) : webhookAck(slug, upstream);
30490
+ }, "runForwardToApp");
30491
+ var forwardToApp = /* @__PURE__ */ __name(async (c, input2) => {
30492
+ const { slug, app: app2, endpoint, installs } = input2;
30493
+ return traced(
30494
+ `app.endpoint ${slug}`,
30495
+ {
30496
+ attrs: {
30497
+ "app.slug": slug,
30498
+ "app.version": app2.version,
30499
+ "app.binding": app2.binding,
30500
+ "app.hook": `endpoint.${endpoint.method} ${endpoint.route}`,
30501
+ "app.endpoint_mode": endpoint.mode,
30502
+ "app.installs": installs.length
30503
+ }
30504
+ },
30505
+ () => {
30506
+ return runForwardToApp(c, input2);
30507
+ }
30261
30508
  );
30262
- if (!signed) {
30263
- return { ok: false, reason: "bad_signature" };
30509
+ }, "forwardToApp");
30510
+ var serveEndpoint = /* @__PURE__ */ __name(async (c, input2) => {
30511
+ const { slug, app: app2, endpoint, search } = input2;
30512
+ const refusal = await authorizeEndpoint(c, slug, endpoint);
30513
+ if (refusal !== void 0) {
30514
+ return refusal;
30264
30515
  }
30265
- return { ok: true, claims: parseJsonPart(claimsPart), kid: header.data.kid, key };
30266
- }, "verifyCompactSignature");
30267
-
30268
- // ../../libs/app-ui-token/src/token.ts
30269
- var APP_UI_TOKEN_SKEW_SECONDS = 60;
30270
- var APP_UI_TOKEN_ISSUER = "qinto-portal";
30271
- var claimsSchema = external_exports.object({
30272
- iss: external_exports.literal(APP_UI_TOKEN_ISSUER),
30273
- aud: external_exports.string().min(1),
30274
- sub: external_exports.string().min(1),
30275
- userId: external_exports.string().min(1),
30276
- iat: external_exports.number().int(),
30277
- exp: external_exports.number().int(),
30278
- domainId: external_exports.string().min(1),
30279
- appId: external_exports.string().min(1),
30280
- appInstallId: external_exports.string().min(1),
30281
- appVersionId: external_exports.string().min(1),
30282
- access: external_exports.enum(["read", "write"]),
30283
- // Absent means `full`; see the mint below.
30284
- appRole: external_exports.enum(APP_ROLES).optional()
30285
- }).strict();
30286
- var appUiAudience = /* @__PURE__ */ __name((slug) => {
30287
- return `app-ui:${slug}`;
30288
- }, "appUiAudience");
30289
- var verifyAppUiToken = /* @__PURE__ */ __name(async (input2) => {
30290
- const signature = await verifyCompactSignature(input2.token, input2.keys);
30291
- if (!signature.ok) {
30292
- return signature;
30516
+ const installs = app2.installs.filter((install) => install.active);
30517
+ const [first] = installs;
30518
+ if (first === void 0) {
30519
+ return refuse2(503, "inactive");
30293
30520
  }
30294
- const claims = claimsSchema.safeParse(signature.claims);
30295
- if (!claims.success) {
30296
- return { ok: false, reason: "bad_claims" };
30521
+ const key = endpoint.mode === "api" ? `${slug}:${first.id}:endpoint` : `${slug}:${endpoint.route}:endpoint`;
30522
+ if (await rateLimited(c.env, key)) {
30523
+ return refuse2(429, "rate_limited");
30297
30524
  }
30298
- if (claims.data.aud !== appUiAudience(input2.slug)) {
30299
- return { ok: false, reason: "wrong_app" };
30525
+ let body;
30526
+ if (endpoint.method !== "GET") {
30527
+ const bytes = await readBoundedBody(c.req.raw, MAX_ENDPOINT_BODY_BYTES);
30528
+ if (bytes === null) {
30529
+ return refuse2(413, "too_large");
30530
+ }
30531
+ body = bytes;
30300
30532
  }
30301
- const now = Math.floor(input2.now.getTime() / 1e3);
30302
- if (claims.data.iat > now + (input2.skewSeconds ?? APP_UI_TOKEN_SKEW_SECONDS)) {
30303
- return { ok: false, reason: "future_iat" };
30533
+ return forwardToApp(c, { slug, app: app2, endpoint, installs, body, search });
30534
+ }, "serveEndpoint");
30535
+ var handleAppEndpoint = /* @__PURE__ */ __name(async (c, slug) => {
30536
+ const app2 = siteRegistry().registry.apps[slug];
30537
+ if (app2 === void 0) {
30538
+ return refuse2(404, "not_found");
30304
30539
  }
30305
- if (claims.data.exp <= now) {
30306
- return { ok: false, reason: "expired" };
30540
+ const url2 = new URL(c.req.url);
30541
+ const prefix = `/app/${slug}`;
30542
+ if (url2.pathname !== prefix && !url2.pathname.startsWith(`${prefix}/`)) {
30543
+ return refuse2(404, "not_found");
30307
30544
  }
30308
- return { ok: true, claims: claims.data, kid: signature.kid, key: signature.key };
30309
- }, "verifyAppUiToken");
30310
-
30311
- // ../../libs/app-ui-token/src/runtime-query-ticket.ts
30312
- var APP_RUNTIME_QUERY_TICKET_ISSUER = "qinto-portal";
30313
- var claimsSchema2 = external_exports.object({
30314
- iss: external_exports.literal(APP_RUNTIME_QUERY_TICKET_ISSUER),
30315
- aud: external_exports.string().min(1),
30316
- sub: external_exports.string().min(1),
30317
- iat: external_exports.number().int(),
30318
- exp: external_exports.number().int(),
30319
- domainId: external_exports.string().min(1),
30320
- appId: external_exports.string().min(1),
30321
- appInstallId: external_exports.string().min(1),
30322
- appVersionId: external_exports.string().min(1)
30323
- }).strict();
30324
-
30325
- // ../../libs/app-ui-token/src/write-ticket.ts
30326
- var APP_UI_WRITE_TICKET_TTL_SECONDS = 60;
30327
- var APP_UI_WRITE_TICKET_ISSUER = "qinto-site";
30328
- var claimsSchema3 = external_exports.object({
30329
- iss: external_exports.literal(APP_UI_WRITE_TICKET_ISSUER),
30330
- aud: external_exports.string().min(1),
30331
- sub: external_exports.string().min(1),
30332
- iat: external_exports.number().int(),
30333
- exp: external_exports.number().int(),
30334
- domainId: external_exports.string().min(1),
30335
- appId: external_exports.string().min(1),
30336
- appVersionId: external_exports.string().min(1)
30337
- }).strict();
30338
- var appUiWriteAudience = /* @__PURE__ */ __name((slug) => {
30339
- return `app-ui-write:${slug}`;
30340
- }, "appUiWriteAudience");
30341
- var mintAppUiWriteTicket = /* @__PURE__ */ __name(async (input2) => {
30342
- const iat = Math.floor(input2.now.getTime() / 1e3);
30343
- const claims = {
30344
- iss: APP_UI_WRITE_TICKET_ISSUER,
30345
- aud: appUiWriteAudience(input2.slug),
30346
- sub: input2.userId,
30347
- iat,
30348
- exp: iat + (input2.ttlSeconds ?? APP_UI_WRITE_TICKET_TTL_SECONDS),
30349
- domainId: input2.domainId,
30350
- appId: input2.appId,
30351
- appVersionId: input2.appVersionId
30352
- };
30353
- return signCompact(input2.kid, input2.key, claims);
30354
- }, "mintAppUiWriteTicket");
30545
+ const preflight2 = c.req.method === "OPTIONS";
30546
+ const method = preflight2 ? c.req.header("access-control-request-method") : c.req.method;
30547
+ const endpoint = matchEndpoint(app2, url2.pathname.slice(prefix.length) || "/", method ?? "");
30548
+ if (endpoint === void 0 || preflight2 && endpoint.auth !== "visitor") {
30549
+ return refuse2(404, "not_found");
30550
+ }
30551
+ if (endpoint.auth === "visitor") {
30552
+ return handleVisitorEndpoint(c, { slug, app: app2, method: endpoint.method, preflight: preflight2 }, (visitor, body) => {
30553
+ return forwardToApp(c, { slug, app: app2, endpoint, installs: [visitor.install], body, search: url2.search, anonId: visitor.anonId });
30554
+ });
30555
+ }
30556
+ return serveEndpoint(c, { slug, app: app2, endpoint, search: url2.search });
30557
+ }, "handleAppEndpoint");
30558
+ var appEndpointRoutes = new Hono2();
30559
+ appEndpointRoutes.all("/app/:slug/*", (c) => {
30560
+ return handleAppEndpoint(c, c.req.param("slug"));
30561
+ });
30355
30562
 
30356
30563
  // src/apps/ui-proxy.ts
30357
30564
  var APP_UI_TIMEOUT_MS = 6e4;
@@ -30660,6 +30867,7 @@ var createApiRouter = /* @__PURE__ */ __name(() => new OpenAPIHono({ defaultHook
30660
30867
  var CONNECTION_CF_HEADER = "x-connection-cf";
30661
30868
  var SESSION_HEADER = "x-session";
30662
30869
  var GPC_HEADER = "x-gpc";
30870
+ var RECONNECT_HEADER = "x-reconnect";
30663
30871
  var OPEN_PAGE_PARAMS = ["url", "referrer"];
30664
30872
  var MAX_HOST_USER_AGENT_LENGTH = 512;
30665
30873
  var MAX_HOST_LOCALE_LENGTH = 64;
@@ -30671,6 +30879,7 @@ var INTERNAL_HEADERS = /* @__PURE__ */ new Set([
30671
30879
  CONNECTION_CF_HEADER,
30672
30880
  SESSION_HEADER,
30673
30881
  GPC_HEADER,
30882
+ RECONNECT_HEADER,
30674
30883
  TRACE_HEADER,
30675
30884
  CF_DATA_HEADER
30676
30885
  ]);
@@ -30759,7 +30968,7 @@ var boundedHost = /* @__PURE__ */ __name((request) => {
30759
30968
  };
30760
30969
  return bounded.ip === void 0 && bounded.userAgent === void 0 && bounded.locale === void 0 ? void 0 : bounded;
30761
30970
  }, "boundedHost");
30762
- var connectionAttachment = /* @__PURE__ */ __name((request, geo, siteId) => {
30971
+ var connectionAttachment = /* @__PURE__ */ __name((request, geo, { siteId, anonId }) => {
30763
30972
  const userAgent = request.headers.get("user-agent");
30764
30973
  const attrs = {
30765
30974
  "url.full": withoutQuery(request.url),
@@ -30775,7 +30984,7 @@ var connectionAttachment = /* @__PURE__ */ __name((request, geo, siteId) => {
30775
30984
  const sessionBytes = attachmentFieldBytes("session", session2);
30776
30985
  const gpc = request.headers.get(GPC_HEADER) === "1";
30777
30986
  const gpcBytes = attachmentFieldBytes("gpc", gpc ? true : void 0);
30778
- const siteBytes = attachmentFieldBytes("siteId", siteId);
30987
+ const siteBytes = attachmentFieldBytes("siteId", siteId) + attachmentFieldBytes("anonId", anonId);
30779
30988
  const cf = forwardedCf(request);
30780
30989
  const cfSuffix = cf === void 0 ? "" : `,"cf":${JSON.stringify(cf)}`;
30781
30990
  const cfBytes = new TextEncoder().encode(cfSuffix).length;
@@ -30786,6 +30995,7 @@ var connectionAttachment = /* @__PURE__ */ __name((request, geo, siteId) => {
30786
30995
  return {
30787
30996
  ...budgeted,
30788
30997
  siteId,
30998
+ anonId,
30789
30999
  ...session2 === void 0 ? {} : { session: session2 },
30790
31000
  ...browserGeo === void 0 ? {} : { browserGeo },
30791
31001
  ...includedCf === void 0 ? {} : { cf: includedCf },
@@ -30793,6 +31003,14 @@ var connectionAttachment = /* @__PURE__ */ __name((request, geo, siteId) => {
30793
31003
  ...gpc ? { gpc: true } : {}
30794
31004
  };
30795
31005
  }, "connectionAttachment");
31006
+ var attachmentGeo = /* @__PURE__ */ __name((attachment) => {
31007
+ return {
31008
+ // `cf` is the forwarded `request.cf`, kept as plain JSON on the socket.
31009
+ ...geoFromCf(attachment.cf),
31010
+ ip: attachment.host?.ip,
31011
+ userAgent: attachment.host?.userAgent
31012
+ };
31013
+ }, "attachmentGeo");
30796
31014
 
30797
31015
  // src/helpers/request-span.ts
30798
31016
  var OMITTED_HEADERS = /* @__PURE__ */ new Set(["authorization", DISPATCH_HEADER, CF_DATA_HEADER]);
@@ -31192,6 +31410,29 @@ var withSetCookie = /* @__PURE__ */ __name((res, cookies) => {
31192
31410
  }
31193
31411
  return out;
31194
31412
  }, "withSetCookie");
31413
+ var connectCookies = /* @__PURE__ */ __name(async (c, userId, session2) => {
31414
+ const root = c.env.ROOT_DOMAIN;
31415
+ const hostname3 = new URL(c.req.url).hostname;
31416
+ const domain2 = root !== void 0 && root !== "" && (hostname3 === root || hostname3.endsWith(`.${root}`)) ? root : void 0;
31417
+ const attributes = {
31418
+ httpOnly: true,
31419
+ // `wrangler types` narrows COOKIE_SAME_SITE to its dev default; deploys also set none.
31420
+ sameSite: c.env.COOKIE_SAME_SITE,
31421
+ secure: true,
31422
+ path: "/",
31423
+ maxAge: COOKIE_MAX_AGE
31424
+ };
31425
+ const cookies = [
31426
+ generateCookie(COOKIE_NAME2, userId, { ...attributes, ...domain2 === void 0 ? {} : { domain: domain2 } }),
31427
+ // Re-issued on every connect, which is what makes the session expire after 30 idle minutes.
31428
+ generateCookie(SESSION_COOKIE_NAME, `${session2.id}.${session2.startedAt}`, { ...attributes, maxAge: SESSION_MAX_AGE })
31429
+ ];
31430
+ const signature = await signVisitor(c.env, userId);
31431
+ if (signature !== void 0) {
31432
+ cookies.push(generateCookie(VISITOR_SIGNATURE_COOKIE, signature, attributes));
31433
+ }
31434
+ return cookies;
31435
+ }, "connectCookies");
31195
31436
  var open2 = /* @__PURE__ */ __name(async (c) => {
31196
31437
  if (c.req.header("upgrade")?.toLowerCase() !== "websocket") {
31197
31438
  return c.text("expected websocket", 426);
@@ -31204,13 +31445,14 @@ var open2 = /* @__PURE__ */ __name(async (c) => {
31204
31445
  });
31205
31446
  return enriched;
31206
31447
  });
31207
- if (await botRejected(c.env, geo)) {
31448
+ if (await botRejected(c.env, c.req.raw, geo)) {
31208
31449
  return c.text("traffic rejected", 403);
31209
31450
  }
31210
31451
  const existing = getCookie(c, COOKIE_NAME2);
31452
+ const canonical = existing !== void 0 && isCanonicalId(existing);
31211
31453
  let isNewUser = false;
31212
31454
  let userId;
31213
- if (existing !== void 0 && isCanonicalId(existing)) {
31455
+ if (canonical) {
31214
31456
  userId = existing;
31215
31457
  } else {
31216
31458
  const adopted = edgeTagUserId(c);
@@ -31236,6 +31478,10 @@ var open2 = /* @__PURE__ */ __name(async (c) => {
31236
31478
  headers.set(GPC_HEADER, "1");
31237
31479
  setSpanAttributes({ "qinto.gpc": true });
31238
31480
  }
31481
+ headers.delete(RECONNECT_HEADER);
31482
+ if (canonical && c.req.query("reconnect") === "1") {
31483
+ headers.set(RECONNECT_HEADER, "1");
31484
+ }
31239
31485
  const res = await traced("rpc anon.connect", {}, () => {
31240
31486
  headers.delete(TRACE_HEADER);
31241
31487
  const trace = currentTraceContext();
@@ -31244,39 +31490,19 @@ var open2 = /* @__PURE__ */ __name(async (c) => {
31244
31490
  }
31245
31491
  return anonStub(c.env, userId).fetch(new Request(c.req.url, { method: "GET", headers }));
31246
31492
  });
31247
- const root = c.env.ROOT_DOMAIN;
31248
- const hostname3 = new URL(c.req.url).hostname;
31249
- const domain2 = root !== void 0 && root !== "" && (hostname3 === root || hostname3.endsWith(`.${root}`)) ? root : void 0;
31250
- const attributes = {
31251
- httpOnly: true,
31252
- // `wrangler types` narrows COOKIE_SAME_SITE to its dev default; deploys also set none.
31253
- sameSite: c.env.COOKIE_SAME_SITE,
31254
- secure: true,
31255
- path: "/",
31256
- maxAge: COOKIE_MAX_AGE
31257
- };
31258
- const cookies = [
31259
- generateCookie(COOKIE_NAME2, userId, { ...attributes, ...domain2 === void 0 ? {} : { domain: domain2 } }),
31260
- // Re-issued on every connect, which is what makes the session expire after 30 idle minutes.
31261
- generateCookie(SESSION_COOKIE_NAME, `${session2.id}.${session2.startedAt}`, { ...attributes, maxAge: SESSION_MAX_AGE })
31262
- ];
31263
- const signature = await signVisitor(c.env, userId);
31264
- if (signature !== void 0) {
31265
- cookies.push(generateCookie(VISITOR_SIGNATURE_COOKIE, signature, attributes));
31266
- }
31267
- return withSetCookie(res, cookies);
31493
+ return withSetCookie(res, await connectCookies(c, userId, session2));
31268
31494
  }, "open");
31269
31495
 
31270
31496
  // browser-artifact/meta.json
31271
31497
  var meta_default = {
31272
- version: "0.1.14",
31273
- gitSha: "38d2892377935b8b457db896715c24891fc3b1ae",
31274
- builtAt: "2026-10-02T07:54:26.292Z",
31275
- hash: "50484908a3b28aa31a614fda30b1b3d803fe5922c8a485079f12e6bbf24cd9c1"
31498
+ version: "0.1.15",
31499
+ gitSha: "b90e20a6f236a81312f9525a2b089d2019d0c027",
31500
+ builtAt: "2026-10-09T06:11:09.581Z",
31501
+ hash: "a0cb067dc6246faae53d7842489effb3c46aa848ef529484ab6ee3868fd86762"
31276
31502
  };
31277
31503
 
31278
31504
  // src/paths/q-js.ts
31279
- import qScript from "./8809017061697bf9b90fa4d72f7eb2e0f0e00ad4-q.js.txt";
31505
+ import qScript from "./73ef5f028ec35836560c0489d1abdeea83eceede-q.js.txt";
31280
31506
 
31281
31507
  // src/apps-q.ts
31282
31508
  import appsQScript from "./apps-q.json";
@@ -32476,13 +32702,15 @@ var cachedKv = /* @__PURE__ */ __name((kv) => {
32476
32702
  var VERSION_KEY = "schemaVersion";
32477
32703
  var applyMigrations = /* @__PURE__ */ __name((storage, migrations) => {
32478
32704
  const applied = storage.kv.get(VERSION_KEY) ?? 0;
32479
- for (const [index, migration] of migrations.entries()) {
32480
- if (index < applied) {
32481
- continue;
32482
- }
32483
- storage.sql.exec(migration);
32484
- storage.kv.put(VERSION_KEY, index + 1);
32705
+ if (applied >= migrations.length) {
32706
+ return;
32485
32707
  }
32708
+ storage.transactionSync(() => {
32709
+ for (const migration of migrations.slice(applied)) {
32710
+ storage.sql.exec(migration);
32711
+ }
32712
+ storage.kv.put(VERSION_KEY, migrations.length);
32713
+ });
32486
32714
  }, "applyMigrations");
32487
32715
 
32488
32716
  // src/id-graph/retention.ts
@@ -32768,15 +32996,15 @@ var seedMeta = /* @__PURE__ */ __name((kv, anonId, createdAt) => {
32768
32996
  kv.put(META_KEY, { anonId, linkedGlobalId: null, linkedAt: null, createdAt });
32769
32997
  return true;
32770
32998
  }, "seedMeta");
32999
+ var lastActivityAt = /* @__PURE__ */ __name((kv) => {
33000
+ return kv.get(LAST_ACTIVITY_KEY) ?? kv.get(META_KEY)?.createdAt ?? null;
33001
+ }, "lastActivityAt");
32771
33002
  var touchActivity = /* @__PURE__ */ __name((kv, receivedAt) => {
32772
- const last = kv.get(LAST_ACTIVITY_KEY);
32773
- if (last === void 0 || receivedAt - last >= CLEANUP_INTERVAL_MS) {
33003
+ const last = lastActivityAt(kv);
33004
+ if (last === null || receivedAt - last >= CLEANUP_INTERVAL_MS) {
32774
33005
  kv.put(LAST_ACTIVITY_KEY, receivedAt);
32775
33006
  }
32776
33007
  }, "touchActivity");
32777
- var lastActivityAt = /* @__PURE__ */ __name((kv) => {
32778
- return kv.get(LAST_ACTIVITY_KEY) ?? null;
32779
- }, "lastActivityAt");
32780
33008
  var linkedGlobalId = /* @__PURE__ */ __name((kv) => {
32781
33009
  return kv.get(META_KEY)?.linkedGlobalId ?? null;
32782
33010
  }, "linkedGlobalId");
@@ -33010,7 +33238,7 @@ var standsDown = /* @__PURE__ */ __name(async (storage, staleActivity) => {
33010
33238
  if (lastActivity === staleActivity) {
33011
33239
  return false;
33012
33240
  }
33013
- await storage.setAlarm(cleanupTarget(storage, Date.now(), lastActivity ?? Date.now()));
33241
+ await storage.setAlarm(cleanupTarget(storage, Date.now(), lastActivity));
33014
33242
  return true;
33015
33243
  }, "standsDown");
33016
33244
  var relink = /* @__PURE__ */ __name(async (storage, env, anonId, globalId) => {
@@ -33029,7 +33257,7 @@ var relink = /* @__PURE__ */ __name(async (storage, env, anonId, globalId) => {
33029
33257
  addSpanEvent("expiry relink failed", { level: "warn", attrs: { "anon.id": anonId, "global.id": globalId, error: String(error61) } });
33030
33258
  }
33031
33259
  if (mutated) {
33032
- await storage.setAlarm(cleanupTarget(storage, Date.now(), lastActivityAt(storage.kv) ?? Date.now()));
33260
+ await storage.setAlarm(cleanupTarget(storage, Date.now(), lastActivityAt(storage.kv)));
33033
33261
  }
33034
33262
  }, "relink");
33035
33263
  var unlinkFromPerson = /* @__PURE__ */ __name((env, anonId, globalId) => {
@@ -33588,10 +33816,9 @@ var AnonDO = class extends DurableObject2 {
33588
33816
  // In memory on purpose: a hibernated socket sent nothing, so a fresh bucket on wake is the right one.
33589
33817
  frameBuckets = /* @__PURE__ */ new WeakMap();
33590
33818
  emitBudget = createTokenBucket(EMIT_BURST, EMITS_PER_SECOND);
33591
- // deleteAll removes the live schema; only a later write recreates it.
33819
+ stored;
33820
+ // This instance deleted the visitor's storage; only a later write recreates it.
33592
33821
  wiped = false;
33593
- // True while processAction holds ingestQueue, including the transform await.
33594
- ingestTurn = false;
33595
33822
  // Warm write-through mirrors reset together after an expiry or an erase.
33596
33823
  kvCache;
33597
33824
  cachedStorage;
@@ -33607,7 +33834,6 @@ var AnonDO = class extends DurableObject2 {
33607
33834
  sites;
33608
33835
  constructor(ctx, env) {
33609
33836
  super(ctx, env);
33610
- applyMigrations(ctx.storage, ANON_MIGRATIONS);
33611
33837
  this.kvCache = cachedKv(ctx.storage.kv);
33612
33838
  this.cachedStorage = { kv: this.kvCache, sql: ctx.storage.sql };
33613
33839
  this.graph = ownGraphEnv(ctx, env);
@@ -33630,32 +33856,29 @@ var AnonDO = class extends DurableObject2 {
33630
33856
  const trace = parseTraceContext(request.headers.get(TRACE_HEADER));
33631
33857
  return runInvocation({ name: "do.anon.connect", trace, attrs: connectAttrs(request, geo) }, async () => {
33632
33858
  const context = await this.sites.resolve({ domainId: siteId }, Date.now());
33633
- this.revive();
33634
- const now = Date.now();
33635
- this.cleanupAlarm = await seedCleanupAlarm(this.ctx.storage, now, lastActivityAt(this.kv) ?? now, this.cleanupAlarm);
33636
- seedMeta(this.kv, userId, now);
33637
- touchActivity(this.kv, now);
33638
- if (geo !== void 0 && !hasGeoRecord(this.kv)) {
33639
- this.seedGeoOnConnect(userId, geo, now);
33859
+ if (this.isStored()) {
33860
+ touchActivity(this.kv, Date.now());
33640
33861
  }
33641
33862
  const { 0: client, 1: server } = new WebSocketPair();
33642
33863
  this.ctx.acceptWebSocket(server, [siteId]);
33643
- const attachment = connectionAttachment(request, geo, siteId);
33864
+ const attachment = connectionAttachment(request, geo, { siteId, anonId: userId });
33644
33865
  server.serializeAttachment(attachment);
33645
33866
  this.sendSharedContext([server], context);
33646
- const page = connectPage(request);
33647
- this.appFanOut(
33648
- {
33649
- kind: "connect",
33650
- ...attachment.cf === void 0 ? {} : { geo: attachment.cf },
33651
- host: attachment.host,
33652
- ...page === void 0 ? {} : { page },
33653
- ...attachment.session === void 0 ? {} : { session: attachment.session },
33654
- gpc: attachment.gpc
33655
- },
33656
- userId,
33657
- context
33658
- );
33867
+ if (request.headers.get(RECONNECT_HEADER) !== "1") {
33868
+ const page = connectPage(request);
33869
+ this.appFanOut(
33870
+ {
33871
+ kind: "connect",
33872
+ ...attachment.cf === void 0 ? {} : { geo: attachment.cf },
33873
+ host: attachment.host,
33874
+ ...page === void 0 ? {} : { page },
33875
+ ...attachment.session === void 0 ? {} : { session: attachment.session },
33876
+ gpc: attachment.gpc
33877
+ },
33878
+ userId,
33879
+ context
33880
+ );
33881
+ }
33659
33882
  return new Response(null, { status: 101, webSocket: client });
33660
33883
  });
33661
33884
  }
@@ -33693,7 +33916,7 @@ var AnonDO = class extends DurableObject2 {
33693
33916
  }
33694
33917
  if (frame.t === "capture") {
33695
33918
  const { accepted } = await this.capture(
33696
- { apps: frame.apps, location: socketLocation(attachment.cf), gpc: attachment.gpc === true },
33919
+ { anonId: userId, apps: frame.apps, location: socketLocation(attachment.cf), gpc: attachment.gpc === true },
33697
33920
  site
33698
33921
  );
33699
33922
  this.respond(ws, { t: "captureAck", ref, anonId: userId, accepted });
@@ -33738,7 +33961,8 @@ var AnonDO = class extends DurableObject2 {
33738
33961
  syncFromGlobal(snapshot, trace) {
33739
33962
  return runInvocation({ name: "do.anon.sync_from_global", trace, attrs: { "global.id": snapshot.globalId } }, () => {
33740
33963
  return this.enqueue(async () => {
33741
- if (await this.discardIfUnused()) {
33964
+ const anonId = storedAnonId(this.kv);
33965
+ if (anonId === null) {
33742
33966
  addSpanEvent("snapshot dropped: no visitor state", { level: "warn", attrs: { "global.id": snapshot.globalId } });
33743
33967
  return;
33744
33968
  }
@@ -33746,7 +33970,7 @@ var AnonDO = class extends DurableObject2 {
33746
33970
  addSpanEvent("foreign snapshot dropped", { level: "warn", attrs: { "global.id": snapshot.globalId } });
33747
33971
  return;
33748
33972
  }
33749
- this.cleanupAlarm = await seedCleanupAlarm(this.ctx.storage, Date.now(), lastActivityAt(this.kv) ?? Date.now(), this.cleanupAlarm);
33973
+ await this.storeVisitor(anonId, Date.now());
33750
33974
  await this.applyOutcome(applySnapshot(this.cachedStorage, snapshot, this.snapshotCaches()));
33751
33975
  });
33752
33976
  });
@@ -33763,7 +33987,7 @@ var AnonDO = class extends DurableObject2 {
33763
33987
  this.respond(ws, { t: "error", ref: parsed.ref, message: "invalid message" });
33764
33988
  return;
33765
33989
  }
33766
- const userId = storedAnonId(this.kv);
33990
+ const userId = attachment?.anonId ?? null;
33767
33991
  setSpanAttributes(frameSpanAttrs(parsed.frame, userId));
33768
33992
  if (parsed.frame.t === "getConsent") {
33769
33993
  this.respond(ws, this.consentReply(parsed.ref, userId));
@@ -33813,10 +34037,6 @@ var AnonDO = class extends DurableObject2 {
33813
34037
  };
33814
34038
  return runInvocation({ name: "do.anon.app_push", trace, attrs }, () => {
33815
34039
  return this.enqueue(async () => {
33816
- if (await this.discardIfUnused()) {
33817
- addSpanEvent("app push dropped: no visitor state", { level: "warn", attrs: { app: push.slug } });
33818
- return;
33819
- }
33820
34040
  this.deliverAppPush(push, await this.sites.resolve(site, Date.now()));
33821
34041
  });
33822
34042
  });
@@ -33836,26 +34056,19 @@ var AnonDO = class extends DurableObject2 {
33836
34056
  // so a value the caps refused must never be reported as stored.
33837
34057
  capture(input2, site, trace) {
33838
34058
  return runInvocation({ name: "do.anon.capture", trace }, () => {
33839
- return this.enqueue(async () => {
33840
- if (await this.discardIfUnused()) {
33841
- addSpanEvent("capture dropped: no visitor state", { level: "warn" });
33842
- return { accepted: {} };
33843
- }
33844
- return this.runCaptures(input2.apps, site, { location: input2.location, gpc: input2.gpc });
34059
+ return this.enqueue(() => {
34060
+ return this.runCaptures(input2.anonId, input2.apps, site, { location: input2.location, gpc: input2.gpc });
33845
34061
  });
33846
34062
  });
33847
34063
  }
33848
- // The server door. There is no connect to have seeded the visitor, and a migrated visitor can
33849
- // carry click ids before any identify, so unlike the socket this one creates the profile.
34064
+ // A migrated visitor can carry click ids before any identify, so this door creates the profile.
33850
34065
  serverCapture(input2, site, trace) {
33851
34066
  return runInvocation({ name: "do.anon.server_capture", trace }, () => {
33852
34067
  return this.enqueue(async () => {
33853
- this.revive();
33854
34068
  const now = Date.now();
33855
- this.cleanupAlarm = await seedCleanupAlarm(this.ctx.storage, now, lastActivityAt(this.kv) ?? now, this.cleanupAlarm);
33856
- seedMeta(this.kv, input2.anonId, now);
34069
+ await this.storeVisitor(input2.anonId, now);
33857
34070
  touchActivity(this.kv, now);
33858
- return this.runCaptures(input2.apps, site, { location: null, gpc: false });
34071
+ return this.runCaptures(input2.anonId, input2.apps, site, { location: null, gpc: false });
33859
34072
  });
33860
34073
  });
33861
34074
  }
@@ -33864,7 +34077,8 @@ var AnonDO = class extends DurableObject2 {
33864
34077
  appCaptures(input2, site, trace) {
33865
34078
  return runInvocation({ name: "do.anon.app_captures", trace, attrs: { "app.slug": input2.slug } }, () => {
33866
34079
  return this.enqueue(async () => {
33867
- if (await this.discardIfUnused()) {
34080
+ const anonId = this.visitorId();
34081
+ if (anonId === null) {
33868
34082
  addSpanEvent("capture dropped: no visitor state", { level: "warn", attrs: { app: input2.slug } });
33869
34083
  return;
33870
34084
  }
@@ -33877,7 +34091,8 @@ var AnonDO = class extends DurableObject2 {
33877
34091
  })) {
33878
34092
  return;
33879
34093
  }
33880
- this.storeCaptures(
34094
+ await this.storeCaptures(
34095
+ anonId,
33881
34096
  input2.writes.map((write) => {
33882
34097
  return { ...write, appId: input2.slug, source: "app", ttlMs: declaredTtlMs(context.registry, input2.slug, write.key) };
33883
34098
  })
@@ -33885,7 +34100,7 @@ var AnonDO = class extends DurableObject2 {
33885
34100
  });
33886
34101
  });
33887
34102
  }
33888
- async runCaptures(apps, site, { location, gpc }) {
34103
+ async runCaptures(anonId, apps, site, { location, gpc }) {
33889
34104
  const context = await this.sites.resolve(site, Date.now());
33890
34105
  const writes = acceptedCaptures(context.registry, apps, {
33891
34106
  consent: consentState2(this.kv),
@@ -33893,13 +34108,13 @@ var AnonDO = class extends DurableObject2 {
33893
34108
  gpc,
33894
34109
  location
33895
34110
  });
33896
- return { accepted: this.storeCaptures(writes) };
34111
+ return { accepted: await this.storeCaptures(anonId, writes) };
33897
34112
  }
33898
- storeCaptures(writes) {
34113
+ async storeCaptures(anonId, writes) {
33899
34114
  if (writes.length === 0) {
33900
34115
  return {};
33901
34116
  }
33902
- this.revive();
34117
+ await this.storeVisitor(anonId, Date.now());
33903
34118
  const accepted = {};
33904
34119
  const stored = writeCaptures(this.sql, writes, Date.now(), this.captureMirror);
33905
34120
  for (const write of stored) {
@@ -33915,7 +34130,8 @@ var AnonDO = class extends DurableObject2 {
33915
34130
  appContext(input2, site, trace) {
33916
34131
  return runInvocation({ name: "do.anon.app_context", trace, attrs: { "app.slug": input2.slug } }, () => {
33917
34132
  return this.enqueue(async () => {
33918
- if (await this.discardIfUnused()) {
34133
+ const anonId = this.visitorId();
34134
+ if (anonId === null) {
33919
34135
  addSpanEvent("context dropped: no visitor state", { level: "warn", attrs: { app: input2.slug } });
33920
34136
  return [];
33921
34137
  }
@@ -33940,15 +34156,18 @@ var AnonDO = class extends DurableObject2 {
33940
34156
  }
33941
34157
  return true;
33942
34158
  });
33943
- return this.storeContext(writes.map((write) => ({ ...write, appId: input2.slug })));
34159
+ return this.storeContext(
34160
+ anonId,
34161
+ writes.map((write) => ({ ...write, appId: input2.slug }))
34162
+ );
33944
34163
  });
33945
34164
  });
33946
34165
  }
33947
- async storeContext(writes) {
34166
+ async storeContext(anonId, writes) {
33948
34167
  if (writes.length === 0) {
33949
34168
  return [];
33950
34169
  }
33951
- this.revive();
34170
+ await this.storeVisitor(anonId, Date.now());
33952
34171
  const stored = [];
33953
34172
  for (const write of writes) {
33954
34173
  if (writeContext(this.sql, [write], Date.now(), this.contextMirror).length === 0) {
@@ -33966,7 +34185,7 @@ var AnonDO = class extends DurableObject2 {
33966
34185
  return stored;
33967
34186
  }
33968
34187
  hasSharedContext() {
33969
- return !this.wiped && Object.keys(allContext(this.sql, "shared", this.contextMirror)).length > 0;
34188
+ return this.isStored() && Object.keys(allContext(this.sql, "shared", this.contextMirror)).length > 0;
33970
34189
  }
33971
34190
  // Shared context is the level every app may read, so the browser gets it too — the connecting
33972
34191
  // socket on open, every socket on a change. Private never leaves the server. The connect frame is
@@ -33974,7 +34193,7 @@ var AnonDO = class extends DurableObject2 {
33974
34193
  // context from one that has not arrived yet.
33975
34194
  sendSharedContext(sockets, context) {
33976
34195
  const consent2 = consentState2(this.kv);
33977
- const stored = allContext(this.sql, "shared", this.contextMirror);
34196
+ const stored = this.isStored() ? allContext(this.sql, "shared", this.contextMirror) : {};
33978
34197
  for (const socket of sockets) {
33979
34198
  const { gpc } = socket.deserializeAttachment();
33980
34199
  sendFrame(socket, sharedContextFrame(context, consent2, stored, gpc === true));
@@ -34002,12 +34221,10 @@ var AnonDO = class extends DurableObject2 {
34002
34221
  }
34003
34222
  }
34004
34223
  }
34005
- // A read must not resurrect a visitor: an rpc or workflow asking about an expired or unknown anon
34006
- // would otherwise leave an empty object behind that no cleanup alarm collects.
34007
34224
  getAppContext(slug, site, trace) {
34008
34225
  return runInvocation({ name: "do.anon.get_app_context", trace, attrs: { "app.slug": slug } }, () => {
34009
34226
  return this.enqueue(async () => {
34010
- if (await this.discardIfUnused()) {
34227
+ if (!this.isStored()) {
34011
34228
  return readableContext(void 0, {});
34012
34229
  }
34013
34230
  const context = await this.sites.resolve(site, Date.now());
@@ -34026,20 +34243,18 @@ var AnonDO = class extends DurableObject2 {
34026
34243
  });
34027
34244
  });
34028
34245
  }
34029
- // Committed projection: must not join ingestQueue (a transform that lists facts holds that slot)
34030
- // and must not discardIfUnused during an ingest turn (meta is seeded after the transform).
34246
+ // Must not join ingestQueue: a transform that lists facts holds that slot.
34031
34247
  getAppFacts(slug, query, site, trace) {
34032
34248
  return runInvocation({ name: "do.anon.get_app_facts", trace, attrs: { "app.slug": slug } }, async () => {
34033
34249
  const context = await this.sites.resolve(site, Date.now());
34034
- const unused = this.ingestTurn ? false : await this.discardIfUnused();
34035
34250
  const app2 = context.registry.apps[slug];
34036
- if (unused || app2 === void 0 || !app2.data.includes("facts")) {
34251
+ if (app2 === void 0 || !app2.data.includes("facts")) {
34037
34252
  return null;
34038
34253
  }
34039
34254
  if (!consentAllows(consentState2(this.kv), slug, app2.consent.category, { enforced: context.consentEnforced, gpc: false })) {
34040
34255
  return null;
34041
34256
  }
34042
- if (this.wiped) {
34257
+ if (!this.isStored()) {
34043
34258
  return { items: [], next: null };
34044
34259
  }
34045
34260
  return listAppFacts(this.sql, query, Date.now());
@@ -34074,8 +34289,7 @@ var AnonDO = class extends DurableObject2 {
34074
34289
  }
34075
34290
  }
34076
34291
  }
34077
- // A getConsent can arrive before the session is seeded, so the visitor is named only when there
34078
- // is one — the tag treats its absence as "cannot vouch for the memo" rather than as a match.
34292
+ // No anonId tells the tag "cannot vouch for the memo", not a match.
34079
34293
  consentReply(ref, userId) {
34080
34294
  return { t: "consentState", ref, ...userId === null ? {} : { anonId: userId }, ...consentState2(this.kv) };
34081
34295
  }
@@ -34108,7 +34322,6 @@ var AnonDO = class extends DurableObject2 {
34108
34322
  return run;
34109
34323
  }
34110
34324
  async processAction(action, context) {
34111
- this.ingestTurn = true;
34112
34325
  let recorded = action;
34113
34326
  try {
34114
34327
  if (isNoopAction(action)) {
@@ -34122,12 +34335,15 @@ var AnonDO = class extends DurableObject2 {
34122
34335
  await this.runAction(transformed.action, transformed.releaseEmissions, context);
34123
34336
  } finally {
34124
34337
  this.recordActionBlocks(recorded, context);
34125
- this.ingestTurn = false;
34126
34338
  }
34127
34339
  }
34128
34340
  recordActionBlocks(action, context) {
34129
34341
  setSpanAttributes(
34130
- actionBlocks(action, this.appsSnapshot(action.userId, context), this.wiped ? [] : allIdentifiers(this.sql, this.identifiersMirror))
34342
+ actionBlocks(
34343
+ action,
34344
+ this.appsSnapshot(action.userId, context),
34345
+ this.isStored() ? allIdentifiers(this.sql, this.identifiersMirror) : []
34346
+ )
34131
34347
  );
34132
34348
  }
34133
34349
  async runAction(current, releaseEmissions, context) {
@@ -34136,14 +34352,7 @@ var AnonDO = class extends DurableObject2 {
34136
34352
  this.releaseTransformEmissions(releaseEmissions);
34137
34353
  return;
34138
34354
  }
34139
- this.revive();
34140
- this.cleanupAlarm = await seedCleanupAlarm(
34141
- this.ctx.storage,
34142
- Date.now(),
34143
- lastActivityAt(this.kv) ?? current.receivedAt,
34144
- this.cleanupAlarm
34145
- );
34146
- seedMeta(this.kv, current.userId, current.receivedAt);
34355
+ await this.storeVisitor(current.userId, current.receivedAt);
34147
34356
  touchActivity(this.kv, current.receivedAt);
34148
34357
  const novelIdentifier = this.recordIdentifiers(current);
34149
34358
  if (novelIdentifier && linkedGlobalId(this.kv) !== null) {
@@ -34200,16 +34409,15 @@ var AnonDO = class extends DurableObject2 {
34200
34409
  }
34201
34410
  appsSnapshot(anonId, context) {
34202
34411
  const meta3 = storedMeta(this.kv);
34412
+ const stored = this.isStored();
34203
34413
  return {
34204
34414
  anonId,
34205
34415
  globalId: meta3?.linkedGlobalId ?? null,
34206
- createdAt: meta3?.createdAt ?? null,
34416
+ createdAt: meta3?.createdAt ?? Date.now(),
34207
34417
  pii: userProperties(this.kv),
34208
- // A wipe dropped the sql tables, so reading them would throw — and reviving for a transform
34209
- // snapshot would strand recreated storage when the chain fails or drops.
34210
- custom: this.wiped ? {} : customProperties(this.sql, this.customPropsMirror),
34211
- captures: this.wiped ? {} : allCaptures(this.sql, Date.now(), this.captureMirror),
34212
- context: this.wiped ? { private: {}, shared: {} } : {
34418
+ custom: stored ? customProperties(this.sql, this.customPropsMirror) : {},
34419
+ captures: stored ? allCaptures(this.sql, Date.now(), this.captureMirror) : {},
34420
+ context: !stored ? { private: {}, shared: {} } : {
34213
34421
  private: allContext(this.sql, "private", this.contextMirror),
34214
34422
  shared: readableSharedContext(
34215
34423
  context.registry,
@@ -34244,7 +34452,7 @@ var AnonDO = class extends DurableObject2 {
34244
34452
  getProperties(trace) {
34245
34453
  return runInvocation({ name: "do.anon.get_properties", trace }, () => {
34246
34454
  return this.enqueue(async () => {
34247
- if (await this.discardIfUnused()) {
34455
+ if (!this.isStored()) {
34248
34456
  return {};
34249
34457
  }
34250
34458
  return { ...userProperties(this.kv), ...customProperties(this.sql, this.customPropsMirror) };
@@ -34254,16 +34462,15 @@ var AnonDO = class extends DurableObject2 {
34254
34462
  getConsent(trace) {
34255
34463
  return runInvocation({ name: "do.anon.get_consent", trace }, () => {
34256
34464
  return this.enqueue(async () => {
34257
- await this.discardIfUnused();
34258
34465
  return consentState2(this.kv);
34259
34466
  });
34260
34467
  });
34261
34468
  }
34262
- // Null means no session: the id was never seeded or its profile expired, and the object it addressed is discarded.
34469
+ // Null means no session: the id stored nothing yet or its profile expired.
34263
34470
  getPersonLink(trace) {
34264
34471
  return runInvocation({ name: "do.anon.get_person_link", trace }, () => {
34265
34472
  return this.enqueue(async () => {
34266
- if (await this.discardIfUnused()) {
34473
+ if (!this.isStored()) {
34267
34474
  return null;
34268
34475
  }
34269
34476
  return { globalId: linkedGlobalId(this.kv) };
@@ -34273,7 +34480,7 @@ var AnonDO = class extends DurableObject2 {
34273
34480
  getGlobalId(trace) {
34274
34481
  return runInvocation({ name: "do.anon.get_global_id", trace }, () => {
34275
34482
  return this.enqueue(async () => {
34276
- if (await this.discardIfUnused()) {
34483
+ if (!this.isStored()) {
34277
34484
  return null;
34278
34485
  }
34279
34486
  return linkedGlobalId(this.kv);
@@ -34283,8 +34490,8 @@ var AnonDO = class extends DurableObject2 {
34283
34490
  erasable(trace) {
34284
34491
  return runInvocation({ name: "do.anon.erasable", trace }, () => {
34285
34492
  return this.enqueue(async () => {
34286
- if (await this.discardIfUnused()) {
34287
- return null;
34493
+ if (!this.isStored()) {
34494
+ return this.ctx.getWebSockets().length === 0 ? null : { globalId: null, identifiers: [] };
34288
34495
  }
34289
34496
  const identifiers = allIdentifiers(this.sql, this.identifiersMirror).map(({ type, value }) => ({ type, value }));
34290
34497
  return { globalId: linkedGlobalId(this.kv), identifiers };
@@ -34294,7 +34501,7 @@ var AnonDO = class extends DurableObject2 {
34294
34501
  inspect(trace) {
34295
34502
  return runInvocation({ name: "do.anon.inspect", trace }, () => {
34296
34503
  return this.enqueue(async () => {
34297
- if (await this.discardIfUnused()) {
34504
+ if (!this.isStored()) {
34298
34505
  return { exists: false };
34299
34506
  }
34300
34507
  return { exists: true, objectId: this.ctx.id.toString(), ...dumpStorage(this.ctx.storage) };
@@ -34304,7 +34511,7 @@ var AnonDO = class extends DurableObject2 {
34304
34511
  inspectTable(table, offset, limit, trace) {
34305
34512
  return runInvocation({ name: "do.anon.inspect_table", trace, attrs: { "inspect.table": table } }, () => {
34306
34513
  return this.enqueue(async () => {
34307
- if (await this.discardIfUnused()) {
34514
+ if (!this.isStored()) {
34308
34515
  return null;
34309
34516
  }
34310
34517
  return readTable(this.ctx.storage, table, offset, limit);
@@ -34314,13 +34521,16 @@ var AnonDO = class extends DurableObject2 {
34314
34521
  erase(trace) {
34315
34522
  return runInvocation({ name: "do.anon.erase", trace }, () => {
34316
34523
  return this.enqueue(async () => {
34317
- if (await this.discardIfUnused()) {
34524
+ const stored = this.isStored();
34525
+ if (!stored && this.ctx.getWebSockets().length === 0) {
34318
34526
  return false;
34319
34527
  }
34320
- try {
34321
- await eraseAnon(this.ctx.storage, this.graph);
34322
- } finally {
34323
- this.resetMirrors();
34528
+ if (stored) {
34529
+ try {
34530
+ await eraseAnon(this.ctx.storage, this.graph);
34531
+ } finally {
34532
+ this.resetMirrors();
34533
+ }
34324
34534
  }
34325
34535
  this.wiped = true;
34326
34536
  for (const socket of this.ctx.getWebSockets()) {
@@ -34330,19 +34540,24 @@ var AnonDO = class extends DurableObject2 {
34330
34540
  });
34331
34541
  });
34332
34542
  }
34333
- // Addressing an object runs the constructor's schema exec, so a call that never writes — a read, an
34334
- // inspect, a push or a snapshot for an id that was never used — would otherwise leave an empty object
34335
- // behind that no cleanup alarm ever collects. Every write path seeds meta before it stores anything,
34336
- // so a missing meta record means there is nothing to lose.
34337
- async discardIfUnused() {
34338
- if (storedAnonId(this.kv) !== null) {
34339
- return false;
34543
+ // Every write seeds meta through storeVisitor first, so an object without it has no schema to read.
34544
+ isStored() {
34545
+ if (this.stored === void 0) {
34546
+ this.stored = storedMeta(this.kv) !== void 0;
34547
+ if (this.stored) {
34548
+ applyMigrations(this.ctx.storage, ANON_MIGRATIONS);
34549
+ }
34340
34550
  }
34341
- if (!this.wiped) {
34342
- await this.ctx.storage.deleteAll();
34343
- this.wiped = true;
34551
+ return this.stored;
34552
+ }
34553
+ // Stubs address this object by anon id, but an app may name any user, so only a canonical id counts.
34554
+ visitorId() {
34555
+ const anonId = storedAnonId(this.kv);
34556
+ if (anonId !== null || this.wiped) {
34557
+ return anonId;
34344
34558
  }
34345
- return true;
34559
+ const name = this.ctx.id.name;
34560
+ return name !== void 0 && isCanonicalId(name) ? name : null;
34346
34561
  }
34347
34562
  appendProjectionHistory(outcomes, source) {
34348
34563
  for (const { entry } of outcomes) {
@@ -34551,6 +34766,7 @@ var AnonDO = class extends DurableObject2 {
34551
34766
  }
34552
34767
  async alarm() {
34553
34768
  return runInvocation({ name: "do.anon.cleanup" }, async () => {
34769
+ applyMigrations(this.ctx.storage, ANON_MIGRATIONS);
34554
34770
  this.cleanupAlarm = void 0;
34555
34771
  const now = Date.now();
34556
34772
  const lastActivity = lastActivityAt(this.kv) ?? now;
@@ -34575,6 +34791,7 @@ var AnonDO = class extends DurableObject2 {
34575
34791
  }
34576
34792
  resetMirrors() {
34577
34793
  this.kvCache.clear();
34794
+ this.stored = void 0;
34578
34795
  this.cleanupAlarm = void 0;
34579
34796
  this.identifiersMirror.reset();
34580
34797
  this.conversionsMirror.reset();
@@ -34582,14 +34799,22 @@ var AnonDO = class extends DurableObject2 {
34582
34799
  this.captureMirror.reset();
34583
34800
  this.contextMirror.reset();
34584
34801
  }
34585
- revive() {
34586
- if (this.wiped) {
34802
+ // Callers run in ingestQueue and nothing awaits between the stored check and the seeds.
34803
+ async storeVisitor(anonId, at) {
34804
+ this.cleanupAlarm = await seedCleanupAlarm(this.ctx.storage, Date.now(), lastActivityAt(this.kv) ?? at, this.cleanupAlarm);
34805
+ if (!this.isStored()) {
34587
34806
  applyMigrations(this.ctx.storage, ANON_MIGRATIONS);
34807
+ seedMeta(this.kv, anonId, at);
34808
+ this.stored = true;
34588
34809
  this.wiped = false;
34589
34810
  }
34811
+ const socket = hasGeoRecord(this.kv) ? void 0 : this.ctx.getWebSockets()[0]?.deserializeAttachment();
34812
+ if (socket !== void 0) {
34813
+ updateGeo(this.cachedStorage, attachmentGeo(socket), at);
34814
+ }
34590
34815
  }
34591
34816
  async retargetCleanupAlarm(now) {
34592
- const target = cleanupTarget(this.cachedStorage, now, lastActivityAt(this.kv) ?? now);
34817
+ const target = cleanupTarget(this.cachedStorage, now, lastActivityAt(this.kv));
34593
34818
  if (this.cleanupAlarm === target) {
34594
34819
  return;
34595
34820
  }
@@ -34599,14 +34824,6 @@ var AnonDO = class extends DurableObject2 {
34599
34824
  await this.ctx.storage.setAlarm(target);
34600
34825
  this.cleanupAlarm = target;
34601
34826
  }
34602
- seedGeoOnConnect(anonId, geo, observedAt) {
34603
- const run = this.enqueue(async () => {
34604
- this.revive();
34605
- updateGeo(this.cachedStorage, geo, observedAt);
34606
- await this.retargetCleanupAlarm(Date.now());
34607
- });
34608
- this.ctx.waitUntil(run.catch((error61) => console.warn("geo seed failed", { anonId, error: error61 })));
34609
- }
34610
34827
  };
34611
34828
 
34612
34829
  // src/id-graph/global/do.ts