@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
@@ -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
- const parsed = contracts.module.parseIntegrationsConfig(raw);
50
- return { config: parsed as unknown as Record<string, unknown>, strict: true, contracts };
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:written = integrations 实写 凭据实写(**D-B2**);files 分列两类路径;
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
- // D-B2:written 任一文件实写;files 分列(integrations.json + 凭据路径);
246
- // restartScheduled 跟随 written(凭据文件影响 boot 装配——重启正当)
247
- const integrationsWritten = !dryRun && next !== null;
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 (integrationsWritten) files.push(INTEGRATIONS_FILE);
257
- if (credentialsWritten) files.push(...appliedCredentials.map((a) => `config/credentials/${a.stem}.json`));
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
- function runStrictValidation(next: Record<string, unknown>, issues: IntegrationServiceIssue[]): void {
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
- strict.module.parseIntegrationsConfig(next);
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, !dryRun && plan.length > 0);
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, false);
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, !dryRun && plan.length > 0);
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 节点原值(纯静态,D12):
8
- * services = 服务词汇 模块占用(裸 string[]——owner/标题 UI 按首点段派生,F2);
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.filter((s) => !occupied.has(s));
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) {
@@ -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 → 首点段)= 本地一行 helper(文件格式词汇,静态面零 contracts 依赖)。
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
- /** 域前缀换算(本地 helper——静态面零 contracts 依赖前提) */
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
+ }