@homurajs/auth 26.10.13 → 26.10.14
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 +35 -9
- package/dist/hono.d.ts +1 -1
- package/dist/index.d.ts +11 -2
- package/dist/index.js +64 -1
- package/dist/{manager-DUAs-ASy.d.ts → manager-_5q5sluE.d.ts} +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,30 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
认证:通用标准 OAuth2 (Authorization Code Flow) 客户端 + 自管 session(HS256 短 JWT + opaque refresh rotate + 族吊销)。
|
|
4
4
|
|
|
5
|
+
session store 有两版实现,接口语义严格对齐,按数据方言选:
|
|
6
|
+
|
|
7
|
+
| 实现 | 用于 | 说明 |
|
|
8
|
+
| ------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------- |
|
|
9
|
+
| `createDrizzleSessionStore(db)` | `sqlite` / `pg` | 原生 SQL,`UPDATE...WHERE used_at IS NULL RETURNING` 做原子抢占 |
|
|
10
|
+
| `createRestSessionStore(rest)` | `rest`(qw-pages) | PostgREST:`PATCH ?token_hash=eq&expires_at=gt&used_at=is.null` + `return=representation`,返回空数组即抢占失败 |
|
|
11
|
+
|
|
5
12
|
```ts
|
|
6
13
|
// OAuth 预设来自 @homurajs/3p(结构化契约,鸭子类型互操作)
|
|
7
14
|
import { dingtalkOAuth } from '@homurajs/3p/dingtalk'
|
|
8
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
OAuth2Client,
|
|
17
|
+
SessionManager,
|
|
18
|
+
createDrizzleSessionStore,
|
|
19
|
+
createRestSessionStore,
|
|
20
|
+
} from '@homurajs/auth'
|
|
9
21
|
import { createMemoryTtlKv } from '@homurajs/core'
|
|
10
22
|
|
|
11
23
|
const preset = dingtalkOAuth({ orgId: 'my_org' }) // 对接 ../oauth2-dingtalk
|
|
12
24
|
const client = new OAuth2Client({
|
|
13
|
-
clientId: '...',
|
|
14
|
-
|
|
15
|
-
|
|
25
|
+
clientId: '...',
|
|
26
|
+
clientSecret: '...',
|
|
27
|
+
redirectUri: 'https://app.example/auth/callback',
|
|
28
|
+
stateKv: createMemoryTtlKv(), // SQL 单机够用;多实例换 createDbTtlKv;qw-pages 换 createRestTtlKv
|
|
29
|
+
...preset, // endpoints + mapUser
|
|
16
30
|
})
|
|
17
31
|
|
|
32
|
+
const store = isRest
|
|
33
|
+
? createRestSessionStore(rest) // qw-pages
|
|
34
|
+
: createDrizzleSessionStore(db) // standalone
|
|
18
35
|
const sessions = new SessionManager(
|
|
19
|
-
{ secret: SESSION_SECRET /*>=32B*/, accessTokenTtlSec: 7200, refreshTokenTtlSec: 30*24*3600 },
|
|
20
|
-
|
|
36
|
+
{ secret: SESSION_SECRET /*>=32B*/, accessTokenTtlSec: 7200, refreshTokenTtlSec: 30 * 24 * 3600 },
|
|
37
|
+
store,
|
|
21
38
|
)
|
|
22
39
|
|
|
23
40
|
// Hono 集成
|
|
24
41
|
import { createAuthRoutes, sessionMiddleware, requireAuth } from '@homurajs/auth/hono'
|
|
25
|
-
app.use('*', sessionMiddleware(sessions))
|
|
26
|
-
app.route('/', createAuthRoutes({ client, sessions }))
|
|
42
|
+
app.use('*', sessionMiddleware(sessions, { secure }))
|
|
43
|
+
app.route('/', createAuthRoutes({ client, sessions, secure }))
|
|
27
44
|
app.use('/api/*', requireAuth)
|
|
28
45
|
app.get('/api/me', (c) => c.json(c.get('auth')))
|
|
29
46
|
```
|
|
30
47
|
|
|
31
|
-
|
|
48
|
+
**`secure` 必须按形态传**:qw-pages 在平台网关后、对外是 https,`secure` 应为 `true`,否则 cookie 不带 `Secure`。模板里由 `adapter.trustProxy()` 接线(standalone 由 `TRUST_PROXY=1` 控制)。
|
|
49
|
+
|
|
50
|
+
**qw-pages 上的两个前置条件**:
|
|
51
|
+
|
|
52
|
+
1. `refresh_tokens` 表与 anon RLS policy 必须在部署期用平台受控迁移建好(App 无 DDL 权限)。缺 policy 时 PostgREST 是静默失败——`SELECT` 返回 `200 []`、`INSERT` 返回 401 + `42501`。
|
|
53
|
+
2. `SESSION_SECRET` 不能用仓库里的公开缺省值(模板 `loadConfig` 会在 Pages/生产形态下直接拒绝启动)。密钥经 `config/homura.config.env` 随产物携带——实测产物不可被公网读取。
|
|
54
|
+
|
|
55
|
+
`consume` 的原子抢占语义已在真实 PostgREST 上验证:三路并发抢占恰好一个赢家、过期 token 不会被误报为复用、族吊销只命中未消费行。
|
|
56
|
+
|
|
57
|
+
上游契约与 state 语义见 `docs/specs/module-oauth2`;session 状态机见 `module-session`。使用示范见模板 `src/server/app.ts`(buildAuth)与 `src/server/routes/api.ts`(`c.get('auth')`)。
|
package/dist/hono.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { l as OAuth2Client, n as SessionManager, t as SessionClaims } from "./manager-_5q5sluE.js";
|
|
2
2
|
import { Hono, MiddlewareHandler } from "hono";
|
|
3
3
|
//#region src/hono/routes.d.ts
|
|
4
4
|
interface AuthPaths {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
|
-
import { a as RefreshTokenStore, c as
|
|
2
|
-
|
|
1
|
+
import { a as RefreshTokenStore, c as RefreshTokenRecord, d as AuthUser, f as OAuth2Endpoints, i as SessionTokens, l as OAuth2Client, n as SessionManager, o as createDrizzleSessionStore, p as TokenResponse, r as SessionManagerConfig, s as ConsumedToken, t as SessionClaims, u as OAuth2ClientConfig } from "./manager-_5q5sluE.js";
|
|
2
|
+
import { RestDb } from "@homurajs/db";
|
|
3
|
+
//#region src/session/rest-store.d.ts
|
|
4
|
+
export declare const REFRESH_TOKENS_TABLE = "refresh_tokens";
|
|
5
|
+
interface RestSessionStoreOptions {
|
|
6
|
+
table?: string;
|
|
7
|
+
now?: () => number;
|
|
8
|
+
}
|
|
9
|
+
export declare function createRestSessionStore(rest: RestDb, opts?: RestSessionStoreOptions): RefreshTokenStore;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { type AuthUser, type ConsumedToken, OAuth2Client, type OAuth2ClientConfig, type OAuth2Endpoints, type RefreshTokenRecord, type RefreshTokenStore, type RestSessionStoreOptions, type SessionClaims, SessionManager, type SessionManagerConfig, type SessionTokens, type TokenResponse, createDrizzleSessionStore };
|
package/dist/index.js
CHANGED
|
@@ -243,4 +243,67 @@ RETURNING family_id, sid, sub, profile`))[0];
|
|
|
243
243
|
};
|
|
244
244
|
}
|
|
245
245
|
//#endregion
|
|
246
|
-
|
|
246
|
+
//#region src/session/rest-store.ts
|
|
247
|
+
const REFRESH_TOKENS_TABLE = "refresh_tokens";
|
|
248
|
+
function createRestSessionStore(rest, opts = {}) {
|
|
249
|
+
const t = rest.table(opts.table ?? "refresh_tokens");
|
|
250
|
+
const now = opts.now ?? (() => Date.now());
|
|
251
|
+
const isUsed = (v) => v !== null && v !== void 0;
|
|
252
|
+
return {
|
|
253
|
+
async create(r) {
|
|
254
|
+
await t.insert({
|
|
255
|
+
token_hash: r.tokenHash,
|
|
256
|
+
family_id: r.familyId,
|
|
257
|
+
sid: r.sid,
|
|
258
|
+
sub: r.sub,
|
|
259
|
+
expires_at: r.expiresAt,
|
|
260
|
+
profile: r.profile ?? null
|
|
261
|
+
});
|
|
262
|
+
},
|
|
263
|
+
async consume(tokenHash) {
|
|
264
|
+
const ts = now();
|
|
265
|
+
const hit = (await t.update({ used_at: ts }, {
|
|
266
|
+
token_hash: tokenHash,
|
|
267
|
+
expires_at: {
|
|
268
|
+
op: "gt",
|
|
269
|
+
value: ts
|
|
270
|
+
},
|
|
271
|
+
used_at: null
|
|
272
|
+
}))[0];
|
|
273
|
+
if (hit) return {
|
|
274
|
+
familyId: hit.family_id,
|
|
275
|
+
sid: hit.sid,
|
|
276
|
+
sub: hit.sub,
|
|
277
|
+
profile: hit.profile,
|
|
278
|
+
revoked: false
|
|
279
|
+
};
|
|
280
|
+
const row = (await t.select({ token_hash: tokenHash }, {
|
|
281
|
+
columns: "family_id,used_at",
|
|
282
|
+
limit: 1
|
|
283
|
+
}))[0];
|
|
284
|
+
if (!row) return null;
|
|
285
|
+
if (isUsed(row.used_at)) return {
|
|
286
|
+
familyId: row.family_id,
|
|
287
|
+
sid: "",
|
|
288
|
+
sub: "",
|
|
289
|
+
profile: null,
|
|
290
|
+
revoked: true
|
|
291
|
+
};
|
|
292
|
+
return null;
|
|
293
|
+
},
|
|
294
|
+
async revokeFamily(familyId) {
|
|
295
|
+
await t.update({ used_at: now() }, {
|
|
296
|
+
family_id: familyId,
|
|
297
|
+
used_at: null
|
|
298
|
+
});
|
|
299
|
+
},
|
|
300
|
+
async revokeUser(sub) {
|
|
301
|
+
await t.update({ used_at: now() }, {
|
|
302
|
+
sub,
|
|
303
|
+
used_at: null
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
//#endregion
|
|
309
|
+
export { OAuth2Client, REFRESH_TOKENS_TABLE, SessionManager, createDrizzleSessionStore, createRestSessionStore };
|
|
@@ -173,4 +173,4 @@ declare class SessionManager {
|
|
|
173
173
|
private issueRefresh;
|
|
174
174
|
}
|
|
175
175
|
//#endregion
|
|
176
|
-
export { RefreshTokenStore as a,
|
|
176
|
+
export { RefreshTokenStore as a, RefreshTokenRecord as c, AuthUser as d, OAuth2Endpoints as f, SessionTokens as i, OAuth2Client as l, SessionManager as n, createDrizzleSessionStore as o, TokenResponse as p, SessionManagerConfig as r, ConsumedToken as s, SessionClaims as t, OAuth2ClientConfig as u };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homurajs/auth",
|
|
3
|
-
"version": "26.10.
|
|
3
|
+
"version": "26.10.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "homura 认证:通用标准 OAuth2 (Authorization Code Flow) 客户端 + 自管 session(HS256 短 JWT + opaque refresh rotate)",
|
|
6
6
|
"engines": {
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"registry": "https://registry.npmjs.org/"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@homurajs/core": "26.10.
|
|
32
|
-
"@homurajs/db": "26.10.
|
|
31
|
+
"@homurajs/core": "26.10.14",
|
|
32
|
+
"@homurajs/db": "26.10.14",
|
|
33
33
|
"jose": "^6.0.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|