@tokenoftrust/storefront-runner 2.4.50 → 2.4.52
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/src/lib/admin/orderClient.ts +45 -11
- package/apps/storefront/src/lib/admin/services/orders.ts +524 -87
- package/apps/storefront/src/lib/admin/viewmodels/orders.ts +37 -1
- package/apps/storefront/src/lib/commerce/finalCartAuthorization.ts +234 -0
- package/apps/storefront/src/lib/inventory/allocations.ts +749 -0
- package/apps/storefront/src/pages/api/admin/orders/[id].ts +20 -5
- package/apps/storefront/src/pages/api/admin/orders.ts +8 -12
- package/apps/storefront/src/pages/api/commerce/final-cart-authorization.ts +366 -0
- package/package.json +1 -1
|
@@ -2,18 +2,22 @@
|
|
|
2
2
|
* `GET /api/admin/orders/:id` — one order's full detail for the Orders tab.
|
|
3
3
|
*
|
|
4
4
|
* Server-side proxy to checkout `GET /commerce/orders/:orderId` for the
|
|
5
|
-
* resolved tenant + env. Bearer stays server-side
|
|
6
|
-
*
|
|
7
|
-
* Read-only
|
|
5
|
+
* resolved tenant + env. Bearer stays server-side. Address is included only
|
|
6
|
+
* for an owner of this tenant and stage. Readiness is assessed, not inferred.
|
|
7
|
+
* Read-only.
|
|
8
8
|
*/
|
|
9
9
|
import type { APIContext } from "astro";
|
|
10
10
|
import { readEnv } from "@/lib/env";
|
|
11
11
|
import { requireAdminApiEntry } from "@/lib/auth/adminApiGuard";
|
|
12
12
|
import {
|
|
13
|
+
commerceActorForAdmin,
|
|
14
|
+
commerceStageForCustomization,
|
|
13
15
|
createCheckoutOrderClient,
|
|
14
16
|
orderApiBaseUrl,
|
|
15
17
|
OrderApiError,
|
|
18
|
+
projectCheckoutOrder,
|
|
16
19
|
} from "@/lib/admin/orderClient";
|
|
20
|
+
import { assessCheckoutOrder } from "@/lib/admin/services/orders";
|
|
17
21
|
|
|
18
22
|
export const prerender = false;
|
|
19
23
|
|
|
@@ -49,8 +53,19 @@ export async function GET(context: APIContext): Promise<Response> {
|
|
|
49
53
|
});
|
|
50
54
|
|
|
51
55
|
try {
|
|
52
|
-
const
|
|
53
|
-
|
|
56
|
+
const stage = commerceStageForCustomization(customizationEnv);
|
|
57
|
+
const actor = commerceActorForAdmin({
|
|
58
|
+
tenantId: tenant.appDomain,
|
|
59
|
+
stage,
|
|
60
|
+
owner: access.entry.isOwnerViewer,
|
|
61
|
+
admitted: access.entry.admitted,
|
|
62
|
+
});
|
|
63
|
+
const order = projectCheckoutOrder(await client.getOrder(orderId), actor, {
|
|
64
|
+
tenantId: tenant.appDomain,
|
|
65
|
+
stage,
|
|
66
|
+
});
|
|
67
|
+
const readiness = assessCheckoutOrder(order);
|
|
68
|
+
return new Response(JSON.stringify({ order, readiness }), { headers: JSON_HEADERS });
|
|
54
69
|
} catch (err) {
|
|
55
70
|
if (err instanceof OrderApiError) return jsonError(err.code, err.status);
|
|
56
71
|
return jsonError("unexpected", 500);
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
* host/approver layer) — same posture as `api/evidence.json.ts`; no new gate here.
|
|
11
11
|
*/
|
|
12
12
|
import type { APIContext } from "astro";
|
|
13
|
-
import type { CheckoutOrderListFilters } from "@tot/public-runtime";
|
|
14
13
|
import { readEnv } from "@/lib/env";
|
|
15
14
|
import { requireAdminApiEntry } from "@/lib/auth/adminApiGuard";
|
|
16
15
|
import {
|
|
@@ -18,6 +17,7 @@ import {
|
|
|
18
17
|
orderApiBaseUrl,
|
|
19
18
|
OrderApiError,
|
|
20
19
|
} from "@/lib/admin/orderClient";
|
|
20
|
+
import { assessCheckoutOrder, parseOrderFilters } from "@/lib/admin/services/orders";
|
|
21
21
|
|
|
22
22
|
export const prerender = false;
|
|
23
23
|
|
|
@@ -50,16 +50,8 @@ export async function GET(context: APIContext): Promise<Response> {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
const url = new URL(context.request.url);
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
const filters: CheckoutOrderListFilters = {
|
|
56
|
-
queue: q.get("queue") ?? undefined,
|
|
57
|
-
status: q.get("status") ?? undefined,
|
|
58
|
-
from: q.get("from") ?? undefined,
|
|
59
|
-
to: q.get("to") ?? undefined,
|
|
60
|
-
limit: limitRaw != null ? Number(limitRaw) : undefined,
|
|
61
|
-
cursor: q.get("cursor") ?? undefined,
|
|
62
|
-
};
|
|
53
|
+
// Tenant comes from the host, never from a query parameter.
|
|
54
|
+
const filters = parseOrderFilters(url.searchParams);
|
|
63
55
|
|
|
64
56
|
const client = createCheckoutOrderClient({
|
|
65
57
|
baseUrl: orderApiBaseUrl(customizationEnv),
|
|
@@ -69,7 +61,11 @@ export async function GET(context: APIContext): Promise<Response> {
|
|
|
69
61
|
|
|
70
62
|
try {
|
|
71
63
|
const data = await client.listOrders(filters);
|
|
72
|
-
|
|
64
|
+
const orders = data.orders.map((order) => ({
|
|
65
|
+
...order,
|
|
66
|
+
readiness: assessCheckoutOrder(order),
|
|
67
|
+
}));
|
|
68
|
+
return new Response(JSON.stringify({ ...data, orders }), { headers: JSON_HEADERS });
|
|
73
69
|
} catch (err) {
|
|
74
70
|
if (err instanceof OrderApiError) return jsonError(err.code, err.status);
|
|
75
71
|
return jsonError("unexpected", 500);
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/commerce/final-cart-authorization — time-limited final-cart admission.
|
|
3
|
+
* Caller-owned asymmetric tokens only. Catalog availability, a browser confirmation,
|
|
4
|
+
* and missing verification evidence do not authorize payment.
|
|
5
|
+
*/
|
|
6
|
+
import type { APIContext } from "astro";
|
|
7
|
+
import { createLocalJWKSet, decodeJwt, decodeProtectedHeader, jwtVerify, type JSONWebKeySet, type JWTPayload } from "jose";
|
|
8
|
+
import type { TenantConfig } from "@tot/public-runtime";
|
|
9
|
+
import {
|
|
10
|
+
type CallerToken,
|
|
11
|
+
type CommerceEvidence,
|
|
12
|
+
type CommerceStage,
|
|
13
|
+
type EvidenceState,
|
|
14
|
+
type FinalCartBinding,
|
|
15
|
+
type FulfillmentState,
|
|
16
|
+
type LabelState,
|
|
17
|
+
type PaymentState,
|
|
18
|
+
type StockState,
|
|
19
|
+
} from "@tot/public-runtime";
|
|
20
|
+
import { fromD1, listAllActiveProducts } from "@/lib/d1/catalog.js";
|
|
21
|
+
import { readD1, readEnv } from "@/lib/env";
|
|
22
|
+
import { evaluateFinalCart, type SeamView } from "@/lib/commerce/finalCartAuthorization.js";
|
|
23
|
+
import { d1CommerceRunner, mirrorOnHand, reserveAllocation } from "@/lib/inventory/allocations.js";
|
|
24
|
+
import { POST as compliancePreflight } from "../compliance/preflight.js";
|
|
25
|
+
|
|
26
|
+
export const prerender = false;
|
|
27
|
+
|
|
28
|
+
const EVIDENCE_STATES = new Set(["fresh", "UNKNOWN", "stale", "unreadable", "absent"]);
|
|
29
|
+
const STAGES = new Set(["preview", "publication"]);
|
|
30
|
+
|
|
31
|
+
function json(body: unknown, status = 200): Response {
|
|
32
|
+
return new Response(JSON.stringify(body), {
|
|
33
|
+
status,
|
|
34
|
+
headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function deny(finding: string, status: number, owner?: string): Response {
|
|
39
|
+
return json(owner ? { ok: false, finding, owner } : { ok: false, finding }, status);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function statusFor(finding: string): number {
|
|
43
|
+
if (finding === "store-unconfigured" || finding === "caller-unconfigured") return 503;
|
|
44
|
+
if (
|
|
45
|
+
finding === "browser-not-authority" ||
|
|
46
|
+
finding === "tenant-unbound" ||
|
|
47
|
+
finding === "shared-secret" ||
|
|
48
|
+
finding === "signature" ||
|
|
49
|
+
finding === "kid" ||
|
|
50
|
+
finding === "issuer" ||
|
|
51
|
+
finding === "audience" ||
|
|
52
|
+
finding === "expired" ||
|
|
53
|
+
finding === "lifetime" ||
|
|
54
|
+
finding === "jti" ||
|
|
55
|
+
finding === "tenant" ||
|
|
56
|
+
finding === "stage"
|
|
57
|
+
) {
|
|
58
|
+
return 401;
|
|
59
|
+
}
|
|
60
|
+
return 409;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function claimString(payload: JWTPayload, key: string): string {
|
|
64
|
+
const value = payload[key];
|
|
65
|
+
return typeof value === "string" ? value : "";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseList(value: string | undefined): string[] | null {
|
|
69
|
+
if (!value || value.trim() === "") return null;
|
|
70
|
+
const trimmed = value.trim();
|
|
71
|
+
if (trimmed.startsWith("[")) {
|
|
72
|
+
try {
|
|
73
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
74
|
+
if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string" || item.trim() === "")) return null;
|
|
75
|
+
return parsed as string[];
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const parts = trimmed.split(",").map((part) => part.trim()).filter(Boolean);
|
|
81
|
+
return parts.length > 0 ? parts : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function publicJwks(value: string | undefined): JSONWebKeySet | null {
|
|
85
|
+
if (!value) return null;
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(value) as unknown;
|
|
88
|
+
if (!parsed || typeof parsed !== "object" || !Array.isArray((parsed as { keys?: unknown }).keys)) return null;
|
|
89
|
+
const keys = (parsed as { keys: unknown[] }).keys;
|
|
90
|
+
if (keys.length < 1) return null;
|
|
91
|
+
for (const key of keys) {
|
|
92
|
+
if (!key || typeof key !== "object") return null;
|
|
93
|
+
const record = key as Record<string, unknown>;
|
|
94
|
+
if (typeof record.d === "string" || record.kty === "oct" || typeof record.k === "string") return null;
|
|
95
|
+
}
|
|
96
|
+
return parsed as JSONWebKeySet;
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function readCart(value: unknown): FinalCartBinding | null {
|
|
103
|
+
if (!value || typeof value !== "object") return null;
|
|
104
|
+
const record = value as Record<string, unknown>;
|
|
105
|
+
if (typeof record.tenantId !== "string" || typeof record.currency !== "string" || typeof record.destinationDigest !== "string") {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
if (typeof record.stage !== "string" || !STAGES.has(record.stage)) return null;
|
|
109
|
+
if (typeof record.verificationExpiresAt !== "string" || typeof record.taxExpiresAt !== "string") return null;
|
|
110
|
+
if (!Array.isArray(record.lines) || record.lines.length > 50) return null;
|
|
111
|
+
const lines = [];
|
|
112
|
+
for (const line of record.lines) {
|
|
113
|
+
if (!line || typeof line !== "object") return null;
|
|
114
|
+
const row = line as Record<string, unknown>;
|
|
115
|
+
if (typeof row.sku !== "string" || typeof row.quantity !== "number" || typeof row.unitMinor !== "number") return null;
|
|
116
|
+
lines.push({ sku: row.sku, quantity: row.quantity, unitMinor: row.unitMinor });
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
tenantId: record.tenantId,
|
|
120
|
+
stage: record.stage as CommerceStage,
|
|
121
|
+
currency: record.currency,
|
|
122
|
+
lines,
|
|
123
|
+
destinationDigest: record.destinationDigest,
|
|
124
|
+
verificationExpiresAt: record.verificationExpiresAt,
|
|
125
|
+
taxExpiresAt: record.taxExpiresAt,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function readEvidence(value: unknown): CommerceEvidence | null {
|
|
130
|
+
if (!value || typeof value !== "object") return null;
|
|
131
|
+
const record = value as Record<string, unknown>;
|
|
132
|
+
const evidence = {} as CommerceEvidence;
|
|
133
|
+
for (const field of ["verification", "tax", "risk", "stock"] as const) {
|
|
134
|
+
const state = record[field];
|
|
135
|
+
if (typeof state !== "string" || !EVIDENCE_STATES.has(state)) return null;
|
|
136
|
+
evidence[field] = state as EvidenceState;
|
|
137
|
+
}
|
|
138
|
+
return evidence;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function readState<T extends string>(value: unknown, allowed: readonly T[]): T | undefined {
|
|
142
|
+
return typeof value === "string" && (allowed as readonly string[]).includes(value) ? (value as T) : undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function destinationOf(value: unknown): { region: string; country: string } | null {
|
|
146
|
+
if (!value || typeof value !== "object") return null;
|
|
147
|
+
const record = value as Record<string, unknown>;
|
|
148
|
+
const region = typeof record.region === "string" ? record.region.trim() : "";
|
|
149
|
+
const country = typeof record.country === "string" ? record.country.trim() : "";
|
|
150
|
+
if (!/^[A-Za-z]{2}$/.test(region)) return null;
|
|
151
|
+
if (country !== "" && !/^[A-Za-z]{2}$/.test(country)) return null;
|
|
152
|
+
return { region: region.toUpperCase(), country: country === "" ? "US" : country.toUpperCase() };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function catalogOffers(sku: string): Promise<boolean> {
|
|
156
|
+
const database = await readD1("CATALOG_DB");
|
|
157
|
+
if (!database) return false;
|
|
158
|
+
try {
|
|
159
|
+
const products = await listAllActiveProducts(fromD1(database));
|
|
160
|
+
for (const product of products) {
|
|
161
|
+
for (const variant of product.variants) {
|
|
162
|
+
if (variant.sku === sku && variant.inventory.available === true) return true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function verificationSeam(
|
|
172
|
+
context: APIContext,
|
|
173
|
+
cart: FinalCartBinding,
|
|
174
|
+
destination: { region: string; country: string } | null,
|
|
175
|
+
): Promise<SeamView> {
|
|
176
|
+
const tot = (context.locals as { tot?: { getProductByHandle: (handle: string) => Promise<unknown> } }).tot ?? {
|
|
177
|
+
getProductByHandle: async () => null,
|
|
178
|
+
};
|
|
179
|
+
const response = await compliancePreflight({
|
|
180
|
+
request: new Request("https://synthetic-tenant.invalid/api/compliance/preflight", {
|
|
181
|
+
method: "POST",
|
|
182
|
+
headers: { "content-type": "application/json" },
|
|
183
|
+
body: JSON.stringify({
|
|
184
|
+
lines: cart.lines.map((line) => ({ handle: line.sku, sku: line.sku, quantity: line.quantity })),
|
|
185
|
+
...(destination ? { destination: { region: destination.region, country: destination.country } } : {}),
|
|
186
|
+
}),
|
|
187
|
+
}),
|
|
188
|
+
locals: { tenant: context.locals.tenant, tot },
|
|
189
|
+
} as APIContext);
|
|
190
|
+
let body: unknown;
|
|
191
|
+
try {
|
|
192
|
+
body = await response.json();
|
|
193
|
+
} catch {
|
|
194
|
+
return { status: "absent" };
|
|
195
|
+
}
|
|
196
|
+
const decision = body && typeof body === "object" ? (body as { decision?: { status?: unknown; failedClosed?: unknown; blocks?: unknown } }).decision : undefined;
|
|
197
|
+
if (!decision || decision.status === "clear") return { status: decision ? "clear" : "absent" };
|
|
198
|
+
const blocks = Array.isArray(decision.blocks) ? decision.blocks : [];
|
|
199
|
+
const codes = blocks
|
|
200
|
+
.map((block) => (block && typeof block === "object" ? (block as { code?: unknown }).code : undefined))
|
|
201
|
+
.filter((code): code is string => typeof code === "string");
|
|
202
|
+
return { status: "blocked", failedClosed: decision.failedClosed === true, codes };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
206
|
+
const tenant = context.locals.tenant as TenantConfig | undefined;
|
|
207
|
+
const appDomain = tenant?.appDomain ?? "";
|
|
208
|
+
if (!tenant || appDomain === "") return deny("tenant-unbound", 401);
|
|
209
|
+
|
|
210
|
+
let body: Record<string, unknown>;
|
|
211
|
+
try {
|
|
212
|
+
const parsed = await context.request.json();
|
|
213
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return deny("malformed-request", 409);
|
|
214
|
+
body = parsed as Record<string, unknown>;
|
|
215
|
+
} catch {
|
|
216
|
+
return deny("malformed-request", 409);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (body.scheme === "shared-secret") {
|
|
220
|
+
const decision = await evaluateFinalCart({
|
|
221
|
+
now: new Date().toISOString(),
|
|
222
|
+
routedTenantId: appDomain,
|
|
223
|
+
quoted: readCart(body.quoted) ?? emptyCart(appDomain),
|
|
224
|
+
presented: readCart(body.presented) ?? emptyCart(appDomain),
|
|
225
|
+
evidence: readEvidence(body.evidence),
|
|
226
|
+
seam: { status: "clear" },
|
|
227
|
+
catalogAvailable: false,
|
|
228
|
+
browserConfirmed: body.browserConfirmed === true,
|
|
229
|
+
token: { scheme: "shared-secret" },
|
|
230
|
+
trustedIssuers: [],
|
|
231
|
+
expectedAudience: "",
|
|
232
|
+
activeKids: [],
|
|
233
|
+
authorities: [],
|
|
234
|
+
});
|
|
235
|
+
return deny(decision.ok ? "shared-secret" : decision.finding, statusFor(decision.ok ? "shared-secret" : decision.finding), decision.ok ? undefined : decision.owner);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const quoted = readCart(body.quoted);
|
|
239
|
+
const presented = readCart(body.presented);
|
|
240
|
+
if (!quoted || !presented) return deny("malformed-request", 409);
|
|
241
|
+
|
|
242
|
+
const jwks = publicJwks(await readEnv("SCRAPPY_CALLER_JWKS"));
|
|
243
|
+
const issuers = parseList(await readEnv("SCRAPPY_CALLER_ISSUERS"));
|
|
244
|
+
const audience = await readEnv("SCRAPPY_CALLER_AUDIENCE");
|
|
245
|
+
const activeKids = parseList(await readEnv("SCRAPPY_CALLER_ACTIVE_KIDS"));
|
|
246
|
+
if (!jwks || !issuers || !audience || !activeKids) return deny("caller-unconfigured", 503);
|
|
247
|
+
|
|
248
|
+
const header = context.request.headers.get("authorization") ?? "";
|
|
249
|
+
const bearer = header.startsWith("Bearer ") ? header.slice("Bearer ".length).trim() : "";
|
|
250
|
+
let token: CallerToken | null = null;
|
|
251
|
+
if (bearer !== "") {
|
|
252
|
+
try {
|
|
253
|
+
const decoded = decodeJwt(bearer);
|
|
254
|
+
const protectedHeader = decodeProtectedHeader(bearer);
|
|
255
|
+
let signatureValid = false;
|
|
256
|
+
try {
|
|
257
|
+
await jwtVerify(bearer, createLocalJWKSet(jwks), { algorithms: ["EdDSA"] });
|
|
258
|
+
signatureValid = true;
|
|
259
|
+
} catch (error) {
|
|
260
|
+
const code = error && typeof error === "object" && "code" in error ? String((error as { code: unknown }).code) : "";
|
|
261
|
+
signatureValid = code === "ERR_JWT_EXPIRED" || code === "ERR_JWT_CLAIM_VALIDATION_FAILED";
|
|
262
|
+
}
|
|
263
|
+
const exp = typeof decoded.exp === "number" ? new Date(decoded.exp * 1000).toISOString() : "";
|
|
264
|
+
const audienceClaim = typeof decoded.aud === "string" ? decoded.aud : Array.isArray(decoded.aud) ? decoded.aud[0] ?? "" : "";
|
|
265
|
+
const stageClaim = claimString(decoded, "stage");
|
|
266
|
+
const stage: CommerceStage =
|
|
267
|
+
stageClaim === "preview" || stageClaim === "publication"
|
|
268
|
+
? stageClaim
|
|
269
|
+
: presented.stage === "preview"
|
|
270
|
+
? "publication"
|
|
271
|
+
: "preview";
|
|
272
|
+
token = {
|
|
273
|
+
scheme: "asymmetric-jwks",
|
|
274
|
+
signatureValid,
|
|
275
|
+
kid: typeof protectedHeader.kid === "string" ? protectedHeader.kid : "",
|
|
276
|
+
issuer: typeof decoded.iss === "string" ? decoded.iss : "",
|
|
277
|
+
audience: audienceClaim,
|
|
278
|
+
expiresAt: exp,
|
|
279
|
+
jti: claimString(decoded, "jti"),
|
|
280
|
+
tenantId: claimString(decoded, "tenantId"),
|
|
281
|
+
stage,
|
|
282
|
+
};
|
|
283
|
+
} catch {
|
|
284
|
+
token = null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const provider = tenant.inventory?.provider ?? "mirror";
|
|
289
|
+
const authorities: unknown[] = [];
|
|
290
|
+
const now = new Date().toISOString();
|
|
291
|
+
if (provider !== "mirror") {
|
|
292
|
+
authorities.push({ provider, sku: presented.lines[0]?.sku });
|
|
293
|
+
} else {
|
|
294
|
+
const skus = [...new Set(presented.lines.map((line) => line.sku))];
|
|
295
|
+
for (const sku of skus) {
|
|
296
|
+
const onHand = mirrorOnHand(sku, "unspecified");
|
|
297
|
+
authorities.push(
|
|
298
|
+
onHand == null
|
|
299
|
+
? null
|
|
300
|
+
: { provider: "mirror", readable: true, locationId: "unspecified", sku, onHand, asOf: now },
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
let catalogAvailable = false;
|
|
305
|
+
for (const line of presented.lines) {
|
|
306
|
+
if (await catalogOffers(line.sku)) catalogAvailable = true;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const decision = await evaluateFinalCart({
|
|
310
|
+
now,
|
|
311
|
+
routedTenantId: appDomain,
|
|
312
|
+
quoted,
|
|
313
|
+
presented,
|
|
314
|
+
evidence: readEvidence(body.evidence),
|
|
315
|
+
seam: await verificationSeam(context, presented, destinationOf(body.destination)),
|
|
316
|
+
catalogAvailable,
|
|
317
|
+
browserConfirmed: body.browserConfirmed === true,
|
|
318
|
+
token,
|
|
319
|
+
trustedIssuers: issuers,
|
|
320
|
+
expectedAudience: audience,
|
|
321
|
+
activeKids,
|
|
322
|
+
authorities,
|
|
323
|
+
payment: readState<PaymentState>(body.payment, ["UNKNOWN", "pending", "authorized", "captured", "capture_unknown", "voided", "refund_requested", "refunded", "refund_unknown"]),
|
|
324
|
+
stock: readState<StockState>(body.stock, ["UNKNOWN", "unallocated", "reserved", "confirmed", "released", "expired"]),
|
|
325
|
+
fulfillment: readState<FulfillmentState>(body.fulfillment, ["UNKNOWN", "held", "ready", "handed_off", "cancelled"]),
|
|
326
|
+
label: readState<LabelState>(body.label, ["UNKNOWN", "none", "intent", "purchased", "printed", "void_requested", "voided"]),
|
|
327
|
+
});
|
|
328
|
+
if (!decision.ok) return deny(decision.finding, statusFor(decision.finding), decision.owner);
|
|
329
|
+
|
|
330
|
+
const database = await readD1("STOREFRONT_APPS_DB");
|
|
331
|
+
if (!database || typeof database.prepare !== "function") return deny("store-unconfigured", 503);
|
|
332
|
+
const reserved = await reserveAllocation(d1CommerceRunner(database), {
|
|
333
|
+
tenantId: decision.tenantId,
|
|
334
|
+
stage: decision.stage,
|
|
335
|
+
orderId: `a${crypto.randomUUID()}`,
|
|
336
|
+
cartDigest: decision.digest,
|
|
337
|
+
expiresAt: decision.expiresAt,
|
|
338
|
+
now,
|
|
339
|
+
currency: presented.currency,
|
|
340
|
+
amountMinor: decision.amountMinor,
|
|
341
|
+
issuer: decision.issuer,
|
|
342
|
+
jti: decision.jti,
|
|
343
|
+
holds: decision.holds,
|
|
344
|
+
});
|
|
345
|
+
if (!reserved.ok) return deny(reserved.finding, statusFor(reserved.finding));
|
|
346
|
+
return json({
|
|
347
|
+
ok: true,
|
|
348
|
+
authorizationId: reserved.authorizationId,
|
|
349
|
+
cartDigest: decision.digest,
|
|
350
|
+
expiresAt: reserved.expiresAt,
|
|
351
|
+
repeated: reserved.repeated,
|
|
352
|
+
stage: decision.stage,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function emptyCart(tenantId: string): FinalCartBinding {
|
|
357
|
+
return {
|
|
358
|
+
tenantId,
|
|
359
|
+
stage: "preview",
|
|
360
|
+
currency: "USD",
|
|
361
|
+
lines: [{ sku: "sku-1", quantity: 1, unitMinor: 0 }],
|
|
362
|
+
destinationDigest: "dest-synthetic",
|
|
363
|
+
verificationExpiresAt: "1970-01-01T00:00:00.000Z",
|
|
364
|
+
taxExpiresAt: "1970-01-01T00:00:00.000Z",
|
|
365
|
+
};
|
|
366
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/storefront-runner",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.52",
|
|
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",
|