create-ailk 0.19.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/add-module.js +7 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/scaffold-render-sweep.d.ts +23 -0
- package/dist/lib/scaffold-render-sweep.js +52 -0
- package/dist/module-architecture.js +9 -3
- package/dist/programmatic.js +8 -1
- package/dist/site-roster.js +21 -4
- package/dist/surfaces.d.ts +14 -0
- package/dist/surfaces.js +31 -1
- package/dist/topology.js +29 -1
- package/package.json +3 -3
- package/templates/apps/api/.env.example +17 -1
- package/templates/apps/api/CLAUDE.md +10 -4
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +19 -0
- package/templates/apps/api/src/__tests__/stripe-boot.test.ts +132 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +8 -0
- package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +237 -0
- package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +334 -0
- package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +217 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +7 -0
- package/templates/apps/api/src/lib/consent-store.ts +335 -0
- package/templates/apps/api/src/lib/stripe.ts +97 -5
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +10 -4
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +51 -0
- package/templates/apps/api/src/middleware/auth.ts +11 -3
- package/templates/apps/api/src/middleware/rate-limit.ts +5 -0
- package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +167 -0
- package/templates/apps/api/src/routes/checkout/session.ts +3 -3
- package/templates/apps/api/src/routes/checkout/sessions.ts +6 -4
- package/templates/apps/api/src/routes/consent/cookie.ts +90 -0
- package/templates/apps/api/src/routes/consent/index.ts +18 -0
- package/templates/apps/api/src/routes/consent/unsubscribe.ts +127 -0
- package/templates/apps/api/src/routes/leads/index.ts +15 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +5 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +17 -4
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +2 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +159 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +158 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +2 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +17 -0
- package/templates/apps/api/src/server.ts +111 -16
- package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +503 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +18 -3
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +115 -4
- package/templates/apps/api/src/services/__tests__/person.test.ts +392 -0
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +87 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +57 -17
- package/templates/apps/api/src/services/consent.ts +488 -3
- package/templates/apps/api/src/services/lead-routing.ts +105 -12
- package/templates/apps/api/src/services/person.ts +501 -1
- package/templates/apps/api/src/services/project-listings.ts +7 -2
- package/templates/apps/api/src/services/request-confirm.ts +52 -0
- package/templates/apps/api/src/services/revalidation.ts +11 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/account-frame-sign-out.test.tsx +175 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +17 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +156 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +89 -14
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +146 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +122 -18
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +15 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +290 -20
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +55 -0
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +74 -4
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +4 -7
- package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/general/contact-actions.ts +81 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +66 -4
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +26 -3
- package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +2 -2
- package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +89 -37
- package/templates/apps/web/app/[locale]/(authed)/account/security/security-panel.tsx +102 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/security-source.ts +62 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +41 -33
- package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +1 -1
- package/templates/apps/web/auth.ts +20 -1
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +84 -0
- package/templates/apps/web/lib/auth-email.ts +45 -10
- package/templates/apps/web/lib/topology.generated.ts +9 -0
- package/templates/apps/web/messages/en.json +54 -14
- package/templates/database/CHANGELOG.md +16 -0
- package/templates/database/__tests__/consent-ledger.db.test.ts +345 -0
- package/templates/database/__tests__/fence-fan-out.db.test.ts +6 -2
- package/templates/database/__tests__/fixtures/person-contact-backfill.json +182 -0
- package/templates/database/__tests__/person-contact.db.test.ts +563 -0
- package/templates/database/__tests__/person.db.test.ts +3 -1
- package/templates/database/inbox/schema.prisma +76 -10
- package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +122 -0
- package/templates/database/migrations/20261014120000_person_contact/migration.sql +231 -0
- package/templates/database/package.json +1 -1
- package/versions.json +35 -35
package/dist/add-module.js
CHANGED
|
@@ -40,7 +40,7 @@ import { ParseError, parseModules } from "./parse-args.js";
|
|
|
40
40
|
import { writeSpokeFlowsReadme } from "./seed-flows.js";
|
|
41
41
|
import { SITE_ROSTER_PATH, moduleRosterRows, planRosterAppend, readSiteRoster, siteRosterProblems, } from "./site-roster.js";
|
|
42
42
|
import { spokeCheckoutSite, spokeEnvRowOptions, writeSpokeCheckoutSlice, } from "./spoke-checkout.js";
|
|
43
|
-
import { MODULE_SURFACES, NAV_CONFIG_PATH, readNavConfigStamp, renderNavConfig, resolveSurfaces, } from "./surfaces.js";
|
|
43
|
+
import { MODULE_SURFACES, NAV_CONFIG_PATH, isUnservedSurfaceContent, readNavConfigStamp, renderNavConfig, resolveSurfaces, } from "./surfaces.js";
|
|
44
44
|
import { syncRoutes } from "./sync-routes.js";
|
|
45
45
|
import { TopologyError, isSpokeExcluded, readSiteTopology, readSpokeShape, spokeDestination, spokeModuleRefusal, } from "./topology.js";
|
|
46
46
|
/**
|
|
@@ -155,9 +155,14 @@ export async function applyModuleToScaffold(opts) {
|
|
|
155
155
|
markers: opts.markers,
|
|
156
156
|
existingFilePolicy: "skip-existing",
|
|
157
157
|
checkPatches: checkPlusConfigPatches,
|
|
158
|
+
// #8427 D4 — composed with the spoke filter: a module bundle never
|
|
159
|
+
// writes a seed under a gateable surface this site's own
|
|
160
|
+
// `architecture.yaml` header (resolved above, into `surfaces`) does
|
|
161
|
+
// not enable.
|
|
162
|
+
skip: (destination) => (spoke !== undefined && isSpokeExcluded(destination, spoke)) ||
|
|
163
|
+
isUnservedSurfaceContent(destination, surfaces),
|
|
158
164
|
...(spoke !== undefined
|
|
159
165
|
? {
|
|
160
|
-
skip: (destination) => isSpokeExcluded(destination, spoke),
|
|
161
166
|
// #7876 D10 — a module flow file lands in flows/ as declared data.
|
|
162
167
|
remapDestination: spokeDestination,
|
|
163
168
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,6 @@ export type { HomeSeedOutcome } from "./design-home.js";
|
|
|
14
14
|
export { SITE_ROSTER_PATH, readSiteRoster, siteRosterProblems, } from "./site-roster.js";
|
|
15
15
|
export { SYNC_ROUTES_HELP, formatSyncRoutesReport, listTemplateRouteDirs, parseSyncRoutesArgs, syncRoutes, type RouteReport, type SyncRoutesOptions, type SyncRoutesResult, } from "./sync-routes.js";
|
|
16
16
|
export { deriveArchitecture } from "./derive-architecture.js";
|
|
17
|
-
export { CORE_ROUTE_DIRS, CORE_SURFACE_PATHS, NAV_CONFIG_PATH, GATEABLE_SURFACES, MODULE_SURFACES, RECOGNIZED_SURFACES, ROUTE_SINCE, SurfaceError, excludedContentPaths, excludedRoutePaths, parseSurfaces, renderNavConfig, resolveSurfaces, surfaceForRouteDir, type Surface, } from "./surfaces.js";
|
|
17
|
+
export { CORE_ROUTE_DIRS, CORE_SURFACE_PATHS, NAV_CONFIG_PATH, GATEABLE_SURFACES, MODULE_SURFACES, RECOGNIZED_SURFACES, ROUTE_SINCE, SurfaceError, excludedContentPaths, excludedRoutePaths, isUnservedSurfaceContent, parseSurfaces, renderNavConfig, resolveSurfaces, surfaceForRouteDir, type Surface, } from "./surfaces.js";
|
|
18
18
|
export { substituteMarkers, substituteWorkspaceVersions, type MarkerMap, type WorkspaceVersionMap, } from "./substitute.js";
|
|
19
19
|
export { validateTarget, ValidationError } from "./validate.js";
|
package/dist/index.js
CHANGED
|
@@ -30,6 +30,6 @@ export { deriveArchitecture } from "./derive-architecture.js";
|
|
|
30
30
|
// The route-surface registry (#4154) — which public routes a scaffold carries.
|
|
31
31
|
// Exported so tooling can answer "what surfaces does this site declare" without
|
|
32
32
|
// re-parsing architecture.yaml.
|
|
33
|
-
export { CORE_ROUTE_DIRS, CORE_SURFACE_PATHS, NAV_CONFIG_PATH, GATEABLE_SURFACES, MODULE_SURFACES, RECOGNIZED_SURFACES, ROUTE_SINCE, SurfaceError, excludedContentPaths, excludedRoutePaths, parseSurfaces, renderNavConfig, resolveSurfaces, surfaceForRouteDir, } from "./surfaces.js";
|
|
33
|
+
export { CORE_ROUTE_DIRS, CORE_SURFACE_PATHS, NAV_CONFIG_PATH, GATEABLE_SURFACES, MODULE_SURFACES, RECOGNIZED_SURFACES, ROUTE_SINCE, SurfaceError, excludedContentPaths, excludedRoutePaths, isUnservedSurfaceContent, parseSurfaces, renderNavConfig, resolveSurfaces, surfaceForRouteDir, } from "./surfaces.js";
|
|
34
34
|
export { substituteMarkers, substituteWorkspaceVersions, } from "./substitute.js";
|
|
35
35
|
export { validateTarget, ValidationError } from "./validate.js";
|
|
@@ -105,6 +105,20 @@ export declare function assessConfigGatedRoute(input: {
|
|
|
105
105
|
html: string;
|
|
106
106
|
locale: string;
|
|
107
107
|
}): SweepVerdict;
|
|
108
|
+
/**
|
|
109
|
+
* The environment the sitemap sweep's server runs under (#8501): `env` with
|
|
110
|
+
* every `CONFIG_GATED_ROUTES` variable removed.
|
|
111
|
+
*
|
|
112
|
+
* `/sitemap.xml` is prerendered at `next build`, from the content seeds, so it
|
|
113
|
+
* describes the scaffold AS GENERATED — and a config-gated route's seeds are
|
|
114
|
+
* served only while its variable is unset (the Free placeholder hand-off,
|
|
115
|
+
* #6942). Set it and the route serves its live module page instead: `/meet`
|
|
116
|
+
* becomes the booker for the operator's enabled events, `/meet/<seed>` a 404.
|
|
117
|
+
* A leg that configures the module for its booking steps would therefore score
|
|
118
|
+
* the sitemap against pages it was never written for, so the sweep runs
|
|
119
|
+
* against a server that leaves the gated variables unset. Pure.
|
|
120
|
+
*/
|
|
121
|
+
export declare function sitemapSweepEnv(env: Readonly<Record<string, string | undefined>>): Record<string, string | undefined>;
|
|
108
122
|
/**
|
|
109
123
|
* Every FILE under `dir`, recursing into directories, as sorted slash-joined
|
|
110
124
|
* paths relative to it — so `public/.well-known/security.txt` is listed and
|
|
@@ -124,6 +138,15 @@ export declare function assessUnknownSlug(input: {
|
|
|
124
138
|
hops: RedirectHop[];
|
|
125
139
|
status: number | null;
|
|
126
140
|
}): SweepVerdict;
|
|
141
|
+
/**
|
|
142
|
+
* Every `<loc>` path in a sitemap XML document, in order, deduped (#8427
|
|
143
|
+
* D6). Pure — no network or filesystem access, like its neighbours. Each
|
|
144
|
+
* `<loc>` is un-escaped against the five entities `escapeXml` writes, then
|
|
145
|
+
* read as `new URL(loc).pathname` — so a query string contributes nothing
|
|
146
|
+
* and an `<xhtml:link rel="alternate">` sibling, which carries no `<loc>` of
|
|
147
|
+
* its own, contributes nothing either.
|
|
148
|
+
*/
|
|
149
|
+
export declare function sitemapLocPaths(xml: string): string[];
|
|
127
150
|
/** Fails, naming each route, when `.next/prerender-manifest.json`'s `dynamicRoutes` has a key under `/[locale]/`. */
|
|
128
151
|
export declare function assessPrerenderManifest(manifest: {
|
|
129
152
|
dynamicRoutes?: Record<string, unknown>;
|
|
@@ -239,6 +239,23 @@ export function assessConfigGatedRoute(input) {
|
|
|
239
239
|
function headingTexts(html) {
|
|
240
240
|
return [...html.matchAll(/<h1\b[^>]*>([\s\S]*?)<\/h1>/gi)].map((m) => (m[1] ?? "").replace(/<[^>]*>/g, ""));
|
|
241
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* The environment the sitemap sweep's server runs under (#8501): `env` with
|
|
244
|
+
* every `CONFIG_GATED_ROUTES` variable removed.
|
|
245
|
+
*
|
|
246
|
+
* `/sitemap.xml` is prerendered at `next build`, from the content seeds, so it
|
|
247
|
+
* describes the scaffold AS GENERATED — and a config-gated route's seeds are
|
|
248
|
+
* served only while its variable is unset (the Free placeholder hand-off,
|
|
249
|
+
* #6942). Set it and the route serves its live module page instead: `/meet`
|
|
250
|
+
* becomes the booker for the operator's enabled events, `/meet/<seed>` a 404.
|
|
251
|
+
* A leg that configures the module for its booking steps would therefore score
|
|
252
|
+
* the sitemap against pages it was never written for, so the sweep runs
|
|
253
|
+
* against a server that leaves the gated variables unset. Pure.
|
|
254
|
+
*/
|
|
255
|
+
export function sitemapSweepEnv(env) {
|
|
256
|
+
const gated = new Set(Object.values(CONFIG_GATED_ROUTES));
|
|
257
|
+
return Object.fromEntries(Object.entries(env).filter(([name]) => !gated.has(name)));
|
|
258
|
+
}
|
|
242
259
|
/**
|
|
243
260
|
* Every FILE under `dir`, recursing into directories, as sorted slash-joined
|
|
244
261
|
* paths relative to it — so `public/.well-known/security.txt` is listed and
|
|
@@ -291,6 +308,41 @@ export function assessUnknownSlug(input) {
|
|
|
291
308
|
return { ok: false, reason: `${route} -> status ${status} (expected 404 for an unknown slug)` };
|
|
292
309
|
return { ok: true, reason: "404" };
|
|
293
310
|
}
|
|
311
|
+
/**
|
|
312
|
+
* The five entities `escapeXml` (`apps/web/app/sitemap.xml/route.ts`) writes,
|
|
313
|
+
* reversed.
|
|
314
|
+
*/
|
|
315
|
+
const XML_ENTITIES = {
|
|
316
|
+
"&": "&",
|
|
317
|
+
"<": "<",
|
|
318
|
+
">": ">",
|
|
319
|
+
""": '"',
|
|
320
|
+
"'": "'",
|
|
321
|
+
};
|
|
322
|
+
function unescapeXml(value) {
|
|
323
|
+
return value.replace(/&|<|>|"|'/g, (entity) => XML_ENTITIES[entity] ?? entity);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Every `<loc>` path in a sitemap XML document, in order, deduped (#8427
|
|
327
|
+
* D6). Pure — no network or filesystem access, like its neighbours. Each
|
|
328
|
+
* `<loc>` is un-escaped against the five entities `escapeXml` writes, then
|
|
329
|
+
* read as `new URL(loc).pathname` — so a query string contributes nothing
|
|
330
|
+
* and an `<xhtml:link rel="alternate">` sibling, which carries no `<loc>` of
|
|
331
|
+
* its own, contributes nothing either.
|
|
332
|
+
*/
|
|
333
|
+
export function sitemapLocPaths(xml) {
|
|
334
|
+
const seen = new Set();
|
|
335
|
+
const paths = [];
|
|
336
|
+
for (const match of xml.matchAll(/<loc>([^<]*)<\/loc>/g)) {
|
|
337
|
+
const loc = unescapeXml(match[1] ?? "");
|
|
338
|
+
const path = new URL(loc).pathname;
|
|
339
|
+
if (seen.has(path))
|
|
340
|
+
continue;
|
|
341
|
+
seen.add(path);
|
|
342
|
+
paths.push(path);
|
|
343
|
+
}
|
|
344
|
+
return paths;
|
|
345
|
+
}
|
|
294
346
|
/** Fails, naming each route, when `.next/prerender-manifest.json`'s `dynamicRoutes` has a key under `/[locale]/`. */
|
|
295
347
|
export function assessPrerenderManifest(manifest) {
|
|
296
348
|
const locale = Object.keys(manifest.dynamicRoutes ?? {})
|
|
@@ -77,8 +77,8 @@ const PUBLIC_SCHEDULING_VAR_PATTERN = [
|
|
|
77
77
|
* live-key-shaped token in this file's source would itself trip the repo's
|
|
78
78
|
* own live-key hard block (`rules/environment-boundaries.md`). `apps/api` is
|
|
79
79
|
* excluded because `STRIPE_SECRET_KEY` legitimately lives there, gated by
|
|
80
|
-
*
|
|
81
|
-
* `
|
|
80
|
+
* lib/stripe.ts's admitted-key guard at request time and by
|
|
81
|
+
* `admitCommerceStripeKey` at boot (server.ts) — this pattern catches a
|
|
82
82
|
* live-shaped key leaking anywhere ELSE (a fixture, a client component, a
|
|
83
83
|
* `NEXT_PUBLIC_` var).
|
|
84
84
|
*/
|
|
@@ -186,6 +186,8 @@ const MEMBER_AREA_FILES = [
|
|
|
186
186
|
"apps/api/src/routes/member/artifact-review.ts",
|
|
187
187
|
"apps/api/src/routes/member/profile.ts",
|
|
188
188
|
"apps/api/src/routes/member/deletion-request.ts",
|
|
189
|
+
// #8438 D6 — the member's own consents; the same gate as the writes above.
|
|
190
|
+
"apps/api/src/routes/member/consents.ts",
|
|
189
191
|
"apps/api/src/services/member-area.ts",
|
|
190
192
|
"apps/api/src/lib/member-store.ts",
|
|
191
193
|
// #8226 D12 — the member-request match CLI; both import member-store.ts,
|
|
@@ -785,6 +787,10 @@ export const MODULE_SLICES = {
|
|
|
785
787
|
"apps/api/src/routes/workspaces/index.ts",
|
|
786
788
|
"apps/api/src/routes/workspaces/sessions.ts",
|
|
787
789
|
"apps/api/src/routes/workspaces/config.ts",
|
|
790
|
+
// #8438 D8 — the Admin owner's consents to Working Theory: registered
|
|
791
|
+
// under `isModuleEnabled("multi-tenant")` alone, the module that
|
|
792
|
+
// contributes the Admin app.
|
|
793
|
+
"apps/api/src/routes/consent/platform.ts",
|
|
788
794
|
// Co-owned with `workspace` (#5853): the content-write scope is gated on
|
|
789
795
|
// content-write AND (workspace || multi-tenant) and this file imports
|
|
790
796
|
// lib/tenant-db.ts, so each tenancy slice must deliver it.
|
|
@@ -1843,7 +1849,7 @@ export const MODULE_SLICES = {
|
|
|
1843
1849
|
{
|
|
1844
1850
|
pattern: STRIPE_LIVE_KEY_PATTERN,
|
|
1845
1851
|
paths_excluded: ["apps/api/**"],
|
|
1846
|
-
reason: "The commerce credential belongs to apps/api, behind
|
|
1852
|
+
reason: "The commerce credential belongs to apps/api, behind lib/stripe.ts's admitted-key guard. A live-mode-shaped key anywhere else is a leak, not a config choice",
|
|
1847
1853
|
},
|
|
1848
1854
|
],
|
|
1849
1855
|
// ownedFiles (S1, #5088). SOURCE files only (#5182 D6) — no __tests__/,
|
package/dist/programmatic.js
CHANGED
|
@@ -45,7 +45,7 @@ import { SITE_FONTS_PATH, renderSiteFonts } from "./site-fonts.js";
|
|
|
45
45
|
import { planSiteManifest } from "./site-manifest.js";
|
|
46
46
|
import { SITE_ROSTER_PATH, planHomeRecord, planSiteManifestDesign, readSiteRoster, withMeetPlaceholderSeeds, } from "./site-roster.js";
|
|
47
47
|
import { spokeCheckoutSite, spokeEnvRowOptions, writeSpokeCheckoutSlice, } from "./spoke-checkout.js";
|
|
48
|
-
import { GATEABLE_SURFACES, MODULE_SURFACES, NAV_CONFIG_PATH, excludedRoutePaths, renderNavConfig, resolveSurfaces, } from "./surfaces.js";
|
|
48
|
+
import { GATEABLE_SURFACES, MODULE_SURFACES, NAV_CONFIG_PATH, excludedRoutePaths, isUnservedSurfaceContent, renderNavConfig, resolveSurfaces, } from "./surfaces.js";
|
|
49
49
|
import { syncRoutes } from "./sync-routes.js";
|
|
50
50
|
import { BASE_LAYER_DIR, SHAPE_STARTERS, TEMPLATE_MANIFESTS, TemplateError, mergeRoster, resolveTemplate, resolveTemplateLineage, rosterDeclares, templateDelivery, templateTreeDir, } from "./template-manifest.js";
|
|
51
51
|
import { SPOKE_ONLY_FILES, TopologyError, applySpokeTransforms, isSpokeExcluded, normalizeHubSignInUrl, normalizeHubUrl, readSiteTopology, spokeExclusions, spokeDestination, spokeModuleRefusal, spokeModules, writeHubOriginEnv, writeHubSignInEnv, } from "./topology.js";
|
|
@@ -573,7 +573,14 @@ export async function scaffold(opts) {
|
|
|
573
573
|
versions,
|
|
574
574
|
markers,
|
|
575
575
|
ownership,
|
|
576
|
+
// #8427 D4 — a module bundle never writes a file inside the content
|
|
577
|
+
// directory of a gateable surface the result does not enable (the
|
|
578
|
+
// signal-collectors `projects`-surface seeds, on a scaffold whose
|
|
579
|
+
// roster carries no /projects route). The spoke filter still drops
|
|
580
|
+
// whatever it already drops; remapDestination is unchanged.
|
|
576
581
|
...spokeSkip,
|
|
582
|
+
skip: (destination) => spokeSkip.skip?.(destination) === true ||
|
|
583
|
+
isUnservedSurfaceContent(destination, result.surfaces),
|
|
577
584
|
});
|
|
578
585
|
}
|
|
579
586
|
// The template lands LAST — after `scaffoldSync` and after every module
|
package/dist/site-roster.js
CHANGED
|
@@ -33,6 +33,7 @@ import { locatePageSections } from "./design-presets.js";
|
|
|
33
33
|
import { diffTwoStrings } from "./lib/extract-module-bundle.js";
|
|
34
34
|
import { MODULE_SLICES, normalizeModules, } from "./module-architecture.js";
|
|
35
35
|
import { substituteMarkers } from "./substitute.js";
|
|
36
|
+
import { isUnservedSurfaceContent, resolveSurfaces } from "./surfaces.js";
|
|
36
37
|
import { BASE_LAYER_DIR, SHAPE_STARTERS, TEMPLATE_MANIFESTS, resolveTemplate, rosterDeclares, shippedManifestPath, templateTreeDir, } from "./template-manifest.js";
|
|
37
38
|
/** The site-root file that is the roster of record (#6658 D7). */
|
|
38
39
|
export const SITE_ROSTER_PATH = ".ailk-site-manifest.json";
|
|
@@ -613,7 +614,7 @@ function spliceRows(file, rows) {
|
|
|
613
614
|
return text.slice(0, open + 1) + body + text.slice(close);
|
|
614
615
|
}
|
|
615
616
|
/** Every `content/en/**.mdx` some layer installs into a site of this template and module set. */
|
|
616
|
-
function layerInstalledSeeds(template, modules, opts) {
|
|
617
|
+
function layerInstalledSeeds(template, modules, opts, surfaces) {
|
|
617
618
|
const out = new Set();
|
|
618
619
|
for (const root of [
|
|
619
620
|
opts.templatesDir,
|
|
@@ -640,7 +641,15 @@ function layerInstalledSeeds(template, modules, opts) {
|
|
|
640
641
|
for (const path of row.content ?? [])
|
|
641
642
|
out.add(path);
|
|
642
643
|
}
|
|
643
|
-
|
|
644
|
+
// #8427 D4 — a module installMap destination the scaffold's own
|
|
645
|
+
// surfaces do not serve is not "installed": the writer
|
|
646
|
+
// (programmatic.ts's module-bundle loop, add-module.ts) declines the
|
|
647
|
+
// write, so the reader must agree or report a seed that is not there.
|
|
648
|
+
for (const dest of Object.values(slice.installMap ?? {})) {
|
|
649
|
+
if (isUnservedSurfaceContent(dest, surfaces))
|
|
650
|
+
continue;
|
|
651
|
+
destinations.push(dest);
|
|
652
|
+
}
|
|
644
653
|
}
|
|
645
654
|
for (const to of destinations) {
|
|
646
655
|
if (to.startsWith("content/en/") && to.endsWith(".mdx"))
|
|
@@ -666,7 +675,15 @@ export function siteRosterProblems(opts) {
|
|
|
666
675
|
const roster = readSiteRoster(opts.targetDir);
|
|
667
676
|
if (roster === undefined)
|
|
668
677
|
return [];
|
|
669
|
-
const { modules } = readArchitectureHeader(readFileSync(join(opts.targetDir, "architecture.yaml"), "utf8"));
|
|
678
|
+
const { modules, surfaces: headerSurfaces } = readArchitectureHeader(readFileSync(join(opts.targetDir, "architecture.yaml"), "utf8"));
|
|
679
|
+
// #8427 D4 — the FULL effective surface set (module-contributed surfaces
|
|
680
|
+
// unioned with the header's own explicit list), the same set the writer
|
|
681
|
+
// resolves. Throws SurfaceError on a header surface that is unknown or
|
|
682
|
+
// owned by a module the site lacks — intended: `add-module` already
|
|
683
|
+
// refuses such a header before its first write, so a direct caller
|
|
684
|
+
// reaches the same refusal instead of a report built on a header the
|
|
685
|
+
// scaffold would never actually write.
|
|
686
|
+
const surfaces = resolveSurfaces(modules, headerSurfaces);
|
|
670
687
|
const problems = [];
|
|
671
688
|
const routes = roster.rows.map((r) => r.value["route"]);
|
|
672
689
|
const seen = new Set();
|
|
@@ -715,7 +732,7 @@ export function siteRosterProblems(opts) {
|
|
|
715
732
|
});
|
|
716
733
|
// #6942 — the Free /meet placeholder seeds the scheduling module's rows keep.
|
|
717
734
|
const declared = withMeetPlaceholderSeeds(effective);
|
|
718
|
-
const installed = [...layerInstalledSeeds(roster.template, modules, opts)]
|
|
735
|
+
const installed = [...layerInstalledSeeds(roster.template, modules, opts, surfaces)]
|
|
719
736
|
.filter((path) => existsSync(join(opts.targetDir, path)))
|
|
720
737
|
.sort();
|
|
721
738
|
for (const path of installed) {
|
package/dist/surfaces.d.ts
CHANGED
|
@@ -126,6 +126,20 @@ export declare function excludedRoutePaths(enabled: string[]): string[];
|
|
|
126
126
|
* route has been removed would advertise URLs that 404.
|
|
127
127
|
*/
|
|
128
128
|
export declare function excludedContentPaths(enabled: string[]): string[];
|
|
129
|
+
/**
|
|
130
|
+
* True when `path` (scaffold-relative, `/`-separated) lies inside one of the
|
|
131
|
+
* content directories `excludedContentPaths(enabled)` names, with `<locale>`
|
|
132
|
+
* matching any one path segment (#8427 D4).
|
|
133
|
+
*
|
|
134
|
+
* This is `excludedContentPaths`' first non-export reader — its own comment
|
|
135
|
+
* already named exactly this class ("orphan content makes `/sitemap.xml`
|
|
136
|
+
* advertise URLs that 404"). It is wired into the `skip` predicate
|
|
137
|
+
* `extractModuleBundle` already takes (`programmatic.ts`, `add-module.ts`),
|
|
138
|
+
* and into `layerInstalledSeeds`'s installed-seed count (`site-roster.ts`),
|
|
139
|
+
* so a module bundle never writes — and a roster check never counts — a
|
|
140
|
+
* seed under a gateable surface the scaffold does not enable.
|
|
141
|
+
*/
|
|
142
|
+
export declare function isUnservedSurfaceContent(path: string, enabled: readonly string[]): boolean;
|
|
129
143
|
/**
|
|
130
144
|
* Route manifest (#4986): every template route directory, keyed relative to
|
|
131
145
|
* ROUTE_ROOT (`apps/web/app/[locale]`), → the `@working-theory/*` fixed-group
|
package/dist/surfaces.js
CHANGED
|
@@ -503,6 +503,31 @@ export function excludedContentPaths(enabled) {
|
|
|
503
503
|
}
|
|
504
504
|
return out.sort();
|
|
505
505
|
}
|
|
506
|
+
/**
|
|
507
|
+
* True when `path` (scaffold-relative, `/`-separated) lies inside one of the
|
|
508
|
+
* content directories `excludedContentPaths(enabled)` names, with `<locale>`
|
|
509
|
+
* matching any one path segment (#8427 D4).
|
|
510
|
+
*
|
|
511
|
+
* This is `excludedContentPaths`' first non-export reader — its own comment
|
|
512
|
+
* already named exactly this class ("orphan content makes `/sitemap.xml`
|
|
513
|
+
* advertise URLs that 404"). It is wired into the `skip` predicate
|
|
514
|
+
* `extractModuleBundle` already takes (`programmatic.ts`, `add-module.ts`),
|
|
515
|
+
* and into `layerInstalledSeeds`'s installed-seed count (`site-roster.ts`),
|
|
516
|
+
* so a module bundle never writes — and a roster check never counts — a
|
|
517
|
+
* seed under a gateable surface the scaffold does not enable.
|
|
518
|
+
*/
|
|
519
|
+
export function isUnservedSurfaceContent(path, enabled) {
|
|
520
|
+
const segments = path.split("/");
|
|
521
|
+
for (const contentPath of excludedContentPaths([...enabled])) {
|
|
522
|
+
const prefixSegments = contentPath.split("/");
|
|
523
|
+
if (prefixSegments.length > segments.length)
|
|
524
|
+
continue;
|
|
525
|
+
const matches = prefixSegments.every((segment, i) => segment === "<locale>" || segment === segments[i]);
|
|
526
|
+
if (matches)
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
506
531
|
/**
|
|
507
532
|
* Route manifest (#4986): every template route directory, keyed relative to
|
|
508
533
|
* ROUTE_ROOT (`apps/web/app/[locale]`), → the `@working-theory/*` fixed-group
|
|
@@ -834,13 +859,18 @@ export const ROUTE_DEPENDENCIES = {
|
|
|
834
859
|
`${ROUTE_ROOT}/(authed)/account/components/send-message.tsx`,
|
|
835
860
|
],
|
|
836
861
|
// #8339 D11 — Security holds the former Sign-in section, moved verbatim
|
|
837
|
-
// from General: `sign-in-method.ts` and what it imports.
|
|
862
|
+
// from General: `sign-in-method.ts` and what it imports. #8431 D8 — the
|
|
863
|
+
// tab's failed-read row (`error-row.tsx` and its `retry-button.tsx`); its
|
|
864
|
+
// own `security-source.ts` and `security-panel.tsx` live in the route dir
|
|
865
|
+
// and copy with it.
|
|
838
866
|
"(authed)/account/security": [
|
|
839
867
|
"apps/web/lib/account-config.ts",
|
|
840
868
|
"apps/web/lib/auth-env.ts",
|
|
841
869
|
"apps/web/lib/server-api.ts",
|
|
842
870
|
"apps/web/lib/hub-session/config.ts",
|
|
843
871
|
`${ROUTE_ROOT}/(authed)/account/account-data.ts`,
|
|
872
|
+
`${ROUTE_ROOT}/(authed)/account/error-row.tsx`,
|
|
873
|
+
`${ROUTE_ROOT}/(authed)/account/retry-button.tsx`,
|
|
844
874
|
`${ROUTE_ROOT}/(authed)/account/settings-frame.tsx`,
|
|
845
875
|
`${ROUTE_ROOT}/(authed)/account/sign-in-method.ts`,
|
|
846
876
|
],
|
package/dist/topology.js
CHANGED
|
@@ -273,7 +273,9 @@ const WEB_SEAM_HEADER = `// topology.generated.ts — how this site reads a sess
|
|
|
273
273
|
// UNSERVED_MCP_TOOLS names the MCP tools /.well-known/mcp.json leaves out
|
|
274
274
|
// on this topology (#8070).
|
|
275
275
|
// loadSpokeBilling reaches the spoke billing action only on a spoke that
|
|
276
|
-
// signs in through its hub and carries commerce (#8217); elsewhere, null
|
|
276
|
+
// signs in through its hub and carries commerce (#8217); elsewhere, null.
|
|
277
|
+
// readOwnAccounts lists the signed-in person's own sign-in accounts through
|
|
278
|
+
// Better Auth on a hub (#8431); a spoke has none here, so it answers null.`;
|
|
277
279
|
const MCP_SEAM_HEADER = `// topology.generated.ts — how this MCP server resolves its workspace store.
|
|
278
280
|
//
|
|
279
281
|
// GENERATED by create-ailk for this site's topology (#7426). Do not edit:
|
|
@@ -303,6 +305,26 @@ const SITE_SESSION_TYPE = `export type SiteSession = {
|
|
|
303
305
|
createdAt?: Date | string | null;
|
|
304
306
|
};
|
|
305
307
|
} | null;`;
|
|
308
|
+
/**
|
|
309
|
+
* `readOwnAccounts` (#8431 D2a): the session's own sign-in accounts, which
|
|
310
|
+
* the account Security tab reads to say whether the person has a password.
|
|
311
|
+
* The hub render loads `auth.ts` only when it is called; a spoke has no local
|
|
312
|
+
* accounts, so its renders answer `null` and import nothing.
|
|
313
|
+
*/
|
|
314
|
+
function ownAccountsReader(topology) {
|
|
315
|
+
if (topology === "hub") {
|
|
316
|
+
return `export async function readOwnAccounts(
|
|
317
|
+
headers: Headers,
|
|
318
|
+
): Promise<ReadonlyArray<{ providerId: string }> | null> {
|
|
319
|
+
const { auth } = await import("../auth");
|
|
320
|
+
return auth.api.listUserAccounts({ headers });
|
|
321
|
+
}`;
|
|
322
|
+
}
|
|
323
|
+
return `export function readOwnAccounts(headers: Headers): Promise<ReadonlyArray<{ providerId: string }> | null> {
|
|
324
|
+
void headers;
|
|
325
|
+
return Promise.resolve(null);
|
|
326
|
+
}`;
|
|
327
|
+
}
|
|
306
328
|
/** The sign-in exports every web seam render carries (#7894 D8). */
|
|
307
329
|
function signInExports(values) {
|
|
308
330
|
const literal = (value) => value === null ? "null" : JSON.stringify(value);
|
|
@@ -382,6 +404,8 @@ export async function readSession(headers: Headers): Promise<SiteSession> {
|
|
|
382
404
|
return auth.api.getSession({ headers });
|
|
383
405
|
}
|
|
384
406
|
|
|
407
|
+
${ownAccountsReader("hub")}
|
|
408
|
+
|
|
385
409
|
export async function registerSiteStores(): Promise<void> {
|
|
386
410
|
const { registerLicenseKeyStore } = await import("./license-key-store");
|
|
387
411
|
await registerLicenseKeyStore();
|
|
@@ -404,6 +428,8 @@ export function readSession(headers: Headers): Promise<SiteSession> {
|
|
|
404
428
|
return readHubSession(headers);
|
|
405
429
|
}
|
|
406
430
|
|
|
431
|
+
${ownAccountsReader("spoke")}
|
|
432
|
+
|
|
407
433
|
export function registerSiteStores(): Promise<void> {
|
|
408
434
|
return Promise.resolve();
|
|
409
435
|
}
|
|
@@ -424,6 +450,8 @@ export function readSession(headers: Headers): Promise<SiteSession> {
|
|
|
424
450
|
return Promise.resolve(null);
|
|
425
451
|
}
|
|
426
452
|
|
|
453
|
+
${ownAccountsReader("spoke")}
|
|
454
|
+
|
|
427
455
|
export function registerSiteStores(): Promise<void> {
|
|
428
456
|
return Promise.resolve();
|
|
429
457
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-ailk",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"description": "Scaffolder for AI Launch Kit — `pnpm create ailk@latest <dir>` produces a starter repo consuming @working-theory/* from npm (per ADR 0007).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"tar": "^7.5.21"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@working-theory/schema": "0.
|
|
32
|
-
"@working-theory/templates": "0.
|
|
31
|
+
"@working-theory/schema": "0.40.0",
|
|
32
|
+
"@working-theory/templates": "0.40.0"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
|
35
35
|
"type": "git",
|
|
@@ -32,8 +32,14 @@ DATABASE_URL="postgresql://user:password@localhost:5432/customer_template"
|
|
|
32
32
|
# Stripe (Optional)
|
|
33
33
|
# =============================================================================
|
|
34
34
|
# Consumed by apps/api/src/lib/stripe.ts via @working-theory/stripe-client wrapper.
|
|
35
|
-
# Use sandbox keys only — live-mode keys are rejected at runtime
|
|
35
|
+
# Use sandbox keys only — live-mode keys are rejected at runtime, unless
|
|
36
|
+
# STRIPE_MODE=live below admits a restricted live key (#8450).
|
|
36
37
|
STRIPE_SECRET_KEY=sk_test_xxx
|
|
38
|
+
# test (the default when unset) or live. `live` is admitted only beside a
|
|
39
|
+
# restricted live key, on a production build — VERCEL_ENV=production, or,
|
|
40
|
+
# off Vercel, NODE_ENV=production, so a staging host must not set the latter.
|
|
41
|
+
# Every other live key stays refused at boot and on every route.
|
|
42
|
+
STRIPE_MODE=test
|
|
37
43
|
STRIPE_WEBHOOK_SECRET=whsec_xxx
|
|
38
44
|
|
|
39
45
|
# =============================================================================
|
|
@@ -46,6 +52,16 @@ STRIPE_WEBHOOK_SECRET=whsec_xxx
|
|
|
46
52
|
# Generate with: openssl rand -hex 32
|
|
47
53
|
# OSS_API_INTERNAL_SECRET=your-shared-secret-here
|
|
48
54
|
|
|
55
|
+
# =============================================================================
|
|
56
|
+
# Consent — the signed unsubscribe link (#8438)
|
|
57
|
+
# =============================================================================
|
|
58
|
+
# Keys the HMAC on the unsubscribe token an email's link carries
|
|
59
|
+
# (POST /v1/consent/unsubscribe). Optional: unset, no link can be minted and the
|
|
60
|
+
# route answers 503 link_secret_not_configured. When set it must be at least
|
|
61
|
+
# 32 bytes — a shorter value logs an error at boot and is treated as unset.
|
|
62
|
+
# Generate with: openssl rand -hex 32
|
|
63
|
+
# AILK_CONSENT_LINK_SECRET=
|
|
64
|
+
|
|
49
65
|
# =============================================================================
|
|
50
66
|
# Scheduling — the `booker` section's provider (#4416)
|
|
51
67
|
# =============================================================================
|