@homeflare/alchemy 0.12.0 → 0.13.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.
- package/dist/proxmox/alertmanager-body.d.ts +54 -0
- package/dist/proxmox/alertmanager-body.d.ts.map +1 -0
- package/dist/proxmox/credential-literals.d.ts +36 -0
- package/dist/proxmox/credential-literals.d.ts.map +1 -0
- package/dist/proxmox/fake-pbs-notify.d.ts +23 -0
- package/dist/proxmox/fake-pbs-notify.d.ts.map +1 -0
- package/dist/proxmox/fake-pve.d.ts +5 -0
- package/dist/proxmox/fake-pve.d.ts.map +1 -1
- package/dist/proxmox/generated/pbs.d.ts +278 -0
- package/dist/proxmox/generated/pbs.d.ts.map +1 -1
- package/dist/proxmox/generated/pve.d.ts +57 -0
- package/dist/proxmox/generated/pve.d.ts.map +1 -1
- package/dist/proxmox/index.d.ts +8 -0
- package/dist/proxmox/index.d.ts.map +1 -1
- package/dist/proxmox/index.js +681 -183
- package/dist/proxmox/index.js.map +14 -4
- package/dist/proxmox/notification-matcher-form.d.ts +100 -0
- package/dist/proxmox/notification-matcher-form.d.ts.map +1 -0
- package/dist/proxmox/notification-matcher.d.ts +46 -0
- package/dist/proxmox/notification-matcher.d.ts.map +1 -0
- package/dist/proxmox/pbs-notification-matcher.d.ts +53 -0
- package/dist/proxmox/pbs-notification-matcher.d.ts.map +1 -0
- package/dist/proxmox/pbs-notification-target-form.d.ts +63 -0
- package/dist/proxmox/pbs-notification-target-form.d.ts.map +1 -0
- package/dist/proxmox/pbs-notification-target-lifecycle.d.ts +100 -0
- package/dist/proxmox/pbs-notification-target-lifecycle.d.ts.map +1 -0
- package/dist/proxmox/pbs-notification-target-wire.d.ts +30 -0
- package/dist/proxmox/pbs-notification-target-wire.d.ts.map +1 -0
- package/dist/proxmox/pbs-notification-target.d.ts +123 -0
- package/dist/proxmox/pbs-notification-target.d.ts.map +1 -0
- package/dist/proxmox/resource.d.ts +4 -3
- package/dist/proxmox/resource.d.ts.map +1 -1
- package/dist/proxmox/write-only.d.ts +35 -0
- package/dist/proxmox/write-only.d.ts.map +1 -0
- package/dist/verify/fake-engine.d.ts +5 -0
- package/dist/verify/fake-engine.d.ts.map +1 -1
- package/docs/adopted-deploys.md +8 -0
- package/docs/pbs-alertmanager-body.md +73 -0
- package/docs/pbs-notifications.md +163 -0
- package/docs/proxmox.md +3 -0
- package/package.json +1 -1
- package/src/proxmox/alertmanager-body.ts +125 -0
- package/src/proxmox/credential-literals.test.ts +148 -0
- package/src/proxmox/credential-literals.ts +74 -0
- package/src/proxmox/fake-pbs-notify.ts +108 -0
- package/src/proxmox/fake-pve.ts +12 -2
- package/src/proxmox/generated/pbs.ts +329 -0
- package/src/proxmox/generated/pve.ts +62 -0
- package/src/proxmox/index.test.ts +3 -0
- package/src/proxmox/index.ts +18 -0
- package/src/proxmox/notification-matcher-form.ts +221 -0
- package/src/proxmox/notification-matcher.test.ts +234 -0
- package/src/proxmox/notification-matcher.ts +76 -0
- package/src/proxmox/pbs-notification-matcher.ts +81 -0
- package/src/proxmox/pbs-notification-target-drift.test.ts +83 -0
- package/src/proxmox/pbs-notification-target-form.ts +229 -0
- package/src/proxmox/pbs-notification-target-lifecycle.ts +177 -0
- package/src/proxmox/pbs-notification-target-state.test.ts +191 -0
- package/src/proxmox/pbs-notification-target-wire.ts +112 -0
- package/src/proxmox/pbs-notification-target.test.ts +226 -0
- package/src/proxmox/pbs-notification-target.ts +139 -0
- package/src/proxmox/pbs-notifications-stack.test.ts +94 -0
- package/src/proxmox/resource.ts +6 -6
- package/src/proxmox/write-only.test.ts +75 -0
- package/src/proxmox/write-only.ts +106 -0
- package/src/verify/fake-engine.ts +8 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A notification matcher — the rule that routes a notification to its targets — as ONE shape
|
|
3
|
+
* shared by PVE (`Proxmox.NotificationMatcher`) and PBS (`Pbs.NotificationMatcher`).
|
|
4
|
+
*
|
|
5
|
+
* ★ ONE FILE FOR BOTH PRODUCTS BECAUSE IT IS ONE IMPLEMENTATION. Both servers run the same Rust
|
|
6
|
+
* crate, `proxmox-notify` (matcher.rs), PVE through its Perl bindings; the schemas in
|
|
7
|
+
* generated/pve.ts and generated/pbs.ts carry the same nine fields, and the forms below are
|
|
8
|
+
* typed against BOTH, so a drift on either side fails `tsc` here rather than on a server.
|
|
9
|
+
*
|
|
10
|
+
* ⚠️ A MATCHER IS ITS WHOLE RULE, SO EVERY RULE FIELD IS COMPARED, DECLARED OR NOT. Most families
|
|
11
|
+
* here leave an undeclared field alone (backup-job.ts's trade). A matcher cannot: an undeclared
|
|
12
|
+
* `match-field` on a matcher that has one is a declaration claiming "route everything" over a
|
|
13
|
+
* server that routes one job type — and `invert-match` flips the meaning of every other line.
|
|
14
|
+
* So `match-*` default to empty, `mode` to `all`, `invert-match` and `disable` to false, all are
|
|
15
|
+
* compared, and a PUT sends all of them (an emptied list as `delete`). Only `comment` keeps the
|
|
16
|
+
* usual "undeclared is unmanaged" rule, because nothing routes on it.
|
|
17
|
+
* ★ THE COST IS THAT ADOPTING A MATCHER MEANS DECLARING IT AS IT IS. PBS ships `default-matcher`
|
|
18
|
+
* with `invert-match`, a `match-field` and a `match-severity` (measured on PBS 4.2, 2026-09-22),
|
|
19
|
+
* so adopting it as-is means writing those three down. A declaration that leaves them out
|
|
20
|
+
* plans an UPDATE, which is the plan telling the truth about what the deploy would do.
|
|
21
|
+
*/
|
|
22
|
+
import type { ClusterNotificationsMatchersNamePutParams, ClusterNotificationsMatchersPostParams } from './generated/pve.ts';
|
|
23
|
+
import type { ConfigNotificationsMatchersNamePutParams, ConfigNotificationsMatchersPostParams } from './generated/pbs.ts';
|
|
24
|
+
/** Everything a matcher declares except where it lives — the resource files add `target`. */
|
|
25
|
+
export interface NotificationMatcherFields {
|
|
26
|
+
/** The primary key, a safe id (2–32 chars on PBS). Changing it is a replace. */
|
|
27
|
+
name: string;
|
|
28
|
+
/**
|
|
29
|
+
* ⛔ THE TARGETS, BY NAME, AND REQUIRED: a matcher that notifies nobody is a routing rule for
|
|
30
|
+
* silence. `[]` is still declarable, and clears the list.
|
|
31
|
+
* ⚠️ `targets`, NOT THE SERVER'S `target`: every resource here already spends `target` on the
|
|
32
|
+
* HOST (resource.ts `WithTarget`), and one key cannot mean both. The wire still says `target`.
|
|
33
|
+
*/
|
|
34
|
+
targets: readonly string[];
|
|
35
|
+
/**
|
|
36
|
+
* Severity rules. EACH ITEM IS ITSELF A COMMA LIST: `'warning,error'` matches either.
|
|
37
|
+
* ⛔ `['warning', 'error']` IS NOT THE SAME RULE AND UNDER `mode: 'all'` NEVER MATCHES. Items are
|
|
38
|
+
* combined with `mode`; the severities inside one item are always OR-ed (matcher.rs
|
|
39
|
+
* `SeverityMatcher`). A notification has one severity, so two items under AND ask for a
|
|
40
|
+
* notification that is both — and routing nothing is the quietest failure a matcher has.
|
|
41
|
+
* Severities: `info`, `notice`, `warning`, `error`, `unknown`.
|
|
42
|
+
*/
|
|
43
|
+
'match-severity'?: readonly string[];
|
|
44
|
+
/**
|
|
45
|
+
* Metadata rules, `exact:<field>=<v1>,<v2>` or `regex:<field>=<re>`. ⚠️ THE PREFIX IS REQUIRED:
|
|
46
|
+
* the schema's pattern makes it optional and the parser then refuses it. A notification WITHOUT
|
|
47
|
+
* the field never matches its rule — so `exact:job-id=…` excludes every garbage collection.
|
|
48
|
+
*/
|
|
49
|
+
'match-field'?: readonly string[];
|
|
50
|
+
/** Time windows, e.g. `mon-fri 9:00-17:00`. Compared as written; the server keeps the text. */
|
|
51
|
+
'match-calendar'?: readonly string[];
|
|
52
|
+
/** How the rule items combine. The server's default, and this package's, is `all`. */
|
|
53
|
+
mode?: 'all' | 'any';
|
|
54
|
+
/** Invert the whole rule. ⚠️ IGNORED WHEN THERE ARE NO RULES — an empty matcher always matches. */
|
|
55
|
+
'invert-match'?: boolean;
|
|
56
|
+
/** A disabled matcher routes nothing. */
|
|
57
|
+
disable?: boolean;
|
|
58
|
+
/** Free text. The one field left alone when undeclared; `''` clears it. */
|
|
59
|
+
comment?: string;
|
|
60
|
+
}
|
|
61
|
+
/** ⚠️ Lists are CANONICAL and SORTED, so a re-ordered declaration is not a diff. */
|
|
62
|
+
export interface NotificationMatcherAttributes {
|
|
63
|
+
name: string;
|
|
64
|
+
/**
|
|
65
|
+
* `builtin`, `modified-builtin` or `user-created`. REPORTED, NEVER COMPARED.
|
|
66
|
+
* ⛔ ON A BUILT-IN A DELETE REVERTS RATHER THAN REMOVES, and reports success either way — Alchemy
|
|
67
|
+
* drops the row while `default-matcher` goes on routing. Read this before destroying one.
|
|
68
|
+
* ★ MEASURED ON THE PER-OBJECT READ, which the published schema does not list: PVE 9.2.11 and
|
|
69
|
+
* PBS 4.2 both return it from `…/matchers/default-matcher` (2026-09-22).
|
|
70
|
+
*/
|
|
71
|
+
origin: string;
|
|
72
|
+
comment: string;
|
|
73
|
+
disable: boolean;
|
|
74
|
+
'invert-match': boolean;
|
|
75
|
+
mode: string;
|
|
76
|
+
targets: readonly string[];
|
|
77
|
+
'match-severity': readonly string[];
|
|
78
|
+
'match-field': readonly string[];
|
|
79
|
+
'match-calendar': readonly string[];
|
|
80
|
+
}
|
|
81
|
+
/** A declaration's lists, canonical and sorted — the form `matches` compares. */
|
|
82
|
+
export declare const canonicalRules: (fields: NotificationMatcherFields) => {
|
|
83
|
+
'match-calendar': string[];
|
|
84
|
+
'match-field': string[];
|
|
85
|
+
'match-severity': string[];
|
|
86
|
+
targets: string[];
|
|
87
|
+
};
|
|
88
|
+
export declare const matcherAttributes: (live: Record<string, unknown>, name: string) => NotificationMatcherAttributes;
|
|
89
|
+
export declare const matcherMatches: (attributes: NotificationMatcherAttributes, fields: NotificationMatcherFields) => boolean;
|
|
90
|
+
export type MatcherCreateForm = ConfigNotificationsMatchersPostParams & ClusterNotificationsMatchersPostParams;
|
|
91
|
+
export type MatcherUpdateForm = ConfigNotificationsMatchersNamePutParams & ClusterNotificationsMatchersNamePutParams;
|
|
92
|
+
/** ⛔ `name` is create-only: the body carries it once and the path forever after. */
|
|
93
|
+
export declare const matcherCreateForm: (fields: NotificationMatcherFields) => MatcherCreateForm;
|
|
94
|
+
/**
|
|
95
|
+
* ⛔ AN EMPTIED LIST IS A `delete`, NOT AN EMPTY VALUE. A form cannot carry an empty array — no
|
|
96
|
+
* key is sent at all — so without the `delete` entry a declaration of `'match-field': []`
|
|
97
|
+
* would plan an update, PUT nothing for that field, and plan the same update forever.
|
|
98
|
+
*/
|
|
99
|
+
export declare const matcherUpdateForm: (fields: NotificationMatcherFields) => MatcherUpdateForm;
|
|
100
|
+
//# sourceMappingURL=notification-matcher-form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification-matcher-form.d.ts","sourceRoot":"","sources":["../../src/proxmox/notification-matcher-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EACV,yCAAyC,EACzC,sCAAsC,EACvC,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EACV,wCAAwC,EACxC,qCAAqC,EACtC,MAAM,oBAAoB,CAAC;AAG5B,6FAA6F;AAC7F,MAAM,WAAW,yBAAyB;IACxC,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;;OAIG;IACH,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,+FAA+F;IAC/F,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,sFAAsF;IACtF,IAAI,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IACrB,mGAAmG;IACnG,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,yCAAyC;IACzC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,oFAAoF;AACpF,MAAM,WAAW,6BAA6B;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,EAAE,OAAO,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAiCD,iFAAiF;AACjF,eAAO,MAAM,cAAc,WAAY,yBAAyB;IAC9D,gBAAgB;IAChB,aAAa;IACb,gBAAgB;IAChB,OAAO;CACP,CAAC;AAEH,eAAO,MAAM,iBAAiB,SACtB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,QACvB,MAAM,KACX,6BAYD,CAAC;AAKH,eAAO,MAAM,cAAc,eACb,6BAA6B,UACjC,yBAAyB,KAChC,OAYF,CAAC;AA0BF,MAAM,MAAM,iBAAiB,GAAG,qCAAqC,GACnE,sCAAsC,CAAC;AACzC,MAAM,MAAM,iBAAiB,GAAG,wCAAwC,GACtE,yCAAyC,CAAC;AAE5C,oFAAoF;AACpF,eAAO,MAAM,iBAAiB,WAAY,yBAAyB,KAAG,iBAKpE,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,WAAY,yBAAyB,KAAG,iBAYrE,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Proxmox.NotificationMatcher` — which notifications a PVE cluster sends to which targets.
|
|
3
|
+
*
|
|
4
|
+
* ★ THE HALF OF THE NOTIFICATION SYSTEM notification-target.ts LEAVES OPEN. A target is where a
|
|
5
|
+
* message CAN go; a matcher decides whether it DOES. A cluster ships one, `default-matcher`,
|
|
6
|
+
* routing everything to `mail-to-root`, and a target no matcher names receives nothing — so a
|
|
7
|
+
* declared webhook with no matcher is a delivery path that is never used, and looks finished.
|
|
8
|
+
*
|
|
9
|
+
* ★ ADOPTING THE BUILT-IN AS IT IS: declare what it holds and nothing more. MEASURED on PVE
|
|
10
|
+
* 9.2.11 (2026-09-22, `pvesh get /cluster/notifications/matchers/default-matcher`): mode `all`,
|
|
11
|
+
* one target `mail-to-root`, no rules, the comment "Route all notifications to mail-to-root".
|
|
12
|
+
* ```ts
|
|
13
|
+
* ProxmoxNotificationMatcher('default-matcher', {
|
|
14
|
+
* target: cluster, name: 'default-matcher', targets: ['mail-to-root'],
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
* That plans `noop` and deploys with no write (notification-matcher.test.ts). Leaving `comment`
|
|
18
|
+
* out leaves it alone; every rule field is compared — notification-matcher-form.ts says why.
|
|
19
|
+
*
|
|
20
|
+
* ★ WHAT THIS NEEDS, READ OFF THE CLUSTER'S OWN SCHEMA (generated/pve.ts source, 9.2.11):
|
|
21
|
+
* read GET matchers/{name} Mapping.Audit or Mapping.Modify on /mapping/notifications
|
|
22
|
+
* write POST, PUT, DELETE Mapping.Modify on /mapping/notifications
|
|
23
|
+
* The same pair as `Proxmox.NotificationTarget`, with that file's ⚠️ about granting it on `/`.
|
|
24
|
+
* ⛔ `Mapping.Use` IS NOT ENOUGH, though the COLLECTION GET accepts it: the per-object read this
|
|
25
|
+
* family makes lists only Audit and Modify (pve-manager API2/Cluster/Notifications.pm
|
|
26
|
+
* `get_matcher`, read at HEAD 2026-09-22 — corrected here; PR 95 said Use sufficed).
|
|
27
|
+
* ⛔ A MISSING READ PRIVILEGE LOOKS LIKE "ABSENT", not like a 403 — see the same file. For
|
|
28
|
+
* `default-matcher` that turns an adoption into a planned CREATE that PVE refuses as a
|
|
29
|
+
* duplicate: the noop below needs a read lease holding Mapping.Audit.
|
|
30
|
+
*/
|
|
31
|
+
import { Resource } from 'alchemy';
|
|
32
|
+
import * as Provider from 'alchemy/Provider';
|
|
33
|
+
import { type NotificationMatcherAttributes, type NotificationMatcherFields } from './notification-matcher-form.ts';
|
|
34
|
+
import { type PveRequirements, type WithTarget } from './resource.ts';
|
|
35
|
+
export interface NotificationMatcherProps extends NotificationMatcherFields, WithTarget {
|
|
36
|
+
}
|
|
37
|
+
export interface ProxmoxNotificationMatcher extends Resource<'Proxmox.NotificationMatcher', NotificationMatcherProps, NotificationMatcherAttributes, never, PveRequirements> {
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* ⚠️ NO `defaultRemovalPolicy: 'retain'`, for notification-target.ts's reason: a matcher holds no
|
|
41
|
+
* data, and a plan line is the only warning a delete gets. On `default-matcher` the delete
|
|
42
|
+
* REVERTS it to the shipped rule instead — see `origin` on the attributes.
|
|
43
|
+
*/
|
|
44
|
+
export declare const ProxmoxNotificationMatcher: import("alchemy").ResourceClass<ProxmoxNotificationMatcher>;
|
|
45
|
+
export declare const ProxmoxNotificationMatcherProvider: () => import("effect/Layer").Layer<Provider.Provider<ProxmoxNotificationMatcher>, never, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
46
|
+
//# sourceMappingURL=notification-matcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification-matcher.d.ts","sourceRoot":"","sources":["../../src/proxmox/notification-matcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAE7C,OAAO,EACL,KAAK,6BAA6B,EAClC,KAAK,yBAAyB,EAK/B,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAe,MAAM,eAAe,CAAC;AAEnF,MAAM,WAAW,wBAAyB,SAAQ,yBAAyB,EAAE,UAAU;CAAG;AAE1F,MAAM,WAAW,0BAA2B,SAAQ,QAAQ,CAC1D,6BAA6B,EAC7B,wBAAwB,EACxB,6BAA6B,EAC7B,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,6DAEtC,CAAC;AAWF,eAAO,MAAM,kCAAkC,gJAI5C,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Pbs.NotificationMatcher` — which Proxmox Backup Server notifications reach which targets.
|
|
3
|
+
*
|
|
4
|
+
* ★ THIS IS WHERE A FAILED VERIFY, SYNC OR GARBAGE COLLECTION BECOMES A PAGE. pbs-verify-job.ts
|
|
5
|
+
* explains why verification is the only thing that notices rotten chunks; a matcher is what
|
|
6
|
+
* turns what it notices into a message. Route `match-severity: ['error']` to a webhook target
|
|
7
|
+
* (pbs-notification-target.ts, docs/pbs-notifications.md) and a failure pages someone.
|
|
8
|
+
*
|
|
9
|
+
* ⛔ A DATASTORE IN `legacy-sendmail` MODE BYPASSES EVERY MATCHER. PBS reads the datastore's
|
|
10
|
+
* `notification-mode` before it builds anything (src/server/notifications/mod.rs
|
|
11
|
+
* `lookup_datastore_notify_settings`, read at HEAD 2026-09-22): `legacy-sendmail` mails the
|
|
12
|
+
* `notify-user` directly and never consults a matcher. The default is `notification-system`
|
|
13
|
+
* today (pbs-api-types `NotificationMode`), but a datastore created before PBS 3.2 may say
|
|
14
|
+
* `legacy-sendmail` explicitly. `Pbs.Datastore` deliberately does not declare the mode (see
|
|
15
|
+
* `notify` in pbs-datastore.ts), so check it by hand before trusting a matcher with a datastore:
|
|
16
|
+
* `proxmox-backup-manager datastore show <name>`.
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ THE EVENT `type` FOR A VERIFICATION JOB IS `verify`, NOT `verification`. PBS's own
|
|
19
|
+
* notifications chapter lists `verification` in its event table and `verify` in its example,
|
|
20
|
+
* and the source settles it: `send_verify_status` sets `("type", "verify")` (same file). A
|
|
21
|
+
* `match-field: ['exact:type=verification']` rule matches nothing, silently.
|
|
22
|
+
* Types at HEAD: `gc`, `prune`, `sync`, `verify`, `tape-backup`, `tape-load`,
|
|
23
|
+
* `package-updates`, `acme`, `thresholds`, `system-mail`. GC carries `datastore` and `hostname`
|
|
24
|
+
* but NO `job-id`; prune, sync and verify carry all three; the UI's "Test" carries no fields.
|
|
25
|
+
*
|
|
26
|
+
* ★ ADOPTING THE BUILT-IN AS IT IS. MEASURED on PBS 4.2 (2026-09-22, `proxmox-backup-manager
|
|
27
|
+
* notification matcher show default-matcher`): it routes everything EXCEPT successful prune
|
|
28
|
+
* jobs to `mail-to-root` — `invert-match` over `exact:type=prune` AND severity `info`:
|
|
29
|
+
* ```ts
|
|
30
|
+
* PbsNotificationMatcher('default-matcher', {
|
|
31
|
+
* target: pbs, name: 'default-matcher', targets: ['mail-to-root'],
|
|
32
|
+
* 'invert-match': true, 'match-field': ['exact:type=prune'], 'match-severity': ['info'],
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* ★ WHAT THIS NEEDS, READ OFF THE SERVER'S SCHEMA (generated/pbs.ts source):
|
|
37
|
+
* read GET Sys.Audit on /system/notifications
|
|
38
|
+
* write POST, PUT, DELETE Sys.Modify on /system/notifications
|
|
39
|
+
* ⚠️ NOT A DATASTORE PRIVILEGE. A PBS credential scoped to `/datastore/...` reads every matcher
|
|
40
|
+
* as absent — the "absent, then already exists" signature notification-target.ts describes.
|
|
41
|
+
*/
|
|
42
|
+
import { Resource } from 'alchemy';
|
|
43
|
+
import * as Provider from 'alchemy/Provider';
|
|
44
|
+
import { type NotificationMatcherAttributes, type NotificationMatcherFields } from './notification-matcher-form.ts';
|
|
45
|
+
import { type PveRequirements, type WithPbsTarget } from './resource.ts';
|
|
46
|
+
export interface PbsNotificationMatcherProps extends NotificationMatcherFields, WithPbsTarget {
|
|
47
|
+
}
|
|
48
|
+
export interface PbsNotificationMatcher extends Resource<'Pbs.NotificationMatcher', PbsNotificationMatcherProps, NotificationMatcherAttributes, never, PveRequirements> {
|
|
49
|
+
}
|
|
50
|
+
/** ⚠️ No `retain`, for notification-matcher.ts's reason; a built-in REVERTS on delete. */
|
|
51
|
+
export declare const PbsNotificationMatcher: import("alchemy").ResourceClass<PbsNotificationMatcher>;
|
|
52
|
+
export declare const PbsNotificationMatcherProvider: () => import("effect/Layer").Layer<Provider.Provider<PbsNotificationMatcher>, never, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
53
|
+
//# sourceMappingURL=pbs-notification-matcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pbs-notification-matcher.d.ts","sourceRoot":"","sources":["../../src/proxmox/pbs-notification-matcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAE7C,OAAO,EACL,KAAK,6BAA6B,EAClC,KAAK,yBAAyB,EAK/B,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,aAAa,EAAe,MAAM,eAAe,CAAC;AAEtF,MAAM,WAAW,2BAA4B,SAAQ,yBAAyB,EAAE,aAAa;CAAG;AAEhG,MAAM,WAAW,sBAAuB,SAAQ,QAAQ,CACtD,yBAAyB,EACzB,2BAA2B,EAC3B,6BAA6B,EAC7B,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,0FAA0F;AAC1F,eAAO,MAAM,sBAAsB,yDAA8D,CAAC;AAWlG,eAAO,MAAM,8BAA8B,4IAIxC,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When a PBS notification target has changed, and the forms that change it.
|
|
3
|
+
*
|
|
4
|
+
* ★ THREE QUESTIONS, NOT ONE, because the three kinds of field can be checked three ways:
|
|
5
|
+
* plain every field the server returns in full — compared by value, like any family here
|
|
6
|
+
* header names from the read, values by digest (pbs-notification-target-wire.ts)
|
|
7
|
+
* sealed secret values and the smtp password: the server returns names at best, so values
|
|
8
|
+
* are checked against the seal of what this provider last wrote (write-only.ts)
|
|
9
|
+
* Each answers `match`, `stale` or `unknown`; only `stale` writes. `unknown` is "the plan cannot
|
|
10
|
+
* see it" — a variable missing from the plan's environment, or a secret this provider never
|
|
11
|
+
* wrote (an adoption) — and it is PRESENCE-ONLY by design: a plan never invents drift it
|
|
12
|
+
* cannot see, and adoption stays free.
|
|
13
|
+
*
|
|
14
|
+
* ⛔ A WRITE SENDS ONLY THE GROUPS THAT ARE STALE. Omitting `header` or `secret` from a PUT keeps
|
|
15
|
+
* what the server holds (api/webhook.rs `update_endpoint`), so a changed comment is written
|
|
16
|
+
* without reading a single secret — and a deploy environment without the secrets can still
|
|
17
|
+
* repair the rest. A group that IS stale needs every value it holds; a missing one fails the
|
|
18
|
+
* deploy by variable name before anything is sent.
|
|
19
|
+
*/
|
|
20
|
+
import type { PveForm } from './client.ts';
|
|
21
|
+
import type { PbsNotificationTargetAttributes, PbsNotificationTargetProps } from './pbs-notification-target.ts';
|
|
22
|
+
import { type Environment } from './write-only.ts';
|
|
23
|
+
type Props = PbsNotificationTargetProps;
|
|
24
|
+
/**
|
|
25
|
+
* Why this declaration cannot be written, or nothing. Checked at PLAN, so a webhook without a
|
|
26
|
+
* URL fails `alchemy plan` naming the field rather than a deploy failing on a 400.
|
|
27
|
+
* ⚠️ "AT PLAN" MEANS `diff` FOR A TARGET WITH STATE AND THE ADOPTION PROBE (`read`) FOR ONE
|
|
28
|
+
* WITHOUT — pbs-notification-target-lifecycle.ts has why both are needed.
|
|
29
|
+
*/
|
|
30
|
+
export declare const refusals: (props: Props) => string[];
|
|
31
|
+
/** Each group's values, resolved from `env`, plus the variables that were missing. */
|
|
32
|
+
export declare const resolveGroups: (props: Props, env?: Environment) => {
|
|
33
|
+
header: {
|
|
34
|
+
missing: readonly string[];
|
|
35
|
+
values: {
|
|
36
|
+
[x: string]: string;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
sealed: {
|
|
40
|
+
readonly values: Readonly<Record<string, string>>;
|
|
41
|
+
readonly missing: readonly string[];
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
export type Groups = ReturnType<typeof resolveGroups>;
|
|
45
|
+
export type GroupState = 'match' | 'stale' | 'unknown';
|
|
46
|
+
/** Every field the server returns in full. `disable` is the one with no undeclared case. */
|
|
47
|
+
export declare const plainMatches: (live: PbsNotificationTargetAttributes, props: Props) => boolean;
|
|
48
|
+
export declare const headerState: (live: PbsNotificationTargetAttributes, props: Props, groups: Groups) => GroupState;
|
|
49
|
+
export declare const sealedState: (live: PbsNotificationTargetAttributes, props: Props, groups: Groups, sealed: string) => GroupState;
|
|
50
|
+
/** Which write-only groups a write carries. A create carries every declared one. */
|
|
51
|
+
export interface Carry {
|
|
52
|
+
readonly header: boolean;
|
|
53
|
+
readonly sealed: boolean;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The body of a create or an update — keys are the server's own, checked against generated/pbs.ts
|
|
57
|
+
* by pbs-notification-target.test.ts. ⛔ `disable` IS ALWAYS SENT, `0` included — the PVE family's
|
|
58
|
+
* reason: it is always compared, and "undeclared" and "false" must be one state, not a plan that
|
|
59
|
+
* asks for an update the PUT never performs.
|
|
60
|
+
*/
|
|
61
|
+
export declare const targetForm: (props: Props, groups: Groups, carry: Carry, mode: 'create' | 'update') => PveForm;
|
|
62
|
+
export {};
|
|
63
|
+
//# sourceMappingURL=pbs-notification-target-form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pbs-notification-target-form.d.ts","sourceRoot":"","sources":["../../src/proxmox/pbs-notification-target-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAG3C,OAAO,KAAK,EACV,+BAA+B,EAC/B,0BAA0B,EAC3B,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAE,KAAK,WAAW,EAAyC,MAAM,iBAAiB,CAAC;AAE1F,KAAK,KAAK,GAAG,0BAA0B,CAAC;AAkCxC;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,UAAW,KAAK,KAAG,MAAM,EAsB7C,CAAC;AAIF,sFAAsF;AACtF,eAAO,MAAM,aAAa,UAAW,KAAK,QAAO,WAAW;;;;;;;;;;;CAgB3D,CAAC;AAEF,MAAM,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC;AAKvD,4FAA4F;AAC5F,eAAO,MAAM,YAAY,SAAU,+BAA+B,SAAS,KAAK,KAAG,OAagB,CAAC;AAEpG,eAAO,MAAM,WAAW,SAChB,+BAA+B,SAC9B,KAAK,UACJ,MAAM,KACb,UAKF,CAAC;AAEF,eAAO,MAAM,WAAW,SAChB,+BAA+B,SAC9B,KAAK,UACJ,MAAM,UACN,MAAM,KACb,UAMF,CAAC;AAEF,oFAAoF;AACpF,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAmCD;;;;;GAKG;AACH,eAAO,MAAM,UAAU,UACd,KAAK,UACJ,MAAM,SACP,KAAK,QACN,QAAQ,GAAG,QAAQ,KACxB,OAkBF,CAAC"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Input } from 'alchemy/Input';
|
|
2
|
+
import * as Effect from 'effect/Effect';
|
|
3
|
+
import type { PbsNotificationTargetAttributes, PbsNotificationTargetProps } from './pbs-notification-target.ts';
|
|
4
|
+
type Props = PbsNotificationTargetProps;
|
|
5
|
+
type Attributes = PbsNotificationTargetAttributes;
|
|
6
|
+
export declare const handlers: {
|
|
7
|
+
/** ⛔ EMPTY: `mail-to-root` is on every host, and adoption stays an explicit act (resource.ts). */
|
|
8
|
+
list: () => Effect.Effect<never[], never, never>;
|
|
9
|
+
/**
|
|
10
|
+
* ⛔ WITH NO `output` THIS IS ALCHEMY'S ADOPTION PROBE, AND THE ONLY PLAN-TIME HOOK A NEW TARGET
|
|
11
|
+
* GETS — so the refusals run here too. Measured on beta.79 (Plan.ts, the `oldState ===
|
|
12
|
+
* undefined` branch): a declaration with no state row is `read` with `olds: news` and never
|
|
13
|
+
* `diff`ed, and Apply then commits its props as `creating` BEFORE `reconcile` runs. A refusal
|
|
14
|
+
* left to `reconcile` fired after the props — a literal token included — were in the store.
|
|
15
|
+
* ⚠️ ONLY WHEN THE PROPS ARE RESOLVED: Plan skips the probe while any prop is an unresolved
|
|
16
|
+
* Output, and then `reconcile` is the first check. docs/pbs-notifications.md says so.
|
|
17
|
+
* ★ A DEFECT (`Effect.die`), NOT A FAILURE, AND THAT IS WHAT KEEPS IT FROM WEDGING A STAGE. The
|
|
18
|
+
* same `read`-with-no-output shape is Alchemy's RECOVERY read of an interrupted create, at plan
|
|
19
|
+
* and before a delete (Plan.ts, Apply.ts), with the STORED props — and both wrap it in
|
|
20
|
+
* `catchDefect`, degrading to "nothing recovered". A typed failure would pass that catch and
|
|
21
|
+
* block the plan or the delete on props the user already fixed. The cold-start probe has no
|
|
22
|
+
* catch, so there the defect fails the plan, as intended. A refused create sent no request,
|
|
23
|
+
* so "nothing recovered" is also the truth.
|
|
24
|
+
*/
|
|
25
|
+
read: ({ olds, output }: {
|
|
26
|
+
olds: Props;
|
|
27
|
+
output: Attributes | undefined;
|
|
28
|
+
}) => Effect.Effect<{
|
|
29
|
+
sealed: string;
|
|
30
|
+
name: string;
|
|
31
|
+
type: import("./pbs-notification-target.ts").PbsNotificationTargetType;
|
|
32
|
+
origin: string;
|
|
33
|
+
comment: string;
|
|
34
|
+
disable: boolean;
|
|
35
|
+
mailto: string;
|
|
36
|
+
'mailto-user': string;
|
|
37
|
+
'from-address': string;
|
|
38
|
+
author: string;
|
|
39
|
+
server: string;
|
|
40
|
+
port: number;
|
|
41
|
+
mode: string;
|
|
42
|
+
username: string;
|
|
43
|
+
url: string;
|
|
44
|
+
method: string;
|
|
45
|
+
body: string;
|
|
46
|
+
header: string;
|
|
47
|
+
headerDigest: string;
|
|
48
|
+
secret: string;
|
|
49
|
+
} | undefined, never, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
50
|
+
/**
|
|
51
|
+
* ⚠️ `Input<Props>` and `isResolved`, resource.ts's reason: at plan time a prop can still be an
|
|
52
|
+
* unresolved Output, and comparing a placeholder to a live value reports a phantom update.
|
|
53
|
+
* ⛔ A NEW `type` UNDER THE SAME `name` REPLACES DELETE-FIRST: names are unique across every
|
|
54
|
+
* family on the host (api `ensure_unique`), so creating the new one first would be refused.
|
|
55
|
+
*/
|
|
56
|
+
diff: ({ news, olds, output, }: {
|
|
57
|
+
news: Input<Props>;
|
|
58
|
+
olds: Props;
|
|
59
|
+
output: Attributes | undefined;
|
|
60
|
+
}) => Effect.Effect<{
|
|
61
|
+
readonly action: 'replace';
|
|
62
|
+
readonly deleteFirst: boolean;
|
|
63
|
+
} | {
|
|
64
|
+
readonly deleteFirst?: never;
|
|
65
|
+
readonly action: 'update';
|
|
66
|
+
} | {
|
|
67
|
+
readonly deleteFirst?: never;
|
|
68
|
+
readonly action: 'noop';
|
|
69
|
+
} | undefined, never, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
70
|
+
reconcile: ({ news, output }: {
|
|
71
|
+
news: Props;
|
|
72
|
+
output: Attributes | undefined;
|
|
73
|
+
}) => Effect.Effect<{
|
|
74
|
+
sealed: string;
|
|
75
|
+
name: string;
|
|
76
|
+
type: import("./pbs-notification-target.ts").PbsNotificationTargetType;
|
|
77
|
+
origin: string;
|
|
78
|
+
comment: string;
|
|
79
|
+
disable: boolean;
|
|
80
|
+
mailto: string;
|
|
81
|
+
'mailto-user': string;
|
|
82
|
+
'from-address': string;
|
|
83
|
+
author: string;
|
|
84
|
+
server: string;
|
|
85
|
+
port: number;
|
|
86
|
+
mode: string;
|
|
87
|
+
username: string;
|
|
88
|
+
url: string;
|
|
89
|
+
method: string;
|
|
90
|
+
body: string;
|
|
91
|
+
header: string;
|
|
92
|
+
headerDigest: string;
|
|
93
|
+
secret: string;
|
|
94
|
+
}, Error, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
95
|
+
delete: ({ olds }: {
|
|
96
|
+
olds: Props;
|
|
97
|
+
}) => Effect.Effect<unknown, Error, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
98
|
+
};
|
|
99
|
+
export {};
|
|
100
|
+
//# sourceMappingURL=pbs-notification-target-lifecycle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pbs-notification-target-lifecycle.d.ts","sourceRoot":"","sources":["../../src/proxmox/pbs-notification-target-lifecycle.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,EACV,+BAA+B,EAC/B,0BAA0B,EAC3B,MAAM,8BAA8B,CAAC;AActC,KAAK,KAAK,GAAG,0BAA0B,CAAC;AACxC,KAAK,UAAU,GAAG,+BAA+B,CAAC;AA8ClD,eAAO,MAAM,QAAQ;IACnB,kGAAkG;IAClG,IAAI;IACJ;;;;;;;;;;;;;;;OAeG;IACH,IAAI,qBAAqB;QAAE,IAAI,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;;;;;;;;;;;;;;;;;;;;;;IAOxE;;;;;OAKG;IACH,IAAI,4BAID;QACD,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QACnB,IAAI,EAAE,KAAK,CAAC;QACZ,MAAM,EAAE,UAAU,GAAG,SAAS,CAAC;KAChC;yBAIsB,SAAS;;;;yBAIa,QAAQ;;;yBAElC,MAAM;;IAGzB,SAAS,qBAAqB;QAAE,IAAI,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;;;;;;;;;;;;;;;;;;;;;;IAyC7E,MAAM,aAAa;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE;CACnC,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { PbsNotificationTargetAttributes, PbsNotificationTargetProps } from './pbs-notification-target.ts';
|
|
2
|
+
export declare const base64: (value: string) => string;
|
|
3
|
+
/** ⚠️ Node's decoder skips characters it does not know rather than throwing, so this never throws. */
|
|
4
|
+
export declare const unbase64: (value: string) => string;
|
|
5
|
+
/**
|
|
6
|
+
* One `KeyAndBase64Val` property string — `name=<name>,value=<base64>` — split into its halves.
|
|
7
|
+
* ⚠️ KEYED BY NAME, NOT BY POSITION: the server serialises `name` first today, and a parser that
|
|
8
|
+
* relied on that would read a reordered item as a header called `value=…`.
|
|
9
|
+
* ★ A comma cannot occur inside either half: base64 has none, and `refusals` keeps names clean.
|
|
10
|
+
*/
|
|
11
|
+
export declare const keyAndValue: (item: string) => {
|
|
12
|
+
name: string;
|
|
13
|
+
value?: string;
|
|
14
|
+
};
|
|
15
|
+
/** The wire item for one header or secret. The value is base64 — the server decodes it to use it. */
|
|
16
|
+
export declare const wireItem: (name: string, value: string) => string;
|
|
17
|
+
/** Sorted, comma-joined names — presence, the only thing the store may know about the values. */
|
|
18
|
+
export declare const names: (keys: readonly string[]) => string;
|
|
19
|
+
/**
|
|
20
|
+
* ★ A FIXED SALT PER TARGET, SO AN UNCHANGED HEADER SET DIGESTS THE SAME ON EVERY READ. A random
|
|
21
|
+
* one would rewrite the state row on every plan. The server shows these values to its auditors
|
|
22
|
+
* anyway; what the salt and scrypt buy is that the STORE does not become a second, weaker copy.
|
|
23
|
+
*/
|
|
24
|
+
export declare const headerDigest: (target: string, pairs: Readonly<Record<string, string>>) => string;
|
|
25
|
+
/**
|
|
26
|
+
* Live JSON to attributes. `sealed` is left empty here: it is not the server's to report, and the
|
|
27
|
+
* handlers in pbs-notification-target.ts carry it forward from the previous state.
|
|
28
|
+
*/
|
|
29
|
+
export declare const targetAttributes: (live: Record<string, unknown>, props: PbsNotificationTargetProps) => PbsNotificationTargetAttributes;
|
|
30
|
+
//# sourceMappingURL=pbs-notification-target-wire.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pbs-notification-target-wire.d.ts","sourceRoot":"","sources":["../../src/proxmox/pbs-notification-target-wire.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EACV,+BAA+B,EAC/B,0BAA0B,EAC3B,MAAM,8BAA8B,CAAC;AAItC,eAAO,MAAM,MAAM,UAAW,MAAM,KAAG,MAAuD,CAAC;AAE/F,sGAAsG;AACtG,eAAO,MAAM,QAAQ,UAAW,MAAM,KAAG,MAAuD,CAAC;AAEjG;;;;;GAKG;AACH,eAAO,MAAM,WAAW,SAAU,MAAM,KAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAQxE,CAAC;AAEF,qGAAqG;AACrG,eAAO,MAAM,QAAQ,SAAU,MAAM,SAAS,MAAM,KAAG,MAChB,CAAC;AAKxC,iGAAiG;AACjG,eAAO,MAAM,KAAK,SAAU,SAAS,MAAM,EAAE,KAAG,MAIlC,CAAC;AAEf;;;;GAIG;AACH,eAAO,MAAM,YAAY,WAAY,MAAM,SAAS,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,KAAG,MAC3B,CAAC;AAE7D;;;GAGG;AACH,eAAO,MAAM,gBAAgB,SACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,SACtB,0BAA0B,KAChC,+BAkCF,CAAC"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Pbs.NotificationTarget` — where a Proxmox Backup Server sends a notification: a `webhook`, or
|
|
3
|
+
* mail through `sendmail` or `smtp`.
|
|
4
|
+
*
|
|
5
|
+
* ★ THE WEBHOOK IS THE POINT. A failed verify, sync or garbage collection reaches a human through a
|
|
6
|
+
* target a matcher names (pbs-notification-matcher.ts). Out of the box that is `mail-to-root`,
|
|
7
|
+
* one mailbox on one host; a webhook can post an Alertmanager alert instead, and then the failure
|
|
8
|
+
* pages like everything else. docs/pbs-alertmanager-body.md has the body template.
|
|
9
|
+
*
|
|
10
|
+
* ⛔ SECRETS ARE DECLARED BY THE NAME OF AN ENVIRONMENT VARIABLE, NEVER BY VALUE, and NO VALUE EVER
|
|
11
|
+
* REACHES STATE — not as a prop, not as an attribute. write-only.ts has the whole argument; the
|
|
12
|
+
* short form: Alchemy stores props and attributes unencrypted, so `secret`, `password` and any
|
|
13
|
+
* header holding a credential take `{ fromEnv: 'NAME' }`, read by the deploying process at call
|
|
14
|
+
* time. What the store keeps is presence (names) and digests, and the plan diffs on those.
|
|
15
|
+
* ★ THIS IS WHERE THE PBS FAMILY PARTS FROM notification-target.ts (PVE), which refuses every
|
|
16
|
+
* secret and so cannot create a signed webhook at all.
|
|
17
|
+
*
|
|
18
|
+
* ⛔ THE PER-OBJECT PATH CARRIES THE FAMILY: `config/notifications/endpoints/{type}/{name}`. `type`
|
|
19
|
+
* is identity, and changing it or `name` plans a REPLACE here — `diff` compares them with the
|
|
20
|
+
* recorded props. `/config/notifications/targets` is a read-only union view, as on PVE.
|
|
21
|
+
*
|
|
22
|
+
* ⚠️ A BUILT-IN TARGET REVERTS ON DELETE instead of disappearing (`origin: builtin`, measured on
|
|
23
|
+
* PBS 4.2 for `mail-to-root`), and PBS reports success either way — notification-target.ts has
|
|
24
|
+
* the consequence. Read `origin` before declaring a name you did not create.
|
|
25
|
+
*
|
|
26
|
+
* ★ WHAT THIS NEEDS (generated/pbs.ts source): Sys.Audit on /system/notifications to read,
|
|
27
|
+
* Sys.Modify there to write. A datastore-scoped credential reads every target as absent.
|
|
28
|
+
*/
|
|
29
|
+
import { Resource } from 'alchemy';
|
|
30
|
+
import * as Provider from 'alchemy/Provider';
|
|
31
|
+
import type { PveRequirements, WithPbsTarget } from './resource.ts';
|
|
32
|
+
import type { FromEnv } from './write-only.ts';
|
|
33
|
+
export type PbsNotificationTargetType = 'sendmail' | 'smtp' | 'webhook';
|
|
34
|
+
/**
|
|
35
|
+
* ⚠️ PBS'S OWN KEY NAMES, DASHES INCLUDED, for the reason backup-job.ts gives. A field outside its
|
|
36
|
+
* family (a `url` on a sendmail target) is refused at plan by name, not sent for a 400.
|
|
37
|
+
* ⚠️ AN UNDECLARED FIELD IS NEITHER SENT NOR COMPARED; `''` or `[]` (or `{}`) CLEARS ONE.
|
|
38
|
+
*/
|
|
39
|
+
export interface PbsNotificationTargetProps extends WithPbsTarget {
|
|
40
|
+
/** ⛔ IDENTITY: it picks the family and the URL. Changing it is a replace. */
|
|
41
|
+
type: PbsNotificationTargetType;
|
|
42
|
+
/** Unique across ALL families on the host: a webhook cannot be called `mail-to-root`. */
|
|
43
|
+
name: string;
|
|
44
|
+
comment?: string;
|
|
45
|
+
/** Configured but not delivering. Always sent and always compared. */
|
|
46
|
+
disable?: boolean;
|
|
47
|
+
/** sendmail, smtp: recipient addresses. Order is not meaning. */
|
|
48
|
+
mailto?: readonly string[];
|
|
49
|
+
/** sendmail, smtp: PBS users whose configured address receives the mail. */
|
|
50
|
+
'mailto-user'?: readonly string[];
|
|
51
|
+
/** smtp: required. sendmail: optional. */
|
|
52
|
+
'from-address'?: string;
|
|
53
|
+
author?: string;
|
|
54
|
+
/** smtp: the relay host. Required. */
|
|
55
|
+
server?: string;
|
|
56
|
+
/** smtp. Unset lets PBS pick from `mode`. */
|
|
57
|
+
port?: number;
|
|
58
|
+
/** smtp. PBS's default is `tls`. */
|
|
59
|
+
mode?: 'insecure' | 'starttls' | 'tls';
|
|
60
|
+
/** smtp. */
|
|
61
|
+
username?: string;
|
|
62
|
+
/** smtp: ⛔ WRITE-ONLY. The server never returns it; the plan compares its seal. */
|
|
63
|
+
password?: FromEnv;
|
|
64
|
+
/** webhook: required. Supports templating: `{{ secrets.<name> }}` keeps a token out of it. */
|
|
65
|
+
url?: string;
|
|
66
|
+
/** webhook: required. */
|
|
67
|
+
method?: 'get' | 'post' | 'put';
|
|
68
|
+
/**
|
|
69
|
+
* webhook: header name → template text, or `{ fromEnv }` for a value that is itself a secret.
|
|
70
|
+
* ⚠️ PLAIN VALUES ARE PROPS, AND PROPS ARE STORED: `Content-Type` is fine, a token is not. Put a
|
|
71
|
+
* credential in `secret` and write `'Bearer {{ secrets.token }}'` here instead.
|
|
72
|
+
*/
|
|
73
|
+
header?: Readonly<Record<string, FromEnv | string>>;
|
|
74
|
+
/**
|
|
75
|
+
* webhook: the body TEMPLATE, as text. ★ Declared DECODED — the wire is base64, and this family
|
|
76
|
+
* encodes on write and decodes on read, so a plan shows a readable diff.
|
|
77
|
+
*/
|
|
78
|
+
body?: string;
|
|
79
|
+
/** webhook: ⛔ WRITE-ONLY. Name → variable. Referenced as `{{ secrets.<name> }}`. */
|
|
80
|
+
secret?: Readonly<Record<string, FromEnv>>;
|
|
81
|
+
}
|
|
82
|
+
/** ⛔ NO VALUE OF A SECRET, PASSWORD OR HEADER — names and digests only. */
|
|
83
|
+
export interface PbsNotificationTargetAttributes {
|
|
84
|
+
name: string;
|
|
85
|
+
type: PbsNotificationTargetType;
|
|
86
|
+
/** `builtin`, `modified-builtin`, `user-created` — reported, never compared. */
|
|
87
|
+
origin: string;
|
|
88
|
+
comment: string;
|
|
89
|
+
disable: boolean;
|
|
90
|
+
mailto: string;
|
|
91
|
+
'mailto-user': string;
|
|
92
|
+
'from-address': string;
|
|
93
|
+
author: string;
|
|
94
|
+
server: string;
|
|
95
|
+
/** 0 means PBS holds no port. */
|
|
96
|
+
port: number;
|
|
97
|
+
mode: string;
|
|
98
|
+
username: string;
|
|
99
|
+
url: string;
|
|
100
|
+
method: string;
|
|
101
|
+
/** The decoded body template. */
|
|
102
|
+
body: string;
|
|
103
|
+
/** Header NAMES, sorted — presence. */
|
|
104
|
+
header: string;
|
|
105
|
+
/** scrypt digest of the live header pairs, fixed salt per target. */
|
|
106
|
+
headerDigest: string;
|
|
107
|
+
/** Secret NAMES, sorted — all PBS will say. */
|
|
108
|
+
secret: string;
|
|
109
|
+
/**
|
|
110
|
+
* Seal of the secret values and password THIS PROVIDER LAST WROTE; `''` if it never wrote them
|
|
111
|
+
* (an adoption). ⚠️ `''` makes those values presence-only until the first write that carries them.
|
|
112
|
+
*/
|
|
113
|
+
sealed: string;
|
|
114
|
+
}
|
|
115
|
+
export interface PbsNotificationTarget extends Resource<'Pbs.NotificationTarget', PbsNotificationTargetProps, PbsNotificationTargetAttributes, never, PveRequirements> {
|
|
116
|
+
}
|
|
117
|
+
export declare const PbsNotificationTarget: import("alchemy").ResourceClass<PbsNotificationTarget>;
|
|
118
|
+
/**
|
|
119
|
+
* ⛔ `Provider.of` STAYS HERE, for resource.ts's reason: it is the one place the handlers are
|
|
120
|
+
* checked against the concrete resource types, so the lifecycle file cannot drift from them.
|
|
121
|
+
*/
|
|
122
|
+
export declare const PbsNotificationTargetProvider: () => import("effect/Layer").Layer<Provider.Provider<PbsNotificationTarget>, never, import("effect/unstable/http/HttpClient").HttpClient>;
|
|
123
|
+
//# sourceMappingURL=pbs-notification-target.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pbs-notification-target.d.ts","sourceRoot":"","sources":["../../src/proxmox/pbs-notification-target.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAG7C,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAE/C,MAAM,MAAM,yBAAyB,GAAG,UAAU,GAAG,MAAM,GAAG,SAAS,CAAC;AAExE;;;;GAIG;AACH,MAAM,WAAW,0BAA2B,SAAQ,aAAa;IAC/D,6EAA6E;IAC7E,IAAI,EAAE,yBAAyB,CAAC;IAChC,yFAAyF;IACzF,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iEAAiE;IACjE,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,4EAA4E;IAC5E,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,0CAA0C;IAC1C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sCAAsC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oCAAoC;IACpC,IAAI,CAAC,EAAE,UAAU,GAAG,UAAU,GAAG,KAAK,CAAC;IACvC,YAAY;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mFAAmF;IACnF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,8FAA8F;IAC9F,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,yBAAyB;IACzB,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,CAAC;IAChC;;;;OAIG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC;IACpD;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oFAAoF;IACpF,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC5C;AAED,2EAA2E;AAC3E,MAAM,WAAW,+BAA+B;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,yBAAyB,CAAC;IAChC,gFAAgF;IAChF,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,YAAY,EAAE,MAAM,CAAC;IACrB,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,qBAAsB,SAAQ,QAAQ,CACrD,wBAAwB,EACxB,0BAA0B,EAC1B,+BAA+B,EAC/B,KAAK,EACL,eAAe,CAChB;CAAG;AAEJ,eAAO,MAAM,qBAAqB,wDAA4D,CAAC;AAE/F;;;GAGG;AACH,eAAO,MAAM,6BAA6B,2IAIvC,CAAC"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Input } from 'alchemy/Input';
|
|
2
2
|
import * as Effect from 'effect/Effect';
|
|
3
3
|
import type * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
4
|
+
import { type PveForm } from './client.ts';
|
|
4
5
|
import type { ApiTarget, PbsTarget, PveRole, PveTarget } from './credentials.ts';
|
|
5
6
|
/**
|
|
6
7
|
* ★ WHY SEVERAL FAMILIES HERE DECLARE `defaultRemovalPolicy: 'retain'`, WRITTEN ONCE.
|
|
@@ -58,15 +59,15 @@ export type PveSpec<Props extends WithApiTarget, Attributes> = {
|
|
|
58
59
|
readonly collection: (props: Props) => string;
|
|
59
60
|
/** Live JSON to attributes. Returning undefined means "this is not really there". */
|
|
60
61
|
readonly attributes: (live: Record<string, unknown>, props: Props) => Attributes | undefined;
|
|
61
|
-
/** The form PVE wants on create. ⚠️ PVE takes form encoding, not JSON. */
|
|
62
|
-
readonly createForm: (props: Props) =>
|
|
62
|
+
/** The form PVE wants on create. ⚠️ PVE takes form encoding, not JSON. Arrays: client.ts. */
|
|
63
|
+
readonly createForm: (props: Props) => PveForm;
|
|
63
64
|
/**
|
|
64
65
|
* The form for an update, or undefined when the object has no mutable fields.
|
|
65
66
|
*
|
|
66
67
|
* ⚠️ SOME PVE OBJECTS CANNOT BE UPDATED AT ALL. Returning undefined makes a changed prop a
|
|
67
68
|
* REPLACE rather than a silent no-op, which is the honest answer for an immutable object.
|
|
68
69
|
*/
|
|
69
|
-
readonly updateForm?: (props: Props) =>
|
|
70
|
+
readonly updateForm?: (props: Props) => PveForm;
|
|
70
71
|
/** True when live already matches props. Decides noop vs update. */
|
|
71
72
|
readonly matches: (attributes: Attributes, props: Props) => boolean;
|
|
72
73
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../../src/proxmox/resource.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,UAAU,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../../src/proxmox/resource.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,UAAU,MAAM,iCAAiC,CAAC;AACnE,OAAO,EAAE,KAAK,OAAO,EAAO,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGjF;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;;;GAKG;AACH;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,UAAU,CAAC;AAEpD,MAAM,MAAM,UAAU,GAAG;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,CAAC;AAC/C,MAAM,MAAM,aAAa,GAAG;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,CAAC;AAClD,MAAM,MAAM,aAAa,GAAG;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,CAAC;AAElD,MAAM,MAAM,OAAO,CAAC,KAAK,SAAS,aAAa,EAAE,UAAU,IAAI;IAC7D,2FAA2F;IAC3F,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC;IACxC,+DAA+D;IAC/D,QAAQ,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC;IAC9C,qFAAqF;IACrF,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,KAAK,UAAU,GAAG,SAAS,CAAC;IAC7F,6FAA6F;IAC7F,QAAQ,CAAC,UAAU,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;IAC/C;;;;;OAKG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;IAChD,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;IACpE;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,KAAK,SAAS,aAAa,EAAE,UAAU,QAC7D,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC;kBAGX,KAAK;iBAiBX,KAAK,CAAC,KAAK,CAAC,UAAU,UAAU,GAAG,SAAS;yBASZ,QAAQ;;yBACF,MAAM;;yBAGtC,SAAS;;sBAIV,KAAK;IA4DvB;;;;OAIG;oBACa,KAAK;CAExB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,SAAS,aAAa,EAAE,UAAU,SAAS,MAAM,QAC1E,OAAO,CAAC,KAAK,EAAE,UAAU,CAAC;IAI9B,IAAI;IACJ,IAAI,aAAa;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE;IAChC,IAAI,qBAAqB;QAAE,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;QAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;KAAE;yBArHlC,QAAQ;;yBACF,MAAM;;yBAGtC,SAAS;;IAmH5B,SAAS,aAAa;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE;IACrC,MAAM,aAAa;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE;CAErC,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** A write-only value, by the NAME of the environment variable holding it. Never the value. */
|
|
2
|
+
export interface FromEnv {
|
|
3
|
+
readonly fromEnv: string;
|
|
4
|
+
}
|
|
5
|
+
/** Where a value is looked up. Defaults to `process.env`, read at call time, never at load. */
|
|
6
|
+
export type Environment = Readonly<Record<string, string | undefined>>;
|
|
7
|
+
/**
|
|
8
|
+
* Each `name → FromEnv` resolved, or the variable names that could not be.
|
|
9
|
+
*
|
|
10
|
+
* ⚠️ AN EMPTY VARIABLE IS A MISSING ONE. A secret renderer that failed writes `NAME=` rather than
|
|
11
|
+
* omitting the line, and sending `''` would store an empty secret on the server — a webhook that
|
|
12
|
+
* authenticates with nothing, reported as a successful deploy.
|
|
13
|
+
* ⚠️ THE VALUE IS NOT TRIMMED. A token with a trailing newline is a broken header either way, and
|
|
14
|
+
* trimming here would make the seal disagree with a value written by any other tool.
|
|
15
|
+
*/
|
|
16
|
+
export declare const resolveAll: (refs: Readonly<Record<string, FromEnv>>, env?: Environment) => {
|
|
17
|
+
readonly values: Readonly<Record<string, string>>;
|
|
18
|
+
readonly missing: readonly string[];
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* `scrypt:<salt>:<digest>`, or `''` when there is nothing to seal.
|
|
22
|
+
*
|
|
23
|
+
* ★ A RANDOM SALT BY DEFAULT, FOR WHAT THIS PROVIDER WROTE. Pass a fixed one only for a value the
|
|
24
|
+
* server returns anyway (a header), where a fresh salt per read would churn the state row on
|
|
25
|
+
* every plan without protecting anything the server does not already show its auditors.
|
|
26
|
+
*/
|
|
27
|
+
export declare const seal: (values: Readonly<Record<string, string>>, salt?: string) => string;
|
|
28
|
+
/**
|
|
29
|
+
* Whether `values` are the ones `sealed` was made from.
|
|
30
|
+
*
|
|
31
|
+
* ⚠️ A MALFORMED OR EMPTY SEAL ANSWERS FALSE, never throws: callers decide what "no evidence"
|
|
32
|
+
* means, and every caller here treats it as "cannot tell" rather than as drift.
|
|
33
|
+
*/
|
|
34
|
+
export declare const sealMatches: (sealed: string, values: Readonly<Record<string, string>>) => boolean;
|
|
35
|
+
//# sourceMappingURL=write-only.d.ts.map
|