create-ailk 0.11.5 → 0.12.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/component-catalog.json +35 -2
- package/component-catalog.md +9 -6
- package/dist/add-module.js +13 -0
- package/dist/module-architecture.d.ts +8 -0
- package/dist/module-architecture.js +33 -0
- package/dist/paid-sections.d.ts +23 -0
- package/dist/paid-sections.js +119 -0
- package/dist/programmatic.js +8 -0
- package/package.json +3 -3
- package/templates/.env.example +69 -3
- package/templates/apps/api/.env.example +22 -0
- package/templates/apps/api/CLAUDE.md +19 -7
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/bin/spokes.ts +137 -0
- package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +96 -0
- package/templates/apps/api/src/config/index.ts +97 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +35 -0
- package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +152 -0
- package/templates/apps/api/src/lib/spoke-store.ts +131 -0
- package/templates/apps/api/src/routes/leads/__tests__/spoke.test.ts +331 -0
- package/templates/apps/api/src/routes/leads/index.ts +33 -3
- package/templates/apps/api/src/routes/project-listings/__tests__/directory.test.ts +528 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +204 -0
- package/templates/apps/api/src/routes/project-listings/index.ts +14 -2
- package/templates/apps/api/src/routes/project-listings/public.ts +42 -1
- package/templates/apps/api/src/routes/project-listings/submit.ts +3 -0
- package/templates/apps/api/src/routes/webhooks/index.ts +29 -0
- package/templates/apps/api/src/server.ts +48 -0
- package/templates/apps/api/src/services/__tests__/default-tenant.test.ts +147 -0
- package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +70 -0
- package/templates/apps/api/src/services/__tests__/spoke-resolution.test.ts +374 -0
- package/templates/apps/api/src/services/default-tenant.ts +51 -28
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +89 -3
- package/templates/apps/api/src/services/lead-routing.ts +13 -1
- package/templates/apps/api/src/services/project-listings.ts +209 -18
- package/templates/apps/api/src/services/public-site-resolver.ts +149 -28
- package/templates/apps/api/src/services/slack-notify.ts +54 -3
- package/templates/apps/web/.env.example +14 -0
- package/templates/apps/web/app/[locale]/[slug]/[detail]/__tests__/page.test.tsx +137 -0
- package/templates/apps/web/app/[locale]/[slug]/[detail]/page.tsx +60 -20
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +5 -16
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +8 -0
- package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +5 -16
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +8 -0
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +5 -19
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +8 -0
- package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +5 -14
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +8 -0
- package/templates/apps/web/app/[locale]/layout.tsx +4 -0
- package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +5 -16
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +8 -0
- package/templates/apps/web/app/[locale]/portfolio/[slug]/page.tsx +4 -7
- package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +5 -14
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +8 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +2 -2
- package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +5 -22
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +8 -0
- package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +5 -14
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +8 -0
- package/templates/apps/web/jest.config.cjs +1 -1
- package/templates/apps/web/lib/__tests__/listings.test.tsx +352 -0
- package/templates/apps/web/lib/listings.tsx +472 -0
- package/templates/apps/web/lib/paid-section-contracts.ts +22 -0
- package/templates/apps/web/lib/paid-sections.generated.ts +16 -0
- package/templates/apps/web/messages/en.json +10 -0
- package/templates/apps/web/src/lib/__tests__/commerce-sections.test.ts +57 -0
- package/templates/apps/web/src/lib/commerce-sections.ts +33 -71
- package/templates/database/CHANGELOG.md +34 -0
- package/templates/database/__tests__/build.test.ts +131 -0
- package/templates/database/__tests__/spoke-registry.db.test.ts +191 -0
- package/templates/database/auth/schema.prisma +31 -0
- package/templates/database/config/schema.prisma +26 -0
- package/templates/database/inbox/schema.prisma +21 -0
- package/templates/database/migrations/20261003120000_spoke_registry/migration.sql +80 -0
- package/templates/database/migrations/20261003130000_scheduling_connection/migration.sql +49 -0
- package/templates/database/package.json +3 -3
- package/versions.json +34 -34
package/component-catalog.json
CHANGED
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"importPath": "@working-theory/ui-signal-collectors",
|
|
98
98
|
"rootBarrel": "@working-theory/ui-signal-collectors",
|
|
99
99
|
"sourceRoot": "packages-paid/ui-signal-collectors/src/sections",
|
|
100
|
-
"count":
|
|
100
|
+
"count": 5
|
|
101
101
|
},
|
|
102
102
|
{
|
|
103
103
|
"name": "signal-collectors-blocks",
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"importPath": "@working-theory/ui-signal-collectors",
|
|
106
106
|
"rootBarrel": "@working-theory/ui-signal-collectors",
|
|
107
107
|
"sourceRoot": "packages-paid/ui-signal-collectors/src/blocks",
|
|
108
|
-
"count":
|
|
108
|
+
"count": 9
|
|
109
109
|
},
|
|
110
110
|
{
|
|
111
111
|
"name": "commerce-sections",
|
|
@@ -3213,6 +3213,28 @@
|
|
|
3213
3213
|
"descriptionGrade": "strong",
|
|
3214
3214
|
"sectionType": null
|
|
3215
3215
|
},
|
|
3216
|
+
{
|
|
3217
|
+
"name": "ListingDetail",
|
|
3218
|
+
"tier": "signal-collectors-sections",
|
|
3219
|
+
"sourcePath": "packages-paid/ui-signal-collectors/src/sections/ListingDetail.tsx",
|
|
3220
|
+
"importPath": "@working-theory/ui-signal-collectors",
|
|
3221
|
+
"exported": true,
|
|
3222
|
+
"description": "One accepted entry's own page: its name as the page's `<h1>`, its summary, its own link, and its PROFILE — the applicant's answers to the listicle's Application fields, as label · value in the wizard's order. The profile is built and filtered server-side (`listingProfile`); this component renders what it is handed, in order, and nothing else.",
|
|
3223
|
+
"descriptionSource": "doc-comment",
|
|
3224
|
+
"descriptionGrade": "strong",
|
|
3225
|
+
"sectionType": null
|
|
3226
|
+
},
|
|
3227
|
+
{
|
|
3228
|
+
"name": "ListingShelves",
|
|
3229
|
+
"tier": "signal-collectors-sections",
|
|
3230
|
+
"sourcePath": "packages-paid/ui-signal-collectors/src/sections/ListingShelves.tsx",
|
|
3231
|
+
"importPath": "@working-theory/ui-signal-collectors",
|
|
3232
|
+
"exported": true,
|
|
3233
|
+
"description": "A listicle's accepted entries on its landing page, as two shelves partitioned from ONE list: `status === \"active\"` on one, `\"closed\"` on the other. `closed` is computed by the API from the window end; this component only reads it. Featured entries lead the active shelf (stable sort, so the API's order otherwise survives).",
|
|
3234
|
+
"descriptionSource": "doc-comment",
|
|
3235
|
+
"descriptionGrade": "strong",
|
|
3236
|
+
"sectionType": null
|
|
3237
|
+
},
|
|
3216
3238
|
{
|
|
3217
3239
|
"name": "ProjectListingShelves",
|
|
3218
3240
|
"tier": "signal-collectors-sections",
|
|
@@ -3235,6 +3257,17 @@
|
|
|
3235
3257
|
"descriptionGrade": "strong",
|
|
3236
3258
|
"sectionType": null
|
|
3237
3259
|
},
|
|
3260
|
+
{
|
|
3261
|
+
"name": "ListingCard",
|
|
3262
|
+
"tier": "signal-collectors-blocks",
|
|
3263
|
+
"sourcePath": "packages-paid/ui-signal-collectors/src/blocks/ListingCard.tsx",
|
|
3264
|
+
"importPath": "@working-theory/ui-signal-collectors",
|
|
3265
|
+
"exported": true,
|
|
3266
|
+
"description": "The tile for one accepted entry of a listicle's directory. Generic by name and by shape: it reads the directory's `ListingEntryPublic` — name, summary, featured flag, waitlist count — and none of the project-shaped columns `ProjectListingCard` renders. The whole tile is one anchor, composed from `Link` directly (the LinkCard precedent: Card renders a <div>).",
|
|
3267
|
+
"descriptionSource": "doc-comment",
|
|
3268
|
+
"descriptionGrade": "strong",
|
|
3269
|
+
"sectionType": null
|
|
3270
|
+
},
|
|
3238
3271
|
{
|
|
3239
3272
|
"name": "ListingFullPreview",
|
|
3240
3273
|
"tier": "signal-collectors-blocks",
|
package/component-catalog.md
CHANGED
|
@@ -15,13 +15,13 @@ The component inventory of `@working-theory/ui` and the shells of `@working-theo
|
|
|
15
15
|
3. Check the Primitives, Panels and Shells tables too — a low-level control, a collapsible layout container, or a route-level frame lives there, not under Sections or Blocks.
|
|
16
16
|
4. Only then build. If you do build, you are declaring the capability is genuinely absent.
|
|
17
17
|
|
|
18
|
-
Inventory: **
|
|
18
|
+
Inventory: **307 component files** across 16 tiers — 55 sections, 99 blocks, 18 chrome, 79 primitives, 6 panels, 6 shells, 5 docs-blog-blocks, 1 docs-blog-chrome, 2 docs-blog-shells, 2 scheduling-sections, 5 scheduling-blocks, 5 signal-collectors-sections, 9 signal-collectors-blocks, 2 commerce-sections, 12 commerce-blocks, 1 commerce-panels. 304 distinct names: `SchedulerEmbed`, `Separator`, `Tabs` exist in more than one tier as genuinely different components, listed once per tier with its own source path.
|
|
19
19
|
|
|
20
|
-
Description coverage: **
|
|
20
|
+
Description coverage: **303/307** components carry a capability description derived from source — 44 from the section registry's hand-authored when-to-use text, 228 from the component's own doc-comment header, 31 from its tier barrel's roster line. **4** have none; they are listed under [Undescribed components](#undescribed-components) rather than given a sentence invented from their name.
|
|
21
21
|
|
|
22
|
-
Of those, **
|
|
22
|
+
Of those, **272** are *strong* — a capability sentence you could match a requirement against — and **31** are *weak*: either one short clause, or a long one that spends itself on wiring (client boundaries, class names, which primitives it composes) instead of on what the thing is for. Weak rows are marked `(weak)` and listed in full under [Weak descriptions](#weak-descriptions). Coverage is 99%, but **findable-without-luck coverage is 89%**.
|
|
23
23
|
|
|
24
|
-
Per tier, strong out of total: **Sections** 54/55 · **Blocks** 93/99 · **Chrome** 10/18 · **Primitives** 64/79 (4 undescribed) · **Panels** 5/6 · **Shells** 5/6 · **Docs + Blog Blocks** 5/5 · **Docs + Blog Chrome** 1/1 · **Docs + Blog Shells** 0/2 · **Scheduling Sections** 2/2 · **Scheduling Blocks** 5/5 · **Signal-Collectors Sections**
|
|
24
|
+
Per tier, strong out of total: **Sections** 54/55 · **Blocks** 93/99 · **Chrome** 10/18 · **Primitives** 64/79 (4 undescribed) · **Panels** 5/6 · **Shells** 5/6 · **Docs + Blog Blocks** 5/5 · **Docs + Blog Chrome** 1/1 · **Docs + Blog Shells** 0/2 · **Scheduling Sections** 2/2 · **Scheduling Blocks** 5/5 · **Signal-Collectors Sections** 5/5 · **Signal-Collectors Blocks** 8/9 · **Commerce Sections** 2/2 · **Commerce Blocks** 12/12 · **Commerce Panels** 1/1. A tier low on this line is a tier where a capability search is most likely to come up empty on something that exists.
|
|
25
25
|
|
|
26
26
|
That grade is **mechanical**. It counts what it can measure — how much a description adds beyond the component's own name, how much of it is spent on wiring, whether the extraction came out damaged — and it cannot tell a fluent paragraph about the wrong subject from a useful one. A person reading these tables will find more rows unusable than the weak count admits. Treat it as a floor on the problem, not a measurement of it, and read the source before concluding a capability is absent.
|
|
27
27
|
|
|
@@ -452,7 +452,7 @@ Import: `import { X } from "@working-theory/ui-scheduling";` (or from the `@work
|
|
|
452
452
|
|
|
453
453
|
---
|
|
454
454
|
|
|
455
|
-
## Signal-Collectors Sections (
|
|
455
|
+
## Signal-Collectors Sections (5)
|
|
456
456
|
|
|
457
457
|
Paid — delivered by the `signal-collectors` module bundle, not present in a Free scaffold. Multi-step flow and listing surfaces — the page-level bands a signal-collection route is assembled from.
|
|
458
458
|
|
|
@@ -462,11 +462,13 @@ Import: `import { X } from "@working-theory/ui-signal-collectors";` (or from the
|
|
|
462
462
|
| --- | --- | --- |
|
|
463
463
|
| `FlowStepper` | Rendering a served flow config as a real multi-step form. | `import { FlowStepper } from "@working-theory/ui-signal-collectors"` |
|
|
464
464
|
| `ListingApplicationFlow` | This is the thing a consuming site mounts INSTEAD of hand-writing three proxy routes, a save-on-advance form, and a decision about where a bearer token lives. | `import { ListingApplicationFlow } from "@working-theory/ui-signal-collectors"` |
|
|
465
|
+
| `ListingDetail` | One accepted entry's own page: its name as the page's `<h1>`, its summary, its own link, and its PROFILE — the applicant's answers to the listicle's Application fields, as label · value in the wizard's order. The profile is built and filtered server-side (`listingProfile`); this component renders what it is handed, in order, and nothing else. | `import { ListingDetail } from "@working-theory/ui-signal-collectors"` |
|
|
466
|
+
| `ListingShelves` | A listicle's accepted entries on its landing page, as two shelves partitioned from ONE list: `status === "active"` on one, `"closed"` on the other. `closed` is computed by the API from the window end; this component only reads it. Featured entries lead the active shelf (stable sort, so the API's order otherwise survives). | `import { ListingShelves } from "@working-theory/ui-signal-collectors"` |
|
|
465
467
|
| `ProjectListingShelves` | Two shelves of ProjectListingCards partitioned from ONE list of public listing rows: `status === "active"` on one shelf, `status === "closed"` on the other. `closed` is COMPUTED by the API read (the window-end check lives server-side, / D6b) — this component never recomputes it, it only reads the field. | `import { ProjectListingShelves } from "@working-theory/ui-signal-collectors"` |
|
|
466
468
|
|
|
467
469
|
---
|
|
468
470
|
|
|
469
|
-
## Signal-Collectors Blocks (
|
|
471
|
+
## Signal-Collectors Blocks (9)
|
|
470
472
|
|
|
471
473
|
Paid — delivered by the `signal-collectors` module bundle, not present in a Free scaffold. Flow step, listing card, response chart and results-reveal tiles the sections above compose.
|
|
472
474
|
|
|
@@ -475,6 +477,7 @@ Import: `import { X } from "@working-theory/ui-signal-collectors";` (or from the
|
|
|
475
477
|
| Component | What it does | How to use it |
|
|
476
478
|
| --- | --- | --- |
|
|
477
479
|
| `FlowSidePanel` | The per-view side panel of the flow/recommender surface. ONE CHASSIS, THREE VIEWS. | `import { FlowSidePanel } from "@working-theory/ui-signal-collectors"` |
|
|
480
|
+
| `ListingCard` | The tile for one accepted entry of a listicle's directory. Generic by name and by shape: it reads the directory's `ListingEntryPublic` — name, summary, featured flag, waitlist count — and none of the project-shaped columns `ProjectListingCard` renders. The whole tile is one anchor, composed from `Link` directly (the LinkCard precedent: Card renders a <div>). | `import { ListingCard } from "@working-theory/ui-signal-collectors"` |
|
|
478
481
|
| `ListingFullPreview` | The full-page-preview `custom` step renderer. The founder's assembled application, rendered as a page, with an edit link back to the step that owns each rendered part. Purely presentational: every entry's label and formatted value arrive pre-built on `entries` (this block does no formatting of its own), and `onEdit` is the only way it ever changes anything — it never mutates, saves, or fetches. | internal — compose from `packages-paid/ui-signal-collectors/src/blocks/ListingFullPreview.tsx` |
|
|
479
482
|
| `ListingPrivacyNotice` | The per-listing privacy notice. It names the FOUNDER — not the platform — as the party collecting a visitor's personal data, and it renders ON THE FLOW (where a visitor submits answers), not on the listing page. | `import { ListingPrivacyNotice } from "@working-theory/ui-signal-collectors"` |
|
|
480
483
|
| `ListingQuestionReview` | The edit-only question-review `custom` step renderer. The founder is refining a diagnostic template the SITE authored, not composing one from nothing (`projectListingQuestionsSchema`, `packages/validation/src/schemas/project-listings.ts`). | internal — compose from `packages-paid/ui-signal-collectors/src/blocks/ListingQuestionReview.tsx` |
|
package/dist/add-module.js
CHANGED
|
@@ -34,6 +34,7 @@ import { LEGACY_NAV_RENDERERS } from "./legacy-nav-renderers.js";
|
|
|
34
34
|
import { writeAuthSecretFile } from "./lib/auth-secret-file.js";
|
|
35
35
|
import { diffTwoStrings, extractModuleBundle, } from "./lib/extract-module-bundle.js";
|
|
36
36
|
import { normalizeModules } from "./module-architecture.js";
|
|
37
|
+
import { PAID_SECTIONS_PATH, writePaidSections } from "./paid-sections.js";
|
|
37
38
|
import { ParseError, parseModules } from "./parse-args.js";
|
|
38
39
|
import { SITE_ROSTER_PATH, moduleRosterRows, planRosterAppend, readSiteRoster, siteRosterProblems, } from "./site-roster.js";
|
|
39
40
|
import { MODULE_SURFACES, NAV_CONFIG_PATH, readNavConfigStamp, renderNavConfig, resolveSurfaces, } from "./surfaces.js";
|
|
@@ -126,7 +127,19 @@ export async function applyModuleToScaffold(opts) {
|
|
|
126
127
|
// The same composition `syncArchitectureSync` writes — a pure function of
|
|
127
128
|
// the module set, so an unchanged set rewrites identical bytes.
|
|
128
129
|
writeFileSync(archPath, deriveArchitecture(modules, surfaces));
|
|
130
|
+
// #7536 D3 — the paid-sections registry follows `apps/web/package.json`,
|
|
131
|
+
// which the bundle's `patches` just edited. Generated and always
|
|
132
|
+
// overwritten, like `architecture.yaml` above.
|
|
133
|
+
const registryPath = join(targetAbs, PAID_SECTIONS_PATH);
|
|
134
|
+
const registryBefore = existsSync(registryPath)
|
|
135
|
+
? readFileSync(registryPath, "utf8")
|
|
136
|
+
: undefined;
|
|
137
|
+
writePaidSections(targetAbs);
|
|
138
|
+
const registryChanged = existsSync(registryPath) &&
|
|
139
|
+
readFileSync(registryPath, "utf8") !== registryBefore;
|
|
129
140
|
const rows = [...report];
|
|
141
|
+
if (registryChanged)
|
|
142
|
+
rows.push({ path: PAID_SECTIONS_PATH, action: "written" });
|
|
130
143
|
const navRow = syncNavConfig(targetAbs, current.surfaces, surfaces);
|
|
131
144
|
if (navRow !== undefined)
|
|
132
145
|
rows.push(navRow);
|
|
@@ -83,6 +83,14 @@ export interface PageRosterRow {
|
|
|
83
83
|
* scaffold carries (#6940).
|
|
84
84
|
*/
|
|
85
85
|
export interface ModuleSlice {
|
|
86
|
+
/**
|
|
87
|
+
* The paid UI package base-tree code loads through
|
|
88
|
+
* `apps/web/lib/paid-sections.generated.ts` (#7536). Present → one row in the
|
|
89
|
+
* generated registry: a bundled `import()` when the scaffold's
|
|
90
|
+
* `apps/web/package.json` declares the package, a stub answering `null`
|
|
91
|
+
* otherwise. Only a module with a BASE-tree consumer sets it.
|
|
92
|
+
*/
|
|
93
|
+
paidSectionsPackage?: string;
|
|
86
94
|
/** Packages this module adds to apps/web.may_import_from (its presentation surface). */
|
|
87
95
|
webImports?: string[];
|
|
88
96
|
/** Apps this module declares (api, mcp replace the base block; admin is new). web is BASE, never here. */
|
|
@@ -790,6 +790,13 @@ export const MODULE_SLICES = {
|
|
|
790
790
|
// deliverable by BOTH bundles.
|
|
791
791
|
"apps/api/src/services/inbox-messages.ts",
|
|
792
792
|
"apps/api/src/routes/workspaces/messages.ts",
|
|
793
|
+
// #7535 — the Inbox submissions and subscribers reads: the same
|
|
794
|
+
// workspace||multi-tenant gate and the same tenant-db.ts import as
|
|
795
|
+
// the two lines above, so co-owned with `workspace` for the same
|
|
796
|
+
// reason (see that slice's entry below).
|
|
797
|
+
"apps/api/src/routes/workspaces/submissions.ts",
|
|
798
|
+
"apps/api/src/routes/workspaces/subscribers.ts",
|
|
799
|
+
"apps/api/src/services/inbox-submissions.ts",
|
|
793
800
|
// #7399 — the two siblings #6216 left `workspace`-only. They register
|
|
794
801
|
// under that same `workspace || multi-tenant` gate, so a scaffold with
|
|
795
802
|
// `multi-tenant` and without `workspace` loaded them with no file behind
|
|
@@ -910,6 +917,11 @@ export const MODULE_SLICES = {
|
|
|
910
917
|
// bundle alone.
|
|
911
918
|
"apps/api/src/routes/workspaces/messages.ts",
|
|
912
919
|
"apps/api/src/services/inbox-messages.ts",
|
|
920
|
+
// Co-owned with `multi-tenant` (#7535), for the reason the two
|
|
921
|
+
// message lines above carry.
|
|
922
|
+
"apps/api/src/routes/workspaces/submissions.ts",
|
|
923
|
+
"apps/api/src/routes/workspaces/subscribers.ts",
|
|
924
|
+
"apps/api/src/services/inbox-submissions.ts",
|
|
913
925
|
// Co-owned with `multi-tenant` (#5853) — see that slice's entry.
|
|
914
926
|
"apps/api/src/routes/workspaces/content.ts",
|
|
915
927
|
...ASSET_STORE_FILES,
|
|
@@ -1143,6 +1155,9 @@ export const MODULE_SLICES = {
|
|
|
1143
1155
|
"apps/api/src/lib/public-meetings-config.ts",
|
|
1144
1156
|
"apps/api/src/routes/schedule/meeting-types.ts",
|
|
1145
1157
|
"apps/api/src/routes/schedule/__tests__/meeting-types.test.ts",
|
|
1158
|
+
// #7532 — a hub spoke's meeting types and booking, through the real
|
|
1159
|
+
// scheduling routes; it ships and strips with them.
|
|
1160
|
+
"apps/api/src/routes/schedule/__tests__/spoke.test.ts",
|
|
1146
1161
|
// meetings.ts is the FIRST scheduling-gated route to use the generic
|
|
1147
1162
|
// tenantDb() scope layer directly (its sibling, appointments.ts, does
|
|
1148
1163
|
// its own scoped read in services/scheduling/appointments.ts instead)
|
|
@@ -1153,6 +1168,22 @@ export const MODULE_SLICES = {
|
|
|
1153
1168
|
// records: "each tenancy slice must deliver it").
|
|
1154
1169
|
"apps/api/src/lib/tenant-db.ts",
|
|
1155
1170
|
"apps/api/src/services/tenant-context.ts",
|
|
1171
|
+
// #7533 — a scheduling connection per workspace. `connections.ts` (the
|
|
1172
|
+
// cipher + the one SchedulingConnection data-access boundary) and
|
|
1173
|
+
// `resolve.ts` (the workspace-aware provider resolution the meetings,
|
|
1174
|
+
// schedule and webhook routes above now call), plus each new suite,
|
|
1175
|
+
// individually (§ D12, property 1: a glob ships no test). The config
|
|
1176
|
+
// suite is listed here although `config/index.ts` itself stays base:
|
|
1177
|
+
// it tests a scheduling-only variable, so a keyless scaffold carries
|
|
1178
|
+
// neither the variable's purpose nor its test.
|
|
1179
|
+
"apps/api/src/services/scheduling/connections.ts",
|
|
1180
|
+
"apps/api/src/services/scheduling/resolve.ts",
|
|
1181
|
+
"apps/api/src/services/scheduling/__tests__/connections.test.ts",
|
|
1182
|
+
"apps/api/src/services/scheduling/__tests__/provider-resolution.test.ts",
|
|
1183
|
+
"apps/api/src/routes/workspaces/__tests__/meetings-connection.test.ts",
|
|
1184
|
+
"apps/api/src/routes/schedule/__tests__/public-connection.test.ts",
|
|
1185
|
+
"apps/api/src/routes/webhooks/__tests__/calcom-connection.test.ts",
|
|
1186
|
+
"apps/api/src/config/__tests__/scheduling-credential-key.test.ts",
|
|
1156
1187
|
],
|
|
1157
1188
|
// pageContributions (spec 5478; parent § D6 shape 1) — the booker, the
|
|
1158
1189
|
// per-booking-type page and the confirmation page, whose route files
|
|
@@ -1285,6 +1316,7 @@ export const MODULE_SLICES = {
|
|
|
1285
1316
|
// cannot place an unreached file either) is unchanged and still governs
|
|
1286
1317
|
// where all eleven live.
|
|
1287
1318
|
"signal-collectors": {
|
|
1319
|
+
paidSectionsPackage: "@working-theory/ui-signal-collectors",
|
|
1288
1320
|
ownedFiles: [
|
|
1289
1321
|
"apps/api/src/routes/flows/index.ts",
|
|
1290
1322
|
// README.md documents the flows rate-limit rationale (#4730) — not a
|
|
@@ -1590,6 +1622,7 @@ export const MODULE_SLICES = {
|
|
|
1590
1622
|
// `lib/stripe.ts` and the webhook receiver this module owns — declared
|
|
1591
1623
|
// here, not engineered around.
|
|
1592
1624
|
commerce: {
|
|
1625
|
+
paidSectionsPackage: "@working-theory/ui-commerce",
|
|
1593
1626
|
apps: {
|
|
1594
1627
|
api: {
|
|
1595
1628
|
path: "apps/api",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Scaffold-relative path of the generated registry. */
|
|
2
|
+
export declare const PAID_SECTIONS_PATH = "apps/web/lib/paid-sections.generated.ts";
|
|
3
|
+
export interface PaidSectionRow {
|
|
4
|
+
module: string;
|
|
5
|
+
package: string;
|
|
6
|
+
loader: string;
|
|
7
|
+
contract: string;
|
|
8
|
+
}
|
|
9
|
+
/** One row per slice declaring `paidSectionsPackage`, in `RECOGNIZED_MODULES` order. */
|
|
10
|
+
export declare function paidSectionRows(): PaidSectionRow[];
|
|
11
|
+
/**
|
|
12
|
+
* Render the registry. `present` holds the package names whose row is a
|
|
13
|
+
* bundled `import()`; every other row is a stub. Pure and order-independent.
|
|
14
|
+
* Line 1 is `// oss: false` exactly when a row imports a paid package, so the
|
|
15
|
+
* public strip drops the file (\`check-oss-strip.sh\` condition (e)).
|
|
16
|
+
*/
|
|
17
|
+
export declare function renderPaidSections(present: ReadonlySet<string>): string;
|
|
18
|
+
/**
|
|
19
|
+
* Write the registry into a scaffold, deriving presence from the scaffold's
|
|
20
|
+
* own `apps/web/package.json`. Returns `false` (writing nothing) when that
|
|
21
|
+
* file or `apps/web/lib/` is absent — a scaffold with no web app.
|
|
22
|
+
*/
|
|
23
|
+
export declare function writePaidSections(targetDir: string): boolean;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The paid-sections registry (#7536) — `apps/web/lib/paid-sections.generated.ts`,
|
|
3
|
+
* the one file through which base-tree code reaches a paid UI package.
|
|
4
|
+
*
|
|
5
|
+
* Base code used to load `@working-theory/ui-commerce` with a
|
|
6
|
+
* `webpackIgnore`d `import()`, which hands resolution to Node's ESM loader at
|
|
7
|
+
* request time. That loader cannot read our `dist`, so the load failed and the
|
|
8
|
+
* `catch` answered `false`: the feature rendered nothing in production. This
|
|
9
|
+
* file owns the optionality at scaffold time instead. A package the scaffold
|
|
10
|
+
* carries is a plain `import()` webpack bundles; one it does not carry is a
|
|
11
|
+
* typed stub that answers `null`.
|
|
12
|
+
*
|
|
13
|
+
* Rows come from `MODULE_SLICES` (`paidSectionsPackage`); presence comes from
|
|
14
|
+
* the scaffold's own `apps/web/package.json` `dependencies`, the one fact
|
|
15
|
+
* every layer (snapshot transform, module `patches`, `add-module`) already
|
|
16
|
+
* keeps true. The monorepo's committed copy is the render with every row
|
|
17
|
+
* present and is pinned byte-equal by `__tests__/paid-sections.test.ts`.
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { MODULE_SLICES } from "./module-architecture.js";
|
|
22
|
+
import { RECOGNIZED_MODULES } from "./parse-args.js";
|
|
23
|
+
/** Scaffold-relative path of the generated registry. */
|
|
24
|
+
export const PAID_SECTIONS_PATH = "apps/web/lib/paid-sections.generated.ts";
|
|
25
|
+
function pascalCase(shortName) {
|
|
26
|
+
return shortName
|
|
27
|
+
.split("-")
|
|
28
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
29
|
+
.join("");
|
|
30
|
+
}
|
|
31
|
+
/** One row per slice declaring `paidSectionsPackage`, in `RECOGNIZED_MODULES` order. */
|
|
32
|
+
export function paidSectionRows() {
|
|
33
|
+
const rows = [];
|
|
34
|
+
for (const name of RECOGNIZED_MODULES) {
|
|
35
|
+
const pkg = MODULE_SLICES[name].paidSectionsPackage;
|
|
36
|
+
if (pkg === undefined)
|
|
37
|
+
continue;
|
|
38
|
+
const pascal = pascalCase(pkg.slice(pkg.indexOf("/") + 1));
|
|
39
|
+
rows.push({
|
|
40
|
+
module: name,
|
|
41
|
+
package: pkg,
|
|
42
|
+
loader: `load${pascal}`,
|
|
43
|
+
contract: `${pascal}Contract`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return rows;
|
|
47
|
+
}
|
|
48
|
+
const HEADER = `// paid-sections.generated.ts — the paid section packages this site carries.
|
|
49
|
+
//
|
|
50
|
+
// GENERATED by create-ailk from MODULE_SLICES (\`paidSectionsPackage\`) and
|
|
51
|
+
// this app's package.json dependencies (#7536). Do not edit: every scaffold
|
|
52
|
+
// and \`create-ailk add-module\` rewrites it. A carried package is a bundled
|
|
53
|
+
// dynamic import; one this site does not carry is a stub that answers null.
|
|
54
|
+
`;
|
|
55
|
+
/**
|
|
56
|
+
* Render the registry. `present` holds the package names whose row is a
|
|
57
|
+
* bundled `import()`; every other row is a stub. Pure and order-independent.
|
|
58
|
+
* Line 1 is `// oss: false` exactly when a row imports a paid package, so the
|
|
59
|
+
* public strip drops the file (\`check-oss-strip.sh\` condition (e)).
|
|
60
|
+
*/
|
|
61
|
+
export function renderPaidSections(present) {
|
|
62
|
+
const rows = paidSectionRows();
|
|
63
|
+
const contracts = rows.map((row) => row.contract).sort();
|
|
64
|
+
const anyPresent = rows.some((row) => present.has(row.package));
|
|
65
|
+
const lines = [];
|
|
66
|
+
if (anyPresent)
|
|
67
|
+
lines.push("// oss: false");
|
|
68
|
+
lines.push(HEADER.trimEnd());
|
|
69
|
+
lines.push("import type {");
|
|
70
|
+
for (const contract of contracts)
|
|
71
|
+
lines.push(` ${contract},`);
|
|
72
|
+
lines.push('} from "./paid-section-contracts";');
|
|
73
|
+
for (const row of rows) {
|
|
74
|
+
lines.push("");
|
|
75
|
+
const type = `Promise<${row.contract} | null>`;
|
|
76
|
+
// Shaped as Prettier formats it, so the committed copy survives the
|
|
77
|
+
// repo's format hook byte-for-byte.
|
|
78
|
+
if (present.has(row.package)) {
|
|
79
|
+
const head = `export const ${row.loader} = (): ${type} =>`;
|
|
80
|
+
const body = `import("${row.package}");`;
|
|
81
|
+
lines.push(head.length <= 80
|
|
82
|
+
? `${head}\n ${body}`
|
|
83
|
+
: `export const ${row.loader} =\n (): ${type} =>\n ${body}`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
const head = `export const ${row.loader} = async (): ${type} =>`;
|
|
87
|
+
lines.push(`${head} null;`.length <= 80
|
|
88
|
+
? `${head} null;`
|
|
89
|
+
: head.length <= 80
|
|
90
|
+
? `${head}\n null;`
|
|
91
|
+
: `export const ${row.loader} =\n async (): ${type} => null;`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return `${lines.join("\n")}\n`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Write the registry into a scaffold, deriving presence from the scaffold's
|
|
98
|
+
* own `apps/web/package.json`. Returns `false` (writing nothing) when that
|
|
99
|
+
* file or `apps/web/lib/` is absent — a scaffold with no web app.
|
|
100
|
+
*/
|
|
101
|
+
export function writePaidSections(targetDir) {
|
|
102
|
+
const pkgPath = join(targetDir, "apps/web/package.json");
|
|
103
|
+
if (!existsSync(pkgPath) || !existsSync(join(targetDir, "apps/web/lib"))) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
let pkg;
|
|
107
|
+
try {
|
|
108
|
+
pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
throw new Error(`cannot read ${pkgPath} to write the paid-sections registry: ${err.message}`, { cause: err });
|
|
112
|
+
}
|
|
113
|
+
const deps = pkg.dependencies ?? {};
|
|
114
|
+
const present = new Set(paidSectionRows()
|
|
115
|
+
.map((row) => row.package)
|
|
116
|
+
.filter((name) => Object.hasOwn(deps, name)));
|
|
117
|
+
writeFileSync(join(targetDir, PAID_SECTIONS_PATH), renderPaidSections(present));
|
|
118
|
+
return true;
|
|
119
|
+
}
|
package/dist/programmatic.js
CHANGED
|
@@ -34,6 +34,7 @@ import { assertLicenseKeyShape, writeLicenseKeyFiles, } from "./lib/license-key-
|
|
|
34
34
|
import { assertDesignAllowed, assertReleased, deriveLevel, resolveModuleSet, resolvePaidModules, } from "./lib/module-license-gate.js";
|
|
35
35
|
import { classifyModules } from "./lib/module-tier.js";
|
|
36
36
|
import { normalizeModules } from "./module-architecture.js";
|
|
37
|
+
import { writePaidSections } from "./paid-sections.js";
|
|
37
38
|
import { RECOGNIZED_SHAPES, RESERVED_SHAPES, } from "./parse-args.js";
|
|
38
39
|
import { SITE_CHROME_PATH, renderSiteChrome, resolveSiteChrome, } from "./site-chrome.js";
|
|
39
40
|
import { planSiteManifest } from "./site-manifest.js";
|
|
@@ -176,6 +177,10 @@ export function scaffoldSync(opts) {
|
|
|
176
177
|
// synchronous caller) writes DEFAULT_SITE_CHROME, byte-identical to the
|
|
177
178
|
// monorepo's own committed apps/web/lib/site-chrome.ts.
|
|
178
179
|
writeFileSync(join(targetAbs, SITE_CHROME_PATH), renderSiteChrome(resolveSiteChrome(opts.layout, opts.designLayout)));
|
|
180
|
+
// The paid-sections registry (#7536), from this scaffold's own
|
|
181
|
+
// `apps/web/package.json` — on the keyless snapshot, the all-stub render.
|
|
182
|
+
// `scaffold()` writes it again once every layer has landed.
|
|
183
|
+
writePaidSections(targetAbs);
|
|
179
184
|
// Opt-in only (#4636). Every write above this line is unconditional, so a
|
|
180
185
|
// scaffold with `withCatalog` unset touches exactly the files it did before.
|
|
181
186
|
if (opts.withCatalog === true)
|
|
@@ -653,6 +658,9 @@ export async function scaffold(opts) {
|
|
|
653
658
|
}));
|
|
654
659
|
}
|
|
655
660
|
}
|
|
661
|
+
// #7536 D3 — the registry follows `apps/web/package.json`, which a module
|
|
662
|
+
// bundle's `patches` (and the template layer) just finished editing.
|
|
663
|
+
writePaidSections(result.targetDir);
|
|
656
664
|
// #6107 D2 — after every layer landed, so nothing later overwrites it: a
|
|
657
665
|
// served scaffold needs BETTER_AUTH_SECRET (#6044), and this is its value.
|
|
658
666
|
writeAuthSecretFile(result.targetDir);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-ailk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"tar": "^7.5.21"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@working-theory/schema": "0.
|
|
31
|
-
"@working-theory/templates": "0.
|
|
30
|
+
"@working-theory/schema": "0.34.1",
|
|
31
|
+
"@working-theory/templates": "0.34.1"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
package/templates/.env.example
CHANGED
|
@@ -85,7 +85,9 @@ CONTENT_ADAPTER=mdx
|
|
|
85
85
|
# ── Single-tenant operator pin (apps/api) ────────────────────────────────────
|
|
86
86
|
# REQUIRED on a build WITHOUT the multi-tenant module — for the submissions
|
|
87
87
|
# review dashboard AND for the `workspace` module's settings surface.
|
|
88
|
-
#
|
|
88
|
+
# REQUIRED with the multi-tenant module too, on a deployment that has exactly
|
|
89
|
+
# one workspace (the seeded default) — see "With the multi-tenant module"
|
|
90
|
+
# below (#7523).
|
|
89
91
|
#
|
|
90
92
|
# On a single-tenant deployment (the default Workspace exists and is the ONLY
|
|
91
93
|
# Workspace) the FIRST authenticated user to resolve a tenant context is
|
|
@@ -111,8 +113,35 @@ CONTENT_ADAPTER=mdx
|
|
|
111
113
|
# to its owner.
|
|
112
114
|
#
|
|
113
115
|
# If your own dashboard returns 403, this variable is the first thing to
|
|
114
|
-
# check.
|
|
115
|
-
#
|
|
116
|
+
# check.
|
|
117
|
+
#
|
|
118
|
+
# With the multi-tenant module (#7523): a session that has activated a
|
|
119
|
+
# workspace (every Admin-app request) resolves from that workspace and its
|
|
120
|
+
# Membership, unchanged. A session with NO active workspace — every apps/web
|
|
121
|
+
# request, including the /admin* and /waitlist* pages — is granted the
|
|
122
|
+
# default workspace only when this pin is set, the caller's VERIFIED email
|
|
123
|
+
# equals it, and the deployment has exactly one workspace. Unset, apps/api
|
|
124
|
+
# seats nobody: those requests answer 403 and no Membership is written. Every
|
|
125
|
+
# default-organization member other than the pinned operator reaches their
|
|
126
|
+
# workspace through the Admin app instead.
|
|
127
|
+
#
|
|
128
|
+
# The every-site waitlist read (kind: "all" on /v1/waitlist-experiments/*)
|
|
129
|
+
# needs the caller to BE this pin — a sentinel-organization owner whose
|
|
130
|
+
# verified email is this address. Holding an owner Membership on the default
|
|
131
|
+
# organization is not enough on its own.
|
|
132
|
+
#
|
|
133
|
+
# If a one-workspace multi-tenant deployment EVER ran with this unset, the
|
|
134
|
+
# first signed-in caller may already hold the owner Membership, and this fix
|
|
135
|
+
# does not revoke a Membership minted earlier. Check, on the database:
|
|
136
|
+
# 1. every Membership on organization
|
|
137
|
+
# org_00000000-0000-7000-8000-000000000001 — start with id
|
|
138
|
+
# member_00000000-0000-7000-8000-000000000001 (the operator Membership) —
|
|
139
|
+
# and every pending Invitation on it;
|
|
140
|
+
# 2. every Workspace other than wksp_00000000-0000-7000-8000-000000000001
|
|
141
|
+
# (one extra workspace also stops the real operator binding through the
|
|
142
|
+
# fallback);
|
|
143
|
+
# 3. delete whatever does not belong to the pinned operator or someone they
|
|
144
|
+
# invited.
|
|
116
145
|
# AILK_OPERATOR_EMAIL=
|
|
117
146
|
#
|
|
118
147
|
# ── Site id (apps/api, apps/web) ─────────────────────────────────────────────
|
|
@@ -399,8 +428,14 @@ CONTENT_ADAPTER=mdx
|
|
|
399
428
|
# Server-side only — read inside apps/api, never exposed to the browser.
|
|
400
429
|
# Unset is a supported state: the booker degrades to the provider's own
|
|
401
430
|
# booking page. Full notes in apps/api/.env.example.
|
|
431
|
+
# CAL_API_KEY is the deployment's account; it serves a workspace only on a
|
|
432
|
+
# one-workspace deployment — each workspace can connect its own (#7533).
|
|
402
433
|
# CAL_API_KEY=cal_live_xxx
|
|
403
434
|
# CAL_API_BASE_URL=https://api.cal.com
|
|
435
|
+
# The key per-workspace connections are encrypted under (#7533): exactly 32
|
|
436
|
+
# random bytes, `openssl rand -base64 32`. Unset = connections cannot be
|
|
437
|
+
# written; any other shape stops the API. Losing it unreads every connection.
|
|
438
|
+
# SCHEDULING_CREDENTIAL_KEY=
|
|
404
439
|
# HMAC-SHA256 key POST /v1/webhooks/calcom verifies the x-cal-signature-256
|
|
405
440
|
# header against (#6085). Unset makes the receiver answer 503 and accept
|
|
406
441
|
# nothing — the same fail-closed posture an unset STRIPE_WEBHOOK_SECRET has.
|
|
@@ -472,12 +507,43 @@ CONTENT_ADAPTER=mdx
|
|
|
472
507
|
# AILK_RATE_LIMIT_LEADS_PER_MINUTE=10
|
|
473
508
|
# AILK_RATE_LIMIT_LEADS_PER_KEY_PER_MINUTE=300
|
|
474
509
|
#
|
|
510
|
+
# HUB (apps/api). The listicle directory read's per-IP limit per minute
|
|
511
|
+
# (#7521) — its own scope, not the leads one, because the web server renders
|
|
512
|
+
# it from one egress address. A whole number from 1 to 999999. Default: 120.
|
|
513
|
+
# AILK_RATE_LIMIT_DIRECTORY_PER_MINUTE=120
|
|
514
|
+
#
|
|
475
515
|
# SPOKE (apps/web, server-only — never NEXT_PUBLIC_). This spoke's key: one
|
|
476
516
|
# entry of the hub's AILK_VISITOR_FORWARD_KEYS. Sent only when AILK_API_URL
|
|
477
517
|
# is https (or loopback). Issue a key only to a spoke whose platform
|
|
478
518
|
# overwrites x-forwarded-for / x-real-ip with the real client address.
|
|
479
519
|
# AILK_VISITOR_FORWARD_KEY=
|
|
480
520
|
#
|
|
521
|
+
# Hub and spokes — a spoke's reads and writes resolve to ITS OWN workspace
|
|
522
|
+
# (#7532). A hub holding one workspace per spoke registers each spoke in its
|
|
523
|
+
# database (`pnpm --filter @working-theory/api spokes register --workspace
|
|
524
|
+
# <wksp_id> --name <label>`), which prints the spoke's key once. The spoke
|
|
525
|
+
# sends it on every server-side call, and the hub resolves meeting types, the
|
|
526
|
+
# three lead writes, bookings and public content reads to that spoke's
|
|
527
|
+
# workspace. Runbook: docs/runbooks/site-configuration.md § Hub and spokes:
|
|
528
|
+
# registering a spoke. Both unset = the behaviour before #7532.
|
|
529
|
+
#
|
|
530
|
+
# HUB (apps/api). `true` (or `1`) refuses every header-less call to those
|
|
531
|
+
# surfaces: no key, no workspace. Unset, empty, `false` or `0` = off; any
|
|
532
|
+
# other value stops the API at boot, and so does `true` with no DATABASE_URL.
|
|
533
|
+
# Do NOT turn it on while a spoke still renders the browser booker section,
|
|
534
|
+
# NewsletterFormInline or StepFlow — those post from the BROWSER, which holds
|
|
535
|
+
# no key. Register every caller first, the hub's own apps/web and apps/mcp
|
|
536
|
+
# included. On a hub, AILK_SITE_ID must name the PLATFORM OPERATOR's own
|
|
537
|
+
# workspace, never a spoke's: it is where every header-less lead and every
|
|
538
|
+
# booking no spoke bound lands.
|
|
539
|
+
# AILK_HUB_REQUIRE_SPOKE_KEY=
|
|
540
|
+
#
|
|
541
|
+
# SPOKE (apps/web and apps/mcp, server-only — never NEXT_PUBLIC_). The key the
|
|
542
|
+
# hub's `spokes register` printed. Sent as `x-ailk-spoke-key` on every
|
|
543
|
+
# server-side call to AILK_API_URL, and only when that is https (or
|
|
544
|
+
# loopback). Not the same value as AILK_VISITOR_FORWARD_KEY above.
|
|
545
|
+
# AILK_SPOKE_KEY=
|
|
546
|
+
#
|
|
481
547
|
# NEXT_PUBLIC_ENABLED_LOCALES — do NOT set by hand. Baked at build time by
|
|
482
548
|
# apps/web/next.config.mjs from project.yaml `product.locales` (falls back to
|
|
483
549
|
# "en" when project.yaml is absent) and read by middleware.ts, the sitemap
|
|
@@ -56,6 +56,13 @@ STRIPE_WEBHOOK_SECRET=whsec_xxx
|
|
|
56
56
|
# Unset CAL_API_KEY is a supported state, not a broken one — the routes answer
|
|
57
57
|
# 503 not_configured with the provider's public booking page, and the section
|
|
58
58
|
# renders a link out to it rather than an error.
|
|
59
|
+
#
|
|
60
|
+
# CAL_API_KEY is the DEPLOYMENT's account. Since #7533 each workspace can hold
|
|
61
|
+
# its own connection (PUT /v1/workspaces/:workspaceId/meetings/connection),
|
|
62
|
+
# and a workspace's connection always wins. CAL_API_KEY serves a workspace
|
|
63
|
+
# only on a one-workspace deployment (its sole workspace the seeded default,
|
|
64
|
+
# as on every single-tenant scaffold); on a deployment with two or more
|
|
65
|
+
# workspaces, a workspace with no connection answers not_configured.
|
|
59
66
|
# CAL_API_KEY=cal_live_xxx
|
|
60
67
|
# Self-hosted Cal instances only; defaults to https://api.cal.com.
|
|
61
68
|
# CAL_API_BASE_URL=https://api.cal.com
|
|
@@ -65,12 +72,27 @@ STRIPE_WEBHOOK_SECRET=whsec_xxx
|
|
|
65
72
|
# (#4288) lands as a second value here with no change to the section.
|
|
66
73
|
# SCHEDULING_PROVIDER=cal
|
|
67
74
|
#
|
|
75
|
+
# Per-workspace scheduling connections (#7533) — the AES-256-GCM key each
|
|
76
|
+
# workspace's provider credential and webhook secret are encrypted under.
|
|
77
|
+
# Exactly 32 random bytes in canonical standard base64:
|
|
78
|
+
# openssl rand -base64 32
|
|
79
|
+
# Any other value stops the API at startup. Unset means connections cannot be
|
|
80
|
+
# written (PUT answers 503 credential_key_not_configured) and CAL_API_KEY works
|
|
81
|
+
# exactly as before. LOSING OR CHANGING THIS KEY makes every stored connection
|
|
82
|
+
# unreadable: each workspace must then reconnect. Never log or share it.
|
|
83
|
+
# SCHEDULING_CREDENTIAL_KEY=
|
|
84
|
+
#
|
|
68
85
|
# The booking webhook receiver (#6085) — a SEPARATE credential from the
|
|
69
86
|
# three above: it verifies POST /v1/webhooks/calcom's x-cal-signature-256
|
|
70
87
|
# header, it does not call the provider. Unset makes the receiver answer 503
|
|
71
88
|
# and accept nothing (the same fail-closed posture STRIPE_WEBHOOK_SECRET
|
|
72
89
|
# unset already has) — every booking is then silently lost, so this must be
|
|
73
90
|
# set for the receiver to do anything.
|
|
91
|
+
# It verifies only the ENV account's receiver, whose bookings land in the
|
|
92
|
+
# AILK_SITE_ID workspace. A workspace's own connection has its own entry point
|
|
93
|
+
# (POST /v1/webhooks/calcom/<token>) and its own secret, both returned once by
|
|
94
|
+
# the connect PUT (#7533). On a hub whose spokes all have connections, unset
|
|
95
|
+
# this to retire the env receiver (it then answers 503).
|
|
74
96
|
# CAL_WEBHOOK_SECRET=
|
|
75
97
|
# GET /v1/schedule/appointments only returns rows where this deployment's
|
|
76
98
|
# AILK_SITE_ID (declared below, under "Project listings — site addressing")
|