@jcoder-stack/abp-react 0.1.1 → 0.1.2

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,37 +1,37 @@
1
1
  # @jcoder-stack/abp-react
2
2
 
3
- `abp-react-start` 的运行时内核——从零自研的纯 React ABP 前端框架,总览见仓库根 README
3
+ The runtime core of `abp-react-start` a pure-React frontend framework for ABP backends. See the repository root README for the big picture.
4
4
 
5
- 各域按**子路径**导出,没有把所有东西糊在一起的根导出:一个根导出会让纯前端的 bundle 拖进 `proxy`/`auth` 这类服务端代码。
5
+ Every domain is exported through a **subpath**; there is no root export: an aggregate entry point would drag server-side modules like `proxy` and `auth` into the browser bundle.
6
6
 
7
- | 子路径 | 职责 |
7
+ | Subpath | Responsibility |
8
8
  | --- | --- |
9
- | `@jcoder-stack/abp-react/logger` | 同构日志:作用域、字段绑定、默认脱敏、env 开关 |
10
- | `@jcoder-stack/abp-react/core` | ABP 线上格式归一:application-configuration 的类型 + zod(容错解析)、`PagedResult<T>`、错误信封(`HttpError`/`toHttpError`) |
11
- | `@jcoder-stack/abp-react/auth` | 授权认证核心:登录策略层(OIDC/password)+ 会话层(加密分块 cookie、刷新、登出);宿主无关、后端无关 |
12
- | `@jcoder-stack/abp-react/proxy` | ABP 代理网关与调用层:贴 Bearer 转发、401→刷新→重放、幂等重试、超时;策略头组装、会话代调、身份派生;ABP auth 运行时工厂 `createAbpAuthRuntime` 与登录/回调/登出/文化/租户 handler |
13
- | `@jcoder-stack/abp-react/permissions` | 权限判定原语 `isGranted` + 变参 checker |
14
- | `@jcoder-stack/abp-react/i18n` | 两层合并 translator(后端 ABP 资源覆盖前端词库),可注入 interpolate/plural |
15
- | `@jcoder-stack/abp-react/react` | `AppConfigProvider` / `SessionProvider` + hooks(用户/权限/设置/特性/本地化/菜单)+ `<PermissionGuard>`/`<FeatureGuard>` |
16
- | `@jcoder-stack/abp-react/router` | TanStack Router beforeLoad 路由守卫 `requireAuth` / `requirePermission` |
9
+ | `@jcoder-stack/abp-react/logger` | Isomorphic logging: scopes, field binding, redaction by default, env switches |
10
+ | `@jcoder-stack/abp-react/core` | Normalizing ABP wire formats: application-configuration types + zod (tolerant parsing), `PagedResult<T>`, the error envelope (`HttpError`/`toHttpError`) |
11
+ | `@jcoder-stack/abp-react/auth` | Authentication core: the sign-in strategy layer (OIDC/password) + the session layer (encrypted chunked cookies, refresh, logout); host-agnostic and backend-agnostic |
12
+ | `@jcoder-stack/abp-react/proxy` | The ABP proxy gateway and call layer: Bearer-attached forwarding, 401→refresh→replay, idempotent retries, timeouts; policy-header assembly, session brokering, identity derivation; the ABP auth runtime factory `createAbpAuthRuntime` plus the login/callback/logout/culture/tenant handlers |
13
+ | `@jcoder-stack/abp-react/permissions` | The permission primitive `isGranted` + a variadic checker |
14
+ | `@jcoder-stack/abp-react/i18n` | A two-layer merging translator (backend ABP resources override the frontend catalog), with injectable interpolate/plural |
15
+ | `@jcoder-stack/abp-react/react` | `AppConfigProvider` / `SessionProvider` + hooks (user/permissions/settings/features/localization/menu) + `<PermissionGuard>`/`<FeatureGuard>` |
16
+ | `@jcoder-stack/abp-react/router` | TanStack Router beforeLoad guards `requireAuth` / `requirePermission` |
17
17
 
18
- ## 安装
18
+ ## Install
19
19
 
20
20
  ```bash
21
21
  bun add @jcoder-stack/abp-react
22
22
  ```
23
23
 
24
- 多数项目是配合 `@jcoder-stack/cli` 使用的——`jc-abp init` 会把接线代码落进你的项目,之后这些文件归你维护。手工接线见下。
24
+ Most projects use it together with `@jcoder-stack/cli` — `jc-abp init` writes the wiring code into your project, and those files are yours to maintain afterwards. Manual wiring is described below.
25
25
 
26
26
  ## peerDependencies
27
27
 
28
- `react`、`@tanstack/react-router`、`zod` 三者全部声明为 **optional** peer:纯 BFF 消费者只用 `/proxy`、`/auth`,不该被逼装 React router;纯前端消费者只用 `/react`、`/i18n`,也不该被逼装 zod。装哪几个由你实际用到的子路径决定,缺的那些包管理器不会报警。
28
+ `react`, `@tanstack/react-router`, and `zod` are all declared as **optional** peers: a pure BFF consumer only uses `/proxy` and `/auth` and should not be forced to install React and the router; a pure frontend consumer only uses `/react` and `/i18n` and should not be forced to install zod. Install whichever ones the subpaths you actually use require — package managers stay silent about the missing rest.
29
29
 
30
- ## 用法
30
+ ## Usage
31
31
 
32
- ### 服务端:auth 运行时
32
+ ### Server side: the auth runtime
33
33
 
34
- `createAbpAuthRuntime` 从环境变量读配置(`AUTH_ISSUER`、`AUTH_CLIENT_ID`、`AUTH_SESSION_SECRET`、`AUTH_REDIRECT_URI`、`AUTH_ABP_BASE_URL`),返回登录/回调/登出所需的一切。每个覆盖项都有默认值:
34
+ `createAbpAuthRuntime` reads its configuration from environment variables (`AUTH_ISSUER`, `AUTH_CLIENT_ID`, `AUTH_SESSION_SECRET`, `AUTH_REDIRECT_URI`, `AUTH_ABP_BASE_URL`) and returns everything the login/callback/logout handlers need. Every override has a default:
35
35
 
36
36
  ```ts
37
37
  import { createAbpAuthRuntime } from "@jcoder-stack/abp-react/proxy";
@@ -45,11 +45,11 @@ export const createRuntime = () =>
45
45
  });
46
46
  ```
47
47
 
48
- `AUTH_SESSION_SECRET` 至少 32 字符——会话 cookie 用它经 HKDF 派生 AES-GCM 密钥,短于此长度会直接抛错而非静默产出弱密钥。
48
+ `AUTH_SESSION_SECRET` must be at least 32 characters — the session cookie derives its AES-GCM key from it via HKDF, and anything shorter throws outright instead of silently producing a weak key.
49
49
 
50
- ### 客户端:Provider hooks
50
+ ### Client side: providers and hooks
51
51
 
52
- 两个 Provider 分开挂:配置(本地化/设置/特性)与身份的失效时机不同,合成一个会让身份刷新连带重建 translator
52
+ The two providers are mounted separately: configuration (localization/settings/features) and identity invalidate at different times, and folding them into one would make an identity refresh rebuild the translator along with it.
53
53
 
54
54
  ```tsx
55
55
  import { AppConfigProvider, SessionProvider } from "@jcoder-stack/abp-react/react";
@@ -61,9 +61,9 @@ import { AppConfigProvider, SessionProvider } from "@jcoder-stack/abp-react/reac
61
61
  </AppConfigProvider>;
62
62
  ```
63
63
 
64
- `messages` 需引用稳定(模块常量或 `useMemo`)——它参与 translator 的重建判定。回调 props(`onMissingKey`、`createTranslator`)经 ref 读取,写成内联箭头函数不会导致 context 重建。
64
+ `messages` needs a stable reference (a module constant or `useMemo`) — it participates in the translator rebuild check. Callback props (`onMissingKey`, `createTranslator`) are read through refs, so writing them as inline arrows does not rebuild the context.
65
65
 
66
- ### 路由守卫
66
+ ### Route guards
67
67
 
68
68
  ```ts
69
69
  import { requireAuth, requirePermission } from "@jcoder-stack/abp-react/router";
@@ -73,7 +73,7 @@ export const Route = createFileRoute("/_layout/_authed")({
73
73
  });
74
74
  ```
75
75
 
76
- 守卫是**纯 UX**——真正的安全判定在 ABP 服务端。`requirePermission` 缺权限时跳 `/forbidden`;匿名用户没有任何 grantedPolicies,默认也会落到 403,传 `loginPath` 可让未认证访客先去登录:
76
+ The guards are **pure UX** — the real authorization decision lives in the ABP backend. `requirePermission` redirects to `/forbidden` when the permission is missing; an anonymous user has no grantedPolicies at all, so they land on the 403 too by default. Pass `loginPath` to send unauthenticated visitors to sign in first:
77
77
 
78
78
  ```ts
79
79
  beforeLoad: requirePermission(IdentityPermissions.Users.Default, {
@@ -81,6 +81,6 @@ beforeLoad: requirePermission(IdentityPermissions.Users.Default, {
81
81
  });
82
82
  ```
83
83
 
84
- 推荐的做法仍是把受保护页面挂在一个跑 `requireAuth()` 的父路由下,`loginPath` 用于单独使用 `requirePermission` 的场景。
84
+ The recommended layout is still to nest protected pages under a parent route running `requireAuth()`; `loginPath` is for the cases where `requirePermission` is used on its own.
85
85
 
86
- 两者都要求祖先路由的 `beforeLoad` 已把 `identity` 放进 route context;没放会抛出带指引的错误而不是静默放行。
86
+ Both guards require an ancestor route's `beforeLoad` to have put `identity` into the route context; when it is missing they throw an error with guidance instead of silently letting the request through.
package/dist/auth.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { a as CookieOptions, T as TokenClient } from './oidc-7fu5kKVF.js';
2
- export { A as Auth, b as COOKIE_CHUNK_SIZE, C as Codec, c as CodecSchema, d as OidcMetadata, O as OidcStrategy, S as SessionManager, e as TokenClientConfig, f as TokenGrant, g as chunkCookieValue, h as clearChunkedCookie, i as clearCookie, j as createAuth, k as createCodec, l as createSessionManager, m as createTokenClient, n as discoverMetadata, o as oidcMetadataSchema, p as oidcStrategy, q as parseCookieHeader, r as readChunkedCookie, s as serializeCookie, t as toTokenResult } from './oidc-7fu5kKVF.js';
1
+ import { C as CookieOptions, T as TokenClient } from './oidc-CIQh5yHw.js';
2
+ export { A as Auth, a as COOKIE_CHUNK_SIZE, b as Codec, c as CodecSchema, O as OidcMetadata, d as OidcStrategy, S as SessionManager, e as TokenClientConfig, f as TokenGrant, g as chunkCookieValue, h as clearChunkedCookie, i as clearCookie, j as createAuth, k as createCodec, l as createSessionManager, m as createTokenClient, n as discoverMetadata, o as oidcMetadataSchema, p as oidcStrategy, q as parseCookieHeader, r as readChunkedCookie, s as serializeCookie, t as toTokenResult } from './oidc-CIQh5yHw.js';
3
3
  import { e as Logger } from './logger-BSnS65IC.js';
4
- import { S as SessionStore, b as AuthStrategy } from './types-Bj0MpXtI.js';
5
- export { A as AuthSession, B as BeginInput, C as CompleteInput, F as FetchFn, H as Handshake, I as Identity, c as IdentityContext, a as IdentityResolver, T as TokenResult, d as authSessionSchema, e as authTokensSchema, h as handshakeSchema } from './types-Bj0MpXtI.js';
4
+ import { S as SessionStore, A as AuthStrategy } from './types-BeHmw3RC.js';
5
+ export { a as AuthSession, B as BeginInput, C as CompleteInput, F as FetchFn, H as Handshake, I as Identity, b as IdentityContext, c as IdentityResolver, T as TokenResult, d as authSessionSchema, e as authTokensSchema, h as handshakeSchema } from './types-BeHmw3RC.js';
6
6
  import 'zod';
7
7
 
8
8
  /** 默认 SessionStore:把整个 AuthSession 密封进(超长时分块的)加密 cookie,自包含无后端。 */
@@ -1,5 +1,5 @@
1
1
  import { e as Logger } from './logger-BSnS65IC.js';
2
- import { F as FetchFn, T as TokenResult, A as AuthSession, S as SessionStore, b as AuthStrategy, c as IdentityContext, I as Identity, a as IdentityResolver, B as BeginInput, H as Handshake } from './types-Bj0MpXtI.js';
2
+ import { F as FetchFn, T as TokenResult, a as AuthSession, S as SessionStore, A as AuthStrategy, b as IdentityContext, I as Identity, c as IdentityResolver, B as BeginInput, H as Handshake } from './types-BeHmw3RC.js';
3
3
  import { z } from 'zod';
4
4
 
5
5
  /** 把值密封成(并从中解封)AES-GCM 加密、URL 安全的字符串。 */
@@ -199,4 +199,4 @@ declare function oidcStrategy(cfg: {
199
199
  handshakeMaxAgeSeconds?: number;
200
200
  }): OidcStrategy;
201
201
 
202
- export { type Auth as A, type Codec as C, type OidcStrategy as O, type SessionManager as S, type TokenClient as T, type CookieOptions as a, COOKIE_CHUNK_SIZE as b, type CodecSchema as c, type OidcMetadata as d, type TokenClientConfig as e, type TokenGrant as f, chunkCookieValue as g, clearChunkedCookie as h, clearCookie as i, createAuth as j, createCodec as k, createSessionManager as l, createTokenClient as m, discoverMetadata as n, oidcMetadataSchema as o, oidcStrategy as p, parseCookieHeader as q, readChunkedCookie as r, serializeCookie as s, toTokenResult as t };
202
+ export { type Auth as A, type CookieOptions as C, type OidcMetadata as O, type SessionManager as S, type TokenClient as T, COOKIE_CHUNK_SIZE as a, type Codec as b, type CodecSchema as c, type OidcStrategy as d, type TokenClientConfig as e, type TokenGrant as f, chunkCookieValue as g, clearChunkedCookie as h, clearCookie as i, createAuth as j, createCodec as k, createSessionManager as l, createTokenClient as m, discoverMetadata as n, oidcMetadataSchema as o, oidcStrategy as p, parseCookieHeader as q, readChunkedCookie as r, serializeCookie as s, toTokenResult as t };
package/dist/proxy.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { e as Logger } from './logger-BSnS65IC.js';
2
- import { A as AuthSession, I as Identity, a as IdentityResolver, F as FetchFn, H as Handshake } from './types-Bj0MpXtI.js';
3
- import { A as Auth, O as OidcStrategy, C as Codec, a as CookieOptions } from './oidc-7fu5kKVF.js';
2
+ import { a as AuthSession, I as Identity, c as IdentityResolver, F as FetchFn, H as Handshake } from './types-BeHmw3RC.js';
3
+ import { A as Auth, d as OidcStrategy, b as Codec, C as CookieOptions } from './oidc-CIQh5yHw.js';
4
4
  import { A as ApplicationConfiguration } from './application-configuration-DhOZRqtz.js';
5
5
  import { z } from 'zod';
6
6
 
@@ -95,10 +95,12 @@ declare const abpAuthEnvSchema: z.ZodObject<{
95
95
  sessionSecret: z.ZodString;
96
96
  abpBaseUrl: z.ZodString;
97
97
  debug: z.ZodDefault<z.ZodBoolean>;
98
+ extraCaFile: z.ZodOptional<z.ZodString>;
98
99
  }, z.core.$strip>;
99
100
  type AbpAuthEnv = z.infer<typeof abpAuthEnvSchema>;
100
101
  /**
101
102
  * 把 AUTH_* 记录解析成 AbpAuthEnv。
103
+ * 缺失/不合法时抛聚合后的人话错误(点名 .env 里的变量名),原始 ZodError 挂在 `cause` 上。
102
104
  * @param env 通常是 process.env。
103
105
  * @param opts.schema 覆盖默认 zod schema(`abpAuthEnvSchema`),用于给 AUTH_* 契约加更严的
104
106
  * 校验/精化;解析产物必须仍是 AbpAuthEnv(类型系统兜底,故只能 `.extend()`/`.merge()` 等
@@ -198,4 +200,28 @@ declare function handleSetCulture(request: Request): Response;
198
200
  /** GET /api/tenant?tenant=t1&returnUrl=/:落租户 cookie(缺 tenant 则清除)并弹回。 */
199
201
  declare function handleSetTenant(request: Request): Response;
200
202
 
201
- export { type AbpAuthEnv, type AbpAuthRuntimeOptions, type AbpCallRuntime, type AbpProxy, type AbpProxyAuth, AbpProxyError, type AbpProxyRequest, type AbpProxyResponse, type AppState, type AuthCookieConfig, type AuthCookieSettings, type AuthRuntime, CULTURE_COOKIE, DEFAULT_LOGIN_COOKIE, DEFAULT_LOGIN_COOKIE_MAX_AGE, DEFAULT_SESSION_COOKIE, DEFAULT_SESSION_COOKIE_MAX_AGE, SWITCH_COOKIE_MAX_AGE, TENANT_COOKIE, abpAuthEnvSchema, buildPolicyHeaders, callAbpWithSession, cookieAttributesOf, createAbpAuthRuntime, createAbpIdentityResolver, createAbpProxy, deriveIdentity, handleCallback, handleLogin, handleLogout, handleSetCulture, handleSetTenant, loadAppState, resolveAbpAuthEnv };
203
+ /**
204
+ * 若 error 源于 TLS 证书不受信,返回对应的 OpenSSL 错误码,否则返回 null。
205
+ * 沿 `cause` 链与 AggregateError 的 `errors` 向下找——fetch 抛出的是包了一层的 `TypeError: fetch failed`。
206
+ */
207
+ declare function tlsTrustFailureCode(error: unknown): string | null;
208
+ /** 若 error 表示上游不可达(拒连/解析失败/超时),返回错误码,否则返回 null。查找方式同上。 */
209
+ declare function upstreamUnreachableCode(error: unknown): string | null;
210
+ /** 上游不可达时的处置说明;`code` 取自 {@link upstreamUnreachableCode},`url` 为请求的上游地址。 */
211
+ declare function upstreamUnreachableMessage(code: string, url: string): string;
212
+ /** 证书不受信时的处置说明;`code` 取自 {@link tlsTrustFailureCode},`url` 为请求的上游地址。 */
213
+ declare function tlsTrustFailureMessage(code: string, url: string): string;
214
+ interface RuntimeCaApi {
215
+ getCACertificates?: (kind: "default") => string[];
216
+ setDefaultCACertificates?: (certs: readonly string[]) => void;
217
+ }
218
+ type InstallExtraCaResult = "installed" | "already-installed" | "unsupported";
219
+ /**
220
+ * 把一张 PEM 证书追加进进程默认 CA,让此后所有 TLS 连接(含全局 fetch)信任它。
221
+ * 与 `NODE_EXTRA_CA_CERTS` 不同,这条路是运行时 API(Node >= 22.15),dotenv 读完 .env 再调也来得及。
222
+ * 运行时没有该 API(旧 Node、Bun)时返回 "unsupported",由调用方决定怎么提示;文件不可读则抛错。
223
+ * @param caApi 测试注入口,默认 `node:tls`。
224
+ */
225
+ declare function installExtraCa(caFile: string, caApi?: RuntimeCaApi): InstallExtraCaResult;
226
+
227
+ export { type AbpAuthEnv, type AbpAuthRuntimeOptions, type AbpCallRuntime, type AbpProxy, type AbpProxyAuth, AbpProxyError, type AbpProxyRequest, type AbpProxyResponse, type AppState, type AuthCookieConfig, type AuthCookieSettings, type AuthRuntime, CULTURE_COOKIE, DEFAULT_LOGIN_COOKIE, DEFAULT_LOGIN_COOKIE_MAX_AGE, DEFAULT_SESSION_COOKIE, DEFAULT_SESSION_COOKIE_MAX_AGE, type InstallExtraCaResult, SWITCH_COOKIE_MAX_AGE, TENANT_COOKIE, abpAuthEnvSchema, buildPolicyHeaders, callAbpWithSession, cookieAttributesOf, createAbpAuthRuntime, createAbpIdentityResolver, createAbpProxy, deriveIdentity, handleCallback, handleLogin, handleLogout, handleSetCulture, handleSetTenant, installExtraCa, loadAppState, resolveAbpAuthEnv, tlsTrustFailureCode, tlsTrustFailureMessage, upstreamUnreachableCode, upstreamUnreachableMessage };
package/dist/proxy.js CHANGED
@@ -124,11 +124,34 @@ var abpAuthEnvSchema = z.object({
124
124
  postLogoutRedirectUri: z.string().url().optional(),
125
125
  sessionSecret: z.string().min(32),
126
126
  abpBaseUrl: z.string().url(),
127
- debug: z.boolean().default(false)
127
+ debug: z.boolean().default(false),
128
+ /** 额外信任的 CA 证书路径(PEM);本地 ABP 自签证书场景用,见 installExtraCa。 */
129
+ extraCaFile: z.string().optional()
128
130
  });
131
+ var ENV_NAMES = {
132
+ issuer: "AUTH_ISSUER",
133
+ clientId: "AUTH_CLIENT_ID",
134
+ clientSecret: "AUTH_CLIENT_SECRET",
135
+ scope: "AUTH_SCOPE",
136
+ redirectUri: "AUTH_REDIRECT_URI",
137
+ postLogoutRedirectUri: "AUTH_POST_LOGOUT_REDIRECT_URI",
138
+ sessionSecret: "AUTH_SESSION_SECRET",
139
+ abpBaseUrl: "AUTH_ABP_BASE_URL",
140
+ debug: "AUTH_DEBUG",
141
+ extraCaFile: "AUTH_EXTRA_CA_FILE"
142
+ };
143
+ function describeIssues(error, raw) {
144
+ return error.issues.map((issue) => {
145
+ const field = String(issue.path[0] ?? "");
146
+ const name = ENV_NAMES[field] ?? field;
147
+ const value = raw[field];
148
+ const detail = value === void 0 || value === "" ? "not set" : issue.message;
149
+ return `${name} (${detail})`;
150
+ }).join(", ");
151
+ }
129
152
  function resolveAbpAuthEnv(env, opts = {}) {
130
153
  const schema = opts.schema ?? abpAuthEnvSchema;
131
- return schema.parse({
154
+ const raw = {
132
155
  issuer: env.AUTH_ISSUER,
133
156
  clientId: env.AUTH_CLIENT_ID,
134
157
  clientSecret: env.AUTH_CLIENT_SECRET,
@@ -137,11 +160,23 @@ function resolveAbpAuthEnv(env, opts = {}) {
137
160
  postLogoutRedirectUri: env.AUTH_POST_LOGOUT_REDIRECT_URI,
138
161
  sessionSecret: env.AUTH_SESSION_SECRET,
139
162
  abpBaseUrl: env.AUTH_ABP_BASE_URL,
140
- debug: env.AUTH_DEBUG === "true"
141
- });
163
+ debug: env.AUTH_DEBUG === "true",
164
+ // `AUTH_EXTRA_CA_FILE=` 的空值行等同未设置,不能拿空串去 readFileSync。
165
+ extraCaFile: env.AUTH_EXTRA_CA_FILE === "" ? void 0 : env.AUTH_EXTRA_CA_FILE
166
+ };
167
+ const parsed = schema.safeParse(raw);
168
+ if (parsed.success) return parsed.data;
169
+ throw new Error(
170
+ `auth env is missing or invalid: ${describeIssues(parsed.error, raw)}. Fill these in the project's .env (start from .env.example \u2014 each variable is documented there).`,
171
+ { cause: parsed.error }
172
+ );
142
173
  }
143
174
 
144
175
  // src/proxy/tls-trust.ts
176
+ import { readFileSync } from "fs";
177
+ import { homedir } from "os";
178
+ import { join } from "path";
179
+ import tls from "tls";
145
180
  var TRUST_FAILURE_CODES = /* @__PURE__ */ new Set([
146
181
  "CERT_HAS_EXPIRED",
147
182
  "CERT_NOT_YET_VALID",
@@ -153,29 +188,65 @@ var TRUST_FAILURE_CODES = /* @__PURE__ */ new Set([
153
188
  "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
154
189
  "UNABLE_TO_VERIFY_LEAF_SIGNATURE"
155
190
  ]);
191
+ var UNREACHABLE_CODES = /* @__PURE__ */ new Set([
192
+ "ECONNREFUSED",
193
+ "ECONNRESET",
194
+ "EHOSTUNREACH",
195
+ "ENETUNREACH",
196
+ "ENOTFOUND",
197
+ "ETIMEDOUT",
198
+ "UND_ERR_CONNECT_TIMEOUT",
199
+ "UND_ERR_HEADERS_TIMEOUT"
200
+ ]);
156
201
  var MAX_DEPTH = 5;
157
202
  function property(value, key) {
158
203
  if (typeof value !== "object" || value === null) return void 0;
159
204
  return value[key];
160
205
  }
161
- function search(value, depth) {
206
+ function search(value, depth, codes) {
162
207
  if (depth > MAX_DEPTH || typeof value !== "object" || value === null) return null;
163
208
  const code = property(value, "code");
164
- if (typeof code === "string" && TRUST_FAILURE_CODES.has(code)) return code;
209
+ if (typeof code === "string" && codes.has(code)) return code;
165
210
  const aggregated = property(value, "errors");
166
211
  if (Array.isArray(aggregated)) {
167
212
  for (const inner of aggregated) {
168
- const found = search(inner, depth + 1);
213
+ const found = search(inner, depth + 1, codes);
169
214
  if (found !== null) return found;
170
215
  }
171
216
  }
172
- return search(property(value, "cause"), depth + 1);
217
+ return search(property(value, "cause"), depth + 1, codes);
173
218
  }
174
219
  function tlsTrustFailureCode(error) {
175
- return search(error, 0);
220
+ return search(error, 0, TRUST_FAILURE_CODES);
221
+ }
222
+ function upstreamUnreachableCode(error) {
223
+ return search(error, 0, UNREACHABLE_CODES);
224
+ }
225
+ function upstreamUnreachableMessage(code, url) {
226
+ return `abp proxy: cannot reach the ABP backend (${code}) at ${new URL(url).origin}. The backend is not running, or AUTH_ABP_BASE_URL in .env points at the wrong place. Start the backend (or fix the address) and reload.`;
176
227
  }
177
228
  function tlsTrustFailureMessage(code, url) {
178
- return `abp proxy: upstream TLS certificate is not trusted (${code}) at ${new URL(url).origin}. Node verifies against its own CA list and ignores the OS keychain, so a self-signed dev certificate the browser accepts still fails here. Start the server with NODE_EXTRA_CA_CERTS=<pem path>; it is read at startup, so putting it in .env has no effect.`;
229
+ return `abp proxy: upstream TLS certificate is not trusted (${code}) at ${new URL(url).origin}. Node verifies against its own CA list and ignores the OS keychain, so a self-signed dev certificate the browser accepts still fails here. Export the certificate and set AUTH_EXTRA_CA_FILE=<pem path> in .env (Node >= 22.15), or start the server with NODE_EXTRA_CA_CERTS=<pem path> \u2014 that one is read at startup, so .env cannot carry it.`;
230
+ }
231
+ function expandHome(path) {
232
+ if (path !== "~" && !path.startsWith("~/")) return path;
233
+ return join(homedir(), path.slice(2));
234
+ }
235
+ function installExtraCa(caFile, caApi = tls) {
236
+ if (typeof caApi.getCACertificates !== "function" || typeof caApi.setDefaultCACertificates !== "function") {
237
+ return "unsupported";
238
+ }
239
+ const resolved = expandHome(caFile);
240
+ let pem;
241
+ try {
242
+ pem = readFileSync(resolved, "utf8");
243
+ } catch (error) {
244
+ throw new Error(`extra CA file is not readable: ${resolved}`, { cause: error });
245
+ }
246
+ const current = caApi.getCACertificates("default");
247
+ if (current.includes(pem)) return "already-installed";
248
+ caApi.setDefaultCACertificates([...current, pem]);
249
+ return "installed";
179
250
  }
180
251
 
181
252
  // src/proxy/proxy.ts
@@ -277,7 +348,9 @@ function createAbpProxy(opts) {
277
348
  opts.logger?.debug("proxy retry after network error", { attempt, path: req.path });
278
349
  if (await backoff()) continue;
279
350
  }
280
- const failure = tlsCode === null ? error : new Error(tlsTrustFailureMessage(tlsCode, url), { cause: error });
351
+ const unreachableCode = tlsCode === null ? upstreamUnreachableCode(error) : null;
352
+ const explanation = tlsCode !== null ? tlsTrustFailureMessage(tlsCode, url) : unreachableCode !== null ? upstreamUnreachableMessage(unreachableCode, url) : null;
353
+ const failure = explanation === null ? error : new Error(explanation, { cause: error });
281
354
  if (setCookies.length > 0) {
282
355
  throw new AbpProxyError("abp proxy request failed after refresh", setCookies, {
283
356
  cause: failure
@@ -330,7 +403,16 @@ function cookieAttributesOf(settings) {
330
403
  return { secure: settings.secure, sameSite: settings.sameSite };
331
404
  }
332
405
  function createAbpAuthRuntime(envRecord, opts = {}) {
333
- const env = resolveAbpAuthEnv(envRecord, { schema: opts.envSchema });
406
+ let env;
407
+ try {
408
+ env = resolveAbpAuthEnv(envRecord, { schema: opts.envSchema });
409
+ } catch (error) {
410
+ const fallback = opts.logger ?? createLogger({ scope: "auth", config: resolveConfig({}) });
411
+ fallback.error("auth env resolution failed", {
412
+ error: error instanceof Error ? error.message : String(error)
413
+ });
414
+ throw error;
415
+ }
334
416
  const cookies = {
335
417
  session: {
336
418
  ...opts.cookies?.session,
@@ -347,6 +429,20 @@ function createAbpAuthRuntime(envRecord, opts = {}) {
347
429
  scope: "auth",
348
430
  config: resolveConfig({ LOG_LEVEL: env.debug ? "debug" : "info" })
349
431
  });
432
+ if (env.extraCaFile !== void 0) {
433
+ const outcome = installExtraCa(env.extraCaFile);
434
+ if (outcome === "unsupported") {
435
+ logger.warn(
436
+ "AUTH_EXTRA_CA_FILE is set but this runtime lacks tls.setDefaultCACertificates (Node >= 22.15 required); falling back \u2014 start the process with NODE_EXTRA_CA_CERTS instead",
437
+ { caFile: env.extraCaFile }
438
+ );
439
+ } else {
440
+ logger.info("extra CA certificate trusted for upstream TLS", {
441
+ caFile: env.extraCaFile,
442
+ outcome
443
+ });
444
+ }
445
+ }
350
446
  const tokenClient = createTokenClient({
351
447
  issuer: env.issuer,
352
448
  clientId: env.clientId,
@@ -569,6 +665,11 @@ export {
569
665
  handleLogout,
570
666
  handleSetCulture,
571
667
  handleSetTenant,
668
+ installExtraCa,
572
669
  loadAppState,
573
- resolveAbpAuthEnv
670
+ resolveAbpAuthEnv,
671
+ tlsTrustFailureCode,
672
+ tlsTrustFailureMessage,
673
+ upstreamUnreachableCode,
674
+ upstreamUnreachableMessage
574
675
  };
package/dist/react.d.ts CHANGED
@@ -2,7 +2,7 @@ import { ReactNode } from 'react';
2
2
  import { A as ApplicationConfiguration } from './application-configuration-DhOZRqtz.js';
3
3
  import { F as FrontendCatalog, a as TranslatorOptions, T as Translator } from './translator-B3hyoZmK.js';
4
4
  import { G as GrantedPolicies } from './is-granted-C0-1wvoW.js';
5
- import { I as Identity } from './types-Bj0MpXtI.js';
5
+ import { I as Identity } from './types-BeHmw3RC.js';
6
6
  import { PermissionChecker } from './permissions.js';
7
7
  import 'zod';
8
8
 
package/dist/router.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { I as Identity } from './types-Bj0MpXtI.js';
1
+ import { I as Identity } from './types-BeHmw3RC.js';
2
2
  import { P as PermissionStrategy } from './is-granted-C0-1wvoW.js';
3
3
  import 'zod';
4
4
 
@@ -94,4 +94,4 @@ interface IdentityContext {
94
94
  type IdentityResolver = (session: AuthSession | null, ctx: IdentityContext) => Promise<Identity>;
95
95
  type FetchFn = typeof fetch;
96
96
 
97
- export { type AuthSession as A, type BeginInput as B, type CompleteInput as C, type FetchFn as F, type Handshake as H, type Identity as I, type SessionStore as S, type TokenResult as T, type IdentityResolver as a, type AuthStrategy as b, type IdentityContext as c, authSessionSchema as d, authTokensSchema as e, handshakeSchema as h };
97
+ export { type AuthStrategy as A, type BeginInput as B, type CompleteInput as C, type FetchFn as F, type Handshake as H, type Identity as I, type SessionStore as S, type TokenResult as T, type AuthSession as a, type IdentityContext as b, type IdentityResolver as c, authSessionSchema as d, authTokensSchema as e, handshakeSchema as h };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jcoder-stack/abp-react",
3
- "description": "React ABP 前端框架运行时:日志、ABP 类型与 zod 解析、fetch client、认证会话、ABP 代理网关、权限、i18nReact Provider/hooksTanStack Router 守卫;按子路径分域导出",
4
- "version": "0.1.1",
3
+ "description": "Pure-React runtime for ABP backends: logging, ABP types with zod parsing, fetch client, auth sessions, the ABP proxy gateway, permissions, i18n, React providers/hooks, and TanStack Router guards — exported per domain via subpaths",
4
+ "version": "0.1.2",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {