@storyshelf/app 0.6.0 → 0.6.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/routers/auth.mjs
CHANGED
|
@@ -91,6 +91,7 @@ async function finishCredentialLogin(c, engine, email, password) {
|
|
|
91
91
|
if (res.status !== 200) {
|
|
92
92
|
if (res.status === 429) return renderEngineLogin(c, engine, "Invalid credentials", email, 429);
|
|
93
93
|
if (res.status >= 500) return renderEngineLogin(c, engine, "Invalid credentials", email, 502);
|
|
94
|
+
if (res.status === 400) return renderEngineLogin(c, engine, "Enter a valid email address", email, 400);
|
|
94
95
|
await notifySystem("sys:auth-failed", { email });
|
|
95
96
|
return renderEngineLogin(c, engine, "Invalid credentials", email, 401);
|
|
96
97
|
}
|
|
@@ -219,6 +220,7 @@ function registerEngineAuth(app, engine) {
|
|
|
219
220
|
if (sole?.kind === "oauth" || sole?.kind === "sso") return c.redirect(`/auth/engine/${sole.id}`);
|
|
220
221
|
return await renderEngineLogin(c, engine);
|
|
221
222
|
});
|
|
223
|
+
app.get("/auth/engine/login", (c) => c.redirect("/auth/login"));
|
|
222
224
|
app.post("/auth/engine/login", async (c) => await handleEngineLogin(c, engine));
|
|
223
225
|
app.get("/auth/engine/:providerId", async (c) => await handleEngineStart(c, engine));
|
|
224
226
|
app.get("/auth/invites/:inviteId", async (c) => await handleInvitePage(c, engine));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.mjs","names":[],"sources":["../../src/routers/auth.ts"],"sourcesContent":["import type { Auth, EngineLoginMethod } from \"@storyshelf/auth\";\nimport { MIN_PASSWORD_LENGTH } from \"@storyshelf/auth\";\nimport { UserModel } from \"@storyshelf/core/models\";\nimport { SESSION_COOKIE } from \"@storyshelf/core/types\";\nimport type { Context } from \"hono\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { notifySystem } from \"../notify.ts\";\nimport { renderInviteInvalidPage, renderInvitePage } from \"../pages/invite.tsx\";\nimport { renderLoginPage } from \"../pages/login.tsx\";\nimport { getStore } from \"../store.ts\";\nimport { syncLoginMemberships } from \"./auth-sync.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\n\n/** Provider ids are URL path segments: strict charset, bounded length. */\nfunction validProviderId(value: string): boolean {\n return /^[a-zA-Z0-9_-]{1,64}$/u.test(value);\n}\n\nconst ENGINE_PASSWORD_ACTION = \"/auth/engine/login\";\n\n/** Forward a request to the mounted engine, preserving origin and cookies. */\nexport async function forwardToEngine(\n c: Context,\n engine: Auth,\n path: string,\n body?: unknown,\n): Promise<Response> {\n return await forwardEngineRequest(c, engine, path, \"POST\", body);\n}\n\nasync function forwardEngineRequest(\n c: Context,\n engine: Auth,\n path: string,\n method: \"GET\" | \"POST\",\n body?: unknown,\n): Promise<Response> {\n const headers = new Headers();\n for (const name of [\"cookie\", \"origin\", \"referer\", \"user-agent\"]) {\n const value = c.req.header(name);\n if (value) {\n headers.set(name, value);\n }\n }\n if (method === \"POST\") {\n headers.set(\"content-type\", \"application/json\");\n }\n return await engine.handler(\n new Request(new URL(path, c.req.url), {\n method,\n headers,\n ...(method === \"POST\" ? { body: JSON.stringify(body ?? {}) } : {}),\n }),\n );\n}\n\nfunction copySetCookies(c: Context, res: Response): void {\n const cookies =\n typeof res.headers.getSetCookie === \"function\"\n ? res.headers.getSetCookie()\n : splitSetCookie(res.headers.get(\"set-cookie\"));\n for (const cookie of cookies) {\n c.header(\"set-cookie\", cookie, { append: true });\n }\n}\n\n/** Split a joined Set-Cookie header (runtimes without getSetCookie). */\nfunction splitSetCookie(header: string | null): string[] {\n if (!header) {\n return [];\n }\n // Split on commas starting a new `name=` pair; Expires dates (\"Wed, 21\")\n // never match (no `=` after the token). Fallback only — Node has getSetCookie.\n return header\n .split(/,\\s*(?=[^;\\s=]+=)/u)\n .map((part) => part.trim())\n .filter((part) => part !== \"\");\n}\n\nfunction loginLinks(\n methods: EngineLoginMethod[],\n): { kind: \"password\" | \"oauth\" | \"passkey\" | \"sso\"; id: string; label: string; url: string }[] {\n return methods.map((method) => ({\n ...method,\n // Passkey sign-in is JS-driven on the page; the url is an unused fallback.\n url:\n method.kind === \"oauth\" || method.kind === \"sso\"\n ? `/auth/engine/${method.id}`\n : ENGINE_PASSWORD_ACTION,\n }));\n}\n\nasync function renderEngineLogin(\n c: Context,\n engine: Auth,\n error?: string,\n email?: string,\n status?: 200 | 401 | 429 | 500 | 502,\n): Promise<Response> {\n return c.html(\n await renderLoginPage({\n error,\n email,\n engineMethods: loginLinks(engine.loginMethods()),\n }),\n status ?? 200,\n );\n}\n\n/** Membership sync for first-seen engine users only (never demote existing roles). */\nasync function syncEngineUser(user: { id: string; email: string; name: string }): Promise<void> {\n const existing = await new UserModel(getStore().db).get(user.id);\n if (existing) {\n return;\n }\n await syncLoginMemberships(\n getStore().db,\n {\n users: getStore().db.tables.users,\n projects: getStore().db.tables.projects,\n projectMembers: getStore().db.tables.projectMembers,\n projectGroupMappings: getStore().db.tables.projectGroupMappings,\n },\n { ...user, role: \"member\" },\n );\n}\n\nasync function readCredentials(c: Context): Promise<{ email: string; password: string }> {\n const form = await c.req.formData();\n const rawEmail = form.get(\"email\");\n const rawPassword = form.get(\"password\");\n return {\n email: typeof rawEmail === \"string\" ? rawEmail : \"\",\n password: typeof rawPassword === \"string\" ? rawPassword : \"\",\n };\n}\n\nasync function finishCredentialLogin(\n c: Context,\n engine: Auth,\n email: string,\n password: string,\n): Promise<Response> {\n const res = await forwardToEngine(c, engine, \"/api/auth/sign-in/email\", { email, password });\n if (res.status !== 200) {\n // Generic message either way (no user enumeration), but propagate\n // rate-limit/upstream statuses instead of masking them as 401.\n if (res.status === 429) {\n return renderEngineLogin(c, engine, \"Invalid credentials\", email, 429);\n }\n if (res.status >= 500) {\n return renderEngineLogin(c, engine, \"Invalid credentials\", email, 502);\n }\n // Credential failures only (never rate-limit/upstream noise, never\n // secrets): admins opted into sys:* see a login-attempt signal. Spray\n // attempts can fan out here; the /auth/* rate-limit bucket bounds it.\n await notifySystem(\"sys:auth-failed\", { email });\n return renderEngineLogin(c, engine, \"Invalid credentials\", email, 401);\n }\n copySetCookies(c, res);\n const body = (await res.json()) as { user: { id: string; email: string; name: string } };\n await syncEngineUser(body.user);\n return hxRedirect(c, \"/\");\n}\n\nasync function handleEngineLogin(c: Context, engine: Auth): Promise<Response> {\n const { email, password } = await readCredentials(c);\n return finishCredentialLogin(c, engine, email, password);\n}\n\nfunction resolveEngineMethod(c: Context, engine: Auth): EngineLoginMethod | null {\n const id = c.req.param(\"providerId\") ?? \"\";\n if (!validProviderId(id)) {\n throw new HTTPException(400, { message: \"Unknown provider\" });\n }\n return (\n engine.loginMethods().find((entry) => entry.id === id && entry.kind !== \"password\") ?? null\n );\n}\n\nasync function finishOAuthStart(c: Context, engine: Auth, id: string): Promise<Response> {\n const res = await forwardToEngine(c, engine, \"/api/auth/sign-in/social\", {\n provider: id,\n callbackURL: \"/\",\n disableRedirect: true,\n });\n if (res.status !== 200) {\n throw new HTTPException(502, { message: \"Sign in failed\" });\n }\n const body = (await res.json()) as { url?: string };\n if (!body.url) {\n throw new HTTPException(502, { message: \"Sign in failed\" });\n }\n copySetCookies(c, res);\n return c.redirect(body.url);\n}\n\nasync function finishSSOStart(c: Context, engine: Auth, id: string): Promise<Response> {\n const res = await forwardToEngine(c, engine, \"/api/auth/sign-in/sso\", {\n providerId: id,\n callbackURL: \"/\",\n });\n if (res.status !== 200) {\n throw new HTTPException(502, { message: \"Sign in failed\" });\n }\n const body = (await res.json()) as { url?: string };\n if (!body.url) {\n throw new HTTPException(502, { message: \"Sign in failed\" });\n }\n copySetCookies(c, res);\n return c.redirect(body.url);\n}\n\nasync function handleEngineStart(c: Context, engine: Auth): Promise<Response> {\n const method = resolveEngineMethod(c, engine);\n if (!method || method.kind === \"passkey\") {\n return c.notFound();\n }\n if (method.kind === \"sso\") {\n return await finishSSOStart(c, engine, method.id);\n }\n return await finishOAuthStart(c, engine, method.id);\n}\n\nasync function handleInvitePage(c: Context, engine: Auth): Promise<Response> {\n const inviteId = c.req.param(\"inviteId\") ?? \"\";\n const token = c.req.query(\"token\") ?? \"\";\n // The invite token renders into the page: never store.\n c.header(\"Cache-Control\", \"no-store\");\n if (!inviteId || !token) {\n return c.html(renderInviteInvalidPage(\"Missing invite link parameters\"), 400);\n }\n try {\n const { email, name } = await engine.verifyInvite({ inviteId, token });\n return c.html(await renderInvitePage({ inviteId, token, email, name }));\n } catch {\n return c.html(renderInviteInvalidPage(\"Invalid or expired invite\"), 400);\n }\n}\n\nasync function readInviteForm(c: Context): Promise<{\n inviteId: string;\n token: string;\n password: string;\n confirm: string;\n}> {\n const form = await c.req.formData();\n return {\n inviteId: c.req.param(\"inviteId\") ?? \"\",\n token: formText(form.get(\"token\")),\n password: formText(form.get(\"password\")),\n confirm: formText(form.get(\"confirm\")),\n };\n}\n\nfunction formText(value: unknown): string {\n return typeof value === \"string\" ? value : \"\";\n}\n\nasync function finishInviteAccept(\n c: Context,\n engine: Auth,\n inviteId: string,\n token: string,\n password: string,\n): Promise<Response> {\n const user = await engine.acceptInvite({ inviteId, token, password });\n const res = await forwardToEngine(c, engine, \"/api/auth/sign-in/email\", {\n email: user.email,\n password,\n });\n if (res.status !== 200) {\n throw new Error(\"Sign in failed\");\n }\n copySetCookies(c, res);\n await syncEngineUser({ id: user.id, email: user.email, name: user.name });\n return c.redirect(\"/profile\");\n}\n\nasync function inviteAcceptError(\n c: Context,\n inviteId: string,\n token: string,\n error: unknown,\n): Promise<Response> {\n const message = error instanceof Error ? error.message : \"Invalid or expired invite\";\n if (message.includes(`at least ${MIN_PASSWORD_LENGTH}`)) {\n return c.html(await renderInvitePage({ inviteId, token, error: message }), 400);\n }\n return c.html(renderInviteInvalidPage(message), 400);\n}\n\nasync function handleInviteAccept(c: Context, engine: Auth): Promise<Response> {\n const { inviteId, token, password, confirm } = await readInviteForm(c);\n if (!inviteId || !token) {\n return c.html(renderInviteInvalidPage(\"Missing invite link parameters\"), 400);\n }\n if (password !== confirm) {\n return c.html(\n await renderInvitePage({ inviteId, token, error: \"Passwords do not match\" }),\n 400,\n );\n }\n try {\n return await finishInviteAccept(c, engine, inviteId, token, password);\n } catch (error) {\n return await inviteAcceptError(c, inviteId, token, error);\n }\n}\n\n/** Register the engine mount and descriptor-driven login routes. */\nexport function registerEngineAuth(app: ShelfRouter, engine: Auth): void {\n app.all(\"/api/auth/*\", async (c) => await engine.handler(c.req.raw));\n\n app.get(\"/auth/login\", async (c) => {\n const methods = engine.loginMethods();\n const sole = methods.length === 1 ? methods.at(0) : undefined;\n if (sole?.kind === \"oauth\" || sole?.kind === \"sso\") {\n return c.redirect(`/auth/engine/${sole.id}`);\n }\n return await renderEngineLogin(c, engine);\n });\n\n app.post(\"/auth/engine/login\", async (c) => await handleEngineLogin(c, engine));\n\n app.get(\"/auth/engine/:providerId\", async (c) => await handleEngineStart(c, engine));\n\n app.get(\"/auth/invites/:inviteId\", async (c) => await handleInvitePage(c, engine));\n\n app.post(\"/auth/invites/:inviteId\", async (c) => await handleInviteAccept(c, engine));\n\n app.post(\"/auth/logout\", async (c) => {\n const res = await forwardToEngine(c, engine, \"/api/auth/sign-out\", {});\n copySetCookies(c, res);\n // Mirror the Secure attribute or the clear-cookie misses Secure session\n // cookies over HTTPS (behind TLS-terminating proxies check forwarded proto).\n const secure =\n new URL(c.req.url).protocol === \"https:\" || c.req.header(\"x-forwarded-proto\") === \"https\";\n c.header(\n \"set-cookie\",\n `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0${secure ? \"; Secure\" : \"\"}`,\n { append: true },\n );\n return hxRedirect(c, \"/auth/login\");\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"auth.mjs","names":[],"sources":["../../src/routers/auth.ts"],"sourcesContent":["import type { Auth, EngineLoginMethod } from \"@storyshelf/auth\";\nimport { MIN_PASSWORD_LENGTH } from \"@storyshelf/auth\";\nimport { UserModel } from \"@storyshelf/core/models\";\nimport { SESSION_COOKIE } from \"@storyshelf/core/types\";\nimport type { Context } from \"hono\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { notifySystem } from \"../notify.ts\";\nimport { renderInviteInvalidPage, renderInvitePage } from \"../pages/invite.tsx\";\nimport { renderLoginPage } from \"../pages/login.tsx\";\nimport { getStore } from \"../store.ts\";\nimport { syncLoginMemberships } from \"./auth-sync.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\n\n/** Provider ids are URL path segments: strict charset, bounded length. */\nfunction validProviderId(value: string): boolean {\n return /^[a-zA-Z0-9_-]{1,64}$/u.test(value);\n}\n\nconst ENGINE_PASSWORD_ACTION = \"/auth/engine/login\";\n\n/** Forward a request to the mounted engine, preserving origin and cookies. */\nexport async function forwardToEngine(\n c: Context,\n engine: Auth,\n path: string,\n body?: unknown,\n): Promise<Response> {\n return await forwardEngineRequest(c, engine, path, \"POST\", body);\n}\n\nasync function forwardEngineRequest(\n c: Context,\n engine: Auth,\n path: string,\n method: \"GET\" | \"POST\",\n body?: unknown,\n): Promise<Response> {\n const headers = new Headers();\n for (const name of [\"cookie\", \"origin\", \"referer\", \"user-agent\"]) {\n const value = c.req.header(name);\n if (value) {\n headers.set(name, value);\n }\n }\n if (method === \"POST\") {\n headers.set(\"content-type\", \"application/json\");\n }\n return await engine.handler(\n new Request(new URL(path, c.req.url), {\n method,\n headers,\n ...(method === \"POST\" ? { body: JSON.stringify(body ?? {}) } : {}),\n }),\n );\n}\n\nfunction copySetCookies(c: Context, res: Response): void {\n const cookies =\n typeof res.headers.getSetCookie === \"function\"\n ? res.headers.getSetCookie()\n : splitSetCookie(res.headers.get(\"set-cookie\"));\n for (const cookie of cookies) {\n c.header(\"set-cookie\", cookie, { append: true });\n }\n}\n\n/** Split a joined Set-Cookie header (runtimes without getSetCookie). */\nfunction splitSetCookie(header: string | null): string[] {\n if (!header) {\n return [];\n }\n // Split on commas starting a new `name=` pair; Expires dates (\"Wed, 21\")\n // never match (no `=` after the token). Fallback only — Node has getSetCookie.\n return header\n .split(/,\\s*(?=[^;\\s=]+=)/u)\n .map((part) => part.trim())\n .filter((part) => part !== \"\");\n}\n\nfunction loginLinks(\n methods: EngineLoginMethod[],\n): { kind: \"password\" | \"oauth\" | \"passkey\" | \"sso\"; id: string; label: string; url: string }[] {\n return methods.map((method) => ({\n ...method,\n // Passkey sign-in is JS-driven on the page; the url is an unused fallback.\n url:\n method.kind === \"oauth\" || method.kind === \"sso\"\n ? `/auth/engine/${method.id}`\n : ENGINE_PASSWORD_ACTION,\n }));\n}\n\nasync function renderEngineLogin(\n c: Context,\n engine: Auth,\n error?: string,\n email?: string,\n status?: 200 | 400 | 401 | 429 | 500 | 502,\n): Promise<Response> {\n return c.html(\n await renderLoginPage({\n error,\n email,\n engineMethods: loginLinks(engine.loginMethods()),\n }),\n status ?? 200,\n );\n}\n\n/** Membership sync for first-seen engine users only (never demote existing roles). */\nasync function syncEngineUser(user: { id: string; email: string; name: string }): Promise<void> {\n const existing = await new UserModel(getStore().db).get(user.id);\n if (existing) {\n return;\n }\n await syncLoginMemberships(\n getStore().db,\n {\n users: getStore().db.tables.users,\n projects: getStore().db.tables.projects,\n projectMembers: getStore().db.tables.projectMembers,\n projectGroupMappings: getStore().db.tables.projectGroupMappings,\n },\n { ...user, role: \"member\" },\n );\n}\n\nasync function readCredentials(c: Context): Promise<{ email: string; password: string }> {\n const form = await c.req.formData();\n const rawEmail = form.get(\"email\");\n const rawPassword = form.get(\"password\");\n return {\n email: typeof rawEmail === \"string\" ? rawEmail : \"\",\n password: typeof rawPassword === \"string\" ? rawPassword : \"\",\n };\n}\n\nasync function finishCredentialLogin(\n c: Context,\n engine: Auth,\n email: string,\n password: string,\n): Promise<Response> {\n const res = await forwardToEngine(c, engine, \"/api/auth/sign-in/email\", { email, password });\n if (res.status !== 200) {\n // Generic message either way (no user enumeration), but propagate\n // rate-limit/upstream statuses instead of masking them as 401.\n if (res.status === 429) {\n return renderEngineLogin(c, engine, \"Invalid credentials\", email, 429);\n }\n if (res.status >= 500) {\n return renderEngineLogin(c, engine, \"Invalid credentials\", email, 502);\n }\n if (res.status === 400) {\n return renderEngineLogin(c, engine, \"Enter a valid email address\", email, 400);\n }\n // Credential failures only (never rate-limit/upstream noise, never\n // secrets): admins opted into sys:* see a login-attempt signal. Spray\n // attempts can fan out here; the /auth/* rate-limit bucket bounds it.\n await notifySystem(\"sys:auth-failed\", { email });\n return renderEngineLogin(c, engine, \"Invalid credentials\", email, 401);\n }\n copySetCookies(c, res);\n const body = (await res.json()) as { user: { id: string; email: string; name: string } };\n await syncEngineUser(body.user);\n return hxRedirect(c, \"/\");\n}\n\nasync function handleEngineLogin(c: Context, engine: Auth): Promise<Response> {\n const { email, password } = await readCredentials(c);\n return finishCredentialLogin(c, engine, email, password);\n}\n\nfunction resolveEngineMethod(c: Context, engine: Auth): EngineLoginMethod | null {\n const id = c.req.param(\"providerId\") ?? \"\";\n if (!validProviderId(id)) {\n throw new HTTPException(400, { message: \"Unknown provider\" });\n }\n return (\n engine.loginMethods().find((entry) => entry.id === id && entry.kind !== \"password\") ?? null\n );\n}\n\nasync function finishOAuthStart(c: Context, engine: Auth, id: string): Promise<Response> {\n const res = await forwardToEngine(c, engine, \"/api/auth/sign-in/social\", {\n provider: id,\n callbackURL: \"/\",\n disableRedirect: true,\n });\n if (res.status !== 200) {\n throw new HTTPException(502, { message: \"Sign in failed\" });\n }\n const body = (await res.json()) as { url?: string };\n if (!body.url) {\n throw new HTTPException(502, { message: \"Sign in failed\" });\n }\n copySetCookies(c, res);\n return c.redirect(body.url);\n}\n\nasync function finishSSOStart(c: Context, engine: Auth, id: string): Promise<Response> {\n const res = await forwardToEngine(c, engine, \"/api/auth/sign-in/sso\", {\n providerId: id,\n callbackURL: \"/\",\n });\n if (res.status !== 200) {\n throw new HTTPException(502, { message: \"Sign in failed\" });\n }\n const body = (await res.json()) as { url?: string };\n if (!body.url) {\n throw new HTTPException(502, { message: \"Sign in failed\" });\n }\n copySetCookies(c, res);\n return c.redirect(body.url);\n}\n\nasync function handleEngineStart(c: Context, engine: Auth): Promise<Response> {\n const method = resolveEngineMethod(c, engine);\n if (!method || method.kind === \"passkey\") {\n return c.notFound();\n }\n if (method.kind === \"sso\") {\n return await finishSSOStart(c, engine, method.id);\n }\n return await finishOAuthStart(c, engine, method.id);\n}\n\nasync function handleInvitePage(c: Context, engine: Auth): Promise<Response> {\n const inviteId = c.req.param(\"inviteId\") ?? \"\";\n const token = c.req.query(\"token\") ?? \"\";\n // The invite token renders into the page: never store.\n c.header(\"Cache-Control\", \"no-store\");\n if (!inviteId || !token) {\n return c.html(renderInviteInvalidPage(\"Missing invite link parameters\"), 400);\n }\n try {\n const { email, name } = await engine.verifyInvite({ inviteId, token });\n return c.html(await renderInvitePage({ inviteId, token, email, name }));\n } catch {\n return c.html(renderInviteInvalidPage(\"Invalid or expired invite\"), 400);\n }\n}\n\nasync function readInviteForm(c: Context): Promise<{\n inviteId: string;\n token: string;\n password: string;\n confirm: string;\n}> {\n const form = await c.req.formData();\n return {\n inviteId: c.req.param(\"inviteId\") ?? \"\",\n token: formText(form.get(\"token\")),\n password: formText(form.get(\"password\")),\n confirm: formText(form.get(\"confirm\")),\n };\n}\n\nfunction formText(value: unknown): string {\n return typeof value === \"string\" ? value : \"\";\n}\n\nasync function finishInviteAccept(\n c: Context,\n engine: Auth,\n inviteId: string,\n token: string,\n password: string,\n): Promise<Response> {\n const user = await engine.acceptInvite({ inviteId, token, password });\n const res = await forwardToEngine(c, engine, \"/api/auth/sign-in/email\", {\n email: user.email,\n password,\n });\n if (res.status !== 200) {\n throw new Error(\"Sign in failed\");\n }\n copySetCookies(c, res);\n await syncEngineUser({ id: user.id, email: user.email, name: user.name });\n return c.redirect(\"/profile\");\n}\n\nasync function inviteAcceptError(\n c: Context,\n inviteId: string,\n token: string,\n error: unknown,\n): Promise<Response> {\n const message = error instanceof Error ? error.message : \"Invalid or expired invite\";\n if (message.includes(`at least ${MIN_PASSWORD_LENGTH}`)) {\n return c.html(await renderInvitePage({ inviteId, token, error: message }), 400);\n }\n return c.html(renderInviteInvalidPage(message), 400);\n}\n\nasync function handleInviteAccept(c: Context, engine: Auth): Promise<Response> {\n const { inviteId, token, password, confirm } = await readInviteForm(c);\n if (!inviteId || !token) {\n return c.html(renderInviteInvalidPage(\"Missing invite link parameters\"), 400);\n }\n if (password !== confirm) {\n return c.html(\n await renderInvitePage({ inviteId, token, error: \"Passwords do not match\" }),\n 400,\n );\n }\n try {\n return await finishInviteAccept(c, engine, inviteId, token, password);\n } catch (error) {\n return await inviteAcceptError(c, inviteId, token, error);\n }\n}\n\n/** Register the engine mount and descriptor-driven login routes. */\nexport function registerEngineAuth(app: ShelfRouter, engine: Auth): void {\n app.all(\"/api/auth/*\", async (c) => await engine.handler(c.req.raw));\n\n app.get(\"/auth/login\", async (c) => {\n const methods = engine.loginMethods();\n const sole = methods.length === 1 ? methods.at(0) : undefined;\n if (sole?.kind === \"oauth\" || sole?.kind === \"sso\") {\n return c.redirect(`/auth/engine/${sole.id}`);\n }\n return await renderEngineLogin(c, engine);\n });\n\n // A failed POST leaves the browser on this URL; a reload must not 404.\n app.get(\"/auth/engine/login\", (c) => c.redirect(\"/auth/login\"));\n\n app.post(\"/auth/engine/login\", async (c) => await handleEngineLogin(c, engine));\n\n app.get(\"/auth/engine/:providerId\", async (c) => await handleEngineStart(c, engine));\n\n app.get(\"/auth/invites/:inviteId\", async (c) => await handleInvitePage(c, engine));\n\n app.post(\"/auth/invites/:inviteId\", async (c) => await handleInviteAccept(c, engine));\n\n app.post(\"/auth/logout\", async (c) => {\n const res = await forwardToEngine(c, engine, \"/api/auth/sign-out\", {});\n copySetCookies(c, res);\n // Mirror the Secure attribute or the clear-cookie misses Secure session\n // cookies over HTTPS (behind TLS-terminating proxies check forwarded proto).\n const secure =\n new URL(c.req.url).protocol === \"https:\" || c.req.header(\"x-forwarded-proto\") === \"https\";\n c.header(\n \"set-cookie\",\n `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0${secure ? \"; Secure\" : \"\"}`,\n { append: true },\n );\n return hxRedirect(c, \"/auth/login\");\n });\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@storyshelf/app",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "StoryShelf app: Hono app, API routes, and server-rendered UI over @storyshelf/core.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chromatic-alternative",
|
|
@@ -57,8 +57,8 @@
|
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@hono/swagger-ui": "^0.6.1",
|
|
59
59
|
"@hono/zod-openapi": "^1.1.5",
|
|
60
|
-
"@storyshelf/core": "0.6.
|
|
61
|
-
"@storyshelf/observability": "0.6.
|
|
60
|
+
"@storyshelf/core": "0.6.1",
|
|
61
|
+
"@storyshelf/observability": "0.6.1",
|
|
62
62
|
"hono": "^4.11.0",
|
|
63
63
|
"zod": "^4.2.0"
|
|
64
64
|
},
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
"@opentelemetry/context-async-hooks": "^2.11.0",
|
|
68
68
|
"@opentelemetry/core": "^2.11.0",
|
|
69
69
|
"@opentelemetry/sdk-trace-base": "^2.11.0",
|
|
70
|
-
"@storyshelf/auth": "0.6.
|
|
71
|
-
"@storyshelf/db-sqlite": "0.6.
|
|
70
|
+
"@storyshelf/auth": "0.6.1",
|
|
71
|
+
"@storyshelf/db-sqlite": "0.6.1",
|
|
72
72
|
"@types/node": "^26.0.0",
|
|
73
73
|
"@vitest/coverage-v8": "^4.0.0",
|
|
74
74
|
"adm-zip": "0.6.0",
|