@mandujs/core 0.20.10 → 0.22.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/README.md +2 -1
- package/package.json +28 -3
- package/src/auth/__tests__/login.test.ts +419 -0
- package/src/auth/__tests__/password.test.ts +122 -0
- package/src/auth/__tests__/reset.test.ts +296 -0
- package/src/auth/__tests__/tokens.test.ts +274 -0
- package/src/auth/__tests__/verification.test.ts +274 -0
- package/src/auth/index.ts +76 -0
- package/src/auth/login.ts +225 -0
- package/src/auth/password.ts +120 -0
- package/src/auth/reset.ts +243 -0
- package/src/auth/tokens.ts +612 -0
- package/src/auth/verification.ts +253 -0
- package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -0
- package/src/bundler/__tests__/cold-start.test.ts +504 -0
- package/src/bundler/__tests__/csp-nonce.test.ts +278 -0
- package/src/bundler/__tests__/dev-reliability.test.ts +619 -0
- package/src/bundler/__tests__/extended-watch.test.ts +710 -0
- package/src/bundler/__tests__/fast-refresh.test.ts +596 -0
- package/src/bundler/__tests__/hdr.test.ts +353 -0
- package/src/bundler/__tests__/hmr-client.test.ts +532 -0
- package/src/bundler/__tests__/manifest-schema.test.ts +266 -0
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -0
- package/src/bundler/__tests__/slot-dispatch.test.ts +573 -0
- package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -0
- package/src/bundler/__tests__/vendor-cache.test.ts +455 -0
- package/src/bundler/build.test.ts +8 -1
- package/src/bundler/build.ts +495 -37
- package/src/bundler/css.ts +326 -323
- package/src/bundler/dev.ts +1671 -80
- package/src/bundler/fast-refresh-plugin.ts +307 -0
- package/src/bundler/hmr-types.ts +252 -0
- package/src/bundler/manifest-schema.ts +301 -0
- package/src/bundler/safe-build.test.ts +128 -0
- package/src/bundler/safe-build.ts +77 -0
- package/src/bundler/scenario-matrix.ts +229 -0
- package/src/bundler/types.ts +19 -0
- package/src/bundler/vendor-cache-types.ts +130 -0
- package/src/bundler/vendor-cache.ts +526 -0
- package/src/client/router.ts +214 -56
- package/src/config/validate.ts +1 -0
- package/src/db/__tests__/db.test.ts +485 -0
- package/src/db/index.ts +513 -0
- package/src/db/migrations/__tests__/runner.test.ts +661 -0
- package/src/db/migrations/history-table.ts +345 -0
- package/src/db/migrations/lock.ts +269 -0
- package/src/db/migrations/runner.ts +633 -0
- package/src/desktop/__tests__/smoke.test.ts +100 -0
- package/src/desktop/__tests__/window.test.ts +172 -0
- package/src/desktop/__tests__/worker.test.ts +266 -0
- package/src/desktop/index.ts +43 -0
- package/src/desktop/types.ts +158 -0
- package/src/desktop/window.ts +492 -0
- package/src/desktop/worker.ts +180 -0
- package/src/devtools/ai/mcp-connector.ts +18 -16
- package/src/devtools/client/components/mandu-character.tsx +4 -1
- package/src/devtools/client/components/panel/panel-container.tsx +20 -5
- package/src/email/__tests__/email.test.ts +355 -0
- package/src/email/index.ts +282 -0
- package/src/email/resend.ts +163 -0
- package/src/email/smtp.ts +64 -0
- package/src/filling/__tests__/session-sqlite.test.ts +454 -0
- package/src/filling/context.ts +72 -78
- package/src/filling/cookie-codec.ts +299 -0
- package/src/filling/deps.ts +25 -1
- package/src/filling/filling.ts +28 -3
- package/src/filling/session-sqlite.ts +617 -0
- package/src/filling/session.ts +265 -216
- package/src/guard/decision-memory.test.ts +52 -22
- package/src/id/__tests__/id.test.ts +120 -0
- package/src/id/index.ts +105 -0
- package/src/kitchen/index.ts +2 -2
- package/src/kitchen/kitchen-handler.ts +86 -0
- package/src/kitchen/stream/activity-sse.ts +2 -1
- package/src/middleware/csrf.ts +328 -0
- package/src/middleware/index.ts +40 -0
- package/src/middleware/oauth/__tests__/oauth.test.ts +574 -0
- package/src/middleware/oauth/index.ts +505 -0
- package/src/middleware/oauth/providers.ts +115 -0
- package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -0
- package/src/middleware/rate-limit/index.ts +522 -0
- package/src/middleware/rate-limit/sqlite-store.ts +382 -0
- package/src/middleware/secure/__tests__/secure.test.ts +360 -0
- package/src/middleware/secure/csp.ts +193 -0
- package/src/middleware/secure/index.ts +417 -0
- package/src/middleware/session.ts +174 -0
- package/src/observability/event-bus.ts +81 -79
- package/src/paths.ts +37 -0
- package/src/perf/hmr-markers.ts +215 -0
- package/src/perf/index.ts +104 -0
- package/src/resource/__tests__/generator.test.ts +603 -2
- package/src/resource/ddl/__tests__/diff.test.ts +639 -0
- package/src/resource/ddl/__tests__/emit.test.ts +799 -0
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -0
- package/src/resource/ddl/diff.ts +392 -0
- package/src/resource/ddl/emit.ts +548 -0
- package/src/resource/ddl/persistence-types.ts +218 -0
- package/src/resource/ddl/snapshot.ts +447 -0
- package/src/resource/ddl/type-map.ts +223 -0
- package/src/resource/ddl/types.ts +232 -0
- package/src/resource/generator-repo.ts +610 -0
- package/src/resource/generator-schema.ts +476 -0
- package/src/resource/generator.ts +117 -1
- package/src/resource/index.ts +17 -1
- package/src/resource/schema.ts +30 -0
- package/src/router/fs-scanner.ts +3 -0
- package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -0
- package/src/runtime/__tests__/hdr-client.test.ts +223 -0
- package/src/runtime/__tests__/http-errors.test.ts +117 -0
- package/src/runtime/__tests__/not-found.test.ts +152 -0
- package/src/runtime/boundary.tsx +21 -1
- package/src/runtime/fast-refresh-runtime.ts +322 -0
- package/src/runtime/fast-refresh-types.ts +128 -0
- package/src/runtime/hmr-client.ts +409 -0
- package/src/runtime/http-errors.ts +113 -0
- package/src/runtime/index.ts +6 -0
- package/src/runtime/logger.ts +678 -677
- package/src/runtime/not-found.ts +93 -0
- package/src/runtime/redirect.ts +133 -0
- package/src/runtime/server.ts +679 -23
- package/src/runtime/ssr.ts +340 -10
- package/src/runtime/streaming-ssr.ts +222 -19
- package/src/scheduler/__tests__/scheduler.test.ts +514 -0
- package/src/scheduler/index.ts +343 -0
- package/src/storage/s3/__tests__/s3.test.ts +479 -0
- package/src/storage/s3/index.ts +412 -0
- package/src/testing/index.ts +247 -189
package/src/filling/session.ts
CHANGED
|
@@ -1,216 +1,265 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu Session Storage
|
|
3
|
-
* 쿠키 기반 서버 사이드 세션 관리
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { type CookieManager, type CookieOptions } from "./context";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Session Storage
|
|
3
|
+
* 쿠키 기반 서버 사이드 세션 관리
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { type CookieManager, type CookieOptions } from "./context";
|
|
7
|
+
import { newId } from "../id";
|
|
8
|
+
|
|
9
|
+
// ========== Types ==========
|
|
10
|
+
|
|
11
|
+
export interface SessionData {
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SessionStorage {
|
|
16
|
+
/** 요청의 쿠키에서 세션 가져오기 */
|
|
17
|
+
getSession(cookies: CookieManager): Promise<Session>;
|
|
18
|
+
/** 세션을 직렬화하여 Set-Cookie 헤더 문자열 반환 */
|
|
19
|
+
commitSession(session: Session): Promise<string>;
|
|
20
|
+
/** 세션 파기 (쿠키 삭제) */
|
|
21
|
+
destroySession(session: Session): Promise<string>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CookieSessionOptions {
|
|
25
|
+
cookie: {
|
|
26
|
+
/** 쿠키 이름 (기본: "__session") */
|
|
27
|
+
name?: string;
|
|
28
|
+
/** HMAC 서명 시크릿 */
|
|
29
|
+
secrets: string[];
|
|
30
|
+
/** 기본 쿠키 옵션 */
|
|
31
|
+
httpOnly?: boolean;
|
|
32
|
+
secure?: boolean;
|
|
33
|
+
sameSite?: "strict" | "lax" | "none";
|
|
34
|
+
maxAge?: number;
|
|
35
|
+
path?: string;
|
|
36
|
+
domain?: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ========== Session Class ==========
|
|
41
|
+
|
|
42
|
+
export class Session {
|
|
43
|
+
private data: SessionData;
|
|
44
|
+
private flash: Map<string, unknown> = new Map();
|
|
45
|
+
/**
|
|
46
|
+
* Dirty bit — set by any mutation (`set`/`unset`/`setFlash`/`clear`) and
|
|
47
|
+
* cleared by `markClean()` after a successful commit. Middleware/helpers use
|
|
48
|
+
* this to skip no-op commits.
|
|
49
|
+
*
|
|
50
|
+
* Intentionally private; observed via `isDirty()` to keep the invariant
|
|
51
|
+
* that only in-class mutations can flip it.
|
|
52
|
+
*/
|
|
53
|
+
private _dirty = false;
|
|
54
|
+
readonly id: string;
|
|
55
|
+
|
|
56
|
+
constructor(data: SessionData = {}, id?: string) {
|
|
57
|
+
this.data = { ...data };
|
|
58
|
+
this.id = id ?? newId();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get<T = unknown>(key: string): T | undefined {
|
|
62
|
+
// flash 데이터는 한번 읽으면 제거
|
|
63
|
+
if (this.flash.has(key)) {
|
|
64
|
+
const value = this.flash.get(key);
|
|
65
|
+
this.flash.delete(key);
|
|
66
|
+
return value as T;
|
|
67
|
+
}
|
|
68
|
+
return this.data[key] as T | undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
set(key: string, value: unknown): void {
|
|
72
|
+
this.data[key] = value;
|
|
73
|
+
this._dirty = true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
has(key: string): boolean {
|
|
77
|
+
return key in this.data || this.flash.has(key);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
unset(key: string): void {
|
|
81
|
+
// Preserve original unconditional-delete semantics; flip dirty regardless
|
|
82
|
+
// so callers can observe intent via `isDirty()` even when the key was
|
|
83
|
+
// already absent.
|
|
84
|
+
delete this.data[key];
|
|
85
|
+
this._dirty = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Flash 메시지 — 다음 요청에서 한번만 읽을 수 있는 데이터
|
|
90
|
+
* 로그인 성공 메시지, 에러 알림 등에 사용
|
|
91
|
+
*/
|
|
92
|
+
setFlash(key: string, value: unknown): void {
|
|
93
|
+
this.flash.set(key, value);
|
|
94
|
+
// flash 데이터도 직렬화에 포함
|
|
95
|
+
this.data[`__flash_${key}`] = value;
|
|
96
|
+
this._dirty = true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Whether this session has been mutated since it was constructed or last
|
|
101
|
+
* cleaned via {@link markClean}. `saveSession` consults this to avoid
|
|
102
|
+
* re-committing unchanged sessions.
|
|
103
|
+
*/
|
|
104
|
+
isDirty(): boolean {
|
|
105
|
+
return this._dirty;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Reset the dirty bit. Called by `saveSession` after a successful
|
|
110
|
+
* `commitSession`. Not intended for handler code.
|
|
111
|
+
*
|
|
112
|
+
* @internal
|
|
113
|
+
*/
|
|
114
|
+
markClean(): void {
|
|
115
|
+
this._dirty = false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Wipe in-memory data + flash. Called by `destroySession` so subsequent
|
|
120
|
+
* handler code sees an empty session. Flips the dirty bit.
|
|
121
|
+
*/
|
|
122
|
+
clear(): void {
|
|
123
|
+
this.data = {};
|
|
124
|
+
this.flash.clear();
|
|
125
|
+
this._dirty = true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 내부 직렬화용 */
|
|
129
|
+
toJSON(): SessionData {
|
|
130
|
+
return { ...this.data };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** flash 데이터 복원 */
|
|
134
|
+
static fromJSON(data: SessionData): Session {
|
|
135
|
+
const session = new Session();
|
|
136
|
+
const flashKeys: string[] = [];
|
|
137
|
+
|
|
138
|
+
for (const [key, value] of Object.entries(data)) {
|
|
139
|
+
if (key.startsWith("__flash_")) {
|
|
140
|
+
const realKey = key.slice(8);
|
|
141
|
+
session.flash.set(realKey, value);
|
|
142
|
+
flashKeys.push(key);
|
|
143
|
+
} else {
|
|
144
|
+
session.data[key] = value;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// flash 키는 data에서 제거 (한번 복원되면 끝)
|
|
149
|
+
for (const key of flashKeys) {
|
|
150
|
+
delete session.data[key];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Loaded-from-cookie state is, by definition, clean until handler code
|
|
154
|
+
// mutates it. This runs after data population so any mutations above
|
|
155
|
+
// don't accidentally leave _dirty=true.
|
|
156
|
+
session._dirty = false;
|
|
157
|
+
return session;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ========== Cookie Session Storage ==========
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 쿠키 기반 세션 스토리지 생성
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```typescript
|
|
168
|
+
* import { createCookieSessionStorage } from "@mandujs/core";
|
|
169
|
+
*
|
|
170
|
+
* const sessionStorage = createCookieSessionStorage({
|
|
171
|
+
* cookie: {
|
|
172
|
+
* name: "__session",
|
|
173
|
+
* secrets: [process.env.SESSION_SECRET!],
|
|
174
|
+
* httpOnly: true,
|
|
175
|
+
* secure: true,
|
|
176
|
+
* sameSite: "lax",
|
|
177
|
+
* maxAge: 60 * 60 * 24, // 1일
|
|
178
|
+
* },
|
|
179
|
+
* });
|
|
180
|
+
*
|
|
181
|
+
* // filling에서 사용
|
|
182
|
+
* .action("login", async (ctx) => {
|
|
183
|
+
* const session = await sessionStorage.getSession(ctx.cookies);
|
|
184
|
+
* session.set("userId", user.id);
|
|
185
|
+
* session.setFlash("message", "로그인 성공!");
|
|
186
|
+
* const setCookie = await sessionStorage.commitSession(session);
|
|
187
|
+
* return ctx.redirect("/dashboard", {
|
|
188
|
+
* headers: { "Set-Cookie": setCookie },
|
|
189
|
+
* });
|
|
190
|
+
* });
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
export function createCookieSessionStorage(options: CookieSessionOptions): SessionStorage {
|
|
194
|
+
const {
|
|
195
|
+
name = "__session",
|
|
196
|
+
secrets,
|
|
197
|
+
httpOnly = true,
|
|
198
|
+
secure = process.env.NODE_ENV === "production",
|
|
199
|
+
sameSite = "lax",
|
|
200
|
+
maxAge = 86400,
|
|
201
|
+
path = "/",
|
|
202
|
+
domain,
|
|
203
|
+
} = options.cookie;
|
|
204
|
+
|
|
205
|
+
if (!secrets.length) {
|
|
206
|
+
throw new Error("[Mandu Session] At least one secret is required");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const cookieOptions: CookieOptions = {
|
|
210
|
+
httpOnly,
|
|
211
|
+
secure,
|
|
212
|
+
sameSite,
|
|
213
|
+
maxAge,
|
|
214
|
+
path,
|
|
215
|
+
domain,
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
async getSession(cookies: CookieManager): Promise<Session> {
|
|
220
|
+
// Secret rotation: 모든 시크릿으로 검증 시도 (서명은 항상 secrets[0]으로)
|
|
221
|
+
for (const secret of secrets) {
|
|
222
|
+
const raw = await cookies.getSigned(name, secret);
|
|
223
|
+
if (typeof raw === "string" && raw.length > 0) {
|
|
224
|
+
try {
|
|
225
|
+
const data = JSON.parse(raw) as SessionData;
|
|
226
|
+
return Session.fromJSON(data);
|
|
227
|
+
} catch {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return new Session();
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
async commitSession(session: Session): Promise<string> {
|
|
236
|
+
const value = JSON.stringify(session.toJSON());
|
|
237
|
+
// 서명된 쿠키로 직렬화
|
|
238
|
+
const encoder = new TextEncoder();
|
|
239
|
+
const key = await crypto.subtle.importKey(
|
|
240
|
+
"raw",
|
|
241
|
+
encoder.encode(secrets[0]),
|
|
242
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
243
|
+
false,
|
|
244
|
+
["sign"]
|
|
245
|
+
);
|
|
246
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(value));
|
|
247
|
+
const sigBase64 = btoa(String.fromCharCode(...new Uint8Array(signature))).replace(/=+$/, "");
|
|
248
|
+
|
|
249
|
+
const cookieValue = `${value}.${sigBase64}`;
|
|
250
|
+
const parts = [`${name}=${encodeURIComponent(cookieValue)}`];
|
|
251
|
+
if (cookieOptions.path) parts.push(`Path=${cookieOptions.path}`);
|
|
252
|
+
if (cookieOptions.domain) parts.push(`Domain=${cookieOptions.domain}`);
|
|
253
|
+
if (cookieOptions.maxAge) parts.push(`Max-Age=${cookieOptions.maxAge}`);
|
|
254
|
+
if (cookieOptions.httpOnly) parts.push("HttpOnly");
|
|
255
|
+
if (cookieOptions.secure) parts.push("Secure");
|
|
256
|
+
if (cookieOptions.sameSite) parts.push(`SameSite=${cookieOptions.sameSite}`);
|
|
257
|
+
|
|
258
|
+
return parts.join("; ");
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
async destroySession(_session: Session): Promise<string> {
|
|
262
|
+
return `${name}=; Path=${path}; Max-Age=0; HttpOnly${secure ? "; Secure" : ""}`;
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
}
|
|
@@ -226,24 +226,33 @@ describe("Decision Memory", () => {
|
|
|
226
226
|
|
|
227
227
|
describe("saveDecision", () => {
|
|
228
228
|
it("should save new decision as markdown file", async () => {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
229
|
+
// Local tempdir so the added ADR-003 does not leak into the shared
|
|
230
|
+
// TEST_DIR used by getAllDecisions / searchDecisions / …, which
|
|
231
|
+
// assert exact counts or specific ADR contents under --randomize.
|
|
232
|
+
const localDir = await mkdtemp(join(tmpdir(), "save-decision-test-"));
|
|
233
|
+
try {
|
|
234
|
+
await mkdir(join(localDir, "spec", "decisions"), { recursive: true });
|
|
235
|
+
const newDecision: Omit<ArchitectureDecision, "date"> = {
|
|
236
|
+
id: "ADR-003",
|
|
237
|
+
title: "Use Feature Flags",
|
|
238
|
+
status: "proposed",
|
|
239
|
+
tags: ["feature", "deployment"],
|
|
240
|
+
context: "Need controlled rollout",
|
|
241
|
+
decision: "Use feature flags for gradual rollout",
|
|
242
|
+
consequences: ["Need flag management system"],
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const result = await saveDecision(localDir, newDecision);
|
|
246
|
+
|
|
247
|
+
expect(result.success).toBe(true);
|
|
248
|
+
expect(result.filePath).toContain("ADR-003");
|
|
249
|
+
|
|
250
|
+
// 파일이 실제로 생성되었는지 확인
|
|
251
|
+
const content = await readFile(result.filePath, "utf-8");
|
|
252
|
+
expect(content).toContain("Use Feature Flags");
|
|
253
|
+
} finally {
|
|
254
|
+
await rm(localDir, { recursive: true, force: true });
|
|
255
|
+
}
|
|
247
256
|
});
|
|
248
257
|
});
|
|
249
258
|
|
|
@@ -275,10 +284,31 @@ describe("Decision Memory", () => {
|
|
|
275
284
|
|
|
276
285
|
describe("getNextDecisionId", () => {
|
|
277
286
|
it("should return next sequential ID", async () => {
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
287
|
+
// Use an isolated tempdir so other tests in this suite (which assert
|
|
288
|
+
// on the shared TEST_DIR's ADR count) are not polluted by the fixture
|
|
289
|
+
// this test needs.
|
|
290
|
+
const localDir = await mkdtemp(join(tmpdir(), "next-id-test-"));
|
|
291
|
+
try {
|
|
292
|
+
await mkdir(join(localDir, "spec", "decisions"), { recursive: true });
|
|
293
|
+
await Bun.write(
|
|
294
|
+
join(localDir, "spec", "decisions", "ADR-001-a.md"),
|
|
295
|
+
"**ID:** ADR-001\n"
|
|
296
|
+
);
|
|
297
|
+
await Bun.write(
|
|
298
|
+
join(localDir, "spec", "decisions", "ADR-002-b.md"),
|
|
299
|
+
"**ID:** ADR-002\n"
|
|
300
|
+
);
|
|
301
|
+
await Bun.write(
|
|
302
|
+
join(localDir, "spec", "decisions", "ADR-003-c.md"),
|
|
303
|
+
"**ID:** ADR-003\n"
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
const nextId = await getNextDecisionId(localDir);
|
|
307
|
+
|
|
308
|
+
expect(nextId).toBe("ADR-004");
|
|
309
|
+
} finally {
|
|
310
|
+
await rm(localDir, { recursive: true, force: true });
|
|
311
|
+
}
|
|
282
312
|
});
|
|
283
313
|
|
|
284
314
|
it("should return ADR-001 for empty project", async () => {
|