@tokenoftrust/storefront-runner 2.0.1 → 2.0.3
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/dev-plugins/tenant-hot-reload.mjs +19 -1
- package/apps/storefront/dev-plugins/tenant-validate-on-save.mjs +78 -0
- package/apps/storefront/src/middleware/index.ts +17 -1
- package/package.json +1 -1
- package/scripts/lib/tenant-dirs.mjs +80 -0
- package/scripts/tenant/lint-embeds.mjs +180 -0
- package/scripts/tenant/materialize-tenant-content.mjs +533 -0
- package/scripts/tenant/validate.mjs +837 -0
- package/apps/storefront/scripts/build-v84-clean-clone-standalone.mjs +0 -68
- package/apps/storefront/scripts/deny-v84-clean-clone-standalone-capabilities.mjs +0 -111
- package/apps/storefront/scripts/deny-v84-dogfood-capabilities.mjs +0 -2
- package/apps/storefront/scripts/deny-v84-lifecycle-capabilities.mjs +0 -247
- package/apps/storefront/scripts/dogfood-v84-nonportable-routes.ts +0 -73
- package/apps/storefront/scripts/fixed-v84-native-install-producer.ts +0 -79
- package/apps/storefront/scripts/generated/v84-clean-clone-replay.bundle.mjs +0 -10982
- package/apps/storefront/scripts/import-v84-through-ws2.ts +0 -38
- package/apps/storefront/scripts/initialize-v84-lifecycle-tooling-dependency.ts +0 -2
- package/apps/storefront/scripts/initialize-v84-lifecycle-tooling.ts +0 -4
- package/apps/storefront/scripts/install-v84-native-through-ws1.ts +0 -185
- package/apps/storefront/scripts/prove-clone-isolation.ts +0 -506
- package/apps/storefront/scripts/replay-v84-clean-clone-pass.ts +0 -148
- package/apps/storefront/scripts/replay-v84-clean-clone-standalone.ts +0 -35
- package/apps/storefront/scripts/replay-v84-clean-clone.ts +0 -214
- package/apps/storefront/scripts/run-fixed-v84-native-install-producer.mjs +0 -18
- package/apps/storefront/scripts/run-v84-clone-isolation-proof.mjs +0 -18
- package/apps/storefront/scripts/run-v84-clone-lifecycle-proof.mjs +0 -18
- package/apps/storefront/scripts/run-v84-import-through-ws2-proof.mjs +0 -17
- package/apps/storefront/scripts/run-v84-nonportable-route-dogfood.mjs +0 -15
- package/apps/storefront/scripts/sanitize-v84-proof-environment.mjs +0 -21
- package/apps/storefront/scripts/v84CleanCloneGraphPin.ts +0 -42
- package/apps/storefront/scripts/v84CleanCloneSourceGraph.ts +0 -145
- package/apps/storefront/scripts/validate-v84-clone-lifecycle.ts +0 -18
- package/apps/storefront/src/components/admin/AdminAIWorkflowsTab.astro +0 -531
- package/apps/storefront/src/components/admin/AdminBlogDetailRail.astro +0 -878
- package/apps/storefront/src/components/admin/AdminBlogTab.astro +0 -1435
- package/apps/storefront/src/components/admin/AdminCheckoutSettingsPanel.astro +0 -226
- package/apps/storefront/src/components/admin/AdminCustomersTab.astro +0 -593
- package/apps/storefront/src/components/admin/AdminFulfillmentTab.astro +0 -465
- package/apps/storefront/src/components/admin/AdminIntegrationsTab.astro +0 -105
- package/apps/storefront/src/components/admin/AdminProductsTab.astro +0 -901
- package/apps/storefront/src/components/admin/AdminPublishTab.astro +0 -2978
- package/apps/storefront/src/components/admin/AdminReportingTab.astro +0 -435
- package/apps/storefront/src/components/admin/AdminSettingsTab.astro +0 -782
- package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +0 -611
- package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +0 -1223
- package/apps/storefront/src/lib/dev/cockpitStore.ts +0 -57
- package/apps/storefront/src/lib/publish/adminPublishTab.ts +0 -4625
- package/apps/storefront/src/lib/publish/apex-readiness.ts +0 -360
- package/apps/storefront/src/lib/publish/buildStream.ts +0 -269
- package/apps/storefront/src/lib/publish/collapsePersistence.ts +0 -72
- package/apps/storefront/src/lib/publish/dispatchHealth.ts +0 -267
- package/apps/storefront/src/lib/publish/domainState.ts +0 -360
- package/apps/storefront/src/lib/publish/lifecycleState.ts +0 -369
- package/apps/storefront/src/lib/publish/liveReleaseSummary.ts +0 -29
- package/apps/storefront/src/lib/publish/mergeDoctorOnFailure.ts +0 -78
- package/apps/storefront/src/lib/publish/mergeHealthBanner.ts +0 -62
- package/apps/storefront/src/lib/publish/mergeRowFindings.ts +0 -124
- package/apps/storefront/src/lib/publish/mergeSupportEscalation.ts +0 -75
- package/apps/storefront/src/lib/publish/panelPolling.ts +0 -78
- package/apps/storefront/src/lib/publish/publish-guard.ts +0 -88
- package/apps/storefront/src/lib/publish/publishFormat.ts +0 -165
- package/apps/storefront/src/lib/publish/publishHero.ts +0 -177
- package/apps/storefront/src/lib/publish/reviewRelease/changeState.ts +0 -66
- package/apps/storefront/src/lib/publish/reviewRelease/copy.ts +0 -141
- package/apps/storefront/src/lib/publish/reviewRelease/index.ts +0 -41
- package/apps/storefront/src/lib/publish/reviewRelease/permissions.ts +0 -47
- package/apps/storefront/src/lib/publish/reviewRelease/releaseState.ts +0 -42
- package/apps/storefront/src/lib/publish/reviewRelease/types.ts +0 -59
- package/apps/storefront/src/lib/publish/reviewRelease/viewModel.ts +0 -440
- package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/actions.ts +0 -161
- package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/controller.ts +0 -325
- package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/render.ts +0 -241
- package/apps/storefront/src/lib/publish/shipWorkspace.ts +0 -439
- package/apps/storefront/src/lib/publish/sseRoute.ts +0 -119
- package/apps/storefront/src/lib/publish/stageBundleUpload.ts +0 -215
- package/apps/storefront/src/lib/publish/streamLogConsole.ts +0 -142
- package/apps/storefront/src/lib/publish/versionNumber.ts +0 -52
- package/apps/storefront/src/pages/admin/ops-timeline.astro +0 -420
- package/apps/storefront/src/pages/admin/review-release.astro +0 -265
- package/apps/storefront/src/pages/admin/select.astro +0 -96
- package/apps/storefront/src/pages/admin.astro +0 -568
- package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +0 -86
- package/apps/storefront/src/pages/api/apps/admin/gtm-migration-install.ts +0 -174
- package/apps/storefront/src/pages/api/apps/admin/gtm-migration.ts +0 -96
- package/apps/storefront/src/pages/api/apps/admin/health.ts +0 -44
- package/apps/storefront/src/pages/api/apps/admin/install.ts +0 -125
- package/apps/storefront/src/pages/api/apps/admin/integration-lifecycle.ts +0 -118
- package/apps/storefront/src/pages/api/apps/admin/integrations.ts +0 -89
- package/apps/storefront/src/pages/api/apps/admin/list.ts +0 -26
- package/apps/storefront/src/pages/api/apps/admin/platform-adapters.ts +0 -85
- package/apps/storefront/src/pages/api/apps/admin/resume.ts +0 -79
- package/apps/storefront/src/pages/api/apps/admin/suspend.ts +0 -80
- package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +0 -98
- package/apps/storefront/src/pages/api/apps/admin/update.ts +0 -126
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +0 -70
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries.ts +0 -51
- package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +0 -255
- package/apps/storefront/src/pages/api/apps/oauth/token.ts +0 -87
- package/apps/storefront/src/pages/api/apps/v1/attribution.ts +0 -162
- package/apps/storefront/src/pages/api/apps/v1/catalog/products/[handle].ts +0 -39
- package/apps/storefront/src/pages/api/apps/v1/catalog/products.ts +0 -49
- package/apps/storefront/src/pages/api/apps/v1/health.ts +0 -32
- package/apps/storefront/src/pages/api/apps/v1/inventory.ts +0 -59
- package/apps/storefront/src/pages/api/apps/v1/orders/[id].ts +0 -48
- package/apps/storefront/src/pages/api/apps/v1/orders.ts +0 -73
- package/apps/storefront/src/pages/api/apps/v1/reports.ts +0 -21
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +0 -74
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries.ts +0 -47
- package/apps/storefront/src/pages/api/cache-purge.ts +0 -55
- package/apps/storefront/src/pages/api/compliance/tamper.ts +0 -88
- package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +0 -154
- package/apps/storefront/src/pages/api/dashboard/vendor-search.ts +0 -120
- package/apps/storefront/src/pages/api/integrations/v1/[operation].ts +0 -108
- package/apps/storefront/src/pages/api/publish/stage-bundle.ts +0 -70
- package/apps/storefront/src/pages/api/test/dev-session.ts +0 -133
- package/apps/storefront/src/pages/cockpit.astro +0 -1548
- package/apps/storefront/src/pages/dashboard/[appDomain]/apps/index.astro +0 -119
- package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +0 -105
- package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +0 -177
- package/apps/storefront/src/pages/dashboard/index.astro +0 -160
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `GET /api/apps/v1/health` — the D4 install delivery/telemetry health read
|
|
3
|
-
* (PrivateApps epic; `docs/private-apps/contract/openapi.yaml`'s
|
|
4
|
-
* `getInstallHealth`). Requires no scope beyond a valid token — only ever the
|
|
5
|
-
* caller's OWN install health (the `sub` claim `authorizeAppRequest` already
|
|
6
|
-
* resolved), never another install's.
|
|
7
|
-
*
|
|
8
|
-
* `WebhookDeliveryStore.getInstallHealth`'s `InstallHealth` return already IS
|
|
9
|
-
* the contract's `InstallHealth` shape 1:1, incl. `p95DeliveryMs` (a
|
|
10
|
-
* never-attempted install reads as healthy/zeroed) — no mapper needed, unlike
|
|
11
|
-
* `Delivery` below.
|
|
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 } from "@/lib/webhooks/deliveryStore.js";
|
|
17
|
-
import { fromD1 } from "@/lib/d1/catalog.js";
|
|
18
|
-
import { readD1 } from "@/lib/env";
|
|
19
|
-
|
|
20
|
-
export const prerender = false;
|
|
21
|
-
|
|
22
|
-
export async function GET(context: APIContext): Promise<Response> {
|
|
23
|
-
const auth = await authorizeAppRequest(context, null);
|
|
24
|
-
if (!auth.ok) return withRequestId(auth.response, context);
|
|
25
|
-
|
|
26
|
-
const d1 = await readD1("STOREFRONT_APPS_DB");
|
|
27
|
-
if (!d1) return withRequestId(json({ error: "webhook store not configured", code: "unconfigured" }, 503), context);
|
|
28
|
-
const store = new WebhookDeliveryStore(fromD1(d1));
|
|
29
|
-
|
|
30
|
-
const health = await store.getInstallHealth(auth.install.installId);
|
|
31
|
-
return withRequestId(json(health), context);
|
|
32
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `GET /api/apps/v1/inventory` — the D3 inventory-availability read
|
|
3
|
-
* (PrivateApps epic; `docs/private-apps/contract/openapi.yaml`'s
|
|
4
|
-
* `listInventory`). Requires scope `inventory:read`.
|
|
5
|
-
*
|
|
6
|
-
* No new table: this projects `{sku, available, quantity?, policy?}` off the
|
|
7
|
-
* variants embedded in the existing catalog read model
|
|
8
|
-
* (`../../../lib/d1/catalog.js`'s `listAllActiveProducts`) — active products
|
|
9
|
-
* only. A variant with no `sku` is skipped; the contract's `InventoryList`
|
|
10
|
-
* item requires one, and there's nothing to key it by otherwise.
|
|
11
|
-
*/
|
|
12
|
-
import type { APIContext } from "astro";
|
|
13
|
-
import { authorizeAppRequest } from "@/lib/apps/apiAuth.js";
|
|
14
|
-
import { json, withRequestId } from "@/lib/apps/apiRoute.js";
|
|
15
|
-
import { listAllActiveProducts, fromD1 } from "@/lib/d1/catalog.js";
|
|
16
|
-
import { readD1 } from "@/lib/env";
|
|
17
|
-
|
|
18
|
-
export const prerender = false;
|
|
19
|
-
|
|
20
|
-
interface InventoryItem {
|
|
21
|
-
sku: string;
|
|
22
|
-
available: boolean;
|
|
23
|
-
quantity?: number;
|
|
24
|
-
policy?: "deny" | "continue";
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function toInventoryItem(policy: string | undefined, quantity: number | undefined, sku: string, available: boolean): InventoryItem {
|
|
28
|
-
return {
|
|
29
|
-
sku,
|
|
30
|
-
available,
|
|
31
|
-
quantity,
|
|
32
|
-
policy: policy === "deny" || policy === "continue" ? policy : undefined,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export async function GET(context: APIContext): Promise<Response> {
|
|
37
|
-
const auth = await authorizeAppRequest(context, "inventory:read");
|
|
38
|
-
if (!auth.ok) return withRequestId(auth.response, context);
|
|
39
|
-
|
|
40
|
-
const d1 = await readD1("CATALOG_DB");
|
|
41
|
-
if (!d1) return withRequestId(json({ error: "catalog not configured", code: "unconfigured" }, 503), context);
|
|
42
|
-
const db = fromD1(d1);
|
|
43
|
-
|
|
44
|
-
const skuFilter = new URL(context.request.url).searchParams.get("sku");
|
|
45
|
-
const products = await listAllActiveProducts(db);
|
|
46
|
-
|
|
47
|
-
const items: InventoryItem[] = [];
|
|
48
|
-
for (const product of products) {
|
|
49
|
-
for (const variant of product.variants) {
|
|
50
|
-
if (!variant.sku) continue;
|
|
51
|
-
if (skuFilter && variant.sku !== skuFilter) continue;
|
|
52
|
-
items.push(
|
|
53
|
-
toInventoryItem(variant.inventory.policy, variant.inventory.quantity, variant.sku, variant.inventory.available),
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return withRequestId(json({ items }), context);
|
|
59
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `GET /api/apps/v1/orders/{id}` — the D6 minimal single-order read
|
|
3
|
-
* (PrivateApps epic; `docs/private-apps/contract/openapi.yaml`'s `getOrder`).
|
|
4
|
-
* Requires scope `orders:read:minimal`.
|
|
5
|
-
*
|
|
6
|
-
* `{id}` is tot-foxycart's `externalId` — the same id `listOrders` echoes
|
|
7
|
-
* back as `Order.id`. There's no cross-tenant oracle here: the Foxy partner
|
|
8
|
-
* API key is itself tenant-scoped, so a call can only ever resolve THIS
|
|
9
|
-
* tenant's own orders — an unknown id and a different tenant's id both come
|
|
10
|
-
* back `null` from `FoxyOrderClient.getOrder` and are reported identically
|
|
11
|
-
* as 404.
|
|
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 } from "@/lib/apps/orders/orderMapper.js";
|
|
17
|
-
import { getFoxyOrderClient } from "@/lib/apps/orders/foxyOrderClient.js";
|
|
18
|
-
|
|
19
|
-
export const prerender = false;
|
|
20
|
-
|
|
21
|
-
export async function GET(context: APIContext): Promise<Response> {
|
|
22
|
-
const auth = await authorizeAppRequest(context, "orders:read:minimal");
|
|
23
|
-
if (!auth.ok) return withRequestId(auth.response, context);
|
|
24
|
-
|
|
25
|
-
const id = context.params.id;
|
|
26
|
-
if (!id) return withRequestId(json({ error: "missing order id", code: "bad_request" }, 400), context);
|
|
27
|
-
|
|
28
|
-
let client;
|
|
29
|
-
try {
|
|
30
|
-
client = await getFoxyOrderClient();
|
|
31
|
-
} catch {
|
|
32
|
-
return withRequestId(json({ error: "orders not configured", code: "unconfigured" }, 503), context);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
let foxyOrder;
|
|
36
|
-
try {
|
|
37
|
-
foxyOrder = await client.getOrder(auth.install.tenantId, id);
|
|
38
|
-
} catch {
|
|
39
|
-
return withRequestId(json({ error: "failed to read order", code: "upstream_error" }, 502), context);
|
|
40
|
-
}
|
|
41
|
-
if (!foxyOrder) return withRequestId(json({ error: "order not found", code: "not_found" }, 404), context);
|
|
42
|
-
|
|
43
|
-
const order = await toContractOrder(foxyOrder, {
|
|
44
|
-
tenantId: auth.install.tenantId,
|
|
45
|
-
installId: auth.install.installId,
|
|
46
|
-
});
|
|
47
|
-
return withRequestId(json(order), context);
|
|
48
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
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
|
-
import { recordActivity } from "@/lib/activity/recordActivity";
|
|
31
|
-
|
|
32
|
-
export const prerender = false;
|
|
33
|
-
|
|
34
|
-
export async function POST(context: APIContext): Promise<Response> {
|
|
35
|
-
const auth = await authorizeAppRequest(context, null);
|
|
36
|
-
if (!auth.ok) return withRequestId(auth.response, context);
|
|
37
|
-
|
|
38
|
-
if (!context.request.headers.get("idempotency-key")) {
|
|
39
|
-
return withRequestId(json({ error: "Idempotency-Key header is required" }, 400), context);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const deliveryId = context.params.deliveryId;
|
|
43
|
-
if (!deliveryId) {
|
|
44
|
-
return withRequestId(json({ error: "deliveryId is required", code: "not_found" }, 404), context);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const d1 = await readD1("STOREFRONT_APPS_DB");
|
|
48
|
-
if (!d1) return withRequestId(json({ error: "webhook store not configured", code: "unconfigured" }, 503), context);
|
|
49
|
-
const store = new WebhookDeliveryStore(fromD1(d1));
|
|
50
|
-
|
|
51
|
-
const delivery = await store.getDelivery(deliveryId);
|
|
52
|
-
if (!delivery || delivery.installId !== auth.install.installId) {
|
|
53
|
-
return withRequestId(json({ error: "Delivery not found.", code: "not_found" }, 404), context);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const dispatcher = await getDispatcher();
|
|
57
|
-
if (dispatcher instanceof Response) return withRequestId(dispatcher, context);
|
|
58
|
-
|
|
59
|
-
const requeued = await store.requeueForReplay(deliveryId);
|
|
60
|
-
await dispatcher.enqueue({ deliveryId });
|
|
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
|
-
});
|
|
73
|
-
return withRequestId(json(toContractDelivery(requeued), 202), context);
|
|
74
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* POST /api/cache-purge — bump a tenant's edge page-cache version.
|
|
3
|
-
*
|
|
4
|
-
* The ETL load step calls this per tenant after a (re)migration so freshly
|
|
5
|
-
* migrated content is visible immediately, rather than waiting out the page
|
|
6
|
-
* TTL. This closes the loop that HttpToTClient.purgeTenant() (catalog API cache)
|
|
7
|
-
* only half-covered: that purges the data cache; this purges the rendered HTML.
|
|
8
|
-
*
|
|
9
|
-
* Auth: `Authorization: Bearer <CACHE_PURGE_SECRET>`. JSON bodies are exempt from
|
|
10
|
-
* Astro's CSRF origin check, so the server-to-server ETL caller works.
|
|
11
|
-
*/
|
|
12
|
-
import type { APIContext } from "astro";
|
|
13
|
-
import { readEnv, readKv } from "@/lib/env";
|
|
14
|
-
import { bumpCacheVersion } from "@/lib/edgeCache";
|
|
15
|
-
import { recordActivity } from "@/lib/activity/recordActivity";
|
|
16
|
-
|
|
17
|
-
export const prerender = false;
|
|
18
|
-
|
|
19
|
-
export async function POST(context: APIContext): Promise<Response> {
|
|
20
|
-
const secret = await readEnv("CACHE_PURGE_SECRET");
|
|
21
|
-
const auth = context.request.headers.get("authorization");
|
|
22
|
-
if (!secret || auth !== `Bearer ${secret}`) {
|
|
23
|
-
return new Response("Unauthorized", { status: 401 });
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const kv = await readKv("TENANT_CACHE");
|
|
27
|
-
if (!kv) {
|
|
28
|
-
return new Response("No cache store bound", { status: 503 });
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
let tenantId: string | undefined;
|
|
32
|
-
try {
|
|
33
|
-
tenantId = ((await context.request.json()) as { tenantId?: string })?.tenantId;
|
|
34
|
-
} catch {
|
|
35
|
-
/* fall through to 400 */
|
|
36
|
-
}
|
|
37
|
-
if (!tenantId) {
|
|
38
|
-
return new Response("tenantId required", { status: 400 });
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const version = await bumpCacheVersion(kv, tenantId);
|
|
42
|
-
// D11: the caller is the Bearer-secret ETL load step (a machine, no session) — emit as
|
|
43
|
-
// the `agent` cohort with an opaque handle. Best-effort; never affects the purge.
|
|
44
|
-
recordActivity({
|
|
45
|
-
action: "cache.purged",
|
|
46
|
-
actor: { kind: "agent", id: "storefront" },
|
|
47
|
-
source: "server",
|
|
48
|
-
outcome: { status: "succeeded" },
|
|
49
|
-
scope: { tenantId },
|
|
50
|
-
payload: { args: { version } },
|
|
51
|
-
});
|
|
52
|
-
return new Response(JSON.stringify({ tenantId, version }), {
|
|
53
|
-
headers: { "content-type": "application/json" },
|
|
54
|
-
});
|
|
55
|
-
}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* POST /api/compliance/tamper — the server receiver for protected-surface tamper
|
|
3
|
-
* beacons (D-R7, `integration-protected-tamper-recovery`).
|
|
4
|
-
*
|
|
5
|
-
* The browser sentinel restores safe state IN the document and posts a
|
|
6
|
-
* `TamperEvidenceBeacon` here. This route validates it fail-closed, records the
|
|
7
|
-
* durable evidence + operator alert, and — when the beacon is attributable to a
|
|
8
|
-
* single active install — SUSPENDS that install. It is a DETECTIVE control: the
|
|
9
|
-
* suspension is after-the-fact recovery, never prevention.
|
|
10
|
-
*
|
|
11
|
-
* TENANT SCOPE is server-owned: the install lookup and suspend are ALWAYS keyed on
|
|
12
|
-
* `locals.tenant` (middleware-resolved from the request host), never on the
|
|
13
|
-
* body-supplied `tenant` — a page cannot ask to suspend another tenant's app. The
|
|
14
|
-
* beacon body carries no secret (the contract rejects one), and the response is a
|
|
15
|
-
* write-path ack the emitter ignores, so a failed post never breaks the page.
|
|
16
|
-
*/
|
|
17
|
-
import type { APIContext } from "astro";
|
|
18
|
-
import { getRegistryService, json } from "@/lib/apps/adminService.js";
|
|
19
|
-
import type { AppInstall } from "@/lib/apps/registryService.js";
|
|
20
|
-
import { RegistryError } from "@/lib/apps/registryService.js";
|
|
21
|
-
import { planTamperRecovery, type AttributableInstall } from "@tot/public-runtime";
|
|
22
|
-
|
|
23
|
-
export const prerender = false;
|
|
24
|
-
|
|
25
|
-
const SYSTEM_ACTOR = "system:tamper-sentinel";
|
|
26
|
-
|
|
27
|
-
function toAttributable(install: AppInstall): AttributableInstall {
|
|
28
|
-
return {
|
|
29
|
-
installId: install.installId,
|
|
30
|
-
appId: install.appId,
|
|
31
|
-
appVersion: install.appVersion,
|
|
32
|
-
status: install.status,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export async function POST(context: APIContext): Promise<Response> {
|
|
37
|
-
const tenantId = context.locals.tenant?.tenant_id;
|
|
38
|
-
// No resolved tenant ⇒ nothing third-party could be scoped to; accept-and-drop so
|
|
39
|
-
// the emitter never errors or retries.
|
|
40
|
-
if (!tenantId) return json({ ok: true, accepted: false }, 202);
|
|
41
|
-
|
|
42
|
-
let body: unknown;
|
|
43
|
-
try {
|
|
44
|
-
body = await context.request.json();
|
|
45
|
-
} catch {
|
|
46
|
-
return json({ ok: false, error: "invalid JSON body" }, 400);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Resolve the tenant's installs (empty when the registry binding isn't configured
|
|
50
|
-
// in this runtime — the beacon is still validated + recorded, just not actioned).
|
|
51
|
-
const registry = await getRegistryService();
|
|
52
|
-
const installs: AttributableInstall[] =
|
|
53
|
-
registry instanceof Response
|
|
54
|
-
? []
|
|
55
|
-
: (await registry.listByTenant(tenantId)).map(toAttributable);
|
|
56
|
-
|
|
57
|
-
const plan = planTamperRecovery(body, installs, {
|
|
58
|
-
recordId: crypto.randomUUID(),
|
|
59
|
-
receivedAt: new Date().toISOString(),
|
|
60
|
-
});
|
|
61
|
-
if (!plan.ok) return json({ ok: false, error: "invalid tamper beacon" }, 400);
|
|
62
|
-
|
|
63
|
-
const { record, suspend } = plan.plan;
|
|
64
|
-
// Durable, secret-free evidence line for operations. The beacon itself is the record.
|
|
65
|
-
// eslint-disable-next-line no-console
|
|
66
|
-
console.warn(
|
|
67
|
-
`[tamper] tenant=${tenantId} record=${record.recordId} surface=${record.beacon.surface} kind=${record.beacon.kind} recovered=${record.beacon.recovered} app=${record.beacon.attribution?.appId ?? "unattributable"}`,
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
let suspended = false;
|
|
71
|
-
if (suspend && !(registry instanceof Response)) {
|
|
72
|
-
try {
|
|
73
|
-
await registry.suspend(suspend.installId, SYSTEM_ACTOR);
|
|
74
|
-
suspended = true;
|
|
75
|
-
} catch (err) {
|
|
76
|
-
// An already-suspended/uninstalled install (invalid_state) is a benign
|
|
77
|
-
// no-op under rapid re-mutation; anything else is swallowed so the write
|
|
78
|
-
// path never errors the emitter. The audit row from a successful suspend is
|
|
79
|
-
// the durable trail.
|
|
80
|
-
if (!(err instanceof RegistryError)) {
|
|
81
|
-
// eslint-disable-next-line no-console
|
|
82
|
-
console.error(`[tamper] suspend failed for ${suspend.installId}`, err);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return json({ ok: true, accepted: true, recovered: record.beacon.recovered, suspended }, 202);
|
|
88
|
-
}
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* POST /api/dashboard/enter-vendor { vendorAppDomain } — the ToT-STAFF admission
|
|
3
|
-
* endpoint (#8425 staff access). Phase 2 of two-phase admission: on the staff
|
|
4
|
-
* viewer explicitly selecting a searched vendor, tot20 writes the server-side
|
|
5
|
-
* AUDIT row and returns the staffRoles-scoped capability; we then stamp a
|
|
6
|
-
* short-lived {@link StaffSelection} on the session and admit the viewer to
|
|
7
|
-
* `/dashboard/<appDomain>` scoped to that capability.
|
|
8
|
-
*
|
|
9
|
-
* AUTHORIZATION — two independent axes:
|
|
10
|
-
* NEAR (this endpoint): a valid `tot_session` carrying ToT-STAFF scope
|
|
11
|
-
* (`staff[]` non-empty). No session → 401; no staff scope → 403.
|
|
12
|
-
* FAR (tot20): `/clients/staffVendorAccess`, operator-authed as storefront,
|
|
13
|
-
* presenting a storefront-signed staff broker assertion in the
|
|
14
|
-
* `X-ToT-Broker-Assertion` header (minted from the verified session email).
|
|
15
|
-
* tot20 verifies it, re-checks staff scope from its `sub`, scopes the capability,
|
|
16
|
-
* and writes the audit row — it is the authority. No plaintext email on the wire.
|
|
17
|
-
*
|
|
18
|
-
* The staff selection is a DISTINCT access path from `tenants[]`: we NEVER
|
|
19
|
-
* synthesize a membership. The dashboard guard + login gate consult the marker
|
|
20
|
-
* separately (see tenantSelection.staffAdmits / loginGate.sessionAdmitsHost).
|
|
21
|
-
*/
|
|
22
|
-
import type { APIContext } from "astro";
|
|
23
|
-
import { readViewerSession, getSessionMutationHandle } from "@/lib/auth/route";
|
|
24
|
-
import { updateSession } from "@/lib/auth/session";
|
|
25
|
-
import { isStaffSession, buildStaffSelection } from "@/lib/dashboard/staffAdmission";
|
|
26
|
-
import {
|
|
27
|
-
createStaffVendorAccessClient,
|
|
28
|
-
staffAccessHttpStatus,
|
|
29
|
-
StaffVendorAccessClientError,
|
|
30
|
-
} from "@/lib/dashboard/staffVendorAccess";
|
|
31
|
-
import { mintBrokerAssertion, BrokerAssertionError } from "@/lib/auth/brokerAssertion";
|
|
32
|
-
import { resolveUiActor, recordUiActivity } from "@/lib/activity/uiActor";
|
|
33
|
-
|
|
34
|
-
export const prerender = false;
|
|
35
|
-
|
|
36
|
-
/** JSON response with no-store (staff access state must never be edge-cached). */
|
|
37
|
-
function json(body: unknown, status = 200): Response {
|
|
38
|
-
return new Response(JSON.stringify(body), {
|
|
39
|
-
status,
|
|
40
|
-
headers: { "content-type": "application/json", "cache-control": "no-store" },
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export async function POST(context: APIContext): Promise<Response> {
|
|
45
|
-
const session = await readViewerSession(context);
|
|
46
|
-
if (!session) return json({ error: "not signed in" }, 401);
|
|
47
|
-
if (!isStaffSession(session)) {
|
|
48
|
-
return json({ error: "staff access required", reason: "notStaff" }, 403);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
let body: { vendorAppDomain?: unknown };
|
|
52
|
-
try {
|
|
53
|
-
body = (await context.request.json()) as typeof body;
|
|
54
|
-
} catch {
|
|
55
|
-
return json({ error: "invalid JSON body" }, 400);
|
|
56
|
-
}
|
|
57
|
-
const vendorAppDomain =
|
|
58
|
-
typeof body.vendorAppDomain === "string" ? body.vendorAppDomain.trim() : "";
|
|
59
|
-
if (!vendorAppDomain) return json({ error: "vendorAppDomain is required" }, 400);
|
|
60
|
-
|
|
61
|
-
// D11: staff-impersonation audit event. Resolve the actor AFTER the staff gate above,
|
|
62
|
-
// so the cohort is always `admin` (a ToT-staff session); the staff email is PII and is
|
|
63
|
-
// never stored (actor.id is its salted hash). Reused at each emit exit point below.
|
|
64
|
-
const activityActor = await resolveUiActor(context, {});
|
|
65
|
-
|
|
66
|
-
let client;
|
|
67
|
-
try {
|
|
68
|
-
client = await createStaffVendorAccessClient();
|
|
69
|
-
} catch (err) {
|
|
70
|
-
if (err instanceof StaffVendorAccessClientError) {
|
|
71
|
-
return json({ error: "staff-access integration not configured" }, 503);
|
|
72
|
-
}
|
|
73
|
-
throw err;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// #8453 `staff-invite-proof-via-broker`: prove the staff actor with a fresh
|
|
77
|
-
// storefront-signed assertion minted from the VERIFIED session email. Fail closed
|
|
78
|
-
// (503) if the signing key is unconfigured.
|
|
79
|
-
let brokerAssertion: string;
|
|
80
|
-
try {
|
|
81
|
-
brokerAssertion = await mintBrokerAssertion(session.email);
|
|
82
|
-
} catch (err) {
|
|
83
|
-
if (err instanceof BrokerAssertionError) {
|
|
84
|
-
return json({ error: "staff-proof signing not configured" }, 503);
|
|
85
|
-
}
|
|
86
|
-
throw err;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// FAR authority: tot20 verifies the assertion, derives the staff actor, scopes the
|
|
90
|
-
// capability, and writes the audit row. We admit ONLY on its ok.
|
|
91
|
-
const result = await client.grantStaffAccess(brokerAssertion, vendorAppDomain);
|
|
92
|
-
if (!result.ok) {
|
|
93
|
-
recordUiActivity(activityActor, {
|
|
94
|
-
action: "dashboard.vendor_entered",
|
|
95
|
-
outcome: { status: "refused", errorClass: result.reason ?? `http_${result.status}` },
|
|
96
|
-
scope: { tenantId: vendorAppDomain },
|
|
97
|
-
payload: { args: { resource: vendorAppDomain } },
|
|
98
|
-
});
|
|
99
|
-
return json(
|
|
100
|
-
{ error: result.message, reason: result.reason, code: result.code },
|
|
101
|
-
staffAccessHttpStatus(result.reason, result.status),
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Stamp the distinct staff-selection marker on the session (short-lived, scoped
|
|
106
|
-
// to the returned capability). Never a tenants[] entry.
|
|
107
|
-
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
108
|
-
const selection = buildStaffSelection({
|
|
109
|
-
resource: vendorAppDomain,
|
|
110
|
-
capability: result.capability,
|
|
111
|
-
nowSeconds,
|
|
112
|
-
});
|
|
113
|
-
const handle = await getSessionMutationHandle(context);
|
|
114
|
-
if (!handle) {
|
|
115
|
-
// No SESSION KV / no cookie (e.g. plain `astro dev`) — the audit row is
|
|
116
|
-
// written but we can't persist the marker. Fail closed so the viewer isn't
|
|
117
|
-
// told they're admitted when the guard can't see it.
|
|
118
|
-
recordUiActivity(activityActor, {
|
|
119
|
-
action: "dashboard.vendor_entered",
|
|
120
|
-
outcome: { status: "failed", errorClass: "session_store_unavailable" },
|
|
121
|
-
scope: { tenantId: vendorAppDomain },
|
|
122
|
-
payload: { args: { resource: vendorAppDomain, capability: result.capability } },
|
|
123
|
-
});
|
|
124
|
-
return json({ error: "session store unavailable; cannot persist admission" }, 503);
|
|
125
|
-
}
|
|
126
|
-
const updated = await updateSession(
|
|
127
|
-
handle.store,
|
|
128
|
-
handle.id,
|
|
129
|
-
(rec) => ({ ...rec, staffSelection: selection }),
|
|
130
|
-
nowSeconds,
|
|
131
|
-
);
|
|
132
|
-
if (!updated) {
|
|
133
|
-
recordUiActivity(activityActor, {
|
|
134
|
-
action: "dashboard.vendor_entered",
|
|
135
|
-
outcome: { status: "failed", errorClass: "session_expired" },
|
|
136
|
-
scope: { tenantId: vendorAppDomain },
|
|
137
|
-
payload: { args: { resource: vendorAppDomain, capability: result.capability } },
|
|
138
|
-
});
|
|
139
|
-
return json({ error: "session expired; sign in again" }, 401);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
recordUiActivity(activityActor, {
|
|
143
|
-
action: "dashboard.vendor_entered",
|
|
144
|
-
outcome: { status: "succeeded" },
|
|
145
|
-
scope: { tenantId: vendorAppDomain },
|
|
146
|
-
payload: { args: { resource: vendorAppDomain, capability: result.capability } },
|
|
147
|
-
});
|
|
148
|
-
return json({
|
|
149
|
-
ok: true,
|
|
150
|
-
appDomain: vendorAppDomain,
|
|
151
|
-
capability: result.capability,
|
|
152
|
-
redirect: `/dashboard/${encodeURIComponent(vendorAppDomain)}`,
|
|
153
|
-
});
|
|
154
|
-
}
|