@tbox.cn/app-toolkit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/chunk-VV3ERZXS.js +1 -0
- package/dist/contracts-resolver-QM4FBQQV.js +1 -0
- package/dist/index.d.ts +1909 -0
- package/dist/index.js +8 -0
- package/package.json +47 -0
- package/src/assembly/app-manifest.ts +132 -0
- package/src/assembly/provider-catalog.ts +167 -0
- package/src/assembly/walk-manifests.ts +461 -0
- package/src/core/contracts-expected.ts +31 -0
- package/src/core/contracts-resolver.ts +274 -0
- package/src/core/credential-format.ts +98 -0
- package/src/core/credential-mask.ts +19 -0
- package/src/core/env-expansion.ts +51 -0
- package/src/core/errors.ts +46 -0
- package/src/core/file-cache.ts +65 -0
- package/src/core/fskit.ts +21 -0
- package/src/core/module-schema.ts +139 -0
- package/src/dto.ts +298 -0
- package/src/factory.ts +297 -0
- package/src/index.ts +137 -0
- package/src/integrations/credentials.ts +153 -0
- package/src/integrations/mock-bindings.ts +55 -0
- package/src/integrations/predicate-io.ts +199 -0
- package/src/integrations/predicate.ts +598 -0
- package/src/integrations/read.ts +53 -0
- package/src/integrations/write.ts +448 -0
- package/src/views/app.ts +28 -0
- package/src/views/context.ts +70 -0
- package/src/views/modules.ts +48 -0
- package/src/views/providers.ts +165 -0
- package/src/views/service-detail.ts +32 -0
- package/src/views/service-resolutions.ts +323 -0
- package/tests/contracts-resolver.test.ts +203 -0
- package/tests/demo-app.ts +146 -0
- package/tests/dev-manifest-catalog.test.ts +114 -0
- package/tests/error-name-safety.test.ts +26 -0
- package/tests/file-cache.test.ts +242 -0
- package/tests/import-layers.test.ts +111 -0
- package/tests/module-schema.test.ts +84 -0
- package/tests/naming-alignment.test.ts +50 -0
- package/tests/views.test.ts +427 -0
- package/tests/write-core.test.ts +188 -0
- package/tsconfig.json +11 -0
- package/tsup.config.ts +29 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import{a as ce,b as M,c as le}from"./chunk-VV3ERZXS.js";import{z as y}from"zod";var F=/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/,kn=y.object({cardType:y.string(),schemaVersion:y.number().optional()}),Ve=y.object({route:y.string(),component:y.string()}),$e=y.object({route:y.string(),label:y.string().optional(),icon:y.string().optional(),order:y.number().optional()}),_e=y.object({id:y.string(),range:y.string().optional(),required:y.boolean().default(!0)}),Fe=y.object({service:y.string(),provider:y.string().regex(F,"provider 名含非法字符(需 /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/)"),implementation:y.string(),credentialType:y.string(),local:y.boolean().optional(),configSchema:y.string().optional(),credentialSchema:y.string().optional()}),Le=y.object({service:y.string(),optional:y.boolean().default(!0),defaults:y.array(y.object({provider:y.string().regex(F,"provider 名含非法字符(需 /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/)"),implementation:y.string()})).default([]),title:y.string().optional(),description:y.string().optional()}),We=y.object({id:y.string(),type:y.string()}),Be=y.object({schemaVersion:y.number().default(1),name:y.string(),version:y.string().default("0.0.0"),kind:y.enum(["platform","business","third-party"]).default("business"),risk:y.object({level:y.enum(["high","medium","low"]).default("low"),writeBoundary:y.enum(["sdk-enforced","source"]).default("source")}).default({}),distribution:y.object({defaultMode:y.enum(["hybrid","sdk","codegen","local"]).default("codegen")}).default({}),contributes:y.object({handlers:y.array(y.string()).default([]),tools:y.array(y.string()).default([]),cards:y.array(kn).default([]),routes:y.array(y.string()).default([]),pages:y.array(Ve).default([]),tabs:y.array($e).default([]),providers:y.object({slots:y.array(Fe).default([])}).default({}),services:y.array(Le).default([]),resources:y.array(We).default([])}).default({}),dependencies:y.object({modules:y.array(_e).default([])}).default({}),env:y.array(y.string()).default([])}).strict();function J(e){let n=Be.safeParse(e);return n.success?{ok:!0,value:n.data}:{ok:!1,errors:n.error.issues.map(r=>`${r.path.join(".")||"(root)"}: ${r.message}`)}}var bn=/\$\$?\{([A-Z][A-Z0-9_]*)\}/g;function Cn(e){return JSON.stringify(e).slice(1,-1)}function An(e,n=process.env){let r=[],t=e.replace(bn,(a,i)=>{if(a.startsWith("$$"))return a.slice(1);let o=n[i];return o===void 0?(r.push(i),a):Cn(o)});return r.length>0?{missing:r}:{text:t,missing:[]}}import{mkdirSync as xn,existsSync as ze,readFileSync as En,writeFileSync as Mn}from"fs";import{dirname as On,join as Ke}from"path";function q(e){let n={};for(let[r,t]of Object.entries(e)){if(typeof t!="string"){n[r]=null;continue}n[r]=t.length<=4?"****":`${t.slice(0,2)}****`}return n}function Q(e,n){if(n!==void 0&&(n.includes("/")||n.includes("\\")||n.includes("..")))throw new Error(`凭据 stem 派生输入含路径敌对字符(${e})——拒绝派生`)}function W(e){if(Q("provider",e.provider),Q("service",e.service),Q("instanceId",e.instanceId),Q("domain",e.domain),e.instanceId!==void 0){let n=e.domain??e.service??"global";return`${e.provider}-${e.instanceId}-${n}`}return e.service!==void 0?`${e.provider}-${e.service}`:`${e.provider}-global`}function jn(e){return`secret://${e}`}function _(e){return`config/credentials/${e}.json`}function Y(e,n){let r=Ke(e,_(n));if(!ze(r))return null;try{let t=JSON.parse(En(r,"utf8"));if(typeof t.type!="string")return null;let{type:a,...i}=t,o={};for(let[g,d]of Object.entries(i))typeof d=="string"&&(o[g]=d);return{type:a,values:o}}catch{return null}}function de(e,n,r){let t=Ke(e,_(n)),a=ze(t);return xn(On(t),{recursive:!0}),Mn(t,`${JSON.stringify({type:r.type,...r.values},null,2)}
|
|
2
|
+
`,"utf8"),a}import{mkdirSync as Tn,renameSync as Nn,writeFileSync as Pn}from"fs";import{dirname as Dn}from"path";function $(e,n){Ue(Dn(e));let r=`${e}.tmp-${process.pid}-${Date.now()}`;Pn(r,n,"utf8"),Nn(r,e)}function Ue(e){Tn(e,{recursive:!0})}function ue(e){if(!e||typeof e!="object")return!1;let n=Object.keys(e),r=new Set(["provider","credentialType","configSchema"]);if(!n.every(a=>r.has(a)))return!1;let t=e;return typeof t.provider=="string"&&t.provider.length>0&&typeof t.credentialType=="string"&&t.credentialType.length>0&&(t.configSchema===void 0||typeof t.configSchema=="string")}function Je(e,n,r){return e==="*"||r.has(e)?null:{severity:"error",code:"INSTANCE_KEY_NOT_REGISTERED",instance:e,message:`integrations: ${n} 键 ${e} 不在 malls 白名单内(${[...r].join(", ")})`}}function pe(e,n){let r=new Set(n),t=[],a=(i,o)=>{let g=Je(i,o,r);g&&t.push(g)};for(let i of Object.keys(e.configByInstance??{}))a(i,"configByInstance");for(let i of Object.keys(e.credentialRefByInstance??{}))a(i,"credentialRefByInstance");for(let[i,o]of Object.entries(e.services??{}))for(let g of Object.keys(o.instances??{}))a(g,`services.${i}.instances`);return t}function X(e){let n=[],r=e.contracts,{integrations:t,suppliedProviders:a,demandedServices:i,supplies:o,malls:g,credentialSchemaMissing:d,vendorConfigKeys:p,vendorConfigSchemaMissing:m,credentialFileMissing:v,credentialFileType:f,realmSwitchHints:u,moduleResourceNeeds:k}=e,s=g?new Set(g):void 0;for(let c of u??[])n.push({severity:"warning",code:"REALM_SWITCH_HINT",message:c});let l=new Set(Object.keys(a).map(c=>c.slice(0,c.indexOf("\0"))).filter(c=>c.length>0)),h=(c,S,b,I)=>{if(typeof b?.provider=="string"&&typeof b.implementation=="string"&&b.provider&&b.implementation)return a[Z(b.provider,b.implementation)]?.credentialType;if(typeof S.provider=="string"&&typeof S.implementation=="string")return a[Z(S.provider,S.implementation)]?.credentialType;let A=S.provider?.credentialType;if(typeof A=="string")return A;if(!o)return;let x=I??Object.keys(S.instances??{})[0]??"",E=r.resolveEffectiveService(t,c,x,o);if(!(E.status!=="ok"||typeof E.effective.provider!="string"))return a[Z(E.effective.provider,E.effective.implementation)]?.credentialType};if(v||f)for(let[c,S]of Object.entries(t.services))for(let[b,I]of Object.entries(S.instances??{})){if(I.enabled===!1)continue;let A=I.credentialRef??S.credentialRef??t.credentialRefByInstance?.[b]??t.credentialRef;if(typeof A!="string"||!A.startsWith("secret://"))continue;let x=A.slice(9);v?.[x]===!0&&n.push({severity:"error",code:"CREDENTIAL_FILE_MISSING",service:c,instance:b,message:`integrations: 凭据文件缺失 ${x}(${c}@${b} effective ref=${A};档内 config/credentials/ 无此文件)`});let E=f?.[x];if(E){let N=h(c,S,I,b);N&&E!==N&&n.push({severity:"error",code:"CREDENTIAL_TYPE_MISMATCH",service:c,instance:b,message:`integrations: 凭据文件 ${x}.json type=${E} 与绑定 credentialType ${N} 不一致(${c}@${b}——transport 创建期将按 type 选策略)`})}}let C=(c,S)=>{if(!s)return;let b=Je(c,S,s);b&&n.push(b)};for(let[c]of Object.entries(t.configByInstance??{}))C(c,"configByInstance");for(let[c]of Object.entries(t.credentialRefByInstance??{}))C(c,"credentialRefByInstance");let j=Object.entries(t.services).filter(([,c])=>c.instances==null||c.instances["*"]!=null).map(([c])=>c);if(j.length>0){let c=Object.entries(t.services).filter(([,S])=>S.instances!=null&&S.instances["*"]==null).map(([S])=>S);c.length>0&&n.push({severity:"warning",code:"WILDCARD_ALIGNMENT",message:`integrations: 激活形态不对称(通配:${j.join(", ")};显式键集:${c.join(", ")})——动态 mallId 将在显式键集槽上 INSTANCE_NOT_FOUND(provider 业务限制表达位),确认是否齐步`})}let R=r.normalizeInstances({instances:t.instances,defaultInstanceId:t.defaultInstanceId});R.instances.length>=2&&R.defaultInstanceId===null&&n.push({severity:"warning",code:"MULTI_INSTANCE_NO_DEFAULT",message:`integrations: ${R.instances.length} 个部署实例(${R.instances.map(c=>c.id).join(", ")})均无 defaultInstanceId——无 scope 参数访问将落全局会话(默认商圈不注入)`});for(let[c,S]of Object.entries(t.services)){if(i[c]||n.push({severity:"warning",code:"SLOT_NOT_DECLARED",service:c,message:`integrations: 服务槽 ${c} 无消费方声明(拼写错误或需求方未安装)——若为拆分前旧槽位词汇(member.main/parking.main/promotion.read/shopping-guide.main),参照 cookbook·app-config 槽位迁移配方展开为新槽`}),typeof S.provider=="string"?typeof S.implementation=="string"&&!a[Z(S.provider,S.implementation)]&&n.push({severity:"error",code:"INSTANCE_SUPPLY_UNMATCHED",service:c,message:`integrations: provider ${S.provider}/${S.implementation} 未命中已装供给(${c} 槽级)——装对应 provider 包或改绑`}):S.provider!==void 0&&!ue(S.provider)&&n.push({severity:"error",code:"INLINE_PROVIDER_INVALID",service:c,message:`integrations: 内联 provider 超受控形状(${c} 槽级;允许键 provider/credentialType/configSchema)`}),d){let I=h(c,S);I&&d[I]===!0&&n.push({severity:"error",code:"SCHEMA_FILE_MISSING",service:c,message:`integrations: credentialType ${I} 的 schema 引用不存在(${c};manifest 声明指向缺失文件)`})}typeof S.provider=="object"&&typeof S.implementation!="string"&&n.push({severity:"error",code:"IMPL_WITHOUT_PROVIDER",service:c,message:`integrations: 内联厂商声明必须同时提供 implementation(${c} 槽级)`});for(let[I,A]of Object.entries(S.instances??{})){C(I,`services.${c}.instances`);let x=A.provider!==void 0,E=A.implementation!==void 0;if(E&&!x&&n.push({severity:"error",code:"IMPL_WITHOUT_PROVIDER",service:c,instance:I,message:`integrations: 实例级 implementation 需伴随 provider(${c}@${I}——impl-only 禁止,provider 是绑定组头)`}),x&&E){let N=A.provider,V=A.implementation;typeof N=="string"&&typeof V=="string"?a[Z(N,V)]||n.push({severity:"error",code:"INSTANCE_SUPPLY_UNMATCHED",service:c,instance:I,message:`integrations: 实例级 provider 覆盖 ${N}/${V} 未命中已装供给(${c}@${I})`}):ue(N)||n.push({severity:"error",code:"INLINE_PROVIDER_INVALID",service:c,instance:I,message:`integrations: 实例级内联 provider 超受控形状(${c}@${I})`})}}}for(let[c,S]of[["root",t.provider],...Object.entries(t.domains??{}).map(([b,I])=>[`domains.${b}`,I?.provider])])typeof S!="string"||S.length===0||l.has(S)||n.push({severity:"error",code:"VENDOR_NOT_INSTALLED",module:c.startsWith("domains.")?c.slice(8):void 0,message:`integrations: ${c} provider ${S} 未命中已装供给——装对应 provider 包或 provider use 改绑`});if(o)for(let[c,S]of Object.entries(t.services)){let b=S.instances?Object.keys(S.instances):[""];for(let I of b){let A=r.resolveEffectiveService(t,c,I,o);A.status==="binding-unresolved"&&n.push({severity:"error",code:"BINDING_UNRESOLVED",service:c,instance:I||void 0,message:`integrations: ${A.message}`})}}for(let[c,S]of Object.entries(t.services))S.implementation!==void 0&&S.provider===void 0&&n.push({severity:"error",code:"IMPL_WITHOUT_PROVIDER",service:c,message:`integrations: 槽级 implementation 需伴随 provider(${c}——impl-only 禁止,provider 是绑定组头)`});let w=new Set([...Object.keys(t.services).map(c=>r.moduleDomainOf(c)),...Object.keys(i).map(c=>r.moduleDomainOf(c))]);for(let c of Object.keys(t.domains??{}))w.has(c)||n.push({severity:"warning",code:"DOMAIN_KEY_UNMATCHED",module:c,message:`integrations: domains.${c} 无对应槽位或需求声明(域前缀拼写错误?——键 = 槽位 id 点分前缀)`});if(p){let c=new Set,S=!1,b=I=>{typeof I!="string"||I.length===0||(p[I]&&c.add(I),!p[I]&&!m?.[I]&&(S=!0),m?.[I]&&(S=!0))};b(t.provider);for(let I of Object.values(t.domains??{}))b(I?.provider);for(let I of Object.values(t.services)){let A=I.provider;b(typeof I.provider=="string"?I.provider:A?.provider)}if(c.size>0&&!S){let I=new Set;for(let x of c)for(let E of p[x]??[])I.add(E);let A=(x,E,N)=>{for(let[V]of Object.entries(x??{}))(N??I).has(V)||n.push({severity:"error",code:"DOMAIN_CONFIG_INVALID",message:`integrations: ${E} 键 ${V} 不在引用厂商(${[...c].join("/")})任何槽 schema properties 内(死配置)`})};A(t.config,"root.config");for(let[x,E]of Object.entries(t.configByInstance??{}))A(E,`root.configByInstance[${x}]`);for(let[x,E]of Object.entries(t.domains??{})){if(!E?.config)continue;let N=typeof E.provider=="string"&&E.provider?E.provider:typeof t.provider=="string"?t.provider:void 0;if(N===void 0){A(E.config,`domains.${x}.config`);continue}let V=p[N];!V||m?.[N]||A(E.config,`domains.${x}.config`,new Set(V))}}}for(let[c,S]of Object.entries(i)){let b=t.services[c];if(!b){n.push({severity:"warning",code:"SLOT_NOT_CONFIGURED",service:c,message:`integrations: 服务槽 ${c}(需求方 ${S.owners.join(", ")})未配置${S.optional?"(可选槽)":"(必需槽——模块将走降级链)"}`});continue}let I=Object.entries(b.instances??{}).filter(([,A])=>A.enabled===!1).map(([A])=>A);I.length>0&&n.push({severity:"warning",code:"INSTANCE_DISABLED",service:c,instance:I.join(", "),message:`integrations: ${c} 实例禁用(${I.join(", ")})`})}for(let[c,S]of Object.entries(t.modules??{})){let b=k?.[c];k&&!(c in k)&&n.push({severity:"warning",code:"RESOURCE_NOT_DECLARED",module:c,message:`integrations: modules.${c} 未命中已装模块(键 = install id——检查 .tbox/app.json entry.id / packages/ 目录名)`});for(let[I,A]of Object.entries(S?.resources??{})){let x=A?.type;(typeof x!="string"||!r.RESOURCE_TYPE_IDS.includes(x))&&n.push({severity:"error",code:"RESOURCE_TYPE_UNKNOWN",module:c,message:`integrations: modules.${c}.resources.${I} type 非法(${String(x)};词汇表 = ${r.RESOURCE_TYPE_IDS.join("/")})`}),b&&b.size>0&&!b.has(I)&&n.push({severity:"warning",code:"RESOURCE_NOT_DECLARED",module:c,message:`integrations: modules.${c}.resources.${I} 未被模块 manifest contributes.resources 声明`})}}return n}function Z(e,n){return`${e}\0${n}`}import{existsSync as fe,readFileSync as Vn,readdirSync as $n}from"fs";import{join as ee}from"path";function Ze(e){return e.replaceAll("-","_").toUpperCase()}function Ye(e){if(!fe(e))return null;try{let n=JSON.parse(Vn(e,"utf8"));return n&&typeof n=="object"?n:null}catch{return null}}function _n(e){let n=new Set,r=e.properties;if(r&&typeof r=="object"&&!Array.isArray(r))for(let t of Object.keys(r))n.add(t);return n}function ge(e,n,r,t=process.env){let a={};for(let[R,w]of Object.entries(n.catalog.providers))for(let[c,S]of Object.entries(w))a[`${R}\0${c}`]={services:[...S.services],owner:S.owner,credentialType:S.credentialType};let i={};for(let[R,w]of Object.entries(n.catalog.services))i[R]={optional:w.optional,owners:[...w.owners]};let o={},g={},d={},p={};for(let R of n.modules){p[R.id]=new Set((R.descriptor.contributes.resources??[]).map(w=>w.id));for(let w of R.descriptor.contributes.providers?.slots??[]){if(w.configSchema){let c=Ye(ee(R.dir,w.configSchema));if(c){let S=o[w.provider]??=new Set;for(let b of _n(c))S.add(b)}else g[w.provider]=!0}w.credentialSchema&&!fe(ee(R.dir,w.credentialSchema))&&(d[w.credentialType]=!0)}}let m={},v=new Set,f=ee(e,"config","credentials");if(fe(f))for(let R of $n(f)){if(!R.endsWith(".json"))continue;let w=R.slice(0,-5);v.add(w);let c=Ye(ee(f,R));c&&typeof c.type=="string"&&(m[w]=c.type)}let u=r,k=[],s=R=>{typeof R=="string"&&R.startsWith("secret://")&&k.push(R)};for(let R of Object.values(u.services??{}))if(R&&typeof R=="object"){s(R.credentialRef);for(let w of Object.values(R.instances??{}))w&&typeof w=="object"&&s(w.credentialRef)}for(let R of Object.values(u.credentialRefByInstance??{}))s(R);s(u.credentialRef);let l={};for(let R of k){let w=R.slice(9);t[Ze(w)]===void 0&&(v.has(w)||(l[w]=!0))}let h=[];for(let[R,w]of Object.entries(u.services??{})){let c=w&&typeof w=="object"&&typeof w.provider=="string"?w.provider:void 0;for(let[S,b]of Object.entries((w&&typeof w=="object"?w.instances:void 0)??{})){let I=b&&typeof b=="object"&&typeof b.provider=="string"?b.provider:void 0;c&&I&&I!==c&&h.push(`integrations: ${R}@${S} 实现覆盖跨厂商(${c} → ${I})——凭据域切换,确认该 mall 对应凭据在场`)}}let j=((Array.isArray(u.instances)?u.instances:void 0)??[]).map(R=>R&&typeof R=="object"&&typeof R.id=="string"?R.id:void 0).filter(R=>typeof R=="string");return{suppliedProviders:a,demandedServices:i,credentialFileMissing:l,credentialFileType:m,credentialSchemaMissing:d,vendorConfigKeys:o,vendorConfigSchemaMissing:g,realmSwitchHints:h,moduleResourceNeeds:p,...j.length>0?{malls:j}:{}}}import{existsSync as D,readdirSync as ve,readFileSync as B}from"fs";import{join as T}from"path";import{existsSync as Fn,readFileSync as He,mkdirSync as Ln,writeFileSync as Wn}from"fs";import Ge from"path";function ne(e){return Ge.join(e,".tbox","app.json")}function qe(e){let n=ne(e);if(!Fn(n))return null;try{let r=JSON.parse(He(n,"utf8")),t=r?.templateVersion;return typeof t!="string"||t.trim().length===0?null:{templateVersion:t.trim(),npmModules:Array.isArray(r?.npmModules)?r.npmModules:[]}}catch{return null}}function Bn(e,n){let r=ne(e);Ln(Ge.dirname(r),{recursive:!0}),Wn(r,`${JSON.stringify(n,null,2)}
|
|
3
|
+
`,"utf8")}function me(e){let n=qe(e);if(n===null)return null;let r=ne(e);try{let t=JSON.parse(He(r,"utf8"));return{...n,miniappContainers:Array.isArray(t?.miniappContainers)?t.miniappContainers:[]}}catch{return{...n,miniappContainers:[]}}}function zn(e,n){let r=e.npmModules.filter(t=>t.id!==n.id);return{...e,npmModules:[...r,n]}}function Kn(e,n){return{...e,npmModules:e.npmModules.filter(r=>r.id!==n)}}function Un(e,n){let r=(e.miniappContainers??[]).filter(t=>t.id!==n.id);return{...e,miniappContainers:[...r,n]}}function Jn(e,n,r){return{...e,npmModules:e.npmModules.map(t=>t.id===n?{...t,...r}:t)}}function Qe(e){return/^[a-z0-9]+(-[a-z0-9]+)*$/.test(e)&&!e.includes("..")}function Yn(e,n){if(!Qe(e))throw new Error(`非法模块 id "${e}"(来自 ${n}):仅允许小写字母/数字/中划线`)}function te(e,n){let r=t=>{try{let a=JSON.parse(t),i=J(a);if(i.ok)return{kind:"strict",name:i.value.name,version:i.value.version,descriptor:i.value,contributes:i.value.contributes};if(a&&typeof a=="object"&&"contributes"in a){let o=a;return{kind:"loose",...typeof o.name=="string"?{name:o.name}:{},contributes:{providers:o.contributes?.providers,services:o.contributes?.services}}}return null}catch{return null}};if(n)return n.read(e,r)??null;if(!D(e))return null;try{return r(B(e,"utf8"))}catch{return null}}function Zn(e){let n=T(e,".tbox","app.json");if(!D(n))return[];try{let r=JSON.parse(B(n,"utf8"));return Array.isArray(r.npmModules)?r.npmModules.filter(t=>!!t&&typeof t=="object"&&typeof t.id=="string"&&typeof t.package=="string"):[]}catch{return[]}}function Hn(e){let n=T(e,".tbox-dev.local"),r=T(n,"dev-manifest.json");if(!D(r))return[];try{let t=JSON.parse(B(r,"utf8")),a=[];for(let[i,o]of Object.entries(t.redirects??{}))typeof o!="string"||!i.startsWith("@tbox.cn/app-")||a.push({pkg:i,dir:T(n,o)});return a}catch{return[]}}function Gn(e,n){let r=[],t=[e];for(let i of["apps","packages"]){let o=T(e,i);if(D(o))try{for(let g of ve(o,{withFileTypes:!0}))g.isDirectory()&&t.push(T(o,g.name))}catch{}}let a=new Set;for(let i of t){let o=T(i,"node_modules","@tbox.cn");if(!D(o))continue;let g=[];try{g=ve(o)}catch{continue}for(let d of g){let p=T(o,d);if(a.has(p))continue;a.add(p);let m=T(p,"service-slots.json");if(!D(m))continue;let v=u=>{try{let k=JSON.parse(u);if(Array.isArray(k.slots))return k.slots.filter(s=>typeof s=="string")}catch{}return null},f=n?n.read(m,v):v(B(m,"utf8"));f!=null&&r.push({dir:p,slots:f})}}return r}function P(e,n){let r=[],t={providers:{},services:{},credentialTypes:{}},a={},i=[],o=new Set,g=new Map,d=(f,u,k)=>{for(let s of f){let l=t.providers[s.provider]??={},h=l[s.implementation];h?h.services.includes(s.service)||h.services.push(s.service):(l[s.implementation]={provider:s.provider,implementation:s.implementation,credentialType:s.credentialType,owner:u,...s.local?{local:!0}:{},...s.configSchema?{configSchema:s.configSchema}:{},...s.credentialSchema?{credentialSchema:s.credentialSchema}:{},services:[s.service]},t.credentialTypes[s.credentialType]??={owner:u,...s.credentialSchema?{credentialSchema:s.credentialSchema}:{}});let C=`${s.provider}\0${s.service}`;a[C]||(a[C]={...s.configSchema?{configSchema:s.configSchema}:{},...s.credentialSchema?{credentialSchema:s.credentialSchema}:{},packageDir:k})}},p=(f,u)=>{let k=f.optional!==!1,s=f.defaults??[],l=t.services[f.service];if(l){l.optional=l.optional&&k;let j=new Set(l.defaults.map(R=>`${R.provider}\0${R.implementation}`));for(let R of s){let w=`${R.provider}\0${R.implementation}`;j.has(w)||(l.defaults.push(R),j.add(w))}l.owners.includes(u)||l.owners.push(u);return}t.services[f.service]={optional:k,defaults:[...s],...f.title?{title:f.title}:{},...f.description?{description:f.description}:{},owners:[u]};let h=f.service.includes(".")?f.service.slice(0,f.service.indexOf(".")):f.service,C=g.get(h);C===void 0?g.set(h,u):C!==u&&i.push({severity:"warning",code:"DOMAIN_KEY_CONFLICT",module:u,message:`域键 ${h} 被多模块声明(${C} 与 ${u})——domains 换算歧义(P13)`})};for(let f of Zn(e)){if(o.has(f.id))continue;let u=T(e,"packages",f.id),k=T(e,"node_modules",...f.package.split("/")),s=te(T(u,"tbox.module.json"),n)??te(T(k,"tbox.module.json"),n);if(!s)continue;o.add(f.id);let l=D(T(u,"tbox.module.json"))?u:k,h=s.contributes;s.kind==="strict"&&r.push({id:f.id,dir:l,pkg:f.mode==="sdk"?f.package:`@app/${f.id}`,descriptor:s.descriptor});let C=h.providers?.slots??[];C.length&&d(C,f.package,l);let j=h.services??[];if(j.length)for(let R of j)p(R,f.package)}for(let{pkg:f,dir:u}of Hn(e)){let k=te(T(u,"tbox.module.json"),n);if(!k?.name||o.has(k.name))continue;o.add(k.name);let s=k.contributes.providers?.slots??[];s.length&&d(s,f,u);let l=k.contributes.services??[];if(l.length)for(let h of l)p(h,f)}let m=T(e,"packages");if(D(m)){let f=[];try{f=ve(m,{withFileTypes:!0}).filter(u=>u.isDirectory()).map(u=>u.name)}catch{}for(let u of f){if(o.has(u))continue;let k=T(m,u),s=te(T(k,"tbox.module.json"),n);if(!s)continue;let l=s.name??u;if(o.has(l))continue;o.add(l);let h=s.contributes;s.kind==="strict"&&r.push({id:l,dir:k,pkg:`@app/${l}`,descriptor:s.descriptor});let C=h.providers?.slots??[];C.length&&d(C,`@app/${l}`,k);let j=h.services??[];if(j.length)for(let R of j)p(R,`@app/${l}`)}}let v=new Set;for(let f of Gn(e,n))for(let u of f.slots)v.add(u);return{modules:r,catalog:t,serviceSchemas:a,vocabulary:[...v].sort(),domainKeyIssues:i}}function Xe(e,n,r){let t=T(e,"src",n,"index.ts");if(!D(t))return!1;try{let a=B(t,"utf8");return new RegExp(`export\\s+const\\s+${r}\\b`).test(a)}catch{return!1}}function qn(e){let n=T(e,"src","server","index.ts");if(!D(n))return"";try{return B(n,"utf8")}catch{return""}}function en(e){return e.match(/declaration\s*:\s*\{[\s\S]*?moduleId\s*:\s*['"]([^'"]+)['"]/)?.[1]}function Qn(e){let n=[],r=P(e),t=me(e),a=new Map;for(let i of t?.npmModules??[])a.set(i.id,i.mode);for(let i of r.modules){let o=a.get(i.id)??"local",g=Xe(i.dir,"server","serverModule"),d=i.descriptor.contributes;n.push({id:i.id,pkg:o==="sdk"?i.pkg:`@app/${i.id}`,mode:o,hasServerEntry:g,hasClientEntry:Xe(i.dir,"client","clientModule"),hasCards:(d.cards?.length??0)>0,...g?{moduleId:en(qn(i.dir))}:{}})}return n.sort((i,o)=>i.id.localeCompare(o.id))}import{existsSync as Jt,readFileSync as Yt}from"fs";import{join as Ht,resolve as Gt}from"path";function ye(e){return P(e).catalog}function nn(e){let n=P(e).catalog,r={};for(let[i,o]of Object.entries(n.providers)){r[i]={};for(let[g,d]of Object.entries(o))r[i][g]={provider:d.provider,implementation:d.implementation,credentialType:d.credentialType,owner:d.owner,...d.local?{local:!0}:{},...d.configSchema?{configSchema:d.configSchema}:{},...d.credentialSchema?{credentialSchema:d.credentialSchema}:{},services:[...d.services]}}let t={};for(let[i,o]of Object.entries(n.services))t[i]={optional:o.optional,defaults:o.defaults.map(g=>({...g})),owners:[...o.owners]};let a={};for(let[i,o]of Object.entries(n.credentialTypes))a[i]={...o};return{providers:r,services:t,credentialTypes:a}}import{existsSync as Xn,readFileSync as et}from"fs";import{join as tn}from"path";var nt=tn("config","integrations.json");function z(e,n){let r=tn(e,nt),t=a=>JSON.parse(a);if(n)return n.read(r,t)??null;if(!Xn(r))return null;try{return t(et(r,"utf8"))}catch{return null}}async function rn(e,n){let r=await M(e),t=z(e,n);return t===null?{config:null,strict:!1,contracts:r}:r.strictValidation&&r.module.parseIntegrationsConfig?{config:r.module.parseIntegrationsConfig(t),strict:!0,contracts:r}:{config:t,strict:!1,contracts:r}}import{existsSync as ln,readFileSync as dn}from"fs";import{join as K,resolve as ot}from"path";import st from"ajv";var H=["provider","config","configByInstance","credentialRef","credentialRefByInstance","instances","defaultInstanceId"];import{readdirSync as tt,existsSync as re,readFileSync as rt}from"fs";import{join as ie}from"path";function on(e,n,r){let t=[];if(n){let a=W({provider:e});t.push({stem:a,ref:`secret://${a}`,input:n})}for(let[a,i]of Object.entries(r??{})){let o=W({provider:e,instanceId:a});t.push({instanceId:a,stem:o,ref:`secret://${o}`,input:i})}return t}function sn(e,n,r,t,a){let i=[];if(t){let o=W({provider:e,service:n});i.push({stem:o,ref:`secret://${o}`,input:t})}for(let[o,g]of Object.entries(a??{})){let d=W({provider:e,instanceId:o,domain:r});i.push({instanceId:o,stem:d,ref:`secret://${d}`,input:g})}return i}function Re(e,n,r){return n.map(t=>({stem:t.stem,ref:t.ref,overwrote:r?re(ie(e,_(t.stem))):de(e,t.stem,t.input)}))}function it(e,n){return re(ie(e,_(n)))}function Se(e,n){if(typeof e=="string"){e.startsWith("secret://")&&n.add(e.slice(9));return}if(Array.isArray(e)){for(let r of e)Se(r,n);return}if(e&&typeof e=="object")for(let r of Object.values(e))Se(r,n)}function we(e){let n=ie(e,"config","credentials"),r=new Set,t=ie(e,"config","integrations.json");if(re(t))try{Se(JSON.parse(rt(t,"utf8")),r)}catch{}let a=[];if(!re(n))return a;let i=[];try{i=tt(n).filter(o=>o.endsWith(".json")).map(o=>o.slice(0,-5))}catch{return a}for(let o of i.sort()){if(r.has(o))continue;let g=Y(e,o);a.push({severity:"warning",code:"ORPHANED_CREDENTIAL",message:`config/credentials/${o}.json 不再被引用(凭据文件保留,可手动清理)`})}return a}var O=class extends Error{code;httpStatus;fields;constructor(n,r,t,a){super(t),this.name="AppToolkitError",this.code=n,this.httpStatus=r,this.fields=a}};function Ie(e){return e.version===null?"contracts 不可解析(未安装)——检查应用依赖安装后重试":e.strictValidation?e.notes[0]??"contracts workspace 源码态未构建——执行 pnpm --filter @tbox.cn/app-contracts build 后重试":`contracts ${e.version} 无严格校验面(需 ≥0.9)——升级应用 @tbox.cn/app-contracts 依赖后可用`}var L=K("config","integrations.json");function U(e,n){if(e===n)return!0;if(Array.isArray(e)&&Array.isArray(n))return e.length!==n.length?!1:e.every((r,t)=>U(r,n[t]));if(e&&n&&typeof e=="object"&&typeof n=="object"){let r=Object.keys(e),t=Object.keys(n);return r.length!==t.length?!1:r.every(a=>U(e[a],n[a]))}return!1}function ae(e,n){if(typeof e!="string"&&e&&typeof e=="object")for(let[r,t]of Object.entries(e)){if((r==="provider"||r==="instanceId")&&typeof t=="string"&&!F.test(t))throw new O("VALIDATION_FAILED",400,`${r} 名含非法字符(需 /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/)`,[`${n}.${r}`]);ae(t,`${n}.${r}`)}}function oe(e,n,r){if(!(!e||typeof e!="object"||Array.isArray(e))){for(let t of Object.keys(e))if(!(r&&(t==="*"||t==="**"))&&!F.test(t))throw new O("VALIDATION_FAILED",400,`${n} 键 ${t} 含非法字符(需 /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/)——进凭据 stem 派生,防路径逃逸`,[n])}}function an(e,n,r){if(r==="root"){let t=e.instances;Array.isArray(t)&&t.forEach((a,i)=>{let o=a&&typeof a=="object"?a.id:void 0;if(typeof o=="string"&&!F.test(o))throw new O("VALIDATION_FAILED",400,`instances[${i}].id 含非法字符(需 /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/)`,[`root.instances[${i}].id`])}),oe(e.configByInstance,"root.configByInstance",!1),oe(e.credentialRefByInstance,"root.credentialRefByInstance",!1)}else oe(e.instances,"services.instances",!0);oe(n.credentialsByInstance,"credentialsByInstance",!1)}function se(e,n){let r=K(e,L),t=a=>JSON.parse(a);if(n)return n.read(r,t)??null;if(!ln(r))return null;try{return t(dn(r,"utf8"))}catch{return null}}function he(e){let n=(e.instances??[]).map(r=>r.id);return n.length===0?[]:pe(e,n)}function at(e,n,r,t){let{catalog:a}=P(e,t),i=a.providers[n];if(!i)return;let o=new Set;for(let g of Object.values(i))(r===void 0||g.services.includes(r))&&o.add(g.credentialType);return o.size===1?[...o][0]:void 0}function cn(e,n,r,t,a,i){let o=a??at(e,n,r,i);if(o!==void 0&&o!=="-"&&t.type!==o)throw new O("VALIDATION_FAILED",400,`凭据 type=${t.type} 与绑定 credentialType ${o} 不一致`,["credentials.type"]);if(r===void 0)return;let{serviceSchemas:g}=P(e,i),d=g[`${n}\0${r}`];if(!d?.credentialSchema)return;let p=ot(d.packageDir,d.credentialSchema);if(ln(p))try{let v=new st({allErrors:!0}).compile(JSON.parse(dn(p,"utf8")));if(!v({type:t.type,...t.values})){let f=(v.errors??[]).map(u=>`credentials.values.${String(u.instancePath||u.params?.missingProperty||"").replace(/^\//,"")}`);throw new O("VALIDATION_FAILED",400,"凭据字段校验失败(ajv 字段级)",f)}}catch(m){if(m instanceof O)throw m}}function ke(e,n={}){let{onApplied:r,cache:t}=n;function a(d,p,m,v,f){let u=!v&&d!==null;u&&($(K(e,L),`${JSON.stringify(d,null,2)}
|
|
4
|
+
`),t?.invalidate([K(e,L)]));let k=u||f,s=k&&r!==void 0;s&&r?.();let l=[];return u&&l.push(L),f&&l.push(...p.map(h=>`config/credentials/${h.stem}.json`)),{written:k,restartScheduled:s,files:l,credentials:p,issues:m}}let i;async function o(){i=await M(e)}function g(d,p){let m=i;if(!m||!m.strictValidation||!m.module.parseIntegrationsConfig)throw new O("CONTRACTS_NOT_RESOLVED",503,"contracts 严格校验面不可用(需 ≥0.9)——写路径门控失败(工厂边缘断言应先行拦截)");m.module.parseIntegrationsConfig(d)}return{warmup:o,moduleDomainKey(d){let{modules:p}=P(e,t),m=p.find(u=>u.id===d);if(!m)return;let v=m.descriptor.contributes.services??[],f=new Set(v.map(u=>u.service.includes(".")?u.service.slice(0,u.service.indexOf(".")):u.service));return f.size===1?[...f][0]:d},applyApp(d,p={}){ae(d,"root"),an(d,p,"root");let m=[],v=se(e,t),f=v??{services:{}};v===null&&m.push({severity:"warning",code:"SKELETON_MATERIALIZED",message:"config/integrations.json 缺席——已物化骨架(写时自动初始化;R13 语义翻转提示)"});let u={...f};for(let w of H)w in d||delete u[w];for(let[w,c]of Object.entries(d))u[w]=c;let k=typeof u.provider=="string"?u.provider:"app",s=on(k,p.credentials,p.credentialsByInstance);for(let w of s)cn(e,k,void 0,w.input,void 0,t);let l=s.find(w=>w.instanceId===void 0);if(p.credentials&&l&&(u.credentialRef=l.ref),p.credentialsByInstance){let w={...u.credentialRefByInstance??{}};for(let c of s)c.instanceId!==void 0&&(w[c.instanceId]=c.ref);u.credentialRefByInstance=w}let h=p.credentials!==void 0||p.credentialsByInstance!==void 0,C=U(v,u);if(C&&!h)return{written:!1,restartScheduled:!1,files:[],credentials:[],issues:m};g(u,m);let j=p.dryRun===!0,R=Re(e,s,j);return m.push(...he(u)),a(C?null:u,R,m,p.dryRun,!j&&s.length>0)},applyModule(d,p,m={}){ae(p,"root");let v=[],f=se(e,t);if(f===null&&Object.keys(p).length===0)return{written:!1,restartScheduled:!1,files:[],credentials:[],issues:v};f===null&&v.push({severity:"warning",code:"SKELETON_MATERIALIZED",message:"config/integrations.json 缺席——已物化骨架(写时自动初始化;R13 语义翻转提示)"});let u=f??{services:{}},k=this.moduleDomainKey(d);if(k===void 0)throw new O("MODULE_NOT_FOUND",404,`模块 ${d} 非已装模块`);let s={...u.domains??{}},l={...u,domains:s};return Object.keys(p).length===0?delete s[k]:s[k]={...p},U(f,l)?{written:!1,restartScheduled:!1,files:[],credentials:[],issues:v}:(g(l,v),v.push(...he(l)),a(l,[],v,m.dryRun,!1))},applyService(d,p,m={}){ae(p,"root"),an(p,m,"slot");let v=[],f=se(e,t);f===null&&v.push({severity:"warning",code:"SKELETON_MATERIALIZED",message:"config/integrations.json 缺席——已物化骨架(写时自动初始化;R13 语义翻转提示)"});let u=f??{services:{}},k={...u.services??{}},s={...u,services:k};k[d]={...p};let l=p.provider,h=l!==null&&typeof l=="object"?l:void 0,C=typeof l=="string"?l:typeof h?.provider=="string"?h.provider:"app",j=d.includes(".")?d.slice(0,d.indexOf(".")):d,R=sn(C,d,j,m.credentials,m.credentialsByInstance);for(let x of R)cn(e,C,d,x.input,typeof h?.credentialType=="string"?h.credentialType:void 0,t);let w=k[d],c=R.find(x=>x.instanceId===void 0);if(m.credentials&&c&&(w.credentialRef=c.ref),m.credentialsByInstance){let x={...w.instances??{}};for(let E of R){if(E.instanceId===void 0)continue;let N={...x[E.instanceId]??{}};N.credentialRef=E.ref,x[E.instanceId]=N}w.instances=x}let S=m.credentials!==void 0||m.credentialsByInstance!==void 0,b=U(f,s);if(b&&!S)return{written:!1,restartScheduled:!1,files:[],credentials:[],issues:v};g(s,v);let I=m.dryRun===!0,A=Re(e,R,I);return v.push(...he(s)),a(b?null:s,A,v,m.dryRun,!I&&R.length>0)},deleteService(d){let p=[],m=se(e,t),v={...m?.services??{}};if(!(d in v))return{written:!1,restartScheduled:!1,files:[],credentials:[],issues:p};delete v[d];let f={...m??{services:{}},services:v};$(K(e,L),`${JSON.stringify(f,null,2)}
|
|
5
|
+
`),t?.invalidate([K(e,L)]),p.push(...we(e));let u=r!==void 0;return u&&r?.(),{written:!0,restartScheduled:u,files:[L],credentials:[],issues:p}}}}import{existsSync as ct}from"fs";import{join as lt}from"path";function be(e){let n=lt(e,"config","integrations.json");if(ct(n))return{generated:!1,reason:"in-place",services:[]};let r=[];for(let i of P(e).modules)for(let o of i.descriptor.contributes.providers?.slots??[])o.local===!0&&r.push({service:o.service,provider:o.provider,implementation:o.implementation});if(r.length===0)return{generated:!1,reason:"no-local-slots",services:[]};let t={},a=new Set(r.map(i=>i.provider));if(a.size===1){let[i]=a;for(let o of r)t[o.service]={};$(n,`${JSON.stringify({provider:i,services:t},null,2)}
|
|
6
|
+
`)}else{for(let i of r)t[i.service]={provider:i.provider,implementation:i.implementation};$(n,`${JSON.stringify({services:t},null,2)}
|
|
7
|
+
`)}return{generated:!0,reason:"generated",services:r.map(i=>i.service).sort()}}function G(e){let n=e.indexOf(".");return n===-1?e:e.slice(0,n)}function un(e,n){let r=P(e.appDir,e.cache),t=z(e.appDir,e.cache);return{appDir:e.appDir,walk:r,catalog:r.catalog,integrations:t,contracts:n}}function pn(e){let n=new Set;for(let r of e.modules){for(let t of r.descriptor.contributes.services??[])n.add(t.service);for(let t of r.descriptor.contributes.providers?.slots??[])n.add(t.service)}return n}function fn(e,n,r){let t=e.providers[n];return t?Object.values(t).some(a=>a.services.includes(r)):!1}function gn(e,n){let r=[];for(let[t,a]of Object.entries(e.providers))Object.values(a).some(i=>i.services.includes(n))&&r.push(t);return r.sort()}function Ce(e){return{modules:e.walk.modules.map(r=>({id:r.id,name:r.descriptor.name,title:r.descriptor.name,description:""})).sort((r,t)=>r.id.localeCompare(t.id))}}function mn(e,n){let r=new Set(e.map(t=>G(t.service)));return r.size===1?[...r][0]:n}function Ae(e,n){let r=e.walk.modules.find(g=>g.id===n);if(!r)throw new O("MODULE_NOT_FOUND",404,`模块 ${n} 非已装模块`);let t=(r.descriptor.contributes.services??[]).map(g=>({service:g.service,...g.title?{title:g.title}:{},...g.description?{description:g.description}:{},required:g.optional!==!1})),a=e.integrations?.domains??{},i=mn(t,n);return{integration:a[i]??null,services:t}}function xe(e){let n=pn(e.walk),r=e.walk.vocabulary.filter(i=>!n.has(i)),t=e.integrations,a=null;if(t){let i={};for(let o of H)o in t&&(i[o]=t[o]);a=i}return{services:r,integration:a}}function Ee(e,n){let r=e.integrations?.services??{},t=e.catalog.services[n],a=e.walk.vocabulary.includes(n),i=n in r;if(!a&&!i)throw new O("SERVICE_NOT_FOUND",404,`服务 ${n} 不在服务词汇(已装契约包 ∪ 已配置)`);let o=r[n]??null;return{service:n,...t?.title?{title:t.title}:{title:n},...t?.description?{description:t.description}:{},required:t?!t.optional:!1,defaults:t?t.defaults.map(g=>({...g})):[],integration:o,suppliedBy:gn(e.catalog,n)}}import{existsSync as Rn,readFileSync as wn,realpathSync as vn}from"fs";import{isAbsolute as dt,join as ut,relative as pt,resolve as yn,sep as ft}from"path";function Me(e){let n=[];for(let[r,t]of Object.entries(e.catalog.providers)){let a=Object.values(t);a.length!==0&&n.push({provider:r,local:a.every(i=>i.local===!0),owner:a[0].owner})}return{providers:n.sort((r,t)=>r.provider.localeCompare(t.provider))}}function Oe(e,n){let r=e.catalog.providers[n];if(!r)throw new O("PROVIDER_NOT_FOUND",404,`厂商 ${n} 不 ∈ catalog(内联声明厂商无包级档案——身份与表单经组合端点暴露)`);let t={};for(let i of Object.values(r))for(let o of i.services)(t[o]??={implementations:[]}).implementations.push({implementation:i.implementation,credentialType:i.credentialType});let a=Object.values(r);return{provider:n,local:a.every(i=>i.local===!0),owner:a[0].owner,services:t}}function je(e,n,r){let t=e.catalog.providers[n],a=e.integrations?.services??{},i=[];if(t)for(let v of Object.values(t))v.services.includes(r)&&i.push({implementation:v.implementation,credentialType:v.credentialType});let o=a[r]?.provider,g=o&&typeof o=="object"&&o.provider===n?o:void 0;if(i.length===0&&!g)throw t?new O("SUPPLY_NOT_FOUND",404,`厂商 ${n} 未供给服务 ${r}(AI 帮我实现流:PUT 显式绑定后实施)`):new O("PROVIDER_NOT_FOUND",404,`厂商 ${n} 完全未知(不 ∈ catalog 且无任何内联声明)`);i.length===0&&g&&i.push({implementation:a[r]?.implementation??`${n}-inline@1`,credentialType:g.credentialType});let d=null,p=null,m=P(e.appDir).serviceSchemas[`${n}\0${r}`];if(m)d=Sn(e.appDir,m.packageDir,m.configSchema),p=Sn(e.appDir,m.packageDir,m.credentialSchema);else if(g){let v=o?.configSchema;typeof v=="string"&&(d=gt(e.appDir,v))}return{provider:n,service:r,implementations:i,integrationSchemas:{configSchema:d,credentialSchema:p}}}function Sn(e,n,r){if(!r)return null;let t=ut(n,r);if(!Rn(t))return null;try{return JSON.parse(wn(t,"utf8"))}catch{return null}}function gt(e,n){let r=yn(e,"config","schemas"),t=yn(e,n),a=pt(r,t);if(a===""||a.startsWith("..")||dt(a))throw new O("VALIDATION_FAILED",400,"configSchema 引用必须落在 config/schemas/ 内(禁路径逃逸)",["provider.configSchema"]);if(!Rn(t))return null;let i,o;try{i=vn(r),o=vn(t)}catch{return null}if(o!==i&&!o.startsWith(i+ft))throw new O("VALIDATION_FAILED",400,"configSchema 引用经 symlink 逃逸出 config/schemas/(realpath 复核拒绝)",["provider.configSchema"]);try{return JSON.parse(wn(t,"utf8"))}catch{return null}}function In(e){return{version:e.version,evaluation:e.evaluation,strictValidation:e.strictValidation,notes:[...e.notes]}}function mt(e,n,r){let t=e.integrations,a=G(n),i=t?.services?.[n],o=i?.instances?.[r]??i?.instances?.["*"];return o&&o.provider!==void 0?"instance":i?.provider!==void 0?"service":t?.domains?.[a]?.provider!==void 0?"module":"app"}function vt(e,n){if(n===void 0||!n.startsWith("secret://"))return;let r=n.slice(9),t=Y(e,r);if(t)return{type:t.type,ref:n,file:_(r),masked:q(t.values)}}function hn(e,n,r,t,a){let i=n.resolveEffectiveService(e.integrations,r,t,a);if(i.status==="ok"&&i.effective){let o=i.effective.provider,g=typeof o=="string"?o:String(o?.provider??"");return{status:"ok",provider:g,implementation:i.effective.implementation,effective:{provider:g,implementation:i.effective.implementation,...i.effective.config!==void 0?{config:{...i.effective.config}}:{},...i.effective.credentialRef!==void 0?{credentialRef:i.effective.credentialRef}:{},layer:mt(e,r,t)}}}return i.status==="binding-unresolved"?{status:"binding-unresolved",statusMessage:i.message}:{status:i.status}}function Te(e){let n=e.contracts.module;if(!n.resolveEffectiveService||!n.normalizeInstances)throw new Error("contracts 求值面缺席(工厂门控应先行拦截)");return n}function Ne(e,n){let r=Te(n),{integrations:t,catalog:a}=n,i=r.buildSupplyLookup(a),o=t,d=r.normalizeInstances({instances:o?.instances,defaultInstanceId:o?.defaultInstanceId}).defaultInstanceId??"*",p=hn(n,r,e,d,i),m=n.integrations?.services?.[e]?.provider,v=p.provider??(typeof m=="string"?m:typeof m=="object"&&m?String(m.provider):void 0),f=new Set(Object.keys(n.catalog.providers)),u=v!==void 0&&f.has(v)?yt(n.catalog,v,e):void 0,k=a.services[e],s=k!==void 0?!k.optional:void 0,l=k?.owners[0];return{status:p.status,...p.statusMessage?{statusMessage:p.statusMessage}:{},...v!==void 0?{provider:v}:{},...u!==void 0?{supplied:u}:{},...l!==void 0?{module:l}:{},...s!==void 0?{required:s}:{}}}function yt(e,n,r){return fn(e,n,r)}function Pe(e,n,r){let t=new Set([...e.walk.vocabulary,...Object.keys(e.catalog.services)]),a=e.integrations?.services??{};for(let o of Object.keys(a))t.add(o);let i={};for(let o of[...t].sort())r&&!r.includes(o)||(i[o]=Ne(o,e));return{contracts:In(e.contracts),resolutions:i,issues:n}}function De(e,n,r){let t=Te(n),a=t.buildSupplyLookup(n.catalog),i=n.integrations,g=t.normalizeInstances({instances:i?.instances,defaultInstanceId:i?.defaultInstanceId}).instances.map(s=>s.id),d=n.integrations?.services?.[e],p=d?.instances?Object.keys(d.instances):void 0,m;g.length>0?(m=[...g],p?.includes("*")&&m.push("*")):p!==void 0&&p.length>0?m=p.includes("*")?["*"]:p:m=[];let v=m.map(s=>{let l=hn(n,t,e,s,a),h=vt(n.appDir,l.effective?.credentialRef);return{instanceId:s,status:l.status,...l.statusMessage?{statusMessage:l.statusMessage}:{},...l.effective?{effective:l.effective}:{},...h?{credential:h}:{}}}),f=St(n,e),k={...Ne(e,n),instances:v,inheritance:f};return{contracts:In(n.contracts),resolution:k,issues:r.filter(s=>s.service===e)}}function St(e,n){let r=e.integrations,t=G(n),a=r?.services?.[n],i=e.integrations,g=Te(e).normalizeInstances({instances:i?.instances,defaultInstanceId:i?.defaultInstanceId}).defaultInstanceId??"*",d=a?.instances?.[g]??a?.instances?.["*"];return[{layer:"app",...r?.provider?{provider:r.provider}:{}},{layer:"module",...r?.domains?.[t]?.provider?{provider:r.domains[t].provider}:{}},{layer:"service",...typeof a?.provider=="string"?{provider:a.provider}:{},...a?.implementation?{implementation:a.implementation}:{}},{layer:"instance",...d?.provider?{provider:d.provider}:{},...d?.implementation?{implementation:d.implementation}:{}}]}import Rt from"path";function wt(e,n={}){let r=ce(),t=Promise.resolve(),a={appDir:e,cache:r};function i(s){if(!s.strictValidation)throw new O("CONTRACTS_NOT_RESOLVED",503,Ie(s))}function o(s){return un(a,s)}async function g(){let s=await M(e),l=o(s),h=l.integrations;if(h===null)return[];let C={resolveEffectiveService:d(s.module.resolveEffectiveService,"resolveEffectiveService"),buildSupplyLookup:d(s.module.buildSupplyLookup,"buildSupplyLookup"),moduleDomainOf:d(s.module.moduleDomainOf,"moduleDomainOf"),normalizeInstances:d(s.module.normalizeInstances,"normalizeInstances"),RESOURCE_TYPE_IDS:s.module.RESOURCE_TYPE_IDS??[]},j=ge(e,l.walk,h,process.env);return X({contracts:C,integrations:h,supplies:C.buildSupplyLookup(l.catalog),...j})}function d(s,l){if(typeof s!="function")throw new O("CONTRACTS_NOT_RESOLVED",503,`contracts 求值面 ${l} 缺席(形态异常)`);return s}function p(s){let l=t.then(s,s);return t=l.catch(()=>{}),l}let m={async loadModules(){let s=await M(e);return Ce(o(s))},async loadModule(s){let l=await M(e);return Ae(o(l),s)},async loadApp(){let s=await M(e);return xe(o(s))},async loadService(s){let l=await M(e);return Ee(o(l),s)},async loadServiceResolutions(s){let l=await M(e);i(l);let h=await g();return Pe(o(l),h,s)},async loadServiceResolution(s){let l=await M(e);i(l);let h=await g();return De(s,o(l),h)},async loadProviders(){let s=await M(e);return Me(o(s))},async loadProvider(s){let l=await M(e);return Oe(o(l),s)},async loadProviderService(s,l){let h=await M(e);return je(o(h),s,l)}},v=ke(e,{onApplied:n.onApplied,cache:r}),f={async writeAppIntegration(s,l){await v.warmup();let h=await M(e);return i(h),p(()=>v.applyApp(s,l))},async writeModuleIntegration(s,l,h){await v.warmup();let C=await M(e);return i(C),p(()=>v.applyModule(s,l,h))},async writeServiceIntegration(s,l,h){await v.warmup();let C=await M(e);return i(C),p(()=>v.applyService(s,l,h))},async deleteServiceIntegration(s){await v.warmup();let l=await M(e);return i(l),p(()=>v.deleteService(s))}},u={invalidate(){r.invalidate(),le(e)},async resolveContracts(){return M(e)},walkManifests(){return k(e)},parseModuleDescriptor(s){return J(s)},loadProviderCatalogRaw(){return ye(e)},async readIntegrationsConfig(){return z(e,r)},async writeIntegrationsConfig(s){await v.warmup();let l=await M(e);return i(l),p(()=>It(e,s,r))},async ensureMockBindings(){await v.warmup();let s=await M(e);return i(s),p(()=>be(e))},async evaluateIntegrationServices(s){await v.warmup();let l=await M(e);i(l);let h={resolveEffectiveService:d(l.module.resolveEffectiveService,"resolveEffectiveService"),buildSupplyLookup:d(l.module.buildSupplyLookup,"buildSupplyLookup"),moduleDomainOf:d(l.module.moduleDomainOf,"moduleDomainOf"),normalizeInstances:d(l.module.normalizeInstances,"normalizeInstances"),RESOURCE_TYPE_IDS:l.module.RESOURCE_TYPE_IDS??[]};return X({...s,contracts:h})}};function k(s){return P(s,r)}return{...m,...f,...u}}function It(e,n,r){let t=Rt.join(e,"config","integrations.json");$(t,`${JSON.stringify(n,null,2)}
|
|
8
|
+
`),r.invalidate([t])}export{H as APP_INTEGRATION_NODE_KEYS,O as AppToolkitError,F as SAFE_NAME_PATTERN,Yn as assertValidModuleId,ge as buildPredicateAssembly,pe as checkInstanceKeys,Qn as collectDeclaredModules,Ie as contractsNotResolvedMessage,wt as createAppToolkit,ce as createFileCache,ke as createWriteCore,it as credentialArtifactExists,U as deepEqual,W as deriveStem,Ue as ensureDir,be as ensureMockBindings,X as evaluateIntegrationServices,An as expandEnvVars,en as extractModuleIdFromSource,le as invalidateContractsResolver,ue as isControlledInlineProvider,Qe as isValidModuleId,xe as loadAppView,Ae as loadModuleDetailView,Ce as loadModulesView,nn as loadProviderCatalog,ye as loadProviderCatalogRaw,Oe as loadProviderDetailView,je as loadProviderServiceDetailView,Me as loadProvidersView,Ee as loadServiceDetailView,De as loadServiceResolutionDetail,Pe as loadServiceResolutionsView,ne as manifestPath,q as maskCredentialValues,_e as moduleDependencySchema,Be as moduleDescriptorSchema,mn as moduleDomainKeyOf,Ve as pageContribSchema,J as parseModuleDescriptor,Fe as providerSlotContribSchema,qe as readAppManifest,me as readAppManifestFull,Y as readCredentialFile,z as readIntegrationsConfig,rn as readIntegrationsStrict,we as reconcileOrphanCredentials,Ze as refToEnvKey,Kn as removeNpmModule,M as resolveContractsForApp,M as resolveContractsForAppExport,Ne as resolveServiceSummary,We as resourceNeedContribSchema,Le as serviceNeedContribSchema,_ as stemToFilePath,jn as stemToRef,$e as tabContribSchema,Jn as updateNpmModule,Un as upsertMiniappContainer,zn as upsertNpmModule,P as walkManifests,Bn as writeAppManifest,$ as writeAtomic,de as writeCredentialFile};
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tbox.cn/app-toolkit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "tbox 应用工具面(运行面/工具面对仗:app-sdk 给运行的应用用,app-toolkit 给操作应用的工具用)——模块解析、集成配置求值/校验/写入内核,contracts 经应用侧动态解析(沙箱与目标应用版本解耦)。",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"ajv": "8.20.0",
|
|
18
|
+
"zod": "3.25.76"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@tbox.cn/app-contracts": "0.9.0",
|
|
22
|
+
"typescript": "5.9.3",
|
|
23
|
+
"tsup": "8.5.1",
|
|
24
|
+
"vitest": "4.1.10",
|
|
25
|
+
"@types/node": "24.13.3"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20.0.0"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"src",
|
|
33
|
+
"tests",
|
|
34
|
+
"tsconfig.json",
|
|
35
|
+
"tsconfig.build.json",
|
|
36
|
+
"tsup.config.ts",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsup",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"smoke:dist": "node scripts/smoke-dist.mjs"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* .tbox/app.json npmModules 读写(自 CLI manifest.ts 迁入——assembly 层目录知识单源)。
|
|
6
|
+
* npmModules 登记 = sdk 包双语义磁盘足迹之一(业务包 = packages/<id>/ 目录,sdk 包 = 本清单登记)。
|
|
7
|
+
* 本地模块(packages/module-*)不在 npmModules 登记——靠 tbox.module.json 约定发现。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type ModuleMode = 'local' | 'sdk' | 'codegen';
|
|
11
|
+
|
|
12
|
+
export interface NpmModuleEntry {
|
|
13
|
+
/** 模块短 id(如 "module-member"),装配别名来源 */
|
|
14
|
+
id: string;
|
|
15
|
+
/** 发布源包名;codegen 的应用内身份由 id 派生为 @app/<id>,sdk 保持该包名 */
|
|
16
|
+
package: string;
|
|
17
|
+
/** 安装版本 */
|
|
18
|
+
version: string;
|
|
19
|
+
/** 分发模式 */
|
|
20
|
+
mode: ModuleMode;
|
|
21
|
+
/** 3-way merge 基线(codegen 模式) */
|
|
22
|
+
baseVersion?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface AppManifest {
|
|
26
|
+
templateVersion: string;
|
|
27
|
+
npmModules: NpmModuleEntry[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function manifestPath(appDir: string): string {
|
|
31
|
+
return path.join(appDir, '.tbox', 'app.json');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 读应用 manifest(缺席 → null;损坏/版本非法 → null——宽松解析与 CLI 语义一致) */
|
|
35
|
+
export function readAppManifest(appDir: string): AppManifest | null {
|
|
36
|
+
const file = manifestPath(appDir);
|
|
37
|
+
if (!existsSync(file)) return null;
|
|
38
|
+
try {
|
|
39
|
+
const raw = JSON.parse(readFileSync(file, 'utf8'));
|
|
40
|
+
const templateVersion = raw?.templateVersion;
|
|
41
|
+
if (typeof templateVersion !== 'string' || templateVersion.trim().length === 0) return null;
|
|
42
|
+
return {
|
|
43
|
+
templateVersion: templateVersion.trim(),
|
|
44
|
+
npmModules: Array.isArray(raw?.npmModules) ? raw.npmModules : [],
|
|
45
|
+
};
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 写应用 manifest(目录缺席物化;同步——写核心串行队列内使用) */
|
|
52
|
+
export function writeAppManifest(appDir: string, manifest: AppManifest): void {
|
|
53
|
+
const file = manifestPath(appDir);
|
|
54
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
55
|
+
writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── CLI manifest 全表面(C5 迁入——upsert/remove/miniapp 容器/模块 id 合法性)──
|
|
59
|
+
|
|
60
|
+
/** 小程序容器登记项(miniapp-container init 写入;与 npmModules 正交,不参与装配/doctor) */
|
|
61
|
+
export interface MiniappContainerEntry {
|
|
62
|
+
/** 平台 id:'alipay' / 'weapp'(= CLI platform 参数,展开目录名后缀) */
|
|
63
|
+
id: string;
|
|
64
|
+
/** 源包名(@tbox.cn/app-miniapp-container-*) */
|
|
65
|
+
package: string;
|
|
66
|
+
version: string;
|
|
67
|
+
/** 展开目录(相对应用根,如 'apps/miniapp-container-alipay') */
|
|
68
|
+
dir: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface AppManifestFull extends AppManifest {
|
|
72
|
+
/** 已初始化的小程序容器(可选——旧 manifest 无此字段,读侧归一为 []) */
|
|
73
|
+
miniappContainers?: MiniappContainerEntry[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 读应用 manifest(完整面——miniappContainers 归一) */
|
|
77
|
+
export function readAppManifestFull(appDir: string): AppManifestFull | null {
|
|
78
|
+
const base = readAppManifest(appDir);
|
|
79
|
+
if (base === null) return null;
|
|
80
|
+
const file = manifestPath(appDir);
|
|
81
|
+
try {
|
|
82
|
+
const raw = JSON.parse(readFileSync(file, 'utf8')) as { miniappContainers?: unknown };
|
|
83
|
+
return {
|
|
84
|
+
...base,
|
|
85
|
+
miniappContainers: Array.isArray(raw?.miniappContainers) ? (raw.miniappContainers as MiniappContainerEntry[]) : [],
|
|
86
|
+
};
|
|
87
|
+
} catch {
|
|
88
|
+
return { ...base, miniappContainers: [] };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function upsertNpmModule(manifest: AppManifest, entry: NpmModuleEntry): AppManifest {
|
|
93
|
+
const rest = manifest.npmModules.filter((m) => m.id !== entry.id);
|
|
94
|
+
return { ...manifest, npmModules: [...rest, entry] };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function removeNpmModule(manifest: AppManifest, id: string): AppManifest {
|
|
98
|
+
return { ...manifest, npmModules: manifest.npmModules.filter((m) => m.id !== id) };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 登记小程序容器(按 id 去重替换;miniapp-container init 用) */
|
|
102
|
+
export function upsertMiniappContainer(manifest: AppManifestFull, entry: MiniappContainerEntry): AppManifestFull {
|
|
103
|
+
const rest = (manifest.miniappContainers ?? []).filter((m) => m.id !== entry.id);
|
|
104
|
+
return { ...manifest, miniappContainers: [...rest, entry] };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 更新某模块的元数据(update 命令用:version / baseVersion) */
|
|
108
|
+
export function updateNpmModule(
|
|
109
|
+
manifest: AppManifest,
|
|
110
|
+
id: string,
|
|
111
|
+
patch: Partial<Pick<NpmModuleEntry, 'version' | 'baseVersion'>>,
|
|
112
|
+
): AppManifest {
|
|
113
|
+
return {
|
|
114
|
+
...manifest,
|
|
115
|
+
npmModules: manifest.npmModules.map((m) => (m.id === id ? { ...m, ...patch } : m)),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 模块 id 合法性:小写字母/数字开头,单词段以单个中划线分隔(无连续/尾部中划线),
|
|
121
|
+
* 防路径穿越/目录注入,并保证 camelCaseId 派生别名唯一(a-b 与 a--b 不允许共存碰撞)。
|
|
122
|
+
*/
|
|
123
|
+
export function isValidModuleId(id: string): boolean {
|
|
124
|
+
return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(id) && !id.includes('..');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 校验失败抛出(统一错误信息) */
|
|
128
|
+
export function assertValidModuleId(id: string, source: string): void {
|
|
129
|
+
if (!isValidModuleId(id)) {
|
|
130
|
+
throw new Error(`非法模块 id "${id}"(来自 ${source}):仅允许小写字母/数字/中划线`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { walkManifests, type WalkCatalog, type WalkProviderEntry } from './walk-manifests.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* provider catalog 双形态(ai-build C3-2b/C3-5):
|
|
7
|
+
* - **桥源 `loadProviderCatalog(appDir)`**:签名与返回形态冻结(app-sdk re-export 桥源——
|
|
8
|
+
* 存量克隆应用模板导入零改动;≠ 富形态,双形态有意)。实现自 app-sdk
|
|
9
|
+
* runtime/provider-catalog.ts 等价迁入(共享 walkManifests 缓存生命周期——本包内统一走
|
|
10
|
+
* walkManifests 单次遍历产物投喂,行为等价性由 app-sdk 桥 golden 锁定)。
|
|
11
|
+
* - **富形态 `loadProviderCatalogRaw(appDir)`**:工厂迁移面(providers 嵌套保真 + per-service
|
|
12
|
+
* schema 复合键 + 词汇产物——walkManifests 产物直通)。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type { WalkCatalog, WalkProviderEntry };
|
|
16
|
+
|
|
17
|
+
/** 桥源形态冻结:ProviderCatalog = contracts TIER0 形状(providers/services/credentialTypes) */
|
|
18
|
+
export interface BridgeProviderCatalog {
|
|
19
|
+
providers: Record<
|
|
20
|
+
string,
|
|
21
|
+
Record<
|
|
22
|
+
string,
|
|
23
|
+
{
|
|
24
|
+
provider: string;
|
|
25
|
+
implementation: string;
|
|
26
|
+
credentialType: string;
|
|
27
|
+
owner: string;
|
|
28
|
+
local?: boolean;
|
|
29
|
+
configSchema?: string;
|
|
30
|
+
credentialSchema?: string;
|
|
31
|
+
services: string[];
|
|
32
|
+
}
|
|
33
|
+
>
|
|
34
|
+
>;
|
|
35
|
+
services: Record<string, { optional: boolean; defaults: Array<{ provider: string; implementation: string }>; owners: string[] }>;
|
|
36
|
+
credentialTypes: Record<string, { owner: string; credentialSchema?: string }>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 富形态(工厂迁移面):walkManifests 产物直通 */
|
|
40
|
+
export function loadProviderCatalogRaw(appDir: string): WalkCatalog {
|
|
41
|
+
return walkManifests(appDir).catalog;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 桥源(形态冻结):富形态 → 桥形状投影(demand meta title/description 剥离——桥面无此字段,
|
|
45
|
+
* additive 差异声明见 app-sdk 桥 golden) */
|
|
46
|
+
export function loadProviderCatalog(appDir: string): BridgeProviderCatalog {
|
|
47
|
+
const rich = walkManifests(appDir).catalog;
|
|
48
|
+
const providers: BridgeProviderCatalog['providers'] = {};
|
|
49
|
+
for (const [vendor, impls] of Object.entries(rich.providers)) {
|
|
50
|
+
providers[vendor] = {};
|
|
51
|
+
for (const [impl, entry] of Object.entries(impls)) {
|
|
52
|
+
providers[vendor][impl] = {
|
|
53
|
+
provider: entry.provider,
|
|
54
|
+
implementation: entry.implementation,
|
|
55
|
+
credentialType: entry.credentialType,
|
|
56
|
+
owner: entry.owner,
|
|
57
|
+
...(entry.local ? { local: true } : {}),
|
|
58
|
+
...(entry.configSchema ? { configSchema: entry.configSchema } : {}),
|
|
59
|
+
...(entry.credentialSchema ? { credentialSchema: entry.credentialSchema } : {}),
|
|
60
|
+
services: [...entry.services],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const services: BridgeProviderCatalog['services'] = {};
|
|
65
|
+
for (const [service, entry] of Object.entries(rich.services)) {
|
|
66
|
+
services[service] = {
|
|
67
|
+
optional: entry.optional,
|
|
68
|
+
defaults: entry.defaults.map((d) => ({ ...d })),
|
|
69
|
+
owners: [...entry.owners],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const credentialTypes: BridgeProviderCatalog['credentialTypes'] = {};
|
|
73
|
+
for (const [ct, entry] of Object.entries(rich.credentialTypes)) {
|
|
74
|
+
credentialTypes[ct] = { ...entry };
|
|
75
|
+
}
|
|
76
|
+
return { providers, services, credentialTypes };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// provider-schemas:组合单体装配链(C3-2;表单 schema 唯一暴露面的服务端装配)
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
export interface ProviderServiceSchemas {
|
|
84
|
+
provider: string;
|
|
85
|
+
service: string;
|
|
86
|
+
/** 恒数组(H1——通常 1;多 impl = 升级窗口) */
|
|
87
|
+
implementations: Array<{ implementation: string; credentialType: string }>;
|
|
88
|
+
integrationSchemas: {
|
|
89
|
+
configSchema: object | null;
|
|
90
|
+
credentialSchema: object | null;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type ProviderServiceSchemasResult =
|
|
95
|
+
| { found: true; detail: ProviderServiceSchemas }
|
|
96
|
+
| { found: false; reason: 'provider-not-found' | 'supply-not-found' };
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 组合单体 schema 解析(api.md 4.9 数据源;P10/P11 单源):
|
|
100
|
+
* - 供给解析 = catalog 槽供给在场 → implementations[] 数组装配(vendor→impl 展开该 service 的
|
|
101
|
+
* 全部 {implementation, credentialType},声明序);service 级内联声明在场(该 provider 名)→
|
|
102
|
+
* 单元素(声明对)。
|
|
103
|
+
* - schema 解析自默认 impl(唯一供给 ?? manifest 声明序首个——H1 单源规则);复合键
|
|
104
|
+
* `${provider}\u0000${service}` 产物读取;供给在场未声明 = null(mock 合法,非 404——P11)。
|
|
105
|
+
* - 内联声明 configSchema 引用文件解析:**路径约束(H9 安全钉子)**——纯相对路径 + resolve 后
|
|
106
|
+
* 必须落在 <appDir>/config/schemas/ 内 + realpath 复核;违例 → throw(HTTP 层 400)。
|
|
107
|
+
*/
|
|
108
|
+
export function loadProviderServiceSchemas(
|
|
109
|
+
appDir: string,
|
|
110
|
+
provider: string,
|
|
111
|
+
service: string,
|
|
112
|
+
): ProviderServiceSchemasResult {
|
|
113
|
+
const { catalog, serviceSchemas } = walkManifests(appDir);
|
|
114
|
+
const vendorImpls = catalog.providers[provider];
|
|
115
|
+
if (vendorImpls === undefined) {
|
|
116
|
+
// provider 完全未知(catalog 外)——内联声明(integrations.json 服务级对象 provider)
|
|
117
|
+
// 的统一路径双来源解析归 views 层(装配层纯目录知识,零 integrations 依赖)
|
|
118
|
+
return { found: false, reason: 'provider-not-found' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const implementations: Array<{ implementation: string; credentialType: string }> = [];
|
|
122
|
+
for (const entry of Object.values(vendorImpls)) {
|
|
123
|
+
if (entry.services.includes(service)) {
|
|
124
|
+
implementations.push({ implementation: entry.implementation, credentialType: entry.credentialType });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (implementations.length === 0) {
|
|
128
|
+
// 厂商已知但未供给该服务——SUPPLY_NOT_FOUND 语义(HTTP 层 404)
|
|
129
|
+
return { found: false, reason: 'supply-not-found' };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// schema 解析自默认 impl(唯一供给 ?? 声明序首个——Object.values 保序 = manifest 声明序)
|
|
133
|
+
const schemaKey = `${provider}\u0000${service}`;
|
|
134
|
+
const schemaEntry = serviceSchemas[schemaKey] ?? null;
|
|
135
|
+
const configSchema = schemaEntry?.configSchema ? readSchemaFile(appDir, schemaEntry.configSchema) : null;
|
|
136
|
+
const credentialSchema = schemaEntry?.credentialSchema ? readSchemaFile(appDir, schemaEntry.credentialSchema) : null;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
found: true,
|
|
140
|
+
detail: {
|
|
141
|
+
provider,
|
|
142
|
+
service,
|
|
143
|
+
implementations,
|
|
144
|
+
integrationSchemas: { configSchema, credentialSchema },
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** schema 引用文件读取(相对包根或应用 config/schemas/;路径约束 H9——config/schemas/ 内 + realpath 复核) */
|
|
150
|
+
function readSchemaFile(appDir: string, ref: string): object | null {
|
|
151
|
+
// 相对路径约束:resolve 后必须落在 <appDir>/config/schemas/ 内(禁 .. 逃逸/绝对路径)
|
|
152
|
+
const schemasRoot = resolve(appDir, 'config', 'schemas');
|
|
153
|
+
const target = resolve(appDir, ref);
|
|
154
|
+
if (!target.startsWith(schemasRoot + '\\') && !target.startsWith(schemasRoot + '/')) {
|
|
155
|
+
// 非法引用:H9 违例——抛给 HTTP 层转 400(fields: provider.configSchema)
|
|
156
|
+
throw new Error(
|
|
157
|
+
`configSchema 引用必须落在 config/schemas/ 内(禁路径逃逸):${ref}`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
const file = join(target);
|
|
161
|
+
if (!existsSync(file)) return null; // SCHEMA_FILE_MISSING 归谓词 #6(此处宽松 null)
|
|
162
|
+
try {
|
|
163
|
+
return JSON.parse(readFileSync(file, 'utf8')) as object;
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|