@homeflare/alchemy 0.57.0 → 0.58.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.
@@ -10,9 +10,9 @@
10
10
  * before comparing — never applied to only `live` or only `declared` (the specific gap MEDIUM-4's
11
11
  * own mutant found in `network-drift.ts`; see its header, and `acl-rule-drift.ts`'s own repeat of
12
12
  * this same rule for `protocolFilter`/`enforcingDeviceFilter`).
13
- * ★ `action`/`destination`/`ipProtocolScope`/`schedule`/`source` compare with the plain default
14
- * `equal` (whole-value `deepEqual`) — see `firewall-policy-form.ts`'s header for the accepted
15
- * known gap this leaves in their own nested set-like arrays.
13
+ * ★ `destination`/`ipProtocolScope`/`schedule`/`source` run through `normalizeSetArrays` on BOTH
14
+ * sides (set-like nested arrays compare as sets; `firewall-policy-form.ts`'s header), then the
15
+ * default `deepEqual`. `action` compares wholesale.
16
16
  */
17
17
  import type * as firewall from '@distilled.cloud/unifi-network/firewall';
18
18
  import { type FirewallPolicyAttributes, type FirewallPolicyProps } from './firewall-policy-form.ts';
@@ -1 +1 @@
1
- {"version":3,"file":"firewall-policy-drift.d.ts","sourceRoot":"","sources":["../../src/unifi/firewall-policy-drift.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,KAAK,QAAQ,MAAM,yCAAyC,CAAC;AAEzE,OAAO,EACL,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,EAGzB,MAAM,2BAA2B,CAAC;AAoBnC,eAAO,MAAM,OAAO,eACN,wBAAwB,SAC7B,mBAAmB,KACzB,OAAqD,CAAC;AAEzD;;;;GAIG;AACH,eAAO,MAAM,OAAO,SAAU,QAAQ,CAAC,cAAc,SAAS,mBAAmB,sCACnC,CAAC"}
1
+ {"version":3,"file":"firewall-policy-drift.d.ts","sourceRoot":"","sources":["../../src/unifi/firewall-policy-drift.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,KAAK,QAAQ,MAAM,yCAAyC,CAAC;AAEzE,OAAO,EACL,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,EAIzB,MAAM,2BAA2B,CAAC;AAoCnC,eAAO,MAAM,OAAO,eACN,wBAAwB,SAC7B,mBAAmB,KACzB,OAAqD,CAAC;AAEzD;;;;GAIG;AACH,eAAO,MAAM,OAAO,SAAU,QAAQ,CAAC,cAAc,SAAS,mBAAmB,sCACnC,CAAC"}
@@ -15,17 +15,18 @@
15
15
  * `deepEqual` (`stripNullish`) as ONE value, same proportion `acl-rule-form.ts`'s
16
16
  * `enforcingDeviceFilter` gets (one level of structure, not exploded into scalars).
17
17
  *
18
- * ⚠️ KNOWN GAP (T15, same shape as `acl-rule-form.ts`'s own — accepted, not fixed, here either):
19
- * several of the vendor's filter variants nested inside `source`/`destination`/`ipProtocolScope`
20
- * are themselves SET-LIKE arrays with no documented order —
21
- * `FirewallPolicyMACAddressFilter.macAddresses`, `FirewallPolicyNetworkFilter.networkIds`,
22
- * `FirewallPolicyApplicationFilter.applicationIds`, `FirewallPolicyIPAddressFilter.items`,
23
- * `FirewallPolicyRegionFilter.regions`, `FirewallPolicyVPNServerFilter.vpnServerIds`,
24
- * `FirewallSchedule.repeatOnDays` — `deepEqual` compares every one of them order-sensitively, so a
25
- * console-side reorder with no membership change would report a spurious drift this read-only
26
- * family's `reconcile` then refuses forever. Not fixed here for the same reason ACL's own gap
27
- * isn't: normalizing 12 filter variants' own nested arrays is a bigger, separate change, not a
28
- * proportional extension of typing per-discriminator (A3) into set-aware comparison too.
18
+ * ★ SET-LIKE NESTED ARRAYS COMPARE AS SETS (closed 2026-10-19; was T15's accepted gap). The
19
+ * vendor's filter variants inside `source`/`destination`/`ipProtocolScope`/`schedule` carry
20
+ * arrays with no documented order: `FirewallPolicyIPAddressFilter.items`, the port filter's
21
+ * `items`, `FirewallPolicyMACAddressFilter.macAddresses`, `FirewallPolicyNetworkFilter.networkIds`,
22
+ * `FirewallPolicyApplicationFilter.applicationIds`, `FirewallPolicyRegionFilter.regions`,
23
+ * `FirewallPolicyVPNServerFilter.vpnServerIds`, `FirewallPolicyDomainFilter.domains`,
24
+ * `applicationCategoryIds` and `FirewallSchedule.repeatOnDays`. Measured:
25
+ * the controller (Network 11.0.83) stored a declared port list [443, 9100] as [9100, 443], so a
26
+ * correct create failed its read-back (`UnifiCreateDidNotConverge`, homeflare-network PR 79).
27
+ * `normalizeSetArrays` sorts every array under one of those keys (by canonical JSON) on BOTH
28
+ * sides before `deepEqual`. Membership still has to match exactly, and duplicates are kept, so a
29
+ * real change is never hidden.
29
30
  *
30
31
  * ⛔ `connectionStateFilter` IS THE ONE TOP-LEVEL PROPS ARRAY, AND IS A SET — "Match on firewall
31
32
  * connection state. If null, matches all connection states" describes membership, not a sequence
@@ -48,6 +49,8 @@ import type * as firewall from '@distilled.cloud/unifi-network/firewall';
48
49
  * see `acl-rule-form.ts`'s own header for why a normalizer applied to only one side is a bug
49
50
  * class (MEDIUM-4). */
50
51
  export declare const normalizeConnectionStateFilter: (value: readonly string[] | undefined) => string[] | undefined;
52
+ /** A deep copy with every SET_ARRAY_KEYS array sorted by canonical JSON (duplicates kept). */
53
+ export declare const normalizeSetArrays: <T>(value: T) => T;
51
54
  /** ⚠️ EVERY OPTIONAL FIELD SPELLS OUT `| undefined` — `exactOptionalPropertyTypes`; see
52
55
  * `network-form.ts`'s own header.
53
56
  *
@@ -64,17 +67,17 @@ export interface FirewallPolicyProps {
64
67
  /** Connection states this policy matches — compared as a SET, see the header. */
65
68
  connectionStateFilter?: string[] | undefined;
66
69
  description?: string | undefined;
67
- /** Traffic destination — compound, compared wholesale; see the header's known gap. */
70
+ /** Traffic destination — compound, compared wholesale with set-like arrays normalized; see the header. */
68
71
  destination: firewall.FirewallPolicyDestination;
69
72
  enabled: boolean;
70
- /** IP version + protocol scope — compound, compared wholesale; see the header's known gap. */
73
+ /** IP version + protocol scope — compound, compared wholesale with set-like arrays normalized; see the header. */
71
74
  ipProtocolScope: firewall.FirewallPolicyIPProtocolScope;
72
75
  ipsecFilter?: string | undefined;
73
76
  loggingEnabled: boolean;
74
77
  name: string;
75
- /** Schedule this policy is active on — compound, compared wholesale; see the header's known gap. */
78
+ /** Schedule this policy is active on — compound, compared wholesale with set-like arrays normalized; see the header. */
76
79
  schedule?: firewall.FirewallSchedule | undefined;
77
- /** Traffic source — compound, compared wholesale; see the header's known gap. */
80
+ /** Traffic source — compound, compared wholesale with set-like arrays normalized; see the header. */
78
81
  source: firewall.FirewallPolicySource;
79
82
  }
80
83
  export interface FirewallPolicyAttributes {
@@ -1 +1 @@
1
- {"version":3,"file":"firewall-policy-form.d.ts","sourceRoot":"","sources":["../../src/unifi/firewall-policy-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,OAAO,KAAK,KAAK,QAAQ,MAAM,yCAAyC,CAAC;AAIzE;;wBAEwB;AACxB,eAAO,MAAM,8BAA8B,UAClC,SAAS,MAAM,EAAE,GAAG,SAAS,KACnC,MAAM,EAAE,GAAG,SAA2D,CAAC;AAE1E;;;;;;;;6FAQ6F;AAC7F,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,MAAM,EAAE,QAAQ,CAAC,oBAAoB,CAAC;IACtC,iFAAiF;IACjF,qBAAqB,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC7C,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,sFAAsF;IACtF,WAAW,EAAE,QAAQ,CAAC,yBAAyB,CAAC;IAChD,OAAO,EAAE,OAAO,CAAC;IACjB,8FAA8F;IAC9F,eAAe,EAAE,QAAQ,CAAC,6BAA6B,CAAC;IACxD,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,cAAc,EAAE,OAAO,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,oGAAoG;IACpG,QAAQ,CAAC,EAAE,QAAQ,CAAC,gBAAgB,GAAG,SAAS,CAAC;IACjD,iFAAiF;IACjF,MAAM,EAAE,QAAQ,CAAC,oBAAoB,CAAC;CACvC;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,QAAQ,CAAC,oBAAoB,CAAC;IACtC,qBAAqB,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC5C,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,WAAW,EAAE,QAAQ,CAAC,yBAAyB,CAAC;IAChD,OAAO,EAAE,OAAO,CAAC;IACjB,oGAAoG;IACpG,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,QAAQ,CAAC,6BAA6B,CAAC;IACxD,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,cAAc,EAAE,OAAO,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,QAAQ,CAAC,gBAAgB,GAAG,SAAS,CAAC;IAChD,MAAM,EAAE,QAAQ,CAAC,oBAAoB,CAAC;IACtC,6FAA6F;IAC7F,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,eAAO,MAAM,YAAY,SACjB,QAAQ,CAAC,cAAc,SACtB,mBAAmB,KACzB,wBAgBD,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,SAC1B,QAAQ,CAAC,cAAc,UACrB,MAAM,KACb,mBAcD,CAAC"}
1
+ {"version":3,"file":"firewall-policy-form.d.ts","sourceRoot":"","sources":["../../src/unifi/firewall-policy-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,OAAO,KAAK,KAAK,QAAQ,MAAM,yCAAyC,CAAC;AAIzE;;wBAEwB;AACxB,eAAO,MAAM,8BAA8B,UAClC,SAAS,MAAM,EAAE,GAAG,SAAS,KACnC,MAAM,EAAE,GAAG,SAA2D,CAAC;AA6B1E,8FAA8F;AAC9F,eAAO,MAAM,kBAAkB,GAAI,CAAC,SAAS,CAAC,KAAG,CAehD,CAAC;AAEF;;;;;;;;6FAQ6F;AAC7F,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,MAAM,EAAE,QAAQ,CAAC,oBAAoB,CAAC;IACtC,iFAAiF;IACjF,qBAAqB,CAAC,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC7C,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,0GAA0G;IAC1G,WAAW,EAAE,QAAQ,CAAC,yBAAyB,CAAC;IAChD,OAAO,EAAE,OAAO,CAAC;IACjB,kHAAkH;IAClH,eAAe,EAAE,QAAQ,CAAC,6BAA6B,CAAC;IACxD,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,cAAc,EAAE,OAAO,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,wHAAwH;IACxH,QAAQ,CAAC,EAAE,QAAQ,CAAC,gBAAgB,GAAG,SAAS,CAAC;IACjD,qGAAqG;IACrG,MAAM,EAAE,QAAQ,CAAC,oBAAoB,CAAC;CACvC;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,QAAQ,CAAC,oBAAoB,CAAC;IACtC,qBAAqB,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;IAC5C,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,WAAW,EAAE,QAAQ,CAAC,yBAAyB,CAAC;IAChD,OAAO,EAAE,OAAO,CAAC;IACjB,oGAAoG;IACpG,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,QAAQ,CAAC,6BAA6B,CAAC;IACxD,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,cAAc,EAAE,OAAO,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,QAAQ,CAAC,gBAAgB,GAAG,SAAS,CAAC;IAChD,MAAM,EAAE,QAAQ,CAAC,oBAAoB,CAAC;IACtC,6FAA6F;IAC7F,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,eAAO,MAAM,YAAY,SACjB,QAAQ,CAAC,cAAc,SACtB,mBAAmB,KACzB,wBAgBD,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,SAC1B,QAAQ,CAAC,cAAc,UACrB,MAAM,KACb,mBAcD,CAAC"}
@@ -318,6 +318,15 @@ var computePatch = (news, olds, patchKeys) => {
318
318
  }
319
319
  return { set, unset };
320
320
  };
321
+ var acceptInto = (olds, news, accepted) => {
322
+ const next = { ...olds };
323
+ for (const field of accepted) {
324
+ const key = String(field);
325
+ if (key in news || key in next)
326
+ next[key] = news[key];
327
+ }
328
+ return next;
329
+ };
321
330
  var updateReconcile = (spec, { news, olds, output, note }) => Effect5.gen(function* () {
322
331
  const identity = spec.describe(news);
323
332
  const live = yield* spec.fetchLive(news);
@@ -334,15 +343,22 @@ var updateReconcile = (spec, { news, olds, output, note }) => Effect5.gen(functi
334
343
  if (previous !== identity) {
335
344
  return yield* new UnifiIdentityChanged({ type: spec.type, identity, previous });
336
345
  }
346
+ const disagrees = new Set(update.driftOf(live, news).map((d) => String(d.field)));
337
347
  const drifted = update.driftOf(live, olds).map((d) => d.field);
338
- if (drifted.length > 0) {
348
+ const conflicts = drifted.filter((field) => disagrees.has(String(field)));
349
+ if (conflicts.length > 0) {
339
350
  return yield* new UnifiLiveDriftedSinceDeploy({
340
351
  type: spec.type,
341
352
  identity,
342
- fields: drifted
353
+ fields: conflicts
343
354
  });
344
355
  }
345
- const patch = computePatch(news, olds, update.patchKeys);
356
+ const accepted = drifted.filter((field) => !disagrees.has(String(field)));
357
+ const baseline = acceptInto(olds, news, accepted);
358
+ if (accepted.length > 0) {
359
+ yield* note(`${spec.type} ${identity}: live edits since the last deploy already match the declaration, accepted: [${accepted.map(String).join(", ")}]`);
360
+ }
361
+ const patch = computePatch(news, baseline, update.patchKeys);
346
362
  if (Object.keys(patch.set).length === 0 && patch.unset.length === 0) {
347
363
  return yield* refuseWrite(spec.type, identity, "update");
348
364
  }
@@ -936,6 +952,30 @@ var writeFirewallPolicyCreate = (props) => firewall2.createFirewallPolicy(firewa
936
952
  // src/unifi/firewall-policy-form.ts
937
953
  var sortedSet4 = (values) => [...new Set(values)].sort();
938
954
  var normalizeConnectionStateFilter = (value) => value == null ? undefined : sortedSet4(value);
955
+ var SET_ARRAY_KEYS = new Set([
956
+ "items",
957
+ "macAddresses",
958
+ "networkIds",
959
+ "applicationIds",
960
+ "regions",
961
+ "vpnServerIds",
962
+ "repeatOnDays",
963
+ "domains",
964
+ "applicationCategoryIds"
965
+ ]);
966
+ var sortKeys = (value) => Array.isArray(value) ? value.map(sortKeys) : value !== null && typeof value === "object" ? Object.fromEntries(Object.keys(value).sort().map((k) => [k, sortKeys(value[k])])) : value;
967
+ var canonical = (value) => JSON.stringify(sortKeys(normalizeSetArrays(value)));
968
+ var normalizeSetArrays = (value) => {
969
+ if (Array.isArray(value))
970
+ return value.map((v) => normalizeSetArrays(v));
971
+ if (value === null || typeof value !== "object")
972
+ return value;
973
+ const out = {};
974
+ for (const [key, inner] of Object.entries(value)) {
975
+ out[key] = SET_ARRAY_KEYS.has(key) && Array.isArray(inner) ? [...inner].map((v) => normalizeSetArrays(v)).sort((x, y) => canonical(x) < canonical(y) ? -1 : canonical(x) > canonical(y) ? 1 : 0) : normalizeSetArrays(inner);
976
+ }
977
+ return out;
978
+ };
939
979
  var attributesOf7 = (live, props) => ({
940
980
  siteId: props.siteId,
941
981
  firewallPolicyId: live.id,
@@ -978,14 +1018,30 @@ var fieldDrift7 = makeDriftOf([
978
1018
  declared: (p) => normalizeConnectionStateFilter(p.connectionStateFilter)
979
1019
  },
980
1020
  { field: "description", live: (a) => a.description, declared: (p) => p.description },
981
- { field: "destination", live: (a) => a.destination, declared: (p) => p.destination },
1021
+ {
1022
+ field: "destination",
1023
+ live: (a) => normalizeSetArrays(a.destination),
1024
+ declared: (p) => normalizeSetArrays(p.destination)
1025
+ },
982
1026
  { field: "enabled", live: (a) => a.enabled, declared: (p) => p.enabled },
983
- { field: "ipProtocolScope", live: (a) => a.ipProtocolScope, declared: (p) => p.ipProtocolScope },
1027
+ {
1028
+ field: "ipProtocolScope",
1029
+ live: (a) => normalizeSetArrays(a.ipProtocolScope),
1030
+ declared: (p) => normalizeSetArrays(p.ipProtocolScope)
1031
+ },
984
1032
  { field: "ipsecFilter", live: (a) => a.ipsecFilter, declared: (p) => p.ipsecFilter },
985
1033
  { field: "loggingEnabled", live: (a) => a.loggingEnabled, declared: (p) => p.loggingEnabled },
986
1034
  { field: "name", live: (a) => a.name, declared: (p) => p.name },
987
- { field: "schedule", live: (a) => a.schedule, declared: (p) => p.schedule },
988
- { field: "source", live: (a) => a.source, declared: (p) => p.source }
1035
+ {
1036
+ field: "schedule",
1037
+ live: (a) => normalizeSetArrays(a.schedule),
1038
+ declared: (p) => normalizeSetArrays(p.schedule)
1039
+ },
1040
+ {
1041
+ field: "source",
1042
+ live: (a) => normalizeSetArrays(a.source),
1043
+ declared: (p) => normalizeSetArrays(p.source)
1044
+ }
989
1045
  ]);
990
1046
  var matches7 = (attributes, props) => fieldDrift7(attributes, props).length === 0;
991
1047
  var driftOf7 = (live, props) => fieldDrift7(attributesOf7(live, props), props);
@@ -1195,5 +1251,5 @@ export {
1195
1251
  driftOf6 as wifiBroadcastDriftOf
1196
1252
  };
1197
1253
 
1198
- //# debugId=F58E0BCA65985B3464756E2164756E21
1254
+ //# debugId=D14D4404F58A5F1264756E2164756E21
1199
1255
  //# sourceMappingURL=index.js.map