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