@blotout/qinto-cli 0.1.10 → 0.1.12

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.
@@ -14706,7 +14706,7 @@ var scheduleSchema = external_exports.object({
14706
14706
  id: external_exports.string().min(1).max(64),
14707
14707
  ord: external_exports.number().int().min(1),
14708
14708
  label: external_exports.string().max(100),
14709
- catalogQueryTicket: external_exports.string().min(1).max(2048)
14709
+ queryTicket: external_exports.string().min(1).max(2048)
14710
14710
  }).strict()
14711
14711
  ).min(1).max(MAX_INSTALLS_PER_APP)
14712
14712
  }).strict();
@@ -14722,7 +14722,10 @@ var toolSchema = external_exports.object({
14722
14722
  // call that names no arguments normalizes to `{}` rather than reaching the handler as undefined.
14723
14723
  arguments: external_exports.record(external_exports.string(), external_exports.unknown()).default({}),
14724
14724
  // Stable across a collapsed retry, so an app can deduplicate its external effects on it.
14725
- callId: external_exports.string().min(1).max(64)
14725
+ callId: external_exports.string().min(1).max(64),
14726
+ // The short-lived install/version-scoped credential used only by the app runtime's query
14727
+ // broker. It never enters handler arguments or app-authored result data.
14728
+ queryTicket: external_exports.string().min(1).max(2048)
14726
14729
  }).strict().refine((tool) => {
14727
14730
  return new TextEncoder().encode(JSON.stringify(tool.arguments)).length <= MAX_TOOL_ARGUMENTS_BYTES;
14728
14731
  }, "Tool arguments exceed the size cap");
@@ -14932,6 +14935,9 @@ var appUiWritePath = (domainId, appSlug2) => {
14932
14935
  var appUiCatalogQueryPath = (domainId, appSlug2, resourceIdentifier) => {
14933
14936
  return `/app-ui/sites/${domainId}/apps/${appSlug2}/catalogs/${resourceIdentifier}/query`;
14934
14937
  };
14938
+ var appUiAnalyticsQueryPath = (domainId, appSlug2, resourceIdentifier) => {
14939
+ return `/app-ui/sites/${domainId}/apps/${appSlug2}/analytics/${resourceIdentifier}/query`;
14940
+ };
14935
14941
  var uiInstallSchema = external_exports.object({ id: external_exports.string().min(1).max(64), ord: external_exports.number().int().min(1), label: external_exports.string().max(100), enabled: external_exports.boolean() }).strict();
14936
14942
  var uiContextSchema = external_exports.object({
14937
14943
  version: external_exports.string().min(1).max(32),
@@ -16195,43 +16201,58 @@ var failureMessage = (error51) => {
16195
16201
  return message.slice(0, MAX_ERROR_MESSAGE_LENGTH);
16196
16202
  };
16197
16203
 
16198
- // src/catalog-query.ts
16199
- var MAX_CATALOG_RESPONSE_BYTES = 5 * 1024 * 1024;
16200
- var parseCatalogResponse = async (response) => {
16201
- const raw = await readBody(response, MAX_CATALOG_RESPONSE_BYTES);
16204
+ // src/query.ts
16205
+ var MAX_QUERY_RESPONSE_BYTES = 5 * 1024 * 1024;
16206
+ var parseQueryResponse = async (kind, response) => {
16207
+ const raw = await readBody(response, MAX_QUERY_RESPONSE_BYTES);
16202
16208
  if (raw === null) {
16203
- throw new Error("The catalog query response exceeded the app runtime limit.");
16209
+ throw new Error(`The ${kind} query response exceeded the app runtime limit.`);
16204
16210
  }
16205
16211
  let body;
16206
16212
  try {
16207
16213
  body = JSON.parse(raw);
16208
16214
  } catch {
16209
- throw new Error(`The catalog query returned an invalid response (${response.status}).`);
16215
+ throw new Error(`The ${kind} query returned an invalid response (${response.status}).`);
16210
16216
  }
16211
16217
  if (!response.ok) {
16212
16218
  const error51 = body;
16213
16219
  const reason = typeof error51.message === "string" ? error51.message : typeof error51.error === "string" ? error51.error : `request failed with status ${response.status}`;
16214
- throw new Error(`Catalog query failed: ${reason}`);
16220
+ throw new Error(`${kind === "catalog" ? "Catalog" : "Analytics"} query failed: ${reason}`);
16215
16221
  }
16216
16222
  return body;
16217
16223
  };
16218
- var buildCatalogQuery = ({ env, ticket, installId }) => {
16224
+ var buildQuery = ({ env, ticket, installId }) => {
16225
+ const platform = env;
16226
+ const context = JSON.parse(platform.APP_CONTEXT);
16227
+ const post = async (kind, path, resourceIdentifier, sql) => {
16228
+ return traced(
16229
+ `qinto.${kind}.query`,
16230
+ { attrs: { "app.install_id": installId, [`app.${kind}.resource`]: resourceIdentifier } },
16231
+ async () => {
16232
+ const response = await fetch(new URL(path, platform.QINTO_PLATFORM_API), {
16233
+ method: "POST",
16234
+ headers: { authorization: `Bearer ${ticket}`, "content-type": "application/json" },
16235
+ body: JSON.stringify({ sql })
16236
+ });
16237
+ return parseQueryResponse(kind, response);
16238
+ }
16239
+ );
16240
+ };
16219
16241
  return {
16220
16242
  catalog: async (resourceIdentifier, sql) => {
16221
- const platform = env;
16222
- const context = JSON.parse(platform.APP_CONTEXT);
16223
- const path = appUiCatalogQueryPath(context.domainId, context.slug, resourceIdentifier);
16224
- return traced(
16225
- "qinto.catalog.query",
16226
- { attrs: { "app.install_id": installId, "app.catalog.resource": resourceIdentifier } },
16227
- async () => {
16228
- const response = await fetch(new URL(path, platform.QINTO_PLATFORM_API), {
16229
- method: "POST",
16230
- headers: { authorization: `Bearer ${ticket}`, "content-type": "application/json" },
16231
- body: JSON.stringify({ sql })
16232
- });
16233
- return parseCatalogResponse(response);
16234
- }
16243
+ return post(
16244
+ "catalog",
16245
+ appUiCatalogQueryPath(context.domainId, context.slug, resourceIdentifier),
16246
+ resourceIdentifier,
16247
+ sql
16248
+ );
16249
+ },
16250
+ analytics: async (resourceIdentifier, sql) => {
16251
+ return post(
16252
+ "analytics",
16253
+ appUiAnalyticsQueryPath(context.domainId, context.slug, resourceIdentifier),
16254
+ resourceIdentifier,
16255
+ sql
16235
16256
  );
16236
16257
  }
16237
16258
  };
@@ -16273,7 +16294,7 @@ var dispatchSchedule = async (envelope, bundle, env) => {
16273
16294
  appSecrets: appScope.appSecrets,
16274
16295
  observability: buildObservability(install.ord),
16275
16296
  apps: buildAppsClient(env, { depth: 0 }),
16276
- query: buildCatalogQuery({ env, ticket: install.catalogQueryTicket, installId: install.id }),
16297
+ query: buildQuery({ env, ticket: install.queryTicket, installId: install.id }),
16277
16298
  push: async (userId, message) => {
16278
16299
  await brokerPush(env, {
16279
16300
  userId,
@@ -16393,6 +16414,7 @@ var buildContext2 = (envelope, env) => {
16393
16414
  appSecrets: appScope.appSecrets,
16394
16415
  observability: buildObservability(install.ord),
16395
16416
  apps: buildAppsClient(env, { depth: 0 }),
16417
+ query: buildQuery({ env, ticket: envelope.tool.queryTicket, installId: install.id }),
16396
16418
  fail: (message) => {
16397
16419
  return { kind: "tool_error", message: truncate(message) };
16398
16420
  }
@@ -570,6 +570,92 @@ var appendSpan = /* @__PURE__ */ __name((span) => {
570
570
  scope.buffer.spans.push(span);
571
571
  }, "appendSpan");
572
572
 
573
+ // ../../node_modules/.pnpm/hono@4.12.28/node_modules/hono/dist/http-exception.js
574
+ var HTTPException = class extends Error {
575
+ static {
576
+ __name(this, "HTTPException");
577
+ }
578
+ res;
579
+ status;
580
+ /**
581
+ * Creates an instance of `HTTPException`.
582
+ * @param status - HTTP status code for the exception. Defaults to 500.
583
+ * @param options - Additional options for the exception.
584
+ */
585
+ constructor(status = 500, options) {
586
+ super(options?.message, { cause: options?.cause });
587
+ this.res = options?.res;
588
+ this.status = status;
589
+ }
590
+ /**
591
+ * Returns the response object associated with the exception.
592
+ * If a response object is not provided, a new response is created with the error message and status code.
593
+ * @returns The response object.
594
+ */
595
+ getResponse() {
596
+ if (this.res) {
597
+ const newResponse = new Response(this.res.body, {
598
+ status: this.status,
599
+ headers: this.res.headers
600
+ });
601
+ return newResponse;
602
+ }
603
+ return new Response(this.message, {
604
+ status: this.status
605
+ });
606
+ }
607
+ };
608
+
609
+ // ../../node_modules/.pnpm/hono@4.12.28/node_modules/hono/dist/middleware/body-limit/index.js
610
+ var ERROR_MESSAGE = "Payload Too Large";
611
+ var bodyLimit = /* @__PURE__ */ __name((options) => {
612
+ const onError = options.onError || (() => {
613
+ const res = new Response(ERROR_MESSAGE, {
614
+ status: 413
615
+ });
616
+ throw new HTTPException(413, { res });
617
+ });
618
+ const maxSize = options.maxSize;
619
+ return /* @__PURE__ */ __name(async function bodyLimit2(c, next) {
620
+ if (!c.req.raw.body) {
621
+ return next();
622
+ }
623
+ const hasTransferEncoding = c.req.raw.headers.has("transfer-encoding");
624
+ const hasContentLength = c.req.raw.headers.has("content-length");
625
+ if (hasContentLength && !hasTransferEncoding) {
626
+ const contentLength = parseInt(c.req.raw.headers.get("content-length") || "0", 10);
627
+ return contentLength > maxSize ? onError(c) : next();
628
+ }
629
+ let size = 0;
630
+ const chunks = [];
631
+ const rawReader = c.req.raw.body.getReader();
632
+ for (; ; ) {
633
+ const { done, value } = await rawReader.read();
634
+ if (done) {
635
+ break;
636
+ }
637
+ size += value.length;
638
+ if (size > maxSize) {
639
+ return onError(c);
640
+ }
641
+ chunks.push(value);
642
+ }
643
+ const requestInit = {
644
+ body: new ReadableStream({
645
+ start(controller) {
646
+ for (const chunk of chunks) {
647
+ controller.enqueue(chunk);
648
+ }
649
+ controller.close();
650
+ }
651
+ }),
652
+ duplex: "half"
653
+ };
654
+ c.req.raw = new Request(c.req.raw, requestInit);
655
+ return next();
656
+ }, "bodyLimit2");
657
+ }, "bodyLimit");
658
+
573
659
  // ../../node_modules/.pnpm/hono@4.12.28/node_modules/hono/dist/middleware/cors/index.js
574
660
  var cors = /* @__PURE__ */ __name((options) => {
575
661
  const opts = {
@@ -653,42 +739,6 @@ var cors = /* @__PURE__ */ __name((options) => {
653
739
  }, "cors2");
654
740
  }, "cors");
655
741
 
656
- // ../../node_modules/.pnpm/hono@4.12.28/node_modules/hono/dist/http-exception.js
657
- var HTTPException = class extends Error {
658
- static {
659
- __name(this, "HTTPException");
660
- }
661
- res;
662
- status;
663
- /**
664
- * Creates an instance of `HTTPException`.
665
- * @param status - HTTP status code for the exception. Defaults to 500.
666
- * @param options - Additional options for the exception.
667
- */
668
- constructor(status = 500, options) {
669
- super(options?.message, { cause: options?.cause });
670
- this.res = options?.res;
671
- this.status = status;
672
- }
673
- /**
674
- * Returns the response object associated with the exception.
675
- * If a response object is not provided, a new response is created with the error message and status code.
676
- * @returns The response object.
677
- */
678
- getResponse() {
679
- if (this.res) {
680
- const newResponse = new Response(this.res.body, {
681
- status: this.status,
682
- headers: this.res.headers
683
- });
684
- return newResponse;
685
- }
686
- return new Response(this.message, {
687
- status: this.status
688
- });
689
- }
690
- };
691
-
692
742
  // src/apps/broker.ts
693
743
  import { timingSafeEqual } from "node:crypto";
694
744
  import { WorkerEntrypoint } from "cloudflare:workers";
@@ -15932,7 +15982,7 @@ var scheduleSchema = external_exports.object({
15932
15982
  id: external_exports.string().min(1).max(64),
15933
15983
  ord: external_exports.number().int().min(1),
15934
15984
  label: external_exports.string().max(100),
15935
- catalogQueryTicket: external_exports.string().min(1).max(2048)
15985
+ queryTicket: external_exports.string().min(1).max(2048)
15936
15986
  }).strict()
15937
15987
  ).min(1).max(MAX_INSTALLS_PER_APP)
15938
15988
  }).strict();
@@ -15948,7 +15998,10 @@ var toolSchema = external_exports.object({
15948
15998
  // call that names no arguments normalizes to `{}` rather than reaching the handler as undefined.
15949
15999
  arguments: external_exports.record(external_exports.string(), external_exports.unknown()).default({}),
15950
16000
  // Stable across a collapsed retry, so an app can deduplicate its external effects on it.
15951
- callId: external_exports.string().min(1).max(64)
16001
+ callId: external_exports.string().min(1).max(64),
16002
+ // The short-lived install/version-scoped credential used only by the app runtime's query
16003
+ // broker. It never enters handler arguments or app-authored result data.
16004
+ queryTicket: external_exports.string().min(1).max(2048)
15952
16005
  }).strict().refine((tool) => {
15953
16006
  return new TextEncoder().encode(JSON.stringify(tool.arguments)).length <= MAX_TOOL_ARGUMENTS_BYTES;
15954
16007
  }, "Tool arguments exceed the size cap");
@@ -20993,10 +21046,10 @@ var verifyAppUiToken = /* @__PURE__ */ __name(async (input) => {
20993
21046
  return { ok: true, claims: claims.data, kid: signature.kid, key: signature.key };
20994
21047
  }, "verifyAppUiToken");
20995
21048
 
20996
- // ../../libs/app-ui-token/src/schedule-catalog-ticket.ts
20997
- var APP_SCHEDULE_CATALOG_TICKET_ISSUER = "qinto-portal";
21049
+ // ../../libs/app-ui-token/src/runtime-query-ticket.ts
21050
+ var APP_RUNTIME_QUERY_TICKET_ISSUER = "qinto-portal";
20998
21051
  var claimsSchema2 = external_exports.object({
20999
- iss: external_exports.literal(APP_SCHEDULE_CATALOG_TICKET_ISSUER),
21052
+ iss: external_exports.literal(APP_RUNTIME_QUERY_TICKET_ISSUER),
21000
21053
  aud: external_exports.string().min(1),
21001
21054
  sub: external_exports.string().min(1),
21002
21055
  iat: external_exports.number().int(),
@@ -25968,9 +26021,29 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_facts_conversion ON facts (key) WHERE kind
25968
26021
  "ALTER TABLE facts ADD COLUMN expires_at INTEGER;"
25969
26022
  ];
25970
26023
 
26024
+ // src/id-graph/anon/token-bucket.ts
26025
+ var createTokenBucket = /* @__PURE__ */ __name((capacity, refillPerSecond) => {
26026
+ let tokens = capacity;
26027
+ let updatedAt = null;
26028
+ return {
26029
+ take: /* @__PURE__ */ __name((now) => {
26030
+ if (updatedAt !== null) {
26031
+ tokens = Math.min(capacity, tokens + Math.max(0, now - updatedAt) * (refillPerSecond / 1e3));
26032
+ }
26033
+ updatedAt = now;
26034
+ if (tokens < 1) {
26035
+ return false;
26036
+ }
26037
+ tokens -= 1;
26038
+ return true;
26039
+ }, "take")
26040
+ };
26041
+ }, "createTokenBucket");
26042
+
25971
26043
  // src/id-graph/anon/websocket.ts
25972
26044
  var MAX_BROWSER_RESULT_BYTES = 1024;
25973
26045
  var MAX_BROWSER_RESULTS_BYTES = 32 * 1024;
26046
+ var MAX_FRAME_BYTES = 64 * 1024;
25974
26047
  var browserContextFrame = external_exports.object({
25975
26048
  occurredAt: external_exports.number().int().nonnegative().safe(),
25976
26049
  page: external_exports.object({ url: external_exports.string().min(1).max(4096), referrer: external_exports.string().min(1).max(4096).optional() }).strict(),
@@ -26043,6 +26116,10 @@ var pluckRef = /* @__PURE__ */ __name((parsed) => {
26043
26116
  return typeof ref === "string" && ref.length > 0 ? ref : void 0;
26044
26117
  }, "pluckRef");
26045
26118
  var parseFrame = /* @__PURE__ */ __name((message) => {
26119
+ const size = typeof message === "string" ? message.length : message.byteLength;
26120
+ if (size > MAX_FRAME_BYTES) {
26121
+ return { ok: false, ref: void 0, error: new Error(`frame exceeds ${MAX_FRAME_BYTES} bytes`) };
26122
+ }
26046
26123
  const text = typeof message === "string" ? message : new TextDecoder().decode(message);
26047
26124
  let parsed;
26048
26125
  try {
@@ -26141,12 +26218,19 @@ var siteAudience = /* @__PURE__ */ __name(({ registry: registry3, app: app2, top
26141
26218
  const allowed = app2.events.publishes.includes(topic) && app2.data.includes(SHARED_CONTEXT_CAPABILITY);
26142
26219
  return allowed ? siteEventReceivers(registry3, consent2, enforced) : null;
26143
26220
  }, "siteAudience");
26221
+ var FRAME_BURST = 60;
26222
+ var FRAMES_PER_SECOND = 10;
26223
+ var EMIT_BURST = 120;
26224
+ var EMITS_PER_SECOND = 5;
26144
26225
  var AnonDO = class extends DurableObject2 {
26145
26226
  static {
26146
26227
  __name(this, "AnonDO");
26147
26228
  }
26148
26229
  // Serialize link decisions so concurrent first identifiers cannot split the graph.
26149
26230
  ingestQueue = Promise.resolve();
26231
+ // In memory on purpose: a hibernated socket sent nothing, so a fresh bucket on wake is the right one.
26232
+ frameBuckets = /* @__PURE__ */ new WeakMap();
26233
+ emitBudget = createTokenBucket(EMIT_BURST, EMITS_PER_SECOND);
26150
26234
  // deleteAll removes the live schema; only a later write recreates it.
26151
26235
  wiped = false;
26152
26236
  // Warm write-through mirrors reset together after expiry.
@@ -26205,9 +26289,50 @@ var AnonDO = class extends DurableObject2 {
26205
26289
  ...action.type === "track" ? { "action.event": action.payload.event } : {}
26206
26290
  };
26207
26291
  return runInvocation({ name: "do.anon.ingest", trace, attrs }, () => {
26292
+ if (action.origin !== void 0 && !this.emitBudget.take(Date.now())) {
26293
+ addSpanEvent("emit rejected: the visitor\u2019s emit budget is spent", {
26294
+ level: "warn",
26295
+ attrs: { app: action.origin.app, depth: action.origin.depth }
26296
+ });
26297
+ return Promise.resolve();
26298
+ }
26208
26299
  return this.ingestLocal(action);
26209
26300
  });
26210
26301
  }
26302
+ // The frames that ask instead of act are answered in place; an action frame comes back to be queued.
26303
+ async answerQuery(ws, parsed, userId, attachment) {
26304
+ const { frame, ref } = parsed;
26305
+ if (frame.t === "getSession") {
26306
+ this.respond(ws, {
26307
+ t: "session",
26308
+ ref,
26309
+ anonId: userId,
26310
+ globalId: linkedGlobalId(this.kv),
26311
+ ip: attachment?.host?.ip ?? null,
26312
+ geo: attachment?.browserGeo ?? null
26313
+ });
26314
+ return null;
26315
+ }
26316
+ if (frame.t === "capture") {
26317
+ const { accepted } = await this.capture({ apps: frame.apps });
26318
+ this.respond(ws, { t: "captureAck", ref, anonId: userId, accepted });
26319
+ return null;
26320
+ }
26321
+ return frame;
26322
+ }
26323
+ admitFrame(ws) {
26324
+ let bucket = this.frameBuckets.get(ws);
26325
+ if (bucket === void 0) {
26326
+ bucket = createTokenBucket(FRAME_BURST, FRAMES_PER_SECOND);
26327
+ this.frameBuckets.set(ws, bucket);
26328
+ }
26329
+ if (bucket.take(Date.now())) {
26330
+ return true;
26331
+ }
26332
+ addSpanEvent("websocket frame rate limited", { level: "warn" });
26333
+ this.respond(ws, { t: "error", ref: void 0, message: "rate limited" });
26334
+ return false;
26335
+ }
26211
26336
  ingestLocal(action) {
26212
26337
  return this.enqueue(() => this.processAction(action));
26213
26338
  }
@@ -26230,6 +26355,9 @@ var AnonDO = class extends DurableObject2 {
26230
26355
  async webSocketMessage(ws, message) {
26231
26356
  const attachment = ws.deserializeAttachment() ?? void 0;
26232
26357
  return runInvocation({ name: "ws.message", attrs: attachment?.attrs }, async () => {
26358
+ if (!this.admitFrame(ws)) {
26359
+ return;
26360
+ }
26233
26361
  const parsed = parseFrame(message);
26234
26362
  if (!parsed.ok) {
26235
26363
  addSpanEvent("invalid websocket frame", { level: "warn", attrs: { error: String(parsed.error) } });
@@ -26247,23 +26375,11 @@ var AnonDO = class extends DurableObject2 {
26247
26375
  this.respond(ws, { t: "error", ref: parsed.ref, message: "session not initialized" });
26248
26376
  return;
26249
26377
  }
26250
- if (parsed.frame.t === "getSession") {
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
- });
26259
- return;
26260
- }
26261
- if (parsed.frame.t === "capture") {
26262
- const { accepted } = await this.capture({ apps: parsed.frame.apps });
26263
- this.respond(ws, { t: "captureAck", ref: parsed.ref, anonId: userId, accepted });
26378
+ const frame = await this.answerQuery(ws, { frame: parsed.frame, ref: parsed.ref }, userId, attachment);
26379
+ if (frame === null) {
26264
26380
  return;
26265
26381
  }
26266
- const action = actionFromFrame(parsed.frame, userId, attachment?.cf, attachment?.host);
26382
+ const action = actionFromFrame(frame, userId, attachment?.cf, attachment?.host);
26267
26383
  const ref = parsed.ref ?? action.actionId;
26268
26384
  setSpanAttributes({ "action.id": action.actionId });
26269
26385
  try {
@@ -27189,6 +27305,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_facts_key ON facts (kind, scope, key);
27189
27305
  ];
27190
27306
 
27191
27307
  // src/id-graph/global/do.ts
27308
+ var SIBLING_FAN_OUT_CONCURRENCY = 6;
27192
27309
  var GlobalDO = class extends DurableObject3 {
27193
27310
  static {
27194
27311
  __name(this, "GlobalDO");
@@ -27290,8 +27407,9 @@ var GlobalDO = class extends DurableObject3 {
27290
27407
  }
27291
27408
  await runInvocation({ name: "do.global.fan_out", trace, attrs: { "global.id": globalId } }, async () => {
27292
27409
  const snapshot = this.snapshot(globalId);
27293
- await Promise.all(
27294
- recipients.map(async (anonId) => {
27410
+ await runPool(
27411
+ SIBLING_FAN_OUT_CONCURRENCY,
27412
+ recipients.map((anonId) => async () => {
27295
27413
  try {
27296
27414
  await traced("rpc anon.sync_from_global", { attrs: { "anon.id": anonId } }, () => {
27297
27415
  return anonStub(this.env, anonId).syncFromGlobal(snapshot, currentTraceContext());
@@ -27314,8 +27432,10 @@ app.use("/q.js", cors());
27314
27432
  app.get("/q.js", qJs);
27315
27433
  app.use("/health", cors());
27316
27434
  app.route("/", health);
27435
+ var MAX_V1_BODY_BYTES = 1024 * 1024;
27317
27436
  var allowCors = cors({ origin: /* @__PURE__ */ __name((origin) => origin, "origin"), credentials: true });
27318
27437
  app.use("/v1/*", allowCors);
27438
+ app.use("/v1/*", bodyLimit({ maxSize: MAX_V1_BODY_BYTES, onError: /* @__PURE__ */ __name((c) => c.json({ error: "too_large" }, 413), "onError") }));
27319
27439
  app.route("/", alias);
27320
27440
  app.route("/", identify);
27321
27441
  app.route("/", track);
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "main": {
3
3
  "version": "0.1.0",
4
- "gitSha": "eccf5ef7a3b8ef9131bc38d28b66cc5a3add163b",
5
- "builtAt": "2026-08-29T05:28:12.232Z",
4
+ "gitSha": "539e97d61a67ca9c5c6580b94350354e917fcf99",
5
+ "builtAt": "2026-08-31T10:11:02.141Z",
6
6
  "compatibilityDate": "2026-06-23",
7
7
  "compatibilityFlags": [
8
8
  "nodejs_compat"
@@ -23,8 +23,8 @@
23
23
  },
24
24
  "wfp": {
25
25
  "version": "0.1.0",
26
- "gitSha": "c71e3d19431cd96a0c9f43eccabaa36d021401f7",
27
- "builtAt": "2026-08-24T10:38:20.398Z",
26
+ "gitSha": "6064f0fb074965ec6add05255c92ece93bb31321",
27
+ "builtAt": "2026-08-31T12:30:46.702Z",
28
28
  "compatibilityDate": "2026-06-23",
29
29
  "compatibilityFlags": [
30
30
  "nodejs_compat"
@@ -40,8 +40,8 @@
40
40
  },
41
41
  "app-runtime": {
42
42
  "version": "0.1.0",
43
- "gitSha": "eccf5ef7a3b8ef9131bc38d28b66cc5a3add163b",
44
- "builtAt": "2026-08-29T05:28:14.997Z",
43
+ "gitSha": "83bcac7072f2feb866505c4a7b99f7281b08d972",
44
+ "builtAt": "2026-08-29T21:13:19.154Z",
45
45
  "compatibilityDate": "2026-06-23",
46
46
  "compatibilityFlags": [
47
47
  "nodejs_compat"
@@ -2158,10 +2158,9 @@ var EDGETAG_PREFIXES = [
2158
2158
 
2159
2159
  // src/allowed-paths.ts
2160
2160
  var QINTO_PREFIXES = ["/open", "/q.js", "/health", "/v1/", "/openapi.json", "/apps/", "/app/"];
2161
- var QINTO_EXACT = /* @__PURE__ */ new Set(["/__qinto/control"]);
2162
2161
  var ALLOWED_PREFIXES = [...QINTO_PREFIXES, ...EDGETAG_PREFIXES];
2163
2162
  var allowedPaths = /* @__PURE__ */ __name((path) => {
2164
- return path === "/" || QINTO_EXACT.has(path) || ALLOWED_PREFIXES.some((prefix) => path.startsWith(prefix));
2163
+ return path === "/" || ALLOWED_PREFIXES.some((prefix) => path.startsWith(prefix));
2165
2164
  }, "allowedPaths");
2166
2165
 
2167
2166
  // ../../libs/wfp-control-token/src/control-token.ts
@@ -2244,6 +2243,21 @@ var requireControlToken = /* @__PURE__ */ __name(async (c, next) => {
2244
2243
  return next();
2245
2244
  }, "requireControlToken");
2246
2245
 
2246
+ // src/control/app.ts
2247
+ var forwardAppControl = /* @__PURE__ */ __name(async (c) => {
2248
+ const workerName = c.req.query("workerName");
2249
+ if (workerName === void 0 || workerName === "") {
2250
+ return c.text("workerName required", 400);
2251
+ }
2252
+ const url = new URL(c.req.url);
2253
+ url.pathname = "/__qinto/control";
2254
+ url.search = "";
2255
+ const headers = new Headers(c.req.raw.headers);
2256
+ headers.delete("authorization");
2257
+ const body = await c.req.raw.text();
2258
+ return c.env.DISPATCHER.get(workerName).fetch(new Request(url, { method: "POST", headers, body }));
2259
+ }, "forwardAppControl");
2260
+
2247
2261
  // src/control/id-graph.ts
2248
2262
  var INSPECT_AUTHORIZATION_HEADER = "x-qinto-inspect-authorization";
2249
2263
  var ACTIONS = /* @__PURE__ */ new Set(["resolve", "inspect", "rows"]);
@@ -2683,6 +2697,7 @@ var Tail = class extends DurableObject {
2683
2697
  // src/index.ts
2684
2698
  var app = new Hono2();
2685
2699
  app.get("/tail", requireControlToken, openTailSocket);
2700
+ app.post("/control/app", requireControlToken, forwardAppControl);
2686
2701
  app.post("/control/id-graph/:action", requireControlToken, forwardIdGraph);
2687
2702
  app.all("*", (c) => {
2688
2703
  if (!allowedPaths(c.req.path)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blotout/qinto-cli",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "type": "module",
5
5
  "description": "Qinto CLI: authenticate against the Qinto platform and operate it from the terminal or from an agent.",
6
6
  "repository": {
@@ -40,9 +40,9 @@
40
40
  "typescript": "6.0.3",
41
41
  "vite": "^8.0.0",
42
42
  "vitest": "4.1.10",
43
- "@qinto/app-build": "0.0.0",
44
- "@qinto/app-dev": "0.0.0",
45
43
  "@qinto/app-manifest": "0.0.0",
44
+ "@qinto/app-dev": "0.0.0",
45
+ "@qinto/app-build": "0.0.0",
46
46
  "@qinto/oauth-grammar": "0.0.0"
47
47
  },
48
48
  "scripts": {