loginbase 1.6.0 → 1.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/README.md CHANGED
@@ -1,64 +1,131 @@
1
1
  # loginbase
2
2
 
3
- > The shared login foundation for my apps — email OTP + social OAuth + session management.
4
- > Cloudflare Workers server library + Kotlin Multiplatform client.
3
+ > Email OTP, social sign-in and session management for Cloudflare Workers.
5
4
 
6
- App 共用的登录底座:邮箱验证码登录、社交 OAuth(GitHub 等)、会话管理(refresh 轮换 + 重用检测 + 丢回执救活)。服务端库跑在各 App 自己的 Cloudflare Worker 里(数据在各自 D1,不做中心化账号),客户端库以 Kotlin Multiplatform 提供,住在姊妹仓 [HarlonWang/loginbase-kt](https://github.com/HarlonWang/loginbase-kt)(第 4 步新建)。**协议契约 [docs/protocol.md](docs/protocol.md) 只住本仓,是两端的唯一权威。**
5
+ **English** | [简体中文](README.zh-CN.md)
7
6
 
8
- ## 由来
7
+ [![npm](https://img.shields.io/npm/v/loginbase)](https://www.npmjs.com/package/loginbase)
8
+ [![license](https://img.shields.io/npm/l/loginbase)](LICENSE)
9
9
 
10
- - 起点是 TrendingAI 替换 Logto 的调研(邮箱登录被迫走托管 web 页、CN 链路差),结论选定自建,见 [docs/logto-替换方案-调研.md](docs/logto-替换方案-调研.md)
11
- - 服务端实现的母本是 Tono-Server 已在生产验证的邮箱验证码登录(含全套测试),本仓库是它的抽取 + 泛化
12
- - 路线选择(公共库而非中心化服务)与仓库设计见 [docs/design.md](docs/design.md),命名记录见 [docs/naming.md](docs/naming.md)
10
+ loginbase mounts into a Worker you already run. Users, sessions and login events live in **your** D1 database — there is no central account server, no vendor dashboard, and nothing extra to deploy. An official Kotlin Multiplatform client, [loginbase-kt](https://github.com/HarlonWang/loginbase-kt), implements the other half.
13
11
 
14
- ## 结构(两个仓库:一份协议,两个产物)
12
+ ## Why
15
13
 
14
+ **Hosted auth costs you the experience and the data.** Email sign-in gets redirected to someone else's web page, latency depends on someone else's edge, and your users' identities sit in someone else's database — which turns into a migration project the day pricing or policy changes.
15
+
16
+ **Rolling your own is harder than it looks.** Refresh rotation, replay detection, the race between a dropped refresh response and the client's retry, a dozen in-flight requests all refreshing at once. Get any of these wrong and it fails *silently*: nothing breaks in testing, and you find out when users are logged out for no reason — or you never find out at all.
17
+
18
+ loginbase is the middle path. The session model of a real auth product, shipped as a dependency you own, running inside the Worker you already have.
19
+
20
+ ## What you get
21
+
22
+ - **Passwordless sign-in, more than one way.** Six-digit email codes with enumeration-safe responses and three layers of rate limiting, GitHub OAuth, and account linking — so a signed-in user claims a second identity instead of ending up with a duplicate account. Code emails ship in English and Chinese.
23
+ - **Token theft is detected; bad networks aren't punished.** Refresh tokens rotate on every use, and a replayed token kills the session on the spot. A refresh response lost to a flaky connection is *not* theft, and gets recovered rather than punished.
24
+ - **Passes app-store review.** Passwordless login can't produce the static credentials Google Play and App Store Connect ask for. An optional demo account can, without opening an authentication bypass.
25
+ - **Login analytics built in.** Every send, verify, refresh and revoke lands in your own `auth_events` table, with geography from `request.cf` — no external dependency, no data leaving your account.
26
+ - **A client that hides tokens entirely.** With [loginbase-kt](https://github.com/HarlonWang/loginbase-kt), your app code never contains a token, a refresh call, or a 401 handler.
27
+
28
+ ## Quick start
29
+
30
+ **1. Install.** Hono comes along as a peer dependency — if your Worker already uses it, that stays the single copy and the version is yours to pick.
31
+
32
+ ```bash
33
+ npm install loginbase
16
34
  ```
17
- loginbase/ # 本仓:TS 服务端库 + 协议契约
18
- ├── src/ # TS 服务端库(Hono sub-app 工厂),从 Tono-Server 平移
19
- ├── test/ # vitest
20
- ├── queries/ # 登录统计取数 SQL(人肉执行;口径见 docs/stats-design.md)
21
- ├── docs/
22
- │ ├── design.md # 路线与仓库设计决策
23
- │ ├── server-design.md # 服务端技术方案(公共 API/协议草案/会话模型/平移策略)
24
- │ ├── plan.md # 实施计划(五步任务清单/验收点/版本线)
25
- │ ├── naming.md # 命名讨论记录
26
- │ ├── protocol.md # API 契约(唯一权威)——代码平移时落笔
27
- │ ├── email-identity.md # 邮箱与身份锚点:**接入方必读**(GitHub 邮箱模型 / 为何不该拿邮箱找号 / 业界做法)
28
- │ └── logto-替换方案-调研.md # 背景调研(自 TrendingProjects 迁入)
29
- └── README.md
30
-
31
- loginbase-kt/ # 姊妹仓:KMP 客户端库(独立 gradle 工程、独立 CI 与版本线)
35
+
36
+ **2. Apply the migrations.** The package ships its own DDL (`sessions`, `auth_events`).
37
+
38
+ ```toml
39
+ # wrangler.toml — for a D1 database dedicated to auth
40
+ [[d1_databases]]
41
+ binding = "DB"
42
+ database_name = "my-app"
43
+ database_id = "..."
44
+ migrations_dir = "node_modules/loginbase/migrations"
32
45
  ```
33
46
 
34
- ## 产物坐标
47
+ ```bash
48
+ npx wrangler d1 migrations apply my-app --remote
49
+ ```
35
50
 
51
+ Sharing a D1 that already has migrations of its own? Copy the two files from `node_modules/loginbase/migrations/` into your own migrations directory instead. **Skipping `0002_auth_events.sql` is silent** — login keeps working, analytics just never land.
52
+
53
+ **3. Create and mount.** The only thing loginbase asks of you is how to turn a verified identity into a user id. Everything about your user table stays yours.
54
+
55
+ ```ts
56
+ import { Hono } from "hono";
57
+ import { createLogin } from "loginbase";
58
+
59
+ const login = createLogin<Env>((env) => ({
60
+ db: env.DB,
61
+ kv: env.EMAIL_CODES,
62
+ jwt: { secret: env.JWT_SECRET },
63
+ email: {
64
+ resendApiKey: env.RESEND_API_KEY,
65
+ from: "Acme <login@acme.com>",
66
+ brand: "Acme",
67
+ },
68
+ async onVerified({ email }) {
69
+ const user = await findOrCreateUser(env.DB, email);
70
+ return { userId: user.id, isNewUser: user.isNew };
71
+ },
72
+ }));
73
+
74
+ const app = new Hono<{ Bindings: Env }>();
75
+ app.route("/", login.app); // serves /auth/*
76
+ app.get("/api/me", login.middleware, (c) => // Bearer verification
77
+ c.json({ userId: c.get("userId") })
78
+ );
79
+
80
+ export default app;
36
81
  ```
37
- 服务端仓 HarlonWang/loginbase (含 protocol.md,协议唯一权威)
38
- 客户端仓 HarlonWang/loginbase-kt
39
- npm loginbase (npm registry,本仓 tag 触发 CI 发布)
40
- Maven wang.harlon:loginbase-kt (Maven Central,客户端仓 tag 触发 CI 发布)
41
- Kotlin 包 wang.harlon.loginbase
82
+
83
+ Not using Hono for your own routes? `login.fetch(request, env, ctx)` behind one `pathname.startsWith("/auth")` works the same.
84
+
85
+ **4. Add GitHub sign-in** (optional) by giving loginbase your OAuth app and the deep links it's allowed to return to:
86
+
87
+ ```ts
88
+ socials: {
89
+ github: {
90
+ clientId: env.GITHUB_CLIENT_ID,
91
+ clientSecret: env.GITHUB_CLIENT_SECRET,
92
+ allowedRedirects: ["acme://auth"],
93
+ },
94
+ },
42
95
  ```
43
96
 
44
- 两仓各自独立版本线,tag 为裸版本号;协议兼容关系由 `protocol.md` 版本历史 + 客户端仓 README 的对齐声明表达,不靠版本号相等表达。
97
+ **5. Connect your app.** Point [loginbase-kt](https://github.com/HarlonWang/loginbase-kt) at `https://your-worker.example.com/auth` and you're done it owns storage, refresh and the OAuth browser round trip from there.
98
+
99
+ ## How the two halves line up
100
+
101
+ One redirect value has to match in three places, or social sign-in fails in ways that are tedious to diagnose:
102
+
103
+ | Where | What |
104
+ |---|---|
105
+ | Server | `socials.github.allowedRedirects` |
106
+ | Android app | `manifestPlaceholders["loginbaseRedirectScheme"]` |
107
+ | Client runtime | derived from that same placeholder |
108
+
109
+ The client can print exactly what to whitelist — call `Loginbase.redirectUri(context)` and paste the result into `allowedRedirects`.
110
+
111
+ ## Requirements
112
+
113
+ Cloudflare Workers with D1 and KV bindings · `hono` ^4.12.8 · a [Resend](https://resend.com) account for delivery.
45
114
 
46
- ## 状态与路线
115
+ ## Not included
47
116
 
48
- **落地第 1~3 步已完成**(2026-08-13):`loginbase@1.1.0` 上线 npm;Tono-Server(钩子化)与 github-ai-trending-api(双轨迁移:loginbase 优先 + Logto fallback)均已部署生产并通过分层验收。下一步 = 4 KMP 客户端。落地顺序:
117
+ loginbase deliberately stops at authentication and sessions. It has no password login, no OIDC or SAML, no multi-tenancy, no admin UI, and no user profile storage — your `onVerified` owns the user table. Sign-in providers are email and GitHub; email delivery is Resend; the runtime is Cloudflare Workers. If you need an identity provider rather than a login foundation, use one.
49
118
 
50
- 1. 从 Tono-Server 平移服务端代码与测试,Tono-Server 改为依赖本包(其现有测试即抽取验收)
51
- 2. 钩子化(`onVerified` 用户回调)+ zh/en 邮件模板 + github-oauth 可选插件
52
- 3. TrendingAI 后端接入(`/auth` 挂载 + requireAuth 双轨 + Logto 存量迁移)
53
- 4. KMP 客户端库 + TrendingAI 登录 UI(commonMain)
54
- 5. Tono-Android 择机换用 loginbase-kt 的 android target
119
+ ## Documentation
55
120
 
56
- ## 设计红线
121
+ | | |
122
+ |---|---|
123
+ | [Protocol contract](docs/protocol.md) | The wire API — single source of truth for both halves |
124
+ | [Server design](docs/server-design.md) | Configuration surface, session model, hooks |
125
+ | [Email and identity](docs/email-identity.md) | **Read before integrating** — why an email address is a poor identity anchor |
126
+ | [Login analytics](docs/stats-design.md) | Event schema and metric definitions |
127
+ | [Design decisions](docs/design.md) | Why a library instead of a service, and other roads not taken |
57
128
 
58
- - 依赖准入(2026-08-18 由「依赖最小集」改判):auth 库是供应链攻击的最高价值目标,所以**审查来源,而不是一味压数量**。放行三类,其余一律先停下来问值不值:
59
- 1. 现有基座——服务端 hono + jose(+ zod-validator),客户端 ktor-client-core + kotlinx-serialization-json + kotlinx-coroutines-core;
60
- 2. 业界权威库——四条判据全满足:① 生态事实标准,组织或多人维护、发布节奏稳定;② 发布带 provenance / trusted publishing(npm)或签名(Maven Central);③ 传递依赖 ≤ 2 且同样满足 ①;④ 无安装脚本;
61
- 3. 自己的库(`HarlonWang/*`)——同样走 trusted publishing 发布,在库里优先声明为 peerDependency,由消费方定版本
129
+ ## License
62
130
 
63
- 仍然拒绝:为省几十行代码的工具包、单人维护的新包或小众包、运行时联网或带安装脚本的包、为一个功能拖进整个框架的包。版本钉死 + lockfile + provenance 不变
64
- - 协议变更:服务端实现 + `docs/protocol.md` 同一个 commit,并在 `loginbase-kt` 仓开跟进 issue,客户端版本落地前不关(2026-08-13 由 monorepo 三位一体改判,理由见 design.md)
131
+ MIT
@@ -0,0 +1,131 @@
1
+ # loginbase
2
+
3
+ > 为 Cloudflare Workers 而生的邮箱验证码登录、社交登录与会话管理。
4
+
5
+ [English](README.md) | **简体中文**
6
+
7
+ [![npm](https://img.shields.io/npm/v/loginbase)](https://www.npmjs.com/package/loginbase)
8
+ [![license](https://img.shields.io/npm/l/loginbase)](LICENSE)
9
+
10
+ loginbase 挂进你已经在跑的 Worker。用户、会话、登录事件全部落在**你自己的** D1 里——没有中心化账号服务,没有厂商后台,也不需要额外部署任何东西。官方 Kotlin Multiplatform 客户端 [loginbase-kt](https://github.com/HarlonWang/loginbase-kt) 负责另一半。
11
+
12
+ ## 为什么
13
+
14
+ **托管方案要你交出体验和数据。** 邮箱登录被迫跳去别人的 web 页,延迟取决于别人的边缘节点,用户身份存在别人的数据库里——等哪天定价或政策一变,这就是一个迁移项目。
15
+
16
+ **自己写又比看上去难。** refresh 轮换、重放检测、回执丢包与客户端重试之间的竞态、十几个在飞请求同时刷新……任何一条写错都是**静默失败**:测试期间什么都不会坏,等到用户莫名其妙被登出你才发现,或者根本发现不了。
17
+
18
+ loginbase 是中间那条路:一套成熟认证产品该有的会话模型,以你自己持有的依赖形态交付,跑在你已经有的那个 Worker 里。
19
+
20
+ ## 能力
21
+
22
+ - **不止一种无密码登录。** 六位邮箱验证码(防账号枚举的响应 + 三层限流)、GitHub OAuth,以及身份绑定——已登录用户认领第二身份,而不是稀里糊涂多出一个重复账号。验证码邮件内置中英文。
23
+ - **盗用会被发现,弱网不会被误伤。** refresh token 每次使用即轮换,重放即当场终止会话;而弱网丢掉的那次回执**不算**盗用,会被救活而不是被惩罚。
24
+ - **过得了应用商店审核。** 无密码登录交不出 Google Play 与 App Store Connect 要的静态凭据,可选的演示账号能——且不开任何鉴权旁路。
25
+ - **内建登录统计。** 发码、验证、刷新、注销逐条落进你自己的 `auth_events` 表,地理信息取自 `request.cf`——零外部依赖,数据不出你的账号。
26
+ - **一个把 token 彻底藏起来的客户端。** 用 [loginbase-kt](https://github.com/HarlonWang/loginbase-kt),你的 App 代码里不会出现 token、刷新调用或 401 处理。
27
+
28
+ ## 快速开始
29
+
30
+ **1. 安装。** Hono 作为 peer dependency 自动带上——你的 Worker 若已经在用它,那就是同一份,版本由你定。
31
+
32
+ ```bash
33
+ npm install loginbase
34
+ ```
35
+
36
+ **2. 执行迁移。** DDL(`sessions`、`auth_events`)随包分发。
37
+
38
+ ```toml
39
+ # wrangler.toml —— 用一个专供认证的 D1 时
40
+ [[d1_databases]]
41
+ binding = "DB"
42
+ database_name = "my-app"
43
+ database_id = "..."
44
+ migrations_dir = "node_modules/loginbase/migrations"
45
+ ```
46
+
47
+ ```bash
48
+ npx wrangler d1 migrations apply my-app --remote
49
+ ```
50
+
51
+ 如果共用一个已有自己迁移的 D1,改为把 `node_modules/loginbase/migrations/` 下的两个文件复制进你自己的迁移目录。**漏掉 `0002_auth_events.sql` 是静默的**——登录照常工作,只是统计永远不落库。
52
+
53
+ **3. 建实例并挂载。** loginbase 只要求你一件事:把一个已验证的身份换成 userId。用户表的一切仍然归你。
54
+
55
+ ```ts
56
+ import { Hono } from "hono";
57
+ import { createLogin } from "loginbase";
58
+
59
+ const login = createLogin<Env>((env) => ({
60
+ db: env.DB,
61
+ kv: env.EMAIL_CODES,
62
+ jwt: { secret: env.JWT_SECRET },
63
+ email: {
64
+ resendApiKey: env.RESEND_API_KEY,
65
+ from: "Acme <login@acme.com>",
66
+ brand: "Acme",
67
+ },
68
+ async onVerified({ email }) {
69
+ const user = await findOrCreateUser(env.DB, email);
70
+ return { userId: user.id, isNewUser: user.isNew };
71
+ },
72
+ }));
73
+
74
+ const app = new Hono<{ Bindings: Env }>();
75
+ app.route("/", login.app); // 提供 /auth/*
76
+ app.get("/api/me", login.middleware, (c) => // Bearer 校验
77
+ c.json({ userId: c.get("userId") })
78
+ );
79
+
80
+ export default app;
81
+ ```
82
+
83
+ 自己的路由没用 Hono?把 `login.fetch(request, env, ctx)` 放在一个 `pathname.startsWith("/auth")` 后面,效果相同。
84
+
85
+ **4. 加上 GitHub 登录**(可选)——把 OAuth 应用凭据和允许回跳的 deep link 交给 loginbase:
86
+
87
+ ```ts
88
+ socials: {
89
+ github: {
90
+ clientId: env.GITHUB_CLIENT_ID,
91
+ clientSecret: env.GITHUB_CLIENT_SECRET,
92
+ allowedRedirects: ["acme://auth"],
93
+ },
94
+ },
95
+ ```
96
+
97
+ **5. 接上你的 App。** 把 [loginbase-kt](https://github.com/HarlonWang/loginbase-kt) 指向 `https://your-worker.example.com/auth` 就完事了——存储、刷新、OAuth 的浏览器往返都归它。
98
+
99
+ ## 两端如何对上
100
+
101
+ 同一个 redirect 要在三处保持一致,否则社交登录会以很难查的方式失败:
102
+
103
+ | 位置 | 取值 |
104
+ |---|---|
105
+ | 服务端 | `socials.github.allowedRedirects` |
106
+ | Android App | `manifestPlaceholders["loginbaseRedirectScheme"]` |
107
+ | 客户端运行时 | 由同一个占位符推导 |
108
+
109
+ 客户端能直接打印该填什么——调 `Loginbase.redirectUri(context)`,把结果粘进 `allowedRedirects`。
110
+
111
+ ## 运行要求
112
+
113
+ Cloudflare Workers,带 D1 与 KV binding · `hono` ^4.12.8 · 一个用于投递的 [Resend](https://resend.com) 账号。
114
+
115
+ ## 不包含什么
116
+
117
+ loginbase 刻意止步于认证与会话:没有密码登录,没有 OIDC / SAML,没有多租户,没有管理后台,也不存用户档案——用户表归你的 `onVerified` 管。登录方式只有邮箱与 GitHub,邮件只走 Resend,运行时只有 Cloudflare Workers。如果你要的是一个身份提供商而不是一个登录底座,请去用身份提供商。
118
+
119
+ ## 文档
120
+
121
+ | | |
122
+ |---|---|
123
+ | [协议契约](docs/protocol.md) | wire API——两端的唯一权威 |
124
+ | [服务端设计](docs/server-design.md) | 配置面、会话模型、钩子 |
125
+ | [邮箱与身份](docs/email-identity.md) | **接入前必读**——为什么邮箱不适合当身份锚点 |
126
+ | [登录统计](docs/stats-design.md) | 事件模型与指标口径 |
127
+ | [设计决策](docs/design.md) | 为什么是库而不是服务,以及那些没走的路 |
128
+
129
+ ## License
130
+
131
+ MIT
@@ -13,6 +13,20 @@ const OTC_TTL_SECONDS = 60;
13
13
  const DEFAULT_SCOPE = "user:email";
14
14
  // App 自定义的冲突 reason 会进回跳 URL,限制字符集防畸形/敏感内容外泄
15
15
  const REASON_PATTERN = /^[A-Za-z0-9_]{1,64}$/;
16
+ // start/callback 是浏览器发出的请求,UA 即浏览器身份——「issued 后回跳丢失」(D11)
17
+ // 定位靠它;exchange 来自 App 网络栈、UA 无信息量,不记。sec-ch-ua 必须一并记:
18
+ // Brave 等套壳浏览器的 UA 与 Chrome 一字不差,只有品牌列表分得出。截断防超长头污染统计表
19
+ const UA_MAX = 256;
20
+ function browserMeta(c) {
21
+ const out = {};
22
+ const ua = c.req.header("User-Agent");
23
+ if (ua)
24
+ out.ua = ua.slice(0, UA_MAX);
25
+ const brands = c.req.header("sec-ch-ua");
26
+ if (brands)
27
+ out.secChUa = brands.slice(0, UA_MAX);
28
+ return out;
29
+ }
16
30
  // 结构化校验而非字符串前缀:startsWith("https://example.com") 会被
17
31
  // https://example.com.evil.com 绕过(开放重定向 → otc 泄露给攻击者域)。
18
32
  // scheme + host 精确匹配,path 只允许白名单条目的前缀扩展。
@@ -134,12 +148,14 @@ export function registerGithubOauth(auth, getConfig, basePath) {
134
148
  const gh = github(c);
135
149
  if (!gh)
136
150
  return c.json({ error: "not_configured" }, 404);
151
+ const ua = browserMeta(c);
137
152
  const redirect = c.req.query("redirect") ?? "";
138
153
  if (!redirect || !redirectAllowed(redirect, gh)) {
139
154
  track(c, {
140
155
  event: "oauth_start",
141
156
  outcome: "invalid_redirect",
142
157
  provider: "github",
158
+ ...(Object.keys(ua).length ? { meta: ua } : {}),
143
159
  });
144
160
  return c.json({ error: "invalid_redirect" }, 400);
145
161
  }
@@ -149,7 +165,13 @@ export function registerGithubOauth(auth, getConfig, basePath) {
149
165
  await cfg(c).kv.put(`oauth:state:${state}`, JSON.stringify(record), {
150
166
  expirationTtl: STATE_TTL_SECONDS,
151
167
  });
152
- track(c, { event: "oauth_start", outcome: "ok", provider: "github", flowId });
168
+ track(c, {
169
+ event: "oauth_start",
170
+ outcome: "ok",
171
+ provider: "github",
172
+ flowId,
173
+ ...(Object.keys(ua).length ? { meta: ua } : {}),
174
+ });
153
175
  return c.redirect(buildAuthorizeUrl(gh, callbackUrlFor(c, gh), state), 302);
154
176
  });
155
177
  // 已登录用户绑定第二身份。**必须是 POST**:浏览器导航带不了 Authorization 头,
@@ -197,6 +219,7 @@ export function registerGithubOauth(auth, getConfig, basePath) {
197
219
  const gh = github(c);
198
220
  if (!gh)
199
221
  return c.json({ error: "not_configured" }, 404);
222
+ const ua = browserMeta(c);
200
223
  const code = c.req.query("code") ?? "";
201
224
  const providerError = c.req.query("error") ?? "";
202
225
  const state = c.req.query("state") ?? "";
@@ -209,6 +232,7 @@ export function registerGithubOauth(auth, getConfig, basePath) {
209
232
  event: "oauth_callback",
210
233
  outcome: "invalid_state",
211
234
  provider: "github",
235
+ ...(Object.keys(ua).length ? { meta: ua } : {}),
212
236
  });
213
237
  return c.json({ error: "invalid_state" }, 400);
214
238
  }
@@ -220,7 +244,7 @@ export function registerGithubOauth(auth, getConfig, basePath) {
220
244
  provider: "github",
221
245
  ...(flowId ? { flowId } : {}),
222
246
  ...(userId ? { userId } : {}),
223
- meta: { mode: mode ?? "login", ...meta },
247
+ meta: { mode: mode ?? "login", ...ua, ...meta },
224
248
  });
225
249
  // GitHub 用 ?error= 回报用户拒绝授权,此时没有 code;state 已验过,回跳地址可信
226
250
  if (!code) {
@@ -311,7 +335,7 @@ export function registerGithubOauth(auth, getConfig, basePath) {
311
335
  userId: verified.userId,
312
336
  ...(flowId ? { flowId } : {}),
313
337
  ...(verified.isNewUser !== undefined ? { isNewUser: verified.isNewUser } : {}),
314
- meta: { mode: "login" },
338
+ meta: { mode: "login", ...ua },
315
339
  });
316
340
  return c.redirect(withParam(redirect, "otc", otc), 302);
317
341
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loginbase",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
4
4
  "description": "Shared login foundation for Cloudflare Workers: email OTP + social OAuth + session management, as a Hono sub-app factory.",
5
5
  "type": "module",
6
6
  "license": "MIT",