@blotout/qinto-cli 0.1.8 → 0.1.10

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.
@@ -739,6 +739,10 @@ var MAX_APPS_Q_BYTES = 4 * 1024 * 1024;
739
739
  // ../../libs/app-manifest/src/cron.ts
740
740
  var MAX_CRON_LENGTH = 128;
741
741
 
742
+ // ../../libs/app-manifest/src/tool-names.ts
743
+ var MAX_APP_TOOL_NAME_LENGTH = 32;
744
+ var APP_TOOL_NAME_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*$/;
745
+
742
746
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
743
747
  var external_exports = {};
744
748
  __export(external_exports, {
@@ -15853,6 +15857,12 @@ var validateEnvelopeResponse = /* @__PURE__ */ __name((raw2, installCount) => {
15853
15857
  var CONTROL_ENVELOPE_VERSION = 1;
15854
15858
  var MAX_CONTROL_BYTES = 256 * 1024;
15855
15859
  var MAX_MIGRATION_CHECKPOINT_BYTES = 16 * 1024;
15860
+ var MAX_TOOL_ARGUMENTS_BYTES = 32 * 1024;
15861
+ var MAX_TOOL_RESULT_BYTES = 64 * 1024;
15862
+ var MAX_TOOL_DATA_DEPTH = 8;
15863
+ var MAX_TOOL_DATA_NODES = 5e3;
15864
+ var CONTROL_FORWARD_TIMEOUT_MS = 1e4;
15865
+ var CONTROL_TOOL_FORWARD_TIMEOUT_MS = 5e4;
15856
15866
  var installSchema2 = external_exports.object({
15857
15867
  id: external_exports.string().min(1).max(64),
15858
15868
  ord: external_exports.number().int().min(1),
@@ -15926,15 +15936,32 @@ var scheduleSchema = external_exports.object({
15926
15936
  }).strict()
15927
15937
  ).min(1).max(MAX_INSTALLS_PER_APP)
15928
15938
  }).strict();
15939
+ var toolSchema = external_exports.object({
15940
+ // The manifest's tools[].name, never the `<slug>_` composed MCP name: the slug is already in
15941
+ // `app.slug`, and carrying it twice would give the app worker two sources for one fact.
15942
+ name: external_exports.string().min(1).max(MAX_APP_TOOL_NAME_LENGTH).regex(APP_TOOL_NAME_PATTERN),
15943
+ // Who asked, and what they may do on this site. Derived from the bearer by the portal, never
15944
+ // from the model's arguments.
15945
+ viewer: external_exports.object({ userId: external_exports.string().min(1).max(64), access: external_exports.enum(["read", "write"]) }).strict(),
15946
+ // Already validated against the tool's declared inputSchema, with the reserved routing names
15947
+ // stripped. Every declared input schema has an object root, so this is an object too — and a
15948
+ // call that names no arguments normalizes to `{}` rather than reaching the handler as undefined.
15949
+ arguments: external_exports.record(external_exports.string(), external_exports.unknown()).default({}),
15950
+ // Stable across a collapsed retry, so an app can deduplicate its external effects on it.
15951
+ callId: external_exports.string().min(1).max(64)
15952
+ }).strict().refine((tool) => {
15953
+ return new TextEncoder().encode(JSON.stringify(tool.arguments)).length <= MAX_TOOL_ARGUMENTS_BYTES;
15954
+ }, "Tool arguments exceed the size cap");
15929
15955
  var controlEnvelopeSchema = external_exports.object({
15930
15956
  v: external_exports.literal(CONTROL_ENVELOPE_VERSION),
15931
- kind: external_exports.enum(["noop", "lifecycle", "migration", "schedule"]),
15957
+ kind: external_exports.enum(["noop", "lifecycle", "migration", "schedule", "tool"]),
15932
15958
  site: external_exports.object({ origin: external_exports.string().min(1).max(255) }).strict(),
15933
15959
  app: external_exports.object({ slug: external_exports.string().min(1).max(30), version: external_exports.string().min(1).max(32) }).strict(),
15934
15960
  install: external_exports.object({ id: external_exports.string().min(1).max(64), ord: external_exports.number().int().min(1), label: external_exports.string().max(100) }).strict().optional(),
15935
15961
  lifecycle: lifecycleEventSchema.optional(),
15936
15962
  migration: migrationSchema.optional(),
15937
- schedule: scheduleSchema.optional()
15963
+ schedule: scheduleSchema.optional(),
15964
+ tool: toolSchema.optional()
15938
15965
  }).strict().superRefine((envelope, ctx) => {
15939
15966
  if (envelope.kind === "lifecycle" !== (envelope.lifecycle !== void 0)) {
15940
15967
  ctx.addIssue({ code: "custom", path: ["lifecycle"], message: "Lifecycle payload must match the control kind" });
@@ -15945,13 +15972,50 @@ var controlEnvelopeSchema = external_exports.object({
15945
15972
  if (envelope.kind === "schedule" !== (envelope.schedule !== void 0)) {
15946
15973
  ctx.addIssue({ code: "custom", path: ["schedule"], message: "Schedule payload must match the control kind" });
15947
15974
  }
15975
+ if (envelope.kind === "tool" !== (envelope.tool !== void 0)) {
15976
+ ctx.addIssue({ code: "custom", path: ["tool"], message: "Tool payload must match the control kind" });
15977
+ }
15948
15978
  if (envelope.kind === "lifecycle" && envelope.install === void 0) {
15949
15979
  ctx.addIssue({ code: "custom", path: ["install"], message: "Lifecycle control needs an install" });
15950
15980
  }
15981
+ if (envelope.kind === "tool" && envelope.install === void 0) {
15982
+ ctx.addIssue({ code: "custom", path: ["install"], message: "Tool control needs an install" });
15983
+ }
15951
15984
  });
15985
+ var toolDataWithinLimits = /* @__PURE__ */ __name((value) => {
15986
+ const stack = [{ value, depth: 0 }];
15987
+ let seen = 0;
15988
+ while (stack.length > 0) {
15989
+ const next = stack.pop();
15990
+ seen += 1;
15991
+ if (next.depth > MAX_TOOL_DATA_DEPTH || seen > MAX_TOOL_DATA_NODES) {
15992
+ return false;
15993
+ }
15994
+ if (next.value !== null && typeof next.value === "object") {
15995
+ for (const child of Object.values(next.value)) {
15996
+ stack.push({ value: child, depth: next.depth + 1 });
15997
+ }
15998
+ }
15999
+ }
16000
+ return true;
16001
+ }, "toolDataWithinLimits");
16002
+ var toolResultSchema = external_exports.object({
16003
+ content: external_exports.string().max(MAX_TOOL_RESULT_BYTES),
16004
+ // MCP defines `structuredContent` as an object, so a bare scalar or array is refused here rather
16005
+ // than surfacing as a protocol error at the client.
16006
+ data: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
16007
+ // True when the handler chose to fail with `ctx.fail`: `content` is then the app's own message
16008
+ // for the model. A thrown handler is `ok: false` with no `tool` at all.
16009
+ failed: external_exports.boolean()
16010
+ }).strict().refine((result) => {
16011
+ return toolDataWithinLimits(result.data);
16012
+ }, "Tool result nests too deeply or carries too many values").refine((result) => {
16013
+ return new TextEncoder().encode(JSON.stringify({ content: result.content, data: result.data ?? null })).length <= MAX_TOOL_RESULT_BYTES;
16014
+ }, "Tool result exceeds the size cap");
15952
16015
  var controlResponseSchema = external_exports.object({
15953
16016
  v: external_exports.literal(CONTROL_ENVELOPE_VERSION),
15954
16017
  ok: external_exports.boolean(),
16018
+ tool: toolResultSchema.optional(),
15955
16019
  migration: external_exports.union([
15956
16020
  external_exports.object({ done: external_exports.literal(true) }).strict(),
15957
16021
  external_exports.object({ done: external_exports.literal(false), checkpoint: external_exports.unknown() }).strict().refine((result) => {
@@ -15966,12 +16030,21 @@ var ENDPOINT_CONTEXT_HEADER = "x-qinto-endpoint-context";
15966
16030
  var ENDPOINT_OUTCOMES_HEADER = "x-qinto-endpoint-outcomes";
15967
16031
  var MAX_ENDPOINT_BODY_BYTES = 1024 * 1024;
15968
16032
  var MAX_ENDPOINT_OUTCOMES_BYTES = 64 * 1024;
16033
+ var MAX_ENDPOINT_PATH_LENGTH = 16 * 1024;
15969
16034
  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
16035
  var endpointContextSchema = external_exports.object({
15971
- path: external_exports.string().min(1).max(255),
16036
+ // The declared template, which keys the bundle's handler map; `path` is what the caller asked
16037
+ // for. Only `route` may key the rate limiter — a concrete path gives every parameter value its
16038
+ // own bucket, which is no limit at all.
16039
+ route: external_exports.string().min(1).max(255),
16040
+ path: external_exports.string().min(1).max(MAX_ENDPOINT_PATH_LENGTH),
16041
+ // A name is cut from `route`, so the route's own cap bounds it; anything tighter would reject a
16042
+ // parameter the manifest grammar accepts and the worker already matched.
16043
+ params: external_exports.record(external_exports.string().min(1).max(255), external_exports.string().max(MAX_ENDPOINT_PATH_LENGTH)).default({}),
15972
16044
  method: external_exports.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
15973
16045
  mode: external_exports.enum(["webhook", "api"]),
15974
16046
  version: external_exports.string().min(1).max(32),
16047
+ site: external_exports.object({ origin: external_exports.string().min(1).max(255) }).strict(),
15975
16048
  installs: external_exports.array(installSchema3).min(1).max(15)
15976
16049
  }).strict().refine((context) => {
15977
16050
  return context.mode === "webhook" || context.installs.length === 1;
@@ -15998,6 +16071,71 @@ var aggregateEndpointOutcomes = /* @__PURE__ */ __name((outcomes) => {
15998
16071
  return { status: failed ? 502 : 200 };
15999
16072
  }, "aggregateEndpointOutcomes");
16000
16073
 
16074
+ // ../../libs/app-envelope/src/observability.ts
16075
+ var APP_OBSERVABILITY_HEADER = "x-qinto-app-observability";
16076
+ var APP_OBSERVABILITY_VERSION = 1;
16077
+ var MAX_APP_OBSERVABILITY_EVENTS = 20;
16078
+ var MAX_APP_SAVED_ERRORS = 5;
16079
+ var MAX_APP_EVENT_NAME_BYTES = 128;
16080
+ var MAX_APP_ACTION_NAME_BYTES = 256;
16081
+ var MAX_APP_ERROR_DESCRIPTION_BYTES = 2048;
16082
+ var MAX_APP_ERROR_PAYLOAD_BYTES = 4096;
16083
+ var MAX_APP_DATA_POINT_BLOB_BYTES = 16 * 1024;
16084
+ var MAX_APP_OBSERVABILITY_BYTES = 32 * 1024;
16085
+ var APP_OBSERVABILITY_JSON_BUDGET = MAX_APP_OBSERVABILITY_BYTES / 4 * 3;
16086
+ var MAX_APP_DATA_POINTS_PER_INVOCATION = 250;
16087
+ var APP_EVENT_STATUSES = ["success", "error"];
16088
+ var APP_ERROR_CATEGORIES = ["authorization", "configuration", "validation", "rate_limit", "upstream", "internal"];
16089
+ var encoder3 = new TextEncoder();
16090
+ var decoder2 = new TextDecoder();
16091
+ 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();
16092
+ var savedErrorSchema = external_exports.object({
16093
+ ord: external_exports.number().int().min(1),
16094
+ category: external_exports.enum(APP_ERROR_CATEGORIES),
16095
+ action: external_exports.string().min(1).max(MAX_APP_ACTION_NAME_BYTES),
16096
+ description: external_exports.string().min(1).max(MAX_APP_ERROR_DESCRIPTION_BYTES),
16097
+ payload: external_exports.string().max(MAX_APP_ERROR_PAYLOAD_BYTES).optional(),
16098
+ error: external_exports.string().max(MAX_APP_ERROR_PAYLOAD_BYTES).optional()
16099
+ }).strict();
16100
+ var appObservabilityRecordSchema = external_exports.object({
16101
+ v: external_exports.literal(APP_OBSERVABILITY_VERSION),
16102
+ truncated: external_exports.boolean().optional(),
16103
+ events: external_exports.array(observabilityEventSchema).max(MAX_APP_OBSERVABILITY_EVENTS),
16104
+ errors: external_exports.array(savedErrorSchema).max(MAX_APP_SAVED_ERRORS)
16105
+ }).strict();
16106
+ var utf8Length = /* @__PURE__ */ __name((value) => {
16107
+ return encoder3.encode(value).length;
16108
+ }, "utf8Length");
16109
+ var truncateUtf8 = /* @__PURE__ */ __name((value, maxBytes) => {
16110
+ if (utf8Length(value) <= maxBytes) {
16111
+ return value;
16112
+ }
16113
+ let kept = "";
16114
+ let used = 0;
16115
+ for (const character of value) {
16116
+ const size = utf8Length(character);
16117
+ if (used + size > maxBytes) {
16118
+ break;
16119
+ }
16120
+ kept += character;
16121
+ used += size;
16122
+ }
16123
+ return kept;
16124
+ }, "truncateUtf8");
16125
+ var parseAppObservability = /* @__PURE__ */ __name((header) => {
16126
+ if (header === null || header.length > MAX_APP_OBSERVABILITY_BYTES) {
16127
+ return null;
16128
+ }
16129
+ let parsed;
16130
+ try {
16131
+ parsed = JSON.parse(decoder2.decode(Uint8Array.from(atob(header), (character) => character.charCodeAt(0))));
16132
+ } catch {
16133
+ return null;
16134
+ }
16135
+ const result = appObservabilityRecordSchema.safeParse(parsed);
16136
+ return result.success ? result.data : null;
16137
+ }, "parseAppObservability");
16138
+
16001
16139
  // ../../libs/app-envelope/src/ui.ts
16002
16140
  var UI_CONTEXT_HEADER = "x-qinto-ui-context";
16003
16141
  var MAX_UI_BODY_BYTES = 1024 * 1024;
@@ -16204,13 +16342,13 @@ var getPattern = /* @__PURE__ */ __name((label, next) => {
16204
16342
  }
16205
16343
  return null;
16206
16344
  }, "getPattern");
16207
- var tryDecode = /* @__PURE__ */ __name((str, decoder2) => {
16345
+ var tryDecode = /* @__PURE__ */ __name((str, decoder3) => {
16208
16346
  try {
16209
- return decoder2(str);
16347
+ return decoder3(str);
16210
16348
  } catch {
16211
16349
  return str.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match2) => {
16212
16350
  try {
16213
- return decoder2(match2);
16351
+ return decoder3(match2);
16214
16352
  } catch {
16215
16353
  return match2;
16216
16354
  }
@@ -18977,6 +19115,118 @@ var readableContext = /* @__PURE__ */ __name((own, shared) => {
18977
19115
  return { private: own === void 0 ? null : wireEntry(own), shared: sharedContextWire(shared) };
18978
19116
  }, "readableContext");
18979
19117
 
19118
+ // src/apps/observability.ts
19119
+ var APP_ERROR_DATA_POINT_RESERVE = 50;
19120
+ var observabilityApp = /* @__PURE__ */ __name((slug, app2) => {
19121
+ return { slug, name: app2.name, version: app2.version };
19122
+ }, "observabilityApp");
19123
+ var createDataPointBudget = /* @__PURE__ */ __name(() => {
19124
+ let events = 0;
19125
+ let total = 0;
19126
+ return {
19127
+ take: /* @__PURE__ */ __name((kind2) => {
19128
+ if (total >= MAX_APP_DATA_POINTS_PER_INVOCATION) {
19129
+ return false;
19130
+ }
19131
+ if (kind2 === "event") {
19132
+ if (events >= MAX_APP_DATA_POINTS_PER_INVOCATION - APP_ERROR_DATA_POINT_RESERVE) {
19133
+ return false;
19134
+ }
19135
+ events += 1;
19136
+ }
19137
+ total += 1;
19138
+ return true;
19139
+ }, "take")
19140
+ };
19141
+ }, "createDataPointBudget");
19142
+ var totalLength = /* @__PURE__ */ __name((blobs) => {
19143
+ return blobs.reduce((sum, blob) => {
19144
+ return sum + utf8Length(blob);
19145
+ }, 0);
19146
+ }, "totalLength");
19147
+ var errorBlobs = /* @__PURE__ */ __name((app2, install, record2, rowId) => {
19148
+ const fixed = [app2.slug, app2.name, install.id, install.label, app2.version, record2.category, record2.action, rowId];
19149
+ let remaining = MAX_APP_DATA_POINT_BLOB_BYTES - totalLength(fixed);
19150
+ const description = truncateUtf8(record2.description, Math.max(0, remaining));
19151
+ remaining -= utf8Length(description);
19152
+ const payload = truncateUtf8(record2.payload ?? "", Math.max(0, remaining));
19153
+ remaining -= utf8Length(payload);
19154
+ const error51 = truncateUtf8(record2.error ?? "", Math.max(0, remaining));
19155
+ return [app2.slug, app2.name, install.id, install.label, app2.version, record2.category, record2.action, description, payload, error51, rowId];
19156
+ }, "errorBlobs");
19157
+ var eventBlobs = /* @__PURE__ */ __name((app2, install, record2) => {
19158
+ return [app2.slug, app2.name, install.id, install.label, app2.version, record2.name, record2.status];
19159
+ }, "eventBlobs");
19160
+ var writeEvents = /* @__PURE__ */ __name((env, target, byOrd, events) => {
19161
+ for (const [index, event] of events.entries()) {
19162
+ const install = byOrd.get(event.ord);
19163
+ if (install === void 0) {
19164
+ addSpanEvent("app observability names an install the site did not send", { level: "warn", attrs: { app: target.app.slug } });
19165
+ continue;
19166
+ }
19167
+ if (!target.budget.take("event")) {
19168
+ addSpanEvent("app observability dropped events at the data-point cap", {
19169
+ level: "warn",
19170
+ attrs: { app: target.app.slug, skipped: events.length - index }
19171
+ });
19172
+ break;
19173
+ }
19174
+ env.APP_EVENTS.writeDataPoint({ indexes: [`${install.id}:${event.status}`], blobs: eventBlobs(target.app, install, event) });
19175
+ }
19176
+ }, "writeEvents");
19177
+ var writeErrors = /* @__PURE__ */ __name((env, target, byOrd, errors) => {
19178
+ for (const [index, saved] of errors.entries()) {
19179
+ const install = byOrd.get(saved.ord);
19180
+ if (install === void 0) {
19181
+ addSpanEvent("app observability names an install the site did not send", { level: "warn", attrs: { app: target.app.slug } });
19182
+ continue;
19183
+ }
19184
+ if (!target.budget.take("error")) {
19185
+ addSpanEvent("app observability dropped saved errors at the data-point cap", {
19186
+ level: "warn",
19187
+ attrs: { app: target.app.slug, skipped: errors.length - index }
19188
+ });
19189
+ break;
19190
+ }
19191
+ env.APP_ERRORS.writeDataPoint({
19192
+ indexes: [`${install.id}:${saved.category}`],
19193
+ blobs: errorBlobs(target.app, install, saved, ulid3())
19194
+ });
19195
+ }
19196
+ }, "writeErrors");
19197
+ var guarded = /* @__PURE__ */ __name((target, write) => {
19198
+ try {
19199
+ write();
19200
+ return true;
19201
+ } catch (error51) {
19202
+ addSpanEvent("app observability write failed", { level: "warn", attrs: { app: target.app.slug, error: String(error51) } });
19203
+ return false;
19204
+ }
19205
+ }, "guarded");
19206
+ var writeAppObservability = /* @__PURE__ */ __name((env, response, target) => {
19207
+ const record2 = parseAppObservability(response.headers.get(APP_OBSERVABILITY_HEADER));
19208
+ if (record2 === null) {
19209
+ return;
19210
+ }
19211
+ const byOrd = new Map(
19212
+ target.installs.map((install) => {
19213
+ return [install.ord, install];
19214
+ })
19215
+ );
19216
+ const eventsWritten = guarded(target, () => {
19217
+ writeEvents(env, target, byOrd, record2.events);
19218
+ });
19219
+ const errorsWritten = guarded(target, () => {
19220
+ writeErrors(env, target, byOrd, record2.errors);
19221
+ });
19222
+ if (!eventsWritten || !errorsWritten) {
19223
+ return;
19224
+ }
19225
+ if (record2.truncated === true) {
19226
+ addSpanEvent("the app recorded more observability than one invocation carries", { level: "warn", attrs: { app: target.app.slug } });
19227
+ }
19228
+ }, "writeAppObservability");
19229
+
18980
19230
  // src/apps/read-bounded.ts
18981
19231
  var readBoundedBody = /* @__PURE__ */ __name(async (message, maxBytes) => {
18982
19232
  const reader = message.body?.getReader();
@@ -19027,7 +19277,7 @@ var adoptAppSpans = /* @__PURE__ */ __name((response) => {
19027
19277
  }, "adoptAppSpans");
19028
19278
 
19029
19279
  // src/apps/invoke.ts
19030
- var HOOK_TIMEOUT_MS = 1e4;
19280
+ var APP_INVOCATION_TIMEOUT_MS = 1e4;
19031
19281
  var MAX_EMIT_DEPTH = 2;
19032
19282
  var HOOK_URL = "https://app/__qinto/hook";
19033
19283
  var invocationContext = /* @__PURE__ */ __name((env) => {
@@ -19233,19 +19483,24 @@ var hookResponseBody = /* @__PURE__ */ __name(async (response, slug) => {
19233
19483
  }
19234
19484
  return new TextDecoder().decode(bytes);
19235
19485
  }, "hookResponseBody");
19236
- var runInvokeApp = /* @__PURE__ */ __name(async (env, { target, trigger, snapshot, context, deliverPush, writeAttribution: writeAttribution2, writeContext: writeContext2 }) => {
19486
+ var runInvokeApp = /* @__PURE__ */ __name(async (env, { target, trigger, snapshot, context, deliverPush, writeAttribution: writeAttribution2, writeContext: writeContext2, budget }) => {
19237
19487
  try {
19238
19488
  const key = await deriveAppInternalKey(context.master, context.domainId, target.slug);
19239
19489
  const response = await postHookEnvelope(env, {
19240
19490
  binding: target.app.binding,
19241
19491
  key,
19242
19492
  envelope: buildEnvelope(target, trigger, snapshot, context),
19243
- timeoutMs: HOOK_TIMEOUT_MS
19493
+ timeoutMs: APP_INVOCATION_TIMEOUT_MS
19244
19494
  });
19245
19495
  if (response === void 0) {
19246
19496
  addSpanEvent("app service binding is missing", { level: "warn", attrs: { app: target.slug, binding: target.app.binding } });
19247
19497
  return;
19248
19498
  }
19499
+ writeAppObservability(env, response, {
19500
+ app: observabilityApp(target.slug, target.app),
19501
+ installs: target.installs,
19502
+ budget
19503
+ });
19249
19504
  setSpanAttributes({ "app.status": response.status });
19250
19505
  const body = await hookResponseBody(response, target.slug);
19251
19506
  if (body === void 0) {
@@ -19329,6 +19584,9 @@ var installSchema4 = external_exports.object({
19329
19584
  });
19330
19585
  var appSchema = external_exports.object({
19331
19586
  binding: external_exports.string().min(1),
19587
+ // Required, with no default, for the same reason `events` is: the display name is stored on every
19588
+ // observability data point, and a default would be a compatibility path for an older deploy.
19589
+ name: external_exports.string().min(1).max(100),
19332
19590
  version: external_exports.string().min(1),
19333
19591
  // Absent from an old embedded registry; the default never equals a real claim, so the ui proxy
19334
19592
  // refuses stale registries with token_stale instead of turning the app off.
@@ -19616,11 +19874,16 @@ var runRelayRpc = /* @__PURE__ */ __name(async (env, { context, body, callee, in
19616
19874
  // JSON.stringify drops an undefined key, and the envelope requires `args` to be present.
19617
19875
  rpc: { caller: body.slug, fn: body.fn, args: body.args ?? null }
19618
19876
  },
19619
- timeoutMs: HOOK_TIMEOUT_MS
19877
+ timeoutMs: APP_INVOCATION_TIMEOUT_MS
19620
19878
  });
19621
19879
  if (response === void 0) {
19622
19880
  return calleeFailed(body.callee, "The callee service binding is missing.");
19623
19881
  }
19882
+ writeAppObservability(env, response, {
19883
+ app: observabilityApp(body.callee, callee),
19884
+ installs: [install],
19885
+ budget: createDataPointBudget()
19886
+ });
19624
19887
  setSpanAttributes({ "app.status": response.status });
19625
19888
  if (!response.ok) {
19626
19889
  return calleeFailed(body.callee, `The callee answered status ${response.status}.`);
@@ -19888,6 +20151,18 @@ var controlTargets = /* @__PURE__ */ __name(() => {
19888
20151
  // src/apps/control.ts
19889
20152
  var CONTROL_PATH = "/__qinto/control";
19890
20153
  var MAX_CLOCK_SKEW_MS = 5 * 60 * 1e3;
20154
+ var writeControlObservability = /* @__PURE__ */ __name((env, upstream, envelope) => {
20155
+ const installs = envelope.schedule?.installs ?? (envelope.install === void 0 ? [] : [envelope.install]);
20156
+ const app2 = siteRegistry().registry.apps[envelope.app.slug];
20157
+ if (installs.length === 0 || app2 === void 0) {
20158
+ return;
20159
+ }
20160
+ writeAppObservability(env, upstream, {
20161
+ app: observabilityApp(envelope.app.slug, { name: app2.name, version: envelope.app.version }),
20162
+ installs,
20163
+ budget: createDataPointBudget()
20164
+ });
20165
+ }, "writeControlObservability");
19891
20166
  var textBody = /* @__PURE__ */ __name(async (request) => {
19892
20167
  const bytes = await readBoundedBody(request, MAX_CONTROL_BYTES);
19893
20168
  return bytes === null ? null : new TextDecoder().decode(bytes);
@@ -19953,9 +20228,12 @@ var handleControl = /* @__PURE__ */ __name(async (request, env) => {
19953
20228
  method: "POST",
19954
20229
  headers,
19955
20230
  body,
19956
- signal: AbortSignal.timeout(1e4)
20231
+ // Strictly inside the portal's own deadline for the same kind, so a slow tool times out here
20232
+ // first and the failure is attributable to the app worker rather than to the hop above it.
20233
+ signal: AbortSignal.timeout(envelope.kind === "tool" ? CONTROL_TOOL_FORWARD_TIMEOUT_MS : CONTROL_FORWARD_TIMEOUT_MS)
19957
20234
  });
19958
20235
  adoptAppSpans(upstream);
20236
+ writeControlObservability(env, upstream, envelope);
19959
20237
  const responseBytes = await readBoundedBody(upstream, MAX_CONTROL_BYTES);
19960
20238
  if (responseBytes === null) {
19961
20239
  return Response.json({ error: "invalid_response" }, { status: 502 });
@@ -19974,6 +20252,73 @@ var appControlRoutes = new Hono2().post(CONTROL_PATH, (c) => {
19974
20252
  return handleControl(c.req.raw, c.env);
19975
20253
  });
19976
20254
 
20255
+ // ../../libs/app-manifest/src/route.ts
20256
+ var isParam = /* @__PURE__ */ __name((segment) => {
20257
+ return segment.startsWith(":");
20258
+ }, "isParam");
20259
+ var splitRoute = /* @__PURE__ */ __name((route) => {
20260
+ return route === "/" ? [] : route.slice(1).split("/");
20261
+ }, "splitRoute");
20262
+ var splitRequest = /* @__PURE__ */ __name((requestPath) => {
20263
+ if (requestPath === "/") {
20264
+ return [];
20265
+ }
20266
+ const decoded = [];
20267
+ for (const segment of requestPath.slice(1).split("/")) {
20268
+ try {
20269
+ decoded.push(decodeURIComponent(segment));
20270
+ } catch {
20271
+ return null;
20272
+ }
20273
+ }
20274
+ return decoded;
20275
+ }, "splitRequest");
20276
+ var matches = /* @__PURE__ */ __name((parts, segments) => {
20277
+ if (parts.length !== segments.length) {
20278
+ return false;
20279
+ }
20280
+ return parts.every((part, index) => {
20281
+ const segment = segments[index];
20282
+ return isParam(part) ? segment.length > 0 : part === segment;
20283
+ });
20284
+ }, "matches");
20285
+ var isMoreSpecific = /* @__PURE__ */ __name((candidate, current) => {
20286
+ for (const [index, part] of candidate.entries()) {
20287
+ const candidateParam = isParam(part);
20288
+ const currentParam = isParam(current[index]);
20289
+ if (candidateParam !== currentParam) {
20290
+ return !candidateParam;
20291
+ }
20292
+ }
20293
+ return false;
20294
+ }, "isMoreSpecific");
20295
+ var extract = /* @__PURE__ */ __name((parts, segments) => {
20296
+ const params = {};
20297
+ for (const [index, part] of parts.entries()) {
20298
+ if (isParam(part)) {
20299
+ params[part.slice(1)] = segments[index];
20300
+ }
20301
+ }
20302
+ return params;
20303
+ }, "extract");
20304
+ var matchEndpointPath = /* @__PURE__ */ __name((routes, requestPath) => {
20305
+ const segments = splitRequest(requestPath);
20306
+ if (segments === null) {
20307
+ return void 0;
20308
+ }
20309
+ let best;
20310
+ for (const route of routes) {
20311
+ const parts = splitRoute(route);
20312
+ if (matches(parts, segments) && (best === void 0 || isMoreSpecific(parts, best.parts))) {
20313
+ best = { route, parts };
20314
+ }
20315
+ }
20316
+ if (best === void 0) {
20317
+ return void 0;
20318
+ }
20319
+ return { route: best.route, params: extract(best.parts, segments) };
20320
+ }, "matchEndpointPath");
20321
+
19977
20322
  // ../../libs/worker-access-token/src/endpoints.ts
19978
20323
  var APP_ENDPOINT_PREFIX = "/app/";
19979
20324
  var MAX_CUSTOM_ENDPOINT_LENGTH = 128;
@@ -20209,7 +20554,76 @@ var requireScope = /* @__PURE__ */ __name((bit) => {
20209
20554
  };
20210
20555
  }, "requireScope");
20211
20556
 
20557
+ // src/apps/stream-deadlines.ts
20558
+ var streamWithDeadlines = /* @__PURE__ */ __name((source, { idleMs, totalMs }) => {
20559
+ if (source === null) {
20560
+ return null;
20561
+ }
20562
+ const reader = source.getReader();
20563
+ let target;
20564
+ let totalTimer;
20565
+ let idleTimer = null;
20566
+ let stopped = false;
20567
+ const clearIdleTimer = /* @__PURE__ */ __name(() => {
20568
+ if (idleTimer !== null) {
20569
+ clearTimeout(idleTimer);
20570
+ idleTimer = null;
20571
+ }
20572
+ }, "clearIdleTimer");
20573
+ const clearTimers = /* @__PURE__ */ __name(() => {
20574
+ clearTimeout(totalTimer);
20575
+ clearIdleTimer();
20576
+ }, "clearTimers");
20577
+ const stop = /* @__PURE__ */ __name((message) => {
20578
+ stopped = true;
20579
+ clearTimers();
20580
+ const error51 = new DOMException(message, "TimeoutError");
20581
+ target.error(error51);
20582
+ void reader.cancel(error51).catch(() => void 0);
20583
+ }, "stop");
20584
+ return new ReadableStream({
20585
+ start: /* @__PURE__ */ __name((controller) => {
20586
+ target = controller;
20587
+ totalTimer = setTimeout(() => {
20588
+ stop("The app endpoint stream exceeded its maximum duration.");
20589
+ }, totalMs);
20590
+ }, "start"),
20591
+ pull: /* @__PURE__ */ __name(async (controller) => {
20592
+ idleTimer = setTimeout(() => {
20593
+ stop("The app endpoint stream stopped producing data.");
20594
+ }, idleMs);
20595
+ let result;
20596
+ try {
20597
+ result = await reader.read();
20598
+ } catch (error51) {
20599
+ stopped = true;
20600
+ clearTimers();
20601
+ throw error51;
20602
+ }
20603
+ clearIdleTimer();
20604
+ if (stopped) {
20605
+ return;
20606
+ }
20607
+ if (result.done) {
20608
+ stopped = true;
20609
+ clearTimers();
20610
+ controller.close();
20611
+ return;
20612
+ }
20613
+ controller.enqueue(result.value);
20614
+ }, "pull"),
20615
+ cancel: /* @__PURE__ */ __name(async (reason) => {
20616
+ stopped = true;
20617
+ clearTimers();
20618
+ await reader.cancel(reason);
20619
+ }, "cancel")
20620
+ });
20621
+ }, "streamWithDeadlines");
20622
+
20212
20623
  // src/apps/endpoint-proxy.ts
20624
+ var API_ENDPOINT_START_TIMEOUT_MS = 1e4;
20625
+ var API_ENDPOINT_STREAM_IDLE_TIMEOUT_MS = 15e4;
20626
+ var API_ENDPOINT_STREAM_MAX_DURATION_MS = 3e5;
20213
20627
  var DENIED_REQUEST_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "x-cf-data"]);
20214
20628
  var refuse = /* @__PURE__ */ __name((status, error51) => {
20215
20629
  return Response.json({ error: error51 }, { status });
@@ -20225,16 +20639,23 @@ var matchEndpoint = /* @__PURE__ */ __name((app2, path, method) => {
20225
20639
  if (!isHttpMethod(method)) {
20226
20640
  return void 0;
20227
20641
  }
20228
- const declared = app2.endpoints.find((endpoint) => {
20229
- return endpoint.path === path && endpoint.methods.includes(method);
20642
+ const serving = app2.endpoints.filter((endpoint) => {
20643
+ return endpoint.methods.includes(method);
20230
20644
  });
20231
- if (declared === void 0) {
20645
+ const matched = matchEndpointPath(
20646
+ serving.map((endpoint) => endpoint.path),
20647
+ path
20648
+ );
20649
+ if (matched === void 0) {
20232
20650
  return void 0;
20233
20651
  }
20652
+ const declared = serving.find((endpoint) => {
20653
+ return endpoint.path === matched.route;
20654
+ });
20234
20655
  if (declared.auth !== "public" && declared.auth !== "token" || declared.mode !== "webhook" && declared.mode !== "api") {
20235
20656
  return void 0;
20236
20657
  }
20237
- return { path: declared.path, method, auth: declared.auth, mode: declared.mode };
20658
+ return { route: declared.path, path, params: matched.params, method, auth: declared.auth, mode: declared.mode };
20238
20659
  }, "matchEndpoint");
20239
20660
  var authorizeEndpoint = /* @__PURE__ */ __name(async (c, slug, endpoint) => {
20240
20661
  if (endpoint.auth === "public") {
@@ -20266,12 +20687,17 @@ var passThroughResponse = /* @__PURE__ */ __name((upstream) => {
20266
20687
  const headers = new Headers(upstream.headers);
20267
20688
  headers.delete("set-cookie");
20268
20689
  headers.delete(APP_SPANS_HEADER);
20690
+ headers.delete(APP_OBSERVABILITY_HEADER);
20269
20691
  for (const name of [...headers.keys()]) {
20270
20692
  if (name.startsWith("access-control-")) {
20271
20693
  headers.delete(name);
20272
20694
  }
20273
20695
  }
20274
- return new Response(upstream.body, { status: upstream.status, headers });
20696
+ const body = streamWithDeadlines(upstream.body, {
20697
+ idleMs: API_ENDPOINT_STREAM_IDLE_TIMEOUT_MS,
20698
+ totalMs: API_ENDPOINT_STREAM_MAX_DURATION_MS
20699
+ });
20700
+ return new Response(body, { status: upstream.status, headers });
20275
20701
  }, "passThroughResponse");
20276
20702
  var parseJson3 = /* @__PURE__ */ __name((text) => {
20277
20703
  try {
@@ -20301,6 +20727,17 @@ var webhookAck = /* @__PURE__ */ __name(async (slug, upstream) => {
20301
20727
  }
20302
20728
  return refuse(502, "upstream");
20303
20729
  }, "webhookAck");
20730
+ var fetchApiResponse = /* @__PURE__ */ __name(async (service, url2, init) => {
20731
+ const controller = new AbortController();
20732
+ const timer = setTimeout(() => {
20733
+ controller.abort(new DOMException("The app endpoint did not start within its deadline.", "TimeoutError"));
20734
+ }, API_ENDPOINT_START_TIMEOUT_MS);
20735
+ try {
20736
+ return await service.fetch(url2, { ...init, signal: controller.signal });
20737
+ } finally {
20738
+ clearTimeout(timer);
20739
+ }
20740
+ }, "fetchApiResponse");
20304
20741
  var runForwardToApp = /* @__PURE__ */ __name(async (c, input) => {
20305
20742
  const { slug, app: app2, endpoint, installs, body, search } = input;
20306
20743
  const context = invocationContext(c.env);
@@ -20320,25 +20757,30 @@ var runForwardToApp = /* @__PURE__ */ __name(async (c, input) => {
20320
20757
  headers.set(
20321
20758
  ENDPOINT_CONTEXT_HEADER,
20322
20759
  JSON.stringify({
20760
+ route: endpoint.route,
20323
20761
  path: endpoint.path,
20762
+ params: endpoint.params,
20324
20763
  method: endpoint.method,
20325
20764
  mode: endpoint.mode,
20326
20765
  version: app2.version,
20766
+ site: { origin: context.origin },
20327
20767
  installs: installs.map((install) => ({ id: install.id, ord: install.ord, label: install.label }))
20328
20768
  })
20329
20769
  );
20330
20770
  let upstream;
20331
20771
  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
- });
20772
+ const url2 = `https://app/__qinto/endpoint${endpoint.route}${search}`;
20773
+ const init = { method: endpoint.method, headers, body };
20774
+ upstream = endpoint.mode === "api" ? await fetchApiResponse(service, url2, init) : await service.fetch(url2, { ...init, signal: AbortSignal.timeout(APP_INVOCATION_TIMEOUT_MS) });
20338
20775
  } catch (error51) {
20339
20776
  return upstreamFailed(slug, String(error51));
20340
20777
  }
20341
20778
  adoptAppSpans(upstream);
20779
+ writeAppObservability(c.env, upstream, {
20780
+ app: observabilityApp(slug, app2),
20781
+ installs,
20782
+ budget: createDataPointBudget()
20783
+ });
20342
20784
  setSpanAttributes({ "app.status": upstream.status });
20343
20785
  return endpoint.mode === "api" ? passThroughResponse(upstream) : webhookAck(slug, upstream);
20344
20786
  }, "runForwardToApp");
@@ -20351,7 +20793,7 @@ var forwardToApp = /* @__PURE__ */ __name(async (c, input) => {
20351
20793
  "app.slug": slug,
20352
20794
  "app.version": app2.version,
20353
20795
  "app.binding": app2.binding,
20354
- "app.hook": `endpoint.${endpoint.method} ${endpoint.path}`,
20796
+ "app.hook": `endpoint.${endpoint.method} ${endpoint.route}`,
20355
20797
  "app.endpoint_mode": endpoint.mode,
20356
20798
  "app.installs": installs.length
20357
20799
  }
@@ -20367,7 +20809,11 @@ var handleAppEndpoint = /* @__PURE__ */ __name(async (c, slug) => {
20367
20809
  return refuse(404, "not_found");
20368
20810
  }
20369
20811
  const url2 = new URL(c.req.url);
20370
- const endpoint = matchEndpoint(app2, url2.pathname.slice(`/app/${slug}`.length), c.req.method);
20812
+ const prefix = `/app/${slug}`;
20813
+ if (url2.pathname !== prefix && !url2.pathname.startsWith(`${prefix}/`)) {
20814
+ return refuse(404, "not_found");
20815
+ }
20816
+ const endpoint = matchEndpoint(app2, url2.pathname.slice(prefix.length) || "/", c.req.method);
20371
20817
  if (endpoint === void 0) {
20372
20818
  return refuse(404, "not_found");
20373
20819
  }
@@ -20380,7 +20826,7 @@ var handleAppEndpoint = /* @__PURE__ */ __name(async (c, slug) => {
20380
20826
  if (first === void 0) {
20381
20827
  return refuse(503, "inactive");
20382
20828
  }
20383
- const key = endpoint.mode === "api" ? `${slug}:${first.id}:endpoint` : `${slug}:${endpoint.path}:endpoint`;
20829
+ const key = endpoint.mode === "api" ? `${slug}:${first.id}:endpoint` : `${slug}:${endpoint.route}:endpoint`;
20384
20830
  if (await rateLimited(c.env, key)) {
20385
20831
  return refuse(429, "rate_limited");
20386
20832
  }
@@ -20650,16 +21096,22 @@ var runForwardToApp2 = /* @__PURE__ */ __name(async (c, input) => {
20650
21096
  method: c.req.method,
20651
21097
  headers,
20652
21098
  body,
20653
- signal: AbortSignal.timeout(HOOK_TIMEOUT_MS)
21099
+ signal: AbortSignal.timeout(APP_INVOCATION_TIMEOUT_MS)
20654
21100
  });
20655
21101
  } catch (error51) {
20656
21102
  return upstreamFailed2(origin, slug, String(error51));
20657
21103
  }
20658
21104
  adoptAppSpans(upstream);
21105
+ writeAppObservability(c.env, upstream, {
21106
+ app: observabilityApp(slug, app2),
21107
+ installs: [{ id: install.id, ord: install.ord, label: install.label }],
21108
+ budget: createDataPointBudget()
21109
+ });
20659
21110
  setSpanAttributes({ "app.status": upstream.status });
20660
21111
  const responseHeaders = new Headers(upstream.headers);
20661
21112
  responseHeaders.delete("set-cookie");
20662
21113
  responseHeaders.delete(APP_SPANS_HEADER);
21114
+ responseHeaders.delete(APP_OBSERVABILITY_HEADER);
20663
21115
  for (const name of [...responseHeaders.keys()]) {
20664
21116
  if (name.startsWith("access-control-")) {
20665
21117
  responseHeaders.delete(name);
@@ -23693,6 +24145,21 @@ var withoutQuery = /* @__PURE__ */ __name((url2) => {
23693
24145
  parsed.search = "";
23694
24146
  return parsed.toString();
23695
24147
  }, "withoutQuery");
24148
+ var browserGeoFrom = /* @__PURE__ */ __name((geo) => {
24149
+ if (geo === void 0) {
24150
+ return void 0;
24151
+ }
24152
+ return {
24153
+ country: geo.country ?? null,
24154
+ // EdgeTag's geoRegion is Cloudflare's subdivision code.
24155
+ region: geo.regionCode ?? null,
24156
+ state: geo.region ?? null,
24157
+ isEU: geo.isEUCountry === "yes"
24158
+ };
24159
+ }, "browserGeoFrom");
24160
+ var attachmentFieldBytes = /* @__PURE__ */ __name((name, value) => {
24161
+ return value === void 0 ? 0 : new TextEncoder().encode(`,"${name}":${JSON.stringify(value)}`).length;
24162
+ }, "attachmentFieldBytes");
23696
24163
  var connectionAttachment = /* @__PURE__ */ __name((request, geo) => {
23697
24164
  const userAgent = request.headers.get("user-agent");
23698
24165
  const attrs = {
@@ -23708,15 +24175,18 @@ var connectionAttachment = /* @__PURE__ */ __name((request, geo) => {
23708
24175
  locale: raw2.locale !== void 0 && raw2.locale.length <= MAX_HOST_LOCALE_LENGTH ? raw2.locale : void 0
23709
24176
  };
23710
24177
  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;
24178
+ const hostBytes = attachmentFieldBytes("host", host);
24179
+ const browserGeo = browserGeoFrom(geo);
24180
+ const browserGeoBytes = attachmentFieldBytes("browserGeo", browserGeo);
23712
24181
  const cf = forwardedCf(request);
23713
24182
  const cfSuffix = cf === void 0 ? "" : `,"cf":${JSON.stringify(cf)}`;
23714
24183
  const cfBytes = new TextEncoder().encode(cfSuffix).length;
23715
24184
  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));
24185
+ const includedCf = cfBytes <= MAX_ATTACHMENT_BYTES - emptyAttrsBytes - hostBytes - browserGeoBytes ? cf : void 0;
24186
+ const budgeted = budgetAttrs(attrs, MAX_ATTACHMENT_BYTES - hostBytes - browserGeoBytes - (includedCf === void 0 ? 0 : cfBytes));
23718
24187
  return {
23719
24188
  ...budgeted,
24189
+ ...browserGeo === void 0 ? {} : { browserGeo },
23720
24190
  ...includedCf === void 0 ? {} : { cf: includedCf },
23721
24191
  ...host === void 0 ? {} : { host }
23722
24192
  };
@@ -23803,14 +24273,14 @@ var open = /* @__PURE__ */ __name(async (c) => {
23803
24273
 
23804
24274
  // browser-artifact/meta.json
23805
24275
  var meta_default = {
23806
- version: "0.1.1",
23807
- gitSha: "45acc2f65889b33f7cf2828b967a26fd879455f0",
23808
- builtAt: "2026-08-25T22:08:34.269Z",
23809
- hash: "8f1c955d16978159926d9c98b73ee2d0a3c3d1ea51d4ec8e1b017c3ea6892ea1"
24276
+ version: "0.1.3",
24277
+ gitSha: "66e1bc45acbe4891948cbb3851f3f43ce06abc84",
24278
+ builtAt: "2026-08-28T17:17:19.113Z",
24279
+ hash: "050bb7dd702ac407623ce130c324f3342f048b8a201e2975994d5ef54de7f19f"
23810
24280
  };
23811
24281
 
23812
24282
  // src/paths/q-js.ts
23813
- import qScript from "./9dcfd236748459af038da772477100c4bb0223b7-q.js.txt";
24283
+ import qScript from "./9026260a7a674b710223f296c5d2e7a01930cfed-q.js.txt";
23814
24284
 
23815
24285
  // src/apps-q.ts
23816
24286
  import appsQScript from "./apps-q.json";
@@ -23867,6 +24337,7 @@ var appsMetadata = /* @__PURE__ */ __name(() => {
23867
24337
  return Object.fromEntries(
23868
24338
  Object.entries(registry3.apps).map(([slug, app2]) => {
23869
24339
  const capture = declaredCapture(app2);
24340
+ const geo = app2.data.includes("geo") ? { geo: true } : {};
23870
24341
  const sharedContext = app2.data.includes(SHARED_CONTEXT_CAPABILITY) ? { sharedContext: true } : {};
23871
24342
  return [
23872
24343
  slug,
@@ -23874,6 +24345,7 @@ var appsMetadata = /* @__PURE__ */ __name(() => {
23874
24345
  category: app2.consent.category,
23875
24346
  eventResults: app2.eventResults,
23876
24347
  ...capture === void 0 ? {} : { capture },
24348
+ ...geo,
23877
24349
  ...sharedContext
23878
24350
  }
23879
24351
  ];
@@ -24520,7 +24992,7 @@ var runPool = /* @__PURE__ */ __name(async (limit, tasks) => {
24520
24992
  }
24521
24993
  await Promise.all(running);
24522
24994
  }, "runPool");
24523
- var fanOutToApps = /* @__PURE__ */ __name(async (env, trigger, snapshot, { deliverPush, writeAttribution: writeAttribution2, writeContext: writeContext2, trace }) => {
24995
+ var fanOutToApps = /* @__PURE__ */ __name(async (env, trigger, snapshot, { deliverPush, writeAttribution: writeAttribution2, writeContext: writeContext2, budget, trace }) => {
24524
24996
  const { registry: registry3, error: error51 } = siteRegistry();
24525
24997
  const targets = eligibleApps(registry3, trigger, snapshot.consent, consentEnforced(env));
24526
24998
  if (error51 === void 0 && targets.length === 0) {
@@ -24548,7 +25020,7 @@ var fanOutToApps = /* @__PURE__ */ __name(async (env, trigger, snapshot, { deliv
24548
25020
  await runPool(
24549
25021
  FAN_OUT_CONCURRENCY,
24550
25022
  capped.map((target) => {
24551
- return () => invokeApp(env, { target, trigger, snapshot, context, deliverPush, writeAttribution: writeAttribution2, writeContext: writeContext2 });
25023
+ return () => invokeApp(env, { target, trigger, snapshot, context, deliverPush, writeAttribution: writeAttribution2, writeContext: writeContext2, budget });
24552
25024
  })
24553
25025
  );
24554
25026
  }
@@ -24605,7 +25077,7 @@ var withReplacedPayload = /* @__PURE__ */ __name((entry, action, payload) => {
24605
25077
  }
24606
25078
  return { ...action, payload: parsed.data };
24607
25079
  }, "withReplacedPayload");
24608
- var postTransform = /* @__PURE__ */ __name(async (env, entry, post) => {
25080
+ var postTransform = /* @__PURE__ */ __name(async (env, entry, post, dataPoints) => {
24609
25081
  let response;
24610
25082
  try {
24611
25083
  response = await postHookEnvelope(env, { binding: entry.app.binding, ...post });
@@ -24615,6 +25087,11 @@ var postTransform = /* @__PURE__ */ __name(async (env, entry, post) => {
24615
25087
  if (response === void 0) {
24616
25088
  throw entryFailure(entry, `the ${entry.app.binding} service binding is missing`);
24617
25089
  }
25090
+ writeAppObservability(env, response, {
25091
+ app: observabilityApp(entry.slug, entry.app),
25092
+ installs: [entry.install],
25093
+ budget: dataPoints
25094
+ });
24618
25095
  setSpanAttributes({ "app.status": response.status });
24619
25096
  if (!response.ok) {
24620
25097
  throw entryFailure(entry, `the hook answered status ${response.status}`);
@@ -24629,14 +25106,14 @@ var postTransform = /* @__PURE__ */ __name(async (env, entry, post) => {
24629
25106
  throw entryFailure(entry, String(error51));
24630
25107
  }
24631
25108
  }, "postTransform");
24632
- var runTransformEntry = /* @__PURE__ */ __name(async (env, { entry, action, snapshot, context, timeoutMs }) => {
25109
+ var runTransformEntry = /* @__PURE__ */ __name(async (env, { entry, action, snapshot, context, timeoutMs, dataPoints }) => {
24633
25110
  const target = { slug: entry.slug, app: entry.app, installs: [entry.install] };
24634
25111
  const envelope = {
24635
25112
  ...buildEnvelope(target, { kind: "event", action }, snapshot, context),
24636
25113
  kind: "transform"
24637
25114
  };
24638
25115
  const key = await deriveAppInternalKey(context.master, context.domainId, entry.slug);
24639
- const verdict = validateEnvelopeResponse(await postTransform(env, entry, { key, envelope, timeoutMs }), 1);
25116
+ const verdict = validateEnvelopeResponse(await postTransform(env, entry, { key, envelope, timeoutMs }, dataPoints), 1);
24640
25117
  if (!verdict.ok) {
24641
25118
  throw entryFailure(entry, verdict.reason);
24642
25119
  }
@@ -24704,7 +25181,7 @@ var buildRelease = /* @__PURE__ */ __name((env, { collected, action, deliverPush
24704
25181
  }
24705
25182
  };
24706
25183
  }, "buildRelease");
24707
- var runTransformChain = /* @__PURE__ */ __name(async (env, { action, snapshot, deliverPush, writeAttribution: writeAttribution2, writeContext: writeContext2 }) => {
25184
+ var runTransformChain = /* @__PURE__ */ __name(async (env, { action, snapshot, deliverPush, writeAttribution: writeAttribution2, writeContext: writeContext2, dataPoints }) => {
24708
25185
  const { registry: registry3, error: error51 } = siteRegistry();
24709
25186
  if (error51 !== void 0) {
24710
25187
  throw new TransformChainError(`the apps registry is malformed: ${error51}`);
@@ -24724,7 +25201,14 @@ var runTransformChain = /* @__PURE__ */ __name(async (env, { action, snapshot, d
24724
25201
  for (const entry of entries) {
24725
25202
  const stepped = entryBudget(budget, entry, Date.now());
24726
25203
  budget = stepped.budget;
24727
- const verdict = await invokeTransformEntry(env, { entry, action: current, snapshot, context, timeoutMs: stepped.timeoutMs });
25204
+ const verdict = await invokeTransformEntry(env, {
25205
+ entry,
25206
+ action: current,
25207
+ snapshot,
25208
+ context,
25209
+ timeoutMs: stepped.timeoutMs,
25210
+ dataPoints
25211
+ });
24728
25212
  if (verdict.effects.accepted.length > 0) {
24729
25213
  collected.push({ slug: entry.slug, installs: [entry.install], groups: verdict.effects.accepted });
24730
25214
  }
@@ -25705,7 +26189,11 @@ var AnonDO = class extends DurableObject2 {
25705
26189
  const attachment = connectionAttachment(request, geo);
25706
26190
  server.serializeAttachment(attachment);
25707
26191
  this.deliverSharedContext(server);
25708
- this.appFanOut({ kind: "connect", ...attachment.cf === void 0 ? {} : { geo: attachment.cf }, host: attachment.host }, userId);
26192
+ this.appFanOut(
26193
+ { kind: "connect", ...attachment.cf === void 0 ? {} : { geo: attachment.cf }, host: attachment.host },
26194
+ userId,
26195
+ createDataPointBudget()
26196
+ );
25709
26197
  return new Response(null, { status: 101, webSocket: client });
25710
26198
  });
25711
26199
  }
@@ -25760,7 +26248,14 @@ var AnonDO = class extends DurableObject2 {
25760
26248
  return;
25761
26249
  }
25762
26250
  if (parsed.frame.t === "getSession") {
25763
- this.respond(ws, { t: "session", ref: parsed.ref, anonId: userId, globalId: linkedGlobalId(this.kv) });
26251
+ this.respond(ws, {
26252
+ t: "session",
26253
+ ref: parsed.ref,
26254
+ anonId: userId,
26255
+ globalId: linkedGlobalId(this.kv),
26256
+ ip: attachment?.host?.ip ?? null,
26257
+ geo: attachment?.browserGeo ?? null
26258
+ });
25764
26259
  return;
25765
26260
  }
25766
26261
  if (parsed.frame.t === "capture") {
@@ -26046,12 +26541,13 @@ var AnonDO = class extends DurableObject2 {
26046
26541
  if (isNoopAction(action)) {
26047
26542
  return;
26048
26543
  }
26049
- const transformed = await this.transformAction(action);
26544
+ const dataPoints = createDataPointBudget();
26545
+ const transformed = await this.transformAction(action, dataPoints);
26050
26546
  if (transformed === null) {
26051
26547
  return;
26052
26548
  }
26053
26549
  recorded = transformed.action;
26054
- await this.runAction(transformed.action, transformed.releaseEmissions);
26550
+ await this.runAction(transformed.action, transformed.releaseEmissions, dataPoints);
26055
26551
  } finally {
26056
26552
  this.recordActionBlocks(recorded);
26057
26553
  }
@@ -26061,9 +26557,9 @@ var AnonDO = class extends DurableObject2 {
26061
26557
  actionBlocks(action, this.appsSnapshot(action.userId), this.wiped ? [] : allIdentifiers(this.sql, this.identifiersMirror))
26062
26558
  );
26063
26559
  }
26064
- async runAction(current, releaseEmissions) {
26560
+ async runAction(current, releaseEmissions, dataPoints) {
26065
26561
  if (current.type !== "alias" && isDeliveryOnlyAction(current)) {
26066
- this.appFanOut({ kind: "event", action: current }, current.userId);
26562
+ this.appFanOut({ kind: "event", action: current }, current.userId, dataPoints);
26067
26563
  this.releaseTransformEmissions(releaseEmissions);
26068
26564
  return;
26069
26565
  }
@@ -26099,7 +26595,7 @@ var AnonDO = class extends DurableObject2 {
26099
26595
  } else {
26100
26596
  await this.ingestConsent(current);
26101
26597
  }
26102
- this.appFanOut({ kind: "event", action: current }, current.userId);
26598
+ this.appFanOut({ kind: "event", action: current }, current.userId, dataPoints);
26103
26599
  this.releaseTransformEmissions(releaseEmissions);
26104
26600
  } finally {
26105
26601
  if (novelIdentifier) {
@@ -26108,7 +26604,7 @@ var AnonDO = class extends DurableObject2 {
26108
26604
  }
26109
26605
  }
26110
26606
  // null means a transform dropped the action: the turn succeeds with nothing persisted (§5.4).
26111
- async transformAction(action) {
26607
+ async transformAction(action, dataPoints) {
26112
26608
  if (action.type !== "track" && action.type !== "identify") {
26113
26609
  return { action, releaseEmissions: null };
26114
26610
  }
@@ -26117,7 +26613,8 @@ var AnonDO = class extends DurableObject2 {
26117
26613
  snapshot: this.appsSnapshot(action.userId),
26118
26614
  deliverPush: this.pushDelivery,
26119
26615
  writeAttribution: this.attributionDelivery,
26120
- writeContext: this.contextDelivery
26616
+ writeContext: this.contextDelivery,
26617
+ dataPoints
26121
26618
  });
26122
26619
  if (outcome.dropped) {
26123
26620
  this.releaseTransformEmissions(outcome.releaseEmissions);
@@ -26153,13 +26650,14 @@ var AnonDO = class extends DurableObject2 {
26153
26650
  }
26154
26651
  this.ctx.waitUntil(release().catch((error51) => console.warn("transform emission release failed", { error: error51 })));
26155
26652
  }
26156
- appFanOut(trigger, anonId) {
26653
+ appFanOut(trigger, anonId, dataPoints) {
26157
26654
  const snapshot = this.appsSnapshot(anonId);
26158
26655
  this.ctx.waitUntil(
26159
26656
  fanOutToApps(this.env, trigger, snapshot, {
26160
26657
  deliverPush: this.pushDelivery,
26161
26658
  writeAttribution: this.attributionDelivery,
26162
26659
  writeContext: this.contextDelivery,
26660
+ budget: dataPoints,
26163
26661
  trace: currentTraceContext()
26164
26662
  }).catch((error51) => console.warn("app fan-out failed", { error: error51 }))
26165
26663
  );