@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 +21 -0
- package/README.md +202 -0
- package/dist/chunk-YJDXTIO7.js +927 -0
- package/dist/chunk-YJDXTIO7.js.map +1 -0
- package/dist/index.cjs +964 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +306 -0
- package/dist/index.d.ts +306 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/next.cjs +964 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.cts +28 -0
- package/dist/next.d.ts +28 -0
- package/dist/next.js +17 -0
- package/dist/next.js.map +1 -0
- package/package.json +61 -0
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`.
|