@blotout/qinto-cli 0.1.21 → 0.1.23

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
@@ -21625,6 +21663,7 @@ var MAX_TOOL_DATA_NODES = 5e3;
21625
21663
  var CONTROL_DEADLINES_MS = {
21626
21664
  noop: { platform: 15e3, site: 1e4 },
21627
21665
  lifecycle: { platform: 15e3, site: 1e4 },
21666
+ deleteRequest: { platform: 15e3, site: 1e4 },
21628
21667
  migration: { platform: 15e3, site: 1e4 },
21629
21668
  schedule: { platform: 65e3, site: 6e4 },
21630
21669
  tool: { platform: 55e3, site: 5e4 }
@@ -21666,6 +21705,14 @@ var lifecycleEventSchema = external_exports.discriminatedUnion("type", [
21666
21705
  }).strict(),
21667
21706
  external_exports.object({ ...lifecycleBase, type: external_exports.literal("uninstalled"), scope: external_exports.enum(["app", "install"]) }).strict()
21668
21707
  ]);
21708
+ var deleteRequestSchema = external_exports.object({
21709
+ eventId: external_exports.string().min(1).max(64),
21710
+ occurredAt: external_exports.number().int().nonnegative().safe(),
21711
+ install: installSchema2,
21712
+ personId: external_exports.string().min(1).nullable(),
21713
+ anonIds: external_exports.array(external_exports.string().min(1)),
21714
+ identifiers: external_exports.array(external_exports.object({ type: external_exports.enum(["email", "phone", "custom"]), value: external_exports.string().min(1) }).strict()).optional()
21715
+ }).strict();
21669
21716
  var migrationSchema = external_exports.discriminatedUnion("phase", [
21670
21717
  external_exports.object({
21671
21718
  resource: external_exports.string().min(1).max(64),
@@ -21708,7 +21755,12 @@ var toolSchema2 = external_exports.object({
21708
21755
  name: external_exports.string().min(1).max(MAX_APP_TOOL_NAME_LENGTH).regex(APP_TOOL_NAME_PATTERN),
21709
21756
  // Who asked, and what they may do on this site. Derived from the bearer by the portal, never
21710
21757
  // from the model's arguments.
21711
- viewer: external_exports.object({ userId: external_exports.string().min(1).max(64), access: external_exports.enum(["read", "write"]) }).strict(),
21758
+ viewer: external_exports.object({
21759
+ userId: external_exports.string().min(1).max(64),
21760
+ access: external_exports.enum(["read", "write"]),
21761
+ // Optional only while a site worker or app runtime that predates it can still receive this envelope; absent means `full`.
21762
+ appRole: external_exports.enum(APP_ROLES).optional()
21763
+ }).strict(),
21712
21764
  // Already validated against the tool's declared inputSchema, with the reserved routing names
21713
21765
  // stripped. Every declared input schema has an object root, so this is an object too — and a
21714
21766
  // call that names no arguments normalizes to `{}` rather than reaching the handler as undefined.
@@ -21723,10 +21775,11 @@ var toolSchema2 = external_exports.object({
21723
21775
  }, "Tool arguments exceed the size cap");
21724
21776
  var controlEnvelopeSchema = external_exports.object({
21725
21777
  v: external_exports.literal(CONTROL_ENVELOPE_VERSION),
21726
- kind: external_exports.enum(["noop", "lifecycle", "migration", "schedule", "tool"]),
21778
+ kind: external_exports.enum(["noop", "lifecycle", "deleteRequest", "migration", "schedule", "tool"]),
21727
21779
  app: external_exports.object({ slug: external_exports.string().min(1).max(30), version: external_exports.string().min(1).max(32) }).strict(),
21728
21780
  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(),
21729
21781
  lifecycle: lifecycleEventSchema.optional(),
21782
+ deleteRequest: deleteRequestSchema.optional(),
21730
21783
  migration: migrationSchema.optional(),
21731
21784
  schedule: scheduleSchema2.optional(),
21732
21785
  tool: toolSchema2.optional()
@@ -21734,6 +21787,9 @@ var controlEnvelopeSchema = external_exports.object({
21734
21787
  if (envelope.kind === "lifecycle" !== (envelope.lifecycle !== void 0)) {
21735
21788
  ctx.addIssue({ code: "custom", path: ["lifecycle"], message: "Lifecycle payload must match the control kind" });
21736
21789
  }
21790
+ if (envelope.kind === "deleteRequest" !== (envelope.deleteRequest !== void 0)) {
21791
+ ctx.addIssue({ code: "custom", path: ["deleteRequest"], message: "Delete request payload must match the control kind" });
21792
+ }
21737
21793
  if (envelope.kind === "migration" !== (envelope.migration !== void 0)) {
21738
21794
  ctx.addIssue({ code: "custom", path: ["migration"], message: "Migration payload must match the control kind" });
21739
21795
  }
@@ -21743,8 +21799,8 @@ var controlEnvelopeSchema = external_exports.object({
21743
21799
  if (envelope.kind === "tool" !== (envelope.tool !== void 0)) {
21744
21800
  ctx.addIssue({ code: "custom", path: ["tool"], message: "Tool payload must match the control kind" });
21745
21801
  }
21746
- if (envelope.kind === "lifecycle" && envelope.install === void 0) {
21747
- ctx.addIssue({ code: "custom", path: ["install"], message: "Lifecycle control needs an install" });
21802
+ if ((envelope.kind === "lifecycle" || envelope.kind === "deleteRequest") && envelope.install === void 0) {
21803
+ ctx.addIssue({ code: "custom", path: ["install"], message: `${envelope.kind} control needs an install` });
21748
21804
  }
21749
21805
  if (envelope.kind === "tool" && envelope.install === void 0) {
21750
21806
  ctx.addIssue({ code: "custom", path: ["install"], message: "Tool control needs an install" });
@@ -21783,6 +21839,8 @@ var toolResultSchema = external_exports.object({
21783
21839
  var controlResponseSchema = external_exports.object({
21784
21840
  v: external_exports.literal(CONTROL_ENVELOPE_VERSION),
21785
21841
  ok: external_exports.boolean(),
21842
+ // The bundle has no delete request handler, so a retry cannot succeed.
21843
+ skipped: external_exports.literal(true).optional(),
21786
21844
  tool: toolResultSchema.optional(),
21787
21845
  migration: external_exports.union([
21788
21846
  external_exports.object({ done: external_exports.literal(true) }).strict(),
@@ -21914,7 +21972,12 @@ var MAX_UI_BODY_BYTES = 1024 * 1024;
21914
21972
  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();
21915
21973
  var uiContextSchema = external_exports.object({
21916
21974
  version: external_exports.string().min(1).max(32),
21917
- viewer: external_exports.object({ userId: external_exports.string().min(1).max(64), access: external_exports.enum(["read", "write"]) }).strict(),
21975
+ viewer: external_exports.object({
21976
+ userId: external_exports.string().min(1).max(64),
21977
+ access: external_exports.enum(["read", "write"]),
21978
+ // Optional only while a site worker that predates it can still send this header; absent means `full`.
21979
+ appRole: external_exports.enum(APP_ROLES).optional()
21980
+ }).strict(),
21918
21981
  install: uiInstallSchema,
21919
21982
  installs: external_exports.array(uiInstallSchema).min(1).max(MAX_INSTALLS_PER_APP),
21920
21983
  writeTicket: external_exports.string().min(1).max(2048).optional()
@@ -26275,6 +26338,16 @@ var identifierField2 = external_exports.string().trim().min(1).refine((value) =>
26275
26338
  description: "Who the request applies to: a canonical Qinto id (`anon_\u2026`, or a `tag_user_id` adopted from EdgeTag), or any identifier from your own systems (email, phone, custom id). Internal `gid_\u2026` ids are rejected.",
26276
26339
  example: "crm-user-8412"
26277
26340
  });
26341
+ var readIdentifierField = identifierField2.optional().openapi({
26342
+ param: { name: "identifier", in: "query" },
26343
+ description: "User to read: any identifier from your own systems (email, phone, custom id), or a canonical Qinto id (`anon_\u2026`, or a `tag_user_id` adopted from EdgeTag). Omit it to read the user from the first-party cookie instead. Internal `gid_\u2026` ids are rejected.",
26344
+ example: "ada@example.com"
26345
+ });
26346
+ var canonicalIdentifierField = external_exports.string().min(1).optional().openapi({
26347
+ param: { name: "identifier", in: "query" },
26348
+ description: "Canonical Qinto id (`anon_\u2026`) of the user to read. Omit it to resolve the user from the first-party cookie instead. Reading by a custom identifier arrives with authenticated API access.",
26349
+ example: "anon_7b8d1a52-6c1e-4b6f-9a30-2f8a54d1c3e9"
26350
+ });
26278
26351
  var consentCategoriesField = external_exports.object({
26279
26352
  // Necessary consent cannot be opted out.
26280
26353
  necessary: external_exports.literal(true).optional().openapi({
@@ -27102,6 +27175,9 @@ var linkedAnonsPage = /* @__PURE__ */ __name((sql, afterRowId, limit) => {
27102
27175
  var linkedAnonsExcept = /* @__PURE__ */ __name((sql, originAnonId) => {
27103
27176
  return sql.exec("SELECT key FROM facts WHERE kind = 'anon' AND key != ?", originAnonId).toArray().map((row) => row.key);
27104
27177
  }, "linkedAnonsExcept");
27178
+ var allLinkedAnons = /* @__PURE__ */ __name((sql) => {
27179
+ return sql.exec("SELECT key FROM facts WHERE kind = 'anon' ORDER BY rowid").toArray().map((row) => row.key);
27180
+ }, "allLinkedAnons");
27105
27181
 
27106
27182
  // src/id-graph/person.ts
27107
27183
  var IDENTITY_CAPABILITY = "identity";
@@ -27179,6 +27255,124 @@ var identityFields = {
27179
27255
  key: external_exports.string().min(1)
27180
27256
  };
27181
27257
 
27258
+ // src/apps/currency-rates.ts
27259
+ var CURRENCY_FEED_URL = "https://cdn.shopify.com/s/javascripts/currencies.js";
27260
+ var RATES_TTL_MS = 60 * 60 * 1e3;
27261
+ var RETRY_AFTER_FAILURE_MS = 60 * 1e3;
27262
+ var FEED_TIMEOUT_MS = 5e3;
27263
+ var FEED_EDGE_CACHE_TTL_S = 8 * 60 * 60;
27264
+ var MIN_CURRENCY_CODES = 100;
27265
+ var currencyMemo;
27266
+ var parseRatesLiteral = /* @__PURE__ */ __name((feed) => {
27267
+ const opening = "rates:{";
27268
+ const start = feed.indexOf(opening);
27269
+ if (start === -1) {
27270
+ return null;
27271
+ }
27272
+ const entry = /([A-Z]{3}):(-?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)([,}])/y;
27273
+ entry.lastIndex = start + opening.length;
27274
+ const rates = {};
27275
+ for (; ; ) {
27276
+ const match2 = entry.exec(feed);
27277
+ if (match2 === null) {
27278
+ return null;
27279
+ }
27280
+ const [, code, value, terminator] = match2;
27281
+ if (Object.hasOwn(rates, code)) {
27282
+ return null;
27283
+ }
27284
+ rates[code] = Number(value);
27285
+ if (terminator === "}") {
27286
+ return rates;
27287
+ }
27288
+ }
27289
+ }, "parseRatesLiteral");
27290
+ var parseCurrencyFeed = /* @__PURE__ */ __name((feed) => {
27291
+ const rates = parseRatesLiteral(feed);
27292
+ if (rates === null) {
27293
+ return { ok: false, reason: "the rates literal does not parse" };
27294
+ }
27295
+ if (rates["USD"] !== 1) {
27296
+ return { ok: false, reason: "USD is not 1" };
27297
+ }
27298
+ const invalid = Object.entries(rates).find(([, rate]) => !Number.isFinite(rate) || rate <= 0);
27299
+ if (invalid !== void 0) {
27300
+ return { ok: false, reason: `${invalid[0]} is not a positive finite rate` };
27301
+ }
27302
+ const count = Object.keys(rates).length;
27303
+ if (count < MIN_CURRENCY_CODES) {
27304
+ return { ok: false, reason: `the feed has ${count} codes` };
27305
+ }
27306
+ return { ok: true, rates };
27307
+ }, "parseCurrencyFeed");
27308
+ var fetchCurrencyRates = /* @__PURE__ */ __name(async () => {
27309
+ return traced("currency.fetch", { attrs: { "http.url": CURRENCY_FEED_URL } }, async () => {
27310
+ let feed;
27311
+ try {
27312
+ const response = await fetch(CURRENCY_FEED_URL, {
27313
+ signal: AbortSignal.timeout(FEED_TIMEOUT_MS),
27314
+ cf: { cacheTtlByStatus: { "200-299": FEED_EDGE_CACHE_TTL_S, "300-599": 0 } }
27315
+ });
27316
+ if (!response.ok) {
27317
+ addSpanEvent("currency fetch failed", { level: "error", attrs: { status: response.status } });
27318
+ return null;
27319
+ }
27320
+ feed = await response.text();
27321
+ } catch (error61) {
27322
+ addSpanEvent("currency fetch failed", { level: "error", attrs: { error: String(error61) } });
27323
+ return null;
27324
+ }
27325
+ const verdict = parseCurrencyFeed(feed);
27326
+ if (!verdict.ok) {
27327
+ addSpanEvent("currency feed rejected", { level: "error", attrs: { reason: verdict.reason } });
27328
+ return null;
27329
+ }
27330
+ return verdict.rates;
27331
+ });
27332
+ }, "fetchCurrencyRates");
27333
+ var refreshCurrencyRates = /* @__PURE__ */ __name(async () => {
27334
+ const rates = await fetchCurrencyRates();
27335
+ const now = Date.now();
27336
+ if (rates === null) {
27337
+ const current2 = currencyMemo?.current ?? null;
27338
+ currencyMemo = { current: current2, nextAttemptAt: now + RETRY_AFTER_FAILURE_MS };
27339
+ return current2;
27340
+ }
27341
+ const current = { rates, fetchedAt: now };
27342
+ currencyMemo = { current, nextAttemptAt: now + RATES_TTL_MS };
27343
+ return current;
27344
+ }, "refreshCurrencyRates");
27345
+ var currentCurrencyRates = /* @__PURE__ */ __name(async (ctx) => {
27346
+ const now = Date.now();
27347
+ if (currencyMemo !== void 0 && now < currencyMemo.nextAttemptAt) {
27348
+ return currencyMemo.current;
27349
+ }
27350
+ const stale = currencyMemo?.current ?? null;
27351
+ if (stale === null) {
27352
+ return refreshCurrencyRates();
27353
+ }
27354
+ currencyMemo = { current: stale, nextAttemptAt: now + RETRY_AFTER_FAILURE_MS };
27355
+ ctx.waitUntil(
27356
+ runInvocation({ name: "currency.refresh", trace: currentTraceContext() }, () => {
27357
+ return refreshCurrencyRates();
27358
+ })
27359
+ );
27360
+ return stale;
27361
+ }, "currentCurrencyRates");
27362
+
27363
+ // src/apps/broker-currency.ts
27364
+ var currencyRatesBody = external_exports.object({
27365
+ ...identityFields,
27366
+ kind: external_exports.literal("currencyRates")
27367
+ }).strict();
27368
+ var handleCurrencyRates = /* @__PURE__ */ __name(async (ctx) => {
27369
+ const current = await currentCurrencyRates(ctx);
27370
+ if (current === null) {
27371
+ return Response.json({ error: "currency_rates_unavailable" }, { status: 503 });
27372
+ }
27373
+ return Response.json(current);
27374
+ }, "handleCurrencyRates");
27375
+
27182
27376
  // src/apps/emit-payload.ts
27183
27377
  var emitPayloadSchema = external_exports.object({
27184
27378
  event: external_exports.string().min(1),
@@ -27981,6 +28175,7 @@ var brokerBody = external_exports.discriminatedUnion("kind", [
27981
28175
  contextWriteBody,
27982
28176
  factsReadBody,
27983
28177
  identityReadBody,
28178
+ currencyRatesBody,
27984
28179
  trackBody
27985
28180
  ]);
27986
28181
  var BODY_ENVELOPE_ALLOWANCE_BYTES = 2 * 1024;
@@ -28316,7 +28511,7 @@ var handleActive = /* @__PURE__ */ __name(async (env, body) => {
28316
28511
  }
28317
28512
  return Response.json({ active: true });
28318
28513
  }, "handleActive");
28319
- var handleBroker = /* @__PURE__ */ __name(async (request, env) => {
28514
+ var handleBroker = /* @__PURE__ */ __name(async (request, env, ctx) => {
28320
28515
  const bytes = await readBoundedBody(request, MAX_BODY_BYTES);
28321
28516
  if (bytes === null) {
28322
28517
  return Response.json({ error: "too_large" }, { status: 413 });
@@ -28368,6 +28563,9 @@ var handleBroker = /* @__PURE__ */ __name(async (request, env) => {
28368
28563
  case "identityRead": {
28369
28564
  return handleIdentityRead(env, body);
28370
28565
  }
28566
+ case "currencyRates": {
28567
+ return handleCurrencyRates(ctx);
28568
+ }
28371
28569
  case "track": {
28372
28570
  return handleTrack(env, body);
28373
28571
  }
@@ -28378,7 +28576,7 @@ var dispatchGated = /* @__PURE__ */ __name((c) => {
28378
28576
  if (c.env.DEPLOY_TARGET !== "dispatch") {
28379
28577
  return c.notFound();
28380
28578
  }
28381
- return handleBroker(c.req.raw, c.env);
28579
+ return handleBroker(c.req.raw, c.env, c.executionCtx);
28382
28580
  }, "dispatchGated");
28383
28581
  for (const path of BROKER_PATHS) {
28384
28582
  brokerRoutes.post(path, dispatchGated);
@@ -28390,7 +28588,7 @@ var Broker = class extends WorkerEntrypoint {
28390
28588
  fetch(request) {
28391
28589
  const trace = parseTraceContext(request.headers.get(TRACE_HEADER));
28392
28590
  return runInvocation({ name: "broker", trace }, () => {
28393
- return handleBroker(request, this.env);
28591
+ return handleBroker(request, this.env, this.ctx);
28394
28592
  });
28395
28593
  }
28396
28594
  };
@@ -29391,20 +29589,17 @@ var matchEndpoint = /* @__PURE__ */ __name((app2, path, method) => {
29391
29589
  if (!isHttpMethod(method)) {
29392
29590
  return void 0;
29393
29591
  }
29394
- const serving = app2.endpoints.filter((endpoint) => {
29395
- return endpoint.methods.includes(method);
29396
- });
29397
29592
  const matched = matchEndpointPath(
29398
- serving.map((endpoint) => endpoint.path),
29593
+ app2.endpoints.map((endpoint) => endpoint.path),
29399
29594
  path
29400
29595
  );
29401
29596
  if (matched === void 0) {
29402
29597
  return void 0;
29403
29598
  }
29404
- const declared = serving.find((endpoint) => {
29599
+ const declared = app2.endpoints.find((endpoint) => {
29405
29600
  return endpoint.path === matched.route;
29406
29601
  });
29407
- if (!["public", "token", "visitor"].includes(declared.auth) || declared.mode !== "webhook" && declared.mode !== "api") {
29602
+ if (!declared.methods.includes(method) || !["public", "token", "visitor"].includes(declared.auth) || declared.mode !== "webhook" && declared.mode !== "api") {
29408
29603
  return void 0;
29409
29604
  }
29410
29605
  return {
@@ -29738,7 +29933,9 @@ var claimsSchema = external_exports.object({
29738
29933
  appId: external_exports.string().min(1),
29739
29934
  appInstallId: external_exports.string().min(1),
29740
29935
  appVersionId: external_exports.string().min(1),
29741
- access: external_exports.enum(["read", "write"])
29936
+ access: external_exports.enum(["read", "write"]),
29937
+ // Absent means `full`; see the mint below.
29938
+ appRole: external_exports.enum(APP_ROLES).optional()
29742
29939
  }).strict();
29743
29940
  var appUiAudience = /* @__PURE__ */ __name((slug) => {
29744
29941
  return `app-ui:${slug}`;
@@ -29907,7 +30104,8 @@ var forwardToApp2 = /* @__PURE__ */ __name(async (c, input2) => {
29907
30104
  "app.install_ord": install.ord,
29908
30105
  "app.install_label": install.label,
29909
30106
  "app.viewer_id": viewer.userId,
29910
- "app.viewer_access": viewer.access
30107
+ "app.viewer_access": viewer.access,
30108
+ "app.viewer_app_role": viewer.appRole
29911
30109
  }
29912
30110
  },
29913
30111
  () => {
@@ -29953,7 +30151,13 @@ var authorizeUi = /* @__PURE__ */ __name(async (c, slug, origin) => {
29953
30151
  appId: verdict.claims.appId,
29954
30152
  appVersionId: verdict.claims.appVersionId
29955
30153
  }) : void 0;
29956
- return { app: app2, install, viewer: { userId: verdict.claims.userId, access: verdict.claims.access }, writeTicket };
30154
+ return {
30155
+ app: app2,
30156
+ install,
30157
+ // The portal mints `full` as no claim, so a site worker that predates the claim still accepts the token.
30158
+ viewer: { userId: verdict.claims.userId, access: verdict.claims.access, appRole: verdict.claims.appRole ?? "full" },
30159
+ writeTicket
30160
+ };
29957
30161
  }, "authorizeUi");
29958
30162
  var handleAppUi = /* @__PURE__ */ __name(async (c, slug) => {
29959
30163
  const host = c.env.APP_UI_HOST;
@@ -30001,7 +30205,7 @@ appUiRoutes.all("/apps/:slug/ui/*", (c) => {
30001
30205
  var apiInfo = {
30002
30206
  title: "Qinto Edge API",
30003
30207
  version: "0.1.0",
30004
- description: "HTTP API for recording user activity on the Qinto platform: track events, attach identity traits, link custom identifiers, and record or read a user\u2019s consent and properties.\n\nEach request applies to one user. Writes address that user by any identifier of your own (email, phone, custom id) or a canonical Qinto id (`anon_\u2026`); the same identifier always reaches the same user. Reads take a canonical id or the first-party cookie; `GET /v1/session` also accepts your own.\n\nEvery `/v1` operation takes a worker access token as `Authorization: Bearer <token>`, scoped to the endpoints it may call; generate one from the Qinto platform\u2019s worker access tokens page. Errors use one JSON envelope: `error` is a machine-readable slug to switch on, and `details` optionally carries field-level validation errors. The health check is unauthenticated and replies with plain text."
30208
+ description: 'HTTP API for recording user activity on the Qinto platform: track events, attach identity traits, link custom identifiers, and record or read a user\u2019s consent and properties.\n\nEach request applies to one user. Writes address that user by any identifier of your own (email, phone, custom id) or a canonical Qinto id (`anon_\u2026`); the same identifier always reaches the same user. Reads take a canonical id or the first-party cookie; `GET /v1/session` also accepts your own.\n\nEvery `/v1` operation takes a worker access token as `Authorization: Bearer <token>`, scoped to the endpoints it may call; generate one from the Qinto platform\u2019s worker access tokens page. Errors use one JSON envelope: `error` is a machine-readable slug to switch on, and `details` optionally carries field-level validation errors. The health check is unauthenticated and answers `{ "status": "success" }`.'
30005
30209
  };
30006
30210
  var apiTags = [
30007
30211
  {
@@ -30049,7 +30253,7 @@ var ERRORS = {
30049
30253
  },
30050
30254
  internal: {
30051
30255
  status: 500,
30052
- description: "Unexpected server error. Nothing is guaranteed to have been persisted; reads are safe to retry, and conversions dedupe on their order id, but a retried ingestion request may record other parts of the action a second time."
30256
+ description: "Unexpected server error. Nothing is guaranteed to have been persisted; reads are safe to retry, and conversions dedupe on their order id, but retrying an ingestion request can deliver the event to the site\u2019s apps again."
30053
30257
  }
30054
30258
  };
30055
30259
  var DETAILS_SLUGS = /* @__PURE__ */ new Set(["invalid_params", "invalid_body"]);
@@ -30306,6 +30510,140 @@ var health = createApiRouter().openapi(definition, (c) => {
30306
30510
  return respond(c, healthResponse, { status: "success" });
30307
30511
  });
30308
30512
 
30513
+ // src/id-graph/delete-request.ts
30514
+ var readAlias = /* @__PURE__ */ __name((env, key) => {
30515
+ return traced("rpc alias.get", {}, () => {
30516
+ return aliasStub(env, key).get(currentTraceContext());
30517
+ });
30518
+ }, "readAlias");
30519
+ var readAnon = /* @__PURE__ */ __name((env, anonId) => {
30520
+ return traced("rpc anon.erasable", { attrs: { "anon.id": anonId } }, () => {
30521
+ return anonStub(env, anonId).erasable(currentTraceContext());
30522
+ });
30523
+ }, "readAnon");
30524
+ var readPerson = /* @__PURE__ */ __name((env, globalId) => {
30525
+ return traced("rpc global.erasable", { attrs: { "global.id": globalId } }, () => {
30526
+ return globalStub(env, globalId).erasable(currentTraceContext());
30527
+ });
30528
+ }, "readPerson");
30529
+ var typedOf = /* @__PURE__ */ __name(async (env, value) => {
30530
+ if (isCanonicalId(value)) {
30531
+ return { inputType: "anon", kind: "anon", name: value, alias: null, personId: null, anonId: value };
30532
+ }
30533
+ if (isGlobalId(value)) {
30534
+ return { inputType: "person", kind: "global", name: value, alias: null, personId: value, anonId: null };
30535
+ }
30536
+ const identifier = classifyIdentifier(value);
30537
+ const key = await aliasKey(identifier);
30538
+ const assignment = await readAlias(env, key);
30539
+ return {
30540
+ inputType: identifier.type,
30541
+ kind: "alias",
30542
+ name: key,
30543
+ alias: assignment === null ? null : identifier,
30544
+ personId: assignment?.globalId ?? null,
30545
+ anonId: assignment?.anonId ?? null
30546
+ };
30547
+ }, "typedOf");
30548
+ var uniqueIdentifiers = /* @__PURE__ */ __name((identifiers) => {
30549
+ const byKey = new Map(identifiers.map((identifier) => [`${identifier.type}:${identifier.value}`, identifier]));
30550
+ return [...byKey.values()];
30551
+ }, "uniqueIdentifiers");
30552
+ var reachFrom = /* @__PURE__ */ __name(async (env, typed) => {
30553
+ const anons = /* @__PURE__ */ new Map();
30554
+ let personId = typed.personId;
30555
+ if (personId === null && typed.anonId !== null) {
30556
+ const anon = await readAnon(env, typed.anonId);
30557
+ if (anon !== null) {
30558
+ anons.set(typed.anonId, anon);
30559
+ personId = anon.globalId;
30560
+ }
30561
+ }
30562
+ const person2 = personId === null ? null : await readPerson(env, personId);
30563
+ for (const anonId of person2?.anonIds ?? []) {
30564
+ const anon = anons.has(anonId) ? null : await readAnon(env, anonId);
30565
+ if (anon !== null) {
30566
+ anons.set(anonId, anon);
30567
+ }
30568
+ }
30569
+ return { personId, person: person2, anons };
30570
+ }, "reachFrom");
30571
+ var ownedAliases = /* @__PURE__ */ __name(async (env, typed, reached) => {
30572
+ const aliasKeys = [];
30573
+ const identifiers = [];
30574
+ const held = [...reached.person?.identifiers ?? [], ...[...reached.anons.values()].flatMap((anon) => anon.identifiers)];
30575
+ for (const identifier of uniqueIdentifiers(held)) {
30576
+ const key = await aliasKey(identifier);
30577
+ const assignment = key === typed.name ? null : await readAlias(env, key);
30578
+ const owner = assignment?.globalId ?? null;
30579
+ const owned = assignment === null || (owner === null ? [...reached.anons.keys()].some((anonId) => anonId === assignment.anonId) : owner === reached.personId);
30580
+ if (key !== typed.name && owned) {
30581
+ identifiers.push(identifier);
30582
+ if (assignment !== null) {
30583
+ aliasKeys.push(key);
30584
+ }
30585
+ }
30586
+ }
30587
+ if (typed.alias !== null) {
30588
+ aliasKeys.push(typed.name);
30589
+ identifiers.push(typed.alias);
30590
+ }
30591
+ return { aliasKeys, identifiers };
30592
+ }, "ownedAliases");
30593
+ var planOf = /* @__PURE__ */ __name(async (env, typed) => {
30594
+ const reached = await reachFrom(env, typed);
30595
+ const { aliasKeys, identifiers } = await ownedAliases(env, typed, reached);
30596
+ return {
30597
+ inputType: typed.inputType,
30598
+ personId: reached.person === null ? null : reached.personId,
30599
+ anonIds: [...reached.anons.keys()],
30600
+ aliasKeys,
30601
+ identifiers
30602
+ };
30603
+ }, "planOf");
30604
+ var planDeleteRequest = /* @__PURE__ */ __name(async (env, value) => {
30605
+ return planOf(env, await typedOf(env, value));
30606
+ }, "planDeleteRequest");
30607
+ var ERASERS = Object.freeze({
30608
+ alias: /* @__PURE__ */ __name((env, name) => {
30609
+ return traced("rpc alias.erase", {}, () => {
30610
+ return aliasStub(env, name).erase(currentTraceContext());
30611
+ });
30612
+ }, "alias"),
30613
+ anon: /* @__PURE__ */ __name((env, name) => {
30614
+ return traced("rpc anon.erase", { attrs: { "anon.id": name } }, () => {
30615
+ return anonStub(env, name).erase(currentTraceContext());
30616
+ });
30617
+ }, "anon"),
30618
+ global: /* @__PURE__ */ __name((env, name) => {
30619
+ return traced("rpc global.erase", { attrs: { "global.id": name } }, () => {
30620
+ return globalStub(env, name).erase(currentTraceContext());
30621
+ });
30622
+ }, "global")
30623
+ });
30624
+ var eraseOrder = /* @__PURE__ */ __name((plan, typed) => {
30625
+ const steps = [
30626
+ ...plan.aliasKeys.map((name) => ({ kind: "alias", name })),
30627
+ ...plan.anonIds.map((name) => ({ kind: "anon", name })),
30628
+ ...plan.personId === null ? [] : [{ kind: "global", name: plan.personId }]
30629
+ ];
30630
+ const isTyped = /* @__PURE__ */ __name((step) => {
30631
+ return step.kind === typed.kind && step.name === typed.name;
30632
+ }, "isTyped");
30633
+ return [...steps.filter((step) => !isTyped(step)), ...steps.filter(isTyped)];
30634
+ }, "eraseOrder");
30635
+ var runDeleteRequest = /* @__PURE__ */ __name(async (env, value) => {
30636
+ const typed = await typedOf(env, value);
30637
+ const plan = await planOf(env, typed);
30638
+ const erased = { alias: 0, anon: 0, global: 0 };
30639
+ for (const step of eraseOrder(plan, typed)) {
30640
+ if (await ERASERS[step.kind](env, step.name)) {
30641
+ erased[step.kind] += 1;
30642
+ }
30643
+ }
30644
+ return { inputType: plan.inputType, anons: erased.anon, aliases: erased.alias, persons: erased.global };
30645
+ }, "runDeleteRequest");
30646
+
30309
30647
  // src/id-graph/inspect.ts
30310
30648
  var MAX_TABLE_PAGE = 100;
30311
30649
  var INTERNAL_TABLE = /^(sqlite_|_cf_)/;
@@ -30430,6 +30768,7 @@ var rowsBody = inspectBody.extend({
30430
30768
  offset: external_exports.number().int().min(0).default(0),
30431
30769
  limit: external_exports.number().int().min(1).max(MAX_TABLE_PAGE).default(MAX_TABLE_PAGE)
30432
30770
  });
30771
+ var valueBody = external_exports.object({ value: external_exports.string().min(1) });
30433
30772
  var readBody = /* @__PURE__ */ __name(async (c, schema) => {
30434
30773
  const parsed = schema.safeParse(await c.req.json().catch(() => null));
30435
30774
  return parsed.success ? parsed.data : null;
@@ -30457,6 +30796,20 @@ idGraphInspect.post("/internal/id-graph/rows", async (c) => {
30457
30796
  }
30458
30797
  return c.json({ page: await inspectObjectTable(c.env, body) });
30459
30798
  });
30799
+ idGraphInspect.post("/internal/id-graph/plan", async (c) => {
30800
+ const body = await readBody(c, valueBody);
30801
+ if (body === null) {
30802
+ return c.json({ error: "invalid_body" }, 400);
30803
+ }
30804
+ return c.json(await planDeleteRequest(c.env, body.value));
30805
+ });
30806
+ idGraphInspect.post("/internal/id-graph/delete", async (c) => {
30807
+ const body = await readBody(c, valueBody);
30808
+ if (body === null) {
30809
+ return c.json({ error: "invalid_body" }, 400);
30810
+ }
30811
+ return c.json(await runDeleteRequest(c.env, body.value));
30812
+ });
30460
30813
 
30461
30814
  // src/paths/open.ts
30462
30815
  var withSetCookie = /* @__PURE__ */ __name((res, cookies) => {
@@ -30544,14 +30897,14 @@ var open2 = /* @__PURE__ */ __name(async (c) => {
30544
30897
 
30545
30898
  // browser-artifact/meta.json
30546
30899
  var meta_default = {
30547
- version: "0.1.10",
30548
- gitSha: "f21182929c88fa0748f575164bde781b31f4fbdd",
30549
- builtAt: "2026-09-23T15:34:39.048Z",
30550
- hash: "d1804e7914363626fed0280696cad5e06728b899e23c8a5ee6dcced3b67cf831"
30900
+ version: "0.1.11",
30901
+ gitSha: "bb87bca6d3b96627f24bc0c29675f2f19680ce60",
30902
+ builtAt: "2026-09-28T04:35:01.606Z",
30903
+ hash: "e32dfe041826d6daaa3682d1f5cac137ffcc6a24f55bfef1a936f7e3ef4cde12"
30551
30904
  };
30552
30905
 
30553
30906
  // src/paths/q-js.ts
30554
- import qScript from "./7bc13565a6fa9d520d0ebed788d11591e8db5b44-q.js.txt";
30907
+ import qScript from "./944c722e73fe144b1067da970f87716a835c697e-q.js.txt";
30555
30908
 
30556
30909
  // src/apps-q.ts
30557
30910
  import appsQScript from "./apps-q.json";
@@ -30797,11 +31150,7 @@ var consent = createApiRouter().openapi(definition4, async (c) => {
30797
31150
 
30798
31151
  // src/paths/v1/consent-state.ts
30799
31152
  var consentStateQuery = external_exports.object({
30800
- identifier: external_exports.string().min(1).optional().openapi({
30801
- param: { name: "identifier", in: "query" },
30802
- description: "Canonical Qinto id (`anon_\u2026`) of the user to read. Omit it to resolve the user from the first-party cookie instead. Reading by a custom identifier arrives with authenticated API access.",
30803
- example: "anon_7b8d1a52-6c1e-4b6f-9a30-2f8a54d1c3e9"
30804
- })
31153
+ identifier: canonicalIdentifierField
30805
31154
  });
30806
31155
  var consentStateResponse = external_exports.object({
30807
31156
  categories: external_exports.object({
@@ -30828,7 +31177,7 @@ var definition5 = createRoute({
30828
31177
  summary: "Get user consent",
30829
31178
  description: operationDescription({
30830
31179
  intro: "Returns the consent state currently stored for the user.",
30831
- howItWorks: "A request that carries neither a `identifier` nor the cookie \u2014 or a `identifier` that is not canonical \u2014 fails with `invalid_params`. A choice saved on another session reaches this one separately, so a read taken right after a write elsewhere can return the previous value, and a session with no further activity can stay behind indefinitely. Read back from the session that made the change when you need the value you just wrote."
31180
+ howItWorks: "A request that carries neither an `identifier` nor the cookie \u2014 or an `identifier` that is not canonical \u2014 fails with `invalid_params`. A choice saved on another session reaches this one separately, so a read taken right after a write elsewhere can return the previous value, and a session with no further activity can stay behind indefinitely. Read back from the session that made the change when you need the value you just wrote."
30832
31181
  }),
30833
31182
  request: { query: consentStateQuery },
30834
31183
  responses: {
@@ -30899,11 +31248,7 @@ var identify = createApiRouter().openapi(definition6, async (c) => {
30899
31248
 
30900
31249
  // src/paths/v1/person.ts
30901
31250
  var personQuery = external_exports.object({
30902
- identifier: identifierField2.optional().openapi({
30903
- param: { name: "identifier", in: "query" },
30904
- description: "User to read: any identifier from your own systems (email, phone, custom id), or a canonical Qinto id (`anon_\u2026`, or a `tag_user_id` adopted from EdgeTag). Omit it to read the user from the first-party cookie instead. Internal `gid_\u2026` ids are rejected.",
30905
- example: "ada@example.com"
30906
- }),
31251
+ identifier: readIdentifierField,
30907
31252
  cursor: external_exports.string().regex(/^\d+$/).optional().openapi({
30908
31253
  param: { name: "cursor", in: "query" },
30909
31254
  description: "The `nextCursor` of the previous page. Omit it for the first page.",
@@ -30956,11 +31301,7 @@ var person = createApiRouter().openapi(definition7, async (c) => {
30956
31301
 
30957
31302
  // src/paths/v1/properties.ts
30958
31303
  var propertiesQuery = external_exports.object({
30959
- identifier: external_exports.string().min(1).optional().openapi({
30960
- param: { name: "identifier", in: "query" },
30961
- description: "Canonical Qinto id (`anon_\u2026`) of the user to read. Omit it to resolve the user from the first-party cookie instead. Reading by a custom identifier arrives with authenticated API access.",
30962
- example: "anon_7b8d1a52-6c1e-4b6f-9a30-2f8a54d1c3e9"
30963
- })
31304
+ identifier: canonicalIdentifierField
30964
31305
  });
30965
31306
  var propertiesResponse = external_exports.record(external_exports.string(), external_exports.unknown()).openapi("UserProperties", { description: "Every property currently stored for the user, keyed by property name." });
30966
31307
  var definition8 = createRoute({
@@ -30971,10 +31312,7 @@ var definition8 = createRoute({
30971
31312
  security: [{ QintoWorkerAccessToken: [] }],
30972
31313
  middleware: [requireScope(SCOPE.propertiesRead)],
30973
31314
  summary: "Get user properties",
30974
- description: operationDescription({
30975
- intro: "Returns every property currently stored for the user.",
30976
- howItWorks: "A request that carries neither a `identifier` nor the cookie \u2014 or a `identifier` that is not canonical \u2014 fails with `invalid_params`."
30977
- }),
31315
+ description: operationDescription({ intro: "Returns every property currently stored for the user." }),
30978
31316
  request: { query: propertiesQuery },
30979
31317
  responses: {
30980
31318
  200: {
@@ -30997,11 +31335,7 @@ var properties = createApiRouter().openapi(definition8, async (c) => {
30997
31335
 
30998
31336
  // src/paths/v1/session.ts
30999
31337
  var sessionQuery = external_exports.object({
31000
- identifier: identifierField2.optional().openapi({
31001
- param: { name: "identifier", in: "query" },
31002
- description: "User to read: any identifier from your own systems (email, phone, custom id), or a canonical Qinto id (`anon_\u2026`). Omit it to read the user from the first-party cookie instead. Internal `gid_\u2026` ids are rejected.",
31003
- example: "ada@example.com"
31004
- })
31338
+ identifier: readIdentifierField
31005
31339
  });
31006
31340
  var sessionResponse = external_exports.object({
31007
31341
  anonId: external_exports.string().nullable().openapi({
@@ -31478,6 +31812,16 @@ var AliasDO = class extends DurableObject {
31478
31812
  return { exists: true, objectId: this.ctx.id.toString(), assignment, databaseSize: this.ctx.storage.sql.databaseSize };
31479
31813
  });
31480
31814
  }
31815
+ async erase(trace) {
31816
+ return runInvocation({ name: "do.alias.erase", trace }, async () => {
31817
+ if (await this.record() === void 0) {
31818
+ return false;
31819
+ }
31820
+ await this.ctx.storage.deleteAll();
31821
+ this.assignment = void 0;
31822
+ return true;
31823
+ });
31824
+ }
31481
31825
  async record() {
31482
31826
  if (!this.loaded) {
31483
31827
  this.assignment = await this.ctx.storage.get(ASSIGNMENT_KEY);
@@ -32254,6 +32598,19 @@ var relink = /* @__PURE__ */ __name(async (storage, env, anonId, globalId) => {
32254
32598
  await storage.setAlarm(cleanupTarget(storage, Date.now(), lastActivityAt(storage.kv) ?? Date.now()));
32255
32599
  }
32256
32600
  }, "relink");
32601
+ var unlinkFromPerson = /* @__PURE__ */ __name((env, anonId, globalId) => {
32602
+ return traced("rpc global.unlink_anon", { attrs: { "global.id": globalId } }, () => {
32603
+ return globalStub(env, globalId).unlinkAnon(anonId, currentTraceContext());
32604
+ });
32605
+ }, "unlinkFromPerson");
32606
+ var eraseAnon = /* @__PURE__ */ __name(async (storage, env) => {
32607
+ const anonId = storedAnonId(storage.kv);
32608
+ const globalId = linkedGlobalId(storage.kv);
32609
+ if (anonId !== null && globalId !== null) {
32610
+ await unlinkFromPerson(env, anonId, globalId);
32611
+ }
32612
+ await storage.deleteAll();
32613
+ }, "eraseAnon");
32257
32614
  var expireAnon = /* @__PURE__ */ __name(async (storage, env, staleActivity) => {
32258
32615
  if (await standsDown(storage, staleActivity)) {
32259
32616
  return false;
@@ -32262,9 +32619,7 @@ var expireAnon = /* @__PURE__ */ __name(async (storage, env, staleActivity) => {
32262
32619
  const globalId = linkedGlobalId(storage.kv);
32263
32620
  if (anonId !== null && globalId !== null) {
32264
32621
  try {
32265
- await traced("rpc global.unlink_anon", { attrs: { "global.id": globalId } }, () => {
32266
- return globalStub(env, globalId).unlinkAnon(anonId, currentTraceContext());
32267
- });
32622
+ await unlinkFromPerson(env, anonId, globalId);
32268
32623
  } catch (error61) {
32269
32624
  addSpanEvent("expiry unlink failed", { level: "warn", attrs: { "anon.id": anonId, "global.id": globalId, error: String(error61) } });
32270
32625
  await storage.setAlarm(Date.now() + CLEANUP_INTERVAL_MS);
@@ -32770,7 +33125,7 @@ var AnonDO = class extends DurableObject2 {
32770
33125
  wiped = false;
32771
33126
  // True while processAction holds ingestQueue, including the transform await.
32772
33127
  ingestTurn = false;
32773
- // Warm write-through mirrors reset together after expiry.
33128
+ // Warm write-through mirrors reset together after an expiry or an erase.
32774
33129
  kvCache;
32775
33130
  cachedStorage;
32776
33131
  identifiersMirror = identifiersCache();
@@ -33409,6 +33764,17 @@ var AnonDO = class extends DurableObject2 {
33409
33764
  });
33410
33765
  });
33411
33766
  }
33767
+ erasable(trace) {
33768
+ return runInvocation({ name: "do.anon.erasable", trace }, () => {
33769
+ return this.enqueue(async () => {
33770
+ if (await this.discardIfUnused()) {
33771
+ return null;
33772
+ }
33773
+ const identifiers = allIdentifiers(this.sql, this.identifiersMirror).map(({ type, value }) => ({ type, value }));
33774
+ return { globalId: linkedGlobalId(this.kv), identifiers };
33775
+ });
33776
+ });
33777
+ }
33412
33778
  inspect(trace) {
33413
33779
  return runInvocation({ name: "do.anon.inspect", trace }, () => {
33414
33780
  return this.enqueue(async () => {
@@ -33429,6 +33795,25 @@ var AnonDO = class extends DurableObject2 {
33429
33795
  });
33430
33796
  });
33431
33797
  }
33798
+ erase(trace) {
33799
+ return runInvocation({ name: "do.anon.erase", trace }, () => {
33800
+ return this.enqueue(async () => {
33801
+ if (await this.discardIfUnused()) {
33802
+ return false;
33803
+ }
33804
+ try {
33805
+ await eraseAnon(this.ctx.storage, this.graph);
33806
+ } finally {
33807
+ this.resetMirrors();
33808
+ }
33809
+ this.wiped = true;
33810
+ for (const socket of this.ctx.getWebSockets()) {
33811
+ socket.close(1e3, "erased");
33812
+ }
33813
+ return true;
33814
+ });
33815
+ });
33816
+ }
33432
33817
  // Addressing an object runs the constructor's schema exec, so a call that never writes — a read, an
33433
33818
  // inspect, a push or a snapshot for an id that was never used — would otherwise leave an empty object
33434
33819
  // behind that no cleanup alarm ever collects. Every write path seeds meta before it stores anything,
@@ -33668,16 +34053,19 @@ var AnonDO = class extends DurableObject2 {
33668
34053
  try {
33669
34054
  this.wiped = await expireAnon(this.ctx.storage, this.graph, staleActivity);
33670
34055
  } finally {
33671
- this.kvCache.clear();
33672
- this.cleanupAlarm = void 0;
33673
- this.identifiersMirror.reset();
33674
- this.conversionsMirror.reset();
33675
- this.customPropsMirror.reset();
33676
- this.captureMirror.reset();
33677
- this.contextMirror.reset();
34056
+ this.resetMirrors();
33678
34057
  }
33679
34058
  });
33680
34059
  }
34060
+ resetMirrors() {
34061
+ this.kvCache.clear();
34062
+ this.cleanupAlarm = void 0;
34063
+ this.identifiersMirror.reset();
34064
+ this.conversionsMirror.reset();
34065
+ this.customPropsMirror.reset();
34066
+ this.captureMirror.reset();
34067
+ this.contextMirror.reset();
34068
+ }
33681
34069
  revive() {
33682
34070
  if (this.wiped) {
33683
34071
  applyMigrations(this.ctx.storage, ANON_MIGRATIONS);
@@ -33919,7 +34307,9 @@ var GlobalDO = class extends DurableObject3 {
33919
34307
  }
33920
34308
  async unlinkAnon(anonId, trace) {
33921
34309
  return runInvocation({ name: "do.global.unlink_anon", trace, attrs: { "anon.id": anonId } }, async () => {
33922
- this.revive();
34310
+ if (await this.discardIfUnused()) {
34311
+ return;
34312
+ }
33923
34313
  unlinkAnon(this.sql, anonId);
33924
34314
  });
33925
34315
  }
@@ -33944,6 +34334,30 @@ var GlobalDO = class extends DurableObject3 {
33944
34334
  return linkedAnonsPage(this.sql, afterRowId, limit);
33945
34335
  });
33946
34336
  }
34337
+ async erasable(trace) {
34338
+ return runInvocation({ name: "do.global.erasable", trace }, async () => {
34339
+ if (await this.discardIfUnused()) {
34340
+ return null;
34341
+ }
34342
+ const identifiers = allIdentifiers2(this.sql, this.identifiersMirror).map(({ type, value }) => ({ type, value }));
34343
+ return { anonIds: allLinkedAnons(this.sql), identifiers };
34344
+ });
34345
+ }
34346
+ async erase(trace) {
34347
+ return runInvocation({ name: "do.global.erase", trace }, async () => {
34348
+ if (await this.discardIfUnused()) {
34349
+ return false;
34350
+ }
34351
+ try {
34352
+ await this.ctx.storage.deleteAll();
34353
+ } finally {
34354
+ this.kvCache.clear();
34355
+ this.identifiersMirror = identifiersCache2();
34356
+ }
34357
+ this.wiped = true;
34358
+ return true;
34359
+ });
34360
+ }
33947
34361
  async inspect(trace) {
33948
34362
  return runInvocation({ name: "do.global.inspect", trace }, async () => {
33949
34363
  if (await this.discardIfUnused()) {