@tokenoftrust/storefront-runner 1.4.0 → 1.4.2-rc.0
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/apps/storefront/astro.config.mjs +17 -1
- package/apps/storefront/integrations/materialize-guard.mjs +56 -0
- package/apps/storefront/migrations-apps/0001_woozy_lyja.sql +22 -0
- package/apps/storefront/migrations-apps/0002_good_switch.sql +26 -0
- package/apps/storefront/migrations-apps/0003_mute_marauders.sql +12 -0
- package/apps/storefront/migrations-apps/meta/0001_snapshot.json +990 -0
- package/apps/storefront/migrations-apps/meta/0002_snapshot.json +1160 -0
- package/apps/storefront/migrations-apps/meta/0003_snapshot.json +1235 -0
- package/apps/storefront/migrations-apps/meta/_journal.json +21 -0
- package/apps/storefront/package.json +0 -1
- package/apps/storefront/public/js/dashboard-team.js +38 -2
- package/apps/storefront/src/components/Seo.astro +65 -1
- package/apps/storefront/src/components/admin/AdminBlogDetailRail.astro +878 -0
- package/apps/storefront/src/components/admin/AdminBlogTab.astro +1432 -0
- package/apps/storefront/src/components/admin/AdminCustomersTab.astro +3 -7
- package/apps/storefront/src/components/admin/AdminPublishTab.astro +1756 -1382
- package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +1 -3
- package/apps/storefront/src/components/blog/BlogPaywall.astro +87 -0
- package/apps/storefront/src/components/blog/PostAccessBadge.astro +43 -0
- package/apps/storefront/src/components/blog/PostGrid.astro +57 -0
- package/apps/storefront/src/components/blog/ReactionBar.astro +214 -0
- package/apps/storefront/src/layouts/Layout.astro +48 -3
- package/apps/storefront/src/lib/activity/alerts.ts +426 -0
- package/apps/storefront/src/lib/activity/changeActorAttribution.ts +85 -0
- package/apps/storefront/src/lib/activity/deployVersion.ts +120 -0
- package/apps/storefront/src/lib/activity/ingest.ts +188 -0
- package/apps/storefront/src/lib/activity/ingestAuth.ts +105 -0
- package/apps/storefront/src/lib/activity/killSwitch.ts +80 -0
- package/apps/storefront/src/lib/activity/query.ts +403 -0
- package/apps/storefront/src/lib/activity/recordActivity.ts +101 -0
- package/apps/storefront/src/lib/activity/store.ts +120 -0
- package/apps/storefront/src/lib/activity/uiActor.ts +150 -0
- package/apps/storefront/src/lib/activity/workerCommit.ts +70 -0
- package/apps/storefront/src/lib/admin/adminShell.ts +290 -0
- package/apps/storefront/src/lib/admin/ai/approvalMode.ts +2 -3
- package/apps/storefront/src/lib/admin/ai/audit.ts +3 -5
- package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +7 -7
- package/apps/storefront/src/lib/admin/checkoutStyleTarget.ts +3 -3
- package/apps/storefront/src/lib/admin/orderClient.ts +3 -5
- package/apps/storefront/src/lib/admin/services/orders.ts +17 -19
- package/apps/storefront/src/lib/admin/viewmodels/aiWorkflows.ts +4 -6
- package/apps/storefront/src/lib/admin/viewmodels/settings.ts +1 -1
- package/apps/storefront/src/lib/apps/adminService.ts +4 -4
- package/apps/storefront/src/lib/apps/adminSession.ts +53 -44
- package/apps/storefront/src/lib/apps/apiAuth.ts +2 -2
- package/apps/storefront/src/lib/apps/apiRoute.ts +2 -2
- package/apps/storefront/src/lib/apps/catalogMapper.ts +1 -1
- package/apps/storefront/src/lib/apps/credentials.ts +2 -2
- package/apps/storefront/src/lib/apps/gatewayKeys.ts +4 -4
- package/apps/storefront/src/lib/apps/healthAggregate.ts +5 -5
- package/apps/storefront/src/lib/apps/orders/attributionService.ts +2 -2
- package/apps/storefront/src/lib/apps/orders/customerHash.ts +1 -1
- package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +12 -12
- package/apps/storefront/src/lib/apps/orders/idempotency.ts +7 -7
- package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +1 -1
- package/apps/storefront/src/lib/apps/orders/orderMapper.ts +2 -2
- package/apps/storefront/src/lib/apps/orders/ordersStore.ts +6 -6
- package/apps/storefront/src/lib/apps/registryService.ts +6 -6
- package/apps/storefront/src/lib/apps/scopes.ts +2 -2
- package/apps/storefront/src/lib/apps/tokenIssuer.ts +4 -4
- package/apps/storefront/src/lib/apps/tokenVerifier.ts +6 -6
- package/apps/storefront/src/lib/apps/widgets/eligibility.ts +2 -2
- package/apps/storefront/src/lib/apps/widgets/frameProps.ts +3 -3
- package/apps/storefront/src/lib/apps/widgets/launchToken.ts +4 -4
- package/apps/storefront/src/lib/apps/widgets/placements.ts +2 -2
- package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +1 -1
- package/apps/storefront/src/lib/auth/adminEntry.ts +9 -9
- package/apps/storefront/src/lib/auth/identityToken.ts +6 -14
- package/apps/storefront/src/lib/auth/loginGate.ts +75 -13
- package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +6 -4
- package/apps/storefront/src/lib/auth/operatorSession.ts +113 -0
- package/apps/storefront/src/lib/auth/route.ts +44 -4
- package/apps/storefront/src/lib/auth/session.ts +7 -8
- package/apps/storefront/src/lib/auth/totAccessClient.ts +8 -18
- package/apps/storefront/src/lib/basePath.ts +9 -2
- package/apps/storefront/src/lib/blog/access.ts +29 -0
- package/apps/storefront/src/lib/blog/collection.ts +313 -0
- package/apps/storefront/src/lib/blog/markdown.ts +174 -0
- package/apps/storefront/src/lib/blog/provider.ts +5 -0
- package/apps/storefront/src/lib/blog/reactions.ts +87 -0
- package/apps/storefront/src/lib/blog/rss.ts +92 -0
- package/apps/storefront/src/lib/blog/teaser.ts +83 -0
- package/apps/storefront/src/lib/blog/types.ts +34 -0
- package/apps/storefront/src/lib/blog/viewCounts.ts +243 -0
- package/apps/storefront/src/lib/d1/catalog.ts +2 -2
- package/apps/storefront/src/lib/d1/schema-apps.ts +147 -14
- package/apps/storefront/src/lib/dev/activityIngestToken.ts +2 -4
- package/apps/storefront/src/lib/dev/activityStore.ts +3 -5
- package/apps/storefront/src/lib/dev/apiBase.ts +1 -4
- package/apps/storefront/src/lib/dev/cockpitStore.ts +1 -9
- package/apps/storefront/src/lib/dev/devDraft.ts +1 -1
- package/apps/storefront/src/lib/dev/previewStatus.ts +9 -21
- package/apps/storefront/src/lib/dev/rendezvousBroker.ts +9 -24
- package/apps/storefront/src/lib/edgeCache.ts +17 -0
- package/apps/storefront/src/lib/env.ts +51 -0
- package/apps/storefront/src/lib/http/fetchTransport.ts +63 -0
- package/apps/storefront/src/lib/i18n.ts +25 -0
- package/apps/storefront/src/lib/jsonld.ts +118 -0
- package/apps/storefront/src/lib/monitoring/manifest.ts +11 -11
- package/apps/storefront/src/lib/newsletter/confirmationEmail.ts +30 -0
- package/apps/storefront/src/lib/newsletter/consent.ts +23 -0
- package/apps/storefront/src/lib/newsletter/digest.ts +117 -0
- package/apps/storefront/src/lib/newsletter/resendClient.ts +79 -0
- package/apps/storefront/src/lib/newsletter/send.ts +265 -0
- package/apps/storefront/src/lib/newsletter/subscribers.ts +156 -0
- package/apps/storefront/src/lib/publish/adminPublishTab.ts +3519 -0
- package/apps/storefront/src/lib/publish/apex-readiness.ts +41 -18
- package/apps/storefront/src/lib/publish/dispatchHealth.ts +5 -7
- package/apps/storefront/src/lib/publish/domainState.ts +22 -13
- package/apps/storefront/src/lib/publish/panelPolling.ts +78 -0
- package/apps/storefront/src/lib/publish/shipWorkspace.ts +100 -42
- package/apps/storefront/src/lib/publish/stageBundleUpload.ts +215 -0
- package/apps/storefront/src/lib/rate/kvWindow.ts +53 -0
- package/apps/storefront/src/lib/search/index.ts +2 -2
- package/apps/storefront/src/lib/seo/alternates.ts +42 -0
- package/apps/storefront/src/lib/seo/meta.ts +65 -0
- package/apps/storefront/src/lib/social/golive.ts +212 -0
- package/apps/storefront/src/lib/social/notify.ts +32 -0
- package/apps/storefront/src/lib/social/tokenStore.ts +171 -0
- package/apps/storefront/src/lib/the-build/provider.ts +27 -0
- package/apps/storefront/src/lib/the-build/readingTime.ts +35 -0
- package/apps/storefront/src/lib/the-build/toc.ts +95 -0
- package/apps/storefront/src/lib/tot/ToTClient.ts +1 -1
- package/apps/storefront/src/lib/tot/d1Client.ts +2 -2
- package/apps/storefront/src/lib/tot/totClientInterface.ts +3 -4
- package/apps/storefront/src/lib/webhooks/deliveryStore.ts +16 -17
- package/apps/storefront/src/middleware/index.ts +130 -2
- package/apps/storefront/src/pages/admin/ops-timeline.astro +420 -0
- package/apps/storefront/src/pages/admin.astro +52 -1206
- package/apps/storefront/src/pages/api/activity.ts +137 -0
- package/apps/storefront/src/pages/api/admin/activity-alerts.ts +73 -0
- package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +27 -1
- package/apps/storefront/src/pages/api/apps/admin/install.ts +26 -1
- package/apps/storefront/src/pages/api/apps/admin/resume.ts +24 -1
- package/apps/storefront/src/pages/api/apps/admin/suspend.ts +24 -1
- package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +25 -1
- package/apps/storefront/src/pages/api/apps/admin/update.ts +24 -1
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +11 -0
- package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +11 -0
- package/apps/storefront/src/pages/api/apps/v1/attribution.ts +20 -0
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +12 -0
- package/apps/storefront/src/pages/api/blog/[collection]/[slug]/react.ts +104 -0
- package/apps/storefront/src/pages/api/cache-purge.ts +11 -0
- package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +30 -0
- package/apps/storefront/src/pages/api/newsletter/[collection]/subscribe.ts +146 -0
- package/apps/storefront/src/pages/api/newsletter/confirm.ts +75 -0
- package/apps/storefront/src/pages/api/newsletter/unsubscribe.ts +90 -0
- package/apps/storefront/src/pages/api/publish/stage-bundle.ts +70 -0
- package/apps/storefront/src/pages/api/rum/vitals.ts +21 -2
- package/apps/storefront/src/pages/auth/login.astro +5 -4
- package/apps/storefront/src/pages/blog/[slug].astro +96 -16
- package/apps/storefront/src/pages/blog/author/[author].astro +140 -0
- package/apps/storefront/src/pages/blog/category/[category].astro +67 -0
- package/apps/storefront/src/pages/blog/index.astro +2 -0
- package/apps/storefront/src/pages/blog/tag/[tag].astro +65 -0
- package/apps/storefront/src/pages/cockpit.astro +74 -12
- package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +15 -11
- package/apps/storefront/src/pages/search.astro +1 -1
- package/apps/storefront/src/pages/sitemap.xml.ts +20 -0
- package/apps/storefront/src/pages/the-build/[slug].astro +129 -0
- package/apps/storefront/src/pages/the-build/index.astro +146 -0
- package/apps/storefront/src/pages/the-build/rss.xml.ts +33 -0
- package/apps/storefront/src/styles/admin.css +1080 -0
- package/apps/storefront/tsconfig.json +5 -0
- package/package.json +1 -1
- package/packages/public-runtime/package.json +1 -0
- package/packages/public-runtime/src/activity/README.md +146 -0
- package/packages/public-runtime/src/activity/catalog.ts +553 -0
- package/packages/public-runtime/src/activity/event.ts +168 -0
- package/packages/public-runtime/src/activity/index.ts +16 -0
- package/packages/public-runtime/src/activity/redaction.ts +263 -0
- package/packages/public-runtime/src/candidate-index.ts +13 -0
- package/packages/public-runtime/src/catalog-d1.ts +1 -1
- package/packages/public-runtime/src/customization-preview.ts +25 -2
- package/packages/public-runtime/src/customization-runtime.ts +125 -3
- package/packages/public-runtime/src/customization-versioning.ts +10 -0
- package/packages/public-runtime/src/index.ts +7 -0
- package/packages/public-runtime/src/static-bundle.ts +357 -0
- package/packages/public-runtime/src/tenant.ts +36 -8
- package/scripts/build/copy-tenant-assets.mjs +14 -4
- package/scripts/dev/ai-edit.mjs +1 -1
- package/scripts/dev/checkout-watch.mjs +2 -2
- package/scripts/dev/file-browser.mjs +2 -4
- package/scripts/dev/file-writer.mjs +1 -1
- package/scripts/dev/git-status.mjs +2 -2
- package/scripts/dev/locate-handler.mjs +3 -3
- package/scripts/dev/port-check.mjs +3 -3
- package/scripts/dev/publish.mjs +5 -9
- package/scripts/dev/shot.mjs +1 -1
- package/scripts/dev/unified-diff.mjs +8 -3
- package/scripts/tot-dev.mjs +6 -9
- package/apps/storefront/src/lib/webhooks/signing.ts +0 -29
- package/apps/storefront/src/lib/webhooks/webhookSigningKey.ts +0 -146
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/activity — the ingest endpoint of the operational activity telemetry
|
|
3
|
+
* spine (card D4). Accepts one `ActivityEvent` (or a batch array) as JSON from
|
|
4
|
+
* the emitter cohorts — the CLI (D3) and the UI (D6), both posting the wire shape
|
|
5
|
+
* of `packages/public-runtime/src/activity/event.ts` — authenticates the caller,
|
|
6
|
+
* re-redacts server-side, and fans each accepted event out to the sink(s) its
|
|
7
|
+
* catalog entry declares. See DECISIONS.md §8 for the store decision.
|
|
8
|
+
*
|
|
9
|
+
* AUTH — HMAC-SHA256 service-auth (`src/lib/activity/ingestAuth.ts`). The caller
|
|
10
|
+
* presents the authenticated tenant in `x-tot-activity-tenant` and a hex
|
|
11
|
+
* signature over `<tenant>\n<rawBody>` in `x-tot-activity-signature`, keyed by
|
|
12
|
+
* the `ACTIVITY_INGEST_SECRET` Worker secret. Binding the tenant into the signed
|
|
13
|
+
* material is what makes the tenant scope trustworthy: the route writes every
|
|
14
|
+
* row under THIS verified tenant and discards any body-supplied `scope.tenantId`
|
|
15
|
+
* (multi-tenant isolation). An unset secret is a 503 (seam not configured — the
|
|
16
|
+
* same convention as the MCP webhook receivers), not an auth failure.
|
|
17
|
+
*
|
|
18
|
+
* DEFENSE IN DEPTH — every event is rebuilt through `createActivityEvent()`
|
|
19
|
+
* (`ingestEvents`), re-applying the default-deny allowlist + canary scan even
|
|
20
|
+
* though emitters already redacted. A value-canary trip is self-telemetried as
|
|
21
|
+
* `system.ingest.rejected` so a leak attempt is visible (D6/D7 alert on it).
|
|
22
|
+
*
|
|
23
|
+
* OFF THE CRITICAL PATH — auth + parse + validate + redact are cheap synchronous
|
|
24
|
+
* CPU; the sink WRITES (D1 + Analytics Engine) run past the response via
|
|
25
|
+
* `runAfterResponse` (Workers `waitUntil`). The response is a 202 with per-event
|
|
26
|
+
* accepted/rejected counts the instant validation is done — a slow D1 write never
|
|
27
|
+
* holds the emitter's request open. JSON bodies are exempt from Astro's CSRF
|
|
28
|
+
* origin check, so the server-to-server callers work.
|
|
29
|
+
*/
|
|
30
|
+
import type { APIContext } from "astro";
|
|
31
|
+
import { readD1, readEnv, readAnalytics, runAfterResponse, type CfExecutionContextLike } from "@/lib/env";
|
|
32
|
+
import { fromD1 } from "@/lib/d1/catalog";
|
|
33
|
+
import { verifyIngestAuth } from "@/lib/activity/ingestAuth";
|
|
34
|
+
import { ingestEvents, type IngestRejection } from "@/lib/activity/ingest";
|
|
35
|
+
import { fanOutActivity, type AnalyticsEngineLike, type ActivitySinks } from "@/lib/activity/store";
|
|
36
|
+
import { isActivityDisabled } from "@/lib/activity/killSwitch";
|
|
37
|
+
import { createActivityEvent } from "@tot/public-runtime";
|
|
38
|
+
|
|
39
|
+
export const prerender = false;
|
|
40
|
+
|
|
41
|
+
/** Cap the batch so a single POST can't fan out unbounded work past the response. */
|
|
42
|
+
const MAX_BATCH = 200;
|
|
43
|
+
|
|
44
|
+
function json(body: unknown, status: number): Response {
|
|
45
|
+
return new Response(JSON.stringify(body), {
|
|
46
|
+
status,
|
|
47
|
+
// Write-path ack — never cacheable. Mirrors the house convention.
|
|
48
|
+
headers: { "content-type": "application/json", "cache-control": "no-store" },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
53
|
+
// Kill switch (D8): telemetry off ⇒ accept-and-drop. Fail-open toward the EMITTER —
|
|
54
|
+
// a 202 (never a 4xx/5xx) so a CLI/UI poster never errors, retries, or is delayed by
|
|
55
|
+
// the switch; nothing is validated, stored, or fanned out.
|
|
56
|
+
if (isActivityDisabled()) {
|
|
57
|
+
return json({ ok: true, disabled: true, accepted: 0, rejected: [] }, 202);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const secret = await readEnv("ACTIVITY_INGEST_SECRET");
|
|
61
|
+
if (!secret) {
|
|
62
|
+
// Seam not configured (emitters don't post without the shared ingest secret).
|
|
63
|
+
return new Response("activity ingest not configured", { status: 503 });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Read the RAW body once: it is both the auth material and the JSON payload.
|
|
67
|
+
const rawBody = await context.request.text();
|
|
68
|
+
|
|
69
|
+
const auth = await verifyIngestAuth(context.request.headers, rawBody, secret);
|
|
70
|
+
if (!auth.ok) return new Response(auth.reason, { status: auth.status });
|
|
71
|
+
|
|
72
|
+
let parsed: unknown;
|
|
73
|
+
try {
|
|
74
|
+
parsed = JSON.parse(rawBody);
|
|
75
|
+
} catch {
|
|
76
|
+
return json({ ok: false, reason: "invalid JSON body" }, 400);
|
|
77
|
+
}
|
|
78
|
+
const events = Array.isArray(parsed) ? parsed : [parsed];
|
|
79
|
+
if (events.length === 0) return json({ ok: true, accepted: 0, rejected: [] }, 202);
|
|
80
|
+
if (events.length > MAX_BATCH) {
|
|
81
|
+
return json({ ok: false, reason: `batch too large (max ${MAX_BATCH})` }, 413);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Validate + re-redact under the AUTHENTICATED tenant (cheap, synchronous).
|
|
85
|
+
const { accepted, rejected } = ingestEvents(events, auth.tenantId);
|
|
86
|
+
|
|
87
|
+
// Resolve the sink bindings (absent in dev/until provisioned ⇒ that sink is
|
|
88
|
+
// skipped, never an error).
|
|
89
|
+
const d1 = await readD1("STOREFRONT_APPS_DB");
|
|
90
|
+
const analytics = (await readAnalytics("ACTIVITY_ANALYTICS")) as AnalyticsEngineLike | undefined;
|
|
91
|
+
const sinks: ActivitySinks = {
|
|
92
|
+
...(d1 ? { db: fromD1(d1) } : {}),
|
|
93
|
+
...(analytics ? { analytics } : {}),
|
|
94
|
+
ingestedAt: new Date().toISOString(),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Self-telemetry: a value-canary trip during server-side redaction means an
|
|
98
|
+
// emitter leaked (or tried to leak) a secret/PII into the payload. Emit a
|
|
99
|
+
// `system.ingest.rejected` event (itself routed to logs+analytics) so D6/D7 can
|
|
100
|
+
// alert — the fact of the trip, the action, never the offending value. Rejected
|
|
101
|
+
// events get the same self-telemetry so a spoof/unknown-action attempt is
|
|
102
|
+
// visible. Built here (synchronously) but fanned out off-path below.
|
|
103
|
+
const selfTelemetry = [
|
|
104
|
+
...accepted
|
|
105
|
+
.filter((a) => a.canaryTripped)
|
|
106
|
+
.map((a) =>
|
|
107
|
+
createActivityEvent({
|
|
108
|
+
action: "system.ingest.rejected",
|
|
109
|
+
actor: { kind: "system", id: "activity-ingest" },
|
|
110
|
+
source: "server",
|
|
111
|
+
outcome: { status: "refused", errorClass: "canary_tripped" },
|
|
112
|
+
scope: { tenantId: auth.tenantId },
|
|
113
|
+
payload: { args: { reason: "canary_tripped", attemptedAction: a.event.action } },
|
|
114
|
+
}).event,
|
|
115
|
+
),
|
|
116
|
+
...rejected.map((r: IngestRejection) =>
|
|
117
|
+
createActivityEvent({
|
|
118
|
+
action: "system.ingest.rejected",
|
|
119
|
+
actor: { kind: "system", id: "activity-ingest" },
|
|
120
|
+
source: "server",
|
|
121
|
+
outcome: { status: "refused", errorClass: r.reason },
|
|
122
|
+
scope: { tenantId: auth.tenantId },
|
|
123
|
+
payload: { args: { reason: r.reason, ...(r.attemptedAction ? { attemptedAction: r.attemptedAction } : {}) } },
|
|
124
|
+
}).event,
|
|
125
|
+
),
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
// Fan every accepted event + the self-telemetry out to its sinks, PAST the
|
|
129
|
+
// response. Best-effort per sink (never throws). The adapter's per-request
|
|
130
|
+
// ExecutionContext lives at `locals.runtime.ctx`; runAfterResponse floats the
|
|
131
|
+
// promise in dev/Node when it is absent.
|
|
132
|
+
const cfContext = (context.locals as { runtime?: { ctx?: CfExecutionContextLike } })?.runtime?.ctx ?? null;
|
|
133
|
+
const work = Promise.all([...accepted.map((a) => a.event), ...selfTelemetry].map((e) => fanOutActivity(e, sinks)));
|
|
134
|
+
runAfterResponse(work, cfContext);
|
|
135
|
+
|
|
136
|
+
return json({ ok: true, accepted: accepted.length, rejected }, 202);
|
|
137
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GET /api/admin/activity-alerts` — the polled/on-demand surface for card D7's
|
|
3
|
+
* operational-activity alert conditions.
|
|
4
|
+
*
|
|
5
|
+
* On-demand by design (see `lib/activity/alerts.ts` header): D7 deliberately does
|
|
6
|
+
* NOT add a cron/scheduled runner (no established precedent for one in this repo).
|
|
7
|
+
* This route runs `checkActivityAlerts()` fresh on every call, so it doubles as:
|
|
8
|
+
* (a) the data source for the `/admin/ops-timeline` alerts banner (that page
|
|
9
|
+
* calls the same function directly, in-process — this route is for OTHER
|
|
10
|
+
* callers), and
|
|
11
|
+
* (b) a lightweight endpoint an external uptime/monitor tool can poll on an
|
|
12
|
+
* interval, turning "on-demand" into "effectively continuous" without this
|
|
13
|
+
* repo standing up its own scheduler.
|
|
14
|
+
*
|
|
15
|
+
* ACCESS CONTROL mirrors `/admin/ops-timeline` exactly (same owner/admin-of-tenant
|
|
16
|
+
* or ToT-staff gate via `resolveAdminEntry`/`resolveAdminTenantMode`) rather than a
|
|
17
|
+
* new service-secret seam — this repo's other machine-to-machine surfaces
|
|
18
|
+
* (candidate-reconcile, the activity ingest route) authenticate a SPECIFIC known
|
|
19
|
+
* caller (MCP, an emitter) via a shared secret; there is no such caller for
|
|
20
|
+
* "a monitor polls for alerts" yet, and inventing one is the "new notification
|
|
21
|
+
* infra" the card scopes OUT. An admin/staff session (the same one that already
|
|
22
|
+
* views ops-timeline) is the correct, already-available caller today.
|
|
23
|
+
*
|
|
24
|
+
* TENANT ISOLATION — scoped to `Astro.locals.tenant.tenant_id` exactly like
|
|
25
|
+
* ops-timeline; never a URL/query value.
|
|
26
|
+
*/
|
|
27
|
+
import type { APIContext } from "astro";
|
|
28
|
+
import { readViewerSession } from "@/lib/auth/route";
|
|
29
|
+
import { resolveAdminEntry } from "@/lib/auth/adminEntry";
|
|
30
|
+
import { resolveAdminTenantMode } from "@/lib/adminTenantEnvelope";
|
|
31
|
+
import { readD1 } from "@/lib/env";
|
|
32
|
+
import { fromD1 } from "@/lib/d1/catalog";
|
|
33
|
+
import { checkActivityAlerts } from "@/lib/activity/alerts";
|
|
34
|
+
|
|
35
|
+
export const prerender = false;
|
|
36
|
+
|
|
37
|
+
const JSON_HEADERS = {
|
|
38
|
+
"content-type": "application/json; charset=utf-8",
|
|
39
|
+
"cache-control": "no-store",
|
|
40
|
+
"x-robots-tag": "noindex, nofollow",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function json(body: unknown, status = 200): Response {
|
|
44
|
+
return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function GET(context: APIContext): Promise<Response> {
|
|
48
|
+
const { tenant } = context.locals;
|
|
49
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
50
|
+
|
|
51
|
+
const viewerSession = await readViewerSession(context);
|
|
52
|
+
if (!viewerSession) return json({ ok: false, reason: "sign_in_required" }, 401);
|
|
53
|
+
|
|
54
|
+
const adminEntry = resolveAdminEntry({
|
|
55
|
+
record: viewerSession,
|
|
56
|
+
hostResource: tenant.appDomain,
|
|
57
|
+
nowSeconds,
|
|
58
|
+
});
|
|
59
|
+
const { isStaff } = resolveAdminTenantMode({
|
|
60
|
+
session: viewerSession,
|
|
61
|
+
adminAppTenant: tenant.appDomain,
|
|
62
|
+
requestedTarget: null,
|
|
63
|
+
nowSeconds,
|
|
64
|
+
});
|
|
65
|
+
const authorized = adminEntry.isOwnerViewer || isStaff;
|
|
66
|
+
if (!authorized) return json({ ok: false, reason: "not_authorized" }, 403);
|
|
67
|
+
|
|
68
|
+
const d1 = await readD1("STOREFRONT_APPS_DB");
|
|
69
|
+
if (!d1) return json({ ok: true, dbAvailable: false, alerts: [] });
|
|
70
|
+
|
|
71
|
+
const alerts = await checkActivityAlerts(fromD1(d1), tenant.tenant_id);
|
|
72
|
+
return json({ ok: true, dbAvailable: true, alerts });
|
|
73
|
+
}
|
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
json,
|
|
25
25
|
} from "@/lib/apps/adminService.js";
|
|
26
26
|
import { RegistryError } from "@/lib/apps/registryService.js";
|
|
27
|
+
import { resolveUiActor } from "@/lib/activity/uiActor";
|
|
28
|
+
import { recordActivity } from "@/lib/activity/recordActivity";
|
|
27
29
|
|
|
28
30
|
export const prerender = false;
|
|
29
31
|
|
|
@@ -50,11 +52,35 @@ export async function POST(context: APIContext): Promise<Response> {
|
|
|
50
52
|
const owned = await requireOwnedInstall(registry, installId, auth.tenantId);
|
|
51
53
|
if (owned instanceof Response) return owned;
|
|
52
54
|
|
|
55
|
+
// D10: acting cohort (merchant/admin), emitted source "server". SECURITY: the
|
|
56
|
+
// rotated secret (`issued.secret`) is NEVER placed in the payload — the allowlist
|
|
57
|
+
// names only the non-sensitive app handle (appId/env, from the owned install) +
|
|
58
|
+
// rotation time.
|
|
59
|
+
const activityActor = await resolveUiActor(context, { owner: auth.tenantId });
|
|
60
|
+
|
|
53
61
|
try {
|
|
54
62
|
const issued = await registry.rotateCredentials(installId, auth.actor);
|
|
63
|
+
recordActivity({
|
|
64
|
+
action: "app.credential.rotated",
|
|
65
|
+
actor: activityActor,
|
|
66
|
+
source: "server",
|
|
67
|
+
outcome: { status: "succeeded" },
|
|
68
|
+
scope: { tenantId: auth.tenantId },
|
|
69
|
+
payload: { args: { appId: owned.appId, env: owned.env, rotatedAt: new Date().toISOString() } },
|
|
70
|
+
});
|
|
55
71
|
return json({ credential: { clientId: issued.clientId, secret: issued.secret } });
|
|
56
72
|
} catch (err) {
|
|
57
|
-
if (err instanceof RegistryError)
|
|
73
|
+
if (err instanceof RegistryError) {
|
|
74
|
+
recordActivity({
|
|
75
|
+
action: "app.credential.rotated",
|
|
76
|
+
actor: activityActor,
|
|
77
|
+
source: "server",
|
|
78
|
+
outcome: { status: "failed", errorClass: err.code },
|
|
79
|
+
scope: { tenantId: auth.tenantId },
|
|
80
|
+
payload: { args: { appId: owned.appId, env: owned.env } },
|
|
81
|
+
});
|
|
82
|
+
return registryErrorResponse(err);
|
|
83
|
+
}
|
|
58
84
|
throw err;
|
|
59
85
|
}
|
|
60
86
|
}
|
|
@@ -13,6 +13,8 @@ import { resolveAdminSession } from "@/lib/apps/adminSession.js";
|
|
|
13
13
|
import { getRegistryService, registryErrorResponse, json } from "@/lib/apps/adminService.js";
|
|
14
14
|
import { RegistryError, type InstallInput } from "@/lib/apps/registryService.js";
|
|
15
15
|
import { emitWebhookEvent } from "@/lib/webhooks/emit.js";
|
|
16
|
+
import { resolveUiActor } from "@/lib/activity/uiActor";
|
|
17
|
+
import { recordActivity } from "@/lib/activity/recordActivity";
|
|
16
18
|
|
|
17
19
|
export const prerender = false;
|
|
18
20
|
|
|
@@ -75,6 +77,11 @@ export async function POST(context: APIContext): Promise<Response> {
|
|
|
75
77
|
const registry = await getRegistryService();
|
|
76
78
|
if (registry instanceof Response) return registry;
|
|
77
79
|
|
|
80
|
+
// D10: resolve the acting cohort ONCE (merchant/admin — a store owner managing
|
|
81
|
+
// their own app, or ToT staff operating-as). Emitted as source "server" (the
|
|
82
|
+
// app.* catalog source), best-effort, never alters control flow.
|
|
83
|
+
const activityActor = await resolveUiActor(context, { owner: auth.tenantId });
|
|
84
|
+
|
|
78
85
|
try {
|
|
79
86
|
const result = await registry.install(input, { tenantId: auth.tenantId, actor: auth.actor });
|
|
80
87
|
try {
|
|
@@ -92,9 +99,27 @@ export async function POST(context: APIContext): Promise<Response> {
|
|
|
92
99
|
}),
|
|
93
100
|
);
|
|
94
101
|
}
|
|
102
|
+
recordActivity({
|
|
103
|
+
action: "app.install",
|
|
104
|
+
actor: activityActor,
|
|
105
|
+
source: "server",
|
|
106
|
+
outcome: { status: "succeeded" },
|
|
107
|
+
scope: { tenantId: auth.tenantId },
|
|
108
|
+
payload: { args: { appId: input.appId, appVersion: input.appVersion, env: input.env } },
|
|
109
|
+
});
|
|
95
110
|
return json(result, 201);
|
|
96
111
|
} catch (err) {
|
|
97
|
-
if (err instanceof RegistryError)
|
|
112
|
+
if (err instanceof RegistryError) {
|
|
113
|
+
recordActivity({
|
|
114
|
+
action: "app.install",
|
|
115
|
+
actor: activityActor,
|
|
116
|
+
source: "server",
|
|
117
|
+
outcome: { status: err.code === "scope_denied" ? "refused" : "failed", errorClass: err.code },
|
|
118
|
+
scope: { tenantId: auth.tenantId },
|
|
119
|
+
payload: { args: { appId: input.appId, appVersion: input.appVersion, env: input.env } },
|
|
120
|
+
});
|
|
121
|
+
return registryErrorResponse(err);
|
|
122
|
+
}
|
|
98
123
|
throw err;
|
|
99
124
|
}
|
|
100
125
|
}
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
json,
|
|
21
21
|
} from "@/lib/apps/adminService.js";
|
|
22
22
|
import { RegistryError } from "@/lib/apps/registryService.js";
|
|
23
|
+
import { resolveUiActor } from "@/lib/activity/uiActor";
|
|
24
|
+
import { recordActivity } from "@/lib/activity/recordActivity";
|
|
23
25
|
|
|
24
26
|
export const prerender = false;
|
|
25
27
|
|
|
@@ -46,11 +48,32 @@ export async function POST(context: APIContext): Promise<Response> {
|
|
|
46
48
|
const owned = await requireOwnedInstall(registry, installId, auth.tenantId);
|
|
47
49
|
if (owned instanceof Response) return owned;
|
|
48
50
|
|
|
51
|
+
// D10: acting cohort (merchant/admin), emitted source "server" per the app.* catalog.
|
|
52
|
+
const activityActor = await resolveUiActor(context, { owner: auth.tenantId });
|
|
53
|
+
|
|
49
54
|
try {
|
|
50
55
|
const install = await registry.resume(installId, auth.actor);
|
|
56
|
+
recordActivity({
|
|
57
|
+
action: "app.resume",
|
|
58
|
+
actor: activityActor,
|
|
59
|
+
source: "server",
|
|
60
|
+
outcome: { status: "succeeded" },
|
|
61
|
+
scope: { tenantId: auth.tenantId },
|
|
62
|
+
payload: { args: { appId: owned.appId, env: owned.env } },
|
|
63
|
+
});
|
|
51
64
|
return json({ install });
|
|
52
65
|
} catch (err) {
|
|
53
|
-
if (err instanceof RegistryError)
|
|
66
|
+
if (err instanceof RegistryError) {
|
|
67
|
+
recordActivity({
|
|
68
|
+
action: "app.resume",
|
|
69
|
+
actor: activityActor,
|
|
70
|
+
source: "server",
|
|
71
|
+
outcome: { status: err.code === "invalid_state" ? "refused" : "failed", errorClass: err.code },
|
|
72
|
+
scope: { tenantId: auth.tenantId },
|
|
73
|
+
payload: { args: { appId: owned.appId, env: owned.env } },
|
|
74
|
+
});
|
|
75
|
+
return registryErrorResponse(err);
|
|
76
|
+
}
|
|
54
77
|
throw err;
|
|
55
78
|
}
|
|
56
79
|
}
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
json,
|
|
22
22
|
} from "@/lib/apps/adminService.js";
|
|
23
23
|
import { RegistryError } from "@/lib/apps/registryService.js";
|
|
24
|
+
import { resolveUiActor } from "@/lib/activity/uiActor";
|
|
25
|
+
import { recordActivity } from "@/lib/activity/recordActivity";
|
|
24
26
|
|
|
25
27
|
export const prerender = false;
|
|
26
28
|
|
|
@@ -47,11 +49,32 @@ export async function POST(context: APIContext): Promise<Response> {
|
|
|
47
49
|
const owned = await requireOwnedInstall(registry, installId, auth.tenantId);
|
|
48
50
|
if (owned instanceof Response) return owned;
|
|
49
51
|
|
|
52
|
+
// D10: acting cohort (merchant/admin), emitted source "server" per the app.* catalog.
|
|
53
|
+
const activityActor = await resolveUiActor(context, { owner: auth.tenantId });
|
|
54
|
+
|
|
50
55
|
try {
|
|
51
56
|
const install = await registry.suspend(installId, auth.actor);
|
|
57
|
+
recordActivity({
|
|
58
|
+
action: "app.suspend",
|
|
59
|
+
actor: activityActor,
|
|
60
|
+
source: "server",
|
|
61
|
+
outcome: { status: "succeeded" },
|
|
62
|
+
scope: { tenantId: auth.tenantId },
|
|
63
|
+
payload: { args: { appId: owned.appId, env: owned.env } },
|
|
64
|
+
});
|
|
52
65
|
return json({ install });
|
|
53
66
|
} catch (err) {
|
|
54
|
-
if (err instanceof RegistryError)
|
|
67
|
+
if (err instanceof RegistryError) {
|
|
68
|
+
recordActivity({
|
|
69
|
+
action: "app.suspend",
|
|
70
|
+
actor: activityActor,
|
|
71
|
+
source: "server",
|
|
72
|
+
outcome: { status: err.code === "invalid_state" ? "refused" : "failed", errorClass: err.code },
|
|
73
|
+
scope: { tenantId: auth.tenantId },
|
|
74
|
+
payload: { args: { appId: owned.appId, env: owned.env } },
|
|
75
|
+
});
|
|
76
|
+
return registryErrorResponse(err);
|
|
77
|
+
}
|
|
55
78
|
throw err;
|
|
56
79
|
}
|
|
57
80
|
}
|
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
} from "@/lib/apps/adminService.js";
|
|
23
23
|
import { RegistryError } from "@/lib/apps/registryService.js";
|
|
24
24
|
import { emitWebhookEvent } from "@/lib/webhooks/emit.js";
|
|
25
|
+
import { resolveUiActor } from "@/lib/activity/uiActor";
|
|
26
|
+
import { recordActivity } from "@/lib/activity/recordActivity";
|
|
25
27
|
|
|
26
28
|
export const prerender = false;
|
|
27
29
|
|
|
@@ -48,6 +50,10 @@ export async function POST(context: APIContext): Promise<Response> {
|
|
|
48
50
|
const owned = await requireOwnedInstall(registry, installId, auth.tenantId);
|
|
49
51
|
if (owned instanceof Response) return owned;
|
|
50
52
|
|
|
53
|
+
// D10: acting cohort (merchant/admin), emitted source "server" per the app.*
|
|
54
|
+
// catalog. Best-effort, never alters control flow.
|
|
55
|
+
const activityActor = await resolveUiActor(context, { owner: auth.tenantId });
|
|
56
|
+
|
|
51
57
|
try {
|
|
52
58
|
const install = await registry.uninstall(installId, auth.actor);
|
|
53
59
|
try {
|
|
@@ -66,9 +72,27 @@ export async function POST(context: APIContext): Promise<Response> {
|
|
|
66
72
|
}),
|
|
67
73
|
);
|
|
68
74
|
}
|
|
75
|
+
recordActivity({
|
|
76
|
+
action: "app.uninstall",
|
|
77
|
+
actor: activityActor,
|
|
78
|
+
source: "server",
|
|
79
|
+
outcome: { status: "succeeded" },
|
|
80
|
+
scope: { tenantId: auth.tenantId },
|
|
81
|
+
payload: { args: { appId: owned.appId, env: owned.env } },
|
|
82
|
+
});
|
|
69
83
|
return json({ install });
|
|
70
84
|
} catch (err) {
|
|
71
|
-
if (err instanceof RegistryError)
|
|
85
|
+
if (err instanceof RegistryError) {
|
|
86
|
+
recordActivity({
|
|
87
|
+
action: "app.uninstall",
|
|
88
|
+
actor: activityActor,
|
|
89
|
+
source: "server",
|
|
90
|
+
outcome: { status: "failed", errorClass: err.code },
|
|
91
|
+
scope: { tenantId: auth.tenantId },
|
|
92
|
+
payload: { args: { appId: owned.appId, env: owned.env } },
|
|
93
|
+
});
|
|
94
|
+
return registryErrorResponse(err);
|
|
95
|
+
}
|
|
72
96
|
throw err;
|
|
73
97
|
}
|
|
74
98
|
}
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
json,
|
|
22
22
|
} from "@/lib/apps/adminService.js";
|
|
23
23
|
import { RegistryError, type AppInstallPatch } from "@/lib/apps/registryService.js";
|
|
24
|
+
import { resolveUiActor } from "@/lib/activity/uiActor";
|
|
25
|
+
import { recordActivity } from "@/lib/activity/recordActivity";
|
|
24
26
|
|
|
25
27
|
export const prerender = false;
|
|
26
28
|
|
|
@@ -93,11 +95,32 @@ export async function POST(context: APIContext): Promise<Response> {
|
|
|
93
95
|
const owned = await requireOwnedInstall(registry, installId, auth.tenantId);
|
|
94
96
|
if (owned instanceof Response) return owned;
|
|
95
97
|
|
|
98
|
+
// D10: acting cohort (merchant/admin), emitted source "server" per the app.* catalog.
|
|
99
|
+
const activityActor = await resolveUiActor(context, { owner: auth.tenantId });
|
|
100
|
+
|
|
96
101
|
try {
|
|
97
102
|
const install = await registry.update(installId, patch, auth.actor);
|
|
103
|
+
recordActivity({
|
|
104
|
+
action: "app.update",
|
|
105
|
+
actor: activityActor,
|
|
106
|
+
source: "server",
|
|
107
|
+
outcome: { status: "succeeded" },
|
|
108
|
+
scope: { tenantId: auth.tenantId },
|
|
109
|
+
payload: { args: { appId: owned.appId, env: owned.env } },
|
|
110
|
+
});
|
|
98
111
|
return json({ install });
|
|
99
112
|
} catch (err) {
|
|
100
|
-
if (err instanceof RegistryError)
|
|
113
|
+
if (err instanceof RegistryError) {
|
|
114
|
+
recordActivity({
|
|
115
|
+
action: "app.update",
|
|
116
|
+
actor: activityActor,
|
|
117
|
+
source: "server",
|
|
118
|
+
outcome: { status: err.code === "invalid_state" ? "refused" : "failed", errorClass: err.code },
|
|
119
|
+
scope: { tenantId: auth.tenantId },
|
|
120
|
+
payload: { args: { appId: owned.appId, env: owned.env } },
|
|
121
|
+
});
|
|
122
|
+
return registryErrorResponse(err);
|
|
123
|
+
}
|
|
101
124
|
throw err;
|
|
102
125
|
}
|
|
103
126
|
}
|
|
@@ -27,6 +27,7 @@ import { WebhookDeliveryStore } from "@/lib/webhooks/deliveryStore.js";
|
|
|
27
27
|
import { getDispatcher } from "@/lib/webhooks/getDispatcher.js";
|
|
28
28
|
import { fromD1 } from "@/lib/d1/catalog.js";
|
|
29
29
|
import { readD1 } from "@/lib/env";
|
|
30
|
+
import { resolveUiActor, recordUiActivity } from "@/lib/activity/uiActor";
|
|
30
31
|
|
|
31
32
|
export const prerender = false;
|
|
32
33
|
|
|
@@ -55,5 +56,15 @@ export async function POST(context: APIContext): Promise<Response> {
|
|
|
55
56
|
const requeued = await store.requeueForReplay(deliveryId);
|
|
56
57
|
await dispatcher.enqueue({ deliveryId });
|
|
57
58
|
|
|
59
|
+
// D11: ops/support console replay. Resolve the acting cohort (admin/merchant per
|
|
60
|
+
// resolveOperatorSession) from the session; `surface: "admin"` disambiguates from the
|
|
61
|
+
// app-facing v1 call site that shares this key. deliveryId is the opaque wire id.
|
|
62
|
+
const activityActor = await resolveUiActor(context, {});
|
|
63
|
+
recordUiActivity(activityActor, {
|
|
64
|
+
action: "webhook.delivery.replayed",
|
|
65
|
+
outcome: { status: "succeeded" },
|
|
66
|
+
scope: { tenantId: auth.tenantId },
|
|
67
|
+
payload: { args: { deliveryId, surface: "admin" } },
|
|
68
|
+
});
|
|
58
69
|
return json({ delivery: requeued }, 202);
|
|
59
70
|
}
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type OrderForwardInput,
|
|
25
25
|
} from "@/lib/apps/orders/orderForwardReceiver.js";
|
|
26
26
|
import type { OrderLineItem, OrderStatus } from "@/lib/apps/orders/ordersStore.js";
|
|
27
|
+
import { recordActivity } from "@/lib/activity/recordActivity";
|
|
27
28
|
|
|
28
29
|
export const prerender = false;
|
|
29
30
|
|
|
@@ -168,5 +169,15 @@ export async function POST(context: APIContext): Promise<Response> {
|
|
|
168
169
|
if (!d1) return json({ error: "orders store not configured" }, 503);
|
|
169
170
|
|
|
170
171
|
const result = await receiveForwardedOrder(fromD1(d1), input);
|
|
172
|
+
// D11: HIGH-VOLUME machine action (the tot-foxycart forwarder, HMAC-authed) → analytics
|
|
173
|
+
// only, never timelined. Coarse dimensions only; customer/order ids never stored.
|
|
174
|
+
recordActivity({
|
|
175
|
+
action: "app.order.forwarded",
|
|
176
|
+
actor: { kind: "agent", id: "order-forwarder" },
|
|
177
|
+
source: "server",
|
|
178
|
+
outcome: { status: "succeeded" },
|
|
179
|
+
scope: { tenantId: input.tenantId },
|
|
180
|
+
payload: { args: { status: result.order.status, env: input.env, currency: input.currency } },
|
|
181
|
+
});
|
|
171
182
|
return json({ ok: true, status: result.order.status, emitted: result.emitted }, 200);
|
|
172
183
|
}
|
|
@@ -18,6 +18,7 @@ import { checkIdempotency, computeBodyHash, recordIdempotency } from "@/lib/apps
|
|
|
18
18
|
import { AppAttributionService, AttributionError } from "@/lib/apps/orders/attributionService.js";
|
|
19
19
|
import { fromD1 } from "@/lib/d1/catalog.js";
|
|
20
20
|
import { readD1 } from "@/lib/env";
|
|
21
|
+
import { recordActivity } from "@/lib/activity/recordActivity";
|
|
21
22
|
|
|
22
23
|
export const prerender = false;
|
|
23
24
|
|
|
@@ -130,6 +131,14 @@ export async function POST(context: APIContext): Promise<Response> {
|
|
|
130
131
|
});
|
|
131
132
|
} catch (err) {
|
|
132
133
|
if (err instanceof AttributionError) {
|
|
134
|
+
recordActivity({
|
|
135
|
+
action: "app.attribution.recorded",
|
|
136
|
+
actor: { kind: "agent", id: auth.install.installId },
|
|
137
|
+
source: "server",
|
|
138
|
+
outcome: { status: "failed", errorClass: err.code },
|
|
139
|
+
scope: { tenantId: auth.install.tenantId },
|
|
140
|
+
...(parsed.campaign ? { payload: { args: { campaign: parsed.campaign } } } : {}),
|
|
141
|
+
});
|
|
133
142
|
const status =
|
|
134
143
|
err.code === "attribution_forbidden" ? 403 : err.code === "attribution_conflict" ? 409 : 400;
|
|
135
144
|
return withRequestId(json({ error: err.message, code: err.code }, status), context);
|
|
@@ -138,5 +147,16 @@ export async function POST(context: APIContext): Promise<Response> {
|
|
|
138
147
|
}
|
|
139
148
|
|
|
140
149
|
await recordIdempotency(db, auth.install.installId, idempotencyKey, bodyHash, record);
|
|
150
|
+
// D11: HIGH-VOLUME machine action (an installed private app / external bot, app-JWT
|
|
151
|
+
// authed) → analytics only, never timelined. installId is an opaque, per-install handle
|
|
152
|
+
// (non-PII) — a good stable actor id. referralCode/customerHash are never stored.
|
|
153
|
+
recordActivity({
|
|
154
|
+
action: "app.attribution.recorded",
|
|
155
|
+
actor: { kind: "agent", id: auth.install.installId },
|
|
156
|
+
source: "server",
|
|
157
|
+
outcome: { status: "succeeded" },
|
|
158
|
+
scope: { tenantId: auth.install.tenantId },
|
|
159
|
+
...(parsed.campaign ? { payload: { args: { campaign: parsed.campaign } } } : {}),
|
|
160
|
+
});
|
|
141
161
|
return withRequestId(json(record, 201), context);
|
|
142
162
|
}
|
|
@@ -27,6 +27,7 @@ import { toContractDelivery } from "@/lib/webhooks/deliveryMapper.js";
|
|
|
27
27
|
import { getDispatcher } from "@/lib/webhooks/getDispatcher.js";
|
|
28
28
|
import { fromD1 } from "@/lib/d1/catalog.js";
|
|
29
29
|
import { readD1 } from "@/lib/env";
|
|
30
|
+
import { recordActivity } from "@/lib/activity/recordActivity";
|
|
30
31
|
|
|
31
32
|
export const prerender = false;
|
|
32
33
|
|
|
@@ -58,5 +59,16 @@ export async function POST(context: APIContext): Promise<Response> {
|
|
|
58
59
|
const requeued = await store.requeueForReplay(deliveryId);
|
|
59
60
|
await dispatcher.enqueue({ deliveryId });
|
|
60
61
|
|
|
62
|
+
// D11: the app-facing (install-owned, app-JWT authed) replay — the `agent` cohort. Same
|
|
63
|
+
// catalog key as the admin console call site; `surface: "v1"` disambiguates. installId
|
|
64
|
+
// is an opaque per-install handle; deliveryId is the opaque CloudEvents wire id.
|
|
65
|
+
recordActivity({
|
|
66
|
+
action: "webhook.delivery.replayed",
|
|
67
|
+
actor: { kind: "agent", id: auth.install.installId },
|
|
68
|
+
source: "server",
|
|
69
|
+
outcome: { status: "succeeded" },
|
|
70
|
+
scope: { tenantId: auth.install.tenantId },
|
|
71
|
+
payload: { args: { deliveryId, surface: "v1" } },
|
|
72
|
+
});
|
|
61
73
|
return withRequestId(json(toContractDelivery(requeued), 202), context);
|
|
62
74
|
}
|