@blotout/qinto-cli 0.1.22 → 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.
@@ -21663,6 +21663,7 @@ var MAX_TOOL_DATA_NODES = 5e3;
21663
21663
  var CONTROL_DEADLINES_MS = {
21664
21664
  noop: { platform: 15e3, site: 1e4 },
21665
21665
  lifecycle: { platform: 15e3, site: 1e4 },
21666
+ deleteRequest: { platform: 15e3, site: 1e4 },
21666
21667
  migration: { platform: 15e3, site: 1e4 },
21667
21668
  schedule: { platform: 65e3, site: 6e4 },
21668
21669
  tool: { platform: 55e3, site: 5e4 }
@@ -21704,6 +21705,14 @@ var lifecycleEventSchema = external_exports.discriminatedUnion("type", [
21704
21705
  }).strict(),
21705
21706
  external_exports.object({ ...lifecycleBase, type: external_exports.literal("uninstalled"), scope: external_exports.enum(["app", "install"]) }).strict()
21706
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();
21707
21716
  var migrationSchema = external_exports.discriminatedUnion("phase", [
21708
21717
  external_exports.object({
21709
21718
  resource: external_exports.string().min(1).max(64),
@@ -21766,10 +21775,11 @@ var toolSchema2 = external_exports.object({
21766
21775
  }, "Tool arguments exceed the size cap");
21767
21776
  var controlEnvelopeSchema = external_exports.object({
21768
21777
  v: external_exports.literal(CONTROL_ENVELOPE_VERSION),
21769
- kind: external_exports.enum(["noop", "lifecycle", "migration", "schedule", "tool"]),
21778
+ kind: external_exports.enum(["noop", "lifecycle", "deleteRequest", "migration", "schedule", "tool"]),
21770
21779
  app: external_exports.object({ slug: external_exports.string().min(1).max(30), version: external_exports.string().min(1).max(32) }).strict(),
21771
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(),
21772
21781
  lifecycle: lifecycleEventSchema.optional(),
21782
+ deleteRequest: deleteRequestSchema.optional(),
21773
21783
  migration: migrationSchema.optional(),
21774
21784
  schedule: scheduleSchema2.optional(),
21775
21785
  tool: toolSchema2.optional()
@@ -21777,6 +21787,9 @@ var controlEnvelopeSchema = external_exports.object({
21777
21787
  if (envelope.kind === "lifecycle" !== (envelope.lifecycle !== void 0)) {
21778
21788
  ctx.addIssue({ code: "custom", path: ["lifecycle"], message: "Lifecycle payload must match the control kind" });
21779
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
+ }
21780
21793
  if (envelope.kind === "migration" !== (envelope.migration !== void 0)) {
21781
21794
  ctx.addIssue({ code: "custom", path: ["migration"], message: "Migration payload must match the control kind" });
21782
21795
  }
@@ -21786,8 +21799,8 @@ var controlEnvelopeSchema = external_exports.object({
21786
21799
  if (envelope.kind === "tool" !== (envelope.tool !== void 0)) {
21787
21800
  ctx.addIssue({ code: "custom", path: ["tool"], message: "Tool payload must match the control kind" });
21788
21801
  }
21789
- if (envelope.kind === "lifecycle" && envelope.install === void 0) {
21790
- 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` });
21791
21804
  }
21792
21805
  if (envelope.kind === "tool" && envelope.install === void 0) {
21793
21806
  ctx.addIssue({ code: "custom", path: ["install"], message: "Tool control needs an install" });
@@ -21826,6 +21839,8 @@ var toolResultSchema = external_exports.object({
21826
21839
  var controlResponseSchema = external_exports.object({
21827
21840
  v: external_exports.literal(CONTROL_ENVELOPE_VERSION),
21828
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(),
21829
21844
  tool: toolResultSchema.optional(),
21830
21845
  migration: external_exports.union([
21831
21846
  external_exports.object({ done: external_exports.literal(true) }).strict(),
@@ -26323,6 +26338,16 @@ var identifierField2 = external_exports.string().trim().min(1).refine((value) =>
26323
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.",
26324
26339
  example: "crm-user-8412"
26325
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
+ });
26326
26351
  var consentCategoriesField = external_exports.object({
26327
26352
  // Necessary consent cannot be opted out.
26328
26353
  necessary: external_exports.literal(true).optional().openapi({
@@ -27150,6 +27175,9 @@ var linkedAnonsPage = /* @__PURE__ */ __name((sql, afterRowId, limit) => {
27150
27175
  var linkedAnonsExcept = /* @__PURE__ */ __name((sql, originAnonId) => {
27151
27176
  return sql.exec("SELECT key FROM facts WHERE kind = 'anon' AND key != ?", originAnonId).toArray().map((row) => row.key);
27152
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");
27153
27181
 
27154
27182
  // src/id-graph/person.ts
27155
27183
  var IDENTITY_CAPABILITY = "identity";
@@ -27227,6 +27255,124 @@ var identityFields = {
27227
27255
  key: external_exports.string().min(1)
27228
27256
  };
27229
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
+
27230
27376
  // src/apps/emit-payload.ts
27231
27377
  var emitPayloadSchema = external_exports.object({
27232
27378
  event: external_exports.string().min(1),
@@ -28029,6 +28175,7 @@ var brokerBody = external_exports.discriminatedUnion("kind", [
28029
28175
  contextWriteBody,
28030
28176
  factsReadBody,
28031
28177
  identityReadBody,
28178
+ currencyRatesBody,
28032
28179
  trackBody
28033
28180
  ]);
28034
28181
  var BODY_ENVELOPE_ALLOWANCE_BYTES = 2 * 1024;
@@ -28364,7 +28511,7 @@ var handleActive = /* @__PURE__ */ __name(async (env, body) => {
28364
28511
  }
28365
28512
  return Response.json({ active: true });
28366
28513
  }, "handleActive");
28367
- var handleBroker = /* @__PURE__ */ __name(async (request, env) => {
28514
+ var handleBroker = /* @__PURE__ */ __name(async (request, env, ctx) => {
28368
28515
  const bytes = await readBoundedBody(request, MAX_BODY_BYTES);
28369
28516
  if (bytes === null) {
28370
28517
  return Response.json({ error: "too_large" }, { status: 413 });
@@ -28416,6 +28563,9 @@ var handleBroker = /* @__PURE__ */ __name(async (request, env) => {
28416
28563
  case "identityRead": {
28417
28564
  return handleIdentityRead(env, body);
28418
28565
  }
28566
+ case "currencyRates": {
28567
+ return handleCurrencyRates(ctx);
28568
+ }
28419
28569
  case "track": {
28420
28570
  return handleTrack(env, body);
28421
28571
  }
@@ -28426,7 +28576,7 @@ var dispatchGated = /* @__PURE__ */ __name((c) => {
28426
28576
  if (c.env.DEPLOY_TARGET !== "dispatch") {
28427
28577
  return c.notFound();
28428
28578
  }
28429
- return handleBroker(c.req.raw, c.env);
28579
+ return handleBroker(c.req.raw, c.env, c.executionCtx);
28430
28580
  }, "dispatchGated");
28431
28581
  for (const path of BROKER_PATHS) {
28432
28582
  brokerRoutes.post(path, dispatchGated);
@@ -28438,7 +28588,7 @@ var Broker = class extends WorkerEntrypoint {
28438
28588
  fetch(request) {
28439
28589
  const trace = parseTraceContext(request.headers.get(TRACE_HEADER));
28440
28590
  return runInvocation({ name: "broker", trace }, () => {
28441
- return handleBroker(request, this.env);
28591
+ return handleBroker(request, this.env, this.ctx);
28442
28592
  });
28443
28593
  }
28444
28594
  };
@@ -29439,20 +29589,17 @@ var matchEndpoint = /* @__PURE__ */ __name((app2, path, method) => {
29439
29589
  if (!isHttpMethod(method)) {
29440
29590
  return void 0;
29441
29591
  }
29442
- const serving = app2.endpoints.filter((endpoint) => {
29443
- return endpoint.methods.includes(method);
29444
- });
29445
29592
  const matched = matchEndpointPath(
29446
- serving.map((endpoint) => endpoint.path),
29593
+ app2.endpoints.map((endpoint) => endpoint.path),
29447
29594
  path
29448
29595
  );
29449
29596
  if (matched === void 0) {
29450
29597
  return void 0;
29451
29598
  }
29452
- const declared = serving.find((endpoint) => {
29599
+ const declared = app2.endpoints.find((endpoint) => {
29453
29600
  return endpoint.path === matched.route;
29454
29601
  });
29455
- 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") {
29456
29603
  return void 0;
29457
29604
  }
29458
29605
  return {
@@ -30058,7 +30205,7 @@ appUiRoutes.all("/apps/:slug/ui/*", (c) => {
30058
30205
  var apiInfo = {
30059
30206
  title: "Qinto Edge API",
30060
30207
  version: "0.1.0",
30061
- 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" }`.'
30062
30209
  };
30063
30210
  var apiTags = [
30064
30211
  {
@@ -30106,7 +30253,7 @@ var ERRORS = {
30106
30253
  },
30107
30254
  internal: {
30108
30255
  status: 500,
30109
- 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."
30110
30257
  }
30111
30258
  };
30112
30259
  var DETAILS_SLUGS = /* @__PURE__ */ new Set(["invalid_params", "invalid_body"]);
@@ -30363,6 +30510,140 @@ var health = createApiRouter().openapi(definition, (c) => {
30363
30510
  return respond(c, healthResponse, { status: "success" });
30364
30511
  });
30365
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
+
30366
30647
  // src/id-graph/inspect.ts
30367
30648
  var MAX_TABLE_PAGE = 100;
30368
30649
  var INTERNAL_TABLE = /^(sqlite_|_cf_)/;
@@ -30487,6 +30768,7 @@ var rowsBody = inspectBody.extend({
30487
30768
  offset: external_exports.number().int().min(0).default(0),
30488
30769
  limit: external_exports.number().int().min(1).max(MAX_TABLE_PAGE).default(MAX_TABLE_PAGE)
30489
30770
  });
30771
+ var valueBody = external_exports.object({ value: external_exports.string().min(1) });
30490
30772
  var readBody = /* @__PURE__ */ __name(async (c, schema) => {
30491
30773
  const parsed = schema.safeParse(await c.req.json().catch(() => null));
30492
30774
  return parsed.success ? parsed.data : null;
@@ -30514,6 +30796,20 @@ idGraphInspect.post("/internal/id-graph/rows", async (c) => {
30514
30796
  }
30515
30797
  return c.json({ page: await inspectObjectTable(c.env, body) });
30516
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
+ });
30517
30813
 
30518
30814
  // src/paths/open.ts
30519
30815
  var withSetCookie = /* @__PURE__ */ __name((res, cookies) => {
@@ -30601,14 +30897,14 @@ var open2 = /* @__PURE__ */ __name(async (c) => {
30601
30897
 
30602
30898
  // browser-artifact/meta.json
30603
30899
  var meta_default = {
30604
- version: "0.1.10",
30605
- gitSha: "f21182929c88fa0748f575164bde781b31f4fbdd",
30606
- builtAt: "2026-09-23T15:34:39.048Z",
30607
- hash: "d1804e7914363626fed0280696cad5e06728b899e23c8a5ee6dcced3b67cf831"
30900
+ version: "0.1.11",
30901
+ gitSha: "bb87bca6d3b96627f24bc0c29675f2f19680ce60",
30902
+ builtAt: "2026-09-28T04:35:01.606Z",
30903
+ hash: "e32dfe041826d6daaa3682d1f5cac137ffcc6a24f55bfef1a936f7e3ef4cde12"
30608
30904
  };
30609
30905
 
30610
30906
  // src/paths/q-js.ts
30611
- import qScript from "./7bc13565a6fa9d520d0ebed788d11591e8db5b44-q.js.txt";
30907
+ import qScript from "./944c722e73fe144b1067da970f87716a835c697e-q.js.txt";
30612
30908
 
30613
30909
  // src/apps-q.ts
30614
30910
  import appsQScript from "./apps-q.json";
@@ -30854,11 +31150,7 @@ var consent = createApiRouter().openapi(definition4, async (c) => {
30854
31150
 
30855
31151
  // src/paths/v1/consent-state.ts
30856
31152
  var consentStateQuery = external_exports.object({
30857
- identifier: external_exports.string().min(1).optional().openapi({
30858
- param: { name: "identifier", in: "query" },
30859
- 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.",
30860
- example: "anon_7b8d1a52-6c1e-4b6f-9a30-2f8a54d1c3e9"
30861
- })
31153
+ identifier: canonicalIdentifierField
30862
31154
  });
30863
31155
  var consentStateResponse = external_exports.object({
30864
31156
  categories: external_exports.object({
@@ -30885,7 +31177,7 @@ var definition5 = createRoute({
30885
31177
  summary: "Get user consent",
30886
31178
  description: operationDescription({
30887
31179
  intro: "Returns the consent state currently stored for the user.",
30888
- 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."
30889
31181
  }),
30890
31182
  request: { query: consentStateQuery },
30891
31183
  responses: {
@@ -30956,11 +31248,7 @@ var identify = createApiRouter().openapi(definition6, async (c) => {
30956
31248
 
30957
31249
  // src/paths/v1/person.ts
30958
31250
  var personQuery = external_exports.object({
30959
- identifier: identifierField2.optional().openapi({
30960
- param: { name: "identifier", in: "query" },
30961
- 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.",
30962
- example: "ada@example.com"
30963
- }),
31251
+ identifier: readIdentifierField,
30964
31252
  cursor: external_exports.string().regex(/^\d+$/).optional().openapi({
30965
31253
  param: { name: "cursor", in: "query" },
30966
31254
  description: "The `nextCursor` of the previous page. Omit it for the first page.",
@@ -31013,11 +31301,7 @@ var person = createApiRouter().openapi(definition7, async (c) => {
31013
31301
 
31014
31302
  // src/paths/v1/properties.ts
31015
31303
  var propertiesQuery = external_exports.object({
31016
- identifier: external_exports.string().min(1).optional().openapi({
31017
- param: { name: "identifier", in: "query" },
31018
- 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.",
31019
- example: "anon_7b8d1a52-6c1e-4b6f-9a30-2f8a54d1c3e9"
31020
- })
31304
+ identifier: canonicalIdentifierField
31021
31305
  });
31022
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." });
31023
31307
  var definition8 = createRoute({
@@ -31028,10 +31312,7 @@ var definition8 = createRoute({
31028
31312
  security: [{ QintoWorkerAccessToken: [] }],
31029
31313
  middleware: [requireScope(SCOPE.propertiesRead)],
31030
31314
  summary: "Get user properties",
31031
- description: operationDescription({
31032
- intro: "Returns every property currently stored for the user.",
31033
- howItWorks: "A request that carries neither a `identifier` nor the cookie \u2014 or a `identifier` that is not canonical \u2014 fails with `invalid_params`."
31034
- }),
31315
+ description: operationDescription({ intro: "Returns every property currently stored for the user." }),
31035
31316
  request: { query: propertiesQuery },
31036
31317
  responses: {
31037
31318
  200: {
@@ -31054,11 +31335,7 @@ var properties = createApiRouter().openapi(definition8, async (c) => {
31054
31335
 
31055
31336
  // src/paths/v1/session.ts
31056
31337
  var sessionQuery = external_exports.object({
31057
- identifier: identifierField2.optional().openapi({
31058
- param: { name: "identifier", in: "query" },
31059
- 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.",
31060
- example: "ada@example.com"
31061
- })
31338
+ identifier: readIdentifierField
31062
31339
  });
31063
31340
  var sessionResponse = external_exports.object({
31064
31341
  anonId: external_exports.string().nullable().openapi({
@@ -31535,6 +31812,16 @@ var AliasDO = class extends DurableObject {
31535
31812
  return { exists: true, objectId: this.ctx.id.toString(), assignment, databaseSize: this.ctx.storage.sql.databaseSize };
31536
31813
  });
31537
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
+ }
31538
31825
  async record() {
31539
31826
  if (!this.loaded) {
31540
31827
  this.assignment = await this.ctx.storage.get(ASSIGNMENT_KEY);
@@ -32311,6 +32598,19 @@ var relink = /* @__PURE__ */ __name(async (storage, env, anonId, globalId) => {
32311
32598
  await storage.setAlarm(cleanupTarget(storage, Date.now(), lastActivityAt(storage.kv) ?? Date.now()));
32312
32599
  }
32313
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");
32314
32614
  var expireAnon = /* @__PURE__ */ __name(async (storage, env, staleActivity) => {
32315
32615
  if (await standsDown(storage, staleActivity)) {
32316
32616
  return false;
@@ -32319,9 +32619,7 @@ var expireAnon = /* @__PURE__ */ __name(async (storage, env, staleActivity) => {
32319
32619
  const globalId = linkedGlobalId(storage.kv);
32320
32620
  if (anonId !== null && globalId !== null) {
32321
32621
  try {
32322
- await traced("rpc global.unlink_anon", { attrs: { "global.id": globalId } }, () => {
32323
- return globalStub(env, globalId).unlinkAnon(anonId, currentTraceContext());
32324
- });
32622
+ await unlinkFromPerson(env, anonId, globalId);
32325
32623
  } catch (error61) {
32326
32624
  addSpanEvent("expiry unlink failed", { level: "warn", attrs: { "anon.id": anonId, "global.id": globalId, error: String(error61) } });
32327
32625
  await storage.setAlarm(Date.now() + CLEANUP_INTERVAL_MS);
@@ -32827,7 +33125,7 @@ var AnonDO = class extends DurableObject2 {
32827
33125
  wiped = false;
32828
33126
  // True while processAction holds ingestQueue, including the transform await.
32829
33127
  ingestTurn = false;
32830
- // Warm write-through mirrors reset together after expiry.
33128
+ // Warm write-through mirrors reset together after an expiry or an erase.
32831
33129
  kvCache;
32832
33130
  cachedStorage;
32833
33131
  identifiersMirror = identifiersCache();
@@ -33466,6 +33764,17 @@ var AnonDO = class extends DurableObject2 {
33466
33764
  });
33467
33765
  });
33468
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
+ }
33469
33778
  inspect(trace) {
33470
33779
  return runInvocation({ name: "do.anon.inspect", trace }, () => {
33471
33780
  return this.enqueue(async () => {
@@ -33486,6 +33795,25 @@ var AnonDO = class extends DurableObject2 {
33486
33795
  });
33487
33796
  });
33488
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
+ }
33489
33817
  // Addressing an object runs the constructor's schema exec, so a call that never writes — a read, an
33490
33818
  // inspect, a push or a snapshot for an id that was never used — would otherwise leave an empty object
33491
33819
  // behind that no cleanup alarm ever collects. Every write path seeds meta before it stores anything,
@@ -33725,16 +34053,19 @@ var AnonDO = class extends DurableObject2 {
33725
34053
  try {
33726
34054
  this.wiped = await expireAnon(this.ctx.storage, this.graph, staleActivity);
33727
34055
  } finally {
33728
- this.kvCache.clear();
33729
- this.cleanupAlarm = void 0;
33730
- this.identifiersMirror.reset();
33731
- this.conversionsMirror.reset();
33732
- this.customPropsMirror.reset();
33733
- this.captureMirror.reset();
33734
- this.contextMirror.reset();
34056
+ this.resetMirrors();
33735
34057
  }
33736
34058
  });
33737
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
+ }
33738
34069
  revive() {
33739
34070
  if (this.wiped) {
33740
34071
  applyMigrations(this.ctx.storage, ANON_MIGRATIONS);
@@ -33976,7 +34307,9 @@ var GlobalDO = class extends DurableObject3 {
33976
34307
  }
33977
34308
  async unlinkAnon(anonId, trace) {
33978
34309
  return runInvocation({ name: "do.global.unlink_anon", trace, attrs: { "anon.id": anonId } }, async () => {
33979
- this.revive();
34310
+ if (await this.discardIfUnused()) {
34311
+ return;
34312
+ }
33980
34313
  unlinkAnon(this.sql, anonId);
33981
34314
  });
33982
34315
  }
@@ -34001,6 +34334,30 @@ var GlobalDO = class extends DurableObject3 {
34001
34334
  return linkedAnonsPage(this.sql, afterRowId, limit);
34002
34335
  });
34003
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
+ }
34004
34361
  async inspect(trace) {
34005
34362
  return runInvocation({ name: "do.global.inspect", trace }, async () => {
34006
34363
  if (await this.discardIfUnused()) {