@jcoder-stack/abp-react 0.1.1 → 0.1.3
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 +114 -13
- 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,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" &&
|
|
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.
|
|
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
|
|
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
|
-
|
|
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-
|
|
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.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|