@blotout/qinto-cli 0.1.8 → 0.1.9

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.
@@ -15966,12 +15966,21 @@ var ENDPOINT_CONTEXT_HEADER = "x-qinto-endpoint-context";
15966
15966
  var ENDPOINT_OUTCOMES_HEADER = "x-qinto-endpoint-outcomes";
15967
15967
  var MAX_ENDPOINT_BODY_BYTES = 1024 * 1024;
15968
15968
  var MAX_ENDPOINT_OUTCOMES_BYTES = 64 * 1024;
15969
+ var MAX_ENDPOINT_PATH_LENGTH = 16 * 1024;
15969
15970
  var installSchema3 = external_exports.object({ id: external_exports.string().min(1).max(64), ord: external_exports.number().int().min(1), label: external_exports.string().max(100) }).strict();
15970
15971
  var endpointContextSchema = external_exports.object({
15971
- path: external_exports.string().min(1).max(255),
15972
+ // The declared template, which keys the bundle's handler map; `path` is what the caller asked
15973
+ // for. Only `route` may key the rate limiter — a concrete path gives every parameter value its
15974
+ // own bucket, which is no limit at all.
15975
+ route: external_exports.string().min(1).max(255),
15976
+ path: external_exports.string().min(1).max(MAX_ENDPOINT_PATH_LENGTH),
15977
+ // A name is cut from `route`, so the route's own cap bounds it; anything tighter would reject a
15978
+ // parameter the manifest grammar accepts and the worker already matched.
15979
+ params: external_exports.record(external_exports.string().min(1).max(255), external_exports.string().max(MAX_ENDPOINT_PATH_LENGTH)).default({}),
15972
15980
  method: external_exports.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
15973
15981
  mode: external_exports.enum(["webhook", "api"]),
15974
15982
  version: external_exports.string().min(1).max(32),
15983
+ site: external_exports.object({ origin: external_exports.string().min(1).max(255) }).strict(),
15975
15984
  installs: external_exports.array(installSchema3).min(1).max(15)
15976
15985
  }).strict().refine((context) => {
15977
15986
  return context.mode === "webhook" || context.installs.length === 1;
@@ -15998,6 +16007,71 @@ var aggregateEndpointOutcomes = /* @__PURE__ */ __name((outcomes) => {
15998
16007
  return { status: failed ? 502 : 200 };
15999
16008
  }, "aggregateEndpointOutcomes");
16000
16009
 
16010
+ // ../../libs/app-envelope/src/observability.ts
16011
+ var APP_OBSERVABILITY_HEADER = "x-qinto-app-observability";
16012
+ var APP_OBSERVABILITY_VERSION = 1;
16013
+ var MAX_APP_OBSERVABILITY_EVENTS = 20;
16014
+ var MAX_APP_SAVED_ERRORS = 5;
16015
+ var MAX_APP_EVENT_NAME_BYTES = 128;
16016
+ var MAX_APP_ACTION_NAME_BYTES = 256;
16017
+ var MAX_APP_ERROR_DESCRIPTION_BYTES = 2048;
16018
+ var MAX_APP_ERROR_PAYLOAD_BYTES = 4096;
16019
+ var MAX_APP_DATA_POINT_BLOB_BYTES = 16 * 1024;
16020
+ var MAX_APP_OBSERVABILITY_BYTES = 32 * 1024;
16021
+ var APP_OBSERVABILITY_JSON_BUDGET = MAX_APP_OBSERVABILITY_BYTES / 4 * 3;
16022
+ var MAX_APP_DATA_POINTS_PER_INVOCATION = 250;
16023
+ var APP_EVENT_STATUSES = ["success", "error"];
16024
+ var APP_ERROR_CATEGORIES = ["authorization", "configuration", "validation", "rate_limit", "upstream", "internal"];
16025
+ var encoder3 = new TextEncoder();
16026
+ var decoder2 = new TextDecoder();
16027
+ var observabilityEventSchema = external_exports.object({ ord: external_exports.number().int().min(1), name: external_exports.string().min(1).max(MAX_APP_EVENT_NAME_BYTES), status: external_exports.enum(APP_EVENT_STATUSES) }).strict();
16028
+ var savedErrorSchema = external_exports.object({
16029
+ ord: external_exports.number().int().min(1),
16030
+ category: external_exports.enum(APP_ERROR_CATEGORIES),
16031
+ action: external_exports.string().min(1).max(MAX_APP_ACTION_NAME_BYTES),
16032
+ description: external_exports.string().min(1).max(MAX_APP_ERROR_DESCRIPTION_BYTES),
16033
+ payload: external_exports.string().max(MAX_APP_ERROR_PAYLOAD_BYTES).optional(),
16034
+ error: external_exports.string().max(MAX_APP_ERROR_PAYLOAD_BYTES).optional()
16035
+ }).strict();
16036
+ var appObservabilityRecordSchema = external_exports.object({
16037
+ v: external_exports.literal(APP_OBSERVABILITY_VERSION),
16038
+ truncated: external_exports.boolean().optional(),
16039
+ events: external_exports.array(observabilityEventSchema).max(MAX_APP_OBSERVABILITY_EVENTS),
16040
+ errors: external_exports.array(savedErrorSchema).max(MAX_APP_SAVED_ERRORS)
16041
+ }).strict();
16042
+ var utf8Length = /* @__PURE__ */ __name((value) => {
16043
+ return encoder3.encode(value).length;
16044
+ }, "utf8Length");
16045
+ var truncateUtf8 = /* @__PURE__ */ __name((value, maxBytes) => {
16046
+ if (utf8Length(value) <= maxBytes) {
16047
+ return value;
16048
+ }
16049
+ let kept = "";
16050
+ let used = 0;
16051
+ for (const character of value) {
16052
+ const size = utf8Length(character);
16053
+ if (used + size > maxBytes) {
16054
+ break;
16055
+ }
16056
+ kept += character;
16057
+ used += size;
16058
+ }
16059
+ return kept;
16060
+ }, "truncateUtf8");
16061
+ var parseAppObservability = /* @__PURE__ */ __name((header) => {
16062
+ if (header === null || header.length > MAX_APP_OBSERVABILITY_BYTES) {
16063
+ return null;
16064
+ }
16065
+ let parsed;
16066
+ try {
16067
+ parsed = JSON.parse(decoder2.decode(Uint8Array.from(atob(header), (character) => character.charCodeAt(0))));
16068
+ } catch {
16069
+ return null;
16070
+ }
16071
+ const result = appObservabilityRecordSchema.safeParse(parsed);
16072
+ return result.success ? result.data : null;
16073
+ }, "parseAppObservability");
16074
+
16001
16075
  // ../../libs/app-envelope/src/ui.ts
16002
16076
  var UI_CONTEXT_HEADER = "x-qinto-ui-context";
16003
16077
  var MAX_UI_BODY_BYTES = 1024 * 1024;
@@ -16204,13 +16278,13 @@ var getPattern = /* @__PURE__ */ __name((label, next) => {
16204
16278
  }
16205
16279
  return null;
16206
16280
  }, "getPattern");
16207
- var tryDecode = /* @__PURE__ */ __name((str, decoder2) => {
16281
+ var tryDecode = /* @__PURE__ */ __name((str, decoder3) => {
16208
16282
  try {
16209
- return decoder2(str);
16283
+ return decoder3(str);
16210
16284
  } catch {
16211
16285
  return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match2) => {
16212
16286
  try {
16213
- return decoder2(match2);
16287
+ return decoder3(match2);
16214
16288
  } catch {
16215
16289
  return match2;
16216
16290
  }
@@ -18977,6 +19051,82 @@ var readableContext = /* @__PURE__ */ __name((own, shared) => {
18977
19051
  return { private: own === void 0 ? null : wireEntry(own), shared: sharedContextWire(shared) };
18978
19052
  }, "readableContext");
18979
19053
 
19054
+ // src/apps/observability.ts
19055
+ var observabilityApp = /* @__PURE__ */ __name((slug, app2) => {
19056
+ return { slug, name: app2.name, version: app2.version };
19057
+ }, "observabilityApp");
19058
+ var createDataPointBudget = /* @__PURE__ */ __name(() => {
19059
+ let spent = 0;
19060
+ return {
19061
+ take: /* @__PURE__ */ __name(() => {
19062
+ if (spent >= MAX_APP_DATA_POINTS_PER_INVOCATION) {
19063
+ return false;
19064
+ }
19065
+ spent += 1;
19066
+ return true;
19067
+ }, "take")
19068
+ };
19069
+ }, "createDataPointBudget");
19070
+ var totalLength = /* @__PURE__ */ __name((blobs) => {
19071
+ return blobs.reduce((sum, blob) => {
19072
+ return sum + utf8Length(blob);
19073
+ }, 0);
19074
+ }, "totalLength");
19075
+ var errorBlobs = /* @__PURE__ */ __name((app2, install, record2, rowId) => {
19076
+ const fixed = [app2.slug, app2.name, install.id, install.label, app2.version, record2.category, record2.action, rowId];
19077
+ let remaining = MAX_APP_DATA_POINT_BLOB_BYTES - totalLength(fixed);
19078
+ const description = truncateUtf8(record2.description, Math.max(0, remaining));
19079
+ remaining -= utf8Length(description);
19080
+ const payload = truncateUtf8(record2.payload ?? "", Math.max(0, remaining));
19081
+ remaining -= utf8Length(payload);
19082
+ const error51 = truncateUtf8(record2.error ?? "", Math.max(0, remaining));
19083
+ return [app2.slug, app2.name, install.id, install.label, app2.version, record2.category, record2.action, description, payload, error51, rowId];
19084
+ }, "errorBlobs");
19085
+ var eventBlobs = /* @__PURE__ */ __name((app2, install, record2) => {
19086
+ return [app2.slug, app2.name, install.id, install.label, app2.version, record2.name, record2.status];
19087
+ }, "eventBlobs");
19088
+ var writeAppObservability = /* @__PURE__ */ __name((env, response, target) => {
19089
+ const record2 = parseAppObservability(response.headers.get(APP_OBSERVABILITY_HEADER));
19090
+ if (record2 === null) {
19091
+ return;
19092
+ }
19093
+ const byOrd = new Map(
19094
+ target.installs.map((install) => {
19095
+ return [install.ord, install];
19096
+ })
19097
+ );
19098
+ for (const event of record2.events) {
19099
+ const install = byOrd.get(event.ord);
19100
+ if (install === void 0) {
19101
+ addSpanEvent("app observability names an install the site did not send", { level: "warn", attrs: { app: target.app.slug } });
19102
+ continue;
19103
+ }
19104
+ if (!target.budget.take()) {
19105
+ addSpanEvent("app observability dropped at the data-point cap", { level: "warn", attrs: { app: target.app.slug } });
19106
+ return;
19107
+ }
19108
+ env.APP_EVENTS.writeDataPoint({ indexes: [`${install.id}:${event.status}`], blobs: eventBlobs(target.app, install, event) });
19109
+ }
19110
+ for (const saved of record2.errors) {
19111
+ const install = byOrd.get(saved.ord);
19112
+ if (install === void 0) {
19113
+ addSpanEvent("app observability names an install the site did not send", { level: "warn", attrs: { app: target.app.slug } });
19114
+ continue;
19115
+ }
19116
+ if (!target.budget.take()) {
19117
+ addSpanEvent("app observability dropped at the data-point cap", { level: "warn", attrs: { app: target.app.slug } });
19118
+ return;
19119
+ }
19120
+ env.APP_ERRORS.writeDataPoint({
19121
+ indexes: [`${install.id}:${saved.category}`],
19122
+ blobs: errorBlobs(target.app, install, saved, ulid3())
19123
+ });
19124
+ }
19125
+ if (record2.truncated === true) {
19126
+ addSpanEvent("the app recorded more observability than one invocation carries", { level: "warn", attrs: { app: target.app.slug } });
19127
+ }
19128
+ }, "writeAppObservability");
19129
+
18980
19130
  // src/apps/read-bounded.ts
18981
19131
  var readBoundedBody = /* @__PURE__ */ __name(async (message, maxBytes) => {
18982
19132
  const reader = message.body?.getReader();
@@ -19027,7 +19177,7 @@ var adoptAppSpans = /* @__PURE__ */ __name((response) => {
19027
19177
  }, "adoptAppSpans");
19028
19178
 
19029
19179
  // src/apps/invoke.ts
19030
- var HOOK_TIMEOUT_MS = 1e4;
19180
+ var APP_INVOCATION_TIMEOUT_MS = 1e4;
19031
19181
  var MAX_EMIT_DEPTH = 2;
19032
19182
  var HOOK_URL = "https://app/__qinto/hook";
19033
19183
  var invocationContext = /* @__PURE__ */ __name((env) => {
@@ -19233,19 +19383,24 @@ var hookResponseBody = /* @__PURE__ */ __name(async (response, slug) => {
19233
19383
  }
19234
19384
  return new TextDecoder().decode(bytes);
19235
19385
  }, "hookResponseBody");
19236
- var runInvokeApp = /* @__PURE__ */ __name(async (env, { target, trigger, snapshot, context, deliverPush, writeAttribution: writeAttribution2, writeContext: writeContext2 }) => {
19386
+ var runInvokeApp = /* @__PURE__ */ __name(async (env, { target, trigger, snapshot, context, deliverPush, writeAttribution: writeAttribution2, writeContext: writeContext2, budget }) => {
19237
19387
  try {
19238
19388
  const key = await deriveAppInternalKey(context.master, context.domainId, target.slug);
19239
19389
  const response = await postHookEnvelope(env, {
19240
19390
  binding: target.app.binding,
19241
19391
  key,
19242
19392
  envelope: buildEnvelope(target, trigger, snapshot, context),
19243
- timeoutMs: HOOK_TIMEOUT_MS
19393
+ timeoutMs: APP_INVOCATION_TIMEOUT_MS
19244
19394
  });
19245
19395
  if (response === void 0) {
19246
19396
  addSpanEvent("app service binding is missing", { level: "warn", attrs: { app: target.slug, binding: target.app.binding } });
19247
19397
  return;
19248
19398
  }
19399
+ writeAppObservability(env, response, {
19400
+ app: observabilityApp(target.slug, target.app),
19401
+ installs: target.installs,
19402
+ budget
19403
+ });
19249
19404
  setSpanAttributes({ "app.status": response.status });
19250
19405
  const body = await hookResponseBody(response, target.slug);
19251
19406
  if (body === void 0) {
@@ -19329,6 +19484,9 @@ var installSchema4 = external_exports.object({
19329
19484
  });
19330
19485
  var appSchema = external_exports.object({
19331
19486
  binding: external_exports.string().min(1),
19487
+ // Required, with no default, for the same reason `events` is: the display name is stored on every
19488
+ // observability data point, and a default would be a compatibility path for an older deploy.
19489
+ name: external_exports.string().min(1).max(100),
19332
19490
  version: external_exports.string().min(1),
19333
19491
  // Absent from an old embedded registry; the default never equals a real claim, so the ui proxy
19334
19492
  // refuses stale registries with token_stale instead of turning the app off.
@@ -19616,11 +19774,16 @@ var runRelayRpc = /* @__PURE__ */ __name(async (env, { context, body, callee, in
19616
19774
  // JSON.stringify drops an undefined key, and the envelope requires `args` to be present.
19617
19775
  rpc: { caller: body.slug, fn: body.fn, args: body.args ?? null }
19618
19776
  },
19619
- timeoutMs: HOOK_TIMEOUT_MS
19777
+ timeoutMs: APP_INVOCATION_TIMEOUT_MS
19620
19778
  });
19621
19779
  if (response === void 0) {
19622
19780
  return calleeFailed(body.callee, "The callee service binding is missing.");
19623
19781
  }
19782
+ writeAppObservability(env, response, {
19783
+ app: observabilityApp(body.callee, callee),
19784
+ installs: [install],
19785
+ budget: createDataPointBudget()
19786
+ });
19624
19787
  setSpanAttributes({ "app.status": response.status });
19625
19788
  if (!response.ok) {
19626
19789
  return calleeFailed(body.callee, `The callee answered status ${response.status}.`);
@@ -19888,6 +20051,18 @@ var controlTargets = /* @__PURE__ */ __name(() => {
19888
20051
  // src/apps/control.ts
19889
20052
  var CONTROL_PATH = "/__qinto/control";
19890
20053
  var MAX_CLOCK_SKEW_MS = 5 * 60 * 1e3;
20054
+ var writeControlObservability = /* @__PURE__ */ __name((env, upstream, envelope) => {
20055
+ const installs = envelope.schedule?.installs ?? (envelope.install === void 0 ? [] : [envelope.install]);
20056
+ const app2 = siteRegistry().registry.apps[envelope.app.slug];
20057
+ if (installs.length === 0 || app2 === void 0) {
20058
+ return;
20059
+ }
20060
+ writeAppObservability(env, upstream, {
20061
+ app: observabilityApp(envelope.app.slug, { name: app2.name, version: envelope.app.version }),
20062
+ installs,
20063
+ budget: createDataPointBudget()
20064
+ });
20065
+ }, "writeControlObservability");
19891
20066
  var textBody = /* @__PURE__ */ __name(async (request) => {
19892
20067
  const bytes = await readBoundedBody(request, MAX_CONTROL_BYTES);
19893
20068
  return bytes === null ? null : new TextDecoder().decode(bytes);
@@ -19956,6 +20131,7 @@ var handleControl = /* @__PURE__ */ __name(async (request, env) => {
19956
20131
  signal: AbortSignal.timeout(1e4)
19957
20132
  });
19958
20133
  adoptAppSpans(upstream);
20134
+ writeControlObservability(env, upstream, envelope);
19959
20135
  const responseBytes = await readBoundedBody(upstream, MAX_CONTROL_BYTES);
19960
20136
  if (responseBytes === null) {
19961
20137
  return Response.json({ error: "invalid_response" }, { status: 502 });
@@ -19974,6 +20150,73 @@ var appControlRoutes = new Hono2().post(CONTROL_PATH, (c) => {
19974
20150
  return handleControl(c.req.raw, c.env);
19975
20151
  });
19976
20152
 
20153
+ // ../../libs/app-manifest/src/route.ts
20154
+ var isParam = /* @__PURE__ */ __name((segment) => {
20155
+ return segment.startsWith(":");
20156
+ }, "isParam");
20157
+ var splitRoute = /* @__PURE__ */ __name((route) => {
20158
+ return route === "/" ? [] : route.slice(1).split("/");
20159
+ }, "splitRoute");
20160
+ var splitRequest = /* @__PURE__ */ __name((requestPath) => {
20161
+ if (requestPath === "/") {
20162
+ return [];
20163
+ }
20164
+ const decoded = [];
20165
+ for (const segment of requestPath.slice(1).split("/")) {
20166
+ try {
20167
+ decoded.push(decodeURIComponent(segment));
20168
+ } catch {
20169
+ return null;
20170
+ }
20171
+ }
20172
+ return decoded;
20173
+ }, "splitRequest");
20174
+ var matches = /* @__PURE__ */ __name((parts, segments) => {
20175
+ if (parts.length !== segments.length) {
20176
+ return false;
20177
+ }
20178
+ return parts.every((part, index) => {
20179
+ const segment = segments[index];
20180
+ return isParam(part) ? segment.length > 0 : part === segment;
20181
+ });
20182
+ }, "matches");
20183
+ var isMoreSpecific = /* @__PURE__ */ __name((candidate, current) => {
20184
+ for (const [index, part] of candidate.entries()) {
20185
+ const candidateParam = isParam(part);
20186
+ const currentParam = isParam(current[index]);
20187
+ if (candidateParam !== currentParam) {
20188
+ return !candidateParam;
20189
+ }
20190
+ }
20191
+ return false;
20192
+ }, "isMoreSpecific");
20193
+ var extract = /* @__PURE__ */ __name((parts, segments) => {
20194
+ const params = {};
20195
+ for (const [index, part] of parts.entries()) {
20196
+ if (isParam(part)) {
20197
+ params[part.slice(1)] = segments[index];
20198
+ }
20199
+ }
20200
+ return params;
20201
+ }, "extract");
20202
+ var matchEndpointPath = /* @__PURE__ */ __name((routes, requestPath) => {
20203
+ const segments = splitRequest(requestPath);
20204
+ if (segments === null) {
20205
+ return void 0;
20206
+ }
20207
+ let best;
20208
+ for (const route of routes) {
20209
+ const parts = splitRoute(route);
20210
+ if (matches(parts, segments) && (best === void 0 || isMoreSpecific(parts, best.parts))) {
20211
+ best = { route, parts };
20212
+ }
20213
+ }
20214
+ if (best === void 0) {
20215
+ return void 0;
20216
+ }
20217
+ return { route: best.route, params: extract(best.parts, segments) };
20218
+ }, "matchEndpointPath");
20219
+
19977
20220
  // ../../libs/worker-access-token/src/endpoints.ts
19978
20221
  var APP_ENDPOINT_PREFIX = "/app/";
19979
20222
  var MAX_CUSTOM_ENDPOINT_LENGTH = 128;
@@ -20209,7 +20452,76 @@ var requireScope = /* @__PURE__ */ __name((bit) => {
20209
20452
  };
20210
20453
  }, "requireScope");
20211
20454
 
20455
+ // src/apps/stream-deadlines.ts
20456
+ var streamWithDeadlines = /* @__PURE__ */ __name((source, { idleMs, totalMs }) => {
20457
+ if (source === null) {
20458
+ return null;
20459
+ }
20460
+ const reader = source.getReader();
20461
+ let target;
20462
+ let totalTimer;
20463
+ let idleTimer = null;
20464
+ let stopped = false;
20465
+ const clearIdleTimer = /* @__PURE__ */ __name(() => {
20466
+ if (idleTimer !== null) {
20467
+ clearTimeout(idleTimer);
20468
+ idleTimer = null;
20469
+ }
20470
+ }, "clearIdleTimer");
20471
+ const clearTimers = /* @__PURE__ */ __name(() => {
20472
+ clearTimeout(totalTimer);
20473
+ clearIdleTimer();
20474
+ }, "clearTimers");
20475
+ const stop = /* @__PURE__ */ __name((message) => {
20476
+ stopped = true;
20477
+ clearTimers();
20478
+ const error51 = new DOMException(message, "TimeoutError");
20479
+ target.error(error51);
20480
+ void reader.cancel(error51).catch(() => void 0);
20481
+ }, "stop");
20482
+ return new ReadableStream({
20483
+ start: /* @__PURE__ */ __name((controller) => {
20484
+ target = controller;
20485
+ totalTimer = setTimeout(() => {
20486
+ stop("The app endpoint stream exceeded its maximum duration.");
20487
+ }, totalMs);
20488
+ }, "start"),
20489
+ pull: /* @__PURE__ */ __name(async (controller) => {
20490
+ idleTimer = setTimeout(() => {
20491
+ stop("The app endpoint stream stopped producing data.");
20492
+ }, idleMs);
20493
+ let result;
20494
+ try {
20495
+ result = await reader.read();
20496
+ } catch (error51) {
20497
+ stopped = true;
20498
+ clearTimers();
20499
+ throw error51;
20500
+ }
20501
+ clearIdleTimer();
20502
+ if (stopped) {
20503
+ return;
20504
+ }
20505
+ if (result.done) {
20506
+ stopped = true;
20507
+ clearTimers();
20508
+ controller.close();
20509
+ return;
20510
+ }
20511
+ controller.enqueue(result.value);
20512
+ }, "pull"),
20513
+ cancel: /* @__PURE__ */ __name(async (reason) => {
20514
+ stopped = true;
20515
+ clearTimers();
20516
+ await reader.cancel(reason);
20517
+ }, "cancel")
20518
+ });
20519
+ }, "streamWithDeadlines");
20520
+
20212
20521
  // src/apps/endpoint-proxy.ts
20522
+ var API_ENDPOINT_START_TIMEOUT_MS = 1e4;
20523
+ var API_ENDPOINT_STREAM_IDLE_TIMEOUT_MS = 15e4;
20524
+ var API_ENDPOINT_STREAM_MAX_DURATION_MS = 3e5;
20213
20525
  var DENIED_REQUEST_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "x-cf-data"]);
20214
20526
  var refuse = /* @__PURE__ */ __name((status, error51) => {
20215
20527
  return Response.json({ error: error51 }, { status });
@@ -20225,16 +20537,23 @@ var matchEndpoint = /* @__PURE__ */ __name((app2, path, method) => {
20225
20537
  if (!isHttpMethod(method)) {
20226
20538
  return void 0;
20227
20539
  }
20228
- const declared = app2.endpoints.find((endpoint) => {
20229
- return endpoint.path === path && endpoint.methods.includes(method);
20540
+ const serving = app2.endpoints.filter((endpoint) => {
20541
+ return endpoint.methods.includes(method);
20230
20542
  });
20231
- if (declared === void 0) {
20543
+ const matched = matchEndpointPath(
20544
+ serving.map((endpoint) => endpoint.path),
20545
+ path
20546
+ );
20547
+ if (matched === void 0) {
20232
20548
  return void 0;
20233
20549
  }
20550
+ const declared = serving.find((endpoint) => {
20551
+ return endpoint.path === matched.route;
20552
+ });
20234
20553
  if (declared.auth !== "public" && declared.auth !== "token" || declared.mode !== "webhook" && declared.mode !== "api") {
20235
20554
  return void 0;
20236
20555
  }
20237
- return { path: declared.path, method, auth: declared.auth, mode: declared.mode };
20556
+ return { route: declared.path, path, params: matched.params, method, auth: declared.auth, mode: declared.mode };
20238
20557
  }, "matchEndpoint");
20239
20558
  var authorizeEndpoint = /* @__PURE__ */ __name(async (c, slug, endpoint) => {
20240
20559
  if (endpoint.auth === "public") {
@@ -20266,12 +20585,17 @@ var passThroughResponse = /* @__PURE__ */ __name((upstream) => {
20266
20585
  const headers = new Headers(upstream.headers);
20267
20586
  headers.delete("set-cookie");
20268
20587
  headers.delete(APP_SPANS_HEADER);
20588
+ headers.delete(APP_OBSERVABILITY_HEADER);
20269
20589
  for (const name of [...headers.keys()]) {
20270
20590
  if (name.startsWith("access-control-")) {
20271
20591
  headers.delete(name);
20272
20592
  }
20273
20593
  }
20274
- return new Response(upstream.body, { status: upstream.status, headers });
20594
+ const body = streamWithDeadlines(upstream.body, {
20595
+ idleMs: API_ENDPOINT_STREAM_IDLE_TIMEOUT_MS,
20596
+ totalMs: API_ENDPOINT_STREAM_MAX_DURATION_MS
20597
+ });
20598
+ return new Response(body, { status: upstream.status, headers });
20275
20599
  }, "passThroughResponse");
20276
20600
  var parseJson3 = /* @__PURE__ */ __name((text) => {
20277
20601
  try {
@@ -20301,6 +20625,17 @@ var webhookAck = /* @__PURE__ */ __name(async (slug, upstream) => {
20301
20625
  }
20302
20626
  return refuse(502, "upstream");
20303
20627
  }, "webhookAck");
20628
+ var fetchApiResponse = /* @__PURE__ */ __name(async (service, url2, init) => {
20629
+ const controller = new AbortController();
20630
+ const timer = setTimeout(() => {
20631
+ controller.abort(new DOMException("The app endpoint did not start within its deadline.", "TimeoutError"));
20632
+ }, API_ENDPOINT_START_TIMEOUT_MS);
20633
+ try {
20634
+ return await service.fetch(url2, { ...init, signal: controller.signal });
20635
+ } finally {
20636
+ clearTimeout(timer);
20637
+ }
20638
+ }, "fetchApiResponse");
20304
20639
  var runForwardToApp = /* @__PURE__ */ __name(async (c, input) => {
20305
20640
  const { slug, app: app2, endpoint, installs, body, search } = input;
20306
20641
  const context = invocationContext(c.env);
@@ -20320,25 +20655,30 @@ var runForwardToApp = /* @__PURE__ */ __name(async (c, input) => {
20320
20655
  headers.set(
20321
20656
  ENDPOINT_CONTEXT_HEADER,
20322
20657
  JSON.stringify({
20658
+ route: endpoint.route,
20323
20659
  path: endpoint.path,
20660
+ params: endpoint.params,
20324
20661
  method: endpoint.method,
20325
20662
  mode: endpoint.mode,
20326
20663
  version: app2.version,
20664
+ site: { origin: context.origin },
20327
20665
  installs: installs.map((install) => ({ id: install.id, ord: install.ord, label: install.label }))
20328
20666
  })
20329
20667
  );
20330
20668
  let upstream;
20331
20669
  try {
20332
- upstream = await service.fetch(`https://app/__qinto/endpoint${endpoint.path}${search}`, {
20333
- method: endpoint.method,
20334
- headers,
20335
- body,
20336
- signal: AbortSignal.timeout(HOOK_TIMEOUT_MS)
20337
- });
20670
+ const url2 = `https://app/__qinto/endpoint${endpoint.route}${search}`;
20671
+ const init = { method: endpoint.method, headers, body };
20672
+ upstream = endpoint.mode === "api" ? await fetchApiResponse(service, url2, init) : await service.fetch(url2, { ...init, signal: AbortSignal.timeout(APP_INVOCATION_TIMEOUT_MS) });
20338
20673
  } catch (error51) {
20339
20674
  return upstreamFailed(slug, String(error51));
20340
20675
  }
20341
20676
  adoptAppSpans(upstream);
20677
+ writeAppObservability(c.env, upstream, {
20678
+ app: observabilityApp(slug, app2),
20679
+ installs,
20680
+ budget: createDataPointBudget()
20681
+ });
20342
20682
  setSpanAttributes({ "app.status": upstream.status });
20343
20683
  return endpoint.mode === "api" ? passThroughResponse(upstream) : webhookAck(slug, upstream);
20344
20684
  }, "runForwardToApp");
@@ -20351,7 +20691,7 @@ var forwardToApp = /* @__PURE__ */ __name(async (c, input) => {
20351
20691
  "app.slug": slug,
20352
20692
  "app.version": app2.version,
20353
20693
  "app.binding": app2.binding,
20354
- "app.hook": `endpoint.${endpoint.method} ${endpoint.path}`,
20694
+ "app.hook": `endpoint.${endpoint.method} ${endpoint.route}`,
20355
20695
  "app.endpoint_mode": endpoint.mode,
20356
20696
  "app.installs": installs.length
20357
20697
  }
@@ -20367,7 +20707,11 @@ var handleAppEndpoint = /* @__PURE__ */ __name(async (c, slug) => {
20367
20707
  return refuse(404, "not_found");
20368
20708
  }
20369
20709
  const url2 = new URL(c.req.url);
20370
- const endpoint = matchEndpoint(app2, url2.pathname.slice(`/app/${slug}`.length), c.req.method);
20710
+ const prefix = `/app/${slug}`;
20711
+ if (url2.pathname !== prefix && !url2.pathname.startsWith(`${prefix}/`)) {
20712
+ return refuse(404, "not_found");
20713
+ }
20714
+ const endpoint = matchEndpoint(app2, url2.pathname.slice(prefix.length) || "/", c.req.method);
20371
20715
  if (endpoint === void 0) {
20372
20716
  return refuse(404, "not_found");
20373
20717
  }
@@ -20380,7 +20724,7 @@ var handleAppEndpoint = /* @__PURE__ */ __name(async (c, slug) => {
20380
20724
  if (first === void 0) {
20381
20725
  return refuse(503, "inactive");
20382
20726
  }
20383
- const key = endpoint.mode === "api" ? `${slug}:${first.id}:endpoint` : `${slug}:${endpoint.path}:endpoint`;
20727
+ const key = endpoint.mode === "api" ? `${slug}:${first.id}:endpoint` : `${slug}:${endpoint.route}:endpoint`;
20384
20728
  if (await rateLimited(c.env, key)) {
20385
20729
  return refuse(429, "rate_limited");
20386
20730
  }
@@ -20650,16 +20994,22 @@ var runForwardToApp2 = /* @__PURE__ */ __name(async (c, input) => {
20650
20994
  method: c.req.method,
20651
20995
  headers,
20652
20996
  body,
20653
- signal: AbortSignal.timeout(HOOK_TIMEOUT_MS)
20997
+ signal: AbortSignal.timeout(APP_INVOCATION_TIMEOUT_MS)
20654
20998
  });
20655
20999
  } catch (error51) {
20656
21000
  return upstreamFailed2(origin, slug, String(error51));
20657
21001
  }
20658
21002
  adoptAppSpans(upstream);
21003
+ writeAppObservability(c.env, upstream, {
21004
+ app: observabilityApp(slug, app2),
21005
+ installs: [{ id: install.id, ord: install.ord, label: install.label }],
21006
+ budget: createDataPointBudget()
21007
+ });
20659
21008
  setSpanAttributes({ "app.status": upstream.status });
20660
21009
  const responseHeaders = new Headers(upstream.headers);
20661
21010
  responseHeaders.delete("set-cookie");
20662
21011
  responseHeaders.delete(APP_SPANS_HEADER);
21012
+ responseHeaders.delete(APP_OBSERVABILITY_HEADER);
20663
21013
  for (const name of [...responseHeaders.keys()]) {
20664
21014
  if (name.startsWith("access-control-")) {
20665
21015
  responseHeaders.delete(name);
@@ -23693,6 +24043,21 @@ var withoutQuery = /* @__PURE__ */ __name((url2) => {
23693
24043
  parsed.search = "";
23694
24044
  return parsed.toString();
23695
24045
  }, "withoutQuery");
24046
+ var browserGeoFrom = /* @__PURE__ */ __name((geo) => {
24047
+ if (geo === void 0) {
24048
+ return void 0;
24049
+ }
24050
+ return {
24051
+ country: geo.country ?? null,
24052
+ // EdgeTag's geoRegion is Cloudflare's subdivision code.
24053
+ region: geo.regionCode ?? null,
24054
+ state: geo.region ?? null,
24055
+ isEU: geo.isEUCountry === "yes"
24056
+ };
24057
+ }, "browserGeoFrom");
24058
+ var attachmentFieldBytes = /* @__PURE__ */ __name((name, value) => {
24059
+ return value === void 0 ? 0 : new TextEncoder().encode(`,"${name}":${JSON.stringify(value)}`).length;
24060
+ }, "attachmentFieldBytes");
23696
24061
  var connectionAttachment = /* @__PURE__ */ __name((request, geo) => {
23697
24062
  const userAgent = request.headers.get("user-agent");
23698
24063
  const attrs = {
@@ -23708,15 +24073,18 @@ var connectionAttachment = /* @__PURE__ */ __name((request, geo) => {
23708
24073
  locale: raw2.locale !== void 0 && raw2.locale.length <= MAX_HOST_LOCALE_LENGTH ? raw2.locale : void 0
23709
24074
  };
23710
24075
  const host = bounded.ip === void 0 && bounded.userAgent === void 0 && bounded.locale === void 0 ? void 0 : bounded;
23711
- const hostBytes = host === void 0 ? 0 : new TextEncoder().encode(`,"host":${JSON.stringify(host)}`).length;
24076
+ const hostBytes = attachmentFieldBytes("host", host);
24077
+ const browserGeo = browserGeoFrom(geo);
24078
+ const browserGeoBytes = attachmentFieldBytes("browserGeo", browserGeo);
23712
24079
  const cf = forwardedCf(request);
23713
24080
  const cfSuffix = cf === void 0 ? "" : `,"cf":${JSON.stringify(cf)}`;
23714
24081
  const cfBytes = new TextEncoder().encode(cfSuffix).length;
23715
24082
  const emptyAttrsBytes = new TextEncoder().encode(JSON.stringify({ attrs: {} })).length;
23716
- const includedCf = cfBytes <= MAX_ATTACHMENT_BYTES - emptyAttrsBytes - hostBytes ? cf : void 0;
23717
- const budgeted = budgetAttrs(attrs, MAX_ATTACHMENT_BYTES - hostBytes - (includedCf === void 0 ? 0 : cfBytes));
24083
+ const includedCf = cfBytes <= MAX_ATTACHMENT_BYTES - emptyAttrsBytes - hostBytes - browserGeoBytes ? cf : void 0;
24084
+ const budgeted = budgetAttrs(attrs, MAX_ATTACHMENT_BYTES - hostBytes - browserGeoBytes - (includedCf === void 0 ? 0 : cfBytes));
23718
24085
  return {
23719
24086
  ...budgeted,
24087
+ ...browserGeo === void 0 ? {} : { browserGeo },
23720
24088
  ...includedCf === void 0 ? {} : { cf: includedCf },
23721
24089
  ...host === void 0 ? {} : { host }
23722
24090
  };
@@ -23803,14 +24171,14 @@ var open = /* @__PURE__ */ __name(async (c) => {
23803
24171
 
23804
24172
  // browser-artifact/meta.json
23805
24173
  var meta_default = {
23806
- version: "0.1.1",
23807
- gitSha: "45acc2f65889b33f7cf2828b967a26fd879455f0",
23808
- builtAt: "2026-08-25T22:08:34.269Z",
23809
- hash: "8f1c955d16978159926d9c98b73ee2d0a3c3d1ea51d4ec8e1b017c3ea6892ea1"
24174
+ version: "0.1.3",
24175
+ gitSha: "66e1bc45acbe4891948cbb3851f3f43ce06abc84",
24176
+ builtAt: "2026-08-28T17:17:19.113Z",
24177
+ hash: "050bb7dd702ac407623ce130c324f3342f048b8a201e2975994d5ef54de7f19f"
23810
24178
  };
23811
24179
 
23812
24180
  // src/paths/q-js.ts
23813
- import qScript from "./9dcfd236748459af038da772477100c4bb0223b7-q.js.txt";
24181
+ import qScript from "./9026260a7a674b710223f296c5d2e7a01930cfed-q.js.txt";
23814
24182
 
23815
24183
  // src/apps-q.ts
23816
24184
  import appsQScript from "./apps-q.json";
@@ -23867,6 +24235,7 @@ var appsMetadata = /* @__PURE__ */ __name(() => {
23867
24235
  return Object.fromEntries(
23868
24236
  Object.entries(registry3.apps).map(([slug, app2]) => {
23869
24237
  const capture = declaredCapture(app2);
24238
+ const geo = app2.data.includes("geo") ? { geo: true } : {};
23870
24239
  const sharedContext = app2.data.includes(SHARED_CONTEXT_CAPABILITY) ? { sharedContext: true } : {};
23871
24240
  return [
23872
24241
  slug,
@@ -23874,6 +24243,7 @@ var appsMetadata = /* @__PURE__ */ __name(() => {
23874
24243
  category: app2.consent.category,
23875
24244
  eventResults: app2.eventResults,
23876
24245
  ...capture === void 0 ? {} : { capture },
24246
+ ...geo,
23877
24247
  ...sharedContext
23878
24248
  }
23879
24249
  ];
@@ -24520,7 +24890,7 @@ var runPool = /* @__PURE__ */ __name(async (limit, tasks) => {
24520
24890
  }
24521
24891
  await Promise.all(running);
24522
24892
  }, "runPool");
24523
- var fanOutToApps = /* @__PURE__ */ __name(async (env, trigger, snapshot, { deliverPush, writeAttribution: writeAttribution2, writeContext: writeContext2, trace }) => {
24893
+ var fanOutToApps = /* @__PURE__ */ __name(async (env, trigger, snapshot, { deliverPush, writeAttribution: writeAttribution2, writeContext: writeContext2, budget, trace }) => {
24524
24894
  const { registry: registry3, error: error51 } = siteRegistry();
24525
24895
  const targets = eligibleApps(registry3, trigger, snapshot.consent, consentEnforced(env));
24526
24896
  if (error51 === void 0 && targets.length === 0) {
@@ -24548,7 +24918,7 @@ var fanOutToApps = /* @__PURE__ */ __name(async (env, trigger, snapshot, { deliv
24548
24918
  await runPool(
24549
24919
  FAN_OUT_CONCURRENCY,
24550
24920
  capped.map((target) => {
24551
- return () => invokeApp(env, { target, trigger, snapshot, context, deliverPush, writeAttribution: writeAttribution2, writeContext: writeContext2 });
24921
+ return () => invokeApp(env, { target, trigger, snapshot, context, deliverPush, writeAttribution: writeAttribution2, writeContext: writeContext2, budget });
24552
24922
  })
24553
24923
  );
24554
24924
  }
@@ -24605,7 +24975,7 @@ var withReplacedPayload = /* @__PURE__ */ __name((entry, action, payload) => {
24605
24975
  }
24606
24976
  return { ...action, payload: parsed.data };
24607
24977
  }, "withReplacedPayload");
24608
- var postTransform = /* @__PURE__ */ __name(async (env, entry, post) => {
24978
+ var postTransform = /* @__PURE__ */ __name(async (env, entry, post, dataPoints) => {
24609
24979
  let response;
24610
24980
  try {
24611
24981
  response = await postHookEnvelope(env, { binding: entry.app.binding, ...post });
@@ -24615,6 +24985,11 @@ var postTransform = /* @__PURE__ */ __name(async (env, entry, post) => {
24615
24985
  if (response === void 0) {
24616
24986
  throw entryFailure(entry, `the ${entry.app.binding} service binding is missing`);
24617
24987
  }
24988
+ writeAppObservability(env, response, {
24989
+ app: observabilityApp(entry.slug, entry.app),
24990
+ installs: [entry.install],
24991
+ budget: dataPoints
24992
+ });
24618
24993
  setSpanAttributes({ "app.status": response.status });
24619
24994
  if (!response.ok) {
24620
24995
  throw entryFailure(entry, `the hook answered status ${response.status}`);
@@ -24629,14 +25004,14 @@ var postTransform = /* @__PURE__ */ __name(async (env, entry, post) => {
24629
25004
  throw entryFailure(entry, String(error51));
24630
25005
  }
24631
25006
  }, "postTransform");
24632
- var runTransformEntry = /* @__PURE__ */ __name(async (env, { entry, action, snapshot, context, timeoutMs }) => {
25007
+ var runTransformEntry = /* @__PURE__ */ __name(async (env, { entry, action, snapshot, context, timeoutMs, dataPoints }) => {
24633
25008
  const target = { slug: entry.slug, app: entry.app, installs: [entry.install] };
24634
25009
  const envelope = {
24635
25010
  ...buildEnvelope(target, { kind: "event", action }, snapshot, context),
24636
25011
  kind: "transform"
24637
25012
  };
24638
25013
  const key = await deriveAppInternalKey(context.master, context.domainId, entry.slug);
24639
- const verdict = validateEnvelopeResponse(await postTransform(env, entry, { key, envelope, timeoutMs }), 1);
25014
+ const verdict = validateEnvelopeResponse(await postTransform(env, entry, { key, envelope, timeoutMs }, dataPoints), 1);
24640
25015
  if (!verdict.ok) {
24641
25016
  throw entryFailure(entry, verdict.reason);
24642
25017
  }
@@ -24704,7 +25079,7 @@ var buildRelease = /* @__PURE__ */ __name((env, { collected, action, deliverPush
24704
25079
  }
24705
25080
  };
24706
25081
  }, "buildRelease");
24707
- var runTransformChain = /* @__PURE__ */ __name(async (env, { action, snapshot, deliverPush, writeAttribution: writeAttribution2, writeContext: writeContext2 }) => {
25082
+ var runTransformChain = /* @__PURE__ */ __name(async (env, { action, snapshot, deliverPush, writeAttribution: writeAttribution2, writeContext: writeContext2, dataPoints }) => {
24708
25083
  const { registry: registry3, error: error51 } = siteRegistry();
24709
25084
  if (error51 !== void 0) {
24710
25085
  throw new TransformChainError(`the apps registry is malformed: ${error51}`);
@@ -24724,7 +25099,14 @@ var runTransformChain = /* @__PURE__ */ __name(async (env, { action, snapshot, d
24724
25099
  for (const entry of entries) {
24725
25100
  const stepped = entryBudget(budget, entry, Date.now());
24726
25101
  budget = stepped.budget;
24727
- const verdict = await invokeTransformEntry(env, { entry, action: current, snapshot, context, timeoutMs: stepped.timeoutMs });
25102
+ const verdict = await invokeTransformEntry(env, {
25103
+ entry,
25104
+ action: current,
25105
+ snapshot,
25106
+ context,
25107
+ timeoutMs: stepped.timeoutMs,
25108
+ dataPoints
25109
+ });
24728
25110
  if (verdict.effects.accepted.length > 0) {
24729
25111
  collected.push({ slug: entry.slug, installs: [entry.install], groups: verdict.effects.accepted });
24730
25112
  }
@@ -25705,7 +26087,11 @@ var AnonDO = class extends DurableObject2 {
25705
26087
  const attachment = connectionAttachment(request, geo);
25706
26088
  server.serializeAttachment(attachment);
25707
26089
  this.deliverSharedContext(server);
25708
- this.appFanOut({ kind: "connect", ...attachment.cf === void 0 ? {} : { geo: attachment.cf }, host: attachment.host }, userId);
26090
+ this.appFanOut(
26091
+ { kind: "connect", ...attachment.cf === void 0 ? {} : { geo: attachment.cf }, host: attachment.host },
26092
+ userId,
26093
+ createDataPointBudget()
26094
+ );
25709
26095
  return new Response(null, { status: 101, webSocket: client });
25710
26096
  });
25711
26097
  }
@@ -25760,7 +26146,14 @@ var AnonDO = class extends DurableObject2 {
25760
26146
  return;
25761
26147
  }
25762
26148
  if (parsed.frame.t === "getSession") {
25763
- this.respond(ws, { t: "session", ref: parsed.ref, anonId: userId, globalId: linkedGlobalId(this.kv) });
26149
+ this.respond(ws, {
26150
+ t: "session",
26151
+ ref: parsed.ref,
26152
+ anonId: userId,
26153
+ globalId: linkedGlobalId(this.kv),
26154
+ ip: attachment?.host?.ip ?? null,
26155
+ geo: attachment?.browserGeo ?? null
26156
+ });
25764
26157
  return;
25765
26158
  }
25766
26159
  if (parsed.frame.t === "capture") {
@@ -26046,12 +26439,13 @@ var AnonDO = class extends DurableObject2 {
26046
26439
  if (isNoopAction(action)) {
26047
26440
  return;
26048
26441
  }
26049
- const transformed = await this.transformAction(action);
26442
+ const dataPoints = createDataPointBudget();
26443
+ const transformed = await this.transformAction(action, dataPoints);
26050
26444
  if (transformed === null) {
26051
26445
  return;
26052
26446
  }
26053
26447
  recorded = transformed.action;
26054
- await this.runAction(transformed.action, transformed.releaseEmissions);
26448
+ await this.runAction(transformed.action, transformed.releaseEmissions, dataPoints);
26055
26449
  } finally {
26056
26450
  this.recordActionBlocks(recorded);
26057
26451
  }
@@ -26061,9 +26455,9 @@ var AnonDO = class extends DurableObject2 {
26061
26455
  actionBlocks(action, this.appsSnapshot(action.userId), this.wiped ? [] : allIdentifiers(this.sql, this.identifiersMirror))
26062
26456
  );
26063
26457
  }
26064
- async runAction(current, releaseEmissions) {
26458
+ async runAction(current, releaseEmissions, dataPoints) {
26065
26459
  if (current.type !== "alias" && isDeliveryOnlyAction(current)) {
26066
- this.appFanOut({ kind: "event", action: current }, current.userId);
26460
+ this.appFanOut({ kind: "event", action: current }, current.userId, dataPoints);
26067
26461
  this.releaseTransformEmissions(releaseEmissions);
26068
26462
  return;
26069
26463
  }
@@ -26099,7 +26493,7 @@ var AnonDO = class extends DurableObject2 {
26099
26493
  } else {
26100
26494
  await this.ingestConsent(current);
26101
26495
  }
26102
- this.appFanOut({ kind: "event", action: current }, current.userId);
26496
+ this.appFanOut({ kind: "event", action: current }, current.userId, dataPoints);
26103
26497
  this.releaseTransformEmissions(releaseEmissions);
26104
26498
  } finally {
26105
26499
  if (novelIdentifier) {
@@ -26108,7 +26502,7 @@ var AnonDO = class extends DurableObject2 {
26108
26502
  }
26109
26503
  }
26110
26504
  // null means a transform dropped the action: the turn succeeds with nothing persisted (§5.4).
26111
- async transformAction(action) {
26505
+ async transformAction(action, dataPoints) {
26112
26506
  if (action.type !== "track" && action.type !== "identify") {
26113
26507
  return { action, releaseEmissions: null };
26114
26508
  }
@@ -26117,7 +26511,8 @@ var AnonDO = class extends DurableObject2 {
26117
26511
  snapshot: this.appsSnapshot(action.userId),
26118
26512
  deliverPush: this.pushDelivery,
26119
26513
  writeAttribution: this.attributionDelivery,
26120
- writeContext: this.contextDelivery
26514
+ writeContext: this.contextDelivery,
26515
+ dataPoints
26121
26516
  });
26122
26517
  if (outcome.dropped) {
26123
26518
  this.releaseTransformEmissions(outcome.releaseEmissions);
@@ -26153,13 +26548,14 @@ var AnonDO = class extends DurableObject2 {
26153
26548
  }
26154
26549
  this.ctx.waitUntil(release().catch((error51) => console.warn("transform emission release failed", { error: error51 })));
26155
26550
  }
26156
- appFanOut(trigger, anonId) {
26551
+ appFanOut(trigger, anonId, dataPoints) {
26157
26552
  const snapshot = this.appsSnapshot(anonId);
26158
26553
  this.ctx.waitUntil(
26159
26554
  fanOutToApps(this.env, trigger, snapshot, {
26160
26555
  deliverPush: this.pushDelivery,
26161
26556
  writeAttribution: this.attributionDelivery,
26162
26557
  writeContext: this.contextDelivery,
26558
+ budget: dataPoints,
26163
26559
  trace: currentTraceContext()
26164
26560
  }).catch((error51) => console.warn("app fan-out failed", { error: error51 }))
26165
26561
  );