@jcoder-stack/abp-react 0.1.0 → 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 +25 -25
- package/dist/auth.d.ts +4 -4
- package/dist/{oidc-7fu5kKVF.d.ts → oidc-CIQh5yHw.d.ts} +2 -2
- package/dist/proxy.d.ts +29 -3
- package/dist/proxy.js +149 -9
- package/dist/react.d.ts +1 -1
- package/dist/router.d.ts +1 -1
- package/dist/{types-Bj0MpXtI.d.ts → types-BeHmw3RC.d.ts} +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
1
|
# @jcoder-stack/abp-react
|
|
2
2
|
|
|
3
|
-
`abp-react-start`
|
|
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
|
-
|
|
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` |
|
|
10
|
-
| `@jcoder-stack/abp-react/core` | ABP
|
|
11
|
-
| `@jcoder-stack/abp-react/auth` |
|
|
12
|
-
| `@jcoder-stack/abp-react/proxy` | ABP
|
|
13
|
-
| `@jcoder-stack/abp-react/permissions` |
|
|
14
|
-
| `@jcoder-stack/abp-react/i18n` |
|
|
15
|
-
| `@jcoder-stack/abp-react/react` | `AppConfigProvider` / `SessionProvider` + hooks
|
|
16
|
-
| `@jcoder-stack/abp-react/router` | TanStack Router beforeLoad
|
|
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
|
-
|
|
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
|
|
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
|
-
###
|
|
32
|
+
### Server side: the auth runtime
|
|
33
33
|
|
|
34
|
-
`createAbpAuthRuntime`
|
|
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`
|
|
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
|
-
###
|
|
50
|
+
### Client side: providers and hooks
|
|
51
51
|
|
|
52
|
-
|
|
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`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
2
|
-
export { A as Auth,
|
|
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,
|
|
5
|
-
export {
|
|
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,
|
|
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
|
|
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 {
|
|
3
|
-
import { A as Auth,
|
|
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
|
-
|
|
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
|
-
|
|
154
|
+
const raw = {
|
|
132
155
|
issuer: env.AUTH_ISSUER,
|
|
133
156
|
clientId: env.AUTH_CLIENT_ID,
|
|
134
157
|
clientSecret: env.AUTH_CLIENT_SECRET,
|
|
@@ -137,8 +160,93 @@ 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
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
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";
|
|
180
|
+
var TRUST_FAILURE_CODES = /* @__PURE__ */ new Set([
|
|
181
|
+
"CERT_HAS_EXPIRED",
|
|
182
|
+
"CERT_NOT_YET_VALID",
|
|
183
|
+
"CERT_UNTRUSTED",
|
|
184
|
+
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
185
|
+
"ERR_TLS_CERT_ALTNAME_INVALID",
|
|
186
|
+
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
187
|
+
"UNABLE_TO_GET_ISSUER_CERT",
|
|
188
|
+
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
|
|
189
|
+
"UNABLE_TO_VERIFY_LEAF_SIGNATURE"
|
|
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
|
+
]);
|
|
201
|
+
var MAX_DEPTH = 5;
|
|
202
|
+
function property(value, key) {
|
|
203
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
204
|
+
return value[key];
|
|
205
|
+
}
|
|
206
|
+
function search(value, depth, codes) {
|
|
207
|
+
if (depth > MAX_DEPTH || typeof value !== "object" || value === null) return null;
|
|
208
|
+
const code = property(value, "code");
|
|
209
|
+
if (typeof code === "string" && codes.has(code)) return code;
|
|
210
|
+
const aggregated = property(value, "errors");
|
|
211
|
+
if (Array.isArray(aggregated)) {
|
|
212
|
+
for (const inner of aggregated) {
|
|
213
|
+
const found = search(inner, depth + 1, codes);
|
|
214
|
+
if (found !== null) return found;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return search(property(value, "cause"), depth + 1, codes);
|
|
218
|
+
}
|
|
219
|
+
function tlsTrustFailureCode(error) {
|
|
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.`;
|
|
227
|
+
}
|
|
228
|
+
function tlsTrustFailureMessage(code, url) {
|
|
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";
|
|
142
250
|
}
|
|
143
251
|
|
|
144
252
|
// src/proxy/proxy.ts
|
|
@@ -235,16 +343,20 @@ function createAbpProxy(opts) {
|
|
|
235
343
|
signal: AbortSignal.any([...stops, AbortSignal.timeout(timeoutMs)])
|
|
236
344
|
});
|
|
237
345
|
} catch (error) {
|
|
238
|
-
|
|
346
|
+
const tlsCode = tlsTrustFailureCode(error);
|
|
347
|
+
if (tlsCode === null && attempt < maxRetries && !stopped()) {
|
|
239
348
|
opts.logger?.debug("proxy retry after network error", { attempt, path: req.path });
|
|
240
349
|
if (await backoff()) continue;
|
|
241
350
|
}
|
|
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 });
|
|
242
354
|
if (setCookies.length > 0) {
|
|
243
355
|
throw new AbpProxyError("abp proxy request failed after refresh", setCookies, {
|
|
244
|
-
cause:
|
|
356
|
+
cause: failure
|
|
245
357
|
});
|
|
246
358
|
}
|
|
247
|
-
throw
|
|
359
|
+
throw failure;
|
|
248
360
|
}
|
|
249
361
|
if (res.status === 401 && !refreshedOnce && session?.tokens.refreshToken !== void 0) {
|
|
250
362
|
refreshedOnce = true;
|
|
@@ -291,7 +403,16 @@ function cookieAttributesOf(settings) {
|
|
|
291
403
|
return { secure: settings.secure, sameSite: settings.sameSite };
|
|
292
404
|
}
|
|
293
405
|
function createAbpAuthRuntime(envRecord, opts = {}) {
|
|
294
|
-
|
|
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
|
+
}
|
|
295
416
|
const cookies = {
|
|
296
417
|
session: {
|
|
297
418
|
...opts.cookies?.session,
|
|
@@ -308,6 +429,20 @@ function createAbpAuthRuntime(envRecord, opts = {}) {
|
|
|
308
429
|
scope: "auth",
|
|
309
430
|
config: resolveConfig({ LOG_LEVEL: env.debug ? "debug" : "info" })
|
|
310
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
|
+
}
|
|
311
446
|
const tokenClient = createTokenClient({
|
|
312
447
|
issuer: env.issuer,
|
|
313
448
|
clientId: env.clientId,
|
|
@@ -530,6 +665,11 @@ export {
|
|
|
530
665
|
handleLogout,
|
|
531
666
|
handleSetCulture,
|
|
532
667
|
handleSetTenant,
|
|
668
|
+
installExtraCa,
|
|
533
669
|
loadAppState,
|
|
534
|
-
resolveAbpAuthEnv
|
|
670
|
+
resolveAbpAuthEnv,
|
|
671
|
+
tlsTrustFailureCode,
|
|
672
|
+
tlsTrustFailureMessage,
|
|
673
|
+
upstreamUnreachableCode,
|
|
674
|
+
upstreamUnreachableMessage
|
|
535
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-
|
|
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
|
@@ -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
|
|
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": "
|
|
4
|
-
"version": "0.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": {
|