create-ailk 0.1.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/LICENSE +201 -0
- package/README.md +39 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +80 -0
- package/dist/copy.d.ts +8 -0
- package/dist/copy.js +109 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/lib/pro-callout.d.ts +12 -0
- package/dist/lib/pro-callout.js +20 -0
- package/dist/parse-args.d.ts +13 -0
- package/dist/parse-args.js +81 -0
- package/dist/programmatic.d.ts +49 -0
- package/dist/programmatic.js +89 -0
- package/dist/substitute.d.ts +31 -0
- package/dist/substitute.js +45 -0
- package/dist/validate.d.ts +8 -0
- package/dist/validate.js +70 -0
- package/package.json +47 -0
- package/templates/.claude/.gitkeep +0 -0
- package/templates/.claude/agents/web.md +80 -0
- package/templates/.claude/rules/architecture.md +62 -0
- package/templates/.claude/rules/preserve-existing-style.md +51 -0
- package/templates/.claude/skills/README.md +43 -0
- package/templates/.claude/skills/add-schema/SKILL.md +270 -0
- package/templates/.claude/skills/add-schema/references/conflict-handling.md +200 -0
- package/templates/.claude/skills/add-schema/references/file-finding.md +123 -0
- package/templates/.claude/skills/add-schema/references/schema-types.md +131 -0
- package/templates/.claude/skills/audit-site/SKILL.md +213 -0
- package/templates/.claude/skills/audit-site/references/common-errors.md +113 -0
- package/templates/.claude/skills/audit-site/references/findings-glossary.md +138 -0
- package/templates/.claude/skills/audit-site/references/severity-levels.md +114 -0
- package/templates/.claude/skills/provision-config/SKILL.md +243 -0
- package/templates/.claude/skills/provision-config/references/env-vars-template.md +184 -0
- package/templates/.claude/skills/provision-config/references/runbook-template.md +248 -0
- package/templates/.claude/skills/provision-config/references/vercel-json-template.md +98 -0
- package/templates/.claude/skills/scaffold-auth/SKILL.md +261 -0
- package/templates/.claude/skills/scaffold-auth/references/env-vars.md +91 -0
- package/templates/.claude/skills/scaffold-auth/references/oauth-app-setup.md +171 -0
- package/templates/.claude/skills/scaffold-commerce/SKILL.md +310 -0
- package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +71 -0
- package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +83 -0
- package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +72 -0
- package/templates/.claude/skills/scaffold-page/SKILL.md +243 -0
- package/templates/.claude/skills/scaffold-page/references/page-types.md +169 -0
- package/templates/.claude/skills/scaffold-page/templates/content.mdx.tmpl +28 -0
- package/templates/.claude/skills/scaffold-page/templates/route.tsx.tmpl +28 -0
- package/templates/.claude/skills/suggest-site-pages/SKILL.md +242 -0
- package/templates/.claude/skills/suggest-site-pages/references/categories.md +287 -0
- package/templates/.env.example +205 -0
- package/templates/.gitkeep +2 -0
- package/templates/CLAUDE.md +142 -0
- package/templates/CONVENTIONS.md +222 -0
- package/templates/README.md +110 -0
- package/templates/apps/api/.env.example +40 -0
- package/templates/apps/api/CLAUDE.md +63 -0
- package/templates/apps/api/api/index.ts +144 -0
- package/templates/apps/api/jest.config.cjs +74 -0
- package/templates/apps/api/package.json +54 -0
- package/templates/apps/api/src/__tests__/factories/content-fixture.ts +138 -0
- package/templates/apps/api/src/__tests__/factories/index.ts +70 -0
- package/templates/apps/api/src/__tests__/factories/lead-fixture.ts +82 -0
- package/templates/apps/api/src/__tests__/index.test.ts +118 -0
- package/templates/apps/api/src/__tests__/server.test.ts +207 -0
- package/templates/apps/api/src/__tests__/services/enrichment-shim.test.ts +126 -0
- package/templates/apps/api/src/__tests__/setup.ts +24 -0
- package/templates/apps/api/src/cli.ts +30 -0
- package/templates/apps/api/src/config/__tests__/index.test.ts +97 -0
- package/templates/apps/api/src/config/index.ts +56 -0
- package/templates/apps/api/src/index.ts +32 -0
- package/templates/apps/api/src/lib/__mocks__/prisma-client.js +15 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +45 -0
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +178 -0
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +100 -0
- package/templates/apps/api/src/lib/__tests__/errors.test.ts +94 -0
- package/templates/apps/api/src/lib/__tests__/file-fallback.test.ts +144 -0
- package/templates/apps/api/src/lib/__tests__/fixtures/HomePage/home.mdx +8 -0
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +227 -0
- package/templates/apps/api/src/lib/__tests__/outbound-http.test.ts +125 -0
- package/templates/apps/api/src/lib/__tests__/prisma.test.ts +165 -0
- package/templates/apps/api/src/lib/__tests__/require-paid-tier.test.ts +129 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +74 -0
- package/templates/apps/api/src/lib/content-adapter.ts +86 -0
- package/templates/apps/api/src/lib/error-serializer.ts +77 -0
- package/templates/apps/api/src/lib/errors.ts +95 -0
- package/templates/apps/api/src/lib/file-fallback.ts +87 -0
- package/templates/apps/api/src/lib/lead-capture-store.ts +110 -0
- package/templates/apps/api/src/lib/locales.ts +201 -0
- package/templates/apps/api/src/lib/outbound-http.ts +122 -0
- package/templates/apps/api/src/lib/prisma.ts +21 -0
- package/templates/apps/api/src/lib/require-paid-tier.ts +53 -0
- package/templates/apps/api/src/lib/stripe.ts +30 -0
- package/templates/apps/api/src/lib/webhook-idempotency.ts +92 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +111 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +172 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +119 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +138 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +149 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +139 -0
- package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +175 -0
- package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +76 -0
- package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +153 -0
- package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +72 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +292 -0
- package/templates/apps/api/src/middleware/auth.ts +246 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +132 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +129 -0
- package/templates/apps/api/src/openapi/index.ts +12 -0
- package/templates/apps/api/src/openapi/spec.ts +408 -0
- package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +193 -0
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +201 -0
- package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +294 -0
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +486 -0
- package/templates/apps/api/src/routes/auth/tokens.ts +199 -0
- package/templates/apps/api/src/routes/auth/user.ts +244 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +588 -0
- package/templates/apps/api/src/routes/checkout/index.ts +13 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +185 -0
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +239 -0
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +201 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +141 -0
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +122 -0
- package/templates/apps/api/src/routes/content/__tests__/list-types.test.ts +44 -0
- package/templates/apps/api/src/routes/content/__tests__/locale.test.ts +118 -0
- package/templates/apps/api/src/routes/content/__tests__/rate-limit.test.ts +51 -0
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +252 -0
- package/templates/apps/api/src/routes/content/create.ts +114 -0
- package/templates/apps/api/src/routes/content/delete.ts +81 -0
- package/templates/apps/api/src/routes/content/get-page.ts +98 -0
- package/templates/apps/api/src/routes/content/index.ts +32 -0
- package/templates/apps/api/src/routes/content/list-pages.ts +83 -0
- package/templates/apps/api/src/routes/content/list-types.ts +19 -0
- package/templates/apps/api/src/routes/content/rate-limit.ts +13 -0
- package/templates/apps/api/src/routes/content/slug.ts +9 -0
- package/templates/apps/api/src/routes/content/update.ts +111 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +415 -0
- package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +165 -0
- package/templates/apps/api/src/routes/leads/index.ts +130 -0
- package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +296 -0
- package/templates/apps/api/src/routes/test/auth/session.ts +141 -0
- package/templates/apps/api/src/routes/webhooks/README.md +57 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +247 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +198 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +633 -0
- package/templates/apps/api/src/routes/webhooks/index.ts +41 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +253 -0
- package/templates/apps/api/src/server.ts +157 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +395 -0
- package/templates/apps/api/src/services/enrichment-shim.ts +53 -0
- package/templates/apps/api/src/services/lead-routing.ts +338 -0
- package/templates/apps/api/tsconfig.json +34 -0
- package/templates/apps/api/vercel.json +17 -0
- package/templates/apps/mcp/.env.example +52 -0
- package/templates/apps/mcp/CLAUDE.md +84 -0
- package/templates/apps/mcp/__tests__/abuse/daily-cap.test.ts +52 -0
- package/templates/apps/mcp/__tests__/abuse/email-validator.test.ts +46 -0
- package/templates/apps/mcp/__tests__/abuse/honeypot.test.ts +36 -0
- package/templates/apps/mcp/__tests__/abuse/user-agent-deny.test.ts +54 -0
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +110 -0
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +213 -0
- package/templates/apps/mcp/__tests__/audit.test.ts +217 -0
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +52 -0
- package/templates/apps/mcp/__tests__/create_page.test.ts +201 -0
- package/templates/apps/mcp/__tests__/delete_page.test.ts +176 -0
- package/templates/apps/mcp/__tests__/get_business_hours.test.ts +82 -0
- package/templates/apps/mcp/__tests__/get_faq.test.ts +80 -0
- package/templates/apps/mcp/__tests__/get_locations.test.ts +65 -0
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +90 -0
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +80 -0
- package/templates/apps/mcp/__tests__/get_site_info.test.ts +81 -0
- package/templates/apps/mcp/__tests__/http-transport.test.ts +323 -0
- package/templates/apps/mcp/__tests__/lib/errors.test.ts +75 -0
- package/templates/apps/mcp/__tests__/lib/require-paid-tier.test.ts +75 -0
- package/templates/apps/mcp/__tests__/list_pages.test.ts +112 -0
- package/templates/apps/mcp/__tests__/locale.test.ts +135 -0
- package/templates/apps/mcp/__tests__/parity.test.ts +205 -0
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +34 -0
- package/templates/apps/mcp/__tests__/read_content.test.ts +153 -0
- package/templates/apps/mcp/__tests__/request_callback.test.ts +163 -0
- package/templates/apps/mcp/__tests__/search_content.test.ts +153 -0
- package/templates/apps/mcp/__tests__/server.test.ts +115 -0
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +165 -0
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +161 -0
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +227 -0
- package/templates/apps/mcp/__tests__/tools/scaffold.test.ts +237 -0
- package/templates/apps/mcp/__tests__/update_page.test.ts +195 -0
- package/templates/apps/mcp/jest.config.cjs +82 -0
- package/templates/apps/mcp/package.json +37 -0
- package/templates/apps/mcp/src/abuse/daily-cap.ts +47 -0
- package/templates/apps/mcp/src/abuse/email-validator.ts +25 -0
- package/templates/apps/mcp/src/abuse/honeypot.ts +21 -0
- package/templates/apps/mcp/src/abuse/index.ts +111 -0
- package/templates/apps/mcp/src/abuse/user-agent-deny.ts +58 -0
- package/templates/apps/mcp/src/http-transport.ts +303 -0
- package/templates/apps/mcp/src/index.ts +67 -0
- package/templates/apps/mcp/src/lib/audit-single-page.ts +194 -0
- package/templates/apps/mcp/src/lib/errors.ts +61 -0
- package/templates/apps/mcp/src/lib/locales.ts +171 -0
- package/templates/apps/mcp/src/lib/require-paid-tier.ts +44 -0
- package/templates/apps/mcp/src/lib/resolve-locale.ts +30 -0
- package/templates/apps/mcp/src/server.ts +139 -0
- package/templates/apps/mcp/src/tools/audit.ts +103 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +52 -0
- package/templates/apps/mcp/src/tools/create_page.ts +95 -0
- package/templates/apps/mcp/src/tools/delete_page.ts +73 -0
- package/templates/apps/mcp/src/tools/get_business_hours.ts +66 -0
- package/templates/apps/mcp/src/tools/get_faq.ts +69 -0
- package/templates/apps/mcp/src/tools/get_locations.ts +85 -0
- package/templates/apps/mcp/src/tools/get_offerings.ts +66 -0
- package/templates/apps/mcp/src/tools/get_pricing.ts +74 -0
- package/templates/apps/mcp/src/tools/get_site_info.ts +84 -0
- package/templates/apps/mcp/src/tools/index.ts +125 -0
- package/templates/apps/mcp/src/tools/list_pages.ts +72 -0
- package/templates/apps/mcp/src/tools/purchase.ts +89 -0
- package/templates/apps/mcp/src/tools/read_content.ts +61 -0
- package/templates/apps/mcp/src/tools/request_callback.ts +51 -0
- package/templates/apps/mcp/src/tools/scaffold.ts +119 -0
- package/templates/apps/mcp/src/tools/search_content.ts +59 -0
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +47 -0
- package/templates/apps/mcp/src/tools/update_page.ts +91 -0
- package/templates/apps/mcp/tsconfig.json +19 -0
- package/templates/apps/web/.env.example +47 -0
- package/templates/apps/web/CLAUDE.md +81 -0
- package/templates/apps/web/__tests__/auth/adversarial/cookie-attributes.test.ts +94 -0
- package/templates/apps/web/__tests__/auth/adversarial/oauth-state-parameter.test.ts +75 -0
- package/templates/apps/web/__tests__/auth/adversarial/redirect-uri-allowlist.test.ts +82 -0
- package/templates/apps/web/__tests__/auth/env-helpers.test.ts +168 -0
- package/templates/apps/web/__tests__/authority-list-routes.test.tsx +208 -0
- package/templates/apps/web/__tests__/better-stack.test.ts +66 -0
- package/templates/apps/web/__tests__/blog-dir-resolution.test.ts +87 -0
- package/templates/apps/web/__tests__/blog-source-populated.test.ts +130 -0
- package/templates/apps/web/__tests__/data-testid-coverage.test.tsx +60 -0
- package/templates/apps/web/__tests__/google-analytics.test.tsx +104 -0
- package/templates/apps/web/__tests__/i18n/middleware.test.ts +119 -0
- package/templates/apps/web/__tests__/i18n/routing.test.ts +114 -0
- package/templates/apps/web/__tests__/i18n/translations.test.ts +191 -0
- package/templates/apps/web/__tests__/json-ld.test.ts +98 -0
- package/templates/apps/web/__tests__/knowledge-events-routes.test.tsx +580 -0
- package/templates/apps/web/__tests__/layout-stealth.test.ts +39 -0
- package/templates/apps/web/__tests__/lighthouse-fixtures/improvement-image-shrink.tsx +103 -0
- package/templates/apps/web/__tests__/lighthouse-fixtures/override-fixture.md +114 -0
- package/templates/apps/web/__tests__/lighthouse-fixtures/regression-bloat.tsx +94 -0
- package/templates/apps/web/__tests__/page.test.tsx +230 -0
- package/templates/apps/web/__tests__/products-services-phase1.test.tsx +321 -0
- package/templates/apps/web/__tests__/projects-routes.test.tsx +392 -0
- package/templates/apps/web/__tests__/resources-downloads-integrations-phase2.test.tsx +475 -0
- package/templates/apps/web/__tests__/routes/ai-plugin-json.test.ts +53 -0
- package/templates/apps/web/__tests__/routes/blog-route.test.tsx +191 -0
- package/templates/apps/web/__tests__/routes/llms-txt.test.ts +123 -0
- package/templates/apps/web/__tests__/routes/robots-txt.test.ts +52 -0
- package/templates/apps/web/__tests__/routes/sitemap-xml.test.ts +78 -0
- package/templates/apps/web/__tests__/routes/well-known-mcp.test.ts +203 -0
- package/templates/apps/web/__tests__/section-routes.test.tsx +693 -0
- package/templates/apps/web/__tests__/security-headers.test.ts +201 -0
- package/templates/apps/web/__tests__/sentry-config.test.ts +116 -0
- package/templates/apps/web/__tests__/setup.ts +82 -0
- package/templates/apps/web/__tests__/sitemap-locales.test.ts +415 -0
- package/templates/apps/web/__tests__/standalone-informational-routes.test.tsx +844 -0
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +81 -0
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +110 -0
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +58 -0
- package/templates/apps/web/app/[locale]/__tests__/layout.test.tsx +120 -0
- package/templates/apps/web/app/[locale]/__tests__/page-renderer-migration.test.tsx +202 -0
- package/templates/apps/web/app/[locale]/about/page.tsx +31 -0
- package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +173 -0
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +45 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +117 -0
- package/templates/apps/web/app/[locale]/careers/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +113 -0
- package/templates/apps/web/app/[locale]/case-studies/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/clients/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +130 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +152 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +92 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +79 -0
- package/templates/apps/web/app/[locale]/contact/error.tsx +24 -0
- package/templates/apps/web/app/[locale]/contact/page.tsx +38 -0
- package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +119 -0
- package/templates/apps/web/app/[locale]/courses/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +160 -0
- package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +25 -0
- package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +117 -0
- package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +58 -0
- package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +118 -0
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +72 -0
- package/templates/apps/web/app/[locale]/downloads/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +47 -0
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +130 -0
- package/templates/apps/web/app/[locale]/events/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/faq/page.tsx +36 -0
- package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +78 -0
- package/templates/apps/web/app/[locale]/how-to/page.tsx +40 -0
- package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +43 -0
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +129 -0
- package/templates/apps/web/app/[locale]/integrations/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/launch-service/page.tsx +31 -0
- package/templates/apps/web/app/[locale]/layout.tsx +241 -0
- package/templates/apps/web/app/[locale]/page.tsx +41 -0
- package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +113 -0
- package/templates/apps/web/app/[locale]/partners/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +43 -0
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +129 -0
- package/templates/apps/web/app/[locale]/products/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +39 -0
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +138 -0
- package/templates/apps/web/app/[locale]/projects/page.tsx +30 -0
- package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +51 -0
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +117 -0
- package/templates/apps/web/app/[locale]/resources/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +43 -0
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +129 -0
- package/templates/apps/web/app/[locale]/services/page.tsx +25 -0
- package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +81 -0
- package/templates/apps/web/app/[locale]/software/page.tsx +39 -0
- package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/tech-articles/page.tsx +39 -0
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/testimonials/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/use-cases/page.tsx +27 -0
- package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +34 -0
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/whitepapers/page.tsx +39 -0
- package/templates/apps/web/app/__tests__/error-boundaries.test.tsx +152 -0
- package/templates/apps/web/app/api/auth/[...all]/route.ts +16 -0
- package/templates/apps/web/app/error-fallback.tsx +75 -0
- package/templates/apps/web/app/error.tsx +23 -0
- package/templates/apps/web/app/layout.tsx +18 -0
- package/templates/apps/web/app/llms.txt/route.ts +101 -0
- package/templates/apps/web/app/not-found.tsx +48 -0
- package/templates/apps/web/app/og/__tests__/route.test.ts +164 -0
- package/templates/apps/web/app/og/route.tsx +135 -0
- package/templates/apps/web/app/robots.txt/route.ts +36 -0
- package/templates/apps/web/app/sitemap.xml/route.ts +146 -0
- package/templates/apps/web/auth.ts +141 -0
- package/templates/apps/web/components/ThemeToggle.tsx +60 -0
- package/templates/apps/web/components/google-analytics.tsx +34 -0
- package/templates/apps/web/content/en/blog/index.mdx +15 -0
- package/templates/apps/web/content/en/blog/launching-ailk.mdx +20 -0
- package/templates/apps/web/content/en/docs/getting-started.mdx +30 -0
- package/templates/apps/web/i18n/request.ts +36 -0
- package/templates/apps/web/jest.config.cjs +100 -0
- package/templates/apps/web/lib/__mocks__/next-mdx-remote-rsc.js +12 -0
- package/templates/apps/web/lib/__mocks__/source-server.js +31 -0
- package/templates/apps/web/lib/__tests__/og.test.ts +65 -0
- package/templates/apps/web/lib/auth-env.ts +66 -0
- package/templates/apps/web/lib/blog-list-data.ts +95 -0
- package/templates/apps/web/lib/blog-post-data.ts +79 -0
- package/templates/apps/web/lib/nav-config.ts +13 -0
- package/templates/apps/web/lib/observability/better-stack.ts +27 -0
- package/templates/apps/web/lib/og.ts +23 -0
- package/templates/apps/web/lib/prisma-for-auth.ts +34 -0
- package/templates/apps/web/lib/source.ts +34 -0
- package/templates/apps/web/messages/en.json +131 -0
- package/templates/apps/web/middleware.ts +80 -0
- package/templates/apps/web/next.config.mjs +167 -0
- package/templates/apps/web/package.json +62 -0
- package/templates/apps/web/public/.well-known/security.txt +17 -0
- 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 +4 -0
- package/templates/apps/web/public/logo-dark.svg +21 -0
- package/templates/apps/web/public/logo-light.svg +21 -0
- package/templates/apps/web/public/logomark-dark.svg +4 -0
- package/templates/apps/web/public/logomark-light.svg +4 -0
- package/templates/apps/web/public/og-default.png +0 -0
- package/templates/apps/web/public/site.webmanifest +19 -0
- package/templates/apps/web/scripts/assert-content-bundled.ts +55 -0
- package/templates/apps/web/security-headers.config.ts +132 -0
- package/templates/apps/web/sentry.client.config.ts +27 -0
- package/templates/apps/web/sentry.edge.config.ts +15 -0
- package/templates/apps/web/sentry.server.config.ts +20 -0
- package/templates/apps/web/source.config.ts +78 -0
- package/templates/apps/web/src/components/SiteNav.tsx +21 -0
- package/templates/apps/web/src/lib/routes.ts +37 -0
- package/templates/apps/web/tsconfig.json +35 -0
- package/templates/apps/web/types/messages.d.ts +19 -0
- package/templates/content/.gitkeep +0 -0
- package/templates/content/CLAUDE.md +85 -0
- package/templates/content/_site.mdx +38 -0
- package/templates/content/ar/HomePage/home.mdx +123 -0
- package/templates/content/ar/_site.mdx +35 -0
- package/templates/content/de/_site.mdx +29 -0
- package/templates/content/en/.gitkeep +0 -0
- package/templates/content/en/AboutPage/about.mdx +19 -0
- package/templates/content/en/AboutPage/about.schema.json +82 -0
- package/templates/content/en/BookCall/book-call.mdx +19 -0
- package/templates/content/en/BookCall/book-call.schema.json +121 -0
- package/templates/content/en/BreadcrumbList/site-breadcrumbs.mdx +20 -0
- package/templates/content/en/CareersList/careers.mdx +24 -0
- package/templates/content/en/CaseStudiesList/case-studies.mdx +22 -0
- package/templates/content/en/CaseStudiesList/showcase.mdx +8 -0
- package/templates/content/en/CaseStudy/ailk-aeo-rollout.mdx +32 -0
- package/templates/content/en/CaseStudy/schema-builder-migration.mdx +41 -0
- package/templates/content/en/ClientsList/clients.mdx +38 -0
- package/templates/content/en/ContactPage/contact.mdx +19 -0
- package/templates/content/en/ContactPage/contact.schema.json +100 -0
- package/templates/content/en/ContactPoint/primary.mdx +14 -0
- package/templates/content/en/Course/aeo-fundamentals.mdx +37 -0
- package/templates/content/en/Course/schema-builder-workshop.mdx +62 -0
- package/templates/content/en/CoursesList/courses.mdx +22 -0
- package/templates/content/en/DownloadsList/downloads.mdx +22 -0
- package/templates/content/en/Event/aeo-office-hours-may-2026.mdx +39 -0
- package/templates/content/en/Event/ailk-launch-day-2026.mdx +35 -0
- package/templates/content/en/EventsList/events.mdx +26 -0
- package/templates/content/en/FAQPage/faq.mdx +420 -0
- package/templates/content/en/FAQPage/faq.schema.json +231 -0
- package/templates/content/en/GlossaryList/glossary.mdx +19 -0
- package/templates/content/en/GlossaryList/glossary.schema.json +300 -0
- package/templates/content/en/HomePage/home.mdx +312 -0
- package/templates/content/en/HomePage/home.schema.json +88 -0
- package/templates/content/en/HomePage/launch-service.mdx +475 -0
- package/templates/content/en/HomePage/launch-service.schema.json +138 -0
- package/templates/content/en/HowTo/add-mdx-content.mdx +38 -0
- package/templates/content/en/HowTo/configure-aeo-scoring.mdx +35 -0
- package/templates/content/en/HowTo/set-up-json-ld.mdx +36 -0
- package/templates/content/en/Integration/github-actions.mdx +31 -0
- package/templates/content/en/Integration/openai-api.mdx +26 -0
- package/templates/content/en/Integration/vercel.mdx +32 -0
- package/templates/content/en/IntegrationsList/integrations.mdx +22 -0
- package/templates/content/en/JobPosting/content-strategist.mdx +46 -0
- package/templates/content/en/JobPosting/developer-advocate.mdx +47 -0
- package/templates/content/en/JobPosting/senior-fullstack-engineer.mdx +57 -0
- package/templates/content/en/LandingPage/aeo-audit.mdx +19 -0
- package/templates/content/en/LandingPage/aeo-audit.schema.json +92 -0
- package/templates/content/en/LandingPage/agency-waitlist.mdx +340 -0
- package/templates/content/en/LandingPage/agency-waitlist.schema.json +152 -0
- package/templates/content/en/LandingPage/developers.mdx +19 -0
- package/templates/content/en/LandingPage/developers.schema.json +109 -0
- package/templates/content/en/LandingPage/for-agencies.mdx +390 -0
- package/templates/content/en/LandingPage/for-agencies.schema.json +107 -0
- package/templates/content/en/LandingPage/for-ai-search-agencies.mdx +417 -0
- package/templates/content/en/LandingPage/for-ai-search-agencies.schema.json +105 -0
- package/templates/content/en/LandingPage/for-businesses.mdx +19 -0
- package/templates/content/en/LandingPage/for-businesses.schema.json +96 -0
- package/templates/content/en/LandingPage/platform.mdx +369 -0
- package/templates/content/en/LandingPage/platform.schema.json +166 -0
- package/templates/content/en/LandingPage/vs-ai-website-builders.mdx +19 -0
- package/templates/content/en/LandingPage/vs-ai-website-builders.schema.json +102 -0
- package/templates/content/en/LandingPage/vs-wordpress.mdx +19 -0
- package/templates/content/en/LandingPage/vs-wordpress.schema.json +53 -0
- package/templates/content/en/NewsletterSignup/newsletter.mdx +236 -0
- package/templates/content/en/NewsletterSignup/newsletter.schema.json +91 -0
- package/templates/content/en/Organization/site-owner.mdx +16 -0
- package/templates/content/en/Partner/prisma.mdx +31 -0
- package/templates/content/en/Partner/supabase.mdx +30 -0
- package/templates/content/en/Partner/vercel.mdx +28 -0
- package/templates/content/en/PartnersList/partners.mdx +22 -0
- package/templates/content/en/Person/founder.mdx +18 -0
- package/templates/content/en/Pricing/pricing.mdx +538 -0
- package/templates/content/en/Pricing/pricing.schema.json +209 -0
- package/templates/content/en/PrivacyPage/privacy.mdx +165 -0
- package/templates/content/en/PrivacyPage/privacy.schema.json +64 -0
- package/templates/content/en/Product/ailk.mdx +19 -0
- package/templates/content/en/Product/ailk.schema.json +94 -0
- package/templates/content/en/Product/api-launch-kit.mdx +26 -0
- package/templates/content/en/Product/launch-kit.mdx +28 -0
- package/templates/content/en/ProductsList/products.mdx +26 -0
- package/templates/content/en/Project/ailk-phase-2.mdx +54 -0
- package/templates/content/en/ProjectsList/projects.mdx +25 -0
- package/templates/content/en/ResourceDetail/aeo-scoring-guide.mdx +27 -0
- package/templates/content/en/ResourceDetail/content-model-whitepaper.mdx +28 -0
- package/templates/content/en/ResourceDetail/schema-org-quick-reference.mdx +39 -0
- package/templates/content/en/ResourcesHub/resources.mdx +22 -0
- package/templates/content/en/Service/consulting.mdx +22 -0
- package/templates/content/en/Service/implementation.mdx +30 -0
- package/templates/content/en/Service/launch-service.mdx +232 -0
- package/templates/content/en/Service/launch.mdx +228 -0
- package/templates/content/en/ServicesList/services.mdx +84 -0
- package/templates/content/en/SoftwareProduct/ailk-aeo.mdx +44 -0
- package/templates/content/en/SoftwareProduct/ailk-content-adapters.mdx +47 -0
- package/templates/content/en/SoftwareProduct/ailk-schema.mdx +49 -0
- package/templates/content/en/TeamList/team.mdx +63 -0
- package/templates/content/en/TeamMember/founder.mdx +72 -0
- package/templates/content/en/TermsPage/terms.mdx +19 -0
- package/templates/content/en/TermsPage/terms.schema.json +99 -0
- package/templates/content/en/TestimonialsList/testimonials.mdx +44 -0
- package/templates/content/en/UseCasesList/use-cases.mdx +30 -0
- package/templates/content/en/Whitepaper/ax-first-product-development.mdx +27 -0
- package/templates/content/en/Whitepaper/open-core-commercial-strategy.mdx +27 -0
- package/templates/content/en/Whitepaper/structured-data-for-answer-engines.mdx +31 -0
- package/templates/content/en-XA/AboutPage/about.mdx +8 -0
- package/templates/content/en-XA/FAQPage/faq.mdx +8 -0
- package/templates/content/en-XA/HomePage/home.mdx +8 -0
- package/templates/content/es/_site.mdx +30 -0
- package/templates/content/fr/_site.mdx +30 -0
- package/templates/content/ja/_site.mdx +30 -0
- package/templates/content/pt-BR/AboutPage/about.mdx +8 -0
- package/templates/content/pt-BR/FAQPage/faq.mdx +8 -0
- package/templates/content/pt-BR/HomePage/home.mdx +23 -0
- package/templates/database/CHANGELOG.md +5 -0
- package/templates/database/README.md +234 -0
- package/templates/database/__tests__/user-cascade.test.ts +125 -0
- package/templates/database/auth/.gitkeep +0 -0
- package/templates/database/config/.gitkeep +0 -0
- package/templates/database/content/.gitkeep +0 -0
- package/templates/database/inbox/.gitkeep +0 -0
- package/templates/database/inbox/README.md +3 -0
- package/templates/database/inbox/__tests__/schema.test.ts +40 -0
- package/templates/database/inbox/schema.prisma +52 -0
- package/templates/database/jest.config.cjs +29 -0
- package/templates/database/migrations/20260508000000_add_inbox_namespace/migration.sql +328 -0
- package/templates/database/migrations/20260513000000_better_auth_migration/migration.sql +100 -0
- package/templates/database/migrations/20260515000000_add_user_billing_fields/migration.sql +28 -0
- package/templates/database/migrations/20260516000000_add_user_tier_care_plan_seat/migration.sql +25 -0
- package/templates/database/migrations/20260518000000_add_telemetry_records/migration.sql +42 -0
- package/templates/database/migrations/20260525000000_theme_wiring/migration.sql +67 -0
- package/templates/database/migrations/20260710000000_retire_commercial_tier/migration.sql +21 -0
- package/templates/database/migrations/migration_lock.toml +3 -0
- package/templates/database/ops/.gitkeep +0 -0
- package/templates/database/package.json +45 -0
- package/templates/database/prisma.config.ts +20 -0
- package/templates/database/schema.prisma +487 -0
- package/templates/database/scripts/db-generate-locked.sh +80 -0
- package/templates/database/seed-test.ts +85 -0
- package/templates/database/tsconfig.build.json +16 -0
- package/templates/database/tsconfig.json +9 -0
- package/templates/package.json +125 -0
- package/templates/pnpm-workspace.yaml +6 -0
- package/templates/project.yaml +59 -0
- package/templates/tsconfig.base.json +18 -0
- package/templates/tsconfig.json +45 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# apps/web — Next.js Site
|
|
2
|
+
|
|
3
|
+
> **NOT the production marketing site.** The production AI Launch Kit marketing site at https://ailaunchkit.ai deploys from `apps-paid/marketing-web/`, NOT this app. `apps/web` is the OSS customer-facing template that downstream forks build on. See `project.yaml.deployments` for the canonical URL → app mapping.
|
|
4
|
+
|
|
5
|
+
Inherits root `CLAUDE.md`. This file adds web-specific rules.
|
|
6
|
+
|
|
7
|
+
## Stack
|
|
8
|
+
|
|
9
|
+
- Next.js (App Router), React, TypeScript
|
|
10
|
+
- Styling: Tailwind via `@working-theory/theme` tokens
|
|
11
|
+
- Content: MDX via `@working-theory/content-adapters`
|
|
12
|
+
- JSON-LD: emitted via `@working-theory/schema` builders
|
|
13
|
+
- UI primitives: `@working-theory/ui`
|
|
14
|
+
|
|
15
|
+
## Always do first
|
|
16
|
+
|
|
17
|
+
- For any UI work: invoke the `frontend-design` skill before writing code.
|
|
18
|
+
- For any page-rendering or schema-emission work: read `packages/schema/README.md` (catalog) and `packages/schema/CLAUDE.md` (operational layer + component map) if you haven't this session.
|
|
19
|
+
- For content authoring questions: read `content/CLAUDE.md` first.
|
|
20
|
+
- For a full page build, follow the per-page loop (Content intent → Layout → Author content → Visual iterate). The visual-iterate step uses `screenshots/` as scratch (see § Iterate visually).
|
|
21
|
+
|
|
22
|
+
## The render path (don't reinvent it)
|
|
23
|
+
|
|
24
|
+
MDX file → content adapter parses frontmatter → adapter emits `ContentPage` → `@working-theory/page-renderer`'s `renderPage(input)` composes the populated/empty-state output, calls `@working-theory/schema`'s `buildJsonLd(page)`, and emits `<script type="application/ld+json">` inside the rendered tree.
|
|
25
|
+
|
|
26
|
+
- Never hand-roll JSON-LD in a React component.
|
|
27
|
+
- Never read MDX directly — go through `@working-theory/content-adapters`.
|
|
28
|
+
- Never modify `@working-theory/schema` from `apps/web`. Schema changes happen in `packages/schema`.
|
|
29
|
+
|
|
30
|
+
## Component vocabulary
|
|
31
|
+
|
|
32
|
+
Use `@working-theory/ui` components for content patterns. See `content/CLAUDE.md` for the full vocabulary — it's identical across page types because the rule is about the component, not the page. For the per-page-type editorial map, see `packages/schema/CLAUDE.md` § `@working-theory/ui` component map.
|
|
33
|
+
|
|
34
|
+
Raw HTML is allowed only when no `@working-theory/ui` component fits. Flag it in PR review when you add raw HTML.
|
|
35
|
+
|
|
36
|
+
## Theme & styling
|
|
37
|
+
|
|
38
|
+
- Colors and tokens come from `@working-theory/theme`. Never hardcode hex values.
|
|
39
|
+
- Tailwind utilities are fine; **Tailwind defaults are not** — no `indigo-500`, `blue-600`, etc. Use theme tokens.
|
|
40
|
+
- Mobile-first responsive. Test at 320px, 768px, 1024px, and 1440px before marking a page done. Touch targets must be ≥44×44px (use `min-h-11 min-w-11`). Every interactive element needs a visible `focus-visible:` state. Wrap animations in `motion-safe:` so `prefers-reduced-motion` is honored.
|
|
41
|
+
- Animations: only `transform` and `opacity`. Never `transition-all`.
|
|
42
|
+
- Every interactive element needs hover, focus-visible, and active states.
|
|
43
|
+
|
|
44
|
+
For the full responsive contract (path-scoped to UI files), see `.claude/rules/responsive.md`. For brand-asset discovery (logos, colors, photos), see `.claude/rules/brand.md`.
|
|
45
|
+
|
|
46
|
+
## Local dev
|
|
47
|
+
|
|
48
|
+
- `pnpm dev` from repo root runs all apps.
|
|
49
|
+
- Web-only: `turbo run dev --filter=./apps/web` → `http://localhost:3000`.
|
|
50
|
+
- Content root: `src/lib/content/index.ts` resolves content via `AILK_CONTENT_ROOT` env var (if set) or `process.cwd()/../../content` (default for dev/build). In production (Vercel standalone container), set `AILK_CONTENT_ROOT` in Vercel project settings to the absolute path where `outputFileTracingIncludes` copied `content/` — typically `/var/task/content`. See `docs/runbooks/page-renderer-deploy-verification.md` §1 Cause 3.
|
|
51
|
+
|
|
52
|
+
## Iterate visually
|
|
53
|
+
|
|
54
|
+
Don't ship a page after one render. The expected per-page loop:
|
|
55
|
+
|
|
56
|
+
1. **Content intent.** Decide what the page is about — key facts, target citation phrases, who's reading. Pull from `brand_assets/brand-brief.md` (positioning, messaging, target citation phrases).
|
|
57
|
+
2. **Layout design.** Given intent + the global theme in `brand_assets/brand-guide.md`, choose `@working-theory/ui` components and their arrangement.
|
|
58
|
+
3. **Author content.** Fill components with AEO-optimized copy per `content/CLAUDE.md` § AEO editorial craft.
|
|
59
|
+
4. **Visual iterate.**
|
|
60
|
+
- Check `brand_assets/` before designing. If logos, color values, or a brand guide exist there, use them — don't invent brand colors or placeholder logos when real assets are present.
|
|
61
|
+
- Save working screenshots to `screenshots/` (gitignored — contents not committed; the directory itself is tracked via `.gitkeep`). Don't commit screenshot images.
|
|
62
|
+
- Render the page locally; switch DevTools to responsive mode and verify at 320 / 768 / 1024 / 1440.
|
|
63
|
+
- Compare against the reference (designer mockup, `brand_assets/reference/` if present, or the design intent).
|
|
64
|
+
- Fix mismatches. Be specific in self-review: "heading is 32px, reference shows 24px", not "looks off."
|
|
65
|
+
- Repeat until no visible differences and no responsive issues at any of the four breakpoints.
|
|
66
|
+
|
|
67
|
+
## AEO & performance
|
|
68
|
+
|
|
69
|
+
- Static rendering is the default. Don't introduce client rendering without a reason.
|
|
70
|
+
- Every page must emit valid JSON-LD. If `buildJsonLd` throws, fix the page, not the builder.
|
|
71
|
+
- After non-trivial page changes, run `ailk audit` and address Layer 1 findings before merging.
|
|
72
|
+
- Don't break `/llms.txt`, raw-MDX URLs, or stealth-by-default `robots.txt`.
|
|
73
|
+
|
|
74
|
+
## What NOT to do
|
|
75
|
+
|
|
76
|
+
- Don't emit JSON-LD inline in MDX or components.
|
|
77
|
+
- Don't add a new page type by hand-rolling schema. Add to `packages/schema` first.
|
|
78
|
+
- Don't use default Tailwind colors as brand colors.
|
|
79
|
+
- Don't use `transition-all`.
|
|
80
|
+
- Don't reach into `apps/api` or `apps/mcp` from `apps/web`. They're peer apps — talk via HTTP/MCP.
|
|
81
|
+
- Don't commit images to `screenshots/` — only the `.gitkeep` is tracked.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file cookie-attributes.test.ts (S13 adversarial)
|
|
3
|
+
*
|
|
4
|
+
* Verifies that `apps/web/auth.ts` does NOT weaken Better Auth's secure-cookie
|
|
5
|
+
* defaults. Better Auth's `nextCookies()` plugin emits session cookies with
|
|
6
|
+
* `HttpOnly`, `Secure` (in non-dev environments), `SameSite=Lax`, and
|
|
7
|
+
* `Path=/`. The configuration in `auth.ts` must not override any of those.
|
|
8
|
+
*
|
|
9
|
+
* Strategy: mock `better-auth` so its real ESM-only module never loads in the
|
|
10
|
+
* Jest CJS preset. The mock's `betterAuth(options)` returns `{ options }` so
|
|
11
|
+
* the test can inspect the raw config object the AILK code passed in. The
|
|
12
|
+
* assertions guard the AILK-owned surface, not Better Auth's emission
|
|
13
|
+
* (that's the library's contract).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
jest.mock("better-auth", () => ({
|
|
17
|
+
betterAuth: (options: unknown) => ({ options }),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
jest.mock("better-auth/adapters/prisma", () => ({
|
|
21
|
+
prismaAdapter: () => ({ id: "mock-prisma-adapter" }),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
jest.mock("better-auth/next-js", () => ({
|
|
25
|
+
nextCookies: () => ({ id: "next-cookies" }),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
import { createAuth } from "../../../auth";
|
|
29
|
+
|
|
30
|
+
// Stub PrismaClient — avoids a real DB connection at module-load time.
|
|
31
|
+
// Injected via createAuth({ prisma }) instead of jest.mock("../../../lib/prisma-for-auth").
|
|
32
|
+
// Pattern mirrors apps-paid/dashboard-web/auth.ts DI.
|
|
33
|
+
const stubPrisma = {} as NonNullable<NonNullable<Parameters<typeof createAuth>[0]>["prisma"]>;
|
|
34
|
+
const auth = createAuth({ prisma: stubPrisma });
|
|
35
|
+
|
|
36
|
+
type CookieAttrs = {
|
|
37
|
+
httpOnly?: boolean;
|
|
38
|
+
secure?: boolean;
|
|
39
|
+
sameSite?: "lax" | "strict" | "none";
|
|
40
|
+
path?: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type AdvancedCookies = {
|
|
44
|
+
session_token?: { attributes?: CookieAttrs };
|
|
45
|
+
session_data?: { attributes?: CookieAttrs };
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type AuthOptions = {
|
|
49
|
+
advanced?: {
|
|
50
|
+
useSecureCookies?: boolean;
|
|
51
|
+
cookies?: AdvancedCookies;
|
|
52
|
+
defaultCookieAttributes?: CookieAttrs;
|
|
53
|
+
};
|
|
54
|
+
plugins?: Array<{ id?: string }>;
|
|
55
|
+
session?: { expiresIn?: number; updateAge?: number };
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// eslint-disable-next-line no-restricted-syntax -- the mocked betterAuth returns { options: <unknown> }; the real type carries no public options shape, so a double assertion is the narrow path to inspect the AILK-owned config object.
|
|
59
|
+
const options = (auth as unknown as { options: AuthOptions }).options;
|
|
60
|
+
|
|
61
|
+
describe("S13 adversarial — cookie attributes (config surface)", () => {
|
|
62
|
+
it("does NOT disable useSecureCookies (Better Auth secure default preserved)", () => {
|
|
63
|
+
expect(options.advanced?.useSecureCookies).not.toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("does NOT override session_token cookie to drop HttpOnly", () => {
|
|
67
|
+
const sessionAttrs = options.advanced?.cookies?.session_token?.attributes;
|
|
68
|
+
expect(sessionAttrs?.httpOnly).not.toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("does NOT override session_token cookie to drop Secure", () => {
|
|
72
|
+
const sessionAttrs = options.advanced?.cookies?.session_token?.attributes;
|
|
73
|
+
expect(sessionAttrs?.secure).not.toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("does NOT relax sameSite to 'none' (CSRF protection preserved)", () => {
|
|
77
|
+
expect(
|
|
78
|
+
options.advanced?.cookies?.session_token?.attributes?.sameSite,
|
|
79
|
+
).not.toBe("none");
|
|
80
|
+
expect(options.advanced?.defaultCookieAttributes?.sameSite).not.toBe(
|
|
81
|
+
"none",
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("includes the nextCookies() plugin so Next.js sets cookies via Response", () => {
|
|
86
|
+
const pluginIds = (options.plugins ?? []).map((p) => p.id);
|
|
87
|
+
expect(pluginIds).toContain("next-cookies");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("persists sessions in the database (admin-revocable model)", () => {
|
|
91
|
+
expect(options.session).toBeDefined();
|
|
92
|
+
expect(options.session?.expiresIn).toBeGreaterThan(0);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file oauth-state-parameter.test.ts (S13 adversarial)
|
|
3
|
+
*
|
|
4
|
+
* OAuth `state` parameter protection. Better Auth's social-signin flow
|
|
5
|
+
* generates an opaque `state` value on every OAuth handshake; the callback
|
|
6
|
+
* route validates it. The AILK-owned surface that could disable this is
|
|
7
|
+
* `auth.options` — there is no documented Better Auth knob to turn state
|
|
8
|
+
* generation off, but a future migration to a custom plugin (e.g.
|
|
9
|
+
* `genericOAuth`) could weaken it. This file pins the AILK config so a
|
|
10
|
+
* regression that introduces a state-disabling override is caught.
|
|
11
|
+
*
|
|
12
|
+
* Strategy: mock `better-auth` so the real ESM-only module never loads;
|
|
13
|
+
* inspect the options object the AILK code constructed.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
jest.mock("better-auth", () => ({
|
|
17
|
+
betterAuth: (options: unknown) => ({ options }),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
jest.mock("better-auth/adapters/prisma", () => ({
|
|
21
|
+
prismaAdapter: () => ({ id: "mock-prisma-adapter" }),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
jest.mock("better-auth/next-js", () => ({
|
|
25
|
+
nextCookies: () => ({ id: "next-cookies" }),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
import { createAuth } from "../../../auth";
|
|
29
|
+
|
|
30
|
+
// Stub PrismaClient — avoids a real DB connection at module-load time.
|
|
31
|
+
// Injected via createAuth({ prisma }) instead of jest.mock("../../../lib/prisma-for-auth").
|
|
32
|
+
// Pattern mirrors apps-paid/dashboard-web/auth.ts DI.
|
|
33
|
+
const stubPrisma = {} as NonNullable<NonNullable<Parameters<typeof createAuth>[0]>["prisma"]>;
|
|
34
|
+
const auth = createAuth({ prisma: stubPrisma });
|
|
35
|
+
|
|
36
|
+
type SocialProviderOpts = {
|
|
37
|
+
skipStateCheck?: boolean;
|
|
38
|
+
disableState?: boolean;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type AuthOptions = {
|
|
42
|
+
plugins?: Array<{ id?: string }>;
|
|
43
|
+
socialProviders?: Record<string, SocialProviderOpts>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// eslint-disable-next-line no-restricted-syntax -- the mocked betterAuth returns { options: <unknown> }; the real type carries no public options shape, so a double assertion is the narrow path to inspect the AILK-owned config object.
|
|
47
|
+
const options = (auth as unknown as { options: AuthOptions }).options;
|
|
48
|
+
|
|
49
|
+
describe("S13 adversarial — OAuth state parameter", () => {
|
|
50
|
+
it("auth config does NOT register a generic-oauth plugin (which would need explicit state config review)", () => {
|
|
51
|
+
const plugins = options.plugins ?? [];
|
|
52
|
+
const generic = plugins.find((p) => p.id === "generic-oauth");
|
|
53
|
+
expect(generic).toBeUndefined();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("active social providers do NOT carry a skipStateCheck override", () => {
|
|
57
|
+
const providers = options.socialProviders ?? {};
|
|
58
|
+
for (const [, opts] of Object.entries(providers)) {
|
|
59
|
+
// No Better Auth option named skipStateCheck/disableState exists today.
|
|
60
|
+
// The assertion guards against a custom wrapper that adds one.
|
|
61
|
+
expect(opts.skipStateCheck).toBeFalsy();
|
|
62
|
+
expect(opts.disableState).toBeFalsy();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("social provider entries are syntactically valid (each has an id-shaped key)", () => {
|
|
67
|
+
const providers = options.socialProviders ?? {};
|
|
68
|
+
// When env vars are unset (OSS standalone default) the map is empty.
|
|
69
|
+
// Test passes vacuously. When wired, each provider has a non-empty key
|
|
70
|
+
// which Better Auth uses to route state into the right cookie namespace.
|
|
71
|
+
for (const name of Object.keys(providers)) {
|
|
72
|
+
expect(name.length).toBeGreaterThan(0);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file redirect-uri-allowlist.test.ts (S13 adversarial)
|
|
3
|
+
*
|
|
4
|
+
* Redirect-URI allow-listing. Better Auth uses `trustedOrigins` (option on
|
|
5
|
+
* the betterAuth() config) to gate which origins may receive callback /
|
|
6
|
+
* redirect responses. The OAuth-provider-side `redirect_uri` registration
|
|
7
|
+
* is handled at the provider (e.g. Google Cloud Console); Better Auth's
|
|
8
|
+
* defence-in-depth layer is `trustedOrigins`.
|
|
9
|
+
*
|
|
10
|
+
* Asserted on the AILK-owned config surface:
|
|
11
|
+
* - `auth.options.trustedOrigins` is either undefined (Better Auth
|
|
12
|
+
* defaults to baseURL only) or a non-empty list / function. NEVER
|
|
13
|
+
* `["*"]` or an empty list.
|
|
14
|
+
* - If the config sets `trustedOrigins` as an array, every entry is a
|
|
15
|
+
* concrete origin (not a wildcard).
|
|
16
|
+
* - `baseURL` (Better Auth's primary callback origin) is sourced from
|
|
17
|
+
* `BETTER_AUTH_URL`, not hardcoded.
|
|
18
|
+
*
|
|
19
|
+
* Strategy: mock `better-auth` so the real ESM module never loads.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
jest.mock("better-auth", () => ({
|
|
23
|
+
betterAuth: (options: unknown) => ({ options }),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
jest.mock("better-auth/adapters/prisma", () => ({
|
|
27
|
+
prismaAdapter: () => ({ id: "mock-prisma-adapter" }),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
jest.mock("better-auth/next-js", () => ({
|
|
31
|
+
nextCookies: () => ({ id: "next-cookies" }),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
import { createAuth } from "../../../auth";
|
|
35
|
+
|
|
36
|
+
// Stub PrismaClient — avoids a real DB connection at module-load time.
|
|
37
|
+
// Injected via createAuth({ prisma }) instead of jest.mock("../../../lib/prisma-for-auth").
|
|
38
|
+
// Pattern mirrors apps-paid/dashboard-web/auth.ts DI.
|
|
39
|
+
const stubPrisma = {} as NonNullable<NonNullable<Parameters<typeof createAuth>[0]>["prisma"]>;
|
|
40
|
+
const auth = createAuth({ prisma: stubPrisma });
|
|
41
|
+
|
|
42
|
+
type TrustedOriginsOption =
|
|
43
|
+
| string[]
|
|
44
|
+
| ((req: unknown) => string[] | Promise<string[]>)
|
|
45
|
+
| undefined;
|
|
46
|
+
|
|
47
|
+
type AuthOptions = {
|
|
48
|
+
trustedOrigins?: TrustedOriginsOption;
|
|
49
|
+
baseURL?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// eslint-disable-next-line no-restricted-syntax -- the mocked betterAuth returns { options: <unknown> }; the real type carries no public options shape, so a double assertion is the narrow path to inspect the AILK-owned config object.
|
|
53
|
+
const options = (auth as unknown as { options: AuthOptions }).options;
|
|
54
|
+
|
|
55
|
+
function isWildcardEntry(origin: string): boolean {
|
|
56
|
+
return origin === "*" || origin.includes("*");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe("S13 adversarial — redirect URI allow-list", () => {
|
|
60
|
+
it("does NOT configure trustedOrigins as an empty array (would block all callbacks)", () => {
|
|
61
|
+
const trusted = options.trustedOrigins;
|
|
62
|
+
if (Array.isArray(trusted)) {
|
|
63
|
+
expect(trusted.length).toBeGreaterThan(0);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("does NOT include a wildcard entry in trustedOrigins", () => {
|
|
68
|
+
const trusted = options.trustedOrigins;
|
|
69
|
+
if (Array.isArray(trusted)) {
|
|
70
|
+
for (const origin of trusted) {
|
|
71
|
+
expect(isWildcardEntry(origin)).toBe(false);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("sources baseURL from the BETTER_AUTH_URL env var (no hardcoded production origin)", () => {
|
|
77
|
+
const baseURL = options.baseURL;
|
|
78
|
+
if (baseURL !== undefined) {
|
|
79
|
+
expect(baseURL).toBe(process.env.BETTER_AUTH_URL);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for auth env-introspection helpers.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* 1. isAuthRequired — AILK_AUTH_REQUIRED flag behavior
|
|
6
|
+
* 2. getActiveProviders — provider list derived from env var presence
|
|
7
|
+
*
|
|
8
|
+
* Both helpers must be importable without next-auth being configured,
|
|
9
|
+
* so they live in apps/web/lib/auth-env.ts (no next-auth import).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const ORIGINAL_ENV = process.env;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
// Fresh env copy before each test; prevents cross-test leakage.
|
|
16
|
+
process.env = { ...ORIGINAL_ENV };
|
|
17
|
+
// Clear all auth-related vars to ensure clean isolation.
|
|
18
|
+
delete process.env.AILK_AUTH_REQUIRED;
|
|
19
|
+
delete process.env.GOOGLE_CLIENT_ID;
|
|
20
|
+
delete process.env.GOOGLE_CLIENT_SECRET;
|
|
21
|
+
delete process.env.GITHUB_CLIENT_ID;
|
|
22
|
+
delete process.env.GITHUB_CLIENT_SECRET;
|
|
23
|
+
delete process.env.AUTH_EMAIL_SERVER;
|
|
24
|
+
delete process.env.AUTH_EMAIL_FROM;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterAll(() => {
|
|
28
|
+
process.env = ORIGINAL_ENV;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// ── isAuthRequired ────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
describe("isAuthRequired", () => {
|
|
34
|
+
let isAuthRequired: () => boolean;
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
jest.resetModules();
|
|
38
|
+
|
|
39
|
+
({ isAuthRequired } = require("../../lib/auth-env"));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("returns false when AILK_AUTH_REQUIRED is unset (OSS default)", () => {
|
|
43
|
+
expect(isAuthRequired()).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("returns false when AILK_AUTH_REQUIRED=0", () => {
|
|
47
|
+
process.env.AILK_AUTH_REQUIRED = "0";
|
|
48
|
+
expect(isAuthRequired()).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns false when AILK_AUTH_REQUIRED=false", () => {
|
|
52
|
+
process.env.AILK_AUTH_REQUIRED = "false";
|
|
53
|
+
expect(isAuthRequired()).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("returns true when AILK_AUTH_REQUIRED=1", () => {
|
|
57
|
+
process.env.AILK_AUTH_REQUIRED = "1";
|
|
58
|
+
expect(isAuthRequired()).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns true when AILK_AUTH_REQUIRED=true", () => {
|
|
62
|
+
process.env.AILK_AUTH_REQUIRED = "true";
|
|
63
|
+
expect(isAuthRequired()).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ── getActiveProviderIds ──────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
describe("getActiveProviderIds", () => {
|
|
70
|
+
let getActiveProviderIds: () => string[];
|
|
71
|
+
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
jest.resetModules();
|
|
74
|
+
|
|
75
|
+
({ getActiveProviderIds } = require("../../lib/auth-env"));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("returns an empty array when no auth env vars are set", () => {
|
|
79
|
+
expect(getActiveProviderIds()).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("activates 'google' only when both GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are set", () => {
|
|
83
|
+
process.env.GOOGLE_CLIENT_ID = "gid";
|
|
84
|
+
process.env.GOOGLE_CLIENT_SECRET = "gsecret";
|
|
85
|
+
expect(getActiveProviderIds()).toEqual(["google"]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("does NOT activate 'google' when only GOOGLE_CLIENT_ID is set", () => {
|
|
89
|
+
process.env.GOOGLE_CLIENT_ID = "gid";
|
|
90
|
+
expect(getActiveProviderIds()).not.toContain("google");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("does NOT activate 'google' when only GOOGLE_CLIENT_SECRET is set", () => {
|
|
94
|
+
process.env.GOOGLE_CLIENT_SECRET = "gsecret";
|
|
95
|
+
expect(getActiveProviderIds()).not.toContain("google");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("activates 'github' only when both GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET are set", () => {
|
|
99
|
+
process.env.GITHUB_CLIENT_ID = "ghid";
|
|
100
|
+
process.env.GITHUB_CLIENT_SECRET = "ghsecret";
|
|
101
|
+
expect(getActiveProviderIds()).toEqual(["github"]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("does NOT activate 'github' when only one GitHub var is set", () => {
|
|
105
|
+
process.env.GITHUB_CLIENT_ID = "ghid";
|
|
106
|
+
expect(getActiveProviderIds()).not.toContain("github");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("activates 'email' only when both AUTH_EMAIL_SERVER and AUTH_EMAIL_FROM are set", () => {
|
|
110
|
+
process.env.AUTH_EMAIL_SERVER = "smtp://localhost";
|
|
111
|
+
process.env.AUTH_EMAIL_FROM = "noreply@example.com";
|
|
112
|
+
expect(getActiveProviderIds()).toEqual(["email"]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("does NOT activate 'email' when only AUTH_EMAIL_SERVER is set", () => {
|
|
116
|
+
process.env.AUTH_EMAIL_SERVER = "smtp://localhost";
|
|
117
|
+
expect(getActiveProviderIds()).not.toContain("email");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("activates all three providers when all env vars are set", () => {
|
|
121
|
+
process.env.GOOGLE_CLIENT_ID = "gid";
|
|
122
|
+
process.env.GOOGLE_CLIENT_SECRET = "gsecret";
|
|
123
|
+
process.env.GITHUB_CLIENT_ID = "ghid";
|
|
124
|
+
process.env.GITHUB_CLIENT_SECRET = "ghsecret";
|
|
125
|
+
process.env.AUTH_EMAIL_SERVER = "smtp://localhost";
|
|
126
|
+
process.env.AUTH_EMAIL_FROM = "noreply@example.com";
|
|
127
|
+
expect(getActiveProviderIds()).toEqual(
|
|
128
|
+
expect.arrayContaining(["google", "github", "email"]),
|
|
129
|
+
);
|
|
130
|
+
expect(getActiveProviderIds()).toHaveLength(3);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("activates only the providers whose vars are complete (mixed partial env)", () => {
|
|
134
|
+
// Google: complete; GitHub: partial (missing secret); Email: complete
|
|
135
|
+
process.env.GOOGLE_CLIENT_ID = "gid";
|
|
136
|
+
process.env.GOOGLE_CLIENT_SECRET = "gsecret";
|
|
137
|
+
process.env.GITHUB_CLIENT_ID = "ghid";
|
|
138
|
+
// GITHUB_CLIENT_SECRET intentionally omitted
|
|
139
|
+
process.env.AUTH_EMAIL_SERVER = "smtp://localhost";
|
|
140
|
+
process.env.AUTH_EMAIL_FROM = "noreply@example.com";
|
|
141
|
+
const active = getActiveProviderIds();
|
|
142
|
+
expect(active).toContain("google");
|
|
143
|
+
expect(active).toContain("email");
|
|
144
|
+
expect(active).not.toContain("github");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ── Zero-auth boot (no env vars at all) ─────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
describe("zero-env auth boot", () => {
|
|
151
|
+
beforeEach(() => {
|
|
152
|
+
jest.resetModules();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("getActiveProviderIds returns empty array — site boots without throwing", () => {
|
|
156
|
+
|
|
157
|
+
const { getActiveProviderIds } = require("../../lib/auth-env");
|
|
158
|
+
expect(() => getActiveProviderIds()).not.toThrow();
|
|
159
|
+
expect(getActiveProviderIds()).toEqual([]);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("isAuthRequired returns false — site boots without throwing", () => {
|
|
163
|
+
|
|
164
|
+
const { isAuthRequired } = require("../../lib/auth-env");
|
|
165
|
+
expect(() => isAuthRequired()).not.toThrow();
|
|
166
|
+
expect(isAuthRequired()).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
});
|