@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 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 { OAuth2Client, SessionManager } from '@homurajs/auth'
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: '...', clientSecret: '...', redirectUri: 'https://app.example/auth/callback',
14
- stateKv: createMemoryTtlKv(), // 多实例部署换 createDbTtlKv
15
- ...preset, // endpoints + mapUser
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
- createDrizzleSessionStore(db),
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
- 上游契约与 state 语义见 `docs/specs/module-oauth2`;session 状态机见 `module-session`。使用示范见模板 `src/server/app.ts`(buildAuth)与 `src/server/routes/api.ts`(c.get('auth'))。
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 { n as SessionManager, s as OAuth2Client, t as SessionClaims } from "./manager-DUAs-ASy.js";
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 OAuth2ClientConfig, d as TokenResponse, i as SessionTokens, l as AuthUser, n as SessionManager, o as createDrizzleSessionStore, r as SessionManagerConfig, s as OAuth2Client, t as SessionClaims, u as OAuth2Endpoints } from "./manager-DUAs-ASy.js";
2
- export { type AuthUser, OAuth2Client, type OAuth2ClientConfig, type OAuth2Endpoints, type RefreshTokenStore, type SessionClaims, SessionManager, type SessionManagerConfig, type SessionTokens, type TokenResponse, createDrizzleSessionStore };
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
- export { OAuth2Client, SessionManager, createDrizzleSessionStore };
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, OAuth2ClientConfig as c, TokenResponse as d, SessionTokens as i, AuthUser as l, SessionManager as n, createDrizzleSessionStore as o, SessionManagerConfig as r, OAuth2Client as s, SessionClaims as t, OAuth2Endpoints as u };
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.13",
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.13",
32
- "@homurajs/db": "26.10.13",
31
+ "@homurajs/core": "26.10.14",
32
+ "@homurajs/db": "26.10.14",
33
33
  "jose": "^6.0.0"
34
34
  },
35
35
  "peerDependencies": {