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 +111 -44
- package/README.zh-CN.md +131 -0
- package/dist/plugins/github.js +27 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,64 +1,131 @@
|
|
|
1
1
|
# loginbase
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
> Cloudflare Workers server library + Kotlin Multiplatform client.
|
|
3
|
+
> Email OTP, social sign-in and session management for Cloudflare Workers.
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
**English** | [简体中文](README.zh-CN.md)
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
[](https://www.npmjs.com/package/loginbase)
|
|
8
|
+
[](LICENSE)
|
|
9
9
|
|
|
10
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
- 协议变更:服务端实现 + `docs/protocol.md` 同一个 commit,并在 `loginbase-kt` 仓开跟进 issue,客户端版本落地前不关(2026-08-13 由 monorepo 三位一体改判,理由见 design.md)
|
|
131
|
+
MIT
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# loginbase
|
|
2
|
+
|
|
3
|
+
> 为 Cloudflare Workers 而生的邮箱验证码登录、社交登录与会话管理。
|
|
4
|
+
|
|
5
|
+
[English](README.md) | **简体中文**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/loginbase)
|
|
8
|
+
[](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
|
package/dist/plugins/github.js
CHANGED
|
@@ -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, {
|
|
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