@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.
- package/README.md +2 -2
- package/dist/{chunk-VV3ERZXS.js → chunk-KPD3LUH4.js} +1 -1
- package/dist/contracts-resolver-E4SECBOG.js +1 -0
- package/dist/index.d.ts +234 -43
- package/dist/index.js +8 -8
- package/package.json +2 -2
- package/src/assembly/app-manifest.ts +6 -0
- package/src/assembly/provider-catalog.ts +6 -1
- package/src/assembly/walk-manifests.ts +2 -1
- package/src/core/contracts-resolver.ts +3 -2
- package/src/core/credential-format.ts +4 -2
- package/src/core/errors.ts +9 -3
- package/src/dto.ts +127 -15
- package/src/factory.ts +129 -16
- package/src/index.ts +15 -4
- package/src/integrations/credentials.ts +1 -1
- package/src/integrations/domain-binding.ts +81 -0
- package/src/integrations/predicate.ts +7 -6
- package/src/integrations/read.ts +23 -3
- package/src/integrations/write.ts +93 -34
- package/src/views/app.ts +7 -5
- package/src/views/context.ts +3 -6
- package/src/views/credential-types.ts +28 -0
- package/src/views/credentials.ts +57 -0
- package/src/views/integration-schemas.ts +226 -0
- package/src/views/modules.ts +33 -14
- package/src/views/providers.ts +37 -22
- package/src/views/service-detail.ts +3 -4
- package/src/views/service-resolutions.ts +31 -25
- package/tests/credentials-view.test.ts +152 -0
- package/tests/demo-app.ts +14 -1
- package/tests/file-cache.test.ts +3 -1
- package/tests/naming-alignment.test.ts +8 -5
- package/tests/vendor-schema-keywords.test.ts +101 -0
- package/tests/views.test.ts +1285 -13
- package/tests/write-core.test.ts +107 -8
- package/dist/contracts-resolver-QM4FBQQV.js +0 -1
package/src/integrations/read.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { resolveContractsForApp, type ResolvedContracts } from '../core/contracts-resolver.js';
|
|
4
|
+
import { AppToolkitError } from '../core/errors.js';
|
|
4
5
|
import type { FileCache } from '../core/file-cache.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -45,9 +46,28 @@ export async function readIntegrationsStrict(
|
|
|
45
46
|
const raw = readIntegrationsConfig(appDir, cache);
|
|
46
47
|
if (raw === null) return { config: null, strict: false, contracts };
|
|
47
48
|
if (contracts.strictValidation && contracts.module.parseIntegrationsConfig) {
|
|
48
|
-
// 严格链:assertNoInlineSecrets → 旧键直切防线 → zod strict(contracts
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
// 严格链:assertNoInlineSecrets → 旧键直切防线 → zod strict(contracts 单源);
|
|
50
|
+
// 失败转换(A1——裸 ZodError/Error 穿透 500 的同族缺口):zod → 400 + fields(原始路径——
|
|
51
|
+
// 文件级读无节点上下文);普通 Error → 400 直通。结构性判定同 write.runStrictValidation。
|
|
52
|
+
try {
|
|
53
|
+
const parsed = contracts.module.parseIntegrationsConfig(raw);
|
|
54
|
+
return { config: parsed as unknown as Record<string, unknown>, strict: true, contracts };
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (err instanceof Error && Array.isArray((err as { issues?: unknown }).issues)) {
|
|
57
|
+
const fields = ((err as unknown as { issues: Array<{ path?: unknown }> }).issues)
|
|
58
|
+
.filter((i) => Array.isArray(i.path))
|
|
59
|
+
.map((i) => (i.path as Array<string | number>).map(String).join('.'))
|
|
60
|
+
.filter((p) => p.length > 0);
|
|
61
|
+
throw new AppToolkitError('VALIDATION_FAILED', 400, 'integrations.json 校验失败', fields.length > 0 ? fields : undefined);
|
|
62
|
+
}
|
|
63
|
+
throw new AppToolkitError('VALIDATION_FAILED', 400, err instanceof Error ? err.message : String(err));
|
|
64
|
+
}
|
|
51
65
|
}
|
|
52
66
|
return { config: raw, strict: false, contracts };
|
|
53
67
|
}
|
|
68
|
+
|
|
69
|
+
/** P9 服务身份判据单源(A2):GET/PUT/DELETE 共用「词汇 ∪ 已配置」——service-detail 判据逐字冻结
|
|
70
|
+
* (api.md P9 的「demanded 退化」口径为文档层 nuance,现行实现与 GET 均不含 demanded——冻结现状即单源) */
|
|
71
|
+
export function isKnownService(vocabulary: readonly string[], configuredKeys: readonly string[], service: string): boolean {
|
|
72
|
+
return vocabulary.includes(service) || configuredKeys.includes(service);
|
|
73
|
+
}
|
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import Ajv from 'ajv';
|
|
4
4
|
import { resolveContractsForApp } from '../core/contracts-resolver.js';
|
|
5
|
+
import { stemToFilePath } from '../core/credential-format.js';
|
|
5
6
|
import type { FileCache } from '../core/file-cache.js';
|
|
6
7
|
import { writeAtomic } from '../core/fskit.js';
|
|
7
8
|
import { SAFE_NAME_PATTERN } from '../core/module-schema.js';
|
|
@@ -17,6 +18,8 @@ import {
|
|
|
17
18
|
} from './credentials.js';
|
|
18
19
|
import type { IntegrationServiceIssue } from './predicate.js';
|
|
19
20
|
import { checkInstanceKeys } from './predicate.js';
|
|
21
|
+
import { isKnownService } from './read.js';
|
|
22
|
+
import { displayOwnerName, moduleDomainWritable } from './domain-binding.js';
|
|
20
23
|
import { AppToolkitError } from '../core/errors.js';
|
|
21
24
|
|
|
22
25
|
/**
|
|
@@ -34,8 +37,9 @@ import { AppToolkitError } from '../core/errors.js';
|
|
|
34
37
|
* 7 严格整文档校验(contracts parseIntegrationsConfig——触发 = !noDiff ∥ 有凭据输入)
|
|
35
38
|
* 8 凭据工件落盘(**后置于校验通过**——防「凭据已写 + zod 拒绝」不一致态;
|
|
36
39
|
* dryRun → writeCredentialArtifacts 零写盘 + existsSync 预览)
|
|
37
|
-
* 9 finalize
|
|
38
|
-
* restartScheduled = written && onApplied(凭据影响 boot
|
|
40
|
+
* 9 finalize(D35):planned = 将触碰(files 预览单源——dryRun 响应 ≡ 同 body 真实写,files 逐字对称);
|
|
41
|
+
* written = 实写(!dryRun 派生,D-B2);restartScheduled = written && onApplied(凭据影响 boot 装配——
|
|
42
|
+
* 重启正当);写后缓存失效
|
|
39
43
|
* 归一化:N1 模块节点 {} → 删 domains[键];N3 服务节点 {} 不归一化(显式通配激活 = 合法 TIER0 态);
|
|
40
44
|
* N2 已随 ?instance= 退役。dryRun(PUT only):复读完整管线不落盘不排程。
|
|
41
45
|
* DELETE:节点移除 + 孤儿凭据写后对账(覆盖 DELETE 与 PUT 替换两类来源,只报告不清理——D9)。
|
|
@@ -185,6 +189,11 @@ function validateCredential(
|
|
|
185
189
|
inlineCredentialType: string | undefined,
|
|
186
190
|
cache?: FileCache,
|
|
187
191
|
): void {
|
|
192
|
+
// ⓪ type 在场性(A1 无条件前置):缺 type 的凭据输入原会以 { type: undefined, ...values }
|
|
193
|
+
// 落盘(JSON.stringify 静默丢键)→ 无 type 文件 + GET 回显静默缺席——收紧为显式 400
|
|
194
|
+
if (typeof credentials.type !== 'string' || credentials.type.length === 0) {
|
|
195
|
+
throw new AppToolkitError('VALIDATION_FAILED', 400, '凭据 type 缺失或为空字符串', ['credentials.type']);
|
|
196
|
+
}
|
|
188
197
|
// ① type 匹配(内联声明优先;catalog 槽供给次之;均缺席 → 跳过——宽松与 P11 对齐)
|
|
189
198
|
const expectedType = inlineCredentialType ?? declaredCredentialType(appDir, provider, service, cache);
|
|
190
199
|
if (expectedType !== undefined && expectedType !== '-' && credentials.type !== expectedType) {
|
|
@@ -222,14 +231,12 @@ export interface WriteCore {
|
|
|
222
231
|
warmup(): Promise<void>;
|
|
223
232
|
/** PUT /app/integration(root 节点全集) */
|
|
224
233
|
applyApp(node: Record<string, unknown>, opts?: WriteOptions): SaveResult;
|
|
225
|
-
/** PUT /modules/:module/integration(domains 换算;N1 {}
|
|
234
|
+
/** PUT /modules/:module/integration(domains 换算;N1 {} 删域键;v4.4 D34 写守卫单源) */
|
|
226
235
|
applyModule(moduleId: string, node: Record<string, unknown>, opts?: WriteOptions): SaveResult;
|
|
227
|
-
/** PUT /services/:service/integration(整节点含 instances map;N3 {}
|
|
236
|
+
/** PUT /services/:service/integration(整节点含 instances map;N3 {} 不归一化;v4.4 A2 身份判据) */
|
|
228
237
|
applyService(service: string, node: Record<string, unknown>, opts?: WriteOptions): SaveResult;
|
|
229
|
-
/** DELETE /services/:service/integration
|
|
238
|
+
/** DELETE /services/:service/integration(恒整节点;幂等仅判据内成员;无预演) */
|
|
230
239
|
deleteService(service: string): SaveResult;
|
|
231
|
-
/** 模块服务域键(module → domains 键 = 服务集派生域键集 upsert 位) */
|
|
232
|
-
moduleDomainKey(moduleId: string): string | undefined;
|
|
233
240
|
}
|
|
234
241
|
|
|
235
242
|
export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): WriteCore {
|
|
@@ -240,11 +247,13 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
240
247
|
appliedCredentials: AppliedCredential[],
|
|
241
248
|
issues: IntegrationServiceIssue[],
|
|
242
249
|
dryRun: boolean | undefined,
|
|
243
|
-
credentialsWritten: boolean,
|
|
244
250
|
): SaveResult {
|
|
245
|
-
//
|
|
246
|
-
// restartScheduled 跟随 written(凭据文件影响 boot 装配——重启正当)
|
|
247
|
-
const
|
|
251
|
+
// D35 双轴派生:planned = 将触碰(files 预览单源——dryRun 响应 ≡ 同 body 真实写,files 逐字对称);
|
|
252
|
+
// written = 实际落盘(!dryRun 派生——D-B2);restartScheduled 跟随 written(凭据文件影响 boot 装配——重启正当)
|
|
253
|
+
const integrationsPlanned = next !== null;
|
|
254
|
+
const credentialsPlanned = appliedCredentials.length > 0;
|
|
255
|
+
const integrationsWritten = !dryRun && integrationsPlanned;
|
|
256
|
+
const credentialsWritten = !dryRun && credentialsPlanned;
|
|
248
257
|
if (integrationsWritten) {
|
|
249
258
|
writeAtomic(join(appDir, INTEGRATIONS_FILE), `${JSON.stringify(next, null, 2)}\n`);
|
|
250
259
|
cache?.invalidate([join(appDir, INTEGRATIONS_FILE)]);
|
|
@@ -253,8 +262,8 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
253
262
|
const restartScheduled = written && onApplied !== undefined;
|
|
254
263
|
if (restartScheduled) onApplied?.();
|
|
255
264
|
const files: string[] = [];
|
|
256
|
-
if (
|
|
257
|
-
if (
|
|
265
|
+
if (integrationsPlanned) files.push(INTEGRATIONS_FILE);
|
|
266
|
+
if (credentialsPlanned) files.push(...appliedCredentials.map((a) => stemToFilePath(a.stem)));
|
|
258
267
|
return {
|
|
259
268
|
written,
|
|
260
269
|
restartScheduled,
|
|
@@ -270,7 +279,23 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
270
279
|
strictContracts = await resolveContractsForApp(appDir);
|
|
271
280
|
}
|
|
272
281
|
|
|
273
|
-
|
|
282
|
+
/** zod issues → 节点相对 fields(A1):字面量前缀剥离——service 名含点号,禁按段数切分;
|
|
283
|
+
* 无前缀命中的文档级路径(如 'services')原样保留;空 path(文档根级形态)→ 滤除
|
|
284
|
+
* (fields 直指字段名——空串指向不存在)。 */
|
|
285
|
+
function toNodeFields(issues: Array<{ path?: unknown }>, prefix: string): string[] {
|
|
286
|
+
return issues
|
|
287
|
+
.filter((i) => Array.isArray(i.path))
|
|
288
|
+
.map((i) => {
|
|
289
|
+
const p = (i.path as Array<string | number>).map(String).join('.');
|
|
290
|
+
return prefix !== '' && p.startsWith(prefix) && p.length > prefix.length ? p.slice(prefix.length) : p;
|
|
291
|
+
})
|
|
292
|
+
.filter((p) => p.length > 0);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** 严格整文档校验(A1 单点转换):zod → 400 + fields(节点相对);普通 Error(旧键直切/
|
|
296
|
+
* 内联密钥防线——contracts parseIntegrationsConfig 内部 assert 族)→ 同族 400 message 直通。
|
|
297
|
+
* 结构性 ZodError 判定(禁 instanceof——contracts 动态 import,类身份跨模块实例不可靠)。 */
|
|
298
|
+
function runStrictValidation(next: Record<string, unknown>, issues: IntegrationServiceIssue[], prefix: string): void {
|
|
274
299
|
const strict = strictContracts;
|
|
275
300
|
if (!strict || !strict.strictValidation || !strict.module.parseIntegrationsConfig) {
|
|
276
301
|
throw new AppToolkitError(
|
|
@@ -280,20 +305,23 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
280
305
|
);
|
|
281
306
|
}
|
|
282
307
|
void issues;
|
|
283
|
-
|
|
308
|
+
try {
|
|
309
|
+
strict.module.parseIntegrationsConfig(next);
|
|
310
|
+
} catch (err) {
|
|
311
|
+
if (err instanceof Error && Array.isArray((err as { issues?: unknown }).issues)) {
|
|
312
|
+
throw new AppToolkitError(
|
|
313
|
+
'VALIDATION_FAILED',
|
|
314
|
+
400,
|
|
315
|
+
'节点 schema 校验失败',
|
|
316
|
+
toNodeFields((err as unknown as { issues: Array<{ path?: unknown }> }).issues, prefix),
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
throw new AppToolkitError('VALIDATION_FAILED', 400, err instanceof Error ? err.message : String(err));
|
|
320
|
+
}
|
|
284
321
|
}
|
|
285
322
|
|
|
286
323
|
return {
|
|
287
324
|
warmup,
|
|
288
|
-
moduleDomainKey(moduleId: string): string | undefined {
|
|
289
|
-
const { modules } = walkManifests(appDir, cache);
|
|
290
|
-
const mod = modules.find((m) => m.id === moduleId);
|
|
291
|
-
if (!mod) return undefined;
|
|
292
|
-
const services = mod.descriptor.contributes.services ?? [];
|
|
293
|
-
const domains = new Set(services.map((s) => (s.service.includes('.') ? s.service.slice(0, s.service.indexOf('.')) : s.service)));
|
|
294
|
-
// 单域模块 → 该域键;多域/零域 → 模块 id 兜底键(domains 换算层约定)
|
|
295
|
-
return domains.size === 1 ? [...domains][0] : moduleId;
|
|
296
|
-
},
|
|
297
325
|
|
|
298
326
|
applyApp(node: Record<string, unknown>, writeOpts: WriteOptions = {}): SaveResult {
|
|
299
327
|
assertSafeNames(node, 'root');
|
|
@@ -332,16 +360,43 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
332
360
|
if (noDiff && !hasCredentialInput) {
|
|
333
361
|
return { written: false, restartScheduled: false, files: [], credentials: [], issues };
|
|
334
362
|
}
|
|
335
|
-
runStrictValidation(next, issues);
|
|
363
|
+
runStrictValidation(next, issues, '');
|
|
336
364
|
// 凭据工件落盘(后置于校验通过;dryRun 零写盘 + existsSync 预览)
|
|
337
365
|
const dryRun = writeOpts.dryRun === true;
|
|
338
366
|
const applied = writeCredentialArtifacts(appDir, plan, dryRun);
|
|
339
367
|
issues.push(...registryCheck(next));
|
|
340
|
-
return finalize(noDiff ? null : next, applied, issues, writeOpts.dryRun
|
|
368
|
+
return finalize(noDiff ? null : next, applied, issues, writeOpts.dryRun);
|
|
341
369
|
},
|
|
342
370
|
|
|
343
371
|
applyModule(moduleId: string, node: Record<string, unknown>, writeOpts: WriteOptions = {}): SaveResult {
|
|
344
372
|
assertSafeNames(node, 'root');
|
|
373
|
+
// D34 写守卫(v4.4,moduleDomainWritable 单源):404(模块在册)→ 400(多域/零域/共享键
|
|
374
|
+
// 三分支)→ 既有管线。守卫先于 no-op 短路——多域/零域/共享键模块 PUT {} 清域同 400
|
|
375
|
+
// (「写路径拒绝歧义操作」含清除;P13/P17)。
|
|
376
|
+
const { modules, catalog } = walkManifests(appDir, cache);
|
|
377
|
+
const mod = modules.find((m) => m.id === moduleId);
|
|
378
|
+
if (!mod) {
|
|
379
|
+
throw new AppToolkitError('MODULE_NOT_FOUND', 404, `模块 ${moduleId} 非已装模块`);
|
|
380
|
+
}
|
|
381
|
+
const w = moduleDomainWritable({ modules, catalog }, mod);
|
|
382
|
+
if (!w.writable) {
|
|
383
|
+
if (w.reason === 'multi-domain') {
|
|
384
|
+
throw new AppToolkitError(
|
|
385
|
+
'VALIDATION_FAILED',
|
|
386
|
+
400,
|
|
387
|
+
`模块 ${moduleId} 跨 ${w.domainKeys.length} 个域(${w.domainKeys.join('、')})——模块级统一配置仅支持单域模块,请按服务级配置(编辑各服务行)`,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
if (w.reason === 'zero-domain') {
|
|
391
|
+
throw new AppToolkitError('VALIDATION_FAILED', 400, `模块 ${moduleId} 无服务需求声明,无域配置位`);
|
|
392
|
+
}
|
|
393
|
+
throw new AppToolkitError(
|
|
394
|
+
'VALIDATION_FAILED',
|
|
395
|
+
400,
|
|
396
|
+
`域键 ${w.domainKey} 由 ${w.coOwners.map((o) => displayOwnerName(modules, o)).join('、')} 共用——写通道拒绝歧义操作,请按服务级配置`,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
const domainKey = w.domainKey;
|
|
345
400
|
const issues: IntegrationServiceIssue[] = [];
|
|
346
401
|
const current = loadConfig(appDir, cache);
|
|
347
402
|
if (current === null && Object.keys(node).length === 0) {
|
|
@@ -356,10 +411,6 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
356
411
|
});
|
|
357
412
|
}
|
|
358
413
|
const base: Record<string, unknown> = current ?? { services: {} };
|
|
359
|
-
const domainKey = this.moduleDomainKey(moduleId);
|
|
360
|
-
if (domainKey === undefined) {
|
|
361
|
-
throw new AppToolkitError('MODULE_NOT_FOUND', 404, `模块 ${moduleId} 非已装模块`);
|
|
362
|
-
}
|
|
363
414
|
const domains = { ...((base.domains as Record<string, unknown>) ?? {}) };
|
|
364
415
|
const next: Record<string, unknown> = { ...base, domains };
|
|
365
416
|
// N1:模块节点 {} → 删 domains[键](清域);其余 = 域键全量替换
|
|
@@ -368,9 +419,9 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
368
419
|
if (deepEqual(current, next)) {
|
|
369
420
|
return { written: false, restartScheduled: false, files: [], credentials: [], issues };
|
|
370
421
|
}
|
|
371
|
-
runStrictValidation(next, issues);
|
|
422
|
+
runStrictValidation(next, issues, `domains.${domainKey}.`);
|
|
372
423
|
issues.push(...registryCheck(next));
|
|
373
|
-
return finalize(next, [], issues, writeOpts.dryRun
|
|
424
|
+
return finalize(next, [], issues, writeOpts.dryRun);
|
|
374
425
|
},
|
|
375
426
|
|
|
376
427
|
applyService(service: string, node: Record<string, unknown>, writeOpts: WriteOptions = {}): SaveResult {
|
|
@@ -378,6 +429,10 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
378
429
|
assertSafeInstanceKeys(node, writeOpts, 'slot');
|
|
379
430
|
const issues: IntegrationServiceIssue[] = [];
|
|
380
431
|
const current = loadConfig(appDir, cache);
|
|
432
|
+
// P9 服务身份判据单源(A2):写入集 = 词汇 ∪ 已配置——未知服务(含 typo)404(api.md §5)
|
|
433
|
+
if (!isKnownService(walkManifests(appDir, cache).vocabulary, Object.keys((current?.services as Record<string, unknown> | undefined) ?? {}), service)) {
|
|
434
|
+
throw new AppToolkitError('SERVICE_NOT_FOUND', 404, `服务 ${service} 不在服务词汇(已装契约包 ∪ 已配置)`);
|
|
435
|
+
}
|
|
381
436
|
if (current === null) {
|
|
382
437
|
issues.push({
|
|
383
438
|
severity: 'warning',
|
|
@@ -418,17 +473,21 @@ export function createWriteCore(appDir: string, opts: WriteCoreOptions = {}): Wr
|
|
|
418
473
|
if (noDiff && !hasCredentialInput) {
|
|
419
474
|
return { written: false, restartScheduled: false, files: [], credentials: [], issues };
|
|
420
475
|
}
|
|
421
|
-
runStrictValidation(next, issues);
|
|
476
|
+
runStrictValidation(next, issues, `services.${service}.`);
|
|
422
477
|
// 凭据工件落盘(后置于校验通过;dryRun 零写盘 + existsSync 预览)
|
|
423
478
|
const dryRun = writeOpts.dryRun === true;
|
|
424
479
|
const applied = writeCredentialArtifacts(appDir, plan, dryRun);
|
|
425
480
|
issues.push(...registryCheck(next));
|
|
426
|
-
return finalize(noDiff ? null : next, applied, issues, writeOpts.dryRun
|
|
481
|
+
return finalize(noDiff ? null : next, applied, issues, writeOpts.dryRun);
|
|
427
482
|
},
|
|
428
483
|
|
|
429
484
|
deleteService(service: string): SaveResult {
|
|
430
485
|
const issues: IntegrationServiceIssue[] = [];
|
|
431
486
|
const current = loadConfig(appDir, cache);
|
|
487
|
+
// P9 服务身份判据单源(A2):判据外未知服务 404;判据内缺席节点维持幂等 written:false
|
|
488
|
+
if (!isKnownService(walkManifests(appDir, cache).vocabulary, Object.keys((current?.services as Record<string, unknown> | undefined) ?? {}), service)) {
|
|
489
|
+
throw new AppToolkitError('SERVICE_NOT_FOUND', 404, `服务 ${service} 不在服务词汇(已装契约包 ∪ 已配置)`);
|
|
490
|
+
}
|
|
432
491
|
const services = { ...((current?.services as Record<string, unknown>) ?? {}) };
|
|
433
492
|
if (!(service in services)) {
|
|
434
493
|
// 幂等:清不存在的节点 → written:false(无 409;无预演)
|
package/src/views/app.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { AppView } from '../dto.js';
|
|
1
|
+
import type { AppView, ServiceDemand } from '../dto.js';
|
|
2
2
|
import { APP_INTEGRATION_NODE_KEYS } from '../dto.js';
|
|
3
3
|
import { moduleOccupiedServices, type ViewSnapshot } from './context.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* 应用静态投影(views/app;C4;FX-1c B10——root
|
|
7
|
-
* /app = 平台服务 + root
|
|
8
|
-
*
|
|
6
|
+
* 应用静态投影(views/app;C4;FX-1c B10——root 节点投影修;v4.4 D29——services 双端点同型)。
|
|
7
|
+
* /app = 平台服务 + root 节点原值(纯静态):services = 服务词汇 − 模块占用
|
|
8
|
+
* (ServiceDemand[]——平台条目 required 恒 false、title/description 缺席,D29);
|
|
9
9
|
* 平台服务状态不在此(→ /service-resolutions);模块服务不在此(→ /modules/:m)。
|
|
10
10
|
* integration = **root 节点投影**(APP_INTEGRATION_NODE_KEYS——D21/L1:GET 预填 ≡ PUT body;
|
|
11
11
|
* 文件另含 services/domains/modules 域键,不进 root 节点投影——原实现整文件透传,
|
|
@@ -14,7 +14,9 @@ import { moduleOccupiedServices, type ViewSnapshot } from './context.js';
|
|
|
14
14
|
|
|
15
15
|
export function loadAppView(snapshot: ViewSnapshot): AppView {
|
|
16
16
|
const occupied = moduleOccupiedServices(snapshot.walk);
|
|
17
|
-
const services = snapshot.walk.vocabulary
|
|
17
|
+
const services: ServiceDemand[] = snapshot.walk.vocabulary
|
|
18
|
+
.filter((s) => !occupied.has(s))
|
|
19
|
+
.map((s) => ({ service: s, required: false }));
|
|
18
20
|
const config = snapshot.integrations;
|
|
19
21
|
let integration: AppView['integration'] = null;
|
|
20
22
|
if (config) {
|
package/src/views/context.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { readIntegrationsConfig } from '../integrations/read.js';
|
|
2
|
+
import { domainOfService } from '../integrations/domain-binding.js';
|
|
2
3
|
import { walkManifests, type WalkCatalog, type WalkManifestsResult } from '../assembly/walk-manifests.js';
|
|
3
4
|
import type { FileCache } from '../core/file-cache.js';
|
|
4
5
|
import type { ResolvedContracts } from '../core/contracts-resolver.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* 视图装配共享上下文(五投影共用;目录枚举零缓存——walkManifests 每次调用执行)。
|
|
8
|
-
* 域前缀换算(service →
|
|
9
|
+
* 域前缀换算(service → 首点段)自 v4.4 F1 迁 integrations/domain-binding.ts(本文件 re-export 保导入路径)。
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
export interface ViewContext {
|
|
@@ -21,11 +22,7 @@ export interface ViewSnapshot {
|
|
|
21
22
|
contracts: ResolvedContracts;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
export function domainOfService(service: string): string {
|
|
26
|
-
const i = service.indexOf('.');
|
|
27
|
-
return i === -1 ? service : service.slice(0, i);
|
|
28
|
-
}
|
|
25
|
+
export { domainOfService };
|
|
29
26
|
|
|
30
27
|
/** 读取视图输入快照(每方法调用独立——Freshness Contract 单位 = 方法调用) */
|
|
31
28
|
export async function loadViewSnapshot(ctx: ViewContext): Promise<ViewSnapshot> {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CredentialTypesView } from '../dto.js';
|
|
2
|
+
import type { ViewSnapshot } from './context.js';
|
|
3
|
+
import { readSchemaRef } from './providers.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 供给轴凭据结构清单(views/credential-types;v4.4 D33 独立端点 GET /credential-types[?provider=]):
|
|
7
|
+
* 结构按 credentialType 键(wanda 全部槽共享同一 type 同一 schema 文件——(provider, service)
|
|
8
|
+
* 组合寻址是错位键);源 = catalog.credentialTypes(既有聚合产物零新读取路径,聚合条目携
|
|
9
|
+
* packageDir 供 schema 文件解析)。静态轴(稳定可缓存、永不 503)。
|
|
10
|
+
* ?provider= 过滤 ≡ ?service= 先例(过滤非寻址;未知 provider → 空 types 不 404)。
|
|
11
|
+
* '-' = 无凭据语义(mock/local——UI 判 '-' 不渲染凭据表单);credentialSchema null = 未声明。
|
|
12
|
+
*/
|
|
13
|
+
export function loadCredentialTypesView(snapshot: ViewSnapshot, provider?: string): CredentialTypesView {
|
|
14
|
+
const types =
|
|
15
|
+
provider === undefined
|
|
16
|
+
? Object.keys(snapshot.catalog.credentialTypes)
|
|
17
|
+
: [...new Set(Object.values(snapshot.catalog.providers[provider] ?? {}).map((e) => e.credentialType))];
|
|
18
|
+
return {
|
|
19
|
+
types: types.sort().map((t) => {
|
|
20
|
+
const entry = snapshot.catalog.credentialTypes[t];
|
|
21
|
+
return {
|
|
22
|
+
credentialType: t,
|
|
23
|
+
owner: entry?.owner ?? '',
|
|
24
|
+
credentialSchema: entry ? readSchemaRef(snapshot.appDir, entry.packageDir, entry.credentialSchema) : null,
|
|
25
|
+
};
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { CredentialBindingEcho, CredentialEcho, CredentialsView } from '../dto.js';
|
|
2
|
+
import { maskCredentialValues, readCredentialFile, stemToFilePath } from '../core/credential-format.js';
|
|
3
|
+
import type { ViewSnapshot } from './context.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 凭据状态轴投影(views/credentials;v4.4 D33 独立端点 GET /credentials):
|
|
7
|
+
* 绑定位全量掩码回显——应用状态轴(随配置失效、零求值零 contracts、永不 503)。
|
|
8
|
+
* GET 永无真值(仅 ref/masked);掩码单源 maskCredentialValues;echoForRef 自
|
|
9
|
+
* service-resolutions 迁入共享(求值单体 instances[].credential 已删净——求值路径零凭据 I/O)。
|
|
10
|
+
*
|
|
11
|
+
* 键集 = 文件语义忠实镜像(api.md §0「键域不对称」):
|
|
12
|
+
* app.byInstance = 注册表 ids ∩ 有 ref(无 ref 的注册实例不出现;未注册 ref 跳过不虚报)
|
|
13
|
+
* services[s].byInstance = 节点 instances map 全键(含 '*',无 ref 亦在场 → null)
|
|
14
|
+
* null 语义:credential/byInstance 值 = 「未就绪」(ref 缺席或文件缺失)——缺失诊断归谓词 #1
|
|
15
|
+
* (CREDENTIAL_FILE_MISSING),echo = 状态回显、issues = 诊断定位(P16 分工)。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** 凭据文件掩码回显(ref 非 secret:// 或文件缺席 → undefined——调用方映射 null) */
|
|
19
|
+
export function echoForRef(appDir: string, ref: string | undefined): CredentialEcho | undefined {
|
|
20
|
+
if (ref === undefined || !ref.startsWith('secret://')) return undefined;
|
|
21
|
+
const stem = ref.slice('secret://'.length);
|
|
22
|
+
const file = readCredentialFile(appDir, stem);
|
|
23
|
+
if (!file) return undefined;
|
|
24
|
+
return { type: file.type, ref, file: stemToFilePath(stem), masked: maskCredentialValues(file.values) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function loadCredentialsView(snapshot: ViewSnapshot): CredentialsView {
|
|
28
|
+
const cfg = snapshot.integrations as {
|
|
29
|
+
credentialRef?: unknown;
|
|
30
|
+
credentialRefByInstance?: Record<string, string>;
|
|
31
|
+
instances?: Array<{ id: string }>;
|
|
32
|
+
services?: Record<string, { credentialRef?: string; instances?: Record<string, { credentialRef?: string }> }>;
|
|
33
|
+
} | null;
|
|
34
|
+
|
|
35
|
+
const app: CredentialBindingEcho = {
|
|
36
|
+
credential: echoForRef(snapshot.appDir, typeof cfg?.credentialRef === 'string' ? cfg.credentialRef : undefined) ?? null,
|
|
37
|
+
byInstance: {},
|
|
38
|
+
};
|
|
39
|
+
for (const { id } of cfg?.instances ?? []) {
|
|
40
|
+
const ref = cfg?.credentialRefByInstance?.[id];
|
|
41
|
+
if (ref === undefined) continue; // 无 ref 的注册实例不出现(api.md §4.9a 实证)
|
|
42
|
+
app.byInstance[id] = echoForRef(snapshot.appDir, ref) ?? null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const services: Record<string, CredentialBindingEcho> = {};
|
|
46
|
+
for (const [s, node] of Object.entries(cfg?.services ?? {})) {
|
|
47
|
+
const slot: CredentialBindingEcho = {
|
|
48
|
+
credential: echoForRef(snapshot.appDir, node.credentialRef) ?? null,
|
|
49
|
+
byInstance: {},
|
|
50
|
+
};
|
|
51
|
+
for (const [k, inst] of Object.entries(node.instances ?? {})) {
|
|
52
|
+
slot.byInstance[k] = echoForRef(snapshot.appDir, inst.credentialRef) ?? null;
|
|
53
|
+
}
|
|
54
|
+
services[s] = slot;
|
|
55
|
+
}
|
|
56
|
+
return { app, services };
|
|
57
|
+
}
|