create-ailk 0.19.1 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -7
- package/component-catalog.json +184 -6
- package/component-catalog.md +20 -9
- package/dist/apply-design.d.ts +9 -0
- package/dist/apply-design.js +78 -31
- package/dist/cli.js +35 -0
- package/dist/copy.d.ts +41 -0
- package/dist/copy.js +87 -21
- package/dist/design-home.d.ts +54 -11
- package/dist/design-home.js +160 -22
- package/dist/lib/design-checks.d.ts +6 -1
- package/dist/lib/design-checks.js +23 -2
- package/dist/lib/fetch-template-snapshot.d.ts +21 -0
- package/dist/lib/fetch-template-snapshot.js +205 -0
- package/dist/lib/merge-three-way.d.ts +31 -0
- package/dist/lib/merge-three-way.js +65 -0
- package/dist/module-architecture.js +55 -2
- package/dist/programmatic.d.ts +7 -1
- package/dist/programmatic.js +76 -22
- package/dist/site-roster.d.ts +4 -2
- package/dist/site-roster.js +7 -5
- package/dist/spoke-checkout.d.ts +7 -0
- package/dist/spoke-checkout.js +8 -1
- package/dist/starter-grid.js +16 -0
- package/dist/surfaces.js +78 -2
- package/dist/sync-routes.d.ts +28 -0
- package/dist/sync-routes.js +123 -1
- package/dist/template-record.d.ts +59 -0
- package/dist/template-record.js +183 -0
- package/dist/update.d.ts +53 -0
- package/dist/update.js +606 -0
- package/package.json +3 -3
- package/site-templates/_base/ailk-template.json +2 -2
- package/site-templates/_base/content/en/WorkingAgreementPage/working-agreement.mdx +78 -0
- package/site-templates/designs/minimalism/DESIGN.md +1 -1
- package/site-templates/designs/minimalism/contact/local-business-service.mdx +23 -0
- package/site-templates/designs/minimalism/contact/organization-product.mdx +23 -0
- package/site-templates/designs/minimalism/contact/organization-service.mdx +23 -0
- package/site-templates/designs/minimalism/contact/person-service.mdx +23 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +11 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +34 -23
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/local-business-service/TEMPLATE-MANIFEST.md +33 -23
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/organization-service/TEMPLATE-MANIFEST.md +33 -23
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.json +18 -1
- package/site-templates/shapes/person-service/TEMPLATE-MANIFEST.md +31 -21
- package/templates/.claude/cloudparity-repo-hooks.json +9 -0
- package/templates/.claude/hooks/architecture-check.sh +8 -0
- package/templates/.claude/rules/loop-contract.md +129 -0
- package/templates/.claude/settings.json +3 -2
- package/templates/.env.example +13 -0
- package/templates/CLAUDE.md +1 -1
- package/templates/CONVENTIONS.md +3 -2
- package/templates/README.md +26 -25
- package/templates/_gitignore +162 -0
- package/templates/apps/api/CLAUDE.md +18 -10
- package/templates/apps/api/jest.config.cjs +1 -0
- package/templates/apps/api/package.json +2 -1
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +22 -2
- package/templates/apps/api/src/__tests__/setup.ts +7 -0
- package/templates/apps/api/src/bin/__tests__/rate-limit-probe.test.ts +184 -0
- package/templates/apps/api/src/bin/rate-limit-probe.ts +53 -0
- package/templates/apps/api/src/lib/proposal-paid-store.ts +49 -0
- package/templates/apps/api/src/lib/rate-limit-probe.ts +165 -0
- package/templates/apps/api/src/lib/rate-limit-store.ts +142 -0
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +5 -2
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +2 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit-cross-isolate.db.test.ts +220 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit-store.test.ts +430 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +21 -14
- package/templates/apps/api/src/middleware/auth.ts +8 -4
- package/templates/apps/api/src/middleware/default-tenant.ts +9 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +290 -82
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +26 -0
- package/templates/apps/api/src/routes/webhooks/README.md +33 -5
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-proposal-paid.test.ts +369 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +442 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +184 -6
- package/templates/apps/api/src/server.ts +14 -2
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +91 -0
- package/templates/apps/api/src/services/audit-log.ts +72 -10
- package/templates/apps/api/src/services/default-tenant.ts +12 -3
- package/templates/apps/api/src/services/hub-sign-in.ts +2 -1
- package/templates/apps/api/src/services/project-listings.ts +6 -1
- package/templates/apps/api/src/services/public-site-resolver.ts +21 -2
- package/templates/apps/api/vercel.json +1 -1
- package/templates/apps/mcp/CLAUDE.md +19 -7
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +139 -44
- package/templates/apps/mcp/jest.config.cjs +1 -0
- package/templates/apps/mcp/src/tools/person_notes.ts +25 -0
- package/templates/apps/mcp/src/tools/proposals.ts +27 -0
- package/templates/apps/mcp/src/workspace-server.ts +16 -0
- package/templates/apps/web/CLAUDE.md +26 -1
- package/templates/apps/web/_gitignore +6 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +165 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +19 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +106 -23
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up.actions.ts +125 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/agreements-prompt.test.tsx +281 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +28 -83
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +27 -13
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +57 -2
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +2 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +102 -18
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/onboarding.test.tsx +323 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/privacy.test.tsx +365 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +3 -2
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +8 -0
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-actions.ts +170 -0
- package/templates/apps/web/app/[locale]/(authed)/account/agreements-prompt.tsx +181 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +88 -3
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/pay-form.tsx +65 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +27 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +176 -3
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/pay.test.tsx +193 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +188 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +65 -4
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +17 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +11 -2
- package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +5 -24
- package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +64 -18
- package/templates/apps/web/app/[locale]/(authed)/account/onboarding/actions.ts +120 -0
- package/templates/apps/web/app/[locale]/(authed)/account/onboarding/onboarding-form.tsx +322 -0
- package/templates/apps/web/app/[locale]/(authed)/account/onboarding/page.tsx +126 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +43 -1
- package/templates/apps/web/app/[locale]/(authed)/account/privacy/actions.ts +65 -0
- package/templates/apps/web/app/[locale]/(authed)/account/privacy/page.tsx +226 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +5 -3
- package/templates/apps/web/app/[locale]/about/[slug]/page.tsx +46 -0
- package/templates/apps/web/app/[locale]/about/__tests__/about-routes.test.tsx +350 -0
- package/templates/apps/web/app/[locale]/about/page.tsx +83 -1
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/consent-banner.tsx +32 -0
- package/templates/apps/web/app/[locale]/consent.actions.ts +71 -0
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +7 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-magnet.test.ts +138 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-form-text.test.ts +17 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact-magnet-page.test.tsx +347 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +73 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +5 -3
- package/templates/apps/web/app/[locale]/contact/contact-form-magnet.ts +77 -0
- package/templates/apps/web/app/[locale]/contact/contact-form-text.ts +2 -0
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +48 -4
- package/templates/apps/web/app/[locale]/contact/page.tsx +81 -9
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +25 -3
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/layout.tsx +46 -4
- package/templates/apps/web/app/[locale]/newsletter/newsletter.actions.ts +12 -1
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +2 -2
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe-rsc-boundary.test.tsx +209 -0
- package/templates/apps/web/app/[locale]/unsubscribe/__tests__/unsubscribe.test.tsx +158 -0
- package/templates/apps/web/app/[locale]/unsubscribe/page.tsx +54 -0
- package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe-button.tsx +73 -0
- package/templates/apps/web/app/[locale]/unsubscribe/unsubscribe.actions.ts +55 -0
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/working-agreement/__tests__/page.test.tsx +63 -0
- package/templates/apps/web/app/[locale]/working-agreement/page.tsx +41 -0
- package/templates/apps/web/app/robots.txt/route.ts +13 -10
- package/templates/apps/web/jest.config.cjs +4 -0
- package/templates/apps/web/lib/__tests__/consent-visitor.test.ts +163 -0
- package/templates/apps/web/lib/__tests__/deck-page.test.ts +244 -0
- package/templates/apps/web/lib/__tests__/relay.test.ts +49 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +100 -9
- package/templates/apps/web/lib/account-config.ts +31 -16
- package/templates/apps/web/lib/consent-visitor.ts +103 -0
- package/templates/apps/web/lib/deck-page.ts +186 -0
- package/templates/apps/web/lib/relay.ts +20 -0
- package/templates/apps/web/messages/en.json +83 -9
- package/templates/apps/web/next.config.mjs +8 -2
- package/templates/database/CHANGELOG.md +48 -0
- package/templates/database/__tests__/outbound-message-action.db.test.ts +80 -0
- package/templates/database/__tests__/person-notes.db.test.ts +249 -0
- package/templates/database/__tests__/piece-kind-migration.test.ts +67 -0
- package/templates/database/__tests__/piece-kind.db.test.ts +208 -0
- package/templates/database/__tests__/seed.test.ts +39 -2
- package/templates/database/_gitignore +8 -0
- package/templates/database/inbox/schema.prisma +38 -12
- package/templates/database/jest.config.cjs +1 -0
- package/templates/database/migrations/20261015120000_person_notes/migration.sql +77 -0
- package/templates/database/migrations/20261015120000_rate_limit_counters/migration.sql +43 -0
- package/templates/database/migrations/20261016120000_piece_kind/migration.sql +95 -0
- package/templates/database/migrations/20261017120000_outbound_message_action/migration.sql +30 -0
- package/templates/database/ops/schema.prisma +16 -0
- package/templates/database/package.json +1 -1
- package/templates/database/seed.ts +29 -0
- package/templates/project.yaml +6 -3
- package/templates/tools/jest/pin-timezone.cjs +34 -0
- package/versions.json +35 -35
- package/templates/.claude/rules/preserve-existing-style.md +0 -51
|
@@ -83,7 +83,7 @@ Every tool maps 1:1 to an `apps/api` endpoint. When you change a tool, change th
|
|
|
83
83
|
|
|
84
84
|
- **20 in the `TOOLS` array / parity test**: the `TOOLS` array in
|
|
85
85
|
`packages/metadata/src/mcp-tool-catalog.ts` defines 20 anonymous tools; the
|
|
86
|
-
parity test counts that full array. It also holds the
|
|
86
|
+
parity test counts that full array. It also holds the fifteen signed-in
|
|
87
87
|
workspace tools (#3591), which no anonymous view returns — see § The
|
|
88
88
|
signed-in workspace surface below.
|
|
89
89
|
- **19 registered on a KEYLESS deployment, 20 on a commerce-claimed one**
|
|
@@ -126,16 +126,24 @@ here. Spec: `docs/specs/3591-tenant-scoped-agent-auth-secure-spec.md` D13–D16.
|
|
|
126
126
|
- **apps/mcp checks only the key's shape.** The key is attached as the SDK's `req.auth` for that
|
|
127
127
|
request alone and reaches a tool as `extra.authInfo.token`. apps/api is the one place it is
|
|
128
128
|
checked, and its tenant guard and the caller's clamped role decide every answer.
|
|
129
|
-
- **
|
|
129
|
+
- **Fifteen tools, each a `twin`.** The catalog rows carry `surface: "workspace"`, `tier: "pro"`
|
|
130
130
|
and `modulesAnyOf` (the modules any one of which registers the tool — its twin route's gate):
|
|
131
|
-
`get_agent_context`, the four message tools, the three submission tools
|
|
132
|
-
application tools
|
|
131
|
+
`get_agent_context`, the four message tools, the three submission tools, the three
|
|
132
|
+
application tools and (#8514) the two People notes tools, `get_person_notes` and
|
|
133
|
+
`set_person_notes` — the team's private note on one client, which an agent reads and writes
|
|
134
|
+
only with the `member-pieces-manage` grant; `set_person_notes` requires the `ifUpdatedAt` it
|
|
135
|
+
read and is refused `notes_changed` when someone saved since — and (#8516) the two proposals
|
|
136
|
+
tools, `list_proposals` and `create_proposal`, twinning `GET`/`POST
|
|
137
|
+
/v1/workspaces/:workspaceId/people/:personId/proposals` with the same grant: a proposal is
|
|
138
|
+
private to one client, `payments` says whether approval leads to a Stripe page, and an agent's
|
|
139
|
+
price is refused `proposal_needs_person` (a person prices). Each takes its route's params, query and body and returns the route's
|
|
133
140
|
`data`. They are in the parity guard like every other tool, and never in
|
|
134
141
|
`/.well-known/mcp.json`.
|
|
135
142
|
- **Handlers forward, nothing else.** `workspace-server.ts` resolves each tool to a paid-slice
|
|
136
143
|
handler file (`tools/agent_context.ts`, `inbox_messages.ts`, `inbox_submissions.ts`,
|
|
137
|
-
`inbox_applications.ts`, all `// oss: false`) through a
|
|
138
|
-
Each calls `@working-theory/api-client`'s `inbox` surface
|
|
144
|
+
`inbox_applications.ts`, `person_notes.ts`, `proposals.ts`, all `// oss: false`) through a
|
|
145
|
+
specifier table, as `server.ts` does. Each calls `@working-theory/api-client`'s `inbox` surface
|
|
146
|
+
(the notes and proposals tools, its `people` surface) with the key. A refusal from the
|
|
139
147
|
route is an MCP tool error naming the route's `error.code` and HTTP status only.
|
|
140
148
|
- **Never log or return the key.** Don't log or serialise `extra`, `extra.authInfo` or
|
|
141
149
|
`extra.requestInfo` — the SDK puts the raw `authorization` header there.
|
|
@@ -211,7 +219,11 @@ Example (lead routing):
|
|
|
211
219
|
- Don't add a tool without the matching `apps/api` endpoint.
|
|
212
220
|
- Don't put business logic in tool handlers — they're adapters.
|
|
213
221
|
|
|
214
|
-
Last updated: 2026-10-
|
|
222
|
+
Last updated: 2026-10-14 — #8516: the signed-in workspace surface is fifteen tools — `list_proposals` and `create_proposal` (`tools/proposals.ts`) twin `GET`/`POST /v1/workspaces/:workspaceId/people/:personId/proposals` — verified against `packages/metadata/src/mcp-tool-catalog.ts`, `apps/mcp/src/workspace-server.ts` and `tests/parity/api-parity/kinds/site.ts`.
|
|
223
|
+
|
|
224
|
+
Prior: 2026-10-13 — #8514: the signed-in workspace surface is thirteen tools — `get_person_notes` and `set_person_notes` (`tools/person_notes.ts`) twin `GET`/`PUT /v1/workspaces/:workspaceId/people/:personId/notes` — verified against `packages/metadata/src/mcp-tool-catalog.ts`, `apps/mcp/src/workspace-server.ts` and `tests/parity/api-parity/kinds/site.ts`.
|
|
225
|
+
|
|
226
|
+
Prior: 2026-10-10 — #8071: `create_booking` and `submit_flow_step` join the HTTP write branch — § The free anonymous tools names its six members, the Booking and Flow rows record it, and § What a spoke's MCP host on Vercel enforces lists the two new daily caps — verified against `apps/mcp/src/http-transport.ts` and `apps/mcp/src/abuse/index.ts`.
|
|
215
227
|
|
|
216
228
|
Prior: 2026-10-10 — #8070 (amendment 2): the Vercel entry exports a handler, no `vercel.json`; § Two entrypoints, one boot — the Vercel entry `src/app.ts`, the shared `src/boot.ts`, `MCP_HTTP_STATELESS`, `mcpContentRoot()`, `create_project` withheld on a spoke, and what a spoke's MCP host enforces — verified against `apps/mcp/src/{app,boot,serverless-app,index,http-transport,server}.ts` and `lib/content-adapter.ts`.
|
|
217
229
|
|
|
@@ -41,8 +41,29 @@ import {
|
|
|
41
41
|
startMcpClaimRefresh,
|
|
42
42
|
type McpCommerceClaim,
|
|
43
43
|
} from "../src/lib/commerce-claim.js";
|
|
44
|
+
import { UNSERVED_MCP_TOOLS } from "../src/lib/topology.generated.js";
|
|
44
45
|
import { createMcpServer, listRegisteredTools } from "../src/server.js";
|
|
45
46
|
|
|
47
|
+
// The topology seam's UNSERVED_MCP_TOOLS (#8070 D7), made switchable the way
|
|
48
|
+
// server.test.ts's AC-8b suite does: a getter returning an override when one
|
|
49
|
+
// is set, else the real module's value, so the 16-state loop below reads
|
|
50
|
+
// this scaffold's own seam ([] in the monorepo and a hub, ["create_project"]
|
|
51
|
+
// in a spoke) and the #8513 D2 describe can force either state regardless.
|
|
52
|
+
const mockSeam: { unserved: readonly string[] | undefined } = {
|
|
53
|
+
unserved: undefined,
|
|
54
|
+
};
|
|
55
|
+
// eslint-disable-next-line no-restricted-syntax -- the generated topology seam IS the boundary under test (#8513 D2); every other export passes through from the real module
|
|
56
|
+
jest.mock("../src/lib/topology.generated.js", () => {
|
|
57
|
+
const actual: { UNSERVED_MCP_TOOLS: readonly string[] } =
|
|
58
|
+
jest.requireActual("../src/lib/topology.generated.js");
|
|
59
|
+
const mocked = { ...actual };
|
|
60
|
+
Object.defineProperty(mocked, "UNSERVED_MCP_TOOLS", {
|
|
61
|
+
enumerable: true,
|
|
62
|
+
get: () => mockSeam.unserved ?? actual.UNSERVED_MCP_TOOLS,
|
|
63
|
+
});
|
|
64
|
+
return mocked;
|
|
65
|
+
});
|
|
66
|
+
|
|
46
67
|
/** The injected licence reader — same seam `commerce-claim.test.ts` types. */
|
|
47
68
|
type CheckLicenseSeam = NonNullable<
|
|
48
69
|
NonNullable<Parameters<typeof initMcpCommerceClaim>[0]>["checkLicense"]
|
|
@@ -125,6 +146,17 @@ function catalogOptionsFor(
|
|
|
125
146
|
};
|
|
126
147
|
}
|
|
127
148
|
|
|
149
|
+
/**
|
|
150
|
+
* The catalog this topology actually serves — the full catalog minus the
|
|
151
|
+
* seam's own UNSERVED_MCP_TOOLS, read at use (never destructured at module
|
|
152
|
+
* load) so a mocked override above is seen (#8513 D2, C3).
|
|
153
|
+
*/
|
|
154
|
+
function servedCatalog(options: McpCatalogOptions): McpToolCatalogEntry[] {
|
|
155
|
+
return getMcpToolCatalog(options).filter(
|
|
156
|
+
(t) => !UNSERVED_MCP_TOOLS.includes(t.name),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
128
160
|
describe.each(CLAIM_STATES)(
|
|
129
161
|
"MCP catalog ↔ server drift — commerce %s, mode %s, held %j",
|
|
130
162
|
(claim, mode, held) => {
|
|
@@ -150,18 +182,18 @@ describe.each(CLAIM_STATES)(
|
|
|
150
182
|
}
|
|
151
183
|
});
|
|
152
184
|
|
|
153
|
-
it("registered tool names equal catalog
|
|
185
|
+
it("registered tool names equal the catalog minus this topology's unserved tools", () => {
|
|
154
186
|
const registered = registeredToolsOf(createMcpServer());
|
|
155
187
|
const registeredNames = Object.keys(registered).sort();
|
|
156
|
-
const catalogNames =
|
|
188
|
+
const catalogNames = servedCatalog(options)
|
|
157
189
|
.map((t) => t.name)
|
|
158
190
|
.sort();
|
|
159
191
|
expect(registeredNames).toEqual(catalogNames);
|
|
160
192
|
});
|
|
161
193
|
|
|
162
|
-
it("registered tool descriptions equal catalog descriptions", () => {
|
|
194
|
+
it("registered tool descriptions equal the served catalog's descriptions", () => {
|
|
163
195
|
const registered = registeredToolsOf(createMcpServer());
|
|
164
|
-
for (const entry of
|
|
196
|
+
for (const entry of servedCatalog(options)) {
|
|
165
197
|
expect(registered[entry.name]).toBeDefined();
|
|
166
198
|
expect(registered[entry.name]!.description).toBe(entry.description);
|
|
167
199
|
}
|
|
@@ -193,17 +225,18 @@ describe.each(CLAIM_STATES)(
|
|
|
193
225
|
},
|
|
194
226
|
);
|
|
195
227
|
|
|
196
|
-
describe("MCP catalog ↔ server
|
|
228
|
+
describe("MCP catalog ↔ server drift — UNSERVED_MCP_TOOLS subtracts from the served catalog (#8513 D2, AC-2)", () => {
|
|
197
229
|
const priorEnabledModules = process.env.AILK_ENABLED_MODULES;
|
|
198
230
|
|
|
199
231
|
beforeEach(() => {
|
|
200
232
|
process.env.AILK_ENABLED_MODULES = PAID_MODULES;
|
|
201
|
-
jest.useFakeTimers();
|
|
202
233
|
});
|
|
203
234
|
|
|
204
235
|
afterEach(() => {
|
|
205
|
-
|
|
206
|
-
|
|
236
|
+
mockSeam.unserved = undefined;
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
afterAll(() => {
|
|
207
240
|
if (priorEnabledModules === undefined) {
|
|
208
241
|
delete process.env.AILK_ENABLED_MODULES;
|
|
209
242
|
} else {
|
|
@@ -211,49 +244,111 @@ describe("MCP catalog ↔ server agreement after a bounded re-read (#7156 AC-1)"
|
|
|
211
244
|
}
|
|
212
245
|
});
|
|
213
246
|
|
|
214
|
-
it("a
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
checkLicense: resolving({
|
|
218
|
-
tier: "pro",
|
|
219
|
-
features: ["scheduling", "flows-deliverables", "commerce"],
|
|
220
|
-
}),
|
|
221
|
-
});
|
|
247
|
+
it("a spoke's seam (['create_project']) withholds it from registration and the served catalog", () => {
|
|
248
|
+
mockSeam.unserved = ["create_project"];
|
|
249
|
+
const options = catalogOptionsFor("unclaimed", "warn", []);
|
|
222
250
|
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
};
|
|
228
|
-
const bootNames = Object.keys(registeredToolsOf(createMcpServer())).sort();
|
|
229
|
-
expect(bootNames).toEqual(
|
|
230
|
-
getMcpToolCatalog(bootOptions)
|
|
231
|
-
.map((t) => t.name)
|
|
232
|
-
.sort(),
|
|
233
|
-
);
|
|
234
|
-
expect(bootNames).toHaveLength(20);
|
|
235
|
-
|
|
236
|
-
startMcpClaimRefresh({ checkLicense: resolving({ features: [] }) });
|
|
237
|
-
await jest.advanceTimersByTimeAsync(MCP_CLAIM_REFRESH_MS);
|
|
238
|
-
|
|
239
|
-
const refreshedOptions: McpCatalogOptions = {
|
|
240
|
-
addons: { commerce: false },
|
|
241
|
-
features: [],
|
|
242
|
-
featureGate: "refuse",
|
|
243
|
-
};
|
|
244
|
-
const catalogNames = getMcpToolCatalog(refreshedOptions)
|
|
251
|
+
const registeredNames = Object.keys(
|
|
252
|
+
registeredToolsOf(createMcpServer()),
|
|
253
|
+
).sort();
|
|
254
|
+
const servedNames = servedCatalog(options)
|
|
245
255
|
.map((t) => t.name)
|
|
246
256
|
.sort();
|
|
247
|
-
expect(catalogNames).toHaveLength(14);
|
|
248
257
|
|
|
249
|
-
|
|
258
|
+
expect(registeredNames).toEqual(servedNames);
|
|
259
|
+
expect(registeredNames).not.toContain("create_project");
|
|
260
|
+
expect(servedNames).not.toContain("create_project");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("a hub's seam ([]) registers the full catalog, create_project included", () => {
|
|
264
|
+
mockSeam.unserved = [];
|
|
265
|
+
const options = catalogOptionsFor("unclaimed", "warn", []);
|
|
266
|
+
|
|
267
|
+
const registeredNames = Object.keys(
|
|
250
268
|
registeredToolsOf(createMcpServer()),
|
|
251
269
|
).sort();
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const listedNames = listRegisteredTools()
|
|
270
|
+
const fullCatalogNames = getMcpToolCatalog(options)
|
|
255
271
|
.map((t) => t.name)
|
|
256
272
|
.sort();
|
|
257
|
-
|
|
273
|
+
|
|
274
|
+
expect(registeredNames).toEqual(fullCatalogNames);
|
|
275
|
+
expect(registeredNames).toContain("create_project");
|
|
258
276
|
});
|
|
259
277
|
});
|
|
278
|
+
|
|
279
|
+
describe.each([
|
|
280
|
+
["hub", []],
|
|
281
|
+
["spoke", ["create_project"]],
|
|
282
|
+
] as const)(
|
|
283
|
+
"MCP catalog ↔ server agreement after a bounded re-read (#7156 AC-1) — %s seam",
|
|
284
|
+
(_topology, unserved) => {
|
|
285
|
+
const priorEnabledModules = process.env.AILK_ENABLED_MODULES;
|
|
286
|
+
|
|
287
|
+
beforeEach(() => {
|
|
288
|
+
process.env.AILK_ENABLED_MODULES = PAID_MODULES;
|
|
289
|
+
mockSeam.unserved = unserved;
|
|
290
|
+
jest.useFakeTimers();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
afterEach(() => {
|
|
294
|
+
resetMcpCommerceClaim();
|
|
295
|
+
mockSeam.unserved = undefined;
|
|
296
|
+
jest.useRealTimers();
|
|
297
|
+
if (priorEnabledModules === undefined) {
|
|
298
|
+
delete process.env.AILK_ENABLED_MODULES;
|
|
299
|
+
} else {
|
|
300
|
+
process.env.AILK_ENABLED_MODULES = priorEnabledModules;
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("a new server and listRegisteredTools() match the catalog once the re-read resolves", async () => {
|
|
305
|
+
__setMcpFeatureGateForTest("refuse");
|
|
306
|
+
await initMcpCommerceClaim({
|
|
307
|
+
checkLicense: resolving({
|
|
308
|
+
tier: "pro",
|
|
309
|
+
features: ["scheduling", "flows-deliverables", "commerce"],
|
|
310
|
+
}),
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const bootOptions: McpCatalogOptions = {
|
|
314
|
+
addons: { commerce: true },
|
|
315
|
+
features: ["scheduling", "flows-deliverables"],
|
|
316
|
+
featureGate: "refuse",
|
|
317
|
+
};
|
|
318
|
+
const bootNames = Object.keys(
|
|
319
|
+
registeredToolsOf(createMcpServer()),
|
|
320
|
+
).sort();
|
|
321
|
+
// Both counts below are of the served catalog — the full catalog (20 at
|
|
322
|
+
// boot, 14 after the re-read) minus this seam's UNSERVED_MCP_TOOLS (#8665).
|
|
323
|
+
expect(bootNames).toEqual(
|
|
324
|
+
servedCatalog(bootOptions)
|
|
325
|
+
.map((t) => t.name)
|
|
326
|
+
.sort(),
|
|
327
|
+
);
|
|
328
|
+
expect(bootNames).toHaveLength(20 - UNSERVED_MCP_TOOLS.length);
|
|
329
|
+
|
|
330
|
+
startMcpClaimRefresh({ checkLicense: resolving({ features: [] }) });
|
|
331
|
+
await jest.advanceTimersByTimeAsync(MCP_CLAIM_REFRESH_MS);
|
|
332
|
+
|
|
333
|
+
const refreshedOptions: McpCatalogOptions = {
|
|
334
|
+
addons: { commerce: false },
|
|
335
|
+
features: [],
|
|
336
|
+
featureGate: "refuse",
|
|
337
|
+
};
|
|
338
|
+
const servedNames = servedCatalog(refreshedOptions)
|
|
339
|
+
.map((t) => t.name)
|
|
340
|
+
.sort();
|
|
341
|
+
expect(servedNames).toHaveLength(14 - UNSERVED_MCP_TOOLS.length);
|
|
342
|
+
|
|
343
|
+
const serverNames = Object.keys(
|
|
344
|
+
registeredToolsOf(createMcpServer()),
|
|
345
|
+
).sort();
|
|
346
|
+
expect(serverNames).toEqual(servedNames);
|
|
347
|
+
|
|
348
|
+
const listedNames = listRegisteredTools()
|
|
349
|
+
.map((t) => t.name)
|
|
350
|
+
.sort();
|
|
351
|
+
expect(listedNames).toEqual(servedNames);
|
|
352
|
+
});
|
|
353
|
+
},
|
|
354
|
+
);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// oss: false
|
|
2
|
+
/**
|
|
3
|
+
* The signed-in workspace surface's People notes tools (#8514 D8) — the twins
|
|
4
|
+
* of `apps/api/src/routes/workspaces/people.ts`'s notes pair:
|
|
5
|
+
*
|
|
6
|
+
* get_person_notes GET /v1/workspaces/:workspaceId/people/:personId/notes
|
|
7
|
+
* set_person_notes PUT /v1/workspaces/:workspaceId/people/:personId/notes
|
|
8
|
+
*
|
|
9
|
+
* Each takes the route's params and body (the SDK has already parsed them
|
|
10
|
+
* against the catalog's input shape, which is the route's own schemas),
|
|
11
|
+
* forwards the caller's agent key through `@working-theory/api-client`, and
|
|
12
|
+
* returns the route's answer untouched. apps/api checks the key, the grant and
|
|
13
|
+
* the workspace; this file only carries the key, and never logs or returns it.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { people, type PersonRef } from "@working-theory/api-client";
|
|
17
|
+
import type { PersonNotesPutInput } from "@working-theory/validation";
|
|
18
|
+
|
|
19
|
+
export function ailkGetPersonNotes(key: string, input: PersonRef) {
|
|
20
|
+
return people.notes(key, input);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function ailkSetPersonNotes(key: string, input: PersonRef & PersonNotesPutInput) {
|
|
24
|
+
return people.setNotes(key, input);
|
|
25
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// oss: false
|
|
2
|
+
/**
|
|
3
|
+
* The signed-in workspace surface's proposals tools (#8516 D7) — the twins of
|
|
4
|
+
* `apps/api/src/routes/workspaces/pieces.ts`'s proposals pair:
|
|
5
|
+
*
|
|
6
|
+
* list_proposals GET /v1/workspaces/:workspaceId/people/:personId/proposals
|
|
7
|
+
* create_proposal POST /v1/workspaces/:workspaceId/people/:personId/proposals
|
|
8
|
+
*
|
|
9
|
+
* Each takes the route's params and body (the SDK has already parsed them
|
|
10
|
+
* against the catalog's input shape, which is the route's own schemas),
|
|
11
|
+
* forwards the caller's agent key through `@working-theory/api-client`, and
|
|
12
|
+
* returns the route's answer untouched. apps/api checks the key, the grant,
|
|
13
|
+
* the workspace and the money gate (an agent's price is `403
|
|
14
|
+
* proposal_needs_person`); this file only carries the key, and never logs or
|
|
15
|
+
* returns it.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { people, type PersonRef } from "@working-theory/api-client";
|
|
19
|
+
import type { ProposalCreateBody } from "@working-theory/validation";
|
|
20
|
+
|
|
21
|
+
export function ailkListProposals(key: string, input: PersonRef) {
|
|
22
|
+
return people.proposals(key, input);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function ailkCreateProposal(key: string, input: PersonRef & ProposalCreateBody) {
|
|
26
|
+
return people.createProposal(key, input);
|
|
27
|
+
}
|
|
@@ -95,6 +95,22 @@ const WORKSPACE_TOOL_ENTRIES: Record<
|
|
|
95
95
|
specifier: "./tools/inbox_applications.js",
|
|
96
96
|
exportName: "ailkDecideInboxApplication",
|
|
97
97
|
},
|
|
98
|
+
get_person_notes: {
|
|
99
|
+
specifier: "./tools/person_notes.js",
|
|
100
|
+
exportName: "ailkGetPersonNotes",
|
|
101
|
+
},
|
|
102
|
+
set_person_notes: {
|
|
103
|
+
specifier: "./tools/person_notes.js",
|
|
104
|
+
exportName: "ailkSetPersonNotes",
|
|
105
|
+
},
|
|
106
|
+
list_proposals: {
|
|
107
|
+
specifier: "./tools/proposals.js",
|
|
108
|
+
exportName: "ailkListProposals",
|
|
109
|
+
},
|
|
110
|
+
create_proposal: {
|
|
111
|
+
specifier: "./tools/proposals.js",
|
|
112
|
+
exportName: "ailkCreateProposal",
|
|
113
|
+
},
|
|
98
114
|
};
|
|
99
115
|
|
|
100
116
|
async function resolveWorkspaceHandler(
|
|
@@ -123,6 +123,31 @@ the site's contact email, never a form that would report a message as sent
|
|
|
123
123
|
when it reached nobody. A failed status read counts as accepting, so the
|
|
124
124
|
form renders and its submit reports honestly.
|
|
125
125
|
|
|
126
|
+
The form's fields come from the workspace's **Contact form magnet** (#8661):
|
|
127
|
+
every workspace is seeded one, live at slug `contact`, and the page reads it
|
|
128
|
+
on every request (`force-dynamic`) through the public flow read,
|
|
129
|
+
`flows.get("contact")` — the API resolves the workspace (the site alias
|
|
130
|
+
keyless, a spoke by its key), so the page names none. `contact-form-magnet.ts`
|
|
131
|
+
maps the config's first step onto the block's fields, terms statement and
|
|
132
|
+
submit label. The order is: a usable magnet (its first step has `name` and
|
|
133
|
+
`email` fields), else the page's `contact-form` section fields when it
|
|
134
|
+
declares any, else the built-in form, which is the seeded config itself. Any
|
|
135
|
+
failed read (absent, draft, a Free build with no flow route, a throw) is the
|
|
136
|
+
fallback, never an error. A magnet-driven post carries a hidden
|
|
137
|
+
`source: "flow:contact"`, so it is an Inbox submission of the Contact form
|
|
138
|
+
with every answer shown; a fallback post stays `web`. The terms statement
|
|
139
|
+
links Terms, Privacy Policy and Working Agreement from a page-built
|
|
140
|
+
`policies` map, and `captureLeadAction` refuses a post without
|
|
141
|
+
`consent=true` (the intake excepted) and keeps only the sources `web`,
|
|
142
|
+
`conversational-intake` and `flow:contact`.
|
|
143
|
+
|
|
144
|
+
A `contact-form` section in the `column` variant (the minimalism design's
|
|
145
|
+
Contact page) renders one centred heading, one line and the form, and the
|
|
146
|
+
page passes the block `look="column"` (the stacked look, CSS in
|
|
147
|
+
`packages/theme/src/tokens.css`) and `consentGate="on-submit"`: Send stays
|
|
148
|
+
enabled and an unticked submit shows the terms error. Every other look keeps
|
|
149
|
+
Send disabled until the box is ticked.
|
|
150
|
+
|
|
126
151
|
The chat-style intake is not mounted by default.
|
|
127
152
|
`ConversationalIntakeWrapper.tsx` remains for a site that prefers it: it hosts
|
|
128
153
|
`@working-theory/ui`'s `ConversationalIntake` block (contract in
|
|
@@ -158,4 +183,4 @@ A visitor's browser never calls the hub. Lead capture, newsletter signup, callba
|
|
|
158
183
|
- Don't reach into `apps/api` or `apps/mcp` from `apps/web`. They're peer apps — talk via HTTP/MCP.
|
|
159
184
|
- Don't commit images to `screenshots/` — only the `.gitkeep` is tracked.
|
|
160
185
|
|
|
161
|
-
Last updated: 2026-10-
|
|
186
|
+
Last updated: 2026-10-14 — § Contact page: the Contact form magnet read, the fallback order and the `column` look (#8661).
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for SignUpCard's sign-up agreements (#8438 D13, AC-3).
|
|
3
|
+
*
|
|
4
|
+
* Every submit — the password form and each OAuth provider — is gated by
|
|
5
|
+
* the site's `signUp` agreements, rendered as a `ConsentGroup` below
|
|
6
|
+
* `AuthCard`. Nothing happens on any entry point until every required
|
|
7
|
+
* agreement is ticked. The password path also mints the carried-agreement
|
|
8
|
+
* cookie (`rememberSignUpAgreementsAction`) before calling Better Auth.
|
|
9
|
+
*/
|
|
10
|
+
import { render, screen } from "@testing-library/react";
|
|
11
|
+
import userEvent from "@testing-library/user-event";
|
|
12
|
+
|
|
13
|
+
const mockSignUpEmail = jest.fn();
|
|
14
|
+
const mockSignInSocial = jest.fn();
|
|
15
|
+
jest.mock("better-auth/react", () => ({
|
|
16
|
+
__esModule: true,
|
|
17
|
+
createAuthClient: () => ({
|
|
18
|
+
signUp: { email: (...args: unknown[]) => mockSignUpEmail(...args) },
|
|
19
|
+
signIn: { social: (...args: unknown[]) => mockSignInSocial(...args) },
|
|
20
|
+
}),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
const mockRememberAgreements = jest.fn();
|
|
24
|
+
// eslint-disable-next-line no-restricted-syntax -- test-seam mock: the Server Action module is a "use server" boundary; the card only passes it through.
|
|
25
|
+
jest.mock("../sign-up.actions", () => ({
|
|
26
|
+
__esModule: true,
|
|
27
|
+
rememberSignUpAgreementsAction: (...args: unknown[]) => mockRememberAgreements(...args),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
import { SignUpCard, type SignUpCardProps } from "../sign-up-card";
|
|
31
|
+
|
|
32
|
+
const AGREEMENT = {
|
|
33
|
+
id: "account_terms",
|
|
34
|
+
required: true,
|
|
35
|
+
statement: "You agree to our Terms and Privacy Policy.",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function props(overrides: Partial<SignUpCardProps> = {}): SignUpCardProps {
|
|
39
|
+
return {
|
|
40
|
+
providers: ["google"],
|
|
41
|
+
method: "password",
|
|
42
|
+
next: "/account",
|
|
43
|
+
signInHref: "/sign-in",
|
|
44
|
+
labels: {
|
|
45
|
+
signUpHeading: "Create your account",
|
|
46
|
+
signUpSubhead: "",
|
|
47
|
+
signUpSubmit: "Sign up",
|
|
48
|
+
signUpFooterPrompt: "Already have an account?",
|
|
49
|
+
signUpFooterLink: "Sign in",
|
|
50
|
+
ssoPrefix: "Continue with",
|
|
51
|
+
google: "Google",
|
|
52
|
+
github: "GitHub",
|
|
53
|
+
email: "Email",
|
|
54
|
+
password: "Password",
|
|
55
|
+
orContinue: "or continue with",
|
|
56
|
+
},
|
|
57
|
+
notConfiguredSubhead: "Sign-up is not configured.",
|
|
58
|
+
errorLabels: { signUpFailed: "Sign-up failed.", providerFailed: "Provider failed." },
|
|
59
|
+
agreements: [AGREEMENT],
|
|
60
|
+
agreementPolicies: { terms: "/terms", privacy: "/privacy" },
|
|
61
|
+
agreementsLegend: "Agreements",
|
|
62
|
+
...overrides,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function fillAndSubmit(): Promise<void> {
|
|
67
|
+
const user = userEvent.setup();
|
|
68
|
+
await user.type(screen.getByTestId("auth-email"), "ada@example.com");
|
|
69
|
+
await user.type(screen.getByTestId("auth-password"), "correct horse battery staple");
|
|
70
|
+
await user.click(screen.getByTestId("auth-submit"));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function tick(purposeId: string): Promise<void> {
|
|
74
|
+
const user = userEvent.setup();
|
|
75
|
+
await user.click(screen.getByTestId(`consent-${purposeId}`));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
mockSignUpEmail.mockReset().mockResolvedValue({ data: { status: true }, error: null });
|
|
80
|
+
mockSignInSocial.mockReset().mockResolvedValue({ data: { status: true }, error: null });
|
|
81
|
+
mockRememberAgreements.mockReset().mockResolvedValue(undefined);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("the password submit is gated (AC-3)", () => {
|
|
85
|
+
it("renders the agreement, unticked, and the card marked disabled", () => {
|
|
86
|
+
render(<SignUpCard {...props()} />);
|
|
87
|
+
expect(screen.getByTestId("consent-account_terms")).toHaveAttribute("aria-checked", "false");
|
|
88
|
+
expect(screen.getByTestId("sign-up-card-gate")).toHaveAttribute("aria-disabled", "true");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("does nothing on submit while the agreement is unticked", async () => {
|
|
92
|
+
render(<SignUpCard {...props()} />);
|
|
93
|
+
await fillAndSubmit();
|
|
94
|
+
expect(mockSignUpEmail).not.toHaveBeenCalled();
|
|
95
|
+
expect(mockRememberAgreements).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("mints the carried-agreement cookie, then signs up, once every required agreement is ticked", async () => {
|
|
99
|
+
render(<SignUpCard {...props()} />);
|
|
100
|
+
await tick("account_terms");
|
|
101
|
+
expect(screen.getByTestId("sign-up-card-gate")).toHaveAttribute("aria-disabled", "false");
|
|
102
|
+
await fillAndSubmit();
|
|
103
|
+
expect(mockRememberAgreements).toHaveBeenCalledWith("ada@example.com", ["account_terms"]);
|
|
104
|
+
expect(mockSignUpEmail).toHaveBeenCalledWith({
|
|
105
|
+
email: "ada@example.com",
|
|
106
|
+
password: "correct horse battery staple",
|
|
107
|
+
name: "ada@example.com",
|
|
108
|
+
callbackURL: "/account",
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("re-disables after unticking", async () => {
|
|
113
|
+
render(<SignUpCard {...props()} />);
|
|
114
|
+
await tick("account_terms");
|
|
115
|
+
await tick("account_terms");
|
|
116
|
+
expect(screen.getByTestId("sign-up-card-gate")).toHaveAttribute("aria-disabled", "true");
|
|
117
|
+
await fillAndSubmit();
|
|
118
|
+
expect(mockSignUpEmail).not.toHaveBeenCalled();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("with two required agreements, both must be ticked", async () => {
|
|
122
|
+
render(
|
|
123
|
+
<SignUpCard
|
|
124
|
+
{...props({
|
|
125
|
+
agreements: [
|
|
126
|
+
AGREEMENT,
|
|
127
|
+
{ id: "service_terms", required: true, statement: "You agree to being contacted." },
|
|
128
|
+
],
|
|
129
|
+
})}
|
|
130
|
+
/>,
|
|
131
|
+
);
|
|
132
|
+
await tick("account_terms");
|
|
133
|
+
await fillAndSubmit();
|
|
134
|
+
expect(mockSignUpEmail).not.toHaveBeenCalled();
|
|
135
|
+
await tick("service_terms");
|
|
136
|
+
await fillAndSubmit();
|
|
137
|
+
expect(mockSignUpEmail).toHaveBeenCalledTimes(1);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("a failed cookie mint still submits — convenience only", async () => {
|
|
141
|
+
mockRememberAgreements.mockRejectedValue(new Error("no secret"));
|
|
142
|
+
render(<SignUpCard {...props()} />);
|
|
143
|
+
await tick("account_terms");
|
|
144
|
+
await fillAndSubmit();
|
|
145
|
+
expect(mockSignUpEmail).toHaveBeenCalledTimes(1);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("provider buttons are gated too (AC-3)", () => {
|
|
150
|
+
it("a click on Google does nothing while unticked", async () => {
|
|
151
|
+
render(<SignUpCard {...props({ method: "sso", providers: ["google"] })} />);
|
|
152
|
+
const user = userEvent.setup();
|
|
153
|
+
await user.click(screen.getByTestId("auth-sso-google"));
|
|
154
|
+
expect(mockSignInSocial).not.toHaveBeenCalled();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("launches the provider once ticked, with no cookie mint", async () => {
|
|
158
|
+
render(<SignUpCard {...props({ method: "sso", providers: ["google"] })} />);
|
|
159
|
+
await tick("account_terms");
|
|
160
|
+
const user = userEvent.setup();
|
|
161
|
+
await user.click(screen.getByTestId("auth-sso-google"));
|
|
162
|
+
expect(mockSignInSocial).toHaveBeenCalledWith({ provider: "google", callbackURL: "/account" });
|
|
163
|
+
expect(mockRememberAgreements).not.toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
|
|
16
16
|
import { localeHref } from "@working-theory/ui";
|
|
17
17
|
import { AuthCard, type AuthCardLabels } from "@working-theory/ui/blocks";
|
|
18
|
+
import {
|
|
19
|
+
resolveConsentStatement,
|
|
20
|
+
SITE_CONSENT_CONFIG_DEFAULTS,
|
|
21
|
+
signUpAgreements,
|
|
22
|
+
} from "@working-theory/validation";
|
|
18
23
|
import type { Metadata } from "next";
|
|
19
24
|
import { getTranslations } from "next-intl/server";
|
|
20
25
|
import type { ReactElement } from "react";
|
|
@@ -79,6 +84,17 @@ export default async function SignUpPage({
|
|
|
79
84
|
orContinue: t("orContinue"),
|
|
80
85
|
};
|
|
81
86
|
|
|
87
|
+
// #8438 D13 — the site's sign-up agreements, resolved to this locale. No
|
|
88
|
+
// per-site registry row is wired yet (C1), so this always reads the
|
|
89
|
+
// platform default — the same registry the member Privacy tab and the
|
|
90
|
+
// account-settings write both read from.
|
|
91
|
+
const config = SITE_CONSENT_CONFIG_DEFAULTS;
|
|
92
|
+
const agreements = signUpAgreements(config).map((purpose) => ({
|
|
93
|
+
id: purpose.id,
|
|
94
|
+
required: purpose.required,
|
|
95
|
+
statement: resolveConsentStatement(purpose, locale).statement,
|
|
96
|
+
}));
|
|
97
|
+
|
|
82
98
|
return (
|
|
83
99
|
<SignUpCard
|
|
84
100
|
providers={providers}
|
|
@@ -91,6 +107,9 @@ export default async function SignUpPage({
|
|
|
91
107
|
signUpFailed: t("signUpFailed"),
|
|
92
108
|
providerFailed: t("providerFailed"),
|
|
93
109
|
}}
|
|
110
|
+
agreements={agreements}
|
|
111
|
+
agreementPolicies={config.policies}
|
|
112
|
+
agreementsLegend={t("signUpAgreementsLegend")}
|
|
94
113
|
/>
|
|
95
114
|
);
|
|
96
115
|
}
|