create-ailk 0.27.0 → 0.28.1
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/README.md +84 -7
- package/dist/add-module-core.js +2 -1
- package/dist/add-module.d.ts +2 -1
- package/dist/add-module.js +9 -4
- package/dist/add-route.js +46 -19
- package/dist/body-heading.d.ts +56 -0
- package/dist/body-heading.js +394 -0
- package/dist/cli.js +28 -1
- package/dist/content-payloads.js +2 -2
- package/dist/derive-architecture.js +8 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/lib/auth-secret-file.d.ts +6 -2
- package/dist/lib/auth-secret-file.js +46 -20
- package/dist/lib/chrome-readers.d.ts +21 -5
- package/dist/lib/chrome-readers.js +128 -20
- package/dist/lib/declared-env.d.ts +6 -0
- package/dist/lib/declared-env.js +2 -0
- package/dist/lib/env-catalog.js +20 -0
- package/dist/lib/import-graph.d.ts +16 -0
- package/dist/lib/import-graph.js +53 -0
- package/dist/lib/site-modules.d.ts +8 -0
- package/dist/lib/site-modules.js +34 -0
- package/dist/module-architecture.js +23 -14
- package/dist/paid-sections.d.ts +17 -0
- package/dist/paid-sections.js +39 -0
- package/dist/programmatic.d.ts +16 -0
- package/dist/programmatic.js +73 -4
- package/dist/remove-module.d.ts +98 -0
- package/dist/remove-module.js +620 -0
- package/dist/seed-flows.d.ts +2 -2
- package/dist/seed-flows.js +16 -9
- package/dist/site-roster.js +4 -1
- package/dist/starter-grid.d.ts +26 -0
- package/dist/starter-grid.js +34 -0
- package/dist/surfaces.d.ts +26 -0
- package/dist/surfaces.js +105 -9
- package/dist/sync-routes.d.ts +7 -0
- package/dist/sync-routes.js +53 -6
- package/dist/template-pages.d.ts +36 -0
- package/dist/template-pages.js +129 -0
- package/dist/template-record.d.ts +13 -4
- package/dist/template-record.js +8 -4
- package/dist/topology.d.ts +2 -1
- package/dist/topology.js +4 -2
- package/dist/update.d.ts +34 -2
- package/dist/update.js +395 -48
- package/package.json +3 -3
- package/release-scope.json +2 -1
- package/site-templates/aliases/hub.TEMPLATE-MANIFEST.json +115 -0
- package/templates/.env.example +5 -2
- package/templates/apps/api/.env.example +9 -5
- package/templates/apps/api/CLAUDE.md +14 -9
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +37 -0
- package/templates/apps/api/src/vercel-handler.ts +8 -1
- package/templates/apps/mcp/CLAUDE.md +18 -10
- package/templates/apps/web/.env.example +25 -11
- package/templates/apps/web/CLAUDE.md +45 -4
- package/templates/apps/web/__tests__/setup.ts +7 -0
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +7 -7
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +61 -2
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +81 -0
- package/templates/apps/web/app/[locale]/(authed)/account/connect/__tests__/page.test.tsx +197 -0
- package/templates/apps/web/app/[locale]/(authed)/account/connect/page.tsx +152 -0
- package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/careers/[slug]/[action]/page.tsx +76 -21
- package/templates/apps/web/app/[locale]/careers/[slug]/apply.actions.ts +46 -16
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +20 -3
- package/templates/apps/web/app/[locale]/careers/__tests__/apply.actions.test.ts +180 -2
- package/templates/apps/web/app/[locale]/careers/__tests__/careers-set.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/careers/__tests__/job-apply-page.test.tsx +122 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +20 -3
- package/templates/apps/web/app/[locale]/case-studies/__tests__/hub-children.test.tsx +65 -0
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/case-studies/page.tsx +23 -11
- package/templates/apps/web/app/[locale]/checkout/__tests__/thank-you-account.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/consent.actions.ts +44 -6
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +4 -4
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +269 -42
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +88 -30
- package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +20 -3
- package/templates/apps/web/app/[locale]/courses/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/courses/page.tsx +22 -6
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +15 -2
- package/templates/apps/web/app/[locale]/events/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +8 -2
- package/templates/apps/web/app/[locale]/events/page.tsx +23 -7
- package/templates/apps/web/app/[locale]/features/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/features/page.tsx +23 -25
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +95 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/save.actions.test.ts +193 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/email-moment.ts +92 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +10 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +513 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +122 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.no-commerce.test.tsx +195 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/landing.test.tsx +293 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +397 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/checkout-seam.ts +74 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +264 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/save.actions.ts +75 -0
- package/templates/apps/web/app/[locale]/for/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +5 -1
- package/templates/apps/web/app/[locale]/for/page.tsx +23 -25
- package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +20 -3
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +25 -4
- package/templates/apps/web/app/[locale]/integrations/__tests__/hub-children.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +8 -2
- package/templates/apps/web/app/[locale]/integrations/page.tsx +23 -11
- package/templates/apps/web/app/[locale]/layout.tsx +157 -139
- package/templates/apps/web/app/[locale]/page.tsx +51 -3
- package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +16 -3
- package/templates/apps/web/app/[locale]/partners/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/partners/page.tsx +22 -6
- package/templates/apps/web/app/[locale]/portfolio/__tests__/hub-children.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/portfolio/page.tsx +25 -24
- package/templates/apps/web/app/[locale]/pricing/[slug]/__tests__/page.test.tsx +321 -0
- package/templates/apps/web/app/[locale]/pricing/[slug]/page.tsx +87 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/offerings.test.tsx +3 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/pricing-pages.test.tsx +215 -0
- package/templates/apps/web/app/[locale]/pricing/page.tsx +56 -341
- package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +21 -4
- package/templates/apps/web/app/[locale]/products/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +8 -2
- package/templates/apps/web/app/[locale]/products/page.tsx +22 -6
- package/templates/apps/web/app/[locale]/projects/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +4 -0
- package/templates/apps/web/app/[locale]/projects/page.tsx +18 -11
- package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +25 -4
- package/templates/apps/web/app/[locale]/resources/__tests__/hub-children.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +6 -2
- package/templates/apps/web/app/[locale]/resources/page.tsx +20 -10
- package/templates/apps/web/app/[locale]/services/[slug]/[child]/page.tsx +68 -0
- package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +64 -9
- package/templates/apps/web/app/[locale]/services/__tests__/hub-children.test.tsx +158 -0
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +12 -2
- package/templates/apps/web/app/[locale]/services/__tests__/service-detail-sections.test.tsx +224 -0
- package/templates/apps/web/app/[locale]/services/__tests__/service-families.test.tsx +368 -0
- package/templates/apps/web/app/[locale]/services/page.tsx +45 -19
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/team/__tests__/hub-children.test.tsx +32 -0
- package/templates/apps/web/app/[locale]/team/page.tsx +25 -24
- package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +24 -3
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/templates/[slug]/__tests__/page.test.tsx +203 -0
- package/templates/apps/web/app/[locale]/templates/[slug]/page.tsx +81 -0
- package/templates/apps/web/app/[locale]/templates/__tests__/page.test.tsx +237 -0
- package/templates/apps/web/app/[locale]/templates/page.tsx +108 -0
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +24 -3
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +204 -2
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +151 -33
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/__tests__/route.test.ts +233 -7
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +92 -41
- package/templates/apps/web/app/api/offerings/checkout/__tests__/route.test.ts +77 -39
- package/templates/apps/web/app/llms.txt/route.ts +41 -3
- package/templates/apps/web/app/robots.txt/route.ts +5 -1
- package/templates/apps/web/app/sitemap.xml/route.ts +45 -3
- package/templates/apps/web/auth.ts +102 -50
- package/templates/apps/web/components/signed-in-nav.tsx +8 -1
- package/templates/apps/web/lib/__tests__/bot-check.test.ts +254 -0
- package/templates/apps/web/lib/__tests__/consent-visitor.test.ts +212 -13
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +13 -1
- package/templates/apps/web/lib/__tests__/docs-index.test.ts +93 -0
- package/templates/apps/web/lib/__tests__/docs-path.test.ts +168 -0
- package/templates/apps/web/lib/__tests__/hub-children.fixtures.ts +279 -0
- package/templates/apps/web/lib/__tests__/nav-label-keys.test.ts +138 -0
- package/templates/apps/web/lib/__tests__/newsletter.actions.test.ts +178 -2
- package/templates/apps/web/lib/__tests__/offerings-pricing.test.ts +183 -0
- package/templates/apps/web/lib/__tests__/page-chrome.test.ts +412 -0
- package/templates/apps/web/lib/__tests__/pricing-page.test.ts +142 -0
- package/templates/apps/web/lib/__tests__/relay.test.ts +202 -10
- package/templates/apps/web/lib/__tests__/route-family.test.ts +57 -0
- package/templates/apps/web/lib/__tests__/service-listing.test.ts +111 -0
- package/templates/apps/web/lib/__tests__/services-path.test.ts +96 -0
- package/templates/apps/web/lib/__tests__/site-header.test.ts +151 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +148 -6
- package/templates/apps/web/lib/__tests__/telemetry-scrub.test.ts +84 -0
- package/templates/apps/web/lib/__tests__/template-listing.test.ts +158 -0
- package/templates/apps/web/lib/auth-secret.ts +57 -0
- package/templates/apps/web/lib/bot-check.ts +138 -0
- package/templates/apps/web/lib/consent-visitor.ts +81 -7
- package/templates/apps/web/lib/deck-page.ts +6 -3
- package/templates/apps/web/lib/docs-index.ts +100 -0
- package/templates/apps/web/lib/docs-path.ts +94 -0
- package/templates/apps/web/lib/hub-children.tsx +318 -0
- package/templates/apps/web/lib/modules.generated.ts +9 -0
- package/templates/apps/web/lib/nav-label-check.ts +61 -0
- package/templates/apps/web/lib/nav-label-keys.ts +78 -0
- package/templates/apps/web/lib/newsletter.actions.ts +31 -6
- package/templates/apps/web/lib/offerings-pricing.ts +135 -6
- package/templates/apps/web/lib/page-chrome.ts +367 -0
- package/templates/apps/web/lib/posting-title.ts +26 -4
- package/templates/apps/web/lib/pricing-page.ts +430 -0
- package/templates/apps/web/lib/relay.ts +31 -106
- package/templates/apps/web/lib/route-family.ts +69 -0
- package/templates/apps/web/lib/service-listing.ts +89 -0
- package/templates/apps/web/lib/services-path.ts +97 -0
- package/templates/apps/web/lib/site-header.ts +94 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +68 -3
- package/templates/apps/web/lib/template-listing.ts +169 -0
- package/templates/apps/web/messages/en.json +86 -30
- package/templates/apps/web/middleware.ts +171 -11
- package/templates/apps/web/next.config.mjs +211 -2
- package/templates/apps/web/package.json +1 -1
- package/templates/apps/web/scripts/assert-content-bundled.ts +28 -6
- package/templates/apps/web/scripts/check-nav-labels.ts +39 -0
- package/templates/apps/web/sentry.server.config.ts +30 -2
- package/templates/apps/web/src/lib/__tests__/rate-limit.test.ts +67 -9
- package/templates/apps/web/src/lib/rate-limit.ts +68 -27
- package/templates/apps/web/src/lib/routes.ts +15 -1
- package/templates/tools/declared-env.ts +7 -0
- package/versions.json +48 -48
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* See: docs/runbooks/page-renderer-deploy-verification.md §1 Cause 2
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { existsSync } from "node:fs";
|
|
12
|
-
import { join, resolve } from "node:path";
|
|
11
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
12
|
+
import { join, relative, resolve } from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
|
|
15
15
|
// apps/web/ is one level up from scripts/
|
|
@@ -18,10 +18,32 @@ const scriptDir = resolve(fileURLToPath(import.meta.url), "..");
|
|
|
18
18
|
const appWebDir = resolve(scriptDir, "..");
|
|
19
19
|
const standaloneDir = join(appWebDir, ".next", "standalone");
|
|
20
20
|
|
|
21
|
-
// Sentinel file
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
|
|
21
|
+
// Sentinel: the first MDX file under <repo-root>/content on disk — one traced
|
|
22
|
+
// file proves the tracing; it does not count the others. A site that
|
|
23
|
+
// dropped the marketing module (#10040) has no HomePage — only its legal
|
|
24
|
+
// pages — so the sentinel is whichever page this site carries, traced from
|
|
25
|
+
// ../../content/<locale>/<Type>/<page>.mdx (relative to apps/web) and placed
|
|
26
|
+
// at standaloneDir/content/... by Next.js file tracing.
|
|
27
|
+
const contentDir = resolve(appWebDir, "..", "..", "content");
|
|
28
|
+
function firstMdx(dir: string): string | undefined {
|
|
29
|
+
if (!existsSync(dir)) return undefined;
|
|
30
|
+
for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
31
|
+
const abs = join(dir, entry.name);
|
|
32
|
+
if (entry.isDirectory()) {
|
|
33
|
+
const found = firstMdx(abs);
|
|
34
|
+
if (found !== undefined) return found;
|
|
35
|
+
} else if (entry.name.endsWith(".mdx")) {
|
|
36
|
+
return abs;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const sentinelAbs = firstMdx(contentDir);
|
|
42
|
+
if (sentinelAbs === undefined) {
|
|
43
|
+
console.error("[assert-content-bundled] ERROR: no .mdx file under content/ to trace.");
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
const sentinelRelative = join("content", relative(contentDir, sentinelAbs));
|
|
25
47
|
const sentinelPath = join(standaloneDir, sentinelRelative);
|
|
26
48
|
|
|
27
49
|
if (!existsSync(standaloneDir)) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fail the build when a nav key has no text (#10045 D2).
|
|
3
|
+
*
|
|
4
|
+
* Every `labelKey` / `headingKey` / `descKey` in `lib/nav-config.ts`, and the
|
|
5
|
+
* keys the layout resolves by name, must exist in the `nav` namespace of every
|
|
6
|
+
* `messages/<locale>.json` (or its site overlay). A missing key would render
|
|
7
|
+
* as raw text in the header or footer; this check names the key and the file
|
|
8
|
+
* instead. Runs as the first step of `pnpm build`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { resolve } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
findMissingNavLabels,
|
|
16
|
+
formatMissingNavLabels,
|
|
17
|
+
} from "../lib/nav-label-check";
|
|
18
|
+
import { usedNavLabelKeys } from "../lib/nav-label-keys";
|
|
19
|
+
|
|
20
|
+
const appWebDir = resolve(fileURLToPath(import.meta.url), "..", "..");
|
|
21
|
+
const messagesDir = resolve(appWebDir, "messages");
|
|
22
|
+
|
|
23
|
+
const keys = usedNavLabelKeys();
|
|
24
|
+
const missing = findMissingNavLabels(messagesDir, keys);
|
|
25
|
+
|
|
26
|
+
if (missing.length > 0) {
|
|
27
|
+
console.error(
|
|
28
|
+
`[check-nav-labels] ERROR:\n${formatMissingNavLabels(missing)}`,
|
|
29
|
+
);
|
|
30
|
+
console.error(
|
|
31
|
+
"[check-nav-labels] Add each key's text to the `nav` namespace of that file " +
|
|
32
|
+
"(or messages/site/<locale>.json).",
|
|
33
|
+
);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log(
|
|
38
|
+
`[check-nav-labels] ✓ ${keys.size} nav keys resolve in every locale`,
|
|
39
|
+
);
|
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
|
|
13
13
|
import * as Sentry from "@sentry/nextjs";
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
scrubSpanDescription,
|
|
17
|
+
scrubTelemetryQuery,
|
|
18
|
+
scrubTelemetryUrl,
|
|
19
|
+
} from "./lib/telemetry-scrub";
|
|
16
20
|
|
|
17
21
|
// #5613 / security review PR #5639: the report-grant token (?g=<id> on
|
|
18
22
|
// /reports/<slug>, #5562) is a bearer credential for a private customer
|
|
@@ -21,13 +25,29 @@ import { scrubTelemetryUrl } from "./lib/telemetry-scrub";
|
|
|
21
25
|
// header, the transaction name, navigation breadcrumbs, and any URL
|
|
22
26
|
// captured as span/trace data (e.g. `url.full`,
|
|
23
27
|
// `http.request.header.referer`) here too.
|
|
28
|
+
//
|
|
29
|
+
// #9969 D9 (#8329): the Stripe SDK's outgoing requests carry a customer id in
|
|
30
|
+
// their URL and query, so a span's `description` (`<METHOD> <url>`), its raw
|
|
31
|
+
// `url`/`url.path`/`http.target`, and the separate query keys (`http.query`,
|
|
32
|
+
// `url.query`) on spans, the trace and breadcrumbs are scrubbed as well.
|
|
33
|
+
const QUERY_KEYS = ["http.query", "url.query"];
|
|
34
|
+
|
|
35
|
+
function scrubQueryData(data: Record<string, unknown>): void {
|
|
36
|
+
for (const key of QUERY_KEYS) {
|
|
37
|
+
const value = data[key];
|
|
38
|
+
if (typeof value === "string") {
|
|
39
|
+
data[key] = scrubTelemetryQuery(value);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
24
44
|
function scrubSentryEvent<
|
|
25
45
|
T extends {
|
|
26
46
|
request?: { url?: string; headers?: Record<string, string> };
|
|
27
47
|
transaction?: string;
|
|
28
48
|
breadcrumbs?: { category?: string; data?: Record<string, unknown> }[];
|
|
29
49
|
contexts?: { trace?: { data?: Record<string, unknown> } };
|
|
30
|
-
spans?: { data?: Record<string, unknown> }[];
|
|
50
|
+
spans?: { description?: string; data?: Record<string, unknown> }[];
|
|
31
51
|
},
|
|
32
52
|
>(event: T): T {
|
|
33
53
|
if (event.request?.url) {
|
|
@@ -60,6 +80,7 @@ function scrubSentryEvent<
|
|
|
60
80
|
if (typeof url === "string") {
|
|
61
81
|
crumb.data["url"] = scrubTelemetryUrl(url);
|
|
62
82
|
}
|
|
83
|
+
scrubQueryData(crumb.data);
|
|
63
84
|
}
|
|
64
85
|
const scrubSpanData = (data?: Record<string, unknown>): void => {
|
|
65
86
|
if (!data) return;
|
|
@@ -67,15 +88,22 @@ function scrubSentryEvent<
|
|
|
67
88
|
"url.full",
|
|
68
89
|
"http.url",
|
|
69
90
|
"http.request.header.referer",
|
|
91
|
+
"url",
|
|
92
|
+
"url.path",
|
|
93
|
+
"http.target",
|
|
70
94
|
]) {
|
|
71
95
|
const value = data[key];
|
|
72
96
|
if (typeof value === "string") {
|
|
73
97
|
data[key] = scrubTelemetryUrl(value);
|
|
74
98
|
}
|
|
75
99
|
}
|
|
100
|
+
scrubQueryData(data);
|
|
76
101
|
};
|
|
77
102
|
scrubSpanData(event.contexts?.trace?.data);
|
|
78
103
|
for (const span of event.spans ?? []) {
|
|
104
|
+
if (span.description) {
|
|
105
|
+
span.description = scrubSpanDescription(span.description);
|
|
106
|
+
}
|
|
79
107
|
scrubSpanData(span.data);
|
|
80
108
|
}
|
|
81
109
|
return event;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The per-visitor limiter the /contact action runs before it writes a lead
|
|
3
3
|
* (#8747): a fixed window per key, refusing the call past the limit, and a
|
|
4
|
-
* client address read from the platform's own headers
|
|
4
|
+
* client address read from the platform's own headers — on Vercel only, and
|
|
5
|
+
* a shared, wider bucket for a visitor it cannot read (#9967 D1).
|
|
5
6
|
*/
|
|
6
7
|
import {
|
|
7
8
|
__resetRateLimit,
|
|
8
9
|
checkRateLimit,
|
|
9
10
|
clientIpFromHeaders,
|
|
11
|
+
takeVisitorLimit,
|
|
10
12
|
} from "../rate-limit";
|
|
11
13
|
|
|
12
14
|
const WINDOW_MS = 10 * 60 * 1000;
|
|
@@ -15,9 +17,13 @@ function headersOf(values: Record<string, string>): { get(name: string): string
|
|
|
15
17
|
return { get: (name) => values[name] ?? null };
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
const originalVercel = process.env.VERCEL;
|
|
21
|
+
|
|
18
22
|
afterEach(() => {
|
|
19
23
|
__resetRateLimit();
|
|
20
24
|
jest.useRealTimers();
|
|
25
|
+
if (originalVercel === undefined) delete process.env.VERCEL;
|
|
26
|
+
else process.env.VERCEL = originalVercel;
|
|
21
27
|
});
|
|
22
28
|
|
|
23
29
|
describe("checkRateLimit", () => {
|
|
@@ -46,22 +52,74 @@ describe("checkRateLimit", () => {
|
|
|
46
52
|
});
|
|
47
53
|
|
|
48
54
|
describe("clientIpFromHeaders", () => {
|
|
49
|
-
it("
|
|
55
|
+
it("on Vercel, reads x-vercel-forwarded-for, first value", () => {
|
|
56
|
+
process.env.VERCEL = "1";
|
|
50
57
|
expect(
|
|
51
58
|
clientIpFromHeaders(
|
|
52
59
|
headersOf({
|
|
53
|
-
"x-vercel-forwarded-for": "198.51.100.
|
|
60
|
+
"x-vercel-forwarded-for": "198.51.100.1, 10.0.0.1",
|
|
54
61
|
"x-forwarded-for": "203.0.113.5",
|
|
55
62
|
}),
|
|
56
63
|
),
|
|
57
|
-
).toBe("198.51.100.
|
|
64
|
+
).toBe("198.51.100.1");
|
|
58
65
|
});
|
|
59
66
|
|
|
60
|
-
it("
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
);
|
|
64
|
-
expect(clientIpFromHeaders(headersOf({ "x-real-ip": "192.0.2.9" }))).toBe("192.0.2.9");
|
|
67
|
+
it("never keys a client-sent x-forwarded-for or x-real-ip (#9945)", () => {
|
|
68
|
+
process.env.VERCEL = "1";
|
|
69
|
+
expect(clientIpFromHeaders(headersOf({ "x-forwarded-for": "203.0.113.5" }))).toBe("unknown");
|
|
70
|
+
expect(clientIpFromHeaders(headersOf({ "x-real-ip": "192.0.2.9" }))).toBe("unknown");
|
|
65
71
|
expect(clientIpFromHeaders(headersOf({}))).toBe("unknown");
|
|
66
72
|
});
|
|
73
|
+
|
|
74
|
+
it("off Vercel, trusts no header at all", () => {
|
|
75
|
+
delete process.env.VERCEL;
|
|
76
|
+
expect(
|
|
77
|
+
clientIpFromHeaders(headersOf({ "x-vercel-forwarded-for": "198.51.100.1, 10.0.0.1" })),
|
|
78
|
+
).toBe("unknown");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("takeVisitorLimit (#9967 D1)", () => {
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
process.env.VERCEL = "1";
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("refuses past the limit with the window's remaining wait, not the whole window", () => {
|
|
88
|
+
jest.useFakeTimers();
|
|
89
|
+
jest.setSystemTime(new Date("2026-10-26T10:00:00Z"));
|
|
90
|
+
const h = headersOf({ "x-vercel-forwarded-for": "198.51.100.1" });
|
|
91
|
+
expect(takeVisitorLimit("t", h, 2, 60_000)).toEqual({ allowed: true });
|
|
92
|
+
expect(takeVisitorLimit("t", h, 2, 60_000)).toEqual({ allowed: true });
|
|
93
|
+
|
|
94
|
+
jest.setSystemTime(new Date("2026-10-26T10:00:20Z"));
|
|
95
|
+
expect(takeVisitorLimit("t", h, 2, 60_000)).toEqual({ allowed: false, retryAfterSeconds: 40 });
|
|
96
|
+
// Another address keeps its own bucket.
|
|
97
|
+
expect(takeVisitorLimit("t", headersOf({ "x-vercel-forwarded-for": "198.51.100.2" }), 2, 60_000)).toEqual({
|
|
98
|
+
allowed: true,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("charges an unreadable visitor one shared bucket at ten times the limit, never skips it", () => {
|
|
103
|
+
const none = headersOf({ "x-forwarded-for": "203.0.113.5" });
|
|
104
|
+
for (let i = 0; i < 20; i += 1) {
|
|
105
|
+
expect(takeVisitorLimit("t", none, 2, 60_000).allowed).toBe(true);
|
|
106
|
+
}
|
|
107
|
+
expect(takeVisitorLimit("t", headersOf({ "x-forwarded-for": "203.0.113.6" }), 2, 60_000).allowed).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("keeps the shared bucket out of reach of a flood of address keys", () => {
|
|
111
|
+
const none = headersOf({});
|
|
112
|
+
for (let i = 0; i < 20; i += 1) takeVisitorLimit("t", none, 2, 60_000);
|
|
113
|
+
for (let i = 0; i < 5000; i += 1) {
|
|
114
|
+
takeVisitorLimit("t", headersOf({ "x-vercel-forwarded-for": `10.0.${i >> 8}.${i & 255}` }), 2, 60_000);
|
|
115
|
+
}
|
|
116
|
+
expect(takeVisitorLimit("t", none, 2, 60_000).allowed).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("counts each scope's shared bucket on its own", () => {
|
|
120
|
+
const none = headersOf({});
|
|
121
|
+
for (let i = 0; i < 20; i += 1) takeVisitorLimit("a", none, 2, 60_000);
|
|
122
|
+
expect(takeVisitorLimit("a", none, 2, 60_000).allowed).toBe(false);
|
|
123
|
+
expect(takeVisitorLimit("b", none, 2, 60_000).allowed).toBe(true);
|
|
124
|
+
});
|
|
67
125
|
});
|
|
@@ -17,44 +17,64 @@ type Bucket = { count: number; resetAt: number };
|
|
|
17
17
|
|
|
18
18
|
const buckets = new Map<string, Bucket>();
|
|
19
19
|
|
|
20
|
+
// The `<scope>:unknown` buckets (#9967 D1): one per scope, kept apart from
|
|
21
|
+
// `buckets` so no flood of address keys can evict them through its
|
|
22
|
+
// oldest-first bound.
|
|
23
|
+
const unknownBuckets = new Map<string, Bucket>();
|
|
24
|
+
|
|
20
25
|
// Bound the map so a caller rotating keys (spoofed addresses) cannot grow it
|
|
21
26
|
// without limit between resets.
|
|
22
27
|
const MAX_TRACKED_KEYS = 5000;
|
|
23
28
|
|
|
29
|
+
/** A visitor with no readable address shares one bucket this many times as wide. */
|
|
30
|
+
export const UNKNOWN_VISITOR_FACTOR = 10;
|
|
31
|
+
|
|
24
32
|
/** Test-only reset. Not reachable from a request. */
|
|
25
33
|
export function __resetRateLimit(): void {
|
|
26
34
|
buckets.clear();
|
|
35
|
+
unknownBuckets.clear();
|
|
27
36
|
}
|
|
28
37
|
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
/** Count one call in `map`'s fixed window for `key`; the window's end either way. */
|
|
39
|
+
function hitWindow(
|
|
40
|
+
map: Map<string, Bucket>,
|
|
41
|
+
key: string,
|
|
42
|
+
limit: number,
|
|
43
|
+
windowMs: number,
|
|
44
|
+
now: number,
|
|
45
|
+
): { allowed: boolean; resetAt: number } {
|
|
46
|
+
const existing = map.get(key);
|
|
36
47
|
|
|
37
48
|
if (!existing || existing.resetAt <= now) {
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
if (map.size >= MAX_TRACKED_KEYS) pruneExpired(map, now);
|
|
50
|
+
const resetAt = now + windowMs;
|
|
51
|
+
map.set(key, { count: 1, resetAt });
|
|
52
|
+
return { allowed: true, resetAt };
|
|
41
53
|
}
|
|
42
54
|
|
|
43
|
-
if (existing.count >= limit) return false;
|
|
55
|
+
if (existing.count >= limit) return { allowed: false, resetAt: existing.resetAt };
|
|
44
56
|
|
|
45
57
|
existing.count += 1;
|
|
46
|
-
return true;
|
|
58
|
+
return { allowed: true, resetAt: existing.resetAt };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Fixed-window check. `true` when the caller is under `limit` in the current
|
|
63
|
+
* window (and counts the call); `false` once it has used `limit` calls.
|
|
64
|
+
*/
|
|
65
|
+
export function checkRateLimit(key: string, limit: number, windowMs: number): boolean {
|
|
66
|
+
return hitWindow(buckets, key, limit, windowMs, Date.now()).allowed;
|
|
47
67
|
}
|
|
48
68
|
|
|
49
|
-
function pruneExpired(now: number): void {
|
|
50
|
-
for (const [key, bucket] of
|
|
51
|
-
if (bucket.resetAt <= now)
|
|
69
|
+
function pruneExpired(map: Map<string, Bucket>, now: number): void {
|
|
70
|
+
for (const [key, bucket] of map) {
|
|
71
|
+
if (bucket.resetAt <= now) map.delete(key);
|
|
52
72
|
}
|
|
53
73
|
// Nothing expired (keys rotated inside one window): evict the oldest entry
|
|
54
74
|
// — `Map` iterates in insertion order — so the bound always holds.
|
|
55
|
-
if (
|
|
56
|
-
const oldestKey =
|
|
57
|
-
if (oldestKey !== undefined)
|
|
75
|
+
if (map.size >= MAX_TRACKED_KEYS) {
|
|
76
|
+
const oldestKey = map.keys().next().value;
|
|
77
|
+
if (oldestKey !== undefined) map.delete(oldestKey);
|
|
58
78
|
}
|
|
59
79
|
}
|
|
60
80
|
|
|
@@ -64,16 +84,37 @@ export interface HeaderReader {
|
|
|
64
84
|
}
|
|
65
85
|
|
|
66
86
|
/**
|
|
67
|
-
* The visitor's address, coarse: `
|
|
68
|
-
* `x-forwarded-for`,
|
|
69
|
-
* `"unknown"
|
|
70
|
-
*
|
|
87
|
+
* The visitor's address, coarse: on Vercel (`VERCEL=1`), the first value of
|
|
88
|
+
* `x-vercel-forwarded-for`, which Vercel's edge overwrites on every request;
|
|
89
|
+
* else `"unknown"` (#9967 D1). `x-forwarded-for` and `x-real-ip` are never
|
|
90
|
+
* read, and off Vercel no header is, since a client can set any of them. An
|
|
71
91
|
* abuse-throttling key only — never an authorization decision.
|
|
72
92
|
*/
|
|
73
93
|
export function clientIpFromHeaders(headers: HeaderReader): string {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
94
|
+
if (process.env.VERCEL !== "1") return "unknown";
|
|
95
|
+
return headers.get("x-vercel-forwarded-for")?.split(",")[0]?.trim() || "unknown";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export type VisitorLimit = { allowed: true } | { allowed: false; retryAfterSeconds: number };
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The per-visitor limit (#9967 D1): `limit` calls per window on
|
|
102
|
+
* `<scope>:<address>`, or — for a visitor whose address cannot be read —
|
|
103
|
+
* `UNKNOWN_VISITOR_FACTOR * limit` on one shared `<scope>:unknown` bucket,
|
|
104
|
+
* never a skip. A refusal names the seconds left in the window, at least 1.
|
|
105
|
+
*/
|
|
106
|
+
export function takeVisitorLimit(
|
|
107
|
+
scope: string,
|
|
108
|
+
headers: HeaderReader,
|
|
109
|
+
limit: number,
|
|
110
|
+
windowMs: number,
|
|
111
|
+
): VisitorLimit {
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
const address = clientIpFromHeaders(headers);
|
|
114
|
+
const { allowed, resetAt } =
|
|
115
|
+
address === "unknown"
|
|
116
|
+
? hitWindow(unknownBuckets, `${scope}:unknown`, UNKNOWN_VISITOR_FACTOR * limit, windowMs, now)
|
|
117
|
+
: hitWindow(buckets, `${scope}:${address}`, limit, windowMs, now);
|
|
118
|
+
if (allowed) return { allowed: true };
|
|
119
|
+
return { allowed: false, retryAfterSeconds: Math.max(1, Math.ceil((resetAt - now) / 1000)) };
|
|
79
120
|
}
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* handlers to avoid copy-paste duplication.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { routeFor } from "@working-theory/templates/routes";
|
|
8
|
+
import { pagePath, routeFor } from "@working-theory/templates/routes";
|
|
9
9
|
|
|
10
|
+
import { SERVICES_PATH } from "../../lib/services-path";
|
|
10
11
|
import { SITE_BASE_URL } from "../../lib/site-brand";
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -43,3 +44,16 @@ export function resolveBaseUrl(): string {
|
|
|
43
44
|
* depend on, even though `routeFor`'s own `slug` parameter is optional.
|
|
44
45
|
*/
|
|
45
46
|
export const slugToPath: (type: string, slug: string) => string = routeFor;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* One listed page's public path (#10043 D9): `slugToPath`, except that the
|
|
50
|
+
* services live at the site's base and a `Service` with a `parent` sits under
|
|
51
|
+
* its category. Drives /sitemap.xml and /llms.txt.
|
|
52
|
+
*/
|
|
53
|
+
export function pagePathFor(page: {
|
|
54
|
+
type: string;
|
|
55
|
+
slug: string;
|
|
56
|
+
parent?: string | undefined;
|
|
57
|
+
}): string {
|
|
58
|
+
return pagePath(page, SERVICES_PATH);
|
|
59
|
+
}
|
|
@@ -1025,6 +1025,12 @@ export interface PlanEnvRowsOptions {
|
|
|
1025
1025
|
* with no reader and so no `scope`; a platform or refused name is skipped.
|
|
1026
1026
|
*/
|
|
1027
1027
|
declare?: readonly string[];
|
|
1028
|
+
/**
|
|
1029
|
+
* When set, only reads in these site-relative files get a row (#10173):
|
|
1030
|
+
* `update` declares what the files it wrote read, never a name only the
|
|
1031
|
+
* site's own code reads.
|
|
1032
|
+
*/
|
|
1033
|
+
onlyFiles?: ReadonlySet<string>;
|
|
1028
1034
|
}
|
|
1029
1035
|
|
|
1030
1036
|
/** The header the writer puts above a block it creates. */
|
|
@@ -1066,6 +1072,7 @@ export function planEnvRows(
|
|
|
1066
1072
|
const refused = new Set(opts.topology === "spoke" ? SPOKE_REFUSED_ENV : []);
|
|
1067
1073
|
const files = new Map<string, string[]>();
|
|
1068
1074
|
for (const read of collectSiteReads(root).reads) {
|
|
1075
|
+
if (opts.onlyFiles !== undefined && !opts.onlyFiles.has(read.file)) continue;
|
|
1069
1076
|
if (isPlatformEnvName(read.name) || refused.has(read.name)) continue;
|
|
1070
1077
|
if (declared.has(read.name)) continue;
|
|
1071
1078
|
const list = files.get(read.name) ?? [];
|
package/versions.json
CHANGED
|
@@ -1,66 +1,66 @@
|
|
|
1
1
|
{
|
|
2
|
-
"@working-theory/admin-ui": "0.
|
|
3
|
-
"@working-theory/aeo": "0.
|
|
4
|
-
"@working-theory/agent-context": "0.
|
|
2
|
+
"@working-theory/admin-ui": "0.50.0",
|
|
3
|
+
"@working-theory/aeo": "0.50.0",
|
|
4
|
+
"@working-theory/agent-context": "0.50.0",
|
|
5
5
|
"@working-theory/agent-contract": "0.1.0",
|
|
6
6
|
"@working-theory/agent-memory": "0.0.0",
|
|
7
7
|
"@working-theory/ai": "0.1.0",
|
|
8
|
-
"@working-theory/analytics": "0.
|
|
9
|
-
"@working-theory/api": "0.
|
|
10
|
-
"@working-theory/api-client": "0.
|
|
8
|
+
"@working-theory/analytics": "0.50.0",
|
|
9
|
+
"@working-theory/api": "0.50.0",
|
|
10
|
+
"@working-theory/api-client": "0.50.0",
|
|
11
11
|
"@working-theory/cda-filesystem": "0.0.0",
|
|
12
|
-
"@working-theory/cli": "0.
|
|
13
|
-
"@working-theory/common": "0.
|
|
12
|
+
"@working-theory/cli": "0.50.0",
|
|
13
|
+
"@working-theory/common": "0.50.0",
|
|
14
14
|
"@working-theory/consent": "0.1.0",
|
|
15
|
-
"@working-theory/content-adapters": "0.
|
|
16
|
-
"@working-theory/content-projection": "0.
|
|
17
|
-
"@working-theory/email": "0.
|
|
15
|
+
"@working-theory/content-adapters": "0.50.0",
|
|
16
|
+
"@working-theory/content-projection": "0.50.0",
|
|
17
|
+
"@working-theory/email": "0.50.0",
|
|
18
18
|
"@working-theory/env": "0.0.0",
|
|
19
|
-
"@working-theory/eslint-config": "0.
|
|
20
|
-
"@working-theory/fumadocs-schema": "0.
|
|
21
|
-
"@working-theory/ids": "0.16.
|
|
22
|
-
"@working-theory/jsonld": "0.
|
|
23
|
-
"@working-theory/license": "0.
|
|
24
|
-
"@working-theory/mcp-server": "0.
|
|
25
|
-
"@working-theory/metadata": "0.
|
|
26
|
-
"@working-theory/observability": "0.
|
|
27
|
-
"@working-theory/page-renderer": "0.
|
|
19
|
+
"@working-theory/eslint-config": "0.50.0",
|
|
20
|
+
"@working-theory/fumadocs-schema": "0.50.0",
|
|
21
|
+
"@working-theory/ids": "0.16.1",
|
|
22
|
+
"@working-theory/jsonld": "0.50.0",
|
|
23
|
+
"@working-theory/license": "0.50.0",
|
|
24
|
+
"@working-theory/mcp-server": "0.50.0",
|
|
25
|
+
"@working-theory/metadata": "0.50.0",
|
|
26
|
+
"@working-theory/observability": "0.50.0",
|
|
27
|
+
"@working-theory/page-renderer": "0.50.0",
|
|
28
28
|
"@working-theory/plugin-mcp": "0.1.3",
|
|
29
|
-
"@working-theory/schema": "0.
|
|
30
|
-
"@working-theory/schema-pack-registry": "0.
|
|
31
|
-
"@working-theory/schema-validator": "0.
|
|
32
|
-
"@working-theory/sections-registry": "0.
|
|
33
|
-
"@working-theory/site-config": "0.
|
|
34
|
-
"@working-theory/spoke-checkout": "0.7.
|
|
35
|
-
"@working-theory/stripe-client": "0.
|
|
36
|
-
"@working-theory/templates": "0.
|
|
37
|
-
"@working-theory/theme": "0.
|
|
38
|
-
"@working-theory/ui": "0.
|
|
39
|
-
"@working-theory/validation": "0.
|
|
40
|
-
"@working-theory/web-i18n": "0.
|
|
41
|
-
"@working-theory/admin": "0.
|
|
42
|
-
"@working-theory/api-commerce": "0.1.
|
|
43
|
-
"@working-theory/api-content-write": "0.1.
|
|
44
|
-
"@working-theory/api-hub": "0.3.
|
|
45
|
-
"@working-theory/api-multi-tenant": "0.1.
|
|
46
|
-
"@working-theory/api-scheduling": "0.4.
|
|
47
|
-
"@working-theory/api-signal-collectors": "0.2.
|
|
48
|
-
"@working-theory/api-workspace": "0.
|
|
29
|
+
"@working-theory/schema": "0.50.0",
|
|
30
|
+
"@working-theory/schema-pack-registry": "0.50.0",
|
|
31
|
+
"@working-theory/schema-validator": "0.50.0",
|
|
32
|
+
"@working-theory/sections-registry": "0.50.0",
|
|
33
|
+
"@working-theory/site-config": "0.50.0",
|
|
34
|
+
"@working-theory/spoke-checkout": "0.7.6",
|
|
35
|
+
"@working-theory/stripe-client": "0.50.0",
|
|
36
|
+
"@working-theory/templates": "0.50.0",
|
|
37
|
+
"@working-theory/theme": "0.50.0",
|
|
38
|
+
"@working-theory/ui": "0.50.0",
|
|
39
|
+
"@working-theory/validation": "0.50.0",
|
|
40
|
+
"@working-theory/web-i18n": "0.50.0",
|
|
41
|
+
"@working-theory/admin": "0.7.1",
|
|
42
|
+
"@working-theory/api-commerce": "0.1.4",
|
|
43
|
+
"@working-theory/api-content-write": "0.1.4",
|
|
44
|
+
"@working-theory/api-hub": "0.3.1",
|
|
45
|
+
"@working-theory/api-multi-tenant": "0.1.4",
|
|
46
|
+
"@working-theory/api-scheduling": "0.4.1",
|
|
47
|
+
"@working-theory/api-signal-collectors": "0.2.3",
|
|
48
|
+
"@working-theory/api-workspace": "0.3.0",
|
|
49
49
|
"@working-theory/billing-client": "0.0.0",
|
|
50
50
|
"@working-theory/branding": "0.0.0",
|
|
51
51
|
"@working-theory/license-client": "0.0.0",
|
|
52
52
|
"@working-theory/license-manifest": "0.0.0",
|
|
53
|
-
"@working-theory/mcp-scheduling": "0.3.
|
|
54
|
-
"@working-theory/mcp-signal-collectors": "0.2.
|
|
55
|
-
"@working-theory/mcp-workspace": "0.3.
|
|
53
|
+
"@working-theory/mcp-scheduling": "0.3.3",
|
|
54
|
+
"@working-theory/mcp-signal-collectors": "0.2.4",
|
|
55
|
+
"@working-theory/mcp-workspace": "0.3.3",
|
|
56
56
|
"@working-theory/partner-session": "0.0.0",
|
|
57
57
|
"@working-theory/preview-protocol": "0.0.0",
|
|
58
|
-
"@working-theory/ui-commerce": "0.23.
|
|
59
|
-
"@working-theory/ui-docs-blog": "0.23.
|
|
60
|
-
"@working-theory/ui-scheduling": "0.25.
|
|
61
|
-
"@working-theory/ui-signal-collectors": "0.27.
|
|
58
|
+
"@working-theory/ui-commerce": "0.23.28",
|
|
59
|
+
"@working-theory/ui-docs-blog": "0.23.28",
|
|
60
|
+
"@working-theory/ui-scheduling": "0.25.2",
|
|
61
|
+
"@working-theory/ui-signal-collectors": "0.27.6",
|
|
62
62
|
"@working-theory/web": "0.0.0",
|
|
63
63
|
"@working-theory/app-api": "1.0.0",
|
|
64
64
|
"@working-theory/mcp": "0.0.0",
|
|
65
|
-
"@working-theory/database": "0.
|
|
65
|
+
"@working-theory/database": "0.50.0"
|
|
66
66
|
}
|