@seojin08/skill-finder 0.3.3 → 0.3.4

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +2 -1
  3. package/seed-index.json.gz +0 -0
  4. package/server/src/app.js +3 -0
  5. package/server/src/bootstrap.js +2 -1
  6. package/server/src/db/schema.js +3 -0
  7. package/server/src/db/seed.js +89 -0
  8. package/server/src/db/store.js +4 -3
  9. package/server/src/index.js +11 -0
  10. package/server/src/indexer.js +5 -1
  11. package/server/src/install/installed.js +95 -25
  12. package/server/src/recommend/readme.js +14 -2
  13. package/server/src/routes/favorites.js +55 -10
  14. package/server/src/routes/install.js +33 -7
  15. package/server/src/routes/translate.js +35 -0
  16. package/server/src/search/result.js +9 -1
  17. package/server/src/security.js +4 -1
  18. package/server/src/sources/gitTree.js +11 -1
  19. package/server/src/sources/github.js +17 -2
  20. package/server/src/tasks.js +45 -5
  21. package/server/src/translate/models.js +140 -0
  22. package/web/dist/THIRD-PARTY-LICENSES.txt +6 -0
  23. package/web/dist/assets/{ArasakaGame-C1Wkmqik.js → ArasakaGame-B2qY9Lds.js} +2 -2
  24. package/web/dist/assets/__vite-browser-external-BDfy7Drd.js +1 -0
  25. package/web/dist/assets/index-BJ6vm3xw.js +66 -0
  26. package/web/dist/assets/index-BiikysiH.css +2 -0
  27. package/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  28. package/web/dist/assets/translator-Cyk_ZOaM.js +2 -0
  29. package/web/dist/bergamot/bergamot-translator-worker.js +2919 -0
  30. package/web/dist/bergamot/bergamot-translator-worker.wasm +0 -0
  31. package/web/dist/bergamot/translator-worker.js +475 -0
  32. package/web/dist/index.html +3 -2
  33. package/web/dist/assets/index-BgL1OaU4.css +0 -2
  34. package/web/dist/assets/index-Ec2XBK4-.js +0 -66
@@ -0,0 +1,35 @@
1
+ import express, { Router } from 'express';
2
+ import { MODEL_LANGS, MODEL_URL_PATH } from "../translate/models.js";
3
+ const langOf = (v) => (MODEL_LANGS.includes(v) ? v : null);
4
+ /** The translation model for browsers without a built-in translator: its state, and the one-time download. */
5
+ export function translateRouter(ctx) {
6
+ const r = Router();
7
+ r.get('/translate/models/:lang', (req, res) => {
8
+ const lang = langOf(req.params.lang);
9
+ if (!lang)
10
+ return void res.status(404).json({ error: '번역 모델이 없는 언어입니다' });
11
+ res.json(ctx.translateModels.status(lang));
12
+ });
13
+ r.post('/translate/models/:lang', (req, res) => {
14
+ const lang = langOf(req.params.lang);
15
+ if (!lang)
16
+ return void res.status(404).json({ error: '번역 모델이 없는 언어입니다' });
17
+ res.json(ctx.translateModels.start(lang));
18
+ });
19
+ return r;
20
+ }
21
+ /**
22
+ * The downloaded files and the engine's registry, outside /api: the engine fetches them without the API header.
23
+ * Read only; the registry's addresses carry each file's hash (?v=), so the files are cached for good.
24
+ */
25
+ export function translateFiles(ctx) {
26
+ const r = Router();
27
+ r.get(`${MODEL_URL_PATH}/index.json`, (_req, res) => {
28
+ res.setHeader('Cache-Control', 'no-cache');
29
+ res.json(ctx.translateModels.registry());
30
+ });
31
+ r.use(MODEL_URL_PATH, express.static(ctx.translateModels.root, { index: false, dotfiles: 'deny', maxAge: '30d', immutable: true }));
32
+ // a file that is not there is a 404, not the page's index.html (the engine saw that only as an integrity error)
33
+ r.use(MODEL_URL_PATH, (_req, res) => void res.status(404).end());
34
+ return r;
35
+ }
@@ -2,7 +2,7 @@ import { RISK_ORDER } from "../analyze/risk.js";
2
2
  import { planInstall } from "../install/plan.js";
3
3
  import { truncate } from "../util/sanitize.js";
4
4
  import { categorize } from "./category.js";
5
- import { trustTier } from "./rank.js";
5
+ import { freshInstalls, freshness, TRUST_SCORE, trustTier } from "./rank.js";
6
6
  export const TRUST_LABEL = { official: '공식', partner: '파트너', community: '커뮤니티' };
7
7
  export function treeUrl(repo, ref, dir) {
8
8
  const base = `https://github.com/${repo}`;
@@ -81,6 +81,14 @@ export function storeLookup(store) {
81
81
  },
82
82
  };
83
83
  }
84
+ /** An indexed skill as a card outside a search (favorites, installed): no relevance or popularity score. */
85
+ export function indexItem(store, lookup, skill) {
86
+ const plugin = lookup.plugin(skill.plugin_id);
87
+ const source = plugin ? marketplaceSource(skill, plugin) : githubSource(skill);
88
+ const breakdown = { R: 0, P: 0, F: freshness(skill.last_commit_at), T: TRUST_SCORE[trustTier(skill, plugin)] };
89
+ const stars = skill.repo ? (store.getRepo(skill.repo)?.stars ?? null) : null;
90
+ return buildResultItem({ skill, lookup, sources: source ? [source] : [], installs: freshInstalls(skill), stars, breakdown, score: 0 });
91
+ }
84
92
  export function buildResultItem(a) {
85
93
  const { skill } = a;
86
94
  const plugin = a.lookup.plugin(skill.plugin_id);
@@ -49,5 +49,8 @@ export function securityMiddleware(opts) {
49
49
  next();
50
50
  };
51
51
  }
52
- export const CSP = "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; " +
52
+ // 'wasm-unsafe-eval': the translation engine is WebAssembly (served from here); it allows compiling WASM, not eval.
53
+ // Its worker takes the policy of its own script's response, but the page has it too for browsers that give workers
54
+ // the page's policy.
55
+ export const CSP = "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; " +
53
56
  "connect-src 'self'; font-src 'self' data:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'";
@@ -2,6 +2,13 @@ import { createHash, randomBytes } from 'node:crypto';
2
2
  import { mkdir, rm } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { run } from "../util/proc.js";
5
+ /**
6
+ * git asked for a login: GitHub answers so for a repo that is deleted or private (credentials are turned off
7
+ * here), so it is gone, not a network hiccup. It used to count as "could not read" in searches and was asked
8
+ * again on every sync (2026-11-03).
9
+ */
10
+ export class RepoUnreachableError extends Error {
11
+ }
5
12
  const REPO_RE = /^[A-Za-z0-9-]{1,39}\/[A-Za-z0-9._-]{1,100}$/;
6
13
  const SHA_RE = /^[0-9a-f]{40}$/i;
7
14
  const REF_RE = /^[A-Za-z0-9._\/-]{1,200}$/;
@@ -75,8 +82,11 @@ export async function gitTree(repo, sha, workRoot) {
75
82
  if (init.exitCode !== 0)
76
83
  throw new Error(`git init failed: ${init.stderr.trim()}`);
77
84
  const fetch = await git(['fetch', '-q', '--depth', '1', '--filter=blob:none', '--no-tags', repoUrl(repo), sha], dir);
78
- if (fetch.exitCode !== 0)
85
+ if (fetch.exitCode !== 0) {
86
+ if (/could not read Username|Repository not found/i.test(fetch.stderr))
87
+ throw new RepoUnreachableError(`저장소를 찾을 수 없습니다: ${repo}`);
79
88
  throw new Error(`git fetch failed: ${fetch.stderr.trim().split('\n')[0] || fetch.exitCode}`);
89
+ }
80
90
  if (sha === 'HEAD') {
81
91
  const rev = await git(['rev-parse', 'FETCH_HEAD'], dir);
82
92
  sha = rev.stdout.trim();
@@ -27,6 +27,8 @@ function isSafeRepoPath(path) {
27
27
  }
28
28
  /** Raw files (SKILL.md included) are read up to this many bytes; the rest is cut off. */
29
29
  export const RAW_MAX_BYTES = 512 * 1024;
30
+ /** First wait after a raw download got 429, doubled on each retry. */
31
+ const RAW_THROTTLE_WAIT_MS = 1000;
30
32
  export class GitHub {
31
33
  store;
32
34
  config;
@@ -44,6 +46,8 @@ export class GitHub {
44
46
  refCache = new Map();
45
47
  /** raw.githubusercontent.com answers bursts of hundreds of parallel downloads with 429s; keep it civil. */
46
48
  rawSlots = new Semaphore(24);
49
+ /** raw.githubusercontent.com said 429: no download starts before this time. */
50
+ rawPausedUntil = 0;
47
51
  metaInFlight = new Map();
48
52
  constructor(store, config) {
49
53
  this.store = store;
@@ -244,8 +248,19 @@ export class GitHub {
244
248
  const url = `https://raw.githubusercontent.com/${repo}/${ref}/${path.split('/').map(encodeURIComponent).join('/')}`;
245
249
  for (let attempt = 0;; attempt++) {
246
250
  try {
247
- const res = await this.rawSlots.run(() => fetchText(url, { maxBytes, timeoutMs: 15_000 }));
248
- if ((res.status === 429 || res.status >= 500) && attempt < 2) {
251
+ const res = await this.rawSlots.run(async () => {
252
+ const pause = this.rawPausedUntil - Date.now();
253
+ if (pause > 0)
254
+ await sleep(pause);
255
+ return fetchText(url, { maxBytes, timeoutMs: 15_000 });
256
+ });
257
+ if (res.status === 429 && attempt < 3) {
258
+ // Throttled: every download waits (1, 2, 4 s), not only this one. Retrying after 0.4 s still failed
259
+ // while a repo with hundreds of skills was read (2026-11-03).
260
+ this.rawPausedUntil = Math.max(this.rawPausedUntil, Date.now() + RAW_THROTTLE_WAIT_MS * 2 ** attempt);
261
+ continue;
262
+ }
263
+ if (res.status >= 500 && attempt < 2) {
249
264
  await sleep(400 * (attempt + 1));
250
265
  continue;
251
266
  }
@@ -1,7 +1,7 @@
1
1
  import { RISK_RULES_VERSION, riskLevel } from "./analyze/risk.js";
2
2
  import { skillMdPath } from "./discovery/tree.js";
3
3
  import { PluginUnavailableError, recheckRisk } from "./indexer.js";
4
- import { GRAPHQL_BATCH } from "./sources/github.js";
4
+ import { GitHubError, GRAPHQL_BATCH } from "./sources/github.js";
5
5
  import { syncMarketplace } from "./sources/marketplace.js";
6
6
  import { collectPopularRepos, popularState } from "./sources/popularRepos.js";
7
7
  import { searchActivity } from "./search/activity.js";
@@ -41,18 +41,25 @@ export function startSync(ctx, marketplace, force = false) {
41
41
  .listMarketplaces()
42
42
  .filter((m) => (marketplace ? m.name === marketplace : m.enabled === 1));
43
43
  ctl.setTotal(targets.length);
44
+ const eager = [];
44
45
  for (const m of targets) {
45
46
  await ctl.checkpoint();
46
47
  try {
47
48
  await syncMarketplace(ctx.store, ctx.github, m.name, force);
48
49
  ctl.tick();
50
+ if (m.index_mode === 'eager')
51
+ eager.push(m.name);
49
52
  }
50
53
  catch (err) {
51
54
  ctl.tick(`${m.name}: ${String(err.message ?? err).slice(0, 200)}`);
52
- continue;
53
55
  }
54
- if (m.index_mode === 'eager')
55
- await indexPending(ctx, m.name, ctl);
56
+ }
57
+ // Every list first, then the skills of the marketplace with the fewest plugins to read: Anthropic's own skills
58
+ // (5 plugins) used to wait about 100 s for the 300-plugin official list on a first run (2026-11-03).
59
+ const pending = new Map(eager.map((name) => [name, ctx.store.listPluginsNeedingIndex(name).length]));
60
+ for (const name of eager.sort((a, b) => pending.get(a) - pending.get(b))) {
61
+ await indexPending(ctx, name, ctl);
62
+ ctx.store.clearSearchCache();
56
63
  }
57
64
  ctx.store.clearSearchCache();
58
65
  if (!marketplace && getSettings(ctx.store).autoCollect) {
@@ -68,6 +75,30 @@ const POPULAR_EVERY_MS = 24 * 60 * 60 * 1000;
68
75
  export const REPO_META_EVERY_MS = 6 * 60 * 60 * 1000;
69
76
  const REPO_META_STALE_MS = 18 * 60 * 60 * 1000;
70
77
  let repoMetaRunning = false;
78
+ /** The smallest group asked again in halves; a group this small that still fails waits for the next run. */
79
+ const REPO_META_MIN_SPLIT = 25;
80
+ /** GitHub gave up on the query (502/503/504, usually when it takes too long) or our own 30 s limit ran out. */
81
+ const gaveUp = (err) => (err instanceof GitHubError && [502, 503, 504].includes(err.status)) || err?.name === 'TimeoutError';
82
+ /**
83
+ * One group of repos, asked again in halves when GitHub gives up on it: a hundred lookups in one query can take
84
+ * too long, and one 502 used to end the whole run, leaving the rest stale until the next one 6 hours later
85
+ * (2026-11-03). Other errors (limit reached, network) still end the run.
86
+ */
87
+ async function refreshRepoGroup(ctx, repos) {
88
+ try {
89
+ return { stored: await ctx.github.refreshRepoMeta(repos), left: 0 };
90
+ }
91
+ catch (err) {
92
+ if (!gaveUp(err))
93
+ throw err;
94
+ if (repos.length <= REPO_META_MIN_SPLIT)
95
+ return { stored: 0, left: repos.length, error: err };
96
+ const half = Math.ceil(repos.length / 2);
97
+ const a = await refreshRepoGroup(ctx, repos.slice(0, half));
98
+ const b = await refreshRepoGroup(ctx, repos.slice(half));
99
+ return { stored: a.stored + b.stored, left: a.left + b.left, error: b.error ?? a.error };
100
+ }
101
+ }
71
102
  /**
72
103
  * Keeps star counts of the repos skills come from current (GraphQL, 100 per call), so a search reads them
73
104
  * from the database instead of asking GitHub per repo, and fills repos never fetched (indexed without a
@@ -80,11 +111,20 @@ export async function refreshRepoMeta(ctx) {
80
111
  let stored = 0;
81
112
  try {
82
113
  const repos = ctx.store.listReposToRefresh(new Date(Date.now() - REPO_META_STALE_MS).toISOString(), 10_000);
114
+ let left = 0;
83
115
  for (let i = 0; i < repos.length; i += GRAPHQL_BATCH) {
84
116
  while (searchActivity.busy())
85
117
  await sleep(500);
86
- stored += await ctx.github.refreshRepoMeta(repos.slice(i, i + GRAPHQL_BATCH));
118
+ const group = repos.slice(i, i + GRAPHQL_BATCH);
119
+ const r = await refreshRepoGroup(ctx, group);
120
+ stored += r.stored;
121
+ left += r.left;
122
+ // not one piece answered: GitHub is down rather than slow, so stop asking until the next run
123
+ if (r.left === group.length)
124
+ throw r.error;
87
125
  }
126
+ if (left)
127
+ console.warn('[repo-meta]', `GitHub가 일부 요청에 답하지 않아 저장소 ${left}개는 다음 갱신 때 다시 묻습니다`);
88
128
  }
89
129
  catch (err) {
90
130
  console.warn('[repo-meta]', String(err.message ?? err).slice(0, 200));
@@ -0,0 +1,140 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readFileSync, statSync } from 'node:fs';
3
+ import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import { zstdDecompress } from 'node:zlib';
7
+ export const MODEL_LANGS = ['ko', 'ja'];
8
+ const CDN = 'https://firefox-settings-attachments.cdn.mozilla.net/';
9
+ /**
10
+ * Pinned from Firefox's Remote Settings collection `translations-models-v2` (desktop records, version 3.0), the
11
+ * versions tried with the engine. A newer model may need a newer engine, so they are changed together.
12
+ */
13
+ export const MODEL_FILES = {
14
+ ko: [
15
+ { part: 'lex', name: 'lex.50.50.enko.s2t.bin', location: 'main-workspace/translations-models-v2/493336e3-8e7b-4223-86c5-e88846a85d46.zst', size: 2675843, sha256: '7216367b7f524c0ae1f39353e5c4622b230d1c2e47924182a5853418c1e69f64', unpackedSize: 6676356, unpackedSha256: '7734265822ce315c72334ad538e698ab3900ab8e8f04bd43768d0aecad6b6df7' },
16
+ { part: 'model', name: 'model.enko.intgemm.alphas.bin', location: 'main-workspace/translations-models-v2/e4e7e781-bd8e-4dae-9f8e-d0c8a8a5b07b.zst', size: 44942689, sha256: 'ebe3a360c2a007e31df8595bc01a856376c195718ed3c0a27b336eb9b349d313', unpackedSize: 59504955, unpackedSha256: '1c310a79b61b8824b2eb26b045db043d92722f3e66ea06998f7c89f48da9f6bc' },
17
+ { part: 'vocab', name: 'vocab.enko.spm', location: 'main-workspace/translations-models-v2/ac6a46cf-1cf0-4ea0-b1ad-9fa40d6f6513.zst', size: 595440, sha256: 'dc5b8d46f6dff08950bf36db741f52431a41466cf33f5a105d1c5154586559d2', unpackedSize: 1406926, unpackedSha256: '709bf0e425345e0e92ce41ed84348a1296a2107c92bdb6624404450ffecbd1f9' },
18
+ ],
19
+ ja: [
20
+ { part: 'lex', name: 'lex.50.50.enja.s2t.bin', location: 'main-workspace/translations-models-v2/83e22dc9-40c4-4ac1-8c25-5d3877b5f628.zst', size: 1751977, sha256: '8954d5b63432366992b22ad2aaa11895e2643014835049fe677655540a705915', unpackedSize: 4128360, unpackedSha256: 'edfb7eb47b98a2689b804ab3614c31b24f1257aa54b1154da3d85e1ae8152d9f' },
21
+ { part: 'trgvocab', name: 'trgvocab.enja.spm', location: 'main-workspace/translations-models-v2/e54d177c-1131-4fbf-90ea-6741a98d6675.zst', size: 361180, sha256: '5384abf34f0e1298f795bd14909a577272059041e60f91a5eb10500e35d0ee98', unpackedSize: 827144, unpackedSha256: '3b3d9f8f3a034d98d0a476f1794fa79c01e4e98a967ceb6777a66ba2d03ec1e1' },
22
+ { part: 'srcvocab', name: 'srcvocab.enja.spm', location: 'main-workspace/translations-models-v2/d10e4482-b9ba-4565-ab44-5ba3cebcf94b.zst', size: 345747, sha256: 'dd1e93d625d9a8d8a42f75255343f302ea37527933eda1a490652b66a7408a64', unpackedSize: 796275, unpackedSha256: '970c98d174fc01e0339fbabbf45af36a4be3f26f819ec1a5ea1189f71e091889' },
23
+ { part: 'model', name: 'model.enja.intgemm.alphas.bin', location: 'main-workspace/translations-models-v2/a01a5318-41c7-4266-8fe1-66c105d6d711.zst', size: 32431590, sha256: 'f92259b9904812350b71f440c3328f3e2e856f31c1a3ab6002756380dd908e00', unpackedSize: 43849787, unpackedSha256: '59ae659f9bb63e4f81f474fe3c03d3f4499434b5f9e779fab7c12a45f31fd562' },
24
+ ],
25
+ };
26
+ /** The address the page reads the files from (outside /api: the engine's fetch sends no X-Skill-Finder header). */
27
+ export const MODEL_URL_PATH = '/translate-models';
28
+ const FILE_TIMEOUT_MS = 5 * 60 * 1000;
29
+ const sha256 = (b) => createHash('sha256').update(b).digest('hex');
30
+ const pairDir = (lang) => `en${lang}`;
31
+ /** Written last, so a half-finished download is never taken for a model. Its content names the pinned files. */
32
+ const READY = 'ready.json';
33
+ export class TranslateModels {
34
+ dir;
35
+ fetchImpl;
36
+ files;
37
+ running = new Map();
38
+ /** `dir`: where the unpacked files live (`<dataDir>/translate-models`), served as {@link MODEL_URL_PATH}. `files`: tests. */
39
+ constructor(dir, fetchImpl = (...a) => fetch(...a), files = MODEL_FILES) {
40
+ this.dir = dir;
41
+ this.fetchImpl = fetchImpl;
42
+ this.files = files;
43
+ }
44
+ get root() {
45
+ return this.dir;
46
+ }
47
+ readyMark(lang) {
48
+ return JSON.stringify(this.files[lang].map((f) => f.unpackedSha256));
49
+ }
50
+ /** The mark, and every file still there at its size (one deleted by hand or quarantined means download again). */
51
+ ready(lang) {
52
+ try {
53
+ const dir = join(this.dir, pairDir(lang));
54
+ return readFileSync(join(dir, READY), 'utf8') === this.readyMark(lang) && this.files[lang].every((f) => statSync(join(dir, f.name)).size === f.unpackedSize);
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ }
60
+ status(lang) {
61
+ const run = this.running.get(lang);
62
+ if (run && run.state !== 'ready')
63
+ return { ...run };
64
+ const total = this.files[lang].reduce((n, f) => n + f.size, 0);
65
+ return this.ready(lang) ? { lang, state: 'ready', received: total, total, error: null } : { lang, state: 'absent', received: 0, total, error: null };
66
+ }
67
+ /** Starts the download unless it is there or under way; the returned status is polled with {@link status}. */
68
+ start(lang) {
69
+ const now = this.status(lang);
70
+ if (now.state === 'ready' || now.state === 'downloading')
71
+ return now;
72
+ const run = { lang, state: 'downloading', received: 0, total: now.total, error: null };
73
+ this.running.set(lang, run);
74
+ this.download(lang, run).then(() => (run.state = 'ready'), (err) => {
75
+ run.state = 'error';
76
+ run.error = String(err?.message ?? err).slice(0, 300);
77
+ console.warn('[translate]', run.error);
78
+ });
79
+ return { ...run };
80
+ }
81
+ async download(lang, run) {
82
+ const dir = join(this.dir, pairDir(lang));
83
+ await mkdir(dir, { recursive: true });
84
+ await rm(join(dir, READY), { force: true });
85
+ for (const f of this.files[lang]) {
86
+ const target = join(dir, f.name);
87
+ if (existsSync(target) && sha256(readFileSync(target)) === f.unpackedSha256) {
88
+ run.received += f.size;
89
+ continue;
90
+ }
91
+ // network failures and the time limit in the app's words (they used to show as English runtime messages)
92
+ const failed = (err) => new Error(`${f.name}: 내려받지 못했습니다 (${err?.name === 'TimeoutError' ? '시간 초과' : '연결 실패'})`);
93
+ const res = await this.fetchImpl(CDN + f.location, { signal: AbortSignal.timeout(FILE_TIMEOUT_MS) }).catch((err) => {
94
+ throw failed(err);
95
+ });
96
+ if (!res.ok || !res.body)
97
+ throw new Error(`${f.name}: 내려받지 못했습니다 (${res.status})`);
98
+ const chunks = [];
99
+ let size = 0;
100
+ try {
101
+ for await (const chunk of res.body) {
102
+ size += chunk.byteLength;
103
+ if (size > f.size)
104
+ throw new Error(`${f.name}: 크기가 맞지 않습니다`);
105
+ chunks.push(chunk);
106
+ run.received += chunk.byteLength;
107
+ }
108
+ }
109
+ catch (err) {
110
+ throw size > f.size ? err : failed(err);
111
+ }
112
+ const packed = Buffer.concat(chunks);
113
+ if (sha256(packed) !== f.sha256)
114
+ throw new Error(`${f.name}: SHA-256이 맞지 않습니다`);
115
+ const unpacked = await promisify(zstdDecompress)(packed);
116
+ if (unpacked.length !== f.unpackedSize || sha256(unpacked) !== f.unpackedSha256)
117
+ throw new Error(`${f.name}: 압축을 푼 파일이 맞지 않습니다`);
118
+ await writeFile(`${target}.part`, unpacked);
119
+ await rename(`${target}.part`, target);
120
+ }
121
+ await writeFile(join(dir, READY), this.readyMark(lang));
122
+ }
123
+ /**
124
+ * The engine's model registry for the languages that are ready: per pair, its files with their addresses and
125
+ * SHA-256 (the engine checks them again with subresource integrity). The address carries the file's hash, so a
126
+ * browser that kept an older model (cached for good) asks for the new one when the pins change.
127
+ */
128
+ registry() {
129
+ const out = {};
130
+ for (const lang of MODEL_LANGS) {
131
+ if (!this.ready(lang))
132
+ continue;
133
+ out[pairDir(lang)] = Object.fromEntries(this.files[lang].map((f) => [
134
+ f.part,
135
+ { name: `${MODEL_URL_PATH}/${pairDir(lang)}/${f.name}?v=${f.unpackedSha256.slice(0, 16)}`, size: f.unpackedSize, expectedSha256Hash: f.unpackedSha256 },
136
+ ]));
137
+ }
138
+ return out;
139
+ }
140
+ }
@@ -73,6 +73,8 @@ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
73
73
  IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
74
74
  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
75
75
 
76
+ ## @mkljczk/bergamot-translator - 0.4.16 (MPL-2.0)
77
+
76
78
  ## @phosphor-icons/react - 2.1.10 (MIT)
77
79
 
78
80
  MIT License
@@ -2265,3 +2267,7 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
2265
2267
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2266
2268
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2267
2269
  SOFTWARE.
2270
+
2271
+ ## @mkljczk/bergamot-translator - 0.4.16 (MPL-2.0)
2272
+
2273
+ The translation engine in bergamot/ is shipped unmodified. Its source (version 0.4.16): https://codeberg.org/mkljczk/bergamot-translator (MPL-2.0, https://mozilla.org/MPL/2.0/). Its WebAssembly is built from bergamot-translator with the libraries it includes, such as Marian NMT (MIT), SentencePiece (Apache-2.0) and intgemm (MIT); their notices are in that source. The translation models are Mozilla's (MPL-2.0), downloaded from Mozilla when turned on.