@gaonjs/web 0.3.0 → 0.5.0
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/dist/app.d.ts +6 -0
- package/dist/app.js +16 -0
- package/dist/auth.d.ts +1 -1
- package/dist/controller.d.ts +18 -0
- package/dist/controller.js +18 -0
- package/dist/dispatch.js +13 -1
- package/dist/host-router.d.ts +20 -0
- package/dist/host-router.js +114 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -1
- package/package.json +8 -8
package/dist/app.d.ts
CHANGED
|
@@ -27,6 +27,12 @@ export interface AppSpec {
|
|
|
27
27
|
* 이름별로 모은 맵. realtime(NATS)이 설정된 경우에만 활성.
|
|
28
28
|
*/
|
|
29
29
|
readonly channels?: ChannelMap;
|
|
30
|
+
/**
|
|
31
|
+
* app.config.ts 의 `hosts:` — 이 앱이 응답할 도메인(v0.16 §3.3).
|
|
32
|
+
* 생략해도 `<앱이름>.localhost` 관례는 자동 성립한다(host-router).
|
|
33
|
+
* 지정 시 그 도메인들도 이 앱으로 라우팅된다.
|
|
34
|
+
*/
|
|
35
|
+
readonly hosts?: readonly string[];
|
|
30
36
|
}
|
|
31
37
|
export interface SecurityOptions {
|
|
32
38
|
/** CORS 설정. 기본은 same-origin 만 허용(origin:false). false 로 완전 비활성. */
|
package/dist/app.js
CHANGED
|
@@ -16,6 +16,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
16
16
|
import { enterLogContext, rootLogger, setRootLogger } from '@gaonjs/core';
|
|
17
17
|
import { createChannelRuntime } from '@gaonjs/async';
|
|
18
18
|
import { prefixFor, registerApp } from './dispatch.js';
|
|
19
|
+
import { resolveHostRewrite } from './host-router.js';
|
|
19
20
|
import { registerSession } from './session.js';
|
|
20
21
|
import { jwtAuthAugment, sessionAuthAugment } from './auth.js';
|
|
21
22
|
import { registerChannels, registerWebSocketSupport, } from './websocket.js';
|
|
@@ -31,10 +32,25 @@ export async function createApp(opts) {
|
|
|
31
32
|
// 심어(enterLogContext) 요청 처리 중 모든 log.* 에 자동 전파한다.
|
|
32
33
|
const logger = rootLogger();
|
|
33
34
|
setRootLogger(logger);
|
|
35
|
+
// <앱>.localhost 매핑 준비 — 앱별 프리픽스·hosts 를 한 표로 만든다(v0.16 §3.3).
|
|
36
|
+
// rewriteUrl 은 라우트 해석 이전에 실행돼 URL 을 다시 쓰면 그대로 라우터가
|
|
37
|
+
// 새 URL 을 매칭한다(Fastify 문서 · rewriteUrl 옵션).
|
|
38
|
+
const hostBindings = opts.apps.map((s) => ({
|
|
39
|
+
name: s.name,
|
|
40
|
+
prefix: s.prefix ?? prefixFor(s.name),
|
|
41
|
+
hosts: (s.hosts ?? []).map((h) => h.toLowerCase()),
|
|
42
|
+
}));
|
|
34
43
|
const app = Fastify({
|
|
35
44
|
loggerInstance: logger,
|
|
36
45
|
// 분산 추적: 클라이언트가 x-request-id 를 주면 이어받는다.
|
|
37
46
|
requestIdHeader: 'x-request-id',
|
|
47
|
+
// <앱>.localhost / app.config.ts hosts: → URL 을 앱 프리픽스로 재작성.
|
|
48
|
+
// 관례에 없는 host 는 그대로 통과(원 프리픽스 접근이 계속 동작).
|
|
49
|
+
rewriteUrl: (req) => {
|
|
50
|
+
const rawUrl = req.url ?? '/';
|
|
51
|
+
const rewritten = resolveHostRewrite({ host: req.headers?.host, url: rawUrl }, hostBindings);
|
|
52
|
+
return rewritten ?? rawUrl;
|
|
53
|
+
},
|
|
38
54
|
...opts.fastify,
|
|
39
55
|
});
|
|
40
56
|
app.addHook('onRequest', async (request) => {
|
package/dist/auth.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export declare function isUnauthorized(e: unknown): boolean;
|
|
|
7
7
|
export interface AuthOptions {
|
|
8
8
|
/**
|
|
9
9
|
* session.userId → 사용자 로드. 앱이 도메인 모델로 구현한다(예:
|
|
10
|
-
* (id) => User.
|
|
10
|
+
* (id) => User.where('id','=',BigInt(id)).first()). falsy 반환은
|
|
11
11
|
* 비로그인 취급. web 이 data 를 직접 import 하지 않게 하는 이음새다.
|
|
12
12
|
*/
|
|
13
13
|
loadUser(id: unknown): Promise<GaonCurrentUser | null | undefined> | GaonCurrentUser | null | undefined;
|
package/dist/controller.d.ts
CHANGED
|
@@ -47,6 +47,13 @@ export declare class ValidationError extends Error {
|
|
|
47
47
|
readonly issues: readonly ValidationIssue[];
|
|
48
48
|
constructor(message: string, issues: readonly ValidationIssue[]);
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* this.notFound() 가 던지는 404 신호를 만든다. requireAuth 의 unauthorizedError
|
|
52
|
+
* 와 같은 방식(마커 프로퍼티) — 디스패처가 응답 분기에서 404 로 마감한다.
|
|
53
|
+
*/
|
|
54
|
+
export declare function notFoundError(message?: string): Error;
|
|
55
|
+
/** 던져진 값이 404 신호인지 판정한다(디스패처가 응답 분기에 사용). */
|
|
56
|
+
export declare function isNotFound(e: unknown): boolean;
|
|
50
57
|
export interface ValidationIssue {
|
|
51
58
|
/** 문제 필드 이름. */
|
|
52
59
|
readonly field: string;
|
|
@@ -152,6 +159,17 @@ export interface ControllerContext {
|
|
|
152
159
|
* 리다이렉트(폼/Inertia)하거나 401(API)로 응답한다. 로그인 상태면 사용자를 돌려준다.
|
|
153
160
|
*/
|
|
154
161
|
requireAuth(): GaonCurrentUser;
|
|
162
|
+
/**
|
|
163
|
+
* 리소스 부재 가드 — 404 로 응답한다. requireAuth 와 동형으로 예외를 던져
|
|
164
|
+
* 디스패처가 404 로 마감한다(never 반환이라 `return this.notFound()` 이후로는
|
|
165
|
+
* 값이 존재하는 것으로 좁혀진다):
|
|
166
|
+
* const post = await Post.find(id)
|
|
167
|
+
* if (!post) return this.notFound() // 이 뒤로 post 는 non-null
|
|
168
|
+
* return this.render('posts/Show', { post })
|
|
169
|
+
* never 를 반환하므로 액션의 추론 반환 타입(Rendered<P>)을 넓히지 않아
|
|
170
|
+
* 타입 브리지(pageProps/api)가 그대로 유지된다.
|
|
171
|
+
*/
|
|
172
|
+
notFound(message?: string): never;
|
|
155
173
|
readonly request: FastifyRequest;
|
|
156
174
|
readonly reply: FastifyReply;
|
|
157
175
|
}
|
package/dist/controller.js
CHANGED
|
@@ -27,6 +27,21 @@ export class ValidationError extends Error {
|
|
|
27
27
|
this.issues = issues;
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
/** this.notFound() 가 던지는 리소스 부재 신호. unauthorizedError 와 동형 마커. */
|
|
31
|
+
const NOT_FOUND = Symbol.for('gaon.web.notFound');
|
|
32
|
+
/**
|
|
33
|
+
* this.notFound() 가 던지는 404 신호를 만든다. requireAuth 의 unauthorizedError
|
|
34
|
+
* 와 같은 방식(마커 프로퍼티) — 디스패처가 응답 분기에서 404 로 마감한다.
|
|
35
|
+
*/
|
|
36
|
+
export function notFoundError(message) {
|
|
37
|
+
const e = new Error(message ?? 'Not Found');
|
|
38
|
+
e[NOT_FOUND] = true;
|
|
39
|
+
return e;
|
|
40
|
+
}
|
|
41
|
+
/** 던져진 값이 404 신호인지 판정한다(디스패처가 응답 분기에 사용). */
|
|
42
|
+
export function isNotFound(e) {
|
|
43
|
+
return (typeof e === 'object' && e !== null && e[NOT_FOUND] === true);
|
|
44
|
+
}
|
|
30
45
|
/** 요청 없이 호출되는 기본 컨텍스트(단위 테스트·타입 브리지). */
|
|
31
46
|
const defaultContext = {
|
|
32
47
|
render(page, props) {
|
|
@@ -62,6 +77,9 @@ const defaultContext = {
|
|
|
62
77
|
requireAuth() {
|
|
63
78
|
throw new Error('requireAuth: 인증 컨텍스트가 없습니다(단위 테스트 기본 컨텍스트).');
|
|
64
79
|
},
|
|
80
|
+
notFound(message) {
|
|
81
|
+
throw notFoundError(message);
|
|
82
|
+
},
|
|
65
83
|
// request/reply 는 실제 요청에서만 존재한다 — 기본 컨텍스트(단위 테스트·타입
|
|
66
84
|
// 브리지)는 의도적으로 생략하므로 unknown 경유로 캐스트한다.
|
|
67
85
|
};
|
package/dist/dispatch.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// 앱별 프리픽스는 createApp 의 캡슐화 스코프(register({ prefix }))가 담당하므로,
|
|
9
9
|
// 여기서는 라우트를 프리픽스 없는 상대 경로로 등록한다 — 세션·CSRF 도 같은
|
|
10
10
|
// 스코프에 갇혀 앱별 완전 분리가 성립한다(질문 13).
|
|
11
|
-
import { isJsonResult, isRedirect, isRendered, rawActions, ValidationError, } from './controller.js';
|
|
11
|
+
import { isJsonResult, isNotFound, isRedirect, isRendered, notFoundError, rawActions, ValidationError, } from './controller.js';
|
|
12
12
|
import { inertiaRedirect, renderInertia } from './inertia.js';
|
|
13
13
|
import { serializeProps } from './serialize.js';
|
|
14
14
|
import { isUnauthorized, unauthorizedError } from './auth.js';
|
|
@@ -113,6 +113,9 @@ function buildContext(request, reply, multipart) {
|
|
|
113
113
|
throw unauthorizedError();
|
|
114
114
|
return this.currentUser;
|
|
115
115
|
},
|
|
116
|
+
notFound(message) {
|
|
117
|
+
throw notFoundError(message);
|
|
118
|
+
},
|
|
116
119
|
request,
|
|
117
120
|
reply,
|
|
118
121
|
};
|
|
@@ -182,6 +185,15 @@ export function registerApp(app, dispatch, opts = {}) {
|
|
|
182
185
|
reply.code(401).send({ error: 'Unauthorized' });
|
|
183
186
|
return reply;
|
|
184
187
|
}
|
|
188
|
+
if (isNotFound(err)) {
|
|
189
|
+
// this.notFound(): 리소스 부재 → 404 JSON. Inertia 요청이면 클라이언트가
|
|
190
|
+
// 비-2xx 를 에러 모달로 처리한다(ValidationError 422 와 동형 마감).
|
|
191
|
+
reply
|
|
192
|
+
.code(404)
|
|
193
|
+
.header('Content-Type', 'application/json; charset=utf-8')
|
|
194
|
+
.send(JSON.stringify({ error: 'Not Found', message: err.message }));
|
|
195
|
+
return reply;
|
|
196
|
+
}
|
|
185
197
|
if (err instanceof ValidationError) {
|
|
186
198
|
// errata E-3: 검증 실패 → 422 JSON 자동. issues 는 필드·출처·사유 배열.
|
|
187
199
|
reply
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** 앱 하나에 대한 라우팅 관례. */
|
|
2
|
+
export interface AppHostBinding {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
/** URL 프리픽스(관례: web → '/', 그 외 → '/<name>'). */
|
|
5
|
+
readonly prefix: string;
|
|
6
|
+
/** app.config.ts 의 hosts: 배열(운영 도메인). 소문자로 정규화된 값. */
|
|
7
|
+
readonly hosts: readonly string[];
|
|
8
|
+
}
|
|
9
|
+
/** onRequest 훅이 호출자에게 판단을 위임하기 위해 쓰는 최소 뷰. */
|
|
10
|
+
export interface RequestView {
|
|
11
|
+
/** Host 헤더 그대로(포트 포함 가능). */
|
|
12
|
+
readonly host: string | undefined;
|
|
13
|
+
/** 현재 URL(예: '/foo?bar=1'). */
|
|
14
|
+
readonly url: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 요청 하나에 대한 URL 재작성을 계산한다(순수 함수 — 테스트 편의).
|
|
18
|
+
* 재작성이 필요 없으면 undefined 를 낸다.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveHostRewrite(req: RequestView, bindings: readonly AppHostBinding[]): string | undefined;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// @gaonjs/web · Host 헤더 기반 앱 라우팅 (v0.16 §3.3 · v0.11 확정)
|
|
2
|
+
//
|
|
3
|
+
// v0.16 §3.3 정본:
|
|
4
|
+
// "개발 모드에서는 로컬 도메인 매핑을 기본 지원한다(v0.11 확정) — 앱은
|
|
5
|
+
// admin.localhost 처럼 <앱이름>.localhost 로도 접근되며, 프리픽스
|
|
6
|
+
// 접근(/admin)도 함께 동작한다."
|
|
7
|
+
//
|
|
8
|
+
// 구현 전략: Fastify 라우트는 이미 앱별 프리픽스(admin → /admin) 아래 등록
|
|
9
|
+
// 돼 있다(app.ts createApp). 이 훅은 요청 URL 을 **먼저** 재작성한다:
|
|
10
|
+
//
|
|
11
|
+
// Host: admin.localhost:3000, URL: /foo
|
|
12
|
+
// → URL: /admin/foo (기존 프리픽스 라우팅이 받는다)
|
|
13
|
+
//
|
|
14
|
+
// 그러면 앱별 세션·라우트 스코프가 그대로 성립한다 — 새 라우팅 층을 만드는
|
|
15
|
+
// 대신 한 층만 추가한다(The One Way · §1.1).
|
|
16
|
+
//
|
|
17
|
+
// 규칙:
|
|
18
|
+
// 1) Host 가 정확히 `<앱이름>.localhost` 또는 `<앱이름>.localhost:<port>`
|
|
19
|
+
// 이고, 그 앱이 실제로 등록돼 있으면 URL 을 `/<앱이름>` 프리픽스로 재작성.
|
|
20
|
+
// 2) 이미 그 프리픽스가 URL 에 있으면(예: 사용자가 이미 명시적으로 접근)
|
|
21
|
+
// 재작성하지 않는다 — 중복 프리픽스 방지.
|
|
22
|
+
// 3) app.config.ts 의 hosts: 배열에 지정된 도메인도 동일하게 처리(운영 도메인
|
|
23
|
+
// 매핑까지 자연스럽게 확장).
|
|
24
|
+
// 4) web 앱(프리픽스 '/')은 재작성 대상이 아니다 — web.localhost 로 오면
|
|
25
|
+
// 원래 URL 이 그대로 web 앱 라우팅(프리픽스 '/')에 도달한다.
|
|
26
|
+
// 5) 매칭 실패는 조용히 통과(no-op) — 다른 앱 프리픽스 접근이 계속 동작한다.
|
|
27
|
+
/**
|
|
28
|
+
* 요청 하나에 대한 URL 재작성을 계산한다(순수 함수 — 테스트 편의).
|
|
29
|
+
* 재작성이 필요 없으면 undefined 를 낸다.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveHostRewrite(req, bindings) {
|
|
32
|
+
const host = normalizeHost(req.host);
|
|
33
|
+
if (!host)
|
|
34
|
+
return undefined;
|
|
35
|
+
// 1) 명시 hosts 우선 — 운영 도메인(예: 'admin.example.com').
|
|
36
|
+
// 2) 그다음 <앱이름>.localhost 관례.
|
|
37
|
+
const match = matchApp(host, bindings);
|
|
38
|
+
if (!match)
|
|
39
|
+
return undefined;
|
|
40
|
+
// web 앱(프리픽스 '/')은 URL 을 건들지 않는다.
|
|
41
|
+
if (match.prefix === '/' || match.prefix === '')
|
|
42
|
+
return undefined;
|
|
43
|
+
// 이미 프리픽스가 붙어 있으면 no-op(중복 방지).
|
|
44
|
+
if (urlStartsWithPrefix(req.url, match.prefix))
|
|
45
|
+
return undefined;
|
|
46
|
+
return joinPrefix(match.prefix, req.url);
|
|
47
|
+
}
|
|
48
|
+
/** Host 헤더에서 포트를 벗기고 소문자화한다. IPv6([::1]:port)는 대상 외. */
|
|
49
|
+
function normalizeHost(host) {
|
|
50
|
+
if (!host)
|
|
51
|
+
return undefined;
|
|
52
|
+
const trimmed = host.trim().toLowerCase();
|
|
53
|
+
if (!trimmed)
|
|
54
|
+
return undefined;
|
|
55
|
+
// IPv6 는 브래킷 형태 · 개발 모드 대상이 아님(관례는 localhost 서브도메인).
|
|
56
|
+
if (trimmed.startsWith('['))
|
|
57
|
+
return trimmed;
|
|
58
|
+
const colon = trimmed.lastIndexOf(':');
|
|
59
|
+
// 콜론이 있고 뒤가 숫자만이면 포트로 간주해 제거.
|
|
60
|
+
if (colon > 0 && /^\d+$/.test(trimmed.slice(colon + 1))) {
|
|
61
|
+
return trimmed.slice(0, colon);
|
|
62
|
+
}
|
|
63
|
+
return trimmed;
|
|
64
|
+
}
|
|
65
|
+
/** host 가 어떤 앱에 속하는지 찾는다. */
|
|
66
|
+
function matchApp(host, bindings) {
|
|
67
|
+
// 1) hosts: 명시 매칭 — 가장 강함(운영 정합).
|
|
68
|
+
for (const b of bindings) {
|
|
69
|
+
if (b.hosts.some((h) => h === host))
|
|
70
|
+
return b;
|
|
71
|
+
}
|
|
72
|
+
// 2) <이름>.localhost 관례.
|
|
73
|
+
// 예: 'admin.localhost' → 앱 이름 'admin'.
|
|
74
|
+
if (host.endsWith('.localhost')) {
|
|
75
|
+
const sub = host.slice(0, -'.localhost'.length);
|
|
76
|
+
// 서브도메인이 정확히 앱 이름과 같아야 한다(다단계 서브도메인은 제외).
|
|
77
|
+
if (sub && !sub.includes('.')) {
|
|
78
|
+
const found = bindings.find((b) => b.name === sub);
|
|
79
|
+
if (found)
|
|
80
|
+
return found;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
/** URL 이 특정 프리픽스(경로 부분)로 시작하는지 안전 검사. */
|
|
86
|
+
function urlStartsWithPrefix(url, prefix) {
|
|
87
|
+
// 프리픽스 정규화: 앞 '/' 보장 · 끝 '/' 제거.
|
|
88
|
+
const p = normalizePrefix(prefix);
|
|
89
|
+
if (p === '/')
|
|
90
|
+
return true;
|
|
91
|
+
// URL 은 쿼리·해시를 앞뒤로 가질 수 있어 pathname 만 잘라 비교.
|
|
92
|
+
const q = url.indexOf('?');
|
|
93
|
+
const pathname = q >= 0 ? url.slice(0, q) : url;
|
|
94
|
+
if (pathname === p)
|
|
95
|
+
return true;
|
|
96
|
+
return pathname.startsWith(p + '/');
|
|
97
|
+
}
|
|
98
|
+
/** URL 앞에 프리픽스를 붙인다(중복 슬래시 방지). */
|
|
99
|
+
function joinPrefix(prefix, url) {
|
|
100
|
+
const p = normalizePrefix(prefix);
|
|
101
|
+
if (p === '/')
|
|
102
|
+
return url;
|
|
103
|
+
if (url.startsWith('/'))
|
|
104
|
+
return p + url;
|
|
105
|
+
return p + '/' + url;
|
|
106
|
+
}
|
|
107
|
+
function normalizePrefix(prefix) {
|
|
108
|
+
if (!prefix || prefix === '/')
|
|
109
|
+
return '/';
|
|
110
|
+
let p = prefix.startsWith('/') ? prefix : '/' + prefix;
|
|
111
|
+
if (p.length > 1 && p.endsWith('/'))
|
|
112
|
+
p = p.slice(0, -1);
|
|
113
|
+
return p;
|
|
114
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export declare const version: string;
|
|
2
2
|
export { routes, type RouteBuilder, type RouteDef, type RouteEntry, type HttpMethod, } from "./routes.js";
|
|
3
|
-
export { controller, rawActions, RAW_ACTIONS, isRendered, isRedirect, isJsonResult, ValidationError, type Rendered, type Redirect, type JsonResult, type ActionResult, type ControllerContext, type AuthApi, type JwtApi, type JwtTokens, type GaonCurrentUser, type ValidationIssue, } from "./controller.js";
|
|
3
|
+
export { controller, rawActions, RAW_ACTIONS, isRendered, isRedirect, isJsonResult, isNotFound, notFoundError, ValidationError, type Rendered, type Redirect, type JsonResult, type ActionResult, type ControllerContext, type AuthApi, type JwtApi, type JwtTokens, type GaonCurrentUser, type ValidationIssue, } from "./controller.js";
|
|
4
4
|
export { mergeSources, coerceParams, normalizeQuery, type ParsedQuery, type Source, } from "./params.js";
|
|
5
5
|
export { collectRoutes, renderRoutesDts, generateRoutesDts, toImportSpecifier, bindingName, type DiscoveredRoute, } from "./generator.js";
|
|
6
6
|
export { createApp, type AppSpec, type SecurityOptions, type CreateAppOptions, } from "./app.js";
|
|
7
|
+
export { resolveHostRewrite, type AppHostBinding, type RequestView, } from "./host-router.js";
|
|
7
8
|
export { registerApp, prefixFor, type AppDispatch, type DispatchOptions, type ContextExtras, } from "./dispatch.js";
|
|
8
9
|
export { registerSession, type AppSessionOptions, } from "./session.js";
|
|
9
10
|
export { redisSessionStore, type RedisStoreOptions, } from "./redisStore.js";
|
package/dist/index.js
CHANGED
|
@@ -10,13 +10,15 @@ export const version = VERSION;
|
|
|
10
10
|
// 라우팅 DSL (§5.1) — 타입 브리지 + 실행 레이어 라우트 표
|
|
11
11
|
export { routes, } from "./routes.js";
|
|
12
12
|
// controller() — this.render/redirect/params/currentUser (§5.2·§7)
|
|
13
|
-
export { controller, rawActions, RAW_ACTIONS, isRendered, isRedirect, isJsonResult, ValidationError, } from "./controller.js";
|
|
13
|
+
export { controller, rawActions, RAW_ACTIONS, isRendered, isRedirect, isJsonResult, isNotFound, notFoundError, ValidationError, } from "./controller.js";
|
|
14
14
|
// this.params 안전 규칙 (errata E-3 §5.1~5.4)
|
|
15
15
|
export { mergeSources, coerceParams, normalizeQuery, } from "./params.js";
|
|
16
16
|
// routes.d.ts 생성기 (앱별 타입 브리지)
|
|
17
17
|
export { collectRoutes, renderRoutesDts, generateRoutesDts, toImportSpecifier, bindingName, } from "./generator.js";
|
|
18
18
|
// HTTP 실행 레이어 (Fastify 앱 팩토리·보안 기본값)
|
|
19
19
|
export { createApp, } from "./app.js";
|
|
20
|
+
// Host 헤더 기반 앱 라우팅 (v0.16 §3.3 · <앱>.localhost 매핑)
|
|
21
|
+
export { resolveHostRewrite, } from "./host-router.js";
|
|
20
22
|
// 라우트 디스패치 (라우트 표 → Fastify 등록)
|
|
21
23
|
export { registerApp, prefixFor, } from "./dispatch.js";
|
|
22
24
|
// 세션 배터리 (앱별 분리 + CSRF · Redis 스토어)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaonjs/web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Gaon 웹 레이어: Fastify 통합·파일 기반 라우팅·보안 기본값 (구현 예정)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"dist",
|
|
24
24
|
"README.md"
|
|
25
25
|
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json"
|
|
28
|
+
},
|
|
26
29
|
"dependencies": {
|
|
27
30
|
"@fastify/cookie": "^11.1.2",
|
|
28
31
|
"@fastify/cors": "^11.3.0",
|
|
@@ -31,14 +34,11 @@
|
|
|
31
34
|
"@fastify/rate-limit": "^11.1.0",
|
|
32
35
|
"@fastify/session": "^11.1.2",
|
|
33
36
|
"@fastify/websocket": "^11.0.2",
|
|
37
|
+
"@gaonjs/async": "workspace:*",
|
|
38
|
+
"@gaonjs/core": "workspace:*",
|
|
34
39
|
"bcryptjs": "^3.0.3",
|
|
35
40
|
"fastify": "^5.10.0",
|
|
36
41
|
"ioredis": "^5.11.1",
|
|
37
|
-
"jose": "^6.2.4"
|
|
38
|
-
"@gaonjs/async": "0.2.2",
|
|
39
|
-
"@gaonjs/core": "0.1.4"
|
|
40
|
-
},
|
|
41
|
-
"scripts": {
|
|
42
|
-
"build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json"
|
|
42
|
+
"jose": "^6.2.4"
|
|
43
43
|
}
|
|
44
|
-
}
|
|
44
|
+
}
|