@homeflare/alchemy 0.39.0 → 0.40.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 (207) hide show
  1. package/README.md +10 -8
  2. package/dist/ceph/auth-entity.d.ts +34 -0
  3. package/dist/ceph/auth-entity.d.ts.map +1 -0
  4. package/dist/ceph/ceph-argv-config.d.ts +10 -0
  5. package/dist/ceph/ceph-argv-config.d.ts.map +1 -0
  6. package/dist/ceph/ceph-argv.d.ts +19 -0
  7. package/dist/ceph/ceph-argv.d.ts.map +1 -0
  8. package/dist/ceph/ceph-auth-form.d.ts +54 -0
  9. package/dist/ceph/ceph-auth-form.d.ts.map +1 -0
  10. package/dist/ceph/ceph-auth-parse.d.ts +56 -0
  11. package/dist/ceph/ceph-auth-parse.d.ts.map +1 -0
  12. package/dist/ceph/ceph-auth-reconcile.d.ts +35 -0
  13. package/dist/ceph/ceph-auth-reconcile.d.ts.map +1 -0
  14. package/dist/ceph/ceph-config-options.d.ts +23 -0
  15. package/dist/ceph/ceph-config-options.d.ts.map +1 -0
  16. package/dist/ceph/ceph-key-leak-assert.d.ts +25 -0
  17. package/dist/ceph/ceph-key-leak-assert.d.ts.map +1 -0
  18. package/dist/ceph/ceph-transport.d.ts +37 -0
  19. package/dist/ceph/ceph-transport.d.ts.map +1 -0
  20. package/dist/ceph/fake-ceph-dial.d.ts +35 -0
  21. package/dist/ceph/fake-ceph-dial.d.ts.map +1 -0
  22. package/dist/ceph/index.d.ts +13 -0
  23. package/dist/ceph/index.d.ts.map +1 -0
  24. package/dist/ceph/index.js +1609 -0
  25. package/dist/ceph/index.js.map +38 -0
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/proxmox/index.js +15 -6
  29. package/dist/proxmox/index.js.map +3 -3
  30. package/dist/proxmox/qemu-volume.d.ts +12 -2
  31. package/dist/proxmox/qemu-volume.d.ts.map +1 -1
  32. package/dist/talos/credentials-write.d.ts +46 -0
  33. package/dist/talos/credentials-write.d.ts.map +1 -0
  34. package/dist/talos/credentials.d.ts +51 -9
  35. package/dist/talos/credentials.d.ts.map +1 -1
  36. package/dist/talos/fake-process.d.ts +56 -0
  37. package/dist/talos/fake-process.d.ts.map +1 -0
  38. package/dist/talos/index.d.ts +19 -4
  39. package/dist/talos/index.d.ts.map +1 -1
  40. package/dist/talos/index.js +357 -173
  41. package/dist/talos/index.js.map +16 -10
  42. package/dist/talos/kubeconfig-attrs.d.ts +4 -0
  43. package/dist/talos/kubeconfig-attrs.d.ts.map +1 -0
  44. package/dist/talos/kubeconfig.d.ts +41 -23
  45. package/dist/talos/kubeconfig.d.ts.map +1 -1
  46. package/dist/talos/machine-config-fixtures.d.ts +60 -0
  47. package/dist/talos/machine-config-fixtures.d.ts.map +1 -0
  48. package/dist/talos/machine-config-poll.d.ts +43 -0
  49. package/dist/talos/machine-config-poll.d.ts.map +1 -0
  50. package/dist/talos/machine-config-read.d.ts +49 -0
  51. package/dist/talos/machine-config-read.d.ts.map +1 -0
  52. package/dist/talos/resource.d.ts +18 -0
  53. package/dist/talos/resource.d.ts.map +1 -1
  54. package/dist/talos/talos-bootstrap-etcd.d.ts +38 -0
  55. package/dist/talos/talos-bootstrap-etcd.d.ts.map +1 -0
  56. package/dist/talos/talos-bootstrap.d.ts +87 -0
  57. package/dist/talos/talos-bootstrap.d.ts.map +1 -1
  58. package/dist/talos/talos-cluster-health.d.ts +43 -1
  59. package/dist/talos/talos-cluster-health.d.ts.map +1 -1
  60. package/dist/talos/talos-errors.d.ts +68 -0
  61. package/dist/talos/talos-errors.d.ts.map +1 -0
  62. package/dist/talos/talos-machine-config.d.ts +51 -15
  63. package/dist/talos/talos-machine-config.d.ts.map +1 -1
  64. package/dist/talos/talosctl.d.ts +7 -0
  65. package/dist/talos/talosctl.d.ts.map +1 -1
  66. package/dist/talos/values.d.ts +23 -3
  67. package/dist/talos/values.d.ts.map +1 -1
  68. package/dist/unifi/acl-rule-drift.d.ts +22 -0
  69. package/dist/unifi/acl-rule-drift.d.ts.map +1 -0
  70. package/dist/unifi/acl-rule-form.d.ts +90 -0
  71. package/dist/unifi/acl-rule-form.d.ts.map +1 -0
  72. package/dist/unifi/acl-rule-ordering-form.d.ts +49 -0
  73. package/dist/unifi/acl-rule-ordering-form.d.ts.map +1 -0
  74. package/dist/unifi/acl-rule-ordering.d.ts +54 -0
  75. package/dist/unifi/acl-rule-ordering.d.ts.map +1 -0
  76. package/dist/unifi/acl-rule.d.ts +49 -0
  77. package/dist/unifi/acl-rule.d.ts.map +1 -0
  78. package/dist/unifi/dns-policy-drift.d.ts +21 -0
  79. package/dist/unifi/dns-policy-drift.d.ts.map +1 -0
  80. package/dist/unifi/dns-policy-form.d.ts +97 -0
  81. package/dist/unifi/dns-policy-form.d.ts.map +1 -0
  82. package/dist/unifi/dns-policy.d.ts +55 -0
  83. package/dist/unifi/dns-policy.d.ts.map +1 -0
  84. package/dist/unifi/firewall-policy-drift.d.ts +26 -0
  85. package/dist/unifi/firewall-policy-drift.d.ts.map +1 -0
  86. package/dist/unifi/firewall-policy-form.d.ts +102 -0
  87. package/dist/unifi/firewall-policy-form.d.ts.map +1 -0
  88. package/dist/unifi/firewall-policy-ordering-form.d.ts +66 -0
  89. package/dist/unifi/firewall-policy-ordering-form.d.ts.map +1 -0
  90. package/dist/unifi/firewall-policy-ordering.d.ts +57 -0
  91. package/dist/unifi/firewall-policy-ordering.d.ts.map +1 -0
  92. package/dist/unifi/firewall-policy.d.ts +57 -0
  93. package/dist/unifi/firewall-policy.d.ts.map +1 -0
  94. package/dist/unifi/index.d.ts +7 -0
  95. package/dist/unifi/index.d.ts.map +1 -1
  96. package/dist/unifi/index.js +588 -3
  97. package/dist/unifi/index.js.map +24 -4
  98. package/dist/unifi/paginate.d.ts +0 -12
  99. package/dist/unifi/paginate.d.ts.map +1 -1
  100. package/dist/unifi/providers.d.ts +1 -1
  101. package/dist/unifi/providers.d.ts.map +1 -1
  102. package/dist/unifi/scan-source.d.ts +11 -0
  103. package/dist/unifi/scan-source.d.ts.map +1 -0
  104. package/dist/unifi/traffic-matching-list-drift.d.ts +22 -0
  105. package/dist/unifi/traffic-matching-list-drift.d.ts.map +1 -0
  106. package/dist/unifi/traffic-matching-list-form.d.ts +56 -0
  107. package/dist/unifi/traffic-matching-list-form.d.ts.map +1 -0
  108. package/dist/unifi/traffic-matching-list.d.ts +51 -0
  109. package/dist/unifi/traffic-matching-list.d.ts.map +1 -0
  110. package/dist/unifi/wifi-broadcast-drift.d.ts +22 -0
  111. package/dist/unifi/wifi-broadcast-drift.d.ts.map +1 -0
  112. package/dist/unifi/wifi-broadcast-form.d.ts +127 -0
  113. package/dist/unifi/wifi-broadcast-form.d.ts.map +1 -0
  114. package/dist/unifi/wifi-broadcast.d.ts +73 -0
  115. package/dist/unifi/wifi-broadcast.d.ts.map +1 -0
  116. package/docs/unifi-acl-rule.md +122 -0
  117. package/docs/unifi-dns-policy.md +75 -0
  118. package/docs/unifi-firewall-policy.md +109 -0
  119. package/docs/unifi-traffic-matching-list.md +82 -0
  120. package/docs/unifi-wifi.md +120 -0
  121. package/docs/unifi.md +10 -5
  122. package/package.json +6 -2
  123. package/src/ceph/auth-entity.ts +74 -0
  124. package/src/ceph/ceph-argv-config.ts +78 -0
  125. package/src/ceph/ceph-argv.test.ts +226 -0
  126. package/src/ceph/ceph-argv.ts +189 -0
  127. package/src/ceph/ceph-auth-form.test.ts +103 -0
  128. package/src/ceph/ceph-auth-form.ts +107 -0
  129. package/src/ceph/ceph-auth-parse.test.ts +155 -0
  130. package/src/ceph/ceph-auth-parse.ts +121 -0
  131. package/src/ceph/ceph-auth-reconcile-secrecy.test.ts +82 -0
  132. package/src/ceph/ceph-auth-reconcile.test.ts +233 -0
  133. package/src/ceph/ceph-auth-reconcile.ts +119 -0
  134. package/src/ceph/ceph-config-options.test.ts +44 -0
  135. package/src/ceph/ceph-config-options.ts +36 -0
  136. package/src/ceph/ceph-key-leak-assert.ts +35 -0
  137. package/src/ceph/ceph-no-output-logging.test.ts +52 -0
  138. package/src/ceph/ceph-transport.test.ts +125 -0
  139. package/src/ceph/ceph-transport.ts +117 -0
  140. package/src/ceph/fake-ceph-dial.ts +55 -0
  141. package/src/ceph/index.ts +12 -0
  142. package/src/index.ts +1 -0
  143. package/src/proxmox/qemu-volume.test.ts +153 -4
  144. package/src/proxmox/qemu-volume.ts +71 -14
  145. package/src/talos/credentials-write.test.ts +81 -0
  146. package/src/talos/credentials-write.ts +95 -0
  147. package/src/talos/credentials.test.ts +157 -0
  148. package/src/talos/credentials.ts +160 -90
  149. package/src/talos/fake-process.ts +94 -0
  150. package/src/talos/index.ts +39 -4
  151. package/src/talos/kubeconfig-attrs.ts +55 -0
  152. package/src/talos/kubeconfig.test.ts +256 -0
  153. package/src/talos/kubeconfig.ts +170 -90
  154. package/src/talos/machine-config-fixtures.ts +86 -0
  155. package/src/talos/machine-config-poll.test.ts +57 -0
  156. package/src/talos/machine-config-poll.ts +82 -0
  157. package/src/talos/machine-config-read.ts +118 -0
  158. package/src/talos/resource.ts +19 -0
  159. package/src/talos/secrets-gate.test.ts +63 -0
  160. package/src/talos/talos-bootstrap-etcd.ts +71 -0
  161. package/src/talos/talos-bootstrap.test.ts +218 -0
  162. package/src/talos/talos-bootstrap.ts +151 -45
  163. package/src/talos/talos-cluster-health.test.ts +143 -0
  164. package/src/talos/talos-cluster-health.ts +91 -28
  165. package/src/talos/talos-errors.ts +85 -0
  166. package/src/talos/talos-machine-config.test.ts +189 -0
  167. package/src/talos/talos-machine-config.ts +102 -88
  168. package/src/talos/talosctl.ts +8 -0
  169. package/src/talos/values.test.ts +34 -6
  170. package/src/talos/values.ts +41 -7
  171. package/src/unifi/acl-rule-drift.ts +57 -0
  172. package/src/unifi/acl-rule-form.ts +135 -0
  173. package/src/unifi/acl-rule-ordering-form.ts +79 -0
  174. package/src/unifi/acl-rule-ordering.test.ts +141 -0
  175. package/src/unifi/acl-rule-ordering.ts +70 -0
  176. package/src/unifi/acl-rule.test.ts +183 -0
  177. package/src/unifi/acl-rule.ts +61 -0
  178. package/src/unifi/discriminated-filter-decode.test.ts +134 -0
  179. package/src/unifi/dns-policy-drift.ts +48 -0
  180. package/src/unifi/dns-policy-form.ts +142 -0
  181. package/src/unifi/dns-policy.test.ts +154 -0
  182. package/src/unifi/dns-policy.ts +67 -0
  183. package/src/unifi/firewall-policy-drift.test.ts +151 -0
  184. package/src/unifi/firewall-policy-drift.ts +55 -0
  185. package/src/unifi/firewall-policy-filter-roundtrip.test.ts +143 -0
  186. package/src/unifi/firewall-policy-form.ts +146 -0
  187. package/src/unifi/firewall-policy-ordering-form.ts +111 -0
  188. package/src/unifi/firewall-policy-ordering.test.ts +185 -0
  189. package/src/unifi/firewall-policy-ordering.ts +87 -0
  190. package/src/unifi/firewall-policy.test.ts +183 -0
  191. package/src/unifi/firewall-policy.ts +73 -0
  192. package/src/unifi/index.ts +63 -0
  193. package/src/unifi/paginate.test.ts +24 -0
  194. package/src/unifi/paginate.ts +15 -3
  195. package/src/unifi/providers.ts +19 -1
  196. package/src/unifi/scan-source.ts +64 -0
  197. package/src/unifi/traffic-matching-list-drift.ts +40 -0
  198. package/src/unifi/traffic-matching-list-form.ts +77 -0
  199. package/src/unifi/traffic-matching-list.test.ts +202 -0
  200. package/src/unifi/traffic-matching-list.ts +78 -0
  201. package/src/unifi/wifi-broadcast-details-call.test.ts +92 -0
  202. package/src/unifi/wifi-broadcast-drift.ts +69 -0
  203. package/src/unifi/wifi-broadcast-form.ts +211 -0
  204. package/src/unifi/wifi-broadcast-secrets.test.ts +145 -0
  205. package/src/unifi/wifi-broadcast.test.ts +155 -0
  206. package/src/unifi/wifi-broadcast.ts +90 -0
  207. package/src/unifi/write-op-reference.test.ts +8 -54
@@ -0,0 +1,185 @@
1
+ /**
2
+ * `Unifi.FirewallPolicyOrdering`'s `spec` against a fake UniFi Network API — mirrors
3
+ * `acl-rule-ordering.test.ts`. The two things this file exists to prove that no other family test
4
+ * does: the zone-pair QUERY identity actually reaches the wire (unlike `acl-rule-ordering.ts`'s
5
+ * one-per-site path with no query at all), and T5's "order IS the value, never `sortedSet`" rule
6
+ * holds independently for BOTH `beforeSystemDefined` and `afterSystemDefined`.
7
+ */
8
+ import { describe, expect, test } from 'bun:test';
9
+ import type * as firewall from '@distilled.cloud/unifi-network/firewall';
10
+ import * as Retry from '@distilled.cloud/unifi-network/Retry';
11
+ import * as Effect from 'effect/Effect';
12
+ import { fakeFailure, fakeUnifi, fakeUnifiLayer } from './fake-unifi.ts';
13
+ import { attributesOf, driftOf, matches } from './firewall-policy-ordering-form.ts';
14
+ import {
15
+ type FirewallPolicyOrderingProps,
16
+ declareFirewallPolicyOrdering,
17
+ spec,
18
+ } from './firewall-policy-ordering.ts';
19
+ import { unifiOperations } from './resource.ts';
20
+
21
+ const ORDERING_PATH = '/proxy/network/integration/v1/sites/site-1/firewall/policies/ordering';
22
+
23
+ const liveOrdering = (
24
+ overrides: Partial<firewall.IntegrationFirewallPolicyOrderingDto> = {},
25
+ ): firewall.IntegrationFirewallPolicyOrderingDto => ({
26
+ orderedFirewallPolicyIds: {
27
+ beforeSystemDefined: ['fp-1', 'fp-2'],
28
+ afterSystemDefined: ['fp-9'],
29
+ },
30
+ ...overrides,
31
+ });
32
+
33
+ const PROPS: FirewallPolicyOrderingProps = {
34
+ siteId: 'site-1',
35
+ sourceFirewallZoneId: 'zone-iot',
36
+ destinationFirewallZoneId: 'zone-external',
37
+ beforeSystemDefined: ['fp-1', 'fp-2'],
38
+ afterSystemDefined: ['fp-9'],
39
+ };
40
+
41
+ describe('Unifi.FirewallPolicyOrdering spec.fetchLive', () => {
42
+ test('a real GET response decodes into the typed live object, with the zone pair as a query', async () => {
43
+ const fake = fakeUnifi((method, url) =>
44
+ method === 'GET' && url.pathname === ORDERING_PATH
45
+ ? Response.json(liveOrdering())
46
+ : fakeFailure(400, 'unexpected request'),
47
+ );
48
+ const live = await Effect.runPromise(
49
+ spec.fetchLive(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
50
+ );
51
+ expect(live?.orderedFirewallPolicyIds.beforeSystemDefined).toEqual(['fp-1', 'fp-2']);
52
+ expect(fake.seen).toHaveLength(1);
53
+ const sentUrl = new URL(`http://x${fake.seen[0]?.path}`);
54
+ expect(sentUrl.pathname).toBe(ORDERING_PATH);
55
+ expect(sentUrl.searchParams.get('sourceFirewallZoneId')).toBe('zone-iot');
56
+ expect(sentUrl.searchParams.get('destinationFirewallZoneId')).toBe('zone-external');
57
+ });
58
+
59
+ test('a genuine 404 folds to undefined', async () => {
60
+ const fake = fakeUnifi(() => fakeFailure(404, 'not found'));
61
+ const live = await Effect.runPromise(
62
+ spec.fetchLive(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
63
+ );
64
+ expect(live).toBeUndefined();
65
+ });
66
+
67
+ test('a 500 fails loudly -- only a genuine not-found may mean absent (T14)', async () => {
68
+ const fake = fakeUnifi(() => fakeFailure(500, 'boom'));
69
+ const failure = await Effect.runPromise(
70
+ Effect.flip(
71
+ spec.fetchLive(PROPS).pipe(Retry.none, Effect.provide(fakeUnifiLayer(fake.fetch))),
72
+ ),
73
+ );
74
+ expect(failure._tag).toBe('InternalServerError');
75
+ });
76
+ });
77
+
78
+ describe('declareFirewallPolicyOrdering -- the declaration renderer', () => {
79
+ test('its output matches attributesOf(live) by construction, for any live object', () => {
80
+ const live = liveOrdering({
81
+ orderedFirewallPolicyIds: { beforeSystemDefined: ['fp-5'], afterSystemDefined: [] },
82
+ });
83
+ const declared = declareFirewallPolicyOrdering(live, 'site-1', 'zone-iot', 'zone-external');
84
+ const attrs = attributesOf(live, declared);
85
+ expect(matches(attrs, declared)).toBe(true);
86
+ });
87
+ });
88
+
89
+ describe('T5 -- order IS the value, never sortedSet, independently on each half', () => {
90
+ test('an identical pair of lists is a noop', () => {
91
+ expect(driftOf(liveOrdering(), PROPS)).toEqual([]);
92
+ expect(matches(attributesOf(liveOrdering(), PROPS), PROPS)).toBe(true);
93
+ });
94
+
95
+ test('the SAME ids in a DIFFERENT order within beforeSystemDefined is real drift', () => {
96
+ const live = liveOrdering({
97
+ orderedFirewallPolicyIds: {
98
+ beforeSystemDefined: ['fp-2', 'fp-1'],
99
+ afterSystemDefined: ['fp-9'],
100
+ },
101
+ });
102
+ expect(matches(attributesOf(live, PROPS), PROPS)).toBe(false);
103
+ expect(driftOf(live, PROPS)).toEqual([
104
+ { field: 'beforeSystemDefined', live: ['fp-2', 'fp-1'], declared: ['fp-1', 'fp-2'] },
105
+ ]);
106
+ });
107
+
108
+ test('a reorder within afterSystemDefined alone is caught, beforeSystemDefined untouched', () => {
109
+ const live = liveOrdering({
110
+ orderedFirewallPolicyIds: {
111
+ beforeSystemDefined: ['fp-1', 'fp-2'],
112
+ afterSystemDefined: ['fp-9', 'fp-10'],
113
+ },
114
+ });
115
+ const props: FirewallPolicyOrderingProps = { ...PROPS, afterSystemDefined: ['fp-10', 'fp-9'] };
116
+ expect(driftOf(live, props)).toEqual([
117
+ { field: 'afterSystemDefined', live: ['fp-9', 'fp-10'], declared: ['fp-10', 'fp-9'] },
118
+ ]);
119
+ });
120
+
121
+ test('a policy moving from beforeSystemDefined to afterSystemDefined drifts on both fields', () => {
122
+ // The two halves can never swap through this endpoint by "reordering" alone -- membership moving
123
+ // across them is a genuine drift on each side independently, not one combined-list reorder.
124
+ const live = liveOrdering({
125
+ orderedFirewallPolicyIds: {
126
+ beforeSystemDefined: ['fp-1'],
127
+ afterSystemDefined: ['fp-2', 'fp-9'],
128
+ },
129
+ });
130
+ expect(driftOf(live, PROPS)).toEqual([
131
+ { field: 'beforeSystemDefined', live: ['fp-1'], declared: ['fp-1', 'fp-2'] },
132
+ { field: 'afterSystemDefined', live: ['fp-2', 'fp-9'], declared: ['fp-9'] },
133
+ ]);
134
+ });
135
+ });
136
+
137
+ describe('Unifi.FirewallPolicyOrdering write paths never reach the vendor API', () => {
138
+ const ops = unifiOperations(spec);
139
+
140
+ test('reconcile on an exact-match adoption (H6) makes no request but the one read', async () => {
141
+ const fake = fakeUnifi((method, url) =>
142
+ method === 'GET' && url.pathname === ORDERING_PATH
143
+ ? Response.json(liveOrdering())
144
+ : fakeFailure(400, 'unexpected request'),
145
+ );
146
+ const exit = await Effect.runPromiseExit(
147
+ ops.reconcile(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
148
+ );
149
+ expect(exit._tag).toBe('Success');
150
+ expect(fake.seen.every((s) => s.method === 'GET')).toBe(true);
151
+ });
152
+
153
+ test('reconcile on drift (a reorder) refuses instead of PUTting the whole pair', async () => {
154
+ const fake = fakeUnifi((method, url) =>
155
+ method === 'GET' && url.pathname === ORDERING_PATH
156
+ ? Response.json(
157
+ liveOrdering({
158
+ orderedFirewallPolicyIds: {
159
+ beforeSystemDefined: ['fp-2', 'fp-1'],
160
+ afterSystemDefined: [],
161
+ },
162
+ }),
163
+ )
164
+ : fakeFailure(400, 'unexpected request'),
165
+ );
166
+ const exit = await Effect.runPromiseExit(
167
+ ops.reconcile(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
168
+ );
169
+ expect(exit._tag).toBe('Failure');
170
+ expect(fake.seen.every((s) => s.method === 'GET')).toBe(true);
171
+ });
172
+
173
+ test('delete always refuses -- no DELETE is ever sent', async () => {
174
+ const fake = fakeUnifi((method, url) =>
175
+ method === 'GET' && url.pathname === ORDERING_PATH
176
+ ? Response.json(liveOrdering())
177
+ : fakeFailure(400, 'unexpected request'),
178
+ );
179
+ const exit = await Effect.runPromiseExit(
180
+ ops.destroy(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
181
+ );
182
+ expect(exit._tag).toBe('Failure');
183
+ expect(fake.seen).toEqual([]);
184
+ });
185
+ });
@@ -0,0 +1,87 @@
1
+ /**
2
+ * `Unifi.FirewallPolicyOrdering` — the ordered `before`/`afterSystemDefined` policy-id lists for
3
+ * ONE `(sourceFirewallZoneId, destinationFirewallZoneId)` zone pair (`getFirewallPolicyOrdering`,
4
+ * `GET /v1/sites/{siteId}/firewall/policies/ordering`), READ-ONLY (`policy.ts`). T5/I4's per-family
5
+ * variant: `docs/unifi.md`'s own header named this "a future `Unifi.FirewallPolicyOrdering`" as a
6
+ * separate, bigger PR (see `acl-rule-ordering.ts`'s header for the same forward reference, now
7
+ * resolved) — this is that resource.
8
+ *
9
+ * ⚠️ NOT ONE OBJECT PER SITE, UNLIKE `acl-rule-ordering.ts` — the identity is the TRIPLE
10
+ * `(siteId, sourceFirewallZoneId, destinationFirewallZoneId)`, both zone ids passed as QUERY
11
+ * parameters on the same path every time (`firewall.ts`'s `GetFirewallPolicyOrderingRequest`).
12
+ * There is no "list every zone pair with an ordering" endpoint — a caller (a future Lane C import)
13
+ * has to already know which pairs exist, typically by walking `firewall.getFirewallPolicies`
14
+ * (T9's own 424-live-policy scale — B0b's `pageAll`, see `firewall-policy.test.ts`'s own decode
15
+ * proof) and grouping by each policy's `source.zoneId`/`destination.zoneId`. Nothing here performs
16
+ * that discovery — this resource is one already-known pair's ordering, same as
17
+ * `firewall-policy.ts`/`acl-rule.ts` are one already-known object's own fields.
18
+ */
19
+ import { Resource } from 'alchemy';
20
+ import { adopt } from 'alchemy/AdoptPolicy';
21
+ import * as Provider from 'alchemy/Provider';
22
+ import * as firewall from '@distilled.cloud/unifi-network/firewall';
23
+ import * as Effect from 'effect/Effect';
24
+ import {
25
+ type FirewallPolicyOrderingAttributes,
26
+ type FirewallPolicyOrderingProps,
27
+ attributesOf,
28
+ matches,
29
+ } from './firewall-policy-ordering-form.ts';
30
+ import type { UnifiRequirements, UnifiSpec } from './resource.ts';
31
+ import { unifiHandlers } from './resource.ts';
32
+
33
+ export type {
34
+ FirewallPolicyOrderingAttributes,
35
+ FirewallPolicyOrderingProps,
36
+ } from './firewall-policy-ordering-form.ts';
37
+ export { declareFirewallPolicyOrdering, driftOf } from './firewall-policy-ordering-form.ts';
38
+
39
+ export interface UnifiFirewallPolicyOrdering extends Resource<
40
+ 'Unifi.FirewallPolicyOrdering',
41
+ FirewallPolicyOrderingProps,
42
+ FirewallPolicyOrderingAttributes,
43
+ never,
44
+ UnifiRequirements
45
+ > {}
46
+
47
+ export const UnifiFirewallPolicyOrdering = Resource<UnifiFirewallPolicyOrdering>(
48
+ 'Unifi.FirewallPolicyOrdering',
49
+ { defaultRemovalPolicy: 'retain' },
50
+ );
51
+
52
+ /** `firewallPolicyOrdering('iot-to-external', props)` — `adopt(true)` piped on by default (H5); see
53
+ * `network.ts`. */
54
+ export const firewallPolicyOrdering = (id: string, props: FirewallPolicyOrderingProps) =>
55
+ UnifiFirewallPolicyOrdering(id, props).pipe(adopt(true));
56
+
57
+ /** ★ EXPORTED for direct testing against a fake `Credentials` layer — see `network.ts`'s own. */
58
+ export const spec: UnifiSpec<
59
+ FirewallPolicyOrderingProps,
60
+ firewall.IntegrationFirewallPolicyOrderingDto,
61
+ FirewallPolicyOrderingAttributes,
62
+ firewall.GetFirewallPolicyOrderingError
63
+ > = {
64
+ type: 'Unifi.FirewallPolicyOrdering',
65
+ describe: (props) =>
66
+ `sites/${props.siteId}/firewall/policies/ordering` +
67
+ `?sourceFirewallZoneId=${props.sourceFirewallZoneId}` +
68
+ `&destinationFirewallZoneId=${props.destinationFirewallZoneId}`,
69
+ fetchLive: (props) =>
70
+ firewall
71
+ .getFirewallPolicyOrdering({
72
+ siteId: props.siteId,
73
+ sourceFirewallZoneId: props.sourceFirewallZoneId,
74
+ destinationFirewallZoneId: props.destinationFirewallZoneId,
75
+ })
76
+ .pipe(Effect.catchTag('NotFound', () => Effect.succeed(undefined))),
77
+ attributes: attributesOf,
78
+ matches,
79
+ };
80
+
81
+ export const handlers = unifiHandlers(spec);
82
+
83
+ export const UnifiFirewallPolicyOrderingProvider = () =>
84
+ Provider.effect(
85
+ UnifiFirewallPolicyOrdering,
86
+ Effect.succeed(UnifiFirewallPolicyOrdering.Provider.of(handlers)),
87
+ );
@@ -0,0 +1,183 @@
1
+ /**
2
+ * `Unifi.FirewallPolicy`'s `spec` against a fake UniFi Network API — mirrors `acl-rule.test.ts`/
3
+ * `dns-policy.test.ts`.
4
+ *
5
+ * ⚠️ `driftOf`/`matches` coverage lives in `firewall-policy-drift.test.ts` (the field-level table
6
+ * plus I2's per-field completeness check) and `firewall-policy-filter-roundtrip.test.ts` (I1's
7
+ * filter-bearing round trip) — both split out to keep this file under the house 250-line cap once
8
+ * that coverage grew, same split `network-drift.test.ts` is from `network.test.ts`, not a
9
+ * different family. Red team, 2026-09-26.
10
+ *
11
+ * The last `describe` block is this family's own decode-proof of B0b's `pageAll` (T9) — not used by
12
+ * `spec.fetchLive` itself (`getFirewallPolicy` reads one policy by id directly, same as
13
+ * `acl-rule.ts`), but a real console's FirewallPolicy list is the one T9 names by scale (424 live
14
+ * policies, `alchemy-ledger-network.md:13`) — `paginate.test.ts`'s own "against the real SDK" section
15
+ * proves the pager's decode path against `networks.getNetworksOverviewPage`; this proves it against
16
+ * `firewall.getFirewallPolicies`'s own page shape too, so a future Lane C import walking this
17
+ * family's full list is walking a shape this suite has already exercised end to end.
18
+ */
19
+ import { describe, expect, test } from 'bun:test';
20
+ import * as firewall from '@distilled.cloud/unifi-network/firewall';
21
+ import * as Retry from '@distilled.cloud/unifi-network/Retry';
22
+ import * as Effect from 'effect/Effect';
23
+ import { fakeFailure, fakeUnifi, fakeUnifiLayer } from './fake-unifi.ts';
24
+ import { attributesOf } from './firewall-policy-form.ts';
25
+ import { driftOf, matches } from './firewall-policy-drift.ts';
26
+ import { type FirewallPolicyProps, declareFirewallPolicy, spec } from './firewall-policy.ts';
27
+ import { pageAll } from './paginate.ts';
28
+ import { unifiOperations } from './resource.ts';
29
+
30
+ const POLICY_PATH = '/proxy/network/integration/v1/sites/site-1/firewall/policies/fp-1';
31
+
32
+ const liveFirewallPolicy = (
33
+ overrides: Partial<firewall.FirewallPolicy> = {},
34
+ ): firewall.FirewallPolicy => ({
35
+ action: { type: 'ALLOW' },
36
+ connectionStateFilter: ['ESTABLISHED', 'RELATED'],
37
+ description: 'Allow IoT devices outbound to the internet',
38
+ destination: { zoneId: 'zone-external' },
39
+ enabled: true,
40
+ id: 'fp-1',
41
+ index: 3,
42
+ ipProtocolScope: { ipVersion: 'IPV4' },
43
+ loggingEnabled: false,
44
+ metadata: { origin: 'USER' },
45
+ name: 'Allow IoT to Internet',
46
+ source: { zoneId: 'zone-iot' },
47
+ ...overrides,
48
+ });
49
+
50
+ const PROPS: FirewallPolicyProps = {
51
+ siteId: 'site-1',
52
+ firewallPolicyId: 'fp-1',
53
+ action: { type: 'ALLOW' },
54
+ connectionStateFilter: ['ESTABLISHED', 'RELATED'],
55
+ description: 'Allow IoT devices outbound to the internet',
56
+ destination: { zoneId: 'zone-external' },
57
+ enabled: true,
58
+ ipProtocolScope: { ipVersion: 'IPV4' },
59
+ loggingEnabled: false,
60
+ name: 'Allow IoT to Internet',
61
+ source: { zoneId: 'zone-iot' },
62
+ };
63
+
64
+ describe('Unifi.FirewallPolicy spec.fetchLive', () => {
65
+ test('a real GET response decodes into the typed live object', async () => {
66
+ const fake = fakeUnifi((method, url) =>
67
+ method === 'GET' && url.pathname === POLICY_PATH
68
+ ? Response.json(liveFirewallPolicy())
69
+ : fakeFailure(400, 'unexpected request'),
70
+ );
71
+ const live = await Effect.runPromise(
72
+ spec.fetchLive(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
73
+ );
74
+ expect(live?.id).toBe('fp-1');
75
+ expect(fake.seen).toEqual([{ method: 'GET', path: POLICY_PATH }]);
76
+ });
77
+
78
+ test('a genuine 404 folds to undefined -- never a false "unreadable" on anything else', async () => {
79
+ const fake = fakeUnifi(() => fakeFailure(404, 'not found'));
80
+ const live = await Effect.runPromise(
81
+ spec.fetchLive(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
82
+ );
83
+ expect(live).toBeUndefined();
84
+ });
85
+
86
+ test('a 500 fails loudly -- only a genuine not-found may mean absent (T14)', async () => {
87
+ // ⚠️ `Retry.none`: the default policy retries a 500 indefinitely with backoff -- this test
88
+ // asserts the FAILURE, not the retry schedule.
89
+ const fake = fakeUnifi(() => fakeFailure(500, 'boom'));
90
+ const failure = await Effect.runPromise(
91
+ Effect.flip(
92
+ spec.fetchLive(PROPS).pipe(Retry.none, Effect.provide(fakeUnifiLayer(fake.fetch))),
93
+ ),
94
+ );
95
+ expect(failure._tag).toBe('InternalServerError');
96
+ });
97
+ });
98
+
99
+ describe('declareFirewallPolicy -- the declaration renderer', () => {
100
+ test('its output matches attributesOf(live) by construction, for any live object', () => {
101
+ const live = liveFirewallPolicy({
102
+ connectionStateFilter: ['RELATED', 'ESTABLISHED'],
103
+ index: 9,
104
+ });
105
+ const declared = declareFirewallPolicy(live, 'site-1');
106
+ const attrs = attributesOf(live, declared);
107
+ expect(matches(attrs, declared)).toBe(true);
108
+ expect(driftOf(live, declared)).toEqual([]);
109
+ });
110
+ });
111
+
112
+ describe('Unifi.FirewallPolicy write paths never reach the vendor API', () => {
113
+ const ops = unifiOperations(spec);
114
+
115
+ test('reconcile on an exact-match adoption (H6) makes no request but the one read', async () => {
116
+ const fake = fakeUnifi((method, url) =>
117
+ method === 'GET' && url.pathname === POLICY_PATH
118
+ ? Response.json(liveFirewallPolicy())
119
+ : fakeFailure(400, 'unexpected request'),
120
+ );
121
+ const exit = await Effect.runPromiseExit(
122
+ ops.reconcile(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
123
+ );
124
+ expect(exit._tag).toBe('Success');
125
+ expect(fake.seen.every((s) => s.method === 'GET')).toBe(true);
126
+ });
127
+
128
+ test('reconcile on drift refuses instead of PATCHing/PUTting a merged body', async () => {
129
+ const fake = fakeUnifi((method, url) =>
130
+ method === 'GET' && url.pathname === POLICY_PATH
131
+ ? Response.json(liveFirewallPolicy({ enabled: false }))
132
+ : fakeFailure(400, 'unexpected request'),
133
+ );
134
+ const exit = await Effect.runPromiseExit(
135
+ ops.reconcile(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
136
+ );
137
+ expect(exit._tag).toBe('Failure');
138
+ expect(fake.seen.every((s) => s.method === 'GET')).toBe(true);
139
+ });
140
+
141
+ test('delete always refuses -- no DELETE is ever sent', async () => {
142
+ const fake = fakeUnifi((method, url) =>
143
+ method === 'GET' && url.pathname === POLICY_PATH
144
+ ? Response.json(liveFirewallPolicy())
145
+ : fakeFailure(400, 'unexpected request'),
146
+ );
147
+ const exit = await Effect.runPromiseExit(
148
+ ops.destroy(PROPS).pipe(Effect.provide(fakeUnifiLayer(fake.fetch))),
149
+ );
150
+ expect(exit._tag).toBe('Failure');
151
+ expect(fake.seen).toEqual([]);
152
+ });
153
+ });
154
+
155
+ describe("T9/B0b -- pageAll walks this family's own page shape (decode proof)", () => {
156
+ test('every row across several pages of getFirewallPolicies decodes, in order', async () => {
157
+ const rows = Array.from({ length: 7 }, (_, i) =>
158
+ liveFirewallPolicy({ id: `fp-${i}`, index: i }),
159
+ );
160
+ const fake = fakeUnifi((method, url) => {
161
+ if (
162
+ method !== 'GET' ||
163
+ url.pathname !== '/proxy/network/integration/v1/sites/site-1/firewall/policies'
164
+ ) {
165
+ return fakeFailure(400, 'unexpected request');
166
+ }
167
+ const offset = Number(url.searchParams.get('offset') ?? '0');
168
+ const limit = Number(url.searchParams.get('limit') ?? '3');
169
+ const data = rows.slice(offset, offset + limit);
170
+ return Response.json({ count: data.length, data, limit, offset, totalCount: rows.length });
171
+ });
172
+
173
+ const result = await Effect.runPromise(
174
+ pageAll(firewall.getFirewallPolicies, { siteId: 'site-1', limit: 3 }).pipe(
175
+ Effect.provide(fakeUnifiLayer(fake.fetch)),
176
+ ),
177
+ );
178
+
179
+ expect(result.map((p) => p.id)).toEqual(rows.map((r) => r.id));
180
+ expect(fake.seen.every((s) => s.method === 'GET')).toBe(true);
181
+ expect(fake.seen).toHaveLength(3);
182
+ });
183
+ });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * `Unifi.FirewallPolicy` — one firewall policy on a UniFi Network site, READ-ONLY (`policy.ts`).
3
+ * `Unifi.FirewallPolicyOrdering` (`firewall-policy-ordering.ts`) is the sibling resource for policy
4
+ * ORDER within a zone pair (T5) — one per `(sourceFirewallZoneId, destinationFirewallZoneId)`, never
5
+ * this file's concern. `docs/unifi.md`'s own header names this pair as "a bigger, separate PR";
6
+ * this is that PR — see `docs/unifi-firewall-policy.md` for the full breakdown.
7
+ *
8
+ * ★ T11 GATE, CHECKED 2026-09-26: the `Firewall` tag's 13 operations (5 already covered for
9
+ * `Unifi.FirewallZone` in `docs/unifi.md`, 8 new here for `FirewallPolicy` — `getFirewallPolicies`,
10
+ * `createFirewallPolicy`, `getFirewallPolicyOrdering`, `updateFirewallPolicyOrdering`,
11
+ * `deleteFirewallPolicy`, `getFirewallPolicy`, `patchFirewallPolicy`, `updateFirewallPolicy`) are
12
+ * byte-identical between 10.4.57 and 10.6.97, full raw operation objects; the 106-schema closure
13
+ * reachable from all 13 (`$ref` + `discriminator.mapping`) shares zero names with the 14 schemas
14
+ * that DID change elsewhere in the document. Corroborated independently by the A3 changeset's own
15
+ * broader 25-operation/139-schema re-check (`.changeset/unifi-network-a3-typed-discriminator-filters.md`).
16
+ * Full breakdown: `docs/unifi-firewall-policy.md`.
17
+ */
18
+ import { Resource } from 'alchemy';
19
+ import { adopt } from 'alchemy/AdoptPolicy';
20
+ import * as Provider from 'alchemy/Provider';
21
+ import * as firewall from '@distilled.cloud/unifi-network/firewall';
22
+ import * as Effect from 'effect/Effect';
23
+ import {
24
+ type FirewallPolicyAttributes,
25
+ type FirewallPolicyProps,
26
+ attributesOf,
27
+ } from './firewall-policy-form.ts';
28
+ import { matches } from './firewall-policy-drift.ts';
29
+ import type { UnifiRequirements, UnifiSpec } from './resource.ts';
30
+ import { unifiHandlers } from './resource.ts';
31
+
32
+ export type { FirewallPolicyAttributes, FirewallPolicyProps } from './firewall-policy-form.ts';
33
+ export { declareFirewallPolicy } from './firewall-policy-form.ts';
34
+ export { driftOf } from './firewall-policy-drift.ts';
35
+
36
+ export interface UnifiFirewallPolicy extends Resource<
37
+ 'Unifi.FirewallPolicy',
38
+ FirewallPolicyProps,
39
+ FirewallPolicyAttributes,
40
+ never,
41
+ UnifiRequirements
42
+ > {}
43
+
44
+ export const UnifiFirewallPolicy = Resource<UnifiFirewallPolicy>('Unifi.FirewallPolicy', {
45
+ defaultRemovalPolicy: 'retain',
46
+ });
47
+
48
+ /** `firewallPolicy('block-iot-wan', props)` — `adopt(true)` piped on by default (H5); see
49
+ * `network.ts`. */
50
+ export const firewallPolicy = (id: string, props: FirewallPolicyProps) =>
51
+ UnifiFirewallPolicy(id, props).pipe(adopt(true));
52
+
53
+ /** ★ EXPORTED for direct testing against a fake `Credentials` layer — see `network.ts`'s own. */
54
+ export const spec: UnifiSpec<
55
+ FirewallPolicyProps,
56
+ firewall.FirewallPolicy,
57
+ FirewallPolicyAttributes,
58
+ firewall.GetFirewallPolicyError
59
+ > = {
60
+ type: 'Unifi.FirewallPolicy',
61
+ describe: (props) => `sites/${props.siteId}/firewall/policies/${props.firewallPolicyId}`,
62
+ fetchLive: (props) =>
63
+ firewall
64
+ .getFirewallPolicy({ siteId: props.siteId, firewallPolicyId: props.firewallPolicyId })
65
+ .pipe(Effect.catchTag('NotFound', () => Effect.succeed(undefined))),
66
+ attributes: attributesOf,
67
+ matches,
68
+ };
69
+
70
+ export const handlers = unifiHandlers(spec);
71
+
72
+ export const UnifiFirewallPolicyProvider = () =>
73
+ Provider.effect(UnifiFirewallPolicy, Effect.succeed(UnifiFirewallPolicy.Provider.of(handlers)));
@@ -37,4 +37,67 @@ export {
37
37
  type FirewallZoneProps,
38
38
  driftOf as firewallZoneDriftOf,
39
39
  } from './firewall-zone.ts';
40
+ export {
41
+ UnifiDnsPolicy,
42
+ UnifiDnsPolicyProvider,
43
+ declareDnsPolicy,
44
+ dnsPolicy,
45
+ type DnsPolicyAttributes,
46
+ type DnsPolicyProps,
47
+ driftOf as dnsPolicyDriftOf,
48
+ } from './dns-policy.ts';
49
+ export {
50
+ UnifiAclRule,
51
+ UnifiAclRuleProvider,
52
+ aclRule,
53
+ declareAclRule,
54
+ type AclRuleAttributes,
55
+ type AclRuleProps,
56
+ driftOf as aclRuleDriftOf,
57
+ } from './acl-rule.ts';
58
+ export {
59
+ UnifiAclRuleOrdering,
60
+ UnifiAclRuleOrderingProvider,
61
+ aclRuleOrdering,
62
+ declareAclRuleOrdering,
63
+ type AclRuleOrderingAttributes,
64
+ type AclRuleOrderingProps,
65
+ driftOf as aclRuleOrderingDriftOf,
66
+ } from './acl-rule-ordering.ts';
67
+ export {
68
+ UnifiWifiBroadcast,
69
+ UnifiWifiBroadcastProvider,
70
+ declareWifiBroadcast,
71
+ wifiBroadcast,
72
+ type WifiBroadcastAttributes,
73
+ type WifiBroadcastProps,
74
+ driftOf as wifiBroadcastDriftOf,
75
+ } from './wifi-broadcast.ts';
76
+ export {
77
+ UnifiFirewallPolicy,
78
+ UnifiFirewallPolicyProvider,
79
+ declareFirewallPolicy,
80
+ firewallPolicy,
81
+ type FirewallPolicyAttributes,
82
+ type FirewallPolicyProps,
83
+ driftOf as firewallPolicyDriftOf,
84
+ } from './firewall-policy.ts';
85
+ export {
86
+ UnifiFirewallPolicyOrdering,
87
+ UnifiFirewallPolicyOrderingProvider,
88
+ declareFirewallPolicyOrdering,
89
+ firewallPolicyOrdering,
90
+ type FirewallPolicyOrderingAttributes,
91
+ type FirewallPolicyOrderingProps,
92
+ driftOf as firewallPolicyOrderingDriftOf,
93
+ } from './firewall-policy-ordering.ts';
94
+ export {
95
+ UnifiTrafficMatchingList,
96
+ UnifiTrafficMatchingListProvider,
97
+ declareTrafficMatchingList,
98
+ trafficMatchingList,
99
+ type TrafficMatchingListAttributes,
100
+ type TrafficMatchingListProps,
101
+ driftOf as trafficMatchingListDriftOf,
102
+ } from './traffic-matching-list.ts';
40
103
  export { providers } from './providers.ts';
@@ -5,6 +5,12 @@
5
5
  * list operation (`getNetworksOverviewPage`) through the fake UniFi server. `M2`: this file does
6
6
  * not repeat the 404/500 decode tests `network.test.ts` already covers — those are about ONE
7
7
  * page's failure, not the walk this file exists to prove.
8
+ *
9
+ * ⛔ MINOR-3 (red team, 2026-09-26): `page.offset`/`page.totalCount` are wire-controlled values
10
+ * `@distilled.cloud/core` never validates (`wifi-broadcast.ts`'s header documents why), so the
11
+ * "malformed response" test below plants a non-numeric value in each and proves `paginate.ts`'s
12
+ * `numOrPlaceholder` keeps it out of the error text, instead of assuming a vendor response is
13
+ * always well-typed just because the SDK's TS types say so.
8
14
  */
9
15
  import { describe, expect, test } from 'bun:test';
10
16
  import * as Effect from 'effect/Effect';
@@ -176,6 +182,24 @@ describe('pageAll -- IMPORTANT-3: typed failures on a self-contradicting respons
176
182
  expect(failure.reason).toBe('page-ceiling');
177
183
  });
178
184
 
185
+ test('a non-numeric offset/totalCount from a malformed response never appears in the error text (MINOR-3)', async () => {
186
+ const SENTINEL = 'sentinel-do-not-log-me';
187
+ // Cast, not a typo: the SDK's TS types say `offset`/`totalCount` are numbers, but nothing
188
+ // validates that at runtime -- this is exactly the shape a genuinely malformed page could take.
189
+ const fetchPage = () =>
190
+ Effect.succeed({
191
+ data: [],
192
+ offset: SENTINEL,
193
+ totalCount: SENTINEL,
194
+ } as unknown as OffsetPage<Row>);
195
+
196
+ const failure = await flip(pageAll(fetchPage, {}));
197
+
198
+ expect(failure.reason).toBe('offset-mismatch');
199
+ expect(failure.detail).not.toContain(SENTINEL);
200
+ expect(failure.message).not.toContain(SENTINEL);
201
+ });
202
+
179
203
  test('an honest, well-behaved server never trips any of the three checks', async () => {
180
204
  const rows = Array.from({ length: 9 }, (_, id) => ({ id }));
181
205
  const { fetchPage } = fakePages(rows, 4);
@@ -78,7 +78,19 @@ export class UnifiPaginationInconsistent extends Data.TaggedError('UnifiPaginati
78
78
  * a `totalCount` and final row count that agree by coincidence. What this closes is the SILENT
79
79
  * version of every case actually observed in review: an ignored `offset`, a `totalCount` that
80
80
  * drifts between calls, and an undercounted `totalCount` that truncates the walk early.
81
+ *
82
+ * ⛔ `numOrPlaceholder` (red team, MINOR-3, 2026-09-26): nothing in `@distilled.cloud/core` decodes
83
+ * or validates a page response's shape (this file's own `wifi-broadcast.ts` neighbor documents
84
+ * why), so `page.offset`/`page.totalCount` can be ANY JSON value, not just a number, if the wire
85
+ * ever sends something malformed. Every `detail` string below prints these through
86
+ * `numOrPlaceholder` rather than raw template interpolation, so a non-numeric value — however it
87
+ * got there — can never inject arbitrary response text into an error message a caller might log.
81
88
  */
89
+ /** A finite number renders as itself; anything else (a malformed wire value) renders as a fixed
90
+ * placeholder, never as the value itself — see this file's header (MINOR-3). */
91
+ const numOrPlaceholder = (value: unknown): string =>
92
+ typeof value === 'number' && Number.isFinite(value) ? String(value) : '<non-numeric>';
93
+
82
94
  export const pageAll = <Req extends OffsetPageRequest, T, E, R>(
83
95
  fetchPage: (request: Req) => Effect.Effect<OffsetPage<T>, E, R>,
84
96
  request: Req,
@@ -103,13 +115,13 @@ export const pageAll = <Req extends OffsetPageRequest, T, E, R>(
103
115
  if (page.offset !== offset) {
104
116
  return yield* new UnifiPaginationInconsistent({
105
117
  reason: 'offset-mismatch',
106
- detail: `requested offset ${offset}, page echoed ${page.offset} -- rows may be duplicated or skipped`,
118
+ detail: `requested offset ${offset}, page echoed ${numOrPlaceholder(page.offset)} -- rows may be duplicated or skipped`,
107
119
  });
108
120
  }
109
121
  if (totalCount !== undefined && page.totalCount !== totalCount) {
110
122
  return yield* new UnifiPaginationInconsistent({
111
123
  reason: 'total-count-changed',
112
- detail: `totalCount changed from ${totalCount} to ${page.totalCount} between page calls`,
124
+ detail: `totalCount changed from ${numOrPlaceholder(totalCount)} to ${numOrPlaceholder(page.totalCount)} between page calls`,
113
125
  });
114
126
  }
115
127
  totalCount = page.totalCount;
@@ -122,7 +134,7 @@ export const pageAll = <Req extends OffsetPageRequest, T, E, R>(
122
134
  if (rows.length !== expected) {
123
135
  return yield* new UnifiPaginationInconsistent({
124
136
  reason: 'row-count-mismatch',
125
- detail: `collected ${rows.length} rows but totalCount(${totalCount}) - startOffset(${startOffset}) = ${expected}`,
137
+ detail: `collected ${rows.length} rows but totalCount(${numOrPlaceholder(totalCount)}) - startOffset(${startOffset}) = ${numOrPlaceholder(expected)}`,
126
138
  });
127
139
  }
128
140
  return rows;