@hanamesh/dsh-app-host 0.6.20 → 0.6.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/client-ui.d.ts +4 -1
- package/dist/client-ui.js +23 -19
- package/dist/install-target/fixtures/catalog.json +99 -8
- package/dist/install-target/fixtures/install-failure-cases.json +385 -35
- package/dist/install-target/fixtures/reference-consumer.js +1 -1
- package/dist/install-target/suite.d.ts +2 -1
- package/dist/install-target/suite.js +26 -6
- package/dist/library/catalog.js +24 -7
- package/dist/library/github-target.js +42 -0
- package/dist/library/handoff.js +5 -2
- package/dist/library/install.js +17 -7
- package/dist/library/recovery.js +5 -3
- package/dist/library/routes.js +1 -0
- package/dist/library/service.js +35 -19
- package/docs/CONTRACT.md +2 -2
- package/docs/INSTALL_TARGET.md +20 -4
- package/docs/LIBRARY.md +1 -1
- package/docs/LICENSES.json +1 -1
- package/docs/SECURITY.md +2 -0
- package/package.json +1 -1
- package/schemas/catalog-provider-page.schema.json +97 -10
- package/schemas/install-target.schema.json +252 -38
package/dist/library/handoff.js
CHANGED
|
@@ -14,6 +14,7 @@ import { installationStatusCors, serveInstallationStatus } from './installation-
|
|
|
14
14
|
*/
|
|
15
15
|
import { createServer } from 'node:http';
|
|
16
16
|
import { randomUUID } from 'node:crypto';
|
|
17
|
+
import {sameInstallTarget} from './github-target.js';
|
|
17
18
|
import { AppHostError, requireCondition } from '../errors.js';
|
|
18
19
|
|
|
19
20
|
export const HANDOFF_STATES = Object.freeze(['pending', 'shown', 'confirmed', 'installed', 'cancelled', 'failed']);
|
|
@@ -35,7 +36,7 @@ export const HANDOFF_RULES = Object.freeze([
|
|
|
35
36
|
{ rule: '只有「允许交接的网站」里的网站能探测本客户端、交接安装、读交接状态;别的网站什么也读不到', why: '否则任何网页都能知道你装了什么客户端、缺什么,用来识别你' },
|
|
36
37
|
{ rule: '网站交接只会在应用库里弹出确认;只有你点「确认安装」才会安装', why: '网站不能替你决定装什么' },
|
|
37
38
|
{ rule: '交接状态只给发起它的那个网站看', why: '一个网站不能读另一个网站的交接' },
|
|
38
|
-
{ rule: `交接请求体不超过 ${HANDOFF_BODY_MAX_BYTES} 字节`, why: '
|
|
39
|
+
{ rule: `交接请求体不超过 ${HANDOFF_BODY_MAX_BYTES} 字节`, why: '交接只包含目录身份、合约版本及已审核的结构化安装目标;正文上限为 4096 字节,不接受来源命令' },
|
|
39
40
|
{ rule: '端口在 1024–65535 之间、互不重复,至少一个', why: '1024 以下要管理员权限;没有端口就没有地方让网站找到' },
|
|
40
41
|
{ rule: '网站必须写成来源(协议 + 域名 [+ 端口]),http 只允许 127.0.0.1 / localhost', why: '浏览器按来源校验;明文 http 的公网网站可被中间人冒充' },
|
|
41
42
|
{ rule: `轮询间隔是 1–${TIMER_MAX_MS} 毫秒的整数,作废时间必须大于轮询间隔`, why: '计时器超过上限会立即触发;作废早于第一次轮询,交接永远不会被看到' },
|
|
@@ -75,6 +76,7 @@ export function createHandoffQueue({ ttlMs = () => HANDOFF_DEFAULTS.ttlMs, now =
|
|
|
75
76
|
const publish = record => { for (const listener of listeners) { try { listener(view(record)); } catch (error) { onError('hanamesh-app-host handoff: subscriber failed', error); } } };
|
|
76
77
|
const view = record => ({ handoffId: record.handoffId, state: record.state, origin: record.origin,
|
|
77
78
|
...(record.itemId ? { itemId: record.itemId } : {}), ...(record.packageName ? { packageName: record.packageName } : {}),
|
|
79
|
+
...(record.target.installTarget ? {installTarget:structuredClone(record.target.installTarget)} : {}),
|
|
78
80
|
...(record.displayName ? { displayName: record.displayName } : {}), ...(record.result ? { result: record.result } : {}),
|
|
79
81
|
...(record.error ? { error: record.error } : {}), createdAt: record.createdAt, updatedAt: record.updatedAt });
|
|
80
82
|
const move = (record, state, extra = {}) => {
|
|
@@ -97,7 +99,7 @@ export function createHandoffQueue({ ttlMs = () => HANDOFF_DEFAULTS.ttlMs, now =
|
|
|
97
99
|
return Object.freeze({
|
|
98
100
|
create({ origin, target, item, alreadyInstalled }) {
|
|
99
101
|
expire();
|
|
100
|
-
const at = now(), record = { handoffId: randomUUID(), state: 'pending', origin, target,
|
|
102
|
+
const at = now(), record = { handoffId: randomUUID(), state: 'pending', origin, target:structuredClone({...target,...(item.installTarget?{installTarget:item.installTarget}:{})}),
|
|
101
103
|
itemId: item.id, packageName: item.package?.name, displayName: item.displayName, createdAt: at, updatedAt: at };
|
|
102
104
|
records.set(record.handoffId, record);
|
|
103
105
|
if (alreadyInstalled) move(record, 'installed', { result: { status: 'already-installed' } }); else publish(record);
|
|
@@ -132,6 +134,7 @@ export function createHandoffQueue({ ttlMs = () => HANDOFF_DEFAULTS.ttlMs, now =
|
|
|
132
134
|
const name = event?.packageName ?? event?.result?.packageName; if (!name) return;
|
|
133
135
|
for (const record of records.values()) {
|
|
134
136
|
if (record.packageName !== name || !['shown', 'confirmed'].includes(record.state)) continue;
|
|
137
|
+
if(record.target.installTarget&&(!event.installTarget&&!event.result?.installTarget||!sameInstallTarget(record.target.installTarget,event.installTarget??event.result?.installTarget)))continue;
|
|
135
138
|
if (event.type === 'library.install-started') move(record, 'confirmed');
|
|
136
139
|
else if (event.type === 'library.install-done') move(record, 'installed', { result: { status: event.status ?? event.result?.status } });
|
|
137
140
|
else if (event.type === 'library.install-failed') move(record, 'failed', { error: { code: event.code ?? 'INSTALL_FAILED', message: '安装失败' } });
|
package/dist/library/install.js
CHANGED
|
@@ -11,6 +11,7 @@ import { validateDefinition } from '../descriptor.js';
|
|
|
11
11
|
import { itemKind } from './catalog.js';
|
|
12
12
|
import { profileSnapshot, runtimeSnapshot, profileChecks, runtimeChecks, packageRestoreArgs, keepOriginals } from './recovery.js';
|
|
13
13
|
import {SUITE_BUNDLE,prepareSuiteUpgrade,readSuiteStatus,createCoreSuiteAdapter} from './suite.js';
|
|
14
|
+
import {packageInstallTarget,installTargetName,pinnedGithubSpec} from './github-target.js';
|
|
14
15
|
|
|
15
16
|
/** Suite packages are changed only by the confirmed suite upgrade command. */
|
|
16
17
|
export const PROTECTED_PACKAGES=Object.freeze([SUITE_BUNDLE,'@hanamesh/core','@hanamesh/usage','@hanamesh/dsh-app-host','hanamesh-brand']);
|
|
@@ -160,23 +161,31 @@ export function createLibraryInstaller({profileDir,profileName,dataRoot,nodeBina
|
|
|
160
161
|
const name=item.package.name,operationId=/^[A-Za-z0-9-]{1,64}$/.test(context?.operationId??'')?context.operationId:randomUUID();let stage='baseline',base,runtime,installRegistry=null;
|
|
161
162
|
try{
|
|
162
163
|
base=await profileSnapshot(profileDir,name);
|
|
163
|
-
stage='before-add';let version;
|
|
164
|
+
stage='before-add';let version;const target=item.installTarget,github=target?.source==='github';
|
|
165
|
+
if(target?.source==='npm'){version=target.version;installRegistry=registry??null;}
|
|
166
|
+
else if(!github)({version,registry:installRegistry}=await latest(name));
|
|
167
|
+
const spec=github?pinnedGithubSpec(target.repo,target.ref,target.path):`${name}@${version}`;
|
|
164
168
|
const status='restart-required';
|
|
165
|
-
stage='add';await run(['add','--save-exact',...(installRegistry===null?[]:[`--registry=${installRegistry}`])
|
|
169
|
+
stage='add';await run(['add','--save-exact',...(installRegistry===null?[]:[`--registry=${installRegistry}`]),...(target?.allowBuild??[]).map(entry=>`--allow-build=${entry}`),spec]);
|
|
166
170
|
stage='readback';const added=await profileSnapshot(profileDir,name);
|
|
167
|
-
|
|
168
|
-
|
|
171
|
+
if(github){
|
|
172
|
+
const actual=await json(join(profileDir,'node_modules',...name.split('/'),'package.json'));
|
|
173
|
+
requireCondition(added.spec===spec&&actual.name===name&&typeof actual.version==='string'&&typeof actual.dsh?.bundle?.patch==='string'&&actual.dsh.bundle.patch.length>0&&added.bundles.includes(name),
|
|
174
|
+
'INSTALL_READBACK_MISMATCH','The profile does not resolve and enable the reviewed GitHub package.',{expected:spec,spec:added.spec,packageName:actual.name,active:added.bundles.includes(name)},502);
|
|
175
|
+
version=actual.version;
|
|
176
|
+
}else requireCondition(added.spec===version&&added.module===version,'INSTALL_READBACK_MISMATCH','The profile does not resolve the exact version that was added.',{expected:version,spec:added.spec,module:added.module},502);
|
|
177
|
+
if(kind==='plugin')return{status,kind,packageName:name,version,...(target?{installTarget:target}:{}),...(github?{commit:target.ref}:{})};
|
|
169
178
|
stage='definition';const record=await installed(name);const declared=record.definition.deployments.map(deployment=>deployment.runtime).find(Boolean);
|
|
170
179
|
if(declared){stage='provision';const root=runtimeRoot(record.definition.id);runtime={root,base:await runtimeSnapshot(await runtimeApi('ledger'),root)};
|
|
171
180
|
await (await runtimeApi('provision'))(declared.manifest,{root,only:[declared.item],onProgress:event=>emit({type:'library.provision-progress',appId:record.definition.id,...event})});}
|
|
172
|
-
return{status:'restart-required',kind,appId:record.definition.id,packageName:name,version};
|
|
181
|
+
return{status:'restart-required',kind,appId:record.definition.id,packageName:name,version,...(target?{installTarget:target}:{}),...(github?{commit:target.ref}:{})};
|
|
173
182
|
}catch(error){noteFailurePhase(error,{baseline:'profile baseline','before-add':'registry fetch',add:'CLI run',readback:'profile readback',definition:'application readback',provision:'runtime provision'}[stage]);error.recovery=await recover({name,stage,base,runtime,operationId,installRegistry});throw error;}
|
|
174
183
|
}
|
|
175
184
|
async function installAs(item,kind,context){
|
|
176
185
|
permitted(item.package.name);const name=item.package.name;
|
|
177
|
-
emit({type:'library.install-started',...(item.id?{itemId:item.id}:{}),packageName:name,kind});
|
|
186
|
+
emit({type:'library.install-started',...(item.id?{itemId:item.id}:{}),packageName:name,kind,...(item.installTarget?{installTarget:item.installTarget}:{})});
|
|
178
187
|
try{const result=await profileWriter.run(()=>transaction(item,kind,context));emit({type:'library.install-done',...result});return result;}
|
|
179
|
-
catch(error){emit({type:'library.install-failed',...(item.id?{itemId:item.id}:{}),packageName:name,kind,code:error.code??'INSTALL_FAILED',...(error.recovery?{recovery:error.recovery}:{})});throw error;}
|
|
188
|
+
catch(error){emit({type:'library.install-failed',...(item.id?{itemId:item.id}:{}),packageName:name,kind,...(item.installTarget?{installTarget:item.installTarget}:{}),code:error.code??'INSTALL_FAILED',...(error.recovery?{recovery:error.recovery}:{})});throw error;}
|
|
180
189
|
}
|
|
181
190
|
let suitePlan;
|
|
182
191
|
const suiteProvider=coreSuitePlan??createCoreSuiteAdapter({core:coreService,registry:registry??'https://registry.npmjs.org',suiteSourceMode,suiteSourceService});
|
|
@@ -216,6 +225,7 @@ export function createLibraryInstaller({profileDir,profileName,dataRoot,nodeBina
|
|
|
216
225
|
}
|
|
217
226
|
});},
|
|
218
227
|
async install(item,context){
|
|
228
|
+
if(item.installTarget){const target=packageInstallTarget(item.installTarget);item={...item,installTarget:target,package:{registry:target.source,name:installTargetName(target)}};}
|
|
219
229
|
const kind=itemKind(item);
|
|
220
230
|
requireCondition(kind!=='listing','NOT_INSTALLABLE','Catalog item has no npm package to install.');
|
|
221
231
|
return await installAs(item,kind,context);
|
package/dist/library/recovery.js
CHANGED
|
@@ -15,11 +15,11 @@ async function optional(path){try{return await readFile(path);}catch(error){if(e
|
|
|
15
15
|
|
|
16
16
|
/** The profile facts one package operation can change: its direct spec, bundle selection, lockfile and resolved module. */
|
|
17
17
|
export async function profileSnapshot(profileDir,name){
|
|
18
|
-
const manifestBytes=await optional(join(profileDir,'package.json')),lockBytes=await optional(join(profileDir,'pnpm-lock.yaml'));
|
|
18
|
+
const manifestBytes=await optional(join(profileDir,'package.json')),lockBytes=await optional(join(profileDir,'pnpm-lock.yaml')),workspaceBytes=await optional(join(profileDir,'pnpm-workspace.yaml'));
|
|
19
19
|
const manifest=manifestBytes===null?{}:JSON.parse(manifestBytes.toString('utf8'));
|
|
20
20
|
const direct=Object.assign({},...DEPENDENCY_FIELDS.map(field=>manifest[field]??{}));
|
|
21
21
|
const moduleBytes=await optional(join(profileDir,'node_modules',...name.split('/'),'package.json'));
|
|
22
|
-
return{manifestBytes,lockBytes,spec:direct[name]??null,others:Object.fromEntries(Object.entries(direct).filter(([key])=>key!==name)),
|
|
22
|
+
return{manifestBytes,lockBytes,workspaceBytes,buildPermissions:digest(workspaceBytes),spec:direct[name]??null,others:Object.fromEntries(Object.entries(direct).filter(([key])=>key!==name)),
|
|
23
23
|
bundles:manifest.dsh?.profile?.bundles??[],lock:digest(lockBytes),module:moduleBytes===null?null:JSON.parse(moduleBytes.toString('utf8')).version??null};
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -39,6 +39,7 @@ export function profileChecks(base,now,name){
|
|
|
39
39
|
{item:'dependency',expected:base.spec,actual:now.spec},
|
|
40
40
|
{item:'module',expected:base.module,actual:now.module},
|
|
41
41
|
{item:'bundle',expected:base.bundles.includes(name),actual:now.bundles.includes(name)},
|
|
42
|
+
{item:'build-permissions',expected:base.buildPermissions,actual:now.buildPermissions},
|
|
42
43
|
].map(row=>({...row,ok:same(row.expected,row.actual)}));
|
|
43
44
|
if(concurrent.length)rows.push({item:'lockfile',expected:base.lock,actual:now.lock,ok:false,reason:'CONCURRENT_PROFILE_WRITE',concurrent});
|
|
44
45
|
else rows.push({item:'lockfile',expected:base.lock,actual:now.lock,ok:base.lock===now.lock},{item:'bundles',expected:base.bundles,actual:now.bundles,ok:same(base.bundles,now.bundles)});
|
|
@@ -51,7 +52,7 @@ export function runtimeChecks(base,now){
|
|
|
51
52
|
|
|
52
53
|
/** The official CLI arguments that put one package back to its baseline spec, or none when nothing package-side changed. */
|
|
53
54
|
export function packageRestoreArgs(base,now,name){
|
|
54
|
-
if(profileChecks(base,now,name).every(row=>row.ok||row.reason==='CONCURRENT_PROFILE_WRITE'))return null;
|
|
55
|
+
if(profileChecks(base,now,name).every(row=>row.ok||row.reason==='CONCURRENT_PROFILE_WRITE'||row.item==='build-permissions'))return null;
|
|
55
56
|
if(base.spec===null)return now.spec===null?['install']:['remove',name];
|
|
56
57
|
return['add',...(EXACT.test(base.spec)?['--save-exact']:[]),`${name}@${base.spec}`];
|
|
57
58
|
}
|
|
@@ -61,6 +62,7 @@ export async function keepOriginals(dir,base,runtimeBase){
|
|
|
61
62
|
await mkdir(dir,{recursive:true,mode:0o700});const files=[];
|
|
62
63
|
if(base.manifestBytes!==null){await writeFile(join(dir,'package.json'),base.manifestBytes,{mode:0o600});files.push('package.json');}
|
|
63
64
|
if(base.lockBytes!==null){await writeFile(join(dir,'pnpm-lock.yaml'),base.lockBytes,{mode:0o600});files.push('pnpm-lock.yaml');}
|
|
65
|
+
if(base.workspaceBytes){await writeFile(join(dir,'pnpm-workspace.yaml'),base.workspaceBytes,{mode:0o600});files.push('pnpm-workspace.yaml');}
|
|
64
66
|
if(runtimeBase){await writeFile(join(dir,'runtime-ledger.json'),JSON.stringify(runtimeBase,undefined,2)+'\n',{mode:0o600});files.push('runtime-ledger.json');}
|
|
65
67
|
return{dir,files,sha256:{...(base.manifestBytes===null?{}:{'package.json':digest(base.manifestBytes)}),...(base.lockBytes===null?{}:{'pnpm-lock.yaml':digest(base.lockBytes)})}};
|
|
66
68
|
}
|
package/dist/library/routes.js
CHANGED
|
@@ -52,6 +52,7 @@ export function createLibraryHttpHandler(library,{parentOrigin,transportOrigin,a
|
|
|
52
52
|
requireCondition(subject&&typeof subject.principalId==='string','UNAUTHENTICATED','Host authentication is required.',{},401);
|
|
53
53
|
if(readOnly)requireCondition(new Set(url.searchParams.keys()).size===[...url.searchParams.keys()].length,'INVALID_INPUT','Duplicate query fields are not accepted.');
|
|
54
54
|
const input=readOnly?Object.fromEntries(url.searchParams):await jsonBody(req);
|
|
55
|
+
if(readOnly&&path==='/hanamesh/library/target'&&input.installTarget!==undefined){try{input.installTarget=JSON.parse(input.installTarget);}catch{throw new AppHostError('INVALID_INPUT','installTarget must contain a structured catalog target.');}}
|
|
55
56
|
requireCondition(await authorize(subject,path,input)===true,'FORBIDDEN','Operation is not authorized.',{},403);
|
|
56
57
|
let result;
|
|
57
58
|
if(NODE_RUNTIME_ROUTES.includes(path)){
|
package/dist/library/service.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { AppHostError, requireCondition, SerialQueue } from '../errors.js';
|
|
3
3
|
import { loadCatalog, isApplication, itemKind, upgradeAvailable, validateManifestUrl } from './catalog.js';
|
|
4
|
-
import { scanInstalled, scanInstalledPlugins, snapshotProfileDependencies } from './installed.js';
|
|
4
|
+
import { scanInstalled, scanInstalledPlugins, snapshotProfileDependencies, readProfileManifest } from './installed.js';
|
|
5
|
+
import {packageInstallTarget,installTargetName,sameInstallTarget,catalogPackageTargets,pinnedGithubSpec,catalogInstallationAllowed} from './github-target.js';
|
|
5
6
|
import {SUITE_BUNDLE,SUITE_TARGET,SUITE_PACKAGES,readSuiteStatus,CORE_SUITE_DECLARATION_MISSING,readSuiteSourceState} from './suite.js';
|
|
6
7
|
import { PROTECTED_PACKAGES } from './install.js';
|
|
7
8
|
import { handoffSettings } from './handoff.js';
|
|
@@ -15,6 +16,7 @@ const text=(value,max)=>typeof value==='string'&&value.length>0&&value.length<=m
|
|
|
15
16
|
const RESTART_STATES=new Set(['installed-not-loaded','uninstalled-not-unloaded']);
|
|
16
17
|
const protectedPackage=name=>PROTECTED_PACKAGES.includes(name);
|
|
17
18
|
const present=row=>row&&['registered','installed','installed-not-loaded'].includes(row.state);
|
|
19
|
+
const npmUpgradeAvailable=(target,row)=>Boolean(target?.source==='npm'&&row?.version&&row.state!=='uninstalled-not-unloaded'&&upgradeAvailable(target.version,row.version));
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* `client`: which client this host is ('dsh' | 'hanamesh', see client-identity.js). `unavailable`: when there is no
|
|
@@ -64,45 +66,59 @@ export function createLibraryService({domain,host,config={},dataRoot,ledgerReade
|
|
|
64
66
|
requireCondition(input&&typeof input==='object'&&!Array.isArray(input),'INVALID_INPUT','Only itemId and packageName are accepted.');
|
|
65
67
|
// rc.8: optional install-target contract declaration. Absent keeps the v1 semantics; anything but '1' is refused explicitly, before field checks.
|
|
66
68
|
requireCondition(input.contractVersion===undefined||input.contractVersion===INSTALL_TARGET_CONTRACT_VERSION,'CONTRACT_VERSION_UNSUPPORTED','This market does not implement the requested install-target contract version.',{supported:[INSTALL_TARGET_CONTRACT_VERSION]});
|
|
67
|
-
requireCondition(Object.keys(input).every(key=>['itemId','packageName','contractVersion'].includes(key)),'INVALID_INPUT','Only
|
|
69
|
+
requireCondition(Object.keys(input).every(key=>['itemId','packageName','contractVersion','installTarget'].includes(key)),'INVALID_INPUT','Only catalog identity and package installation target are accepted.');
|
|
68
70
|
const{itemId,packageName}=input;
|
|
71
|
+
const expected=input.installTarget===undefined?undefined:packageInstallTarget(input.installTarget);
|
|
72
|
+
requireCondition(!expected||itemId!==undefined,'INVALID_INPUT','A structured installation target requires itemId.');
|
|
69
73
|
if(itemId===SUITE_TARGET){
|
|
70
|
-
requireCondition(packageName===undefined,'INVALID_INPUT','套件升级不接受包名或任意替换参数。');
|
|
74
|
+
requireCondition(packageName===undefined&&!expected,'INVALID_INPUT','套件升级不接受包名或任意替换参数。');
|
|
71
75
|
requireCondition(typeof installer?.prepareSuiteUpgrade==='function','SUITE_UPGRADE_UNAVAILABLE','当前客户端还不支持安全套件升级,请使用补齐命令。',{},503);
|
|
72
76
|
const plan=await installer.prepareSuiteUpgrade();
|
|
73
|
-
return{contractVersion:'1',source:{kind:'suite',manifestUrl:'https://github.com/yzsnstotz'},item:{id:SUITE_TARGET,displayName:'HanaMesh 套件',kind:'suite-upgrade',package:{registry:'npm',name:SUITE_BUNDLE},
|
|
77
|
+
return{contractVersion:'1',source:{kind:'suite',manifestUrl:'https://github.com/yzsnstotz'},item:{id:SUITE_TARGET,displayName:'HanaMesh 套件',kind:'suite-upgrade',package:{registry:'npm',name:SUITE_BUNDLE},installed:{state:'installed',version:suiteStatus.version},upgradeAvailable:plan.available,suiteUpgrade:plan}};
|
|
74
78
|
}
|
|
75
79
|
requireCondition(itemId!==undefined||packageName!==undefined,'INVALID_INPUT','itemId or packageName is required.');
|
|
76
80
|
requireCondition(itemId===undefined||(text(itemId,160)&&!/[\u0000-\u001f\u007f]/.test(itemId)),'INVALID_INPUT','itemId is invalid.');
|
|
77
81
|
requireCondition(packageName===undefined||(text(packageName,214)&&PACKAGE_NAME.test(packageName)),'INVALID_INPUT','packageName is invalid.');
|
|
82
|
+
async function resolved(found,source){
|
|
83
|
+
requireCondition(catalogInstallationAllowed(found),'NOT_INSTALLABLE','The catalog has not approved one-click installation.',{reason:found.oneClickInstall?.reason});
|
|
84
|
+
const targets=catalogPackageTargets(found),selected=targets.find(target=>expected?sameInstallTarget(target,expected):packageName===undefined||installTargetName(target)===packageName);
|
|
85
|
+
requireCondition(!expected||selected,'CATALOG_CHANGED','The reviewed installation source or build permission changed. Review the current target again.',{},409);
|
|
86
|
+
requireCondition(selected||(found.installMethods===undefined&&found.package?.registry==='npm'),'NOT_INSTALLABLE','This catalog entry has no fixed package installation target for the requested package.',{reason:'fixed-install-target-required'});
|
|
87
|
+
const name=selected?installTargetName(selected):found.package.name;
|
|
88
|
+
requireCondition(!protectedPackage(name),'PACKAGE_DENIED','The HanaMesh suite is not managed through the market.',{},403);
|
|
89
|
+
const rows=[...await installedPlugins(),...await installed()],row=rows.find(value=>value.packageName===name)??null;
|
|
90
|
+
let upgrade=npmUpgradeAvailable(selected,row);
|
|
91
|
+
if(selected?.source==='github'&&row){
|
|
92
|
+
const saved=await readProfileManifest(config.profileDir);
|
|
93
|
+
upgrade=saved.dependencies[name]!==pinnedGithubSpec(selected.repo,selected.ref,selected.path)||row.active===false;
|
|
94
|
+
}
|
|
95
|
+
const item={...found,...(selected?{package:{registry:selected.source,name},installTarget:selected}:{}),installed:row,upgradeAvailable:upgrade};
|
|
96
|
+
return{contractVersion:INSTALL_TARGET_CONTRACT_VERSION,source,item:{...item,kind:itemKind(item)}};
|
|
97
|
+
}
|
|
98
|
+
const packageMatches=row=>row.package?.name===packageName||(row.installMethods??[]).some(method=>method.target&&(method.target.packageName??method.target.name)===packageName);
|
|
78
99
|
let cursor;const seen=new Set(),matches=new Map();
|
|
79
100
|
do{
|
|
80
|
-
const page=await catalog({q:packageName??'',category:'',cursor});
|
|
81
|
-
const found=page.items.find(row=>(itemId===undefined||row.id===itemId)&&(packageName===undefined||row
|
|
101
|
+
const page=await catalog({q:itemId===undefined?packageName??'':'',category:'',cursor});
|
|
102
|
+
const found=page.items.find(row=>(itemId===undefined||row.id===itemId)&&(packageName===undefined||packageMatches(row)));
|
|
82
103
|
if(found&&itemId===undefined&&page.schemaVersion==='hanamesh.catalog.v1'){
|
|
83
104
|
for(const row of page.items.filter(row=>row.package?.name===packageName))matches.set(row.id,{row,source:page.source});
|
|
84
105
|
requireCondition(matches.size<=1,'CATALOG_TARGET_AMBIGUOUS','Choose an itemId: this npm name belongs to multiple catalog identities.',{packageName},409);
|
|
85
106
|
}else if(found){
|
|
86
|
-
|
|
87
|
-
requireCondition(!protectedPackage(found.package.name),'PACKAGE_DENIED','The HanaMesh suite is not managed through the market.',{},403);
|
|
88
|
-
const rows=[...await installedPlugins(),...await installed()],row=rows.find(value=>value.packageName===found.package.name)??null;
|
|
89
|
-
return{contractVersion:INSTALL_TARGET_CONTRACT_VERSION,source:page.source,item:{...found,kind:itemKind(found),installed:row,upgradeAvailable:Boolean(row?.version&&row.state!=='uninstalled-not-unloaded'&&upgradeAvailable(found.latestVersion,row.version))}};
|
|
107
|
+
return await resolved(found,page.source);
|
|
90
108
|
}
|
|
91
109
|
cursor=page.page?.nextCursor;
|
|
92
110
|
requireCondition(!cursor||!seen.has(cursor),'INVALID_CURSOR','Catalog cursor repeated.');if(cursor)seen.add(cursor);
|
|
93
111
|
}while(cursor);
|
|
94
112
|
if(matches.size===1){
|
|
95
113
|
const{row:found,source}=matches.values().next().value;
|
|
96
|
-
|
|
97
|
-
const rows=[...await installedPlugins(),...await installed()],row=rows.find(value=>value.packageName===found.package.name)??null;
|
|
98
|
-
return{contractVersion:INSTALL_TARGET_CONTRACT_VERSION,source,item:{...found,kind:itemKind(found),installed:row,upgradeAvailable:Boolean(row?.version&&row.state!=='uninstalled-not-unloaded'&&upgradeAvailable(found.latestVersion,row.version))}};
|
|
114
|
+
return await resolved(found,source);
|
|
99
115
|
}
|
|
100
116
|
throw new AppHostError('CATALOG_ITEM_MISSING','The market catalog does not contain this target.',{},404);
|
|
101
117
|
}
|
|
102
118
|
function installItem(item){
|
|
103
|
-
const key=item.package.name;if(installing.has(key))
|
|
119
|
+
const key=item.package.name;if(installing.has(key)){const prior=installing.get(key);requireCondition(JSON.stringify(prior.target)===JSON.stringify(item.installTarget),'CATALOG_CHANGED','Another installation of this package is using a different reviewed target.',{},409);return prior.started;}
|
|
104
120
|
const started=operation('install',async operationId=>{try{return await markLive(await installer.install(item,{operationId}));}finally{installing.delete(key);}});
|
|
105
|
-
installing.set(key,started);return started;
|
|
121
|
+
installing.set(key,{started,target:item.installTarget});return started;
|
|
106
122
|
}
|
|
107
123
|
async function findByPackage(packageName){
|
|
108
124
|
requireCondition(text(packageName,214)&&PACKAGE_NAME.test(packageName),'INVALID_INPUT','packageName is required.');
|
|
@@ -120,9 +136,9 @@ export function createLibraryService({domain,host,config={},dataRoot,ledgerReade
|
|
|
120
136
|
const page=await catalog({q,category,cursor});const installedRows=await installed();const pluginRows=await installedPlugins();
|
|
121
137
|
suiteStatus={...await suiteFacts(),...(suiteStatus.restartRequired?{restartRequired:true}:{}),...(suiteStatus.upgradeError?{upgradeError:suiteStatus.upgradeError}:{})};
|
|
122
138
|
const byPackage=new Map([...pluginRows,...installedRows].map(row=>[row.packageName,row]));
|
|
123
|
-
let items=page.items.filter(item=>!protectedPackage(item.package?.name)).map(item=>{const row=byPackage.get(item.package?.name)??null;const kind=itemKind(item);
|
|
124
|
-
return{...item,kind,application:isApplication(item),installed:row,upgradeAvailable:
|
|
125
|
-
if(config.profileDir&&!cursor&&(!category||category==='plugin')&&(!q||'hanamesh 套件'.includes(q.toLowerCase())))items.unshift({id:'hanamesh:suite',displayName:'HanaMesh 套件',summary:'官方 · 不计分。Core、使用记录与应用库一次装齐。',kind:'suite',official:true,
|
|
139
|
+
let items=page.items.map(item=>{const selected=catalogPackageTargets(item)[0];return selected?{...item,installTarget:selected,package:{registry:selected.source,name:installTargetName(selected)}}:item;}).filter(item=>!protectedPackage(item.package?.name)).map(item=>{const row=byPackage.get(item.package?.name)??null;const kind=itemKind(item);
|
|
140
|
+
return{...item,kind,application:isApplication(item),installed:row,upgradeAvailable:npmUpgradeAvailable(item.installTarget,row)};});
|
|
141
|
+
if(config.profileDir&&!cursor&&(!category||category==='plugin')&&(!q||'hanamesh 套件'.includes(q.toLowerCase())))items.unshift({id:'hanamesh:suite',displayName:'HanaMesh 套件',summary:'官方 · 不计分。Core、使用记录与应用库一次装齐。',kind:'suite',official:true,installed:suiteStatus.status==='installed'?{state:'installed',version:suiteStatus.version}:null,suite:suiteStatus,...((installer?.suiteUpgradeUnavailable??suiteStatus.suiteUpgradeUnavailable)?{suiteUpgradeUnavailable:installer?.suiteUpgradeUnavailable??suiteStatus.suiteUpgradeUnavailable}:{})});
|
|
126
142
|
const categories=[...new Set(page.items.flatMap(item=>item.categories??[]))].sort();
|
|
127
143
|
return{source:page.source,items,categories,installed:installedRows,plugins:pluginRows,restartRequired:[...pluginRows,...installedRows].some(row=>RESTART_STATES.has(row.state)),...(bootSnapshotError?{restartBaseline:{known:false,code:bootSnapshotError}}:{}),page:page.page};},
|
|
128
144
|
sources(){return{sources:structuredClone(state.sources),revision:state.revision};},
|
|
@@ -154,7 +170,7 @@ export function createLibraryService({domain,host,config={},dataRoot,ledgerReade
|
|
|
154
170
|
requireCondition(input&&Object.keys(input).length===1&&typeof input.planId==='string','INVALID_INPUT','Only the current planId is accepted.');
|
|
155
171
|
return await operation('install',async()=>{try{const result=await installer.upgradeSuite(input);suiteStatus={...await suiteFacts(),restartRequired:true};return result;}catch(error){if(error.details?.stage!=='suite-upgrade')throw error;const actual=await readSuiteStatus(config.profileDir);suiteStatus={...await suiteFacts(),restartRequired:true,upgradeError:error.code??'SUITE_UPGRADE_FAILED'};error.details={...error.details,stage:'suite-upgrade',actual,command:error.details.command};emit({type:'library.install-failed',packageName:SUITE_BUNDLE,code:error.code??'SUITE_UPGRADE_FAILED',details:{stage:'suite-upgrade',actual,command:error.details.command}});throw error;}});
|
|
156
172
|
},
|
|
157
|
-
async install(input){requireCondition(input?.itemId!==SUITE_TARGET,'SUITE_CONFIRMATION_REQUIRED','请在客户端确认完整升级计划。',{},403);requireInstaller('Install requires absolute profileDir, nodeBinary, dshBin and profileName.');const{item}=await resolveTarget(input??{});if(present(item.installed)&&!item.upgradeAvailable)return{status:'already-installed',itemId:item.id,packageName:item.package.name};return await installItem(item);},
|
|
173
|
+
async install(input){requireCondition(input?.itemId!==SUITE_TARGET,'SUITE_CONFIRMATION_REQUIRED','请在客户端确认完整升级计划。',{},403);requireInstaller('Install requires absolute profileDir, nodeBinary, dshBin and profileName.');const{item}=await resolveTarget(input??{});requireCondition(item.installTarget?.source!=='github'||input.installTarget,'INVALID_INPUT','Confirm the displayed fixed GitHub installation target before installing.');if(present(item.installed)&&!item.upgradeAvailable)return{status:'already-installed',itemId:item.id,packageName:item.package.name};return await installItem(item);},
|
|
158
174
|
async provision(input){requireInstaller('Runtime provision is unavailable.');
|
|
159
175
|
// LIB-PROVISION-INPUT: a missing field is the caller's error (400), not an internal TypeError inside the operation.
|
|
160
176
|
requireCondition(text(input?.appId,160),'INVALID_INPUT','appId is required.');requireCondition(text(input?.packageName,214)&&PACKAGE_NAME.test(input.packageName),'INVALID_INPUT','packageName is required.');requireCondition(text(input?.runtimeItem,160),'INVALID_INPUT','runtimeItem is required.');
|
package/docs/CONTRACT.md
CHANGED
|
@@ -149,9 +149,9 @@ DSH 绑定必须提供同一个 `single` domain 的一次完整镜像 publish
|
|
|
149
149
|
|
|
150
150
|
| 路由 | 方法 | 输入 | 输出 |
|
|
151
151
|
|---|---|---|---|
|
|
152
|
-
| `/hanamesh/library` | GET | `q`、`category`(缺省 `hanamesh-app`;`category=` 空 = 全部)、`cursor` | `{source, items[], categories[], installed[], plugins[], restartRequired, page}`;`items[]` 每条目在目录字段之外加 `kind`(`application` = `hanamesh-app` + npm 包;`plugin` = 其它 npm 包;`listing` = 无 npm 包,仅收录)、`application`(兼容)、`installed`(应用行或插件行,或 `null`)、`upgradeAvailable
|
|
152
|
+
| `/hanamesh/library` | GET | `q`、`category`(缺省 `hanamesh-app`;`category=` 空 = 全部)、`cursor` | `{source, items[], categories[], installed[], plugins[], restartRequired, page}`;`items[]` 每条目在目录字段之外加 `kind`(`application` = `hanamesh-app` + npm 包;`plugin` = 其它 npm 包;`listing` = 无 npm 包,仅收录)、`application`(兼容)、`installed`(应用行或插件行,或 `null`)、`upgradeAvailable`(已选 npm `installTarget.version` 严格新于已装 `version`;不读取退休目录版本;已装行处于 `uninstalled-not-unloaded` 时恒 false);`categories` = 本页条目出现过的类别(目录契约没有类别清单端点) |
|
|
153
153
|
| `/hanamesh/library/installedPlugins` | GET(POST → 405) | 无参数 | `{plugins[], apps[], restartRequired}`;`plugins[]`:`{packageName, version, kind:'plugin', bundle, active, state, errorCode?}`,`state` ∈ `installed` / `installed-not-loaded`(启动后新增)/ `uninstalled-not-unloaded`(启动后移除,进程仍加载)/ `invalid`(依赖里有、`node_modules` 里读不到);`apps[]` = 原已安装应用扫描行(`registered` / `installed-not-loaded` / `runtime-missing` / `invalid`) |
|
|
154
|
-
| `/hanamesh/library/install` | POST | `{itemId, packageName?}` | 202 `{operationId,status:'started'}`;`packageName` 给出时用它精确查目录(`q=<包名>&category=`)再按 `itemId`
|
|
154
|
+
| `/hanamesh/library/install` | POST | `{itemId, packageName?}` | 202 `{operationId,status:'started'}`;`packageName` 给出时用它精确查目录(`q=<包名>&category=`)再按 `itemId` 匹配;应用与插件同一路径走官方 CLI;有 installMethods 时必须选中固定 target,npm 用 target.version,GitHub 用完整 SHA/子目录/构建名单,不在安装时解析 latest。无匹配目标拒绝 NOT_INSTALLABLE 并返回具名原因。未提供 installMethods 的标准目录保持既有 npm 路径;应用另外供给 runtime;也是升级路径 |
|
|
155
155
|
| `/hanamesh/library/plugins/install` | POST | `{packageName}` | 202;包必须在启用目录里能搜到(否则 404 `CATALOG_ITEM_MISSING`);目录条目是应用则走应用路径 |
|
|
156
156
|
| `/hanamesh/library/plugins/uninstall` | POST | `{packageName}` | 202;`dsh plugin remove <包>`;若该包是已装应用则转应用卸载(runtime 清理 + 运行中 `INSTANCE_IN_USE` 409) |
|
|
157
157
|
| `/hanamesh/library/provision` | POST | `{appId, packageName, runtimeItem}` 三者必填 | 202;缺字段/非法 → 400 `INVALID_INPUT`(`LIB-PROVISION-INPUT`) |
|
package/docs/INSTALL_TARGET.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# 客户端市场安装目标入口
|
|
2
2
|
|
|
3
|
+
## 0.6.21 候选 · 固定 GitHub 安装目标
|
|
4
|
+
|
|
5
|
+
目标新增 `installTarget`,采用 C-INDEXER-CATALOG 已确认的两个形状:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
type PackageInstallTarget =
|
|
9
|
+
| {source:'npm';name:string;version:string;allowBuild:string[]}
|
|
10
|
+
| {source:'github';repo:string;ref:string;path:string|null;packageName:string;allowBuild:string[]};
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
GitHub ref 必须是收录时固定的完整 40 位 SHA,不在安装时解析分支。结构化目标必须带 itemId;App-host 从自己的启用目录 installMethods 解析并逐字段核对,整个条目 oneClickInstall.allowed=false 时拒绝,不能换用同名 npm 包。条目带 installMethods 而未选中匹配目标时,返回 NOT_INSTALLABLE / details.reason=fixed-install-target-required,不得退回 package 展示名去装 npm latest;只有未提供 installMethods 的条目保留原标准 npm 路径。HanaMesh 视图拒绝退休的 latestVersion/license,确认页不显示版本行,升级按钮不拼目录版本;npm 升级按 installTarget.version 与已装实际版本比较。GET target/导航片段将 installTarget JSON 编码;POST install/网站 handoff 使用对象。响应 item.installTarget 是客户端确认回传的预期目标。目录 SHA、路径或构建许可变化会拒绝 CATALOG_CHANGED,必须重新查看;不能在确认之后自动换目标。
|
|
14
|
+
|
|
15
|
+
确认沿现有市场表面,展示完整仓库、commit、子目录;allowBuild 非空时单列「会在本机执行构建脚本」和名单,并说明 pnpm 会保存许可。取消或预览不执行 CLI。本人确认后,仅调用原受控 `dsh plugin --profile <profile> add --save-exact [--allow-build=<项>…] github:owner/repo#<SHA>[&path:<目录>]`,每段都是独立 argv,不执行来源 command。npm 目标直接使用收录时固定的精确版本,不在安装时再解析版本。
|
|
16
|
+
|
|
17
|
+
GitHub CLI exit 0 后还须核对 profile 精确保存的 Git spec、模块包名/版本、dsh.bundle 和 profile 启用列表,随后报告 restart-required;重开后由原生 DSH 加载。不自行 clone/build,不改上游,不用进程内安装器。失败补偿沿原 CLI;pnpm-workspace.yaml 若发生变化,恢复报告保留 build-permissions 残留及原文件,不擅自撤销或静默宣称恢复。handoff 事件按完整目标匹配,不能只靠同名包把另一个 SHA 报为成功。
|
|
18
|
+
|
|
3
19
|
网站发现:DSH 本地路由与交接监听的 `/hanamesh/library/install-target` 都按当前 `library.handoff.allowedOrigins` 回答跨来源 GET / OPTIONS;只回显获准 Origin(Vary: Origin),不使用通配 CORS。未获准来源为 403 ORIGIN_DENIED,正文无客户端能力。正常工作区同源读取、无 Origin 的本机工具读取保留。其余安装/状态路由继续需要各自认证与确认。
|
|
4
20
|
|
|
5
21
|
轮询设置:`pollMs` 写入应用库真实 storage domain;保存、卸载再加载后保持。旧记录没有该字段时使用既有配置/默认 1500 ms,不重写其它存储数据。
|
|
@@ -14,7 +30,7 @@ AppHost `0.2.0-rc.10`(安装目标入口自 rc7;合约声明与随包套件
|
|
|
14
30
|
|
|
15
31
|
在 DSH 的正常客户端模块系统中取得 `@hanamesh/dsh-app-host` 的公开客户端模块,调用 `openInstallTarget({itemId?, packageName?})`。已有客户端 `market` service 也提供同名方法:`market.openInstallTarget(target)`;原 `market.render({preferredSubsectionId})` 与 `setSettingsVisible()` 保持原协议。
|
|
16
32
|
|
|
17
|
-
|
|
33
|
+
至少一个标识;两者都给时必须匹配同一条目录项。可选 installTarget 仅为上述目录目标的预期值;其它字段(包括顶层 version、url、downloadUrl)拒绝。itemId 最长 160 字符;packageName 按 npm 包名语法校验、最长 214 字符。壳不得解析目录、代填版本/下载地址、点击私有 DOM、注入脚本、代按确认或访问组件状态。
|
|
18
34
|
|
|
19
35
|
入口先显现 AppHost 自己的侧栏市场覆盖层。若宿主已用 market 席位拥有市场表面(setSettingsVisible(false)),调用方需选中/呈现现有 market.render() 表面,AppHost 不叠加第二个覆盖层。即使尚未挂载,目标确认也保存在本客户端模块内,正常挂载后读回;不跨 profile 保存。
|
|
20
36
|
|
|
@@ -22,7 +38,7 @@ Promise 在目录解析后返回 `{status,itemId?,packageName?}`:
|
|
|
22
38
|
|
|
23
39
|
| status | 意义 |
|
|
24
40
|
| --- | --- |
|
|
25
|
-
| confirmation-required |
|
|
41
|
+
| confirmation-required | 市场已按当前目录显示名称、包名和目录来源;结构化 GitHub 目标另显示完整 SHA/子目录;尚未安装 |
|
|
26
42
|
| installing | 相同目录项已有安装进行中,复用该状态 |
|
|
27
43
|
| already-installed | 当前 profile 已安装且目录没有升级,不重复安装 |
|
|
28
44
|
| superseded | 解析过程中收到更新目标或取消,旧输入不再打开确认 |
|
|
@@ -31,9 +47,9 @@ Promise 在目录解析后返回 `{status,itemId?,packageName?}`:
|
|
|
31
47
|
|
|
32
48
|
## HTTP 契约
|
|
33
49
|
|
|
34
|
-
|
|
50
|
+
只读 `GET /hanamesh/library/target?itemId=...&packageName=...` 接受两个标识及可选 JSON 编码 installTarget。宿主用当前市场目录逐页解析,返回 `{item,source,traceId}`;item 含 kind、installed、upgradeAvailable、选定 installTarget 和原目录元数据,source 含市场自己的 manifestUrl;fixture 来源明确标「测试目录」。查询不会安装。
|
|
35
51
|
|
|
36
|
-
同实例同端口的精确 `127.0.0.1:<port>` 与 `localhost:<port>` Host 同时可用(同一 parentOrigin 配置,无切换)。Origin 若出现必须与本次请求 Host 的 http origin 精确一致;两个别名仍是不同的浏览器 origin,不允许跨别名请求。未知 Host、其他端口、IPv6/尾点别名、外来或 opaque Origin 明确拒绝;非 iframe、authenticate、authorize 检查保持。GET 允许同源浏览器默认不带 Origin;不接受外来 Origin。实际安装只用原 `POST /hanamesh/library/install`,要求精确同源 Origin、`X-HanaMesh-Client: workspace-v1`、资源端身份及授权;重新解析当前目录,不信任请求内版本或地址。进行中返回原 `202 {operationId,status:'started'}
|
|
52
|
+
同实例同端口的精确 `127.0.0.1:<port>` 与 `localhost:<port>` Host 同时可用(同一 parentOrigin 配置,无切换)。Origin 若出现必须与本次请求 Host 的 http origin 精确一致;两个别名仍是不同的浏览器 origin,不允许跨别名请求。未知 Host、其他端口、IPv6/尾点别名、外来或 opaque Origin 明确拒绝;非 iframe、authenticate、authorize 检查保持。GET 允许同源浏览器默认不带 Origin;不接受外来 Origin。实际安装只用原 `POST /hanamesh/library/install`,要求精确同源 Origin、`X-HanaMesh-Client: workspace-v1`、资源端身份及授权;重新解析当前目录,不信任请求内版本或地址。进行中返回原 `202 {operationId,status:'started'}`;相同包且相同结构化目标共享该 operationId,目标不同拒绝 CATALOG_CHANGED。已经安装且没有目录升级时返回 `200 {status:'already-installed',itemId,packageName}`。安装事件/结果与既有安装器不改。结构化 npm 来源只执行收录时固定的精确版本;GitHub 只执行已确认的 SHA。最终已安装版本来自 profile 扫描,GitHub 还须读回精确来源与启用项,均与操作结果一致才报告读回成功。
|
|
37
53
|
|
|
38
54
|
## 合约版本与一致性套件
|
|
39
55
|
|
package/docs/LIBRARY.md
CHANGED
|
@@ -43,7 +43,7 @@ rc.10:add 之后的读回、定义校验或 provision 失败按基线补偿(
|
|
|
43
43
|
## rc.28 · 市场浏览语义
|
|
44
44
|
|
|
45
45
|
- 市场页默认 `category=`(全部);筛选:全部 / 应用(`hanamesh-app`)/ 插件(客户端视图:本页非应用的 npm 条目)/ 目录页里出现过的类别(累积自 `categories`)。路由 `/hanamesh/library` 的缺省 `category=hanamesh-app` 不变(向后兼容)。
|
|
46
|
-
- 卡片:`kind · 发布者 ·
|
|
46
|
+
- 卡片:`kind · 发布者 · 类别`;已装行显示实际状态与版本;「升级」出现在 `upgradeAvailable` 时,npm 仅按 Indexer 固定 target.version 比较;确认页不显示目录版本;插件「卸载」走 `plugins/uninstall`。页面底部「已安装」列出应用与插件(含 bundle 未启用为 profile 层的提示)。
|
|
47
47
|
- 「需重新打开客户端」横幅:任一装卸操作完成后,或列表 `restartRequired` 为真时显示;所有渠道均提示完全退出 HanaMesh 客户端或 DSH 后重新打开,关闭窗口不等于退出。不提供旧壳深链或 postMessage 重启桥;只有正式宿主重启 API 供给后才另行接入。
|
|
48
48
|
|
|
49
49
|
### 0.3.0 · catalog major compatibility
|
package/docs/LICENSES.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": {
|
|
3
3
|
"name": "@hanamesh/dsh-app-host",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.21",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"source": "git+https://github.com/yzsnstotz/hanamesh-plugin-app-host.git",
|
|
7
7
|
"purpose": "Application ownership, view leases, Router forwarding, and application library for official DSH"
|
package/docs/SECURITY.md
CHANGED
|
@@ -8,6 +8,8 @@ D-214: verification is trusted producer/client-view configuration, never a Targe
|
|
|
8
8
|
|
|
9
9
|
# 安全与运行边界
|
|
10
10
|
|
|
11
|
+
0.6.21 GitHub 安装候选(R25 修复):仅消费 Indexer 的完整 SHA/包名/子目录/构建许可,且与 App-host 当前启用目录逐字段核对。条目存在 installMethods 时必须选中其中的固定目标;没有匹配目标即 NOT_INSTALLABLE,不从 npm 展示名回退解析 latest。HanaMesh 视图拒绝退休的 latestVersion/license,确认页和升级按钮不显示目录版本;npm 升级只比较固定目标 version 与已装实际版本。网站或客户端的 installTarget 是预期值,不是任意执行源;未知字段、浮动 ref、参数注入/越界目录拒绝,套件保护名单同样适用于 Git 包。既有确认回传完整目标,目录变更不得偷换 SHA 或放行额外脚本。官方 CLI 用 argv,不执行采集来源的 shell command;受控独立 Node、Git helper 与监测边界不变。脚本许可由 pnpm 管理并可能持久化,安装失败保留具体 build-permissions 残留,不手改策略、不冒充恢复。仍不是 OS 沙箱。
|
|
12
|
+
|
|
11
13
|
## 可信边界
|
|
12
14
|
|
|
13
15
|
注册描述符、DSH bridge 与宿主代码可信;浏览器请求、应用 HTTP 内容、旧租约和磁盘错误不可信。浏览器不能传命令、argv、env、cwd、dataDir 或任意上游 URL。实际 HTTP 拒绝和负向测试见 acceptance。
|
package/package.json
CHANGED
|
@@ -585,16 +585,6 @@
|
|
|
585
585
|
"homepage": {
|
|
586
586
|
"$ref": "#/$defs/hana_httpsUri"
|
|
587
587
|
},
|
|
588
|
-
"latestVersion": {
|
|
589
|
-
"type": "string",
|
|
590
|
-
"minLength": 1,
|
|
591
|
-
"maxLength": 64
|
|
592
|
-
},
|
|
593
|
-
"license": {
|
|
594
|
-
"type": "string",
|
|
595
|
-
"minLength": 1,
|
|
596
|
-
"maxLength": 80
|
|
597
|
-
},
|
|
598
588
|
"categories": {
|
|
599
589
|
"type": "array",
|
|
600
590
|
"maxItems": 32,
|
|
@@ -658,6 +648,32 @@
|
|
|
658
648
|
},
|
|
659
649
|
"suite": {
|
|
660
650
|
"$ref": "#/$defs/hana_suite"
|
|
651
|
+
},
|
|
652
|
+
"installMethods": {
|
|
653
|
+
"type": "array",
|
|
654
|
+
"items": {
|
|
655
|
+
"$ref": "#/$defs/hana_installMethod"
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
"oneClickInstall": {
|
|
659
|
+
"type": "object",
|
|
660
|
+
"additionalProperties": false,
|
|
661
|
+
"required": [
|
|
662
|
+
"allowed",
|
|
663
|
+
"reason"
|
|
664
|
+
],
|
|
665
|
+
"properties": {
|
|
666
|
+
"allowed": {
|
|
667
|
+
"type": "boolean"
|
|
668
|
+
},
|
|
669
|
+
"reason": {
|
|
670
|
+
"type": [
|
|
671
|
+
"string",
|
|
672
|
+
"null"
|
|
673
|
+
],
|
|
674
|
+
"maxLength": 160
|
|
675
|
+
}
|
|
676
|
+
}
|
|
661
677
|
}
|
|
662
678
|
},
|
|
663
679
|
"allOf": [
|
|
@@ -1231,6 +1247,77 @@
|
|
|
1231
1247
|
"$ref": "#/$defs/hana_page"
|
|
1232
1248
|
}
|
|
1233
1249
|
}
|
|
1250
|
+
},
|
|
1251
|
+
"hana_installMethod": {
|
|
1252
|
+
"type": "object",
|
|
1253
|
+
"additionalProperties": false,
|
|
1254
|
+
"required": [
|
|
1255
|
+
"channel",
|
|
1256
|
+
"kind",
|
|
1257
|
+
"spec",
|
|
1258
|
+
"command",
|
|
1259
|
+
"allowBuild",
|
|
1260
|
+
"sourceId",
|
|
1261
|
+
"sourceRef"
|
|
1262
|
+
],
|
|
1263
|
+
"properties": {
|
|
1264
|
+
"channel": {
|
|
1265
|
+
"enum": [
|
|
1266
|
+
"hanamesh",
|
|
1267
|
+
"dsh",
|
|
1268
|
+
"claude",
|
|
1269
|
+
"codex"
|
|
1270
|
+
]
|
|
1271
|
+
},
|
|
1272
|
+
"kind": {
|
|
1273
|
+
"enum": [
|
|
1274
|
+
"npm",
|
|
1275
|
+
"github",
|
|
1276
|
+
"agent"
|
|
1277
|
+
]
|
|
1278
|
+
},
|
|
1279
|
+
"spec": {
|
|
1280
|
+
"type": "string",
|
|
1281
|
+
"minLength": 1,
|
|
1282
|
+
"maxLength": 4096
|
|
1283
|
+
},
|
|
1284
|
+
"command": {
|
|
1285
|
+
"type": [
|
|
1286
|
+
"string",
|
|
1287
|
+
"null"
|
|
1288
|
+
],
|
|
1289
|
+
"maxLength": 4096
|
|
1290
|
+
},
|
|
1291
|
+
"allowBuild": {
|
|
1292
|
+
"type": "array",
|
|
1293
|
+
"items": {
|
|
1294
|
+
"type": "string",
|
|
1295
|
+
"minLength": 1,
|
|
1296
|
+
"maxLength": 2048
|
|
1297
|
+
}
|
|
1298
|
+
},
|
|
1299
|
+
"sourceId": {
|
|
1300
|
+
"type": "string",
|
|
1301
|
+
"minLength": 1,
|
|
1302
|
+
"maxLength": 120
|
|
1303
|
+
},
|
|
1304
|
+
"sourceRef": {
|
|
1305
|
+
"type": "string",
|
|
1306
|
+
"minLength": 1,
|
|
1307
|
+
"maxLength": 512
|
|
1308
|
+
},
|
|
1309
|
+
"npmOwnership": {
|
|
1310
|
+
"type": "object"
|
|
1311
|
+
},
|
|
1312
|
+
"target": {
|
|
1313
|
+
"$ref": "https://hanamesh.com/schemas/app-host/install-target.schema.json#/$defs/packageInstallTarget"
|
|
1314
|
+
},
|
|
1315
|
+
"targetUnavailableReason": {
|
|
1316
|
+
"type": "string",
|
|
1317
|
+
"minLength": 1,
|
|
1318
|
+
"maxLength": 160
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1234
1321
|
}
|
|
1235
1322
|
},
|
|
1236
1323
|
"allOf": [
|