@shinsou/plugin 0.0.0-stage → 0.1.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/dist/cli.js ADDED
@@ -0,0 +1,3096 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { createInterface } from "node:readline";
5
+ import { parseArgs } from "node:util";
6
+ import { existsSync as existsSync8 } from "node:fs";
7
+ import { resolve as resolve3 } from "node:path";
8
+ import qrcode from "qrcode-terminal";
9
+
10
+ // src/core/build.ts
11
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
12
+ import { dirname as dirname2, join as join3 } from "node:path";
13
+ import { build as esbuild } from "esbuild";
14
+ import { isDeepStrictEqual as isDeepStrictEqual2 } from "node:util";
15
+
16
+ // src/core/bytes.ts
17
+ var encoder = new TextEncoder();
18
+ var strictDecoder = new TextDecoder("utf-8", { fatal: true });
19
+ function utf8(text) {
20
+ if (hasLoneSurrogate(text)) throw new Error("String contains a lone UTF-16 surrogate");
21
+ return encoder.encode(text);
22
+ }
23
+ function decodeUtf8(bytes) {
24
+ return strictDecoder.decode(bytes);
25
+ }
26
+ function hasLoneSurrogate(text) {
27
+ for (let i = 0; i < text.length; i++) {
28
+ const c = text.charCodeAt(i);
29
+ if (c >= 55296 && c <= 56319) {
30
+ const next = text.charCodeAt(i + 1);
31
+ if (!(next >= 56320 && next <= 57343)) return true;
32
+ i++;
33
+ } else if (c >= 56320 && c <= 57343) {
34
+ return true;
35
+ }
36
+ }
37
+ return false;
38
+ }
39
+ function concatBytes(...parts) {
40
+ const total = parts.reduce((n, p) => n + p.length, 0);
41
+ const out = new Uint8Array(total);
42
+ let off = 0;
43
+ for (const p of parts) {
44
+ out.set(p, off);
45
+ off += p.length;
46
+ }
47
+ return out;
48
+ }
49
+ function toHex(bytes) {
50
+ let s = "";
51
+ for (const b of bytes) s += b.toString(16).padStart(2, "0");
52
+ return s;
53
+ }
54
+ function base64UrlEncode(bytes) {
55
+ return Buffer.from(bytes).toString("base64url");
56
+ }
57
+ function base64UrlDecodeCanonical(value2, expectedBytes) {
58
+ if (value2.length > 128 && expectedBytes !== void 0) throw new Error("Encoded signing material is too large");
59
+ if (!/^[A-Za-z0-9_-]*$/.test(value2)) throw new Error("Non-canonical base64url");
60
+ const bytes = new Uint8Array(Buffer.from(value2, "base64url"));
61
+ if (base64UrlEncode(bytes) !== value2) throw new Error("Non-canonical base64url");
62
+ if (expectedBytes !== void 0 && bytes.length !== expectedBytes) throw new Error("Invalid signing material size");
63
+ return bytes;
64
+ }
65
+ function bytesEqual(a, b) {
66
+ if (a.length !== b.length) return false;
67
+ let diff = 0;
68
+ for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
69
+ return diff === 0;
70
+ }
71
+
72
+ // src/core/crypto.ts
73
+ import * as ed from "@noble/ed25519";
74
+ import { sha256 as nobleSha256, sha512 } from "@noble/hashes/sha2.js";
75
+ ed.hashes.sha512 = sha512;
76
+ function sha256(data) {
77
+ return nobleSha256(data);
78
+ }
79
+ function sha256Hex(data) {
80
+ return toHex(sha256(data));
81
+ }
82
+ function publicKeyFromSeed(seed) {
83
+ if (seed.length !== 32) throw new Error("Ed25519 seed must be 32 bytes");
84
+ return ed.getPublicKey(seed);
85
+ }
86
+ function signEd25519(message, seed) {
87
+ return ed.sign(message, seed);
88
+ }
89
+ function verifyEd25519(signature, message, publicKey) {
90
+ try {
91
+ return ed.verify(signature, message, publicKey, { zip215: false });
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
96
+ function randomSeed() {
97
+ return ed.utils.randomSecretKey();
98
+ }
99
+ function fingerprint(publicKey) {
100
+ return "sha256:" + base64UrlEncode(sha256(publicKey));
101
+ }
102
+ function displayFingerprint(publicKey) {
103
+ const hex = toHex(sha256(publicKey).subarray(0, 16)).toUpperCase();
104
+ return hex.match(/.{4}/g).join(" ");
105
+ }
106
+
107
+ // src/core/consistency.ts
108
+ import { isDeepStrictEqual } from "node:util";
109
+ import { Script } from "node:vm";
110
+
111
+ // src/core/manifest.ts
112
+ import { readFileSync } from "node:fs";
113
+ import { join } from "node:path";
114
+
115
+ // src/core/canonical-json.ts
116
+ var CanonicalJsonError = class extends Error {
117
+ };
118
+ function canonicalJson(value2) {
119
+ return write(value2, 0);
120
+ }
121
+ function write(value2, depth) {
122
+ if (depth > 256) throw new CanonicalJsonError("JSON nesting too deep");
123
+ if (value2 === null) return "null";
124
+ if (value2 === true) return "true";
125
+ if (value2 === false) return "false";
126
+ if (typeof value2 === "number") return writeNumber(value2);
127
+ if (typeof value2 === "string") return writeString(value2);
128
+ if (Array.isArray(value2)) return "[" + value2.map((v) => write(v, depth + 1)).join(",") + "]";
129
+ if (typeof value2 === "object") {
130
+ const keys = Object.keys(value2).sort();
131
+ return "{" + keys.map((k) => writeString(k) + ":" + write(value2[k], depth + 1)).join(",") + "}";
132
+ }
133
+ throw new CanonicalJsonError(`Unsupported JSON value of type ${typeof value2}`);
134
+ }
135
+ function writeNumber(n) {
136
+ if (!Number.isSafeInteger(n)) throw new CanonicalJsonError(`Only safe integers are canonical, got ${n}`);
137
+ if (Object.is(n, -0)) throw new CanonicalJsonError("Negative zero is not canonical");
138
+ return String(n);
139
+ }
140
+ function writeString(s) {
141
+ if (hasLoneSurrogate(s)) throw new CanonicalJsonError("String contains a lone surrogate");
142
+ let out = '"';
143
+ for (let i = 0; i < s.length; i++) {
144
+ const ch = s[i];
145
+ const c = s.charCodeAt(i);
146
+ switch (ch) {
147
+ case '"':
148
+ out += '\\"';
149
+ break;
150
+ case "\\":
151
+ out += "\\\\";
152
+ break;
153
+ case "\n":
154
+ out += "\\n";
155
+ break;
156
+ case "\r":
157
+ out += "\\r";
158
+ break;
159
+ case " ":
160
+ out += "\\t";
161
+ break;
162
+ case "\b":
163
+ out += "\\b";
164
+ break;
165
+ case "\f":
166
+ out += "\\f";
167
+ break;
168
+ default:
169
+ out += c < 32 ? "\\u" + c.toString(16).padStart(4, "0") : ch;
170
+ }
171
+ }
172
+ return out + '"';
173
+ }
174
+ function parseStrictJson(text) {
175
+ const p = new Parser(text);
176
+ p.ws();
177
+ const v = p.value(0);
178
+ p.ws();
179
+ if (p.i !== text.length) p.fail("trailing data");
180
+ return v;
181
+ }
182
+ var Parser = class {
183
+ constructor(s) {
184
+ this.s = s;
185
+ }
186
+ s;
187
+ i = 0;
188
+ fail(msg) {
189
+ throw new CanonicalJsonError(`Invalid JSON at offset ${this.i}: ${msg}`);
190
+ }
191
+ ws() {
192
+ while (this.i < this.s.length) {
193
+ const c = this.s[this.i];
194
+ if (c === " " || c === " " || c === "\n" || c === "\r") this.i++;
195
+ else break;
196
+ }
197
+ }
198
+ value(depth) {
199
+ if (depth > 256) this.fail("nesting too deep");
200
+ const c = this.s[this.i];
201
+ if (c === "{") return this.object(depth);
202
+ if (c === "[") return this.array(depth);
203
+ if (c === '"') return this.string();
204
+ if (c === "-" || c >= "0" && c <= "9") return this.number();
205
+ if (this.s.startsWith("true", this.i)) {
206
+ this.i += 4;
207
+ return true;
208
+ }
209
+ if (this.s.startsWith("false", this.i)) {
210
+ this.i += 5;
211
+ return false;
212
+ }
213
+ if (this.s.startsWith("null", this.i)) {
214
+ this.i += 4;
215
+ return null;
216
+ }
217
+ return this.fail("unexpected character");
218
+ }
219
+ object(depth) {
220
+ this.i++;
221
+ const out = {};
222
+ const seen = /* @__PURE__ */ new Set();
223
+ this.ws();
224
+ if (this.s[this.i] === "}") {
225
+ this.i++;
226
+ return out;
227
+ }
228
+ for (; ; ) {
229
+ this.ws();
230
+ if (this.s[this.i] !== '"') this.fail("expected key");
231
+ const key = this.string();
232
+ if (seen.has(key)) this.fail(`duplicate key ${JSON.stringify(key)}`);
233
+ seen.add(key);
234
+ this.ws();
235
+ if (this.s[this.i] !== ":") this.fail("expected ':'");
236
+ this.i++;
237
+ this.ws();
238
+ const v = this.value(depth + 1);
239
+ Object.defineProperty(out, key, { value: v, enumerable: true, writable: true, configurable: true });
240
+ this.ws();
241
+ const c = this.s[this.i++];
242
+ if (c === "}") return out;
243
+ if (c !== ",") this.fail("expected ',' or '}'");
244
+ }
245
+ }
246
+ array(depth) {
247
+ this.i++;
248
+ const out = [];
249
+ this.ws();
250
+ if (this.s[this.i] === "]") {
251
+ this.i++;
252
+ return out;
253
+ }
254
+ for (; ; ) {
255
+ this.ws();
256
+ out.push(this.value(depth + 1));
257
+ this.ws();
258
+ const c = this.s[this.i++];
259
+ if (c === "]") return out;
260
+ if (c !== ",") this.fail("expected ',' or ']'");
261
+ }
262
+ }
263
+ string() {
264
+ this.i++;
265
+ let out = "";
266
+ for (; ; ) {
267
+ if (this.i >= this.s.length) this.fail("unterminated string");
268
+ const c = this.s[this.i++];
269
+ if (c === '"') return out;
270
+ if (c === "\\") {
271
+ const e = this.s[this.i++];
272
+ switch (e) {
273
+ case '"':
274
+ out += '"';
275
+ break;
276
+ case "\\":
277
+ out += "\\";
278
+ break;
279
+ case "/":
280
+ out += "/";
281
+ break;
282
+ case "b":
283
+ out += "\b";
284
+ break;
285
+ case "f":
286
+ out += "\f";
287
+ break;
288
+ case "n":
289
+ out += "\n";
290
+ break;
291
+ case "r":
292
+ out += "\r";
293
+ break;
294
+ case "t":
295
+ out += " ";
296
+ break;
297
+ case "u": {
298
+ const hex = this.s.slice(this.i, this.i + 4);
299
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) this.fail("bad \\u escape");
300
+ out += String.fromCharCode(parseInt(hex, 16));
301
+ this.i += 4;
302
+ break;
303
+ }
304
+ default:
305
+ this.fail("bad escape");
306
+ }
307
+ } else {
308
+ if (c.charCodeAt(0) < 32) this.fail("raw control character in string");
309
+ out += c;
310
+ }
311
+ }
312
+ }
313
+ number() {
314
+ const m = /^-?(0|[1-9][0-9]*)/.exec(this.s.slice(this.i));
315
+ if (!m) this.fail("bad number");
316
+ const text = m[0];
317
+ const next = this.s[this.i + text.length];
318
+ if (next === "." || next === "e" || next === "E") this.fail("only integers are allowed");
319
+ if (text === "-0") this.fail("negative zero is not allowed");
320
+ const n = Number(text);
321
+ if (!Number.isSafeInteger(n)) this.fail("integer outside \xB19007199254740991");
322
+ this.i += text.length;
323
+ return n;
324
+ }
325
+ };
326
+
327
+ // src/core/manifest.ts
328
+ var MANIFEST_FILE = "shinsou.plugin.json";
329
+ var INDEX_FORMAT = "shinsou-extension-v2";
330
+ var SIDECAR_FORMAT = "shinsou-extension-sidecar-v2";
331
+ var CONTENT_CONTRACT = "extension-content-v2";
332
+ var CONTRACT_VERSION = 2;
333
+ var EVENTS_PROTOCOL = "dev.shinsou.system";
334
+ var CONTENT_KINDS = ["IMAGE_SEQUENCE", "PLAIN_TEXT", "EPUB_SPINE"];
335
+ var CONTENT_TYPES = ["manga", "novel", "both"];
336
+ var CAPABILITIES = [
337
+ "CATALOGUE",
338
+ "LATEST",
339
+ "BROWSE",
340
+ "METADATA",
341
+ "UNITS",
342
+ "CONTENT",
343
+ "SEARCH",
344
+ "PREFERENCES",
345
+ "LOGIN",
346
+ "FAVORITE"
347
+ ];
348
+ var RUNTIME_PERMISSIONS = [
349
+ "EXECUTE_SCRIPT",
350
+ "NETWORK",
351
+ "COOKIE_STORAGE",
352
+ "CREDENTIAL_ACCESS",
353
+ "LOGIN_PROMPT",
354
+ "FAVORITE_MUTATION",
355
+ "BROWSER_CHALLENGE",
356
+ // ADR-0002 §5.1: reading cookie values (getCookie/getCookies also need COOKIE_STORAGE in the app).
357
+ "COOKIE_READ",
358
+ // Loading the source's browserImageOrigins in an isolated browser (macOS/iOS).
359
+ "BROWSER_IMAGES"
360
+ ];
361
+ var HOST_PERMISSIONS = [
362
+ "REQUEST_LOGIN_UI",
363
+ "REQUEST_SOURCE_REFRESH",
364
+ "REQUEST_LOGOUT",
365
+ "REPORT_DIAGNOSTIC",
366
+ "REPORT_USER_MESSAGE",
367
+ "REQUEST_BROWSER_CHALLENGE"
368
+ ];
369
+ var TIER_CEILING_HOST_PERMISSIONS = ["REPORT_USER_MESSAGE"];
370
+ var EVENT_IDS = [
371
+ "command.auth.login.request",
372
+ "command.source.refresh.request",
373
+ "command.auth.logout.request",
374
+ "event.diagnostic.message.report"
375
+ ];
376
+ var ManifestError = class extends Error {
377
+ };
378
+ function loadManifest(projectDir) {
379
+ const path = join(projectDir, MANIFEST_FILE);
380
+ let raw;
381
+ try {
382
+ raw = parseStrictJson(readFileSync(path, "utf8"));
383
+ } catch (err) {
384
+ throw new ManifestError(`${MANIFEST_FILE}: ${err.message}`);
385
+ }
386
+ return validateManifest(raw);
387
+ }
388
+ function validateManifest(raw) {
389
+ const errors = [];
390
+ const o = obj(raw, "manifest", errors);
391
+ const str = (v, label) => {
392
+ if (typeof v !== "string" || !v.trim()) errors.push(`${label} must be a non-empty string`);
393
+ return typeof v === "string" ? v : "";
394
+ };
395
+ const strList = (v, label, allowed) => {
396
+ if (v === void 0) return [];
397
+ if (!Array.isArray(v) || v.some((x) => typeof x !== "string")) {
398
+ errors.push(`${label} must be an array of strings`);
399
+ return [];
400
+ }
401
+ const list = v;
402
+ if (new Set(list).size !== list.length) errors.push(`${label} must not contain duplicates`);
403
+ if (allowed) {
404
+ for (const x of list) if (!allowed.includes(x)) errors.push(`${label}: unknown value ${x}`);
405
+ }
406
+ return list;
407
+ };
408
+ if (o.manifestVersion !== 1) errors.push("manifestVersion must be 1");
409
+ const packageId = str(o.packageId, "packageId");
410
+ if (packageId && !/^[a-z0-9][a-z0-9._-]{0,127}$/.test(packageId)) {
411
+ errors.push("packageId must be lowercase [a-z0-9._-], 1\u2013128 characters, e.g. 'zh.example'");
412
+ }
413
+ const versionCode = o.versionCode;
414
+ if (typeof versionCode !== "number" || !Number.isInteger(versionCode) || versionCode < 0 || versionCode > 2147483647) {
415
+ errors.push("versionCode must be a non-negative 32-bit integer");
416
+ }
417
+ if (typeof o.nsfw !== "boolean") errors.push("nsfw must be a boolean");
418
+ const contentType = str(o.contentType, "contentType");
419
+ if (contentType && !CONTENT_TYPES.includes(contentType)) errors.push(`contentType must be one of ${CONTENT_TYPES.join(", ")}`);
420
+ const contentKinds = strList(o.contentKinds, "contentKinds", CONTENT_KINDS);
421
+ if (contentKinds.length === 0) errors.push("contentKinds must not be empty");
422
+ const events = obj(o.systemEvents ?? {}, "systemEvents", errors);
423
+ const repo = obj(o.repository, "repository", errors);
424
+ const sourcesRaw = Array.isArray(o.sources) ? o.sources : (errors.push("sources must be an array"), []);
425
+ if (sourcesRaw.length === 0) errors.push("sources must contain at least one source");
426
+ const sources = sourcesRaw.map((s, i) => {
427
+ const so = obj(s, `sources[${i}]`, errors);
428
+ const label = `sources[${i}]`;
429
+ const legacy = so.legacyLongId;
430
+ if (legacy !== void 0 && legacy !== null && typeof legacy !== "string") errors.push(`${label}.legacyLongId must be a string or null`);
431
+ const source = {
432
+ sourceId: str(so.sourceId, `${label}.sourceId`),
433
+ legacyLongId: typeof legacy === "string" ? legacy : null,
434
+ name: str(so.name, `${label}.name`),
435
+ lang: str(so.lang, `${label}.lang`),
436
+ baseUrl: str(so.baseUrl, `${label}.baseUrl`),
437
+ requestOrigins: strList(so.requestOrigins, `${label}.requestOrigins`),
438
+ credentialOrigins: strList(so.credentialOrigins, `${label}.credentialOrigins`),
439
+ contentOrigins: strList(so.contentOrigins, `${label}.contentOrigins`),
440
+ browserSessionOrigins: strList(so.browserSessionOrigins, `${label}.browserSessionOrigins`),
441
+ contentHostSuffixes: strList(so.contentHostSuffixes, `${label}.contentHostSuffixes`),
442
+ browserImageOrigins: strList(so.browserImageOrigins, `${label}.browserImageOrigins`)
443
+ };
444
+ if (so.contentType !== void 0) source.contentType = str(so.contentType, `${label}.contentType`);
445
+ if (so.contentKinds !== void 0) source.contentKinds = strList(so.contentKinds, `${label}.contentKinds`, CONTENT_KINDS);
446
+ return source;
447
+ });
448
+ const ids = sources.map((s) => s.sourceId);
449
+ if (new Set(ids).size !== ids.length) errors.push("sourceId values must be unique");
450
+ const manifest = {
451
+ manifestVersion: 1,
452
+ packageId,
453
+ name: str(o.name, "name"),
454
+ version: str(o.version, "version"),
455
+ versionCode: typeof versionCode === "number" ? versionCode : 0,
456
+ lang: str(o.lang, "lang"),
457
+ nsfw: o.nsfw === true,
458
+ contract: typeof o.contract === "string" ? o.contract : "shinsou",
459
+ runtime: typeof o.runtime === "string" ? o.runtime : "legacy-shinsou-adapter-v2",
460
+ contentType,
461
+ contentKinds,
462
+ capabilities: strList(o.capabilities, "capabilities", CAPABILITIES),
463
+ runtimePermissions: strList(o.runtimePermissions, "runtimePermissions", RUNTIME_PERMISSIONS),
464
+ requestedHostPermissions: strList(o.requestedHostPermissions, "requestedHostPermissions", HOST_PERMISSIONS),
465
+ systemEvents: {
466
+ required: strList(events.required, "systemEvents.required", EVENT_IDS),
467
+ optional: strList(events.optional, "systemEvents.optional", EVENT_IDS)
468
+ },
469
+ entry: str(o.entry, "entry"),
470
+ sources,
471
+ repository: {
472
+ name: str(repo.name, "repository.name"),
473
+ shortName: typeof repo.shortName === "string" ? repo.shortName : str(repo.name, "repository.name"),
474
+ website: str(repo.website, "repository.website"),
475
+ mirrors: strList(repo.mirrors, "repository.mirrors")
476
+ }
477
+ };
478
+ if (typeof o.description === "string") manifest.description = o.description;
479
+ if (o.test && typeof o.test === "object" && !Array.isArray(o.test)) {
480
+ const t = o.test;
481
+ manifest.test = {
482
+ cases: typeof t.cases === "string" ? t.cases : void 0,
483
+ fixtures: typeof t.fixtures === "string" ? t.fixtures : void 0
484
+ };
485
+ }
486
+ if (errors.length) throw new ManifestError(`${MANIFEST_FILE} is invalid:
487
+ - ${errors.join("\n - ")}`);
488
+ return manifest;
489
+ }
490
+ function obj(v, label, errors) {
491
+ if (!v || typeof v !== "object" || Array.isArray(v)) {
492
+ errors.push(`${label} must be an object`);
493
+ return {};
494
+ }
495
+ return v;
496
+ }
497
+ function scriptUrl(m) {
498
+ return `plugins/${m.packageId}.js`;
499
+ }
500
+ function sidecarUrl(m) {
501
+ return `sidecars/${m.packageId}.json`;
502
+ }
503
+ function eventDeclaration(m) {
504
+ return {
505
+ protocol: EVENTS_PROTOCOL,
506
+ minVersion: 1,
507
+ maxVersion: 1,
508
+ required: [...m.systemEvents.required],
509
+ optional: [...m.systemEvents.optional]
510
+ };
511
+ }
512
+ function executableMetadata(m) {
513
+ return {
514
+ contractVersion: CONTRACT_VERSION,
515
+ contentContract: CONTENT_CONTRACT,
516
+ packageId: m.packageId,
517
+ contentType: m.contentType,
518
+ contentKinds: [...m.contentKinds],
519
+ systemEvents: eventDeclaration(m),
520
+ requestedHostPermissions: [...m.requestedHostPermissions]
521
+ };
522
+ }
523
+ function originFields(s) {
524
+ const out = {
525
+ requestOrigins: [...s.requestOrigins],
526
+ credentialOrigins: [...s.credentialOrigins],
527
+ contentOrigins: [...s.contentOrigins],
528
+ browserSessionOrigins: [...s.browserSessionOrigins]
529
+ };
530
+ if (s.contentHostSuffixes.length) out.contentHostSuffixes = [...s.contentHostSuffixes];
531
+ if (s.browserImageOrigins.length) out.browserImageOrigins = [...s.browserImageOrigins];
532
+ return out;
533
+ }
534
+ function indexEntry(m, artifact) {
535
+ const entry = {
536
+ id: m.packageId,
537
+ name: m.name,
538
+ version: m.version,
539
+ versionCode: m.versionCode,
540
+ lang: m.lang,
541
+ nsfw: m.nsfw,
542
+ contract: m.contract,
543
+ runtime: m.runtime,
544
+ contentType: m.contentType,
545
+ contentKinds: [...m.contentKinds],
546
+ scriptUrl: scriptUrl(m),
547
+ sidecarUrl: sidecarUrl(m),
548
+ sha256: artifact.sha256,
549
+ byteSize: artifact.byteSize,
550
+ capabilities: [...m.capabilities],
551
+ runtimePermissions: [...m.runtimePermissions],
552
+ requestedHostPermissions: [...m.requestedHostPermissions],
553
+ systemEvents: eventDeclaration(m),
554
+ sources: m.sources.map((s) => {
555
+ const src = {
556
+ sourceId: s.sourceId,
557
+ legacyLongId: s.legacyLongId,
558
+ name: s.name,
559
+ lang: s.lang,
560
+ baseUrl: s.baseUrl
561
+ };
562
+ if (s.contentType) src.contentType = s.contentType;
563
+ src.contentKinds = [...s.contentKinds ?? m.contentKinds];
564
+ return { ...src, ...originFields(s) };
565
+ })
566
+ };
567
+ if (m.description) entry.description = m.description;
568
+ return entry;
569
+ }
570
+ function indexDocument(m, entries) {
571
+ return {
572
+ format: INDEX_FORMAT,
573
+ contractVersion: CONTRACT_VERSION,
574
+ contentContract: CONTENT_CONTRACT,
575
+ contentContractVersion: CONTRACT_VERSION,
576
+ repository: { name: m.repository.name, source: "index.json" },
577
+ systemEvents: {
578
+ protocol: EVENTS_PROTOCOL,
579
+ minVersion: 1,
580
+ maxVersion: 1,
581
+ knownPermissions: HOST_PERMISSIONS.filter((p) => !TIER_CEILING_HOST_PERMISSIONS.includes(p))
582
+ },
583
+ packages: entries
584
+ };
585
+ }
586
+ function sidecarDocument(m, artifact) {
587
+ return {
588
+ format: SIDECAR_FORMAT,
589
+ contractVersion: CONTRACT_VERSION,
590
+ packageId: m.packageId,
591
+ name: m.name,
592
+ version: m.version,
593
+ versionCode: m.versionCode,
594
+ contract: m.contract,
595
+ runtime: m.runtime,
596
+ lang: m.lang,
597
+ nsfw: m.nsfw,
598
+ installable: true,
599
+ artifact: { scriptUrl: scriptUrl(m), sha256: artifact.sha256, byteSize: artifact.byteSize },
600
+ content: { contract: CONTENT_CONTRACT, contractVersion: CONTRACT_VERSION, type: m.contentType, kinds: [...m.contentKinds] },
601
+ capabilities: [...m.capabilities],
602
+ systemEvents: eventDeclaration(m),
603
+ requestedHostPermissions: [...m.requestedHostPermissions],
604
+ runtimePermissions: [...m.runtimePermissions],
605
+ sources: m.sources.map((s) => {
606
+ const src = {
607
+ sourceKey: { contractVersion: CONTRACT_VERSION, packageId: m.packageId, sourceId: s.sourceId, legacyLongId: s.legacyLongId },
608
+ sourceId: s.sourceId,
609
+ name: s.name,
610
+ lang: s.lang,
611
+ baseUrl: s.baseUrl
612
+ };
613
+ if (s.contentType) src.contentType = s.contentType;
614
+ return {
615
+ ...src,
616
+ capabilities: [...m.capabilities],
617
+ contentKinds: [...s.contentKinds ?? m.contentKinds],
618
+ systemEvents: eventDeclaration(m),
619
+ requestedHostPermissions: [...m.requestedHostPermissions],
620
+ runtimePermissions: [...m.runtimePermissions],
621
+ ...originFields(s)
622
+ };
623
+ })
624
+ };
625
+ }
626
+ function repoMetaDocument(m, signingKeyFingerprint) {
627
+ return {
628
+ meta: {
629
+ name: m.repository.name,
630
+ shortName: m.repository.shortName,
631
+ website: m.repository.website,
632
+ signingKeyFingerprint
633
+ }
634
+ };
635
+ }
636
+ function defaultSourceId(digest) {
637
+ let v = 0n;
638
+ for (let i = 0; i < 8; i++) v = v << 8n | BigInt(digest[i]);
639
+ return String(v & 0x7fffffffffffffffn);
640
+ }
641
+
642
+ // src/core/consistency.ts
643
+ var EXECUTABLE_DECLARATION = /var __shinsouExtensionV2 = (\{[^\n]+\});/;
644
+ function readExecutableMetadata(scriptText) {
645
+ const m = EXECUTABLE_DECLARATION.exec(scriptText);
646
+ if (!m) return null;
647
+ try {
648
+ return JSON.parse(m[1]);
649
+ } catch {
650
+ return null;
651
+ }
652
+ }
653
+ function checkPackageConsistency(args) {
654
+ const problems = [];
655
+ const expect = (cond, msg) => {
656
+ if (!cond) problems.push(msg);
657
+ };
658
+ const same = (a, b, msg) => expect(isDeepStrictEqual(a, b), msg);
659
+ const { index, pkg, sidecar, scriptBytes } = args;
660
+ const id = String(pkg.id);
661
+ expect(index.format === INDEX_FORMAT, "unexpected repository index format");
662
+ expect(index.contractVersion === 2, "unexpected v2 contract version");
663
+ expect(index.contentContract === CONTENT_CONTRACT, "unexpected v2 content contract");
664
+ expect(pkg.contractVersion === void 0 || pkg.contractVersion === 2, `${id} contract version`);
665
+ expect(typeof pkg.id === "string" && pkg.id.length > 0, "package id");
666
+ expect(!("hostPermissions" in pkg), `${id} must not self-grant host permissions`);
667
+ noTraversal(pkg.scriptUrl, `${id} scriptUrl`, expect);
668
+ noTraversal(pkg.sidecarUrl, `${id} sidecarUrl`, expect);
669
+ expect(String(pkg.scriptUrl).startsWith("plugins/"), `${id} scriptUrl must be under plugins`);
670
+ expect(String(pkg.sidecarUrl).startsWith("sidecars/"), `${id} sidecarUrl must be under sidecars`);
671
+ const kinds = pkg.contentKinds;
672
+ expect(Array.isArray(kinds) && kinds.length > 0, `${id} content kinds`);
673
+ for (const k of kinds ?? []) expect(CONTENT_KINDS.includes(k), `${id} unknown content kind ${k}`);
674
+ checkEvents(pkg.systemEvents, `${id} package events`, expect);
675
+ checkList(pkg.requestedHostPermissions, HOST_PERMISSIONS, `${id} requestedHostPermissions`, expect);
676
+ checkList(pkg.runtimePermissions, RUNTIME_PERMISSIONS, `${id} runtimePermissions`, expect);
677
+ const sources = pkg.sources ?? [];
678
+ for (const s of sources) checkBrowserSessionOrigins(s.browserSessionOrigins ?? [], `${id} source browserSessionOrigins`, expect);
679
+ expect(scriptBytes.length === pkg.byteSize, `${id} index byteSize mismatch`);
680
+ expect(sha256Hex(scriptBytes) === pkg.sha256, `${id} index sha256 mismatch`);
681
+ let scriptText = "";
682
+ try {
683
+ scriptText = decodeUtf8(scriptBytes);
684
+ new Script(scriptText, { filename: String(pkg.scriptUrl) });
685
+ } catch (err) {
686
+ problems.push(`${id} script does not parse as a classic script: ${err.message}`);
687
+ }
688
+ expect(scriptText.includes("source.v2 = __shinsouExtensionV2"), `${id} missing v2 metadata export`);
689
+ const meta = readExecutableMetadata(scriptText);
690
+ expect(meta, `${id} missing executable v2 declaration`);
691
+ if (meta) {
692
+ expect(meta.contractVersion === 2, `${id} executable contract version`);
693
+ expect(meta.contentContract === CONTENT_CONTRACT, `${id} executable content contract`);
694
+ expect(meta.packageId === pkg.id, `${id} executable package identity`);
695
+ same(meta.contentType, pkg.contentType, `${id} executable content type`);
696
+ same(meta.contentKinds, pkg.contentKinds, `${id} executable content kinds`);
697
+ same(meta.systemEvents, pkg.systemEvents, `${id} executable event declaration`);
698
+ same(meta.requestedHostPermissions, pkg.requestedHostPermissions, `${id} executable requested permissions`);
699
+ }
700
+ expect(sidecar.format === SIDECAR_FORMAT, `${id} sidecar format`);
701
+ expect(sidecar.contractVersion === 2, `${id} sidecar contract version`);
702
+ expect(sidecar.packageId === pkg.id, `${id} sidecar package identity`);
703
+ expect(sidecar.version === pkg.version && sidecar.versionCode === pkg.versionCode, `${id} sidecar version identity`);
704
+ expect(sidecar.runtime === void 0 || sidecar.runtime === pkg.runtime, `${id} sidecar runtime binding`);
705
+ expect(!("hostPermissions" in sidecar), `${id} sidecar must not self-grant host permissions`);
706
+ const artifact = sidecar.artifact ?? {};
707
+ expect(artifact.scriptUrl === pkg.scriptUrl, `${id} sidecar artifact binding`);
708
+ expect(artifact.sha256 === pkg.sha256 && artifact.byteSize === pkg.byteSize, `${id} sidecar artifact metadata`);
709
+ const content = sidecar.content ?? {};
710
+ expect(content.contract === CONTENT_CONTRACT && content.contractVersion === 2, `${id} sidecar content contract`);
711
+ expect(content.type === pkg.contentType, `${id} sidecar content type`);
712
+ same(content.kinds, pkg.contentKinds, `${id} sidecar content kinds`);
713
+ same(sidecar.capabilities, pkg.capabilities, `${id} sidecar capabilities`);
714
+ checkEvents(sidecar.systemEvents, `${id} sidecar events`, expect);
715
+ same(sidecar.systemEvents, pkg.systemEvents, `${id} sidecar package events`);
716
+ same(sidecar.requestedHostPermissions, pkg.requestedHostPermissions, `${id} sidecar requested permissions`);
717
+ same(sidecar.runtimePermissions, pkg.runtimePermissions, `${id} sidecar runtime permissions`);
718
+ const sidecarSources = sidecar.sources ?? [];
719
+ expect(sidecarSources.length === sources.length, `${id} sidecar source count`);
720
+ for (const s of sources) {
721
+ const match = sidecarSources.find((e) => e.sourceId === s.sourceId);
722
+ if (!match) {
723
+ problems.push(`${id} sidecar is missing source ${String(s.sourceId)}`);
724
+ continue;
725
+ }
726
+ const key = match.sourceKey ?? {};
727
+ expect(key.packageId === pkg.id && key.sourceId === s.sourceId && key.contractVersion === 2, `${id} sourceKey binding`);
728
+ expect(key.legacyLongId === s.legacyLongId, `${id} sourceKey legacyLongId`);
729
+ for (const field of ["name", "lang", "baseUrl", "requestOrigins", "credentialOrigins", "contentOrigins", "browserSessionOrigins", "browserImageOrigins", "contentHostSuffixes"]) {
730
+ same(match[field] ?? null, s[field] ?? null, `${id} source ${String(s.sourceId)} ${field} binding`);
731
+ }
732
+ expect("contentKinds" in match, `${id} sidecar source contentKinds must be explicit`);
733
+ same(match.contentKinds, s.contentKinds ?? pkg.contentKinds, `${id} source contentKinds binding`);
734
+ same(match.capabilities, pkg.capabilities, `${id} source capabilities binding`);
735
+ same(match.systemEvents, pkg.systemEvents, `${id} source events binding`);
736
+ same(match.requestedHostPermissions, pkg.requestedHostPermissions, `${id} source requested permissions binding`);
737
+ same(match.runtimePermissions, pkg.runtimePermissions, `${id} source runtime permissions binding`);
738
+ }
739
+ const events = pkg.systemEvents ?? {};
740
+ const declared = [...events.required ?? [], ...events.optional ?? []];
741
+ if (declared.includes("command.auth.login.request")) {
742
+ expect(/bridge\.(?:system\.)?requestLogin\s*\(/.test(scriptText), `${id} declares login but never asks the app to show it (bridge.requestLogin)`);
743
+ }
744
+ const caps = pkg.capabilities ?? [];
745
+ if (/supportsLogin\s*:\s*true/.test(scriptText)) expect(caps.includes("LOGIN"), `${id} script supports login but LOGIN capability is not declared`);
746
+ if (caps.includes("LOGIN")) expect(/supportsLogin\s*:\s*true/.test(scriptText), `${id} declares LOGIN but the script does not set supportsLogin: true`);
747
+ return problems;
748
+ }
749
+ function noTraversal(value2, label, expect) {
750
+ expect(typeof value2 === "string" && value2.length > 0, `${label} must be a non-empty string`);
751
+ expect(typeof value2 === "string" && !value2.split("/").includes(".."), `${label} must not traverse outside the repository root`);
752
+ }
753
+ function checkEvents(events, label, expect) {
754
+ const e = events ?? {};
755
+ expect(e.protocol === EVENTS_PROTOCOL, `${label} protocol`);
756
+ expect(e.minVersion === 1 && e.maxVersion === 1, `${label} version range`);
757
+ expect(Array.isArray(e.required) && Array.isArray(e.optional), `${label} event arrays`);
758
+ const required = new Set(e.required ?? []);
759
+ const optional = new Set(e.optional ?? []);
760
+ for (const id of required) expect(EVENT_IDS.includes(id), `${label} unknown required event ${id}`);
761
+ for (const id of optional) expect(EVENT_IDS.includes(id), `${label} unknown optional event ${id}`);
762
+ for (const id of required) expect(!optional.has(id), `${label} event is both required and optional: ${id}`);
763
+ }
764
+ function checkList(list, allowed, label, expect) {
765
+ expect(Array.isArray(list), `${label} must be an array`);
766
+ for (const p of list ?? []) expect(allowed.includes(p), `${label} contains unknown permission ${p}`);
767
+ }
768
+ function checkBrowserSessionOrigins(origins, label, expect) {
769
+ expect(Array.isArray(origins), `${label} must be an array`);
770
+ const list = origins ?? [];
771
+ expect(list.length <= 4, `${label} must remain bounded`);
772
+ for (const origin2 of list) {
773
+ try {
774
+ const parsed = new URL(origin2);
775
+ expect(parsed.protocol === "https:", `${label} must use HTTPS`);
776
+ expect(parsed.origin === origin2 && parsed.pathname === "/", `${label} must contain exact origins only`);
777
+ } catch {
778
+ expect(false, `${label} contains an invalid origin ${origin2}`);
779
+ }
780
+ }
781
+ }
782
+
783
+ // src/core/state.ts
784
+ import { existsSync, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "node:fs";
785
+ import { dirname, join as join2 } from "node:path";
786
+ var STATE_FORMAT = "shinsou-plugin-state-v1";
787
+ function statePath(projectDir) {
788
+ return join2(projectDir, ".shinsou", "state.json");
789
+ }
790
+ function emptyState() {
791
+ return {
792
+ format: STATE_FORMAT,
793
+ publisherId: null,
794
+ currentKey: null,
795
+ retiredKeys: [],
796
+ rotationSequence: 0,
797
+ keyRevoked: false,
798
+ revokedArtifacts: [],
799
+ documents: {}
800
+ };
801
+ }
802
+ function loadState(projectDir) {
803
+ const path = statePath(projectDir);
804
+ if (!existsSync(path)) return emptyState();
805
+ const s = JSON.parse(readFileSync2(path, "utf8"));
806
+ if (s.format !== STATE_FORMAT) throw new Error(`${path}: unsupported state format`);
807
+ return { ...emptyState(), ...s };
808
+ }
809
+ function saveState(projectDir, state) {
810
+ const path = statePath(projectDir);
811
+ mkdirSync(dirname(path), { recursive: true });
812
+ const tmp = path + ".tmp";
813
+ writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n");
814
+ renameSync(tmp, path);
815
+ }
816
+ function nextSequence(state, documentId, payload) {
817
+ const digest = sha256Hex(utf8(canonicalJson(payload)));
818
+ const current = state.documents[documentId];
819
+ if (current && current.payloadSha256 === digest) return current.sequence;
820
+ const sequence = current ? current.sequence + 1 : 1;
821
+ state.documents[documentId] = { sequence, payloadSha256: digest };
822
+ return sequence;
823
+ }
824
+
825
+ // src/core/build.ts
826
+ var DEFAULT_OUT_DIR = "build/repo";
827
+ var MAX_SCRIPT_BYTES = 2 * 1024 * 1024;
828
+ var BuildError = class extends Error {
829
+ };
830
+ async function bundleEntry(projectDir, manifest) {
831
+ const result = await esbuild({
832
+ absWorkingDir: projectDir,
833
+ entryPoints: [manifest.entry],
834
+ bundle: true,
835
+ write: false,
836
+ // ESM output without exports is a plain top-level script: `var source = {...}` stays global.
837
+ format: "esm",
838
+ // Nothing is "used" from the host's point of view; keep every top-level declaration.
839
+ treeShaking: false,
840
+ platform: "neutral",
841
+ target: "es2020",
842
+ charset: "utf8",
843
+ legalComments: "inline",
844
+ logLevel: "silent",
845
+ mainFields: ["module", "main"]
846
+ }).catch((err) => {
847
+ const msgs = (err.errors ?? []).map((e) => `${e.location ? `${e.location.file}:${e.location.line}: ` : ""}${e.text}`);
848
+ throw new BuildError(`esbuild failed:
849
+ ${msgs.join("\n ") || String(err)}`);
850
+ });
851
+ const code = result.outputFiles[0].text;
852
+ if (/^\s*export\s/m.test(code) || /^\s*import\s/m.test(code)) {
853
+ throw new BuildError(
854
+ `${manifest.entry} must not export anything: declare \`var source = {...}\` (or \`var sources = [...]\`) at top level instead`
855
+ );
856
+ }
857
+ if (!/^(?:var\s+sources?\b|globalThis\.sources?\s*=)/m.test(code)) {
858
+ throw new BuildError(
859
+ `${manifest.entry} must declare \`var source = {...}\` (or \`var sources = [...]\`) at top level; let/const do not become globals and the host looks the source up on the global object`
860
+ );
861
+ }
862
+ return code;
863
+ }
864
+ function assembleScript(manifest, bundled) {
865
+ const generated = executableMetadata(manifest);
866
+ const existing = readExecutableMetadata(bundled);
867
+ if (existing) {
868
+ if (!isDeepStrictEqual2(existing, generated)) {
869
+ throw new BuildError(
870
+ `The script's own __shinsouExtensionV2 declaration disagrees with shinsou.plugin.json.
871
+ script: ${JSON.stringify(existing)}
872
+ manifest: ${JSON.stringify(generated)}
873
+ Remove the declaration from the source; build generates it.`
874
+ );
875
+ }
876
+ }
877
+ const header = `/* ${manifest.packageId} ${manifest.version} (${manifest.versionCode}) \u2014 built by @shinsou/plugin from shinsou.plugin.json */
878
+ ` + (existing ? "" : `var __shinsouExtensionV2 = ${JSON.stringify(generated)};
879
+ `);
880
+ const hasExport = bundled.includes("source.v2 = __shinsouExtensionV2");
881
+ const footer = hasExport ? "" : '\n// Bounded, host-audited v2 metadata (generated).\nif (typeof source === "object" && source) {\n source.v2 = __shinsouExtensionV2;\n}\nif (typeof sources === "object" && sources) {\n (Array.isArray(sources) ? sources : Object.keys(sources).map(function (k) { return sources[k]; }))\n .forEach(function (s) { if (s && typeof s === "object") s.v2 = __shinsouExtensionV2; });\n}\n';
882
+ return header + bundled.replace(/\s*$/, "\n") + footer;
883
+ }
884
+ async function buildProject(projectDir, opts = {}) {
885
+ const manifest = loadManifest(projectDir);
886
+ const bundledCode = await bundleEntry(projectDir, manifest);
887
+ const script = assembleScript(manifest, bundledCode);
888
+ const scriptBytes = utf8(script);
889
+ if (scriptBytes.length > MAX_SCRIPT_BYTES) {
890
+ throw new BuildError(`Script is ${scriptBytes.length} bytes; the limit is ${MAX_SCRIPT_BYTES} (2 MiB)`);
891
+ }
892
+ const artifact = { sha256: sha256Hex(scriptBytes), byteSize: scriptBytes.length };
893
+ const pkg = indexEntry(manifest, artifact);
894
+ const index = indexDocument(manifest, [pkg]);
895
+ const sidecar = sidecarDocument(manifest, artifact);
896
+ const state = loadState(projectDir);
897
+ const repoMeta = repoMetaDocument(manifest, state.currentKey?.fingerprint ?? "");
898
+ const files = {
899
+ [scriptUrl(manifest)]: scriptBytes,
900
+ [sidecarUrl(manifest)]: JSON.stringify(sidecar, null, 2) + "\n",
901
+ "index.json": JSON.stringify(index, null, 2) + "\n",
902
+ "repo.json": JSON.stringify(repoMeta, null, 2) + "\n"
903
+ };
904
+ const reIndex = JSON.parse(files["index.json"]);
905
+ const reSidecar = JSON.parse(files[sidecarUrl(manifest)]);
906
+ const rePkg = reIndex.packages.find((p) => p.id === manifest.packageId);
907
+ const problems = checkPackageConsistency({ index: reIndex, pkg: rePkg, sidecar: reSidecar, scriptBytes });
908
+ if (problems.length) throw new BuildError(`Script metadata / index / sidecar disagree:
909
+ - ${problems.join("\n - ")}`);
910
+ const outDir = join3(projectDir, opts.outDir ?? DEFAULT_OUT_DIR);
911
+ if (opts.write !== false) {
912
+ for (const [rel, content] of Object.entries(files)) {
913
+ const target = join3(outDir, rel);
914
+ mkdirSync2(dirname2(target), { recursive: true });
915
+ writeFileSync2(target, content);
916
+ }
917
+ }
918
+ return { manifest, script, scriptBytes, bundledCode, ...artifact, index, pkg, sidecar, repoMeta, outDir, files };
919
+ }
920
+
921
+ // src/core/keystore.ts
922
+ import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
923
+ import { homedir } from "node:os";
924
+ import { join as join4 } from "node:path";
925
+ import { xchacha20poly1305 } from "@noble/ciphers/chacha.js";
926
+ import { scrypt } from "@noble/hashes/scrypt.js";
927
+ import { randomBytes } from "@noble/hashes/utils.js";
928
+ var KEY_FORMAT = "shinsou-plugin-key-v1";
929
+ var KEY_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
930
+ var DEFAULT_SCRYPT = { N: 2 ** 17, r: 8, p: 1 };
931
+ function configHome() {
932
+ return process.env.SHINSOU_PLUGIN_HOME ?? join4(process.env.XDG_CONFIG_HOME ?? join4(homedir(), ".config"), "shinsou-plugin");
933
+ }
934
+ function keyPath(name) {
935
+ if (!KEY_NAME.test(name)) throw new Error(`Invalid key name '${name}' (letters, digits, . _ -; up to 64)`);
936
+ return join4(configHome(), "keys", `${name}.json`);
937
+ }
938
+ function aad(fp) {
939
+ return utf8(`${KEY_FORMAT}:${fp}`);
940
+ }
941
+ function createKeyFile(args) {
942
+ const seed = args.seed ?? randomSeed();
943
+ const pk = publicKeyFromSeed(seed);
944
+ const fp = fingerprint(pk);
945
+ const file = {
946
+ format: KEY_FORMAT,
947
+ name: args.name,
948
+ algorithm: "Ed25519",
949
+ publicKey: base64UrlEncode(pk),
950
+ fingerprint: fp,
951
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
952
+ };
953
+ if (args.passphrase) {
954
+ const params = args.scryptParams ?? DEFAULT_SCRYPT;
955
+ const salt = randomBytes(16);
956
+ const nonce = randomBytes(24);
957
+ const key = scrypt(utf8(args.passphrase.normalize("NFC")), salt, { N: params.N, r: params.r, p: params.p, dkLen: 32 });
958
+ const ciphertext = xchacha20poly1305(key, nonce, aad(fp)).encrypt(seed);
959
+ file.encryption = {
960
+ kdf: "scrypt",
961
+ ...params,
962
+ salt: base64UrlEncode(salt),
963
+ cipher: "xchacha20poly1305",
964
+ nonce: base64UrlEncode(nonce),
965
+ ciphertext: base64UrlEncode(ciphertext)
966
+ };
967
+ } else {
968
+ file.seed = base64UrlEncode(seed);
969
+ }
970
+ return file;
971
+ }
972
+ function saveKeyFile(file, { overwrite = false } = {}) {
973
+ const path = keyPath(file.name);
974
+ if (existsSync2(path) && !overwrite) throw new Error(`Key '${file.name}' already exists at ${path}`);
975
+ const dir = join4(configHome(), "keys");
976
+ mkdirSync3(dir, { recursive: true, mode: 448 });
977
+ chmodSync(dir, 448);
978
+ writeFileSync3(path, JSON.stringify(file, null, 2) + "\n", { mode: 384 });
979
+ chmodSync(path, 384);
980
+ return path;
981
+ }
982
+ function readKeyFile(name) {
983
+ const envKey = process.env.SHINSOU_PLUGIN_KEY;
984
+ if (envKey) {
985
+ const file2 = JSON.parse(envKey);
986
+ if (file2.format !== KEY_FORMAT) throw new Error("SHINSOU_PLUGIN_KEY is not a shinsou-plugin key file");
987
+ if (file2.name === name) return file2;
988
+ }
989
+ const path = keyPath(name);
990
+ if (!existsSync2(path)) throw new Error(`Key '${name}' not found (${path}). Run: shinsou-plugin keygen --name ${name}`);
991
+ const file = JSON.parse(readFileSync3(path, "utf8"));
992
+ if (file.format !== KEY_FORMAT) throw new Error(`${path} is not a shinsou-plugin key file`);
993
+ return file;
994
+ }
995
+ function unlockKeyFile(file, passphrase) {
996
+ let seed;
997
+ if (file.encryption) {
998
+ if (!passphrase) throw new Error(`Key '${file.name}' is encrypted: a passphrase is required`);
999
+ const e = file.encryption;
1000
+ if (e.kdf !== "scrypt" || e.cipher !== "xchacha20poly1305") throw new Error("Unsupported key encryption");
1001
+ const key = scrypt(utf8(passphrase.normalize("NFC")), base64UrlDecodeCanonical(e.salt), { N: e.N, r: e.r, p: e.p, dkLen: 32 });
1002
+ try {
1003
+ seed = xchacha20poly1305(key, base64UrlDecodeCanonical(e.nonce), aad(file.fingerprint)).decrypt(base64UrlDecodeCanonical(e.ciphertext));
1004
+ } catch {
1005
+ throw new Error(`Wrong passphrase for key '${file.name}'`);
1006
+ }
1007
+ } else if (file.seed) {
1008
+ seed = base64UrlDecodeCanonical(file.seed, 32);
1009
+ } else {
1010
+ throw new Error(`Key file '${file.name}' has no seed`);
1011
+ }
1012
+ const pk = publicKeyFromSeed(seed);
1013
+ if (fingerprint(pk) !== file.fingerprint || base64UrlEncode(pk) !== file.publicKey) {
1014
+ throw new Error(`Key file '${file.name}' is corrupt: seed does not match its public key`);
1015
+ }
1016
+ return { name: file.name, seed, publicKey: file.publicKey, fingerprint: file.fingerprint };
1017
+ }
1018
+ function describeKey(file) {
1019
+ return { fingerprint: file.fingerprint, display: displayFingerprint(base64UrlDecodeCanonical(file.publicKey, 32)) };
1020
+ }
1021
+
1022
+ // src/core/lint.ts
1023
+ import { isIP } from "node:net";
1024
+
1025
+ // src/core/names.ts
1026
+ var BLOCKED_TERMS = [
1027
+ "official",
1028
+ "offical",
1029
+ "oficial",
1030
+ "officiel",
1031
+ "offiziell",
1032
+ "\u5B98\u65B9",
1033
+ "\u5B98\u7DB2",
1034
+ "\u5B98\u7F51",
1035
+ "\u516C\u5F0F"
1036
+ // Japanese 「公式」 = official
1037
+ ];
1038
+ var DISCLAIMERS = ["unofficial", "unoffical", "nonofficial", "\u975E\u5B98\u65B9", "\u975E\u516C\u5F0F"];
1039
+ var LOOKALIKES = { "0": "o", "1": "i", "!": "i", "|": "i", "$": "s", "3": "e", "@": "a" };
1040
+ function normalizeForNameCheck(value2) {
1041
+ let s = value2.normalize("NFKC").normalize("NFKD").replace(new RegExp("\\p{M}", "gu"), "");
1042
+ s = s.toLowerCase();
1043
+ s = s.replace(/[\u200b-\u200f\u2060\ufeff\u00ad]/g, "");
1044
+ s = s.replace(/[\s_\-.\u00b7\u30fb\uff65]+/g, "");
1045
+ return [...s].map((c) => LOOKALIKES[c] ?? c).join("");
1046
+ }
1047
+ function misleadingTerm(value2) {
1048
+ let n = normalizeForNameCheck(value2);
1049
+ for (const d of DISCLAIMERS) n = n.split(normalizeForNameCheck(d)).join(" ");
1050
+ for (const term of BLOCKED_TERMS) {
1051
+ if (n.includes(normalizeForNameCheck(term))) return term;
1052
+ }
1053
+ if (/of+i?ci?al/.test(n)) return "official";
1054
+ return null;
1055
+ }
1056
+
1057
+ // src/core/psl.ts
1058
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
1059
+ import { join as join5 } from "node:path";
1060
+ import { fileURLToPath } from "node:url";
1061
+ import { domainToASCII } from "node:url";
1062
+ function dataDir() {
1063
+ const here = fileURLToPath(new URL(".", import.meta.url));
1064
+ for (const candidate of [join5(here, "..", "data"), join5(here, "..", "..", "data")]) {
1065
+ if (existsSync3(join5(candidate, "public_suffix_list.dat"))) return candidate;
1066
+ }
1067
+ throw new Error("data/public_suffix_list.dat not found next to the CLI");
1068
+ }
1069
+ var cached;
1070
+ function parsePublicSuffixList(text) {
1071
+ const normal = /* @__PURE__ */ new Set();
1072
+ const wildcards = /* @__PURE__ */ new Set();
1073
+ const exceptions = /* @__PURE__ */ new Set();
1074
+ for (const line of text.split(/\r?\n/)) {
1075
+ const rule = line.split(/[ \t]/)[0].trim();
1076
+ if (!rule || rule.startsWith("//")) continue;
1077
+ const prefix = rule.startsWith("!") ? "!" : rule.startsWith("*.") ? "*." : "";
1078
+ const ascii = domainToASCII(rule.slice(prefix.length)).toLowerCase();
1079
+ if (!/^[a-z0-9.-]+$/.test(ascii)) throw new Error(`Unexpected public suffix rule '${rule}'`);
1080
+ if (prefix === "!") exceptions.add(ascii);
1081
+ else if (prefix === "*.") wildcards.add(ascii);
1082
+ else normal.add(ascii);
1083
+ }
1084
+ if (normal.size + wildcards.size + exceptions.size < 5e3) throw new Error("Public Suffix List looks truncated");
1085
+ return { normal, wildcards, exceptions };
1086
+ }
1087
+ function rules() {
1088
+ return cached ??= parsePublicSuffixList(readFileSync4(join5(dataDir(), "public_suffix_list.dat"), "utf8"));
1089
+ }
1090
+ function normalizedLabels(host) {
1091
+ const n = host.trim().replace(/\.+$/, "").toLowerCase();
1092
+ if (!n || n.length > 253 || !/^[a-z0-9.-]+$/.test(n)) return null;
1093
+ const labels = n.split(".");
1094
+ if (labels.some((l) => !l || l.length > 63)) return null;
1095
+ return labels;
1096
+ }
1097
+ function publicSuffixOf(host) {
1098
+ const labels = normalizedLabels(host);
1099
+ if (!labels) return null;
1100
+ const r = rules();
1101
+ for (let start = 0; start < labels.length; start++) {
1102
+ const candidate = labels.slice(start).join(".");
1103
+ if (r.exceptions.has(candidate)) return labels.slice(start + 1).join(".");
1104
+ if (r.normal.has(candidate)) return candidate;
1105
+ if (start + 1 < labels.length && r.wildcards.has(labels.slice(start + 1).join("."))) return candidate;
1106
+ }
1107
+ return labels[labels.length - 1];
1108
+ }
1109
+ function registrableDomain(host) {
1110
+ const labels = normalizedLabels(host);
1111
+ const suffix = publicSuffixOf(host);
1112
+ if (!labels || !suffix) return null;
1113
+ const suffixLabels = suffix.split(".").length;
1114
+ if (labels.length <= suffixLabels) return null;
1115
+ return labels.slice(labels.length - suffixLabels - 1).join(".");
1116
+ }
1117
+
1118
+ // src/core/lint.ts
1119
+ var BRIDGE_OPS = {
1120
+ httpGet: { runtime: ["NETWORK"] },
1121
+ httpGetWithHeaders: { runtime: ["NETWORK"] },
1122
+ httpGetResponse: { runtime: ["NETWORK"] },
1123
+ httpPost: { runtime: ["NETWORK"] },
1124
+ httpPostResponse: { runtime: ["NETWORK"] },
1125
+ httpPostBatch: { runtime: ["NETWORK"] },
1126
+ "net.fetch": { runtime: ["NETWORK"] },
1127
+ browserSessionRequest: { runtime: ["BROWSER_CHALLENGE"] },
1128
+ getCredentialUsername: { runtime: ["CREDENTIAL_ACCESS"] },
1129
+ hasCredential: { runtime: ["CREDENTIAL_ACCESS"] },
1130
+ getCredentialPassword: { runtime: ["CREDENTIAL_ACCESS"] },
1131
+ setCredential: { runtime: ["CREDENTIAL_ACCESS"] },
1132
+ clearCredential: { runtime: ["CREDENTIAL_ACCESS"] },
1133
+ getCookie: { runtime: ["COOKIE_STORAGE", "COOKIE_READ"] },
1134
+ getCookies: { runtime: ["COOKIE_STORAGE", "COOKIE_READ"] },
1135
+ setCookie: { runtime: ["COOKIE_STORAGE"] },
1136
+ deleteCookie: { runtime: ["COOKIE_STORAGE"] },
1137
+ clearCookies: { runtime: ["COOKIE_STORAGE"] },
1138
+ requestLogin: { runtime: ["LOGIN_PROMPT"], host: ["REQUEST_LOGIN_UI"] },
1139
+ "system.requestLogin": { host: ["REQUEST_LOGIN_UI"], events: ["command.auth.login.request"] },
1140
+ "system.requestRefresh": { host: ["REQUEST_SOURCE_REFRESH"], events: ["command.source.refresh.request"] },
1141
+ "system.requestLogout": { host: ["REQUEST_LOGOUT"], events: ["command.auth.logout.request"] },
1142
+ "system.reportMessage": { host: ["REPORT_DIAGNOSTIC"], events: ["event.diagnostic.message.report"] },
1143
+ "system.submit": {},
1144
+ requestHostEvent: {},
1145
+ getHostEventContext: {},
1146
+ getHostEventCapabilities: {},
1147
+ getPreference: {},
1148
+ setPreference: {},
1149
+ log: {},
1150
+ parseHtml: {},
1151
+ domRelease: {},
1152
+ domReleaseAll: {}
1153
+ };
1154
+ var EVENT_HOST_PERMISSION = {
1155
+ "command.auth.login.request": "REQUEST_LOGIN_UI",
1156
+ "command.source.refresh.request": "REQUEST_SOURCE_REFRESH",
1157
+ "command.auth.logout.request": "REQUEST_LOGOUT",
1158
+ "event.diagnostic.message.report": "REPORT_DIAGNOSTIC"
1159
+ };
1160
+ var CAPABILITY_HOOKS = {
1161
+ CATALOGUE: "getPopularManga",
1162
+ LATEST: "getLatestUpdates",
1163
+ SEARCH: "getSearchManga",
1164
+ METADATA: "getMangaDetails",
1165
+ UNITS: "getChapterList",
1166
+ CONTENT: "getPageList"
1167
+ };
1168
+ function usedBridgeOps(code) {
1169
+ const ops = /* @__PURE__ */ new Set();
1170
+ for (const m of code.matchAll(/\bbridge\s*\.\s*(system|net)\s*\.\s*([A-Za-z_$][\w$]*)\s*\(/g)) ops.add(`${m[1]}.${m[2]}`);
1171
+ for (const m of code.matchAll(/\bbridge\s*\.\s*([A-Za-z_$][\w$]*)\s*\(/g)) ops.add(m[1]);
1172
+ return ops;
1173
+ }
1174
+ function lintBuild(result) {
1175
+ const report = { errors: [], warnings: [] };
1176
+ const err = (rule, message) => report.errors.push({ rule, message });
1177
+ const warn = (rule, message) => report.warnings.push({ rule, message });
1178
+ const m = result.manifest;
1179
+ const code = result.bundledCode;
1180
+ const names = [
1181
+ ["packageId", m.packageId],
1182
+ ["name", m.name],
1183
+ ["repository.name", m.repository.name],
1184
+ ["repository.shortName", m.repository.shortName],
1185
+ ...m.sources.flatMap((s, i) => [[`sources[${i}].name`, s.name], [`sources[${i}].sourceId`, s.sourceId]])
1186
+ ];
1187
+ for (const [field, value2] of names) {
1188
+ const term = misleadingTerm(value2);
1189
+ if (term) err("misleading-name", `${field} "${value2}" contains the misleading word "${term}": Shinsou has no official plugins`);
1190
+ }
1191
+ if (result.byteSize > MAX_SCRIPT_BYTES) err("script-size", `script is ${result.byteSize} bytes (limit 2 MiB)`);
1192
+ if (!m.runtimePermissions.includes("EXECUTE_SCRIPT")) err("permissions", "runtimePermissions must include EXECUTE_SCRIPT");
1193
+ const ops = usedBridgeOps(code);
1194
+ const neededRuntime = /* @__PURE__ */ new Set(["EXECUTE_SCRIPT"]);
1195
+ const neededHost = /* @__PURE__ */ new Set();
1196
+ const declaredEvents = /* @__PURE__ */ new Set([...m.systemEvents.required, ...m.systemEvents.optional]);
1197
+ for (const op of ops) {
1198
+ const rule = BRIDGE_OPS[op];
1199
+ if (!rule) {
1200
+ err("bridge-op", `bridge.${op}() is not a bridge operation the sandbox provides (it would fail with UNKNOWN_OPERATION)`);
1201
+ continue;
1202
+ }
1203
+ for (const p of rule.runtime ?? []) {
1204
+ neededRuntime.add(p);
1205
+ if (!m.runtimePermissions.includes(p)) err("bridge-permission", `bridge.${op}() requires runtime permission ${p}`);
1206
+ }
1207
+ for (const p of rule.host ?? []) {
1208
+ neededHost.add(p);
1209
+ if (!m.requestedHostPermissions.includes(p)) err("bridge-permission", `bridge.${op}() requires host permission ${p}`);
1210
+ }
1211
+ for (const e of rule.events ?? []) {
1212
+ if (!declaredEvents.has(e)) err("bridge-permission", `bridge.${op}() requires systemEvents to declare ${e}`);
1213
+ }
1214
+ if (op === "system.submit" || op === "requestHostEvent") {
1215
+ warn("bridge-dynamic", `bridge.${op}() submits arbitrary events: make sure systemEvents and requestedHostPermissions cover them`);
1216
+ }
1217
+ }
1218
+ if (/\bbridge\s*\[/.test(code)) warn("bridge-dynamic", "bridge[...] dynamic access cannot be checked statically");
1219
+ for (const p of m.runtimePermissions) {
1220
+ if (!neededRuntime.has(p) && p !== "FAVORITE_MUTATION") warn("least-privilege", `runtime permission ${p} is declared but no bridge call needs it`);
1221
+ }
1222
+ for (const e of declaredEvents) {
1223
+ const p = EVENT_HOST_PERMISSION[e];
1224
+ if (p && !m.requestedHostPermissions.includes(p)) err("events", `systemEvents declares ${e}, which needs host permission ${p}`);
1225
+ }
1226
+ const usedHost = /* @__PURE__ */ new Set([...neededHost, ...[...declaredEvents].map((e) => EVENT_HOST_PERMISSION[e]).filter(Boolean)]);
1227
+ for (const p of m.requestedHostPermissions) {
1228
+ if (TIER_CEILING_HOST_PERMISSIONS.includes(p)) err("tier-ceiling", `${p} is never granted to third-party packages (ADR-0002 \xA76.2)`);
1229
+ else if (!usedHost.has(p)) warn("least-privilege", `host permission ${p} is declared but nothing uses it`);
1230
+ }
1231
+ for (const cap of m.capabilities) {
1232
+ const hook = CAPABILITY_HOOKS[cap];
1233
+ const found = (h) => new RegExp(`\\b${h}\\b`).test(code);
1234
+ if (hook && !found(hook) && !(cap === "CONTENT" && found("getChapterText"))) warn("capabilities", `capability ${cap} is declared but no ${hook} hook was found`);
1235
+ }
1236
+ if (m.capabilities.includes("LOGIN") && !m.runtimePermissions.includes("CREDENTIAL_ACCESS")) {
1237
+ warn("capabilities", "LOGIN normally needs CREDENTIAL_ACCESS to store the account");
1238
+ }
1239
+ for (const [i, s] of m.sources.entries()) {
1240
+ const kinds = s.contentKinds ?? m.contentKinds;
1241
+ if (kinds.includes("PLAIN_TEXT") && !/\bgetChapterText\b/.test(code)) {
1242
+ warn("capabilities", `sources[${i}] serves PLAIN_TEXT but no getChapterText hook was found (the app reads chapter text from getChapterText(chapter), which returns a string)`);
1243
+ }
1244
+ }
1245
+ for (const [i, s] of m.sources.entries()) lintSource(m, s, `sources[${i}]`, code, err, warn);
1246
+ lintUrlLiterals(m, code, warn);
1247
+ return report;
1248
+ }
1249
+ function lintSource(m, s, label, code, err, warn) {
1250
+ const isLong = (v) => /^-?\d{1,19}$/.test(v) && BigInt(v) <= 0x7fffffffffffffffn && BigInt(v) >= -0x8000000000000000n;
1251
+ if (/^\d{1,19}$/.test(s.sourceId)) {
1252
+ if (!isLong(s.sourceId)) err("source-id", `${label}.sourceId must fit a 64-bit integer (the app stores it as a Long)`);
1253
+ if (s.legacyLongId !== null && s.legacyLongId !== s.sourceId) warn("source-id", `${label}.legacyLongId differs from sourceId`);
1254
+ } else if (/^[a-z0-9][a-z0-9._-]{0,127}$/.test(s.sourceId)) {
1255
+ if (s.legacyLongId === null || !isLong(s.legacyLongId)) {
1256
+ err("source-id", `${label}.sourceId "${s.sourceId}" is not numeric, so legacyLongId must be a 64-bit integer string`);
1257
+ }
1258
+ } else {
1259
+ err("source-id", `${label}.sourceId must be a decimal 64-bit integer or a lowercase dotted id such as zh.example`);
1260
+ }
1261
+ for (const field of ["requestOrigins", "credentialOrigins", "contentOrigins", "browserSessionOrigins"]) {
1262
+ for (const origin2 of s[field]) {
1263
+ const problem = exactHttpsOriginProblem(origin2);
1264
+ if (problem) err("origin", `${label}.${field}: ${origin2} ${problem}`);
1265
+ }
1266
+ }
1267
+ const base = exactHttpsOriginProblem(safeOrigin(s.baseUrl) ?? s.baseUrl);
1268
+ if (base) err("origin", `${label}.baseUrl ${s.baseUrl} ${base}`);
1269
+ const baseOrigin = safeOrigin(s.baseUrl);
1270
+ if (m.runtimePermissions.includes("NETWORK") && s.requestOrigins.length === 0) {
1271
+ err("origin", `${label}.requestOrigins is empty: the app denies every request of a v2 source without declared origins`);
1272
+ }
1273
+ if (baseOrigin && s.requestOrigins.length && !s.requestOrigins.includes(baseOrigin) && !s.contentOrigins.includes(baseOrigin)) {
1274
+ warn("origin", `${label}.baseUrl origin ${baseOrigin} is neither a request nor a content origin`);
1275
+ }
1276
+ if (s.requestOrigins.length && !m.runtimePermissions.includes("NETWORK")) {
1277
+ warn("origin", `${label}.requestOrigins is declared but NETWORK is not requested`);
1278
+ }
1279
+ for (const o of s.credentialOrigins) {
1280
+ if (!s.requestOrigins.includes(o)) err("origin", `${label}.credentialOrigins ${o} must also be a request origin`);
1281
+ }
1282
+ if (s.browserSessionOrigins.length > 4) err("origin", `${label}.browserSessionOrigins allows at most 4 origins`);
1283
+ if (s.browserSessionOrigins.length && !m.runtimePermissions.includes("BROWSER_CHALLENGE")) {
1284
+ err("origin", `${label}.browserSessionOrigins needs runtime permission BROWSER_CHALLENGE`);
1285
+ }
1286
+ if (/\bbridge\s*\.\s*browserSessionRequest\s*\(/.test(code) && s.browserSessionOrigins.length === 0) {
1287
+ err("origin", `${label}: browserSessionRequest is used but browserSessionOrigins is empty`);
1288
+ }
1289
+ for (const suffix of s.contentHostSuffixes) {
1290
+ const problem = contentHostSuffixProblem(suffix);
1291
+ if (problem) err("content-host-suffix", `${label}.contentHostSuffixes ${suffix}: ${problem}`);
1292
+ }
1293
+ if (s.contentHostSuffixes.length > MAX_CONTENT_HOST_SUFFIXES) {
1294
+ err("content-host-suffix", `${label}.contentHostSuffixes allows at most ${MAX_CONTENT_HOST_SUFFIXES} entries`);
1295
+ }
1296
+ if (s.browserImageOrigins.length > 4) err("origin", `${label}.browserImageOrigins allows at most 4 origins`);
1297
+ for (const origin2 of s.browserImageOrigins) {
1298
+ const problem = exactHttpsOriginProblem(origin2);
1299
+ if (problem) err("origin", `${label}.browserImageOrigins: ${origin2} ${problem}`);
1300
+ else if (/:\d+$/.test(origin2)) err("origin", `${label}.browserImageOrigins: ${origin2} must use the default port 443`);
1301
+ if (!s.contentOrigins.includes(origin2)) err("origin", `${label}.browserImageOrigins ${origin2} must also be a content origin`);
1302
+ }
1303
+ if (s.browserImageOrigins.length && !m.runtimePermissions.includes("BROWSER_IMAGES")) {
1304
+ err("origin", `${label}.browserImageOrigins needs runtime permission BROWSER_IMAGES`);
1305
+ }
1306
+ if (/\bsetFavorite\s*[:=(]/.test(code) && !m.runtimePermissions.includes("FAVORITE_MUTATION")) {
1307
+ warn("permission", `${label}: setFavorite is defined but FAVORITE_MUTATION is not requested, so the app will refuse it`);
1308
+ }
1309
+ }
1310
+ function lintUrlLiterals(m, code, warn) {
1311
+ const allowed = new Set(m.sources.flatMap((s) => [...s.requestOrigins, ...s.contentOrigins, ...s.browserSessionOrigins]));
1312
+ const seen = /* @__PURE__ */ new Set();
1313
+ for (const match of code.matchAll(/\bhttps?:\/\/[A-Za-z0-9.-]+(?::\d+)?/g)) {
1314
+ const origin2 = safeOrigin(match[0]);
1315
+ if (!origin2 || seen.has(origin2)) continue;
1316
+ seen.add(origin2);
1317
+ if (origin2.startsWith("http:")) warn("origin", `script contains a plain-HTTP URL (${origin2}); third-party packages may only use HTTPS`);
1318
+ else if (!allowed.has(origin2)) warn("origin", `script mentions ${origin2}, which is not a declared request/content origin`);
1319
+ }
1320
+ }
1321
+ function safeOrigin(url) {
1322
+ try {
1323
+ return new URL(url).origin;
1324
+ } catch {
1325
+ return null;
1326
+ }
1327
+ }
1328
+ function exactHttpsOriginProblem(origin2) {
1329
+ let u;
1330
+ try {
1331
+ u = new URL(origin2);
1332
+ } catch {
1333
+ return "is not a valid URL";
1334
+ }
1335
+ if (u.protocol !== "https:") return "must use HTTPS";
1336
+ if (u.origin !== origin2) return `must be an exact origin (expected ${u.origin})`;
1337
+ const host = u.hostname.replace(/^\[|\]$/g, "").toLowerCase();
1338
+ if (isIP(host)) return "must not be an IP address";
1339
+ if (host === "localhost" || /\.(localhost|local|internal|lan|home|test|invalid|example)$/.test(host) || !host.includes(".")) {
1340
+ return "must be a public host name (private, loopback and LAN hosts are never granted)";
1341
+ }
1342
+ return null;
1343
+ }
1344
+ var CONTENT_HOST_SUFFIX_HIGH_PORTS = ":1024-65535";
1345
+ var MAX_CONTENT_HOST_SUFFIXES = 8;
1346
+ function contentHostSuffixProblem(declared) {
1347
+ const s = declared.endsWith(CONTENT_HOST_SUFFIX_HIGH_PORTS) ? declared.slice(0, -CONTENT_HOST_SUFFIX_HIGH_PORTS.length) : declared;
1348
+ if (s !== s.toLowerCase()) return "must be lower-case";
1349
+ if (s.startsWith(".") || s.startsWith("*")) return "write the bare suffix (example.com), without a leading dot or wildcard";
1350
+ if (s.endsWith(".")) return "must not end with a dot";
1351
+ if (s.length > 253 || !/^[a-z0-9.-]+$/.test(s)) {
1352
+ return `must be lower-case ASCII host name characters; the only allowed port suffix is ${CONTENT_HOST_SUFFIX_HIGH_PORTS}`;
1353
+ }
1354
+ const labels = s.split(".");
1355
+ if (labels.length < 2 || labels.some((l) => !l || l.length > 63 || l.startsWith("-") || l.endsWith("-"))) {
1356
+ return "is not a valid host name with at least two labels";
1357
+ }
1358
+ if (/^\d+$/.test(labels[labels.length - 1]) || isIP(s)) return "must not be an IP address";
1359
+ if (registrableDomain(s) === null) return "is a public suffix; it must be at least a registrable domain (e.g. cdn.example.com)";
1360
+ return null;
1361
+ }
1362
+
1363
+ // src/core/publish.ts
1364
+ import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
1365
+ import { dirname as dirname3, join as join6, resolve } from "node:path";
1366
+
1367
+ // src/core/envelope.ts
1368
+ var ENVELOPE_FORMAT = "shinsou-signed-envelope-v1";
1369
+ var SIGNATURE_DOMAIN = "dev.shinsou.repository.signature.v1\0";
1370
+ var DOCUMENT_IDS = {
1371
+ repo: "repo.json",
1372
+ index: "index.json",
1373
+ legacyIndex: "index.min.json",
1374
+ rotation: "publisher-key-rotation.json",
1375
+ revocations: "publisher-revocations.json",
1376
+ keyRevoked: "publisher-key-revoked.json"
1377
+ };
1378
+ var ENVELOPE_FIELDS = ["format", "algorithm", "keyFingerprint", "publicKey", "payloadType", "sequence", "payload", "signature"];
1379
+ var TrustError = class extends Error {
1380
+ constructor(category, message) {
1381
+ super(`${category}: ${message}`);
1382
+ this.category = category;
1383
+ }
1384
+ category;
1385
+ };
1386
+ function normalizeRepositoryUrl(value2, { allowHttp = false } = {}) {
1387
+ const trimmed = value2.trim().replace(/\/+$/, "");
1388
+ if (trimmed.includes("?") || trimmed.includes("#")) throw new Error(`Repository URL must not have a query or fragment: ${value2}`);
1389
+ let parsed;
1390
+ try {
1391
+ parsed = new URL(trimmed);
1392
+ } catch {
1393
+ throw new Error(`Invalid repository URL: ${value2}`);
1394
+ }
1395
+ if (parsed.protocol !== "https:" && !(allowHttp && parsed.protocol === "http:")) {
1396
+ throw new Error(`Repository URL must use HTTPS: ${value2}`);
1397
+ }
1398
+ if (parsed.username || parsed.password) throw new Error(`Repository URL must not contain credentials: ${value2}`);
1399
+ const path = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/+$/, "");
1400
+ if (path.split("/").some((s) => s === "." || s === "..")) throw new Error(`Repository URL must not contain dot segments: ${value2}`);
1401
+ return parsed.origin + path;
1402
+ }
1403
+ function signingMessage(args) {
1404
+ if (!Number.isSafeInteger(args.sequence) || args.sequence < 0) throw new Error("Repository sequence must be non-negative");
1405
+ if (!args.documentId.trim() || args.documentId.length > 512) throw new Error("Invalid repository document id");
1406
+ const statement = {
1407
+ algorithm: "Ed25519",
1408
+ documentId: args.documentId,
1409
+ format: ENVELOPE_FORMAT,
1410
+ keyFingerprint: args.keyFingerprint,
1411
+ payload: args.payload,
1412
+ payloadType: args.payloadType,
1413
+ repository: args.repository,
1414
+ sequence: args.sequence
1415
+ };
1416
+ return concatBytes(utf8(SIGNATURE_DOMAIN), utf8(canonicalJson(statement)));
1417
+ }
1418
+ function signEnvelope(args) {
1419
+ const publicKey = publicKeyFromSeed(args.seed);
1420
+ const keyFingerprint = fingerprint(publicKey);
1421
+ if (args.payloadType === "repository-meta") {
1422
+ const advertised = metaFingerprint(args.payload);
1423
+ if (advertised !== keyFingerprint) {
1424
+ throw new Error("repo.json meta.signingKeyFingerprint must equal the signing key fingerprint");
1425
+ }
1426
+ }
1427
+ const message = signingMessage({ ...args, keyFingerprint });
1428
+ return {
1429
+ format: ENVELOPE_FORMAT,
1430
+ algorithm: "Ed25519",
1431
+ keyFingerprint,
1432
+ publicKey: base64UrlEncode(publicKey),
1433
+ payloadType: args.payloadType,
1434
+ sequence: args.sequence,
1435
+ payload: args.payload,
1436
+ signature: base64UrlEncode(signEd25519(message, args.seed))
1437
+ };
1438
+ }
1439
+ function metaFingerprint(payload) {
1440
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return void 0;
1441
+ const meta = payload.meta;
1442
+ if (!meta || typeof meta !== "object" || Array.isArray(meta)) return void 0;
1443
+ const fp = meta.signingKeyFingerprint;
1444
+ return typeof fp === "string" ? fp : void 0;
1445
+ }
1446
+ function verifyEnvelope(args) {
1447
+ const env = args.envelope;
1448
+ const malformed = (msg) => {
1449
+ throw new TrustError("Malformed", msg);
1450
+ };
1451
+ if (!env || typeof env !== "object" || Array.isArray(env)) return malformed("Envelope is not an object");
1452
+ const e = env;
1453
+ const keys = Object.keys(e);
1454
+ if (keys.length !== ENVELOPE_FIELDS.length || !ENVELOPE_FIELDS.every((k) => keys.includes(k))) {
1455
+ malformed("Unknown or missing signed-envelope field");
1456
+ }
1457
+ const str = (k) => {
1458
+ const v = e[k];
1459
+ if (typeof v !== "string" || !v.trim()) malformed(`Missing '${k}'`);
1460
+ return v;
1461
+ };
1462
+ if (str("format") !== ENVELOPE_FORMAT) malformed("Unexpected envelope format");
1463
+ if (str("algorithm") !== "Ed25519") malformed("Unexpected algorithm");
1464
+ if (str("payloadType") !== args.payloadType) malformed("Unexpected payload type");
1465
+ const sequence = e.sequence;
1466
+ if (typeof sequence !== "number" || !Number.isSafeInteger(sequence)) malformed("Missing signed-envelope sequence");
1467
+ if (sequence < 0) malformed("Repository sequence must be non-negative");
1468
+ let wirePublicKey, wireSignature, pinnedPublicKey;
1469
+ try {
1470
+ wirePublicKey = base64UrlDecodeCanonical(str("publicKey"), 32);
1471
+ wireSignature = base64UrlDecodeCanonical(str("signature"), 64);
1472
+ pinnedPublicKey = base64UrlDecodeCanonical(args.pinned.publicKey, 32);
1473
+ } catch (err) {
1474
+ if (err instanceof TrustError) throw err;
1475
+ return malformed(err.message);
1476
+ }
1477
+ const wireFingerprint = str("keyFingerprint");
1478
+ const calculated = fingerprint(pinnedPublicKey);
1479
+ if (args.pinned.fingerprint !== calculated || wireFingerprint !== calculated || !bytesEqual(wirePublicKey, pinnedPublicKey)) {
1480
+ throw new TrustError("KeySubstitution", "Repository signing key does not match its pinned trust root");
1481
+ }
1482
+ const payload = e.payload;
1483
+ if (args.payloadType === "repository-meta" && metaFingerprint(payload) !== wireFingerprint) {
1484
+ malformed("Repository metadata signing fingerprint does not match the authenticated key");
1485
+ }
1486
+ let message;
1487
+ try {
1488
+ message = signingMessage({
1489
+ repository: args.repository,
1490
+ payloadType: args.payloadType,
1491
+ documentId: args.documentId,
1492
+ sequence,
1493
+ keyFingerprint: wireFingerprint,
1494
+ payload
1495
+ });
1496
+ } catch (err) {
1497
+ return malformed(err.message);
1498
+ }
1499
+ if (!verifyEd25519(wireSignature, message, pinnedPublicKey)) {
1500
+ throw new TrustError("InvalidSignature", "Invalid repository author signature");
1501
+ }
1502
+ return { payload, sequence };
1503
+ }
1504
+
1505
+ // src/core/publisher.ts
1506
+ var ROTATION_DOMAIN = "dev.shinsou.publisher.rotation.v1\0";
1507
+ var SHA256_HEX = /^[0-9a-f]{64}$/;
1508
+ var MAX_REVOKED_ARTIFACTS = 4096;
1509
+ var DEFAULT_REVOCATIONS_TTL_SECONDS = 30 * 24 * 60 * 60;
1510
+ function rotationStatement(args) {
1511
+ if (!Number.isSafeInteger(args.effectiveSequence) || args.effectiveSequence < 0) {
1512
+ throw new Error("effectiveSequence must be a non-negative integer");
1513
+ }
1514
+ const body = {
1515
+ effectiveSequence: args.effectiveSequence,
1516
+ newKeyFingerprint: args.newKeyFingerprint,
1517
+ newPublicKey: args.newPublicKey,
1518
+ oldKeyFingerprint: args.oldKeyFingerprint,
1519
+ publisherId: args.publisherId
1520
+ };
1521
+ return concatBytes(utf8(ROTATION_DOMAIN), utf8(canonicalJson(body)));
1522
+ }
1523
+ function rotationPayload(args) {
1524
+ const newPublicKeyBytes = publicKeyFromSeed(args.newSeed);
1525
+ const newPublicKey = base64UrlEncode(newPublicKeyBytes);
1526
+ const newKeyFingerprint = fingerprint(newPublicKeyBytes);
1527
+ const statement = rotationStatement({
1528
+ publisherId: args.publisherId,
1529
+ oldKeyFingerprint: args.oldKeyFingerprint,
1530
+ newPublicKey,
1531
+ newKeyFingerprint,
1532
+ effectiveSequence: args.effectiveSequence
1533
+ });
1534
+ return {
1535
+ publisherId: args.publisherId,
1536
+ newPublicKey,
1537
+ newKeyFingerprint,
1538
+ effectiveSequence: args.effectiveSequence,
1539
+ newKeySignature: base64UrlEncode(signEd25519(statement, args.newSeed))
1540
+ };
1541
+ }
1542
+ function revocationsPayload(args) {
1543
+ const artifacts = [...new Set(args.revokedArtifacts)];
1544
+ for (const a of artifacts) if (!SHA256_HEX.test(a)) throw new Error(`Revoked artifact must be lowercase SHA-256 hex: ${a}`);
1545
+ if (artifacts.length > MAX_REVOKED_ARTIFACTS) throw new Error(`At most ${MAX_REVOKED_ARTIFACTS} revoked artifacts`);
1546
+ if (!Number.isSafeInteger(args.expiresAt) || args.expiresAt <= 0) throw new Error("expiresAt must be Unix seconds");
1547
+ return { publisherId: args.publisherId, revokedArtifacts: artifacts, expiresAt: args.expiresAt };
1548
+ }
1549
+ function keyRevokedPayload(args) {
1550
+ return { publisherId: args.publisherId, revokedKeyFingerprint: args.revokedKeyFingerprint };
1551
+ }
1552
+
1553
+ // src/core/publish.ts
1554
+ var DEFAULT_PUBLIC_DIR = "public";
1555
+ function mirrorSlug(normalizedUrl) {
1556
+ return normalizedUrl.replace(/^https:\/\//, "").replace(/[^A-Za-z0-9.-]+/g, "_");
1557
+ }
1558
+ function resolveMirrors(projectDir, mirrors) {
1559
+ const list = mirrors?.length ? mirrors : loadManifest(projectDir).repository.mirrors;
1560
+ if (!list.length) throw new Error("No mirror URL: pass --mirror <https://\u2026> or set repository.mirrors in shinsou.plugin.json");
1561
+ return [...new Set(list.map((m) => normalizeRepositoryUrl(m)))];
1562
+ }
1563
+ function requireSigningKey(state, key) {
1564
+ if (!state.publisherId || !state.currentKey) {
1565
+ state.publisherId = key.fingerprint;
1566
+ state.currentKey = { name: key.name, publicKey: key.publicKey, fingerprint: key.fingerprint };
1567
+ return;
1568
+ }
1569
+ if (state.keyRevoked) throw new Error("The publisher key was revoked; publish under a new key (users must trust it again)");
1570
+ if (state.currentKey.fingerprint !== key.fingerprint) {
1571
+ const retired = state.retiredKeys.some((k) => k.fingerprint === key.fingerprint);
1572
+ throw new Error(
1573
+ `${key.name} (${key.fingerprint}) is ${retired ? "a retired key" : "not this repository's key"}; the current key is '${state.currentKey.name}' (${state.currentKey.fingerprint}). Use 'rotate' to change keys.`
1574
+ );
1575
+ }
1576
+ }
1577
+ function revocationsDoc(state, opts) {
1578
+ const previous = state.documents[DOCUMENT_IDS.revocations];
1579
+ const half = Math.floor(opts.ttlSeconds / 2);
1580
+ const keep = !opts.refresh && previous?.expiresAt !== void 0 && previous.expiresAt - opts.now > half;
1581
+ const expiresAt = keep ? previous.expiresAt : opts.now + opts.ttlSeconds;
1582
+ return {
1583
+ documentId: DOCUMENT_IDS.revocations,
1584
+ payloadType: "publisher-revocations",
1585
+ payload: revocationsPayload({ publisherId: state.publisherId, revokedArtifacts: state.revokedArtifacts, expiresAt })
1586
+ };
1587
+ }
1588
+ function signDocs(args) {
1589
+ if (args.flat && args.mirrors.length !== 1) throw new Error("--flat needs exactly one mirror");
1590
+ const sequences = args.docs.map((d) => nextSequence(args.state, d.documentId, d.payload));
1591
+ const outputs = [];
1592
+ for (const mirror of args.mirrors) {
1593
+ const dir = args.flat ? args.outDir : join6(args.outDir, mirrorSlug(mirror));
1594
+ const documents = [];
1595
+ args.docs.forEach((d, i) => {
1596
+ const envelope = signEnvelope({
1597
+ seed: args.key.seed,
1598
+ repository: mirror,
1599
+ payloadType: d.payloadType,
1600
+ documentId: d.documentId,
1601
+ sequence: sequences[i],
1602
+ payload: d.payload
1603
+ });
1604
+ verifyEnvelope({
1605
+ envelope,
1606
+ repository: mirror,
1607
+ payloadType: d.payloadType,
1608
+ documentId: d.documentId,
1609
+ pinned: { publicKey: args.key.publicKey, fingerprint: args.key.fingerprint }
1610
+ });
1611
+ const target = join6(dir, d.documentId);
1612
+ mkdirSync4(dirname3(target), { recursive: true });
1613
+ writeFileSync4(target, JSON.stringify(envelope, null, 2) + "\n");
1614
+ documents.push({ documentId: d.documentId, sequence: sequences[i] });
1615
+ });
1616
+ for (const [rel, from] of Object.entries(args.copy ?? {})) {
1617
+ const target = join6(dir, rel);
1618
+ mkdirSync4(dirname3(target), { recursive: true });
1619
+ copyFileSync(from, target);
1620
+ }
1621
+ outputs.push({ mirror, dir, documents });
1622
+ }
1623
+ if (args.docs.some((d) => d.documentId === DOCUMENT_IDS.revocations)) {
1624
+ const rev = args.docs.find((d) => d.documentId === DOCUMENT_IDS.revocations).payload;
1625
+ args.state.documents[DOCUMENT_IDS.revocations].expiresAt = rev.expiresAt;
1626
+ }
1627
+ return outputs;
1628
+ }
1629
+ async function signRepository(args) {
1630
+ const state = loadState(args.projectDir);
1631
+ requireSigningKey(state, args.key);
1632
+ saveState(args.projectDir, state);
1633
+ const built = await buildProject(args.projectDir);
1634
+ const mirrors = resolveMirrors(args.projectDir, args.mirrors);
1635
+ const m = built.manifest;
1636
+ const docs = [
1637
+ { documentId: DOCUMENT_IDS.repo, payloadType: "repository-meta", payload: repoMetaDocument(m, args.key.fingerprint) },
1638
+ { documentId: DOCUMENT_IDS.index, payloadType: "repository-index", payload: built.index },
1639
+ { documentId: sidecarUrl(m), payloadType: "plugin-sidecar", payload: built.sidecar },
1640
+ revocationsDoc(state, { refresh: false, ttlSeconds: args.ttlSeconds ?? DEFAULT_REVOCATIONS_TTL_SECONDS, now: args.now ?? nowSeconds() })
1641
+ ];
1642
+ const outputs = signDocs({
1643
+ state,
1644
+ key: args.key,
1645
+ mirrors,
1646
+ docs,
1647
+ outDir: resolve(args.projectDir, args.outDir ?? DEFAULT_PUBLIC_DIR),
1648
+ copy: { [scriptUrl(m)]: join6(built.outDir, scriptUrl(m)) }
1649
+ });
1650
+ saveState(args.projectDir, state);
1651
+ return outputs;
1652
+ }
1653
+ function refreshRevocations(args) {
1654
+ const state = loadState(args.projectDir);
1655
+ if (!state.publisherId) throw new Error("Nothing signed yet: run 'shinsou-plugin sign' first");
1656
+ requireSigningKey(state, args.key);
1657
+ if (args.onlyIfDue && !(args.add ?? []).length) {
1658
+ const expiresAt = state.documents[DOCUMENT_IDS.revocations]?.expiresAt;
1659
+ const ttl = args.ttlSeconds ?? DEFAULT_REVOCATIONS_TTL_SECONDS;
1660
+ if (expiresAt !== void 0 && expiresAt - (args.now ?? nowSeconds()) > Math.floor(ttl / 2)) return [];
1661
+ }
1662
+ for (const raw of args.add ?? []) {
1663
+ const digest = raw.toLowerCase().replace(/^sha256:/, "");
1664
+ if (!SHA256_HEX.test(digest)) throw new Error(`Not a SHA-256 hex digest: ${raw}`);
1665
+ if (!state.revokedArtifacts.includes(digest)) state.revokedArtifacts.push(digest);
1666
+ }
1667
+ const outputs = signDocs({
1668
+ state,
1669
+ key: args.key,
1670
+ mirrors: resolveMirrors(args.projectDir, args.mirrors),
1671
+ docs: [revocationsDoc(state, { refresh: true, ttlSeconds: args.ttlSeconds ?? DEFAULT_REVOCATIONS_TTL_SECONDS, now: args.now ?? nowSeconds() })],
1672
+ outDir: resolve(args.projectDir, args.outDir ?? DEFAULT_PUBLIC_DIR),
1673
+ flat: args.flat
1674
+ });
1675
+ saveState(args.projectDir, state);
1676
+ return outputs;
1677
+ }
1678
+ async function rotateKey(args) {
1679
+ const state = loadState(args.projectDir);
1680
+ if (!state.publisherId || !state.currentKey) throw new Error("Nothing signed yet: run 'shinsou-plugin sign' first");
1681
+ requireSigningKey(state, args.oldKey);
1682
+ if (args.newKey.fingerprint === args.oldKey.fingerprint) throw new Error("The new key is the current key");
1683
+ if (state.retiredKeys.some((k) => k.fingerprint === args.newKey.fingerprint)) throw new Error("The new key was already retired");
1684
+ const effectiveSequence = state.rotationSequence + 1;
1685
+ const payload = rotationPayload({
1686
+ publisherId: state.publisherId,
1687
+ oldKeyFingerprint: args.oldKey.fingerprint,
1688
+ newSeed: args.newKey.seed,
1689
+ effectiveSequence
1690
+ });
1691
+ const mirrors = resolveMirrors(args.projectDir, args.mirrors);
1692
+ const outDir = join6(args.projectDir, args.outDir ?? DEFAULT_PUBLIC_DIR);
1693
+ const rotation = signDocs({
1694
+ state,
1695
+ key: args.oldKey,
1696
+ mirrors,
1697
+ docs: [{ documentId: DOCUMENT_IDS.rotation, payloadType: "publisher-key-rotation", payload }],
1698
+ outDir
1699
+ });
1700
+ state.retiredKeys.push({ ...state.currentKey, retiredAt: (/* @__PURE__ */ new Date()).toISOString() });
1701
+ state.currentKey = { name: args.newKey.name, publicKey: args.newKey.publicKey, fingerprint: args.newKey.fingerprint };
1702
+ state.rotationSequence = effectiveSequence;
1703
+ saveState(args.projectDir, state);
1704
+ const resigned = await signRepository({ projectDir: args.projectDir, key: args.newKey, mirrors, outDir: args.outDir, now: args.now });
1705
+ return { rotation, resigned, effectiveSequence };
1706
+ }
1707
+ function revokeKey(args) {
1708
+ const state = loadState(args.projectDir);
1709
+ if (!state.publisherId) throw new Error("Nothing signed yet");
1710
+ const isCurrent = state.currentKey?.fingerprint === args.key.fingerprint;
1711
+ const isRetired = state.retiredKeys.some((k) => k.fingerprint === args.key.fingerprint);
1712
+ if (!isCurrent && !isRetired) throw new Error(`${args.key.name} is not a key of this publisher`);
1713
+ const outputs = signDocs({
1714
+ state,
1715
+ key: args.key,
1716
+ mirrors: resolveMirrors(args.projectDir, args.mirrors),
1717
+ docs: [
1718
+ {
1719
+ documentId: DOCUMENT_IDS.keyRevoked,
1720
+ payloadType: "publisher-key-revoked",
1721
+ payload: keyRevokedPayload({ publisherId: state.publisherId, revokedKeyFingerprint: args.key.fingerprint })
1722
+ }
1723
+ ],
1724
+ outDir: resolve(args.projectDir, args.outDir ?? DEFAULT_PUBLIC_DIR)
1725
+ });
1726
+ if (isCurrent) state.keyRevoked = true;
1727
+ saveState(args.projectDir, state);
1728
+ return outputs;
1729
+ }
1730
+ function nowSeconds() {
1731
+ return Math.floor(Date.now() / 1e3);
1732
+ }
1733
+ async function signExistingRepository(args) {
1734
+ if (!args.mirrors.length) throw new Error("No mirror URL: pass --mirror <https://\u2026> (the URL users add, without index.json)");
1735
+ const mirrors = [...new Set(args.mirrors.map((m) => normalizeRepositoryUrl(m)))];
1736
+ const readJson = (rel) => {
1737
+ const path = join6(args.repoDir, rel);
1738
+ if (!existsSync4(path)) throw new Error(`${rel} is missing`);
1739
+ const value2 = JSON.parse(readFileSync5(path, "utf8"));
1740
+ if (value2 && typeof value2 === "object" && "signature" in value2 && "payload" in value2) {
1741
+ throw new Error(`${rel} is already a signed envelope; sign the unsigned source files`);
1742
+ }
1743
+ return value2;
1744
+ };
1745
+ const relPath = (value2, what) => {
1746
+ const rel = String(value2 ?? "");
1747
+ if (!rel || rel.startsWith("/") || rel.includes("\\") || rel.split("/").some((p) => p === ".." || p === "")) {
1748
+ throw new Error(`${what} '${rel}' is not a safe relative path`);
1749
+ }
1750
+ return rel;
1751
+ };
1752
+ const repo = readJson(DOCUMENT_IDS.repo);
1753
+ const meta = repo.meta ?? {};
1754
+ for (const field of ["name", "shortName", "website"]) {
1755
+ if (typeof meta[field] !== "string" || !meta[field].length) throw new Error(`repo.json meta.${field} is missing`);
1756
+ }
1757
+ const index = readJson(DOCUMENT_IDS.index);
1758
+ if (index.format !== INDEX_FORMAT) throw new Error(`index.json format must be ${INDEX_FORMAT}`);
1759
+ const packages = index.packages ?? [];
1760
+ if (!Array.isArray(packages) || packages.length === 0) throw new Error("index.json has no packages");
1761
+ const problems = [];
1762
+ const named = (value2, what) => {
1763
+ const term = misleadingTerm(String(value2 ?? ""));
1764
+ if (term) problems.push(`${what} '${String(value2)}' uses the misleading term '${term}'`);
1765
+ };
1766
+ named(meta.name, "repository name");
1767
+ named(meta.shortName, "repository shortName");
1768
+ const docs = [];
1769
+ const copy = {};
1770
+ const ids = [];
1771
+ const unchecked = [];
1772
+ for (const pkg of packages) {
1773
+ const id = String(pkg.id);
1774
+ ids.push(id);
1775
+ named(pkg.name, `${id} name`);
1776
+ for (const source of pkg.sources ?? []) named(source.name, `${id} source name`);
1777
+ const script = relPath(pkg.scriptUrl, `${id} scriptUrl`);
1778
+ const scriptFile = join6(args.repoDir, script);
1779
+ if (!existsSync4(scriptFile)) {
1780
+ problems.push(`${id}: ${script} is missing`);
1781
+ continue;
1782
+ }
1783
+ const scriptBytes = new Uint8Array(readFileSync5(scriptFile));
1784
+ if (sha256Hex(scriptBytes) !== pkg.sha256) problems.push(`${id}: ${script} does not match its sha256 in index.json`);
1785
+ if (scriptBytes.length !== pkg.byteSize) problems.push(`${id}: ${script} is ${scriptBytes.length} bytes, index.json says ${String(pkg.byteSize)}`);
1786
+ copy[script] = scriptFile;
1787
+ if (pkg.sidecarUrl === void 0) {
1788
+ if (pkg.contract === "shinsou") problems.push(`${id}: a Shinsou package needs a sidecarUrl`);
1789
+ unchecked.push(id);
1790
+ continue;
1791
+ }
1792
+ const sidecarRel = relPath(pkg.sidecarUrl, `${id} sidecarUrl`);
1793
+ const sidecar = readJson(sidecarRel);
1794
+ if (pkg.contract === "shinsou") {
1795
+ for (const p of checkPackageConsistency({ index, pkg, sidecar, scriptBytes })) problems.push(p);
1796
+ } else {
1797
+ unchecked.push(id);
1798
+ }
1799
+ docs.push({ documentId: sidecarRel, payloadType: "plugin-sidecar", payload: sidecar });
1800
+ }
1801
+ if (new Set(ids).size !== ids.length) problems.push("index.json lists a package id more than once");
1802
+ if (problems.length) throw new Error(`Repository cannot be signed:
1803
+ - ${problems.join("\n - ")}`);
1804
+ const state = loadState(args.repoDir);
1805
+ requireSigningKey(state, args.key);
1806
+ const repoMeta = {
1807
+ ...repo,
1808
+ meta: { name: meta.name, shortName: meta.shortName, website: meta.website, signingKeyFingerprint: args.key.fingerprint }
1809
+ };
1810
+ const outputs = signDocs({
1811
+ state,
1812
+ key: args.key,
1813
+ mirrors,
1814
+ docs: [
1815
+ { documentId: DOCUMENT_IDS.repo, payloadType: "repository-meta", payload: repoMeta },
1816
+ { documentId: DOCUMENT_IDS.index, payloadType: "repository-index", payload: index },
1817
+ ...docs,
1818
+ revocationsDoc(state, { refresh: false, ttlSeconds: args.ttlSeconds ?? DEFAULT_REVOCATIONS_TTL_SECONDS, now: args.now ?? nowSeconds() })
1819
+ ],
1820
+ outDir: resolve(args.repoDir, args.outDir ?? "signed"),
1821
+ copy,
1822
+ flat: args.flat
1823
+ });
1824
+ saveState(args.repoDir, state);
1825
+ return { outputs, packages: ids, unchecked };
1826
+ }
1827
+
1828
+ // src/commands/init.ts
1829
+ import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync5, readdirSync, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
1830
+ import { basename, join as join7, resolve as resolve2 } from "node:path";
1831
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
1832
+ var TEMPLATES = ["manga", "novel", "mixed", "async"];
1833
+ function templatesDir() {
1834
+ const here = fileURLToPath2(new URL(".", import.meta.url));
1835
+ for (const candidate of [join7(here, "..", "templates"), join7(here, "..", "..", "templates")]) {
1836
+ if (existsSync5(join7(candidate, "manga"))) return candidate;
1837
+ }
1838
+ throw new Error("templates directory not found next to the CLI");
1839
+ }
1840
+ function initProject(args) {
1841
+ const dir = resolve2(args.dir);
1842
+ if (!TEMPLATES.includes(args.template)) throw new Error(`Unknown template '${args.template}' (manga | novel | mixed | async)`);
1843
+ mkdirSync5(dir, { recursive: true });
1844
+ if (existsSync5(join7(dir, MANIFEST_FILE))) throw new Error(`${join7(dir, MANIFEST_FILE)} already exists`);
1845
+ const leftovers = readdirSync(dir).filter((n) => !n.startsWith("."));
1846
+ if (leftovers.includes("src") || leftovers.includes("test")) throw new Error(`${dir} already has src/ or test/; use an empty directory`);
1847
+ const lang = args.lang ?? "zh";
1848
+ const slug = basename(dir).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "plugin";
1849
+ const packageId = args.packageId ?? `${lang}.${slug.replace(/-/g, "")}`;
1850
+ const name = args.name ?? slug;
1851
+ const sourceId = defaultSourceId(sha256(utf8(`${packageId}/${lang}/1`)));
1852
+ const t = templatesDir();
1853
+ cpSync(join7(t, args.template), dir, { recursive: true });
1854
+ const mangaSourceId = sourceId;
1855
+ const novelSourceId = defaultSourceId(sha256(utf8(`${packageId}/${lang}/2`)));
1856
+ if (args.template === "mixed") {
1857
+ for (const file of ["src/index.js", "test/cases.json"]) {
1858
+ const path = join7(dir, file);
1859
+ writeFileSync5(
1860
+ path,
1861
+ readFileSync6(path, "utf8").replaceAll("__MANGA_SOURCE_ID__", mangaSourceId).replaceAll("__NOVEL_SOURCE_ID__", novelSourceId)
1862
+ );
1863
+ }
1864
+ }
1865
+ cpSync(join7(t, "common", ".github"), join7(dir, ".github"), { recursive: true });
1866
+ cpSync(join7(t, "common", "gitignore"), join7(dir, ".gitignore"));
1867
+ const profile = {
1868
+ manga: {
1869
+ contentType: "manga",
1870
+ contentKinds: ["IMAGE_SEQUENCE"],
1871
+ capabilities: ["CATALOGUE", "LATEST", "BROWSE", "METADATA", "UNITS", "CONTENT", "SEARCH"],
1872
+ baseUrl: "https://example.com",
1873
+ requestOrigins: ["https://example.com"],
1874
+ contentOrigins: ["https://example.com"]
1875
+ },
1876
+ novel: {
1877
+ contentType: "novel",
1878
+ contentKinds: ["PLAIN_TEXT"],
1879
+ capabilities: ["CATALOGUE", "LATEST", "BROWSE", "METADATA", "UNITS", "CONTENT", "SEARCH"],
1880
+ baseUrl: "https://example.com",
1881
+ requestOrigins: ["https://example.com"],
1882
+ contentOrigins: []
1883
+ },
1884
+ mixed: {
1885
+ contentType: "both",
1886
+ contentKinds: ["IMAGE_SEQUENCE", "PLAIN_TEXT"],
1887
+ capabilities: ["CATALOGUE", "LATEST", "BROWSE", "METADATA", "UNITS", "CONTENT", "SEARCH"],
1888
+ baseUrl: "https://example.com",
1889
+ requestOrigins: ["https://example.com"],
1890
+ contentOrigins: ["https://example.com"]
1891
+ },
1892
+ async: {
1893
+ contentType: "manga",
1894
+ contentKinds: ["IMAGE_SEQUENCE"],
1895
+ capabilities: ["CATALOGUE", "BROWSE", "METADATA", "UNITS", "CONTENT", "SEARCH"],
1896
+ baseUrl: "https://example.com",
1897
+ requestOrigins: ["https://api.example.com"],
1898
+ contentOrigins: ["https://example.com", "https://cdn.example.com"]
1899
+ }
1900
+ }[args.template];
1901
+ const sourceFor = (id, sourceName, p, extra = {}) => ({
1902
+ sourceId: id,
1903
+ legacyLongId: null,
1904
+ name: sourceName,
1905
+ lang,
1906
+ baseUrl: p.baseUrl,
1907
+ ...extra,
1908
+ requestOrigins: p.requestOrigins,
1909
+ credentialOrigins: [],
1910
+ contentOrigins: p.contentOrigins,
1911
+ browserSessionOrigins: [],
1912
+ contentHostSuffixes: []
1913
+ });
1914
+ const sources = args.template === "mixed" ? [
1915
+ sourceFor(mangaSourceId, name, profile, { contentType: "manga", contentKinds: ["IMAGE_SEQUENCE"] }),
1916
+ sourceFor(
1917
+ novelSourceId,
1918
+ `${name} Novel`,
1919
+ { baseUrl: "https://novel.example.com", requestOrigins: ["https://novel.example.com"], contentOrigins: [] },
1920
+ { contentType: "novel", contentKinds: ["PLAIN_TEXT"] }
1921
+ )
1922
+ ] : [sourceFor(sourceId, name, profile)];
1923
+ const manifest = {
1924
+ manifestVersion: 1,
1925
+ packageId,
1926
+ name,
1927
+ version: "0.1.0",
1928
+ versionCode: 1,
1929
+ lang,
1930
+ nsfw: false,
1931
+ contract: "shinsou",
1932
+ runtime: "legacy-shinsou-adapter-v2",
1933
+ contentType: profile.contentType,
1934
+ contentKinds: profile.contentKinds,
1935
+ capabilities: profile.capabilities,
1936
+ runtimePermissions: ["EXECUTE_SCRIPT", "NETWORK"],
1937
+ requestedHostPermissions: [],
1938
+ systemEvents: { required: [], optional: [] },
1939
+ entry: "src/index.js",
1940
+ sources,
1941
+ repository: {
1942
+ name: "My Shinsou Plugins",
1943
+ shortName: "My Plugins",
1944
+ website: "https://github.com/you/your-plugins",
1945
+ mirrors: ["https://you.github.io/your-plugins"]
1946
+ },
1947
+ test: { cases: "test/cases.json", fixtures: "test/fixtures" }
1948
+ };
1949
+ writeFileSync5(join7(dir, MANIFEST_FILE), JSON.stringify(manifest, null, 2) + "\n");
1950
+ if (!existsSync5(join7(dir, "package.json"))) {
1951
+ const pkg = {
1952
+ name: slug,
1953
+ private: true,
1954
+ type: "module",
1955
+ scripts: {
1956
+ build: "shinsou-plugin build",
1957
+ lint: "shinsou-plugin lint",
1958
+ test: "shinsou-plugin test",
1959
+ dev: "shinsou-plugin serve",
1960
+ sign: "shinsou-plugin sign"
1961
+ },
1962
+ devDependencies: { "@shinsou/plugin": `^${args.cliVersion}` }
1963
+ };
1964
+ writeFileSync5(join7(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
1965
+ }
1966
+ return [MANIFEST_FILE, "package.json", "src/", "test/", ".github/workflows/shinsou-refresh.yml", ".gitignore"];
1967
+ }
1968
+
1969
+ // src/commands/serve.ts
1970
+ import { existsSync as existsSync6, readFileSync as readFileSync7, statSync, watch } from "node:fs";
1971
+ import { createServer } from "node:http";
1972
+ import { isIP as isIP2 } from "node:net";
1973
+ import { networkInterfaces } from "node:os";
1974
+ import { join as join8, normalize, relative, sep } from "node:path";
1975
+ function isPrivateIPv4(ip) {
1976
+ const p = ip.split(".").map(Number);
1977
+ if (p.length !== 4 || p.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return false;
1978
+ return p[0] === 10 || p[0] === 172 && p[1] >= 16 && p[1] <= 31 || p[0] === 192 && p[1] === 168 || p[0] === 169 && p[1] === 254;
1979
+ }
1980
+ function localLanAddresses() {
1981
+ return Object.values(networkInterfaces()).flat().filter((i) => !!i && i.family === "IPv4" && !i.internal && isPrivateIPv4(i.address)).map((i) => i.address);
1982
+ }
1983
+ function resolveServeHost(host) {
1984
+ if (host === "127.0.0.1" || host === "localhost") return "127.0.0.1";
1985
+ if (host === "::1") return "::1";
1986
+ if (host === "lan") {
1987
+ const lan = localLanAddresses();
1988
+ if (!lan.length) throw new Error("\u627E\u4E0D\u5230\u5340\u57DF\u7DB2\u8DEF IPv4 \u4F4D\u5740\uFF1B\u8ACB\u6539\u7528 --host <IP>");
1989
+ return lan[0];
1990
+ }
1991
+ if (isIP2(host) !== 4 || !isPrivateIPv4(host)) {
1992
+ throw new Error(`--host \u53EA\u63A5\u53D7 127.0.0.1\u3001::1 \u6216\u672C\u6A5F\u7684\u79C1\u6709\u5340\u57DF\u7DB2\u8DEF IPv4 \u4F4D\u5740\uFF0810/8\u3001172.16/12\u3001192.168/16\uFF09\uFF0C\u6536\u5230 ${host}`);
1993
+ }
1994
+ if (!localLanAddresses().includes(host)) throw new Error(`${host} \u4E0D\u662F\u9019\u53F0\u96FB\u8166\u7684\u7DB2\u8DEF\u4ECB\u9762\u4F4D\u5740`);
1995
+ return host;
1996
+ }
1997
+ function devRepositoryLink(baseUrl) {
1998
+ return `shinsou://add-dev-repository?url=${encodeURIComponent(baseUrl)}`;
1999
+ }
2000
+ var CONTENT_TYPES2 = {
2001
+ ".json": "application/json; charset=utf-8",
2002
+ ".js": "text/javascript; charset=utf-8",
2003
+ ".png": "image/png",
2004
+ ".webp": "image/webp"
2005
+ };
2006
+ async function serveProject(args) {
2007
+ const log = args.onEvent ?? ((m) => console.log(m));
2008
+ const host = resolveServeHost(args.host);
2009
+ const root = join8(args.projectDir, DEFAULT_OUT_DIR);
2010
+ let revision = 0;
2011
+ let last = null;
2012
+ let lastError = null;
2013
+ const rebuild = async () => {
2014
+ try {
2015
+ last = await buildProject(args.projectDir);
2016
+ revision++;
2017
+ lastError = null;
2018
+ log(`[build #${revision}] ${last.manifest.packageId} ${last.manifest.version} sha256=${last.sha256.slice(0, 12)}\u2026 ${last.byteSize} bytes`);
2019
+ } catch (err) {
2020
+ lastError = err.message;
2021
+ log(`[build \u5931\u6557\uFF0C\u7E7C\u7E8C\u63D0\u4F9B\u4E0A\u4E00\u500B\u7248\u672C] ${lastError}`);
2022
+ }
2023
+ };
2024
+ await rebuild();
2025
+ const server = createServer((req, res) => {
2026
+ const url = new URL(req.url ?? "/", "http://dev.invalid");
2027
+ res.setHeader("Cache-Control", "no-store");
2028
+ if (url.pathname === "/__shinsou-dev/status") {
2029
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
2030
+ res.end(JSON.stringify({ revision, ok: lastError === null, error: lastError, sha256: last?.sha256 ?? null, packageId: last?.manifest.packageId ?? null }));
2031
+ return;
2032
+ }
2033
+ let rel;
2034
+ try {
2035
+ rel = normalize(decodeURIComponent(url.pathname)).replace(/^[/\\]+/, "");
2036
+ } catch {
2037
+ res.statusCode = 400;
2038
+ res.end();
2039
+ return;
2040
+ }
2041
+ const file = join8(root, rel || "index.json");
2042
+ const inside = relative(root, file);
2043
+ if (inside.startsWith("..") || inside.split(sep).includes("..") || !existsSync6(file) || !statSync(file).isFile()) {
2044
+ res.statusCode = 404;
2045
+ res.end("not found");
2046
+ return;
2047
+ }
2048
+ const ext = file.slice(file.lastIndexOf("."));
2049
+ res.setHeader("Content-Type", CONTENT_TYPES2[ext] ?? "application/octet-stream");
2050
+ res.end(readFileSync7(file));
2051
+ log(`${req.method} ${url.pathname}`);
2052
+ });
2053
+ await new Promise((resolveListen, reject) => {
2054
+ server.once("error", reject);
2055
+ server.listen(args.port, host, () => resolveListen());
2056
+ });
2057
+ const address = server.address();
2058
+ const port = typeof address === "object" && address ? address.port : args.port;
2059
+ const baseUrl = `http://${host.includes(":") ? `[${host}]` : host}:${port}`;
2060
+ let watcher = null;
2061
+ if (args.watchFiles !== false) {
2062
+ let timer = null;
2063
+ const ignored = /^(build|public|node_modules|\.git|\.shinsou)([/\\]|$)/;
2064
+ watcher = watch(args.projectDir, { recursive: true }, (_event, filename) => {
2065
+ if (!filename || ignored.test(String(filename))) return;
2066
+ if (timer) clearTimeout(timer);
2067
+ timer = setTimeout(() => void rebuild(), 150);
2068
+ });
2069
+ }
2070
+ return {
2071
+ server,
2072
+ baseUrl,
2073
+ link: devRepositoryLink(baseUrl),
2074
+ close: () => new Promise((r) => {
2075
+ watcher?.close();
2076
+ server.close(() => r());
2077
+ })
2078
+ };
2079
+ }
2080
+
2081
+ // src/runner/harness.ts
2082
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6, readdirSync as readdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "node:fs";
2083
+ import { join as join9 } from "node:path";
2084
+ import { isDeepStrictEqual as isDeepStrictEqual3 } from "node:util";
2085
+
2086
+ // src/core/sandbox-protocol.ts
2087
+ var MAX_FRAME_BYTES = 1 << 20;
2088
+ var CHUNK_PAYLOAD_BYTES = MAX_FRAME_BYTES - 16;
2089
+ var ERROR_CODES = {
2090
+ SCRIPT_ERROR: 1,
2091
+ LIMIT_MEMORY: 2,
2092
+ LIMIT_STACK: 3,
2093
+ LIMIT_TIME: 4,
2094
+ LIMIT_BRIDGE: 5,
2095
+ LIMIT_RESULT: 6,
2096
+ CANCELLED: 7,
2097
+ NO_SUCH_HOOK: 8,
2098
+ PROMISE_STALLED: 9,
2099
+ INTERNAL: 10,
2100
+ BAD_ARGUMENT: 11,
2101
+ UNSUPPORTED_PROTOCOL: 32,
2102
+ PERMISSION_DENIED: 33,
2103
+ UNKNOWN_OPERATION: 34,
2104
+ HOST_FAILURE: 35,
2105
+ NETWORK_FAILED: 36,
2106
+ REVOKED: 37
2107
+ };
2108
+ var KNOWN_ERROR_CODES = new Set(Object.values(ERROR_CODES));
2109
+ var DEFAULT_FIELD_LIMITS = {
2110
+ maxScriptBytes: 8 << 20,
2111
+ maxMethodBytes: 256,
2112
+ maxJsonBytes: 4 << 20,
2113
+ maxLogBytes: 16 * 1024
2114
+ };
2115
+
2116
+ // src/runner/fixture-host.ts
2117
+ var CREDENTIAL_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
2118
+ "set-cookie",
2119
+ "set-cookie2",
2120
+ "cookie",
2121
+ "authorization",
2122
+ "proxy-authorization",
2123
+ "www-authenticate",
2124
+ "proxy-authenticate"
2125
+ ]);
2126
+ function fixtureKey(method, url, body = "") {
2127
+ return `${method.toUpperCase()} ${url}${method.toUpperCase() === "GET" ? "" : `
2128
+ ${body}`}`;
2129
+ }
2130
+ var FixtureBridgeHost = class {
2131
+ constructor(manifest, source, fixtures, opts = {}) {
2132
+ this.manifest = manifest;
2133
+ this.source = source;
2134
+ for (const f of fixtures) this.fixtures.set(fixtureKey(f.request.method, f.request.url, f.request.body ?? ""), f);
2135
+ for (const [k, v] of Object.entries(opts.preferences ?? {})) this.prefs.set(k, v);
2136
+ this.credential = opts.credential ?? null;
2137
+ }
2138
+ manifest;
2139
+ source;
2140
+ events = [];
2141
+ misses = [];
2142
+ logs = [];
2143
+ fixtures = /* @__PURE__ */ new Map();
2144
+ prefs = /* @__PURE__ */ new Map();
2145
+ cookies = /* @__PURE__ */ new Map();
2146
+ credential;
2147
+ log(message) {
2148
+ this.logs.push(message);
2149
+ }
2150
+ call(op, argsJson) {
2151
+ let args;
2152
+ try {
2153
+ args = JSON.parse(argsJson);
2154
+ if (!Array.isArray(args)) return fail(ERROR_CODES.BAD_ARGUMENT);
2155
+ } catch {
2156
+ return fail(ERROR_CODES.BAD_ARGUMENT);
2157
+ }
2158
+ const s = (i) => typeof args[i] === "string" ? args[i] : args[i] == null ? "" : String(args[i]);
2159
+ const map = (i) => args[i] && typeof args[i] === "object" ? args[i] : {};
2160
+ switch (op) {
2161
+ case "httpGet":
2162
+ case "httpGetWithHeaders":
2163
+ case "httpGetResponse": {
2164
+ const r = this.http(op, "GET", s(0), "", map(1));
2165
+ if (!r.ok) return r;
2166
+ return value(op === "httpGetResponse" ? { status: r.response.status, body: r.response.body } : r.response.body);
2167
+ }
2168
+ case "httpPost":
2169
+ case "httpPostResponse": {
2170
+ const r = this.http(op, "POST", s(0), s(1), map(2));
2171
+ if (!r.ok) return r;
2172
+ return value(op === "httpPostResponse" ? { status: r.response.status, body: r.response.body } : r.response.body);
2173
+ }
2174
+ case "httpPostBatch": {
2175
+ const urls = Array.isArray(args[0]) ? args[0] : [];
2176
+ const bodies = Array.isArray(args[1]) ? args[1] : [];
2177
+ const out = [];
2178
+ for (let i = 0; i < urls.length; i++) {
2179
+ const r = this.http(op, "POST", String(urls[i]), String(bodies[i] ?? ""), map(2));
2180
+ if (!r.ok && r.code === ERROR_CODES.PERMISSION_DENIED) return r;
2181
+ out.push(r.ok ? r.response.body : null);
2182
+ }
2183
+ return value(JSON.stringify(out));
2184
+ }
2185
+ case "browserSessionRequest": {
2186
+ if (!this.has("BROWSER_CHALLENGE")) return this.deny(op, "BROWSER_CHALLENGE not declared");
2187
+ const target = origin(s(0));
2188
+ if (!target || !this.source.browserSessionOrigins.includes(target)) return this.deny(op, `${s(0)} is not a browserSessionOrigin`);
2189
+ const r = this.http(op, s(1) || "GET", s(0), s(2), map(3), true);
2190
+ return r.ok ? value({ status: r.response.status, body: r.response.body }) : r;
2191
+ }
2192
+ case "getPreference":
2193
+ return value(this.prefs.has(s(0)) ? this.prefs.get(s(0)) : null);
2194
+ case "setPreference":
2195
+ this.prefs.set(s(0), s(1));
2196
+ return value(null);
2197
+ case "getCredentialUsername":
2198
+ return this.has("CREDENTIAL_ACCESS") ? value(this.credential?.username ?? null) : this.deny(op, "CREDENTIAL_ACCESS not declared");
2199
+ case "hasCredential":
2200
+ return this.has("CREDENTIAL_ACCESS") ? value(!!this.credential?.username) : this.deny(op, "CREDENTIAL_ACCESS not declared");
2201
+ case "getCredentialPassword":
2202
+ case "setCredential":
2203
+ case "clearCredential":
2204
+ return this.deny(op, "credential secrets are only available during a host-started login");
2205
+ case "getCookie":
2206
+ case "getCookies":
2207
+ return this.has("COOKIE_STORAGE") && this.has("COOKIE_READ") ? value(op === "getCookie" ? this.cookies.get(s(0)) ?? null : Object.fromEntries(this.cookies)) : this.deny(op, "COOKIE_STORAGE and COOKIE_READ are both required");
2208
+ case "setCookie":
2209
+ if (!this.has("COOKIE_STORAGE")) return this.deny(op, "COOKIE_STORAGE not declared");
2210
+ this.cookies.set(s(0), s(1));
2211
+ return value(true);
2212
+ case "deleteCookie":
2213
+ if (!this.has("COOKIE_STORAGE")) return this.deny(op, "COOKIE_STORAGE not declared");
2214
+ this.cookies.delete(s(0));
2215
+ return value(null);
2216
+ case "clearCookies":
2217
+ if (!this.has("COOKIE_STORAGE")) return this.deny(op, "COOKIE_STORAGE not declared");
2218
+ this.cookies.clear();
2219
+ return value(null);
2220
+ case "requestLogin":
2221
+ return value(false);
2222
+ case "requestHostEvent":
2223
+ return value(JSON.stringify({ messageId: "", disposition: "UNSUPPORTED" }));
2224
+ case "getHostEventContext":
2225
+ return value(null);
2226
+ case "getHostEventCapabilities":
2227
+ return value({ enabled: false, protocol: "dev.shinsou.system", version: null, grantedCapabilities: [], hardLimits: null });
2228
+ case "log":
2229
+ this.logs.push(s(0));
2230
+ return value(null);
2231
+ // DOM bookkeeping calls made by the bootstrap's in-sandbox parser.
2232
+ case "domParse":
2233
+ return value({ maxNodes: 2e5, maxDepth: 512 });
2234
+ case "domSelect":
2235
+ case "domParsed":
2236
+ case "domDepth":
2237
+ case "domRelease":
2238
+ case "domReleaseAll":
2239
+ return value(null);
2240
+ default:
2241
+ this.events.push({ kind: "unknown-op", op, detail: "not a sandbox bridge operation" });
2242
+ return fail(ERROR_CODES.UNKNOWN_OPERATION);
2243
+ }
2244
+ }
2245
+ async callAsync(op, argsJson) {
2246
+ if (op !== "net.fetch") return fail(ERROR_CODES.UNKNOWN_OPERATION);
2247
+ let args;
2248
+ try {
2249
+ args = JSON.parse(argsJson);
2250
+ } catch {
2251
+ return fail(ERROR_CODES.BAD_ARGUMENT);
2252
+ }
2253
+ const options = args[1] && typeof args[1] === "object" ? args[1] : {};
2254
+ const method = (options.method ?? "GET").toUpperCase();
2255
+ if (method !== "GET" && method !== "POST") return fail(ERROR_CODES.BAD_ARGUMENT);
2256
+ if (method === "GET" && options.body) return fail(ERROR_CODES.BAD_ARGUMENT);
2257
+ const r = this.http(op, method, String(args[0]), options.body ?? "", options.headers ?? {});
2258
+ if (!r.ok) return r;
2259
+ const headers = Object.fromEntries(
2260
+ Object.entries(r.response.headers ?? {}).filter(([k]) => !CREDENTIAL_RESPONSE_HEADERS.has(k.toLowerCase())).map(([k, v]) => [k.toLowerCase(), v])
2261
+ );
2262
+ return value({ status: r.response.status, headers, body: r.response.body });
2263
+ }
2264
+ has(permission) {
2265
+ return this.manifest.runtimePermissions.includes(permission);
2266
+ }
2267
+ deny(op, detail) {
2268
+ this.events.push({ kind: "denied", op, detail });
2269
+ return fail(ERROR_CODES.PERMISSION_DENIED);
2270
+ }
2271
+ http(op, method, url, body, headers, browserSession = false) {
2272
+ if (!this.has("NETWORK")) return this.deny(op, "NETWORK not declared");
2273
+ const o = origin(url);
2274
+ if (!o || !o.startsWith("https:")) return this.deny(op, `${url} is not an HTTPS URL`);
2275
+ if (!browserSession && !this.source.requestOrigins.includes(o)) {
2276
+ return this.deny(op, `${o} is not in requestOrigins`);
2277
+ }
2278
+ const f = this.fixtures.get(fixtureKey(method, url, body));
2279
+ if (!f) {
2280
+ this.events.push({ kind: "missing-fixture", op, detail: `${method} ${url}` });
2281
+ this.misses.push({ method: method.toUpperCase(), url, body, headers });
2282
+ return { ok: false, code: ERROR_CODES.NETWORK_FAILED };
2283
+ }
2284
+ return { ok: true, response: f.response };
2285
+ }
2286
+ };
2287
+ function origin(url) {
2288
+ try {
2289
+ return new URL(url).origin;
2290
+ } catch {
2291
+ return null;
2292
+ }
2293
+ }
2294
+ function value(v) {
2295
+ return { ok: true, json: v === null || v === void 0 ? null : JSON.stringify(v) };
2296
+ }
2297
+ function fail(code) {
2298
+ return { ok: false, code };
2299
+ }
2300
+
2301
+ // src/runner/quickjs.ts
2302
+ import { getQuickJS, shouldInterruptAfterDeadline } from "quickjs-emscripten";
2303
+
2304
+ // src/runner/legacy-bootstrap.ts
2305
+ var LEGACY_BOOTSTRAP = `(function(global){
2306
+ 'use strict';
2307
+ function nativeCall(method,args){
2308
+ var encoded=__shinsouBridge(String(method),JSON.stringify(args||[]));
2309
+ try{
2310
+ var value=JSON.parse(String(encoded));
2311
+ if(value&&value.__shinsouResourceLimit){var limitError=new Error(String(value.error||'Plugin resource limit exceeded'));limitError.__shinsouResourceLimit=true;throw limitError;}
2312
+ return value;
2313
+ }catch(e){if(e&&e.__shinsouResourceLimit)throw e;return null;}
2314
+ }
2315
+ global.bridge={
2316
+ httpGet:function(url){return nativeCall('httpGet',[String(url)]);},
2317
+ httpGetWithHeaders:function(url,headers){return nativeCall('httpGetWithHeaders',[String(url),headers||{}]);},
2318
+ httpGetResponse:function(url,headers){return nativeCall('httpGetResponse',[String(url),headers||{}]);},
2319
+ httpPost:function(url,body,headers){return nativeCall('httpPost',[String(url),String(body||''),headers||{}]);},
2320
+ httpPostResponse:function(url,body,headers){return nativeCall('httpPostResponse',[String(url),String(body||''),headers||{}]);},
2321
+ httpPostBatch:function(urls,bodies,headers){return nativeCall('httpPostBatch',[urls||[],bodies||[],headers||{}]);},
2322
+ browserSessionRequest:function(url,method,body,headers){return nativeCall('browserSessionRequest',[String(url),String(method||'GET'),String(body||''),headers||{}]);},
2323
+ log:function(sourceOrMessage,message){nativeCall('log',[String(message===undefined?sourceOrMessage:message)]);},
2324
+ getPreference:function(key){return nativeCall('getPreference',[String(key)]);},
2325
+ setPreference:function(key,value){nativeCall('setPreference',[String(key),String(value)]);},
2326
+ getCredentialUsername:function(){return nativeCall('getCredentialUsername',[]);},
2327
+ getCredentialPassword:function(){return nativeCall('getCredentialPassword',[]);},
2328
+ setCredential:function(username,password){nativeCall('setCredential',[String(username),String(password)]);},
2329
+ clearCredential:function(){nativeCall('clearCredential',[]);},
2330
+ hasCredential:function(){return !!nativeCall('hasCredential',[]);},
2331
+ requestLogin:function(reason){return !!nativeCall('requestLogin',reason==null?[]:[String(reason)]);},
2332
+ requestHostEvent:function(envelope){
2333
+ var value=nativeCall('requestHostEvent',[String(envelope)]);
2334
+ if(typeof value==='string'){try{return JSON.parse(value);}catch(e){return null;}}
2335
+ return value;
2336
+ },
2337
+ getHostEventContext:function(){return nativeCall('getHostEventContext',[]);},
2338
+ getHostEventCapabilities:function(){return nativeCall('getHostEventCapabilities',[])||{enabled:false};},
2339
+ getCookie:function(name,url){return nativeCall('getCookie',[String(name),String(url)]);},
2340
+ getCookies:function(url){return nativeCall('getCookies',[String(url)])||{};},
2341
+ setCookie:function(name,value,domain,path,seconds){return !!nativeCall('setCookie',[String(name),String(value),String(domain),String(path||'/'),Number(seconds||0)]);},
2342
+ deleteCookie:function(name,domain){nativeCall('deleteCookie',[String(name),String(domain)]);},
2343
+ clearCookies:function(){nativeCall('clearCookies',[]);},
2344
+ domRelease:function(id){nativeCall('domRelease',[Number(id||0)]);},
2345
+ // JSC owns DOM objects through GC; this legacy hint must not reset invocation quotas.
2346
+ domReleaseAll:function(){nativeCall('domReleaseAll',[]);},
2347
+ parseHtml:function(html,selector){var doc=parseHtml(String(html),'');return doc.select(String(selector)).map(function(e){return{text:e.text(),html:e.html(),outerHtml:e.outerHtml(),attr_href:e.attr('href'),attr_src:e.attr('src'),tagName:e.tagName()};});}
2348
+ };
2349
+ bridge.system={
2350
+ submit:function(name,kind,payload,options){
2351
+ options=options||{};
2352
+ var envelope={protocol:'dev.shinsou.system',version:1,kind:String(kind),name:String(name),
2353
+ id:String(options.id||('event-'+Date.now())),payloadVersion:1,payload:payload||{}};
2354
+ if(options.idempotencyKey!=null)envelope.idempotencyKey=String(options.idempotencyKey);
2355
+ if(options.contextRef!=null)envelope.contextRef=String(options.contextRef);
2356
+ return bridge.requestHostEvent(JSON.stringify(envelope));
2357
+ },
2358
+ requestLogin:function(reason){return this.submit('auth.login.request','command',{reasonCode:null,fallbackMessage:reason==null?null:String(reason)});},
2359
+ requestRefresh:function(scope,contextRef){scope=String(scope||'SELF');if(scope==='ACTIVE_CONTEXT'&&contextRef==null)contextRef=bridge.getHostEventContext();return this.submit('source.refresh.request','command',{scope:scope,reasonCode:null},{contextRef:contextRef});},
2360
+ requestLogout:function(reason){return this.submit('auth.logout.request','command',{reasonCode:null,fallbackMessage:reason==null?null:String(reason)});},
2361
+ reportMessage:function(message){return this.submit('diagnostic.message.report','event',message||{});}
2362
+ };
2363
+ global.console={log:bridge.log,error:bridge.log,warn:bridge.log,info:bridge.log};
2364
+
2365
+ function decodeEntities(value){
2366
+ var named={amp:'&',lt:'<',gt:'>',quot:'"',apos:"'",nbsp:' '};
2367
+ return String(value||'').replace(/&(#x[0-9a-f]+|#[0-9]+|amp|lt|gt|quot|apos|nbsp);/gi,function(all,key){
2368
+ var lower=key.toLowerCase();
2369
+ if(lower.charAt(0)==='#'){
2370
+ var hex=lower.charAt(1)==='x';var n=parseInt(lower.substring(hex?2:1),hex?16:10);
2371
+ return isNaN(n)?all:String.fromCharCode(n);
2372
+ }
2373
+ return named[lower]||all;
2374
+ });
2375
+ }
2376
+ function escapeText(value){return String(value).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
2377
+ function escapeAttr(value){return escapeText(value).replace(/"/g,'&quot;');}
2378
+ function TextNode(text,parent,raw){this.value=text;this._parent=parent||null;this.raw=!!raw;}
2379
+ TextNode.prototype.outerHtml=function(){return this.raw?this.value:escapeText(this.value);};
2380
+
2381
+ function Element(tag,attrs,parent,baseUri,document){
2382
+ this._tag=String(tag||'').toLowerCase();this._attrs=attrs||{};this._parent=parent||null;
2383
+ this._children=[];this._baseUri=baseUri||'';this._document=document||this;
2384
+ }
2385
+ function Document(baseUri){Element.call(this,'#document',{},null,baseUri||'',this);}
2386
+ Document.prototype=Object.create(Element.prototype);Document.prototype.constructor=Document;
2387
+ var voidTags={area:1,base:1,br:1,col:1,embed:1,hr:1,img:1,input:1,link:1,meta:1,param:1,source:1,track:1,wbr:1,'amp-img':1};
2388
+
2389
+ Element.prototype.tagName=function(){return this._tag==='#document'?'#root':this._tag;};
2390
+ Element.prototype.attr=function(name){name=String(name).toLowerCase();return Object.prototype.hasOwnProperty.call(this._attrs,name)?String(this._attrs[name]):'';};
2391
+ Element.prototype.hasAttr=function(name){return Object.prototype.hasOwnProperty.call(this._attrs,String(name).toLowerCase());};
2392
+ Element.prototype.id=function(){return this.attr('id');};
2393
+ Element.prototype.className=function(){return this.attr('class');};
2394
+ Element.prototype.children=function(){return new Elements(this._children.filter(function(n){return n instanceof Element;}));};
2395
+ Element.prototype.parent=function(){return this._parent instanceof Element?this._parent:null;};
2396
+ Element.prototype.nextElementSibling=function(){var p=this.parent();if(!p)return null;var a=p.children()._arr,i=a.indexOf(this);return i>=0&&i+1<a.length?a[i+1]:null;};
2397
+ Element.prototype.previousElementSibling=function(){var p=this.parent();if(!p)return null;var a=p.children()._arr,i=a.indexOf(this);return i>0?a[i-1]:null;};
2398
+ Element.prototype.remove=function(){if(!this._parent)return;var i=this._parent._children.indexOf(this);if(i>=0)this._parent._children.splice(i,1);this._parent=null;};
2399
+ // JavaScriptCore object liveness is GC-managed. The host resets accounting only at a new
2400
+ // invocation boundary; a script-visible reset would let still-live DOM objects bypass quota.
2401
+ Element.prototype.release=function(){};
2402
+ Element.prototype.ownText=function(){return normalizeText(this._children.filter(function(n){return n instanceof TextNode;}).map(function(n){return n.value;}).join(' '));};
2403
+ Element.prototype.text=function(){var out=[];(function visit(node){node._children.forEach(function(child){if(child instanceof TextNode)out.push(child.value);else visit(child);});})(this);return normalizeText(out.join(' '));};
2404
+ Element.prototype.html=function(){return this._children.map(function(n){return n.outerHtml();}).join('');};
2405
+ Element.prototype.outerHtml=function(){
2406
+ if(this._tag==='#document')return this.html();
2407
+ var attrs='';for(var key in this._attrs)if(Object.prototype.hasOwnProperty.call(this._attrs,key))attrs+=' '+key+'="'+escapeAttr(this._attrs[key])+'"';
2408
+ return '<'+this._tag+attrs+'>'+(voidTags[this._tag]?'':this.html()+'</'+this._tag+'>');
2409
+ };
2410
+ Element.prototype.absUrl=function(name){return resolveUrl(this._baseUri,this.attr(name));};
2411
+ Element.prototype.select=function(selector){selector=String(selector);var result=querySelectorAll(this,selector);nativeCall('domSelect',[selector,result.length]);return result;};
2412
+ Element.prototype.selectFirst=function(selector){var values=this.select(selector);return values.length?values[0]:null;};
2413
+ Element.prototype.getElementsByTag=function(tag){return this.select(String(tag));};
2414
+ Element.prototype.getElementsByClass=function(name){return this.select('.'+String(name));};
2415
+ Element.prototype.getElementById=function(id){return this.selectFirst('#'+String(id));};
2416
+ Element.prototype.toString=function(){return this.outerHtml();};
2417
+
2418
+ function Elements(values){this._arr=values||[];this.length=this._arr.length;for(var i=0;i<this.length;i++)this[i]=this._arr[i];}
2419
+ Elements.prototype.get=function(i){return i>=0&&i<this.length?this._arr[i]:null;};
2420
+ Elements.prototype.first=function(){return this.get(0);};Elements.prototype.last=function(){return this.get(this.length-1);};
2421
+ Elements.prototype.size=function(){return this.length;};Elements.prototype.isEmpty=function(){return this.length===0;};
2422
+ Elements.prototype.forEach=function(fn){for(var i=0;i<this.length;i++)fn(this._arr[i],i);};
2423
+ Elements.prototype.map=function(fn){var out=[];for(var i=0;i<this.length;i++)out.push(fn(this._arr[i],i));return out;};
2424
+ Elements.prototype.filter=function(fn){var out=[];for(var i=0;i<this.length;i++)if(fn(this._arr[i],i))out.push(this._arr[i]);return new Elements(out);};
2425
+ Elements.prototype.text=function(){return normalizeText(this.map(function(e){return e.text();}).join(' '));};
2426
+ Elements.prototype.attr=function(name){return this.length?this._arr[0].attr(name):'';};
2427
+ Elements.prototype.hasAttr=function(name){return this.length?this._arr[0].hasAttr(name):false;};
2428
+ Elements.prototype.html=function(){return this.length?this._arr[0].html():'';};
2429
+ Elements.prototype.select=function(selector){var out=[];this.forEach(function(e){e.select(selector).forEach(function(x){if(out.indexOf(x)<0)out.push(x);});});return new Elements(out);};
2430
+ Elements.prototype.eachAttr=function(name){return this.map(function(e){return e.attr(name);});};Elements.prototype.eachText=function(){return this.map(function(e){return e.text();});};
2431
+ Elements.prototype.releaseAll=function(){};
2432
+
2433
+ function normalizeText(value){return String(value||'').replace(/\\s+/g,' ').trim();}
2434
+ function resolveUrl(base,value){
2435
+ value=String(value||'');base=String(base||'');if(!value)return '';
2436
+ if(/^[a-z][a-z0-9+.-]*:/i.test(value))return value;
2437
+ var scheme=(base.match(/^([a-z][a-z0-9+.-]*):/i)||[])[1]||'https';
2438
+ if(value.substring(0,2)==='//')return scheme+':'+value;
2439
+ var origin=(base.match(/^([a-z][a-z0-9+.-]*:\\/\\/[^\\/]+)/i)||[])[1]||'';
2440
+ if(value.charAt(0)==='/')return origin+value;
2441
+ var clean=base.split('#')[0].split('?')[0],slash=clean.lastIndexOf('/');
2442
+ return (slash>=scheme.length+3?clean.substring(0,slash+1):clean.replace(/\\/?/,'/'))+value;
2443
+ }
2444
+ function parseAttributes(raw){
2445
+ var attrs={},re=/([^\\s=\\/]+)(?:\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s"'=<>\`]+)))?/g,m;
2446
+ while((m=re.exec(raw)))attrs[String(m[1]).toLowerCase()]=decodeEntities(m[2]!==undefined?m[2]:(m[3]!==undefined?m[3]:(m[4]!==undefined?m[4]:'')));
2447
+ return attrs;
2448
+ }
2449
+ function parseHtml(html,baseUri){
2450
+ html=String(html||'');var quota=nativeCall('domParse',[html])||{},maxNodes=Number(quota.maxNodes||0),maxDepth=Number(quota.maxDepth||0),nodeCount=1;
2451
+ function reserveNode(){nodeCount++;if(nodeCount>maxNodes){nativeCall('domParsed',[nodeCount]);throw new Error('Plugin DOM handle limit exceeded');}}
2452
+ var doc=new Document(baseUri||''),stack=[doc],i=0;
2453
+ while(i<html.length){
2454
+ var parent=stack[stack.length-1];
2455
+ if(parent._tag==='script'||parent._tag==='style'){
2456
+ var closeToken='</'+parent._tag,closeAt=html.toLowerCase().indexOf(closeToken,i);
2457
+ if(closeAt<0){reserveNode();parent._children.push(new TextNode(html.substring(i),parent,true));stack.pop();break;}
2458
+ reserveNode();parent._children.push(new TextNode(html.substring(i,closeAt),parent,true));
2459
+ var closeEnd=findTagEnd(html,closeAt+1);
2460
+ stack.pop();i=closeEnd<0?html.length:closeEnd+1;continue;
2461
+ }
2462
+ var open=html.indexOf('<',i);
2463
+ if(open<0){reserveNode();parent._children.push(new TextNode(decodeEntities(html.substring(i)),parent,false));break;}
2464
+ if(open>i){reserveNode();parent._children.push(new TextNode(decodeEntities(html.substring(i,open)),parent,false));}
2465
+ if(html.substring(open,open+4)==='<!--'){var endComment=html.indexOf('-->',open+4);i=endComment<0?html.length:endComment+3;continue;}
2466
+ var end=findTagEnd(html,open+1);if(end<0){reserveNode();parent._children.push(new TextNode(decodeEntities(html.substring(open)),parent,false));break;}
2467
+ var raw=html.substring(open+1,end).trim();i=end+1;if(!raw||raw.charAt(0)==='!'||raw.charAt(0)==='?')continue;
2468
+ if(raw.charAt(0)==='/'){
2469
+ var closing=raw.substring(1).trim().split(/\\s+/)[0].toLowerCase();
2470
+ for(var s=stack.length-1;s>0;s--)if(stack[s]._tag===closing){stack.length=s;break;}
2471
+ continue;
2472
+ }
2473
+ var selfClosing=/\\/\\s*$/.test(raw);if(selfClosing)raw=raw.replace(/\\/\\s*$/,'');
2474
+ var match=raw.match(/^([^\\s]+)/);if(!match)continue;var tag=match[1].toLowerCase();
2475
+ reserveNode();var element=new Element(tag,parseAttributes(raw.substring(match[0].length)),parent,baseUri||'',doc);
2476
+ parent._children.push(element);if(!selfClosing&&!voidTags[tag]){if(stack.length>=maxDepth){nativeCall('domDepth',[stack.length+1]);throw new Error('Plugin DOM depth limit exceeded');}stack.push(element);}
2477
+ }
2478
+ nativeCall('domParsed',[nodeCount]);
2479
+ return doc;
2480
+ }
2481
+ function findTagEnd(html,start){var quote='';for(var i=start;i<html.length;i++){var c=html.charAt(i);if(quote){if(c===quote)quote='';}else if(c==='"'||c==="'")quote=c;else if(c==='>')return i;}return -1;}
2482
+
2483
+ function splitTopLevel(value,separator){
2484
+ var out=[],start=0,round=0,square=0,quote='';
2485
+ for(var i=0;i<value.length;i++){var c=value.charAt(i);if(quote){if(c===quote&&value.charAt(i-1)!=='\\\\')quote='';continue;}if(c==='"'||c==="'"){quote=c;continue;}if(c==='(')round++;else if(c===')')round--;else if(c==='[')square++;else if(c===']')square--;else if(c===separator&&round===0&&square===0){out.push(value.substring(start,i));start=i+1;}}
2486
+ out.push(value.substring(start));return out;
2487
+ }
2488
+ function selectorSteps(selector){
2489
+ selector=selector.trim();var out=[],buf='',pending='desc',round=0,square=0,quote='';
2490
+ function push(){var value=buf.trim();if(value){out.push({combinator:pending,simple:value});buf='';pending='desc';}}
2491
+ for(var i=0;i<selector.length;i++){var c=selector.charAt(i);if(quote){buf+=c;if(c===quote&&selector.charAt(i-1)!=='\\\\')quote='';continue;}if(c==='"'||c==="'"){quote=c;buf+=c;continue;}if(c==='('){round++;buf+=c;continue;}if(c===')'){round--;buf+=c;continue;}if(c==='['){square++;buf+=c;continue;}if(c===']'){square--;buf+=c;continue;}
2492
+ if(round===0&&square===0&&(c==='>'||c==='+')){push();pending=c==='>'?'child':'adjacent';continue;}
2493
+ if(round===0&&square===0&&/\\s/.test(c)){if(buf.trim())push();continue;}buf+=c;
2494
+ }push();return out;
2495
+ }
2496
+ function descendants(element){var out=[];(function walk(node){node._children.forEach(function(child){if(child instanceof Element){out.push(child);walk(child);}});})(element);return out;}
2497
+ function candidates(element,combinator){if(combinator==='child')return element.children()._arr;if(combinator==='adjacent'){var n=element.nextElementSibling();return n?[n]:[];}return descendants(element);}
2498
+ function querySelectorAll(root,selector){
2499
+ var result=[];splitTopLevel(String(selector),',').forEach(function(group){var steps=selectorSteps(group),current=[root];steps.forEach(function(step){var next=[];current.forEach(function(node){candidates(node,step.combinator).forEach(function(candidate){if(matchesSimple(candidate,step.simple)&&next.indexOf(candidate)<0)next.push(candidate);});});current=next;});current.forEach(function(e){if(result.indexOf(e)<0)result.push(e);});});return new Elements(result);
2500
+ }
2501
+ function readBalanced(value,start,open,close){var depth=0,quote='';for(var i=start;i<value.length;i++){var c=value.charAt(i);if(quote){if(c===quote&&value.charAt(i-1)!=='\\\\')quote='';continue;}if(c==='"'||c==="'"){quote=c;continue;}if(c===open)depth++;else if(c===close){depth--;if(depth===0)return i;}}return value.length-1;}
2502
+ function stripQuotes(value){value=String(value||'').trim();if((value.charAt(0)==='"'&&value.charAt(value.length-1)==='"')||(value.charAt(0)==="'"&&value.charAt(value.length-1)==="'"))return value.substring(1,value.length-1);return value;}
2503
+ function matchesAttribute(element,body){
2504
+ var m=body.match(/^\\s*([^\\s~|^*!=\\x24]+)\\s*(?:(\\^=|\\*=|\\x24=|~=|\\|=|=)\\s*(?:"([^"]*)"|'([^']*)'|([^\\s]+)))?\\s*$/);if(!m)return false;
2505
+ var name=m[1].toLowerCase(),op=m[2],expected=m[3]!==undefined?m[3]:(m[4]!==undefined?m[4]:(m[5]!==undefined?m[5]:''));
2506
+ if(!element.hasAttr(name))return false;if(!op)return true;var actual=element.attr(name);
2507
+ if(op==='=')return actual===expected;if(op==='^=')return actual.indexOf(expected)===0;if(op==='*=')return actual.indexOf(expected)>=0;if(op.charCodeAt(0)===36)return actual.lastIndexOf(expected)===actual.length-expected.length;if(op==='~=')return actual.split(/\\s+/).indexOf(expected)>=0;if(op==='|=')return actual===expected||actual.indexOf(expected+'-')===0;return false;
2508
+ }
2509
+ function rootOf(element){var current=element;while(current._parent)current=current._parent;return current;}
2510
+ function matchesComplex(element,selector){
2511
+ return splitTopLevel(String(selector),',').some(function(group){
2512
+ var steps=selectorSteps(group);
2513
+ if(steps.length===1)return matchesSimple(element,steps[0].simple);
2514
+ return querySelectorAll(rootOf(element),group)._arr.indexOf(element)>=0;
2515
+ });
2516
+ }
2517
+ function matchesSimple(element,simple){
2518
+ var i=0,tag=simple.substring(i).match(/^[a-zA-Z*][a-zA-Z0-9_-]*/);if(tag){if(tag[0]!=='*'&&element._tag!==tag[0].toLowerCase())return false;i+=tag[0].length;}
2519
+ while(i<simple.length){var c=simple.charAt(i);
2520
+ if(c==='#'||c==='.'){var m=simple.substring(i+1).match(/^[a-zA-Z0-9_-]+/);if(!m)return false;var value=m[0];if(c==='#'&&element.id()!==value)return false;if(c==='.'&&element.className().split(/\\s+/).indexOf(value)<0)return false;i+=value.length+1;continue;}
2521
+ if(c==='['){var end=readBalanced(simple,i,'[',']');if(!matchesAttribute(element,simple.substring(i+1,end)))return false;i=end+1;continue;}
2522
+ if(c===':'){var nm=simple.substring(i+1).match(/^[a-zA-Z-]+/);if(!nm)return false;var name=nm[0].toLowerCase();i+=name.length+1;var arg=null;if(simple.charAt(i)==='('){var endp=readBalanced(simple,i,'(',')');arg=simple.substring(i+1,endp);i=endp+1;}
2523
+ var siblings=element.parent()?element.parent().children()._arr:[element],position=siblings.indexOf(element)+1;
2524
+ if(name==='contains'&&element.text().indexOf(stripQuotes(arg))<0)return false;
2525
+ // Internal predicate checks do not expose handles to the plugin.
2526
+ else if(name==='has'&&querySelectorAll(element,arg||'').isEmpty())return false;
2527
+ else if(name==='not'&&matchesComplex(element,arg||''))return false;
2528
+ else if(name==='first-child'&&position!==1)return false;
2529
+ else if(name==='last-child'&&position!==siblings.length)return false;
2530
+ else if(name==='nth-child'){var wanted=stripQuotes(arg);if(wanted==='odd'&&position%2!==1)return false;else if(wanted==='even'&&position%2!==0)return false;else if(wanted!=='odd'&&wanted!=='even'&&position!==parseInt(wanted,10))return false;}
2531
+ continue;
2532
+ }
2533
+ i++;
2534
+ }
2535
+ return true;
2536
+ }
2537
+
2538
+ global.Jsoup={parse:function(html,baseUri){return parseHtml(String(html),baseUri?String(baseUri):'');}};
2539
+ global.fetchAndParse=function(url,baseUri){var html=bridge.httpGet(String(url));return !html||html.error?null:Jsoup.parse(html,baseUri||url);};
2540
+ global.SManga={create:function(){return{url:'',title:'',author:null,artist:null,description:null,genre:null,status:0,thumbnailUrl:null,initialized:false};},UNKNOWN:0,ONGOING:1,COMPLETED:2,LICENSED:3,PUBLISHING_FINISHED:4,CANCELLED:5,ON_HIATUS:6};
2541
+ global.SChapter={create:function(){return{url:'',name:'',scanlator:null,dateUpload:0,chapterNumber:-1};}};
2542
+ global.Page=function(index,url,imageUrl){this.index=index||0;this.url=url||'';this.imageUrl=imageUrl||null;};
2543
+ global.MangasPage=function(mangas,hasNextPage){this.mangas=mangas||[];this.hasNextPage=!!hasNextPage;};
2544
+ if(!String.prototype.substringAfter)String.prototype.substringAfter=function(d){var i=this.indexOf(d);return i>=0?this.substring(i+d.length):String(this);};
2545
+ if(!String.prototype.substringBefore)String.prototype.substringBefore=function(d){var i=this.indexOf(d);return i>=0?this.substring(0,i):String(this);};
2546
+ if(!String.prototype.substringAfterLast)String.prototype.substringAfterLast=function(d){var i=this.lastIndexOf(d);return i>=0?this.substring(i+d.length):String(this);};
2547
+ if(!String.prototype.substringBeforeLast)String.prototype.substringBeforeLast=function(d){var i=this.lastIndexOf(d);return i>=0?this.substring(0,i):String(this);};
2548
+ global.setUrlWithoutDomain=function(object,url){var match=String(url).match(/^https?:\\/\\/[^\\/]+(\\/[^#]*)/i);object.url=match?match[1]:String(url);};
2549
+ global.__shinsouSelectSource=function(requestedId){
2550
+ if(requestedId!==null&&requestedId!==undefined&&typeof global.sources==='object'&&global.sources){
2551
+ var requested=String(requestedId),matches=[];
2552
+ Object.keys(global.sources).forEach(function(key){
2553
+ var candidate=global.sources[key];
2554
+ if(!candidate||typeof candidate!=='object')return;
2555
+ var candidateId=candidate.id!==undefined?candidate.id:(candidate.sourceId!==undefined?candidate.sourceId:key);
2556
+ if(String(candidateId)===requested)matches.push(candidate);
2557
+ });
2558
+ if(matches.length!==1)throw new Error('Plugin does not export exactly one source '+requested);
2559
+ global.source=matches[0];
2560
+ }
2561
+ if(typeof global.source!=='object'||!global.source)throw new Error('Plugin does not export source');
2562
+ if(requestedId!==null&&requestedId!==undefined){
2563
+ global.source.id=String(requestedId);
2564
+ if(global.__shinsouRequestedSourceName!==undefined)global.source.name=global.__shinsouRequestedSourceName;
2565
+ if(global.__shinsouRequestedSourceLang!==undefined)global.source.lang=global.__shinsouRequestedSourceLang;
2566
+ if(global.__shinsouRequestedSourceBaseUrl!==undefined)global.source.baseUrl=global.__shinsouRequestedSourceBaseUrl;
2567
+ }
2568
+ return true;
2569
+ };
2570
+ global.__shinsouMetadata=function(){if(typeof source!=='object'||!source)throw new Error('Plugin does not export source');return JSON.stringify({baseUrl:source.baseUrl||'',supportsLatest:!!source.supportsLatest,supportsLogin:!!source.supportsLogin,supportsFavorites:!!source.supportsFavorites,headers:source.headers||{},webChallengeUrl:source.webChallengeUrl||null,webChallengeLocalStorageKeys:Array.isArray(source.webChallengeLocalStorageKeys)?source.webChallengeLocalStorageKeys:[],requiredWebChallengeLocalStorageKeys:Array.isArray(source.requiredWebChallengeLocalStorageKeys)?source.requiredWebChallengeLocalStorageKeys:[]});};
2571
+ global.__shinsouPreferences=function(){if(typeof source!=='object'||!source)return '[]';var values=typeof source.getPreferenceDefinitions==='function'?source.getPreferenceDefinitions():(source.preferences||[]);return JSON.stringify(values||[]);};
2572
+ global.__shinsouInvoke=function(method,argsJson){if(typeof source!=='object'||!source||typeof source[method]!=='function')throw new Error('Plugin has no function '+method);var result=source[method].apply(source,JSON.parse(argsJson||'[]'));return JSON.stringify(result===undefined?null:result);};
2573
+ })(this);`;
2574
+
2575
+ // src/runner/sandbox.ts
2576
+ var DEFAULT_ENGINE_LIMITS = {
2577
+ heapBytes: 64 << 20,
2578
+ stackBytes: 1 << 20,
2579
+ invokeTimeoutMs: 3e4
2580
+ };
2581
+
2582
+ // src/runner/quickjs.ts
2583
+ var CLI_BOOTSTRAP = `
2584
+ (function(global){
2585
+ 'use strict';
2586
+ bridge.net = {
2587
+ fetch: function(url, options){
2588
+ return __shinsouBridgeAsync('net.fetch', JSON.stringify([String(url), options || {}])).then(function(text){ return JSON.parse(text); });
2589
+ }
2590
+ };
2591
+ global.__cliInvoke = function(method, argsJson){
2592
+ if (typeof source !== 'object' || !source || typeof source[method] !== 'function') throw new Error('Plugin has no function ' + method);
2593
+ return Promise.resolve(source[method].apply(source, JSON.parse(argsJson || '[]'))).then(function(v){
2594
+ return JSON.stringify(v === undefined ? null : v);
2595
+ });
2596
+ };
2597
+ })(this);
2598
+ `;
2599
+ var QuickJsEngine = class {
2600
+ name = "quickjs-emscripten";
2601
+ async create(args) {
2602
+ const QuickJS = await getQuickJS();
2603
+ const limits = args.limits ?? DEFAULT_ENGINE_LIMITS;
2604
+ const runtime = QuickJS.newRuntime();
2605
+ runtime.setMemoryLimit(limits.heapBytes);
2606
+ runtime.setMaxStackSize(limits.stackBytes);
2607
+ const ctx = runtime.newContext();
2608
+ const host = args.host;
2609
+ const bridgeFn = ctx.newFunction("__shinsouBridge", (opH, argsH) => {
2610
+ const outcome = host.call(ctx.getString(opH), ctx.getString(argsH));
2611
+ return ctx.newString(outcome.ok ? outcome.json ?? "null" : "null");
2612
+ });
2613
+ ctx.setProp(ctx.global, "__shinsouBridge", bridgeFn);
2614
+ bridgeFn.dispose();
2615
+ const asyncFn = ctx.newFunction("__shinsouBridgeAsync", (opH, argsH) => {
2616
+ const deferred = ctx.newPromise();
2617
+ host.callAsync(ctx.getString(opH), ctx.getString(argsH)).then(
2618
+ (outcome) => {
2619
+ if (!ctx.alive) return;
2620
+ if (outcome.ok) {
2621
+ const s = ctx.newString(outcome.json ?? "null");
2622
+ deferred.resolve(s);
2623
+ s.dispose();
2624
+ } else {
2625
+ const e = ctx.newError(`bridge error ${outcome.code}`);
2626
+ deferred.reject(e);
2627
+ e.dispose();
2628
+ }
2629
+ runtime.executePendingJobs();
2630
+ },
2631
+ () => {
2632
+ if (!ctx.alive) return;
2633
+ const e = ctx.newError("bridge host failure");
2634
+ deferred.reject(e);
2635
+ e.dispose();
2636
+ runtime.executePendingJobs();
2637
+ }
2638
+ );
2639
+ return deferred.handle;
2640
+ });
2641
+ ctx.setProp(ctx.global, "__shinsouBridgeAsync", asyncFn);
2642
+ asyncFn.dispose();
2643
+ const evalOrThrow = (code, filename) => {
2644
+ runtime.setInterruptHandler(shouldInterruptAfterDeadline(Date.now() + limits.invokeTimeoutMs));
2645
+ const r = ctx.evalCode(code, filename);
2646
+ if (r.error) {
2647
+ const message = describe(ctx, r.error);
2648
+ r.error.dispose();
2649
+ throw new Error(`${filename}: ${message}`);
2650
+ }
2651
+ r.value.dispose();
2652
+ };
2653
+ try {
2654
+ evalOrThrow(LEGACY_BOOTSTRAP, "bootstrap.js");
2655
+ evalOrThrow(CLI_BOOTSTRAP, "cli-bootstrap.js");
2656
+ evalOrThrow(args.script, "plugin.js");
2657
+ evalOrThrow(`__shinsouSelectSource(${JSON.stringify(args.sourceId)})`, "select-source.js");
2658
+ } catch (err) {
2659
+ ctx.dispose();
2660
+ runtime.dispose();
2661
+ throw err;
2662
+ }
2663
+ return {
2664
+ async invoke(method, argsJson) {
2665
+ const deadline = Date.now() + limits.invokeTimeoutMs;
2666
+ runtime.setInterruptHandler(shouldInterruptAfterDeadline(deadline));
2667
+ const fn = ctx.getProp(ctx.global, "__cliInvoke");
2668
+ const m = ctx.newString(method);
2669
+ const a = ctx.newString(argsJson);
2670
+ const r = ctx.callFunction(fn, ctx.undefined, m, a);
2671
+ fn.dispose();
2672
+ m.dispose();
2673
+ a.dispose();
2674
+ if (r.error) {
2675
+ const error = describe(ctx, r.error);
2676
+ r.error.dispose();
2677
+ return { ok: false, error };
2678
+ }
2679
+ const promise = r.value;
2680
+ const settled = ctx.resolvePromise(promise);
2681
+ promise.dispose();
2682
+ let done = false;
2683
+ settled.finally(() => done = true);
2684
+ while (!done) {
2685
+ const jobs = runtime.executePendingJobs();
2686
+ if (jobs.error) {
2687
+ const error = describe(ctx, jobs.error);
2688
+ jobs.error.dispose();
2689
+ return { ok: false, error };
2690
+ }
2691
+ if (Date.now() > deadline) return { ok: false, error: "invocation timed out", limit: "TIME" };
2692
+ await new Promise((resolve4) => setImmediate(resolve4));
2693
+ }
2694
+ const result = await settled;
2695
+ if (result.error) {
2696
+ const error = describe(ctx, result.error);
2697
+ result.error.dispose();
2698
+ return { ok: false, error };
2699
+ }
2700
+ const valueJson = ctx.getString(result.value);
2701
+ result.value.dispose();
2702
+ return { ok: true, valueJson };
2703
+ },
2704
+ dispose() {
2705
+ ctx.dispose();
2706
+ runtime.dispose();
2707
+ }
2708
+ };
2709
+ }
2710
+ };
2711
+ function describe(ctx, handle) {
2712
+ const v = ctx.dump(handle);
2713
+ if (v && typeof v === "object") {
2714
+ const e = v;
2715
+ return [e.name && e.message ? `${e.name}: ${e.message}` : e.message ?? JSON.stringify(v), e.stack].filter(Boolean).join("\n");
2716
+ }
2717
+ return String(v);
2718
+ }
2719
+
2720
+ // src/runner/harness.ts
2721
+ var DEFAULT_CASES = "test/cases.json";
2722
+ var DEFAULT_FIXTURES = "test/fixtures";
2723
+ function loadFixtures(dir) {
2724
+ if (!existsSync7(dir)) return [];
2725
+ const out = [];
2726
+ for (const name of readdirSync2(dir).filter((n) => n.endsWith(".json")).sort()) {
2727
+ const data = JSON.parse(readFileSync8(join9(dir, name), "utf8"));
2728
+ out.push(...Array.isArray(data) ? data : data.entries ?? []);
2729
+ }
2730
+ return out;
2731
+ }
2732
+ async function runTests(args) {
2733
+ const { projectDir, manifest } = args;
2734
+ const casesPath = join9(projectDir, manifest.test?.cases ?? DEFAULT_CASES);
2735
+ const fixturesDir = join9(projectDir, manifest.test?.fixtures ?? DEFAULT_FIXTURES);
2736
+ if (!existsSync7(casesPath)) throw new Error(`No test cases: ${casesPath}`);
2737
+ const cases = JSON.parse(readFileSync8(casesPath, "utf8")).filter((c) => !args.only || c.name.includes(args.only));
2738
+ const engine = args.engine ?? new QuickJsEngine();
2739
+ const results = [];
2740
+ for (const c of cases) {
2741
+ const source = manifest.sources.find((s) => s.sourceId === (c.sourceId ?? manifest.sources[0].sourceId));
2742
+ if (!source) {
2743
+ results.push({ name: c.name, ok: false, problems: [`unknown sourceId ${c.sourceId}`], events: [], logs: [] });
2744
+ continue;
2745
+ }
2746
+ let result;
2747
+ for (let round = 0; round < 20; round++) {
2748
+ const fixtures = loadFixtures(fixturesDir);
2749
+ const host = new FixtureBridgeHost(manifest, source, fixtures, { preferences: c.preferences, credential: c.credential });
2750
+ const problems = [];
2751
+ let value2;
2752
+ try {
2753
+ const instance = await engine.create({ script: args.script, sourceId: source.sourceId, host });
2754
+ try {
2755
+ const out = await instance.invoke(c.hook, JSON.stringify(c.args ?? []));
2756
+ if (out.ok) value2 = JSON.parse(out.valueJson);
2757
+ else problems.push(`hook threw: ${out.error}`);
2758
+ } finally {
2759
+ instance.dispose();
2760
+ }
2761
+ } catch (err) {
2762
+ problems.push(`script failed to load: ${err.message}`);
2763
+ }
2764
+ if (args.record && host.misses.length) {
2765
+ await recordMisses(fixturesDir, c.name, host.misses, args.fetchImpl ?? fetch);
2766
+ continue;
2767
+ }
2768
+ for (const e of host.events) if (e.kind !== "log") problems.push(`${e.kind}: bridge.${e.op} \u2014 ${e.detail}`);
2769
+ if (problems.length === 0) problems.push(...checkExpectations(projectDir, c, value2, !!args.update));
2770
+ result = { name: c.name, ok: problems.length === 0, problems, events: host.events, logs: host.logs, value: value2 };
2771
+ break;
2772
+ }
2773
+ results.push(result ?? { name: c.name, ok: false, problems: ["recording did not converge after 20 rounds"], events: [], logs: [] });
2774
+ }
2775
+ return results;
2776
+ }
2777
+ async function recordMisses(dir, caseName, misses, fetchImpl) {
2778
+ mkdirSync6(dir, { recursive: true });
2779
+ const file = join9(dir, `${caseName.replace(/[^\w.-]+/g, "_")}.json`);
2780
+ const existing = existsSync7(file) ? JSON.parse(readFileSync8(file, "utf8")) : [];
2781
+ const keys = new Set(existing.map((f) => fixtureKey(f.request.method, f.request.url, f.request.body ?? "")));
2782
+ for (const miss of misses) {
2783
+ const key = fixtureKey(miss.method, miss.url, miss.body);
2784
+ if (keys.has(key)) continue;
2785
+ keys.add(key);
2786
+ const res = await fetchImpl(miss.url, {
2787
+ method: miss.method,
2788
+ headers: miss.headers,
2789
+ body: miss.method === "GET" ? void 0 : miss.body,
2790
+ redirect: "follow"
2791
+ });
2792
+ const headers = {};
2793
+ res.headers.forEach((v, k) => {
2794
+ if (!["set-cookie", "authorization", "cookie"].includes(k)) headers[k] = v;
2795
+ });
2796
+ existing.push({
2797
+ request: { method: miss.method, url: miss.url, ...miss.method === "GET" ? {} : { body: miss.body } },
2798
+ response: { status: res.status, headers, body: await res.text() }
2799
+ });
2800
+ }
2801
+ writeFileSync6(file, JSON.stringify(existing, null, 2) + "\n");
2802
+ }
2803
+ function at(value2, path) {
2804
+ if (!path) return value2;
2805
+ return path.split(".").reduce((v, k) => v && typeof v === "object" ? v[k] : void 0, value2);
2806
+ }
2807
+ var MAX_CHAPTER_TEXT_BYTES = 4 * 1024 * 1024;
2808
+ function chapterTextProblems(result) {
2809
+ const text = typeof result === "string" ? result : result && typeof result === "object" ? result.text : void 0;
2810
+ if (typeof text !== "string") return ["getChapterText must return a string (or { text }); the app ignores anything else"];
2811
+ if (text.length === 0) return ["getChapterText returned an empty string (the app treats it as no text)"];
2812
+ if (Buffer.byteLength(text, "utf8") > MAX_CHAPTER_TEXT_BYTES) return ["getChapterText result exceeds the 4 MiB result limit"];
2813
+ return [];
2814
+ }
2815
+ function checkExpectations(projectDir, c, result, update) {
2816
+ const problems = [];
2817
+ if (c.hook === "getChapterText") problems.push(...chapterTextProblems(result));
2818
+ const e = c.expect;
2819
+ if (!e) return problems;
2820
+ const v = at(result, e.path);
2821
+ const label = e.path ? `result.${e.path}` : "result";
2822
+ if (e.notEmpty && (v === null || v === void 0 || v === "" || Array.isArray(v) && v.length === 0)) problems.push(`${label} is empty`);
2823
+ if (e.minItems !== void 0) {
2824
+ if (!Array.isArray(v)) problems.push(`${label} is not an array`);
2825
+ else if (v.length < e.minItems) problems.push(`${label} has ${v.length} items, expected at least ${e.minItems}`);
2826
+ }
2827
+ if ("equals" in e && !isDeepStrictEqual3(v, e.equals)) problems.push(`${label} = ${JSON.stringify(v)}, expected ${JSON.stringify(e.equals)}`);
2828
+ if (e.contains !== void 0 && !(typeof v === "string" ? v : JSON.stringify(v) ?? "").includes(e.contains)) {
2829
+ problems.push(`${label} does not contain ${JSON.stringify(e.contains)}`);
2830
+ }
2831
+ for (const [k, want] of Object.entries(e.matches ?? {})) {
2832
+ const got = at(v, k);
2833
+ if (!isDeepStrictEqual3(got, want)) problems.push(`${label}.${k} = ${JSON.stringify(got)}, expected ${JSON.stringify(want)}`);
2834
+ }
2835
+ if (e.snapshot) {
2836
+ const dir = join9(projectDir, "test", "snapshots");
2837
+ const file = join9(dir, `${c.name.replace(/[^\w.-]+/g, "_")}.json`);
2838
+ if (update || !existsSync7(file)) {
2839
+ mkdirSync6(dir, { recursive: true });
2840
+ writeFileSync6(file, JSON.stringify(result, null, 2) + "\n");
2841
+ } else if (!isDeepStrictEqual3(JSON.parse(readFileSync8(file, "utf8")), result)) {
2842
+ problems.push(`result differs from snapshot ${file} (run with --update to accept)`);
2843
+ }
2844
+ }
2845
+ return problems;
2846
+ }
2847
+
2848
+ // src/cli.ts
2849
+ import { cpSync as cpSync2, mkdirSync as mkdirSync7 } from "node:fs";
2850
+ import { join as join10 } from "node:path";
2851
+ var VERSION = true ? "0.1.0" : "0.0.0-dev";
2852
+ var HELP = `shinsou-plugin ${VERSION} \u2014 Shinsou X \u5957\u4EF6\u4F5C\u8005\u5DE5\u5177
2853
+
2854
+ \u7528\u6CD5\uFF1Ashinsou-plugin <\u6307\u4EE4> [\u9078\u9805]
2855
+
2856
+ keygen [--name <\u540D\u7A31>] [--passphrase] \u7522\u751F Ed25519 \u767C\u4F48\u8005\u91D1\u9470
2857
+ fingerprint [--key <\u540D\u7A31>] \u986F\u793A\u91D1\u9470\u6307\u7D0B
2858
+ init [\u76EE\u9304] [--template manga|novel|mixed|async] [--id <packageId>] [--name <\u540D\u7A31>] [--lang <\u8A9E\u8A00>]
2859
+ build \u6253\u5305\u8173\u672C\u4E26\u7522\u751F index.json \u8207 sidecar
2860
+ lint \u6B0A\u9650\u3001origin\u3001\u540D\u7A31\u8207\u5927\u5C0F\u6AA2\u67E5
2861
+ test [--record] [--update] [--only <\u540D\u7A31>] \u4EE5\u9304\u88FD\u7684 HTTP fixture \u57F7\u884C hook
2862
+ serve [--host 127.0.0.1|lan|<\u5340\u7DB2 IP>] [--port <\u57E0>] \u958B\u767C\u8005\u7528\u672A\u7C3D\u7AE0\u5132\u5B58\u5EAB\uFF08\u5373\u6642\u91CD\u5EFA\uFF09
2863
+ sign [--key <\u540D\u7A31>] [--mirror <URL>]... [--out <\u76EE\u9304>] \u7C3D\u7F72 repo/index/sidecar/\u64A4\u56DE\u6E05\u55AE
2864
+ sign-repo --mirror <URL>... [--key <\u540D\u7A31>] [--out <\u76EE\u9304>] [--flat] \u7C3D\u7F72\u65E2\u6709\u7684\u591A\u5957\u4EF6\u5132\u5B58\u5EAB\uFF08repo.json/index.json/sidecars\uFF09
2865
+ refresh [--key <\u540D\u7A31>] [--mirror <URL>]... [--if-due] [--flat] \u91CD\u7C3D\u64A4\u56DE\u6E05\u55AE\uFF08\u5EF6\u9577 expiresAt\uFF1B--if-due \u53EA\u5728\u5269\u4E0D\u5230\u4E00\u534A\u6548\u671F\u6642\u91CD\u7C3D\uFF09
2866
+ revoke <sha256>... [--key <\u540D\u7A31>] \u64A4\u56DE\u5DF2\u767C\u4F48\u7684\u7248\u672C
2867
+ rotate --to <\u65B0\u91D1\u9470> [--key <\u820A\u91D1\u9470>] \u91D1\u9470\u8F2A\u66FF\uFF08\u96D9\u7C3D\uFF09\u4E26\u4EE5\u65B0\u91D1\u9470\u91CD\u7C3D
2868
+ revoke-key [--key <\u540D\u7A31>] \u91D1\u9470\u81EA\u6211\u64A4\u92B7
2869
+ ci-template \u5BEB\u5165\u5B9A\u671F refresh \u7684 GitHub Actions
2870
+
2871
+ \u5171\u540C\u9078\u9805\uFF1A--dir <\u5C08\u6848\u76EE\u9304>\uFF08\u9810\u8A2D\u76EE\u524D\u76EE\u9304\uFF09\u3001--days <\u64A4\u56DE\u6E05\u55AE\u6709\u6548\u5929\u6578\uFF0C\u9810\u8A2D 30>
2872
+ \u74B0\u5883\u8B8A\u6578\uFF1ASHINSOU_PLUGIN_PASSPHRASE\u3001SHINSOU_PLUGIN_NEW_PASSPHRASE\u3001SHINSOU_PLUGIN_KEY\uFF08CI \u7528\u91D1\u9470 JSON\uFF09\u3001
2873
+ SHINSOU_PLUGIN_HOME\uFF08\u91D1\u9470\u76EE\u9304\uFF0C\u9810\u8A2D ~/.config/shinsou-plugin\uFF09`;
2874
+ async function prompt(question, hidden) {
2875
+ const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
2876
+ if (hidden) {
2877
+ const r = rl;
2878
+ r._writeToOutput = (s) => {
2879
+ if (s.includes(question)) r.output.write(s);
2880
+ };
2881
+ }
2882
+ return new Promise(
2883
+ (done) => rl.question(question, (answer) => {
2884
+ rl.close();
2885
+ if (hidden) process.stderr.write("\n");
2886
+ done(answer);
2887
+ })
2888
+ );
2889
+ }
2890
+ async function passphraseFor(name, envName = "SHINSOU_PLUGIN_PASSPHRASE") {
2891
+ if (process.env[envName]) return process.env[envName];
2892
+ if (!process.stdin.isTTY) return void 0;
2893
+ return prompt(`\u91D1\u9470 '${name}' \u7684\u5BC6\u8A9E\uFF1A`, true);
2894
+ }
2895
+ async function loadKey(name, envName) {
2896
+ const file = readKeyFile(name);
2897
+ return unlockKeyFile(file, file.encryption ? await passphraseFor(name, envName) : void 0);
2898
+ }
2899
+ function defaultKeyName(dir, given) {
2900
+ return given ?? loadState(dir).currentKey?.name ?? "default";
2901
+ }
2902
+ function printSigned(outputs) {
2903
+ for (const o of outputs) {
2904
+ console.log(`\u2713 ${o.mirror}`);
2905
+ console.log(` \u2192 ${o.dir}`);
2906
+ for (const d of o.documents) console.log(` ${d.documentId} sequence=${d.sequence}`);
2907
+ }
2908
+ }
2909
+ async function main(argv) {
2910
+ const [command, ...rest] = argv;
2911
+ if (!command || command === "help" || command === "--help" || command === "-h") {
2912
+ console.log(HELP);
2913
+ return 0;
2914
+ }
2915
+ if (command === "--version" || command === "-v") {
2916
+ console.log(VERSION);
2917
+ return 0;
2918
+ }
2919
+ const { values, positionals } = parseArgs({
2920
+ args: rest,
2921
+ allowPositionals: true,
2922
+ options: {
2923
+ dir: { type: "string" },
2924
+ name: { type: "string" },
2925
+ key: { type: "string" },
2926
+ to: { type: "string" },
2927
+ template: { type: "string" },
2928
+ id: { type: "string" },
2929
+ lang: { type: "string" },
2930
+ passphrase: { type: "boolean" },
2931
+ mirror: { type: "string", multiple: true },
2932
+ out: { type: "string" },
2933
+ days: { type: "string" },
2934
+ host: { type: "string" },
2935
+ port: { type: "string" },
2936
+ record: { type: "boolean" },
2937
+ update: { type: "boolean" },
2938
+ only: { type: "string" },
2939
+ force: { type: "boolean" },
2940
+ flat: { type: "boolean" },
2941
+ "if-due": { type: "boolean" }
2942
+ }
2943
+ });
2944
+ const dir = resolve3(values.dir ?? (command === "init" ? positionals[0] ?? "." : "."));
2945
+ const ttlSeconds = values.days ? Math.round(Number(values.days) * 86400) : void 0;
2946
+ if (ttlSeconds !== void 0 && (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0)) throw new Error("--days \u5FC5\u9808\u662F\u6B63\u6578");
2947
+ switch (command) {
2948
+ case "keygen": {
2949
+ const name = values.name ?? "default";
2950
+ if (existsSync8(keyPath(name)) && !values.force) throw new Error(`\u91D1\u9470 '${name}' \u5DF2\u5B58\u5728\uFF08${keyPath(name)}\uFF09\uFF1B\u8981\u8986\u5BEB\u8ACB\u52A0 --force\uFF0C\u4F46\u820A\u91D1\u9470\u7C3D\u904E\u7684\u5132\u5B58\u5EAB\u5C07\u7121\u6CD5\u518D\u66F4\u65B0`);
2951
+ let passphrase = process.env.SHINSOU_PLUGIN_PASSPHRASE;
2952
+ if (!passphrase && process.stdin.isTTY) {
2953
+ passphrase = await prompt(values.passphrase ? "\u8A2D\u5B9A\u5BC6\u8A9E\uFF1A" : "\u8A2D\u5B9A\u5BC6\u8A9E\uFF08\u7559\u7A7A\u5247\u4E0D\u52A0\u5BC6\uFF09\uFF1A", true);
2954
+ if (passphrase) {
2955
+ const again = await prompt("\u518D\u8F38\u5165\u4E00\u6B21\uFF1A", true);
2956
+ if (again !== passphrase) throw new Error("\u5169\u6B21\u8F38\u5165\u7684\u5BC6\u8A9E\u4E0D\u540C");
2957
+ }
2958
+ }
2959
+ if (values.passphrase && !passphrase) throw new Error("--passphrase \u9700\u8981\u5BC6\u8A9E\uFF08\u4E92\u52D5\u8F38\u5165\u6216 SHINSOU_PLUGIN_PASSPHRASE\uFF09");
2960
+ const file = createKeyFile({ name, passphrase: passphrase || void 0 });
2961
+ const path = saveKeyFile(file, { overwrite: !!values.force });
2962
+ const fp = describeKey(file);
2963
+ console.log(`\u5DF2\u7522\u751F\u91D1\u9470 '${name}'${file.encryption ? "\uFF08\u4EE5\u5BC6\u8A9E\u52A0\u5BC6\uFF09" : "\uFF08\u672A\u52A0\u5BC6\uFF09"}\uFF1A${path}`);
2964
+ console.log(`\u5B8C\u6574\u6307\u7D0B\uFF1A${fp.fingerprint}`);
2965
+ console.log(`\u986F\u793A\u6307\u7D0B\uFF1A${fp.display}`);
2966
+ console.log("\u8ACB\u5099\u4EFD\u9019\u500B\u6A94\u6848\uFF1B\u907A\u5931\u79C1\u9470\u5F8C\u53EA\u80FD\u8ACB\u4F7F\u7528\u8005\u624B\u52D5\u6539\u4FE1\u4EFB\u65B0\u91D1\u9470\u3002");
2967
+ return 0;
2968
+ }
2969
+ case "fingerprint": {
2970
+ const file = readKeyFile(defaultKeyName(dir, values.key));
2971
+ const fp = describeKey(file);
2972
+ console.log(`${file.name}
2973
+ \u5B8C\u6574\u6307\u7D0B\uFF1A${fp.fingerprint}
2974
+ \u986F\u793A\u6307\u7D0B\uFF1A${fp.display}
2975
+ \u516C\u9470\uFF1A${file.publicKey}`);
2976
+ return 0;
2977
+ }
2978
+ case "init": {
2979
+ const template = values.template ?? "manga";
2980
+ if (!TEMPLATES.includes(template)) throw new Error(`--template \u5FC5\u9808\u662F ${TEMPLATES.join(" | ")}`);
2981
+ const files = initProject({ dir, template, packageId: values.id, name: values.name, lang: values.lang, cliVersion: VERSION });
2982
+ console.log(`\u5DF2\u5728 ${dir} \u5EFA\u7ACB ${template} \u5957\u4EF6\u5C08\u6848\uFF1A
2983
+ ${files.join("\n ")}`);
2984
+ console.log("\u4E0B\u4E00\u6B65\uFF1A\u7DE8\u8F2F shinsou.plugin.json\uFF08\u5957\u4EF6 ID\u3001\u540D\u7A31\u3001origin\u3001repository.mirrors\uFF09\uFF0C\u7136\u5F8C\u57F7\u884C shinsou-plugin build && shinsou-plugin test");
2985
+ return 0;
2986
+ }
2987
+ case "build": {
2988
+ const r = await buildProject(dir);
2989
+ console.log(`\u2713 ${r.manifest.packageId} ${r.manifest.version} (${r.manifest.versionCode})`);
2990
+ console.log(` sha256 ${r.sha256}
2991
+ ${r.byteSize} bytes
2992
+ \u2192 ${r.outDir}`);
2993
+ return 0;
2994
+ }
2995
+ case "lint": {
2996
+ const r = await buildProject(dir, { write: false });
2997
+ const report = lintBuild(r);
2998
+ for (const w of report.warnings) console.log(`\u26A0 [${w.rule}] ${w.message}`);
2999
+ for (const e of report.errors) console.log(`\u2716 [${e.rule}] ${e.message}`);
3000
+ console.log(report.errors.length ? `lint \u5931\u6557\uFF1A${report.errors.length} \u500B\u932F\u8AA4\u3001${report.warnings.length} \u500B\u8B66\u544A` : `lint \u901A\u904E\uFF08${report.warnings.length} \u500B\u8B66\u544A\uFF09`);
3001
+ return report.errors.length ? 1 : 0;
3002
+ }
3003
+ case "test": {
3004
+ const r = await buildProject(dir);
3005
+ const results = await runTests({ projectDir: dir, manifest: r.manifest, script: r.script, record: values.record, update: values.update, only: values.only });
3006
+ for (const res of results) {
3007
+ console.log(`${res.ok ? "\u2713" : "\u2716"} ${res.name}`);
3008
+ for (const p of res.problems) console.log(` ${p}`);
3009
+ if (!res.ok) for (const l of res.logs.slice(-10)) console.log(` log: ${l}`);
3010
+ }
3011
+ const failed = results.filter((x) => !x.ok).length;
3012
+ console.log(failed ? `${failed}/${results.length} \u500B\u6E2C\u8A66\u5931\u6557` : `${results.length} \u500B\u6E2C\u8A66\u5168\u90E8\u901A\u904E`);
3013
+ return failed ? 1 : 0;
3014
+ }
3015
+ case "serve": {
3016
+ const s = await serveProject({ projectDir: dir, host: values.host ?? "127.0.0.1", port: Number(values.port ?? 8787) });
3017
+ console.log(`
3018
+ \u958B\u767C\u8005\u5132\u5B58\u5EAB\uFF08\u672A\u7C3D\u7AE0\uFF0C\u53EA\u80FD\u5728 App \u7684\u300C\u5957\u4EF6\u958B\u767C\u8005\u6A21\u5F0F\u300D\u8F09\u5165\uFF09\uFF1A${s.baseUrl}`);
3019
+ console.log(`\u52A0\u5165\u9023\u7D50\uFF1A${s.link}
3020
+ `);
3021
+ qrcode.generate(s.link, { small: true });
3022
+ console.log("\u4FEE\u6539\u6A94\u6848\u6703\u81EA\u52D5\u91CD\u5EFA\uFF1B\u6309 Ctrl+C \u7D50\u675F\u3002");
3023
+ await new Promise(() => {
3024
+ });
3025
+ return 0;
3026
+ }
3027
+ case "sign": {
3028
+ const key = await loadKey(defaultKeyName(dir, values.key));
3029
+ printSigned(await signRepository({ projectDir: dir, key, mirrors: values.mirror, outDir: values.out, ttlSeconds }));
3030
+ console.log("\u628A\u6BCF\u500B\u93E1\u50CF\u76EE\u9304\u7684\u5167\u5BB9\u539F\u6A23\u4E0A\u50B3\u5230\u5C0D\u61C9\u7684\u7DB2\u5740\u5373\u53EF\uFF1B.shinsou/state.json \u5FC5\u9808\u4E00\u8D77\u63D0\u4EA4\u3002");
3031
+ return 0;
3032
+ }
3033
+ case "sign-repo": {
3034
+ const key = await loadKey(defaultKeyName(dir, values.key));
3035
+ const r = await signExistingRepository({ repoDir: dir, key, mirrors: values.mirror ?? [], outDir: values.out, ttlSeconds, flat: values.flat });
3036
+ printSigned(r.outputs);
3037
+ console.log(`\u5DF2\u7C3D\u7F72 ${r.packages.length} \u500B\u5957\u4EF6\u3002`);
3038
+ if (r.unchecked.length) console.log(`\u53EA\u6838\u5C0D\u4E86 SHA-256\uFF08\u975E Shinsou \u5951\u7D04\uFF0CApp \u6703\u7565\u904E\uFF09\uFF1A${r.unchecked.join("\u3001")}`);
3039
+ console.log("\u628A\u93E1\u50CF\u76EE\u9304\u7684\u5167\u5BB9\u653E\u5230\u8A72\u7DB2\u5740\u7684\u6839\u76EE\u9304\u5373\u53EF\uFF1B.shinsou/state.json \u5FC5\u9808\u4E00\u8D77\u63D0\u4EA4\uFF08\u5E8F\u865F\u8207\u64A4\u56DE\u6E05\u55AE\uFF09\u3002");
3040
+ return 0;
3041
+ }
3042
+ case "refresh": {
3043
+ const key = await loadKey(defaultKeyName(dir, values.key));
3044
+ const signed = refreshRevocations({ projectDir: dir, key, mirrors: values.mirror, outDir: values.out, ttlSeconds, flat: values.flat, onlyIfDue: values["if-due"] });
3045
+ if (!signed.length) console.log("\u64A4\u56DE\u6E05\u55AE\u9084\u5728\u6709\u6548\u671F\u7684\u524D\u534A\uFF0C\u4E0D\u9700\u8981\u91CD\u7C3D\u3002");
3046
+ printSigned(signed);
3047
+ return 0;
3048
+ }
3049
+ case "revoke": {
3050
+ if (!positionals.length) throw new Error("\u7528\u6CD5\uFF1Ashinsou-plugin revoke <sha256>...");
3051
+ const key = await loadKey(defaultKeyName(dir, values.key));
3052
+ printSigned(refreshRevocations({ projectDir: dir, key, mirrors: values.mirror, outDir: values.out, ttlSeconds, add: positionals }));
3053
+ console.log("\u5DF2\u64A4\u56DE\uFF1BApp \u4E0B\u6B21\u53D6\u5F97\u64A4\u56DE\u6E05\u55AE\u6642\u6703\u505C\u7528\u9019\u4E9B\u7248\u672C\u3002");
3054
+ return 0;
3055
+ }
3056
+ case "rotate": {
3057
+ if (!values.to) throw new Error("\u7528\u6CD5\uFF1Ashinsou-plugin rotate --to <\u65B0\u91D1\u9470\u540D\u7A31> [--key <\u820A\u91D1\u9470\u540D\u7A31>]");
3058
+ const oldKey = await loadKey(defaultKeyName(dir, values.key));
3059
+ const newKey = await loadKey(values.to, "SHINSOU_PLUGIN_NEW_PASSPHRASE");
3060
+ const r = await rotateKey({ projectDir: dir, oldKey, newKey, mirrors: values.mirror, outDir: values.out });
3061
+ console.log(`\u8F2A\u66FF\u8072\u660E\uFF08effectiveSequence=${r.effectiveSequence}\uFF0C\u820A\u91D1\u9470\u7C3D envelope\u3001\u65B0\u91D1\u9470\u7C3D newKeySignature\uFF09\uFF1A`);
3062
+ printSigned(r.rotation);
3063
+ console.log("\u5DF2\u7528\u65B0\u91D1\u9470\u91CD\u7C3D\uFF1A");
3064
+ printSigned(r.resigned);
3065
+ const fp = describeKey(newKey);
3066
+ console.log(`\u65B0\u6307\u7D0B\uFF1A${fp.fingerprint}\uFF08${fp.display}\uFF09\u3002publisher-key-rotation.json \u8ACB\u9577\u671F\u4FDD\u7559\u5728\u5132\u5B58\u5EAB\u4E2D\u3002`);
3067
+ return 0;
3068
+ }
3069
+ case "revoke-key": {
3070
+ const name = defaultKeyName(dir, values.key);
3071
+ const key = await loadKey(name);
3072
+ printSigned(revokeKey({ projectDir: dir, key, mirrors: values.mirror, outDir: values.out }));
3073
+ console.log(`\u91D1\u9470 '${name}' \u5DF2\u81EA\u6211\u64A4\u92B7\u3002\u82E5\u662F\u76EE\u524D\u91D1\u9470\uFF0C\u4F7F\u7528\u8005\u5FC5\u9808\u91CD\u65B0\u4FE1\u4EFB\u4F60\u7684\u65B0\u91D1\u9470\u3002`);
3074
+ return 0;
3075
+ }
3076
+ case "ci-template": {
3077
+ const target = join10(dir, ".github", "workflows");
3078
+ mkdirSync7(target, { recursive: true });
3079
+ cpSync2(join10(templatesDir(), "common", ".github", "workflows", "shinsou-refresh.yml"), join10(target, "shinsou-refresh.yml"));
3080
+ console.log(`\u5DF2\u5BEB\u5165 ${join10(target, "shinsou-refresh.yml")}\uFF1B\u8ACB\u5728 GitHub \u8A2D\u5B9A SHINSOU_PLUGIN_KEY\uFF08\u8207 SHINSOU_PLUGIN_PASSPHRASE\uFF09secrets\u3002`);
3081
+ return 0;
3082
+ }
3083
+ default:
3084
+ console.error(`\u672A\u77E5\u6307\u4EE4\uFF1A${command}
3085
+
3086
+ ${HELP}`);
3087
+ return 2;
3088
+ }
3089
+ }
3090
+ main(process.argv.slice(2)).then(
3091
+ (code) => process.exit(code),
3092
+ (err) => {
3093
+ console.error(`\u932F\u8AA4\uFF1A${err.message}`);
3094
+ process.exit(1);
3095
+ }
3096
+ );