@zaimokuza/dsh-plugin-hub 0.1.0 → 0.1.1

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/lib/client.js CHANGED
@@ -115,7 +115,7 @@ var pairs = [
115
115
  ["\u4E0D\u652F\u6301\u7684\u8BF7\u6C42\u65B9\u6CD5", "Unsupported request method"],
116
116
  ["\u63A5\u53E3\u4E0D\u5B58\u5728", "Endpoint not found"],
117
117
  ["\u8BF7\u6C42\u8FC7\u5927", "Request body is too large"],
118
- ["\u76EE\u5F55 npm \u5305\u6CA1\u6709 integrity\uFF0C\u65E0\u6CD5\u6821\u9A8C", "Catalog package has no integrity field"],
118
+ ["\u76EE\u5F55 npm \u5305\u7F3A\u5C11\u6709\u6548\u7684 integrity \u6216 shasum\uFF0C\u65E0\u6CD5\u6821\u9A8C", "Catalog package has no valid integrity or shasum for verification"],
119
119
  ["\u76EE\u5F55 npm \u5305\u5B8C\u6574\u6027\u6821\u9A8C\u5931\u8D25", "Catalog package integrity check failed"],
120
120
  ["\u76EE\u5F55\u6765\u6E90\u5FC5\u987B\u662F npm \u6570\u636E\u5305", "Catalog source must be an npm data package"],
121
121
  ["\u76EE\u5F55 npm \u5305\u6CA1\u6709\u53EF\u4E0B\u8F7D\u7684 latest \u7248\u672C", "Catalog package has no downloadable latest release"],
@@ -484,7 +484,7 @@ function Market({ locale, t }) {
484
484
  setQuery("");
485
485
  setTag("all");
486
486
  setTab("all");
487
- } }, t("\u67E5\u770B\u5168\u90E8\u63D2\u4EF6"))), /* @__PURE__ */ import_react.default.createElement("footer", { className: "hub-footer" }, /* @__PURE__ */ import_react.default.createElement("span", null, /* @__PURE__ */ import_react.default.createElement("i", null), t("\u76EE\u5F55 {version} \xB7 \u66F4\u65B0\u4E8E {date}", { version: data.catalog.version ?? t("\u672A\u52A0\u8F7D"), date: date(data.catalog.updatedAt) })), /* @__PURE__ */ import_react.default.createElement("span", null, t("\u5E02\u573A v{version}", { version: data.marketVersion }))), /* @__PURE__ */ import_react.default.createElement("p", { className: "hub-footnote" }, t("\u201C\u7B26\u5408\u5B89\u88C5\u6761\u4EF6\u201D\u4F9D\u636E\u7248\u672C\u58F0\u660E\u4E0E\u53D1\u5E03\u65F6\u95F4\uFF0C\u5B9E\u9645\u4E0B\u8F7D\u4ECD\u4EE5 Nexus \u4E3A\u51C6\u3002")), !!data.jobs.length && /* @__PURE__ */ import_react.default.createElement("aside", { className: "hub-queue" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-queue-summary" }, /* @__PURE__ */ import_react.default.createElement("span", { className: "hub-queue-icon" }, /* @__PURE__ */ import_react.default.createElement(Icon, { name: pending ? "download" : "check" })), /* @__PURE__ */ import_react.default.createElement("span", null, /* @__PURE__ */ import_react.default.createElement("b", null, pending ? t("{count} \u4E2A\u63D2\u4EF6\u64CD\u4F5C\u6B63\u5728\u6267\u884C", { count: pending }) : t("\u63D2\u4EF6\u64CD\u4F5C\u5DF2\u5B8C\u6210")), /* @__PURE__ */ import_react.default.createElement("small", null, t("{count} \u9879\u5F85\u624B\u52A8\u91CD\u542F\u751F\u6548", { count: data.pendingRestart }))), /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Button, { size: "sm", onClick: () => setShowJobs(!showJobs), "aria-expanded": showJobs }, t(showJobs ? "\u6536\u8D77" : "\u67E5\u770B\u4EFB\u52A1"))), showJobs && /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-jobs" }, data.jobs.slice().reverse().map((job) => /* @__PURE__ */ import_react.default.createElement("div", { key: job.id }, /* @__PURE__ */ import_react.default.createElement("span", null, /* @__PURE__ */ import_react.default.createElement("b", null, all.find((p) => p.packageName === job.packageName)?.displayName ?? job.packageName), /* @__PURE__ */ import_react.default.createElement("small", null, t(job.action === "uninstall" ? "\u5378\u8F7D" : "\u5B89\u88C5"), " \xB7 ", job.version)), /* @__PURE__ */ import_react.default.createElement(Badge, { tone: job.status === "completed" ? "green" : job.status === "failed" ? "red" : "amber" }, t({ queued: "\u6392\u961F\u4E2D", installing: job.action === "uninstall" ? "\u5378\u8F7D\u4E2D" : "\u5B89\u88C5\u4E2D", completed: "\u5F85\u91CD\u542F", failed: job.action === "uninstall" ? "\u5378\u8F7D\u5931\u8D25" : "\u5B89\u88C5\u5931\u8D25" }[job.status])), job.error && /* @__PURE__ */ import_react.default.createElement("p", null, explain(job.error)))))), /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Modal, { open: Boolean(detail), onClose: () => setDetailName(null), title: detail?.displayName ?? "", closeLabel: t("\u5173\u95ED\u8BE6\u60C5"), className: "hub-detail-dialog", contentClassName: "hub-detail-scroll", description: detail?.description, footer: detail && /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-detail-actions" }, /* @__PURE__ */ import_react.default.createElement("div", null, installButton(detail, releaseOf(detail)), uninstallButton(detail)), documentLinks(detail)) }, detail && /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-detail-body" }, /* @__PURE__ */ import_react.default.createElement("code", { className: "hub-package-name" }, detail.packageName), /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-detail-info" }, /* @__PURE__ */ import_react.default.createElement("span", null, t("\u7EF4\u62A4\u56E2\u961F"), /* @__PURE__ */ import_react.default.createElement("b", null, detail.owner)), /* @__PURE__ */ import_react.default.createElement("span", null, t("\u5F53\u524D\u5BBF\u4E3B"), /* @__PURE__ */ import_react.default.createElement("b", null, "DSH ", data.host.dsh))), /* @__PURE__ */ import_react.default.createElement("h3", null, t("\u53D1\u884C\u7248\u672C")), /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-releases" }, detail.versions.map((release) => {
487
+ } }, t("\u67E5\u770B\u5168\u90E8\u63D2\u4EF6"))), /* @__PURE__ */ import_react.default.createElement("footer", { className: "hub-footer" }, /* @__PURE__ */ import_react.default.createElement("span", null, /* @__PURE__ */ import_react.default.createElement("i", null), t("\u76EE\u5F55 {version} \xB7 \u66F4\u65B0\u4E8E {date}", { version: data.catalog.version ?? t("\u672A\u52A0\u8F7D"), date: date(data.catalog.updatedAt) })), /* @__PURE__ */ import_react.default.createElement("span", null, t("\u5E02\u573A v{version}", { version: data.marketVersion }))), /* @__PURE__ */ import_react.default.createElement("p", { className: "hub-footnote" }, t("\u201C\u7B26\u5408\u5B89\u88C5\u6761\u4EF6\u201D\u4F9D\u636E\u7248\u672C\u58F0\u660E\u4E0E\u53D1\u5E03\u65F6\u95F4\uFF0C\u5B9E\u9645\u4E0B\u8F7D\u4ECD\u4EE5 Nexus \u4E3A\u51C6\u3002")), !!data.jobs.length && /* @__PURE__ */ import_react.default.createElement("aside", { className: "hub-queue" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-queue-summary" }, /* @__PURE__ */ import_react.default.createElement("span", { className: "hub-queue-icon" }, /* @__PURE__ */ import_react.default.createElement(Icon, { name: pending ? "download" : "check" })), /* @__PURE__ */ import_react.default.createElement("span", null, /* @__PURE__ */ import_react.default.createElement("b", null, pending ? t("{count} \u4E2A\u63D2\u4EF6\u64CD\u4F5C\u6B63\u5728\u6267\u884C", { count: pending }) : t("\u63D2\u4EF6\u64CD\u4F5C\u5DF2\u5B8C\u6210")), /* @__PURE__ */ import_react.default.createElement("small", null, t("{count} \u9879\u5F85\u624B\u52A8\u91CD\u542F\u751F\u6548", { count: data.pendingRestart }))), /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Button, { size: "sm", onClick: () => setShowJobs(!showJobs), "aria-expanded": showJobs }, t(showJobs ? "\u6536\u8D77" : "\u67E5\u770B\u4EFB\u52A1"))), showJobs && /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-jobs" }, data.jobs.slice().reverse().map((job) => /* @__PURE__ */ import_react.default.createElement("div", { key: job.id }, /* @__PURE__ */ import_react.default.createElement("span", null, /* @__PURE__ */ import_react.default.createElement("b", null, all.find((p) => p.packageName === job.packageName)?.displayName ?? job.packageName), /* @__PURE__ */ import_react.default.createElement("small", null, t(job.action === "uninstall" ? "\u5378\u8F7D" : "\u5B89\u88C5"), " \xB7 ", job.version)), /* @__PURE__ */ import_react.default.createElement(Badge, { tone: job.status === "completed" ? "green" : job.status === "failed" ? "red" : "amber" }, t({ queued: "\u6392\u961F\u4E2D", installing: job.action === "uninstall" ? "\u5378\u8F7D\u4E2D" : "\u5B89\u88C5\u4E2D", completed: "\u5F85\u91CD\u542F", failed: job.action === "uninstall" ? "\u5378\u8F7D\u5931\u8D25" : "\u5B89\u88C5\u5931\u8D25" }[job.status])), job.error && /* @__PURE__ */ import_react.default.createElement("p", null, explain(job.error)))))), /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Modal, { open: Boolean(detail), onClose: () => setDetailName(null), title: detail?.displayName ?? "", closeLabel: t("\u5173\u95ED\u8BE6\u60C5"), className: "hub-detail-dialog", contentClassName: "hub-detail-scroll", description: detail?.description, footer: detail && /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-detail-actions" }, /* @__PURE__ */ import_react.default.createElement("div", null, installButton(detail, releaseOf(detail)), uninstallButton(detail)), documentLinks(detail)) }, detail && /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-detail-body" }, /* @__PURE__ */ import_react.default.createElement("code", { className: "hub-package-name" }, detail.packageName), /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-detail-info" }, detail.owner && /* @__PURE__ */ import_react.default.createElement("span", null, t("\u7EF4\u62A4\u56E2\u961F"), /* @__PURE__ */ import_react.default.createElement("b", null, detail.owner)), /* @__PURE__ */ import_react.default.createElement("span", null, t("\u5F53\u524D\u5BBF\u4E3B"), /* @__PURE__ */ import_react.default.createElement("b", null, "DSH ", data.host.dsh))), /* @__PURE__ */ import_react.default.createElement("h3", null, t("\u53D1\u884C\u7248\u672C")), /* @__PURE__ */ import_react.default.createElement("div", { className: "hub-releases" }, detail.versions.map((release) => {
488
488
  const state = status(detail, release, t, data.minimumAgeHours);
489
489
  return /* @__PURE__ */ import_react.default.createElement("div", { key: release.version, className: "hub-release" }, /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement(import_dsh_client_ui_primitives.Button, { variant: releaseOf(detail)?.version === release.version ? "toolbar" : "outline", size: "sm", "aria-pressed": releaseOf(detail)?.version === release.version, onClick: () => setSelected((previous) => ({ ...previous, [detail.packageName]: release.version })) }, release.version), /* @__PURE__ */ import_react.default.createElement(Badge, { tone: state.tone }, state.label)), /* @__PURE__ */ import_react.default.createElement("small", null, t("\u53D1\u5E03\u65F6\u95F4 {date} \xB7 DSH {range}", { date: date(release.publishedAt), range: release.dshRange ?? t(release.compatibilityBasis === "dshPluginHub.hostCompatibility" ? "\u4E0D\u6309\u7248\u672C\u9650\u5236\uFF0C\u8FD0\u884C\u65F6\u68C0\u67E5\u63A5\u53E3" : release.compatibilityBasis === "peerDependencies" ? "\u5BBF\u4E3B\u4F9D\u8D56\u58F0\u660E" : release.compatibilityBasis?.includes("dshReleases") ? "\u4F5C\u8005\u9010\u7248\u672C\u58F0\u660E" : "\u672A\u58F0\u660E") })), release.reasons.length > 0 && /* @__PURE__ */ import_react.default.createElement("p", null, release.reasons.map(explain).join(" \xB7 ")), release.age === "waiting" && /* @__PURE__ */ import_react.default.createElement("small", null, Number.isInteger(data.minimumAgeHours) ? t("\u9884\u8BA1\u6EE1 {hours} \u5C0F\u65F6\uFF1A{date}", { hours: data.minimumAgeHours, date: date(release.eligibleAt) }) : t("\u9884\u8BA1\u6EE1 {minutes} \u5206\u949F\uFF1A{date}", { minutes: data.minimumAgeMinutes, date: date(release.eligibleAt) })));
490
490
  }), detail.queryError && /* @__PURE__ */ import_react.default.createElement("p", null, explain(detail.queryError))))));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaimokuza/dsh-plugin-hub",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "private": false,
5
5
  "description": "Extensible DSH plugin marketplace and reusable enterprise marketplace runtime",
6
6
  "type": "module",
package/src/catalog.js CHANGED
@@ -16,10 +16,11 @@ export function validateCatalog(value) {
16
16
  if (typeof item.packageName !== 'string' || !PACKAGE_NAME.test(item.packageName) || item.packageName.length > 214) fail('npm 包名无效');
17
17
  if (seen.has(item.packageName)) fail('npm 包名重复');
18
18
  seen.add(item.packageName);
19
- for (const [key, limit] of [['displayName', 80], ['description', 300], ['owner', 80]]) {
19
+ for (const [key, limit] of [['displayName', 80], ['description', 300]]) {
20
20
  if (typeof item[key] !== 'string' || !item[key].trim() || item[key].length > limit) fail(`${key} 无效`);
21
21
  }
22
- if (!['internal', 'community'].includes(item.origin)) fail('origin 无效');
22
+ if (item.owner !== undefined && (typeof item.owner !== 'string' || !item.owner.trim() || item.owner.length > 80)) fail('owner 无效');
23
+ if (item.origin !== undefined && !['internal', 'community'].includes(item.origin)) fail('origin 无效');
23
24
  if (!Array.isArray(item.tags) || item.tags.length < 1 || item.tags.length > 5 || new Set(item.tags).size !== item.tags.length || item.tags.some(tag => typeof tag !== 'string' || !/^[a-z][a-z0-9-]{0,39}$/.test(tag))) fail('tags 无效');
24
25
  for (const key of ['documentationUrl', 'troubleshootingUrl', 'repositoryUrl']) {
25
26
  if (item[key] === undefined) continue;
@@ -187,7 +187,7 @@ export function Market({ locale, t }) {
187
187
  {!!data.jobs.length && <aside className="hub-queue"><div className="hub-queue-summary"><span className="hub-queue-icon"><Icon name={pending ? 'download' : 'check'}/></span><span><b>{pending ? t('{count} 个插件操作正在执行', { count: pending }) : t('插件操作已完成')}</b><small>{t('{count} 项待手动重启生效', { count: data.pendingRestart })}</small></span><Button size="sm" onClick={() => setShowJobs(!showJobs)} aria-expanded={showJobs}>{t(showJobs ? '收起' : '查看任务')}</Button></div>{showJobs && <div className="hub-jobs">{data.jobs.slice().reverse().map(job => <div key={job.id}><span><b>{all.find(p => p.packageName === job.packageName)?.displayName ?? job.packageName}</b><small>{t(job.action === 'uninstall' ? '卸载' : '安装')} · {job.version}</small></span><Badge tone={job.status === 'completed' ? 'green' : job.status === 'failed' ? 'red' : 'amber'}>{t({ queued: '排队中', installing: job.action === 'uninstall' ? '卸载中' : '安装中', completed: '待重启', failed: job.action === 'uninstall' ? '卸载失败' : '安装失败' }[job.status])}</Badge>{job.error && <p>{explain(job.error)}</p>}</div>)}</div>}</aside>}
188
188
 
189
189
  <Modal open={Boolean(detail)} onClose={() => setDetailName(null)} title={detail?.displayName ?? ''} closeLabel={t('关闭详情')} className="hub-detail-dialog" contentClassName="hub-detail-scroll" description={detail?.description} footer={detail && <div className="hub-detail-actions"><div>{installButton(detail, releaseOf(detail))}{uninstallButton(detail)}</div>{documentLinks(detail)}</div>}>
190
- {detail && <div className="hub-detail-body"><code className="hub-package-name">{detail.packageName}</code><div className="hub-detail-info"><span>{t('维护团队')}<b>{detail.owner}</b></span><span>{t('当前宿主')}<b>DSH {data.host.dsh}</b></span></div><h3>{t('发行版本')}</h3><div className="hub-releases">{detail.versions.map(release => { const state = status(detail, release, t, data.minimumAgeHours); return <div key={release.version} className="hub-release"><div><Button variant={releaseOf(detail)?.version === release.version ? 'toolbar' : 'outline'} size="sm" aria-pressed={releaseOf(detail)?.version === release.version} onClick={() => setSelected(previous => ({ ...previous, [detail.packageName]: release.version }))}>{release.version}</Button><Badge tone={state.tone}>{state.label}</Badge></div><small>{t('发布时间 {date} · DSH {range}', { date: date(release.publishedAt), range: release.dshRange ?? t(release.compatibilityBasis === 'dshPluginHub.hostCompatibility' ? '不按版本限制,运行时检查接口' : release.compatibilityBasis === 'peerDependencies' ? '宿主依赖声明' : release.compatibilityBasis?.includes('dshReleases') ? '作者逐版本声明' : '未声明') })}</small>{release.reasons.length > 0 && <p>{release.reasons.map(explain).join(' · ')}</p>}{release.age === 'waiting' && <small>{Number.isInteger(data.minimumAgeHours) ? t('预计满 {hours} 小时:{date}', { hours: data.minimumAgeHours, date: date(release.eligibleAt) }) : t('预计满 {minutes} 分钟:{date}', { minutes: data.minimumAgeMinutes, date: date(release.eligibleAt) })}</small>}</div>; })}{detail.queryError && <p>{explain(detail.queryError)}</p>}</div></div>}
190
+ {detail && <div className="hub-detail-body"><code className="hub-package-name">{detail.packageName}</code><div className="hub-detail-info">{detail.owner && <span>{t('维护团队')}<b>{detail.owner}</b></span>}<span>{t('当前宿主')}<b>DSH {data.host.dsh}</b></span></div><h3>{t('发行版本')}</h3><div className="hub-releases">{detail.versions.map(release => { const state = status(detail, release, t, data.minimumAgeHours); return <div key={release.version} className="hub-release"><div><Button variant={releaseOf(detail)?.version === release.version ? 'toolbar' : 'outline'} size="sm" aria-pressed={releaseOf(detail)?.version === release.version} onClick={() => setSelected(previous => ({ ...previous, [detail.packageName]: release.version }))}>{release.version}</Button><Badge tone={state.tone}>{state.label}</Badge></div><small>{t('发布时间 {date} · DSH {range}', { date: date(release.publishedAt), range: release.dshRange ?? t(release.compatibilityBasis === 'dshPluginHub.hostCompatibility' ? '不按版本限制,运行时检查接口' : release.compatibilityBasis === 'peerDependencies' ? '宿主依赖声明' : release.compatibilityBasis?.includes('dshReleases') ? '作者逐版本声明' : '未声明') })}</small>{release.reasons.length > 0 && <p>{release.reasons.map(explain).join(' · ')}</p>}{release.age === 'waiting' && <small>{Number.isInteger(data.minimumAgeHours) ? t('预计满 {hours} 小时:{date}', { hours: data.minimumAgeHours, date: date(release.eligibleAt) }) : t('预计满 {minutes} 分钟:{date}', { minutes: data.minimumAgeMinutes, date: date(release.eligibleAt) })}</small>}</div>; })}{detail.queryError && <p>{explain(detail.queryError)}</p>}</div></div>}
191
191
  </Modal>
192
192
  </section>;
193
193
  }
@@ -24,7 +24,7 @@ const pairs = [
24
24
  ['声明不支持当前 DSH profile','This DSH profile is not covered by the declaration'],
25
25
  ['作者逐版本声明','Author release declaration'],
26
26
  ['不支持的请求方法','Unsupported request method'], ['接口不存在','Endpoint not found'], ['请求过大','Request body is too large'],
27
- ['目录 npm 包没有 integrity,无法校验','Catalog package has no integrity field'],
27
+ ['目录 npm 包缺少有效的 integrity 或 shasum,无法校验','Catalog package has no valid integrity or shasum for verification'],
28
28
  ['目录 npm 包完整性校验失败','Catalog package integrity check failed'],
29
29
  ['目录来源必须是 npm 数据包','Catalog source must be an npm data package'],
30
30
  ['目录 npm 包没有可下载的 latest 版本','Catalog package has no downloadable latest release'],
package/src/identity.js CHANGED
@@ -1,5 +1,9 @@
1
1
  export const MARKET_PACKAGE = '@zaimokuza/dsh-plugin-hub';
2
2
  export const CATALOG_PACKAGE = '@zaimokuza/dsh-plugin-hub-catalog-demo';
3
+ export function catalogVerification(value = 'if-present') {
4
+ if (!['if-present', 'required', 'none'].includes(value)) throw new Error('catalogVerification must be if-present, required or none');
5
+ return value;
6
+ }
3
7
  export const DEFAULT_BRAND = Object.freeze({ title: 'Plugin Hub', subTitle: 'DSH EXTENSIONS', primaryColor: '#596579', navTitle: 'Plugin Hub' });
4
8
  export function marketIdentity(id = 'hub') {
5
9
  if (typeof id !== 'string' || !/^[a-z][a-z0-9-]{0,47}$/.test(id)) throw new Error('marketId must be a lowercase identifier (max 48 characters)');
package/src/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { MARKET_PACKAGE, CATALOG_PACKAGE, marketIdentity, normalizeBrand } from './identity.js';
1
+ import { MARKET_PACKAGE, CATALOG_PACKAGE, marketIdentity, normalizeBrand, catalogVerification } from './identity.js';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { createRequire } from 'node:module';
4
4
  import { join } from 'node:path';
@@ -37,7 +37,7 @@ export async function apply(ctx, options = {}) {
37
37
  const releaseAge = await readReleaseAge(environment.profileDir);
38
38
  const sources = options.catalogSources ?? [{ id: 'company', displayName: source.packageName === CATALOG_PACKAGE ? 'Demo catalog' : brand.title, ...source, priority: 100 }];
39
39
  if (!Array.isArray(sources) || sources.length > 20 || sources.some(s => s?.kind !== 'npm')) throw new Error('catalogSources must contain at most 20 npm sources; JSON providers register through the plugin API');
40
- const config = { source, sources, brand, identity, packageName: options.packageName ?? MARKET_PACKAGE, registry: registry.href.replace(/\/?$/, '/'), cacheDir: join(environment.profileDir, '.dsh-plugin-hub', identity.id), ...releaseAge };
40
+ const config = { source, sources, brand, identity, catalogVerification: catalogVerification(options.catalogVerification), packageName: options.packageName ?? MARKET_PACKAGE, registry: registry.href.replace(/\/?$/, '/'), cacheDir: join(environment.profileDir, '.dsh-plugin-hub', identity.id), ...releaseAge };
41
41
  const installer = createDshInstaller(environment, config);
42
42
  const market = new Marketplace({ config, host: environment.host, fetcher: createRegistryFetch(config.registry), installer });
43
43
  // The public package ships an independent catalog snapshot for first-open discovery.
@@ -3,8 +3,8 @@ export interface CatalogPlugin {
3
3
  packageName: string;
4
4
  displayName: string;
5
5
  description: string;
6
- owner: string;
7
- origin: 'internal' | 'community';
6
+ owner?: string;
7
+ origin?: 'internal' | 'community';
8
8
  tags: string[];
9
9
  documentationUrl?: string;
10
10
  troubleshootingUrl?: string;
package/src/providers.js CHANGED
@@ -3,11 +3,13 @@ import { readFile, writeFile, rename, mkdir } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { PACKAGE_NAME, validateCatalog } from './catalog.js';
5
5
  import { loadCatalogSource } from './source.js';
6
+ import { catalogVerification as validateVerification } from './identity.js';
6
7
 
7
8
  /** Host-only v1 API. Providers contribute data; registry and installation policy stay market-owned. */
8
9
  export class CatalogProviders {
9
- constructor({ cacheDir, registry, fetcher, onChange = () => {}, timeoutMs = 15000 }) {
10
+ constructor({ cacheDir, registry, fetcher, onChange = () => {}, timeoutMs = 15000, catalogVerification = 'if-present' }) {
10
11
  Object.assign(this, { cacheDir, registry, fetcher, onChange, timeoutMs });
12
+ this.catalogVerification = validateVerification(catalogVerification);
11
13
  this.entries = new Map();
12
14
  }
13
15
  registerSource(input, { primary = false } = {}) {
@@ -21,7 +23,7 @@ export class CatalogProviders {
21
23
  if (input.displayName !== undefined && (typeof input.displayName !== 'string' || input.displayName.length > 80)) throw new Error('Invalid source displayName');
22
24
  if (input.cacheVersion !== undefined && (typeof input.cacheVersion !== 'string' || input.cacheVersion.length > 80)) throw new Error('Invalid source cacheVersion');
23
25
  const source = Object.freeze({ ...input, displayName: input.displayName || input.id, priority: input.priority ?? 0 });
24
- const key = JSON.stringify([this.registry, source.id, source.kind, source.packageName, source.cacheVersion ?? '1']);
26
+ const key = JSON.stringify([this.registry, source.id, source.kind, source.packageName, source.cacheVersion ?? '1', source.kind === 'npm' ? this.catalogVerification : null]);
25
27
  const row = { source, primary, key, value: null, error: null, loaded: false, controller: null };
26
28
  row.file = join(this.cacheDir, 'sources', createHash('sha256').update(key).digest('hex') + '.json');
27
29
  this.entries.set(source.id, row); this.onChange();
@@ -48,7 +50,7 @@ export class CatalogProviders {
48
50
  let timer;
49
51
  try {
50
52
  const load = async () => {
51
- if (row.source.kind === 'npm') return loadCatalogSource(row.source, this.registry, this.fetcher);
53
+ if (row.source.kind === 'npm') return loadCatalogSource(row.source, this.registry, this.fetcher, this.catalogVerification);
52
54
  const data = await row.source.getCatalog({ signal: controller.signal });
53
55
  if (Buffer.byteLength(JSON.stringify(data)) > 32 * 1024 * 1024) throw new Error('响应超过大小限制');
54
56
  return { catalog: validateCatalog(data), source: row.source.id, version: row.source.cacheVersion ?? '1' };
@@ -76,7 +78,7 @@ export class CatalogProviders {
76
78
  }
77
79
  return {
78
80
  plugins: [...plugins.values()], conflicts,
79
- sources: rows.map(({ source, primary, value, error }) => ({ primary, id: source.id, displayName: source.displayName, kind: source.kind, packageName: source.packageName, priority: source.priority, version: value?.version ?? null, updatedAt: value?.updatedAt ?? null, count: value?.catalog.plugins.length ?? 0, error, stale: Boolean(error && value) })),
81
+ sources: rows.map(({ source, primary, value, error }) => ({ primary, id: source.id, displayName: source.displayName, kind: source.kind, packageName: source.packageName, priority: source.priority, version: value?.version ?? null, verification: value?.verification ?? null, updatedAt: value?.updatedAt ?? null, count: value?.catalog.plugins.length ?? 0, error, stale: Boolean(error && value) })),
80
82
  };
81
83
  }
82
84
  close() { for (const row of this.entries.values()) row.controller?.abort(); this.entries.clear(); }
package/src/service.js CHANGED
@@ -6,6 +6,7 @@ import { randomUUID } from 'node:crypto';
6
6
  import { releaseList } from './catalog.js';
7
7
  import { fetchJson } from './source.js';
8
8
  import { CatalogProviders } from './providers.js';
9
+ const { version: marketVersion } = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
9
10
 
10
11
  async function readSaved(file) {
11
12
  try { return JSON.parse(await readFile(file, 'utf8')); }
@@ -113,7 +114,7 @@ export class Marketplace {
113
114
  metadataProgress: this.metadataProgress, lazyMetadata: this.lazyMetadata,
114
115
  minimumAgeMinutes: this.config.minimumAgeMinutes ?? Math.round(this.config.minimumAgeHours * 60), releaseAgeSource: this.config.releaseAgeSource ?? 'default',
115
116
  brand: this.config.brand, marketId: this.config.identity?.id, sources: sourceState.sources, sourceConflicts: sourceState.conflicts,
116
- marketVersion: '0.1.0', host: this.host, demo: Boolean(this.demo), minimumAgeHours: this.config.minimumAgeHours,
117
+ marketVersion, host: this.host, demo: Boolean(this.demo), minimumAgeHours: this.config.minimumAgeHours,
117
118
  catalog: { version: this.catalog?.version ?? null, updatedAt: this.catalog?.updatedAt ?? null, source: this.catalog?.source ?? this.config.source?.packageName ?? '', error: this.catalogError, stale: sourceState.sources.some(s => s.stale), count: this.plugins.length },
118
119
  plugins: this.plugins.map(plugin => ({ ...plugin, installedVersion: installed[plugin.packageName] ?? null })),
119
120
  jobs: this.jobs, pendingRestart: this.jobs.filter(job => job.status === 'completed').length,
package/src/source.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createHash, timingSafeEqual } from 'node:crypto';
2
2
  import { Parser } from 'tar';
3
3
  import { validateCatalog } from './catalog.js';
4
+ import { catalogVerification } from './identity.js';
4
5
 
5
6
  const LIMIT = 5 * 1024 * 1024;
6
7
  const CATALOG_LIMIT = 32 * 1024 * 1024;
@@ -23,14 +24,28 @@ export async function fetchJson(fetcher, url) {
23
24
  catch { throw new Error('返回内容不是有效 JSON,已保留上一份可用数据'); }
24
25
  }
25
26
 
26
- export function checkIntegrity(bytes, integrity) {
27
- if (typeof integrity !== 'string') throw new Error('目录 npm 包没有 integrity,无法校验');
28
- const options = integrity.split(/\s+/).map(token => token.match(/^(sha512|sha384|sha256)-([A-Za-z0-9+/=]+)$/)).filter(Boolean);
29
- if (!options.some(([, algorithm, digest]) => {
27
+ export function checkIntegrity(bytes, integrity, shasum, policy = 'if-present') {
28
+ catalogVerification(policy);
29
+ if (policy === 'none') return 'disabled';
30
+ const missing = value => value == null || (typeof value === 'string' && !value.trim());
31
+ // Older npm-compatible registries may expose only the tarball's SHA-1 sum.
32
+ if (missing(integrity)) {
33
+ if (missing(shasum) && policy === 'if-present') return 'unavailable';
34
+ if (typeof shasum !== 'string' || !/^[a-f\d]{40}$/i.test(shasum)) throw new Error('目录 npm 包缺少有效的 integrity 或 shasum,无法校验');
35
+ const actual = createHash('sha1').update(bytes).digest();
36
+ if (!timingSafeEqual(actual, Buffer.from(shasum, 'hex'))) throw new Error('目录 npm 包完整性校验失败');
37
+ return 'shasum';
38
+ }
39
+ if (typeof integrity !== 'string') throw new Error('目录 npm 包完整性校验失败');
40
+ const options = integrity.trim().split(/\s+/).map(token => token.match(/^(sha512|sha384|sha256|sha1)-([A-Za-z0-9+/=]+)$/)).filter(Boolean);
41
+ const strongest = ['sha512', 'sha384', 'sha256', 'sha1'].find(algorithm => options.some(option => option[1] === algorithm));
42
+ // A present integrity field must verify; never fall back after a mismatch.
43
+ if (!options.filter(option => option[1] === strongest).some(([, algorithm, digest]) => {
30
44
  const expected = Buffer.from(digest, 'base64');
31
45
  const actual = createHash(algorithm).update(bytes).digest();
32
46
  return actual.length === expected.length && timingSafeEqual(actual, expected);
33
47
  })) throw new Error('目录 npm 包完整性校验失败');
48
+ return 'integrity';
34
49
  }
35
50
 
36
51
  /** Read only the data entry from the archive; never extract paths or execute scripts. */
@@ -53,7 +68,8 @@ export function readCatalogTarball(bytes) {
53
68
  });
54
69
  }
55
70
 
56
- export async function loadCatalogSource(source, registry, fetcher) {
71
+ export async function loadCatalogSource(source, registry, fetcher, policy = 'if-present') {
72
+ catalogVerification(policy);
57
73
  if (source.kind !== 'npm') throw new Error('目录来源必须是 npm 数据包');
58
74
  const { value: metadata } = await fetchJson(fetcher, registry + encodeURIComponent(source.packageName));
59
75
  const version = metadata['dist-tags']?.latest;
@@ -64,6 +80,6 @@ export async function loadCatalogSource(source, registry, fetcher) {
64
80
  if (url.origin !== new URL(registry).origin || url.username || url.password) throw new Error('目录 tarball 地址不属于配置的仓库');
65
81
  const response = await fetcher(url, { signal: AbortSignal.timeout(15000), redirect: 'error' });
66
82
  const bytes = await boundedBytes(response, CATALOG_LIMIT);
67
- checkIntegrity(bytes, dist.integrity);
68
- return { catalog: await readCatalogTarball(bytes), version, source: source.packageName };
83
+ const method = checkIntegrity(bytes, dist.integrity, dist.shasum, policy);
84
+ return { catalog: await readCatalogTarball(bytes), version, source: source.packageName, verification: { policy, method } };
69
85
  }