@opys/core 0.1.2

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/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # @opys/core
2
+
3
+ Data model for Opys manifests. Types, factory functions, Zod parsers, and encode/decode utilities. No I/O.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install @opys/core @opys/rules zod
9
+ ```
10
+
11
+ ## Types
12
+
13
+ ### `Source` — artifact origin
14
+
15
+ ```ts
16
+ type Source =
17
+ | { kind: 'url'; url: string }
18
+ | { kind: 'file'; file: string }
19
+ | { kind: 'string'; string: string }
20
+ | { kind: 'empty' };
21
+
22
+ // Factory functions
23
+ sourceUrl('https://example.com/file.jar');
24
+ sourceFile('./local/file.jar');
25
+ sourceString('inline content');
26
+ sourceEmpty();
27
+
28
+ // Parse / encode
29
+ SourceSchema.parse(raw);
30
+ encodeSource(source);
31
+ ```
32
+
33
+ ### `ExtractRule` — zip extraction instructions
34
+
35
+ ```ts
36
+ type ExtractRule =
37
+ | { kind: 'pick'; file: string; into: string } // single file
38
+ | { kind: 'scan'; matches: string; into: string; ... } // glob match
39
+ | { kind: 'dump'; into: string; clean?: boolean; ... } // full extract
40
+
41
+ // Factory functions
42
+ extractPick('lwjgl.dll', '${natives_directory}')
43
+ extractScan('*.so', '${natives_directory}', { excludes: ['META-INF/'] })
44
+ extractDump('${natives_directory}', { clean: true, excludes: ['META-INF/'] })
45
+
46
+ // Parse / encode
47
+ ExtractSchema.parse(raw) // always returns ExtractRule[]
48
+ encodeExtract(rules)
49
+ ```
50
+
51
+ ### `Artifact` — a single installable artifact
52
+
53
+ An artifact has a source, optional integrity/size checks, optional extract rules, and optional rulesets that gate it per platform or feature.
54
+
55
+ ```ts
56
+ import { ArtifactSchema, encodeArtifact } from '@opys/core';
57
+
58
+ const artifact = ArtifactSchema.parse(raw);
59
+ encodeArtifact(artifact);
60
+ ```
61
+
62
+ ### `Manifest` — the manifest
63
+
64
+ ```ts
65
+ interface Manifest {
66
+ vars: ValDefs;
67
+ launch?: Launch;
68
+ artifacts: ReadonlyArray<Artifact>;
69
+ }
70
+
71
+ // Parse JSON string
72
+ const manifest = await parseManifest(jsonString);
73
+
74
+ // Filter to current platform
75
+ const filtered = filterManifest(manifest, { name: 'linux', arch: 'x64' });
76
+
77
+ // Encode back to JSON-serializable object
78
+ encodeManifest(manifest);
79
+ ```
80
+
81
+ ### `ValDefs` — interpolation variables
82
+
83
+ Variables support OS-conditional values and `${ref}` interpolation.
84
+
85
+ ```ts
86
+ import {
87
+ parseValDefs,
88
+ encodeValDefs,
89
+ resolveValDefs,
90
+ resolveVars,
91
+ } from '@opys/core';
92
+
93
+ const defs = parseValDefs(raw);
94
+ const flat = resolveValDefs(defs, platform); // pick OS-appropriate values
95
+ const vars = resolveVars(flat); // resolve ${ref} chains
96
+ const result = interpolate('${root}/assets', vars);
97
+ ```
98
+
99
+ ### `defineConfig` — config file helper
100
+
101
+ Used as the default export in `opys.config.mjs`:
102
+
103
+ ```ts
104
+ import { defineConfig } from '@opys/core';
105
+
106
+ export default defineConfig({
107
+ output: 'opys.json',
108
+ manifest: {
109
+ artifacts: [...],
110
+ vars: [...],
111
+ launch: { ... },
112
+ },
113
+ });
114
+
115
+ // Or as a function for context-aware configs
116
+ export default defineConfig((ctx) => ({
117
+ manifest: {
118
+ artifacts: ctx.mode === 'build' ? [...] : [],
119
+ },
120
+ }));
121
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,413 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
12
+ key = keys[i];
13
+ if (!__hasOwnProp.call(to, key) && key !== except) {
14
+ __defProp(to, key, {
15
+ get: ((k) => from[k]).bind(null, key),
16
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
17
+ });
18
+ }
19
+ }
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
24
+ value: mod,
25
+ enumerable: true
26
+ }) : target, mod));
27
+
28
+ //#endregion
29
+ let zod = require("zod");
30
+ let _opys_core_binding = require("@opys/core-binding");
31
+ _opys_core_binding = __toESM(_opys_core_binding);
32
+
33
+ //#region lib/fetch.ts
34
+ /**
35
+ * Wrap `fetch()` with bounded retry on transient network failures and
36
+ * 5xx-class responses. Errors that won't recover from a retry (4xx,
37
+ * `AbortError`, JSON parse failures downstream of the response) are
38
+ * surfaced unchanged so callers handle them with their own logic.
39
+ *
40
+ * Network-level retries cover the AggregateError ETIMEDOUT / ECONNRESET
41
+ * / ENOTFOUND / EAI_AGAIN / ECONNREFUSED cases that node:undici reports
42
+ * as `TypeError: fetch failed` with a populated `cause`. Without this
43
+ * one DNS hiccup mid-`opys launch` aborts the whole config evaluation.
44
+ */
45
+ /**
46
+ * Default `User-Agent` for every opys HTTP request. The bare `undici`
47
+ * agent gets rejected or rate-limited by some CDNs and by the CurseForge
48
+ * API, so we identify ourselves. The patch component is omitted so the
49
+ * string doesn't churn against `package.json` on every release.
50
+ */
51
+ const OPYS_USER_AGENT = "opys/1.0";
52
+ const DEFAULT_RETRY_STATUSES = new Set([
53
+ 408,
54
+ 425,
55
+ 429,
56
+ 500,
57
+ 502,
58
+ 503,
59
+ 504
60
+ ]);
61
+ const TRANSIENT_CODES = new Set([
62
+ "ETIMEDOUT",
63
+ "ECONNRESET",
64
+ "ECONNREFUSED",
65
+ "ENOTFOUND",
66
+ "EAI_AGAIN",
67
+ "EPIPE",
68
+ "UND_ERR_SOCKET",
69
+ "UND_ERR_CONNECT_TIMEOUT",
70
+ "UND_ERR_HEADERS_TIMEOUT",
71
+ "UND_ERR_BODY_TIMEOUT"
72
+ ]);
73
+ function isTransientFetchError(err) {
74
+ if (!err || typeof err !== "object") return false;
75
+ const cause = err.cause;
76
+ if (cause) {
77
+ const code = cause.code;
78
+ if (code && TRANSIENT_CODES.has(code)) return true;
79
+ const inner = cause.errors;
80
+ if (Array.isArray(inner) && inner.length > 0) return inner.every((e) => {
81
+ const c = e?.code;
82
+ return c !== void 0 && TRANSIENT_CODES.has(c);
83
+ });
84
+ }
85
+ const code = err.code;
86
+ if (code && TRANSIENT_CODES.has(code)) return true;
87
+ return err instanceof TypeError && typeof err.message === "string" && err.message.toLowerCase().includes("fetch failed");
88
+ }
89
+ function backoffMs(attempt, base, max) {
90
+ const exp = base * 2 ** (attempt - 1);
91
+ const capped = Math.min(exp, max);
92
+ return Math.floor(capped * (.5 + Math.random() * .5));
93
+ }
94
+ function sleep(ms, signal) {
95
+ return new Promise((resolve, reject) => {
96
+ if (signal?.aborted) {
97
+ reject(signal.reason ?? /* @__PURE__ */ new Error("aborted"));
98
+ return;
99
+ }
100
+ const t = setTimeout(() => {
101
+ signal?.removeEventListener("abort", onAbort);
102
+ resolve();
103
+ }, ms);
104
+ const onAbort = () => {
105
+ clearTimeout(t);
106
+ reject(signal?.reason ?? /* @__PURE__ */ new Error("aborted"));
107
+ };
108
+ signal?.addEventListener("abort", onAbort, { once: true });
109
+ });
110
+ }
111
+ async function fetchWithRetry(input, init = {}, retry = {}) {
112
+ const attempts = Math.max(1, retry.attempts ?? 4);
113
+ const base = retry.baseDelayMs ?? 250;
114
+ const max = retry.maxDelayMs ?? 5e3;
115
+ const retryStatuses = retry.retryStatuses instanceof Set ? retry.retryStatuses : retry.retryStatuses ? new Set(retry.retryStatuses) : DEFAULT_RETRY_STATUSES;
116
+ const signal = retry.signal ?? init.signal ?? void 0;
117
+ const headers = new Headers(init.headers);
118
+ if (!headers.has("user-agent")) headers.set("user-agent", OPYS_USER_AGENT);
119
+ let lastErr;
120
+ for (let attempt = 1; attempt <= attempts; attempt++) try {
121
+ const res = await fetch(input, {
122
+ ...init,
123
+ headers,
124
+ signal
125
+ });
126
+ if (res.ok || attempt === attempts || !retryStatuses.has(res.status)) return res;
127
+ try {
128
+ await res.body?.cancel();
129
+ } catch {}
130
+ const delay = backoffMs(attempt, base, max);
131
+ retry.onRetry?.({
132
+ attempt,
133
+ delayMs: delay,
134
+ status: res.status
135
+ });
136
+ await sleep(delay, signal);
137
+ } catch (err) {
138
+ lastErr = err;
139
+ if (signal?.aborted) throw err;
140
+ if (!isTransientFetchError(err) || attempt === attempts) throw err;
141
+ const delay = backoffMs(attempt, base, max);
142
+ retry.onRetry?.({
143
+ attempt,
144
+ delayMs: delay,
145
+ error: err
146
+ });
147
+ await sleep(delay, signal);
148
+ }
149
+ throw lastErr;
150
+ }
151
+
152
+ //#endregion
153
+ //#region lib/index.ts
154
+ /**
155
+ * `@opys/core` — the frozen-manifest contract. Behaviors are backed by the
156
+ * Rust `opys-core` crate (via napi-rs); domain types, factories and small
157
+ * sugar helpers are hand-written TS.
158
+ *
159
+ * Strategy:
160
+ * - Algorithms that touch the manifest contract (decode, encode, resolve,
161
+ * filter, interpolate, glob) → typed wrappers around the Rust binding.
162
+ * - Domain types → hand-typed here, matching the frozen wire shape. These
163
+ * are pure data shapes; consumers construct them as plain JS objects.
164
+ * - Factories / type guards / small dedup helpers → pure TS, no boundary
165
+ * crossing. They are sugar over the typed shapes.
166
+ * - `parseShortRuleset` is implemented in TS as the shorthand sugar — the
167
+ * Rust binding accepts shorthand directly via `satisfiesRuleset` so the
168
+ * TS impl exists for consumers that need expanded `Rule` objects.
169
+ */
170
+ const OsNameSchema = zod.z.enum([
171
+ "linux",
172
+ "windows",
173
+ "osx"
174
+ ]);
175
+ const OsArchSchema = zod.z.enum([
176
+ "x86",
177
+ "x86_64",
178
+ "arm",
179
+ "aarch64",
180
+ "any"
181
+ ]);
182
+ const OsConstraintSchema = zod.z.object({
183
+ name: OsNameSchema.optional(),
184
+ version: zod.z.string().optional(),
185
+ arch: OsArchSchema.optional()
186
+ });
187
+ const RuleActionSchema = zod.z.enum(["allow", "disallow"]);
188
+ const RuleSchema = zod.z.union([
189
+ zod.z.object({
190
+ action: RuleActionSchema,
191
+ os: OsConstraintSchema
192
+ }),
193
+ zod.z.object({
194
+ action: RuleActionSchema,
195
+ features: zod.z.record(zod.z.string(), zod.z.boolean())
196
+ }),
197
+ zod.z.object({ action: RuleActionSchema })
198
+ ]);
199
+ function decodeManifest(wire) {
200
+ return _opys_core_binding.decodeManifest(wire);
201
+ }
202
+ function encodeManifest(domain) {
203
+ return _opys_core_binding.encodeManifest(domain);
204
+ }
205
+ function parseManifest(input) {
206
+ return _opys_core_binding.parseManifest(input);
207
+ }
208
+ function filterManifest(manifest, platform, features = []) {
209
+ return _opys_core_binding.filterManifest(manifest, platform, features);
210
+ }
211
+ function resolveVars(vars) {
212
+ return _opys_core_binding.resolveVars(vars);
213
+ }
214
+ function interpolate(template, vars) {
215
+ return _opys_core_binding.interpolate(template, vars);
216
+ }
217
+ function resolvedArgs(launch, platform, features = []) {
218
+ return _opys_core_binding.resolvedArgs(launch, platform, features);
219
+ }
220
+ function resolvedEnvs(launch, platform, features = []) {
221
+ return _opys_core_binding.resolvedEnvs(launch, platform, features);
222
+ }
223
+ function satisfiesRuleset(rules, platform, features = []) {
224
+ return _opys_core_binding.satisfiesRuleset(rules, platform, features);
225
+ }
226
+ function globBase(glob) {
227
+ return _opys_core_binding.globBase(glob);
228
+ }
229
+ function globToRegexSource(glob) {
230
+ return _opys_core_binding.globToRegexSource(glob);
231
+ }
232
+ /** Compile a glob to a real `RegExp` (the binding returns the source string). */
233
+ function globToRegex(glob) {
234
+ return new RegExp(_opys_core_binding.globToRegexSource(glob));
235
+ }
236
+ const sourceUrl = (url) => ({
237
+ kind: "url",
238
+ url
239
+ });
240
+ const sourceFile = (file) => ({
241
+ kind: "file",
242
+ file
243
+ });
244
+ const sourceString = (string) => ({
245
+ kind: "string",
246
+ string
247
+ });
248
+ const sourcePointer = (pointer) => ({
249
+ kind: "pointer",
250
+ pointer
251
+ });
252
+ const sourceBytes = (bytes) => ({
253
+ kind: "bytes",
254
+ bytes: Buffer.from(bytes).toString("base64")
255
+ });
256
+ const isSourceUrl = (s) => s.kind === "url";
257
+ const isSourceFile = (s) => s.kind === "file";
258
+ const isSourceString = (s) => s.kind === "string";
259
+ const isSourceBytes = (s) => s.kind === "bytes";
260
+ const isSourcePointer = (s) => s.kind === "pointer";
261
+ const extractPick = (file, into) => ({
262
+ kind: "pick",
263
+ file,
264
+ into
265
+ });
266
+ const extractScan = (matches, into, opts) => ({
267
+ kind: "scan",
268
+ matches,
269
+ into,
270
+ ...opts
271
+ });
272
+ const extractDump = (into, opts) => ({
273
+ kind: "dump",
274
+ into,
275
+ ...opts
276
+ });
277
+ const emptyRuleset = () => [];
278
+ const allowOsRuleset = (name) => [{
279
+ action: "allow",
280
+ os: { name }
281
+ }];
282
+ /** Deduplicate by normalized (posix) path; later entries win. */
283
+ function deduplicateArtifacts(artifacts) {
284
+ const norm = (p) => {
285
+ const parts = p.split("/");
286
+ const stack = [];
287
+ const lead = p.startsWith("/");
288
+ for (const seg of parts) {
289
+ if (seg === "" || seg === ".") continue;
290
+ if (seg === "..") {
291
+ if (stack.length > 0 && stack[stack.length - 1] !== "..") stack.pop();
292
+ else if (!lead) stack.push("..");
293
+ } else stack.push(seg);
294
+ }
295
+ const joined = stack.join("/");
296
+ if (lead) return "/" + joined;
297
+ return joined === "" ? "." : joined;
298
+ };
299
+ const map = /* @__PURE__ */ new Map();
300
+ for (const u of artifacts) map.set(norm(u.path), u);
301
+ return [...map.values()];
302
+ }
303
+ function parseShortRule(raw) {
304
+ if (typeof raw !== "string") return raw;
305
+ const parts = raw.split(".");
306
+ const action = parts[0];
307
+ if (action !== "allow" && action !== "disallow") throw new Error(`Unknown action '${action}'`);
308
+ const type = parts[1];
309
+ if (!type) return { action };
310
+ const rest = parts.slice(2).join(".");
311
+ switch (type) {
312
+ case "os": {
313
+ if (!rest) throw new Error("missing OS name");
314
+ const atIdx = rest.indexOf("@");
315
+ const name = atIdx === -1 ? rest : rest.slice(0, atIdx);
316
+ if (![
317
+ "linux",
318
+ "windows",
319
+ "osx"
320
+ ].includes(name)) throw new Error(`invalid os name '${name}'`);
321
+ const version = atIdx === -1 ? void 0 : rest.slice(atIdx + 1);
322
+ return version ? {
323
+ action,
324
+ os: {
325
+ name,
326
+ version
327
+ }
328
+ } : {
329
+ action,
330
+ os: { name }
331
+ };
332
+ }
333
+ case "features":
334
+ if (!rest) throw new Error("missing feature name");
335
+ return {
336
+ action,
337
+ features: { [rest]: true }
338
+ };
339
+ case "arch": {
340
+ if (!rest) throw new Error("missing arch");
341
+ const arch = rest;
342
+ if (![
343
+ "x86",
344
+ "x86_64",
345
+ "arm",
346
+ "aarch64",
347
+ "any"
348
+ ].includes(arch)) throw new Error(`invalid arch '${arch}'`);
349
+ return {
350
+ action,
351
+ os: { arch }
352
+ };
353
+ }
354
+ default: throw new Error(`unknown rule type '${type}'`);
355
+ }
356
+ }
357
+ function parseShortRuleset(raw) {
358
+ return (Array.isArray(raw) ? raw : [raw]).map(parseShortRule);
359
+ }
360
+ function parseValset(raw) {
361
+ if (!Array.isArray(raw)) throw new Error("parseValset: expected an array");
362
+ return raw.map((entry) => {
363
+ if (typeof entry === "string") return {
364
+ rules: [],
365
+ value: [entry]
366
+ };
367
+ if (entry && typeof entry === "object") {
368
+ const obj = entry;
369
+ return {
370
+ rules: obj.rules ? parseShortRuleset(obj.rules) : [],
371
+ value: Array.isArray(obj.value) ? obj.value : [obj.value]
372
+ };
373
+ }
374
+ throw new Error("parseValset: invalid entry");
375
+ });
376
+ }
377
+
378
+ //#endregion
379
+ exports.OPYS_USER_AGENT = OPYS_USER_AGENT;
380
+ exports.OsArchSchema = OsArchSchema;
381
+ exports.OsNameSchema = OsNameSchema;
382
+ exports.RuleSchema = RuleSchema;
383
+ exports.allowOsRuleset = allowOsRuleset;
384
+ exports.decodeManifest = decodeManifest;
385
+ exports.deduplicateArtifacts = deduplicateArtifacts;
386
+ exports.emptyRuleset = emptyRuleset;
387
+ exports.encodeManifest = encodeManifest;
388
+ exports.extractDump = extractDump;
389
+ exports.extractPick = extractPick;
390
+ exports.extractScan = extractScan;
391
+ exports.fetchWithRetry = fetchWithRetry;
392
+ exports.filterManifest = filterManifest;
393
+ exports.globBase = globBase;
394
+ exports.globToRegex = globToRegex;
395
+ exports.globToRegexSource = globToRegexSource;
396
+ exports.interpolate = interpolate;
397
+ exports.isSourceBytes = isSourceBytes;
398
+ exports.isSourceFile = isSourceFile;
399
+ exports.isSourcePointer = isSourcePointer;
400
+ exports.isSourceString = isSourceString;
401
+ exports.isSourceUrl = isSourceUrl;
402
+ exports.parseManifest = parseManifest;
403
+ exports.parseShortRuleset = parseShortRuleset;
404
+ exports.parseValset = parseValset;
405
+ exports.resolveVars = resolveVars;
406
+ exports.resolvedArgs = resolvedArgs;
407
+ exports.resolvedEnvs = resolvedEnvs;
408
+ exports.satisfiesRuleset = satisfiesRuleset;
409
+ exports.sourceBytes = sourceBytes;
410
+ exports.sourceFile = sourceFile;
411
+ exports.sourcePointer = sourcePointer;
412
+ exports.sourceString = sourceString;
413
+ exports.sourceUrl = sourceUrl;