@camstack/addon-agent-ui 1.2.334 → 1.2.336

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -5849,7 +5849,7 @@ var ZodIssueCode = {
5849
5849
  var ZodFirstPartyTypeKind;
5850
5850
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5851
5851
  //#endregion
5852
- //#region ../types/dist/sleep-CmjWurK5.mjs
5852
+ //#region ../types/dist/sleep-D7ZKm4nb.mjs
5853
5853
  /**
5854
5854
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5855
5855
  * window to float samples (D455).
@@ -8897,96 +8897,6 @@ function normalizeAddonInitResult(result) {
8897
8897
  if (Array.isArray(result)) return { providers: result };
8898
8898
  return result;
8899
8899
  }
8900
- /**
8901
- * The share-view scope kinds a share-link token can carry. The hub mints
8902
- * and validates them (`ShareViewGrantSchema` builds its
8903
- * enum from this list); a data plane names the kinds it ACCEPTS in `share`.
8904
- */
8905
- var SHARE_VIEW_KINDS = ["grid-view", "events-view"];
8906
- /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
8907
- var PeerBytesTicketSchema = object({
8908
- /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
8909
- url: string().min(1),
8910
- /**
8911
- * The HOST node this URL means something on — the hub or a named agent,
8912
- * never a runner. {@link AddonPeerBytes.open} compares it to its own and
8913
- * refuses `cross-node` by name when they differ, without dialling.
8914
- */
8915
- hostNodeId: string().min(1),
8916
- expiresAtMs: number().int().nonnegative(),
8917
- /**
8918
- * What the producer DECLARED the body to be, when it knows — `null` when it
8919
- * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
8920
- * must be able to tell "the producer did not say" from "the body is empty".
8921
- */
8922
- declaredBytes: number().int().nonnegative().nullable()
8923
- });
8924
- /**
8925
- * Record kinds (D881): the declaration an addon makes of one kind of fact, the
8926
- * Zod row schema that describes it, and the helper that turns the two into the
8927
- * JSON the engine stores.
8928
- *
8929
- * A kind is declared ONCE, next to the code that owns it, and the engine's
8930
- * reconciler creates its indexes - nothing creates DDL at runtime from a write.
8931
- * Everything the generic machinery needs (secret fields, the entry descriptor)
8932
- * is DERIVED from the Zod schema: `.meta({ secret: true })` marks a credential,
8933
- * never a field name (D879).
8934
- */
8935
- /** A JSON field path: `trackId`, `auth.accessToken`. Charset-restricted: it is rendered into DDL. */
8936
- var RecordFieldPathSchema = string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,63}(\.[A-Za-z_][A-Za-z0-9_]{0,63}){0,3}$/);
8937
- var RecordIndexSchema = object({
8938
- name: string().regex(/^[a-z][a-z0-9_]{0,31}$/),
8939
- fields: array(RecordFieldPathSchema).min(1).max(4),
8940
- unique: boolean().optional()
8941
- }).strict();
8942
- var RecordRetentionSchema = union([
8943
- object({
8944
- ttlMs: number().int().positive(),
8945
- from: _enum(["at", "write"])
8946
- }).strict(),
8947
- object({
8948
- maxRows: number().int().positive().optional(),
8949
- maxRowsPerDevice: number().int().positive().optional()
8950
- }).strict().refine((r) => r.maxRows !== void 0 || r.maxRowsPerDevice !== void 0, { message: "name maxRows, maxRowsPerDevice or both" }),
8951
- object({
8952
- owner: string().min(1),
8953
- reason: string().min(1)
8954
- }).strict(),
8955
- object({ none: string().min(1) }).strict()
8956
- ]);
8957
- /** The descriptor a generic entries UI reads (D881 "generic entries"). */
8958
- var EntryDescriptorSchema = object({
8959
- title: string().min(1),
8960
- /** The record body field shown as the entry's name. */
8961
- nameField: string().min(1),
8962
- /** JSON Schema of the entry body; `secret: true` marks a password field. */
8963
- fields: record(string(), unknown())
8964
- }).strict();
8965
- /** What crosses the wire from an addon: JSON only, no Zod. An unknown key is refused. */
8966
- var RecordKindDeclarationSchema = object({
8967
- database: _enum(RECORD_DATABASES).default("settings"),
8968
- class: _enum(RECORD_CLASSES),
8969
- writeTrigger: string().min(1),
8970
- device: _enum([
8971
- "required",
8972
- "optional",
8973
- "none"
8974
- ]).default("none"),
8975
- at: _enum(["required", "none"]).default("none"),
8976
- indexes: array(RecordIndexSchema).max(8).default([]),
8977
- retention: RecordRetentionSchema,
8978
- deviceRemoval: _enum(["delete", "none"]).default("none"),
8979
- maxDataBytes: number().int().positive().max(RECORD_MAX_DATA_BYTES).default(RECORD_DEFAULT_MAX_DATA_BYTES),
8980
- /** Derived from the Zod schema; never written by hand. */
8981
- secretPaths: array(string().min(1)).default([]),
8982
- /** The id key of each array that carries a secret (`{ headers: 'name' }`). */
8983
- secretArrayKeys: record(string(), string()).default({}),
8984
- /** Kinds that must agree through `transact` sit in one file under one family name. */
8985
- transactFamily: string().min(1).optional(),
8986
- entry: EntryDescriptorSchema.optional()
8987
- }).strict();
8988
- /** The kind names: lower-case, dash separated, short enough for an index name. */
8989
- var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8990
8900
  object({
8991
8901
  field: string().min(1),
8992
8902
  on: literal("advance")
@@ -9075,6 +8985,96 @@ function optionalMethod(input, output, options) {
9075
8985
  providerOptional: true
9076
8986
  };
9077
8987
  }
8988
+ /**
8989
+ * Record kinds (D881): the declaration an addon makes of one kind of fact, the
8990
+ * Zod row schema that describes it, and the helper that turns the two into the
8991
+ * JSON the engine stores.
8992
+ *
8993
+ * A kind is declared ONCE, next to the code that owns it, and the engine's
8994
+ * reconciler creates its indexes - nothing creates DDL at runtime from a write.
8995
+ * Everything the generic machinery needs (secret fields, the entry descriptor)
8996
+ * is DERIVED from the Zod schema: `.meta({ secret: true })` marks a credential,
8997
+ * never a field name (D879).
8998
+ */
8999
+ /** A JSON field path: `trackId`, `auth.accessToken`. Charset-restricted: it is rendered into DDL. */
9000
+ var RecordFieldPathSchema = string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,63}(\.[A-Za-z_][A-Za-z0-9_]{0,63}){0,3}$/);
9001
+ var RecordIndexSchema = object({
9002
+ name: string().regex(/^[a-z][a-z0-9_]{0,31}$/),
9003
+ fields: array(RecordFieldPathSchema).min(1).max(4),
9004
+ unique: boolean().optional()
9005
+ }).strict();
9006
+ var RecordRetentionSchema = union([
9007
+ object({
9008
+ ttlMs: number().int().positive(),
9009
+ from: _enum(["at", "write"])
9010
+ }).strict(),
9011
+ object({
9012
+ maxRows: number().int().positive().optional(),
9013
+ maxRowsPerDevice: number().int().positive().optional()
9014
+ }).strict().refine((r) => r.maxRows !== void 0 || r.maxRowsPerDevice !== void 0, { message: "name maxRows, maxRowsPerDevice or both" }),
9015
+ object({
9016
+ owner: string().min(1),
9017
+ reason: string().min(1)
9018
+ }).strict(),
9019
+ object({ none: string().min(1) }).strict()
9020
+ ]);
9021
+ /** The descriptor a generic entries UI reads (D881 "generic entries"). */
9022
+ var EntryDescriptorSchema = object({
9023
+ title: string().min(1),
9024
+ /** The record body field shown as the entry's name. */
9025
+ nameField: string().min(1),
9026
+ /** JSON Schema of the entry body; `secret: true` marks a password field. */
9027
+ fields: record(string(), unknown())
9028
+ }).strict();
9029
+ /** What crosses the wire from an addon: JSON only, no Zod. An unknown key is refused. */
9030
+ var RecordKindDeclarationSchema = object({
9031
+ database: _enum(RECORD_DATABASES).default("settings"),
9032
+ class: _enum(RECORD_CLASSES),
9033
+ writeTrigger: string().min(1),
9034
+ device: _enum([
9035
+ "required",
9036
+ "optional",
9037
+ "none"
9038
+ ]).default("none"),
9039
+ at: _enum(["required", "none"]).default("none"),
9040
+ indexes: array(RecordIndexSchema).max(8).default([]),
9041
+ retention: RecordRetentionSchema,
9042
+ deviceRemoval: _enum(["delete", "none"]).default("none"),
9043
+ maxDataBytes: number().int().positive().max(RECORD_MAX_DATA_BYTES).default(RECORD_DEFAULT_MAX_DATA_BYTES),
9044
+ /** Derived from the Zod schema; never written by hand. */
9045
+ secretPaths: array(string().min(1)).default([]),
9046
+ /** The id key of each array that carries a secret (`{ headers: 'name' }`). */
9047
+ secretArrayKeys: record(string(), string()).default({}),
9048
+ /** Kinds that must agree through `transact` sit in one file under one family name. */
9049
+ transactFamily: string().min(1).optional(),
9050
+ entry: EntryDescriptorSchema.optional()
9051
+ }).strict();
9052
+ /** The kind names: lower-case, dash separated, short enough for an index name. */
9053
+ var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
9054
+ /**
9055
+ * The share-view scope kinds a share-link token can carry. The hub mints
9056
+ * and validates them (`ShareViewGrantSchema` builds its
9057
+ * enum from this list); a data plane names the kinds it ACCEPTS in `share`.
9058
+ */
9059
+ var SHARE_VIEW_KINDS = ["grid-view", "events-view"];
9060
+ /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
9061
+ var PeerBytesTicketSchema = object({
9062
+ /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
9063
+ url: string().min(1),
9064
+ /**
9065
+ * The HOST node this URL means something on — the hub or a named agent,
9066
+ * never a runner. {@link AddonPeerBytes.open} compares it to its own and
9067
+ * refuses `cross-node` by name when they differ, without dialling.
9068
+ */
9069
+ hostNodeId: string().min(1),
9070
+ expiresAtMs: number().int().nonnegative(),
9071
+ /**
9072
+ * What the producer DECLARED the body to be, when it knows — `null` when it
9073
+ * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
9074
+ * must be able to tell "the producer did not say" from "the body is empty".
9075
+ */
9076
+ declaredBytes: number().int().nonnegative().nullable()
9077
+ });
9078
9078
  var READING_QUANTITIES = [
9079
9079
  "temperature",
9080
9080
  "pressure",
@@ -9671,7 +9671,6 @@ var CAP_NAMES = Object.freeze([
9671
9671
  "climate-control",
9672
9672
  "color",
9673
9673
  "composer",
9674
- "connection-test",
9675
9674
  "connectivity",
9676
9675
  "consumables",
9677
9676
  "contact",
@@ -11691,8 +11690,7 @@ Object.freeze({
11691
11690
  "offerArtifact": 3e5,
11692
11691
  "readResult": 3e5,
11693
11692
  "submitJob": 18e5,
11694
- "uploadDataset": 72e5,
11695
- "validateCredentials": 3e4
11693
+ "uploadDataset": 72e5
11696
11694
  }
11697
11695
  });
11698
11696
  object({
@@ -13047,6 +13045,432 @@ var IntegrationInstanceRefSchema = object({
13047
13045
  ]),
13048
13046
  candidateStableId: string().optional()
13049
13047
  });
13048
+ /**
13049
+ * notification-output — canonical, capability-gated notification delivery.
13050
+ *
13051
+ * Apprise-derived model (see
13052
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
13053
+ * callers emit ONE canonical `Notification`; each provider declares a
13054
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
13055
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
13056
+ * message to what the kind supports — callers never special-case a service.
13057
+ *
13058
+ * DESIGN DECISIONS (locked):
13059
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
13060
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
13061
+ * cap. Rationale: the admin UI needs one uniform surface across the
13062
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
13063
+ * alternative would fork the UI per addon and cannot host the
13064
+ * discovery→adopt flow.
13065
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
13066
+ * the generated cap-mount auto-`concatCollection`-fans them across every
13067
+ * registered provider (notifiers addon + HA addon) so one catalog is
13068
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
13069
+ * `addonId` the generated collection router extracts from the call input.
13070
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
13071
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
13072
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
13073
+ * base64 fallback needed.
13074
+ *
13075
+ * TODO (deferred, closed-set change — separate decision): add
13076
+ * `providerKind: 'notify'` so notification providers surface on the unified
13077
+ * admin "Integrations" page.
13078
+ */
13079
+ /**
13080
+ * Zentik-derived typed-media enum — the superset across every kind. Each
13081
+ * adapter picks what it supports and the degrade engine filters the rest.
13082
+ */
13083
+ var AttachmentMediaTypeSchema = _enum([
13084
+ "image",
13085
+ "video",
13086
+ "gif",
13087
+ "audio",
13088
+ "icon"
13089
+ ]);
13090
+ /**
13091
+ * A single attachment. Exactly one of `url` (remote source, most adapters
13092
+ * prefer this) or `bytes` (inline source; required for Pushover-style
13093
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
13094
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
13095
+ */
13096
+ var AttachmentSchema = object({
13097
+ mediaType: AttachmentMediaTypeSchema,
13098
+ url: string().optional(),
13099
+ bytes: _instanceof(Uint8Array).optional(),
13100
+ mime: string().optional(),
13101
+ name: string().optional()
13102
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
13103
+ var NotificationFormatSchema = _enum([
13104
+ "text",
13105
+ "markdown",
13106
+ "html"
13107
+ ]);
13108
+ /**
13109
+ * The CLOSED icon vocabulary an action button may use.
13110
+ *
13111
+ * A closed set, not a free string, and that is the whole point: an arbitrary
13112
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
13113
+ * notices — the same class of gap as a zone vocabulary nothing produced
13114
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
13115
+ * declares `actionIcons: false` and the degrade engine strips the field.
13116
+ *
13117
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
13118
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
13119
+ * differently.
13120
+ *
13121
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
13122
+ *
13123
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
13124
+ * the whole set onto its own renderer's vocabulary through a
13125
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
13126
+ * fallback, so adding a member here fails every adapter's build until someone
13127
+ * decides its glyph, which is the only place that decision can be made
13128
+ * honestly.
13129
+ *
13130
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
13131
+ * `disarm` straight through; iOS feeds that string to
13132
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
13133
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
13134
+ * mapping, and "the field is documented" is not "the value renders".
13135
+ *
13136
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
13137
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
13138
+ * so an addon that emits a token the running hub does not know does not lose an
13139
+ * icon — its whole `send` fails Zod validation and the notification never
13140
+ * arrives. Never emit a new token from an addon before the train carrying it.
13141
+ */
13142
+ var NotificationActionIconSchema = _enum([
13143
+ "acknowledge",
13144
+ "dismiss",
13145
+ "silence",
13146
+ "snooze",
13147
+ "view",
13148
+ "play",
13149
+ "open",
13150
+ "close",
13151
+ "lock",
13152
+ "unlock",
13153
+ "arm",
13154
+ "arm-home",
13155
+ "arm-away",
13156
+ "arm-night",
13157
+ "disarm",
13158
+ "light",
13159
+ "alert",
13160
+ "camera"
13161
+ ]);
13162
+ /** A single tap-through action button. */
13163
+ var NotificationActionSchema = object({
13164
+ id: string(),
13165
+ label: string(),
13166
+ url: string().optional(),
13167
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
13168
+ icon: NotificationActionIconSchema.optional(),
13169
+ /**
13170
+ * Renders in a warning style where the notifier supports it.
13171
+ *
13172
+ * A HINT, never a gate. The callback's authority is its token and nothing
13173
+ * else — see `notification-center/action-token.ts` for what that does and
13174
+ * does not buy.
13175
+ */
13176
+ destructive: boolean().optional(),
13177
+ /**
13178
+ * How the tap should REACH the url.
13179
+ *
13180
+ * `navigate` (absent, and every button authored before this field) opens it:
13181
+ * the phone leaves the notification and shows whatever the callback returns.
13182
+ * That is right for a button whose answer the operator wants to read.
13183
+ *
13184
+ * `background` fires it as a POST and stays put. It exists for the buttons
13185
+ * whose whole point is not to interrupt — "silence this for 30 minutes" is
13186
+ * an answer to the notification, and being thrown into a browser tab to
13187
+ * confirm it costs more attention than the notification did. A backend that
13188
+ * cannot do a background call renders it as an ordinary link (the adapters
13189
+ * fall back rather than dropping the button), so this is a preference, never
13190
+ * a requirement.
13191
+ */
13192
+ mode: _enum(["navigate", "background"]).optional()
13193
+ });
13194
+ /** A delivery parameter value: what a notifier declared it takes. */
13195
+ var DeliveryParamValueSchema = union([
13196
+ string(),
13197
+ number(),
13198
+ boolean()
13199
+ ]);
13200
+ /**
13201
+ * One extra delivery parameter a notifier KIND declares (D938). A rule that
13202
+ * addresses a target of that kind may set it; a kind that declares nothing is
13203
+ * never handed one. The admin UI derives its control from this descriptor —
13204
+ * no editor knows a service by name.
13205
+ *
13206
+ * Optional fields are `.optional()`, never `.default()` (a Zod default does not
13207
+ * run on the addon cap path).
13208
+ */
13209
+ var DeliveryParamSchema = object({
13210
+ key: string().min(1),
13211
+ label: string(),
13212
+ /** i18n key of the label, resolved by the declaring addon's namespace. */
13213
+ labelKey: string().optional(),
13214
+ type: _enum([
13215
+ "enum",
13216
+ "number",
13217
+ "boolean",
13218
+ "string"
13219
+ ]),
13220
+ /** `enum` only: the closed set of accepted values. */
13221
+ options: array(object({
13222
+ value: string(),
13223
+ label: string()
13224
+ })).optional(),
13225
+ /** `number` only. */
13226
+ min: number().optional(),
13227
+ max: number().optional(),
13228
+ /** What the notifier does when the rule sets nothing; shown as the placeholder. */
13229
+ default: DeliveryParamValueSchema.optional()
13230
+ });
13231
+ /**
13232
+ * The canonical notification. `body` is the only hard field (Apprise model).
13233
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
13234
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
13235
+ * the adapter maps this ordinal onto its native level. `level?` is an
13236
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
13237
+ * `priority` for that one target.
13238
+ */
13239
+ var NotificationSchema = object({
13240
+ body: string(),
13241
+ title: string().optional(),
13242
+ format: NotificationFormatSchema.default("text"),
13243
+ priority: number().int().min(1).max(5).default(3),
13244
+ level: string().optional(),
13245
+ attachments: array(AttachmentSchema).optional(),
13246
+ clickUrl: string().optional(),
13247
+ actions: array(NotificationActionSchema).optional(),
13248
+ sound: string().optional(),
13249
+ ttl: number().optional(),
13250
+ tag: string().optional(),
13251
+ deviceId: number().optional(),
13252
+ eventId: string().optional(),
13253
+ metadata: record(string(), unknown()).optional(),
13254
+ /**
13255
+ * Values for the parameters the target's kind DECLARES in
13256
+ * {@link TargetKindSchema} `deliveryParams`. Already validated against the
13257
+ * declaration by the sender; a notifier reads only the keys it declared.
13258
+ */
13259
+ delivery: record(string(), DeliveryParamValueSchema).optional()
13260
+ });
13261
+ /** One declared native severity/priority level for a kind. */
13262
+ var TargetKindLevelSchema = object({
13263
+ id: string(),
13264
+ label: string(),
13265
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
13266
+ ordinal: number().int().min(1).max(5).nullable(),
13267
+ flags: object({
13268
+ critical: boolean().optional(),
13269
+ silent: boolean().optional(),
13270
+ noPush: boolean().optional()
13271
+ }).optional(),
13272
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
13273
+ requires: array(string()).optional(),
13274
+ description: string().optional()
13275
+ });
13276
+ /** The full capability block consulted before dispatch. */
13277
+ var TargetKindCapsSchema = object({
13278
+ attachments: object({
13279
+ mediaTypes: array(AttachmentMediaTypeSchema),
13280
+ mode: _enum([
13281
+ "url",
13282
+ "bytes",
13283
+ "both"
13284
+ ]),
13285
+ max: number().int().nonnegative(),
13286
+ maxBytes: number().int().positive().optional()
13287
+ }),
13288
+ /** Max action buttons (0 = none). */
13289
+ actions: number().int().nonnegative(),
13290
+ /**
13291
+ * Whether this kind renders a per-action ICON.
13292
+ *
13293
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
13294
+ * run on the addon cap path — three production failures in one day taught
13295
+ * this repo that once. Absent is read as false by the degrade engine, which
13296
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
13297
+ * assumed and dropped costs the operator's trust in the field.
13298
+ */
13299
+ actionIcons: boolean().optional(),
13300
+ levels: array(TargetKindLevelSchema),
13301
+ format: array(NotificationFormatSchema),
13302
+ clickUrl: boolean(),
13303
+ sound: boolean(),
13304
+ ttl: boolean(),
13305
+ bodyMaxLen: number().int().positive()
13306
+ });
13307
+ /**
13308
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
13309
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
13310
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
13311
+ * the union is large and not meant for runtime validation here; the exported
13312
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
13313
+ */
13314
+ var ConfigSchemaPassthrough$4 = unknown();
13315
+ var TargetKindSchema = object({
13316
+ kind: string(),
13317
+ label: string(),
13318
+ icon: string(),
13319
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
13320
+ addonId: string(),
13321
+ /**
13322
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
13323
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
13324
+ * when the addon bundles no icon for that kind — the client then falls back
13325
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
13326
+ *
13327
+ * Root-relative on purpose: it resolves against whatever origin serves a web
13328
+ * client, and a native client joins it onto its own hub base.
13329
+ *
13330
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
13331
+ * field that survived only because the runtime cap-router forwards provider
13332
+ * output verbatim — so every consumer had to re-declare it by hand to stop
13333
+ * its own Zod parse from stripping it, and the whole arrangement would have
13334
+ * broken silently the moment output validation was tightened anywhere.
13335
+ */
13336
+ iconUrl: string().optional(),
13337
+ /**
13338
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
13339
+ *
13340
+ * The server knows this and therefore says it, because the client cannot
13341
+ * safely guess: a React-Native client renders SVG and raster through two
13342
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
13343
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
13344
+ * placeholder glyph for every vector icon while the web build looked fine.
13345
+ *
13346
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
13347
+ * not been updated — a client that cannot determine the type should prefer
13348
+ * its raster path, which is the safe default for an unknown image.
13349
+ */
13350
+ iconMediaType: string().optional(),
13351
+ configSchema: ConfigSchemaPassthrough$4,
13352
+ /**
13353
+ * The config keys that decide WHERE this kind's secrets are sent (server,
13354
+ * base URL, instance, chat, recipient). A save that changes any of them
13355
+ * while leaving a secret redacted is refused by the provider (D634), so an
13356
+ * editor uses this to ask for the secret on exactly those edits instead of
13357
+ * on every edit.
13358
+ *
13359
+ * Optional: absent from a provider that holds no secret (the Home Assistant
13360
+ * kinds) and from a provider older than this field — an editor that finds
13361
+ * it absent must assume ANY edit may need the secret.
13362
+ */
13363
+ destinationKeys: array(string()).optional(),
13364
+ supportsDiscovery: boolean(),
13365
+ /**
13366
+ * Extra delivery parameters a rule may set for a target of this kind (D938).
13367
+ * Absent = none; the rule editor then offers nothing.
13368
+ */
13369
+ deliveryParams: array(DeliveryParamSchema).optional(),
13370
+ caps: TargetKindCapsSchema
13371
+ });
13372
+ /**
13373
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
13374
+ * (return a presence marker only) when serving `listTargets` — never
13375
+ * round-trip a stored secret to the UI.
13376
+ */
13377
+ var TargetSchema = object({
13378
+ id: string(),
13379
+ name: string(),
13380
+ kind: string(),
13381
+ addonId: string(),
13382
+ enabled: boolean(),
13383
+ config: record(string(), unknown())
13384
+ });
13385
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
13386
+ var DiscoveredTargetSchema = object({
13387
+ kind: string(),
13388
+ suggestedName: string(),
13389
+ config: record(string(), unknown())
13390
+ });
13391
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
13392
+ var RenderedAsSchema = object({
13393
+ level: string(),
13394
+ format: NotificationFormatSchema,
13395
+ attachmentsSent: number().int().nonnegative(),
13396
+ actionsSent: number().int().nonnegative(),
13397
+ truncated: boolean(),
13398
+ dropped: array(string())
13399
+ });
13400
+ var SendResultSchema = object({
13401
+ success: boolean(),
13402
+ error: string().optional(),
13403
+ renderedAs: RenderedAsSchema.optional()
13404
+ });
13405
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
13406
+ var TestResultSchema = SendResultSchema;
13407
+ /**
13408
+ * Pre-save credential probe against a partial (unsaved) target config.
13409
+ * `resolvedUrl` never includes secrets (token stripped from path).
13410
+ */
13411
+ var ProbeTargetConfigResultSchema = object({
13412
+ ok: boolean(),
13413
+ resolvedUrl: string(),
13414
+ httpStatus: number().int().optional(),
13415
+ stateInstance: string().optional(),
13416
+ error: string().optional()
13417
+ });
13418
+ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
13419
+ kind: string(),
13420
+ config: record(string(), unknown()).optional(),
13421
+ /**
13422
+ * When editing / duplicating, the blank password field never round-trips
13423
+ * the stored token. Pass the source target id so the provider can restore
13424
+ * secrets server-side before dialling (same merge as upsert).
13425
+ */
13426
+ targetId: string().optional()
13427
+ }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
13428
+ kind: string(),
13429
+ config: record(string(), unknown()),
13430
+ /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
13431
+ targetId: string().optional()
13432
+ }), ProbeTargetConfigResultSchema, {
13433
+ kind: "mutation",
13434
+ auth: "admin"
13435
+ }), method(object({
13436
+ targetId: string(),
13437
+ notification: NotificationSchema
13438
+ }), SendResultSchema, {
13439
+ kind: "mutation",
13440
+ auth: "admin"
13441
+ }), method(object({
13442
+ targetId: string(),
13443
+ sample: NotificationSchema.optional()
13444
+ }), TestResultSchema, {
13445
+ kind: "mutation",
13446
+ auth: "admin"
13447
+ }), method(object({
13448
+ target: TargetSchema,
13449
+ /**
13450
+ * Duplicate flow: create a NEW target id while copying secrets from an
13451
+ * existing row. The UI never holds the stored token; blank / redacted
13452
+ * password fields are restored from this source (D634 still applies if
13453
+ * destinations diverge without a re-typed secret).
13454
+ */
13455
+ cloneSecretsFrom: string().optional()
13456
+ }), TargetSchema, {
13457
+ kind: "mutation",
13458
+ auth: "admin"
13459
+ }), method(object({ targetId: string() }), _void(), {
13460
+ kind: "mutation",
13461
+ auth: "admin"
13462
+ }), method(object({
13463
+ targetId: string(),
13464
+ enabled: boolean()
13465
+ }), _void(), {
13466
+ kind: "mutation",
13467
+ auth: "admin"
13468
+ });
13469
+ looseObject({
13470
+ name: string().min(1),
13471
+ enabled: boolean().optional()
13472
+ });
13473
+ TargetSchema.omit({ id: true });
13050
13474
  var EncodeProfileSchema = object({
13051
13475
  video: object({
13052
13476
  codec: _enum([
@@ -21254,7 +21678,6 @@ var SYSTEM_CAP_VOCABULARY = {
21254
21678
  "calendar-source": system(INTERNAL),
21255
21679
  calendars: system("calendars feed schedules and conditions, not device actions"),
21256
21680
  composer: system("device composition — an authoring tool, not an action"),
21257
- "connection-test": system(INTERNAL),
21258
21681
  "core-blocks": system(INTERNAL),
21259
21682
  "custom-model-registry": system(ML),
21260
21683
  "data-store-provider": system(INTERNAL),
@@ -42934,663 +43357,242 @@ var AutomationRunVerdictSchema = object({
42934
43357
  decision: _enum([
42935
43358
  "run",
42936
43359
  "queued",
42937
- "restart",
42938
- "drop",
42939
- "cooldown",
42940
- "bypass"
42941
- ]),
42942
- reason: string()
42943
- });
42944
- /** One device of a multi-device step: did the call land. */
42945
- var StepDeviceTraceSchema = object({
42946
- deviceId: number().int(),
42947
- outcome: _enum(["ok", "error"]),
42948
- message: string().optional()
42949
- });
42950
- var AutomationStepTraceSchema = object({
42951
- /** `actions.<i>` */
42952
- path: string().min(1),
42953
- kind: string().min(1),
42954
- cap: string().optional(),
42955
- method: string().optional(),
42956
- /** The vocabulary action the step performs (`cover.open`), when it names one. */
42957
- actionId: string().optional(),
42958
- deviceId: number().int().optional(),
42959
- /** What happened per device, when the step drove several (or a domain). */
42960
- devices: array(StepDeviceTraceSchema).optional(),
42961
- startedAt: number().optional(),
42962
- endedAt: number().optional(),
42963
- /**
42964
- * The parameters the step SENT, after `{{vars.x}}` substitution — what the
42965
- * device was actually asked. Bounded like trigger `vars`; a secret parameter
42966
- * is shown as {@link REDACTED_ARG}. Absent on traces written before it existed.
42967
- */
42968
- args: record(string(), TraceValueSchema).optional(),
42969
- /** `empty`: the target resolved to no device, so nothing was done — never silently `ok`. */
42970
- outcome: _enum([
42971
- "ok",
42972
- "error",
42973
- "not-run",
42974
- "empty"
42975
- ]),
42976
- message: string().optional()
42977
- });
42978
- var AutomationRunTraceSchema = object({
42979
- runId: string().min(1),
42980
- startedAt: number(),
42981
- endedAt: number(),
42982
- trigger: AutomationTriggerTraceSchema,
42983
- conditions: array(AutomationConditionTraceNodeSchema),
42984
- verdict: AutomationRunVerdictSchema.optional(),
42985
- steps: array(AutomationStepTraceSchema),
42986
- outcome: AutomationRunOutcomeSchema,
42987
- /** How many identical consecutive skips this trace stands for (>= 1). */
42988
- repeat: number().int().min(1),
42989
- /** When the latest of those repeats happened. */
42990
- lastAt: number(),
42991
- /**
42992
- * A scheduler point's own outcome (D983). `outcome` carries the closest
42993
- * automation outcome so a reader that does not know schedulers still reads
42994
- * the trace; this field says exactly what happened.
42995
- */
42996
- scheduler: SchedulerOutcomeSchema.optional()
42997
- });
42998
- /**
42999
- * What the hub's `runs` ledger holds for one owner (`automation:<blockId>` or
43000
- * `action:<actionId>`): the cooldown stamp and the trace ring.
43001
- */
43002
- var AutomationStateDocSchema = object({
43003
- version: literal(1),
43004
- lastRunStartedAt: number().nullable(),
43005
- traces: array(AutomationRunTraceSchema)
43006
- });
43007
- /**
43008
- * Actions — named, runnable things an operator (or an automation) can start,
43009
- * declared as LIST ENTRIES, never as devices.
43010
- *
43011
- * An entry is either **internal** (a short list of cap/wait steps kept in the
43012
- * hub's `actions` record kind) or declared by an addon through the
43013
- * `action-source` collection (a Home Assistant script, a scene …). The one
43014
- * surface the UI and automations talk to is the `actions` cap; it keeps no
43015
- * catalog of its own (D3) — every `list` asks the providers live.
43016
- *
43017
- * This file is the data half: the entry schema, the variables an entry may
43018
- * declare, and the pure resolution of the values a run was given. It holds no
43019
- * runtime.
43020
- */
43021
- /** What a variable's value is. `select` takes one of `options`. */
43022
- var ActionVariableTypeSchema = _enum([
43023
- "string",
43024
- "number",
43025
- "boolean",
43026
- "select"
43027
- ]);
43028
- var ACTION_VARIABLE_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
43029
- var ActionVariableValueSchema = union([
43030
- string(),
43031
- number(),
43032
- boolean()
43033
- ]);
43034
- /**
43035
- * One declared variable. Steps reference it as `{ $var: 'name' }` (the whole
43036
- * value) or `{{vars.name}}` (inside a string) in their args.
43037
- */
43038
- var ActionVariableSchema = object({
43039
- name: string().regex(ACTION_VARIABLE_NAME_RE),
43040
- label: string().min(1).max(120).optional(),
43041
- type: ActionVariableTypeSchema,
43042
- required: boolean().optional(),
43043
- default: ActionVariableValueSchema.optional(),
43044
- /** The allowed values of a `select`. */
43045
- options: array(string().min(1)).min(1).optional()
43046
- }).superRefine((v, ctx) => {
43047
- if (v.type === "select" && v.options === void 0) ctx.addIssue({
43048
- code: "custom",
43049
- message: "a select variable needs options",
43050
- path: ["options"]
43051
- });
43052
- if (v.default !== void 0 && variableValueError(v, v.default) !== null) ctx.addIssue({
43053
- code: "custom",
43054
- message: variableValueError(v, v.default) ?? "invalid default",
43055
- path: ["default"]
43056
- });
43057
- });
43058
- /** Why `value` is not a legal value for the variable, or null when it is. */
43059
- function variableValueError(variable, value) {
43060
- switch (variable.type) {
43061
- case "string": return typeof value === "string" ? null : `variable "${variable.name}" must be a string`;
43062
- case "number": return typeof value === "number" && Number.isFinite(value) ? null : `variable "${variable.name}" must be a number`;
43063
- case "boolean": return typeof value === "boolean" ? null : `variable "${variable.name}" must be true or false`;
43064
- case "select": return typeof value === "string" && (variable.options ?? []).includes(value) ? null : `variable "${variable.name}" must be one of ${(variable.options ?? []).join(", ")}`;
43065
- }
43066
- }
43067
- var ActionSourceKindSchema = _enum(["internal", "addon"]);
43068
- function isInternalActionId(actionId) {
43069
- return actionId.startsWith("internal:") && actionId.length > 9;
43070
- }
43071
- /** The addon behind a `<addonId>:<localId>` id, or null for anything else (internal included). */
43072
- function addonIdOfActionId(actionId) {
43073
- if (isInternalActionId(actionId)) return null;
43074
- const at = actionId.indexOf(":");
43075
- return at > 0 && at < actionId.length - 1 ? actionId.slice(0, at) : null;
43076
- }
43077
- var ActionStatusSchema = object({
43078
- isRunning: boolean(),
43079
- /** Ms epoch the latest run STARTED; null when it never ran. */
43080
- lastRunAt: number().nullable(),
43081
- /** The latest run's failure; null on success or when it never ran. */
43082
- lastError: string().nullable()
43083
- });
43084
- var ActionEntrySchema = object({
43085
- /** `internal:<uuid>` or `<addonId>:<localId>`. */
43086
- id: string().min(3),
43087
- name: string().min(1).max(120),
43088
- /** The addon that declared it; `core-blocks` for an internal one. */
43089
- addonId: string().min(1),
43090
- source: ActionSourceKindSchema,
43091
- /** An addon-declared entry is edited where it was declared. */
43092
- readOnly: boolean(),
43093
- /** Internal only: cap and wait steps. No action-in-action, so no cycles. */
43094
- steps: array(ActionStepSchema).min(1).max(50).optional(),
43095
- variables: array(ActionVariableSchema).max(20).optional(),
43096
- status: ActionStatusSchema
43097
- }).superRefine((entry, ctx) => {
43098
- if (entry.source === "internal") {
43099
- if (!isInternalActionId(entry.id)) ctx.addIssue({
43100
- code: "custom",
43101
- message: "an internal action id is internal:<uuid>",
43102
- path: ["id"]
43103
- });
43104
- return;
43105
- }
43106
- if (entry.steps !== void 0) ctx.addIssue({
43107
- code: "custom",
43108
- message: "only an internal action carries steps",
43109
- path: ["steps"]
43110
- });
43111
- if (!entry.readOnly) ctx.addIssue({
43112
- code: "custom",
43113
- message: "an addon action is read-only here",
43114
- path: ["readOnly"]
43115
- });
43116
- if (addonIdOfActionId(entry.id) === null) ctx.addIssue({
43117
- code: "custom",
43118
- message: "an addon action id is <addonId>:<localId>",
43119
- path: ["id"]
43120
- });
43121
- });
43122
- /** What an operator writes for an internal action. */
43123
- var ActionInputSchema = object({
43124
- name: string().min(1).max(120),
43125
- steps: array(ActionStepSchema).min(1).max(50),
43126
- variables: array(ActionVariableSchema).max(20).optional()
43127
- });
43128
- /** Who started a run. Absent = the operator pressed it. */
43129
- var ActionRunOriginSchema = object({
43130
- /** The automation block whose step started it. */
43131
- blockId: string().min(1).optional() });
43132
- /**
43133
- * `started` = the caller did not ask to wait. `timed-out` = the run reached the
43134
- * ceiling (`ACTION_RUN_MAX_MS`) and was stopped; it is never a `ran`.
43135
- */
43136
- var ActionRunOutcomeSchema = union([
43137
- AutomationRunOutcomeSchema,
43138
- literal("started"),
43139
- literal("timed-out")
43140
- ]);
43141
- var ActionRunResultSchema = object({
43142
- runId: string().min(1),
43143
- outcome: ActionRunOutcomeSchema,
43144
- /** Why it did not run, or what failed. */
43145
- message: string().optional()
43146
- });
43147
- _enum([
43148
- "not-found",
43149
- "read-only",
43150
- "invalid-variables",
43151
- "invalid-steps",
43152
- "not-linked",
43153
- "busy"
43154
- ]);
43155
- object({
43156
- actionId: string(),
43157
- runId: string(),
43158
- isRunning: boolean(),
43159
- /** Set when the run ended. */
43160
- outcome: AutomationRunOutcomeSchema.optional(),
43161
- lastRunAt: number().nullable(),
43162
- lastError: string().nullable()
43163
- });
43164
- /** A bare number accepted as a base unit — the save succeeded, say so. */
43165
- var UnitWarningSchema = object({
43166
- /** Where in the saved record, e.g. `conditions.children.0` or `steps.2.args.grams`. */
43167
- path: string(),
43168
- deviceId: number().int().optional(),
43169
- field: string(),
43170
- value: number(),
43171
- assumedUnit: string(),
43172
- message: string()
43173
- });
43174
- /**
43175
- * notification-output — canonical, capability-gated notification delivery.
43176
- *
43177
- * Apprise-derived model (see
43178
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
43179
- * callers emit ONE canonical `Notification`; each provider declares a
43180
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
43181
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
43182
- * message to what the kind supports — callers never special-case a service.
43183
- *
43184
- * DESIGN DECISIONS (locked):
43185
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
43186
- * `setTargetEnabled`), each provider persisting via the `settings-store`
43187
- * cap. Rationale: the admin UI needs one uniform surface across the
43188
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
43189
- * alternative would fork the UI per addon and cannot host the
43190
- * discovery→adopt flow.
43191
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
43192
- * the generated cap-mount auto-`concatCollection`-fans them across every
43193
- * registered provider (notifiers addon + HA addon) so one catalog is
43194
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
43195
- * `addonId` the generated collection router extracts from the call input.
43196
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
43197
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
43198
- * `storage` / `storage-provider` / `recording` caps over the same path. No
43199
- * base64 fallback needed.
43200
- *
43201
- * TODO (deferred, closed-set change — separate decision): add
43202
- * `providerKind: 'notify'` so notification providers surface on the unified
43203
- * admin "Integrations" page.
43204
- */
43205
- /**
43206
- * Zentik-derived typed-media enum — the superset across every kind. Each
43207
- * adapter picks what it supports and the degrade engine filters the rest.
43208
- */
43209
- var AttachmentMediaTypeSchema = _enum([
43210
- "image",
43211
- "video",
43212
- "gif",
43213
- "audio",
43214
- "icon"
43215
- ]);
43216
- /**
43217
- * A single attachment. Exactly one of `url` (remote source, most adapters
43218
- * prefer this) or `bytes` (inline source; required for Pushover-style
43219
- * bytes-only kinds) MUST be present — the degrade engine expresses a
43220
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
43221
- */
43222
- var AttachmentSchema = object({
43223
- mediaType: AttachmentMediaTypeSchema,
43224
- url: string().optional(),
43225
- bytes: _instanceof(Uint8Array).optional(),
43226
- mime: string().optional(),
43227
- name: string().optional()
43228
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
43229
- var NotificationFormatSchema = _enum([
43230
- "text",
43231
- "markdown",
43232
- "html"
43233
- ]);
43234
- /**
43235
- * The CLOSED icon vocabulary an action button may use.
43236
- *
43237
- * A closed set, not a free string, and that is the whole point: an arbitrary
43238
- * icon name is one that ntfy renders, zentik silently drops, and nobody
43239
- * notices — the same class of gap as a zone vocabulary nothing produced
43240
- * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
43241
- * declares `actionIcons: false` and the degrade engine strips the field.
43242
- *
43243
- * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
43244
- * renderer's icon set; "acknowledge" survives an adapter that draws it
43245
- * differently.
43246
- *
43247
- * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
43248
- *
43249
- * These names are for US. **No adapter may forward one verbatim.** Each maps
43250
- * the whole set onto its own renderer's vocabulary through a
43251
- * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
43252
- * fallback, so adding a member here fails every adapter's build until someone
43253
- * decides its glyph, which is the only place that decision can be made
43254
- * honestly.
43255
- *
43256
- * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
43257
- * `disarm` straight through; iOS feeds that string to
43258
- * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
43259
- * and every snooze and alarm button arrived BLANK. A pass-through is not a
43260
- * mapping, and "the field is documented" is not "the value renders".
43261
- *
43262
- * Adding a member is TRAIN-BOUND. The enum lives in the published
43263
- * `@camstack/server` closure and the cap seam validates against the HUB's copy,
43264
- * so an addon that emits a token the running hub does not know does not lose an
43265
- * icon — its whole `send` fails Zod validation and the notification never
43266
- * arrives. Never emit a new token from an addon before the train carrying it.
43267
- */
43268
- var NotificationActionIconSchema = _enum([
43269
- "acknowledge",
43270
- "dismiss",
43271
- "silence",
43272
- "snooze",
43273
- "view",
43274
- "play",
43275
- "open",
43276
- "close",
43277
- "lock",
43278
- "unlock",
43279
- "arm",
43280
- "arm-home",
43281
- "arm-away",
43282
- "arm-night",
43283
- "disarm",
43284
- "light",
43285
- "alert",
43286
- "camera"
43287
- ]);
43288
- /** A single tap-through action button. */
43289
- var NotificationActionSchema = object({
43290
- id: string(),
43291
- label: string(),
43292
- url: string().optional(),
43293
- /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
43294
- icon: NotificationActionIconSchema.optional(),
43295
- /**
43296
- * Renders in a warning style where the notifier supports it.
43297
- *
43298
- * A HINT, never a gate. The callback's authority is its token and nothing
43299
- * else — see `notification-center/action-token.ts` for what that does and
43300
- * does not buy.
43301
- */
43302
- destructive: boolean().optional(),
43303
- /**
43304
- * How the tap should REACH the url.
43305
- *
43306
- * `navigate` (absent, and every button authored before this field) opens it:
43307
- * the phone leaves the notification and shows whatever the callback returns.
43308
- * That is right for a button whose answer the operator wants to read.
43309
- *
43310
- * `background` fires it as a POST and stays put. It exists for the buttons
43311
- * whose whole point is not to interrupt — "silence this for 30 minutes" is
43312
- * an answer to the notification, and being thrown into a browser tab to
43313
- * confirm it costs more attention than the notification did. A backend that
43314
- * cannot do a background call renders it as an ordinary link (the adapters
43315
- * fall back rather than dropping the button), so this is a preference, never
43316
- * a requirement.
43317
- */
43318
- mode: _enum(["navigate", "background"]).optional()
43319
- });
43320
- /** A delivery parameter value: what a notifier declared it takes. */
43321
- var DeliveryParamValueSchema = union([
43322
- string(),
43323
- number(),
43324
- boolean()
43325
- ]);
43326
- /**
43327
- * One extra delivery parameter a notifier KIND declares (D938). A rule that
43328
- * addresses a target of that kind may set it; a kind that declares nothing is
43329
- * never handed one. The admin UI derives its control from this descriptor —
43330
- * no editor knows a service by name.
43331
- *
43332
- * Optional fields are `.optional()`, never `.default()` (a Zod default does not
43333
- * run on the addon cap path).
43334
- */
43335
- var DeliveryParamSchema = object({
43336
- key: string().min(1),
43337
- label: string(),
43338
- /** i18n key of the label, resolved by the declaring addon's namespace. */
43339
- labelKey: string().optional(),
43340
- type: _enum([
43341
- "enum",
43342
- "number",
43343
- "boolean",
43344
- "string"
43345
- ]),
43346
- /** `enum` only: the closed set of accepted values. */
43347
- options: array(object({
43348
- value: string(),
43349
- label: string()
43350
- })).optional(),
43351
- /** `number` only. */
43352
- min: number().optional(),
43353
- max: number().optional(),
43354
- /** What the notifier does when the rule sets nothing; shown as the placeholder. */
43355
- default: DeliveryParamValueSchema.optional()
43356
- });
43357
- /**
43358
- * The canonical notification. `body` is the only hard field (Apprise model).
43359
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
43360
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
43361
- * the adapter maps this ordinal onto its native level. `level?` is an
43362
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
43363
- * `priority` for that one target.
43364
- */
43365
- var NotificationSchema = object({
43366
- body: string(),
43367
- title: string().optional(),
43368
- format: NotificationFormatSchema.default("text"),
43369
- priority: number().int().min(1).max(5).default(3),
43370
- level: string().optional(),
43371
- attachments: array(AttachmentSchema).optional(),
43372
- clickUrl: string().optional(),
43373
- actions: array(NotificationActionSchema).optional(),
43374
- sound: string().optional(),
43375
- ttl: number().optional(),
43376
- tag: string().optional(),
43377
- deviceId: number().optional(),
43378
- eventId: string().optional(),
43379
- metadata: record(string(), unknown()).optional(),
43360
+ "restart",
43361
+ "drop",
43362
+ "cooldown",
43363
+ "bypass"
43364
+ ]),
43365
+ reason: string()
43366
+ });
43367
+ /** One device of a multi-device step: did the call land. */
43368
+ var StepDeviceTraceSchema = object({
43369
+ deviceId: number().int(),
43370
+ outcome: _enum(["ok", "error"]),
43371
+ message: string().optional()
43372
+ });
43373
+ var AutomationStepTraceSchema = object({
43374
+ /** `actions.<i>` */
43375
+ path: string().min(1),
43376
+ kind: string().min(1),
43377
+ cap: string().optional(),
43378
+ method: string().optional(),
43379
+ /** The vocabulary action the step performs (`cover.open`), when it names one. */
43380
+ actionId: string().optional(),
43381
+ deviceId: number().int().optional(),
43382
+ /** What happened per device, when the step drove several (or a domain). */
43383
+ devices: array(StepDeviceTraceSchema).optional(),
43384
+ startedAt: number().optional(),
43385
+ endedAt: number().optional(),
43380
43386
  /**
43381
- * Values for the parameters the target's kind DECLARES in
43382
- * {@link TargetKindSchema} `deliveryParams`. Already validated against the
43383
- * declaration by the sender; a notifier reads only the keys it declared.
43387
+ * The parameters the step SENT, after `{{vars.x}}` substitution — what the
43388
+ * device was actually asked. Bounded like trigger `vars`; a secret parameter
43389
+ * is shown as {@link REDACTED_ARG}. Absent on traces written before it existed.
43384
43390
  */
43385
- delivery: record(string(), DeliveryParamValueSchema).optional()
43386
- });
43387
- /** One declared native severity/priority level for a kind. */
43388
- var TargetKindLevelSchema = object({
43389
- id: string(),
43390
- label: string(),
43391
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
43392
- ordinal: number().int().min(1).max(5).nullable(),
43393
- flags: object({
43394
- critical: boolean().optional(),
43395
- silent: boolean().optional(),
43396
- noPush: boolean().optional()
43397
- }).optional(),
43398
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
43399
- requires: array(string()).optional(),
43400
- description: string().optional()
43391
+ args: record(string(), TraceValueSchema).optional(),
43392
+ /** `empty`: the target resolved to no device, so nothing was done — never silently `ok`. */
43393
+ outcome: _enum([
43394
+ "ok",
43395
+ "error",
43396
+ "not-run",
43397
+ "empty"
43398
+ ]),
43399
+ message: string().optional()
43401
43400
  });
43402
- /** The full capability block consulted before dispatch. */
43403
- var TargetKindCapsSchema = object({
43404
- attachments: object({
43405
- mediaTypes: array(AttachmentMediaTypeSchema),
43406
- mode: _enum([
43407
- "url",
43408
- "bytes",
43409
- "both"
43410
- ]),
43411
- max: number().int().nonnegative(),
43412
- maxBytes: number().int().positive().optional()
43413
- }),
43414
- /** Max action buttons (0 = none). */
43415
- actions: number().int().nonnegative(),
43401
+ var AutomationRunTraceSchema = object({
43402
+ runId: string().min(1),
43403
+ startedAt: number(),
43404
+ endedAt: number(),
43405
+ trigger: AutomationTriggerTraceSchema,
43406
+ conditions: array(AutomationConditionTraceNodeSchema),
43407
+ verdict: AutomationRunVerdictSchema.optional(),
43408
+ steps: array(AutomationStepTraceSchema),
43409
+ outcome: AutomationRunOutcomeSchema,
43410
+ /** How many identical consecutive skips this trace stands for (>= 1). */
43411
+ repeat: number().int().min(1),
43412
+ /** When the latest of those repeats happened. */
43413
+ lastAt: number(),
43416
43414
  /**
43417
- * Whether this kind renders a per-action ICON.
43418
- *
43419
- * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
43420
- * run on the addon cap path — three production failures in one day taught
43421
- * this repo that once. Absent is read as false by the degrade engine, which
43422
- * is the safe direction: an icon that is not rendered costs nothing, an icon
43423
- * assumed and dropped costs the operator's trust in the field.
43415
+ * A scheduler point's own outcome (D983). `outcome` carries the closest
43416
+ * automation outcome so a reader that does not know schedulers still reads
43417
+ * the trace; this field says exactly what happened.
43424
43418
  */
43425
- actionIcons: boolean().optional(),
43426
- levels: array(TargetKindLevelSchema),
43427
- format: array(NotificationFormatSchema),
43428
- clickUrl: boolean(),
43429
- sound: boolean(),
43430
- ttl: boolean(),
43431
- bodyMaxLen: number().int().positive()
43419
+ scheduler: SchedulerOutcomeSchema.optional()
43432
43420
  });
43433
43421
  /**
43434
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
43435
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
43436
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
43437
- * the union is large and not meant for runtime validation here; the exported
43438
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
43422
+ * What the hub's `runs` ledger holds for one owner (`automation:<blockId>` or
43423
+ * `action:<actionId>`): the cooldown stamp and the trace ring.
43439
43424
  */
43440
- var ConfigSchemaPassthrough$4 = unknown();
43441
- var TargetKindSchema = object({
43442
- kind: string(),
43443
- label: string(),
43444
- icon: string(),
43445
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
43446
- addonId: string(),
43447
- /**
43448
- * URL of the kind's bundled BRAND icon, served by the providing addon over
43449
- * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
43450
- * when the addon bundles no icon for that kind — the client then falls back
43451
- * to a neutral glyph rather than rendering the raw `icon` NAME as text.
43452
- *
43453
- * Root-relative on purpose: it resolves against whatever origin serves a web
43454
- * client, and a native client joins it onto its own hub base.
43455
- *
43456
- * DECLARED here deliberately. It used to travel as an undeclared passthrough
43457
- * field that survived only because the runtime cap-router forwards provider
43458
- * output verbatim — so every consumer had to re-declare it by hand to stop
43459
- * its own Zod parse from stripping it, and the whole arrangement would have
43460
- * broken silently the moment output validation was tightened anywhere.
43461
- */
43462
- iconUrl: string().optional(),
43463
- /**
43464
- * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
43465
- *
43466
- * The server knows this and therefore says it, because the client cannot
43467
- * safely guess: a React-Native client renders SVG and raster through two
43468
- * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
43469
- * not decode SVG on iOS/Android), so without this it silently fell back to a
43470
- * placeholder glyph for every vector icon while the web build looked fine.
43471
- *
43472
- * Absent when {@link iconUrl} is absent, or for a legacy provider that has
43473
- * not been updated — a client that cannot determine the type should prefer
43474
- * its raster path, which is the safe default for an unknown image.
43475
- */
43476
- iconMediaType: string().optional(),
43477
- configSchema: ConfigSchemaPassthrough$4,
43478
- /**
43479
- * The config keys that decide WHERE this kind's secrets are sent (server,
43480
- * base URL, instance, chat, recipient). A save that changes any of them
43481
- * while leaving a secret redacted is refused by the provider (D634), so an
43482
- * editor uses this to ask for the secret on exactly those edits instead of
43483
- * on every edit.
43484
- *
43485
- * Optional: absent from a provider that holds no secret (the Home Assistant
43486
- * kinds) and from a provider older than this field — an editor that finds
43487
- * it absent must assume ANY edit may need the secret.
43488
- */
43489
- destinationKeys: array(string()).optional(),
43490
- supportsDiscovery: boolean(),
43491
- /**
43492
- * Extra delivery parameters a rule may set for a target of this kind (D938).
43493
- * Absent = none; the rule editor then offers nothing.
43494
- */
43495
- deliveryParams: array(DeliveryParamSchema).optional(),
43496
- caps: TargetKindCapsSchema
43425
+ var AutomationStateDocSchema = object({
43426
+ version: literal(1),
43427
+ lastRunStartedAt: number().nullable(),
43428
+ traces: array(AutomationRunTraceSchema)
43497
43429
  });
43498
43430
  /**
43499
- * A persisted target. `config` holds secrets; providers REDACT secret fields
43500
- * (return a presence marker only) when serving `listTargets` — never
43501
- * round-trip a stored secret to the UI.
43431
+ * Actions — named, runnable things an operator (or an automation) can start,
43432
+ * declared as LIST ENTRIES, never as devices.
43433
+ *
43434
+ * An entry is either **internal** (a short list of cap/wait steps kept in the
43435
+ * hub's `actions` record kind) or declared by an addon through the
43436
+ * `action-source` collection (a Home Assistant script, a scene …). The one
43437
+ * surface the UI and automations talk to is the `actions` cap; it keeps no
43438
+ * catalog of its own (D3) — every `list` asks the providers live.
43439
+ *
43440
+ * This file is the data half: the entry schema, the variables an entry may
43441
+ * declare, and the pure resolution of the values a run was given. It holds no
43442
+ * runtime.
43502
43443
  */
43503
- var TargetSchema = object({
43504
- id: string(),
43505
- name: string(),
43506
- kind: string(),
43507
- addonId: string(),
43508
- enabled: boolean(),
43509
- config: record(string(), unknown())
43444
+ /** What a variable's value is. `select` takes one of `options`. */
43445
+ var ActionVariableTypeSchema = _enum([
43446
+ "string",
43447
+ "number",
43448
+ "boolean",
43449
+ "select"
43450
+ ]);
43451
+ var ACTION_VARIABLE_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
43452
+ var ActionVariableValueSchema = union([
43453
+ string(),
43454
+ number(),
43455
+ boolean()
43456
+ ]);
43457
+ /**
43458
+ * One declared variable. Steps reference it as `{ $var: 'name' }` (the whole
43459
+ * value) or `{{vars.name}}` (inside a string) in their args.
43460
+ */
43461
+ var ActionVariableSchema = object({
43462
+ name: string().regex(ACTION_VARIABLE_NAME_RE),
43463
+ label: string().min(1).max(120).optional(),
43464
+ type: ActionVariableTypeSchema,
43465
+ required: boolean().optional(),
43466
+ default: ActionVariableValueSchema.optional(),
43467
+ /** The allowed values of a `select`. */
43468
+ options: array(string().min(1)).min(1).optional()
43469
+ }).superRefine((v, ctx) => {
43470
+ if (v.type === "select" && v.options === void 0) ctx.addIssue({
43471
+ code: "custom",
43472
+ message: "a select variable needs options",
43473
+ path: ["options"]
43474
+ });
43475
+ if (v.default !== void 0 && variableValueError(v, v.default) !== null) ctx.addIssue({
43476
+ code: "custom",
43477
+ message: variableValueError(v, v.default) ?? "invalid default",
43478
+ path: ["default"]
43479
+ });
43510
43480
  });
43511
- /** A discovery-surfaced candidate (config is partial + non-secret). */
43512
- var DiscoveredTargetSchema = object({
43513
- kind: string(),
43514
- suggestedName: string(),
43515
- config: record(string(), unknown())
43481
+ /** Why `value` is not a legal value for the variable, or null when it is. */
43482
+ function variableValueError(variable, value) {
43483
+ switch (variable.type) {
43484
+ case "string": return typeof value === "string" ? null : `variable "${variable.name}" must be a string`;
43485
+ case "number": return typeof value === "number" && Number.isFinite(value) ? null : `variable "${variable.name}" must be a number`;
43486
+ case "boolean": return typeof value === "boolean" ? null : `variable "${variable.name}" must be true or false`;
43487
+ case "select": return typeof value === "string" && (variable.options ?? []).includes(value) ? null : `variable "${variable.name}" must be one of ${(variable.options ?? []).join(", ")}`;
43488
+ }
43489
+ }
43490
+ var ActionSourceKindSchema = _enum(["internal", "addon"]);
43491
+ function isInternalActionId(actionId) {
43492
+ return actionId.startsWith("internal:") && actionId.length > 9;
43493
+ }
43494
+ /** The addon behind a `<addonId>:<localId>` id, or null for anything else (internal included). */
43495
+ function addonIdOfActionId(actionId) {
43496
+ if (isInternalActionId(actionId)) return null;
43497
+ const at = actionId.indexOf(":");
43498
+ return at > 0 && at < actionId.length - 1 ? actionId.slice(0, at) : null;
43499
+ }
43500
+ var ActionStatusSchema = object({
43501
+ isRunning: boolean(),
43502
+ /** Ms epoch the latest run STARTED; null when it never ran. */
43503
+ lastRunAt: number().nullable(),
43504
+ /** The latest run's failure; null on success or when it never ran. */
43505
+ lastError: string().nullable()
43516
43506
  });
43517
- /** The degrade engine's report — what was resolved / dropped / degraded. */
43518
- var RenderedAsSchema = object({
43519
- level: string(),
43520
- format: NotificationFormatSchema,
43521
- attachmentsSent: number().int().nonnegative(),
43522
- actionsSent: number().int().nonnegative(),
43523
- truncated: boolean(),
43524
- dropped: array(string())
43507
+ var ActionEntrySchema = object({
43508
+ /** `internal:<uuid>` or `<addonId>:<localId>`. */
43509
+ id: string().min(3),
43510
+ name: string().min(1).max(120),
43511
+ /** The addon that declared it; `core-blocks` for an internal one. */
43512
+ addonId: string().min(1),
43513
+ source: ActionSourceKindSchema,
43514
+ /** An addon-declared entry is edited where it was declared. */
43515
+ readOnly: boolean(),
43516
+ /** Internal only: cap and wait steps. No action-in-action, so no cycles. */
43517
+ steps: array(ActionStepSchema).min(1).max(50).optional(),
43518
+ variables: array(ActionVariableSchema).max(20).optional(),
43519
+ status: ActionStatusSchema
43520
+ }).superRefine((entry, ctx) => {
43521
+ if (entry.source === "internal") {
43522
+ if (!isInternalActionId(entry.id)) ctx.addIssue({
43523
+ code: "custom",
43524
+ message: "an internal action id is internal:<uuid>",
43525
+ path: ["id"]
43526
+ });
43527
+ return;
43528
+ }
43529
+ if (entry.steps !== void 0) ctx.addIssue({
43530
+ code: "custom",
43531
+ message: "only an internal action carries steps",
43532
+ path: ["steps"]
43533
+ });
43534
+ if (!entry.readOnly) ctx.addIssue({
43535
+ code: "custom",
43536
+ message: "an addon action is read-only here",
43537
+ path: ["readOnly"]
43538
+ });
43539
+ if (addonIdOfActionId(entry.id) === null) ctx.addIssue({
43540
+ code: "custom",
43541
+ message: "an addon action id is <addonId>:<localId>",
43542
+ path: ["id"]
43543
+ });
43525
43544
  });
43526
- var SendResultSchema = object({
43527
- success: boolean(),
43528
- error: string().optional(),
43529
- renderedAs: RenderedAsSchema.optional()
43545
+ /** What an operator writes for an internal action. */
43546
+ var ActionInputSchema = object({
43547
+ name: string().min(1).max(120),
43548
+ steps: array(ActionStepSchema).min(1).max(50),
43549
+ variables: array(ActionVariableSchema).max(20).optional()
43530
43550
  });
43531
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
43532
- var TestResultSchema = SendResultSchema;
43551
+ /** Who started a run. Absent = the operator pressed it. */
43552
+ var ActionRunOriginSchema = object({
43553
+ /** The automation block whose step started it. */
43554
+ blockId: string().min(1).optional() });
43533
43555
  /**
43534
- * Pre-save credential probe against a partial (unsaved) target config.
43535
- * `resolvedUrl` never includes secrets (token stripped from path).
43556
+ * `started` = the caller did not ask to wait. `timed-out` = the run reached the
43557
+ * ceiling (`ACTION_RUN_MAX_MS`) and was stopped; it is never a `ran`.
43536
43558
  */
43537
- var ProbeTargetConfigResultSchema = object({
43538
- ok: boolean(),
43539
- resolvedUrl: string(),
43540
- httpStatus: number().int().optional(),
43541
- stateInstance: string().optional(),
43542
- error: string().optional()
43559
+ var ActionRunOutcomeSchema = union([
43560
+ AutomationRunOutcomeSchema,
43561
+ literal("started"),
43562
+ literal("timed-out")
43563
+ ]);
43564
+ var ActionRunResultSchema = object({
43565
+ runId: string().min(1),
43566
+ outcome: ActionRunOutcomeSchema,
43567
+ /** Why it did not run, or what failed. */
43568
+ message: string().optional()
43543
43569
  });
43544
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
43545
- kind: string(),
43546
- config: record(string(), unknown()).optional(),
43547
- /**
43548
- * When editing / duplicating, the blank password field never round-trips
43549
- * the stored token. Pass the source target id so the provider can restore
43550
- * secrets server-side before dialling (same merge as upsert).
43551
- */
43552
- targetId: string().optional()
43553
- }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
43554
- kind: string(),
43555
- config: record(string(), unknown()),
43556
- /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
43557
- targetId: string().optional()
43558
- }), ProbeTargetConfigResultSchema, {
43559
- kind: "mutation",
43560
- auth: "admin"
43561
- }), method(object({
43562
- targetId: string(),
43563
- notification: NotificationSchema
43564
- }), SendResultSchema, {
43565
- kind: "mutation",
43566
- auth: "admin"
43567
- }), method(object({
43568
- targetId: string(),
43569
- sample: NotificationSchema.optional()
43570
- }), TestResultSchema, {
43571
- kind: "mutation",
43572
- auth: "admin"
43573
- }), method(object({
43574
- target: TargetSchema,
43575
- /**
43576
- * Duplicate flow: create a NEW target id while copying secrets from an
43577
- * existing row. The UI never holds the stored token; blank / redacted
43578
- * password fields are restored from this source (D634 still applies if
43579
- * destinations diverge without a re-typed secret).
43580
- */
43581
- cloneSecretsFrom: string().optional()
43582
- }), TargetSchema, {
43583
- kind: "mutation",
43584
- auth: "admin"
43585
- }), method(object({ targetId: string() }), _void(), {
43586
- kind: "mutation",
43587
- auth: "admin"
43588
- }), method(object({
43589
- targetId: string(),
43590
- enabled: boolean()
43591
- }), _void(), {
43592
- kind: "mutation",
43593
- auth: "admin"
43570
+ _enum([
43571
+ "not-found",
43572
+ "read-only",
43573
+ "invalid-variables",
43574
+ "invalid-steps",
43575
+ "not-linked",
43576
+ "busy"
43577
+ ]);
43578
+ object({
43579
+ actionId: string(),
43580
+ runId: string(),
43581
+ isRunning: boolean(),
43582
+ /** Set when the run ended. */
43583
+ outcome: AutomationRunOutcomeSchema.optional(),
43584
+ lastRunAt: number().nullable(),
43585
+ lastError: string().nullable()
43586
+ });
43587
+ /** A bare number accepted as a base unit — the save succeeded, say so. */
43588
+ var UnitWarningSchema = object({
43589
+ /** Where in the saved record, e.g. `conditions.children.0` or `steps.2.args.grams`. */
43590
+ path: string(),
43591
+ deviceId: number().int().optional(),
43592
+ field: string(),
43593
+ value: number(),
43594
+ assumedUnit: string(),
43595
+ message: string()
43594
43596
  });
43595
43597
  _enum([
43596
43598
  "automation",
@@ -47441,37 +47443,6 @@ method(object({
47441
47443
  kind: "mutation",
47442
47444
  auth: "admin"
47443
47445
  }), method(object({ blockIds: array(string()) }), object({ states: array(CompositionBlockStateSchema) }), { auth: "admin" });
47444
- var ConnectionTestOutcomeSchema = discriminatedUnion("outcome", [
47445
- object({
47446
- outcome: literal("validated"),
47447
- /** Round-trip of the sign-in, when the provider measured it. */
47448
- latencyMs: number().nonnegative().optional(),
47449
- /** Optional human detail worth showing next to the tick
47450
- * ("3 devices visible on this account"). */
47451
- detail: string().optional()
47452
- }).strict(),
47453
- object({
47454
- outcome: literal("rejected"),
47455
- error: string()
47456
- }).strict(),
47457
- object({
47458
- outcome: literal("inconclusive"),
47459
- error: string()
47460
- }).strict()
47461
- ]);
47462
- var ConnectionTestInputSchema = object({
47463
- /** Candidate integration settings, exactly as the create form collected them. */
47464
- settings: record(string(), unknown()) });
47465
- /**
47466
- * What the provider's test actually DOES, so the UI can say it in words before
47467
- * the operator presses the button ("Signs in to the Dreo cloud"). Purely
47468
- * descriptive — it never changes routing.
47469
- */
47470
- var ConnectionTestDescriptorSchema = object({ label: string() });
47471
- method(ConnectionTestInputSchema, ConnectionTestOutcomeSchema, {
47472
- kind: "mutation",
47473
- auth: "admin"
47474
- }), method(_void(), ConnectionTestDescriptorSchema, { auth: "admin" });
47475
47446
  object({
47476
47447
  /** True when the upstream system considers the entity connected. */
47477
47448
  connected: boolean(),
@@ -53325,10 +53296,9 @@ var AvailableIntegrationTypeSchema = object({
53325
53296
  * locations" checkbox. Provider-declared in the addon manifest. */
53326
53297
  supportsLocationImport: boolean(),
53327
53298
  /**
53328
- * True when this integration DECLARES a pre-creation test (the
53329
- * `connection-test` cap, or a broker whose settings it stores). Drives the
53330
- * Test button: an integration that cannot be tested must say so up front
53331
- * rather than offering a button that always answers the same nonsense.
53299
+ * True when this integration's instances declare a Test (`integration-instances.describe().canTest`,
53300
+ * D1016). An integration that cannot be tested must say so up front rather than offering a
53301
+ * button that always answers the same nonsense.
53332
53302
  */
53333
53303
  canTest: boolean(),
53334
53304
  existingInstances: array(object({
@@ -53382,7 +53352,9 @@ var CreateIntegrationInputSchema = object({
53382
53352
  info: record(string(), unknown()).optional(),
53383
53353
  /** `stableId` of the discovery candidate the form was seeded from (D984). */
53384
53354
  originStableId: string().optional(),
53385
- settings: record(string(), unknown()).optional()
53355
+ settings: record(string(), unknown()).optional(),
53356
+ /** The instance kind being created, for a provider with several; omitted = its only testable kind. */
53357
+ kind: string().min(1).optional()
53386
53358
  });
53387
53359
  var UpdateIntegrationInputSchema = object({
53388
53360
  id: string(),
@@ -53408,13 +53380,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
53408
53380
  }), method(object({ id: string() }), DeleteIntegrationResultSchema, {
53409
53381
  kind: "mutation",
53410
53382
  auth: "admin"
53411
- }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
53412
- addonId: string(),
53413
- settings: record(string(), unknown())
53414
- }), TestConnectionResultSchema, {
53415
- kind: "mutation",
53416
- auth: "admin"
53417
- });
53383
+ }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" });
53418
53384
  /**
53419
53385
  * `integration-instances` - the connection of an addon that holds ONE or SEVERAL
53420
53386
  * (an account, a relay, a destination) as typed records (D985, D986). The addon's
@@ -61631,13 +61597,6 @@ var AddBrokerInputSchema = object({
61631
61597
  });
61632
61598
  var AddBrokerResultSchema = object({ id: string() });
61633
61599
  var IdInputSchema = object({ id: string() });
61634
- var TestResultSchema$1 = discriminatedUnion("ok", [object({
61635
- ok: literal(true),
61636
- latencyMs: number()
61637
- }), object({
61638
- ok: literal(false),
61639
- error: string()
61640
- })]);
61641
61600
  var StartEmbeddedInputSchema = object({
61642
61601
  port: number().int().min(1).max(65535).default(1883),
61643
61602
  /** Allow anonymous connect (no username/password). Default: false. */
@@ -61660,9 +61619,6 @@ method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnection
61660
61619
  }), method(IdInputSchema, _void(), {
61661
61620
  kind: "mutation",
61662
61621
  auth: "admin"
61663
- }), method(IdInputSchema, TestResultSchema$1, {
61664
- kind: "mutation",
61665
- auth: "admin"
61666
61622
  }), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
61667
61623
  kind: "mutation",
61668
61624
  auth: "admin"
@@ -73274,7 +73230,7 @@ var TrainingProviderTypeSchema = object({
73274
73230
  label: string(),
73275
73231
  /** Path under the addon package (e.g. `assets/training-modal/icon.svg`). */
73276
73232
  iconAsset: string(),
73277
- /** Owning `training-provider` addon — collection selector for createEntry. */
73233
+ /** Owning `training-provider` addon. */
73278
73234
  addonId: string(),
73279
73235
  /**
73280
73236
  * Fields for one entry of this type (credentials + defaults). Absent on a
@@ -73319,36 +73275,6 @@ var TrainingProviderDescriptorSchema = object({
73319
73275
  */
73320
73276
  formValues: record(string(), unknown()).optional()
73321
73277
  });
73322
- /**
73323
- * Patch one entry's name, credentials and defaults. Pin with owning `{ addonId }`.
73324
- * An omitted or empty `tokenSecret` means KEEP the stored secret (D787).
73325
- * `{ $secretClear: true }` clears it.
73326
- */
73327
- var TrainingProviderUpdateEntryInputSchema = object({
73328
- entryId: string().min(1),
73329
- name: string().min(1),
73330
- tokenId: string().optional(),
73331
- tokenSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
73332
- environment: string().optional(),
73333
- gpuType: string().optional(),
73334
- region: string().optional(),
73335
- costCapUsd: number().nonnegative().optional(),
73336
- maxRuntimeSec: number().int().positive().optional(),
73337
- failedDatasetGraceHours: number().nonnegative().optional()
73338
- });
73339
- var TrainingCredentialCheckSchema = object({
73340
- ok: boolean(),
73341
- /** Workspace / account name, when the provider says. */
73342
- account: string().optional(),
73343
- reason: _enum([
73344
- "missing",
73345
- "invalid",
73346
- "forbidden",
73347
- "network",
73348
- "quota"
73349
- ]).optional(),
73350
- detail: string().optional()
73351
- });
73352
73278
  var ProviderJobStateSchema = _enum([
73353
73279
  "pending",
73354
73280
  "starting",
@@ -73396,22 +73322,6 @@ var TrainingLogLineSchema = object({
73396
73322
  text: string()
73397
73323
  });
73398
73324
  method(object({}), array(TrainingProviderDescriptorSchema).readonly(), { auth: "admin" }), method(object({}), array(TrainingProviderTypeSchema).readonly(), { auth: "admin" }), method(object({}), array(TrainingProviderEntrySchema).readonly(), { auth: "admin" }), method(object({
73399
- typeId: string().min(1),
73400
- name: string().min(1)
73401
- }), object({ entryId: string() }), {
73402
- kind: "mutation",
73403
- auth: "admin"
73404
- }), method(object({ entryId: string().min(1) }), object({ ok: literal(true) }), {
73405
- kind: "mutation",
73406
- auth: "admin"
73407
- }), method(TrainingProviderUpdateEntryInputSchema, object({ ok: literal(true) }), {
73408
- kind: "mutation",
73409
- auth: "admin"
73410
- }), method(object({ entryId: string().min(1) }), TrainingCredentialCheckSchema, {
73411
- kind: "mutation",
73412
- auth: "admin",
73413
- timeoutMs: 3e4
73414
- }), method(object({
73415
73325
  entryId: string().min(1),
73416
73326
  gpuType: string(),
73417
73327
  region: string(),
@@ -76797,20 +76707,6 @@ Object.freeze({
76797
76707
  access: "view",
76798
76708
  role: "admin"
76799
76709
  },
76800
- "connectionTest.describeTest": {
76801
- capName: "connection-test",
76802
- capScope: "system",
76803
- addonId: null,
76804
- access: "view",
76805
- role: "admin"
76806
- },
76807
- "connectionTest.testSettings": {
76808
- capName: "connection-test",
76809
- capScope: "system",
76810
- addonId: null,
76811
- access: "create",
76812
- role: "admin"
76813
- },
76814
76710
  "connectivity.getStatus": {
76815
76711
  capName: "connectivity",
76816
76712
  capScope: "device",
@@ -79177,13 +79073,6 @@ Object.freeze({
79177
79073
  access: "view",
79178
79074
  role: "admin"
79179
79075
  },
79180
- "integrations.testConnection": {
79181
- capName: "integrations",
79182
- capScope: "system",
79183
- addonId: null,
79184
- access: "create",
79185
- role: "admin"
79186
- },
79187
79076
  "integrations.update": {
79188
79077
  capName: "integrations",
79189
79078
  capScope: "system",
@@ -80416,13 +80305,6 @@ Object.freeze({
80416
80305
  access: "create",
80417
80306
  role: "admin"
80418
80307
  },
80419
- "mqttBroker.testConnection": {
80420
- capName: "mqtt-broker",
80421
- capScope: "system",
80422
- addonId: null,
80423
- access: "create",
80424
- role: "admin"
80425
- },
80426
80308
  "nativeObjectDetection.getOptions": {
80427
80309
  capName: "native-object-detection",
80428
80310
  capScope: "device",
@@ -85253,20 +85135,6 @@ Object.freeze({
85253
85135
  access: "create",
85254
85136
  role: "admin"
85255
85137
  },
85256
- "trainingProvider.createEntry": {
85257
- capName: "training-provider",
85258
- capScope: "system",
85259
- addonId: null,
85260
- access: "create",
85261
- role: "admin"
85262
- },
85263
- "trainingProvider.deleteEntry": {
85264
- capName: "training-provider",
85265
- capScope: "system",
85266
- addonId: null,
85267
- access: "delete",
85268
- role: "admin"
85269
- },
85270
85138
  "trainingProvider.describe": {
85271
85139
  capName: "training-provider",
85272
85140
  capScope: "system",
@@ -85337,13 +85205,6 @@ Object.freeze({
85337
85205
  access: "create",
85338
85206
  role: "admin"
85339
85207
  },
85340
- "trainingProvider.updateEntry": {
85341
- capName: "training-provider",
85342
- capScope: "system",
85343
- addonId: null,
85344
- access: "create",
85345
- role: "admin"
85346
- },
85347
85208
  "trainingProvider.uploadDataset": {
85348
85209
  capName: "training-provider",
85349
85210
  capScope: "system",
@@ -85351,13 +85212,6 @@ Object.freeze({
85351
85212
  access: "create",
85352
85213
  role: "admin"
85353
85214
  },
85354
- "trainingProvider.validateCredentials": {
85355
- capName: "training-provider",
85356
- capScope: "system",
85357
- addonId: null,
85358
- access: "create",
85359
- role: "admin"
85360
- },
85361
85215
  "turnProvider.getTurnServers": {
85362
85216
  capName: "turn-provider",
85363
85217
  capScope: "system",
@@ -90458,7 +90312,7 @@ var AgentUIAddon = class extends BaseAddon {
90458
90312
  capability: adminUiCapability,
90459
90313
  provider: {
90460
90314
  getStaticDir: async () => ({ staticDir: path.resolve(__dirname) }),
90461
- getVersion: async () => ({ version: "1.2.334" })
90315
+ getVersion: async () => ({ version: "1.2.336" })
90462
90316
  }
90463
90317
  }];
90464
90318
  }