@tokenoftrust/storefront-runner 1.3.4-rc.4 → 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/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 +433 -69
- 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,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RS256 signing-key loader for outbound webhook delivery (PrivateApps epic,
|
|
3
|
+
* D4 Chunk B) — the key `signing.ts` uses to attach HTTP Message Signatures
|
|
4
|
+
* (RFC 9421) to every CloudEvents webhook POST.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors `../apps/gatewayKeys.ts`'s `loadGatewaySigningConfig`/
|
|
7
|
+
* `loadGatewayJwks` structure EXACTLY (import cache keyed on the base64
|
|
8
|
+
* source, JWKS derivation that strips every private RSA field), but over a
|
|
9
|
+
* SEPARATE key (`WEBHOOK_SIGNING_PRIVATE_KEY_B64`) with a separate kid
|
|
10
|
+
* (`webhook-signing-1`) — per `d4-design-resolutions`, this key must never be
|
|
11
|
+
* the same as the D2 gateway key. The gateway key attests requests an app
|
|
12
|
+
* makes INTO the storefront (app → gateway trust); the webhook signing key
|
|
13
|
+
* attests events the storefront pushes OUT to an app (gateway → app trust,
|
|
14
|
+
* the opposite direction). Sharing a key would let a receiver's verification
|
|
15
|
+
* key double as the ability to (offline) verify gateway tokens, and rotating
|
|
16
|
+
* one would force rotating the other.
|
|
17
|
+
*
|
|
18
|
+
* The private key never leaves this module. `loadWebhookJwks` derives the
|
|
19
|
+
* PUBLIC-only JWKS from it, published (Chunk B) via a new field on
|
|
20
|
+
* `/.well-known/tot-app-authorization-metadata`.
|
|
21
|
+
*/
|
|
22
|
+
import { importPKCS8, exportJWK, type CryptoKey, type JWK, type JSONWebKeySet } from "jose";
|
|
23
|
+
import { readEnv } from "@/lib/env";
|
|
24
|
+
// Re-exported (not re-defined) — `@tokenoftrust/private-apps-devkit`'s
|
|
25
|
+
// signing.ts is the ONE source of truth for this constant since D6 Chunk B
|
|
26
|
+
// (it's the `alg` its sign/verify functions produce/accept); this module
|
|
27
|
+
// just keeps the historical import path (`./webhookSigningKey.js`) working.
|
|
28
|
+
export { WEBHOOK_SIGNING_HTTPSIG_ALG } from "@tokenoftrust/private-apps-devkit";
|
|
29
|
+
|
|
30
|
+
/** The only algorithm the webhook signer signs with (verifiers reject anything else). */
|
|
31
|
+
export const WEBHOOK_SIGNING_ALG = "RS256";
|
|
32
|
+
|
|
33
|
+
/** Default key id (overridable via `WEBHOOK_SIGNING_KID`). Names the published key. */
|
|
34
|
+
export const DEFAULT_WEBHOOK_SIGNING_KID = "webhook-signing-1";
|
|
35
|
+
|
|
36
|
+
/** Env keys the loader reads (server-only). */
|
|
37
|
+
export const WEBHOOK_SIGNING_PRIVATE_KEY_ENV = "WEBHOOK_SIGNING_PRIVATE_KEY_B64";
|
|
38
|
+
export const WEBHOOK_SIGNING_KID_ENV = "WEBHOOK_SIGNING_KID";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Thrown when the webhook signer can't load its signing key — the private
|
|
42
|
+
* key env is absent/malformed. Callers FAIL CLOSED: with no key, no delivery
|
|
43
|
+
* can be signed, and the metadata route omits the webhook JWKS field rather
|
|
44
|
+
* than publish a key that will never actually sign anything.
|
|
45
|
+
*/
|
|
46
|
+
export class WebhookSigningKeyError extends Error {}
|
|
47
|
+
|
|
48
|
+
/** Optional test/DI overrides (avoids touching process env in unit tests). */
|
|
49
|
+
export interface WebhookSigningKeyOverrides {
|
|
50
|
+
/** base64 of the PKCS8 PEM private key (defaults to the env value). */
|
|
51
|
+
privateKeyB64?: string;
|
|
52
|
+
kid?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface WebhookSigningConfig {
|
|
56
|
+
key: CryptoKey;
|
|
57
|
+
kid: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Same cache shape as gatewayKeys.ts's signingKeyCache/publicJwkCache, keyed
|
|
61
|
+
// on the base64 string so a rotated key transparently re-imports/re-derives.
|
|
62
|
+
const signingKeyCache = new Map<string, CryptoKey>();
|
|
63
|
+
const publicJwkCache = new Map<string, JWK>();
|
|
64
|
+
|
|
65
|
+
async function importPrivateKey(privateKeyB64: string): Promise<CryptoKey> {
|
|
66
|
+
const cached = signingKeyCache.get(privateKeyB64);
|
|
67
|
+
if (cached) return cached;
|
|
68
|
+
let pem: string;
|
|
69
|
+
try {
|
|
70
|
+
pem = Buffer.from(privateKeyB64, "base64").toString("utf8");
|
|
71
|
+
} catch (cause) {
|
|
72
|
+
throw new WebhookSigningKeyError(
|
|
73
|
+
`webhook signing: ${WEBHOOK_SIGNING_PRIVATE_KEY_ENV} is not valid base64 ` +
|
|
74
|
+
(cause instanceof Error ? cause.message : String(cause)),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (!/-----BEGIN (RSA )?PRIVATE KEY-----/.test(pem)) {
|
|
78
|
+
throw new WebhookSigningKeyError(
|
|
79
|
+
`webhook signing: decoded ${WEBHOOK_SIGNING_PRIVATE_KEY_ENV} is not a PEM private key`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
let key: CryptoKey;
|
|
83
|
+
try {
|
|
84
|
+
// extractable: true — required so `loadWebhookJwks` below can derive the
|
|
85
|
+
// public JWK from this same key, and so `signing.ts` can sign raw bytes
|
|
86
|
+
// with `crypto.subtle.sign` directly (no JWS wrapping needed for RFC
|
|
87
|
+
// 9421 — the signature is over the derived signature base, not a JWT).
|
|
88
|
+
key = (await importPKCS8(pem, WEBHOOK_SIGNING_ALG, { extractable: true })) as CryptoKey;
|
|
89
|
+
} catch (cause) {
|
|
90
|
+
throw new WebhookSigningKeyError(
|
|
91
|
+
`webhook signing: failed to import the RS256 private key ` +
|
|
92
|
+
(cause instanceof Error ? cause.message : String(cause)),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
signingKeyCache.set(privateKeyB64, key);
|
|
96
|
+
return key;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Resolve the active signing config (key + kid) from env, or `overrides` in
|
|
101
|
+
* tests. FAILS CLOSED: throws {@link WebhookSigningKeyError} when the private
|
|
102
|
+
* key env is absent/malformed.
|
|
103
|
+
*/
|
|
104
|
+
export async function loadWebhookSigningConfig(
|
|
105
|
+
overrides: WebhookSigningKeyOverrides = {},
|
|
106
|
+
): Promise<WebhookSigningConfig> {
|
|
107
|
+
const privateKeyB64 =
|
|
108
|
+
overrides.privateKeyB64 ?? (await readEnv(WEBHOOK_SIGNING_PRIVATE_KEY_ENV));
|
|
109
|
+
if (!privateKeyB64) {
|
|
110
|
+
throw new WebhookSigningKeyError(
|
|
111
|
+
`webhook signing: ${WEBHOOK_SIGNING_PRIVATE_KEY_ENV} is not configured (fail closed)`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
const kid = overrides.kid ?? (await readEnv(WEBHOOK_SIGNING_KID_ENV)) ?? DEFAULT_WEBHOOK_SIGNING_KID;
|
|
115
|
+
const key = await importPrivateKey(privateKeyB64);
|
|
116
|
+
return { key, kid };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Derive the PUBLIC JWKS for the active webhook signing key — sanitized
|
|
121
|
+
* (every private RSA field stripped) and cached by the source private-key
|
|
122
|
+
* base64. V1 always returns exactly one key; the envelope supports more
|
|
123
|
+
* without a shape change later (rotation-with-overlap).
|
|
124
|
+
*/
|
|
125
|
+
export async function loadWebhookJwks(
|
|
126
|
+
overrides: WebhookSigningKeyOverrides = {},
|
|
127
|
+
): Promise<JSONWebKeySet> {
|
|
128
|
+
const privateKeyB64 =
|
|
129
|
+
overrides.privateKeyB64 ?? (await readEnv(WEBHOOK_SIGNING_PRIVATE_KEY_ENV));
|
|
130
|
+
if (!privateKeyB64) {
|
|
131
|
+
throw new WebhookSigningKeyError(
|
|
132
|
+
`webhook signing: ${WEBHOOK_SIGNING_PRIVATE_KEY_ENV} is not configured (fail closed)`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
const { key, kid } = await loadWebhookSigningConfig(overrides);
|
|
136
|
+
let jwk = publicJwkCache.get(privateKeyB64);
|
|
137
|
+
if (!jwk) {
|
|
138
|
+
const full = (await exportJWK(key)) as Record<string, unknown>;
|
|
139
|
+
// Strip every private RSA field (RFC 7518 §6.3.2) — only the public parts
|
|
140
|
+
// (kty, n, e) are ever published. This line is the one place that matters.
|
|
141
|
+
const { d: _d, p: _p, q: _q, dp: _dp, dq: _dq, qi: _qi, oth: _oth, ...publicOnly } = full;
|
|
142
|
+
jwk = { ...publicOnly, kid, alg: WEBHOOK_SIGNING_ALG, use: "sig" };
|
|
143
|
+
publicJwkCache.set(privateKeyB64, jwk);
|
|
144
|
+
}
|
|
145
|
+
return { keys: [jwk] };
|
|
146
|
+
}
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
buildCsp,
|
|
31
31
|
STOREFRONT_CSP_ALLOWLIST,
|
|
32
32
|
checkoutCspAdditions,
|
|
33
|
+
appWidgetCspAdditions,
|
|
33
34
|
CSP_REPORT_GROUP,
|
|
34
35
|
CSP_REPORT_PATH,
|
|
35
36
|
readPublishedArtifact,
|
|
@@ -46,13 +47,13 @@ import {
|
|
|
46
47
|
parseGateMode,
|
|
47
48
|
isAuthPath,
|
|
48
49
|
sessionAdmitsHost,
|
|
50
|
+
sessionHostCapability,
|
|
49
51
|
PUBLIC_HOME_TENANT_ID,
|
|
50
52
|
} from "@/lib/auth/loginGate";
|
|
51
53
|
import { gatedHoldingResponse } from "@/lib/auth/gatePage";
|
|
52
54
|
import { readSession, KvSessionStore, SESSION_COOKIE } from "@/lib/auth/session";
|
|
53
55
|
import { isMaintenanceOn, maintenanceResponse } from "@/lib/maintenance";
|
|
54
56
|
import { HEALTH_PATH, healthResponse } from "@/lib/health";
|
|
55
|
-
import { membershipFor } from "@/lib/dashboard/tenantSelection";
|
|
56
57
|
import type { DeepPartial, ThemeTokens } from "@tot/public-runtime";
|
|
57
58
|
|
|
58
59
|
export const onRequest = defineMiddleware(async (context, next) => {
|
|
@@ -160,17 +161,18 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
160
161
|
return gatedHoldingResponse(decision.location);
|
|
161
162
|
}
|
|
162
163
|
if (admitted && viewerRecord) {
|
|
163
|
-
// Surface the viewer's
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
164
|
+
// Surface the viewer's standing FOR THIS host's tenant as chrome, BOUND to
|
|
165
|
+
// the host so a capability the session holds for another tenant never leaks
|
|
166
|
+
// in: a member sees their tenants[] role; the owner of their own tenant sees
|
|
167
|
+
// the resource-scoped session's capability; a ToT-staff viewer admitted via
|
|
168
|
+
// an explicit vendor selection sees the staffRoles-scoped capability. The
|
|
169
|
+
// ship gates (u10 decideIsOwner/resolveShipPrincipal) read this capability,
|
|
170
|
+
// so binding it here keeps owner-resolution server-side + tenant-correct.
|
|
171
|
+
const hostCapability = sessionHostCapability(viewerRecord, tenant.appDomain, nowSeconds);
|
|
170
172
|
locals.viewer = {
|
|
171
173
|
email: viewerRecord.email,
|
|
172
|
-
roles:
|
|
173
|
-
capability:
|
|
174
|
+
roles: hostCapability ? [hostCapability] : viewerRecord.roles,
|
|
175
|
+
capability: hostCapability ?? viewerRecord.capability,
|
|
174
176
|
};
|
|
175
177
|
}
|
|
176
178
|
}
|
|
@@ -210,8 +212,14 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
210
212
|
if (hit) return hit;
|
|
211
213
|
}
|
|
212
214
|
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
+
// Catalog source selection (F17 graduation path — docs/architecture/product-catalog.md).
|
|
216
|
+
// A store's product source graduates over its life: JSON fixtures (dev default) ->
|
|
217
|
+
// D1 edge replica (staging) -> hosted ToT catalog (go-live). Here we pick the read
|
|
218
|
+
// backend: USE_D1_CATALOG=1 (+ a CATALOG_DB binding) reads the D1 edge replica;
|
|
219
|
+
// otherwise createToTClient falls back to the bundled JSON fixture snapshot. The
|
|
220
|
+
// hosted catalog graduates the SAME canonical records into the D1 read model
|
|
221
|
+
// (emitCatalogSql), so JSON/D1/hosted render identically — pinned by
|
|
222
|
+
// lib/tot/sourceSelection.test.ts.
|
|
215
223
|
const d1 =
|
|
216
224
|
(await readEnv("USE_D1_CATALOG")) === "1" ? await readD1("CATALOG_DB") : undefined;
|
|
217
225
|
const tot = await createToTClient(tenant, d1);
|
|
@@ -360,6 +368,10 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
360
368
|
locals.canonicalBase = canonicalBase;
|
|
361
369
|
locals.customizationEnv = customization.env;
|
|
362
370
|
locals.cspNonce = cspNonce;
|
|
371
|
+
// Seeded here, filled in by whichever page/layout renders a widget slot
|
|
372
|
+
// (renderSlot.ts) as the page renders inside next() below — read back out
|
|
373
|
+
// after next() resolves so the CSP frame-src reflects exactly what rendered.
|
|
374
|
+
locals.activeWidgetPlacements = [];
|
|
363
375
|
// Prefix that path-routed pages live under ("" for host-routed). Pages stay
|
|
364
376
|
// tenant-agnostic; the Layout uses this to keep root-absolute links inside
|
|
365
377
|
// the tenant prefix on the client.
|
|
@@ -427,6 +439,9 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
427
439
|
// strict policy only when this tenant has a store — so add-to-cart, the
|
|
428
440
|
// sidecart iframe/XHR, and the cross-origin form POST all pass.
|
|
429
441
|
const checkoutCsp = commerce ? checkoutCspAdditions(commerce.origin) : null;
|
|
442
|
+
// Private-app widget origins (D5) — read back AFTER next() so this reflects
|
|
443
|
+
// exactly the placements that rendered on THIS page, never a wildcard.
|
|
444
|
+
const widgetCsp = appWidgetCspAdditions(locals.activeWidgetPlacements ?? []);
|
|
430
445
|
response.headers.set(
|
|
431
446
|
cspHeader,
|
|
432
447
|
buildCsp({
|
|
@@ -450,7 +465,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
450
465
|
...(checkoutCsp?.connectSrc ?? []),
|
|
451
466
|
],
|
|
452
467
|
imgSrc: [...STOREFRONT_CSP_ALLOWLIST.imgSrc, ...(checkoutCsp?.imgSrc ?? [])],
|
|
453
|
-
frameSrc: checkoutCsp?.frameSrc ?? [],
|
|
468
|
+
frameSrc: [...(checkoutCsp?.frameSrc ?? []), ...widgetCsp.frameSrc],
|
|
454
469
|
formAction: checkoutCsp?.formAction ?? [],
|
|
455
470
|
reportGroup: CSP_REPORT_GROUP,
|
|
456
471
|
reportUri: CSP_REPORT_PATH,
|
|
@@ -2,15 +2,25 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* 404 — written in the Northwind voice, not a stub. Offers a way home and a
|
|
4
4
|
* few collections to wander into so a dead link still lands somewhere useful.
|
|
5
|
+
*
|
|
6
|
+
* DELIBERATELY on-demand — do NOT prerender (audit finding "selective-prerender").
|
|
7
|
+
* Fully tenant-varying: it reads Astro.locals.tenant + tot.listCollections() and
|
|
8
|
+
* renders under locals.themeStyle, all set per-request by the middleware from the
|
|
9
|
+
* resolved Host. Prerendering would freeze a single build-time tenant AND strip the
|
|
10
|
+
* middleware-applied CSP + security headers (no static _headers fallback exists).
|
|
5
11
|
*/
|
|
12
|
+
export const prerender = false;
|
|
13
|
+
|
|
6
14
|
import Layout from "@/layouts/Layout.astro";
|
|
7
15
|
import CollectionCard from "@/components/CollectionCard.astro";
|
|
8
16
|
import { withBase } from "@/lib/basePath";
|
|
9
17
|
|
|
10
|
-
const { tenant, tot, basePath } = Astro.locals;
|
|
18
|
+
const { tenant, tot, basePath, features } = Astro.locals;
|
|
11
19
|
|
|
12
|
-
// A handful of collections to redirect lost visitors toward.
|
|
13
|
-
|
|
20
|
+
// A handful of collections to redirect lost visitors toward. Marketing tenants
|
|
21
|
+
// (features.catalog === false) have no /collections route at all — skip the
|
|
22
|
+
// fetch entirely rather than trust it to come back empty.
|
|
23
|
+
const collections = features.catalog ? (await tot.listCollections()).slice(0, 3) : [];
|
|
14
24
|
---
|
|
15
25
|
|
|
16
26
|
<Layout
|
|
@@ -36,12 +46,14 @@ const collections = (await tot.listCollections()).slice(0, 3);
|
|
|
36
46
|
>
|
|
37
47
|
Back home
|
|
38
48
|
</a>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
{features.catalog && (
|
|
50
|
+
<a
|
|
51
|
+
href={withBase(basePath, "/collections")}
|
|
52
|
+
class="inline-flex items-center rounded-full border border-[var(--color-border)] px-6 py-3 font-display text-sm font-medium text-[var(--color-text)] transition-colors hover:border-[var(--color-primary)]"
|
|
53
|
+
>
|
|
54
|
+
Browse collections
|
|
55
|
+
</a>
|
|
56
|
+
)}
|
|
45
57
|
</div>
|
|
46
58
|
</div>
|
|
47
59
|
</section>
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import Layout from "@/layouts/Layout.astro";
|
|
9
9
|
import Breadcrumbs from "@/components/Breadcrumbs.astro";
|
|
10
|
+
import ProseSections from "@/components/content/ProseSections.astro";
|
|
11
|
+
import ProseToc from "@/components/content/ProseToc.astro";
|
|
10
12
|
import { breadcrumbLd } from "@/lib/jsonld";
|
|
11
13
|
import { truncate } from "@/lib/format";
|
|
12
14
|
import { rawHtmlPolicy, rawHtmlResponse } from "@/lib/rawMarketingHtml";
|
|
@@ -56,6 +58,27 @@ const crumbs = [
|
|
|
56
58
|
const description =
|
|
57
59
|
page.seoDescription ||
|
|
58
60
|
(page.intro ? truncate(page.intro, 160) : undefined);
|
|
61
|
+
|
|
62
|
+
// Structured pages (F4) drive the body from `sections`; legacy pages keep the
|
|
63
|
+
// sanitized `body_html`. Prefer structured whenever a page ships sections.
|
|
64
|
+
const structured = (page.sections?.length ?? 0) > 0;
|
|
65
|
+
|
|
66
|
+
// Reviewer-only needs-review flags render in preview/dev only — mirrors the
|
|
67
|
+
// middleware's "production read" definition (no preview host / token / cookie),
|
|
68
|
+
// so a real shopper never sees scaffolding but a reviewer does.
|
|
69
|
+
const isPreview =
|
|
70
|
+
import.meta.env.DEV ||
|
|
71
|
+
Astro.url.hostname.toLowerCase().startsWith("preview.") ||
|
|
72
|
+
Astro.url.searchParams.has("__preview") ||
|
|
73
|
+
Astro.cookies.has("tot_preview");
|
|
74
|
+
|
|
75
|
+
const updatedLabel = page.updated
|
|
76
|
+
? new Date(page.updated).toLocaleDateString("en-US", {
|
|
77
|
+
year: "numeric",
|
|
78
|
+
month: "long",
|
|
79
|
+
day: "numeric",
|
|
80
|
+
})
|
|
81
|
+
: null;
|
|
59
82
|
---
|
|
60
83
|
|
|
61
84
|
<Layout
|
|
@@ -80,10 +103,21 @@ const description =
|
|
|
80
103
|
{page.intro}
|
|
81
104
|
</p>
|
|
82
105
|
)}
|
|
106
|
+
{updatedLabel && (
|
|
107
|
+
<p class="mt-3 text-sm text-[var(--color-muted)]">Last updated {updatedLabel}</p>
|
|
108
|
+
)}
|
|
109
|
+
{structured && <ProseToc sections={page.sections!} />}
|
|
83
110
|
</header>
|
|
84
111
|
|
|
85
|
-
{/* Body — constrained to a readable measure.
|
|
112
|
+
{/* Body — constrained to a readable measure. Structured pages (F4) render
|
|
113
|
+
framework widgets from `sections`; legacy pages keep the sanitized HTML. */}
|
|
86
114
|
<div class="container-prose pb-[clamp(3rem,7vw,6rem)] pt-8">
|
|
87
|
-
|
|
115
|
+
{structured ? (
|
|
116
|
+
<div class="mx-auto max-w-[68ch]">
|
|
117
|
+
<ProseSections sections={page.sections!} includeNeedsReview={isPreview} />
|
|
118
|
+
</div>
|
|
119
|
+
) : (
|
|
120
|
+
<div class="rte mx-auto max-w-[68ch] text-[var(--color-text)]" set:html={page.body_html} />
|
|
121
|
+
)}
|
|
88
122
|
</div>
|
|
89
123
|
</Layout>
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* SHELL CONTRACT — every AdminXTab.astro component mounted below must preserve:
|
|
14
14
|
* - A `<section id="{tab}" data-admin-panel data-agent-scope="{tab}">` root,
|
|
15
15
|
* where {tab} is one of: orders, products, subscriptions, customers, fulfillment,
|
|
16
|
-
* reporting, ai-workflows, settings. The shell's hash router shows/hides
|
|
16
|
+
* reporting, ai-workflows, publish, settings. The shell's hash router shows/hides
|
|
17
17
|
* panels by matching `html[data-admin-tab]` to this id, and the AI Ask
|
|
18
18
|
* envelope reads `data-agent-scope` to label its active context.
|
|
19
19
|
* - No tab-switching chrome of its own — `#{tab}` deep links and the shell's
|
|
@@ -37,9 +37,11 @@ import OrdersWorkspace from "../components/admin/orders/OrdersWorkspace.astro";
|
|
|
37
37
|
import AdminProductsTab from "../components/admin/AdminProductsTab.astro";
|
|
38
38
|
import AdminSubscriptionsTab from "../components/admin/AdminSubscriptionsTab.astro";
|
|
39
39
|
import AdminReportingTab from "../components/admin/AdminReportingTab.astro";
|
|
40
|
+
import AdminPublishTab from "../components/admin/AdminPublishTab.astro";
|
|
40
41
|
import AdminSettingsTab from "../components/admin/AdminSettingsTab.astro";
|
|
41
42
|
import { readViewerSession } from "@/lib/auth/route";
|
|
42
43
|
import { resolveAdminTenantMode } from "@/lib/adminTenantEnvelope";
|
|
44
|
+
import { resolveAdminEntry } from "@/lib/auth/adminEntry";
|
|
43
45
|
|
|
44
46
|
const { tenant, basePath, cspNonce } = Astro.locals;
|
|
45
47
|
const adminPath = `${basePath || ""}/admin`;
|
|
@@ -49,6 +51,19 @@ const viewerEmail = (Astro.locals.viewer?.email ?? viewerSession?.email ?? "").t
|
|
|
49
51
|
const viewerDomain = viewerEmail.split("@").at(-1) ?? "";
|
|
50
52
|
const canViewInternalGuide = viewerDomain === "tokenoftrust.com";
|
|
51
53
|
|
|
54
|
+
// dg6 — admin session ENTRY resolved SERVER-SIDE against the resolved tenant's
|
|
55
|
+
// appDomain (never client input, never a bearer secret): an authenticated
|
|
56
|
+
// owner/team member for THIS tenant reaches the admin context; a non-member /
|
|
57
|
+
// anonymous visitor was already held at the sign-in gate (middleware). Consumes
|
|
58
|
+
// u10's owner + ship-capability model; the go-live tabs (AdminPublishTab) read
|
|
59
|
+
// the same host-bound `Astro.locals.viewer.capability`, so the shell just stamps
|
|
60
|
+
// the authoritative principal for chrome + tests — it does not re-gate.
|
|
61
|
+
const adminEntry = resolveAdminEntry({
|
|
62
|
+
record: viewerSession,
|
|
63
|
+
hostResource: tenant.appDomain,
|
|
64
|
+
nowSeconds: Math.floor(Date.now() / 1000),
|
|
65
|
+
});
|
|
66
|
+
|
|
52
67
|
// Internal admin-app tenant mode (baseline doc §Admin App As Tenant,
|
|
53
68
|
// tenant-envelope-contract): every mounted tab receives an explicit
|
|
54
69
|
// { adminAppTenant, targetTenant } envelope. Only a real ToT-staff session
|
|
@@ -72,6 +87,11 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
72
87
|
data-admin-target-tenant={tenantEnvelope.targetTenant}
|
|
73
88
|
data-admin-operator={tenantEnvelope.operator ?? ""}
|
|
74
89
|
data-admin-is-staff={isStaff ? "true" : "false"}
|
|
90
|
+
data-admin-viewer-authenticated={viewerSession ? "true" : "false"}
|
|
91
|
+
data-admin-entry-principal={adminEntry.principal}
|
|
92
|
+
data-admin-is-owner={adminEntry.isOwnerViewer ? "true" : "false"}
|
|
93
|
+
data-admin-can-ship={adminEntry.shipCapabilities.shipOnBehalf ? "true" : "false"}
|
|
94
|
+
data-admin-can-apex-cutover={adminEntry.shipCapabilities.apexCutover ? "true" : "false"}
|
|
75
95
|
>
|
|
76
96
|
<head>
|
|
77
97
|
<meta charset="utf-8" />
|
|
@@ -80,7 +100,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
80
100
|
<title>{tenantLabel} Admin</title>
|
|
81
101
|
<script is:inline nonce={cspNonce}>
|
|
82
102
|
(() => {
|
|
83
|
-
const adminTabs = ["orders", "products", "subscriptions", "customers", "fulfillment", "reporting", "ai-workflows", "settings"];
|
|
103
|
+
const adminTabs = ["orders", "products", "subscriptions", "customers", "fulfillment", "reporting", "ai-workflows", "publish", "settings"];
|
|
84
104
|
const canViewInternalGuide = document.documentElement.dataset.internalAdminGuide === "true";
|
|
85
105
|
const defaultTab = canViewInternalGuide ? "orders" : "settings";
|
|
86
106
|
const hash = window.location.hash.slice(1);
|
|
@@ -508,6 +528,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
508
528
|
html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="fulfillment"] #fulfillment,
|
|
509
529
|
html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="reporting"] #reporting,
|
|
510
530
|
html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="ai-workflows"] #ai-workflows,
|
|
531
|
+
html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="publish"] #publish,
|
|
511
532
|
html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="settings"] #settings {
|
|
512
533
|
display: grid;
|
|
513
534
|
}
|
|
@@ -1133,6 +1154,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
1133
1154
|
<a href="#fulfillment" data-admin-tab-link><svg><use href="#i-truck"></use></svg><span>Fulfillment</span></a>
|
|
1134
1155
|
<a href="#reporting" data-admin-tab-link><svg><use href="#i-chart"></use></svg><span>Reporting</span></a>
|
|
1135
1156
|
<a href="#ai-workflows" data-admin-tab-link><svg><use href="#i-workflows"></use></svg><span>AI Workflows</span></a>
|
|
1157
|
+
<a href="#publish" data-admin-tab-link><svg><use href="#i-export"></use></svg><span>Publish</span></a>
|
|
1136
1158
|
<a href="#settings" data-admin-tab-link><svg><use href="#i-gear"></use></svg><span>Settings</span></a>
|
|
1137
1159
|
</nav>
|
|
1138
1160
|
|
|
@@ -1159,6 +1181,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
1159
1181
|
<a class="tab" href="#fulfillment" role="tab" aria-controls="fulfillment" aria-selected="false" data-admin-tab-link>Fulfillment</a>
|
|
1160
1182
|
<a class="tab" href="#reporting" role="tab" aria-controls="reporting" aria-selected="false" data-admin-tab-link>Reporting</a>
|
|
1161
1183
|
<a class="tab" href="#ai-workflows" role="tab" aria-controls="ai-workflows" aria-selected="false" data-admin-tab-link>AI Workflows</a>
|
|
1184
|
+
<a class="tab" href="#publish" role="tab" aria-controls="publish" aria-selected="false" data-admin-tab-link>Publish</a>
|
|
1162
1185
|
<a class="tab" href="#settings" role="tab" aria-controls="settings" aria-selected="false" data-admin-tab-link>Settings</a>
|
|
1163
1186
|
</div>
|
|
1164
1187
|
|
|
@@ -1206,6 +1229,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
1206
1229
|
<a id="admin-tab-fulfillment" class="tab" href="#fulfillment" role="tab" aria-controls="fulfillment" aria-selected="false" data-admin-tab-link>Fulfillment</a>
|
|
1207
1230
|
<a id="admin-tab-reporting" class="tab" href="#reporting" role="tab" aria-controls="reporting" aria-selected="false" data-admin-tab-link>Reporting</a>
|
|
1208
1231
|
<a id="admin-tab-ai-workflows" class="tab" href="#ai-workflows" role="tab" aria-controls="ai-workflows" aria-selected="false" data-admin-tab-link>AI Workflows</a>
|
|
1232
|
+
<a id="admin-tab-publish" class="tab" href="#publish" role="tab" aria-controls="publish" aria-selected="false" data-admin-tab-link>Publish</a>
|
|
1209
1233
|
<a id="admin-tab-settings" class="tab" href="#settings" role="tab" aria-controls="settings" aria-selected="false" data-admin-tab-link>Settings</a>
|
|
1210
1234
|
</div>
|
|
1211
1235
|
|
|
@@ -1331,6 +1355,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
1331
1355
|
<AdminFulfillmentTab envelope={tenantEnvelope} />
|
|
1332
1356
|
<AdminReportingTab envelope={tenantEnvelope} />
|
|
1333
1357
|
<AdminAIWorkflowsTab envelope={tenantEnvelope} />
|
|
1358
|
+
<AdminPublishTab envelope={tenantEnvelope} />
|
|
1334
1359
|
<AdminSettingsTab envelope={tenantEnvelope} />
|
|
1335
1360
|
|
|
1336
1361
|
{canViewInternalGuide && (
|
|
@@ -1447,7 +1472,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
1447
1472
|
|
|
1448
1473
|
<script is:inline nonce={cspNonce}>
|
|
1449
1474
|
(() => {
|
|
1450
|
-
const tabIds = ["orders", "products", "subscriptions", "customers", "fulfillment", "reporting", "ai-workflows", "settings"];
|
|
1475
|
+
const tabIds = ["orders", "products", "subscriptions", "customers", "fulfillment", "reporting", "ai-workflows", "publish", "settings"];
|
|
1451
1476
|
const tabSet = new Set(tabIds);
|
|
1452
1477
|
const tabHashPattern = new RegExp(`^#(${tabIds.join("|")})$`);
|
|
1453
1478
|
const canViewInternalGuide = document.documentElement.dataset.internalAdminGuide === "true";
|
|
@@ -1460,6 +1485,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
1460
1485
|
fulfillment: "Fulfillment",
|
|
1461
1486
|
reporting: "Reporting",
|
|
1462
1487
|
"ai-workflows": "AI Workflows",
|
|
1488
|
+
publish: "Publish",
|
|
1463
1489
|
settings: "Settings",
|
|
1464
1490
|
};
|
|
1465
1491
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/apps/admin/credentials/rotate — rotate an install's client
|
|
3
|
+
* secret, keeping the same `client_id` (PrivateApps epic, D2 Chunk C; D8
|
|
4
|
+
* console-session auth added). The new secret is returned ONCE here and never
|
|
5
|
+
* again — the same one-time-secret guarantee `install()` gives (see
|
|
6
|
+
* `registryService.ts`'s `InstallResult` doc).
|
|
7
|
+
*
|
|
8
|
+
* `registryService.rotateCredentials()` takes a bare `installId` with no
|
|
9
|
+
* tenant check of its own (Chunk A assumption #1) — so this route resolves
|
|
10
|
+
* the install via `requireOwnedInstall` BEFORE calling it, rejecting a
|
|
11
|
+
* foreign-tenant `installId` with the SAME 404 a truly-missing one gets (no
|
|
12
|
+
* cross-tenant existence oracle). Mutating: `requireManageRole` gates a
|
|
13
|
+
* console `developer` role out — the host-bound/Bearer path is exempt (no
|
|
14
|
+
* `role`).
|
|
15
|
+
*/
|
|
16
|
+
import type { APIContext } from "astro";
|
|
17
|
+
import { resolveOperatorSession } from "@/lib/apps/adminSession.js";
|
|
18
|
+
import {
|
|
19
|
+
getRegistryService,
|
|
20
|
+
registryErrorResponse,
|
|
21
|
+
requireOwnedInstall,
|
|
22
|
+
requireManageRole,
|
|
23
|
+
parseInstallId,
|
|
24
|
+
json,
|
|
25
|
+
} from "@/lib/apps/adminService.js";
|
|
26
|
+
import { RegistryError } from "@/lib/apps/registryService.js";
|
|
27
|
+
|
|
28
|
+
export const prerender = false;
|
|
29
|
+
|
|
30
|
+
export async function POST(context: APIContext): Promise<Response> {
|
|
31
|
+
const requestedTenant = new URL(context.request.url).searchParams.get("tenant");
|
|
32
|
+
const auth = await resolveOperatorSession(context, requestedTenant);
|
|
33
|
+
if (!auth.ok) return json({ error: auth.message }, auth.status);
|
|
34
|
+
const manageDenied = requireManageRole(auth);
|
|
35
|
+
if (manageDenied) return manageDenied;
|
|
36
|
+
|
|
37
|
+
let body: unknown;
|
|
38
|
+
try {
|
|
39
|
+
body = await context.request.json();
|
|
40
|
+
} catch {
|
|
41
|
+
return json({ error: "invalid JSON body" }, 400);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const installId = parseInstallId(body);
|
|
45
|
+
if (!installId) return json({ error: "installId is required" }, 400);
|
|
46
|
+
|
|
47
|
+
const registry = await getRegistryService();
|
|
48
|
+
if (registry instanceof Response) return registry;
|
|
49
|
+
|
|
50
|
+
const owned = await requireOwnedInstall(registry, installId, auth.tenantId);
|
|
51
|
+
if (owned instanceof Response) return owned;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const issued = await registry.rotateCredentials(installId, auth.actor);
|
|
55
|
+
return json({ credential: { clientId: issued.clientId, secret: issued.secret } });
|
|
56
|
+
} catch (err) {
|
|
57
|
+
if (err instanceof RegistryError) return registryErrorResponse(err);
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/apps/admin/health — the console's per-install health rollup for
|
|
3
|
+
* the resolved operator tenant (PrivateApps epic, D8 Chunk C). Read-only:
|
|
4
|
+
* any membership role admits (mirrors `list.ts`'s `?tenant=`-as-candidate
|
|
5
|
+
* scoping — the tenant actually queried is always `auth.tenantId`, never the
|
|
6
|
+
* raw param).
|
|
7
|
+
*
|
|
8
|
+
* Folds `registryService.listByTenant` with `deliveryStore.getInstallHealth`
|
|
9
|
+
* per install through `healthAggregate.ts`'s `buildInstallHealthView` — that
|
|
10
|
+
* view is what data-minimizes `webhookEndpoint` down to `hasWebhookEndpoint`,
|
|
11
|
+
* so this route never even holds the raw endpoint value to leak.
|
|
12
|
+
*/
|
|
13
|
+
import type { APIContext } from "astro";
|
|
14
|
+
import { resolveOperatorSession } from "@/lib/apps/adminSession.js";
|
|
15
|
+
import { getRegistryService, json } from "@/lib/apps/adminService.js";
|
|
16
|
+
import { WebhookDeliveryStore } from "@/lib/webhooks/deliveryStore.js";
|
|
17
|
+
import { buildInstallHealthView, type AppInstallHealthView } from "@/lib/apps/healthAggregate.js";
|
|
18
|
+
import { fromD1 } from "@/lib/d1/catalog.js";
|
|
19
|
+
import { readD1 } from "@/lib/env";
|
|
20
|
+
|
|
21
|
+
export const prerender = false;
|
|
22
|
+
|
|
23
|
+
export async function GET(context: APIContext): Promise<Response> {
|
|
24
|
+
const requestedTenant = new URL(context.request.url).searchParams.get("tenant");
|
|
25
|
+
const auth = await resolveOperatorSession(context, requestedTenant);
|
|
26
|
+
if (!auth.ok) return json({ error: auth.message }, auth.status);
|
|
27
|
+
|
|
28
|
+
const registry = await getRegistryService();
|
|
29
|
+
if (registry instanceof Response) return registry;
|
|
30
|
+
|
|
31
|
+
const d1 = await readD1("STOREFRONT_APPS_DB");
|
|
32
|
+
if (!d1) return json({ error: "webhook store not configured" }, 503);
|
|
33
|
+
const deliveryStore = new WebhookDeliveryStore(fromD1(d1));
|
|
34
|
+
|
|
35
|
+
const installs = await registry.listByTenant(auth.tenantId);
|
|
36
|
+
const apps: AppInstallHealthView[] = await Promise.all(
|
|
37
|
+
installs.map(async (install) => {
|
|
38
|
+
const health = await deliveryStore.getInstallHealth(install.installId);
|
|
39
|
+
return buildInstallHealthView(install, health);
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return json({ apps });
|
|
44
|
+
}
|