@tbox.cn/app-toolkit 0.1.0 → 0.3.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.
Files changed (37) hide show
  1. package/README.md +2 -2
  2. package/dist/{chunk-VV3ERZXS.js → chunk-KPD3LUH4.js} +1 -1
  3. package/dist/contracts-resolver-E4SECBOG.js +1 -0
  4. package/dist/index.d.ts +234 -43
  5. package/dist/index.js +8 -8
  6. package/package.json +2 -2
  7. package/src/assembly/app-manifest.ts +6 -0
  8. package/src/assembly/provider-catalog.ts +6 -1
  9. package/src/assembly/walk-manifests.ts +2 -1
  10. package/src/core/contracts-resolver.ts +3 -2
  11. package/src/core/credential-format.ts +4 -2
  12. package/src/core/errors.ts +9 -3
  13. package/src/dto.ts +127 -15
  14. package/src/factory.ts +129 -16
  15. package/src/index.ts +15 -4
  16. package/src/integrations/credentials.ts +1 -1
  17. package/src/integrations/domain-binding.ts +81 -0
  18. package/src/integrations/predicate.ts +7 -6
  19. package/src/integrations/read.ts +23 -3
  20. package/src/integrations/write.ts +93 -34
  21. package/src/views/app.ts +7 -5
  22. package/src/views/context.ts +3 -6
  23. package/src/views/credential-types.ts +28 -0
  24. package/src/views/credentials.ts +57 -0
  25. package/src/views/integration-schemas.ts +226 -0
  26. package/src/views/modules.ts +33 -14
  27. package/src/views/providers.ts +37 -22
  28. package/src/views/service-detail.ts +3 -4
  29. package/src/views/service-resolutions.ts +31 -25
  30. package/tests/credentials-view.test.ts +152 -0
  31. package/tests/demo-app.ts +14 -1
  32. package/tests/file-cache.test.ts +3 -1
  33. package/tests/naming-alignment.test.ts +8 -5
  34. package/tests/vendor-schema-keywords.test.ts +101 -0
  35. package/tests/views.test.ts +1285 -13
  36. package/tests/write-core.test.ts +107 -8
  37. package/dist/contracts-resolver-QM4FBQQV.js +0 -1
@@ -0,0 +1,152 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { join } from 'node:path';
3
+ import { createAppToolkit, type AppToolkit } from '../src/factory.js';
4
+ import { invalidateContractsResolver } from '../src/core/contracts-resolver.js';
5
+ import { createTempApp, writeFakeContractsPackage, linkContracts, writeAppFile, type TempApp } from './demo-app.js';
6
+
7
+ /**
8
+ * v4.4 D33 凭据域视图矩阵(F6——C1-C5):键集三断言(app.byInstance = 注册表 ids ∩ 有 ref /
9
+ * services = 已配置 / services.byInstance = 节点 instances map 全键含 '*')/ null 语义
10
+ * (未就绪——诊断归 issues #1)/ 零真值(masked 形态)/ credential-types 清单 + ?provider= 过滤 /
11
+ * 删净断言(instances[].credential、组合单体 credentialSchema——序列化面不信 typecheck)。
12
+ */
13
+
14
+ const OK_RESOLVE = [
15
+ 'const slot = integrations && integrations.services && integrations.services[service];',
16
+ 'if (!slot) return { status: "service-not-configured" };',
17
+ 'const prov = typeof slot.provider === "string" ? slot.provider : slot.provider && slot.provider.provider;',
18
+ 'if (!prov) return { status: "service-not-configured" };',
19
+ 'return { status: "ok", effective: { provider: prov, implementation: slot.implementation || "impl@1", config: slot.config || {}, ...(slot.credentialRef ? { credentialRef: slot.credentialRef } : {}) } };',
20
+ ].join(' ');
21
+
22
+ function credHarness(): { app: TempApp; tk: AppToolkit } {
23
+ const app = createTempApp('tbox-cred-');
24
+ invalidateContractsResolver();
25
+ const store = join(app.appDir, 'contracts');
26
+ writeFakeContractsPackage(store, { version: '0.9.0', resolveBody: OK_RESOLVE });
27
+ linkContracts(app.appDir, store, { subdir: 'apps/server' });
28
+ // 供给 manifest(wanda 两槽共享 type + schema 文件;mock '-' 槽)——types 清单与组合单体删净断言用
29
+ writeAppFile(
30
+ app.appDir,
31
+ 'packages/provider-w/tbox.module.json',
32
+ JSON.stringify({
33
+ schemaVersion: 1,
34
+ name: 'provider-w',
35
+ version: '0.1.0',
36
+ kind: 'business',
37
+ contributes: {
38
+ providers: {
39
+ slots: [
40
+ { service: 'parking.payment', provider: 'wanda', implementation: 'wanda-parking@1', credentialType: 'wanda-c-signed-v1', credentialSchema: 'schemas/cred.json' },
41
+ { service: 'mall.info', provider: 'mock', implementation: 'mock-mall@1', credentialType: '-' },
42
+ ],
43
+ },
44
+ },
45
+ dependencies: { modules: [] },
46
+ env: [],
47
+ }),
48
+ );
49
+ writeAppFile(app.appDir, 'packages/provider-w/schemas/cred.json', JSON.stringify({ type: 'object', properties: { appKey: { type: 'string' } } }));
50
+ writeAppFile(
51
+ app.appDir,
52
+ 'config/integrations.json',
53
+ JSON.stringify({
54
+ provider: 'wanda',
55
+ credentialRef: 'secret://wanda-global',
56
+ instances: [
57
+ { id: 'mall-bj-01', name: '北京店' },
58
+ { id: 'mall-sh-01', name: '上海店' },
59
+ ],
60
+ credentialRefByInstance: { 'mall-sh-01': 'secret://wanda-mall-sh-01-global', 'ghost-id': 'secret://wanda-ghost' },
61
+ services: {
62
+ 'parking.payment': {
63
+ provider: 'wanda',
64
+ implementation: 'wanda-parking@1',
65
+ credentialRef: 'secret://wanda-parking.payment',
66
+ instances: { 'mall-bj-01': { credentialRef: 'secret://wanda-mall-bj-01-parking' }, '*': {} },
67
+ },
68
+ },
69
+ }),
70
+ );
71
+ writeAppFile(app.appDir, 'config/credentials/wanda-global.json', JSON.stringify({ type: 'wanda-c-signed-v1', appKey: 'KEY-GLOBAL', salt: 'SALT-GLOBAL-VALUE' }));
72
+ writeAppFile(app.appDir, 'config/credentials/wanda-mall-sh-01-global.json', JSON.stringify({ type: 'wanda-c-signed-v1', appKey: 'SH' }));
73
+ writeAppFile(app.appDir, 'config/credentials/wanda-parking.payment.json', JSON.stringify({ type: 'wanda-c-signed-v1', appKey: 'SVC-KEY' }));
74
+ // wanda-mall-bj-01-parking.json 故意缺席(文件缺失 → null——诊断归 issues #1)
75
+ return { app, tk: createAppToolkit(app.appDir) };
76
+ }
77
+
78
+ describe('v4.4 D33 凭据域(F6)', () => {
79
+ it('C1 键集三断言:app.byInstance = 注册表 ∩ 有 ref;services = 已配置;byInstance = 节点全键含 *', async () => {
80
+ const { app, tk } = credHarness();
81
+ const view = await tk.loadCredentials();
82
+ // app 本位 = wanda-global echo
83
+ expect(view.app.credential).toEqual({
84
+ type: 'wanda-c-signed-v1',
85
+ ref: 'secret://wanda-global',
86
+ file: 'config/credentials/wanda-global.json',
87
+ masked: { appKey: 'KE****', salt: 'SA****' },
88
+ });
89
+ // app.byInstance:mall-bj-01(无 ref)不出现;ghost-id(未注册 ref)跳过不虚报;mall-sh-01 在场
90
+ expect(Object.keys(view.app.byInstance)).toEqual(['mall-sh-01']);
91
+ expect(view.app.byInstance['mall-sh-01']?.ref).toBe('secret://wanda-mall-sh-01-global');
92
+ // services 键集 = 已配置服务;byInstance 键集 = 节点 instances map 全键(含 '*')
93
+ expect(Object.keys(view.services)).toEqual(['parking.payment']);
94
+ expect(Object.keys(view.services['parking.payment']!.byInstance).sort()).toEqual(['*', 'mall-bj-01']);
95
+ app.dispose();
96
+ });
97
+
98
+ it('C2 null 语义:ref 缺席/文件缺失 → null(未就绪——诊断归 issues #1 分工)', async () => {
99
+ const { app, tk } = credHarness();
100
+ const view = await tk.loadCredentials();
101
+ expect(view.services['parking.payment']!.credential).toEqual({
102
+ type: 'wanda-c-signed-v1',
103
+ ref: 'secret://wanda-parking.payment',
104
+ file: 'config/credentials/wanda-parking.payment.json',
105
+ masked: { appKey: 'SV****' },
106
+ });
107
+ expect(view.services['parking.payment']!.byInstance['*']).toBeNull(); // 无 ref → null
108
+ expect(view.services['parking.payment']!.byInstance['mall-bj-01']).toBeNull(); // ref 在场文件缺失 → null
109
+ app.dispose();
110
+ });
111
+
112
+ it('C3 零真值:序列化无明文;≤4 字符全掩码(掩码单源)', async () => {
113
+ const { app, tk } = credHarness();
114
+ const view = await tk.loadCredentials();
115
+ const serialized = JSON.stringify(view);
116
+ expect(serialized).not.toContain('KEY-GLOBAL');
117
+ expect(serialized).not.toContain('SALT-GLOBAL-VALUE');
118
+ expect(serialized).not.toContain('SVC-KEY');
119
+ expect(view.app.byInstance['mall-sh-01']?.masked.appKey).toBe('****'); // ≤4 字符全掩码
120
+ app.dispose();
121
+ });
122
+
123
+ it('C4 credential-types:全量清单 + ?provider= 过滤(未知 → 空不 404)+ schema 原文', async () => {
124
+ const { app, tk } = credHarness();
125
+ const all = await tk.loadCredentialTypes();
126
+ expect(all.types.map((t) => t.credentialType)).toEqual(['-', 'wanda-c-signed-v1']);
127
+ const wanda = await tk.loadCredentialTypes('wanda');
128
+ expect(wanda.types).toHaveLength(1);
129
+ expect(wanda.types[0]).toEqual({
130
+ credentialType: 'wanda-c-signed-v1',
131
+ owner: expect.any(String),
132
+ credentialSchema: { type: 'object', properties: { appKey: { type: 'string' } } },
133
+ });
134
+ expect((await tk.loadCredentialTypes('mock')).types.map((t) => t.credentialType)).toEqual(['-']);
135
+ expect((await tk.loadCredentialTypes('ghost')).types).toEqual([]);
136
+ app.dispose();
137
+ });
138
+
139
+ it('C5 删净断言:单体 instances[].credential 与组合单体 credentialSchema 全链不在场', async () => {
140
+ const { app, tk } = credHarness();
141
+ const detail = await tk.loadServiceResolution('parking.payment');
142
+ const inst = detail.resolution.instances.find((i) => i.instanceId === 'mall-bj-01');
143
+ // 假 resolve 仅读槽级 ref(简化级联)——effective.credentialRef = 槽级 secret://wanda-parking.payment
144
+ expect(inst?.effective?.credentialRef).toBe('secret://wanda-parking.payment');
145
+ expect(inst && 'credential' in inst).toBe(false);
146
+ expect('credential' in JSON.parse(JSON.stringify(detail.resolution))).toBe(false);
147
+ const combo = await tk.loadProviderService('wanda', 'parking.payment');
148
+ expect('credentialSchema' in combo.integrationSchemas).toBe(false);
149
+ expect(combo.implementations[0]?.credentialType).toBe('wanda-c-signed-v1'); // 寻址键保留
150
+ app.dispose();
151
+ });
152
+ });
package/tests/demo-app.ts CHANGED
@@ -48,15 +48,24 @@ export interface FakeContractsOptions {
48
48
  /** resolveEffectiveService 自定义函数体(FX-2c O4 fixture:默认恒 service-not-configured;
49
49
  * 入参 (integrations, service, instanceId, supplies),需 return EffectiveServiceResolution 形状) */
50
50
  resolveBody?: string;
51
+ /** service-slots.json 词汇(P9 身份判据/求值键集;缺省不物化 = 词汇空) */
52
+ slots?: string[];
53
+ /** zod 形拒绝 fixture(A1 T9/T10):parseIntegrationsConfig 恒抛带 issues 的结构化错误
54
+ * (结构性 ZodError 形——issues[].path 供 fields 断言;仅在专属 fixture 上使用) */
55
+ zodRejectPaths?: string[][];
51
56
  }
52
57
 
53
58
  /** 假 contracts 包:package.json exports 形态逐字段复刻发布面(types+import 双条件、无 require/default)
54
59
  * ——resolver 不经 exports 解析(包根直探),该形态为 E4 类错误的机械防线。 */
55
60
  export function writeFakeContractsPackage(packageRoot: string, opts: FakeContractsOptions = {}): void {
56
- const { version = '0.9.0', withStrict = true, withDist = true, brokenDist = false, missingEvaluationExports = false, rejectMarker, resolveBody } = opts;
61
+ const { version = '0.9.0', withStrict = true, withDist = true, brokenDist = false, missingEvaluationExports = false, rejectMarker, resolveBody, slots, zodRejectPaths } = opts;
57
62
  const strictBody = withStrict
58
63
  ? `export function parseIntegrationsConfig(raw) { ${
59
64
  rejectMarker ? `if (JSON.stringify(raw).includes(${JSON.stringify(rejectMarker)})) throw new Error("fake strict reject"); ` : ''
65
+ }${
66
+ zodRejectPaths
67
+ ? `throw Object.assign(new Error("fake zod reject"), { issues: ${JSON.stringify(zodRejectPaths.map((p) => ({ path: p })))} }); `
68
+ : ''
60
69
  }return raw; }`
61
70
  : '';
62
71
  const resolveLine = `export function resolveEffectiveService(integrations, service, instanceId, supplies) { ${
@@ -104,6 +113,10 @@ export function writeFakeContractsPackage(packageRoot: string, opts: FakeContrac
104
113
  mkdirSync(join(packageRoot, 'src'), { recursive: true });
105
114
  writeFileSync(join(packageRoot, 'src', 'runtime.ts'), exportsBody);
106
115
  }
116
+ // service-slots.json(P9 词汇源——探测链发现位 = 包根;缺省不物化 = 词汇空)
117
+ if (slots && slots.length > 0) {
118
+ writeFileSync(join(packageRoot, 'service-slots.json'), `${JSON.stringify({ slots }, null, 2)}\n`);
119
+ }
107
120
  }
108
121
 
109
122
  export interface LinkOptions {
@@ -180,7 +180,9 @@ describe('Freshness Contract W3/W7 — agent 动线矩阵(工厂方法级;FX
180
180
  }),
181
181
  );
182
182
  const modules = await tk.loadModules();
183
- expect(modules.modules.map((m) => m.id)).toContain('provider-x');
183
+ // provider 家族包不在清单(2026-09-16 bug-fix)——排除本身随目录新增即时生效;
184
+ // loadModules 新鲜度由下方 case c(module-sdk-y)承载,供给轴新鲜度见下两行
185
+ expect(modules.modules.map((m) => m.id)).not.toContain('provider-x');
184
186
  const providers = await tk.loadProviders();
185
187
  expect(providers.providers.map((p) => p.provider)).toContain('joycity');
186
188
 
@@ -6,7 +6,7 @@ import { describe, it, expect } from 'vitest';
6
6
  * (方法名断言依赖 factory 导出——届时 import { createAppToolkit } 逐行断言 typeof)。
7
7
  */
8
8
 
9
- /** 13 行映射表(api.md §8 单源;C4 补全为逐行断言) */
9
+ /** 15 行映射表(api.md §8 单源——v4.4 +GET /credentials、+GET /credential-types) */
10
10
  export const URL_METHOD_MAP: ReadonlyArray<{ method: string; url: string }> = [
11
11
  // ── 静态(7)──
12
12
  { method: 'loadModules', url: 'GET /modules' },
@@ -19,6 +19,9 @@ export const URL_METHOD_MAP: ReadonlyArray<{ method: string; url: string }> = [
19
19
  // ── 求值(2)──
20
20
  { method: 'loadServiceResolutions', url: 'GET /service-resolutions' },
21
21
  { method: 'loadServiceResolution', url: 'GET /service-resolutions/:service' },
22
+ // ── 凭据域(2;v4.4 D33)──
23
+ { method: 'loadCredentials', url: 'GET /credentials' },
24
+ { method: 'loadCredentialTypes', url: 'GET /credential-types' },
22
25
  // ── 写(4)──
23
26
  { method: 'writeAppIntegration', url: 'PUT /app/integration' },
24
27
  { method: 'writeModuleIntegration', url: 'PUT /modules/:module/integration' },
@@ -26,10 +29,10 @@ export const URL_METHOD_MAP: ReadonlyArray<{ method: string; url: string }> = [
26
29
  { method: 'deleteServiceIntegration', url: 'DELETE /services/:service/integration' },
27
30
  ];
28
31
 
29
- describe('naming-alignment(L1 映射表——C2 骨架)', () => {
30
- it('映射表骨架 = 13 行(静态 7 + 求值 2 + 写 4)——计数守卫', () => {
31
- expect(URL_METHOD_MAP).toHaveLength(13);
32
- expect(URL_METHOD_MAP.filter((r) => r.url.startsWith('GET '))).toHaveLength(9);
32
+ describe('naming-alignment(L1 映射表)', () => {
33
+ it('映射表 = 15 行(静态 7 + 求值 2 + 凭据域 2 + 写 4)——计数守卫(v4.4)', () => {
34
+ expect(URL_METHOD_MAP).toHaveLength(15);
35
+ expect(URL_METHOD_MAP.filter((r) => r.url.startsWith('GET '))).toHaveLength(11);
33
36
  expect(URL_METHOD_MAP.filter((r) => r.url.startsWith('PUT '))).toHaveLength(3);
34
37
  expect(URL_METHOD_MAP.filter((r) => r.url.startsWith('DELETE '))).toHaveLength(1);
35
38
  });
@@ -0,0 +1,101 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ /**
7
+ * 同仓关键词矩阵静态断言(v4.4 D32 配套——tbox-app 侧义务):
8
+ * 真实 vendor schema 文件(modules/<pkg>/schemas/**,config + credentials 双面)关键词
9
+ * ⊆ agtcodingbox SchemaForm v2 渲染器支持集(清单钉 module-tab-config-refinement Note
10
+ * 前端配套节:format/minLength/maxLength/additionalProperties/$schema 提示级不降级;
11
+ * oneOf/anyOf/$ref/items/patternProperties 只读降级 + 值保全)。
12
+ *
13
+ * 断言红 = 新 vendor schema 引入渲染器不支持的键 → 人工确认渲染器扩容或改 schema 后
14
+ * 同批更新 SUPPORTED(禁止静默扩清单——该断言是「表单引导能力」的tripwire)。
15
+ */
16
+
17
+ const SUPPORTED: ReadonlySet<string> = new Set([
18
+ // 结构基础(SchemaForm v2 全渲染)
19
+ 'type', 'properties', 'required', 'enum', 'title', 'description', 'default',
20
+ 'minimum', 'maximum', 'writeOnly',
21
+ // 提示级(note 扩容清单——不降级)
22
+ 'format', 'minLength', 'maxLength', 'additionalProperties', '$schema',
23
+ // 结构性(只读降级 + 值保全——支持集含之,出现不红)
24
+ 'oneOf', 'anyOf', '$ref', 'items', 'patternProperties',
25
+ ]);
26
+
27
+ /** 递归走已知 subschema 位,收集 schema 节点自身键(properties 值下的键 = 字段名,不属关键词) */
28
+ function collectKeywords(node: unknown, out: Set<string>): void {
29
+ if (Array.isArray(node)) {
30
+ for (const v of node) collectKeywords(v, out);
31
+ return;
32
+ }
33
+ if (!node || typeof node !== 'object') return;
34
+ for (const [k, v] of Object.entries(node as Record<string, unknown>)) {
35
+ out.add(k);
36
+ if (k === 'properties' || k === 'patternProperties') {
37
+ for (const sv of Object.values(v as Record<string, unknown>)) collectKeywords(sv, out);
38
+ } else if (k === 'items' && v !== null && typeof v === 'object') {
39
+ collectKeywords(v, out);
40
+ } else if ((k === 'oneOf' || k === 'anyOf' || k === 'allOf') && Array.isArray(v)) {
41
+ for (const sv of v) collectKeywords(sv, out);
42
+ } else if (k === 'additionalProperties' && v !== null && typeof v === 'object') {
43
+ collectKeywords(v, out);
44
+ } else if (k === '$defs' || k === 'definitions') {
45
+ for (const sv of Object.values(v as Record<string, unknown>)) collectKeywords(sv, out);
46
+ }
47
+ }
48
+ }
49
+
50
+ function schemaFiles(): Array<{ pkg: string; rel: string }> {
51
+ const modulesRoot = join(fileURLToPath(new URL('../../..', import.meta.url)), 'modules');
52
+ const out: Array<{ pkg: string; rel: string }> = [];
53
+ for (const pkg of readdirSync(modulesRoot)) {
54
+ const dir = join(modulesRoot, pkg, 'schemas');
55
+ if (!statSyncSafe(dir)) continue;
56
+ (function scan(d: string): void {
57
+ for (const e of readdirSync(d)) {
58
+ const p = join(d, e);
59
+ if (statSync(p).isDirectory()) scan(p);
60
+ else if (e.endsWith('.json')) out.push({ pkg, rel: p });
61
+ }
62
+ })(dir);
63
+ }
64
+ return out;
65
+ }
66
+
67
+ function statSyncSafe(p: string): boolean {
68
+ try {
69
+ return statSync(p).isDirectory();
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ describe('vendor schema 关键词矩阵 ⊆ 渲染器支持集(D32 表单引导 tripwire)', () => {
76
+ it('modules/**/schemas 全文件关键词 ⊆ SUPPORTED(红 = 渲染器扩容人工确认)', () => {
77
+ const files = schemaFiles();
78
+ expect(files.length, 'vendor schema 文件扫描面非空(防目录改名静默失效)').toBeGreaterThan(5);
79
+ for (const { pkg, rel } of files) {
80
+ const keywords = new Set<string>();
81
+ try {
82
+ collectKeywords(JSON.parse(readFileSync(rel, 'utf8')), keywords);
83
+ } catch (err) {
84
+ throw new Error(`vendor schema 解析失败(${pkg}):${(err as Error).message}`);
85
+ }
86
+ for (const k of keywords) {
87
+ expect(SUPPORTED.has(k), `${pkg} schema 关键词 "${k}" ∉ 渲染器支持集——确认 SchemaForm v2 扩容后同批更新本清单`).toBe(true);
88
+ }
89
+ }
90
+ });
91
+
92
+ it('实际用到的关键词 ⊆ SUPPORTED 且扫描面覆盖 config/credentials 双面(自检)', () => {
93
+ const files = schemaFiles();
94
+ const used = new Set<string>();
95
+ for (const { rel } of files) collectKeywords(JSON.parse(readFileSync(rel, 'utf8')), used);
96
+ // 实测基线(2026-09 扫描):14 键——防「提取器失效全空集」假绿
97
+ for (const k of ['$schema', 'type', 'properties', 'required', 'format', 'writeOnly', 'additionalProperties']) {
98
+ expect(used.has(k), `基线关键词 ${k} 未被提取到——提取器或目录布局变化,人工核对`).toBe(true);
99
+ }
100
+ });
101
+ });