@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,120 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GET|POST /api/dashboard/vendor-search?q=<prefix> — the ToT-STAFF vendor search
|
|
3
|
-
* proxy (#8425 staff access). Phase 1 of two-phase staff admission: a staff viewer
|
|
4
|
-
* finds a vendor by prefix; there is no silent access to every host.
|
|
5
|
-
*
|
|
6
|
-
* AUTHORIZATION — two independent axes:
|
|
7
|
-
* NEAR (this endpoint): a valid `tot_session` that carries ToT-STAFF scope
|
|
8
|
-
* (`staff[]` non-empty). No session → 401; a session without staff scope →
|
|
9
|
-
* 403 (a plain member has nothing to search across — they use their picker).
|
|
10
|
-
* FAR (tot20): storefront calls `/clients/vendorSearch` as its OWN operator
|
|
11
|
-
* identity, presenting a storefront-signed staff broker assertion in the
|
|
12
|
-
* `X-ToT-Broker-Assertion` header (minted from the verified session email);
|
|
13
|
-
* tot20 verifies it, derives the staff actor from its `sub`, and scopes results
|
|
14
|
-
* to that user's staffRoles. No spoofable plaintext email crosses the wire.
|
|
15
|
-
*
|
|
16
|
-
* GET reads `q` (+ optional `limit`) from the query; POST reads them from a JSON
|
|
17
|
-
* body — the autocomplete client uses GET, but POST is accepted so a no-JS/form
|
|
18
|
-
* path works too. Returns `{ vendors: [{ appDomain, displayName }] }`.
|
|
19
|
-
*/
|
|
20
|
-
import type { APIContext } from "astro";
|
|
21
|
-
import { readViewerSession } from "@/lib/auth/route";
|
|
22
|
-
import { isStaffSession } from "@/lib/dashboard/staffAdmission";
|
|
23
|
-
import {
|
|
24
|
-
createStaffVendorAccessClient,
|
|
25
|
-
staffAccessHttpStatus,
|
|
26
|
-
StaffVendorAccessClientError,
|
|
27
|
-
} from "@/lib/dashboard/staffVendorAccess";
|
|
28
|
-
import { mintBrokerAssertion, BrokerAssertionError } from "@/lib/auth/brokerAssertion";
|
|
29
|
-
import {
|
|
30
|
-
parseVendorQuery,
|
|
31
|
-
ANCHORED_FETCH_LIMIT,
|
|
32
|
-
ANCHORED_DISPLAY_LIMIT,
|
|
33
|
-
} from "@/lib/dashboard/vendorQuery";
|
|
34
|
-
|
|
35
|
-
export const prerender = false;
|
|
36
|
-
|
|
37
|
-
/** JSON response with no-store (staff access state must never be edge-cached). */
|
|
38
|
-
function json(body: unknown, status = 200): Response {
|
|
39
|
-
return new Response(JSON.stringify(body), {
|
|
40
|
-
status,
|
|
41
|
-
headers: { "content-type": "application/json", "cache-control": "no-store" },
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function handle(context: APIContext, q: string, limit: number | undefined): Promise<Response> {
|
|
46
|
-
const session = await readViewerSession(context);
|
|
47
|
-
if (!session) return json({ error: "not signed in" }, 401);
|
|
48
|
-
if (!isStaffSession(session)) {
|
|
49
|
-
return json({ error: "staff access required", reason: "notStaff" }, 403);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
let client;
|
|
53
|
-
try {
|
|
54
|
-
client = await createStaffVendorAccessClient();
|
|
55
|
-
} catch (err) {
|
|
56
|
-
if (err instanceof StaffVendorAccessClientError) {
|
|
57
|
-
return json({ error: "staff-access integration not configured" }, 503);
|
|
58
|
-
}
|
|
59
|
-
throw err;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// #8453 `staff-invite-proof-via-broker`: prove the staff actor with a fresh
|
|
63
|
-
// storefront-signed assertion minted from the VERIFIED session email — tot20
|
|
64
|
-
// derives the actor + scopes results from it. Fail closed (503) if unconfigured.
|
|
65
|
-
let brokerAssertion: string;
|
|
66
|
-
try {
|
|
67
|
-
brokerAssertion = await mintBrokerAssertion(session.email);
|
|
68
|
-
} catch (err) {
|
|
69
|
-
if (err instanceof BrokerAssertionError) {
|
|
70
|
-
return json({ error: "staff-proof signing not configured" }, 503);
|
|
71
|
-
}
|
|
72
|
-
throw err;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Anchor syntax (^prefix / suffix$ / ^exact$) narrows tot20's substring search.
|
|
76
|
-
// Send the anchor-stripped CORE term upstream (with a widened cap so the exact hit
|
|
77
|
-
// is in the set), then post-filter by the anchor against appDomain. A plain query
|
|
78
|
-
// is unchanged: sent as-is, no post-filter. See @/lib/dashboard/vendorQuery.
|
|
79
|
-
const parsed = parseVendorQuery(q);
|
|
80
|
-
const upstreamQuery = parsed.anchored ? parsed.core : q;
|
|
81
|
-
const upstreamLimit = parsed.anchored ? ANCHORED_FETCH_LIMIT : limit;
|
|
82
|
-
|
|
83
|
-
const result = await client.searchVendors(brokerAssertion, upstreamQuery, upstreamLimit);
|
|
84
|
-
if (!result.ok) {
|
|
85
|
-
return json(
|
|
86
|
-
{ error: result.message, reason: result.reason, code: result.code },
|
|
87
|
-
staffAccessHttpStatus(result.reason, result.status),
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
const vendors = parsed.anchored
|
|
91
|
-
? result.vendors
|
|
92
|
-
.filter((v) => parsed.matches(v.appDomain))
|
|
93
|
-
.slice(0, limit && limit > 0 ? limit : ANCHORED_DISPLAY_LIMIT)
|
|
94
|
-
: result.vendors;
|
|
95
|
-
return json({ vendors });
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function parseLimit(raw: string | null | undefined): number | undefined {
|
|
99
|
-
if (raw == null || raw === "") return undefined;
|
|
100
|
-
const n = Number(raw);
|
|
101
|
-
return Number.isFinite(n) && n > 0 ? Math.floor(n) : undefined;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export async function GET(context: APIContext): Promise<Response> {
|
|
105
|
-
const url = new URL(context.request.url);
|
|
106
|
-
const q = (url.searchParams.get("q") ?? "").trim();
|
|
107
|
-
return handle(context, q, parseLimit(url.searchParams.get("limit")));
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export async function POST(context: APIContext): Promise<Response> {
|
|
111
|
-
let body: { q?: unknown; limit?: unknown };
|
|
112
|
-
try {
|
|
113
|
-
body = (await context.request.json()) as typeof body;
|
|
114
|
-
} catch {
|
|
115
|
-
return json({ error: "invalid JSON body" }, 400);
|
|
116
|
-
}
|
|
117
|
-
const q = typeof body.q === "string" ? body.q.trim() : "";
|
|
118
|
-
const limit = typeof body.limit === "number" ? body.limit : undefined;
|
|
119
|
-
return handle(context, q, limit);
|
|
120
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
/** Published managed analytics v1 RPC transport: basePath + client method name. */
|
|
2
|
-
import type { APIContext } from "astro";
|
|
3
|
-
import { resolveOperatorSession } from "@/lib/apps/adminSession.js";
|
|
4
|
-
import { getManagedAnalyticsService, json, requireManageRole } from "@/lib/apps/adminService.js";
|
|
5
|
-
import {
|
|
6
|
-
validateCreateIntegrationInstallRequest, validateCreateMigrationDraftRequest,
|
|
7
|
-
validateDeleteIntegrationInstallRequest, validateDeleteMigrationDraftRequest,
|
|
8
|
-
validateGetAdapterDefinitionRequest, validateGetIntegrationInstallRequest,
|
|
9
|
-
validateGetIntegrationRevisionRequest, validateGetMigrationDraftRequest,
|
|
10
|
-
validateListAdapterDefinitionsRequest, validateListIntegrationInstallsRequest,
|
|
11
|
-
validateListIntegrationRevisionsRequest, validateReviseIntegrationInstallRequest,
|
|
12
|
-
validateSetIntegrationInstallStatusRequest, validateUpdateMigrationDraftRequest,
|
|
13
|
-
type CreateIntegrationInstallRequest, type CreateMigrationDraftRequest,
|
|
14
|
-
type DeleteIntegrationInstallRequest, type DeleteMigrationDraftRequest,
|
|
15
|
-
type GetAdapterDefinitionRequest, type GetIntegrationInstallRequest,
|
|
16
|
-
type GetIntegrationRevisionRequest, type GetMigrationDraftRequest,
|
|
17
|
-
type ListAdapterDefinitionsRequest, type ListIntegrationInstallsRequest,
|
|
18
|
-
type ListIntegrationRevisionsRequest, type ReviseIntegrationInstallRequest,
|
|
19
|
-
type SetIntegrationInstallStatusRequest, type UpdateMigrationDraftRequest,
|
|
20
|
-
} from "@tot/public-runtime";
|
|
21
|
-
import { MANAGED_ANALYTICS_READ_OPERATIONS, managedAnalyticsOpaqueId, sanitizeManagedAnalyticsOutbound, type ManagedAnalyticsOperation } from "@/lib/apps/managedAnalyticsService.js";
|
|
22
|
-
|
|
23
|
-
export const prerender = false;
|
|
24
|
-
type Validator = (input: unknown) => { ok: true; value: unknown } | { ok: false; violations: Array<{ path: string; code: string; message: string }> };
|
|
25
|
-
|
|
26
|
-
// Null-prototype + own-property lookup: `constructor`/prototype names can never
|
|
27
|
-
// enter dispatch, even if a runtime supplies them as a dynamic path segment.
|
|
28
|
-
const VALIDATORS: Readonly<Record<string, Validator>> = Object.assign(Object.create(null) as Record<string, Validator>, {
|
|
29
|
-
listAdapterDefinitions: validateListAdapterDefinitionsRequest,
|
|
30
|
-
getAdapterDefinition: validateGetAdapterDefinitionRequest,
|
|
31
|
-
listIntegrationInstalls: validateListIntegrationInstallsRequest,
|
|
32
|
-
getIntegrationInstall: validateGetIntegrationInstallRequest,
|
|
33
|
-
listIntegrationRevisions: validateListIntegrationRevisionsRequest,
|
|
34
|
-
getIntegrationRevision: validateGetIntegrationRevisionRequest,
|
|
35
|
-
getMigrationDraft: validateGetMigrationDraftRequest,
|
|
36
|
-
createMigrationDraft: validateCreateMigrationDraftRequest,
|
|
37
|
-
updateMigrationDraft: validateUpdateMigrationDraftRequest,
|
|
38
|
-
deleteMigrationDraft: validateDeleteMigrationDraftRequest,
|
|
39
|
-
createIntegrationInstall: validateCreateIntegrationInstallRequest,
|
|
40
|
-
reviseIntegrationInstall: validateReviseIntegrationInstallRequest,
|
|
41
|
-
setIntegrationInstallStatus: validateSetIntegrationInstallStatusRequest,
|
|
42
|
-
deleteIntegrationInstall: validateDeleteIntegrationInstallRequest,
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const STATUS: Record<string, number> = {
|
|
46
|
-
invalid_request: 400, unauthorized: 401, forbidden: 403, not_found: 404,
|
|
47
|
-
validation_failed: 422, unsupported_version: 400, stale_revision: 409,
|
|
48
|
-
idempotency_conflict: 409, adapter_deprecated: 409, conflict: 409,
|
|
49
|
-
internal_error: 500,
|
|
50
|
-
};
|
|
51
|
-
function response(body: unknown): Response {
|
|
52
|
-
const code = body && typeof body === "object" && "error" in body
|
|
53
|
-
? (body as { error?: { code?: string } }).error?.code : undefined;
|
|
54
|
-
return json(body, code ? STATUS[code] ?? 500 : 200);
|
|
55
|
-
}
|
|
56
|
-
function routeError(code: string, message: string, details?: Array<{ path: string; code: string; message: string }>): Response {
|
|
57
|
-
return response({ apiVersion: "v1", requestId: managedAnalyticsOpaqueId("req"), error: { code, message, ...(details?.length ? { details } : {}) } });
|
|
58
|
-
}
|
|
59
|
-
function operationResponse(operation: ManagedAnalyticsOperation, body: unknown): Response {
|
|
60
|
-
const safe = sanitizeManagedAnalyticsOutbound(operation, body);
|
|
61
|
-
return safe === null ? routeError("internal_error", "The integration service could not complete the request.") : response(safe);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export async function POST(context: APIContext): Promise<Response> {
|
|
65
|
-
try {
|
|
66
|
-
const operation = context.params.operation;
|
|
67
|
-
if (!operation || !Object.prototype.hasOwnProperty.call(VALIDATORS, operation)) {
|
|
68
|
-
return routeError("invalid_request", "Unknown managed analytics v1 operation.");
|
|
69
|
-
}
|
|
70
|
-
const op = operation as ManagedAnalyticsOperation;
|
|
71
|
-
const url = new URL(context.request.url);
|
|
72
|
-
const auth = await resolveOperatorSession(context, url.searchParams.get("tenant"));
|
|
73
|
-
if (!auth.ok) return routeError(auth.status === 401 ? "unauthorized" : "forbidden", "The request is not authorized.");
|
|
74
|
-
if (!MANAGED_ANALYTICS_READ_OPERATIONS.has(op) && requireManageRole(auth)) {
|
|
75
|
-
return routeError("forbidden", "The request is not authorized.");
|
|
76
|
-
}
|
|
77
|
-
let raw: unknown;
|
|
78
|
-
try { raw = await context.request.json(); }
|
|
79
|
-
catch { return routeError("invalid_request", "The request body must be valid JSON."); }
|
|
80
|
-
const parsed = VALIDATORS[operation]!(raw);
|
|
81
|
-
if (!parsed.ok) {
|
|
82
|
-
const unsupportedVersion = parsed.violations.some(({ path, code }) => path === "apiVersion" && code === "unsupported_value");
|
|
83
|
-
return routeError(unsupportedVersion ? "unsupported_version" : "invalid_request", unsupportedVersion ? "The API version is not supported." : "The request failed validation.", parsed.violations);
|
|
84
|
-
}
|
|
85
|
-
const service = await getManagedAnalyticsService();
|
|
86
|
-
if (service instanceof Response) return routeError("internal_error", "The integration service is unavailable.");
|
|
87
|
-
const ctx = { tenantId: auth.tenantId, actor: auth.actor };
|
|
88
|
-
const request = parsed.value;
|
|
89
|
-
switch (op) {
|
|
90
|
-
case "listAdapterDefinitions": return operationResponse(op, service.listAdapterDefinitions(request as ListAdapterDefinitionsRequest));
|
|
91
|
-
case "getAdapterDefinition": return operationResponse(op, service.getAdapterDefinition((request as GetAdapterDefinitionRequest).query.adapterId));
|
|
92
|
-
case "listIntegrationInstalls": return operationResponse(op, await service.listIntegrationInstalls(ctx, request as ListIntegrationInstallsRequest));
|
|
93
|
-
case "getIntegrationInstall": return operationResponse(op, await service.getIntegrationInstall(ctx, (request as GetIntegrationInstallRequest).query.installId));
|
|
94
|
-
case "listIntegrationRevisions": { const value = request as ListIntegrationRevisionsRequest; return operationResponse(op, await service.listIntegrationRevisions(ctx, value.query.installId, value.query)); }
|
|
95
|
-
case "getIntegrationRevision": { const value = request as GetIntegrationRevisionRequest; return operationResponse(op, await service.getIntegrationRevision(ctx, value.query.installId, value.query.revisionId)); }
|
|
96
|
-
case "getMigrationDraft": return operationResponse(op, await service.getMigrationDraft(ctx, (request as GetMigrationDraftRequest).query.draftId));
|
|
97
|
-
case "createMigrationDraft": return operationResponse(op, await service.createMigrationDraft(ctx, request as CreateMigrationDraftRequest));
|
|
98
|
-
case "updateMigrationDraft": return operationResponse(op, await service.updateMigrationDraft(ctx, request as UpdateMigrationDraftRequest));
|
|
99
|
-
case "deleteMigrationDraft": return operationResponse(op, await service.deleteMigrationDraft(ctx, request as DeleteMigrationDraftRequest));
|
|
100
|
-
case "createIntegrationInstall": return operationResponse(op, await service.createIntegrationInstall(ctx, request as CreateIntegrationInstallRequest));
|
|
101
|
-
case "reviseIntegrationInstall": return operationResponse(op, await service.reviseIntegrationInstall(ctx, request as ReviseIntegrationInstallRequest));
|
|
102
|
-
case "setIntegrationInstallStatus": return operationResponse(op, await service.setIntegrationInstallStatus(ctx, request as SetIntegrationInstallStatusRequest));
|
|
103
|
-
case "deleteIntegrationInstall": return operationResponse(op, await service.deleteIntegrationInstall(ctx, request as DeleteIntegrationInstallRequest));
|
|
104
|
-
}
|
|
105
|
-
} catch {
|
|
106
|
-
return routeError("internal_error", "The integration service could not complete the request.");
|
|
107
|
-
}
|
|
108
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PUT/HEAD /api/publish/stage-bundle?key=versions/<digest>/<rel> — the
|
|
3
|
-
* authenticated Worker staging surface that writes bundle objects into the
|
|
4
|
-
* `STATIC_BUNDLES` R2 binding, so CI stages digests WITHOUT a static R2
|
|
5
|
-
* S3-compatible key pair. Auth (a CI `private_key_jwt` assertion, RFC 7523/8707,
|
|
6
|
-
* one-time `jti`) + the write logic live in `@/lib/publish/stageBundleUpload`;
|
|
7
|
-
* this route only resolves the runtime bindings/config.
|
|
8
|
-
*
|
|
9
|
-
* Fails CLOSED (503 "not configured") until a human wires the CI client's public
|
|
10
|
-
* JWKS (`STATIC_BUNDLE_UPLOAD_JWKS`) + the endpoint's own audience
|
|
11
|
-
* (`STATIC_BUNDLE_UPLOAD_AUDIENCE`) — the same "needs provisioning" gate the R2
|
|
12
|
-
* buckets themselves sit behind. It is EXEMPT from the demo login gate
|
|
13
|
-
* (loginGate.ts `isStaticBundleUploadPath`): it carries its own, stricter auth,
|
|
14
|
-
* so it must answer server-to-server rather than being masked by a "Coming soon"
|
|
15
|
-
* holding page.
|
|
16
|
-
*/
|
|
17
|
-
import type { APIContext } from "astro";
|
|
18
|
-
import { readEnv, readKv, readR2 } from "@/lib/env";
|
|
19
|
-
import {
|
|
20
|
-
handleStageBundleUpload,
|
|
21
|
-
kvJtiReplayStore,
|
|
22
|
-
DEFAULT_STAGE_BUNDLE_UPLOAD_ISSUER,
|
|
23
|
-
type StageBundleUploadBucket,
|
|
24
|
-
type StageBundleUploadConfig,
|
|
25
|
-
type JtiKvLike,
|
|
26
|
-
} from "@/lib/publish/stageBundleUpload";
|
|
27
|
-
|
|
28
|
-
export const prerender = false;
|
|
29
|
-
|
|
30
|
-
async function handle(context: APIContext): Promise<Response> {
|
|
31
|
-
const bucket = (await readR2("STATIC_BUNDLES")) as StageBundleUploadBucket | undefined;
|
|
32
|
-
if (!bucket) {
|
|
33
|
-
return new Response(JSON.stringify({ error: "no_bundle_store" }), {
|
|
34
|
-
status: 503,
|
|
35
|
-
headers: { "content-type": "application/json" },
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const jwksRaw = await readEnv("STATIC_BUNDLE_UPLOAD_JWKS");
|
|
40
|
-
const audience = await readEnv("STATIC_BUNDLE_UPLOAD_AUDIENCE");
|
|
41
|
-
if (!jwksRaw || !audience) {
|
|
42
|
-
return new Response(JSON.stringify({ error: "upload_not_configured" }), {
|
|
43
|
-
status: 503,
|
|
44
|
-
headers: { "content-type": "application/json" },
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
let jwks: StageBundleUploadConfig["jwks"];
|
|
48
|
-
try {
|
|
49
|
-
jwks = JSON.parse(jwksRaw) as StageBundleUploadConfig["jwks"];
|
|
50
|
-
} catch {
|
|
51
|
-
return new Response(JSON.stringify({ error: "upload_not_configured" }), {
|
|
52
|
-
status: 503,
|
|
53
|
-
headers: { "content-type": "application/json" },
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
const issuer =
|
|
57
|
-
(await readEnv("STATIC_BUNDLE_UPLOAD_ISSUER")) ?? DEFAULT_STAGE_BUNDLE_UPLOAD_ISSUER;
|
|
58
|
-
|
|
59
|
-
const kv = (await readKv("TENANT_CACHE")) as JtiKvLike | undefined;
|
|
60
|
-
|
|
61
|
-
return handleStageBundleUpload({
|
|
62
|
-
request: context.request,
|
|
63
|
-
bucket,
|
|
64
|
-
config: { jwks, issuer, audience },
|
|
65
|
-
...(kv ? { jtiStore: kvJtiReplayStore(kv) } : {}),
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export const PUT = handle;
|
|
70
|
-
export const HEAD = handle;
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* POST /api/test/dev-session — E2E-ONLY hands-off sign-in for automated tests.
|
|
3
|
-
*
|
|
4
|
-
* WHY THIS EXISTS
|
|
5
|
-
* ---------------
|
|
6
|
-
* The hosted /cockpit `hostedActivity` flow can only be driven by a signed-in,
|
|
7
|
-
* store-LINKED developer session, and the real sign-in paths (`/api/auth/verify`
|
|
8
|
-
* OTP code, `/auth/magic` token) deliver their secret ONLY by email — so an
|
|
9
|
-
* automated regression test can never bootstrap itself without a human relaying a
|
|
10
|
-
* code/link out of an inbox. This endpoint mints that session directly for a test
|
|
11
|
-
* developer + tenant so `e2e/cockpit-signal-rail.spec.ts` can run unattended.
|
|
12
|
-
*
|
|
13
|
-
* IT DOES NOT WEAKEN THE REAL AUTH FLOW. It is a SEPARATE, secret-gated door used
|
|
14
|
-
* only as a test PRECONDITION — auth-flow fidelity (does invite→email→magic→session
|
|
15
|
-
* actually work?) is a different concern and must still be tested against the real
|
|
16
|
-
* path with a real/test inbox. See e2e/README.md §"Why a mint endpoint, not a flag".
|
|
17
|
-
*
|
|
18
|
-
* SAFETY — fail CLOSED, three independent gates (a env-detection bug alone can't open it):
|
|
19
|
-
* 1. `E2E_TEST_SECRET` must be provisioned. Unset → 404 (route behaves as if it
|
|
20
|
-
* does not exist). PROVISION IT ONLY IN THE TEST/PREVIEW TIER, NEVER PROD.
|
|
21
|
-
* 2. `NODE_ENV === "production"` → 404 regardless of anything else (defense in depth;
|
|
22
|
-
* preview/qa run NODE_ENV=test, so this permits them and blocks prod).
|
|
23
|
-
* 3. The `x-e2e-secret` header must match `E2E_TEST_SECRET` (constant-time compare).
|
|
24
|
-
* Blast radius is further bounded: the tenant must resolve via resolvePreviewTenant
|
|
25
|
-
* (a known store), and the minted session/token are the ordinary revocable KV
|
|
26
|
-
* artifacts (deleting the KV entry revokes them) — nothing new or long-lived.
|
|
27
|
-
*
|
|
28
|
-
* This mirrors the existing shared-secret seam the storefront already uses for
|
|
29
|
-
* cli-signin-code (lib/dev/cliSignInCode.ts → MCP `MCP_SERVICE_SHARED_SECRET`): a
|
|
30
|
-
* capability-gated internal door, not an ambient env-mode branch.
|
|
31
|
-
*/
|
|
32
|
-
import type { APIContext } from "astro";
|
|
33
|
-
import { getSessionStore, setSessionCookie } from "@/lib/auth/route";
|
|
34
|
-
import { createSession, DEFAULT_SESSION_TTL_SECONDS } from "@/lib/auth/session";
|
|
35
|
-
import { readEnv, readKv } from "@/lib/env";
|
|
36
|
-
import { mintActivityIngestToken } from "@/lib/dev/activityIngestToken";
|
|
37
|
-
import { resolvePreviewTenant } from "@/config/previewTenantLookup";
|
|
38
|
-
|
|
39
|
-
export const prerender = false;
|
|
40
|
-
|
|
41
|
-
function json(body: unknown, status = 200): Response {
|
|
42
|
-
return new Response(JSON.stringify(body), {
|
|
43
|
-
status,
|
|
44
|
-
headers: { "content-type": "application/json", "cache-control": "no-store" },
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Length-independent constant-time string compare (avoids leaking the secret by timing). */
|
|
49
|
-
function timingSafeEqual(a: string, b: string): boolean {
|
|
50
|
-
// XOR the SHA-equalised inputs isn't necessary here; compare fixed-work over the
|
|
51
|
-
// longer length so mismatched lengths still take constant time relative to input.
|
|
52
|
-
const len = Math.max(a.length, b.length);
|
|
53
|
-
let diff = a.length ^ b.length;
|
|
54
|
-
for (let i = 0; i < len; i++) {
|
|
55
|
-
diff |= (a.charCodeAt(i) || 0) ^ (b.charCodeAt(i) || 0);
|
|
56
|
-
}
|
|
57
|
-
return diff === 0;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export async function POST(context: APIContext): Promise<Response> {
|
|
61
|
-
// GATE 1 — secret must be provisioned; absent ⇒ pretend the route doesn't exist.
|
|
62
|
-
const secret = await readEnv("E2E_TEST_SECRET");
|
|
63
|
-
if (!secret) return json({ error: "not found" }, 404);
|
|
64
|
-
|
|
65
|
-
// GATE 2 — never in the production tier, even if a secret leaked into it.
|
|
66
|
-
const nodeEnv = ((await readEnv("NODE_ENV")) ?? "").toLowerCase();
|
|
67
|
-
if (nodeEnv === "production") return json({ error: "not found" }, 404);
|
|
68
|
-
|
|
69
|
-
// GATE 3 — present the secret.
|
|
70
|
-
const presented = context.request.headers.get("x-e2e-secret") ?? "";
|
|
71
|
-
if (!timingSafeEqual(presented, secret)) return json({ error: "forbidden" }, 403);
|
|
72
|
-
|
|
73
|
-
let body: { email?: unknown; tenant?: unknown };
|
|
74
|
-
try {
|
|
75
|
-
body = (await context.request.json()) as typeof body;
|
|
76
|
-
} catch {
|
|
77
|
-
return json({ error: "invalid JSON body" }, 400);
|
|
78
|
-
}
|
|
79
|
-
const email = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
|
|
80
|
-
const tenantId = typeof body.tenant === "string" ? body.tenant.trim().toLowerCase() : "";
|
|
81
|
-
if (!email || !tenantId) return json({ error: "email and tenant are required" }, 400);
|
|
82
|
-
|
|
83
|
-
// Bound to a KNOWN store — never mints against an arbitrary/unknown tenant.
|
|
84
|
-
const tenant = await resolvePreviewTenant(tenantId);
|
|
85
|
-
if (!tenant) return json({ error: "unknown tenant" }, 404);
|
|
86
|
-
|
|
87
|
-
const store = await getSessionStore();
|
|
88
|
-
if (!store) return json({ error: "session store unavailable" }, 503);
|
|
89
|
-
|
|
90
|
-
// Mint a store-LINKED developer session (tenants[0].resource is what the
|
|
91
|
-
// /api/dev/activity poll resolves — see api/dev/activity.ts GET).
|
|
92
|
-
const now = Math.floor(Date.now() / 1000);
|
|
93
|
-
const resource = tenant.appDomain;
|
|
94
|
-
const { id } = await createSession(
|
|
95
|
-
store,
|
|
96
|
-
{
|
|
97
|
-
email,
|
|
98
|
-
resource,
|
|
99
|
-
roles: ["developer"],
|
|
100
|
-
capability: "developer",
|
|
101
|
-
tenants: [{ resource, role: "developer" }],
|
|
102
|
-
staff: [],
|
|
103
|
-
},
|
|
104
|
-
DEFAULT_SESSION_TTL_SECONDS,
|
|
105
|
-
now,
|
|
106
|
-
);
|
|
107
|
-
setSessionCookie(context, id, DEFAULT_SESSION_TTL_SECONDS);
|
|
108
|
-
|
|
109
|
-
// Also hand back an activity-ingest token so the test can POST heartbeat / save /
|
|
110
|
-
// obstacle events the way the local `tot dev` loop would (bearer-authed, not cookie).
|
|
111
|
-
const kv = await readKv("SESSION");
|
|
112
|
-
let activityToken: string | null = null;
|
|
113
|
-
let activityExpiresAt: string | null = null;
|
|
114
|
-
if (kv) {
|
|
115
|
-
const minted = await mintActivityIngestToken(kv, {
|
|
116
|
-
email,
|
|
117
|
-
tenant: tenant.tenant_id,
|
|
118
|
-
capability: "developer",
|
|
119
|
-
});
|
|
120
|
-
activityToken = minted.token;
|
|
121
|
-
activityExpiresAt = minted.expiresAt;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return json({
|
|
125
|
-
ok: true,
|
|
126
|
-
email,
|
|
127
|
-
tenant: tenant.tenant_id,
|
|
128
|
-
resource,
|
|
129
|
-
activityToken,
|
|
130
|
-
activityExpiresAt,
|
|
131
|
-
activityUrl: new URL(context.request.url).origin,
|
|
132
|
-
});
|
|
133
|
-
}
|