@braedonsaunders/appkit-auth 0.1.3
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/LICENSE +661 -0
- package/README.md +36 -0
- package/client.d.ts +129 -0
- package/client.d.ts.map +1 -0
- package/client.js +10 -0
- package/client.js.map +1 -0
- package/drizzle.d.ts +23 -0
- package/drizzle.d.ts.map +1 -0
- package/drizzle.js +66 -0
- package/drizzle.js.map +1 -0
- package/index.d.ts +4 -0
- package/index.d.ts.map +1 -0
- package/index.js +3 -0
- package/index.js.map +1 -0
- package/invites.d.ts +79 -0
- package/invites.d.ts.map +1 -0
- package/invites.js +185 -0
- package/invites.js.map +1 -0
- package/mail.d.ts +36 -0
- package/mail.d.ts.map +1 -0
- package/mail.js +32 -0
- package/mail.js.map +1 -0
- package/memory.d.ts +7 -0
- package/memory.d.ts.map +1 -0
- package/memory.js +24 -0
- package/memory.js.map +1 -0
- package/next.d.ts +2 -0
- package/next.d.ts.map +1 -0
- package/next.js +2 -0
- package/next.js.map +1 -0
- package/package.json +81 -0
- package/react.d.ts +74 -0
- package/react.d.ts.map +1 -0
- package/react.js +129 -0
- package/react.js.map +1 -0
- package/server.d.ts +510 -0
- package/server.d.ts.map +1 -0
- package/server.js +191 -0
- package/server.js.map +1 -0
package/mail.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type AuthEmailKind = 'magic-link' | 'invitation' | 'password-reset';
|
|
2
|
+
export type AuthEmail = {
|
|
3
|
+
kind: AuthEmailKind;
|
|
4
|
+
to: string;
|
|
5
|
+
subject: string;
|
|
6
|
+
text: string;
|
|
7
|
+
html: string;
|
|
8
|
+
};
|
|
9
|
+
export type AuthEmailSender = (email: AuthEmail) => Promise<void>;
|
|
10
|
+
export type AuthEmailRenderer = {
|
|
11
|
+
magicLink?: (input: {
|
|
12
|
+
appName: string;
|
|
13
|
+
email: string;
|
|
14
|
+
url: string;
|
|
15
|
+
invitation: boolean;
|
|
16
|
+
tenantName: string | null;
|
|
17
|
+
}) => Omit<AuthEmail, 'kind' | 'to'>;
|
|
18
|
+
passwordReset?: (input: {
|
|
19
|
+
appName: string;
|
|
20
|
+
email: string;
|
|
21
|
+
url: string;
|
|
22
|
+
}) => Omit<AuthEmail, 'kind' | 'to'>;
|
|
23
|
+
};
|
|
24
|
+
export declare function renderMagicLinkEmail(input: {
|
|
25
|
+
appName: string;
|
|
26
|
+
email: string;
|
|
27
|
+
url: string;
|
|
28
|
+
invitation: boolean;
|
|
29
|
+
tenantName: string | null;
|
|
30
|
+
}): Omit<AuthEmail, 'kind' | 'to'>;
|
|
31
|
+
export declare function renderPasswordResetEmail(input: {
|
|
32
|
+
appName: string;
|
|
33
|
+
email: string;
|
|
34
|
+
url: string;
|
|
35
|
+
}): Omit<AuthEmail, 'kind' | 'to'>;
|
|
36
|
+
//# sourceMappingURL=mail.d.ts.map
|
package/mail.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mail.d.ts","sourceRoot":"","sources":["../src/mail.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG,YAAY,GAAG,YAAY,GAAG,gBAAgB,CAAA;AAE1E,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,aAAa,CAAA;IACnB,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;AAEjE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE;QAClB,OAAO,EAAE,MAAM,CAAA;QACf,KAAK,EAAE,MAAM,CAAA;QACb,GAAG,EAAE,MAAM,CAAA;QACX,UAAU,EAAE,OAAO,CAAA;QACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAC1B,KAAK,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC,CAAA;IACpC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE;QACtB,OAAO,EAAE,MAAM,CAAA;QACf,KAAK,EAAE,MAAM,CAAA;QACb,GAAG,EAAE,MAAM,CAAA;KACZ,KAAK,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC,CAAA;CACrC,CAAA;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAC1C,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,OAAO,CAAA;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B,GAAG,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC,CAejC;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE;IAC9C,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;CACZ,GAAG,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC,CAMjC"}
|
package/mail.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function renderMagicLinkEmail(input) {
|
|
2
|
+
const appName = input.appName.trim();
|
|
3
|
+
const tenantName = input.tenantName?.trim() || 'your workspace';
|
|
4
|
+
if (input.invitation) {
|
|
5
|
+
return {
|
|
6
|
+
subject: `You're invited to ${tenantName} in ${appName}`,
|
|
7
|
+
text: `You've been invited to join ${tenantName} in ${appName}.\n\nAccept the invitation and sign in:\n\n${input.url}\n\nThis one-time link expires soon. If you weren't expecting this invitation, ignore this email.`,
|
|
8
|
+
html: `<p>You've been invited to join <strong>${escapeHtml(tenantName)}</strong> in ${escapeHtml(appName)}.</p><p><a href="${escapeHtml(input.url)}">Accept the invitation and sign in</a></p><p>This one-time link expires soon.</p>`,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
subject: `Sign in to ${appName}`,
|
|
13
|
+
text: `Click this link to sign in to ${appName}:\n\n${input.url}\n\nThis one-time link expires soon. If you didn't request it, ignore this email.`,
|
|
14
|
+
html: `<p>Click <a href="${escapeHtml(input.url)}">here</a> to sign in to ${escapeHtml(appName)}.</p><p>This one-time link expires soon.</p>`,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function renderPasswordResetEmail(input) {
|
|
18
|
+
return {
|
|
19
|
+
subject: `Reset your ${input.appName.trim()} password`,
|
|
20
|
+
text: `A password reset was requested for your ${input.appName.trim()} account.\n\nSet a new password:\n\n${input.url}\n\nThis link expires soon. If you didn't request it, ignore this email—your password won't change.`,
|
|
21
|
+
html: `<p>A password reset was requested for your ${escapeHtml(input.appName.trim())} account.</p><p><a href="${escapeHtml(input.url)}">Set a new password</a></p><p>This link expires soon. If you didn't request it, ignore this email—your password won't change.</p>`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function escapeHtml(value) {
|
|
25
|
+
return value
|
|
26
|
+
.replace(/&/g, '&')
|
|
27
|
+
.replace(/</g, '<')
|
|
28
|
+
.replace(/>/g, '>')
|
|
29
|
+
.replace(/"/g, '"')
|
|
30
|
+
.replace(/'/g, ''');
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=mail.js.map
|
package/mail.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mail.js","sourceRoot":"","sources":["../src/mail.ts"],"names":[],"mappings":"AA2BA,MAAM,UAAU,oBAAoB,CAAC,KAMpC;IACC,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAA;IACpC,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,EAAE,IAAI,EAAE,IAAI,gBAAgB,CAAA;IAC/D,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC;QACrB,OAAO;YACL,OAAO,EAAE,qBAAqB,UAAU,OAAO,OAAO,EAAE;YACxD,IAAI,EAAE,+BAA+B,UAAU,OAAO,OAAO,8CAA8C,KAAK,CAAC,GAAG,mGAAmG;YACvN,IAAI,EAAE,0CAA0C,UAAU,CAAC,UAAU,CAAC,gBAAgB,UAAU,CAAC,OAAO,CAAC,oBAAoB,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,oFAAoF;SACvO,CAAA;IACH,CAAC;IACD,OAAO;QACL,OAAO,EAAE,cAAc,OAAO,EAAE;QAChC,IAAI,EAAE,iCAAiC,OAAO,QAAQ,KAAK,CAAC,GAAG,mFAAmF;QAClJ,IAAI,EAAE,qBAAqB,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,4BAA4B,UAAU,CAAC,OAAO,CAAC,8CAA8C;KAC9I,CAAA;AACH,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,KAIxC;IACC,OAAO;QACL,OAAO,EAAE,cAAc,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,WAAW;QACtD,IAAI,EAAE,2CAA2C,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,uCAAuC,KAAK,CAAC,GAAG,qGAAqG;QAC1N,IAAI,EAAE,8CAA8C,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,4BAA4B,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,oIAAoI;KAC1Q,CAAA;AACH,CAAC;AAED,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,KAAK;SACT,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;AAC3B,CAAC","sourcesContent":["export type AuthEmailKind = 'magic-link' | 'invitation' | 'password-reset'\n\nexport type AuthEmail = {\n kind: AuthEmailKind\n to: string\n subject: string\n text: string\n html: string\n}\n\nexport type AuthEmailSender = (email: AuthEmail) => Promise<void>\n\nexport type AuthEmailRenderer = {\n magicLink?: (input: {\n appName: string\n email: string\n url: string\n invitation: boolean\n tenantName: string | null\n }) => Omit<AuthEmail, 'kind' | 'to'>\n passwordReset?: (input: {\n appName: string\n email: string\n url: string\n }) => Omit<AuthEmail, 'kind' | 'to'>\n}\n\nexport function renderMagicLinkEmail(input: {\n appName: string\n email: string\n url: string\n invitation: boolean\n tenantName: string | null\n}): Omit<AuthEmail, 'kind' | 'to'> {\n const appName = input.appName.trim()\n const tenantName = input.tenantName?.trim() || 'your workspace'\n if (input.invitation) {\n return {\n subject: `You're invited to ${tenantName} in ${appName}`,\n text: `You've been invited to join ${tenantName} in ${appName}.\\n\\nAccept the invitation and sign in:\\n\\n${input.url}\\n\\nThis one-time link expires soon. If you weren't expecting this invitation, ignore this email.`,\n html: `<p>You've been invited to join <strong>${escapeHtml(tenantName)}</strong> in ${escapeHtml(appName)}.</p><p><a href=\"${escapeHtml(input.url)}\">Accept the invitation and sign in</a></p><p>This one-time link expires soon.</p>`,\n }\n }\n return {\n subject: `Sign in to ${appName}`,\n text: `Click this link to sign in to ${appName}:\\n\\n${input.url}\\n\\nThis one-time link expires soon. If you didn't request it, ignore this email.`,\n html: `<p>Click <a href=\"${escapeHtml(input.url)}\">here</a> to sign in to ${escapeHtml(appName)}.</p><p>This one-time link expires soon.</p>`,\n }\n}\n\nexport function renderPasswordResetEmail(input: {\n appName: string\n email: string\n url: string\n}): Omit<AuthEmail, 'kind' | 'to'> {\n return {\n subject: `Reset your ${input.appName.trim()} password`,\n text: `A password reset was requested for your ${input.appName.trim()} account.\\n\\nSet a new password:\\n\\n${input.url}\\n\\nThis link expires soon. If you didn't request it, ignore this email—your password won't change.`,\n html: `<p>A password reset was requested for your ${escapeHtml(input.appName.trim())} account.</p><p><a href=\"${escapeHtml(input.url)}\">Set a new password</a></p><p>This link expires soon. If you didn't request it, ignore this email—your password won't change.</p>`,\n }\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n}\n"]}
|
package/memory.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { InviteRecord, InviteStore } from './invites.js';
|
|
2
|
+
export type MemoryInviteStore = InviteStore & {
|
|
3
|
+
records: Map<string, InviteRecord>;
|
|
4
|
+
};
|
|
5
|
+
/** Deterministic adapter for tests, local apps, and database-free demos. */
|
|
6
|
+
export declare function createMemoryInviteStore(seed?: InviteRecord[]): MemoryInviteStore;
|
|
7
|
+
//# sourceMappingURL=memory.d.ts.map
|
package/memory.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,YAAY,EACZ,WAAW,EACZ,MAAM,WAAW,CAAA;AAGlB,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG;IAC5C,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;CACnC,CAAA;AAED,4EAA4E;AAC5E,wBAAgB,uBAAuB,CAAC,IAAI,GAAE,YAAY,EAAO,GAAG,iBAAiB,CAmBpF"}
|
package/memory.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { evaluateInviteAccess } from './invites.js';
|
|
2
|
+
/** Deterministic adapter for tests, local apps, and database-free demos. */
|
|
3
|
+
export function createMemoryInviteStore(seed = []) {
|
|
4
|
+
const records = new Map(seed.map((record) => [record.membershipId, structuredClone(record)]));
|
|
5
|
+
return {
|
|
6
|
+
records,
|
|
7
|
+
async inspect(membershipId) {
|
|
8
|
+
const record = records.get(membershipId);
|
|
9
|
+
return record ? structuredClone(record) : null;
|
|
10
|
+
},
|
|
11
|
+
async accept(payload, sessionUserId) {
|
|
12
|
+
const record = records.get(payload.membershipId) ?? null;
|
|
13
|
+
const state = evaluateInviteAccess(payload, sessionUserId, record);
|
|
14
|
+
if (state !== 'pending' || !record)
|
|
15
|
+
return state;
|
|
16
|
+
records.set(record.membershipId, {
|
|
17
|
+
...record,
|
|
18
|
+
status: 'active',
|
|
19
|
+
});
|
|
20
|
+
return 'active';
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=memory.js.map
|
package/memory.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.js","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAMhD,4EAA4E;AAC5E,MAAM,UAAU,uBAAuB,CAAC,OAAuB,EAAE;IAC/D,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;IAC7F,OAAO;QACL,OAAO;QACP,KAAK,CAAC,OAAO,CAAC,YAAY;YACxB,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;YACxC,OAAO,MAAM,CAAC,CAAC,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;QAChD,CAAC;QACD,KAAK,CAAC,MAAM,CAAC,OAA2B,EAAE,aAAqB;YAC7D,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,IAAI,CAAA;YACxD,MAAM,KAAK,GAAG,oBAAoB,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,CAAC,CAAA;YAClE,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,MAAM;gBAAE,OAAO,KAAK,CAAA;YAChD,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,YAAY,EAAE;gBAC/B,GAAG,MAAM;gBACT,MAAM,EAAE,QAAQ;aACjB,CAAC,CAAA;YACF,OAAO,QAAQ,CAAA;QACjB,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["import type {\n InviteGrantPayload,\n InviteRecord,\n InviteStore,\n} from './invites'\nimport { evaluateInviteAccess } from './invites'\n\nexport type MemoryInviteStore = InviteStore & {\n records: Map<string, InviteRecord>\n}\n\n/** Deterministic adapter for tests, local apps, and database-free demos. */\nexport function createMemoryInviteStore(seed: InviteRecord[] = []): MemoryInviteStore {\n const records = new Map(seed.map((record) => [record.membershipId, structuredClone(record)]))\n return {\n records,\n async inspect(membershipId) {\n const record = records.get(membershipId)\n return record ? structuredClone(record) : null\n },\n async accept(payload: InviteGrantPayload, sessionUserId: string) {\n const record = records.get(payload.membershipId) ?? null\n const state = evaluateInviteAccess(payload, sessionUserId, record)\n if (state !== 'pending' || !record) return state\n records.set(record.membershipId, {\n ...record,\n status: 'active',\n })\n return 'active'\n },\n }\n}\n"]}
|
package/next.d.ts
ADDED
package/next.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"next.d.ts","sourceRoot":"","sources":["../src/next.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA"}
|
package/next.js
ADDED
package/next.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"next.js","sourceRoot":"","sources":["../src/next.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA","sourcesContent":["export { toNextJsHandler } from 'better-auth/next-js'\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@braedonsaunders/appkit-auth",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Production authentication: durable sessions, passwords, magic links, invitation grants, reset flows, React UI, and Postgres adapters.",
|
|
5
|
+
"license": "AGPL-3.0-or-later",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./index.d.ts",
|
|
10
|
+
"import": "./index.js",
|
|
11
|
+
"default": "./index.js"
|
|
12
|
+
},
|
|
13
|
+
"./server": {
|
|
14
|
+
"types": "./server.d.ts",
|
|
15
|
+
"import": "./server.js",
|
|
16
|
+
"default": "./server.js"
|
|
17
|
+
},
|
|
18
|
+
"./client": {
|
|
19
|
+
"types": "./client.d.ts",
|
|
20
|
+
"import": "./client.js",
|
|
21
|
+
"default": "./client.js"
|
|
22
|
+
},
|
|
23
|
+
"./invites": {
|
|
24
|
+
"types": "./invites.d.ts",
|
|
25
|
+
"import": "./invites.js",
|
|
26
|
+
"default": "./invites.js"
|
|
27
|
+
},
|
|
28
|
+
"./drizzle": {
|
|
29
|
+
"types": "./drizzle.d.ts",
|
|
30
|
+
"import": "./drizzle.js",
|
|
31
|
+
"default": "./drizzle.js"
|
|
32
|
+
},
|
|
33
|
+
"./memory": {
|
|
34
|
+
"types": "./memory.d.ts",
|
|
35
|
+
"import": "./memory.js",
|
|
36
|
+
"default": "./memory.js"
|
|
37
|
+
},
|
|
38
|
+
"./react": {
|
|
39
|
+
"types": "./react.d.ts",
|
|
40
|
+
"import": "./react.js",
|
|
41
|
+
"default": "./react.js"
|
|
42
|
+
},
|
|
43
|
+
"./next": {
|
|
44
|
+
"types": "./next.d.ts",
|
|
45
|
+
"import": "./next.js",
|
|
46
|
+
"default": "./next.js"
|
|
47
|
+
},
|
|
48
|
+
"./package.json": "./package.json"
|
|
49
|
+
},
|
|
50
|
+
"main": "./index.js",
|
|
51
|
+
"types": "./index.d.ts",
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@braedonsaunders/appkit-db": "^0.2.0",
|
|
54
|
+
"@braedonsaunders/appkit-ui": "^0.2.0",
|
|
55
|
+
"better-auth": "^1.6.23",
|
|
56
|
+
"drizzle-orm": "^0.45.2",
|
|
57
|
+
"pg": "^8.22.0",
|
|
58
|
+
"zod": "^4.4.3"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"react": ">=18.2 <20"
|
|
62
|
+
},
|
|
63
|
+
"author": "Braedon Saunders",
|
|
64
|
+
"repository": {
|
|
65
|
+
"type": "git",
|
|
66
|
+
"url": "git+https://github.com/braedonsaunders/appkit.git",
|
|
67
|
+
"directory": "packages/auth"
|
|
68
|
+
},
|
|
69
|
+
"homepage": "https://github.com/braedonsaunders/appkit/tree/main/packages/auth#readme",
|
|
70
|
+
"bugs": {
|
|
71
|
+
"url": "https://github.com/braedonsaunders/appkit/issues"
|
|
72
|
+
},
|
|
73
|
+
"engines": {
|
|
74
|
+
"node": ">=22"
|
|
75
|
+
},
|
|
76
|
+
"keywords": [
|
|
77
|
+
"appkit",
|
|
78
|
+
"application-framework",
|
|
79
|
+
"typescript"
|
|
80
|
+
]
|
|
81
|
+
}
|
package/react.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export type AuthFormLabels = Partial<{
|
|
3
|
+
passwordMode: string;
|
|
4
|
+
magicLinkMode: string;
|
|
5
|
+
email: string;
|
|
6
|
+
password: string;
|
|
7
|
+
forgotPassword: string;
|
|
8
|
+
signIn: string;
|
|
9
|
+
sendMagicLink: string;
|
|
10
|
+
signingIn: string;
|
|
11
|
+
magicLinkSent: string;
|
|
12
|
+
invalidCredentials: string;
|
|
13
|
+
requestReset: string;
|
|
14
|
+
requestingReset: string;
|
|
15
|
+
resetSent: string;
|
|
16
|
+
newPassword: string;
|
|
17
|
+
confirmPassword: string;
|
|
18
|
+
showPassword: string;
|
|
19
|
+
hidePassword: string;
|
|
20
|
+
updatePassword: string;
|
|
21
|
+
updatingPassword: string;
|
|
22
|
+
passwordTooShort: string;
|
|
23
|
+
passwordsDoNotMatch: string;
|
|
24
|
+
invalidResetLink: string;
|
|
25
|
+
}>;
|
|
26
|
+
export type SignInFormProps = {
|
|
27
|
+
signInWithPassword: (input: {
|
|
28
|
+
email: string;
|
|
29
|
+
password: string;
|
|
30
|
+
}) => Promise<{
|
|
31
|
+
error?: string | null;
|
|
32
|
+
}>;
|
|
33
|
+
signInWithMagicLink: (input: {
|
|
34
|
+
email: string;
|
|
35
|
+
callbackURL: string;
|
|
36
|
+
}) => Promise<{
|
|
37
|
+
error?: string | null;
|
|
38
|
+
}>;
|
|
39
|
+
onSignedIn: () => void;
|
|
40
|
+
callbackURL?: string;
|
|
41
|
+
forgotPasswordHref?: string;
|
|
42
|
+
defaultMode?: 'password' | 'magic';
|
|
43
|
+
labels?: AuthFormLabels;
|
|
44
|
+
};
|
|
45
|
+
/** Source-parity password/magic-link sign-in form with app-owned navigation. */
|
|
46
|
+
export declare function SignInForm({ signInWithPassword, signInWithMagicLink, onSignedIn, callbackURL, forgotPasswordHref, defaultMode, labels, }: SignInFormProps): import("react").JSX.Element;
|
|
47
|
+
export type ForgotPasswordFormProps = {
|
|
48
|
+
requestReset: (email: string) => Promise<void>;
|
|
49
|
+
labels?: AuthFormLabels;
|
|
50
|
+
};
|
|
51
|
+
/** Account-enumeration-safe reset request form. */
|
|
52
|
+
export declare function ForgotPasswordForm({ requestReset, labels }: ForgotPasswordFormProps): import("react").JSX.Element;
|
|
53
|
+
export type ResetPasswordFormProps = {
|
|
54
|
+
token: string;
|
|
55
|
+
resetPassword: (input: {
|
|
56
|
+
token: string;
|
|
57
|
+
newPassword: string;
|
|
58
|
+
}) => Promise<{
|
|
59
|
+
error?: string | null;
|
|
60
|
+
}>;
|
|
61
|
+
onReset: () => void;
|
|
62
|
+
minLength?: number;
|
|
63
|
+
labels?: AuthFormLabels;
|
|
64
|
+
};
|
|
65
|
+
/** Token-bound reset form preserving the source validation and recovery states. */
|
|
66
|
+
export declare function ResetPasswordForm({ token, resetPassword, onReset, minLength, labels, }: ResetPasswordFormProps): import("react").JSX.Element;
|
|
67
|
+
export declare function AuthScreen({ logo, title, description, children, footer, }: {
|
|
68
|
+
logo?: ReactNode;
|
|
69
|
+
title: ReactNode;
|
|
70
|
+
description?: ReactNode;
|
|
71
|
+
children: ReactNode;
|
|
72
|
+
footer?: ReactNode;
|
|
73
|
+
}): import("react").JSX.Element;
|
|
74
|
+
//# sourceMappingURL=react.d.ts.map
|
package/react.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"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"}
|
package/react.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useTransition } from 'react';
|
|
4
|
+
import { Alert, AlertDescription, Button, Input, Label } from '@braedonsaunders/appkit-ui';
|
|
5
|
+
const defaults = {
|
|
6
|
+
passwordMode: 'Password',
|
|
7
|
+
magicLinkMode: 'Email link',
|
|
8
|
+
email: 'Email',
|
|
9
|
+
password: 'Password',
|
|
10
|
+
forgotPassword: 'Forgot password?',
|
|
11
|
+
signIn: 'Sign in',
|
|
12
|
+
sendMagicLink: 'Email me a sign-in link',
|
|
13
|
+
signingIn: 'Signing in…',
|
|
14
|
+
magicLinkSent: 'Check your email for a sign-in link.',
|
|
15
|
+
invalidCredentials: 'Sign-in failed. Check your details and try again.',
|
|
16
|
+
requestReset: 'Send reset link',
|
|
17
|
+
requestingReset: 'Sending…',
|
|
18
|
+
resetSent: 'If an account exists for that email, a reset link is on its way.',
|
|
19
|
+
newPassword: 'New password',
|
|
20
|
+
confirmPassword: 'Confirm password',
|
|
21
|
+
showPassword: 'Show password',
|
|
22
|
+
hidePassword: 'Hide password',
|
|
23
|
+
updatePassword: 'Update password',
|
|
24
|
+
updatingPassword: 'Updating…',
|
|
25
|
+
passwordTooShort: 'Password must be at least 8 characters.',
|
|
26
|
+
passwordsDoNotMatch: 'Passwords do not match.',
|
|
27
|
+
invalidResetLink: 'This reset link has expired or already been used. Request a new one.',
|
|
28
|
+
};
|
|
29
|
+
/** Source-parity password/magic-link sign-in form with app-owned navigation. */
|
|
30
|
+
export function SignInForm({ signInWithPassword, signInWithMagicLink, onSignedIn, callbackURL = '/auth/continue', forgotPasswordHref = '/forgot-password', defaultMode = 'password', labels, }) {
|
|
31
|
+
const copy = { ...defaults, ...labels };
|
|
32
|
+
const [pending, startTransition] = useTransition();
|
|
33
|
+
const [mode, setMode] = useState(defaultMode);
|
|
34
|
+
const [email, setEmail] = useState('');
|
|
35
|
+
const [password, setPassword] = useState('');
|
|
36
|
+
const [error, setError] = useState(null);
|
|
37
|
+
const [info, setInfo] = useState(null);
|
|
38
|
+
function submit(event) {
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
setError(null);
|
|
41
|
+
setInfo(null);
|
|
42
|
+
startTransition(async () => {
|
|
43
|
+
try {
|
|
44
|
+
if (mode === 'password') {
|
|
45
|
+
const result = await signInWithPassword({ email: email.trim().toLowerCase(), password });
|
|
46
|
+
if (result.error)
|
|
47
|
+
return setError(result.error || copy.invalidCredentials);
|
|
48
|
+
onSignedIn();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const result = await signInWithMagicLink({
|
|
52
|
+
email: email.trim().toLowerCase(),
|
|
53
|
+
callbackURL,
|
|
54
|
+
});
|
|
55
|
+
if (result.error)
|
|
56
|
+
return setError(result.error || copy.invalidCredentials);
|
|
57
|
+
setInfo(copy.magicLinkSent);
|
|
58
|
+
}
|
|
59
|
+
catch (cause) {
|
|
60
|
+
setError(cause instanceof Error && cause.message ? cause.message : copy.invalidCredentials);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return (_jsxs("form", { onSubmit: submit, className: "space-y-4", children: [_jsx("div", { className: "flex gap-1 rounded-md border border-border bg-surface p-1 text-sm", children: ['password', 'magic'].map((candidate) => (_jsx("button", { type: "button", "aria-pressed": mode === candidate, onClick: () => setMode(candidate), className: mode === candidate
|
|
65
|
+
? 'flex-1 rounded bg-bg-subtle px-3 py-1.5 font-medium text-fg'
|
|
66
|
+
: 'flex-1 rounded px-3 py-1.5 text-fg-muted transition-colors hover:bg-surface-hover hover:text-fg', children: candidate === 'password' ? copy.passwordMode : copy.magicLinkMode }, candidate))) }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "auth-email", children: copy.email }), _jsx(Input, { id: "auth-email", type: "email", autoComplete: "email", required: true, value: email, onChange: (event) => setEmail(event.currentTarget.value) })] }), mode === 'password' ? (_jsxs("div", { className: "space-y-1.5", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx(Label, { htmlFor: "auth-password", children: copy.password }), _jsx("a", { href: forgotPasswordHref, className: "text-xs text-primary hover:underline", children: copy.forgotPassword })] }), _jsx(Input, { id: "auth-password", type: "password", autoComplete: "current-password", required: true, value: password, onChange: (event) => setPassword(event.currentTarget.value) })] })) : null, _jsx(AuthMessage, { error: error, info: info }), _jsx(Button, { type: "submit", className: "w-full", disabled: pending, children: pending ? copy.signingIn : mode === 'password' ? copy.signIn : copy.sendMagicLink })] }));
|
|
67
|
+
}
|
|
68
|
+
/** Account-enumeration-safe reset request form. */
|
|
69
|
+
export function ForgotPasswordForm({ requestReset, labels }) {
|
|
70
|
+
const copy = { ...defaults, ...labels };
|
|
71
|
+
const [pending, startTransition] = useTransition();
|
|
72
|
+
const [email, setEmail] = useState('');
|
|
73
|
+
const [sent, setSent] = useState(false);
|
|
74
|
+
function submit(event) {
|
|
75
|
+
event.preventDefault();
|
|
76
|
+
startTransition(async () => {
|
|
77
|
+
try {
|
|
78
|
+
await requestReset(email.trim().toLowerCase());
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Deliberately indistinguishable: never reveal whether the identity exists.
|
|
82
|
+
}
|
|
83
|
+
setSent(true);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return (_jsxs("form", { onSubmit: submit, className: "space-y-4", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "reset-email", children: copy.email }), _jsx(Input, { id: "reset-email", type: "email", autoComplete: "email", required: true, value: email, onChange: (event) => setEmail(event.currentTarget.value) })] }), sent ? (_jsx(Alert, { variant: "success", children: _jsx(AlertDescription, { children: copy.resetSent }) })) : null, _jsx(Button, { type: "submit", className: "w-full", disabled: pending || sent, children: pending ? copy.requestingReset : copy.requestReset })] }));
|
|
87
|
+
}
|
|
88
|
+
/** Token-bound reset form preserving the source validation and recovery states. */
|
|
89
|
+
export function ResetPasswordForm({ token, resetPassword, onReset, minLength = 8, labels, }) {
|
|
90
|
+
const copy = { ...defaults, ...labels };
|
|
91
|
+
const [pending, startTransition] = useTransition();
|
|
92
|
+
const [password, setPassword] = useState('');
|
|
93
|
+
const [confirm, setConfirm] = useState('');
|
|
94
|
+
const [show, setShow] = useState(false);
|
|
95
|
+
const [error, setError] = useState(null);
|
|
96
|
+
function submit(event) {
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
if (!token)
|
|
99
|
+
return setError(copy.invalidResetLink);
|
|
100
|
+
if (password.length < minLength)
|
|
101
|
+
return setError(copy.passwordTooShort);
|
|
102
|
+
if (password !== confirm)
|
|
103
|
+
return setError(copy.passwordsDoNotMatch);
|
|
104
|
+
setError(null);
|
|
105
|
+
startTransition(async () => {
|
|
106
|
+
try {
|
|
107
|
+
const result = await resetPassword({ token, newPassword: password });
|
|
108
|
+
if (result.error)
|
|
109
|
+
return setError(result.error);
|
|
110
|
+
onReset();
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
setError(copy.invalidResetLink);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return (_jsxs("form", { onSubmit: submit, className: "space-y-4", children: [_jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "new-password", children: copy.newPassword }), _jsx(Input, { id: "new-password", type: show ? 'text' : 'password', autoComplete: "new-password", minLength: minLength, required: true, value: password, onChange: (event) => setPassword(event.currentTarget.value) })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx(Label, { htmlFor: "confirm-password", children: copy.confirmPassword }), _jsx(Input, { id: "confirm-password", type: show ? 'text' : 'password', autoComplete: "new-password", minLength: minLength, required: true, value: confirm, onChange: (event) => setConfirm(event.currentTarget.value) })] }), _jsx("button", { type: "button", onClick: () => setShow((value) => !value), className: "text-xs text-fg-muted hover:text-fg hover:underline", children: show ? copy.hidePassword : copy.showPassword }), _jsx(AuthMessage, { error: error, info: null }), _jsx(Button, { type: "submit", className: "w-full", disabled: pending, children: pending ? copy.updatingPassword : copy.updatePassword })] }));
|
|
118
|
+
}
|
|
119
|
+
export function AuthScreen({ logo, title, description, children, footer, }) {
|
|
120
|
+
return (_jsx("main", { className: "grid min-h-screen place-items-center bg-bg px-4", children: _jsxs("div", { className: "w-full max-w-sm space-y-6", children: [_jsxs("div", { className: "text-center", children: [logo ? _jsx("div", { className: "mb-5 flex justify-center", children: logo }) : null, _jsx("h1", { className: "text-xl font-semibold text-fg", children: title }), description ? _jsx("p", { className: "mt-1 text-sm text-fg-muted", children: description }) : null] }), children, footer ? _jsx("div", { className: "text-center text-xs text-fg-muted", children: footer }) : null] }) }));
|
|
121
|
+
}
|
|
122
|
+
function AuthMessage({ error, info }) {
|
|
123
|
+
if (error)
|
|
124
|
+
return _jsx(Alert, { variant: "destructive", children: _jsx(AlertDescription, { children: error }) });
|
|
125
|
+
if (info)
|
|
126
|
+
return _jsx(Alert, { variant: "success", children: _jsx(AlertDescription, { children: info }) });
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=react.js.map
|
package/react.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.js","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAkC,MAAM,OAAO,CAAA;AAC/E,OAAO,EAAE,KAAK,EAAE,gBAAgB,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,4BAA4B,CAAA;AA2B1F,MAAM,QAAQ,GAAG;IACf,YAAY,EAAE,UAAU;IACxB,aAAa,EAAE,YAAY;IAC3B,KAAK,EAAE,OAAO;IACd,QAAQ,EAAE,UAAU;IACpB,cAAc,EAAE,kBAAkB;IAClC,MAAM,EAAE,SAAS;IACjB,aAAa,EAAE,yBAAyB;IACxC,SAAS,EAAE,aAAa;IACxB,aAAa,EAAE,sCAAsC;IACrD,kBAAkB,EAAE,mDAAmD;IACvE,YAAY,EAAE,iBAAiB;IAC/B,eAAe,EAAE,UAAU;IAC3B,SAAS,EAAE,kEAAkE;IAC7E,WAAW,EAAE,cAAc;IAC3B,eAAe,EAAE,kBAAkB;IACnC,YAAY,EAAE,eAAe;IAC7B,YAAY,EAAE,eAAe;IAC7B,cAAc,EAAE,iBAAiB;IACjC,gBAAgB,EAAE,WAAW;IAC7B,gBAAgB,EAAE,yCAAyC;IAC3D,mBAAmB,EAAE,yBAAyB;IAC9C,gBAAgB,EAAE,sEAAsE;CAChF,CAAA;AAYV,gFAAgF;AAChF,MAAM,UAAU,UAAU,CAAC,EACzB,kBAAkB,EAClB,mBAAmB,EACnB,UAAU,EACV,WAAW,GAAG,gBAAgB,EAC9B,kBAAkB,GAAG,kBAAkB,EACvC,WAAW,GAAG,UAAU,EACxB,MAAM,GACU;IAChB,MAAM,IAAI,GAAG,EAAE,GAAG,QAAQ,EAAE,GAAG,MAAM,EAAE,CAAA;IACvC,MAAM,CAAC,OAAO,EAAE,eAAe,CAAC,GAAG,aAAa,EAAE,CAAA;IAClD,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAuB,WAAW,CAAC,CAAA;IACnE,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IACtC,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IAC5C,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAA;IACvD,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAA;IAErD,SAAS,MAAM,CAAC,KAAgB;QAC9B,KAAK,CAAC,cAAc,EAAE,CAAA;QACtB,QAAQ,CAAC,IAAI,CAAC,CAAA;QACd,OAAO,CAAC,IAAI,CAAC,CAAA;QACb,eAAe,CAAC,KAAK,IAAI,EAAE;YACzB,IAAI,CAAC;gBACH,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;oBACxB,MAAM,MAAM,GAAG,MAAM,kBAAkB,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAA;oBACxF,IAAI,MAAM,CAAC,KAAK;wBAAE,OAAO,QAAQ,CAAC,MAAM,CAAC,KAAK,IAAI,IAAI,CAAC,kBAAkB,CAAC,CAAA;oBAC1E,UAAU,EAAE,CAAA;oBACZ,OAAM;gBACR,CAAC;gBACD,MAAM,MAAM,GAAG,MAAM,mBAAmB,CAAC;oBACvC,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE;oBACjC,WAAW;iBACZ,CAAC,CAAA;gBACF,IAAI,MAAM,CAAC,KAAK;oBAAE,OAAO,QAAQ,CAAC,MAAM,CAAC,KAAK,IAAI,IAAI,CAAC,kBAAkB,CAAC,CAAA;gBAC1E,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;YAC7B,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,QAAQ,CAAC,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAA;YAC7F,CAAC;QACH,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,OAAO,CACL,gBAAM,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAC,WAAW,aAC3C,cAAK,SAAS,EAAC,mEAAmE,YAC9E,CAAC,UAAU,EAAE,OAAO,CAAW,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CACnD,iBAEE,IAAI,EAAC,QAAQ,kBACC,IAAI,KAAK,SAAS,EAChC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,EACjC,SAAS,EACP,IAAI,KAAK,SAAS;wBAChB,CAAC,CAAC,6DAA6D;wBAC/D,CAAC,CAAC,iGAAiG,YAGtG,SAAS,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,IAV7D,SAAS,CAWP,CACV,CAAC,GACE,EACN,eAAK,SAAS,EAAC,aAAa,aAC1B,KAAC,KAAK,IAAC,OAAO,EAAC,YAAY,YAAE,IAAI,CAAC,KAAK,GAAS,EAChD,KAAC,KAAK,IACJ,EAAE,EAAC,YAAY,EACf,IAAI,EAAC,OAAO,EACZ,YAAY,EAAC,OAAO,EACpB,QAAQ,QACR,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,GACxD,IACE,EACL,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,CACrB,eAAK,SAAS,EAAC,aAAa,aAC1B,eAAK,SAAS,EAAC,mCAAmC,aAChD,KAAC,KAAK,IAAC,OAAO,EAAC,eAAe,YAAE,IAAI,CAAC,QAAQ,GAAS,EACtD,YAAG,IAAI,EAAE,kBAAkB,EAAE,SAAS,EAAC,sCAAsC,YAC1E,IAAI,CAAC,cAAc,GAClB,IACA,EACN,KAAC,KAAK,IACJ,EAAE,EAAC,eAAe,EAClB,IAAI,EAAC,UAAU,EACf,YAAY,EAAC,kBAAkB,EAC/B,QAAQ,QACR,KAAK,EAAE,QAAQ,EACf,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,GAC3D,IACE,CACP,CAAC,CAAC,CAAC,IAAI,EACR,KAAC,WAAW,IAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,GAAI,EACzC,KAAC,MAAM,IAAC,IAAI,EAAC,QAAQ,EAAC,SAAS,EAAC,QAAQ,EAAC,QAAQ,EAAE,OAAO,YACvD,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,GAC3E,IACJ,CACR,CAAA;AACH,CAAC;AAOD,mDAAmD;AACnD,MAAM,UAAU,kBAAkB,CAAC,EAAE,YAAY,EAAE,MAAM,EAA2B;IAClF,MAAM,IAAI,GAAG,EAAE,GAAG,QAAQ,EAAE,GAAG,MAAM,EAAE,CAAA;IACvC,MAAM,CAAC,OAAO,EAAE,eAAe,CAAC,GAAG,aAAa,EAAE,CAAA;IAClD,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IACtC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACvC,SAAS,MAAM,CAAC,KAAgB;QAC9B,KAAK,CAAC,cAAc,EAAE,CAAA;QACtB,eAAe,CAAC,KAAK,IAAI,EAAE;YACzB,IAAI,CAAC;gBACH,MAAM,YAAY,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAA;YAChD,CAAC;YAAC,MAAM,CAAC;gBACP,4EAA4E;YAC9E,CAAC;YACD,OAAO,CAAC,IAAI,CAAC,CAAA;QACf,CAAC,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,CACL,gBAAM,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAC,WAAW,aAC3C,eAAK,SAAS,EAAC,aAAa,aAC1B,KAAC,KAAK,IAAC,OAAO,EAAC,aAAa,YAAE,IAAI,CAAC,KAAK,GAAS,EACjD,KAAC,KAAK,IACJ,EAAE,EAAC,aAAa,EAChB,IAAI,EAAC,OAAO,EACZ,YAAY,EAAC,OAAO,EACpB,QAAQ,QACR,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,GACxD,IACE,EACL,IAAI,CAAC,CAAC,CAAC,CACN,KAAC,KAAK,IAAC,OAAO,EAAC,SAAS,YAAC,KAAC,gBAAgB,cAAE,IAAI,CAAC,SAAS,GAAoB,GAAQ,CACvF,CAAC,CAAC,CAAC,IAAI,EACR,KAAC,MAAM,IAAC,IAAI,EAAC,QAAQ,EAAC,SAAS,EAAC,QAAQ,EAAC,QAAQ,EAAE,OAAO,IAAI,IAAI,YAC/D,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,GAC5C,IACJ,CACR,CAAA;AACH,CAAC;AAUD,mFAAmF;AACnF,MAAM,UAAU,iBAAiB,CAAC,EAChC,KAAK,EACL,aAAa,EACb,OAAO,EACP,SAAS,GAAG,CAAC,EACb,MAAM,GACiB;IACvB,MAAM,IAAI,GAAG,EAAE,GAAG,QAAQ,EAAE,GAAG,MAAM,EAAE,CAAA;IACvC,MAAM,CAAC,OAAO,EAAE,eAAe,CAAC,GAAG,aAAa,EAAE,CAAA;IAClD,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IAC5C,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IAC1C,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACvC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAA;IACvD,SAAS,MAAM,CAAC,KAAgB;QAC9B,KAAK,CAAC,cAAc,EAAE,CAAA;QACtB,IAAI,CAAC,KAAK;YAAE,OAAO,QAAQ,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAA;QAClD,IAAI,QAAQ,CAAC,MAAM,GAAG,SAAS;YAAE,OAAO,QAAQ,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAA;QACvE,IAAI,QAAQ,KAAK,OAAO;YAAE,OAAO,QAAQ,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAA;QACnE,QAAQ,CAAC,IAAI,CAAC,CAAA;QACd,eAAe,CAAC,KAAK,IAAI,EAAE;YACzB,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC,CAAA;gBACpE,IAAI,MAAM,CAAC,KAAK;oBAAE,OAAO,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;gBAC/C,OAAO,EAAE,CAAA;YACX,CAAC;YAAC,MAAM,CAAC;gBACP,QAAQ,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAA;YACjC,CAAC;QACH,CAAC,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,CACL,gBAAM,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAC,WAAW,aAC3C,eAAK,SAAS,EAAC,aAAa,aAC1B,KAAC,KAAK,IAAC,OAAO,EAAC,cAAc,YAAE,IAAI,CAAC,WAAW,GAAS,EACxD,KAAC,KAAK,IAAC,EAAE,EAAC,cAAc,EAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,EAAE,YAAY,EAAC,cAAc,EAAC,SAAS,EAAE,SAAS,EAAE,QAAQ,QAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,GAAI,IAClM,EACN,eAAK,SAAS,EAAC,aAAa,aAC1B,KAAC,KAAK,IAAC,OAAO,EAAC,kBAAkB,YAAE,IAAI,CAAC,eAAe,GAAS,EAChE,KAAC,KAAK,IAAC,EAAE,EAAC,kBAAkB,EAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,EAAE,YAAY,EAAC,cAAc,EAAC,SAAS,EAAE,SAAS,EAAE,QAAQ,QAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,GAAI,IACpM,EACN,iBAAQ,IAAI,EAAC,QAAQ,EAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,EAAE,SAAS,EAAC,qDAAqD,YAC7H,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,GACtC,EACT,KAAC,WAAW,IAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,GAAI,EACzC,KAAC,MAAM,IAAC,IAAI,EAAC,QAAQ,EAAC,SAAS,EAAC,QAAQ,EAAC,QAAQ,EAAE,OAAO,YACvD,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,GAC/C,IACJ,CACR,CAAA;AACH,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,EACzB,IAAI,EACJ,KAAK,EACL,WAAW,EACX,QAAQ,EACR,MAAM,GAOP;IACC,OAAO,CACL,eAAM,SAAS,EAAC,iDAAiD,YAC/D,eAAK,SAAS,EAAC,2BAA2B,aACxC,eAAK,SAAS,EAAC,aAAa,aACzB,IAAI,CAAC,CAAC,CAAC,cAAK,SAAS,EAAC,0BAA0B,YAAE,IAAI,GAAO,CAAC,CAAC,CAAC,IAAI,EACrE,aAAI,SAAS,EAAC,+BAA+B,YAAE,KAAK,GAAM,EACzD,WAAW,CAAC,CAAC,CAAC,YAAG,SAAS,EAAC,4BAA4B,YAAE,WAAW,GAAK,CAAC,CAAC,CAAC,IAAI,IAC7E,EACL,QAAQ,EACR,MAAM,CAAC,CAAC,CAAC,cAAK,SAAS,EAAC,mCAAmC,YAAE,MAAM,GAAO,CAAC,CAAC,CAAC,IAAI,IAC9E,GACD,CACR,CAAA;AACH,CAAC;AAED,SAAS,WAAW,CAAC,EAAE,KAAK,EAAE,IAAI,EAAiD;IACjF,IAAI,KAAK;QAAE,OAAO,KAAC,KAAK,IAAC,OAAO,EAAC,aAAa,YAAC,KAAC,gBAAgB,cAAE,KAAK,GAAoB,GAAQ,CAAA;IACnG,IAAI,IAAI;QAAE,OAAO,KAAC,KAAK,IAAC,OAAO,EAAC,SAAS,YAAC,KAAC,gBAAgB,cAAE,IAAI,GAAoB,GAAQ,CAAA;IAC7F,OAAO,IAAI,CAAA;AACb,CAAC","sourcesContent":["'use client'\n\nimport { useState, useTransition, type FormEvent, type ReactNode } from 'react'\nimport { Alert, AlertDescription, Button, Input, Label } from '@braedonsaunders/appkit-ui'\n\nexport type AuthFormLabels = Partial<{\n passwordMode: string\n magicLinkMode: string\n email: string\n password: string\n forgotPassword: string\n signIn: string\n sendMagicLink: string\n signingIn: string\n magicLinkSent: string\n invalidCredentials: string\n requestReset: string\n requestingReset: string\n resetSent: string\n newPassword: string\n confirmPassword: string\n showPassword: string\n hidePassword: string\n updatePassword: string\n updatingPassword: string\n passwordTooShort: string\n passwordsDoNotMatch: string\n invalidResetLink: string\n}>\n\nconst defaults = {\n passwordMode: 'Password',\n magicLinkMode: 'Email link',\n email: 'Email',\n password: 'Password',\n forgotPassword: 'Forgot password?',\n signIn: 'Sign in',\n sendMagicLink: 'Email me a sign-in link',\n signingIn: 'Signing in…',\n magicLinkSent: 'Check your email for a sign-in link.',\n invalidCredentials: 'Sign-in failed. Check your details and try again.',\n requestReset: 'Send reset link',\n requestingReset: 'Sending…',\n resetSent: 'If an account exists for that email, a reset link is on its way.',\n newPassword: 'New password',\n confirmPassword: 'Confirm password',\n showPassword: 'Show password',\n hidePassword: 'Hide password',\n updatePassword: 'Update password',\n updatingPassword: 'Updating…',\n passwordTooShort: 'Password must be at least 8 characters.',\n passwordsDoNotMatch: 'Passwords do not match.',\n invalidResetLink: 'This reset link has expired or already been used. Request a new one.',\n} as const\n\nexport type SignInFormProps = {\n signInWithPassword: (input: { email: string; password: string }) => Promise<{ error?: string | null }>\n signInWithMagicLink: (input: { email: string; callbackURL: string }) => Promise<{ error?: string | null }>\n onSignedIn: () => void\n callbackURL?: string\n forgotPasswordHref?: string\n defaultMode?: 'password' | 'magic'\n labels?: AuthFormLabels\n}\n\n/** Source-parity password/magic-link sign-in form with app-owned navigation. */\nexport function SignInForm({\n signInWithPassword,\n signInWithMagicLink,\n onSignedIn,\n callbackURL = '/auth/continue',\n forgotPasswordHref = '/forgot-password',\n defaultMode = 'password',\n labels,\n}: SignInFormProps) {\n const copy = { ...defaults, ...labels }\n const [pending, startTransition] = useTransition()\n const [mode, setMode] = useState<'password' | 'magic'>(defaultMode)\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [error, setError] = useState<string | null>(null)\n const [info, setInfo] = useState<string | null>(null)\n\n function submit(event: FormEvent) {\n event.preventDefault()\n setError(null)\n setInfo(null)\n startTransition(async () => {\n try {\n if (mode === 'password') {\n const result = await signInWithPassword({ email: email.trim().toLowerCase(), password })\n if (result.error) return setError(result.error || copy.invalidCredentials)\n onSignedIn()\n return\n }\n const result = await signInWithMagicLink({\n email: email.trim().toLowerCase(),\n callbackURL,\n })\n if (result.error) return setError(result.error || copy.invalidCredentials)\n setInfo(copy.magicLinkSent)\n } catch (cause) {\n setError(cause instanceof Error && cause.message ? cause.message : copy.invalidCredentials)\n }\n })\n }\n\n return (\n <form onSubmit={submit} className=\"space-y-4\">\n <div className=\"flex gap-1 rounded-md border border-border bg-surface p-1 text-sm\">\n {(['password', 'magic'] as const).map((candidate) => (\n <button\n key={candidate}\n type=\"button\"\n aria-pressed={mode === candidate}\n onClick={() => setMode(candidate)}\n className={\n mode === candidate\n ? 'flex-1 rounded bg-bg-subtle px-3 py-1.5 font-medium text-fg'\n : 'flex-1 rounded px-3 py-1.5 text-fg-muted transition-colors hover:bg-surface-hover hover:text-fg'\n }\n >\n {candidate === 'password' ? copy.passwordMode : copy.magicLinkMode}\n </button>\n ))}\n </div>\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"auth-email\">{copy.email}</Label>\n <Input\n id=\"auth-email\"\n type=\"email\"\n autoComplete=\"email\"\n required\n value={email}\n onChange={(event) => setEmail(event.currentTarget.value)}\n />\n </div>\n {mode === 'password' ? (\n <div className=\"space-y-1.5\">\n <div className=\"flex items-center justify-between\">\n <Label htmlFor=\"auth-password\">{copy.password}</Label>\n <a href={forgotPasswordHref} className=\"text-xs text-primary hover:underline\">\n {copy.forgotPassword}\n </a>\n </div>\n <Input\n id=\"auth-password\"\n type=\"password\"\n autoComplete=\"current-password\"\n required\n value={password}\n onChange={(event) => setPassword(event.currentTarget.value)}\n />\n </div>\n ) : null}\n <AuthMessage error={error} info={info} />\n <Button type=\"submit\" className=\"w-full\" disabled={pending}>\n {pending ? copy.signingIn : mode === 'password' ? copy.signIn : copy.sendMagicLink}\n </Button>\n </form>\n )\n}\n\nexport type ForgotPasswordFormProps = {\n requestReset: (email: string) => Promise<void>\n labels?: AuthFormLabels\n}\n\n/** Account-enumeration-safe reset request form. */\nexport function ForgotPasswordForm({ requestReset, labels }: ForgotPasswordFormProps) {\n const copy = { ...defaults, ...labels }\n const [pending, startTransition] = useTransition()\n const [email, setEmail] = useState('')\n const [sent, setSent] = useState(false)\n function submit(event: FormEvent) {\n event.preventDefault()\n startTransition(async () => {\n try {\n await requestReset(email.trim().toLowerCase())\n } catch {\n // Deliberately indistinguishable: never reveal whether the identity exists.\n }\n setSent(true)\n })\n }\n return (\n <form onSubmit={submit} className=\"space-y-4\">\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"reset-email\">{copy.email}</Label>\n <Input\n id=\"reset-email\"\n type=\"email\"\n autoComplete=\"email\"\n required\n value={email}\n onChange={(event) => setEmail(event.currentTarget.value)}\n />\n </div>\n {sent ? (\n <Alert variant=\"success\"><AlertDescription>{copy.resetSent}</AlertDescription></Alert>\n ) : null}\n <Button type=\"submit\" className=\"w-full\" disabled={pending || sent}>\n {pending ? copy.requestingReset : copy.requestReset}\n </Button>\n </form>\n )\n}\n\nexport type ResetPasswordFormProps = {\n token: string\n resetPassword: (input: { token: string; newPassword: string }) => Promise<{ error?: string | null }>\n onReset: () => void\n minLength?: number\n labels?: AuthFormLabels\n}\n\n/** Token-bound reset form preserving the source validation and recovery states. */\nexport function ResetPasswordForm({\n token,\n resetPassword,\n onReset,\n minLength = 8,\n labels,\n}: ResetPasswordFormProps) {\n const copy = { ...defaults, ...labels }\n const [pending, startTransition] = useTransition()\n const [password, setPassword] = useState('')\n const [confirm, setConfirm] = useState('')\n const [show, setShow] = useState(false)\n const [error, setError] = useState<string | null>(null)\n function submit(event: FormEvent) {\n event.preventDefault()\n if (!token) return setError(copy.invalidResetLink)\n if (password.length < minLength) return setError(copy.passwordTooShort)\n if (password !== confirm) return setError(copy.passwordsDoNotMatch)\n setError(null)\n startTransition(async () => {\n try {\n const result = await resetPassword({ token, newPassword: password })\n if (result.error) return setError(result.error)\n onReset()\n } catch {\n setError(copy.invalidResetLink)\n }\n })\n }\n return (\n <form onSubmit={submit} className=\"space-y-4\">\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"new-password\">{copy.newPassword}</Label>\n <Input id=\"new-password\" type={show ? 'text' : 'password'} autoComplete=\"new-password\" minLength={minLength} required value={password} onChange={(event) => setPassword(event.currentTarget.value)} />\n </div>\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"confirm-password\">{copy.confirmPassword}</Label>\n <Input id=\"confirm-password\" type={show ? 'text' : 'password'} autoComplete=\"new-password\" minLength={minLength} required value={confirm} onChange={(event) => setConfirm(event.currentTarget.value)} />\n </div>\n <button type=\"button\" onClick={() => setShow((value) => !value)} className=\"text-xs text-fg-muted hover:text-fg hover:underline\">\n {show ? copy.hidePassword : copy.showPassword}\n </button>\n <AuthMessage error={error} info={null} />\n <Button type=\"submit\" className=\"w-full\" disabled={pending}>\n {pending ? copy.updatingPassword : copy.updatePassword}\n </Button>\n </form>\n )\n}\n\nexport function AuthScreen({\n logo,\n title,\n description,\n children,\n footer,\n}: {\n logo?: ReactNode\n title: ReactNode\n description?: ReactNode\n children: ReactNode\n footer?: ReactNode\n}) {\n return (\n <main className=\"grid min-h-screen place-items-center bg-bg px-4\">\n <div className=\"w-full max-w-sm space-y-6\">\n <div className=\"text-center\">\n {logo ? <div className=\"mb-5 flex justify-center\">{logo}</div> : null}\n <h1 className=\"text-xl font-semibold text-fg\">{title}</h1>\n {description ? <p className=\"mt-1 text-sm text-fg-muted\">{description}</p> : null}\n </div>\n {children}\n {footer ? <div className=\"text-center text-xs text-fg-muted\">{footer}</div> : null}\n </div>\n </main>\n )\n}\n\nfunction AuthMessage({ error, info }: { error: string | null; info: string | null }) {\n if (error) return <Alert variant=\"destructive\"><AlertDescription>{error}</AlertDescription></Alert>\n if (info) return <Alert variant=\"success\"><AlertDescription>{info}</AlertDescription></Alert>\n return null\n}\n"]}
|