@camstack/addon-mqtt-broker 1.2.153 → 1.2.154

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.
@@ -6028,7 +6028,7 @@ var ZodIssueCode = {
6028
6028
  var ZodFirstPartyTypeKind;
6029
6029
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6030
6030
  //#endregion
6031
- //#region ../types/dist/sleep-CmjWurK5.mjs
6031
+ //#region ../types/dist/sleep-CdKka2ri.mjs
6032
6032
  /**
6033
6033
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6034
6034
  * window to float samples (D455).
@@ -9559,30 +9559,94 @@ function normalizeAddonInitResult(result) {
9559
9559
  if (Array.isArray(result)) return { providers: result };
9560
9560
  return result;
9561
9561
  }
9562
+ object({
9563
+ field: string().min(1),
9564
+ on: literal("advance")
9565
+ });
9562
9566
  /**
9563
- * The share-view scope kinds a share-link token can carry. The hub mints
9564
- * and validates them (`ShareViewGrantSchema` builds its
9565
- * enum from this list); a data plane names the kinds it ACCEPTS in `share`.
9567
+ * Output schema shared by the contribution + live methods.
9568
+ *
9569
+ * Mirrors the `ConfigUISchemaWithValues` shape (sections[] + optional
9570
+ * tabs[]) without importing from `../interfaces/config-ui.js` — a
9571
+ * concrete-but-lenient Zod object keeps tRPC output inference happy
9572
+ * (using `z.unknown()` here collapses unrelated router branches to
9573
+ * `unknown` when the generator re-inlines the huge AppRouter type).
9574
+ *
9575
+ * `.passthrough()` on sections/fields accepts whatever FormBuilder
9576
+ * extensions the caller adds (showWhen, displayScale, …) without
9577
+ * rebuilding every time a new field kind is introduced.
9566
9578
  */
9567
- var SHARE_VIEW_KINDS = ["grid-view", "events-view"];
9568
- /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
9569
- var PeerBytesTicketSchema = object({
9570
- /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
9571
- url: string().min(1),
9572
- /**
9573
- * The HOST node this URL means something on — the hub or a named agent,
9574
- * never a runner. {@link AddonPeerBytes.open} compares it to its own and
9575
- * refuses `cross-node` by name when they differ, without dialling.
9576
- */
9577
- hostNodeId: string().min(1),
9578
- expiresAtMs: number().int().nonnegative(),
9579
- /**
9580
- * What the producer DECLARED the body to be, when it knows — `null` when it
9581
- * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
9582
- * must be able to tell "the producer did not say" from "the body is empty".
9583
- */
9584
- declaredBytes: number().int().nonnegative().nullable()
9579
+ var ContributionSectionSchema = object({
9580
+ id: string(),
9581
+ title: string(),
9582
+ description: string().optional(),
9583
+ style: _enum(["card", "accordion"]).optional(),
9584
+ defaultCollapsed: boolean().optional(),
9585
+ columns: union([
9586
+ literal(1),
9587
+ literal(2),
9588
+ literal(3),
9589
+ literal(4)
9590
+ ]).optional(),
9591
+ tab: string().optional(),
9592
+ location: _enum([
9593
+ "settings",
9594
+ "top-tab",
9595
+ "hero"
9596
+ ]).optional(),
9597
+ order: number().optional(),
9598
+ fields: array(any())
9585
9599
  });
9600
+ object({
9601
+ tabs: array(object({
9602
+ id: string(),
9603
+ label: string(),
9604
+ icon: string(),
9605
+ order: number().optional()
9606
+ })).optional(),
9607
+ sections: array(ContributionSectionSchema)
9608
+ }).nullable();
9609
+ object({ deviceId: number() }), object({ deviceId: number() }), object({
9610
+ deviceId: number(),
9611
+ patch: record(string(), unknown())
9612
+ }), object({ success: literal(true) });
9613
+ object({ deviceId: number() }), unknown().nullable();
9614
+ function method(input, output, options) {
9615
+ return {
9616
+ input,
9617
+ output,
9618
+ kind: options?.kind ?? "query",
9619
+ ...options?.auth !== void 0 ? { auth: options.auth } : {},
9620
+ ...options?.access !== void 0 ? { access: options.access } : {},
9621
+ ...options?.role !== void 0 ? { role: options.role } : {},
9622
+ ...options?.caller !== void 0 ? { caller: options.caller } : {},
9623
+ timeoutMs: options?.timeoutMs
9624
+ };
9625
+ }
9626
+ /**
9627
+ * A wrapper/system-only method: served exclusively by the cap's system-level
9628
+ * provider (`InferProvider`), and OPTIONAL on `InferNativeProvider` so per-device
9629
+ * driver natives don't stub out a wrapper concern (e.g. a cross-device cache
9630
+ * overview). The `systemOnly: true` literal is what `InferNativeProvider` keys on.
9631
+ */
9632
+ function systemMethod(input, output, options) {
9633
+ return {
9634
+ ...method(input, output, options),
9635
+ systemOnly: true
9636
+ };
9637
+ }
9638
+ /**
9639
+ * A method a SOURCE of a collection cap may legitimately not serve — OPTIONAL
9640
+ * on `InferProvider`. The `providerOptional: true` literal is what
9641
+ * `InferProvider` keys on; see {@link CapabilityMethodSchema.providerOptional}
9642
+ * for when this is the honest answer and when it is a soft stub.
9643
+ */
9644
+ function optionalMethod(input, output, options) {
9645
+ return {
9646
+ ...method(input, output, options),
9647
+ providerOptional: true
9648
+ };
9649
+ }
9586
9650
  function defOf(schema) {
9587
9651
  const zodBag = Reflect.get(schema, "_zod");
9588
9652
  if (typeof zodBag !== "object" || zodBag === null) return {};
@@ -9839,94 +9903,30 @@ function defineEntryKind(name, spec) {
9839
9903
  if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
9840
9904
  return defined;
9841
9905
  }
9842
- object({
9843
- field: string().min(1),
9844
- on: literal("advance")
9845
- });
9846
9906
  /**
9847
- * Output schema shared by the contribution + live methods.
9848
- *
9849
- * Mirrors the `ConfigUISchemaWithValues` shape (sections[] + optional
9850
- * tabs[]) without importing from `../interfaces/config-ui.js` — a
9851
- * concrete-but-lenient Zod object keeps tRPC output inference happy
9852
- * (using `z.unknown()` here collapses unrelated router branches to
9853
- * `unknown` when the generator re-inlines the huge AppRouter type).
9854
- *
9855
- * `.passthrough()` on sections/fields accepts whatever FormBuilder
9856
- * extensions the caller adds (showWhen, displayScale, …) without
9857
- * rebuilding every time a new field kind is introduced.
9907
+ * The share-view scope kinds a share-link token can carry. The hub mints
9908
+ * and validates them (`ShareViewGrantSchema` builds its
9909
+ * enum from this list); a data plane names the kinds it ACCEPTS in `share`.
9858
9910
  */
9859
- var ContributionSectionSchema = object({
9860
- id: string(),
9861
- title: string(),
9862
- description: string().optional(),
9863
- style: _enum(["card", "accordion"]).optional(),
9864
- defaultCollapsed: boolean().optional(),
9865
- columns: union([
9866
- literal(1),
9867
- literal(2),
9868
- literal(3),
9869
- literal(4)
9870
- ]).optional(),
9871
- tab: string().optional(),
9872
- location: _enum([
9873
- "settings",
9874
- "top-tab",
9875
- "hero"
9876
- ]).optional(),
9877
- order: number().optional(),
9878
- fields: array(any())
9911
+ var SHARE_VIEW_KINDS = ["grid-view", "events-view"];
9912
+ /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
9913
+ var PeerBytesTicketSchema = object({
9914
+ /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
9915
+ url: string().min(1),
9916
+ /**
9917
+ * The HOST node this URL means something on — the hub or a named agent,
9918
+ * never a runner. {@link AddonPeerBytes.open} compares it to its own and
9919
+ * refuses `cross-node` by name when they differ, without dialling.
9920
+ */
9921
+ hostNodeId: string().min(1),
9922
+ expiresAtMs: number().int().nonnegative(),
9923
+ /**
9924
+ * What the producer DECLARED the body to be, when it knows — `null` when it
9925
+ * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
9926
+ * must be able to tell "the producer did not say" from "the body is empty".
9927
+ */
9928
+ declaredBytes: number().int().nonnegative().nullable()
9879
9929
  });
9880
- object({
9881
- tabs: array(object({
9882
- id: string(),
9883
- label: string(),
9884
- icon: string(),
9885
- order: number().optional()
9886
- })).optional(),
9887
- sections: array(ContributionSectionSchema)
9888
- }).nullable();
9889
- object({ deviceId: number() }), object({ deviceId: number() }), object({
9890
- deviceId: number(),
9891
- patch: record(string(), unknown())
9892
- }), object({ success: literal(true) });
9893
- object({ deviceId: number() }), unknown().nullable();
9894
- function method(input, output, options) {
9895
- return {
9896
- input,
9897
- output,
9898
- kind: options?.kind ?? "query",
9899
- ...options?.auth !== void 0 ? { auth: options.auth } : {},
9900
- ...options?.access !== void 0 ? { access: options.access } : {},
9901
- ...options?.role !== void 0 ? { role: options.role } : {},
9902
- ...options?.caller !== void 0 ? { caller: options.caller } : {},
9903
- timeoutMs: options?.timeoutMs
9904
- };
9905
- }
9906
- /**
9907
- * A wrapper/system-only method: served exclusively by the cap's system-level
9908
- * provider (`InferProvider`), and OPTIONAL on `InferNativeProvider` so per-device
9909
- * driver natives don't stub out a wrapper concern (e.g. a cross-device cache
9910
- * overview). The `systemOnly: true` literal is what `InferNativeProvider` keys on.
9911
- */
9912
- function systemMethod(input, output, options) {
9913
- return {
9914
- ...method(input, output, options),
9915
- systemOnly: true
9916
- };
9917
- }
9918
- /**
9919
- * A method a SOURCE of a collection cap may legitimately not serve — OPTIONAL
9920
- * on `InferProvider`. The `providerOptional: true` literal is what
9921
- * `InferProvider` keys on; see {@link CapabilityMethodSchema.providerOptional}
9922
- * for when this is the honest answer and when it is a soft stub.
9923
- */
9924
- function optionalMethod(input, output, options) {
9925
- return {
9926
- ...method(input, output, options),
9927
- providerOptional: true
9928
- };
9929
- }
9930
9930
  var READING_QUANTITIES = [
9931
9931
  "temperature",
9932
9932
  "pressure",
@@ -10505,7 +10505,6 @@ var CAP_NAMES = Object.freeze([
10505
10505
  "climate-control",
10506
10506
  "color",
10507
10507
  "composer",
10508
- "connection-test",
10509
10508
  "connectivity",
10510
10509
  "consumables",
10511
10510
  "contact",
@@ -12525,8 +12524,7 @@ Object.freeze({
12525
12524
  "offerArtifact": 3e5,
12526
12525
  "readResult": 3e5,
12527
12526
  "submitJob": 18e5,
12528
- "uploadDataset": 72e5,
12529
- "validateCredentials": 3e4
12527
+ "uploadDataset": 72e5
12530
12528
  }
12531
12529
  });
12532
12530
  object({
@@ -13881,6 +13879,432 @@ var IntegrationInstanceRefSchema = object({
13881
13879
  ]),
13882
13880
  candidateStableId: string().optional()
13883
13881
  });
13882
+ /**
13883
+ * notification-output — canonical, capability-gated notification delivery.
13884
+ *
13885
+ * Apprise-derived model (see
13886
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
13887
+ * callers emit ONE canonical `Notification`; each provider declares a
13888
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
13889
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
13890
+ * message to what the kind supports — callers never special-case a service.
13891
+ *
13892
+ * DESIGN DECISIONS (locked):
13893
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
13894
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
13895
+ * cap. Rationale: the admin UI needs one uniform surface across the
13896
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
13897
+ * alternative would fork the UI per addon and cannot host the
13898
+ * discovery→adopt flow.
13899
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
13900
+ * the generated cap-mount auto-`concatCollection`-fans them across every
13901
+ * registered provider (notifiers addon + HA addon) so one catalog is
13902
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
13903
+ * `addonId` the generated collection router extracts from the call input.
13904
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
13905
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
13906
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
13907
+ * base64 fallback needed.
13908
+ *
13909
+ * TODO (deferred, closed-set change — separate decision): add
13910
+ * `providerKind: 'notify'` so notification providers surface on the unified
13911
+ * admin "Integrations" page.
13912
+ */
13913
+ /**
13914
+ * Zentik-derived typed-media enum — the superset across every kind. Each
13915
+ * adapter picks what it supports and the degrade engine filters the rest.
13916
+ */
13917
+ var AttachmentMediaTypeSchema = _enum([
13918
+ "image",
13919
+ "video",
13920
+ "gif",
13921
+ "audio",
13922
+ "icon"
13923
+ ]);
13924
+ /**
13925
+ * A single attachment. Exactly one of `url` (remote source, most adapters
13926
+ * prefer this) or `bytes` (inline source; required for Pushover-style
13927
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
13928
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
13929
+ */
13930
+ var AttachmentSchema = object({
13931
+ mediaType: AttachmentMediaTypeSchema,
13932
+ url: string().optional(),
13933
+ bytes: _instanceof(Uint8Array).optional(),
13934
+ mime: string().optional(),
13935
+ name: string().optional()
13936
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
13937
+ var NotificationFormatSchema = _enum([
13938
+ "text",
13939
+ "markdown",
13940
+ "html"
13941
+ ]);
13942
+ /**
13943
+ * The CLOSED icon vocabulary an action button may use.
13944
+ *
13945
+ * A closed set, not a free string, and that is the whole point: an arbitrary
13946
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
13947
+ * notices — the same class of gap as a zone vocabulary nothing produced
13948
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
13949
+ * declares `actionIcons: false` and the degrade engine strips the field.
13950
+ *
13951
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
13952
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
13953
+ * differently.
13954
+ *
13955
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
13956
+ *
13957
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
13958
+ * the whole set onto its own renderer's vocabulary through a
13959
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
13960
+ * fallback, so adding a member here fails every adapter's build until someone
13961
+ * decides its glyph, which is the only place that decision can be made
13962
+ * honestly.
13963
+ *
13964
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
13965
+ * `disarm` straight through; iOS feeds that string to
13966
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
13967
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
13968
+ * mapping, and "the field is documented" is not "the value renders".
13969
+ *
13970
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
13971
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
13972
+ * so an addon that emits a token the running hub does not know does not lose an
13973
+ * icon — its whole `send` fails Zod validation and the notification never
13974
+ * arrives. Never emit a new token from an addon before the train carrying it.
13975
+ */
13976
+ var NotificationActionIconSchema = _enum([
13977
+ "acknowledge",
13978
+ "dismiss",
13979
+ "silence",
13980
+ "snooze",
13981
+ "view",
13982
+ "play",
13983
+ "open",
13984
+ "close",
13985
+ "lock",
13986
+ "unlock",
13987
+ "arm",
13988
+ "arm-home",
13989
+ "arm-away",
13990
+ "arm-night",
13991
+ "disarm",
13992
+ "light",
13993
+ "alert",
13994
+ "camera"
13995
+ ]);
13996
+ /** A single tap-through action button. */
13997
+ var NotificationActionSchema = object({
13998
+ id: string(),
13999
+ label: string(),
14000
+ url: string().optional(),
14001
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
14002
+ icon: NotificationActionIconSchema.optional(),
14003
+ /**
14004
+ * Renders in a warning style where the notifier supports it.
14005
+ *
14006
+ * A HINT, never a gate. The callback's authority is its token and nothing
14007
+ * else — see `notification-center/action-token.ts` for what that does and
14008
+ * does not buy.
14009
+ */
14010
+ destructive: boolean().optional(),
14011
+ /**
14012
+ * How the tap should REACH the url.
14013
+ *
14014
+ * `navigate` (absent, and every button authored before this field) opens it:
14015
+ * the phone leaves the notification and shows whatever the callback returns.
14016
+ * That is right for a button whose answer the operator wants to read.
14017
+ *
14018
+ * `background` fires it as a POST and stays put. It exists for the buttons
14019
+ * whose whole point is not to interrupt — "silence this for 30 minutes" is
14020
+ * an answer to the notification, and being thrown into a browser tab to
14021
+ * confirm it costs more attention than the notification did. A backend that
14022
+ * cannot do a background call renders it as an ordinary link (the adapters
14023
+ * fall back rather than dropping the button), so this is a preference, never
14024
+ * a requirement.
14025
+ */
14026
+ mode: _enum(["navigate", "background"]).optional()
14027
+ });
14028
+ /** A delivery parameter value: what a notifier declared it takes. */
14029
+ var DeliveryParamValueSchema = union([
14030
+ string(),
14031
+ number(),
14032
+ boolean()
14033
+ ]);
14034
+ /**
14035
+ * One extra delivery parameter a notifier KIND declares (D938). A rule that
14036
+ * addresses a target of that kind may set it; a kind that declares nothing is
14037
+ * never handed one. The admin UI derives its control from this descriptor —
14038
+ * no editor knows a service by name.
14039
+ *
14040
+ * Optional fields are `.optional()`, never `.default()` (a Zod default does not
14041
+ * run on the addon cap path).
14042
+ */
14043
+ var DeliveryParamSchema = object({
14044
+ key: string().min(1),
14045
+ label: string(),
14046
+ /** i18n key of the label, resolved by the declaring addon's namespace. */
14047
+ labelKey: string().optional(),
14048
+ type: _enum([
14049
+ "enum",
14050
+ "number",
14051
+ "boolean",
14052
+ "string"
14053
+ ]),
14054
+ /** `enum` only: the closed set of accepted values. */
14055
+ options: array(object({
14056
+ value: string(),
14057
+ label: string()
14058
+ })).optional(),
14059
+ /** `number` only. */
14060
+ min: number().optional(),
14061
+ max: number().optional(),
14062
+ /** What the notifier does when the rule sets nothing; shown as the placeholder. */
14063
+ default: DeliveryParamValueSchema.optional()
14064
+ });
14065
+ /**
14066
+ * The canonical notification. `body` is the only hard field (Apprise model).
14067
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
14068
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
14069
+ * the adapter maps this ordinal onto its native level. `level?` is an
14070
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
14071
+ * `priority` for that one target.
14072
+ */
14073
+ var NotificationSchema = object({
14074
+ body: string(),
14075
+ title: string().optional(),
14076
+ format: NotificationFormatSchema.default("text"),
14077
+ priority: number().int().min(1).max(5).default(3),
14078
+ level: string().optional(),
14079
+ attachments: array(AttachmentSchema).optional(),
14080
+ clickUrl: string().optional(),
14081
+ actions: array(NotificationActionSchema).optional(),
14082
+ sound: string().optional(),
14083
+ ttl: number().optional(),
14084
+ tag: string().optional(),
14085
+ deviceId: number().optional(),
14086
+ eventId: string().optional(),
14087
+ metadata: record(string(), unknown()).optional(),
14088
+ /**
14089
+ * Values for the parameters the target's kind DECLARES in
14090
+ * {@link TargetKindSchema} `deliveryParams`. Already validated against the
14091
+ * declaration by the sender; a notifier reads only the keys it declared.
14092
+ */
14093
+ delivery: record(string(), DeliveryParamValueSchema).optional()
14094
+ });
14095
+ /** One declared native severity/priority level for a kind. */
14096
+ var TargetKindLevelSchema = object({
14097
+ id: string(),
14098
+ label: string(),
14099
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
14100
+ ordinal: number().int().min(1).max(5).nullable(),
14101
+ flags: object({
14102
+ critical: boolean().optional(),
14103
+ silent: boolean().optional(),
14104
+ noPush: boolean().optional()
14105
+ }).optional(),
14106
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
14107
+ requires: array(string()).optional(),
14108
+ description: string().optional()
14109
+ });
14110
+ /** The full capability block consulted before dispatch. */
14111
+ var TargetKindCapsSchema = object({
14112
+ attachments: object({
14113
+ mediaTypes: array(AttachmentMediaTypeSchema),
14114
+ mode: _enum([
14115
+ "url",
14116
+ "bytes",
14117
+ "both"
14118
+ ]),
14119
+ max: number().int().nonnegative(),
14120
+ maxBytes: number().int().positive().optional()
14121
+ }),
14122
+ /** Max action buttons (0 = none). */
14123
+ actions: number().int().nonnegative(),
14124
+ /**
14125
+ * Whether this kind renders a per-action ICON.
14126
+ *
14127
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
14128
+ * run on the addon cap path — three production failures in one day taught
14129
+ * this repo that once. Absent is read as false by the degrade engine, which
14130
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
14131
+ * assumed and dropped costs the operator's trust in the field.
14132
+ */
14133
+ actionIcons: boolean().optional(),
14134
+ levels: array(TargetKindLevelSchema),
14135
+ format: array(NotificationFormatSchema),
14136
+ clickUrl: boolean(),
14137
+ sound: boolean(),
14138
+ ttl: boolean(),
14139
+ bodyMaxLen: number().int().positive()
14140
+ });
14141
+ /**
14142
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
14143
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
14144
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
14145
+ * the union is large and not meant for runtime validation here; the exported
14146
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
14147
+ */
14148
+ var ConfigSchemaPassthrough$4 = unknown();
14149
+ var TargetKindSchema = object({
14150
+ kind: string(),
14151
+ label: string(),
14152
+ icon: string(),
14153
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
14154
+ addonId: string(),
14155
+ /**
14156
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
14157
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
14158
+ * when the addon bundles no icon for that kind — the client then falls back
14159
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
14160
+ *
14161
+ * Root-relative on purpose: it resolves against whatever origin serves a web
14162
+ * client, and a native client joins it onto its own hub base.
14163
+ *
14164
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
14165
+ * field that survived only because the runtime cap-router forwards provider
14166
+ * output verbatim — so every consumer had to re-declare it by hand to stop
14167
+ * its own Zod parse from stripping it, and the whole arrangement would have
14168
+ * broken silently the moment output validation was tightened anywhere.
14169
+ */
14170
+ iconUrl: string().optional(),
14171
+ /**
14172
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
14173
+ *
14174
+ * The server knows this and therefore says it, because the client cannot
14175
+ * safely guess: a React-Native client renders SVG and raster through two
14176
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
14177
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
14178
+ * placeholder glyph for every vector icon while the web build looked fine.
14179
+ *
14180
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
14181
+ * not been updated — a client that cannot determine the type should prefer
14182
+ * its raster path, which is the safe default for an unknown image.
14183
+ */
14184
+ iconMediaType: string().optional(),
14185
+ configSchema: ConfigSchemaPassthrough$4,
14186
+ /**
14187
+ * The config keys that decide WHERE this kind's secrets are sent (server,
14188
+ * base URL, instance, chat, recipient). A save that changes any of them
14189
+ * while leaving a secret redacted is refused by the provider (D634), so an
14190
+ * editor uses this to ask for the secret on exactly those edits instead of
14191
+ * on every edit.
14192
+ *
14193
+ * Optional: absent from a provider that holds no secret (the Home Assistant
14194
+ * kinds) and from a provider older than this field — an editor that finds
14195
+ * it absent must assume ANY edit may need the secret.
14196
+ */
14197
+ destinationKeys: array(string()).optional(),
14198
+ supportsDiscovery: boolean(),
14199
+ /**
14200
+ * Extra delivery parameters a rule may set for a target of this kind (D938).
14201
+ * Absent = none; the rule editor then offers nothing.
14202
+ */
14203
+ deliveryParams: array(DeliveryParamSchema).optional(),
14204
+ caps: TargetKindCapsSchema
14205
+ });
14206
+ /**
14207
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
14208
+ * (return a presence marker only) when serving `listTargets` — never
14209
+ * round-trip a stored secret to the UI.
14210
+ */
14211
+ var TargetSchema = object({
14212
+ id: string(),
14213
+ name: string(),
14214
+ kind: string(),
14215
+ addonId: string(),
14216
+ enabled: boolean(),
14217
+ config: record(string(), unknown())
14218
+ });
14219
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
14220
+ var DiscoveredTargetSchema = object({
14221
+ kind: string(),
14222
+ suggestedName: string(),
14223
+ config: record(string(), unknown())
14224
+ });
14225
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
14226
+ var RenderedAsSchema = object({
14227
+ level: string(),
14228
+ format: NotificationFormatSchema,
14229
+ attachmentsSent: number().int().nonnegative(),
14230
+ actionsSent: number().int().nonnegative(),
14231
+ truncated: boolean(),
14232
+ dropped: array(string())
14233
+ });
14234
+ var SendResultSchema = object({
14235
+ success: boolean(),
14236
+ error: string().optional(),
14237
+ renderedAs: RenderedAsSchema.optional()
14238
+ });
14239
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
14240
+ var TestResultSchema = SendResultSchema;
14241
+ /**
14242
+ * Pre-save credential probe against a partial (unsaved) target config.
14243
+ * `resolvedUrl` never includes secrets (token stripped from path).
14244
+ */
14245
+ var ProbeTargetConfigResultSchema = object({
14246
+ ok: boolean(),
14247
+ resolvedUrl: string(),
14248
+ httpStatus: number().int().optional(),
14249
+ stateInstance: string().optional(),
14250
+ error: string().optional()
14251
+ });
14252
+ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
14253
+ kind: string(),
14254
+ config: record(string(), unknown()).optional(),
14255
+ /**
14256
+ * When editing / duplicating, the blank password field never round-trips
14257
+ * the stored token. Pass the source target id so the provider can restore
14258
+ * secrets server-side before dialling (same merge as upsert).
14259
+ */
14260
+ targetId: string().optional()
14261
+ }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
14262
+ kind: string(),
14263
+ config: record(string(), unknown()),
14264
+ /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
14265
+ targetId: string().optional()
14266
+ }), ProbeTargetConfigResultSchema, {
14267
+ kind: "mutation",
14268
+ auth: "admin"
14269
+ }), method(object({
14270
+ targetId: string(),
14271
+ notification: NotificationSchema
14272
+ }), SendResultSchema, {
14273
+ kind: "mutation",
14274
+ auth: "admin"
14275
+ }), method(object({
14276
+ targetId: string(),
14277
+ sample: NotificationSchema.optional()
14278
+ }), TestResultSchema, {
14279
+ kind: "mutation",
14280
+ auth: "admin"
14281
+ }), method(object({
14282
+ target: TargetSchema,
14283
+ /**
14284
+ * Duplicate flow: create a NEW target id while copying secrets from an
14285
+ * existing row. The UI never holds the stored token; blank / redacted
14286
+ * password fields are restored from this source (D634 still applies if
14287
+ * destinations diverge without a re-typed secret).
14288
+ */
14289
+ cloneSecretsFrom: string().optional()
14290
+ }), TargetSchema, {
14291
+ kind: "mutation",
14292
+ auth: "admin"
14293
+ }), method(object({ targetId: string() }), _void(), {
14294
+ kind: "mutation",
14295
+ auth: "admin"
14296
+ }), method(object({
14297
+ targetId: string(),
14298
+ enabled: boolean()
14299
+ }), _void(), {
14300
+ kind: "mutation",
14301
+ auth: "admin"
14302
+ });
14303
+ looseObject({
14304
+ name: string().min(1),
14305
+ enabled: boolean().optional()
14306
+ });
14307
+ TargetSchema.omit({ id: true });
13884
14308
  var EncodeProfileSchema = object({
13885
14309
  video: object({
13886
14310
  codec: _enum([
@@ -22098,7 +22522,6 @@ var SYSTEM_CAP_VOCABULARY = {
22098
22522
  "calendar-source": system(INTERNAL),
22099
22523
  calendars: system("calendars feed schedules and conditions, not device actions"),
22100
22524
  composer: system("device composition — an authoring tool, not an action"),
22101
- "connection-test": system(INTERNAL),
22102
22525
  "core-blocks": system(INTERNAL),
22103
22526
  "custom-model-registry": system(ML),
22104
22527
  "data-store-provider": system(INTERNAL),
@@ -43784,657 +44207,236 @@ var AutomationRunVerdictSchema = object({
43784
44207
  "bypass"
43785
44208
  ]),
43786
44209
  reason: string()
43787
- });
43788
- /** One device of a multi-device step: did the call land. */
43789
- var StepDeviceTraceSchema = object({
43790
- deviceId: number().int(),
43791
- outcome: _enum(["ok", "error"]),
43792
- message: string().optional()
43793
- });
43794
- var AutomationStepTraceSchema = object({
43795
- /** `actions.<i>` */
43796
- path: string().min(1),
43797
- kind: string().min(1),
43798
- cap: string().optional(),
43799
- method: string().optional(),
43800
- /** The vocabulary action the step performs (`cover.open`), when it names one. */
43801
- actionId: string().optional(),
43802
- deviceId: number().int().optional(),
43803
- /** What happened per device, when the step drove several (or a domain). */
43804
- devices: array(StepDeviceTraceSchema).optional(),
43805
- startedAt: number().optional(),
43806
- endedAt: number().optional(),
43807
- /**
43808
- * The parameters the step SENT, after `{{vars.x}}` substitution — what the
43809
- * device was actually asked. Bounded like trigger `vars`; a secret parameter
43810
- * is shown as {@link REDACTED_ARG}. Absent on traces written before it existed.
43811
- */
43812
- args: record(string(), TraceValueSchema).optional(),
43813
- /** `empty`: the target resolved to no device, so nothing was done — never silently `ok`. */
43814
- outcome: _enum([
43815
- "ok",
43816
- "error",
43817
- "not-run",
43818
- "empty"
43819
- ]),
43820
- message: string().optional()
43821
- });
43822
- var AutomationRunTraceSchema = object({
43823
- runId: string().min(1),
43824
- startedAt: number(),
43825
- endedAt: number(),
43826
- trigger: AutomationTriggerTraceSchema,
43827
- conditions: array(AutomationConditionTraceNodeSchema),
43828
- verdict: AutomationRunVerdictSchema.optional(),
43829
- steps: array(AutomationStepTraceSchema),
43830
- outcome: AutomationRunOutcomeSchema,
43831
- /** How many identical consecutive skips this trace stands for (>= 1). */
43832
- repeat: number().int().min(1),
43833
- /** When the latest of those repeats happened. */
43834
- lastAt: number(),
43835
- /**
43836
- * A scheduler point's own outcome (D983). `outcome` carries the closest
43837
- * automation outcome so a reader that does not know schedulers still reads
43838
- * the trace; this field says exactly what happened.
43839
- */
43840
- scheduler: SchedulerOutcomeSchema.optional()
43841
- });
43842
- /**
43843
- * What the hub's `runs` ledger holds for one owner (`automation:<blockId>` or
43844
- * `action:<actionId>`): the cooldown stamp and the trace ring.
43845
- */
43846
- var AutomationStateDocSchema = object({
43847
- version: literal(1),
43848
- lastRunStartedAt: number().nullable(),
43849
- traces: array(AutomationRunTraceSchema)
43850
- });
43851
- /**
43852
- * Actions — named, runnable things an operator (or an automation) can start,
43853
- * declared as LIST ENTRIES, never as devices.
43854
- *
43855
- * An entry is either **internal** (a short list of cap/wait steps kept in the
43856
- * hub's `actions` record kind) or declared by an addon through the
43857
- * `action-source` collection (a Home Assistant script, a scene …). The one
43858
- * surface the UI and automations talk to is the `actions` cap; it keeps no
43859
- * catalog of its own (D3) — every `list` asks the providers live.
43860
- *
43861
- * This file is the data half: the entry schema, the variables an entry may
43862
- * declare, and the pure resolution of the values a run was given. It holds no
43863
- * runtime.
43864
- */
43865
- /** What a variable's value is. `select` takes one of `options`. */
43866
- var ActionVariableTypeSchema = _enum([
43867
- "string",
43868
- "number",
43869
- "boolean",
43870
- "select"
43871
- ]);
43872
- var ACTION_VARIABLE_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
43873
- var ActionVariableValueSchema = union([
43874
- string(),
43875
- number(),
43876
- boolean()
43877
- ]);
43878
- /**
43879
- * One declared variable. Steps reference it as `{ $var: 'name' }` (the whole
43880
- * value) or `{{vars.name}}` (inside a string) in their args.
43881
- */
43882
- var ActionVariableSchema = object({
43883
- name: string().regex(ACTION_VARIABLE_NAME_RE),
43884
- label: string().min(1).max(120).optional(),
43885
- type: ActionVariableTypeSchema,
43886
- required: boolean().optional(),
43887
- default: ActionVariableValueSchema.optional(),
43888
- /** The allowed values of a `select`. */
43889
- options: array(string().min(1)).min(1).optional()
43890
- }).superRefine((v, ctx) => {
43891
- if (v.type === "select" && v.options === void 0) ctx.addIssue({
43892
- code: "custom",
43893
- message: "a select variable needs options",
43894
- path: ["options"]
43895
- });
43896
- if (v.default !== void 0 && variableValueError(v, v.default) !== null) ctx.addIssue({
43897
- code: "custom",
43898
- message: variableValueError(v, v.default) ?? "invalid default",
43899
- path: ["default"]
43900
- });
43901
- });
43902
- /** Why `value` is not a legal value for the variable, or null when it is. */
43903
- function variableValueError(variable, value) {
43904
- switch (variable.type) {
43905
- case "string": return typeof value === "string" ? null : `variable "${variable.name}" must be a string`;
43906
- case "number": return typeof value === "number" && Number.isFinite(value) ? null : `variable "${variable.name}" must be a number`;
43907
- case "boolean": return typeof value === "boolean" ? null : `variable "${variable.name}" must be true or false`;
43908
- case "select": return typeof value === "string" && (variable.options ?? []).includes(value) ? null : `variable "${variable.name}" must be one of ${(variable.options ?? []).join(", ")}`;
43909
- }
43910
- }
43911
- var ActionSourceKindSchema = _enum(["internal", "addon"]);
43912
- function isInternalActionId(actionId) {
43913
- return actionId.startsWith("internal:") && actionId.length > 9;
43914
- }
43915
- /** The addon behind a `<addonId>:<localId>` id, or null for anything else (internal included). */
43916
- function addonIdOfActionId(actionId) {
43917
- if (isInternalActionId(actionId)) return null;
43918
- const at = actionId.indexOf(":");
43919
- return at > 0 && at < actionId.length - 1 ? actionId.slice(0, at) : null;
43920
- }
43921
- var ActionStatusSchema = object({
43922
- isRunning: boolean(),
43923
- /** Ms epoch the latest run STARTED; null when it never ran. */
43924
- lastRunAt: number().nullable(),
43925
- /** The latest run's failure; null on success or when it never ran. */
43926
- lastError: string().nullable()
43927
- });
43928
- var ActionEntrySchema = object({
43929
- /** `internal:<uuid>` or `<addonId>:<localId>`. */
43930
- id: string().min(3),
43931
- name: string().min(1).max(120),
43932
- /** The addon that declared it; `core-blocks` for an internal one. */
43933
- addonId: string().min(1),
43934
- source: ActionSourceKindSchema,
43935
- /** An addon-declared entry is edited where it was declared. */
43936
- readOnly: boolean(),
43937
- /** Internal only: cap and wait steps. No action-in-action, so no cycles. */
43938
- steps: array(ActionStepSchema).min(1).max(50).optional(),
43939
- variables: array(ActionVariableSchema).max(20).optional(),
43940
- status: ActionStatusSchema
43941
- }).superRefine((entry, ctx) => {
43942
- if (entry.source === "internal") {
43943
- if (!isInternalActionId(entry.id)) ctx.addIssue({
43944
- code: "custom",
43945
- message: "an internal action id is internal:<uuid>",
43946
- path: ["id"]
43947
- });
43948
- return;
43949
- }
43950
- if (entry.steps !== void 0) ctx.addIssue({
43951
- code: "custom",
43952
- message: "only an internal action carries steps",
43953
- path: ["steps"]
43954
- });
43955
- if (!entry.readOnly) ctx.addIssue({
43956
- code: "custom",
43957
- message: "an addon action is read-only here",
43958
- path: ["readOnly"]
43959
- });
43960
- if (addonIdOfActionId(entry.id) === null) ctx.addIssue({
43961
- code: "custom",
43962
- message: "an addon action id is <addonId>:<localId>",
43963
- path: ["id"]
43964
- });
43965
- });
43966
- /** What an operator writes for an internal action. */
43967
- var ActionInputSchema = object({
43968
- name: string().min(1).max(120),
43969
- steps: array(ActionStepSchema).min(1).max(50),
43970
- variables: array(ActionVariableSchema).max(20).optional()
43971
- });
43972
- /** Who started a run. Absent = the operator pressed it. */
43973
- var ActionRunOriginSchema = object({
43974
- /** The automation block whose step started it. */
43975
- blockId: string().min(1).optional() });
43976
- /**
43977
- * `started` = the caller did not ask to wait. `timed-out` = the run reached the
43978
- * ceiling (`ACTION_RUN_MAX_MS`) and was stopped; it is never a `ran`.
43979
- */
43980
- var ActionRunOutcomeSchema = union([
43981
- AutomationRunOutcomeSchema,
43982
- literal("started"),
43983
- literal("timed-out")
43984
- ]);
43985
- var ActionRunResultSchema = object({
43986
- runId: string().min(1),
43987
- outcome: ActionRunOutcomeSchema,
43988
- /** Why it did not run, or what failed. */
43989
- message: string().optional()
43990
- });
43991
- _enum([
43992
- "not-found",
43993
- "read-only",
43994
- "invalid-variables",
43995
- "invalid-steps",
43996
- "not-linked",
43997
- "busy"
43998
- ]);
43999
- object({
44000
- actionId: string(),
44001
- runId: string(),
44002
- isRunning: boolean(),
44003
- /** Set when the run ended. */
44004
- outcome: AutomationRunOutcomeSchema.optional(),
44005
- lastRunAt: number().nullable(),
44006
- lastError: string().nullable()
44007
- });
44008
- /** A bare number accepted as a base unit — the save succeeded, say so. */
44009
- var UnitWarningSchema = object({
44010
- /** Where in the saved record, e.g. `conditions.children.0` or `steps.2.args.grams`. */
44011
- path: string(),
44012
- deviceId: number().int().optional(),
44013
- field: string(),
44014
- value: number(),
44015
- assumedUnit: string(),
44016
- message: string()
44017
- });
44018
- /**
44019
- * notification-output — canonical, capability-gated notification delivery.
44020
- *
44021
- * Apprise-derived model (see
44022
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
44023
- * callers emit ONE canonical `Notification`; each provider declares a
44024
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
44025
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
44026
- * message to what the kind supports — callers never special-case a service.
44027
- *
44028
- * DESIGN DECISIONS (locked):
44029
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
44030
- * `setTargetEnabled`), each provider persisting via the `settings-store`
44031
- * cap. Rationale: the admin UI needs one uniform surface across the
44032
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
44033
- * alternative would fork the UI per addon and cannot host the
44034
- * discovery→adopt flow.
44035
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
44036
- * the generated cap-mount auto-`concatCollection`-fans them across every
44037
- * registered provider (notifiers addon + HA addon) so one catalog is
44038
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
44039
- * `addonId` the generated collection router extracts from the call input.
44040
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
44041
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
44042
- * `storage` / `storage-provider` / `recording` caps over the same path. No
44043
- * base64 fallback needed.
44044
- *
44045
- * TODO (deferred, closed-set change — separate decision): add
44046
- * `providerKind: 'notify'` so notification providers surface on the unified
44047
- * admin "Integrations" page.
44048
- */
44049
- /**
44050
- * Zentik-derived typed-media enum — the superset across every kind. Each
44051
- * adapter picks what it supports and the degrade engine filters the rest.
44052
- */
44053
- var AttachmentMediaTypeSchema = _enum([
44054
- "image",
44055
- "video",
44056
- "gif",
44057
- "audio",
44058
- "icon"
44059
- ]);
44060
- /**
44061
- * A single attachment. Exactly one of `url` (remote source, most adapters
44062
- * prefer this) or `bytes` (inline source; required for Pushover-style
44063
- * bytes-only kinds) MUST be present — the degrade engine expresses a
44064
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
44065
- */
44066
- var AttachmentSchema = object({
44067
- mediaType: AttachmentMediaTypeSchema,
44068
- url: string().optional(),
44069
- bytes: _instanceof(Uint8Array).optional(),
44070
- mime: string().optional(),
44071
- name: string().optional()
44072
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
44073
- var NotificationFormatSchema = _enum([
44074
- "text",
44075
- "markdown",
44076
- "html"
44077
- ]);
44078
- /**
44079
- * The CLOSED icon vocabulary an action button may use.
44080
- *
44081
- * A closed set, not a free string, and that is the whole point: an arbitrary
44082
- * icon name is one that ntfy renders, zentik silently drops, and nobody
44083
- * notices — the same class of gap as a zone vocabulary nothing produced
44084
- * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
44085
- * declares `actionIcons: false` and the degrade engine strips the field.
44086
- *
44087
- * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
44088
- * renderer's icon set; "acknowledge" survives an adapter that draws it
44089
- * differently.
44090
- *
44091
- * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
44092
- *
44093
- * These names are for US. **No adapter may forward one verbatim.** Each maps
44094
- * the whole set onto its own renderer's vocabulary through a
44095
- * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
44096
- * fallback, so adding a member here fails every adapter's build until someone
44097
- * decides its glyph, which is the only place that decision can be made
44098
- * honestly.
44099
- *
44100
- * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
44101
- * `disarm` straight through; iOS feeds that string to
44102
- * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
44103
- * and every snooze and alarm button arrived BLANK. A pass-through is not a
44104
- * mapping, and "the field is documented" is not "the value renders".
44105
- *
44106
- * Adding a member is TRAIN-BOUND. The enum lives in the published
44107
- * `@camstack/server` closure and the cap seam validates against the HUB's copy,
44108
- * so an addon that emits a token the running hub does not know does not lose an
44109
- * icon — its whole `send` fails Zod validation and the notification never
44110
- * arrives. Never emit a new token from an addon before the train carrying it.
44111
- */
44112
- var NotificationActionIconSchema = _enum([
44113
- "acknowledge",
44114
- "dismiss",
44115
- "silence",
44116
- "snooze",
44117
- "view",
44118
- "play",
44119
- "open",
44120
- "close",
44121
- "lock",
44122
- "unlock",
44123
- "arm",
44124
- "arm-home",
44125
- "arm-away",
44126
- "arm-night",
44127
- "disarm",
44128
- "light",
44129
- "alert",
44130
- "camera"
44131
- ]);
44132
- /** A single tap-through action button. */
44133
- var NotificationActionSchema = object({
44134
- id: string(),
44135
- label: string(),
44136
- url: string().optional(),
44137
- /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
44138
- icon: NotificationActionIconSchema.optional(),
44139
- /**
44140
- * Renders in a warning style where the notifier supports it.
44141
- *
44142
- * A HINT, never a gate. The callback's authority is its token and nothing
44143
- * else — see `notification-center/action-token.ts` for what that does and
44144
- * does not buy.
44145
- */
44146
- destructive: boolean().optional(),
44147
- /**
44148
- * How the tap should REACH the url.
44149
- *
44150
- * `navigate` (absent, and every button authored before this field) opens it:
44151
- * the phone leaves the notification and shows whatever the callback returns.
44152
- * That is right for a button whose answer the operator wants to read.
44153
- *
44154
- * `background` fires it as a POST and stays put. It exists for the buttons
44155
- * whose whole point is not to interrupt — "silence this for 30 minutes" is
44156
- * an answer to the notification, and being thrown into a browser tab to
44157
- * confirm it costs more attention than the notification did. A backend that
44158
- * cannot do a background call renders it as an ordinary link (the adapters
44159
- * fall back rather than dropping the button), so this is a preference, never
44160
- * a requirement.
44161
- */
44162
- mode: _enum(["navigate", "background"]).optional()
44163
- });
44164
- /** A delivery parameter value: what a notifier declared it takes. */
44165
- var DeliveryParamValueSchema = union([
44166
- string(),
44167
- number(),
44168
- boolean()
44169
- ]);
44170
- /**
44171
- * One extra delivery parameter a notifier KIND declares (D938). A rule that
44172
- * addresses a target of that kind may set it; a kind that declares nothing is
44173
- * never handed one. The admin UI derives its control from this descriptor —
44174
- * no editor knows a service by name.
44175
- *
44176
- * Optional fields are `.optional()`, never `.default()` (a Zod default does not
44177
- * run on the addon cap path).
44178
- */
44179
- var DeliveryParamSchema = object({
44180
- key: string().min(1),
44181
- label: string(),
44182
- /** i18n key of the label, resolved by the declaring addon's namespace. */
44183
- labelKey: string().optional(),
44184
- type: _enum([
44185
- "enum",
44186
- "number",
44187
- "boolean",
44188
- "string"
44189
- ]),
44190
- /** `enum` only: the closed set of accepted values. */
44191
- options: array(object({
44192
- value: string(),
44193
- label: string()
44194
- })).optional(),
44195
- /** `number` only. */
44196
- min: number().optional(),
44197
- max: number().optional(),
44198
- /** What the notifier does when the rule sets nothing; shown as the placeholder. */
44199
- default: DeliveryParamValueSchema.optional()
44200
- });
44201
- /**
44202
- * The canonical notification. `body` is the only hard field (Apprise model).
44203
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
44204
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
44205
- * the adapter maps this ordinal onto its native level. `level?` is an
44206
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
44207
- * `priority` for that one target.
44208
- */
44209
- var NotificationSchema = object({
44210
- body: string(),
44211
- title: string().optional(),
44212
- format: NotificationFormatSchema.default("text"),
44213
- priority: number().int().min(1).max(5).default(3),
44214
- level: string().optional(),
44215
- attachments: array(AttachmentSchema).optional(),
44216
- clickUrl: string().optional(),
44217
- actions: array(NotificationActionSchema).optional(),
44218
- sound: string().optional(),
44219
- ttl: number().optional(),
44220
- tag: string().optional(),
44221
- deviceId: number().optional(),
44222
- eventId: string().optional(),
44223
- metadata: record(string(), unknown()).optional(),
44210
+ });
44211
+ /** One device of a multi-device step: did the call land. */
44212
+ var StepDeviceTraceSchema = object({
44213
+ deviceId: number().int(),
44214
+ outcome: _enum(["ok", "error"]),
44215
+ message: string().optional()
44216
+ });
44217
+ var AutomationStepTraceSchema = object({
44218
+ /** `actions.<i>` */
44219
+ path: string().min(1),
44220
+ kind: string().min(1),
44221
+ cap: string().optional(),
44222
+ method: string().optional(),
44223
+ /** The vocabulary action the step performs (`cover.open`), when it names one. */
44224
+ actionId: string().optional(),
44225
+ deviceId: number().int().optional(),
44226
+ /** What happened per device, when the step drove several (or a domain). */
44227
+ devices: array(StepDeviceTraceSchema).optional(),
44228
+ startedAt: number().optional(),
44229
+ endedAt: number().optional(),
44224
44230
  /**
44225
- * Values for the parameters the target's kind DECLARES in
44226
- * {@link TargetKindSchema} `deliveryParams`. Already validated against the
44227
- * declaration by the sender; a notifier reads only the keys it declared.
44231
+ * The parameters the step SENT, after `{{vars.x}}` substitution — what the
44232
+ * device was actually asked. Bounded like trigger `vars`; a secret parameter
44233
+ * is shown as {@link REDACTED_ARG}. Absent on traces written before it existed.
44228
44234
  */
44229
- delivery: record(string(), DeliveryParamValueSchema).optional()
44230
- });
44231
- /** One declared native severity/priority level for a kind. */
44232
- var TargetKindLevelSchema = object({
44233
- id: string(),
44234
- label: string(),
44235
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
44236
- ordinal: number().int().min(1).max(5).nullable(),
44237
- flags: object({
44238
- critical: boolean().optional(),
44239
- silent: boolean().optional(),
44240
- noPush: boolean().optional()
44241
- }).optional(),
44242
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
44243
- requires: array(string()).optional(),
44244
- description: string().optional()
44235
+ args: record(string(), TraceValueSchema).optional(),
44236
+ /** `empty`: the target resolved to no device, so nothing was done — never silently `ok`. */
44237
+ outcome: _enum([
44238
+ "ok",
44239
+ "error",
44240
+ "not-run",
44241
+ "empty"
44242
+ ]),
44243
+ message: string().optional()
44245
44244
  });
44246
- /** The full capability block consulted before dispatch. */
44247
- var TargetKindCapsSchema = object({
44248
- attachments: object({
44249
- mediaTypes: array(AttachmentMediaTypeSchema),
44250
- mode: _enum([
44251
- "url",
44252
- "bytes",
44253
- "both"
44254
- ]),
44255
- max: number().int().nonnegative(),
44256
- maxBytes: number().int().positive().optional()
44257
- }),
44258
- /** Max action buttons (0 = none). */
44259
- actions: number().int().nonnegative(),
44245
+ var AutomationRunTraceSchema = object({
44246
+ runId: string().min(1),
44247
+ startedAt: number(),
44248
+ endedAt: number(),
44249
+ trigger: AutomationTriggerTraceSchema,
44250
+ conditions: array(AutomationConditionTraceNodeSchema),
44251
+ verdict: AutomationRunVerdictSchema.optional(),
44252
+ steps: array(AutomationStepTraceSchema),
44253
+ outcome: AutomationRunOutcomeSchema,
44254
+ /** How many identical consecutive skips this trace stands for (>= 1). */
44255
+ repeat: number().int().min(1),
44256
+ /** When the latest of those repeats happened. */
44257
+ lastAt: number(),
44260
44258
  /**
44261
- * Whether this kind renders a per-action ICON.
44262
- *
44263
- * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
44264
- * run on the addon cap path — three production failures in one day taught
44265
- * this repo that once. Absent is read as false by the degrade engine, which
44266
- * is the safe direction: an icon that is not rendered costs nothing, an icon
44267
- * assumed and dropped costs the operator's trust in the field.
44259
+ * A scheduler point's own outcome (D983). `outcome` carries the closest
44260
+ * automation outcome so a reader that does not know schedulers still reads
44261
+ * the trace; this field says exactly what happened.
44268
44262
  */
44269
- actionIcons: boolean().optional(),
44270
- levels: array(TargetKindLevelSchema),
44271
- format: array(NotificationFormatSchema),
44272
- clickUrl: boolean(),
44273
- sound: boolean(),
44274
- ttl: boolean(),
44275
- bodyMaxLen: number().int().positive()
44263
+ scheduler: SchedulerOutcomeSchema.optional()
44276
44264
  });
44277
44265
  /**
44278
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
44279
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
44280
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
44281
- * the union is large and not meant for runtime validation here; the exported
44282
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
44266
+ * What the hub's `runs` ledger holds for one owner (`automation:<blockId>` or
44267
+ * `action:<actionId>`): the cooldown stamp and the trace ring.
44283
44268
  */
44284
- var ConfigSchemaPassthrough$4 = unknown();
44285
- var TargetKindSchema = object({
44286
- kind: string(),
44287
- label: string(),
44288
- icon: string(),
44289
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
44290
- addonId: string(),
44291
- /**
44292
- * URL of the kind's bundled BRAND icon, served by the providing addon over
44293
- * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
44294
- * when the addon bundles no icon for that kind — the client then falls back
44295
- * to a neutral glyph rather than rendering the raw `icon` NAME as text.
44296
- *
44297
- * Root-relative on purpose: it resolves against whatever origin serves a web
44298
- * client, and a native client joins it onto its own hub base.
44299
- *
44300
- * DECLARED here deliberately. It used to travel as an undeclared passthrough
44301
- * field that survived only because the runtime cap-router forwards provider
44302
- * output verbatim — so every consumer had to re-declare it by hand to stop
44303
- * its own Zod parse from stripping it, and the whole arrangement would have
44304
- * broken silently the moment output validation was tightened anywhere.
44305
- */
44306
- iconUrl: string().optional(),
44307
- /**
44308
- * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
44309
- *
44310
- * The server knows this and therefore says it, because the client cannot
44311
- * safely guess: a React-Native client renders SVG and raster through two
44312
- * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
44313
- * not decode SVG on iOS/Android), so without this it silently fell back to a
44314
- * placeholder glyph for every vector icon while the web build looked fine.
44315
- *
44316
- * Absent when {@link iconUrl} is absent, or for a legacy provider that has
44317
- * not been updated — a client that cannot determine the type should prefer
44318
- * its raster path, which is the safe default for an unknown image.
44319
- */
44320
- iconMediaType: string().optional(),
44321
- configSchema: ConfigSchemaPassthrough$4,
44322
- /**
44323
- * The config keys that decide WHERE this kind's secrets are sent (server,
44324
- * base URL, instance, chat, recipient). A save that changes any of them
44325
- * while leaving a secret redacted is refused by the provider (D634), so an
44326
- * editor uses this to ask for the secret on exactly those edits instead of
44327
- * on every edit.
44328
- *
44329
- * Optional: absent from a provider that holds no secret (the Home Assistant
44330
- * kinds) and from a provider older than this field — an editor that finds
44331
- * it absent must assume ANY edit may need the secret.
44332
- */
44333
- destinationKeys: array(string()).optional(),
44334
- supportsDiscovery: boolean(),
44335
- /**
44336
- * Extra delivery parameters a rule may set for a target of this kind (D938).
44337
- * Absent = none; the rule editor then offers nothing.
44338
- */
44339
- deliveryParams: array(DeliveryParamSchema).optional(),
44340
- caps: TargetKindCapsSchema
44269
+ var AutomationStateDocSchema = object({
44270
+ version: literal(1),
44271
+ lastRunStartedAt: number().nullable(),
44272
+ traces: array(AutomationRunTraceSchema)
44341
44273
  });
44342
44274
  /**
44343
- * A persisted target. `config` holds secrets; providers REDACT secret fields
44344
- * (return a presence marker only) when serving `listTargets` — never
44345
- * round-trip a stored secret to the UI.
44275
+ * Actions — named, runnable things an operator (or an automation) can start,
44276
+ * declared as LIST ENTRIES, never as devices.
44277
+ *
44278
+ * An entry is either **internal** (a short list of cap/wait steps kept in the
44279
+ * hub's `actions` record kind) or declared by an addon through the
44280
+ * `action-source` collection (a Home Assistant script, a scene …). The one
44281
+ * surface the UI and automations talk to is the `actions` cap; it keeps no
44282
+ * catalog of its own (D3) — every `list` asks the providers live.
44283
+ *
44284
+ * This file is the data half: the entry schema, the variables an entry may
44285
+ * declare, and the pure resolution of the values a run was given. It holds no
44286
+ * runtime.
44346
44287
  */
44347
- var TargetSchema = object({
44348
- id: string(),
44349
- name: string(),
44350
- kind: string(),
44351
- addonId: string(),
44352
- enabled: boolean(),
44353
- config: record(string(), unknown())
44288
+ /** What a variable's value is. `select` takes one of `options`. */
44289
+ var ActionVariableTypeSchema = _enum([
44290
+ "string",
44291
+ "number",
44292
+ "boolean",
44293
+ "select"
44294
+ ]);
44295
+ var ACTION_VARIABLE_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
44296
+ var ActionVariableValueSchema = union([
44297
+ string(),
44298
+ number(),
44299
+ boolean()
44300
+ ]);
44301
+ /**
44302
+ * One declared variable. Steps reference it as `{ $var: 'name' }` (the whole
44303
+ * value) or `{{vars.name}}` (inside a string) in their args.
44304
+ */
44305
+ var ActionVariableSchema = object({
44306
+ name: string().regex(ACTION_VARIABLE_NAME_RE),
44307
+ label: string().min(1).max(120).optional(),
44308
+ type: ActionVariableTypeSchema,
44309
+ required: boolean().optional(),
44310
+ default: ActionVariableValueSchema.optional(),
44311
+ /** The allowed values of a `select`. */
44312
+ options: array(string().min(1)).min(1).optional()
44313
+ }).superRefine((v, ctx) => {
44314
+ if (v.type === "select" && v.options === void 0) ctx.addIssue({
44315
+ code: "custom",
44316
+ message: "a select variable needs options",
44317
+ path: ["options"]
44318
+ });
44319
+ if (v.default !== void 0 && variableValueError(v, v.default) !== null) ctx.addIssue({
44320
+ code: "custom",
44321
+ message: variableValueError(v, v.default) ?? "invalid default",
44322
+ path: ["default"]
44323
+ });
44354
44324
  });
44355
- /** A discovery-surfaced candidate (config is partial + non-secret). */
44356
- var DiscoveredTargetSchema = object({
44357
- kind: string(),
44358
- suggestedName: string(),
44359
- config: record(string(), unknown())
44325
+ /** Why `value` is not a legal value for the variable, or null when it is. */
44326
+ function variableValueError(variable, value) {
44327
+ switch (variable.type) {
44328
+ case "string": return typeof value === "string" ? null : `variable "${variable.name}" must be a string`;
44329
+ case "number": return typeof value === "number" && Number.isFinite(value) ? null : `variable "${variable.name}" must be a number`;
44330
+ case "boolean": return typeof value === "boolean" ? null : `variable "${variable.name}" must be true or false`;
44331
+ case "select": return typeof value === "string" && (variable.options ?? []).includes(value) ? null : `variable "${variable.name}" must be one of ${(variable.options ?? []).join(", ")}`;
44332
+ }
44333
+ }
44334
+ var ActionSourceKindSchema = _enum(["internal", "addon"]);
44335
+ function isInternalActionId(actionId) {
44336
+ return actionId.startsWith("internal:") && actionId.length > 9;
44337
+ }
44338
+ /** The addon behind a `<addonId>:<localId>` id, or null for anything else (internal included). */
44339
+ function addonIdOfActionId(actionId) {
44340
+ if (isInternalActionId(actionId)) return null;
44341
+ const at = actionId.indexOf(":");
44342
+ return at > 0 && at < actionId.length - 1 ? actionId.slice(0, at) : null;
44343
+ }
44344
+ var ActionStatusSchema = object({
44345
+ isRunning: boolean(),
44346
+ /** Ms epoch the latest run STARTED; null when it never ran. */
44347
+ lastRunAt: number().nullable(),
44348
+ /** The latest run's failure; null on success or when it never ran. */
44349
+ lastError: string().nullable()
44360
44350
  });
44361
- /** The degrade engine's report — what was resolved / dropped / degraded. */
44362
- var RenderedAsSchema = object({
44363
- level: string(),
44364
- format: NotificationFormatSchema,
44365
- attachmentsSent: number().int().nonnegative(),
44366
- actionsSent: number().int().nonnegative(),
44367
- truncated: boolean(),
44368
- dropped: array(string())
44351
+ var ActionEntrySchema = object({
44352
+ /** `internal:<uuid>` or `<addonId>:<localId>`. */
44353
+ id: string().min(3),
44354
+ name: string().min(1).max(120),
44355
+ /** The addon that declared it; `core-blocks` for an internal one. */
44356
+ addonId: string().min(1),
44357
+ source: ActionSourceKindSchema,
44358
+ /** An addon-declared entry is edited where it was declared. */
44359
+ readOnly: boolean(),
44360
+ /** Internal only: cap and wait steps. No action-in-action, so no cycles. */
44361
+ steps: array(ActionStepSchema).min(1).max(50).optional(),
44362
+ variables: array(ActionVariableSchema).max(20).optional(),
44363
+ status: ActionStatusSchema
44364
+ }).superRefine((entry, ctx) => {
44365
+ if (entry.source === "internal") {
44366
+ if (!isInternalActionId(entry.id)) ctx.addIssue({
44367
+ code: "custom",
44368
+ message: "an internal action id is internal:<uuid>",
44369
+ path: ["id"]
44370
+ });
44371
+ return;
44372
+ }
44373
+ if (entry.steps !== void 0) ctx.addIssue({
44374
+ code: "custom",
44375
+ message: "only an internal action carries steps",
44376
+ path: ["steps"]
44377
+ });
44378
+ if (!entry.readOnly) ctx.addIssue({
44379
+ code: "custom",
44380
+ message: "an addon action is read-only here",
44381
+ path: ["readOnly"]
44382
+ });
44383
+ if (addonIdOfActionId(entry.id) === null) ctx.addIssue({
44384
+ code: "custom",
44385
+ message: "an addon action id is <addonId>:<localId>",
44386
+ path: ["id"]
44387
+ });
44369
44388
  });
44370
- var SendResultSchema = object({
44371
- success: boolean(),
44372
- error: string().optional(),
44373
- renderedAs: RenderedAsSchema.optional()
44389
+ /** What an operator writes for an internal action. */
44390
+ var ActionInputSchema = object({
44391
+ name: string().min(1).max(120),
44392
+ steps: array(ActionStepSchema).min(1).max(50),
44393
+ variables: array(ActionVariableSchema).max(20).optional()
44374
44394
  });
44375
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
44376
- var TestResultSchema = SendResultSchema;
44395
+ /** Who started a run. Absent = the operator pressed it. */
44396
+ var ActionRunOriginSchema = object({
44397
+ /** The automation block whose step started it. */
44398
+ blockId: string().min(1).optional() });
44377
44399
  /**
44378
- * Pre-save credential probe against a partial (unsaved) target config.
44379
- * `resolvedUrl` never includes secrets (token stripped from path).
44400
+ * `started` = the caller did not ask to wait. `timed-out` = the run reached the
44401
+ * ceiling (`ACTION_RUN_MAX_MS`) and was stopped; it is never a `ran`.
44380
44402
  */
44381
- var ProbeTargetConfigResultSchema = object({
44382
- ok: boolean(),
44383
- resolvedUrl: string(),
44384
- httpStatus: number().int().optional(),
44385
- stateInstance: string().optional(),
44386
- error: string().optional()
44403
+ var ActionRunOutcomeSchema = union([
44404
+ AutomationRunOutcomeSchema,
44405
+ literal("started"),
44406
+ literal("timed-out")
44407
+ ]);
44408
+ var ActionRunResultSchema = object({
44409
+ runId: string().min(1),
44410
+ outcome: ActionRunOutcomeSchema,
44411
+ /** Why it did not run, or what failed. */
44412
+ message: string().optional()
44387
44413
  });
44388
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
44389
- kind: string(),
44390
- config: record(string(), unknown()).optional(),
44391
- /**
44392
- * When editing / duplicating, the blank password field never round-trips
44393
- * the stored token. Pass the source target id so the provider can restore
44394
- * secrets server-side before dialling (same merge as upsert).
44395
- */
44396
- targetId: string().optional()
44397
- }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
44398
- kind: string(),
44399
- config: record(string(), unknown()),
44400
- /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
44401
- targetId: string().optional()
44402
- }), ProbeTargetConfigResultSchema, {
44403
- kind: "mutation",
44404
- auth: "admin"
44405
- }), method(object({
44406
- targetId: string(),
44407
- notification: NotificationSchema
44408
- }), SendResultSchema, {
44409
- kind: "mutation",
44410
- auth: "admin"
44411
- }), method(object({
44412
- targetId: string(),
44413
- sample: NotificationSchema.optional()
44414
- }), TestResultSchema, {
44415
- kind: "mutation",
44416
- auth: "admin"
44417
- }), method(object({
44418
- target: TargetSchema,
44419
- /**
44420
- * Duplicate flow: create a NEW target id while copying secrets from an
44421
- * existing row. The UI never holds the stored token; blank / redacted
44422
- * password fields are restored from this source (D634 still applies if
44423
- * destinations diverge without a re-typed secret).
44424
- */
44425
- cloneSecretsFrom: string().optional()
44426
- }), TargetSchema, {
44427
- kind: "mutation",
44428
- auth: "admin"
44429
- }), method(object({ targetId: string() }), _void(), {
44430
- kind: "mutation",
44431
- auth: "admin"
44432
- }), method(object({
44433
- targetId: string(),
44434
- enabled: boolean()
44435
- }), _void(), {
44436
- kind: "mutation",
44437
- auth: "admin"
44414
+ _enum([
44415
+ "not-found",
44416
+ "read-only",
44417
+ "invalid-variables",
44418
+ "invalid-steps",
44419
+ "not-linked",
44420
+ "busy"
44421
+ ]);
44422
+ object({
44423
+ actionId: string(),
44424
+ runId: string(),
44425
+ isRunning: boolean(),
44426
+ /** Set when the run ended. */
44427
+ outcome: AutomationRunOutcomeSchema.optional(),
44428
+ lastRunAt: number().nullable(),
44429
+ lastError: string().nullable()
44430
+ });
44431
+ /** A bare number accepted as a base unit — the save succeeded, say so. */
44432
+ var UnitWarningSchema = object({
44433
+ /** Where in the saved record, e.g. `conditions.children.0` or `steps.2.args.grams`. */
44434
+ path: string(),
44435
+ deviceId: number().int().optional(),
44436
+ field: string(),
44437
+ value: number(),
44438
+ assumedUnit: string(),
44439
+ message: string()
44438
44440
  });
44439
44441
  _enum([
44440
44442
  "automation",
@@ -48313,37 +48315,6 @@ method(object({
48313
48315
  kind: "mutation",
48314
48316
  auth: "admin"
48315
48317
  }), method(object({ blockIds: array(string()) }), object({ states: array(CompositionBlockStateSchema) }), { auth: "admin" });
48316
- var ConnectionTestOutcomeSchema = discriminatedUnion("outcome", [
48317
- object({
48318
- outcome: literal("validated"),
48319
- /** Round-trip of the sign-in, when the provider measured it. */
48320
- latencyMs: number().nonnegative().optional(),
48321
- /** Optional human detail worth showing next to the tick
48322
- * ("3 devices visible on this account"). */
48323
- detail: string().optional()
48324
- }).strict(),
48325
- object({
48326
- outcome: literal("rejected"),
48327
- error: string()
48328
- }).strict(),
48329
- object({
48330
- outcome: literal("inconclusive"),
48331
- error: string()
48332
- }).strict()
48333
- ]);
48334
- var ConnectionTestInputSchema = object({
48335
- /** Candidate integration settings, exactly as the create form collected them. */
48336
- settings: record(string(), unknown()) });
48337
- /**
48338
- * What the provider's test actually DOES, so the UI can say it in words before
48339
- * the operator presses the button ("Signs in to the Dreo cloud"). Purely
48340
- * descriptive — it never changes routing.
48341
- */
48342
- var ConnectionTestDescriptorSchema = object({ label: string() });
48343
- method(ConnectionTestInputSchema, ConnectionTestOutcomeSchema, {
48344
- kind: "mutation",
48345
- auth: "admin"
48346
- }), method(_void(), ConnectionTestDescriptorSchema, { auth: "admin" });
48347
48318
  object({
48348
48319
  /** True when the upstream system considers the entity connected. */
48349
48320
  connected: boolean(),
@@ -54197,10 +54168,9 @@ var AvailableIntegrationTypeSchema = object({
54197
54168
  * locations" checkbox. Provider-declared in the addon manifest. */
54198
54169
  supportsLocationImport: boolean(),
54199
54170
  /**
54200
- * True when this integration DECLARES a pre-creation test (the
54201
- * `connection-test` cap, or a broker whose settings it stores). Drives the
54202
- * Test button: an integration that cannot be tested must say so up front
54203
- * rather than offering a button that always answers the same nonsense.
54171
+ * True when this integration's instances declare a Test (`integration-instances.describe().canTest`,
54172
+ * D1016). An integration that cannot be tested must say so up front rather than offering a
54173
+ * button that always answers the same nonsense.
54204
54174
  */
54205
54175
  canTest: boolean(),
54206
54176
  existingInstances: array(object({
@@ -54254,7 +54224,9 @@ var CreateIntegrationInputSchema = object({
54254
54224
  info: record(string(), unknown()).optional(),
54255
54225
  /** `stableId` of the discovery candidate the form was seeded from (D984). */
54256
54226
  originStableId: string().optional(),
54257
- settings: record(string(), unknown()).optional()
54227
+ settings: record(string(), unknown()).optional(),
54228
+ /** The instance kind being created, for a provider with several; omitted = its only testable kind. */
54229
+ kind: string().min(1).optional()
54258
54230
  });
54259
54231
  var UpdateIntegrationInputSchema = object({
54260
54232
  id: string(),
@@ -54280,13 +54252,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
54280
54252
  }), method(object({ id: string() }), DeleteIntegrationResultSchema, {
54281
54253
  kind: "mutation",
54282
54254
  auth: "admin"
54283
- }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
54284
- addonId: string(),
54285
- settings: record(string(), unknown())
54286
- }), TestConnectionResultSchema, {
54287
- kind: "mutation",
54288
- auth: "admin"
54289
- });
54255
+ }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" });
54290
54256
  /**
54291
54257
  * `integration-instances` - the connection of an addon that holds ONE or SEVERAL
54292
54258
  * (an account, a relay, a destination) as typed records (D985, D986). The addon's
@@ -62529,13 +62495,6 @@ var AddBrokerInputSchema = object({
62529
62495
  });
62530
62496
  var AddBrokerResultSchema = object({ id: string() });
62531
62497
  var IdInputSchema = object({ id: string() });
62532
- var TestResultSchema$1 = discriminatedUnion("ok", [object({
62533
- ok: literal(true),
62534
- latencyMs: number()
62535
- }), object({
62536
- ok: literal(false),
62537
- error: string()
62538
- })]);
62539
62498
  var StartEmbeddedInputSchema = object({
62540
62499
  port: number().int().min(1).max(65535).default(1883),
62541
62500
  /** Allow anonymous connect (no username/password). Default: false. */
@@ -62572,10 +62531,6 @@ var mqttBrokerCapability = {
62572
62531
  kind: "mutation",
62573
62532
  auth: "admin"
62574
62533
  }),
62575
- testConnection: method(IdInputSchema, TestResultSchema$1, {
62576
- kind: "mutation",
62577
- auth: "admin"
62578
- }),
62579
62534
  startEmbeddedBroker: method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
62580
62535
  kind: "mutation",
62581
62536
  auth: "admin"
@@ -74191,7 +74146,7 @@ var TrainingProviderTypeSchema = object({
74191
74146
  label: string(),
74192
74147
  /** Path under the addon package (e.g. `assets/training-modal/icon.svg`). */
74193
74148
  iconAsset: string(),
74194
- /** Owning `training-provider` addon — collection selector for createEntry. */
74149
+ /** Owning `training-provider` addon. */
74195
74150
  addonId: string(),
74196
74151
  /**
74197
74152
  * Fields for one entry of this type (credentials + defaults). Absent on a
@@ -74236,36 +74191,6 @@ var TrainingProviderDescriptorSchema = object({
74236
74191
  */
74237
74192
  formValues: record(string(), unknown()).optional()
74238
74193
  });
74239
- /**
74240
- * Patch one entry's name, credentials and defaults. Pin with owning `{ addonId }`.
74241
- * An omitted or empty `tokenSecret` means KEEP the stored secret (D787).
74242
- * `{ $secretClear: true }` clears it.
74243
- */
74244
- var TrainingProviderUpdateEntryInputSchema = object({
74245
- entryId: string().min(1),
74246
- name: string().min(1),
74247
- tokenId: string().optional(),
74248
- tokenSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
74249
- environment: string().optional(),
74250
- gpuType: string().optional(),
74251
- region: string().optional(),
74252
- costCapUsd: number().nonnegative().optional(),
74253
- maxRuntimeSec: number().int().positive().optional(),
74254
- failedDatasetGraceHours: number().nonnegative().optional()
74255
- });
74256
- var TrainingCredentialCheckSchema = object({
74257
- ok: boolean(),
74258
- /** Workspace / account name, when the provider says. */
74259
- account: string().optional(),
74260
- reason: _enum([
74261
- "missing",
74262
- "invalid",
74263
- "forbidden",
74264
- "network",
74265
- "quota"
74266
- ]).optional(),
74267
- detail: string().optional()
74268
- });
74269
74194
  var ProviderJobStateSchema = _enum([
74270
74195
  "pending",
74271
74196
  "starting",
@@ -74313,22 +74238,6 @@ var TrainingLogLineSchema = object({
74313
74238
  text: string()
74314
74239
  });
74315
74240
  method(object({}), array(TrainingProviderDescriptorSchema).readonly(), { auth: "admin" }), method(object({}), array(TrainingProviderTypeSchema).readonly(), { auth: "admin" }), method(object({}), array(TrainingProviderEntrySchema).readonly(), { auth: "admin" }), method(object({
74316
- typeId: string().min(1),
74317
- name: string().min(1)
74318
- }), object({ entryId: string() }), {
74319
- kind: "mutation",
74320
- auth: "admin"
74321
- }), method(object({ entryId: string().min(1) }), object({ ok: literal(true) }), {
74322
- kind: "mutation",
74323
- auth: "admin"
74324
- }), method(TrainingProviderUpdateEntryInputSchema, object({ ok: literal(true) }), {
74325
- kind: "mutation",
74326
- auth: "admin"
74327
- }), method(object({ entryId: string().min(1) }), TrainingCredentialCheckSchema, {
74328
- kind: "mutation",
74329
- auth: "admin",
74330
- timeoutMs: 3e4
74331
- }), method(object({
74332
74241
  entryId: string().min(1),
74333
74242
  gpuType: string(),
74334
74243
  region: string(),
@@ -77714,20 +77623,6 @@ Object.freeze({
77714
77623
  access: "view",
77715
77624
  role: "admin"
77716
77625
  },
77717
- "connectionTest.describeTest": {
77718
- capName: "connection-test",
77719
- capScope: "system",
77720
- addonId: null,
77721
- access: "view",
77722
- role: "admin"
77723
- },
77724
- "connectionTest.testSettings": {
77725
- capName: "connection-test",
77726
- capScope: "system",
77727
- addonId: null,
77728
- access: "create",
77729
- role: "admin"
77730
- },
77731
77626
  "connectivity.getStatus": {
77732
77627
  capName: "connectivity",
77733
77628
  capScope: "device",
@@ -80094,13 +79989,6 @@ Object.freeze({
80094
79989
  access: "view",
80095
79990
  role: "admin"
80096
79991
  },
80097
- "integrations.testConnection": {
80098
- capName: "integrations",
80099
- capScope: "system",
80100
- addonId: null,
80101
- access: "create",
80102
- role: "admin"
80103
- },
80104
79992
  "integrations.update": {
80105
79993
  capName: "integrations",
80106
79994
  capScope: "system",
@@ -81333,13 +81221,6 @@ Object.freeze({
81333
81221
  access: "create",
81334
81222
  role: "admin"
81335
81223
  },
81336
- "mqttBroker.testConnection": {
81337
- capName: "mqtt-broker",
81338
- capScope: "system",
81339
- addonId: null,
81340
- access: "create",
81341
- role: "admin"
81342
- },
81343
81224
  "nativeObjectDetection.getOptions": {
81344
81225
  capName: "native-object-detection",
81345
81226
  capScope: "device",
@@ -86170,20 +86051,6 @@ Object.freeze({
86170
86051
  access: "create",
86171
86052
  role: "admin"
86172
86053
  },
86173
- "trainingProvider.createEntry": {
86174
- capName: "training-provider",
86175
- capScope: "system",
86176
- addonId: null,
86177
- access: "create",
86178
- role: "admin"
86179
- },
86180
- "trainingProvider.deleteEntry": {
86181
- capName: "training-provider",
86182
- capScope: "system",
86183
- addonId: null,
86184
- access: "delete",
86185
- role: "admin"
86186
- },
86187
86054
  "trainingProvider.describe": {
86188
86055
  capName: "training-provider",
86189
86056
  capScope: "system",
@@ -86254,13 +86121,6 @@ Object.freeze({
86254
86121
  access: "create",
86255
86122
  role: "admin"
86256
86123
  },
86257
- "trainingProvider.updateEntry": {
86258
- capName: "training-provider",
86259
- capScope: "system",
86260
- addonId: null,
86261
- access: "create",
86262
- role: "admin"
86263
- },
86264
86124
  "trainingProvider.uploadDataset": {
86265
86125
  capName: "training-provider",
86266
86126
  capScope: "system",
@@ -86268,13 +86128,6 @@ Object.freeze({
86268
86128
  access: "create",
86269
86129
  role: "admin"
86270
86130
  },
86271
- "trainingProvider.validateCredentials": {
86272
- capName: "training-provider",
86273
- capScope: "system",
86274
- addonId: null,
86275
- access: "create",
86276
- role: "admin"
86277
- },
86278
86131
  "turnProvider.getTurnServers": {
86279
86132
  capName: "turn-provider",
86280
86133
  capScope: "system",
@@ -123262,7 +123115,7 @@ var BrokerProbeCache = class {
123262
123115
  }
123263
123116
  if (this.inflight) await this.inflight.catch(() => void 0);
123264
123117
  }
123265
- /** Force a refresh (used by `testConnection` and the page's Refresh button). */
123118
+ /** Force a refresh (used by the page's Refresh button). */
123266
123119
  async probeNow(id) {
123267
123120
  const broker = this.getBrokers().find((b) => b.id === id);
123268
123121
  if (!broker) return void 0;
@@ -124244,8 +124097,8 @@ var MqttAclStore = class MqttAclStore {
124244
124097
  * - The legacy single embedded broker is an onboard `brokers` record (migrated from the flat
124245
124098
  * `embedded*` keys, same id); `startEmbeddedBroker` / `stopEmbeddedBroker` act on that record.
124246
124099
  * - `addBroker` / `removeBroker` mutate the records and the runtime state.
124247
- * - `testConnection` does a one-shot `mqtt.connect` race against a 3s
124248
- * timeout AND refreshes the probe cache for the broker.
124100
+ * - The instance test (`integration-instances.testInstance`) does a one-shot `mqtt.connect`
124101
+ * race against a 3s timeout.
124249
124102
  */
124250
124103
  /** bcrypt cost factor — exported for tests. */
124251
124104
  var MQTT_EMBEDDED_BCRYPT_ROUNDS = 10;
@@ -124381,7 +124234,6 @@ var MqttBrokerAddon = class extends BaseAddon {
124381
124234
  getBrokerConfig: async ({ id }) => this.getBrokerConfig(id),
124382
124235
  addBroker: async (input) => this.addBroker(input),
124383
124236
  removeBroker: async ({ id }) => this.removeBroker(id),
124384
- testConnection: async ({ id }) => this.testConnection(id),
124385
124237
  startEmbeddedBroker: async (input) => this.startEmbeddedBroker(input),
124386
124238
  stopEmbeddedBroker: async ({ id }) => this.stopEmbeddedBroker(id),
124387
124239
  getStatus: async () => ({
@@ -124524,24 +124376,10 @@ var MqttBrokerAddon = class extends BaseAddon {
124524
124376
  return;
124525
124377
  }
124526
124378
  }
124527
- async testConnection(id) {
124528
- let cfg;
124529
- try {
124530
- cfg = this.getBrokerConfig(id);
124531
- } catch (err) {
124532
- return {
124533
- ok: false,
124534
- error: errMsg(err)
124535
- };
124536
- }
124537
- this.probeCache?.probeNow(id).catch(() => void 0);
124538
- return this.probeMqtt(cfg);
124539
- }
124540
124379
  /**
124541
124380
  * One-shot connectivity probe against an MQTT endpoint built from a
124542
- * flat config (`{url, username?, password?}`). Shared by the
124543
- * id-keyed `testConnection` (existing broker) and the pre-creation
124544
- * the instance test (`testInstance`) of a broker not saved yet (the typed values).
124381
+ * flat config (`{url, username?, password?}`), used by the instance test
124382
+ * (`integration-instances.testInstance`), stored or draft.
124545
124383
  * Never throws — every failure resolves to `{ ok: false, error }`.
124546
124384
  */
124547
124385
  async probeMqtt(cfg) {