@camstack/addon-mqtt-broker 1.2.153 → 1.2.155

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-D7ZKm4nb.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).
@@ -6730,10 +6730,33 @@ function integrationRowsApiOver(api) {
6730
6730
  };
6731
6731
  }
6732
6732
  var PREFIX = "records/";
6733
+ /**
6734
+ * Cross-realm brand. An addon bundles its own copy of this module (D15) while
6735
+ * `ctx.records` is the HOST's door, so an error thrown by the host is an
6736
+ * instance of the host's classes, not of the addon's. The brand plus the code
6737
+ * make `instanceof` answer by identity of the failure, not of the copy.
6738
+ */
6739
+ var RECORDS_ERROR_BRAND = Symbol.for("camstack.records-error");
6740
+ function carriesBrand(value) {
6741
+ return typeof value === "object" && value !== null && Reflect.get(value, RECORDS_ERROR_BRAND) === true;
6742
+ }
6733
6743
  /** The base of every records failure. `detail` is the human part of the message. */
6734
6744
  var RecordsError = class extends Error {
6745
+ /** The code a subclass stands for; the base stands for every code. */
6746
+ static code = void 0;
6747
+ /**
6748
+ * `instanceof` across copies of this module: any branded error is a
6749
+ * `RecordsError`, and is an instance of a subclass only when its code is the
6750
+ * subclass's code. Same-copy answers are unchanged.
6751
+ */
6752
+ static [Symbol.hasInstance](value) {
6753
+ if (Function.prototype[Symbol.hasInstance].call(this, value)) return true;
6754
+ if (!carriesBrand(value)) return false;
6755
+ return this.code === void 0 || value.code === this.code;
6756
+ }
6735
6757
  code;
6736
6758
  detail;
6759
+ [RECORDS_ERROR_BRAND] = true;
6737
6760
  constructor(code, detail) {
6738
6761
  super(`${PREFIX}${code}: ${detail}`);
6739
6762
  this.name = "RecordsError";
@@ -6742,22 +6765,25 @@ var RecordsError = class extends Error {
6742
6765
  }
6743
6766
  };
6744
6767
  /** The keychain could not store or serve a value: explicit, never an empty string. */
6745
- var RecordSecretUnavailableError = class extends RecordsError {
6768
+ var RecordSecretUnavailableError = class RecordSecretUnavailableError extends RecordsError {
6769
+ static code = "secret-unavailable";
6746
6770
  constructor(detail) {
6747
- super("secret-unavailable", detail);
6771
+ super(RecordSecretUnavailableError.code, detail);
6748
6772
  this.name = "RecordSecretUnavailableError";
6749
6773
  }
6750
6774
  };
6751
- var InvalidRecordError = class extends RecordsError {
6775
+ var InvalidRecordError = class InvalidRecordError extends RecordsError {
6776
+ static code = "invalid-record";
6752
6777
  constructor(detail) {
6753
- super("invalid-record", detail);
6778
+ super(InvalidRecordError.code, detail);
6754
6779
  this.name = "InvalidRecordError";
6755
6780
  }
6756
6781
  };
6757
6782
  /** A kind declaration the engine refuses (non-additive change, bad shape, wrong file). */
6758
- var DeclarationRefusedError = class extends RecordsError {
6783
+ var DeclarationRefusedError = class DeclarationRefusedError extends RecordsError {
6784
+ static code = "declaration-refused";
6759
6785
  constructor(detail) {
6760
- super("declaration-refused", detail);
6786
+ super(DeclarationRefusedError.code, detail);
6761
6787
  this.name = "DeclarationRefusedError";
6762
6788
  }
6763
6789
  };
@@ -9552,286 +9578,6 @@ function normalizeAddonInitResult(result) {
9552
9578
  if (Array.isArray(result)) return { providers: result };
9553
9579
  return result;
9554
9580
  }
9555
- /**
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`.
9559
- */
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()
9578
- });
9579
- function defOf(schema) {
9580
- const zodBag = Reflect.get(schema, "_zod");
9581
- if (typeof zodBag !== "object" || zodBag === null) return {};
9582
- const def = Reflect.get(zodBag, "def");
9583
- if (typeof def !== "object" || def === null) return {};
9584
- return def;
9585
- }
9586
- function declaredHere(schema) {
9587
- return globalRegistry.get(schema)?.secret === true;
9588
- }
9589
- /** Wrapper kinds whose `innerType` is the field itself. */
9590
- var WRAPPERS = new Set([
9591
- "optional",
9592
- "nullable",
9593
- "default",
9594
- "prefault",
9595
- "catch",
9596
- "readonly",
9597
- "nonoptional"
9598
- ]);
9599
- /**
9600
- * The schema layers that make up ONE field: the schema itself and every
9601
- * wrapper below it (optional / default / nullable / readonly / catch), plus
9602
- * both sides of a pipe (`z.preprocess` puts the real schema on `out`,
9603
- * `.transform` on `in`).
9604
- */
9605
- function layersOf(schema) {
9606
- const out = [];
9607
- const queue = [schema];
9608
- while (queue.length > 0) {
9609
- const s = queue.shift();
9610
- if (!s || out.includes(s)) continue;
9611
- out.push(s);
9612
- const def = defOf(s);
9613
- if (def.type !== void 0 && WRAPPERS.has(def.type) && def.innerType) queue.push(def.innerType);
9614
- if (def.type === "pipe") {
9615
- if (def.in) queue.push(def.in);
9616
- if (def.out) queue.push(def.out);
9617
- }
9618
- }
9619
- return out;
9620
- }
9621
- /** True when any layer of this field declares `.meta({ secret: true })`. */
9622
- function isZodSecret(schema) {
9623
- return layersOf(schema).some(declaredHere);
9624
- }
9625
- /**
9626
- * Every declared secret path in a config schema: dotted keys, `[]` for "each
9627
- * element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
9628
- * map" (`applianceSecrets{}`). A declared
9629
- * object or array is reported as one path and not descended into — everything
9630
- * under it is secret.
9631
- */
9632
- function collectZodSecretPaths(schema, prefix = "") {
9633
- if (isZodSecret(schema)) return prefix.length > 0 ? [prefix] : [];
9634
- const paths = [];
9635
- for (const layer of layersOf(schema)) {
9636
- const def = defOf(layer);
9637
- if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
9638
- else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
9639
- else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
9640
- else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
9641
- else if (def.type === "intersection") {
9642
- if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
9643
- if (def.right) paths.push(...collectZodSecretPaths(def.right, prefix));
9644
- }
9645
- }
9646
- return [...new Set(paths)];
9647
- }
9648
- /**
9649
- * Record kinds (D881): the declaration an addon makes of one kind of fact, the
9650
- * Zod row schema that describes it, and the helper that turns the two into the
9651
- * JSON the engine stores.
9652
- *
9653
- * A kind is declared ONCE, next to the code that owns it, and the engine's
9654
- * reconciler creates its indexes - nothing creates DDL at runtime from a write.
9655
- * Everything the generic machinery needs (secret fields, the entry descriptor)
9656
- * is DERIVED from the Zod schema: `.meta({ secret: true })` marks a credential,
9657
- * never a field name (D879).
9658
- */
9659
- /** A JSON field path: `trackId`, `auth.accessToken`. Charset-restricted: it is rendered into DDL. */
9660
- var RecordFieldPathSchema = string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,63}(\.[A-Za-z_][A-Za-z0-9_]{0,63}){0,3}$/);
9661
- var RecordIndexSchema = object({
9662
- name: string().regex(/^[a-z][a-z0-9_]{0,31}$/),
9663
- fields: array(RecordFieldPathSchema).min(1).max(4),
9664
- unique: boolean().optional()
9665
- }).strict();
9666
- var RecordRetentionSchema = union([
9667
- object({
9668
- ttlMs: number().int().positive(),
9669
- from: _enum(["at", "write"])
9670
- }).strict(),
9671
- object({
9672
- maxRows: number().int().positive().optional(),
9673
- maxRowsPerDevice: number().int().positive().optional()
9674
- }).strict().refine((r) => r.maxRows !== void 0 || r.maxRowsPerDevice !== void 0, { message: "name maxRows, maxRowsPerDevice or both" }),
9675
- object({
9676
- owner: string().min(1),
9677
- reason: string().min(1)
9678
- }).strict(),
9679
- object({ none: string().min(1) }).strict()
9680
- ]);
9681
- /** The descriptor a generic entries UI reads (D881 "generic entries"). */
9682
- var EntryDescriptorSchema = object({
9683
- title: string().min(1),
9684
- /** The record body field shown as the entry's name. */
9685
- nameField: string().min(1),
9686
- /** JSON Schema of the entry body; `secret: true` marks a password field. */
9687
- fields: record(string(), unknown())
9688
- }).strict();
9689
- /** What crosses the wire from an addon: JSON only, no Zod. An unknown key is refused. */
9690
- var RecordKindDeclarationSchema = object({
9691
- database: _enum(RECORD_DATABASES).default("settings"),
9692
- class: _enum(RECORD_CLASSES),
9693
- writeTrigger: string().min(1),
9694
- device: _enum([
9695
- "required",
9696
- "optional",
9697
- "none"
9698
- ]).default("none"),
9699
- at: _enum(["required", "none"]).default("none"),
9700
- indexes: array(RecordIndexSchema).max(8).default([]),
9701
- retention: RecordRetentionSchema,
9702
- deviceRemoval: _enum(["delete", "none"]).default("none"),
9703
- maxDataBytes: number().int().positive().max(RECORD_MAX_DATA_BYTES).default(RECORD_DEFAULT_MAX_DATA_BYTES),
9704
- /** Derived from the Zod schema; never written by hand. */
9705
- secretPaths: array(string().min(1)).default([]),
9706
- /** The id key of each array that carries a secret (`{ headers: 'name' }`). */
9707
- secretArrayKeys: record(string(), string()).default({}),
9708
- /** Kinds that must agree through `transact` sit in one file under one family name. */
9709
- transactFamily: string().min(1).optional(),
9710
- entry: EntryDescriptorSchema.optional()
9711
- }).strict();
9712
- /** The kind names: lower-case, dash separated, short enough for an index name. */
9713
- var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
9714
- /** Cross-field rules a declaration must satisfy. Throws {@link DeclarationRefusedError}. */
9715
- function assertValidDeclaration(name, decl) {
9716
- const refuse = (why) => {
9717
- throw new DeclarationRefusedError(`kind "${name}": ${why}`);
9718
- };
9719
- if (!RecordKindNameSchema.safeParse(name).success) refuse("the name must be lower-case-dashed");
9720
- const seen = /* @__PURE__ */ new Set();
9721
- for (const index of decl.indexes) {
9722
- if (seen.has(index.name)) refuse(`index name "${index.name}" is declared twice`);
9723
- seen.add(index.name);
9724
- }
9725
- const { retention } = decl;
9726
- const hasSecrets = decl.secretPaths.length > 0;
9727
- if ("ttlMs" in retention) {
9728
- if (retention.from === "at" && decl.at !== "required") refuse("a ttl from \"at\" needs at: \"required\"");
9729
- if (hasSecrets) refuse("a kind with secret fields cannot use engine TTL (its keychain values would be orphaned)");
9730
- }
9731
- if ("maxRows" in retention || "maxRowsPerDevice" in retention) {
9732
- if (retention.maxRowsPerDevice !== void 0 && decl.device !== "required") refuse("maxRowsPerDevice needs device: \"required\"");
9733
- if (decl.at !== "required") refuse("a row bound trims the oldest \"at\": it needs at: \"required\"");
9734
- if (hasSecrets) refuse("a kind with secret fields cannot use an engine row bound");
9735
- const bound = Math.max(retention.maxRows ?? 0, retention.maxRowsPerDevice ?? 0);
9736
- if (decl.database === "settings" && bound > 5e4) refuse(`a row bound above ${RECORD_SETTINGS_MAX_ROWS} belongs in database: "events" (a big delete on the settings thread stalls every configuration read)`);
9737
- }
9738
- if (decl.deviceRemoval === "delete" && decl.device === "none") refuse("deviceRemoval: \"delete\" needs a device: \"required\" or \"optional\"");
9739
- for (const path of decl.secretPaths) for (const prefix of secretArrayPrefixes(path)) if (decl.secretArrayKeys[prefix] === void 0) refuse(`secret path "${path}" crosses the array "${prefix}": declare its id key in secretArrayKeys`);
9740
- }
9741
- function bodyFieldsOf(schema) {
9742
- return { ...toJSONSchema(schema, {
9743
- unrepresentable: "any",
9744
- io: "input"
9745
- }) };
9746
- }
9747
- function isRecordLike(value) {
9748
- return typeof value === "object" && value !== null && !Array.isArray(value);
9749
- }
9750
- /** Does every branch of the body schema carry `field`? (A discriminated union has several.) */
9751
- function hasNameField(json, field) {
9752
- const branches = json["oneOf"] ?? json["anyOf"];
9753
- if (Array.isArray(branches)) return branches.length > 0 && branches.every((b) => isRecordLike(b) && hasNameField(b, field));
9754
- const properties = json["properties"];
9755
- return isRecordLike(properties) && Object.prototype.hasOwnProperty.call(properties, field);
9756
- }
9757
- /**
9758
- * Declare the kinds of a package. The keys are the kind names. The result is
9759
- * what `ctx.records.declare` sends to the engine and what `ctx.records.kind`
9760
- * types its handle from.
9761
- */
9762
- function defineRecordKinds(specs) {
9763
- const out = {};
9764
- for (const [name, spec] of Object.entries(specs)) {
9765
- const secretPaths = collectZodSecretPaths(spec.schema);
9766
- const nameField = spec.entry?.nameField ?? "name";
9767
- const fields = bodyFieldsOf(spec.schema);
9768
- if (spec.entry !== void 0 && !hasNameField(fields, nameField)) throw new DeclarationRefusedError(`kind "${name}": an entry kind's schema needs a "${nameField}" field`);
9769
- const parsed = RecordKindDeclarationSchema.parse({
9770
- database: spec.database,
9771
- class: spec.class,
9772
- writeTrigger: spec.writeTrigger,
9773
- device: spec.device,
9774
- at: spec.at,
9775
- indexes: spec.indexes === void 0 ? void 0 : [...spec.indexes],
9776
- retention: spec.retention,
9777
- deviceRemoval: spec.deviceRemoval,
9778
- maxDataBytes: spec.maxDataBytes,
9779
- secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
9780
- secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
9781
- transactFamily: spec.transactFamily,
9782
- entry: spec.entry === void 0 ? void 0 : {
9783
- title: spec.entry.title,
9784
- nameField,
9785
- fields
9786
- }
9787
- });
9788
- assertValidDeclaration(name, parsed);
9789
- out[name] = {
9790
- name,
9791
- schema: spec.schema,
9792
- declaration: parsed
9793
- };
9794
- }
9795
- return out;
9796
- }
9797
- /**
9798
- * Entry kinds (D881): every list of user-created entries (notification
9799
- * targets, brokers, LLM profiles) is ONE record kind with an id, a name and a
9800
- * kind-specific body, and the admin UI lists/adds/edits/deletes all of them
9801
- * through the `record-entries` capability with password fields derived from
9802
- * the schema (`.meta({ secret: true })`).
9803
- */
9804
- /** The body of a plain entry: a name and the fields the kind adds. */
9805
- function entryBody(shape) {
9806
- return object({
9807
- name: string().min(1),
9808
- enabled: boolean().optional(),
9809
- ...shape
9810
- });
9811
- }
9812
- /**
9813
- * Declare one entry kind. An entry is configuration: never trimmed by the
9814
- * engine, never aged, belonging to no device.
9815
- */
9816
- function defineEntryKind(name, spec) {
9817
- const defined = defineRecordKinds({ [name]: {
9818
- schema: spec.schema,
9819
- class: "config",
9820
- writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
9821
- retention: { none: "an entry goes only when the operator deletes it" },
9822
- ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
9823
- ...spec.database === void 0 ? {} : { database: spec.database },
9824
- ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9825
- ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
9826
- ...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
9827
- entry: {
9828
- title: spec.title,
9829
- ...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
9830
- }
9831
- } })[name];
9832
- if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
9833
- return defined;
9834
- }
9835
9581
  object({
9836
9582
  field: string().min(1),
9837
9583
  on: literal("advance")
@@ -9920,6 +9666,286 @@ function optionalMethod(input, output, options) {
9920
9666
  providerOptional: true
9921
9667
  };
9922
9668
  }
9669
+ function defOf(schema) {
9670
+ const zodBag = Reflect.get(schema, "_zod");
9671
+ if (typeof zodBag !== "object" || zodBag === null) return {};
9672
+ const def = Reflect.get(zodBag, "def");
9673
+ if (typeof def !== "object" || def === null) return {};
9674
+ return def;
9675
+ }
9676
+ function declaredHere(schema) {
9677
+ return globalRegistry.get(schema)?.secret === true;
9678
+ }
9679
+ /** Wrapper kinds whose `innerType` is the field itself. */
9680
+ var WRAPPERS = new Set([
9681
+ "optional",
9682
+ "nullable",
9683
+ "default",
9684
+ "prefault",
9685
+ "catch",
9686
+ "readonly",
9687
+ "nonoptional"
9688
+ ]);
9689
+ /**
9690
+ * The schema layers that make up ONE field: the schema itself and every
9691
+ * wrapper below it (optional / default / nullable / readonly / catch), plus
9692
+ * both sides of a pipe (`z.preprocess` puts the real schema on `out`,
9693
+ * `.transform` on `in`).
9694
+ */
9695
+ function layersOf(schema) {
9696
+ const out = [];
9697
+ const queue = [schema];
9698
+ while (queue.length > 0) {
9699
+ const s = queue.shift();
9700
+ if (!s || out.includes(s)) continue;
9701
+ out.push(s);
9702
+ const def = defOf(s);
9703
+ if (def.type !== void 0 && WRAPPERS.has(def.type) && def.innerType) queue.push(def.innerType);
9704
+ if (def.type === "pipe") {
9705
+ if (def.in) queue.push(def.in);
9706
+ if (def.out) queue.push(def.out);
9707
+ }
9708
+ }
9709
+ return out;
9710
+ }
9711
+ /** True when any layer of this field declares `.meta({ secret: true })`. */
9712
+ function isZodSecret(schema) {
9713
+ return layersOf(schema).some(declaredHere);
9714
+ }
9715
+ /**
9716
+ * Every declared secret path in a config schema: dotted keys, `[]` for "each
9717
+ * element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
9718
+ * map" (`applianceSecrets{}`). A declared
9719
+ * object or array is reported as one path and not descended into — everything
9720
+ * under it is secret.
9721
+ */
9722
+ function collectZodSecretPaths(schema, prefix = "") {
9723
+ if (isZodSecret(schema)) return prefix.length > 0 ? [prefix] : [];
9724
+ const paths = [];
9725
+ for (const layer of layersOf(schema)) {
9726
+ const def = defOf(layer);
9727
+ if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
9728
+ else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
9729
+ else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
9730
+ else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
9731
+ else if (def.type === "intersection") {
9732
+ if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
9733
+ if (def.right) paths.push(...collectZodSecretPaths(def.right, prefix));
9734
+ }
9735
+ }
9736
+ return [...new Set(paths)];
9737
+ }
9738
+ /**
9739
+ * Record kinds (D881): the declaration an addon makes of one kind of fact, the
9740
+ * Zod row schema that describes it, and the helper that turns the two into the
9741
+ * JSON the engine stores.
9742
+ *
9743
+ * A kind is declared ONCE, next to the code that owns it, and the engine's
9744
+ * reconciler creates its indexes - nothing creates DDL at runtime from a write.
9745
+ * Everything the generic machinery needs (secret fields, the entry descriptor)
9746
+ * is DERIVED from the Zod schema: `.meta({ secret: true })` marks a credential,
9747
+ * never a field name (D879).
9748
+ */
9749
+ /** A JSON field path: `trackId`, `auth.accessToken`. Charset-restricted: it is rendered into DDL. */
9750
+ var RecordFieldPathSchema = string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,63}(\.[A-Za-z_][A-Za-z0-9_]{0,63}){0,3}$/);
9751
+ var RecordIndexSchema = object({
9752
+ name: string().regex(/^[a-z][a-z0-9_]{0,31}$/),
9753
+ fields: array(RecordFieldPathSchema).min(1).max(4),
9754
+ unique: boolean().optional()
9755
+ }).strict();
9756
+ var RecordRetentionSchema = union([
9757
+ object({
9758
+ ttlMs: number().int().positive(),
9759
+ from: _enum(["at", "write"])
9760
+ }).strict(),
9761
+ object({
9762
+ maxRows: number().int().positive().optional(),
9763
+ maxRowsPerDevice: number().int().positive().optional()
9764
+ }).strict().refine((r) => r.maxRows !== void 0 || r.maxRowsPerDevice !== void 0, { message: "name maxRows, maxRowsPerDevice or both" }),
9765
+ object({
9766
+ owner: string().min(1),
9767
+ reason: string().min(1)
9768
+ }).strict(),
9769
+ object({ none: string().min(1) }).strict()
9770
+ ]);
9771
+ /** The descriptor a generic entries UI reads (D881 "generic entries"). */
9772
+ var EntryDescriptorSchema = object({
9773
+ title: string().min(1),
9774
+ /** The record body field shown as the entry's name. */
9775
+ nameField: string().min(1),
9776
+ /** JSON Schema of the entry body; `secret: true` marks a password field. */
9777
+ fields: record(string(), unknown())
9778
+ }).strict();
9779
+ /** What crosses the wire from an addon: JSON only, no Zod. An unknown key is refused. */
9780
+ var RecordKindDeclarationSchema = object({
9781
+ database: _enum(RECORD_DATABASES).default("settings"),
9782
+ class: _enum(RECORD_CLASSES),
9783
+ writeTrigger: string().min(1),
9784
+ device: _enum([
9785
+ "required",
9786
+ "optional",
9787
+ "none"
9788
+ ]).default("none"),
9789
+ at: _enum(["required", "none"]).default("none"),
9790
+ indexes: array(RecordIndexSchema).max(8).default([]),
9791
+ retention: RecordRetentionSchema,
9792
+ deviceRemoval: _enum(["delete", "none"]).default("none"),
9793
+ maxDataBytes: number().int().positive().max(RECORD_MAX_DATA_BYTES).default(RECORD_DEFAULT_MAX_DATA_BYTES),
9794
+ /** Derived from the Zod schema; never written by hand. */
9795
+ secretPaths: array(string().min(1)).default([]),
9796
+ /** The id key of each array that carries a secret (`{ headers: 'name' }`). */
9797
+ secretArrayKeys: record(string(), string()).default({}),
9798
+ /** Kinds that must agree through `transact` sit in one file under one family name. */
9799
+ transactFamily: string().min(1).optional(),
9800
+ entry: EntryDescriptorSchema.optional()
9801
+ }).strict();
9802
+ /** The kind names: lower-case, dash separated, short enough for an index name. */
9803
+ var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
9804
+ /** Cross-field rules a declaration must satisfy. Throws {@link DeclarationRefusedError}. */
9805
+ function assertValidDeclaration(name, decl) {
9806
+ const refuse = (why) => {
9807
+ throw new DeclarationRefusedError(`kind "${name}": ${why}`);
9808
+ };
9809
+ if (!RecordKindNameSchema.safeParse(name).success) refuse("the name must be lower-case-dashed");
9810
+ const seen = /* @__PURE__ */ new Set();
9811
+ for (const index of decl.indexes) {
9812
+ if (seen.has(index.name)) refuse(`index name "${index.name}" is declared twice`);
9813
+ seen.add(index.name);
9814
+ }
9815
+ const { retention } = decl;
9816
+ const hasSecrets = decl.secretPaths.length > 0;
9817
+ if ("ttlMs" in retention) {
9818
+ if (retention.from === "at" && decl.at !== "required") refuse("a ttl from \"at\" needs at: \"required\"");
9819
+ if (hasSecrets) refuse("a kind with secret fields cannot use engine TTL (its keychain values would be orphaned)");
9820
+ }
9821
+ if ("maxRows" in retention || "maxRowsPerDevice" in retention) {
9822
+ if (retention.maxRowsPerDevice !== void 0 && decl.device !== "required") refuse("maxRowsPerDevice needs device: \"required\"");
9823
+ if (decl.at !== "required") refuse("a row bound trims the oldest \"at\": it needs at: \"required\"");
9824
+ if (hasSecrets) refuse("a kind with secret fields cannot use an engine row bound");
9825
+ const bound = Math.max(retention.maxRows ?? 0, retention.maxRowsPerDevice ?? 0);
9826
+ if (decl.database === "settings" && bound > 5e4) refuse(`a row bound above ${RECORD_SETTINGS_MAX_ROWS} belongs in database: "events" (a big delete on the settings thread stalls every configuration read)`);
9827
+ }
9828
+ if (decl.deviceRemoval === "delete" && decl.device === "none") refuse("deviceRemoval: \"delete\" needs a device: \"required\" or \"optional\"");
9829
+ for (const path of decl.secretPaths) for (const prefix of secretArrayPrefixes(path)) if (decl.secretArrayKeys[prefix] === void 0) refuse(`secret path "${path}" crosses the array "${prefix}": declare its id key in secretArrayKeys`);
9830
+ }
9831
+ function bodyFieldsOf(schema) {
9832
+ return { ...toJSONSchema(schema, {
9833
+ unrepresentable: "any",
9834
+ io: "input"
9835
+ }) };
9836
+ }
9837
+ function isRecordLike(value) {
9838
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9839
+ }
9840
+ /** Does every branch of the body schema carry `field`? (A discriminated union has several.) */
9841
+ function hasNameField(json, field) {
9842
+ const branches = json["oneOf"] ?? json["anyOf"];
9843
+ if (Array.isArray(branches)) return branches.length > 0 && branches.every((b) => isRecordLike(b) && hasNameField(b, field));
9844
+ const properties = json["properties"];
9845
+ return isRecordLike(properties) && Object.prototype.hasOwnProperty.call(properties, field);
9846
+ }
9847
+ /**
9848
+ * Declare the kinds of a package. The keys are the kind names. The result is
9849
+ * what `ctx.records.declare` sends to the engine and what `ctx.records.kind`
9850
+ * types its handle from.
9851
+ */
9852
+ function defineRecordKinds(specs) {
9853
+ const out = {};
9854
+ for (const [name, spec] of Object.entries(specs)) {
9855
+ const secretPaths = collectZodSecretPaths(spec.schema);
9856
+ const nameField = spec.entry?.nameField ?? "name";
9857
+ const fields = bodyFieldsOf(spec.schema);
9858
+ if (spec.entry !== void 0 && !hasNameField(fields, nameField)) throw new DeclarationRefusedError(`kind "${name}": an entry kind's schema needs a "${nameField}" field`);
9859
+ const parsed = RecordKindDeclarationSchema.parse({
9860
+ database: spec.database,
9861
+ class: spec.class,
9862
+ writeTrigger: spec.writeTrigger,
9863
+ device: spec.device,
9864
+ at: spec.at,
9865
+ indexes: spec.indexes === void 0 ? void 0 : [...spec.indexes],
9866
+ retention: spec.retention,
9867
+ deviceRemoval: spec.deviceRemoval,
9868
+ maxDataBytes: spec.maxDataBytes,
9869
+ secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
9870
+ secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
9871
+ transactFamily: spec.transactFamily,
9872
+ entry: spec.entry === void 0 ? void 0 : {
9873
+ title: spec.entry.title,
9874
+ nameField,
9875
+ fields
9876
+ }
9877
+ });
9878
+ assertValidDeclaration(name, parsed);
9879
+ out[name] = {
9880
+ name,
9881
+ schema: spec.schema,
9882
+ declaration: parsed
9883
+ };
9884
+ }
9885
+ return out;
9886
+ }
9887
+ /**
9888
+ * Entry kinds (D881): every list of user-created entries (notification
9889
+ * targets, brokers, LLM profiles) is ONE record kind with an id, a name and a
9890
+ * kind-specific body, and the admin UI lists/adds/edits/deletes all of them
9891
+ * through the `record-entries` capability with password fields derived from
9892
+ * the schema (`.meta({ secret: true })`).
9893
+ */
9894
+ /** The body of a plain entry: a name and the fields the kind adds. */
9895
+ function entryBody(shape) {
9896
+ return object({
9897
+ name: string().min(1),
9898
+ enabled: boolean().optional(),
9899
+ ...shape
9900
+ });
9901
+ }
9902
+ /**
9903
+ * Declare one entry kind. An entry is configuration: never trimmed by the
9904
+ * engine, never aged, belonging to no device.
9905
+ */
9906
+ function defineEntryKind(name, spec) {
9907
+ const defined = defineRecordKinds({ [name]: {
9908
+ schema: spec.schema,
9909
+ class: "config",
9910
+ writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
9911
+ retention: { none: "an entry goes only when the operator deletes it" },
9912
+ ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
9913
+ ...spec.database === void 0 ? {} : { database: spec.database },
9914
+ ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9915
+ ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
9916
+ ...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
9917
+ entry: {
9918
+ title: spec.title,
9919
+ ...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
9920
+ }
9921
+ } })[name];
9922
+ if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
9923
+ return defined;
9924
+ }
9925
+ /**
9926
+ * The share-view scope kinds a share-link token can carry. The hub mints
9927
+ * and validates them (`ShareViewGrantSchema` builds its
9928
+ * enum from this list); a data plane names the kinds it ACCEPTS in `share`.
9929
+ */
9930
+ var SHARE_VIEW_KINDS = ["grid-view", "events-view"];
9931
+ /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
9932
+ var PeerBytesTicketSchema = object({
9933
+ /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
9934
+ url: string().min(1),
9935
+ /**
9936
+ * The HOST node this URL means something on — the hub or a named agent,
9937
+ * never a runner. {@link AddonPeerBytes.open} compares it to its own and
9938
+ * refuses `cross-node` by name when they differ, without dialling.
9939
+ */
9940
+ hostNodeId: string().min(1),
9941
+ expiresAtMs: number().int().nonnegative(),
9942
+ /**
9943
+ * What the producer DECLARED the body to be, when it knows — `null` when it
9944
+ * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
9945
+ * must be able to tell "the producer did not say" from "the body is empty".
9946
+ */
9947
+ declaredBytes: number().int().nonnegative().nullable()
9948
+ });
9923
9949
  var READING_QUANTITIES = [
9924
9950
  "temperature",
9925
9951
  "pressure",
@@ -10498,7 +10524,6 @@ var CAP_NAMES = Object.freeze([
10498
10524
  "climate-control",
10499
10525
  "color",
10500
10526
  "composer",
10501
- "connection-test",
10502
10527
  "connectivity",
10503
10528
  "consumables",
10504
10529
  "contact",
@@ -12518,8 +12543,7 @@ Object.freeze({
12518
12543
  "offerArtifact": 3e5,
12519
12544
  "readResult": 3e5,
12520
12545
  "submitJob": 18e5,
12521
- "uploadDataset": 72e5,
12522
- "validateCredentials": 3e4
12546
+ "uploadDataset": 72e5
12523
12547
  }
12524
12548
  });
12525
12549
  object({
@@ -13874,6 +13898,432 @@ var IntegrationInstanceRefSchema = object({
13874
13898
  ]),
13875
13899
  candidateStableId: string().optional()
13876
13900
  });
13901
+ /**
13902
+ * notification-output — canonical, capability-gated notification delivery.
13903
+ *
13904
+ * Apprise-derived model (see
13905
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
13906
+ * callers emit ONE canonical `Notification`; each provider declares a
13907
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
13908
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
13909
+ * message to what the kind supports — callers never special-case a service.
13910
+ *
13911
+ * DESIGN DECISIONS (locked):
13912
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
13913
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
13914
+ * cap. Rationale: the admin UI needs one uniform surface across the
13915
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
13916
+ * alternative would fork the UI per addon and cannot host the
13917
+ * discovery→adopt flow.
13918
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
13919
+ * the generated cap-mount auto-`concatCollection`-fans them across every
13920
+ * registered provider (notifiers addon + HA addon) so one catalog is
13921
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
13922
+ * `addonId` the generated collection router extracts from the call input.
13923
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
13924
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
13925
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
13926
+ * base64 fallback needed.
13927
+ *
13928
+ * TODO (deferred, closed-set change — separate decision): add
13929
+ * `providerKind: 'notify'` so notification providers surface on the unified
13930
+ * admin "Integrations" page.
13931
+ */
13932
+ /**
13933
+ * Zentik-derived typed-media enum — the superset across every kind. Each
13934
+ * adapter picks what it supports and the degrade engine filters the rest.
13935
+ */
13936
+ var AttachmentMediaTypeSchema = _enum([
13937
+ "image",
13938
+ "video",
13939
+ "gif",
13940
+ "audio",
13941
+ "icon"
13942
+ ]);
13943
+ /**
13944
+ * A single attachment. Exactly one of `url` (remote source, most adapters
13945
+ * prefer this) or `bytes` (inline source; required for Pushover-style
13946
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
13947
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
13948
+ */
13949
+ var AttachmentSchema = object({
13950
+ mediaType: AttachmentMediaTypeSchema,
13951
+ url: string().optional(),
13952
+ bytes: _instanceof(Uint8Array).optional(),
13953
+ mime: string().optional(),
13954
+ name: string().optional()
13955
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
13956
+ var NotificationFormatSchema = _enum([
13957
+ "text",
13958
+ "markdown",
13959
+ "html"
13960
+ ]);
13961
+ /**
13962
+ * The CLOSED icon vocabulary an action button may use.
13963
+ *
13964
+ * A closed set, not a free string, and that is the whole point: an arbitrary
13965
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
13966
+ * notices — the same class of gap as a zone vocabulary nothing produced
13967
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
13968
+ * declares `actionIcons: false` and the degrade engine strips the field.
13969
+ *
13970
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
13971
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
13972
+ * differently.
13973
+ *
13974
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
13975
+ *
13976
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
13977
+ * the whole set onto its own renderer's vocabulary through a
13978
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
13979
+ * fallback, so adding a member here fails every adapter's build until someone
13980
+ * decides its glyph, which is the only place that decision can be made
13981
+ * honestly.
13982
+ *
13983
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
13984
+ * `disarm` straight through; iOS feeds that string to
13985
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
13986
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
13987
+ * mapping, and "the field is documented" is not "the value renders".
13988
+ *
13989
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
13990
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
13991
+ * so an addon that emits a token the running hub does not know does not lose an
13992
+ * icon — its whole `send` fails Zod validation and the notification never
13993
+ * arrives. Never emit a new token from an addon before the train carrying it.
13994
+ */
13995
+ var NotificationActionIconSchema = _enum([
13996
+ "acknowledge",
13997
+ "dismiss",
13998
+ "silence",
13999
+ "snooze",
14000
+ "view",
14001
+ "play",
14002
+ "open",
14003
+ "close",
14004
+ "lock",
14005
+ "unlock",
14006
+ "arm",
14007
+ "arm-home",
14008
+ "arm-away",
14009
+ "arm-night",
14010
+ "disarm",
14011
+ "light",
14012
+ "alert",
14013
+ "camera"
14014
+ ]);
14015
+ /** A single tap-through action button. */
14016
+ var NotificationActionSchema = object({
14017
+ id: string(),
14018
+ label: string(),
14019
+ url: string().optional(),
14020
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
14021
+ icon: NotificationActionIconSchema.optional(),
14022
+ /**
14023
+ * Renders in a warning style where the notifier supports it.
14024
+ *
14025
+ * A HINT, never a gate. The callback's authority is its token and nothing
14026
+ * else — see `notification-center/action-token.ts` for what that does and
14027
+ * does not buy.
14028
+ */
14029
+ destructive: boolean().optional(),
14030
+ /**
14031
+ * How the tap should REACH the url.
14032
+ *
14033
+ * `navigate` (absent, and every button authored before this field) opens it:
14034
+ * the phone leaves the notification and shows whatever the callback returns.
14035
+ * That is right for a button whose answer the operator wants to read.
14036
+ *
14037
+ * `background` fires it as a POST and stays put. It exists for the buttons
14038
+ * whose whole point is not to interrupt — "silence this for 30 minutes" is
14039
+ * an answer to the notification, and being thrown into a browser tab to
14040
+ * confirm it costs more attention than the notification did. A backend that
14041
+ * cannot do a background call renders it as an ordinary link (the adapters
14042
+ * fall back rather than dropping the button), so this is a preference, never
14043
+ * a requirement.
14044
+ */
14045
+ mode: _enum(["navigate", "background"]).optional()
14046
+ });
14047
+ /** A delivery parameter value: what a notifier declared it takes. */
14048
+ var DeliveryParamValueSchema = union([
14049
+ string(),
14050
+ number(),
14051
+ boolean()
14052
+ ]);
14053
+ /**
14054
+ * One extra delivery parameter a notifier KIND declares (D938). A rule that
14055
+ * addresses a target of that kind may set it; a kind that declares nothing is
14056
+ * never handed one. The admin UI derives its control from this descriptor —
14057
+ * no editor knows a service by name.
14058
+ *
14059
+ * Optional fields are `.optional()`, never `.default()` (a Zod default does not
14060
+ * run on the addon cap path).
14061
+ */
14062
+ var DeliveryParamSchema = object({
14063
+ key: string().min(1),
14064
+ label: string(),
14065
+ /** i18n key of the label, resolved by the declaring addon's namespace. */
14066
+ labelKey: string().optional(),
14067
+ type: _enum([
14068
+ "enum",
14069
+ "number",
14070
+ "boolean",
14071
+ "string"
14072
+ ]),
14073
+ /** `enum` only: the closed set of accepted values. */
14074
+ options: array(object({
14075
+ value: string(),
14076
+ label: string()
14077
+ })).optional(),
14078
+ /** `number` only. */
14079
+ min: number().optional(),
14080
+ max: number().optional(),
14081
+ /** What the notifier does when the rule sets nothing; shown as the placeholder. */
14082
+ default: DeliveryParamValueSchema.optional()
14083
+ });
14084
+ /**
14085
+ * The canonical notification. `body` is the only hard field (Apprise model).
14086
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
14087
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
14088
+ * the adapter maps this ordinal onto its native level. `level?` is an
14089
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
14090
+ * `priority` for that one target.
14091
+ */
14092
+ var NotificationSchema = object({
14093
+ body: string(),
14094
+ title: string().optional(),
14095
+ format: NotificationFormatSchema.default("text"),
14096
+ priority: number().int().min(1).max(5).default(3),
14097
+ level: string().optional(),
14098
+ attachments: array(AttachmentSchema).optional(),
14099
+ clickUrl: string().optional(),
14100
+ actions: array(NotificationActionSchema).optional(),
14101
+ sound: string().optional(),
14102
+ ttl: number().optional(),
14103
+ tag: string().optional(),
14104
+ deviceId: number().optional(),
14105
+ eventId: string().optional(),
14106
+ metadata: record(string(), unknown()).optional(),
14107
+ /**
14108
+ * Values for the parameters the target's kind DECLARES in
14109
+ * {@link TargetKindSchema} `deliveryParams`. Already validated against the
14110
+ * declaration by the sender; a notifier reads only the keys it declared.
14111
+ */
14112
+ delivery: record(string(), DeliveryParamValueSchema).optional()
14113
+ });
14114
+ /** One declared native severity/priority level for a kind. */
14115
+ var TargetKindLevelSchema = object({
14116
+ id: string(),
14117
+ label: string(),
14118
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
14119
+ ordinal: number().int().min(1).max(5).nullable(),
14120
+ flags: object({
14121
+ critical: boolean().optional(),
14122
+ silent: boolean().optional(),
14123
+ noPush: boolean().optional()
14124
+ }).optional(),
14125
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
14126
+ requires: array(string()).optional(),
14127
+ description: string().optional()
14128
+ });
14129
+ /** The full capability block consulted before dispatch. */
14130
+ var TargetKindCapsSchema = object({
14131
+ attachments: object({
14132
+ mediaTypes: array(AttachmentMediaTypeSchema),
14133
+ mode: _enum([
14134
+ "url",
14135
+ "bytes",
14136
+ "both"
14137
+ ]),
14138
+ max: number().int().nonnegative(),
14139
+ maxBytes: number().int().positive().optional()
14140
+ }),
14141
+ /** Max action buttons (0 = none). */
14142
+ actions: number().int().nonnegative(),
14143
+ /**
14144
+ * Whether this kind renders a per-action ICON.
14145
+ *
14146
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
14147
+ * run on the addon cap path — three production failures in one day taught
14148
+ * this repo that once. Absent is read as false by the degrade engine, which
14149
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
14150
+ * assumed and dropped costs the operator's trust in the field.
14151
+ */
14152
+ actionIcons: boolean().optional(),
14153
+ levels: array(TargetKindLevelSchema),
14154
+ format: array(NotificationFormatSchema),
14155
+ clickUrl: boolean(),
14156
+ sound: boolean(),
14157
+ ttl: boolean(),
14158
+ bodyMaxLen: number().int().positive()
14159
+ });
14160
+ /**
14161
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
14162
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
14163
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
14164
+ * the union is large and not meant for runtime validation here; the exported
14165
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
14166
+ */
14167
+ var ConfigSchemaPassthrough$4 = unknown();
14168
+ var TargetKindSchema = object({
14169
+ kind: string(),
14170
+ label: string(),
14171
+ icon: string(),
14172
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
14173
+ addonId: string(),
14174
+ /**
14175
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
14176
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
14177
+ * when the addon bundles no icon for that kind — the client then falls back
14178
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
14179
+ *
14180
+ * Root-relative on purpose: it resolves against whatever origin serves a web
14181
+ * client, and a native client joins it onto its own hub base.
14182
+ *
14183
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
14184
+ * field that survived only because the runtime cap-router forwards provider
14185
+ * output verbatim — so every consumer had to re-declare it by hand to stop
14186
+ * its own Zod parse from stripping it, and the whole arrangement would have
14187
+ * broken silently the moment output validation was tightened anywhere.
14188
+ */
14189
+ iconUrl: string().optional(),
14190
+ /**
14191
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
14192
+ *
14193
+ * The server knows this and therefore says it, because the client cannot
14194
+ * safely guess: a React-Native client renders SVG and raster through two
14195
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
14196
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
14197
+ * placeholder glyph for every vector icon while the web build looked fine.
14198
+ *
14199
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
14200
+ * not been updated — a client that cannot determine the type should prefer
14201
+ * its raster path, which is the safe default for an unknown image.
14202
+ */
14203
+ iconMediaType: string().optional(),
14204
+ configSchema: ConfigSchemaPassthrough$4,
14205
+ /**
14206
+ * The config keys that decide WHERE this kind's secrets are sent (server,
14207
+ * base URL, instance, chat, recipient). A save that changes any of them
14208
+ * while leaving a secret redacted is refused by the provider (D634), so an
14209
+ * editor uses this to ask for the secret on exactly those edits instead of
14210
+ * on every edit.
14211
+ *
14212
+ * Optional: absent from a provider that holds no secret (the Home Assistant
14213
+ * kinds) and from a provider older than this field — an editor that finds
14214
+ * it absent must assume ANY edit may need the secret.
14215
+ */
14216
+ destinationKeys: array(string()).optional(),
14217
+ supportsDiscovery: boolean(),
14218
+ /**
14219
+ * Extra delivery parameters a rule may set for a target of this kind (D938).
14220
+ * Absent = none; the rule editor then offers nothing.
14221
+ */
14222
+ deliveryParams: array(DeliveryParamSchema).optional(),
14223
+ caps: TargetKindCapsSchema
14224
+ });
14225
+ /**
14226
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
14227
+ * (return a presence marker only) when serving `listTargets` — never
14228
+ * round-trip a stored secret to the UI.
14229
+ */
14230
+ var TargetSchema = object({
14231
+ id: string(),
14232
+ name: string(),
14233
+ kind: string(),
14234
+ addonId: string(),
14235
+ enabled: boolean(),
14236
+ config: record(string(), unknown())
14237
+ });
14238
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
14239
+ var DiscoveredTargetSchema = object({
14240
+ kind: string(),
14241
+ suggestedName: string(),
14242
+ config: record(string(), unknown())
14243
+ });
14244
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
14245
+ var RenderedAsSchema = object({
14246
+ level: string(),
14247
+ format: NotificationFormatSchema,
14248
+ attachmentsSent: number().int().nonnegative(),
14249
+ actionsSent: number().int().nonnegative(),
14250
+ truncated: boolean(),
14251
+ dropped: array(string())
14252
+ });
14253
+ var SendResultSchema = object({
14254
+ success: boolean(),
14255
+ error: string().optional(),
14256
+ renderedAs: RenderedAsSchema.optional()
14257
+ });
14258
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
14259
+ var TestResultSchema = SendResultSchema;
14260
+ /**
14261
+ * Pre-save credential probe against a partial (unsaved) target config.
14262
+ * `resolvedUrl` never includes secrets (token stripped from path).
14263
+ */
14264
+ var ProbeTargetConfigResultSchema = object({
14265
+ ok: boolean(),
14266
+ resolvedUrl: string(),
14267
+ httpStatus: number().int().optional(),
14268
+ stateInstance: string().optional(),
14269
+ error: string().optional()
14270
+ });
14271
+ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
14272
+ kind: string(),
14273
+ config: record(string(), unknown()).optional(),
14274
+ /**
14275
+ * When editing / duplicating, the blank password field never round-trips
14276
+ * the stored token. Pass the source target id so the provider can restore
14277
+ * secrets server-side before dialling (same merge as upsert).
14278
+ */
14279
+ targetId: string().optional()
14280
+ }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
14281
+ kind: string(),
14282
+ config: record(string(), unknown()),
14283
+ /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
14284
+ targetId: string().optional()
14285
+ }), ProbeTargetConfigResultSchema, {
14286
+ kind: "mutation",
14287
+ auth: "admin"
14288
+ }), method(object({
14289
+ targetId: string(),
14290
+ notification: NotificationSchema
14291
+ }), SendResultSchema, {
14292
+ kind: "mutation",
14293
+ auth: "admin"
14294
+ }), method(object({
14295
+ targetId: string(),
14296
+ sample: NotificationSchema.optional()
14297
+ }), TestResultSchema, {
14298
+ kind: "mutation",
14299
+ auth: "admin"
14300
+ }), method(object({
14301
+ target: TargetSchema,
14302
+ /**
14303
+ * Duplicate flow: create a NEW target id while copying secrets from an
14304
+ * existing row. The UI never holds the stored token; blank / redacted
14305
+ * password fields are restored from this source (D634 still applies if
14306
+ * destinations diverge without a re-typed secret).
14307
+ */
14308
+ cloneSecretsFrom: string().optional()
14309
+ }), TargetSchema, {
14310
+ kind: "mutation",
14311
+ auth: "admin"
14312
+ }), method(object({ targetId: string() }), _void(), {
14313
+ kind: "mutation",
14314
+ auth: "admin"
14315
+ }), method(object({
14316
+ targetId: string(),
14317
+ enabled: boolean()
14318
+ }), _void(), {
14319
+ kind: "mutation",
14320
+ auth: "admin"
14321
+ });
14322
+ looseObject({
14323
+ name: string().min(1),
14324
+ enabled: boolean().optional()
14325
+ });
14326
+ TargetSchema.omit({ id: true });
13877
14327
  var EncodeProfileSchema = object({
13878
14328
  video: object({
13879
14329
  codec: _enum([
@@ -22091,7 +22541,6 @@ var SYSTEM_CAP_VOCABULARY = {
22091
22541
  "calendar-source": system(INTERNAL),
22092
22542
  calendars: system("calendars feed schedules and conditions, not device actions"),
22093
22543
  composer: system("device composition — an authoring tool, not an action"),
22094
- "connection-test": system(INTERNAL),
22095
22544
  "core-blocks": system(INTERNAL),
22096
22545
  "custom-model-registry": system(ML),
22097
22546
  "data-store-provider": system(INTERNAL),
@@ -44008,427 +44457,6 @@ var UnitWarningSchema = object({
44008
44457
  assumedUnit: string(),
44009
44458
  message: string()
44010
44459
  });
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(),
44217
- /**
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.
44221
- */
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()
44238
- });
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(),
44253
- /**
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.
44261
- */
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()
44269
- });
44270
- /**
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`.
44276
- */
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
44334
- });
44335
- /**
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.
44339
- */
44340
- var TargetSchema = object({
44341
- id: string(),
44342
- name: string(),
44343
- kind: string(),
44344
- addonId: string(),
44345
- enabled: boolean(),
44346
- config: record(string(), unknown())
44347
- });
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())
44353
- });
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())
44362
- });
44363
- var SendResultSchema = object({
44364
- success: boolean(),
44365
- error: string().optional(),
44366
- renderedAs: RenderedAsSchema.optional()
44367
- });
44368
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
44369
- var TestResultSchema = SendResultSchema;
44370
- /**
44371
- * Pre-save credential probe against a partial (unsaved) target config.
44372
- * `resolvedUrl` never includes secrets (token stripped from path).
44373
- */
44374
- var ProbeTargetConfigResultSchema = object({
44375
- ok: boolean(),
44376
- resolvedUrl: string(),
44377
- httpStatus: number().int().optional(),
44378
- stateInstance: string().optional(),
44379
- error: string().optional()
44380
- });
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"
44431
- });
44432
44460
  _enum([
44433
44461
  "automation",
44434
44462
  "action",
@@ -48306,37 +48334,6 @@ method(object({
48306
48334
  kind: "mutation",
48307
48335
  auth: "admin"
48308
48336
  }), 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
48337
  object({
48341
48338
  /** True when the upstream system considers the entity connected. */
48342
48339
  connected: boolean(),
@@ -54190,10 +54187,9 @@ var AvailableIntegrationTypeSchema = object({
54190
54187
  * locations" checkbox. Provider-declared in the addon manifest. */
54191
54188
  supportsLocationImport: boolean(),
54192
54189
  /**
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.
54190
+ * True when this integration's instances declare a Test (`integration-instances.describe().canTest`,
54191
+ * D1016). An integration that cannot be tested must say so up front rather than offering a
54192
+ * button that always answers the same nonsense.
54197
54193
  */
54198
54194
  canTest: boolean(),
54199
54195
  existingInstances: array(object({
@@ -54247,7 +54243,9 @@ var CreateIntegrationInputSchema = object({
54247
54243
  info: record(string(), unknown()).optional(),
54248
54244
  /** `stableId` of the discovery candidate the form was seeded from (D984). */
54249
54245
  originStableId: string().optional(),
54250
- settings: record(string(), unknown()).optional()
54246
+ settings: record(string(), unknown()).optional(),
54247
+ /** The instance kind being created, for a provider with several; omitted = its only testable kind. */
54248
+ kind: string().min(1).optional()
54251
54249
  });
54252
54250
  var UpdateIntegrationInputSchema = object({
54253
54251
  id: string(),
@@ -54273,13 +54271,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
54273
54271
  }), method(object({ id: string() }), DeleteIntegrationResultSchema, {
54274
54272
  kind: "mutation",
54275
54273
  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
- });
54274
+ }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" });
54283
54275
  /**
54284
54276
  * `integration-instances` - the connection of an addon that holds ONE or SEVERAL
54285
54277
  * (an account, a relay, a destination) as typed records (D985, D986). The addon's
@@ -62522,13 +62514,6 @@ var AddBrokerInputSchema = object({
62522
62514
  });
62523
62515
  var AddBrokerResultSchema = object({ id: string() });
62524
62516
  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
62517
  var StartEmbeddedInputSchema = object({
62533
62518
  port: number().int().min(1).max(65535).default(1883),
62534
62519
  /** Allow anonymous connect (no username/password). Default: false. */
@@ -62565,10 +62550,6 @@ var mqttBrokerCapability = {
62565
62550
  kind: "mutation",
62566
62551
  auth: "admin"
62567
62552
  }),
62568
- testConnection: method(IdInputSchema, TestResultSchema$1, {
62569
- kind: "mutation",
62570
- auth: "admin"
62571
- }),
62572
62553
  startEmbeddedBroker: method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
62573
62554
  kind: "mutation",
62574
62555
  auth: "admin"
@@ -74184,7 +74165,7 @@ var TrainingProviderTypeSchema = object({
74184
74165
  label: string(),
74185
74166
  /** Path under the addon package (e.g. `assets/training-modal/icon.svg`). */
74186
74167
  iconAsset: string(),
74187
- /** Owning `training-provider` addon — collection selector for createEntry. */
74168
+ /** Owning `training-provider` addon. */
74188
74169
  addonId: string(),
74189
74170
  /**
74190
74171
  * Fields for one entry of this type (credentials + defaults). Absent on a
@@ -74229,36 +74210,6 @@ var TrainingProviderDescriptorSchema = object({
74229
74210
  */
74230
74211
  formValues: record(string(), unknown()).optional()
74231
74212
  });
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
74213
  var ProviderJobStateSchema = _enum([
74263
74214
  "pending",
74264
74215
  "starting",
@@ -74306,22 +74257,6 @@ var TrainingLogLineSchema = object({
74306
74257
  text: string()
74307
74258
  });
74308
74259
  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
74260
  entryId: string().min(1),
74326
74261
  gpuType: string(),
74327
74262
  region: string(),
@@ -77707,20 +77642,6 @@ Object.freeze({
77707
77642
  access: "view",
77708
77643
  role: "admin"
77709
77644
  },
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
77645
  "connectivity.getStatus": {
77725
77646
  capName: "connectivity",
77726
77647
  capScope: "device",
@@ -80087,13 +80008,6 @@ Object.freeze({
80087
80008
  access: "view",
80088
80009
  role: "admin"
80089
80010
  },
80090
- "integrations.testConnection": {
80091
- capName: "integrations",
80092
- capScope: "system",
80093
- addonId: null,
80094
- access: "create",
80095
- role: "admin"
80096
- },
80097
80011
  "integrations.update": {
80098
80012
  capName: "integrations",
80099
80013
  capScope: "system",
@@ -81326,13 +81240,6 @@ Object.freeze({
81326
81240
  access: "create",
81327
81241
  role: "admin"
81328
81242
  },
81329
- "mqttBroker.testConnection": {
81330
- capName: "mqtt-broker",
81331
- capScope: "system",
81332
- addonId: null,
81333
- access: "create",
81334
- role: "admin"
81335
- },
81336
81243
  "nativeObjectDetection.getOptions": {
81337
81244
  capName: "native-object-detection",
81338
81245
  capScope: "device",
@@ -86163,20 +86070,6 @@ Object.freeze({
86163
86070
  access: "create",
86164
86071
  role: "admin"
86165
86072
  },
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
86073
  "trainingProvider.describe": {
86181
86074
  capName: "training-provider",
86182
86075
  capScope: "system",
@@ -86247,13 +86140,6 @@ Object.freeze({
86247
86140
  access: "create",
86248
86141
  role: "admin"
86249
86142
  },
86250
- "trainingProvider.updateEntry": {
86251
- capName: "training-provider",
86252
- capScope: "system",
86253
- addonId: null,
86254
- access: "create",
86255
- role: "admin"
86256
- },
86257
86143
  "trainingProvider.uploadDataset": {
86258
86144
  capName: "training-provider",
86259
86145
  capScope: "system",
@@ -86261,13 +86147,6 @@ Object.freeze({
86261
86147
  access: "create",
86262
86148
  role: "admin"
86263
86149
  },
86264
- "trainingProvider.validateCredentials": {
86265
- capName: "training-provider",
86266
- capScope: "system",
86267
- addonId: null,
86268
- access: "create",
86269
- role: "admin"
86270
- },
86271
86150
  "turnProvider.getTurnServers": {
86272
86151
  capName: "turn-provider",
86273
86152
  capScope: "system",
@@ -123255,7 +123134,7 @@ var BrokerProbeCache = class {
123255
123134
  }
123256
123135
  if (this.inflight) await this.inflight.catch(() => void 0);
123257
123136
  }
123258
- /** Force a refresh (used by `testConnection` and the page's Refresh button). */
123137
+ /** Force a refresh (used by the page's Refresh button). */
123259
123138
  async probeNow(id) {
123260
123139
  const broker = this.getBrokers().find((b) => b.id === id);
123261
123140
  if (!broker) return void 0;
@@ -124237,8 +124116,8 @@ var MqttAclStore = class MqttAclStore {
124237
124116
  * - The legacy single embedded broker is an onboard `brokers` record (migrated from the flat
124238
124117
  * `embedded*` keys, same id); `startEmbeddedBroker` / `stopEmbeddedBroker` act on that record.
124239
124118
  * - `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.
124119
+ * - The instance test (`integration-instances.testInstance`) does a one-shot `mqtt.connect`
124120
+ * race against a 3s timeout.
124242
124121
  */
124243
124122
  /** bcrypt cost factor — exported for tests. */
124244
124123
  var MQTT_EMBEDDED_BCRYPT_ROUNDS = 10;
@@ -124374,7 +124253,6 @@ var MqttBrokerAddon = class extends BaseAddon {
124374
124253
  getBrokerConfig: async ({ id }) => this.getBrokerConfig(id),
124375
124254
  addBroker: async (input) => this.addBroker(input),
124376
124255
  removeBroker: async ({ id }) => this.removeBroker(id),
124377
- testConnection: async ({ id }) => this.testConnection(id),
124378
124256
  startEmbeddedBroker: async (input) => this.startEmbeddedBroker(input),
124379
124257
  stopEmbeddedBroker: async ({ id }) => this.stopEmbeddedBroker(id),
124380
124258
  getStatus: async () => ({
@@ -124517,24 +124395,10 @@ var MqttBrokerAddon = class extends BaseAddon {
124517
124395
  return;
124518
124396
  }
124519
124397
  }
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
124398
  /**
124534
124399
  * 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).
124400
+ * flat config (`{url, username?, password?}`), used by the instance test
124401
+ * (`integration-instances.testInstance`), stored or draft.
124538
124402
  * Never throws — every failure resolves to `{ ok: false, error }`.
124539
124403
  */
124540
124404
  async probeMqtt(cfg) {