@taicode/common-server 3.7.13 → 4.0.0-beta.1

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.
Files changed (130) hide show
  1. package/output/app/auth.test.d.ts +2 -0
  2. package/output/app/auth.test.d.ts.map +1 -0
  3. package/output/app/auth.test.js +148 -0
  4. package/output/app/boot.d.ts +48 -0
  5. package/output/app/boot.d.ts.map +1 -0
  6. package/output/app/boot.js +123 -0
  7. package/output/app/config.test.d.ts +2 -0
  8. package/output/app/config.test.d.ts.map +1 -0
  9. package/output/app/config.test.js +132 -0
  10. package/output/app/context.d.ts +68 -0
  11. package/output/app/context.d.ts.map +1 -0
  12. package/output/app/context.js +73 -0
  13. package/output/app/core/cookie.d.ts +25 -0
  14. package/output/app/core/cookie.d.ts.map +1 -0
  15. package/output/app/core/cookie.js +22 -0
  16. package/output/app/core/identity.d.ts +12 -0
  17. package/output/app/core/identity.d.ts.map +1 -0
  18. package/output/app/core/identity.js +14 -0
  19. package/output/app/core/index.d.ts +44 -0
  20. package/output/app/core/index.d.ts.map +1 -0
  21. package/output/app/core/index.js +57 -0
  22. package/output/app/core/logging.d.ts +13 -0
  23. package/output/app/core/logging.d.ts.map +1 -0
  24. package/output/app/core/logging.js +29 -0
  25. package/output/app/core/openapi.d.ts +17 -0
  26. package/output/app/core/openapi.d.ts.map +1 -0
  27. package/output/app/core/openapi.js +42 -0
  28. package/output/app/core/request-id.d.ts +36 -0
  29. package/output/app/core/request-id.d.ts.map +1 -0
  30. package/output/app/core/request-id.js +66 -0
  31. package/output/app/core/response.d.ts +41 -0
  32. package/output/app/core/response.d.ts.map +1 -0
  33. package/output/app/core/response.js +89 -0
  34. package/output/app/define-server.negative-cases.d.ts +32 -0
  35. package/output/app/define-server.negative-cases.d.ts.map +1 -0
  36. package/output/app/define-server.negative-cases.js +209 -0
  37. package/output/app/define-server.test.d.ts +2 -0
  38. package/output/app/define-server.test.d.ts.map +1 -0
  39. package/output/app/define-server.test.js +97 -0
  40. package/output/app/entries.d.ts +29 -0
  41. package/output/app/entries.d.ts.map +1 -0
  42. package/output/app/entries.js +64 -0
  43. package/output/app/env.d.ts +59 -0
  44. package/output/app/env.d.ts.map +1 -0
  45. package/output/app/env.js +66 -0
  46. package/output/app/env.test.d.ts +2 -0
  47. package/output/app/env.test.d.ts.map +1 -0
  48. package/output/app/env.test.js +124 -0
  49. package/output/app/error-contract.test.d.ts +2 -0
  50. package/output/app/error-contract.test.d.ts.map +1 -0
  51. package/output/app/error-contract.test.js +125 -0
  52. package/output/app/errors.d.ts +20 -0
  53. package/output/app/errors.d.ts.map +1 -0
  54. package/output/app/errors.js +51 -0
  55. package/output/app/generate-openapi.test.d.ts +2 -0
  56. package/output/app/generate-openapi.test.d.ts.map +1 -0
  57. package/output/app/generate-openapi.test.js +123 -0
  58. package/output/app/index.d.ts +16 -0
  59. package/output/app/index.d.ts.map +1 -0
  60. package/output/app/index.js +18 -0
  61. package/output/app/lifecycle-logging.test.d.ts +2 -0
  62. package/output/app/lifecycle-logging.test.d.ts.map +1 -0
  63. package/output/app/lifecycle-logging.test.js +155 -0
  64. package/output/app/lifecycle.d.ts +70 -0
  65. package/output/app/lifecycle.d.ts.map +1 -0
  66. package/output/app/lifecycle.js +99 -0
  67. package/output/app/lifecycle.test.d.ts +2 -0
  68. package/output/app/lifecycle.test.d.ts.map +1 -0
  69. package/output/app/lifecycle.test.js +152 -0
  70. package/output/app/middleware/auth.d.ts +71 -0
  71. package/output/app/middleware/auth.d.ts.map +1 -0
  72. package/output/app/middleware/auth.js +190 -0
  73. package/output/app/middleware/context.d.ts +13 -0
  74. package/output/app/middleware/context.d.ts.map +1 -0
  75. package/output/app/middleware/context.js +15 -0
  76. package/output/app/middleware/cors.d.ts +14 -0
  77. package/output/app/middleware/cors.d.ts.map +1 -0
  78. package/output/app/middleware/cors.js +30 -0
  79. package/output/app/middleware/index.d.ts +17 -0
  80. package/output/app/middleware/index.d.ts.map +1 -0
  81. package/output/app/middleware/index.js +16 -0
  82. package/output/app/middleware/rate-limit.d.ts +24 -0
  83. package/output/app/middleware/rate-limit.d.ts.map +1 -0
  84. package/output/app/middleware/rate-limit.js +46 -0
  85. package/output/app/middleware-order.test.d.ts +2 -0
  86. package/output/app/middleware-order.test.d.ts.map +1 -0
  87. package/output/app/middleware-order.test.js +148 -0
  88. package/output/app/permission-operations.d.ts +48 -0
  89. package/output/app/permission-operations.d.ts.map +1 -0
  90. package/output/app/permission-operations.js +77 -0
  91. package/output/app/permission-operations.test.d.ts +2 -0
  92. package/output/app/permission-operations.test.d.ts.map +1 -0
  93. package/output/app/permission-operations.test.js +72 -0
  94. package/output/app/permission-registration.test.d.ts +2 -0
  95. package/output/app/permission-registration.test.d.ts.map +1 -0
  96. package/output/app/permission-registration.test.js +138 -0
  97. package/output/app/redis.d.ts +45 -0
  98. package/output/app/redis.d.ts.map +1 -0
  99. package/output/app/redis.js +61 -0
  100. package/output/app/registered-operations.test.d.ts +2 -0
  101. package/output/app/registered-operations.test.d.ts.map +1 -0
  102. package/output/app/registered-operations.test.js +57 -0
  103. package/output/app/request-id.test.d.ts +2 -0
  104. package/output/app/request-id.test.d.ts.map +1 -0
  105. package/output/app/request-id.test.js +130 -0
  106. package/output/app/resources.test.d.ts +2 -0
  107. package/output/app/resources.test.d.ts.map +1 -0
  108. package/output/app/resources.test.js +197 -0
  109. package/output/app/shutdown.d.ts +22 -0
  110. package/output/app/shutdown.d.ts.map +1 -0
  111. package/output/app/shutdown.js +30 -0
  112. package/output/app/shutdown.test.d.ts +2 -0
  113. package/output/app/shutdown.test.d.ts.map +1 -0
  114. package/output/app/shutdown.test.js +103 -0
  115. package/output/app/signal.d.ts +12 -0
  116. package/output/app/signal.d.ts.map +1 -0
  117. package/output/app/signal.js +45 -0
  118. package/output/app/smoke.test.d.ts +2 -0
  119. package/output/app/smoke.test.d.ts.map +1 -0
  120. package/output/app/smoke.test.js +210 -0
  121. package/output/app/test-support.d.ts +21 -0
  122. package/output/app/test-support.d.ts.map +1 -0
  123. package/output/app/test-support.js +24 -0
  124. package/output/app/type-augment.d.ts +34 -0
  125. package/output/app/type-augment.d.ts.map +1 -0
  126. package/output/app/type-augment.js +7 -0
  127. package/output/app/types.d.ts +211 -0
  128. package/output/app/types.d.ts.map +1 -0
  129. package/output/app/types.js +2 -0
  130. package/package.json +12 -5
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=auth.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.test.d.ts","sourceRoot":"","sources":["../../source/app/auth.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,148 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { defineServer, startServer } from './entries.js';
3
+ import { createAuthMiddleware } from './middleware/auth.js';
4
+ import { createContextMiddleware } from './middleware/context.js';
5
+ const listWidgetSchema = { operationId: 'listWidget' };
6
+ const widgetDetailSchema = { operationId: 'getWidgetDetail' };
7
+ function captureVerify(capture) {
8
+ return async (input) => {
9
+ capture.push(input);
10
+ return { user: { id: 'user-1' }, token: input.token };
11
+ };
12
+ }
13
+ function createServer(configuration, authOptions = {}) {
14
+ return defineServer({
15
+ app: { service: 'auth', title: 'Auth', version: '1.0.0' },
16
+ resources: () => ({}),
17
+ config: () => configuration,
18
+ services: () => ({}),
19
+ middlewares: (ctx) => [
20
+ createContextMiddleware(),
21
+ createAuthMiddleware(ctx, { registerPermissions: false, ...authOptions }),
22
+ ],
23
+ routers: (ctx) => {
24
+ ctx.app.get('/widget/with-operation', { schema: listWidgetSchema }, () => ({ ok: true }));
25
+ ctx.app.get('/widget/no-operation', () => ({ ok: true }));
26
+ ctx.app.get('/widget/:id/detail', { schema: widgetDetailSchema }, () => ({ ok: true }));
27
+ },
28
+ });
29
+ }
30
+ const bearerToken = ['Bearer', 'token-1'].join(' ');
31
+ async function request(app, url, headers = {}) {
32
+ const response = await app.inject({
33
+ method: 'GET',
34
+ url,
35
+ headers: { authorization: bearerToken, ...headers },
36
+ });
37
+ expect(response.statusCode).toBe(200);
38
+ }
39
+ afterEach(() => {
40
+ vi.unstubAllGlobals();
41
+ });
42
+ describe('auth verify 入参', () => {
43
+ it('operation 优先取路由 schema.operationId,未声明时回落 url;service 取身份块', async () => {
44
+ const capture = [];
45
+ const app = await startServer(createServer({ account: { enabled: true, serviceUrl: 'http://account.local' } }, { verify: captureVerify(capture) }), { listen: false });
46
+ try {
47
+ await request(app, '/widget/with-operation');
48
+ await request(app, '/widget/no-operation');
49
+ expect(capture.map((input) => input.operation)).toEqual(['listWidget', '/widget/no-operation']);
50
+ expect(capture.map((input) => input.service)).toEqual(['auth', 'auth']);
51
+ }
52
+ finally {
53
+ await app.close();
54
+ }
55
+ });
56
+ });
57
+ describe('resource / metadata', () => {
58
+ it('resource 取路由参数、metadata 填请求元信息', async () => {
59
+ const capture = [];
60
+ const app = await startServer(createServer({ account: { enabled: true, serviceUrl: 'http://account.local' } }, { verify: captureVerify(capture) }), { listen: false });
61
+ try {
62
+ await request(app, '/widget/w-9/detail', { 'user-agent': 'auth-test-agent' });
63
+ const input = capture[0];
64
+ expect(input.resource).toEqual({ id: 'w-9' });
65
+ expect(input.metadata).toMatchObject({
66
+ method: 'GET',
67
+ url: '/widget/w-9/detail',
68
+ userAgent: 'auth-test-agent',
69
+ });
70
+ expect(typeof input.metadata?.requestId).toBe('string');
71
+ expect(typeof input.metadata?.ip).toBe('string');
72
+ }
73
+ finally {
74
+ await app.close();
75
+ }
76
+ });
77
+ it('内置 verify 的 POST body 一并带上 resource 与 metadata', async () => {
78
+ const requests = [];
79
+ const fetchStub = vi.fn(async (input, init) => {
80
+ requests.push({
81
+ url: typeof input === 'string' ? input : input.toString(),
82
+ body: init?.body ? JSON.parse(String(init.body)) : undefined,
83
+ });
84
+ return {
85
+ ok: true,
86
+ status: 200,
87
+ json: async () => ({ status: 'SUCCESS', data: { user: { id: 'u' }, token: 'token-1' } }),
88
+ };
89
+ });
90
+ vi.stubGlobal('fetch', fetchStub);
91
+ const app = await startServer(createServer({ account: { enabled: true, serviceUrl: 'http://account.local' } }), { listen: false });
92
+ try {
93
+ await request(app, '/widget/w-9/detail', { 'user-agent': 'auth-test-agent' });
94
+ expect(requests).toHaveLength(1);
95
+ expect(requests[0].url).toBe('http://account.local/gateway/verify');
96
+ expect(requests[0].body?.operation).toBe('getWidgetDetail');
97
+ expect(requests[0].body?.resource).toEqual({ id: 'w-9' });
98
+ expect(requests[0].body?.metadata).toMatchObject({
99
+ method: 'GET',
100
+ url: '/widget/w-9/detail',
101
+ userAgent: 'auth-test-agent',
102
+ });
103
+ }
104
+ finally {
105
+ await app.close();
106
+ }
107
+ });
108
+ });
109
+ describe('account serviceUrl 门槛仅作用于内置 verify', () => {
110
+ it('自定义 verify + account 无 serviceUrl 时仍然校验', async () => {
111
+ const capture = [];
112
+ const app = await startServer(createServer({ account: { enabled: false } }, { verify: captureVerify(capture) }), { listen: false });
113
+ try {
114
+ await request(app, '/widget/with-operation');
115
+ expect(capture.map((input) => input.operation)).toEqual(['listWidget']);
116
+ }
117
+ finally {
118
+ await app.close();
119
+ }
120
+ });
121
+ it('内置 verify + account 无 serviceUrl 时放行(不校验、不发请求)', async () => {
122
+ const fetchStub = vi.fn(async () => {
123
+ throw new Error('should not be called');
124
+ });
125
+ vi.stubGlobal('fetch', fetchStub);
126
+ const app = await startServer(createServer({ account: { enabled: false } }), { listen: false });
127
+ try {
128
+ // 无凭证也 200:无 serviceUrl 时内置 verify 直接放行
129
+ await request(app, '/widget/with-operation', { authorization: '' });
130
+ expect(fetchStub).not.toHaveBeenCalled();
131
+ }
132
+ finally {
133
+ await app.close();
134
+ }
135
+ });
136
+ it('跳过 docPath / OPTIONS / NoLoginRequired(默认 skip)', async () => {
137
+ const capture = [];
138
+ const app = await startServer(createServer({ account: { enabled: true, serviceUrl: 'http://account.local' } }, { verify: captureVerify(capture) }), { listen: false });
139
+ try {
140
+ const optionsResponse = await app.inject({ method: 'OPTIONS', url: '/widget/with-operation' });
141
+ expect(optionsResponse.statusCode).not.toBe(500);
142
+ expect(capture).toHaveLength(0);
143
+ }
144
+ finally {
145
+ await app.close();
146
+ }
147
+ });
148
+ });
@@ -0,0 +1,48 @@
1
+ import type { ErrorClassifier } from './core/response.js';
2
+ import type { ConfigEnv } from './env.js';
3
+ import type { ResourcesView, Server, ShutdownContext } from './types.js';
4
+ import type { FastifyInstance } from 'fastify';
5
+ /** 内部步序(`prepare` → `compose` → `activate`):`startServer` / `generateOpenApi` 共用。 */
6
+ export declare const BOOT_PHASE_START = "start";
7
+ export declare const BOOT_PHASE_OPENAPI = "openapi";
8
+ /** 两个入口的内部步序跳过集 */
9
+ export type BootPhase = typeof BOOT_PHASE_START | typeof BOOT_PHASE_OPENAPI;
10
+ /** 默认监听地址 */
11
+ export declare const DEFAULT_HOST = "0.0.0.0";
12
+ export interface BootInput<R, C, S> {
13
+ server: Server<R, C, S>;
14
+ env: ConfigEnv;
15
+ phase: BootPhase;
16
+ listen: boolean;
17
+ shutdown?: (ctx: ShutdownContext<R, C, S>) => Promise<void>;
18
+ }
19
+ export interface BootedServer<R, S> {
20
+ app: FastifyInstance;
21
+ resources: ResourcesView<R>;
22
+ services: Awaited<S>;
23
+ dispose: () => Promise<void>;
24
+ }
25
+ /**
26
+ * 资源包里的数据层错误分类器。
27
+ *
28
+ * 约定:`resources(ctx)` 返回的资源包里**任一**提供 `classifyError` 函数的值都会被用上
29
+ * (框架不提供数据层 helper;由服务仓在资源包里自行提供一个带 `classifyError` 的值)。
30
+ * 都没有 → 返回 `undefined`,响应层落 `UNKNOWN_ERROR` 兜底。
31
+ */
32
+ export declare function resolveErrorClassifier(resources: Record<string, unknown>): ErrorClassifier | undefined;
33
+ /**
34
+ * 装配一个服务(`startServer` / `generateOpenApi` 共用)。
35
+ *
36
+ * 步序(N1 §5):
37
+ * ```
38
+ * 1. 加载 env(框架固定,调用方已做) → env
39
+ * 2. resources(ctx={env,log}) → R(只构造,不建连)
40
+ * 3. config(ctx={env,log,resources}) → C(仅 start 路径;出 spec 路径不跑)
41
+ * 4. services(ctx={log,disposer,resources,config}) → S,随后按声明序 init()(仅 start 路径)
42
+ * 5. middlewares(ctx={app,log,services,config}) → 按数组序 register(app)
43
+ * 6. routers(ctx={app,log,services}) → 注册路由
44
+ * 7. ready / listen
45
+ * ```
46
+ */
47
+ export declare function bootServer<R, C, S>(input: BootInput<R, C, S>): Promise<BootedServer<R, S>>;
48
+ //# sourceMappingURL=boot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boot.d.ts","sourceRoot":"","sources":["../../source/app/boot.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AACzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AACzC,OAAO,KAAK,EAAe,aAAa,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AACrF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAE9C,qFAAqF;AACrF,eAAO,MAAM,gBAAgB,UAAU,CAAA;AACvC,eAAO,MAAM,kBAAkB,YAAY,CAAA;AAC3C,mBAAmB;AACnB,MAAM,MAAM,SAAS,GAAG,OAAO,gBAAgB,GAAG,OAAO,kBAAkB,CAAA;AAC3E,aAAa;AACb,eAAO,MAAM,YAAY,YAAY,CAAA;AAErC,MAAM,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;IACvB,GAAG,EAAE,SAAS,CAAA;IACd,KAAK,EAAE,SAAS,CAAA;IAChB,MAAM,EAAE,OAAO,CAAA;IACf,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5D;AAED,MAAM,WAAW,YAAY,CAAC,CAAC,EAAE,CAAC;IAChC,GAAG,EAAE,eAAe,CAAA;IACpB,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC,CAAA;IAC3B,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;IACpB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC7B;AAcD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,eAAe,GAAG,SAAS,CAOtG;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,UAAU,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAkGhG"}
@@ -0,0 +1,123 @@
1
+ import { Disposer } from '@taicode/common-base';
2
+ import { createConfigContext, createConfigHolder, createMiddlewareContext, createResourcesContext, createRouterContext, createServiceContext, createShutdownContext, liveResources, readConfigKey, requireConfig, } from './context.js';
3
+ import { createFastify, registerCore } from './core/index.js';
4
+ import { disposeServices, initServiceUnits, MIDDLEWARES_LOG_STAGE, middlewareUnitName } from './lifecycle.js';
5
+ import { runShutdown } from './shutdown.js';
6
+ import { attachSignalHandlers } from './signal.js';
7
+ /** 内部步序(`prepare` → `compose` → `activate`):`startServer` / `generateOpenApi` 共用。 */
8
+ export const BOOT_PHASE_START = 'start';
9
+ export const BOOT_PHASE_OPENAPI = 'openapi';
10
+ /** 默认监听地址 */
11
+ export const DEFAULT_HOST = '0.0.0.0';
12
+ function resolvePort(env) {
13
+ const parsed = Number(env.API_PORT);
14
+ if (!Number.isInteger(parsed) || parsed <= 0) {
15
+ throw new Error(`[server] API_PORT is required to listen (got "${env.API_PORT}")`);
16
+ }
17
+ return parsed;
18
+ }
19
+ function isRecord(value) {
20
+ return typeof value === 'object' && value !== null;
21
+ }
22
+ /**
23
+ * 资源包里的数据层错误分类器。
24
+ *
25
+ * 约定:`resources(ctx)` 返回的资源包里**任一**提供 `classifyError` 函数的值都会被用上
26
+ * (框架不提供数据层 helper;由服务仓在资源包里自行提供一个带 `classifyError` 的值)。
27
+ * 都没有 → 返回 `undefined`,响应层落 `UNKNOWN_ERROR` 兜底。
28
+ */
29
+ export function resolveErrorClassifier(resources) {
30
+ for (const value of Object.values(resources)) {
31
+ if (isRecord(value) && typeof value.classifyError === 'function') {
32
+ return (error) => value.classifyError(error);
33
+ }
34
+ }
35
+ return undefined;
36
+ }
37
+ /**
38
+ * 装配一个服务(`startServer` / `generateOpenApi` 共用)。
39
+ *
40
+ * 步序(N1 §5):
41
+ * ```
42
+ * 1. 加载 env(框架固定,调用方已做) → env
43
+ * 2. resources(ctx={env,log}) → R(只构造,不建连)
44
+ * 3. config(ctx={env,log,resources}) → C(仅 start 路径;出 spec 路径不跑)
45
+ * 4. services(ctx={log,disposer,resources,config}) → S,随后按声明序 init()(仅 start 路径)
46
+ * 5. middlewares(ctx={app,log,services,config}) → 按数组序 register(app)
47
+ * 6. routers(ctx={app,log,services}) → 注册路由
48
+ * 7. ready / listen
49
+ * ```
50
+ */
51
+ export async function bootServer(input) {
52
+ const { server, env, phase } = input;
53
+ const isStart = phase === BOOT_PHASE_START;
54
+ const service = server.app.service;
55
+ // ── prepare ──────────────────────────────────────────────────────────────
56
+ // ① createFastify(+ 内置 logger / requestId)
57
+ const app = createFastify(env);
58
+ const instance = app;
59
+ const log = app.log;
60
+ const disposer = new Disposer();
61
+ const holder = createConfigHolder();
62
+ // ② resources:只构造,不建连(出 spec 路径同样执行)
63
+ const resources = (await server.resources(createResourcesContext({ env, log })));
64
+ const resourcesView = liveResources(resources);
65
+ // ③ core 装配(schema → cookie → response → openapi → 路由注册表)
66
+ await registerCore(app, {
67
+ identity: server.app,
68
+ env,
69
+ classifyError: resolveErrorClassifier(resourcesView),
70
+ readConfig: (key) => readConfigKey(requireConfig(holder), key),
71
+ });
72
+ // ── compose ──────────────────────────────────────────────────────────────
73
+ // ① config(仅 start 路径;出 spec 路径不跑 → `ctx.config` 读到即抛 CONFIG_UNAVAILABLE)
74
+ if (isStart) {
75
+ holder.current = (await server.config(createConfigContext({ env, log, resources: resourcesView })));
76
+ }
77
+ // ② services(两个入口都执行;ctx 无 env、无 services)
78
+ const services = server.services(createServiceContext({
79
+ log,
80
+ disposer,
81
+ resources: resourcesView,
82
+ holder,
83
+ }));
84
+ // ③ initLifecycleUnits(仅 start 路径)
85
+ if (isStart) {
86
+ await initServiceUnits({ service, log, services: services });
87
+ }
88
+ // ④ middlewares(两个入口都执行:出 spec 也照注册,保证 schema 一致)
89
+ const middlewares = server.middlewares(createMiddlewareContext({ app: instance, log, services, holder }));
90
+ for (const [index, unit] of middlewares.entries()) {
91
+ await unit.register(instance);
92
+ log.debug({ service, unit: middlewareUnitName(service, unit, index), stage: MIDDLEWARES_LOG_STAGE }, '[middleware] registered');
93
+ }
94
+ // ⑤ routers(两个入口都执行)
95
+ await server.routers(createRouterContext({ app: instance, log, services }));
96
+ // ── activate ─────────────────────────────────────────────────────────────
97
+ const runShutdownFlow = async () => {
98
+ if (input.shutdown) {
99
+ await input.shutdown(createShutdownContext({
100
+ app: instance,
101
+ log,
102
+ disposer,
103
+ resources: resourcesView,
104
+ holder,
105
+ services,
106
+ }));
107
+ return;
108
+ }
109
+ await runShutdown({
110
+ stopProducers: () => disposer.dispose(),
111
+ disposeServices: () => disposeServices({ service, log, services: services }),
112
+ }, log);
113
+ };
114
+ app.addHook('onClose', () => runShutdownFlow());
115
+ // ⑥ ready(两个入口都执行)
116
+ await app.ready();
117
+ // ⑦ listen / signal(仅 start 路径)
118
+ if (isStart && input.listen) {
119
+ await app.listen({ port: resolvePort(env), host: DEFAULT_HOST });
120
+ attachSignalHandlers(app, log);
121
+ }
122
+ return { app, resources: resourcesView, services, dispose: runShutdownFlow };
123
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=config.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.test.d.ts","sourceRoot":"","sources":["../../source/app/config.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,132 @@
1
+ import { describe, expect, expectTypeOf, it } from 'vitest';
2
+ import { defineServer, generateOpenApi, startServer } from './entries.js';
3
+ import { SystemError, UserError } from './errors.js';
4
+ const rows = { 'server.domain': 'example.com' };
5
+ const fakeDatabase = {
6
+ get: (key) => Promise.resolve(rows[key]),
7
+ };
8
+ describe('config 槽位(N1 §2 / N2 §2.4)', () => {
9
+ it('① 入参含 env/log/resources,且不含 config/services/app', async () => {
10
+ const captured = [];
11
+ const server = defineServer({
12
+ app: { service: 'config-test', title: 'Config Test', version: '1.0.0' },
13
+ resources: () => ({ database: fakeDatabase }),
14
+ config: (ctx) => {
15
+ captured.push(ctx);
16
+ return { domain: 'example.com' };
17
+ },
18
+ services: () => ({}),
19
+ middlewares: () => [],
20
+ routers: () => undefined,
21
+ });
22
+ const app = await startServer(server, { listen: false });
23
+ try {
24
+ expect(captured).toHaveLength(1);
25
+ const ctx = captured[0];
26
+ expect('config' in ctx).toBe(false);
27
+ expect('services' in ctx).toBe(false);
28
+ expect('app' in ctx).toBe(false);
29
+ expect(ctx.resources.database).toBe(fakeDatabase);
30
+ expect(typeof ctx.log.info).toBe('function');
31
+ expect(Object.keys(ctx.env).sort()).toEqual([
32
+ 'API_PORT',
33
+ 'DATABASE_URL',
34
+ 'ENV',
35
+ 'LOG_LEVEL',
36
+ 'REDIS_URL',
37
+ ]);
38
+ }
39
+ finally {
40
+ await app.close();
41
+ }
42
+ });
43
+ it('② 可从「库」读配置:`services` / `middlewares` 经 `ctx.config` 拿到值', async () => {
44
+ let domainInServices;
45
+ let domainInMiddlewares;
46
+ const server = defineServer({
47
+ app: { service: 'config-from-database', title: 'Config From Database', version: '1.0.0' },
48
+ resources: () => ({ database: fakeDatabase }),
49
+ config: async (ctx) => ({ domain: (await ctx.resources.database.get('server.domain')) ?? '' }),
50
+ services: (ctx) => {
51
+ domainInServices = ctx.config.domain;
52
+ return {};
53
+ },
54
+ middlewares: (ctx) => {
55
+ domainInMiddlewares = ctx.config.domain;
56
+ return [];
57
+ },
58
+ routers: (ctx) => {
59
+ ctx.app.get('/domain', () => ({ domain: ctx.services === null ? '' : 'ok' }));
60
+ },
61
+ });
62
+ const app = await startServer(server, { listen: false });
63
+ try {
64
+ expect(domainInServices).toBe('example.com');
65
+ expect(domainInMiddlewares).toBe('example.com');
66
+ }
67
+ finally {
68
+ await app.close();
69
+ }
70
+ });
71
+ it('③ 同一份定义:startServer 跑 config,generateOpenApi 不跑(红线)', async () => {
72
+ let calls = 0;
73
+ const server = defineServer({
74
+ app: { service: 'config-redline', title: 'Config Red Line', version: '1.0.0' },
75
+ resources: () => ({ database: fakeDatabase }),
76
+ config: () => {
77
+ calls += 1;
78
+ return {};
79
+ },
80
+ services: () => ({}),
81
+ middlewares: () => [],
82
+ routers: () => undefined,
83
+ });
84
+ await generateOpenApi(server);
85
+ expect(calls).toBe(0);
86
+ const app = await startServer(server, { listen: false });
87
+ expect(calls).toBe(1);
88
+ await app.close();
89
+ });
90
+ it('④ 出 spec 路径:services 读 ctx.config 抛 CONFIG_UNAVAILABLE(构造期不得读配置)', async () => {
91
+ const server = defineServer({
92
+ app: { service: 'config-openapi', title: 'Config OpenApi', version: '1.0.0' },
93
+ resources: () => ({}),
94
+ config: () => ({ domain: 'spec' }),
95
+ services: (ctx) => {
96
+ // 构造期不得读配置;出 spec 路径上读到即抛
97
+ void ctx.config;
98
+ return {};
99
+ },
100
+ middlewares: () => [],
101
+ routers: () => undefined,
102
+ });
103
+ let caught;
104
+ try {
105
+ await generateOpenApi(server);
106
+ }
107
+ catch (error) {
108
+ caught = error;
109
+ }
110
+ expect(SystemError.is(caught)).toBe(true);
111
+ expect(caught.type).toBe('CONFIG_UNAVAILABLE');
112
+ });
113
+ it('⑤ `UserError` / `SystemError` 的 `status` 是字面量、`is()` 走品牌(N2 §2.12)', () => {
114
+ const user = new UserError('INVALID_TOKEN', undefined, 'boom');
115
+ const system = new SystemError('DATABASE_ERROR', undefined, 'boom');
116
+ // `status` 是字面量类型(改错直接编译失败)
117
+ expectTypeOf(user.status).toEqualTypeOf();
118
+ expectTypeOf(system.status).toEqualTypeOf();
119
+ expect(user.type).toBe('INVALID_TOKEN');
120
+ expect(user.status).toBe(400);
121
+ expect(system.status).toBe(500);
122
+ // `is()` 走 `Symbol.for` 品牌:**不用 `instanceof`**,原型链丢失也能识别
123
+ const plainUserError = Object.assign(Object.create(null), {
124
+ type: 'INVALID_TOKEN',
125
+ message: 'boom',
126
+ });
127
+ Object.defineProperty(plainUserError, Symbol.for('@taicode/common/UserError'), { value: true });
128
+ expect(UserError.is(plainUserError)).toBe(true);
129
+ expect(SystemError.is(plainUserError)).toBe(false);
130
+ expect(UserError.is(new Error('boom'))).toBe(false);
131
+ });
132
+ });
@@ -0,0 +1,68 @@
1
+ import type { Disposer } from '@taicode/common-base';
2
+ import type { ConfigEnv } from './env.js';
3
+ import type { AppInstance, ConfigContext, LoggerLike, MiddlewareContext, ResourcesContext, ResourcesView, RouterContext, ServiceContext, ShutdownContext } from './types.js';
4
+ /**
5
+ * 分阶段 ctx 的运行时构造(N1 §2)。
6
+ *
7
+ * 每个槽位拿到的是**字段最小集合**的普通对象;`config` 用 getter 暴露,出 spec 路径
8
+ * (`generateOpenApi` 不跑 `config` 槽位)读到它时抛 `SystemError('CONFIG_UNAVAILABLE')`
9
+ * —— 让「构造期不得读配置」有运行期护栏,而不仅是文档规则。
10
+ */
11
+ /** 运行时镜像 `Live<T>`:剔除值为 `false` 的键(编译期静态关闭) */
12
+ export declare function liveResources(resources: object): Record<string, unknown>;
13
+ /** `config` 槽位产物持有者(`startServer` 路径写入;出 spec 路径留空) */
14
+ export interface ConfigHolder<C> {
15
+ current: Awaited<C> | undefined;
16
+ }
17
+ export declare function createConfigHolder<C>(): ConfigHolder<C>;
18
+ /** 读配置:react 未就绪时抛 `SystemError('CONFIG_UNAVAILABLE')` */
19
+ export declare function requireConfig<C>(holder: ConfigHolder<C>): Awaited<C>;
20
+ /** 按**已知键**从配置值里取字段(框架内置单元消费配置的单一入口;不做形状校验) */
21
+ export declare function readConfigKey(config: unknown, key: string): unknown;
22
+ /** `resources(ctx)` 入参 */
23
+ export interface ResourcesContextParams {
24
+ env: ConfigEnv;
25
+ log: LoggerLike;
26
+ }
27
+ export declare function createResourcesContext(params: ResourcesContextParams): ResourcesContext;
28
+ /** `config(ctx)` 入参 */
29
+ export interface ConfigContextParams<R> {
30
+ env: ConfigEnv;
31
+ log: LoggerLike;
32
+ resources: ResourcesView<R>;
33
+ }
34
+ export declare function createConfigContext<R>(params: ConfigContextParams<R>): ConfigContext<R>;
35
+ /** `services(ctx)` 入参 */
36
+ export interface ServiceContextParams<R, C> {
37
+ log: LoggerLike;
38
+ disposer: Disposer;
39
+ resources: ResourcesView<R>;
40
+ holder: ConfigHolder<C>;
41
+ }
42
+ export declare function createServiceContext<R, C>(params: ServiceContextParams<R, C>): ServiceContext<R, C>;
43
+ /** `middlewares(ctx)` 入参 */
44
+ export interface MiddlewareContextParams<C, S> {
45
+ app: AppInstance;
46
+ log: LoggerLike;
47
+ services: Awaited<S>;
48
+ holder: ConfigHolder<C>;
49
+ }
50
+ export declare function createMiddlewareContext<C, S>(params: MiddlewareContextParams<C, S>): MiddlewareContext<C, S>;
51
+ /** `routers(ctx)` 入参 */
52
+ export interface RouterContextParams<S> {
53
+ app: AppInstance;
54
+ log: LoggerLike;
55
+ services: Awaited<S>;
56
+ }
57
+ export declare function createRouterContext<S>(params: RouterContextParams<S>): RouterContext<S>;
58
+ /** 关闭阶段视图入参 */
59
+ export interface ShutdownContextParams<R, C, S> {
60
+ app: AppInstance;
61
+ log: LoggerLike;
62
+ disposer: Disposer;
63
+ resources: ResourcesView<R>;
64
+ holder: ConfigHolder<C>;
65
+ services: Awaited<S>;
66
+ }
67
+ export declare function createShutdownContext<R, C, S>(params: ShutdownContextParams<R, C, S>): ShutdownContext<R, C, S>;
68
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../source/app/context.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ import { SystemError } from './errors.js';
2
+ /**
3
+ * 分阶段 ctx 的运行时构造(N1 §2)。
4
+ *
5
+ * 每个槽位拿到的是**字段最小集合**的普通对象;`config` 用 getter 暴露,出 spec 路径
6
+ * (`generateOpenApi` 不跑 `config` 槽位)读到它时抛 `SystemError('CONFIG_UNAVAILABLE')`
7
+ * —— 让「构造期不得读配置」有运行期护栏,而不仅是文档规则。
8
+ */
9
+ /** 运行时镜像 `Live<T>`:剔除值为 `false` 的键(编译期静态关闭) */
10
+ export function liveResources(resources) {
11
+ const result = {};
12
+ for (const [key, value] of Object.entries(resources)) {
13
+ if (value !== false) {
14
+ result[key] = value;
15
+ }
16
+ }
17
+ return result;
18
+ }
19
+ export function createConfigHolder() {
20
+ return { current: undefined };
21
+ }
22
+ /** 读配置:react 未就绪时抛 `SystemError('CONFIG_UNAVAILABLE')` */
23
+ export function requireConfig(holder) {
24
+ if (holder.current === undefined) {
25
+ throw new SystemError('CONFIG_UNAVAILABLE', undefined, '配置尚未就绪(generateOpenApi 路径不跑 config 槽位):任何单元不得在构造期读配置');
26
+ }
27
+ return holder.current;
28
+ }
29
+ /** 按**已知键**从配置值里取字段(框架内置单元消费配置的单一入口;不做形状校验) */
30
+ export function readConfigKey(config, key) {
31
+ return config?.[key];
32
+ }
33
+ export function createResourcesContext(params) {
34
+ return { env: params.env, log: params.log };
35
+ }
36
+ export function createConfigContext(params) {
37
+ return { env: params.env, log: params.log, resources: params.resources };
38
+ }
39
+ export function createServiceContext(params) {
40
+ return {
41
+ log: params.log,
42
+ disposer: params.disposer,
43
+ resources: params.resources,
44
+ get config() {
45
+ return requireConfig(params.holder);
46
+ },
47
+ };
48
+ }
49
+ export function createMiddlewareContext(params) {
50
+ return {
51
+ app: params.app,
52
+ log: params.log,
53
+ services: params.services,
54
+ get config() {
55
+ return requireConfig(params.holder);
56
+ },
57
+ };
58
+ }
59
+ export function createRouterContext(params) {
60
+ return { app: params.app, log: params.log, services: params.services };
61
+ }
62
+ export function createShutdownContext(params) {
63
+ return {
64
+ app: params.app,
65
+ log: params.log,
66
+ disposer: params.disposer,
67
+ resources: params.resources,
68
+ get config() {
69
+ return requireConfig(params.holder);
70
+ },
71
+ services: params.services,
72
+ };
73
+ }
@@ -0,0 +1,25 @@
1
+ import type { ConfigEnv } from '../env.js';
2
+ import type { FastifyInstance } from 'fastify';
3
+ /**
4
+ * core `cookie`(设计 §5.1)——**框架内置、锁定、不可替换/修改**。
5
+ *
6
+ * `@fastify/cookie`;cookie 名 / domain 从 `ctx.readConfig('server')` 取(**请求期惰性现读**,刷新后即生效);
7
+ * 显式设 `sameSite`(默认 `lax`)与 `secure`(默认 `ENV === 'production'`);注册在 `auth` 之前
8
+ * (框架内置序),故 `auth` 读 cookie 无需用户保证。
9
+ */
10
+ export interface AuthCookieOptions {
11
+ maxAge?: number;
12
+ path?: string;
13
+ domain?: string;
14
+ }
15
+ export interface CookieCoreOptions {
16
+ env: ConfigEnv;
17
+ /** 取最新配置中某个键的访问器(**每次调用现读**,不得缓存返回值) */
18
+ readConfig: (key: string) => unknown;
19
+ }
20
+ /** 从 `readConfig('server')` 的返回值读取 cookie 名列表(缺省 `['token']`,对齐四家现状) */
21
+ export declare function resolveCookieNames(server: unknown): string[];
22
+ /** 从 `readConfig('server')` 的返回值读取 cookie domain */
23
+ export declare function resolveCookieDomain(server: unknown): string | undefined;
24
+ export declare function registerCookie(app: FastifyInstance, options: CookieCoreOptions): void;
25
+ //# sourceMappingURL=cookie.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cookie.d.ts","sourceRoot":"","sources":["../../../source/app/core/cookie.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAC1C,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,SAAS,CAAA;AAE5D;;;;;;GAMG;AAEH,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAOD,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,SAAS,CAAA;IACd,wCAAwC;IACxC,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAA;CACrC;AAED,wEAAwE;AACxE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,EAAE,CAG5D;AAED,oDAAoD;AACpD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAEvE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAerF"}