@simplysm/sd-claude 14.0.82 → 14.0.83
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/claude/references/sd-requirement-source-handling.md +20 -20
- package/claude/references/sd-simplysm14/README.md +13 -13
- package/claude/references/sd-simplysm14/manuals/client-component.md +92 -92
- package/claude/references/sd-simplysm14/manuals/client-crud.md +11 -11
- package/claude/references/sd-simplysm14/manuals/client-demo.md +28 -28
- package/claude/references/sd-simplysm14/manuals/client-rules.md +1 -1
- package/claude/references/sd-simplysm14/manuals/client-setup.md +21 -21
- package/claude/references/sd-simplysm14/manuals/client-tab.md +3 -3
- package/claude/references/sd-simplysm14/manuals/logging.md +15 -15
- package/claude/references/sd-simplysm14/manuals/orm-union.md +6 -6
- package/claude/references/sd-simplysm14/manuals/orm.md +19 -19
- package/claude/references/sd-simplysm14/manuals/test.md +33 -33
- package/claude/rules/sd-base-rules.md +44 -43
- package/claude/rules/sd-design-rules.md +18 -18
- package/claude/skills/sd-commit/SKILL.md +10 -10
- package/claude/skills/sd-config/SKILL.md +2 -2
- package/claude/skills/sd-demo/SKILL.md +45 -45
- package/claude/skills/sd-dev/SKILL.md +15 -15
- package/claude/skills/sd-docs/SKILL.md +7 -7
- package/claude/skills/sd-docs/references/subagent-prompt.md +33 -33
- package/claude/skills/sd-impl/SKILL.md +60 -60
- package/claude/skills/sd-review/SKILL.md +9 -9
- package/claude/skills/sd-skill/SKILL.md +74 -74
- package/claude/skills/sd-skill/evals/fixtures/existing-skill/.claude/skills/todo-format/SKILL.md +1 -1
- package/claude/skills/sd-spec/SKILL.md +355 -319
- package/claude/skills/sd-spec/references/example-spec.md +104 -104
- package/claude/skills/sd-unpack/SKILL.md +34 -34
- package/claude/skills/sd-use/SKILL.md +4 -4
- package/package.json +1 -1
- package/claude/references/sd-simplysm14/apis/angular/README.md +0 -37
- package/claude/references/sd-simplysm14/apis/angular/app-structure.md +0 -92
- package/claude/references/sd-simplysm14/apis/angular/buttons.md +0 -88
- package/claude/references/sd-simplysm14/apis/angular/crud.md +0 -100
- package/claude/references/sd-simplysm14/apis/angular/forms.md +0 -200
- package/claude/references/sd-simplysm14/apis/angular/infrastructure.md +0 -231
- package/claude/references/sd-simplysm14/apis/angular/kanban.md +0 -80
- package/claude/references/sd-simplysm14/apis/angular/layout.md +0 -92
- package/claude/references/sd-simplysm14/apis/angular/modal.md +0 -115
- package/claude/references/sd-simplysm14/apis/angular/routing.md +0 -107
- package/claude/references/sd-simplysm14/apis/angular/select-dropdown.md +0 -35
- package/claude/references/sd-simplysm14/apis/angular/selection-managers.md +0 -82
- package/claude/references/sd-simplysm14/apis/angular/shared-data.md +0 -134
- package/claude/references/sd-simplysm14/apis/angular/sheet.md +0 -127
- package/claude/references/sd-simplysm14/apis/angular/toast.md +0 -97
- package/claude/references/sd-simplysm14/apis/angular/visual.md +0 -167
- package/claude/references/sd-simplysm14/apis/capacitor-plugin-auto-update/README.md +0 -79
- package/claude/references/sd-simplysm14/apis/capacitor-plugin-file-system/README.md +0 -83
- package/claude/references/sd-simplysm14/apis/capacitor-plugin-intent/README.md +0 -91
- package/claude/references/sd-simplysm14/apis/capacitor-plugin-usb-storage/README.md +0 -49
- package/claude/references/sd-simplysm14/apis/core-browser/README.md +0 -143
- package/claude/references/sd-simplysm14/apis/core-common/README.md +0 -58
- package/claude/references/sd-simplysm14/apis/core-common/extensions.md +0 -88
- package/claude/references/sd-simplysm14/apis/core-common/features.md +0 -51
- package/claude/references/sd-simplysm14/apis/core-common/types.md +0 -88
- package/claude/references/sd-simplysm14/apis/core-common/utils.md +0 -189
- package/claude/references/sd-simplysm14/apis/core-node/README.md +0 -12
- package/claude/references/sd-simplysm14/apis/core-node/consola.md +0 -59
- package/claude/references/sd-simplysm14/apis/core-node/cpx.md +0 -44
- package/claude/references/sd-simplysm14/apis/core-node/fs-watcher.md +0 -42
- package/claude/references/sd-simplysm14/apis/core-node/fsx.md +0 -53
- package/claude/references/sd-simplysm14/apis/core-node/pathx.md +0 -24
- package/claude/references/sd-simplysm14/apis/core-node/worker.md +0 -65
- package/claude/references/sd-simplysm14/apis/excel/README.md +0 -193
- package/claude/references/sd-simplysm14/apis/lint/README.md +0 -94
- package/claude/references/sd-simplysm14/apis/orm-common/README.md +0 -58
- package/claude/references/sd-simplysm14/apis/orm-common/db-context.md +0 -77
- package/claude/references/sd-simplysm14/apis/orm-common/executable.md +0 -20
- package/claude/references/sd-simplysm14/apis/orm-common/expr.md +0 -92
- package/claude/references/sd-simplysm14/apis/orm-common/queryable.md +0 -98
- package/claude/references/sd-simplysm14/apis/orm-common/schema-builders.md +0 -128
- package/claude/references/sd-simplysm14/apis/orm-node/README.md +0 -69
- package/claude/references/sd-simplysm14/apis/sd-claude/README.md +0 -32
- package/claude/references/sd-simplysm14/apis/sd-cli/README.md +0 -80
- package/claude/references/sd-simplysm14/apis/sd-cli/sd-config.md +0 -155
- package/claude/references/sd-simplysm14/apis/service-client/README.md +0 -131
- package/claude/references/sd-simplysm14/apis/service-common/README.md +0 -29
- package/claude/references/sd-simplysm14/apis/service-common/app-structure.md +0 -63
- package/claude/references/sd-simplysm14/apis/service-common/messages.md +0 -56
- package/claude/references/sd-simplysm14/apis/service-common/protocol.md +0 -64
- package/claude/references/sd-simplysm14/apis/service-common/service-types.md +0 -43
- package/claude/references/sd-simplysm14/apis/service-server/README.md +0 -13
- package/claude/references/sd-simplysm14/apis/service-server/auth.md +0 -39
- package/claude/references/sd-simplysm14/apis/service-server/builtin-services.md +0 -71
- package/claude/references/sd-simplysm14/apis/service-server/define-service.md +0 -55
- package/claude/references/sd-simplysm14/apis/service-server/internals.md +0 -82
- package/claude/references/sd-simplysm14/apis/service-server/server.md +0 -57
- package/claude/references/sd-simplysm14/apis/storage/README.md +0 -71
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
# @simplysm/service-server — 인증
|
|
2
|
-
|
|
3
|
-
## `auth(fn)` / `auth(permissions, fn)`
|
|
4
|
-
|
|
5
|
-
서비스 factory 또는 개별 메서드를 래핑해 인증 요구사항을 부착. 래핑 함수는 호출 동작을 그대로 유지하며 내부 심볼에 `permissions: string[]` 저장.
|
|
6
|
-
|
|
7
|
-
- 서비스 수준: `defineService("X", auth((ctx) => ({ ... })))` — 모든 메서드 로그인 필요.
|
|
8
|
-
- 서비스 수준 + 역할: `defineService("X", auth(["admin"], (ctx) => ({ ... })))`.
|
|
9
|
-
- 메서드 수준: factory 내부에서 `someMethod: auth(() => result)`.
|
|
10
|
-
- 메서드 수준 + 역할: `someMethod: auth(["admin", "owner"], () => result)` — 배열 중 하나라도 매칭하면 통과(OR).
|
|
11
|
-
|
|
12
|
-
실행 시(`executeServiceMethod`) 동작:
|
|
13
|
-
|
|
14
|
-
- 메서드 권한이 있으면 서비스 권한을 **덮어쓴다**. 메서드 권한 미부착 시 서비스 권한 사용.
|
|
15
|
-
- 권한 요구 + `options.auth == null` → "auth 설정이 필요합니다" throw(서버 설정 오류).
|
|
16
|
-
- 권한 요구 + `options.auth === false` → 인증 스킵.
|
|
17
|
-
- 권한 요구 + `options.auth = { jwtSecret }` → payload 없으면 "로그인이 필요합니다" throw. `permissions.length > 0` 이면 `payload.roles` 중 하나라도 일치해야 통과, 아니면 "권한이 부족합니다" throw. `permissions.length === 0` (= `auth(fn)`) 이면 토큰만 있으면 통과.
|
|
18
|
-
|
|
19
|
-
## `AuthTokenPayload<TAuthInfo>`
|
|
20
|
-
|
|
21
|
-
`jose.JWTPayload` 확장.
|
|
22
|
-
|
|
23
|
-
- `roles: string[]` — `auth(["role"], fn)` 검사 대상.
|
|
24
|
-
- `data: TAuthInfo` — 임의 사용자 데이터. `ctx.authInfo` 로 노출.
|
|
25
|
-
|
|
26
|
-
## JWT 헬퍼
|
|
27
|
-
|
|
28
|
-
`server.signAuthToken`/`verifyAuthToken` 이 동일 알고리즘으로 감싸므로 일반 시나리오에선 그쪽을 쓴다. 직접 import 는 서버 인스턴스 없이 토큰을 다루는 경우(테스트, 별도 인증 서버, 디코드 등)만.
|
|
29
|
-
|
|
30
|
-
- `signJwt<T>(jwtSecret, payload): Promise<string>` — HS256, `iat` 자동, `exp = 12h` 고정. 수명 변경 필요 시 `jose.SignJWT` 직접 사용.
|
|
31
|
-
- `verifyJwt<T>(jwtSecret, token): Promise<AuthTokenPayload<T>>` — 만료: "토큰이 만료되었습니다", 그 외 위조: "유효하지 않은 토큰입니다" throw.
|
|
32
|
-
- `decodeJwt<T>(token): AuthTokenPayload<T>` — 서명 검증 없이 페이로드만 디코드. 클라이언트 측 만료 표시 등 비보안 용도에만.
|
|
33
|
-
|
|
34
|
-
## 예
|
|
35
|
-
|
|
36
|
-
```ts
|
|
37
|
-
const token = await signJwt(secret, { roles: ["admin"], data: { userId: "U1" } });
|
|
38
|
-
const payload = await verifyJwt<{ userId: string }>(secret, token);
|
|
39
|
-
```
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
# @simplysm/service-server — 빌트인 서비스
|
|
2
|
-
|
|
3
|
-
`options.services` 에 추가해 사용. 각 `*ServiceType` 은 클라이언트의 `client.getService<*ServiceType>("<이름>")` 에 그대로 사용.
|
|
4
|
-
|
|
5
|
-
## `OrmService`
|
|
6
|
-
|
|
7
|
-
DB 연결을 WebSocket 세션과 묶어 원격 ORM 사용.
|
|
8
|
-
|
|
9
|
-
- 별칭: `["Orm", "SdOrmService"]`.
|
|
10
|
-
- **WebSocket 전용**. HTTP 호출 시 "WebSocket 연결이 필요합니다…" throw(트랜잭션을 소켓 라이프타임에 묶기 위함).
|
|
11
|
-
- `auth()` 래핑이라 로그인 필수. 역할 제한은 없음.
|
|
12
|
-
- 설정 소스: `ctx.getConfig<Record<string, DbConnConfig>>("orm")` → `opt.configName` 키 조회. 호출 시 `opt.config` 가 base 설정을 부분 override.
|
|
13
|
-
- 연결 ID 는 소켓별 카운터(1부터, 가장 큰 값+1). 소켓 close 시 해당 소켓의 모든 열린 DB 연결 자동 정리(에러는 warn 후 무시).
|
|
14
|
-
- `dialect === "mssql-azure"` 는 외부 노출 시 `"mssql"` 로 정규화.
|
|
15
|
-
|
|
16
|
-
메서드:
|
|
17
|
-
|
|
18
|
-
- `getInfo(opt)` → `{ dialect, database?, schema? }` (연결 안 만들고 설정 조회만).
|
|
19
|
-
- `connect(opt): Promise<number>` — connId 반환.
|
|
20
|
-
- `close(connId)`.
|
|
21
|
-
- `beginTransaction(connId, isolationLevel?)`, `commitTransaction(connId)`, `rollbackTransaction(connId)`.
|
|
22
|
-
- `executeParametrized(connId, query, params?)` — `unknown[][]` (멀티 결과셋).
|
|
23
|
-
- `executeDefs(connId, defs: QueryDef[], options?: (ResultMeta | undefined)[])` — `options` 가 전부 null/undefined 면 단일 multi-SQL 실행 후 빈 배열들, 아니면 def 별로 build → execute → `pickResultSets` → opt 있으면 `parseQueryResult`, 없으면 raw.
|
|
24
|
-
- `bulkInsert(connId, tableName, columnDefs, records)`.
|
|
25
|
-
|
|
26
|
-
`opt: DbConnOptions & { configName: string }`. 타입 export: `OrmServiceType`.
|
|
27
|
-
|
|
28
|
-
## `AutoUpdateService`
|
|
29
|
-
|
|
30
|
-
- 별칭: `["AutoUpdate", "SdAutoUpdateService"]`. 인증 없음.
|
|
31
|
-
- `getLastVersion(platform: string): Promise<{ version, downloadPath } | undefined>` — `<clientPath>/<platform>/updates/` 에서 `platform === "android"` → `.apk`, 그 외 → `.exe` 중 파일명이 `^[0-9.]*$` 인 것만 후보. `semver.maxSatisfying(versions, "*")` 로 최신 선정. clientName 미설정 시 throw, 디렉터리/매칭 파일 없으면 undefined.
|
|
32
|
-
- `downloadPath` = `/<clientName>/<platform>/updates/<fileName>` (POSIX 정규화).
|
|
33
|
-
- 타입 export: `AutoUpdateServiceType`.
|
|
34
|
-
|
|
35
|
-
## `AppStructureService(itemsMap)`
|
|
36
|
-
|
|
37
|
-
다른 두 서비스와 달리 **팩토리 함수**(서비스 정의가 아님). 호출 결과를 `services` 에 등록.
|
|
38
|
-
|
|
39
|
-
- 시그니처: `AppStructureService(itemsMap: Record<string, AppStructureItem[]>): ServiceDefinition`
|
|
40
|
-
- 서비스 이름: `"AppStructure"`. 인증 없음.
|
|
41
|
-
- 메서드: `getItems(): Record<string, AppStructureItem[]>` — 주입된 map 그대로 반환.
|
|
42
|
-
- 타입 export: `AppStructureServiceType`.
|
|
43
|
-
|
|
44
|
-
`AppStructureItem` 은 `@simplysm/service-common` 정의(메뉴/라우트 트리 항목).
|
|
45
|
-
|
|
46
|
-
```ts
|
|
47
|
-
services: [AppStructureService({ admin: [...], user: [...] })];
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
## V1 레거시 자동 업데이트
|
|
51
|
-
|
|
52
|
-
ver≠"2" 로 접속한 구버전 클라이언트 호환. JSON 라인 프로토콜.
|
|
53
|
-
|
|
54
|
-
### `handleV1Connection(socket, autoUpdateMethods, clientNameSetter?)` / `handleV1Connection(socket, options)`
|
|
55
|
-
|
|
56
|
-
`ServiceServer` 가 자동 호출. `services` 에 `AutoUpdate` 가 있으면 그 factory 의 `getLastVersion` 을 V1 메서드로 자동 매핑. `legacyV1Handlers` 도 그대로 전달. 둘 다 없으면 1008 거부.
|
|
57
|
-
|
|
58
|
-
`V1ConnectionOptions`:
|
|
59
|
-
|
|
60
|
-
- `serviceContext?` 또는 `serviceContextFactory?: (req: V1Request) => ServiceContext` — 핸들러용 컨텍스트(둘 중 하나).
|
|
61
|
-
- `handlers?: V1RequestHandler[]` — 우선 실행. 각 핸들러는 `{ handled: true, state?: "success"|"error", body } | { handled: false }` 반환. handled=true 면 즉시 응답.
|
|
62
|
-
- `autoUpdateMethods?` 또는 `autoUpdateMethodsFactory?: (ctx) => V1AutoUpdateMethods` — 모든 사용자 핸들러가 미처리 + `command === "SdAutoUpdateService.getLastVersion"` 일 때 fallback.
|
|
63
|
-
- `clientNameSetter?: (clientName: string | undefined) => void` — 매 요청의 `clientName` 알림 콜백.
|
|
64
|
-
- 어디서도 처리 안 되면 `{ code: "UPGRADE_REQUIRED", message: "앱 업그레이드가 필요합니다." }` 응답.
|
|
65
|
-
|
|
66
|
-
타입:
|
|
67
|
-
|
|
68
|
-
- `V1Request = { uuid, command, params: unknown[], clientName? }`
|
|
69
|
-
- `V1Response = { name: "response", reqUuid, state: "success" | "error", body }`
|
|
70
|
-
- `V1AutoUpdateMethods = { getLastVersion(platform): Promise<unknown> | unknown }`
|
|
71
|
-
- `V1RequestHandler`, `V1RequestHandlerContext`, `V1RequestHandlerResult`.
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# @simplysm/service-server — 서비스 정의
|
|
2
|
-
|
|
3
|
-
## `defineService(name, factory): ServiceDefinition<TMethods>`
|
|
4
|
-
|
|
5
|
-
- `name: string | string[]` — 단일 또는 별칭 목록. 첫 요소가 primary(`def.name`), 전체가 `def.names`. RPC 라우팅은 `names.includes(요청서비스명)` 매칭. 빈 배열이면 throw.
|
|
6
|
-
- `factory: (ctx: ServiceContext) => TMethods` — 매 요청마다 호출되어 메서드 객체 생성(컨텍스트 캡처). factory 자체가 `auth(...)` 래핑이면 `authPermissions` 가 자동 추출돼 서비스 수준 인증으로 승격.
|
|
7
|
-
|
|
8
|
-
반환 `ServiceDefinition<TMethods>`:
|
|
9
|
-
|
|
10
|
-
- `name: string`
|
|
11
|
-
- `names: string[]`
|
|
12
|
-
- `factory`
|
|
13
|
-
- `authPermissions?: string[]`
|
|
14
|
-
|
|
15
|
-
## `ServiceContext<TAuthInfo>`
|
|
16
|
-
|
|
17
|
-
서비스 factory 가 받는 요청별 컨텍스트.
|
|
18
|
-
|
|
19
|
-
- `server: ServiceServer<TAuthInfo>`
|
|
20
|
-
- `socket?: ServiceSocket` — WebSocket 경로일 때만.
|
|
21
|
-
- `http?: { clientName: string; authTokenPayload?: AuthTokenPayload<TAuthInfo> }` — HTTP 경로.
|
|
22
|
-
- `legacy?: { clientName?: string }` — V1 경로.
|
|
23
|
-
- `get authInfo(): TAuthInfo | undefined` — socket 의 payload.data 우선, 없으면 http.
|
|
24
|
-
- `get clientName(): string | undefined` — socket → http → legacy 순. 빈 문자/`..`/`/`/`\\` 포함 시 throw.
|
|
25
|
-
- `get clientPath(): string | undefined` — `<rootPath>/www/<clientName>`. clientName 없으면 undefined.
|
|
26
|
-
- `getConfig<T>(section: string): Promise<T>` — `<rootPath>/.config.json` + `<clientPath>/.config.json` 을 `obj.merge` 한 뒤 `section` 키 반환. 섹션 없으면 throw. 설정 파일은 `FsWatcher` 로 변경 감시되어 자동 리로드.
|
|
27
|
-
|
|
28
|
-
## `ServiceMethods<TDefinition>` 타입
|
|
29
|
-
|
|
30
|
-
`ServiceDefinition<M>` 에서 `M` 추출. 클라이언트의 `client.getService<MyServiceType>("MyService")` 에 사용.
|
|
31
|
-
|
|
32
|
-
```ts
|
|
33
|
-
export const UserService = defineService("User", (ctx) => ({
|
|
34
|
-
getProfile: () => ({ name: "kim" }),
|
|
35
|
-
}));
|
|
36
|
-
export type UserServiceType = ServiceMethods<typeof UserService>;
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## 보조 export
|
|
40
|
-
|
|
41
|
-
- `createServiceContext(server, socket?, http?, legacy?): ServiceContext` — 컨텍스트 직조. 커스텀 라우트/테스트용.
|
|
42
|
-
- `getServiceAuthPermissions(fn): string[] | undefined` — `auth()` 가 함수에 심어둔 권한 배열 추출. 래핑 안 됐으면 undefined. 내부 executor 가 사용.
|
|
43
|
-
|
|
44
|
-
## 예
|
|
45
|
-
|
|
46
|
-
```ts
|
|
47
|
-
const HealthService = defineService("Health", () => ({
|
|
48
|
-
check: () => ({ status: "ok" }),
|
|
49
|
-
}));
|
|
50
|
-
|
|
51
|
-
const UserService = defineService(["User", "MyUser"], auth((ctx) => ({
|
|
52
|
-
me: () => ctx.authInfo,
|
|
53
|
-
adminOnly: auth(["admin"], () => "ok"),
|
|
54
|
-
})));
|
|
55
|
-
```
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
# @simplysm/service-server — 내부 전송 계층
|
|
2
|
-
|
|
3
|
-
`ServiceServer.listen()` 이 자동 결선하므로 일반 사용에선 불필요. 커스텀 Fastify 라우트·테스트·비표준 전송 시 참고.
|
|
4
|
-
|
|
5
|
-
## HTTP
|
|
6
|
-
|
|
7
|
-
### `handleHttpRequest(req, reply, jwtSecret, runMethod)`
|
|
8
|
-
|
|
9
|
-
`/api/:service/:method` 라우트 핸들러.
|
|
10
|
-
|
|
11
|
-
- `x-sd-client-name` 헤더 필수(없으면 throw).
|
|
12
|
-
- `Authorization: Bearer <jwt>` 있고 `jwtSecret` 도 있으면 `verifyJwt`. 토큰만 있고 secret 없으면 throw. 검증 실패 시 401 응답.
|
|
13
|
-
- GET: `?json=<JSON encoded array>` → `json.parse`.
|
|
14
|
-
- POST: body 가 배열이어야 함(아니면 400).
|
|
15
|
-
- 그 외 메서드: 405.
|
|
16
|
-
- `runMethod({ serviceName, methodName, params, http: { clientName, authTokenPayload } })` 호출 후 결과를 `reply.send`.
|
|
17
|
-
|
|
18
|
-
### `handleUpload(req, reply, rootPath, jwtSecret)`
|
|
19
|
-
|
|
20
|
-
multipart 업로드. `Authorization` 헤더 필수(없으면 401, `jwtSecret` 미구성 시 401). 각 파일을 `<rootPath>/www/uploads/<uuid><ext>` 로 저장. 응답 `ServiceUploadResult[] = { path, filename, size }[]` (`path` = `uploads/<saveName>`). 도중 truncated 또는 에러 발생 시 진행 중 + 이미 저장된 모든 파일 삭제 후 500.
|
|
21
|
-
|
|
22
|
-
### `handleStaticFile(req, reply, rootPath, urlPath)`
|
|
23
|
-
|
|
24
|
-
`<rootPath>/www/<urlPath>` 정적 서빙.
|
|
25
|
-
|
|
26
|
-
- 경로 탐색 차단: `pathx.isChildPath(targetFilePath, allowedRootPath)` 가드.
|
|
27
|
-
- 디렉터리 + URL 미 trailing slash → `pathname + "/"` 리다이렉트. trailing slash 있으면 `<dir>/index.html` 사용.
|
|
28
|
-
- 숨김 파일(basename 이 `.` 시작) → 403.
|
|
29
|
-
- ENOENT 404, 기타 500. 에러는 HTML 응답.
|
|
30
|
-
|
|
31
|
-
## WebSocket
|
|
32
|
-
|
|
33
|
-
### `createWebSocketHandler(runMethod, jwtSecret): WebSocketHandler`
|
|
34
|
-
|
|
35
|
-
여러 WS 연결 풀 관리. 처리 메시지:
|
|
36
|
-
|
|
37
|
-
- `<service>.<method>` (body 가 배열) → RPC. `runMethod` 위임.
|
|
38
|
-
- `evt:add { key, name, info }` — 이벤트 리스너 등록.
|
|
39
|
-
- `evt:remove { key }` — 제거.
|
|
40
|
-
- `evt:gets { name }` — 모든 소켓의 해당 이벤트 리스너 정보 조회.
|
|
41
|
-
- `evt:emit { keys, data }` — 매칭 키 가진 소켓에게 `evt:on` 푸시.
|
|
42
|
-
- `auth <token>` — JWT 검증 후 `socket.authTokenPayload` 설정.
|
|
43
|
-
|
|
44
|
-
에러 코드: `BAD_MESSAGE`(알 수 없는 요청), `INTERNAL_ERROR`. `env("DEV")` truthy 시 `stack` 포함.
|
|
45
|
-
|
|
46
|
-
`addSocket(socket, clientId, clientName, connReq)` 동일 clientId 이전 연결 자동 종료 후 교체. `emit(name, infoSelector, data)` 는 `ServiceServer.emitEvent` 의 백엔드.
|
|
47
|
-
|
|
48
|
-
### `createServiceSocket(socket, clientId, clientName, connReq): ServiceSocket`
|
|
49
|
-
|
|
50
|
-
단일 WS 관리.
|
|
51
|
-
|
|
52
|
-
- 5초 ping/pong (`socket.ping()` → 응답 없으면 `terminate()`).
|
|
53
|
-
- 1바이트 `0x01`(ping) 수신 → `0x02`(pong) 송신.
|
|
54
|
-
- 메시지는 `createServerProtocolWrapper()` 통과. 진행률(`type === "progress"`) 디코드 결과는 자동으로 `progress` 메시지 회신.
|
|
55
|
-
|
|
56
|
-
표면: `connectedAtDateTime: DateTime`, `clientName`, `connReq`, `authTokenPayload`(get/set), `close()`, `send(uuid, msg)`(전송 바이트), `addListener(key, eventName, info)`, `removeListener(key)`, `getEventListeners(eventName)`, `filterEventTargetKeys(targetKeys)`, `on("error"|"close"|"message", handler)`.
|
|
57
|
-
|
|
58
|
-
## 프로토콜 래퍼
|
|
59
|
-
|
|
60
|
-
### `createServerProtocolWrapper(): ServerProtocolWrapper`
|
|
61
|
-
|
|
62
|
-
`@simplysm/service-common` 의 `createServiceProtocol()` 기반. 무거운 케이스만 워커 스레드로 위임:
|
|
63
|
-
|
|
64
|
-
- encode: body 가 `Uint8Array` 이거나 `Uint8Array` 요소를 하나라도 가진 배열 → 워커.
|
|
65
|
-
- decode: 입력 바이트 > 30KB → 워커.
|
|
66
|
-
|
|
67
|
-
워커는 모듈 로드 시 1회 생성 lazy singleton(`maxOldGenerationSizeMb: 4096`). 워커 모듈: `workers/service-protocol.worker.ts`.
|
|
68
|
-
|
|
69
|
-
표면: `encode(uuid, message)`, `decode(bytes)`, `dispose()`(메인 스레드 프로토콜만 정리, 워커는 공유).
|
|
70
|
-
|
|
71
|
-
## 설정 캐시
|
|
72
|
-
|
|
73
|
-
### `getConfig<T>(filePath: string): Promise<T | undefined>`
|
|
74
|
-
|
|
75
|
-
JSON 설정 파일 로더.
|
|
76
|
-
|
|
77
|
-
- `LazyGcMap` 캐시: 만료 1시간, GC 10분 간격. 캐시 히트 시 만료 시간 자동 갱신.
|
|
78
|
-
- 파일 없으면 undefined.
|
|
79
|
-
- `FsWatcher` 로 변경 감시 → 100ms 디바운스 후 리로드. 삭제 감지 시 캐시·워처 정리.
|
|
80
|
-
- 만료 시 워처도 함께 해제.
|
|
81
|
-
|
|
82
|
-
`ServiceContext.getConfig(section)` 이 root/client `.config.json` 두 경로를 이 함수로 읽어 `obj.merge`. 그 외 경로의 설정 파일을 읽을 때만 직접 호출.
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# @simplysm/service-server — 서버
|
|
2
|
-
|
|
3
|
-
## `createServiceServer<TAuthInfo>(options): ServiceServer<TAuthInfo>`
|
|
4
|
-
|
|
5
|
-
`new ServiceServer(options)` 단순 팩토리. `TAuthInfo` 는 JWT payload 의 `data` 필드 타입.
|
|
6
|
-
|
|
7
|
-
## `ServiceServerOptions`
|
|
8
|
-
|
|
9
|
-
- `rootPath: string` — 정적/업로드/설정 루트. 정적은 `<rootPath>/www/`, 업로드 저장은 `<rootPath>/www/uploads/`, 설정은 `<rootPath>/.config.json` 및 `<rootPath>/www/<clientName>/.config.json`.
|
|
10
|
-
- `port: number` — listen 포트. host 는 항상 `0.0.0.0`.
|
|
11
|
-
- `ssl?: { pfxBytes: Uint8Array; passphrase: string }` — 지정 시 HTTPS. 내부에서 `Buffer.from(pfxBytes)` 변환. 미지정 시 helmet 의 `upgrade-insecure-requests` 제거, `hsts`/`crossOriginOpenerPolicy` 비활성.
|
|
12
|
-
- `auth?: { jwtSecret: string } | false`
|
|
13
|
-
- `undefined`(미지정): auth 미구성. 인증 요구 서비스가 하나라도 있으면 `listen()` 시 throw.
|
|
14
|
-
- `false`: 의도적 비활성화. `auth()` 래핑된 서비스/메서드도 인증 스킵.
|
|
15
|
-
- 객체: jwt 시크릿 등록.
|
|
16
|
-
- `services: ServiceDefinition[]` — `defineService()` 결과 배열.
|
|
17
|
-
- `legacyV1Handlers?: V1RequestHandler[]` — ver≠"2" 로 접속한 클라이언트의 커스텀 핸들러. 이 배열도 비고 `AutoUpdate` 서비스도 services 에 없으면 V1 연결을 1008 로 거부.
|
|
18
|
-
|
|
19
|
-
## `ServiceServer<TAuthInfo>`
|
|
20
|
-
|
|
21
|
-
`EventEmitter<{ ready: void; close: void }>` 상속.
|
|
22
|
-
|
|
23
|
-
- `readonly options: ServiceServerOptions`
|
|
24
|
-
- `readonly fastify: FastifyInstance` — 생성자에서 즉시 생성, 플러그인은 `listen()` 시 등록.
|
|
25
|
-
- `isOpen: boolean`
|
|
26
|
-
- `listen(): Promise<void>` — fastify 플러그인(`@fastify/websocket`, `@fastify/helmet`, `@fastify/multipart`, `@fastify/static`, `@fastify/cors`) 등록, JSON 파서/직렬화기를 `@simplysm/core-common` 의 `json` 으로 교체(Date/BigInt/Uint8Array 보존), 라우트 바인딩, `0.0.0.0:port` listen, SIGINT/SIGTERM 핸들러 등록(10초 후 `process.exit(1)` 강제), `ready` 이벤트 발생.
|
|
27
|
-
- `close(): Promise<void>` — 모든 WebSocket 종료 → `fastify.close()` → `close` 이벤트.
|
|
28
|
-
- `getEvent<TEventDef>(eventName): ServerEventProxy<TEventDef>` — `{ emit(infoSelector, data): Promise<void> }` 핸들 반환.
|
|
29
|
-
- `emitEvent<TEventDef>(eventName, infoSelector, data): Promise<void>` — 클라이언트가 `evt:add` 로 등록한 리스너 중 `infoSelector(info) === true` 인 키에만 푸시.
|
|
30
|
-
- `signAuthToken(payload: AuthTokenPayload<TAuthInfo>): Promise<string>` — HS256, `exp` 12h 고정. `jwtSecret` 미구성 시 throw.
|
|
31
|
-
- `verifyAuthToken(token: string): Promise<AuthTokenPayload<TAuthInfo>>` — 만료/위조 시 throw.
|
|
32
|
-
|
|
33
|
-
`TEventDef` 는 `@simplysm/service-common` 의 `ServiceEventDef`(`{ $info; $data }` 형태). `infoSelector` 는 클라이언트가 listener 등록 시 보낸 `info` 객체를 받아 boolean 반환.
|
|
34
|
-
|
|
35
|
-
## 자동 등록되는 라우트
|
|
36
|
-
|
|
37
|
-
- `ALL /api/:service/:method` — RPC. `x-sd-client-name` 헤더 필수, `Authorization: Bearer <jwt>` 옵션. GET 은 `?json=<JSON encoded array>`, POST 는 body 가 params 배열. 그 외 HTTP 메서드 405.
|
|
38
|
-
- `ALL /upload` — multipart 업로드. 인증 헤더 필수. 응답 `ServiceUploadResult[]`.
|
|
39
|
-
- `GET /`, `GET /ws` — WebSocket. 쿼리 `ver=2&clientId=<id>&clientName=<name>` (clientId/clientName 누락 시 1008). ver≠"2" → V1 레거시.
|
|
40
|
-
- `* /*` — 정적 서빙(`/api/...`·`/upload`·`/`·`/ws` 외).
|
|
41
|
-
|
|
42
|
-
## 예
|
|
43
|
-
|
|
44
|
-
```ts
|
|
45
|
-
const server = createServiceServer<{ userId: string }>({
|
|
46
|
-
rootPath: process.cwd(),
|
|
47
|
-
port: 50080,
|
|
48
|
-
auth: { jwtSecret: process.env.JWT_SECRET! },
|
|
49
|
-
services: [UserService, OrmService],
|
|
50
|
-
});
|
|
51
|
-
server.on("ready", () => console.log("up"));
|
|
52
|
-
await server.listen();
|
|
53
|
-
|
|
54
|
-
const token = await server.signAuthToken({ roles: ["admin"], data: { userId: "u1" } });
|
|
55
|
-
await server.getEvent<{ $info: { shopId: number }; $data: { id: number } }>("order-created")
|
|
56
|
-
.emit(info => info.shopId === 1, { id: 99 });
|
|
57
|
-
```
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
# @simplysm/storage
|
|
2
|
-
|
|
3
|
-
FTP/FTPS/SFTP 원격 스토리지에 동일 인터페이스(`StorageClient`)로 파일을 읽고/쓰는 Node 라이브러리.
|
|
4
|
-
|
|
5
|
-
## 사용 트리거 인덱스
|
|
6
|
-
|
|
7
|
-
- **`StorageFactory.connect`** — FTP/FTPS/SFTP 어느 것이든 연결→작업→자동 종료를 한 번에 처리할 때 (권장 진입점).
|
|
8
|
-
- **`StorageClient`** — 콜백 안에서 사용하는 공통 파일 작업 인터페이스(mkdir/list/readFile/put/uploadDir/remove/rename/exists).
|
|
9
|
-
- **`FtpStorageClient` / `SftpStorageClient`** — 연결 생명주기를 직접 관리해야 할 때만 (장기 연결 풀 등). 그 외엔 `StorageFactory.connect` 사용.
|
|
10
|
-
- **`StorageConnConfig`** — `connect`/`StorageFactory.connect` 에 넘기는 접속 정보(host/port/user/password) 타입.
|
|
11
|
-
- **`StorageProtocol`** — `StorageFactory.connect` 의 `type` 인자로 쓰는 프로토콜 리터럴(`"ftp" | "ftps" | "sftp"`).
|
|
12
|
-
- **`FileInfo`** — `list` 결과 항목 타입(`{ name, isFile }`).
|
|
13
|
-
|
|
14
|
-
## StorageFactory.connect
|
|
15
|
-
|
|
16
|
-
```ts
|
|
17
|
-
class StorageFactory {
|
|
18
|
-
static connect<R>(
|
|
19
|
-
type: StorageProtocol, // "ftp" | "ftps" | "sftp"
|
|
20
|
-
config: StorageConnConfig,
|
|
21
|
-
fn: (storage: StorageClient) => R | Promise<R>,
|
|
22
|
-
): Promise<R>;
|
|
23
|
-
}
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
- `type` 에 따라 `FtpStorageClient(secure=false)` / `FtpStorageClient(secure=true)` / `SftpStorageClient` 생성.
|
|
27
|
-
- 콜백 실행 전 `connect`, 콜백 종료(예외 포함) 후 `close` 자동 호출. `close` 실패는 무시.
|
|
28
|
-
|
|
29
|
-
```ts
|
|
30
|
-
const list = await StorageFactory.connect("sftp", { host, user, password }, async (s) => {
|
|
31
|
-
await s.mkdir("/up/2026");
|
|
32
|
-
await s.put(Buffer.from("hi"), "/up/2026/a.txt");
|
|
33
|
-
return await s.list("/up/2026");
|
|
34
|
-
});
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## StorageClient
|
|
38
|
-
|
|
39
|
-
```ts
|
|
40
|
-
interface StorageClient {
|
|
41
|
-
connect(config: StorageConnConfig): Promise<void>;
|
|
42
|
-
mkdir(dirPath: string): Promise<void>; // 부모 디렉토리 자동 생성
|
|
43
|
-
rename(fromPath: string, toPath: string): Promise<void>;
|
|
44
|
-
list(dirPath: string): Promise<FileInfo[]>; // { name, isFile }
|
|
45
|
-
readFile(filePath: string): Promise<Bytes>; // Bytes = Uint8Array (@simplysm/core-common)
|
|
46
|
-
exists(filePath: string): Promise<boolean>; // 모든 예외 시 false
|
|
47
|
-
put(localPathOrBuffer: string | Bytes, storageFilePath: string): Promise<void>; // string=로컬 경로, Bytes=메모리 버퍼
|
|
48
|
-
uploadDir(fromPath: string, toPath: string): Promise<void>; // 디렉토리 통째로 업로드
|
|
49
|
-
remove(filePath: string): Promise<void>;
|
|
50
|
-
close(): Promise<void>; // 이미 종료돼 있어도 안전
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
interface StorageConnConfig { host: string; port?: number; user?: string; password?: string; }
|
|
54
|
-
interface FileInfo { name: string; isFile: boolean; }
|
|
55
|
-
type StorageProtocol = "ftp" | "ftps" | "sftp";
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## FtpStorageClient / SftpStorageClient
|
|
59
|
-
|
|
60
|
-
`StorageClient` 구현체. 직접 사용 시 주의:
|
|
61
|
-
|
|
62
|
-
- `connect` 후 반드시 `close`. 동일 인스턴스에서 `connect` 중복 호출 금지(연결 누수 → `SdError` throw). `close` 후 재연결은 가능.
|
|
63
|
-
- `FtpStorageClient(secure: boolean)` — `secure=true` 가 FTPS.
|
|
64
|
-
- `SftpStorageClient` — `config.password` 가 있으면 비밀번호 인증. 없으면 `~/.ssh/id_ed25519` 키 + (있으면) `SSH_AUTH_SOCK` agent 로 시도, 키 파싱 실패 시 agent 단독 재시도.
|
|
65
|
-
- `FtpStorageClient.exists` — 파일은 `size()` 로 O(1), 디렉토리는 부모 `list()` 스캔. 슬래시 없는 경로는 `/` 기준.
|
|
66
|
-
|
|
67
|
-
```ts
|
|
68
|
-
const client = new SftpStorageClient();
|
|
69
|
-
await client.connect({ host, user, password });
|
|
70
|
-
try { /* ... */ } finally { await client.close(); }
|
|
71
|
-
```
|