@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.
@@ -6028,7 +6028,7 @@ var ZodIssueCode = {
6028
6028
  var ZodFirstPartyTypeKind;
6029
6029
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6030
6030
  //#endregion
6031
- //#region ../types/dist/sleep-CmjWurK5.mjs
6031
+ //#region ../types/dist/sleep-D7ZKm4nb.mjs
6032
6032
  /**
6033
6033
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6034
6034
  * window to float samples (D455).
@@ -6737,10 +6737,33 @@ function integrationRowsApiOver(api) {
6737
6737
  };
6738
6738
  }
6739
6739
  var PREFIX = "records/";
6740
+ /**
6741
+ * Cross-realm brand. An addon bundles its own copy of this module (D15) while
6742
+ * `ctx.records` is the HOST's door, so an error thrown by the host is an
6743
+ * instance of the host's classes, not of the addon's. The brand plus the code
6744
+ * make `instanceof` answer by identity of the failure, not of the copy.
6745
+ */
6746
+ var RECORDS_ERROR_BRAND = Symbol.for("camstack.records-error");
6747
+ function carriesBrand(value) {
6748
+ return typeof value === "object" && value !== null && Reflect.get(value, RECORDS_ERROR_BRAND) === true;
6749
+ }
6740
6750
  /** The base of every records failure. `detail` is the human part of the message. */
6741
6751
  var RecordsError = class extends Error {
6752
+ /** The code a subclass stands for; the base stands for every code. */
6753
+ static code = void 0;
6754
+ /**
6755
+ * `instanceof` across copies of this module: any branded error is a
6756
+ * `RecordsError`, and is an instance of a subclass only when its code is the
6757
+ * subclass's code. Same-copy answers are unchanged.
6758
+ */
6759
+ static [Symbol.hasInstance](value) {
6760
+ if (Function.prototype[Symbol.hasInstance].call(this, value)) return true;
6761
+ if (!carriesBrand(value)) return false;
6762
+ return this.code === void 0 || value.code === this.code;
6763
+ }
6742
6764
  code;
6743
6765
  detail;
6766
+ [RECORDS_ERROR_BRAND] = true;
6744
6767
  constructor(code, detail) {
6745
6768
  super(`${PREFIX}${code}: ${detail}`);
6746
6769
  this.name = "RecordsError";
@@ -6749,22 +6772,25 @@ var RecordsError = class extends Error {
6749
6772
  }
6750
6773
  };
6751
6774
  /** The keychain could not store or serve a value: explicit, never an empty string. */
6752
- var RecordSecretUnavailableError = class extends RecordsError {
6775
+ var RecordSecretUnavailableError = class RecordSecretUnavailableError extends RecordsError {
6776
+ static code = "secret-unavailable";
6753
6777
  constructor(detail) {
6754
- super("secret-unavailable", detail);
6778
+ super(RecordSecretUnavailableError.code, detail);
6755
6779
  this.name = "RecordSecretUnavailableError";
6756
6780
  }
6757
6781
  };
6758
- var InvalidRecordError = class extends RecordsError {
6782
+ var InvalidRecordError = class InvalidRecordError extends RecordsError {
6783
+ static code = "invalid-record";
6759
6784
  constructor(detail) {
6760
- super("invalid-record", detail);
6785
+ super(InvalidRecordError.code, detail);
6761
6786
  this.name = "InvalidRecordError";
6762
6787
  }
6763
6788
  };
6764
6789
  /** A kind declaration the engine refuses (non-additive change, bad shape, wrong file). */
6765
- var DeclarationRefusedError = class extends RecordsError {
6790
+ var DeclarationRefusedError = class DeclarationRefusedError extends RecordsError {
6791
+ static code = "declaration-refused";
6766
6792
  constructor(detail) {
6767
- super("declaration-refused", detail);
6793
+ super(DeclarationRefusedError.code, detail);
6768
6794
  this.name = "DeclarationRefusedError";
6769
6795
  }
6770
6796
  };
@@ -9559,286 +9585,6 @@ function normalizeAddonInitResult(result) {
9559
9585
  if (Array.isArray(result)) return { providers: result };
9560
9586
  return result;
9561
9587
  }
9562
- /**
9563
- * The share-view scope kinds a share-link token can carry. The hub mints
9564
- * and validates them (`ShareViewGrantSchema` builds its
9565
- * enum from this list); a data plane names the kinds it ACCEPTS in `share`.
9566
- */
9567
- var SHARE_VIEW_KINDS = ["grid-view", "events-view"];
9568
- /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
9569
- var PeerBytesTicketSchema = object({
9570
- /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
9571
- url: string().min(1),
9572
- /**
9573
- * The HOST node this URL means something on — the hub or a named agent,
9574
- * never a runner. {@link AddonPeerBytes.open} compares it to its own and
9575
- * refuses `cross-node` by name when they differ, without dialling.
9576
- */
9577
- hostNodeId: string().min(1),
9578
- expiresAtMs: number().int().nonnegative(),
9579
- /**
9580
- * What the producer DECLARED the body to be, when it knows — `null` when it
9581
- * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
9582
- * must be able to tell "the producer did not say" from "the body is empty".
9583
- */
9584
- declaredBytes: number().int().nonnegative().nullable()
9585
- });
9586
- function defOf(schema) {
9587
- const zodBag = Reflect.get(schema, "_zod");
9588
- if (typeof zodBag !== "object" || zodBag === null) return {};
9589
- const def = Reflect.get(zodBag, "def");
9590
- if (typeof def !== "object" || def === null) return {};
9591
- return def;
9592
- }
9593
- function declaredHere(schema) {
9594
- return globalRegistry.get(schema)?.secret === true;
9595
- }
9596
- /** Wrapper kinds whose `innerType` is the field itself. */
9597
- var WRAPPERS = new Set([
9598
- "optional",
9599
- "nullable",
9600
- "default",
9601
- "prefault",
9602
- "catch",
9603
- "readonly",
9604
- "nonoptional"
9605
- ]);
9606
- /**
9607
- * The schema layers that make up ONE field: the schema itself and every
9608
- * wrapper below it (optional / default / nullable / readonly / catch), plus
9609
- * both sides of a pipe (`z.preprocess` puts the real schema on `out`,
9610
- * `.transform` on `in`).
9611
- */
9612
- function layersOf(schema) {
9613
- const out = [];
9614
- const queue = [schema];
9615
- while (queue.length > 0) {
9616
- const s = queue.shift();
9617
- if (!s || out.includes(s)) continue;
9618
- out.push(s);
9619
- const def = defOf(s);
9620
- if (def.type !== void 0 && WRAPPERS.has(def.type) && def.innerType) queue.push(def.innerType);
9621
- if (def.type === "pipe") {
9622
- if (def.in) queue.push(def.in);
9623
- if (def.out) queue.push(def.out);
9624
- }
9625
- }
9626
- return out;
9627
- }
9628
- /** True when any layer of this field declares `.meta({ secret: true })`. */
9629
- function isZodSecret(schema) {
9630
- return layersOf(schema).some(declaredHere);
9631
- }
9632
- /**
9633
- * Every declared secret path in a config schema: dotted keys, `[]` for "each
9634
- * element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
9635
- * map" (`applianceSecrets{}`). A declared
9636
- * object or array is reported as one path and not descended into — everything
9637
- * under it is secret.
9638
- */
9639
- function collectZodSecretPaths(schema, prefix = "") {
9640
- if (isZodSecret(schema)) return prefix.length > 0 ? [prefix] : [];
9641
- const paths = [];
9642
- for (const layer of layersOf(schema)) {
9643
- const def = defOf(layer);
9644
- if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
9645
- else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
9646
- else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
9647
- else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
9648
- else if (def.type === "intersection") {
9649
- if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
9650
- if (def.right) paths.push(...collectZodSecretPaths(def.right, prefix));
9651
- }
9652
- }
9653
- return [...new Set(paths)];
9654
- }
9655
- /**
9656
- * Record kinds (D881): the declaration an addon makes of one kind of fact, the
9657
- * Zod row schema that describes it, and the helper that turns the two into the
9658
- * JSON the engine stores.
9659
- *
9660
- * A kind is declared ONCE, next to the code that owns it, and the engine's
9661
- * reconciler creates its indexes - nothing creates DDL at runtime from a write.
9662
- * Everything the generic machinery needs (secret fields, the entry descriptor)
9663
- * is DERIVED from the Zod schema: `.meta({ secret: true })` marks a credential,
9664
- * never a field name (D879).
9665
- */
9666
- /** A JSON field path: `trackId`, `auth.accessToken`. Charset-restricted: it is rendered into DDL. */
9667
- var RecordFieldPathSchema = string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,63}(\.[A-Za-z_][A-Za-z0-9_]{0,63}){0,3}$/);
9668
- var RecordIndexSchema = object({
9669
- name: string().regex(/^[a-z][a-z0-9_]{0,31}$/),
9670
- fields: array(RecordFieldPathSchema).min(1).max(4),
9671
- unique: boolean().optional()
9672
- }).strict();
9673
- var RecordRetentionSchema = union([
9674
- object({
9675
- ttlMs: number().int().positive(),
9676
- from: _enum(["at", "write"])
9677
- }).strict(),
9678
- object({
9679
- maxRows: number().int().positive().optional(),
9680
- maxRowsPerDevice: number().int().positive().optional()
9681
- }).strict().refine((r) => r.maxRows !== void 0 || r.maxRowsPerDevice !== void 0, { message: "name maxRows, maxRowsPerDevice or both" }),
9682
- object({
9683
- owner: string().min(1),
9684
- reason: string().min(1)
9685
- }).strict(),
9686
- object({ none: string().min(1) }).strict()
9687
- ]);
9688
- /** The descriptor a generic entries UI reads (D881 "generic entries"). */
9689
- var EntryDescriptorSchema = object({
9690
- title: string().min(1),
9691
- /** The record body field shown as the entry's name. */
9692
- nameField: string().min(1),
9693
- /** JSON Schema of the entry body; `secret: true` marks a password field. */
9694
- fields: record(string(), unknown())
9695
- }).strict();
9696
- /** What crosses the wire from an addon: JSON only, no Zod. An unknown key is refused. */
9697
- var RecordKindDeclarationSchema = object({
9698
- database: _enum(RECORD_DATABASES).default("settings"),
9699
- class: _enum(RECORD_CLASSES),
9700
- writeTrigger: string().min(1),
9701
- device: _enum([
9702
- "required",
9703
- "optional",
9704
- "none"
9705
- ]).default("none"),
9706
- at: _enum(["required", "none"]).default("none"),
9707
- indexes: array(RecordIndexSchema).max(8).default([]),
9708
- retention: RecordRetentionSchema,
9709
- deviceRemoval: _enum(["delete", "none"]).default("none"),
9710
- maxDataBytes: number().int().positive().max(RECORD_MAX_DATA_BYTES).default(RECORD_DEFAULT_MAX_DATA_BYTES),
9711
- /** Derived from the Zod schema; never written by hand. */
9712
- secretPaths: array(string().min(1)).default([]),
9713
- /** The id key of each array that carries a secret (`{ headers: 'name' }`). */
9714
- secretArrayKeys: record(string(), string()).default({}),
9715
- /** Kinds that must agree through `transact` sit in one file under one family name. */
9716
- transactFamily: string().min(1).optional(),
9717
- entry: EntryDescriptorSchema.optional()
9718
- }).strict();
9719
- /** The kind names: lower-case, dash separated, short enough for an index name. */
9720
- var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
9721
- /** Cross-field rules a declaration must satisfy. Throws {@link DeclarationRefusedError}. */
9722
- function assertValidDeclaration(name, decl) {
9723
- const refuse = (why) => {
9724
- throw new DeclarationRefusedError(`kind "${name}": ${why}`);
9725
- };
9726
- if (!RecordKindNameSchema.safeParse(name).success) refuse("the name must be lower-case-dashed");
9727
- const seen = /* @__PURE__ */ new Set();
9728
- for (const index of decl.indexes) {
9729
- if (seen.has(index.name)) refuse(`index name "${index.name}" is declared twice`);
9730
- seen.add(index.name);
9731
- }
9732
- const { retention } = decl;
9733
- const hasSecrets = decl.secretPaths.length > 0;
9734
- if ("ttlMs" in retention) {
9735
- if (retention.from === "at" && decl.at !== "required") refuse("a ttl from \"at\" needs at: \"required\"");
9736
- if (hasSecrets) refuse("a kind with secret fields cannot use engine TTL (its keychain values would be orphaned)");
9737
- }
9738
- if ("maxRows" in retention || "maxRowsPerDevice" in retention) {
9739
- if (retention.maxRowsPerDevice !== void 0 && decl.device !== "required") refuse("maxRowsPerDevice needs device: \"required\"");
9740
- if (decl.at !== "required") refuse("a row bound trims the oldest \"at\": it needs at: \"required\"");
9741
- if (hasSecrets) refuse("a kind with secret fields cannot use an engine row bound");
9742
- const bound = Math.max(retention.maxRows ?? 0, retention.maxRowsPerDevice ?? 0);
9743
- 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)`);
9744
- }
9745
- if (decl.deviceRemoval === "delete" && decl.device === "none") refuse("deviceRemoval: \"delete\" needs a device: \"required\" or \"optional\"");
9746
- 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`);
9747
- }
9748
- function bodyFieldsOf(schema) {
9749
- return { ...toJSONSchema(schema, {
9750
- unrepresentable: "any",
9751
- io: "input"
9752
- }) };
9753
- }
9754
- function isRecordLike(value) {
9755
- return typeof value === "object" && value !== null && !Array.isArray(value);
9756
- }
9757
- /** Does every branch of the body schema carry `field`? (A discriminated union has several.) */
9758
- function hasNameField(json, field) {
9759
- const branches = json["oneOf"] ?? json["anyOf"];
9760
- if (Array.isArray(branches)) return branches.length > 0 && branches.every((b) => isRecordLike(b) && hasNameField(b, field));
9761
- const properties = json["properties"];
9762
- return isRecordLike(properties) && Object.prototype.hasOwnProperty.call(properties, field);
9763
- }
9764
- /**
9765
- * Declare the kinds of a package. The keys are the kind names. The result is
9766
- * what `ctx.records.declare` sends to the engine and what `ctx.records.kind`
9767
- * types its handle from.
9768
- */
9769
- function defineRecordKinds(specs) {
9770
- const out = {};
9771
- for (const [name, spec] of Object.entries(specs)) {
9772
- const secretPaths = collectZodSecretPaths(spec.schema);
9773
- const nameField = spec.entry?.nameField ?? "name";
9774
- const fields = bodyFieldsOf(spec.schema);
9775
- if (spec.entry !== void 0 && !hasNameField(fields, nameField)) throw new DeclarationRefusedError(`kind "${name}": an entry kind's schema needs a "${nameField}" field`);
9776
- const parsed = RecordKindDeclarationSchema.parse({
9777
- database: spec.database,
9778
- class: spec.class,
9779
- writeTrigger: spec.writeTrigger,
9780
- device: spec.device,
9781
- at: spec.at,
9782
- indexes: spec.indexes === void 0 ? void 0 : [...spec.indexes],
9783
- retention: spec.retention,
9784
- deviceRemoval: spec.deviceRemoval,
9785
- maxDataBytes: spec.maxDataBytes,
9786
- secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
9787
- secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
9788
- transactFamily: spec.transactFamily,
9789
- entry: spec.entry === void 0 ? void 0 : {
9790
- title: spec.entry.title,
9791
- nameField,
9792
- fields
9793
- }
9794
- });
9795
- assertValidDeclaration(name, parsed);
9796
- out[name] = {
9797
- name,
9798
- schema: spec.schema,
9799
- declaration: parsed
9800
- };
9801
- }
9802
- return out;
9803
- }
9804
- /**
9805
- * Entry kinds (D881): every list of user-created entries (notification
9806
- * targets, brokers, LLM profiles) is ONE record kind with an id, a name and a
9807
- * kind-specific body, and the admin UI lists/adds/edits/deletes all of them
9808
- * through the `record-entries` capability with password fields derived from
9809
- * the schema (`.meta({ secret: true })`).
9810
- */
9811
- /** The body of a plain entry: a name and the fields the kind adds. */
9812
- function entryBody(shape) {
9813
- return object({
9814
- name: string().min(1),
9815
- enabled: boolean().optional(),
9816
- ...shape
9817
- });
9818
- }
9819
- /**
9820
- * Declare one entry kind. An entry is configuration: never trimmed by the
9821
- * engine, never aged, belonging to no device.
9822
- */
9823
- function defineEntryKind(name, spec) {
9824
- const defined = defineRecordKinds({ [name]: {
9825
- schema: spec.schema,
9826
- class: "config",
9827
- writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
9828
- retention: { none: "an entry goes only when the operator deletes it" },
9829
- ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
9830
- ...spec.database === void 0 ? {} : { database: spec.database },
9831
- ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9832
- ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
9833
- ...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
9834
- entry: {
9835
- title: spec.title,
9836
- ...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
9837
- }
9838
- } })[name];
9839
- if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
9840
- return defined;
9841
- }
9842
9588
  object({
9843
9589
  field: string().min(1),
9844
9590
  on: literal("advance")
@@ -9927,6 +9673,286 @@ function optionalMethod(input, output, options) {
9927
9673
  providerOptional: true
9928
9674
  };
9929
9675
  }
9676
+ function defOf(schema) {
9677
+ const zodBag = Reflect.get(schema, "_zod");
9678
+ if (typeof zodBag !== "object" || zodBag === null) return {};
9679
+ const def = Reflect.get(zodBag, "def");
9680
+ if (typeof def !== "object" || def === null) return {};
9681
+ return def;
9682
+ }
9683
+ function declaredHere(schema) {
9684
+ return globalRegistry.get(schema)?.secret === true;
9685
+ }
9686
+ /** Wrapper kinds whose `innerType` is the field itself. */
9687
+ var WRAPPERS = new Set([
9688
+ "optional",
9689
+ "nullable",
9690
+ "default",
9691
+ "prefault",
9692
+ "catch",
9693
+ "readonly",
9694
+ "nonoptional"
9695
+ ]);
9696
+ /**
9697
+ * The schema layers that make up ONE field: the schema itself and every
9698
+ * wrapper below it (optional / default / nullable / readonly / catch), plus
9699
+ * both sides of a pipe (`z.preprocess` puts the real schema on `out`,
9700
+ * `.transform` on `in`).
9701
+ */
9702
+ function layersOf(schema) {
9703
+ const out = [];
9704
+ const queue = [schema];
9705
+ while (queue.length > 0) {
9706
+ const s = queue.shift();
9707
+ if (!s || out.includes(s)) continue;
9708
+ out.push(s);
9709
+ const def = defOf(s);
9710
+ if (def.type !== void 0 && WRAPPERS.has(def.type) && def.innerType) queue.push(def.innerType);
9711
+ if (def.type === "pipe") {
9712
+ if (def.in) queue.push(def.in);
9713
+ if (def.out) queue.push(def.out);
9714
+ }
9715
+ }
9716
+ return out;
9717
+ }
9718
+ /** True when any layer of this field declares `.meta({ secret: true })`. */
9719
+ function isZodSecret(schema) {
9720
+ return layersOf(schema).some(declaredHere);
9721
+ }
9722
+ /**
9723
+ * Every declared secret path in a config schema: dotted keys, `[]` for "each
9724
+ * element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
9725
+ * map" (`applianceSecrets{}`). A declared
9726
+ * object or array is reported as one path and not descended into — everything
9727
+ * under it is secret.
9728
+ */
9729
+ function collectZodSecretPaths(schema, prefix = "") {
9730
+ if (isZodSecret(schema)) return prefix.length > 0 ? [prefix] : [];
9731
+ const paths = [];
9732
+ for (const layer of layersOf(schema)) {
9733
+ const def = defOf(layer);
9734
+ if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
9735
+ else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
9736
+ else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
9737
+ else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
9738
+ else if (def.type === "intersection") {
9739
+ if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
9740
+ if (def.right) paths.push(...collectZodSecretPaths(def.right, prefix));
9741
+ }
9742
+ }
9743
+ return [...new Set(paths)];
9744
+ }
9745
+ /**
9746
+ * Record kinds (D881): the declaration an addon makes of one kind of fact, the
9747
+ * Zod row schema that describes it, and the helper that turns the two into the
9748
+ * JSON the engine stores.
9749
+ *
9750
+ * A kind is declared ONCE, next to the code that owns it, and the engine's
9751
+ * reconciler creates its indexes - nothing creates DDL at runtime from a write.
9752
+ * Everything the generic machinery needs (secret fields, the entry descriptor)
9753
+ * is DERIVED from the Zod schema: `.meta({ secret: true })` marks a credential,
9754
+ * never a field name (D879).
9755
+ */
9756
+ /** A JSON field path: `trackId`, `auth.accessToken`. Charset-restricted: it is rendered into DDL. */
9757
+ var RecordFieldPathSchema = string().regex(/^[A-Za-z_][A-Za-z0-9_]{0,63}(\.[A-Za-z_][A-Za-z0-9_]{0,63}){0,3}$/);
9758
+ var RecordIndexSchema = object({
9759
+ name: string().regex(/^[a-z][a-z0-9_]{0,31}$/),
9760
+ fields: array(RecordFieldPathSchema).min(1).max(4),
9761
+ unique: boolean().optional()
9762
+ }).strict();
9763
+ var RecordRetentionSchema = union([
9764
+ object({
9765
+ ttlMs: number().int().positive(),
9766
+ from: _enum(["at", "write"])
9767
+ }).strict(),
9768
+ object({
9769
+ maxRows: number().int().positive().optional(),
9770
+ maxRowsPerDevice: number().int().positive().optional()
9771
+ }).strict().refine((r) => r.maxRows !== void 0 || r.maxRowsPerDevice !== void 0, { message: "name maxRows, maxRowsPerDevice or both" }),
9772
+ object({
9773
+ owner: string().min(1),
9774
+ reason: string().min(1)
9775
+ }).strict(),
9776
+ object({ none: string().min(1) }).strict()
9777
+ ]);
9778
+ /** The descriptor a generic entries UI reads (D881 "generic entries"). */
9779
+ var EntryDescriptorSchema = object({
9780
+ title: string().min(1),
9781
+ /** The record body field shown as the entry's name. */
9782
+ nameField: string().min(1),
9783
+ /** JSON Schema of the entry body; `secret: true` marks a password field. */
9784
+ fields: record(string(), unknown())
9785
+ }).strict();
9786
+ /** What crosses the wire from an addon: JSON only, no Zod. An unknown key is refused. */
9787
+ var RecordKindDeclarationSchema = object({
9788
+ database: _enum(RECORD_DATABASES).default("settings"),
9789
+ class: _enum(RECORD_CLASSES),
9790
+ writeTrigger: string().min(1),
9791
+ device: _enum([
9792
+ "required",
9793
+ "optional",
9794
+ "none"
9795
+ ]).default("none"),
9796
+ at: _enum(["required", "none"]).default("none"),
9797
+ indexes: array(RecordIndexSchema).max(8).default([]),
9798
+ retention: RecordRetentionSchema,
9799
+ deviceRemoval: _enum(["delete", "none"]).default("none"),
9800
+ maxDataBytes: number().int().positive().max(RECORD_MAX_DATA_BYTES).default(RECORD_DEFAULT_MAX_DATA_BYTES),
9801
+ /** Derived from the Zod schema; never written by hand. */
9802
+ secretPaths: array(string().min(1)).default([]),
9803
+ /** The id key of each array that carries a secret (`{ headers: 'name' }`). */
9804
+ secretArrayKeys: record(string(), string()).default({}),
9805
+ /** Kinds that must agree through `transact` sit in one file under one family name. */
9806
+ transactFamily: string().min(1).optional(),
9807
+ entry: EntryDescriptorSchema.optional()
9808
+ }).strict();
9809
+ /** The kind names: lower-case, dash separated, short enough for an index name. */
9810
+ var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
9811
+ /** Cross-field rules a declaration must satisfy. Throws {@link DeclarationRefusedError}. */
9812
+ function assertValidDeclaration(name, decl) {
9813
+ const refuse = (why) => {
9814
+ throw new DeclarationRefusedError(`kind "${name}": ${why}`);
9815
+ };
9816
+ if (!RecordKindNameSchema.safeParse(name).success) refuse("the name must be lower-case-dashed");
9817
+ const seen = /* @__PURE__ */ new Set();
9818
+ for (const index of decl.indexes) {
9819
+ if (seen.has(index.name)) refuse(`index name "${index.name}" is declared twice`);
9820
+ seen.add(index.name);
9821
+ }
9822
+ const { retention } = decl;
9823
+ const hasSecrets = decl.secretPaths.length > 0;
9824
+ if ("ttlMs" in retention) {
9825
+ if (retention.from === "at" && decl.at !== "required") refuse("a ttl from \"at\" needs at: \"required\"");
9826
+ if (hasSecrets) refuse("a kind with secret fields cannot use engine TTL (its keychain values would be orphaned)");
9827
+ }
9828
+ if ("maxRows" in retention || "maxRowsPerDevice" in retention) {
9829
+ if (retention.maxRowsPerDevice !== void 0 && decl.device !== "required") refuse("maxRowsPerDevice needs device: \"required\"");
9830
+ if (decl.at !== "required") refuse("a row bound trims the oldest \"at\": it needs at: \"required\"");
9831
+ if (hasSecrets) refuse("a kind with secret fields cannot use an engine row bound");
9832
+ const bound = Math.max(retention.maxRows ?? 0, retention.maxRowsPerDevice ?? 0);
9833
+ 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)`);
9834
+ }
9835
+ if (decl.deviceRemoval === "delete" && decl.device === "none") refuse("deviceRemoval: \"delete\" needs a device: \"required\" or \"optional\"");
9836
+ 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`);
9837
+ }
9838
+ function bodyFieldsOf(schema) {
9839
+ return { ...toJSONSchema(schema, {
9840
+ unrepresentable: "any",
9841
+ io: "input"
9842
+ }) };
9843
+ }
9844
+ function isRecordLike(value) {
9845
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9846
+ }
9847
+ /** Does every branch of the body schema carry `field`? (A discriminated union has several.) */
9848
+ function hasNameField(json, field) {
9849
+ const branches = json["oneOf"] ?? json["anyOf"];
9850
+ if (Array.isArray(branches)) return branches.length > 0 && branches.every((b) => isRecordLike(b) && hasNameField(b, field));
9851
+ const properties = json["properties"];
9852
+ return isRecordLike(properties) && Object.prototype.hasOwnProperty.call(properties, field);
9853
+ }
9854
+ /**
9855
+ * Declare the kinds of a package. The keys are the kind names. The result is
9856
+ * what `ctx.records.declare` sends to the engine and what `ctx.records.kind`
9857
+ * types its handle from.
9858
+ */
9859
+ function defineRecordKinds(specs) {
9860
+ const out = {};
9861
+ for (const [name, spec] of Object.entries(specs)) {
9862
+ const secretPaths = collectZodSecretPaths(spec.schema);
9863
+ const nameField = spec.entry?.nameField ?? "name";
9864
+ const fields = bodyFieldsOf(spec.schema);
9865
+ if (spec.entry !== void 0 && !hasNameField(fields, nameField)) throw new DeclarationRefusedError(`kind "${name}": an entry kind's schema needs a "${nameField}" field`);
9866
+ const parsed = RecordKindDeclarationSchema.parse({
9867
+ database: spec.database,
9868
+ class: spec.class,
9869
+ writeTrigger: spec.writeTrigger,
9870
+ device: spec.device,
9871
+ at: spec.at,
9872
+ indexes: spec.indexes === void 0 ? void 0 : [...spec.indexes],
9873
+ retention: spec.retention,
9874
+ deviceRemoval: spec.deviceRemoval,
9875
+ maxDataBytes: spec.maxDataBytes,
9876
+ secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
9877
+ secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
9878
+ transactFamily: spec.transactFamily,
9879
+ entry: spec.entry === void 0 ? void 0 : {
9880
+ title: spec.entry.title,
9881
+ nameField,
9882
+ fields
9883
+ }
9884
+ });
9885
+ assertValidDeclaration(name, parsed);
9886
+ out[name] = {
9887
+ name,
9888
+ schema: spec.schema,
9889
+ declaration: parsed
9890
+ };
9891
+ }
9892
+ return out;
9893
+ }
9894
+ /**
9895
+ * Entry kinds (D881): every list of user-created entries (notification
9896
+ * targets, brokers, LLM profiles) is ONE record kind with an id, a name and a
9897
+ * kind-specific body, and the admin UI lists/adds/edits/deletes all of them
9898
+ * through the `record-entries` capability with password fields derived from
9899
+ * the schema (`.meta({ secret: true })`).
9900
+ */
9901
+ /** The body of a plain entry: a name and the fields the kind adds. */
9902
+ function entryBody(shape) {
9903
+ return object({
9904
+ name: string().min(1),
9905
+ enabled: boolean().optional(),
9906
+ ...shape
9907
+ });
9908
+ }
9909
+ /**
9910
+ * Declare one entry kind. An entry is configuration: never trimmed by the
9911
+ * engine, never aged, belonging to no device.
9912
+ */
9913
+ function defineEntryKind(name, spec) {
9914
+ const defined = defineRecordKinds({ [name]: {
9915
+ schema: spec.schema,
9916
+ class: "config",
9917
+ writeTrigger: spec.writeTrigger ?? "an operator adds or edits an entry in the entries UI",
9918
+ retention: { none: "an entry goes only when the operator deletes it" },
9919
+ ...spec.maxDataBytes === void 0 ? {} : { maxDataBytes: spec.maxDataBytes },
9920
+ ...spec.database === void 0 ? {} : { database: spec.database },
9921
+ ...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
9922
+ ...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
9923
+ ...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
9924
+ entry: {
9925
+ title: spec.title,
9926
+ ...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
9927
+ }
9928
+ } })[name];
9929
+ if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
9930
+ return defined;
9931
+ }
9932
+ /**
9933
+ * The share-view scope kinds a share-link token can carry. The hub mints
9934
+ * and validates them (`ShareViewGrantSchema` builds its
9935
+ * enum from this list); a data plane names the kinds it ACCEPTS in `share`.
9936
+ */
9937
+ var SHARE_VIEW_KINDS = ["grid-view", "events-view"];
9938
+ /** The wire shape of {@link PeerBytesTicket} — see the type for what it is. */
9939
+ var PeerBytesTicketSchema = object({
9940
+ /** `http://127.0.0.1:<port>/<token>`. One `GET` takes it. */
9941
+ url: string().min(1),
9942
+ /**
9943
+ * The HOST node this URL means something on — the hub or a named agent,
9944
+ * never a runner. {@link AddonPeerBytes.open} compares it to its own and
9945
+ * refuses `cross-node` by name when they differ, without dialling.
9946
+ */
9947
+ hostNodeId: string().min(1),
9948
+ expiresAtMs: number().int().nonnegative(),
9949
+ /**
9950
+ * What the producer DECLARED the body to be, when it knows — `null` when it
9951
+ * does not. Never `0` for unknown (D393): a consumer sizing a bound off this
9952
+ * must be able to tell "the producer did not say" from "the body is empty".
9953
+ */
9954
+ declaredBytes: number().int().nonnegative().nullable()
9955
+ });
9930
9956
  var READING_QUANTITIES = [
9931
9957
  "temperature",
9932
9958
  "pressure",
@@ -10505,7 +10531,6 @@ var CAP_NAMES = Object.freeze([
10505
10531
  "climate-control",
10506
10532
  "color",
10507
10533
  "composer",
10508
- "connection-test",
10509
10534
  "connectivity",
10510
10535
  "consumables",
10511
10536
  "contact",
@@ -12525,8 +12550,7 @@ Object.freeze({
12525
12550
  "offerArtifact": 3e5,
12526
12551
  "readResult": 3e5,
12527
12552
  "submitJob": 18e5,
12528
- "uploadDataset": 72e5,
12529
- "validateCredentials": 3e4
12553
+ "uploadDataset": 72e5
12530
12554
  }
12531
12555
  });
12532
12556
  object({
@@ -13881,6 +13905,432 @@ var IntegrationInstanceRefSchema = object({
13881
13905
  ]),
13882
13906
  candidateStableId: string().optional()
13883
13907
  });
13908
+ /**
13909
+ * notification-output — canonical, capability-gated notification delivery.
13910
+ *
13911
+ * Apprise-derived model (see
13912
+ * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
13913
+ * callers emit ONE canonical `Notification`; each provider declares a
13914
+ * per-kind capability descriptor (`TargetKind`), and the pure degrade
13915
+ * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
13916
+ * message to what the kind supports — callers never special-case a service.
13917
+ *
13918
+ * DESIGN DECISIONS (locked):
13919
+ * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
13920
+ * `setTargetEnabled`), each provider persisting via the `settings-store`
13921
+ * cap. Rationale: the admin UI needs one uniform surface across the
13922
+ * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
13923
+ * alternative would fork the UI per addon and cannot host the
13924
+ * discovery→adopt flow.
13925
+ * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
13926
+ * the generated cap-mount auto-`concatCollection`-fans them across every
13927
+ * registered provider (notifiers addon + HA addon) so one catalog is
13928
+ * routable. `send` / `testTarget` / CRUD route to ONE provider by the
13929
+ * `addonId` the generated collection router extracts from the call input.
13930
+ * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
13931
+ * transformer) + UDS MsgPack both round-trip typed arrays — already used by
13932
+ * `storage` / `storage-provider` / `recording` caps over the same path. No
13933
+ * base64 fallback needed.
13934
+ *
13935
+ * TODO (deferred, closed-set change — separate decision): add
13936
+ * `providerKind: 'notify'` so notification providers surface on the unified
13937
+ * admin "Integrations" page.
13938
+ */
13939
+ /**
13940
+ * Zentik-derived typed-media enum — the superset across every kind. Each
13941
+ * adapter picks what it supports and the degrade engine filters the rest.
13942
+ */
13943
+ var AttachmentMediaTypeSchema = _enum([
13944
+ "image",
13945
+ "video",
13946
+ "gif",
13947
+ "audio",
13948
+ "icon"
13949
+ ]);
13950
+ /**
13951
+ * A single attachment. Exactly one of `url` (remote source, most adapters
13952
+ * prefer this) or `bytes` (inline source; required for Pushover-style
13953
+ * bytes-only kinds) MUST be present — the degrade engine expresses a
13954
+ * url→bytes fetch as a `needsFetch` directive the adapter executes.
13955
+ */
13956
+ var AttachmentSchema = object({
13957
+ mediaType: AttachmentMediaTypeSchema,
13958
+ url: string().optional(),
13959
+ bytes: _instanceof(Uint8Array).optional(),
13960
+ mime: string().optional(),
13961
+ name: string().optional()
13962
+ }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
13963
+ var NotificationFormatSchema = _enum([
13964
+ "text",
13965
+ "markdown",
13966
+ "html"
13967
+ ]);
13968
+ /**
13969
+ * The CLOSED icon vocabulary an action button may use.
13970
+ *
13971
+ * A closed set, not a free string, and that is the whole point: an arbitrary
13972
+ * icon name is one that ntfy renders, zentik silently drops, and nobody
13973
+ * notices — the same class of gap as a zone vocabulary nothing produced
13974
+ * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
13975
+ * declares `actionIcons: false` and the degrade engine strips the field.
13976
+ *
13977
+ * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
13978
+ * renderer's icon set; "acknowledge" survives an adapter that draws it
13979
+ * differently.
13980
+ *
13981
+ * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
13982
+ *
13983
+ * These names are for US. **No adapter may forward one verbatim.** Each maps
13984
+ * the whole set onto its own renderer's vocabulary through a
13985
+ * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
13986
+ * fallback, so adding a member here fails every adapter's build until someone
13987
+ * decides its glyph, which is the only place that decision can be made
13988
+ * honestly.
13989
+ *
13990
+ * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
13991
+ * `disarm` straight through; iOS feeds that string to
13992
+ * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
13993
+ * and every snooze and alarm button arrived BLANK. A pass-through is not a
13994
+ * mapping, and "the field is documented" is not "the value renders".
13995
+ *
13996
+ * Adding a member is TRAIN-BOUND. The enum lives in the published
13997
+ * `@camstack/server` closure and the cap seam validates against the HUB's copy,
13998
+ * so an addon that emits a token the running hub does not know does not lose an
13999
+ * icon — its whole `send` fails Zod validation and the notification never
14000
+ * arrives. Never emit a new token from an addon before the train carrying it.
14001
+ */
14002
+ var NotificationActionIconSchema = _enum([
14003
+ "acknowledge",
14004
+ "dismiss",
14005
+ "silence",
14006
+ "snooze",
14007
+ "view",
14008
+ "play",
14009
+ "open",
14010
+ "close",
14011
+ "lock",
14012
+ "unlock",
14013
+ "arm",
14014
+ "arm-home",
14015
+ "arm-away",
14016
+ "arm-night",
14017
+ "disarm",
14018
+ "light",
14019
+ "alert",
14020
+ "camera"
14021
+ ]);
14022
+ /** A single tap-through action button. */
14023
+ var NotificationActionSchema = object({
14024
+ id: string(),
14025
+ label: string(),
14026
+ url: string().optional(),
14027
+ /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
14028
+ icon: NotificationActionIconSchema.optional(),
14029
+ /**
14030
+ * Renders in a warning style where the notifier supports it.
14031
+ *
14032
+ * A HINT, never a gate. The callback's authority is its token and nothing
14033
+ * else — see `notification-center/action-token.ts` for what that does and
14034
+ * does not buy.
14035
+ */
14036
+ destructive: boolean().optional(),
14037
+ /**
14038
+ * How the tap should REACH the url.
14039
+ *
14040
+ * `navigate` (absent, and every button authored before this field) opens it:
14041
+ * the phone leaves the notification and shows whatever the callback returns.
14042
+ * That is right for a button whose answer the operator wants to read.
14043
+ *
14044
+ * `background` fires it as a POST and stays put. It exists for the buttons
14045
+ * whose whole point is not to interrupt — "silence this for 30 minutes" is
14046
+ * an answer to the notification, and being thrown into a browser tab to
14047
+ * confirm it costs more attention than the notification did. A backend that
14048
+ * cannot do a background call renders it as an ordinary link (the adapters
14049
+ * fall back rather than dropping the button), so this is a preference, never
14050
+ * a requirement.
14051
+ */
14052
+ mode: _enum(["navigate", "background"]).optional()
14053
+ });
14054
+ /** A delivery parameter value: what a notifier declared it takes. */
14055
+ var DeliveryParamValueSchema = union([
14056
+ string(),
14057
+ number(),
14058
+ boolean()
14059
+ ]);
14060
+ /**
14061
+ * One extra delivery parameter a notifier KIND declares (D938). A rule that
14062
+ * addresses a target of that kind may set it; a kind that declares nothing is
14063
+ * never handed one. The admin UI derives its control from this descriptor —
14064
+ * no editor knows a service by name.
14065
+ *
14066
+ * Optional fields are `.optional()`, never `.default()` (a Zod default does not
14067
+ * run on the addon cap path).
14068
+ */
14069
+ var DeliveryParamSchema = object({
14070
+ key: string().min(1),
14071
+ label: string(),
14072
+ /** i18n key of the label, resolved by the declaring addon's namespace. */
14073
+ labelKey: string().optional(),
14074
+ type: _enum([
14075
+ "enum",
14076
+ "number",
14077
+ "boolean",
14078
+ "string"
14079
+ ]),
14080
+ /** `enum` only: the closed set of accepted values. */
14081
+ options: array(object({
14082
+ value: string(),
14083
+ label: string()
14084
+ })).optional(),
14085
+ /** `number` only. */
14086
+ min: number().optional(),
14087
+ max: number().optional(),
14088
+ /** What the notifier does when the rule sets nothing; shown as the placeholder. */
14089
+ default: DeliveryParamValueSchema.optional()
14090
+ });
14091
+ /**
14092
+ * The canonical notification. `body` is the only hard field (Apprise model).
14093
+ * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
14094
+ * NOT a fixed severity enum — each kind declares its own `caps.levels` and
14095
+ * the adapter maps this ordinal onto its native level. `level?` is an
14096
+ * optional kind-native level id (`emergency`, `silent`, …) that overrides
14097
+ * `priority` for that one target.
14098
+ */
14099
+ var NotificationSchema = object({
14100
+ body: string(),
14101
+ title: string().optional(),
14102
+ format: NotificationFormatSchema.default("text"),
14103
+ priority: number().int().min(1).max(5).default(3),
14104
+ level: string().optional(),
14105
+ attachments: array(AttachmentSchema).optional(),
14106
+ clickUrl: string().optional(),
14107
+ actions: array(NotificationActionSchema).optional(),
14108
+ sound: string().optional(),
14109
+ ttl: number().optional(),
14110
+ tag: string().optional(),
14111
+ deviceId: number().optional(),
14112
+ eventId: string().optional(),
14113
+ metadata: record(string(), unknown()).optional(),
14114
+ /**
14115
+ * Values for the parameters the target's kind DECLARES in
14116
+ * {@link TargetKindSchema} `deliveryParams`. Already validated against the
14117
+ * declaration by the sender; a notifier reads only the keys it declared.
14118
+ */
14119
+ delivery: record(string(), DeliveryParamValueSchema).optional()
14120
+ });
14121
+ /** One declared native severity/priority level for a kind. */
14122
+ var TargetKindLevelSchema = object({
14123
+ id: string(),
14124
+ label: string(),
14125
+ /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
14126
+ ordinal: number().int().min(1).max(5).nullable(),
14127
+ flags: object({
14128
+ critical: boolean().optional(),
14129
+ silent: boolean().optional(),
14130
+ noPush: boolean().optional()
14131
+ }).optional(),
14132
+ /** e.g. Pushover `emergency` requires `retry` / `expire`. */
14133
+ requires: array(string()).optional(),
14134
+ description: string().optional()
14135
+ });
14136
+ /** The full capability block consulted before dispatch. */
14137
+ var TargetKindCapsSchema = object({
14138
+ attachments: object({
14139
+ mediaTypes: array(AttachmentMediaTypeSchema),
14140
+ mode: _enum([
14141
+ "url",
14142
+ "bytes",
14143
+ "both"
14144
+ ]),
14145
+ max: number().int().nonnegative(),
14146
+ maxBytes: number().int().positive().optional()
14147
+ }),
14148
+ /** Max action buttons (0 = none). */
14149
+ actions: number().int().nonnegative(),
14150
+ /**
14151
+ * Whether this kind renders a per-action ICON.
14152
+ *
14153
+ * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
14154
+ * run on the addon cap path — three production failures in one day taught
14155
+ * this repo that once. Absent is read as false by the degrade engine, which
14156
+ * is the safe direction: an icon that is not rendered costs nothing, an icon
14157
+ * assumed and dropped costs the operator's trust in the field.
14158
+ */
14159
+ actionIcons: boolean().optional(),
14160
+ levels: array(TargetKindLevelSchema),
14161
+ format: array(NotificationFormatSchema),
14162
+ clickUrl: boolean(),
14163
+ sound: boolean(),
14164
+ ttl: boolean(),
14165
+ bodyMaxLen: number().int().positive()
14166
+ });
14167
+ /**
14168
+ * `configSchema` is a `ConfigUISchema` tree passed through to the admin
14169
+ * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
14170
+ * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
14171
+ * the union is large and not meant for runtime validation here; the exported
14172
+ * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
14173
+ */
14174
+ var ConfigSchemaPassthrough$4 = unknown();
14175
+ var TargetKindSchema = object({
14176
+ kind: string(),
14177
+ label: string(),
14178
+ icon: string(),
14179
+ /** Stamped by each provider so the concat-fanned catalog stays routable. */
14180
+ addonId: string(),
14181
+ /**
14182
+ * URL of the kind's bundled BRAND icon, served by the providing addon over
14183
+ * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
14184
+ * when the addon bundles no icon for that kind — the client then falls back
14185
+ * to a neutral glyph rather than rendering the raw `icon` NAME as text.
14186
+ *
14187
+ * Root-relative on purpose: it resolves against whatever origin serves a web
14188
+ * client, and a native client joins it onto its own hub base.
14189
+ *
14190
+ * DECLARED here deliberately. It used to travel as an undeclared passthrough
14191
+ * field that survived only because the runtime cap-router forwards provider
14192
+ * output verbatim — so every consumer had to re-declare it by hand to stop
14193
+ * its own Zod parse from stripping it, and the whole arrangement would have
14194
+ * broken silently the moment output validation was tightened anywhere.
14195
+ */
14196
+ iconUrl: string().optional(),
14197
+ /**
14198
+ * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
14199
+ *
14200
+ * The server knows this and therefore says it, because the client cannot
14201
+ * safely guess: a React-Native client renders SVG and raster through two
14202
+ * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
14203
+ * not decode SVG on iOS/Android), so without this it silently fell back to a
14204
+ * placeholder glyph for every vector icon while the web build looked fine.
14205
+ *
14206
+ * Absent when {@link iconUrl} is absent, or for a legacy provider that has
14207
+ * not been updated — a client that cannot determine the type should prefer
14208
+ * its raster path, which is the safe default for an unknown image.
14209
+ */
14210
+ iconMediaType: string().optional(),
14211
+ configSchema: ConfigSchemaPassthrough$4,
14212
+ /**
14213
+ * The config keys that decide WHERE this kind's secrets are sent (server,
14214
+ * base URL, instance, chat, recipient). A save that changes any of them
14215
+ * while leaving a secret redacted is refused by the provider (D634), so an
14216
+ * editor uses this to ask for the secret on exactly those edits instead of
14217
+ * on every edit.
14218
+ *
14219
+ * Optional: absent from a provider that holds no secret (the Home Assistant
14220
+ * kinds) and from a provider older than this field — an editor that finds
14221
+ * it absent must assume ANY edit may need the secret.
14222
+ */
14223
+ destinationKeys: array(string()).optional(),
14224
+ supportsDiscovery: boolean(),
14225
+ /**
14226
+ * Extra delivery parameters a rule may set for a target of this kind (D938).
14227
+ * Absent = none; the rule editor then offers nothing.
14228
+ */
14229
+ deliveryParams: array(DeliveryParamSchema).optional(),
14230
+ caps: TargetKindCapsSchema
14231
+ });
14232
+ /**
14233
+ * A persisted target. `config` holds secrets; providers REDACT secret fields
14234
+ * (return a presence marker only) when serving `listTargets` — never
14235
+ * round-trip a stored secret to the UI.
14236
+ */
14237
+ var TargetSchema = object({
14238
+ id: string(),
14239
+ name: string(),
14240
+ kind: string(),
14241
+ addonId: string(),
14242
+ enabled: boolean(),
14243
+ config: record(string(), unknown())
14244
+ });
14245
+ /** A discovery-surfaced candidate (config is partial + non-secret). */
14246
+ var DiscoveredTargetSchema = object({
14247
+ kind: string(),
14248
+ suggestedName: string(),
14249
+ config: record(string(), unknown())
14250
+ });
14251
+ /** The degrade engine's report — what was resolved / dropped / degraded. */
14252
+ var RenderedAsSchema = object({
14253
+ level: string(),
14254
+ format: NotificationFormatSchema,
14255
+ attachmentsSent: number().int().nonnegative(),
14256
+ actionsSent: number().int().nonnegative(),
14257
+ truncated: boolean(),
14258
+ dropped: array(string())
14259
+ });
14260
+ var SendResultSchema = object({
14261
+ success: boolean(),
14262
+ error: string().optional(),
14263
+ renderedAs: RenderedAsSchema.optional()
14264
+ });
14265
+ /** Same shape as SendResult — kept as a distinct name for the test panel. */
14266
+ var TestResultSchema = SendResultSchema;
14267
+ /**
14268
+ * Pre-save credential probe against a partial (unsaved) target config.
14269
+ * `resolvedUrl` never includes secrets (token stripped from path).
14270
+ */
14271
+ var ProbeTargetConfigResultSchema = object({
14272
+ ok: boolean(),
14273
+ resolvedUrl: string(),
14274
+ httpStatus: number().int().optional(),
14275
+ stateInstance: string().optional(),
14276
+ error: string().optional()
14277
+ });
14278
+ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
14279
+ kind: string(),
14280
+ config: record(string(), unknown()).optional(),
14281
+ /**
14282
+ * When editing / duplicating, the blank password field never round-trips
14283
+ * the stored token. Pass the source target id so the provider can restore
14284
+ * secrets server-side before dialling (same merge as upsert).
14285
+ */
14286
+ targetId: string().optional()
14287
+ }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
14288
+ kind: string(),
14289
+ config: record(string(), unknown()),
14290
+ /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
14291
+ targetId: string().optional()
14292
+ }), ProbeTargetConfigResultSchema, {
14293
+ kind: "mutation",
14294
+ auth: "admin"
14295
+ }), method(object({
14296
+ targetId: string(),
14297
+ notification: NotificationSchema
14298
+ }), SendResultSchema, {
14299
+ kind: "mutation",
14300
+ auth: "admin"
14301
+ }), method(object({
14302
+ targetId: string(),
14303
+ sample: NotificationSchema.optional()
14304
+ }), TestResultSchema, {
14305
+ kind: "mutation",
14306
+ auth: "admin"
14307
+ }), method(object({
14308
+ target: TargetSchema,
14309
+ /**
14310
+ * Duplicate flow: create a NEW target id while copying secrets from an
14311
+ * existing row. The UI never holds the stored token; blank / redacted
14312
+ * password fields are restored from this source (D634 still applies if
14313
+ * destinations diverge without a re-typed secret).
14314
+ */
14315
+ cloneSecretsFrom: string().optional()
14316
+ }), TargetSchema, {
14317
+ kind: "mutation",
14318
+ auth: "admin"
14319
+ }), method(object({ targetId: string() }), _void(), {
14320
+ kind: "mutation",
14321
+ auth: "admin"
14322
+ }), method(object({
14323
+ targetId: string(),
14324
+ enabled: boolean()
14325
+ }), _void(), {
14326
+ kind: "mutation",
14327
+ auth: "admin"
14328
+ });
14329
+ looseObject({
14330
+ name: string().min(1),
14331
+ enabled: boolean().optional()
14332
+ });
14333
+ TargetSchema.omit({ id: true });
13884
14334
  var EncodeProfileSchema = object({
13885
14335
  video: object({
13886
14336
  codec: _enum([
@@ -22098,7 +22548,6 @@ var SYSTEM_CAP_VOCABULARY = {
22098
22548
  "calendar-source": system(INTERNAL),
22099
22549
  calendars: system("calendars feed schedules and conditions, not device actions"),
22100
22550
  composer: system("device composition — an authoring tool, not an action"),
22101
- "connection-test": system(INTERNAL),
22102
22551
  "core-blocks": system(INTERNAL),
22103
22552
  "custom-model-registry": system(ML),
22104
22553
  "data-store-provider": system(INTERNAL),
@@ -44015,427 +44464,6 @@ var UnitWarningSchema = object({
44015
44464
  assumedUnit: string(),
44016
44465
  message: string()
44017
44466
  });
44018
- /**
44019
- * notification-output — canonical, capability-gated notification delivery.
44020
- *
44021
- * Apprise-derived model (see
44022
- * `docs/superpowers/specs/2026-07-03-notification-output-notifier-matrix.md`):
44023
- * callers emit ONE canonical `Notification`; each provider declares a
44024
- * per-kind capability descriptor (`TargetKind`), and the pure degrade
44025
- * engine (`@camstack/types` `prepareNotification`) transcodes / degrades the
44026
- * message to what the kind supports — callers never special-case a service.
44027
- *
44028
- * DESIGN DECISIONS (locked):
44029
- * - Target CRUD lives on THIS cap (`upsertTarget` / `deleteTarget` /
44030
- * `setTargetEnabled`), each provider persisting via the `settings-store`
44031
- * cap. Rationale: the admin UI needs one uniform surface across the
44032
- * notifiers addon AND the HA addon; the addon-`globalSettingsSchema`-array
44033
- * alternative would fork the UI per addon and cannot host the
44034
- * discovery→adopt flow.
44035
- * - `listTargetKinds` / `listTargets` / `discoverTargets` return arrays →
44036
- * the generated cap-mount auto-`concatCollection`-fans them across every
44037
- * registered provider (notifiers addon + HA addon) so one catalog is
44038
- * routable. `send` / `testTarget` / CRUD route to ONE provider by the
44039
- * `addonId` the generated collection router extracts from the call input.
44040
- * - `Attachment.bytes` is `Uint8Array`. Transport-safe: superjson (the tRPC
44041
- * transformer) + UDS MsgPack both round-trip typed arrays — already used by
44042
- * `storage` / `storage-provider` / `recording` caps over the same path. No
44043
- * base64 fallback needed.
44044
- *
44045
- * TODO (deferred, closed-set change — separate decision): add
44046
- * `providerKind: 'notify'` so notification providers surface on the unified
44047
- * admin "Integrations" page.
44048
- */
44049
- /**
44050
- * Zentik-derived typed-media enum — the superset across every kind. Each
44051
- * adapter picks what it supports and the degrade engine filters the rest.
44052
- */
44053
- var AttachmentMediaTypeSchema = _enum([
44054
- "image",
44055
- "video",
44056
- "gif",
44057
- "audio",
44058
- "icon"
44059
- ]);
44060
- /**
44061
- * A single attachment. Exactly one of `url` (remote source, most adapters
44062
- * prefer this) or `bytes` (inline source; required for Pushover-style
44063
- * bytes-only kinds) MUST be present — the degrade engine expresses a
44064
- * url→bytes fetch as a `needsFetch` directive the adapter executes.
44065
- */
44066
- var AttachmentSchema = object({
44067
- mediaType: AttachmentMediaTypeSchema,
44068
- url: string().optional(),
44069
- bytes: _instanceof(Uint8Array).optional(),
44070
- mime: string().optional(),
44071
- name: string().optional()
44072
- }).refine((a) => a.url !== void 0 || a.bytes !== void 0, { message: "Attachment requires either `url` or `bytes`" });
44073
- var NotificationFormatSchema = _enum([
44074
- "text",
44075
- "markdown",
44076
- "html"
44077
- ]);
44078
- /**
44079
- * The CLOSED icon vocabulary an action button may use.
44080
- *
44081
- * A closed set, not a free string, and that is the whole point: an arbitrary
44082
- * icon name is one that ntfy renders, zentik silently drops, and nobody
44083
- * notices — the same class of gap as a zone vocabulary nothing produced
44084
- * ([D35](../../../docs/decisions/adr-0035.md)). Every adapter maps this set or
44085
- * declares `actionIcons: false` and the degrade engine strips the field.
44086
- *
44087
- * Named by INTENT, never by glyph. "check" would tie the vocabulary to one
44088
- * renderer's icon set; "acknowledge" survives an adapter that draws it
44089
- * differently.
44090
- *
44091
- * ── A TOKEN IS NOT A WIRE VALUE ─────────────────────────────────────
44092
- *
44093
- * These names are for US. **No adapter may forward one verbatim.** Each maps
44094
- * the whole set onto its own renderer's vocabulary through a
44095
- * `Record<NotificationActionIcon, string>` — a Record, never a lookup with a
44096
- * fallback, so adding a member here fails every adapter's build until someone
44097
- * decides its glyph, which is the only place that decision can be made
44098
- * honestly.
44099
- *
44100
- * This paragraph is the bug. Zentik declared `actionIcons: true` and passed
44101
- * `disarm` straight through; iOS feeds that string to
44102
- * `UNNotificationActionIcon(systemImageName:)`, `disarm` is not an SF Symbol,
44103
- * and every snooze and alarm button arrived BLANK. A pass-through is not a
44104
- * mapping, and "the field is documented" is not "the value renders".
44105
- *
44106
- * Adding a member is TRAIN-BOUND. The enum lives in the published
44107
- * `@camstack/server` closure and the cap seam validates against the HUB's copy,
44108
- * so an addon that emits a token the running hub does not know does not lose an
44109
- * icon — its whole `send` fails Zod validation and the notification never
44110
- * arrives. Never emit a new token from an addon before the train carrying it.
44111
- */
44112
- var NotificationActionIconSchema = _enum([
44113
- "acknowledge",
44114
- "dismiss",
44115
- "silence",
44116
- "snooze",
44117
- "view",
44118
- "play",
44119
- "open",
44120
- "close",
44121
- "lock",
44122
- "unlock",
44123
- "arm",
44124
- "arm-home",
44125
- "arm-away",
44126
- "arm-night",
44127
- "disarm",
44128
- "light",
44129
- "alert",
44130
- "camera"
44131
- ]);
44132
- /** A single tap-through action button. */
44133
- var NotificationActionSchema = object({
44134
- id: string(),
44135
- label: string(),
44136
- url: string().optional(),
44137
- /** Dropped by the degrade engine for a kind with `caps.actionIcons: false`. */
44138
- icon: NotificationActionIconSchema.optional(),
44139
- /**
44140
- * Renders in a warning style where the notifier supports it.
44141
- *
44142
- * A HINT, never a gate. The callback's authority is its token and nothing
44143
- * else — see `notification-center/action-token.ts` for what that does and
44144
- * does not buy.
44145
- */
44146
- destructive: boolean().optional(),
44147
- /**
44148
- * How the tap should REACH the url.
44149
- *
44150
- * `navigate` (absent, and every button authored before this field) opens it:
44151
- * the phone leaves the notification and shows whatever the callback returns.
44152
- * That is right for a button whose answer the operator wants to read.
44153
- *
44154
- * `background` fires it as a POST and stays put. It exists for the buttons
44155
- * whose whole point is not to interrupt — "silence this for 30 minutes" is
44156
- * an answer to the notification, and being thrown into a browser tab to
44157
- * confirm it costs more attention than the notification did. A backend that
44158
- * cannot do a background call renders it as an ordinary link (the adapters
44159
- * fall back rather than dropping the button), so this is a preference, never
44160
- * a requirement.
44161
- */
44162
- mode: _enum(["navigate", "background"]).optional()
44163
- });
44164
- /** A delivery parameter value: what a notifier declared it takes. */
44165
- var DeliveryParamValueSchema = union([
44166
- string(),
44167
- number(),
44168
- boolean()
44169
- ]);
44170
- /**
44171
- * One extra delivery parameter a notifier KIND declares (D938). A rule that
44172
- * addresses a target of that kind may set it; a kind that declares nothing is
44173
- * never handed one. The admin UI derives its control from this descriptor —
44174
- * no editor knows a service by name.
44175
- *
44176
- * Optional fields are `.optional()`, never `.default()` (a Zod default does not
44177
- * run on the addon cap path).
44178
- */
44179
- var DeliveryParamSchema = object({
44180
- key: string().min(1),
44181
- label: string(),
44182
- /** i18n key of the label, resolved by the declaring addon's namespace. */
44183
- labelKey: string().optional(),
44184
- type: _enum([
44185
- "enum",
44186
- "number",
44187
- "boolean",
44188
- "string"
44189
- ]),
44190
- /** `enum` only: the closed set of accepted values. */
44191
- options: array(object({
44192
- value: string(),
44193
- label: string()
44194
- })).optional(),
44195
- /** `number` only. */
44196
- min: number().optional(),
44197
- max: number().optional(),
44198
- /** What the notifier does when the rule sets nothing; shown as the placeholder. */
44199
- default: DeliveryParamValueSchema.optional()
44200
- });
44201
- /**
44202
- * The canonical notification. `body` is the only hard field (Apprise model).
44203
- * `priority` is a 5-level ORDINAL (1=lowest … 3=normal(default) … 5=urgent),
44204
- * NOT a fixed severity enum — each kind declares its own `caps.levels` and
44205
- * the adapter maps this ordinal onto its native level. `level?` is an
44206
- * optional kind-native level id (`emergency`, `silent`, …) that overrides
44207
- * `priority` for that one target.
44208
- */
44209
- var NotificationSchema = object({
44210
- body: string(),
44211
- title: string().optional(),
44212
- format: NotificationFormatSchema.default("text"),
44213
- priority: number().int().min(1).max(5).default(3),
44214
- level: string().optional(),
44215
- attachments: array(AttachmentSchema).optional(),
44216
- clickUrl: string().optional(),
44217
- actions: array(NotificationActionSchema).optional(),
44218
- sound: string().optional(),
44219
- ttl: number().optional(),
44220
- tag: string().optional(),
44221
- deviceId: number().optional(),
44222
- eventId: string().optional(),
44223
- metadata: record(string(), unknown()).optional(),
44224
- /**
44225
- * Values for the parameters the target's kind DECLARES in
44226
- * {@link TargetKindSchema} `deliveryParams`. Already validated against the
44227
- * declaration by the sender; a notifier reads only the keys it declared.
44228
- */
44229
- delivery: record(string(), DeliveryParamValueSchema).optional()
44230
- });
44231
- /** One declared native severity/priority level for a kind. */
44232
- var TargetKindLevelSchema = object({
44233
- id: string(),
44234
- label: string(),
44235
- /** Which canonical priority (1..5) this level maps to. `null` = qualitative-only. */
44236
- ordinal: number().int().min(1).max(5).nullable(),
44237
- flags: object({
44238
- critical: boolean().optional(),
44239
- silent: boolean().optional(),
44240
- noPush: boolean().optional()
44241
- }).optional(),
44242
- /** e.g. Pushover `emergency` requires `retry` / `expire`. */
44243
- requires: array(string()).optional(),
44244
- description: string().optional()
44245
- });
44246
- /** The full capability block consulted before dispatch. */
44247
- var TargetKindCapsSchema = object({
44248
- attachments: object({
44249
- mediaTypes: array(AttachmentMediaTypeSchema),
44250
- mode: _enum([
44251
- "url",
44252
- "bytes",
44253
- "both"
44254
- ]),
44255
- max: number().int().nonnegative(),
44256
- maxBytes: number().int().positive().optional()
44257
- }),
44258
- /** Max action buttons (0 = none). */
44259
- actions: number().int().nonnegative(),
44260
- /**
44261
- * Whether this kind renders a per-action ICON.
44262
- *
44263
- * `.optional()`, deliberately NOT `.default(false)`: a Zod default does not
44264
- * run on the addon cap path — three production failures in one day taught
44265
- * this repo that once. Absent is read as false by the degrade engine, which
44266
- * is the safe direction: an icon that is not rendered costs nothing, an icon
44267
- * assumed and dropped costs the operator's trust in the field.
44268
- */
44269
- actionIcons: boolean().optional(),
44270
- levels: array(TargetKindLevelSchema),
44271
- format: array(NotificationFormatSchema),
44272
- clickUrl: boolean(),
44273
- sound: boolean(),
44274
- ttl: boolean(),
44275
- bodyMaxLen: number().int().positive()
44276
- });
44277
- /**
44278
- * `configSchema` is a `ConfigUISchema` tree passed through to the admin
44279
- * FormBuilder. Stored as `z.unknown()` at the cap seam (mirrors
44280
- * `device-provider.getChildCreationSchema` `CreationSchemaOutputSchema`) —
44281
- * the union is large and not meant for runtime validation here; the exported
44282
- * `TargetKind` type re-tightens `configSchema` to `ConfigUISchema`.
44283
- */
44284
- var ConfigSchemaPassthrough$4 = unknown();
44285
- var TargetKindSchema = object({
44286
- kind: string(),
44287
- label: string(),
44288
- icon: string(),
44289
- /** Stamped by each provider so the concat-fanned catalog stays routable. */
44290
- addonId: string(),
44291
- /**
44292
- * URL of the kind's bundled BRAND icon, served by the providing addon over
44293
- * its own `addon-routes` surface (`/addon/<addonId>/icons/<kind>`). Absent
44294
- * when the addon bundles no icon for that kind — the client then falls back
44295
- * to a neutral glyph rather than rendering the raw `icon` NAME as text.
44296
- *
44297
- * Root-relative on purpose: it resolves against whatever origin serves a web
44298
- * client, and a native client joins it onto its own hub base.
44299
- *
44300
- * DECLARED here deliberately. It used to travel as an undeclared passthrough
44301
- * field that survived only because the runtime cap-router forwards provider
44302
- * output verbatim — so every consumer had to re-declare it by hand to stop
44303
- * its own Zod parse from stripping it, and the whole arrangement would have
44304
- * broken silently the moment output validation was tightened anywhere.
44305
- */
44306
- iconUrl: string().optional(),
44307
- /**
44308
- * Media type of {@link iconUrl} (`image/svg+xml`, `image/png`, …).
44309
- *
44310
- * The server knows this and therefore says it, because the client cannot
44311
- * safely guess: a React-Native client renders SVG and raster through two
44312
- * DIFFERENT components (`react-native-svg` vs `expo-image` — expo-image does
44313
- * not decode SVG on iOS/Android), so without this it silently fell back to a
44314
- * placeholder glyph for every vector icon while the web build looked fine.
44315
- *
44316
- * Absent when {@link iconUrl} is absent, or for a legacy provider that has
44317
- * not been updated — a client that cannot determine the type should prefer
44318
- * its raster path, which is the safe default for an unknown image.
44319
- */
44320
- iconMediaType: string().optional(),
44321
- configSchema: ConfigSchemaPassthrough$4,
44322
- /**
44323
- * The config keys that decide WHERE this kind's secrets are sent (server,
44324
- * base URL, instance, chat, recipient). A save that changes any of them
44325
- * while leaving a secret redacted is refused by the provider (D634), so an
44326
- * editor uses this to ask for the secret on exactly those edits instead of
44327
- * on every edit.
44328
- *
44329
- * Optional: absent from a provider that holds no secret (the Home Assistant
44330
- * kinds) and from a provider older than this field — an editor that finds
44331
- * it absent must assume ANY edit may need the secret.
44332
- */
44333
- destinationKeys: array(string()).optional(),
44334
- supportsDiscovery: boolean(),
44335
- /**
44336
- * Extra delivery parameters a rule may set for a target of this kind (D938).
44337
- * Absent = none; the rule editor then offers nothing.
44338
- */
44339
- deliveryParams: array(DeliveryParamSchema).optional(),
44340
- caps: TargetKindCapsSchema
44341
- });
44342
- /**
44343
- * A persisted target. `config` holds secrets; providers REDACT secret fields
44344
- * (return a presence marker only) when serving `listTargets` — never
44345
- * round-trip a stored secret to the UI.
44346
- */
44347
- var TargetSchema = object({
44348
- id: string(),
44349
- name: string(),
44350
- kind: string(),
44351
- addonId: string(),
44352
- enabled: boolean(),
44353
- config: record(string(), unknown())
44354
- });
44355
- /** A discovery-surfaced candidate (config is partial + non-secret). */
44356
- var DiscoveredTargetSchema = object({
44357
- kind: string(),
44358
- suggestedName: string(),
44359
- config: record(string(), unknown())
44360
- });
44361
- /** The degrade engine's report — what was resolved / dropped / degraded. */
44362
- var RenderedAsSchema = object({
44363
- level: string(),
44364
- format: NotificationFormatSchema,
44365
- attachmentsSent: number().int().nonnegative(),
44366
- actionsSent: number().int().nonnegative(),
44367
- truncated: boolean(),
44368
- dropped: array(string())
44369
- });
44370
- var SendResultSchema = object({
44371
- success: boolean(),
44372
- error: string().optional(),
44373
- renderedAs: RenderedAsSchema.optional()
44374
- });
44375
- /** Same shape as SendResult — kept as a distinct name for the test panel. */
44376
- var TestResultSchema = SendResultSchema;
44377
- /**
44378
- * Pre-save credential probe against a partial (unsaved) target config.
44379
- * `resolvedUrl` never includes secrets (token stripped from path).
44380
- */
44381
- var ProbeTargetConfigResultSchema = object({
44382
- ok: boolean(),
44383
- resolvedUrl: string(),
44384
- httpStatus: number().int().optional(),
44385
- stateInstance: string().optional(),
44386
- error: string().optional()
44387
- });
44388
- method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({ targetId: string() }), TargetSchema, { auth: "admin" }), method(object({
44389
- kind: string(),
44390
- config: record(string(), unknown()).optional(),
44391
- /**
44392
- * When editing / duplicating, the blank password field never round-trips
44393
- * the stored token. Pass the source target id so the provider can restore
44394
- * secrets server-side before dialling (same merge as upsert).
44395
- */
44396
- targetId: string().optional()
44397
- }), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
44398
- kind: string(),
44399
- config: record(string(), unknown()),
44400
- /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
44401
- targetId: string().optional()
44402
- }), ProbeTargetConfigResultSchema, {
44403
- kind: "mutation",
44404
- auth: "admin"
44405
- }), method(object({
44406
- targetId: string(),
44407
- notification: NotificationSchema
44408
- }), SendResultSchema, {
44409
- kind: "mutation",
44410
- auth: "admin"
44411
- }), method(object({
44412
- targetId: string(),
44413
- sample: NotificationSchema.optional()
44414
- }), TestResultSchema, {
44415
- kind: "mutation",
44416
- auth: "admin"
44417
- }), method(object({
44418
- target: TargetSchema,
44419
- /**
44420
- * Duplicate flow: create a NEW target id while copying secrets from an
44421
- * existing row. The UI never holds the stored token; blank / redacted
44422
- * password fields are restored from this source (D634 still applies if
44423
- * destinations diverge without a re-typed secret).
44424
- */
44425
- cloneSecretsFrom: string().optional()
44426
- }), TargetSchema, {
44427
- kind: "mutation",
44428
- auth: "admin"
44429
- }), method(object({ targetId: string() }), _void(), {
44430
- kind: "mutation",
44431
- auth: "admin"
44432
- }), method(object({
44433
- targetId: string(),
44434
- enabled: boolean()
44435
- }), _void(), {
44436
- kind: "mutation",
44437
- auth: "admin"
44438
- });
44439
44467
  _enum([
44440
44468
  "automation",
44441
44469
  "action",
@@ -48313,37 +48341,6 @@ method(object({
48313
48341
  kind: "mutation",
48314
48342
  auth: "admin"
48315
48343
  }), method(object({ blockIds: array(string()) }), object({ states: array(CompositionBlockStateSchema) }), { auth: "admin" });
48316
- var ConnectionTestOutcomeSchema = discriminatedUnion("outcome", [
48317
- object({
48318
- outcome: literal("validated"),
48319
- /** Round-trip of the sign-in, when the provider measured it. */
48320
- latencyMs: number().nonnegative().optional(),
48321
- /** Optional human detail worth showing next to the tick
48322
- * ("3 devices visible on this account"). */
48323
- detail: string().optional()
48324
- }).strict(),
48325
- object({
48326
- outcome: literal("rejected"),
48327
- error: string()
48328
- }).strict(),
48329
- object({
48330
- outcome: literal("inconclusive"),
48331
- error: string()
48332
- }).strict()
48333
- ]);
48334
- var ConnectionTestInputSchema = object({
48335
- /** Candidate integration settings, exactly as the create form collected them. */
48336
- settings: record(string(), unknown()) });
48337
- /**
48338
- * What the provider's test actually DOES, so the UI can say it in words before
48339
- * the operator presses the button ("Signs in to the Dreo cloud"). Purely
48340
- * descriptive — it never changes routing.
48341
- */
48342
- var ConnectionTestDescriptorSchema = object({ label: string() });
48343
- method(ConnectionTestInputSchema, ConnectionTestOutcomeSchema, {
48344
- kind: "mutation",
48345
- auth: "admin"
48346
- }), method(_void(), ConnectionTestDescriptorSchema, { auth: "admin" });
48347
48344
  object({
48348
48345
  /** True when the upstream system considers the entity connected. */
48349
48346
  connected: boolean(),
@@ -54197,10 +54194,9 @@ var AvailableIntegrationTypeSchema = object({
54197
54194
  * locations" checkbox. Provider-declared in the addon manifest. */
54198
54195
  supportsLocationImport: boolean(),
54199
54196
  /**
54200
- * True when this integration DECLARES a pre-creation test (the
54201
- * `connection-test` cap, or a broker whose settings it stores). Drives the
54202
- * Test button: an integration that cannot be tested must say so up front
54203
- * rather than offering a button that always answers the same nonsense.
54197
+ * True when this integration's instances declare a Test (`integration-instances.describe().canTest`,
54198
+ * D1016). An integration that cannot be tested must say so up front rather than offering a
54199
+ * button that always answers the same nonsense.
54204
54200
  */
54205
54201
  canTest: boolean(),
54206
54202
  existingInstances: array(object({
@@ -54254,7 +54250,9 @@ var CreateIntegrationInputSchema = object({
54254
54250
  info: record(string(), unknown()).optional(),
54255
54251
  /** `stableId` of the discovery candidate the form was seeded from (D984). */
54256
54252
  originStableId: string().optional(),
54257
- settings: record(string(), unknown()).optional()
54253
+ settings: record(string(), unknown()).optional(),
54254
+ /** The instance kind being created, for a provider with several; omitted = its only testable kind. */
54255
+ kind: string().min(1).optional()
54258
54256
  });
54259
54257
  var UpdateIntegrationInputSchema = object({
54260
54258
  id: string(),
@@ -54280,13 +54278,7 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
54280
54278
  }), method(object({ id: string() }), DeleteIntegrationResultSchema, {
54281
54279
  kind: "mutation",
54282
54280
  auth: "admin"
54283
- }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" }), method(object({
54284
- addonId: string(),
54285
- settings: record(string(), unknown())
54286
- }), TestConnectionResultSchema, {
54287
- kind: "mutation",
54288
- auth: "admin"
54289
- });
54281
+ }), method(_void(), array(AvailableIntegrationTypeSchema), { auth: "admin" });
54290
54282
  /**
54291
54283
  * `integration-instances` - the connection of an addon that holds ONE or SEVERAL
54292
54284
  * (an account, a relay, a destination) as typed records (D985, D986). The addon's
@@ -62529,13 +62521,6 @@ var AddBrokerInputSchema = object({
62529
62521
  });
62530
62522
  var AddBrokerResultSchema = object({ id: string() });
62531
62523
  var IdInputSchema = object({ id: string() });
62532
- var TestResultSchema$1 = discriminatedUnion("ok", [object({
62533
- ok: literal(true),
62534
- latencyMs: number()
62535
- }), object({
62536
- ok: literal(false),
62537
- error: string()
62538
- })]);
62539
62524
  var StartEmbeddedInputSchema = object({
62540
62525
  port: number().int().min(1).max(65535).default(1883),
62541
62526
  /** Allow anonymous connect (no username/password). Default: false. */
@@ -62572,10 +62557,6 @@ var mqttBrokerCapability = {
62572
62557
  kind: "mutation",
62573
62558
  auth: "admin"
62574
62559
  }),
62575
- testConnection: method(IdInputSchema, TestResultSchema$1, {
62576
- kind: "mutation",
62577
- auth: "admin"
62578
- }),
62579
62560
  startEmbeddedBroker: method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
62580
62561
  kind: "mutation",
62581
62562
  auth: "admin"
@@ -74191,7 +74172,7 @@ var TrainingProviderTypeSchema = object({
74191
74172
  label: string(),
74192
74173
  /** Path under the addon package (e.g. `assets/training-modal/icon.svg`). */
74193
74174
  iconAsset: string(),
74194
- /** Owning `training-provider` addon — collection selector for createEntry. */
74175
+ /** Owning `training-provider` addon. */
74195
74176
  addonId: string(),
74196
74177
  /**
74197
74178
  * Fields for one entry of this type (credentials + defaults). Absent on a
@@ -74236,36 +74217,6 @@ var TrainingProviderDescriptorSchema = object({
74236
74217
  */
74237
74218
  formValues: record(string(), unknown()).optional()
74238
74219
  });
74239
- /**
74240
- * Patch one entry's name, credentials and defaults. Pin with owning `{ addonId }`.
74241
- * An omitted or empty `tokenSecret` means KEEP the stored secret (D787).
74242
- * `{ $secretClear: true }` clears it.
74243
- */
74244
- var TrainingProviderUpdateEntryInputSchema = object({
74245
- entryId: string().min(1),
74246
- name: string().min(1),
74247
- tokenId: string().optional(),
74248
- tokenSecret: union([string(), object({ $secretClear: literal(true) })]).optional(),
74249
- environment: string().optional(),
74250
- gpuType: string().optional(),
74251
- region: string().optional(),
74252
- costCapUsd: number().nonnegative().optional(),
74253
- maxRuntimeSec: number().int().positive().optional(),
74254
- failedDatasetGraceHours: number().nonnegative().optional()
74255
- });
74256
- var TrainingCredentialCheckSchema = object({
74257
- ok: boolean(),
74258
- /** Workspace / account name, when the provider says. */
74259
- account: string().optional(),
74260
- reason: _enum([
74261
- "missing",
74262
- "invalid",
74263
- "forbidden",
74264
- "network",
74265
- "quota"
74266
- ]).optional(),
74267
- detail: string().optional()
74268
- });
74269
74220
  var ProviderJobStateSchema = _enum([
74270
74221
  "pending",
74271
74222
  "starting",
@@ -74313,22 +74264,6 @@ var TrainingLogLineSchema = object({
74313
74264
  text: string()
74314
74265
  });
74315
74266
  method(object({}), array(TrainingProviderDescriptorSchema).readonly(), { auth: "admin" }), method(object({}), array(TrainingProviderTypeSchema).readonly(), { auth: "admin" }), method(object({}), array(TrainingProviderEntrySchema).readonly(), { auth: "admin" }), method(object({
74316
- typeId: string().min(1),
74317
- name: string().min(1)
74318
- }), object({ entryId: string() }), {
74319
- kind: "mutation",
74320
- auth: "admin"
74321
- }), method(object({ entryId: string().min(1) }), object({ ok: literal(true) }), {
74322
- kind: "mutation",
74323
- auth: "admin"
74324
- }), method(TrainingProviderUpdateEntryInputSchema, object({ ok: literal(true) }), {
74325
- kind: "mutation",
74326
- auth: "admin"
74327
- }), method(object({ entryId: string().min(1) }), TrainingCredentialCheckSchema, {
74328
- kind: "mutation",
74329
- auth: "admin",
74330
- timeoutMs: 3e4
74331
- }), method(object({
74332
74267
  entryId: string().min(1),
74333
74268
  gpuType: string(),
74334
74269
  region: string(),
@@ -77714,20 +77649,6 @@ Object.freeze({
77714
77649
  access: "view",
77715
77650
  role: "admin"
77716
77651
  },
77717
- "connectionTest.describeTest": {
77718
- capName: "connection-test",
77719
- capScope: "system",
77720
- addonId: null,
77721
- access: "view",
77722
- role: "admin"
77723
- },
77724
- "connectionTest.testSettings": {
77725
- capName: "connection-test",
77726
- capScope: "system",
77727
- addonId: null,
77728
- access: "create",
77729
- role: "admin"
77730
- },
77731
77652
  "connectivity.getStatus": {
77732
77653
  capName: "connectivity",
77733
77654
  capScope: "device",
@@ -80094,13 +80015,6 @@ Object.freeze({
80094
80015
  access: "view",
80095
80016
  role: "admin"
80096
80017
  },
80097
- "integrations.testConnection": {
80098
- capName: "integrations",
80099
- capScope: "system",
80100
- addonId: null,
80101
- access: "create",
80102
- role: "admin"
80103
- },
80104
80018
  "integrations.update": {
80105
80019
  capName: "integrations",
80106
80020
  capScope: "system",
@@ -81333,13 +81247,6 @@ Object.freeze({
81333
81247
  access: "create",
81334
81248
  role: "admin"
81335
81249
  },
81336
- "mqttBroker.testConnection": {
81337
- capName: "mqtt-broker",
81338
- capScope: "system",
81339
- addonId: null,
81340
- access: "create",
81341
- role: "admin"
81342
- },
81343
81250
  "nativeObjectDetection.getOptions": {
81344
81251
  capName: "native-object-detection",
81345
81252
  capScope: "device",
@@ -86170,20 +86077,6 @@ Object.freeze({
86170
86077
  access: "create",
86171
86078
  role: "admin"
86172
86079
  },
86173
- "trainingProvider.createEntry": {
86174
- capName: "training-provider",
86175
- capScope: "system",
86176
- addonId: null,
86177
- access: "create",
86178
- role: "admin"
86179
- },
86180
- "trainingProvider.deleteEntry": {
86181
- capName: "training-provider",
86182
- capScope: "system",
86183
- addonId: null,
86184
- access: "delete",
86185
- role: "admin"
86186
- },
86187
86080
  "trainingProvider.describe": {
86188
86081
  capName: "training-provider",
86189
86082
  capScope: "system",
@@ -86254,13 +86147,6 @@ Object.freeze({
86254
86147
  access: "create",
86255
86148
  role: "admin"
86256
86149
  },
86257
- "trainingProvider.updateEntry": {
86258
- capName: "training-provider",
86259
- capScope: "system",
86260
- addonId: null,
86261
- access: "create",
86262
- role: "admin"
86263
- },
86264
86150
  "trainingProvider.uploadDataset": {
86265
86151
  capName: "training-provider",
86266
86152
  capScope: "system",
@@ -86268,13 +86154,6 @@ Object.freeze({
86268
86154
  access: "create",
86269
86155
  role: "admin"
86270
86156
  },
86271
- "trainingProvider.validateCredentials": {
86272
- capName: "training-provider",
86273
- capScope: "system",
86274
- addonId: null,
86275
- access: "create",
86276
- role: "admin"
86277
- },
86278
86157
  "turnProvider.getTurnServers": {
86279
86158
  capName: "turn-provider",
86280
86159
  capScope: "system",
@@ -123262,7 +123141,7 @@ var BrokerProbeCache = class {
123262
123141
  }
123263
123142
  if (this.inflight) await this.inflight.catch(() => void 0);
123264
123143
  }
123265
- /** Force a refresh (used by `testConnection` and the page's Refresh button). */
123144
+ /** Force a refresh (used by the page's Refresh button). */
123266
123145
  async probeNow(id) {
123267
123146
  const broker = this.getBrokers().find((b) => b.id === id);
123268
123147
  if (!broker) return void 0;
@@ -124244,8 +124123,8 @@ var MqttAclStore = class MqttAclStore {
124244
124123
  * - The legacy single embedded broker is an onboard `brokers` record (migrated from the flat
124245
124124
  * `embedded*` keys, same id); `startEmbeddedBroker` / `stopEmbeddedBroker` act on that record.
124246
124125
  * - `addBroker` / `removeBroker` mutate the records and the runtime state.
124247
- * - `testConnection` does a one-shot `mqtt.connect` race against a 3s
124248
- * timeout AND refreshes the probe cache for the broker.
124126
+ * - The instance test (`integration-instances.testInstance`) does a one-shot `mqtt.connect`
124127
+ * race against a 3s timeout.
124249
124128
  */
124250
124129
  /** bcrypt cost factor — exported for tests. */
124251
124130
  var MQTT_EMBEDDED_BCRYPT_ROUNDS = 10;
@@ -124381,7 +124260,6 @@ var MqttBrokerAddon = class extends BaseAddon {
124381
124260
  getBrokerConfig: async ({ id }) => this.getBrokerConfig(id),
124382
124261
  addBroker: async (input) => this.addBroker(input),
124383
124262
  removeBroker: async ({ id }) => this.removeBroker(id),
124384
- testConnection: async ({ id }) => this.testConnection(id),
124385
124263
  startEmbeddedBroker: async (input) => this.startEmbeddedBroker(input),
124386
124264
  stopEmbeddedBroker: async ({ id }) => this.stopEmbeddedBroker(id),
124387
124265
  getStatus: async () => ({
@@ -124524,24 +124402,10 @@ var MqttBrokerAddon = class extends BaseAddon {
124524
124402
  return;
124525
124403
  }
124526
124404
  }
124527
- async testConnection(id) {
124528
- let cfg;
124529
- try {
124530
- cfg = this.getBrokerConfig(id);
124531
- } catch (err) {
124532
- return {
124533
- ok: false,
124534
- error: errMsg(err)
124535
- };
124536
- }
124537
- this.probeCache?.probeNow(id).catch(() => void 0);
124538
- return this.probeMqtt(cfg);
124539
- }
124540
124405
  /**
124541
124406
  * One-shot connectivity probe against an MQTT endpoint built from a
124542
- * flat config (`{url, username?, password?}`). Shared by the
124543
- * id-keyed `testConnection` (existing broker) and the pre-creation
124544
- * the instance test (`testInstance`) of a broker not saved yet (the typed values).
124407
+ * flat config (`{url, username?, password?}`), used by the instance test
124408
+ * (`integration-instances.testInstance`), stored or draft.
124545
124409
  * Never throws — every failure resolves to `{ ok: false, error }`.
124546
124410
  */
124547
124411
  async probeMqtt(cfg) {