@homeflare/alchemy 0.49.0 → 0.51.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 (104) hide show
  1. package/dist/kubernetes/helm-chart-bounded.d.ts +44 -0
  2. package/dist/kubernetes/helm-chart-bounded.d.ts.map +1 -0
  3. package/dist/kubernetes/index.d.ts +12 -0
  4. package/dist/kubernetes/index.d.ts.map +1 -0
  5. package/dist/kubernetes/index.js +585 -0
  6. package/dist/kubernetes/index.js.map +19 -0
  7. package/dist/kubernetes/ready-checks.d.ts +45 -0
  8. package/dist/kubernetes/ready-checks.d.ts.map +1 -0
  9. package/dist/kubernetes/ready-classify.d.ts +23 -0
  10. package/dist/kubernetes/ready-classify.d.ts.map +1 -0
  11. package/dist/kubernetes/ready-connect.d.ts +29 -0
  12. package/dist/kubernetes/ready-connect.d.ts.map +1 -0
  13. package/dist/kubernetes/ready-declare.d.ts +5 -0
  14. package/dist/kubernetes/ready-declare.d.ts.map +1 -0
  15. package/dist/kubernetes/ready-errors.d.ts +80 -0
  16. package/dist/kubernetes/ready-errors.d.ts.map +1 -0
  17. package/dist/kubernetes/ready-poll.d.ts +61 -0
  18. package/dist/kubernetes/ready-poll.d.ts.map +1 -0
  19. package/dist/kubernetes/ready.d.ts +74 -0
  20. package/dist/kubernetes/ready.d.ts.map +1 -0
  21. package/dist/talos/fake-apiserver.d.ts +11 -2
  22. package/dist/talos/fake-apiserver.d.ts.map +1 -1
  23. package/dist/unifi/acl-rule-ordering.d.ts +6 -1
  24. package/dist/unifi/acl-rule-ordering.d.ts.map +1 -1
  25. package/dist/unifi/acl-rule.d.ts +6 -1
  26. package/dist/unifi/acl-rule.d.ts.map +1 -1
  27. package/dist/unifi/dns-policy.d.ts +6 -1
  28. package/dist/unifi/dns-policy.d.ts.map +1 -1
  29. package/dist/unifi/fake-unifi.d.ts +4 -0
  30. package/dist/unifi/fake-unifi.d.ts.map +1 -1
  31. package/dist/unifi/firewall-policy-ordering.d.ts +6 -1
  32. package/dist/unifi/firewall-policy-ordering.d.ts.map +1 -1
  33. package/dist/unifi/firewall-policy.d.ts +6 -1
  34. package/dist/unifi/firewall-policy.d.ts.map +1 -1
  35. package/dist/unifi/firewall-zone.d.ts +6 -1
  36. package/dist/unifi/firewall-zone.d.ts.map +1 -1
  37. package/dist/unifi/index.d.ts +5 -3
  38. package/dist/unifi/index.d.ts.map +1 -1
  39. package/dist/unifi/index.js +339 -86
  40. package/dist/unifi/index.js.map +10 -6
  41. package/dist/unifi/network-scope.d.ts +31 -0
  42. package/dist/unifi/network-scope.d.ts.map +1 -0
  43. package/dist/unifi/network-update.d.ts +44 -0
  44. package/dist/unifi/network-update.d.ts.map +1 -0
  45. package/dist/unifi/network.d.ts +32 -12
  46. package/dist/unifi/network.d.ts.map +1 -1
  47. package/dist/unifi/policy.d.ts +89 -1
  48. package/dist/unifi/policy.d.ts.map +1 -1
  49. package/dist/unifi/resource.d.ts +17 -43
  50. package/dist/unifi/resource.d.ts.map +1 -1
  51. package/dist/unifi/traffic-matching-list.d.ts +6 -1
  52. package/dist/unifi/traffic-matching-list.d.ts.map +1 -1
  53. package/dist/unifi/update-reconcile.d.ts +31 -0
  54. package/dist/unifi/update-reconcile.d.ts.map +1 -0
  55. package/dist/unifi/wifi-broadcast.d.ts +6 -1
  56. package/dist/unifi/wifi-broadcast.d.ts.map +1 -1
  57. package/dist/unifi/wire-guard.d.ts +33 -0
  58. package/dist/unifi/wire-guard.d.ts.map +1 -0
  59. package/docs/kubernetes-ready.md +146 -0
  60. package/docs/peers.md +13 -0
  61. package/docs/provider-standard.md +7 -0
  62. package/docs/unifi-network-write.md +90 -0
  63. package/docs/unifi.md +36 -36
  64. package/package.json +7 -1
  65. package/src/kubernetes/helm-chart-bounded.test.ts +242 -0
  66. package/src/kubernetes/helm-chart-bounded.ts +110 -0
  67. package/src/kubernetes/index.ts +28 -0
  68. package/src/kubernetes/ready-backoff.test.ts +124 -0
  69. package/src/kubernetes/ready-checks.test.ts +106 -0
  70. package/src/kubernetes/ready-checks.ts +165 -0
  71. package/src/kubernetes/ready-classify.ts +76 -0
  72. package/src/kubernetes/ready-connect.ts +52 -0
  73. package/src/kubernetes/ready-declare.test.ts +85 -0
  74. package/src/kubernetes/ready-declare.ts +50 -0
  75. package/src/kubernetes/ready-errors.ts +102 -0
  76. package/src/kubernetes/ready-first-install.test.ts +126 -0
  77. package/src/kubernetes/ready-poll-states.test.ts +92 -0
  78. package/src/kubernetes/ready-poll.test.ts +194 -0
  79. package/src/kubernetes/ready-poll.ts +215 -0
  80. package/src/kubernetes/ready-rollout.test.ts +58 -0
  81. package/src/kubernetes/ready-secrecy.test.ts +131 -0
  82. package/src/kubernetes/ready.fixtures.ts +103 -0
  83. package/src/kubernetes/ready.test.ts +205 -0
  84. package/src/kubernetes/ready.ts +188 -0
  85. package/src/talos/fake-apiserver.ts +21 -3
  86. package/src/unifi/fake-unifi.ts +15 -2
  87. package/src/unifi/handler-allow-news.test.ts +80 -0
  88. package/src/unifi/index.ts +21 -5
  89. package/src/unifi/network-scope.test.ts +203 -0
  90. package/src/unifi/network-scope.ts +75 -0
  91. package/src/unifi/network-update.test.ts +250 -0
  92. package/src/unifi/network-update.ts +95 -0
  93. package/src/unifi/network.test.ts +27 -1
  94. package/src/unifi/network.ts +56 -12
  95. package/src/unifi/policy.ts +128 -10
  96. package/src/unifi/resource.test.ts +15 -3
  97. package/src/unifi/resource.ts +64 -105
  98. package/src/unifi/update-reconcile.ts +126 -0
  99. package/src/unifi/wire-guard-anchor.test.ts +198 -0
  100. package/src/unifi/wire-guard-round2.test.ts +120 -0
  101. package/src/unifi/wire-guard.test.ts +218 -0
  102. package/src/unifi/wire-guard.ts +192 -0
  103. package/src/unifi/write-op-reference.test.ts +38 -2
  104. package/src/unifi/get-only-guard.test.ts +0 -165
@@ -1,5 +1,7 @@
1
1
  import * as Effect from 'effect/Effect';
2
- export declare const UNIFI_READ_ONLY_POLICY = "read-only by Tim's rule, 2026-09-24; lifting it is a kit change";
2
+ export declare const UNIFI_WRITE_POLICY: string;
3
+ /** @deprecated renamed `UNIFI_WRITE_POLICY`; kept so existing consumer imports do not break. */
4
+ export declare const UNIFI_READ_ONLY_POLICY: string;
3
5
  /** What kind of write was asked for and refused — for the message only, never acted on. */
4
6
  export type UnifiWriteAction = 'create' | 'update' | 'delete';
5
7
  declare const UnifiWriteRefused_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
@@ -16,5 +18,91 @@ export declare class UnifiWriteRefused extends UnifiWriteRefused_base<{
16
18
  }
17
19
  /** The only thing `reconcile`/`delete` ever return for a write this family cannot make. */
18
20
  export declare const refuseWrite: (type: string, identity: string, action: UnifiWriteAction) => Effect.Effect<never, UnifiWriteRefused, never>;
21
+ declare const UnifiLiveDriftedSinceDeploy_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
22
+ readonly _tag: "UnifiLiveDriftedSinceDeploy";
23
+ } & Readonly<A>;
24
+ /** Live no longer equals what the last deploy wrote: a hand edit is never overwritten. */
25
+ export declare class UnifiLiveDriftedSinceDeploy extends UnifiLiveDriftedSinceDeploy_base<{
26
+ readonly type: string;
27
+ readonly identity: string;
28
+ readonly fields: ReadonlyArray<string>;
29
+ }> {
30
+ get message(): string;
31
+ }
32
+ declare const UnifiUpdateWouldBeNoop_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
33
+ readonly _tag: "UnifiUpdateWouldBeNoop";
34
+ } & Readonly<A>;
35
+ /** The merged body equals live: a PUT would change nothing (keeps a re-PUT loop impossible). */
36
+ export declare class UnifiUpdateWouldBeNoop extends UnifiUpdateWouldBeNoop_base<{
37
+ readonly type: string;
38
+ readonly identity: string;
39
+ }> {
40
+ get message(): string;
41
+ }
42
+ declare const UnifiUpdateDidNotConverge_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
43
+ readonly _tag: "UnifiUpdateDidNotConverge";
44
+ } & Readonly<A>;
45
+ /** The PUT answered, yet the response still differs from the declaration. */
46
+ export declare class UnifiUpdateDidNotConverge extends UnifiUpdateDidNotConverge_base<{
47
+ readonly type: string;
48
+ readonly identity: string;
49
+ readonly fields: ReadonlyArray<string>;
50
+ }> {
51
+ get message(): string;
52
+ }
53
+ declare const UnifiFieldNotRemovable_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
54
+ readonly _tag: "UnifiFieldNotRemovable";
55
+ } & Readonly<A>;
56
+ /** A patch would remove a key outside the removable allowlist (an omitted block is turned off). */
57
+ export declare class UnifiFieldNotRemovable extends UnifiFieldNotRemovable_base<{
58
+ readonly type: string;
59
+ readonly identity: string;
60
+ readonly fields: ReadonlyArray<string>;
61
+ }> {
62
+ get message(): string;
63
+ }
64
+ declare const UnifiFieldNotSettable_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
65
+ readonly _tag: "UnifiFieldNotSettable";
66
+ } & Readonly<A>;
67
+ /** A patch would SET a key outside the settable allowlist (only `ipv6Configuration` may be written). */
68
+ export declare class UnifiFieldNotSettable extends UnifiFieldNotSettable_base<{
69
+ readonly type: string;
70
+ readonly identity: string;
71
+ readonly fields: ReadonlyArray<string>;
72
+ }> {
73
+ get message(): string;
74
+ }
75
+ declare const UnifiManagementNetworkRefused_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
76
+ readonly _tag: "UnifiManagementNetworkRefused";
77
+ } & Readonly<A>;
78
+ /** The live object is the site's default (management) network: never written. */
79
+ export declare class UnifiManagementNetworkRefused extends UnifiManagementNetworkRefused_base<{
80
+ readonly type: string;
81
+ readonly identity: string;
82
+ }> {
83
+ get message(): string;
84
+ }
85
+ declare const UnifiImmutableFieldChanged_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
86
+ readonly _tag: "UnifiImmutableFieldChanged";
87
+ } & Readonly<A>;
88
+ /** A patch changes a field that decides where the network lives or what serves it. */
89
+ export declare class UnifiImmutableFieldChanged extends UnifiImmutableFieldChanged_base<{
90
+ readonly type: string;
91
+ readonly identity: string;
92
+ readonly fields: ReadonlyArray<string>;
93
+ }> {
94
+ get message(): string;
95
+ }
96
+ declare const UnifiIdentityChanged_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
97
+ readonly _tag: "UnifiIdentityChanged";
98
+ } & Readonly<A>;
99
+ /** `olds` and `news` name different objects: the prior state is no baseline for this write. */
100
+ export declare class UnifiIdentityChanged extends UnifiIdentityChanged_base<{
101
+ readonly type: string;
102
+ readonly identity: string;
103
+ readonly previous: string;
104
+ }> {
105
+ get message(): string;
106
+ }
19
107
  export {};
20
108
  //# sourceMappingURL=policy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/unifi/policy.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,eAAO,MAAM,sBAAsB,oEACgC,CAAC;AAEpE,2FAA2F;AAC3F,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;;;;AAE9D,qBAAa,iBAAkB,SAAQ,uBAAsC;IAC3E,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;CACnC,CAAC;IACA,IAAa,OAAO,IAAI,MAAM,CAE7B;CACF;AAED,2FAA2F;AAC3F,eAAO,MAAM,WAAW,SAAU,MAAM,YAAY,MAAM,UAAU,gBAAgB,mDACpB,CAAC"}
1
+ {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/unifi/policy.ts"],"names":[],"mappings":"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"}
@@ -1,16 +1,17 @@
1
1
  import type { Input } from 'alchemy/Input';
2
2
  import type { UnifiNetworkOpContext } from '@distilled.cloud/unifi-network/Protocol';
3
3
  import * as Effect from 'effect/Effect';
4
- import * as HttpClient from 'effect/http/HttpClient';
4
+ import type * as HttpClient from 'effect/http/HttpClient';
5
+ import { type ReconcileArgs, type UnifiUpdate } from './update-reconcile.ts';
5
6
  /** What every handler needs from the caller's runtime once `CredentialsFromEnv` is baked in. */
6
7
  export type UnifiRequirements = HttpClient.HttpClient;
7
8
  /**
8
9
  * One UniFi Network object's read. `Live` is whatever the SDK decodes (a `NetworkDetails`, a
9
10
  * `FirewallZone`, …); `E` is left to each resource file to declare (its own `fetchLive`'s error
10
11
  * union, with `NotFound` already folded away) so the precise per-operation error union flows
11
- * through instead of being widened by hand here.
12
+ * through instead of being widened by hand here. `E2` is the error union of the optional write.
12
13
  */
13
- export type UnifiSpec<Props extends object, Live, Attributes extends object, E> = {
14
+ export type UnifiSpec<Props extends object, Live, Attributes extends object, E, E2 = never> = {
14
15
  /** The vendor type string, e.g. `Unifi.Network` — for the refusal message only. */
15
16
  readonly type: string;
16
17
  /** Already folds a genuine not-found to `undefined`; nothing else is folded (see header). */
@@ -19,8 +20,10 @@ export type UnifiSpec<Props extends object, Live, Attributes extends object, E>
19
20
  readonly matches: (attributes: Attributes, props: Props) => boolean;
20
21
  /** For the refusal message and read-back failures. */
21
22
  readonly describe: (props: Props) => string;
23
+ /** Present only on a family allowed to write (`Unifi.Network`); see `update-reconcile.ts`. */
24
+ readonly update?: UnifiUpdate<Props, Live, E2>;
22
25
  };
23
- export declare const unifiOperations: <Props extends object, Live, Attributes extends object, E>(spec: UnifiSpec<Props, Live, Attributes, E>) => {
26
+ export declare const unifiOperations: <Props extends object, Live, Attributes extends object, E, E2>(spec: UnifiSpec<Props, Live, Attributes, E, E2>) => {
24
27
  read: (props: Props) => Effect.Effect<Attributes | undefined, E, UnifiNetworkOpContext>;
25
28
  readHandler: ({ olds, output }: {
26
29
  olds: Props;
@@ -31,51 +34,17 @@ export declare const unifiOperations: <Props extends object, Live, Attributes ex
31
34
  } | {
32
35
  readonly action: 'noop';
33
36
  } | undefined, E, UnifiNetworkOpContext>;
34
- reconcile: (news: Props) => Effect.Effect<Attributes, E | import("./policy.ts").UnifiWriteRefused, UnifiNetworkOpContext>;
37
+ /** Pre-state callers (tests, adopt-only probes) pass just `news`: `olds`/`output` unset. */
38
+ reconcile: (news: Props, rest?: Partial<Omit<ReconcileArgs<Props>, 'news'>>) => Effect.Effect<Attributes, E | E2 | import("./policy.ts").UnifiIdentityChanged | import("./policy.ts").UnifiLiveDriftedSinceDeploy | import("./policy.ts").UnifiUpdateDidNotConverge | import("./policy.ts").UnifiWriteRefused, UnifiNetworkOpContext>;
35
39
  destroy: (olds: Props) => Effect.Effect<never, import("./policy.ts").UnifiWriteRefused, never>;
36
40
  };
37
41
  /**
38
- * B0a / T12 DEFENSE IN DEPTH. `policy.ts`'s `UnifiWriteRefused` stops write INTENT at
39
- * `reconcile`/`destroy` — every path this engine exposes already returns that refusal instead of
40
- * calling an SDK write op. This guard stops the same thing one layer lower, AT THE WIRE: it wraps
41
- * whatever `HttpClient` the caller already provides (the stack's `FetchHttpClient.layer` in
42
- * production, `fakeUnifiLayer` in tests — `openbao/bao-http.ts`'s header names this same
43
- * "wrap, don't replace" seam) so a future resource file that, by mistake, called an SDK write
44
- * operation directly — bypassing `unifiOperations` entirely — would still never reach the vendor
45
- * API. `write-op-reference.test.ts` proves no such call exists today; this is what stops one from
46
- * ever taking effect if one is ever added.
47
- *
48
- * ⚠️ `Effect.die`, NOT A TYPED FAILURE — classified per distilled-doctrine's "classify every
49
- * die/orDie". Every generated SDK operation (`networks.getNetworkDetails`, …) declares its OWN
50
- * closed error union from the pinned OpenAPI spec, and generated files are never hand-edited to
51
- * add a member to it. A typed `Effect.fail` here would be a failure mode absent from every op's
52
- * declared type the moment it occurred — a type-system lie. A defect is the sound way to add a
53
- * NEW "this must be structurally impossible" failure underneath types this package does not
54
- * own, exactly like `openbao`'s `refuse()` helpers' `Effect.die(new Error(...))` for a
55
- * configuration move that should never be reachable.
56
- */
57
- export declare class UnifiNonGetRequest extends Error {
58
- readonly method: string;
59
- /** PATH ONLY, never the full URL — see `requestPath` below. */
60
- readonly path: string;
61
- constructor(method: string,
62
- /** PATH ONLY, never the full URL — see `requestPath` below. */
63
- path: string);
64
- }
65
- /**
66
- * A `Layer` that reads whatever `HttpClient` is already in the calling context and replaces it,
67
- * for everything downstream, with `guardGetOnly`'s wrapped version (`HttpClient.layerMergedContext`
68
- * — see the guard above). Exported so `get-only-guard.test.ts` can prove the mechanism directly,
69
- * against a bare `HttpClient` service, without needing `CredentialsFromEnv` or any env var at all.
70
- */
71
- export declare const GetOnlyHttpClient: import("effect/Layer").Layer<HttpClient.HttpClient, never, HttpClient.HttpClient>;
72
- /**
73
- * The four provider handlers for a spec'd read-only UniFi object, wired once.
42
+ * The four provider handlers for a spec'd UniFi object, wired once.
74
43
  *
75
44
  * ⛔ `list` ANSWERS EMPTY. `GET /v1/sites/{siteId}/networks` answers every network on the site;
76
45
  * adoption stays explicit, the same reasoning `Proxmox.User`'s and NetBox's `list` give.
77
46
  */
78
- export declare const unifiHandlers: <Props extends object, Live, Attributes extends object, E>(spec: UnifiSpec<Props, Live, Attributes, E>) => {
47
+ export declare const unifiHandlers: <Props extends object, Live, Attributes extends object, E, E2>(spec: UnifiSpec<Props, Live, Attributes, E, E2>) => {
79
48
  list: () => Effect.Effect<never[], never, never>;
80
49
  read: (args: {
81
50
  olds: Props;
@@ -91,7 +60,12 @@ export declare const unifiHandlers: <Props extends object, Live, Attributes exte
91
60
  } | undefined, E, HttpClient.HttpClient>;
92
61
  reconcile: (args: {
93
62
  news: Props;
94
- }) => Effect.Effect<Attributes, E | import("./policy.ts").UnifiWriteRefused, HttpClient.HttpClient>;
63
+ olds: Props | undefined;
64
+ output: Attributes | undefined;
65
+ session: {
66
+ note: (message: string) => Effect.Effect<void, never, never>;
67
+ };
68
+ }) => Effect.Effect<Attributes, E | E2 | import("./policy.ts").UnifiIdentityChanged | import("./policy.ts").UnifiLiveDriftedSinceDeploy | import("./policy.ts").UnifiUpdateDidNotConverge | import("./policy.ts").UnifiWriteRefused, HttpClient.HttpClient>;
95
69
  delete: (args: {
96
70
  olds: Props;
97
71
  }) => Effect.Effect<never, import("./policy.ts").UnifiWriteRefused, HttpClient.HttpClient>;
@@ -1 +1 @@
1
- {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../../src/unifi/resource.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAE3C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AACrF,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,UAAU,MAAM,wBAAwB,CAAC;AAGrD,gGAAgG;AAChG,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC,UAAU,CAAC;AAEtD;;;;;GAKG;AACH,MAAM,MAAM,SAAS,CAAC,KAAK,SAAS,MAAM,EAAE,IAAI,EAAE,UAAU,SAAS,MAAM,EAAE,CAAC,IAAI;IAChF,mFAAmF;IACnF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,6FAA6F;IAC7F,QAAQ,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,GAAG,SAAS,EAAE,CAAC,EAAE,qBAAqB,CAAC,CAAC;IAChG,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,KAAK,UAAU,CAAC;IAC9D,QAAQ,CAAC,OAAO,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;IACpE,sDAAsD;IACtD,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC;CAC7C,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,KAAK,SAAS,MAAM,EAAE,IAAI,EAAE,UAAU,SAAS,MAAM,EAAE,CAAC,QAChF,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;kBAEtB,KAAK;oCAQQ;QAAE,IAAI,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;iBAUlE,KAAK,CAAC,KAAK,CAAC,UAAU,UAAU,GAAG,SAAS;yBAIZ,QAAQ;;yBAElC,MAAM;;sBAIP,KAAK;oBAYP,KAAK;CAExB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAEzC,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,MAAM;IAHvB,YACW,MAAM,EAAE,MAAM;IACvB,+DAA+D;IACtD,IAAI,EAAE,MAAM,EAStB;CACF;AA0BD;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,mFAE7B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GAAI,KAAK,SAAS,MAAM,EAAE,IAAI,EAAE,UAAU,SAAS,MAAM,EAAE,CAAC,QAC9E,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;IAMzC,IAAI;IACJ,IAAI,SAAS;QAAE,IAAI,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;IAE5D,IAAI,SAAS;QAAE,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;yBA5GtB,QAAQ;;yBAElC,MAAM;;IA4GzB,SAAS,SAAS;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE;IACjC,MAAM,SAAS;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE;CAEjC,CAAC"}
1
+ {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../../src/unifi/resource.ts"],"names":[],"mappings":"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"}
@@ -42,7 +42,12 @@ export declare const handlers: {
42
42
  } | undefined, trafficMatchingLists.UnifiNetworkOpError, import("effect/http/HttpClient").HttpClient>;
43
43
  reconcile: (args: {
44
44
  news: TrafficMatchingListProps;
45
- }) => Effect.Effect<TrafficMatchingListAttributes, import("./policy.ts").UnifiWriteRefused | trafficMatchingLists.UnifiNetworkOpError, import("effect/http/HttpClient").HttpClient>;
45
+ olds: TrafficMatchingListProps | undefined;
46
+ output: TrafficMatchingListAttributes | undefined;
47
+ session: {
48
+ note: (message: string) => Effect.Effect<void, never, never>;
49
+ };
50
+ }) => Effect.Effect<TrafficMatchingListAttributes, import("./policy.ts").UnifiIdentityChanged | import("./policy.ts").UnifiLiveDriftedSinceDeploy | import("./policy.ts").UnifiUpdateDidNotConverge | import("./policy.ts").UnifiWriteRefused | trafficMatchingLists.UnifiNetworkOpError, import("effect/http/HttpClient").HttpClient>;
46
51
  delete: (args: {
47
52
  olds: TrafficMatchingListProps;
48
53
  }) => Effect.Effect<never, import("./policy.ts").UnifiWriteRefused, import("effect/http/HttpClient").HttpClient>;
@@ -1 +1 @@
1
- {"version":3,"file":"traffic-matching-list.d.ts","sourceRoot":"","sources":["../../src/unifi/traffic-matching-list.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,oBAAoB,MAAM,uDAAuD,CAAC;AAC9F,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EACL,KAAK,6BAA6B,EAClC,KAAK,wBAAwB,EAE9B,MAAM,iCAAiC,CAAC;AAEzC,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAGlE,YAAY,EACV,6BAA6B,EAC7B,wBAAwB,GACzB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAE3D,MAAM,WAAW,wBAAyB,SAAQ,QAAQ,CACxD,2BAA2B,EAC3B,wBAAwB,EACxB,6BAA6B,EAC7B,KAAK,EACL,iBAAiB,CAClB;CAAG;AAEJ,eAAO,MAAM,wBAAwB,2DAGpC,CAAC;AAEF;oBACoB;AACpB,eAAO,MAAM,mBAAmB,OAAQ,MAAM,SAAS,wBAAwB,gGACxB,CAAC;AAExD,iGAAiG;AACjG,eAAO,MAAM,IAAI,EAAE,SAAS,CAC1B,wBAAwB,EACxB,oBAAoB,CAAC,mBAAmB,EACxC,6BAA6B,EAC7B,oBAAoB,CAAC,2BAA2B,CAcjD,CAAC;AAEF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;CAAsB,CAAC;AAE5C,eAAO,MAAM,gCAAgC,qIAI1C,CAAC"}
1
+ {"version":3,"file":"traffic-matching-list.d.ts","sourceRoot":"","sources":["../../src/unifi/traffic-matching-list.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,oBAAoB,MAAM,uDAAuD,CAAC;AAC9F,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EACL,KAAK,6BAA6B,EAClC,KAAK,wBAAwB,EAE9B,MAAM,iCAAiC,CAAC;AAEzC,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAGlE,YAAY,EACV,6BAA6B,EAC7B,wBAAwB,GACzB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAC;AAE3D,MAAM,WAAW,wBAAyB,SAAQ,QAAQ,CACxD,2BAA2B,EAC3B,wBAAwB,EACxB,6BAA6B,EAC7B,KAAK,EACL,iBAAiB,CAClB;CAAG;AAEJ,eAAO,MAAM,wBAAwB,2DAGpC,CAAC;AAEF;oBACoB;AACpB,eAAO,MAAM,mBAAmB,OAAQ,MAAM,SAAS,wBAAwB,gGACxB,CAAC;AAExD,iGAAiG;AACjG,eAAO,MAAM,IAAI,EAAE,SAAS,CAC1B,wBAAwB,EACxB,oBAAoB,CAAC,mBAAmB,EACxC,6BAA6B,EAC7B,oBAAoB,CAAC,2BAA2B,CAcjD,CAAC;AAEF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;CAAsB,CAAC;AAE5C,eAAO,MAAM,gCAAgC,qIAI1C,CAAC"}
@@ -0,0 +1,31 @@
1
+ import type { UnifiNetworkOpContext } from '@distilled.cloud/unifi-network/Protocol';
2
+ import * as Effect from 'effect/Effect';
3
+ import type { FieldDrift } from './drift.ts';
4
+ import { UnifiIdentityChanged, UnifiLiveDriftedSinceDeploy, UnifiUpdateDidNotConverge } from './policy.ts';
5
+ import type { UnifiSpec } from './resource.ts';
6
+ import type { AllowedWrite } from './wire-guard.ts';
7
+ export type Patch<Props> = {
8
+ set: Partial<Props>;
9
+ unset: ReadonlyArray<keyof Props>;
10
+ };
11
+ /** The write half of a spec: everything needed to PUT one object, and nothing to create/delete. */
12
+ export type UnifiUpdate<Props extends object, Live, E2> = {
13
+ /** The one PUT the wire guard may pass for this row (path pinned to the row's own ids). */
14
+ readonly allowedWrite: (props: Props) => AllowedWrite;
15
+ /** Declarable keys; never the identity keys. */
16
+ readonly patchKeys: ReadonlyArray<keyof Props>;
17
+ readonly driftOf: (live: Live, props: Props) => ReadonlyArray<FieldDrift>;
18
+ /** Refuses (typed, before any note or request) a patch outside this family's write scope. */
19
+ readonly checkScope: (live: Live, patch: Patch<Props>, props: Props) => Effect.Effect<void, E2>;
20
+ readonly write: (live: Live, patch: Patch<Props>, props: Props) => Effect.Effect<Live, E2, UnifiNetworkOpContext>;
21
+ };
22
+ /** Keys whose `news` value differs from `olds`: set when still declared, unset when dropped. */
23
+ export declare const computePatch: <Props extends object>(news: Props, olds: Props, patchKeys: ReadonlyArray<keyof Props>) => Patch<Props>;
24
+ export type ReconcileArgs<Props extends object> = {
25
+ readonly news: Props;
26
+ readonly olds: Props | undefined;
27
+ readonly output: unknown;
28
+ readonly note: (message: string) => Effect.Effect<void>;
29
+ };
30
+ export declare const updateReconcile: <Props extends object, Live, Attributes extends object, E, E2>(spec: UnifiSpec<Props, Live, Attributes, E, E2>, { news, olds, output, note }: ReconcileArgs<Props>) => Effect.Effect<Attributes, E | E2 | UnifiIdentityChanged | UnifiLiveDriftedSinceDeploy | UnifiUpdateDidNotConverge | import("./policy.ts").UnifiWriteRefused, UnifiNetworkOpContext>;
31
+ //# sourceMappingURL=update-reconcile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update-reconcile.d.ts","sourceRoot":"","sources":["../../src/unifi/update-reconcile.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AACrF,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EACL,oBAAoB,EACpB,2BAA2B,EAC3B,yBAAyB,EAE1B,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEpD,MAAM,MAAM,KAAK,CAAC,KAAK,IAAI;IAAE,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IAAC,KAAK,EAAE,aAAa,CAAC,MAAM,KAAK,CAAC,CAAA;CAAE,CAAC;AAEtF,mGAAmG;AACnG,MAAM,MAAM,WAAW,CAAC,KAAK,SAAS,MAAM,EAAE,IAAI,EAAE,EAAE,IAAI;IACxD,2FAA2F;IAC3F,QAAQ,CAAC,YAAY,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,YAAY,CAAC;IACtD,gDAAgD;IAChD,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC,MAAM,KAAK,CAAC,CAAC;IAC/C,QAAQ,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,KAAK,aAAa,CAAC,UAAU,CAAC,CAAC;IAC1E,6FAA6F;IAC7F,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAChG,QAAQ,CAAC,KAAK,EAAE,CACd,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,EACnB,KAAK,EAAE,KAAK,KACT,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,EAAE,qBAAqB,CAAC,CAAC;CACrD,CAAC;AAEF,gGAAgG;AAChG,eAAO,MAAM,YAAY,GAAI,KAAK,SAAS,MAAM,QACzC,KAAK,QACL,KAAK,aACA,aAAa,CAAC,MAAM,KAAK,CAAC,KACpC,KAAK,CAAC,KAAK,CASb,CAAC;AAEF,MAAM,MAAM,aAAa,CAAC,KAAK,SAAS,MAAM,IAAI;IAChD,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,KAAK,GAAG,SAAS,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;CACzD,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,KAAK,SAAS,MAAM,EAAE,IAAI,EAAE,UAAU,SAAS,MAAM,EAAE,CAAC,EAAE,EAAE,QACpF,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,EAAE,CAAC,gCACjB,aAAa,CAAC,KAAK,CAAC,wLA2ChD,CAAC"}
@@ -64,7 +64,12 @@ export declare const handlers: {
64
64
  } | undefined, UnifiPaginationInconsistent | wifiBroadcasts.UnifiNetworkOpError, import("effect/http/HttpClient").HttpClient>;
65
65
  reconcile: (args: {
66
66
  news: WifiBroadcastProps;
67
- }) => Effect.Effect<WifiBroadcastAttributes, UnifiPaginationInconsistent | import("./policy.ts").UnifiWriteRefused | wifiBroadcasts.UnifiNetworkOpError, import("effect/http/HttpClient").HttpClient>;
67
+ olds: WifiBroadcastProps | undefined;
68
+ output: WifiBroadcastAttributes | undefined;
69
+ session: {
70
+ note: (message: string) => Effect.Effect<void, never, never>;
71
+ };
72
+ }) => Effect.Effect<WifiBroadcastAttributes, import("./policy.ts").UnifiIdentityChanged | import("./policy.ts").UnifiLiveDriftedSinceDeploy | UnifiPaginationInconsistent | import("./policy.ts").UnifiUpdateDidNotConverge | import("./policy.ts").UnifiWriteRefused | wifiBroadcasts.UnifiNetworkOpError, import("effect/http/HttpClient").HttpClient>;
68
73
  delete: (args: {
69
74
  olds: WifiBroadcastProps;
70
75
  }) => Effect.Effect<never, import("./policy.ts").UnifiWriteRefused, import("effect/http/HttpClient").HttpClient>;
@@ -1 +1 @@
1
- {"version":3,"file":"wifi-broadcast.d.ts","sourceRoot":"","sources":["../../src/unifi/wifi-broadcast.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,cAAc,MAAM,gDAAgD,CAAC;AACjF,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EAExB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAAE,KAAK,2BAA2B,EAAW,MAAM,eAAe,CAAC;AAC1E,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAGlE,YAAY,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC5F,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAEpD,MAAM,WAAW,kBAAmB,SAAQ,QAAQ,CAClD,qBAAqB,EACrB,kBAAkB,EAClB,uBAAuB,EACvB,KAAK,EACL,iBAAiB,CAClB;CAAG;AAEJ,eAAO,MAAM,kBAAkB,qDAE7B,CAAC;AAEH,kGAAkG;AAClG,eAAO,MAAM,aAAa,OAAQ,MAAM,SAAS,kBAAkB,0FAClB,CAAC;AAElD,iGAAiG;AACjG,eAAO,MAAM,IAAI,EAAE,SAAS,CAC1B,kBAAkB,EAClB,cAAc,CAAC,qBAAqB,EACpC,uBAAuB,EACvB,cAAc,CAAC,yBAAyB,GAAG,2BAA2B,CAWvE,CAAC;AAEF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;CAAsB,CAAC;AAE5C,eAAO,MAAM,0BAA0B,+HACwD,CAAC"}
1
+ {"version":3,"file":"wifi-broadcast.d.ts","sourceRoot":"","sources":["../../src/unifi/wifi-broadcast.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,cAAc,MAAM,gDAAgD,CAAC;AACjF,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EAExB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAAE,KAAK,2BAA2B,EAAW,MAAM,eAAe,CAAC;AAC1E,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAGlE,YAAY,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC5F,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AAEpD,MAAM,WAAW,kBAAmB,SAAQ,QAAQ,CAClD,qBAAqB,EACrB,kBAAkB,EAClB,uBAAuB,EACvB,KAAK,EACL,iBAAiB,CAClB;CAAG;AAEJ,eAAO,MAAM,kBAAkB,qDAE7B,CAAC;AAEH,kGAAkG;AAClG,eAAO,MAAM,aAAa,OAAQ,MAAM,SAAS,kBAAkB,0FAClB,CAAC;AAElD,iGAAiG;AACjG,eAAO,MAAM,IAAI,EAAE,SAAS,CAC1B,kBAAkB,EAClB,cAAc,CAAC,qBAAqB,EACpC,uBAAuB,EACvB,cAAc,CAAC,yBAAyB,GAAG,2BAA2B,CAWvE,CAAC;AAEF,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;CAAsB,CAAC;AAE5C,eAAO,MAAM,0BAA0B,+HACwD,CAAC"}
@@ -0,0 +1,33 @@
1
+ import * as HttpClient from 'effect/http/HttpClient';
2
+ /**
3
+ * One non-GET request the guard may let through. `tail` is the exact route below the configured
4
+ * base URL (`/v1/sites/<site>/networks/<id>`): the guard compares the request's origin and path to
5
+ * `baseUrl + tail` and requires an empty query and fragment (`isAllowed`).
6
+ */
7
+ export type AllowedWrite = {
8
+ readonly method: 'PUT';
9
+ readonly tail: string;
10
+ };
11
+ export declare class UnifiRefusedRequest extends Error {
12
+ readonly method: string;
13
+ /** PATH ONLY, never the full URL — see `requestPath` below. */
14
+ readonly path: string;
15
+ constructor(method: string,
16
+ /** PATH ONLY, never the full URL — see `requestPath` below. */
17
+ path: string, detail?: string);
18
+ }
19
+ /** @deprecated renamed `UnifiRefusedRequest`; kept so existing consumer imports do not break. */
20
+ export declare const UnifiNonGetRequest: typeof UnifiRefusedRequest;
21
+ export type UnifiNonGetRequest = UnifiRefusedRequest;
22
+ /**
23
+ * A `Layer` that reads whatever `HttpClient` is already in the calling context and replaces it,
24
+ * for everything downstream, with the guarded version. Exported so `wire-guard.test.ts` can prove
25
+ * the mechanism against a bare `HttpClient`, without `CredentialsFromEnv` or any env var.
26
+ */
27
+ export declare const guardedHttpClient: (allow: ReadonlyArray<AllowedWrite>, baseUrl?: string) => import("effect/Layer").Layer<HttpClient.HttpClient, never, HttpClient.HttpClient>;
28
+ /**
29
+ * No write may pass; the posture of `read`/`diff`/`delete` and of every non-updatable family. A 3xx
30
+ * answer to a GET is refused here exactly as under the allow-list posture — see REDIRECTS above.
31
+ */
32
+ export declare const GetOnlyHttpClient: import("effect/Layer").Layer<HttpClient.HttpClient, never, HttpClient.HttpClient>;
33
+ //# sourceMappingURL=wire-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wire-guard.d.ts","sourceRoot":"","sources":["../../src/unifi/wire-guard.ts"],"names":[],"mappings":"AAkCA,OAAO,KAAK,UAAU,MAAM,wBAAwB,CAAC;AAKrD;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG;IAAE,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7E,qBAAa,mBAAoB,SAAQ,KAAK;IAE1C,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,+DAA+D;IAC/D,QAAQ,CAAC,IAAI,EAAE,MAAM;IAHvB,YACW,MAAM,EAAE,MAAM;IACvB,+DAA+D;IACtD,IAAI,EAAE,MAAM,EACrB,MAAM,SAAiC,EASxC;CACF;AAED,iGAAiG;AACjG,eAAO,MAAM,kBAAkB,4BAAsB,CAAC;AACtD,MAAM,MAAM,kBAAkB,GAAG,mBAAmB,CAAC;AA0FrD;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,UAAW,aAAa,CAAC,YAAY,CAAC,YAAY,MAAM,sFASrF,CAAC;AAkBF;;;GAGG;AACH,eAAO,MAAM,iBAAiB,mFAAwB,CAAC"}
@@ -0,0 +1,146 @@
1
+ # HomeFlare.Kubernetes.Ready
2
+
3
+ `@homeflare/alchemy/kubernetes` — a readiness gate for a Kubernetes cluster reached through the
4
+ `talos-openbao` adapter. Walked against `alchemy@2.0.0-beta.81`. Proven offline only (fake `bao`,
5
+ fake apiserver); **not** exercised against a live cluster.
6
+
7
+ ## Why it exists
8
+
9
+ Upstream ships no readiness resource. `Kubernetes.HelmChart` reconcile returns when server-side
10
+ apply returns, and Alchemy releases every downstream row at that moment. A row chained after the
11
+ Cilium chart would start before one Cilium pod runs. Declare a `Ready` row with
12
+ `after: [chart.objects]` and chain later rows on `ready.ready`.
13
+
14
+ ## Declaring it
15
+
16
+ ```ts
17
+ const cniReady =
18
+ yield *
19
+ KubernetesReady('cni-ready', {
20
+ connection: talosOpenBaoConnection(PINNED_UID), // a literal uid, never an Output
21
+ checks: [
22
+ { kind: 'DaemonSet', namespace: 'kube-system', name: 'cilium', minReady: 2 },
23
+ { kind: 'Deployment', namespace: 'kube-system', name: 'cilium-operator' },
24
+ { kind: 'CustomResourceDefinition', name: 'ciliumloadbalancerippools.cilium.io' },
25
+ ],
26
+ waitTimeout: '10m0s', // Go style h/m/s, default 10m0s
27
+ pollInterval: '10s', // default 10s
28
+ after: [cilium.objects],
29
+ });
30
+ ```
31
+
32
+ Providers: `Layer.mergeAll(Kubernetes.providers(), TalosOpenBaoAdapter(...), KubernetesReadyProvider())`.
33
+ The provider requires `ChildProcessSpawner` (the adapter shells to `bao`).
34
+
35
+ ## What "ready" means
36
+
37
+ The rules are `kubectl rollout status` (kubectl v0.34.0 `rollout_status.go`), not "Available":
38
+
39
+ | kind | ready when |
40
+ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
41
+ | DaemonSet | `updateStrategy` is `RollingUpdate`, `observedGeneration >= generation`, `updatedNumberScheduled >= minReady`, `numberAvailable - (desiredNumberScheduled - updatedNumberScheduled) >= minReady` |
42
+ | Deployment | `observedGeneration >= generation`, `updatedReplicas >= spec.replicas`, `replicas == updatedReplicas`, `availableReplicas >= updatedReplicas` |
43
+ | CustomResourceDefinition | condition `Established` is `True` |
44
+
45
+ `minReady` defaults to `desiredNumberScheduled`, and a DaemonSet that wants zero pods is never ready.
46
+ `numberAvailable` also counts available pods of the OLD template, so it is never compared to
47
+ `minReady` alone: subtracting `desired - updated` (the most old pods there can be) leaves a lower
48
+ bound on pods that are both updated and available. Without it, minReady 2 of 4 with two crashlooping
49
+ new pods and two healthy old ones would pass.
50
+ A Deployment whose `Progressing` condition has reason `ProgressDeadlineExceeded` (for the
51
+ generation the controller has observed), and a DaemonSet whose `updateStrategy` is not
52
+ `RollingUpdate` (absent counts as `RollingUpdate`), are typed failures, `KubernetesRolloutFailed`
53
+ (`reason`), not pending.
54
+
55
+ ## Refused at declaration
56
+
57
+ - `checks` must be non-empty; each needs a known `kind`, a non-empty `name`, a non-empty
58
+ `namespace` for DaemonSet and Deployment, and `minReady` (DaemonSet only) an integer >= 1:
59
+ `KubernetesReadyBadCheck { index, problem }`.
60
+ - `after` must hold lazy Outputs of the chart (`chart.objects`): `KubernetesReadyBadAfter { index }`
61
+ for a plain value, a bare `chart`, or a stable such as `chart.connection`. Those resolve at plan
62
+ time, so the row would plan `noop` before the chart's update and the gate would be skipped.
63
+
64
+ ## Errors
65
+
66
+ | outcome | `read` / `diff` (one pass) | `reconcile` (poll) |
67
+ | -------------------------------------------------------------- | -------------------------- | --------------------------------------------------------- |
68
+ | 404 (`_tag` `KubernetesNotFound`, or `KubernetesApiError` 404) | pending | pending |
69
+ | per-GET timeout (5 s), 5xx, 429, upstream transport `Error` | read: throws; diff: update | pending, recorded as `lastTransient` (tag + status) |
70
+ | the same on the connect's identity GET (incl. its 5 s timeout) | propagates | connect retried every `pollInterval`, inside the deadline |
71
+ | 401, 403, other 4xx | propagates | propagates |
72
+ | uid mismatch, vault (`bao`) failure | propagates | propagates |
73
+ | any other untagged `Error` (a defect, e.g. an empty namespace) | propagates | propagates |
74
+
75
+ - "Upstream transport `Error`" is only the `Failed Kubernetes ...` wrapper (alchemy `client.ts`).
76
+ - `lastTransient` is the transient of the LAST pass; one that has cleared is not blamed.
77
+ - `read` with no prior output (Alchemy's deferred adoption read on a first create) returns
78
+ `undefined` without touching the cluster: the row owns nothing to adopt.
79
+ - Every error leaving a GET, the connect's identity GET included, is scrubbed of the response body.
80
+
81
+ - 404 is matched by tag string with no import, so it is proof against alchemy raising
82
+ `KubernetesNotFound` instead of `KubernetesApiError` (standing watch: drop the second shape once
83
+ upstream settles).
84
+ - A deadline fails with `KubernetesReadyTimeout { failing, seconds, lastTransient?, states? }`.
85
+ `failing` holds **check keys** (`DaemonSet/kube-system/cilium`) only; `states` records per key
86
+ what the last pass saw: `{ notFound: true }` (a 404 that never resolves) or `{ counts }`
87
+ (generation and rollout numbers), so a missing object reads differently from a slow rollout.
88
+ - A propagated apiserver refusal is rewrapped as `KubernetesReadyApiError { method, path,
89
+ statusCode }`: upstream's `KubernetesApiError` message quotes up to 1000 bytes of the response
90
+ body, which this row does not republish.
91
+ - Upstream `readObject` takes no signal, so a timed-out GET abandons its socket rather than
92
+ aborting it, and process exit is not a bound for a long apply. The poll therefore rations
93
+ timeouts: after one GET times out the rest of that pass is pending with no GET, and the pause
94
+ doubles per consecutive timeout (cap 30 s), resetting on a clean pass. Standing watch: drop this
95
+ when upstream's `readObject` takes an AbortSignal (alchemy PR 1948, per-attempt deadlines).
96
+ - Upstream also retries a transport error itself, for about 40 s (a `spaced` 5 s schedule capped
97
+ by `recurs(8)`), longer than the per-GET bound, so a dead socket surfaces as the GET timeout.
98
+
99
+ ## Lifecycle
100
+
101
+ - `read`: one pass, `ready: false` when any check is pending. Never waits.
102
+ - `diff`: refuses a non-literal `connection` (`TalosUidNotLiteral`, also at declaration);
103
+ `undefined` when unresolved; `replace` when the connection differs (it is in `stables`, so
104
+ dependants would otherwise see the old one for a deploy; `delete` is a no-op, so nothing live
105
+ is touched); `noop` when the live pass is ready and the check keys are unchanged; otherwise
106
+ `update`. It is live on every plan, like `Talos.ClusterHealth`, but a transient (5xx, 429, a
107
+ timeout, on the connect or a GET) is `update`, never a failed plan: `reconcile` does the
108
+ waiting. 401/403 and vault failures still propagate.
109
+ - `reconcile`: polls until every check passes or fails typed.
110
+ - `delete` is a no-op and `list` is empty: the row owns nothing in the cluster. `connection` is
111
+ `stables`, and is copied onto the attributes so the row is `ClusterLike`.
112
+
113
+ ## Secrecy
114
+
115
+ Attributes are `{ connection, ready, checks }`; the connection is `{ kind, uid }`. No kubeconfig,
116
+ PEM, token or response body reaches an attribute, an error, a log or a file
117
+ (`ready-secrecy.test.ts`).
118
+
119
+ ## Bounded HelmChart (`boundedHelmChartProvider`, ledger row `t10-k1b`)
120
+
121
+ Upstream's `Kubernetes.HelmChart` has no deadline: alchemy 2.0.0-beta.81's `requestJson` sets no
122
+ timeout and no signal, so a hung apiserver blocks `reconcile`/`read` forever and would hold a 1h
123
+ platform token across a stuck deploy. `boundedHelmChartProvider()` wraps upstream's provider (never
124
+ re-implements it) in `Effect.timeoutOrElse`, failing typed `KubernetesReconcileTimeout { id,
125
+ operation, seconds }`: `HELM_RECONCILE_TIMEOUT = '5m0s'` (render, discovery, ~60 SSA PATCHes) and
126
+ `HELM_READ_TIMEOUT = '1m0s'`.
127
+
128
+ - Merge it beside `Kubernetes.providers()`: `Layer.mergeAll(Kubernetes.providers(),
129
+ boundedHelmChartProvider())`. A direct `Provider(HelmChart)` service beats the collection
130
+ (`Provider.ts` `tryFindProviderRegistrationByType`), so the wrapper is the one the engine resolves.
131
+ - `diff`, `delete`, `stables` and `aliases` are upstream's by spread: state identity and replace
132
+ semantics do not change.
133
+ - The interrupted fiber abandons its socket (upstream takes no signal); the run fails typed and the
134
+ process exits once main completes. Standing watch: upstream puts no `AbortSignal` on its
135
+ requests; drop the wrapper when it ships request deadlines (alchemy PR 1948).
136
+ - A malformed override dies at layer build rather than leaving the call unbounded, as
137
+ `KubernetesReadyBadDuration` naming `Kubernetes.HelmChart` and the field
138
+ (`reconcileTimeout`/`readTimeout`). The timeout message names the row and seconds only.
139
+ - Errors that pass through (an upstream `KubernetesApiError` quotes up to 1000 bytes of the
140
+ apiserver body, `client.ts:39-41`) are scrubbed to the body-free `KubernetesReadyApiError`.
141
+ - The read bound rarely fires with the `talos-openbao` adapter: upstream's read only connects, and
142
+ the kit already caps connect at 10 s and the uid read at 5 s. It is a backstop.
143
+ - `delete` stays unbounded on purpose: every platform row is `retain`, replace cleanup skips
144
+ deletion of retained rows (alchemy `Apply.ts:1111-1112`), and design §8 only drops state.
145
+ - `isBoundedHelmChartProvider(service)` is true for a service this layer built, so a consuming
146
+ stack can assert its real `providers` layer resolves the wrapper and not upstream's.
package/docs/peers.md CHANGED
@@ -29,6 +29,8 @@ asserted against that field; change the field first, then copy its `overrides` b
29
29
  "@effect/platform-node": "4.0.1",
30
30
  "@effect/platform-node-shared": "4.0.1",
31
31
  "@effect/platform-bun": "4.0.1",
32
+ "@effect/sql-d1": "4.0.1",
33
+ "@effect/sql-sqlite-do": "4.0.1",
32
34
  "rolldown": "1.2.8",
33
35
  "redis": "6.3.0"
34
36
  }
@@ -50,6 +52,7 @@ next rc and breaks at import. Pin the whole set.
50
52
  | `rolldown` → 1.2.9 via vite's `~1.2.6` (measured 2026-09-16) | `GET …/rolldown-1.2.9.tgz - 404` at `bun add` |
51
53
  | no `mime` (measured 2026-09-17 against Alchemy 78) | `Cannot find package 'mime'` from cloudflare-runtime |
52
54
  | `redis` → 6.3.0 via the `>=5.0.0 <7.0.0` peer of platform-node/sql-pg (measured 2026-09-30) | `No version matching "6.3.0" found for specifier "@redis/time-series" (but package exists)` at `bun add` |
55
+ | `@effect/sql-d1`/`-sqlite-do` → 4.0.2 via alchemy's `^4.0.0` (measured 2026-10-09) | unmet peer: 4.0.2 wants `effect ^4.0.2`, the contract pins 4.0.1 |
53
56
 
54
57
  Alchemy 78 adapted to rc.115 — that first row is why we used to pin 112, not a reason to
55
58
  stay there. The override is still the only thing that holds the set together.
@@ -62,6 +65,16 @@ failed outright for that window. The line completed the same hour — every `@re
62
65
  sub-package published at 6.3.0 (measured 11:12:19Z) — and the pin moved onto that complete
63
66
  set. The pin stays exact because the same class of gap can recur with any future redis minor.
64
67
 
68
+ ⛔ **`@effect/sql-d1` and `@effect/sql-sqlite-do` (added 2026-10-09, ledger row
69
+ kit-contract-effect-402) are exact in `overrides` because alchemy beta.81 itself depends on
70
+ both at `^4.0.0`** (`npm view alchemy@2.0.0-beta.81 dependencies`). A fresh, lockless install
71
+ resolves `^4.0.0` to 4.0.2, whose peer is `effect ^4.0.2` — an **unmet peer against the
72
+ contract's effect 4.0.1 on every fresh consumer install**; only an already-locked tree hid it
73
+ (the kit's own `bun.lock` said 4.0.1 because it was locked earlier). 4.0.1 peers on
74
+ `effect ^4.0.1`, so the pin holds the D1 / Durable-Object SQL resources on the version the
75
+ kit is tested on — the runtime path is unverified, as no stack uses those resources yet. They
76
+ are overrides, not install-line pins: they arrive transitively through `alchemy`.
77
+
65
78
  ⚠️ `@effect/platform-node` is **required, not optional**: Alchemy's module graph reaches
66
79
  `Cloudflare/Workers/WorkerBridge → @effect/platform-node/NodeServices` even when you only
67
80
  import the Proxmox subpath.
@@ -98,6 +98,13 @@ already ships is a finding, even when it works.
98
98
  `Redacted` values in plaintext, and only one state store encrypts them.
99
99
  - **Bounded waits (S26).** A retry stops after at most 8 to 10 attempts and under about a
100
100
  minute. A poll never runs past about 90 seconds.
101
+ - **The one exception: readiness gates** (`Talos.ClusterHealth`, `Kubernetes.Ready`). For these,
102
+ waiting IS the resource's job: a CNI or control-plane rollout takes minutes. So the wait may
103
+ run to a declared `waitTimeout` (default minutes, never unbounded) when ALL of these hold:
104
+ the timeout is a typed, declared prop; every single request inside it is bounded (a per-call
105
+ deadline); transient errors are pending, never a retry storm; and the timeout is a typed error
106
+ naming what was still pending. Any other resource keeps the 90-second rule. (Added
107
+ 2026-10-10 with `Kubernetes.Ready`, PR 373; `Talos.ClusterHealth` already waited 20m.)
101
108
 
102
109
  ## The Bun line for this package (S42–S45)
103
110