create-ailk 0.7.0 → 0.9.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 +112 -10
- package/component-catalog.json +798 -49
- package/component-catalog.md +9 -6
- package/dist/add-module.d.ts +20 -0
- package/dist/add-module.js +43 -7
- package/dist/add-route.d.ts +56 -0
- package/dist/add-route.js +286 -0
- package/dist/apply-design-md.d.ts +7 -0
- package/dist/apply-design-md.js +50 -0
- package/dist/apply-design.d.ts +64 -0
- package/dist/apply-design.js +273 -0
- package/dist/claude-md.d.ts +111 -0
- package/dist/claude-md.js +334 -0
- package/dist/cli.js +114 -28
- package/dist/derive-architecture.js +41 -36
- package/dist/design-compile.d.ts +71 -0
- package/dist/design-compile.js +98 -0
- package/dist/design-package.d.ts +83 -0
- package/dist/design-package.js +468 -0
- package/dist/design-presets.d.ts +86 -0
- package/dist/design-presets.js +642 -0
- package/dist/design-scaffold.d.ts +76 -0
- package/dist/design-scaffold.js +220 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/legacy-nav-renderers.d.ts +45 -0
- package/dist/legacy-nav-renderers.js +545 -0
- package/dist/lib/auth-secret-file.d.ts +19 -0
- package/dist/lib/auth-secret-file.js +112 -0
- package/dist/lib/design-checks.d.ts +78 -0
- package/dist/lib/design-checks.js +174 -0
- package/dist/lib/extract-module-bundle.js +5 -1
- package/dist/lib/license-key-files.d.ts +9 -0
- package/dist/lib/license-key-files.js +2 -2
- package/dist/lib/module-license-gate.d.ts +89 -0
- package/dist/lib/module-license-gate.js +171 -12
- package/dist/lib/module-tier.d.ts +45 -16
- package/dist/lib/module-tier.js +76 -0
- package/dist/lib/pro-callout.js +10 -2
- package/dist/lib/render-verdict.d.ts +52 -0
- package/dist/lib/render-verdict.js +77 -0
- package/dist/lib/scaffold-render-sweep.d.ts +105 -0
- package/dist/lib/scaffold-render-sweep.js +236 -0
- package/dist/module-architecture.d.ts +19 -16
- package/dist/module-architecture.js +64 -21
- package/dist/parse-args.d.ts +88 -10
- package/dist/parse-args.js +272 -21
- package/dist/programmatic.d.ts +66 -12
- package/dist/programmatic.js +361 -34
- package/dist/site-chrome.d.ts +19 -9
- package/dist/site-chrome.js +23 -13
- package/dist/site-manifest.d.ts +25 -0
- package/dist/site-manifest.js +254 -0
- package/dist/site-roster.d.ts +41 -0
- package/dist/site-roster.js +149 -11
- package/dist/starter-grid.d.ts +232 -0
- package/dist/starter-grid.js +545 -0
- package/dist/surfaces.d.ts +41 -9
- package/dist/surfaces.js +268 -92
- package/dist/template-manifest.d.ts +65 -29
- package/dist/template-manifest.js +161 -382
- package/dist/template-names.d.ts +14 -12
- package/dist/template-names.js +13 -17
- package/package.json +5 -4
- package/site-templates/_base/content/en/Feature/feature-1.mdx +69 -0
- package/site-templates/_base/content/en/Feature/feature-2.mdx +69 -0
- package/site-templates/_base/content/en/Feature/feature-3.mdx +69 -0
- package/site-templates/_base/content/en/FeaturesList/features.mdx +91 -0
- package/site-templates/_base/content/en/GlossaryList/glossary.mdx +0 -1
- package/site-templates/_base/content/en/NewsletterSignup/newsletter.mdx +0 -1
- package/site-templates/_base/content/en/PortfolioItem/sample-four.mdx +105 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-one.mdx +34 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-three.mdx +94 -0
- package/site-templates/_base/content/en/PortfolioItem/sample-two.mdx +45 -0
- package/site-templates/_base/content/en/PortfolioList/portfolio.mdx +169 -0
- package/site-templates/_base/content/en/PrivacyPage/privacy.mdx +15 -0
- package/site-templates/_base/content/en/UseCase/persona-1.mdx +70 -0
- package/site-templates/_base/content/en/UseCase/persona-2.mdx +70 -0
- package/site-templates/_base/content/en/UseCase/persona-3.mdx +70 -0
- package/site-templates/{minimalism-site/TEMPLATE-MANIFEST.json → aliases/minimalism-site.TEMPLATE-MANIFEST.json} +72 -40
- package/site-templates/designs/minimalism/DESIGN.md +98 -6
- package/site-templates/designs/minimalism/sample/README.md +48 -0
- package/site-templates/designs/standard/DESIGN.md +103 -6
- package/site-templates/marketing-site/DESIGN.md +103 -6
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +144 -47
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +82 -41
- package/site-templates/marketing-site-free/DESIGN.md +103 -6
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +150 -48
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +114 -67
- package/site-templates/marketing-site-free/ailk-template.json +1 -1
- package/site-templates/marketing-site-free/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +199 -0
- package/site-templates/saas-product/DESIGN.md +103 -6
- package/site-templates/saas-product/TEMPLATE-MANIFEST.md +9 -1
- package/site-templates/{simple-service → shapes/local-business-service}/TEMPLATE-MANIFEST.json +210 -81
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +356 -0
- package/site-templates/shapes/local-business-service/ailk-template.json +10 -0
- package/site-templates/{simple-service → shapes/local-business-service}/content/en/AboutPage/about.mdx +4 -4
- package/site-templates/shapes/local-business-service/content/en/HomePage/home.mdx +70 -0
- package/site-templates/shapes/local-business-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/local-business-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/{minimalism-site → shapes/local-business-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +7 -8
- package/site-templates/shapes/local-business-service/content/en/Service/first-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/Service/second-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/Service/third-job.mdx +39 -0
- package/site-templates/shapes/local-business-service/content/en/ServicesList/services.mdx +61 -0
- package/site-templates/shapes/local-business-service/provenance.json +4 -0
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +471 -0
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +354 -0
- package/site-templates/shapes/organization-service/ailk-template.json +10 -0
- package/site-templates/shapes/organization-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/shapes/organization-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/shapes/organization-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/organization-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/shapes/organization-service/content/en/PurchasePolicyPage/purchase-policy.mdx +123 -0
- package/site-templates/{minimalism-site → shapes/organization-service}/content/en/ServicesList/services.mdx +3 -4
- package/site-templates/shapes/organization-service/provenance.json +4 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +478 -0
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +353 -0
- package/site-templates/shapes/person-service/ailk-template.json +10 -0
- package/site-templates/shapes/person-service/content/_site.mdx +85 -0
- package/site-templates/shapes/person-service/content/en/AboutPage/about.mdx +39 -0
- package/site-templates/shapes/person-service/content/en/HomePage/home.mdx +97 -0
- package/site-templates/shapes/person-service/content/en/NewsletterSignup/newsletter.mdx +59 -0
- package/site-templates/shapes/person-service/content/en/PortfolioList/portfolio.mdx +171 -0
- package/site-templates/{simple-service → shapes/person-service}/content/en/PurchasePolicyPage/purchase-policy.mdx +10 -11
- package/site-templates/shapes/person-service/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/shapes/person-service/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/shapes/person-service/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/{simple-service → shapes/person-service}/content/en/ServicesList/services.mdx +16 -16
- package/site-templates/shapes/person-service/provenance.json +4 -0
- package/templates/.claude/rules/architecture.md +7 -0
- package/templates/.claude/skills/README.md +5 -3
- package/templates/.claude/skills/add-auth/references/env-vars.md +22 -15
- package/templates/.claude/skills/add-auth/references/oauth-app-setup.md +3 -2
- package/templates/.claude/skills/add-page/SKILL.md +241 -238
- package/templates/.claude/skills/add-page/references/page-types.md +85 -72
- package/templates/.claude/skills/add-page/scripts/site-pages.mjs +87 -0
- package/templates/.claude/skills/add-page/templates/comparison-page.mdx.tmpl +99 -0
- package/templates/.claude/skills/add-page/templates/content.mdx.tmpl +8 -18
- package/templates/.claude/skills/add-page/templates/flow-landing-page.mdx.tmpl +43 -0
- package/templates/.claude/skills/add-page/templates/report-page.mdx.tmpl +52 -0
- package/templates/.claude/skills/add-page/templates/reports-list.mdx.tmpl +38 -0
- package/templates/.claude/skills/check-site/SKILL.md +23 -0
- package/templates/.claude/skills/fill-page/SKILL.md +26 -7
- package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +1 -1
- package/templates/.claude/skills/setup-database/SKILL.md +156 -0
- package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +154 -0
- package/templates/.claude/skills/setup-deploy/SKILL.md +64 -32
- package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +44 -36
- package/templates/.claude/skills/setup-deploy/references/runbook-template.md +58 -102
- package/templates/.claude/skills/update-page/SKILL.md +117 -0
- package/templates/.claude/skills/update-page/scripts/update-page.ts +640 -0
- package/templates/.claude/skills/update-page/templates/redirects.mjs.tmpl +31 -0
- package/templates/.claude/skills/upgrade-plan/SKILL.md +12 -8
- package/templates/.github/workflows/ci.yml +4 -0
- package/templates/.prettierignore +1 -0
- package/templates/CLAUDE.md +146 -103
- package/templates/DESIGN.md +103 -6
- package/templates/README.md +63 -43
- package/templates/apps/api/.env.example +2 -1
- package/templates/apps/api/CLAUDE.md +3 -2
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +2 -0
- package/templates/apps/api/src/__tests__/server.test.ts +116 -0
- package/templates/apps/api/src/config/__tests__/index.test.ts +115 -14
- package/templates/apps/api/src/config/index.ts +35 -16
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +30 -0
- package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +96 -0
- package/templates/apps/api/src/lib/lead-capture-store.ts +12 -0
- package/templates/apps/api/src/lib/lead-delivery.ts +52 -0
- package/templates/apps/api/src/lib/stripe.ts +9 -0
- package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +140 -0
- package/templates/apps/api/src/middleware/auth.ts +54 -0
- package/templates/apps/api/src/openapi/spec.ts +63 -12
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +5 -2
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +628 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +222 -24
- package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +88 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +100 -0
- package/templates/apps/api/src/routes/leads/index.ts +46 -1
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +1 -1
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +5 -1
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +521 -20
- package/templates/apps/api/src/routes/webhooks/stripe.ts +124 -32
- package/templates/apps/api/src/server.ts +35 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +194 -0
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +124 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +90 -15
- package/templates/apps/api/src/services/checkout-buyer.ts +72 -0
- package/templates/apps/api/src/services/lead-routing.ts +140 -30
- package/templates/apps/api/src/services/leads-inbox.ts +48 -11
- package/templates/apps/api/src/services/revalidation.ts +60 -15
- package/templates/apps/mcp/__tests__/parity.test.ts +5 -4
- package/templates/apps/mcp/__tests__/tools/create_project.test.ts +30 -24
- package/templates/apps/mcp/src/tools/create_project.ts +21 -6
- package/templates/apps/mcp/src/tools/purchase.ts +4 -2
- package/templates/apps/web/.env.example +13 -0
- package/templates/apps/web/CLAUDE.md +22 -28
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +1 -1
- package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +24 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +17 -6
- package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +110 -0
- package/templates/apps/web/app/[locale]/[slug]/[detail]/page.tsx +60 -0
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +1 -1
- package/templates/apps/web/app/[locale]/alternatives/__tests__/page.test.tsx +161 -0
- package/templates/apps/web/app/[locale]/alternatives/page.tsx +104 -0
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +14 -6
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +10 -9
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +32 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +129 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +227 -84
- package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +50 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +73 -20
- package/templates/apps/web/app/[locale]/contact/page.tsx +105 -15
- package/templates/apps/web/app/[locale]/contact/site-contact-email.ts +54 -0
- package/templates/apps/web/app/[locale]/features/[slug]/page.tsx +64 -0
- package/templates/apps/web/app/[locale]/features/page.tsx +52 -0
- package/templates/apps/web/app/[locale]/{use-cases → for}/[slug]/page.tsx +21 -5
- package/templates/apps/web/app/[locale]/for/__tests__/page-renderer-migration.test.tsx +73 -0
- package/templates/apps/web/app/[locale]/{use-cases → for}/page.tsx +8 -7
- package/templates/apps/web/app/[locale]/glossary/page.tsx +1 -1
- package/templates/apps/web/app/[locale]/layout.tsx +65 -17
- package/templates/apps/web/app/[locale]/newsletter/__tests__/newsletter.actions.test.ts +75 -0
- package/templates/apps/web/app/[locale]/newsletter/__tests__/page.test.tsx +163 -0
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +66 -0
- package/templates/apps/web/app/[locale]/newsletter/page.tsx +106 -14
- package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +59 -0
- package/templates/apps/web/app/[locale]/{book-a-call → portfolio}/page.tsx +12 -13
- package/templates/apps/web/app/[locale]/vs/[slug]/__tests__/page.test.tsx +112 -0
- package/templates/apps/web/app/[locale]/vs/[slug]/page.tsx +70 -0
- package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +236 -44
- package/templates/apps/web/app/api/revalidate/route.ts +78 -36
- package/templates/apps/web/lib/__tests__/site-brand.test.ts +79 -0
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +104 -11
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +21 -4
- package/templates/apps/web/lib/locale-guard.ts +18 -0
- package/templates/apps/web/lib/meet-placeholder.ts +5 -0
- package/templates/apps/web/lib/nav-config.ts +50 -37
- package/templates/apps/web/lib/site-brand.tsx +13 -9
- package/templates/apps/web/lib/site-identity.ts +14 -0
- package/templates/apps/web/lib/site-logo.tsx +36 -0
- package/templates/apps/web/lib/site-theme.ts +46 -26
- package/templates/apps/web/messages/en.json +45 -5
- package/templates/apps/web/middleware.ts +28 -4
- package/templates/content/_site.mdx +4 -2
- package/templates/content/en/BlogList/blog.mdx +37 -16
- package/templates/content/en/BookCall/book-a-call.mdx +80 -0
- package/templates/content/en/BookCall/onsite-estimate.mdx +56 -0
- package/templates/content/en/BookCall/onsite-service.mdx +56 -0
- package/templates/content/en/BookCall/start-project.mdx +56 -0
- package/templates/content/en/ContactPage/contact.mdx +43 -34
- package/templates/content/en/HomePage/home.mdx +10 -22
- package/templates/content/en/LandingPage/meet.mdx +59 -0
- package/templates/content/en/PurchasePolicyPage/purchase-policy.mdx +0 -1
- package/templates/content/en/UseCasesList/{use-cases.mdx → for.mdx} +19 -18
- package/templates/database/CHANGELOG.md +14 -0
- package/templates/database/package.json +1 -1
- package/templates/package.json +2 -1
- package/templates/tools/check-claude-md.ts +128 -0
- package/versions.json +33 -33
- package/site-templates/_base/content/en/BookCall/book-call.mdx +0 -53
- package/site-templates/_base/content/en/UseCase/first-use-case.mdx +0 -23
- package/site-templates/_base/content/en/UseCase/second-use-case.mdx +0 -23
- package/site-templates/_base/content/en/UseCase/third-use-case.mdx +0 -23
- package/site-templates/minimalism-site/DESIGN.md +0 -162
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +0 -470
- package/site-templates/minimalism-site/ailk-template.json +0 -23
- package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +0 -110
- package/site-templates/minimalism-site/content/en/BookCall/book-call.mdx +0 -53
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +0 -34
- package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +0 -57
- package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +0 -77
- package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +0 -73
- package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +0 -52
- package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +0 -52
- package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +0 -52
- package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +0 -52
- package/site-templates/minimalism-site/flows/referral.flow.json +0 -71
- package/site-templates/minimalism-site/provenance.json +0 -4
- package/site-templates/simple-service/DESIGN.md +0 -162
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +0 -333
- package/site-templates/simple-service/ailk-template.json +0 -11
- package/site-templates/simple-service/content/en/BookCall/book-call.mdx +0 -53
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +0 -32
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +0 -56
- package/site-templates/simple-service/content/en/HomePage/home.mdx +0 -85
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +0 -40
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +0 -40
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +0 -40
- package/site-templates/simple-service/provenance.json +0 -4
- package/templates/.claude/skills/add-page/templates/route.tsx.tmpl +0 -28
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +0 -211
- package/templates/apps/web/app/[locale]/contact/__tests__/ContactForm.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +0 -72
- package/templates/content/en/SoftwareProduct/product.mdx +0 -175
- /package/site-templates/{minimalism-site → designs/minimalism/sample}/content/en/HomePage/home.mdx +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-1.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-2.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-3.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-4.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-5.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/avatar-placeholder-6.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/hero-backdrop.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-1.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-2.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/logo-placeholder-3.svg +0 -0
- /package/site-templates/{minimalism-site/assets → designs/minimalism/sample/public}/wordmark-placeholder.svg +0 -0
- /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/first-engagement.mdx +0 -0
- /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/second-engagement.mdx +0 -0
- /package/site-templates/{minimalism-site → shapes/organization-service}/content/en/Service/third-engagement.mdx +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** One row of theme's contrast audit, as the audit script prints it. */
|
|
2
|
+
export interface ContrastRow {
|
|
3
|
+
name: string;
|
|
4
|
+
bg: string;
|
|
5
|
+
fg: string;
|
|
6
|
+
ratio: number | null;
|
|
7
|
+
threshold: number;
|
|
8
|
+
status: "pass" | "fail" | "unmeasurable";
|
|
9
|
+
}
|
|
10
|
+
export type DesignContrastVerdict = {
|
|
11
|
+
ok: true;
|
|
12
|
+
pairs: number;
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
pairs: number;
|
|
16
|
+
failing: ContrastRow[];
|
|
17
|
+
reason: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Decide whether `design`'s derived token set passes WCAG AA. Every row whose
|
|
21
|
+
* status is not `pass` fails it; an audit of no pair never passes.
|
|
22
|
+
*/
|
|
23
|
+
export declare function assessDesignContrast(design: string, results: ContrastRow[]): DesignContrastVerdict;
|
|
24
|
+
/** `__tests__/fixtures/page-jsonld.ts`'s stdout: page path → its composed JSON-LD. */
|
|
25
|
+
export type ComposedPages = Record<string, {
|
|
26
|
+
jsonld: string;
|
|
27
|
+
sectionsValid: boolean;
|
|
28
|
+
}>;
|
|
29
|
+
/** One changed node: the page and its JSON-pointer path (or a `(page …)` pseudo-path). */
|
|
30
|
+
export interface DriftNode {
|
|
31
|
+
page: string;
|
|
32
|
+
path: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Every node that differs between two compositions, sorted by page then path.
|
|
36
|
+
* A removed or added page, and a page whose sections no longer validate after,
|
|
37
|
+
* are one pseudo-path node each; a `logo` subtree is never compared.
|
|
38
|
+
*/
|
|
39
|
+
export declare function jsonLdDrift(before: ComposedPages, after: ComposedPages): DriftNode[];
|
|
40
|
+
export type JsonLdDriftVerdict = {
|
|
41
|
+
ok: true;
|
|
42
|
+
pages: number;
|
|
43
|
+
} | {
|
|
44
|
+
ok: false;
|
|
45
|
+
pages: number;
|
|
46
|
+
changed: DriftNode[];
|
|
47
|
+
reason: string;
|
|
48
|
+
};
|
|
49
|
+
/** Decide whether `apply-design <design>` left every page's JSON-LD unchanged (logo excluded). */
|
|
50
|
+
export declare function assessJsonLdDrift(design: string, before: ComposedPages, after: ComposedPages): JsonLdDriftVerdict;
|
|
51
|
+
export interface MeasureApplyDesignDriftOptions {
|
|
52
|
+
/** create-ailk's built `dist/cli.js`. */
|
|
53
|
+
cliPath: string;
|
|
54
|
+
/** The monorepo's `tsx` binary. */
|
|
55
|
+
tsxPath: string;
|
|
56
|
+
/** `__tests__/fixtures/page-jsonld.ts` — the one production composer, run from source. */
|
|
57
|
+
composerPath: string;
|
|
58
|
+
/** Its tsconfig, passed as `TSX_TSCONFIG_PATH`. */
|
|
59
|
+
composerTsconfig: string;
|
|
60
|
+
/** The design `apply-design` applies (a name or a package path). */
|
|
61
|
+
design: string;
|
|
62
|
+
/** An existing empty directory; the drift site is scaffolded at `<workDir>/site`. */
|
|
63
|
+
workDir: string;
|
|
64
|
+
/** The loopback registry the CLI resolves modules and templates from. */
|
|
65
|
+
registryUrl?: string;
|
|
66
|
+
/** Written to the site's own `ailk.key`, which `apply-design` gates a non-Free design on. */
|
|
67
|
+
licenseKey?: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Scaffold a keyless `organization-product` site on `standard`, compose its
|
|
71
|
+
* JSON-LD, run `apply-design <design>` on it, and compose again. A non-zero
|
|
72
|
+
* exit at any step throws naming the step and the child's stderr.
|
|
73
|
+
*/
|
|
74
|
+
export declare function measureApplyDesignDrift(opts: MeasureApplyDesignDriftOptions): {
|
|
75
|
+
before: ComposedPages;
|
|
76
|
+
after: ComposedPages;
|
|
77
|
+
applyStdout: string;
|
|
78
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two design checks a design-wearing run of `scripts/verify-scaffold.mjs`
|
|
3
|
+
* runs before install (#6823, spec
|
|
4
|
+
* docs/specs/6823-the-matrix-runs-the-derived-spec.md D3).
|
|
5
|
+
*
|
|
6
|
+
* CONTRAST — `assessDesignContrast` turns the rows
|
|
7
|
+
* `packages/theme/scripts/audit-site-theme-contrast.ts` prints (theme's
|
|
8
|
+
* `auditTokenSetContrast` over the design's derived token set) into a verdict
|
|
9
|
+
* naming every non-passing `{bg, fg}` pair and the design. The WCAG math and
|
|
10
|
+
* the pair list stay theme's; this file only decides pass/fail.
|
|
11
|
+
*
|
|
12
|
+
* DRIFT — `jsonLdDrift` / `assessJsonLdDrift` compare every page's composed
|
|
13
|
+
* JSON-LD before and after `apply-design`, excluding the logo image (the PRD
|
|
14
|
+
* metric "structural drift after apply"), and `measureApplyDesignDrift`
|
|
15
|
+
* gathers the two snapshots (the `renderRoute` precedent, #6821 D4): it
|
|
16
|
+
* touches the filesystem and processes only through the paths it is handed,
|
|
17
|
+
* so the driver and `__tests__/verify-scaffold-checks.test.ts` run the same
|
|
18
|
+
* procedure. Nothing here imports `@working-theory/theme`.
|
|
19
|
+
*/
|
|
20
|
+
import { execFileSync } from "node:child_process";
|
|
21
|
+
import { writeFileSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
function describePair(row) {
|
|
24
|
+
const measured = row.status === "unmeasurable" || row.ratio === null
|
|
25
|
+
? "unmeasurable"
|
|
26
|
+
: `${row.ratio.toFixed(2)}:1 < ${row.threshold}:1`;
|
|
27
|
+
return `${row.name} {bg ${row.bg}, fg ${row.fg}} ${measured}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Decide whether `design`'s derived token set passes WCAG AA. Every row whose
|
|
31
|
+
* status is not `pass` fails it; an audit of no pair never passes.
|
|
32
|
+
*/
|
|
33
|
+
export function assessDesignContrast(design, results) {
|
|
34
|
+
const pairs = results.length;
|
|
35
|
+
if (pairs === 0) {
|
|
36
|
+
return { ok: false, pairs, failing: [], reason: `design ${design}: no contrast pair was audited` };
|
|
37
|
+
}
|
|
38
|
+
const failing = results.filter((row) => row.status !== "pass");
|
|
39
|
+
if (failing.length === 0)
|
|
40
|
+
return { ok: true, pairs };
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
pairs,
|
|
44
|
+
failing,
|
|
45
|
+
reason: `design ${design}: ${failing.length} of ${pairs} contrast pair(s) below WCAG AA — ${failing
|
|
46
|
+
.map(describePair)
|
|
47
|
+
.join("; ")}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** schema.org's logo property — the one subtree the PRD's "excluding the logo image" skips. */
|
|
51
|
+
const LOGO_KEY = "logo";
|
|
52
|
+
const kindOf = (value) => value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
|
|
53
|
+
function walk(before, after, path, out) {
|
|
54
|
+
const kind = kindOf(before);
|
|
55
|
+
if (kind !== kindOf(after)) {
|
|
56
|
+
out.push(path);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (kind === "array") {
|
|
60
|
+
const a = before;
|
|
61
|
+
const b = after;
|
|
62
|
+
if (a.length !== b.length)
|
|
63
|
+
out.push(path);
|
|
64
|
+
for (let i = 0; i < Math.min(a.length, b.length); i++)
|
|
65
|
+
walk(a[i], b[i], `${path}/${i}`, out);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (kind === "object") {
|
|
69
|
+
const a = before;
|
|
70
|
+
const b = after;
|
|
71
|
+
for (const key of [...new Set([...Object.keys(a), ...Object.keys(b)])].sort()) {
|
|
72
|
+
if (key === LOGO_KEY)
|
|
73
|
+
continue;
|
|
74
|
+
walk(a[key], b[key], `${path}/${key}`, out);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (before !== after)
|
|
79
|
+
out.push(path);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Every node that differs between two compositions, sorted by page then path.
|
|
83
|
+
* A removed or added page, and a page whose sections no longer validate after,
|
|
84
|
+
* are one pseudo-path node each; a `logo` subtree is never compared.
|
|
85
|
+
*/
|
|
86
|
+
export function jsonLdDrift(before, after) {
|
|
87
|
+
const nodes = [];
|
|
88
|
+
for (const page of [...new Set([...Object.keys(before), ...Object.keys(after)])]) {
|
|
89
|
+
const b = before[page];
|
|
90
|
+
const a = after[page];
|
|
91
|
+
if (a === undefined) {
|
|
92
|
+
nodes.push({ page, path: "(page removed)" });
|
|
93
|
+
}
|
|
94
|
+
else if (b === undefined) {
|
|
95
|
+
nodes.push({ page, path: "(page added)" });
|
|
96
|
+
}
|
|
97
|
+
else if (!a.sectionsValid) {
|
|
98
|
+
nodes.push({ page, path: "(sections invalid)" });
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const paths = [];
|
|
102
|
+
walk(JSON.parse(b.jsonld), JSON.parse(a.jsonld), "", paths);
|
|
103
|
+
for (const path of paths)
|
|
104
|
+
nodes.push({ page, path: path === "" ? "/" : path });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
108
|
+
return nodes.sort((x, y) => cmp(x.page, y.page) || cmp(x.path, y.path));
|
|
109
|
+
}
|
|
110
|
+
/** How many changed nodes a drift reason lists before it stops. */
|
|
111
|
+
const REASON_NODE_LIMIT = 10;
|
|
112
|
+
/** Decide whether `apply-design <design>` left every page's JSON-LD unchanged (logo excluded). */
|
|
113
|
+
export function assessJsonLdDrift(design, before, after) {
|
|
114
|
+
const pages = Object.keys(before).length;
|
|
115
|
+
if (pages === 0) {
|
|
116
|
+
return { ok: false, pages, changed: [], reason: `design ${design}: no page was composed` };
|
|
117
|
+
}
|
|
118
|
+
const changed = jsonLdDrift(before, after);
|
|
119
|
+
if (changed.length === 0)
|
|
120
|
+
return { ok: true, pages };
|
|
121
|
+
const listed = changed
|
|
122
|
+
.slice(0, REASON_NODE_LIMIT)
|
|
123
|
+
.map((n) => `${n.page} ${n.path}`)
|
|
124
|
+
.join("; ");
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
pages,
|
|
128
|
+
changed,
|
|
129
|
+
reason: `design ${design}: ${changed.length} JSON-LD node(s) changed by apply-design — ${listed}${changed.length > REASON_NODE_LIMIT ? "; …" : ""}`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Scaffold a keyless `organization-product` site on `standard`, compose its
|
|
134
|
+
* JSON-LD, run `apply-design <design>` on it, and compose again. A non-zero
|
|
135
|
+
* exit at any step throws naming the step and the child's stderr.
|
|
136
|
+
*/
|
|
137
|
+
export function measureApplyDesignDrift(opts) {
|
|
138
|
+
const baseEnv = { ...process.env };
|
|
139
|
+
delete baseEnv["AILK_LICENSE_KEY"];
|
|
140
|
+
const registryEnv = opts.registryUrl === undefined
|
|
141
|
+
? {}
|
|
142
|
+
: { AILK_MODULE_REGISTRY_URL: opts.registryUrl, AILK_TEMPLATE_REGISTRY_URL: opts.registryUrl };
|
|
143
|
+
const site = join(opts.workDir, "site");
|
|
144
|
+
const exec = (label, cmd, args, env) => {
|
|
145
|
+
try {
|
|
146
|
+
return execFileSync(cmd, args, {
|
|
147
|
+
encoding: "utf8",
|
|
148
|
+
env: { ...baseEnv, ...env },
|
|
149
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
150
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
const { stderr, message } = err;
|
|
155
|
+
throw new Error(`drift: ${label} failed: ${(stderr || message || "").trim()}`, { cause: err });
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
const compose = (label) => {
|
|
159
|
+
const stdout = exec(label, opts.tsxPath, [opts.composerPath, site], { TSX_TSCONFIG_PATH: opts.composerTsconfig });
|
|
160
|
+
try {
|
|
161
|
+
return JSON.parse(stdout);
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
throw new Error(`drift: ${label} printed no JSON: ${err.message}`, { cause: err });
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
exec("scaffold organization-product on standard", process.execPath, [opts.cliPath, site, "--shape", "organization-product", "--design", "standard"], registryEnv);
|
|
168
|
+
if (opts.licenseKey !== undefined)
|
|
169
|
+
writeFileSync(join(site, "ailk.key"), `${opts.licenseKey}\n`);
|
|
170
|
+
const before = compose("compose JSON-LD before apply-design");
|
|
171
|
+
const applyStdout = exec(`apply-design ${opts.design}`, process.execPath, [opts.cliPath, "apply-design", opts.design, "--target", site], opts.registryUrl === undefined ? {} : { AILK_MODULE_REGISTRY_URL: opts.registryUrl });
|
|
172
|
+
const after = compose("compose JSON-LD after apply-design");
|
|
173
|
+
return { before, after, applyStdout };
|
|
174
|
+
}
|
|
@@ -655,6 +655,11 @@ export function installBundleTree(options) {
|
|
|
655
655
|
* path-safety check is needed here — `srcRoot` is our own staging tree, not
|
|
656
656
|
* attacker-controlled input.
|
|
657
657
|
*
|
|
658
|
+
* A directory is created only as the parent of a file actually copied, never
|
|
659
|
+
* for its own sake: a source directory whose every file is excluded (mapped
|
|
660
|
+
* away) leaves nothing behind at its original path (#6787 — an empty root
|
|
661
|
+
* `content/en/blog/` reads as an unregistered page type).
|
|
662
|
+
*
|
|
658
663
|
* `markers`, when given, is substituted into every TEXT file on the way
|
|
659
664
|
* through (#6460) — see `InstallBundleTreeOptions.markers`.
|
|
660
665
|
*/
|
|
@@ -669,7 +674,6 @@ function copyTree(srcRoot, destRoot, excluded, markers, policy, report) {
|
|
|
669
674
|
const rel = relative(srcRoot, srcPath);
|
|
670
675
|
const destPath = join(destRoot, rel);
|
|
671
676
|
if (entry.isDirectory()) {
|
|
672
|
-
mkdirSync(destPath, { recursive: true });
|
|
673
677
|
stack.push(srcPath);
|
|
674
678
|
}
|
|
675
679
|
else if (entry.isFile()) {
|
|
@@ -6,6 +6,15 @@ export declare const LICENSE_ENV_VAR = "AILK_LICENSE_KEY";
|
|
|
6
6
|
* describes the rule, never the value.
|
|
7
7
|
*/
|
|
8
8
|
export declare function assertLicenseKeyShape(key: string): void;
|
|
9
|
+
/**
|
|
10
|
+
* Write `content` only when it differs, with mode 0600 either way. An existing
|
|
11
|
+
* file is tightened BEFORE the write — `mode` applies only on creation, so a
|
|
12
|
+
* looser pre-existing `.env` would otherwise hold the key for a moment at its
|
|
13
|
+
* old permissions.
|
|
14
|
+
*/
|
|
15
|
+
export declare function writeOwnerOnly(path: string, content: string): void;
|
|
16
|
+
/** `text` plus `line`, starting on a fresh line. */
|
|
17
|
+
export declare function appendLine(text: string, line: string): string;
|
|
9
18
|
/**
|
|
10
19
|
* Persist `key` into the scaffold at `dir`: `ailk.key` (0600), the
|
|
11
20
|
* `apps/api/.env` line (0600, other lines kept), and a `.gitignore` entry for
|
|
@@ -48,7 +48,7 @@ export function assertLicenseKeyShape(key) {
|
|
|
48
48
|
* looser pre-existing `.env` would otherwise hold the key for a moment at its
|
|
49
49
|
* old permissions.
|
|
50
50
|
*/
|
|
51
|
-
function writeOwnerOnly(path, content) {
|
|
51
|
+
export function writeOwnerOnly(path, content) {
|
|
52
52
|
mkdirSync(dirname(path), { recursive: true });
|
|
53
53
|
if (existsSync(path))
|
|
54
54
|
chmodSync(path, 0o600);
|
|
@@ -58,7 +58,7 @@ function writeOwnerOnly(path, content) {
|
|
|
58
58
|
chmodSync(path, 0o600);
|
|
59
59
|
}
|
|
60
60
|
/** `text` plus `line`, starting on a fresh line. */
|
|
61
|
-
function appendLine(text, line) {
|
|
61
|
+
export function appendLine(text, line) {
|
|
62
62
|
const sep = text.length === 0 || text.endsWith("\n") ? "" : "\n";
|
|
63
63
|
return `${text}${sep}${line}\n`;
|
|
64
64
|
}
|
|
@@ -16,8 +16,13 @@
|
|
|
16
16
|
* `resolvePaidModules` returns immediately, before touching `process.env` or
|
|
17
17
|
* the network, so a bare/all-Free scaffold is provably unaffected by this
|
|
18
18
|
* gate (K1's regression floor).
|
|
19
|
+
*
|
|
20
|
+
* Also carries the design gate (#6760): `assertDesignAllowed` refuses a
|
|
21
|
+
* non-`standard` design at the `free` level, naming the design and the
|
|
22
|
+
* run's level, before `scaffold()`'s first disk write.
|
|
19
23
|
*/
|
|
20
24
|
import type { ModuleName } from "../parse-args.js";
|
|
25
|
+
import { type Level } from "./module-tier.js";
|
|
21
26
|
/**
|
|
22
27
|
* Where a refused run sends the buyer. `/pricing`, not `/pro`: there is no
|
|
23
28
|
* `/pro` route and there will not be one (#6439, operator decision OD-2) —
|
|
@@ -33,6 +38,29 @@ export declare const CONVERSION_HINT_URL = "https://ailaunchkit.ai/pricing";
|
|
|
33
38
|
export declare class LicenseRequiredError extends Error {
|
|
34
39
|
constructor(message: string);
|
|
35
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Design names every level renders (#6760 D1). Anything else in
|
|
43
|
+
* `STARTER_DESIGNS` is Pro and up. A path design's Free-ness is decided by
|
|
44
|
+
* `resolveDesign`'s identity rule (#6813 D2), never by this list directly —
|
|
45
|
+
* `assertDesignAllowed`'s `free` parameter carries that verdict in.
|
|
46
|
+
*/
|
|
47
|
+
export declare const FREE_DESIGNS: readonly string[];
|
|
48
|
+
/** Levels that render any design (#6760 D1) — an allow-list: an unknown level is not in it. */
|
|
49
|
+
export declare const DESIGN_LEVELS: readonly Level[];
|
|
50
|
+
/**
|
|
51
|
+
* Refuse a non-Free design at a level below Pro (#6760 D1, D6; widened to a
|
|
52
|
+
* path design #6813 D7). Pure and synchronous — takes the level
|
|
53
|
+
* `deriveLevel`/`resolveModuleSet` already resolved; never reads a key or
|
|
54
|
+
* derives a level itself (D2, fail-closed: both checks below are allow-lists,
|
|
55
|
+
* so an unrecognized `level` refuses rather than granting it).
|
|
56
|
+
*
|
|
57
|
+
* @param free Whether `design` is Free — defaults to `FREE_DESIGNS.includes(design)`
|
|
58
|
+
* (a design NAME's own answer), so a caller passing a name never changes
|
|
59
|
+
* behaviour. A path design's caller (`programmatic.ts`, off `resolveDesign`'s
|
|
60
|
+
* identity rule) passes it explicitly: a byte-identical copy of a shipped
|
|
61
|
+
* Free design is Free; any other path is not, whatever it is named.
|
|
62
|
+
*/
|
|
63
|
+
export declare function assertDesignAllowed(design: string | undefined, level: Level, free?: boolean): void;
|
|
36
64
|
export interface ResolvePaidModulesOptions {
|
|
37
65
|
/** The requested module set's paid subset (see `module-tier.ts`). */
|
|
38
66
|
paidModules: ModuleName[];
|
|
@@ -52,7 +80,20 @@ export interface ResolvePaidModulesOptions {
|
|
|
52
80
|
* to do. Absent ⟹ a plain `--modules` run, whose messages are unchanged.
|
|
53
81
|
*/
|
|
54
82
|
templateName?: string;
|
|
83
|
+
/**
|
|
84
|
+
* The key's derived level (#6759 D5), when one was derived on this call's
|
|
85
|
+
* path. Absent on the `--modules`-keep branch, where no level is derived
|
|
86
|
+
* (NFR-2) — the refusal then names only the module and its own level,
|
|
87
|
+
* never the caller's.
|
|
88
|
+
*/
|
|
89
|
+
level?: Level;
|
|
55
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* The message phrase for one module (D5): what level includes it, or that it
|
|
93
|
+
* is an add-on. Pure — derived from `levelOf`, never from the current run's
|
|
94
|
+
* own derived level.
|
|
95
|
+
*/
|
|
96
|
+
export declare function levelPhrase(m: ModuleName): string;
|
|
56
97
|
/**
|
|
57
98
|
* Resolve every requested paid module to its fetched bundle bytes, or throw
|
|
58
99
|
* `LicenseRequiredError` naming exactly why the run cannot proceed. Never
|
|
@@ -61,3 +102,51 @@ export interface ResolvePaidModulesOptions {
|
|
|
61
102
|
* BEFORE the scaffold copy — see `programmatic.ts`'s `scaffold()`).
|
|
62
103
|
*/
|
|
63
104
|
export declare function resolvePaidModules(options: ResolvePaidModulesOptions): Promise<Map<ModuleName, Uint8Array>>;
|
|
105
|
+
/** Inputs shared by `deriveLevel` and `resolveModuleSet` (D4/D5). */
|
|
106
|
+
export interface GateOptions {
|
|
107
|
+
/** Defaults to `process.env["AILK_LICENSE_KEY"]`. Injectable for tests. */
|
|
108
|
+
licenseKey?: string | undefined;
|
|
109
|
+
/** Forwarded to `fetchModule`; defaults to `AILK_MODULE_REGISTRY_URL` / the built-in default. */
|
|
110
|
+
registryUrl?: string;
|
|
111
|
+
/** Forwarded to `fetchModule`; defaults to this package's own version. */
|
|
112
|
+
version?: string;
|
|
113
|
+
/** Forwarded to `fetchModule`; defaults to `globalThis.fetch`. Never a live call in tests. */
|
|
114
|
+
fetchImpl?: typeof fetch;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Derive the key's level from what the registry actually answers (D4) — the
|
|
118
|
+
* level only chooses which modules to ASK FOR; every byte is still
|
|
119
|
+
* authorized by the registry per module. Nothing client-side is an
|
|
120
|
+
* entitlement claim: the JWT is never parsed here.
|
|
121
|
+
*
|
|
122
|
+
* No key ⟹ `"free"`, zero network. A present key is shape-checked first,
|
|
123
|
+
* whichever source it came from (SEC-2). Then: probe every paid member of
|
|
124
|
+
* `LEVEL_MODULES.pro`, in order — a licence error on any of them is refused
|
|
125
|
+
* (a key that does not unlock all of Pro is not silently downgraded); any
|
|
126
|
+
* other failure (404, 5xx, unreachable) is wrapped so the caller sees it
|
|
127
|
+
* could not determine the level, and is told the escape hatch (`--modules`).
|
|
128
|
+
* Then probe `multi-tenant` (what Pro Plus adds beyond Pro): a licence error
|
|
129
|
+
* there is the expected "this key is Pro, not Pro Plus" answer, not a
|
|
130
|
+
* refusal.
|
|
131
|
+
*/
|
|
132
|
+
export declare function deriveLevel(options: GateOptions): Promise<{
|
|
133
|
+
level: Level;
|
|
134
|
+
bundles: Map<ModuleName, Uint8Array>;
|
|
135
|
+
}>;
|
|
136
|
+
/**
|
|
137
|
+
* The one entry point that turns `--modules` + the key into the effective
|
|
138
|
+
* module set and its resolved bundles (D5) — `programmatic.ts`'s `scaffold()`
|
|
139
|
+
* calls this for every run except a named `--template` alias (NFR-4).
|
|
140
|
+
*
|
|
141
|
+
* A named list with a non-addition (the KEEP branch, D2) resolves it exactly
|
|
142
|
+
* as today, with no level derived and no extra request (NFR-2). Otherwise the
|
|
143
|
+
* level is derived first, the set composed from it, and any named addition
|
|
144
|
+
* not already fetched during the level probe is resolved on top.
|
|
145
|
+
*/
|
|
146
|
+
export declare function resolveModuleSet(options: GateOptions & {
|
|
147
|
+
named?: readonly ModuleName[] | undefined;
|
|
148
|
+
}): Promise<{
|
|
149
|
+
modules: ModuleName[];
|
|
150
|
+
bundles: Map<ModuleName, Uint8Array>;
|
|
151
|
+
level?: Level;
|
|
152
|
+
}>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { FetchModuleError, fetchModule } from "./fetch-module.js";
|
|
2
|
+
import { assertLicenseKeyShape } from "./license-key-files.js";
|
|
3
|
+
import { ADD_ONS, LEVEL_LABEL, LEVEL_MODULES, OPT_IN_MODULES, PAID_MODULES, classifyModules, composeModuleSet, levelOf, } from "./module-tier.js";
|
|
2
4
|
/**
|
|
3
5
|
* Where a refused run sends the buyer. `/pricing`, not `/pro`: there is no
|
|
4
6
|
* `/pro` route and there will not be one (#6439, operator decision OD-2) —
|
|
@@ -17,6 +19,64 @@ export class LicenseRequiredError extends Error {
|
|
|
17
19
|
this.name = "LicenseRequiredError";
|
|
18
20
|
}
|
|
19
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Design names every level renders (#6760 D1). Anything else in
|
|
24
|
+
* `STARTER_DESIGNS` is Pro and up. A path design's Free-ness is decided by
|
|
25
|
+
* `resolveDesign`'s identity rule (#6813 D2), never by this list directly —
|
|
26
|
+
* `assertDesignAllowed`'s `free` parameter carries that verdict in.
|
|
27
|
+
*/
|
|
28
|
+
export const FREE_DESIGNS = ["standard"];
|
|
29
|
+
/** Levels that render any design (#6760 D1) — an allow-list: an unknown level is not in it. */
|
|
30
|
+
export const DESIGN_LEVELS = ["pro", "pro-plus"];
|
|
31
|
+
/**
|
|
32
|
+
* Refuse a non-Free design at a level below Pro (#6760 D1, D6; widened to a
|
|
33
|
+
* path design #6813 D7). Pure and synchronous — takes the level
|
|
34
|
+
* `deriveLevel`/`resolveModuleSet` already resolved; never reads a key or
|
|
35
|
+
* derives a level itself (D2, fail-closed: both checks below are allow-lists,
|
|
36
|
+
* so an unrecognized `level` refuses rather than granting it).
|
|
37
|
+
*
|
|
38
|
+
* @param free Whether `design` is Free — defaults to `FREE_DESIGNS.includes(design)`
|
|
39
|
+
* (a design NAME's own answer), so a caller passing a name never changes
|
|
40
|
+
* behaviour. A path design's caller (`programmatic.ts`, off `resolveDesign`'s
|
|
41
|
+
* identity rule) passes it explicitly: a byte-identical copy of a shipped
|
|
42
|
+
* Free design is Free; any other path is not, whatever it is named.
|
|
43
|
+
*/
|
|
44
|
+
export function assertDesignAllowed(design, level, free = design !== undefined && FREE_DESIGNS.includes(design)) {
|
|
45
|
+
if (design === undefined || free)
|
|
46
|
+
return;
|
|
47
|
+
if (DESIGN_LEVELS.includes(level))
|
|
48
|
+
return;
|
|
49
|
+
throw new LicenseRequiredError(`design "${design}" is a Pro feature — this run's licence level is ${LEVEL_LABEL[level]}; pass --license <key> or set AILK_LICENSE_KEY with a Pro or Pro Plus key → ${CONVERSION_HINT_URL}`);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The message phrase for one module (D5): what level includes it, or that it
|
|
53
|
+
* is an add-on. Pure — derived from `levelOf`, never from the current run's
|
|
54
|
+
* own derived level.
|
|
55
|
+
*/
|
|
56
|
+
export function levelPhrase(m) {
|
|
57
|
+
const level = levelOf(m);
|
|
58
|
+
if (level === "add-on") {
|
|
59
|
+
return `"${m}" is an add-on, bought on top of Pro or Pro Plus`;
|
|
60
|
+
}
|
|
61
|
+
if (level === "free") {
|
|
62
|
+
return `"${m}" is included at ${LEVEL_LABEL.free}`;
|
|
63
|
+
}
|
|
64
|
+
return `"${m}" is included at ${LEVEL_LABEL[level]}`;
|
|
65
|
+
}
|
|
66
|
+
/** M1 / M1t (D5): the no-key refusal, with or without a named template. */
|
|
67
|
+
function noKeyMessage(paidModules, templateName) {
|
|
68
|
+
const phrases = paidModules.map((m) => levelPhrase(m)).join("; ");
|
|
69
|
+
const intro = templateName !== undefined
|
|
70
|
+
? `template "${templateName}" needs a licence: ${phrases}`
|
|
71
|
+
: `no licence key, so this is a Free scaffold: ${phrases}`;
|
|
72
|
+
return `${intro} — set AILK_LICENSE_KEY first → ${CONVERSION_HINT_URL}`;
|
|
73
|
+
}
|
|
74
|
+
/** M2 (D5): a key the registry rejected for one module. */
|
|
75
|
+
function licenseErrorMessage(err, moduleName, templateName, level) {
|
|
76
|
+
const templateSuffix = templateName !== undefined ? ` (required by template "${templateName}")` : "";
|
|
77
|
+
const levelSuffix = level !== undefined ? ` — your licence is at ${LEVEL_LABEL[level]}` : "";
|
|
78
|
+
return `${err.message}; ${levelPhrase(moduleName)}${templateSuffix}${levelSuffix} → ${CONVERSION_HINT_URL}`;
|
|
79
|
+
}
|
|
20
80
|
/**
|
|
21
81
|
* Resolve every requested paid module to its fetched bundle bytes, or throw
|
|
22
82
|
* `LicenseRequiredError` naming exactly why the run cannot proceed. Never
|
|
@@ -33,8 +93,11 @@ export async function resolvePaidModules(options) {
|
|
|
33
93
|
}
|
|
34
94
|
const token = options.licenseKey ?? process.env["AILK_LICENSE_KEY"];
|
|
35
95
|
if (token === undefined || token.length === 0) {
|
|
36
|
-
throw new LicenseRequiredError(
|
|
96
|
+
throw new LicenseRequiredError(noKeyMessage(paidModules, options.templateName));
|
|
37
97
|
}
|
|
98
|
+
// SEC-2 — the `--license` flag and the env var are validated alike,
|
|
99
|
+
// whichever source this token came from, before it reaches a request.
|
|
100
|
+
assertLicenseKeyShape(token);
|
|
38
101
|
const bundles = new Map();
|
|
39
102
|
for (const moduleName of paidModules) {
|
|
40
103
|
try {
|
|
@@ -55,11 +118,9 @@ export async function resolvePaidModules(options) {
|
|
|
55
118
|
if (err instanceof FetchModuleError) {
|
|
56
119
|
if (err.isLicenseError) {
|
|
57
120
|
// DD5 row 2: the mapped 401/403 already names the module the key
|
|
58
|
-
// does not cover; the template
|
|
59
|
-
// WHICH of the things they asked for needs it.
|
|
60
|
-
throw new LicenseRequiredError(options.templateName
|
|
61
|
-
? `${err.message}, required by template "${options.templateName}" → ${CONVERSION_HINT_URL}`
|
|
62
|
-
: `${err.message} → ${CONVERSION_HINT_URL}`);
|
|
121
|
+
// does not cover; the template and level are appended so the buyer
|
|
122
|
+
// can see WHICH of the things they asked for needs it.
|
|
123
|
+
throw new LicenseRequiredError(licenseErrorMessage(err, moduleName, options.templateName, options.level));
|
|
63
124
|
}
|
|
64
125
|
// Not a license-gating signal (e.g. 404 "not yet available", or the
|
|
65
126
|
// registry unreachable) — a generic failure, not the honest-denial
|
|
@@ -72,10 +133,108 @@ export async function resolvePaidModules(options) {
|
|
|
72
133
|
}
|
|
73
134
|
return bundles;
|
|
74
135
|
}
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
?
|
|
79
|
-
:
|
|
80
|
-
|
|
136
|
+
function forward(options) {
|
|
137
|
+
return {
|
|
138
|
+
...(options.registryUrl !== undefined ? { registryUrl: options.registryUrl } : {}),
|
|
139
|
+
...(options.version !== undefined ? { version: options.version } : {}),
|
|
140
|
+
...(options.fetchImpl !== undefined ? { fetchImpl: options.fetchImpl } : {}),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Derive the key's level from what the registry actually answers (D4) — the
|
|
145
|
+
* level only chooses which modules to ASK FOR; every byte is still
|
|
146
|
+
* authorized by the registry per module. Nothing client-side is an
|
|
147
|
+
* entitlement claim: the JWT is never parsed here.
|
|
148
|
+
*
|
|
149
|
+
* No key ⟹ `"free"`, zero network. A present key is shape-checked first,
|
|
150
|
+
* whichever source it came from (SEC-2). Then: probe every paid member of
|
|
151
|
+
* `LEVEL_MODULES.pro`, in order — a licence error on any of them is refused
|
|
152
|
+
* (a key that does not unlock all of Pro is not silently downgraded); any
|
|
153
|
+
* other failure (404, 5xx, unreachable) is wrapped so the caller sees it
|
|
154
|
+
* could not determine the level, and is told the escape hatch (`--modules`).
|
|
155
|
+
* Then probe `multi-tenant` (what Pro Plus adds beyond Pro): a licence error
|
|
156
|
+
* there is the expected "this key is Pro, not Pro Plus" answer, not a
|
|
157
|
+
* refusal.
|
|
158
|
+
*/
|
|
159
|
+
export async function deriveLevel(options) {
|
|
160
|
+
const token = options.licenseKey ?? process.env["AILK_LICENSE_KEY"];
|
|
161
|
+
if (token === undefined || token.length === 0) {
|
|
162
|
+
return { level: "free", bundles: new Map() };
|
|
163
|
+
}
|
|
164
|
+
assertLicenseKeyShape(token);
|
|
165
|
+
const bundles = new Map();
|
|
166
|
+
const proModules = LEVEL_MODULES.pro.filter((m) => PAID_MODULES.includes(m));
|
|
167
|
+
for (const moduleName of proModules) {
|
|
168
|
+
try {
|
|
169
|
+
const bytes = await fetchModule({ moduleName, token, ...forward(options) });
|
|
170
|
+
bundles.set(moduleName, bytes);
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
if (err instanceof FetchModuleError) {
|
|
174
|
+
if (err.isLicenseError) {
|
|
175
|
+
throw new LicenseRequiredError(licenseErrorMessage(err, moduleName));
|
|
176
|
+
}
|
|
177
|
+
throw new FetchModuleError(`could not determine your licence level: ${err.message} — name the modules you want with --modules`);
|
|
178
|
+
}
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const bytes = await fetchModule({
|
|
184
|
+
moduleName: "multi-tenant",
|
|
185
|
+
token,
|
|
186
|
+
...forward(options),
|
|
187
|
+
});
|
|
188
|
+
bundles.set("multi-tenant", bytes);
|
|
189
|
+
return { level: "pro-plus", bundles };
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
if (err instanceof FetchModuleError) {
|
|
193
|
+
if (err.isLicenseError) {
|
|
194
|
+
return { level: "pro", bundles };
|
|
195
|
+
}
|
|
196
|
+
throw new FetchModuleError(`could not determine your licence level: ${err.message} — name the modules you want with --modules`);
|
|
197
|
+
}
|
|
198
|
+
throw err;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* The one entry point that turns `--modules` + the key into the effective
|
|
203
|
+
* module set and its resolved bundles (D5) — `programmatic.ts`'s `scaffold()`
|
|
204
|
+
* calls this for every run except a named `--template` alias (NFR-4).
|
|
205
|
+
*
|
|
206
|
+
* A named list with a non-addition (the KEEP branch, D2) resolves it exactly
|
|
207
|
+
* as today, with no level derived and no extra request (NFR-2). Otherwise the
|
|
208
|
+
* level is derived first, the set composed from it, and any named addition
|
|
209
|
+
* not already fetched during the level probe is resolved on top.
|
|
210
|
+
*/
|
|
211
|
+
export async function resolveModuleSet(options) {
|
|
212
|
+
const namedList = options.named ?? [];
|
|
213
|
+
const additions = [...ADD_ONS, ...OPT_IN_MODULES];
|
|
214
|
+
const isKeep = namedList.some((m) => !additions.includes(m));
|
|
215
|
+
if (isKeep) {
|
|
216
|
+
const modules = composeModuleSet(namedList, "free");
|
|
217
|
+
const { paid } = classifyModules(modules);
|
|
218
|
+
const bundles = await resolvePaidModules({
|
|
219
|
+
paidModules: paid,
|
|
220
|
+
licenseKey: options.licenseKey,
|
|
221
|
+
...forward(options),
|
|
222
|
+
});
|
|
223
|
+
return { modules, bundles };
|
|
224
|
+
}
|
|
225
|
+
const { level, bundles } = await deriveLevel(options);
|
|
226
|
+
const modules = composeModuleSet(namedList, level);
|
|
227
|
+
const { paid } = classifyModules(modules);
|
|
228
|
+
const missing = paid.filter((m) => !bundles.has(m));
|
|
229
|
+
if (missing.length > 0) {
|
|
230
|
+
const extra = await resolvePaidModules({
|
|
231
|
+
paidModules: missing,
|
|
232
|
+
licenseKey: options.licenseKey,
|
|
233
|
+
level,
|
|
234
|
+
...forward(options),
|
|
235
|
+
});
|
|
236
|
+
for (const [k, v] of extra)
|
|
237
|
+
bundles.set(k, v);
|
|
238
|
+
}
|
|
239
|
+
return { modules, bundles, level };
|
|
81
240
|
}
|