@blotout/qinto-cli 0.1.20 → 0.1.22

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.
@@ -790,6 +790,10 @@ var deriveVisitorSignature = /* @__PURE__ */ __name(async (masterBase64, domainI
790
790
  return hmacHex(masterBase64, `${domainId}#visitor#${anonId}`);
791
791
  }, "deriveVisitorSignature");
792
792
 
793
+ // ../../libs/app-manifest/src/app-roles.ts
794
+ var DECLARABLE_APP_ROLES = ["owner", "admin", "manager", "editor", "member", "viewer"];
795
+ var APP_ROLES = ["full", ...DECLARABLE_APP_ROLES];
796
+
793
797
  // ../../libs/app-manifest/src/cron.ts
794
798
  var MAX_CRON_LENGTH = 128;
795
799
  var MINUTE_LITERAL = /^([0-9]|[1-5][0-9])$/;
@@ -20348,6 +20352,10 @@ var VARIABLE_NAME = /^[A-Z][A-Z0-9_]*$/;
20348
20352
  var APP_SLUG_PATTERN = /^[a-z][a-z0-9-]{2,29}$/;
20349
20353
  var FUNCTION_NAME = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
20350
20354
  var identifierField = external_exports.string().min(1).max(64).regex(IDENTIFIER);
20355
+ var RENDERABLE_TEXT = /^[^\u0000-\u0009\u000B-\u001F\u007F\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF]*$/;
20356
+ var renderableText = /* @__PURE__ */ __name((min, max) => {
20357
+ return external_exports.string().min(min).max(max).regex(RENDERABLE_TEXT, "must not carry control or bidirectional-override characters");
20358
+ }, "renderableText");
20351
20359
  var httpsUrl = /* @__PURE__ */ __name((field) => {
20352
20360
  return external_exports.url().max(2048).refine((value) => value.startsWith("https://"), `${field} must be an HTTPS URL`);
20353
20361
  }, "httpsUrl");
@@ -20483,6 +20491,35 @@ var validateOauth = /* @__PURE__ */ __name((input2, ctx) => {
20483
20491
  }
20484
20492
  }, "validateOauth");
20485
20493
 
20494
+ // ../../libs/app-manifest/src/roles.ts
20495
+ var MAX_APP_ROLE_DESCRIPTION = 200;
20496
+ var roleSchema = external_exports.object({
20497
+ key: external_exports.enum(DECLARABLE_APP_ROLES, {
20498
+ error: /* @__PURE__ */ __name((issue2) => {
20499
+ return issue2.input === "full" ? "full access is always available and is not declared" : void 0;
20500
+ }, "error")
20501
+ }),
20502
+ description: renderableText(1, MAX_APP_ROLE_DESCRIPTION).optional()
20503
+ }).strict();
20504
+ var rolesSchema = external_exports.array(roleSchema).max(DECLARABLE_APP_ROLES.length).superRefine((roles, ctx) => {
20505
+ uniqueOrIssue(
20506
+ ctx,
20507
+ roles.map((role) => role.key),
20508
+ [],
20509
+ "role"
20510
+ );
20511
+ }).transform((roles) => {
20512
+ return [...roles].sort((a, b) => APP_ROLES.indexOf(a.key) - APP_ROLES.indexOf(b.key));
20513
+ });
20514
+ var validateRoles = /* @__PURE__ */ __name((manifest, ctx) => {
20515
+ const declared = /* @__PURE__ */ new Set(["full", ...(manifest.roles ?? []).map((role) => role.key)]);
20516
+ for (const [index, tool] of manifest.tools.entries()) {
20517
+ if (tool.role !== void 0 && !declared.has(tool.role)) {
20518
+ ctx.addIssue({ code: "custom", path: ["tools", index, "role"], message: `Tool role ${tool.role} is not declared in roles` });
20519
+ }
20520
+ }
20521
+ }, "validateRoles");
20522
+
20486
20523
  // ../../libs/app-manifest/src/route.ts
20487
20524
  var ENDPOINT_PATH_PATTERN = /^\/(?:(?::[a-zA-Z][a-zA-Z0-9_]*|[a-zA-Z0-9_-]+)(?:\/(?::[a-zA-Z][a-zA-Z0-9_]*|[a-zA-Z0-9_-]+))*)?$/;
20488
20525
  var isParam = /* @__PURE__ */ __name((segment) => {
@@ -20579,10 +20616,6 @@ var MAX_MANIFEST_TOOLS_BYTES = 150 * 1024;
20579
20616
  var TOOL_PROPERTY_NAME = /^[a-zA-Z][a-zA-Z0-9_]*$/;
20580
20617
  var RESERVED_TOOL_ARGUMENTS = ["teamId", "siteId", "installId"];
20581
20618
  var TOOL_STRING_FORMATS = ["uuid", "date", "date-time", "email", "uri"];
20582
- var RENDERABLE_TEXT = /^[^\u0000-\u0009\u000B-\u001F\u007F\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF]*$/;
20583
- var renderableText = /* @__PURE__ */ __name((min, max) => {
20584
- return external_exports.string().min(min).max(max).regex(RENDERABLE_TEXT, "must not carry control or bidirectional-override characters");
20585
- }, "renderableText");
20586
20619
  var propertyName = external_exports.string().min(1).max(48).regex(TOOL_PROPERTY_NAME);
20587
20620
  var FORMAT_SCHEMAS = {
20588
20621
  uuid: external_exports.uuid(),
@@ -20678,7 +20711,8 @@ var toolSchema = external_exports.object({
20678
20711
  title: renderableText(1, MAX_APP_TOOL_TITLE_LENGTH),
20679
20712
  description: renderableText(MIN_APP_TOOL_DESCRIPTION, MAX_APP_TOOL_DESCRIPTION),
20680
20713
  inputSchema: toolInputSchema,
20681
- annotations: toolAnnotationsSchema
20714
+ annotations: toolAnnotationsSchema,
20715
+ role: external_exports.enum(APP_ROLES).optional()
20682
20716
  }).strict();
20683
20717
  var nodeChildren = /* @__PURE__ */ __name((value) => {
20684
20718
  const properties2 = value["properties"];
@@ -21082,6 +21116,9 @@ var baseSchema = external_exports.object({
21082
21116
  description: "The push topics this app broadcasts to every app on the site. Requires the `sharedContext` data capability; a topic left out reaches only this app\u2019s own browser code."
21083
21117
  }),
21084
21118
  endpoints: external_exports.array(endpointSchema).max(16).default([]).meta({ description: "The HTTP routes served at https://<site>/app/<slug><path>." }),
21119
+ roles: rolesSchema.optional().meta({
21120
+ description: "The app roles this app uses, from the fixed list owner, admin, manager, editor, member, viewer. Full access is always available and is not declared. A team member is given one app role per app."
21121
+ }),
21085
21122
  tools: toolsSchema.meta({
21086
21123
  description: "The MCP tools this app serves to AI assistants, published as `<slug>_<name>`. Each one a site manager accepts becomes callable on their behalf by an assistant."
21087
21124
  }),
@@ -21438,6 +21475,7 @@ var appManifestSchema = baseSchema.superRefine((manifest, ctx) => {
21438
21475
  validateOauth(manifest, ctx);
21439
21476
  validateServer(manifest, ctx);
21440
21477
  validateTools(manifest.tools, ctx);
21478
+ validateRoles(manifest, ctx);
21441
21479
  });
21442
21480
 
21443
21481
  // ../../libs/app-manifest/src/oauth-flow.ts
@@ -21500,9 +21538,10 @@ var effectGroupSchema = external_exports.object({
21500
21538
  context: external_exports.array(contextWriteSchema).optional()
21501
21539
  }).strict();
21502
21540
  var captureValueSchema = external_exports.object({ value: external_exports.string().max(512), createdAt: external_exports.number().int(), updatedAt: external_exports.number().int(), type: external_exports.enum(CAPTURE_TYPES) }).strict();
21541
+ var pageSchema = external_exports.object({ url: external_exports.string().min(1).max(4096), referrer: external_exports.string().min(1).max(4096).optional() }).strict();
21503
21542
  var actionContextFields = {
21504
21543
  occurredAt: external_exports.number().int().nonnegative().safe(),
21505
- page: external_exports.object({ url: external_exports.string().min(1).max(4096), referrer: external_exports.string().min(1).max(4096).optional() }).strict().optional(),
21544
+ page: pageSchema.optional(),
21506
21545
  network: external_exports.object({
21507
21546
  ip: external_exports.string().min(1).max(64).optional(),
21508
21547
  userAgent: external_exports.string().min(1).max(512).optional(),
@@ -21530,6 +21569,8 @@ var envelopeRequestSchema = external_exports.object({
21530
21569
  }).strict().optional(),
21531
21570
  consent: external_exports.object({ enabled: external_exports.boolean(), categories: external_exports.record(external_exports.string(), external_exports.boolean()), apps: external_exports.record(external_exports.string(), external_exports.boolean()) }).strict().optional(),
21532
21571
  geo: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
21572
+ // Connect envelopes only: the page the visitor's socket opened from.
21573
+ page: pageSchema.optional(),
21533
21574
  // Only what this app captured or wrote for this visitor; `updatedAt` is when the current value
21534
21575
  // was first seen, which is the click time a CAPI `fbc` is built from.
21535
21576
  captures: external_exports.record(external_exports.string().max(64), captureValueSchema).optional(),
@@ -21705,7 +21746,12 @@ var toolSchema2 = external_exports.object({
21705
21746
  name: external_exports.string().min(1).max(MAX_APP_TOOL_NAME_LENGTH).regex(APP_TOOL_NAME_PATTERN),
21706
21747
  // Who asked, and what they may do on this site. Derived from the bearer by the portal, never
21707
21748
  // from the model's arguments.
21708
- viewer: external_exports.object({ userId: external_exports.string().min(1).max(64), access: external_exports.enum(["read", "write"]) }).strict(),
21749
+ viewer: external_exports.object({
21750
+ userId: external_exports.string().min(1).max(64),
21751
+ access: external_exports.enum(["read", "write"]),
21752
+ // Optional only while a site worker or app runtime that predates it can still receive this envelope; absent means `full`.
21753
+ appRole: external_exports.enum(APP_ROLES).optional()
21754
+ }).strict(),
21709
21755
  // Already validated against the tool's declared inputSchema, with the reserved routing names
21710
21756
  // stripped. Every declared input schema has an object root, so this is an object too — and a
21711
21757
  // call that names no arguments normalizes to `{}` rather than reaching the handler as undefined.
@@ -21911,7 +21957,12 @@ var MAX_UI_BODY_BYTES = 1024 * 1024;
21911
21957
  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();
21912
21958
  var uiContextSchema = external_exports.object({
21913
21959
  version: external_exports.string().min(1).max(32),
21914
- viewer: external_exports.object({ userId: external_exports.string().min(1).max(64), access: external_exports.enum(["read", "write"]) }).strict(),
21960
+ viewer: external_exports.object({
21961
+ userId: external_exports.string().min(1).max(64),
21962
+ access: external_exports.enum(["read", "write"]),
21963
+ // Optional only while a site worker that predates it can still send this header; absent means `full`.
21964
+ appRole: external_exports.enum(APP_ROLES).optional()
21965
+ }).strict(),
21915
21966
  install: uiInstallSchema,
21916
21967
  installs: external_exports.array(uiInstallSchema).min(1).max(MAX_INSTALLS_PER_APP),
21917
21968
  writeTicket: external_exports.string().min(1).max(2048).optional()
@@ -26305,14 +26356,15 @@ var captureAppsField = external_exports.record(external_exports.string().max(30)
26305
26356
  description: "Values to record, keyed by app slug and then by capture name. A name the app does not declare is dropped.",
26306
26357
  example: { facebook: { fbclid: "IwAR0abc" }, "google-ads": { gclid: "Cj0KCQ" } }
26307
26358
  });
26359
+ var pageContextField = external_exports.object({
26360
+ url: external_exports.string().min(1).max(4096),
26361
+ referrer: external_exports.string().min(1).max(4096).optional()
26362
+ }).strict();
26308
26363
  var serverActionContextField = external_exports.object({
26309
26364
  occurredAt: external_exports.number().int().nonnegative().safe().optional().openapi({
26310
26365
  description: "When the visitor action occurred, as Unix milliseconds; defaults to Qinto receipt time."
26311
26366
  }),
26312
- page: external_exports.object({
26313
- url: external_exports.string().min(1).max(4096),
26314
- referrer: external_exports.string().min(1).max(4096).optional()
26315
- }).strict().optional(),
26367
+ page: pageContextField.optional(),
26316
26368
  network: external_exports.object({
26317
26369
  ip: external_exports.string().min(1).max(64).optional(),
26318
26370
  userAgent: external_exports.string().min(1).max(512).optional(),
@@ -27718,6 +27770,7 @@ var buildEnvelope = /* @__PURE__ */ __name((target, trigger, snapshot, context)
27718
27770
  },
27719
27771
  ...buckets.includes("consentState") ? { consent: { enabled: context.consentEnforced, categories: snapshot.consent.categories, apps: snapshot.consent.apps } } : {},
27720
27772
  ...geoAllowed && currentGeo !== void 0 ? { geo: currentGeo } : {},
27773
+ ...trigger.kind === "connect" && trigger.page !== void 0 && buckets.includes("eventContext") ? { page: trigger.page } : {},
27721
27774
  // Not gated on a capabilities.data bucket: an app only ever sees what it captured or wrote.
27722
27775
  ...captures === void 0 ? {} : { captures },
27723
27776
  ...visitorContext === void 0 ? {} : { context: visitorContext },
@@ -29733,7 +29786,9 @@ var claimsSchema = external_exports.object({
29733
29786
  appId: external_exports.string().min(1),
29734
29787
  appInstallId: external_exports.string().min(1),
29735
29788
  appVersionId: external_exports.string().min(1),
29736
- access: external_exports.enum(["read", "write"])
29789
+ access: external_exports.enum(["read", "write"]),
29790
+ // Absent means `full`; see the mint below.
29791
+ appRole: external_exports.enum(APP_ROLES).optional()
29737
29792
  }).strict();
29738
29793
  var appUiAudience = /* @__PURE__ */ __name((slug) => {
29739
29794
  return `app-ui:${slug}`;
@@ -29902,7 +29957,8 @@ var forwardToApp2 = /* @__PURE__ */ __name(async (c, input2) => {
29902
29957
  "app.install_ord": install.ord,
29903
29958
  "app.install_label": install.label,
29904
29959
  "app.viewer_id": viewer.userId,
29905
- "app.viewer_access": viewer.access
29960
+ "app.viewer_access": viewer.access,
29961
+ "app.viewer_app_role": viewer.appRole
29906
29962
  }
29907
29963
  },
29908
29964
  () => {
@@ -29948,7 +30004,13 @@ var authorizeUi = /* @__PURE__ */ __name(async (c, slug, origin) => {
29948
30004
  appId: verdict.claims.appId,
29949
30005
  appVersionId: verdict.claims.appVersionId
29950
30006
  }) : void 0;
29951
- return { app: app2, install, viewer: { userId: verdict.claims.userId, access: verdict.claims.access }, writeTicket };
30007
+ return {
30008
+ app: app2,
30009
+ install,
30010
+ // The portal mints `full` as no claim, so a site worker that predates the claim still accepts the token.
30011
+ viewer: { userId: verdict.claims.userId, access: verdict.claims.access, appRole: verdict.claims.appRole ?? "full" },
30012
+ writeTicket
30013
+ };
29952
30014
  }, "authorizeUi");
29953
30015
  var handleAppUi = /* @__PURE__ */ __name(async (c, slug) => {
29954
30016
  const host = c.env.APP_UI_HOST;
@@ -30087,9 +30149,141 @@ var defaultHook = /* @__PURE__ */ __name((result, c) => {
30087
30149
  }, "defaultHook");
30088
30150
  var createApiRouter = /* @__PURE__ */ __name(() => new OpenAPIHono({ defaultHook }), "createApiRouter");
30089
30151
 
30152
+ // src/id-graph/anon/connection.ts
30153
+ var CONNECTION_CF_HEADER = "x-connection-cf";
30154
+ var SESSION_HEADER = "x-session";
30155
+ var OPEN_PAGE_PARAMS = ["url", "referrer"];
30156
+ var MAX_HOST_USER_AGENT_LENGTH = 512;
30157
+ var MAX_HOST_LOCALE_LENGTH = 64;
30158
+ var INTERNAL_HEADERS = /* @__PURE__ */ new Set([
30159
+ "x-user-id",
30160
+ "x-new-user",
30161
+ "x-geo",
30162
+ SITE_ID_HEADER,
30163
+ CONNECTION_CF_HEADER,
30164
+ SESSION_HEADER,
30165
+ TRACE_HEADER,
30166
+ CF_DATA_HEADER
30167
+ ]);
30168
+ var REDACTED_HEADERS = /* @__PURE__ */ new Set(["authorization"]);
30169
+ var encodeConnectionCf = /* @__PURE__ */ __name((cf) => {
30170
+ if (cf === void 0) {
30171
+ return void 0;
30172
+ }
30173
+ const encoded = headerJson(cf);
30174
+ return encoded.length > MAX_ATTACHMENT_BYTES ? void 0 : encoded;
30175
+ }, "encodeConnectionCf");
30176
+ var forwardedCf = /* @__PURE__ */ __name((request) => {
30177
+ const raw2 = request.headers.get(CONNECTION_CF_HEADER);
30178
+ if (raw2 === null) {
30179
+ return void 0;
30180
+ }
30181
+ return JSON.parse(raw2);
30182
+ }, "forwardedCf");
30183
+ var forwardedSession = /* @__PURE__ */ __name((request) => {
30184
+ const raw2 = request.headers.get(SESSION_HEADER);
30185
+ if (raw2 === null) {
30186
+ return void 0;
30187
+ }
30188
+ return JSON.parse(raw2);
30189
+ }, "forwardedSession");
30190
+ var withoutPageParams = /* @__PURE__ */ __name((url2) => {
30191
+ const parsed = new URL(url2);
30192
+ for (const param of OPEN_PAGE_PARAMS) {
30193
+ parsed.searchParams.delete(param);
30194
+ }
30195
+ return parsed.toString();
30196
+ }, "withoutPageParams");
30197
+ var connectPage = /* @__PURE__ */ __name((request) => {
30198
+ const params = new URL(request.url).searchParams;
30199
+ const url2 = params.get("url");
30200
+ if (url2 === null) {
30201
+ return void 0;
30202
+ }
30203
+ const referrer = params.get("referrer");
30204
+ const parsed = pageContextField.safeParse({ url: url2, ...referrer === null ? {} : { referrer } });
30205
+ return parsed.success ? parsed.data : void 0;
30206
+ }, "connectPage");
30207
+ var connectAttrs = /* @__PURE__ */ __name((request, geo) => {
30208
+ const headers = {};
30209
+ for (const [name, value] of request.headers) {
30210
+ const lower = name.toLowerCase();
30211
+ if (!INTERNAL_HEADERS.has(lower) && !REDACTED_HEADERS.has(lower)) {
30212
+ headers[name] = value;
30213
+ }
30214
+ }
30215
+ const cf = forwardedCf(request);
30216
+ return {
30217
+ "url.full": withoutPageParams(request.url),
30218
+ "http.request.headers": JSON.stringify(headers),
30219
+ ...cf === void 0 ? {} : { "qinto.cf": JSON.stringify(cf) },
30220
+ ...geo === void 0 ? {} : { "qinto.geo": JSON.stringify(loggableGeo(geo)) }
30221
+ };
30222
+ }, "connectAttrs");
30223
+ var withoutQuery = /* @__PURE__ */ __name((url2) => {
30224
+ const parsed = new URL(url2);
30225
+ parsed.search = "";
30226
+ return parsed.toString();
30227
+ }, "withoutQuery");
30228
+ var browserGeoFrom = /* @__PURE__ */ __name((geo) => {
30229
+ if (geo === void 0) {
30230
+ return void 0;
30231
+ }
30232
+ return {
30233
+ country: geo.country ?? null,
30234
+ // EdgeTag's geoRegion is Cloudflare's subdivision code.
30235
+ region: geo.regionCode ?? null,
30236
+ state: geo.region ?? null,
30237
+ isEU: geo.isEUCountry === "yes"
30238
+ };
30239
+ }, "browserGeoFrom");
30240
+ var attachmentFieldBytes = /* @__PURE__ */ __name((name, value) => {
30241
+ return value === void 0 ? 0 : new TextEncoder().encode(`,"${name}":${JSON.stringify(value)}`).length;
30242
+ }, "attachmentFieldBytes");
30243
+ var boundedHost = /* @__PURE__ */ __name((request) => {
30244
+ const raw2 = hostFromRequest(request);
30245
+ const bounded = {
30246
+ ip: raw2.ip,
30247
+ userAgent: raw2.userAgent !== void 0 && raw2.userAgent.length <= MAX_HOST_USER_AGENT_LENGTH ? raw2.userAgent : void 0,
30248
+ locale: raw2.locale !== void 0 && raw2.locale.length <= MAX_HOST_LOCALE_LENGTH ? raw2.locale : void 0
30249
+ };
30250
+ return bounded.ip === void 0 && bounded.userAgent === void 0 && bounded.locale === void 0 ? void 0 : bounded;
30251
+ }, "boundedHost");
30252
+ var connectionAttachment = /* @__PURE__ */ __name((request, geo, siteId) => {
30253
+ const userAgent = request.headers.get("user-agent");
30254
+ const attrs = {
30255
+ "url.full": withoutQuery(request.url),
30256
+ ...geo?.country === void 0 ? {} : { "qinto.country": geo.country },
30257
+ ...geo?.city === void 0 ? {} : { "qinto.city": geo.city },
30258
+ ...userAgent === null ? {} : { user_agent: userAgent }
30259
+ };
30260
+ const host = boundedHost(request);
30261
+ const hostBytes = attachmentFieldBytes("host", host);
30262
+ const browserGeo = browserGeoFrom(geo);
30263
+ const browserGeoBytes = attachmentFieldBytes("browserGeo", browserGeo);
30264
+ const session2 = forwardedSession(request);
30265
+ const sessionBytes = attachmentFieldBytes("session", session2);
30266
+ const siteBytes = attachmentFieldBytes("siteId", siteId);
30267
+ const cf = forwardedCf(request);
30268
+ const cfSuffix = cf === void 0 ? "" : `,"cf":${JSON.stringify(cf)}`;
30269
+ const cfBytes = new TextEncoder().encode(cfSuffix).length;
30270
+ const emptyAttrsBytes = new TextEncoder().encode(JSON.stringify({ attrs: {} })).length;
30271
+ const fixedBytes = siteBytes + sessionBytes + hostBytes + browserGeoBytes;
30272
+ const includedCf = cfBytes <= MAX_ATTACHMENT_BYTES - emptyAttrsBytes - fixedBytes ? cf : void 0;
30273
+ const budgeted = budgetAttrs(attrs, MAX_ATTACHMENT_BYTES - fixedBytes - (includedCf === void 0 ? 0 : cfBytes));
30274
+ return {
30275
+ ...budgeted,
30276
+ siteId,
30277
+ ...session2 === void 0 ? {} : { session: session2 },
30278
+ ...browserGeo === void 0 ? {} : { browserGeo },
30279
+ ...includedCf === void 0 ? {} : { cf: includedCf },
30280
+ ...host === void 0 ? {} : { host }
30281
+ };
30282
+ }, "connectionAttachment");
30283
+
30090
30284
  // src/helpers/request-span.ts
30091
30285
  var OMITTED_HEADERS = /* @__PURE__ */ new Set(["authorization", DISPATCH_HEADER, CF_DATA_HEADER]);
30092
- var requestAttrs = /* @__PURE__ */ __name((request) => {
30286
+ var requestAttrs = /* @__PURE__ */ __name((request, pathname) => {
30093
30287
  const headers = {};
30094
30288
  for (const [name, value] of request.headers) {
30095
30289
  if (!OMITTED_HEADERS.has(name.toLowerCase())) {
@@ -30099,7 +30293,7 @@ var requestAttrs = /* @__PURE__ */ __name((request) => {
30099
30293
  const cf = request.cf;
30100
30294
  return {
30101
30295
  "http.request.method": request.method,
30102
- "url.full": request.url,
30296
+ "url.full": pathname === "/open" ? withoutPageParams(request.url) : request.url,
30103
30297
  "http.request.headers": JSON.stringify(headers),
30104
30298
  ...cf === void 0 ? {} : { "qinto.cf": JSON.stringify(cf) }
30105
30299
  };
@@ -30114,7 +30308,7 @@ var requestSpan = /* @__PURE__ */ __name(async (c, next) => {
30114
30308
  const pathname = new URL(c.req.url).pathname;
30115
30309
  const dispatch = parseDispatchTrace(c.req.raw.headers.get(DISPATCH_HEADER));
30116
30310
  const trace = dispatch === void 0 ? inboundTrace(c.req.raw, pathname) : { traceId: dispatch.traceId, parent: dispatch.spanId };
30117
- await runInvocation({ name: `${c.req.method} ${pathname}`, trace, attrs: requestAttrs(c.req.raw) }, async () => {
30311
+ await runInvocation({ name: `${c.req.method} ${pathname}`, trace, attrs: requestAttrs(c.req.raw, pathname) }, async () => {
30118
30312
  try {
30119
30313
  await next();
30120
30314
  setSpanAttributes({ "http.response.status_code": c.res.status });
@@ -30321,120 +30515,6 @@ idGraphInspect.post("/internal/id-graph/rows", async (c) => {
30321
30515
  return c.json({ page: await inspectObjectTable(c.env, body) });
30322
30516
  });
30323
30517
 
30324
- // src/id-graph/anon/connection.ts
30325
- var CONNECTION_CF_HEADER = "x-connection-cf";
30326
- var SESSION_HEADER = "x-session";
30327
- var MAX_HOST_USER_AGENT_LENGTH = 512;
30328
- var MAX_HOST_LOCALE_LENGTH = 64;
30329
- var INTERNAL_HEADERS = /* @__PURE__ */ new Set([
30330
- "x-user-id",
30331
- "x-new-user",
30332
- "x-geo",
30333
- SITE_ID_HEADER,
30334
- CONNECTION_CF_HEADER,
30335
- SESSION_HEADER,
30336
- TRACE_HEADER,
30337
- CF_DATA_HEADER
30338
- ]);
30339
- var REDACTED_HEADERS = /* @__PURE__ */ new Set(["authorization"]);
30340
- var encodeConnectionCf = /* @__PURE__ */ __name((cf) => {
30341
- if (cf === void 0) {
30342
- return void 0;
30343
- }
30344
- const encoded = headerJson(cf);
30345
- return encoded.length > MAX_ATTACHMENT_BYTES ? void 0 : encoded;
30346
- }, "encodeConnectionCf");
30347
- var forwardedCf = /* @__PURE__ */ __name((request) => {
30348
- const raw2 = request.headers.get(CONNECTION_CF_HEADER);
30349
- if (raw2 === null) {
30350
- return void 0;
30351
- }
30352
- return JSON.parse(raw2);
30353
- }, "forwardedCf");
30354
- var forwardedSession = /* @__PURE__ */ __name((request) => {
30355
- const raw2 = request.headers.get(SESSION_HEADER);
30356
- if (raw2 === null) {
30357
- return void 0;
30358
- }
30359
- return JSON.parse(raw2);
30360
- }, "forwardedSession");
30361
- var connectAttrs = /* @__PURE__ */ __name((request, geo) => {
30362
- const headers = {};
30363
- for (const [name, value] of request.headers) {
30364
- const lower = name.toLowerCase();
30365
- if (!INTERNAL_HEADERS.has(lower) && !REDACTED_HEADERS.has(lower)) {
30366
- headers[name] = value;
30367
- }
30368
- }
30369
- const cf = forwardedCf(request);
30370
- return {
30371
- "url.full": request.url,
30372
- "http.request.headers": JSON.stringify(headers),
30373
- ...cf === void 0 ? {} : { "qinto.cf": JSON.stringify(cf) },
30374
- ...geo === void 0 ? {} : { "qinto.geo": JSON.stringify(loggableGeo(geo)) }
30375
- };
30376
- }, "connectAttrs");
30377
- var withoutQuery = /* @__PURE__ */ __name((url2) => {
30378
- const parsed = new URL(url2);
30379
- parsed.search = "";
30380
- return parsed.toString();
30381
- }, "withoutQuery");
30382
- var browserGeoFrom = /* @__PURE__ */ __name((geo) => {
30383
- if (geo === void 0) {
30384
- return void 0;
30385
- }
30386
- return {
30387
- country: geo.country ?? null,
30388
- // EdgeTag's geoRegion is Cloudflare's subdivision code.
30389
- region: geo.regionCode ?? null,
30390
- state: geo.region ?? null,
30391
- isEU: geo.isEUCountry === "yes"
30392
- };
30393
- }, "browserGeoFrom");
30394
- var attachmentFieldBytes = /* @__PURE__ */ __name((name, value) => {
30395
- return value === void 0 ? 0 : new TextEncoder().encode(`,"${name}":${JSON.stringify(value)}`).length;
30396
- }, "attachmentFieldBytes");
30397
- var boundedHost = /* @__PURE__ */ __name((request) => {
30398
- const raw2 = hostFromRequest(request);
30399
- const bounded = {
30400
- ip: raw2.ip,
30401
- userAgent: raw2.userAgent !== void 0 && raw2.userAgent.length <= MAX_HOST_USER_AGENT_LENGTH ? raw2.userAgent : void 0,
30402
- locale: raw2.locale !== void 0 && raw2.locale.length <= MAX_HOST_LOCALE_LENGTH ? raw2.locale : void 0
30403
- };
30404
- return bounded.ip === void 0 && bounded.userAgent === void 0 && bounded.locale === void 0 ? void 0 : bounded;
30405
- }, "boundedHost");
30406
- var connectionAttachment = /* @__PURE__ */ __name((request, geo, siteId) => {
30407
- const userAgent = request.headers.get("user-agent");
30408
- const attrs = {
30409
- "url.full": withoutQuery(request.url),
30410
- ...geo?.country === void 0 ? {} : { "qinto.country": geo.country },
30411
- ...geo?.city === void 0 ? {} : { "qinto.city": geo.city },
30412
- ...userAgent === null ? {} : { user_agent: userAgent }
30413
- };
30414
- const host = boundedHost(request);
30415
- const hostBytes = attachmentFieldBytes("host", host);
30416
- const browserGeo = browserGeoFrom(geo);
30417
- const browserGeoBytes = attachmentFieldBytes("browserGeo", browserGeo);
30418
- const session2 = forwardedSession(request);
30419
- const sessionBytes = attachmentFieldBytes("session", session2);
30420
- const siteBytes = attachmentFieldBytes("siteId", siteId);
30421
- const cf = forwardedCf(request);
30422
- const cfSuffix = cf === void 0 ? "" : `,"cf":${JSON.stringify(cf)}`;
30423
- const cfBytes = new TextEncoder().encode(cfSuffix).length;
30424
- const emptyAttrsBytes = new TextEncoder().encode(JSON.stringify({ attrs: {} })).length;
30425
- const fixedBytes = siteBytes + sessionBytes + hostBytes + browserGeoBytes;
30426
- const includedCf = cfBytes <= MAX_ATTACHMENT_BYTES - emptyAttrsBytes - fixedBytes ? cf : void 0;
30427
- const budgeted = budgetAttrs(attrs, MAX_ATTACHMENT_BYTES - fixedBytes - (includedCf === void 0 ? 0 : cfBytes));
30428
- return {
30429
- ...budgeted,
30430
- siteId,
30431
- ...session2 === void 0 ? {} : { session: session2 },
30432
- ...browserGeo === void 0 ? {} : { browserGeo },
30433
- ...includedCf === void 0 ? {} : { cf: includedCf },
30434
- ...host === void 0 ? {} : { host }
30435
- };
30436
- }, "connectionAttachment");
30437
-
30438
30518
  // src/paths/open.ts
30439
30519
  var withSetCookie = /* @__PURE__ */ __name((res, cookies) => {
30440
30520
  if (res.webSocket) {
@@ -31638,6 +31718,10 @@ var acceptedCaptures = /* @__PURE__ */ __name((registry3, consent2, apps, enforc
31638
31718
  }
31639
31719
  return accepted;
31640
31720
  }, "acceptedCaptures");
31721
+ var declaredTtlMs = /* @__PURE__ */ __name((registry3, appId, key) => {
31722
+ const app2 = registry3.apps[appId];
31723
+ return app2 === void 0 ? null : declaration(app2, key)?.ttlMs ?? null;
31724
+ }, "declaredTtlMs");
31641
31725
 
31642
31726
  // src/id-graph/kv-cache.ts
31643
31727
  var cachedKv = /* @__PURE__ */ __name((kv) => {
@@ -32562,7 +32646,7 @@ var MAX_BROWSER_RESULTS_BYTES = 32 * 1024;
32562
32646
  var MAX_FRAME_BYTES = 64 * 1024;
32563
32647
  var browserContextFrame = external_exports.object({
32564
32648
  occurredAt: external_exports.number().int().nonnegative().safe(),
32565
- page: external_exports.object({ url: external_exports.string().min(1).max(4096), referrer: external_exports.string().min(1).max(4096).optional() }).strict()
32649
+ page: pageContextField
32566
32650
  }).strict();
32567
32651
  var browserResult = external_exports.unknown().refine((value) => {
32568
32652
  return new TextEncoder().encode(JSON.stringify(value)).length <= MAX_BROWSER_RESULT_BYTES;
@@ -32794,8 +32878,14 @@ var AnonDO = class extends DurableObject2 {
32794
32878
  const attachment = connectionAttachment(request, geo, siteId);
32795
32879
  server.serializeAttachment(attachment);
32796
32880
  this.sendSharedContext([server], context);
32881
+ const page = connectPage(request);
32797
32882
  this.appFanOut(
32798
- { kind: "connect", ...attachment.cf === void 0 ? {} : { geo: attachment.cf }, host: attachment.host },
32883
+ {
32884
+ kind: "connect",
32885
+ ...attachment.cf === void 0 ? {} : { geo: attachment.cf },
32886
+ host: attachment.host,
32887
+ ...page === void 0 ? {} : { page }
32888
+ },
32799
32889
  userId,
32800
32890
  context
32801
32891
  );
@@ -32996,7 +33086,11 @@ var AnonDO = class extends DurableObject2 {
32996
33086
  if (!appWriteAllowed(context.registry, consentState2(this.kv), input2.slug, context.consentEnforced)) {
32997
33087
  return;
32998
33088
  }
32999
- this.storeCaptures(input2.writes.map((write) => ({ ...write, appId: input2.slug, source: "app", ttlMs: null })));
33089
+ this.storeCaptures(
33090
+ input2.writes.map((write) => {
33091
+ return { ...write, appId: input2.slug, source: "app", ttlMs: declaredTtlMs(context.registry, input2.slug, write.key) };
33092
+ })
33093
+ );
33000
33094
  });
33001
33095
  });
33002
33096
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "main": {
3
- "version": "0.1.4",
4
- "gitSha": "84a03b1ccdff814840f560c7efd0f72837454227",
5
- "builtAt": "2026-09-24T14:23:50.391Z",
3
+ "version": "0.1.6",
4
+ "gitSha": "802179565243bb2e4dd99f79f00a3815d4f0568c",
5
+ "builtAt": "2026-09-28T04:28:30.340Z",
6
6
  "compatibilityDate": "2026-06-23",
7
7
  "compatibilityFlags": [
8
8
  "nodejs_compat"
@@ -39,9 +39,9 @@
39
39
  ]
40
40
  },
41
41
  "app-runtime": {
42
- "version": "0.1.2",
43
- "gitSha": "5c24e6442ad668125bba59aaec084adb43b70d1d",
44
- "builtAt": "2026-09-24T14:23:53.656Z",
42
+ "version": "0.1.4",
43
+ "gitSha": "b53a595d63827d8fda4f898391c6c0986f0fd1fa",
44
+ "builtAt": "2026-09-28T04:28:33.821Z",
45
45
  "compatibilityDate": "2026-06-23",
46
46
  "compatibilityFlags": [
47
47
  "nodejs_compat"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blotout/qinto-cli",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
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": {
@@ -16,8 +16,7 @@
16
16
  },
17
17
  "files": [
18
18
  "bin",
19
- "dist",
20
- "templates"
19
+ "dist"
21
20
  ],
22
21
  "publishConfig": {
23
22
  "access": "public"
@@ -35,6 +34,7 @@
35
34
  "@qinto/app-build": "0.0.0",
36
35
  "@qinto/app-dev": "0.0.0",
37
36
  "@qinto/app-manifest": "0.0.0",
37
+ "@qinto/app-templates": "0.0.0",
38
38
  "@qinto/oauth-grammar": "0.0.0",
39
39
  "@types/node": "25.9.5",
40
40
  "@types/proper-lockfile": "4.1.4",