@opensoha/contracts 0.1.15 → 0.1.17

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 (211) hide show
  1. package/COMPATIBILITY.md +46 -0
  2. package/README.md +12 -3
  3. package/auth/catalog_generated.go +2 -2
  4. package/auth/catalog_test.go +32 -0
  5. package/auth/permission-catalog.json +537 -8
  6. package/compat/json-schema-ecosystem-0.1.0.json +1 -1
  7. package/compat/openapi-loose-boundaries-0.1.0.json +58 -1
  8. package/fixtures/json-schema/network-ingest-event/invalid/duplicate-event.json +33 -0
  9. package/fixtures/json-schema/network-ingest-event/invalid/message-payload-mismatch.json +23 -0
  10. package/fixtures/json-schema/network-ingest-event/invalid/password.json +22 -0
  11. package/fixtures/json-schema/network-ingest-event/invalid/proxy-flow-destination.json +28 -0
  12. package/fixtures/json-schema/network-ingest-event/valid/endpoint-proxy-flow.json +27 -0
  13. package/fixtures/json-schema/network-ingest-event/valid/freeradius-accounting.json +25 -0
  14. package/fixtures/json-schema/network-ingest-event/valid/gateway.json +53 -0
  15. package/fixtures/json-schema/network-radius-accounting/invalid/password.json +9 -0
  16. package/fixtures/json-schema/network-radius-accounting/valid/accounting.json +16 -0
  17. package/fixtures/json-schema/network-runtime-protocol/invalid/invalid-cidr.json +31 -0
  18. package/fixtures/json-schema/network-runtime-protocol/invalid/message-payload-mismatch.json +19 -0
  19. package/fixtures/json-schema/network-runtime-protocol/invalid/mihomo-raw-subscription.json +33 -0
  20. package/fixtures/json-schema/network-runtime-protocol/invalid/nas-authorization-password.json +18 -0
  21. package/fixtures/json-schema/network-runtime-protocol/invalid/nas-eap-without-certificate.json +17 -0
  22. package/fixtures/json-schema/network-runtime-protocol/invalid/private-key.json +20 -0
  23. package/fixtures/json-schema/network-runtime-protocol/invalid/raw-credential-reference.json +16 -0
  24. package/fixtures/json-schema/network-runtime-protocol/invalid/runtime-interval-too-short.json +23 -0
  25. package/fixtures/json-schema/network-runtime-protocol/invalid/wireguard-capability-without-key.json +19 -0
  26. package/fixtures/json-schema/network-runtime-protocol/invalid/wireguard-default-route.json +31 -0
  27. package/fixtures/json-schema/network-runtime-protocol/invalid/ztna-without-access-grant.json +17 -0
  28. package/fixtures/json-schema/network-runtime-protocol/valid/configuration-desired.json +86 -0
  29. package/fixtures/json-schema/network-runtime-protocol/valid/direct-ztna-connect.json +19 -0
  30. package/fixtures/json-schema/network-runtime-protocol/valid/direct-ztna-result.json +33 -0
  31. package/fixtures/json-schema/network-runtime-protocol/valid/internal-ztna-connect.json +19 -0
  32. package/fixtures/json-schema/network-runtime-protocol/valid/mihomo-app-subscription-configuration.json +32 -0
  33. package/fixtures/json-schema/network-runtime-protocol/valid/mihomo-manual-node-configuration.json +32 -0
  34. package/fixtures/json-schema/network-runtime-protocol/valid/mihomo-wireguard-configuration.json +70 -0
  35. package/fixtures/json-schema/network-runtime-protocol/valid/nas-authorization-request.json +19 -0
  36. package/fixtures/json-schema/network-runtime-protocol/valid/nas-authorization-result.json +24 -0
  37. package/fixtures/json-schema/network-runtime-protocol/valid/nas-session-command-result.json +17 -0
  38. package/fixtures/json-schema/network-runtime-protocol/valid/nas-session-command.json +27 -0
  39. package/fixtures/json-schema/network-runtime-protocol/valid/vpn-connect-request.json +17 -0
  40. package/fixtures/json-schema/network-runtime-protocol/valid/vpn-connect-result.json +34 -0
  41. package/fixtures/json-schema/network-runtime-protocol/valid/wireguard-enrollment.json +20 -0
  42. package/fixtures/json-schema/network-runtime-protocol/valid/wireguard-site-link.json +48 -0
  43. package/fixtures/openapi/AlertRuleEnvelope/invalid/missing-data.json +1 -0
  44. package/fixtures/openapi/AlertRuleEnvelope/valid/metrics.json +11 -0
  45. package/fixtures/openapi/AlertRuleInput/invalid/missing-name.json +3 -0
  46. package/fixtures/openapi/AlertRuleInput/valid/metrics.json +25 -0
  47. package/fixtures/openapi/AlertRuleRunListEnvelope/invalid/missing-rule-id.json +12 -0
  48. package/fixtures/openapi/AlertRuleRunListEnvelope/valid/items.json +13 -0
  49. package/fixtures/openapi/AlertRuleTestResultEnvelope/invalid/missing-executed-at.json +5 -0
  50. package/fixtures/openapi/AlertRuleTestResultEnvelope/valid/clear.json +10 -0
  51. package/fixtures/openapi/AnnouncementReceiptPageEnvelope/invalid/missing-read-count.json +9 -0
  52. package/fixtures/openapi/AnnouncementReceiptPageEnvelope/valid/read-and-unread.json +28 -0
  53. package/fixtures/openapi/ApplicationRuntimeEnvironment/invalid/unknown-status.json +7 -0
  54. package/fixtures/openapi/ApplicationRuntimeEnvironment/valid/unavailable.json +7 -0
  55. package/fixtures/openapi/ApplicationWorkflowInput/invalid/owner-fields.json +7 -0
  56. package/fixtures/openapi/ApplicationWorkflowInput/valid/minimal.json +9 -0
  57. package/fixtures/openapi/BrandingAssetUploadEnvelope/valid/svg-data-url.json +5 -0
  58. package/fixtures/openapi/BrowserHandoffCompletionEnvelope/invalid/token-leak.json +7 -0
  59. package/fixtures/openapi/BrowserHandoffCompletionEnvelope/valid/completed.json +6 -0
  60. package/fixtures/openapi/BrowserHandoffEnvelope/invalid/missing-principal.json +12 -0
  61. package/fixtures/openapi/BrowserHandoffEnvelope/valid/pending.json +11 -0
  62. package/fixtures/openapi/ChangePasswordRequest/invalid/missing-current-password.json +3 -0
  63. package/fixtures/openapi/ChangePasswordRequest/invalid/short-new-password.json +4 -0
  64. package/fixtures/openapi/ChangePasswordRequest/valid/password.json +4 -0
  65. package/fixtures/openapi/DesktopAuthAttemptCreateRequest/invalid/https-redirect.json +6 -0
  66. package/fixtures/openapi/DesktopAuthAttemptCreateRequest/invalid/localhost-redirect.json +6 -0
  67. package/fixtures/openapi/DesktopAuthAttemptCreateRequest/invalid/plain-challenge.json +6 -0
  68. package/fixtures/openapi/DesktopAuthAttemptCreateRequest/invalid/privileged-port.json +6 -0
  69. package/fixtures/openapi/DesktopAuthAttemptCreateRequest/invalid/short-callback.json +6 -0
  70. package/fixtures/openapi/DesktopAuthAttemptCreateRequest/valid/loopback.json +6 -0
  71. package/fixtures/openapi/DesktopAuthAttemptEnvelope/invalid/missing-expiry.json +6 -0
  72. package/fixtures/openapi/DesktopAuthAttemptEnvelope/valid/attempt.json +7 -0
  73. package/fixtures/openapi/DesktopAuthAttemptExchangeRequest/invalid/short-verifier.json +4 -0
  74. package/fixtures/openapi/DesktopAuthAttemptExchangeRequest/valid/exchange.json +4 -0
  75. package/fixtures/openapi/EndpointDevice/invalid/missing-owner.json +8 -0
  76. package/fixtures/openapi/EndpointDevice/valid/active.json +35 -0
  77. package/fixtures/openapi/EndpointDeviceRegistrationInput/invalid/client-owned-status.json +5 -0
  78. package/fixtures/openapi/EndpointDeviceRegistrationInput/invalid/oversized-network-inventory.json +18 -0
  79. package/fixtures/openapi/EndpointDeviceRegistrationInput/valid/macos.json +28 -0
  80. package/fixtures/openapi/IdentityApplication/invalid/missing-id.json +7 -0
  81. package/fixtures/openapi/IdentityApplication/valid/full.json +30 -0
  82. package/fixtures/openapi/IdentityApplicationEnvelope/invalid/missing-data.json +1 -0
  83. package/fixtures/openapi/IdentityApplicationEnvelope/valid/minimal.json +10 -0
  84. package/fixtures/openapi/IdentityApplicationLaunchListEnvelope/invalid/missing-user.json +11 -0
  85. package/fixtures/openapi/IdentityApplicationLaunchListEnvelope/valid/minimal.json +12 -0
  86. package/fixtures/openapi/LLMUpstreamTestResultEnvelope/invalid/models-not-array.json +9 -0
  87. package/fixtures/openapi/LLMUpstreamTestResultEnvelope/valid/discovered-models.json +11 -0
  88. package/fixtures/openapi/LogQuery/invalid/empty-data-source-id.json +7 -0
  89. package/fixtures/openapi/LogQuery/valid/durable-data-source.json +14 -0
  90. package/fixtures/openapi/ManifestPackage/invalid/missing-application.json +12 -0
  91. package/fixtures/openapi/ManifestPackage/valid/service-scoped.json +29 -0
  92. package/fixtures/openapi/NetworkAccessGrant/invalid/token-digest.json +15 -0
  93. package/fixtures/openapi/NetworkAccessGrant/valid/issued.json +14 -0
  94. package/fixtures/openapi/NetworkAccessGrantInput/invalid/no-resources.json +7 -0
  95. package/fixtures/openapi/NetworkAccessGrantInput/valid/direct-ztna.json +8 -0
  96. package/fixtures/openapi/NetworkAccessGrantSecret/invalid/short-token.json +17 -0
  97. package/fixtures/openapi/NetworkAccessGrantSecret/valid/issued.json +17 -0
  98. package/fixtures/openapi/NetworkAccessPolicy/invalid/missing-version.json +20 -0
  99. package/fixtures/openapi/NetworkAccessPolicy/valid/published-draft.json +21 -0
  100. package/fixtures/openapi/NetworkAccessPolicyInput/invalid/duplicate-team.json +17 -0
  101. package/fixtures/openapi/NetworkAccessPolicyInput/valid/team-and-tag-allow.json +17 -0
  102. package/fixtures/openapi/NetworkConflictAnalysisRequest/invalid/ipv6-range.json +9 -0
  103. package/fixtures/openapi/NetworkConflictAnalysisRequest/valid/runtime-ranges.json +15 -0
  104. package/fixtures/openapi/NetworkConnectionOption/invalid/secret.json +10 -0
  105. package/fixtures/openapi/NetworkConnectionOption/valid/wifi.json +9 -0
  106. package/fixtures/openapi/NetworkConnectionOption/valid/wired.json +8 -0
  107. package/fixtures/openapi/NetworkGateway/invalid/missing-site.json +7 -0
  108. package/fixtures/openapi/NetworkGateway/valid/online.json +23 -0
  109. package/fixtures/openapi/NetworkGatewayInput/invalid/default-route-overlay.json +13 -0
  110. package/fixtures/openapi/NetworkGatewayInput/valid/routed.json +14 -0
  111. package/fixtures/openapi/NetworkLease/invalid/empty-cidrs.json +12 -0
  112. package/fixtures/openapi/NetworkLease/valid/active.json +12 -0
  113. package/fixtures/openapi/NetworkMihomoProfile/invalid/subscription-leak.json +20 -0
  114. package/fixtures/openapi/NetworkMihomoProfile/valid/managed.json +21 -0
  115. package/fixtures/openapi/NetworkMihomoProfileInput/invalid/app-subscription-secret.json +14 -0
  116. package/fixtures/openapi/NetworkMihomoProfileInput/invalid/manual-node-with-subscription.json +20 -0
  117. package/fixtures/openapi/NetworkMihomoProfileInput/valid/app-subscription.json +14 -0
  118. package/fixtures/openapi/NetworkMihomoProfileInput/valid/managed.json +16 -0
  119. package/fixtures/openapi/NetworkMihomoProfileInput/valid/manual-node.json +21 -0
  120. package/fixtures/openapi/NetworkNASBinding/invalid/secret.json +13 -0
  121. package/fixtures/openapi/NetworkNASBinding/valid/default.json +16 -0
  122. package/fixtures/openapi/NetworkNASBindingInput/invalid/secret.json +10 -0
  123. package/fixtures/openapi/NetworkNASBindingInput/valid/default.json +13 -0
  124. package/fixtures/openapi/NetworkPolicyPreviewRequest/invalid/unknown-mode.json +6 -0
  125. package/fixtures/openapi/NetworkPolicyPreviewRequest/valid/external-direct-ztna.json +6 -0
  126. package/fixtures/openapi/NetworkPolicyPreviewRequest/valid/external-vpn-ztna.json +6 -0
  127. package/fixtures/openapi/NetworkPolicyPreviewRequest/valid/external-vpn.json +6 -0
  128. package/fixtures/openapi/NetworkPolicyPreviewRequest/valid/internal-direct.json +7 -0
  129. package/fixtures/openapi/NetworkPolicyPreviewRequest/valid/internal-ztna.json +7 -0
  130. package/fixtures/openapi/NetworkPolicyPreviewResult/invalid/bypass-path.json +9 -0
  131. package/fixtures/openapi/NetworkPolicyPreviewResult/valid/onboarding.json +10 -0
  132. package/fixtures/openapi/NetworkPolicyPreviewResult/valid/protected-broad-path-denied.json +9 -0
  133. package/fixtures/openapi/NetworkPolicyPreviewResult/valid/protected-resource.json +9 -0
  134. package/fixtures/openapi/NetworkPolicyPreviewResult/valid/quarantine.json +10 -0
  135. package/fixtures/openapi/NetworkPolicyPreviewResult/valid/restricted.json +10 -0
  136. package/fixtures/openapi/NetworkPolicySnapshot/invalid/zero-version.json +7 -0
  137. package/fixtures/openapi/NetworkPolicySnapshot/valid/published.json +7 -0
  138. package/fixtures/openapi/NetworkResourceInput/invalid/invalid-port.json +10 -0
  139. package/fixtures/openapi/NetworkResourceInput/valid/protected-cidr.json +10 -0
  140. package/fixtures/openapi/NetworkRuntimeEnrollmentInput/invalid/excessive-ttl.json +7 -0
  141. package/fixtures/openapi/NetworkRuntimeEnrollmentInput/valid/endpoint.json +7 -0
  142. package/fixtures/openapi/NetworkRuntimeEnrollmentSecret/invalid/short-token.json +15 -0
  143. package/fixtures/openapi/NetworkRuntimeEnrollmentSecret/valid/pending.json +15 -0
  144. package/fixtures/openapi/NetworkSession/invalid/unknown-status.json +14 -0
  145. package/fixtures/openapi/NetworkSession/valid/restricted-vpn-ztna.json +16 -0
  146. package/fixtures/openapi/NetworkSessionActionExecuteInput/invalid/short-hash.json +6 -0
  147. package/fixtures/openapi/NetworkSessionActionExecuteInput/valid/restrict.json +6 -0
  148. package/fixtures/openapi/NetworkSessionActionInput/invalid/unknown-action.json +5 -0
  149. package/fixtures/openapi/NetworkSessionActionInput/valid/restrict.json +5 -0
  150. package/fixtures/openapi/NetworkSessionActionPlan/invalid/missing-hash.json +12 -0
  151. package/fixtures/openapi/NetworkSessionActionPlan/valid/fallback-disconnect.json +13 -0
  152. package/fixtures/openapi/NetworkSessionCommand/invalid/unknown-status.json +14 -0
  153. package/fixtures/openapi/NetworkSessionCommand/valid/pending.json +14 -0
  154. package/fixtures/openapi/NetworkSiteInput/invalid/unknown-status.json +4 -0
  155. package/fixtures/openapi/NetworkSiteInput/valid/shanghai.json +5 -0
  156. package/fixtures/openapi/NetworkSiteProfileBinding/invalid/vlan.json +9 -0
  157. package/fixtures/openapi/NetworkSiteProfileBinding/valid/default.json +10 -0
  158. package/fixtures/openapi/NetworkSiteProfileBindingInput/invalid/timeout.json +7 -0
  159. package/fixtures/openapi/NetworkSiteProfileBindingInput/valid/default.json +7 -0
  160. package/fixtures/openapi/NetworkSpaceInput/invalid/empty-cidrs.json +6 -0
  161. package/fixtures/openapi/NetworkSpaceInput/invalid/malformed-cidr.json +6 -0
  162. package/fixtures/openapi/NetworkSpaceInput/valid/corp.json +6 -0
  163. package/fixtures/openapi/NetworkTelemetrySummary/invalid/raw-destination.json +29 -0
  164. package/fixtures/openapi/NetworkTelemetrySummary/valid/proxy-flow.json +36 -0
  165. package/fixtures/openapi/OIDCClientInput/valid/generated-client-id.json +8 -0
  166. package/fixtures/openapi/OIDCClientSecretRevealEnvelope/invalid/missing-secret.json +6 -0
  167. package/fixtures/openapi/OIDCClientSecretRevealEnvelope/valid/reveal.json +7 -0
  168. package/fixtures/openapi/PasswordChangeResult/invalid/unexpected-status.json +3 -0
  169. package/fixtures/openapi/PasswordChangeResult/valid/ok.json +3 -0
  170. package/fixtures/openapi/PortalApplicationListEnvelope/invalid/missing-items.json +1 -0
  171. package/fixtures/openapi/PortalApplicationListEnvelope/valid/minimal.json +12 -0
  172. package/fixtures/openapi/PortalBootstrapEnvelope/invalid/missing-security.json +17 -0
  173. package/fixtures/openapi/PortalBootstrapEnvelope/valid/minimal.json +31 -0
  174. package/fixtures/openapi/PortalLaunchDecisionEnvelope/invalid/missing-launch-url.json +14 -0
  175. package/fixtures/openapi/PortalLaunchDecisionEnvelope/valid/desktop-handoff.json +16 -0
  176. package/fixtures/openapi/PortalLaunchRequest/invalid/unknown-surface.json +3 -0
  177. package/fixtures/openapi/PortalLaunchRequest/valid/default-web.json +1 -0
  178. package/fixtures/openapi/PortalLaunchRequest/valid/desktop.json +3 -0
  179. package/fixtures/openapi/PortalSecuritySummaryEnvelope/invalid/missing-session-count.json +15 -0
  180. package/fixtures/openapi/PortalSecuritySummaryEnvelope/valid/minimal.json +16 -0
  181. package/fixtures/openapi/ResourceLease/invalid/missing-session.json +10 -0
  182. package/fixtures/openapi/ResourceLease/valid/active.json +11 -0
  183. package/fixtures/openapi/ScopeGrantInput/invalid/delivery-missing-business-line.json +7 -0
  184. package/fixtures/openapi/ScopeGrantInput/invalid/platform-missing-clusters.json +7 -0
  185. package/fixtures/openapi/ScopeGrantInput/valid/platform.json +8 -0
  186. package/fixtures/openapi/SoftwarePackage/valid/software-package.json +6 -0
  187. package/fixtures/openapi/SoftwarePackageDownloadRecord/invalid/negative-duration.json +6 -0
  188. package/fixtures/openapi/SoftwarePackageDownloadRecord/valid/software-package-download-record.json +8 -0
  189. package/fixtures/openapi/SoftwarePackageURLImportRequest/valid/software-package-url-import-request.json +4 -0
  190. package/fixtures/openapi/SoftwareStorage/invalid/software-storage.json +1 -1
  191. package/fixtures/openapi/SoftwareStorage/valid/software-storage.json +13 -1
  192. package/fixtures/openapi/SystemIntegrationCreateRequest/invalid/s3-storage-missing-name.json +5 -0
  193. package/fixtures/openapi/SystemIntegrationCreateRequest/valid/s3-software-storage.json +18 -0
  194. package/fixtures/openapi/UpdateProfileRequest/invalid/invalid-avatar-fit.json +5 -0
  195. package/fixtures/openapi/UpdateProfileRequest/invalid/missing-email.json +3 -0
  196. package/fixtures/openapi/UpdateProfileRequest/valid/profile.json +7 -0
  197. package/gen/go/sohaapi/client.go +210 -5
  198. package/gen/go/sohaapi/client_test.go +128 -5
  199. package/gen/go/sohaapi/types.go +3326 -369
  200. package/gen/go/sohaapi/types_test.go +22 -0
  201. package/gen/ts/sohaapi/index.ts +11612 -6479
  202. package/network/README.md +116 -0
  203. package/network/network-ingest-event.schema.json +187 -0
  204. package/network/network-radius-accounting.schema.json +41 -0
  205. package/network/network-runtime-protocol.schema.json +752 -0
  206. package/network/schema.go +26 -0
  207. package/network/schema_test.go +22 -0
  208. package/openapi/soha-api.json +29591 -18317
  209. package/openapi/soha-api.yaml +11603 -6720
  210. package/openapi/spec_generated.go +3 -3
  211. package/package.json +5 -1
@@ -0,0 +1,116 @@
1
+ # Network contract semantics
2
+
3
+ The JSON Schemas in this directory define strict serialized shapes. Every
4
+ consumer must also enforce the cross-field rules below before using a message
5
+ for authorization, routing, firewall configuration or accounting.
6
+
7
+ ## Runtime messages
8
+
9
+ - Runtime mTLS certificates carry exactly one Soha URI SAN:
10
+ `spiffe://opensoha.local/network-control/{runtimeKind}/{runtimeId}`.
11
+ Ingest certificates use the separate scope
12
+ `spiffe://opensoha.local/network-ingest/{producerKind}/{producerId}` and may
13
+ not be reused for control. The body identity is only a consistency check.
14
+ - `devicePublicKey` is one PKIX `PUBLIC KEY` PEM block and must match the
15
+ public key of the authenticated enrollment certificate.
16
+ - A runtime advertising the `wireguard` capability must submit exactly one
17
+ WireGuard public key during enrollment. The corresponding private key is
18
+ generated and retained in runtime-local protected storage. Re-enrollment
19
+ advances the credential/key generation and retires the prior public key.
20
+ - Reject a message when `expiresAt` is not later than `occurredAt` or is
21
+ already expired.
22
+ - Parse every CIDR with the platform standard library, require canonical
23
+ network form, and reject anything not accepted by `netip.ParsePrefix` or an
24
+ equivalent parser. `network-runtime/v1alpha1` carries IPv4 CIDRs only.
25
+ - Resolve `runtimeId` to the authenticated runtime credential. Never trust the
26
+ body to select another runtime.
27
+ - Accept `nas.*` messages only from or for a runtime whose registered kind is
28
+ `nas`; the authenticated runtime must also match the active NAS binding for
29
+ `payload.nasId` and its site.
30
+ - Treat `nas.authorization.request` as authorization input only. It never
31
+ carries a password, EAP exchange, RADIUS packet or shared secret. The
32
+ `password-compatible` method is lower trust than `eap-tls` and policy may
33
+ only preserve or reduce its access profile.
34
+ - For `eap-tls`, resolve the certificate's canonical serial plus authority key
35
+ identifier to one active endpoint credential and derive `deviceId` from that
36
+ credential. Match `subjectId` to the credential; treat `stationId` only as
37
+ NAS evidence, never as device identity. Password-compatible requests identify
38
+ the device explicitly and may not claim client-certificate binding fields.
39
+ - Emit RADIUS attributes only for an allow decision. The NAS adapter maps
40
+ `vlanId`, `filterId` and `sessionTimeoutSeconds` to standard RADIUS
41
+ attributes; vendor-specific attributes stay outside this contract.
42
+ - Match authorization result `requestId`, session command result `commandId`
43
+ and `sessionId` to the outstanding request or command before changing state.
44
+ - Copy the Soha authorization `sessionId` into the standard RADIUS `Class`
45
+ reply attribute and require NAS accounting to echo it as `sessionId` when
46
+ available. Session commands repeat the bound `subjectId` and `deviceId` so
47
+ the adapter can target CoA/Disconnect without querying management APIs.
48
+ - If a requested CoA is unsupported, issue a disconnect command when the NAS
49
+ binding supports it; otherwise fail closed and record the unsupported
50
+ enforcement result.
51
+ - For endpoint desired state, require every lease `deviceId` to match the
52
+ enrolled device bound to `runtimeId`.
53
+ - Require all leases in one desired state to belong to the expected session
54
+ and subject/device binding.
55
+ - Require lease `expiresAt` to be later than `issuedAt`, no later than the
56
+ enclosing message/configuration validity, and within the configured maximum
57
+ lease TTL.
58
+ - Apply only monotonically newer configuration versions. A rollback is a new
59
+ version whose content intentionally references a prior snapshot.
60
+ - Treat `runtimeIntervals` as optional scheduling guidance for backward
61
+ compatibility. When present, both heartbeat and configuration polling use
62
+ the same 30–300 second bounds; clients apply local ±20% jitter.
63
+ - Accept `vpn.connect.request` only from an enrolled endpoint bound to the
64
+ requested device. The selected site, NetworkSpace, gateway and NetworkLease
65
+ must all be active and authorized by the loaded policy snapshot.
66
+ - `vpn.connect.request.gatewayId` only selects an ingress gateway; it never
67
+ expands the requested site, NetworkSpace, resource or policy scope. When it
68
+ is omitted, the control plane may select an eligible gateway using the same
69
+ authorization checks.
70
+ - A gateway without `hubGatewayId` is a topology root. A gateway with
71
+ `hubGatewayId` is a routed spoke of that active root. Spokes may not point to
72
+ another spoke, use SNAT, or form direct spoke-to-spoke peers; traffic between
73
+ spokes transits the root.
74
+ - Every `advertisedCidrs` entry must be canonical, belong to an active
75
+ NetworkSpace at that gateway's site, and not overlap another active site or
76
+ gateway overlay prefix. A site link is transport authorization, not user or
77
+ resource authorization.
78
+ - WireGuard `allowedIPs` on an endpoint peer identify only that endpoint's
79
+ overlay address. A site peer may additionally carry the explicit overlay and
80
+ advertised prefixes of the remote topology members reachable through that
81
+ peer. Endpoint split routes still come only from active leases, and the
82
+ `0.0.0.0/0` default route is outside v1alpha1.
83
+ - Gateway firewall evaluation is ordered and default-deny. An exact active
84
+ ResourceLease allow precedes the matching ProtectedSet deny, which in turn
85
+ precedes a broader NetworkLease allow. Site-link rules carry
86
+ `siteLinkRuntimeId` plus an explicit direction and expire with the peer
87
+ credential; raw site LAN sources remain blocked from protected destinations.
88
+ - `routingMode=routed` requires the site LAN to route the overlay CIDR back to
89
+ the gateway. `routingMode=snat` is an explicit deployment fallback and does
90
+ not broaden any lease or firewall rule.
91
+ - Treat `credentialReference` only as an opaque runtime-local `secretref:`
92
+ identifier. It is never dereferenced by an untrusted caller and never
93
+ written to logs, telemetry or public responses.
94
+ - Validate ProtectedSet before NetworkLease coverage. A broad network lease
95
+ cannot authorize a protected resource without a valid ResourceLease.
96
+
97
+ ## Ingest batches
98
+
99
+ - Authenticate `producerId` from the transport credential; do not trust the
100
+ body to select another producer.
101
+ - Deduplicate by `(producerId, event.id)` for the retention window. Exact
102
+ duplicate objects are rejected by schema, but duplicates with changed
103
+ payloads must also be rejected.
104
+ - Accept out-of-order `sequence` values as telemetry while recording gaps and
105
+ regressions. Sequence never changes authorization.
106
+ - Require flow `windowEndedAt` to be later than `windowStartedAt` and
107
+ `deniedConnections <= connections`.
108
+ - Reject events outside the configured clock-skew and retention window.
109
+ - Enforce HTTP body, decompressed body, event-count and per-producer rate
110
+ limits before persistence.
111
+ - Accounting and flow events are observability input only. They cannot create,
112
+ renew, broaden or revoke a NetworkLease or ResourceLease.
113
+
114
+ Consumers must cover these rules with deterministic tests. Failure is
115
+ fail-closed for control messages and bounded reject/drop-with-counter behavior
116
+ for telemetry.
@@ -0,0 +1,187 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://contracts.opensoha.dev/network/network-ingest-event.schema.json",
4
+ "title": "OpenSoha Network Ingest Event Batch",
5
+ "description": "Strict bounded telemetry batches for runtime heartbeat, RADIUS Accounting, flow aggregates and connection summaries. Ingest events are not authorization facts.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schemaVersion", "batchId", "producerId", "producerKind", "sentAt", "events"],
9
+ "properties": {
10
+ "schemaVersion": { "const": "network-ingest/v1alpha1" },
11
+ "batchId": { "$ref": "#/$defs/Identifier" },
12
+ "producerId": { "$ref": "#/$defs/Identifier" },
13
+ "producerKind": { "enum": ["endpoint", "gateway", "freeradius", "network-control"] },
14
+ "sentAt": { "type": "string", "format": "date-time" },
15
+ "events": {
16
+ "type": "array",
17
+ "minItems": 1,
18
+ "maxItems": 1000,
19
+ "uniqueItems": true,
20
+ "items": { "$ref": "#/$defs/Event" }
21
+ }
22
+ },
23
+ "$defs": {
24
+ "Identifier": {
25
+ "type": "string",
26
+ "minLength": 1,
27
+ "maxLength": 128,
28
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$"
29
+ },
30
+ "Event": {
31
+ "type": "object",
32
+ "additionalProperties": false,
33
+ "required": ["id", "type", "sequence", "occurredAt", "payload"],
34
+ "properties": {
35
+ "id": { "$ref": "#/$defs/Identifier" },
36
+ "type": {
37
+ "enum": [
38
+ "runtime.heartbeat",
39
+ "radius.accounting",
40
+ "network.flow.aggregate",
41
+ "network.connection.summary",
42
+ "proxy.flow.aggregate"
43
+ ]
44
+ },
45
+ "sequence": { "type": "integer", "minimum": 0 },
46
+ "occurredAt": { "type": "string", "format": "date-time" },
47
+ "payload": {
48
+ "oneOf": [
49
+ { "$ref": "#/$defs/Heartbeat" },
50
+ { "$ref": "#/$defs/RadiusAccounting" },
51
+ { "$ref": "#/$defs/FlowAggregate" },
52
+ { "$ref": "#/$defs/ConnectionSummary" },
53
+ { "$ref": "#/$defs/ProxyFlowAggregate" }
54
+ ]
55
+ }
56
+ },
57
+ "allOf": [
58
+ {
59
+ "if": { "properties": { "type": { "const": "runtime.heartbeat" } }, "required": ["type"] },
60
+ "then": { "properties": { "payload": { "$ref": "#/$defs/Heartbeat" } } }
61
+ },
62
+ {
63
+ "if": { "properties": { "type": { "const": "radius.accounting" } }, "required": ["type"] },
64
+ "then": { "properties": { "payload": { "$ref": "#/$defs/RadiusAccounting" } } }
65
+ },
66
+ {
67
+ "if": { "properties": { "type": { "const": "network.flow.aggregate" } }, "required": ["type"] },
68
+ "then": { "properties": { "payload": { "$ref": "#/$defs/FlowAggregate" } } }
69
+ },
70
+ {
71
+ "if": { "properties": { "type": { "const": "network.connection.summary" } }, "required": ["type"] },
72
+ "then": { "properties": { "payload": { "$ref": "#/$defs/ConnectionSummary" } } }
73
+ },
74
+ {
75
+ "if": { "properties": { "type": { "const": "proxy.flow.aggregate" } }, "required": ["type"] },
76
+ "then": { "properties": { "payload": { "$ref": "#/$defs/ProxyFlowAggregate" } } }
77
+ }
78
+ ]
79
+ },
80
+ "Heartbeat": {
81
+ "type": "object",
82
+ "additionalProperties": false,
83
+ "required": ["status", "configurationVersion", "policyVersion", "uptimeSeconds"],
84
+ "properties": {
85
+ "status": { "enum": ["healthy", "degraded", "unhealthy"] },
86
+ "configurationVersion": { "type": "integer", "minimum": 0 },
87
+ "policyVersion": { "type": "integer", "minimum": 0 },
88
+ "uptimeSeconds": { "type": "integer", "minimum": 0 },
89
+ "diagnostics": {
90
+ "type": "array",
91
+ "maxItems": 32,
92
+ "uniqueItems": true,
93
+ "items": { "type": "string", "minLength": 1, "maxLength": 128 }
94
+ }
95
+ }
96
+ },
97
+ "RadiusAccounting": {
98
+ "type": "object",
99
+ "additionalProperties": false,
100
+ "required": ["statusType", "accountingSessionId", "nasId", "sessionTimeSeconds"],
101
+ "properties": {
102
+ "statusType": { "enum": ["start", "interim-update", "stop", "accounting-on", "accounting-off"] },
103
+ "accountingSessionId": { "$ref": "#/$defs/Identifier" },
104
+ "sessionId": { "$ref": "#/$defs/Identifier" },
105
+ "nasId": { "$ref": "#/$defs/Identifier" },
106
+ "nasPortId": { "type": "string", "minLength": 1, "maxLength": 128 },
107
+ "userId": { "$ref": "#/$defs/Identifier" },
108
+ "deviceId": { "$ref": "#/$defs/Identifier" },
109
+ "sessionTimeSeconds": { "type": "integer", "minimum": 0 },
110
+ "inputOctets": { "type": "integer", "minimum": 0 },
111
+ "outputOctets": { "type": "integer", "minimum": 0 },
112
+ "inputPackets": { "type": "integer", "minimum": 0 },
113
+ "outputPackets": { "type": "integer", "minimum": 0 },
114
+ "terminationCause": { "type": "string", "minLength": 1, "maxLength": 128 }
115
+ }
116
+ },
117
+ "FlowAggregate": {
118
+ "type": "object",
119
+ "additionalProperties": false,
120
+ "required": [
121
+ "windowStartedAt",
122
+ "windowEndedAt",
123
+ "sessionId",
124
+ "resourceId",
125
+ "direction",
126
+ "protocol",
127
+ "bytes",
128
+ "packets",
129
+ "connections",
130
+ "deniedConnections"
131
+ ],
132
+ "properties": {
133
+ "windowStartedAt": { "type": "string", "format": "date-time" },
134
+ "windowEndedAt": { "type": "string", "format": "date-time" },
135
+ "sessionId": { "$ref": "#/$defs/Identifier" },
136
+ "resourceId": { "$ref": "#/$defs/Identifier" },
137
+ "direction": { "enum": ["ingress", "egress"] },
138
+ "protocol": { "enum": ["tcp", "udp", "icmp", "other"] },
139
+ "bytes": { "type": "integer", "minimum": 0 },
140
+ "packets": { "type": "integer", "minimum": 0 },
141
+ "connections": { "type": "integer", "minimum": 0 },
142
+ "deniedConnections": { "type": "integer", "minimum": 0 }
143
+ }
144
+ },
145
+ "ConnectionSummary": {
146
+ "type": "object",
147
+ "additionalProperties": false,
148
+ "required": ["sessionId", "resourceId", "protocol", "destinationPort", "state", "reasonCode"],
149
+ "properties": {
150
+ "sessionId": { "$ref": "#/$defs/Identifier" },
151
+ "resourceId": { "$ref": "#/$defs/Identifier" },
152
+ "protocol": { "enum": ["tcp", "udp", "icmp", "other"] },
153
+ "destinationPort": { "type": "integer", "minimum": 0, "maximum": 65535 },
154
+ "state": { "enum": ["opened", "closed", "denied"] },
155
+ "reasonCode": { "type": "string", "minLength": 1, "maxLength": 128 }
156
+ }
157
+ },
158
+ "ProxyFlowAggregate": {
159
+ "type": "object",
160
+ "additionalProperties": false,
161
+ "required": [
162
+ "windowStartedAt",
163
+ "windowEndedAt",
164
+ "engine",
165
+ "profileId",
166
+ "profileRevision",
167
+ "mode",
168
+ "selectedProxy",
169
+ "uploadBytes",
170
+ "downloadBytes",
171
+ "activeConnections"
172
+ ],
173
+ "properties": {
174
+ "windowStartedAt": { "type": "string", "format": "date-time" },
175
+ "windowEndedAt": { "type": "string", "format": "date-time" },
176
+ "engine": { "const": "mihomo" },
177
+ "profileId": { "$ref": "#/$defs/Identifier" },
178
+ "profileRevision": { "type": "integer", "minimum": 1 },
179
+ "mode": { "enum": ["managed_follow", "app_subscription"] },
180
+ "selectedProxy": { "type": "string", "minLength": 1, "maxLength": 128 },
181
+ "uploadBytes": { "type": "integer", "minimum": 0, "maximum": 9223372036854775807 },
182
+ "downloadBytes": { "type": "integer", "minimum": 0, "maximum": 9223372036854775807 },
183
+ "activeConnections": { "type": "integer", "minimum": 0, "maximum": 1000000 }
184
+ }
185
+ }
186
+ }
187
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://contracts.opensoha.dev/network/network-radius-accounting.schema.json",
4
+ "title": "OpenSoha FreeRADIUS Accounting Adapter Request",
5
+ "description": "A strict credential-free request converted by soha-ingest into one canonical radius.accounting event.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "schemaVersion",
10
+ "eventTimestamp",
11
+ "statusType",
12
+ "accountingSessionId",
13
+ "nasId",
14
+ "sessionTimeSeconds"
15
+ ],
16
+ "properties": {
17
+ "schemaVersion": { "const": "network-radius-accounting/v1alpha1" },
18
+ "eventTimestamp": { "type": "integer", "minimum": 1, "maximum": 253402300799 },
19
+ "statusType": { "enum": ["start", "interim-update", "stop", "accounting-on", "accounting-off"] },
20
+ "accountingSessionId": { "$ref": "#/$defs/Identifier" },
21
+ "sessionId": { "$ref": "#/$defs/Identifier" },
22
+ "nasId": { "$ref": "#/$defs/Identifier" },
23
+ "nasPortId": { "type": "string", "minLength": 1, "maxLength": 128 },
24
+ "userId": { "$ref": "#/$defs/Identifier" },
25
+ "deviceId": { "$ref": "#/$defs/Identifier" },
26
+ "sessionTimeSeconds": { "type": "integer", "minimum": 0 },
27
+ "inputOctets": { "type": "integer", "minimum": 0 },
28
+ "outputOctets": { "type": "integer", "minimum": 0 },
29
+ "inputPackets": { "type": "integer", "minimum": 0 },
30
+ "outputPackets": { "type": "integer", "minimum": 0 },
31
+ "terminationCause": { "type": "string", "minLength": 1, "maxLength": 128 }
32
+ },
33
+ "$defs": {
34
+ "Identifier": {
35
+ "type": "string",
36
+ "minLength": 1,
37
+ "maxLength": 128,
38
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$"
39
+ }
40
+ }
41
+ }