@hwp-editor/server 1.0.0-rc.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Young Joon Lee
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # @hwp-editor/server
2
+
3
+ Node adapter that serves the `@hwp-editor/core` HTTP contract by spawning the
4
+ external [hwp-cli](https://github.com/STAIxBWLB/hwp-cli) binary. All document
5
+ work (read/render/edit/compose/validate) is delegated to that binary; this
6
+ package contains no HWP parsing, only a hardened subprocess wrapper and a
7
+ framework-agnostic `(Request) => Response` handler.
8
+
9
+ ## Usage
10
+
11
+ Next.js App Router:
12
+
13
+ ```ts
14
+ // app/api/hwp-editor/[...action]/route.ts
15
+ import { createHwpEditorRoutes } from "@hwp-editor/server/next";
16
+
17
+ export const runtime = "nodejs"; // the handler spawns a child process
18
+ export const { GET, POST } = createHwpEditorRoutes({});
19
+ ```
20
+
21
+ Any Fetch-API runtime (Hono, Bun, Deno, a bare `node:http` bridge):
22
+
23
+ ```ts
24
+ import { createHwpEditorHandler } from "@hwp-editor/server";
25
+
26
+ const handler = createHwpEditorHandler({ bin: process.env.HWP_EDITOR_BIN });
27
+ // handler(request) -> Response, for POST /read|render|edit|compose|validate
28
+ // and GET /capabilities. The action is the last path segment.
29
+ ```
30
+
31
+ The client is `createHttpEngine(baseUrl)` from `@hwp-editor/core`; point it at
32
+ whatever path you mounted the handler on.
33
+
34
+ ## Options
35
+
36
+ `createHwpEditorRoutes` and `createHwpEditorHandler` take the same
37
+ `RoutesOptions` object. The Next.js factory declares no options interface of
38
+ its own and forwards yours whole, so every field below reaches both.
39
+
40
+ | Option | Type | Notes |
41
+ | ------ | ---- | ----- |
42
+ | `engine` | `HwpEngine` | Serve a supplied engine instead of the default `CliEngine`. A plain `HwpEngine` receives no per-call options, so its children are not cancellable from here and its caches are not scope-salted. |
43
+ | `bin` | `string` | Default engine only: explicit path to the `hwp` binary. First in the resolution order below. |
44
+ | `timeoutMs` | `number` | Default engine only: per-invocation timeout, default `60000`. On a host with a hard request budget (a 60s serverless function), set this a few seconds under it so this package's 504 beats the platform's kill. |
45
+ | `locale` | `string` | Default engine only: language passed to the child as `HWP_LANG`, default `en`. Accepts `en`/`eng`/`english`/`c`/`posix` and `ko`/`kor`/`korean`. Unrelated to `@hwp-editor/react`'s `locale` prop, which is UI chrome only. |
46
+ | `maxRequestBytes` | `number` | Largest request admitted, default `52428800` (50 MiB). Compared against `Content-Length` before any buffering, so the figure covers the whole request envelope (multipart boundaries and part headers included), not the document alone. **Not a memory bound**; see Deployment assumptions. |
47
+ | `sessions` | `SessionStore \| false` | In-memory cache of read-pipeline extras (fields/bookmarks/slots/info), keyed by an opaque id with a 30-minute idle TTL. It retains no document bytes and touches no filesystem. Pass `false` to disable. |
48
+ | `authorize` | `AuthorizeFn` | `(req, action) => Promise<string \| null>`. The trust boundary; see below. Defaults to allow-all. |
49
+
50
+ ## Trust boundary
51
+
52
+ **This package owns no authentication and by default admits every request.**
53
+ A deny-by-default would break every existing caller and the one-line example
54
+ above, so the default is permissive and stated rather than hidden. Mounting
55
+ this handler on a route an untrusted client can reach, without supplying
56
+ `authorize`, means that client can run the binary on any document they send.
57
+
58
+ `authorize(req, action)` is the insertion point:
59
+
60
+ ```ts
61
+ createHwpEditorRoutes({
62
+ async authorize(req, action) {
63
+ const session = await getSession(req); // your auth, your rules
64
+ if (session === null) return null; // -> HTTP 403, code `forbidden`
65
+ return session.tenantId; // -> the cache scope for this request
66
+ },
67
+ });
68
+ ```
69
+
70
+ Its return value answers two questions in one call, deliberately:
71
+
72
+ - **Admission.** `null` refuses the request with HTTP 403 and `error.code`
73
+ `forbidden`. The message is a fixed literal: no host-authored reason string
74
+ can ride out to an unauthenticated client. It is awaited **before any body is
75
+ read**, so a refusal costs zero uploaded bytes and zero engine calls.
76
+ - **Tenancy.** The string it returns is the scope every server-side cache key is
77
+ salted with: the session map in the handler and the inspection and undo-
78
+ snapshot caches inside the engine. Two callers uploading identical bytes under
79
+ different scopes share no entry of any kind. Because one call decides both,
80
+ admission and isolation cannot disagree.
81
+
82
+ `GET /capabilities` is gated too; it discloses the resolved hwp-cli version.
83
+
84
+ With no hook supplied every request is allowed and every request uses the same
85
+ fixed scope, which is the correct behaviour for a single-tenant host and the
86
+ wrong behaviour for a multi-tenant one.
87
+
88
+ ## Request admission
89
+
90
+ Every refusal below is decided before the corresponding cost is incurred.
91
+
92
+ Pre-buffer, from the method, URL and headers alone:
93
+
94
+ 1. **Unknown action** → 404 `not_found`.
95
+ 2. **Wrong method** → 405 `method_not_allowed` (`GET` for `capabilities`, `POST`
96
+ for the rest).
97
+ 3. **`authorize`** → 403 `forbidden`.
98
+ 4. **`Content-Length`** → 413 when over `maxRequestBytes`, 400 when the header is
99
+ absent or unparseable.
100
+
101
+ Post-buffer, before the binary spawns:
102
+
103
+ 5. **Magic-byte sniff**: the upload must be a CFBF/OLE2 container (HWP5) or a
104
+ zip whose first entry is a STORED `mimetype` reading `application/hwp+zip`
105
+ (HWPX). Anything else is 400. About ninety bytes are read and nothing is
106
+ decompressed.
107
+ 6. **Op path filter**: an `edit` whose ops include `insert-image` or `seal` is
108
+ refused 400 `path_traversal`.
109
+
110
+ Two operational consequences worth knowing before you deploy:
111
+
112
+ - **A request with no `Content-Length` is refused with 400.** A proxy or ingress
113
+ that re-frames uploads as `Transfer-Encoding: chunked` will therefore break
114
+ every upload. The alternative, counting bytes as they arrive, would have to
115
+ read the body first, which is exactly the cost the gate exists to avoid.
116
+ - **`insert-image` and `seal` are unavailable over HTTP.** Both name a path on
117
+ the server's own filesystem, so over HTTP a client could otherwise ask the
118
+ binary to embed any file the server process can read. Both remain available on
119
+ the Tauri transport, which is a local application. A staged-asset upload flow
120
+ that makes them usable here is planned but not shipped.
121
+
122
+ ## Deployment assumptions
123
+
124
+ Four things this package assumes and does not enforce. Each is enforced by the
125
+ container or the host, so each is stated here rather than in code.
126
+
127
+ **Memory: size the container at 2 GiB or more per concurrent CLI invocation.**
128
+ hwp-cli's own default limit profile (`hwp-cli-native-v1`) permits 512 MiB per
129
+ archive entry and 2 GiB per package at a compression ratio of up to 1000:1, and
130
+ `hwp cat` (which every read performs) materialises image data in memory. A
131
+ measured 9.0 MB HWPX upload drove a single invocation to 1.70 GB peak RSS while
132
+ staying entirely inside those legal limits. **`maxRequestBytes` (default 50 MiB)
133
+ is a buffering bound on the request envelope, not a memory bound**; the two do
134
+ not compose, and sizing the container against the upload cap is the specific
135
+ mistake this section exists to prevent. The figure is per concurrent
136
+ invocation, not a total.
137
+
138
+ **Temp filesystem: size it for the staged input plus the render output of as
139
+ many calls as run concurrently.** Each invocation stages its input document and
140
+ any render output in a private temp directory, removed only after the child
141
+ exits.
142
+
143
+ **Concurrency: this package does not bound it; the host does.** There is no
144
+ queue, no semaphore and no in-flight cap here. Combined with the memory figure
145
+ above, an unbounded concurrent request rate is an unbounded memory bill.
146
+
147
+ **Archive limits: this handler relies on `hwp-cli-native-v1` and deliberately
148
+ implements no second decompression, entry-count or XML-size guard.** A duplicate
149
+ guard in TypeScript would be a second thing to keep correct against the same
150
+ threat. Bumping the binary therefore means re-checking that the profile still
151
+ applies and is still default-on.
152
+
153
+ ## Binary
154
+
155
+ Resolution order, first match wins:
156
+
157
+ 1. the `bin` option
158
+ 2. `HWP_EDITOR_BIN`
159
+ 3. `HWP_CLI`
160
+ 4. `hwp` on `PATH`
161
+
162
+ The resolved binary is verified once per engine instance, and must satisfy both
163
+ checks:
164
+
165
+ - **Version range**: at least `0.16.0`, and below `1.0.0`. The floor is hard: a
166
+ binary under it lacks flags this package emits. The ceiling is only a
167
+ major-version gate, because a version string is what a binary calls itself.
168
+ - **Flag handshake**: `hwp edit --help` must list every long flag the edit-op
169
+ grammar emits, plus `--verify` and `--allow-partial`, matched on word
170
+ boundaries. A binary that reports an acceptable version but has dropped a flag
171
+ is refused with `error.code` `version`. That is the check that actually
172
+ binds this package to what the binary can do.
173
+
174
+ Child environment: the child is spawned with a scrubbed environment. `PATH` and
175
+ `HOME` are passed through, `HWP_FONT_DIR` is the only `HWP_*` variable copied
176
+ from the ambient environment, and `HWP_LANG`, `LANG`, `LC_ALL` and `LC_MESSAGES`
177
+ are pinned by this package (the locale three to `C.UTF-8`). Everything else,
178
+ including any credential in the parent's environment, is stripped.
179
+
180
+ ## Errors
181
+
182
+ Every non-2xx response is an `ErrorResponse`: `{ error: { code, message } }`.
183
+ Codes map to statuses as follows: `bad_request`/`path_traversal`/
184
+ `unsupported_format` 400, `forbidden` 403, `not_found`/`session_not_found` 404,
185
+ `method_not_allowed` 405, `output_too_large` 413, `failed`/`protected` 422,
186
+ `cancelled` 499, `internal`/`version` 500, `unavailable` 503, `timeout` 504.
187
+
188
+ Messages carry no filesystem path and no raw CLI output. The binary path, the
189
+ staged temp file name and the child's stdout/stderr live on non-serialized
190
+ fields of the thrown `HwpCliError` (`stderr`, `detail`), which a host can catch
191
+ and log itself. This package has no logger and adds none.
192
+
193
+ ## Develop
194
+
195
+ ```sh
196
+ pnpm install
197
+ pnpm -r build
198
+ pnpm -r test # server integration tests skip without a binary
199
+ pnpm -r typecheck
200
+ ```
201
+
202
+ Point the suite at a specific binary with `HWP_EDITOR_BIN`.