@tokenoftrust/storefront-runner 1.3.4-rc.3 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/storefront/astro.config.mjs +15 -0
- package/apps/storefront/dev-plugins/dev-publish.mjs +55 -0
- package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +86 -7
- package/apps/storefront/drizzle.config.apps.ts +13 -0
- package/apps/storefront/env.d.ts +10 -0
- package/apps/storefront/migrations/README.md +13 -7
- package/apps/storefront/migrations-apps/0000_fast_millenium_guard.sql +129 -0
- package/apps/storefront/migrations-apps/meta/0000_snapshot.json +843 -0
- package/apps/storefront/migrations-apps/meta/_journal.json +13 -0
- package/apps/storefront/package.json +13 -1
- package/apps/storefront/perf/README.md +64 -0
- package/apps/storefront/perf/assert-budgets.ts +159 -0
- package/apps/storefront/playwright.config.ts +23 -0
- package/apps/storefront/public/js/dashboard-apps.js +173 -0
- package/apps/storefront/public/shared/commerce-marketing.css +221 -0
- package/apps/storefront/src/components/CollectionCard.astro +1 -0
- package/apps/storefront/src/components/ProductCard.astro +1 -0
- package/apps/storefront/src/components/admin/AdminPublishTab.astro +1959 -0
- package/apps/storefront/src/components/apps/AppWidgetFrame.astro +30 -0
- package/apps/storefront/src/components/chrome/NavDropdown.astro +6 -3
- package/apps/storefront/src/components/chrome/SiteFooter.astro +10 -0
- package/apps/storefront/src/components/chrome/SiteHeader.astro +10 -0
- package/apps/storefront/src/components/commerce/RatingStars.astro +3 -2
- package/apps/storefront/src/components/content/Callout.astro +75 -0
- package/apps/storefront/src/components/content/NeedsReviewCallout.astro +66 -0
- package/apps/storefront/src/components/content/ProseSections.astro +121 -0
- package/apps/storefront/src/components/content/ProseToc.astro +34 -0
- package/apps/storefront/src/components/content/RichText.astro +44 -0
- package/apps/storefront/src/components/content/TrustStrip.astro +46 -0
- package/apps/storefront/src/components/home/Hero.astro +14 -0
- package/apps/storefront/src/components/islands/CheckoutComplianceGate.tsx +295 -0
- package/apps/storefront/src/components/islands/ImageGallery.tsx +42 -21
- package/apps/storefront/src/components/islands/VariantSelector.tsx +62 -12
- package/apps/storefront/src/components/plp/FacetSidebar.astro +2 -2
- package/apps/storefront/src/components/subscription/ManageSubscriptionEntry.astro +1 -0
- package/apps/storefront/src/config/compliance/rulesets.ts +79 -0
- package/apps/storefront/src/config/storeName.ts +34 -0
- package/apps/storefront/src/layouts/Layout.astro +97 -23
- package/apps/storefront/src/lib/analytics/budgets.json +69 -0
- package/apps/storefront/src/lib/analytics/lighthouseReport.ts +109 -0
- package/apps/storefront/src/lib/analytics/perfBudgets.ts +452 -0
- package/apps/storefront/src/lib/analytics/rumAlert.ts +179 -0
- package/apps/storefront/src/lib/analytics/webVitals.ts +269 -0
- package/apps/storefront/src/lib/apps/adminService.ts +106 -0
- package/apps/storefront/src/lib/apps/adminSession.ts +205 -0
- package/apps/storefront/src/lib/apps/apiAuth.ts +91 -0
- package/apps/storefront/src/lib/apps/apiRoute.ts +35 -0
- package/apps/storefront/src/lib/apps/catalogMapper.ts +39 -0
- package/apps/storefront/src/lib/apps/credentials.ts +153 -0
- package/apps/storefront/src/lib/apps/gatewayKeys.ts +156 -0
- package/apps/storefront/src/lib/apps/healthAggregate.ts +66 -0
- package/apps/storefront/src/lib/apps/orders/attributionService.ts +206 -0
- package/apps/storefront/src/lib/apps/orders/customerHash.ts +28 -0
- package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +203 -0
- package/apps/storefront/src/lib/apps/orders/idempotency.ts +100 -0
- package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +117 -0
- package/apps/storefront/src/lib/apps/orders/orderMapper.ts +91 -0
- package/apps/storefront/src/lib/apps/orders/ordersStore.ts +181 -0
- package/apps/storefront/src/lib/apps/registryService.ts +579 -0
- package/apps/storefront/src/lib/apps/scopes.ts +79 -0
- package/apps/storefront/src/lib/apps/tokenIssuer.ts +121 -0
- package/apps/storefront/src/lib/apps/tokenVerifier.ts +148 -0
- package/apps/storefront/src/lib/apps/widgets/eligibility.ts +18 -0
- package/apps/storefront/src/lib/apps/widgets/frameProps.ts +52 -0
- package/apps/storefront/src/lib/apps/widgets/launchToken.ts +84 -0
- package/apps/storefront/src/lib/apps/widgets/placements.ts +57 -0
- package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +111 -0
- package/apps/storefront/src/lib/auth/adminEntry.ts +119 -0
- package/apps/storefront/src/lib/auth/identityToken.ts +21 -2
- package/apps/storefront/src/lib/auth/loginGate.ts +102 -18
- package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +215 -0
- package/apps/storefront/src/lib/auth/route.ts +16 -1
- package/apps/storefront/src/lib/auth/session.ts +8 -0
- package/apps/storefront/src/lib/auth/stepUpChallenge.ts +107 -0
- package/apps/storefront/src/lib/auth/totAccessClient.ts +208 -0
- package/apps/storefront/src/lib/blog/provider.ts +39 -0
- package/apps/storefront/src/lib/blog/types.ts +26 -0
- package/apps/storefront/src/lib/checkoutCommerce.ts +39 -1
- package/apps/storefront/src/lib/chrome/model.ts +11 -0
- package/apps/storefront/src/lib/compliance/enforcement.ts +95 -0
- package/apps/storefront/src/lib/content/callout.ts +78 -0
- package/apps/storefront/src/lib/content/index.ts +27 -0
- package/apps/storefront/src/lib/content/needsReview.ts +58 -0
- package/apps/storefront/src/lib/content/prose.ts +186 -0
- package/apps/storefront/src/lib/content/richtext.ts +76 -0
- package/apps/storefront/src/lib/content/trustStrip.ts +74 -0
- package/apps/storefront/src/lib/content-edit/client.ts +70 -14
- package/apps/storefront/src/lib/d1/catalog.ts +12 -0
- package/apps/storefront/src/lib/d1/schema-apps.ts +213 -0
- package/apps/storefront/src/lib/dev/apiBase.ts +8 -2
- package/apps/storefront/src/lib/dev/cliSignInCode.ts +65 -115
- package/apps/storefront/src/lib/dev/cockpitStore.ts +65 -0
- package/apps/storefront/src/lib/dev/obstacleStore.ts +214 -0
- package/apps/storefront/src/lib/dev/previewStatus.ts +112 -0
- package/apps/storefront/src/lib/dev/rendezvousBroker.ts +241 -0
- package/apps/storefront/src/lib/dev/subjectReissue.ts +67 -0
- package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +10 -10
- package/apps/storefront/src/lib/env.ts +12 -0
- package/apps/storefront/src/lib/jsonld.ts +12 -17
- package/apps/storefront/src/lib/membership/eligibility.ts +37 -0
- package/apps/storefront/src/lib/monitoring/manifest.ts +302 -0
- package/apps/storefront/src/lib/privacy/emailHint.ts +13 -5
- package/apps/storefront/src/lib/publish/apex-readiness.ts +337 -0
- package/apps/storefront/src/lib/publish/dispatchHealth.ts +269 -0
- package/apps/storefront/src/lib/publish/domainState.ts +351 -0
- package/apps/storefront/src/lib/publish/shipWorkspace.ts +362 -0
- package/apps/storefront/src/lib/rawChrome.ts +34 -3
- package/apps/storefront/src/lib/storyblok/content-model.ts +34 -2
- package/apps/storefront/src/lib/storyblok/provider.ts +11 -4
- package/apps/storefront/src/lib/subscription/model.ts +114 -0
- package/apps/storefront/src/lib/tot/ToTClient.ts +3 -3
- package/apps/storefront/src/lib/tot/query.ts +32 -0
- package/apps/storefront/src/lib/webhooks/cloudflareQueueDispatcher.ts +82 -0
- package/apps/storefront/src/lib/webhooks/deliveryEngine.ts +245 -0
- package/apps/storefront/src/lib/webhooks/deliveryMapper.ts +34 -0
- package/apps/storefront/src/lib/webhooks/deliveryStore.ts +668 -0
- package/apps/storefront/src/lib/webhooks/dispatcher.ts +168 -0
- package/apps/storefront/src/lib/webhooks/emit.ts +167 -0
- package/apps/storefront/src/lib/webhooks/endpointGuard.ts +135 -0
- package/apps/storefront/src/lib/webhooks/events.ts +98 -0
- package/apps/storefront/src/lib/webhooks/getDispatcher.ts +49 -0
- package/apps/storefront/src/lib/webhooks/signing.ts +29 -0
- package/apps/storefront/src/lib/webhooks/webhookSigningKey.ts +146 -0
- package/apps/storefront/src/middleware/index.ts +28 -13
- package/apps/storefront/src/pages/404.astro +21 -9
- package/apps/storefront/src/pages/[...slug].astro +36 -2
- package/apps/storefront/src/pages/admin.astro +29 -3
- package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +60 -0
- package/apps/storefront/src/pages/api/apps/admin/health.ts +44 -0
- package/apps/storefront/src/pages/api/apps/admin/install.ts +100 -0
- package/apps/storefront/src/pages/api/apps/admin/list.ts +26 -0
- package/apps/storefront/src/pages/api/apps/admin/resume.ts +56 -0
- package/apps/storefront/src/pages/api/apps/admin/suspend.ts +57 -0
- package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +74 -0
- package/apps/storefront/src/pages/api/apps/admin/update.ts +103 -0
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +59 -0
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries.ts +51 -0
- package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +172 -0
- package/apps/storefront/src/pages/api/apps/oauth/token.ts +87 -0
- package/apps/storefront/src/pages/api/apps/v1/attribution.ts +142 -0
- package/apps/storefront/src/pages/api/apps/v1/catalog/products/[handle].ts +39 -0
- package/apps/storefront/src/pages/api/apps/v1/catalog/products.ts +49 -0
- package/apps/storefront/src/pages/api/apps/v1/health.ts +32 -0
- package/apps/storefront/src/pages/api/apps/v1/inventory.ts +59 -0
- package/apps/storefront/src/pages/api/apps/v1/orders/[id].ts +48 -0
- package/apps/storefront/src/pages/api/apps/v1/orders.ts +73 -0
- package/apps/storefront/src/pages/api/apps/v1/reports.ts +21 -0
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +62 -0
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries.ts +47 -0
- package/apps/storefront/src/pages/api/auth/magic-exchange.ts +48 -1
- package/apps/storefront/src/pages/api/auth/step-up-send.ts +57 -0
- package/apps/storefront/src/pages/api/auth/step-up-verify.ts +129 -0
- package/apps/storefront/src/pages/api/auth/verify.ts +23 -0
- package/apps/storefront/src/pages/api/compliance/preflight.ts +206 -0
- package/apps/storefront/src/pages/api/rum/vitals.ts +54 -0
- package/apps/storefront/src/pages/api/test/dev-session.ts +133 -0
- package/apps/storefront/src/pages/auth/login.astro +197 -41
- package/apps/storefront/src/pages/auth/magic.astro +75 -43
- package/apps/storefront/src/pages/blog/[slug].astro +107 -0
- package/apps/storefront/src/pages/blog/index.astro +98 -0
- package/apps/storefront/src/pages/capabilities.astro +8 -0
- package/apps/storefront/src/pages/cockpit.astro +496 -74
- package/apps/storefront/src/pages/collections/[handle].astro +8 -0
- package/apps/storefront/src/pages/collections/index.astro +10 -2
- package/apps/storefront/src/pages/dashboard/[appDomain]/apps/index.astro +119 -0
- package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +5 -0
- package/apps/storefront/src/pages/index.astro +57 -0
- package/apps/storefront/src/pages/llms.txt.ts +31 -10
- package/apps/storefront/src/pages/products/[handle].astro +100 -9
- package/apps/storefront/src/pages/sitemap.xml.ts +21 -4
- package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +198 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +7 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +7 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +7 -0
- package/apps/storefront/src/pages/style-guide/index.astro +10 -0
- package/apps/storefront/src/styles/fonts.css +54 -0
- package/apps/storefront/src/styles/global.css +22 -2
- package/apps/storefront/src/themes/schema.ts +3 -25
- package/apps/storefront/tsconfig.json +1 -1
- package/apps/storefront/vitest.config.ts +4 -1
- package/package.json +1 -1
- package/packages/public-runtime/src/candidate-index.ts +311 -0
- package/packages/public-runtime/src/checkout.ts +94 -2
- package/packages/public-runtime/src/compliance/evaluate.ts +265 -0
- package/packages/public-runtime/src/compliance/evidence-signals.ts +81 -0
- package/packages/public-runtime/src/compliance/index.ts +22 -0
- package/packages/public-runtime/src/compliance/pact-report.ts +94 -0
- package/packages/public-runtime/src/compliance/profile.ts +198 -0
- package/packages/public-runtime/src/compliance/ruleset.ts +117 -0
- package/packages/public-runtime/src/compliance/verification.ts +81 -0
- package/packages/public-runtime/src/csp.ts +27 -3
- package/packages/public-runtime/src/customization-reconcile.ts +35 -0
- package/packages/public-runtime/src/customization-runtime.ts +8 -0
- package/packages/public-runtime/src/customization-versioning.ts +17 -0
- package/packages/public-runtime/src/extension-contract.ts +2 -1
- package/packages/public-runtime/src/hash.ts +25 -0
- package/packages/public-runtime/src/index.ts +6 -0
- package/packages/public-runtime/src/membership.ts +353 -0
- package/packages/public-runtime/src/product.ts +24 -2
- package/packages/public-runtime/src/review-trust-proof.ts +194 -0
- package/packages/public-runtime/src/tenant-assets.ts +40 -5
- package/packages/public-runtime/src/tenant.ts +236 -0
- package/packages/public-runtime/src/widget-postmessage.ts +205 -0
- package/scripts/dev/publish.mjs +158 -0
- package/scripts/dev/transient-files.mjs +2 -1
- package/tenants/home/public/fonts/inter-latin-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-700-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-800-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-700-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-800-normal.woff2 +0 -0
- package/tenants/home/public/pages/storefront.css +23 -0
- package/apps/storefront/src/lib/dev/hostedCockpit.ts +0 -169
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured content contracts (spec §F4) — ONE import surface for the
|
|
3
|
+
* data-driven framework widgets, so tenant content (JSON) and the .astro
|
|
4
|
+
* components share a single typed vocabulary and a tenant never copies HTML/CSS.
|
|
5
|
+
*
|
|
6
|
+
* New F4 contracts: rich text, callouts, needs-review, prose/policy sections,
|
|
7
|
+
* trust strip. Re-exported here alongside the tier/comparison contracts F10
|
|
8
|
+
* already owns (in lib/membership/model) so a membership page and a policy page
|
|
9
|
+
* import from the same place. (The <BrandGrid> `Brand` contract lives with its
|
|
10
|
+
* component in components/commerce/BrandGrid.astro.)
|
|
11
|
+
*/
|
|
12
|
+
export * from "./richtext.js";
|
|
13
|
+
export * from "./callout.js";
|
|
14
|
+
export * from "./needsReview.js";
|
|
15
|
+
export * from "./prose.js";
|
|
16
|
+
export * from "./trustStrip.js";
|
|
17
|
+
|
|
18
|
+
// Reused tier contracts (F10) — surfaced here so all structured widgets share
|
|
19
|
+
// one contract entry point.
|
|
20
|
+
export type {
|
|
21
|
+
Tier,
|
|
22
|
+
TierFigure,
|
|
23
|
+
TierFeature,
|
|
24
|
+
TierCta,
|
|
25
|
+
ComparisonRow,
|
|
26
|
+
} from "@/lib/membership/model";
|
|
27
|
+
export { isIllustrative } from "@/lib/membership/model";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Needs-review contract (spec §F4). The structured form of the f05 sheet's
|
|
3
|
+
* `.needs-review` flag: a first-class marker that a piece of migrated/authored
|
|
4
|
+
* content is a PLACEHOLDER pending human confirmation before go-live (a refund
|
|
5
|
+
* window not yet confirmed with the merchant, a claim we won't fabricate). It is
|
|
6
|
+
* the honest counterpart to f09's real-or-absent rule — where f09 forbids
|
|
7
|
+
* FABRICATING proof, this lets a tenant explicitly SAY "this needs confirming"
|
|
8
|
+
* instead of shipping a guess.
|
|
9
|
+
*
|
|
10
|
+
* Two roles:
|
|
11
|
+
* 1. A visible dev/preview affordance (<NeedsReviewCallout>) — a reviewer sees
|
|
12
|
+
* exactly what still needs sign-off. It is NEVER shown to shoppers: the
|
|
13
|
+
* renderer includes it only in preview (see <ProseSections includeNeedsReview>).
|
|
14
|
+
* 2. A go-live GUARD ({@link auditNeedsReview}) — a `blocker`-severity flag
|
|
15
|
+
* fails the audit, mirroring f09's auditTrustProof, so unresolved
|
|
16
|
+
* placeholders can't silently ship.
|
|
17
|
+
* Node-free + presentation-free.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export type NeedsReviewSeverity = "blocker" | "note";
|
|
21
|
+
|
|
22
|
+
export interface NeedsReview {
|
|
23
|
+
/** What must be confirmed, e.g. "Refund window not confirmed with merchant". */
|
|
24
|
+
reason: string;
|
|
25
|
+
/** blocker → must resolve before go-live; note → advisory. Default "note". */
|
|
26
|
+
severity?: NeedsReviewSeverity;
|
|
27
|
+
/** Optional owner/assignee (a name or handle) responsible for the sign-off. */
|
|
28
|
+
owner?: string;
|
|
29
|
+
/** Optional link to the source/ticket/context for the review. */
|
|
30
|
+
href?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const NEEDS_REVIEW_TITLE = "Needs review before go-live";
|
|
34
|
+
|
|
35
|
+
/** True when a flag must block go-live (severity "blocker"). */
|
|
36
|
+
export function isBlocking(n: Pick<NeedsReview, "severity">): boolean {
|
|
37
|
+
return n.severity === "blocker";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface NeedsReviewViolation {
|
|
41
|
+
reason: string;
|
|
42
|
+
owner?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Go-live guard: given the needs-review flags a page/tenant carries, `ok` is
|
|
47
|
+
* false when ANY is a blocker. Parallels f09's auditTrustProof — a clean page
|
|
48
|
+
* (no flags, or only advisory notes) passes; an unresolved blocker fails and
|
|
49
|
+
* lists what's outstanding. The deploy path calls this before promoting.
|
|
50
|
+
*/
|
|
51
|
+
export function auditNeedsReview(
|
|
52
|
+
flags: ReadonlyArray<NeedsReview>,
|
|
53
|
+
): { ok: boolean; blockers: NeedsReviewViolation[] } {
|
|
54
|
+
const blockers = flags
|
|
55
|
+
.filter(isBlocking)
|
|
56
|
+
.map((f) => ({ reason: f.reason, owner: f.owner }));
|
|
57
|
+
return { ok: blockers.length === 0, blockers };
|
|
58
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prose / policy page contract (spec §F4). The marquee promotion: a policy /
|
|
3
|
+
* about / compliance page is authored as STRUCTURED sections — headings,
|
|
4
|
+
* paragraphs, lists, definition glossaries, callouts, a contact block, and
|
|
5
|
+
* needs-review flags — instead of the `body_html` blob that `[...slug].astro`
|
|
6
|
+
* used to `set:html`. <ProseSections> renders these with real headings, `<dl>`
|
|
7
|
+
* semantics, list landmarks, and safe links (no `set:html`), so a tenant drives
|
|
8
|
+
* a full policy page from JSON with no HTML/CSS to copy (the F4 acceptance).
|
|
9
|
+
*
|
|
10
|
+
* The section shapes mirror the repeated patterns the f05 sheet styled for the
|
|
11
|
+
* raw-HTML path (`.prose`, `.toc`, `.contact-card`, `.disclaimer`/`.warn-band`,
|
|
12
|
+
* `.needs-review`) — one content vocabulary, two delivery paths (raw fragment
|
|
13
|
+
* vs. structured widget), the same look from f06 tokens.
|
|
14
|
+
*
|
|
15
|
+
* Node-free + presentation-free.
|
|
16
|
+
*/
|
|
17
|
+
import type { RichText } from "./richtext.js";
|
|
18
|
+
import { plainText } from "./richtext.js";
|
|
19
|
+
import type { CalloutTone } from "./callout.js";
|
|
20
|
+
import type { NeedsReview } from "./needsReview.js";
|
|
21
|
+
|
|
22
|
+
/** A section heading (renders <h2> or <h3>; carries/derives an anchor id). */
|
|
23
|
+
export interface ProseHeadingSection {
|
|
24
|
+
kind: "heading";
|
|
25
|
+
text: string;
|
|
26
|
+
/** 2 → <h2> (default), 3 → <h3>. Keeps a real document outline. */
|
|
27
|
+
level?: 2 | 3;
|
|
28
|
+
/** Anchor id for TOC links; derived from `text` when omitted. */
|
|
29
|
+
id?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ProseParagraphSection {
|
|
33
|
+
kind: "paragraph";
|
|
34
|
+
text: RichText;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A bullet (default) or numbered list. */
|
|
38
|
+
export interface ProseListSection {
|
|
39
|
+
kind: "list";
|
|
40
|
+
items: RichText[];
|
|
41
|
+
ordered?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A definition glossary — renders a semantic <dl>/<dt>/<dd>. */
|
|
45
|
+
export interface ProseDefinitionsSection {
|
|
46
|
+
kind: "definitions";
|
|
47
|
+
items: { term: string; description: RichText }[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A tone-styled callout embedded in the flow (see callout.ts). */
|
|
51
|
+
export interface ProseCalloutSection {
|
|
52
|
+
kind: "callout";
|
|
53
|
+
tone: CalloutTone;
|
|
54
|
+
title?: string;
|
|
55
|
+
body: RichText;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A contact block — renders a semantic <address> with real mailto/tel links. */
|
|
59
|
+
export interface ProseContactSection {
|
|
60
|
+
kind: "contact";
|
|
61
|
+
heading?: string;
|
|
62
|
+
email?: string;
|
|
63
|
+
phone?: string;
|
|
64
|
+
/** Postal address lines, rendered in order. */
|
|
65
|
+
address?: string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A needs-review flag embedded in the flow (see needsReview.ts). */
|
|
69
|
+
export type ProseNeedsReviewSection = { kind: "needs_review" } & NeedsReview;
|
|
70
|
+
|
|
71
|
+
export type ProseSection =
|
|
72
|
+
| ProseHeadingSection
|
|
73
|
+
| ProseParagraphSection
|
|
74
|
+
| ProseListSection
|
|
75
|
+
| ProseDefinitionsSection
|
|
76
|
+
| ProseCalloutSection
|
|
77
|
+
| ProseContactSection
|
|
78
|
+
| ProseNeedsReviewSection;
|
|
79
|
+
|
|
80
|
+
/** A full structured page (policy / about / compliance). */
|
|
81
|
+
export interface ProsePage {
|
|
82
|
+
title: string;
|
|
83
|
+
eyebrow?: string;
|
|
84
|
+
intro?: string;
|
|
85
|
+
/** ISO date (YYYY-MM-DD) rendered as a "Last updated" line. */
|
|
86
|
+
updated?: string;
|
|
87
|
+
sections: ProseSection[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Slugify heading text into a stable, URL-safe anchor id. */
|
|
91
|
+
export function slugifyHeading(text: string): string {
|
|
92
|
+
return text
|
|
93
|
+
.toLowerCase()
|
|
94
|
+
.trim()
|
|
95
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
96
|
+
.replace(/^-+|-+$/g, "");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Drop structurally-empty sections (a blank heading, an empty paragraph/list,
|
|
101
|
+
* an empty definitions/contact block) so a partially-authored page never
|
|
102
|
+
* renders a stray empty node.
|
|
103
|
+
*/
|
|
104
|
+
export function normalizeProse(
|
|
105
|
+
sections: ReadonlyArray<ProseSection>,
|
|
106
|
+
): ProseSection[] {
|
|
107
|
+
const out: ProseSection[] = [];
|
|
108
|
+
for (const s of sections) {
|
|
109
|
+
switch (s.kind) {
|
|
110
|
+
case "heading":
|
|
111
|
+
if (s.text.trim()) out.push(s);
|
|
112
|
+
break;
|
|
113
|
+
case "paragraph":
|
|
114
|
+
if (plainText(s.text)) out.push(s);
|
|
115
|
+
break;
|
|
116
|
+
case "list": {
|
|
117
|
+
const items = s.items.filter((i) => plainText(i));
|
|
118
|
+
if (items.length) out.push({ ...s, items });
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case "definitions": {
|
|
122
|
+
const items = s.items.filter((d) => d.term.trim() && plainText(d.description));
|
|
123
|
+
if (items.length) out.push({ ...s, items });
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case "callout":
|
|
127
|
+
if (plainText(s.body)) out.push(s);
|
|
128
|
+
break;
|
|
129
|
+
case "contact":
|
|
130
|
+
if (s.email || s.phone || (s.address && s.address.length)) out.push(s);
|
|
131
|
+
break;
|
|
132
|
+
case "needs_review":
|
|
133
|
+
if (s.reason.trim()) out.push(s);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Fill in a derived `id` for every heading that lacks one (for TOC anchoring). */
|
|
141
|
+
export function withHeadingIds(
|
|
142
|
+
sections: ReadonlyArray<ProseSection>,
|
|
143
|
+
): ProseSection[] {
|
|
144
|
+
const seen = new Map<string, number>();
|
|
145
|
+
return sections.map((s) => {
|
|
146
|
+
if (s.kind !== "heading") return s;
|
|
147
|
+
const base = s.id?.trim() || slugifyHeading(s.text);
|
|
148
|
+
// De-dupe collisions so anchors stay unique on a long policy page.
|
|
149
|
+
const n = seen.get(base) ?? 0;
|
|
150
|
+
seen.set(base, n + 1);
|
|
151
|
+
const id = n === 0 ? base : `${base}-${n + 1}`;
|
|
152
|
+
return { ...s, id };
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface TocEntry {
|
|
157
|
+
id: string;
|
|
158
|
+
text: string;
|
|
159
|
+
level: 2 | 3;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The table-of-contents entries for a page — one per level-2 heading (the
|
|
164
|
+
* top-level policy sections). Assumes {@link withHeadingIds} has run so every
|
|
165
|
+
* heading has an id. Level-3 headings are excluded to keep the chip row short.
|
|
166
|
+
*/
|
|
167
|
+
export function tableOfContents(
|
|
168
|
+
sections: ReadonlyArray<ProseSection>,
|
|
169
|
+
): TocEntry[] {
|
|
170
|
+
const out: TocEntry[] = [];
|
|
171
|
+
for (const s of sections) {
|
|
172
|
+
if (s.kind === "heading" && (s.level ?? 2) === 2 && s.id) {
|
|
173
|
+
out.push({ id: s.id, text: s.text, level: 2 });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Every needs-review flag on a page (for the go-live audit). */
|
|
180
|
+
export function collectNeedsReview(
|
|
181
|
+
sections: ReadonlyArray<ProseSection>,
|
|
182
|
+
): NeedsReview[] {
|
|
183
|
+
return sections
|
|
184
|
+
.filter((s): s is ProseNeedsReviewSection => s.kind === "needs_review")
|
|
185
|
+
.map(({ kind: _kind, ...flag }) => flag);
|
|
186
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline rich-text model (spec §F4 structured framework widgets).
|
|
3
|
+
*
|
|
4
|
+
* The structured content widgets replace hand-authored HTML fragments with
|
|
5
|
+
* DATA. Prose still needs a LITTLE inline formatting — a link, a bold run —
|
|
6
|
+
* without reopening the raw-HTML door (which f09's real-or-absent rule and the
|
|
7
|
+
* CSP both push us to close). So instead of a `body_html` blob a paragraph
|
|
8
|
+
* carries a {@link RichText}: either a plain string (the common case) or an
|
|
9
|
+
* array of typed {@link InlineRun}s. Components render runs as real DOM
|
|
10
|
+
* (anchors, <strong>, <em>) with NO `set:html`, so there is no injection seam
|
|
11
|
+
* and no per-tenant markup to copy.
|
|
12
|
+
*
|
|
13
|
+
* Node-free + presentation-free: this is the shared shape the .astro widgets
|
|
14
|
+
* and the contract tests both import.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** A run of inline content inside a {@link RichText}. */
|
|
18
|
+
export type InlineRun =
|
|
19
|
+
/** Plain (optionally emphasized) text. */
|
|
20
|
+
| { kind: "text"; text: string; strong?: boolean; em?: boolean }
|
|
21
|
+
/** A hyperlink. `external` is derived by {@link isExternalHref} when omitted. */
|
|
22
|
+
| { kind: "link"; text: string; href: string; external?: boolean };
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Inline content for a paragraph / list item / callout body / definition. A
|
|
26
|
+
* bare string is the ergonomic common case (one text run); an array carries
|
|
27
|
+
* mixed links + emphasis. Authors never write HTML here.
|
|
28
|
+
*/
|
|
29
|
+
export type RichText = string | InlineRun[];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* True when a href points off-site (so the link component adds
|
|
33
|
+
* rel="noopener noreferrer" + a new-tab affordance). Protocol-relative (`//…`)
|
|
34
|
+
* and absolute http(s) URLs are external; site-root (`/…`), anchors (`#…`),
|
|
35
|
+
* and `mailto:` / `tel:` are treated as in-context (same tab, no rel).
|
|
36
|
+
*/
|
|
37
|
+
export function isExternalHref(href: string): boolean {
|
|
38
|
+
const h = href.trim();
|
|
39
|
+
if (h.startsWith("//")) return true;
|
|
40
|
+
return /^https?:\/\//i.test(h);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Normalize any {@link RichText} to an array of runs. A plain string becomes a
|
|
45
|
+
* single text run; an existing array is returned filtered of empty text runs
|
|
46
|
+
* (an empty link text keeps its href-less nothing dropped too). Every returned
|
|
47
|
+
* link run has an explicit `external` flag so the component needn't recompute.
|
|
48
|
+
*/
|
|
49
|
+
export function toRuns(rich: RichText | null | undefined): InlineRun[] {
|
|
50
|
+
if (rich == null) return [];
|
|
51
|
+
if (typeof rich === "string") {
|
|
52
|
+
return rich.length > 0 ? [{ kind: "text", text: rich }] : [];
|
|
53
|
+
}
|
|
54
|
+
const out: InlineRun[] = [];
|
|
55
|
+
for (const run of rich) {
|
|
56
|
+
if (run.kind === "text") {
|
|
57
|
+
if (run.text.length > 0) out.push(run);
|
|
58
|
+
} else if (run.kind === "link") {
|
|
59
|
+
if (run.text.length > 0 && run.href.length > 0) {
|
|
60
|
+
out.push({ ...run, external: run.external ?? isExternalHref(run.href) });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Flatten a {@link RichText} to plain text — for aria-labels, SEO descriptions,
|
|
69
|
+
* and heading-id slugs. Never emits markup.
|
|
70
|
+
*/
|
|
71
|
+
export function plainText(rich: RichText | null | undefined): string {
|
|
72
|
+
return toRuns(rich)
|
|
73
|
+
.map((r) => r.text)
|
|
74
|
+
.join("")
|
|
75
|
+
.trim();
|
|
76
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trust-strip contract (spec §F4, governed by f09's real-or-absent rule).
|
|
3
|
+
*
|
|
4
|
+
* A trust strip is the quiet row of proof tokens a store repeats across pages
|
|
5
|
+
* ("Ships from USA", "Lab-tested", a rating). Because a rating/review token is
|
|
6
|
+
* exactly the kind of social proof f09 forbids FABRICATING, this contract is
|
|
7
|
+
* real-or-absent BY CONSTRUCTION: {@link sanitizeTrustStrip} reuses f09's
|
|
8
|
+
* `isFabricatedTrustProof` detector to drop any claim that is actually scraped
|
|
9
|
+
* review-widget markup, and drops any rating token whose average/count aren't
|
|
10
|
+
* real positive finite numbers (the same test `readReviewAggregate` applies). A
|
|
11
|
+
* strip with nothing real left renders NOTHING — never a fabricated badge.
|
|
12
|
+
*
|
|
13
|
+
* Node-free + presentation-free; the .astro widget renders the sanitized items.
|
|
14
|
+
*/
|
|
15
|
+
import { isFabricatedTrustProof } from "@tot/public-runtime";
|
|
16
|
+
|
|
17
|
+
export type TrustStripItem =
|
|
18
|
+
/** A plain proof claim, e.g. "Ships from USA", "Lab-tested". */
|
|
19
|
+
| { kind: "claim"; label: string }
|
|
20
|
+
/**
|
|
21
|
+
* A REAL aggregate rating. Surfaced only when `average` (1–5) and `count`
|
|
22
|
+
* are real positive finite numbers — a half-populated aggregate is dropped.
|
|
23
|
+
*/
|
|
24
|
+
| { kind: "rating"; average: number; count: number; label?: string };
|
|
25
|
+
|
|
26
|
+
/** True when a rating token is backed by real, positive, finite numbers. */
|
|
27
|
+
export function isRealRating(
|
|
28
|
+
item: Extract<TrustStripItem, { kind: "rating" }>,
|
|
29
|
+
): boolean {
|
|
30
|
+
const { average, count } = item;
|
|
31
|
+
return (
|
|
32
|
+
Number.isFinite(average) &&
|
|
33
|
+
average > 0 &&
|
|
34
|
+
average <= 5 &&
|
|
35
|
+
Number.isFinite(count) &&
|
|
36
|
+
count > 0
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Drop everything that isn't real proof:
|
|
42
|
+
* - a `claim` whose label carries scraped review-widget markup / a baked-in
|
|
43
|
+
* aggregate (f09's `isFabricatedTrustProof`), or is blank;
|
|
44
|
+
* - a `rating` that isn't a real positive finite aggregate ({@link isRealRating}).
|
|
45
|
+
* The result is safe to render verbatim; an empty result means "render nothing".
|
|
46
|
+
*/
|
|
47
|
+
export function sanitizeTrustStrip(
|
|
48
|
+
items: ReadonlyArray<TrustStripItem>,
|
|
49
|
+
): TrustStripItem[] {
|
|
50
|
+
const out: TrustStripItem[] = [];
|
|
51
|
+
for (const item of items) {
|
|
52
|
+
if (item.kind === "claim") {
|
|
53
|
+
const label = item.label.trim();
|
|
54
|
+
if (label.length > 0 && !isFabricatedTrustProof(label)) {
|
|
55
|
+
out.push({ kind: "claim", label });
|
|
56
|
+
}
|
|
57
|
+
} else if (item.kind === "rating") {
|
|
58
|
+
if (isRealRating(item)) out.push(item);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A screen-reader label for a real rating token, e.g.
|
|
66
|
+
* "Rated 4.9 out of 5 from 369 reviews". Callers pass only sanitized ratings.
|
|
67
|
+
*/
|
|
68
|
+
export function ratingAriaLabel(
|
|
69
|
+
item: Extract<TrustStripItem, { kind: "rating" }>,
|
|
70
|
+
): string {
|
|
71
|
+
const avg = Math.round(item.average * 10) / 10;
|
|
72
|
+
const noun = item.count === 1 ? "review" : "reviews";
|
|
73
|
+
return `Rated ${avg} out of 5 from ${item.count} ${noun}`;
|
|
74
|
+
}
|
|
@@ -5,14 +5,20 @@
|
|
|
5
5
|
* same tool-result unwrap, same typed errors) — reusing the forge seam's
|
|
6
6
|
* exported transport + error types so there is ONE wire contract, not two.
|
|
7
7
|
*
|
|
8
|
-
* AUTH SHAPE (
|
|
9
|
-
* backend calls `content_edit`
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
8
|
+
* AUTH SHAPE (Option A, `content-edit-machine-identity` — mirrors change/B3):
|
|
9
|
+
* the storefront backend calls `content_edit` with the SAME machine-identity
|
|
10
|
+
* APP token the change/forge clients use — a `client_credentials` +
|
|
11
|
+
* `private_key_jwt` token minted against the MCP's OAuth token endpoint via
|
|
12
|
+
* {@link AppTokenProvider}, attached as Bearer at `initialize` and on every
|
|
13
|
+
* request by {@link FetchTransport} — NOT the retired `credential_validate`
|
|
14
|
+
* handshake and NOT a human `TOT_ACCESS_TOKEN`. Developer-level authorization
|
|
15
|
+
* is, and remains, the caller's job: `/dev`'s own `readViewerSession()` +
|
|
16
|
+
* capability check (BEFORE this client is ever constructed) derives the
|
|
17
|
+
* tenant from the signed-in developer's OWN session, never from a
|
|
18
|
+
* caller-supplied value — tenant isolation is enforced there, unaffected by
|
|
19
|
+
* this change. This client only proves "a legitimate storefront backend
|
|
20
|
+
* (machine identity) is asking", the same trust boundary
|
|
21
|
+
* `/api/changes/accept` already relies on post-B3.
|
|
16
22
|
*/
|
|
17
23
|
import {
|
|
18
24
|
FetchTransport,
|
|
@@ -20,16 +26,25 @@ import {
|
|
|
20
26
|
ForgeConfigError,
|
|
21
27
|
ForgeTransportError,
|
|
22
28
|
SESSION_HEADER,
|
|
29
|
+
AppTokenProvider,
|
|
30
|
+
deriveMcpOauthEndpoints,
|
|
23
31
|
type McpTransport,
|
|
24
32
|
type JsonRpcRequest,
|
|
25
33
|
type TotCredentials,
|
|
26
34
|
type TransportResponse,
|
|
35
|
+
type BearerProvider,
|
|
27
36
|
} from "@/lib/forge";
|
|
28
37
|
import { mapForgeError } from "@/lib/forge/errors.js";
|
|
29
38
|
import { readEnv } from "@/lib/env";
|
|
30
39
|
import type { ContentEditFile, ContentEditResult } from "./types.js";
|
|
31
40
|
|
|
32
41
|
export const CONTENT_EDIT_TOOL = {
|
|
42
|
+
/**
|
|
43
|
+
* Session-validation tool for the NON-OAuth (api-key/secret) construction —
|
|
44
|
+
* kept ONLY for backward-compatible test-transport injection (no
|
|
45
|
+
* bearerProvider). The deployed path never calls this — see the module
|
|
46
|
+
* header.
|
|
47
|
+
*/
|
|
33
48
|
validate: "credential_validate",
|
|
34
49
|
edit: "content_edit",
|
|
35
50
|
} as const;
|
|
@@ -77,10 +92,21 @@ function unwrapToolResult<T>(result: unknown): T {
|
|
|
77
92
|
export interface JsonRpcContentEditClientConfig {
|
|
78
93
|
/** MCP base URL (the `POST /mcp` endpoint). */
|
|
79
94
|
baseUrl: string;
|
|
80
|
-
/** Storefront's ToT
|
|
95
|
+
/** Storefront's ToT credentials for the legacy `credential_validate` path. */
|
|
81
96
|
credentials: TotCredentials;
|
|
82
97
|
/** Transport override (defaults to fetch). Injected by tests. */
|
|
83
98
|
transport?: McpTransport;
|
|
99
|
+
/**
|
|
100
|
+
* Machine-identity app-token source (mirrors forge's B2 / change's B3,
|
|
101
|
+
* binding `c0-forge-auth-supersede`). When present, the client
|
|
102
|
+
* authenticates PURELY via a `client_credentials` + `private_key_jwt` app
|
|
103
|
+
* token — attached as Bearer at `initialize` and on every request by
|
|
104
|
+
* {@link FetchTransport}, re-minted on expiry/401 — and NEVER calls the
|
|
105
|
+
* retired `credential_validate` handshake. Built by
|
|
106
|
+
* {@link createContentEditClient} from `MCP_BASE_URL`; tests may inject
|
|
107
|
+
* their own {@link BearerProvider}.
|
|
108
|
+
*/
|
|
109
|
+
bearerProvider?: BearerProvider;
|
|
84
110
|
}
|
|
85
111
|
|
|
86
112
|
// --- The client -----------------------------------------------------------
|
|
@@ -92,6 +118,8 @@ export class JsonRpcContentEditClient {
|
|
|
92
118
|
private nextId = 1;
|
|
93
119
|
/** Cached session validation — validate once, reuse (lazily). */
|
|
94
120
|
private sessionPromise?: Promise<string | undefined>;
|
|
121
|
+
/** True when a machine-identity app token authenticates the session (mirrors forge B2 / change B3). */
|
|
122
|
+
private readonly usesServiceAuth: boolean;
|
|
95
123
|
|
|
96
124
|
constructor(config: JsonRpcContentEditClientConfig) {
|
|
97
125
|
if (!config.baseUrl) {
|
|
@@ -104,7 +132,9 @@ export class JsonRpcContentEditClient {
|
|
|
104
132
|
}
|
|
105
133
|
this.baseUrl = config.baseUrl;
|
|
106
134
|
this.credentials = config.credentials;
|
|
107
|
-
this.
|
|
135
|
+
this.usesServiceAuth = config.bearerProvider != null;
|
|
136
|
+
this.transport =
|
|
137
|
+
config.transport ?? new FetchTransport(config.baseUrl, config.bearerProvider);
|
|
108
138
|
}
|
|
109
139
|
|
|
110
140
|
async editContent(
|
|
@@ -123,7 +153,17 @@ export class JsonRpcContentEditClient {
|
|
|
123
153
|
// --- internals (mirror JsonRpcChangeClient) --------------------------------
|
|
124
154
|
|
|
125
155
|
private ensureSession(): Promise<string | undefined> {
|
|
126
|
-
|
|
156
|
+
// Service-auth (mirrors forge B2 / change B3 / binding
|
|
157
|
+
// `c0-forge-auth-supersede`): the app token is attached as Bearer at the
|
|
158
|
+
// transport layer (initialize + every request), so the legacy
|
|
159
|
+
// `credential_validate` round-trip is skipped entirely — it was retired
|
|
160
|
+
// MCP-side and would now be rejected. FetchTransport still threads the
|
|
161
|
+
// real `Mcp-Session-Id` the Bearer-authed handshake minted, so a session
|
|
162
|
+
// id is never needed from here. Without a bearerProvider (e.g. an
|
|
163
|
+
// injected mock transport in unit tests) the legacy validate path is kept.
|
|
164
|
+
this.sessionPromise ??= this.usesServiceAuth
|
|
165
|
+
? Promise.resolve(undefined)
|
|
166
|
+
: this.validateSession();
|
|
127
167
|
return this.sessionPromise;
|
|
128
168
|
}
|
|
129
169
|
|
|
@@ -211,7 +251,21 @@ export class JsonRpcContentEditClient {
|
|
|
211
251
|
|
|
212
252
|
/**
|
|
213
253
|
* Build a {@link JsonRpcContentEditClient} from the runtime env — the SAME env
|
|
214
|
-
* the change client reads
|
|
254
|
+
* the change client reads:
|
|
255
|
+
* - `MCP_BASE_URL` — the `POST /mcp` endpoint (its ORIGIN also hosts the OAuth
|
|
256
|
+
* token endpoint + resource the app token binds to; already `…/storefront`
|
|
257
|
+
* per B4, so the app token's resource + this RPC endpoint match).
|
|
258
|
+
* - `TOT_API_KEY` (+ optional `TOT_SECRET_KEY`) — storefront's OWN ToT
|
|
259
|
+
* credentials (retained for legacy/local paths; the deployed content-edit
|
|
260
|
+
* session no longer uses them for auth).
|
|
261
|
+
*
|
|
262
|
+
* Machine identity (mirrors forge B2 / change B3 / binding
|
|
263
|
+
* `c0-forge-auth-supersede`): the content-edit session authenticates with a
|
|
264
|
+
* `client_credentials` + `private_key_jwt` APP token minted against the MCP's
|
|
265
|
+
* OAuth token endpoint via {@link AppTokenProvider} — NOT a human
|
|
266
|
+
* `TOT_ACCESS_TOKEN` and NOT a shared secret. `TOT_ACCESS_TOKEN` is
|
|
267
|
+
* deliberately NOT read here.
|
|
268
|
+
*
|
|
215
269
|
* Throws {@link ForgeConfigError} when the base URL or api key is missing.
|
|
216
270
|
* Secrets are read via {@link readEnv} and never logged.
|
|
217
271
|
*/
|
|
@@ -224,7 +278,9 @@ export async function createContentEditClient(
|
|
|
224
278
|
({
|
|
225
279
|
apiKey: (await readEnv("TOT_API_KEY")) ?? "",
|
|
226
280
|
secretKey: await readEnv("TOT_SECRET_KEY"),
|
|
227
|
-
accessToken: await readEnv("TOT_ACCESS_TOKEN"),
|
|
228
281
|
} satisfies TotCredentials);
|
|
229
|
-
|
|
282
|
+
const bearerProvider =
|
|
283
|
+
overrides.bearerProvider ??
|
|
284
|
+
(baseUrl ? new AppTokenProvider(deriveMcpOauthEndpoints(baseUrl)) : undefined);
|
|
285
|
+
return new JsonRpcContentEditClient({ ...overrides, baseUrl, credentials, bearerProvider });
|
|
230
286
|
}
|
|
@@ -153,6 +153,18 @@ export async function getProductByHandle(
|
|
|
153
153
|
return row ? (JSON.parse(row.data) as CatalogProduct) : null;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
/**
|
|
157
|
+
* All active-status products, full data. No pagination — the D3 inventory
|
|
158
|
+
* projection (`api/apps/v1/inventory.ts`) reads the whole active catalog to
|
|
159
|
+
* build its per-SKU list, mirroring `searchProducts`'s active-only scoping.
|
|
160
|
+
*/
|
|
161
|
+
export async function listAllActiveProducts(db: Queryable): Promise<CatalogProduct[]> {
|
|
162
|
+
const rows = await db.all<{ data: string }>(
|
|
163
|
+
`SELECT data FROM products WHERE status = 'active' ORDER BY position ASC`,
|
|
164
|
+
);
|
|
165
|
+
return rows.map((r) => JSON.parse(r.data) as CatalogProduct);
|
|
166
|
+
}
|
|
167
|
+
|
|
156
168
|
function sortClause(sort?: ListProductsQuery["sort"]): string {
|
|
157
169
|
switch (sort) {
|
|
158
170
|
case "price-asc":
|