@tokenoftrust/storefront-runner 2.4.56 → 2.4.57

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.
@@ -0,0 +1,145 @@
1
+ /**
2
+ * GET/POST /api/admin/commerce-readiness
3
+ *
4
+ * Phase 1: the client fulfills in their own carrier account. This route reports
5
+ * whether order and payment reconcile has been exercised. It does not read an
6
+ * order row, write a payment, or call a carrier. Postage is not a result.
7
+ *
8
+ * The exercised comparison is scripts/commerce/scrappy-reconcile.mjs. This
9
+ * surface has no ledger, so configured, absent, and unreadable stay unhealthy.
10
+ */
11
+ import type { APIContext } from "astro";
12
+ import { requireAdminApiEntry } from "@/lib/auth/adminApiGuard";
13
+ import { workerCommit } from "@/lib/activity/workerCommit";
14
+ import { readEnv } from "@/lib/env";
15
+ import {
16
+ COMMERCE_SCHEDULER,
17
+ actorMayCommand,
18
+ decideCommerceCommand,
19
+ type CommerceEvidence,
20
+ type CommerceStage,
21
+ } from "@tot/public-runtime";
22
+
23
+ export const prerender = false;
24
+
25
+ const JSON_HEADERS = {
26
+ "content-type": "application/json; charset=utf-8",
27
+ "cache-control": "no-store",
28
+ "x-robots-tag": "noindex, nofollow",
29
+ };
30
+
31
+ const READINESS = ["configured-only", "exercised", "stale", "failed", "absent", "unreadable"] as const;
32
+ type Readiness = (typeof READINESS)[number];
33
+
34
+ export interface CommerceReadinessReport {
35
+ schema: "scrappy-reconcile-v1";
36
+ readiness: Readiness;
37
+ healthy: boolean;
38
+ sourceSha: string | null;
39
+ deployedSha: string | null;
40
+ watermark: { asOf: string; source: { cursor: string | null }; tot: { cursor: string | null } } | null;
41
+ orderCount: number | null;
42
+ capturedMinor: number | null;
43
+ refundedMinor: number | null;
44
+ labelAndPostageCompared: false;
45
+ discrepancies: { code: string; id: string; owner: string }[] | null;
46
+ owner: "owner";
47
+ }
48
+
49
+ const EVIDENCE: CommerceEvidence = { verification: "UNKNOWN", tax: "UNKNOWN", risk: "UNKNOWN", stock: "UNKNOWN" };
50
+ const REPLAY_KEYS = new Set(["kind", "orderId", "sourceEventId", "stage"]);
51
+ const OPAQUE = /^[A-Za-z0-9:_-]{1,80}$/;
52
+
53
+ function json(body: unknown, status = 200): Response {
54
+ return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
55
+ }
56
+
57
+ export function projectAdminReadiness(input: {
58
+ scheduler: string | undefined;
59
+ deployedSha: string;
60
+ }): CommerceReadinessReport {
61
+ const configured = input.scheduler === COMMERCE_SCHEDULER;
62
+ const deployedSha = input.deployedSha.length > 0 ? input.deployedSha : null;
63
+ const readiness: Readiness = deployedSha === null ? "unreadable" : configured ? "configured-only" : "absent";
64
+ return {
65
+ schema: "scrappy-reconcile-v1",
66
+ readiness,
67
+ healthy: false,
68
+ sourceSha: null,
69
+ deployedSha,
70
+ watermark: null,
71
+ orderCount: null,
72
+ capturedMinor: null,
73
+ refundedMinor: null,
74
+ labelAndPostageCompared: false,
75
+ discrepancies: readiness === "unreadable" ? null : [],
76
+ owner: "owner",
77
+ };
78
+ }
79
+
80
+ function replayStage(value: unknown): CommerceStage | null {
81
+ return value === "preview" || value === "publication" ? value : null;
82
+ }
83
+
84
+ export function admitAdminReplay(input: {
85
+ tenantId: string;
86
+ stage: CommerceStage;
87
+ kind: "order" | "refund";
88
+ }): { admitted: boolean; reason: "admitted" | "denied" | "refused" } {
89
+ const actor = { role: "owner" as const, tenantId: input.tenantId, stage: input.stage };
90
+ const resource = { tenantId: input.tenantId, stage: input.stage };
91
+ if (!actorMayCommand(actor, resource)) return { admitted: false, reason: "denied" };
92
+ const decision = decideCommerceCommand({
93
+ command: "reconcile",
94
+ payment: input.kind === "refund" ? "refund_unknown" : "captured",
95
+ stock: "UNKNOWN",
96
+ fulfillment: "UNKNOWN",
97
+ label: "none",
98
+ evidence: EVIDENCE,
99
+ providerOutcome: null,
100
+ });
101
+ if (!decision.admit || decision.restock !== false || decision.blindRepurchase !== false) {
102
+ return { admitted: false, reason: "refused" };
103
+ }
104
+ return { admitted: true, reason: "admitted" };
105
+ }
106
+
107
+ export async function GET(context: APIContext): Promise<Response> {
108
+ const access = await requireAdminApiEntry(context, { requireOwner: true });
109
+ if (!access.ok) return json({ ok: false, reason: access.reason }, access.status);
110
+ const scheduler = await readEnv("SCRAPPY_COMMERCE_SCHEDULER");
111
+ return json({ ok: true, report: projectAdminReadiness({ scheduler, deployedSha: workerCommit() }) });
112
+ }
113
+
114
+ export async function POST(context: APIContext): Promise<Response> {
115
+ const access = await requireAdminApiEntry(context, { requireOwner: true });
116
+ if (!access.ok) return json({ ok: false, reason: access.reason }, access.status);
117
+ const text = await context.request.text();
118
+ if (text.length > 2000) return json({ ok: false, reason: "refused" }, 400);
119
+ let body: unknown;
120
+ try {
121
+ body = JSON.parse(text);
122
+ } catch {
123
+ return json({ ok: false, reason: "refused" }, 400);
124
+ }
125
+ if (body === null || typeof body !== "object" || Array.isArray(body)) return json({ ok: false, reason: "refused" }, 400);
126
+ const record = body as Record<string, unknown>;
127
+ if (Object.keys(record).some((key) => !REPLAY_KEYS.has(key))) return json({ ok: false, reason: "refused" }, 400);
128
+ const stage = replayStage(record.stage);
129
+ const kind = record.kind === "order" || record.kind === "refund" ? record.kind : null;
130
+ if (!stage || !kind || typeof record.orderId !== "string" || typeof record.sourceEventId !== "string") {
131
+ return json({ ok: false, reason: "refused" }, 400);
132
+ }
133
+ if (!OPAQUE.test(record.orderId) || !OPAQUE.test(record.sourceEventId)) return json({ ok: false, reason: "refused" }, 400);
134
+ const decision = admitAdminReplay({ tenantId: context.locals.tenant.tenant_id, stage, kind });
135
+ if (!decision.admitted) return json({ ok: false, reason: decision.reason }, 403);
136
+ const scheduler = await readEnv("SCRAPPY_COMMERCE_SCHEDULER");
137
+ return json({
138
+ ok: true,
139
+ admitted: true,
140
+ durableWrite: false,
141
+ command: "reconcile",
142
+ reason: decision.reason,
143
+ report: projectAdminReadiness({ scheduler, deployedSha: workerCommit() }),
144
+ });
145
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.4.56",
3
+ "version": "2.4.57",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",