@opensoha/contracts 0.1.25 → 0.1.27

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 (200) hide show
  1. package/COMPATIBILITY.md +34 -0
  2. package/README.md +1 -1
  3. package/agent/environment/export.go +1 -0
  4. package/agent/environment/protocol_test.go +1 -1
  5. package/agent/guest/protocol_test.go +2 -2
  6. package/auth/catalog_generated.go +2 -2
  7. package/auth/permission-catalog.json +481 -1
  8. package/compat/json-schema-ecosystem-0.1.0.json +1 -1
  9. package/compat/openapi-loose-boundaries-0.1.0.json +1 -1
  10. package/delivery/document.schema.json +127 -9
  11. package/endpointcontrol/decode.go +64 -0
  12. package/endpointcontrol/models.go +138 -0
  13. package/endpointcontrol/models_test.go +51 -0
  14. package/endpointcontrol/protocol.schema.json +397 -0
  15. package/examples/relations_compat_test.go +18 -0
  16. package/fixtures/json-schema/endpoint-controls/invalid/controls.json +16 -0
  17. package/fixtures/json-schema/endpoint-controls/invalid/missing-channel-decision.json +15 -0
  18. package/fixtures/json-schema/endpoint-controls/invalid/receipt-content.json +36 -0
  19. package/fixtures/json-schema/endpoint-controls/valid/controls.json +16 -0
  20. package/fixtures/json-schema/endpoint-controls/valid/receipt.json +35 -0
  21. package/fixtures/json-schema/network-runtime-protocol/invalid/eap-activate-bad-fingerprint.json +14 -0
  22. package/fixtures/json-schema/network-runtime-protocol/invalid/eap-gateway-request.json +14 -0
  23. package/fixtures/json-schema/network-runtime-protocol/invalid/eap-private-key-request.json +15 -0
  24. package/fixtures/json-schema/network-runtime-protocol/invalid/eap-profile-without-eap-credential.json +22 -0
  25. package/fixtures/json-schema/network-runtime-protocol/invalid/endpoint-dns-observation.json +25 -0
  26. package/fixtures/json-schema/network-runtime-protocol/invalid/gateway-presence.json +22 -0
  27. package/fixtures/json-schema/network-runtime-protocol/invalid/mihomo-protection-zero-version.json +45 -0
  28. package/fixtures/json-schema/network-runtime-protocol/invalid/presence-raw-key.json +23 -0
  29. package/fixtures/json-schema/network-runtime-protocol/invalid/presence-zero-sequence.json +22 -0
  30. package/fixtures/json-schema/network-runtime-protocol/invalid/workload-wide-vpn.json +17 -0
  31. package/fixtures/json-schema/network-runtime-protocol/valid/eap-certificate-activate.json +14 -0
  32. package/fixtures/json-schema/network-runtime-protocol/valid/eap-certificate-request.json +14 -0
  33. package/fixtures/json-schema/network-runtime-protocol/valid/eap-profile-enrollment.json +24 -0
  34. package/fixtures/json-schema/network-runtime-protocol/valid/endpoint-presence.json +22 -0
  35. package/fixtures/json-schema/network-runtime-protocol/valid/ipv6-configuration.json +102 -0
  36. package/fixtures/json-schema/network-runtime-protocol/valid/mihomo-protected-policy.json +45 -0
  37. package/fixtures/json-schema/network-runtime-protocol/valid/site-dns-ipv6.json +25 -0
  38. package/fixtures/json-schema/network-runtime-protocol/valid/workload-direct-ztna.json +20 -0
  39. package/fixtures/openapi/DLPShadowDecision/invalid/step-up-type.json +14 -0
  40. package/fixtures/openapi/DLPShadowDecision/valid/archive-v2.json +14 -0
  41. package/fixtures/openapi/DLPShadowDecision/valid/step-up.json +14 -0
  42. package/fixtures/openapi/DLPShadowInput/invalid/self-reported-result.json +7 -0
  43. package/fixtures/openapi/DLPShadowInput/invalid/too-many-samples.json +14 -0
  44. package/fixtures/openapi/DLPShadowInput/valid/bounded.json +7 -0
  45. package/fixtures/openapi/DeliveryDocument/invalid/migration-missing-guide-build-template.json +30 -0
  46. package/fixtures/openapi/DeliveryDocument/invalid/migration-missing-guide-deployment-template.json +30 -0
  47. package/fixtures/openapi/DeliveryDocument/invalid/migration-missing-guide-workflow-template.json +27 -0
  48. package/fixtures/openapi/DeliveryDocument/valid/migration-build-template.json +31 -0
  49. package/fixtures/openapi/DeliveryDocument/valid/migration-deployment-template.json +31 -0
  50. package/fixtures/openapi/DeliveryDocument/valid/migration-workflow-template.json +28 -0
  51. package/fixtures/openapi/DeliveryPlanInput/invalid/manual-plan.json +7 -0
  52. package/fixtures/openapi/DeliveryPlanInput/valid/manual-plan.json +6 -0
  53. package/fixtures/openapi/DeliveryTargetInput/invalid/candidate-on-runtime-deploy.json +9 -0
  54. package/fixtures/openapi/DeliveryTargetInput/invalid/config-update-missing-service.json +7 -0
  55. package/fixtures/openapi/DeliveryTargetInput/valid/template-configuration-only.json +8 -0
  56. package/fixtures/openapi/DeliveryTargetInput/valid/template-configuration-without-runtime-environment.json +1 -0
  57. package/fixtures/openapi/DockerVolumeDLPAppealInput/invalid/p4-response.json +3 -0
  58. package/fixtures/openapi/DockerVolumeDLPAppealInput/valid/p4-response.json +3 -0
  59. package/fixtures/openapi/DockerVolumeDLPAppealReviewInput/invalid/p4-response.json +5 -0
  60. package/fixtures/openapi/DockerVolumeDLPAppealReviewInput/valid/p4-response.json +5 -0
  61. package/fixtures/openapi/DockerVolumeDLPAuthorizationInput/invalid/p4-response.json +3 -0
  62. package/fixtures/openapi/DockerVolumeDLPAuthorizationInput/valid/p4-response.json +3 -0
  63. package/fixtures/openapi/DockerVolumeDLPContent/invalid/archive-unsupported-channel.json +9 -0
  64. package/fixtures/openapi/DockerVolumeDLPContent/invalid/p4.json +6 -0
  65. package/fixtures/openapi/DockerVolumeDLPContent/invalid/step-up-audit.json +9 -0
  66. package/fixtures/openapi/DockerVolumeDLPContent/invalid/unavailable-engine.json +9 -0
  67. package/fixtures/openapi/DockerVolumeDLPContent/invalid/unknown-data-level.json +12 -0
  68. package/fixtures/openapi/DockerVolumeDLPContent/valid/classified-rule.json +16 -0
  69. package/fixtures/openapi/DockerVolumeDLPContent/valid/p4.json +8 -0
  70. package/fixtures/openapi/DockerVolumeDLPContent/valid/step-up.json +9 -0
  71. package/fixtures/openapi/DockerVolumeDLPDecision/invalid/p4.json +9 -0
  72. package/fixtures/openapi/DockerVolumeDLPDecision/invalid/step-up-type.json +12 -0
  73. package/fixtures/openapi/DockerVolumeDLPDecision/valid/archive-v2.json +12 -0
  74. package/fixtures/openapi/DockerVolumeDLPDecision/valid/p4.json +11 -0
  75. package/fixtures/openapi/DockerVolumeDLPDecision/valid/step-up.json +12 -0
  76. package/fixtures/openapi/DockerVolumeDLPSaveInput/invalid/p4.json +11 -0
  77. package/fixtures/openapi/DockerVolumeDLPSaveInput/valid/p4.json +11 -0
  78. package/fixtures/openapi/DockerVolumeFileReadInput/invalid/p4-response.json +5 -0
  79. package/fixtures/openapi/DockerVolumeFileReadInput/valid/p4-response.json +4 -0
  80. package/fixtures/openapi/EndpointDeviceInput/invalid/reported-management.json +5 -0
  81. package/fixtures/openapi/EndpointDeviceInput/valid/management-state.json +5 -0
  82. package/fixtures/openapi/EndpointDeviceRegistrationInput/invalid/self-promote-management.json +5 -0
  83. package/fixtures/openapi/KubernetesImportCandidateListEnvelope/invalid/source-with-spec.json +33 -0
  84. package/fixtures/openapi/KubernetesImportCandidateListEnvelope/invalid/source-without-uid.json +29 -0
  85. package/fixtures/openapi/KubernetesImportCandidateListEnvelope/valid/stable-source.json +30 -0
  86. package/fixtures/openapi/KubernetesPod/invalid/pod-uid.json +9 -0
  87. package/fixtures/openapi/KubernetesPod/valid/pod-uid.json +9 -0
  88. package/fixtures/openapi/KubernetesPodDetail/invalid/pod-uid.json +6 -0
  89. package/fixtures/openapi/KubernetesPodDetail/valid/pod-uid.json +6 -0
  90. package/fixtures/openapi/KubernetesServiceImportRequest/invalid/empty-source-uid.json +48 -0
  91. package/fixtures/openapi/KubernetesServiceImportRequest/valid/pinned-workloads-and-cr.json +48 -0
  92. package/fixtures/openapi/ManagedContentPasteInput/invalid/boundary.json +5 -0
  93. package/fixtures/openapi/ManagedContentPasteInput/valid/boundary.json +6 -0
  94. package/fixtures/openapi/ManagedContentPolicy/invalid/self-reported-allow.json +8 -0
  95. package/fixtures/openapi/ManagedContentPolicy/invalid/step-up-audit.json +8 -0
  96. package/fixtures/openapi/ManagedContentPolicy/invalid/unknown-extractor.json +8 -0
  97. package/fixtures/openapi/ManagedContentPolicy/valid/archive-v2.json +8 -0
  98. package/fixtures/openapi/ManagedContentPolicy/valid/network-and-level.json +10 -0
  99. package/fixtures/openapi/ManagedContentPolicy/valid/step-up.json +8 -0
  100. package/fixtures/openapi/ManagedContentReviewInput/invalid/boundary.json +5 -0
  101. package/fixtures/openapi/ManagedContentReviewInput/valid/boundary.json +5 -0
  102. package/fixtures/openapi/ManagedContentSaveInput/invalid/boundary.json +14 -0
  103. package/fixtures/openapi/ManagedContentSaveInput/valid/boundary.json +16 -0
  104. package/fixtures/openapi/ManagedContentUploadInput/invalid/boundary.json +7 -0
  105. package/fixtures/openapi/ManagedContentUploadInput/valid/boundary.json +6 -0
  106. package/fixtures/openapi/ManifestExecutionTaskPayload/invalid/initialization.json +7 -0
  107. package/fixtures/openapi/ManifestExecutionTaskPayload/valid/initialization.json +7 -0
  108. package/fixtures/openapi/ManifestRootResource/invalid/root.json +1 -0
  109. package/fixtures/openapi/ManifestRootResource/valid/root.json +1 -0
  110. package/fixtures/openapi/NetworkAccessPolicyInput/invalid/unknown-management-state.json +35 -0
  111. package/fixtures/openapi/NetworkAccessPolicyInput/valid/managed-device.json +35 -0
  112. package/fixtures/openapi/NetworkAdmissionPreviewRequest/invalid/deviceId-characters.json +4 -0
  113. package/fixtures/openapi/NetworkAdmissionPreviewRequest/invalid/nasBindingId-characters.json +4 -0
  114. package/fixtures/openapi/NetworkAdmissionPreviewRequest/invalid/subject-override.json +1 -0
  115. package/fixtures/openapi/NetworkAdmissionPreviewRequest/valid/device-nas.json +1 -0
  116. package/fixtures/openapi/NetworkAdmissionPreviewResult/invalid/missing-attributeVersion.json +13 -0
  117. package/fixtures/openapi/NetworkAdmissionPreviewResult/invalid/missing-evaluatedAt.json +13 -0
  118. package/fixtures/openapi/NetworkAdmissionPreviewResult/valid/versioned.json +14 -0
  119. package/fixtures/openapi/NetworkConflictAnalysisRequest/invalid/missing-runtime-ranges.json +1 -0
  120. package/fixtures/openapi/NetworkConflictAnalysisRequest/invalid/unknown-source-type.json +9 -0
  121. package/fixtures/openapi/NetworkConflictAnalysisRequest/valid/empty-runtime-ranges.json +3 -0
  122. package/fixtures/openapi/NetworkConflictAnalysisRequest/{invalid → valid}/ipv6-range.json +1 -1
  123. package/fixtures/openapi/NetworkEAPServerTrust/invalid/no-server-validation.json +5 -0
  124. package/fixtures/openapi/NetworkEAPServerTrust/valid/named-server.json +9 -0
  125. package/fixtures/openapi/NetworkEndpointResponsePolicyInput/invalid/all-sites-implicit.json +45 -0
  126. package/fixtures/openapi/NetworkEndpointResponsePolicyInput/invalid/unbounded-stages.json +54 -0
  127. package/fixtures/openapi/NetworkEndpointResponsePolicyInput/invalid/zero-heartbeat.json +47 -0
  128. package/fixtures/openapi/NetworkEndpointResponsePolicyInput/valid/staged-protection.json +47 -0
  129. package/fixtures/openapi/NetworkEndpointResponseRestoreInput/invalid/missing-reason.json +3 -0
  130. package/fixtures/openapi/NetworkEndpointResponseRestoreInput/valid/expected-current.json +4 -0
  131. package/fixtures/openapi/NetworkPolicyPreviewResult/invalid/attribute-version.json +1 -0
  132. package/fixtures/openapi/NetworkPolicyPreviewResult/valid/admission-version.json +1 -0
  133. package/fixtures/openapi/NetworkPolicySubjects/invalid/duplicate-role.json +1 -0
  134. package/fixtures/openapi/NetworkPolicySubjects/invalid/role-characters.json +5 -0
  135. package/fixtures/openapi/NetworkPolicySubjects/valid/role-bound.json +1 -0
  136. package/fixtures/openapi/NetworkProxyConfigurationInput/invalid/missing-content.json +1 -0
  137. package/fixtures/openapi/NetworkProxyConfigurationPatchInput/invalid/bad-binding-id.json +9 -0
  138. package/fixtures/openapi/NetworkProxyConfigurationPatchInput/invalid/empty-content.json +1 -0
  139. package/fixtures/openapi/NetworkProxyConfigurationPatchInput/valid/binding-only.json +11 -0
  140. package/fixtures/openapi/NetworkProxyConfigurationPatchInput/valid/clear-binding-disabled.json +5 -0
  141. package/fixtures/openapi/NetworkProxyConfigurationPatchInput/valid/enabled.json +1 -0
  142. package/fixtures/openapi/NetworkSiteInput/invalid/unknown-governance.json +6 -0
  143. package/fixtures/openapi/NetworkSiteInput/valid/restricted-unmanaged.json +6 -0
  144. package/fixtures/openapi/ResourceRelationConfig/invalid/executable-rule.json +17 -0
  145. package/fixtures/openapi/ResourceRelationConfig/valid/unknown-kind.json +13 -0
  146. package/fixtures/openapi/ServiceDeploymentTemplateInput/invalid/migration-missing-guide-valid-yaml.json +38 -0
  147. package/fixtures/openapi/ServiceDeploymentTemplateInput/valid/migration-valid-yaml.json +39 -0
  148. package/fixtures/openapi/TemplateMigrationAssessment/invalid/initialization.json +9 -0
  149. package/fixtures/openapi/TemplateMigrationAssessment/valid/initialization.json +9 -0
  150. package/fixtures/openapi/TemplateMigrationPolicy/invalid/blank-guide.json +5 -0
  151. package/fixtures/openapi/TemplateMigrationPolicy/invalid/missing-guide.json +4 -0
  152. package/fixtures/openapi/TemplateMigrationPolicy/invalid/unknown-escape-hatch.json +6 -0
  153. package/fixtures/openapi/TemplateMigrationPolicy/valid/legacy-safe.json +4 -0
  154. package/fixtures/openapi/TemplateMigrationPolicy/valid/ordered-data-change.json +5 -0
  155. package/fixtures/openapi/TemplateUpdateBatchInput/invalid/duplicate-candidates.json +11 -0
  156. package/fixtures/openapi/TemplateUpdateBatchInput/invalid/empty-identities.json +10 -0
  157. package/fixtures/openapi/TemplateUpdateBatchInput/valid/staged-adoption.json +10 -0
  158. package/fixtures/openapi/TemplateUpdatePolicyInput/invalid/unknown-mode.json +8 -0
  159. package/fixtures/openapi/TemplateUpdatePolicyInput/valid/explicit-automatic.json +8 -0
  160. package/fixtures/openapi/TemplateUpdatePreviewInput/invalid/empty-identities.json +10 -0
  161. package/fixtures/openapi/TemplateUpdatePreviewInput/invalid/empty-targets.json +6 -0
  162. package/fixtures/openapi/TemplateUpdatePreviewInput/valid/cross-version-manual.json +14 -0
  163. package/gen/go/sohaapi/client.go +1 -1
  164. package/gen/go/sohaapi/client_test.go +2 -2
  165. package/gen/go/sohaapi/import_source_compat_test.go +12 -0
  166. package/gen/go/sohaapi/proxy_configuration_test.go +38 -0
  167. package/gen/go/sohaapi/types.go +3760 -345
  168. package/gen/ts/sohaapi/index.ts +5573 -181
  169. package/go.mod +2 -2
  170. package/helmrelease/delivery.go +4 -1
  171. package/helmrelease/initialization.go +10 -0
  172. package/helmrelease/runtime/delivery.go +40 -10
  173. package/helmrelease/runtime/delivery_test.go +4 -0
  174. package/helmrelease/runtime/initialization.go +186 -0
  175. package/helmrelease/runtime/initialization_test.go +203 -0
  176. package/network/README.md +34 -0
  177. package/network/network-radius-accounting.schema.json +4 -1
  178. package/network/network-runtime-protocol.schema.json +2166 -317
  179. package/network/proxy-runtime.schema.json +482 -67
  180. package/openapi/network_control_test.go +64 -0
  181. package/openapi/soha-api.json +53524 -43459
  182. package/openapi/soha-api.yaml +26431 -20491
  183. package/openapi/spec_generated.go +3 -3
  184. package/openapi/spec_test.go +42 -0
  185. package/package.json +6 -3
  186. package/proxytransport/handler.go +271 -0
  187. package/proxytransport/probe.go +30 -0
  188. package/proxytransport/scope.go +89 -0
  189. package/proxytransport/transport_test.go +60 -0
  190. package/resource/models.go +2 -0
  191. package/resource/relations.go +225 -0
  192. package/resource/relations_test.go +47 -0
  193. package/resource/root_cr.go +38 -0
  194. package/resource/runtime/argocd_integration_test.go +1 -1
  195. package/resource/runtime/initialization.go +66 -0
  196. package/resource/runtime/initialization_test.go +80 -0
  197. package/resource/runtime/relations.go +161 -0
  198. package/resource/runtime/relations_test.go +100 -0
  199. package/resource/runtime/root_cr.go +79 -0
  200. package/resource/runtime/root_cr_test.go +118 -0
@@ -177,6 +177,9 @@
177
177
  "default": true,
178
178
  "type": "boolean"
179
179
  },
180
+ "migrationPolicy": {
181
+ "$ref": "#/$defs/TemplateMigrationPolicy"
182
+ },
180
183
  "variableSchema": {
181
184
  "additionalProperties": {
182
185
  "$ref": "#/$defs/DeliveryBuildVariableSchema"
@@ -317,6 +320,9 @@
317
320
  "health": {
318
321
  "$ref": "#/$defs/DeploymentTemplateHealth"
319
322
  },
323
+ "migrationPolicy": {
324
+ "$ref": "#/$defs/TemplateMigrationPolicy"
325
+ },
320
326
  "parameterSchema": {
321
327
  "$ref": "#/$defs/TemplateParameterSchema"
322
328
  },
@@ -804,21 +810,68 @@
804
810
  "additionalProperties": false,
805
811
  "allOf": [
806
812
  {
807
- "if": {
813
+ "else": {
814
+ "allOf": [
815
+ {
816
+ "if": {
817
+ "properties": {
818
+ "action": {
819
+ "enum": [
820
+ "build_deploy",
821
+ "deploy",
822
+ "config_update"
823
+ ]
824
+ }
825
+ }
826
+ },
827
+ "then": {
828
+ "required": [
829
+ "applicationEnvironmentId"
830
+ ]
831
+ }
832
+ }
833
+ ],
808
834
  "properties": {
809
- "action": {
810
- "enum": [
811
- "build_deploy",
812
- "deploy",
813
- "config_update"
814
- ]
835
+ "serviceId": {
836
+ "minLength": 1,
837
+ "type": "string"
815
838
  }
816
839
  }
817
840
  },
818
- "then": {
841
+ "if": {
819
842
  "required": [
820
- "applicationEnvironmentId"
843
+ "templateCandidateId"
821
844
  ]
845
+ },
846
+ "then": {
847
+ "not": {
848
+ "anyOf": [
849
+ {
850
+ "required": [
851
+ "helmRevision"
852
+ ]
853
+ },
854
+ {
855
+ "required": [
856
+ "buildArgs"
857
+ ]
858
+ },
859
+ {
860
+ "required": [
861
+ "repositoryRefs"
862
+ ]
863
+ }
864
+ ]
865
+ },
866
+ "properties": {
867
+ "action": {
868
+ "const": "config_update"
869
+ },
870
+ "releaseBundleId": {
871
+ "maxLength": 0,
872
+ "type": "string"
873
+ }
874
+ }
822
875
  }
823
876
  },
824
877
  {
@@ -897,6 +950,10 @@
897
950
  "type": "array"
898
951
  },
899
952
  "serviceId": {
953
+ "type": "string"
954
+ },
955
+ "templateCandidateId": {
956
+ "maxLength": 256,
900
957
  "minLength": 1,
901
958
  "type": "string"
902
959
  }
@@ -1068,6 +1125,9 @@
1068
1125
  "enabled": {
1069
1126
  "default": true,
1070
1127
  "type": "boolean"
1128
+ },
1129
+ "migrationPolicy": {
1130
+ "$ref": "#/$defs/TemplateMigrationPolicy"
1071
1131
  }
1072
1132
  },
1073
1133
  "required": [
@@ -1376,6 +1436,64 @@
1376
1436
  },
1377
1437
  "type": "object"
1378
1438
  },
1439
+ "TemplateMigrationPolicy": {
1440
+ "additionalProperties": false,
1441
+ "if": {
1442
+ "anyOf": [
1443
+ {
1444
+ "properties": {
1445
+ "sequential": {
1446
+ "const": true
1447
+ }
1448
+ },
1449
+ "required": [
1450
+ "sequential"
1451
+ ]
1452
+ },
1453
+ {
1454
+ "properties": {
1455
+ "irreversible": {
1456
+ "const": true
1457
+ }
1458
+ },
1459
+ "required": [
1460
+ "irreversible"
1461
+ ]
1462
+ }
1463
+ ]
1464
+ },
1465
+ "properties": {
1466
+ "guide": {
1467
+ "description": "Human migration/recovery instructions. Never executed as a script.",
1468
+ "maxLength": 2000,
1469
+ "type": "string"
1470
+ },
1471
+ "irreversible": {
1472
+ "description": "Forward changes require approval; ordinary rollback across this boundary is forbidden.",
1473
+ "type": "boolean"
1474
+ },
1475
+ "sequential": {
1476
+ "description": "Must cross this boundary as adjacent published versions; approval cannot waive it.",
1477
+ "type": "boolean"
1478
+ }
1479
+ },
1480
+ "required": [
1481
+ "sequential",
1482
+ "irreversible"
1483
+ ],
1484
+ "then": {
1485
+ "properties": {
1486
+ "guide": {
1487
+ "minLength": 1,
1488
+ "pattern": "\\S"
1489
+ }
1490
+ },
1491
+ "required": [
1492
+ "guide"
1493
+ ]
1494
+ },
1495
+ "type": "object"
1496
+ },
1379
1497
  "TemplateParameterSchema": {
1380
1498
  "additionalProperties": false,
1381
1499
  "description": "Restricted JSON Schema. Objects reject undeclared keys unless mapValues specifies their schema. secret_reference accepts only a namespaced Secret name/key reference, never a plaintext credential. External references and executable expressions are unsupported.",
@@ -0,0 +1,64 @@
1
+ package endpointcontrol
2
+
3
+ import (
4
+ "errors"
5
+ "time"
6
+ )
7
+
8
+ // Required booleans must be present, including explicit false. Missing input
9
+ // must never become a newer all-off policy through Go zero values.
10
+ func (c *Controls) UnmarshalJSON(raw []byte) error {
11
+ var w struct {
12
+ USB *bool `json:"usb"`
13
+ Print *bool `json:"print"`
14
+ Clipboard *bool `json:"clipboard"`
15
+ }
16
+ if err := Decode(raw, &w); err != nil {
17
+ return err
18
+ }
19
+ if w.USB == nil || w.Print == nil || w.Clipboard == nil {
20
+ return errors.New("all native channel decisions are required")
21
+ }
22
+ *c = Controls{USB: *w.USB, Print: *w.Print, Clipboard: *w.Clipboard}
23
+ return nil
24
+ }
25
+ func (d *Desired) UnmarshalJSON(raw []byte) error {
26
+ var w struct {
27
+ Configured *bool `json:"configured"`
28
+ SchemaVersion string `json:"schemaVersion"`
29
+ DeviceID string `json:"deviceId"`
30
+ RuntimeID string `json:"runtimeId"`
31
+ CredentialID string `json:"credentialId"`
32
+ PolicyVersion *int64 `json:"policyVersion"`
33
+ Controls *Controls `json:"controls"`
34
+ Challenge string `json:"challenge"`
35
+ IssuedAt *time.Time `json:"issuedAt"`
36
+ ValidUntil *time.Time `json:"validUntil"`
37
+ }
38
+ if err := Decode(raw, &w); err != nil {
39
+ return err
40
+ }
41
+ if w.Configured == nil || w.PolicyVersion == nil || w.Controls == nil || w.IssuedAt == nil || w.ValidUntil == nil {
42
+ return errors.New("native channel envelope has missing required fields")
43
+ }
44
+ *d = Desired{Configured: *w.Configured, SchemaVersion: w.SchemaVersion, DeviceID: w.DeviceID, RuntimeID: w.RuntimeID, CredentialID: w.CredentialID, PolicyVersion: *w.PolicyVersion, Controls: *w.Controls, Challenge: w.Challenge, IssuedAt: *w.IssuedAt, ValidUntil: *w.ValidUntil}
45
+ return nil
46
+ }
47
+ func (c *Channel) UnmarshalJSON(raw []byte) error {
48
+ var w struct {
49
+ Name string `json:"name"`
50
+ Scope string `json:"scope"`
51
+ State string `json:"state"`
52
+ Targets *int `json:"targets"`
53
+ Controlled *int `json:"controlled"`
54
+ ReasonCode string `json:"reasonCode"`
55
+ }
56
+ if err := Decode(raw, &w); err != nil {
57
+ return err
58
+ }
59
+ if w.Targets == nil || w.Controlled == nil {
60
+ return errors.New("native channel counts are required")
61
+ }
62
+ *c = Channel{Name: w.Name, Scope: w.Scope, State: w.State, Targets: *w.Targets, Controlled: *w.Controlled, ReasonCode: w.ReasonCode}
63
+ return nil
64
+ }
@@ -0,0 +1,138 @@
1
+ // Package endpointcontrol owns the bounded native endpoint-channel protocol.
2
+ package endpointcontrol
3
+
4
+ import (
5
+ "bytes"
6
+ "encoding/json"
7
+ "errors"
8
+ "io"
9
+ "regexp"
10
+ "time"
11
+ )
12
+
13
+ const Version = "endpoint-controls/v1alpha1"
14
+ const Capability = "windows-channel-controls-v1"
15
+
16
+ type Controls struct {
17
+ USB bool `json:"usb"`
18
+ Print bool `json:"print"`
19
+ Clipboard bool `json:"clipboard"`
20
+ }
21
+ type Policy struct {
22
+ DeviceID string `json:"deviceId"`
23
+ Revision int64 `json:"revision"`
24
+ PublishedVersion int64 `json:"publishedVersion"`
25
+ Draft Controls `json:"draft"`
26
+ Published *Controls `json:"published,omitempty"`
27
+ UpdatedAt time.Time `json:"updatedAt"`
28
+ }
29
+ type SaveInput struct {
30
+ ExpectedRevision int64 `json:"expectedRevision"`
31
+ Draft Controls `json:"draft"`
32
+ }
33
+ type PublishInput struct {
34
+ ExpectedRevision int64 `json:"expectedRevision"`
35
+ }
36
+ type Desired struct {
37
+ Configured bool `json:"configured"`
38
+ SchemaVersion string `json:"schemaVersion"`
39
+ DeviceID string `json:"deviceId"`
40
+ RuntimeID string `json:"runtimeId"`
41
+ CredentialID string `json:"credentialId"`
42
+ PolicyVersion int64 `json:"policyVersion"`
43
+ Controls Controls `json:"controls"`
44
+ Challenge string `json:"challenge"`
45
+ IssuedAt time.Time `json:"issuedAt"`
46
+ ValidUntil time.Time `json:"validUntil"`
47
+ }
48
+ type Channel struct {
49
+ Name string `json:"name"`
50
+ Scope string `json:"scope"`
51
+ State string `json:"state"`
52
+ Targets int `json:"targets"`
53
+ Controlled int `json:"controlled"`
54
+ ReasonCode string `json:"reasonCode"`
55
+ }
56
+ type Receipt struct {
57
+ SchemaVersion string `json:"schemaVersion"`
58
+ DeviceID string `json:"deviceId"`
59
+ RuntimeID string `json:"runtimeId"`
60
+ CredentialID string `json:"credentialId"`
61
+ PolicyVersion int64 `json:"policyVersion"`
62
+ Challenge string `json:"challenge"`
63
+ ObservedAt time.Time `json:"observedAt"`
64
+ Channels []Channel `json:"channels"`
65
+ }
66
+ type View struct {
67
+ Policy Policy `json:"policy"`
68
+ Receipt *Observation `json:"receipt,omitempty"`
69
+ ReceivedAt *time.Time `json:"receivedAt,omitempty"`
70
+ Fresh bool `json:"fresh"`
71
+ RuntimeCount int `json:"runtimeCount"`
72
+ PendingRuntimeCount int `json:"pendingRuntimeCount"`
73
+ }
74
+ type Observation struct {
75
+ RuntimeID string `json:"runtimeId"`
76
+ CredentialID string `json:"credentialId"`
77
+ PolicyVersion int64 `json:"policyVersion"`
78
+ ObservedAt time.Time `json:"observedAt"`
79
+ Channels []Channel `json:"channels"`
80
+ }
81
+
82
+ var identifier = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$`)
83
+ var challenge = regexp.MustCompile(`^[A-Za-z0-9_-]{43}$`)
84
+ var reason = regexp.MustCompile(`^[a-z][a-z0-9_]{0,63}$`)
85
+
86
+ func Decode(raw []byte, out any) error {
87
+ if len(raw) > 32768 {
88
+ return errors.New("endpoint channel message too large")
89
+ }
90
+ d := json.NewDecoder(bytes.NewReader(raw))
91
+ d.DisallowUnknownFields()
92
+ if err := d.Decode(out); err != nil {
93
+ return err
94
+ }
95
+ if d.Decode(new(any)) != io.EOF {
96
+ return errors.New("trailing channel message")
97
+ }
98
+ return nil
99
+ }
100
+ func ValidDesired(d Desired, now time.Time) bool {
101
+ return (d.Configured && d.PolicyVersion > 0 || !d.Configured && d.PolicyVersion == 0) && validIdentity(d.SchemaVersion, d.DeviceID, d.RuntimeID, d.CredentialID, d.PolicyVersion, d.Challenge) && !d.IssuedAt.After(now.Add(30*time.Second)) && d.ValidUntil.After(now) && d.ValidUntil.After(d.IssuedAt) && d.ValidUntil.Sub(d.IssuedAt) <= time.Minute
102
+ }
103
+ func ValidReceipt(r Receipt, now time.Time) bool {
104
+ if !validIdentity(r.SchemaVersion, r.DeviceID, r.RuntimeID, r.CredentialID, r.PolicyVersion, r.Challenge) || len(r.Channels) != 3 || r.ObservedAt.Before(now.Add(-time.Minute)) || r.ObservedAt.After(now.Add(30*time.Second)) {
105
+ return false
106
+ }
107
+ seen := map[string]bool{}
108
+ for _, c := range r.Channels {
109
+ if seen[c.Name] || !ValidChannel(c) {
110
+ return false
111
+ }
112
+ seen[c.Name] = true
113
+ }
114
+ return seen["usb"] && seen["print"] && seen["clipboard"]
115
+ }
116
+ func validIdentity(version, device, runtime, credential string, revision int64, nonce string) bool {
117
+ return version == Version && identifier.MatchString(device) && identifier.MatchString(runtime) && identifier.MatchString(credential) && revision >= 0 && challenge.MatchString(nonce)
118
+ }
119
+ func ValidChannel(c Channel) bool {
120
+ scopes := map[string]string{"usb": "current_usb_volumes", "print": "local_new_print_jobs", "clipboard": "enrolled_console_win32"}
121
+ if scopes[c.Name] == "" || c.Scope != scopes[c.Name] || c.Targets < 0 || c.Targets > 256 || c.Controlled < 0 || c.Controlled > c.Targets || !reason.MatchString(c.ReasonCode) {
122
+ return false
123
+ }
124
+ if c.State == "controlled" && (c.Targets == 0 || c.Controlled != c.Targets) {
125
+ return false
126
+ }
127
+ if (c.State == "off" || c.State == "unconfigured" || c.State == "not_present") && c.Controlled != 0 {
128
+ return false
129
+ }
130
+ if (c.State == "unconfigured" || c.State == "not_present") && c.Targets != 0 {
131
+ return false
132
+ }
133
+ switch c.State {
134
+ case "off", "unconfigured", "not_present", "controlled", "partial", "failed", "unsupported", "conflict":
135
+ return true
136
+ }
137
+ return false
138
+ }
@@ -0,0 +1,51 @@
1
+ package endpointcontrol
2
+
3
+ import (
4
+ "testing"
5
+ "time"
6
+ )
7
+
8
+ func TestNativeReceiptCannotClaimContradictoryControl(t *testing.T) {
9
+ cases := []Channel{
10
+ {Name: "usb", Scope: "current_usb_volumes", State: "controlled", Targets: 1, Controlled: 0, ReasonCode: "native_scope_only"},
11
+ {Name: "usb", Scope: "current_usb_volumes", State: "controlled", Targets: 0, Controlled: 0, ReasonCode: "native_scope_only"},
12
+ {Name: "clipboard", Scope: "current_usb_volumes", State: "controlled", Targets: 1, Controlled: 1, ReasonCode: "native_scope_only"},
13
+ {Name: "print", Scope: "local_new_print_jobs", State: "off", Targets: 1, Controlled: 1, ReasonCode: "disabled"},
14
+ }
15
+ for _, c := range cases {
16
+ if ValidChannel(c) {
17
+ t.Fatalf("contradictory control accepted: %+v", c)
18
+ }
19
+ }
20
+ if !ValidChannel(Channel{Name: "clipboard", Scope: "enrolled_console_win32", State: "controlled", Targets: 1, Controlled: 1, ReasonCode: "native_scope_only"}) {
21
+ t.Fatal("bounded native control rejected")
22
+ }
23
+ }
24
+ func TestUnconfiguredIsNotAPublishedRelease(t *testing.T) {
25
+ now := time.Now().UTC()
26
+ d := Desired{SchemaVersion: Version, DeviceID: "d", RuntimeID: "r", CredentialID: "c", Challenge: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", IssuedAt: now, ValidUntil: now.Add(time.Minute)}
27
+ if !ValidDesired(d, now) {
28
+ t.Fatal("unconfigured envelope rejected")
29
+ }
30
+ d.PolicyVersion = 1
31
+ if ValidDesired(d, now) {
32
+ t.Fatal("positive unpublished revision accepted")
33
+ }
34
+ d.Configured = true
35
+ if !ValidDesired(d, now) {
36
+ t.Fatal("explicit published release rejected")
37
+ }
38
+ }
39
+
40
+ func TestMissingBooleanCannotDecodeAsRelease(t *testing.T) {
41
+ var controls Controls
42
+ for _, raw := range []string{`{}`, `{"usb":false,"print":false}`, `{"usb":false,"print":false,"clipboard":null}`} {
43
+ if Decode([]byte(raw), &controls) == nil {
44
+ t.Fatal("incomplete channel decisions accepted", raw)
45
+ }
46
+ }
47
+ var desired Desired
48
+ if Decode([]byte(`{"configured":true,"schemaVersion":"endpoint-controls/v1alpha1","deviceId":"device","runtimeId":"runtime","credentialId":"credential","policyVersion":99,"challenge":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","issuedAt":"2026-10-28T00:00:00Z","validUntil":"2026-10-28T00:01:00Z"}`), &desired) == nil {
49
+ t.Fatal("missing controls became release")
50
+ }
51
+ }