@h-ai/serv 1.0.0-beta.15 → 1.0.0-beta.17

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.en.md CHANGED
@@ -485,7 +485,7 @@ await apiClient.init({
485
485
  })
486
486
  ```
487
487
 
488
- > **Cookie attributes**: `HttpOnly; SameSite=Strict; Secure` in production; `Path` is limited to `{apiPrefix}/auth/refresh`.
488
+ > Refresh cookies use `HttpOnly` and `SameSite=Strict` and are scoped to `{apiPrefix}/auth/refresh`. Production adds `Secure` by default.
489
489
 
490
490
  ### API overview
491
491
 
@@ -638,7 +638,9 @@ const app = serv.createApp({
638
638
  })
639
639
  ```
640
640
 
641
- In production, empty or `*` `_serv.yml` `cors.origin` fails startup. Web and Capacitor/Tauri origins use exact normalized full-Origin matching to prevent suffix spoofing. Production also enforces Secure refresh cookies and disables OpenAPI/docs; configuration controls nonproduction endpoints.
641
+ Production requires an explicit `_serv.yml` `cors.origin` allowlist and disables OpenAPI and docs. Web and Capacitor/Tauri origins match by their exact normalized values.
642
+
643
+ For a production HTTP endpoint on a controlled private network or VPN, set `HAI_SERV_REFRESH_COOKIE_SECURE=false` in the backend process environment. After restarting the backend and signing in, login and refresh responses omit `Secure` from the cookie, and HTTP carries the refresh credential in plaintext. `true` forces `Secure`. An empty or unrecognized value uses the environment default. When calling `serv.createApp` directly, an explicit `refreshCookie.secure` takes precedence over this variable, followed by the production check on `NODE_ENV` or `HAI_CORE_ENV`.
642
644
 
643
645
  ### Configuration driven authentication
644
646
 
package/README.ja.md CHANGED
@@ -485,7 +485,7 @@ await apiClient.init({
485
485
  })
486
486
  ```
487
487
 
488
- > **Cookie 属性**:本番は `HttpOnly; SameSite=Strict; Secure`。`Path` は `{apiPrefix}/auth/refresh` に限定。
488
+ > リフレッシュ Cookie には `HttpOnly` と `SameSite=Strict` が付き、パスは `{apiPrefix}/auth/refresh` に限定されます。本番では既定で `Secure` も付きます。
489
489
 
490
490
  ### API 一覧
491
491
 
@@ -638,7 +638,9 @@ const app = serv.createApp({
638
638
  })
639
639
  ```
640
640
 
641
- 本番では `_serv.yml` の `cors.origin` が空・`*` なら起動に失敗します。Web・Capacitor/Tauri は正規化済み Origin 全体を厳密照合し、末尾偽装を防ぎます。本番は Secure refresh cookie を強制し OpenAPI/docs を無効化します。非本番の endpoint は設定ファイルで決めます。
641
+ 本番では `_serv.yml` の `cors.origin` に許可するオリジンを明示し、OpenAPI と docs は無効になります。Web と Capacitor/Tauri のオリジンは、正規化した値全体で照合します。
642
+
643
+ 管理されたプライベートネットワークや VPN の本番 HTTP 入口では、バックエンドのプロセス環境に `HAI_SERV_REFRESH_COOKIE_SECURE=false` を設定できます。再起動してログインし直すと、ログイン応答と更新応答の Cookie から `Secure` が外れ、HTTP でリフレッシュ認証情報が平文で送信されます。`true` は `Secure` を強制し、空欄や未対応の値では環境の既定値が使われます。`serv.createApp` を直接呼ぶ場合は、明示した `refreshCookie.secure`、この環境変数、`NODE_ENV` または `HAI_CORE_ENV` による本番判定の順に適用されます。
642
644
 
643
645
  ### 設定による認証画面
644
646
 
package/README.md CHANGED
@@ -485,7 +485,7 @@ await apiClient.init({
485
485
  })
486
486
  ```
487
487
 
488
- > **Cookie 规格**:`HttpOnly; SameSite=Strict; Secure`(生产);`Path` 限制为 `{apiPrefix}/auth/refresh`。
488
+ > 刷新 Cookie 带有 `HttpOnly` 和 `SameSite=Strict`,路径限制为 `{apiPrefix}/auth/refresh`。生产默认还会设置 `Secure`。
489
489
 
490
490
  ### API 概览
491
491
 
@@ -638,7 +638,9 @@ const app = serv.createApp({
638
638
  })
639
639
  ```
640
640
 
641
- 生产环境下 `_serv.yml` 的 `cors.origin` 为空或为 `*` 会在启动时失败;Web 与 Capacitor/Tauri Origin 均按规范化后的完整 Origin 精确匹配,避免后缀伪造。生产环境同时强制 Secure refresh cookie 并关闭 OpenAPI/docs,配置文件仍决定非生产环境具体挂载哪些端点。
641
+ 生产环境要求 `_serv.yml` 的 `cors.origin` 显式列出允许的来源,并关闭 OpenAPI 和 docs。Web 与 Capacitor/Tauri Origin 按规范化后的完整值精确匹配。
642
+
643
+ 受控内网或 VPN 的生产 HTTP 入口可以在后端进程环境设置 `HAI_SERV_REFRESH_COOKIE_SECURE=false`。重新启动后端并登录后,登录和刷新响应会下发省略 `Secure` 的 Cookie,刷新凭据会经 HTTP 明文传输。设置 `true` 可强制启用 `Secure`,留空或其他值沿用环境默认值。直接调用 `serv.createApp` 时,显式 `refreshCookie.secure` 优先,其次是此变量,再按 `NODE_ENV` 或 `HAI_CORE_ENV` 判断是否为生产。
642
644
 
643
645
  ### 配置驱动认证页面
644
646
 
@@ -2,8 +2,8 @@ import * as _orpc_server from '@orpc/server';
2
2
  import * as zod_v4_core from 'zod/v4/core';
3
3
  import * as zod from 'zod';
4
4
  import { AIFunctions } from '@h-ai/ai';
5
- import { S as ServContext } from '../serv-context-DE1JH5bd.js';
6
- import { A as AuthenticatedServContext } from '../serv-router-BJk04-oS.js';
5
+ import { S as ServContext } from '../serv-context-BsNvQ-bb.js';
6
+ import { A as AuthenticatedServContext } from '../serv-router-BnkxPvS6.js';
7
7
  import '@h-ai/core';
8
8
 
9
9
  /**
@@ -2,8 +2,8 @@ import * as _orpc_server from '@orpc/server';
2
2
  import * as zod from 'zod';
3
3
  import * as zod_v4_core from 'zod/v4/core';
4
4
  import { FeedbackFunctions, FeedbackContext } from '@h-ai/feedback';
5
- import { S as ServContext } from '../serv-context-DE1JH5bd.js';
6
- import { A as AuthenticatedServContext } from '../serv-router-BJk04-oS.js';
5
+ import { S as ServContext } from '../serv-context-BsNvQ-bb.js';
6
+ import { A as AuthenticatedServContext } from '../serv-router-BnkxPvS6.js';
7
7
  import '@h-ai/core';
8
8
 
9
9
  /**
@@ -4,7 +4,7 @@ import * as zod_v4_core from 'zod/v4/core';
4
4
  import * as zod from 'zod';
5
5
  import { IamFunctions } from '@h-ai/iam';
6
6
  export { TokenPair } from '@h-ai/iam';
7
- import { S as ServContext } from '../serv-context-DE1JH5bd.js';
7
+ import { S as ServContext } from '../serv-context-BsNvQ-bb.js';
8
8
  import '@h-ai/core';
9
9
 
10
10
  /**
@@ -2,8 +2,8 @@ import * as _orpc_server from '@orpc/server';
2
2
  import * as zod_v4_core from 'zod/v4/core';
3
3
  import * as zod from 'zod';
4
4
  import { StorageFunctions } from '@h-ai/storage';
5
- import { S as ServContext } from '../serv-context-DE1JH5bd.js';
6
- import { A as AuthenticatedServContext } from '../serv-router-BJk04-oS.js';
5
+ import { S as ServContext } from '../serv-context-BsNvQ-bb.js';
6
+ import { A as AuthenticatedServContext } from '../serv-router-BnkxPvS6.js';
7
7
  import '@h-ai/core';
8
8
 
9
9
  /**
package/dist/index.d.ts CHANGED
@@ -3,15 +3,15 @@ import { HaiResult, TrustedProxies, RequestAccessConfigInput, ValidationFormErro
3
3
  import { AIFunctions, AudioWsStartMessage, AudioFormat } from '@h-ai/ai';
4
4
  import { AnyContractRouter, OpenAPI } from '@orpc/contract';
5
5
  import { ProcedureHandler, ErrorMap, Meta, ProcedureHandlerOptions, ORPCErrorConstructorMap, Router } from '@orpc/server';
6
- import { a as ServSession, S as ServContext, b as ServIam, R as RefreshCookieConfig, C as CreateServContext, c as ServRequestMetadata, p as parseRequestContext, d as buildAuthContextFactory } from './serv-context-DE1JH5bd.js';
7
- export { e as CreateServContextInput, N as NativeRefreshTokenTransportConfig, f as RefreshTokenPair } from './serv-context-DE1JH5bd.js';
6
+ import { a as ServSession, S as ServContext, b as ServIam, R as RefreshCookieConfig, C as CreateServContext, c as ServRequestMetadata, p as parseRequestContext, d as buildAuthContextFactory } from './serv-context-BsNvQ-bb.js';
7
+ export { e as CreateServContextInput, N as NativeRefreshTokenTransportConfig, f as RefreshTokenPair } from './serv-context-BsNvQ-bb.js';
8
8
  import { MiddlewareHandler } from 'hono';
9
9
  import { z, ZodType } from 'zod';
10
10
  import { TransportServerKeyRing, CryptoFunctions, TransportKeyStore } from '@h-ai/crypto';
11
11
  import { ServerType } from '@hono/node-server';
12
12
  import { AddressInfo } from 'node:net';
13
- import { i as implement } from './serv-router-BJk04-oS.js';
14
- export { A as AuthenticatedServContext, S as ServImplementer, a as ServRouteHandler, b as ServRoutePath, c as ServRouterBuilder } from './serv-router-BJk04-oS.js';
13
+ import { i as implement } from './serv-router-BnkxPvS6.js';
14
+ export { A as AuthenticatedServContext, S as ServImplementer, a as ServRouteHandler, b as ServRoutePath, c as ServRouterBuilder } from './serv-router-BnkxPvS6.js';
15
15
  import { StorageFunctions } from '@h-ai/storage';
16
16
 
17
17
  /**
@@ -1120,11 +1120,13 @@ interface ServRuntimeSecurityPolicyInput {
1120
1120
  interface ServRuntimeSecurityPolicy {
1121
1121
  readonly allowOrigin: (origin: string) => boolean;
1122
1122
  readonly isNativeOrigin: (origin: string) => boolean;
1123
+ /** 刷新 Cookie 是否启用 Secure,默认在生产开启。 */
1123
1124
  readonly secureRefreshCookie: boolean;
1124
1125
  readonly exposeApiDocs: boolean;
1125
1126
  }
1126
1127
  /**
1127
1128
  * 校验部署 Origin 并生成 Cookie、文档与跨端 CORS 安全策略。
1129
+ * `HAI_SERV_REFRESH_COOKIE_SECURE` 可覆盖刷新 Cookie 的 Secure 默认值。
1128
1130
  *
1129
1131
  * @param input - 部署环境与显式允许的 Web/原生 Origin
1130
1132
  * @returns 可用于 HTTP 装配的策略
package/dist/index.js CHANGED
@@ -702,6 +702,81 @@ function buildAuthContextFactory(verifyToken) {
702
702
  }
703
703
  };
704
704
  }
705
+ function createRuntimeSecurityPolicy(input) {
706
+ const production = input.environment?.trim().toLowerCase() === "production";
707
+ const configuredOrigins = parseOrigins(input.corsOrigin);
708
+ const configuredNativeOrigins = parseNativeOrigins(input.nativeOrigins);
709
+ const allowsEveryOrigin = configuredOrigins === "*";
710
+ if (production && (allowsEveryOrigin || configuredOrigins.length === 0))
711
+ throw new Error("serv.cors.origin must contain explicit HTTP(S) origins in production");
712
+ const allowedOrigins = allowsEveryOrigin ? void 0 : new Set(configuredOrigins);
713
+ const allowedNativeOrigins = new Set(configuredNativeOrigins);
714
+ return {
715
+ allowOrigin: (origin) => {
716
+ const normalizedNativeOrigin = tryNormalizeNativeOrigin(origin);
717
+ if (normalizedNativeOrigin !== void 0 && allowedNativeOrigins.has(normalizedNativeOrigin))
718
+ return true;
719
+ if (allowedOrigins === void 0)
720
+ return true;
721
+ const normalizedWebOrigin = tryNormalizeWebOrigin(origin);
722
+ return normalizedWebOrigin !== void 0 && allowedOrigins.has(normalizedWebOrigin);
723
+ },
724
+ isNativeOrigin: (origin) => allowedNativeOrigins.has(tryNormalizeNativeOrigin(origin) ?? ""),
725
+ secureRefreshCookie: resolveSecureRefreshCookie(production),
726
+ exposeApiDocs: !production
727
+ };
728
+ }
729
+ function resolveSecureRefreshCookie(defaultSecure) {
730
+ const override = process.env.HAI_SERV_REFRESH_COOKIE_SECURE;
731
+ if (override === "true" || override === "false")
732
+ return override === "true";
733
+ return defaultSecure;
734
+ }
735
+ function parseOrigins(value) {
736
+ const entries = (value ?? "*").split(",").map((origin) => origin.trim()).filter(Boolean);
737
+ if (entries.includes("*"))
738
+ return "*";
739
+ return entries.map(normalizeWebOrigin);
740
+ }
741
+ function parseNativeOrigins(value) {
742
+ return (value ?? "").split(",").map((origin) => origin.trim()).filter(Boolean).map(normalizeNativeOrigin);
743
+ }
744
+ function normalizeWebOrigin(value) {
745
+ const normalized = tryNormalizeWebOrigin(value);
746
+ if (normalized === void 0)
747
+ throw new Error(`serv.cors.origin contains an invalid origin: ${value}`);
748
+ return normalized;
749
+ }
750
+ function normalizeNativeOrigin(value) {
751
+ const normalized = tryNormalizeNativeOrigin(value);
752
+ if (normalized === void 0)
753
+ throw new Error(`serv.cors.nativeOrigins contains an invalid origin: ${value}`);
754
+ return normalized;
755
+ }
756
+ function tryNormalizeWebOrigin(value) {
757
+ let url;
758
+ try {
759
+ url = new URL(value);
760
+ } catch {
761
+ return void 0;
762
+ }
763
+ if (!["http:", "https:"].includes(url.protocol) || url.origin === "null" || url.pathname !== "/" || url.search || url.hash)
764
+ return void 0;
765
+ return url.origin;
766
+ }
767
+ function tryNormalizeNativeOrigin(value) {
768
+ let url;
769
+ try {
770
+ url = new URL(value);
771
+ } catch {
772
+ return void 0;
773
+ }
774
+ if (!["http:", "https:", "capacitor:", "tauri:"].includes(url.protocol) || !url.hostname || url.username || url.password || !["", "/"].includes(url.pathname) || url.search || url.hash)
775
+ return void 0;
776
+ return ["capacitor:", "tauri:"].includes(url.protocol) ? `${url.protocol}//${url.host}` : url.origin;
777
+ }
778
+
779
+ // src/serv-cookie-auth.ts
705
780
  var IAM_AUTH_PATHS = {
706
781
  login: apiContract.pathOf(apiContract.iam.auth.login),
707
782
  loginWithOtp: apiContract.pathOf(apiContract.iam.auth.loginWithOtp),
@@ -721,7 +796,7 @@ function resolveRefreshCookieConfig(apiPrefix, config, iam) {
721
796
  return {
722
797
  cookieName: config.cookieName ?? "hai_refresh_token",
723
798
  maxAge: config.maxAge ?? 30 * 24 * 3600,
724
- isSecure: config.secure ?? (process.env.NODE_ENV === "production" || process.env.HAI_CORE_ENV === "production"),
799
+ isSecure: config.secure ?? resolveSecureRefreshCookie(process.env.NODE_ENV === "production" || process.env.HAI_CORE_ENV === "production"),
725
800
  refreshCookiePath: `${apiPrefix}${IAM_AUTH_PATHS.refresh}`,
726
801
  onRefresh,
727
802
  nativeTokenTransport: config.nativeTokenTransport
@@ -1482,75 +1557,6 @@ function listen(app, options = {}) {
1482
1557
  return handle;
1483
1558
  }
1484
1559
 
1485
- // src/serv-runtime-security.ts
1486
- function createRuntimeSecurityPolicy(input) {
1487
- const production = input.environment?.trim().toLowerCase() === "production";
1488
- const configuredOrigins = parseOrigins(input.corsOrigin);
1489
- const configuredNativeOrigins = parseNativeOrigins(input.nativeOrigins);
1490
- const allowsEveryOrigin = configuredOrigins === "*";
1491
- if (production && (allowsEveryOrigin || configuredOrigins.length === 0))
1492
- throw new Error("serv.cors.origin must contain explicit HTTP(S) origins in production");
1493
- const allowedOrigins = allowsEveryOrigin ? void 0 : new Set(configuredOrigins);
1494
- const allowedNativeOrigins = new Set(configuredNativeOrigins);
1495
- return {
1496
- allowOrigin: (origin) => {
1497
- const normalizedNativeOrigin = tryNormalizeNativeOrigin(origin);
1498
- if (normalizedNativeOrigin !== void 0 && allowedNativeOrigins.has(normalizedNativeOrigin))
1499
- return true;
1500
- if (allowedOrigins === void 0)
1501
- return true;
1502
- const normalizedWebOrigin = tryNormalizeWebOrigin(origin);
1503
- return normalizedWebOrigin !== void 0 && allowedOrigins.has(normalizedWebOrigin);
1504
- },
1505
- isNativeOrigin: (origin) => allowedNativeOrigins.has(tryNormalizeNativeOrigin(origin) ?? ""),
1506
- secureRefreshCookie: production,
1507
- exposeApiDocs: !production
1508
- };
1509
- }
1510
- function parseOrigins(value) {
1511
- const entries = (value ?? "*").split(",").map((origin) => origin.trim()).filter(Boolean);
1512
- if (entries.includes("*"))
1513
- return "*";
1514
- return entries.map(normalizeWebOrigin);
1515
- }
1516
- function parseNativeOrigins(value) {
1517
- return (value ?? "").split(",").map((origin) => origin.trim()).filter(Boolean).map(normalizeNativeOrigin);
1518
- }
1519
- function normalizeWebOrigin(value) {
1520
- const normalized = tryNormalizeWebOrigin(value);
1521
- if (normalized === void 0)
1522
- throw new Error(`serv.cors.origin contains an invalid origin: ${value}`);
1523
- return normalized;
1524
- }
1525
- function normalizeNativeOrigin(value) {
1526
- const normalized = tryNormalizeNativeOrigin(value);
1527
- if (normalized === void 0)
1528
- throw new Error(`serv.cors.nativeOrigins contains an invalid origin: ${value}`);
1529
- return normalized;
1530
- }
1531
- function tryNormalizeWebOrigin(value) {
1532
- let url;
1533
- try {
1534
- url = new URL(value);
1535
- } catch {
1536
- return void 0;
1537
- }
1538
- if (!["http:", "https:"].includes(url.protocol) || url.origin === "null" || url.pathname !== "/" || url.search || url.hash)
1539
- return void 0;
1540
- return url.origin;
1541
- }
1542
- function tryNormalizeNativeOrigin(value) {
1543
- let url;
1544
- try {
1545
- url = new URL(value);
1546
- } catch {
1547
- return void 0;
1548
- }
1549
- if (!["http:", "https:", "capacitor:", "tauri:"].includes(url.protocol) || !url.hostname || url.username || url.password || !["", "/"].includes(url.pathname) || url.search || url.hash)
1550
- return void 0;
1551
- return ["capacitor:", "tauri:"].includes(url.protocol) ? `${url.protocol}//${url.host}` : url.origin;
1552
- }
1553
-
1554
1560
  // src/serv-storage-assets.ts
1555
1561
  function storageAssets(config) {
1556
1562
  const pathPrefix = config.pathPrefix.endsWith("/") ? config.pathPrefix : `${config.pathPrefix}/`;