create-ailk 0.6.0 → 0.7.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/README.md +44 -5
- package/component-catalog.json +146 -21
- package/component-catalog.md +9 -6
- package/dist/add-module-core.d.ts +22 -0
- package/dist/add-module-core.js +50 -0
- package/dist/add-module.d.ts +89 -0
- package/dist/add-module.js +334 -0
- package/dist/category-profile.d.ts +126 -0
- package/dist/category-profile.js +262 -0
- package/dist/cli.js +124 -13
- package/dist/copy.d.ts +14 -0
- package/dist/copy.js +38 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +6 -1
- package/dist/lib/extract-module-bundle.d.ts +76 -2
- package/dist/lib/extract-module-bundle.js +219 -8
- package/dist/lib/license-key-files.d.ts +15 -0
- package/dist/lib/license-key-files.js +102 -0
- package/dist/lib/module-license-gate.d.ts +6 -1
- package/dist/lib/module-license-gate.js +6 -1
- package/dist/lib/pro-callout.d.ts +16 -10
- package/dist/lib/pro-callout.js +27 -4
- package/dist/license-set.d.ts +34 -0
- package/dist/license-set.js +194 -0
- package/dist/module-architecture.d.ts +20 -0
- package/dist/module-architecture.js +77 -19
- package/dist/modules/commerce/0.6.0.tar.gz +0 -0
- package/dist/modules/commerce/0.6.1.tar.gz +0 -0
- package/dist/modules/docs-blog/0.6.0.tar.gz +0 -0
- package/dist/modules/docs-blog/0.6.1.tar.gz +0 -0
- package/dist/modules/marketing-site/0.6.1.tar.gz +0 -0
- package/dist/modules/minimalism-site/0.6.1.tar.gz +0 -0
- package/dist/modules/multi-tenant/0.6.0.tar.gz +0 -0
- package/dist/modules/multi-tenant/0.6.1.tar.gz +0 -0
- package/dist/modules/saas-product/0.6.1.tar.gz +0 -0
- package/dist/modules/scheduling/0.6.0.tar.gz +0 -0
- package/dist/modules/scheduling/0.6.1.tar.gz +0 -0
- package/dist/modules/signal-collectors/0.6.0.tar.gz +0 -0
- package/dist/modules/signal-collectors/0.6.1.tar.gz +0 -0
- package/dist/modules/simple-service/0.6.1.tar.gz +0 -0
- package/dist/modules/workspace/0.6.0.tar.gz +0 -0
- package/dist/modules/workspace/0.6.1.tar.gz +0 -0
- package/dist/parse-args.d.ts +7 -0
- package/dist/parse-args.js +23 -1
- package/dist/programmatic.d.ts +46 -0
- package/dist/programmatic.js +144 -19
- package/dist/site-chrome.d.ts +18 -7
- package/dist/site-chrome.js +32 -14
- package/dist/site-roster.d.ts +127 -0
- package/dist/site-roster.js +494 -0
- package/dist/surfaces.d.ts +20 -7
- package/dist/surfaces.js +149 -28
- package/dist/sync-routes.d.ts +9 -0
- package/dist/sync-routes.js +2 -2
- package/dist/template-manifest.d.ts +58 -2
- package/dist/template-manifest.js +196 -69
- package/package.json +6 -4
- package/site-templates/_base/content/en/BookCall/book-call.mdx +53 -0
- package/site-templates/_base/content/en/GlossaryList/glossary.mdx +66 -0
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +68 -0
- package/site-templates/_base/content/en/Pricing/pricing.mdx +71 -62
- package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +2 -3
- package/site-templates/_base/content/en/TermsPage/terms.mdx +2 -3
- package/site-templates/_base/content/en/UseCase/first-use-case.mdx +23 -0
- package/site-templates/_base/content/en/UseCase/second-use-case.mdx +23 -0
- package/site-templates/_base/content/en/UseCase/third-use-case.mdx +23 -0
- package/site-templates/designs/minimalism/DESIGN.md +162 -0
- package/site-templates/designs/standard/DESIGN.md +162 -0
- package/site-templates/marketing-site/DESIGN.md +162 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +312 -57
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +209 -90
- package/site-templates/marketing-site/ailk-template.json +1 -0
- package/site-templates/marketing-site-free/DESIGN.md +162 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +260 -66
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +294 -114
- package/site-templates/marketing-site-free/ailk-template.json +3 -2
- package/site-templates/minimalism-site/DESIGN.md +162 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +246 -19
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +185 -51
- package/site-templates/minimalism-site/ailk-template.json +3 -2
- package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +53 -0
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +3 -37
- package/site-templates/minimalism-site/content/en/PurchasePolicyPage/purchase-policy.mdx +124 -0
- package/site-templates/saas-product/DESIGN.md +162 -0
- package/site-templates/simple-service/DESIGN.md +162 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.json +135 -26
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +114 -59
- package/site-templates/simple-service/ailk-template.json +4 -3
- package/site-templates/simple-service/content/en/BookCall/book-call.mdx +53 -0
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +1 -36
- package/site-templates/simple-service/content/en/PurchasePolicyPage/purchase-policy.mdx +124 -0
- package/templates/.claude/settings.json +15 -0
- package/templates/.claude/skills/README.md +8 -8
- package/templates/.claude/skills/{scaffold-auth → add-auth}/SKILL.md +7 -7
- package/templates/.claude/skills/{scaffold-auth → add-auth}/references/env-vars.md +2 -2
- package/templates/.claude/skills/{scaffold-auth → add-auth}/references/oauth-app-setup.md +3 -3
- package/templates/.claude/skills/{scaffold-page → add-page}/SKILL.md +76 -62
- package/templates/.claude/skills/add-page/references/page-types.md +111 -0
- package/templates/.claude/skills/{scaffold-page → add-page}/templates/content.mdx.tmpl +4 -4
- package/templates/.claude/skills/{scaffold-page → add-page}/templates/route.tsx.tmpl +1 -1
- package/templates/.claude/skills/{audit-site → check-site}/SKILL.md +16 -17
- package/templates/.claude/skills/{audit-site → check-site}/references/common-errors.md +3 -3
- package/templates/.claude/skills/{audit-site → check-site}/references/findings-glossary.md +16 -14
- package/templates/.claude/skills/{audit-site → check-site}/references/severity-levels.md +1 -1
- package/templates/.claude/skills/fill-page/SKILL.md +180 -0
- package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +80 -0
- package/templates/.claude/skills/fill-page/references/section-sources.md +41 -0
- package/templates/.claude/skills/{provision-config → setup-deploy}/SKILL.md +8 -8
- package/templates/.claude/skills/{provision-config → setup-deploy}/references/env-vars-template.md +2 -2
- package/templates/.claude/skills/{provision-config → setup-deploy}/references/runbook-template.md +8 -8
- package/templates/.claude/skills/{provision-config → setup-deploy}/references/vercel-json-template.md +1 -1
- package/templates/.claude/skills/upgrade-plan/SKILL.md +117 -0
- package/templates/.claude/skills/upgrade-plan/references/options.md +24 -0
- package/templates/.claude/skills/upgrade-plan/scripts/upgrade-plan.ts +527 -0
- package/templates/.github/workflows/ci.yml +5 -0
- package/templates/CLAUDE.md +10 -5
- package/templates/DESIGN.md +162 -0
- package/templates/README.md +134 -38
- package/templates/apps/api/.env.example +2 -1
- package/templates/apps/api/package.json +1 -1
- package/templates/apps/api/src/config/__tests__/index.test.ts +37 -2
- package/templates/apps/api/src/config/index.ts +19 -6
- package/templates/apps/api/src/lib/license-key-store.ts +45 -0
- package/templates/apps/api/src/lib/site.ts +5 -3
- package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +97 -0
- package/templates/apps/api/src/middleware/auth.ts +9 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +127 -5
- package/templates/apps/api/src/routes/content/get-page.ts +86 -2
- package/templates/apps/api/src/server.ts +6 -0
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +75 -1
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +57 -9
- package/templates/apps/mcp/package.json +2 -1
- package/templates/apps/mcp/src/index.ts +7 -0
- package/templates/apps/mcp/src/lib/audit-single-page.ts +36 -10
- package/templates/apps/mcp/src/lib/license-key-store.ts +45 -0
- package/templates/apps/web/PRODUCT.md +86 -0
- package/templates/apps/web/__tests__/setup.ts +18 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +10 -2
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +2 -2
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +10 -4
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +22 -14
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +26 -18
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +12 -4
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +35 -4
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +49 -9
- package/templates/apps/web/app/[locale]/book-a-call/page.tsx +50 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +94 -33
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +53 -23
- package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +96 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +84 -2
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +5 -6
- package/templates/apps/web/app/[locale]/contact/page.tsx +25 -7
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +4 -5
- package/templates/apps/web/app/[locale]/glossary/page.tsx +50 -0
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/how-to/page.tsx +25 -15
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/layout.tsx +113 -51
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +50 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +4 -5
- package/templates/apps/web/app/[locale]/{shipping → purchase-policy}/page.tsx +15 -10
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +4 -5
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +14 -1
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/software/page.tsx +25 -15
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/tech-articles/page.tsx +25 -15
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/use-cases/[slug]/page.tsx +48 -0
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +3 -4
- package/templates/apps/web/app/[locale]/use-cases/page.tsx +15 -2
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/whitepapers/page.tsx +25 -15
- package/templates/apps/web/app/error-fallback.tsx +17 -45
- package/templates/apps/web/app/not-found.tsx +14 -38
- package/templates/apps/web/app/og/route.tsx +6 -0
- package/templates/apps/web/eslint.config.mjs +3 -0
- package/templates/apps/web/instrumentation.ts +14 -0
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +135 -20
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +38 -0
- package/templates/apps/web/lib/license-key-store.ts +52 -0
- package/templates/apps/web/lib/nav-config.ts +64 -12
- package/templates/apps/web/lib/prisma-for-auth.ts +9 -6
- package/templates/apps/web/lib/site-brand.tsx +4 -8
- package/templates/apps/web/lib/site-chrome.ts +3 -3
- package/templates/apps/web/lib/site-identity.ts +134 -0
- package/templates/apps/web/lib/site-theme.ts +11 -0
- package/templates/apps/web/messages/en.json +52 -37
- package/templates/apps/web/middleware.ts +1 -1
- package/templates/apps/web/public/android-chrome-192x192.png +0 -0
- package/templates/apps/web/public/android-chrome-512x512.png +0 -0
- package/templates/apps/web/public/apple-touch-icon.png +0 -0
- package/templates/apps/web/public/favicon-16x16.png +0 -0
- package/templates/apps/web/public/favicon-32x32.png +0 -0
- package/templates/apps/web/public/favicon.ico +0 -0
- package/templates/apps/web/public/favicon.svg +3 -3
- package/templates/apps/web/public/logomark.svg +6 -3
- package/templates/apps/web/public/logos/ai-launch-kit-light.svg +4 -0
- package/templates/apps/web/public/logos/ai-launch-kit.svg +4 -0
- package/templates/apps/web/public/logos/atomic-agent-light.svg +4 -0
- package/templates/apps/web/public/logos/atomic-agent.svg +4 -0
- package/templates/apps/web/public/logos/salessmyth-light.svg +4 -0
- package/templates/apps/web/public/logos/salessmyth.svg +4 -0
- package/templates/apps/web/public/site.webmanifest +4 -4
- package/templates/apps/web/public/wordmark-light.svg +4 -0
- package/templates/apps/web/public/wordmark.svg +3 -3
- package/templates/apps/web/security-headers.config.ts +5 -1
- package/templates/apps/web/src/lib/commerce-sections.ts +35 -3
- package/templates/content/_site.mdx +41 -3
- package/templates/content/en/AboutPage/about.mdx +150 -144
- package/templates/content/en/BlogList/blog.mdx +105 -0
- package/templates/content/en/ContactPage/contact.mdx +44 -85
- package/templates/content/en/FAQPage/faq.mdx +86 -57
- package/templates/content/en/HomePage/home.mdx +276 -217
- package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +182 -0
- package/templates/content/en/SoftwareProduct/product.mdx +175 -0
- package/templates/content/en/UseCasesList/use-cases.mdx +170 -0
- package/templates/database/CHANGELOG.md +14 -0
- package/templates/database/package.json +3 -2
- package/templates/package.json +3 -1
- package/templates/tools/lint-architecture.ts +315 -0
- package/templates/tools/tsconfig.json +14 -0
- package/versions.json +33 -33
- package/templates/.claude/skills/add-schema/SKILL.md +0 -277
- package/templates/.claude/skills/add-schema/references/conflict-handling.md +0 -200
- package/templates/.claude/skills/add-schema/references/file-finding.md +0 -123
- package/templates/.claude/skills/add-schema/references/schema-types.md +0 -234
- package/templates/.claude/skills/scaffold-page/references/page-types.md +0 -169
- package/templates/.claude/skills/suggest-site-pages/SKILL.md +0 -242
- package/templates/.claude/skills/suggest-site-pages/references/categories.md +0 -287
- package/templates/apps/web/app/[locale]/cancellation/page.tsx +0 -44
- package/templates/apps/web/app/[locale]/returns/page.tsx +0 -44
- package/templates/apps/web/public/logo-dark.svg +0 -21
- package/templates/apps/web/public/logo-light.svg +0 -21
- package/templates/content/en/CancellationPolicyPage/cancellation.mdx +0 -108
- package/templates/content/en/LandingPage/blog.mdx +0 -83
- package/templates/content/en/LandingPage/features.mdx +0 -103
- package/templates/content/en/LandingPage/how-it-works.mdx +0 -143
- package/templates/content/en/ReturnsPolicyPage/returns.mdx +0 -108
- package/templates/content/en/ShippingPolicyPage/shipping.mdx +0 -108
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file auth.database-not-configured.test.ts
|
|
3
|
+
*
|
|
4
|
+
* DATABASE_URL unset → 503 database_not_configured (#6482).
|
|
5
|
+
*
|
|
6
|
+
* Outside production apps/api boots database-less. The preHandler must say
|
|
7
|
+
* so before it reads any credential: placed after the lookups, the existing
|
|
8
|
+
* `.catch(() => undefined)` would turn the connection failure into a
|
|
9
|
+
* misleading 401.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import Fastify from "fastify";
|
|
13
|
+
import type { FastifyInstance } from "fastify";
|
|
14
|
+
|
|
15
|
+
// eslint-disable-next-line no-restricted-syntax -- jest mock factory must be hoisted
|
|
16
|
+
jest.mock("../../lib/prisma.js", () => ({
|
|
17
|
+
prisma: {
|
|
18
|
+
session: { findUnique: jest.fn() },
|
|
19
|
+
apiToken: {
|
|
20
|
+
findUnique: jest.fn(),
|
|
21
|
+
update: jest.fn().mockResolvedValue(undefined),
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
import { prisma } from "../../lib/prisma.js";
|
|
27
|
+
import { authPreHandlerPlugin } from "../auth.js";
|
|
28
|
+
|
|
29
|
+
const echoHandler = jest.fn(async () => ({ ok: true }));
|
|
30
|
+
|
|
31
|
+
function buildApp(): FastifyInstance {
|
|
32
|
+
const app = Fastify();
|
|
33
|
+
app.register(authPreHandlerPlugin, {
|
|
34
|
+
frontendOrigin: "https://app.example.com",
|
|
35
|
+
});
|
|
36
|
+
app.get("/echo", echoHandler);
|
|
37
|
+
app.post("/echo", echoHandler);
|
|
38
|
+
return app;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("auth preHandler — DATABASE_URL not configured", () => {
|
|
42
|
+
const originalDatabaseUrl = process.env.DATABASE_URL;
|
|
43
|
+
let app: FastifyInstance;
|
|
44
|
+
|
|
45
|
+
beforeEach(async () => {
|
|
46
|
+
jest.clearAllMocks();
|
|
47
|
+
delete process.env.DATABASE_URL;
|
|
48
|
+
app = buildApp();
|
|
49
|
+
await app.ready();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(async () => {
|
|
53
|
+
await app.close();
|
|
54
|
+
if (originalDatabaseUrl === undefined) {
|
|
55
|
+
delete process.env.DATABASE_URL;
|
|
56
|
+
} else {
|
|
57
|
+
process.env.DATABASE_URL = originalDatabaseUrl;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it.each([
|
|
62
|
+
["no credentials", "GET", {}],
|
|
63
|
+
["a bearer token", "GET", { authorization: "Bearer ailk_abc123" }],
|
|
64
|
+
["a session cookie", "GET", { cookie: "better-auth.session_token=tok" }],
|
|
65
|
+
[
|
|
66
|
+
"a non-GET with a session cookie",
|
|
67
|
+
"POST",
|
|
68
|
+
{
|
|
69
|
+
cookie: "better-auth.session_token=tok",
|
|
70
|
+
origin: "https://app.example.com",
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
] as const)(
|
|
74
|
+
'returns 503 { error: "database_not_configured" } for %s without any lookup',
|
|
75
|
+
async (_label, method, headers) => {
|
|
76
|
+
const res = await app.inject({ method, url: "/echo", headers });
|
|
77
|
+
|
|
78
|
+
expect(res.statusCode).toBe(503);
|
|
79
|
+
expect(JSON.parse(res.body)).toEqual({
|
|
80
|
+
error: "database_not_configured",
|
|
81
|
+
});
|
|
82
|
+
expect(prisma.session.findUnique).not.toHaveBeenCalled();
|
|
83
|
+
expect(prisma.apiToken.findUnique).not.toHaveBeenCalled();
|
|
84
|
+
expect(echoHandler).not.toHaveBeenCalled();
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
it("control: with DATABASE_URL set and no credentials, answers 401 unauthorized", async () => {
|
|
89
|
+
process.env.DATABASE_URL = "postgresql://test:test@localhost/test";
|
|
90
|
+
|
|
91
|
+
const res = await app.inject({ method: "GET", url: "/echo" });
|
|
92
|
+
|
|
93
|
+
expect(res.statusCode).toBe(401);
|
|
94
|
+
expect(JSON.parse(res.body)).toEqual({ error: "unauthorized" });
|
|
95
|
+
expect(echoHandler).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* - Both modes invalid → 401 unauthorized.
|
|
23
23
|
* - Cookie expired + bearer valid → 401 (no silent fallback).
|
|
24
24
|
* - Cookie absent, bearer malformed → 401 (no silent fallback).
|
|
25
|
+
* - DATABASE_URL unset → 503 database_not_configured (#6482).
|
|
25
26
|
*
|
|
26
27
|
* Public routes (health, content, leads, webhooks, openapi) are NOT registered
|
|
27
28
|
* with this preHandler — server.ts narrows scope via a sub-plugin.
|
|
@@ -48,6 +49,7 @@ const COOKIE_NAMES = [
|
|
|
48
49
|
] as const;
|
|
49
50
|
|
|
50
51
|
const UNAUTHORIZED = { error: "unauthorized" } as const;
|
|
52
|
+
const DATABASE_NOT_CONFIGURED = { error: "database_not_configured" } as const;
|
|
51
53
|
|
|
52
54
|
declare module "fastify" {
|
|
53
55
|
interface FastifyRequest {
|
|
@@ -201,6 +203,13 @@ export function makeAuthPreHandler(opts: AuthPreHandlerOptions = {}) {
|
|
|
201
203
|
request: FastifyRequest,
|
|
202
204
|
reply: FastifyReply,
|
|
203
205
|
): Promise<void> {
|
|
206
|
+
// A database-less dev boot (#6482). First, and read per request: after
|
|
207
|
+
// the lookups below, their `.catch` would already have answered 401.
|
|
208
|
+
if (!process.env.DATABASE_URL) {
|
|
209
|
+
reply.code(503).send(DATABASE_NOT_CONFIGURED);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
204
213
|
const now = getClock().now();
|
|
205
214
|
const cookieToken = readSessionCookie(request);
|
|
206
215
|
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* No internal module mocks — the route exercises the real adapter stack.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { writeFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
9
12
|
import type { FastifyInstance } from "fastify";
|
|
10
13
|
|
|
11
14
|
import { setupContentFixture } from "../../../__tests__/factories/content-fixture";
|
|
@@ -17,6 +20,30 @@ import { contentRoutes } from "../index";
|
|
|
17
20
|
|
|
18
21
|
const fixture = setupContentFixture();
|
|
19
22
|
|
|
23
|
+
/**
|
|
24
|
+
* A `_site.mdx` in the fixture's content root — an Organization identity at
|
|
25
|
+
* `https://example.com` (#6597). `logo` adds an identity-declared logo.
|
|
26
|
+
*/
|
|
27
|
+
function writeSiteMdx(root: string, opts: { logo?: string } = {}): void {
|
|
28
|
+
writeFileSync(
|
|
29
|
+
join(root, "_site.mdx"),
|
|
30
|
+
[
|
|
31
|
+
"---",
|
|
32
|
+
'version: "1"',
|
|
33
|
+
"siteName: Fixture Site",
|
|
34
|
+
"baseUrl: https://example.com",
|
|
35
|
+
"defaultLocale: en",
|
|
36
|
+
"siteIdentity:",
|
|
37
|
+
" kind: Organization",
|
|
38
|
+
" name: Fixture Co",
|
|
39
|
+
" url: https://example.com",
|
|
40
|
+
...(opts.logo !== undefined ? [` logo: ${opts.logo}`] : []),
|
|
41
|
+
"---",
|
|
42
|
+
"",
|
|
43
|
+
].join("\n"),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
20
47
|
describe("GET /api/content/:type/:slug", () => {
|
|
21
48
|
let app: FastifyInstance;
|
|
22
49
|
|
|
@@ -31,6 +58,7 @@ describe("GET /api/content/:type/:slug", () => {
|
|
|
31
58
|
|
|
32
59
|
it("returns the page with resolved JSON-LD at the document root", async () => {
|
|
33
60
|
// The fixture writes "home" HomePage in beforeEach by default.
|
|
61
|
+
writeSiteMdx(fixture.getContentRoot());
|
|
34
62
|
const res = await app.inject({
|
|
35
63
|
method: "GET",
|
|
36
64
|
url: "/v1/content/HomePage/home",
|
|
@@ -39,8 +67,8 @@ describe("GET /api/content/:type/:slug", () => {
|
|
|
39
67
|
expect(res.statusCode).toBe(200);
|
|
40
68
|
const body = JSON.parse(res.body);
|
|
41
69
|
expect(body["@context"]).toBe("https://schema.org");
|
|
42
|
-
// HomePage
|
|
43
|
-
//
|
|
70
|
+
// A HomePage body keeps its @graph envelope: the WebPage spine plus the
|
|
71
|
+
// sitewide identity from _site.mdx (#6597). Page fields live on the spine.
|
|
44
72
|
const graph = body["@graph"] as Array<Record<string, unknown>>;
|
|
45
73
|
expect(graph.map((n) => n["@type"])).toEqual([
|
|
46
74
|
"WebPage",
|
|
@@ -405,9 +433,8 @@ describe("GET /v1/content/:type/:slug under CONTENT_ADAPTER=database (#5852)", (
|
|
|
405
433
|
// route's capability branch is exercised through the real resolver — the file
|
|
406
434
|
// header's "no internal module mocks" (#507) holds for these cases too.
|
|
407
435
|
|
|
408
|
-
import { mkdtempSync
|
|
436
|
+
import { mkdtempSync } from "node:fs";
|
|
409
437
|
import { tmpdir } from "node:os";
|
|
410
|
-
import { join } from "node:path";
|
|
411
438
|
|
|
412
439
|
import * as contentAdapterModule from "../../../lib/content-adapter.js";
|
|
413
440
|
import { resetContentAdapter } from "../../../lib/content-adapter.js";
|
|
@@ -453,7 +480,12 @@ describe("GET /v1/content/:type/:slug?include=body (#5890)", () => {
|
|
|
453
480
|
});
|
|
454
481
|
|
|
455
482
|
// AC-1, the floor half — the unparameterised read is unchanged to the byte.
|
|
483
|
+
// #6597 rewrote this pin by hand: the HomePage @graph is now composed from
|
|
484
|
+
// the fixture's _site.mdx rather than the page title (AC-9 checks every
|
|
485
|
+
// field of the pre-#6597 body is still present). The #5890 claim it guards
|
|
486
|
+
// — no `body` key without the parameter — is unchanged.
|
|
456
487
|
it("serves a response byte-identical to the pre-change handler when the parameter is absent", async () => {
|
|
488
|
+
writeSiteMdx(fixture.getContentRoot());
|
|
457
489
|
const res = await app.inject({
|
|
458
490
|
method: "GET",
|
|
459
491
|
url: "/v1/content/HomePage/home",
|
|
@@ -461,7 +493,7 @@ describe("GET /v1/content/:type/:slug?include=body (#5890)", () => {
|
|
|
461
493
|
|
|
462
494
|
expect(res.statusCode).toBe(200);
|
|
463
495
|
expect(res.body).toMatchInlineSnapshot(
|
|
464
|
-
`"{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"HomePage home title","description":"HomePage home description"},{"@context":"https://schema.org","@type":"Organization","name":"
|
|
496
|
+
`"{"@context":"https://schema.org","@graph":[{"@context":"https://schema.org","@type":"WebPage","inLanguage":"en","name":"HomePage home title","description":"HomePage home description","mainEntity":{"@id":"https://example.com/#organization"},"isPartOf":{"@id":"https://example.com/#website"}},{"@context":"https://schema.org","@type":"Organization","@id":"https://example.com/#organization","name":"Fixture Co","url":"https://example.com"},{"@context":"https://schema.org","@type":"WebSite","@id":"https://example.com/#website","name":"Fixture Site","url":"https://example.com","inLanguage":"en","publisher":{"@id":"https://example.com/#organization"}}],"page":{"type":"HomePage","slug":"home","title":"HomePage home title","description":"HomePage home description"}}"`,
|
|
465
497
|
);
|
|
466
498
|
expect(JSON.parse(res.body)).not.toHaveProperty("body");
|
|
467
499
|
});
|
|
@@ -693,3 +725,93 @@ describe("GET /v1/content/:type/:slug?include=body under CONTENT_ADAPTER=databas
|
|
|
693
725
|
expect(prisma.artifactVersion.findFirst).not.toHaveBeenCalled();
|
|
694
726
|
});
|
|
695
727
|
});
|
|
728
|
+
|
|
729
|
+
// ─── #6597 AC-9 — the /v1 HomePage response keeps its @graph envelope ─────────
|
|
730
|
+
|
|
731
|
+
describe("GET /v1/content/HomePage/home — sitewide identity envelope (#6597)", () => {
|
|
732
|
+
let app: FastifyInstance;
|
|
733
|
+
|
|
734
|
+
beforeEach(async () => {
|
|
735
|
+
app = require("fastify")();
|
|
736
|
+
await app.register(contentRoutes);
|
|
737
|
+
await fixture.writeFixturePage("HomePage", "home", {
|
|
738
|
+
type: "HomePage",
|
|
739
|
+
slug: "home",
|
|
740
|
+
title: "HomePage home title",
|
|
741
|
+
description: "HomePage home description",
|
|
742
|
+
image: "https://example.com/home.png",
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
afterEach(async () => {
|
|
747
|
+
await app.close();
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
async function homeBody(query = ""): Promise<{
|
|
751
|
+
body: Record<string, unknown>;
|
|
752
|
+
graph: Array<Record<string, unknown>>;
|
|
753
|
+
}> {
|
|
754
|
+
const res = await app.inject({ method: "GET", url: `/v1/content/HomePage/home${query}` });
|
|
755
|
+
expect(res.statusCode).toBe(200);
|
|
756
|
+
const body = JSON.parse(res.body) as Record<string, unknown>;
|
|
757
|
+
return { body, graph: body["@graph"] as Array<Record<string, unknown>> };
|
|
758
|
+
}
|
|
759
|
+
const byType = (graph: Array<Record<string, unknown>>, type: string) =>
|
|
760
|
+
graph.find((n) => n["@type"] === type)!;
|
|
761
|
+
|
|
762
|
+
it("composes WebPage + Organization + WebSite from _site.mdx, keeping every pre-#6597 field", async () => {
|
|
763
|
+
writeSiteMdx(fixture.getContentRoot());
|
|
764
|
+
const { body, graph } = await homeBody();
|
|
765
|
+
expect(body["@context"]).toBe("https://schema.org");
|
|
766
|
+
expect(graph.map((n) => n["@type"])).toEqual(["WebPage", "Organization", "WebSite"]);
|
|
767
|
+
|
|
768
|
+
const org = byType(graph, "Organization");
|
|
769
|
+
expect(org["@id"]).toBe("https://example.com/#organization");
|
|
770
|
+
expect(org["name"]).toBe("Fixture Co");
|
|
771
|
+
// The pre-#6597 Organization took `logo` from the page image; kept as the
|
|
772
|
+
// fallback when the identity declares none.
|
|
773
|
+
expect(org["logo"]).toBe("https://example.com/home.png");
|
|
774
|
+
|
|
775
|
+
// Every key of every node of the pre-#6597 pinned body survives.
|
|
776
|
+
const pinned: Record<string, string[]> = {
|
|
777
|
+
WebPage: ["@context", "@type", "inLanguage", "name", "description"],
|
|
778
|
+
Organization: ["@context", "@type", "name"],
|
|
779
|
+
WebSite: ["@context", "@type", "name", "inLanguage"],
|
|
780
|
+
};
|
|
781
|
+
for (const [type, keys] of Object.entries(pinned)) {
|
|
782
|
+
for (const key of keys) expect(byType(graph, type)).toHaveProperty([key]);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const webPage = byType(graph, "WebPage");
|
|
786
|
+
expect(byType(graph, "WebSite")["inLanguage"]).toBe(webPage["inLanguage"]);
|
|
787
|
+
expect(webPage["inLanguage"]).toBe("en");
|
|
788
|
+
expect(graph.filter((n) => n["name"] === "HomePage home title")).toEqual([webPage]);
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
it("the WebSite and WebPage inLanguage stay one value under ?locale=<second locale>", async () => {
|
|
792
|
+
writeSiteMdx(fixture.getContentRoot());
|
|
793
|
+
const { graph } = await homeBody("?locale=pt-BR");
|
|
794
|
+
expect(byType(graph, "WebSite")["inLanguage"]).toBe(byType(graph, "WebPage")["inLanguage"]);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it("an identity-declared logo wins over the page image", async () => {
|
|
798
|
+
writeSiteMdx(fixture.getContentRoot(), { logo: "https://example.com/logo.png" });
|
|
799
|
+
const { graph } = await homeBody();
|
|
800
|
+
expect(byType(graph, "Organization")["logo"]).toBe("https://example.com/logo.png");
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
it("with no _site.mdx the body still has @context and a one-node @graph", async () => {
|
|
804
|
+
const { body, graph } = await homeBody();
|
|
805
|
+
expect(body["@context"]).toBe("https://schema.org");
|
|
806
|
+
expect(graph.map((n) => n["@type"])).toEqual(["WebPage"]);
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it("other types keep their bare JSON-LD — no @graph key", async () => {
|
|
810
|
+
writeSiteMdx(fixture.getContentRoot());
|
|
811
|
+
const res = await app.inject({ method: "GET", url: "/v1/content/AboutPage/about" });
|
|
812
|
+
expect(res.statusCode).toBe(200);
|
|
813
|
+
const body = JSON.parse(res.body) as Record<string, unknown>;
|
|
814
|
+
expect(body).not.toHaveProperty("@graph");
|
|
815
|
+
expect(body["@type"]).toBe("AboutPage");
|
|
816
|
+
});
|
|
817
|
+
});
|
|
@@ -23,7 +23,15 @@
|
|
|
23
23
|
* independent `getPageSource` call. Two independent adapter reads can
|
|
24
24
|
* straddle a publish that lands between them (frontmatter from revision N,
|
|
25
25
|
* body from revision N+1); one read cannot.
|
|
26
|
+
*
|
|
27
|
+
* #6597: a `HomePage` body keeps its `@context` + `@graph` envelope, now
|
|
28
|
+
* composed from the deployment's `_site.mdx` identity (identity node, optional
|
|
29
|
+
* secondary, WebSite) rather than the page title. Every other type's body is
|
|
30
|
+
* the page's own JSON-LD, spread at the root, as before.
|
|
26
31
|
*/
|
|
32
|
+
import { readFileSync } from "node:fs";
|
|
33
|
+
import { join } from "node:path";
|
|
34
|
+
|
|
27
35
|
import {
|
|
28
36
|
SCHEMA_ORG_TYPES,
|
|
29
37
|
type ContentAdapter,
|
|
@@ -32,10 +40,23 @@ import {
|
|
|
32
40
|
type MdxPageSource,
|
|
33
41
|
type SchemaOrgType,
|
|
34
42
|
} from "@working-theory/content-adapters";
|
|
35
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
buildJsonLd,
|
|
45
|
+
composeGraph,
|
|
46
|
+
graphNodes,
|
|
47
|
+
type ContentPageInput,
|
|
48
|
+
type JsonLdObject,
|
|
49
|
+
type SitewideIdentityInput,
|
|
50
|
+
} from "@working-theory/schema";
|
|
51
|
+
import {
|
|
52
|
+
SiteConfigSchema,
|
|
53
|
+
type SiteConfig,
|
|
54
|
+
} from "@working-theory/site-config/schema";
|
|
36
55
|
import type { FastifyPluginAsync } from "fastify";
|
|
56
|
+
import { parse as parseYaml } from "yaml";
|
|
37
57
|
import { z } from "zod";
|
|
38
58
|
|
|
59
|
+
import { getContentRoot } from "../../lib/content-adapter.js";
|
|
39
60
|
import { resolveLocale } from "../../lib/locales.js";
|
|
40
61
|
import { resolvePublicContentAdapter } from "../../services/public-site-resolver.js";
|
|
41
62
|
|
|
@@ -64,6 +85,67 @@ function hasPageSource(
|
|
|
64
85
|
);
|
|
65
86
|
}
|
|
66
87
|
|
|
88
|
+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
89
|
+
|
|
90
|
+
/** `_site.mdx` per content root, read once; `null` when it does not load. */
|
|
91
|
+
const siteConfigByRoot = new Map<string, SiteConfig | null>();
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The deployment's `_site.mdx`, validated against `SiteConfigSchema`, or
|
|
95
|
+
* `undefined` on any failure (absent, unreadable, invalid). A local read with
|
|
96
|
+
* the client-safe `/schema` entry: `@working-theory/site-config/server` imports
|
|
97
|
+
* `server-only`, which throws outside a React Server Components runtime.
|
|
98
|
+
*/
|
|
99
|
+
function readSiteConfig(contentRoot: string): SiteConfig | undefined {
|
|
100
|
+
if (!siteConfigByRoot.has(contentRoot)) {
|
|
101
|
+
let config: SiteConfig | null = null;
|
|
102
|
+
try {
|
|
103
|
+
const raw = readFileSync(join(contentRoot, "_site.mdx"), "utf8");
|
|
104
|
+
const match = FRONTMATTER_RE.exec(raw);
|
|
105
|
+
if (match) {
|
|
106
|
+
const parsed = SiteConfigSchema.safeParse(parseYaml(match[1] ?? ""));
|
|
107
|
+
if (parsed.success) config = parsed.data;
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
// Absent or unreadable — no site.
|
|
111
|
+
}
|
|
112
|
+
siteConfigByRoot.set(contentRoot, config);
|
|
113
|
+
}
|
|
114
|
+
return siteConfigByRoot.get(contentRoot) ?? undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The `HomePage` body's `@graph` (#6597, spec D11): the spine plus the site's
|
|
119
|
+
* sitewide identity nodes. The WebSite's `inLanguage` is the spine's own, so
|
|
120
|
+
* both carry one value; the Organization falls back to the page's `image` as
|
|
121
|
+
* its `logo` when the identity declares none, as the title-derived node did.
|
|
122
|
+
*/
|
|
123
|
+
function homePageGraph(
|
|
124
|
+
primary: JsonLdObject,
|
|
125
|
+
image: string | undefined,
|
|
126
|
+
): { "@context": "https://schema.org"; "@graph": JsonLdObject[] } {
|
|
127
|
+
const config = readSiteConfig(getContentRoot());
|
|
128
|
+
let site: SitewideIdentityInput | undefined;
|
|
129
|
+
if (config !== undefined) {
|
|
130
|
+
const identity = config.siteIdentity;
|
|
131
|
+
site = {
|
|
132
|
+
origin: config.baseUrl.replace(/\/+$/, ""),
|
|
133
|
+
siteName: config.siteName,
|
|
134
|
+
baseUrl: config.baseUrl,
|
|
135
|
+
inLanguage:
|
|
136
|
+
typeof primary["inLanguage"] === "string" ? primary["inLanguage"] : undefined,
|
|
137
|
+
identity:
|
|
138
|
+
identity.kind !== "Person" && identity.logo === undefined && image !== undefined
|
|
139
|
+
? { ...identity, logo: image }
|
|
140
|
+
: identity,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
"@context": "https://schema.org",
|
|
145
|
+
"@graph": graphNodes(composeGraph({ primary, site, isHomePage: true })),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
67
149
|
export const getPageRoute: FastifyPluginAsync = async (fastify) => {
|
|
68
150
|
fastify.get<{
|
|
69
151
|
Params: { type: string; slug: string };
|
|
@@ -165,7 +247,9 @@ export const getPageRoute: FastifyPluginAsync = async (fastify) => {
|
|
|
165
247
|
// now preserves `sections`; before, it was stripped at validation.
|
|
166
248
|
...(page.sections !== undefined && { sections: page.sections }),
|
|
167
249
|
};
|
|
168
|
-
const
|
|
250
|
+
const primary = buildJsonLd(input);
|
|
251
|
+
const jsonLd =
|
|
252
|
+
page.type === "HomePage" ? homePageGraph(primary, page.image) : primary;
|
|
169
253
|
return reply.send({
|
|
170
254
|
...jsonLd,
|
|
171
255
|
page,
|
|
@@ -23,6 +23,7 @@ import { config } from "./config/index.js";
|
|
|
23
23
|
import { isModuleEnabled } from "./config/modules.js";
|
|
24
24
|
import { registerErrorSerializer } from "./lib/error-serializer.js";
|
|
25
25
|
import { validateLeadCaptureDir } from "./lib/lead-capture-store.js";
|
|
26
|
+
import { registerLicenseKeyStore } from "./lib/license-key-store.js";
|
|
26
27
|
import { requireCommerceClaim } from "./lib/require-commerce-claim.js";
|
|
27
28
|
import { assertSiteAliasResolves } from "./lib/site.js";
|
|
28
29
|
import { loadSlice } from "./lib/slice-load.js";
|
|
@@ -522,6 +523,11 @@ export async function build() {
|
|
|
522
523
|
// database-less dev boot).
|
|
523
524
|
await assertSiteAliasResolves();
|
|
524
525
|
|
|
526
|
+
// #6677 DD-6 — the renewed licence key's durable home: with a database,
|
|
527
|
+
// one deployment-wide SiteSetting row, so a redeploy keeps the key this
|
|
528
|
+
// site renewed. Never throws; without DATABASE_URL the file store stays.
|
|
529
|
+
await registerLicenseKeyStore();
|
|
530
|
+
|
|
525
531
|
// Runtime sandbox-mode guard: reject live-mode Stripe keys before any
|
|
526
532
|
// webhook or checkout call can exercise them. Idempotent — safe even if
|
|
527
533
|
// assertSandboxKey is called again inside a route handler. UNCONDITIONAL
|
|
@@ -15,9 +15,17 @@
|
|
|
15
15
|
* - No fetch() call (structural test)
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { readFileSync } from "node:fs";
|
|
18
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
19
20
|
import { resolve, join } from "node:path";
|
|
20
21
|
|
|
22
|
+
import {
|
|
23
|
+
entityMarkup,
|
|
24
|
+
runAudit,
|
|
25
|
+
schemaCoverage,
|
|
26
|
+
type AuditReport,
|
|
27
|
+
} from "@working-theory/aeo";
|
|
28
|
+
|
|
21
29
|
import { buildSinglePageAuditInput } from "../src/lib/audit-single-page.js";
|
|
22
30
|
|
|
23
31
|
const REPO_ROOT = resolve(__dirname, "../../..");
|
|
@@ -211,3 +219,69 @@ describe("structural guard — no fetch()", () => {
|
|
|
211
219
|
expect(codeOnly).not.toMatch(/\bfetch\s*\(/);
|
|
212
220
|
});
|
|
213
221
|
});
|
|
222
|
+
|
|
223
|
+
describe("buildSinglePageAuditInput — sitewide identity (#6597 AC-7)", () => {
|
|
224
|
+
const evidenceOf = (report: AuditReport, id: string) =>
|
|
225
|
+
report.perRule.find((r) => r.id === id);
|
|
226
|
+
const auditOf = async (input: { slug: string; type?: "AboutPage" }, root: string) =>
|
|
227
|
+
runAudit(await buildSinglePageAuditInput(input, root), [schemaCoverage, entityMarkup]);
|
|
228
|
+
|
|
229
|
+
it("schema-coverage passes for the monorepo's AboutPage/about", async () => {
|
|
230
|
+
const report = await auditOf({ slug: "about", type: "AboutPage" }, CONTENT_ROOT);
|
|
231
|
+
expect(evidenceOf(report, "schema-coverage")?.pass).toBe(true);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("the monorepo home finds the identity: Identity (organization) ✓", async () => {
|
|
235
|
+
const report = await auditOf({ slug: "home" }, CONTENT_ROOT);
|
|
236
|
+
expect(evidenceOf(report, "entity-markup")?.evidence).toContain(
|
|
237
|
+
"Identity (organization) ✓",
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("stores the flat node array, the page's own primary first", async () => {
|
|
242
|
+
const input = await buildSinglePageAuditInput({ slug: "about", type: "AboutPage" }, CONTENT_ROOT);
|
|
243
|
+
const nodes = input.jsonLdByPage["AboutPage/about"] as Array<Record<string, unknown>>;
|
|
244
|
+
expect(nodes[0]!["@type"]).toBe("AboutPage");
|
|
245
|
+
expect(nodes.map((n) => n["@type"])).toEqual(
|
|
246
|
+
expect.arrayContaining(["Organization", "WebSite"]),
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe("a Person site", () => {
|
|
251
|
+
let root: string;
|
|
252
|
+
beforeAll(() => {
|
|
253
|
+
root = mkdtempSync(join(tmpdir(), "mcp-6597-"));
|
|
254
|
+
writeFileSync(
|
|
255
|
+
join(root, "_site.mdx"),
|
|
256
|
+
[
|
|
257
|
+
"---",
|
|
258
|
+
'version: "1"',
|
|
259
|
+
"siteName: Ada",
|
|
260
|
+
"baseUrl: https://example.com",
|
|
261
|
+
"defaultLocale: en",
|
|
262
|
+
"siteIdentity:",
|
|
263
|
+
" kind: Person",
|
|
264
|
+
" name: Ada Lovelace",
|
|
265
|
+
" url: https://example.com",
|
|
266
|
+
"---",
|
|
267
|
+
"",
|
|
268
|
+
].join("\n"),
|
|
269
|
+
);
|
|
270
|
+
mkdirSync(join(root, "en", "HomePage"), { recursive: true });
|
|
271
|
+
writeFileSync(
|
|
272
|
+
join(root, "en", "HomePage", "home.mdx"),
|
|
273
|
+
["---", "type: HomePage", "slug: home", "title: Ada", "description: Ada's site.", "---", ""].join("\n"),
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
afterAll(() => {
|
|
277
|
+
rmSync(root, { recursive: true, force: true });
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("derives person mode from the kind: Identity (person) ✓", async () => {
|
|
281
|
+
const input = await buildSinglePageAuditInput({ slug: "home" }, root);
|
|
282
|
+
expect(input.identityMode).toBe("person");
|
|
283
|
+
const report = runAudit(input, [entityMarkup]);
|
|
284
|
+
expect(evidenceOf(report, "entity-markup")?.evidence).toContain("Identity (person) ✓");
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
});
|
|
@@ -10,18 +10,18 @@
|
|
|
10
10
|
* error, never logged. This is the ordinary "site has nothing of this
|
|
11
11
|
* type" case every real adapter (MDX, database) takes today.
|
|
12
12
|
* - a type whose adapter call THROWS is a genuine failure. It must not take
|
|
13
|
-
* down the audit (the other
|
|
13
|
+
* down the audit (the other two reads still complete and their pages
|
|
14
14
|
* still count), but it must be visible — logged via `console.error`
|
|
15
15
|
* (stderr, NOT stdout — the default stdio MCP transport's JSON-RPC
|
|
16
16
|
* framing channel, #6012 round 2) — rather than silently collapsing into
|
|
17
17
|
* the same vacuous "nothing found" result.
|
|
18
18
|
*
|
|
19
|
-
* The call-count assertions below prove that all
|
|
19
|
+
* The call-count assertions below prove that all three reads still ran despite
|
|
20
20
|
* the throw — the read AFTER the failing one is not skipped, which a
|
|
21
21
|
* sequential loop that ABORTED on the first throw would fail to show. They
|
|
22
22
|
* are NOT, on their own, proof of CONCURRENCY: the prior sequential `for`
|
|
23
23
|
* loop this replaced used `continue` (not a re-throw), so it too issued all
|
|
24
|
-
*
|
|
24
|
+
* three calls. The `Promise.all` in the source is what delivers the
|
|
25
25
|
* concurrency AC; these assertions cover the narrower, still load-bearing
|
|
26
26
|
* property that a throw part-way through never truncates the scan.
|
|
27
27
|
*/
|
|
@@ -93,10 +93,10 @@ describe("scanSitePages — type-not-registered vs adapter-threw (#6012)", () =>
|
|
|
93
93
|
|
|
94
94
|
expect(result.readiness!.sitePages).toEqual([]);
|
|
95
95
|
expect(errorSpy).not.toHaveBeenCalled();
|
|
96
|
-
// All
|
|
97
|
-
// listPages (
|
|
96
|
+
// All three reads still ran — two listDetail (Product, Service), one
|
|
97
|
+
// listPages (PurchasePolicyPage, #6646).
|
|
98
98
|
expect(listDetail).toHaveBeenCalledTimes(2);
|
|
99
|
-
expect(listPages).toHaveBeenCalledTimes(
|
|
99
|
+
expect(listPages).toHaveBeenCalledTimes(1);
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
it("a type whose adapter call throws is logged to stderr and does not fail the audit", async () => {
|
|
@@ -135,9 +135,57 @@ describe("scanSitePages — type-not-registered vs adapter-threw (#6012)", () =>
|
|
|
135
135
|
// the stdio transport's JSON-RPC channel).
|
|
136
136
|
expect(errorSpy).toHaveBeenCalledTimes(1);
|
|
137
137
|
expect(errorSpy.mock.calls[0]![0]).toMatch(/Service/);
|
|
138
|
-
// All
|
|
139
|
-
// throwing one (
|
|
138
|
+
// All three reads still ran despite the throw — the read AFTER the
|
|
139
|
+
// throwing one (PurchasePolicyPage) was not skipped.
|
|
140
140
|
expect(listDetail).toHaveBeenCalledTimes(2);
|
|
141
|
-
expect(listPages).toHaveBeenCalledTimes(
|
|
141
|
+
expect(listPages).toHaveBeenCalledTimes(1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("a PurchasePolicyPage contributes its document-chassis section ids (#6646)", async () => {
|
|
145
|
+
const purchasePolicy: ContentPage = {
|
|
146
|
+
type: "PurchasePolicyPage",
|
|
147
|
+
slug: "purchase-policy",
|
|
148
|
+
title: "Purchase Policy",
|
|
149
|
+
description: "Returns, shipping and refunds.",
|
|
150
|
+
sections: [
|
|
151
|
+
{
|
|
152
|
+
id: "purchase-policy-document",
|
|
153
|
+
type: "document-chassis",
|
|
154
|
+
data: {
|
|
155
|
+
headline: "Purchase Policy",
|
|
156
|
+
sections: [
|
|
157
|
+
{ id: "returns", heading: "Returns", blocks: [] },
|
|
158
|
+
{ id: "shipping", heading: "Shipping", blocks: [] },
|
|
159
|
+
{ id: "refunds", heading: "Refunds", blocks: [] },
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
] as ContentPage["sections"],
|
|
164
|
+
};
|
|
165
|
+
mockResolveContentAdapter.mockReturnValue({
|
|
166
|
+
getPageBySlug: jest.fn().mockResolvedValue(HOME_PAGE),
|
|
167
|
+
listPages: jest.fn().mockResolvedValue([purchasePolicy]),
|
|
168
|
+
listDetail: jest.fn().mockResolvedValue([]),
|
|
169
|
+
getDetailBySlug: jest.fn(),
|
|
170
|
+
searchPages: jest.fn(),
|
|
171
|
+
extractStructuredData: jest.fn(),
|
|
172
|
+
createPage: jest.fn(),
|
|
173
|
+
updatePage: jest.fn(),
|
|
174
|
+
deletePage: jest.fn(),
|
|
175
|
+
getPageVersion: jest.fn(),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const result = await buildSinglePageAuditInput({
|
|
179
|
+
slug: "home",
|
|
180
|
+
type: "HomePage",
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(result.readiness!.sitePages).toEqual([
|
|
184
|
+
{
|
|
185
|
+
slug: "purchase-policy",
|
|
186
|
+
type: "PurchasePolicyPage",
|
|
187
|
+
chassisSectionIds: ["returns", "shipping", "refunds"],
|
|
188
|
+
},
|
|
189
|
+
]);
|
|
142
190
|
});
|
|
143
191
|
});
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"lint": "eslint .",
|
|
10
10
|
"build": "tsc -b",
|
|
11
11
|
"type-check": "tsc --noEmit",
|
|
12
|
-
"dev": "
|
|
12
|
+
"dev": "tsx watch src/index.ts",
|
|
13
|
+
"start": "node --experimental-vm-modules dist/index.js",
|
|
13
14
|
"test": "jest",
|
|
14
15
|
"test:watch": "jest --watch",
|
|
15
16
|
"test:coverage": "jest --coverage"
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { initMcpCommerceClaim } from "./lib/commerce-claim.js";
|
|
25
|
+
import { registerLicenseKeyStore } from "./lib/license-key-store.js";
|
|
25
26
|
import { initMcpPublicWorkspace } from "./lib/public-workspace.js";
|
|
26
27
|
|
|
27
28
|
// Resolves the MCP server's own workspace ONCE, before either transport
|
|
@@ -48,6 +49,12 @@ try {
|
|
|
48
49
|
);
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
// Registers the renewed licence key's durable home (#6677 DD-6) BEFORE the
|
|
53
|
+
// commerce claim below memoises the licence: with a database, the key a
|
|
54
|
+
// sibling app renewed lives in one SiteSetting row, and the claim must read
|
|
55
|
+
// it. Never throws; without DATABASE_URL the file store stays in place.
|
|
56
|
+
await registerLicenseKeyStore();
|
|
57
|
+
|
|
51
58
|
// Resolves the deployment's `commerce` add-on claim ONCE, before either
|
|
52
59
|
// transport branch — the sibling of the workspace resolution above and the
|
|
53
60
|
// same #5876 idiom (#5971 spec D2). Without this await the memoised claim is
|