omc-core 0.0.0-stage → 0.4.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 (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +13 -3
  3. package/dist/application-discovery.js +107 -0
  4. package/dist/approval-frame-readability.js +317 -0
  5. package/dist/approval-protocol.js +143 -0
  6. package/dist/autonomous-delivery-demonstration-host.js +72 -0
  7. package/dist/autonomous-delivery-demonstration.js +153 -0
  8. package/dist/b1-dead-runner-contract.js +284 -0
  9. package/dist/b2-extension-contract.js +89 -0
  10. package/dist/backend-extension-release.js +295 -0
  11. package/dist/bizsheet-html-elements.js +57 -0
  12. package/dist/bizsheet-online-js.js +162 -0
  13. package/dist/bootstrap-lifecycle.js +562 -0
  14. package/dist/bootstrap-plan-registry.js +218 -0
  15. package/dist/business-rule-integration.js +169 -0
  16. package/dist/businessrule-contract.js +322 -0
  17. package/dist/capability-catalog.js +133 -0
  18. package/dist/change-workflow.js +336 -0
  19. package/dist/childtable-field-contract.js +144 -0
  20. package/dist/childtable-live-runner-contract.js +79 -0
  21. package/dist/childtable-sheet201-contract.js +104 -0
  22. package/dist/cloudpivot-86-rule-integration-knowledge-pack.js +92 -0
  23. package/dist/cloudpivot-adapter.js +250 -0
  24. package/dist/cloudpivot-auth.js +378 -0
  25. package/dist/cloudpivot-business-object-acceptance.js +179 -0
  26. package/dist/cloudpivot-capability-builders.js +118 -0
  27. package/dist/cloudpivot-fingerprints.js +73 -0
  28. package/dist/cloudpivot-frontend-knowledge-pack.js +145 -0
  29. package/dist/cloudpivot-low-code.js +828 -0
  30. package/dist/cloudpivot-native-configuration.js +384 -0
  31. package/dist/cloudpivot-native-lifecycle-contract.js +45 -0
  32. package/dist/cloudpivot-online-js.js +92 -0
  33. package/dist/cloudpivot-operation-seam.js +190 -0
  34. package/dist/compatibility-assessment.js +124 -0
  35. package/dist/config-only-runtime-validation-host.js +14 -0
  36. package/dist/config-only-runtime-validation.js +157 -0
  37. package/dist/configuration-handoff.js +112 -0
  38. package/dist/configuration-intent.js +83 -0
  39. package/dist/controlled-executor.js +98 -0
  40. package/dist/cpharness-backend.js +68 -0
  41. package/dist/cpharness-contracts.js +18 -0
  42. package/dist/cpharness-extension-package-builder.js +67 -0
  43. package/dist/dbsweep.js +88 -0
  44. package/dist/delivery-child-agent.js +45 -0
  45. package/dist/delivery-graph.js +402 -0
  46. package/dist/dsh-history.js +199 -0
  47. package/dist/dsh-rpc.js +209 -0
  48. package/dist/dsh-run.js +111 -0
  49. package/dist/end-to-end-virtual-delivery.js +141 -0
  50. package/dist/environment-release-freeze.js +140 -0
  51. package/dist/families/address-attachment.js +128 -0
  52. package/dist/families/date-time.js +121 -0
  53. package/dist/families/dictionary-enum.js +30 -0
  54. package/dist/families/numeric-logical.js +113 -0
  55. package/dist/families/people-department.js +247 -0
  56. package/dist/families/registry.js +192 -0
  57. package/dist/families/relevance.js +183 -0
  58. package/dist/families/text-extension.js +116 -0
  59. package/dist/families/types.js +16 -0
  60. package/dist/field-live-host-tools.js +170 -0
  61. package/dist/field-probe-runner-contract.js +92 -0
  62. package/dist/formal-capability-discovery.js +162 -0
  63. package/dist/formula-controls-contract.js +103 -0
  64. package/dist/formula-field-contract.js +147 -0
  65. package/dist/formula-live-runner-contract.js +109 -0
  66. package/dist/full-capability-test-harness.js +95 -0
  67. package/dist/host-operations.js +145 -0
  68. package/dist/host-package-registry.js +422 -0
  69. package/dist/index.js +81 -0
  70. package/dist/knowledge-pack-builder.js +104 -0
  71. package/dist/knowledge-pack-runtime.js +94 -0
  72. package/dist/launcher-package-adapter.js +50 -0
  73. package/dist/local-online-js-draft.js +148 -0
  74. package/dist/m2-data-entry-contract.js +18 -0
  75. package/dist/master-plan-approval.js +443 -0
  76. package/dist/omc-planning.js +146 -0
  77. package/dist/omc-review.js +24 -0
  78. package/dist/omc-validation.js +25 -0
  79. package/dist/online-js-governance-contract.js +74 -0
  80. package/dist/online-js-package-adapter.js +77 -0
  81. package/dist/online-js-promotion.js +72 -0
  82. package/dist/ordinary-synthesis-router.js +123 -0
  83. package/dist/orphan-cleanup.js +113 -0
  84. package/dist/output-declaration-e2e-scenario.js +84 -0
  85. package/dist/permission-product-crud-capabilities.js +155 -0
  86. package/dist/project-output-declaration-intent.js +107 -0
  87. package/dist/release-knowledge-pack-adapter.js +66 -0
  88. package/dist/representative-acceptance-direct-rpc.js +39 -0
  89. package/dist/runtime-acceptance.js +217 -0
  90. package/dist/s28-s29-static-contract.js +40 -0
  91. package/dist/s29-workflow-greenfield-contract.js +73 -0
  92. package/dist/s32-restful-contract.js +41 -0
  93. package/dist/scout-accelerator.js +69 -0
  94. package/dist/synthesis-delivery-request.js +128 -0
  95. package/dist/ticket02-direct-rpc.js +80 -0
  96. package/dist/ticket02-evidence.js +59 -0
  97. package/dist/ticket02-implementation-delegation.js +70 -0
  98. package/dist/ticket02-model-route.js +107 -0
  99. package/dist/ticket02-native-adapter.js +195 -0
  100. package/dist/ticket02-session-reporting.js +108 -0
  101. package/dist/ticket02-workflow-validation.js +67 -0
  102. package/dist/ticket03-background-delegation.js +248 -0
  103. package/dist/ticket03-form-adapter.js +116 -0
  104. package/dist/ticket03-native-adapter.js +110 -0
  105. package/dist/ticket03-native-dsh.js +593 -0
  106. package/dist/ticket04-background-task-details.js +74 -0
  107. package/dist/ticket04-local-delivery.js +242 -0
  108. package/dist/ticket05-backend-extension-contract.js +42 -0
  109. package/dist/ticket05-parallel-worktrees.js +78 -0
  110. package/dist/ticket06-mixed-read-only-delivery.js +448 -0
  111. package/dist/ticket07-environments-approvals-delivery.js +55 -0
  112. package/dist/ticket08-local-vertical-slice.js +61 -0
  113. package/dist/ticket08-mixed-requirement-acceptance.js +70 -0
  114. package/dist/ticket08-native-contract-host.js +50 -0
  115. package/dist/ticket08-native-contract.js +142 -0
  116. package/dist/ticket08-test-model-mutation.js +901 -0
  117. package/dist/ticket09-test-field-mutation.js +1026 -0
  118. package/dist/ticket10-test-field-publish.js +886 -0
  119. package/dist/ticket11-test-form-draft.js +1014 -0
  120. package/dist/ticket12-test-form-publish.js +917 -0
  121. package/dist/ticket13-full-ladder.js +485 -0
  122. package/dist/ticket20-test-data-rule.js +780 -0
  123. package/dist/two-stage-change-package.js +486 -0
  124. package/dist/workflow-rule-integration-adapters.js +187 -0
  125. package/package.json +38 -6
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Oh-My-CpAgent contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,13 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # omc-core
2
+
3
+ OMC(Oh-My-CpAgent)配置能力族的共享内核:能力契约、治理原语(审批协议、受控执行器、
4
+ 写租约)、CloudPivot wire 解码器,以及字段/模型/表单的契约阶梯。
5
+
6
+ 硬性保证:
7
+
8
+ - **零 DSH 依赖。** `omc-core` 从不 import `@deepseek-ai/dsh-*`。由
9
+ `scripts/check-zero-dsh.mjs` 强制(已接入 CI;违反即为红色)。
10
+ - **宿主无关**:没有宿主插件、没有 profile shell、没有安装 shell——那些属于宿主包
11
+ (`oh-my-cpagents` 根包、DSH 层)。
12
+
13
+ 本包是 `omc-cli` 的共享运行时依赖,也会与 `omc-cli` 使用相同版本发布到 GitLab npm Package Registry。使用方通常直接安装 `omc-cli`,npm 会自动解析 `omc-core`。
@@ -0,0 +1,107 @@
1
+ import { createHash } from "node:crypto";
2
+ import { DatabaseSync } from "node:sqlite";
3
+ import { mkdirSync } from "node:fs";
4
+ import { dirname as pathDirname } from "node:path";
5
+ export async function discoverApplicationModelFieldForms(catalog, adapter, credentials) {
6
+ const capabilityId = "capabilityId" in adapter ? adapter.capabilityId : undefined;
7
+ if (!capabilityId)
8
+ throw new Error("Discovery adapter must identify its catalog capability.");
9
+ const capability = catalog.get(capabilityId);
10
+ assertExecutableDiscoveryCapability(capability);
11
+ const discovery = await adapter.discover(credentials);
12
+ const bindings = discovery.models.map((model) => toBinding(discovery, model));
13
+ return { discovery, bindings, baselineFingerprint: fingerprint({ discovery, bindings }) };
14
+ }
15
+ export class ResourceCodeReservations {
16
+ db;
17
+ constructor(databasePath) {
18
+ mkdirSync(pathDirname(databasePath), { recursive: true });
19
+ this.db = new DatabaseSync(databasePath);
20
+ this.db.exec("CREATE TABLE IF NOT EXISTS resource_code_reservation (resource_type TEXT NOT NULL, code TEXT NOT NULL, owner TEXT NOT NULL, PRIMARY KEY (resource_type, code))");
21
+ }
22
+ reserve(discovery, plans, owner) {
23
+ if (!plans.length)
24
+ throw new Error("At least one resource code must be planned.");
25
+ if (!owner.trim())
26
+ throw new Error("Reservation owner is required.");
27
+ const discovered = discoveredCodes(discovery);
28
+ const requested = flattenPlans(plans);
29
+ const duplicate = requested.find((resource) => discovered.has(key(resource.type, resource.code)));
30
+ if (duplicate)
31
+ throw new Error(`Planned ${duplicate.type} code collides with discovered resource: ${duplicate.code}.`);
32
+ const seen = new Set();
33
+ for (const resource of requested) {
34
+ const identity = key(resource.type, resource.code);
35
+ if (seen.has(identity))
36
+ throw new Error(`Duplicate planned ${resource.type} code: ${resource.code}.`);
37
+ seen.add(identity);
38
+ }
39
+ this.db.exec("BEGIN IMMEDIATE");
40
+ try {
41
+ for (const resource of requested) {
42
+ try {
43
+ this.db.prepare("INSERT INTO resource_code_reservation (resource_type, code, owner) VALUES (?, ?, ?)").run(resource.type, resource.code, owner.trim());
44
+ }
45
+ catch {
46
+ throw new Error(`Planned ${resource.type} code is already reserved: ${resource.code}.`);
47
+ }
48
+ }
49
+ this.db.exec("COMMIT");
50
+ }
51
+ catch (error) {
52
+ this.db.exec("ROLLBACK");
53
+ throw error;
54
+ }
55
+ }
56
+ reservations() {
57
+ return this.db.prepare("SELECT resource_type, code, owner FROM resource_code_reservation ORDER BY resource_type, code").all()
58
+ .map((row) => ({ type: String(row.resource_type), code: String(row.code), owner: String(row.owner) }));
59
+ }
60
+ }
61
+ function assertExecutableDiscoveryCapability(capability) {
62
+ if (capability.status !== "executable" || capability.promotionStatus !== "promoted" || capability.freeze.blocked) {
63
+ throw new Error(`Capability is not an executable discovery adapter: ${capability.id}.`);
64
+ }
65
+ }
66
+ function toBinding(discovery, model) {
67
+ const application = discovery.applications.find((candidate) => candidate.code === model.applicationCode);
68
+ if (!application)
69
+ throw new Error(`Read-back model references an undiscovered application: ${model.applicationCode}.`);
70
+ const fields = [...model.fields.draft, ...model.fields.published];
71
+ const fieldCodes = {};
72
+ for (const field of fields) {
73
+ if (fieldCodes[field.code] && fieldCodes[field.code].type !== field.type)
74
+ throw new Error(`Read-back field has conflicting types: ${field.code}.`);
75
+ fieldCodes[field.code] = { id: field.id, type: field.type, published: field.published || fieldCodes[field.code]?.published === true };
76
+ }
77
+ if (model.defaultForm.binding.modelCode !== model.schemaCode)
78
+ throw new Error(`Read-back form is not bound to model: ${model.schemaCode}.`);
79
+ return { applicationCode: application.code, modelCode: model.schemaCode, modelId: model.id, formCode: model.defaultForm.sheetCode, formId: model.defaultForm.id, fieldCodes, formBinding: model.defaultForm.binding };
80
+ }
81
+ function discoveredCodes(discovery) {
82
+ const codes = new Set();
83
+ for (const application of discovery.applications)
84
+ codes.add(key("application", application.code));
85
+ for (const model of discovery.models) {
86
+ codes.add(key("model", model.schemaCode));
87
+ codes.add(key("form", model.defaultForm.sheetCode));
88
+ for (const field of [...model.fields.draft, ...model.fields.published])
89
+ codes.add(key("field", field.code));
90
+ }
91
+ return codes;
92
+ }
93
+ function flattenPlans(plans) {
94
+ return plans.flatMap((plan) => [
95
+ ...(plan.applicationCode ? [{ type: "application", code: plan.applicationCode }] : []),
96
+ ...(plan.modelCode ? [{ type: "model", code: plan.modelCode }] : []),
97
+ ...(plan.formCode ? [{ type: "form", code: plan.formCode }] : []),
98
+ ...(plan.fieldCodes ?? []).map((code) => ({ type: "field", code })),
99
+ ]).map((resource) => {
100
+ if (!resource.code.trim())
101
+ throw new Error(`Planned ${resource.type} code is required.`);
102
+ return { ...resource, code: resource.code.trim() };
103
+ });
104
+ }
105
+ function key(type, code) { return `${type}:${code}`; }
106
+ function fingerprint(value) { return createHash("sha256").update(JSON.stringify(value)).digest("hex"); }
107
+ //# sourceMappingURL=application-discovery.js.map
@@ -0,0 +1,317 @@
1
+ // Host-owned human-readable approval summary blocks (Ticket 15).
2
+ //
3
+ // Iron rules (series spec, .scratch/omc-approval-readability/spec.md):
4
+ // 1. The summary block is DERIVED from the immutable plan/package by the
5
+ // Host. Every line is composed from a fixed per-capability registry plus
6
+ // grammar-validated resource identities taken from the validated approval
7
+ // or package — there is no free-text parameter, so model-authored phrasing
8
+ // can never enter an approval frame.
9
+ // 2. The block is APPENDED after the byte-identical machine prefix; every
10
+ // established runner keeps matching `reason.startsWith(reasonPrefix)`.
11
+ // 3. A rendered block that diverges from the immutable identities/scope is
12
+ // rejected: `verifyApprovalFrameHumanBlock` re-derives the block and
13
+ // fails any frame that does not end with exactly that block, and
14
+ // `assertApprovalFrameHumanBlock` gates the DSH approval request itself.
15
+ import { activeFieldFamily, fieldFamilyMembers } from "./families/registry.js";
16
+ /** Resource identity grammar: code-like segments only — never prose. */
17
+ const IDENTITY_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9_:/-]{0,191}$/;
18
+ const SCOPE_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9_:/+-]{0,191}$/;
19
+ const TEST_FIXTURE_RUN = "测试夹具运行,资源将按已批准契约分级清理";
20
+ const TEST_FIXTURE_CLEANUP_RUN = "测试夹具运行,清理以权威精确缺失回读收尾";
21
+ // Ticket 25: the ladder's family rungs derive their Chinese prose and the
22
+ // family-field identity labels from the ACTIVE registry family (src/families
23
+ // /<family>.ts) — never a per-rung literal — and the field stages are
24
+ // VARIADIC in the family-field identity slots (one schema + one code per
25
+ // registered member), so the identity-count gate and label rendering scale
26
+ // with the active member enumeration.
27
+ const FAMILY_PLAN = activeFieldFamily().planRow;
28
+ const FAMILY_MEMBER_COUNT = activeFieldFamily().members.length;
29
+ const CAPABILITY_SPECS = {
30
+ "bootstrap-model-create": {
31
+ change: "在 test 应用默认分组下创建清单所列 LIST 业务模型,每模型一步(序列化模型创建 + 权威 GET 回读),随后按已批准清理契约收尾",
32
+ boundary: "仅创建模型并回读验证;绝不 app publish、绝不 Environment release、绝不运行期业务数据变更",
33
+ runIdentity: TEST_FIXTURE_RUN,
34
+ identityLabels: ["模型"],
35
+ variadicIdentities: true,
36
+ },
37
+ "ticket02-model-create": {
38
+ change: "在 test 应用既有分组下创建一个序列化 LIST 模型(单步)并以权威 GET 回读确认,随后执行已批准的模型物理删除清理步",
39
+ boundary: "仅模型创建与回读;绝不 app publish、绝不 Environment release;写入仅经唯一配置写者租约执行",
40
+ runIdentity: TEST_FIXTURE_RUN,
41
+ identityLabels: ["模型"],
42
+ },
43
+ "ticket08-model-create": {
44
+ change: "创建一个序列化 LIST 测试夹具模型(单步;权威回读与行为验证;清理需另行单独审批)",
45
+ boundary: "仅创建测试夹具模型;绝不 app publish、绝不 Environment release;写入仅经唯一 Forge 配置写者租约执行",
46
+ runIdentity: TEST_FIXTURE_RUN,
47
+ identityLabels: ["夹具模型"],
48
+ },
49
+ "ticket08-model-cleanup": {
50
+ change: "物理删除本运行持有的测试夹具模型(单步;任务历史 END + 精确缺失回读)",
51
+ boundary: "仅删除归属明确的夹具模型;不确定结果立即停止、保留并上报,绝不盲试重放",
52
+ runIdentity: TEST_FIXTURE_CLEANUP_RUN,
53
+ identityLabels: ["夹具模型"],
54
+ },
55
+ "ticket08-orphan-cleanup": {
56
+ change: "经归属采纳预检后,物理删除遗留的本主机域测试夹具模型(单步;任务历史 END + 精确缺失回读)",
57
+ boundary: "仅删除采纳确认归属的遗留夹具模型;不确定结果立即停止、保留并上报",
58
+ runIdentity: TEST_FIXTURE_CLEANUP_RUN,
59
+ identityLabels: ["采纳夹具模型"],
60
+ },
61
+ "ticket09-field-create": {
62
+ change: `在夹具模型上创建${FAMILY_PLAN.familyShortLabel}族${FAMILY_MEMBER_COUNT}个类型化草稿字段(${[FAMILY_PLAN.fieldCreateQualifiers[0], FAMILY_PLAN.fieldCreateQualifiers[1] ?? ""].filter(Boolean).join("、")};逐字段草态/发布态两权威回读)`,
63
+ boundary: "仅草稿族字段创建;绝不 app publish、绝不 Environment release;清理分两段另行单独审批",
64
+ runIdentity: TEST_FIXTURE_RUN,
65
+ identityLabels: ["夹具模型", "族草稿字段"],
66
+ variadicIdentities: true,
67
+ identityCount: FAMILY_MEMBER_COUNT + 1,
68
+ },
69
+ "ticket09-field-cleanup": {
70
+ change: `强制删除本运行持有的${FAMILY_MEMBER_COUNT}个草稿族字段(逐字段;草态精确缺失回读)`,
71
+ boundary: "仅删除归属明确的草稿族字段;不确定结果停止、保留并上报",
72
+ runIdentity: TEST_FIXTURE_CLEANUP_RUN,
73
+ identityLabels: ["夹具模型", "族草稿字段"],
74
+ variadicIdentities: true,
75
+ identityCount: FAMILY_MEMBER_COUNT + 1,
76
+ },
77
+ "ticket10-field-publish": {
78
+ change: `一次模型级数据项发布夹具模型上${FAMILY_PLAN.familyShortLabel}族${FAMILY_MEMBER_COUNT}个字段(草态/发布态两权威回读与逐成员行为验证)`,
79
+ boundary: "仅模型级族字段发布;绝不 app publish、绝不表单发布、绝不 Environment release",
80
+ runIdentity: TEST_FIXTURE_RUN,
81
+ identityLabels: ["夹具模型", "族字段"],
82
+ variadicIdentities: true,
83
+ identityCount: FAMILY_MEMBER_COUNT + 1,
84
+ },
85
+ "ticket10-field-cleanup": {
86
+ change: `删除本运行持有的${FAMILY_MEMBER_COUNT}个已发布族字段(逐字段先探测 force_delete;确定性拒绝时按声明的模型级联契约收尾;两态精确缺失回读)`,
87
+ boundary: "仅处置归属明确的已发布族字段;不确定结果停止、保留并上报",
88
+ runIdentity: TEST_FIXTURE_CLEANUP_RUN,
89
+ identityLabels: ["夹具模型", "族字段"],
90
+ variadicIdentities: true,
91
+ identityCount: FAMILY_MEMBER_COUNT + 1,
92
+ },
93
+ "ticket11-form-draft": {
94
+ change: `保存夹具模型默认表单草稿(单步;空布局时组合设计器标准脚手架;绑定${FAMILY_MEMBER_COUNT}个族类型化控件,选项只写 visible 与总是只读布尔;发布态保持不变)`,
95
+ boundary: "仅草稿保存;绝不表单发布、绝不 app publish、绝不 Environment release",
96
+ runIdentity: TEST_FIXTURE_RUN,
97
+ identityLabels: ["夹具模型", "默认表单", "族控件字段"],
98
+ variadicIdentities: true,
99
+ identityCount: FAMILY_MEMBER_COUNT + 2,
100
+ },
101
+ "ticket20-data-rule": {
102
+ change: `经设计器原生规则面创建一条无条件必填规则行(单步;dataRuleType=4,选项 {requiredFormula:true,functionType:true},绑定${FAMILY_PLAN.ruleTargetLabel};规则列表权威回读解码)`,
103
+ boundary: "仅必填规则行创建;绝不只读/显示条件规则、绝不表单发布、绝不 app publish、绝不 Environment release",
104
+ runIdentity: TEST_FIXTURE_RUN,
105
+ identityLabels: ["夹具模型", "规则演示字段"],
106
+ },
107
+ "ticket12-form-publish": {
108
+ change: `发布夹具模型默认表单(模型级表单发布;${FAMILY_MEMBER_COUNT}个类型化控件随发布侧解码;发布历史解码;此前记录的表单一致性滞后在此了结)`,
109
+ boundary: "仅模型级默认表单发布;绝不 app publish、绝不 Environment release",
110
+ runIdentity: TEST_FIXTURE_RUN,
111
+ identityLabels: ["夹具模型", "默认表单", "族控件字段"],
112
+ variadicIdentities: true,
113
+ identityCount: FAMILY_MEMBER_COUNT + 2,
114
+ },
115
+ };
116
+ /** Validates that every identity/scope entry is code-like — model-authored phrasing is rejected. */
117
+ function assertCodeLike(value, pattern, label) {
118
+ if (typeof value !== "string" || !pattern.test(value))
119
+ throw new Error(`Approval human-readable summary ${label} is not a valid immutable code reference: ${String(value).slice(0, 80)}`);
120
+ }
121
+ function validateHumanBlockInput(input) {
122
+ const spec = CAPABILITY_SPECS[input.capability];
123
+ if (!spec)
124
+ throw new Error(`Approval human-readable summary has no Host-owned capability registry entry for ${String(input.capability)}.`);
125
+ if (input.kind !== "delivery-plan" && input.kind !== "environment-change")
126
+ throw new Error("Approval human-readable summary requires the frame kind.");
127
+ const identityCount = spec.identityCount ?? spec.identityLabels.length;
128
+ if (!Array.isArray(input.identities) || (spec.identityCount !== undefined ? input.identities.length !== identityCount : spec.variadicIdentities ? input.identities.length < identityCount : input.identities.length !== identityCount))
129
+ throw new Error(`Approval human-readable summary requires ${spec.identityCount !== undefined ? "exactly " : spec.variadicIdentities ? "at least " : "exactly "}${identityCount} resource identit${identityCount === 1 ? "y" : "ies"} for ${input.capability}.`);
130
+ input.identities.forEach((identity) => assertCodeLike(identity, IDENTITY_PATTERN, "resource identity"));
131
+ if (!Array.isArray(input.scope) || !input.scope.length)
132
+ throw new Error("Approval human-readable summary requires the exact approved operation scope.");
133
+ input.scope.forEach((scope) => assertCodeLike(scope, SCOPE_PATTERN, "operation scope"));
134
+ if (/(password|token|cookie|client[_-]?secret|authorization|api[_-]?key|private[_-]?key|bearer\s|-----begin|raw[_-]?(request|response|log))/i.test([...input.identities, ...input.scope].join(" ")))
135
+ throw new Error("Approval human-readable summary identities contain prohibited sensitive data.");
136
+ return spec;
137
+ }
138
+ /** Renders the four-line human-readable block from the registry + validated identities/scope. */
139
+ export function renderApprovalHumanBlock(input) {
140
+ const spec = validateHumanBlockInput(input);
141
+ const identities = input.identities.map((identity, index) => `${spec.identityLabels[Math.min(index, spec.identityLabels.length - 1)]} ${identity}`).join(" / ");
142
+ return [
143
+ ` 变更内容:${spec.change}`,
144
+ ` 边界承诺:${spec.boundary}`,
145
+ ` 运行身份:${spec.runIdentity}`,
146
+ ` 资源身份:${identities}(操作范围 ${input.scope.join(", ")})`,
147
+ ].join("\n");
148
+ }
149
+ /** Appends the human-readable block after the byte-identical machine prefix. */
150
+ export function appendApprovalHumanBlock(machinePrefix, input) {
151
+ if (typeof machinePrefix !== "string" || !machinePrefix || machinePrefix.includes("\n"))
152
+ throw new Error("Approval machine prefix must be the single unchanged machine line.");
153
+ return `${machinePrefix}\n${renderApprovalHumanBlock(input)}`;
154
+ }
155
+ // ---------------------------------------------------------------------------
156
+ // Ticket 17 (issue 03) — the single master approval frame, format polish.
157
+ //
158
+ // Grill decisions of 2026-09-05: the conversational plan-of-record carries
159
+ // the entire reading budget (the omc_master_plan_compose tool output), so the
160
+ // master frame is reduced to exactly THREE host-derived lines (summary /
161
+ // impact+boundary / anchor+guidance) with the machine prefix line DELETED —
162
+ // the master frame is a new contract with no archived-replay obligation, and
163
+ // the binding is structural (the approve tool call plus the plan hash sealed
164
+ // in the master plan record), never the displayed text. The 8-hex anchor
165
+ // prefix stays on line three for human cross-checking against the plan
166
+ // receipt. The same iron rules carry over: the frame is composed only from
167
+ // the fixed registry below plus grammar-validated enumeration input taken
168
+ // from the immutable master plan record — there is no free-text parameter,
169
+ // so model-authored phrasing can never enter the master frame; a rendered
170
+ // frame that diverges from the record is rejected before the human is asked.
171
+ // The legacy per-package four-line 变更内容 blocks above stay byte-identical.
172
+ // ---------------------------------------------------------------------------
173
+ /** Compact per-stage chain titles for the master frame's line-one summary (fixed text). */
174
+ const MASTER_STAGE_SUMMARY_TITLES = {
175
+ "ticket08-model-create": "模型",
176
+ "ticket09-field-create": `字段(${activeFieldFamily().planRow.familyShortLabel})`,
177
+ "ticket10-field-publish": "发布",
178
+ "ticket11-form-draft": "草稿",
179
+ "ticket20-data-rule": "规则(必填)",
180
+ "ticket12-form-publish": "发布表单",
181
+ };
182
+ function validateMasterHumanBlockInput(input) {
183
+ if (!Array.isArray(input.stages) || input.stages.length !== 6)
184
+ throw new Error("Master approval human-readable summary requires the six enumerated chain stages.");
185
+ input.stages.forEach((stage, index) => {
186
+ if (!MASTER_STAGE_SUMMARY_TITLES[stage.capability])
187
+ throw new Error(`Master approval human-readable summary stage ${index + 1} has no Host-owned registry entry for ${String(stage.capability)}.`);
188
+ if (input.stages.slice(0, index).some((prior) => MASTER_STAGE_SUMMARY_TITLES[prior.capability] === MASTER_STAGE_SUMMARY_TITLES[stage.capability]))
189
+ throw new Error("Master approval human-readable summary stages must be the six distinct enumerated capabilities.");
190
+ // Ticket 25 family rung: identity cardinality is exact per capability:
191
+ // model=1, field/rule=1+N/2, and form=2+N. This keeps the three-line
192
+ // summary bound to the immutable plan instead of accepting arbitrary
193
+ // extra resources under a loose upper bound.
194
+ const expectedIdentityCount = stage.capability === "ticket08-model-create" ? 1
195
+ : stage.capability === "ticket09-field-create" || stage.capability === "ticket09-field-cleanup" || stage.capability === "ticket10-field-publish" || stage.capability === "ticket10-field-cleanup" ? fieldFamilyMembers().length + 1
196
+ : stage.capability === "ticket11-form-draft" || stage.capability === "ticket12-form-publish" ? fieldFamilyMembers().length + 2
197
+ : stage.capability === "ticket20-data-rule" ? 2
198
+ : undefined;
199
+ if (!Array.isArray(stage.identities) || expectedIdentityCount === undefined || stage.identities.length !== expectedIdentityCount)
200
+ throw new Error(`Master approval human-readable summary stage ${index + 1} requires exactly ${expectedIdentityCount ?? "the registered"} resource identities.`);
201
+ stage.identities.forEach((identity) => assertCodeLike(identity, IDENTITY_PATTERN, "master resource identity"));
202
+ if (typeof stage.scope !== "string" || !SCOPE_PATTERN.test(stage.scope))
203
+ throw new Error(`Master approval human-readable summary stage ${index + 1} requires the exact operation scope.`);
204
+ });
205
+ // The master plan always declares exactly the two staged cleanups
206
+ // (composeMasterPlan pins them); any other count is a foreign shape.
207
+ if (input.cleanupCount !== 2)
208
+ throw new Error("Master approval human-readable summary requires the two declared staged cleanups.");
209
+ for (const value of [input.environment, input.application, input.folderId, input.testRunId])
210
+ assertCodeLike(value, IDENTITY_PATTERN, "master impact reference");
211
+ if (typeof input.folderName !== "string" || !input.folderName.trim())
212
+ throw new Error("Master approval human-readable summary requires the folder name.");
213
+ // Display names are the only free-form-ish text on the approval surface;
214
+ // line breaks or control characters could forge the frame structure.
215
+ if (/[\n\r\t\u0000-\u001f]/.test(input.folderName))
216
+ throw new Error("Master approval human-readable summary folder name must be single-line text.");
217
+ if (!/^[a-f0-9]{64}$/i.test(input.anchorHash))
218
+ throw new Error("Master approval human-readable summary requires the 64-hex plan anchor hash.");
219
+ if (/(password|token|cookie|client[_-]?secret|authorization|api[_-]?key|private[_-]?key|bearer\s|-----begin|raw[_-]?(request|response|log))/i.test([input.testRunId, input.folderName, ...input.stages.flatMap((stage) => [...stage.identities, stage.scope])].join(" ")))
220
+ throw new Error("Master approval human-readable summary contains prohibited sensitive data.");
221
+ }
222
+ /** Renders the exactly-three-line master frame: summary / impact+boundary / anchor+guidance. */
223
+ export function renderMasterApprovalFrame(input) {
224
+ validateMasterHumanBlockInput(input);
225
+ const chain = [...input.stages.map((stage) => MASTER_STAGE_SUMMARY_TITLES[stage.capability]), "分级清理"];
226
+ return [
227
+ `批准执行 ${input.stages.length + input.cleanupCount} 步交付:${chain.join("→")}`,
228
+ `影响:${input.environment} 环境·${input.folderName} · 绝不 app publish / Environment release`,
229
+ `锚点 ${input.anchorHash.slice(0, 8)} · 完整方案见上方对话 · 逐步结果将在对话内汇报`,
230
+ ].join("\n");
231
+ }
232
+ /**
233
+ * Re-derives the exactly-three-line frame from the immutable enumeration
234
+ * input and fails any frame that diverges from it — the complete mismatch
235
+ * gate (the binding itself is structural: the approve tool call plus the
236
+ * plan hash sealed in the master plan record).
237
+ */
238
+ export function verifyMasterApprovalFrameHumanBlock(frame, input) {
239
+ if (typeof frame !== "string" || !frame)
240
+ throw new Error("Approval frame is required.");
241
+ if (frame !== renderMasterApprovalFrame(input))
242
+ throw new Error("Master approval frame human-readable summary diverges from the immutable master plan enumeration.");
243
+ }
244
+ /**
245
+ * Master approval-request structural gate: the frame must be exactly the
246
+ * three Host-rendered lines (summary / impact+boundary / anchor+guidance) —
247
+ * no machine prefix, no fourth line, no injected block — and must carry the
248
+ * 8-hex anchor prefix for human cross-check. The byte-exact re-derivation
249
+ * against the record (verifyMasterApprovalFrameHumanBlock) remains the
250
+ * complete mismatch gate.
251
+ */
252
+ export function assertMasterApprovalFrameHumanBlock(frame) {
253
+ if (typeof frame !== "string" || !frame)
254
+ throw new Error("Approval frame is required.");
255
+ const lines = frame.split("\n");
256
+ if (lines.length !== 3)
257
+ throw new Error("Master approval frame is not the exactly-three-line contract.");
258
+ if (!/^批准执行 [0-9]+ 步交付:.+→分级清理$/.test(lines[0]))
259
+ throw new Error("Master approval frame summary line is not Host-rendered.");
260
+ if (!lines[1].startsWith("影响:") || !lines[1].includes("绝不 app publish") || !lines[1].includes("Environment release"))
261
+ throw new Error("Master approval frame impact line is not Host-rendered.");
262
+ if (!/^锚点 [a-f0-9]{8} · 完整方案见上方对话 · 逐步结果将在对话内汇报$/.test(lines[2]))
263
+ throw new Error("Master approval frame anchor line is not Host-rendered.");
264
+ if (/(password|token|cookie|client[_-]?secret|authorization|api[_-]?key|private[_-]?key|bearer\s|-----begin|raw[_-]?(request|response|log))/i.test(frame))
265
+ throw new Error("Master approval frame human-readable summary contains prohibited sensitive data.");
266
+ }
267
+ /** Read-only access to a capability's Host-owned registry spec (shared with the conversational plan renderer). */
268
+ export function approvalHumanCapabilitySpec(capability) {
269
+ const spec = CAPABILITY_SPECS[capability];
270
+ if (!spec)
271
+ throw new Error(`Approval human-readable summary has no Host-owned capability registry entry for ${String(capability)}.`);
272
+ return { change: spec.change, boundary: spec.boundary, runIdentity: spec.runIdentity };
273
+ }
274
+ function humanBlockStart(frame) {
275
+ return frame.indexOf("\n 变更内容:");
276
+ }
277
+ /** Re-derives the block from the immutable input and fails any frame that diverges from it. */
278
+ export function verifyApprovalFrameHumanBlock(frame, input) {
279
+ if (typeof frame !== "string" || !frame)
280
+ throw new Error("Approval frame is required.");
281
+ const block = renderApprovalHumanBlock(input);
282
+ const start = humanBlockStart(frame);
283
+ if (start < 0 || frame.slice(start + 1) !== block || frame.split(block).length !== 2)
284
+ throw new Error("Approval frame human-readable summary diverges from the immutable package/plan identities and scope.");
285
+ }
286
+ /**
287
+ * Approval-request gate: the frame must carry exactly one well-formed
288
+ * human-readable block at its end, and every resource identity code in the
289
+ * block must also appear in the machine prefix — which the Host derives from
290
+ * the same validated immutable approval/package binding — so a summary
291
+ * rendered from any other package is rejected before the human is asked.
292
+ */
293
+ export function assertApprovalFrameHumanBlock(frame) {
294
+ if (typeof frame !== "string" || !frame)
295
+ throw new Error("Approval frame is required.");
296
+ const blockStart = humanBlockStart(frame);
297
+ if (blockStart < 0)
298
+ throw new Error("Approval frame is missing the Host-generated human-readable summary block.");
299
+ const machinePrefix = frame.slice(0, blockStart);
300
+ const block = frame.slice(blockStart + 1);
301
+ if (!machinePrefix || machinePrefix.includes("\n 变更内容:"))
302
+ throw new Error("Approval frame machine prefix is malformed.");
303
+ const lines = block.split("\n");
304
+ if (lines.length !== 4 || !lines[0].startsWith(" 变更内容:") || !lines[1].startsWith(" 边界承诺:") || !lines[2].startsWith(" 运行身份:") || !lines[3].startsWith(" 资源身份:") || !lines[3].endsWith(")"))
305
+ throw new Error("Approval frame human-readable summary block is not the Host-rendered four-line contract.");
306
+ if (/(password|token|cookie|client[_-]?secret|authorization|api[_-]?key|private[_-]?key|bearer\s|-----begin|raw[_-]?(request|response|log))/i.test(block))
307
+ throw new Error("Approval frame human-readable summary contains prohibited sensitive data.");
308
+ const identityLine = lines[3].slice(" 资源身份:".length, lines[3].indexOf("(操作范围"));
309
+ for (const identity of identityLine.split(" / ")) {
310
+ const code = identity.slice(identity.lastIndexOf(" ") + 1);
311
+ if (!IDENTITY_PATTERN.test(code))
312
+ throw new Error(`Approval frame human-readable summary carries a non-code resource identity: ${code.slice(0, 80)}`);
313
+ if (!machinePrefix.includes(code))
314
+ throw new Error(`Approval frame human-readable summary identity ${code} diverges from the machine prefix binding.`);
315
+ }
316
+ }
317
+ //# sourceMappingURL=approval-frame-readability.js.map
@@ -0,0 +1,143 @@
1
+ import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
2
+ import { mkdirSync } from "node:fs";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import { dirname } from "node:path";
5
+ export class ApprovalProtocol {
6
+ db;
7
+ key;
8
+ constructor(databasePath, key) {
9
+ mkdirSync(dirname(databasePath), { recursive: true });
10
+ this.key = Buffer.isBuffer(key) ? key : Buffer.from(key, "base64");
11
+ if (this.key.length !== 32)
12
+ throw new Error("Approval protocol integrity key must be exactly 32 bytes.");
13
+ this.db = new DatabaseSync(databasePath);
14
+ this.db.exec(`CREATE TABLE IF NOT EXISTS approval_protocol (
15
+ transaction_id TEXT PRIMARY KEY, kind TEXT NOT NULL, opaque_id TEXT NOT NULL,
16
+ expected_json TEXT NOT NULL, intent_hash TEXT NOT NULL, status TEXT NOT NULL,
17
+ completed_boundaries_json TEXT NOT NULL, record_hash TEXT NOT NULL, integrity_mac TEXT NOT NULL,
18
+ UNIQUE(kind, opaque_id)
19
+ )`);
20
+ for (const column of ["prompt_issued INTEGER NOT NULL DEFAULT 0", "approval_outcome TEXT"]) {
21
+ try {
22
+ this.db.exec(`ALTER TABLE approval_protocol ADD COLUMN ${column}`);
23
+ }
24
+ catch { /* already present */ }
25
+ }
26
+ }
27
+ begin(kind, opaqueId, expected) {
28
+ if (!opaqueId)
29
+ throw new Error("Approval protocol requires an opaque ID.");
30
+ const existing = this.load(kind, opaqueId);
31
+ const intentHash = sha256({ kind, opaqueId, expected });
32
+ if (existing) {
33
+ if (existing.status === "operator-reconcile")
34
+ throw new Error("Approval protocol is blocked for operator reconcile.");
35
+ if (existing.intentHash !== intentHash)
36
+ throw new Error("Approval protocol intent diverges from the authenticated approval frame; operator reconcile is required.");
37
+ return existing;
38
+ }
39
+ const value = { transactionId: randomUUID(), kind, opaqueId, expected, intentHash, status: "pending", completedBoundaries: [], promptIssued: false };
40
+ this.save(value);
41
+ return value;
42
+ }
43
+ get(kind, opaqueId) { return this.load(kind, opaqueId); }
44
+ claimPrompt(intent) {
45
+ const current = this.authenticate(intent);
46
+ if (current.promptIssued)
47
+ return { intent: current, claimed: false };
48
+ const next = { ...current, promptIssued: true };
49
+ const recordHash = sha256(next);
50
+ const mac = hmac(recordHash, this.key);
51
+ const result = this.db.prepare(`UPDATE approval_protocol
52
+ SET prompt_issued = 1, record_hash = ?, integrity_mac = ?
53
+ WHERE transaction_id = ? AND kind = ? AND opaque_id = ? AND intent_hash = ? AND prompt_issued = 0`)
54
+ .run(recordHash, mac, current.transactionId, current.kind, current.opaqueId, current.intentHash);
55
+ if (Number(result.changes) !== 1) {
56
+ const latest = this.authenticate(intent);
57
+ return { intent: latest, claimed: false };
58
+ }
59
+ return { intent: next, claimed: true };
60
+ }
61
+ recordOutcome(intent, outcome) {
62
+ if (!outcome)
63
+ throw new Error("Approval protocol outcome is required.");
64
+ const current = this.authenticate(intent);
65
+ if (!current.promptIssued)
66
+ throw new Error("Approval protocol prompt was not issued.");
67
+ if (current.approvalOutcome && current.approvalOutcome !== outcome)
68
+ throw new Error("Approval protocol approval outcome diverges; operator reconcile is required.");
69
+ if (current.approvalOutcome === outcome)
70
+ return current;
71
+ const next = { ...current, approvalOutcome: outcome };
72
+ this.save(next);
73
+ return next;
74
+ }
75
+ markBoundary(intent, boundary) {
76
+ const current = this.load(intent.kind, intent.opaqueId);
77
+ if (!current || current.transactionId !== intent.transactionId || current.intentHash !== intent.intentHash)
78
+ throw new Error("Approval protocol transaction authentication failed; operator reconcile is required.");
79
+ if (current.status === "operator-reconcile")
80
+ throw new Error("Approval protocol is blocked for operator reconcile.");
81
+ if (current.completedBoundaries.includes(boundary))
82
+ return current;
83
+ const completedBoundaries = [...current.completedBoundaries, boundary];
84
+ const next = { ...current, completedBoundaries, status: completedBoundaries.length === 3 ? "complete" : "pending" };
85
+ this.save(next);
86
+ return next;
87
+ }
88
+ failClosed(intent, reason) {
89
+ const current = this.load(intent.kind, intent.opaqueId);
90
+ if (current && current.transactionId === intent.transactionId && current.intentHash === intent.intentHash)
91
+ this.save({ ...current, status: "operator-reconcile" });
92
+ throw new Error(`Approval protocol requires operator reconcile: ${reason}`);
93
+ }
94
+ abandon(intent) {
95
+ const current = this.load(intent.kind, intent.opaqueId);
96
+ if (!current || current.transactionId !== intent.transactionId || current.intentHash !== intent.intentHash || current.completedBoundaries.length)
97
+ throw new Error("Approval protocol transaction cannot be abandoned.");
98
+ this.db.prepare("DELETE FROM approval_protocol WHERE transaction_id = ?").run(intent.transactionId);
99
+ }
100
+ renew(intent, expected) {
101
+ const current = this.authenticate(intent);
102
+ if (current.kind !== "execution-approval" || current.completedBoundaries.length < 2 || current.approvalOutcome !== "allowed-once")
103
+ throw new Error("Approval protocol execution intent cannot be renewed before authoritative reconciliation.");
104
+ this.db.prepare("DELETE FROM approval_protocol WHERE transaction_id = ?").run(current.transactionId);
105
+ return this.begin(current.kind, current.opaqueId, expected);
106
+ }
107
+ load(kind, opaqueId) {
108
+ const row = this.db.prepare("SELECT * FROM approval_protocol WHERE kind = ? AND opaque_id = ?").get(kind, opaqueId);
109
+ if (!row)
110
+ return undefined;
111
+ const value = { transactionId: String(row.transaction_id), kind: String(row.kind), opaqueId: String(row.opaque_id), expected: JSON.parse(String(row.expected_json)), intentHash: String(row.intent_hash), status: String(row.status), completedBoundaries: JSON.parse(String(row.completed_boundaries_json)), promptIssued: Number(row.prompt_issued) === 1, ...(row.approval_outcome ? { approvalOutcome: String(row.approval_outcome) } : {}) };
112
+ const recordHash = sha256(value);
113
+ const legacyRecordHash = sha256({ transactionId: value.transactionId, kind: value.kind, opaqueId: value.opaqueId, expected: value.expected, intentHash: value.intentHash, status: value.status, completedBoundaries: value.completedBoundaries });
114
+ const storedHash = String(row.record_hash);
115
+ const validRecord = storedHash === recordHash || (storedHash === legacyRecordHash && !value.promptIssued && value.approvalOutcome === undefined);
116
+ if (!validRecord || !sameMac(String(row.integrity_mac), hmac(storedHash, this.key)) || value.intentHash !== sha256({ kind: value.kind, opaqueId: value.opaqueId, expected: value.expected }))
117
+ throw new Error("Approval protocol record authentication is invalid; operator reconcile is required.");
118
+ return value;
119
+ }
120
+ save(value) {
121
+ const recordHash = sha256(value);
122
+ const mac = hmac(recordHash, this.key);
123
+ this.db.prepare(`INSERT INTO approval_protocol (transaction_id, kind, opaque_id, expected_json, intent_hash, status, completed_boundaries_json, prompt_issued, approval_outcome, record_hash, integrity_mac)
124
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(transaction_id) DO UPDATE SET status = excluded.status, completed_boundaries_json = excluded.completed_boundaries_json, prompt_issued = excluded.prompt_issued, approval_outcome = excluded.approval_outcome, record_hash = excluded.record_hash, integrity_mac = excluded.integrity_mac`)
125
+ .run(value.transactionId, value.kind, value.opaqueId, canonical(value.expected), value.intentHash, value.status, canonical(value.completedBoundaries), value.promptIssued ? 1 : 0, value.approvalOutcome ?? null, recordHash, mac);
126
+ }
127
+ authenticate(intent) {
128
+ const current = this.load(intent.kind, intent.opaqueId);
129
+ if (!current || current.transactionId !== intent.transactionId || current.intentHash !== intent.intentHash)
130
+ throw new Error("Approval protocol transaction authentication failed; operator reconcile is required.");
131
+ if (current.status === "operator-reconcile")
132
+ throw new Error("Approval protocol is blocked for operator reconcile.");
133
+ return current;
134
+ }
135
+ }
136
+ function sha256(value) { return createHash("sha256").update(canonical(value)).digest("hex"); }
137
+ function hmac(value, key) { return createHmac("sha256", key).update(value).digest("hex"); }
138
+ function sameMac(actual, expected) { const a = Buffer.from(actual, "hex"); const b = Buffer.from(expected, "hex"); return a.length === b.length && timingSafeEqual(a, b); }
139
+ function canonical(value) { return JSON.stringify(sort(value)); }
140
+ function sort(value) { if (Array.isArray(value))
141
+ return value.map(sort); if (value && typeof value === "object")
142
+ return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, sort(item)])); return value; }
143
+ //# sourceMappingURL=approval-protocol.js.map