create-ailk 0.19.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/add-module.js +7 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/scaffold-render-sweep.d.ts +23 -0
- package/dist/lib/scaffold-render-sweep.js +52 -0
- package/dist/module-architecture.js +9 -3
- package/dist/programmatic.js +8 -1
- package/dist/site-roster.js +21 -4
- package/dist/surfaces.d.ts +14 -0
- package/dist/surfaces.js +31 -1
- package/dist/topology.js +29 -1
- package/package.json +3 -3
- package/templates/apps/api/.env.example +17 -1
- package/templates/apps/api/CLAUDE.md +10 -4
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +19 -0
- package/templates/apps/api/src/__tests__/stripe-boot.test.ts +132 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +8 -0
- package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +237 -0
- package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +334 -0
- package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +217 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +7 -0
- package/templates/apps/api/src/lib/consent-store.ts +335 -0
- package/templates/apps/api/src/lib/stripe.ts +97 -5
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +10 -4
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +51 -0
- package/templates/apps/api/src/middleware/auth.ts +11 -3
- package/templates/apps/api/src/middleware/rate-limit.ts +5 -0
- package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +167 -0
- package/templates/apps/api/src/routes/checkout/session.ts +3 -3
- package/templates/apps/api/src/routes/checkout/sessions.ts +6 -4
- package/templates/apps/api/src/routes/consent/cookie.ts +90 -0
- package/templates/apps/api/src/routes/consent/index.ts +18 -0
- package/templates/apps/api/src/routes/consent/unsubscribe.ts +127 -0
- package/templates/apps/api/src/routes/leads/index.ts +15 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +5 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +17 -4
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +2 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +159 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +158 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +2 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +17 -0
- package/templates/apps/api/src/server.ts +111 -16
- package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +503 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +18 -3
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +115 -4
- package/templates/apps/api/src/services/__tests__/person.test.ts +392 -0
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +87 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +57 -17
- package/templates/apps/api/src/services/consent.ts +488 -3
- package/templates/apps/api/src/services/lead-routing.ts +105 -12
- package/templates/apps/api/src/services/person.ts +501 -1
- package/templates/apps/api/src/services/project-listings.ts +7 -2
- package/templates/apps/api/src/services/request-confirm.ts +52 -0
- package/templates/apps/api/src/services/revalidation.ts +11 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/account-frame-sign-out.test.tsx +175 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +17 -3
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +156 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +89 -14
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +146 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +122 -18
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +15 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +290 -20
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +55 -0
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +74 -4
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +4 -7
- package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +3 -3
- package/templates/apps/web/app/[locale]/(authed)/account/general/contact-actions.ts +81 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +66 -4
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +26 -3
- package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +2 -2
- package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +89 -37
- package/templates/apps/web/app/[locale]/(authed)/account/security/security-panel.tsx +102 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/security-source.ts +62 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +41 -33
- package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +1 -1
- package/templates/apps/web/auth.ts +20 -1
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +84 -0
- package/templates/apps/web/lib/auth-email.ts +45 -10
- package/templates/apps/web/lib/topology.generated.ts +9 -0
- package/templates/apps/web/messages/en.json +54 -14
- package/templates/database/CHANGELOG.md +16 -0
- package/templates/database/__tests__/consent-ledger.db.test.ts +345 -0
- package/templates/database/__tests__/fence-fan-out.db.test.ts +6 -2
- package/templates/database/__tests__/fixtures/person-contact-backfill.json +182 -0
- package/templates/database/__tests__/person-contact.db.test.ts +563 -0
- package/templates/database/__tests__/person.db.test.ts +3 -1
- package/templates/database/inbox/schema.prisma +76 -10
- package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +122 -0
- package/templates/database/migrations/20261014120000_person_contact/migration.sql +231 -0
- package/templates/database/package.json +1 -1
- package/versions.json +35 -35
|
@@ -4,15 +4,22 @@
|
|
|
4
4
|
*
|
|
5
5
|
* `memberSettingsFrame` supplies the member's half of the shared
|
|
6
6
|
* `AccountSettingsFrame` (@working-theory/templates/account, spec 8339 D2):
|
|
7
|
-
* `standalone:
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* directly (Security, Notifications,
|
|
7
|
+
* `standalone: true` — the Admin's shape, full height and flush, because the
|
|
8
|
+
* account layout renders these routes outside its AppShell (#8430) — Back to
|
|
9
|
+
* the Inbox, and the tabs of `SETTINGS_ROUTES`. Every member settings page
|
|
10
|
+
* calls it and renders the result through `AccountSettingsFrame` (General,
|
|
11
|
+
* via the shared `AccountSettings`) or directly (Security, Notifications,
|
|
12
|
+
* Billing).
|
|
12
13
|
*
|
|
13
14
|
* `SettingsSection` stays as it is: contributed sections
|
|
14
15
|
* (`accountSettingsSections`, `accountBillingSections`) still use it, as do
|
|
15
16
|
* General's and Billing's own trailing content.
|
|
17
|
+
*
|
|
18
|
+
* #8441 — `SETTINGS_ROUTES` is the ONE place the settings routes are named:
|
|
19
|
+
* the tabs here and `isSettingsPath` (which `account-frame.tsx` takes these
|
|
20
|
+
* routes out of its shell with) both read it, so a tab added here is a tab
|
|
21
|
+
* that renders outside the shell — never a second `<main>` nested in the
|
|
22
|
+
* shell's. Adding a tab is one line here (plus its page and message).
|
|
16
23
|
*/
|
|
17
24
|
|
|
18
25
|
import type { AccountSettingsFrameProps, AccountSettingsTab } from "@working-theory/templates/account";
|
|
@@ -29,42 +36,43 @@ import type { ReactElement, ReactNode } from "react";
|
|
|
29
36
|
* own `t` with `as unknown as Translate`. */
|
|
30
37
|
export type Translate = (key: string, values?: Record<string, unknown>) => string;
|
|
31
38
|
|
|
39
|
+
/** User settings' tabs, in the reference's order (#8339 D7) — each one a
|
|
40
|
+
* route that renders outside the account shell (#8430, #8441). */
|
|
41
|
+
export const SETTINGS_ROUTES = [
|
|
42
|
+
{ key: "general", path: "/account/general", titleKey: "settings.general.title", icon: UserRound },
|
|
43
|
+
{ key: "security", path: "/account/security", titleKey: "settings.security.title", icon: ShieldCheck },
|
|
44
|
+
{ key: "notifications", path: "/account/notifications", titleKey: "settings.notifications.title", icon: Bell },
|
|
45
|
+
{ key: "billing", path: "/account/billing", titleKey: "settings.billing.title", icon: CreditCard },
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
export type SettingsTabKey = (typeof SETTINGS_ROUTES)[number]["key"];
|
|
49
|
+
|
|
50
|
+
/** Whether `pathname` is one of `SETTINGS_ROUTES`: the request locale's
|
|
51
|
+
* `/<locale>` prefix removed, then the rest compared whole — so a sub-route
|
|
52
|
+
* (`/account/billing/history`) is not a settings route unless it is listed. */
|
|
53
|
+
export function isSettingsPath(pathname: string, locale: string): boolean {
|
|
54
|
+
const prefix = `/${locale}`;
|
|
55
|
+
const bare =
|
|
56
|
+
pathname === prefix || pathname.startsWith(`${prefix}/`) ? pathname.slice(prefix.length) : pathname;
|
|
57
|
+
return SETTINGS_ROUTES.some(({ path }) => path === bare);
|
|
58
|
+
}
|
|
59
|
+
|
|
32
60
|
/** The member's half of `AccountSettingsFrameProps` — every settings page's
|
|
33
61
|
* `frame` prop, less `name` (the caller's own session) and `children`. */
|
|
34
62
|
export function memberSettingsFrame(
|
|
35
63
|
locale: string,
|
|
36
64
|
t: Translate,
|
|
37
|
-
active:
|
|
65
|
+
active: SettingsTabKey,
|
|
38
66
|
): Omit<AccountSettingsFrameProps, "name" | "children"> {
|
|
39
|
-
const tabs: readonly AccountSettingsTab[] =
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
key: "security",
|
|
48
|
-
label: t("settings.security.title"),
|
|
49
|
-
href: localeHref(locale, "/account/security"),
|
|
50
|
-
icon: <Icon icon={ShieldCheck} size="sm" decorative className="shrink-0" />,
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
key: "notifications",
|
|
54
|
-
label: t("settings.notifications.title"),
|
|
55
|
-
href: localeHref(locale, "/account/notifications"),
|
|
56
|
-
icon: <Icon icon={Bell} size="sm" decorative className="shrink-0" />,
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
key: "billing",
|
|
60
|
-
label: t("settings.billing.title"),
|
|
61
|
-
href: localeHref(locale, "/account/billing"),
|
|
62
|
-
icon: <Icon icon={CreditCard} size="sm" decorative className="shrink-0" />,
|
|
63
|
-
},
|
|
64
|
-
];
|
|
67
|
+
const tabs: readonly AccountSettingsTab[] = SETTINGS_ROUTES.map(({ key, path, titleKey, icon }) => ({
|
|
68
|
+
key,
|
|
69
|
+
label: t(titleKey),
|
|
70
|
+
href: localeHref(locale, path),
|
|
71
|
+
icon: <Icon icon={icon} size="sm" decorative className="shrink-0" />,
|
|
72
|
+
}));
|
|
65
73
|
|
|
66
74
|
return {
|
|
67
|
-
standalone:
|
|
75
|
+
standalone: true,
|
|
68
76
|
back: { label: t("settings.back"), href: localeHref(locale, "/account") },
|
|
69
77
|
railLabel: t("settings.title"),
|
|
70
78
|
accountLabel: t("settings.account"),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* account/sign-in-method.ts (server code) — how a member signs in, for User settings ›
|
|
3
|
-
*
|
|
3
|
+
* Security's sign-in block (#8232, spec
|
|
4
4
|
* docs/specs/8232-member-settings-account-general-and-spec.md D7).
|
|
5
5
|
*
|
|
6
6
|
* One of the account tree's TWO topology seams (roadmap D1; the other is
|
|
@@ -324,6 +324,24 @@ export function createAuth(deps: CreateAuthDeps = {}) {
|
|
|
324
324
|
const sendResetPassword = makeSendResetPassword({
|
|
325
325
|
prisma,
|
|
326
326
|
send: authEmailTransport,
|
|
327
|
+
/**
|
|
328
|
+
* Who is signed in on the reset request (#8431 D4) — the Security tab's
|
|
329
|
+
* Set password, for a person with no password yet. Read through THIS
|
|
330
|
+
* instance (assigned below; the callback runs only during a request, by
|
|
331
|
+
* which time it exists), never a second one, and never by verifying the
|
|
332
|
+
* cookie by hand. `disableRefresh`: the mail callback runs in the
|
|
333
|
+
* background after the response, where a refresh's cookie write would
|
|
334
|
+
* throw and drop the mail. No request (a server-side call carries no
|
|
335
|
+
* cookie) or no `api` (a test double of `betterAuth`) is no session.
|
|
336
|
+
*/
|
|
337
|
+
sessionUserId: async (request) => {
|
|
338
|
+
if (request === undefined || Reflect.get(instance, "api") === undefined) return null;
|
|
339
|
+
const session = await instance.api.getSession({
|
|
340
|
+
headers: request.headers,
|
|
341
|
+
query: { disableRefresh: true },
|
|
342
|
+
});
|
|
343
|
+
return session?.user.id ?? null;
|
|
344
|
+
},
|
|
327
345
|
});
|
|
328
346
|
const sendVerificationEmail = makeSendVerificationEmail({
|
|
329
347
|
send: authEmailTransport,
|
|
@@ -419,7 +437,7 @@ export function createAuth(deps: CreateAuthDeps = {}) {
|
|
|
419
437
|
}
|
|
420
438
|
};
|
|
421
439
|
|
|
422
|
-
|
|
440
|
+
const instance = betterAuth({
|
|
423
441
|
database: prismaAdapter(prisma, {
|
|
424
442
|
provider: "postgresql",
|
|
425
443
|
}),
|
|
@@ -643,6 +661,7 @@ export function createAuth(deps: CreateAuthDeps = {}) {
|
|
|
643
661
|
]
|
|
644
662
|
: [nextCookies()],
|
|
645
663
|
});
|
|
664
|
+
return instance;
|
|
646
665
|
}
|
|
647
666
|
|
|
648
667
|
// Lazy singleton: resolved on first access, not at module-load time.
|
|
@@ -152,6 +152,90 @@ describe("sendResetPassword (§ D14 — the credential-account check)", () => {
|
|
|
152
152
|
});
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
+
describe("sendResetPassword — the signed-in carve-out (#8431 D4)", () => {
|
|
156
|
+
const USER = { id: "user_1", email: "person@example.com" };
|
|
157
|
+
const RESET_URL = "https://site.example/api/auth/reset-password/tok";
|
|
158
|
+
const REQUEST = { headers: new Headers({ cookie: "session=abc" }) } as Request;
|
|
159
|
+
|
|
160
|
+
it("sends ONE password-reset to the account's own address when the session is that person's", async () => {
|
|
161
|
+
const send = jest.fn().mockResolvedValue({ ok: true, body: { ok: true } });
|
|
162
|
+
const { lines, log } = makeLog();
|
|
163
|
+
const sessionUserId = jest.fn().mockResolvedValue("user_1");
|
|
164
|
+
const cb = makeSendResetPassword({ prisma: makePrisma(false), send, log, sessionUserId });
|
|
165
|
+
|
|
166
|
+
await cb({ user: USER, url: RESET_URL }, REQUEST);
|
|
167
|
+
|
|
168
|
+
expect(sessionUserId).toHaveBeenCalledWith(REQUEST);
|
|
169
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
170
|
+
const [rawBody] = send.mock.calls[0] as [string];
|
|
171
|
+
expect(JSON.parse(rawBody)).toEqual({
|
|
172
|
+
kind: "password-reset",
|
|
173
|
+
userId: "user_1",
|
|
174
|
+
to: "person@example.com",
|
|
175
|
+
url: RESET_URL,
|
|
176
|
+
expiresInMinutes: 60,
|
|
177
|
+
});
|
|
178
|
+
expect(lines.some((l) => l.fields.reason === "self_service_set")).toBe(true);
|
|
179
|
+
expect(JSON.stringify(lines)).not.toContain("person@example.com");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it.each<[string, () => Promise<string | null>]>([
|
|
183
|
+
["another person's id", () => Promise.resolve("user_2")],
|
|
184
|
+
["null (no session)", () => Promise.resolve(null)],
|
|
185
|
+
["a rejection", () => Promise.reject(new Error("db down"))],
|
|
186
|
+
])("sends NOTHING and logs no address when the session lookup answers %s", async (_label, impl) => {
|
|
187
|
+
const send = jest.fn();
|
|
188
|
+
const { lines, log } = makeLog();
|
|
189
|
+
const cb = makeSendResetPassword({ prisma: makePrisma(false), send, log, sessionUserId: jest.fn(impl) });
|
|
190
|
+
|
|
191
|
+
await expect(cb({ user: USER, url: RESET_URL }, REQUEST)).resolves.toBeUndefined();
|
|
192
|
+
|
|
193
|
+
expect(send).not.toHaveBeenCalled();
|
|
194
|
+
expect(lines.some((l) => l.fields.reason === "no_credential_account")).toBe(true);
|
|
195
|
+
expect(JSON.stringify(lines)).not.toContain("person@example.com");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("logs the failed lookup's kind only", async () => {
|
|
199
|
+
const { lines, log } = makeLog();
|
|
200
|
+
const cb = makeSendResetPassword({
|
|
201
|
+
prisma: makePrisma(false),
|
|
202
|
+
send: jest.fn(),
|
|
203
|
+
log,
|
|
204
|
+
sessionUserId: jest.fn().mockRejectedValue(new Error("person@example.com broke")),
|
|
205
|
+
});
|
|
206
|
+
await cb({ user: USER, url: RESET_URL }, REQUEST);
|
|
207
|
+
expect(lines).toEqual([
|
|
208
|
+
{
|
|
209
|
+
level: "warn",
|
|
210
|
+
fields: {
|
|
211
|
+
userId: "user_1",
|
|
212
|
+
kind: "password-reset",
|
|
213
|
+
reason: "no_credential_account",
|
|
214
|
+
errorKind: "session_lookup_failed",
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("with no sessionUserId it sends nothing, as before", async () => {
|
|
221
|
+
const send = jest.fn();
|
|
222
|
+
const { log } = makeLog();
|
|
223
|
+
const cb = makeSendResetPassword({ prisma: makePrisma(false), send, log });
|
|
224
|
+
await cb({ user: USER, url: RESET_URL }, REQUEST);
|
|
225
|
+
expect(send).not.toHaveBeenCalled();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("a person WITH a password never needs the session lookup", async () => {
|
|
229
|
+
const send = jest.fn().mockResolvedValue({ ok: true, body: { ok: true } });
|
|
230
|
+
const { log } = makeLog();
|
|
231
|
+
const sessionUserId = jest.fn();
|
|
232
|
+
const cb = makeSendResetPassword({ prisma: makePrisma(true), send, log, sessionUserId });
|
|
233
|
+
await cb({ user: USER, url: RESET_URL });
|
|
234
|
+
expect(sessionUserId).not.toHaveBeenCalled();
|
|
235
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
155
239
|
describe("the § D12 PII boundary", () => {
|
|
156
240
|
it("never logs the address, the token or the assembled link", async () => {
|
|
157
241
|
const send = jest.fn().mockResolvedValue({ ok: true, body: { ok: true } });
|
|
@@ -200,6 +200,12 @@ async function dispatch(
|
|
|
200
200
|
|
|
201
201
|
export interface ResetPasswordDeps extends DispatchDeps {
|
|
202
202
|
prisma: AuthEmailPrisma;
|
|
203
|
+
/**
|
|
204
|
+
* The id of the person signed in on `request`, or `null` (#8431 D4). Set,
|
|
205
|
+
* it lets a signed-in person ask for their OWN first password — the
|
|
206
|
+
* Security tab's Set password. Unset, the § D14 guard is unchanged.
|
|
207
|
+
*/
|
|
208
|
+
sessionUserId?: (request: Request | undefined) => Promise<string | null>;
|
|
203
209
|
}
|
|
204
210
|
|
|
205
211
|
/**
|
|
@@ -217,17 +223,29 @@ export interface ResetPasswordDeps extends DispatchDeps {
|
|
|
217
223
|
* address does not exist": the second is the enumeration AC-1 forbids and the
|
|
218
224
|
* first is nearly as useful to an attacker, since it maps a site's users to
|
|
219
225
|
* their identity provider.
|
|
226
|
+
*
|
|
227
|
+
* THE ONE CARVE-OUT (#8431 D4). A person with no password who is SIGNED IN
|
|
228
|
+
* may set one from their Security tab, by the same emailed link: the mail
|
|
229
|
+
* goes out when `deps.sessionUserId(request)` is that same person's id. The
|
|
230
|
+
* concern above is an ANONYMOUS request adding a password path around
|
|
231
|
+
* someone's identity provider; a request carrying that person's own live
|
|
232
|
+
* session is that person. The anonymous path is unchanged and silent, the
|
|
233
|
+
* link still goes only to the account's own address, and a failed session
|
|
234
|
+
* lookup counts as no session.
|
|
220
235
|
*/
|
|
221
236
|
export function makeSendResetPassword(deps: ResetPasswordDeps) {
|
|
222
237
|
const { prisma, send } = deps;
|
|
223
238
|
const log = deps.log ?? defaultLog;
|
|
224
|
-
return async function sendResetPassword(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
239
|
+
return async function sendResetPassword(
|
|
240
|
+
{
|
|
241
|
+
user,
|
|
242
|
+
url,
|
|
243
|
+
}: {
|
|
244
|
+
user: { id: string; email: string };
|
|
245
|
+
url: string;
|
|
246
|
+
},
|
|
247
|
+
request?: Request,
|
|
248
|
+
): Promise<void> {
|
|
231
249
|
let hasPassword: { id: string } | null;
|
|
232
250
|
try {
|
|
233
251
|
hasPassword = await prisma.account.findFirst({
|
|
@@ -245,12 +263,29 @@ export function makeSendResetPassword(deps: ResetPasswordDeps) {
|
|
|
245
263
|
}
|
|
246
264
|
|
|
247
265
|
if (!hasPassword) {
|
|
248
|
-
|
|
266
|
+
let sessionUserId: string | null = null;
|
|
267
|
+
let lookupFailed = false;
|
|
268
|
+
if (deps.sessionUserId) {
|
|
269
|
+
try {
|
|
270
|
+
sessionUserId = await deps.sessionUserId(request);
|
|
271
|
+
} catch {
|
|
272
|
+
lookupFailed = true;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (sessionUserId !== user.id) {
|
|
276
|
+
log.warn({
|
|
277
|
+
userId: user.id,
|
|
278
|
+
kind: "password-reset",
|
|
279
|
+
reason: "no_credential_account",
|
|
280
|
+
...(lookupFailed ? { errorKind: "session_lookup_failed" } : {}),
|
|
281
|
+
});
|
|
282
|
+
return; // no send; the endpoint's 200 body is unchanged
|
|
283
|
+
}
|
|
284
|
+
log.info({
|
|
249
285
|
userId: user.id,
|
|
250
286
|
kind: "password-reset",
|
|
251
|
-
reason: "
|
|
287
|
+
reason: "self_service_set",
|
|
252
288
|
});
|
|
253
|
-
return; // no send; the endpoint's 200 body is unchanged
|
|
254
289
|
}
|
|
255
290
|
|
|
256
291
|
await dispatch(
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
// on this topology (#8070).
|
|
10
10
|
// loadSpokeBilling reaches the spoke billing action only on a spoke that
|
|
11
11
|
// signs in through its hub and carries commerce (#8217); elsewhere, null.
|
|
12
|
+
// readOwnAccounts lists the signed-in person's own sign-in accounts through
|
|
13
|
+
// Better Auth on a hub (#8431); a spoke has none here, so it answers null.
|
|
12
14
|
|
|
13
15
|
export const TOPOLOGY: "hub" | "spoke" = "hub";
|
|
14
16
|
export const UNSERVED_MCP_TOOLS: readonly string[] = [];
|
|
@@ -32,6 +34,13 @@ export async function readSession(headers: Headers): Promise<SiteSession> {
|
|
|
32
34
|
return auth.api.getSession({ headers });
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
export async function readOwnAccounts(
|
|
38
|
+
headers: Headers,
|
|
39
|
+
): Promise<ReadonlyArray<{ providerId: string }> | null> {
|
|
40
|
+
const { auth } = await import("../auth");
|
|
41
|
+
return auth.api.listUserAccounts({ headers });
|
|
42
|
+
}
|
|
43
|
+
|
|
35
44
|
export async function registerSiteStores(): Promise<void> {
|
|
36
45
|
const { registerLicenseKeyStore } = await import("./license-key-store");
|
|
37
46
|
await registerLicenseKeyStore();
|
|
@@ -630,6 +630,7 @@
|
|
|
630
630
|
"forYou": "For you",
|
|
631
631
|
"settings": "Settings",
|
|
632
632
|
"signOut": "Log out",
|
|
633
|
+
"signOutFailed": "We couldn't log you out. You're still signed in; try again.",
|
|
633
634
|
"help": "Help",
|
|
634
635
|
"contactSupport": "Contact support",
|
|
635
636
|
"helpCenter": "Help center",
|
|
@@ -715,26 +716,65 @@
|
|
|
715
716
|
"saveError": "We couldn't save your changes. Try again.",
|
|
716
717
|
"photoInvalid": "Enter a link that starts with https://.",
|
|
717
718
|
"unverified": "Confirm your email address first, then try again.",
|
|
718
|
-
"unavailable": "Profile changes aren't available on this site yet."
|
|
719
|
+
"unavailable": "Profile changes aren't available on this site yet.",
|
|
720
|
+
"contact": {
|
|
721
|
+
"title": "Contact details",
|
|
722
|
+
"lead": "Kept on this site, for the people you work with here.",
|
|
723
|
+
"givenName": "First name",
|
|
724
|
+
"familyName": "Last name",
|
|
725
|
+
"telephone": "Phone",
|
|
726
|
+
"address": "Address",
|
|
727
|
+
"companyName": "Company",
|
|
728
|
+
"jobTitle": "Job title",
|
|
729
|
+
"save": "Save contact details",
|
|
730
|
+
"saving": "Saving…",
|
|
731
|
+
"saved": "Saved.",
|
|
732
|
+
"addressIncomplete": "Fill in the address line, city, state or region, postal code and country, or clear the whole address.",
|
|
733
|
+
"unavailable": "Contact details aren't available on this site right now.",
|
|
734
|
+
"errors": {
|
|
735
|
+
"telephone": "Enter the phone number with its country code.",
|
|
736
|
+
"invalid": "Some of these details couldn't be saved. Check them and try again.",
|
|
737
|
+
"unverified": "Confirm your email address first, then try again.",
|
|
738
|
+
"unavailable": "Contact details can't be saved on this site yet.",
|
|
739
|
+
"failed": "We couldn't save your contact details. Try again."
|
|
740
|
+
}
|
|
741
|
+
}
|
|
719
742
|
},
|
|
720
743
|
"security": {
|
|
721
|
-
"title": "Security"
|
|
744
|
+
"title": "Security",
|
|
745
|
+
"methodsHeading": "Sign-in methods",
|
|
746
|
+
"passwordLabel": "Password",
|
|
747
|
+
"passwordSet": "Set",
|
|
748
|
+
"passwordNotSet": "Not set",
|
|
749
|
+
"setHint": "Add a password so you can also sign in with your email and password.",
|
|
750
|
+
"changeHint": "Change the password you sign in with.",
|
|
751
|
+
"setNote": "We'll email {email} a secure link to create your password.",
|
|
752
|
+
"changeNote": "We'll email {email} a secure link to change your password.",
|
|
753
|
+
"setButton": "Set password",
|
|
754
|
+
"changeButton": "Change password",
|
|
755
|
+
"sending": "Sending…",
|
|
756
|
+
"sent": "Link sent. It works once, for an hour. Saving a password signs you out everywhere.",
|
|
757
|
+
"sendFailed": "We couldn't send the email. Try again in a few minutes.",
|
|
758
|
+
"logOut": "Log out",
|
|
759
|
+
"logOutFailed": "Log out didn't finish. Try again.",
|
|
760
|
+
"signsInWith": "You sign in with {methods}.",
|
|
761
|
+
"signedInAs": "You're signed in as {email}.",
|
|
762
|
+
"methods": {
|
|
763
|
+
"google": "Google",
|
|
764
|
+
"github": "GitHub",
|
|
765
|
+
"magicLink": "a link sent to {email}",
|
|
766
|
+
"password": "your email and password"
|
|
767
|
+
},
|
|
768
|
+
"managedAt": "Your sign-in is managed at {hub}.",
|
|
769
|
+
"managedUnnamed": "Your sign-in is managed by our main site.",
|
|
770
|
+
"manageOnHub": "Manage sign-in on {hub}",
|
|
771
|
+
"passwordUnavailable": "We couldn't check your password settings."
|
|
722
772
|
},
|
|
723
773
|
"notifications": {
|
|
724
774
|
"title": "Notifications"
|
|
725
775
|
},
|
|
726
776
|
"signIn": {
|
|
727
|
-
"lead": "How you sign in to this site."
|
|
728
|
-
"methods": {
|
|
729
|
-
"google": "Google",
|
|
730
|
-
"github": "GitHub",
|
|
731
|
-
"magicLink": "Email sign-in link",
|
|
732
|
-
"password": "Password"
|
|
733
|
-
},
|
|
734
|
-
"available": "Available",
|
|
735
|
-
"none": "No sign-in method is configured.",
|
|
736
|
-
"viaHub": "You sign in through {hub}.",
|
|
737
|
-
"viaHubUnnamed": "You sign in through our main site."
|
|
777
|
+
"lead": "How you sign in to this site."
|
|
738
778
|
},
|
|
739
779
|
"privacy": {
|
|
740
780
|
"lead": "Cookies and your data.",
|
|
@@ -757,7 +797,7 @@
|
|
|
757
797
|
"accessEnds": "Access ends {date}",
|
|
758
798
|
"card": "{brand} ending in {last4}, expires {month}/{year}",
|
|
759
799
|
"noCard": "No card on file",
|
|
760
|
-
"manage": "Manage
|
|
800
|
+
"manage": "Manage billing",
|
|
761
801
|
"none": "You don't have a subscription.",
|
|
762
802
|
"unavailable": "We couldn't load your billing details.",
|
|
763
803
|
"unreadable": "Some of your billing is with another Stripe account, so it isn't shown here.",
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @working-theory/database
|
|
2
2
|
|
|
3
|
+
## 0.40.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#8497](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/8497) [`52df359`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/52df359fb2a4d1aff488136126dba16d13e49a26) Thanks [@samuelhenry](https://github.com/samuelhenry)! - Contact details on the client record ([#8435](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/8435), spec docs/specs/8435-contact-details-on-the-client-spec.md).
|
|
8
|
+
|
|
9
|
+
A Person now carries a given name, a family name, an E.164 telephone, a six-part address, a company name and a job title, with the two columns that say who last saved them by hand. `callbacks` and `subscribers` gain a `person_id` foreign key. A capture fills only a blank field of an uncurated Person; the operator edits the contact from the Person page and a client edits their own on the site, from the account General tab. A magnet field's `mapsTo` tag names the contact field it fills: `email`, `name`, `givenName`, `familyName`, `phone`, `address`, `company`, `jobTitle`. Additive; the migration (`20261014120000_person_contact`, its second half the idempotent backfill) deploys before the code.
|
|
10
|
+
|
|
11
|
+
- [#8490](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/pull/8490) [`64adcf9`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/64adcf9b58a3f3be3b42782e14018234b263b9bc) Thanks [@samuelhenry](https://github.com/samuelhenry)! - The consent ledger ([#8438](https://github.com/working-theory-labs/ai-launch-kit-internal/issues/8438), the API half). The per-site consent registry gains a kind (`agreement` · `consent` · `preference`), a plain description, a policy link and a policy version per purpose, and its platform default now declares `service_terms`, `account_terms` and `analytics_cookies`; a registry stored before this change still parses and means what it meant. `consent_grants` becomes the one ledger: each row names its source (`cookie_banner` · `sign_up` · `magnet` · `account_settings` · `email_unsubscribe` · `api`), its policy version and, when known, its Person, and the database refuses to change or truncate a stored row. A new `consent_visitor_links` table joins a banner visitor to the Person a later form identifies. `@working-theory/api-client` gains `consent.recordCookieChoice`, `consent.unsubscribe`, `member.consents` and `member.setConsent`; `@working-theory/validation` gains their wire schemas and the `platform-consents` permission. Run the new migration before deploying code that writes the ledger.
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [[`52df359`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/52df359fb2a4d1aff488136126dba16d13e49a26), [`64adcf9`](https://github.com/Working-Theory-Labs/ai-launch-kit-internal/commit/64adcf9b58a3f3be3b42782e14018234b263b9bc)]:
|
|
16
|
+
- @working-theory/validation@0.40.0
|
|
17
|
+
- @working-theory/ids@0.13.0
|
|
18
|
+
|
|
3
19
|
## 0.39.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|