create-ailk 0.15.0 → 0.16.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.
Files changed (127) hide show
  1. package/README.md +124 -0
  2. package/dist/add-module.js +52 -7
  3. package/dist/add-route.js +8 -0
  4. package/dist/adopt.d.ts +74 -0
  5. package/dist/adopt.js +314 -0
  6. package/dist/cli.js +82 -2
  7. package/dist/derive-architecture.d.ts +22 -0
  8. package/dist/derive-architecture.js +58 -1
  9. package/dist/lib/declared-env.d.ts +142 -0
  10. package/dist/lib/declared-env.js +985 -0
  11. package/dist/lib/env-catalog.d.ts +17 -0
  12. package/dist/lib/env-catalog.js +234 -0
  13. package/dist/lib/extract-module-bundle.d.ts +9 -0
  14. package/dist/lib/extract-module-bundle.js +18 -1
  15. package/dist/lib/module-tier.d.ts +2 -0
  16. package/dist/lib/module-tier.js +4 -0
  17. package/dist/module-architecture.js +76 -0
  18. package/dist/parse-args.d.ts +5 -0
  19. package/dist/parse-args.js +17 -1
  20. package/dist/programmatic.d.ts +8 -0
  21. package/dist/programmatic.js +78 -9
  22. package/dist/seed-flows.d.ts +65 -0
  23. package/dist/seed-flows.js +350 -0
  24. package/dist/site-manifest.d.ts +43 -0
  25. package/dist/site-manifest.js +98 -3
  26. package/dist/site-roster.d.ts +13 -0
  27. package/dist/site-roster.js +22 -8
  28. package/dist/spoke-checkout.d.ts +42 -0
  29. package/dist/spoke-checkout.js +359 -0
  30. package/dist/surfaces.js +7 -0
  31. package/dist/sync-routes.js +23 -8
  32. package/dist/topology.d.ts +124 -9
  33. package/dist/topology.js +449 -40
  34. package/package.json +3 -3
  35. package/templates/.env.example +10 -0
  36. package/templates/apps/api/CLAUDE.md +19 -4
  37. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +95 -16
  38. package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +122 -0
  39. package/templates/apps/api/src/__tests__/support/hub-sign-in-fake-db.ts +362 -0
  40. package/templates/apps/api/src/bin/spokes.ts +62 -1
  41. package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +75 -0
  42. package/templates/apps/api/src/config/index.ts +35 -0
  43. package/templates/apps/api/src/lib/__mocks__/prisma.ts +89 -0
  44. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +66 -5
  45. package/templates/apps/api/src/lib/__tests__/hub-sign-in-store.test.ts +209 -0
  46. package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +20 -0
  47. package/templates/apps/api/src/lib/deliverable-asset-store.ts +24 -5
  48. package/templates/apps/api/src/lib/hub-sign-in-store.ts +419 -0
  49. package/templates/apps/api/src/lib/spoke-store.ts +25 -3
  50. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +310 -0
  51. package/templates/apps/api/src/middleware/auth.ts +126 -0
  52. package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +483 -0
  53. package/templates/apps/api/src/routes/auth/hub-sign-in.ts +201 -0
  54. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +7 -0
  55. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +105 -0
  56. package/templates/apps/api/src/routes/webhooks/stripe.ts +72 -1
  57. package/templates/apps/api/src/server.ts +105 -0
  58. package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +4 -4
  59. package/templates/apps/api/src/services/__tests__/hub-sign-in.test.ts +854 -0
  60. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +293 -0
  61. package/templates/apps/api/src/services/__tests__/person.test.ts +553 -0
  62. package/templates/apps/api/src/services/hub-sign-in.ts +471 -0
  63. package/templates/apps/api/src/services/lead-routing.ts +131 -2
  64. package/templates/apps/api/src/services/person.ts +541 -0
  65. package/templates/apps/web/.env.example +58 -5
  66. package/templates/apps/web/app/[locale]/(auth)/sign-in/next-path.ts +0 -0
  67. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +206 -0
  68. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +256 -3
  69. package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +5 -8
  70. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +205 -0
  71. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +82 -0
  72. package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +98 -29
  73. package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +6 -3
  74. package/templates/apps/web/app/[locale]/(authed)/layout.tsx +19 -101
  75. package/templates/apps/web/app/[locale]/(authed)/signed-out-gate.generated.tsx +90 -0
  76. package/templates/apps/web/app/[locale]/layout.tsx +28 -15
  77. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +295 -0
  78. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +156 -0
  79. package/templates/apps/web/app/api/hub-session/__tests__/routes.test.ts +341 -0
  80. package/templates/apps/web/app/api/hub-session/callback/route.ts +168 -0
  81. package/templates/apps/web/app/api/hub-session/sign-in/route.ts +76 -0
  82. package/templates/apps/web/app/api/hub-session/sign-out/route.ts +63 -0
  83. package/templates/apps/web/app/openapi.json/__tests__/route.test.ts +15 -0
  84. package/templates/apps/web/app/openapi.json/route.ts +3 -6
  85. package/templates/apps/web/auth.ts +29 -2
  86. package/templates/apps/web/components/sign-out-trigger.tsx +20 -1
  87. package/templates/apps/web/components/signed-in-nav.tsx +17 -3
  88. package/templates/apps/web/lib/__tests__/account-config.test.ts +5 -2
  89. package/templates/apps/web/lib/__tests__/listings-config.test.ts +83 -0
  90. package/templates/apps/web/lib/__tests__/listings.test.tsx +49 -0
  91. package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +49 -1
  92. package/templates/apps/web/lib/__tests__/relay.test.ts +27 -0
  93. package/templates/apps/web/lib/account-config.ts +130 -1
  94. package/templates/apps/web/lib/api-origin.generated.ts +11 -0
  95. package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +262 -0
  96. package/templates/apps/web/lib/hub-session/config.ts +104 -0
  97. package/templates/apps/web/lib/hub-session/index.ts +23 -0
  98. package/templates/apps/web/lib/hub-session/seal.ts +82 -0
  99. package/templates/apps/web/lib/hub-session/session.ts +180 -0
  100. package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +79 -0
  101. package/templates/apps/web/lib/listings-config.ts +31 -0
  102. package/templates/apps/web/lib/listings.tsx +7 -3
  103. package/templates/apps/web/lib/meeting-slugs.ts +11 -1
  104. package/templates/apps/web/lib/next-path.ts +35 -0
  105. package/templates/apps/web/lib/relay.ts +3 -1
  106. package/templates/apps/web/lib/server-api.ts +11 -1
  107. package/templates/apps/web/lib/topology.generated.ts +15 -5
  108. package/templates/apps/web/messages/en.json +32 -3
  109. package/templates/database/CHANGELOG.md +26 -0
  110. package/templates/database/__tests__/agents.db.test.ts +25 -5
  111. package/templates/database/__tests__/fixtures/person-backfill.json +264 -0
  112. package/templates/database/__tests__/member-area.db.test.ts +245 -0
  113. package/templates/database/__tests__/person.db.test.ts +584 -0
  114. package/templates/database/auth/schema.prisma +3 -2
  115. package/templates/database/config/schema.prisma +13 -0
  116. package/templates/database/inbox/schema.prisma +98 -0
  117. package/templates/database/migrations/20261010120000_person/migration.sql +106 -0
  118. package/templates/database/migrations/20261010121000_person_backfill/migration.sql +132 -0
  119. package/templates/database/migrations/20261010130000_agent_grants_people_read/migration.sql +20 -0
  120. package/templates/database/migrations/20261011120000_member_area/migration.sql +158 -0
  121. package/templates/database/migrations/20261011130000_agent_grants_member_pieces_manage/migration.sql +21 -0
  122. package/templates/database/package.json +1 -1
  123. package/templates/database/scripts/person-backfill.sql +132 -0
  124. package/templates/database/tenancy.ts +53 -0
  125. package/templates/tools/check-declared-env.ts +37 -0
  126. package/templates/tools/declared-env.ts +1163 -0
  127. package/versions.json +35 -35
@@ -0,0 +1,79 @@
1
+ /**
2
+ * HubSignInGate — the signed-out account gate of a spoke that signs in
3
+ * through its hub (#7894 D10).
4
+ *
5
+ * A Server Component, rendered by the `(authed)` layout through the generated
6
+ * gate seam (`signed-out-gate.generated.tsx`, its `signIn: "hub"` render). It
7
+ * offers ONE action: a link to this site's own `/api/hub-session/sign-in`,
8
+ * carrying the requested path as `next`, which starts the hub sign-in. The
9
+ * link is a plain anchor (the `Link` primitive), so nothing prefetches the
10
+ * route and starts a flow the visitor did not ask for.
11
+ *
12
+ * Not configured (`hubSessionConfig()` is `null`: a client id, secret or
13
+ * session secret missing), it renders the account area's existing "sign-in
14
+ * isn't available yet" state, whose one action leads back to the site.
15
+ */
16
+
17
+ import { localeHref } from "@working-theory/ui";
18
+ import { Button, Heading, Link, Text } from "@working-theory/ui/primitives";
19
+ import { getTranslations } from "next-intl/server";
20
+ import type { ReactElement } from "react";
21
+
22
+ import { hubSessionConfig } from "./config";
23
+ import { HUB_SESSION_FALLBACK_PATH } from "./session";
24
+
25
+ /** The route that starts the hub sign-in. */
26
+ const HUB_SIGN_IN_ROUTE = "/api/hub-session/sign-in";
27
+
28
+ export interface HubSignInGateProps {
29
+ locale: string;
30
+ /** The incoming request's headers; `x-pathname` is the page to return to. */
31
+ requestHeaders: Headers;
32
+ }
33
+
34
+ export async function HubSignInGate({
35
+ locale,
36
+ requestHeaders,
37
+ }: HubSignInGateProps): Promise<ReactElement> {
38
+ const t = await getTranslations({ locale, namespace: "account.gate" });
39
+ const configured = hubSessionConfig() !== null;
40
+ const next =
41
+ requestHeaders.get("x-pathname") ??
42
+ localeHref(locale, HUB_SESSION_FALLBACK_PATH);
43
+
44
+ return (
45
+ <div
46
+ className="flex min-h-screen flex-col items-center justify-center gap-4 py-section text-center"
47
+ data-testid="account-sign-in-gate"
48
+ >
49
+ <Heading level={1} visual={4}>
50
+ {configured ? t("hubHeadline") : t("emptyHeadline")}
51
+ </Heading>
52
+ <Text variant="body-sm" tone="muted" className="max-w-xs">
53
+ {configured ? t("hubDescription") : t("emptyDescription")}
54
+ </Text>
55
+ {configured ? (
56
+ // The primary action: 44px on a phone, 36px from md up.
57
+ <Button asChild className="max-md:min-h-11">
58
+ <Link
59
+ href={`${HUB_SIGN_IN_ROUTE}?next=${encodeURIComponent(next)}`}
60
+ variant="bare"
61
+ data-testid="account-hub-sign-in"
62
+ >
63
+ {t("hubSignIn")}
64
+ </Link>
65
+ </Button>
66
+ ) : (
67
+ <Button asChild variant="secondary" className="max-md:min-h-11">
68
+ <Link
69
+ href={localeHref(locale, "/")}
70
+ variant="bare"
71
+ data-testid="account-sign-in-gate-home"
72
+ >
73
+ {t("homeLink")}
74
+ </Link>
75
+ </Button>
76
+ )}
77
+ </div>
78
+ );
79
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The listing site key — the ONE reader (#7872 D8).
3
+ *
4
+ * The listicle directory (`./listings.tsx`, #7521) switches on this key:
5
+ * declared, it renders; absent, it is off and makes no read. Every reader of
6
+ * the key goes through here, so two copies cannot drift.
7
+ *
8
+ * 1. `NEXT_PUBLIC_AILK_LISTING_SITE_KEY` — the name the spoke roadmap fixes
9
+ * (§7.3). The value names a site and grants nothing (#5048 D2), so it is
10
+ * public by design, and a client consumer gets it inlined at build.
11
+ * 2. `AILK_LISTING_PUBLIC_KEY` — the server-only name every site
12
+ * configured since create-ailk 0.12.0 already sets, kept as a fallback
13
+ * so an upgrade does not switch a working directory off.
14
+ *
15
+ * An empty or whitespace value counts as unset. Both reads use dot access, so
16
+ * Next inlines the public name at build: changing or rotating it needs a
17
+ * rebuild, and while it is set a runtime change to the fallback has no effect.
18
+ */
19
+
20
+ /** The site's listing key, trimmed, or `undefined` when neither name holds one. */
21
+ export function listingSiteKey(): string | undefined {
22
+ const declared = process.env.NEXT_PUBLIC_AILK_LISTING_SITE_KEY?.trim();
23
+ if (declared) return declared;
24
+ const fallback = process.env.AILK_LISTING_PUBLIC_KEY?.trim();
25
+ return fallback ? fallback : undefined;
26
+ }
27
+
28
+ /** Whether the listicle directory is switched on for this site. */
29
+ export function listingsEnabled(): boolean {
30
+ return listingSiteKey() !== undefined;
31
+ }
@@ -20,8 +20,11 @@
20
20
  * BASE TREE, and inert without two things (D7). It ships in every scaffold,
21
21
  * Free included, and is never slice-owned — the layout imports it, so owning
22
22
  * it would strip every page from a keyless scaffold. Each member answers
23
- * `null` with NO read when `AILK_LISTING_PUBLIC_KEY` is unset or the paid
24
- * `@working-theory/ui-signal-collectors` package is absent. The package is
23
+ * `null` with NO read when the site's listing key is unset or the paid
24
+ * `@working-theory/ui-signal-collectors` package is absent. The key is the
25
+ * switch: `./listings-config.ts` is its one reader
26
+ * (`NEXT_PUBLIC_AILK_LISTING_SITE_KEY`, falling back to
27
+ * `AILK_LISTING_PUBLIC_KEY`, #7872 D8). The package is
25
28
  * reached the way `src/lib/commerce-sections.ts` reaches its own: through
26
29
  * `loadUiSignalCollectors()` in the generated paid-sections registry (#7536),
27
30
  * a bundled `import()` where the site carries the package and a stub that
@@ -64,6 +67,7 @@ import { cache, type ComponentType, type ReactElement, type ReactNode } from "re
64
67
 
65
68
  import { resolveBaseUrl } from "../src/lib/routes";
66
69
 
70
+ import { listingSiteKey } from "./listings-config";
67
71
  import { loadUiSignalCollectors } from "./paid-sections.generated";
68
72
 
69
73
  // ── The paid sections, through the generated registry ──────────────────────
@@ -252,7 +256,7 @@ export function entryJsonLd(entry: ListingEntryPublic, entryType: string, url: s
252
256
  // ── The extension ───────────────────────────────────────────────────────────
253
257
 
254
258
  export function createListingExtension(deps: ListingExtensionDeps = {}): ListingExtension {
255
- const siteKey = deps.siteKey ?? (() => process.env["AILK_LISTING_PUBLIC_KEY"]);
259
+ const siteKey = deps.siteKey ?? listingSiteKey;
256
260
  const pathname =
257
261
  deps.pathname ?? (async () => (await headers()).get("x-pathname"));
258
262
  const locales = deps.locales ?? (() => readLocales());
@@ -17,10 +17,18 @@
17
17
  * A duplicated inline call in both pages is how the two would drift, and
18
18
  * it is also how one of them would forget the fail-closed arm — this is
19
19
  * the ONE place that reasoning lives (D5).
20
+ *
21
+ * ON A SPOKE (#7872 D4), `null` means `[]`: the `SCHEDULING_CAL_LINK`
22
+ * fallback is not consulted. The hub books a spoke's slug only when that
23
+ * spoke's workspace has stored it (`spokeMayBook`), so a fallback slug would
24
+ * advertise a meeting whose every booking fails with `event_not_found`. The
25
+ * other arms are the same on both topologies.
20
26
  */
21
27
  import { schedule } from "@working-theory/api-client";
22
28
  import { MeetingSlugSchema } from "@working-theory/site-config/schema";
23
29
 
30
+ import { TOPOLOGY } from "./topology.generated";
31
+
24
32
  /**
25
33
  * The D4 row-1 fallback: the slug half of the env-configured booker's own
26
34
  * calLink (`"<owner>/<slug>"`), or `[]` when the env var is unset. This is
@@ -49,5 +57,7 @@ function derivedFallbackSlugs(): string[] {
49
57
  export async function enabledMeetingSlugs(): Promise<string[]> {
50
58
  const res = await schedule.meetingTypes();
51
59
  if (!res.ok) return [];
52
- return res.body.slugs ?? derivedFallbackSlugs();
60
+ const { slugs } = res.body;
61
+ if (slugs) return slugs;
62
+ return TOPOLOGY === "spoke" ? [] : derivedFallbackSlugs();
53
63
  }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `?next=` sanitizer for the /sign-in page (#4984) and the hub-session
3
+ * callback (#7894 D7 — moved here from `app/[locale]/(auth)/sign-in/`, which
4
+ * re-exports it, so a spoke that drops the `(auth)` group keeps it).
5
+ *
6
+ * The post-sign-in `callbackURL` handed to Better Auth is caller-controlled
7
+ * via the `next` search param, so it must be a SAME-ORIGIN, SAME-APP path —
8
+ * never a foreign origin (open redirect), a protocol-relative URL, a
9
+ * backslash trick browsers normalise to `//`, or a control character that
10
+ * could split a header. Anything that fails falls back to `fallback`.
11
+ */
12
+
13
+ // U+0000–U+001F and U+007F — the header-splitting / URL-mangling range.
14
+ // eslint-disable-next-line no-control-regex -- the control range is the thing being rejected
15
+ const CONTROL_CHARS = /[\x00-\x1f\x7f]/;
16
+
17
+ /**
18
+ * Returns `raw` when it is a safe in-app path, else `fallback`.
19
+ *
20
+ * Accepts: a string starting with a single "/" (e.g. `/account`,
21
+ * `/en/account?tab=x`, `/waitlist#a`).
22
+ * Rejects: non-strings (incl. Next's `string[]` for a repeated param), "",
23
+ * `//host` (protocol-relative), `/\host` (backslash normalised to `//`),
24
+ * any backslash anywhere, whitespace, control characters, and anything not
25
+ * starting with "/" (`https://…`, `host.tld`).
26
+ */
27
+ export function resolveNextPath(raw: unknown, fallback: string): string {
28
+ if (typeof raw !== "string") return fallback;
29
+ if (!raw.startsWith("/")) return fallback;
30
+ if (raw.startsWith("//")) return fallback;
31
+ if (raw.includes("\\")) return fallback;
32
+ if (/\s/.test(raw)) return fallback;
33
+ if (CONTROL_CHARS.test(raw)) return fallback;
34
+ return raw;
35
+ }
@@ -42,6 +42,8 @@ import { isIP } from "node:net";
42
42
 
43
43
  import { relayToApi, visitorForwardHeaders } from "@working-theory/api-client";
44
44
 
45
+ import { browserApiOrigin } from "./api-origin.generated";
46
+
45
47
  type RelayMethod = "GET" | "POST";
46
48
 
47
49
  /** One relayed (method, path) pair. */
@@ -287,7 +289,7 @@ function hubConfigured(): boolean {
287
289
  if (process.env.NODE_ENV !== "production") return true;
288
290
  return Boolean(
289
291
  process.env.AILK_API_URL ||
290
- process.env.NEXT_PUBLIC_AILK_API_URL ||
292
+ browserApiOrigin() ||
291
293
  process.env.NEXT_PUBLIC_API_URL,
292
294
  );
293
295
  }
@@ -24,6 +24,9 @@
24
24
 
25
25
  import { cookies, headers } from "next/headers";
26
26
 
27
+ import { hubSessionInit } from "./hub-session/session";
28
+ import { SIGN_IN_PROVIDER } from "./topology.generated";
29
+
27
30
  /**
28
31
  * Structural mirror of `@working-theory/api-client`'s `ApiResult<T>`
29
32
  * (`packages/api-client/src/http.ts`) — not imported, because that type is
@@ -75,10 +78,17 @@ interface ErrorBody {
75
78
  * pass the check exactly as a browser-originated one would. RSC `GET`s are
76
79
  * unaffected: `isOriginAllowed` short-circuits `true` for safe methods before
77
80
  * it ever inspects the header.
81
+ *
82
+ * On a spoke that signs in through its hub (`SIGN_IN_PROVIDER === "hub"`,
83
+ * #7894 D12) this is the ONE request-init re-target: it returns the hub
84
+ * credential as `Authorization: Bearer` (`hubSessionInit`), and no header at
85
+ * all signed out — the hub has no cookie session for this site's visitor.
86
+ * Every caller passes the result straight to the api-client unchanged.
78
87
  */
79
88
  export async function forwardedCookieInit(): Promise<{
80
- headers: { cookie: string; origin?: string };
89
+ headers: Record<string, string>;
81
90
  }> {
91
+ if (SIGN_IN_PROVIDER === "hub") return hubSessionInit();
82
92
  const cookieHeader = (await cookies()).toString();
83
93
  const origin = (await headers()).get("origin");
84
94
  return { headers: { cookie: cookieHeader, ...(origin ? { origin } : {}) } };
@@ -1,19 +1,29 @@
1
1
  // topology.generated.ts — how this site reads a session and registers its stores.
2
2
  //
3
3
  // GENERATED by create-ailk for this site's topology (#7426). Do not edit:
4
- // every scaffold rewrites it. A hub reads the session through Better Auth;
5
- // a spoke (`--topology spoke`) has no sign-in and no database of its own,
6
- // so its render reads no session and imports nothing.
7
- import { auth } from "../auth";
4
+ // every scaffold rewrites it. A hub reads the session through Better Auth.
5
+ // A spoke (`--topology spoke`) has no database of its own: by default it
6
+ // has no sign-in, so its render reads no session and imports nothing; with
7
+ // `--hub-sign-in` (#7894) it reads the sealed hub-session cookie.
8
8
 
9
9
  export const TOPOLOGY: "hub" | "spoke" = "hub";
10
10
  export const SIGN_IN_ENABLED: boolean = true;
11
+ export const SIGN_IN_PROVIDER: "site" | "hub" | "none" = "site";
12
+ export const SIGN_IN_PATH: string | null = "/sign-in";
13
+ export const SIGN_UP_PATH: string | null = "/sign-up";
14
+ export const SIGN_OUT_ENDPOINT: string | null = null;
11
15
 
12
16
  export type SiteSession = {
13
- user: { name?: string | null; email: string };
17
+ user: {
18
+ name?: string | null;
19
+ email: string;
20
+ emailVerified: boolean;
21
+ createdAt?: Date | string | null;
22
+ };
14
23
  } | null;
15
24
 
16
25
  export async function readSession(headers: Headers): Promise<SiteSession> {
26
+ const { auth } = await import("../auth");
17
27
  return auth.api.getSession({ headers });
18
28
  }
19
29
 
@@ -303,6 +303,16 @@
303
303
  "ctaLabel": "Back to services"
304
304
  }
305
305
  },
306
+ "checkoutThankYou": {
307
+ "title": "Thank you",
308
+ "body": "Your payment went through. We'll email you a receipt and the next steps."
309
+ },
310
+ "checkoutUnavailable": {
311
+ "title": "Checkout isn't available right now",
312
+ "body": "Nothing was charged. Please try again in a few minutes.",
313
+ "ctaHeadline": "Need a hand?",
314
+ "ctaLabel": "Contact us"
315
+ },
306
316
  "resources": {
307
317
  "heading": "Resources",
308
318
  "emptyState": "Resources listing coming soon."
@@ -649,7 +659,8 @@
649
659
  "unsaved": "Unsaved changes",
650
660
  "clean": "No unsaved changes",
651
661
  "nameRequired": "Enter your name.",
652
- "saveError": "We couldn't save your name. Try again."
662
+ "saveError": "We couldn't save your name. Try again.",
663
+ "manageOnHub": "Manage your account on our main site"
653
664
  },
654
665
  "security": {
655
666
  "title": "Security",
@@ -660,7 +671,8 @@
660
671
  "magicLink": "Email sign-in link",
661
672
  "password": "Password",
662
673
  "available": "Available",
663
- "none": "No sign-in method is configured."
674
+ "none": "No sign-in method is configured.",
675
+ "viaHub": "You sign in through our main site. Your sign-in methods are managed there."
664
676
  },
665
677
  "notifications": {
666
678
  "title": "Notifications",
@@ -696,7 +708,10 @@
696
708
  "emptyHeadline": "Sign-in isn't available yet",
697
709
  "emptyDescription": "This site hasn't turned on sign-in for members yet. Check back soon, or contact the site owner.",
698
710
  "signInLink": "Go to the sign-in page",
699
- "homeLink": "Back to the site"
711
+ "homeLink": "Back to the site",
712
+ "hubHeadline": "Sign in to your account",
713
+ "hubDescription": "You sign in on our main site, then come straight back here.",
714
+ "hubSignIn": "Sign in"
700
715
  },
701
716
  "profile": {
702
717
  "email": "Email",
@@ -718,6 +733,20 @@
718
733
  "period": "Period",
719
734
  "date": "Date"
720
735
  },
736
+ "pieces": {
737
+ "heading": "My pieces",
738
+ "type": "Piece",
739
+ "stage": "Stage",
740
+ "createdAt": "Started",
741
+ "updatedAt": "Updated",
742
+ "needsClient": "This piece is waiting on you."
743
+ },
744
+ "messages": {
745
+ "heading": "My messages",
746
+ "sentAt": "Sent",
747
+ "file": "File",
748
+ "download": "Download {filename}"
749
+ },
721
750
  "subscription": {
722
751
  "headline": "Your payment didn't go through",
723
752
  "action": "Update payment method"
@@ -1,5 +1,31 @@
1
1
  # @working-theory/database
2
2
 
3
+ ## 0.38.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#8037](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/8037) [`89eba8c`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/89eba8c2bab16766a68284f52c872aae86f59fde) Thanks [@samuelhenry](https://github.com/samuelhenry)! - The member area contract ([#7877](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/7877)): a client's pieces, plan and messages.
8
+
9
+ - `@working-theory/database` — `Piece` and `Interaction` on Person (a composite `(person_id, workspace_id)` foreign key to `people`) and `WorkspacePieceStages`, a workspace's declared piece stages; migration `20261011120000_member_area`.
10
+ - `@working-theory/ids` — the `piece_`, `intx_` and `pstages_` prefixes; `zPieceId` and `zInteractionId`.
11
+ - `@working-theory/validation` — the member-area wire schemas (`pieces.ts`, `member.ts`), the Person timeline's `message` kind, the `people-write` permission (owner, admin; not agent-grantable) and the attachment helpers (`ATTACHABLE_CONTENT_TYPES`, `attachmentFilename`).
12
+ - `@working-theory/api-client` — `member.pieces`, `member.messages` and `member.messageFile` (with `getRaw`), the reads a spoke's server makes for its signed-in client. A spoke that re-serves a file sends its five response headers unchanged.
13
+ - `create-ailk` — the member-area files are co-owned by the `workspace` and `multi-tenant` slices.
14
+
15
+ - [#8049](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/8049) [`6a2a120`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/6a2a120765c423303716d5e3ebf216c9c10e118b) Thanks [@samuelhenry](https://github.com/samuelhenry)! - An agent key may be granted `member-pieces-manage` ([#7878](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/7878)).
16
+
17
+ `@working-theory/validation`: the `member-pieces-manage` permission (owner, admin), the seventh entry of `AGENT_GRANTABLE_PERMISSIONS`. The member area's operator writes — a client's pieces, plan and outbound messages — check it through `allows`, so an agent granted it keeps them in its one workspace; `people-write` stays never-grantable. `@working-theory/database`: the `agents_grants_allowed` CHECK widened to admit it (additive).
18
+
19
+ - [#8004](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/8004) [`d9ba65f`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/d9ba65f6334681b17bd77b64bb7ca9e069b1980f) Thanks [@claudio-planck](https://github.com/claudio-planck)! - Person — the per-workspace client record ([#852](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/852)).
20
+
21
+ `@working-theory/database`: a `Person` model (one client per normalized email per workspace), `personId` on `Lead` and `Appointment`, the schema and set-based backfill migrations, the post-cutover re-run script `scripts/person-backfill.sql`, the agent-grant CHECK widened to admit `people-read`, and the timeline's email-fallback SQL constants on the `tenancy` subpath. `@working-theory/ids`: the `person_` prefix and `zPersonId`. `@working-theory/validation`: the `people-read` permission (owner, admin; agent-grantable) and the People wire schemas. `@working-theory/api-client`: `people.list` and `people.get` for an agent key granted `people-read`.
22
+
23
+ ### Patch Changes
24
+
25
+ - Updated dependencies [[`89eba8c`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/89eba8c2bab16766a68284f52c872aae86f59fde), [`6a2a120`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/6a2a120765c423303716d5e3ebf216c9c10e118b), [`6d38b1a`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/6d38b1a5362b36e56d6bce79fdf96c878020cb18), [`d9ba65f`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/d9ba65f6334681b17bd77b64bb7ca9e069b1980f)]:
26
+ - @working-theory/validation@0.38.0
27
+ - @working-theory/ids@0.11.0
28
+
3
29
  ## 0.37.0
4
30
 
5
31
  ### Minor Changes
@@ -18,11 +18,12 @@
18
18
  * 4. Deleting the membership of the person last shown the key sets
19
19
  * `key_issued_to_membership_id` null and keeps the agent.
20
20
  * 5. Deleting the creator's user row leaves the agent (no relation).
21
- * 6. `grants` (#7864 D2, AC-2) defaults to empty, takes any of the five
22
- * grantable permissions, and the `agents_grants_allowed` CHECK refuses
23
- * anything else.
21
+ * 6. `grants` (#7864 D2, AC-2) defaults to empty, takes any of the seven
22
+ * grantable permissions (`people-read` since #852 D9,
23
+ * `member-pieces-manage` since #7878), and the `agents_grants_allowed`
24
+ * CHECK refuses anything else.
24
25
  *
25
- * One case needs no database and always runs: the migration's CHECK names
26
+ * One case needs no database and always runs: the latest migration's CHECK names
26
27
  * exactly `@working-theory/validation`'s `AGENT_GRANTABLE_PERMISSIONS`, so the
27
28
  * SQL and the TypeScript spelling of the list cannot drift (SEC-7864-G).
28
29
  */
@@ -34,11 +35,12 @@ import { Client } from "pg";
34
35
 
35
36
  const describeDb = process.env.DATABASE_URL ? describe : describe.skip;
36
37
 
38
+ // #7878 — the CHECK as it stands now: the latest migration that re-adds it.
37
39
  const GRANTS_MIGRATION = join(
38
40
  __dirname,
39
41
  "..",
40
42
  "migrations",
41
- "20261009120000_agent_grants",
43
+ "20261011130000_agent_grants_member_pieces_manage",
42
44
  "migration.sql",
43
45
  );
44
46
 
@@ -225,6 +227,24 @@ describeDb("the agents table (#3591, live DB)", () => {
225
227
  expect(await grantsOf(id)).toEqual(["content-edit", "magnets-manage"]);
226
228
  });
227
229
 
230
+ it("takes people-read (#852 D9)", async () => {
231
+ const id = await insertAgent();
232
+ await db.query(
233
+ `UPDATE "agents" SET "grants" = ARRAY['people-read']::TEXT[] WHERE "id" = $1`,
234
+ [id],
235
+ );
236
+ expect(await grantsOf(id)).toEqual(["people-read"]);
237
+ });
238
+
239
+ it("takes member-pieces-manage (#7878)", async () => {
240
+ const id = await insertAgent();
241
+ await db.query(
242
+ `UPDATE "agents" SET "grants" = ARRAY['member-pieces-manage','people-read']::TEXT[] WHERE "id" = $1`,
243
+ [id],
244
+ );
245
+ expect(await grantsOf(id)).toEqual(["member-pieces-manage", "people-read"]);
246
+ });
247
+
228
248
  it("refuses any other permission", async () => {
229
249
  const id = await insertAgent();
230
250
  await expect(