@tokenoftrust/storefront-runner 1.3.4-rc.3 → 1.4.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 +15 -0
- package/apps/storefront/dev-plugins/dev-publish.mjs +55 -0
- package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +86 -7
- package/apps/storefront/drizzle.config.apps.ts +13 -0
- package/apps/storefront/env.d.ts +10 -0
- package/apps/storefront/migrations/README.md +13 -7
- package/apps/storefront/migrations-apps/0000_fast_millenium_guard.sql +129 -0
- package/apps/storefront/migrations-apps/meta/0000_snapshot.json +843 -0
- package/apps/storefront/migrations-apps/meta/_journal.json +13 -0
- package/apps/storefront/package.json +13 -1
- package/apps/storefront/perf/README.md +64 -0
- package/apps/storefront/perf/assert-budgets.ts +159 -0
- package/apps/storefront/playwright.config.ts +23 -0
- package/apps/storefront/public/js/dashboard-apps.js +173 -0
- package/apps/storefront/public/shared/commerce-marketing.css +221 -0
- package/apps/storefront/src/components/CollectionCard.astro +1 -0
- package/apps/storefront/src/components/ProductCard.astro +1 -0
- package/apps/storefront/src/components/admin/AdminPublishTab.astro +1959 -0
- package/apps/storefront/src/components/apps/AppWidgetFrame.astro +30 -0
- package/apps/storefront/src/components/chrome/NavDropdown.astro +6 -3
- package/apps/storefront/src/components/chrome/SiteFooter.astro +10 -0
- package/apps/storefront/src/components/chrome/SiteHeader.astro +10 -0
- package/apps/storefront/src/components/commerce/RatingStars.astro +3 -2
- package/apps/storefront/src/components/content/Callout.astro +75 -0
- package/apps/storefront/src/components/content/NeedsReviewCallout.astro +66 -0
- package/apps/storefront/src/components/content/ProseSections.astro +121 -0
- package/apps/storefront/src/components/content/ProseToc.astro +34 -0
- package/apps/storefront/src/components/content/RichText.astro +44 -0
- package/apps/storefront/src/components/content/TrustStrip.astro +46 -0
- package/apps/storefront/src/components/home/Hero.astro +14 -0
- package/apps/storefront/src/components/islands/CheckoutComplianceGate.tsx +295 -0
- package/apps/storefront/src/components/islands/ImageGallery.tsx +42 -21
- package/apps/storefront/src/components/islands/VariantSelector.tsx +62 -12
- package/apps/storefront/src/components/plp/FacetSidebar.astro +2 -2
- package/apps/storefront/src/components/subscription/ManageSubscriptionEntry.astro +1 -0
- package/apps/storefront/src/config/compliance/rulesets.ts +79 -0
- package/apps/storefront/src/config/storeName.ts +34 -0
- package/apps/storefront/src/layouts/Layout.astro +97 -23
- package/apps/storefront/src/lib/analytics/budgets.json +69 -0
- package/apps/storefront/src/lib/analytics/lighthouseReport.ts +109 -0
- package/apps/storefront/src/lib/analytics/perfBudgets.ts +452 -0
- package/apps/storefront/src/lib/analytics/rumAlert.ts +179 -0
- package/apps/storefront/src/lib/analytics/webVitals.ts +269 -0
- package/apps/storefront/src/lib/apps/adminService.ts +106 -0
- package/apps/storefront/src/lib/apps/adminSession.ts +205 -0
- package/apps/storefront/src/lib/apps/apiAuth.ts +91 -0
- package/apps/storefront/src/lib/apps/apiRoute.ts +35 -0
- package/apps/storefront/src/lib/apps/catalogMapper.ts +39 -0
- package/apps/storefront/src/lib/apps/credentials.ts +153 -0
- package/apps/storefront/src/lib/apps/gatewayKeys.ts +156 -0
- package/apps/storefront/src/lib/apps/healthAggregate.ts +66 -0
- package/apps/storefront/src/lib/apps/orders/attributionService.ts +206 -0
- package/apps/storefront/src/lib/apps/orders/customerHash.ts +28 -0
- package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +203 -0
- package/apps/storefront/src/lib/apps/orders/idempotency.ts +100 -0
- package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +117 -0
- package/apps/storefront/src/lib/apps/orders/orderMapper.ts +91 -0
- package/apps/storefront/src/lib/apps/orders/ordersStore.ts +181 -0
- package/apps/storefront/src/lib/apps/registryService.ts +579 -0
- package/apps/storefront/src/lib/apps/scopes.ts +79 -0
- package/apps/storefront/src/lib/apps/tokenIssuer.ts +121 -0
- package/apps/storefront/src/lib/apps/tokenVerifier.ts +148 -0
- package/apps/storefront/src/lib/apps/widgets/eligibility.ts +18 -0
- package/apps/storefront/src/lib/apps/widgets/frameProps.ts +52 -0
- package/apps/storefront/src/lib/apps/widgets/launchToken.ts +84 -0
- package/apps/storefront/src/lib/apps/widgets/placements.ts +57 -0
- package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +111 -0
- package/apps/storefront/src/lib/auth/adminEntry.ts +119 -0
- package/apps/storefront/src/lib/auth/identityToken.ts +21 -2
- package/apps/storefront/src/lib/auth/loginGate.ts +102 -18
- package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +215 -0
- package/apps/storefront/src/lib/auth/route.ts +16 -1
- package/apps/storefront/src/lib/auth/session.ts +8 -0
- package/apps/storefront/src/lib/auth/stepUpChallenge.ts +107 -0
- package/apps/storefront/src/lib/auth/totAccessClient.ts +208 -0
- package/apps/storefront/src/lib/blog/provider.ts +39 -0
- package/apps/storefront/src/lib/blog/types.ts +26 -0
- package/apps/storefront/src/lib/checkoutCommerce.ts +39 -1
- package/apps/storefront/src/lib/chrome/model.ts +11 -0
- package/apps/storefront/src/lib/compliance/enforcement.ts +95 -0
- package/apps/storefront/src/lib/content/callout.ts +78 -0
- package/apps/storefront/src/lib/content/index.ts +27 -0
- package/apps/storefront/src/lib/content/needsReview.ts +58 -0
- package/apps/storefront/src/lib/content/prose.ts +186 -0
- package/apps/storefront/src/lib/content/richtext.ts +76 -0
- package/apps/storefront/src/lib/content/trustStrip.ts +74 -0
- package/apps/storefront/src/lib/content-edit/client.ts +70 -14
- package/apps/storefront/src/lib/d1/catalog.ts +12 -0
- package/apps/storefront/src/lib/d1/schema-apps.ts +213 -0
- package/apps/storefront/src/lib/dev/apiBase.ts +8 -2
- package/apps/storefront/src/lib/dev/cliSignInCode.ts +65 -115
- package/apps/storefront/src/lib/dev/cockpitStore.ts +65 -0
- package/apps/storefront/src/lib/dev/obstacleStore.ts +214 -0
- package/apps/storefront/src/lib/dev/previewStatus.ts +112 -0
- package/apps/storefront/src/lib/dev/rendezvousBroker.ts +241 -0
- package/apps/storefront/src/lib/dev/subjectReissue.ts +67 -0
- package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +10 -10
- package/apps/storefront/src/lib/env.ts +12 -0
- package/apps/storefront/src/lib/jsonld.ts +12 -17
- package/apps/storefront/src/lib/membership/eligibility.ts +37 -0
- package/apps/storefront/src/lib/monitoring/manifest.ts +302 -0
- package/apps/storefront/src/lib/privacy/emailHint.ts +13 -5
- package/apps/storefront/src/lib/publish/apex-readiness.ts +337 -0
- package/apps/storefront/src/lib/publish/dispatchHealth.ts +269 -0
- package/apps/storefront/src/lib/publish/domainState.ts +351 -0
- package/apps/storefront/src/lib/publish/shipWorkspace.ts +362 -0
- package/apps/storefront/src/lib/rawChrome.ts +34 -3
- package/apps/storefront/src/lib/storyblok/content-model.ts +34 -2
- package/apps/storefront/src/lib/storyblok/provider.ts +11 -4
- package/apps/storefront/src/lib/subscription/model.ts +114 -0
- package/apps/storefront/src/lib/tot/ToTClient.ts +3 -3
- package/apps/storefront/src/lib/tot/query.ts +32 -0
- package/apps/storefront/src/lib/webhooks/cloudflareQueueDispatcher.ts +82 -0
- package/apps/storefront/src/lib/webhooks/deliveryEngine.ts +245 -0
- package/apps/storefront/src/lib/webhooks/deliveryMapper.ts +34 -0
- package/apps/storefront/src/lib/webhooks/deliveryStore.ts +668 -0
- package/apps/storefront/src/lib/webhooks/dispatcher.ts +168 -0
- package/apps/storefront/src/lib/webhooks/emit.ts +167 -0
- package/apps/storefront/src/lib/webhooks/endpointGuard.ts +135 -0
- package/apps/storefront/src/lib/webhooks/events.ts +98 -0
- package/apps/storefront/src/lib/webhooks/getDispatcher.ts +49 -0
- package/apps/storefront/src/lib/webhooks/signing.ts +29 -0
- package/apps/storefront/src/lib/webhooks/webhookSigningKey.ts +146 -0
- package/apps/storefront/src/middleware/index.ts +28 -13
- package/apps/storefront/src/pages/404.astro +21 -9
- package/apps/storefront/src/pages/[...slug].astro +36 -2
- package/apps/storefront/src/pages/admin.astro +29 -3
- package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +60 -0
- package/apps/storefront/src/pages/api/apps/admin/health.ts +44 -0
- package/apps/storefront/src/pages/api/apps/admin/install.ts +100 -0
- package/apps/storefront/src/pages/api/apps/admin/list.ts +26 -0
- package/apps/storefront/src/pages/api/apps/admin/resume.ts +56 -0
- package/apps/storefront/src/pages/api/apps/admin/suspend.ts +57 -0
- package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +74 -0
- package/apps/storefront/src/pages/api/apps/admin/update.ts +103 -0
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +59 -0
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries.ts +51 -0
- package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +172 -0
- package/apps/storefront/src/pages/api/apps/oauth/token.ts +87 -0
- package/apps/storefront/src/pages/api/apps/v1/attribution.ts +142 -0
- package/apps/storefront/src/pages/api/apps/v1/catalog/products/[handle].ts +39 -0
- package/apps/storefront/src/pages/api/apps/v1/catalog/products.ts +49 -0
- package/apps/storefront/src/pages/api/apps/v1/health.ts +32 -0
- package/apps/storefront/src/pages/api/apps/v1/inventory.ts +59 -0
- package/apps/storefront/src/pages/api/apps/v1/orders/[id].ts +48 -0
- package/apps/storefront/src/pages/api/apps/v1/orders.ts +73 -0
- package/apps/storefront/src/pages/api/apps/v1/reports.ts +21 -0
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +62 -0
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries.ts +47 -0
- package/apps/storefront/src/pages/api/auth/magic-exchange.ts +48 -1
- package/apps/storefront/src/pages/api/auth/step-up-send.ts +57 -0
- package/apps/storefront/src/pages/api/auth/step-up-verify.ts +129 -0
- package/apps/storefront/src/pages/api/auth/verify.ts +23 -0
- package/apps/storefront/src/pages/api/compliance/preflight.ts +206 -0
- package/apps/storefront/src/pages/api/rum/vitals.ts +54 -0
- package/apps/storefront/src/pages/api/test/dev-session.ts +133 -0
- package/apps/storefront/src/pages/auth/login.astro +197 -41
- package/apps/storefront/src/pages/auth/magic.astro +75 -43
- package/apps/storefront/src/pages/blog/[slug].astro +107 -0
- package/apps/storefront/src/pages/blog/index.astro +98 -0
- package/apps/storefront/src/pages/capabilities.astro +8 -0
- package/apps/storefront/src/pages/cockpit.astro +496 -74
- package/apps/storefront/src/pages/collections/[handle].astro +8 -0
- package/apps/storefront/src/pages/collections/index.astro +10 -2
- package/apps/storefront/src/pages/dashboard/[appDomain]/apps/index.astro +119 -0
- package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +5 -0
- package/apps/storefront/src/pages/index.astro +57 -0
- package/apps/storefront/src/pages/llms.txt.ts +31 -10
- package/apps/storefront/src/pages/products/[handle].astro +100 -9
- package/apps/storefront/src/pages/sitemap.xml.ts +21 -4
- package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +198 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +7 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +7 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +7 -0
- package/apps/storefront/src/pages/style-guide/index.astro +10 -0
- package/apps/storefront/src/styles/fonts.css +54 -0
- package/apps/storefront/src/styles/global.css +22 -2
- package/apps/storefront/src/themes/schema.ts +3 -25
- package/apps/storefront/tsconfig.json +1 -1
- package/apps/storefront/vitest.config.ts +4 -1
- package/package.json +1 -1
- package/packages/public-runtime/src/candidate-index.ts +311 -0
- package/packages/public-runtime/src/checkout.ts +94 -2
- package/packages/public-runtime/src/compliance/evaluate.ts +265 -0
- package/packages/public-runtime/src/compliance/evidence-signals.ts +81 -0
- package/packages/public-runtime/src/compliance/index.ts +22 -0
- package/packages/public-runtime/src/compliance/pact-report.ts +94 -0
- package/packages/public-runtime/src/compliance/profile.ts +198 -0
- package/packages/public-runtime/src/compliance/ruleset.ts +117 -0
- package/packages/public-runtime/src/compliance/verification.ts +81 -0
- package/packages/public-runtime/src/csp.ts +27 -3
- package/packages/public-runtime/src/customization-reconcile.ts +35 -0
- package/packages/public-runtime/src/customization-runtime.ts +8 -0
- package/packages/public-runtime/src/customization-versioning.ts +17 -0
- package/packages/public-runtime/src/extension-contract.ts +2 -1
- package/packages/public-runtime/src/hash.ts +25 -0
- package/packages/public-runtime/src/index.ts +6 -0
- package/packages/public-runtime/src/membership.ts +353 -0
- package/packages/public-runtime/src/product.ts +24 -2
- package/packages/public-runtime/src/review-trust-proof.ts +194 -0
- package/packages/public-runtime/src/tenant-assets.ts +40 -5
- package/packages/public-runtime/src/tenant.ts +236 -0
- package/packages/public-runtime/src/widget-postmessage.ts +205 -0
- package/scripts/dev/publish.mjs +158 -0
- package/scripts/dev/transient-files.mjs +2 -1
- package/tenants/home/public/fonts/inter-latin-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-700-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-800-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-700-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-800-normal.woff2 +0 -0
- package/tenants/home/public/pages/storefront.css +23 -0
- package/apps/storefront/src/lib/dev/hostedCockpit.ts +0 -169
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GET /api/apps/v1/orders` — the D6 minimal order list (PrivateApps epic;
|
|
3
|
+
* `docs/private-apps/contract/openapi.yaml`'s `listOrders`). Requires scope
|
|
4
|
+
* `orders:read:minimal`.
|
|
5
|
+
*
|
|
6
|
+
* Reads live from tot-foxycart via `FoxyOrderClient` (never a raw fetch) and
|
|
7
|
+
* maps every row through `orderMapper.ts`'s `toContractOrder` — nothing
|
|
8
|
+
* beyond the contract's minimized `Order` shape (customer as opaque hash, no
|
|
9
|
+
* checkout/tax/signed-cart internals) ever leaves this route. Fail-closed:
|
|
10
|
+
* an unconfigured Foxy client is a 503, any upstream read failure is a clean
|
|
11
|
+
* 502 — neither leaks Foxy's own error detail.
|
|
12
|
+
*/
|
|
13
|
+
import type { APIContext } from "astro";
|
|
14
|
+
import { authorizeAppRequest } from "@/lib/apps/apiAuth.js";
|
|
15
|
+
import { json, withRequestId } from "@/lib/apps/apiRoute.js";
|
|
16
|
+
import { toContractOrder, type ContractOrderStatus } from "@/lib/apps/orders/orderMapper.js";
|
|
17
|
+
import { getFoxyOrderClient } from "@/lib/apps/orders/foxyOrderClient.js";
|
|
18
|
+
|
|
19
|
+
export const prerender = false;
|
|
20
|
+
|
|
21
|
+
const DEFAULT_PAGE_SIZE = 24;
|
|
22
|
+
const MAX_PAGE_SIZE = 100;
|
|
23
|
+
const ORDER_STATUSES: readonly ContractOrderStatus[] = ["created", "fulfilled", "cancelled"];
|
|
24
|
+
|
|
25
|
+
function parsePage(raw: string | null, fallback: number): number {
|
|
26
|
+
const n = raw != null ? Number(raw) : NaN;
|
|
27
|
+
return Number.isFinite(n) && n >= 1 ? Math.floor(n) : fallback;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseStatus(raw: string | null): ContractOrderStatus | undefined {
|
|
31
|
+
return raw != null && (ORDER_STATUSES as readonly string[]).includes(raw) ? (raw as ContractOrderStatus) : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function GET(context: APIContext): Promise<Response> {
|
|
35
|
+
const auth = await authorizeAppRequest(context, "orders:read:minimal");
|
|
36
|
+
if (!auth.ok) return withRequestId(auth.response, context);
|
|
37
|
+
|
|
38
|
+
const url = new URL(context.request.url);
|
|
39
|
+
const page = parsePage(url.searchParams.get("page"), 1);
|
|
40
|
+
const pageSize = Math.min(
|
|
41
|
+
MAX_PAGE_SIZE,
|
|
42
|
+
Math.max(1, parsePage(url.searchParams.get("pageSize"), DEFAULT_PAGE_SIZE)),
|
|
43
|
+
);
|
|
44
|
+
const statusFilter = parseStatus(url.searchParams.get("status"));
|
|
45
|
+
|
|
46
|
+
let client;
|
|
47
|
+
try {
|
|
48
|
+
client = await getFoxyOrderClient();
|
|
49
|
+
} catch {
|
|
50
|
+
return withRequestId(json({ error: "orders not configured", code: "unconfigured" }, 503), context);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let foxyPage;
|
|
54
|
+
try {
|
|
55
|
+
foxyPage = await client.listOrders(auth.install.tenantId, {
|
|
56
|
+
limit: pageSize,
|
|
57
|
+
offset: (page - 1) * pageSize,
|
|
58
|
+
});
|
|
59
|
+
} catch {
|
|
60
|
+
return withRequestId(json({ error: "failed to read orders", code: "upstream_error" }, 502), context);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const mapCtx = { tenantId: auth.install.tenantId, installId: auth.install.installId };
|
|
64
|
+
let orders = await Promise.all(foxyPage.orders.map((order) => toContractOrder(order, mapCtx)));
|
|
65
|
+
// tot-foxycart's own `status` query param speaks Foxy's status vocabulary, not
|
|
66
|
+
// the contract's created|fulfilled|cancelled enum — there's no confirmed
|
|
67
|
+
// mapping to push this filter upstream, so it's applied post-map here. This
|
|
68
|
+
// can leave a page short of `pageSize`, and `total` still reflects Foxy's
|
|
69
|
+
// UNFILTERED count — a known gap, flagged rather than built around.
|
|
70
|
+
if (statusFilter) orders = orders.filter((order) => order.status === statusFilter);
|
|
71
|
+
|
|
72
|
+
return withRequestId(json({ orders, page, pageSize, total: foxyPage.total }), context);
|
|
73
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GET /api/apps/v1/reports` — the D3 report-snapshot list (PrivateApps
|
|
3
|
+
* epic; `docs/private-apps/contract/openapi.yaml`'s `listReports`). Requires
|
|
4
|
+
* scope `reports:read`.
|
|
5
|
+
*
|
|
6
|
+
* Real auth + scope enforcement, empty data: this milestone ships the
|
|
7
|
+
* endpoint's contract shape (a valid `ReportList`) with no report snapshots
|
|
8
|
+
* behind it yet — there is no report-generation pipeline to read from.
|
|
9
|
+
*/
|
|
10
|
+
import type { APIContext } from "astro";
|
|
11
|
+
import { authorizeAppRequest } from "@/lib/apps/apiAuth.js";
|
|
12
|
+
import { json, withRequestId } from "@/lib/apps/apiRoute.js";
|
|
13
|
+
|
|
14
|
+
export const prerender = false;
|
|
15
|
+
|
|
16
|
+
export async function GET(context: APIContext): Promise<Response> {
|
|
17
|
+
const auth = await authorizeAppRequest(context, "reports:read");
|
|
18
|
+
if (!auth.ok) return withRequestId(auth.response, context);
|
|
19
|
+
|
|
20
|
+
return withRequestId(json({ reports: [] }), context);
|
|
21
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `POST /api/apps/v1/webhooks/deliveries/{deliveryId}/replay` — the D4
|
|
3
|
+
* install-owned replay (PrivateApps epic; `docs/private-apps/contract/
|
|
4
|
+
* openapi.yaml`'s `replayWebhookDelivery`). Authorized by install ownership,
|
|
5
|
+
* not a scope (`x-requiredScope: null`, `x-authorization: install-owns-
|
|
6
|
+
* delivery`): an app may replay only a delivery it owns.
|
|
7
|
+
*
|
|
8
|
+
* Preserves the SAME `deliveryId` (the CloudEvents wire id) throughout — a
|
|
9
|
+
* receiver that already acked a `delivered` delivery still dedupes a
|
|
10
|
+
* replayed resend of it — via `deliveryStore.ts`'s `requeueForReplay`, which
|
|
11
|
+
* resets the row (incl. out of the `delivered`/`dead-lettered` terminal
|
|
12
|
+
* states) to `pending` with a fresh attempt budget so the dispatcher's
|
|
13
|
+
* `attemptDelivery` makes a real HTTP attempt instead of hitting its
|
|
14
|
+
* terminal-state no-op branch. See `requeueForReplay`'s own doc comment for
|
|
15
|
+
* the full rationale.
|
|
16
|
+
*
|
|
17
|
+
* No `Idempotency-Key` ledger exists yet (no table backs it), so this only
|
|
18
|
+
* validates the header's presence, per the contract's `required: true` — it
|
|
19
|
+
* does not yet detect/409 a reused key with a mismatched body (there is no
|
|
20
|
+
* request body on this operation to hash in the first place).
|
|
21
|
+
*/
|
|
22
|
+
import type { APIContext } from "astro";
|
|
23
|
+
import { authorizeAppRequest } from "@/lib/apps/apiAuth.js";
|
|
24
|
+
import { json, withRequestId } from "@/lib/apps/apiRoute.js";
|
|
25
|
+
import { WebhookDeliveryStore } from "@/lib/webhooks/deliveryStore.js";
|
|
26
|
+
import { toContractDelivery } from "@/lib/webhooks/deliveryMapper.js";
|
|
27
|
+
import { getDispatcher } from "@/lib/webhooks/getDispatcher.js";
|
|
28
|
+
import { fromD1 } from "@/lib/d1/catalog.js";
|
|
29
|
+
import { readD1 } from "@/lib/env";
|
|
30
|
+
|
|
31
|
+
export const prerender = false;
|
|
32
|
+
|
|
33
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
34
|
+
const auth = await authorizeAppRequest(context, null);
|
|
35
|
+
if (!auth.ok) return withRequestId(auth.response, context);
|
|
36
|
+
|
|
37
|
+
if (!context.request.headers.get("idempotency-key")) {
|
|
38
|
+
return withRequestId(json({ error: "Idempotency-Key header is required" }, 400), context);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const deliveryId = context.params.deliveryId;
|
|
42
|
+
if (!deliveryId) {
|
|
43
|
+
return withRequestId(json({ error: "deliveryId is required", code: "not_found" }, 404), context);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const d1 = await readD1("STOREFRONT_APPS_DB");
|
|
47
|
+
if (!d1) return withRequestId(json({ error: "webhook store not configured", code: "unconfigured" }, 503), context);
|
|
48
|
+
const store = new WebhookDeliveryStore(fromD1(d1));
|
|
49
|
+
|
|
50
|
+
const delivery = await store.getDelivery(deliveryId);
|
|
51
|
+
if (!delivery || delivery.installId !== auth.install.installId) {
|
|
52
|
+
return withRequestId(json({ error: "Delivery not found.", code: "not_found" }, 404), context);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const dispatcher = await getDispatcher();
|
|
56
|
+
if (dispatcher instanceof Response) return withRequestId(dispatcher, context);
|
|
57
|
+
|
|
58
|
+
const requeued = await store.requeueForReplay(deliveryId);
|
|
59
|
+
await dispatcher.enqueue({ deliveryId });
|
|
60
|
+
|
|
61
|
+
return withRequestId(json(toContractDelivery(requeued), 202), context);
|
|
62
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GET /api/apps/v1/webhooks/deliveries` — the D4 install-owned delivery
|
|
3
|
+
* list (PrivateApps epic; `docs/private-apps/contract/openapi.yaml`'s
|
|
4
|
+
* `listWebhookDeliveries`). Authorized by install ownership, not a scope
|
|
5
|
+
* (`x-requiredScope: null`, `x-authorization: install-owns-delivery`): any
|
|
6
|
+
* installed app may list ITS OWN delivery records, never another install's —
|
|
7
|
+
* `listDeliveriesByInstall` is scoped to `auth.install.installId` from the
|
|
8
|
+
* JWT, never a caller-supplied id.
|
|
9
|
+
*
|
|
10
|
+
* Delivery rows never carry a payload body (see `deliveryStore.ts`'s header),
|
|
11
|
+
* so there is nothing sensitive to redact in `toContractDelivery`'s output.
|
|
12
|
+
*/
|
|
13
|
+
import type { APIContext } from "astro";
|
|
14
|
+
import { authorizeAppRequest } from "@/lib/apps/apiAuth.js";
|
|
15
|
+
import { json, withRequestId } from "@/lib/apps/apiRoute.js";
|
|
16
|
+
import { WebhookDeliveryStore, type DeliveryState } from "@/lib/webhooks/deliveryStore.js";
|
|
17
|
+
import { toContractDelivery } from "@/lib/webhooks/deliveryMapper.js";
|
|
18
|
+
import { fromD1 } from "@/lib/d1/catalog.js";
|
|
19
|
+
import { readD1 } from "@/lib/env";
|
|
20
|
+
|
|
21
|
+
export const prerender = false;
|
|
22
|
+
|
|
23
|
+
const DELIVERY_STATES: readonly DeliveryState[] = ["pending", "retrying", "delivered", "dead-lettered"];
|
|
24
|
+
|
|
25
|
+
/** Absent = no filter (`undefined`); an unrecognized value is a 400, not a silent no-op. */
|
|
26
|
+
function parseStateFilter(raw: string | null): DeliveryState | undefined | "invalid" {
|
|
27
|
+
if (raw === null) return undefined;
|
|
28
|
+
return (DELIVERY_STATES as readonly string[]).includes(raw) ? (raw as DeliveryState) : "invalid";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function GET(context: APIContext): Promise<Response> {
|
|
32
|
+
const auth = await authorizeAppRequest(context, null);
|
|
33
|
+
if (!auth.ok) return withRequestId(auth.response, context);
|
|
34
|
+
|
|
35
|
+
const state = parseStateFilter(new URL(context.request.url).searchParams.get("state"));
|
|
36
|
+
if (state === "invalid") {
|
|
37
|
+
return withRequestId(json({ error: "invalid state filter", code: "invalid_state_filter" }, 400), context);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const d1 = await readD1("STOREFRONT_APPS_DB");
|
|
41
|
+
if (!d1) return withRequestId(json({ error: "webhook store not configured", code: "unconfigured" }, 503), context);
|
|
42
|
+
const store = new WebhookDeliveryStore(fromD1(d1));
|
|
43
|
+
|
|
44
|
+
const deliveries = await store.listDeliveriesByInstall(auth.install.installId, { state });
|
|
45
|
+
|
|
46
|
+
return withRequestId(json({ deliveries: deliveries.map(toContractDelivery) }), context);
|
|
47
|
+
}
|
|
@@ -25,18 +25,30 @@ import {
|
|
|
25
25
|
} from "@/lib/auth/identityToken";
|
|
26
26
|
import { createSession, DEFAULT_SESSION_TTL_SECONDS } from "@/lib/auth/session";
|
|
27
27
|
import { normalizeEmailHint } from "@/lib/privacy/emailHint";
|
|
28
|
+
import { acceptDeveloperInvite } from "@/lib/grants/developerInvites";
|
|
28
29
|
|
|
29
30
|
export const prerender = false;
|
|
30
31
|
|
|
31
32
|
/** Back to the landing page carrying an error (no token in the URL on failure). */
|
|
32
33
|
function magicRedirect(
|
|
33
34
|
context: APIContext,
|
|
34
|
-
params: {
|
|
35
|
+
params: {
|
|
36
|
+
error?: string;
|
|
37
|
+
next?: string;
|
|
38
|
+
tenant?: string;
|
|
39
|
+
emailHint?: string;
|
|
40
|
+
/** Step-up state, distinct from `error` — see `@/lib/auth/stepUpChallenge`. */
|
|
41
|
+
reason?: "step-up-required" | "success-pending";
|
|
42
|
+
/** The RFC 9470 `requiredAcr` from the step-up obligation, when known. */
|
|
43
|
+
requiredAcr?: string;
|
|
44
|
+
},
|
|
35
45
|
): Response {
|
|
36
46
|
const q = new URLSearchParams();
|
|
37
47
|
if (params.error) q.set("error", params.error);
|
|
38
48
|
if (params.tenant) q.set("tenant", params.tenant);
|
|
39
49
|
if (params.emailHint) q.set("email_hint", params.emailHint);
|
|
50
|
+
if (params.reason) q.set("reason", params.reason);
|
|
51
|
+
if (params.requiredAcr) q.set("requiredAcr", params.requiredAcr);
|
|
40
52
|
if (params.next && params.next !== "/dashboard") q.set("next", params.next);
|
|
41
53
|
const qs = q.toString();
|
|
42
54
|
return context.redirect(`/auth/magic${qs ? `?${qs}` : ""}`, 303);
|
|
@@ -67,6 +79,20 @@ export async function POST(context: APIContext): Promise<Response> {
|
|
|
67
79
|
// Consume the token (single-use; burned server-side by tot20).
|
|
68
80
|
const result = await client.exchangeSignInToken(token);
|
|
69
81
|
if (!result.ok) {
|
|
82
|
+
// RFC 9470 step-up challenge / success-pending — same distinct-reason
|
|
83
|
+
// handling as /api/auth/verify (see there for the rationale).
|
|
84
|
+
if (result.reason === "step-up-required") {
|
|
85
|
+
return magicRedirect(context, {
|
|
86
|
+
tenant,
|
|
87
|
+
emailHint,
|
|
88
|
+
reason: "step-up-required",
|
|
89
|
+
requiredAcr: result.stepUp.requiredAcr,
|
|
90
|
+
next,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (result.reason === "success-pending") {
|
|
94
|
+
return magicRedirect(context, { tenant, emailHint, reason: "success-pending", next });
|
|
95
|
+
}
|
|
70
96
|
// reason ∈ invalid|expired|error (tot20's "used" normalizes to invalid).
|
|
71
97
|
const error = result.reason === "expired" ? "expired" : "invalid";
|
|
72
98
|
return magicRedirect(context, { error, tenant, emailHint, next });
|
|
@@ -105,10 +131,31 @@ export async function POST(context: APIContext): Promise<Response> {
|
|
|
105
131
|
capability: "member",
|
|
106
132
|
tenants: derived.tenants,
|
|
107
133
|
staff: derived.staff,
|
|
134
|
+
acr: derived.acr,
|
|
135
|
+
authTime: derived.authTime,
|
|
108
136
|
},
|
|
109
137
|
DEFAULT_SESSION_TTL_SECONDS,
|
|
110
138
|
nowSeconds,
|
|
111
139
|
);
|
|
112
140
|
setSessionCookie(context, id, DEFAULT_SESSION_TTL_SECONDS);
|
|
141
|
+
|
|
142
|
+
// Accept-before-sign-in (#8425): the invitee just affirmatively confirmed — the magic
|
|
143
|
+
// link is a developer invite — so advance the tot20 invite roster pending → accepted as
|
|
144
|
+
// they sign in. Best-effort: NEVER blocks the sign-in (a non-invitee magic sign-in or a
|
|
145
|
+
// tot20 hiccup is skipped/swallowed). tot20 scopes the accept to an invite this app issued.
|
|
146
|
+
try {
|
|
147
|
+
const accepted = await acceptDeveloperInvite(tenant, derived.email);
|
|
148
|
+
if (!accepted.accepted && !accepted.skipped) {
|
|
149
|
+
console.warn("auth/magic-exchange: invite accept did not record", {
|
|
150
|
+
tenant,
|
|
151
|
+
reason: accepted.reason,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.warn("auth/magic-exchange: invite accept threw (swallowed)", {
|
|
156
|
+
error: err instanceof Error ? err.message : String(err),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
113
160
|
return context.redirect(next, 303);
|
|
114
161
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/auth/step-up-send { identifier } — RFC 9470 step-up completion, leg 1.
|
|
3
|
+
*
|
|
4
|
+
* Called after `/api/auth/verify` (or `/api/auth/magic-exchange`) surfaces a
|
|
5
|
+
* `reason=step-up-required` (or `success-pending`) state: asks tot20 to send a
|
|
6
|
+
* one-time SMS code to the identifier bound to that challenge. Deliberately
|
|
7
|
+
* NEUTRAL — always 200 `{ ok: true }`, even when the integration is
|
|
8
|
+
* unconfigured or the transport fails, mirroring `sendFederatedCode`'s
|
|
9
|
+
* enumeration-resistance (the caller never learns whether `identifier` is
|
|
10
|
+
* valid/eligible from this response alone).
|
|
11
|
+
*
|
|
12
|
+
* JSON-first (the future step-up card is expected to call this via `fetch`),
|
|
13
|
+
* but also accepts a plain form post so a no-JS fallback stays possible.
|
|
14
|
+
*/
|
|
15
|
+
import type { APIContext } from "astro";
|
|
16
|
+
import { getAccessClient } from "@/lib/auth/route";
|
|
17
|
+
|
|
18
|
+
export const prerender = false;
|
|
19
|
+
|
|
20
|
+
function json(body: unknown, status = 200): Response {
|
|
21
|
+
return new Response(JSON.stringify(body), {
|
|
22
|
+
status,
|
|
23
|
+
headers: { "content-type": "application/json", "cache-control": "no-store" },
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function readIdentifier(request: Request): Promise<string> {
|
|
28
|
+
const ct = request.headers.get("content-type") || "";
|
|
29
|
+
if (ct.includes("application/json")) {
|
|
30
|
+
const b = (await request.json().catch(() => ({}))) as Record<string, unknown>;
|
|
31
|
+
return typeof b.identifier === "string" ? b.identifier.trim() : "";
|
|
32
|
+
}
|
|
33
|
+
const form = await request.formData();
|
|
34
|
+
return String(form.get("identifier") ?? "").trim();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
38
|
+
let identifier = "";
|
|
39
|
+
try {
|
|
40
|
+
identifier = await readIdentifier(context.request);
|
|
41
|
+
} catch {
|
|
42
|
+
// Never leak parse failures either — stay neutral.
|
|
43
|
+
return json({ ok: true });
|
|
44
|
+
}
|
|
45
|
+
if (!identifier) return json({ ok: true });
|
|
46
|
+
|
|
47
|
+
const client = await getAccessClient();
|
|
48
|
+
if (!client) {
|
|
49
|
+
// Integration not configured server-side; still neutral to the caller —
|
|
50
|
+
// this is an ops problem, not something the caller should be able to probe.
|
|
51
|
+
console.warn("auth/step-up-send: access client unconfigured");
|
|
52
|
+
return json({ ok: true });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await client.sendSmsChallenge(identifier);
|
|
56
|
+
return json({ ok: true });
|
|
57
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/auth/step-up-verify { identifier, code, next? } — RFC 9470 step-up
|
|
3
|
+
* completion, leg 2.
|
|
4
|
+
*
|
|
5
|
+
* Verifies the SMS code tot20 bound to `identifier` (sent via
|
|
6
|
+
* `/api/auth/step-up-send`). On success tot20 returns a FRESHER `id_token`
|
|
7
|
+
* (acr `tot:aal:sms`, fresh `auth_time`) that satisfies the step-up obligation
|
|
8
|
+
* a prior `/api/auth/verify` or `/api/auth/magic-exchange` surfaced. We then
|
|
9
|
+
* complete sign-in the EXACT same way those routes do: verify the assertion
|
|
10
|
+
* OFFLINE against tot's JWKS (aud pinned to `STOREFRONT_APP_DOMAIN`), derive the
|
|
11
|
+
* federated session from its claims, mint the session, set the cookie, and
|
|
12
|
+
* redirect to `next` — no bespoke session path (see `@/lib/auth/route` +
|
|
13
|
+
* `@/lib/auth/identityToken` + `@/lib/auth/session`, the same helpers
|
|
14
|
+
* `verify.ts`/`magic-exchange.ts` use).
|
|
15
|
+
*
|
|
16
|
+
* No-JS-friendly (a plain form post), matching the rest of `/api/auth/*`.
|
|
17
|
+
*/
|
|
18
|
+
import type { APIContext } from "astro";
|
|
19
|
+
import {
|
|
20
|
+
getAccessClient,
|
|
21
|
+
getSessionStore,
|
|
22
|
+
setSessionCookie,
|
|
23
|
+
safeNext,
|
|
24
|
+
loginRedirect,
|
|
25
|
+
} from "@/lib/auth/route";
|
|
26
|
+
import { getFederatedVerifier, sessionFromFederatedClaims } from "@/lib/auth/identityToken";
|
|
27
|
+
import { createSession, DEFAULT_SESSION_TTL_SECONDS } from "@/lib/auth/session";
|
|
28
|
+
|
|
29
|
+
export const prerender = false;
|
|
30
|
+
|
|
31
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
32
|
+
let identifier = "";
|
|
33
|
+
let code = "";
|
|
34
|
+
let next = "/";
|
|
35
|
+
try {
|
|
36
|
+
const form = await context.request.formData();
|
|
37
|
+
identifier = String(form.get("identifier") ?? "").trim();
|
|
38
|
+
code = String(form.get("code") ?? "").trim();
|
|
39
|
+
next = safeNext(String(form.get("next") ?? "/"));
|
|
40
|
+
} catch {
|
|
41
|
+
return loginRedirect(context, { error: "invalid", reason: "step-up-required", next });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!identifier || !code) {
|
|
45
|
+
return loginRedirect(context, {
|
|
46
|
+
sent: true,
|
|
47
|
+
email: identifier,
|
|
48
|
+
reason: "step-up-required",
|
|
49
|
+
error: "invalid",
|
|
50
|
+
next,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const client = await getAccessClient();
|
|
55
|
+
const store = await getSessionStore();
|
|
56
|
+
if (!client || !store) {
|
|
57
|
+
return loginRedirect(context, {
|
|
58
|
+
sent: true,
|
|
59
|
+
email: identifier,
|
|
60
|
+
reason: "step-up-required",
|
|
61
|
+
error: "unconfigured",
|
|
62
|
+
next,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const result = await client.verifySmsChallenge(identifier, code);
|
|
67
|
+
if (!result.ok) {
|
|
68
|
+
// reason ∈ invalid|expired|too_many_attempts|error — keep the step-up card up.
|
|
69
|
+
return loginRedirect(context, {
|
|
70
|
+
sent: true,
|
|
71
|
+
email: identifier,
|
|
72
|
+
reason: "step-up-required",
|
|
73
|
+
error: result.reason,
|
|
74
|
+
next,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Prove authorization offline, identically to a normal federated verify. Fail
|
|
79
|
+
// closed — never trust the flat JSON.
|
|
80
|
+
const verifier = await getFederatedVerifier();
|
|
81
|
+
if (!verifier) {
|
|
82
|
+
console.error("auth/step-up-verify: federated verifier unconfigured (TOT_CORE_URL/STOREFRONT_APP_DOMAIN)");
|
|
83
|
+
return loginRedirect(context, {
|
|
84
|
+
sent: true,
|
|
85
|
+
email: identifier,
|
|
86
|
+
reason: "step-up-required",
|
|
87
|
+
error: "unconfigured",
|
|
88
|
+
next,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let derived;
|
|
93
|
+
try {
|
|
94
|
+
const claims = await verifier.verify(result.idToken); // enforces aud === STOREFRONT_APP_DOMAIN
|
|
95
|
+
derived = sessionFromFederatedClaims(claims);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error("auth/step-up-verify: id_token verification failed; failing closed", {
|
|
98
|
+
error: err instanceof Error ? err.message : String(err),
|
|
99
|
+
});
|
|
100
|
+
return loginRedirect(context, {
|
|
101
|
+
sent: true,
|
|
102
|
+
email: identifier,
|
|
103
|
+
reason: "step-up-required",
|
|
104
|
+
error: "verify_failed",
|
|
105
|
+
next,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
110
|
+
const { id } = await createSession(
|
|
111
|
+
store,
|
|
112
|
+
{
|
|
113
|
+
email: derived.email,
|
|
114
|
+
resource: derived.resource,
|
|
115
|
+
roles: [],
|
|
116
|
+
capability: "member",
|
|
117
|
+
tenants: derived.tenants,
|
|
118
|
+
staff: derived.staff,
|
|
119
|
+
// The whole point of this route: record the assurance the fresher
|
|
120
|
+
// credential established (e.g. `tot:aal:sms`) on the session.
|
|
121
|
+
acr: derived.acr,
|
|
122
|
+
authTime: derived.authTime,
|
|
123
|
+
},
|
|
124
|
+
DEFAULT_SESSION_TTL_SECONDS,
|
|
125
|
+
nowSeconds,
|
|
126
|
+
);
|
|
127
|
+
setSessionCookie(context, id, DEFAULT_SESSION_TTL_SECONDS);
|
|
128
|
+
return context.redirect(next, 303);
|
|
129
|
+
}
|
|
@@ -83,6 +83,25 @@ export async function POST(context: APIContext): Promise<Response> {
|
|
|
83
83
|
|
|
84
84
|
const result = await client.verifyFederatedCode(email, code);
|
|
85
85
|
if (!result.ok) {
|
|
86
|
+
// RFC 9470 step-up challenge: tot20 needs a fresher credential (e.g. SMS)
|
|
87
|
+
// before it will complete sign-in. Surface a distinct `reason` (not a plain
|
|
88
|
+
// `error`) so the login page can offer the step-up completion card instead
|
|
89
|
+
// of "invalid code". The `requiredAcr` lets it know which credential to ask for.
|
|
90
|
+
if (result.reason === "step-up-required") {
|
|
91
|
+
return loginRedirect(context, {
|
|
92
|
+
sent: true,
|
|
93
|
+
email,
|
|
94
|
+
reason: "step-up-required",
|
|
95
|
+
requiredAcr: result.stepUp.requiredAcr,
|
|
96
|
+
next,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// Identity is bound (e.g. from an invite flow); sign-in awaits step-up
|
|
100
|
+
// completion. Distinct from step-up-required: no fresh challenge to answer
|
|
101
|
+
// yet, just a "come back after step-up" state.
|
|
102
|
+
if (result.reason === "success-pending") {
|
|
103
|
+
return loginRedirect(context, { sent: true, email, reason: "success-pending", next });
|
|
104
|
+
}
|
|
86
105
|
// Show the code step again with the specific reason (invalid/expired/too_many_attempts).
|
|
87
106
|
return loginRedirect(context, { sent: true, email, error: result.reason, next });
|
|
88
107
|
}
|
|
@@ -126,6 +145,10 @@ export async function POST(context: APIContext): Promise<Response> {
|
|
|
126
145
|
capability: "member",
|
|
127
146
|
tenants: derived.tenants,
|
|
128
147
|
staff: derived.staff,
|
|
148
|
+
// Record the assurance the assertion carried, if any (e.g. a token already
|
|
149
|
+
// satisfying a step-up would arrive with `acr: 'tot:aal:sms'`).
|
|
150
|
+
acr: derived.acr,
|
|
151
|
+
authTime: derived.authTime,
|
|
129
152
|
},
|
|
130
153
|
DEFAULT_SESSION_TTL_SECONDS,
|
|
131
154
|
nowSeconds,
|