@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/dist/index.d.ts
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { HwpEngine, DocumentHandle, CatEnvelope, RenderOptions, PageImage, EditOp, EditOptions, DocumentSpecV2, ComposeResult, ValidationReport, HwpErrorCode } from '@hwp-editor/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CliEngine — HwpEngine implementation that shells out to the hwp-cli binary.
|
|
5
|
+
*
|
|
6
|
+
* Hardening ported from the ax deployment wrapper (sites/ax/lib/hwp-cli.ts):
|
|
7
|
+
* execFile only (never a shell), an owned 60s budget enforced by this
|
|
8
|
+
* module's own AbortController with SIGTERM-to-SIGKILL escalation (execFile's
|
|
9
|
+
* built-in `timeout` signals once and never escalates, so a signal-ignoring
|
|
10
|
+
* child would hang the request past every budget), a 32MB maxBuffer on every
|
|
11
|
+
* invocation, a scrubbed child environment, and per-call temp directories
|
|
12
|
+
* that are removed on every path including failure. The runCli promise
|
|
13
|
+
* settles ONLY from the execFile callback, which fires after the child has
|
|
14
|
+
* exited; that is what keeps `withWorkDir`'s removal ordered strictly after
|
|
15
|
+
* child exit, so a racing timer must never settle it. Generalizations: the
|
|
16
|
+
* binary is resolved by option/env/PATH instead of a bundled artifact (this
|
|
17
|
+
* package runs on developer machines and servers, not one fixed lambda), and
|
|
18
|
+
* the per-process verification is a minimum-version check instead of a
|
|
19
|
+
* pinned checksum (there is no single reviewed artifact here).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
declare const HWP_TIMEOUT_MS = 60000;
|
|
23
|
+
/**
|
|
24
|
+
* The engine half of the published `HwpErrorCode` vocabulary. Derived with
|
|
25
|
+
* `Extract<>` rather than aliased to the full union on purpose: an alias
|
|
26
|
+
* would make `statusFor`'s switch (routes.ts) non-exhaustive by five at
|
|
27
|
+
* once, and the natural fix for that is a `default:` clause, which
|
|
28
|
+
* permanently destroys the exhaustiveness check that must catch the next
|
|
29
|
+
* code addition.
|
|
30
|
+
*
|
|
31
|
+
* `cancelled` and `output_too_large` were added in Phase 4. Both are engine
|
|
32
|
+
* reasons rather than route-layer codes because both are decided inside
|
|
33
|
+
* `runCli`, from a cause this module owns: only the code that started the
|
|
34
|
+
* child knows whether it ended because the caller went away or because it
|
|
35
|
+
* outran the stdout ceiling. A route-layer code would have to re-infer that
|
|
36
|
+
* from an error shape, which is exactly the guessing this rewrite removed.
|
|
37
|
+
*/
|
|
38
|
+
type HwpCliErrorReason = Extract<HwpErrorCode, "unavailable" | "version" | "timeout" | "failed" | "bad_request" | "unsupported_format" | "protected" | "cancelled" | "output_too_large">;
|
|
39
|
+
/**
|
|
40
|
+
* Two channels, and which one you use decides who sees it.
|
|
41
|
+
*
|
|
42
|
+
* `message` is serialized into the `ErrorResponse` body and crosses the wire
|
|
43
|
+
* to an untrusted client, so it carries the operation and the outcome and
|
|
44
|
+
* nothing else — never the resolved binary path, never a staged temp path,
|
|
45
|
+
* never raw CLI stdout or stderr. `stderr` and `detail` are NOT serialized by
|
|
46
|
+
* `routes.ts`; they are where that context is retained.
|
|
47
|
+
*
|
|
48
|
+
* The scrub is a property of construction rather than a filter applied on the
|
|
49
|
+
* way out: a filter has to be remembered at every new throw site, and the one
|
|
50
|
+
* that is forgotten is the one that leaks.
|
|
51
|
+
*
|
|
52
|
+
* There is no logger in this package, per the no-`console` convention that
|
|
53
|
+
* holds across every package source tree. The host catches the error and
|
|
54
|
+
* decides what to do with `stderr` and `detail` — log them, surface them to
|
|
55
|
+
* an operator, discard them. This module does not make that choice for it.
|
|
56
|
+
*/
|
|
57
|
+
declare class HwpCliError extends Error {
|
|
58
|
+
readonly reason: HwpCliErrorReason;
|
|
59
|
+
/** Raw child stderr, verbatim. Read by `protectedReasonFromStderr`. */
|
|
60
|
+
readonly stderr?: string | undefined;
|
|
61
|
+
/** Operator-facing context: the resolved binary path, CLI output. */
|
|
62
|
+
readonly detail?: string | undefined;
|
|
63
|
+
constructor(reason: HwpCliErrorReason, message: string,
|
|
64
|
+
/** Raw child stderr, verbatim. Read by `protectedReasonFromStderr`. */
|
|
65
|
+
stderr?: string | undefined,
|
|
66
|
+
/** Operator-facing context: the resolved binary path, CLI output. */
|
|
67
|
+
detail?: string | undefined);
|
|
68
|
+
}
|
|
69
|
+
interface CliEngineOptions {
|
|
70
|
+
/**
|
|
71
|
+
* Explicit path to the hwp binary. Resolution order: this option ->
|
|
72
|
+
* HWP_EDITOR_BIN env -> HWP_CLI env -> `hwp` on PATH.
|
|
73
|
+
*/
|
|
74
|
+
bin?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Per-invocation timeout in ms, default HWP_TIMEOUT_MS. Hosts with a hard
|
|
77
|
+
* request budget (e.g. a 60s serverless function) should set this a few
|
|
78
|
+
* seconds below it so the engine's 504 beats the platform's kill.
|
|
79
|
+
*/
|
|
80
|
+
timeoutMs?: number;
|
|
81
|
+
/**
|
|
82
|
+
* Language passed to the child as HWP_LANG, default `en`. Accepts
|
|
83
|
+
* `en`/`eng`/`english`/`c`/`posix` and `ko`/`kor`/`korean`. This sets
|
|
84
|
+
* HWP_LANG only: LANG, LC_ALL and LC_MESSAGES stay pinned to `C.UTF-8`
|
|
85
|
+
* regardless, so a host cannot accidentally change the child's encoding
|
|
86
|
+
* while changing its language.
|
|
87
|
+
*/
|
|
88
|
+
locale?: string;
|
|
89
|
+
}
|
|
90
|
+
/** Everything `read` gathers beyond the pinned CatEnvelope wire shape. */
|
|
91
|
+
interface DocumentInspection {
|
|
92
|
+
envelope: CatEnvelope;
|
|
93
|
+
/** Raw `hwp fields --json` payload (array), null on failure. */
|
|
94
|
+
fields: unknown;
|
|
95
|
+
/** Raw `hwp bookmarks --json` payload (array), null on failure. */
|
|
96
|
+
bookmarks: unknown;
|
|
97
|
+
/** Raw `hwp slots --json` payload (object), null on failure. */
|
|
98
|
+
slots: unknown;
|
|
99
|
+
/** Raw `hwp info --json` payload, null on failure. */
|
|
100
|
+
info: unknown;
|
|
101
|
+
/** Per-document editability derived from `info` (see below). */
|
|
102
|
+
capabilities: {
|
|
103
|
+
editable: boolean;
|
|
104
|
+
reason?: string;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Per-call options this transport accepts beyond the `HwpEngine` contract.
|
|
109
|
+
*
|
|
110
|
+
* Carried as an extra OPTIONAL trailing parameter on every spawning method,
|
|
111
|
+
* which keeps each method assignable to its `HwpEngine` counterpart: the
|
|
112
|
+
* shared interface in `packages/core` is not widened, so the three
|
|
113
|
+
* transports stay interchangeable.
|
|
114
|
+
*/
|
|
115
|
+
interface CliCallOptions {
|
|
116
|
+
/**
|
|
117
|
+
* Aborting this terminates the child (SIGTERM, then SIGKILL after the
|
|
118
|
+
* grace) and rejects with reason `cancelled`. Route handlers pass
|
|
119
|
+
* `req.signal` so a client that disconnects does not leave an orphan.
|
|
120
|
+
*/
|
|
121
|
+
signal?: AbortSignal;
|
|
122
|
+
/**
|
|
123
|
+
* Tenant scope from the server's `authorize` hook, salted into every cache
|
|
124
|
+
* key this engine owns (SEC-04, D-07). One engine instance serves every
|
|
125
|
+
* request a handler sees, so `inspections` and `snapshots` are otherwise a
|
|
126
|
+
* cross-tenant channel: two callers uploading identical bytes would share
|
|
127
|
+
* both entries.
|
|
128
|
+
*
|
|
129
|
+
* The engine RECEIVES a scope and never derives one — the single
|
|
130
|
+
* `authorize` call in routes.ts is the only place it is decided. Omitting
|
|
131
|
+
* it uses `DEFAULT_CALL_SCOPE`, so a direct `CliEngine` consumer with no
|
|
132
|
+
* tenancy of its own keeps working unchanged.
|
|
133
|
+
*/
|
|
134
|
+
scope?: string;
|
|
135
|
+
}
|
|
136
|
+
interface CliEngine extends HwpEngine {
|
|
137
|
+
/**
|
|
138
|
+
* Full read pipeline: cat --with-segments plus fields/bookmarks/slots/info.
|
|
139
|
+
* `read()` is this with the extras dropped, per the pinned wire contract.
|
|
140
|
+
*/
|
|
141
|
+
describe(document: DocumentHandle, call?: CliCallOptions): Promise<DocumentInspection>;
|
|
142
|
+
read(document: DocumentHandle, call?: CliCallOptions): Promise<CatEnvelope>;
|
|
143
|
+
render(document: DocumentHandle, options?: RenderOptions, call?: CliCallOptions): Promise<PageImage[]>;
|
|
144
|
+
edit(document: DocumentHandle, ops: EditOp[], options?: EditOptions, call?: CliCallOptions): Promise<DocumentHandle>;
|
|
145
|
+
compose(spec: DocumentSpecV2, name: string, call?: CliCallOptions): Promise<ComposeResult>;
|
|
146
|
+
validate(document: DocumentHandle, call?: CliCallOptions): Promise<ValidationReport>;
|
|
147
|
+
/**
|
|
148
|
+
* Return the pre-edit snapshot of a document this engine edited, or null.
|
|
149
|
+
* Keyed by the edited document's content hash SALTED WITH THE CALL SCOPE;
|
|
150
|
+
* consumed on use. Takes the same trailing options as the spawning methods
|
|
151
|
+
* (it spawns nothing, but its read must salt exactly as `edit`'s write did,
|
|
152
|
+
* or a scoped write is findable by nobody).
|
|
153
|
+
*/
|
|
154
|
+
undo(document: DocumentHandle, call?: CliCallOptions): DocumentHandle | null;
|
|
155
|
+
/** Resolved binary path and verified version. */
|
|
156
|
+
binaryInfo(): Promise<{
|
|
157
|
+
bin: string;
|
|
158
|
+
version: string;
|
|
159
|
+
}>;
|
|
160
|
+
}
|
|
161
|
+
declare function createCliEngine(opts?: CliEngineOptions): CliEngine;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Server-internal cache of read-pipeline inspections.
|
|
165
|
+
*
|
|
166
|
+
* A per-process, in-memory map from an opaque session id to the extras a
|
|
167
|
+
* `describe()` produced (fields, bookmarks, slots, info, editability), with an
|
|
168
|
+
* idle TTL swept on growth. It touches the filesystem on no code path and
|
|
169
|
+
* retains no document bytes: the wire contract (protocol.ts) is stateless, so
|
|
170
|
+
* every request already carries the document it operates on, and the cache
|
|
171
|
+
* exists only because opening one document otherwise spawns about seven CLI
|
|
172
|
+
* processes.
|
|
173
|
+
*
|
|
174
|
+
* What this store used to be, and why it is not: it kept the current bytes on
|
|
175
|
+
* disk, a pre-edit snapshot history (up to twenty full document copies per
|
|
176
|
+
* session) and an export handle. Nothing in this repository could read any of
|
|
177
|
+
* it back — no route ever called `undo()` or `exportBytes()`, and protocol.ts
|
|
178
|
+
* has no session or undo surface — so the history was disk amplification with
|
|
179
|
+
* no reader (BUG-07, D-05/D-06). Undo lives in the client store,
|
|
180
|
+
* `packages/core/src/state.ts`, bounded at 50 snapshots, and that is the one
|
|
181
|
+
* undo model.
|
|
182
|
+
*
|
|
183
|
+
* Session ids are UUIDs, never client-supplied paths, and `lookup` still
|
|
184
|
+
* checks the shape before the map: with no filesystem left there is no path to
|
|
185
|
+
* confine, but an id from a client is still an id from a client.
|
|
186
|
+
*/
|
|
187
|
+
|
|
188
|
+
declare const DEFAULT_TTL_MS: number;
|
|
189
|
+
declare class SessionNotFoundError extends Error {
|
|
190
|
+
constructor(id: string);
|
|
191
|
+
}
|
|
192
|
+
interface DocumentSession {
|
|
193
|
+
id: string;
|
|
194
|
+
/** Uploaded document file name (basename only); labelling, never a path. */
|
|
195
|
+
name: string;
|
|
196
|
+
createdAt: number;
|
|
197
|
+
touchedAt: number;
|
|
198
|
+
/** Cached read-pipeline extras, when the engine provided them. */
|
|
199
|
+
inspection?: DocumentInspection;
|
|
200
|
+
}
|
|
201
|
+
interface SessionStoreOptions {
|
|
202
|
+
/** Idle time after which sweep() removes a session. Default 30min. */
|
|
203
|
+
ttlMs?: number;
|
|
204
|
+
}
|
|
205
|
+
interface SessionStore {
|
|
206
|
+
/** Register a session for an uploaded document. Bytes are not retained. */
|
|
207
|
+
create(name: string): DocumentSession;
|
|
208
|
+
get(id: string): DocumentSession;
|
|
209
|
+
has(id: string): boolean;
|
|
210
|
+
attachInspection(id: string, inspection: DocumentInspection): void;
|
|
211
|
+
/** Remove expired sessions; returns how many were removed. */
|
|
212
|
+
sweep(now?: number): number;
|
|
213
|
+
/** Drop every session. */
|
|
214
|
+
dispose(): void;
|
|
215
|
+
size(): number;
|
|
216
|
+
/** All live session ids. */
|
|
217
|
+
ids(): string[];
|
|
218
|
+
}
|
|
219
|
+
declare function createSessionStore(opts?: SessionStoreOptions): SessionStore;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Framework-agnostic HTTP handler implementing the wire contract of
|
|
223
|
+
* packages/core/src/protocol.ts as Web Standards `(Request) => Response`.
|
|
224
|
+
* Framework adapters (next.ts, or any Fetch-API runtime) delegate here.
|
|
225
|
+
*
|
|
226
|
+
* Binary payloads cross the wire as base64 inside JSON responses; uploads
|
|
227
|
+
* are multipart/form-data parsed with Request.formData(). Every failure is
|
|
228
|
+
* an ErrorResponse with a non-2xx status.
|
|
229
|
+
*
|
|
230
|
+
* The admission gate runs in a fixed order before any body is buffered:
|
|
231
|
+
* action (404) -> method (405) -> authorize (403) -> size (400/413). Only
|
|
232
|
+
* after all four does a handler touch `req.formData()` or `req.json()`, so a
|
|
233
|
+
* refusal costs zero uploaded bytes and zero engine calls (D-04).
|
|
234
|
+
*
|
|
235
|
+
* Two further checks run after the bytes are in hand, in this order: the
|
|
236
|
+
* magic-byte sniff at the single buffering site (`sniffFormat`, refusing
|
|
237
|
+
* anything that is not an HWP or HWPX document, SEC-07), and the op-path
|
|
238
|
+
* filter in the edit path (refusing `insert-image` and `seal`, which name a
|
|
239
|
+
* file on the server's own disk, SEC-05). Both still precede the engine call.
|
|
240
|
+
*
|
|
241
|
+
* Archive limits — declared entry sizes, decompressed byte ceilings and
|
|
242
|
+
* compression-ratio caps — are NOT reimplemented here. They are hwp-cli's
|
|
243
|
+
* default-on `hwp-cli-native-v1` profile (D-12); this handler relies on it,
|
|
244
|
+
* so bumping the binary means re-checking that the profile still applies.
|
|
245
|
+
*/
|
|
246
|
+
|
|
247
|
+
/** The six actions this handler serves; the runtime guard and `HwpAction` share it. */
|
|
248
|
+
declare const ACTION_LIST: readonly ["read", "render", "edit", "compose", "validate", "capabilities"];
|
|
249
|
+
/** One of the six action names the handler dispatches on. */
|
|
250
|
+
type HwpAction = (typeof ACTION_LIST)[number];
|
|
251
|
+
/**
|
|
252
|
+
* Host-supplied admission hook. Its return value answers two questions at
|
|
253
|
+
* once, deliberately (D-01): a string ADMITS the request AND is the tenant
|
|
254
|
+
* scope every server-side cache key is salted with; `null` REFUSES it with
|
|
255
|
+
* HTTP 403 and code `forbidden`. One call decides both, so admission and
|
|
256
|
+
* tenancy can never disagree.
|
|
257
|
+
*
|
|
258
|
+
* Called once per request, awaited, before any body is buffered — a refusal
|
|
259
|
+
* therefore costs zero bytes of upload and zero engine calls (D-04).
|
|
260
|
+
*
|
|
261
|
+
* The refusal message is a fixed literal: a `{ allow, scope, reason }` shape
|
|
262
|
+
* was rejected precisely so no host-authored reason string can ride out to
|
|
263
|
+
* an unauthenticated client.
|
|
264
|
+
*/
|
|
265
|
+
type AuthorizeFn = (req: Request, action: HwpAction) => Promise<string | null>;
|
|
266
|
+
interface RoutesOptions {
|
|
267
|
+
/** Engine to serve; defaults to a CliEngine resolved from env/PATH. */
|
|
268
|
+
engine?: HwpEngine;
|
|
269
|
+
/** Convenience for the default engine: explicit hwp binary path. */
|
|
270
|
+
bin?: string;
|
|
271
|
+
/** Convenience for the default engine: per-invocation timeout in ms. */
|
|
272
|
+
timeoutMs?: number;
|
|
273
|
+
/**
|
|
274
|
+
* Convenience for the default engine: language passed to the child as
|
|
275
|
+
* HWP_LANG, default `en`. Accepts `en`/`eng`/`english`/`c`/`posix` and
|
|
276
|
+
* `ko`/`kor`/`korean`. Applies to the default engine only — an explicit
|
|
277
|
+
* `engine` carries its own locale.
|
|
278
|
+
*/
|
|
279
|
+
locale?: string;
|
|
280
|
+
/**
|
|
281
|
+
* Largest request admitted, in bytes; defaults to 52428800 (50 MiB).
|
|
282
|
+
* The figure is the WHOLE request envelope — multipart boundaries, field
|
|
283
|
+
* names and part headers included — not the document alone, because it is
|
|
284
|
+
* compared against `Content-Length`. Checked before any buffering; a
|
|
285
|
+
* request over it is refused with 413 and a request with no measurable
|
|
286
|
+
* `Content-Length` with 400.
|
|
287
|
+
*/
|
|
288
|
+
maxRequestBytes?: number;
|
|
289
|
+
/**
|
|
290
|
+
* Cache of read-pipeline inspections, keyed by an opaque session id. Pass
|
|
291
|
+
* false to disable; defaults to a per-handler in-memory store. It retains no
|
|
292
|
+
* document bytes and touches no filesystem — the wire is stateless and undo
|
|
293
|
+
* lives in the client store (D-05/D-06).
|
|
294
|
+
*/
|
|
295
|
+
sessions?: SessionStore | false;
|
|
296
|
+
/**
|
|
297
|
+
* Admission hook run before any body is read. Defaults to allow-all with a
|
|
298
|
+
* fixed scope: this package owns no auth, the host owns the trust boundary
|
|
299
|
+
* (see the trust-boundary section of packages/server/README.md).
|
|
300
|
+
*/
|
|
301
|
+
authorize?: AuthorizeFn;
|
|
302
|
+
}
|
|
303
|
+
type HwpEditorHandler = (req: Request) => Promise<Response>;
|
|
304
|
+
declare function createHwpEditorHandler(opts?: RoutesOptions): HwpEditorHandler;
|
|
305
|
+
|
|
306
|
+
export { type AuthorizeFn, type CliEngine, type CliEngineOptions, DEFAULT_TTL_MS, type DocumentInspection, type DocumentSession, HWP_TIMEOUT_MS, type HwpAction, HwpCliError, type HwpCliErrorReason, type HwpEditorHandler, type RoutesOptions, SessionNotFoundError, type SessionStore, type SessionStoreOptions, createCliEngine, createHwpEditorHandler, createSessionStore };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_TTL_MS,
|
|
3
|
+
HWP_TIMEOUT_MS,
|
|
4
|
+
HwpCliError,
|
|
5
|
+
SessionNotFoundError,
|
|
6
|
+
createCliEngine,
|
|
7
|
+
createHwpEditorHandler,
|
|
8
|
+
createSessionStore
|
|
9
|
+
} from "./chunk-YJDXTIO7.js";
|
|
10
|
+
export {
|
|
11
|
+
DEFAULT_TTL_MS,
|
|
12
|
+
HWP_TIMEOUT_MS,
|
|
13
|
+
HwpCliError,
|
|
14
|
+
SessionNotFoundError,
|
|
15
|
+
createCliEngine,
|
|
16
|
+
createHwpEditorHandler,
|
|
17
|
+
createSessionStore
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|