@blotout/qinto-cli 0.1.11 → 0.1.13
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.
- package/README.md +95 -0
- package/dist/index.js +4156 -3496
- package/dist/index.js.map +1 -1
- package/dist/worker-artifact/main/index.js +318 -174
- package/dist/worker-artifact/version.json +4 -4
- package/dist/worker-artifact/wfp/index.js +17 -2
- package/package.json +2 -2
|
@@ -570,6 +570,92 @@ var appendSpan = /* @__PURE__ */ __name((span) => {
|
|
|
570
570
|
scope.buffer.spans.push(span);
|
|
571
571
|
}, "appendSpan");
|
|
572
572
|
|
|
573
|
+
// ../../node_modules/.pnpm/hono@4.12.28/node_modules/hono/dist/http-exception.js
|
|
574
|
+
var HTTPException = class extends Error {
|
|
575
|
+
static {
|
|
576
|
+
__name(this, "HTTPException");
|
|
577
|
+
}
|
|
578
|
+
res;
|
|
579
|
+
status;
|
|
580
|
+
/**
|
|
581
|
+
* Creates an instance of `HTTPException`.
|
|
582
|
+
* @param status - HTTP status code for the exception. Defaults to 500.
|
|
583
|
+
* @param options - Additional options for the exception.
|
|
584
|
+
*/
|
|
585
|
+
constructor(status = 500, options) {
|
|
586
|
+
super(options?.message, { cause: options?.cause });
|
|
587
|
+
this.res = options?.res;
|
|
588
|
+
this.status = status;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Returns the response object associated with the exception.
|
|
592
|
+
* If a response object is not provided, a new response is created with the error message and status code.
|
|
593
|
+
* @returns The response object.
|
|
594
|
+
*/
|
|
595
|
+
getResponse() {
|
|
596
|
+
if (this.res) {
|
|
597
|
+
const newResponse = new Response(this.res.body, {
|
|
598
|
+
status: this.status,
|
|
599
|
+
headers: this.res.headers
|
|
600
|
+
});
|
|
601
|
+
return newResponse;
|
|
602
|
+
}
|
|
603
|
+
return new Response(this.message, {
|
|
604
|
+
status: this.status
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// ../../node_modules/.pnpm/hono@4.12.28/node_modules/hono/dist/middleware/body-limit/index.js
|
|
610
|
+
var ERROR_MESSAGE = "Payload Too Large";
|
|
611
|
+
var bodyLimit = /* @__PURE__ */ __name((options) => {
|
|
612
|
+
const onError = options.onError || (() => {
|
|
613
|
+
const res = new Response(ERROR_MESSAGE, {
|
|
614
|
+
status: 413
|
|
615
|
+
});
|
|
616
|
+
throw new HTTPException(413, { res });
|
|
617
|
+
});
|
|
618
|
+
const maxSize = options.maxSize;
|
|
619
|
+
return /* @__PURE__ */ __name(async function bodyLimit2(c, next) {
|
|
620
|
+
if (!c.req.raw.body) {
|
|
621
|
+
return next();
|
|
622
|
+
}
|
|
623
|
+
const hasTransferEncoding = c.req.raw.headers.has("transfer-encoding");
|
|
624
|
+
const hasContentLength = c.req.raw.headers.has("content-length");
|
|
625
|
+
if (hasContentLength && !hasTransferEncoding) {
|
|
626
|
+
const contentLength = parseInt(c.req.raw.headers.get("content-length") || "0", 10);
|
|
627
|
+
return contentLength > maxSize ? onError(c) : next();
|
|
628
|
+
}
|
|
629
|
+
let size = 0;
|
|
630
|
+
const chunks = [];
|
|
631
|
+
const rawReader = c.req.raw.body.getReader();
|
|
632
|
+
for (; ; ) {
|
|
633
|
+
const { done, value } = await rawReader.read();
|
|
634
|
+
if (done) {
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
size += value.length;
|
|
638
|
+
if (size > maxSize) {
|
|
639
|
+
return onError(c);
|
|
640
|
+
}
|
|
641
|
+
chunks.push(value);
|
|
642
|
+
}
|
|
643
|
+
const requestInit = {
|
|
644
|
+
body: new ReadableStream({
|
|
645
|
+
start(controller) {
|
|
646
|
+
for (const chunk of chunks) {
|
|
647
|
+
controller.enqueue(chunk);
|
|
648
|
+
}
|
|
649
|
+
controller.close();
|
|
650
|
+
}
|
|
651
|
+
}),
|
|
652
|
+
duplex: "half"
|
|
653
|
+
};
|
|
654
|
+
c.req.raw = new Request(c.req.raw, requestInit);
|
|
655
|
+
return next();
|
|
656
|
+
}, "bodyLimit2");
|
|
657
|
+
}, "bodyLimit");
|
|
658
|
+
|
|
573
659
|
// ../../node_modules/.pnpm/hono@4.12.28/node_modules/hono/dist/middleware/cors/index.js
|
|
574
660
|
var cors = /* @__PURE__ */ __name((options) => {
|
|
575
661
|
const opts = {
|
|
@@ -653,42 +739,6 @@ var cors = /* @__PURE__ */ __name((options) => {
|
|
|
653
739
|
}, "cors2");
|
|
654
740
|
}, "cors");
|
|
655
741
|
|
|
656
|
-
// ../../node_modules/.pnpm/hono@4.12.28/node_modules/hono/dist/http-exception.js
|
|
657
|
-
var HTTPException = class extends Error {
|
|
658
|
-
static {
|
|
659
|
-
__name(this, "HTTPException");
|
|
660
|
-
}
|
|
661
|
-
res;
|
|
662
|
-
status;
|
|
663
|
-
/**
|
|
664
|
-
* Creates an instance of `HTTPException`.
|
|
665
|
-
* @param status - HTTP status code for the exception. Defaults to 500.
|
|
666
|
-
* @param options - Additional options for the exception.
|
|
667
|
-
*/
|
|
668
|
-
constructor(status = 500, options) {
|
|
669
|
-
super(options?.message, { cause: options?.cause });
|
|
670
|
-
this.res = options?.res;
|
|
671
|
-
this.status = status;
|
|
672
|
-
}
|
|
673
|
-
/**
|
|
674
|
-
* Returns the response object associated with the exception.
|
|
675
|
-
* If a response object is not provided, a new response is created with the error message and status code.
|
|
676
|
-
* @returns The response object.
|
|
677
|
-
*/
|
|
678
|
-
getResponse() {
|
|
679
|
-
if (this.res) {
|
|
680
|
-
const newResponse = new Response(this.res.body, {
|
|
681
|
-
status: this.status,
|
|
682
|
-
headers: this.res.headers
|
|
683
|
-
});
|
|
684
|
-
return newResponse;
|
|
685
|
-
}
|
|
686
|
-
return new Response(this.message, {
|
|
687
|
-
status: this.status
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
|
-
};
|
|
691
|
-
|
|
692
742
|
// src/apps/broker.ts
|
|
693
743
|
import { timingSafeEqual } from "node:crypto";
|
|
694
744
|
import { WorkerEntrypoint } from "cloudflare:workers";
|
|
@@ -19281,6 +19331,7 @@ var adoptAppSpans = /* @__PURE__ */ __name((response) => {
|
|
|
19281
19331
|
|
|
19282
19332
|
// src/apps/invoke.ts
|
|
19283
19333
|
var APP_INVOCATION_TIMEOUT_MS = 1e4;
|
|
19334
|
+
var APP_HOOK_TIMEOUT_MS = 25e3;
|
|
19284
19335
|
var MAX_EMIT_DEPTH = 2;
|
|
19285
19336
|
var HOOK_URL = "https://app/__qinto/hook";
|
|
19286
19337
|
var invocationContext = /* @__PURE__ */ __name((env) => {
|
|
@@ -19425,7 +19476,7 @@ var ingestEmit = /* @__PURE__ */ __name(async (env, { slug, ord, depth, emit, an
|
|
|
19425
19476
|
});
|
|
19426
19477
|
await ingest(env, action);
|
|
19427
19478
|
}, "ingestEmit");
|
|
19428
|
-
var deliverGroupPushes = /* @__PURE__ */ __name(({ slug, group, installs, deliverPush }) => {
|
|
19479
|
+
var deliverGroupPushes = /* @__PURE__ */ __name(async ({ slug, group, installs, deliverPush }) => {
|
|
19429
19480
|
const install = installs.find((entry) => entry.ord === group.ord);
|
|
19430
19481
|
for (const push of group.pushes) {
|
|
19431
19482
|
const { topic, data } = push;
|
|
@@ -19435,17 +19486,17 @@ var deliverGroupPushes = /* @__PURE__ */ __name(({ slug, group, installs, delive
|
|
|
19435
19486
|
addSpanEvent("push rejected: its group matches no invoked install", { level: "warn", attrs: { app: slug, ord: group.ord } });
|
|
19436
19487
|
break;
|
|
19437
19488
|
}
|
|
19438
|
-
deliverPush({ slug, installId: install.id, topic, data, scope: "app" });
|
|
19489
|
+
await deliverPush({ slug, installId: install.id, topic, data, scope: "app" });
|
|
19439
19490
|
break;
|
|
19440
19491
|
}
|
|
19441
19492
|
case "app": {
|
|
19442
|
-
deliverPush({ slug, installId: null, topic, data, scope: "app" });
|
|
19493
|
+
await deliverPush({ slug, installId: null, topic, data, scope: "app" });
|
|
19443
19494
|
break;
|
|
19444
19495
|
}
|
|
19445
19496
|
// Whether this app may broadcast site-wide is settled in `deliverAppPush`, the one path every
|
|
19446
19497
|
// delivery funnels through; re-deciding it here would be a second authority to keep in sync.
|
|
19447
19498
|
case "site": {
|
|
19448
|
-
deliverPush({ slug, installId: null, topic, data, scope: "site" });
|
|
19499
|
+
await deliverPush({ slug, installId: null, topic, data, scope: "site" });
|
|
19449
19500
|
break;
|
|
19450
19501
|
}
|
|
19451
19502
|
}
|
|
@@ -19461,7 +19512,7 @@ var applyEffects = /* @__PURE__ */ __name(async (env, { slug, groups, depth, ano
|
|
|
19461
19512
|
await writeContext2(slug, contextWrites);
|
|
19462
19513
|
}
|
|
19463
19514
|
for (const group of groups) {
|
|
19464
|
-
deliverGroupPushes({ slug, group, installs, deliverPush });
|
|
19515
|
+
await deliverGroupPushes({ slug, group, installs, deliverPush });
|
|
19465
19516
|
if (group.emits.length === 0) {
|
|
19466
19517
|
continue;
|
|
19467
19518
|
}
|
|
@@ -19486,14 +19537,14 @@ var hookResponseBody = /* @__PURE__ */ __name(async (response, slug) => {
|
|
|
19486
19537
|
}
|
|
19487
19538
|
return new TextDecoder().decode(bytes);
|
|
19488
19539
|
}, "hookResponseBody");
|
|
19489
|
-
var runInvokeApp = /* @__PURE__ */ __name(async (env, { target, trigger, snapshot, context,
|
|
19540
|
+
var runInvokeApp = /* @__PURE__ */ __name(async (env, { target, trigger, snapshot, context, deliverEffects, budget }) => {
|
|
19490
19541
|
try {
|
|
19491
19542
|
const key = await deriveAppInternalKey(context.master, context.domainId, target.slug);
|
|
19492
19543
|
const response = await postHookEnvelope(env, {
|
|
19493
19544
|
binding: target.app.binding,
|
|
19494
19545
|
key,
|
|
19495
19546
|
envelope: buildEnvelope(target, trigger, snapshot, context),
|
|
19496
|
-
timeoutMs:
|
|
19547
|
+
timeoutMs: APP_HOOK_TIMEOUT_MS
|
|
19497
19548
|
});
|
|
19498
19549
|
if (response === void 0) {
|
|
19499
19550
|
addSpanEvent("app service binding is missing", { level: "warn", attrs: { app: target.slug, binding: target.app.binding } });
|
|
@@ -19520,15 +19571,15 @@ var runInvokeApp = /* @__PURE__ */ __name(async (env, { target, trigger, snapsho
|
|
|
19520
19571
|
for (const ord of verdict.effects.rejectedOrds) {
|
|
19521
19572
|
addSpanEvent("app effects rejected over the per-install caps", { level: "warn", attrs: { app: target.slug, ord } });
|
|
19522
19573
|
}
|
|
19523
|
-
|
|
19574
|
+
if (verdict.effects.accepted.length === 0) {
|
|
19575
|
+
return;
|
|
19576
|
+
}
|
|
19577
|
+
await deliverEffects({
|
|
19578
|
+
anonId: snapshot.anonId,
|
|
19524
19579
|
slug: target.slug,
|
|
19525
19580
|
groups: verdict.effects.accepted,
|
|
19526
19581
|
depth: triggerDepth(trigger),
|
|
19527
|
-
anonId: snapshot.anonId,
|
|
19528
19582
|
installs: target.installs,
|
|
19529
|
-
deliverPush,
|
|
19530
|
-
writeAttribution: writeAttribution2,
|
|
19531
|
-
writeContext: writeContext2,
|
|
19532
19583
|
geo: trigger.kind === "event" ? trigger.action.geo : trigger.geo,
|
|
19533
19584
|
host: trigger.kind === "event" ? trigger.action.host : trigger.host,
|
|
19534
19585
|
context: trigger.kind === "event" ? trigger.action.context : void 0
|
|
@@ -19835,17 +19886,15 @@ var deliverRpcEffects = /* @__PURE__ */ __name(async (env, { body, userId, effec
|
|
|
19835
19886
|
for (const ord of effects.rejectedOrds) {
|
|
19836
19887
|
addSpanEvent("app effects rejected over the per-install caps", { level: "warn", attrs: { app: body.callee, ord } });
|
|
19837
19888
|
}
|
|
19838
|
-
const pushes = [];
|
|
19839
19889
|
await applyEffects(env, {
|
|
19840
19890
|
slug: body.callee,
|
|
19841
19891
|
groups: effects.accepted,
|
|
19842
19892
|
depth: body.depth,
|
|
19843
19893
|
anonId: userId,
|
|
19844
19894
|
installs: [install],
|
|
19845
|
-
deliverPush: /* @__PURE__ */ __name((push) => {
|
|
19846
|
-
|
|
19895
|
+
deliverPush: /* @__PURE__ */ __name(async (push) => {
|
|
19896
|
+
await anonStub(env, userId).appPush(push, currentTraceContext());
|
|
19847
19897
|
}, "deliverPush"),
|
|
19848
|
-
// The broker runs outside the user's AnonDO, so this reaches it the same way a push does.
|
|
19849
19898
|
writeAttribution: /* @__PURE__ */ __name(async (slug, writes) => {
|
|
19850
19899
|
await anonStub(env, userId).appAttribution({ slug, writes }, currentTraceContext());
|
|
19851
19900
|
}, "writeAttribution"),
|
|
@@ -19853,9 +19902,6 @@ var deliverRpcEffects = /* @__PURE__ */ __name(async (env, { body, userId, effec
|
|
|
19853
19902
|
await anonStub(env, userId).appContext({ slug, writes }, currentTraceContext());
|
|
19854
19903
|
}, "writeContext")
|
|
19855
19904
|
});
|
|
19856
|
-
for (const push of pushes) {
|
|
19857
|
-
await anonStub(env, userId).appPush(push, currentTraceContext());
|
|
19858
|
-
}
|
|
19859
19905
|
}, "deliverRpcEffects");
|
|
19860
19906
|
var runRelayRpc = /* @__PURE__ */ __name(async (env, { context, body, callee, install }) => {
|
|
19861
19907
|
let raw2;
|
|
@@ -24717,6 +24763,94 @@ var track = createApiRouter().openapi(definition8, async (c) => {
|
|
|
24717
24763
|
}
|
|
24718
24764
|
});
|
|
24719
24765
|
|
|
24766
|
+
// src/apps/fan-out-entrypoint.ts
|
|
24767
|
+
import { WorkerEntrypoint as WorkerEntrypoint2 } from "cloudflare:workers";
|
|
24768
|
+
|
|
24769
|
+
// src/apps/eligibility.ts
|
|
24770
|
+
var wantsTrigger = /* @__PURE__ */ __name((app2, trigger) => {
|
|
24771
|
+
if (trigger.kind === "connect") {
|
|
24772
|
+
return app2.hooks.onConnect;
|
|
24773
|
+
}
|
|
24774
|
+
return app2.hooks.onEvent.includes(trigger.action.type);
|
|
24775
|
+
}, "wantsTrigger");
|
|
24776
|
+
var gatePasses = /* @__PURE__ */ __name((entry, trigger, consent2, enforced) => {
|
|
24777
|
+
if (trigger.kind === "event" && trigger.action.origin?.app === entry.slug) {
|
|
24778
|
+
return false;
|
|
24779
|
+
}
|
|
24780
|
+
if (trigger.kind === "event" && trigger.action.type === "consent") {
|
|
24781
|
+
return true;
|
|
24782
|
+
}
|
|
24783
|
+
return consentAllows(consent2, entry.slug, entry.app.consent.category, enforced);
|
|
24784
|
+
}, "gatePasses");
|
|
24785
|
+
var eligibleApps = /* @__PURE__ */ __name((registry3, trigger, consent2, enforced) => {
|
|
24786
|
+
const targets = [];
|
|
24787
|
+
for (const slug of registry3.order) {
|
|
24788
|
+
const app2 = registry3.apps[slug];
|
|
24789
|
+
if (app2 === void 0 || !wantsTrigger(app2, trigger) || !gatePasses({ slug, app: app2 }, trigger, consent2, enforced)) {
|
|
24790
|
+
continue;
|
|
24791
|
+
}
|
|
24792
|
+
const installs = app2.installs.filter((install) => install.active).map(({ id, ord, label }) => ({ id, ord, label }));
|
|
24793
|
+
if (installs.length === 0) {
|
|
24794
|
+
continue;
|
|
24795
|
+
}
|
|
24796
|
+
targets.push({ slug, app: app2, installs });
|
|
24797
|
+
}
|
|
24798
|
+
return targets;
|
|
24799
|
+
}, "eligibleApps");
|
|
24800
|
+
|
|
24801
|
+
// src/apps/fan-out.ts
|
|
24802
|
+
var MAX_APPS_PER_DOMAIN = 50;
|
|
24803
|
+
var fanOutToApps = /* @__PURE__ */ __name(async (env, { trigger, snapshot, trace }, { deliverEffects }) => {
|
|
24804
|
+
const { registry: registry3, error: error51 } = siteRegistry();
|
|
24805
|
+
const targets = eligibleApps(registry3, trigger, snapshot.consent, consentEnforced(env));
|
|
24806
|
+
if (error51 === void 0 && targets.length === 0) {
|
|
24807
|
+
return;
|
|
24808
|
+
}
|
|
24809
|
+
await runInvocation({ name: "app.fan_out", trace, attrs: { "fan_out.kind": trigger.kind, "fan_out.apps": targets.length } }, async () => {
|
|
24810
|
+
if (error51 !== void 0) {
|
|
24811
|
+
addSpanEvent("apps registry is malformed; apps are off", { level: "error", attrs: { error: error51 } });
|
|
24812
|
+
return;
|
|
24813
|
+
}
|
|
24814
|
+
const context = invocationContext(env);
|
|
24815
|
+
if (context === void 0) {
|
|
24816
|
+
addSpanEvent("apps skipped: APPS_KEY_MASTER or QINTO_DOMAIN_ID is missing", { level: "warn" });
|
|
24817
|
+
return;
|
|
24818
|
+
}
|
|
24819
|
+
const capped = targets.slice(0, MAX_APPS_PER_DOMAIN);
|
|
24820
|
+
if (capped.length < targets.length) {
|
|
24821
|
+
addSpanEvent("eligible apps exceed the per-event budget", {
|
|
24822
|
+
level: "warn",
|
|
24823
|
+
attrs: { eligible: targets.length, cap: MAX_APPS_PER_DOMAIN }
|
|
24824
|
+
});
|
|
24825
|
+
}
|
|
24826
|
+
const budget = createDataPointBudget();
|
|
24827
|
+
await Promise.allSettled(
|
|
24828
|
+
capped.map((target) => {
|
|
24829
|
+
return invokeApp(env, { target, trigger, snapshot, context, deliverEffects, budget });
|
|
24830
|
+
})
|
|
24831
|
+
);
|
|
24832
|
+
});
|
|
24833
|
+
}, "fanOutToApps");
|
|
24834
|
+
|
|
24835
|
+
// src/apps/fan-out-entrypoint.ts
|
|
24836
|
+
var anonEffectsDelivery = /* @__PURE__ */ __name((env, anonId) => {
|
|
24837
|
+
return (effects) => {
|
|
24838
|
+
return anonStub(env, anonId).appEffects(effects, currentTraceContext());
|
|
24839
|
+
};
|
|
24840
|
+
}, "anonEffectsDelivery");
|
|
24841
|
+
var FanOut = class extends WorkerEntrypoint2 {
|
|
24842
|
+
static {
|
|
24843
|
+
__name(this, "FanOut");
|
|
24844
|
+
}
|
|
24845
|
+
run(job) {
|
|
24846
|
+
this.ctx.waitUntil(
|
|
24847
|
+
fanOutToApps(this.env, job, { deliverEffects: anonEffectsDelivery(this.env, job.snapshot.anonId) }).catch((error51) => {
|
|
24848
|
+
console.warn("app fan-out failed", { error: error51 });
|
|
24849
|
+
})
|
|
24850
|
+
);
|
|
24851
|
+
}
|
|
24852
|
+
};
|
|
24853
|
+
|
|
24720
24854
|
// src/id-graph/alias/do.ts
|
|
24721
24855
|
import { DurableObject } from "cloudflare:workers";
|
|
24722
24856
|
var ASSIGNMENT_KEY = "assignment";
|
|
@@ -24949,87 +25083,6 @@ var acceptedCaptures = /* @__PURE__ */ __name((registry3, consent2, apps, enforc
|
|
|
24949
25083
|
return accepted;
|
|
24950
25084
|
}, "acceptedCaptures");
|
|
24951
25085
|
|
|
24952
|
-
// src/apps/eligibility.ts
|
|
24953
|
-
var wantsTrigger = /* @__PURE__ */ __name((app2, trigger) => {
|
|
24954
|
-
if (trigger.kind === "connect") {
|
|
24955
|
-
return app2.hooks.onConnect;
|
|
24956
|
-
}
|
|
24957
|
-
return app2.hooks.onEvent.includes(trigger.action.type);
|
|
24958
|
-
}, "wantsTrigger");
|
|
24959
|
-
var gatePasses = /* @__PURE__ */ __name((entry, trigger, consent2, enforced) => {
|
|
24960
|
-
if (trigger.kind === "event" && trigger.action.origin?.app === entry.slug) {
|
|
24961
|
-
return false;
|
|
24962
|
-
}
|
|
24963
|
-
if (trigger.kind === "event" && trigger.action.type === "consent") {
|
|
24964
|
-
return true;
|
|
24965
|
-
}
|
|
24966
|
-
return consentAllows(consent2, entry.slug, entry.app.consent.category, enforced);
|
|
24967
|
-
}, "gatePasses");
|
|
24968
|
-
var eligibleApps = /* @__PURE__ */ __name((registry3, trigger, consent2, enforced) => {
|
|
24969
|
-
const targets = [];
|
|
24970
|
-
for (const slug of registry3.order) {
|
|
24971
|
-
const app2 = registry3.apps[slug];
|
|
24972
|
-
if (app2 === void 0 || !wantsTrigger(app2, trigger) || !gatePasses({ slug, app: app2 }, trigger, consent2, enforced)) {
|
|
24973
|
-
continue;
|
|
24974
|
-
}
|
|
24975
|
-
const installs = app2.installs.filter((install) => install.active).map(({ id, ord, label }) => ({ id, ord, label }));
|
|
24976
|
-
if (installs.length === 0) {
|
|
24977
|
-
continue;
|
|
24978
|
-
}
|
|
24979
|
-
targets.push({ slug, app: app2, installs });
|
|
24980
|
-
}
|
|
24981
|
-
return targets;
|
|
24982
|
-
}, "eligibleApps");
|
|
24983
|
-
|
|
24984
|
-
// src/apps/fan-out.ts
|
|
24985
|
-
var MAX_APPS_PER_DOMAIN = 50;
|
|
24986
|
-
var FAN_OUT_CONCURRENCY = 6;
|
|
24987
|
-
var runPool = /* @__PURE__ */ __name(async (limit, tasks) => {
|
|
24988
|
-
const running = /* @__PURE__ */ new Set();
|
|
24989
|
-
for (const task of tasks) {
|
|
24990
|
-
const invocation = task().finally(() => running.delete(invocation));
|
|
24991
|
-
running.add(invocation);
|
|
24992
|
-
if (running.size >= limit) {
|
|
24993
|
-
await Promise.race(running);
|
|
24994
|
-
}
|
|
24995
|
-
}
|
|
24996
|
-
await Promise.all(running);
|
|
24997
|
-
}, "runPool");
|
|
24998
|
-
var fanOutToApps = /* @__PURE__ */ __name(async (env, trigger, snapshot, { deliverPush, writeAttribution: writeAttribution2, writeContext: writeContext2, budget, trace }) => {
|
|
24999
|
-
const { registry: registry3, error: error51 } = siteRegistry();
|
|
25000
|
-
const targets = eligibleApps(registry3, trigger, snapshot.consent, consentEnforced(env));
|
|
25001
|
-
if (error51 === void 0 && targets.length === 0) {
|
|
25002
|
-
return;
|
|
25003
|
-
}
|
|
25004
|
-
await runInvocation(
|
|
25005
|
-
{ name: "do.anon.app_fan_out", trace, attrs: { "fan_out.kind": trigger.kind, "fan_out.apps": targets.length } },
|
|
25006
|
-
async () => {
|
|
25007
|
-
if (error51 !== void 0) {
|
|
25008
|
-
addSpanEvent("apps registry is malformed; apps are off", { level: "error", attrs: { error: error51 } });
|
|
25009
|
-
return;
|
|
25010
|
-
}
|
|
25011
|
-
const context = invocationContext(env);
|
|
25012
|
-
if (context === void 0) {
|
|
25013
|
-
addSpanEvent("apps skipped: APPS_KEY_MASTER or QINTO_DOMAIN_ID is missing", { level: "warn" });
|
|
25014
|
-
return;
|
|
25015
|
-
}
|
|
25016
|
-
const capped = targets.slice(0, MAX_APPS_PER_DOMAIN);
|
|
25017
|
-
if (capped.length < targets.length) {
|
|
25018
|
-
addSpanEvent("eligible apps exceed the per-event budget", {
|
|
25019
|
-
level: "warn",
|
|
25020
|
-
attrs: { eligible: targets.length, cap: MAX_APPS_PER_DOMAIN }
|
|
25021
|
-
});
|
|
25022
|
-
}
|
|
25023
|
-
await runPool(
|
|
25024
|
-
FAN_OUT_CONCURRENCY,
|
|
25025
|
-
capped.map((target) => {
|
|
25026
|
-
return () => invokeApp(env, { target, trigger, snapshot, context, deliverPush, writeAttribution: writeAttribution2, writeContext: writeContext2, budget });
|
|
25027
|
-
})
|
|
25028
|
-
);
|
|
25029
|
-
}
|
|
25030
|
-
);
|
|
25031
|
-
}, "fanOutToApps");
|
|
25032
|
-
|
|
25033
25086
|
// src/apps/transform.ts
|
|
25034
25087
|
var TRANSFORM_TIMEOUT_MS = 500;
|
|
25035
25088
|
var TRANSFORM_CHAIN_BUDGET_MS = 1e3;
|
|
@@ -25971,9 +26024,29 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_facts_conversion ON facts (key) WHERE kind
|
|
|
25971
26024
|
"ALTER TABLE facts ADD COLUMN expires_at INTEGER;"
|
|
25972
26025
|
];
|
|
25973
26026
|
|
|
26027
|
+
// src/id-graph/anon/token-bucket.ts
|
|
26028
|
+
var createTokenBucket = /* @__PURE__ */ __name((capacity, refillPerSecond) => {
|
|
26029
|
+
let tokens = capacity;
|
|
26030
|
+
let updatedAt = null;
|
|
26031
|
+
return {
|
|
26032
|
+
take: /* @__PURE__ */ __name((now) => {
|
|
26033
|
+
if (updatedAt !== null) {
|
|
26034
|
+
tokens = Math.min(capacity, tokens + Math.max(0, now - updatedAt) * (refillPerSecond / 1e3));
|
|
26035
|
+
}
|
|
26036
|
+
updatedAt = now;
|
|
26037
|
+
if (tokens < 1) {
|
|
26038
|
+
return false;
|
|
26039
|
+
}
|
|
26040
|
+
tokens -= 1;
|
|
26041
|
+
return true;
|
|
26042
|
+
}, "take")
|
|
26043
|
+
};
|
|
26044
|
+
}, "createTokenBucket");
|
|
26045
|
+
|
|
25974
26046
|
// src/id-graph/anon/websocket.ts
|
|
25975
26047
|
var MAX_BROWSER_RESULT_BYTES = 1024;
|
|
25976
26048
|
var MAX_BROWSER_RESULTS_BYTES = 32 * 1024;
|
|
26049
|
+
var MAX_FRAME_BYTES = 64 * 1024;
|
|
25977
26050
|
var browserContextFrame = external_exports.object({
|
|
25978
26051
|
occurredAt: external_exports.number().int().nonnegative().safe(),
|
|
25979
26052
|
page: external_exports.object({ url: external_exports.string().min(1).max(4096), referrer: external_exports.string().min(1).max(4096).optional() }).strict(),
|
|
@@ -26046,6 +26119,10 @@ var pluckRef = /* @__PURE__ */ __name((parsed) => {
|
|
|
26046
26119
|
return typeof ref === "string" && ref.length > 0 ? ref : void 0;
|
|
26047
26120
|
}, "pluckRef");
|
|
26048
26121
|
var parseFrame = /* @__PURE__ */ __name((message) => {
|
|
26122
|
+
const size = typeof message === "string" ? message.length : message.byteLength;
|
|
26123
|
+
if (size > MAX_FRAME_BYTES) {
|
|
26124
|
+
return { ok: false, ref: void 0, error: new Error(`frame exceeds ${MAX_FRAME_BYTES} bytes`) };
|
|
26125
|
+
}
|
|
26049
26126
|
const text = typeof message === "string" ? message : new TextDecoder().decode(message);
|
|
26050
26127
|
let parsed;
|
|
26051
26128
|
try {
|
|
@@ -26144,12 +26221,19 @@ var siteAudience = /* @__PURE__ */ __name(({ registry: registry3, app: app2, top
|
|
|
26144
26221
|
const allowed = app2.events.publishes.includes(topic) && app2.data.includes(SHARED_CONTEXT_CAPABILITY);
|
|
26145
26222
|
return allowed ? siteEventReceivers(registry3, consent2, enforced) : null;
|
|
26146
26223
|
}, "siteAudience");
|
|
26224
|
+
var FRAME_BURST = 60;
|
|
26225
|
+
var FRAMES_PER_SECOND = 10;
|
|
26226
|
+
var EMIT_BURST = 120;
|
|
26227
|
+
var EMITS_PER_SECOND = 5;
|
|
26147
26228
|
var AnonDO = class extends DurableObject2 {
|
|
26148
26229
|
static {
|
|
26149
26230
|
__name(this, "AnonDO");
|
|
26150
26231
|
}
|
|
26151
26232
|
// Serialize link decisions so concurrent first identifiers cannot split the graph.
|
|
26152
26233
|
ingestQueue = Promise.resolve();
|
|
26234
|
+
// In memory on purpose: a hibernated socket sent nothing, so a fresh bucket on wake is the right one.
|
|
26235
|
+
frameBuckets = /* @__PURE__ */ new WeakMap();
|
|
26236
|
+
emitBudget = createTokenBucket(EMIT_BURST, EMITS_PER_SECOND);
|
|
26153
26237
|
// deleteAll removes the live schema; only a later write recreates it.
|
|
26154
26238
|
wiped = false;
|
|
26155
26239
|
// Warm write-through mirrors reset together after expiry.
|
|
@@ -26192,11 +26276,7 @@ var AnonDO = class extends DurableObject2 {
|
|
|
26192
26276
|
const attachment = connectionAttachment(request, geo);
|
|
26193
26277
|
server.serializeAttachment(attachment);
|
|
26194
26278
|
this.deliverSharedContext(server);
|
|
26195
|
-
this.appFanOut(
|
|
26196
|
-
{ kind: "connect", ...attachment.cf === void 0 ? {} : { geo: attachment.cf }, host: attachment.host },
|
|
26197
|
-
userId,
|
|
26198
|
-
createDataPointBudget()
|
|
26199
|
-
);
|
|
26279
|
+
this.appFanOut({ kind: "connect", ...attachment.cf === void 0 ? {} : { geo: attachment.cf }, host: attachment.host }, userId);
|
|
26200
26280
|
return new Response(null, { status: 101, webSocket: client });
|
|
26201
26281
|
});
|
|
26202
26282
|
}
|
|
@@ -26208,9 +26288,50 @@ var AnonDO = class extends DurableObject2 {
|
|
|
26208
26288
|
...action.type === "track" ? { "action.event": action.payload.event } : {}
|
|
26209
26289
|
};
|
|
26210
26290
|
return runInvocation({ name: "do.anon.ingest", trace, attrs }, () => {
|
|
26291
|
+
if (action.origin !== void 0 && !this.emitBudget.take(Date.now())) {
|
|
26292
|
+
addSpanEvent("emit rejected: the visitor\u2019s emit budget is spent", {
|
|
26293
|
+
level: "warn",
|
|
26294
|
+
attrs: { app: action.origin.app, depth: action.origin.depth }
|
|
26295
|
+
});
|
|
26296
|
+
return Promise.resolve();
|
|
26297
|
+
}
|
|
26211
26298
|
return this.ingestLocal(action);
|
|
26212
26299
|
});
|
|
26213
26300
|
}
|
|
26301
|
+
// The frames that ask instead of act are answered in place; an action frame comes back to be queued.
|
|
26302
|
+
async answerQuery(ws, parsed, userId, attachment) {
|
|
26303
|
+
const { frame, ref } = parsed;
|
|
26304
|
+
if (frame.t === "getSession") {
|
|
26305
|
+
this.respond(ws, {
|
|
26306
|
+
t: "session",
|
|
26307
|
+
ref,
|
|
26308
|
+
anonId: userId,
|
|
26309
|
+
globalId: linkedGlobalId(this.kv),
|
|
26310
|
+
ip: attachment?.host?.ip ?? null,
|
|
26311
|
+
geo: attachment?.browserGeo ?? null
|
|
26312
|
+
});
|
|
26313
|
+
return null;
|
|
26314
|
+
}
|
|
26315
|
+
if (frame.t === "capture") {
|
|
26316
|
+
const { accepted } = await this.capture({ apps: frame.apps });
|
|
26317
|
+
this.respond(ws, { t: "captureAck", ref, anonId: userId, accepted });
|
|
26318
|
+
return null;
|
|
26319
|
+
}
|
|
26320
|
+
return frame;
|
|
26321
|
+
}
|
|
26322
|
+
admitFrame(ws) {
|
|
26323
|
+
let bucket = this.frameBuckets.get(ws);
|
|
26324
|
+
if (bucket === void 0) {
|
|
26325
|
+
bucket = createTokenBucket(FRAME_BURST, FRAMES_PER_SECOND);
|
|
26326
|
+
this.frameBuckets.set(ws, bucket);
|
|
26327
|
+
}
|
|
26328
|
+
if (bucket.take(Date.now())) {
|
|
26329
|
+
return true;
|
|
26330
|
+
}
|
|
26331
|
+
addSpanEvent("websocket frame rate limited", { level: "warn" });
|
|
26332
|
+
this.respond(ws, { t: "error", ref: void 0, message: "rate limited" });
|
|
26333
|
+
return false;
|
|
26334
|
+
}
|
|
26214
26335
|
ingestLocal(action) {
|
|
26215
26336
|
return this.enqueue(() => this.processAction(action));
|
|
26216
26337
|
}
|
|
@@ -26233,6 +26354,9 @@ var AnonDO = class extends DurableObject2 {
|
|
|
26233
26354
|
async webSocketMessage(ws, message) {
|
|
26234
26355
|
const attachment = ws.deserializeAttachment() ?? void 0;
|
|
26235
26356
|
return runInvocation({ name: "ws.message", attrs: attachment?.attrs }, async () => {
|
|
26357
|
+
if (!this.admitFrame(ws)) {
|
|
26358
|
+
return;
|
|
26359
|
+
}
|
|
26236
26360
|
const parsed = parseFrame(message);
|
|
26237
26361
|
if (!parsed.ok) {
|
|
26238
26362
|
addSpanEvent("invalid websocket frame", { level: "warn", attrs: { error: String(parsed.error) } });
|
|
@@ -26250,23 +26374,11 @@ var AnonDO = class extends DurableObject2 {
|
|
|
26250
26374
|
this.respond(ws, { t: "error", ref: parsed.ref, message: "session not initialized" });
|
|
26251
26375
|
return;
|
|
26252
26376
|
}
|
|
26253
|
-
|
|
26254
|
-
|
|
26255
|
-
t: "session",
|
|
26256
|
-
ref: parsed.ref,
|
|
26257
|
-
anonId: userId,
|
|
26258
|
-
globalId: linkedGlobalId(this.kv),
|
|
26259
|
-
ip: attachment?.host?.ip ?? null,
|
|
26260
|
-
geo: attachment?.browserGeo ?? null
|
|
26261
|
-
});
|
|
26377
|
+
const frame = await this.answerQuery(ws, { frame: parsed.frame, ref: parsed.ref }, userId, attachment);
|
|
26378
|
+
if (frame === null) {
|
|
26262
26379
|
return;
|
|
26263
26380
|
}
|
|
26264
|
-
|
|
26265
|
-
const { accepted } = await this.capture({ apps: parsed.frame.apps });
|
|
26266
|
-
this.respond(ws, { t: "captureAck", ref: parsed.ref, anonId: userId, accepted });
|
|
26267
|
-
return;
|
|
26268
|
-
}
|
|
26269
|
-
const action = actionFromFrame(parsed.frame, userId, attachment?.cf, attachment?.host);
|
|
26381
|
+
const action = actionFromFrame(frame, userId, attachment?.cf, attachment?.host);
|
|
26270
26382
|
const ref = parsed.ref ?? action.actionId;
|
|
26271
26383
|
setSpanAttributes({ "action.id": action.actionId });
|
|
26272
26384
|
try {
|
|
@@ -26303,6 +26415,21 @@ var AnonDO = class extends DurableObject2 {
|
|
|
26303
26415
|
});
|
|
26304
26416
|
});
|
|
26305
26417
|
}
|
|
26418
|
+
// Returns without awaiting the chain: the fan-out worker must not wait on this object's queue. The
|
|
26419
|
+
// chain runs outside the queue turn on purpose — applyEffects awaits attributionDelivery and
|
|
26420
|
+
// contextDelivery, which enqueue, so a queued task would wait on work chained behind itself.
|
|
26421
|
+
appEffects(input, trace) {
|
|
26422
|
+
const run = runInvocation({ name: "do.anon.app_effects", trace, attrs: { "app.slug": input.slug } }, () => {
|
|
26423
|
+
return applyEffects(this.env, {
|
|
26424
|
+
...input,
|
|
26425
|
+
deliverPush: this.pushDelivery,
|
|
26426
|
+
writeAttribution: this.attributionDelivery,
|
|
26427
|
+
writeContext: this.contextDelivery
|
|
26428
|
+
});
|
|
26429
|
+
});
|
|
26430
|
+
this.ctx.waitUntil(run.catch((error51) => console.warn("app effects failed", { app: input.slug, error: error51 })));
|
|
26431
|
+
return Promise.resolve();
|
|
26432
|
+
}
|
|
26306
26433
|
// Returns what storage now holds, not what policy allowed: the browser memoes exactly these pairs,
|
|
26307
26434
|
// so a value the caps refused must never be reported as stored.
|
|
26308
26435
|
capture(input, trace) {
|
|
@@ -26519,7 +26646,7 @@ var AnonDO = class extends DurableObject2 {
|
|
|
26519
26646
|
consentReply(ref, userId) {
|
|
26520
26647
|
return { t: "consentState", ref, ...userId === null ? {} : { anonId: userId }, ...consentState2(this.kv) };
|
|
26521
26648
|
}
|
|
26522
|
-
pushDelivery = /* @__PURE__ */ __name((push) => {
|
|
26649
|
+
pushDelivery = /* @__PURE__ */ __name(async (push) => {
|
|
26523
26650
|
this.deliverAppPush(push);
|
|
26524
26651
|
}, "pushDelivery");
|
|
26525
26652
|
// Calls the public method directly: it enqueues internally, and both effect paths already run
|
|
@@ -26544,13 +26671,12 @@ var AnonDO = class extends DurableObject2 {
|
|
|
26544
26671
|
if (isNoopAction(action)) {
|
|
26545
26672
|
return;
|
|
26546
26673
|
}
|
|
26547
|
-
const
|
|
26548
|
-
const transformed = await this.transformAction(action, dataPoints);
|
|
26674
|
+
const transformed = await this.transformAction(action, createDataPointBudget());
|
|
26549
26675
|
if (transformed === null) {
|
|
26550
26676
|
return;
|
|
26551
26677
|
}
|
|
26552
26678
|
recorded = transformed.action;
|
|
26553
|
-
await this.runAction(transformed.action, transformed.releaseEmissions
|
|
26679
|
+
await this.runAction(transformed.action, transformed.releaseEmissions);
|
|
26554
26680
|
} finally {
|
|
26555
26681
|
this.recordActionBlocks(recorded);
|
|
26556
26682
|
}
|
|
@@ -26560,9 +26686,9 @@ var AnonDO = class extends DurableObject2 {
|
|
|
26560
26686
|
actionBlocks(action, this.appsSnapshot(action.userId), this.wiped ? [] : allIdentifiers(this.sql, this.identifiersMirror))
|
|
26561
26687
|
);
|
|
26562
26688
|
}
|
|
26563
|
-
async runAction(current, releaseEmissions
|
|
26689
|
+
async runAction(current, releaseEmissions) {
|
|
26564
26690
|
if (current.type !== "alias" && isDeliveryOnlyAction(current)) {
|
|
26565
|
-
this.appFanOut({ kind: "event", action: current }, current.userId
|
|
26691
|
+
this.appFanOut({ kind: "event", action: current }, current.userId);
|
|
26566
26692
|
this.releaseTransformEmissions(releaseEmissions);
|
|
26567
26693
|
return;
|
|
26568
26694
|
}
|
|
@@ -26598,7 +26724,7 @@ var AnonDO = class extends DurableObject2 {
|
|
|
26598
26724
|
} else {
|
|
26599
26725
|
await this.ingestConsent(current);
|
|
26600
26726
|
}
|
|
26601
|
-
this.appFanOut({ kind: "event", action: current }, current.userId
|
|
26727
|
+
this.appFanOut({ kind: "event", action: current }, current.userId);
|
|
26602
26728
|
this.releaseTransformEmissions(releaseEmissions);
|
|
26603
26729
|
} finally {
|
|
26604
26730
|
if (novelIdentifier) {
|
|
@@ -26653,16 +26779,16 @@ var AnonDO = class extends DurableObject2 {
|
|
|
26653
26779
|
}
|
|
26654
26780
|
this.ctx.waitUntil(release().catch((error51) => console.warn("transform emission release failed", { error: error51 })));
|
|
26655
26781
|
}
|
|
26656
|
-
appFanOut(trigger, anonId
|
|
26657
|
-
const
|
|
26782
|
+
appFanOut(trigger, anonId) {
|
|
26783
|
+
const { registry: registry3, error: error51 } = siteRegistry();
|
|
26784
|
+
if (error51 === void 0 && eligibleApps(registry3, trigger, consentState2(this.kv), consentEnforced(this.env)).length === 0) {
|
|
26785
|
+
return;
|
|
26786
|
+
}
|
|
26787
|
+
const job = { trigger, snapshot: this.appsSnapshot(anonId), trace: currentTraceContext() };
|
|
26658
26788
|
this.ctx.waitUntil(
|
|
26659
|
-
|
|
26660
|
-
|
|
26661
|
-
|
|
26662
|
-
writeContext: this.contextDelivery,
|
|
26663
|
-
budget: dataPoints,
|
|
26664
|
-
trace: currentTraceContext()
|
|
26665
|
-
}).catch((error51) => console.warn("app fan-out failed", { error: error51 }))
|
|
26789
|
+
traced("rpc fan_out.run", {}, () => {
|
|
26790
|
+
return this.ctx.exports.FanOut.run(job);
|
|
26791
|
+
}).catch((error52) => console.warn("app fan-out hand-off failed", { error: error52 }))
|
|
26666
26792
|
);
|
|
26667
26793
|
}
|
|
26668
26794
|
getProperties(trace) {
|
|
@@ -27009,6 +27135,19 @@ var AnonDO = class extends DurableObject2 {
|
|
|
27009
27135
|
// src/id-graph/global/do.ts
|
|
27010
27136
|
import { DurableObject as DurableObject3 } from "cloudflare:workers";
|
|
27011
27137
|
|
|
27138
|
+
// src/helpers/run-pool.ts
|
|
27139
|
+
var runPool = /* @__PURE__ */ __name(async (limit, tasks) => {
|
|
27140
|
+
const running = /* @__PURE__ */ new Set();
|
|
27141
|
+
for (const task of tasks) {
|
|
27142
|
+
const invocation = task().finally(() => running.delete(invocation));
|
|
27143
|
+
running.add(invocation);
|
|
27144
|
+
if (running.size >= limit) {
|
|
27145
|
+
await Promise.race(running);
|
|
27146
|
+
}
|
|
27147
|
+
}
|
|
27148
|
+
await Promise.all(running);
|
|
27149
|
+
}, "runPool");
|
|
27150
|
+
|
|
27012
27151
|
// src/id-graph/global/facts.ts
|
|
27013
27152
|
var FACT_KINDS2 = [...PROJECTION_KINDS, "identifier", "conversion", "anon"];
|
|
27014
27153
|
var FACT_COLUMNS = "kind, scope, key, value, source, amount, currency, event_id, data_json, first_at, observed_at";
|
|
@@ -27192,6 +27331,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_facts_key ON facts (kind, scope, key);
|
|
|
27192
27331
|
];
|
|
27193
27332
|
|
|
27194
27333
|
// src/id-graph/global/do.ts
|
|
27334
|
+
var SIBLING_FAN_OUT_CONCURRENCY = 6;
|
|
27195
27335
|
var GlobalDO = class extends DurableObject3 {
|
|
27196
27336
|
static {
|
|
27197
27337
|
__name(this, "GlobalDO");
|
|
@@ -27293,8 +27433,9 @@ var GlobalDO = class extends DurableObject3 {
|
|
|
27293
27433
|
}
|
|
27294
27434
|
await runInvocation({ name: "do.global.fan_out", trace, attrs: { "global.id": globalId } }, async () => {
|
|
27295
27435
|
const snapshot = this.snapshot(globalId);
|
|
27296
|
-
await
|
|
27297
|
-
|
|
27436
|
+
await runPool(
|
|
27437
|
+
SIBLING_FAN_OUT_CONCURRENCY,
|
|
27438
|
+
recipients.map((anonId) => async () => {
|
|
27298
27439
|
try {
|
|
27299
27440
|
await traced("rpc anon.sync_from_global", { attrs: { "anon.id": anonId } }, () => {
|
|
27300
27441
|
return anonStub(this.env, anonId).syncFromGlobal(snapshot, currentTraceContext());
|
|
@@ -27317,8 +27458,10 @@ app.use("/q.js", cors());
|
|
|
27317
27458
|
app.get("/q.js", qJs);
|
|
27318
27459
|
app.use("/health", cors());
|
|
27319
27460
|
app.route("/", health);
|
|
27461
|
+
var MAX_V1_BODY_BYTES = 1024 * 1024;
|
|
27320
27462
|
var allowCors = cors({ origin: /* @__PURE__ */ __name((origin) => origin, "origin"), credentials: true });
|
|
27321
27463
|
app.use("/v1/*", allowCors);
|
|
27464
|
+
app.use("/v1/*", bodyLimit({ maxSize: MAX_V1_BODY_BYTES, onError: /* @__PURE__ */ __name((c) => c.json({ error: "too_large" }, 413), "onError") }));
|
|
27322
27465
|
app.route("/", alias);
|
|
27323
27466
|
app.route("/", identify);
|
|
27324
27467
|
app.route("/", track);
|
|
@@ -27359,6 +27502,7 @@ export {
|
|
|
27359
27502
|
AliasDO,
|
|
27360
27503
|
AnonDO,
|
|
27361
27504
|
Broker,
|
|
27505
|
+
FanOut,
|
|
27362
27506
|
GlobalDO,
|
|
27363
27507
|
app,
|
|
27364
27508
|
index_default as default
|