@wasm-oj/organizer 0.2.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 JacobLinCool
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,5 @@
1
+ # `@wasm-oj/organizer`
2
+
3
+ Schemas and publication utilities for problem collections, contest projections, and immutable
4
+ judge packages. The Organizer boundary validates schema, paths, size, digest, and deployability; it
5
+ does not compile, run, score, or assess a reference solution.
@@ -0,0 +1,302 @@
1
+ # Third-party notices
2
+
3
+ This file covers third-party software distributed by the WASM-OJ code and
4
+ toolchain packages, including the content-addressed compiler and runtime assets
5
+ mirrored under `public/toolchains`. The WASM-OJ source code is licensed under the MIT License in
6
+ `LICENSE`; that license does not replace the terms of any component listed
7
+ below.
8
+
9
+ The canonical machine-readable component, distribution, source-revision, and
10
+ license-file inventory is `licenses/components.json`. Its verifier binds every
11
+ listed file to a SHA-256 digest and rejects unlisted toolchain or license files.
12
+
13
+ ## JavaScript runtime dependency
14
+
15
+ ### N-API keyring
16
+
17
+ - Component: `@napi-rs/keyring` 1.3.0
18
+ - Source revision: `Brooooooklyn/keyring-node@e46be75c3ba8d5fde6b88a17c6153b87ffe4b946`
19
+ - Source: <https://github.com/Brooooooklyn/keyring-node/tree/e46be75c3ba8d5fde6b88a17c6153b87ffe4b946>
20
+ - License: MIT
21
+ - License material: `licenses/napi-rs-keyring-MIT.txt`
22
+
23
+ The `woj` CLI stores remote access tokens in the operating system credential
24
+ store through this native binding. It does not provide a plaintext fallback.
25
+
26
+ ### es-module-lexer
27
+
28
+ - Component: `es-module-lexer` 2.3.1
29
+ - npm integrity: `sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==`
30
+ - Source revision: `guybedford/es-module-lexer@2b2e6209bac5c06c6ba457f9730014613e1128fb`
31
+ - Source: <https://github.com/guybedford/es-module-lexer/tree/2b2e6209bac5c06c6ba457f9730014613e1128fb>
32
+ - License: MIT
33
+ - License material: `licenses/es-module-lexer-MIT.txt`
34
+
35
+ WASM-OJ uses es-module-lexer inside the browser runner Worker to reject every
36
+ static or dynamic transitive import before loading a content-pinned runtime
37
+ driver plug-in.
38
+
39
+ ### fflate
40
+
41
+ - Component: `fflate` 0.8.3
42
+ - Source revision: `101arrowz/fflate@v0.8.3`
43
+ - Source: <https://github.com/101arrowz/fflate/tree/v0.8.3>
44
+ - License: MIT
45
+ - License material: `licenses/fflate-MIT.txt`
46
+
47
+ WASM-OJ uses fflate for bounded dependency archive extraction and to verify the
48
+ canonical Go module `h1:` hash over module ZIP entries.
49
+
50
+ ### Wasmer JavaScript SDK
51
+
52
+ - Component: `@wasmer/sdk` 0.10.0
53
+ - npm integrity: `sha512-YQ+s5tGag6P/I8kp9BTH+XhjoS9UFvWiZJvnWEEovClHffhYToKhprWr4UJG7wLP7c/2HQpGkF7ZrjoUvKjdmA==`
54
+ - Source revision: `wasmerio/wasmer-js@93b8b738ebd3ee57e118da0f0eb795b97d5b999e`
55
+ - Locked Rust graph: `Cargo.lock` SHA-256
56
+ `d352926f3f05e3d4308c4e261711d07db568e5c2b4387067180f920da074791f`
57
+ - Source: <https://github.com/wasmerio/wasmer-js/tree/93b8b738ebd3ee57e118da0f0eb795b97d5b999e>
58
+ - License: MIT
59
+ - License material: `licenses/wasmer-sdk-MIT.txt`,
60
+ `licenses/wasmer-sdk-dependencies.html`, and
61
+ `licenses/wasmer-sdk-dependencies.json`
62
+
63
+ WASM-OJ consumes the official npm artifact without patching the installed
64
+ package. The generated dependency report covers every package in the pinned
65
+ normal Cargo dependency graph selected for `wasm32-unknown-unknown`; its compact
66
+ inventory binds all 332 package identities and the exact HTML report digest.
67
+
68
+ ## Distributed toolchain assets
69
+
70
+ ### TypeScript-Go
71
+
72
+ - Distributed asset: `typescript-7.0.2.wasm.gz.bin`
73
+ - Compressed SHA-256: `06e58ce887d95d1895055699b8dc96a1cde7d1f2baa48de40f9b790e3271dc16`
74
+ - Expanded Wasm SHA-256: `27c931087b3b0cc48804f3749b07eb710538c4ab175969db8a8ff680e6ec4fde`
75
+ - Source revision: `microsoft/typescript-go@2bd066d87f5bafd315be9f40889d0a60b9e58e0b`
76
+ - Source: <https://github.com/microsoft/typescript-go/tree/2bd066d87f5bafd315be9f40889d0a60b9e58e0b>
77
+ - Build runtime: Go 1.26.3 standard library (`GOOS=wasip1`, `GOARCH=wasm`)
78
+ - Licenses: TypeScript-Go Apache-2.0; Go standard library BSD-3-Clause
79
+ - License and attribution material: `licenses/Apache-2.0.txt`,
80
+ `licenses/typescript-go-NOTICE.txt`, and
81
+ `licenses/go-BSD-3-Clause.txt`
82
+
83
+ The TypeScript-Go notice is carried from the exact pinned source revision
84
+ (normalized from CRLF to LF without changing its text). The Go license is from
85
+ the exact `go1.26.3` source tag used by the reproducible build script.
86
+
87
+ ### Go compiler toolchain
88
+
89
+ - Distributed assets: `go-1.26.5-wasip1.webc.gz.bin`,
90
+ `go-1.26.5-wasip1.stdlib.gz.bin`, and
91
+ `go-1.26.5-wasip1.manifest.json`
92
+ - Source: <https://go.dev/dl/go1.26.5.src.tar.gz>
93
+ - Source archive SHA-256:
94
+ `495be4bc87176ac567392e5b4116abd98466d33d7b49d41e764ccc6976b2dc42`
95
+ - License: BSD-3-Clause
96
+ - License material: `licenses/go-BSD-3-Clause.txt`
97
+
98
+ WASM-OJ packages the standard Go `compile` and `link` commands plus the matching
99
+ 349-package `GOOS=wasip1 GOARCH=wasm` standard library. The stored license is
100
+ byte-identical to `go/LICENSE` in the exact Go 1.26.5 source distribution.
101
+
102
+ ### Java compiler toolchain
103
+
104
+ - Distributed assets: `java-teavm-0.13.1.wasi.compiler.webc.gz.bin`,
105
+ `java-teavm-0.13.1.compile-classlib.bin`, and
106
+ `java-teavm-0.13.1.runtime-classlib.bin`
107
+ - TeaVM Java compiler source: <https://github.com/konsoletyper/teavm-javac/tree/7e4a44cf521694a4e326e33850dd8aec165eb5c9>
108
+ - TeaVM core source: `konsoletyper/teavm@b3a245b7d9034ff35cdfab2def057a3d4f256efb`
109
+ plus the pinned WASI compiler and math-intrinsic patch
110
+ - Compiler package compressed SHA-256:
111
+ `129f1f51d591e58954f88787d36396b856a9a68ba3ae9c9d14f20bd67c2c7722`
112
+ - Compiler package expanded SHA-256:
113
+ `f8f86761cf31062565187e4a66f73b6903f257fe84c0ce70ea1cd28441b6c2e9`
114
+ - Licenses: TeaVM Apache-2.0; OpenJDK class library GPL-2.0 with Classpath
115
+ Exception
116
+ - License material: `licenses/Apache-2.0.txt` and
117
+ `licenses/openjdk-21-GPL-2.0-with-Classpath-exception.txt`
118
+
119
+ The compiler emits standalone `wasip1` modules. The class-library archives are
120
+ mounted only during compilation and are not included in submitted artifacts.
121
+
122
+ ### QuickJS-ng
123
+
124
+ - Distributed asset: `quickjs-0.15.1.wasm.gz.bin`
125
+ - Compressed SHA-256: `8c7f0588210490e7d77f198fc91f72c1b94787ab4c359c4786ca59a363c4f5e8`
126
+ - Expanded Wasm SHA-256: `956bf2b3700690e1817034eb8e063cfd9781c66b4bffa244ef4f9445656ccfa1`
127
+ - Source revision: `quickjs-ng/quickjs@fd0a0210b7be00957751871e7e01b8291268fc29`
128
+ - Source: <https://github.com/quickjs-ng/quickjs/tree/fd0a0210b7be00957751871e7e01b8291268fc29>
129
+ - Build SDK: [WASI SDK 24.0 release archive for arm64 macOS](https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/wasi-sdk-24.0-arm64-macos.tar.gz),
130
+ SHA-256 `aeae999396d5f5caa5ce419f52e83c35869d5fd21d40af80acba2c80f51b0b3a`
131
+ - WASI SDK source revision: `WebAssembly/wasi-sdk@d2bea01edcc46f731156a817f710cdd9fc9c1c19`
132
+ - LLVM and compiler-rt source revision: `llvm/llvm-project@26a1d6601d727a96f4301d0d8647b5a42760ae0c`
133
+ - WASI libc source revision: `WebAssembly/wasi-libc@b9ef79d7dbd47c6c5bafdae760823467c2f60b70`
134
+ - Sources: <https://github.com/WebAssembly/wasi-sdk/tree/d2bea01edcc46f731156a817f710cdd9fc9c1c19>,
135
+ <https://github.com/llvm/llvm-project/tree/26a1d6601d727a96f4301d0d8647b5a42760ae0c>, and
136
+ <https://github.com/WebAssembly/wasi-libc/tree/b9ef79d7dbd47c6c5bafdae760823467c2f60b70>
137
+ - Licenses: QuickJS-ng and the WASM-OJ adapter MIT; LLVM compiler-rt
138
+ Apache-2.0 WITH LLVM-exception plus the third-party terms collected in its
139
+ license; WASI libc Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT, with
140
+ separately licensed dlmalloc, cloudlibc-derived, and musl-derived portions
141
+ - License and attribution material: `licenses/quickjs-ng-MIT.txt`,
142
+ `licenses/wasi-sdk-24.0-Apache-2.0-LLVM-exception.txt`,
143
+ `licenses/wasi-sdk-24.0-compiler-rt-LICENSE.txt`,
144
+ `licenses/wasi-libc-b9ef79d-LICENSE.txt`,
145
+ `licenses/wasi-libc-b9ef79d-Apache-2.0.txt`,
146
+ `licenses/wasi-libc-b9ef79d-MIT.txt`,
147
+ `licenses/wasi-libc-b9ef79d-cloudlibc-BSD-2-Clause.txt`,
148
+ `licenses/wasi-libc-b9ef79d-dlmalloc-CC0-NOTICE.txt`, and
149
+ `licenses/wasi-libc-b9ef79d-emmalloc-NOTICE.txt`, and
150
+ `licenses/wasi-libc-b9ef79d-musl-MIT.txt`
151
+
152
+ The reproducible build links `crt1-command.o`, `libm.a`,
153
+ `libwasi-emulated-signal.a`, `libc.a`, and
154
+ `libclang_rt.builtins-wasm32.a` from that one digest-verified SDK archive.
155
+ Linker tracing confirms that the selected allocator is dlmalloc, not emmalloc.
156
+ WASM-OJ distributes the resulting stripped Wasm runtime, not the host Clang, LLD,
157
+ or strip executables. The build script nevertheless verifies those executables
158
+ and every linked archive by SHA-256 before compiling.
159
+
160
+ ### Rust compiler and standard libraries
161
+
162
+ - Distributed assets: `rust-1.91.1-dev.webc.gz.bin` and
163
+ `rust-1.91.1-dev.manifest.json`
164
+ - Build source: `olimpiadi-informatica/wasm-compilers@ae62cab6adf0665377d19ffa39daeaf758290431`
165
+ - Build artifact: GitHub Actions run `26545267884`, `rust.tar.br` SHA-256
166
+ `ba0096d05275954d852a3fb3a9c4c9438dad501f8e428b867c0b88cfa7301c14`
167
+ - Linker payload: `@yowasp/clang@22.0.0-git20542-10`, npm archive SHA-256
168
+ `6230ea1afa9691fa065935cf68c01642ff9b31c183fe8ac64cdfda025df06009`;
169
+ its exact source revisions and license closure are recorded in the following
170
+ “Clang, LLD, libc++, and WASI libc” section
171
+ - Rust source submodule: `rust-lang/rust@ed61e7d7e242494fb7057f2657300d9e77bb4fcb`
172
+ - LLVM source submodule: `llvm/llvm-project@87f0227cb60147a26a1eeb4fb06e3b505e9c7261`
173
+ - GCC source submodule: `gcc-mirror/gcc@ae91b5dd14920ff9671db8ff80c0d763d25f977f`
174
+ - Source: <https://github.com/olimpiadi-informatica/wasm-compilers/tree/ae62cab6adf0665377d19ffa39daeaf758290431>
175
+ - Build repository license: Apache-2.0
176
+ - Rust license: MIT OR Apache-2.0; LLVM license: Apache-2.0 WITH LLVM-exception
177
+ - `libloading` 0.8.8: ISC; Cargo.lock checksum
178
+ `07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667`
179
+ - GCC libstdc++ runtime components: GPL-3.0 with GCC Runtime Library
180
+ Exception 3.1
181
+ - License and attribution texts: `licenses/rust-MIT.txt`,
182
+ `licenses/Apache-2.0.txt`, `licenses/LLVM-exception.txt`,
183
+ `licenses/rust-COPYRIGHT.txt`, `licenses/rust-COPYRIGHT.html`,
184
+ `licenses/rust-COPYRIGHT-library.html`, `licenses/libloading-ISC.txt`,
185
+ `licenses/GPL-3.0.txt`, and
186
+ `licenses/GCC-Runtime-Library-Exception-3.1.txt`
187
+
188
+ The pinned build repository contains its complete Rust/LLVM source revisions,
189
+ the applied `rust.patch`, and the GitHub workflow used for the pinned artifact.
190
+ That workflow builds libstdc++ from the pinned GCC submodule before building
191
+ LLVM and rustc. The custom `rust.tar.br` omits Rust's generated binary-release
192
+ notices, so WASM-OJ carries the complete `COPYRIGHT.html` and
193
+ `COPYRIGHT-library.html` from the official Rust 1.91.1 release for the exact
194
+ `ed61e7d7e` source revision. The official release archive used to source those
195
+ notices is `rustc-1.91.1-x86_64-unknown-linux-gnu.tar.xz`, SHA-256
196
+ `4b4c596fc5268435c310a79c2e231a5a3567572c930ac0740ef9e147e83baf4e`.
197
+
198
+ WASM-OJ copies the matching `wasm32-wasip1-threads` sysroot, replaces rustc's
199
+ WASI `random_get` and `clock_time_get` imports with deterministic internal
200
+ implementations, and packages the transformed atom and sysroot together with
201
+ the verified YoWASP LLVM linker atom and resources into one WebC. The provenance
202
+ manifest records source-rustc, transformed-rustc, linker archive, linker core,
203
+ and linker-resource SHA-256 digests. These transformations and the generated
204
+ WebC do not relicense Rust or any third-party component identified by the exact
205
+ Rust COPYRIGHT reports and YoWASP source closure carried here.
206
+
207
+ ### Clang, LLD, libc++, PCH, and WASI libc
208
+
209
+ - Distributed assets: `clang-22.0.0-git20542-10.webc.gz.bin`, the matching
210
+ debug/release libc++ PCH payloads, and their admission manifest
211
+ - Clang source artifact: `@yowasp/clang@22.0.0-git20542-10`, npm archive
212
+ SHA-256 `6230ea1afa9691fa065935cf68c01642ff9b31c183fe8ac64cdfda025df06009`
213
+ - YoWASP source revision: `YoWASP/clang@944dd7c774954180e621cc8e12984023a7f8bcbe`
214
+ - LLVM source revision: `YoWASP/llvm-project@97196c8eeb1d495fa43bb8af2fb26af5ef5b89fb`
215
+ - WASI libc source revision: `WebAssembly/wasi-libc@ac020b86fd44bafe60aa4fa12f407d16e3731329`
216
+ - Sources: <https://github.com/YoWASP/clang/tree/944dd7c774954180e621cc8e12984023a7f8bcbe>
217
+ and <https://github.com/YoWASP/llvm-project/tree/97196c8eeb1d495fa43bb8af2fb26af5ef5b89fb>
218
+ - LLVM license: Apache-2.0 WITH LLVM-exception
219
+ - WASI libc license: Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT,
220
+ with separately licensed portions identified by the upstream notice
221
+ - License and attribution texts: `licenses/LLVM-exception.txt`,
222
+ `licenses/Apache-2.0.txt`, `licenses/wasi-libc-ac020b86-LICENSE.txt`,
223
+ `licenses/wasi-libc-ac020b86-musl-fts-BSD-3-Clause.txt`,
224
+ `licenses/wasi-sdk-24.0-Apache-2.0-LLVM-exception.txt`,
225
+ `licenses/wasi-libc-b9ef79d-Apache-2.0.txt`,
226
+ `licenses/wasi-libc-b9ef79d-MIT.txt`,
227
+ `licenses/wasi-libc-b9ef79d-cloudlibc-BSD-2-Clause.txt`,
228
+ `licenses/wasi-libc-b9ef79d-dlmalloc-CC0-NOTICE.txt`,
229
+ `licenses/wasi-libc-b9ef79d-emmalloc-NOTICE.txt`, and
230
+ `licenses/wasi-libc-b9ef79d-musl-MIT.txt`
231
+
232
+ The published npm manifest incorrectly says `ISC` and omits a license file.
233
+ The exact source revision's root `LICENSE.txt` and npm README both explicitly
234
+ license the package under Apache-2.0. WASM-OJ does not redistribute the npm
235
+ JavaScript wrapper: the packaging script digest-verifies and extracts only the
236
+ LLVM core and LLVM/WASI resource payload, whose complete upstream license terms
237
+ are carried above. The common Apache, MIT, cloudlibc, dlmalloc, emmalloc, and
238
+ musl terms are byte-identical between the pinned `ac020b86` and `b9ef79d7`
239
+ source trees, so the inventory reuses the exact digest-bound `b9ef79d7` copies;
240
+ the revision-specific root and musl-fts notices remain separate. The generated
241
+ WebC and pinned command manifests do not relicense those payloads.
242
+
243
+ ### CPython 3.14.6 for WASI P1
244
+
245
+ - Distributed assets: `python-3.14.6-wasip1.webc.gz.bin` and its provenance
246
+ manifest `python-3.14.6-wasip1.manifest.json`
247
+ - Official source archive: <https://www.python.org/ftp/python/3.14.6/Python-3.14.6.tar.xz>,
248
+ SHA-256 `143b1dddefaec3bd2e21e3b839b34a2b7fb9842272883c576420d605e9f30c63`
249
+ - Official SPDX document: <https://www.python.org/ftp/python/3.14.6/Python-3.14.6.tar.xz.spdx.json>,
250
+ SHA-256 `1f5d394856783fa77e1f1db280f84eabf693bffc1fb06a747f7116de9f99f3bd`
251
+ - Expanded WebC SHA-256:
252
+ `454ffc53936aa13a0d7f4afbb5bd50ada339c8ebd04bbf27ea19e4104cf43207`
253
+ - Distributed gzip SHA-256:
254
+ `218cd20ac4abb443e0700816010a615a345a43eae623a0232da2227135a6c7a6`
255
+ - Provenance manifest SHA-256:
256
+ `054eccad04a7cee7ba1661062142ef0d639976850981eab8fc785f48eb26129e`
257
+ - Deterministically exported `WOJFS002` runtime archive SHA-256:
258
+ `44d894f91487f20c2bb04fe496a9343db37d8720fb706472c2b4a7f3300db039`
259
+ - CPython license: Python Software Foundation License Version 2 and the
260
+ historical notices reproduced with it
261
+ - License text: `licenses/cpython-3.14.6-PSF-2.0.txt`
262
+ - Bundled third-party source: Expat 2.8.1 (MIT), HACL* revision
263
+ `8ba599b2f6c9701b3dc961db895b0856a2210f76` (MIT), and libmpdec 2.5.1
264
+ (BSD-2-Clause); their exact notices are
265
+ `licenses/cpython-expat-2.8.1-MIT.txt`,
266
+ `licenses/cpython-hacl-star-8ba599b-MIT.txt`, and
267
+ `licenses/cpython-libmpdec-2.5.1-BSD-2-Clause.txt`.
268
+ - Build toolchain: WASI SDK 24.0 revision
269
+ `d2bea01edcc46f731156a817f710cdd9fc9c1c19`, LLVM revision
270
+ `26a1d6601d727a96f4301d0d8647b5a42760ae0c`, and wasi-libc revision
271
+ `b9ef79d7dbd47c6c5bafdae760823467c2f60b70`.
272
+
273
+ WASM-OJ compiles this package from the pinned official sources, disables
274
+ `_socket`, removes test/development-only standard-library roots, canonicalizes
275
+ build-only sysconfig paths, and packages the complete source SPDX document and
276
+ applicable CPython, third-party, compiler-rt, WASI SDK, and wasi-libc notices
277
+ inside the WebC. The build does not consume or redistribute the retired Wasmer
278
+ Registry CPython/WASIX package, and WASM-OJ's MIT license does not apply to these
279
+ third-party contents.
280
+
281
+ ## WASM-OJ runtime-core dependency closure
282
+
283
+ The browser runner embeds `wasm-oj-runtime-core` and its locked normal
284
+ dependency graph for the `wasm32-unknown-unknown` target with the `web` feature.
285
+ The complete generated license report, including the selected license text for
286
+ every dependency, is `licenses/runtime-core-dependencies.html`. Its compact,
287
+ machine-verifiable package inventory and report digest are recorded in
288
+ `licenses/runtime-core-dependencies.json`. Both files are generated with pinned
289
+ `cargo-about` 0.9.1 and are verified against `crates/runtime-core/Cargo.lock`.
290
+
291
+ ## Vendored Rust crates
292
+
293
+ WASM-OJ's source tree also contains patched source copies used to build the
294
+ runtime and packaging tools:
295
+
296
+ - `vendor/shared-buffer`: MIT OR Apache-2.0; see
297
+ `vendor/shared-buffer/LICENSE_MIT.md` and
298
+ `vendor/shared-buffer/LICENSE_APACHE.md`.
299
+ - `vendor/virtual-fs`: MIT; see `vendor/virtual-fs/LICENSE`.
300
+
301
+ These vendored sources are build inputs and are not included in the npm package
302
+ allowlist.
@@ -0,0 +1,5 @@
1
+ export { BROWSER_COLLECTION_SCHEMA, BROWSER_PROBLEM_SCHEMA, MANAGED_COLLECTION_SCHEMA, parseManagedCollectionContract, parseManagedCollectionSource, parseManagedCollectionV2, parseManagedCollectionValueV2, parseProblemCollectionIndex, parseStandaloneProblemBundle, problemCollectionRevision, verifyProblemBundleBytes, verifyProblemCollectionRevision } from '@wasm-oj/core';
2
+
3
+ declare function runCollectionCli(arguments_: readonly string[]): Promise<void>;
4
+
5
+ export { runCollectionCli };
package/dist/index.js ADDED
@@ -0,0 +1,546 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, mkdir, open, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import os from "node:os";
6
+ import { BROWSER_COLLECTION_SCHEMA, BROWSER_COLLECTION_SCHEMA as BROWSER_COLLECTION_SCHEMA$1, BROWSER_PROBLEM_SCHEMA, BROWSER_PROBLEM_SCHEMA as BROWSER_PROBLEM_SCHEMA$1, MANAGED_COLLECTION_SCHEMA, MANAGED_COLLECTION_SCHEMA as MANAGED_COLLECTION_SCHEMA$1, assertJudgeDataMatchesPracticePublic, canonicalJsonBytes, contestPublicProjectionBytes, deriveJudgeData, derivePracticePublic, encodeJudgePackage, parseManagedCollectionContract, parseManagedCollectionSource, parseManagedCollectionSource as parseManagedCollectionSource$1, parseManagedCollectionV2, parseManagedCollectionV2 as parseManagedCollectionV2$1, parseManagedCollectionValueV2, parseProblemCollectionIndex, parseProblemCollectionIndex as parseProblemCollectionIndex$1, parseStandaloneProblemBundle, parseStandaloneProblemBundle as parseStandaloneProblemBundle$1, problemCollectionRevision, problemCollectionRevision as problemCollectionRevision$1, validateJudgePackage, verifyProblemBundleBytes, verifyProblemBundleBytes as verifyProblemBundleBytes$1, verifyProblemCollectionRevision, verifyProblemCollectionRevision as verifyProblemCollectionRevision$1 } from "@wasm-oj/core";
7
+ //#region src/path-safety.ts
8
+ /** Resolve only the operating system's own temporary-directory alias. */
9
+ async function canonicalizeSystemTemporaryPrefix(value) {
10
+ const absolute = path.resolve(value);
11
+ const temporary = path.resolve(os.tmpdir());
12
+ if (absolute !== temporary && !absolute.startsWith(`${temporary}${path.sep}`)) return absolute;
13
+ const canonicalTemporary = await realpath(temporary);
14
+ return path.join(canonicalTemporary, path.relative(temporary, absolute));
15
+ }
16
+ /** True only when the absolute path's existing components do not traverse links. */
17
+ async function anchoredPathHasNoSymlink(value) {
18
+ const anchored = await canonicalizeSystemTemporaryPrefix(path.resolve(value));
19
+ const root = path.parse(anchored).root;
20
+ let current = root;
21
+ for (const segment of path.relative(root, anchored).split(path.sep).filter(Boolean)) {
22
+ current = path.join(current, segment);
23
+ try {
24
+ if ((await lstat(current)).isSymbolicLink()) return false;
25
+ } catch (error) {
26
+ if (error.code === "ENOENT") return true;
27
+ throw error;
28
+ }
29
+ }
30
+ return true;
31
+ }
32
+ //#endregion
33
+ //#region src/collection-cli.ts
34
+ var SOURCE_SCHEMA = "wasm-oj-browser-collection-source-v1";
35
+ var DEFAULT_INDEX_PATH = "collection/index.json";
36
+ var DEFAULT_SOURCE_PATH = "collection/source.json";
37
+ var INDEX_MAX_BYTES = 524288;
38
+ var COLLECTION_SOURCE_MAX_BYTES = 2097152;
39
+ var STATEMENT_MAX_BYTES = 2097152;
40
+ var PUBLIC_BUNDLE_MAX_BYTES = 8388608;
41
+ var AUTHORING_BUNDLE_MAX_BYTES = 33554432;
42
+ var MANAGED_MAX_BYTES = 2097152;
43
+ var JUDGE_PACKAGE_MAX_BYTES = 33554432;
44
+ function fail(message) {
45
+ throw new Error(message);
46
+ }
47
+ function normalizedRelativePath(value, label) {
48
+ if (typeof value !== "string" || !value || value.startsWith("/") || value.includes("\\") || value.includes("\0") || value.endsWith("/")) return fail(`${label} must be a normalized relative POSIX path.`);
49
+ if (value.split("/").some((segment) => !segment || segment === "." || segment === "..")) return fail(`${label} must be a normalized relative POSIX path.`);
50
+ return value;
51
+ }
52
+ function portableOutputPath(value, label) {
53
+ const relative = normalizedRelativePath(value, label);
54
+ if (relative.split("/").some((segment) => !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(segment))) return fail(`${label} must use only portable ASCII path segments.`);
55
+ return relative;
56
+ }
57
+ function resolveInside(root, relativeValue, label) {
58
+ const relative = normalizedRelativePath(relativeValue, label);
59
+ return path.join(root, ...relative.split("/"));
60
+ }
61
+ function collisionKey(value) {
62
+ return value.normalize("NFC").toLocaleLowerCase("en-US");
63
+ }
64
+ async function assertRepositoryRoot(root) {
65
+ const anchoredRoot = await canonicalizeSystemTemporaryPrefix(path.resolve(root));
66
+ if (!await anchoredPathHasNoSymlink(anchoredRoot)) fail("repository root must not traverse a symbolic link.");
67
+ const metadata = await lstat(anchoredRoot);
68
+ if (metadata.isSymbolicLink() || !metadata.isDirectory()) fail("repository root must be a real directory.");
69
+ }
70
+ async function readBoundedRepositoryFile(options, relativeValue, label, maximumBytes, expectedBytes) {
71
+ const file = resolveInside(options.root, relativeValue, label);
72
+ if (!await anchoredPathHasNoSymlink(file)) fail(`${label} must not traverse a symbolic link.`);
73
+ let handle;
74
+ try {
75
+ handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
76
+ } catch (error) {
77
+ throw new Error(`${label} must be a readable regular file.`, { cause: error });
78
+ }
79
+ try {
80
+ const metadata = await handle.stat();
81
+ if (!metadata.isFile() || metadata.size > maximumBytes || expectedBytes !== void 0 && metadata.size !== expectedBytes) fail(`${label} is outside its allowed byte limit.`);
82
+ return new Uint8Array(await handle.readFile());
83
+ } finally {
84
+ await handle.close();
85
+ }
86
+ }
87
+ async function assertOutputPathSafe(options, relativeValue, label) {
88
+ const output = resolveInside(options.root, relativeValue, label);
89
+ if (!await anchoredPathHasNoSymlink(output)) fail(`${label} must not traverse a symbolic link.`);
90
+ let metadata;
91
+ try {
92
+ metadata = await lstat(output);
93
+ } catch (error) {
94
+ if (error.code === "ENOENT") return;
95
+ throw error;
96
+ }
97
+ if (metadata.isSymbolicLink() || !metadata.isFile()) fail(`${label} must replace only a regular file.`);
98
+ }
99
+ async function existingPathIdentity(options, relativeValue, label) {
100
+ const file = resolveInside(options.root, relativeValue, label);
101
+ if (!await anchoredPathHasNoSymlink(file)) fail(`${label} must not traverse a symbolic link.`);
102
+ try {
103
+ const metadata = await lstat(file, { bigint: true });
104
+ if (metadata.isSymbolicLink()) fail(`${label} must not be a symbolic link.`);
105
+ return `${metadata.dev}:${metadata.ino}`;
106
+ } catch (error) {
107
+ if (error.code === "ENOENT") return void 0;
108
+ throw error;
109
+ }
110
+ }
111
+ async function assertNoFilesystemAliases(options, inputs, outputs) {
112
+ const inputIdentities = /* @__PURE__ */ new Map();
113
+ for (const input of inputs) {
114
+ const identity = await existingPathIdentity(options, input, `declared input '${input}'`);
115
+ if (identity !== void 0 && !inputIdentities.has(identity)) inputIdentities.set(identity, input);
116
+ }
117
+ const outputIdentities = /* @__PURE__ */ new Map();
118
+ for (const output of outputs) {
119
+ const identity = await existingPathIdentity(options, output, `generated output '${output}'`);
120
+ if (identity === void 0) continue;
121
+ const input = inputIdentities.get(identity);
122
+ if (input !== void 0) fail(`generated output '${output}' aliases declared input '${input}' on this filesystem.`);
123
+ const priorOutput = outputIdentities.get(identity);
124
+ if (priorOutput !== void 0) fail(`generated outputs '${priorOutput}' and '${output}' alias on this filesystem.`);
125
+ outputIdentities.set(identity, output);
126
+ }
127
+ }
128
+ async function ensureOutputParent(options, relativeValue) {
129
+ const relativeParent = path.posix.dirname(normalizedRelativePath(relativeValue, "output path"));
130
+ if (relativeParent === ".") return;
131
+ let current = options.root;
132
+ for (const segment of relativeParent.split("/")) {
133
+ current = path.join(current, segment);
134
+ try {
135
+ const metadata = await lstat(current);
136
+ if (metadata.isSymbolicLink() || !metadata.isDirectory()) fail(`output directory '${relativeParent}' must contain only real directories.`);
137
+ } catch (error) {
138
+ if (error.code !== "ENOENT") throw error;
139
+ try {
140
+ await mkdir(current);
141
+ } catch (mkdirError) {
142
+ if (mkdirError.code !== "EEXIST") throw mkdirError;
143
+ const metadata = await lstat(current);
144
+ if (metadata.isSymbolicLink() || !metadata.isDirectory()) fail(`output directory '${relativeParent}' must contain only real directories.`);
145
+ }
146
+ }
147
+ }
148
+ }
149
+ async function atomicRepositoryWrite(options, relativeValue, bytes) {
150
+ const output = resolveInside(options.root, relativeValue, "output path");
151
+ await ensureOutputParent(options, relativeValue);
152
+ const temporary = `${output}.${process.pid}.${crypto.randomUUID()}.tmp`;
153
+ try {
154
+ await writeFile(temporary, bytes, {
155
+ flag: "wx",
156
+ mode: 420
157
+ });
158
+ await rename(temporary, output);
159
+ } finally {
160
+ await rm(temporary, { force: true });
161
+ }
162
+ }
163
+ function parseOptions(arguments_) {
164
+ const [commandValue, ...rest] = arguments_;
165
+ if (commandValue !== "build" && commandValue !== "verify") return fail("Usage: woj organizer collection <build|verify> [repository-root] [--index path] [--source path] [--managed path] [--managed-source path]");
166
+ let root = ".";
167
+ let indexPath = DEFAULT_INDEX_PATH;
168
+ let sourcePath = DEFAULT_SOURCE_PATH;
169
+ let managedPath;
170
+ let managedSourcePath;
171
+ let sawRoot = false;
172
+ for (let index = 0; index < rest.length; index += 1) {
173
+ const argument = rest[index];
174
+ if (argument === "--index" || argument === "--source" || argument === "--managed" || argument === "--managed-source") {
175
+ const value = rest[index + 1];
176
+ if (!value) return fail(`${argument} requires a path.`);
177
+ if (argument === "--index") indexPath = portableOutputPath(value, "index path");
178
+ else if (argument === "--source") sourcePath = normalizedRelativePath(value, "source path");
179
+ else if (argument === "--managed") managedPath = portableOutputPath(value, "managed contract path");
180
+ else managedSourcePath = normalizedRelativePath(value, "managed source path");
181
+ index += 1;
182
+ continue;
183
+ }
184
+ if (argument?.startsWith("-")) return fail(`Unknown option '${argument}'.`);
185
+ if (sawRoot || !argument) return fail("Only one repository root may be provided.");
186
+ root = argument;
187
+ sawRoot = true;
188
+ }
189
+ if (commandValue !== "build" && managedSourcePath) return fail("--managed-source is only valid for build.");
190
+ if (commandValue === "build" && managedPath && !managedSourcePath) return fail("--managed requires --managed-source when building.");
191
+ if (commandValue === "build" && collisionKey(indexPath) === collisionKey(sourcePath)) return fail("--index and --source must not be the same input and output path.");
192
+ if (commandValue === "build" && managedPath !== void 0 && managedSourcePath !== void 0 && collisionKey(managedPath) === collisionKey(managedSourcePath)) return fail("--managed and --managed-source must not be the same input and output path.");
193
+ return {
194
+ command: commandValue,
195
+ root: path.resolve(root),
196
+ indexPath,
197
+ sourcePath,
198
+ ...managedPath ? { managedPath } : {},
199
+ ...managedSourcePath ? { managedSourcePath } : {}
200
+ };
201
+ }
202
+ function isRecord(value) {
203
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
204
+ }
205
+ function exactKeys(value, expected, label) {
206
+ const actual = Object.keys(value).sort();
207
+ const sortedExpected = [...expected].sort();
208
+ if (JSON.stringify(actual) !== JSON.stringify(sortedExpected)) fail(`${label} must contain exactly: ${sortedExpected.join(", ")}.`);
209
+ }
210
+ function parseAuthoredCollection(value) {
211
+ if (!isRecord(value)) return fail("collection/source.json must be an object.");
212
+ exactKeys(value, [
213
+ "schema",
214
+ "localization",
215
+ "problems"
216
+ ], "collection source");
217
+ if (value.schema !== SOURCE_SCHEMA) return fail(`collection source schema must be '${SOURCE_SCHEMA}'.`);
218
+ if (!isRecord(value.localization)) return fail("collection localization must be an object.");
219
+ exactKeys(value.localization, ["defaultLocale", "supportedLocales"], "collection localization");
220
+ if (value.localization.defaultLocale !== "zh-TW" || JSON.stringify(value.localization.supportedLocales) !== JSON.stringify(["zh-TW", "en"])) return fail("collection localization must declare zh-TW followed by en.");
221
+ if (!Array.isArray(value.problems) || value.problems.length < 1 || value.problems.length > 1e3) return fail("collection source must contain between 1 and 1000 problems.");
222
+ return {
223
+ schema: SOURCE_SCHEMA,
224
+ localization: {
225
+ defaultLocale: "zh-TW",
226
+ supportedLocales: ["zh-TW", "en"]
227
+ },
228
+ problems: value.problems.map((problemValue, index) => {
229
+ if (!isRecord(problemValue)) return fail(`collection source problem ${index + 1} must be an object.`);
230
+ exactKeys(problemValue, ["statementPaths", "bundlePath"], `collection source problem ${index + 1}`);
231
+ if (!isRecord(problemValue.statementPaths)) return fail(`problem ${index + 1} statementPaths must be an object.`);
232
+ exactKeys(problemValue.statementPaths, ["zh-TW", "en"], `problem ${index + 1} statementPaths`);
233
+ const statementPaths = {
234
+ "zh-TW": normalizedRelativePath(problemValue.statementPaths["zh-TW"], `problem ${index + 1} zh-TW statement`),
235
+ en: normalizedRelativePath(problemValue.statementPaths.en, `problem ${index + 1} English statement`)
236
+ };
237
+ if (!statementPaths["zh-TW"].endsWith(".md") || !statementPaths.en.endsWith(".md")) return fail(`problem ${index + 1} statements must be Markdown files.`);
238
+ return {
239
+ statementPaths,
240
+ bundlePath: normalizedRelativePath(problemValue.bundlePath, `problem ${index + 1} bundle`)
241
+ };
242
+ })
243
+ };
244
+ }
245
+ function parseJson(bytes, label) {
246
+ let text;
247
+ try {
248
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
249
+ } catch (error) {
250
+ throw new Error(`${label} is not valid UTF-8.`, { cause: error });
251
+ }
252
+ try {
253
+ return JSON.parse(text);
254
+ } catch (error) {
255
+ throw new Error(`${label} is not valid JSON.`, { cause: error });
256
+ }
257
+ }
258
+ async function sha256Hex(bytes) {
259
+ const digest = await crypto.subtle.digest("SHA-256", bytes.slice().buffer);
260
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
261
+ }
262
+ async function validatePublishedCollection(options, strict) {
263
+ const indexBytes = await readBoundedRepositoryFile(options, options.indexPath, "index path", INDEX_MAX_BYTES);
264
+ const index = parseProblemCollectionIndex$1(parseJson(indexBytes, options.indexPath));
265
+ await verifyProblemCollectionRevision$1(index);
266
+ if (strict && !Buffer.from(indexBytes).equals(Buffer.from(canonicalJsonBytes(index)))) fail(`${options.indexPath} is not canonical; run woj organizer collection build.`);
267
+ const indexDirectory = path.posix.dirname(options.indexPath);
268
+ for (const entry of index.problems) {
269
+ for (const [locale, statementPath] of Object.entries(entry.statementPaths)) if ((await readBoundedRepositoryFile(options, statementPath, `problem '${entry.id}' ${locale} statement`, STATEMENT_MAX_BYTES)).byteLength < 1) fail(`problem '${entry.id}' ${locale} statement must contain between 1 byte and 2 MiB.`);
270
+ const repositoryPath = path.posix.join(indexDirectory, entry.bundle.path);
271
+ const bytes = await readBoundedRepositoryFile(options, repositoryPath, `problem '${entry.id}' bundle`, PUBLIC_BUNDLE_MAX_BYTES, entry.bundle.bytes);
272
+ const problem = await verifyProblemBundleBytes$1(bytes, entry);
273
+ if (strict && !Buffer.from(bytes).equals(Buffer.from(canonicalJsonBytes({
274
+ schema: BROWSER_PROBLEM_SCHEMA$1,
275
+ problem
276
+ })))) fail(`${repositoryPath} is not canonical; run woj organizer collection build.`);
277
+ }
278
+ if (strict) await rejectUndeclaredContentAddressedBundles(options, index);
279
+ if (options.managedPath) await validateManagedContract(options, index);
280
+ return index;
281
+ }
282
+ async function validateManagedContract(options, index) {
283
+ const managedPath = options.managedPath;
284
+ if (!managedPath) fail("managed contract path is required.");
285
+ const contractBytes = await readBoundedRepositoryFile(options, managedPath, "managed contract path", MANAGED_MAX_BYTES);
286
+ const contract = parseManagedCollectionV2$1(contractBytes);
287
+ if (contract.collectionRevision !== index.revision) fail("managed collection revision does not match collection/index.json.");
288
+ if (JSON.stringify(contract.problems.map((problem) => problem.slug)) !== JSON.stringify(index.problems.map((problem) => problem.id))) fail("managed collection problems must exactly match index order.");
289
+ const indexDirectory = path.posix.dirname(options.indexPath);
290
+ for (const [position, publication] of contract.problems.entries()) {
291
+ const entry = index.problems[position];
292
+ const practiceBytes = await readBoundedRepositoryFile(options, path.posix.join(indexDirectory, entry.bundle.path), `problem '${entry.id}' bundle`, PUBLIC_BUNDLE_MAX_BYTES, entry.bundle.bytes);
293
+ const practice = await verifyProblemBundleBytes$1(practiceBytes, entry);
294
+ const contestBytes = await readPublishedObject(options, indexDirectory, publication.contestPublic, `contest-public '${publication.slug}'`, PUBLIC_BUNDLE_MAX_BYTES);
295
+ const expectedContestBytes = contestPublicProjectionBytes(practice, entry.bundle.sha256);
296
+ if (!Buffer.from(contestBytes).equals(Buffer.from(expectedContestBytes))) fail(`contest-public '${publication.slug}' is not the deterministic projection of its practice bundle.`);
297
+ const packageBytes = await readPublishedObject(options, indexDirectory, publication.judgePackage, `judge package '${publication.slug}'`, JUDGE_PACKAGE_MAX_BYTES);
298
+ const validatedPackage = await validateJudgePackage(packageBytes, {
299
+ expectedBytes: publication.judgePackage.bytes,
300
+ expectedSha256: publication.judgePackage.sha256,
301
+ memoryLimitBytes: Math.max(...practice.scoring.policies.map((policy) => policy.limits.memoryLimitBytes))
302
+ });
303
+ if (JSON.stringify(validatedPackage.manifest.allowedProfiles) !== JSON.stringify(publication.allowedProfiles)) fail(`judge package '${publication.slug}' allowedProfiles disagree with collection/managed.json.`);
304
+ assertJudgeDataMatchesPracticePublic(validatedPackage.judgeData, practice, Object.keys(publication.allowedProfiles));
305
+ }
306
+ }
307
+ async function readPublishedObject(options, indexDirectory, object, label, maximumBytes) {
308
+ const bytes = await readBoundedRepositoryFile(options, path.posix.join(indexDirectory, object.repositoryPath), label, maximumBytes, object.bytes);
309
+ if (await sha256Hex(bytes) !== object.sha256) fail(`${label} failed integrity verification.`);
310
+ return bytes;
311
+ }
312
+ async function rejectUndeclaredContentAddressedBundles(options, index) {
313
+ const indexDirectory = path.posix.dirname(options.indexPath);
314
+ const relativeDirectory = path.posix.join(indexDirectory, "problems");
315
+ const directory = resolveInside(options.root, relativeDirectory, "bundle directory");
316
+ if (!await anchoredPathHasNoSymlink(directory)) fail("bundle directory must not traverse a symbolic link.");
317
+ const metadata = await lstat(directory);
318
+ if (metadata.isSymbolicLink() || !metadata.isDirectory()) fail("bundle directory must be a real directory.");
319
+ const declared = new Set(index.problems.map((entry) => path.posix.basename(entry.bundle.path)));
320
+ const entries = await readdir(directory, { withFileTypes: true });
321
+ const digestPattern = /\.[0-9a-f]{64}\.json$/;
322
+ const undeclared = entries.filter((entry) => entry.isFile() && digestPattern.test(entry.name) && !declared.has(entry.name)).map((entry) => entry.name).sort();
323
+ const unsafe = entries.filter((entry) => entry.isSymbolicLink() || !entry.isFile() && !entry.isDirectory()).map((entry) => entry.name).sort();
324
+ if (unsafe.length > 0) fail(`bundle directory contains unsafe entries: ${unsafe.join(", ")}.`);
325
+ if (undeclared.length > 0) fail(`undeclared content-addressed bundles: ${undeclared.join(", ")}.`);
326
+ }
327
+ function assertGeneratedSize(bytes, maximumBytes, label) {
328
+ if (bytes.byteLength > maximumBytes) fail(`${label} exceeds its allowed byte limit.`);
329
+ }
330
+ async function prepareCollection(options) {
331
+ const source = parseAuthoredCollection(parseJson(await readBoundedRepositoryFile(options, options.sourcePath, "source path", COLLECTION_SOURCE_MAX_BYTES), options.sourcePath));
332
+ const indexDirectory = path.posix.dirname(options.indexPath);
333
+ const entries = [];
334
+ const authoredProblems = [];
335
+ const practiceProblems = [];
336
+ const inputs = /* @__PURE__ */ new Set([options.sourcePath]);
337
+ const outputs = [];
338
+ for (const [position, authored] of source.problems.entries()) {
339
+ inputs.add(authored.bundlePath);
340
+ const authoredBytes = await readBoundedRepositoryFile(options, authored.bundlePath, `problem ${position + 1} source bundle`, AUTHORING_BUNDLE_MAX_BYTES);
341
+ const problem = parseStandaloneProblemBundle$1(parseJson(authoredBytes, authored.bundlePath));
342
+ if (problem.number !== position + 1) fail(`problem '${problem.id}' must have number ${position + 1}.`);
343
+ for (const [locale, statementPath] of Object.entries(authored.statementPaths)) {
344
+ inputs.add(statementPath);
345
+ if ((await readBoundedRepositoryFile(options, statementPath, `problem '${problem.id}' ${locale} statement`, STATEMENT_MAX_BYTES)).byteLength < 1) fail(`problem '${problem.id}' ${locale} statement must contain between 1 byte and 2 MiB.`);
346
+ }
347
+ authoredProblems.push(problem);
348
+ const practice = derivePracticePublic(problem);
349
+ practiceProblems.push(practice);
350
+ const bundleBytes = canonicalJsonBytes({
351
+ schema: BROWSER_PROBLEM_SCHEMA$1,
352
+ problem: practice
353
+ });
354
+ assertGeneratedSize(bundleBytes, PUBLIC_BUNDLE_MAX_BYTES, `problem '${problem.id}' public bundle`);
355
+ const digest = await sha256Hex(bundleBytes);
356
+ const bundleName = `${String(problem.number).padStart(3, "0")}-${problem.id}.${digest}.json`;
357
+ outputs.push({
358
+ path: path.posix.join(indexDirectory, "problems", bundleName),
359
+ label: `problem '${problem.id}' public bundle output`,
360
+ bytes: bundleBytes
361
+ });
362
+ entries.push({
363
+ id: problem.id,
364
+ number: problem.number,
365
+ title: problem.title,
366
+ trackId: problem.trackId,
367
+ track: problem.track,
368
+ statementPaths: authored.statementPaths,
369
+ difficulty: problem.difficulty,
370
+ tags: problem.tags,
371
+ caseCount: practice.judgeCases.length,
372
+ bundle: {
373
+ path: `problems/${bundleName}`,
374
+ sha256: digest,
375
+ bytes: bundleBytes.byteLength
376
+ }
377
+ });
378
+ }
379
+ const withoutRevision = {
380
+ schema: BROWSER_COLLECTION_SCHEMA$1,
381
+ problemSchema: BROWSER_PROBLEM_SCHEMA$1,
382
+ localization: source.localization,
383
+ problems: entries
384
+ };
385
+ const index = parseProblemCollectionIndex$1({
386
+ ...withoutRevision,
387
+ revision: await problemCollectionRevision$1(withoutRevision)
388
+ });
389
+ const indexBytes = canonicalJsonBytes(index);
390
+ assertGeneratedSize(indexBytes, INDEX_MAX_BYTES, "collection index output");
391
+ outputs.push({
392
+ path: options.indexPath,
393
+ label: "collection index output",
394
+ bytes: indexBytes
395
+ });
396
+ return {
397
+ index,
398
+ authoredProblems,
399
+ practiceProblems,
400
+ inputs,
401
+ outputs
402
+ };
403
+ }
404
+ async function readDeclaredManagedSourceObject(options, object, label, inputs) {
405
+ inputs.add(object.path);
406
+ const bytes = await readBoundedRepositoryFile(options, object.path, label, JUDGE_PACKAGE_MAX_BYTES, object.bytes);
407
+ if (await sha256Hex(bytes) !== object.sha256) fail(`${label} failed declared size or digest verification.`);
408
+ return bytes;
409
+ }
410
+ async function managedJudgeInput(options, problem, inputs) {
411
+ if (problem.judge.kind === "text") return { kind: "text" };
412
+ const artifact = await readDeclaredManagedSourceObject(options, problem.judge.artifact, `${problem.judge.kind} artifact '${problem.slug}'`, inputs);
413
+ const assets = [];
414
+ for (const asset of problem.judge.assets) assets.push({
415
+ guestPath: asset.guestPath,
416
+ contents: await readDeclaredManagedSourceObject(options, asset, `${problem.judge.kind} asset '${problem.slug}/${asset.path}'`, inputs)
417
+ });
418
+ return problem.judge.kind === "checker" ? {
419
+ kind: "checker",
420
+ runtimeProfile: problem.judge.artifact.runtimeProfile,
421
+ artifact,
422
+ assets,
423
+ args: problem.judge.args
424
+ } : {
425
+ kind: "interactive",
426
+ runtimeProfile: problem.judge.artifact.runtimeProfile,
427
+ artifact,
428
+ assets,
429
+ args: problem.judge.args,
430
+ inputPath: problem.judge.inputPath
431
+ };
432
+ }
433
+ async function prepareManagedCollection(options, built) {
434
+ const managedSourcePath = options.managedSourcePath;
435
+ if (!managedSourcePath) fail("managed source path is required.");
436
+ const inputs = /* @__PURE__ */ new Set([managedSourcePath]);
437
+ const outputs = [];
438
+ const sourceBytes = await readBoundedRepositoryFile(options, managedSourcePath, "managed source path", MANAGED_MAX_BYTES);
439
+ const source = parseManagedCollectionSource$1(parseJson(sourceBytes, managedSourcePath));
440
+ if (JSON.stringify(source.problems.map((problem) => problem.slug)) !== JSON.stringify(built.index.problems.map((problem) => problem.id))) fail("managed source problems must exactly match collection/index.json order.");
441
+ const indexDirectory = path.posix.dirname(options.indexPath);
442
+ const publicationDirectory = path.posix.join(indexDirectory, "managed");
443
+ const publications = [];
444
+ for (const [position, sourceProblem] of source.problems.entries()) {
445
+ const entry = built.index.problems[position];
446
+ const practice = built.practiceProblems[position];
447
+ const authored = built.authoredProblems[position];
448
+ if (!authored || authored.id !== entry.id) fail(`authoring source for '${entry.id}' is unavailable.`);
449
+ const contestBytes = contestPublicProjectionBytes(practice, entry.bundle.sha256);
450
+ assertGeneratedSize(contestBytes, PUBLIC_BUNDLE_MAX_BYTES, `contest-public '${entry.id}' output`);
451
+ const contestSha256 = await sha256Hex(contestBytes);
452
+ const contestName = `${String(entry.number).padStart(3, "0")}-${entry.id}.${contestSha256}.contest.json`;
453
+ outputs.push({
454
+ path: path.posix.join(publicationDirectory, contestName),
455
+ label: `contest-public '${entry.id}' output`,
456
+ bytes: contestBytes
457
+ });
458
+ const encoded = await encodeJudgePackage({
459
+ judgeData: deriveJudgeData(authored, Object.keys(sourceProblem.allowedProfiles)),
460
+ allowedProfiles: sourceProblem.allowedProfiles,
461
+ judge: await managedJudgeInput(options, sourceProblem, inputs)
462
+ });
463
+ assertGeneratedSize(encoded.bytes, JUDGE_PACKAGE_MAX_BYTES, `judge package '${entry.id}' output`);
464
+ const validated = await validateJudgePackage(encoded.bytes, {
465
+ expectedBytes: encoded.bytes.byteLength,
466
+ expectedSha256: encoded.executionSemanticSha256,
467
+ memoryLimitBytes: Math.max(...practice.scoring.policies.map((policy) => policy.limits.memoryLimitBytes))
468
+ });
469
+ assertJudgeDataMatchesPracticePublic(validated.judgeData, practice, Object.keys(sourceProblem.allowedProfiles));
470
+ const packageName = `${String(entry.number).padStart(3, "0")}-${entry.id}.${encoded.executionSemanticSha256}.wasmojjudge`;
471
+ outputs.push({
472
+ path: path.posix.join(publicationDirectory, packageName),
473
+ label: `judge package '${entry.id}' output`,
474
+ bytes: encoded.bytes
475
+ });
476
+ publications.push({
477
+ slug: entry.id,
478
+ allowedProfiles: sourceProblem.allowedProfiles,
479
+ contestPublic: {
480
+ repositoryPath: `managed/${contestName}`,
481
+ bytes: contestBytes.byteLength,
482
+ sha256: contestSha256
483
+ },
484
+ judgePackage: {
485
+ repositoryPath: `managed/${packageName}`,
486
+ bytes: encoded.bytes.byteLength,
487
+ sha256: encoded.executionSemanticSha256
488
+ }
489
+ });
490
+ }
491
+ const contract = parseManagedCollectionV2$1(canonicalJsonBytes({
492
+ schema: MANAGED_COLLECTION_SCHEMA$1,
493
+ collectionRevision: built.index.revision,
494
+ problems: publications
495
+ }));
496
+ const managedPath = options.managedPath ?? path.posix.join(indexDirectory, "managed.json");
497
+ const contractBytes = canonicalJsonBytes(contract);
498
+ assertGeneratedSize(contractBytes, MANAGED_MAX_BYTES, "managed contract output");
499
+ outputs.push({
500
+ path: managedPath,
501
+ label: "managed contract output",
502
+ bytes: contractBytes
503
+ });
504
+ return {
505
+ inputs,
506
+ outputs
507
+ };
508
+ }
509
+ function pathsOverlap(left, right) {
510
+ const leftKey = collisionKey(left);
511
+ const rightKey = collisionKey(right);
512
+ return leftKey === rightKey || leftKey.startsWith(`${rightKey}/`) || rightKey.startsWith(`${leftKey}/`);
513
+ }
514
+ async function writePreparedCollection(options, inputs, outputs) {
515
+ const outputPaths = /* @__PURE__ */ new Set();
516
+ for (const output of outputs) {
517
+ portableOutputPath(output.path, output.label);
518
+ const key = collisionKey(output.path);
519
+ if (outputPaths.has(key)) fail(`generated output '${output.path}' has a case-folded or normalized collision.`);
520
+ outputPaths.add(key);
521
+ for (const input of inputs) if (pathsOverlap(input, output.path)) fail(`generated output '${output.path}' overlaps declared input '${input}'.`);
522
+ }
523
+ await assertNoFilesystemAliases(options, inputs, outputs.map((output) => output.path));
524
+ await Promise.all(outputs.map((output) => assertOutputPathSafe(options, output.path, output.label)));
525
+ for (const output of outputs) await atomicRepositoryWrite(options, output.path, output.bytes);
526
+ }
527
+ async function runCollectionCli(arguments_) {
528
+ const options = parseOptions(arguments_);
529
+ await assertRepositoryRoot(options.root);
530
+ let index;
531
+ if (options.command === "build") {
532
+ const initialManagedOutput = options.managedSourcePath ? options.managedPath ?? path.posix.join(path.posix.dirname(options.indexPath), "managed.json") : void 0;
533
+ await assertNoFilesystemAliases(options, /* @__PURE__ */ new Set([options.sourcePath, ...options.managedSourcePath ? [options.managedSourcePath] : []]), [options.indexPath, ...initialManagedOutput ? [initialManagedOutput] : []]);
534
+ const built = await prepareCollection(options);
535
+ index = built.index;
536
+ const managed = options.managedSourcePath ? await prepareManagedCollection(options, built) : void 0;
537
+ await writePreparedCollection(options, /* @__PURE__ */ new Set([...built.inputs, ...managed?.inputs ?? []]), [...built.outputs, ...managed?.outputs ?? []]);
538
+ } else index = await validatePublishedCollection(options, true);
539
+ process.stdout.write(`${options.command} ok: ${index.problems.length} problems, revision ${index.revision}\n`);
540
+ }
541
+ if (process.argv[1] ? import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href : false) runCollectionCli(process.argv.slice(2)).catch((error) => {
542
+ process.stderr.write(`woj organizer collection: ${error instanceof Error ? error.message : String(error)}\n`);
543
+ process.exitCode = 1;
544
+ });
545
+ //#endregion
546
+ export { BROWSER_COLLECTION_SCHEMA, BROWSER_PROBLEM_SCHEMA, MANAGED_COLLECTION_SCHEMA, parseManagedCollectionContract, parseManagedCollectionSource, parseManagedCollectionV2, parseManagedCollectionValueV2, parseProblemCollectionIndex, parseStandaloneProblemBundle, problemCollectionRevision, runCollectionCli, verifyProblemBundleBytes, verifyProblemCollectionRevision };
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Arjun Barrett
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/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@wasm-oj/organizer",
3
+ "version": "0.2.0",
4
+ "description": "Problem collection and immutable judge-data publication tools for WASM-OJ.",
5
+ "license": "MIT",
6
+ "author": "JacobLinCool",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/wasm-oj/forge.git",
10
+ "directory": "packages/organizer"
11
+ },
12
+ "homepage": "https://github.com/wasm-oj/forge#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/wasm-oj/forge/issues"
15
+ },
16
+ "keywords": [
17
+ "online-judge",
18
+ "problems",
19
+ "contest"
20
+ ],
21
+ "publishConfig": {
22
+ "access": "public",
23
+ "registry": "https://registry.npmjs.org/"
24
+ },
25
+ "type": "module",
26
+ "sideEffects": false,
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ },
33
+ "./package.json": "./package.json"
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "README.md",
38
+ "LICENSE",
39
+ "THIRD_PARTY_NOTICES.md",
40
+ "licenses"
41
+ ],
42
+ "engines": {
43
+ "node": ">=24.18.0 <25"
44
+ },
45
+ "dependencies": {
46
+ "@wasm-oj/core": "0.2.0"
47
+ },
48
+ "scripts": {
49
+ "build": "node ../../scripts/build-library.mjs --package @wasm-oj/organizer"
50
+ }
51
+ }