@gaonjs/cli 0.29.0 → 0.30.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.
@@ -279,10 +279,12 @@ function writeRegenLine(regen) {
279
279
  process.stdout.write(' · .gaon 재생성 — skipped (domain/schema · apps/* 없음)\n');
280
280
  return;
281
281
  }
282
+ // 실 출력 경로를 그대로 찍는다(W2): tables.d.ts 는 루트 `.gaon/`, routes.d.ts 는
283
+ // 앱별 `apps/<앱>/.gaon/`. 옛 로그는 `web/routes.d.ts` 로 찍어 실 경로를 오도했다.
282
284
  const parts = [];
283
285
  if (regen.tables)
284
- parts.push('tables.d.ts');
286
+ parts.push('.gaon/tables.d.ts');
285
287
  for (const app of regen.apps)
286
- parts.push(`${app}/routes.d.ts`);
288
+ parts.push(`apps/${app}/.gaon/routes.d.ts`);
287
289
  process.stdout.write(` ✓ .gaon 재생성 — ${parts.join(', ')}\n`);
288
290
  }
@@ -0,0 +1,29 @@
1
+ import { type RegenResult } from '../dev.js';
2
+ export interface GenCommandOptions {
3
+ readonly cwd?: string;
4
+ readonly json?: boolean;
5
+ }
6
+ export interface GenResult {
7
+ readonly ok: boolean;
8
+ /** 재생성이 스킵됐는가(domain/schema · apps/* 없음). */
9
+ readonly skipped: boolean;
10
+ /** tables.d.ts 를 재생성했는가. */
11
+ readonly tables: boolean;
12
+ /** routes.d.ts·routes.manifest.ts 를 재생성한 앱 이름. */
13
+ readonly apps: readonly string[];
14
+ /** 실패 시 에러 + 수리 안내. */
15
+ readonly error?: string;
16
+ }
17
+ /**
18
+ * cwd 관례로 프로젝트 .gaon 을 1회 전체 재생성한다(검사·서버 없이). 생성기는 사용자
19
+ * .ts 를 동적 import 하므로 `.js`→`.ts` 해석 훅을 먼저 등록한다(gaon check 와 동일).
20
+ * 스키마·앱이 하나도 없으면 재생성 대상 없음으로 스킵한다. 실패는 throw 로 올린다.
21
+ */
22
+ export declare function regenerateProjectGaon(cwd: string): Promise<RegenResult & {
23
+ skipped: boolean;
24
+ }>;
25
+ /**
26
+ * `gaon gen` 진입점. .gaon 을 재생성하고 결과를 보고한다. 재생성 대상이 없으면
27
+ * 스킵으로 리포트(우회가 아니라 "설정 부족" 노출). 실패 시 exit 1 + 수리 안내.
28
+ */
29
+ export declare function runGenCommand(opts?: GenCommandOptions): Promise<number>;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @gaonjs/cli · `gaon gen` — .gaon 타입 브리지 + 런타임 매니페스트 재생성 (결정 127)
3
+ *
4
+ * `gaon dev`(워처)·`gaon check`(검사 직전) 는 이미 재생성을 편승시키지만, **개발 서버
5
+ * 없이 재생성만** 필요한 경로가 있다:
6
+ * · 프로덕션 빌드 — 스캐폴드 `build` 스크립트가 `gaon gen && vite build` 로 돈다.
7
+ * .gaon/ 은 gitignore 대상이라 fresh-clone 에는 없다 — 값 import 인
8
+ * routes.manifest.ts 가 빌드 전에 존재해야 하므로 빌드가 재생성을 탄다(결정 127 · C-1).
9
+ * · 편집기 타입 즉시 반영 — 사용자가 손으로 한 번 돌려 routes.d.ts·tables.d.ts 를 채운다.
10
+ *
11
+ * 규칙 3(결정 127): routes 축은 routes.d.ts(타입) + routes.manifest.ts(런타임 값) 2파일로
12
+ * 물성화된다 — generateRoutesDts 한 번이 둘을 함께 낳는다.
13
+ *
14
+ * §9 실 인프라 · 목업 X — 생성기는 사용자 스키마·컨트롤러 .ts 를 실제로 동적 import 한다.
15
+ */
16
+ import { generateTablesDts } from '@gaonjs/data';
17
+ import { generateRoutesDts } from '@gaonjs/web';
18
+ import { regenerateGaonOnce, resolveDevLayout } from '../dev.js';
19
+ import { registerTsResolve } from '../tsResolve.js';
20
+ /**
21
+ * cwd 관례로 프로젝트 .gaon 을 1회 전체 재생성한다(검사·서버 없이). 생성기는 사용자
22
+ * .ts 를 동적 import 하므로 `.js`→`.ts` 해석 훅을 먼저 등록한다(gaon check 와 동일).
23
+ * 스키마·앱이 하나도 없으면 재생성 대상 없음으로 스킵한다. 실패는 throw 로 올린다.
24
+ */
25
+ export async function regenerateProjectGaon(cwd) {
26
+ const layout = resolveDevLayout(cwd);
27
+ if (!layout.schemaDir && layout.apps.length === 0) {
28
+ return { tables: false, apps: [], skipped: true };
29
+ }
30
+ registerTsResolve();
31
+ const result = await regenerateGaonOnce(layout, {
32
+ regenerateTables: generateTablesDts,
33
+ regenerateRoutes: generateRoutesDts,
34
+ });
35
+ return { ...result, skipped: false };
36
+ }
37
+ /**
38
+ * `gaon gen` 진입점. .gaon 을 재생성하고 결과를 보고한다. 재생성 대상이 없으면
39
+ * 스킵으로 리포트(우회가 아니라 "설정 부족" 노출). 실패 시 exit 1 + 수리 안내.
40
+ */
41
+ export async function runGenCommand(opts = {}) {
42
+ const cwd = opts.cwd ?? process.cwd();
43
+ const json = opts.json ?? false;
44
+ let result;
45
+ try {
46
+ const r = await regenerateProjectGaon(cwd);
47
+ result = { ok: true, skipped: r.skipped, tables: r.tables, apps: r.apps };
48
+ }
49
+ catch (err) {
50
+ const msg = err instanceof Error ? err.message : String(err);
51
+ result = {
52
+ ok: false,
53
+ skipped: false,
54
+ tables: false,
55
+ apps: [],
56
+ error: `.gaon 재생성에 실패했습니다: ${msg}\n` +
57
+ `→ domain/schema/*.ts 와 apps/*/routes.ts·controllers/*.ts 의 구문 오류를 고친 뒤 다시 실행하세요.`,
58
+ };
59
+ }
60
+ if (json) {
61
+ process.stdout.write(JSON.stringify(result) + '\n');
62
+ }
63
+ else if (!result.ok) {
64
+ process.stderr.write(` ✗ gaon gen — 실패\n ${(result.error ?? '').split('\n').join('\n ')}\n`);
65
+ }
66
+ else if (result.skipped) {
67
+ process.stdout.write(' · gaon gen — 재생성 대상 없음 (domain/schema · apps/* 확인)\n');
68
+ }
69
+ else {
70
+ const parts = [];
71
+ if (result.tables)
72
+ parts.push('.gaon/tables.d.ts');
73
+ for (const app of result.apps) {
74
+ parts.push(`apps/${app}/.gaon/routes.d.ts`, `apps/${app}/.gaon/routes.manifest.ts`);
75
+ }
76
+ process.stdout.write(` ✓ gaon gen — ${parts.join(', ')}\n`);
77
+ }
78
+ return result.ok ? 0 : 1;
79
+ }
@@ -24,6 +24,7 @@ import { fileURLToPath } from 'node:url';
24
24
  import { readFileSync } from 'node:fs';
25
25
  import { renderProjectFiles } from '../templates/index.js';
26
26
  import { writeUiKitScaffold } from '../uikit.js';
27
+ import { regenerateProjectGaon } from './gen.js';
27
28
  /** 이름 유효성 — npm 패키지명 규칙(단순 부분)만 검사. */
28
29
  function validateProjectName(name) {
29
30
  if (!name)
@@ -224,6 +225,22 @@ export async function runNewCommand(name, opts = {}) {
224
225
  return 1;
225
226
  }
226
227
  }
228
+ // C-1(결정 127) — 설치 성공 후 .gaon 을 한 번 재생성한다. 생성기가 사용자 .ts 를
229
+ // 동적 import 하므로 node_modules 가 필요하다(skipInstall 이면 건너뜀 — dev/check/build
230
+ // 가 재생성을 탄다). 값 import 인 routes.manifest.ts 가 생겨 gaon new 직후 vite build 도
231
+ // 바로 돌고, routes.d.ts 로 편집기 타입이 즉시 잡힌다. 실패해도 스캐폴드는 성공으로
232
+ // 둔다(best-effort · 재생성은 dev/check/gen 이 상시 보장) — 경고만 남긴다.
233
+ if (installReport.ran && installReport.exitCode === 0) {
234
+ try {
235
+ await regenerateProjectGaon(root);
236
+ }
237
+ catch (err) {
238
+ if (!json) {
239
+ process.stderr.write(` · .gaon 초기 재생성 경고: ${err instanceof Error ? err.message : String(err)}\n` +
240
+ ` → gaon dev·gaon check·gaon gen 이 재생성하니 무시해도 됩니다.\n`);
241
+ }
242
+ }
243
+ }
227
244
  // git init + 첫 커밋 — 스킵 시 그대로 넘어감.
228
245
  const gitReport = { ran: false, skipped: opts.skipGit === true, initialized: false, firstCommit: false };
229
246
  if (!opts.skipGit) {
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export { startDev, resolveDevLayout, regenerateGaonOnce, type DevDeps, type DevL
3
3
  export { runDevCommand, type DevCommandOptions } from "./commands/dev.js";
4
4
  export { createDevConsole, findComposeFile, isDockerAvailable, inspectCompose, composeUp, composeDown, ensureInfra, startTscWatchers, killChild, startRestartWatcher, isRestartChange, resolveWatchRoots, type DevConsole, type DevConsoleOptions, type DevSource, type DevLevel, type ComposeStatus, type ComposeUpOptions, type EnsureInfraResult, type DockerLocateOptions, type TscWatcherOptions, type TscWatcherHandle, type RestartWatcherOptions, type RestartWatcherHandle, } from "./dev/index.js";
5
5
  export { runCheckCommand, type CheckCommandOptions, type CheckStep, type CheckStepStatus, type CheckStepResult, } from "./commands/check.js";
6
+ export { runGenCommand, regenerateProjectGaon, type GenCommandOptions, type GenResult, } from "./commands/gen.js";
6
7
  export { runNewCommand, type NewCommandOptions, type NewCommandResult } from "./commands/new.js";
7
8
  export { runConsoleCommand, type ConsoleCommandOptions } from "./commands/console.js";
8
9
  export { runTestCommand, type TestCommandOptions, type TestScope } from "./commands/test.js";
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@
13
13
  import { MILESTONES, VERSION, HOMEPAGE, loadDotEnv } from "@gaonjs/core";
14
14
  import { runDevCommand } from "./commands/dev.js";
15
15
  import { runCheckCommand } from "./commands/check.js";
16
+ import { runGenCommand } from "./commands/gen.js";
16
17
  import { runNewCommand } from "./commands/new.js";
17
18
  import { runConsoleCommand } from "./commands/console.js";
18
19
  import { runTestCommand } from "./commands/test.js";
@@ -30,6 +31,7 @@ export { startDev, resolveDevLayout, regenerateGaonOnce, } from "./dev.js";
30
31
  export { runDevCommand } from "./commands/dev.js";
31
32
  export { createDevConsole, findComposeFile, isDockerAvailable, inspectCompose, composeUp, composeDown, ensureInfra, startTscWatchers, killChild, startRestartWatcher, isRestartChange, resolveWatchRoots, } from "./dev/index.js";
32
33
  export { runCheckCommand, } from "./commands/check.js";
34
+ export { runGenCommand, regenerateProjectGaon, } from "./commands/gen.js";
33
35
  export { runNewCommand } from "./commands/new.js";
34
36
  export { runConsoleCommand } from "./commands/console.js";
35
37
  export { runTestCommand } from "./commands/test.js";
@@ -99,6 +101,7 @@ function renderHelp(version = VERSION) {
99
101
  " gaon serve --port <n> --host <h> 리슨 포트·호스트 (config 값을 덮음)",
100
102
  " gaon serve --workers <n|auto> node:cluster 워커 다중화 (env WEB_CONCURRENCY · 기본 1)",
101
103
  " gaon check typecheck · vue-tsc · build 통합 검사 (--only <step> · --include-doctor)",
104
+ " gaon gen .gaon 타입 브리지 + api() 런타임 매니페스트만 재생성 (서버·검사 없이 · build 전제 · --json)",
102
105
  " gaon console 프로젝트 컨텍스트 REPL (--no-config)",
103
106
  " gaon test 테스트 러너 (테스트 DB <db>_test 자동 생성·마이그레이션 후 vitest · --scope unit|integration|all · -- vitest 인자)",
104
107
  " gaon doctor 정적 검사 (24 검사 · 응답 혼용·N+1·의존·커넥션·마이그·컴포저블 순수·자동 import·파일명/컬럼 관례·인증 배선·UI 킷 배선·라우트 등록·정적 충돌·_method·CSRF 배선·내부 앵커·pageProps 구조분해·비동기 오프로드·페이지 레이아웃 브레이크포인트·Link>Button 중첩·seal 클라 배선·보안 역전)",
@@ -241,6 +244,20 @@ export function runCli(argv, opts = {}) {
241
244
  });
242
245
  return;
243
246
  }
247
+ // `gaon gen` — .gaon 타입 브리지 + 런타임 매니페스트만 재생성(서버·검사 없이 · 결정 127).
248
+ // 스캐폴드 `build` 스크립트(`gaon gen && vite build`)와 편집기 타입 즉시 반영에 쓴다.
249
+ if (argv[0] === "gen") {
250
+ void runGenCommand({ json: argv.includes("--json") })
251
+ .then((code) => {
252
+ process.exitCode = code;
253
+ })
254
+ .catch((err) => {
255
+ const msg = err instanceof Error ? err.message : String(err);
256
+ process.stderr.write(` ✗ gaon gen 실패: ${msg}\n`);
257
+ process.exitCode = 1;
258
+ });
259
+ return;
260
+ }
244
261
  // `gaon doctor` — 정적 검사(M9-E · 17 검사). --check=<이름>[,<이름>...] 로
245
262
  // 선택 실행, --json 은 자동화 파싱용.
246
263
  // exit code (M9-E-Fix): fatal → 2(사용자 오류) / errors > 0 → 1 / 그 외 → 0.
@@ -0,0 +1,29 @@
1
+ // 웹 앱 팩토리 — gaon g auth 스캐폴드. createWebApp 으로 세션·인증을 배선한다.
2
+ import { createApp, type AppSessionOptions } from 'gaonjs/web'
3
+ import appRoutes from './routes.js'
4
+ import session from './controllers/session.js'
5
+ import registration from './controllers/registration.js'
6
+ import dashboard from './controllers/dashboard.js'
7
+ import { loadUser } from './auth.js'
8
+
9
+ export interface WebAppDeps {
10
+ /** 세션 설정 — { redisUrl, secret } (또는 redis 인스턴스). */
11
+ readonly session: AppSessionOptions
12
+ /** 서명 쿠키/CSRF 용 비밀. */
13
+ readonly cookieSecret?: string
14
+ }
15
+
16
+ export function createWebApp(deps: WebAppDeps) {
17
+ return createApp({
18
+ apps: [
19
+ {
20
+ name: '{{APP_NAME}}',
21
+ routes: appRoutes,
22
+ controllers: { session, registration, dashboard },
23
+ session: deps.session,
24
+ auth: { loadUser, loginRedirect: '/session/new' },
25
+ },
26
+ ],
27
+ cookieSecret: deps.cookieSecret,
28
+ })
29
+ }
@@ -0,0 +1,14 @@
1
+ // 서버 진입점 — gaon g auth 스캐폴드. `node dist/server.js` 로 실행.
2
+ import { createWebApp } from './apps/{{APP_NAME}}/app.js'
3
+
4
+ const app = await createWebApp({
5
+ session: {
6
+ redisUrl: process.env.REDIS_URL ?? 'redis://127.0.0.1:6379',
7
+ secret: process.env.SESSION_SECRET ?? 'change-me-to-a-32+char-random-secret!!',
8
+ },
9
+ cookieSecret: process.env.COOKIE_SECRET,
10
+ })
11
+
12
+ const port = Number(process.env.PORT ?? 3000)
13
+ await app.listen({ port, host: '0.0.0.0' })
14
+ console.log(`web 앱이 http://localhost:${port} 에서 실행 중입니다.`)
@@ -39,9 +39,15 @@ export default channel({
39
39
  onJoin(ctx) {
40
40
  ctx.broadcast({ type: 'joined', member: ctx.member })
41
41
  },
42
- // 클라이언트 메시지 수신
42
+ // 클라이언트 메시지 수신 — raw data 를 그대로 되쏘지 않는다(작성자 위조·XSS 표면).
43
43
  async onMessage(ctx, data) {
44
- ctx.broadcast(data) // 서버의 연결로 팬아웃
44
+ // 본문만 검증·상한해 취하고, 작성자는 서버 권위(ctx.user)로 붙인다 —
45
+ // 클라가 보낸 author/id 는 신뢰하지 않는다(§정본 예시 chatMessages.ts).
46
+ const raw = (data as { text?: unknown }).text
47
+ const text = typeof raw === 'string' ? raw.trim().slice(0, 2000) : ''
48
+ if (!text) return // 빈/비정상 메시지는 흘리지 않는다
49
+ const u = ctx.user as { id: bigint; name: string } | null
50
+ ctx.broadcast({ text, author: u ? { id: String(u.id), name: u.name } : null })
45
51
  },
46
52
  // 이탈 (연결 종료·프레즌스 해제 후)
47
53
  onLeave(ctx) {
@@ -181,10 +187,26 @@ import { channel } from 'gaonjs/async'
181
187
  export default channel({
182
188
  authorize(ctx) { return ctx.user != null },
183
189
  presenceInfo(ctx) { return { name: (ctx.user as { name: string } | null)?.name ?? '익명' } },
184
- async onMessage(ctx, data) { ctx.broadcast(data) },
190
+ async onMessage(ctx, data) {
191
+ // 1) 본문만 클라에서 취한다 — 검증·상한(신뢰 경계). raw data 통째 브로드캐스트 금지.
192
+ const raw = (data as { text?: unknown }).text
193
+ const text = typeof raw === 'string' ? raw.trim().slice(0, 2000) : ''
194
+ if (!text) return // 빈/비정상 메시지는 흘리지 않는다
195
+
196
+ // 2) 작성자는 **서버 권위** — ctx.user 로 못박는다(클라가 보낸 author/id 는 신뢰 금지).
197
+ const u = ctx.user as { id: bigint; name: string } | null
198
+
199
+ // 3) 필요하면 여기서 저장한다(영속·조회): 예) await Message.create({ text, userId: u!.id })
200
+
201
+ // 4) 서버가 조립한 안전한 봉투만 팬아웃한다.
202
+ ctx.broadcast({ text, author: u ? { id: String(u.id), name: u.name } : null })
203
+ },
185
204
  })
186
205
  ```
187
206
 
207
+ 클라가 보낸 페이로드를 통째로 `ctx.broadcast(data)` 로 되쏘는 건 **반정본**이다 — 작성자
208
+ 위조·XSS 표면이 열린다. 본문만 검증·상한하고, 작성자·타임스탬프 같은 신뢰 필드는 서버가 붙인다.
209
+
188
210
  서버 개시(HTTP 요청·잡 처리 결과 등)로 미는 경우는 §2.5 `broadcast(name, data)` 를 쓴다 —
189
211
  "flash 로 클라에 심고 클라가 다시 채널로 중계" 같은 우회는 **반정본**이다(탭 닫힘에 구멍 · 결정 126).
190
212
 
@@ -194,6 +216,10 @@ export default channel({
194
216
  앱 소속 · 라우트처럼 앱 경계 안).
195
217
  - **`presenceInfo` 에 민감 정보 금지** — 접속자 목록은 채널 전원에게
196
218
  공개된다. 공개 메타만.
219
+ - **raw data 에코는 반정본** — `onMessage(ctx, data) { ctx.broadcast(data) }` 처럼
220
+ 클라 페이로드를 통째로 되쏘면 작성자 위조·XSS 표면이 열린다. 본문만 검증·상한해
221
+ 취하고, 작성자 같은 신뢰 필드는 **서버 권위 `ctx.user`** 로 붙인 봉투만 broadcast
222
+ 한다(§2 · 정본 예시 chatMessages.ts).
197
223
  - **웹서버 ↔ 허브를 NATS 로 잇지 않는다** — TCP 지속 연결이 정본
198
224
  (E-2). NATS 는 broadcast 팬아웃 전용.
199
225
  - **서버 푸시 데이터를 `api()` 폴링으로 대체 금지** — 데이터 경로
@@ -26,6 +26,26 @@ seal 은 이들 중 어느 것의 이유도 되지 못한다:
26
26
  > 그런 판단을 하는 순간 seal 은 보안을 **낮춘다**. `gaon doctor` 의 `seal-security` 가
27
27
  > seal 앱에서 rate limit·보안 헤더·CORS 를 명시적으로 끈 경우를 경고한다.
28
28
 
29
+ ### 은닉의 한계 — 여기까지다 (역공학 실측 · Kerckhoffs)
30
+
31
+ 불투명 export·non-literal 시드·미끼 시크릿에도 불구하고, 89KB wasm 은 디스어셈블(wasm2wat·Ghidra)로
32
+ **알고리즘·시드 파생·개봉 로직이 전부 복원**되고, 공격자는 `sealHttp`/`sealWs` 를 직접 호출해 조작
33
+ 데이터를 **올바르게 봉인**할 수 있다. 은닉은 **캐주얼/자동 티어 비용 상승까지만**이며 작정한 공격자는
34
+ 못 막는다. 이유는 구조적이다:
35
+
36
+ 1. **알고리즘은 숨길 수 없다** — 클라이언트에서 실제로 돌아야 하니 바이너리에 있다.
37
+ 2. **시드 입력이 전부 공격자 통제·공지 값이다** — `domain`·`path`·`uaSlice`·`timestamp` 는 요청에서 나온다.
38
+ 3. **진짜 비밀 키가 없다** — masterSecret 은 미끼(공개 전제 · 결정 121·ADR-056).
39
+
40
+ 따라서 보안은 **알고리즘 비밀이 아니라 키 비밀에 있어야 한다(Kerckhoffs)**. 조작·위조를 실제로 막는
41
+ 것은 **서버측 검증**(스키마·인가·CSRF·rate limit)과 **replay 방어**(nonce+drift)·**HTTPS** 이며, seal 은
42
+ 이를 대체하지 않는다.
43
+
44
+ > **"보안 강화" 명목으로 난독화를 더 쌓지 말 것 (security-through-obscurity · 기각).** "시드 포맷 추가 은닉 / 바이트 체인
45
+ > 추가 / 알고리즘 커스터마이즈"는 실익 0(security-through-obscurity)이라 **하지 않는다.** wasm strings
46
+ > 하드닝(자작 알고리즘 문자열·홈경로 제거 · §4)만 실효였고 그것만 했다. 알고리즘 은닉을 더 추격하는 것
47
+ > 자체가 이 원칙 위반이다.
48
+
29
49
  ### 막는 것 / 못 막는 것 경계표
30
50
 
31
51
  | 위협 | seal 이 막나 | 진짜 방어 |
@@ -102,6 +122,14 @@ seal 앱 응답에만 `script-src` 에 `'wasm-unsafe-eval'` 을 **자동 주입*
102
122
  wasm 바이너리 내장**(JS 번들 미노출). **domain·user-agent 는 wasm 이 브라우저(web-sys)에서 직접 읽고**
103
123
  path 는 넘겨받은 URL 에서 wasm 이 파싱한다 — JS 소스에 "무엇이 키 유도 입력인가" 힌트를 남기지 않는다.
104
124
  (위조는 서버가 실 요청 헤더로 독립 유도해 이미 막힌다 · 이 은닉은 힌트 제거·공격 비용 상승 목적.)
125
+ - **wasm strings 하드닝 — 여기까지가 실효**: 빌드 산출 wasm 은 상주 게이트
126
+ (`wasm-hardening.test.ts`)로 ① JS 표면 불투명(고수준 4함수만) ② **자작 알고리즘 문자열 0**(에러/expect
127
+ 라벨에 `aes-gcm`·`base64 decode`·`HMAC` 등 미노출 · `.map_err(|_| "seal …")` 로 중립화) ③ **홈경로(PII) 0**
128
+ (`--remap-path-prefix=$HOME=/` · 이전엔 빌드 머신 사용자명이 박혔다)을 강제한다. **잔존(의도)**: `panic = "abort"`
129
+ 로도 **의존 crate 패닉 위치 경로**(`aes-0.8.4`·`sha2-0.10.9`·`base64-0.22.1` 등)는 안 지워져 `strings` 로
130
+ 알고리즘 **계열**은 여전히 샌다. **완전 차단은 나이틀리 `build-std`+`panic_immediate_abort` 세금이 필요해
131
+ 미채택** — 알고리즘은 비밀이 아니므로(§0 Kerckhoffs) 그 비용을 무는 것 자체가 obscurity 추격이다. 게이트는
132
+ "알고리즘 계열 0" 이 아니라 "자작 문자열 0 + 홈경로 0 + 표면 불투명" 으로 정의된다.
105
133
  - **서버는 wasm 이 아니다** — JS mirror(`crypto.ts`)로 봉인/개봉하고, Rust 정본(wasm)과 **known-vector parity**
106
134
  (`wasm-parity` 테스트)로 byte 호환을 강제한다.
107
135
  - **알고리즘**: AES-256-GCM(12-byte nonce · 16-byte tag) + nibble-swap XOR(0x5A) + base64 · 키 유도 =
@@ -119,6 +147,11 @@ seal 앱 응답에만 `script-src` 에 `'wasm-unsafe-eval'` 을 **자동 주입*
119
147
  경유 봉인 broadcast).
120
148
  - **목업 e2e 로 대체 금지.** 서버 inject·단위·wasm-parity 는 byte 호환을 잠글 뿐 "실 브라우저에서 마운트되나"를
121
149
  못 본다 — 원 wave 가 실-브라우저 e2e 를 미뤄 P0 3건(번들 불가·CSP 차단·Inertia 인터셉터 파손)을 놓친 교훈(결정 124).
150
+ - **봉인 검증은 네트워크 날 바디로만 봐야 한다 (검증법 함정).** seal 앱에서 `page.evaluate(fetch(...))` 나 렌더된
151
+ DOM 으로 봉인을 확인하면 **안 된다** — 클라 인터셉터가 fetch/XHR 응답을 **자동 개봉**하고 화면 DOM 은 개봉된
152
+ 평문이라, 봉인돼 있어도 평문으로 보인다(거짓 음성). 봉인은 **네트워크 계층의 날 응답 바디**
153
+ (Playwright `page.waitForResponse(...).text()`·`page.on('response')` · 외부 `curl`)로 **시그널 헤더 + 암호문**을
154
+ 직접 봐야 드러난다. 정본 게이트 ⑨(결정 125)가 이 방식으로 네비게이션 wire 봉인을 정면 단언한다.
122
155
 
123
156
  ## 알려진 함정
124
157
 
@@ -9,6 +9,10 @@
9
9
  // import.meta.glob 으로 만든다. 심볼 출처가 코드에 그대로 보인다.
10
10
  import { createGaonApp } from 'gaonjs/vue'
11
11
 
12
+ // api() 런타임 매니페스트(결정 127) — 라우트 키 → { method, URL }. gaon dev·check·gen 이
13
+ // .gaon/routes.manifest.ts 를 재생성한다. 이 값을 넘겨야 api() 가 요청 경로를 안다.
14
+ import { routes } from './.gaon/routes.manifest.js'
15
+
12
16
  // 전역 스타일 — Tailwind 레이어 + 디자인 토큰(결정 74). 부수효과 import 라
13
17
  // 번들에 CSS 가 실린다. 앱마다 하나(관례 = 배치).
14
18
  import './style.css'
@@ -24,5 +28,6 @@ const layouts = import.meta.glob('./layouts/*.vue', { eager: true })
24
28
  void createGaonApp({
25
29
  pages,
26
30
  layouts,
31
+ routes,
27
32
  title: (t) => (t ? `${t} · {{PROJECT_NAME}}` : '{{PROJECT_NAME}}'),
28
33
  })
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "scripts": {
11
11
  "dev": "gaon dev",
12
- "build": "vite build",
12
+ "build": "gaon gen && vite build",
13
13
  "serve": "gaon serve",
14
14
  "work": "gaon work",
15
15
  "hub": "gaon hub",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaonjs/cli",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "Gaon CLI 구현: 제너레이터·스캐폴딩·로드맵 출력 (M1 스텁)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,12 +27,12 @@
27
27
  "@modelcontextprotocol/sdk": "^1.29.0",
28
28
  "typescript": "^5.9.0",
29
29
  "vite": "^7.0.0",
30
- "@gaonjs/async": "0.7.0",
31
- "@gaonjs/config": "0.9.1",
30
+ "@gaonjs/config": "0.9.2",
31
+ "@gaonjs/core": "0.2.1",
32
+ "@gaonjs/web": "0.13.0",
32
33
  "@gaonjs/data": "0.13.1",
33
34
  "@gaonjs/mail": "0.1.3",
34
- "@gaonjs/web": "0.12.0",
35
- "@gaonjs/core": "0.2.1"
35
+ "@gaonjs/async": "0.7.0"
36
36
  },
37
37
  "scripts": {
38
38
  "build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json && node -e \"require('fs').cpSync('src/templates','dist/templates',{recursive:true})\""